SPB Git

spb/groupe-ka Public

Groupe KA — site du holding + KA ID (compte unique & SSO des 7 plateformes). Next.js 16, SQLite, Google & Apple login.

TypeScript 93.9% CSS 6%
8.7 KB · 242 lines tsx
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2"use client";34import { useState } from "react";5import { ROLES } from "@/lib/roles";67const ERRORS: Record<string, string> = {8  "google-non-configure":9    "La connexion Google n'est pas encore activée sur ce serveur.",10  "etat-oauth-invalide":11    "La session Google a expiré — réessayez.",12  "echange-google-refuse": "Google a refusé l'échange — réessayez.",13  "id-token-absent": "Réponse Google incomplète — réessayez.",14  "id-token-invalide": "Réponse Google invalide — réessayez.",15  "profil-google-incomplet":16    "Votre compte Google n'a pas de courriel vérifié.",17  "apple-non-configure":18    "La connexion Apple n'est pas encore activée sur ce serveur.",19  "apple-refuse": "La connexion Apple a été annulée — réessayez.",20  "echange-apple-refuse": "Apple a refusé l'échange — réessayez.",21  "profil-apple-incomplet":22    "Votre compte Apple n'a pas de courriel vérifié.",23};2425export default function AuthForm({26  next,27  error,28  via,29}: {30  next: string;31  error?: string;32  via?: string;33}) {34  const [mode, setMode] = useState<"login" | "register">("login");35  const [msg, setMsg] = useState<string | null>(36    error ? (ERRORS[error] ?? "Une erreur est survenue — réessayez.") : null,37  );38  const [busy, setBusy] = useState(false);3940  async function submit(e: React.FormEvent<HTMLFormElement>) {41    e.preventDefault();42    setBusy(true);43    setMsg(null);44    const fd = new FormData(e.currentTarget);45    const body = Object.fromEntries(fd.entries());46    const res = await fetch(`/api/auth/${mode}`, {47      method: "POST",48      headers: { "Content-Type": "application/json" },49      body: JSON.stringify(body),50    });51    const data = await res.json().catch(() => ({}));52    if (res.ok) {53      window.location.href = next;54    } else {55      setMsg(data.error ?? "Une erreur est survenue — réessayez.");56      setBusy(false);57    }58  }5960  return (61    <div className="gk-card p-6 sm:p-8">62      {via && (63        <p className="stat-chip mb-5 !text-[11px]">64          <span className="pulse-dot" aria-hidden="true" />65          Connexion demandée par {via} — vous y retournerez automatiquement66        </p>67      )}6869      <a70        href={`/api/auth/google?next=${encodeURIComponent(next)}`}71        className="btn btn-google w-full"72      >73        {/* « G » officiel Google — couleurs de marque */}74        <svg width="18" height="18" viewBox="0 0 48 48" aria-hidden="true">75          <path76            fill="#EA4335"77            d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"78          />79          <path80            fill="#4285F4"81            d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"82          />83          <path84            fill="#FBBC05"85            d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"86          />87          <path88            fill="#34A853"89            d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"90          />91        </svg>92        Continuer avec Google93      </a>9495      <a96        href={`/api/auth/apple?next=${encodeURIComponent(next)}`}97        className="btn btn-apple mt-3 w-full"98      >99        {/* Pomme Apple — blanche sur noir, style officiel */}100        <svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">101          <path102            fill="currentColor"103            d="M16.36 12.76c-.03-2.62 2.14-3.88 2.24-3.94-1.22-1.79-3.12-2.03-3.8-2.06-1.61-.16-3.15.95-3.97.95-.82 0-2.09-.93-3.43-.9-1.77.03-3.4 1.03-4.31 2.6-1.84 3.19-.47 7.9 1.32 10.49.88 1.27 1.92 2.69 3.29 2.64 1.32-.05 1.82-.85 3.42-.85 1.6 0 2.05.85 3.44.82 1.42-.02 2.32-1.29 3.19-2.56 1-1.46 1.41-2.87 1.44-2.95-.03-.01-2.76-1.06-2.83-4.24ZM13.75 4.99c.73-.88 1.22-2.11 1.08-3.33-1.05.04-2.31.7-3.06 1.58-.67.78-1.26 2.02-1.1 3.22 1.17.09 2.36-.6 3.08-1.47Z"104          />105        </svg>106        Continuer avec Apple107      </a>108109      <div className="my-6 flex items-center gap-4">110        <span className="h-[1.5px] flex-1 bg-[rgba(20,24,20,0.18)]" />111        <span className="klabel">ou par courriel</span>112        <span className="h-[1.5px] flex-1 bg-[rgba(20,24,20,0.18)]" />113      </div>114115      <div className="mb-5 grid grid-cols-2 gap-2" role="tablist">116        {(117          [118            ["login", "Se connecter"],119            ["register", "Créer un compte"],120          ] as const121        ).map(([m, label]) => (122          <button123            key={m}124            role="tab"125            aria-selected={mode === m}126            onClick={() => {127              setMode(m);128              setMsg(null);129            }}130            className={`btn ${mode === m ? "btn-lime" : "btn-ghost"} !min-h-[40px] !text-[13px]`}131            type="button"132          >133            {label}134          </button>135        ))}136      </div>137138      <form onSubmit={submit} className="space-y-4">139        {mode === "register" && (140          <>141            <label className="block">142              <span className="klabel">Votre nom</span>143              <input144                name="name"145                type="text"146                required147                minLength={2}148                autoComplete="name"149                className="field mt-[6px]"150                placeholder="Prénom Nom"151              />152            </label>153            <fieldset>154              <legend className="klabel">Vous êtes…</legend>155              <div className="mt-[6px] space-y-2">156                {Object.entries(ROLES).map(([value, r]) => (157                  <label158                    key={value}159                    className="flex cursor-pointer items-start gap-3 rounded-md border-[1.5px] border-[rgba(20,24,20,0.3)] bg-surface px-4 py-3 transition-colors has-[:checked]:border-ink has-[:checked]:bg-lime-soft has-[:checked]:shadow-[3px_3px_0_rgba(20,24,20,0.15)]"160                  >161                    <input162                      type="radio"163                      name="role"164                      value={value}165                      required166                      className="mt-[3px] accent-[#1c5c41]"167                    />168                    <span>169                      <span className="gk-display block text-[14px] font-bold">170                        {r.label}171                      </span>172                      <span className="mt-[2px] block text-[12px] leading-snug text-ink-2">173                        {r.desc}174                      </span>175                    </span>176                  </label>177                ))}178              </div>179              <p className="gk-mono mt-2 text-[10.5px] text-ink-3">180                Ce statut sera affiché sur votre carte de membre.181              </p>182            </fieldset>183          </>184        )}185        <label className="block">186          <span className="klabel">Courriel</span>187          <input188            name="email"189            type="email"190            required191            autoComplete="email"192            className="field mt-[6px]"193            placeholder="vous@exemple.com"194          />195        </label>196        <label className="block">197          <span className="klabel">198            Mot de passe{mode === "register" ? " (8 caractères min.)" : ""}199          </span>200          <input201            name="password"202            type="password"203            required204            minLength={8}205            autoComplete={mode === "register" ? "new-password" : "current-password"}206            className="field mt-[6px]"207            placeholder="••••••••"208          />209        </label>210211        {msg && (212          <p className="rounded-md border-[1.5px] border-[#b3423a] bg-[#fbe9e7] px-4 py-3 text-[13px] font-medium text-[#7c2f2a]">213            {msg}214          </p>215        )}216217        <button type="submit" disabled={busy} className="btn btn-primary w-full">218          {busy219            ? "Un instant…"220            : mode === "login"221              ? "Se connecter"222              : "Créer mon KA ID"}223        </button>224      </form>225226      <p className="mt-6 border-t-[1.5px] border-dashed border-[rgba(20,24,20,0.25)] pt-4 text-[12px] leading-relaxed text-ink-2">227        Un seul compte — le <strong className="gk-display">KA ID</strong> —228        pour toutes les plateformes du groupe. En continuant, vous acceptez229        les{" "}230        <a href="/conditions" className="underline underline-offset-4">231          conditions d&apos;utilisation232        </a>{" "}233        et la{" "}234        <a href="/confidentialite" className="underline underline-offset-4">235          politique de confidentialité236        </a>237        .238      </p>239    </div>240  );241}242