Files
claudetools/projects/radio-show/website/src/components/episodes/EpisodeCard.astro
Mike Swanson ee89727662 Radio show website: Full Astro build with 194 episodes imported
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>
2026-03-14 20:44:42 -07:00

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>