Skip to content

Frontend Patterns

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.

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>

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
});

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.

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';

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:

  1. Create your component in src/components/Dashboard/{FeatureName}/
  2. Add state for your data in Dashboard.tsx
  3. Add a useEffect to fetch data when the tab is active
  4. Add the tab to the sidebar in Sidebar.tsx
  5. 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.

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 task
interface Submission { ... } // Thai visa submission
  • Use Tailwind CSS utility classes — never write custom CSS unless for animations or very specific overrides
  • No inline style props 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 uses next-themes for the theme toggle

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');