SPB Git forge

spb/house-ka

Public
18commits 1branches 0releases
1.9 MBsize
maindefault branch
19 days agolast push
Python 67% TypeScript 18.2% CSS 14.4%
10.1 KB · 273 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// House-Ka — Homes-for-sale aggregator (Canada outside Québec)3// Author: Simon-Pierre Boucher — contact@spboucher.ai4// pages/Account.tsx : user profile — Groupe KA membership card (KA-ID),5//   read-only hub profile (edited at groupe-ka.com), favourites, actions6// -----------------------------------------------------------------------------7import { useEffect, useState } from "react";8import { Link, useNavigate } from "react-router-dom";9import { FavItem, fetchFavorites, fetchMe, logout } from "../api";10import type { Socials, User } from "../api";11import { Ico } from "../components/Icons";1213const fmtEpoch = (ts: number | null | undefined): string => {14  if (!ts) return "—";15  return new Date(ts * 1000).toLocaleDateString("en-CA", {16    day: "numeric", month: "long", year: "numeric",17  });18};1920const SOCIAL_BASE: Record<string, string> = {21  instagram: "https://www.instagram.com/",22  facebook: "https://www.facebook.com/",23  x: "https://x.com/",24  linkedin: "https://www.linkedin.com/in/",25  tiktok: "https://www.tiktok.com/@",26  youtube: "https://www.youtube.com/@",27};2829/** "@handle" or "handle" -> full platform URL; URLs kept as-is */30function socialUrl(key: string, value: string): string {31  const v = value.trim();32  if (/^https?:\/\//i.test(v)) return v;33  if (key === "website") return `https://${v}`;34  return (SOCIAL_BASE[key] ?? "https://") + v.replace(/^@/, "");35}3637const SOCIAL_FIELDS: { key: keyof Socials; label: string }[] = [38  { key: "instagram", label: "Instagram" },39  { key: "facebook", label: "Facebook" },40  { key: "x", label: "X (Twitter)" },41  { key: "linkedin", label: "LinkedIn" },42  { key: "tiktok", label: "TikTok" },43  { key: "youtube", label: "YouTube" },44];4546/** Profile managed at the Groupe KA HUB — READ-ONLY (edit it once at47 *  groupe-ka.com/compte, visible on every Groupe KA platform). */48function HubProfileSection({ me }: { me: User }) {49  const socialLinks = SOCIAL_FIELDS.filter((s) => (me.socials?.[s.key] ?? "").trim());50  return (51    <section className="hub-profile">52      <h3>My Groupe KA profile</h3>53      {me.bio && <p className="hub-bio">{me.bio}</p>}54      <div className="pub-meta">55        {(me.job_title || me.company) && (56          <span className="stat-chip">57            {[me.job_title, me.company].filter(Boolean).join(" · ")}58          </span>59        )}60        {me.city && <span className="stat-chip">{me.city}</span>}61        {me.age != null && <span className="stat-chip">{me.age} yrs</span>}62        {me.website && (63          <a className="stat-chip" href={socialUrl("website", me.website)}64             target="_blank" rel="noopener noreferrer">Website ↗</a>65        )}66        {socialLinks.map((s) => (67          <a key={s.key} className="stat-chip hub-social-chip"68             href={socialUrl(s.key, me.socials![s.key]!)}69             target="_blank" rel="noopener noreferrer" title={s.label}>70            <Ico name={s.key} size={15} /> {s.label}71          </a>72        ))}73      </div>74      <div>75        <a className="btn btn-primary"76           href="https://www.groupe-ka.com/compte"77           target="_blank" rel="noopener noreferrer">78          Edit my profile on groupe-ka.com ↗79        </a>80      </div>81      <p className="hub-hint">82        Your profile is managed at the group level: enter it once, visible on83        every Groupe KA platform.84      </p>85    </section>86  );87}8889/** House-Ka favourites from the "My Ka universe" central store. */90function FavouritesSection() {91  const [items, setItems] = useState<FavItem[] | null>(null);92  useEffect(() => {93    fetchFavorites().then((f) => setItems(f.items)).catch(() => setItems([]));94  }, []);95  if (items === null || items.length === 0) return null;96  return (97    <section className="hub-profile">98      <h3>My favourites ({items.length})</h3>99      <div className="dups-list">100        {items.map((f) => (101          <Link key={f.item_id} className="dup-item" to={f.url}>102            <span className="dup-src">{f.price_label}</span>103            <span className="dup-broker">{f.title}{f.subtitle ? ` — ${f.subtitle}` : ""}</span>104            <span className="dup-go">See the listing <Ico name="arrow" size={13} /></span>105          </Link>106        ))}107      </div>108      <p className="hub-hint">109        Your ♥ favourites follow you across the whole Groupe KA ecosystem —110        manage them on <a href="https://www.groupe-ka.com/mon-ka" target="_blank"111        rel="noopener noreferrer">groupe-ka.com/mon-ka</a>.112      </p>113    </section>114  );115}116117export default function AccountPage() {118  const [me, setMe] = useState<User | null | undefined>(undefined); // undefined = loading119  const [copied, setCopied] = useState(false);120  const nav = useNavigate();121122  useEffect(() => {123    document.title = "My account | House-Ka";124    fetchMe().then((r) => setMe(r.user)).catch(() => setMe(null));125  }, []);126127  const copyKaId = async () => {128    if (!me?.ka_id) return;129    try {130      await navigator.clipboard.writeText(me.ka_id);131      setCopied(true);132      setTimeout(() => setCopied(false), 1800);133    } catch { /* clipboard unavailable: too bad */ }134  };135136  if (me === undefined) {137    return <div className="container profil"><div className="notice">Loading…</div></div>;138  }139140  if (me === null) {141    return (142      <div className="container profil">143        <span className="kicker">My account</span>144        <h1>Sign in for <span className="hl">your profile</span>.</h1>145        <p className="lede">146          Sign in with your <b>KA ID</b> account — you get your member147          identifier, valid across the whole Groupe KA ecosystem.148        </p>149        <a className="btn btn-primary" href="/api/auth/ka/login?next=%2Faccount">150          Sign in with KA ID151        </a>152      </div>153    );154  }155156  return (157    <div className="container profil">158      <span className="kicker">My account</span>159      <h1>160        {me.name ? <>Hi, <span className="hl">{me.name.split(" ")[0]}</span>.</>161                 : <>Your <span className="hl">profile</span>.</>}162      </h1>163164      {/* ——— Groupe KA membership card ——— */}165      <div className="pc" role="img" aria-label={`Membership card ${me.ka_id || me.sub}`}>166        <div className="pc-watermark" aria-hidden="true">KA</div>167        <div className="pc-head">168          <span className="pc-brand">Groupe <span className="pc-ka">KA</span></span>169          <span className="pc-label">Membership card · Groupe KA</span>170        </div>171        <div className="pc-id-block">172          <span className="pc-id-label">KA-ID</span>173          <span className="pc-id">{me.ka_id || "—"}</span>174        </div>175        <div className="pc-foot">176          <div className="pc-holder">177            <span className="pc-holder-name">{me.name || me.email}</span>178            {me.role_label && (179              <span className="pc-role-badge">{me.role_label}</span>180            )}181            <span className="pc-holder-since">Member since {fmtEpoch(me.created_at)}</span>182          </div>183          {me.picture && (184            <img className="pc-avatar" src={me.picture} alt="" referrerPolicy="no-referrer" />185          )}186        </div>187        <div className="pc-strip" aria-hidden="true">188          {Array.from({ length: 28 }).map((_, i) => <i key={i} />)}189        </div>190      </div>191192      <button className={`btn btn-ghost pc-copy ${copied ? "ok" : ""}`} onClick={copyKaId}>193        {copied ? "✓ Copied" : "Copy my KA-ID"}194      </button>195196      {/* ——— Details ——— */}197      <section className="profil-grid">198        <div className="pg-item">199          <span className="pg-label">Name</span>200          <span className="pg-value">{me.name || "—"}</span>201        </div>202        <div className="pg-item">203          <span className="pg-label">Email</span>204          <span className="pg-value">{me.email || "—"}</span>205        </div>206        <div className="pg-item">207          <span className="pg-label">Member ID</span>208          <span className="pg-value mono">{me.ka_id || me.sub}</span>209        </div>210        <div className="pg-item">211          <span className="pg-label">Sign-in</span>212          <span className="pg-value">213            {me.provider === "ka-id" ? "KA ID (groupe-ka.com)" : "Google account"}214          </span>215        </div>216        {me.city && (217          <div className="pg-item">218            <span className="pg-label">City</span>219            <span className="pg-value">{me.city}</span>220          </div>221        )}222        {me.role_label && (223          <div className="pg-item">224            <span className="pg-label">Status</span>225            <span className="pg-value">{me.role_label}</span>226          </div>227        )}228        <div className="pg-item">229          <span className="pg-label">Member since</span>230          <span className="pg-value">{fmtEpoch(me.created_at)}</span>231        </div>232        <div className="pg-item">233          <span className="pg-label">Last sign-in</span>234          <span className="pg-value">{fmtEpoch(me.last_login)}</span>235        </div>236      </section>237238      <FavouritesSection />239240      {me.profile_source === "groupe-ka" && <HubProfileSection me={me} />}241242      {me.profile_source === "groupe-ka" && me.public && me.public_url && (243        <p className="profil-note">244          Your public profile:{" "}245          <a href={me.public_url} target="_blank" rel="noopener noreferrer" className="mono">246            {me.public_url.replace(/^https?:\/\//, "")}247          </a>{" "}248          — visibility is managed on groupe-ka.com/compte.249        </p>250      )}251252      <p className="profil-note">253        Your <b>KA-ID</b> is your unique identifier across the{" "}254        <a href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer">255          Groupe KA256        </a>{" "}257        ecosystem — it follows you on every platform of the group. House-Ka258        keeps only the information on this profile; nothing else, never sold.259      </p>260261      {/* ——— Actions ——— */}262      <div className="profil-actions">263        <button264          className="btn btn-ghost"265          onClick={async () => { await logout(); nav("/"); window.location.reload(); }}266        >267          Sign out268        </button>269      </div>270    </div>271  );272}273