import React, { useState, useEffect, useCallback, ReactNode, lazy, Suspense } from 'react'; import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom'; const Dashboard = lazy(() => import('./components/Dashboard')); const Comparison = lazy(() => import('./components/Comparison')); const Slides = lazy(() => import('./components/Slides')); import LoadingSkeleton from './components/shared/LoadingSkeleton'; import { fetchData, getCacheStatus, refreshData } from './services/dataService'; import { useLanguage } from './contexts/LanguageContext'; import type { MuseumRecord, 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} ); } interface DataSource { id: string; labelKey: string; enabled: boolean; } function App() { const { t, dir, switchLanguage } = useLanguage(); const [data, setData] = useState([]); 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 [showDataLabels, setShowDataLabels] = useState(false); const [includeVAT, setIncludeVAT] = useState(true); const [dataSource, setDataSource] = useState('museums'); 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); } }, []); useEffect(() => { loadData(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleRefresh = () => { loadData(true); }; 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'} )} {theme === 'dark' ? ( <>> ) : theme === 'light' ? ( ) : ( <>> )} {t('language.switch')} }> } /> } /> } /> {/* Mobile Bottom Navigation */} {t('nav.dashboard')} {t('nav.compare')} {t('nav.slides')} {t('language.switch')} ); } export default App;
{t(`errors.${error.type}`)}