Python 68.8%
TypeScript 18.6%
CSS 8.7%
JavaScript 3.3%
HTML 0.6%
1// -----------------------------------------------------------------------------2// Rent-Ka — Rental listings aggregator (Canada, outside Québec)3// Author: Simon-Pierre Boucher — contact@spboucher.ai4// pages/PublicProfile.tsx: PUBLIC member profile — /u/{ka_id}5// Shareable member card (name, avatar, seniority, KA-ID) — never the6// email. Visible only if the member enabled their public profile.7// -----------------------------------------------------------------------------8import { useEffect, useState } from "react";9import { Link, useParams } from "react-router-dom";10import { PublicProfile, Socials, fetchPublicProfile } from "../api";11import {12 IcoCompass, IcoFacebook, IcoInstagram, IcoLinkedIn, IcoTikTok, IcoX,13 IcoYouTube,14} from "../components/Icons";1516const SOCIAL_META: { key: keyof Socials; label: string; icon: JSX.Element }[] = [17 { key: "instagram", label: "Instagram", icon: <IcoInstagram size={17} /> },18 { key: "facebook", label: "Facebook", icon: <IcoFacebook size={17} /> },19 { key: "x", label: "X (Twitter)", icon: <IcoX size={17} /> },20 { key: "linkedin", label: "LinkedIn", icon: <IcoLinkedIn size={17} /> },21 { key: "tiktok", label: "TikTok", icon: <IcoTikTok size={17} /> },22 { key: "youtube", label: "YouTube", icon: <IcoYouTube size={17} /> },23];2425const SOCIAL_BASE: Record<string, string> = {26 instagram: "https://www.instagram.com/",27 facebook: "https://www.facebook.com/",28 x: "https://x.com/",29 linkedin: "https://www.linkedin.com/in/",30 tiktok: "https://www.tiktok.com/@",31 youtube: "https://www.youtube.com/@",32};3334/** "@handle" or "handle" -> full platform URL; a URL is left as is */35function socialUrl(key: string, value: string): string {36 const v = value.trim();37 if (/^https?:\/\//i.test(v)) return v;38 if (key === "website") return `https://${v}`;39 return (SOCIAL_BASE[key] ?? "https://") + v.replace(/^@/, "");40}4142const fmtEpoch = (ts: number | null | undefined): string => {43 if (!ts) return "—";44 return new Date(ts * 1000).toLocaleDateString("en-CA", {45 day: "numeric", month: "long", year: "numeric",46 });47};4849export default function PublicProfilePage() {50 const { kaId = "" } = useParams();51 const [profile, setProfile] = useState<PublicProfile | null | undefined>(undefined);5253 useEffect(() => {54 fetchPublicProfile(kaId).then(setProfile).catch(() => setProfile(null));55 }, [kaId]);5657 if (profile === undefined) {58 return <div className="container profil"><div className="notice">Loading…</div></div>;59 }6061 if (profile === null) {62 return (63 <div className="container notice">64 <div className="big"><IcoCompass size={40} /></div>65 <h2>Profile not found</h2>66 <p>This member does not exist, or their profile is not public.</p>67 <Link className="btn btn-primary" to="/">Explore rentals</Link>68 </div>69 );70 }7172 return (73 <div className="container profil">74 <span className="kicker">Member profile</span>75 <h1>76 <span className="hl">{profile.name || "Rent-Ka member"}</span>77 </h1>7879 <div className="pc" role="img" aria-label={`Member card ${profile.ka_id}`}>80 <div className="pc-watermark" aria-hidden="true">KA</div>81 <div className="pc-head">82 <span className="pc-brand">Groupe <span className="pc-ka">KA</span></span>83 <span className="pc-label">Member card · Groupe KA</span>84 </div>85 <div className="pc-id-block">86 <span className="pc-id-label">KA-ID</span>87 <span className="pc-id">{profile.ka_id}</span>88 </div>89 <div className="pc-foot">90 <div className="pc-holder">91 <span className="pc-holder-name">{profile.name || "Rent-Ka member"}</span>92 {profile.role_label && (93 <span className="pc-role-badge">{profile.role_label}</span>94 )}95 <span className="pc-holder-since">Member since {fmtEpoch(profile.created_at)}</span>96 </div>97 {profile.picture && (98 <img className="pc-avatar" src={profile.picture} alt="" referrerPolicy="no-referrer" />99 )}100 </div>101 <div className="pc-strip" aria-hidden="true">102 {Array.from({ length: 28 }).map((_, i) => <i key={i} />)}103 </div>104 </div>105106 {(profile.bio || profile.city || profile.role || profile.job_title107 || profile.company || profile.role_label) && (108 <section className="pub-about">109 {profile.bio && <p className="pub-bio">{profile.bio}</p>}110 <div className="pub-meta">111 {(profile.job_title || profile.company) && (112 <span className="stat-chip">113 {[profile.job_title, profile.company].filter(Boolean).join(" · ")}114 </span>115 )}116 {profile.role_label && (117 <span className="stat-chip">{profile.role_label}</span>118 )}119 {profile.city && <span className="stat-chip">{profile.city}</span>}120 {profile.age != null && (121 <span className="stat-chip">{profile.age} years old</span>122 )}123 {profile.role === "gestionnaire" && (124 <span className="stat-chip">Rental manager</span>125 )}126 {profile.role === "locataire" && (127 <span className="stat-chip">Looking for a rental</span>128 )}129 {profile.website && (130 <a className="stat-chip" href={socialUrl("website", profile.website)}131 target="_blank" rel="noopener noreferrer">Website ↗</a>132 )}133 </div>134 </section>135 )}136137 {Object.keys(profile.socials || {}).length > 0 && (138 <div className="pub-socials">139 {SOCIAL_META.filter((s) => profile.socials[s.key]).map((s) => (140 <a141 key={s.key}142 className="pub-social"143 href={socialUrl(s.key, profile.socials[s.key]!)}144 target="_blank" rel="noopener noreferrer"145 aria-label={s.label} title={s.label}146 >147 {s.icon}148 </a>149 ))}150 </div>151 )}152153 <p className="profil-note">154 Member of <b>Rent-Ka</b>, the rental listings aggregator of{" "}155 <a href="https://www.groupe-ka.com" target="_blank" rel="noopener noreferrer">156 Groupe KA157 </a>.158 </p>159160 <div className="profil-actions">161 <Link className="btn btn-primary" to="/">Explore rentals</Link>162 <a className="btn btn-ghost" href="https://www.groupe-ka.com/connexion" target="_blank" rel="noopener noreferrer">Create my account</a>163 </div>164 </div>165 );166}167