03 — admin_console_R architecture¶
A guided tour.
Folder map¶
admin_console_R/
├── index.html ← Vite entry; loads /src/main.tsx
│ includes cdn.gpteng.co/gptengineer.js (Lovable.dev runtime)
├── vite.config.ts ← dev port 8080, alias @ → src/, HMR overlay disabled
├── eslint.config.js ← ESLint 9 flat config
├── tailwind.config.ts ← darkMode 'class'; SF Pro font stack
├── tsconfig.json + .app.json + .node.json
├── components.json ← shadcn CLI config
├── postcss.config.js ← autoprefixer + tailwind
├── package.json
├── package-lock.json ← THE canonical lockfile (npm)
├── yarn.lock ← present but not used
├── bun.lockb ← present but not used
├── public/ ← logo, dashboard.jpg, other static assets
│
└── src/
├── main.tsx ← React 18 createRoot bootstrap
├── App.tsx ← router + providers (QueryClient, AuthProvider, TooltipProvider, Toasters)
├── index.css ← global Tailwind + CSS variables (design tokens)
├── App.css ← leftover, mostly empty
├── vite-env.d.ts
│
├── components/
│ ├── auth/
│ │ ├── LoginForm.tsx ← react-hook-form + zod login form
│ │ └── ProtectedRoute.tsx ← guards authenticated routes
│ ├── layout/
│ │ ├── AppLayout.tsx ← sidebar + main-content shell
│ │ └── Sidebar.tsx ← nav items + logout + account switcher
│ ├── shared/ ← small reusable bits
│ └── ui/ ← 50+ shadcn primitives (Button, Card, Dialog, …)
│
├── config/
│ └── env.ts ← typed env-var accessor (ENV object)
│
├── context/
│ └── AuthContext.tsx ← user/token state, login/logout/checkAuth (~150 lines)
│
├── hooks/
│ ├── useAuth.tsx ← convenience wrapper around AuthContext
│ ├── use-mobile.tsx
│ └── use-toast.ts ← shadcn toast hook
│
├── lib/
│ └── utils.ts ← cn() helper for Tailwind class composition
│
├── pages/
│ ├── Login.tsx ← mounts LoginForm
│ ├── Index.tsx
│ ├── Accounts.tsx + Accounts.css ← probably the largest page (tables + filters + modals)
│ ├── AffiliateMarketing.tsx
│ ├── CustomerService.tsx
│ ├── MyProfile.tsx
│ ├── NotFound.tsx
│ ├── Personnel.tsx
│ └── Prompts.tsx
│
└── services/
└── api.ts ← 184 lines — fetch wrapper + ~25 named API methods
Entry point — src/main.tsx and src/App.tsx¶
main.tsx (tiny):
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
createRoot(document.getElementById('root')!).render(<App />);
App.tsx composes the provider stack and declares routes:
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<BrowserRouter>
<AuthProvider>
<Toaster /> {/* shadcn */}
<Sonner /> {/* sonner */}
<Routes>
<Route path="/" element={<Navigate to="/login" replace />} />
<Route path="/login" element={<Login />} />
<Route path="/accounts" element={<ProtectedRoute><AppLayout><Accounts /></AppLayout></ProtectedRoute>} />
<Route path="/personnel" element={<ProtectedRoute><AppLayout><Personnel /></AppLayout></ProtectedRoute>} />
{/* ... ~10 routes ... */}
<Route path="*" element={<NotFound />} />
</Routes>
</AuthProvider>
</BrowserRouter>
</TooltipProvider>
</QueryClientProvider>
The provider order matters: QueryClient is outermost so children can useQuery; AuthProvider is innermost (before Routes) so all pages can useAuth.
ProtectedRoute → AppLayout → Page¶
This is the canonical wrap for any authenticated page.
// src/components/auth/ProtectedRoute.tsx
function ProtectedRoute({ children }: { children: ReactNode }) {
const { user, isLoading } = useAuth();
if (isLoading) return <Spinner />;
if (!user) return <Navigate to="/login" replace />;
return <>{children}</>;
}
// src/components/layout/AppLayout.tsx
function AppLayout({ children }: { children: ReactNode }) {
return (
<div className="flex">
<Sidebar />
<main>{children}</main>
</div>
);
}
Every protected page repeats <ProtectedRoute><AppLayout><Page /></AppLayout></ProtectedRoute>. Verbose. Don't refactor this as your first PR — it's working and the explicitness has value.
Auth flow¶
1. User opens / → Navigate to /login
2. /login renders <Login /> → mounts <LoginForm />
3. LoginForm calls api.login(email, password) (in src/services/api.ts)
4. api.login POSTs to ENV.API_URL/authenticate (or /webauthenticate — verify which)
5. On success: token + user are stored in localStorage; AuthContext state updates
6. AuthContext's user is no longer null → ProtectedRoute renders the page
7. On every subsequent API call, services/api.ts adds Authorization: Bearer <token>
The checkAuth() function in AuthContext.tsx rehydrates the token from localStorage on app mount, so a refresh keeps the user logged in.
TanStack Query pattern¶
// In a page component:
const { data: accounts, isLoading, error } = useQuery({
queryKey: ['accounts', filters],
queryFn: () => getAccounts(filters),
});
const mutation = useMutation({
mutationFn: updateAccount,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['accounts'] });
toast.success('Account updated');
},
});
This is your default for any backend data. Keys are conventional (singular noun + filters).
Pages — keep them thin¶
Pages are mostly:
- One or two
useQuerycalls - A handful of
useStatefor local UI state (modal open / filters) - A shadcn
<Card>/<Table>to render - Action handlers that call mutations
pages/Accounts.tsx is probably the largest (table + filters + edit modal). The rest are smaller. Try to keep new pages similarly small — extract reusable bits into components/shared/.
Sidebar / navigation¶
src/components/layout/Sidebar.tsx lists nav items + the logout button + an account switcher.
Nav items should be role-gated:
const { user } = useAuth();
{user?.role === ENV.SUPER_ADMIN && <NavItem to="/personnel">Personnel</NavItem>}
Roles come from ENV.SUPER_ADMIN, ENV.ADMIN, etc. — which come from VITE_* env vars. Verify alignment with the BE before assuming a role number.
services/api.ts — the API surface¶
Single file, ~25 named functions:
export async function getAccounts(filters?: AccountFilters): Promise<Account[]> { ... }
export async function getAccount(id: number): Promise<Account> { ... }
export async function updateAccount(account: Account): Promise<Account> { ... }
export async function getPersonnel(): Promise<Personnel[]> { ... }
// ...
Each function:
1. Builds the URL with ${ENV.API_URL}/...
2. Adds Authorization: Bearer <token> and hardcoded Apptype: btoa('admin-console')
3. fetch(), parses JSON, returns typed result
Junior gotcha: the
Apptypeis hardcoded — known finding F-2. Don't add new endpoints with the same hardcoded value if there's a way to useENV.APP_TYPE. But verify backend behaviour first.
Where to put a new file¶
| Adding | Put it in |
|---|---|
| A new page | src/pages/<Name>.tsx, then register in App.tsx's <Routes> (wrapped in ProtectedRoute → AppLayout) |
| A new API method | src/services/api.ts — extend the existing pattern; type the return |
| A new shared component | src/components/shared/<Name>.tsx |
| A new shadcn primitive | npx shadcn-ui@latest add <component> — places it in src/components/ui/ |
| A new hook | src/hooks/use<Name>.tsx |
| A new utility | src/lib/<utility>.ts |
| A new context | src/context/<Name>Context.tsx, then wrap in App.tsx |
| A new sidebar nav item | src/components/layout/Sidebar.tsx — add role gating |
Where to start when building a new feature¶
- Backend API: check
../../audit/Someli-admin-api/API-inventory.mdfor the endpoint, or coordinate with the BE. - Type: add the TS type for the response (in
src/services/api.tsor a newsrc/types/<feature>.ts). - API method: add the fetch function in
src/services/api.ts. - Page: create
src/pages/<Feature>.tsx. - Route: add
<Route path="/<feature>" element={<ProtectedRoute><AppLayout><Feature /></AppLayout></ProtectedRoute>} />inApp.tsx. - Nav: add the nav item to
Sidebar.tsxwith role gating. - Tests: none (no test suite). Manual verify.