v3: comments say what the code can't — the documentation pass wanted JSDoc on every export and every field of an exported interface, which filled the code with restatements ("The dump.", "Whether it is loading.", "The props of X.", "Renders the X.") and module docs that told a reader nothing ("The hot and new feeds, and the props every main feed of the index takes."); some were wrong as well as vague. Every comment in api/, shared/ and src/ is rewritten under one rule, now stated in the README's "Comments" section: every module and export still has a JSDoc, and it gives context (who uses it and where), usage (what a caller must know: a precondition, what it leaves to the caller, a side effect) or its place in the chain (where its input comes from, where its output goes); a field, parameter, private function or local gets one only when it adds to its parent's, and context about a whole type goes on the type; a type's doc is phrased from the value's side ("Returned to Y by X when Z", "Passed to Y by X"); full sentences, as short as they can be, no label-colon-list; // goes wherever the code isn't obvious and says why, and the section banners and narration go. Every claim was checked against the code, which turned up some forty comments that were wrong: DumpCard's claimed it was only used by the hot and new feeds, the WebSocket welcome claimed to be a socket's first message (presence comes first), UpdateUserRequest said null removes a description (it keeps it), deletedDumpIds leaves out that it also holds dumps made private, THEME_COLOR claimed to mirror index.html's, and the like. The routers' JSDoc now says where they are mounted and what becomes of a thrown APIException. doc-lint.ts lets a field without a doc through; a module or export without one still fails. No code changes; 204 backend and 628 frontend tests pass, and doc:lint, deno check, deno lint and tsc are clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-09-29 14:31:14 +00:00
parent 42e0356d21
commit 0d4e344d77
287 changed files with 5679 additions and 5135 deletions

View File

@@ -1,5 +1,7 @@
/**
* The follow buttons, for a user and for a playlist.
* Renders the Follow buttons of a user's profile and of a playlist's page.
* Both go through `FollowContext`, which follows at once and rolls back when
* the server refuses.
*
* @module
*/
@@ -9,25 +11,24 @@ import { Trans } from "@lingui/react/macro";
import { useAuth } from "../../hooks/auth/useAuth.ts";
import { useFollows } from "../../hooks/social/useFollows.ts";
/** The props of {@link FollowUserButton}. */
/** Passed to {@link FollowUserButton} by the `UserPublicProfile` page. */
export interface FollowUserButtonProps {
/** The user's id. */
targetUserId: string;
/** The user's name, for the button's label. */
/** Names the user in the button's accessible label. */
targetUsername: string;
}
/** The props of {@link FollowPlaylistButton}. */
/** Passed to {@link FollowPlaylistButton} by the `PlaylistDetail` page. */
export interface FollowPlaylistButtonProps {
/** The playlist's id. */
targetPlaylistId: string;
/** Whether it is public; only a public playlist can be followed. */
/** Hides the button, since only a public playlist can be followed. */
isPublic: boolean;
}
/**
* Renders the button that follows or unfollows a user; nothing when signed out
* or on one's own profile.
* Renders the button that follows or unfollows a user. It renders nothing for
* a visitor or on the user's own profile, and is disabled until the follows
* have loaded.
*/
export function FollowUserButton(
{ targetUserId, targetUsername }: FollowUserButtonProps,
@@ -56,8 +57,10 @@ export function FollowUserButton(
}
/**
* Renders the button that follows or unfollows a playlist; nothing when signed
* out or for a private playlist.
* Renders the button that follows or unfollows a playlist. It renders nothing
* for a visitor or a private playlist, and is disabled until the follows have
* loaded. Unlike {@link FollowUserButton}, it shows on the user's own
* playlists too.
*/
export function FollowPlaylistButton(
{ targetPlaylistId, isPublic }: FollowPlaylistButtonProps,

View File

@@ -1,5 +1,5 @@
/**
* A comment's like button.
* Renders the like button of a comment in a dump's thread.
*
* @module
*/
@@ -9,25 +9,27 @@ import { t } from "@lingui/core/macro";
import { API_URL } from "../../config/api.ts";
import { UserListPopover } from "./UserListPopover.tsx";
/** The props of {@link LikeButton}. */
/**
* Passed to {@link LikeButton} by `CommentNode`, with the state and the
* functions of `WSContext`.
*/
export interface LikeButtonProps {
/** The comment's id. */
commentId: string;
/** Its like count. */
/** The socket's live count if it has one, else the comment's `likeCount`. */
count: number;
/** Whether the user likes it. */
liked: boolean;
/** Whether it is disabled (signed out). */
/** Set for a visitor, whom the tooltip asks to log in. */
disabled?: boolean;
/** Likes the comment; `shownCount` is the count on screen. */
/** Called with `count`, which `WSContext` needs until it has a live count. */
onLike: (commentId: string, shownCount?: number) => void;
/** Withdraws the like; `shownCount` is the count on screen. */
/** Called with `count`, as `onLike` is. */
onUnlike: (commentId: string, shownCount?: number) => void;
}
/**
* Renders a comment's like button with its count, which opens the list of who
* likes it.
* Renders a comment's like button and its count. Clicking the count, when it
* isn't zero, opens the list of likers (`GET /api/comments/:id/likers`)
* instead.
*/
export function LikeButton(
{ commentId, count, liked, disabled, onLike, onUnlike }: LikeButtonProps,
@@ -54,6 +56,7 @@ export function LikeButton(
ref={countRef}
className="like-count-clickable"
onClick={(e) => {
// Or the button would toggle the like too.
e.stopPropagation();
setPopoverOpen((o) => !o);
}}

View File

@@ -1,5 +1,6 @@
/**
* A popover listing users: who voted for a dump, who likes a comment.
* Renders the list of users behind a count. `VoteButton` opens it on a dump's
* voters, and `LikeButton` on a comment's likers.
*
* @module
*/
@@ -17,17 +18,21 @@ import type { PaginatedData, Wire } from "../../../shared/api.ts";
import { parseAPIResponse } from "../../utils/shared/api.ts";
import type { PublicUser } from "../../../shared/user.ts";
/** The props of {@link UserListPopover}. */
/**
* Passed to {@link UserListPopover} by the button whose count was clicked. The
* button owns the open state and unmounts the popover in `onClose`.
*/
export interface UserListPopoverProps {
/** The element it opens against. */
anchorRef: React.RefObject<HTMLElement | null>;
/** Called to close it: an outside click, Escape, or a scroll. */
/** Called on an outside click, on Escape, and on any scroll. */
onClose: () => void;
/** The paginated endpoint listing the users. */
/**
* A paginated endpoint answering `PaginatedData<PublicUser>`, without a query
* string: the popover appends `page` and `limit`.
*/
fetchUrl: string;
}
/** The list's state: loading, failed, or loaded with a page of users. */
type State =
| { status: "loading" }
| { status: "error"; error: string }
@@ -39,7 +44,8 @@ type State =
loadingMore: boolean;
};
/** Where the popover sits, in viewport coordinates. */
// In viewport coordinates. `bottom` replaces `top` when the popover opens above
// the anchor, so that it grows upward.
interface Placement {
left: number;
top?: number;
@@ -47,17 +53,17 @@ interface Placement {
maxHeight: number;
}
/** The popover's preferred largest height, in px. */
const PREFERRED_MAX_HEIGHT = 280;
/** The space kept clear of the viewport's edges, in px. */
const VIEWPORT_MARGIN = 8;
/** The least height worth opening below the anchor, in px. */
// Below this much room under the anchor, the popover opens above it instead,
// if there is more room there.
const MIN_USABLE_HEIGHT = 100;
/**
* Renders the users from `fetchUrl`, loading more on demand, in a popover
* against `anchorRef`. It is fixed and portaled to <body> so a card's overflow
* can't clip it, and flips above the anchor when there isn't enough room below.
* Renders the users from `fetchUrl` in a popover next to `anchorRef`, with a
* button that loads the next page. It is fixed and portaled to `<body>`, so a
* card's `overflow` can't clip it. It renders nothing until it has measured
* the anchor.
*/
export function UserListPopover(
{ anchorRef, onClose, fetchUrl }: UserListPopoverProps,
@@ -67,11 +73,8 @@ export function UserListPopover(
const [placement, setPlacement] = useState<Placement | null>(null);
const popoverRef = useRef<HTMLDivElement>(null);
// Position via the anchor's viewport rect (mirrors Tooltip.tsx) — fixed +
// portaled to <body> so the popover can't be clipped by a card's overflow.
// Flips above the anchor (anchored from the viewport bottom edge, so it
// grows upward) when there isn't enough room below, so a long list never
// overflows past the bottom of the viewport.
// Placed once, on open: any scroll closes the popover, so it never needs to
// follow the anchor.
useEffect(() => {
const anchorRect = anchorRef.current?.getBoundingClientRect();
if (!anchorRect) {
@@ -117,6 +120,7 @@ export function UserListPopover(
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") onClose();
}
// Captured, so a scroll inside any container closes it too.
function onScroll() {
onClose();
}

View File

@@ -1,5 +1,5 @@
/**
* A dump's vote button.
* Renders the upvote button of a dump, on its card and on its page.
*
* @module
*/
@@ -9,24 +9,26 @@ import { t } from "@lingui/core/macro";
import { API_URL } from "../../config/api.ts";
import { UserListPopover } from "./UserListPopover.tsx";
/** The props of {@link VoteButton}. */
/**
* Passed to {@link VoteButton} by `DumpCard`, `JournalCard` and the `Dump`
* page, with the state and the functions of `WSContext`.
*/
export interface VoteButtonProps {
/** The dump's id. */
dumpId: string;
/** Its vote count. */
/** The socket's live count if it has one, else the dump's `voteCount`. */
count: number;
/** Whether the user upvoted it. */
voted: boolean;
/** Whether it is disabled (signed out). */
/** Set for a visitor, whom the tooltip asks to log in. */
disabled?: boolean;
/** Upvotes the dump; `shownCount` is the count on screen. */
/** Called with `count`, which `WSContext` needs until it has a live count. */
onCast: (dumpId: string, shownCount?: number) => void;
/** Withdraws the vote; `shownCount` is the count on screen. */
/** Called with `count`, as `onCast` is. */
onRemove: (dumpId: string, shownCount?: number) => void;
}
/**
* Renders a dump's vote button with its count, which opens the list of voters.
* Renders a dump's upvote button and its count. Clicking the count, when it
* isn't zero, opens the list of voters (`GET /api/dumps/:id/voters`) instead.
*/
export function VoteButton(
{ dumpId, count, voted, disabled, onCast, onRemove }: VoteButtonProps,
@@ -50,6 +52,7 @@ export function VoteButton(
ref={countRef}
className="vote-count-clickable"
onClick={(e) => {
// Or the button would toggle the vote too.
e.stopPropagation();
setPopoverOpen((o) => !o);
}}