import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ChevronDown } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { libraryGetPlayerStatsDayDetail, libraryGetPlayerStatsRecentDays, type PlaySessionDayDetail, type PlaySessionRecentDay, } from '../../api/library'; import { formatPlayerStatsListeningTotal } from '../../utils/format/formatHumanDuration'; import { formatPlayerStatsDayLabel, PLAYER_STATS_RECENT_DAYS_LIMIT, } from '../../utils/playerStats/formatPlayerStatsDay'; import PlayerStatsDayTracks from './PlayerStatsDayTracks'; type Props = { /** Day selected on the heatmap โ€” auto-expand when present. */ heatmapSelectedDate: string | null; /** Full reload (year change) โ€” shows section spinner. */ refreshKey: number; /** Silent poll while listening โ€” updates list and expanded day details. */ liveRefreshKey: number; }; function formatDayMeta( summary: PlaySessionRecentDay, t: (key: string, opts?: Record) => string, ): string { return [ formatPlayerStatsListeningTotal(summary.totalListenedSec), t('statistics.playerDaySessions', { count: summary.sessionCount }), t('statistics.playerDayTrackPlays', { count: summary.trackPlayCount }), ].join(' ยท '); } export default function PlayerStatsRecentDays({ heatmapSelectedDate, refreshKey, liveRefreshKey, }: Props) { const { t, i18n } = useTranslation(); const locale = i18n.resolvedLanguage ?? i18n.language; const [days, setDays] = useState([]); const [loading, setLoading] = useState(true); const [expandedDates, setExpandedDates] = useState>(() => new Set()); const [details, setDetails] = useState>(() => new Map()); const [loadingDates, setLoadingDates] = useState>(() => new Set()); const detailsRef = useRef(details); detailsRef.current = details; const expandedRef = useRef(expandedDates); expandedRef.current = expandedDates; const loadDetail = useCallback(async (date: string) => { setLoadingDates(prev => new Set(prev).add(date)); try { const detail = await libraryGetPlayerStatsDayDetail(date); setDetails(prev => new Map(prev).set(date, detail)); } catch { /* ignore */ } finally { setLoadingDates(prev => { const next = new Set(prev); next.delete(date); return next; }); } }, []); useEffect(() => { let cancelled = false; setLoading(true); libraryGetPlayerStatsRecentDays(PLAYER_STATS_RECENT_DAYS_LIMIT) .then(rows => { if (!cancelled) { setDays(rows); setLoading(false); } }) .catch(() => { if (!cancelled) { setDays([]); setLoading(false); } }); return () => { cancelled = true; }; }, [refreshKey]); useEffect(() => { if (liveRefreshKey === 0) return; let cancelled = false; libraryGetPlayerStatsRecentDays(PLAYER_STATS_RECENT_DAYS_LIMIT) .then(rows => { if (cancelled) return; setDays(rows); const refreshDates = new Set(expandedRef.current); if (heatmapSelectedDate && expandedRef.current.has(heatmapSelectedDate)) { refreshDates.add(heatmapSelectedDate); } setDetails(prev => { const next = new Map(prev); for (const date of refreshDates) next.delete(date); return next; }); for (const date of refreshDates) { void loadDetail(date); } }) .catch(() => { /* ignore */ }); return () => { cancelled = true; }; }, [liveRefreshKey, heatmapSelectedDate, loadDetail]); const daySet = useMemo(() => new Set(days.map(d => d.date)), [days]); const ensureDetail = useCallback(async (date: string) => { if (detailsRef.current.has(date)) return; await loadDetail(date); }, [loadDetail]); const toggleDate = useCallback((date: string) => { setExpandedDates(prev => { const next = new Set(prev); if (next.has(date)) next.delete(date); else next.add(date); return next; }); void ensureDetail(date); }, [ensureDetail]); useEffect(() => { if (!heatmapSelectedDate) return; setExpandedDates(prev => new Set(prev).add(heatmapSelectedDate)); void ensureDetail(heatmapSelectedDate); }, [heatmapSelectedDate, ensureDetail]); const extraHeatmapDay = heatmapSelectedDate && !daySet.has(heatmapSelectedDate) ? heatmapSelectedDate : null; if (loading) { return (

{t('statistics.playerRecentDaysTitle')}

); } if (days.length === 0 && !extraHeatmapDay) return null; const renderRow = (date: string, summary: PlaySessionRecentDay | null) => { const open = expandedDates.has(date); const detail = details.get(date); const pending = loadingDates.has(date); const label = formatPlayerStatsDayLabel(date, t, locale); const meta = summary ? formatDayMeta(summary, t) : null; return (
{open && (
{pending && !detail && (
)} {detail && }
)}
); }; return (

{t('statistics.playerRecentDaysTitle')}

{extraHeatmapDay && renderRow(extraHeatmapDay, null)} {days.map(day => renderRow(day.date, day))}
); }