import { useState } from 'react'; import { formatDuration, formatRelativeDate, formatNumber, formatSessionDayLabel, } from '../../lib/formatters'; import { BASE_URL } from '../../lib/api-client'; import { getSessionDisplayWordCount } from '../../lib/session-word-count'; import { getSessionNavigationTarget } from '../../lib/stats-navigation'; import type { SessionSummary } from '../../types/stats'; interface RecentSessionsProps { sessions: SessionSummary[]; onNavigateToMediaDetail: (videoId: number, sessionId?: number | null) => void; onNavigateToSession: (sessionId: number) => void; onDeleteSession: (session: SessionSummary) => void; onDeleteDayGroup: (dayLabel: string, daySessions: SessionSummary[]) => void; onDeleteAnimeGroup: (sessions: SessionSummary[]) => void; deletingIds: Set; } interface AnimeGroup { key: string; animeId: number | null; animeTitle: string | null; videoId: number | null; sessions: SessionSummary[]; totalCards: number; totalWords: number; totalActiveMs: number; totalKnownWords: number; } function groupSessionsByDay(sessions: SessionSummary[]): Map { const groups = new Map(); for (const session of sessions) { const dayLabel = formatSessionDayLabel(session.startedAtMs); const group = groups.get(dayLabel); if (group) { group.push(session); } else { groups.set(dayLabel, [session]); } } return groups; } function groupSessionsByAnime(sessions: SessionSummary[]): AnimeGroup[] { const map = new Map(); for (const session of sessions) { const key = session.animeId != null ? `anime-${session.animeId}` : session.videoId != null ? `video-${session.videoId}` : `session-${session.sessionId}`; const existing = map.get(key); const displayWordCount = getSessionDisplayWordCount(session); if (existing) { existing.sessions.push(session); existing.totalCards += session.cardsMined; existing.totalWords += displayWordCount; existing.totalActiveMs += session.activeWatchedMs; existing.totalKnownWords += session.knownWordsSeen; } else { map.set(key, { key, animeId: session.animeId, animeTitle: session.animeTitle, videoId: session.videoId, sessions: [session], totalCards: session.cardsMined, totalWords: displayWordCount, totalActiveMs: session.activeWatchedMs, totalKnownWords: session.knownWordsSeen, }); } } return Array.from(map.values()); } function CoverThumbnail({ animeId, videoId, title, }: { animeId: number | null; videoId: number | null; title: string; }) { const fallbackChar = title.charAt(0) || '?'; const [isFallback, setIsFallback] = useState(false); if ((!animeId && !videoId) || isFallback) { return (
{fallbackChar}
); } const src = animeId != null ? `${BASE_URL}/api/stats/anime/${animeId}/cover` : `${BASE_URL}/api/stats/media/${videoId}/cover`; return ( setIsFallback(true)} /> ); } function SessionItem({ session, onNavigateToMediaDetail, onNavigateToSession, onDelete, deleteDisabled, }: { session: SessionSummary; onNavigateToMediaDetail: (videoId: number, sessionId?: number | null) => void; onNavigateToSession: (sessionId: number) => void; onDelete: () => void; deleteDisabled: boolean; }) { const displayWordCount = getSessionDisplayWordCount(session); const navigationTarget = getSessionNavigationTarget(session); return (
); } function AnimeGroupRow({ group, onNavigateToMediaDetail, onNavigateToSession, onDeleteSession, onDeleteAnimeGroup, deletingIds, }: { group: AnimeGroup; onNavigateToMediaDetail: (videoId: number, sessionId?: number | null) => void; onNavigateToSession: (sessionId: number) => void; onDeleteSession: (session: SessionSummary) => void; onDeleteAnimeGroup: (group: AnimeGroup) => void; deletingIds: Set; }) { const [expanded, setExpanded] = useState(false); const groupDeleting = group.sessions.some((s) => deletingIds.has(s.sessionId)); if (group.sessions.length === 1) { const s = group.sessions[0]!; return ( onDeleteSession(s)} deleteDisabled={deletingIds.has(s.sessionId)} /> ); } const displayTitle = group.animeTitle ?? group.sessions[0]?.canonicalTitle ?? 'Unknown Media'; const mostRecentSession = group.sessions[0]!; const disclosureId = `recent-sessions-${mostRecentSession.sessionId}`; return (
{expanded && (
{group.sessions.map((s) => { const navigationTarget = getSessionNavigationTarget(s); return (
); })}
)}
); } export function RecentSessions({ sessions, onNavigateToMediaDetail, onNavigateToSession, onDeleteSession, onDeleteDayGroup, onDeleteAnimeGroup, deletingIds, }: RecentSessionsProps) { if (sessions.length === 0) { return (
No sessions yet
); } const groups = groupSessionsByDay(sessions); const anyDeleting = deletingIds.size > 0; return (
{Array.from(groups.entries()).map(([dayLabel, daySessions]) => { const animeGroups = groupSessionsByAnime(daySessions); const groupDeleting = daySessions.some((s) => deletingIds.has(s.sessionId)); return (

{dayLabel}

{animeGroups.map((group) => ( onDeleteAnimeGroup(g.sessions)} deletingIds={deletingIds} /> ))}
); })}
); }