Skip to content

Architecture

Folder Conventions

src/
├── components/         Reusable UI shared across multiple pages
│   ├── layout/         App shell (AppLayout, DashboardLayout variants)
│   │   └── dashboard/  Hermina dashboard sider (classic dark green)
│   └── charts/         Chart widgets (Line, Bar, Area, Pie)

├── hooks/              Global hooks — shared across pages
│   ├── useFetchApi.ts          Memoized fetch wrapper
│   ├── useQueryFetch.ts        React Query GET hook
│   ├── useMutationSubmit.ts    React Query POST/PUT/PATCH/DELETE hook
│   ├── useDataPagination.ts    URL-synced paginated list hook
│   ├── useHoneyframe.ts        Honeyframe Data API hooks (config, batch, block, filters)
│   ├── useI18n.ts              i18next + Zustand language hook
│   └── useToast.tsx            Ant Design notification hook

├── lib/                Third-party config + Hermina utilities
│   ├── axios.ts        Axios instance with auth interceptor
│   ├── honeyframe.ts   Honeyframe Data API client (read-only BI)
│   ├── i18n.ts         i18next initialization
│   └── colors.ts       Hermina color constants (import from here, never hardcode hex)

├── pages/              One folder per feature / route group
│   ├── LoginPage.tsx
│   ├── NotFoundPage.tsx
│   └── dashboard/                    ← Dashboard page group
│       ├── ExecSummaryPage.tsx       ← Executive Summary (Ringkasan Eksekutif)
│       ├── CfoCockpitPage.tsx        ← CFO Cockpit
│       ├── OperationalPage.tsx       ← Daily Operations
│       └── [FeatureName]Page.tsx     ← Add new pages here
│           components/               ← Components used ONLY in this page
│           hooks/                    ← Hooks used ONLY in this page

├── providers/          React context providers
│   └── StyledThemeProvider.tsx  styled-components <-> Zustand bridge

├── routes/             Route tree + auth guards
├── stores/             Zustand global state — one store per domain
├── styles/             Global CSS + Ant Design theme + styled-components tokens
├── types/
│   ├── api.ts          REST API response shapes
│   └── honeyframe.ts   Honeyframe Data API types
└── utils/              Pure functions — no React dependency

Rule: Where does a component/hook live?

ScopeLocation
Used in 2+ pagessrc/components/ or src/hooks/
Used in 1 page onlysrc/pages/[feature]/components/ or src/pages/[feature]/hooks/
Global state (auth, theme, lang)src/stores/

Data Flow

Two independent data paths:

── REST API path ─────────────────────────────────────────────────
Page Component

use[Resource]Mutator (page-local hook)

useDataPagination / useQueryFetch / useMutationSubmit  (global hooks)

useFetchApi                        (memoized options merger)

fetchApi                           (core — FormData, error normalization)

axios (lib/axios.ts)               (auth header, 401 redirect)

Hermina 360 REST API

── Honeyframe path (read-only BI) ────────────────────────────────
Page Component

useWebappConfig / usePageBatch / useBlockExecute / useFilterOptions

lib/honeyframe.ts                  (fetch client, groups by dashboard_id)

Honeyframe Data API

State Layers

LayerToolPersisted
Server cacheReact QueryMemory
AuthZustand useAuthStorelocalStorage (hermina360-auth)
ThemeZustand useThemeStorelocalStorage (hermina360-theme)
LanguageZustand useLanguageStorelocalStorage (hermina360-lang)
FormsAnt Design FormComponent
Local UIuseStateComponent

useMutator Pattern

When a page has CRUD operations, group all API interactions into a single use[Resource]Mutator hook:

ts
// src/pages/branches/hooks/useBranchesMutator.ts
import { useDataPagination, useMutationSubmit, useToast } from '@/hooks';

function useBranchesMutator({ editingId, onSaveSuccess }) {
  const toast = useToast();

  const { data, pagination, isLoading, searchByQuery, goToPage, refetch } = useDataPagination({
    dataSourceUrl: '/branches',
    generatedParamsKey: { search: ['search'], region: ['region'] },
  });

  const { mutate: saveBranch, isPending: saving } = useMutationSubmit({
    url: editingId ? `/branches/${editingId}` : '/branches',
    method: editingId ? 'PUT' : 'POST',
    onSuccess: () => { toast.success('Branch saved'); refetch(); onSaveSuccess(); },
  });

  return { data, pagination, isLoading, searchByQuery, goToPage, saveBranch, saving };
}

Dashboard Page Layout (4-Zone Pattern)

Every dashboard page MUST follow this structure (see DESIGN_SYSTEM.md):

Zone 1: Hero Metrics     — 1-3 large cards, height ~130-150px
Zone 2: Supporting KPIs  — 3-5 compact tiles, height ~80-100px
Zone 3: Primary Charts   — 1 main + supporting charts
Zone 4: Action Zone      — Branch alerts, filtered lists

Route Guards

  • PrivateRoute — requires isLoggedIn, else redirects /login
  • PublicOnlyRoute — for auth pages, redirects /dashboard/executive if logged in

Adding a New Dashboard Page

  1. Create src/pages/dashboard/YourPage.tsx
  2. Follow the 4-zone layout from DESIGN_SYSTEM.md
  3. Import colors from @/lib/colors — never hardcode hex
  4. Add a route in src/routes/index.tsx
  5. Add menu item in src/components/layout/dashboard/DashboardLayout.tsxHERMINA_MENU_ITEMS
  6. Test colorblind accessibility (Chrome DevTools → Rendering → Emulate vision deficiencies)

Naming Conventions

ItemPatternExample
ComponentsPascalCaseBranchTable.tsx
Hooksuse prefix camelCaseuseBranchesMutator.ts
CRUD hooksuse[Resource]MutatoruseBranchesMutator.ts
Read-only hooksuse[Resource]DatauseBranchDetailData.ts
Storesuse[Domain]StoreuseAuthStore.ts
TypesPascalCaseApiResponse<T>
UtilscamelCasefetchApi.ts
PagesPascalCase + PageExecSummaryPage.tsx

Import Alias

@/src/. Use for all internal imports.

ts
import { useAuthStore } from '@/stores';
import { useQueryFetch } from '@/hooks';
import { BRAND_GREEN, getStatusColor } from '@/lib/colors';
import type { ApiResponse } from '@/types/api';