admin-panel-builder

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.

181 stars

Best use case

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

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.

Teams using admin-panel-builder 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-panel-builder/SKILL.md --create-dirs "https://raw.githubusercontent.com/majiayu000/claude-skill-registry/main/skills/data/admin-panel-builder/SKILL.md"

Manual Installation

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

How admin-panel-builder Compares

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

Frequently Asked Questions

What does this skill do?

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.

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 Panel Builder

## Current Architecture (2026)

### Layout Pattern
All admin pages now use the **SidebarProvider + AppSidebar + AdminHeader** pattern:

```typescript
import { SidebarProvider } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/AppSidebar";
import AdminHeader from "@/components/admin/AdminHeader";

return (
  <SidebarProvider>
    <div className="min-h-screen flex w-full bg-background">
      <AppSidebar onNavigateToContinueAudio={() => {}} onNavigateToContinueText={() => {}} />
      <main className="flex-1 overflow-auto">
        <AdminHeader
          title="Page Title"
          icon={<Icon className="h-6 w-6 text-primary" />}
          showBackButton={true}
        />
        <div className="p-6">
          {/* Content */}
        </div>
      </main>
    </div>
  </SidebarProvider>
);
```

### Authentication Pattern
Use `@shared-auth/hooks/useUserRole` from the shared package:

```typescript
import { useUserRole } from "@shared-auth/hooks/useUserRole";

const { isAdmin } = useUserRole();

if (!isAdmin) {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <p className="text-muted-foreground">Sinulla ei ole oikeuksia tähän sivuun.</p>
    </div>
  );
}
```

### Import Patterns
```typescript
// UI components from @ui aliases
import { Button } from "@ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";

// Shared auth from @shared-auth
import { useUserRole } from "@shared-auth/hooks/useUserRole";

// Local imports with @/
import AdminHeader from "@/components/admin/AdminHeader";
import { supabase } from "@/integrations/supabase/client";
```

## Existing Admin Pages Structure

### Dashboard (`AdminDashboardPage.tsx`)
- Central hub with overview cards
- Grid layout with clickable admin cards
- Stats overview widget with quick metrics
- Uses lucide-react icons (Bot, Users, Video, etc.)
- Each card has: title, description, icon, path, stats, isLoading

### Specialized Admin Pages
- `AdminAIPage.tsx` - AI management with tabs (usage, prompts, features, pricing)
- `AdminAudioPage.tsx` - Audio/TTS management
- `AdminAuthTokensPage.tsx` - Authentication and API tokens
- `AdminTopicsPage.tsx` - Topic management and translations
- `AdminUsersPage.tsx` - User and role management
- `AdminTranslationsPage.tsx` - Term translation cache
- `AdminVideoPage.tsx` - Video series and clips
- `AdminWidgetAnalyticsPage.tsx` - Widget usage statistics

## Creating New Admin Pages

### Step 1: Create Page Component

```typescript
// apps/raamattu-nyt/src/pages/AdminExamplePage.tsx

import { useUserRole } from "@shared-auth/hooks/useUserRole";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
import { Database, Loader2 } from "lucide-react";
import { AppSidebar } from "@/components/AppSidebar";
import AdminHeader from "@/components/admin/AdminHeader";
import { SidebarProvider } from "@/components/ui/sidebar";
import { supabase } from "@/integrations/supabase/client";
import { useQuery } from "@tanstack/react-query";
import { toast } from "sonner"; // Use sonner for toast notifications

const AdminExamplePage = () => {
  const { isAdmin } = useUserRole();

  // Access control
  if (!isAdmin) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <p className="text-muted-foreground">Sinulla ei ole oikeuksia tähän sivuun.</p>
      </div>
    );
  }

  return (
    <SidebarProvider>
      <div className="min-h-screen flex w-full bg-background">
        <AppSidebar onNavigateToContinueAudio={() => {}} onNavigateToContinueText={() => {}} />
        <main className="flex-1 overflow-auto">
          <AdminHeader
            title="Example Management"
            description="Manage example resources"
            icon={<Database className="h-6 w-6 text-primary" />}
            showBackButton={true}
          />

          <div className="p-6">
            <div className="max-w-6xl mx-auto space-y-6">
              <Tabs defaultValue="list" className="space-y-4">
                <TabsList>
                  <TabsTrigger value="list">List</TabsTrigger>
                  <TabsTrigger value="create">Create</TabsTrigger>
                  <TabsTrigger value="settings">Settings</TabsTrigger>
                </TabsList>

                <TabsContent value="list">
                  <Card>
                    <CardHeader>
                      <CardTitle>Items</CardTitle>
                      <CardDescription>View and manage items</CardDescription>
                    </CardHeader>
                    <CardContent>
                      {/* Component content */}
                    </CardContent>
                  </Card>
                </TabsContent>

                <TabsContent value="create">
                  <Card>
                    <CardHeader>
                      <CardTitle>Create Item</CardTitle>
                      <CardDescription>Add a new item</CardDescription>
                    </CardHeader>
                    <CardContent>
                      {/* Form content */}
                    </CardContent>
                  </Card>
                </TabsContent>
              </Tabs>
            </div>
          </div>
        </main>
      </div>
    </SidebarProvider>
  );
};

export default AdminExamplePage;
```

### Step 2: Add Route to App.tsx

```typescript
// apps/raamattu-nyt/src/App.tsx

import AdminExamplePage from "./pages/AdminExamplePage";

// In Routes:
<Route path="/admin/example" element={<AdminExamplePage />} />
```

### Step 3: Add Card to Admin Dashboard

```typescript
// In AdminDashboardPage.tsx adminCards array:

{
  title: "Example Management",
  description: "Manage example resources",
  icon: Database,
  path: "/admin/example",
  stats: [{ label: "items", value: itemCount || 0 }],
  isLoading: itemsLoading,
},
```

## Creating Admin Components

### Data Table Pattern

```typescript
// apps/raamattu-nyt/src/components/admin/ExampleTable.tsx

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@ui/button";
import { Badge } from "@ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@ui/table";
import { Trash2, Edit, Loader2 } from "lucide-react";
import { toast } from "sonner";
import { supabase } from "@/integrations/supabase/client";

export const ExampleTable = () => {
  const queryClient = useQueryClient();

  // Fetch data
  const { data: items, isLoading } = useQuery({
    queryKey: ["admin-examples"],
    queryFn: async () => {
      const { data, error } = await supabase
        .from("examples")
        .select("*")
        .order("created_at", { ascending: false });

      if (error) throw error;
      return data;
    },
  });

  // Delete mutation
  const deleteMutation = useMutation({
    mutationFn: async (id: string) => {
      const { error } = await supabase
        .from("examples")
        .delete()
        .eq("id", id);

      if (error) throw error;
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["admin-examples"] });
      toast.success("Item deleted successfully");
    },
    onError: (error: Error) => {
      toast.error(`Error: ${error.message}`);
    },
  });

  if (isLoading) {
    return (
      <div className="flex justify-center p-8">
        <Loader2 className="h-8 w-8 animate-spin" />
      </div>
    );
  }

  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Name</TableHead>
          <TableHead>Status</TableHead>
          <TableHead>Created</TableHead>
          <TableHead className="text-right">Actions</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {items?.map((item) => (
          <TableRow key={item.id}>
            <TableCell className="font-medium">{item.name}</TableCell>
            <TableCell>
              <Badge variant={item.is_active ? "default" : "secondary"}>
                {item.is_active ? "Active" : "Inactive"}
              </Badge>
            </TableCell>
            <TableCell>{new Date(item.created_at).toLocaleDateString()}</TableCell>
            <TableCell className="text-right space-x-2">
              <Button variant="outline" size="sm">
                <Edit className="h-4 w-4" />
              </Button>
              <Button
                variant="destructive"
                size="sm"
                onClick={() => deleteMutation.mutate(item.id)}
              >
                <Trash2 className="h-4 w-4" />
              </Button>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
};
```

### Form Component Pattern

```typescript
// apps/raamattu-nyt/src/components/admin/ExampleForm.tsx

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@ui/button";
import { Input } from "@ui/input";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@ui/form";
import { Switch } from "@ui/switch";
import { toast } from "sonner";
import { supabase } from "@/integrations/supabase/client";

const formSchema = z.object({
  name: z.string().min(3, "Name must be at least 3 characters"),
  description: z.string().optional(),
  is_active: z.boolean().default(true),
});

type FormData = z.infer<typeof formSchema>;

export const ExampleForm = () => {
  const queryClient = useQueryClient();

  const form = useForm<FormData>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      name: "",
      description: "",
      is_active: true,
    },
  });

  const createMutation = useMutation({
    mutationFn: async (values: FormData) => {
      const { error } = await supabase
        .from("examples")
        .insert([values]);

      if (error) throw error;
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["admin-examples"] });
      toast.success("Item created successfully");
      form.reset();
    },
    onError: (error: Error) => {
      toast.error(`Error: ${error.message}`);
    },
  });

  const onSubmit = (values: FormData) => {
    createMutation.mutate(values);
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Name</FormLabel>
              <FormControl>
                <Input placeholder="Enter name" {...field} />
              </FormControl>
              <FormDescription>The name of the item</FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="description"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Description</FormLabel>
              <FormControl>
                <Input placeholder="Enter description" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="is_active"
          render={({ field }) => (
            <FormItem className="flex items-center justify-between rounded-lg border p-4">
              <div className="space-y-0.5">
                <FormLabel className="text-base">Active</FormLabel>
                <FormDescription>Enable this item</FormDescription>
              </div>
              <FormControl>
                <Switch
                  checked={field.value}
                  onCheckedChange={field.onChange}
                />
              </FormControl>
            </FormItem>
          )}
        />

        <Button type="submit" disabled={createMutation.isPending}>
          {createMutation.isPending ? "Creating..." : "Create Item"}
        </Button>
      </form>
    </Form>
  );
};
```

## Best Practices

### 1. Access Control
✅ Always check `isAdmin` from `useUserRole()`
✅ Show user-friendly Finnish message: "Sinulla ei ole oikeuksia tähän sivuun."
✅ No loading spinner needed (handled by hook)

### 2. Toast Notifications
✅ Use `toast` from "sonner" (not useToast hook)
✅ `toast.success()` for success messages
✅ `toast.error()` for errors
✅ Keep messages concise and actionable

### 3. Data Fetching
✅ Use React Query (`useQuery`, `useMutation`)
✅ Always invalidate queries after mutations
✅ Handle loading states with Loader2 spinner
✅ Show empty states when no data

### 4. Layout & Styling
✅ Use SidebarProvider + AppSidebar + AdminHeader pattern
✅ Wrap content in `<div className="p-6">`
✅ Use `max-w-6xl mx-auto` for centered content
✅ Use `space-y-6` for vertical spacing
✅ Cards for section containers
✅ Tabs for multi-section pages

### 5. Icons
✅ Import from lucide-react
✅ Use consistent size: `h-4 w-4` for buttons, `h-6 w-6` for headers
✅ Add text-primary to header icons

### 6. Finnish UI Text
✅ Page titles and descriptions in Finnish when user-facing
✅ Error messages in Finnish
✅ Admin navigation in Finnish
✅ Code/technical terms can be in English

## Common UI Components

| Component | Import Path | Use For |
|-----------|-------------|---------|
| Card | @ui/card | Section containers |
| Tabs | @ui/tabs | Multi-section pages |
| Table | @ui/table | Data lists |
| Form | @ui/form | Input forms with validation |
| Button | @ui/button | Actions |
| Badge | @ui/badge | Status indicators |
| Input | @ui/input | Text inputs |
| Switch | @ui/switch | Boolean toggles |
| Select | @ui/select | Dropdowns |
| Dialog | @ui/dialog | Modals |
| AlertDialog | @ui/alert-dialog | Confirmations |
| Alert | @ui/alert | Notices and warnings |

## AdminHeader Props

```typescript
interface AdminHeaderProps {
  title: string;              // Page title (Finnish)
  description?: string;       // Optional subtitle
  icon?: React.ReactNode;    // Optional icon element
  showBackButton?: boolean;  // Show back to admin (default: true)
  showSidebarTrigger?: boolean; // Show sidebar toggle (default: true)
}
```

## Dashboard Card Props

```typescript
interface AdminCardProps {
  title: string;             // Card title (Finnish)
  description: string;       // Card description (Finnish)
  icon: React.ElementType;  // Lucide icon component
  path: string;             // Navigation path
  stats?: Array<{           // Optional statistics
    label: string;
    value: string | number;
  }>;
  isLoading?: boolean;      // Show skeleton for stats
  isExternal?: boolean;     // Open in new window
}
```

## Token Management

### Important Admin Features
- **Authentication Tokens**: Manage Supabase keys, Turnstile tokens
- **OAuth Providers**: Google, Apple (planned)
- **Integration Tokens**: OpenAI, ElevenLabs API keys
- **Direct Dashboard Links**: Quick access to external services
- **Copy-to-clipboard**: Environment variable names
- **Regeneration Guidelines**: When and how to rotate keys
- **Security Warnings**: Sensitive key indicators

### Token Card Pattern
```typescript
<Card>
  <CardHeader>
    <CardTitle className="flex items-center gap-2">
      {token.name}
      {token.isSensitive && (
        <Badge variant="outline" className="bg-red-500/10">
          <Shield className="h-3 w-3 mr-1" />
          Sensitive
        </Badge>
      )}
    </CardTitle>
  </CardHeader>
  <CardContent className="space-y-4">
    {/* Environment variable with copy button */}
    {/* Location info */}
    {/* Usage badges */}
    {/* Dashboard links */}
    {/* Regeneration info */}
  </CardContent>
</Card>
```

## Related Files

### Key Admin Files
- `apps/raamattu-nyt/src/pages/AdminDashboardPage.tsx` - Dashboard hub
- `apps/raamattu-nyt/src/pages/AdminAuthTokensPage.tsx` - Token management example
- `apps/raamattu-nyt/src/components/admin/AdminHeader.tsx` - Header component
- `apps/raamattu-nyt/src/App.tsx` - Route definitions

### Documentation
- `Docs/07-ADMIN-GUIDE.md` - Admin features overview
- `Docs/12-AUTHENTICATION.md` - Auth system details
- `Docs/context/supabase-map.md` - Database schema reference

## Common Patterns

### Stats Query Pattern
```typescript
const { data: itemCount, isLoading } = useQuery({
  queryKey: ["admin-item-count"],
  queryFn: async () => {
    const { count } = await supabase
      .from("items")
      .select("*", { count: "exact", head: true });
    return count || 0;
  },
  enabled: isAdmin, // Only fetch if admin
});
```

### RPC Call Pattern
```typescript
const { data } = await supabase.rpc("get_admin_stats", {
  p_limit: 1000,
});
```

### Conditional Rendering
```typescript
{stats && stats.length > 0 && (
  <CardContent>
    <div className="flex gap-3">
      {isLoading ? (
        <Skeleton className="h-4 w-20" />
      ) : (
        stats.map((stat, i) => (
          <div key={i}>
            <span className="font-semibold">{stat.value}</span>
            <span className="text-muted-foreground ml-1">{stat.label}</span>
          </div>
        ))
      )}
    </div>
  </CardContent>
)}
```

## Tips

1. **Keep components focused**: One component per file, single responsibility
2. **Extract complex logic**: Move business logic to separate functions/hooks
3. **Type everything**: Use TypeScript interfaces for all data structures
4. **Test queries first**: Verify Supabase queries in SQL editor before implementing
5. **Handle empty states**: Always show something when data is empty
6. **Loading states matter**: Use skeletons for stats, spinners for content
7. **Error boundaries**: Wrap risky operations in try-catch
8. **Invalidate smartly**: Only invalidate affected queries after mutations
9. **Consistent naming**: Use Finnish for UI, English for code
10. **Document complex logic**: Add comments for non-obvious implementations

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

admin-infra-hetzner

181
from majiayu000/claude-skill-registry

Deploys infrastructure on Hetzner Cloud with ARM64 or x86 servers. Cost-effective European cloud with excellent price/performance ratio. Use when: setting up Hetzner Cloud infrastructure, deploying ARM64 servers (CAX), x86 servers (CX/CPX), looking for affordable alternative to OCI. Keywords: hetzner, hcloud, ARM64, CAX, CX, CPX, infrastructure, server setup, european cloud