refactor(ts): convert the app shell to TSX (B7)
App.tsx (routing + QueryClient), main.tsx (entry; index.html updated to
main.tsx), useAuth.tsx (typed AuthContextValue/User + MeResponse; uses the safe
useContext()||defaults pattern so no `never` trap), and the layout components
(Layout, Sidebar, NavPill, BrandBlock — NavItem type, LucideIcon-typed nav
config, casts for the untyped overdue/simplefin API responses). TS caught a dead
prop: App passed mainContentId to TrackerPage, which takes no params — removed.
typecheck 0, lint 0 errors (39 warns), build green, 17/17 e2e probe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:49:57 -05:00
|
|
|
import { lazy, Suspense, useId, type ReactNode } from 'react';
|
2026-05-03 19:51:57 -05:00
|
|
|
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
|
|
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
|
|
|
import Layout from '@/components/layout/Layout';
|
2026-05-04 20:12:57 -05:00
|
|
|
import AppNavigation from '@/components/layout/Sidebar';
|
2026-06-07 15:14:09 -05:00
|
|
|
import PageTransition from '@/components/PageTransition';
|
2026-05-03 19:51:57 -05:00
|
|
|
import { ReleaseNotesDialog } from '@/components/ReleaseNotesDialog';
|
2026-05-16 15:38:28 -05:00
|
|
|
import CommandPalette from '@/components/CommandPalette';
|
2026-05-03 19:51:57 -05:00
|
|
|
import LoginPage from '@/pages/LoginPage';
|
2026-06-03 20:28:37 -05:00
|
|
|
import NotFoundPage from '@/pages/NotFoundPage';
|
2026-05-09 18:33:02 -05:00
|
|
|
import ErrorBoundary from '@/components/ErrorBoundary';
|
2026-05-09 22:01:19 -05:00
|
|
|
import PageLoader from '@/components/PageLoader';
|
|
|
|
|
|
2026-05-10 03:10:43 -05:00
|
|
|
// TanStack Query
|
2026-07-03 20:56:31 -05:00
|
|
|
import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query';
|
2026-05-10 03:10:43 -05:00
|
|
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
2026-07-03 20:56:31 -05:00
|
|
|
import { toast } from 'sonner';
|
2026-05-10 03:10:43 -05:00
|
|
|
|
|
|
|
|
const queryClient = new QueryClient({
|
2026-07-03 20:56:31 -05:00
|
|
|
// Global error handling: pages render an inline error on the *initial* load
|
|
|
|
|
// (no data yet), so only surface a toast when a *background refetch* fails
|
|
|
|
|
// while stale data is still shown — otherwise the failure would be silent.
|
|
|
|
|
queryCache: new QueryCache({
|
|
|
|
|
onError: (error, query) => {
|
|
|
|
|
if (query.state.data !== undefined) {
|
|
|
|
|
toast.error(error?.message || 'Could not refresh — showing the last data.');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}),
|
2026-05-10 03:10:43 -05:00
|
|
|
defaultOptions: {
|
|
|
|
|
queries: {
|
|
|
|
|
staleTime: 1000 * 60 * 2, // 2 minutes
|
|
|
|
|
retry: 1,
|
|
|
|
|
refetchOnWindowFocus: false,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-09 22:01:19 -05:00
|
|
|
// 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'));
|
2026-05-28 22:54:07 -05:00
|
|
|
const SubscriptionsPage = lazy(() => import('@/pages/SubscriptionsPage'));
|
2026-06-06 20:44:54 -05:00
|
|
|
const SubscriptionCatalogPage = lazy(() => import('@/pages/SubscriptionCatalogPage'));
|
2026-05-09 22:01:19 -05:00
|
|
|
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'));
|
2026-05-15 22:45:38 -05:00
|
|
|
const PrivacyPage = lazy(() => import('@/pages/PrivacyPage'));
|
v0.25.0: roadmap redesign, import CSRF fix, AdminDashboard removed
- RoadmapPage: kanban-style priority lanes, shadcn Collapsible/Tabs,
lazy-loaded activity log, admin-only /api/about/roadmap + /dev-log endpoints
- Import CSRF fix: added x-csrf-token header to importAdminBackup,
previewSpreadsheetImport, previewUserDbImport raw fetch() calls
- Removed AdminDashboard.jsx, replaced by RoadmapPage
- Added @radix-ui/react-collapsible + collapsible shadcn component
- Security audit by Private_Hudson: PASS (CSRF fix verified,
admin endpoints gated, path traversal mitigated, XSS safe)
2026-05-11 21:42:36 -05:00
|
|
|
const RoadmapPage = lazy(() => import('@/pages/RoadmapPage'));
|
2026-05-09 22:01:19 -05:00
|
|
|
const DataPage = lazy(() => import('@/pages/DataPage'));
|
|
|
|
|
const ProfilePage = lazy(() => import('@/pages/ProfilePage'));
|
2026-05-14 02:11:54 -05:00
|
|
|
const SnowballPage = lazy(() => import('@/pages/SnowballPage'));
|
2026-07-06 14:23:53 -05:00
|
|
|
const HealthPage = lazy(() => import('@/pages/HealthPage'));
|
|
|
|
|
const PayoffPage = lazy(() => import('@/pages/PayoffPage'));
|
|
|
|
|
const SpendingPage = lazy(() => import('@/pages/SpendingPage'));
|
2026-06-12 03:59:42 -05:00
|
|
|
const BankTransactionsPage = lazy(() => import('@/pages/BankTransactionsPage'));
|
2026-05-03 19:51:57 -05:00
|
|
|
|
refactor(ts): convert the app shell to TSX (B7)
App.tsx (routing + QueryClient), main.tsx (entry; index.html updated to
main.tsx), useAuth.tsx (typed AuthContextValue/User + MeResponse; uses the safe
useContext()||defaults pattern so no `never` trap), and the layout components
(Layout, Sidebar, NavPill, BrandBlock — NavItem type, LucideIcon-typed nav
config, casts for the untyped overdue/simplefin API responses). TS caught a dead
prop: App passed mainContentId to TrackerPage, which takes no params — removed.
typecheck 0, lint 0 errors (39 warns), build green, 17/17 e2e probe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:49:57 -05:00
|
|
|
function RequireAuth({ children, role }: { children: ReactNode; role?: string }): ReactNode {
|
2026-05-03 19:51:57 -05:00
|
|
|
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 />;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 20:40:48 -05:00
|
|
|
const roleAllowed = !role || user.role === role || (role === 'user' && user.role === 'admin');
|
|
|
|
|
|
2026-05-04 23:34:24 -05:00
|
|
|
if (role === 'user' && user.is_default_admin) {
|
|
|
|
|
return <Navigate to="/admin" replace />;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 19:51:57 -05:00
|
|
|
// Role mismatch
|
2026-05-03 20:40:48 -05:00
|
|
|
if (!roleAllowed) {
|
2026-05-03 19:51:57 -05:00
|
|
|
return <Navigate to={user.role === 'admin' ? '/admin' : '/'} replace />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return children;
|
|
|
|
|
}
|
|
|
|
|
|
refactor(ts): convert the app shell to TSX (B7)
App.tsx (routing + QueryClient), main.tsx (entry; index.html updated to
main.tsx), useAuth.tsx (typed AuthContextValue/User + MeResponse; uses the safe
useContext()||defaults pattern so no `never` trap), and the layout components
(Layout, Sidebar, NavPill, BrandBlock — NavItem type, LucideIcon-typed nav
config, casts for the untyped overdue/simplefin API responses). TS caught a dead
prop: App passed mainContentId to TrackerPage, which takes no params — removed.
typecheck 0, lint 0 errors (39 warns), build green, 17/17 e2e probe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:49:57 -05:00
|
|
|
function AdminShell({ children }: { children: ReactNode }) {
|
2026-06-07 15:14:09 -05:00
|
|
|
const location = useLocation();
|
|
|
|
|
|
2026-05-04 20:12:57 -05:00
|
|
|
return (
|
2026-07-05 17:18:56 -05:00
|
|
|
<div className="min-h-screen bg-[radial-gradient(circle_at_top_left,oklch(var(--primary)/0.10),transparent_32rem),radial-gradient(circle_at_top_right,oklch(var(--accent)/0.08),transparent_24rem),linear-gradient(180deg,oklch(var(--background)),oklch(var(--muted)/0.18))] text-foreground">
|
2026-05-04 20:12:57 -05:00
|
|
|
<AppNavigation adminMode />
|
2026-05-29 21:16:13 -05:00
|
|
|
<main className="mx-auto w-full max-w-[1500px] px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
|
2026-07-06 14:23:53 -05:00
|
|
|
<PageTransition routeKey={location.pathname}>{children}</PageTransition>
|
2026-05-04 20:12:57 -05:00
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 19:51:57 -05:00
|
|
|
export default function App() {
|
|
|
|
|
const { user } = useAuth();
|
2026-05-10 00:18:36 -05:00
|
|
|
const mainContentId = useId();
|
2026-05-03 19:51:57 -05:00
|
|
|
|
|
|
|
|
return (
|
2026-05-10 03:10:43 -05:00
|
|
|
<QueryClientProvider client={queryClient}>
|
2026-05-03 19:51:57 -05:00
|
|
|
{/* Release notes (only for user role) */}
|
|
|
|
|
{user?.role === 'user' && <ReleaseNotesDialog />}
|
2026-05-16 15:38:28 -05:00
|
|
|
{user && !user.is_default_admin && <CommandPalette />}
|
2026-05-03 19:51:57 -05:00
|
|
|
|
2026-05-10 00:18:36 -05:00
|
|
|
{/* Skip link for keyboard users */}
|
|
|
|
|
<a
|
|
|
|
|
href={`#${mainContentId}`}
|
|
|
|
|
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:bg-background focus:text-foreground focus:px-4 focus:py-2 focus:rounded-md focus:shadow-lg focus:outline-none"
|
|
|
|
|
>
|
|
|
|
|
Skip to main content
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
<main id={mainContentId}>
|
|
|
|
|
<Routes>
|
2026-07-06 14:23:53 -05:00
|
|
|
<Route
|
|
|
|
|
path="/login"
|
|
|
|
|
element={
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<LoginPage />
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="/about"
|
|
|
|
|
element={
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
<AboutPage />
|
|
|
|
|
</Suspense>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="/privacy"
|
|
|
|
|
element={
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
<PrivacyPage />
|
|
|
|
|
</Suspense>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="/release-notes"
|
|
|
|
|
element={
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
<ReleaseNotesPage />
|
|
|
|
|
</Suspense>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
}
|
|
|
|
|
/>
|
2026-05-10 00:18:36 -05:00
|
|
|
|
|
|
|
|
<Route
|
|
|
|
|
path="/admin"
|
|
|
|
|
element={
|
|
|
|
|
<RequireAuth role="admin">
|
|
|
|
|
<ErrorBoundary>
|
2026-05-09 22:01:19 -05:00
|
|
|
<Suspense fallback={<PageLoader />}>
|
2026-05-10 00:18:36 -05:00
|
|
|
<AdminPage />
|
2026-05-09 22:01:19 -05:00
|
|
|
</Suspense>
|
2026-05-10 00:18:36 -05:00
|
|
|
</ErrorBoundary>
|
|
|
|
|
</RequireAuth>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="/admin/about"
|
|
|
|
|
element={
|
|
|
|
|
<RequireAuth role="admin">
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<AdminShell>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
v0.25.0: roadmap redesign, import CSRF fix, AdminDashboard removed
- RoadmapPage: kanban-style priority lanes, shadcn Collapsible/Tabs,
lazy-loaded activity log, admin-only /api/about/roadmap + /dev-log endpoints
- Import CSRF fix: added x-csrf-token header to importAdminBackup,
previewSpreadsheetImport, previewUserDbImport raw fetch() calls
- Removed AdminDashboard.jsx, replaced by RoadmapPage
- Added @radix-ui/react-collapsible + collapsible shadcn component
- Security audit by Private_Hudson: PASS (CSRF fix verified,
admin endpoints gated, path traversal mitigated, XSS safe)
2026-05-11 21:42:36 -05:00
|
|
|
<AboutPage />
|
2026-05-10 00:18:36 -05:00
|
|
|
</Suspense>
|
|
|
|
|
</AdminShell>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
</RequireAuth>
|
2026-05-16 15:38:28 -05:00
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="/roadmap"
|
|
|
|
|
element={
|
|
|
|
|
<RequireAuth role="admin">
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<AdminShell>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
<RoadmapPage />
|
|
|
|
|
</Suspense>
|
|
|
|
|
</AdminShell>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
</RequireAuth>
|
2026-05-10 00:18:36 -05:00
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="/admin/roadmap"
|
|
|
|
|
element={
|
|
|
|
|
<RequireAuth role="admin">
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<AdminShell>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
v0.25.0: roadmap redesign, import CSRF fix, AdminDashboard removed
- RoadmapPage: kanban-style priority lanes, shadcn Collapsible/Tabs,
lazy-loaded activity log, admin-only /api/about/roadmap + /dev-log endpoints
- Import CSRF fix: added x-csrf-token header to importAdminBackup,
previewSpreadsheetImport, previewUserDbImport raw fetch() calls
- Removed AdminDashboard.jsx, replaced by RoadmapPage
- Added @radix-ui/react-collapsible + collapsible shadcn component
- Security audit by Private_Hudson: PASS (CSRF fix verified,
admin endpoints gated, path traversal mitigated, XSS safe)
2026-05-11 21:42:36 -05:00
|
|
|
<RoadmapPage />
|
2026-05-10 00:18:36 -05:00
|
|
|
</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 mainContentId={mainContentId} />
|
|
|
|
|
</RequireAuth>
|
|
|
|
|
}
|
|
|
|
|
>
|
2026-07-06 14:23:53 -05:00
|
|
|
<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="subscriptions"
|
|
|
|
|
element={
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
<SubscriptionsPage />
|
|
|
|
|
</Suspense>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="subscriptions/catalog"
|
|
|
|
|
element={
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
<SubscriptionCatalogPage />
|
|
|
|
|
</Suspense>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="categories"
|
|
|
|
|
element={
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
<CategoriesPage />
|
|
|
|
|
</Suspense>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="health"
|
|
|
|
|
element={
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
<HealthPage />
|
|
|
|
|
</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="snowball"
|
|
|
|
|
element={
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
<SnowballPage />
|
|
|
|
|
</Suspense>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="payoff"
|
|
|
|
|
element={
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
<PayoffPage />
|
|
|
|
|
</Suspense>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="spending"
|
|
|
|
|
element={
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
<SpendingPage />
|
|
|
|
|
</Suspense>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="bank-transactions"
|
|
|
|
|
element={
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
<BankTransactionsPage />
|
|
|
|
|
</Suspense>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="data"
|
|
|
|
|
element={
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
<DataPage />
|
|
|
|
|
</Suspense>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="profile"
|
|
|
|
|
element={
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
<ProfilePage />
|
|
|
|
|
</Suspense>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
}
|
|
|
|
|
/>
|
2026-06-03 20:28:37 -05:00
|
|
|
<Route path="*" element={<NotFoundPage />} />
|
2026-05-10 00:18:36 -05:00
|
|
|
</Route>
|
2026-06-03 20:28:37 -05:00
|
|
|
|
|
|
|
|
{/* Top-level catch-all — covers public paths that don't exist */}
|
|
|
|
|
<Route path="*" element={<NotFoundPage />} />
|
2026-05-10 00:18:36 -05:00
|
|
|
</Routes>
|
|
|
|
|
</main>
|
2026-05-10 03:10:43 -05:00
|
|
|
<ReactQueryDevtools initialIsOpen={false} />
|
|
|
|
|
</QueryClientProvider>
|
2026-05-03 19:51:57 -05:00
|
|
|
);
|
|
|
|
|
}
|