admin-crud

Generate admin dashboard pages with data tables, filters, bulk actions, dialogs, and forms. Use when building admin interfaces, management pages, or dashboard components.

181 stars

Best use case

admin-crud is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Generate admin dashboard pages with data tables, filters, bulk actions, dialogs, and forms. Use when building admin interfaces, management pages, or dashboard components.

Teams using admin-crud should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/admin-crud/SKILL.md --create-dirs "https://raw.githubusercontent.com/majiayu000/claude-skill-registry/main/skills/data/admin-crud/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/admin-crud/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How admin-crud Compares

Feature / Agentadmin-crudStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Generate admin dashboard pages with data tables, filters, bulk actions, dialogs, and forms. Use when building admin interfaces, management pages, or dashboard components.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# 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 <ResourcesList />
}
```

## 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 (
      <div className="flex items-center justify-center h-64">
        <div
          className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-pink-500"
          role="status"
          aria-label="Loading"
        />
      </div>
    )
  }

  if (error) {
    return (
      <div className="text-center py-12">
        <p className="text-red-500">{t('Failed to load')}</p>
      </div>
    )
  }

  const { items, total } = data

  if (items.length === 0) {
    return (
      <div className="text-center py-12">
        <Package className="mx-auto h-12 w-12 text-muted-foreground" />
        <h3 className="mt-2 text-sm font-semibold">{t('No resources')}</h3>
        <p className="mt-1 text-sm text-muted-foreground">
          {t('Get started by creating a new resource.')}
        </p>
        <div className="mt-6">
          <Button asChild>
            <Link to="/admin/resources/new">
              <Plus className="mr-2 h-4 w-4" />
              {t('Add Resource')}
            </Link>
          </Button>
        </div>
      </div>
    )
  }

  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between">
        <h1 className="text-2xl font-bold">{t('Resources')}</h1>
        <Button asChild>
          <Link to="/admin/resources/new">
            <Plus className="mr-2 h-4 w-4" />
            {t('Add Resource')}
          </Link>
        </Button>
      </div>

      <ResourceTable resources={items} />

      <div className="text-sm text-muted-foreground">
        {t('{{count}} total', { count: total })}
      </div>
    </div>
  )
}
```

## 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<Set<string>>(new Set())
  const [sortKey, setSortKey] = useState<string>('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 <ArrowUpDown className="ml-2 h-4 w-4" />
    return sortOrder === 'asc'
      ? <ArrowUp className="ml-2 h-4 w-4" />
      : <ArrowDown className="ml-2 h-4 w-4" />
  }

  return (
    <>
      {selectedIds.size > 0 && (
        <BulkActionsBar
          selectedCount={selectedIds.size}
          onClearSelection={() => setSelectedIds(new Set())}
        />
      )}

      <div className="rounded-md border">
        <table className="w-full">
          <thead>
            <tr className="border-b bg-muted/50">
              <th className="w-12 p-4">
                <Checkbox
                  checked={isAllSelected}
                  indeterminate={isSomeSelected}
                  onCheckedChange={toggleSelectAll}
                />
              </th>
              <th className="p-4 text-left">
                <button
                  className="flex items-center font-medium"
                  onClick={() => handleSort('name')}
                >
                  {t('Name')}
                  <SortIcon columnKey="name" />
                </button>
              </th>
              <th className="p-4 text-left">{t('Status')}</th>
              <th className="p-4 text-left">
                <button
                  className="flex items-center font-medium"
                  onClick={() => handleSort('createdAt')}
                >
                  {t('Created')}
                  <SortIcon columnKey="createdAt" />
                </button>
              </th>
              <th className="w-12 p-4"></th>
            </tr>
          </thead>
          <tbody>
            {resources.map((resource) => (
              <tr
                key={resource.id}
                className={`border-b hover:bg-muted/50 group ${
                  selectedIds.has(resource.id) ? 'bg-pink-500/5' : ''
                }`}
              >
                <td className="p-4">
                  <Checkbox
                    checked={selectedIds.has(resource.id)}
                    onCheckedChange={() => toggleSelect(resource.id)}
                  />
                </td>
                <td className="p-4">
                  <Link
                    to="/admin/resources/$resourceId"
                    params={{ resourceId: resource.id }}
                    className="font-medium hover:underline"
                  >
                    {resource.name.en}
                  </Link>
                </td>
                <td className="p-4">
                  <StatusBadge status={resource.status} />
                </td>
                <td className="p-4 text-muted-foreground">
                  {new Date(resource.createdAt).toLocaleDateString()}
                </td>
                <td className="p-4">
                  <DropdownMenu>
                    <DropdownMenuTrigger asChild>
                      <Button variant="ghost" size="icon">
                        <MoreHorizontal className="h-4 w-4" />
                      </Button>
                    </DropdownMenuTrigger>
                    <DropdownMenuContent align="end">
                      <DropdownMenuItem asChild>
                        <Link
                          to="/admin/resources/$resourceId"
                          params={{ resourceId: resource.id }}
                        >
                          {t('Edit')}
                        </Link>
                      </DropdownMenuItem>
                      <DropdownMenuItem className="text-destructive">
                        {t('Delete')}
                      </DropdownMenuItem>
                    </DropdownMenuContent>
                  </DropdownMenu>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </>
  )
}
```

## 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 (
    <span
      className={cn(
        'inline-flex items-center gap-1.5 rounded-full px-2 py-1 text-xs font-medium uppercase',
        statusStyles[status] || statusStyles.draft
      )}
    >
      <span className="h-1.5 w-1.5 rounded-full bg-current" />
      {status}
    </span>
  )
}
```

## 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 (
    <div className="flex items-center gap-4 rounded-lg border bg-muted/50 p-4">
      <span className="text-sm font-medium">
        {t('{{count}} selected', { count: selectedCount })}
      </span>
      <div className="flex gap-2">
        <Button
          size="sm"
          variant="outline"
          onClick={() => bulkUpdate.mutate('activate')}
          disabled={bulkUpdate.isPending}
        >
          {t('Activate')}
        </Button>
        <Button
          size="sm"
          variant="outline"
          onClick={() => bulkUpdate.mutate('archive')}
          disabled={bulkUpdate.isPending}
        >
          {t('Archive')}
        </Button>
        <Button
          size="sm"
          variant="destructive"
          onClick={() => bulkUpdate.mutate('delete')}
          disabled={bulkUpdate.isPending}
        >
          {t('Delete')}
        </Button>
      </div>
      <Button
        size="sm"
        variant="ghost"
        onClick={onClearSelection}
        className="ml-auto"
      >
        {t('Clear selection')}
      </Button>
    </div>
  )
}
```

## Card-Based Form Layout

```typescript
// Product form pattern with gradient accent cards
<div className="grid lg:grid-cols-3 gap-6">
  {/* Main content - 2 columns */}
  <div className="lg:col-span-2 space-y-6">
    {/* Details Card */}
    <Card className="border-border/50 shadow-xl shadow-foreground/5 bg-card/50 backdrop-blur-sm overflow-hidden">
      <div className="h-1 bg-gradient-to-r from-pink-500 to-purple-500" />
      <CardHeader>
        <CardTitle>{t('Details')}</CardTitle>
      </CardHeader>
      <CardContent>
        {/* Form fields */}
      </CardContent>
    </Card>

    {/* Media Card */}
    <Card className="overflow-hidden">
      <div className="h-1 bg-gradient-to-r from-violet-500 to-fuchsia-500" />
      <CardHeader>
        <CardTitle>{t('Media')}</CardTitle>
      </CardHeader>
      <CardContent>
        {/* Image uploader */}
      </CardContent>
    </Card>
  </div>

  {/* Sidebar - 1 column, sticky */}
  <div className="space-y-6">
    <div className="lg:sticky lg:top-4">
      {/* Status Card */}
      <Card>
        <div className="h-1 bg-gradient-to-r from-emerald-500 to-teal-500" />
        <CardHeader>
          <CardTitle>{t('Status')}</CardTitle>
        </CardHeader>
        <CardContent>
          {/* Status select */}
        </CardContent>
      </Card>
    </div>
  </div>
</div>
```

## 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 (
    <AlertDialog open={open} onOpenChange={onOpenChange}>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>{t('Delete Resource')}</AlertDialogTitle>
          <AlertDialogDescription>
            {t('Are you sure you want to delete "{{name}}"? This action cannot be undone.', {
              name: resourceName,
            })}
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>{t('Cancel')}</AlertDialogCancel>
          <AlertDialogAction
            onClick={onConfirm}
            className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
          >
            {t('Delete')}
          </AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}
```

## 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

Related Skills

administration

181
from majiayu000/claude-skill-registry

How to monitor usage, track costs, configure analytics, and measure ROI for Claude Code. Use when user asks about monitoring, telemetry, metrics, costs, analytics, or OpenTelemetry.

administering-linux

181
from majiayu000/claude-skill-registry

Manage Linux systems covering systemd services, process management, filesystems, networking, performance tuning, and troubleshooting. Use when deploying applications, optimizing server performance, diagnosing production issues, or managing users and security on Linux servers.

admin

181
from majiayu000/claude-skill-registry

Admin panel - RBAC, config, admin tools. Use when building admin UI.

admin-wsl

181
from majiayu000/claude-skill-registry

WSL2 Ubuntu administration from Linux side. Profile-aware - reads preferences from Windows-side profile at /mnt/c/Users/{WIN_USER}/.admin/profiles/{hostname}.json Use when: Inside WSL for apt packages, Docker, Python/uv, shell configs, systemd. Coordinates with admin-windows via shared profile ON THE WINDOWS SIDE.

admin-windows

181
from majiayu000/claude-skill-registry

Windows system administration with PowerShell 7.x. Profile-aware - reads your preferences for package managers (scoop vs winget), paths, and installed tools. Use when: Windows-specific admin tasks, PowerShell automation, PATH configuration, package installation, bash-to-PowerShell translation.

admin-unix

181
from majiayu000/claude-skill-registry

Native macOS and Linux administration (non-WSL). Profile-aware - reads preferences from ~/.admin/profiles/{hostname}.json. Use when: macOS/Linux system admin, Homebrew (macOS), apt (Linux), services. NOT for WSL - use admin-wsl instead.

admin-panel-builder

181
from majiayu000/claude-skill-registry

Expert assistant for creating and maintaining admin panel pages in the KR92 Bible Voice project. Use when creating admin pages, building admin components, integrating with admin navigation, or adding admin features.

admin-mcp

181
from majiayu000/claude-skill-registry

MCP server management for Claude Desktop. Profile-aware - reads MCP server inventory from profile.mcp.servers{} and config path from profile.paths.claudeConfig. Use when: installing MCP servers, configuring Claude Desktop, troubleshooting MCP issues.

admin-interface-rules

181
from majiayu000/claude-skill-registry

Rules for the Admin interface functionalities

admin-infra-vultr

181
from majiayu000/claude-skill-registry

Deploys infrastructure on Vultr with Cloud Compute instances, High-Frequency servers, and VPCs. Excellent value with Kubernetes autoscaling support and global data centers. Use when: setting up Vultr infrastructure, deploying cloud compute or high-frequency instances, configuring firewalls, needing good price/performance with global reach. Keywords: vultr, vultr-cli, VPS, cloud compute, high-frequency, firewall, VPC, kubernetes autoscale, infrastructure

admin-infra-oci

181
from majiayu000/claude-skill-registry

Deploys infrastructure on Oracle Cloud Infrastructure (OCI) with ARM64 instances (Always Free tier eligible). Handles compartments, VCNs, subnets, security lists, and compute instances. Use when: setting up Oracle Cloud infrastructure, deploying ARM64 instances, troubleshooting OUT_OF_HOST_CAPACITY errors, optimizing for Always Free tier. Keywords: oracle cloud, OCI, ARM64, VM.Standard.A1.Flex, Always Free tier, OUT_OF_HOST_CAPACITY, oci compartment, oci vcn

admin-infra-linode

181
from majiayu000/claude-skill-registry

Deploys infrastructure on Linode (Akamai Cloud) with Linodes, Firewalls, and VLANs. Strong Kubernetes support with Cluster Autoscaler and Akamai edge network integration. Use when: setting up Linode/Akamai infrastructure, deploying Linodes, configuring firewalls, needing Kubernetes autoscaling, wanting Akamai CDN integration. Keywords: linode, akamai, linode-cli, VPS, dedicated CPU, firewall, VLAN, kubernetes autoscale, infrastructure