05.01 — Stack de l'Igoristan

05.01 — Stack de l’Igoristan#

package.json#

{
  "react": "^19.2.3",
  "react-dom": "^19.2.3",
  "vike": "^0.4.258",
  "vike-react": "^0.6.21",
  "valibot": "^1.2.0",
  "react-hook-form": "^7.71.1",
  "@hookform/resolvers": "^5.2.2",
  "react-dropzone": "^14.3.8",
  "lucide-react": "^0.562.0",
  "usehooks-ts": "^3.1.1",
  "throttleit": "^2.1.0",
  "uuid": "^13.0.0",
  "clsx": "^2.1.1",
  "class-variance-authority": "^0.7.1",
  "tailwind-merge": "^3.0.2",
  "tailwindcss-animate": "^1.0.7",
  "@radix-ui/react-slot": "^1.2.4"
}
LibRôle
React 19.2UI, hooks
Vike 0.4 + vike-reactSSG / SSR framework au-dessus de Vite (anciennement « vite-plugin-ssr »)
Valibot 1.2Validation de schémas TS (alternative légère à Zod)
react-hook-formGestion de formulaires
react-dropzoneComposant drag-and-drop pour upload
lucide-reactIcones
usehooks-tsHooks utilitaires (useLocalStorage, etc.), utilisé par useAuth
throttleitThrottle
uuidGénération d’IDs
clsx + tailwind-merge + class-variance-authorityUtilitaires Tailwind

devDependencies#

{
  "vite": "^7.3.1",
  "@vitejs/plugin-react": "^5.1.2",
  "@tailwindcss/vite": "^4.1.3",
  "@tailwindcss/postcss": "^4.0.17",
  "tailwindcss": "^4.1.1",
  "terser": "^5.39.0",
  "vite-plugin-compression": "^0.5.1",
  "rollup-plugin-visualizer": "^5.14.0",
  "@qalisa/vike-plugin-sitemap": "^1.7.0",

  "typescript": "^6.0.3",
  "typescript-eslint": "^8.59.0",
  "eslint": "^9.18.0",
  "@eslint/js": "^9.39.2",
  "eslint-plugin-import-x": "^4.6.1",
  "eslint-plugin-perfectionist": "^4.6.0",
  "eslint-plugin-react": "^7.37.4",
  "eslint-plugin-react-hooks": "^5.2.0",
  "eslint-plugin-unused-imports": "^4.2.1",
  "globals": "^15.15.0",

  "prettier": "^3.5.3",
  "prettier-plugin-tailwindcss": "^0.6.11",

  "husky": "^9.1.7",
  "lint-staged": "^15.5.0",
  "commitizen": "^4.3.1",
  "@commitlint/config-conventional": "^19.8.0",
  "@commitlint/cz-commitlint": "^19.8.0",
  "is-ci": "^4.1.0",
  "editorconfig": "^2.0.1",
  "minimatch": "^10.1.1",
  "rimraf": "^6.0.1",
  "wireit": "^0.14.11"
}

engines#

{
  "node": "^24.x",
  "pnpm": "11.x"
}

→ Node 24, pnpm 11. packageManager: pnpm@11.x verrouille la version.

05.02 — Les routes de l'Igoristan

05.02 — Les routes de l’Igoristan#

src/config/routes.ts#

const __ROOT = "/igoristan/";
const DASHBOARD = "dashboard";

const __ROUTES = {
  DONKEY_SAUSAGE_DETECTOR: "donkey-sausage-eater-detector",
  DASHBOARD_NESTED: `${DASHBOARD}/nested`,
  RANDOM_LOADERS: "random-loaders",
  SACRED_UPLOAD: "sacred-upload",
  RANDOM_ERROR: "random-error",
  CHAOTIC_FORM: "chaotic-form",
  CORSICAMON: "corsicamon",
  MADNESS: "madness",
  DASHBOARD,
  HOME: "",
} as const satisfies Routes;

const ROUTES = createRoutes(__ROUTES, __ROOT);
RouteURLRôleChaos volontaire
HOME/igoristan/Page d’accueil avec liens vers les autres pages, autoplay audio — 
RANDOM_LOADERS/igoristan/random-loadersLoaders pseudo-aléatoiresLoaders qui restent affichés un temps variable
SACRED_UPLOAD/igoristan/sacred-uploadFormulaire d’upload react-dropzone — 
DASHBOARD/igoristan/dashboardLogin (mot de passe figatellu), MFA OTP optionneluseAuth peut échouer 10% du temps même avec le bon mdp
DASHBOARD_NESTED/igoristan/dashboard/nestedPage protégée, requiert MFA — 
CORSICAMON/igoristan/corsicamon« Pokédex corse », 3 picks aléatoires via API key1/5 chance de lever Error('lol') artificiellement
RANDOM_ERROR/igoristan/random-errorPage d’erreur facticeTitre matché par ERROR_PAGE_REGEX côté tests
CHAOTIC_FORM/igoristan/chaotic-formFormulaire instableÉléments .catch-me-if-you-can apparaissent aléatoirement
MADNESS/igoristan/madnessHistoires alternées (Cors, ThisIsBastia)Rend l’une OU l’autre au hasard
DONKEY_SAUSAGE_DETECTOR/igoristan/donkey-sausage-eater-detectorDétecteur de sales siciliens de merde30% d’échec aléatoire (BSOD-style)

Page par page#

const links = [
  { href: ROUTES.RANDOM_LOADERS, label: "Random loaders" },
  { href: ROUTES.SACRED_UPLOAD, label: "Sacred upload" },
  { href: ROUTES.DASHBOARD, label: "Dashboard" },
  { href: ROUTES.CORSICAMON, label: "Corsicamon" },
  { href: ROUTES.RANDOM_ERROR, label: "Random Error" },
  { href: ROUTES.CHAOTIC_FORM, label: "Chaotic form" },
  { href: ROUTES.MADNESS, label: "Madness" },
  { href: ROUTES.DONKEY_SAUSAGE_DETECTOR, label: "Donkey Sausage Detector" },
];

home#

8 liens vers les autres pages + autoplay audio Angelus de Jérusalem (amen 🙏).

05.03 — Le faux useAuth + MFA OTP

05.03 — Le faux useAuth + MFA OTP#

Le hook useAuth de l’Igoristan est volontairement faux. Il introduit deux mécaniques pour exercer le rejeu et la coordination des workers d’Ocarina : un échec aléatoire de 10% et un MFA OTP optionnel.

Code#

// src/hooks/useAuth.ts
const VALID_PASSWORD = "figatellu";
const NOT_AUTHENTICATED = -1;
const AUTHENTICATED_WITHOUT_MFA = 1;
const AUTHENTICATED_WITH_MFA = 2;

// * ... This is FAKED
export const useAuth = () => {
  const [_isAuthenticated, setIsAuthenticated] = useLocalStorage(
    "isAuthenticated",
    NOT_AUTHENTICATED,
  );
  const [isLoading, setIsLoading] = useState(true);

  const isAuthenticated = useCallback(
    () => _isAuthenticated !== NOT_AUTHENTICATED,
    [_isAuthenticated],
  );
  const isAuthenticatedWithMFA = useCallback(
    () => _isAuthenticated === AUTHENTICATED_WITH_MFA,
    [_isAuthenticated],
  );
  const logout = useCallback(
    () => setIsAuthenticated(NOT_AUTHENTICATED),
    [setIsAuthenticated],
  );

  useEffect(() => {
    const delay = 500 * randint(1, 5); // ◄── délai aléatoire 0.5-2.5s
    const timer = setTimeout(() => setIsLoading(false), delay);
    return () => clearTimeout(timer);
  }, []);

  const login: LoginFn = ({ pre = false, password, withMFA }) => {
    if (password === VALID_PASSWORD && Math.random() < 0.9) {
      // ◄── 10% d'échec malgré bon mdp
      if (!pre) {
        setIsAuthenticated(
          withMFA ? AUTHENTICATED_WITH_MFA : AUTHENTICATED_WITHOUT_MFA,
        );
      }
      return true;
    }
    return false;
  };

  return { isAuthenticatedWithMFA, isAuthenticated, isLoading, logout, login };
};

Mécaniques#

1. LocalStorage#

const [_isAuthenticated, setIsAuthenticated] = useLocalStorage(
  "isAuthenticated",
  NOT_AUTHENTICATED,
);

Trois valeurs : -1 (pas authentifié), 1 (sans MFA), 2 (avec MFA).
Persistées dans localStorage, faciles d’accès pour du test/debug manuel.

05.04 — Composants UI clés

05.04 — Composants UI clés#

Les composants src/components/ et leur rôle dans le terrain de jeu.

Listing#

src/components/
├── BackToHome.tsx              # bouton de retour à HOME
├── BackToSicily.tsx            # variante
├── Button.tsx                  # button stylisé Tailwind
├── ChaoticForm.tsx             # ⭐ formulaire avec éléments .catch-me-if-you-can
├── Dropzone.tsx                # ⭐ react-dropzone wrapper pour SACRED_UPLOAD
├── ErrorCode.tsx               # composant affichant un code HTTP (utilisé par RANDOM_ERROR)
├── FakeUpload/                 # composant fake d'upload (animation, pas de vrai POST)
├── InternalErrorMsg.tsx        # message d'erreur générique
├── Label.tsx                   # label de formulaire
├── Link.tsx                    # wrapper de <a> avec styles
├── Loading.tsx                 # spinner générique
├── LoginForm.tsx               # ⭐ formulaire login + MFA OTP
├── PlusButton.tsx              # bouton + (corsicamon : ajouter à la collection)
├── RandomBibleVerse.tsx        # verset biblique aléatoire
├── RandomLoader.tsx            # ⭐ loader random
├── UploadPreview.tsx           # preview d'un fichier uploadé
└── XButton.tsx                 # bouton X de fermeture

⭐ = composant clé pour les scénarios Ocarina.

05.05 — Pipelines wireit

05.05 — Pipelines wireit#

wireit est un orchestrateur de scripts NPM avec cache incrémental. L’Igoristan l’utilise pour enchaîner format-check, lint, typecheck, prebuild, build, dev, preview, le tout avec un cache fin par tâche.

package.json#wireit#

"wireit": {
  "ci:format-check": {
    "command": "prettier . --check",
    "files": ["*.{js,ts,jsx,tsx,json,md,mdx,html,css,scss,yml,yaml}"],
    "output": []
  },
  "clean:dist": {
    "command": "rimraf dist",
    "files": ["dist/"],
    "output": []
  },
  "prebuild": {
    "files": ["src/**/*.*", "package.json", "packages/**/*.*", "packages/**/package.json", "vite.config.ts"],
    "dependencies": ["lint", "typecheck"]
  },
  "preview": {
    "command": "vike preview",
    "dependencies": ["build"]
  },
  "dev": {
    "command": "vike dev",
    "files": ["src/**/*.*", "packages/**/*.*"],
    "dependencies": ["prebuild"],
    "output": ["dist/"],
    "packageLocks": ["pnpm-lock.yaml"]
  },
  "build": {
    "command": "vike build",
    "files": ["src/**/*.*", "packages/**/*.*"],
    "dependencies": ["prebuild"],
    "output": ["dist/"],
    "packageLocks": ["pnpm-lock.yaml"]
  },
  "lint": {
    "command": "eslint \"./{src,packages}/**/*.{js,jsx,ts,tsx}\" --max-warnings 0 --cache",
    "files": ["src/**/*.*", "package.json", "packages/**/*.*", "eslint.config.ts"],
    "output": []
  },
  "typecheck": {
    "command": "tsc --build --pretty tsconfig.json",
    "files": ["src/**/*.*", "package.json", "tsconfig.app.json", "tsconfig.json", "tsconfig.node.json"],
    "output": ["typecheck-dist/"]
  }
}

DAG#

                 ┌─────────────────┐
                 │ ci:format-check │ ────► prettier . --check
                 └─────────────────┘
                 ┌─────────────────┐
                 │     lint        │ ────► eslint (max-warnings 0, --cache)
                 └─────┬───────────┘
                       │
                 ┌─────▼───────────┐
                 │   typecheck     │ ────► tsc --build --pretty
                 └─────┬───────────┘
                       │
                 ┌─────▼───────────┐
                 │   prebuild      │ ────► deps: [lint, typecheck]
                 └─────┬───────────┘
                       │
            ┌──────────┴──────────┐
            ▼                     ▼
   ┌─────────────────┐    ┌─────────────────┐
   │   build         │    │   dev           │
   │   vike build    │    │   vike dev      │
   └────┬────────────┘    └─────────────────┘
        ▼
   ┌─────────────────┐
   │   preview       │
   │   vike preview  │
   └─────────────────┘

Avec pnpm dev :

05.06 — Husky + lint-staged + commitlint + commitizen

05.06 — Husky + lint-staged + commitlint + commitizen#

Discipline de commits stricte. Conventionnels, signés, format-checkés.

Outils#

OutilRôle
HuskyInstalle les hooks Git locaux (pre-commit, commit-msg)
lint-stagedLance des outils seulement sur les fichiers staged
commitlintVérifie que le message de commit suit la convention
commitizen + cz-commitlintCLI interactive pour composer un message conforme

Config#

.husky/#

Dossier contenant les hook scripts :

.husky/
├── pre-commit
└── commit-msg

pre-commit :

05.07 — CI/CD de l'Igoristan

05.07 — CI/CD de l’Igoristan#

Deux workflows : ci-pr.yml sur PR (3 jobs parallèles), deploy.yml sur push main (GitHub Pages).

ci-pr.yml#

name: CI/PR

on:
  pull_request:
    branches: ["**"]
  workflow_dispatch:

defaults:
  run:
    shell: bash

jobs:
  format-check:
    runs-on: ubuntu-latest
    env:
      WIREIT_CACHE: github
    steps:
      - uses: actions/checkout@v6
      - uses: pnpm/action-setup@v5
        with: { version: 11 }
      - uses: actions/setup-node@v6
        with: { node-version: 24, cache: "pnpm" }
      - uses: google/wireit@setup-github-actions-caching/v2
      - name: Install project
        run: make install
      - name: Formatcheck
        run: make ci:format-check

  lint:
    runs-on: ubuntu-latest
    env:
      WIREIT_CACHE: github
    steps:
      # ... idem ...
      - uses: actions/cache@v5
        with:
          path: .eslintcache
          key: eslint-cache-${{ hashFiles('eslint.config.ts', 'src/**/*.*', 'package.json') }}
      - name: Lint
        run: pnpm lint

  typecheck:
    runs-on: ubuntu-latest
    env:
      WIREIT_CACHE: github
    steps:
      # ... idem ...
      - name: Typecheck
        run: pnpm typecheck
JobCommandeCache
format-checkpnpm exec prettier . --check (via wireit)wireit + node_modules
lintpnpm exec eslint ... --max-warnings 0 --cachewireit + .eslintcache (actions/cache)
typechecktsc --build --pretty tsconfig.jsonwireit + tsbuildinfo
                          PR opened / push
                                  │
                ┌─────────────────┼─────────────────┐
                ▼                 ▼                 ▼
        ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
        │ format-check │ │     lint     │ │  typecheck   │
        │ (1-2 minutes)│ │ (1-2 minutes)│ │ (1-2 minutes)│
        └──────────────┘ └──────────────┘ └──────────────┘
                │                 │                 │
                └─────────────────┴─────────────────┘
                                  ▼
                             PR mergeable

deploy.yml#

name: Deploy to GitHub Pages

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: pnpm/action-setup@v5
        with: { version: 11 }
      - uses: actions/setup-node@v6
        with: { node-version: 24, cache: "pnpm" }
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
      - name: Build
        run: pnpm build
      - name: Upload Pages artifact
        uses: actions/upload-pages-artifact@v4
        with: { path: dist/client }

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - id: deployment
        uses: actions/deploy-pages@v5
JobEffet
buildpnpm install --frozen-lockfile → pnpm build (= wireit build = lint → typecheck → vike build) → upload dist/client
deployactions/deploy-pages@v5 → publie l’artifact → URL = https://mojo-molotov.github.io/igoristan/

concurrency: pages + cancel-in-progress: false#

concurrency:
  group: "pages"
  cancel-in-progress: false

Empêche deux déploiements simultanés (race condition).
Le second attend (cancel-in-progress: false → on ne cancel pas le run en cours).