From 932140077c671f0486a2cd08ceb6126c7ecde87f Mon Sep 17 00:00:00 2001 From: Yassine Doghri <yassine@doghri.fr> Date: Wed, 13 Oct 2021 15:43:40 +0000 Subject: [PATCH] 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 --- app/Config/Routes.php | 9 +- app/Controllers/EpisodeCommentController.php | 3 +- app/Controllers/EpisodeController.php | 41 ++++- app/Controllers/PodcastController.php | 9 +- app/Controllers/PostController.php | 5 +- app/Helpers/components_helper.php | 94 +---------- app/Helpers/misc_helper.php | 28 ++++ app/Language/en/Comment.php | 2 +- app/Language/en/Common.php | 6 +- app/Language/en/Episode.php | 7 +- app/Language/en/Podcast.php | 7 +- app/Language/en/Post.php | 2 +- app/Language/fr/Common.php | 6 +- app/Language/fr/Episode.php | 7 +- app/Language/fr/Podcast.php | 7 +- app/Libraries/CommentObject.php | 7 +- app/Models/EpisodeCommentModel.php | 4 +- app/Resources/icons/send-plane.svg | 6 + app/Resources/js/app.ts | 3 + app/Resources/js/audio-player.ts | 2 +- .../js/modules/play-episode-button.ts | 80 ++++++---- app/Resources/js/podcast.ts | 2 - app/Resources/styles/index.css | 2 + app/Resources/styles/layout.css | 46 +++--- app/Resources/styles/readMore.css | 53 +++++++ app/Resources/styles/seeMore.css | 52 ++++++ app/Views/Components/Button.php | 2 +- app/Views/Components/DropdownMenu.php | 3 + app/Views/Components/Forms/MarkdownEditor.php | 2 +- app/Views/Components/ReadMore.php | 23 +++ app/Views/Components/SeeMore.php | 23 +++ modules/Admin/Language/en/AdminNavigation.php | 1 + modules/Admin/Language/en/Common.php | 2 +- modules/Admin/Language/en/Episode.php | 4 - modules/Admin/Language/en/Podcast.php | 1 - modules/Admin/Language/fr/Common.php | 2 +- modules/Admin/Language/fr/Episode.php | 3 - modules/Admin/Language/fr/Podcast.php | 1 - modules/Fediverse/Entities/Post.php | 8 - package-lock.json | 72 ++++----- package.json | 35 ++-- tailwind.config.js | 3 +- themes/cp_admin/_layout.php | 45 +----- themes/cp_admin/_partials/_nav_header.php | 87 ++++++++++ themes/cp_admin/episode/edit.php | 4 +- themes/cp_admin/page/edit.php | 2 +- themes/cp_admin/podcast/edit.php | 2 +- themes/cp_admin/podcast/import.php | 12 +- themes/cp_app/_admin_navbar.php | 132 +++++++++------ themes/cp_app/_layout.php | 6 +- themes/cp_app/_persons_modal.php | 46 ++++++ themes/cp_app/episode/_layout.php | 143 +++++++++++++++++ themes/cp_app/episode/_partials/card.php | 29 ++++ .../_partials/comment.php | 10 +- .../episode/_partials/comment_actions.php | 42 +++++ .../_partials/comment_actions_from_post.php | 42 +++++ .../_partials/comment_card.php} | 23 ++- .../_partials/comment_reply.php | 2 +- .../_partials/comment_reply_actions.php | 32 ++++ .../_partials/comment_with_replies.php | 29 ++++ .../cp_app/episode/_partials/navigation.php | 22 +++ .../cp_app/episode/_partials/preview_card.php | 27 ++++ themes/cp_app/episode/activity.php | 68 ++++++++ .../cp_app/{podcast => episode}/comment.php | 4 +- themes/cp_app/episode/comments.php | 69 ++++++++ themes/cp_app/home.php | 4 +- themes/cp_app/map.php | 4 +- themes/cp_app/page.php | 4 +- themes/cp_app/podcast/_layout copy.php | 123 -------------- themes/cp_app/podcast/_layout.php | 146 +++++------------ .../cp_app/podcast/_layout_authenticated.php | 115 -------------- themes/cp_app/podcast/_navigation.php | 23 --- .../podcast/_partials/comment_actions.php | 19 --- .../comment_actions_authenticated.php | 22 --- .../_partials/comment_actions_from_post.php | 19 --- ...omment_actions_from_post_authenticated.php | 22 --- .../_partials/comment_authenticated.php | 26 --- .../cp_app/podcast/_partials/comment_card.php | 38 ----- .../_partials/comment_reply_actions.php | 20 --- .../comment_reply_actions_authenticated.php | 11 -- .../_partials/comment_reply_authenticated.php | 19 --- .../_partials/comment_with_replies.php | 10 -- .../comment_with_replies_authenticated.php | 25 --- .../cp_app/podcast/_partials/episode_card.php | 43 ----- .../_partials/episode_preview_card.php | 26 --- .../podcast/_partials/funding_links_modal.php | 39 +++++ themes/cp_app/podcast/_partials/header.php | 58 ------- .../cp_app/podcast/_partials/navigation.php | 24 +++ themes/cp_app/podcast/_partials/post.php | 36 ----- .../cp_app/podcast/_partials/post_actions.php | 36 ----- .../_partials/post_actions_authenticated.php | 92 ----------- .../podcast/_partials/post_with_replies.php | 27 ---- .../post_with_replies_authenticated.php | 27 ---- .../_partials/reblog_authenticated.php | 43 ----- .../podcast/_partials/reply_actions.php | 36 ----- .../_partials/reply_actions_authenticated.php | 92 ----------- .../podcast/_partials/reply_authenticated.php | 24 --- themes/cp_app/podcast/_partials/sidebar.php | 114 ++++++------- themes/cp_app/podcast/about.php | 46 ++++++ themes/cp_app/podcast/activity.php | 55 +++++-- .../cp_app/podcast/activity_authenticated.php | 73 --------- .../cp_app/podcast/comment_authenticated.php | 41 ----- themes/cp_app/podcast/episode.php | 113 ------------- .../cp_app/podcast/episode_authenticated.php | 150 ------------------ themes/cp_app/podcast/episodes.php | 38 ++--- .../cp_app/podcast/episodes_authenticated.php | 63 -------- themes/cp_app/podcast/follow.php | 20 +-- themes/cp_app/podcast/post_authenticated.php | 40 ----- themes/cp_app/post/_partials/actions.php | 124 +++++++++++++++ .../_partials/card.php} | 21 ++- .../post/_partials/post_with_replies.php | 49 ++++++ .../_partials/preview_card.php | 0 .../{podcast => post}/_partials/reblog.php | 10 +- .../{podcast => post}/_partials/reply.php | 8 +- .../cp_app/post/_partials/reply_actions.php | 124 +++++++++++++++ themes/cp_app/{podcast => post}/post.php | 28 ++-- .../remote_action.php} | 2 +- themes/cp_auth/_layout.php | 2 +- themes/cp_install/_layout.php | 2 +- vite.config.ts | 1 + 120 files changed, 1793 insertions(+), 2137 deletions(-) create mode 100644 app/Resources/icons/send-plane.svg create mode 100644 app/Resources/js/app.ts create mode 100644 app/Resources/styles/readMore.css create mode 100644 app/Resources/styles/seeMore.css create mode 100644 app/Views/Components/ReadMore.php create mode 100644 app/Views/Components/SeeMore.php create mode 100644 themes/cp_admin/_partials/_nav_header.php create mode 100644 themes/cp_app/_persons_modal.php create mode 100644 themes/cp_app/episode/_layout.php create mode 100644 themes/cp_app/episode/_partials/card.php rename themes/cp_app/{podcast => episode}/_partials/comment.php (77%) create mode 100644 themes/cp_app/episode/_partials/comment_actions.php create mode 100644 themes/cp_app/episode/_partials/comment_actions_from_post.php rename themes/cp_app/{podcast/_partials/comment_card_authenticated.php => episode/_partials/comment_card.php} (58%) rename themes/cp_app/{podcast => episode}/_partials/comment_reply.php (93%) create mode 100644 themes/cp_app/episode/_partials/comment_reply_actions.php create mode 100644 themes/cp_app/episode/_partials/comment_with_replies.php create mode 100644 themes/cp_app/episode/_partials/navigation.php create mode 100644 themes/cp_app/episode/_partials/preview_card.php create mode 100644 themes/cp_app/episode/activity.php rename themes/cp_app/{podcast => episode}/comment.php (92%) create mode 100644 themes/cp_app/episode/comments.php delete mode 100644 themes/cp_app/podcast/_layout copy.php delete mode 100644 themes/cp_app/podcast/_layout_authenticated.php delete mode 100644 themes/cp_app/podcast/_navigation.php delete mode 100644 themes/cp_app/podcast/_partials/comment_actions.php delete mode 100644 themes/cp_app/podcast/_partials/comment_actions_authenticated.php delete mode 100644 themes/cp_app/podcast/_partials/comment_actions_from_post.php delete mode 100644 themes/cp_app/podcast/_partials/comment_actions_from_post_authenticated.php delete mode 100644 themes/cp_app/podcast/_partials/comment_authenticated.php delete mode 100644 themes/cp_app/podcast/_partials/comment_card.php delete mode 100644 themes/cp_app/podcast/_partials/comment_reply_actions.php delete mode 100644 themes/cp_app/podcast/_partials/comment_reply_actions_authenticated.php delete mode 100644 themes/cp_app/podcast/_partials/comment_reply_authenticated.php delete mode 100644 themes/cp_app/podcast/_partials/comment_with_replies.php delete mode 100644 themes/cp_app/podcast/_partials/comment_with_replies_authenticated.php delete mode 100644 themes/cp_app/podcast/_partials/episode_card.php delete mode 100644 themes/cp_app/podcast/_partials/episode_preview_card.php create mode 100644 themes/cp_app/podcast/_partials/funding_links_modal.php delete mode 100644 themes/cp_app/podcast/_partials/header.php create mode 100644 themes/cp_app/podcast/_partials/navigation.php delete mode 100644 themes/cp_app/podcast/_partials/post.php delete mode 100644 themes/cp_app/podcast/_partials/post_actions.php delete mode 100644 themes/cp_app/podcast/_partials/post_actions_authenticated.php delete mode 100644 themes/cp_app/podcast/_partials/post_with_replies.php delete mode 100644 themes/cp_app/podcast/_partials/post_with_replies_authenticated.php delete mode 100644 themes/cp_app/podcast/_partials/reblog_authenticated.php delete mode 100644 themes/cp_app/podcast/_partials/reply_actions.php delete mode 100644 themes/cp_app/podcast/_partials/reply_actions_authenticated.php delete mode 100644 themes/cp_app/podcast/_partials/reply_authenticated.php delete mode 100644 themes/cp_app/podcast/activity_authenticated.php delete mode 100644 themes/cp_app/podcast/comment_authenticated.php delete mode 100644 themes/cp_app/podcast/episode.php delete mode 100644 themes/cp_app/podcast/episode_authenticated.php delete mode 100644 themes/cp_app/podcast/episodes_authenticated.php delete mode 100644 themes/cp_app/podcast/post_authenticated.php create mode 100644 themes/cp_app/post/_partials/actions.php rename themes/cp_app/{podcast/_partials/post_authenticated.php => post/_partials/card.php} (63%) create mode 100644 themes/cp_app/post/_partials/post_with_replies.php rename themes/cp_app/{podcast => post}/_partials/preview_card.php (100%) rename themes/cp_app/{podcast => post}/_partials/reblog.php (83%) rename themes/cp_app/{podcast => post}/_partials/reply.php (77%) create mode 100644 themes/cp_app/post/_partials/reply_actions.php rename themes/cp_app/{podcast => post}/post.php (65%) rename themes/cp_app/{podcast/post_remote_action.php => post/remote_action.php} (97%) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 04b8ebfdc5..9234895e8f 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -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', diff --git a/app/Controllers/EpisodeCommentController.php b/app/Controllers/EpisodeCommentController.php index 4dcef86198..f64c1db304 100644 --- a/app/Controllers/EpisodeCommentController.php +++ b/app/Controllers/EpisodeCommentController.php @@ -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, ]); diff --git a/app/Controllers/EpisodeController.php b/app/Controllers/EpisodeController.php index 9aacdc6848..d5ddd9f780 100644 --- a/app/Controllers/EpisodeController.php +++ b/app/Controllers/EpisodeController.php @@ -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, diff --git a/app/Controllers/PodcastController.php b/app/Controllers/PodcastController.php index 21ce0ed081..f330064d43 100644 --- a/app/Controllers/PodcastController.php +++ b/app/Controllers/PodcastController.php @@ -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 diff --git a/app/Controllers/PostController.php b/app/Controllers/PostController.php index 184825adca..69837feb0d 100644 --- a/app/Controllers/PostController.php +++ b/app/Controllers/PostController.php @@ -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, ]); diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php index 657eca2d32..6a85dbf301 100644 --- a/app/Helpers/components_helper.php +++ b/app/Helpers/components_helper.php @@ -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 diff --git a/app/Helpers/misc_helper.php b/app/Helpers/misc_helper.php index 21a7ae5c05..f988de62aa 100644 --- a/app/Helpers/misc_helper.php +++ b/app/Helpers/misc_helper.php @@ -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 diff --git a/app/Language/en/Comment.php b/app/Language/en/Comment.php index aff97d5ee6..741f78961c 100644 --- a/app/Language/en/Comment.php +++ b/app/Language/en/Comment.php @@ -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, diff --git a/app/Language/en/Common.php b/app/Language/en/Common.php index debed34830..3284040fa9 100644 --- a/app/Language/en/Common.php +++ b/app/Language/en/Common.php @@ -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', ]; diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php index 565b7ba1f6..74c32b3f92 100644 --- a/app/Language/en/Episode.php +++ b/app/Language/en/Episode.php @@ -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} diff --git a/app/Language/en/Podcast.php b/app/Language/en/Podcast.php index 0c311ed832..9896a00a46 100644 --- a/app/Language/en/Podcast.php +++ b/app/Language/en/Podcast.php @@ -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', ]; diff --git a/app/Language/en/Post.php b/app/Language/en/Post.php index a60ef30fec..dc348d91bd 100644 --- a/app/Language/en/Post.php +++ b/app/Language/en/Post.php @@ -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, diff --git a/app/Language/fr/Common.php b/app/Language/fr/Common.php index 6fc0c30d38..c5694c3974 100644 --- a/app/Language/fr/Common.php +++ b/app/Language/fr/Common.php @@ -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', ]; diff --git a/app/Language/fr/Episode.php b/app/Language/fr/Episode.php index c6f565c855..ee8a1e4bd2 100644 --- a/app/Language/fr/Episode.php +++ b/app/Language/fr/Episode.php @@ -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} diff --git a/app/Language/fr/Podcast.php b/app/Language/fr/Podcast.php index d332a93362..d9d0a35d2b 100644 --- a/app/Language/fr/Podcast.php +++ b/app/Language/fr/Podcast.php @@ -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', ]; diff --git a/app/Libraries/CommentObject.php b/app/Libraries/CommentObject.php index 7a4b7ce2d5..e7fcac7ffb 100644 --- a/app/Libraries/CommentObject.php +++ b/app/Libraries/CommentObject.php @@ -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]; } diff --git a/app/Models/EpisodeCommentModel.php b/app/Models/EpisodeCommentModel.php index cd950438e7..3440802513 100644 --- a/app/Models/EpisodeCommentModel.php +++ b/app/Models/EpisodeCommentModel.php @@ -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; diff --git a/app/Resources/icons/send-plane.svg b/app/Resources/icons/send-plane.svg new file mode 100644 index 0000000000..74ffbcf9bd --- /dev/null +++ b/app/Resources/icons/send-plane.svg @@ -0,0 +1,6 @@ +<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> diff --git a/app/Resources/js/app.ts b/app/Resources/js/app.ts new file mode 100644 index 0000000000..f1093cda32 --- /dev/null +++ b/app/Resources/js/app.ts @@ -0,0 +1,3 @@ +import Dropdown from "./modules/Dropdown"; + +Dropdown(); diff --git a/app/Resources/js/audio-player.ts b/app/Resources/js/audio-player.ts index 0a120376b4..41a17ab8d6 100644 --- a/app/Resources/js/audio-player.ts +++ b/app/Resources/js/audio-player.ts @@ -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;" > diff --git a/app/Resources/js/modules/play-episode-button.ts b/app/Resources/js/modules/play-episode-button.ts index 7ec0d41705..d8c51fd490 100644 --- a/app/Resources/js/modules/play-episode-button.ts +++ b/app/Resources/js/modules/play-episode-button.ts @@ -188,35 +188,43 @@ export class PlayEpisodeButton extends LitElement { static styles = css` button { - background-color: #ffffff; + background-color: #009486; cursor: pointer; display: inline-flex; align-items: center; - padding: 0.25rem 0.5rem; + padding: 0.5rem 0.5rem; font-size: 0.875rem; line-height: 1.25rem; - font-weight: 600; - border-width: 2px; - border-style: solid; + border: 2px solid transparent; border-radius: 9999px; - border-color: rgba(207, 247, 243, 1); box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); } button:hover { - border-color: #009486; - background-color: #ebf8f8; + background-color: #00564a; } button:focus { - background-color: #ebf8f8; + box-shadow: 0 0 0 2px #e7f9e4, 0 0 0 calc(2px + 2px) #009486; + } + + button.playing { + background-color: #f2faf9; + border: 2px solid #009486; + } + + button.playing:hover { + background-color: #e7f9e4; + } + + button.playing svg { + color: #009486; } svg { font-size: 1.5rem; - margin-right: 0.25rem; - color: #009486; + color: #ffffff; } @keyframes spin { @@ -231,33 +239,37 @@ export class PlayEpisodeButton extends LitElement { `; render(): TemplateResult<1> { - return html`<button @click="${this.isPlaying ? this.pause : this.play}"> + return html`<button + class="${this.isPlaying ? "playing" : ""}" + @click="${this.isPlaying ? this.pause : this.play}" + title="${this.isPlaying ? this.playingLabel : this.playLabel}" + > ${this.isPlaying ? html`<svg - class="animate-spin" - viewBox="0 0 24 24" - fill="currentColor" - width="1em" - height="1em" - > - <g> - <path fill="none" d="M0 0h24v24H0z" /> - <path - d="M13 9.17A3 3 0 1 0 15 12V2.458c4.057 1.274 7 5.064 7 9.542 0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2c.337 0 .671.017 1 .05v7.12z" - /> - </g></svg - >${this.playingLabel}` - : html`<svg - viewBox="0 0 24 24" - fill="currentColor" - width="1em" - height="1em" - > + class="animate-spin" + viewBox="0 0 24 24" + fill="currentColor" + width="1em" + height="1em" + > + <g> <path fill="none" d="M0 0h24v24H0z" /> <path - d="M7.752 5.439l10.508 6.13a.5.5 0 0 1 0 .863l-10.508 6.13A.5.5 0 0 1 7 18.128V5.871a.5.5 0 0 1 .752-.432z" - /></svg - >${this.playLabel}`} + d="M13 9.17A3 3 0 1 0 15 12V2.458c4.057 1.274 7 5.064 7 9.542 0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2c.337 0 .671.017 1 .05v7.12z" + /> + </g> + </svg>` + : html`<svg + viewBox="0 0 24 24" + fill="currentColor" + width="1em" + height="1em" + > + <path fill="none" d="M0 0h24v24H0z" /> + <path + d="M7.752 5.439l10.508 6.13a.5.5 0 0 1 0 .863l-10.508 6.13A.5.5 0 0 1 7 18.128V5.871a.5.5 0 0 1 .752-.432z" + /> + </svg>`} </button>`; } } diff --git a/app/Resources/js/podcast.ts b/app/Resources/js/podcast.ts index f3036ec297..c5250eed80 100644 --- a/app/Resources/js/podcast.ts +++ b/app/Resources/js/podcast.ts @@ -1,10 +1,8 @@ import "@github/time-elements"; -import Dropdown from "./modules/Dropdown"; import Time from "./modules/Time"; import Toggler from "./modules/Toggler"; import Tooltip from "./modules/Tooltip"; -Dropdown(); Time(); Toggler(); Tooltip(); diff --git a/app/Resources/styles/index.css b/app/Resources/styles/index.css index ec4dd9494b..1ce07be46f 100644 --- a/app/Resources/styles/index.css +++ b/app/Resources/styles/index.css @@ -12,3 +12,5 @@ @import "./radioToggler.css"; @import "./formInputTabs.css"; @import "./stickyHeader.css"; +@import "./readMore.css"; +@import "./seeMore.css"; diff --git a/app/Resources/styles/layout.css b/app/Resources/styles/layout.css index 61b88e4c28..75875d4e23 100644 --- a/app/Resources/styles/layout.css +++ b/app/Resources/styles/layout.css @@ -1,34 +1,36 @@ /* Admin layout */ -.holy-grail-grid { - @apply grid min-h-screen overflow-y-auto; - grid-template: auto 1fr auto / auto 1fr; +@layer base { + .holy-grail-grid { + @apply grid min-h-screen overflow-y-auto; + grid-template: 40px 1fr / 300px 1fr; - & .holy-grail__header { - @apply h-10 col-start-1 col-end-4 row-start-1 row-end-2; - } - - & .holy-grail__sidebar { - @apply col-start-1 col-end-2 row-start-2 row-end-4; - - width: 300px; - max-height: calc(100vh - 2.5rem); - } + & .holy-grail__header { + @apply h-10 col-start-1 col-end-4 row-start-1 row-end-2; + } - & .holy-grail__main { - @apply col-start-1 col-end-3 row-start-2 row-end-4; - } + & .holy-grail__sidebar { + @apply col-start-1 col-end-2 row-start-2 row-end-4; - & .holy-grail__footer { - @apply col-start-1 col-end-3 row-start-3 row-end-4; - } + width: 300px; + max-height: calc(100vh - 2.5rem); + } - @screen md { & .holy-grail__main { - @apply col-start-2; + @apply col-start-1 col-end-3 row-start-2 row-end-3; } & .holy-grail__footer { - @apply col-start-2; + @apply col-start-1 col-end-3 row-start-3 row-end-3; + } + + @screen md { + & .holy-grail__main { + @apply col-start-2; + } + + & .holy-grail__footer { + @apply col-start-2; + } } } } diff --git a/app/Resources/styles/readMore.css b/app/Resources/styles/readMore.css new file mode 100644 index 0000000000..d83e0bee08 --- /dev/null +++ b/app/Resources/styles/readMore.css @@ -0,0 +1,53 @@ +/* +Read more component (basic unstyled component) +-- see https://codepen.io/yassinedoghri/pen/QWpwoxp for more info and possible caveats +*/ + +@layer components { + .read-more { + @apply flex flex-col items-start; + /* You can update this variable directly in the html with the style attribute: style="--line-clamp: 3" */ + --line-clamp: 3; + } + + .read-more__text { + @apply overflow-hidden; + display: -webkit-box; + -webkit-line-clamp: var(--line-clamp); + -webkit-box-orient: vertical; + } + + .read-more__checkbox { + @apply absolute overflow-hidden whitespace-nowrap; + clip: rect(0 0 0 0); + clip-path: inset(100%); + height: 1px; + width: 1px; + } + + .read-more__checkbox ~ .read-more__label { + @apply text-xs font-semibold underline cursor-pointer; + } + + /* Don't forget focus and hover styles for accessibility! */ + .read-more__checkbox:focus ~ .read-more__label { + @apply ring; + } + + .read-more__checkbox:hover ~ .read-more__label { + @apply no-underline; + } + + .read-more__checkbox ~ .read-more__label::before { + content: attr(data-read-more); + } + + .read-more__checkbox:checked ~ .read-more__label::before { + content: attr(data-read-less); + } + + .read-more__checkbox:checked ~ .read-more__text { + --line-clamp: none; + -webkit-line-clamp: var(--line-clamp); + } +} diff --git a/app/Resources/styles/seeMore.css b/app/Resources/styles/seeMore.css new file mode 100644 index 0000000000..2a0d78477d --- /dev/null +++ b/app/Resources/styles/seeMore.css @@ -0,0 +1,52 @@ +@layer components { + .see-more { + @apply flex flex-col items-start; + } + + .see-more__content { + @apply relative overflow-hidden; + height: 5rem; + } + + .see-more_content-fade { + @apply absolute bottom-0 left-0 w-full h-full pointer-events-none; + background-image: linear-gradient(to bottom, transparent, #00564a); + } + + .see-more__checkbox { + @apply absolute overflow-hidden whitespace-nowrap; + clip: rect(0 0 0 0); + clip-path: inset(100%); + height: 1px; + width: 1px; + } + + .see-more__checkbox ~ .see-more__label { + @apply text-xs font-semibold underline cursor-pointer; + } + + /* Don't forget focus and hover styles for accessibility! */ + .see-more__checkbox:focus ~ .see-more__label { + @apply ring; + } + + .see-more__checkbox:hover ~ .see-more__label { + @apply no-underline; + } + + .see-more__checkbox ~ .see-more__label::before { + content: attr(data-see-more); + } + + .see-more__checkbox:checked ~ .see-more__label::before { + content: attr(data-see-less); + } + + .see-more__checkbox:checked ~ .see-more__content { + @apply h-auto; + } + + .see-more__checkbox:checked ~ .see-more__content .see-more_content-fade { + @apply bg-none; + } +} diff --git a/app/Views/Components/Button.php b/app/Views/Components/Button.php index dc2080fccc..c7eb510719 100644 --- a/app/Views/Components/Button.php +++ b/app/Views/Components/Button.php @@ -48,7 +48,7 @@ class Button extends Component ]; $basePaddings = [ - 'small' => 'px-2 py-1', + 'small' => 'px-3 py-1', 'base' => 'px-3 py-2', 'large' => 'px-4 py-2', ]; diff --git a/app/Views/Components/DropdownMenu.php b/app/Views/Components/DropdownMenu.php index d806220ec1..62a0997c49 100644 --- a/app/Views/Components/DropdownMenu.php +++ b/app/Views/Components/DropdownMenu.php @@ -32,6 +32,9 @@ class DropdownMenu extends Component 'class' => 'px-4 py-1 hover:bg-gray-100' . (array_key_exists('class', $item) ? ' ' . $item['class'] : ''), ]); break; + case 'html': + $menuItems .= html_entity_decode($item['content']); + break; case 'separator': $menuItems .= '<hr class="my-2 border border-gray-100">'; break; diff --git a/app/Views/Components/Forms/MarkdownEditor.php b/app/Views/Components/Forms/MarkdownEditor.php index a42bc041f2..5307742d2e 100644 --- a/app/Views/Components/Forms/MarkdownEditor.php +++ b/app/Views/Components/Forms/MarkdownEditor.php @@ -13,7 +13,7 @@ class MarkdownEditor extends FormComponent $this->attributes['class'] = 'border-none outline-none focus:border-none focus:outline-none focus:ring-0 w-full h-full'; $this->attributes['rows'] = 6; - $textarea = form_textarea($this->attributes, old($this->name, $this->value, false)); + $textarea = form_textarea($this->attributes, old($this->name, html_entity_decode($this->value), null)); $icons = [ 'heading' => icon('heading'), 'bold' => icon('bold'), diff --git a/app/Views/Components/ReadMore.php b/app/Views/Components/ReadMore.php new file mode 100644 index 0000000000..bf9685e896 --- /dev/null +++ b/app/Views/Components/ReadMore.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +namespace App\Views\Components; + +use ViewComponents\Component; + +class ReadMore extends Component +{ + public function render(): string + { + $readMoreLabel = lang('Common.read_more'); + $readLessLabel = lang('Common.read_less'); + return <<<HTML + <div class="read-more {$this->class}" style="--line-clamp: 3"> + <input id="read-more-checkbox_{$this->id}" type="checkbox" class="read-more__checkbox" aria-hidden="true"> + <div class="mb-2 read-more__text">{$this->slot}</div> + <label for="read-more-checkbox_{$this->id}" class="read-more__label" data-read-more="{$readMoreLabel}" data-read-less="{$readLessLabel}" aria-hidden="true"></label> + </div> + HTML; + } +} diff --git a/app/Views/Components/SeeMore.php b/app/Views/Components/SeeMore.php new file mode 100644 index 0000000000..22481be711 --- /dev/null +++ b/app/Views/Components/SeeMore.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +namespace App\Views\Components; + +use ViewComponents\Component; + +class SeeMore extends Component +{ + public function render(): string + { + $seeMoreLabel = lang('Common.see_more'); + $seeLessLabel = lang('Common.see_less'); + return <<<HTML + <div class="see-more" style="--line-clamp: 3"> + <input id="see-more-checkbox" type="checkbox" class="see-more__checkbox" aria-hidden="true"> + <div class="mb-2 see-more__content {$this->class}"><div class="see-more_content-fade"></div>{$this->slot}</div> + <label for="see-more-checkbox" class="see-more__label" data-see-more="{$seeMoreLabel}" data-see-less="{$seeLessLabel}" aria-hidden="true"></label> + </div> + HTML; + } +} diff --git a/modules/Admin/Language/en/AdminNavigation.php b/modules/Admin/Language/en/AdminNavigation.php index 92a5f59b67..68cbefb3eb 100644 --- a/modules/Admin/Language/en/AdminNavigation.php +++ b/modules/Admin/Language/en/AdminNavigation.php @@ -10,6 +10,7 @@ declare(strict_types=1); return [ 'go_to_website' => 'View site', + 'go_to_admin' => 'Go to admin', 'dashboard' => 'Dashboard', 'admin' => 'Home', 'podcasts' => 'Podcasts', diff --git a/modules/Admin/Language/en/Common.php b/modules/Admin/Language/en/Common.php index f05e5d6a7c..c5048cb9f3 100644 --- a/modules/Admin/Language/en/Common.php +++ b/modules/Admin/Language/en/Common.php @@ -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', diff --git a/modules/Admin/Language/en/Episode.php b/modules/Admin/Language/en/Episode.php index 1761148314..990297f8b9 100644 --- a/modules/Admin/Language/en/Episode.php +++ b/modules/Admin/Language/en/Episode.php @@ -15,10 +15,6 @@ return [ 'number_abbr' => 'Ep. {episodeNumber}', 'season_episode' => 'Season {seasonNumber} episode {episodeNumber}', 'season_episode_abbr' => 'S{seasonNumber}E{episodeNumber}', - 'back_to_episodes' => 'Back to episodes of {podcast}', - 'comments' => 'Comments', - 'activity' => 'Activity', - 'description' => 'Description', 'number_of_comments' => '{numberOfComments, plural, one {# comment} other {# comments} diff --git a/modules/Admin/Language/en/Podcast.php b/modules/Admin/Language/en/Podcast.php index 2a107c52d1..d2aaa10126 100644 --- a/modules/Admin/Language/en/Podcast.php +++ b/modules/Admin/Language/en/Podcast.php @@ -14,7 +14,6 @@ return [ 'create' => 'Create podcast', 'import' => 'Import podcast', 'new_episode' => 'New Episode', - 'feed' => 'RSS', 'view' => 'View podcast', 'edit' => 'Edit podcast', 'delete' => 'Delete podcast', diff --git a/modules/Admin/Language/fr/Common.php b/modules/Admin/Language/fr/Common.php index 2ab2171c70..e02f1b02f0 100644 --- a/modules/Admin/Language/fr/Common.php +++ b/modules/Admin/Language/fr/Common.php @@ -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', diff --git a/modules/Admin/Language/fr/Episode.php b/modules/Admin/Language/fr/Episode.php index bafa62319b..857b6754ff 100644 --- a/modules/Admin/Language/fr/Episode.php +++ b/modules/Admin/Language/fr/Episode.php @@ -16,9 +16,6 @@ return [ 'season_episode' => 'Saison {seasonNumber} épisode {episodeNumber}', 'season_episode_abbr' => 'S{seasonNumber}E{episodeNumber}', 'back_to_episodes' => 'Retour aux épisodes de {podcast}', - 'comments' => 'Commentaires', - 'activity' => 'Activité', - 'description' => 'Description', 'number_of_comments' => '{numberOfComments, plural, one {# commentaire} other {# commentaires} diff --git a/modules/Admin/Language/fr/Podcast.php b/modules/Admin/Language/fr/Podcast.php index a4919c6d14..0d332120f9 100644 --- a/modules/Admin/Language/fr/Podcast.php +++ b/modules/Admin/Language/fr/Podcast.php @@ -14,7 +14,6 @@ return [ 'create' => 'Créer un podcast', 'import' => 'Importer un podcast', 'new_episode' => 'Créer un épisode', - 'feed' => 'RSS', 'view' => 'Voir le podcast', 'edit' => 'Modifier le podcast', 'delete' => 'Supprimer le podcast', diff --git a/modules/Fediverse/Entities/Post.php b/modules/Fediverse/Entities/Post.php index c663964607..6177aa371b 100644 --- a/modules/Fediverse/Entities/Post.php +++ b/modules/Fediverse/Entities/Post.php @@ -31,7 +31,6 @@ use RuntimeException; * @property Time $published_at * @property Time $created_at * - * @property bool $has_preview_card * @property PreviewCard|null $preview_card * * @property bool $has_replies @@ -48,8 +47,6 @@ class Post extends UuidEntity protected ?PreviewCard $preview_card = null; - protected bool $has_preview_card = false; - /** * @var Post[]|null */ @@ -119,11 +116,6 @@ class Post extends UuidEntity return $this->preview_card; } - public function getHasPreviewCard(): bool - { - return $this->getPreviewCard() !== null; - } - /** * @return Post[] */ diff --git a/package-lock.json b/package-lock.json index 76d0a2a41d..9094019298 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,34 +15,35 @@ "@codemirror/commands": "^0.19.5", "@codemirror/lang-xml": "^0.19.2", "@codemirror/state": "^0.19.2", - "@codemirror/view": "^0.19.8", + "@codemirror/view": "^0.19.9", "@github/clipboard-copy-element": "^1.1.2", - "@github/markdown-toolbar-element": "^1.5.1", + "@github/markdown-toolbar-element": "^1.5.3", "@github/time-elements": "^3.1.2", "@popperjs/core": "^2.10.2", - "@vime/core": "^5.0.33", + "@vime/core": "^5.0.34", "choices.js": "^9.0.1", "flatpickr": "^4.6.9", "leaflet": "^1.7.1", "leaflet.markercluster": "^1.5.1", - "lit": "^2.0.0", - "marked": "^3.0.4", + "lit": "^2.0.2", + "marked": "^3.0.7", "xml-formatter": "^2.4.1" }, "devDependencies": { - "@commitlint/cli": "^13.2.0", + "@commitlint/cli": "^13.2.1", "@commitlint/config-conventional": "^13.2.0", "@semantic-release/changelog": "^6.0.0", "@semantic-release/exec": "^6.0.1", "@semantic-release/git": "^10.0.0", "@semantic-release/gitlab": "^7.0.3", + "@tailwindcss/aspect-ratio": "^0.3.0", "@tailwindcss/forms": "^0.3.4", - "@tailwindcss/line-clamp": "^0.2.1", + "@tailwindcss/line-clamp": "^0.2.2", "@tailwindcss/typography": "^0.4.1", "@types/leaflet": "^1.7.5", "@types/marked": "^3.0.1", - "@typescript-eslint/eslint-plugin": "^4.32.0", - "@typescript-eslint/parser": "^4.32.0", + "@typescript-eslint/eslint-plugin": "^4.33.0", + "@typescript-eslint/parser": "^4.33.0", "cross-env": "^7.0.3", "cssnano": "^5.0.8", "cz-conventional-changelog": "^3.3.0", @@ -51,7 +52,7 @@ "eslint-plugin-prettier": "^4.0.0", "husky": "^7.0.2", "is-ci": "^3.0.0", - "lint-staged": "^11.1.2", + "lint-staged": "^11.2.3", "postcss-import": "^14.0.2", "postcss-preset-env": "^6.7.0", "postcss-reporter": "^7.0.3", @@ -64,7 +65,7 @@ "tailwindcss": "^2.2.16", "tailwindcss-scroll-snap": "^1.1.0", "typescript": "^4.4.3", - "vite": "^2.6.1" + "vite": "^2.6.5" } }, "node_modules/@amcharts/amcharts4": { @@ -2098,6 +2099,15 @@ "node": ">=10" } }, + "node_modules/@tailwindcss/aspect-ratio": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.3.0.tgz", + "integrity": "sha512-DMgWskNJR6FNPLbQ8Xoq/PKV/9DfNKh5dvKB+SM8x7lVl4+pnxlZ3Ns4+yGmurA/ze708HrnCG1tXk85HolJmw==", + "dev": true, + "peerDependencies": { + "tailwindcss": ">=2.0.0 || >=3.0.0-alpha.1" + } + }, "node_modules/@tailwindcss/forms": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.3.4.tgz", @@ -5947,20 +5957,6 @@ "resolved": "https://registry.npmjs.org/fscreen/-/fscreen-1.2.0.tgz", "integrity": "sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==" }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -8205,12 +8201,6 @@ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "dev": true }, - "node_modules/nanocolors": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.12.tgz", - "integrity": "sha512-SFNdALvzW+rVlzqexid6epYdt8H9Zol7xDoQarioEFcFN0JHo4CYNztAxmtfgGTVRCmFlEOqqhBpoFGKqSAMug==", - "dev": true - }, "node_modules/nanoid": { "version": "3.1.30", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", @@ -18922,6 +18912,13 @@ "defer-to-connect": "^2.0.0" } }, + "@tailwindcss/aspect-ratio": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.3.0.tgz", + "integrity": "sha512-DMgWskNJR6FNPLbQ8Xoq/PKV/9DfNKh5dvKB+SM8x7lVl4+pnxlZ3Ns4+yGmurA/ze708HrnCG1tXk85HolJmw==", + "dev": true, + "requires": {} + }, "@tailwindcss/forms": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.3.4.tgz", @@ -21927,13 +21924,6 @@ "resolved": "https://registry.npmjs.org/fscreen/-/fscreen-1.2.0.tgz", "integrity": "sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==" }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -23646,12 +23636,6 @@ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "dev": true }, - "nanocolors": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.12.tgz", - "integrity": "sha512-SFNdALvzW+rVlzqexid6epYdt8H9Zol7xDoQarioEFcFN0JHo4CYNztAxmtfgGTVRCmFlEOqqhBpoFGKqSAMug==", - "dev": true - }, "nanoid": { "version": "3.1.30", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", diff --git a/package.json b/package.json index e9ba0a513a..9af9cb361f 100644 --- a/package.json +++ b/package.json @@ -27,62 +27,63 @@ "prepare": "is-ci || husky install" }, "dependencies": { - "@amcharts/amcharts4-geodata": "^4.1.22", "@amcharts/amcharts4": "^4.10.22", + "@amcharts/amcharts4-geodata": "^4.1.22", "@codemirror/basic-setup": "^0.19.0", "@codemirror/commands": "^0.19.5", "@codemirror/lang-xml": "^0.19.2", "@codemirror/state": "^0.19.2", - "@codemirror/view": "^0.19.8", + "@codemirror/view": "^0.19.9", "@github/clipboard-copy-element": "^1.1.2", - "@github/markdown-toolbar-element": "^1.5.1", + "@github/markdown-toolbar-element": "^1.5.3", "@github/time-elements": "^3.1.2", "@popperjs/core": "^2.10.2", - "@vime/core": "^5.0.33", + "@vime/core": "^5.0.34", "choices.js": "^9.0.1", "flatpickr": "^4.6.9", - "leaflet.markercluster": "^1.5.1", "leaflet": "^1.7.1", - "lit": "^2.0.0", - "marked": "^3.0.4", + "leaflet.markercluster": "^1.5.1", + "lit": "^2.0.2", + "marked": "^3.0.7", "xml-formatter": "^2.4.1" }, "devDependencies": { - "@commitlint/cli": "^13.2.0", + "@commitlint/cli": "^13.2.1", "@commitlint/config-conventional": "^13.2.0", "@semantic-release/changelog": "^6.0.0", "@semantic-release/exec": "^6.0.1", "@semantic-release/git": "^10.0.0", "@semantic-release/gitlab": "^7.0.3", + "@tailwindcss/aspect-ratio": "^0.3.0", "@tailwindcss/forms": "^0.3.4", - "@tailwindcss/line-clamp": "^0.2.1", + "@tailwindcss/line-clamp": "^0.2.2", "@tailwindcss/typography": "^0.4.1", "@types/leaflet": "^1.7.5", "@types/marked": "^3.0.1", - "@typescript-eslint/eslint-plugin": "^4.32.0", - "@typescript-eslint/parser": "^4.32.0", + "@typescript-eslint/eslint-plugin": "^4.33.0", + "@typescript-eslint/parser": "^4.33.0", "cross-env": "^7.0.3", "cssnano": "^5.0.8", "cz-conventional-changelog": "^3.3.0", + "eslint": "^7.32.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", - "eslint": "^7.32.0", "husky": "^7.0.2", "is-ci": "^3.0.0", - "lint-staged": "^11.1.2", + "lint-staged": "^11.2.3", "postcss-import": "^14.0.2", "postcss-preset-env": "^6.7.0", "postcss-reporter": "^7.0.3", - "prettier-plugin-organize-imports": "^2.3.4", "prettier": "2.4.1", + "prettier-plugin-organize-imports": "^2.3.4", "semantic-release": "^18.0.0", - "stylelint-config-standard": "^22.0.0", "stylelint": "^13.13.1", + "stylelint-config-standard": "^22.0.0", "svgo": "^2.7.0", - "tailwindcss-scroll-snap": "^1.1.0", "tailwindcss": "^2.2.16", + "tailwindcss-scroll-snap": "^1.1.0", "typescript": "^4.4.3", - "vite": "^2.6.1" + "vite": "^2.6.5" }, "lint-staged": { "*.{js,ts,css,md,json}": "prettier --write", diff --git a/tailwind.config.js b/tailwind.config.js index ea55a83ccc..dcb69af660 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -16,7 +16,6 @@ module.exports = { fontFamily: { sans: ["Inter", ...defaultTheme.fontFamily.sans], display: ["Kumbh Sans", ...defaultTheme.fontFamily.sans], - // body: ["Inter", ...defaultTheme.fontFamily.sans], }, colors: { pine: { @@ -49,6 +48,7 @@ module.exports = { 112: "28rem", }, gridTemplateColumns: { + podcastLayout: "1fr minmax(auto, 768px) 1fr", podcasts: "repeat(auto-fill, minmax(14rem, 1fr))", }, zIndex: { @@ -67,6 +67,7 @@ module.exports = { require("@tailwindcss/forms"), require("@tailwindcss/typography"), require("@tailwindcss/line-clamp"), + require("@tailwindcss/aspect-ratio"), require("tailwindcss-scroll-snap"), ], }; diff --git a/themes/cp_admin/_layout.php b/themes/cp_admin/_layout.php index 5269b9e9c0..c2f212247d 100644 --- a/themes/cp_admin/_layout.php +++ b/themes/cp_admin/_layout.php @@ -19,48 +19,7 @@ <body class="relative bg-pine-50 holy-grail-grid"> <div id="sidebar-backdrop" role="button" tabIndex="0" aria-label="Close" class="fixed z-50 hidden w-full h-full bg-gray-900 bg-opacity-50 md:hidden"></div> - <header class="sticky top-0 z-50 flex items-center justify-between h-10 text-white border-b holy-grail__header bg-pine-800 border-pine-900"> - <div class="inline-flex items-center h-full"> - <a href="<?= route_to( - 'admin', - ) ?>" class="inline-flex items-center h-full px-2 border-r border-pine-900"> - <?= (isset($podcast) ? icon('arrow-left', 'mr-2') : '') . svg('castopod-logo-base', 'h-6') ?> - </a> - <a href="<?= route_to( - 'home', - ) ?>" class="inline-flex items-center h-full px-6 text-sm font-semibold outline-none hover:underline focus:ring"> - <?= lang('AdminNavigation.go_to_website') ?> - <?= icon('external-link', 'ml-1 opacity-60') ?> - </a> - </div> - <button - type="button" - class="inline-flex items-center h-full px-6 mt-auto font-semibold outline-none focus:ring" - id="my-account-dropdown" - data-dropdown="button" - data-dropdown-target="my-account-dropdown-menu" - aria-haspopup="true" - aria-expanded="false"><?= icon('account-circle', 'text-2xl opacity-60 mr-2') . user()->username . icon('caret-down', 'ml-auto text-2xl') ?></button> - <DropdownMenu id="my-account-dropdown-menu" labelledby="my-account-dropdown" items="<?= esc(json_encode([ - [ - 'type' => 'link', - 'title' => lang('AdminNavigation.account.my-account'), - 'uri' => route_to('my-account'), - ], - [ - 'type' => 'link', - 'title' => lang('AdminNavigation.account.change-password'), - 'uri' => route_to('change-password'), - ], - [ - 'type' => 'separator', - ], - [ - 'type' => 'link', - 'title' => lang('AdminNavigation.account.logout'), - 'uri' => route_to('logout'), - ], ])) ?>" /> - </header> + <?= $this->include('_partials/_nav_header') ?> <aside id="admin-sidebar" class="sticky z-50 flex flex-col text-white transition duration-200 ease-in-out transform -translate-x-full border-r top-10 border-pine-900 bg-pine-800 holy-grail__sidebar md:translate-x-0"> <?php if (isset($podcast) && isset($episode)): ?> <?= $this->include('episode/_sidebar') ?> @@ -72,7 +31,7 @@ <footer class="px-2 py-2 mx-auto text-xs text-right"> <?= lang('Common.powered_by', [ 'castopod' => - '<a class="underline hover:no-underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod</a> ' . + '<a class="inline-flex font-semibold hover:underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod' . icon('social/castopod', 'ml-1 text-lg') . '</a> ' . CP_VERSION, ]) ?> </footer> diff --git a/themes/cp_admin/_partials/_nav_header.php b/themes/cp_admin/_partials/_nav_header.php new file mode 100644 index 0000000000..1fcac6ae1f --- /dev/null +++ b/themes/cp_admin/_partials/_nav_header.php @@ -0,0 +1,87 @@ +<header class="sticky top-0 z-50 flex items-center justify-between h-10 text-white border-b holy-grail__header bg-pine-800 border-pine-900"> + <div class="inline-flex items-center h-full"> + <a href="<?= route_to( + 'admin', +) ?>" class="inline-flex items-center h-full px-2 border-r border-pine-900"> + <?= (isset($podcast) ? icon('arrow-left', 'mr-2') : '') . svg('castopod-logo-base', 'h-6') ?> + </a> + <a href="<?= route_to( + 'home', +) ?>" class="inline-flex items-center h-full px-6 text-sm font-semibold outline-none hover:underline focus:ring"> + <?= lang('AdminNavigation.go_to_website') ?> + <?= icon('external-link', 'ml-1 opacity-60') ?> + </a> + </div> + <button + type="button" + class="inline-flex items-center h-full px-3 text-sm font-semibold outline-none focus:ring gap-x-2" + id="my-account-dropdown" + data-dropdown="button" + data-dropdown-target="my-account-dropdown-menu" + aria-haspopup="true" + aria-expanded="false"><div class="relative mr-1"> + <?= icon('account-circle', 'text-3xl opacity-60') ?> + <?= user() + ->podcasts === [] ? '' : '<img src="' . interact_as_actor()->avatar_image_url . '" class="absolute bottom-0 w-4 h-4 rounded-full -right-1" />' ?> + </div> + <?= user()->username ?> + <?= icon('caret-down', 'ml-auto text-2xl') ?></button> + <?php + $interactButtons = ''; + foreach (user()->podcasts as $userPodcast) { + $checkMark = interact_as_actor_id() === $userPodcast->actor_id ? icon('check', 'ml-2 bg-pine-800 text-white rounded-full') : ''; + + $interactButtons .= <<<CODE_SAMPLE + <button class="inline-flex items-center w-full px-4 py-1 hover:bg-gray-100" id="interact-as-actor-{$userPodcast->id}" name="actor_id" value="{$userPodcast->actor_id}"> + <span class="inline-flex items-center flex-1 text-sm"><img src="{$userPodcast->image->thumbnail_url}" class="w-6 h-6 mr-2 rounded-full" />{$userPodcast->title}{$checkMark}</span> + </button> + CODE_SAMPLE; + } + + $interactAsText = lang('Admin.choose_interact'); + $route = route_to('interact-as-actor'); + $csrfField = csrf_field(); + + $menuItems = [ + [ + 'type' => 'link', + 'title' => lang('AdminNavigation.account.my-account'), + 'uri' => route_to('my-account'), + ], + [ + 'type' => 'link', + 'title' => lang('AdminNavigation.account.change-password'), + 'uri' => route_to('change-password'), + ], + [ + 'type' => 'separator', + ], + [ + 'type' => 'link', + 'title' => lang('AdminNavigation.account.logout'), + 'uri' => route_to('logout'), + ], + ]; + + if (user()->podcasts !== []) { + $menuItems = array_merge([ + [ + 'type' => 'html', + 'content' => esc(<<<CODE_SAMPLE + <nav class="flex flex-col py-2 text-black whitespace-no-wrap"> + <span class="px-4 mb-2 text-xs font-semibold tracking-wider text-gray-500 uppercase">{$interactAsText}</span> + <form action="{$route}" method="POST" class="flex flex-col"> + {$csrfField} + {$interactButtons} + </form> + </nav> + CODE_SAMPLE), + ], + [ + 'type' => 'separator', + ], + ], $menuItems); + } + ?> + <DropdownMenu id="my-account-dropdown-menu" labelledby="my-account-dropdown" items="<?= esc(json_encode($menuItems)) ?>" /> +</header> \ No newline at end of file diff --git a/themes/cp_admin/episode/edit.php b/themes/cp_admin/episode/edit.php index e82e2aee2c..88022107f9 100644 --- a/themes/cp_admin/episode/edit.php +++ b/themes/cp_admin/episode/edit.php @@ -120,7 +120,7 @@ as="MarkdownEditor" name="description" label="<?= lang('Episode.form.description') ?>" - value="<?= $episode->description_markdown ?>" + value="<?= htmlspecialchars($episode->description_markdown) ?>" required="true" /> <Forms.Field @@ -128,7 +128,7 @@ name="description_footer" label="<?= lang('Episode.form.description_footer') ?>" hint="<?= lang('Episode.form.description_footer_hint') ?>" - value="<?= $podcast->episode_description_footer_markdown ?? '' ?>" /> + value="<?= htmlspecialchars($podcast->episode_description_footer_markdown) ?? '' ?>" /> </Forms.Section> diff --git a/themes/cp_admin/page/edit.php b/themes/cp_admin/page/edit.php index a8957e20b9..fc9a8254b9 100644 --- a/themes/cp_admin/page/edit.php +++ b/themes/cp_admin/page/edit.php @@ -35,7 +35,7 @@ as="MarkdownEditor" name="content" label="<?= lang('Page.form.content') ?>" - value="<?= $page->content_markdown ?>" + value="<?= htmlspecialchars($page->content_markdown) ?>" required="true" rows="20" /> diff --git a/themes/cp_admin/podcast/edit.php b/themes/cp_admin/podcast/edit.php index 8809d9b5d8..71245af11b 100644 --- a/themes/cp_admin/podcast/edit.php +++ b/themes/cp_admin/podcast/edit.php @@ -44,7 +44,7 @@ as="MarkdownEditor" name="description" label="<?= lang('Podcast.form.description') ?>" - value="<?= $podcast->title ?>" + value="<?= htmlspecialchars($podcast->description) ?>" required="true" /> <fieldset> diff --git a/themes/cp_admin/podcast/import.php b/themes/cp_admin/podcast/import.php index 3b96be3e64..d01838c35c 100644 --- a/themes/cp_admin/podcast/import.php +++ b/themes/cp_admin/podcast/import.php @@ -63,16 +63,16 @@ <fieldset class="flex flex-col mb-4"> <legend><?= lang('PodcastImport.slug_field') ?></legend> - <Forms.Radio id="title" name="slug_field" isChecked="true"><title></span></Forms.Radio> - <Forms.Radio id="link" name="slug_field"><link></span></Forms.Radio> + <Forms.Radio value="title" name="slug_field" isChecked="true"><title></span></Forms.Radio> + <Forms.Radio value="link" name="slug_field"><link></span></Forms.Radio> </fieldset> <fieldset class="flex flex-col mb-4"> <legend><?= lang('PodcastImport.description_field') ?></legend> - <Forms.Radio id="description" name="description_field" isChecked="true"><description></Forms.Radio> - <Forms.Radio id="summary" name="description_field"><itunes:summary></Forms.Radio> - <Forms.Radio id="subtitle_summary" name="description_field"><itunes:subtitle> + <itunes:summary></Forms.Radio> - <Forms.Radio id="content" name="description_field"><content:encoded></Forms.Radio> + <Forms.Radio value="description" name="description_field" isChecked="true"><description></Forms.Radio> + <Forms.Radio value="summary" name="description_field"><itunes:summary></Forms.Radio> + <Forms.Radio value="subtitle_summary" name="description_field"><itunes:subtitle> + <itunes:summary></Forms.Radio> + <Forms.Radio value="content" name="description_field"><content:encoded></Forms.Radio> </fieldset> <Forms.Checkbox name="force_renumber" hint="<?= lang('PodcastImport.force_renumber_hint') ?>"><?= lang('PodcastImport.force_renumber') ?></Forms.Checkbox> diff --git a/themes/cp_app/_admin_navbar.php b/themes/cp_app/_admin_navbar.php index aedbab7bb1..08a928defb 100644 --- a/themes/cp_app/_admin_navbar.php +++ b/themes/cp_app/_admin_navbar.php @@ -1,46 +1,86 @@ -<div class="sticky top-0 left-0 z-50 flex items-center justify-between w-full h-12 px-4 text-white border-b shadow bg-pine-800 border-pine-900"> - <?= anchor( - route_to('admin'), - 'castopod' . svg('castopod-logo-base', 'h-5 ml-1'), - [ - 'class' => - 'text-2xl inline-flex items-baseline font-bold font-display', - ], -) ?> - <?php if (user()->podcasts !== []): ?> - <button type="button" class="inline-flex items-center px-6 py-2 mt-auto font-semibold outline-none focus:ring" id="interact-as-dropdown" data-dropdown="button" data-dropdown-target="interact-as-dropdown-menu" aria-haspopup="true" aria-expanded="false"> - <img src="<?= interact_as_actor() - ->avatar_image_url ?>" class="w-8 h-8 mr-2 rounded-full" /> - <?= '@' . interact_as_actor()->username ?> - <?= icon('caret-down', 'ml-auto') ?> - </button> - <nav id="interact-as-dropdown-menu" class="absolute z-50 flex flex-col py-2 text-black whitespace-no-wrap bg-white border rounded shadow" aria-labelledby="my-accountDropdown" data-dropdown="menu" data-dropdown-placement="bottom-end"> - <span class="px-4 text-xs tracking-wider text-gray-700 uppercase"><?= lang( - 'Admin.choose_interact', - ) ?></span> - <form action="<?= route_to( - 'interact-as-actor', - ) ?>" method="POST" class="flex flex-col"> - <?= csrf_field() ?> - <?php foreach (user()->podcasts as $userPodcast): ?> - <button class="inline-flex items-center w-full px-4 py-1 hover:bg-gray-100" id="<?= "interact-as-actor-{$userPodcast->id}" ?>" name="actor_id" value="<?= $userPodcast->actor_id ?>"> - <span class="inline-flex items-center flex-1"> - <img src="<?= $userPodcast->image - ->thumbnail_url ?>" class="w-8 h-8 mr-2 rounded-full" /><?= $userPodcast->title ?> - <?php if ( - interact_as_actor() - ->id === - $userPodcast->actor_id - ): ?> - </span> - <?= icon( - 'check', - 'ml-4 bg-pine-800 text-white rounded-full', - ) ?> - <?php endif; ?> - </button> - <?php endforeach; ?> - </form> - </nav> - <?php endif; ?> - </div> \ No newline at end of file +<div class="sticky top-0 left-0 z-50 flex items-center justify-between w-full h-10 text-white border-b shadow bg-pine-800 border-pine-900"> + <div class="inline-flex items-center h-full"> + <a href="<?= route_to('home') ?>" class="inline-flex items-center h-full px-2 border-r border-pine-900"> + <?= svg('castopod-logo-base', 'h-6') ?> + </a> + <a href="<?= route_to('admin', ) ?>" class="inline-flex items-center h-full px-6 text-sm font-semibold outline-none hover:underline focus:ring"> + <?= lang('AdminNavigation.go_to_admin') ?> + <?= icon('external-link', 'ml-1 opacity-60') ?> + </a> + </div> + + <div class="inline-flex items-center h-full"> + <button + type="button" + class="inline-flex items-center h-full px-3 text-sm font-semibold outline-none focus:ring gap-x-2" + id="my-account-dropdown" + data-dropdown="button" + data-dropdown-target="my-account-dropdown-menu" + aria-haspopup="true" + aria-expanded="false"><div class="relative mr-1"> + <?= icon('account-circle', 'text-3xl opacity-60') ?> + <?= user() + ->podcasts === [] ? '' : '<img src="' . interact_as_actor()->avatar_image_url . '" class="absolute bottom-0 w-4 h-4 rounded-full -right-1" />' ?> + </div> + <?= user()->username ?> + <?= icon('caret-down', 'ml-auto text-2xl') ?></button> + <?php + $interactButtons = ''; + foreach (user()->podcasts as $userPodcast) { + $checkMark = interact_as_actor_id() === $userPodcast->actor_id ? icon('check', 'ml-2 bg-pine-800 text-white rounded-full') : ''; + + $interactButtons .= <<<CODE_SAMPLE + <button class="inline-flex items-center w-full px-4 py-1 hover:bg-gray-100" id="interact-as-actor-{$userPodcast->id}" name="actor_id" value="{$userPodcast->actor_id}"> + <span class="inline-flex items-center flex-1 text-sm"><img src="{$userPodcast->image->thumbnail_url}" class="w-6 h-6 mr-2 rounded-full" />{$userPodcast->title}{$checkMark}</span> + </button> + CODE_SAMPLE; + } + + $interactAsText = lang('Admin.choose_interact'); + $route = route_to('interact-as-actor'); + $csrfField = csrf_field(); + + $menuItems = [ + [ + 'type' => 'link', + 'title' => lang('AdminNavigation.account.my-account'), + 'uri' => route_to('my-account'), + ], + [ + 'type' => 'link', + 'title' => lang('AdminNavigation.account.change-password'), + 'uri' => route_to('change-password'), + ], + [ + 'type' => 'separator', + ], + [ + 'type' => 'link', + 'title' => lang('AdminNavigation.account.logout'), + 'uri' => route_to('logout'), + ], + ]; + + if (user()->podcasts !== []) { + $menuItems = array_merge([ + [ + 'type' => 'html', + 'content' => esc(<<<CODE_SAMPLE + <nav class="flex flex-col py-2 text-black whitespace-no-wrap"> + <span class="px-4 mb-2 text-xs font-semibold tracking-wider text-gray-500 uppercase">{$interactAsText}</span> + <form action="{$route}" method="POST" class="flex flex-col"> + {$csrfField} + {$interactButtons} + </form> + </nav> + CODE_SAMPLE), + ], + [ + 'type' => 'separator', + ], + ], $menuItems); + } + ?> + <DropdownMenu id="my-account-dropdown-menu" labelledby="my-account-dropdown" items="<?= esc(json_encode($menuItems)) ?>" /> + </div> +</div> \ No newline at end of file diff --git a/themes/cp_app/_layout.php b/themes/cp_app/_layout.php index bc8377c60c..ea88500863 100644 --- a/themes/cp_app/_layout.php +++ b/themes/cp_app/_layout.php @@ -12,11 +12,13 @@ <?= service('vite') ->asset('styles/index.css', 'css') ?> + <?= service('vite') + ->asset('js/app.ts', 'js') ?> <?= service('vite') ->asset('js/audio-player.ts', 'js') ?> </head> -<body class="flex flex-col min-h-screen mx-auto bg-gray-100"> +<body class="flex flex-col min-h-screen mx-auto bg-pine-50"> <?php if (service('authentication')->check()): ?> <?= $this->include('_admin_navbar') ?> <?php endif; ?> @@ -28,7 +30,7 @@ 'arrow-left', 'mr-2', ) . lang('Page.back_to_home') ?></a> - <h1 class="text-3xl font-semibold"><?= isset($page) + <h1 class="text-3xl font-bold font-display"><?= isset($page) ? $page->title : 'Castopod' ?></h1> </div> diff --git a/themes/cp_app/_persons_modal.php b/themes/cp_app/_persons_modal.php new file mode 100644 index 0000000000..973ddaba49 --- /dev/null +++ b/themes/cp_app/_persons_modal.php @@ -0,0 +1,46 @@ +<div id="persons-list" class="fixed top-0 left-0 z-50 flex items-center justify-center hidden w-screen h-screen"> + <div + class="absolute w-full h-full bg-pine-800/75" + role="button" + data-toggle="persons-list" + data-toggle-class="hidden" + aria-label="<?= lang('Common.close') ?>"></div> + <div class="z-10 w-full max-w-xl bg-white rounded-lg shadow-2xl"> + <div class="flex justify-between px-4 py-2 border-b"> + <h3 class="self-center text-lg"><?= $title ?></h3> + <button + data-toggle="persons-list" + data-toggle-class="hidden" + aria-label="<?= lang('Common.close') ?>" + class="self-start p-1 text-2xl"><?= icon('close') ?></button> + </div> + <div class="flex flex-col items-start p-4 gap-y-4"> + <?php foreach ($persons as $person): ?> + <div class="flex gap-x-2"> + <img src="<?= $person->image->thumbnail_url ?>" alt="<?= $person->full_name ?>" class="object-cover w-10 h-10 rounded-full" /> + <div class="flex flex-col"> + <h4 class="text-sm font-semibold hover:underline focus:ring"> + <?php if ($person->information_url): ?> + <a href="<?= $person->information_url ?>" target="_blank" rel="noopener noreferrer"><?= $person->full_name ?></a> + <?php else: ?> + <?= $person->full_name ?> + <?php endif; ?> + </h4> + <p class="text-xs text-gray-500"><?= implode( + ', ', + array_map(function ($role) { + return lang( + 'PersonsTaxonomy.persons.' . + $role->group . + '.roles.' . + $role->role . + '.label', + ); + }, $person->roles), +) ?></p> + </div> + </div> + <?php endforeach; ?> + </div> + </div> +</div> \ No newline at end of file diff --git a/themes/cp_app/episode/_layout.php b/themes/cp_app/episode/_layout.php new file mode 100644 index 0000000000..fd6a08e36d --- /dev/null +++ b/themes/cp_app/episode/_layout.php @@ -0,0 +1,143 @@ +<?= helper('page') ?> + +<!DOCTYPE html> +<html lang="<?= service('request') + ->getLocale() ?>"> + +<head> + <meta charset="UTF-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"/> + <link rel="shortcut icon" type="image/png" href="/favicon.ico" /> + + <?= $this->renderSection('meta-tags') ?> + <?php if ($podcast->payment_pointer): ?> + <meta name="monetization" content="<?= $podcast->payment_pointer ?>" /> + <?php endif; ?> + + <?= service('vite') + ->asset('styles/index.css', 'css') ?> + <?= service('vite') + ->asset('js/app.ts', 'js') ?> + <?= service('vite') + ->asset('js/podcast.ts', 'js') ?> + <?= service('vite') + ->asset('js/audio-player.ts', 'js') ?> +</head> + +<body class="grid items-start mx-auto grid-cols-podcastLayout bg-pine-50"> + <?php if (can_user_interact()): ?> + <div class="col-span-full"> + <?= $this->include('_admin_navbar') ?> + </div> + <?php endif; ?> + + <nav class="flex items-center justify-between h-10 col-start-2 px-2 text-white bg-pine-800"> + <a href="<?= route_to('podcast-episodes', $podcast->handle) ?>" class="inline-flex items-center" title="<?= lang('Episode.back_to_episodes', [ + 'podcast' => $podcast->title, + ]) ?>"> + <?= icon('arrow-left', 'mr-2 text-lg') ?> + <div class="inline-flex items-center gap-x-2"> + <img class="w-8 h-8 rounded-full" src="<?= $episode->podcast->image->thumbnail_url ?>" alt="<?= $episode->podcast->title ?>" /> + <div class="flex flex-col"> + <span class="text-sm font-semibold leading-none"><?= $episode->podcast->title ?></span> + <span class="text-xs"><?= lang('Podcast.followers', [ + 'numberOfFollowers' => $podcast->actor->followers_count, + ]) ?></span> + </div> + </div> + </a> + <div class="inline-flex items-center self-end h-full gap-x-2"> + <?php if (in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true)): ?> + <IconButton glyph="heart" variant="accent" data-toggle="funding-links" data-toggle-class="hidden"><?= lang('Podcast.sponsor') . lang('Podcast.sponsor_title') ?></IconButton> + <?php endif; ?> + <?= anchor_popup( + route_to('follow', $podcast->handle), + icon( + 'social/castopod', + 'mr-2 text-xl text-rose-200 group-hover:text-rose-50', + ) . lang('Podcast.follow'), + [ + 'width' => 420, + 'height' => 620, + 'class' => + 'group inline-flex items-center px-2 py-1 text-xs tracking-wider font-semibold text-white uppercase rounded-full shadow focus:outline-none focus:ring bg-rose-600', + ], + ) ?> + </div> + </nav> + <header class="relative z-50 flex flex-col col-start-2 px-8 pt-8 pb-4 overflow-hidden bg-pine-500 gap-y-4"> + <div class="absolute top-0 left-0 w-full h-full bg-center bg-no-repeat bg-cover blur-lg mix-blend-luminosity" style="background-image: url('<?= $episode->podcast->image->thumbnail_url ?>');"></div> + <div class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-pine-800 to-transparent"></div> + <div class="z-10 flex flex-col items-start gap-y-2 gap-x-4 sm:flex-row"> + <img src="<?= $episode->image->medium_url ?>" alt="<?= $episode->title ?>" loading="lazy" class="rounded-md h-36" /> + <div class="flex flex-col items-start text-white"> + <?= episode_numbering($episode->number, $episode->season_number, 'bg-pine-50 text-sm leading-none font-semibold text-gray-700 border !no-underline border-pine-100', true) ?> + <h1 class="inline-flex items-baseline max-w-md mt-2 text-2xl font-bold leading-none sm:text-3xl font-display line-clamp-2"><?= $episode->title ?></h1> + <div class="flex items-center mt-4 gap-x-8"> + <?php if ($episode->persons !== []): ?> + <button class="flex items-center text-xs font-semibold gap-x-2 hover:underline" data-toggle="persons-list" data-toggle-class="hidden"> + <div class="inline-flex flex-row-reverse"> + <?php $i = 0; ?> + <?php foreach ($episode->persons as $person): ?> + <img src="<?= $person->image->thumbnail_url ?>" alt="<?= $person->full_name ?>" class="object-cover w-8 h-8 -ml-5 border-2 rounded-full border-pine-100 last:ml-0" /> + <?php $i++; if ($i === 3) { + break; + }?> + <?php endforeach; ?> + </div> + <?= lang('Episode.persons', [ + 'personsCount' => count($episode->persons), + ]) ?> + </button> + <?php endif; ?> + <?php if ($episode->location): ?> + <?= location_link($episode->location, 'text-xs font-semibold p-2') ?> + <?php endif; ?> + </div> + </div> + </div> + <div class="z-10 inline-flex items-center text-white gap-x-4"> + <play-episode-button + id="<?= $episode->id ?>" + imageSrc="<?= $episode->image->thumbnail_url ?>" + title="<?= $episode->title ?>" + podcast="<?= $episode->podcast->title ?>" + src="<?= $episode->audio_file_web_url ?>" + mediaType="<?= $episode->audio_file_mimetype ?>" + playLabel="<?= lang('Common.play_episode_button.play') ?>" + playingLabel="<?= lang('Common.play_episode_button.playing') ?>"></play-episode-button> + <div class="text-xs"> + <?= relative_time($episode->published_at) ?> + <span class="mx-1">•</span> + <time datetime="PT<?= $episode->audio_file_duration ?>S"> + <?= format_duration_symbol($episode->audio_file_duration) ?> + </time> + </div> + </div> + </header> + <div class="col-start-2 px-8 py-4 text-white bg-pine-800"> + <h2 class="text-xs font-bold tracking-wider uppercase whitespace-pre-line font-display"><?= lang('Episode.description') ?></h2> + <?php if (substr_count($episode->description_markdown, "\n") > 3 || strlen($episode->description) > 250): ?> + <SeeMore class="max-w-xl prose-sm text-white whitespace-pre-line"><?= $episode->getDescriptionHtml('-+Website+-') ?></SeeMore> + <?php else: ?> + <div class="max-w-xl prose-sm text-white whitespace-pre-line"><?= $episode->getDescriptionHtml('-+Website+-') ?></div> + <?php endif; ?> + </div> + <?= $this->include('episode/_partials/navigation') ?> + <div class="relative grid items-start grid-cols-3 col-start-2 pt-6 pb-12 gap-x-6"> + <main class="col-span-full sm:col-span-2"> + <?= $this->renderSection('content') ?> + </main> + <?= $this->include('podcast/_partials/sidebar') ?> + </div> + <?= view('_persons_modal', [ + 'title' => lang('Episode.persons_list', [ + 'episodeTitle' => $episode->title, + ]), + 'persons' => $episode-> +persons, + ]) ?> + <?php if (in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true)): ?> + <?= $this->include('podcast/_partials/funding_links_modal') ?> + <?php endif; ?> +</body> diff --git a/themes/cp_app/episode/_partials/card.php b/themes/cp_app/episode/_partials/card.php new file mode 100644 index 0000000000..90f26cfc48 --- /dev/null +++ b/themes/cp_app/episode/_partials/card.php @@ -0,0 +1,29 @@ +<article class="w-full bg-white shadow sm:rounded-lg"> + <div class="flex p-4 gap-x-2"> + <div class="relative"> + <time class="absolute px-1 text-xs font-semibold text-white rounded bottom-2 right-2 bg-black/50" datetime="PT<?= $episode->audio_file_duration ?>S"> + <?= format_duration($episode->audio_file_duration) ?> + </time> + <img loading="lazy" src="<?= $episode->image + ->thumbnail_url ?>" alt="<?= $episode->title ?>" class="object-cover w-20 h-20 rounded-lg" /> + </div> + <div class="flex items-center flex-1 gap-x-4"> + <div class="flex flex-col flex-1"> + <div class="inline-flex items-center"> + <?= episode_numbering($episode->number, $episode->season_number, 'text-xs font-semibold text-gray-700 px-1 border mr-2 !no-underline', true) ?> + <?= relative_time($episode->published_at, 'text-xs whitespace-nowrap') ?> + </div> + <h2 class="flex-1 font-semibold line-clamp-2"><a class="hover:underline" href="<?= $episode->link ?>"><?= $episode->title ?></a></h2> + </div> + <play-episode-button + id="<?= $episode->id ?>" + imageSrc="<?= $episode->image->thumbnail_url ?>" + title="<?= $episode->title ?>" + podcast="<?= $episode->podcast->title ?>" + src="<?= $episode->audio_file_web_url ?>" + mediaType="<?= $episode->audio_file_mimetype ?>" + playLabel="<?= lang('Common.play_episode_button.play') ?>" + playingLabel="<?= lang('Common.play_episode_button.playing') ?>"></play-episode-button> + </div> + </div> +</article> diff --git a/themes/cp_app/podcast/_partials/comment.php b/themes/cp_app/episode/_partials/comment.php similarity index 77% rename from themes/cp_app/podcast/_partials/comment.php rename to themes/cp_app/episode/_partials/comment.php index 8109c99787..a7c0e878f6 100644 --- a/themes/cp_app/podcast/_partials/comment.php +++ b/themes/cp_app/episode/_partials/comment.php @@ -1,5 +1,5 @@ -<article class="relative z-10 flex w-full px-4 py-2 rounded-2xl"> - <img src="<?= $comment->actor->avatar_image_url ?>" alt="<?= $comment->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> +<article class="relative z-10 flex w-full px-4 py-2 sm:rounded-2xl"> + <img src="<?= $comment->actor->avatar_image_url ?>" alt="<?= $comment->display_name ?>" class="w-10 h-10 mr-2 rounded-full" /> <div class="flex-1"> <header class="w-full mb-2 text-sm"> <a href="<?= $comment->actor @@ -13,14 +13,14 @@ ($comment->actor->is_local ? '' : '@' . $comment->actor->domain) ?></span> - <?= relative_time($comment->created_at, 'text-xs text-gray-500 ml-auto') ?> + <?= relative_time($comment->created_at, 'text-xs text-gray-500 ml-auto flex-shrink-0') ?> </a> </header> <div class="mb-2 post-content"><?= $comment->message_html ?></div> <?php if ($comment->is_from_post): ?> - <?= $this->include('podcast/_partials/comment_actions_from_post') ?> + <?= $this->include('episode/_partials/comment_actions_from_post') ?> <?php else: ?> - <?= $this->include('podcast/_partials/comment_actions') ?> + <?= $this->include('episode/_partials/comment_actions') ?> <?php endif; ?> </div> </article> diff --git a/themes/cp_app/episode/_partials/comment_actions.php b/themes/cp_app/episode/_partials/comment_actions.php new file mode 100644 index 0000000000..245ced8509 --- /dev/null +++ b/themes/cp_app/episode/_partials/comment_actions.php @@ -0,0 +1,42 @@ +<footer> + <?php if (can_user_interact()): ?> + <form action="<?= route_to('comment-attempt-like', interact_as_actor()->username, $comment->episode->slug, $comment->id) ?>" method="POST" class="flex items-center gap-x-4"> + <button type="submit" name="action" class="inline-flex items-center hover:underline group" title="<?= lang( + 'Comment.likes', + [ + 'numberOfLikes' => $comment->likes_count, + ], +) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-400 group-hover:text-red-600') . $comment->likes_count ?></button> + <Button uri="<?= route_to('episode-comment', $comment->episode->podcast->handle, $comment->episode->slug, $comment->id) ?>" size="small"><?= lang('Comment.reply') ?></Button> + </form> + <?php if ($comment->replies_count): ?> + <?= anchor( + route_to('episode-comment', $comment->episode->podcast->handle, $comment->episode->slug, $comment->id), + icon('caret-down', 'text-xl mr-1') . lang('Comment.view_replies', [ + 'numberOfReplies' => $comment->replies_count, + ]), + [ + 'class' => 'inline-flex items-center text-xs hover:underline', + ] +) ?> + <?php endif; ?> + <?php else: ?> + <button class="inline-flex items-center opacity-50 cursor-not-allowed hover:underline" title="<?= lang( + 'Comment.like', + [ + 'numberOfLikes' => $comment->likes_count, + ], +) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-500') . $comment->likes_count ?></button> + <?php if ($comment->replies_count): ?> + <?= anchor( + route_to('episode-comment', $comment->episode->podcast->handle, $episode->slug, $comment->id), + icon('caret-down', 'text-xl mr-1') . lang('Comment.view_replies', [ + 'numberOfReplies' => $comment->replies_count, + ]), + [ + 'class' => 'inline-flex items-center text-xs hover:underline', + ] +) ?> + <?php endif; ?> + <?php endif; ?> +</footer> diff --git a/themes/cp_app/episode/_partials/comment_actions_from_post.php b/themes/cp_app/episode/_partials/comment_actions_from_post.php new file mode 100644 index 0000000000..fb70838f51 --- /dev/null +++ b/themes/cp_app/episode/_partials/comment_actions_from_post.php @@ -0,0 +1,42 @@ +<footer> + <?php if (can_user_interact()): ?> + <form action="<?= route_to('post-attempt-action', interact_as_actor()->username, $comment->id) ?>" method="POST" class="flex items-center gap-x-4"> + <button type="submit" name="action" value="favourite" class="inline-flex items-center hover:underline group" title="<?= lang( + 'Comment.likes', + [ + 'numberOfLikes' => $comment->likes_count, + ], +) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-400 group-hover:text-red-600') . $comment->likes_count ?></button> + <Button uri="<?= route_to('post', $podcast->handle, $comment->id) ?>" size="small"><?= lang('Comment.reply') ?></Button> + </form> + <?php if ($comment->replies_count): ?> + <?= anchor( + route_to('post', $podcast->handle, $comment->id), + icon('caret-down', 'text-xl mr-1') . lang('Comment.view_replies', [ + 'numberOfReplies' => $comment->replies_count, + ]), + [ + 'class' => 'inline-flex items-center text-xs hover:underline', + ] +) ?> + <?php endif; ?> + <?php else: ?> + <button class="inline-flex items-center opacity-50 cursor-not-allowed hover:underline" title="<?= lang( + 'Comment.like', + [ + 'numberOfLikes' => $comment->likes_count, + ], +) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-500') . $comment->likes_count ?></button> + <?php if ($comment->replies_count): ?> + <?= anchor( + route_to('post', $podcast->handle, $comment->id), + icon('caret-down', 'text-xl mr-1') . lang('Comment.view_replies', [ + 'numberOfReplies' => $comment->replies_count, + ]), + [ + 'class' => 'inline-flex items-center text-xs hover:underline', + ] +) ?> + <?php endif; ?> + <?php endif; ?> +</footer> diff --git a/themes/cp_app/podcast/_partials/comment_card_authenticated.php b/themes/cp_app/episode/_partials/comment_card.php similarity index 58% rename from themes/cp_app/podcast/_partials/comment_card_authenticated.php rename to themes/cp_app/episode/_partials/comment_card.php index 996d2a59da..e68759e283 100644 --- a/themes/cp_app/podcast/_partials/comment_card_authenticated.php +++ b/themes/cp_app/episode/_partials/comment_card.php @@ -1,9 +1,8 @@ -<article class="relative z-10 flex w-full p-4 bg-white shadow rounded-2xl"> +<article class="relative z-10 flex w-full p-4 bg-white shadow sm:rounded-2xl"> <img src="<?= $comment->actor->avatar_image_url ?>" alt="<?= $comment->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> <div class="flex-1"> <header class="w-full mb-2 text-sm"> - <a href="<?= $comment->actor - ->uri ?>" class="flex items-baseline hover:underline" <?= $comment->actor->is_local + <a href="<?= $comment->actor->uri ?>" class="flex items-baseline hover:underline" <?= $comment->actor->is_local ? '' : 'target="_blank" rel="noopener noreferrer"' ?>> <span class="mr-2 font-semibold truncate"><?= $comment->actor @@ -18,10 +17,11 @@ </header> <div class="mb-2 post-content"><?= $comment->message_html ?></div> <?php if ($comment->is_from_post): ?> - <?= $this->include('podcast/_partials/comment_actions_from_post_authenticated') ?> + <?= $this->include('episode/_partials/comment_actions_from_post') ?> <?php else: ?> <footer> - <form action="<?= route_to('comment-attempt-like', interact_as_actor()->username, $episode->slug, $comment->id) ?>" method="POST" class="flex items-center gap-x-4"> + <?php if (can_user_interact()): ?> + <form action="<?= route_to('comment-attempt-like', interact_as_actor()->username, $episode->slug, $comment->id) ?>" method="POST" class="flex items-center gap-x-4"> <button type="submit" name="action" class="inline-flex items-center hover:underline group" title="<?= lang( 'Comment.likes', [ @@ -34,6 +34,19 @@ ], ) ?></button> </form> + <?php else: ?> + <button class="inline-flex items-center opacity-50 cursor-not-allowed" title="<?= lang( + 'Comment.likes', + [ + 'numberOfLikes' => $comment->likes_count, + ], + ) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-500') . lang( + 'Comment.likes', + [ + 'numberOfLikes' => $comment->likes_count, + ], + ) ?></button> + <?php endif; ?> </footer> <?php endif; ?> </div> diff --git a/themes/cp_app/podcast/_partials/comment_reply.php b/themes/cp_app/episode/_partials/comment_reply.php similarity index 93% rename from themes/cp_app/podcast/_partials/comment_reply.php rename to themes/cp_app/episode/_partials/comment_reply.php index 3b8f1b0371..8f6d464bfd 100644 --- a/themes/cp_app/podcast/_partials/comment_reply.php +++ b/themes/cp_app/episode/_partials/comment_reply.php @@ -14,6 +14,6 @@ <?= relative_time($reply->created_at, 'flex-shrink-0 ml-auto text-xs text-gray-600') ?> </header> <p class="mb-2 post-content"><?= $reply->message_html ?></p> - <?= $this->include('podcast/_partials/comment_reply_actions') ?> + <?= $this->include('episode/_partials/comment_reply_actions') ?> </div> </article> diff --git a/themes/cp_app/episode/_partials/comment_reply_actions.php b/themes/cp_app/episode/_partials/comment_reply_actions.php new file mode 100644 index 0000000000..126167a254 --- /dev/null +++ b/themes/cp_app/episode/_partials/comment_reply_actions.php @@ -0,0 +1,32 @@ +<footer> + <?php if (can_user_interact()): ?> + <form action="<?= route_to('comment-attempt-like', interact_as_actor()->username, $episode->slug, $reply->id) ?>" method="POST" class="flex items-center gap-x-4"> + <button type="submit" name="action" class="inline-flex items-center hover:underline group" title="<?= lang( + 'Comment.likes', + [ + 'numberOfLikes' => $reply->likes_count, + ], +) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-400 group-hover:text-red-600') . $reply->likes_count ?></button> + <Button uri="<?= route_to('episode-comment', $podcast->handle, $episode->slug, $reply->id) ?>" size="small"><?= lang('Comment.reply') ?></Button> + </form> + <?php else: ?> + <button type="submit" name="action" class="inline-flex items-center opacity-50 cursor-not-allowed" disabled="disabled" title="<?= lang( + 'Comment.likes', + [ + 'numberOfLikes' => $reply->likes_count, + ], +) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-500') . $reply->likes_count ?></button> + <?php if ($reply->replies_count): ?> + <?= anchor( + route_to('episode-comment', $podcast->handle, $episode->slug, $reply->id), + icon('chat', 'text-2xl mr-1 text-gray-400') . $reply->replies_count, + [ + 'class' => 'inline-flex items-center hover:underline', + 'title' => lang('Comment.replies', [ + 'numberOfReplies' => $reply->replies_count, + ]), + ], +) ?> + <?php endif; ?> + <?php endif; ?> +</footer> diff --git a/themes/cp_app/episode/_partials/comment_with_replies.php b/themes/cp_app/episode/_partials/comment_with_replies.php new file mode 100644 index 0000000000..6c7aed5139 --- /dev/null +++ b/themes/cp_app/episode/_partials/comment_with_replies.php @@ -0,0 +1,29 @@ +<?= $this->include('episode/_partials/comment_card') ?> +<div class="-mt-2 overflow-hidden border-b border-l border-r post-replies rounded-b-xl"> + +<?php if (can_user_interact()): ?> +<form action="<?= route_to('comment-attempt-reply', $podcast->id, $episode->id, $comment->id) ?>" method="POST" class="flex px-6 pt-8 pb-4 bg-gray-50"> + <img src="<?= interact_as_actor() + ->avatar_image_url ?>" alt="<?= interact_as_actor() + ->display_name ?>" class="w-12 h-12 mr-4 rounded-full ring-gray-50 ring-2" /> + <div class="flex flex-col flex-1"> + <Forms.Textarea + name="message" + required="true" + class="w-full mb-4" + placeholder="<?= lang('Comment.form.reply_to_placeholder', [ + 'actorUsername' => $comment->actor->username, + ]) ?>" + rows="1" /> + <Button variant="primary" size="small" type="submit" name="action" value="reply" iconRight="send-plane"><?= lang('Comment.form.submit_reply') ?></Button> + </div> +</form> +<?php endif; ?> + +<?php foreach ($comment->replies as $reply): ?> + <?= view('episode/_partials/comment_reply', [ + 'reply' => $reply, + ]) ?> +<?php endforeach; ?> + +</div> diff --git a/themes/cp_app/episode/_partials/navigation.php b/themes/cp_app/episode/_partials/navigation.php new file mode 100644 index 0000000000..e39db5d955 --- /dev/null +++ b/themes/cp_app/episode/_partials/navigation.php @@ -0,0 +1,22 @@ +<?php declare(strict_types=1); + +$navigationItems = [ + [ + 'uri' => route_to('episode', $podcast->handle, $episode->slug), + 'label' => lang('Episode.comments'), + 'labelInfo' => $episode->comments_count, + ], + [ + 'uri' => route_to('episode-activity', $podcast->handle, $episode->slug), + 'label' => lang('Episode.activity'), + 'labelInfo' => $episode->posts_count, + ], +] +?> +<nav class="sticky z-40 flex col-start-2 px-4 pt-4 bg-white shadow md:px-8 gap-x-2 md:gap-x-4 -top-4 md:rounded-b-xl"> + <?php foreach ($navigationItems as $item): ?> + <?php $isActive = url_is($item['uri']); ?> + <a href="<?= $item['uri'] ?>" class="px-4 py-1 text-sm font-semibold uppercase border-b-4<?= $isActive ? ' border-b-4 text-pine-500 border-pine-500' : ' text-gray-500 hover:text-gray-900 hover:border-gray-200 border-transparent' ?>"><?= $item['label'] ?><span class="px-2 ml-1 font-semibold rounded-full <?= $isActive ? ' bg-pine-100' : ' bg-gray-100' ?>"><?= $item['labelInfo'] ?></span></a> + <?php endforeach; ?> + <button type="button" class="p-2 ml-auto rotate-180 rounded-full sm:hidden focus:outline-none focus:ring-2 focus:ring-pine-500 focus:ring-offset-2 focus:ring-offset-pine-100" data-toggle="podcast-sidebar" data-toggle-class="absolute sticky top-0 right-0 hidden bg-white top-12"><?= icon('menu') ?></button> +</nav> \ No newline at end of file diff --git a/themes/cp_app/episode/_partials/preview_card.php b/themes/cp_app/episode/_partials/preview_card.php new file mode 100644 index 0000000000..a9dfe3b9cf --- /dev/null +++ b/themes/cp_app/episode/_partials/preview_card.php @@ -0,0 +1,27 @@ +<div class="flex items-center border-t border-b"> + <div class="relative"> + <time class="absolute px-1 text-sm font-semibold text-white bg-black/50 bottom-2 right-2" datetime="PT<?= $episode->audio_file_duration ?>S"> + <?= format_duration($episode->audio_file_duration) ?> + </time> + <img + src="<?= $episode->image->thumbnail_url ?>" + alt="<?= $episode->title ?>" class="w-24 h-24"/> + </div> + <div class="flex flex-col flex-1 px-4 py-2"> + <div class="inline-flex"> + <?= episode_numbering($episode->number, $episode->season_number, 'text-xs font-semibold text-gray-700 px-1 border mr-2 !no-underline', true) ?> + <?= relative_time($episode->published_at, 'text-xs whitespace-nowrap text-gray-500') ?> + </div> + <a href="<?= $episode->link ?>" class="flex items-baseline font-semibold line-clamp-2" title="<?= $episode->title ?>"><?= $episode->title ?></a> + </div> + <play-episode-button + class="mr-4" + id="<?= $index . '_' . $episode->id ?>" + imageSrc="<?= $episode->image->thumbnail_url ?>" + title="<?= $episode->title ?>" + podcast="<?= $episode->podcast->title ?>" + src="<?= $episode->audio_file_web_url ?>" + mediaType="<?= $episode->audio_file_mimetype ?>" + playLabel="<?= lang('Common.play_episode_button.play') ?>" + playingLabel="<?= lang('Common.play_episode_button.playing') ?>"></play-episode-button> +</div> \ No newline at end of file diff --git a/themes/cp_app/episode/activity.php b/themes/cp_app/episode/activity.php new file mode 100644 index 0000000000..94c9181894 --- /dev/null +++ b/themes/cp_app/episode/activity.php @@ -0,0 +1,68 @@ +<?= $this->extend('episode/_layout') ?> + +<?= $this->section('meta-tags') ?> +<title><?= $episode->title ?></title> +<meta name="description" content="<?= htmlspecialchars($episode->description) ?>" /> +<link rel="canonical" href="<?= $episode->link ?>" /> +<meta property="og:title" content="<?= $episode->title ?>" /> +<meta property="og:description" content="<?= $episode->description ?>" /> +<meta property="og:locale" content="<?= $podcast->language_code ?>" /> +<meta property="og:site_name" content="<?= $podcast->title ?>" /> +<meta property="og:url" content="<?= current_url() ?>" /> +<meta property="og:image" content="<?= $episode->image->large_url ?>" /> +<meta property="og:image:width" content="<?= config('Images') + ->largeSize ?>" /> +<meta property="og:image:height" content="<?= config('Images') + ->largeSize ?>" /> +<meta property="og:description" content="$description" /> +<meta property="article:published_time" content="<?= $episode->published_at ?>" /> +<meta property="article:modified_time" content="<?= $episode->updated_at ?>" /> +<meta property="og:audio" content="<?= $episode->audio_file_opengraph_url ?>" /> +<meta property="og:audio:type" content="<?= $episode->audio_file_mimetype ?>" /> +<link rel="alternate" type="application/json+oembed" href="<?= base_url(route_to('episode-oembed-json', $podcast->handle, $episode->slug)) ?>" title="<?= $episode->title ?> oEmbed json" /> +<link rel="alternate" type="text/xml+oembed" href="<?= base_url(route_to('episode-oembed-xml', $podcast->handle, $episode->slug)) ?>" title="<?= $episode->title ?> oEmbed xml" /> +<meta name="twitter:title" content="<?= $episode->title ?>" /> +<meta name="twitter:description" content="<?= $episode->description ?>" /> +<meta name="twitter:image" content="<?= $episode->image->large_url ?>" /> +<meta name="twitter:card" content="player" /> +<meta property="twitter:audio:partner" content="<?= $podcast->publisher ?>" /> +<meta property="twitter:audio:artist_name" content="<?= $podcast->owner_name ?>" /> +<meta name="twitter:player" content="<?= $episode->getEmbeddablePlayerUrl('light') ?>" /> +<meta name="twitter:player:width" content="600" /> +<meta name="twitter:player:height" content="200" /> +<?= $this->endSection() ?> + +<?= $this->section('content') ?> + +<div class="flex flex-col gap-y-4"> +<?php if (can_user_interact()): ?> + <form action="<?= route_to('post-attempt-create', $podcast->handle) ?>" method="POST" class="flex p-4 bg-white shadow sm:rounded-2xl"> + <?= csrf_field() ?> + + <?= view('_message_block') ?> + + <img src="<?= interact_as_actor() + ->avatar_image_url ?>" alt="<?= interact_as_actor() + ->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> + <div class="flex flex-col flex-1 min-w-0 gap-y-2"> + <input name="episode_url" value="<?= $episode->link ?>" type="hidden" /> + <Forms.Textarea + name="message" + placeholder="<?= lang('Post.form.episode_message_placeholder') ?>" + required="true" + rows="2" /> + <Button variant="primary" size="small" type="submit" class="self-end" iconRight="send-plane"><?= lang('Post.form.submit') ?></Button> + </div> + </form> +<?php endif; ?> + +<?php foreach ($episode->posts as $key => $post): ?> + <?= view('post/_partials/card', [ + 'index' => $key, + 'post' => $post, + 'podcast' => $podcast, +]) ?> +<?php endforeach; ?> +</div> + +<?= $this->endSection() ?> diff --git a/themes/cp_app/podcast/comment.php b/themes/cp_app/episode/comment.php similarity index 92% rename from themes/cp_app/podcast/comment.php rename to themes/cp_app/episode/comment.php index b7e66a6589..87e31e34d6 100644 --- a/themes/cp_app/podcast/comment.php +++ b/themes/cp_app/episode/comment.php @@ -1,4 +1,4 @@ -<?= $this->extend('podcast/_layout') ?> +<?= $this->extend('episode/_layout') ?> <?= $this->section('meta-tags') ?> <title><?= lang('Comment.title', [ @@ -30,7 +30,7 @@ ]) ?></a> </nav> <div class="pb-12"> - <?= $this->include('podcast/_partials/comment_with_replies') ?> + <?= $this->include('episode/_partials/comment_with_replies') ?> </div> </div> diff --git a/themes/cp_app/episode/comments.php b/themes/cp_app/episode/comments.php new file mode 100644 index 0000000000..6b0e7a3176 --- /dev/null +++ b/themes/cp_app/episode/comments.php @@ -0,0 +1,69 @@ +<?= $this->extend('episode/_layout') ?> + +<?= $this->section('meta-tags') ?> +<title><?= $episode->title ?></title> +<meta name="description" content="<?= htmlspecialchars( + $episode->description, +) ?>" /> +<link rel="canonical" href="<?= $episode->link ?>" /> +<meta property="og:title" content="<?= $episode->title ?>" /> +<meta property="og:description" content="<?= $episode->description ?>" /> +<meta property="og:locale" content="<?= $podcast->language_code ?>" /> +<meta property="og:site_name" content="<?= $podcast->title ?>" /> +<meta property="og:url" content="<?= current_url() ?>" /> +<meta property="og:image" content="<?= $episode->image->large_url ?>" /> +<meta property="og:image:width" content="<?= config('Images') + ->largeSize ?>" /> +<meta property="og:image:height" content="<?= config('Images') + ->largeSize ?>" /> +<meta property="og:description" content="$description" /> +<meta property="article:published_time" content="<?= $episode->published_at ?>" /> +<meta property="article:modified_time" content="<?= $episode->updated_at ?>" /> +<meta property="og:audio" content="<?= $episode->audio_file_opengraph_url ?>" /> +<meta property="og:audio:type" content="<?= $episode->audio_file_mimetype ?>" /> +<link rel="alternate" type="application/json+oembed" href="<?= base_url(route_to('episode-oembed-json', $podcast->handle, $episode->slug)) ?>" title="<?= $episode->title ?> oEmbed json" /> +<link rel="alternate" type="text/xml+oembed" href="<?= base_url(route_to('episode-oembed-xml', $podcast->handle, $episode->slug)) ?>" title="<?= $episode->title ?> oEmbed xml" /> +<meta name="twitter:title" content="<?= $episode->title ?>" /> +<meta name="twitter:description" content="<?= $episode->description ?>" /> +<meta name="twitter:image" content="<?= $episode->image->large_url ?>" /> +<meta name="twitter:card" content="player" /> +<meta property="twitter:audio:partner" content="<?= $podcast->publisher ?>" /> +<meta property="twitter:audio:artist_name" content="<?= $podcast->owner_name ?>" /> +<meta name="twitter:player" content="<?= $episode->getEmbeddablePlayerUrl('light') ?>" /> +<meta name="twitter:player:width" content="600" /> +<meta name="twitter:player:height" content="200" /> +<?= $this->endSection() ?> + +<?= $this->section('content') ?> + +<div class="flex flex-col gap-y-4"> + <?php if (can_user_interact()): ?> + <form action="<?= route_to('comment-attempt-create', $podcast->id, $episode->id) ?>" method="POST" class="flex p-4"> + <?= csrf_field() ?> + + <?= view('_message_block') ?> + + <img src="<?= interact_as_actor() + ->avatar_image_url ?>" alt="<?= interact_as_actor() + ->display_name ?>" class="w-10 h-10 mr-2 rounded-full" /> + <div class="flex flex-col flex-1 min-w-0 gap-y-2"> + <Forms.Textarea + name="message" + required="true" + placeholder="<?= lang('Comment.form.episode_message_placeholder') ?>" + rows="2" /> + <Button class="self-end" variant="primary" size="small" type="submit" iconRight="send-plane"><?= lang('Comment.form.submit') ?></Button> + </div> + </form> + <?php endif; ?> + + <?php foreach ($episode->comments as $comment): ?> + <?= view('episode/_partials/comment', [ + 'comment' => $comment, + 'podcast' => $podcast, +]) ?> + <?php endforeach; ?> +</div> + +<?= $this->endSection() +?> diff --git a/themes/cp_app/home.php b/themes/cp_app/home.php index e903879469..61c03e3f1a 100644 --- a/themes/cp_app/home.php +++ b/themes/cp_app/home.php @@ -11,6 +11,8 @@ <link rel="shortcut icon" type="image/png" href="/favicon.ico" /> <?= service('vite') ->asset('styles/index.css', 'css') ?> + <?= service('vite') + ->asset('js/app.ts', 'js') ?> </head> <body class="flex flex-col min-h-screen mx-auto bg-pine-50"> @@ -52,7 +54,7 @@ <?= render_page_links() ?> <small><?= lang('Common.powered_by', [ 'castopod' => - '<a class="underline hover:no-underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod</a>', + '<a class="inline-flex font-semibold hover:underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod' . icon('social/castopod', 'ml-1 text-lg') . '</a>', ]) ?></small> </footer> </body> diff --git a/themes/cp_app/map.php b/themes/cp_app/map.php index b82e424b4f..dc5ec1cbea 100644 --- a/themes/cp_app/map.php +++ b/themes/cp_app/map.php @@ -11,6 +11,8 @@ <link rel="shortcut icon" type="image/png" href="/favicon.ico" /> <?= service('vite') ->asset('styles/index.css', 'css') ?> + <?= service('vite') + ->asset('js/app.ts', 'js') ?> <?= service('vite') ->asset('js/map.ts', 'js') ?> </head> @@ -37,7 +39,7 @@ <?= render_page_links() ?> <small><?= lang('Common.powered_by', [ 'castopod' => - '<a class="underline hover:no-underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod</a>', + '<a class="inline-flex font-semibold hover:underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod' . icon('social/castopod', 'ml-1 text-lg') . '</a>', ]) ?></small> </footer> </body> diff --git a/themes/cp_app/page.php b/themes/cp_app/page.php index 49cea85027..1cef93164c 100644 --- a/themes/cp_app/page.php +++ b/themes/cp_app/page.php @@ -10,6 +10,8 @@ <link rel="shortcut icon" type="image/png" href="/favicon.ico" /> <?= service('vite') ->asset('styles/index.css', 'css') ?> + <?= service('vite') + ->asset('js/app.ts', 'js') ?> </head> <body class="flex flex-col min-h-screen mx-auto"> @@ -36,7 +38,7 @@ <?= render_page_links() ?> <small><?= lang('Common.powered_by', [ 'castopod' => - '<a class="underline hover:no-underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod</a>', + '<a class="inline-flex font-semibold hover:underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod' . icon('social/castopod', 'ml-1 text-lg') . '</a>', ]) ?></small> </footer> </body> diff --git a/themes/cp_app/podcast/_layout copy.php b/themes/cp_app/podcast/_layout copy.php deleted file mode 100644 index a37e77b4ac..0000000000 --- a/themes/cp_app/podcast/_layout copy.php +++ /dev/null @@ -1,123 +0,0 @@ -<?= helper('page') ?> - -<!DOCTYPE html> -<html lang="<?= service('request') - ->getLocale() ?>"> - -<head> - <meta charset="UTF-8"/> - <meta name="viewport" content="width=device-width, initial-scale=1.0"/> - <link rel="shortcut icon" type="image/png" href="/favicon.ico" /> - - <?= $this->renderSection('meta-tags') ?> - <?php if ($podcast->payment_pointer): ?> - <meta name="monetization" content="<?= $podcast->payment_pointer ?>" /> - <?php endif; ?> - - <?= service('vite') - ->asset('styles/index.css', 'css') ?> - <?= service('vite') - ->asset('js/podcast.ts', 'js') ?> - <?= service('vite') - ->asset('js/audio-player.ts', 'js') ?> -</head> - -<body class="flex w-full min-h-screen pb-20 overflow-x-hidden lg:mx-auto lg:container bg-pine-50 sm:pb-0"> - <?= $this->include('podcast/_partials/header') ?> - - <main class="flex-shrink-0 w-full min-w-0 sm:w-auto sm:flex-1 sm:flex-shrink"> - <nav class="sticky top-0 left-0 z-50 flex items-center w-full h-12 px-2 py-1 sm:hidden bg-pine-800"> - <button - data-toggle="main-header" - data-toggle-class="sticky -translate-x-full" - class="flex-shrink-0 mr-3 overflow-hidden rounded-full focus:ring-2 focus:outline-none focus:ring-pine-50"> - <img src="<?= $podcast->image - ->thumbnail_url ?>" alt="<?= $podcast->title ?>" class="h-10"/> - </button> - <p class="flex flex-col flex-1 min-w-0 mr-2 text-white"> - <span class="text-sm font-semibold truncate"><?= $podcast->title ?></span> - <span class="text-xs">@<?= $podcast->handle ?></span> - </p> - <?= anchor_popup( - route_to('follow', $podcast->handle), - icon( - 'social/castopod', - 'mr-2 text-xl text-pink-200 group-hover:text-pink-50', - ) . lang('Podcast.follow'), - [ - 'width' => 420, - 'height' => 620, - 'class' => - 'group inline-flex mr-2 items-center px-3 py-1 text-xs tracking-wider font-semibold text-white uppercase rounded-full shadow focus:outline-none focus:ring bg-rose-600', - ], - ) ?> - <button - data-toggle="main-sidebar" - data-toggle-class="translate-x-full" - data-toggle-body-class="-ml-64" - class="p-2 text-xl rounded-full focus:outline-none focus:ring-2 focus:ring-pine-600 text-pine-200 hover:text-pine-50"><?= icon( - 'menu', - ) ?><span class="sr-only"><?= lang('Podcast.toggle_podcast_sidebar') ?></span></button> - </nav> - <?= $this->renderSection('content') ?> - </main> - - <?= $this->include('podcast/_partials/sidebar') ?> - - <button - data-toggle="main-sidebar" - data-toggle-class="translate-x-full" - data-toggle-body-class="-ml-64" - class="fixed z-40 hidden p-4 text-xl rounded-full shadow-2xl sm:block lg:hidden bottom-4 left-4 bg-pine-800 focus:outline-none focus:ring-2 focus:ring-pine-600 text-pine-200 hover:text-pine-50"><?= icon( - 'menu', - ) ?><span class="sr-only"><?= lang( - 'Podcast.toggle_podcast_sidebar', - ) ?></span></button> - - <!-- Funding links modal --> - <div id="funding-links" class="fixed top-0 left-0 z-50 flex items-center justify-center hidden w-screen h-screen"> - <div - class="absolute w-full h-full bg-pine-800 bg-opacity-90" - role="button" - data-toggle="funding-links" - data-toggle-class="hidden" - aria-label="<?= lang('Common.close') ?>"></div> - <div class="z-10 w-full max-w-xl bg-white rounded-lg shadow-2xl"> - <div class="flex justify-between px-4 py-2 border-b"> - <h3 class="self-center text-lg"><?= lang( - 'Podcast.funding_links', - [ - 'podcastTitle' => $podcast->title, - ], - ) ?></h3> - <button - data-toggle="funding-links" - data-toggle-class="hidden" - aria-label="<?= lang('Common.close') ?>" - class="self-start p-1 text-2xl"><?= icon('close') ?></button> - </div> - <div class="flex flex-col items-start p-4 space-y-4"> - <?php foreach ( - $podcast->fundingPlatforms - as $fundingPlatform - ): ?> - <?php if ($fundingPlatform->is_visible): ?> - <a - href="<?= $fundingPlatform->link_url ?>" - title="<?= $fundingPlatform->link_content ?>" - target="_blank" - rel="noopener noreferrer" - class="inline-flex items-center font-semibold text-pine-900"> - <?= icon( - $fundingPlatform->type . - '/' . - $fundingPlatform->slug, - 'mr-2', - ) . $fundingPlatform->link_url ?> - </a> - <?php endif; ?> - <?php endforeach; ?> - </div> - </div> - </div> -</body> diff --git a/themes/cp_app/podcast/_layout.php b/themes/cp_app/podcast/_layout.php index 7a632a054d..1629b51378 100644 --- a/themes/cp_app/podcast/_layout.php +++ b/themes/cp_app/podcast/_layout.php @@ -16,118 +16,60 @@ <?= service('vite') ->asset('styles/index.css', 'css') ?> + <?= service('vite') + ->asset('js/app.ts', 'js') ?> <?= service('vite') ->asset('js/podcast.ts', 'js') ?> <?= service('vite') ->asset('js/audio-player.ts', 'js') ?> </head> -<body class="grid items-start w-1/2 grid-cols-9 mx-auto bg-pine-50 gap-y-8 gap-x-6"> - <header class="sticky z-50 flex flex-col bg-white shadow -top-96 rounded-b-xl col-span-full"> - <div style="background-image: url('<?= $podcast->actor->cover_image_url ?>'); background-size: auto 320px;" class="w-full bg-fixed bg-top bg-no-repeat bg-cover bg-pine-800 h-80"></div> - <div class="flex items-center justify-between py-4 ml-8 -mt-28"> - <div class="flex items-center gap-x-4"> - <img src="<?= $podcast->image->thumbnail_url ?>" alt="<?= $podcast->title ?>" loading="lazy" class="rounded-full h-36 ring-4 ring-white" /> - <div class="flex flex-col -mt-4 text-white"> - <h1 class="inline-flex items-center text-2xl font-bold leading-none font-display"><?= $podcast->title . ($podcast->parental_advisory === 'explicit' ? '<span class="px-1 ml-2 text-xs font-semibold leading-tight tracking-wider text-gray-600 uppercase border-2 border-gray-500">' . lang('Common.explicit') . '</span>' : '') ?></h1> - <a href="#" class="hover:underline"><?= lang('Podcast.followers', [ - 'numberOfFollowers' => $podcast->actor->followers_count, - ]) ?></a> - </div> - </div> +<body class="grid items-start mx-auto grid-cols-podcastLayout bg-pine-50"> + <?php if (can_user_interact()): ?> + <div class="col-span-full"> + <?= $this->include('_admin_navbar') ?> + </div> + <?php endif; ?> - <div class="inline-flex items-center mr-4 -mt-4 gap-x-2"> - <?php if (in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true)): ?> - <IconButton glyph="heart" data-toggle="funding-links" data-toggle-class="hidden"><?= lang('Podcast.sponsor') . lang('Podcast.sponsor_title') ?></IconButton> - <?php endif; ?> - <?= anchor_popup( - route_to('follow', $podcast->handle), - icon( - 'social/castopod', - 'mr-2 text-xl text-pink-200 group-hover:text-pink-50', - ) . lang('Podcast.follow'), - [ - 'width' => 420, - 'height' => 620, - 'class' => - 'group inline-flex items-center px-4 py-2 text-xs tracking-wider font-semibold text-white uppercase rounded-full shadow focus:outline-none focus:ring bg-rose-600', - ], - ) ?> + <header class="z-50 flex flex-col-reverse justify-between w-full col-start-2 bg-top bg-no-repeat bg-cover sm:flex-row sm:items-end bg-pine-800" style="background-image: url('<?= $podcast->actor->cover_image_url ?>'); aspect-ratio: 15 / 5;"> + <div class="flex items-center pl-4 -mb-6 md:pl-8 md:-mb-8 gap-x-4"> + <img src="<?= $podcast->image->thumbnail_url ?>" alt="<?= $podcast->title ?>" loading="lazy" class="h-24 rounded-full md:h-28 ring-4 ring-white" /> + <div class="relative flex flex-col text-white -top-2"> + <h1 class="text-lg font-bold leading-none line-clamp-2 md:leading-none md:text-2xl font-display"><?= $podcast->title ?><span class="ml-1 font-sans text-base font-normal">@<?= $podcast->handle ?></span></h1> + <span class="text-xs"><?= lang('Podcast.followers', [ + 'numberOfFollowers' => $podcast->actor->followers_count, + ]) ?></span> </div> </div> - <?= $this->include('podcast/_navigation') ?> - </header> - - <main class="col-span-6"> - <?= $this->renderSection('content') ?> - </main> - - <aside class="sticky col-span-3 top-12"> - <?php if ( - in_array(true, array_column($podcast->socialPlatforms, 'is_visible'), true) - ): ?> - <h2 class="font-semibold"> <?= lang('Podcast.find_on', [ - 'podcastTitle' => $podcast->title, - ]) ?></h2> - <div class="grid items-center justify-center grid-cols-6 gap-3 mt-2"> - <?php foreach ($podcast->socialPlatforms as $socialPlatform): ?> - <?php if ($socialPlatform->is_visible): ?> - <?= anchor( - $socialPlatform->link_url, - icon("{$socialPlatform->type}/{$socialPlatform->slug}"), - [ - 'class' => 'text-2xl text-gray-500 hover:text-gray-700 w-8 h-8 items-center inline-flex justify-center', - 'target' => '_blank', - 'rel' => 'noopener noreferrer', - 'data-toggle' => 'tooltip', - 'data-placement' => 'bottom', - 'title' => $socialPlatform->label, - ], - ) ?> + <div class="inline-flex items-center self-end mt-2 mb-2 mr-2 gap-x-2"> + <?php if (in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true)): ?> + <IconButton glyph="heart" variant="accent" data-toggle="funding-links" data-toggle-class="hidden"><?= lang('Podcast.sponsor') . lang('Podcast.sponsor_title') ?></IconButton> <?php endif; ?> - <?php endforeach; ?> + <?= anchor_popup( + route_to('follow', $podcast->handle), + icon( + 'social/castopod', + 'mr-2 text-xl text-rose-200 group-hover:text-rose-50', + ) . lang('Podcast.follow'), + [ + 'width' => 420, + 'height' => 620, + 'class' => + 'group inline-flex items-center px-2 py-1 text-xs tracking-wider font-semibold text-white uppercase rounded-full shadow focus:outline-none focus:ring bg-rose-600', + ], + ) ?> </div> - <?php endif; ?> + </header> + <?= $this->include('podcast/_partials/navigation') ?> + <div class="grid items-start grid-cols-3 col-start-2 pb-12 mt-6 gap-x-6"> + <main class="col-span-full sm:col-span-2"> + <?= $this->renderSection('content') ?> + </main> + <?= $this->include('podcast/_partials/sidebar') ?> + </div> + + <?php if (in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true)): ?> + <?= $this->include('podcast/_partials/funding_links_modal') ?> + <?php endif; ?> - <h2 class="mt-6 font-semibold"><?= lang('Podcast.listen_on') ?></h2> - <div class="grid items-center justify-center grid-cols-6 gap-3 mt-2"> - <?= anchor(route_to('podcast_feed', $podcast->handle), icon('rss'), [ - 'class' => - 'bg-orange-500 text-xl text-white hover:bg-orange-700 w-8 h-8 inline-flex items-center justify-center rounded-lg', - 'target' => '_blank', - 'rel' => 'noopener noreferrer', - 'data-toggle' => 'tooltip', - 'data-placement' => 'bottom', - 'title' => lang('Podcast.feed'), - ]) ?> - <?php foreach ($podcast->podcastingPlatforms as $podcastingPlatform): ?> - <?php if ($podcastingPlatform->is_visible): ?> - <?= anchor( - $podcastingPlatform->link_url, - icon( - "{$podcastingPlatform->type}/{$podcastingPlatform->slug}", - ), - [ - 'class' => 'text-2xl text-gray-500 hover:text-gray-700 w-8 h-8 items-center inline-flex justify-center', - 'target' => '_blank', - 'rel' => 'noopener noreferrer', - 'data-toggle' => 'tooltip', - 'data-placement' => 'bottom', - 'title' => $podcastingPlatform->label, - ], - ) ?> - <?php endif; ?> - <?php endforeach; ?> - </div> - <footer class="flex flex-col items-center py-2 mt-8 text-xs text-center text-gray-600 border-t"> - <?= render_page_links('inline-flex mb-2 flex-wrap gap-y-1') ?> - <div class="flex flex-col"> - <p><?= $podcast->copyright ?></p> - <p><?= lang('Common.powered_by', [ - 'castopod' => - '<a class="inline-flex font-semibold hover:underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod' . icon('social/castopod', 'ml-1 text-lg') . '</a>', - ]) ?></p> - </div> - </footer> - </aside> </body> diff --git a/themes/cp_app/podcast/_layout_authenticated.php b/themes/cp_app/podcast/_layout_authenticated.php deleted file mode 100644 index eef37085a7..0000000000 --- a/themes/cp_app/podcast/_layout_authenticated.php +++ /dev/null @@ -1,115 +0,0 @@ -<?= helper('page') ?> - -<!DOCTYPE html> -<html lang="<?= service('request') - ->getLocale() ?>"> - -<head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <link rel="shortcut icon" type="image/png" href="/favicon.ico" /> - - <?= $this->renderSection('meta-tags') ?> - <?php if ($podcast->payment_pointer): ?> - <meta name="monetization" content="<?= $podcast->payment_pointer ?>" /> - <?php endif; ?> - - <?= service('vite') - ->asset('styles/index.css', 'css') ?> - <?= service('vite') - ->asset('js/podcast.ts', 'js') ?> - <?= service('vite') - ->asset('js/audio-player.ts', 'js') ?> -</head> - -<body class="w-full min-h-screen pb-20 overflow-x-hidden bg-pine-50 lg:mx-auto sm:pb-0"> - <?= $this->include('_admin_navbar') ?> - <div class="flex"> - - <?= $this->include('podcast/_partials/header') ?> - - <main class="flex-shrink-0 w-full min-w-0 sm:w-auto sm:flex-1 sm:flex-shrink"> - <nav class="sticky top-0 left-0 z-50 flex items-center w-full h-12 px-2 py-1 sm:hidden bg-pine-800"> - <button - data-toggle="main-header" - data-toggle-class="sticky -translate-x-full" - class="flex-shrink-0 mr-3 overflow-hidden rounded-full focus:ring-2 focus:outline-none focus:ring-pine-50"> - <img src="<?= $podcast->image - ->thumbnail_url ?>" alt="<?= $podcast->title ?>" class="h-10"/> - </button> - <p class="flex flex-col flex-1 min-w-0 mr-2 text-white"> - <span class="text-sm font-semibold truncate"><?= $podcast->title ?></span> - <span class="text-xs">@<?= $podcast->handle ?></span> - </p> - <?= anchor_popup( - route_to('follow', $podcast->handle), - icon( - 'social/castopod', - 'mr-2 text-xl text-pink-200 group-hover:text-pink-50', - ) . lang('Podcast.follow'), - [ - 'width' => 420, - 'height' => 620, - 'class' => - 'group inline-flex mr-2 items-center px-3 py-1 text-xs tracking-wider font-semibold text-white uppercase rounded-full shadow focus:outline-none focus:ring bg-rose-600', - ], - ) ?> - <button - data-toggle="main-sidebar" - data-toggle-class="translate-x-full" - data-toggle-body-class="-ml-64" - class="p-2 text-xl rounded-full focus:outline-none focus:ring-2 focus:ring-pine-600 text-pine-200 hover:text-pine-50"><?= icon( - 'menu', - ) ?><span class="sr-only"><?= lang('Podcast.toggle_podcast_sidebar') ?></span></button> - </nav> - <?= $this->renderSection('content') ?> - </main> - - <?= $this->include('podcast/_partials/sidebar') ?> - - <button data-toggle="main-sidebar" data-toggle-class="translate-x-full" data-toggle-body-class="-ml-64" class="fixed z-40 hidden p-4 text-xl rounded-full shadow-2xl sm:block lg:hidden bottom-4 left-4 bg-pine-800 focus:outline-none focus:ring-2 focus:ring-pine-600 text-pine-200 hover:text-pine-50"><?= icon( - 'menu', - ) ?><span class="sr-only"><?= lang( - 'Podcast.toggle_podcast_sidebar', - ) ?></span></button> - - <!-- Funding links modal --> - <div id="funding-links" class="fixed top-0 left-0 z-50 flex items-center justify-center hidden w-screen h-screen"> - <div class="absolute w-full h-full bg-pine-800 bg-opacity-90" role="button" data-toggle="funding-links" data-toggle-class="hidden" aria-label="<?= lang( - 'Common.close', - ) ?>"></div> - <div class="z-10 w-full max-w-xl bg-white rounded-lg shadow-2xl"> - <div class="flex justify-between px-4 py-2 border-b"> - <h3 class="self-center text-lg"><?= lang( - 'Podcast.funding_links', - [ - 'podcastTitle' => $podcast->title, - ], - ) ?></h3> - <button data-toggle="funding-links" data-toggle-class="hidden" aria-label="<?= lang( - 'Common.close', - ) ?>" class="self-start p-1 text-2xl"> - <?= icon('close') ?> - </button> - </div> - <div class="flex flex-col items-start p-4 space-y-4"> - <?php foreach ( - $podcast->fundingPlatforms - as $fundingPlatform - ): ?> - <?php if ($fundingPlatform->is_visible): ?> - <a href="<?= $fundingPlatform->link_url ?>" title="<?= $fundingPlatform->link_content ?>" target="_blank" rel="noopener noreferrer" class="inline-flex items-center font-semibold text-pine-900"> - <?= icon( - $fundingPlatform->type . - '/' . - $fundingPlatform->slug, - 'text-2xl text-gray-400 mr-2', - ) . $fundingPlatform->link_url ?> - </a> - <?php endif; ?> - <?php endforeach; ?> - </div> - </div> - </div> -</div> -</body> diff --git a/themes/cp_app/podcast/_navigation.php b/themes/cp_app/podcast/_navigation.php deleted file mode 100644 index 66ce97f968..0000000000 --- a/themes/cp_app/podcast/_navigation.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php declare(strict_types=1); - -$navigationItems = [ - [ - 'uri' => route_to('podcast-activity', $podcast->handle), - 'label' => lang('Podcast.activity'), - ], - [ - 'uri' => route_to('podcast-episodes', $podcast->handle), - 'label' => lang('Podcast.episodes'), - ], - [ - 'uri' => route_to('podcast-about', $podcast->handle), - 'label' => lang('Podcast.about'), - ], -] -?> -<nav class="flex gap-4 px-8"> - <?php foreach ($navigationItems as $item): ?> - <?php $isActive = url_is($item['uri']); ?> - <a href="<?= $item['uri'] ?>" class="px-4 py-1 font-semibold uppercase border-b-4<?= $isActive ? ' border-b-4 text-pine-500 border-pine-500' : ' text-gray-500 hover:text-gray-900 hover:border-gray-200 border-transparent' ?>"><?= $item['label'] ?></a> - <?php endforeach; ?> -</nav> \ No newline at end of file diff --git a/themes/cp_app/podcast/_partials/comment_actions.php b/themes/cp_app/podcast/_partials/comment_actions.php deleted file mode 100644 index 93d9eb4a52..0000000000 --- a/themes/cp_app/podcast/_partials/comment_actions.php +++ /dev/null @@ -1,19 +0,0 @@ -<footer> - <button class="inline-flex items-center opacity-50 cursor-not-allowed hover:underline" title="<?= lang( - 'Comment.like', - [ - 'numberOfLikes' => $comment->likes_count, - ], -) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-500') . $comment->likes_count ?></button> - <?php if ($comment->replies_count): ?> - <?= anchor( - route_to('comment', $podcast->handle, $episode->slug, $comment->id), - icon('caret-down', 'text-xl mr-1') . lang('Comment.view_replies', [ - 'numberOfReplies' => $comment->replies_count, - ]), - [ - 'class' => 'inline-flex items-center text-xs hover:underline', - ] -) ?> - <?php endif; ?> -</footer> diff --git a/themes/cp_app/podcast/_partials/comment_actions_authenticated.php b/themes/cp_app/podcast/_partials/comment_actions_authenticated.php deleted file mode 100644 index d4812080ee..0000000000 --- a/themes/cp_app/podcast/_partials/comment_actions_authenticated.php +++ /dev/null @@ -1,22 +0,0 @@ -<footer> - <form action="<?= route_to('comment-attempt-like', interact_as_actor()->username, $comment->episode->slug, $comment->id) ?>" method="POST" class="flex items-center gap-x-4"> - <button type="submit" name="action" class="inline-flex items-center hover:underline group" title="<?= lang( - 'Comment.likes', - [ - 'numberOfLikes' => $comment->likes_count, - ], -) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-400 group-hover:text-red-600') . $comment->likes_count ?></button> - <Button uri="<?= route_to('comment', $podcast->handle, $comment->episode->slug, $comment->id) ?>" size="small"><?= lang('Comment.reply') ?></Button> - </form> - <?php if ($comment->replies_count): ?> - <?= anchor( - route_to('comment', $podcast->handle, $comment->episode->slug, $comment->id), - icon('caret-down', 'text-xl mr-1') . lang('Comment.view_replies', [ - 'numberOfReplies' => $comment->replies_count, - ]), - [ - 'class' => 'inline-flex items-center text-xs hover:underline', - ] -) ?> - <?php endif; ?> -</footer> diff --git a/themes/cp_app/podcast/_partials/comment_actions_from_post.php b/themes/cp_app/podcast/_partials/comment_actions_from_post.php deleted file mode 100644 index 5026e64e9a..0000000000 --- a/themes/cp_app/podcast/_partials/comment_actions_from_post.php +++ /dev/null @@ -1,19 +0,0 @@ -<footer> - <button class="inline-flex items-center opacity-50 cursor-not-allowed hover:underline" title="<?= lang( - 'Comment.like', - [ - 'numberOfLikes' => $comment->likes_count, - ], -) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-500') . $comment->likes_count ?></button> - <?php if ($comment->replies_count): ?> - <?= anchor( - route_to('post', $podcast->handle, $comment->id), - icon('caret-down', 'text-xl mr-1') . lang('Comment.view_replies', [ - 'numberOfReplies' => $comment->replies_count, - ]), - [ - 'class' => 'inline-flex items-center text-xs hover:underline', - ] -) ?> - <?php endif; ?> -</footer> diff --git a/themes/cp_app/podcast/_partials/comment_actions_from_post_authenticated.php b/themes/cp_app/podcast/_partials/comment_actions_from_post_authenticated.php deleted file mode 100644 index a5783ef457..0000000000 --- a/themes/cp_app/podcast/_partials/comment_actions_from_post_authenticated.php +++ /dev/null @@ -1,22 +0,0 @@ -<footer> - <form action="<?= route_to('post-attempt-action', interact_as_actor()->username, $comment->id) ?>" method="POST" class="flex items-center gap-x-4"> - <button type="submit" name="action" value="favourite" class="inline-flex items-center hover:underline group" title="<?= lang( - 'Comment.likes', - [ - 'numberOfLikes' => $comment->likes_count, - ], -) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-400 group-hover:text-red-600') . $comment->likes_count ?></button> - <Button uri="<?= route_to('post', $podcast->handle, $comment->id) ?>" size="small"><?= lang('Comment.reply') ?></Button> - </form> - <?php if ($comment->replies_count): ?> - <?= anchor( - route_to('post', $podcast->handle, $comment->id), - icon('caret-down', 'text-xl mr-1') . lang('Comment.view_replies', [ - 'numberOfReplies' => $comment->replies_count, - ]), - [ - 'class' => 'inline-flex items-center text-xs hover:underline', - ] -) ?> - <?php endif; ?> -</footer> diff --git a/themes/cp_app/podcast/_partials/comment_authenticated.php b/themes/cp_app/podcast/_partials/comment_authenticated.php deleted file mode 100644 index 0958e5465e..0000000000 --- a/themes/cp_app/podcast/_partials/comment_authenticated.php +++ /dev/null @@ -1,26 +0,0 @@ -<article class="relative z-10 flex w-full px-4 py-2 rounded-2xl"> - <img src="<?= $comment->actor->avatar_image_url ?>" alt="<?= $comment->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> - <div class="flex-1"> - <header class="w-full mb-2 text-sm"> - <a href="<?= $comment->actor - ->uri ?>" class="flex items-baseline hover:underline" <?= $comment->actor->is_local - ? '' - : 'target="_blank" rel="noopener noreferrer"' ?>> - <span class="mr-2 font-semibold truncate"><?= $comment->actor - ->display_name ?></span> - <span class="text-sm text-gray-500 truncate">@<?= $comment->actor - ->username . - ($comment->actor->is_local - ? '' - : '@' . $comment->actor->domain) ?></span> - <?= relative_time($comment->created_at, 'text-xs text-gray-500 ml-auto') ?> - </a> - </header> - <div class="mb-2 post-content"><?= $comment->message_html ?></div> - <?php if ($comment->is_from_post): ?> - <?= $this->include('podcast/_partials/comment_actions_from_post_authenticated') ?> - <?php else: ?> - <?= $this->include('podcast/_partials/comment_actions_authenticated') ?> - <?php endif; ?> - </div> -</article> diff --git a/themes/cp_app/podcast/_partials/comment_card.php b/themes/cp_app/podcast/_partials/comment_card.php deleted file mode 100644 index c587234b85..0000000000 --- a/themes/cp_app/podcast/_partials/comment_card.php +++ /dev/null @@ -1,38 +0,0 @@ -<article class="relative z-10 flex w-full p-4 bg-white shadow rounded-2xl"> - <img src="<?= $comment->actor->avatar_image_url ?>" alt="<?= $comment->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> - <div class="flex-1"> - <header class="w-full mb-2 text-sm"> - <a href="<?= $comment->actor - ->uri ?>" class="flex items-baseline hover:underline" <?= $comment->actor->is_local - ? '' - : 'target="_blank" rel="noopener noreferrer"' ?>> - <span class="mr-2 font-semibold truncate"><?= $comment->actor - ->display_name ?></span> - <span class="text-sm text-gray-500 truncate">@<?= $comment->actor - ->username . - ($comment->actor->is_local - ? '' - : '@' . $comment->actor->domain) ?></span> - <?= relative_time($comment->created_at, 'text-xs text-gray-500 ml-auto') ?> - </a> - </header> - <div class="mb-2 post-content"><?= $comment->message_html ?></div> - <?php if ($comment->is_from_post): ?> - <?= $this->include('podcast/_partials/comment_actions_from_post') ?> - <?php else: ?> - <footer> - <button class="inline-flex items-center opacity-50 cursor-not-allowed" title="<?= lang( - 'Comment.likes', - [ - 'numberOfLikes' => $comment->likes_count, - ], - ) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-500') . lang( - 'Comment.likes', - [ - 'numberOfLikes' => $comment->likes_count, - ], - ) ?></button> - </footer> - <?php endif; ?> - </div> -</article> diff --git a/themes/cp_app/podcast/_partials/comment_reply_actions.php b/themes/cp_app/podcast/_partials/comment_reply_actions.php deleted file mode 100644 index 80213166d9..0000000000 --- a/themes/cp_app/podcast/_partials/comment_reply_actions.php +++ /dev/null @@ -1,20 +0,0 @@ -<footer class="flex items-center gap-x-4"> - <button type="submit" name="action" class="inline-flex items-center opacity-50 cursor-not-allowed" disabled="disabled" title="<?= lang( - 'Comment.likes', - [ - 'numberOfLikes' => $reply->likes_count, - ], -) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-500') . $reply->likes_count ?></button> - <?php if ($reply->replies_count): ?> - <?= anchor( - route_to('comment', $podcast->handle, $episode->slug, $reply->id), - icon('chat', 'text-2xl mr-1 text-gray-400') . $reply->replies_count, - [ - 'class' => 'inline-flex items-center hover:underline', - 'title' => lang('Comment.replies', [ - 'numberOfReplies' => $reply->replies_count, - ]), - ], -) ?> - <?php endif; ?> -</footer> diff --git a/themes/cp_app/podcast/_partials/comment_reply_actions_authenticated.php b/themes/cp_app/podcast/_partials/comment_reply_actions_authenticated.php deleted file mode 100644 index b11b9c01fd..0000000000 --- a/themes/cp_app/podcast/_partials/comment_reply_actions_authenticated.php +++ /dev/null @@ -1,11 +0,0 @@ -<footer> - <form action="<?= route_to('comment-attempt-like', interact_as_actor()->username, $episode->slug, $reply->id) ?>" method="POST" class="flex items-center gap-x-4"> - <button type="submit" name="action" class="inline-flex items-center hover:underline group" title="<?= lang( - 'Comment.likes', - [ - 'numberOfLikes' => $reply->likes_count, - ], -) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-400 group-hover:text-red-600') . $reply->likes_count ?></button> - <Button uri="<?= route_to('comment', $podcast->handle, $episode->slug, $reply->id) ?>" size="small"><?= lang('Comment.reply') ?></Button> - </form> -</footer> diff --git a/themes/cp_app/podcast/_partials/comment_reply_authenticated.php b/themes/cp_app/podcast/_partials/comment_reply_authenticated.php deleted file mode 100644 index 9602808831..0000000000 --- a/themes/cp_app/podcast/_partials/comment_reply_authenticated.php +++ /dev/null @@ -1,19 +0,0 @@ -<article class="flex px-6 py-4 bg-gray-50"> - <img src="<?= $reply->actor->avatar_image_url ?>" alt="<?= $reply->actor - ->display_name ?>" class="w-12 h-12 mr-4 rounded-full ring-gray-50 ring-2" /> - <div class="flex flex-col flex-1 min-w-0"> - <header class="flex items-center mb-2"> - <a href="<?= $reply->actor - ->uri ?>" class="mr-2 text-base font-semibold truncate hover:underline" <?= $reply - ->actor->is_local - ? '' - : 'target="_blank" rel="noopener noreferrer"' ?>><?= $reply->actor - ->display_name ?><span class="ml-1 text-sm font-normal text-gray-600">@<?= $reply - ->actor->username . - ($reply->actor->is_local ? '' : '@' . $reply->actor->domain) ?></span></a> - <?= relative_time($reply->created_at, 'flex-shrink-0 ml-auto text-xs text-gray-600') ?> - </header> - <p class="mb-2 post-content"><?= $reply->message_html ?></p> - <?= $this->include('podcast/_partials/comment_reply_actions_authenticated') ?> - </div> -</article> diff --git a/themes/cp_app/podcast/_partials/comment_with_replies.php b/themes/cp_app/podcast/_partials/comment_with_replies.php deleted file mode 100644 index 305b988fb1..0000000000 --- a/themes/cp_app/podcast/_partials/comment_with_replies.php +++ /dev/null @@ -1,10 +0,0 @@ -<?= $this->include('podcast/_partials/comment_card') ?> -<div class="-mt-2 overflow-hidden border-b border-l border-r post-replies rounded-b-xl"> - -<?php foreach ($comment->replies as $reply): ?> - <?= view('podcast/_partials/comment_reply', [ - 'reply' => $reply, - ]) ?> -<?php endforeach; ?> - -</div> diff --git a/themes/cp_app/podcast/_partials/comment_with_replies_authenticated.php b/themes/cp_app/podcast/_partials/comment_with_replies_authenticated.php deleted file mode 100644 index dfd69dcc93..0000000000 --- a/themes/cp_app/podcast/_partials/comment_with_replies_authenticated.php +++ /dev/null @@ -1,25 +0,0 @@ -<?= $this->include('podcast/_partials/comment_card_authenticated') ?> -<div class="-mt-2 overflow-hidden border-b border-l border-r post-replies rounded-b-xl"> -<form action="<?= route_to('comment-attempt-reply', $podcast->id, $episode->id, $comment->id) ?>" method="POST" class="flex px-6 pt-8 pb-4 bg-gray-50"> -<img src="<?= interact_as_actor() - ->avatar_image_url ?>" alt="<?= interact_as_actor() - ->display_name ?>" class="w-12 h-12 mr-4 rounded-full ring-gray-50 ring-2" /> -<div class="flex flex-col flex-1"> - <Forms.Textarea - name="message" - required="true" - class="w-full mb-4" - placeholder="<?= lang('Comment.form.reply_to_placeholder', [ - 'actorUsername' => $comment->actor->username, - ]) ?>" - rows="1" /> - <Button variant="primary" size="small" type="submit" name="action" value="reply"><?= lang('Comment.form.submit_reply') ?></Button> -</div> -</form> - -<?php foreach ($comment->replies as $reply): ?> - <?= view('podcast/_partials/comment_reply_authenticated', [ - 'reply' => $reply, - ]) ?> -<?php endforeach; ?> -</div> diff --git a/themes/cp_app/podcast/_partials/episode_card.php b/themes/cp_app/podcast/_partials/episode_card.php deleted file mode 100644 index a77d2b15d0..0000000000 --- a/themes/cp_app/podcast/_partials/episode_card.php +++ /dev/null @@ -1,43 +0,0 @@ -<article class="w-full mb-4 bg-white rounded-lg shadow"> - <div class="flex p-4"> - <div class="relative mr-2"> - <time class="absolute px-1 text-xs font-semibold text-white rounded bottom-2 right-2 bg-black/50" datetime="PT<?= $episode->audio_file_duration ?>S"> - <?= format_duration( - $episode->audio_file_duration, -) ?> - </time> - <img loading="lazy" src="<?= $episode->image - ->thumbnail_url ?>" alt="<?= $episode->title ?>" class="object-cover w-20 h-20 rounded-lg" /> - </div> - <div class="flex flex-col flex-1"> - <a class="flex justify-between text-sm" href="<?= $episode->link ?>"> - <h2 class="flex-1 font-semibold hover:underline"> - <?= episode_numbering( - $episode->number, - $episode->season_number, - 'text-xs font-semibold text-gray-600', - true, - ) ?> - <span class="mx-1">-</span> - <?= $episode->title ?> - </h2> - <?= relative_time($episode->published_at, 'text-xs whitespace-nowrap') ?> - </a> - <div class="flex mt-auto gap-x-4"> - <?= play_episode_button($episode->id, $episode->image->thumbnail_url, $episode->title, $podcast->title, $episode->audio_file_web_url, $episode->audio_file_mimetype, 'mt-auto') ?> - <?= anchor( - route_to('episode', $podcast->handle, $episode->slug), - icon('chat', 'text-xl mr-1 text-gray-400') . - $episode->comments_count, - [ - 'class' => - 'inline-flex items-center hover:underline', - 'title' => lang('Episode.number_of_comments', [ - 'numberOfComments' => $episode->comments_count, - ]), - ], - ) ?> - </div> - </div> - </div> -</article> diff --git a/themes/cp_app/podcast/_partials/episode_preview_card.php b/themes/cp_app/podcast/_partials/episode_preview_card.php deleted file mode 100644 index 6ad4b9092b..0000000000 --- a/themes/cp_app/podcast/_partials/episode_preview_card.php +++ /dev/null @@ -1,26 +0,0 @@ -<div class="flex"> - <div class="relative"> - <time class="absolute px-1 text-sm font-semibold text-white bg-black/50 bottom-2 right-2" datetime="PT<?= $episode->audio_file_duration ?>S"> - <?= format_duration($episode->audio_file_duration) ?> - </time> - <img - src="<?= $episode->image->thumbnail_url ?>" - alt="<?= $episode->title ?>" class="w-24 h-24"/> - </div> - <div class="flex flex-col flex-1 px-4 py-2 border-t border-b"> - <a href="<?= $episode->link ?>" class="flex justify-between flex-1"> - <div class="flex items-baseline font-semibold"> - <?= episode_numbering( - $episode->number, - $episode->season_number, - 'text-xs font-semibold text-gray-600', - true, -) ?> - <span class="mx-1">-</span> - <?= $episode->title ?> - </div> - <?= relative_time($episode->published_at, 'text-xs whitespace-nowrap') ?> - </a> - <?= play_episode_button($episode->id, $episode->image->thumbnail_url, $episode->title, $episode->podcast->title, $episode->audio_file_web_url, $episode->audio_file_mimetype, 'mt-auto') ?> - </div> -</div> diff --git a/themes/cp_app/podcast/_partials/funding_links_modal.php b/themes/cp_app/podcast/_partials/funding_links_modal.php new file mode 100644 index 0000000000..371b191bb7 --- /dev/null +++ b/themes/cp_app/podcast/_partials/funding_links_modal.php @@ -0,0 +1,39 @@ +<div id="funding-links" class="fixed top-0 left-0 z-50 flex items-center justify-center hidden w-screen h-screen"> + <div + class="absolute w-full h-full bg-pine-800/75" + role="button" + data-toggle="funding-links" + data-toggle-class="hidden" + aria-label="<?= lang('Common.close') ?>"></div> + <div class="z-10 w-full max-w-xl bg-white rounded-lg shadow-2xl"> + <div class="flex justify-between px-4 py-2 border-b"> + <h3 class="self-center text-lg"><?= lang('Podcast.funding_links', [ + 'podcastTitle' => $podcast->title, + ]) ?></h3> + <button + data-toggle="funding-links" + data-toggle-class="hidden" + aria-label="<?= lang('Common.close') ?>" + class="self-start p-1 text-2xl"><?= icon('close') ?></button> + </div> + <div class="flex flex-col items-start p-4 space-y-4"> + <?php foreach ($podcast->fundingPlatforms as $fundingPlatform): ?> + <?php if ($fundingPlatform->is_visible): ?> + <a + href="<?= $fundingPlatform->link_url ?>" + title="<?= $fundingPlatform->link_content ?>" + target="_blank" + rel="noopener noreferrer" + class="inline-flex items-center font-semibold text-pine-900"> + <?= icon( + $fundingPlatform->type . + '/' . + $fundingPlatform->slug, + 'mr-2', + ) . $fundingPlatform->link_url ?> + </a> + <?php endif; ?> + <?php endforeach; ?> + </div> + </div> +</div> \ No newline at end of file diff --git a/themes/cp_app/podcast/_partials/header.php b/themes/cp_app/podcast/_partials/header.php deleted file mode 100644 index f5d57ea96b..0000000000 --- a/themes/cp_app/podcast/_partials/header.php +++ /dev/null @@ -1,58 +0,0 @@ -<header id="main-header" class="fixed top-0 left-0 flex-col flex-shrink-0 h-screen transform -translate-x-full sm:left-auto sm:-translate-x-0 sm:sticky w-80 sm:w-64 lg:w-80 xl:w-112 sm:flex"> - <img src="<?= $podcast->actor - ->cover_image_url ?>" alt="" class="object-cover w-full h-48 bg-pine-800"/> - <div class="flex items-center justify-between px-4 py-2 mb-4 lg:px-6 -mt-14 lg:-mt-16 xl:-mt-20"> - <img src="<?= $podcast->image - ->thumbnail_url ?>" alt="<?= $podcast->title ?>" class="h-24 rounded-full shadow-xl xl:h-36 lg:h-28 ring-4 ring-pine-50" /> - <?= anchor_popup( - route_to('follow', $podcast->handle), - icon( - 'social/castopod', - 'mr-2 text-xl text-pink-200 group-hover:text-pink-50', - ) . lang('Podcast.follow'), - [ - 'width' => 420, - 'height' => 620, - 'class' => - 'group inline-flex items-center px-4 py-2 text-xs tracking-wider font-semibold text-white uppercase rounded-full shadow focus:outline-none focus:ring bg-rose-600', - ], - ) ?> - </div> - <div class="px-6"> - <h1 class="inline-flex items-center text-2xl font-bold leading-none font-display"><?= $podcast->title . - ($podcast->parental_advisory === 'explicit' - ? '<span class="px-1 ml-2 text-xs font-semibold leading-tight tracking-wider text-gray-600 uppercase border-2 border-gray-500">' . - lang('Common.explicit') . - '</span>' - : '') ?></h1> - <p class="mb-4 font-semibold text-gray-600">@<?= $podcast->handle ?></p> - <div class="mb-2"><?= $podcast->description_html ?></div> - <?= location_link($podcast->location, 'text-sm mb-4') ?> - <div class="mb-6 space-x-4"> - <span class="px-2 py-1 text-sm text-gray-800 bg-gray-200"> - <?= lang( - 'Podcast.category_options.' . $podcast->category->code, - ) ?> - </span> - <?php foreach ($podcast->other_categories as $other_category): ?> - <span class="px-2 py-1 text-sm text-gray-800 bg-gray-200"> - <?= lang( - 'Podcast.category_options.' . $other_category->code, - ) ?> - </span> - <?php endforeach; ?> - </div> - <?= person_list($podcast->persons, 'mb-6') ?> - <div class="space-x-4"> - <a href="#" class="hover:underline"><?= lang('Podcast.followers', [ - 'numberOfFollowers' => $podcast->actor->followers_count, - ]) ?></a> - <a href="<?= route_to( - 'podcast-activity', - $podcast->handle, - ) ?>" class="hover:underline"><?= lang('Podcast.posts', [ - 'numberOfPosts' => $podcast->actor->posts_count, - ]) ?></a> - </div> - </div> -</header> diff --git a/themes/cp_app/podcast/_partials/navigation.php b/themes/cp_app/podcast/_partials/navigation.php new file mode 100644 index 0000000000..21de984140 --- /dev/null +++ b/themes/cp_app/podcast/_partials/navigation.php @@ -0,0 +1,24 @@ +<?php declare(strict_types=1); + +$navigationItems = [ + [ + 'uri' => route_to('podcast-activity', $podcast->handle), + 'label' => lang('Podcast.activity'), + ], + [ + 'uri' => route_to('podcast-episodes', $podcast->handle), + 'label' => lang('Podcast.episodes'), + ], + [ + 'uri' => route_to('podcast-about', $podcast->handle), + 'label' => lang('Podcast.about'), + ], +] +?> +<nav class="sticky z-40 flex col-start-2 px-4 pt-8 bg-white shadow gap-x-2 md:gap-x-4 md:px-8 -top-8 md:-top-12 md:rounded-b-xl md:pt-12 "> + <?php foreach ($navigationItems as $item): ?> + <?php $isActive = url_is($item['uri']); ?> + <a href="<?= $item['uri'] ?>" class="px-4 py-1 text-sm font-semibold uppercase border-b-4<?= $isActive ? ' border-b-4 text-pine-500 border-pine-500' : ' text-gray-500 hover:text-gray-900 hover:border-gray-200 border-transparent' ?>"><?= $item['label'] ?></a> + <?php endforeach; ?> + <button type="button" class="p-2 ml-auto rotate-180 rounded-full sm:hidden focus:outline-none focus:ring-2 focus:ring-pine-500 focus:ring-offset-2 focus:ring-offset-pine-100"><?= icon('menu') ?></button> +</nav> \ No newline at end of file diff --git a/themes/cp_app/podcast/_partials/post.php b/themes/cp_app/podcast/_partials/post.php deleted file mode 100644 index 3d40f4604b..0000000000 --- a/themes/cp_app/podcast/_partials/post.php +++ /dev/null @@ -1,36 +0,0 @@ -<article class="relative z-10 w-full bg-white shadow rounded-2xl"> - <header class="flex px-6 py-4"> - <img src="<?= $post->actor - ->avatar_image_url ?>" alt="<?= $post->actor->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> - <div class="flex flex-col min-w-0"> - <a href="<?= $post->actor - ->uri ?>" class="flex items-baseline hover:underline" <?= $post - ->actor->is_local - ? '' - : 'target="_blank" rel="noopener noreferrer"' ?>> - <span class="mr-2 font-semibold truncate"><?= $post->actor - ->display_name ?></span> - <span class="text-sm text-gray-500 truncate">@<?= $post->actor - ->username . - ($post->actor->is_local - ? '' - : '@' . $post->actor->domain) ?></span> - </a> - <a href="<?= route_to('post', $podcast->handle, $post->id) ?>" - class="text-xs text-gray-500"> - <?= relative_time($post->published_at) ?> - </a> - </div> - </header> - <div class="px-6 mb-4 post-content"><?= $post->message_html ?></div> - <?php if ($post->episode_id): ?> - <?= view('podcast/_partials/episode_preview_card', [ - 'episode' => $post->episode, - ]) ?> - <?php elseif ($post->has_preview_card): ?> - <?= view('podcast/_partials/preview_card', [ - 'preview_card' => $post->preview_card, - ]) ?> - <?php endif; ?> - <?= $this->include('podcast/_partials/post_actions') ?> -</article> diff --git a/themes/cp_app/podcast/_partials/post_actions.php b/themes/cp_app/podcast/_partials/post_actions.php deleted file mode 100644 index bf29cccca3..0000000000 --- a/themes/cp_app/podcast/_partials/post_actions.php +++ /dev/null @@ -1,36 +0,0 @@ -<footer class="flex justify-around px-6 py-3"> - <?= anchor( - route_to('post', $podcast->handle, $post->id), - icon('chat', 'text-2xl mr-1 text-gray-400') . $post->replies_count, - [ - 'class' => 'inline-flex items-center hover:underline', - 'title' => lang('Post.replies', [ - 'numberOfReplies' => $post->replies_count, - ]), - ], -) ?> - <?= anchor_popup( - route_to('post-remote-action', $podcast->handle, $post->id, 'reblog'), - icon('repeat', 'text-2xl mr-1 text-gray-400') . $post->reblogs_count, - [ - 'class' => 'inline-flex items-center hover:underline', - 'width' => 420, - 'height' => 620, - 'title' => lang('Post.reblogs', [ - 'numberOfReblogs' => $post->reblogs_count, - ]), - ], -) ?> - <?= anchor_popup( - route_to('post-remote-action', $podcast->handle, $post->id, 'favourite'), - icon('heart', 'text-2xl mr-1 text-gray-400') . $post->favourites_count, - [ - 'class' => 'inline-flex items-center hover:underline', - 'width' => 420, - 'height' => 620, - 'title' => lang('Post.favourites', [ - 'numberOfFavourites' => $post->favourites_count, - ]), - ], -) ?> -</footer> diff --git a/themes/cp_app/podcast/_partials/post_actions_authenticated.php b/themes/cp_app/podcast/_partials/post_actions_authenticated.php deleted file mode 100644 index a50c87db2e..0000000000 --- a/themes/cp_app/podcast/_partials/post_actions_authenticated.php +++ /dev/null @@ -1,92 +0,0 @@ -<footer class="px-6 py-3"> - <form action="<?= route_to( - 'post-attempt-action', - interact_as_actor() - ->username, - $post->id, -) ?>" method="POST" class="flex justify-around"> - <?= csrf_field() ?> - <?= anchor( - route_to('post', $podcast->handle, $post->id), - icon('chat', 'text-2xl mr-1 text-gray-400') . $post->replies_count, - [ - 'class' => 'inline-flex items-center hover:underline', - 'title' => lang('Post.replies', [ - 'numberOfReplies' => $post->replies_count, - ]), - ], -) ?> - <button type="submit" name="action" value="reblog" class="inline-flex items-center hover:underline" title="<?= lang( - 'Post.reblogs', - [ - 'numberOfReblogs' => $post->reblogs_count, - ], -) ?>"><?= icon('repeat', 'text-2xl mr-1 text-gray-400') . - $post->reblogs_count ?></button> - <button type="submit" name="action" value="favourite" class="inline-flex items-center hover:underline" title="<?= lang( - 'Post.favourites', - [ - 'numberOfFavourites' => $post->favourites_count, - ], - ) ?>"><?= icon('heart', 'text-2xl mr-1 text-gray-400') . - $post->favourites_count ?></button> - <button id="<?= $post->id . - '-more-dropdown' ?>" type="button" class="px-2 py-1 text-2xl text-gray-500 outline-none focus:ring" data-dropdown="button" data-dropdown-target="<?= $post->id . - '-more-dropdown-menu' ?>" aria-label="<?= lang( - 'Common.more', - ) ?>" aria-haspopup="true" aria-expanded="false"><?= icon('more') ?> - </button> - </form> - <nav id="<?= $post->id . - '-more-dropdown-menu' ?>" class="flex flex-col py-2 text-sm bg-white border rounded-lg shadow" aria-labelledby="<?= $post->id . - '-more-dropdown' ?>" data-dropdown="menu" data-dropdown-placement="bottom"> - <?= anchor( - route_to('post', $podcast->handle, $post->id), - lang('Post.expand'), - [ - 'class' => 'px-4 py-1 hover:bg-gray-100', - ], - ) ?> - <form action="<?= route_to( - 'post-attempt-block-actor', - interact_as_actor() - ->username, - $post->id, - ) ?>" method="POST"> - <?= csrf_field() ?> - <button class="w-full px-4 py-1 text-left hover:bg-gray-100"><?= lang( - 'Post.block_actor', - [ - 'actorUsername' => $post->actor->username, - ], - ) ?></button> - </form> - <form action="<?= route_to( - 'post-attempt-block-domain', - interact_as_actor() - ->username, - $post->id, - ) ?>" method="POST"> - <?= csrf_field() ?> - <button class="w-full px-4 py-1 text-left hover:bg-gray-100"><?= lang( - 'Post.block_domain', - [ - 'actorDomain' => $post->actor->domain, - ], - ) ?></button> - </form> - <?php if ($post->actor->is_local): ?> - <hr class="my-2" /> - <form action="<?= route_to( - 'post-attempt-delete', - $post->actor->username, - $post->id, - ) ?>" method="POST"> - <?= csrf_field() ?> - <button class="w-full px-4 py-1 font-semibold text-left text-red-600 hover:bg-gray-100"><?= lang( - 'Post.delete', - ) ?></button> - </form> - <?php endif; ?> - </nav> -</footer> diff --git a/themes/cp_app/podcast/_partials/post_with_replies.php b/themes/cp_app/podcast/_partials/post_with_replies.php deleted file mode 100644 index ddf3489fe5..0000000000 --- a/themes/cp_app/podcast/_partials/post_with_replies.php +++ /dev/null @@ -1,27 +0,0 @@ -<?= $this->include('podcast/_partials/post') ?> -<div class="-mt-2 overflow-hidden border-b border-l border-r post-replies rounded-b-xl"> - -<div class="px-6 pt-8 pb-4 bg-gray-50"> -<?= anchor_popup( - route_to('post-remote-action', $podcast->handle, $post->id, 'reply'), - lang('Post.reply_to', [ - 'actorUsername' => $post->actor->username, - ]), - [ - 'class' => - 'text-center justify-center font-semibold rounded-full shadow relative z-10 px-4 py-2 w-full bg-rose-600 text-white inline-flex items-center hover:bg-rose-700', - 'width' => 420, - 'height' => 620, - ], -) ?> -</div> - - -<?php if ($post->has_replies): ?> - <?php foreach ($post->replies as $reply): ?> - <?= view('podcast/_partials/reply', [ - 'reply' => $reply, - ]) ?> - <?php endforeach; ?> -<?php endif; ?> -</div> diff --git a/themes/cp_app/podcast/_partials/post_with_replies_authenticated.php b/themes/cp_app/podcast/_partials/post_with_replies_authenticated.php deleted file mode 100644 index 9dbd498a43..0000000000 --- a/themes/cp_app/podcast/_partials/post_with_replies_authenticated.php +++ /dev/null @@ -1,27 +0,0 @@ -<?= $this->include('podcast/_partials/post_authenticated') ?> -<div class="-mt-2 overflow-hidden border-b border-l border-r post-replies rounded-b-xl"> -<form action="<?= route_to('post-attempt-action', interact_as_actor()->username, $post->id) ?>" method="POST" class="flex px-6 pt-8 pb-4 bg-gray-50" > -<img src="<?= interact_as_actor() - ->avatar_image_url ?>" alt="<?= interact_as_actor() - ->display_name ?>" class="w-12 h-12 mr-4 rounded-full ring-gray-50 ring-2" /> -<div class="flex flex-col flex-1"> - <Forms.Textarea - name="message" - class="w-full mb-4" - required="true" - placeholder="<?= lang('Post.form.reply_to_placeholder', [ - 'actorUsername' => $post->actor->username, - ]) ?>" - rows="1" /> - <Button variant="primary" size="small" type="submit" name="action" value="reply" class="self-end"><?= lang('Post.form.submit_reply') ?></Button> -</div> -</form> - -<?php if ($post->has_replies): ?> - <?php foreach ($post->replies as $reply): ?> - <?= view('podcast/_partials/reply_authenticated', [ - 'reply' => $reply, - ]) ?> - <?php endforeach; ?> -<?php endif; ?> -</div> diff --git a/themes/cp_app/podcast/_partials/reblog_authenticated.php b/themes/cp_app/podcast/_partials/reblog_authenticated.php deleted file mode 100644 index cc8a4b4132..0000000000 --- a/themes/cp_app/podcast/_partials/reblog_authenticated.php +++ /dev/null @@ -1,43 +0,0 @@ -<article class="relative z-10 w-full bg-white shadow rounded-2xl"> - <p class="inline-flex px-6 pt-4 text-xs text-gray-700"><?= icon( - 'repeat', - 'text-lg mr-2 text-gray-400', -) . - lang('Post.actor_shared', [ - 'actor' => $post->actor->display_name, - ]) ?></p> - <header class="flex px-6 py-4"> - <img src="<?= $post->actor - ->avatar_image_url ?>" alt="<?= $post->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> - <div class="flex flex-col min-w-0"> - <a href="<?= $post->actor - ->uri ?>" class="flex items-baseline hover:underline" <?= $post - ->actor->is_local - ? '' - : 'target="_blank" rel="noopener noreferrer"' ?>> - <span class="mr-2 font-semibold truncate"><?= $post->actor - ->display_name ?></span> - <span class="text-sm text-gray-500 truncate">@<?= $post->actor - ->username . - ($post->actor->is_local - ? '' - : '@' . $post->actor->domain) ?></span> - </a> - <a href="<?= route_to('post', $podcast->handle, $post->id) ?>" - class="text-xs text-gray-500"> - <?= relative_time($post->published_at) ?> - </a> - </div> - </header> - <div class="px-6 mb-4 post-content"><?= $post->message_html ?></div> - <?php if ($post->episode_id): ?> - <?= view('podcast/_partials/episode_preview_card', [ - 'episode' => $post->episode, - ]) ?> - <?php elseif ($post->has_preview_card): ?> - <?= view('podcast/_partials/preview_card', [ - 'preview_card' => $post->preview_card, - ]) ?> - <?php endif; ?> - <?= $this->include('podcast/_partials/post_actions_authenticated') ?> -</article> diff --git a/themes/cp_app/podcast/_partials/reply_actions.php b/themes/cp_app/podcast/_partials/reply_actions.php deleted file mode 100644 index a11b9da360..0000000000 --- a/themes/cp_app/podcast/_partials/reply_actions.php +++ /dev/null @@ -1,36 +0,0 @@ -<footer class="mt-2 space-x-6 text-sm"> - <?= anchor( - route_to('post', $podcast->handle, $reply->id), - icon('chat', 'text-xl mr-1 text-gray-400') . $reply->replies_count, - [ - 'class' => 'inline-flex items-center hover:underline', - 'title' => lang('Post.replies', [ - 'numberOfReplies' => $reply->replies_count, - ]), - ], -) ?> - <?= anchor_popup( - route_to('post-remote-action', $podcast->handle, $reply->id, 'reblog'), - icon('repeat', 'text-xl mr-1 text-gray-400') . $reply->reblogs_count, - [ - 'class' => 'inline-flex items-center hover:underline', - 'width' => 420, - 'height' => 620, - 'title' => lang('Post.reblogs', [ - 'numberOfReblogs' => $reply->reblogs_count, - ]), - ], -) ?> - <?= anchor_popup( - route_to('post-remote-action', $podcast->handle, $reply->id, 'favourite'), - icon('heart', 'text-xl mr-1 text-gray-400') . $reply->favourites_count, - [ - 'class' => 'inline-flex items-center hover:underline', - 'width' => 420, - 'height' => 620, - 'title' => lang('Post.favourites', [ - 'numberOfFavourites' => $reply->favourites_count, - ]), - ], -) ?> -</footer> diff --git a/themes/cp_app/podcast/_partials/reply_actions_authenticated.php b/themes/cp_app/podcast/_partials/reply_actions_authenticated.php deleted file mode 100644 index ea54f20d23..0000000000 --- a/themes/cp_app/podcast/_partials/reply_actions_authenticated.php +++ /dev/null @@ -1,92 +0,0 @@ -<footer class="mt-2 text-sm"> - <form action="<?= route_to( - 'post-attempt-action', - interact_as_actor() - ->username, - $reply->id, -) ?>" method="POST" class="flex items-start"> - <?= csrf_field() ?> - <?= anchor( - route_to('post', $podcast->handle, $reply->id), - icon('chat', 'text-xl mr-1 text-gray-400') . $reply->replies_count, - [ - 'class' => 'inline-flex items-center mr-6 hover:underline', - 'title' => lang('Post.replies', [ - 'numberOfReplies' => $reply->replies_count, - ]), - ], -) ?> - <button type="submit" name="action" value="reblog" class="inline-flex items-center mr-6 hover:underline" title="<?= lang( - 'Post.reblogs', - [ - 'numberOfReblogs' => $reply->reblogs_count, - ], -) ?>"><?= icon('repeat', 'text-xl mr-1 text-gray-400') . - $reply->reblogs_count ?></button> - <button type="submit" name="action" value="favourite" class="inline-flex items-center mr-6 hover:underline" title="<?= lang( - 'Post.favourites', - [ - 'numberOfFavourites' => $reply->favourites_count, - ], - ) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-400') . - $reply->favourites_count ?></button> - <button id="<?= $reply->id . - '-more-dropdown' ?>" type="button" class="text-xl text-gray-500 outline-none focus:ring" data-dropdown="button" data-dropdown-target="<?= $reply->id . - '-more-dropdown-menu' ?>" aria-label="<?= lang( - 'Common.more', - ) ?>" aria-haspopup="true" aria-expanded="false"><?= icon('more') ?> - </button> - </form> - <nav id="<?= $reply->id . - '-more-dropdown-menu' ?>" class="flex flex-col py-2 text-sm bg-white border rounded-lg shadow" aria-labelledby="<?= $reply->id . - '-more-dropdown' ?>" data-dropdown="menu" data-dropdown-placement="bottom"> - <?= anchor( - route_to('post', $podcast->handle, $reply->id), - lang('Post.expand'), - [ - 'class' => 'px-4 py-1 hover:bg-gray-100', - ], - ) ?> - <form action="<?= route_to( - 'post-attempt-block-actor', - interact_as_actor() - ->username, - $reply->id, - ) ?>" method="POST"> - <?= csrf_field() ?> - <button class="w-full px-4 py-1 text-left hover:bg-gray-100"><?= lang( - 'Post.block_actor', - [ - 'actorUsername' => $reply->actor->username, - ], - ) ?></button> - </form> - <form action="<?= route_to( - 'post-attempt-block-domain', - interact_as_actor() - ->username, - $reply->id, - ) ?>" method="POST"> - <?= csrf_field() ?> - <button class="w-full px-4 py-1 text-left hover:bg-gray-100"><?= lang( - 'Post.block_domain', - [ - 'actorDomain' => $reply->actor->domain, - ], - ) ?></button> - </form> - <?php if ($reply->actor->is_local): ?> - <hr class="my-2" /> - <form action="<?= route_to( - 'post-attempt-delete', - $reply->actor->username, - $reply->id, - ) ?>" method="POST"> - <?= csrf_field() ?> - <button class="w-full px-4 py-1 font-semibold text-left text-red-600 hover:bg-gray-100"><?= lang( - 'Post.delete', - ) ?></button> - </form> - <?php endif; ?> - </nav> -</footer> diff --git a/themes/cp_app/podcast/_partials/reply_authenticated.php b/themes/cp_app/podcast/_partials/reply_authenticated.php deleted file mode 100644 index 582c759481..0000000000 --- a/themes/cp_app/podcast/_partials/reply_authenticated.php +++ /dev/null @@ -1,24 +0,0 @@ -<article class="flex px-6 py-4 bg-gray-50"> - <img src="<?= $reply->actor->avatar_image_url ?>" alt="<?= $reply->actor - ->display_name ?>" class="w-12 h-12 mr-4 rounded-full ring-gray-50 ring-2" /> - <div class="flex flex-col flex-1 min-w-0"> - <header class="flex items-center mb-2"> - <a href="<?= $reply->actor - ->uri ?>" class="mr-2 text-base font-semibold truncate hover:underline" <?= $reply - ->actor->is_local - ? '' - : 'target="_blank" rel="noopener noreferrer"' ?>><?= $reply->actor - ->display_name ?><span class="ml-1 text-sm font-normal text-gray-600">@<?= $reply - ->actor->username . - ($reply->actor->is_local ? '' : '@' . $reply->actor->domain) ?></span></a> - <?= relative_time($post->published_at, 'flex-shrink-0 ml-auto text-xs text-gray-600') ?> - </header> - <p class="mb-2 post-content"><?= $reply->message_html ?></p> - <?php if ($reply->has_preview_card): ?> - <?= view('podcast/_partials/preview_card', [ - 'preview_card' => $reply->preview_card, - ]) ?> - <?php endif; ?> - <?= $this->include('podcast/_partials/reply_actions_authenticated') ?> - </div> -</article> diff --git a/themes/cp_app/podcast/_partials/sidebar.php b/themes/cp_app/podcast/_partials/sidebar.php index 6e0853f59e..2af44f6b48 100644 --- a/themes/cp_app/podcast/_partials/sidebar.php +++ b/themes/cp_app/podcast/_partials/sidebar.php @@ -1,65 +1,49 @@ -<aside id="main-sidebar" class="fixed top-0 right-0 flex flex-col items-start flex-shrink-0 w-64 h-screen px-6 py-4 overflow-y-auto transform translate-x-full lg:sticky lg:translate-x-0"> - <?php if ( - in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true) - ): ?> - <h2 class="mb-2 text-sm font-semibold"><?= lang( - 'Podcast.sponsor_title', - ) ?></h2> - <button - class="inline-flex items-center px-2 py-1 mb-8 text-sm font-semibold text-gray-600 border rounded-full shadow-sm focus:outline-none focus:ring focus:ring-pine-600 hover:bg-rose-200 hover:text-gray-800 border-rose-600 bg-rose-100" - data-toggle="funding-links" - data-toggle-class="hidden"><?= icon('heart', 'mr-2 text-rose-600') . - lang('Podcast.sponsor') ?></button> - <?php endif; ?> - - <?php if ( - in_array(true, array_column($podcast->socialPlatforms, 'is_visible'), true) - ): ?> - <h2 class="mb-2 text-sm font-semibold"> <?= lang('Podcast.find_on', [ - 'podcastTitle' => $podcast->title, - ]) ?></h2> - <div class="grid items-center justify-center grid-cols-5 gap-3 mb-8"> - <?php foreach ($podcast->socialPlatforms as $socialPlatform): ?> - <?php if ($socialPlatform->is_visible): ?> - <?= anchor( - $socialPlatform->link_url, - icon($socialPlatform->type . '/' . $socialPlatform->slug), - [ - 'class' => 'text-2xl text-gray-500 hover:text-gray-700', - 'target' => '_blank', - 'rel' => 'noopener noreferrer', - 'data-toggle' => 'tooltip', - 'data-placement' => 'bottom', - 'title' => $socialPlatform->label, - ], - ) ?> +<aside id="podcast-sidebar" class="sticky hidden col-span-1 sm:block top-12"> + <div class="absolute z-0 w-full h-full sm:hidden bg-pine-800/50"></div> + <div class="z-10 bg-pine-50"> + <a href="<?= route_to('podcast_feed', $podcast->handle) ?>" class="inline-flex items-center mb-6 text-sm font-semibold text-pine-800 group" target="_blank" rel="noopener noreferrer"> + <?= icon('rss', ' mr-2 bg-orange-500 text-xl text-white group-hover:bg-orange-700 p-1 w-6 h-6 inline-flex items-center justify-center rounded-lg') . lang('Podcast.feed') ?> + </a> + <?php if ( + in_array(true, array_column($podcast->socialPlatforms, 'is_visible'), true) + ): ?> + <h2 class="mb-2 font-bold font-display text-pine-900"> <?= lang('Podcast.find_on', [ + 'podcastTitle' => $podcast->title, + ]) ?></h2> + <div class="grid items-center justify-center grid-cols-6 gap-3 mb-6"> + <?php foreach ($podcast->socialPlatforms as $socialPlatform): ?> + <?php if ($socialPlatform->is_visible): ?> + <?= anchor( + $socialPlatform->link_url, + icon("{$socialPlatform->type}/{$socialPlatform->slug}"), + [ + 'class' => 'text-2xl text-gray-500 hover:text-gray-700 w-8 h-8 items-center inline-flex justify-center', + 'target' => '_blank', + 'rel' => 'noopener noreferrer', + 'data-toggle' => 'tooltip', + 'data-placement' => 'bottom', + 'title' => $socialPlatform->label, + ], + ) ?> + <?php endif; ?> + <?php endforeach; ?> + </div> <?php endif; ?> - <?php endforeach; ?> - </div> - <?php endif; ?> - <h2 class="mb-2 text-sm font-semibold"><?= lang('Podcast.listen_on') ?></h2> - <div class="grid items-center justify-center grid-cols-5 gap-3 mb-8"> - <?= anchor(route_to('podcast_feed', $podcast->handle), icon('rss'), [ - 'class' => - 'bg-yellow-500 text-xl text-yellow-900 hover:bg-yellow-600 w-8 h-8 inline-flex items-center justify-center rounded-lg', - 'target' => '_blank', - 'rel' => 'noopener noreferrer', - 'data-toggle' => 'tooltip', - 'data-placement' => 'bottom', - 'title' => lang('Podcast.feed'), - ]) ?> - <?php foreach ($podcast->podcastingPlatforms as $podcastingPlatform): ?> - <?php if ($podcastingPlatform->is_visible): ?> - <?= anchor( + <?php if ( + in_array(true, array_column($podcast->podcastingPlatforms, 'is_visible'), true) + ): ?> + <h2 class="mb-2 font-bold font-display text-pine-900"><?= lang('Podcast.listen_on') ?></h2> + <div class="grid items-center justify-center grid-cols-6 gap-3 mb-6"> + <?php foreach ($podcast->podcastingPlatforms as $podcastingPlatform): ?> + <?php if ($podcastingPlatform->is_visible): ?> + <?= anchor( $podcastingPlatform->link_url, icon( - $podcastingPlatform->type . - '/' . - $podcastingPlatform->slug, + "{$podcastingPlatform->type}/{$podcastingPlatform->slug}", ), [ - 'class' => 'text-2xl text-gray-500 hover:text-gray-700', + 'class' => 'text-2xl text-gray-500 hover:text-gray-700 w-8 h-8 items-center inline-flex justify-center', 'target' => '_blank', 'rel' => 'noopener noreferrer', 'data-toggle' => 'tooltip', @@ -67,19 +51,19 @@ 'title' => $podcastingPlatform->label, ], ) ?> - <?php endif; ?> - <?php endforeach; ?> - </div> - <footer class="px-2 py-4 mt-auto text-gray-600 border-t"> - <div class="container flex flex-col justify-between mx-auto text-xs"> + <?php endif; ?> + <?php endforeach; ?> + </div> + <?php endif; ?> + <footer class="flex flex-col items-center py-2 text-xs text-center text-gray-600 border-t"> <?= render_page_links('inline-flex mb-2 flex-wrap gap-y-1') ?> <div class="flex flex-col"> <p><?= $podcast->copyright ?></p> <p><?= lang('Common.powered_by', [ 'castopod' => - '<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>', + '<a class="inline-flex font-semibold text-gray-500 hover:underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod' . icon('social/castopod', 'ml-1 text-lg') . '</a>', ]) ?></p> </div> - </div> - </footer> -</aside> + </footer> + </div> +</aside> \ No newline at end of file diff --git a/themes/cp_app/podcast/about.php b/themes/cp_app/podcast/about.php index 436beeae4b..a616a64f5a 100644 --- a/themes/cp_app/podcast/about.php +++ b/themes/cp_app/podcast/about.php @@ -29,7 +29,53 @@ <?= $this->section('content') ?> +<div class="px-2 sm:px-4"> +<div class="mb-2"><?= $podcast->description_html ?></div> +<div class="flex gap-x-4 gap-y-2"> + <span class="px-2 py-1 text-sm font-semibold text-gray-800 bg-gray-100 border"> + <?= lang( + 'Podcast.category_options.' . $podcast->category->code, + ) ?> + </span> + <?php foreach ($podcast->other_categories as $other_category): ?> + <span class="px-2 py-1 text-sm font-semibold text-gray-800 bg-gray-100 border"> + <?= lang( + 'Podcast.category_options.' . $other_category->code, + ) ?> + </span> + <?php endforeach; ?> +</div> +<div class="flex items-center mt-4 gap-x-8"> + <?php if ($podcast->persons !== []): ?> + <button class="flex items-center text-xs font-semibold gap-x-2 hover:underline" data-toggle="persons-list" data-toggle-class="hidden"> + <div class="inline-flex flex-row-reverse"> + <?php $i = 0; ?> + <?php foreach ($podcast->persons as $person): ?> + <img src="<?= $person->image->thumbnail_url ?>" alt="<?= $person->full_name ?>" class="object-cover w-8 h-8 -ml-5 border-2 rounded-full border-pine-100 last:ml-0" /> + <?php $i++; if ($i === 3) { + break; + }?> + <?php endforeach; ?> + </div> + <?= lang('Podcast.persons', [ + 'personsCount' => count($podcast->persons), + ]) ?> + </button> + <?php endif; ?> + <?php if ($podcast->location): ?> + <?= location_link($podcast->location, 'text-xs font-semibold p-2') ?> + <?php endif; ?> +</div> +</div> + + +<?= view('_persons_modal', [ + 'title' => lang('Podcast.persons_list', [ + 'podcastTitle' => $podcast->title, + ]), + 'persons' => $podcast->persons, +]) ?> <?= $this->endSection() ?> diff --git a/themes/cp_app/podcast/activity.php b/themes/cp_app/podcast/activity.php index 1a1b5176fa..1fa7a529c2 100644 --- a/themes/cp_app/podcast/activity.php +++ b/themes/cp_app/podcast/activity.php @@ -27,21 +27,52 @@ <?= $this->section('content') ?> -<section class="max-w-2xl mx-auto space-y-8"> +<section class="w-full"> -<?php foreach ($posts as $post): ?> - <?php if ($post->reblog_of_id !== null): ?> - <?= view('podcast/_partials/reblog', [ - 'post' => $post->reblog_of_post, - 'podcast' => $podcast, +<?php if (can_user_interact()): ?> + + <form action="<?= route_to('post-attempt-create', interact_as_actor()->username) ?>" method="POST" class="flex p-4 bg-white shadow rounded-xl"> + <?= csrf_field() ?> + + <?= view('_message_block') ?> + + <img src="<?= interact_as_actor() + ->avatar_image_url ?>" alt="<?= interact_as_actor() + ->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> + <div class="flex flex-col flex-1 min-w-0 gap-y-2"> + <Forms.Textarea + name="message" + required="true" + placeholder="<?= lang('Post.form.message_placeholder') ?>" + rows="2" /> + <Forms.Input + name="episode_url" + type="url" + placeholder="<?= lang('Post.form.episode_url_placeholder') . ' (' . lang('Common.optional') . ')' ?>" /> + <Button variant="primary" size="small" type="submit" class="self-end" iconRight="send-plane"><?= lang('Post.form.submit') ?></Button> + </div> +</form> +<hr class="my-4 border-2 border-pine-100"> + +<?php endif; ?> +<div class="flex flex-col gap-y-6"> + <?php foreach ($posts as $key => $post): ?> + <?php if ($post->reblog_of_id !== null): ?> + <?= view('post/_partials/reblog', [ + 'index' => $key, + 'post' => $post->reblog_of_post, + 'podcast' => $podcast, ]) ?> - <?php else: ?> - <?= view('podcast/_partials/post', [ - 'post' => $post, - 'podcast' => $podcast, + <?php else: ?> + <?= view('post/_partials/card', [ + 'index' => $key, + 'post' => $post, + 'podcast' => $podcast, ]) ?> - <?php endif; ?> -<?php endforeach; ?> + <?php endif; ?> + <?php endforeach; ?> +</div> + </section> <?= $this->endSection() ?> diff --git a/themes/cp_app/podcast/activity_authenticated.php b/themes/cp_app/podcast/activity_authenticated.php deleted file mode 100644 index 8e582c7020..0000000000 --- a/themes/cp_app/podcast/activity_authenticated.php +++ /dev/null @@ -1,73 +0,0 @@ -<?= $this->extend('podcast/_layout_authenticated') ?> - -<?= $this->section('meta-tags') ?> -<link type="application/rss+xml" rel="alternate" title="<?= $podcast->title ?>" href="<?= $podcast->feed_url ?>"/> - -<title><?= $podcast->title ?></title> -<meta name="description" content="<?= htmlspecialchars( - $podcast->description, -) ?>" /> -<link rel="shortcut icon" type="image/png" href="/favicon.ico" /> -<link rel="canonical" href="<?= current_url() ?>" /> -<meta property="og:title" content="<?= $podcast->title ?>" /> -<meta property="og:description" content="<?= $podcast->description ?>" /> -<meta property="og:locale" content="<?= $podcast->language_code ?>" /> -<meta property="og:site_name" content="<?= $podcast->title ?>" /> -<meta property="og:url" content="<?= current_url() ?>" /> -<meta property="og:image" content="<?= $podcast->image->large_url ?>" /> -<meta property="og:image:width" content="<?= config('Images') - ->largeSize ?>" /> -<meta property="og:image:height" content="<?= config('Images') - ->largeSize ?>" /> -<meta name="twitter:card" content="summary_large_image" /> - -<?= service('vite') - ->asset('styles/index.css', 'css') ?> -<?= $this->endSection() ?> - -<?= $this->section('content') ?> - -<?= $this->include('podcast/_navigation') ?> - -<section class="max-w-2xl px-6 py-8 mx-auto"> -<form action="<?= route_to('post-attempt-create', interact_as_actor()->username) ?>" method="POST" class="flex p-4 bg-white shadow rounded-xl"> - <?= csrf_field() ?> - - <?= view('_message_block') ?> - - <img src="<?= interact_as_actor() - ->avatar_image_url ?>" alt="<?= interact_as_actor() - ->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> - <div class="flex flex-col flex-1 min-w-0 gap-y-2"> - <Forms.Textarea - name="message" - required="true" - placeholder="<?= lang('Post.form.message_placeholder') ?>" - rows="2" /> - <Forms.Input - name="episode_url" - type="url" - placeholder="<?= lang('Post.form.episode_url_placeholder') . ' (' . lang('Common.optional') . ')' ?>" /> - <Button variant="primary" size="small" type="submit" class="self-end"><?= lang('Post.form.submit') ?></Button> - </div> -</form> -<hr class="my-4 border-2 border-pine-100"> - -<div class="space-y-8"> -<?php foreach ($posts as $post): ?> - <?php if ($post->reblog_of_id !== null): ?> - <?= view('podcast/_partials/reblog_authenticated', [ - 'post' => $post->reblog_of_post, - 'podcast' => $podcast, -]) ?> - <?php else: ?> - <?= view('podcast/_partials/post_authenticated', [ - 'post' => $post, - 'podcast' => $podcast, -]) ?> - <?php endif; ?> -<?php endforeach; ?> -</div> -</section> - -<?= $this->endSection() ?> diff --git a/themes/cp_app/podcast/comment_authenticated.php b/themes/cp_app/podcast/comment_authenticated.php deleted file mode 100644 index ea145a420b..0000000000 --- a/themes/cp_app/podcast/comment_authenticated.php +++ /dev/null @@ -1,41 +0,0 @@ -<?= $this->extend('podcast/_layout_authenticated') ?> - -<?= $this->section('meta-tags') ?> - <title><?= lang('Comment.title', [ - 'actorDisplayName' => $comment->actor->display_name, - 'episodeTitle' => $episode->title, -]) ?></title> - <meta name="description" content="<?= $comment->message ?>"/> - <meta property="og:title" content="<?= lang('Comment.title', [ - 'actorDisplayName' => $comment->actor->display_name, - 'episodeTitle' => $episode->title, - ]) ?>"/> - <meta property="og:locale" content="<?= service( - 'request', - )->getLocale() ?>" /> - <meta property="og:site_name" content="<?= $comment->actor->display_name ?>" /> - <meta property="og:url" content="<?= current_url() ?>" /> - <meta property="og:image" content="<?= $comment->actor->avatar_image_url ?>" /> - <meta property="og:description" content="<?= $comment->message ?>" /> -<?= $this->endSection() ?> - -<?= $this->section('content') ?> -<div class="max-w-2xl px-6 mx-auto"> - <nav class="py-3"> - <a href="<?= route_to('episode', $podcast->handle, $episode->slug) ?>" - class="inline-flex items-center px-4 py-2 text-sm"><?= icon( - 'arrow-left', - 'mr-2 text-lg', - ) . - lang('Comment.back_to_episode', [ - 'episodeTitle' => $episode->title, - ]) ?></a> - </nav> - <div class="pb-12"> - <?= $this->include( - 'podcast/_partials/comment_with_replies_authenticated', - ) ?> - </div> -</div> - -<?= $this->endSection() ?> diff --git a/themes/cp_app/podcast/episode.php b/themes/cp_app/podcast/episode.php deleted file mode 100644 index dcccf80c17..0000000000 --- a/themes/cp_app/podcast/episode.php +++ /dev/null @@ -1,113 +0,0 @@ -<?= $this->extend('podcast/_layout') ?> - -<?= $this->section('meta-tags') ?> -<title><?= $episode->title ?></title> -<meta name="description" content="<?= htmlspecialchars( - $episode->description, -) ?>" /> -<link rel="canonical" href="<?= $episode->link ?>" /> -<meta property="og:title" content="<?= $episode->title ?>" /> -<meta property="og:description" content="<?= $episode->description ?>" /> -<meta property="og:locale" content="<?= $podcast->language_code ?>" /> -<meta property="og:site_name" content="<?= $podcast->title ?>" /> -<meta property="og:url" content="<?= current_url() ?>" /> -<meta property="og:image" content="<?= $episode->image->large_url ?>" /> -<meta property="og:image:width" content="<?= config('Images') - ->largeSize ?>" /> -<meta property="og:image:height" content="<?= config('Images') - ->largeSize ?>" /> -<meta property="og:description" content="$description" /> -<meta property="article:published_time" content="<?= $episode->published_at ?>" /> -<meta property="article:modified_time" content="<?= $episode->updated_at ?>" /> -<meta property="og:audio" content="<?= $episode->audio_file_opengraph_url ?>" /> -<meta property="og:audio:type" content="<?= $episode->audio_file_mimetype ?>" /> -<link rel="alternate" type="application/json+oembed" href="<?= base_url( - route_to('episode-oembed-json', $podcast->handle, $episode->slug), - ) ?>" title="<?= $episode->title ?> oEmbed json" /> -<link rel="alternate" type="text/xml+oembed" href="<?= base_url( - route_to('episode-oembed-xml', $podcast->handle, $episode->slug), - ) ?>" title="<?= $episode->title ?> oEmbed xml" /> -<meta name="twitter:title" content="<?= $episode->title ?>" /> -<meta name="twitter:description" content="<?= $episode->description ?>" /> -<meta name="twitter:image" content="<?= $episode->image->large_url ?>" /> -<meta name="twitter:card" content="player" /> -<meta property="twitter:audio:partner" content="<?= $podcast->publisher ?>" /> -<meta property="twitter:audio:artist_name" content="<?= $podcast->owner_name ?>" /> -<meta name="twitter:player" content="<?= $episode->getEmbeddablePlayerUrl( - 'light', - ) ?>" /> -<meta name="twitter:player:width" content="600" /> -<meta name="twitter:player:height" content="200" /> -<?= $this->endSection() ?> - -<?= $this->section('content') ?> -<div class="max-w-2xl mx-auto"> - <a href="<?= route_to( - 'podcast-episodes', - $podcast->handle, - ) ?>" class="inline-flex items-center px-4 py-2 mb-2 text-sm"><?= icon( - 'arrow-left', - 'mr-2 text-lg', - ) . lang('Episode.back_to_episodes', [ - 'podcast' => $podcast->title, - ]) ?></a> - <header class="flex flex-col px-6 mb-4 rounded-b-xl"> - <div class="flex flex-wrap items-start"> - <img src="<?= $episode->image - ->medium_url ?>" alt="<?= $episode->title ?>" class="mb-4 mr-6 rounded-xl w-52" /> - <div class="flex flex-col items-start flex-1 mb-4" style="min-width: 14rem"> - <h1 class="text-xl font-bold leading-none font-display line-clamp-2"><?= $episode->title ?></h1> - <?= episode_numbering( - $episode->number, - $episode->season_number, - 'text-gray-700', - ) ?> - <div class="mb-4 text-xs"> - <?= relative_time($episode->published_at) ?> - <span class="mx-1">•</span> - <time datetime="PT<?= $episode->audio_file_duration ?>S"> - <?= format_duration($episode->audio_file_duration) ?> - </time> - </div> - <?= location_link($episode->location, 'text-sm mb-4') ?> - <?= person_list($episode->persons) ?> - <?= play_episode_button($episode->id, $episode->image->thumbnail_url, $episode->title, $podcast->title, $episode->audio_file_web_url, $episode->audio_file_mimetype) ?> - </div> - </div> - </header> - - <div class="tabset"> - <input type="radio" name="tabset" id="comments" aria-controls="comments" checked="checked" /> - <label for="comments"><?= lang('Episode.comments') . ' (' . $episode->comments_count . ')' ?></label> - - <input type="radio" name="tabset" id="activity" aria-controls="activity" /> - <label for="activity"><?= lang('Episode.activity') . ' (' . $episode->posts_count . ')' ?></label> - - <input type="radio" name="tabset" id="description" aria-controls="description" /> - <label for="description"><?= lang('Episode.description') ?></label> - - <div class="tab-panels"> - <section id="comments" class="space-y-6 tab-panel"> - <?php foreach ($episode->comments as $comment): ?> - <?= view('podcast/_partials/comment', [ - 'comment' => $comment, - ]) ?> - <?php endforeach; ?> - </section> - <section id="activity" class="space-y-8 tab-panel"> - <?php foreach ($episode->posts as $post): ?> - <?= view('podcast/_partials/post', [ - 'post' => $post, - 'podcast' => $podcast, - ]) ?> - <?php endforeach; ?> - </section> - <section id="description" class="prose tab-panel"> - <?= $episode->getDescriptionHtml('-+Website+-') ?> - </section> - </div> - </div> -</div> - -<?= $this->endSection() -?> diff --git a/themes/cp_app/podcast/episode_authenticated.php b/themes/cp_app/podcast/episode_authenticated.php deleted file mode 100644 index 0ffc7ed58d..0000000000 --- a/themes/cp_app/podcast/episode_authenticated.php +++ /dev/null @@ -1,150 +0,0 @@ -<?= $this->extend('podcast/_layout_authenticated') ?> - -<?= $this->section('meta-tags') ?> -<title><?= $episode->title ?></title> -<meta name="description" content="<?= htmlspecialchars( - $episode->description, -) ?>" /> -<link rel="canonical" href="<?= $episode->link ?>" /> -<meta property="og:title" content="<?= $episode->title ?>" /> -<meta property="og:description" content="<?= $episode->description ?>" /> -<meta property="og:locale" content="<?= $podcast->language_code ?>" /> -<meta property="og:site_name" content="<?= $podcast->title ?>" /> -<meta property="og:url" content="<?= current_url() ?>" /> -<meta property="og:image" content="<?= $episode->image->large_url ?>" /> -<meta property="og:image:width" content="<?= config('Images') - ->largeSize ?>" /> -<meta property="og:image:height" content="<?= config('Images') - ->largeSize ?>" /> -<meta property="og:description" content="$description" /> -<meta property="article:published_time" content="<?= $episode->published_at ?>" /> -<meta property="article:modified_time" content="<?= $episode->updated_at ?>" /> -<meta property="og:audio" content="<?= $episode->audio_file_opengraph_url ?>" /> -<meta property="og:audio:type" content="<?= $episode->audio_file_mimetype ?>" /> -<link rel="alternate" type="application/json+oembed" href="<?= base_url( - route_to('episode-oembed-json', $podcast->handle, $episode->slug), - ) ?>" title="<?= $episode->title ?> oEmbed json" /> -<link rel="alternate" type="text/xml+oembed" href="<?= base_url( - route_to('episode-oembed-xml', $podcast->handle, $episode->slug), - ) ?>" title="<?= $episode->title ?> oEmbed xml" /> -<meta name="twitter:title" content="<?= $episode->title ?>" /> -<meta name="twitter:description" content="<?= $episode->description ?>" /> -<meta name="twitter:image" content="<?= $episode->image->large_url ?>" /> -<meta name="twitter:card" content="player" /> -<meta property="twitter:audio:partner" content="<?= $podcast->publisher ?>" /> -<meta property="twitter:audio:artist_name" content="<?= $podcast->owner_name ?>" /> -<meta name="twitter:player" content="<?= $episode->getEmbeddablePlayerUrl( - 'light', - ) ?>" /> -<meta name="twitter:player:width" content="600" /> -<meta name="twitter:player:height" content="200" /> -<?= $this->endSection() ?> - -<?= $this->section('content') ?> -<div class="max-w-2xl mx-auto"> - <a href="<?= route_to( - 'podcast-episodes', - $podcast->handle, - ) ?>" class="inline-flex items-center px-4 py-2 mb-2 text-sm"><?= icon( - 'arrow-left', - 'mr-2 mb- text-lg', - ) . lang('Episode.back_to_episodes', [ - 'podcast' => $podcast->title, - ]) ?></a> - <header class="flex flex-col px-6 mb-4 rounded-b-xl"> - <div class="flex flex-wrap items-start"> - <img src="<?= $episode->image - ->medium_url ?>" alt="<?= $episode->title ?>" class="mb-4 mr-6 rounded-xl w-52" /> - <div class="flex flex-col items-start flex-1 mb-4" style="min-width: 14rem"> - <h1 class="text-xl font-bold leading-none font-display line-clamp-2"><?= $episode->title ?></h1> - <?= episode_numbering( - $episode->number, - $episode->season_number, - 'text-gray-700', - ) ?> - <div class="mb-4 text-xs"> - <?= relative_time($episode->published_at) ?> - <span class="mx-1">•</span> - <time datetime="PT<?= $episode->audio_file_duration ?>S"> - <?= format_duration($episode->audio_file_duration) ?> - </time> - </div> - <?= location_link($episode->location, 'text-sm mb-4') ?> - <?= person_list($episode->persons) ?> - <?= play_episode_button($episode->id, $episode->image->thumbnail_url, $episode->title, $podcast->title, $episode->audio_file_web_url, $episode->audio_file_mimetype) ?> - </div> - </div> - </header> - - <div class="tabset"> - <input type="radio" name="tabset" id="comments" aria-controls="comments" checked="checked" /> - <label for="comments"><?= lang('Episode.comments') . ' (' . $episode->comments_count . ')' ?></label> - - <input type="radio" name="tabset" id="activity" aria-controls="activity" /> - <label for="activity"><?= lang('Episode.activity') . ' (' . $episode->posts_count . ')' ?></label> - - <input type="radio" name="tabset" id="description" aria-controls="description" /> - <label for="description"><?= lang('Episode.description') ?></label> - - <div class="tab-panels"> - <section id="comments" class="space-y-6 tab-panel"> - <form action="<?= route_to('comment-attempt-create', $podcast->id, $episode->id) ?>" method="POST" class="flex p-4"> - <?= csrf_field() ?> - - <?= view('_message_block') ?> - - <img src="<?= interact_as_actor() - ->avatar_image_url ?>" alt="<?= interact_as_actor() - ->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> - <div class="flex flex-col flex-1 min-w-0 gap-y-2"> - <Forms.Textarea - name="message" - required="true" - placeholder="<?= lang('Comment.form.episode_message_placeholder') ?>" - rows="2" /> - <Button class="self-end" variant="primary" size="small" type="submit"><?= lang('Comment.form.submit') ?></Button> - </div> - </form> - <?php foreach ($episode->comments as $comment): ?> - <?= view('podcast/_partials/comment_authenticated', [ - 'comment' => $comment, - 'podcast' => $podcast, - ]) ?> - <?php endforeach; ?> - </section> - <section id="activity" class="space-y-8 tab-panel"> - <form action="<?= route_to('post-attempt-create', $podcast->handle) ?>" method="POST" class="flex p-4 bg-white shadow rounded-xl"> - <?= csrf_field() ?> - - <?= view('_message_block') ?> - - <img src="<?= interact_as_actor() - ->avatar_image_url ?>" alt="<?= interact_as_actor() - ->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> - <div class="flex flex-col flex-1 min-w-0 gap-y-2"> - <input name="episode_url" value="<?= $episode->link ?>" type="hidden" /> - <Forms.Textarea - name="message" - placeholder="<?= lang('Post.form.episode_message_placeholder') ?>" - required="true" - rows="2" /> - <Button variant="primary" size="small" type="submit" class="self-end"><?= lang('Post.form.submit') ?></Button> - </div> - </form> - <hr class="my-4 border border-pine-100"> - <?php foreach ($episode->posts as $post): ?> - <?= view('podcast/_partials/post_authenticated', [ - 'post' => $post, - 'podcast' => $podcast, - ]) ?> - <?php endforeach; ?> - </section> - <section id="description" class="prose tab-panel"> - <?= $episode->getDescriptionHtml('-+Website+-') ?> - </section> - </div> - </div> -</div> - -<?= $this->endSection() -?> diff --git a/themes/cp_app/podcast/episodes.php b/themes/cp_app/podcast/episodes.php index ed4a9a523e..d4c26f623b 100644 --- a/themes/cp_app/podcast/episodes.php +++ b/themes/cp_app/podcast/episodes.php @@ -27,10 +27,10 @@ <?= $this->section('content') ?> -<section class="flex flex-col max-w-2xl"> +<section class="flex flex-col max-w-2xl gap-y-4"> <?php if ($episodes): ?> - <div class="flex items-center justify-between mb-4"> - <h1 class="text-xl font-semibold"> + <div class="flex items-center justify-between"> + <h1 class="font-semibold"> <?php if ($activeQuery['type'] === 'year'): ?> <?= lang('Podcast.list_of_episodes_year', [ 'year' => $activeQuery['value'], @@ -44,34 +44,28 @@ <?php endif; ?> </h1> <?php if ($activeQuery): ?> - <button id="episode-lists-dropdown" type="button" class="inline-flex items-center px-2 py-1 text-sm font-semibold outline-none focus:ring" data-dropdown="button" data-dropdown-target="episode-lists-dropdown-menu" aria-label="<?= lang( - 'Common.more', -) ?>" aria-haspopup="true" aria-expanded="false"> - <?= $activeQuery['label'] . - ' (' . - $activeQuery['number_of_episodes'] . - ')' . - icon('caret-down', 'ml-2 text-xl') ?> + <button id="episode-lists-dropdown" type="button" class="inline-flex items-center px-2 py-1 text-sm font-semibold outline-none focus:ring" data-dropdown="button" data-dropdown-target="episode-lists-dropdown-menu" aria-label="<?= lang('Common.more') ?>" aria-haspopup="true" aria-expanded="false"> + <?= $activeQuery['label'] . icon('caret-down', 'ml-2 text-xl') ?> </button> <nav id="episode-lists-dropdown-menu" class="flex flex-col py-2 text-black bg-white border rounded shadow" aria-labelledby="episode-lists-dropdown" data-dropdown="menu" data-dropdown-placement="bottom-end"> <?php foreach ($episodesNav as $link): ?> <?= anchor( - $link['route'], - $link['label'] . ' (' . $link['number_of_episodes'] . ')', - [ - 'class' => - 'px-2 py-1 whitespace-nowrap ' . - ($link['is_active'] - ? 'font-semibold' - : 'text-gray-600 hover:text-gray-900'), - ], - ) ?> + $link['route'], + $link['label'] . ' (' . $link['number_of_episodes'] . ')', + [ + 'class' => + 'px-2 py-1 whitespace-nowrap ' . + ($link['is_active'] + ? 'font-semibold' + : 'text-gray-600 hover:text-gray-900'), + ], +) ?> <?php endforeach; ?> </nav> <?php endif; ?> </div> <?php foreach ($episodes as $episode): ?> - <?= view('podcast/_partials/episode_card', [ + <?= view('episode/_partials/card', [ 'episode' => $episode, 'podcast' => $podcast, ]) ?> diff --git a/themes/cp_app/podcast/episodes_authenticated.php b/themes/cp_app/podcast/episodes_authenticated.php deleted file mode 100644 index 0986f2eb0d..0000000000 --- a/themes/cp_app/podcast/episodes_authenticated.php +++ /dev/null @@ -1,63 +0,0 @@ -<?= $this->extend('podcast/_layout_authenticated') ?> - -<?= $this->section('meta-tags') ?> -<link type="application/rss+xml" rel="alternate" title="<?= $podcast->title ?>" href="<?= $podcast->feed_url ?>" /> - -<title><?= $podcast->title ?></title> -<meta name="description" content="<?= htmlspecialchars( - $podcast->description, -) ?>" /> -<link rel="shortcut icon" type="image/png" href="/favicon.ico" /> -<link rel="canonical" href="<?= current_url() ?>" /> -<meta property="og:title" content="<?= $podcast->title ?>" /> -<meta property="og:description" content="<?= $podcast->description ?>" /> -<meta property="og:locale" content="<?= $podcast->language_code ?>" /> -<meta property="og:site_name" content="<?= $podcast->title ?>" /> -<meta property="og:url" content="<?= current_url() ?>" /> -<meta property="og:image" content="<?= $podcast->image->large_url ?>" /> -<meta property="og:image:width" content="<?= config('Images') - ->largeSize ?>" /> -<meta property="og:image:height" content="<?= config('Images') - ->largeSize ?>" /> -<meta name="twitter:card" content="summary_large_image" /> - -<?= service('vite') - ->asset('styles/index.css', 'css') ?> -<?= $this->endSection() ?> - -<?= $this->section('content') ?> - -<?= $this->include('podcast/_navigation') ?> - -<section class="flex flex-col max-w-2xl px-6 py-8 mx-auto"> - - <?php if ($episodes) : ?> - <h1 class="mb-4 text-xl font-semibold"> - <?php if ($activeQuery['type'] === 'year') : ?> - <?= lang('Podcast.list_of_episodes_year', [ - 'year' => $activeQuery['value'], - 'episodeCount' => count($episodes), -]) ?> - <?php elseif ($activeQuery['type'] === 'season') : ?> - <?= lang('Podcast.list_of_episodes_season', [ - 'seasonNumber' => $activeQuery['value'], - 'episodeCount' => count($episodes), -]) ?> - <?php endif; ?> - </h1> - <?php foreach ($episodes as $episode) : ?> - <?= view('podcast/_partials/episode_card', [ - 'episode' => $episode, - 'podcast' => $podcast, -]) ?> - <?php endforeach; ?> - <?php else : ?> - <h1 class="px-4 mb-2 text-xl text-center"><?= lang( - 'Podcast.no_episode', -) ?></h1> - <p class="italic text-center"><?= lang('Podcast.no_episode_hint') ?></p> - <?php endif; ?> -</section> - -<?= $this->endSection() -?> diff --git a/themes/cp_app/podcast/follow.php b/themes/cp_app/podcast/follow.php index 32682ae9b7..e98440f71a 100644 --- a/themes/cp_app/podcast/follow.php +++ b/themes/cp_app/podcast/follow.php @@ -49,16 +49,16 @@ <main class="w-full max-w-md px-4 mx-auto"> <form action="<?= route_to('attempt-follow', $actor->username) ?>" method="POST" class="flex flex-col"> - <?= csrf_field() ?> - <?= view('_message_block') ?> + <?= csrf_field() ?> + <?= view('_message_block') ?> - <Forms.Field - name="handle" - label="<?= lang('Fediverse.your_handle') ?>" - hint="<?= lang('Fediverse.your_handle_hint') ?>" - required="true" - /> - <Button variant="primary" type="submit" class="self-end"><?= lang('Fediverse.follow.submit') ?></Button> + <Forms.Field + name="handle" + label="<?= lang('Fediverse.your_handle') ?>" + hint="<?= lang('Fediverse.your_handle_hint') ?>" + required="true" + /> + <Button variant="primary" type="submit" class="self-end"><?= lang('Fediverse.follow.submit') ?></Button> </form> </main> @@ -68,7 +68,7 @@ <p> <?= lang('Common.powered_by', [ 'castopod' => - '<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>', + '<a class="inline-flex font-semibold hover:underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod' . icon('social/castopod', 'ml-1 text-lg') . '</a>', ]) ?> </p> </footer> diff --git a/themes/cp_app/podcast/post_authenticated.php b/themes/cp_app/podcast/post_authenticated.php deleted file mode 100644 index f04699351c..0000000000 --- a/themes/cp_app/podcast/post_authenticated.php +++ /dev/null @@ -1,40 +0,0 @@ -<?= $this->extend('podcast/_layout_authenticated') ?> - -<?= $this->section('meta-tags') ?> - <title><?= lang('Post.title', [ - 'actorDisplayName' => $post->actor->display_name, - ]) ?></title> - <meta name="description" content="<?= $post->message ?>"/> - <meta property="og:title" content="<?= lang('Post.title', [ - 'actorDisplayName' => $post->actor->display_name, - ]) ?>"/> - <meta property="og:locale" content="<?= service( - 'request', - )->getLocale() ?>" /> - <meta property="og:site_name" content="<?= $post->actor->display_name ?>" /> - <meta property="og:url" content="<?= current_url() ?>" /> - <meta property="og:image" content="<?= $post->actor->avatar_image_url ?>" /> - <meta property="og:description" content="<?= $post->message ?>" /> -<?= $this->endSection() ?> - -<?= $this->section('content') ?> -<div class="max-w-2xl px-6 mx-auto"> - <nav class="py-3"> - <a href="<?= route_to('podcast-activity', $podcast->handle) ?>" - class="inline-flex items-center px-4 py-2 text-sm"><?= icon( - 'arrow-left', - 'mr-2 text-lg', - ) . - lang('Post.back_to_actor_posts', [ - 'actor' => $post->actor->display_name, - ]) ?></a> - </nav> - <div class="pb-12"> - <?= $this->include( - 'podcast/_partials/post_with_replies_authenticated', - ) ?> - </div> -</div> - -<?= $this->endSection() -?> diff --git a/themes/cp_app/post/_partials/actions.php b/themes/cp_app/post/_partials/actions.php new file mode 100644 index 0000000000..2f92da4bcf --- /dev/null +++ b/themes/cp_app/post/_partials/actions.php @@ -0,0 +1,124 @@ +<footer class="flex justify-around px-6 py-3"> + <?php if (can_user_interact()): ?> + <form action="<?= route_to('post-attempt-action', interact_as_actor()->username, $post->id) ?>" method="POST" class="flex justify-around w-full"> + <?= csrf_field() ?> + <?= anchor( + route_to('post', $podcast->handle, $post->id), + icon('chat', 'text-2xl mr-1 text-gray-400') . $post->replies_count, + [ + 'class' => 'inline-flex items-center hover:underline', + 'title' => lang('Post.replies', [ + 'numberOfReplies' => $post->replies_count, + ]), + ], +) ?> + <button type="submit" name="action" value="reblog" class="inline-flex items-center hover:underline" title="<?= lang( + 'Post.reblogs', + [ + 'numberOfReblogs' => $post->reblogs_count, + ], +) ?>"><?= icon('repeat', 'text-2xl mr-1 text-gray-400') . + $post->reblogs_count ?></button> + <button type="submit" name="action" value="favourite" class="inline-flex items-center hover:underline" title="<?= lang( + 'Post.favourites', + [ + 'numberOfFavourites' => $post->favourites_count, + ], + ) ?>"><?= icon('heart', 'text-2xl mr-1 text-gray-400') . + $post->favourites_count ?></button> + <button id="<?= $post->id . + '-more-dropdown' ?>" type="button" class="px-2 py-1 text-2xl text-gray-500 outline-none focus:ring" data-dropdown="button" data-dropdown-target="<?= $post->id . + '-more-dropdown-menu' ?>" aria-label="<?= lang( + 'Common.more', + ) ?>" aria-haspopup="true" aria-expanded="false"><?= icon('more') ?> + </button> + </form> + <nav id="<?= $post->id . + '-more-dropdown-menu' ?>" class="flex flex-col py-2 text-sm bg-white border rounded-lg shadow" aria-labelledby="<?= $post->id . + '-more-dropdown' ?>" data-dropdown="menu" data-dropdown-placement="bottom"> + <?= anchor( + route_to('post', $podcast->handle, $post->id), + lang('Post.expand'), + [ + 'class' => 'px-4 py-1 hover:bg-gray-100', + ], + ) ?> + <form action="<?= route_to( + 'post-attempt-block-actor', + interact_as_actor() + ->username, + $post->id, + ) ?>" method="POST"> + <?= csrf_field() ?> + <button class="w-full px-4 py-1 text-left hover:bg-gray-100"><?= lang( + 'Post.block_actor', + [ + 'actorUsername' => $post->actor->username, + ], + ) ?></button> + </form> + <form action="<?= route_to( + 'post-attempt-block-domain', + interact_as_actor() + ->username, + $post->id, + ) ?>" method="POST"> + <?= csrf_field() ?> + <button class="w-full px-4 py-1 text-left hover:bg-gray-100"><?= lang( + 'Post.block_domain', + [ + 'actorDomain' => $post->actor->domain, + ], + ) ?></button> + </form> + <?php if ($post->actor->is_local): ?> + <hr class="my-2" /> + <form action="<?= route_to( + 'post-attempt-delete', + $post->actor->username, + $post->id, + ) ?>" method="POST"> + <?= csrf_field() ?> + <button class="w-full px-4 py-1 font-semibold text-left text-red-600 hover:bg-gray-100"><?= lang( + 'Post.delete', + ) ?></button> + </form> + <?php endif; ?> + </nav> + <?php else: ?> + <?= anchor( + route_to('post', $podcast->handle, $post->id), + icon('chat', 'text-2xl mr-1 text-gray-400') . $post->replies_count, + [ + 'class' => 'inline-flex items-center hover:underline', + 'title' => lang('Post.replies', [ + 'numberOfReplies' => $post->replies_count, + ]), + ], + ) ?> + <?= anchor_popup( + route_to('post-remote-action', $podcast->handle, $post->id, 'reblog'), + icon('repeat', 'text-2xl mr-1 text-gray-400') . $post->reblogs_count, + [ + 'class' => 'inline-flex items-center hover:underline', + 'width' => 420, + 'height' => 620, + 'title' => lang('Post.reblogs', [ + 'numberOfReblogs' => $post->reblogs_count, + ]), + ], + ) ?> + <?= anchor_popup( + route_to('post-remote-action', $podcast->handle, $post->id, 'favourite'), + icon('heart', 'text-2xl mr-1 text-gray-400') . $post->favourites_count, + [ + 'class' => 'inline-flex items-center hover:underline', + 'width' => 420, + 'height' => 620, + 'title' => lang('Post.favourites', [ + 'numberOfFavourites' => $post->favourites_count, + ]), + ], + ) ?> + <?php endif; ?> +</footer> diff --git a/themes/cp_app/podcast/_partials/post_authenticated.php b/themes/cp_app/post/_partials/card.php similarity index 63% rename from themes/cp_app/podcast/_partials/post_authenticated.php rename to themes/cp_app/post/_partials/card.php index 9a60f6b36f..74e4f4de5a 100644 --- a/themes/cp_app/podcast/_partials/post_authenticated.php +++ b/themes/cp_app/post/_partials/card.php @@ -1,7 +1,7 @@ -<article class="relative z-10 w-full bg-white shadow rounded-2xl"> +<article class="relative z-10 w-full bg-white shadow sm:rounded-2xl"> <header class="flex px-6 py-4"> <img src="<?= $post->actor - ->avatar_image_url ?>" alt="<?= $post->actor->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> + ->avatar_image_url ?>" alt="<?= $post->actor->display_name ?>" class="w-10 h-10 mr-2 rounded-full" /> <div class="flex flex-col min-w-0"> <a href="<?= $post->actor ->uri ?>" class="flex items-baseline hover:underline" <?= $post @@ -22,15 +22,20 @@ </a> </div> </header> - <div class="px-6 mb-4 post-content"><?= $post->message_html ?></div> + <?php if (substr_count($post->message, "\n") >= 3 || strlen($post->message) > 250): ?> + <ReadMore id="<?= $index ?>" class="px-6 mb-4 post-content"><?= $post->message_html ?></ReadMore> + <?php else: ?> + <div class="px-6 mb-4 post-content"><?= $post->message_html ?></div> + <?php endif; ?> <?php if ($post->episode_id): ?> - <?= view('podcast/_partials/episode_preview_card', [ + <?= view('episode/_partials/preview_card', [ + 'index' => $index, 'episode' => $post->episode, - ]) ?> - <?php elseif ($post->has_preview_card): ?> - <?= view('podcast/_partials/preview_card', [ +]) ?> + <?php elseif ($post->preview_card_id): ?> + <?= view('post/_partials/preview_card', [ 'preview_card' => $post->preview_card, ]) ?> <?php endif; ?> - <?= $this->include('podcast/_partials/post_actions_authenticated') ?> + <?= $this->include('post/_partials/actions') ?> </article> diff --git a/themes/cp_app/post/_partials/post_with_replies.php b/themes/cp_app/post/_partials/post_with_replies.php new file mode 100644 index 0000000000..fc3b66a22f --- /dev/null +++ b/themes/cp_app/post/_partials/post_with_replies.php @@ -0,0 +1,49 @@ +<?= view('post/_partials/card', [ + 'index' => $index, + 'post' => $post, + 'podcast' => $podcast, +]) ?> +<div class="-mt-2 overflow-hidden border-b border-l border-r post-replies rounded-b-xl"> + <div class="px-6 pt-8 pb-4 bg-gray-50"> + <?php if (can_user_interact()): ?> + <form action="<?= route_to('post-attempt-action', interact_as_actor()->username, $post->id) ?>" method="POST" class="flex" > + <img src="<?= interact_as_actor() + ->avatar_image_url ?>" alt="<?= interact_as_actor() + ->display_name ?>" class="w-12 h-12 mr-4 rounded-full ring-gray-50 ring-2" /> + <div class="flex flex-col flex-1"> + <Forms.Textarea + name="message" + class="w-full mb-4" + required="true" + placeholder="<?= lang('Post.form.reply_to_placeholder', [ + 'actorUsername' => $post->actor->username, + ]) ?>" + rows="1" /> + <Button variant="primary" size="small" type="submit" name="action" value="reply" class="self-end" iconRight="send-plane"><?= lang('Post.form.submit_reply') ?></Button> + </div> + </form> + <?php else: ?> + <?= anchor_popup( + route_to('post-remote-action', $podcast->handle, $post->id, 'reply'), + lang('Post.reply_to', [ + 'actorUsername' => $post->actor->username, + ]), + [ + 'class' => + 'text-center justify-center font-semibold rounded-full shadow relative z-10 px-4 py-2 w-full bg-rose-600 text-white inline-flex items-center hover:bg-rose-700', + 'width' => 420, + 'height' => 620, + ], + ) ?> + <?php endif; ?> + </div> + + <?php if ($post->has_replies): ?> + <?php foreach ($post->replies as $reply): ?> + <?= view('post/_partials/reply', [ + 'podcast' => $podcast, + 'reply' => $reply, + ]) ?> + <?php endforeach; ?> + <?php endif; ?> +</div> \ No newline at end of file diff --git a/themes/cp_app/podcast/_partials/preview_card.php b/themes/cp_app/post/_partials/preview_card.php similarity index 100% rename from themes/cp_app/podcast/_partials/preview_card.php rename to themes/cp_app/post/_partials/preview_card.php diff --git a/themes/cp_app/podcast/_partials/reblog.php b/themes/cp_app/post/_partials/reblog.php similarity index 83% rename from themes/cp_app/podcast/_partials/reblog.php rename to themes/cp_app/post/_partials/reblog.php index d5ef3ba136..71914ded4c 100644 --- a/themes/cp_app/podcast/_partials/reblog.php +++ b/themes/cp_app/post/_partials/reblog.php @@ -1,4 +1,4 @@ -<article class="relative z-10 w-full bg-white shadow rounded-2xl"> +<article class="relative z-10 w-full bg-white shadow sm:rounded-2xl"> <p class="inline-flex px-6 pt-4 text-xs text-gray-700"><?= icon( 'repeat', 'text-lg mr-2 text-gray-400', @@ -31,13 +31,13 @@ </header> <div class="px-6 mb-4 post-content"><?= $post->message_html ?></div> <?php if ($post->episode_id): ?> - <?= view('podcast/_partials/episode_preview_card', [ + <?= view('episode/_partials/preview_card', [ 'episode' => $post->episode, ]) ?> - <?php elseif ($post->has_preview_card): ?> - <?= view('podcast/_partials/preview_card', [ + <?php elseif ($post->preview_card_id): ?> + <?= view('post/_partials/preview_card', [ 'preview_card' => $post->preview_card, ]) ?> <?php endif; ?> - <?= $this->include('podcast/_partials/post_actions') ?> + <?= $this->include('post/_partials/actions') ?> </article> diff --git a/themes/cp_app/podcast/_partials/reply.php b/themes/cp_app/post/_partials/reply.php similarity index 77% rename from themes/cp_app/podcast/_partials/reply.php rename to themes/cp_app/post/_partials/reply.php index 912276ad7e..e39bc418a8 100644 --- a/themes/cp_app/podcast/_partials/reply.php +++ b/themes/cp_app/post/_partials/reply.php @@ -11,14 +11,14 @@ ->display_name ?><span class="ml-1 text-sm font-normal text-gray-600">@<?= $reply ->actor->username . ($reply->actor->is_local ? '' : '@' . $reply->actor->domain) ?></span></a> - <?= relative_time($post->published_at, 'flex-shrink-0 ml-auto text-xs text-gray-600') ?> + <?= relative_time($reply->published_at, 'flex-shrink-0 ml-auto text-xs text-gray-600') ?> </header> <p class="mb-2 post-content"><?= $reply->message_html ?></p> - <?php if ($reply->has_preview_card): ?> - <?= view('podcast/_partials/preview_card', [ + <?php if ($reply->preview_card_id): ?> + <?= view('post/_partials/preview_card', [ 'preview_card' => $reply->preview_card, ]) ?> <?php endif; ?> - <?= $this->include('podcast/_partials/reply_actions') ?> + <?= $this->include('post/_partials/reply_actions') ?> </div> </article> diff --git a/themes/cp_app/post/_partials/reply_actions.php b/themes/cp_app/post/_partials/reply_actions.php new file mode 100644 index 0000000000..322f1ed3be --- /dev/null +++ b/themes/cp_app/post/_partials/reply_actions.php @@ -0,0 +1,124 @@ +<footer> + <?php if (can_user_interact()): ?> + <form action="<?= route_to('post-attempt-action', interact_as_actor()->username, $reply->id) ?>" method="POST" class="flex items-start"> + <?= csrf_field() ?> + <?= anchor( + route_to('post', $podcast->handle, $reply->id), + icon('chat', 'text-xl mr-1 text-gray-400') . $reply->replies_count, + [ + 'class' => 'inline-flex items-center mr-6 hover:underline', + 'title' => lang('Post.replies', [ + 'numberOfReplies' => $reply->replies_count, + ]), + ], +) ?> + <button type="submit" name="action" value="reblog" class="inline-flex items-center mr-6 hover:underline" title="<?= lang( + 'Post.reblogs', + [ + 'numberOfReblogs' => $reply->reblogs_count, + ], +) ?>"><?= icon('repeat', 'text-xl mr-1 text-gray-400') . + $reply->reblogs_count ?></button> + <button type="submit" name="action" value="favourite" class="inline-flex items-center mr-6 hover:underline" title="<?= lang( + 'Post.favourites', + [ + 'numberOfFavourites' => $reply->favourites_count, + ], + ) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-400') . + $reply->favourites_count ?></button> + <button id="<?= $reply->id . + '-more-dropdown' ?>" type="button" class="text-xl text-gray-500 outline-none focus:ring" data-dropdown="button" data-dropdown-target="<?= $reply->id . + '-more-dropdown-menu' ?>" aria-label="<?= lang( + 'Common.more', + ) ?>" aria-haspopup="true" aria-expanded="false"><?= icon('more') ?> + </button> + </form> + <nav id="<?= $reply->id . + '-more-dropdown-menu' ?>" class="flex flex-col py-2 text-sm bg-white border rounded-lg shadow" aria-labelledby="<?= $reply->id . + '-more-dropdown' ?>" data-dropdown="menu" data-dropdown-placement="bottom"> + <?= anchor( + route_to('post', $podcast->handle, $reply->id), + lang('Post.expand'), + [ + 'class' => 'px-4 py-1 hover:bg-gray-100', + ], + ) ?> + <form action="<?= route_to( + 'post-attempt-block-actor', + interact_as_actor() + ->username, + $reply->id, + ) ?>" method="POST"> + <?= csrf_field() ?> + <button class="w-full px-4 py-1 text-left hover:bg-gray-100"><?= lang( + 'Post.block_actor', + [ + 'actorUsername' => $reply->actor->username, + ], + ) ?></button> + </form> + <form action="<?= route_to( + 'post-attempt-block-domain', + interact_as_actor() + ->username, + $reply->id, + ) ?>" method="POST"> + <?= csrf_field() ?> + <button class="w-full px-4 py-1 text-left hover:bg-gray-100"><?= lang( + 'Post.block_domain', + [ + 'actorDomain' => $reply->actor->domain, + ], + ) ?></button> + </form> + <?php if ($reply->actor->is_local): ?> + <hr class="my-2" /> + <form action="<?= route_to( + 'post-attempt-delete', + $reply->actor->username, + $reply->id, + ) ?>" method="POST"> + <?= csrf_field() ?> + <button class="w-full px-4 py-1 font-semibold text-left text-red-600 hover:bg-gray-100"><?= lang( + 'Post.delete', + ) ?></button> + </form> + <?php endif; ?> + </nav> + <?php else: ?> + <?= anchor( + route_to('post', $podcast->handle, $reply->id), + icon('chat', 'text-xl mr-1 text-gray-400') . $reply->replies_count, + [ + 'class' => 'inline-flex items-center hover:underline', + 'title' => lang('Post.replies', [ + 'numberOfReplies' => $reply->replies_count, + ]), + ], + ) ?> + <?= anchor_popup( + route_to('post-remote-action', $podcast->handle, $reply->id, 'reblog'), + icon('repeat', 'text-xl mr-1 text-gray-400') . $reply->reblogs_count, + [ + 'class' => 'inline-flex items-center hover:underline', + 'width' => 420, + 'height' => 620, + 'title' => lang('Post.reblogs', [ + 'numberOfReblogs' => $reply->reblogs_count, + ]), + ], + ) ?> + <?= anchor_popup( + route_to('post-remote-action', $podcast->handle, $reply->id, 'favourite'), + icon('heart', 'text-xl mr-1 text-gray-400') . $reply->favourites_count, + [ + 'class' => 'inline-flex items-center hover:underline', + 'width' => 420, + 'height' => 620, + 'title' => lang('Post.favourites', [ + 'numberOfFavourites' => $reply->favourites_count, + ]), + ], + ) ?> + <?php endif; ?> +</footer> diff --git a/themes/cp_app/podcast/post.php b/themes/cp_app/post/post.php similarity index 65% rename from themes/cp_app/podcast/post.php rename to themes/cp_app/post/post.php index 3990e38d8c..3cc1299f10 100644 --- a/themes/cp_app/podcast/post.php +++ b/themes/cp_app/post/post.php @@ -18,21 +18,21 @@ <?= $this->endSection() ?> <?= $this->section('content') ?> -<div class="max-w-2xl px-6 mx-auto"> - <nav class="py-3"> - <a href="<?= route_to('podcast-activity', $podcast->handle) ?>" - class="inline-flex items-center px-4 py-2 text-sm"><?= icon( +<nav class="py-2"> + <a href="<?= route_to('podcast-activity', $podcast->handle) ?>" + class="inline-flex items-center px-4 py-2 text-sm"><?= icon( 'arrow-left', 'mr-2 text-lg', ) . - lang('Post.back_to_actor_posts', [ - 'actor' => $post->actor->display_name, - ]) ?></a> - </nav> - <div class="pb-12"> - <?= $this->include('podcast/_partials/post_with_replies') ?> - </div> + lang('Post.back_to_actor_posts', [ + 'actor' => $post->actor->display_name, + ]) ?></a> +</nav> +<div class="pb-12"> + <?= view('post/_partials/post_with_replies', [ + 'index' => 1, + 'post' => $post, + 'podcast' => $podcast, + ]) ?> </div> - -<?= $this->endSection() -?> +<?= $this->endSection() ?> diff --git a/themes/cp_app/podcast/post_remote_action.php b/themes/cp_app/post/remote_action.php similarity index 97% rename from themes/cp_app/podcast/post_remote_action.php rename to themes/cp_app/post/remote_action.php index 6e2205abe6..36c7ffd361 100644 --- a/themes/cp_app/podcast/post_remote_action.php +++ b/themes/cp_app/post/remote_action.php @@ -38,7 +38,7 @@ ) ?></h1> </header> <main class="flex-1 max-w-xl px-4 pb-8 mx-auto -mt-24"> - <?= $this->include('podcast/_partials/post') ?> + <?= $this->include('post/_partials/card') ?> <form action="<?= route_to('post-attempt-remote-action', $post->id, $action) ?>" method="POST" class="flex flex-col mt-8"> <?= csrf_field() ?> diff --git a/themes/cp_auth/_layout.php b/themes/cp_auth/_layout.php index 30fca7a6e1..6df0641baa 100644 --- a/themes/cp_auth/_layout.php +++ b/themes/cp_auth/_layout.php @@ -31,7 +31,7 @@ <?= $this->renderSection('footer') ?> <small class="py-4 text-center border-t-2 border-pine-100"><?= lang('Common.powered_by', [ 'castopod' => - '<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>', + '<a class="inline-flex font-semibold hover:underline" target="_blank" rel="noreferrer noopener">Castopod' . icon('social/castopod', 'ml-1 text-lg') . '</a>', ]) ?></small> </footer> </body> diff --git a/themes/cp_install/_layout.php b/themes/cp_install/_layout.php index eccc912fbf..dd384d75e9 100644 --- a/themes/cp_install/_layout.php +++ b/themes/cp_install/_layout.php @@ -26,7 +26,7 @@ <footer class="container px-2 py-4 mx-auto text-sm text-right border-t"> <small><?= lang('Common.powered_by', [ 'castopod' => - '<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>', + '<a class="inline-flex font-semibold hover:underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod' . icon('social/castopod', 'ml-1 text-lg') . '</a>', ]) ?></small> </footer> </body> diff --git a/vite.config.ts b/vite.config.ts index f8b3ed543f..617f6fa344 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -14,6 +14,7 @@ export default defineConfig({ input: { "podcast.ts": "app/Resources/js/podcast.ts", "install.ts": "app/Resources/js/install.ts", + "app.ts": "app/Resources/js/app.ts", "admin.ts": "app/Resources/js/admin.ts", "charts.ts": "app/Resources/js/charts.ts", "map.ts": "app/Resources/js/map.ts", -- GitLab