feat(episode): add preview link in admin to view and share episode before publication

This commit is contained in:
Yassine Doghri 2023-08-28 13:53:04 +00:00
parent 7a1eea58d3
commit 7d21b3509e
15 changed files with 515 additions and 26 deletions

View File

@ -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',

View File

@ -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,
]);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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"

View File

@ -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));
}
}
}

View File

@ -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',
],
];

View File

@ -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
*

View File

@ -59,7 +59,7 @@
);
}
.bg-stripes-gray {
.bg-stripes-default {
background-image: repeating-linear-gradient(
-45deg,
#f3f4f6,

View File

@ -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',
],
];

View File

@ -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') ?>

View File

@ -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>

View File

@ -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): ?>

View File

@ -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() ?>

View File

@ -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() ?>