Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 9 additions & 0 deletions app/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->

# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.

This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.

<!-- END:nextjs-agent-rules -->
1 change: 1 addition & 0 deletions app/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
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
4 changes: 2 additions & 2 deletions app/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ const nextConfig: NextConfig = {
// needs into .next/standalone instead of shipping full node_modules.
output: "standalone",
serverExternalPackages: ["node:sqlite"],
// Codespaces, Cloudflare Tunnel, and other dev proxies serve the app
// Cloudflare Tunnel and other dev proxies serve the app
// through a forwarded https:// URL rather than localhost. Next.js
// Server Actions reject requests whose Origin doesn't match the host
// 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
48 changes: 48 additions & 0 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@
"@types/node": "^22.10.2",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"typescript": "^5.7.2",
"vitest": "^4.1.11",
"eslint": "^9.17.0",
"eslint-config-next": "^16.3.1"
"eslint-config-next": "^16.3.1",
"playwright": "^1.62.1",
"typescript": "^5.7.2",
"vitest": "^4.1.11"
},
"engines": {
"node": ">=22.5.0"
Expand Down
Binary file added app/public/fonts/BebasNeue-Regular.ttf
Binary file not shown.
Binary file added app/public/fonts/VandalBlowGraffiti.ttf
Binary file not shown.
Binary file added app/public/fonts/VandalBlowInner.otf
Binary file not shown.
Binary file added app/public/fonts/VandalBlowShadow.otf
Binary file not shown.
Binary file added app/public/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
8 changes: 6 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,10 +87,12 @@ 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");

setReachableForAsks(viewer.id, reachable);
revalidatePath("/asks");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
revalidatePath("/asks/mine");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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/friends/[handle]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface Props {
params: Promise<{ handle: string }>;
}

/** Server page listing direct friends and one additional connection hop for *handle*, enforcing visibility and block rules. */
export default async function ExploreFriendsPage({ params }: Props) {
const { handle: rawHandle } = await params;
const handle = rawHandle.trim().toLowerCase();
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
19 changes: 10 additions & 9 deletions app/src/app/(platform)/messages/[handle]/ThreadComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,26 @@ 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);

return (
<form action={formAction} className="aim-composer">
<form action={formAction} className="msg-composer">
{state.error && (
<div className="error-banner" role="alert">
{state.error}
</div>
)}
<div className="aim-composer-row">
<textarea
name="body"
rows={2}
maxLength={4000}
placeholder={`Message ${recipientHandle}...`}
required
/>
<textarea
name="body"
rows={3}
maxLength={4000}
placeholder={`Message ${recipientHandle}...`}
required
/>
<div className="msg-composer-actions">
<button type="submit" className="btn" disabled={pending}>
{pending ? "Sending…" : "Send"}
</button>
Expand Down
57 changes: 27 additions & 30 deletions app/src/app/(platform)/messages/[handle]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ 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();
if (!viewer) redirect(`/login?next=/messages`);
if (!viewer) redirect(`/login?next=${encodeURIComponent(`/messages/${rawParam}`)}`);

const handle = parseHandleParam(rawParam);
if (!handle) notFound();
Expand All @@ -35,36 +36,32 @@ export default async function MessageThreadPage({ params }: Props) {
}

return (
<main className="container aim-page">
<div className="aim-window">
<div className="aim-titlebar">
<Link href="/messages" className="aim-back">
</Link>
<span>{handle}</span>
</div>
<div className="aim-window-body">
{blocked ? (
<p className="aim-empty">You can&apos;t message this person.</p>
) : (
<>
<div className="aim-thread">
{messages.length === 0 ? (
<p className="aim-empty">No messages yet. Say hi.</p>
) : (
messages.map((m) => (
<p key={m.id} className={m.senderId === viewer.id ? "aim-msg aim-msg-mine" : "aim-msg"}>
<span className="aim-msg-from mono">{m.senderId === viewer.id ? viewer.handle : handle}:</span>{" "}
{m.body}
</p>
))
)}
</div>
<ThreadComposer recipientHandle={handle} />
</>
)}
</div>
<main className="container">
<div className="msg-thread-header">
<Link href="/messages" className="msg-back">
← Back
</Link>
<h1>{handle}</h1>
</div>
{blocked ? (
<p className="msg-empty">You can&apos;t message this person.</p>
) : (
<>
<div className="msg-thread">
{messages.length === 0 ? (
<p className="msg-empty">No messages yet. Say hi.</p>
) : (
messages.map((m) => (
<p key={m.id} className={m.senderId === viewer.id ? "msg-line msg-line-mine" : "msg-line"}>
<span className="msg-sender">{m.senderId === viewer.id ? viewer.handle : handle}:</span>{" "}
{m.body}
</p>
))
)}
</div>
<ThreadComposer recipientHandle={handle} />
</>
)}
</main>
);
}
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
Loading