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:
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user