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>
203 lines
5.0 KiB
Plaintext
203 lines
5.0 KiB
Plaintext
---
|
|
import type { CollectionEntry } from 'astro:content';
|
|
|
|
interface Props {
|
|
episode: CollectionEntry<'episodes'>;
|
|
}
|
|
|
|
const { episode } = Astro.props;
|
|
const { title, season, episode: episodeNum, pubDate, duration, tags, audioUrl } = episode.data;
|
|
|
|
const formattedDate = new Date(pubDate).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
});
|
|
|
|
const seasonLabel = season === 0 ? 'Special' : `S${season}`;
|
|
const episodeLabel = `${seasonLabel} E${String(episodeNum).padStart(2, '0')}`;
|
|
|
|
const description = episode.body
|
|
? episode.body.replace(/\\?\*\\?\*\\?\*/g, '').replace(/\\\[.*?\\\]/g, '').replace(/\[.*?\]/g, '').trim()
|
|
: '';
|
|
---
|
|
|
|
<article
|
|
class="episode-card card"
|
|
data-season={String(season)}
|
|
data-tags={tags.join(',')}
|
|
data-title={title.toLowerCase()}
|
|
data-description={description.toLowerCase()}
|
|
>
|
|
<div class="episode-card__header">
|
|
<span class="badge">{episodeLabel}</span>
|
|
<span class="episode-card__duration">{duration}</span>
|
|
</div>
|
|
|
|
<a href={`/episodes/${episode.id}`} class="episode-card__title-link">
|
|
<h3 class="episode-card__title">{title}</h3>
|
|
</a>
|
|
|
|
<time class="episode-card__date" datetime={pubDate.toISOString()}>
|
|
{formattedDate}
|
|
</time>
|
|
|
|
{description && (
|
|
<p class="episode-card__description">{description}</p>
|
|
)}
|
|
|
|
<div class="episode-card__footer">
|
|
{tags.length > 0 && (
|
|
<div class="episode-card__tags">
|
|
{tags.slice(0, 4).map((tag: string) => (
|
|
<span class="episode-card__tag">{tag}</span>
|
|
))}
|
|
{tags.length > 4 && (
|
|
<span class="episode-card__tag episode-card__tag--more">+{tags.length - 4}</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
class="btn btn--primary episode-card__play"
|
|
data-audio-url={audioUrl}
|
|
data-episode-title={title}
|
|
data-season={String(season)}
|
|
data-episode-num={String(episodeNum)}
|
|
aria-label={`Play ${title}`}
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
|
</svg>
|
|
Play
|
|
</button>
|
|
</div>
|
|
</article>
|
|
|
|
<style>
|
|
.episode-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-3);
|
|
border-left: 3px solid transparent;
|
|
transition:
|
|
transform var(--transition-base),
|
|
box-shadow var(--transition-base),
|
|
border-color var(--transition-base);
|
|
}
|
|
|
|
.episode-card:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: var(--shadow-lg);
|
|
border-left-color: var(--color-accent);
|
|
}
|
|
|
|
.episode-card__header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.episode-card__duration {
|
|
font-size: var(--text-xs);
|
|
color: var(--color-text-muted);
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.episode-card__title-link {
|
|
color: inherit;
|
|
text-decoration: none;
|
|
}
|
|
.episode-card__title-link:hover {
|
|
color: var(--color-accent);
|
|
}
|
|
|
|
.episode-card__title {
|
|
font-size: var(--text-lg);
|
|
font-weight: 600;
|
|
line-height: 1.3;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.episode-card__date {
|
|
font-size: var(--text-sm);
|
|
color: var(--color-text-muted);
|
|
}
|
|
|
|
.episode-card__description {
|
|
font-size: var(--text-sm);
|
|
color: var(--color-text-secondary);
|
|
line-height: 1.5;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.episode-card__footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: var(--space-3);
|
|
margin-top: auto;
|
|
padding-top: var(--space-3);
|
|
border-top: 1px solid var(--color-border);
|
|
}
|
|
|
|
.episode-card__tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--space-1);
|
|
}
|
|
|
|
.episode-card__tag {
|
|
font-size: 0.65rem;
|
|
padding: 2px var(--space-2);
|
|
border-radius: 9999px;
|
|
background: var(--color-bg-tertiary);
|
|
color: var(--color-text-muted);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.episode-card__tag--more {
|
|
background: var(--color-accent-glow);
|
|
color: var(--color-accent);
|
|
}
|
|
|
|
.episode-card__play {
|
|
flex-shrink: 0;
|
|
padding: var(--space-2) var(--space-3);
|
|
font-size: var(--text-xs);
|
|
}
|
|
|
|
.episode-card__play svg {
|
|
width: 12px;
|
|
height: 12px;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
document.addEventListener('astro:page-load', () => {
|
|
document.querySelectorAll<HTMLButtonElement>('.episode-card__play').forEach((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>
|