admin-panel-builder
npx skills add https://github.com/majiayu000/claude-skill-registry --skill admin-panel-builder
Agent 安装分布
Skill 文档
Admin Panel Builder
Current Architecture (2026)
Layout Pattern
All admin pages now use the SidebarProvider + AppSidebar + AdminHeader pattern:
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:
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
// 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 managementAdminAuthTokensPage.tsx– Authentication and API tokensAdminTopicsPage.tsx– Topic management and translationsAdminUsersPage.tsx– User and role managementAdminTranslationsPage.tsx– Term translation cacheAdminVideoPage.tsx– Video series and clipsAdminWidgetAnalyticsPage.tsx– Widget usage statistics
Creating New Admin Pages
Step 1: Create Page Component
// 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
// 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
// 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
// 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
// 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
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
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
<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 hubapps/raamattu-nyt/src/pages/AdminAuthTokensPage.tsx– Token management exampleapps/raamattu-nyt/src/components/admin/AdminHeader.tsx– Header componentapps/raamattu-nyt/src/App.tsx– Route definitions
Documentation
Docs/07-ADMIN-GUIDE.md– Admin features overviewDocs/12-AUTHENTICATION.md– Auth system detailsDocs/context/supabase-map.md– Database schema reference
Common Patterns
Stats Query Pattern
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
const { data } = await supabase.rpc("get_admin_stats", {
p_limit: 1000,
});
Conditional Rendering
{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
- Keep components focused: One component per file, single responsibility
- Extract complex logic: Move business logic to separate functions/hooks
- Type everything: Use TypeScript interfaces for all data structures
- Test queries first: Verify Supabase queries in SQL editor before implementing
- Handle empty states: Always show something when data is empty
- Loading states matter: Use skeletons for stats, spinners for content
- Error boundaries: Wrap risky operations in try-catch
- Invalidate smartly: Only invalidate affected queries after mutations
- Consistent naming: Use Finnish for UI, English for code
- Document complex logic: Add comments for non-obvious implementations