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>
This commit is contained in:
2026-03-14 20:44:42 -07:00
parent 1adc2ed3a4
commit ee89727662
236 changed files with 16513 additions and 0 deletions

View File

@@ -0,0 +1,202 @@
---
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>

View File

@@ -0,0 +1,110 @@
---
interface SeasonCount {
season: number;
count: number;
label: string;
}
interface Props {
seasonCounts: SeasonCount[];
totalCount: number;
}
const { seasonCounts, totalCount } = Astro.props;
---
<div class="season-filter" id="season-filter">
<div class="season-filter__scroll">
<button
class="season-filter__tab season-filter__tab--active"
data-season="all"
type="button"
>
All Episodes
<span class="season-filter__count">{totalCount}</span>
</button>
{seasonCounts.map(({ season, count, label }) => (
<button
class="season-filter__tab"
data-season={String(season)}
type="button"
>
{label}
<span class="season-filter__count">{count}</span>
</button>
))}
</div>
</div>
<style>
.season-filter {
margin-bottom: var(--space-6);
border-bottom: 1px solid var(--color-border);
}
.season-filter__scroll {
display: flex;
gap: var(--space-1);
overflow-x: auto;
scrollbar-width: thin;
scrollbar-color: var(--color-border) transparent;
padding-bottom: 1px;
-webkit-overflow-scrolling: touch;
}
.season-filter__scroll::-webkit-scrollbar {
height: 4px;
}
.season-filter__scroll::-webkit-scrollbar-track {
background: transparent;
}
.season-filter__scroll::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 2px;
}
.season-filter__tab {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: none;
border: none;
border-bottom: 2px solid transparent;
color: var(--color-text-secondary);
font-size: var(--text-sm);
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition:
color var(--transition-fast),
border-color var(--transition-fast);
margin-bottom: -1px;
}
.season-filter__tab:hover {
color: var(--color-text-primary);
}
.season-filter__tab--active {
color: var(--color-accent);
border-bottom-color: var(--color-accent);
}
.season-filter__count {
font-size: var(--text-xs);
padding: 1px var(--space-2);
border-radius: 9999px;
background: var(--color-bg-tertiary);
color: var(--color-text-muted);
font-weight: 600;
}
.season-filter__tab--active .season-filter__count {
background: var(--color-accent-glow);
color: var(--color-accent);
}
</style>