Back to skills
SkillHub ClubAnalyze Data & AIFull StackFrontendData / AI

Building Frontend Dashboards

Build responsive React dashboards with TypeScript, shadcn/ui, TanStack Query, and Supabase for event-studio. Use when user mentions dashboard, metrics, KPI cards, data tables, charts, analytics, admin panel, Recharts, event management UI, booking interface, financial overview, or asks to create pages with data visualization.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
0
Hot score
74
Updated
March 20, 2026
Overall rating
C1.6
Composite score
1.6
Best-practice grade
N/A

Install command

npx @skill-hub/cli install amo-tech-ai-event-studio-frontend-dashboard
ReactDashboardTypeScriptData VisualizationSupabase

Repository

amo-tech-ai/event-studio

Skill path: .claude/skills/frontend-dashboard

Build responsive React dashboards with TypeScript, shadcn/ui, TanStack Query, and Supabase for event-studio. Use when user mentions dashboard, metrics, KPI cards, data tables, charts, analytics, admin panel, Recharts, event management UI, booking interface, financial overview, or asks to create pages with data visualization.

Open repository

Best for

Primary workflow: Analyze Data & AI.

Technical facets: Full Stack, Frontend, Data / AI.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: amo-tech-ai.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install Building Frontend Dashboards into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/amo-tech-ai/event-studio before adding Building Frontend Dashboards to shared team environments
  • Use Building Frontend Dashboards for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 1.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: Building Frontend Dashboards
description: Build responsive React dashboards with TypeScript, shadcn/ui, TanStack Query, and Supabase for event-studio. Use when user mentions dashboard, metrics, KPI cards, data tables, charts, analytics, admin panel, Recharts, event management UI, booking interface, financial overview, or asks to create pages with data visualization.
---

# Building Frontend Dashboards

Expert in building React dashboards for the event-studio project.

## Project Context

**Tech Stack**: React 18 + TypeScript + Vite + shadcn/ui + TanStack Query + Supabase

**Structure**:
```
src/
├── pages/Dashboard*.tsx       # Route components
├── features/[feature]/hooks/  # Custom hooks (useEvents, etc.)
├── components/ui/             # shadcn/ui components
└── integrations/supabase/     # Supabase client & types
```

**Key Patterns**:
- TanStack Query for all data fetching
- Supabase for backend (use `supabase` client from `@/integrations/supabase/client`)
- shadcn/ui components (import from `@/components/ui/`)
- Zustand for state (if needed)
- React Hook Form + Zod for forms

## Standard Dashboard Workflow

Copy this checklist and track your progress:

```
Dashboard Implementation:
- [ ] 1. Identify data requirements (tables, metrics)
- [ ] 2. Create custom hook in features/[feature]/hooks/
- [ ] 3. Build page in src/pages/Dashboard*.tsx
- [ ] 4. Add metric cards and main content
- [ ] 5. Add route in App.tsx
- [ ] 6. Test loading/error states
```

### Step 1: Identify Requirements

Ask user to clarify:
- What data to display?
- What metrics/KPIs?
- What user actions?
- Filters needed?

### Step 2: Create Custom Hook

**Pattern**: Create in `src/features/[feature]/hooks/use*.ts`

```typescript
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';

export function useDashboardMetrics() {
  return useQuery({
    queryKey: ['dashboard-metrics'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('events')
        .select('*, bookings(count)');
      if (error) throw error;
      return data;
    },
  });
}
```

**For more patterns**: See `resources/query-patterns.ts`

### Step 3: Build Page Structure

**Template**:

```typescript
import Sidebar from '@/components/Sidebar';
import { Card } from '@/components/ui/card';

const DashboardNew = () => {
  const { data, isLoading, error } = useYourHook();

  if (isLoading) return <PageLoader />;
  if (error) return <ErrorAlert error={error} />;

  return (
    <div className="flex min-h-screen bg-gray-50">
      <Sidebar />
      <main className="flex-1 p-8">
        <h1 className="text-3xl font-bold mb-6">Dashboard Title</h1>

        {/* Metrics Grid */}
        <div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
          {/* MetricCard components */}
        </div>

        {/* Main Content */}
        <Card className="p-6">
          {/* DataTable or Charts */}
        </Card>
      </main>
    </div>
  );
};

export default DashboardNew;
```

### Step 4: Use Reusable Components

**Import from resources**:

```typescript
import {
  MetricCard,
  DataTable,
  StatusBadge,
  EmptyState,
  ErrorAlert
} from '../skills/frontend-dashboard/resources/component-patterns';
```

**See complete examples**: `resources/component-patterns.tsx`

## Quick Reference

### Supabase Queries

```typescript
// Simple query
const { data } = await supabase.from('events').select('*');

// With joins
const { data } = await supabase
  .from('events')
  .select('*, bookings(*), organizer:users(full_name)');

// With filters
const { data } = await supabase
  .from('events')
  .select('*')
  .eq('status', 'active')
  .gte('start_date', date);
```

**More patterns**: See `resources/supabase-patterns.ts`

### Common Components

```typescript
// Metric Card
<MetricCard
  title="Total Revenue"
  value="$125,000"
  change={12.5}
  icon={<DollarSign />}
/>

// Data Table
<DataTable
  data={events}
  columns={[
    { key: 'title', label: 'Event' },
    { key: 'status', label: 'Status', render: (v) => <StatusBadge status={v} /> }
  ]}
  onEdit={(row) => handleEdit(row)}
/>

// Empty State
<EmptyState
  title="No events yet"
  description="Get started by creating your first event"
  action={{ label: "Create Event", onClick: () => navigate('/new') }}
/>
```

### Layouts

**7 layout patterns** in `resources/layout-examples.tsx`:
- StandardDashboardLayout (sidebar + content)
- DashboardWithActionsBar (search + filters)
- TabbedDashboard (multi-tab analytics)
- SplitViewDashboard (list + detail)
- GridViewDashboard (grid/list toggle)
- ResponsiveDashboard (mobile-first)
- StickyHeaderDashboard (fixed header)

## Examples

### Example 1: Events Dashboard

**User**: "Create a dashboard page showing all events with metrics"

**Your Process**:

1. **Create hook** (`src/features/events/hooks/useEventsDashboard.ts`):
```typescript
export function useEventsDashboard() {
  return useQuery({
    queryKey: ['events-dashboard'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('events')
        .select('*, bookings(count, total_amount.sum())')
        .order('created_at', { ascending: false });
      if (error) throw error;
      return data;
    },
  });
}
```

2. **Create page** (`src/pages/DashboardEvents.tsx`) with:
   - Header with "Create Event" button
   - 4 metric cards (total events, active events, revenue, bookings)
   - Search bar and filters
   - DataTable with events
   - Actions: edit, delete, view details

3. **Add route** in `App.tsx`:
```typescript
<Route path="/dashboard/events" element={<DashboardEvents />} />
```

### Example 2: Analytics Dashboard with Charts

**User**: "Add a revenue analytics page with charts"

**Your Response**:

1. Create `useRevenueAnalytics` hook fetching bookings with date grouping
2. Build page with:
   - Revenue metrics cards
   - Line chart (Recharts) showing revenue over time
   - Pie chart for revenue by category
3. Use Recharts components from `recharts` package
4. Add date range filter

### Example 3: Bookings Management

**User**: "Create a bookings dashboard with search and filters"

**Your Implementation**:
- Search by attendee name
- Filter by status (pending, confirmed, cancelled)
- DataTable with booking details
- Actions: view ticket, refund, send email
- Export to CSV button

## Best Practices

**Always**:
- Show loading states (skeleton or spinner)
- Handle errors with user-friendly messages
- Make responsive (use `md:`, `lg:` prefixes)
- Use TypeScript strictly (no `any`)
- Validate permissions before showing data

**Performance**:
- Implement pagination for large datasets
- Use proper TanStack Query caching (`queryKey`)
- Optimize with `React.memo` if needed

**UX**:
- Provide empty states
- Use toast notifications (from `sonner`)
- Make actions reversible
- Show success feedback

## Resources

**Complete examples and patterns**:
- `resources/component-patterns.tsx` - Reusable components (MetricCard, DataTable, etc.)
- `resources/query-patterns.ts` - TanStack Query hooks and patterns
- `resources/supabase-patterns.ts` - Supabase query examples
- `resources/layout-examples.tsx` - 7 dashboard layout templates

## Troubleshooting

**Query not refetching**: Use `invalidateQueries` in mutation `onSuccess`

**RLS blocking queries**: Check Supabase RLS policies allow the operation

**TypeScript errors**: Regenerate types with `npx supabase gen types typescript`

**Not responsive**: Use Tailwind responsive prefixes, test in dev tools


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### resources/query-patterns.ts

```typescript
// TanStack Query Patterns for event-studio Dashboard

import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';

// ============================================================================
// BASIC QUERY PATTERNS
// ============================================================================

/**
 * Simple data fetching hook
 */
export function useEvents() {
  return useQuery({
    queryKey: ['events'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('events')
        .select('*')
        .order('created_at', { ascending: false });

      if (error) throw error;
      return data;
    },
    staleTime: 1000 * 60 * 5, // 5 minutes
  });
}

/**
 * Query with parameters
 */
export function useEventsByStatus(status: string) {
  return useQuery({
    queryKey: ['events', 'status', status],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('events')
        .select('*')
        .eq('status', status);

      if (error) throw error;
      return data;
    },
    enabled: !!status, // Only run if status is provided
  });
}

/**
 * Single item query
 */
export function useEvent(id: string | undefined) {
  return useQuery({
    queryKey: ['events', id],
    queryFn: async () => {
      if (!id) throw new Error('Event ID is required');

      const { data, error } = await supabase
        .from('events')
        .select(`
          *,
          bookings (
            id,
            user_id,
            total_amount,
            status
          ),
          organizer:users!organizer_id (
            id,
            full_name,
            email
          )
        `)
        .eq('id', id)
        .single();

      if (error) throw error;
      return data;
    },
    enabled: !!id,
  });
}

// ============================================================================
// AGGREGATIONS & ANALYTICS
// ============================================================================

/**
 * Dashboard metrics with aggregations
 */
export function useDashboardMetrics() {
  return useQuery({
    queryKey: ['dashboard-metrics'],
    queryFn: async () => {
      // Get total events
      const { count: totalEvents } = await supabase
        .from('events')
        .select('*', { count: 'exact', head: true });

      // Get active events
      const { count: activeEvents } = await supabase
        .from('events')
        .select('*', { count: 'exact', head: true })
        .eq('status', 'active');

      // Get total bookings
      const { count: totalBookings } = await supabase
        .from('bookings')
        .select('*', { count: 'exact', head: true });

      // Get revenue (last 30 days)
      const thirtyDaysAgo = new Date();
      thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

      const { data: recentBookings } = await supabase
        .from('bookings')
        .select('total_amount')
        .eq('status', 'confirmed')
        .gte('created_at', thirtyDaysAgo.toISOString());

      const totalRevenue = recentBookings?.reduce(
        (sum, booking) => sum + (booking.total_amount || 0),
        0
      ) || 0;

      return {
        totalEvents: totalEvents || 0,
        activeEvents: activeEvents || 0,
        totalBookings: totalBookings || 0,
        totalRevenue,
      };
    },
    staleTime: 1000 * 60, // 1 minute
  });
}

/**
 * Revenue analytics over time
 */
export function useRevenueAnalytics(days: number = 30) {
  return useQuery({
    queryKey: ['revenue-analytics', days],
    queryFn: async () => {
      const startDate = new Date();
      startDate.setDate(startDate.getDate() - days);

      const { data, error } = await supabase
        .from('bookings')
        .select('created_at, total_amount')
        .eq('status', 'confirmed')
        .gte('created_at', startDate.toISOString())
        .order('created_at', { ascending: true });

      if (error) throw error;

      // Group by date
      const groupedData = data.reduce((acc, booking) => {
        const date = new Date(booking.created_at).toLocaleDateString();
        if (!acc[date]) {
          acc[date] = 0;
        }
        acc[date] += booking.total_amount || 0;
        return acc;
      }, {} as Record<string, number>);

      return Object.entries(groupedData).map(([date, revenue]) => ({
        date,
        revenue,
      }));
    },
  });
}

// ============================================================================
// MUTATION PATTERNS
// ============================================================================

/**
 * Create mutation with optimistic update
 */
export function useCreateEvent() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (newEvent: any) => {
      const { data, error } = await supabase
        .from('events')
        .insert(newEvent)
        .select()
        .single();

      if (error) throw error;
      return data;
    },
    onMutate: async (newEvent) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['events'] });

      // Snapshot previous value
      const previousEvents = queryClient.getQueryData(['events']);

      // Optimistically update
      queryClient.setQueryData(['events'], (old: any[]) => [
        { ...newEvent, id: 'temp-id', created_at: new Date().toISOString() },
        ...(old || []),
      ]);

      return { previousEvents };
    },
    onError: (err, newEvent, context) => {
      // Rollback on error
      if (context?.previousEvents) {
        queryClient.setQueryData(['events'], context.previousEvents);
      }
      toast.error('Failed to create event');
    },
    onSuccess: (data) => {
      toast.success('Event created successfully');
    },
    onSettled: () => {
      // Refetch to ensure consistency
      queryClient.invalidateQueries({ queryKey: ['events'] });
    },
  });
}

/**
 * Update mutation
 */
export function useUpdateEvent() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({ id, updates }: { id: string; updates: any }) => {
      const { data, error } = await supabase
        .from('events')
        .update(updates)
        .eq('id', id)
        .select()
        .single();

      if (error) throw error;
      return data;
    },
    onSuccess: (data) => {
      // Update individual event cache
      queryClient.setQueryData(['events', data.id], data);
      // Invalidate list cache
      queryClient.invalidateQueries({ queryKey: ['events'] });
      toast.success('Event updated successfully');
    },
    onError: () => {
      toast.error('Failed to update event');
    },
  });
}

/**
 * Delete mutation
 */
export function useDeleteEvent() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (id: string) => {
      const { error } = await supabase
        .from('events')
        .delete()
        .eq('id', id);

      if (error) throw error;
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['events'] });
      toast.success('Event deleted successfully');
    },
    onError: () => {
      toast.error('Failed to delete event');
    },
  });
}

// ============================================================================
// INFINITE SCROLL PATTERN
// ============================================================================

const PAGE_SIZE = 20;

export function useInfiniteEvents() {
  return useInfiniteQuery({
    queryKey: ['events', 'infinite'],
    queryFn: async ({ pageParam = 0 }) => {
      const { data, error } = await supabase
        .from('events')
        .select('*')
        .order('created_at', { ascending: false })
        .range(pageParam, pageParam + PAGE_SIZE - 1);

      if (error) throw error;
      return data;
    },
    getNextPageParam: (lastPage, allPages) => {
      if (lastPage.length < PAGE_SIZE) return undefined;
      return allPages.length * PAGE_SIZE;
    },
    initialPageParam: 0,
  });
}

// ============================================================================
// DEPENDENT QUERIES
// ============================================================================

/**
 * Query that depends on another query
 */
export function useEventBookings(eventId: string | undefined) {
  return useQuery({
    queryKey: ['bookings', 'event', eventId],
    queryFn: async () => {
      if (!eventId) throw new Error('Event ID required');

      const { data, error } = await supabase
        .from('bookings')
        .select(`
          *,
          user:users!user_id (
            id,
            full_name,
            email
          )
        `)
        .eq('event_id', eventId)
        .order('created_at', { ascending: false });

      if (error) throw error;
      return data;
    },
    enabled: !!eventId, // Only run when eventId is available
  });
}

// ============================================================================
// PARALLEL QUERIES
// ============================================================================

/**
 * Fetch multiple related datasets in parallel
 */
export function useEventDashboard(eventId: string) {
  const event = useEvent(eventId);
  const bookings = useEventBookings(eventId);
  const analytics = useRevenueAnalytics(30);

  return {
    event,
    bookings,
    analytics,
    isLoading: event.isLoading || bookings.isLoading || analytics.isLoading,
    isError: event.isError || bookings.isError || analytics.isError,
  };
}

// ============================================================================
// POLLING PATTERN
// ============================================================================

/**
 * Auto-refresh data every N seconds
 */
export function useLiveBookings() {
  return useQuery({
    queryKey: ['bookings', 'live'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('bookings')
        .select('*')
        .eq('status', 'pending')
        .order('created_at', { ascending: false });

      if (error) throw error;
      return data;
    },
    refetchInterval: 10000, // Refetch every 10 seconds
  });
}

// ============================================================================
// PREFETCH PATTERN
// ============================================================================

/**
 * Prefetch data for better UX
 */
export function usePrefetchEvent() {
  const queryClient = useQueryClient();

  return (eventId: string) => {
    queryClient.prefetchQuery({
      queryKey: ['events', eventId],
      queryFn: async () => {
        const { data, error } = await supabase
          .from('events')
          .select('*')
          .eq('id', eventId)
          .single();

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

// ============================================================================
// USAGE EXAMPLES
// ============================================================================

/*
// Example 1: Simple Query
function EventsList() {
  const { data, isLoading, error } = useEvents();

  if (isLoading) return <PageLoader />;
  if (error) return <ErrorAlert error={error} />;

  return <DataTable data={data} />;
}

// Example 2: Mutation
function CreateEventButton() {
  const createEvent = useCreateEvent();

  const handleCreate = () => {
    createEvent.mutate({
      title: 'New Event',
      status: 'draft',
    });
  };

  return (
    <Button onClick={handleCreate} disabled={createEvent.isPending}>
      {createEvent.isPending ? 'Creating...' : 'Create Event'}
    </Button>
  );
}

// Example 3: Infinite Scroll
function EventsInfiniteList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteEvents();

  return (
    <>
      {data?.pages.map((page, i) => (
        <React.Fragment key={i}>
          {page.map(event => <EventCard key={event.id} event={event} />)}
        </React.Fragment>
      ))}
      {hasNextPage && (
        <Button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? 'Loading...' : 'Load More'}
        </Button>
      )}
    </>
  );
}
*/

```

### resources/component-patterns.tsx

```tsx
// Reusable Dashboard Component Patterns for event-studio

import React from 'react';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
  TrendingUp,
  TrendingDown,
  MoreVertical,
  Download,
  Edit,
  Trash,
  AlertCircle,
  CalendarDays,
  DollarSign,
  Users,
  TrendingDown as TrendingDownIcon,
} from 'lucide-react';

// ============================================================================
// METRIC CARDS
// ============================================================================

interface MetricCardProps {
  title: string;
  value: string | number;
  change?: number;
  changeLabel?: string;
  icon: React.ReactNode;
  trend?: 'up' | 'down' | 'neutral';
  className?: string;
}

export function MetricCard({
  title,
  value,
  change,
  changeLabel = 'from last month',
  icon,
  trend,
  className = '',
}: MetricCardProps) {
  const getTrendColor = () => {
    if (!trend) return change && change > 0 ? 'text-green-600' : 'text-red-600';
    return trend === 'up' ? 'text-green-600' : trend === 'down' ? 'text-red-600' : 'text-gray-600';
  };

  const TrendIcon = trend === 'up' || (change && change > 0) ? TrendingUp : TrendingDownIcon;

  return (
    <Card className={`p-6 ${className}`}>
      <div className="flex items-center justify-between mb-4">
        <span className="text-sm font-medium text-gray-600">{title}</span>
        <div className="p-3 bg-primary/10 rounded-lg text-primary">{icon}</div>
      </div>
      <div className="text-3xl font-bold mb-2">{value}</div>
      {change !== undefined && (
        <div className={`flex items-center text-sm ${getTrendColor()}`}>
          <TrendIcon className="h-4 w-4 mr-1" />
          <span className="font-medium">{Math.abs(change)}%</span>
          <span className="ml-1 text-gray-500">{changeLabel}</span>
        </div>
      )}
    </Card>
  );
}

export function MetricCardSkeleton() {
  return (
    <Card className="p-6">
      <div className="flex items-center justify-between mb-4">
        <Skeleton className="h-4 w-24" />
        <Skeleton className="h-10 w-10 rounded-lg" />
      </div>
      <Skeleton className="h-8 w-32 mb-2" />
      <Skeleton className="h-4 w-28" />
    </Card>
  );
}

// ============================================================================
// STATS GRID
// ============================================================================

interface DashboardStats {
  totalEvents: number;
  totalRevenue: number;
  totalBookings: number;
  revenueChange: number;
}

export function StatsGrid({ stats }: { stats: DashboardStats }) {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
      <MetricCard
        title="Total Events"
        value={stats.totalEvents}
        icon={<CalendarDays className="h-5 w-5" />}
      />
      <MetricCard
        title="Total Revenue"
        value={`$${stats.totalRevenue.toLocaleString()}`}
        change={stats.revenueChange}
        icon={<DollarSign className="h-5 w-5" />}
      />
      <MetricCard
        title="Total Bookings"
        value={stats.totalBookings}
        icon={<Users className="h-5 w-5" />}
      />
      <MetricCard
        title="Active Events"
        value={stats.totalEvents}
        icon={<CalendarDays className="h-5 w-5" />}
      />
    </div>
  );
}

// ============================================================================
// DATA TABLE WITH ACTIONS
// ============================================================================

interface DataTableProps<T> {
  data: T[];
  columns: {
    key: keyof T;
    label: string;
    render?: (value: any, row: T) => React.ReactNode;
  }[];
  onEdit?: (row: T) => void;
  onDelete?: (row: T) => void;
  onView?: (row: T) => void;
}

export function DataTable<T extends { id: string | number }>({
  data,
  columns,
  onEdit,
  onDelete,
  onView,
}: DataTableProps<T>) {
  return (
    <div className="rounded-md border">
      <Table>
        <TableHeader>
          <TableRow>
            {columns.map((col) => (
              <TableHead key={String(col.key)}>{col.label}</TableHead>
            ))}
            {(onEdit || onDelete || onView) && <TableHead className="text-right">Actions</TableHead>}
          </TableRow>
        </TableHeader>
        <TableBody>
          {data.length === 0 ? (
            <TableRow>
              <TableCell colSpan={columns.length + 1} className="text-center py-8 text-gray-500">
                No data available
              </TableCell>
            </TableRow>
          ) : (
            data.map((row) => (
              <TableRow key={row.id}>
                {columns.map((col) => (
                  <TableCell key={String(col.key)}>
                    {col.render ? col.render(row[col.key], row) : String(row[col.key])}
                  </TableCell>
                ))}
                {(onEdit || onDelete || onView) && (
                  <TableCell className="text-right">
                    <DropdownMenu>
                      <DropdownMenuTrigger asChild>
                        <Button variant="ghost" size="icon">
                          <MoreVertical className="h-4 w-4" />
                        </Button>
                      </DropdownMenuTrigger>
                      <DropdownMenuContent align="end">
                        {onView && (
                          <DropdownMenuItem onClick={() => onView(row)}>
                            View Details
                          </DropdownMenuItem>
                        )}
                        {onEdit && (
                          <DropdownMenuItem onClick={() => onEdit(row)}>
                            <Edit className="h-4 w-4 mr-2" />
                            Edit
                          </DropdownMenuItem>
                        )}
                        {onDelete && (
                          <DropdownMenuItem
                            onClick={() => onDelete(row)}
                            className="text-red-600"
                          >
                            <Trash className="h-4 w-4 mr-2" />
                            Delete
                          </DropdownMenuItem>
                        )}
                      </DropdownMenuContent>
                    </DropdownMenu>
                  </TableCell>
                )}
              </TableRow>
            ))
          )}
        </TableBody>
      </Table>
    </div>
  );
}

// ============================================================================
// STATUS BADGE
// ============================================================================

interface StatusBadgeProps {
  status: 'active' | 'draft' | 'cancelled' | 'completed' | 'pending';
  className?: string;
}

export function StatusBadge({ status, className = '' }: StatusBadgeProps) {
  const variants: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
    active: 'default',
    draft: 'secondary',
    cancelled: 'destructive',
    completed: 'outline',
    pending: 'secondary',
  };

  return (
    <Badge variant={variants[status]} className={className}>
      {status.charAt(0).toUpperCase() + status.slice(1)}
    </Badge>
  );
}

// ============================================================================
// EMPTY STATE
// ============================================================================

interface EmptyStateProps {
  icon?: React.ReactNode;
  title: string;
  description: string;
  action?: {
    label: string;
    onClick: () => void;
  };
}

export function EmptyState({ icon, title, description, action }: EmptyStateProps) {
  return (
    <Card className="p-12 text-center">
      {icon && <div className="flex justify-center mb-4 text-gray-400">{icon}</div>}
      <h3 className="text-xl font-semibold mb-2">{title}</h3>
      <p className="text-gray-600 mb-6 max-w-md mx-auto">{description}</p>
      {action && (
        <Button onClick={action.onClick}>
          {action.label}
        </Button>
      )}
    </Card>
  );
}

// ============================================================================
// ERROR ALERT
// ============================================================================

interface ErrorAlertProps {
  error: Error | null;
  onRetry?: () => void;
}

export function ErrorAlert({ error, onRetry }: ErrorAlertProps) {
  if (!error) return null;

  return (
    <Alert variant="destructive" className="mb-6">
      <AlertCircle className="h-4 w-4" />
      <AlertTitle>Error</AlertTitle>
      <AlertDescription className="flex items-center justify-between">
        <span>{error.message || 'Something went wrong. Please try again.'}</span>
        {onRetry && (
          <Button variant="outline" size="sm" onClick={onRetry} className="ml-4">
            Retry
          </Button>
        )}
      </AlertDescription>
    </Alert>
  );
}

// ============================================================================
// LOADING STATES
// ============================================================================

export function PageLoader() {
  return (
    <div className="flex items-center justify-center min-h-[400px]">
      <div className="text-center">
        <div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent mb-4" />
        <p className="text-gray-600">Loading...</p>
      </div>
    </div>
  );
}

export function TableSkeleton({ rows = 5, cols = 4 }) {
  return (
    <div className="space-y-3">
      {[...Array(rows)].map((_, i) => (
        <div key={i} className="flex gap-4">
          {[...Array(cols)].map((_, j) => (
            <Skeleton key={j} className="h-12 flex-1" />
          ))}
        </div>
      ))}
    </div>
  );
}

// ============================================================================
// SECTION HEADER
// ============================================================================

interface SectionHeaderProps {
  title: string;
  description?: string;
  action?: React.ReactNode;
}

export function SectionHeader({ title, description, action }: SectionHeaderProps) {
  return (
    <div className="flex items-center justify-between mb-6">
      <div>
        <h2 className="text-2xl font-bold">{title}</h2>
        {description && <p className="text-gray-600 mt-1">{description}</p>}
      </div>
      {action && <div>{action}</div>}
    </div>
  );
}

// ============================================================================
// USAGE EXAMPLES
// ============================================================================

/*
// Example 1: Metrics Grid
<StatsGrid stats={{
  totalEvents: 42,
  totalRevenue: 125000,
  totalBookings: 350,
  revenueChange: 12.5
}} />

// Example 2: Data Table
<DataTable
  data={events}
  columns={[
    { key: 'title', label: 'Event Title' },
    { key: 'date', label: 'Date' },
    {
      key: 'status',
      label: 'Status',
      render: (value) => <StatusBadge status={value} />
    },
  ]}
  onEdit={(event) => console.log('Edit', event)}
  onDelete={(event) => console.log('Delete', event)}
/>

// Example 3: Empty State
<EmptyState
  icon={<CalendarDays className="h-16 w-16" />}
  title="No events yet"
  description="Get started by creating your first event"
  action={{
    label: "Create Event",
    onClick: () => navigate('/events/new')
  }}
/>

// Example 4: Error with Retry
<ErrorAlert
  error={error}
  onRetry={() => refetch()}
/>
*/

```

### resources/supabase-patterns.ts

```typescript
// Supabase Query Patterns for event-studio

import { supabase } from '@/integrations/supabase/client';

// ============================================================================
// BASIC QUERIES
// ============================================================================

// Select all
const { data } = await supabase.from('events').select('*');

// Select specific columns
const { data } = await supabase.from('events').select('id, title, start_date');

// Single row
const { data } = await supabase.from('events').select('*').eq('id', id).single();

// ============================================================================
// FILTERS
// ============================================================================

// Equality
const { data } = await supabase.from('events').select('*').eq('status', 'active');

// Multiple conditions (AND)
const { data } = await supabase
  .from('events')
  .select('*')
  .eq('status', 'active')
  .eq('visibility', 'public');

// OR conditions
const { data } = await supabase
  .from('events')
  .select('*')
  .or('status.eq.draft,status.eq.active');

// Date range
const { data } = await supabase
  .from('events')
  .select('*')
  .gte('start_date', startDate)
  .lte('start_date', endDate);

// Text search (case-insensitive)
const { data } = await supabase
  .from('events')
  .select('*')
  .ilike('title', `%${query}%`);

// IN filter
const { data } = await supabase
  .from('events')
  .select('*')
  .in('category', ['music', 'tech']);

// ============================================================================
// JOINS & RELATIONSHIPS
// ============================================================================

// One-to-many
const { data } = await supabase
  .from('events')
  .select('*, bookings(id, total_amount, status)')
  .eq('id', eventId)
  .single();

// Many-to-one (foreign key)
const { data } = await supabase
  .from('events')
  .select(`
    *,
    organizer:users!organizer_id (id, full_name, email)
  `)
  .eq('id', eventId)
  .single();

// Multiple levels
const { data } = await supabase
  .from('events')
  .select(`
    *,
    organizer:users!organizer_id (id, full_name),
    bookings (
      id,
      total_amount,
      user:users!user_id (full_name, email)
    )
  `);

// ============================================================================
// AGGREGATIONS
// ============================================================================

// Count
const { count } = await supabase
  .from('events')
  .select('*', { count: 'exact', head: true });

// Count with filter
const { count } = await supabase
  .from('events')
  .select('*', { count: 'exact', head: true })
  .eq('status', 'active');

// Sum (manual aggregation)
const { data } = await supabase
  .from('bookings')
  .select('total_amount')
  .eq('status', 'confirmed');

const total = data.reduce((sum, b) => sum + (b.total_amount || 0), 0);

// ============================================================================
// ORDERING & PAGINATION
// ============================================================================

// Order by
const { data } = await supabase
  .from('events')
  .select('*')
  .order('start_date', { ascending: false });

// Pagination
const from = page * pageSize;
const to = from + pageSize - 1;

const { data, count } = await supabase
  .from('events')
  .select('*', { count: 'exact' })
  .range(from, to)
  .order('created_at', { ascending: false });

// Limit
const { data } = await supabase
  .from('events')
  .select('*')
  .order('created_at', { ascending: false })
  .limit(10);

// ============================================================================
// INSERT, UPDATE, DELETE
// ============================================================================

// Insert
const { data } = await supabase
  .from('events')
  .insert({ title: 'New Event', status: 'draft' })
  .select()
  .single();

// Update
const { data } = await supabase
  .from('events')
  .update({ status: 'active' })
  .eq('id', id)
  .select()
  .single();

// Delete
const { error } = await supabase.from('events').delete().eq('id', id);

// Upsert
const { data } = await supabase
  .from('events')
  .upsert({ id, title: 'Updated' })
  .select()
  .single();

// ============================================================================
// REALTIME SUBSCRIPTIONS
// ============================================================================

// Subscribe to changes
const channel = supabase
  .channel('events-changes')
  .on(
    'postgres_changes',
    { event: '*', schema: 'public', table: 'events' },
    (payload) => {
      console.log('Change:', payload);
    }
  )
  .subscribe();

// Cleanup
supabase.removeChannel(channel);

// ============================================================================
// FILE STORAGE
// ============================================================================

// Upload
const { data } = await supabase.storage
  .from('event-images')
  .upload(`${eventId}/${file.name}`, file);

// Get public URL
const { data: { publicUrl } } = supabase.storage
  .from('event-images')
  .getPublicUrl(path);

// Delete
await supabase.storage.from('event-images').remove([path]);

// ============================================================================
// AUTHENTICATION
// ============================================================================

// Get current user
const { data: { user } } = await supabase.auth.getUser();

// User's events
const { data } = await supabase
  .from('events')
  .select('*')
  .eq('organizer_id', user.id);

// ============================================================================
// COMPLEX QUERIES
// ============================================================================

// Dashboard data (parallel queries)
const [eventsResult, bookingsResult, revenueResult] = await Promise.all([
  supabase.from('events').select('*', { count: 'exact', head: true }),
  supabase.from('bookings').select('*', { count: 'exact', head: true }),
  supabase.from('bookings').select('total_amount').eq('status', 'confirmed'),
]);

// Advanced search with multiple filters
let query = supabase.from('events').select('*');

if (searchTerm) query = query.ilike('title', `%${searchTerm}%`);
if (status) query = query.eq('status', status);
if (category) query = query.eq('category', category);
if (startDate) query = query.gte('start_date', startDate);
if (endDate) query = query.lte('end_date', endDate);

const { data } = await query;

```

### resources/layout-examples.tsx

```tsx
// Dashboard Layout Examples for event-studio

import React, { useState } from 'react';
import Sidebar from '@/components/Sidebar';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Search, Plus, Filter, Download, Calendar } from 'lucide-react';

// ============================================================================
// 1. STANDARD DASHBOARD WITH SIDEBAR
// ============================================================================

export function StandardDashboardLayout() {
  return (
    <div className="flex min-h-screen bg-gray-50">
      <Sidebar />
      <main className="flex-1 p-8">
        <h1 className="text-3xl font-bold mb-6">Dashboard</h1>

        {/* Metrics Grid */}
        <div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
          {/* Metric cards */}
        </div>

        {/* Main Content */}
        <Card className="p-6">{/* Content */}</Card>
      </main>
    </div>
  );
}

// ============================================================================
// 2. DASHBOARD WITH ACTIONS BAR
// ============================================================================

export function DashboardWithActionsBar() {
  const [searchQuery, setSearchQuery] = useState('');

  return (
    <div className="flex min-h-screen bg-gray-50">
      <Sidebar />
      <main className="flex-1 p-8">
        {/* Header */}
        <div className="flex items-center justify-between mb-6">
          <div>
            <h1 className="text-3xl font-bold">Events</h1>
            <p className="text-gray-600">Manage your events</p>
          </div>
          <Button>
            <Plus className="h-4 w-4 mr-2" />
            Create Event
          </Button>
        </div>

        {/* Actions Bar */}
        <div className="flex flex-col sm:flex-row gap-4 mb-6">
          <div className="relative flex-1 max-w-md">
            <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
            <Input
              placeholder="Search..."
              className="pl-10"
              value={searchQuery}
              onChange={(e) => setSearchQuery(e.target.value)}
            />
          </div>
          <div className="flex gap-2">
            <Button variant="outline">
              <Filter className="h-4 w-4 mr-2" />
              Filters
            </Button>
            <Button variant="outline">
              <Download className="h-4 w-4 mr-2" />
              Export
            </Button>
          </div>
        </div>

        {/* Content */}
        <Card className="p-6">{/* Table */}</Card>
      </main>
    </div>
  );
}

// ============================================================================
// 3. TABBED DASHBOARD
// ============================================================================

export function TabbedDashboard() {
  return (
    <div className="flex min-h-screen bg-gray-50">
      <Sidebar />
      <main className="flex-1 p-8">
        <h1 className="text-3xl font-bold mb-6">Analytics</h1>

        <Tabs defaultValue="overview" className="space-y-6">
          <TabsList>
            <TabsTrigger value="overview">Overview</TabsTrigger>
            <TabsTrigger value="revenue">Revenue</TabsTrigger>
            <TabsTrigger value="bookings">Bookings</TabsTrigger>
          </TabsList>

          <TabsContent value="overview">
            <div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
              {/* Metrics */}
            </div>
            <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
              <Card className="p-6">{/* Chart 1 */}</Card>
              <Card className="p-6">{/* Chart 2 */}</Card>
            </div>
          </TabsContent>

          <TabsContent value="revenue">{/* Revenue content */}</TabsContent>
          <TabsContent value="bookings">{/* Bookings content */}</TabsContent>
        </Tabs>
      </main>
    </div>
  );
}

// ============================================================================
// 4. SPLIT VIEW (LIST + DETAIL)
// ============================================================================

export function SplitViewDashboard() {
  const [selectedId, setSelectedId] = useState<string | null>(null);

  return (
    <div className="flex min-h-screen bg-gray-50">
      <Sidebar />

      <div className="flex-1 flex">
        {/* List Panel */}
        <div className="w-96 border-r bg-white p-6 overflow-y-auto">
          <h2 className="text-xl font-bold mb-4">Events</h2>
          <Input placeholder="Search..." className="mb-4" />

          {/* List items */}
          <Card
            className="p-4 cursor-pointer hover:bg-gray-50 mb-2"
            onClick={() => setSelectedId('1')}
          >
            <h3 className="font-semibold">Event Title</h3>
            <p className="text-sm text-gray-600">Date & Location</p>
          </Card>
        </div>

        {/* Detail Panel */}
        <div className="flex-1 p-8 overflow-y-auto">
          {selectedId ? (
            <>
              <h1 className="text-3xl font-bold mb-6">Event Details</h1>
              <Card className="p-6">{/* Details */}</Card>
            </>
          ) : (
            <div className="flex items-center justify-center h-full text-gray-500">
              Select an event to view details
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// ============================================================================
// 5. GRID VIEW DASHBOARD
// ============================================================================

export function GridViewDashboard() {
  const [view, setView] = useState<'grid' | 'list'>('grid');

  return (
    <div className="flex min-h-screen bg-gray-50">
      <Sidebar />
      <main className="flex-1 p-8">
        {/* Header with View Toggle */}
        <div className="flex items-center justify-between mb-6">
          <h1 className="text-3xl font-bold">Events</h1>
          <div className="flex gap-4">
            <Button
              variant={view === 'grid' ? 'default' : 'outline'}
              onClick={() => setView('grid')}
            >
              Grid
            </Button>
            <Button
              variant={view === 'list' ? 'default' : 'outline'}
              onClick={() => setView('list')}
            >
              List
            </Button>
            <Button>
              <Plus className="h-4 w-4 mr-2" />
              New Event
            </Button>
          </div>
        </div>

        {/* Filters */}
        <div className="flex gap-4 mb-6">
          <Input placeholder="Search..." className="max-w-sm" />
          <Button variant="outline">
            <Filter className="h-4 w-4 mr-2" />
            Filters
          </Button>
        </div>

        {/* Grid or List View */}
        {view === 'grid' ? (
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
            <Card className="overflow-hidden">
              <img
                src="https://via.placeholder.com/400x200"
                alt="Event"
                className="w-full h-48 object-cover"
              />
              <div className="p-4">
                <h3 className="font-semibold mb-2">Event Title</h3>
                <p className="text-sm text-gray-600">Date & Location</p>
              </div>
            </Card>
          </div>
        ) : (
          <Card className="p-6">{/* List/table view */}</Card>
        )}
      </main>
    </div>
  );
}

// ============================================================================
// 6. RESPONSIVE MOBILE-FIRST
// ============================================================================

export function ResponsiveDashboard() {
  return (
    <div className="flex min-h-screen bg-gray-50">
      {/* Desktop Sidebar - Hidden on mobile */}
      <div className="hidden lg:block">
        <Sidebar />
      </div>

      <main className="flex-1 p-4 sm:p-6 lg:p-8">
        {/* Page Header - Responsive */}
        <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6 gap-4">
          <div>
            <h1 className="text-2xl sm:text-3xl font-bold">Dashboard</h1>
            <p className="text-sm sm:text-base text-gray-600">Welcome back!</p>
          </div>
          <Button className="w-full sm:w-auto">
            <Plus className="h-4 w-4 mr-2" />
            Create
          </Button>
        </div>

        {/* Metrics - Responsive Grid */}
        <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6 mb-6">
          {/* Metric cards */}
        </div>

        {/* Content - Stack on mobile, grid on desktop */}
        <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
          <Card className="lg:col-span-2 p-4 sm:p-6">{/* Main content */}</Card>
          <Card className="p-4 sm:p-6">{/* Sidebar content */}</Card>
        </div>
      </main>
    </div>
  );
}

// ============================================================================
// 7. STICKY HEADER DASHBOARD
// ============================================================================

export function StickyHeaderDashboard() {
  return (
    <div className="flex min-h-screen bg-gray-50">
      <Sidebar />

      <div className="flex-1 flex flex-col">
        {/* Sticky Header */}
        <header className="sticky top-0 z-10 bg-white border-b p-6 shadow-sm">
          <div className="flex items-center justify-between">
            <div>
              <h1 className="text-2xl font-bold">Events Dashboard</h1>
              <p className="text-sm text-gray-600">{new Date().toLocaleDateString()}</p>
            </div>
            <div className="flex gap-3">
              <Button variant="outline">
                <Download className="h-4 w-4 mr-2" />
                Export
              </Button>
              <Button>
                <Plus className="h-4 w-4 mr-2" />
                New Event
              </Button>
            </div>
          </div>
        </header>

        {/* Scrollable Content */}
        <main className="flex-1 p-8 overflow-y-auto">
          <div className="space-y-6">{/* Content sections */}</div>
        </main>
      </div>
    </div>
  );
}

```

Building Frontend Dashboards | SkillHub