Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e01e192
Fix CodeRabbit security, correctness, and performance findings
claude Aug 20, 2026
5ebf372
Add JSDoc to all functions for 100% docstring coverage
claude Aug 20, 2026
074ab12
Add JSDoc to remaining functions for 100% docstring coverage
claude Aug 20, 2026
a420ada
fix: restrict moderateGuestbookEntry to pending entries only
claude Aug 20, 2026
08f25a3
docs: add JSDoc to all remaining undocumented functions in diff scope
claude Aug 20, 2026
5d70d1f
docs: add JSDoc to remaining StudioClient inner action handlers
claude Aug 20, 2026
be65b1d
Add JSDoc to all webRings.ts exports for 100% docstring coverage
claude Aug 21, 2026
a4c161b
Fix publishExistingDraft JSDoc: clarify it updates editor state to pu…
claude Aug 21, 2026
660884b
Add JSDoc to remaining undocumented functions for 100% coverage
claude Aug 21, 2026
57fa910
Fix commitEdit JSDoc: clarifies current doc is saved to undo stack, n…
claude Aug 21, 2026
3136418
Add JSDoc to ctxWithStatus test helper in moduleRegistry.test.ts for …
claude Aug 21, 2026
a92870c
Add comprehensive mobile/tablet responsive styles (768px, 640px, 400p…
claude Aug 21, 2026
231c7e8
Add JSDoc to useMemo hooks in StudioClient for 100% docstring coverage
claude Aug 21, 2026
6d149c7
Fix all CodeRabbit findings: privacy, correctness, and test coverage
claude Aug 21, 2026
b8dcc90
Add Next.js agent rule files generated by dev server
claude Aug 21, 2026
919e839
UI overhaul: black/white minimalist graffiti theme + messenger redesign
claude Aug 21, 2026
2e00bc5
Self-host Bebas Neue font for reliable rendering
claude Aug 21, 2026
8433f5a
Centered logo nav + Vandal Blow Graffiti font wiring
claude Aug 21, 2026
cead9e5
Add graffiti logo image and Vandal Blow font files
claude Aug 21, 2026
773aa5e
Add playwright dev dependency for screenshot testing
claude Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions app/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ out
*.db-journal
*.db-wal
*.db-shm
.env
.env.local
.env*
coverage
.git
2 changes: 1 addition & 1 deletion app/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Multi-stage build for a small, self-contained production image.
# Needs Node 22.5+ for node:sqlite (same requirement as local dev).

FROM node:22-slim AS base
FROM node:22.13-slim AS base

FROM base AS deps
WORKDIR /app
Expand Down
2 changes: 1 addition & 1 deletion app/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const nextConfig: NextConfig = {
// it's running on unless that origin is explicitly allowed here.
experimental: {
serverActions: {
allowedOrigins: ["localhost:3000", "*.app.github.dev", "*.trycloudflare.com"],
allowedOrigins: ["localhost:3000", "*.trycloudflare.com"],
},
},
};
Expand Down
1 change: 1 addition & 0 deletions app/src/app/(platform)/asks/AnswerForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { answerAskAction, type AskActionState } from "./actions";

const initialState: AskActionState = {};

/** Form that lets the page owner submit an answer to a specific ask. */
export function AnswerForm({ askId }: { askId: string }) {
const boundAction = answerAskAction.bind(null, askId);
const [state, formAction, pending] = useActionState(boundAction, initialState);
Expand Down
2 changes: 2 additions & 0 deletions app/src/app/(platform)/asks/AskCard.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { Answer, Ask } from "@/lib/asks";
import { AnswerForm } from "./AnswerForm";

/** Formats an ISO date string as a short locale date for display in ask cards. */
function formatDate(iso: string): string {
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return iso;
return date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
}

/** Renders a single ask with its answers and, for the page owner, an answer form. */
export function AskCard({
ask,
answers,
Expand Down
1 change: 1 addition & 0 deletions app/src/app/(platform)/asks/AskForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createAskAction, type AskActionState } from "./actions";

const initialState: AskActionState = {};

/** Form that lets a viewer submit a new ask to the current page owner. */
export function AskForm() {
const [state, formAction, pending] = useActionState(createAskAction, initialState);

Expand Down
7 changes: 5 additions & 2 deletions app/src/app/(platform)/asks/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ export interface AskActionState {
success?: string;
}

/** Maps AskError and RateLimitError to user-facing state; re-throws anything else. */
function handleAskError(e: unknown): never | AskActionState {
if (e instanceof AskError) return { error: e.message };
if (e instanceof RateLimitError) return { error: e.message };
throw e;
}

/** Server action: validate and submit a new ask for the signed-in viewer. */
export async function createAskAction(
_prevState: AskActionState,
formData: FormData,
Expand All @@ -36,8 +38,6 @@ export async function createAskAction(
const isSensitive = formData.get("sensitive") === "on";

try {
const key = await rateLimitActorKey("ask-view", viewer.id);
checkRateLimit(key, 20);
createAsk({ askerId: viewer.id, body, domain, isAnonymous, isSensitive });
} catch (e) {
return handleAskError(e);
Expand All @@ -48,6 +48,7 @@ export async function createAskAction(
return { success: "Your ask is posted." };
}

/** Server action: submit an answer to *askId* from the signed-in viewer. */
export async function answerAskAction(
askId: string,
_prevState: AskActionState,
Expand All @@ -70,6 +71,7 @@ export async function answerAskAction(
return { success: "Your answer is posted." };
}

/** Server action: close *askId* — only the original asker may do this. */
export async function closeAskAction(askId: string): Promise<void> {
const viewer = await getCurrentUser();
if (!viewer) redirect("/login?next=/asks/mine");
Expand All @@ -85,6 +87,7 @@ export async function closeAskAction(askId: string): Promise<void> {
revalidatePath("/asks/mine");
}

/** Server action: opt the signed-in viewer in or out of the ask pool. */
export async function setReachableAction(reachable: boolean): Promise<void> {
const viewer = await getCurrentUser();
if (!viewer) redirect("/login?next=/asks");
Expand Down
1 change: 1 addition & 0 deletions app/src/app/(platform)/asks/mine/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { listAnswers, listAsksByUser } from "@/lib/asks";
import { getCurrentUser } from "@/lib/session";
import { closeAskAction } from "../actions";

/** Server page showing the signed-in user's own asks and their statuses. */
export default async function MyAsksPage() {
const viewer = await getCurrentUser();
if (!viewer) redirect("/login?next=/asks/mine");
Expand Down
1 change: 1 addition & 0 deletions app/src/app/(platform)/asks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { setReachableAction } from "./actions";
import { AskCard } from "./AskCard";
import { AskForm } from "./AskForm";

/** Server page rendering the public Ask Us pool, with the ask form for signed-in users. */
export default async function AsksPage() {
const viewer = await getCurrentUser();
if (!viewer) redirect("/login?next=/asks");
Expand Down
1 change: 1 addition & 0 deletions app/src/app/(platform)/explore/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface Props {
searchParams: Promise<{ q?: string }>;
}

/** Server page for the public Explore/discovery feed, with tag and text search. */
export default async function ExplorePage({ searchParams }: Props) {
ensureSeedRings();
ensureSeedCollections();
Expand Down
1 change: 1 addition & 0 deletions app/src/app/(platform)/make/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface MakeState {
error?: string;
}

/** Server action for the page-creation wizard. Validates template and display name, builds and saves the page document, then redirects to the new profile. */
export async function makeFlowAction(_prevState: MakeState, formData: FormData): Promise<MakeState> {
const viewer = await getCurrentUser();
if (!viewer) redirect("/login?next=/make");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { sendMessageAction, type SendMessageState } from "../actions";

const initialState: SendMessageState = {};

/** Message-composition form for a direct-message thread with *recipientHandle*. */
export function ThreadComposer({ recipientHandle }: { recipientHandle: string }) {
const boundAction = sendMessageAction.bind(null, recipientHandle);
const [state, formAction, pending] = useActionState(boundAction, initialState);
Expand Down
1 change: 1 addition & 0 deletions app/src/app/(platform)/messages/[handle]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface Props {
params: Promise<{ handle: string }>;
}

/** Server page rendering the direct-message thread between the signed-in user and the profile at *handle*. */
export default async function MessageThreadPage({ params }: Props) {
const { handle: rawParam } = await params;
const viewer = await getCurrentUser();
Expand Down
2 changes: 2 additions & 0 deletions app/src/app/(platform)/messages/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface SendMessageState {
error?: string;
}

/** Server action: send a message to *recipientHandle* from the signed-in viewer. */
export async function sendMessageAction(
recipientHandle: string,
_prevState: SendMessageState,
Expand Down Expand Up @@ -38,6 +39,7 @@ export async function sendMessageAction(
return {};
}

/** Server action: mark all unread messages in *conversationId* as read for the viewer. */
export async function markConversationReadAction(conversationId: string): Promise<void> {
const viewer = await getCurrentUser();
if (!viewer) redirect("/login?next=/messages");
Expand Down
1 change: 1 addition & 0 deletions app/src/app/(platform)/messages/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Link from "next/link";
import { getCurrentUser } from "@/lib/session";
import { listConversations } from "@/lib/messages";

/** Server page listing all direct-message conversations for the signed-in user. */
export default async function MessagesPage() {
const viewer = await getCurrentUser();
if (!viewer) redirect("/login?next=/messages");
Expand Down
1 change: 1 addition & 0 deletions app/src/app/(platform)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const STEPS = [
{ label: "Wander", desc: "Discover other pages, no feed required" },
];

/** Server page rendering the platform landing page — signed-in users see their dashboard, visitors see the public feature overview. */
export default async function HomePage() {
const viewer = await getCurrentUser();

Expand Down
16 changes: 16 additions & 0 deletions app/src/app/(platform)/studio/StudioClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const TABS: { id: TabId; label: string }[] = [
{ id: "publish", label: "Publish" },
];

/** Builds the live Top Eight preview list from the current handle selections and the user's friend list. */
function buildTopEightPreview(handles: string[], friends: FriendSummary[]): TopEightLink[] {
return handles.map((handle) => ({
handle,
Expand All @@ -75,10 +76,12 @@ export interface StudioClientProps {
guestbookEntries: GuestbookEntry[];
}

/** Generates a new random UUID for client-side module identifiers. */
function newId(): string {
return crypto.randomUUID();
}

/** Resizes a flat pixel array to new dimensions, preserving existing pixels and filling added cells with transparent. */
function resizePixelGrid(
width: number,
height: number,
Expand All @@ -99,6 +102,7 @@ function resizePixelGrid(
return next;
}

/** Returns a blank 8×8 pixel art piece with a generated id and all pixels set to transparent. */
function defaultPixelArtPiece(): PixelArtPiece {
const width = 8;
const height = 8;
Expand All @@ -111,6 +115,7 @@ function defaultPixelArtPiece(): PixelArtPiece {
};
}

/** Interactive client component for the page editor Studio, managing draft state, live preview, and all module editors. */
export function StudioClient({
initialDocument,
publishedDocument,
Expand Down Expand Up @@ -191,6 +196,7 @@ export function StudioClient({
[],
);

/** Saves the current editor state as a draft without publishing it. */
const saveDraft = () => {
runAction("Draft saved — safe to preview without publishing.", () =>
saveDraftAction(JSON.stringify(document)).then((r) => {
Expand All @@ -200,6 +206,7 @@ export function StudioClient({
);
};

/** Saves and immediately publishes the current editor state, discarding any pending draft. */
const saveAndPublish = () => {
runAction("Published live.", () =>
saveAndPublishAction(JSON.stringify(document)).then((r) => {
Expand All @@ -209,6 +216,7 @@ export function StudioClient({
);
};

/** Publishes the saved draft without changing the editor's working state. */
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const publishExistingDraft = () => {
runAction("Draft published live.", () =>
publishDraftAction().then((r) => {
Expand All @@ -222,6 +230,7 @@ export function StudioClient({
);
};

/** Exports the current page document as a JSON file download. */
const handleExport = () => {
startTransition(async () => {
setMessage(null);
Expand All @@ -243,6 +252,7 @@ export function StudioClient({
});
};

/** Reads *file*, imports it as the current page document, and publishes it immediately. */
const handleImport = (file: File) => {
startTransition(async () => {
setMessage(null);
Expand All @@ -263,6 +273,7 @@ export function StudioClient({
});
};

/** Restores a previously saved version by *versionId* and reloads the editor state. */
const restoreVersion = (versionId: string) => {
runAction("Version restored.", () =>
restoreVersionAction(versionId).then((r) => {
Expand Down Expand Up @@ -453,6 +464,7 @@ export function StudioClient({
);
}

/** Studio tab for colors, fonts, background, and theme publishing. */
function LookTab({
document: doc,
onChange,
Expand Down Expand Up @@ -664,6 +676,7 @@ function LookTab({
);
}

/** Studio tab for reordering and toggling the visible page sections. */
function LayoutTab({
document: doc,
onChange,
Expand Down Expand Up @@ -761,6 +774,7 @@ function LayoutTab({
);
}

/** Studio tab for editing all page content: identity, links, modules, Top 8, and more. */
function ContentTab({
document: doc,
onChange,
Expand Down Expand Up @@ -1608,6 +1622,7 @@ function ContentTab({
);
}

/** Studio tab for custom CSS, contrast warnings, and scoped CSS preview. */
function AccessTab({
document: doc,
onChange,
Expand Down Expand Up @@ -1725,6 +1740,7 @@ function AccessTab({
);
}

/** Studio tab for publish state, visibility, discovery, guestbook, and page management actions. */
function PublishTab({
document: doc,
onChange,
Expand Down
Loading