Files
mroberts 7f5d034a1f feat: auth by default, theme toggle, CoderPad startup, review fixes
- Require a session on every route; public routes opt out with @allow_anonymous
- Split password hashing and pepper loading into passwords.py
- Add a system/light/dark theme toggle backed by light-dark() colors
- Ignore stale 401s from an earlier session, PATCH only changed book fields,
  and block overlapping journal-entry saves
- Add bin/start and CoderPad Vite server settings for the pad's start/restart
- Rewrite README as a mise onboarding guide; expand .gitignore
- Include review-round fixes and tests
2026-10-02 16:49:14 -05:00

114 lines
3.1 KiB
TypeScript

import { Fragment, type ReactNode, useState } from "react";
import {
BrowserRouter,
Link,
Navigate,
Outlet,
Route,
Routes,
useParams,
} from "react-router";
import { errorMessage } from "./api";
import { AuthProvider, useAuth } from "./auth/AuthContext";
import { LoginForm } from "./auth/LoginForm";
import { RequireAuth } from "./auth/RequireAuth";
import { BookDetail } from "./books/BookDetail";
import { BookForm } from "./books/BookForm";
import { BookList } from "./books/BookList";
import { AlertIcon, BookIcon } from "./icons";
import { ThemeToggle } from "./ThemeToggle";
import { errorText, secondaryButton } from "./ui";
export function App() {
return (
<BrowserRouter>
<AuthProvider>
<AppRoutes />
</AuthProvider>
</BrowserRouter>
);
}
export function AppRoutes() {
return (
<Routes>
<Route path="/login" element={<LoginForm />} />
<Route element={<RequireAuth />}>
<Route element={<Shell />}>
<Route path="/books" element={<BookList />} />
<Route path="/books/new" element={<BookForm />} />
<Route
path="/books/:id/edit"
element={
<KeyedByBookId>
<BookForm />
</KeyedByBookId>
}
/>
<Route
path="/books/:id"
element={
<KeyedByBookId>
<BookDetail />
</KeyedByBookId>
}
/>
</Route>
</Route>
<Route path="*" element={<Navigate to="/books" replace />} />
</Routes>
);
}
function KeyedByBookId({ children }: { children: ReactNode }) {
const { id } = useParams();
return <Fragment key={id}>{children}</Fragment>;
}
function Shell() {
const { user, logout } = useAuth();
const [error, setError] = useState<string | null>(null);
return (
<div className="min-h-screen">
<header className="border-b border-line">
<div className="mx-auto flex max-w-4xl flex-wrap items-center justify-between gap-3 px-6 py-3.5">
<Link
to="/books"
className="flex items-center gap-2.5 rounded-lg font-serif text-2xl font-semibold focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-accent"
>
<span className="text-accent">
<BookIcon />
</span>
My Library
</Link>
<div className="flex items-center gap-3">
<ThemeToggle />
<span className="text-muted">{user?.username}</span>
<button
type="button"
className={secondaryButton}
onClick={() =>
logout().catch((err) => setError(errorMessage(err)))
}
>
Log out
</button>
</div>
</div>
{error && (
<p
role="alert"
className={`mx-auto max-w-4xl px-6 pb-3 ${errorText}`}
>
<AlertIcon />
{error}
</p>
)}
</header>
<main className="mx-auto max-w-4xl px-6 pt-11 pb-18">
<Outlet />
</main>
</div>
);
}