v3: src/ is documented like api/ and shared/, and the reference covers the whole repository — the frontend had no module docs, JSDoc on some components and hooks and not on others, props typed inline in half the components and in private interfaces in the rest, // comments on declarations, and no return types on 147 exported functions, so deno doc --lint over src/ found 637 errors. The rule api/ follows is applied to every non-test module of src/: a /** … @module */ block, JSDoc on every export and every field of an exported interface, /** */ for declarations and // for notes, verbs for functions ("Renders…" for a component), parameters named in the prose, {@link} only for symbols in scope, and a page's JSDoc names its route and who may see it (App.tsx's module doc lists the route table). For deno doc to show them, every exported function states its return type: components React.JSX.Element (or | null, or a portal), memo and forwardRef components their exotic types, contexts React.Context<…>, and a hook returning several things a documented Use<Name>Result interface (useAuth, useRequiredAuth, useFading, useEmojiTrigger, useMentionAutocomplete, useDumpCardLink, usePlayRichContent; FeedCacheResult, YouTubePlaylistControls and UseApiFormReturn are renamed to match). Every component that takes props takes an exported, documented <Name>Props interface — the 64 inline prop types are lifted into one, DumpFeed gets DumpFeedProps extending MainFeedProps. React types go through the global React namespace: the four default imports of react are gone, since deno doc can't follow them. Two duplicates go too: useMentionAutocomplete's UserResult was shared/'s UserSuggestion, and JournalCard re-exported JournalShape for no one; i18n.ts no longer re-exports Lingui's i18n, which its three users now import from @lingui/core. deno task doc and doc:lint now take src/ (but its tests and the generated locales), the index page gains a Frontend section linking every module by layer and domain, mod.test.ts walks src/ as well, and the docs workflow runs on changes to src/. deno doc doesn't document npm packages, so it takes a type imported from one — react-hook-form's FieldValues in the form fields, frimousse's SkinTone — for a private one; doc:lint now goes through doc-lint.ts, which fails on every diagnostic but those, 25 of them. The README states the frontend's part of the rule. No behaviour changes; 205 backend and 628 frontend tests pass, and tsc, deno check and deno lint are clean.
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 50s
Build and Deploy Static Documentation Site / build-and-push (push) Successful in 22s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-09-28 07:23:03 +00:00
parent b85a71d86e
commit 563daa720c
171 changed files with 3921 additions and 851 deletions

View File

@@ -1,3 +1,29 @@
/**
* The app: its providers and its routes.
*
* - `/` — redirects to the default feed tab.
* - `/~/:feedTab` — the feed of every dump ({@link Index}).
* - `/:categorySlug/:feedTab` — a category's feed; `/:categorySlug` redirects
* to
* its default tab.
* - `/dumps/:selectedDump` — a dump ({@link Dump}); `/edit` — its editor
* ({@link DumpEdit}), signed in.
* - `/login`, `/register` — sign-in and sign-up ({@link UserLogin},
* {@link UserRegister}), signed out.
* - `/reset-password` — a new password from a reset link ({@link
* ResetPassword}).
* - `/users/:username` and `/users/:username/~/:profileTab` — a profile
* ({@link UserPublicProfile}); `/dumps`, `/upvoted`, `/playlists` — its full
* lists ({@link UserDumps}, {@link UserUpvoted}, {@link UserPlaylists}).
* - `/playlists/:playlistId` — a playlist ({@link PlaylistDetail}).
* - `/search` and `/search/~/:searchTab` — search results ({@link Search}).
* - `/notifications` — the user's notifications ({@link Notifications}), signed
* in.
* - anything else — {@link NotFound}.
*
* @module
*/
import { lazy, Suspense } from "react";
import {
BrowserRouter,
@@ -27,6 +53,7 @@ import { ChatFab } from "./components/layout/ChatFab.tsx";
import "./App.css";
/** The feed page, loaded with its route; so is every page below. */
const Index = lazy(() =>
import("./pages/Index.tsx").then((m) => ({ default: m.Index }))
);
@@ -80,22 +107,27 @@ const NotFound = lazy(() =>
import("./pages/NotFound.tsx").then((m) => ({ default: m.NotFound }))
);
// Default feed tab for the current viewer ("followed" needs a session).
/**
* Returns the default feed tab for the current viewer ("followed" needs a
* session).
*/
function useResolvedDefaultTab() {
const { user } = useAuth();
const [preferredTab] = useDefaultFeedTab();
return preferredTab === "followed" && !user ? DEFAULT_FEED_TAB : preferredTab;
}
// The query string has to survive the hop: the Web Share Target posts to "/",
// so an Android share arrives here as `/?share_url=…` and the feed below is the
// only thing that can act on it.
/**
* Redirects `/` to the default feed tab. The query string has to survive the
* hop: the Web Share Target posts to "/", so an Android share arrives here as
* `/?share_url=…` and the feed below is the only thing that can act on it.
*/
function IndexRedirect() {
const { search } = useLocation();
return <Navigate to={`/~/${useResolvedDefaultTab()}${search}`} replace />;
}
// Bare `/<slug>` lands on that category's default feed tab.
/** Redirects a bare `/<slug>` to that category's default feed tab. */
function CategoryRedirect() {
const { categorySlug } = useParams();
const { search } = useLocation();
@@ -107,14 +139,17 @@ function CategoryRedirect() {
);
}
// Both `/~/:feedTab` (all) and `/:categorySlug/:feedTab` render the same feed.
// Keying by scope remounts the feed when switching categories so it refetches,
// while switching tabs within a scope keeps it mounted (client-side re-sort).
/**
* Renders the feed of `/~/:feedTab` (all) and `/:categorySlug/:feedTab` alike.
* Keying by scope remounts the feed when switching categories so it refetches,
* while switching tabs within a scope keeps it mounted (client-side re-sort).
*/
function ScopedIndex() {
const { categorySlug } = useParams();
return <Index key={categorySlug ?? "~"} categorySlug={categorySlug} />;
}
/** Renders the route table, and the floating buttons over every page. */
function AppRoutes() {
return (
<>
@@ -181,7 +216,11 @@ function AppRoutes() {
);
}
export function App() {
/**
* Renders the app: the theme, session, player, socket, follow and category
* providers around the router, the chat, the routes and the global player.
*/
export function App(): React.JSX.Element {
return (
<ThemeProvider>
<AuthProvider>