Frontend Patterns
Auth context
Section titled “Auth context”All authentication state lives in AuthContext. Access it with the useAuth hook:
import { useAuth } from '../contexts/AuthContext';
function MyComponent() { const { user, token, isAuthenticated, login, logout } = useAuth();
// Check role if (user?.role === 'ADMIN') { ... }
// Make an authenticated API call const res = await fetch(`${API_BASE_URL}/api/something`, { headers: { Authorization: `Bearer ${token}` } });}Never read localStorage directly for auth data — always go through useAuth.
Route protection
Section titled “Route protection”Three route wrapper components are defined in App.tsx:
// Requires authentication + optional role check<ProtectedRoute allowedRoles={['ADMIN', 'AGENT']}> <Dashboard /></ProtectedRoute>
// Only shown when NOT authenticated (redirects away if logged in)<PublicRoute> <Login /></PublicRoute>
// No restriction — default for public pages<SiteLayout><Home /></SiteLayout>Making API calls
Section titled “Making API calls”Always use API_BASE_URL from config/api.ts:
import API_BASE_URL from '../config/api';
// GET (authenticated)const data = await fetch(`${API_BASE_URL}/api/tickets`, { headers: { Authorization: `Bearer ${token}` }}).then(r => r.json());
// POST with JSON body (authenticated)const res = await fetch(`${API_BASE_URL}/api/tickets`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ title: 'My Ticket' })});
// POST with file upload (multipart)const formData = new FormData();formData.append('passportPhoto', file);const res = await fetch(`${API_BASE_URL}/api/submissions`, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: formData // Do NOT set Content-Type header — browser sets it with boundary});Notifications (toast)
Section titled “Notifications (toast)”Use sonner for all user feedback:
import { toast } from 'sonner';
toast.success('Ticket submitted successfully');toast.error('Failed to fetch data');toast.loading('Uploading files...');The <Toaster> is mounted once in App.tsx — don’t add it again in individual components.
UI components
Section titled “UI components”The src/components/ui/ folder contains auto-generated shadcn/ui components. Do not edit these files by hand — changes will be overwritten if the component is regenerated. If you need to customize behavior, wrap the component or add a variant via the variants prop pattern.
Import from the component path:
import { Button } from '../ui/button';import { Input } from '../ui/input';import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui/dialog';import { toast } from 'sonner';Dashboard tab pattern
Section titled “Dashboard tab pattern”All dashboard feature tabs follow the same basic shape. The main Dashboard.tsx page holds all state and passes it down as props. Tabs are selected via a state variable (activeTab), and each tab component is rendered conditionally.
When adding a new admin feature tab:
- Create your component in
src/components/Dashboard/{FeatureName}/ - Add state for your data in
Dashboard.tsx - Add a
useEffectto fetch data when the tab is active - Add the tab to the sidebar in
Sidebar.tsx - Conditionally render your component in
Dashboard.tsx
Honest critique: The Dashboard is a single massive component that holds state for every tab simultaneously. This causes all data to be fetched on load even for inactive tabs, and the component is very large. For future features, consider using React Query or SWR for data fetching, or at minimum lazy-load tab content with
React.lazy.
TypeScript types
Section titled “TypeScript types”Shared types live in src/types/dashboard.ts and src/types/ticket.ts. Add new interfaces there rather than defining inline types in component files.
Key types you’ll use frequently:
interface User { id: number; username: string; email: string; role: 'USER' | 'AGENT' | 'ADMIN'; agentType?: string; status: string; kyc_status: string; permissions?: string[]; systems?: string[];}
interface VFSDocument { ... } // VFS tracking taskinterface Submission { ... } // Thai visa submissionStyling conventions
Section titled “Styling conventions”- Use Tailwind CSS utility classes — never write custom CSS unless for animations or very specific overrides
- No inline
styleprops except for dynamic values that can’t be expressed as utilities (e.g., dynamic width percentages) - Dark mode: use Tailwind’s
dark:prefix — the project usesnext-themesfor the theme toggle
Export utility
Section titled “Export utility”src/utils/exportUtils.ts provides a CSV export helper used in the VFS and submission lists. Reuse it for any new tabular data:
import { exportToCSV } from '../utils/exportUtils';
exportToCSV(dataArray, 'filename-prefix');