Mode d'emploi — de zĂ©ro Ă  dĂ©ployĂ©

FileAttente

SaaS de gestion de liste d'attente. 7 Ă©tapes, dans l'ordre. Tu exĂ©cutes chaque action toi-mĂȘme, dans ton terminal.

Sommaire
01Initialiser le projet Next.js
02Base de donnĂ©es — Prisma + Supabase
03Authentification — Better Auth
04CRUD Files d'attente
05CRUD Entrées
06Habillage — Tailwind & shadcn/ui
07Tests — Vitest & Playwright
08DĂ©ploiement — Vercel
RĂšgle du jeu : tu tapes chaque commande toi-mĂȘme dans ton terminal, dans l'ordre. Tu ne passes Ă  l'Ă©tape suivante que quand la case « vĂ©rification » est cochĂ©e. Si une commande plante, copie-colle l'erreur exacte — on corrige ensemble, tu ne devines pas.

RĂ©utilisable vs spĂ©cifique : les Ă©tapes rĂ©utilisable forment un template valable pour n'importe quel futur SaaS de ta stack. Les Ă©tapes spĂ©cifique sont propres au modĂšle de donnĂ©es de FileAttente (Queue/Entry) — Ă  chaque nouveau projet, seule cette partie change vraiment.
📖 C'est quoi chaque techno (Next.js, Prisma, Better Auth...) ? Le lexique complet est sur la page d'accueil →
01
Réutilisable

Initialiser le projet Next.js

Un projet Next.js qui dĂ©marre en local, avec Tailwind et shadcn/ui prĂȘts Ă  l'emploi.

Prérequis : Node.js installé. Terminal ouvert dans le dossier du projet.

  1. Installe pnpm si besoin
    npm install -g pnpm
    pnpm est le gestionnaire de paquets choisi pour ce projet — plus rapide et plus Ă©conome en espace disque que npm.
    👁 Comment voir le rĂ©sultat : tape pnpm -v. Un numĂ©ro de version s'affiche (ex. 9.12.0) — sinon l'installation a Ă©chouĂ©.
  2. Crée le projet Next.js
    pnpm create next-app@latest . --typescript --tailwind --eslint --app --src-dir=false --import-alias "@/*"
    Le nom du dossier ne doit pas contenir de majuscules (contrainte npm). S'il en contient, crée le projet dans un dossier temporaire au nom valide puis déplace tout son contenu dans ton dossier de travail.
    👁 Comment voir le rĂ©sultat : tape ls. Tu dois voir apparaĂźtre app/, package.json, tailwind.config.ts — la commande a rempli ton dossier de fichiers.
  3. Initialise shadcn/ui
    pnpm dlx shadcn@latest init
    Ça crĂ©e components.json et lib/utils.ts, et prĂ©pare Tailwind pour recevoir des composants shadcn/ui prĂȘts Ă  copier.
    👁 Comment voir le rĂ©sultat : tape ls components.json lib/utils.ts — les deux fichiers doivent exister, sans message d'erreur « No such file ».
  4. Crée l'arborescence du projet
    mkdir -p lib/auth lib/db lib/validations prisma testsmkdir lib/auth, lib/db, lib/validations, prisma, tests
    On sĂ©pare dĂšs le dĂ©part : authentification, accĂšs base de donnĂ©es, schĂ©mas de validation. Ça Ă©vite le fichier fourre-tout de 2000 lignes plus tard.
    👁 Comment voir le rĂ©sultat : tape ls lib — les dossiers auth, db, validations doivent apparaĂźtre.
  5. Initialise Git et fais le premier commit
    git init
    git add -A
    git commit -m "chore: init projet Next.js"
    👁 Comment voir le rĂ©sultat : tape git log --oneline — une ligne avec ton message de commit doit s'afficher.
  6. Lance le serveur et vérifie
    pnpm dev
    Ouvre http://localhost:3000. Si tu as une erreur 500 mentionnant un module introuvable (ex. Can't resolve 'xxx') alors qu'il est bien dans package.json : supprime node_modules, pnpm-lock.yaml et .next, relance pnpm install puis pnpm dev. C'est presque toujours une installation incomplĂšte, pas un bug de code.
    👁 Comment voir le rĂ©sultat : dans ton navigateur, ouvre localhost:3000. Tu dois voir la page d'accueil par dĂ©faut de Next.js. Le terminal affiche Ready et n'affiche pas d'erreur rouge.
Vérification
02
Réutilisable

Base de donnĂ©es — Prisma + Supabase

Une base PostgreSQL en ligne, avec deux tables (Queue, Entry) créées via Prisma.

Prérequis : étape 1 validée.

  1. Crée un projet sur Supabase
    Va sur supabase.com → New project. Choisis un nom, une rĂ©gion proche, et un mot de passe de base de donnĂ©es — note-le quelque part.
    👁 Comment voir le rĂ©sultat : le tableau de bord Supabase affiche ton projet avec un statut « Active » (aprĂšs ~1-2 minutes).
  2. RécupÚre l'URL de connexion
    Dans le projet Supabase : Project Settings → Database → Connection string, mode URI. Copie l'URL complùte (mot de passe inclus).
  3. Installe Prisma
    pnpm add prisma --save-dev
    pnpm add @prisma/client
    pnpm dlx prisma init
    Crée prisma/schema.prisma et un fichier .env avec une variable DATABASE_URL vide.
    👁 Comment voir le rĂ©sultat : tape ls prisma/schema.prisma — le fichier doit exister.
  4. Colle l'URL dans .env
    # .env
    DATABASE_URL="postgresql://postgres:[MOT-DE-PASSE]@[HOST]:5432/postgres"
    Ce fichier ne doit jamais ĂȘtre commit (dĂ©jĂ  dans .gitignore par dĂ©faut) — il contient un mot de passe.
  5. Écris le schĂ©ma
    Dans prisma/schema.prisma, sous les blocs generator/datasource déjà présents, ajoute :
    model Queue {
      id        String   @id @default(cuid())
      name      String
      ownerId   String
      createdAt DateTime @default(now())
      entries   Entry[]
    }
    
    model Entry {
      id        String   @id @default(cuid())
      queueId   String
      queue     Queue    @relation(fields: [queueId], references: [id])
      name      String
      note      String?
      status    String   @default("WAITING")
      createdAt DateTime @default(now())
    }
  6. Applique la migration et vérifie
    pnpm dlx prisma migrate dev --name init
    pnpm dlx prisma studio
    migrate dev crée les tables et génÚre le client Prisma typé. studio ouvre une interface web locale pour voir/éditer les données à la main.
    👁 Comment voir le rĂ©sultat : prisma studio ouvre localhost:5555 — les tables Queue et Entry doivent apparaĂźtre dans la liste de gauche.
Vérification

PiĂšge frĂ©quent : connexion refusĂ©e → vĂ©rifie qu'aucun caractĂšre spĂ©cial du mot de passe (@, #...) n'est mal encodĂ© dans l'URL. Supabase propose un bouton pour copier l'URL dĂ©jĂ  encodĂ©e.

03
Réutilisable

Authentification — Better Auth

S'inscrire, se connecter, et protéger /dashboard des visiteurs non connectés.

Prérequis : étape 2 validée (schéma Prisma en place).

  1. Installe Better Auth
    pnpm add better-auth
    👁 Comment voir le rĂ©sultat : better-auth apparaĂźt dans package.json.
  2. Crée le fichier du client Prisma
    touch lib/db/client.tsNew-Item -ItemType File -Force -Path lib/db/client.ts
    👁 Comment voir le rĂ©sultat : tape ls lib/db — client.ts doit apparaĂźtre.
  3. Colle le contenu du client Prisma partagé
    // lib/db/client.ts
    import { PrismaClient } from "@prisma/client";
    
    const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
    
    export const prisma = globalForPrisma.prisma ?? new PrismaClient();
    
    if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
    Une instance unique de PrismaClient, rĂ©utilisĂ©e partout via import { prisma } from "@/lib/db/client" — Ă©vite d'en recrĂ©er une Ă  chaque appel (et les erreurs « too many connections » que ça provoque avec le rechargement Ă  chaud de Next.js en dĂ©veloppement).
  4. Crée le fichier de config serveur
    touch lib/auth/auth.tsNew-Item -ItemType File -Force -Path lib/auth/auth.ts
  5. Colle le contenu de la config serveur
    // lib/auth/auth.ts
    import { betterAuth } from "better-auth";
    import { prismaAdapter } from "better-auth/adapters/prisma";
    import { prisma } from "@/lib/db/client";
    
    export const auth = betterAuth({
      database: prismaAdapter(prisma, { provider: "postgresql" }),
      emailAndPassword: { enabled: true },
    });
  6. Ajoute les tables auth au schéma
    pnpm dlx @better-auth/cli generate
    pnpm dlx prisma migrate dev --name add_auth
    Better Auth génÚre automatiquement les modÚles User, Session, Account dans schema.prisma.
    👁 Comment voir le rĂ©sultat : ouvre prisma studio — les tables User, Session, Account doivent apparaĂźtre.
  7. Ajoute le secret dans .env
    # .env
    BETTER_AUTH_SECRET="génÚre une chaßne aléatoire longue"
    BETTER_AUTH_URL="http://localhost:3000"
  8. Crée le dossier et le fichier de la route API
    mkdir -p "app/api/auth/[...all]" && touch "app/api/auth/[...all]/route.ts"New-Item -ItemType Directory -Force -Path "app/api/auth/[...all]"; New-Item -ItemType File -Force -Path "app/api/auth/[...all]/route.ts"
    Le nom de dossier entre crochets [...all] est la syntaxe Next.js pour une route qui capture toutes les sous-routes — indispensable pour que Better Auth gùre toutes ses URLs (/api/auth/sign-in, /api/auth/sign-up, etc.) avec un seul fichier.
  9. Colle le contenu de la route API
    // app/api/auth/[...all]/route.ts
    import { auth } from "@/lib/auth/auth";
    import { toNextJsHandler } from "better-auth/next-js";
    
    export const { GET, POST } = toNextJsHandler(auth);
    👁 Comment voir le rĂ©sultat : relance pnpm dev et ouvre un endpoint Better Auth (ex. localhost:3000/api/auth/ok) — tu dois obtenir une rĂ©ponse JSON, pas une erreur 404.
  10. Crée le fichier du client auth
    touch lib/auth/client.tsNew-Item -ItemType File -Force -Path lib/auth/client.ts
    // lib/auth/client.ts
    import { createAuthClient } from "better-auth/react";
    
    export const authClient = createAuthClient();
  11. Crée les dossiers et fichiers des pages /signup et /login
    mkdir -p app/signup app/login && touch app/signup/page.tsx app/login/page.tsxmkdir app/signup, app/login; New-Item -ItemType File -Force -Path app/signup/page.tsx, app/login/page.tsx
  12. Colle le contenu de la page /signup
    // app/signup/page.tsx
    "use client";
    import { useState } from "react";
    import { useRouter } from "next/navigation";
    import { authClient } from "@/lib/auth/client";
    
    export default function SignupPage() {
      const [email, setEmail] = useState("");
      const [password, setPassword] = useState("");
      const [error, setError] = useState("");
      const router = useRouter();
    
      async function handleSubmit(e: React.FormEvent) {
        e.preventDefault();
        const { error } = await authClient.signUp.email({ email, password, name: email });
        if (error) return setError(error.message ?? "Inscription refusée");
        router.push("/dashboard");
      }
    
      return (
        <form onSubmit={handleSubmit} className="max-w-sm mx-auto p-8 flex flex-col gap-3">
          <input type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} className="border p-2 rounded" />
          <input type="password" placeholder="Mot de passe" value={password} onChange={(e) => setPassword(e.target.value)} className="border p-2 rounded" />
          {error && <p className="text-red-500 text-sm">{error}</p>}
          <button type="submit" className="bg-black text-white p-2 rounded">Créer un compte</button>
        </form>
      );
    }
    Duplique la mĂȘme structure pour app/login/page.tsx (fonction LoginPage) en remplaçant authClient.signUp.email(...) par authClient.signIn.email({ email, password }).
    👁 Comment voir le rĂ©sultat : crĂ©e un compte test sur localhost:3000/signup, puis vĂ©rifie dans Prisma Studio qu'une ligne apparaĂźt dans la table User. Reconnecte-toi ensuite sur localhost:3000/login avec les mĂȘmes identifiants.
  13. Crée le fichier middleware.ts à la racine du projet
    touch middleware.tsNew-Item -ItemType File -Force -Path middleware.ts
    « À la racine » veut dire au mĂȘme niveau que le dossier app/, pas dedans.
  14. Colle le contenu du middleware.ts
    // middleware.ts
    import { NextRequest, NextResponse } from "next/server";
    import { getSessionCookie } from "better-auth/cookies";
    
    export function middleware(request: NextRequest) {
      const sessionCookie = getSessionCookie(request);
      if (!sessionCookie) {
        return NextResponse.redirect(new URL("/login", request.url));
      }
      return NextResponse.next();
    }
    
    export const config = {
      matcher: ["/dashboard/:path*"],
    };
    VĂ©rifie seulement la prĂ©sence du cookie de session (rapide, pas d'appel base de donnĂ©es) — assez pour bloquer l'accĂšs Ă  la mise en page, la vraie vĂ©rification de session a dĂ©jĂ  lieu dans les Server Actions et les pages.
    👁 Comment voir le rĂ©sultat : en navigation privĂ©e (non connectĂ©), va sur localhost:3000/dashboard — tu dois ĂȘtre redirigĂ© vers /login.
Vérification

PiĂšge frĂ©quent : si la doc officielle de Better Auth diffĂšre de ce qui est Ă©crit ici, suis la doc — c'est une librairie qui Ă©volue vite. Dis-le moi si un nom de fonction a changĂ©, on ajuste.

04
Spécifique FileAttente

CRUD Files d'attente

Créer, lister et supprimer une file depuis /dashboard.

Prérequis : étape 3 validée (session utilisateur disponible).

  1. Ajoute des composants shadcn/ui
    pnpm dlx shadcn@latest add button input card
    👁 Comment voir le rĂ©sultat : ls components/ui — button.tsx, input.tsx, card.tsx doivent apparaĂźtre.
  2. Crée le dossier app/dashboard et le fichier des Server Actions
    mkdir -p app/dashboard && touch app/dashboard/actions.tsNew-Item -ItemType Directory -Force -Path app/dashboard; New-Item -ItemType File -Force -Path app/dashboard/actions.ts
  3. Crée le fichier de validation Zod
    touch lib/validations/queue.tsNew-Item -ItemType File -Force -Path lib/validations/queue.ts
    // lib/validations/queue.ts
    import { z } from "zod";
    export const createQueueSchema = z.object({
      name: z.string().min(1).max(80),
    });
    Écris ce fichier avant les Server Actions ci-dessous — elles l'importent.
  4. Colle le contenu des Server Actions
    // app/dashboard/actions.ts
    "use server";
    import { prisma } from "@/lib/db/client";
    import { auth } from "@/lib/auth/auth";
    import { headers } from "next/headers";
    import { createQueueSchema } from "@/lib/validations/queue";
    import { revalidatePath } from "next/cache";
    
    export async function createQueue(formData: FormData) {
      const session = await auth.api.getSession({ headers: await headers() });
      if (!session) throw new Error("Non connecté");
    
      const parsed = createQueueSchema.safeParse({ name: formData.get("name") });
      if (!parsed.success) throw new Error("Nom invalide");
    
      await prisma.queue.create({
        data: { name: parsed.data.name, ownerId: session.user.id },
      });
      revalidatePath("/dashboard");
    }
    
    export async function deleteQueue(id: string) {
      const session = await auth.api.getSession({ headers: await headers() });
      if (!session) throw new Error("Non connecté");
    
      const queue = await prisma.queue.findUnique({ where: { id } });
      if (queue?.ownerId !== session.user.id) throw new Error("Non autorisé");
    
      await prisma.queue.delete({ where: { id } });
      revalidatePath("/dashboard");
    }
    La vĂ©rification ownerId === session.user.id cĂŽtĂ© serveur est obligatoire — sans elle, n'importe quel utilisateur connectĂ© pourrait supprimer les files d'un autre.
  5. Crée le fichier de la page /dashboard
    touch app/dashboard/page.tsxNew-Item -ItemType File -Force -Path app/dashboard/page.tsx
  6. Colle le contenu de la page /dashboard
    // app/dashboard/page.tsx
    import { prisma } from "@/lib/db/client";
    import { auth } from "@/lib/auth/auth";
    import { headers } from "next/headers";
    import { createQueue, deleteQueue } from "./actions";
    
    export default async function DashboardPage() {
      const session = await auth.api.getSession({ headers: await headers() });
      const queues = await prisma.queue.findMany({ where: { ownerId: session!.user.id } });
    
      return (
        <main className="p-8">
          <h1 className="text-2xl font-bold mb-4">Mes files d'attente</h1>
          <form action={createQueue} className="flex gap-2 mb-6">
            <input name="name" placeholder="Nom de la file" className="border p-2 rounded" required />
            <button type="submit" className="bg-black text-white px-4 rounded">Créer</button>
          </form>
          <ul className="flex flex-col gap-2">
            {queues.map((q) => (
              <li key={q.id} className="flex justify-between border p-3 rounded">
                <a href={`/dashboard/queues/${q.id}`}>{q.name}</a>
                <form action={deleteQueue.bind(null, q.id)}>
                  <button type="submit" className="text-red-500">Supprimer</button>
                </form>
              </li>
            ))}
          </ul>
        </main>
      );
    }
    👁 Comment voir le rĂ©sultat : connectĂ© sur localhost:3000/dashboard, crĂ©e une file test via le formulaire — elle doit apparaĂźtre immĂ©diatement dans la liste, et une nouvelle ligne doit exister dans Prisma Studio (table Queue).
Vérification
05
Spécifique FileAttente

CRUD Entrées

Ajouter une personne Ă  une file, la marquer servie ou la retirer.

Prérequis : étape 4 validée.

  1. Crée le dossier et le fichier des actions
    mkdir -p "app/dashboard/queues/[id]" && touch "app/dashboard/queues/[id]/actions.ts"New-Item -ItemType Directory -Force -Path "app/dashboard/queues/[id]"; New-Item -ItemType File -Force -Path "app/dashboard/queues/[id]/actions.ts"
    Le dossier entre crochets [id] est la syntaxe Next.js pour une route dynamique — id devient accessible dans la page via params.id.
  2. Colle le contenu des actions
    // app/dashboard/queues/[id]/actions.ts
    "use server";
    import { prisma } from "@/lib/db/client";
    import { auth } from "@/lib/auth/auth";
    import { headers } from "next/headers";
    import { revalidatePath } from "next/cache";
    
    async function assertOwnership(queueId: string) {
      const session = await auth.api.getSession({ headers: await headers() });
      const queue = await prisma.queue.findUnique({ where: { id: queueId } });
      if (!session || queue?.ownerId !== session.user.id) throw new Error("Non autorisé");
      return queue;
    }
    
    export async function addEntry(queueId: string, formData: FormData) {
      await assertOwnership(queueId);
      const name = formData.get("name") as string;
      await prisma.entry.create({ data: { queueId, name, status: "WAITING" } });
      revalidatePath(`/dashboard/queues/${queueId}`);
    }
    
    export async function markServed(entryId: string) {
      const entry = await prisma.entry.findUniqueOrThrow({ where: { id: entryId } });
      await assertOwnership(entry.queueId);
      await prisma.entry.update({ where: { id: entryId }, data: { status: "SERVED" } });
      revalidatePath(`/dashboard/queues/${entry.queueId}`);
    }
    
    export async function removeEntry(entryId: string) {
      const entry = await prisma.entry.findUniqueOrThrow({ where: { id: entryId } });
      await assertOwnership(entry.queueId);
      await prisma.entry.delete({ where: { id: entryId } });
      revalidatePath(`/dashboard/queues/${entry.queueId}`);
    }
    Écris ce fichier avant la page ci-dessous — elle l'importe. assertOwnership vĂ©rifie que la file appartient bien Ă  l'utilisateur connectĂ© avant d'agir — sans ça, n'importe qui pourrait modifier les entrĂ©es d'une file qui n'est pas la sienne.
  3. Crée le fichier de la page de détail
    touch "app/dashboard/queues/[id]/page.tsx"New-Item -ItemType File -Force -Path "app/dashboard/queues/[id]/page.tsx"
  4. Colle le contenu de la page de détail
    // app/dashboard/queues/[id]/page.tsx
    import { prisma } from "@/lib/db/client";
    import { auth } from "@/lib/auth/auth";
    import { headers } from "next/headers";
    import { notFound } from "next/navigation";
    import { addEntry, markServed, removeEntry } from "./actions";
    
    export default async function QueueDetailPage({ params }: { params: { id: string } }) {
      const session = await auth.api.getSession({ headers: await headers() });
      const queue = await prisma.queue.findUnique({
        where: { id: params.id },
        include: { entries: { orderBy: { createdAt: "asc" } } },
      });
      if (!queue || queue.ownerId !== session?.user.id) notFound();
    
      return (
        <main className="p-8">
          <h1 className="text-2xl font-bold mb-4">{queue.name}</h1>
          <form action={addEntry.bind(null, queue.id)} className="flex gap-2 mb-6">
            <input name="name" placeholder="Nom de la personne" className="border p-2 rounded" required />
            <button type="submit" className="bg-black text-white px-4 rounded">Ajouter</button>
          </form>
          <ul className="flex flex-col gap-2">
            {queue.entries.map((e) => (
              <li key={e.id} className="flex justify-between border p-3 rounded">
                <span>{e.name} — {e.status}</span>
                <div className="flex gap-2">
                  <form action={markServed.bind(null, e.id)}><button type="submit">Marquer servi</button></form>
                  <form action={removeEntry.bind(null, e.id)}><button type="submit" className="text-red-500">Retirer</button></form>
                </div>
              </li>
            ))}
          </ul>
        </main>
      );
    }
    Les entrĂ©es sont triĂ©es par createdAt croissant (premier arrivĂ©, premier affichĂ©) directement dans la requĂȘte Prisma.
    👁 Comment voir le rĂ©sultat : clique sur une de tes files depuis /dashboard — tu dois arriver sur une page dĂ©diĂ©e avec son nom affichĂ©. Ajoute 2-3 entrĂ©es test via le formulaire — elles doivent apparaĂźtre dans l'ordre d'ajout avec le statut « WAITING ». Clique sur « Marquer servi » : le statut affichĂ© doit passer Ă  « SERVED » sans recharger la page manuellement.
Vérification
06
Réutilisable

Habillage — Tailwind & shadcn/ui

Un rendu visuel cohérent sur tout le site, et une méthode simple pour le modifier plus tard.

Prérequis : étape 5 validée.

  1. Comprends oĂč vit le style
    Il n'y a pas un fichier .css par page. Deux niveaux seulement : les variables de thĂšme globales dans app/globals.css (couleurs, arrondi — gĂ©nĂ©rĂ©es par shadcn init Ă  l'Ă©tape 1, chargĂ©es une seule fois par app/layout.tsx), et les classes Tailwind Ă©crites directement dans le className de chaque composant.
  2. Personnalise les couleurs du thĂšme
    Ouvre app/globals.css — repĂšre le bloc :root { ... } gĂ©nĂ©rĂ© par shadcn init, qui contient des variables comme --primary ou --background. Change leurs valeurs directement dans ce fichier.
    👁 Comment voir le rĂ©sultat : modifie la valeur de --primary, enregistre, recharge localhost:3000/dashboard — n'importe quel composant shadcn/ui affichĂ© sur le dashboard doit changer de couleur automatiquement, sans toucher au code des pages.
  3. Crée une mise en page cohérente, réutilisée sur chaque page
    touch components/container.tsxNew-Item -ItemType File -Force -Path components/container.tsx
    // components/container.tsx
    export default function Container({ children }: { children: React.ReactNode }) {
      return (
        <div className="max-w-5xl mx-auto px-4 py-12">
          {children}
        </div>
      );
    }
    Sans ça, tu rĂ©pĂštes les mĂȘmes classes d'espacement (max-w-5xl mx-auto px-4 py-12) dans chaque page du dashboard — un seul endroit Ă  modifier si tu changes la largeur ou les marges du site.
    👁 Comment voir le rĂ©sultat : enveloppe le contenu retournĂ© par app/dashboard/page.tsx et app/dashboard/queues/[id]/page.tsx avec <Container>...</Container> — chaque page doit s'afficher centrĂ©e, avec la mĂȘme marge, une fois rechargĂ©e.
  4. Remplace le HTML brut par les composants shadcn/ui déjà installés
    // avant — dans app/dashboard/queues/[id]/page.tsx
    <button type="submit" className="bg-black text-white px-4 rounded">Ajouter</button>
    
    // aprĂšs
    import { Button } from "@/components/ui/button";
    <Button type="submit">Ajouter</Button>
    Le composant Button hĂ©rite automatiquement des variables de thĂšme dĂ©finies plus haut — un <button> HTML brut, non. Change --primary et seul le premier ne suivra pas.
  5. Pour modifier le style plus tard
    Garde pnpm dev lancĂ© en continu — le rechargement Ă  chaud applique tes changements dans le navigateur dĂšs que tu sauvegardes, sans redĂ©marrer le serveur. Ouvre le fichier de la page ou du composant concernĂ©, change les classes dans className, enregistre. Pour ajouter un nouveau composant shadcn/ui : pnpm dlx shadcn@latest add <nom> (liste sur ui.shadcn.com). Pour savoir ce que fait une classe (p-4, rounded-lg, text-xl...), consulte le lexique ou tailwindcss.com/docs.
Vérification
07
Réutilisable

Tests — Vitest & Playwright

Automatiser la vérification du parcours critique.

Prérequis : étapes 4 et 5 validées.

  1. Installe et configure Vitest
    pnpm add -D vitest
    Écris un test dans tests/queue.validation.test.ts qui vĂ©rifie que createQueueSchema rejette un nom vide et accepte un nom valide.
  2. Installe et configure Playwright
    pnpm create playwright
    RĂ©pond aux questions de l'installateur (TypeScript, dossier tests-e2e). Écris un test qui : crĂ©e un compte → se connecte → crĂ©e une file → ajoute une entrĂ©e → la marque servie.
  3. Lance les tests
    pnpm exec vitest run
    pnpm exec playwright test
    👁 Comment voir le rĂ©sultat : le terminal affiche un rĂ©sumĂ© avec un nombre de tests « passed » en vert. Playwright peut ouvrir un rapport HTML (pnpm exec playwright show-report) qui rejoue le parcours Ă©tape par Ă©tape, avec captures d'Ă©cran si un test Ă©choue.
Vérification
08
Réutilisable

DĂ©ploiement — Vercel

Une URL publique et fonctionnelle.

Prérequis : étape 7 validée.

  1. Ajoute la génération Prisma au build
    // package.json
    "scripts": { "postinstall": "prisma generate" }
    Sans ça, le build Vercel plante — le client Prisma doit ĂȘtre rĂ©gĂ©nĂ©rĂ© aprĂšs chaque install en production.
  2. Pousse le code sur GitHub
    git remote add origin URL-DE-TON-DEPOT
    git push -u origin master
    👁 Comment voir le rĂ©sultat : rafraĂźchis la page du dĂ©pĂŽt sur GitHub — tes fichiers doivent y apparaĂźtre.
  3. Importe le projet sur Vercel
    Sur vercel.com → Add New → Project → sĂ©lectionne le dĂ©pĂŽt GitHub.
    👁 Comment voir le rĂ©sultat : le tableau de bord Vercel passe de « Building » Ă  « Ready », avec un lien de prĂ©visualisation cliquable.
  4. Renseigne les variables d'environnement
    Copie DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL (mets l'URL Vercel finale) dans Settings → Environment Variables.
    👁 Comment voir le rĂ©sultat : pas d'effet visible immĂ©diatement — redĂ©ploie (« Redeploy ») puis teste Ă  l'Ă©tape suivante.
  5. Déploie et vérifie en ligne
    Ouvre l'URL fournie par Vercel, refais le parcours critique Ă  la main : inscription → crĂ©er une file → ajouter une entrĂ©e → marquer servie.
    👁 Comment voir le rĂ©sultat : sur l'URL *.vercel.app, le parcours complet doit fonctionner exactement comme en local — crĂ©ation de compte, file, entrĂ©e, tout doit persister en base (vĂ©rifiable dans Supabase).
Vérification