# Developer Guide

> See `README.md` for basic setup and deployment. This document covers architecture, patterns, and conventions.

## Table of Contents

1. [Tech Stack](#tech-stack)
2. [Project Structure](#project-structure)
3. [State Management](#state-management)
4. [Routing](#routing)
5. [Service Layer](#service-layer)
6. [Component Conventions](#component-conventions)
7. [Custom Hooks](#custom-hooks)
8. [Permissions (CASL)](#permissions-casl)
9. [Theming](#theming)
10. [Testing](#testing)
11. [Build System](#build-system)
12. [Versioning](#versioning)

---

## Tech Stack

| Concern | Library |
|---|---|
| UI framework | React 19 + TypeScript 5 (strict) |
| Component library | MUI v7 |
| Advanced MUI | DataGrid Pro, DatePickers Pro, TreeView Pro, Charts |
| State | Redux Toolkit 2 + React-Redux 9 |
| Routing | React Router 7 |
| HTTP | Axios |
| Permissions | CASL v6 |
| Rich text | Tiptap v3 (primary), React Quill (legacy) |
| Code editor | CodeMirror 6 via `@uiw/react-codemirror` |
| File uploads | FilePond |
| Drag & drop | dnd-kit |
| Notifications | notistack |
| Date handling | Day.js |
| Analytics | PostHog |
| Error monitoring | Highlight.run |
| Validation | AJV |
| Password strength | zxcvbn-ts |
| Testing | Cypress 14 (component + e2e) |
| Bundler | Webpack 5 + Babel |

The `@/` alias maps to `src/` — use it for all internal imports.

---

## Project Structure

```
src/
├── assets/               # Static assets and global styles
├── common/
│   ├── defaults.ts       # Grid defaults, pagination config
│   ├── enums.ts          # All enums (GridTypes, RouterPaths, LocalStorageKeys, etc.)
│   ├── helpers.ts        # Pure utility functions
│   ├── hooks/            # 29 custom hooks (see Custom Hooks section)
│   ├── local-storage.ts  # localStorage read/write utilities
│   ├── providers/        # Context providers (Breadcrumb, etc.)
│   ├── styles/           # MUI theme config, global style overrides
│   └── types.ts          # ALL shared TypeScript interfaces (single source of truth)
├── components/           # Feature components (see Component Conventions)
├── contexts/
│   ├── ability.ts        # CASL ability builder
│   └── Can.tsx           # AbilityContext + Can component
├── redux/
│   ├── store.ts          # Store assembly + localStorage preloading
│   └── slices/           # One file per slice
├── routes/
│   └── Routes.tsx        # All route definitions (~70 routes)
├── services/             # Axios service wrappers (one per domain)
└── index.tsx             # Entry point

cypress/
├── component/            # Component test specs (*.cy.tsx)
├── e2e/                  # End-to-end specs (*.cy.ts)
├── support/
│   ├── commands.ts       # 50+ custom cy.* commands
│   ├── component.tsx     # Mount helpers (routeAndStoreWrappedMount, etc.)
│   ├── appInit.ts        # Mock user/site data for tests
│   └── test-store.ts     # Redux store for tests
└── config.ts             # Cypress config
```

**Key rule:** All shared TypeScript types live in `src/common/types.ts`. Do not define interfaces in component files.

---

## State Management

### Redux Slices

| Slice | Key | Persisted | Purpose |
|---|---|---|---|
| `appSlice` | `app` | Yes | Theme, drawer, navigation, conditions, tags |
| `dynamicAppSlice` | `dynamic` | No | User, auth, preview availability, grid settings, import/export state |
| `previewSettingsSlice` | `previewSettings` | Yes | Product preview configuration |
| `productGridSlice` | `productGrid` | No | Product grid state |
| `siteSettingSlice` | `siteSettings` | No | Global site settings |
| `contentSlice` | `content` | No | Content/editorial state |

### Persistence

`app` and `previewSettings` are persisted to `localStorage` on initialization. The utilities for this are in `src/common/local-storage.ts` — use `loadStateByKey()` / `saveStateByKey()` there rather than hitting `localStorage` directly. Grid column/filter settings per-user are also persisted via `getGridSettings()` / `saveGridSettings()`.

### Accessing State

Standard `useSelector` / `useDispatch` from React-Redux. The store is typed — `RootState` is exported from `store.ts`.

---

## Routing

Routes are declared in `src/routes/Routes.tsx` using React Router 7. Every protected route is wrapped in `<Protected>`, which checks auth state and redirects to `/admin-portal/login` on 401.

The `RouterPaths` enum in `enums.ts` contains every route path string — use those constants rather than hardcoding strings.

**Breadcrumbs** are managed by `BreadcrumbProvider` (in `src/common/providers/`). Route labels are generated automatically from the route path; override them via the context if a page needs a custom breadcrumb.

---

## Service Layer

All API calls go through `src/services/http-client.ts`, which wraps Axios with:

- Base URL from `window.baseUrl` (set by zeckoShop on page load)
- API key header injected from env
- Credentials included (`withCredentials: true`)
- Global 401 handler — clears Redux state and redirects to login

The client exports `get`, `post`, `put`, `patch`, `del`. Individual service files (e.g. `product-service.ts`, `orders-service.ts`) wrap these with domain-specific typed methods.

**Pattern:**

```typescript
import { get, post } from './http-client'
import { Product } from '@/common/types'

export const getProduct = async (id: number): Promise<Product> => {
    try {
        const res = await get(`api/products/${id}`)
        return res.data
    } catch (e) {
        throw JSON.parse(e as string)
    }
}
```

Use `qs` for query string serialization when building parameterized requests.

---

## Component Conventions

All components are functional, typed, and follow this layout:

```typescript
import { ReactElement } from 'react'

interface Props {
    value: string
    onChange: (value: string) => void
    disabled?: boolean
}

const MyComponent = (props: Props): ReactElement => {
    const { value, onChange, disabled } = props

    return (
        // ...
    )
}

export default MyComponent
```

**Rules enforced by ESLint:**
- Functional components only — no class components
- Return type must be `ReactElement`
- Props passed as `(props: Props)` and destructured inside the body
- No prop spreading
- Self-closing tags for empty elements
- No trailing semicolons
- Single quotes everywhere (JS and JSX)
- Arrow functions throughout
- MUI styling via the `sx` prop — use full object syntax, not shorthand

**File naming:** PascalCase (`UserMenu.tsx`). Hooks start with `use` (`useDebounce.ts`).

**Component directory structure:** Each feature area has its own directory under `src/components/`. Grid-heavy features have a `Grids/` subdirectory. Keep related components co-located.

---

## Custom Hooks

All hooks live in `src/common/hooks/`. Quick reference:

**Data fetching**

| Hook | Purpose |
|---|---|
| `useFetchUserRoles` | Roles list |
| `useFetchCategories` | Category tree |
| `useFetchConditions` / `useFetchConditionGroups` | Condition rules |
| `useFetchManufacturers` | Manufacturer list |
| `useFetchCountries` | Country/province data |
| `useFetchCustomPages` | CMS pages |
| `useFetchCustomFields` | Custom field definitions |
| `useFetchPromotionTypes` | Promotion types |
| `useFetchTags` / `useFetchUserTags` | Tag data |
| `useFetchPreviewSections` | Preview section config |

**Auth / permissions**

| Hook | Purpose |
|---|---|
| `useAbility` | Returns the CASL `Ability` instance from context |
| `useRole` | Returns the current user's role |
| `useIsSuperuser` | Returns boolean superuser status |

**State**

| Hook | Purpose |
|---|---|
| `useSaveState` | Persists a value to Redux + localStorage |
| `useSyncAppState` | Syncs server-side state down to the client store |
| `useCleanupLocalStorage` | Purges stale localStorage keys |
| `useDefaultGridState` | Returns default column/filter state for a grid |
| `useCustomCategoryHierarchyCache` | Memoized category hierarchy |

**Navigation**

| Hook | Purpose |
|---|---|
| `useRoutes` | Available routes for the current user |
| `useGetActiveNavItemName` | Active nav item label |
| `useUnsavedChangesWarning` | Blocks navigation when form is dirty |

**Utilities**

| Hook | Purpose |
|---|---|
| `useDebounce` | Debounces a value |
| `useKeyboardShortcut` | Registers a keyboard shortcut |
| `useGridFilterOperators` | Standard filter operators for DataGrid Pro |
| `useHint` | Controls hint popover display |
| `useAnalytics` / `usePostHogPageTracking` / `useLogPageView` | PostHog wrappers |
| `usePreviewAvailable` | Whether live preview is enabled |
| `useCompareApiVersion` | Checks required vs. current API version |
| `useIsProduction` | Detects APP_ENV |
| `useWhatsNew` | What's New announcement state |

---

## Permissions (CASL)

Authorization uses `@casl/ability` with a custom ability built in `src/contexts/ability.ts`. The ability is seeded from the user's role and permission strings returned by the API on login.

Permission strings follow the format: `action:subject` (e.g. `read:products`, `write:orders`, `delete:filemanager`). Conditions can be embedded as JSON in a third segment.

**In components:**

```typescript
import { useAbility } from '@/common/hooks/useAbility'

const ability = useAbility()

if (ability.can('write', 'products')) {
    // show edit controls
}
```

Or use the `<Can>` component from `src/contexts/Can.tsx` for conditional rendering:

```tsx
import { Can } from '@/contexts/Can'

<Can I='write' a='products'>
    <EditButton />
</Can>
```

Super users bypass all permission checks. Role types are `superuser`, `admin`, and custom roles stored in the system.

---

## Theming

Theme configuration lives in `src/common/styles/theme.ts`, with MUI component overrides in `src/common/styles/material-ui.ts`. Light/dark mode is toggled via `ColorModeContext`.

**Styling rules:**
- Always use the MUI `sx` prop — no inline `style`, no CSS modules, no styled-components
- Full object syntax only — `sx={{ paddingTop: 2 }}` not `sx={{ pt: 2 }}`

---

## Testing

### Setup

Cypress is the only test runner. There are two suites:

- **Component tests** (`cypress/component/**/*.cy.tsx`) — isolated component testing with mocked APIs
- **E2E tests** (`cypress/e2e/**/*.cy.ts`) — full browser flows against a real zeckoShop instance

Copy `example.cypress.env.json` to `cypress.env.json` and fill in credentials from 1Password before running e2e tests.

```bash
npm run test:cypress          # headless component suite (CI)
npx cypress open --component  # interactive
```

### Mount Helpers

`cypress/support/component.tsx` provides several mount wrappers. Pick the right one:

| Helper | Wraps |
|---|---|
| `cy.mount()` | Bare component |
| `cy.routeWrappedMount()` | + React Router |
| `cy.storeWrappedMount()` | + Redux store |
| `cy.snackbarProviderWrappedMount()` | + notistack |
| `cy.routeAndStoreWrappedMount()` | + Router + Redux (most common) |
| `cy.dataRouterWrappedMount()` | + Data Router |

### Custom Commands

All element selection uses `data-cy` attributes. Use `cy.getRef()` to select them:

```typescript
cy.getRef('submit-button').click()
cy.getRef('error-message').should('have.text', 'Required')
```

Other useful commands: `cy.hasText()`, `cy.hasValue()`, `cy.hasTitle()`, `cy.hasTableRow()`, `cy.hasTableRowCount()`, `cy.hasErrorToast()`, `cy.hasSuccessToast()`, `cy.hasTab()`, `cy.displayTabAtIndex()`.

### Test Pattern

```typescript
describe('MyComponent', () => {
    beforeEach(() => {
        cy.intercept('GET', 'api/products*', { fixture: 'products.json' }).as('getProducts')
        cy.routeAndStoreWrappedMount(<MyComponent />)
    })

    it('renders the component', () => {
        cy.getRef('product-list').should('be.visible')
    })

    it('shows an error on failed save', () => {
        cy.intercept('POST', 'api/products', { statusCode: 422 })
        cy.getRef('save-button').click()
        cy.hasErrorToast()
    })
})
```

**Rules:**
- Intercept all API calls in component tests — no real network requests
- Every component gets its own `*.cy.tsx` file
- Tests are permanent — they run in CI, don't mark them as temporary
- Add `data-cy` attributes to new elements as you build them

### Mock Data

`cypress/support/appInit.ts` exports a mock `AppInit` object (super user with full permissions, mock site data). Use it via `cy.setupTest()` or pass it directly to `cy.storeWrappedMount()`.

---

## Build System

Three Webpack configs:

| Config | Used by | Notes |
|---|---|---|
| `webpack.dev.js` | `npm run start` | HMR, source maps, no optimization |
| `webpack.prod.js` | `npm run build` | Vendor chunk split, Brotli compression, Terser minification |
| `webpack.cypress.js` | Cypress | Istanbul instrumentation for coverage |

Output lands in `../dist/admin-portal` (relative to the repo root, inside the zeckoShop install).

**Code splitting:** The production build splits vendor code into a separate chunk for better caching. Webpack handles this automatically — no manual configuration needed when adding dependencies.

**Environment variables** are injected at build time via `dotenv-webpack`. Reference them as `process.env.VARIABLE_NAME`.

**Linting** must pass before a PR:

```bash
npm run lint   # ESLint + Prettier check
```

Prettier will auto-fix format issues; ESLint errors require manual fixes. The config is `eslint.config.mjs` (flat config format).

---

## Versioning

Version is tracked in both `package.json` and `manifest.json`. Always bump both together using:

```bash
gulp patch   # increments patch version in both files
```

`manifest.json` also contains `requiredApiVersion` — update this when the app depends on a new API endpoint or breaking API change, so Deployer and ops know which API version is required.

Branches follow the version number (e.g. `1.0.1.23`). Merging to `main` triggers a Deployer deployment to `assets.zeckoShop.com`.
