--- name: admin-crud description: Generate admin dashboard pages with data tables, filters, bulk actions, dialogs, and forms. Use when building admin interfaces, management pages, or dashboard components. --- # Admin CRUD Generator Create admin dashboard pages following this project's established patterns. ## Admin Page Structure ``` src/ ├── routes/admin/ │ └── resources/ │ ├── index.tsx # List page │ └── $resourceId.tsx # Detail/edit page └── components/admin/ └── resources/ ├── ResourcesList.tsx # List container ├── ResourceForm.tsx # Create/edit form └── components/ ├── ResourceTable.tsx # Data table ├── StatusBadge.tsx # Status indicator ├── BulkActionsBar.tsx # Bulk operations └── ResourceActions.tsx # Row actions ``` ## List Page Template ```typescript // src/routes/admin/resources/index.tsx import { createFileRoute } from '@tanstack/react-router' import { ResourcesList } from '@/components/admin/resources/ResourcesList' export const Route = createFileRoute('/admin/resources/')({ component: ResourcesPage, }) function ResourcesPage() { return } ``` ## List Component with Data Table ```typescript // src/components/admin/resources/ResourcesList.tsx import { useQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { Plus } from 'lucide-react' import { Link } from '@tanstack/react-router' import { Button } from '@/components/ui/button' import { ResourceTable } from './components/ResourceTable' export function ResourcesList() { const { t } = useTranslation() const { data, isLoading, error } = useQuery({ queryKey: ['resources'], queryFn: async () => { const res = await fetch('/api/resources', { credentials: 'include' }) const json = await res.json() if (!json.success) throw new Error(json.error) return json }, }) if (isLoading) { return (
) } if (error) { return (

{t('Failed to load')}

) } const { items, total } = data if (items.length === 0) { return (

{t('No resources')}

{t('Get started by creating a new resource.')}

) } return (

{t('Resources')}

{t('{{count}} total', { count: total })}
) } ``` ## Data Table Component ```typescript // src/components/admin/resources/components/ResourceTable.tsx import { useState } from 'react' import { Link } from '@tanstack/react-router' import { MoreHorizontal, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react' import { useTranslation } from 'react-i18next' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { StatusBadge } from './StatusBadge' interface Resource { id: string name: { en: string } status: 'active' | 'draft' | 'archived' createdAt: string } interface Props { resources: Resource[] } export function ResourceTable({ resources }: Props) { const { t } = useTranslation() const [selectedIds, setSelectedIds] = useState>(new Set()) const [sortKey, setSortKey] = useState('createdAt') const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc') const toggleSelect = (id: string) => { const next = new Set(selectedIds) if (next.has(id)) { next.delete(id) } else { next.add(id) } setSelectedIds(next) } const toggleSelectAll = () => { if (selectedIds.size === resources.length) { setSelectedIds(new Set()) } else { setSelectedIds(new Set(resources.map((r) => r.id))) } } const isAllSelected = selectedIds.size === resources.length const isSomeSelected = selectedIds.size > 0 && selectedIds.size < resources.length const handleSort = (key: string) => { if (sortKey === key) { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc') } else { setSortKey(key) setSortOrder('desc') } } const SortIcon = ({ columnKey }: { columnKey: string }) => { if (sortKey !== columnKey) return return sortOrder === 'asc' ? : } return ( <> {selectedIds.size > 0 && ( setSelectedIds(new Set())} /> )}
{resources.map((resource) => ( ))}
{t('Status')}
toggleSelect(resource.id)} /> {resource.name.en} {new Date(resource.createdAt).toLocaleDateString()} {t('Edit')} {t('Delete')}
) } ``` ## Status Badge ```typescript // src/components/admin/resources/components/StatusBadge.tsx import { cn } from '@/lib/utils' const statusStyles = { active: 'bg-emerald-500/10 text-emerald-500', draft: 'bg-amber-500/10 text-amber-500', archived: 'bg-muted text-muted-foreground', pending: 'bg-blue-500/10 text-blue-500', processing: 'bg-purple-500/10 text-purple-500', shipped: 'bg-cyan-500/10 text-cyan-500', delivered: 'bg-emerald-500/10 text-emerald-500', cancelled: 'bg-red-500/10 text-red-500', paid: 'bg-emerald-500/10 text-emerald-500', failed: 'bg-red-500/10 text-red-500', refunded: 'bg-amber-500/10 text-amber-500', } interface Props { status: keyof typeof statusStyles } export function StatusBadge({ status }: Props) { return ( {status} ) } ``` ## Bulk Actions Bar ```typescript // src/components/admin/resources/components/BulkActionsBar.tsx import { useMutation, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { Button } from '@/components/ui/button' interface Props { selectedCount: number selectedIds: string[] onClearSelection: () => void } export function BulkActionsBar({ selectedCount, selectedIds, onClearSelection }: Props) { const { t } = useTranslation() const queryClient = useQueryClient() const bulkUpdate = useMutation({ mutationFn: async (action: 'activate' | 'archive' | 'delete') => { const res = await fetch('/api/resources/bulk', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ ids: selectedIds, action }), }) const json = await res.json() if (!json.success) throw new Error(json.error) return json }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['resources'] }) onClearSelection() toast.success(t('Updated successfully')) }, onError: (error) => { toast.error(error.message) }, }) return (
{t('{{count}} selected', { count: selectedCount })}
) } ``` ## Card-Based Form Layout ```typescript // Product form pattern with gradient accent cards
{/* Main content - 2 columns */}
{/* Details Card */}
{t('Details')} {/* Form fields */} {/* Media Card */}
{t('Media')} {/* Image uploader */}
{/* Sidebar - 1 column, sticky */}
{/* Status Card */}
{t('Status')} {/* Status select */}
``` ## Gradient Accent Colors | Section | Gradient | | -------- | -------------------------------- | | Details | `from-pink-500 to-purple-500` | | Media | `from-violet-500 to-fuchsia-500` | | Options | `from-blue-500 to-cyan-500` | | Variants | `from-emerald-500 to-teal-500` | | SEO | `from-amber-500 to-orange-500` | ## Confirmation Dialog ```typescript import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' function DeleteConfirmDialog({ open, onOpenChange, onConfirm, resourceName }) { const { t } = useTranslation() return ( {t('Delete Resource')} {t('Are you sure you want to delete "{{name}}"? This action cannot be undone.', { name: resourceName, })} {t('Cancel')} {t('Delete')} ) } ``` ## See Also - `src/components/admin/products/` - Full product CRUD example - `src/components/admin/orders/` - Order management - `src/hooks/useDataTable.ts` - Table state management - `forms` skill - Form patterns with FNForm