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>
251 lines
7.5 KiB
Plaintext
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>
|