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%
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'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