Appearance
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 dependencyRule: Where does a component/hook live?
| Scope | Location |
|---|---|
| Used in 2+ pages | src/components/ or src/hooks/ |
| Used in 1 page only | src/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 APIState Layers
| Layer | Tool | Persisted |
|---|---|---|
| Server cache | React Query | Memory |
| Auth | Zustand useAuthStore | localStorage (hermina360-auth) |
| Theme | Zustand useThemeStore | localStorage (hermina360-theme) |
| Language | Zustand useLanguageStore | localStorage (hermina360-lang) |
| Forms | Ant Design Form | Component |
| Local UI | useState | Component |
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 listsRoute Guards
PrivateRoute— requiresisLoggedIn, else redirects/loginPublicOnlyRoute— for auth pages, redirects/dashboard/executiveif logged in
Adding a New Dashboard Page
- Create
src/pages/dashboard/YourPage.tsx - Follow the 4-zone layout from DESIGN_SYSTEM.md
- Import colors from
@/lib/colors— never hardcode hex - Add a route in
src/routes/index.tsx - Add menu item in
src/components/layout/dashboard/DashboardLayout.tsx→HERMINA_MENU_ITEMS - Test colorblind accessibility (Chrome DevTools → Rendering → Emulate vision deficiencies)
Naming Conventions
| Item | Pattern | Example |
|---|---|---|
| Components | PascalCase | BranchTable.tsx |
| Hooks | use prefix camelCase | useBranchesMutator.ts |
| CRUD hooks | use[Resource]Mutator | useBranchesMutator.ts |
| Read-only hooks | use[Resource]Data | useBranchDetailData.ts |
| Stores | use[Domain]Store | useAuthStore.ts |
| Types | PascalCase | ApiResponse<T> |
| Utils | camelCase | fetchApi.ts |
| Pages | PascalCase + Page | ExecSummaryPage.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';