import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { request } from './api/client.js'; import { Sidebar } from './components/Sidebar.jsx'; import { ToastProvider, useToast } from './components/Toast.jsx'; import { AccountView } from './routes/AccountView.jsx'; import { AlertsView } from './routes/AlertsView.jsx'; import { AuthPanel } from './routes/AuthPanel.jsx'; import { NotificationMethodsView } from './routes/NotificationMethodsView.jsx'; import { SiteSettingsPanel } from './routes/SiteSettingsPanel.jsx'; import { SitesView } from './routes/SitesView.jsx'; const viewPaths = { sites: '/sites', alerts: '/alerts', notifications: '/notifications', account: '/account', }; function normalizedPath(pathname) { if (!pathname || pathname === '/') return '/'; return pathname.endsWith('/') ? pathname.slice(0, -1) : pathname; } function parseRoute(pathname) { const path = normalizedPath(pathname); if (path === '/') return { kind: 'root' }; if (path === '/login') return { kind: 'auth', mode: 'login' }; if (path === '/register') return { kind: 'auth', mode: 'register' }; if (path === '/sites') return { kind: 'sites', activeView: 'sites', protected: true }; if (path === '/alerts') return { kind: 'alerts', activeView: 'alerts', protected: true }; if (path === '/notifications') { return { kind: 'notifications', activeView: 'notifications', protected: true }; } if (path === '/account') return { kind: 'account', activeView: 'account', protected: true }; const siteSettingsMatch = path.match(/^\/sites\/([^/]+)\/settings$/); if (siteSettingsMatch) { return { kind: 'siteSettings', activeView: 'sites', protected: true, siteId: decodeURIComponent(siteSettingsMatch[1]), }; } return { kind: 'notFound' }; } function isProtectedPath(pathname) { return Boolean(parseRoute(pathname).protected); } function currentPath() { return normalizedPath(window.location.pathname); } export function App() { const [user, setUser] = useState(null); const [locationPath, setLocationPath] = useState(currentPath); const [ready, setReady] = useState(false); const pendingReturnPath = useRef(null); const route = parseRoute(locationPath); const navigate = useCallback((path, options = {}) => { const nextPath = normalizedPath(path); if (currentPath() !== nextPath) { const method = options.replace ? 'replaceState' : 'pushState'; window.history[method](null, '', nextPath); } setLocationPath(nextPath); }, []); const logout = useMemo( () => async () => { await request('/api/auth/logout', { method: 'POST' }).catch(() => {}); pendingReturnPath.current = null; setUser(null); navigate('/login', { replace: true }); }, [navigate], ); useEffect(() => { function handlePopState() { setLocationPath(currentPath()); } window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); }, []); useEffect(() => { async function boot() { const csrf = await request('/api/auth/csrf'); localStorage.setItem('csrfToken', csrf.csrfToken); const me = await request('/api/auth/me').catch(() => null); setUser(me?.user ?? null); setReady(true); } boot(); }, []); useEffect(() => { if (!ready) return; if (user) { if (route.kind === 'auth' || route.kind === 'root' || route.kind === 'notFound') { navigate('/sites', { replace: true }); } return; } if (route.protected) { pendingReturnPath.current = locationPath; navigate('/login', { replace: true }); return; } if (route.kind === 'root' || route.kind === 'notFound') { navigate('/login', { replace: true }); } }, [locationPath, navigate, ready, route.kind, route.protected, user]); if (!ready) { return