---
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 (
)
}
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())}
/>
)}
|
|
|
{t('Status')} |
|
|
{resources.map((resource) => (
|
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