- 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
114 lines
3.1 KiB
TypeScript
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>
|
|
);
|
|
}
|