import { Suspense, lazy, useCallback, useState } from 'react'; import { TabBar } from './components/layout/TabBar'; import { OverviewTab } from './components/overview/OverviewTab'; import { useExcludedWords } from './hooks/useExcludedWords'; import type { TabId } from './components/layout/TabBar'; import { closeMediaDetail, createInitialStatsView, navigateToAnime as navigateToAnimeState, navigateToSession as navigateToSessionState, openAnimeEpisodeDetail, openOverviewMediaDetail, openSessionsMediaDetail, switchTab, } from './lib/stats-navigation'; const AnimeTab = lazy(() => import('./components/anime/AnimeTab').then((module) => ({ default: module.AnimeTab, })), ); const TrendsTab = lazy(() => import('./components/trends/TrendsTab').then((module) => ({ default: module.TrendsTab, })), ); const VocabularyTab = lazy(() => import('./components/vocabulary/VocabularyTab').then((module) => ({ default: module.VocabularyTab, })), ); const SessionsTab = lazy(() => import('./components/sessions/SessionsTab').then((module) => ({ default: module.SessionsTab, })), ); const MediaDetailView = lazy(() => import('./components/library/MediaDetailView').then((module) => ({ default: module.MediaDetailView, })), ); const WordDetailPanel = lazy(() => import('./components/vocabulary/WordDetailPanel').then((module) => ({ default: module.WordDetailPanel, })), ); function LoadingSurface({ label, overlay = false }: { label: string; overlay?: boolean }) { return (
{label}
); } export function App() { const [viewState, setViewState] = useState(createInitialStatsView); const [mountedTabs, setMountedTabs] = useState>(() => new Set(['overview'])); const [globalWordId, setGlobalWordId] = useState(null); const { excluded, isExcluded, toggleExclusion, removeExclusion, clearAll } = useExcludedWords(); const { activeTab, selectedAnimeId, focusedSessionId, mediaDetail } = viewState; const activateTab = useCallback((tabId: TabId) => { setViewState((prev) => switchTab(prev, tabId)); setMountedTabs((prev) => { if (prev.has(tabId)) return prev; const next = new Set(prev); next.add(tabId); return next; }); }, []); const navigateToAnime = useCallback((animeId: number) => { setViewState((prev) => navigateToAnimeState(prev, animeId)); setMountedTabs((prev) => { if (prev.has('anime')) return prev; const next = new Set(prev); next.add('anime'); return next; }); }, []); const navigateToSession = useCallback((sessionId: number) => { setViewState((prev) => navigateToSessionState(prev, sessionId)); setMountedTabs((prev) => { if (prev.has('sessions')) return prev; const next = new Set(prev); next.add('sessions'); return next; }); }, []); const navigateToEpisodeDetail = useCallback( (animeId: number, videoId: number, sessionId: number | null = null) => { setViewState((prev) => openAnimeEpisodeDetail(prev, animeId, videoId, sessionId)); }, [], ); const navigateToOverviewMediaDetail = useCallback( (videoId: number, sessionId: number | null = null) => { setViewState((prev) => openOverviewMediaDetail(prev, videoId, sessionId)); }, [], ); const navigateToSessionsMediaDetail = useCallback((videoId: number) => { setViewState((prev) => openSessionsMediaDetail(prev, videoId)); }, []); const openWordDetail = useCallback((wordId: number) => { setGlobalWordId(wordId); }, []); const handleTabChange = useCallback( (tabId: TabId) => { activateTab(tabId); }, [activateTab], ); return (
{mediaDetail ? ( }> setViewState((prev) => prev.mediaDetail ? { ...prev, mediaDetail: { ...prev.mediaDetail, initialSessionId: null, }, } : prev, ) } onBack={() => setViewState((prev) => closeMediaDetail(prev))} backLabel={ mediaDetail.origin.type === 'overview' ? 'Back to Overview' : mediaDetail.origin.type === 'sessions' ? 'Back to Sessions' : 'Back to Library' } onNavigateToAnime={navigateToAnime} /> ) : ( <> {mountedTabs.has('overview') ? ( ) : null} {mountedTabs.has('anime') ? ( ) : null} {mountedTabs.has('trends') ? ( ) : null} {mountedTabs.has('vocabulary') ? ( ) : null} {mountedTabs.has('sessions') ? ( ) : null} )}
{globalWordId !== null ? ( }> setGlobalWordId(null)} onSelectWord={openWordDetail} onNavigateToAnime={navigateToAnime} isExcluded={isExcluded} onToggleExclusion={toggleExclusion} /> ) : null}
); }