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
This commit is contained in:
2026-10-02 16:49:14 -05:00
parent 9d7b0e805c
commit 7f5d034a1f
46 changed files with 1797 additions and 497 deletions
+43 -13
View File
@@ -1,4 +1,4 @@
import { useState } from "react";
import { Fragment, type ReactNode, useState } from "react";
import {
BrowserRouter,
Link,
@@ -6,15 +6,18 @@ import {
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 { BookList } from "./books/BookList";
import { BookDetail } from "./books/BookDetail";
import { BookForm } from "./books/BookForm";
import { secondaryButton } from "./ui";
import { BookList } from "./books/BookList";
import { AlertIcon, BookIcon } from "./icons";
import { ThemeToggle } from "./ThemeToggle";
import { errorText, secondaryButton } from "./ui";
export function App() {
return (
@@ -34,8 +37,22 @@ export function AppRoutes() {
<Route element={<Shell />}>
<Route path="/books" element={<BookList />} />
<Route path="/books/new" element={<BookForm />} />
<Route path="/books/:id/edit" element={<BookForm />} />
<Route path="/books/:id" element={<BookDetail />} />
<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 />} />
@@ -43,18 +60,30 @@ export function AppRoutes() {
);
}
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 bg-slate-50 text-slate-900">
<header className="border-b border-slate-200 bg-white">
<div className="mx-auto flex max-w-4xl items-center justify-between p-4">
<Link to="/books" className="text-lg font-semibold">
<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 text-sm">
<span>{user?.username}</span>
<div className="flex items-center gap-3">
<ThemeToggle />
<span className="text-muted">{user?.username}</span>
<button
type="button"
className={secondaryButton}
@@ -69,13 +98,14 @@ function Shell() {
{error && (
<p
role="alert"
className="mx-auto max-w-4xl px-4 pb-2 text-sm text-red-700"
className={`mx-auto max-w-4xl px-6 pb-3 ${errorText}`}
>
<AlertIcon />
{error}
</p>
)}
</header>
<main className="mx-auto max-w-4xl p-4">
<main className="mx-auto max-w-4xl px-6 pt-11 pb-18">
<Outlet />
</main>
</div>