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:
- Schema Definition (Zod)
- Form Setup (React Hook Form)
- Image Upload (EntityMultiImageUploader)
- 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:
- DATABASE_ARCHITECTURE.md - Database schema
- SUBMISSION_FLOW.md - Submission workflow
- AUTHENTICATION.md - Auth implementation