From 5d4ba7e593c1c9fd4f84cda7b40d8139b9e4992c Mon Sep 17 00:00:00 2001 From: nmemmert Date: Wed, 22 Jul 2026 14:32:05 -0400 Subject: [PATCH] Replace Spotify iframe with custom centered player (layout A); v1.1.5 - EpisodeAudioPlayer full variant: centered title, large gold play button, skip-back 15s / skip-forward 30s flanking controls, full-width scrubber - Compact variant (study sections) unchanged - Episode detail page fetches direct Anchor MP3 URL from /api/episodes by title match; falls back to resolvedEmbedUrl while loading - Plays stream from Anchor CDN so stats still count Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- src/App.css | 238 +++++++++++++++++--------- src/App.tsx | 35 ++-- src/components/EpisodeAudioPlayer.tsx | 203 ++++++++++++++-------- 4 files changed, 303 insertions(+), 175 deletions(-) diff --git a/package.json b/package.json index 452eadf..08c5823 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "1.1.4", + "version": "1.1.5", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.css b/src/App.css index a03c029..3ea7d3b 100644 --- a/src/App.css +++ b/src/App.css @@ -3155,85 +3155,73 @@ } /* Custom audio player */ +/* ── Full / cinematic layout (Option A) ── */ .episode-audio-player { background: var(--brand-black-2, #0f0f0c); border: 1px solid rgba(201, 168, 76, 0.2); - border-radius: 10px; - padding: 1rem 1.1rem; -} - -.episode-audio-player__inner { - display: flex; - align-items: center; - gap: 14px; -} - -.episode-audio-player__art { - width: 56px; - height: 56px; - border-radius: 6px; - object-fit: cover; - flex-shrink: 0; -} - -.episode-audio-player__body { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 4px; -} - -.episode-audio-player__meta { - display: flex; - align-items: center; - justify-content: space-between; + border-radius: 12px; + padding: 1.75rem 1.5rem 1.4rem; + text-align: center; } .episode-audio-player__show-name { - font-size: 0.7rem; + font-size: 0.65rem; + letter-spacing: 0.12em; + text-transform: uppercase; color: var(--brand-gold, #c9a84c); font-family: var(--brand-font-body, Georgia, serif); - letter-spacing: 0.02em; -} - -.episode-audio-player__spotify-link { - color: rgba(176, 164, 140, 0.5); - display: flex; - align-items: center; - transition: color 0.15s; - flex-shrink: 0; -} - -.episode-audio-player__spotify-link:hover { - color: #1DB954; -} - -.episode-audio-player__spotify-link svg { - width: 16px; - height: 16px; + margin-bottom: 0.4rem; } .episode-audio-player__title { - font-size: 0.88rem; + font-size: 1rem; color: var(--brand-warm-white, #f0ead8); - margin: 0; + margin: 0 0 1.4rem; font-family: var(--brand-font-heading, Georgia, serif); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + line-height: 1.35; + text-wrap: balance; } -.episode-audio-player__controls { +.episode-audio-player__play-row { display: flex; align-items: center; - gap: 10px; - margin-top: 2px; + justify-content: center; + gap: 1.1rem; + margin-bottom: 1.1rem; +} + +.episode-audio-player__skip { + background: none; + border: none; + cursor: pointer; + color: rgba(240, 234, 216, 0.45); + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; + transition: color 0.15s; + padding: 0.2rem; +} + +.episode-audio-player__skip:hover { + color: var(--brand-warm-white, #f0ead8); +} + +.episode-audio-player__skip svg { + width: 22px; + height: 22px; +} + +.episode-audio-player__skip span { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 0.55rem; + letter-spacing: 0.06em; + text-transform: uppercase; } .episode-audio-player__play { - width: 34px; - height: 34px; + width: 58px; + height: 58px; border-radius: 50%; background: var(--brand-gold, #c9a84c); border: none; @@ -3243,11 +3231,17 @@ justify-content: center; flex-shrink: 0; color: var(--brand-black, #0a0a08); - transition: background 0.15s; + transition: background 0.15s, transform 0.1s; } .episode-audio-player__play:hover { background: var(--brand-gold-light, #e0c070); + transform: scale(1.05); +} + +.episode-audio-player__play:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.45); } @keyframes buffering-pulse { @@ -3260,25 +3254,18 @@ } .episode-audio-player__play svg { - width: 14px; - height: 14px; -} - -.episode-audio-player__track { - flex: 1; - display: flex; - flex-direction: column; - gap: 4px; - min-width: 0; + width: 20px; + height: 20px; } .episode-audio-player__bar { position: relative; height: 3px; - background: rgba(42, 37, 24, 0.9); + background: rgba(201, 168, 76, 0.12); border-radius: 2px; cursor: pointer; outline: none; + margin-bottom: 6px; } .episode-audio-player__bar:focus-visible { @@ -3286,6 +3273,9 @@ } .episode-audio-player__fill { + position: absolute; + left: 0; + top: 0; height: 100%; background: var(--brand-gold, #c9a84c); border-radius: 2px; @@ -3296,30 +3286,96 @@ position: absolute; top: 50%; transform: translate(-50%, -50%); - width: 10px; - height: 10px; + width: 11px; + height: 11px; border-radius: 50%; - background: var(--brand-gold-light, #e0c070); + background: var(--brand-gold, #c9a84c); pointer-events: none; } .episode-audio-player__times { display: flex; justify-content: space-between; - font-size: 0.68rem; + font-size: 0.62rem; color: var(--brand-muted, #b0a48c); - font-family: var(--brand-font-body, Georgia, serif); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-variant-numeric: tabular-nums; } -/* Compact variant (study sections) */ -.episode-audio-player--compact .episode-audio-player__art { - width: 42px; - height: 42px; +.episode-audio-player__spotify-row { + margin-top: 1rem; + display: flex; + justify-content: center; +} + +.episode-audio-player__spotify-link { + display: inline-flex; + align-items: center; + gap: 6px; + color: rgba(240, 234, 216, 0.4); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 0.73rem; + text-decoration: none; + transition: color 0.15s; +} + +.episode-audio-player__spotify-link:hover { + color: #1DB954; +} + +.episode-audio-player__spotify-link svg { + width: 14px; + height: 14px; + flex-shrink: 0; +} + +/* ── Compact variant (study sections) ── */ +.episode-audio-player--compact { + background: var(--brand-black-2, #0f0f0c); + border: 1px solid rgba(201, 168, 76, 0.2); + border-radius: 10px; + padding: 0.75rem 1rem; + text-align: left; +} + +.episode-audio-player--compact .episode-audio-player__inner { + display: flex; + align-items: center; + gap: 10px; +} + +.episode-audio-player--compact .episode-audio-player__body { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; +} + +.episode-audio-player--compact .episode-audio-player__title { + font-size: 0.8rem; + text-wrap: unset; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 0; + line-height: 1.2; +} + +.episode-audio-player--compact .episode-audio-player__controls { + display: flex; + align-items: center; + gap: 8px; } .episode-audio-player--compact .episode-audio-player__play { width: 28px; height: 28px; + transform: none; +} + +.episode-audio-player--compact .episode-audio-player__play:hover { + transform: none; } .episode-audio-player--compact .episode-audio-player__play svg { @@ -3327,12 +3383,26 @@ height: 11px; } -.episode-audio-player--compact .episode-audio-player__bar { - height: 2px; +.episode-audio-player--compact .episode-audio-player__track { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; } -.episode-audio-player--compact .episode-audio-player__title { - font-size: 0.8rem; +.episode-audio-player--compact .episode-audio-player__bar { + height: 2px; + margin-bottom: 0; +} + +.episode-audio-player--compact .episode-audio-player__spotify-link { + font-size: 0; +} + +.episode-audio-player--compact .episode-audio-player__spotify-link svg { + width: 16px; + height: 16px; } .episode-detail-show-notes, diff --git a/src/App.tsx b/src/App.tsx index 8d223eb..e31370b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1282,6 +1282,21 @@ function EpisodeDetailPage({ content }: { content: SiteContent }) { const { id } = useParams<{ id: string }>() const episode = (content.podcastFeaturedLinks ?? []).find(e => e.id === id) const [resolvedEmbedUrl, setResolvedEmbedUrl] = useState('') + const [audioUrl, setAudioUrl] = useState('') + + // Fetch the direct MP3 URL from the RSS-backed episodes list, matching by title + useEffect(() => { + if (!episode) return + fetch('/api/episodes') + .then(r => r.ok ? r.json() : Promise.reject()) + .then((data: { episodes?: { title: string; audioUrl: string }[] }) => { + const match = (data.episodes ?? []).find(e => + e.title?.trim().toLowerCase() === episode.title?.trim().toLowerCase() + ) + if (match?.audioUrl) setAudioUrl(match.audioUrl) + }) + .catch(() => {}) + }, [episode]) useEffect(() => { let cancelled = false @@ -1383,22 +1398,12 @@ function EpisodeDetailPage({ content }: { content: SiteContent }) {

{episode.title}

{episode.summary &&

{episode.summary}

} - {(episode.embedUrl || episode.url) && ( + {(episode.embedUrl || episode.url || audioUrl) && (
- {resolvedEmbedUrl ? ( - resolvedEmbedUrl.includes('spotify.com') ? ( -