Commit 93214007 authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat: redesign public podcast and episode pages + remove any information clutter for better ux

- add About podcast page
- use different layout for episode pages
- improve on user feedback with
design
- restructure app theme folders
- update js packages to latest versions
parent e3bd9df0
......@@ -114,6 +114,9 @@ $routes->group('@(:podcastHandle)', function ($routes): void {
],
],
]);
$routes->get('activity', 'EpisodeController::activity/$1/$2', [
'as' => 'episode-activity',
]);
$routes->options('comments', 'ActivityPubController::preflight');
$routes->get('comments', 'EpisodeController::comments/$1/$2', [
'as' => 'episode-comments',
......@@ -128,7 +131,7 @@ $routes->group('@(:podcastHandle)', function ($routes): void {
],
]);
$routes->get('comments/(:uuid)', 'EpisodeCommentController::view/$1/$2/$3', [
'as' => 'comment',
'as' => 'episode-comment',
'application/activity+json' => [
'controller-method' => 'EpisodeController::commentObject/$1/$2',
],
......@@ -140,10 +143,10 @@ $routes->group('@(:podcastHandle)', function ($routes): void {
],
]);
$routes->get('comments/(:uuid)/replies', 'EpisodeCommentController::replies/$1/$2/$3', [
'as' => 'comment-replies',
'as' => 'episode-comment-replies',
]);
$routes->post('comments/(:uuid)/like', 'EpisodeCommentController::attemptLike/$1/$2/$3', [
'as' => 'comment-attempt-like',
'as' => 'episode-comment-attempt-like',
]);
$routes->get('oembed.json', 'EpisodeController::oembedJSON/$1/$2', [
'as' => 'episode-oembed-json',
......
......@@ -104,9 +104,8 @@ class EpisodeCommentController extends BaseController
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
helper('form');
return view('podcast/comment_authenticated', $data);
}
return view('podcast/comment', $data, [
return view('episode/comment', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
......
......@@ -87,10 +87,47 @@ class EpisodeController extends BaseController
if (can_user_interact()) {
helper('form');
return view('podcast/episode_authenticated', $data);
}
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('podcast/episode', $data, [
return view('episode/comments', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function activity(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}
$locale = service('request')
->getLocale();
$cacheName =
"page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_{$locale}" .
(can_user_interact() ? '_authenticated' : '');
if (! ($cachedView = cache($cacheName))) {
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
if (can_user_interact()) {
helper('form');
}
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
......
......@@ -87,7 +87,6 @@ class PodcastController extends BaseController
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
helper('form');
return view('podcast/activity_authenticated', $data);
}
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
......@@ -129,10 +128,9 @@ class PodcastController extends BaseController
'podcast' => $this->podcast,
];
// if user is logged in then send to the authenticated activity view
// // if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
helper('form');
return view('podcast/about_authenticated', $data);
}
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
......@@ -256,11 +254,6 @@ class PodcastController extends BaseController
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
// if user is logged in then send to the authenticated episodes view
if (can_user_interact()) {
return view('podcast/episodes_authenticated', $data);
}
return view('podcast/episodes', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
......
......@@ -88,9 +88,8 @@ class PostController extends FediversePostController
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
helper('form');
return view('podcast/post_authenticated', $data);
}
return view('podcast/post', $data, [
return view('post/post', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
......@@ -242,7 +241,7 @@ class PostController extends FediversePostController
helper('form');
return view('podcast/post_remote_action', $data, [
return view('post/remote_action', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
......
......@@ -8,7 +8,6 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
use App\Entities\Location;
use App\Entities\Person;
use CodeIgniter\I18n\Time;
use CodeIgniter\View\Table;
......@@ -189,11 +188,11 @@ if (! function_exists('episode_numbering')) {
$transKey = '';
$args = [];
if ($episodeNumber !== null) {
$args['episodeNumber'] = $episodeNumber;
$args['episodeNumber'] = sprintf('%02d', $episodeNumber);
}
if ($seasonNumber !== null) {
$args['seasonNumber'] = $seasonNumber;
$args['seasonNumber'] = sprintf('%02d', $seasonNumber);
}
if ($episodeNumber !== null && $seasonNumber !== null) {
......@@ -250,95 +249,6 @@ if (! function_exists('location_link')) {
// ------------------------------------------------------------------------
if (! function_exists('person_list')) {
/**
* Returns list of persons images
*
* @param Person[] $persons
*/
function person_list(array $persons, string $class = ''): string
{
if ($persons === []) {
return '';
}
$personList = "<div class='flex w-full space-x-2 overflow-y-auto {$class}'>";
foreach ($persons as $person) {
$personList .= anchor(
$person->information_url ?? '#',
"<img
src='{$person->image->thumbnail_url}'
alt='{$person->full_name}'
class='object-cover w-12 h-12 rounded-full' />",
[
'class' =>
'flex-shrink-0 focus:outline-none focus:ring focus:ring-inset',
'target' => '_blank',
'rel' => 'noreferrer noopener',
'title' =>
'<strong>' .
$person->full_name .
'</strong>' .
implode(
'',
array_map(function ($role) {
return '<br />' .
lang(
'PersonsTaxonomy.persons.' .
$role->group .
'.roles.' .
$role->role .
'.label',
);
}, $person->roles),
),
'data-toggle' => 'tooltip',
'data-placement' => 'bottom',
],
);
}
return $personList . '</div>';
}
}
// ------------------------------------------------------------------------
if (! function_exists('play_episode_button')) {
/**
* Returns play episode button
*/
function play_episode_button(
string $episodeId,
string $episodeThumbnail,
string $episodeTitle,
string $podcastTitle,
string $source,
string $mediaType,
string $class = ''
): string {
$playLabel = lang('Common.play_episode_button.play');
$playingLabel = lang('Common.play_episode_button.playing');
return <<<CODE_SAMPLE
<play-episode-button
class="{$class}"
id="{$episodeId}"
imageSrc={$episodeThumbnail}
title="{$episodeTitle}"
podcast="{$podcastTitle}"
src="{$source}"
mediaType="{$mediaType}"
playLabel="{$playLabel}"
playingLabel="{$playingLabel}"
></play-episode-button>
CODE_SAMPLE;
}
}
// ------------------------------------------------------------------------
if (! function_exists('audio_player')) {
/**
* Returns audio player
......
......@@ -159,6 +159,34 @@ if (! function_exists('format_duration')) {
}
}
if (! function_exists('format_duration_symbol')) {
/**
* Formats duration in seconds to an hh(h) mm(min) ss(s) string. Doesn't show leading zeros if any.
*
* ⚠️ This uses php's gmdate function so any duration > 86000 seconds (24 hours) will not be formatted properly.
*
* @param int $seconds seconds to format
*/
function format_duration_symbol(int $seconds): string
{
if ($seconds < 60) {
return $seconds . 's';
}
if ($seconds < 3600) {
// < 1 hour: returns MM:SS
return ltrim(gmdate('i\m\i\n s\s', $seconds), '0');
}
if ($seconds < 36000) {
// < 10 hours: returns H:MM:SS
return ltrim(gmdate('h\h i\min s\s', $seconds), '0');
}
return gmdate('h\h i\min s\s', $seconds);
}
}
//--------------------------------------------------------------------
if (! function_exists('podcast_uuid')) {
/**
* Generate UUIDv5 for podcast. For more information, see
......
......@@ -14,7 +14,7 @@ return [
'form' => [
'episode_message_placeholder' => 'Write a comment...',
'reply_to_placeholder' => 'Reply to @{actorUsername}',
'submit' => 'Send!',
'submit' => 'Send',
'submit_reply' => 'Reply',
],
'likes' => '{numberOfLikes, plural,
......
......@@ -22,7 +22,7 @@ return [
'home' => 'Home',
'explicit' => 'Explicit',
'mediumDate' => '{0,date,medium}',
'powered_by' => 'Powered by {castopod}.',
'powered_by' => 'Powered by {castopod}',
'actions' => 'Actions',
'pageInfo' => 'Page {currentPage} out of {pageCount}',
'go_back' => 'Go back',
......@@ -30,4 +30,8 @@ return [
'play' => 'Play',
'playing' => 'Playing',
],
'read_more' => 'Read more',
'read_less' => 'Read less',
'see_more' => 'See more',
'see_less' => 'See less',
];
......@@ -15,10 +15,15 @@ return [
'number_abbr' => 'Ep. {episodeNumber}',
'season_episode' => 'Season {seasonNumber} episode {episodeNumber}',
'season_episode_abbr' => 'S{seasonNumber}E{episodeNumber}',
'persons' => '{personsCount, plural,
one {# person}
other {# persons}
}',
'persons_list' => 'Persons',
'back_to_episodes' => 'Back to episodes of {podcast}',
'comments' => 'Comments',
'activity' => 'Activity',
'description' => 'Description',
'description' => 'Episode description',
'number_of_comments' => '{numberOfComments, plural,
one {# comment}
other {# comments}
......
......@@ -14,7 +14,7 @@ return [
'create' => 'Create podcast',
'import' => 'Import podcast',
'new_episode' => 'New Episode',
'feed' => 'RSS',
'feed' => 'RSS Podcast feed',
'view' => 'View podcast',
'edit' => 'Edit podcast',
'delete' => 'Delete podcast',
......@@ -48,4 +48,9 @@ return [
'funding_links' => 'Funding links for {podcastTitle}',
'find_on' => 'Find {podcastTitle} on',
'listen_on' => 'Listen on',
'persons' => '{personsCount, plural,
one {# person}
other {# persons}
}',
'persons_list' => 'Persons',
];
......@@ -18,7 +18,7 @@ return [
'episode_message_placeholder' => 'Write a message for the episode...',
'episode_url_placeholder' => 'Episode URL',
'reply_to_placeholder' => 'Reply to @{actorUsername}',
'submit' => 'Send!',
'submit' => 'Send',
'submit_reply' => 'Reply',
],
'favourites' => '{numberOfFavourites, plural,
......
......@@ -22,7 +22,7 @@ return [
'home' => 'Accueil',
'explicit' => 'Explicite',
'mediumDate' => '{0,date,medium}',
'powered_by' => 'Propulsé par {castopod}.',
'powered_by' => 'Propulsé par {castopod}',
'actions' => 'Actions',
'pageInfo' => 'Page {currentPage} sur {pageCount}',
'go_back' => 'Retour en arrière',
......@@ -30,4 +30,8 @@ return [
'play' => 'Lire',
'playing' => 'En cours',
],
'read_more' => 'Lire plus',
'read_less' => 'Lire moins',
'see_more' => 'Voir plus',
'see_less' => 'Voir moins',
];
......@@ -15,10 +15,15 @@ return [
'number_abbr' => 'Ep. {episodeNumber}',
'season_episode' => 'Saison {seasonNumber} épisode {episodeNumber}',
'season_episode_abbr' => 'S{seasonNumber}E{episodeNumber}',
'persons' => '{personsCount, plural,
one {# intervenant·e}
other {# intervenant·e·s}
}',
'persons_list' => 'Liste des intervenant·e·s',
'back_to_episodes' => 'Retour aux épisodes de {podcast}',
'comments' => 'Commentaires',
'activity' => 'Activité',
'description' => 'Description',
'description' => 'Description de l’épisode',
'number_of_comments' => '{numberOfComments, plural,
one {# commentaire}
other {# commentaires}
......
......@@ -14,7 +14,7 @@ return [
'create' => 'Créer un podcast',
'import' => 'Importer un podcast',
'new_episode' => 'Créer un épisode',
'feed' => 'RSS',
'feed' => 'Podcast RSS feed',
'view' => 'Voir le podcast',
'edit' => 'Modifier le podcast',
'delete' => 'Supprimer le podcast',
......@@ -48,4 +48,9 @@ return [
'funding_links' => 'Liens de financement pour {podcastTitle}',
'find_on' => 'Trouvez {podcastTitle} sur',
'listen_on' => 'Écoutez sur',
'persons' => '{personsCount, plural,
one {# intervenant·e}
other {# intervenant·e·s}
}',
'persons_list' => 'Liste des intervenant·e·s',
];
......@@ -35,7 +35,12 @@ class CommentObject extends ObjectType
$this->inReplyTo = $comment->reply_to_comment->uri;
}
$this->replies = url_to('comment-replies', $comment->actor->username, $comment->episode->slug, $comment->id);
$this->replies = url_to(
'episode-comment-replies',
$comment->actor->username,
$comment->episode->slug,
$comment->id
);
$this->cc = [$comment->actor->followers_url];
}
......
......@@ -93,7 +93,7 @@ class EpisodeCommentModel extends UuidModel
if ($registerActivity) {
// set post id and uri to construct NoteObject
$comment->id = $newCommentId;
$comment->uri = url_to('comment', $comment->actor->username, $comment->episode->slug, $comment->id);
$comment->uri = url_to('episode-comment', $comment->actor->username, $comment->episode->slug, $comment->id);
$createActivity = new CreateActivity();
$createActivity
......@@ -193,7 +193,7 @@ class EpisodeCommentModel extends UuidModel
$episode = model('EpisodeModel', false)
->find((int) $data['data']['episode_id']);
$data['data']['uri'] = url_to('comment', $actor->username, $episode->slug, $uuid4->toString());
$data['data']['uri'] = url_to('episode-comment', $actor->username, $episode->slug, $uuid4->toString());
}
return $data;
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M3 13h6v-2H3V1.846a.5.5 0 0 1 .741-.438l18.462 10.154a.5.5 0 0 1 0 .876L3.741 22.592A.5.5 0 0 1 3 22.154V13z"/>
</g>
</svg>
import Dropdown from "./modules/Dropdown";
Dropdown();
......@@ -39,7 +39,7 @@ import "./modules/play-episode-button";
const player = html`<div
id="castopod-audio-player"
class="fixed bottom-0 left-0 flex flex-col w-full bg-white border-t sm:flex-row"
class="fixed bottom-0 left-0 flex flex-col w-full bg-white border-t sm:flex-row z-50"
data-episode="-1"
style="display: none;"
>
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment