// ----------------------------------------------------------------------------- // Rent-Ka — Rental listings aggregator (Canada, outside Québec) // Author: Simon-Pierre Boucher — contact@spboucher.ai // pages/Profile.tsx: user profile — Groupe KA member card (KA-ID), // account information, actions (sign out, cookies, legal) // ----------------------------------------------------------------------------- import { useRef, useEffect, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { Me, Socials, deleteAvatar, fetchMe, logout, setPublicProfile, updateMyProfile, uploadAvatar, } from "../api"; import { IcoBuilding, IcoCamera, IcoDoc, IcoFacebook, IcoHeart, IcoInstagram, IcoLinkedIn, IcoLock, IcoTikTok, IcoX, IcoYouTube, } from "../components/Icons"; const fmtEpoch = (ts: number | null | undefined): string => { if (!ts) return "—"; return new Date(ts * 1000).toLocaleDateString("en-CA", { day: "numeric", month: "long", year: "numeric", }); }; const SOCIAL_BASE: Record = { instagram: "https://www.instagram.com/", facebook: "https://www.facebook.com/", x: "https://x.com/", linkedin: "https://www.linkedin.com/in/", tiktok: "https://www.tiktok.com/@", youtube: "https://www.youtube.com/@", }; /** "@handle" or "handle" -> full platform URL; a URL is left as is */ function socialUrl(key: string, value: string): string { const v = value.trim(); if (/^https?:\/\//i.test(v)) return v; if (key === "website") return `https://${v}`; return (SOCIAL_BASE[key] ?? "https://") + v.replace(/^@/, ""); } const SOCIAL_FIELDS: { key: keyof Socials; label: string; icon: JSX.Element; placeholder: string }[] = [ { key: "instagram", label: "Instagram", icon: , placeholder: "@handle or URL" }, { key: "facebook", label: "Facebook", icon: , placeholder: "profile or URL" }, { key: "x", label: "X (Twitter)", icon: , placeholder: "@handle or URL" }, { key: "linkedin", label: "LinkedIn", icon: , placeholder: "profile or URL" }, { key: "tiktok", label: "TikTok", icon: , placeholder: "@handle or URL" }, { key: "youtube", label: "YouTube", icon: , placeholder: "channel or URL" }, ]; /** Edit section: photo, display name, bio, contact info, social networks */ function ProfileEditor({ me, onSaved }: { me: Me; onSaved: () => void }) { const [form, setForm] = useState({ display_name: me.display_name || "", bio: me.bio || "", city: me.city || "", phone: me.phone || "", website: me.website || "", socials: { ...me.socials } as Socials, }); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [error, setError] = useState(""); const [avatarBusy, setAvatarBusy] = useState(false); const fileRef = useRef(null); const save = async () => { setSaving(true); setError(""); const res = await updateMyProfile(form); setSaving(false); if (!res.ok) { setError("Could not save — try again."); return; } setSaved(true); setTimeout(() => setSaved(false), 2000); onSaved(); }; const pickPhoto = async (f: File | undefined) => { if (!f) return; setAvatarBusy(true); setError(""); const url = await uploadAvatar(f); setAvatarBusy(false); if (!url) { setError("Photo rejected — JPG/PNG/WebP image, 8 MB max."); return; } onSaved(); }; return (

Customize my profile

{/* — photo — */}
{me.picture ? : {(me.name || me.email).charAt(0).toUpperCase()} }
{me.avatar_url && ( )} JPG, PNG or WebP — cropped to a 512 px square.
pickPhoto(e.target.files?.[0])} />