Files
buzz/.claude/skills/buzz-feature/SKILL.md
T
2026-08-28 13:19:50 +07:00

10 KiB

name, description, metadata
name description metadata
buzz-feature Full workflow for adding new features to Buzz Events
author version
dmgnr 1.0.0

Buzz — Adding & Modifying Features

This skill covers the full workflow for adding new features — from routing and server actions to admin panels and real-time updates.

Project Conventions

Path aliases

Alias Maps to
@/* ./* (project root)
$/* ./lib/*
#/* ./public/*

Tech choices

Concern Choice
Styling Tailwind CSS v4, shadcn/ui (Radix primitives), cn() from @/lib/utils
Forms Custom FormProvider/FormInput/FormAction system with autosave
Data fetching Server components (async, direct db calls) + React Suspense boundaries
Mutations Server actions ("use server") with Zod validation
Real-time Redis pub-sub → SSE via EventSourceEndpoint
Auth better-auth — server adminCheck(), client createAuthClient()
State React context, ICC (IccProvider/shared.state), no global state lib
Icons lucide-react

Key principles

  • Client/server split: Pages are async server components; wrap interactive parts in client components. Use "use client" only where needed (event handlers, state, effects).
  • Server actions in lib/api.ts (cross-feature) or per-feature api.ts — never inline in page files.
  • useEffect is ONLY for synchronizing with external systems (non-React widgets, browser APIs, network subscriptions). Do NOT use for:
    • Data transformation — compute during render instead
    • User events — put logic in the event handler
    • Chaining state updates — calculate all related updates together
    • Notifying parents — call the callback alongside setState
    • Resetting state on prop change — use the prop as the component's key instead
  • SSE events: Use the typed sse/tlSse() endpoints — never publish raw Redis messages.
  • Admin actions always call adminCheck() first and log via actionLog().
  • OG images: Dynamic ImageResponse in opengraph-image.ts files per route.

Adding a Public Feature Page

1. Create route

app/(ui)/<feature>/
  page.tsx            # Server component — fetch data, render layout
  client.tsx          # Client component(s) — interactivity
  form.tsx            # Form wrapper (server action submission)
  api.ts              # Server actions (if per-feature)
  rules.tsx           # Rules/help dialog (if needed)
  opengraph-image.ts  # Dynamic OG image (if needed)

Pattern for page.tsx:

import { Suspense } from "react";
import { FeatureClient } from "./client";

export default function FeaturePage() {
  return (
    <Suspense fallback={<Loading />}>
      <FeatureClient />
    </Suspense>
  );
}

2. Add server actions

For cross-feature actions, add to lib/api.ts. For per-feature actions, create app/(ui)/<feature>/api.ts:

"use server";

import { revalidatePath } from "next/cache";
import { adminCheck } from "$/auth";
import { db } from "$/db";
import { actionLog } from "$/api";

export async function submitSomething(formData: FormData) {
  // validation with zod
  // mutate db
  // revalidatePath(...)
  // publish SSE event
  // return result
}

3. Create form (using form system)

Use FormProvider/FormInput/FormAction from @/components/form:

"use client";

import { FormProvider, FormInput, FormAction } from "@/components/form";

export function FeatureForm() {
  return (
    <FormProvider id="feature-form">
      <FormInput name="name" label="Name" required />
      <FormAction action={submitSomething}>Submit</FormAction>
    </FormProvider>
  );
}

The form system auto-saves to localStorage (keyed by id, 10-min TTL) and restores on remount.

4. Add live SSE updates

Define events in lib/db/sse-endpoints.ts:

export const sse = sseEndpointMap({
  ...existing,
  myFeature: {
    update: z.object({ type: z.enum(["action1", "action2"]) }),
  },
});

Publish in your server action:

sse.myFeature.pub("update", { type: "action1" });

Subscribe on the client:

useEffect(() => {
  const { clean } = sse.myFeature.sub("update", (data) => {
    // handle update
  });
  return clean;
}, []);

For dynamic topics (e.g. per-tierlist), use tlSse(listId):

// Server
tlSse(listId).pub("update_states", states);

// Client — use EventSourceEndpoint directly:
const endpoint = sseEndpoint(`tl.${listId}`, {
  update_states: z.custom<(typeof tierlistStates.$inferSelect)[]>(),
});
endpoint.sub("update_states", (data) => { ... });

SSE endpoints are served via app/sse/[topic]/route.ts.

5. Add OG image

Create opengraph-image.ts using @/lib/og:

import { ImageResponse } from "next/og";

export const size = { width: 1200, height: 630 };

export default function OGImage() {
  return new ImageResponse(
    <div
      style={
        {
          /* ... */
        }
      }
    >
      Buzz Feature
    </div>,
    {
      ...size,
    },
  );
}

Adding an Admin Feature Page

1. Route structure

app/(ui)/admin/
  layout.tsx        # Sidebar layout — already includes auth check
  page.tsx          # Dashboard
  <feature>/
    page.tsx        # List/manage page
    client.tsx      # Client components
    api.ts          # Server actions
  @modal/
    <feature>/
      [id]/page.tsx       # Edit modal
      create/page.tsx     # Create modal
    default.tsx           # Returns empty div when no modal active

2. Layout already handles auth

app/(ui)/admin/layout.tsx checks adminCheck() and redirects to /login if unauthenticated. It also renders {modal} for parallel route modals.

3. Add sidebar navigation

In app/(ui)/admin/layout.tsx, add a SidebarMenuItem:

<SidebarMenuItem>
  <SidebarLink href="/admin/<feature>">
    <IconComponent />
    Feature Name
  </SidebarLink>
</SidebarMenuItem>

4. List/manage page

// app/(ui)/admin/<feature>/page.tsx
import { db } from "$/db";
import { FeatureClient } from "./client";

export default async function AdminFeaturePage() {
  const items = await db
    .select()
    .from(featureTable)
    .orderBy(featureTable.order);
  return <FeatureClient items={items} />;
}

5. Parallel route modal for create/edit

Create modal (@modal/<feature>/create/page.tsx):

import { ModalBase } from "@/components/modal";
import { CreateForm } from "./overrides";

export default function CreateModal() {
  return (
    <ModalBase title="Create Item">
      <CreateForm />
    </ModalBase>
  );
}

The ModalBase component wraps a Dialog that closes via router.back().

For editing, use [id]/page.tsx with the same pattern, fetching the item by ID server-side.

The parallel route renders inside {modal} in the admin layout. @modal/default.tsx returns an empty div when no modal route is matched.

6. Server actions with admin check

export async function updateItem(id: string, data: FormData) {
  if (!(await adminCheck())) throw "Unauthorized";
  // validate with zod
  // update db
  revalidatePath("/admin/<feature>");
  await actionLog(`Updated item ${id}`, data);
  // publish SSE if needed
}

7. Audit logging

Every admin mutation must call actionLog(text, details?) from $api:

await actionLog("Description of what happened", optionalDetails);

This inserts into the auditLog table, revalidates the log page, and publishes an SSE update for the live log view.

8. CDN assets

For features that need image/file uploads:

  • Use CdnChooserProvider + CDN chooser component from @/components/chooser
  • Upload via cdnify() from $api (returns the CDN record ID)
  • Reference CDN IDs in your DB schema as foreign keys to cdn table
  • Check references before deletion via checkCdnRefs() from $api
  • CDN references are tracked automatically in the cdn_references view

9. TanStack Table for data display

For list views with sorting/selection/context menus:

import { DataTable } from "@/components/tantable";
import { columns } from "./columns";

The DataTable component supports row selection, context menus, empty states, and search.

API Routes (non-server-action endpoints)

Create under app/api/<feature>/route.ts:

import { NextResponse } from "next/server";

export async function GET(req: Request) {
  // ...
  return NextResponse.json(data);
}

Patterns used in this project:

  • Proxy routes: /api/enka/[uid], /api/amber/char — forward to external APIs
  • Count routes: /api/artifact/count, /api/rubgram/count — quick aggregate queries
  • Health check: /api/health — returns status of DB, Enka, Amber, Redis
  • State endpoints: /api/tl/[ver]/states — REST-like read model for client

Working with the form system

The form system (@/components/form.tsx) provides:

  • FormProvider: Creates form context with autosave. Accepts id (storage key), optional defaultValues, and optional clean callback.
  • FormInput: Renders a labeled input bound to the form context. Supports name, label, required, type, placeholder, and children (for custom input elements).
  • FormAction: A submit button that calls the server action, handles loading state, shows toast on success/error, and optionally closes a dialog.
  • FormRow: Wraps fields in a horizontal or vertical layout row.
<FormProvider
  id="my-form"
  clean={() => {
    /* on success */
  }}
>
  <FormInput name="title" label="Title" required />
  <FormInput name="description" label="Description" />
  <FormAction action={submitAction}>Save</FormAction>
</FormProvider>

Typed forms (for complex data shapes) should define a TypedFormData type in a type.d.ts file within the feature directory.