SPB Git forge

spb/rent-ka

Public
8commits 1branches 0releases
7.4 MBsize
maindefault branch
19 days agolast push
Python 68.8% TypeScript 18.6% CSS 8.7% JavaScript 3.3% HTML 0.6%
6.5 KB · 167 lines tsx
Raw Blame History
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