diff --git a/frontend/src/components/HeroPlayer.tsx b/frontend/src/components/HeroPlayer.tsx index bfc6fe2..3fc68cb 100644 --- a/frontend/src/components/HeroPlayer.tsx +++ b/frontend/src/components/HeroPlayer.tsx @@ -23,24 +23,30 @@ const lsSet = (k: string, v: string) => { }; export function HeroPlayer({ initialStations }: Props) { + // ssr-safe defaults; localStorage + Date reads happen post-mount to avoid + // hydration mismatches (#418, #423, #425). const [stations, setStations] = useState(initialStations); - const [selectedId, setSelectedId] = useState(() => { - const saved = lsGet('denpa:station'); - if (saved && initialStations.some((s) => s.id === saved)) return saved; - return initialStations[0]?.id ?? ''; - }); - const [format, setFormat] = useState(() => (lsGet('denpa:format') === 'opus' ? 'opus' : 'mp3')); - const [vol, setVol] = useState(() => { - const v = parseFloat(lsGet('denpa:vol') ?? '0.72'); - return isNaN(v) ? 0.72 : Math.max(0, Math.min(1, v)); - }); + const [selectedId, setSelectedId] = useState(initialStations[0]?.id ?? ''); + const [format, setFormat] = useState('mp3'); + const [vol, setVol] = useState(0.72); const [muted, setMuted] = useState(false); const [playing, setPlaying] = useState(false); const [now, setNow] = useState(null); const [listeners, setListeners] = useState(null); const [elapsed, setElapsed] = useState(0); const [copyState, setCopyState] = useState<'idle' | 'ok'>('idle'); - const [clockNow, setClockNow] = useState(() => new Date()); + const [clockText, setClockText] = useState<{ date: string; time: string }>({ date: '', time: '' }); + + // restore persisted UI state once on the client + useEffect(() => { + const savedStation = lsGet('denpa:station'); + if (savedStation && initialStations.some((s) => s.id === savedStation)) { + setSelectedId(savedStation); + } + if (lsGet('denpa:format') === 'opus') setFormat('opus'); + const savedVol = parseFloat(lsGet('denpa:vol') ?? ''); + if (!isNaN(savedVol)) setVol(Math.max(0, Math.min(1, savedVol))); + }, [initialStations]); const audioRef = useRef(null); const station = stations.find((s) => s.id === selectedId); @@ -155,9 +161,14 @@ export function HeroPlayer({ initialStations }: Props) { return () => clearInterval(id); }, [now?.started_at]); - // wall clock for header + // wall clock for header — only ticks after mount so ssr/hydrate match (empty strings) useEffect(() => { - const id = setInterval(() => setClockNow(new Date()), 1000); + const tick = () => { + const d = new Date(); + setClockText({ date: fmtJpDate(d), time: fmtJpTime(d) }); + }; + tick(); + const id = setInterval(tick, 1000); return () => clearInterval(id); }, []); @@ -238,8 +249,8 @@ export function HeroPlayer({ initialStations }: Props) { tune in.
tune out.
melt yr brain {'<3'}
-
{fmtJpDate(clockNow)}
-
{fmtJpTime(clockNow)}
+
{clockText.date || ' '}
+
{clockText.time || ' '}
[{playing ? 'REC' : 'OFF'}] {playing ? 'LIVE' : 'PAUSED'} · {listeners ?? '—'} listening