BillTracker/client/App.jsx

171 lines
6.3 KiB
JavaScript

import { lazy, Suspense } from 'react';
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import Layout from '@/components/layout/Layout';
import AppNavigation from '@/components/layout/Sidebar';
import { ReleaseNotesDialog } from '@/components/ReleaseNotesDialog';
import LoginPage from '@/pages/LoginPage';
import ErrorBoundary from '@/components/ErrorBoundary';
import PageLoader from '@/components/PageLoader';
// Lazy-loaded components
const AdminPage = lazy(() => import('@/pages/AdminPage'));
const TrackerPage = lazy(() => import('@/pages/TrackerPage'));
const CalendarPage = lazy(() => import('@/pages/CalendarPage'));
const SummaryPage = lazy(() => import('@/pages/SummaryPage'));
const BillsPage = lazy(() => import('@/pages/BillsPage'));
const CategoriesPage = lazy(() => import('@/pages/CategoriesPage'));
const SettingsPage = lazy(() => import('@/pages/SettingsPage'));
const StatusPage = lazy(() => import('@/pages/StatusPage'));
const AnalyticsPage = lazy(() => import('@/pages/AnalyticsPage'));
const ReleaseNotesPage = lazy(() => import('@/pages/ReleaseNotesPage'));
const AboutPage = lazy(() => import('@/pages/AboutPage'));
const DataPage = lazy(() => import('@/pages/DataPage'));
const ProfilePage = lazy(() => import('@/pages/ProfilePage'));
function RequireAuth({ children, role }) {
const { user, singleUserMode } = useAuth();
const location = useLocation();
// Loading state
if (user === undefined) {
return (
<div className="flex items-center justify-center min-h-screen bg-background text-muted-foreground text-sm">
Loading...
</div>
);
}
// Single-user mode bypass
if (singleUserMode && role === 'user') return children;
// Not authenticated
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
const roleAllowed = !role || user.role === role || (role === 'user' && user.role === 'admin');
if (role === 'user' && user.is_default_admin) {
return <Navigate to="/admin" replace />;
}
// Role mismatch
if (!roleAllowed) {
return <Navigate to={user.role === 'admin' ? '/admin' : '/'} replace />;
}
return children;
}
function AdminShell({ children }) {
return (
<div className="min-h-screen bg-[radial-gradient(circle_at_top_left,oklch(var(--primary)/0.10),transparent_34rem),linear-gradient(180deg,oklch(var(--background)),oklch(var(--muted)/0.28))] text-foreground">
<AppNavigation adminMode />
<main className="mx-auto max-w-5xl px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
{children}
</main>
</div>
);
}
export default function App() {
const { user } = useAuth();
return (
<>
{/* Release notes (only for user role) */}
{user?.role === 'user' && <ReleaseNotesDialog />}
<Routes>
<Route path="/login" element={<ErrorBoundary><LoginPage /></ErrorBoundary>} />
<Route path="/about" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><AboutPage /></Suspense></ErrorBoundary>} />
<Route path="/release-notes" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><ReleaseNotesPage /></Suspense></ErrorBoundary>} />
<Route
path="/admin"
element={
<RequireAuth role="admin">
<ErrorBoundary>
<Suspense fallback={<PageLoader />}>
<AdminPage />
</Suspense>
</ErrorBoundary>
</RequireAuth>
}
/>
<Route
path="/admin/about"
element={
<RequireAuth role="admin">
<ErrorBoundary>
<AdminShell>
<Suspense fallback={<PageLoader />}>
<AboutPage admin />
</Suspense>
</AdminShell>
</ErrorBoundary>
</RequireAuth>
}
/>
<Route
path="/admin/roadmap"
element={
<RequireAuth role="admin">
<ErrorBoundary>
<AdminShell>
<Suspense fallback={<PageLoader />}>
<AboutPage admin />
</Suspense>
</AdminShell>
</ErrorBoundary>
</RequireAuth>
}
/>
<Route
path="/admin/status"
element={
<RequireAuth role="admin">
<ErrorBoundary>
<AdminShell>
<Suspense fallback={<PageLoader />}>
<StatusPage />
</Suspense>
</AdminShell>
</ErrorBoundary>
</RequireAuth>
}
/>
<Route
path="/status"
element={
<RequireAuth role="admin">
<Navigate to="/admin/status" replace />
</RequireAuth>
}
/>
<Route
element={
<RequireAuth role="user">
<Layout />
</RequireAuth>
}
>
<Route index element={<ErrorBoundary><Suspense fallback={<PageLoader />}><TrackerPage /></Suspense></ErrorBoundary>} />
<Route path="calendar" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><CalendarPage /></Suspense></ErrorBoundary>} />
<Route path="summary" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><SummaryPage /></Suspense></ErrorBoundary>} />
<Route path="bills" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><BillsPage /></Suspense></ErrorBoundary>} />
<Route path="categories" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><CategoriesPage /></Suspense></ErrorBoundary>} />
<Route path="analytics" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><AnalyticsPage /></Suspense></ErrorBoundary>} />
<Route path="settings" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><SettingsPage /></Suspense></ErrorBoundary>} />
<Route path="data" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><DataPage /></Suspense></ErrorBoundary>} />
<Route path="profile" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><ProfilePage /></Suspense></ErrorBoundary>} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>
</>
);
}