odoro 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +12 -0
  2. package/client.d.ts +84 -0
  3. package/dist/build-SNTGJH2J.js +4 -0
  4. package/dist/chunk-2YDI5NKV.js +55 -0
  5. package/dist/chunk-G2WW7N4C.js +1872 -0
  6. package/dist/chunk-G5QXVBYT.js +1139 -0
  7. package/dist/chunk-JMEHF3KN.js +35 -0
  8. package/dist/chunk-PEMYUK2D.js +64 -0
  9. package/dist/chunk-T42X2NJN.js +98 -0
  10. package/dist/chunk-T6RLHSCW.js +123 -0
  11. package/dist/chunk-UGRSODHU.js +154 -0
  12. package/dist/cli.d.ts +40 -0
  13. package/dist/cli.js +236 -0
  14. package/dist/commands-FVAHVVVN.js +793 -0
  15. package/dist/commands-V44Y5G4F.js +245 -0
  16. package/dist/create-SH2M722Y.js +288 -0
  17. package/dist/index.d.ts +354 -0
  18. package/dist/index.js +7 -0
  19. package/dist/package-AUINPBEX.js +62 -0
  20. package/dist/preview-LOAO5Y6V.js +3 -0
  21. package/dist/registry/index.d.ts +316 -0
  22. package/dist/registry/index.js +2 -0
  23. package/dist/server-MZ76LPAG.js +3 -0
  24. package/package.json +57 -0
  25. package/templates/react-ts/README.md +52 -0
  26. package/templates/react-ts/_gitignore +8 -0
  27. package/templates/react-ts/index.html +13 -0
  28. package/templates/react-ts/odoro.config.ts +10 -0
  29. package/templates/react-ts/package.json +23 -0
  30. package/templates/react-ts/public/favicon.svg +4 -0
  31. package/templates/react-ts/src/App.tsx +66 -0
  32. package/templates/react-ts/src/main.tsx +18 -0
  33. package/templates/react-ts/src/odoro-env.d.ts +1 -0
  34. package/templates/react-ts/src/routes/About.tsx +19 -0
  35. package/templates/react-ts/src/routes/Home.tsx +80 -0
  36. package/templates/react-ts/src/routes/NotFound.tsx +14 -0
  37. package/templates/react-ts/src/styles.css +13 -0
  38. package/templates/react-ts/tsconfig.json +25 -0
  39. package/templates/react-ts-server/Dockerfile +51 -0
  40. package/templates/react-ts-server/README.md +87 -0
  41. package/templates/react-ts-server/_dockerignore +8 -0
  42. package/templates/react-ts-server/_env.example +78 -0
  43. package/templates/react-ts-server/_gitignore +8 -0
  44. package/templates/react-ts-server/client/index.html +13 -0
  45. package/templates/react-ts-server/client/public/favicon.svg +4 -0
  46. package/templates/react-ts-server/client/src/App.tsx +66 -0
  47. package/templates/react-ts-server/client/src/main.tsx +18 -0
  48. package/templates/react-ts-server/client/src/odoro-env.d.ts +1 -0
  49. package/templates/react-ts-server/client/src/routes/About.tsx +19 -0
  50. package/templates/react-ts-server/client/src/routes/Home.tsx +148 -0
  51. package/templates/react-ts-server/client/src/routes/NotFound.tsx +14 -0
  52. package/templates/react-ts-server/client/src/styles.css +13 -0
  53. package/templates/react-ts-server/odoro.config.ts +21 -0
  54. package/templates/react-ts-server/package.json +33 -0
  55. package/templates/react-ts-server/scripts/dev.mjs +74 -0
  56. package/templates/react-ts-server/server/src/main.ts +131 -0
  57. package/templates/react-ts-server/server/src/modules/health/index.ts +144 -0
  58. package/templates/react-ts-server/server/tsconfig.json +23 -0
  59. package/templates/react-ts-server/tsconfig.json +27 -0
@@ -0,0 +1,80 @@
1
+ import { useState } from 'react'
2
+
3
+ import { Reveal, Stagger, useAnimate } from '@odoro-cli/libs/motion'
4
+ import { Link } from '@odoro-cli/libs/router'
5
+ import { Button } from '@odoro-cli/libs/ui'
6
+
7
+ /** Trois arguments mis en avant sur la page d'accueil. */
8
+ const FEATURES = [
9
+ {
10
+ title: 'Routeur',
11
+ body: 'Segments dynamiques, routes imbriquees, chargement paresseux et transitions de page.',
12
+ },
13
+ {
14
+ title: 'Animations',
15
+ body: "Une couche mince sur le moteur du navigateur. Les animations reduites sont respectees d'office.",
16
+ },
17
+ {
18
+ title: 'Styles',
19
+ body: 'Des tokens en source de verite, une feuille statique, aucun scan a l execution.',
20
+ },
21
+ ]
22
+
23
+ /** Page d'accueil. */
24
+ export function Home() {
25
+ const [ref, controls] = useAnimate<HTMLSpanElement>()
26
+ const [count, setCount] = useState(0)
27
+
28
+ return (
29
+ <div className="o-flex o-flex-col o-gap-12">
30
+ <Reveal>
31
+ <h1 className="o-text-4xl o-font-bold o-tracking-tight">
32
+ Un point de depart maitrise.
33
+ </h1>
34
+ <p className="o-mt-4 o-text-lg o-text-zinc-500 dark:o-text-zinc-400 o-max-w-prose">
35
+ Ce projet a ete genere par <code className="o-font-mono">odoro create</code>. Le
36
+ routeur, le moteur d animation et le systeme de style viennent tous de la meme
37
+ librairie.
38
+ </p>
39
+ </Reveal>
40
+
41
+ <Stagger step={80} className="o-grid o-grid-cols-1 md:o-grid-cols-3 o-gap-4">
42
+ {FEATURES.map((feature) => (
43
+ <article
44
+ key={feature.title}
45
+ className="o-rounded-lg o-border-w-1 o-border-zinc-200 dark:o-border-zinc-800 o-bg-white dark:o-bg-zinc-900 o-p-5"
46
+ >
47
+ <h2 className="o-font-semibold">{feature.title}</h2>
48
+ <p className="o-mt-2 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
49
+ {feature.body}
50
+ </p>
51
+ </article>
52
+ ))}
53
+ </Stagger>
54
+
55
+ <div className="o-flex o-items-center o-gap-4">
56
+ <Button
57
+ onClick={() => {
58
+ setCount((value) => value + 1)
59
+ void controls.play(
60
+ [
61
+ { transform: 'translateY(0)' },
62
+ { transform: 'translateY(-0.4rem)' },
63
+ { transform: 'translateY(0)' },
64
+ ],
65
+ { duration: 'fast', easing: 'emphasized' },
66
+ )
67
+ }}
68
+ >
69
+ Compter
70
+ </Button>
71
+ <span ref={ref} className="o-inline-block o-text-lg o-tabular-nums">
72
+ {count}
73
+ </span>
74
+ <Link to="/a-propos" className="o-text-sm">
75
+ En savoir plus
76
+ </Link>
77
+ </div>
78
+ </div>
79
+ )
80
+ }
@@ -0,0 +1,14 @@
1
+ import { Link } from '@odoro-cli/libs/router'
2
+
3
+ /** Page affichee pour toute route inconnue. */
4
+ export function NotFound() {
5
+ return (
6
+ <div className="o-flex o-flex-col o-gap-4" role="alert">
7
+ <h1 className="o-text-3xl o-font-bold">404</h1>
8
+ <p className="o-text-zinc-500 dark:o-text-zinc-400">Cette page n existe pas.</p>
9
+ <p>
10
+ <Link to="/">Retour a l accueil</Link>
11
+ </p>
12
+ </div>
13
+ )
14
+ }
@@ -0,0 +1,13 @@
1
+ /* Styles propres a l'application.
2
+ Tout le reste vient des tokens Odoro : surcharger une variable ci-dessous
3
+ suffit a rethemer l'ensemble, composants de la librairie compris. */
4
+
5
+ :root {
6
+ --o-color-primary: oklch(52.4% 0.212 275);
7
+ }
8
+
9
+ .app-shell {
10
+ min-height: 100dvh;
11
+ display: grid;
12
+ grid-template-rows: auto 1fr auto;
13
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "resolveJsonModule": true,
10
+ "isolatedModules": true,
11
+ "verbatimModuleSyntax": true,
12
+ "noEmit": true,
13
+ "strict": true,
14
+ "noUncheckedIndexedAccess": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "skipLibCheck": true,
19
+ "paths": {
20
+ "@/*": ["./src/*"]
21
+ },
22
+ "baseUrl": "."
23
+ },
24
+ "include": ["src", "odoro.config.ts"]
25
+ }
@@ -0,0 +1,51 @@
1
+ # syntax=docker/dockerfile:1
2
+
3
+ # --- Etape 1 : dependances -----------------------------------------------
4
+ # Isolee pour que le cache de couches survive a toute modification du code :
5
+ # seule une modification du manifeste relance l'installation.
6
+ FROM node:22-alpine AS deps
7
+ WORKDIR /app
8
+ COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./
9
+ RUN if [ -f pnpm-lock.yaml ]; then \
10
+ corepack enable && pnpm install --frozen-lockfile; \
11
+ elif [ -f yarn.lock ]; then \
12
+ corepack enable && yarn install --immutable; \
13
+ else \
14
+ npm ci; \
15
+ fi
16
+
17
+ # --- Etape 2 : compilation ------------------------------------------------
18
+ FROM node:22-alpine AS build
19
+ WORKDIR /app
20
+ COPY --from=deps /app/node_modules ./node_modules
21
+ COPY . .
22
+ RUN npm run build
23
+
24
+ # --- Etape 3 : dependances de production ----------------------------------
25
+ # Reinstallees a part : l'image finale ne doit pas embarquer la chaine de
26
+ # compilation, qui pese souvent plus que l'application elle-meme.
27
+ FROM node:22-alpine AS runtime-deps
28
+ WORKDIR /app
29
+ COPY package.json package-lock.json* ./
30
+ RUN npm ci --omit=dev || npm install --omit=dev
31
+
32
+ # --- Etape 4 : image finale -----------------------------------------------
33
+ FROM node:22-alpine AS runtime
34
+ WORKDIR /app
35
+ ENV NODE_ENV=production
36
+ ENV PORT=3001
37
+
38
+ # Un processus applicatif n'a aucune raison de tourner en root.
39
+ RUN addgroup -S odoro && adduser -S odoro -G odoro
40
+
41
+ COPY --from=runtime-deps --chown=odoro:odoro /app/node_modules ./node_modules
42
+ COPY --from=build --chown=odoro:odoro /app/dist ./dist
43
+ COPY --chown=odoro:odoro package.json ./
44
+
45
+ USER odoro
46
+ EXPOSE 3001
47
+
48
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
49
+ CMD node -e "fetch('http://localhost:'+(process.env.PORT||3001)+'/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
50
+
51
+ CMD ["node", "dist/server/main.js"]
@@ -0,0 +1,87 @@
1
+ # Application Odoro — client et serveur
2
+
3
+ Une application monopage React et une API bâtie sur `@odoro-cli/server`, dans le
4
+ même dépôt.
5
+
6
+ ```
7
+ client/ interface, servie par Odoro en developpement
8
+ server/
9
+ src/main.ts assemble les modules, rien d'autre
10
+ src/modules/ un dossier par module — commencez par `health/`
11
+ ```
12
+
13
+ ## Demarrer
14
+
15
+ ```sh
16
+ cp .env.example .env
17
+ npm install
18
+ npm run dev
19
+ ```
20
+
21
+ Le client ecoute sur <http://localhost:5180> et transmet au serveur tout appel
22
+ commencant par `/api`. Le navigateur ne voit donc qu'une seule origine, et
23
+ aucune question de CORS ne se pose en developpement.
24
+
25
+ ## La base de donnees
26
+
27
+ **PostgreSQL, heberge, et rien d'autre.** Il n'y a pas de base locale : l'URL
28
+ pointe sur une base joignable par le reseau.
29
+
30
+ ```sh
31
+ odoro db:create # provisionne une base et ecrit .env
32
+ # ou collez votre propre URL dans .env :
33
+ DATABASE_URL=postgres://utilisateur:motdepasse@hote:5432/base?sslmode=require
34
+ ```
35
+
36
+ Tant qu'elle est absente, **le client demarre quand meme** et le serveur repond
37
+ `503` sur `/api/ready` en disant precisement ce qui manque. On voit donc
38
+ l'interface des la premiere minute, et on sait ce qu'il reste a faire.
39
+
40
+ ## Ecrire un module
41
+
42
+ `server/src/modules/health/` est l'exemple. Un module declare son nom, ce dont
43
+ il a besoin, les services qu'il enregistre et les routes qu'il expose — et
44
+ `main.ts` ne fait que dire lesquels sont actifs.
45
+
46
+ ```ts
47
+ export const billingModule = defineModule({
48
+ name: 'billing',
49
+ requires: ['auth'],
50
+ register: (c) => c.register('billingService', createBillingService),
51
+ routes: billingRoutes,
52
+ })
53
+ ```
54
+
55
+ Le noyau resout l'ordre de chargement depuis `requires`, detecte les cycles, et
56
+ refuse de demarrer si une dependance manque. Activer ou desactiver un module
57
+ tient donc en une ligne dans `main.ts`.
58
+
59
+ ## Deux points de controle, qui ne disent pas la meme chose
60
+
61
+ | Route | Question | Qui l'interroge |
62
+ | ------------- | ------------------------------- | ------------------------------------------------ |
63
+ | `/api/health` | Le processus vit-il ? | L'orchestrateur, pour decider de redemarrer |
64
+ | `/api/ready` | Le service peut-il travailler ? | Le repartiteur, pour decider d'envoyer du trafic |
65
+
66
+ Les confondre donne l'un des deux defauts : un service qui redemarre en boucle
67
+ pendant un incident de base, ou un repartiteur qui envoie du trafic a un
68
+ service incapable de repondre.
69
+
70
+ ## Compiler et deployer
71
+
72
+ ```sh
73
+ npm run build # client dans dist/client, serveur dans dist/server
74
+ npm start # sert les deux depuis un seul processus
75
+ ```
76
+
77
+ Le `Dockerfile` est multi-etapes : les dependances de compilation ne se
78
+ retrouvent pas dans l'image finale, qui tourne sous un utilisateur sans
79
+ privileges.
80
+
81
+ ## Variables d'environnement
82
+
83
+ Voir `.env.example`, commente ligne par ligne. En production, ce qui manque
84
+ arrete le demarrage avec un message qui **liste tout d'un coup** — plutot
85
+ qu'une suite de redemarrages, une variable a la fois.
86
+
87
+ `.env` n'est jamais versionne.
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ dist
3
+ .git
4
+ .env
5
+ .env.*
6
+ *.log
7
+ Dockerfile
8
+ .dockerignore
@@ -0,0 +1,78 @@
1
+ # Copier ce fichier en `.env` et ajuster les valeurs.
2
+ #
3
+ # Toute variable manquante ou invalide arrete le serveur au demarrage, avec un
4
+ # message qui les liste toutes d'un coup : mieux vaut un echec immediat qu'une
5
+ # panne a la centieme requete.
6
+ #
7
+ # Ce fichier est versionne. `.env` ne l'est jamais.
8
+
9
+ # ---------------------------------------------------------------------------
10
+ # Base de donnees
11
+ # ---------------------------------------------------------------------------
12
+
13
+ # PostgreSQL, et rien d'autre. Aucune base locale : l'URL pointe sur une base
14
+ # hebergee, la votre ou celle provisionnee par `odoro db:create`.
15
+ #
16
+ # postgres://utilisateur:motdepasse@hote:5432/base?sslmode=require
17
+ #
18
+ # Tant qu'elle est absente, le client demarre et le serveur repond 503 sur
19
+ # /api/ready en disant ce qui manque.
20
+ DATABASE_URL=
21
+
22
+ # Replica de lecture, optionnel. Le routage vers lui reste explicite : jamais
23
+ # de bascule automatique, le decalage de replication produit des defauts
24
+ # incomprehensibles.
25
+ # DATABASE_REPLICA_URL=
26
+
27
+ # Taille du pool de connexions. Laisser vide pour le defaut de l'environnement.
28
+ # DATABASE_POOL_MAX=
29
+
30
+ # Desactive TLS. A ne poser que sur une base strictement locale a la machine,
31
+ # jamais sur une base joignable par le reseau.
32
+ # DATABASE_TLS_DISABLED=false
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Session
36
+ # ---------------------------------------------------------------------------
37
+
38
+ # Au moins 32 caracteres. Un secret court se retrouve par force brute, et le
39
+ # jour ou c'est arrive, toutes les sessions emises depuis le premier jour sont
40
+ # forgeables.
41
+ #
42
+ # node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"
43
+ SESSION_SECRET=
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Serveur
47
+ # ---------------------------------------------------------------------------
48
+
49
+ # Environnement : development | test | production
50
+ NODE_ENV=development
51
+
52
+ # Port d'ecoute de l'API. Le client de developpement ecoute sur 5180 et lui
53
+ # transmet les appels commencant par /api.
54
+ PORT=3001
55
+
56
+ # URL publique du serveur, employee dans les liens des courriels.
57
+ APP_URL=http://localhost:3001
58
+
59
+ # Origines autorisees, separees par des virgules. Vide = aucune origine
60
+ # tierce. Jamais `*` avec des identifiants.
61
+ ALLOWED_ORIGINS=
62
+
63
+ # Plafond de la taille des corps de requete.
64
+ BODY_LIMIT=1mb
65
+
66
+ # Delai au-dela duquel l'arret propre cesse d'attendre les requetes en cours.
67
+ SHUTDOWN_TIMEOUT=15s
68
+
69
+ # Niveau de journalisation : fatal | error | warn | info | debug | trace
70
+ LOG_LEVEL=info
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Files d'attente et cache
74
+ # ---------------------------------------------------------------------------
75
+
76
+ # Optionnel. Absente, le cache vit en memoire et les taches s'executent en
77
+ # ligne — ce qui convient a une instance unique, et pas au-dela.
78
+ # REDIS_URL=
@@ -0,0 +1,8 @@
1
+ node_modules/
2
+ dist/
3
+ *.local
4
+ .DS_Store
5
+ *.log
6
+ .env
7
+ .env.*
8
+ !.env.example
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
+ <title>Odoro</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <rect width="32" height="32" rx="8" fill="#5b62f4" />
3
+ <circle cx="16" cy="16" r="7" fill="none" stroke="#fff" stroke-width="3" />
4
+ </svg>
@@ -0,0 +1,66 @@
1
+ import { Link, Outlet, Route, Router, Routes, useLocation } from '@odoro-cli/libs/router'
2
+
3
+ import { About } from '@/routes/About'
4
+ import { Home } from '@/routes/Home'
5
+ import { NotFound } from '@/routes/NotFound'
6
+
7
+ /** Barre de navigation, avec mise en valeur de la route courante. */
8
+ function Nav() {
9
+ const { pathname } = useLocation()
10
+
11
+ const links = [
12
+ { to: '/', label: 'Accueil' },
13
+ { to: '/a-propos', label: 'A propos' },
14
+ ]
15
+
16
+ return (
17
+ <nav className="o-flex o-items-center o-gap-4 o-px-6 o-py-4 o-border-b o-border-zinc-200 dark:o-border-zinc-800">
18
+ <span className="o-font-semibold o-text-brand-600 dark:o-text-brand-400">
19
+ Odoro
20
+ </span>
21
+ {links.map((link) => (
22
+ <Link
23
+ key={link.to}
24
+ to={link.to}
25
+ className={
26
+ pathname === link.to
27
+ ? 'o-text-zinc-900 dark:o-text-zinc-50 o-font-medium o-no-underline'
28
+ : 'o-text-zinc-500 dark:o-text-zinc-400 o-no-underline hover:o-text-zinc-900 dark:hover:o-text-zinc-50 o-transition'
29
+ }
30
+ >
31
+ {link.label}
32
+ </Link>
33
+ ))}
34
+ </nav>
35
+ )
36
+ }
37
+
38
+ /** Enveloppe commune a toutes les pages. */
39
+ function Layout() {
40
+ return (
41
+ <div className="app-shell">
42
+ <Nav />
43
+ <main className="o-view-transition-page o-mx-auto o-w-full o-max-w-3xl o-px-6 o-py-12">
44
+ <Outlet />
45
+ </main>
46
+ <footer className="o-px-6 o-py-6 o-text-sm o-text-zinc-500 dark:o-text-zinc-400 o-border-t o-border-zinc-200 dark:o-border-zinc-800">
47
+ Construit avec Odoro.
48
+ </footer>
49
+ </div>
50
+ )
51
+ }
52
+
53
+ /** Racine de l'application. */
54
+ export function App() {
55
+ return (
56
+ <Router>
57
+ <Routes>
58
+ <Route path="/" element={<Layout />}>
59
+ <Route index element={<Home />} />
60
+ <Route path="a-propos" element={<About />} />
61
+ <Route path="*" element={<NotFound />} />
62
+ </Route>
63
+ </Routes>
64
+ </Router>
65
+ )
66
+ }
@@ -0,0 +1,18 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+
4
+ import { App } from '@/App'
5
+
6
+ import '@odoro-cli/libs/styles.css'
7
+ import '@/styles.css'
8
+
9
+ const container = document.getElementById('root')
10
+ if (container === null) {
11
+ throw new Error('Element racine "#root" introuvable dans index.html.')
12
+ }
13
+
14
+ createRoot(container).render(
15
+ <StrictMode>
16
+ <App />
17
+ </StrictMode>,
18
+ )
@@ -0,0 +1 @@
1
+ /// <reference types="odoro/client" />
@@ -0,0 +1,19 @@
1
+ import { Reveal } from '@odoro-cli/libs/motion'
2
+ import { Link } from '@odoro-cli/libs/router'
3
+
4
+ /** Page « a propos », qui demontre la navigation entre routes. */
5
+ export function About() {
6
+ return (
7
+ <Reveal className="o-flex o-flex-col o-gap-6">
8
+ <h1 className="o-text-3xl o-font-bold o-tracking-tight">A propos</h1>
9
+ <p className="o-text-zinc-500 dark:o-text-zinc-400 o-max-w-prose">
10
+ Cette page est servie par le routeur d Odoro. La navigation ne recharge pas le
11
+ document : elle passe par l historique du navigateur, restaure la position de
12
+ defilement et joue une transition de page quand le navigateur la prend en charge.
13
+ </p>
14
+ <p>
15
+ <Link to="/">Retour a l accueil</Link>
16
+ </p>
17
+ </Reveal>
18
+ )
19
+ }
@@ -0,0 +1,148 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ import { Reveal, Stagger } from '@odoro-cli/libs/motion'
4
+ import { Link } from '@odoro-cli/libs/router'
5
+ import { Button } from '@odoro-cli/libs/ui'
6
+
7
+ /** Reponse de la route de sante du serveur. */
8
+ interface Health {
9
+ status: string
10
+ environment: string
11
+ uptime: number
12
+ timestamp: string
13
+ }
14
+
15
+ /** Etat de l'appel a l'API. */
16
+ type Fetching =
17
+ | { state: 'inactif' }
18
+ | { state: 'en-cours' }
19
+ | { state: 'ok'; health: Health }
20
+ | { state: 'echec'; message: string }
21
+
22
+ /**
23
+ * Interroge la route de sante du serveur.
24
+ *
25
+ * L'URL est relative : en developpement le moteur transmet l'appel au serveur,
26
+ * en production c'est le meme serveur qui repond. Il n'y a donc jamais d'URL
27
+ * d'API a configurer cote client.
28
+ */
29
+ async function fetchHealth(signal: AbortSignal): Promise<Health> {
30
+ const response = await fetch('/api/health', { signal })
31
+ if (!response.ok) throw new Error(`Le serveur a repondu ${response.status}.`)
32
+ return (await response.json()) as Health
33
+ }
34
+
35
+ /** Panneau montrant l'etat de la liaison avec l'API. */
36
+ function ApiStatus() {
37
+ const [result, setResult] = useState<Fetching>({ state: 'inactif' })
38
+ const [attempt, setAttempt] = useState(0)
39
+
40
+ useEffect(() => {
41
+ const controller = new AbortController()
42
+ setResult({ state: 'en-cours' })
43
+
44
+ fetchHealth(controller.signal).then(
45
+ (health) => setResult({ state: 'ok', health }),
46
+ (cause: unknown) => {
47
+ if (controller.signal.aborted) return
48
+ setResult({
49
+ state: 'echec',
50
+ message: cause instanceof Error ? cause.message : String(cause),
51
+ })
52
+ },
53
+ )
54
+
55
+ return () => controller.abort()
56
+ }, [attempt])
57
+
58
+ return (
59
+ <section className="o-rounded-lg o-border-w-1 o-border-zinc-200 dark:o-border-zinc-800 o-bg-white dark:o-bg-zinc-900 o-p-5">
60
+ <div className="o-flex o-items-center o-justify-between o-gap-4">
61
+ <h2 className="o-font-semibold">Liaison avec l&rsquo;API</h2>
62
+ <Button
63
+ size="sm"
64
+ tone="secondary"
65
+ loading={result.state === 'en-cours'}
66
+ onClick={() => setAttempt((value) => value + 1)}
67
+ >
68
+ Reinterroger
69
+ </Button>
70
+ </div>
71
+
72
+ <div className="o-mt-3 o-text-sm" role="status">
73
+ {result.state === 'inactif' || result.state === 'en-cours' ? (
74
+ <p className="o-text-zinc-500 dark:o-text-zinc-400">
75
+ Interrogation de /api/health...
76
+ </p>
77
+ ) : result.state === 'echec' ? (
78
+ <p className="o-text-red-600 dark:o-text-red-400">
79
+ Serveur injoignable : {result.message}. Verifiez qu&rsquo;il tourne bien sur
80
+ le port 3001.
81
+ </p>
82
+ ) : (
83
+ <dl className="o-grid o-grid-cols-2 o-gap-2 o-tabular-nums">
84
+ <dt className="o-text-zinc-500 dark:o-text-zinc-400">Etat</dt>
85
+ <dd className="o-text-emerald-600 dark:o-text-emerald-400">
86
+ {result.health.status}
87
+ </dd>
88
+ <dt className="o-text-zinc-500 dark:o-text-zinc-400">Environnement</dt>
89
+ <dd>{result.health.environment}</dd>
90
+ <dt className="o-text-zinc-500 dark:o-text-zinc-400">En ligne depuis</dt>
91
+ <dd>{result.health.uptime} s</dd>
92
+ </dl>
93
+ )}
94
+ </div>
95
+ </section>
96
+ )
97
+ }
98
+
99
+ /** Trois arguments mis en avant sur la page d'accueil. */
100
+ const FEATURES = [
101
+ {
102
+ title: 'Une seule origine',
103
+ body: 'Les appels /api sont transmis au serveur : aucune question de CORS, aucune URL a configurer.',
104
+ },
105
+ {
106
+ title: 'Un seul deploiement',
107
+ body: 'En production, le serveur sert l API et le client compile. Une image, un processus.',
108
+ },
109
+ {
110
+ title: 'Un environnement verifie',
111
+ body: 'Les variables sont validees au demarrage. Une configuration incomplete arrete le serveur tout de suite.',
112
+ },
113
+ ]
114
+
115
+ /** Page d'accueil. */
116
+ export function Home() {
117
+ return (
118
+ <div className="o-flex o-flex-col o-gap-12">
119
+ <Reveal>
120
+ <h1 className="o-text-4xl o-font-bold o-tracking-tight">Client et serveur.</h1>
121
+ <p className="o-mt-4 o-text-lg o-text-zinc-500 dark:o-text-zinc-400 o-max-w-prose">
122
+ Ce projet reunit une application React et une API Express dans un seul depot. Le
123
+ panneau ci-dessous interroge le serveur pour de vrai.
124
+ </p>
125
+ </Reveal>
126
+
127
+ <ApiStatus />
128
+
129
+ <Stagger step={80} className="o-grid o-grid-cols-1 md:o-grid-cols-3 o-gap-4">
130
+ {FEATURES.map((feature) => (
131
+ <article
132
+ key={feature.title}
133
+ className="o-rounded-lg o-border-w-1 o-border-zinc-200 dark:o-border-zinc-800 o-bg-white dark:o-bg-zinc-900 o-p-5"
134
+ >
135
+ <h2 className="o-font-semibold">{feature.title}</h2>
136
+ <p className="o-mt-2 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
137
+ {feature.body}
138
+ </p>
139
+ </article>
140
+ ))}
141
+ </Stagger>
142
+
143
+ <p>
144
+ <Link to="/a-propos">En savoir plus</Link>
145
+ </p>
146
+ </div>
147
+ )
148
+ }
@@ -0,0 +1,14 @@
1
+ import { Link } from '@odoro-cli/libs/router'
2
+
3
+ /** Page affichee pour toute route inconnue. */
4
+ export function NotFound() {
5
+ return (
6
+ <div className="o-flex o-flex-col o-gap-4" role="alert">
7
+ <h1 className="o-text-3xl o-font-bold">404</h1>
8
+ <p className="o-text-zinc-500 dark:o-text-zinc-400">Cette page n existe pas.</p>
9
+ <p>
10
+ <Link to="/">Retour a l accueil</Link>
11
+ </p>
12
+ </div>
13
+ )
14
+ }
@@ -0,0 +1,13 @@
1
+ /* Styles propres a l'application.
2
+ Tout le reste vient des tokens Odoro : surcharger une variable ci-dessous
3
+ suffit a rethemer l'ensemble, composants de la librairie compris. */
4
+
5
+ :root {
6
+ --o-color-primary: oklch(52.4% 0.212 275);
7
+ }
8
+
9
+ .app-shell {
10
+ min-height: 100dvh;
11
+ display: grid;
12
+ grid-template-rows: auto 1fr auto;
13
+ }