--- name: add-admin-page description: Add a new admin React page to the frontend. Use when the user wants a new admin panel page — list views, detail pages, management UIs. Triggers on "add a page for X", "create an admin page", "I need a UI for managing Y", "build the frontend for Z". --- ## Files to create/modify | Action | Path | |--------|------| | Create | `assets/admin/pages/{Name}Page.tsx` | | Edit | `assets/admin/types/index.ts` — add the TypeScript interface | | Edit | `assets/admin/App.tsx` — add the route | ## Step 1 — Add the TypeScript type Add to `assets/admin/types/index.ts`: ```ts export interface SomeName { uuid: string; // ... fields matching the backend array result } ``` ## Step 2 — Register the route In `assets/admin/App.tsx`, add inside the `` routes block: ```tsx import SomeNamePage from './pages/SomeNamePage'; // ... } /> } /> {/* if detail page needed */} ``` ## Step 3 — Create the page Standard list page pattern: ```tsx import React, { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { MagnifyingGlassIcon, PlusIcon, EyeIcon, TrashIcon, ArrowPathIcon, CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse, PaginatedResponse } from '../lib/api'; import { formatDate } from '../lib/utils'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import Pagination from '../components/ui/Pagination'; import type { SomeName } from '../types'; export default function SomeNamePage() { const navigate = useNavigate(); const qc = useQueryClient(); const [page, setPage] = useState(1); const [limit] = useState(15); const [searchInput, setSearchInput] = useState(''); const [search, setSearch] = useState(''); const [deleteTarget, setDeleteTarget] = useState(null); // Debounced search — always 350 ms useEffect(() => { const t = setTimeout(() => { setSearch(searchInput); setPage(1); }, 350); return () => clearTimeout(t); }, [searchInput]); // List query const listQ = useQuery({ queryKey: ['admin-some-names', page, limit, search], queryFn: () => { const p = new URLSearchParams({ page: String(page), limit: String(limit) }); if (search) p.set('search', search); return api.get>(`/api/v1/admin/some-names?${p}`); }, }); const items = listQ.data?.data ?? []; const total = listQ.data?.meta?.totalRecords ?? 0; // Stats query (if stats endpoint exists) const statsQ = useQuery({ queryKey: ['admin-some-names-stats'], queryFn: () => api.get>('/api/v1/admin/some-names/stats'), staleTime: 30_000, }); // IMPORTANT: stats data may be double-nested depending on backend shape. // Use this pattern to handle both cases: const stats = (statsQ.data?.data as any)?.data ?? statsQ.data?.data; // Delete mutation const deleteMut = useMutation({ mutationFn: (uuid: string) => api.delete>(`/api/v1/some-names/${uuid}`), onSuccess: () => { toast.success('حذف شد'); setDeleteTarget(null); qc.invalidateQueries({ queryKey: ['admin-some-names'] }); }, onError: (e: Error) => toast.error(e.message), }); // Toggle status mutation const toggleMut = useMutation({ mutationFn: (uuid: string) => api.post>(`/api/v1/admin/some-names/${uuid}/status`, {}), onSuccess: () => { toast.success('وضعیت تغییر کرد'); qc.invalidateQueries({ queryKey: ['admin-some-names'] }); }, onError: (e: Error) => toast.error(e.message), }); return (
{/* Header */}

عنوان صفحه

توضیح کوتاه
{/* KPI cards — only if stats endpoint exists */} {/*
...
*/} {/* Main card */}
{/* Toolbar */}
setSearchInput(e.target.value)} placeholder="جستجو..." />
{/* Table */}
{listQ.isLoading && Array.from({ length: 5 }).map((_, i) => ( {Array.from({ length: 4 }).map((_, j) => ( ))} ))} {!listQ.isLoading && items.length === 0 && ( )} {!listQ.isLoading && items.map((item) => ( navigate(`/admin/some-names/${item.uuid}`)}> ))}
نام وضعیت تاریخ
موردی یافت نشد
{/* render fields */} {(item as any).is_active ? 'فعال' : 'غیرفعال'} {formatDate((item as any).created_at)} e.stopPropagation()}>
deleteTarget && deleteMut.mutate(deleteTarget.uuid)} onCancel={() => setDeleteTarget(null)} />
); } ``` ## Critical rules - **Never use `data?.data?.data`** unless the endpoint is a `$this->success(['data' => ...])` double-nest. Standard `$this->success($array)` → extract with `data?.data`. `$this->paginated()` → items at `data?.data`, total at `data?.meta?.totalRecords`. - Stats from `$this->success($stats)` may still be double-nested in older endpoints — use `(statsQ.data?.data as any)?.data ?? statsQ.data?.data` to handle both. - Category API (`/api/v1/categorys/{bundle}`) is always triple-nested: extract with `data?.data?.data ?? []`. - Search debounce is always 350ms via `setTimeout` in a `useEffect`. - Query keys follow the format `['admin-entity-name', page, limit, search, ...filters]`. - After creating the page, run `ddev exec yarn dev` to check for TypeScript errors.