feat: add Settings page with hijri seasons CRUD

- Server: seasons CRUD routes + generic NocoDB helpers
- Client: Settings page at /settings with inline add/edit/delete
- Seasons stored in NocoDB Seasons table
- Vite proxy: /api/seasons routed to Express server
- Nav links added (desktop + mobile)
- Locale keys for EN + AR
- Seasons loaded non-blocking on app mount

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fahed
2026-03-31 16:03:50 +03:00
parent 1dd216f933
commit ef48372033
11 changed files with 477 additions and 6 deletions
+19 -3
View File
@@ -3,10 +3,12 @@ import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react
const Dashboard = lazy(() => import('./components/Dashboard'));
const Comparison = lazy(() => import('./components/Comparison'));
const Settings = lazy(() => import('./components/Settings'));
import LoadingSkeleton from './components/shared/LoadingSkeleton';
import { fetchData, getCacheStatus, refreshData } from './services/dataService';
import { fetchSeasons } from './services/seasonsService';
import { useLanguage } from './contexts/LanguageContext';
import type { MuseumRecord, CacheStatus, DataErrorType } from './types';
import type { MuseumRecord, Season, CacheStatus, DataErrorType } from './types';
import { DataError } from './types';
import './App.css';
@@ -43,6 +45,7 @@ function App() {
const [showDataLabels, setShowDataLabels] = useState<boolean>(false);
const [includeVAT, setIncludeVAT] = useState<boolean>(true);
const [dataSource, setDataSource] = useState<string>('museums');
const [seasons, setSeasons] = useState<Season[]>([]);
const [theme, setTheme] = useState<string>(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('hihala_theme') || 'light';
@@ -97,8 +100,14 @@ function App() {
}
}, []);
const loadSeasons = useCallback(async () => {
const s = await fetchSeasons();
setSeasons(s);
}, []);
useEffect(() => {
loadData();
loadSeasons();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -237,8 +246,9 @@ function App() {
<main>
<Suspense fallback={<LoadingSkeleton />}>
<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="/" element={<Dashboard data={data} seasons={seasons} showDataLabels={showDataLabels} setShowDataLabels={setShowDataLabels} includeVAT={includeVAT} setIncludeVAT={setIncludeVAT} />} />
<Route path="/comparison" element={<Comparison data={data} seasons={seasons} showDataLabels={showDataLabels} setShowDataLabels={setShowDataLabels} includeVAT={includeVAT} setIncludeVAT={setIncludeVAT} />} />
<Route path="/settings" element={<Settings onSeasonsChange={loadSeasons} />} />
</Routes>
</Suspense>
</main>
@@ -262,6 +272,12 @@ function App() {
</svg>
<span>{t('nav.compare')}</span>
</NavLink>
<NavLink to="/settings" className="mobile-nav-item">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
<span>{t('nav.settings')}</span>
</NavLink>
<button
className="mobile-nav-item"
onClick={switchLanguage}