3.2 KiB
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
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 })
pathis appended to the meditation base (/apialready implied — pass e.g."/media").body: pass a plain object for JSON, or a FormData for multipart. Build multipart withtoFormData({...})(handles File, arrays askey[], 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 viaPOST /resource/:id(multipart). - Delete:
DELETE /resource/:id. - Lists:
GET /resource.