diff --git a/app/Config/Routes.php b/app/Config/Routes.php index d46a8855682d95c729a092ae238f396c239ddaa1..ae3c9365b1103e1f9dc91fddc87232be22fa2186 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -210,6 +210,15 @@ $routes->get('audio/@(:podcastHandle)/(:slug).(:alphanum)', 'EpisodeAudioControl 'as' => 'episode-audio', ], ); +// episode preview link +$routes->get('/p/(:uuid)', 'EpisodePreviewController::index/$1', [ + 'as' => 'episode-preview', +]); + +$routes->get('/p/(:uuid)/activity', 'EpisodePreviewController::activity/$1', [ + 'as' => 'episode-preview-activity', +]); + // Other pages $routes->get('/credits', 'CreditsController', [ 'as' => 'credits', diff --git a/app/Controllers/EpisodePreviewController.php b/app/Controllers/EpisodePreviewController.php new file mode 100644 index 0000000000000000000000000000000000000000..6c03eef23a0cdb4f48e5a30756dea54e084663cb --- /dev/null +++ b/app/Controllers/EpisodePreviewController.php @@ -0,0 +1,66 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Ad Aures + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Controllers; + +use App\Entities\Episode; +use App\Models\EpisodeModel; +use CodeIgniter\Exceptions\PageNotFoundException; +use CodeIgniter\HTTP\RedirectResponse; + +class EpisodePreviewController extends BaseController +{ + protected Episode $episode; + + public function _remap(string $method, string ...$params): mixed + { + if (count($params) < 1) { + throw PageNotFoundException::forPageNotFound(); + } + + // find episode by previewUUID + $episode = (new EpisodeModel())->getEpisodeByPreviewId($params[0]); + + if (! $episode instanceof Episode) { + throw PageNotFoundException::forPageNotFound(); + } + + $this->episode = $episode; + + if ($episode->publication_status === 'published') { + // redirect to episode page + return redirect()->route('episode', [$episode->podcast->handle, $episode->slug]); + } + + unset($params[0]); + + return $this->{$method}(...$params); + } + + public function index(): RedirectResponse | string + { + helper('form'); + + return view('episode/preview-comments', [ + 'podcast' => $this->episode->podcast, + 'episode' => $this->episode, + ]); + } + + public function activity(): RedirectResponse | string + { + helper('form'); + + return view('episode/preview-activity', [ + 'podcast' => $this->episode->podcast, + 'episode' => $this->episode, + ]); + } +} diff --git a/app/Database/Migrations/2023-08-22-120000_add_episode_preview_id.php b/app/Database/Migrations/2023-08-22-120000_add_episode_preview_id.php new file mode 100644 index 0000000000000000000000000000000000000000..2bd026e2751963cd501c933b2df13a1fa9d4eedb --- /dev/null +++ b/app/Database/Migrations/2023-08-22-120000_add_episode_preview_id.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +namespace App\Database\Migrations; + +class AddEpisodePreviewId extends BaseMigration +{ + public function up(): void + { + $fields = [ + 'preview_id' => [ + 'type' => 'BINARY', + 'constraint' => 16, + 'after' => 'podcast_id', + ], + ]; + + $this->forge->addColumn('episodes', $fields); + + // set preview_id as unique key + $prefix = $this->db->getPrefix(); + $uniquePreviewId = <<<CODE_SAMPLE + ALTER TABLE `{$prefix}episodes` + ADD CONSTRAINT `preview_id` UNIQUE (`preview_id`); + CODE_SAMPLE; + + $this->db->query($uniquePreviewId); + } + + public function down(): void + { + $fields = ['preview_id']; + $this->forge->dropColumn('episodes', $fields); + } +} diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index 41c54a5bc72775ef405bf51f1657783a56666873..c7af02d3f007ce66b9d9f25074c056902ac6805f 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -14,6 +14,7 @@ use App\Entities\Clip\Soundbite; use App\Libraries\SimpleRSSElement; use App\Models\ClipModel; use App\Models\EpisodeCommentModel; +use App\Models\EpisodeModel; use App\Models\PersonModel; use App\Models\PodcastModel; use App\Models\PostModel; @@ -21,6 +22,7 @@ use CodeIgniter\Entity\Entity; use CodeIgniter\Files\File; use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\I18n\Time; +use Exception; use League\CommonMark\Environment\Environment; use League\CommonMark\Extension\Autolink\AutolinkExtension; use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; @@ -39,6 +41,8 @@ use SimpleXMLElement; * @property int $id * @property int $podcast_id * @property Podcast $podcast + * @property ?string $preview_id + * @property string $preview_link * @property string $link * @property string $guid * @property string $slug @@ -150,6 +154,7 @@ class Episode extends Entity protected $casts = [ 'id' => 'integer', 'podcast_id' => 'integer', + 'preview_id' => '?string', 'guid' => 'string', 'slug' => 'string', 'title' => 'string', @@ -509,7 +514,7 @@ class Episode extends Entity if ($this->getPodcast()->episode_description_footer_html) { $descriptionHtml .= "<footer>{$this->getPodcast() - ->episode_description_footer_html}</footer>"; +->episode_description_footer_html}</footer>"; } return $descriptionHtml; @@ -667,4 +672,18 @@ class Episode extends Entity urlencode((string) $this->attributes['guid']) . ($serviceSlug !== null ? '&_from=' . $serviceSlug : ''); } + + public function getPreviewLink(): string + { + if ($this->preview_id === null) { + // generate preview id + if (! $previewUUID = (new EpisodeModel())->setEpisodePreviewId($this->id)) { + throw new Exception('Could not set episode preview id'); + } + + $this->preview_id = $previewUUID; + } + + return url_to('episode-preview', (string) $this->preview_id); + } } diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php index 351d2ec04e8652df6098b49e06060b481055ed2a..176e75c7ae3d4497e2c7859afbf791ff0c8aab77 100644 --- a/app/Helpers/components_helper.php +++ b/app/Helpers/components_helper.php @@ -9,6 +9,7 @@ declare(strict_types=1); */ use App\Entities\Category; +use App\Entities\Episode; use App\Entities\Location; use CodeIgniter\I18n\Time; use CodeIgniter\View\Table; @@ -218,8 +219,8 @@ if (! function_exists('publication_status_banner')) { } return <<<HTML - <div class="flex items-center px-12 py-2 border-b bg-stripes-gray border-subtle" role="alert"> - <p class="flex items-center text-gray-900"> + <div class="flex flex-wrap items-baseline px-4 py-2 border-b md:px-12 bg-stripes-default border-subtle" role="alert"> + <p class="flex items-baseline text-gray-900"> <span class="text-xs font-semibold tracking-wide uppercase">{$bannerDisclaimer}</span> <span class="ml-3 text-sm">{$bannerText}</span> </p> @@ -231,6 +232,58 @@ if (! function_exists('publication_status_banner')) { // ------------------------------------------------------------------------ +if (! function_exists('episode_publication_status_banner')) { + /** + * Publication status banner component for podcasts + * + * Displays the appropriate banner depending on the podcast's publication status. + */ + function episode_publication_status_banner(Episode $episode, string $class = ''): string + { + switch ($episode->publication_status) { + case 'not_published': + $linkRoute = route_to('episode-publish', $episode->podcast_id, $episode->id); + $publishLinkLabel = lang('Episode.publish'); + break; + case 'scheduled': + case 'with_podcast': + $linkRoute = route_to('episode-publish_edit', $episode->podcast_id, $episode->id); + $publishLinkLabel = lang('Episode.publish_edit'); + break; + default: + $bannerDisclaimer = ''; + $linkRoute = ''; + $publishLinkLabel = ''; + break; + } + + $bannerDisclaimer = lang('Episode.publication_status_banner.draft_mode'); + $bannerText = lang('Episode.publication_status_banner.text', [ + 'publication_status' => $episode->publication_status, + 'publication_date' => $episode->published_at instanceof Time ? local_datetime( + $episode->published_at + ) : null, + ], null, false); + $previewLinkLabel = lang('Episode.publication_status_banner.preview'); + + return <<<HTML + <div class="flex flex-wrap gap-4 items-baseline px-4 md:px-12 py-2 bg-stripes-default border-subtle {$class}" role="alert"> + <p class="flex items-baseline text-gray-900"> + <span class="text-xs font-semibold tracking-wide uppercase">{$bannerDisclaimer}</span> + <span class="ml-3 text-sm">{$bannerText}</span> + </p> + <div class="flex items-baseline"> + <a href="{$episode->preview_link}" class="ml-1 text-sm font-semibold underline shadow-xs text-accent-base hover:text-accent-hover hover:no-underline">{$previewLinkLabel}</a> + <span class="mx-1">•</span> + <a href="{$linkRoute}" class="ml-1 text-sm font-semibold underline shadow-xs text-accent-base hover:text-accent-hover hover:no-underline">{$publishLinkLabel}</a> + </div> + </div> + HTML; + } +} + +// ------------------------------------------------------------------------ + if (! function_exists('episode_numbering')) { /** * Returns relevant translated episode numbering. @@ -360,7 +413,7 @@ if (! function_exists('relative_time')) { $datetime = $time->format(DateTime::ATOM); return <<<HTML - <relative-time tense="past" class="{$class}" datetime="{$datetime}"> + <relative-time tense="auto" class="{$class}" datetime="{$datetime}"> <time datetime="{$datetime}" title="{$time}">{$translatedDate}</time> @@ -378,10 +431,10 @@ if (! function_exists('local_datetime')) { 'request' )->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::LONG); $translatedDate = $time->toLocalizedString($formatter->getPattern()); - $datetime = $time->format(DateTime::ISO8601); + $datetime = $time->format(DateTime::ATOM); return <<<HTML - <relative-time datetime="{$datetime}" + <relative-time datetime="{$datetime}" prefix="" threshold="PT0S" weekday="long" diff --git a/app/Helpers/id3_helper.php b/app/Helpers/id3_helper.php index bf91158b9d73410e86775dde2f93081968ca28b5..08d8f14ac07cf145bbf0c3b10754bd194678cf4d 100644 --- a/app/Helpers/id3_helper.php +++ b/app/Helpers/id3_helper.php @@ -82,14 +82,12 @@ if (! function_exists('write_audio_file_tags')) { // write tags if ($tagwriter->WriteTags()) { - echo 'Successfully wrote tags<br>'; + // Successfully wrote tags if ($tagwriter->warnings !== []) { - echo 'There were some warnings:<br>' . - implode('<br><br>', $tagwriter->warnings); + log_message('warning', 'There were some warnings:' . PHP_EOL . implode(PHP_EOL, $tagwriter->warnings)); } } else { - echo 'Failed to write tags!<br>' . - implode('<br><br>', $tagwriter->errors); + log_message('critical', 'Failed to write tags!' . PHP_EOL . implode(PHP_EOL, $tagwriter->errors)); } } } diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php index ebe39336fc45d6f567b74c6a03c131b981afdc36..44be8e384d5d5868f1e92246b35554fe5344b7c2 100644 --- a/app/Language/en/Episode.php +++ b/app/Language/en/Episode.php @@ -30,4 +30,16 @@ return [ }', 'all_podcast_episodes' => 'All podcast episodes', 'back_to_podcast' => 'Go back to podcast', + 'preview' => [ + 'title' => 'Preview', + 'not_published' => 'Not published', + 'text' => '{publication_status, select, + published {This episode is not yet published.} + scheduled {This episode is scheduled for publication on {publication_date}.} + with_podcast {This episode will be published at the same time as the podcast.} + other {This episode is not yet published.} + }', + 'publish' => 'Publish', + 'publish_edit' => 'Edit publication', + ], ]; diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php index bedb194998f355037a5781696fc3b2bb44badc7a..bde68905fedf3474c36a8a5dc01f42c0a1d42384 100644 --- a/app/Models/EpisodeModel.php +++ b/app/Models/EpisodeModel.php @@ -14,9 +14,10 @@ use App\Entities\Episode; use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\BaseResult; use CodeIgniter\I18n\Time; -use CodeIgniter\Model; +use Michalsn\Uuid\UuidModel; +use Ramsey\Uuid\Lazy\LazyUuidFromString; -class EpisodeModel extends Model +class EpisodeModel extends UuidModel { /** * TODO: remove, shouldn't be here @@ -50,6 +51,11 @@ class EpisodeModel extends Model ], ]; + /** + * @var string[] + */ + protected $uuidFields = ['preview_id']; + /** * @var string */ @@ -61,6 +67,7 @@ class EpisodeModel extends Model protected $allowedFields = [ 'id', 'podcast_id', + 'preview_id', 'guid', 'title', 'slug', @@ -188,6 +195,38 @@ class EpisodeModel extends Model return $found; } + public function getEpisodeByPreviewId(string $previewId): ?Episode + { + $cacheName = "podcast_episode#preview-{$previewId}"; + if (! ($found = cache($cacheName))) { + $builder = $this->where([ + 'preview_id' => $this->uuid->fromString($previewId) + ->getBytes(), + ]); + + $found = $builder->first(); + + cache() + ->save($cacheName, $found, DECADE); + } + + return $found; + } + + public function setEpisodePreviewId(int $episodeId): string|false + { + /** @var LazyUuidFromString $uuid */ + $uuid = $this->uuid->{$this->uuidVersion}(); + + if (! $this->update($episodeId, [ + 'preview_id' => $uuid, + ])) { + return false; + } + + return (string) $uuid; + } + /** * Gets all episodes for a podcast ordered according to podcast type Filtered depending on year or season * diff --git a/app/Resources/styles/custom.css b/app/Resources/styles/custom.css index 25883b7427618427ba0e4a71bc826db0b96873ba..6dfa93b1a85be0f5da189e1cc325f115aaf476fb 100644 --- a/app/Resources/styles/custom.css +++ b/app/Resources/styles/custom.css @@ -59,7 +59,7 @@ ); } - .bg-stripes-gray { + .bg-stripes-default { background-image: repeating-linear-gradient( -45deg, #f3f4f6, diff --git a/modules/Admin/Language/en/Episode.php b/modules/Admin/Language/en/Episode.php index 5ed5e3acef257e6d98f03cb839353c24398c5e5c..1a5e57a440d93be5f48bd919a2b86b0033b3f5a8 100644 --- a/modules/Admin/Language/en/Episode.php +++ b/modules/Admin/Language/en/Episode.php @@ -22,6 +22,7 @@ return [ 'all_podcast_episodes' => 'All podcast episodes', 'back_to_podcast' => 'Go back to podcast', 'edit' => 'Edit', + 'preview' => 'Preview', 'publish' => 'Publish', 'publish_edit' => 'Edit publication', 'publish_date_edit' => 'Edit publication date', @@ -211,4 +212,14 @@ return [ 'light' => 'Light', 'light-transparent' => 'Light transparent', ], + 'publication_status_banner' => [ + 'draft_mode' => 'draft mode', + 'text' => '{publication_status, select, + published {This episode is not yet published.} + scheduled {This episode is scheduled for publication on {publication_date}.} + with_podcast {This episode will be published at the same time as the podcast.} + other {This episode is not yet published.} + }', + 'preview' => 'Preview', + ], ]; diff --git a/themes/cp_admin/_layout.php b/themes/cp_admin/_layout.php index bd006e54b37b0652ac7f3c524e3aed3c977d6758..865465fcd1adeba9aafbb948d7da6bbf61cbd928 100644 --- a/themes/cp_admin/_layout.php +++ b/themes/cp_admin/_layout.php @@ -67,6 +67,9 @@ $isEpisodeArea = isset($podcast) && isset($episode); <?= publication_status_banner($podcast->published_at, $podcast->id, $podcast->publication_status) ?> <?php endif; ?> <?php endif; ?> + <?php if ($isEpisodeArea && $episode->publication_status !== 'published'): ?> + <?= episode_publication_status_banner($episode, 'border-b') ?> + <?php endif; ?> <div class="px-2 py-8 mx-auto md:px-12"> <?= view('_message_block') ?> <?= $this->renderSection('content') ?> diff --git a/themes/cp_app/episode/_layout-preview.php b/themes/cp_app/episode/_layout-preview.php new file mode 100644 index 0000000000000000000000000000000000000000..c8caa0368e3c31ccd08ac3e9c09ac5cd84bc7cb5 --- /dev/null +++ b/themes/cp_app/episode/_layout-preview.php @@ -0,0 +1,199 @@ +<?= 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="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" /> + <link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>"> + <link rel="manifest" href="<?= route_to('podcast-webmanifest', esc($podcast->handle)) ?>"> + <meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>"> + <script> + // Check that service workers are supported + if ('serviceWorker' in navigator) { + // Use the window load event to keep the page load performant + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js'); + }); + } + </script> + + <meta name="robots" content="noindex"> + + <title>[<?= lang('Episode.preview.title') ?>] <?= $episode->title ?></title> + <meta name="description" content="<?= esc($episode->description) ?>"> + + <link rel='stylesheet' type='text/css' href='<?= route_to('themes-colors-css') ?>' /> + <?= 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="flex flex-col min-h-screen mx-auto md:min-h-full md:grid md:grid-cols-podcast bg-base theme-<?= service('settings') + ->get('App.theme') ?>"> + <?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 text-white bg-header"> + <a href="<?= route_to('podcast-episodes', esc($podcast->handle)) ?>" class="flex items-center h-full min-w-0 px-2 gap-x-2 focus:ring-accent focus:ring-inset" title="<?= lang('Episode.back_to_episodes', [ + 'podcast' => esc($podcast->title), + ]) ?>"> + <?= icon('arrow-left', 'text-lg flex-shrink-0') ?> + <div class="flex items-center min-w-0 gap-x-2"> + <img class="w-8 h-8 rounded-full" src="<?= $episode->podcast->cover->tiny_url ?>" alt="<?= esc($episode->podcast->title) ?>" loading="lazy" /> + <div class="flex flex-col overflow-hidden"> + <span class="text-sm font-semibold leading-none truncate"><?= esc($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 px-2 gap-x-2"> + <?php if (in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true)): ?> + <button class="p-2 text-red-600 bg-white rounded-full shadow hover:text-red-500 focus:ring-accent" data-toggle="funding-links" data-toggle-class="hidden" title="<?= lang('Podcast.sponsor') ?>"><Icon glyph="heart"></Icon></button> + <?php endif; ?> + <?= anchor_popup( + route_to('follow', esc($podcast->handle)), + icon( + 'social/castopod', + 'mr-2 text-xl text-black/75 group-hover:text-black', + ) . lang('Podcast.follow'), + [ + 'width' => 420, + 'height' => 620, + 'class' => 'group inline-flex items-center px-3 leading-8 text-xs tracking-wider font-semibold text-black uppercase rounded-full shadow focus:ring-accent bg-white', + ], + ) ?> + </div> + </nav> + <header class="relative z-50 flex flex-col col-start-2 px-8 pt-8 pb-4 overflow-hidden bg-accent-base/75 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-overlay filter grayscale" style="background-image: url('<?= get_podcast_banner_url($episode->podcast, 'small') ?>');"></div> + <div class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-background-header to-transparent"></div> + <div class="z-10 flex flex-col items-start gap-y-2 gap-x-4 sm:flex-row"> + <div class="relative flex-shrink-0"> + <?= explicit_badge($episode->parental_advisory === 'explicit', 'rounded absolute left-0 bottom-0 ml-2 mb-2 bg-black/75 text-accent-contrast') ?> + <?php if ($episode->is_premium): ?> + <Icon glyph="exchange-dollar" class="absolute left-0 w-8 pl-2 text-2xl rounded-r-full rounded-tl-lg top-2 text-accent-contrast bg-accent-base" /> + <?php endif; ?> + <img src="<?= $episode->cover->medium_url ?>" alt="<?= esc($episode->title) ?>" class="flex-shrink-0 rounded-md shadow-xl h-36 aspect-square" loading="lazy" /> + </div> + <div class="flex flex-col items-start w-full min-w-0 text-white"> + <?= episode_numbering($episode->number, $episode->season_number, 'text-sm leading-none font-semibold px-1 py-1 text-white/90 border !no-underline border-subtle', true) ?> + <h1 class="inline-flex items-baseline max-w-lg mt-2 text-2xl font-bold sm:leading-none sm:text-3xl font-display line-clamp-2" title="<?= esc($episode->title) ?>"><?= esc($episode->title) ?></h1> + <div class="flex items-center w-full mt-4 gap-x-8"> + <?php if ($episode->persons !== []): ?> + <button class="flex items-center flex-shrink-0 text-xs font-semibold gap-x-2 hover:underline focus:ring-accent" data-toggle="persons-list" data-toggle-class="hidden"> + <span class="inline-flex flex-row-reverse"> + <?php $i = 0; ?> + <?php foreach ($episode->persons as $person): ?> + <img src="<?= get_avatar_url($person, 'thumbnail') ?>" alt="<?= esc($person->full_name) ?>" class="object-cover w-8 h-8 -ml-4 border-2 rounded-full aspect-square border-background-header last:ml-0" loading="lazy" /> + <?php $i++; + if ($i === 3) { + break; + }?> + <?php endforeach; ?> + </span> + <?= 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->cover->thumbnail_url ?>" + title="<?= esc($episode->title) ?>" + podcast="<?= esc($episode->podcast->title) ?>" + src="<?= $episode->audio->file_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"> + <?php if ($episode->published_at): ?> + <?= relative_time($episode->published_at) ?> + <?php else: ?> + <?= lang('Episode.preview.not_published') ?> + <?php endif; ?> + <span class="mx-1">•</span> + <time datetime="PT<?= round($episode->audio->duration, 3) ?>S"> + <?= format_duration_symbol((int) $episode->audio->duration) ?> + </time> + </div> + </div> + </header> + <div class="col-start-2 px-8 py-4 text-white bg-header"> + <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") > 6 || strlen($episode->description) > 500): ?> + <SeeMore class="max-w-xl prose-sm text-white"><?= $episode->getDescriptionHtml('-+Website+-') ?></SeeMore> + <?php else: ?> + <div class="max-w-xl prose-sm text-white"><?= $episode->getDescriptionHtml('-+Website+-') ?></div> + <?php endif; ?> + </div> + <?= $this->include('episode/_partials/navigation') ?> + <?= $this->include('podcast/_partials/premium_banner') ?> + <div class="flex flex-wrap items-center min-h-[2.5rem] col-start-2 p-1 mt-2 md:mt-4 rounded-conditional-full gap-y-2 sm:flex-row bg-accent-base text-accent-contrast" role="alert"> + <div class="flex flex-wrap gap-4 pl-2"> + <div class="inline-flex items-center gap-2 font-semibold tracking-wide uppercase"> + <Icon glyph="eye" /> + <span class="text-xs"><?= lang('Episode.preview.title') ?></span> + </div> + <p class="text-sm"> + <?= lang('Episode.preview.text', [ + 'publication_status' => $episode->publication_status, + 'publication_date' => $episode->published_at ? local_datetime($episode->published_at) : null, + ], null, false); ?> + </p> + </div> + <?php if (auth()->loggedIn()): ?> + <?php if (in_array($episode->publication_status, ['scheduled', 'with_podcast'], true)): ?> + <Button + iconLeft="upload-cloud" + variant="primary" + size="small" + class="ml-auto" + uri="<?= route_to('episode-publish_edit', $episode->podcast_id, $episode->id) ?>"><?= lang('Episode.preview.publish_edit') ?></Button> + <?php else: ?> + <Button + iconLeft="upload-cloud" + variant="secondary" + size="small" + class="ml-auto" + uri="<?= route_to('episode-publish', $episode->podcast_id, $episode->id) ?>"><?= lang('Episode.preview.publish') ?></Button> + <?php endif; ?> + <?php endif; ?> + </div> + <div class="relative grid items-start flex-1 col-start-2 grid-cols-podcastMain gap-x-6"> + <main class="w-full col-start-1 row-start-1 py-6 col-span-full md:col-span-1"> + <?= $this->renderSection('content') ?> + </main> + <div data-sidebar-toggler="backdrop" class="absolute top-0 left-0 z-10 hidden w-full h-full bg-backdrop/75 md:hidden" role="button" tabIndex="0" aria-label="<?= lang('Common.close') ?>"></div> + <?= $this->include('podcast/_partials/sidebar') ?> + </div> + <?= view('_persons_modal', [ + 'title' => lang('Episode.persons_list', [ + 'episodeTitle' => esc($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/navigation.php b/themes/cp_app/episode/_partials/navigation.php index ddeedac4bf8ae3d0fd29ebf93d166235d89b4a3e..73fdcf5b8d42f841972b3c9e721a014b507e35dc 100644 --- a/themes/cp_app/episode/_partials/navigation.php +++ b/themes/cp_app/episode/_partials/navigation.php @@ -1,17 +1,32 @@ <?php declare(strict_types=1); -$navigationItems = [ - [ - 'uri' => route_to('episode', esc($podcast->handle), esc($episode->slug)), - 'label' => lang('Episode.comments'), - 'labelInfo' => $episode->comments_count, - ], - [ - 'uri' => route_to('episode-activity', esc($podcast->handle), esc($episode->slug)), - 'label' => lang('Episode.activity'), - 'labelInfo' => $episode->posts_count, - ], -] +if ($episode->publication_status === 'published') { + $navigationItems = [ + [ + 'uri' => route_to('episode', esc($podcast->handle), esc($episode->slug)), + 'label' => lang('Episode.comments'), + 'labelInfo' => $episode->comments_count, + ], + [ + 'uri' => route_to('episode-activity', esc($podcast->handle), esc($episode->slug)), + 'label' => lang('Episode.activity'), + 'labelInfo' => $episode->posts_count, + ], + ]; +} else { + $navigationItems = [ + [ + 'uri' => route_to('episode-preview', $episode->preview_id), + 'label' => lang('Episode.comments'), + 'labelInfo' => $episode->comments_count, + ], + [ + 'uri' => route_to('episode-preview-activity', $episode->preview_id), + 'label' => lang('Episode.activity'), + 'labelInfo' => $episode->posts_count, + ], + ]; +} ?> <nav class="sticky z-40 flex col-start-2 pt-4 shadow bg-elevated md:px-8 gap-x-2 md:gap-x-4 -top-4 rounded-conditional-b-xl"> <?php foreach ($navigationItems as $item): ?> diff --git a/themes/cp_app/episode/preview-activity.php b/themes/cp_app/episode/preview-activity.php new file mode 100644 index 0000000000000000000000000000000000000000..31320151519145b7ea8bdc5ba32787bba30c6c70 --- /dev/null +++ b/themes/cp_app/episode/preview-activity.php @@ -0,0 +1,15 @@ +<?= $this->extend('episode/_layout-preview') ?> + +<?= $this->section('content') ?> + +<div class="flex flex-col gap-y-4"> + <?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/episode/preview-comments.php b/themes/cp_app/episode/preview-comments.php new file mode 100644 index 0000000000000000000000000000000000000000..33925b23d43268d7af894d9d399724efca86f98c --- /dev/null +++ b/themes/cp_app/episode/preview-comments.php @@ -0,0 +1,14 @@ +<?= $this->extend('episode/_layout-preview') ?> + +<?= $this->section('content') ?> + +<div class="flex flex-col gap-y-2"> + <?php foreach ($episode->comments as $comment): ?> + <?= view('episode/_partials/comment', [ + 'comment' => $comment, + 'podcast' => $podcast, +]) ?> + <?php endforeach; ?> +</div> + +<?= $this->endSection() ?> \ No newline at end of file