111 lines
3.6 KiB
TypeScript
111 lines
3.6 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import { Alert, Box, CssBaseline, LinearProgress, Snackbar, ThemeProvider, useMediaQuery } from "@mui/material";
|
|
import { Navigate, Route, Routes } from "react-router-dom";
|
|
|
|
import AppShell from "./components/AppShell";
|
|
import { AuthProvider, useAuth } from "./lib/auth";
|
|
import { SettingsProvider, useSettings } from "./lib/settings";
|
|
import { buildTheme, type ThemeMode } from "./theme";
|
|
import BlockerPage from "./pages/BlockerPage";
|
|
import LoginPage from "./pages/LoginPage";
|
|
import ReportPage from "./pages/ReportPage";
|
|
import SettingsPage from "./pages/SettingsPage";
|
|
import TasksPage from "./pages/TasksPage";
|
|
import TimerPage from "./pages/TimerPage";
|
|
|
|
function Shell() {
|
|
const { settings, loading, error } = useSettings();
|
|
const prefersDark = useMediaQuery("(prefers-color-scheme: dark)");
|
|
const [toast, setToast] = useState<string | null>(null);
|
|
|
|
const mode: ThemeMode = useMemo(() => {
|
|
const preference = settings?.appearance.theme ?? "system";
|
|
if (preference === "system") return prefersDark ? "dark" : "light";
|
|
return preference;
|
|
}, [settings?.appearance.theme, prefersDark]);
|
|
|
|
const theme = useMemo(
|
|
() => buildTheme(mode, settings?.appearance.compact ?? false),
|
|
[mode, settings?.appearance.compact],
|
|
);
|
|
|
|
// Keep the browser chrome in step with the app's surface.
|
|
useEffect(() => {
|
|
document.documentElement.dataset.theme = mode;
|
|
document.body.style.backgroundColor = theme.palette.background.default;
|
|
}, [mode, theme]);
|
|
|
|
return (
|
|
<ThemeProvider theme={theme}>
|
|
<CssBaseline />
|
|
{loading && <LinearProgress />}
|
|
{error && (
|
|
<Box sx={{ p: 2 }}>
|
|
<Alert severity="error">
|
|
The backend is not reachable ({error}). Start it with{" "}
|
|
<code>docker compose up</code> or check the API token in Settings.
|
|
</Alert>
|
|
</Box>
|
|
)}
|
|
<AppShell onToast={setToast} dark={mode === "dark"}>
|
|
<Routes>
|
|
<Route path="/" element={<TimerPage onToast={setToast} dark={mode === "dark"} />} />
|
|
<Route path="/tasks" element={<TasksPage onToast={setToast} />} />
|
|
<Route path="/blocker" element={<BlockerPage onToast={setToast} />} />
|
|
<Route path="/report" element={<ReportPage dark={mode === "dark"} onToast={setToast} />} />
|
|
<Route path="/settings" element={<SettingsPage onToast={setToast} />} />
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
</Routes>
|
|
</AppShell>
|
|
<Snackbar
|
|
open={toast !== null}
|
|
autoHideDuration={5000}
|
|
onClose={() => setToast(null)}
|
|
message={toast ?? ""}
|
|
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
|
/>
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
function Gate() {
|
|
const { me, loading } = useAuth();
|
|
const prefersDark = useMediaQuery("(prefers-color-scheme: dark)");
|
|
|
|
if (loading || !me) {
|
|
// The paper theme, so the blank moment before `/api/auth/me` answers
|
|
// doesn't flash a default MUI white.
|
|
const theme = buildTheme(prefersDark ? "dark" : "light", false);
|
|
return (
|
|
<ThemeProvider theme={theme}>
|
|
<CssBaseline />
|
|
<LinearProgress />
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
if (!me.authenticated) {
|
|
const theme = buildTheme(prefersDark ? "dark" : "light", false);
|
|
return (
|
|
<ThemeProvider theme={theme}>
|
|
<CssBaseline />
|
|
<LoginPage />
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SettingsProvider>
|
|
<Shell />
|
|
</SettingsProvider>
|
|
);
|
|
}
|
|
|
|
export default function App() {
|
|
return (
|
|
<AuthProvider>
|
|
<Gate />
|
|
</AuthProvider>
|
|
);
|
|
}
|