| name | admin-panel-builder |
| description | 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 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
import { Button } from "@ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
import { useUserRole } from "@shared-auth/hooks/useUserRole";
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
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";
const AdminExamplePage = () => {
const { isAdmin } = ();
(!isAdmin) {
(
);
}
(
);
};
;
Step 2: Add Route to App.tsx
import AdminExamplePage from "./pages/AdminExamplePage";
<Route path="/admin/example" element={<AdminExamplePage />} />
Step 3: Add Card to Admin Dashboard
{
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
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();
const { data: items, isLoading } = useQuery({
queryKey: ["admin-examples"],
queryFn: async () => {
const { data, error } = await supabase
.from()
.()
.(, { : });
(error) error;
data;
},
});
deleteMutation = ({
: (: ) => {
{ error } = supabase
.()
.()
.(, id);
(error) error;
},
: {
queryClient.({ : [] });
toast.();
},
: {
toast.();
},
});
(isLoading) {
(
);
}
(
);
};
Form Component Pattern
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().(),
: z.().(),
});
= z.< formSchema>;
= () => {
queryClient = ();
form = useForm<>({
: (formSchema),
: {
: ,
: ,
: ,
},
});
createMutation = ({
: (: ) => {
{ error } = supabase
.()
.([values]);
(error) error;
},
: {
queryClient.({ : [] });
toast.();
form.();
},
: {
toast.();
},
});
= () => {
createMutation.(values);
};
(
);
};
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;
description?: string;
icon?: React.ReactNode;
showBackButton?: boolean;
showSidebarTrigger?: boolean;
}
Dashboard Card Props
interface AdminCardProps {
title: string;
description: string;
icon: React.ElementType;
path: string;
stats?: Array<{
label: string;
value: string | number;
}>;
isLoading?: boolean;
isExternal?: boolean;
}
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 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
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,
});
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