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%
7.6 KB · 203 lines tsx
Raw Blame History
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