Skip to content

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 useQuery calls
  • A handful of useState for 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/.


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 Apptype is hardcoded — known finding F-2. Don't add new endpoints with the same hardcoded value if there's a way to use ENV.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

  1. Backend API: check ../../audit/Someli-admin-api/API-inventory.md for the endpoint, or coordinate with the BE.
  2. Type: add the TS type for the response (in src/services/api.ts or a new src/types/<feature>.ts).
  3. API method: add the fetch function in src/services/api.ts.
  4. Page: create src/pages/<Feature>.tsx.
  5. Route: add <Route path="/<feature>" element={<ProtectedRoute><AppLayout><Feature /></AppLayout></ProtectedRoute>} /> in App.tsx.
  6. Nav: add the nav item to Sidebar.tsx with role gating.
  7. Tests: none (no test suite). Manual verify.

Next

04-getting-started.md.