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
CertRemind
; } if (!user) { const mode = route.kind === 'auth' ? route.mode : 'login'; return ( navigate(nextMode === 'register' ? '/register' : '/login')} onAuthed={(nextUser) => { setUser(nextUser); const returnPath = pendingReturnPath.current; pendingReturnPath.current = null; navigate(isProtectedPath(returnPath) ? returnPath : '/sites', { replace: true }); }} /> ); } function withShell(content) { return (
navigate(viewPaths[nextView] ?? '/sites')} onLogout={logout} />
{content}
); } if (route.kind === 'siteSettings') { return withShell( navigate('/sites')} navigate={navigate} />, ); } if (route.kind === 'alerts') { return withShell( navigate('/sites')} />); } if (route.kind === 'notifications') { return withShell( navigate('/sites')} />); } if (route.kind === 'account') { return withShell( navigate('/sites')} onSignedOut={() => { pendingReturnPath.current = null; setUser(null); navigate('/login', { replace: true }); }} />, ); } return withShell( navigate(`/sites/${encodeURIComponent(site.siteId)}/settings`) } />, ); } function SiteSettingsRoute({ siteId, onBack, navigate }) { const [site, setSite] = useState(null); const [loadingSiteId, setLoadingSiteId] = useState(siteId); const { showToast } = useToast(); useEffect(() => { let active = true; setSite(null); setLoadingSiteId(siteId); async function loadSite() { try { const data = await request(`/api/sites/${siteId}`); if (active) { setSite(data.site); } } catch (err) { if (active) { showToast({ type: 'error', message: err.message }); navigate('/sites', { replace: true }); } } } loadSite(); return () => { active = false; }; }, [navigate, showToast, siteId]); if (!site || loadingSiteId !== siteId) { return
CertRemind
; } return ; }