feat(web): app shell, library, entry detail

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-30 21:37:56 +01:00
co-authored by Claude Opus 5.5
parent 70bc6c36d3
commit 439d138967
16 changed files with 1743 additions and 3 deletions
+73
View File
@@ -0,0 +1,73 @@
import { QueryCache, QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { StrictMode, type ReactNode } from "react";
import { createRoot } from "react-dom/client";
import { Redirect, Route, Switch } from "wouter";
import { ApiError } from "./api.ts";
import { Shell } from "./components/Shell.tsx";
import { useMe } from "./hooks.ts";
import { Library } from "./routes/library.tsx";
import { Login } from "./routes/login.tsx";
import { NotYet } from "./routes/not-yet.tsx";
import { Register } from "./routes/register.tsx";
const queryClient: QueryClient = new QueryClient({
queryCache: new QueryCache({
// A 401 anywhere means the session ended: re-check who we are, which sends the user to sign in.
onError: (e, query) => {
if (e instanceof ApiError && e.status === 401 && query.queryKey[0] !== "me") void queryClient.invalidateQueries({ queryKey: ["me"] });
},
}),
defaultOptions: {
queries: {
staleTime: 15_000,
retry: (n, e) => !(e instanceof ApiError && e.status > 0 && e.status < 500) && n < 2,
},
},
});
function RequireAuth({ children }: { children: ReactNode }) {
const me = useMe();
if (me.isPending) return <div className="boot"><span className="logo">STTracker</span></div>;
if (me.error instanceof ApiError && me.error.status === 401) return <Redirect to="/login" replace />;
if (me.error) {
return (
<div className="auth">
<div className="auth-card" role="alert">
<span className="logo">STTracker</span>
<h1>Can't load your library</h1>
<p className="lede">{me.error.message}</p>
<button className="btn primary" onClick={() => void me.refetch()}>Try again</button>
</div>
</div>
);
}
return <>{children}</>;
}
function App() {
return (
<Switch>
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
<Route>
<RequireAuth>
<Shell>
<Switch>
<Route path="/">{() => <Library selectedId={null} />}</Route>
<Route path="/entries/:id">{(p) => <Library selectedId={p.id} />}</Route>
<Route path="/:page/*?">{(p) => <NotYet page={p.page} />}</Route>
</Switch>
</Shell>
</RequireAuth>
</Route>
</Switch>
);
}
createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</StrictMode>,
);