chore: migrate to TypeScript
- Convert all .js files to .tsx/.ts - Add types for data structures (MuseumRecord, Metrics, etc.) - Add type declarations for react-chartjs-2 - Configure tsconfig with relaxed strictness for gradual adoption - All components now use TypeScript
This commit is contained in:
232
src/App.tsx
Normal file
232
src/App.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import React, { useState, useEffect, useCallback, ReactNode } from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom';
|
||||
import Dashboard from './components/Dashboard';
|
||||
import Comparison from './components/Comparison';
|
||||
import Slides from './components/Slides';
|
||||
import { fetchData, getCacheStatus, refreshData } from './services/dataService';
|
||||
import { useLanguage } from './contexts/LanguageContext';
|
||||
import type { MuseumRecord, CacheStatus } 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 (
|
||||
<Link to={to} className={`nav-link ${isActive ? 'active' : ''} ${className || ''}`}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
interface DataSource {
|
||||
id: string;
|
||||
labelKey: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const { t, dir, switchLanguage } = useLanguage();
|
||||
const [data, setData] = useState<MuseumRecord[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isOffline, setIsOffline] = useState<boolean>(false);
|
||||
const [cacheInfo, setCacheInfo] = useState<CacheStatus | null>(null);
|
||||
const [showDataLabels, setShowDataLabels] = useState<boolean>(false);
|
||||
const [includeVAT, setIncludeVAT] = useState<boolean>(true);
|
||||
const [dataSource, setDataSource] = useState<string>('museums');
|
||||
|
||||
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) {
|
||||
setError((err as Error).message);
|
||||
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 (
|
||||
<div className="loading-container" dir={dir}>
|
||||
<div className="loading-spinner"></div>
|
||||
<p>{t('app.loading')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="error-container" dir={dir}>
|
||||
<h2>{t('app.error')}</h2>
|
||||
<p style={{maxWidth: '400px', textAlign: 'center', color: '#64748b'}}>{error}</p>
|
||||
<button onClick={() => window.location.reload()}>{t('app.retry')}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<div className="app" dir={dir}>
|
||||
<nav className="nav-bar">
|
||||
<div className="nav-content">
|
||||
<div className="nav-brand">
|
||||
<svg className="nav-brand-icon" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1"/>
|
||||
<rect x="14" y="3" width="7" height="4" rx="1"/>
|
||||
<rect x="3" y="14" width="7" height="7" rx="1"/>
|
||||
<rect x="14" y="11" width="7" height="10" rx="1"/>
|
||||
</svg>
|
||||
<span className="nav-brand-text">
|
||||
HiHala Data
|
||||
<select
|
||||
className="data-source-select"
|
||||
value={dataSource}
|
||||
onChange={e => setDataSource(e.target.value)}
|
||||
>
|
||||
{dataSources.map(src => (
|
||||
<option key={src.id} value={src.id} disabled={!src.enabled}>
|
||||
{t(src.labelKey)}{!src.enabled ? ` (${t('dataSources.soon')})` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<div className="nav-links">
|
||||
<NavLink to="/">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="7" height="9" rx="1"/>
|
||||
<rect x="14" y="3" width="7" height="5" rx="1"/>
|
||||
<rect x="14" y="12" width="7" height="9" rx="1"/>
|
||||
<rect x="3" y="16" width="7" height="5" rx="1"/>
|
||||
</svg>
|
||||
{t('nav.dashboard')}
|
||||
</NavLink>
|
||||
<NavLink to="/comparison">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="20" x2="18" y2="10"/>
|
||||
<line x1="12" y1="20" x2="12" y2="4"/>
|
||||
<line x1="6" y1="20" x2="6" y2="14"/>
|
||||
<polyline points="18 14 22 10 18 6"/>
|
||||
<polyline points="6 10 2 14 6 18"/>
|
||||
</svg>
|
||||
{t('nav.comparison')}
|
||||
</NavLink>
|
||||
{isOffline && (
|
||||
<span className="offline-badge" title={cacheInfo ? `Cached: ${new Date(cacheInfo.timestamp || '').toLocaleString()}` : ''}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
<path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"/>
|
||||
<path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"/>
|
||||
<path d="M10.71 5.05A16 16 0 0 1 22.58 9"/>
|
||||
<path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"/>
|
||||
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"/>
|
||||
<line x1="12" y1="20" x2="12.01" y2="20"/>
|
||||
</svg>
|
||||
{t('app.offline') || 'Offline'}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
className={`nav-refresh-btn ${refreshing ? 'refreshing' : ''}`}
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
title={t('app.refresh') || 'Refresh data'}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="23 4 23 10 17 10"/>
|
||||
<polyline points="1 20 1 14 7 14"/>
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="nav-lang-toggle"
|
||||
onClick={switchLanguage}
|
||||
title="Switch language"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
</svg>
|
||||
{t('language.switch')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard data={data} showDataLabels={showDataLabels} setShowDataLabels={setShowDataLabels} includeVAT={includeVAT} setIncludeVAT={setIncludeVAT} />} />
|
||||
<Route path="/comparison" element={<Comparison data={data} showDataLabels={showDataLabels} setShowDataLabels={setShowDataLabels} includeVAT={includeVAT} setIncludeVAT={setIncludeVAT} />} />
|
||||
<Route path="/slides" element={<Slides data={data} />} />
|
||||
</Routes>
|
||||
|
||||
{/* Mobile Bottom Navigation */}
|
||||
<nav className="mobile-nav">
|
||||
<NavLink to="/" className="mobile-nav-item">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="7" height="9" rx="1"/>
|
||||
<rect x="14" y="3" width="7" height="5" rx="1"/>
|
||||
<rect x="14" y="12" width="7" height="9" rx="1"/>
|
||||
<rect x="3" y="16" width="7" height="5" rx="1"/>
|
||||
</svg>
|
||||
<span>{t('nav.dashboard')}</span>
|
||||
</NavLink>
|
||||
<NavLink to="/comparison" className="mobile-nav-item">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="20" x2="18" y2="10"/>
|
||||
<line x1="12" y1="20" x2="12" y2="4"/>
|
||||
<line x1="6" y1="20" x2="6" y2="14"/>
|
||||
</svg>
|
||||
<span>{t('nav.compare')}</span>
|
||||
</NavLink>
|
||||
<button
|
||||
className="mobile-nav-item"
|
||||
onClick={switchLanguage}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
</svg>
|
||||
<span>{t('language.switch')}</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
Reference in New Issue
Block a user