Files
2026-06-03 03:08:57 +03:30

66 lines
3.2 KiB
Markdown

# Feature page conventions (read before writing any page)
This is a **static-export** Next.js 16 SPA. Every page is a Client Component
(`"use client"` at the top). All data fetching happens in the browser via the
helpers below. Persian (Farsi), RTL. Look at the reference page
`app/dashboard/media/categories/page.tsx` and copy its structure.
## Imports & contracts
```ts
import { apiFetch, ApiError, toFormData, unwrap } from "@/lib/api";
import { useList, useItem } from "@/lib/useResource";
import { useToast } from "@/components/toast";
import { DataTable, type Column } from "@/components/DataTable";
import {
Button, ConfirmDialog, Field, Input, Textarea, Select, Switch,
Modal, PageHeader, Card, Badge, EmptyState,
} from "@/components/ui";
import { EditIcon, PlusIcon, TrashIcon } from "@/components/icons";
import { toFa, formatDuration } from "@/lib/utils";
```
### `apiFetch<T>(path, { method, body, query, baseUrl, auth })`
- `path` is appended to the meditation base (`/api` already implied — pass e.g. `"/media"`).
- `body`: pass a **plain object** for JSON, or a **FormData** for multipart. Build multipart with `toFormData({...})` (handles File, arrays as `key[]`, booleans as 1/0, skips empty).
- Bearer token is attached automatically. Throws `ApiError` (has `.message`, `.status`).
### `useList<T>(path, query?)` → `{ data: T[], loading, error, reload }`
Use for GET list endpoints. Already unwraps `{ data: [...] }`.
### `useItem<T>(path)` → `{ data, loading, error, reload }`
Use for a single GET record (e.g. settings).
### `DataTable<T>` props: `columns`, `rows`, `loading`, `error`, `emptyMessage?`, `actions?(row)`
`Column<T> = { key, header, render?(row), className? }`.
### Form components
- `<Field label required hint>{children}</Field>` wraps an input with a label.
- `<Input/>`, `<Textarea/>`, `<Select/>` are styled native elements (pass value/onChange).
- `<Switch checked onChange={(v)=>...} label/>` for booleans.
- `<Modal open onClose title footer>` — put the form inside; trigger submit via a
`<Button form="my-form" type="submit" loading={saving}>` in the footer and give the
`<form id="my-form" onSubmit={save}>`.
- `<ConfirmDialog open message loading onConfirm onClose/>` for deletes.
- `<Button variant="primary|secondary|danger|ghost" loading icon>`.
### Helpers
- `toFa(value)` → Persian digits for display (use for ids/numbers/durations in tables).
- `formatDuration(seconds)``م:ث`.
## Page skeleton
Every CRUD page: `PageHeader` (title + subtitle + "new" Button) → `DataTable` with
edit/delete `actions` → a `Modal` create/edit form → a `ConfirmDialog` for delete.
On success call `reload()` and `toast.success(...)`; on error `toast.error(err instanceof ApiError ? err.message : "...")`.
Use real Persian labels everywhere. Keep numeric inputs `type="number"`, file inputs
`type="file"` (read `e.target.files?.[0]`). For ltr-ish fields (urls, ids) add `dir="ltr"`.
## REST conventions in this API (important quirks)
- **Create**: usually `POST /resource` (multipart unless noted JSON).
- **Update**: RESTful resources use `PUT /resource/:id` (JSON). BUT file-bearing
resources (**scenes, bell-sounds, background-sounds**) update via `POST /resource/:id` (multipart).
- **Delete**: `DELETE /resource/:id`.
- Lists: `GET /resource`.