add claude skills for database, development, and features
Build and Push Image / docker (push) Successful in 7m6s
Build and Push Image / docker (push) Successful in 7m6s
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
---
|
||||
name: buzz-database
|
||||
description: Drizzle ORM workflows for the Buzz PostgreSQL database
|
||||
metadata:
|
||||
author: dmgnr
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Buzz — Database Workflows (Drizzle ORM)
|
||||
|
||||
This skill covers working with the PostgreSQL database via Drizzle ORM.
|
||||
|
||||
## Schema Overview
|
||||
|
||||
All tables are defined in `lib/db/schema.ts`. The database uses **three named schemas** + the public schema:
|
||||
|
||||
### Public schema
|
||||
|
||||
| Table | Purpose |
|
||||
|---|---|
|
||||
| `cdn` | File storage (bytea), used for images, cards, slips |
|
||||
| `auditLog` | Admin action audit trail |
|
||||
| `characters` | Genshin characters (linked to versions, CDN images) |
|
||||
| `versions` | Game version tracking (self-referencing FK for `from`) |
|
||||
| `settings` | Global app settings (singleton row, PK is boolean `true`) |
|
||||
| `guides` | Character build guide links |
|
||||
| `user`, `session`, `account`, `verification` | Better-auth tables |
|
||||
|
||||
### `artifact` schema
|
||||
|
||||
| Table | Purpose |
|
||||
|---|---|
|
||||
| `submissions` | Artifact review queue submissions |
|
||||
| `cards` | Generated character cards for submissions |
|
||||
| `settings` | Artifact-specific settings (locked, limit) |
|
||||
|
||||
### `endgame` schema
|
||||
|
||||
| Table | Purpose |
|
||||
|---|---|
|
||||
| `submissions` | Rubgram (endgame) queue submissions |
|
||||
| `sarchive` | Archived completed submissions |
|
||||
| `expired` | Expired unpaid submissions |
|
||||
| `slips` | SlipOK payment slip images and data |
|
||||
| `settings` | Rubgram-specific settings |
|
||||
| `discord` | Discord user mapping |
|
||||
| `types` | Service types with pricing |
|
||||
|
||||
### `tierlist` schema
|
||||
|
||||
| Table | Purpose |
|
||||
|---|---|
|
||||
| `types` | Tierlist categories (Spiral Abyss, Stygian Onslaught) |
|
||||
| `tiers` | Tier definitions (S, A, B, etc.) |
|
||||
| `columns` | Column layout definitions |
|
||||
| `badges` | Badge icons for tiers |
|
||||
| `versions` | Tierlist versions per type |
|
||||
| `states` | Character placements per version (JSONB badges) |
|
||||
|
||||
### Key schema patterns
|
||||
|
||||
**UUIDv7 primary keys:**
|
||||
```ts
|
||||
import { uuidv7 } from "uuidv7";
|
||||
|
||||
id: uuid("id").primaryKey().$defaultFn(uuidv7),
|
||||
```
|
||||
|
||||
**Named schemas with `pgSchema`:**
|
||||
```ts
|
||||
import { pgSchema } from "drizzle-orm/pg-core";
|
||||
|
||||
const artifact = pgSchema("artifact");
|
||||
export const submissions = artifact.table("submissions", { ... });
|
||||
```
|
||||
|
||||
**Custom `bytea` column** (for binary data like images):
|
||||
```ts
|
||||
import { bytea } from "$/db/custom";
|
||||
// used in: cdn, cards, slips tables
|
||||
```
|
||||
|
||||
**Generated columns:**
|
||||
```ts
|
||||
paid: boolean().generatedAlwaysAs(
|
||||
sql`(price <= (0)::numeric) OR (slip IS NOT NULL)`
|
||||
),
|
||||
```
|
||||
|
||||
**Serial sequences** for queue numbering per schema:
|
||||
```ts
|
||||
queue: integer().notNull().default(sql`(nextval('artifact.submissions_queue_seq'::regclass))`),
|
||||
```
|
||||
|
||||
**Enums:**
|
||||
```ts
|
||||
export const characterElement = pgEnum("character_element", [
|
||||
"anemo", "geo", "dendro", "hydro", "pyro", "cryo", "electro",
|
||||
]);
|
||||
```
|
||||
|
||||
**Foreign keys with cascade:**
|
||||
```ts
|
||||
char: uuid("char").notNull().references(() => characters.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
```
|
||||
|
||||
## Migration Workflow
|
||||
|
||||
### Important: database boundary
|
||||
|
||||
Schema changes must be pushed to the database **before** the new code deploys. Otherwise the running app will crash on startup when it queries tables or columns that don't exist yet.
|
||||
|
||||
| Environment | Command | Timing |
|
||||
|---|---|---|
|
||||
| **Dev** | `bun ds dr push` | Run inside app container (via `bun ds`) against dev DB |
|
||||
| **Production** | `bun dr push` | Run locally with production env vars against prod DB. **Must run before `git push`.** |
|
||||
|
||||
### Full migration workflow
|
||||
|
||||
1. **Edit schema** in `lib/db/schema.ts`
|
||||
2. **Generate migration files** (optional, for tracking):
|
||||
```bash
|
||||
bun dr generate
|
||||
```
|
||||
3. **Push to dev** to verify:
|
||||
```bash
|
||||
bun ds dr push
|
||||
```
|
||||
4. **If using migrate instead of push** for production:
|
||||
```bash
|
||||
bun dr migrate
|
||||
```
|
||||
5. **Push to production DB** (with production credentials):
|
||||
```bash
|
||||
bun dr push
|
||||
```
|
||||
6. **Deploy code** via `git push` (Gitea Actions handles the rest)
|
||||
|
||||
When using `bun dr push`, Drizzle automatically detects differences between the schema and the database, and applies the necessary ALTER statements. For production, always verify the SQL Drizzle generates first.
|
||||
|
||||
### Drizzle Studio
|
||||
|
||||
Accessible at `http://localhost:4983` when dev containers are running. The `drizzle` service in `dev/compose.yml` provides this:
|
||||
|
||||
```bash
|
||||
bun ds bun dr studio --host $(hostname)
|
||||
```
|
||||
|
||||
## Common Schema Operations
|
||||
|
||||
### Adding a column
|
||||
|
||||
```ts
|
||||
// In the table definition, add:
|
||||
newColumn: varchar("new_column", { length: 255 }).default(""),
|
||||
```
|
||||
|
||||
Generate + push.
|
||||
|
||||
### Adding a new table
|
||||
|
||||
```ts
|
||||
export const myTable = pgSchema("myschema").table("my_table", {
|
||||
id: uuid("id").primaryKey().$defaultFn(uuidv7),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
Export and use in queries. Generate + push.
|
||||
|
||||
### Adding a new enum
|
||||
|
||||
```ts
|
||||
export const myEnum = pgEnum("my_enum", ["value1", "value2"]);
|
||||
|
||||
export const myTable = pgSchema("myschema").table("my_table", {
|
||||
status: myEnum("status").default("value1"),
|
||||
});
|
||||
```
|
||||
|
||||
### Adding foreign key
|
||||
|
||||
```ts
|
||||
refId: uuid("ref_id")
|
||||
.notNull()
|
||||
.references(() => otherTable.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
```
|
||||
|
||||
## Query patterns
|
||||
|
||||
### Basic select
|
||||
```ts
|
||||
import { db } from "$/db";
|
||||
import { submissions } from "$/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
const items = await db.select().from(submissions).where(eq(submissions.checked, false));
|
||||
```
|
||||
|
||||
### Select with join
|
||||
```ts
|
||||
const result = await db
|
||||
.select({
|
||||
name: characters.name,
|
||||
queue: submissions.queue,
|
||||
})
|
||||
.from(submissions)
|
||||
.innerJoin(characters, eq(submissions.char, characters.id))
|
||||
.orderBy(submissions.queue);
|
||||
```
|
||||
|
||||
### Insert
|
||||
```ts
|
||||
const [inserted] = await db
|
||||
.insert(submissions)
|
||||
.values({ name: "Test", uid: "123456789", char: charId })
|
||||
.returning({ id: submissions.id, queue: submissions.queue });
|
||||
```
|
||||
|
||||
### Update
|
||||
```ts
|
||||
await db
|
||||
.update(submissions)
|
||||
.set({ checked: true })
|
||||
.where(eq(submissions.id, id));
|
||||
```
|
||||
|
||||
### Delete with cascade check (CDN)
|
||||
```ts
|
||||
// Use checkCdnRefs() from $api before deleting CDN files
|
||||
```
|
||||
|
||||
### Raw SQL
|
||||
```ts
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
await db.execute(sql`ALTER SEQUENCE artifact.submissions_queue_seq RESTART WITH 1`);
|
||||
```
|
||||
|
||||
### Transactions
|
||||
```ts
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(...);
|
||||
await tx.update(...);
|
||||
});
|
||||
```
|
||||
|
||||
### Counting
|
||||
```ts
|
||||
const count = await db.$count(submissions);
|
||||
```
|
||||
@@ -0,0 +1,167 @@
|
||||
---
|
||||
name: buzz-development
|
||||
description: Buzz development environment setup, maintenance, and BrowserMCP
|
||||
metadata:
|
||||
author: dmgnr
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Buzz — Development Environment & Maintenance
|
||||
|
||||
This skill covers the local dev environment, common maintenance tasks, and BrowserMCP setup.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Bun** (matching the project's Bun version in Docker)
|
||||
- **Docker** with Compose Watch support
|
||||
- Environment file at `dev/.env.development`
|
||||
|
||||
## Dev Environment
|
||||
|
||||
### Starting development
|
||||
|
||||
```bash
|
||||
bun dev
|
||||
```
|
||||
|
||||
This runs `docker compose up --watch` from `dev/compose.yml`, spinning up:
|
||||
|
||||
| Container | Purpose | Notes |
|
||||
|---|---|---|
|
||||
| `app` | Next.js dev server (port 3000) | File sync via Docker watch, autoreloads |
|
||||
| `backend` | Background worker (card caching, cron, webhooks) | Rebuilds on `backend/` changes |
|
||||
| `db` | PostgreSQL (port 5432) | Auto-creates on first run |
|
||||
| `redis` | Redis (port 6379) | Pub-sub for SSE, persistence enabled |
|
||||
| `redis-commander` | Redis GUI (port 6380) | Optional, for debugging |
|
||||
| `drizzle` | Drizzle Studio (port 4983) | DB management UI |
|
||||
|
||||
The `bun dev` command runs **foreground only**. Press Ctrl+C to stop all containers.
|
||||
|
||||
### Attaching to the app container
|
||||
|
||||
```bash
|
||||
bun ds
|
||||
```
|
||||
|
||||
Short for "docker exec -it app bun". Run one-off commands inside the app container:
|
||||
|
||||
```bash
|
||||
bun ds bun nextdev # start next dev manually
|
||||
bun ds bun dr push # push schema to dev database
|
||||
bun ds bun dr studio # start drizzle studio
|
||||
bun ds bun lint # lint inside container
|
||||
```
|
||||
|
||||
### Default dev credentials
|
||||
|
||||
Seeded automatically by the backend on first startup when `ENVIRONMENT=development`:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Email | `[email protected]` |
|
||||
| Password | `youshallnotpass` |
|
||||
| Role | `admin` |
|
||||
|
||||
Seeding is idempotent — it skips if an admin account already exists.
|
||||
|
||||
## Command Reference
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `bun dev` | Start all dev containers with file watch |
|
||||
| `bun ds <cmd>` | Run a command in the app container |
|
||||
| `bun nextdev` | Start Next.js dev server directly (outside Docker) |
|
||||
| `bun build` | Build for production (Turbopack) |
|
||||
| `bun start` | Start production server |
|
||||
| `bun lint` | Run Biome check with auto-fix |
|
||||
| `bun format` | Run Biome formatter |
|
||||
| `bun dr <args>` | Run drizzle-kit commands |
|
||||
| `bun logs` | Tail logs from all containers (`docker compose logs -fn50`) |
|
||||
| `bun test` | Run Playwright tests (outdated — unreliable) |
|
||||
|
||||
## Common Maintenance Tasks
|
||||
|
||||
### Checking logs
|
||||
|
||||
```bash
|
||||
bun logs # all services
|
||||
docker compose -f dev/compose.yml logs -fn50 app # just the app
|
||||
docker compose -f dev/compose.yml logs -fn50 backend
|
||||
```
|
||||
|
||||
### Restarting a service
|
||||
|
||||
```bash
|
||||
docker compose -f dev/compose.yml restart app
|
||||
docker compose -f dev/compose.yml restart backend
|
||||
```
|
||||
|
||||
### Applying linting and formatting
|
||||
|
||||
```bash
|
||||
bun lint # biome check --fix
|
||||
bun format # biome format --write
|
||||
```
|
||||
|
||||
### Running Drizzle Studio
|
||||
|
||||
```bash
|
||||
bun ds bun dr studio --host $(hostname) # via container
|
||||
```
|
||||
|
||||
Then visit `http://localhost:4983`.
|
||||
|
||||
### Database migrations
|
||||
|
||||
See the **buzz-database** skill for the full workflow.
|
||||
|
||||
### Updating dependencies
|
||||
|
||||
```bash
|
||||
bun ds bun install <package>
|
||||
```
|
||||
|
||||
After adding new deps, you may need to rebuild the Docker images.
|
||||
|
||||
## BrowserMCP Setup
|
||||
|
||||
Before doing any UI/interaction work, you **must** set up BrowserMCP for automated browser testing.
|
||||
|
||||
### Flow
|
||||
|
||||
1. The agent will ask you: *"Do you want to set up BrowserMCP for automated browser testing?"*
|
||||
2. If you agree, the agent will:
|
||||
|
||||
a. Install the BrowserMCP package:
|
||||
```bash
|
||||
bun add -d @anthropic/browser-use
|
||||
```
|
||||
|
||||
b. Add the MCP server configuration to `opencode.json` (or create/edit `.opencode.jsonc`):
|
||||
```jsonc
|
||||
{
|
||||
"mcpServers": {
|
||||
"browser": {
|
||||
"command": "bunx",
|
||||
"args": ["@anthropic/browser-use"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
c. Launch the browser and connect to `http://localhost:3000`
|
||||
|
||||
3. The agent will then perform browser-based testing/verification using the MCP browser tools.
|
||||
|
||||
### When to use BrowserMCP
|
||||
|
||||
- Verifying UI changes after implementing a feature
|
||||
- Testing form submissions, login flows, or admin interactions
|
||||
- Checking that SSE live updates work correctly
|
||||
- Validating responsive layout and navigation
|
||||
|
||||
### Important
|
||||
|
||||
- **Do not** run Playwright tests — they are heavily outdated and unreliable.
|
||||
- Always use BrowserMCP for visual/interaction verification instead.
|
||||
- The dev server must already be running for BrowserMCP to connect.
|
||||
@@ -0,0 +1,325 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user