Complete website for The Computer Guru Show (radio.azcomputerguru.com): - Astro 6.0.4 static site with React islands - 194 episodes imported from gurushow.com RSS feed - Dark/light mode HSL design system - Persistent audio player with session persistence - Episode archive with search and season filtering - Home page with animated hero, stats, latest episodes - All pages: About, Subscribe, Community, Live, Contact, Blog, 404 - Podcast RSS feed with iTunes namespace - Session log updated Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
459 lines
12 KiB
Plaintext
459 lines
12 KiB
Plaintext
---
|
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
import EpisodeCard from '../../components/episodes/EpisodeCard.astro';
|
|
import { getCollection, render } from 'astro:content';
|
|
import type { CollectionEntry } from 'astro:content';
|
|
|
|
export async function getStaticPaths() {
|
|
const episodes = await getCollection('episodes');
|
|
return episodes.map((episode) => ({
|
|
params: { slug: episode.id },
|
|
props: { episode },
|
|
}));
|
|
}
|
|
|
|
interface Props {
|
|
episode: CollectionEntry<'episodes'>;
|
|
}
|
|
|
|
const { episode } = Astro.props;
|
|
const { title, season, episode: episodeNum, pubDate, duration, audioUrl, tags, chapters, originalUrl } = episode.data;
|
|
|
|
const { Content } = await render(episode);
|
|
|
|
const formattedDate = new Date(pubDate).toLocaleDateString('en-US', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
});
|
|
|
|
const seasonLabel = season === 0 ? 'Special' : `Season ${season}`;
|
|
const episodeLabel = season === 0 ? 'Special' : `S${season} E${String(episodeNum).padStart(2, '0')}`;
|
|
|
|
const allEpisodes = await getCollection('episodes');
|
|
const relatedEpisodes = allEpisodes
|
|
.filter((ep) => ep.data.season === season && ep.id !== episode.id)
|
|
.sort((a, b) => new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime())
|
|
.slice(0, 4);
|
|
---
|
|
|
|
<BaseLayout title={title} description={`${episodeLabel} of The Computer Guru Show`}>
|
|
<article class="episode-detail">
|
|
<div class="container container--narrow">
|
|
|
|
<nav class="breadcrumb" aria-label="Breadcrumb">
|
|
<ol class="breadcrumb__list">
|
|
<li class="breadcrumb__item">
|
|
<a href="/episodes">Episodes</a>
|
|
</li>
|
|
<li class="breadcrumb__separator" aria-hidden="true">/</li>
|
|
<li class="breadcrumb__item">
|
|
<a href={`/episodes?season=${season}`}>{seasonLabel}</a>
|
|
</li>
|
|
<li class="breadcrumb__separator" aria-hidden="true">/</li>
|
|
<li class="breadcrumb__item breadcrumb__item--current" aria-current="page">
|
|
{title}
|
|
</li>
|
|
</ol>
|
|
</nav>
|
|
|
|
<header class="episode-detail__header">
|
|
<span class="badge">{episodeLabel}</span>
|
|
<h1 class="episode-detail__title">{title}</h1>
|
|
|
|
<div class="episode-detail__meta">
|
|
<time datetime={pubDate.toISOString()} class="episode-detail__date">
|
|
{formattedDate}
|
|
</time>
|
|
<span class="episode-detail__divider" aria-hidden="true">|</span>
|
|
<span class="episode-detail__duration">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
<polyline points="12 6 12 12 16 14"></polyline>
|
|
</svg>
|
|
{duration}
|
|
</span>
|
|
</div>
|
|
</header>
|
|
|
|
<section class="episode-detail__player">
|
|
<audio
|
|
controls
|
|
preload="metadata"
|
|
class="episode-detail__audio"
|
|
src={audioUrl}
|
|
>
|
|
Your browser does not support the audio element.
|
|
</audio>
|
|
<button
|
|
class="btn btn--ghost episode-detail__play-persistent"
|
|
data-audio-url={audioUrl}
|
|
data-episode-title={title}
|
|
data-season={String(season)}
|
|
data-episode-num={String(episodeNum)}
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<polygon points="5 3 19 12 5 21 5 3" fill="currentColor"></polygon>
|
|
</svg>
|
|
Play in Persistent Player
|
|
</button>
|
|
</section>
|
|
|
|
{chapters && chapters.length > 0 && (
|
|
<section class="episode-detail__chapters">
|
|
<h2 class="episode-detail__section-title">Chapters</h2>
|
|
<ol class="chapters-list">
|
|
{chapters.map((chapter) => (
|
|
<li class="chapters-list__item">
|
|
<span class="chapters-list__time">{chapter.time}</span>
|
|
<span class="chapters-list__title">{chapter.title}</span>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</section>
|
|
)}
|
|
|
|
<section class="episode-detail__content">
|
|
<h2 class="episode-detail__section-title">Show Notes</h2>
|
|
<div class="prose">
|
|
<Content />
|
|
</div>
|
|
</section>
|
|
|
|
{tags.length > 0 && (
|
|
<section class="episode-detail__tags">
|
|
<h2 class="episode-detail__section-title">Tags</h2>
|
|
<div class="tag-list">
|
|
{tags.map((tag: string) => (
|
|
<span class="badge">{tag}</span>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
<section class="episode-detail__platforms">
|
|
<h2 class="episode-detail__section-title">Listen On</h2>
|
|
<div class="platform-links">
|
|
<a
|
|
href={audioUrl}
|
|
class="btn btn--ghost"
|
|
download
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
<polyline points="7 10 12 15 17 10"></polyline>
|
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
|
</svg>
|
|
Download MP3
|
|
</a>
|
|
{originalUrl && (
|
|
<a
|
|
href={originalUrl}
|
|
class="btn btn--ghost"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
|
<polyline points="15 3 21 3 21 9"></polyline>
|
|
<line x1="10" y1="14" x2="21" y2="3"></line>
|
|
</svg>
|
|
Original Post
|
|
</a>
|
|
)}
|
|
<a
|
|
href="/feed.xml"
|
|
class="btn btn--ghost"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M4 11a9 9 0 0 1 9 9"></path>
|
|
<path d="M4 4a16 16 0 0 1 16 16"></path>
|
|
<circle cx="5" cy="19" r="1"></circle>
|
|
</svg>
|
|
RSS Feed
|
|
</a>
|
|
</div>
|
|
</section>
|
|
|
|
</div>
|
|
|
|
{relatedEpisodes.length > 0 && (
|
|
<section class="episode-detail__related section">
|
|
<div class="container">
|
|
<h2 class="section__title">More from {seasonLabel}</h2>
|
|
<div class="grid grid--3">
|
|
{relatedEpisodes.map((ep) => (
|
|
<EpisodeCard episode={ep} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
</article>
|
|
</BaseLayout>
|
|
|
|
<style>
|
|
.breadcrumb {
|
|
margin-bottom: var(--space-8);
|
|
padding-top: var(--space-6);
|
|
}
|
|
|
|
.breadcrumb__list {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.breadcrumb__item a {
|
|
font-size: var(--text-sm);
|
|
color: var(--color-text-muted);
|
|
}
|
|
|
|
.breadcrumb__item a:hover {
|
|
color: var(--color-accent);
|
|
}
|
|
|
|
.breadcrumb__item--current {
|
|
font-size: var(--text-sm);
|
|
color: var(--color-text-secondary);
|
|
max-width: 300px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.breadcrumb__separator {
|
|
color: var(--color-text-muted);
|
|
font-size: var(--text-sm);
|
|
}
|
|
|
|
.episode-detail__header {
|
|
margin-bottom: var(--space-8);
|
|
}
|
|
|
|
.episode-detail__header .badge {
|
|
margin-bottom: var(--space-3);
|
|
}
|
|
|
|
.episode-detail__title {
|
|
font-size: var(--text-3xl);
|
|
margin-top: var(--space-3);
|
|
margin-bottom: var(--space-4);
|
|
line-height: 1.15;
|
|
}
|
|
|
|
.episode-detail__meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-3);
|
|
color: var(--color-text-muted);
|
|
font-size: var(--text-sm);
|
|
}
|
|
|
|
.episode-detail__duration {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--space-1);
|
|
}
|
|
|
|
.episode-detail__divider {
|
|
color: var(--color-border);
|
|
}
|
|
|
|
.episode-detail__player {
|
|
margin-bottom: var(--space-8);
|
|
padding: var(--space-6);
|
|
background: var(--color-bg-secondary);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
}
|
|
|
|
.episode-detail__audio {
|
|
width: 100%;
|
|
height: 48px;
|
|
}
|
|
|
|
.episode-detail__play-persistent {
|
|
margin-top: var(--space-3);
|
|
font-size: var(--text-sm);
|
|
}
|
|
|
|
.episode-detail__audio::-webkit-media-controls-panel {
|
|
background: var(--color-bg-tertiary);
|
|
}
|
|
|
|
.episode-detail__section-title {
|
|
font-size: var(--text-xl);
|
|
margin-bottom: var(--space-4);
|
|
}
|
|
|
|
.episode-detail__chapters {
|
|
margin-bottom: var(--space-8);
|
|
}
|
|
|
|
.chapters-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.chapters-list__item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-4);
|
|
padding: var(--space-3) var(--space-4);
|
|
border-bottom: 1px solid var(--color-border);
|
|
transition: background var(--transition-fast);
|
|
}
|
|
|
|
.chapters-list__item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.chapters-list__item:hover {
|
|
background: var(--color-bg-tertiary);
|
|
}
|
|
|
|
.chapters-list__time {
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-sm);
|
|
color: var(--color-accent);
|
|
min-width: 60px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.chapters-list__title {
|
|
font-size: var(--text-sm);
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.episode-detail__content {
|
|
margin-bottom: var(--space-8);
|
|
}
|
|
|
|
.prose {
|
|
color: var(--color-text-secondary);
|
|
line-height: 1.8;
|
|
font-size: var(--text-base);
|
|
}
|
|
|
|
.prose :global(p) {
|
|
margin-bottom: var(--space-4);
|
|
}
|
|
|
|
.prose :global(a) {
|
|
color: var(--color-accent);
|
|
text-decoration: underline;
|
|
text-underline-offset: 2px;
|
|
}
|
|
|
|
.prose :global(a:hover) {
|
|
color: var(--color-accent-hover);
|
|
}
|
|
|
|
.prose :global(ul),
|
|
.prose :global(ol) {
|
|
margin-bottom: var(--space-4);
|
|
padding-left: var(--space-6);
|
|
}
|
|
|
|
.prose :global(li) {
|
|
margin-bottom: var(--space-2);
|
|
}
|
|
|
|
.prose :global(strong) {
|
|
color: var(--color-text-primary);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.prose :global(h2),
|
|
.prose :global(h3) {
|
|
margin-top: var(--space-8);
|
|
margin-bottom: var(--space-3);
|
|
}
|
|
|
|
.episode-detail__tags {
|
|
margin-bottom: var(--space-8);
|
|
}
|
|
|
|
.tag-list {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.episode-detail__platforms {
|
|
margin-bottom: var(--space-8);
|
|
padding: var(--space-6);
|
|
background: var(--color-bg-secondary);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
}
|
|
|
|
.platform-links {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--space-3);
|
|
}
|
|
|
|
.episode-detail__related {
|
|
border-top: 1px solid var(--color-border);
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.episode-detail__title {
|
|
font-size: var(--text-2xl);
|
|
}
|
|
|
|
.episode-detail__meta {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: var(--space-1);
|
|
}
|
|
|
|
.episode-detail__divider {
|
|
display: none;
|
|
}
|
|
|
|
.platform-links {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.platform-links .btn {
|
|
width: 100%;
|
|
justify-content: center;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
document.addEventListener('astro:page-load', () => {
|
|
const btn = document.querySelector<HTMLButtonElement>('.episode-detail__play-persistent');
|
|
if (btn) {
|
|
btn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const audioUrl = btn.dataset.audioUrl;
|
|
const title = btn.dataset.episodeTitle;
|
|
const season = parseInt(btn.dataset.season ?? '0', 10);
|
|
const episode = parseInt(btn.dataset.episodeNum ?? '0', 10);
|
|
|
|
if (audioUrl) {
|
|
document.dispatchEvent(
|
|
new CustomEvent('play-episode', {
|
|
detail: { audioUrl, title, season, episode },
|
|
})
|
|
);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
</script>
|