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

355 lines
10 KiB
Markdown

---
name: buzz-feature
description: Full workflow for adding new features to Buzz Events
metadata:
author: dmgnr
version: 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`:**
```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`:
```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`:
```tsx
"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`:
```ts
export const sse = sseEndpointMap({
...existing,
myFeature: {
update: z.object({ type: z.enum(["action1", "action2"]) }),
},
});
```
Publish in your server action:
```ts
sse.myFeature.pub("update", { type: "action1" });
```
Subscribe on the client:
```ts
useEffect(() => {
const { clean } = sse.myFeature.sub("update", (data) => {
// handle update
});
return clean;
}, []);
```
For dynamic topics (e.g. per-tierlist), use `tlSse(listId)`:
```ts
// 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`:
```tsx
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`:
```tsx
<SidebarMenuItem>
<SidebarLink href="/admin/<feature>">
<IconComponent />
Feature Name
</SidebarLink>
</SidebarMenuItem>
```
### 4. List/manage page
```tsx
// 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`):
```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
```ts
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`:
```ts
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:
```tsx
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`:
```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.
```tsx
<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.