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";56export type ProfileInitial = {7 name: string;8 bio: string;9 city: string;10 phone: string;11 website: string;12 job_title: string;13 company: string;14 birth_date: string;15 socials: Record<string, string>;16 isPublic: boolean;17 kaId: string;18};1920const SOCIALS: { key: string; label: string; placeholder: string }[] = [21 { key: "instagram", label: "Instagram", placeholder: "@pseudo ou URL" },22 { key: "facebook", label: "Facebook", placeholder: "profil ou URL" },23 { key: "x", label: "X (Twitter)", placeholder: "@pseudo ou URL" },24 { key: "linkedin", label: "LinkedIn", placeholder: "profil ou URL" },25 { key: "tiktok", label: "TikTok", placeholder: "@pseudo ou URL" },26 { key: "youtube", label: "YouTube", placeholder: "chaîne ou URL" },27];2829export default function ProfileForm({ initial }: { initial: ProfileInitial }) {30 const [form, setForm] = useState(initial);31 const [saving, setSaving] = useState(false);32 const [saved, setSaved] = useState(false);33 const [error, setError] = useState<string | null>(null);34 const [isPublic, setIsPublic] = useState(initial.isPublic);35 const [copied, setCopied] = useState(false);3637 const publicUrl = `https://www.groupe-ka.com/u/${initial.kaId}`;3839 async function save() {40 setSaving(true);41 setError(null);42 const res = await fetch("/api/profile", {43 method: "POST",44 headers: { "Content-Type": "application/json" },45 body: JSON.stringify(form),46 });47 const data = await res.json().catch(() => ({}));48 setSaving(false);49 if (!res.ok) {50 setError(data.error ?? "Enregistrement impossible — réessayez.");51 return;52 }53 setSaved(true);54 setTimeout(() => setSaved(false), 2000);55 }5657 async function togglePublic(enabled: boolean) {58 await fetch("/api/profile/public", {59 method: "POST",60 headers: { "Content-Type": "application/json" },61 body: JSON.stringify({ enabled }),62 });63 setIsPublic(enabled);64 }6566 const field =67 (key: keyof ProfileInitial, max: number) =>68 (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>69 setForm({ ...form, [key]: e.target.value.slice(0, max) });7071 return (72 <div className="space-y-5">73 <div className="grid gap-4 sm:grid-cols-2">74 <label className="block">75 <span className="klabel">Nom affiché</span>76 <input className="field mt-[6px]" value={form.name} onChange={field("name", 80)} placeholder="Prénom Nom" />77 </label>78 <label className="block">79 <span className="klabel">Ville</span>80 <input className="field mt-[6px]" value={form.city} onChange={field("city", 60)} placeholder="Québec, Montréal…" />81 </label>82 </div>8384 <label className="block">85 <span className="klabel">Bio ({280 - form.bio.length} caractères restants)</span>86 <textarea87 className="field mt-[6px] resize-y"88 rows={3}89 value={form.bio}90 onChange={field("bio", 280)}91 placeholder="Présentez-vous en quelques mots…"92 />93 </label>9495 <div className="grid gap-4 sm:grid-cols-2">96 <label className="block">97 <span className="klabel">Emploi / titre</span>98 <input className="field mt-[6px]" value={form.job_title} onChange={field("job_title", 80)} placeholder="Gestionnaire, courtier, développeur…" />99 </label>100 <label className="block">101 <span className="klabel">Entreprise</span>102 <input className="field mt-[6px]" value={form.company} onChange={field("company", 80)} placeholder="Nom de l'entreprise" />103 </label>104 <label className="block">105 <span className="klabel">Date de naissance</span>106 <input className="field mt-[6px]" type="date" value={form.birth_date} onChange={field("birth_date", 10)} />107 </label>108 <label className="block">109 <span className="klabel">Téléphone</span>110 <input className="field mt-[6px]" value={form.phone} onChange={field("phone", 40)} placeholder="418 555-0123" />111 </label>112 </div>113114 <label className="block">115 <span className="klabel">Site web</span>116 <input className="field mt-[6px]" value={form.website} onChange={field("website", 300)} placeholder="https://…" />117 </label>118119 <div>120 <p className="klabel">Réseaux sociaux</p>121 <div className="mt-2 grid gap-3 sm:grid-cols-2">122 {SOCIALS.map((s) => (123 <label key={s.key} className="block">124 <input125 className="field"126 value={form.socials[s.key] ?? ""}127 onChange={(e) =>128 setForm({129 ...form,130 socials: { ...form.socials, [s.key]: e.target.value.slice(0, 200) },131 })132 }133 placeholder={`${s.label} — ${s.placeholder}`}134 />135 </label>136 ))}137 </div>138 </div>139140 {error && (141 <p className="rounded-md border-[1.5px] border-[#b3423a] bg-[#fbe9e7] px-4 py-3 text-[13px] font-medium text-[#7c2f2a]">142 {error}143 </p>144 )}145 <button type="button" className="btn btn-primary" disabled={saving} onClick={save}>146 {saved ? "✓ Enregistré" : saving ? "Enregistrement…" : "Enregistrer mon profil"}147 </button>148 <p className="gk-mono text-[10.5px] text-ink-3">149 Ces informations sont la source de vérité de votre profil sur TOUTES150 les plateformes ·Ka (Lou·Ka, Immo·Ka, Vrai-Prix…). Le courriel et le151 téléphone ne sont jamais affichés publiquement.152 </p>153154 {/* ——— Profil public ——— */}155 <div className="border-t-[1.5px] border-dashed border-[rgba(20,24,20,0.25)] pt-5">156 <p className="gk-display text-[16px] font-bold uppercase">Profil public</p>157 <p className="mt-1 text-[13px] text-ink-2">158 Activez votre page publique pour partager votre profil — nom, photo,159 statut, bio, emploi, ville et réseaux sociaux.{" "}160 <strong>Jamais votre courriel ni votre téléphone.</strong>161 </p>162 <div className="mt-3 inline-flex overflow-hidden rounded-md border-[1.5px] border-ink" role="group">163 {([[false, "Privé"], [true, "Public"]] as const).map(([v, l]) => (164 <button165 key={l}166 type="button"167 onClick={() => togglePublic(v)}168 className={`px-5 py-2 text-[13px] font-bold transition-colors ${isPublic === v ? "bg-ink text-lime" : "bg-surface text-ink hover:bg-lime-soft"}`}169 >170 {l}171 </button>172 ))}173 </div>174 {isPublic && (175 <div className="mt-3 flex flex-wrap items-center gap-3 rounded-md border-[1.5px] border-green bg-lime-soft px-4 py-3">176 <a177 href={publicUrl}178 target="_blank"179 rel="noopener noreferrer"180 className="gk-mono text-[12.5px] break-all text-green-deep underline underline-offset-4"181 >182 {publicUrl.replace("https://", "")}183 </a>184 <button185 type="button"186 className="btn btn-ghost !min-h-[34px] !px-3 !text-[12px]"187 onClick={async () => {188 try {189 await navigator.clipboard.writeText(publicUrl);190 setCopied(true);191 setTimeout(() => setCopied(false), 1800);192 } catch {}193 }}194 >195 {copied ? "✓ Copié" : "Copier le lien"}196 </button>197 </div>198 )}199 </div>200 </div>201 );202}203