fix(frontend): defer date and localStorage reads to post-mount to fix hydration mismatches
This commit is contained in:
parent
9f9c2148b4
commit
8d00c10b9f
1 changed files with 26 additions and 15 deletions
|
|
@ -23,24 +23,30 @@ const lsSet = (k: string, v: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function HeroPlayer({ initialStations }: Props) {
|
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<Station[]>(initialStations);
|
const [stations, setStations] = useState<Station[]>(initialStations);
|
||||||
const [selectedId, setSelectedId] = useState<string>(() => {
|
const [selectedId, setSelectedId] = useState<string>(initialStations[0]?.id ?? '');
|
||||||
const saved = lsGet('denpa:station');
|
const [format, setFormat] = useState<Format>('mp3');
|
||||||
if (saved && initialStations.some((s) => s.id === saved)) return saved;
|
const [vol, setVol] = useState<number>(0.72);
|
||||||
return initialStations[0]?.id ?? '';
|
|
||||||
});
|
|
||||||
const [format, setFormat] = useState<Format>(() => (lsGet('denpa:format') === 'opus' ? 'opus' : 'mp3'));
|
|
||||||
const [vol, setVol] = useState<number>(() => {
|
|
||||||
const v = parseFloat(lsGet('denpa:vol') ?? '0.72');
|
|
||||||
return isNaN(v) ? 0.72 : Math.max(0, Math.min(1, v));
|
|
||||||
});
|
|
||||||
const [muted, setMuted] = useState(false);
|
const [muted, setMuted] = useState(false);
|
||||||
const [playing, setPlaying] = useState(false);
|
const [playing, setPlaying] = useState(false);
|
||||||
const [now, setNow] = useState<NowPlaying | null>(null);
|
const [now, setNow] = useState<NowPlaying | null>(null);
|
||||||
const [listeners, setListeners] = useState<number | null>(null);
|
const [listeners, setListeners] = useState<number | null>(null);
|
||||||
const [elapsed, setElapsed] = useState(0);
|
const [elapsed, setElapsed] = useState(0);
|
||||||
const [copyState, setCopyState] = useState<'idle' | 'ok'>('idle');
|
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<HTMLAudioElement | null>(null);
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
const station = stations.find((s) => s.id === selectedId);
|
const station = stations.find((s) => s.id === selectedId);
|
||||||
|
|
@ -155,9 +161,14 @@ export function HeroPlayer({ initialStations }: Props) {
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [now?.started_at]);
|
}, [now?.started_at]);
|
||||||
|
|
||||||
// wall clock for header
|
// wall clock for header — only ticks after mount so ssr/hydrate match (empty strings)
|
||||||
useEffect(() => {
|
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);
|
return () => clearInterval(id);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -238,8 +249,8 @@ export function HeroPlayer({ initialStations }: Props) {
|
||||||
tune in.<br/>tune out.<br/>melt yr brain {'<3'}
|
tune in.<br/>tune out.<br/>melt yr brain {'<3'}
|
||||||
</div>
|
</div>
|
||||||
<div className="hero-status">
|
<div className="hero-status">
|
||||||
<div>{fmtJpDate(clockNow)}</div>
|
<div>{clockText.date || ' '}</div>
|
||||||
<div>{fmtJpTime(clockNow)}</div>
|
<div>{clockText.time || ' '}</div>
|
||||||
<div className="live">[<span className="blink">{playing ? 'REC' : 'OFF'}</span>] {playing ? 'LIVE' : 'PAUSED'} · {listeners ?? '—'} listening</div>
|
<div className="live">[<span className="blink">{playing ? 'REC' : 'OFF'}</span>] {playing ? 'LIVE' : 'PAUSED'} · {listeners ?? '—'} listening</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue