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

E-commerce

Une boutique en ligne simple : catalogue, panier, paiement réel via Stripe.

Sommaire
01Initialiser le projet Next.js
02Base de donnĂ©es — Prisma + Supabase
03Authentification — Better Auth
04Paiement — Stripe
05Catalogue, panier & commandes
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 sont valables pour n'importe quel projet de ta stack. Les Ă©tapes spĂ©cifique sont propres Ă  cette boutique — adapte le catalogue Ă  tes propres produits.
📖 C'est quoi chaque techno (Stripe, 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 les tables Produit, Commande et LigneCommande 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 Produit {
      id        String   @id @default(cuid())
      nom       String
      prix      Int      // en centimes, pour éviter les erreurs d'arrondi
      stock     Int      @default(0)
      createdAt DateTime @default(now())
    }
    
    model Commande {
      id        String   @id @default(cuid())
      clientId  String
      statut    String   @default("EN_ATTENTE")
      total     Int
      createdAt DateTime @default(now())
      lignes    LigneCommande[]
    }
    
    model LigneCommande {
      id           String   @id @default(cuid())
      commandeId   String
      commande     Commande @relation(fields: [commandeId], references: [id])
      produitId    String
      quantite     Int
      prixUnitaire Int
    }
    Les prix sont stockĂ©s en centimes (entiers) plutĂŽt qu'en euros dĂ©cimaux — Ă©vite les erreurs d'arrondi classiques avec les nombres flottants.
  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 Produit, Commande et LigneCommande 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

Des comptes clients — historique de commandes, adresse — et une zone /dashboard protĂ©gĂ©e pour l'admin.

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 client Prisma partagé
    touch lib/db/client.tsNew-Item -ItemType File -Force -Path lib/db/client.ts
    Le dossier lib/db existe dĂ©jĂ  depuis l'Ă©tape 1 — il ne manque que le fichier. Ouvre-le et colle ce contenu :
    // 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).
  3. Crée la config serveur
    touch lib/auth/auth.tsNew-Item -ItemType File -Force -Path lib/auth/auth.ts
    // 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 },
    });
  4. 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.
  5. Ajoute le secret dans .env
    # .env
    BETTER_AUTH_SECRET="génÚre une chaßne aléatoire longue"
    BETTER_AUTH_URL="http://localhost:3000"
  6. Crée 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 [...all] (avec les crochets) est la syntaxe Next.js pour capturer toutes les sous-routes — ne le renomme pas.
    // 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.
  7. Crée le 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();
    C'est ce client (cÎté navigateur) que les pages /signup et /login vont utiliser pour appeler signUp.email(...) et signIn.email(...).
  8. Crée les pages d'inscription et de connexion
    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
    // app/signup/page.tsx
    "use client";
    import { useState } from "react";
    import { authClient } from "@/lib/auth/client";
    
    export default function SignupPage() {
      const [email, setEmail] = useState("");
      const [password, setPassword] = useState("");
    
      async function handleSubmit(e: React.FormEvent) {
        e.preventDefault();
        await authClient.signUp.email({ email, password, name: email });
      }
    
      return (
        <form onSubmit={handleSubmit} className="p-8 flex flex-col gap-2 max-w-sm">
          <input type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} required />
          <input type="password" placeholder="Mot de passe" value={password} onChange={(e) => setPassword(e.target.value)} required />
          <button type="submit">Créer un compte</button>
        </form>
      );
    }
    // app/login/page.tsx
    "use client";
    import { useState } from "react";
    import { authClient } from "@/lib/auth/client";
    
    export default function LoginPage() {
      const [email, setEmail] = useState("");
      const [password, setPassword] = useState("");
    
      async function handleSubmit(e: React.FormEvent) {
        e.preventDefault();
        await authClient.signIn.email({ email, password });
      }
    
      return (
        <form onSubmit={handleSubmit} className="p-8 flex flex-col gap-2 max-w-sm">
          <input type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} required />
          <input type="password" placeholder="Mot de passe" value={password} onChange={(e) => setPassword(e.target.value)} required />
          <button type="submit">Se connecter</button>
        </form>
      );
    }
    👁 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 depuis /login avec les mĂȘmes identifiants.
  9. ProtĂšge /dashboard
    touch middleware.tsNew-Item -ItemType File -Force -Path middleware.ts
    // middleware.ts (à la racine du projet, à cÎté de package.json)
    import { NextRequest, NextResponse } from "next/server";
    import { auth } from "@/lib/auth/auth";
    
    export async function middleware(request: NextRequest) {
      const session = await auth.api.getSession({ headers: request.headers });
      if (!session && request.nextUrl.pathname.startsWith("/dashboard")) {
        return NextResponse.redirect(new URL("/login", request.url));
      }
      return NextResponse.next();
    }
    
    export const config = { matcher: ["/dashboard/:path*"] };
    Le matcher limite le middleware aux routes /dashboard/* — il ne s'exĂ©cute pas sur le reste du site.
    👁 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 e-commerce

Paiement — Stripe

Encaisser un vrai paiement carte, en toute sécurité.

Prérequis : étape 3 validée.

  1. Crée un compte Stripe (mode test)
    Sur stripe.com — le mode test permet de simuler des paiements sans vraie carte bancaire, avec le numĂ©ro 4242 4242 4242 4242.
  2. Installe le SDK
    pnpm add stripe
    👁 Comment voir le rĂ©sultat : stripe apparaĂźt dans package.json.
  3. Ajoute les clés dans .env
    # .env
    STRIPE_SECRET_KEY="sk_test_..."
    STRIPE_WEBHOOK_SECRET="whsec_..."
    Les deux se trouvent dans le tableau de bord Stripe, section Développeurs.
  4. Crée une session de paiement
    mkdir -p lib/stripe && touch lib/stripe/checkout.tsmkdir lib/stripe; New-Item -ItemType File -Force -Path lib/stripe/checkout.ts
    // lib/stripe/checkout.ts
    "use server";
    import Stripe from "stripe";
    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
    
    export async function createCheckoutSession(commandeId: string, items: { nom: string; prix: number; quantite: number }[]) {
      const session = await stripe.checkout.sessions.create({
        mode: "payment",
        line_items: items.map((i) => ({
          price_data: {
            currency: "eur",
            product_data: { name: i.nom },
            unit_amount: i.prix,
          },
          quantity: i.quantite,
        })),
        metadata: { commandeId },
        success_url: `${process.env.BETTER_AUTH_URL}/commande/succes`,
        cancel_url: `${process.env.BETTER_AUTH_URL}/panier`,
      });
      return session.url;
    }
    metadata: { commandeId } permet au webhook de retrouver quelle commande marquer comme payĂ©e — sans ça, impossible de savoir quel paiement correspond Ă  quelle ligne en base.
    👁 Comment voir le rĂ©sultat : se testera concrĂštement une fois la page panier branchĂ©e Ă  l'Ă©tape 5 — pour l'instant, vĂ©rifie juste que le fichier ne contient pas d'erreur TypeScript dans ton Ă©diteur.
  5. Crée la route webhook
    mkdir -p app/api/stripe/webhook && touch app/api/stripe/webhook/route.tsmkdir app/api/stripe/webhook; New-Item -ItemType File -Force -Path app/api/stripe/webhook/route.ts
    // app/api/stripe/webhook/route.ts
    import { headers } from "next/headers";
    import Stripe from "stripe";
    import { prisma } from "@/lib/db/client";
    
    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
    
    export async function POST(request: Request) {
      const body = await request.text();
      const signature = (await headers()).get("stripe-signature")!;
    
      const event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
    
      if (event.type === "checkout.session.completed") {
        const session = event.data.object as Stripe.Checkout.Session;
        const commandeId = session.metadata?.commandeId;
        if (commandeId) {
          await prisma.commande.update({
            where: { id: commandeId },
            data: { statut: "PAYEE" },
          });
        }
      }
    
      return new Response(null, { status: 200 });
    }
    Ne marque jamais une commande payĂ©e depuis le navigateur (cĂŽtĂ© client) — seul le webhook, signĂ© par Stripe, fait foi. Sinon n'importe qui peut appeler ton API et se dĂ©clarer « payĂ© » sans payer. session.metadata.commandeId vient de l'Ă©tape prĂ©cĂ©dente : c'est ce qui cible la bonne commande.
    👁 Comment voir le rĂ©sultat : lance stripe listen --forward-to localhost:3000/api/stripe/webhook dans un terminal sĂ©parĂ© — il doit afficher Ready! Your webhook signing secret is whsec_.... Fais un paiement test (carte 4242 4242 4242 4242) : ce terminal doit logguer l'Ă©vĂ©nement checkout.session.completed.
Vérification

PiĂšge frĂ©quent : en local, Stripe ne peut pas atteindre localhost pour envoyer le webhook — utilise stripe listen --forward-to localhost:3000/api/stripe/webhook (CLI Stripe) pendant le dĂ©veloppement.

05
Spécifique e-commerce

Catalogue, panier & commandes

Parcourir les produits, les mettre au panier, passer commande.

Prérequis : étape 4 validée.

  1. Installe et crée le store du panier
    pnpm add zustand
    mkdir -p lib/store && touch lib/store/panier.tsmkdir lib/store; New-Item -ItemType File -Force -Path lib/store/panier.ts
    // lib/store/panier.ts
    import { create } from "zustand";
    
    type ArticlePanier = { id: string; nom: string; prix: number; quantite: number };
    
    type PanierStore = {
      articles: ArticlePanier[];
      ajouter: (produit: { id: string; nom: string; prix: number }) => void;
      retirer: (id: string) => void;
      vider: () => void;
    };
    
    export const usePanier = create<PanierStore>((set) => ({
      articles: [],
      ajouter: (produit) =>
        set((state) => {
          const existant = state.articles.find((a) => a.id === produit.id);
          if (existant) {
            return {
              articles: state.articles.map((a) =>
                a.id === produit.id ? { ...a, quantite: a.quantite + 1 } : a
              ),
            };
          }
          return { articles: [...state.articles, { ...produit, quantite: 1 }] };
        }),
      retirer: (id) => set((state) => ({ articles: state.articles.filter((a) => a.id !== id) })),
      vider: () => set({ articles: [] }),
    }));
    Le panier vit dans le navigateur avant le paiement — pas besoin de base de donnĂ©es pour ça. Zustand garde l'Ă©tat du panier accessible depuis n'importe quel composant, sans tout faire remonter via les props.
  2. Crée le bouton « Ajouter au panier »
    touch components/ajouter-au-panier-button.tsxNew-Item -ItemType File -Force -Path components/ajouter-au-panier-button.tsx
    // components/ajouter-au-panier-button.tsx
    "use client";
    import { usePanier } from "@/lib/store/panier";
    
    export default function AjouterAuPanierButton({ produit }: { produit: { id: string; nom: string; prix: number } }) {
      const ajouter = usePanier((state) => state.ajouter);
      return <button onClick={() => ajouter(produit)}>Ajouter au panier</button>;
    }
    Ce bouton doit ĂȘtre un composant client Ă  part ("use client") car usePanier utilise du state du navigateur — la page catalogue, elle, peut rester un Server Component qui lit Prisma directement.
  3. Page catalogue
    mkdir -p app/produits && touch app/produits/page.tsxmkdir app/produits; New-Item -ItemType File -Force -Path app/produits/page.tsx
    // app/produits/page.tsx
    import { prisma } from "@/lib/db/client";
    import AjouterAuPanierButton from "@/components/ajouter-au-panier-button";
    
    export default async function ProduitsPage() {
      const produits = await prisma.produit.findMany();
    
      return (
        <main className="p-8 grid grid-cols-1 sm:grid-cols-3 gap-4">
          {produits.map((p) => (
            <div key={p.id} className="border rounded-lg p-4">
              <h2 className="font-bold">{p.nom}</h2>
              <p>{(p.prix / 100).toFixed(2)} €</p>
              <AjouterAuPanierButton produit={{ id: p.id, nom: p.nom, prix: p.prix }} />
            </div>
          ))}
        </main>
      );
    }
    👁 Comment voir le rĂ©sultat : ouvre localhost:3000/produits — les produits que tu as ajoutĂ©s dans Prisma Studio doivent s'afficher, un par carte, avec leur prix converti en euros.
  4. Page panier et commande avant paiement
    mkdir -p app/panier && touch app/panier/page.tsx app/panier/actions.tsmkdir app/panier; New-Item -ItemType File -Force -Path app/panier/page.tsx, app/panier/actions.ts
    // app/panier/actions.ts
    "use server";
    import { prisma } from "@/lib/db/client";
    import { createCheckoutSession } from "@/lib/stripe/checkout";
    
    type ArticlePanier = { id: string; nom: string; prix: number; quantite: number };
    
    export async function creerCommandeEtPayer(articles: ArticlePanier[]) {
      const total = articles.reduce((sum, a) => sum + a.prix * a.quantite, 0);
    
      const commande = await prisma.commande.create({
        data: {
          clientId: "TODO: remplace par l'id de l'utilisateur connecté (session Better Auth)",
          statut: "EN_ATTENTE",
          total,
          lignes: {
            create: articles.map((a) => ({
              produitId: a.id,
              quantite: a.quantite,
              prixUnitaire: a.prix,
            })),
          },
        },
      });
    
      return createCheckoutSession(commande.id, articles);
    }
    // app/panier/page.tsx
    "use client";
    import { usePanier } from "@/lib/store/panier";
    import { creerCommandeEtPayer } from "./actions";
    
    export default function PanierPage() {
      const { articles, retirer } = usePanier();
      const total = articles.reduce((sum, a) => sum + a.prix * a.quantite, 0);
    
      async function handlePayer() {
        const url = await creerCommandeEtPayer(articles);
        window.location.href = url;
      }
    
      return (
        <main className="p-8">
          <h1 className="text-2xl font-bold mb-4">Panier</h1>
          <ul>
            {articles.map((a) => (
              <li key={a.id} className="flex justify-between gap-4">
                <span>{a.nom} × {a.quantite}</span>
                <span>{((a.prix * a.quantite) / 100).toFixed(2)} €</span>
                <button onClick={() => retirer(a.id)}>Retirer</button>
              </li>
            ))}
          </ul>
          <p className="mt-4 font-bold">Total : {(total / 100).toFixed(2)} €</p>
          <button onClick={handlePayer} disabled={articles.length === 0}>Payer</button>
        </main>
      );
    }
    Remplace le TODO de clientId par l'id rĂ©cupĂ©rĂ© via auth.api.getSession(...) (Ă©tape 3) une fois l'utilisateur connectĂ© — sans ça, aucune commande ne peut ĂȘtre reliĂ©e Ă  un client. creerCommandeEtPayer crĂ©e d'abord la Commande en base avec statut EN_ATTENTE, puis redirige vers Stripe — le webhook de l'Ă©tape 4 la fera ensuite passer Ă  PAYEE.
    👁 Comment voir le rĂ©sultat : depuis le catalogue, ajoute 2-3 produits au panier puis ouvre localhost:3000/panier — ils doivent y ĂȘtre listĂ©s avec le bon total. Clique sur « Payer » : juste avant la redirection vers Stripe, une nouvelle ligne EN_ATTENTE doit apparaĂźtre dans la table Commande (Prisma Studio).
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 — n'importe quel composant shadcn/ui dĂ©jĂ  installĂ© 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 fichier page.tsx (catalogue, panier...) — 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/page.tsx, la page catalogue et la page panier 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
    // components/ajouter-au-panier-button.tsx — avant
    <button onClick={() => ajouter(produit)}>Ajouter au panier</button>
    
    // aprĂšs
    import { Button } from "@/components/ui/button";
    <Button onClick={() => ajouter(produit)}>Ajouter au panier</Button>
    Le composant Button hĂ©rite automatiquement des variables de thĂšme dĂ©finies plus haut — un <button> HTML brut, non. Fais le mĂȘme remplacement pour le bouton « Retirer » et le bouton « Payer » du panier (Ă©tape 5).
  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/panier.validation.test.ts qui vĂ©rifie que calculTotalPanier calcule correctement le total du panier avec plusieurs articles.
  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 → ajoute un produit au panier → paie avec une carte de test Stripe → vĂ©rifie que la commande apparaĂźt en base.
  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 aussi ouvrir un rapport HTML (pnpm exec playwright show-report) qui montre 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 (URL Vercel finale), STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET dans Settings → Environment Variables.
    👁 Comment voir le rĂ©sultat : pas d'effet visible immĂ©diatement — redĂ©ploie (« Redeploy ») puis teste Ă  l'Ă©tape suivante.
  5. Configure le webhook Stripe en production
    Dans le tableau de bord Stripe : ajoute un endpoint pointant vers https://tondomaine.vercel.app/api/stripe/webhook — l'URL locale utilisĂ©e avec la CLI ne fonctionne plus une fois dĂ©ployĂ©.
    👁 Comment voir le rĂ©sultat : dans le tableau de bord Stripe, section Webhooks, l'endpoint doit afficher un statut vert « Enabled ».
  6. Déploie et vérifie en ligne
    Ouvre l'URL fournie par Vercel, refais le parcours critique Ă  la main : crĂ©er un compte → ajouter un produit au panier → payer avec une carte de test → vĂ©rifier la commande.
    👁 Comment voir le rĂ©sultat : sur l'URL *.vercel.app, un paiement test complet doit faire passer la commande en PAYEE — vĂ©rifiable dans Prisma Studio ou directement dans Supabase.
Vérification