Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# web-react-skeleton — Copilot instructions

Vite + React 18 + TypeScript SPA, styled with MUI v6 (`@mui/material` + `@mui/material-pigment-css`), routed by React Router v6, state via Zustand, forms via Yup, i18n via i18next (locales generated from a Google Sheet). Deployed as static files to Azure Storage `$web` + Azure CDN. Package manager: **Yarn 4** (`packageManager: yarn@4.5.0`). Node: **22.x** in CI, `20.11.1` in the local Docker dev container.

This file is an orientation + index. **It does not restate any rule** — every convention lives in a scoped `.instructions.md` file (see the table below). If a rule seems missing here, load the matching scoped file.

## Repo layout

```
frontend/ React app (Vite, all source, all rules)
terraform/ HCL infra (per-env tfvars in env/)
azure-pipeline/ Azure Pipelines templates (root: azure-pipelines.yml)
doc/ Human docs (Azure setup notes)
.github/instructions/ <-- scoped instruction files (this table)
```

Working directory for every yarn command is `frontend/`.

## Commands

| Command | Purpose |
|---|---|
| `yarn dev` | Vite dev server (default port from `.env` `VITE_PORT`). |
| `yarn build` | Type-check (`tsc -b`) then production build. |
| `yarn preview` | Serve the built `dist/` for smoke-testing. |
| `yarn lint` | Runs all linters + Prettier write. Use before any commit. |
| `yarn lint:scripts` | ESLint on `.ts` / `.tsx`. |
| `yarn lint:styles` | Stylelint on `.scss`. |
| `yarn lint:editor` | eclint on `src/app`. |
| `yarn sheet2i18n` | Regenerate `assets/locales/{en,fr}.json` from the Google Sheet. Never hand-edit those JSONs. |

## Instruction files

Load the ones matching the task. Multi-scope tasks (e.g. "add a new authenticated page with a form that calls an API") legitimately need several at once.

| File | `applyTo` scope | Load when… |
|---|---|---|
| [typescript.instructions.md](instructions/typescript.instructions.md) | `frontend/src/**/*.{ts,tsx}` | Any TS work — naming (`I`/`E` prefix), interfaces, path aliases, strictness. |
| [react.instructions.md](instructions/react.instructions.md) | `frontend/src/**/*.tsx` | Writing a component, hooks, JSX composition. |
| [components-vs-containers.instructions.md](instructions/components-vs-containers.instructions.md) | `frontend/src/app/{components,containers}/**/*.{ts,tsx}` | Deciding which folder a new piece of UI belongs in. |
| [mui-and-uikit.instructions.md](instructions/mui-and-uikit.instructions.md) | `frontend/src/app/components/**`, `pages/uikit/**`, `themes/**`, `vite.config.ts` | Wrapping an MUI primitive; registering a component in the UiKit page. |
| [styling.instructions.md](instructions/styling.instructions.md) | `frontend/src/**/*.{scss,tsx}`, `themes/**/*.ts` | Any styling decision — utility classes, `styled()`, `sx`, CSS Modules, SCSS. |
| [icons.instructions.md](instructions/icons.instructions.md) | `frontend/src/app/icons/**/*.{ts,tsx}` | Adding or editing an inline-SVG icon. |
| [routing-and-auth.instructions.md](instructions/routing-and-auth.instructions.md) | `frontend/src/app/{routes,pages,hocs}/**/*.{ts,tsx}` | Adding a page, a route, or touching auth / `EPermission`. |
| [forms.instructions.md](instructions/forms.instructions.md) | `frontend/src/app/forms/**/*.{ts,tsx}` | Building a form (Yup schema + controlled state + `FieldHelperText`). |
| [services-api.instructions.md](instructions/services-api.instructions.md) | `frontend/src/app/services/**/*.{ts,tsx}` | Adding an Axios call or a new service domain. |
| [state-stores.instructions.md](instructions/state-stores.instructions.md) | `frontend/src/app/stores/**/*.ts` | Adding or consuming a Zustand store. |
| [i18n.instructions.md](instructions/i18n.instructions.md) | `frontend/src/**/*.tsx`, `assets/locales/**/*.json`, `shared/i18n.ts`, `sheet2i18n.config.cjs` | Anything user-facing text or the `sheet2i18n` workflow. |
| [infra.instructions.md](instructions/infra.instructions.md) | `azure-pipelines.yml`, `azure-pipeline/**`, `terraform/**`, `frontend/{docker-compose.yml,entrypoint.sh,example.env}` | CI/CD, Terraform, per-env variables, local Docker dev. |

## Repo-wide invariants

Rules that span every scope. Violating any of these breaks the project's shape.

- **Yarn 4 only.** Never `npm install`, `pnpm`, `bun`, or ad-hoc `npx`. `yarn.lock` is the lock file.
- **One CI/CD system: Azure Pipelines.** Do not add GitHub Actions, CircleCI, or any parallel pipeline.
- **One router: React Router v6.** No TanStack Router, Next.js router, or hand-rolled routing.
- **One state manager: Zustand (v4).** No Redux, MobX, Jotai, Recoil, or Context-as-store.
- **One form/validation stack: Yup + controlled `useState`.** No `react-hook-form`, `formik`, `final-form`.
- **One i18n stack: i18next + `sheet2i18n`.** Locale JSONs are generated — never hand-edited.
- **One styling stack: MUI v6 + Pigment-CSS.** No Tailwind, no Emotion runtime, no styled-components.
- **No icon library.** Every icon is a hand-wrapped SVG under `@icons/*` — never import from `@mui/icons-material` (it's in `package.json` for historical reasons but has zero imports).
- **No production Dockerfile.** The app deploys as static files; Docker is dev-only.
- **Path aliases everywhere** (`@components`, `@containers`, `@forms`, `@hocs`, `@icons`, `@pages`, `@routes`, `@services`, `@shared`, `@stores`, `@styles`, `@assets`, `@enums`). Never use relative imports across folders.
- **Never hand-edit generated files:** `assets/locales/*.json` (regenerated by `sheet2i18n`), `dist/*` (build output), `yarn.lock` (managed by Yarn).
- **Strict TypeScript.** Do not weaken `strict`, `noUnusedLocals`, `noUnusedParameters`, or `noFallthroughCasesInSwitch`. No `// @ts-ignore` without a linked follow-up.
210 changes: 210 additions & 0 deletions .github/instructions/components-vs-containers.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
---
description: How to decide between a component and a container, and the folder/import rules for each.
applyTo: "frontend/src/app/{components,containers}/**/*.{ts,tsx}"
---

# Components vs Containers

Scope: everything under `@components` and `@containers`. This file exists because the two folders look similar but exist for opposite reasons — mixing them is the fastest way to make a piece of UI un-reusable.

Complementary files (do not repeat their content here):
- [react.instructions.md](react.instructions.md) — how to write the component itself, hooks, JSX.
- [typescript.instructions.md](typescript.instructions.md) — naming, interfaces, path aliases.
- [styling.instructions.md](styling.instructions.md) — SCSS/BEM/utility classes.
- [mui-and-uikit.instructions.md](mui-and-uikit.instructions.md) — MUI wrappers and uikit registration.
- [forms.instructions.md](forms.instructions.md) — the third sibling folder alongside components and containers.
- [state-stores.instructions.md](state-stores.instructions.md) — which layers may import `@stores/*`.

---

## The one-sentence rule

- **Component (`@components`)** = **how things look**. Reusable, dumb, no knowledge of the app.
- **Container (`@containers`)** = **how things work**. Owns state, talks to stores/services/router, composes components.

If you find yourself importing a store, service, or router hook inside `@components`, you are writing a container in the wrong folder. Move it.

---

## Decision checklist

Answer top-to-bottom. First "yes" wins.

1. Does it read/write a Zustand store? → **container**
2. Does it call a service (axios) or `localStorage` / `sessionStorage`? → **container**
3. Does it use `useNavigate`, `useParams`, `useLocation`? → **container** (or a page).
4. Does it fetch data on mount, or run business logic in `useEffect`? → **container**
5. Does it need to know about the current user, permissions, cookie consent, or environment? → **container**
6. Would a design-system Storybook page render it without any providers besides theme/i18n? → **component**
7. Can it be reused on multiple pages, receiving different data via props? → **component**

If the answer is genuinely "I don't know", start as a component. Promoting a component to a container is easy; extracting a component out of a container is harder.

---

## What belongs in `@components/`

Presentational only. Every folder under `@components/*` in the repo follows this shape.

**Allowed:**
- `useState` for **UI-only** state (open/closed, focused, hovered, controlled input value passed back via `onChange`).
- MUI primitives, styled components (see `mui-and-uikit.instructions.md`).
- Props typed as `I<Component> extends <MuiProps>` (see [react.instructions.md](react.instructions.md)).
- `useTranslation` **only if** the component owns the translation of its own hard-coded text (rare — usually labels come from the parent per the "translate in parent" rule). Prefer receiving pre-translated strings as props.

**Not allowed:**
- Imports from `@stores`, `@services`, `@routes`, `@pages`, `@forms`, `@containers`, `@hocs`.
- `useNavigate`, `useLocation`, `useParams`.
- `localStorage`, `sessionStorage`, `document.cookie`, `window.location` mutations.
- Reading `import.meta.env` or `__ENV__` / `__API_URL__` / other Vite globals.
- `useEffect` that performs I/O or navigation.
- Business logic (validation rules, permission checks, currency math).

Each component folder holds **one** component:

```
components/
<name>/
<Name>.tsx
<name>.scss <-- optional, colocated (see styling instructions)
```

New MUI wrappers must be registered in the UiKit page (`@pages/uikit`) — see [mui-and-uikit.instructions.md](mui-and-uikit.instructions.md).

---

## What belongs in `@containers/`

Stateful orchestration. Wraps children, feeds them data, handles side effects.

**Expected to do:**
- Own non-trivial state (`useState`, `useReducer`) and pass it down.
- Talk to Zustand stores (`useUserStore`, etc.).
- Call services (`postLogin`, `getMe`), handle their promises, translate errors to `toast`.
- Navigate (`useNavigate`), read the URL (`useParams`), react to route changes.
- Read `localStorage` / cookies / env vars.
- Compose presentational components — usually **no markup of its own** beyond a fragment or a thin wrapping `div`.

**Expected NOT to do:**
- Introduce new styled components or `.scss` files for a look that could be a reusable component. If the visual is worth reusing, extract a component under `@components/`.
- Contain deep JSX trees full of styling. If the container has more than a couple of `div`s doing layout, that layout probably belongs in a component.

A container typically reads `localStorage` and a store, calls a service, navigates on failure, and renders either a loading state or its children — with zero styling of its own.

### Container folder layout

Containers can grow their own private subtree. The full pattern:

```
containers/
<containerName>/
<ContainerName>.tsx <-- the container (default export)
<containerName>.config.ts <-- static config
<containerName>Helper.ts <-- pure helpers (I/O + parsing)
interfaces/
I<Model>.ts <-- domain interfaces used by this container
<subComponentName>/
<SubComponentName>.tsx <-- private presentational sub-component
<sub-component-name>.module.css
```

Rules that pattern encodes:
- The container is the **only** thing exported for external use.
- Sub-components live in **lowerCamelCase subfolders** with a **PascalCase file**, and are imported only by their parent container. If a sub-component becomes reusable outside the container, move it to `@components/` and delete the private copy.
- Colocated helpers (`*Helper.ts`), config (`*.config.ts`), and `interfaces/` are the norm. Do not create parallel top-level folders under `@shared` for container-scoped concerns.
- Interfaces exported from a container are still `I`-prefixed and one-per-file (see [typescript.instructions.md](typescript.instructions.md)).

---

## Containers vs Pages

`@pages` is a third folder that people often confuse with `@containers`.

| | Container | Page |
|---|---|---|
| Purpose | Reusable stateful piece of UI | A route target |
| Registered in `routes.ts` | No | Yes |
| Wrapped by `withAuth` HOC when protected | No | Yes (via `withAuth<Page>.tsx`) |
| Lazy-loaded | No | Yes (`lazy(() => import(...))`) |
| Can be rendered by multiple pages | Yes | No |

Rule of thumb: if the thing has a URL, it's a page. If it's mounted by a page (or by another container), it's a container.

Pages and their `*.route.tsx` / `withAuth<Page>.tsx` files are covered by `routing-and-auth.instructions.md`.

---

## Common mistakes and how to fix them

### 1. "Smart component" — component with a store dep

```tsx
// Bad — components/userGreeting/UserGreeting.tsx
import { useUserStore } from "@stores/userStore";
export default function UserGreeting() {
const { user } = useUserStore();
return <span>Hello, {user?.firstName}</span>;
}
```

Fix: keep the component dumb, move the store read up.

```tsx
// components/userGreeting/UserGreeting.tsx
interface IUserGreeting { name?: string; }
export default function UserGreeting({ name }: IUserGreeting) {
return <span>Hello, {name}</span>;
}

// caller (page or container)
const { user } = useUserStore();
<UserGreeting name={user?.firstName} />
```

### 2. "Layout container" — container with heavy markup

If your container has a `div`-soup that looks like design work, the markup is a component. Keep the container as pure orchestration:

```tsx
// containers/checkout/Checkout.tsx
export default function Checkout() {
const { items, total } = useCartStore();
const onSubmit = useCallback(/* ... */, []);
return <CheckoutSummary items={items} total={total} onSubmit={onSubmit} />;
}

// components/checkoutSummary/CheckoutSummary.tsx — all the JSX + styling
```

### 3. Sub-component that got reused

A private sub-component under `containers/<name>/<subName>/` stays private until a second consumer appears. The moment it does, promote it:

```
components/<subName>/
<SubName>.tsx
<sub-name>.scss
```

...and delete the private copy from the container.

### 4. Business logic inside a component

A component asking "is this user allowed to see the button?" is a container in disguise. The component should receive `showButton: boolean` and render it; the container computes the boolean from `useUserStore()` / `EPermission`.

---

## Import allowlist summary

| From ↓ / To → | `@components` | `@containers` | `@stores` | `@services` | `@routes` / `@pages` | `@hocs` | `@shared` / `@enums` | MUI / third-party |
|---|---|---|---|---|---|---|---|---|
| `@components/*` | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (types, constants only) | ✅ |
| `@containers/*` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |

If an import you need is a ❌ from `@components/*`, the file belongs in `@containers/*` (or `@pages/*`) instead.

---

## Not sure?

Don't take the component vs container separation as a dogma — sometimes the line is hard to draw. Default to a component. Move it to a container only when it starts needing the app to work.
Loading
Loading