import React, { useState, useEffect, useCallback, useMemo, ReactNode, lazy, Suspense } from 'react'; import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom'; const Settings = lazy(() => import('./components/Settings')); const Comparison = lazy(() => import('./components/Comparison')); const Dashboard = lazy(() => import('./components/Dashboard')); import Login from './components/Login'; import LoadingSkeleton from './components/shared/LoadingSkeleton'; import { fetchData, getCacheStatus, refreshData, getUniqueMuseums, getUniqueChannels } from './services/dataService'; import { fetchSeasons } from './services/seasonsService'; import { parseAllowed } from './services/usersService'; import { useLanguage } from './contexts/LanguageContext'; import type { MuseumRecord, Season, CacheStatus, DataErrorType } from './types'; import { DataError } from './types'; import './App.css'; interface NavLinkProps { to: string; children: ReactNode; className?: string; } function NavLink({ to, children, className }: NavLinkProps) { const location = useLocation(); const isActive = location.pathname === to; return ( {children} ); } function AppNav({ children }: { children: ReactNode }) { return <>{children}>; } interface DataSource { id: string; labelKey: string; enabled: boolean; } function App() { const { t, dir, switchLanguage } = useLanguage(); const [authenticated, setAuthenticated] = useState(null); const [userRole, setUserRole] = useState('viewer'); const [userName, setUserName] = useState(''); const [allowedMuseums, setAllowedMuseums] = useState([]); const [allowedChannels, setAllowedChannels] = useState([]); const [data, setData] = useState([]); const allMuseumsList = useMemo(() => getUniqueMuseums(data), [data]); const allChannelsList = useMemo(() => getUniqueChannels(data), [data]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState<{ message: string; type: DataErrorType } | null>(null); const [isOffline, setIsOffline] = useState(false); const [cacheInfo, setCacheInfo] = useState(null); const [includeVAT, setIncludeVAT] = useState(true); const [dataSource, setDataSource] = useState('museums'); const [seasons, setSeasons] = useState([]); const [theme, setTheme] = useState(() => { if (typeof window !== 'undefined') { return localStorage.getItem('hihala_theme') || 'light'; } return 'light'; }); useEffect(() => { const root = document.documentElement; if (theme === 'system') { root.removeAttribute('data-theme'); } else { root.setAttribute('data-theme', theme); } localStorage.setItem('hihala_theme', theme); }, [theme]); const toggleTheme = () => { setTheme(prev => { if (prev === 'system') return 'dark'; if (prev === 'dark') return 'light'; return 'system'; }); }; const dataSources: DataSource[] = [ { id: 'museums', labelKey: 'dataSources.museums', enabled: true }, { id: 'coffees', labelKey: 'dataSources.coffees', enabled: false }, { id: 'ecommerce', labelKey: 'dataSources.ecommerce', enabled: false } ]; const loadData = useCallback(async (forceRefresh: boolean = false) => { try { setLoading(!forceRefresh); setRefreshing(forceRefresh); const result = forceRefresh ? await refreshData() : await fetchData(); setData(result.data); setError(null); setIsOffline(result.fromCache); // Update cache info const status = getCacheStatus(); setCacheInfo(status); } catch (err) { const type = err instanceof DataError ? err.type : 'unknown'; setError({ message: (err as Error).message, type }); console.error(err); } finally { setLoading(false); setRefreshing(false); } }, []); const loadSeasons = useCallback(async () => { const s = await fetchSeasons(); setSeasons(s); }, []); // Check auth on mount useEffect(() => { fetch('/auth/check', { credentials: 'include' }) .then(r => r.json()) .then(d => { setAuthenticated(d.authenticated); if (d.authenticated) { setUserRole(d.role || 'viewer'); setUserName(d.name || ''); setAllowedMuseums(parseAllowed(d.allowedMuseums)); setAllowedChannels(parseAllowed(d.allowedChannels)); loadData(); loadSeasons(); } }) .catch(() => setAuthenticated(false)); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleLogin = (name: string, role: string, rawMuseums: string, rawChannels: string) => { setAuthenticated(true); setUserName(name); setUserRole(role); setAllowedMuseums(parseAllowed(rawMuseums)); setAllowedChannels(parseAllowed(rawChannels)); loadData(); loadSeasons(); }; const handleRefresh = () => { loadData(true); }; // Auth check loading if (authenticated === null) { return ( ); } // Not authenticated — show login if (!authenticated) { return ( ); } if (loading) { return ( ); } if (error) { return ( {t('app.error')} {t(`errors.${error.type}`)} loadData()}>{t('app.retry')} ); } return ( HiHala Data setDataSource(e.target.value)} aria-label={t('dataSources.museums')} > {dataSources.map(src => ( {t(src.labelKey)}{!src.enabled ? ` (${t('dataSources.soon')})` : ''} ))} {t('nav.dashboard')} {t('nav.comparison')} {isOffline && ( {t('app.offline') || 'Offline'} {cacheInfo && ( {` (cached ${new Date(cacheInfo.timestamp || '').toLocaleString()})`} )} )} {theme === 'dark' ? ( <>> ) : theme === 'light' ? ( ) : ( <>> )} {t('language.switch')} }> } /> } /> {userRole === 'admin' && } />} {/* Mobile Bottom Navigation */} {t('nav.dashboard')} {t('nav.compare')} {userRole === 'admin' && ( {t('nav.settings')} )} {t('language.switch')} ); } export default App;
{t(`errors.${error.type}`)}