Files
thrilltrack-explorer/docs/FRONTEND_ARCHITECTURE.md
gpt-engineer-app[bot] bcba0a4f0c Add documentation to files
2025-10-21 14:41:42 +00:00

15 KiB

Frontend Architecture

Complete documentation of ThrillWiki's React frontend architecture, routing, state management, and UI patterns.


Technology Stack

  • Framework: React 18.3 with TypeScript
  • Build Tool: Vite
  • Styling: Tailwind CSS with custom design system
  • UI Components: shadcn/ui (Radix UI primitives)
  • Routing: React Router v6
  • Data Fetching: TanStack Query (React Query)
  • Forms: React Hook Form with Zod validation
  • State Machines: Custom TypeScript state machines
  • Icons: lucide-react

Routing Structure

URL Pattern Standards

Parks:

/parks/                         → Global park list
/parks/{parkSlug}/             → Individual park detail
/parks/{parkSlug}/rides/       → Park's ride list
/operators/{operatorSlug}/parks/ → Operator's parks
/owners/{ownerSlug}/parks/      → Owner's parks

Rides:

/rides/                        → Global ride list
/parks/{parkSlug}/rides/{rideSlug}/ → Ride detail (nested under park)
/manufacturers/{manufacturerSlug}/rides/ → Manufacturer's rides
/manufacturers/{manufacturerSlug}/models/ → Manufacturer's models
/designers/{designerSlug}/rides/ → Designer's rides

Admin:

/admin/                        → Admin dashboard
/admin/moderation/             → Moderation queue
/admin/reports/                → Reports queue
/admin/users/                  → User management
/admin/system-log/             → System activity log
/admin/blog/                   → Blog management
/admin/settings/               → Admin settings

Route Configuration

// src/App.tsx
<BrowserRouter>
  <Routes>
    {/* Public routes */}
    <Route path="/" element={<Index />} />
    <Route path="/parks" element={<Parks />} />
    <Route path="/parks/:slug" element={<ParkDetail />} />
    <Route path="/parks/:parkSlug/rides" element={<ParkRides />} />
    <Route path="/parks/:parkSlug/rides/:rideSlug" element={<RideDetail />} />
    
    {/* Admin routes - auth guard applied in page component */}
    <Route path="/admin" element={<AdminDashboard />} />
    <Route path="/admin/moderation" element={<AdminModeration />} />
    
    {/* Auth routes */}
    <Route path="/auth" element={<Auth />} />
    <Route path="/auth/callback" element={<AuthCallback />} />
    
    {/* 404 */}
    <Route path="*" element={<NotFound />} />
  </Routes>
</BrowserRouter>

Component Architecture

Component Organization

src/components/
├── admin/              # Admin forms and management tools
│   ├── ParkForm.tsx
│   ├── RideForm.tsx
│   ├── UserManagement.tsx
│   └── ...
├── auth/               # Authentication components
│   ├── AuthModal.tsx
│   ├── MFAChallenge.tsx
│   ├── TOTPSetup.tsx
│   └── ...
├── moderation/         # Moderation queue components
│   ├── ModerationQueue.tsx
│   ├── SubmissionReviewManager.tsx
│   ├── QueueFilters.tsx
│   └── ...
├── parks/              # Park display components
│   ├── ParkCard.tsx
│   ├── ParkGrid.tsx
│   ├── ParkFilters.tsx
│   └── ...
├── rides/              # Ride display components
├── upload/             # Image upload components
├── versioning/         # Version history components
├── layout/             # Layout components
│   ├── Header.tsx
│   ├── Footer.tsx
│   └── AdminLayout.tsx
├── common/             # Shared components
│   ├── LoadingGate.tsx
│   ├── ProfileBadge.tsx
│   └── SortControls.tsx
└── ui/                 # shadcn/ui base components
    ├── button.tsx
    ├── dialog.tsx
    └── ...

Page Components

Top-level route components that:

  • Handle auth guards (admin pages use useAdminGuard)
  • Fetch initial data with React Query
  • Implement layout structure
  • Pass data to feature components
// Example: AdminModeration.tsx
export function AdminModeration() {
  const { loading } = useAdminGuard('moderator');
  
  if (loading) return <LoadingGate isLoading />;
  
  return (
    <AdminLayout>
      <AdminPageLayout
        title="Moderation Queue"
        description="Review and approve submissions"
      >
        <ModerationQueue />
      </AdminPageLayout>
    </AdminLayout>
  );
}

Feature Components

Domain-specific components with business logic.

// Example: ModerationQueue.tsx
export function ModerationQueue() {
  const {
    items,
    isLoading,
    filters,
    pagination,
    handleAction,
  } = useModerationQueueManager();
  
  return (
    <div>
      <QueueFilters {...filters} />
      <QueueStats items={items} />
      {items.map(item => (
        <QueueItem 
          key={item.id} 
          item={item}
          onAction={handleAction}
        />
      ))}
      <QueuePagination {...pagination} />
    </div>
  );
}

State Management

React Query (TanStack Query)

Used for ALL server state (data fetching, caching, mutations).

Configuration:

// App.tsx
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
      refetchOnMount: true,
      refetchOnReconnect: true,
      retry: 1,
      staleTime: 30000,      // 30s fresh
      gcTime: 5 * 60 * 1000, // 5min cache
    },
  },
});

<QueryClientProvider client={queryClient}>
  <App />
</QueryClientProvider>

Custom Hook Pattern:

// src/hooks/useEntityVersions.ts
export function useEntityVersions(entityType: EntityType, entityId: string) {
  const query = useQuery({
    queryKey: ['versions', entityType, entityId],
    queryFn: () => fetchVersions(entityType, entityId),
    enabled: !!entityId,
  });
  
  const mutation = useMutation({
    mutationFn: rollbackVersion,
    onSuccess: () => {
      queryClient.invalidateQueries(['versions', entityType, entityId]);
    },
  });
  
  return { 
    versions: query.data, 
    isLoading: query.isLoading,
    rollback: mutation.mutate 
  };
}

State Machines

Used for complex workflows with strict transitions.

1. moderationStateMachine.ts - Moderation workflow

type ModerationState =
  | { status: 'idle' }
  | { status: 'claiming'; itemId: string }
  | { status: 'locked'; itemId: string; lockExpires: string }
  | { status: 'loading_data'; itemId: string; lockExpires: string }
  | { status: 'reviewing'; itemId: string; lockExpires: string; reviewData: SubmissionItemWithDeps[] }
  | { status: 'approving'; itemId: string }
  | { status: 'rejecting'; itemId: string }
  | { status: 'complete'; itemId: string; result: 'approved' | 'rejected' }
  | { status: 'error'; itemId: string; error: string }
  | { status: 'lock_expired'; itemId: string };

type ModerationAction =
  | { type: 'CLAIM_ITEM'; payload: { itemId: string } }
  | { type: 'LOCK_ACQUIRED'; payload: { lockExpires: string } }
  | { type: 'LOAD_DATA' }
  | { type: 'DATA_LOADED'; payload: { reviewData: SubmissionItemWithDeps[] } }
  | { type: 'START_APPROVAL' }
  | { type: 'START_REJECTION' }
  | { type: 'COMPLETE'; payload: { result: 'approved' | 'rejected' } }
  | { type: 'ERROR'; payload: { error: string } }
  | { type: 'LOCK_EXPIRED' }
  | { type: 'RESET' };

function moderationReducer(
  state: ModerationState,
  action: ModerationAction
): ModerationState {
  // ... transition logic with guards
}

// Usage in SubmissionReviewManager.tsx
const [state, dispatch] = useReducer(moderationReducer, { status: 'idle' });

2. deletionDialogMachine.ts - Account deletion wizard

type DeletionStep = 'warning' | 'confirm' | 'code';

type DeletionDialogState = {
  step: DeletionStep;
  confirmationCode: string;
  codeReceived: boolean;
  loading: boolean;
  error: string | null;
};

// Usage in AccountDeletionDialog.tsx

React Hook Form

Used for ALL forms with Zod validation.

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const parkFormSchema = z.object({
  name: z.string().min(1, 'Name required').max(255),
  slug: z.string().regex(/^[a-z0-9-]+$/),
  park_type: z.enum(['theme_park', 'amusement_park', 'water_park']),
  opening_date: z.string().optional(),
  // ... all fields validated
});

export function ParkForm() {
  const form = useForm({
    resolver: zodResolver(parkFormSchema),
    defaultValues: initialData,
  });

  const onSubmit = async (data: ParkFormData) => {
    await submitParkCreation(data, user.id);
    // Goes to moderation queue, NOT direct DB write
  };
  
  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        {/* form fields */}
      </form>
    </Form>
  );
}

Context Providers

1. AuthProvider - Global auth state

// src/hooks/useAuth.tsx
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [session, setSession] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    supabase.auth.getSession().then(({ data: { session } }) => {
      setSession(session);
      setUser(session?.user ?? null);
      setLoading(false);
    });
    
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (_event, session) => {
        setSession(session);
        setUser(session?.user ?? null);
      }
    );
    
    return () => subscription.unsubscribe();
  }, []);
  
  return (
    <AuthContext.Provider value={{ user, session, loading }}>
      {children}
    </AuthContext.Provider>
  );
}

2. ThemeProvider - Light/dark mode

// src/components/theme/ThemeProvider.tsx
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('system');
  
  useEffect(() => {
    const root = window.document.documentElement;
    root.classList.remove('light', 'dark');
    
    if (theme === 'system') {
      const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches 
        ? 'dark' 
        : 'light';
      root.classList.add(systemTheme);
    } else {
      root.classList.add(theme);
    }
  }, [theme]);
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

3. LocationAutoDetectProvider - Auto-detect measurement system

// src/components/providers/LocationAutoDetectProvider.tsx
export function LocationAutoDetectProvider() {
  useEffect(() => {
    const detectLocation = async () => {
      const { data } = await supabase.functions.invoke('detect-location');
      if (data?.country) {
        const system = getMeasurementSystemFromCountry(data.country);
        // Update user preferences
      }
    };
    
    detectLocation();
  }, []);
  
  return null;
}

UI/UX Patterns

Design System

  • Colors: HSL-based semantic tokens in index.css
  • Typography: Custom font scale with Tailwind classes
  • Spacing: Consistent spacing scale (4px, 8px, 16px, etc.)
  • Components: shadcn/ui with custom variants
  • Responsive: Mobile-first, breakpoints at sm, md, lg, xl

Color Tokens (index.css):

:root {
  --background: 0 0% 100%;
  --foreground: 222.2 84% 4.9%;
  --primary: 222.2 47.4% 11.2%;
  --primary-foreground: 210 40% 98%;
  --secondary: 210 40% 96.1%;
  --muted: 210 40% 96.1%;
  --accent: 210 40% 96.1%;
  --destructive: 0 84.2% 60.2%;
  --border: 214.3 31.8% 91.4%;
  --input: 214.3 31.8% 91.4%;
  --ring: 222.2 84% 4.9%;
  --radius: 0.5rem;
}

.dark {
  --background: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;
  /* ... dark mode tokens */
}

Reusable UI Components

AdminPageLayout - Wraps admin pages

<AdminPageLayout
  title="User Management"
  description="Manage users"
  onRefresh={handleRefresh}
  refreshMode="auto"
  pollInterval={30000}
>
  {children}
</AdminPageLayout>

LoadingGate - Handles loading/error states

<LoadingGate 
  isLoading={loading} 
  error={error}
  variant="skeleton"
>
  <Content />
</LoadingGate>

ProfileBadge - User display with role badges

<ProfileBadge
  username="john"
  displayName="John Doe"
  role="moderator"
  showRole
  size="md"
/>

Form Patterns

Entity Forms

All entity forms follow this structure:

  1. Schema Definition (Zod)
  2. Form Setup (React Hook Form)
  3. Image Upload (EntityMultiImageUploader)
  4. Submit Handler (entitySubmissionHelpers)
// 1. Schema
const parkFormSchema = z.object({
  name: z.string().min(1).max(255),
  slug: z.string().regex(/^[a-z0-9-]+$/),
  // ... all fields
});

// 2. Form
const form = useForm({
  resolver: zodResolver(parkFormSchema),
  defaultValues: initialData || defaultValues,
});

// 3. Submit
const onSubmit = async (data: ParkFormData) => {
  if (isEditing) {
    await submitParkUpdate(parkId, data, user.id);
  } else {
    await submitParkCreation(data, user.id);
  }
  toast({ title: "Submitted for review" });
};

Image Upload

const [imageAssignments, setImageAssignments] = useState<ImageAssignments>({
  banner: null,
  card: null,
  uploaded: []
});

<EntityMultiImageUploader
  value={imageAssignments}
  onChange={setImageAssignments}
  entityType="park"
  maxImages={20}
/>

Custom Hooks

Data Fetching Hooks

// src/hooks/useEntityVersions.ts
export function useEntityVersions(entityType, entityId)

// src/hooks/useModerationQueue.ts
export function useModerationQueue(filters, pagination)

// src/hooks/useProfile.tsx
export function useProfile(userId)

// src/hooks/useUserRole.ts
export function useUserRole()

Utility Hooks

// src/hooks/useDebounce.ts
export function useDebounce(value, delay)

// src/hooks/useMobile.tsx
export function useMobile()

// src/hooks/useUnitPreferences.ts
export function useUnitPreferences()

Guard Hooks

// src/hooks/useAdminGuard.ts
export function useAdminGuard(requiredRole: AppRole = 'moderator')

// src/hooks/useRequireMFA.ts
export function useRequireMFA()

Performance Optimizations

React Query Caching

// Aggressive caching for static data
const { data: parks } = useQuery({
  queryKey: ['parks'],
  queryFn: fetchParks,
  staleTime: 5 * 60 * 1000, // 5 minutes
  gcTime: 10 * 60 * 1000,   // 10 minutes
});

// Shorter caching for dynamic data
const { data: queue } = useQuery({
  queryKey: ['moderation', 'queue', filters],
  queryFn: fetchQueue,
  staleTime: 10000,  // 10 seconds
  gcTime: 60000,     // 1 minute
  refetchInterval: 30000, // Auto-refresh every 30s
});

Code Splitting

// Lazy load admin pages
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
const AdminModeration = lazy(() => import('./pages/AdminModeration'));

<Suspense fallback={<LoadingGate isLoading />}>
  <Route path="/admin" element={<AdminDashboard />} />
</Suspense>

Memoization

// Expensive computations
const filteredItems = useMemo(() => {
  return items.filter(item => 
    item.status === statusFilter &&
    item.entity_type === typeFilter
  );
}, [items, statusFilter, typeFilter]);

// Callback stability
const handleAction = useCallback((itemId: string, action: string) => {
  performAction(itemId, action);
}, [performAction]);

See Also: