Python 67%
TypeScript 18.2%
CSS 14.4%
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