Files
claudetools/projects/radio-show/website/src/pages/episodes/index.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

251 lines
7.5 KiB
Plaintext

---
import BaseLayout from '../../layouts/BaseLayout.astro';
import EpisodeCard from '../../components/episodes/EpisodeCard.astro';
import SeasonFilter from '../../components/episodes/SeasonFilter.astro';
import { getCollection } from 'astro:content';
const allEpisodes = await getCollection('episodes');
const sortedEpisodes = allEpisodes.sort((a, b) =>
new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime()
);
const seasonMap = new Map<number, number>();
for (const ep of sortedEpisodes) {
const s = ep.data.season;
seasonMap.set(s, (seasonMap.get(s) || 0) + 1);
}
const seasonCounts = Array.from(seasonMap.entries())
.filter(([season]) => season > 0)
.sort((a, b) => b[0] - a[0])
.map(([season, count]) => ({
season,
count,
label: `Season ${season}`,
}));
const specialCount = seasonMap.get(0) || 0;
if (specialCount > 0) {
seasonCounts.push({ season: 0, count: specialCount, label: 'Specials' });
}
---
<BaseLayout title="Episodes" description="Browse all episodes of The Computer Guru Show">
<section class="section">
<div class="container">
<div class="episodes-header">
<h1 class="episodes-header__title">Episodes</h1>
<span class="episodes-header__count">{sortedEpisodes.length} episodes</span>
</div>
<div class="episodes-search">
<div class="episodes-search__input-wrap">
<svg class="episodes-search__icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input
type="text"
id="episode-search"
class="episodes-search__input"
placeholder="Search episodes..."
autocomplete="off"
/>
<button
type="button"
id="search-clear"
class="episodes-search__clear"
aria-label="Clear search"
hidden
>
<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">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<SeasonFilter seasonCounts={seasonCounts} totalCount={sortedEpisodes.length} />
<p id="results-count" class="results-count" hidden></p>
<p id="no-results" class="no-results" hidden>
No episodes match your search. Try different keywords or clear the filter.
</p>
<div class="grid grid--3" id="episodes-grid">
{sortedEpisodes.map((episode) => (
<EpisodeCard episode={episode} />
))}
</div>
</div>
</section>
<script is:inline>
(function () {
const searchInput = document.getElementById('episode-search');
const clearBtn = document.getElementById('search-clear');
const grid = document.getElementById('episodes-grid');
const cards = Array.from(grid.querySelectorAll('.episode-card'));
const filterTabs = document.querySelectorAll('.season-filter__tab');
const resultsCount = document.getElementById('results-count');
const noResults = document.getElementById('no-results');
let activeSeason = 'all';
let searchTerm = '';
function filterCards() {
let visibleCount = 0;
cards.forEach(function (card) {
const cardSeason = card.getAttribute('data-season');
const cardTitle = card.getAttribute('data-title') || '';
const cardDescription = card.getAttribute('data-description') || '';
const matchesSeason = activeSeason === 'all' || cardSeason === activeSeason;
const matchesSearch = !searchTerm ||
cardTitle.includes(searchTerm) ||
cardDescription.includes(searchTerm);
if (matchesSeason && matchesSearch) {
card.style.display = '';
visibleCount++;
} else {
card.style.display = 'none';
}
});
if (searchTerm || activeSeason !== 'all') {
resultsCount.textContent = visibleCount + ' episode' + (visibleCount !== 1 ? 's' : '') + ' found';
resultsCount.hidden = false;
} else {
resultsCount.hidden = true;
}
noResults.hidden = visibleCount > 0;
}
filterTabs.forEach(function (tab) {
tab.addEventListener('click', function () {
filterTabs.forEach(function (t) {
t.classList.remove('season-filter__tab--active');
});
tab.classList.add('season-filter__tab--active');
activeSeason = tab.getAttribute('data-season');
filterCards();
});
});
searchInput.addEventListener('input', function () {
searchTerm = searchInput.value.toLowerCase().trim();
clearBtn.hidden = searchTerm.length === 0;
filterCards();
});
clearBtn.addEventListener('click', function () {
searchInput.value = '';
searchTerm = '';
clearBtn.hidden = true;
filterCards();
searchInput.focus();
});
})();
</script>
</BaseLayout>
<style>
.episodes-header {
display: flex;
align-items: baseline;
gap: var(--space-4);
margin-bottom: var(--space-8);
}
.episodes-header__title {
font-size: var(--text-3xl);
}
.episodes-header__count {
font-size: var(--text-sm);
color: var(--color-text-muted);
font-weight: 500;
}
.episodes-search {
margin-bottom: var(--space-6);
}
.episodes-search__input-wrap {
position: relative;
max-width: 480px;
}
.episodes-search__icon {
position: absolute;
left: var(--space-4);
top: 50%;
transform: translateY(-50%);
color: var(--color-text-muted);
pointer-events: none;
}
.episodes-search__input {
width: 100%;
padding: var(--space-3) var(--space-4);
padding-left: calc(var(--space-4) + 18px + var(--space-3));
padding-right: calc(var(--space-4) + 16px + var(--space-3));
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
color: var(--color-text-primary);
font-size: var(--text-base);
font-family: var(--font-sans);
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.episodes-search__input::placeholder {
color: var(--color-text-muted);
}
.episodes-search__input:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 3px var(--color-accent-glow);
}
.episodes-search__clear {
position: absolute;
right: var(--space-3);
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
padding: var(--space-1);
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
transition: color var(--transition-fast);
}
.episodes-search__clear:hover {
color: var(--color-text-primary);
}
.results-count {
font-size: var(--text-sm);
color: var(--color-text-muted);
margin-bottom: var(--space-4);
}
.no-results {
text-align: center;
color: var(--color-text-muted);
padding: var(--space-16) var(--space-4);
font-size: var(--text-lg);
}
</style>