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/Gestion.tsx: manager dashboard — claim YOUR page (source) then5// customize it (tagline, description, contact info, logo). The matching6// public page lives at /g/{source}.7// -----------------------------------------------------------------------------8import { useEffect, useMemo, useState } from "react";9import { Link } from "react-router-dom";10import {11 Source, claimOrg, fetchMyOrg, fetchSources, updateOrg,12} from "../api";13import { useAccount } from "../account";14import { IcoBuilding } from "../components/Icons";1516interface OrgMine {17 source: string | null;18 name?: string;19 active_listings?: number;20 profile?: Record<string, string>;21}2223export default function GestionPage() {24 const { me, loaded, refresh } = useAccount();25 const [org, setOrg] = useState<OrgMine | null>(null);26 const [sources, setSources] = useState<Source[]>([]);27 const [pick, setPick] = useState("");28 const [saving, setSaving] = useState(false);29 const [saved, setSaved] = useState(false);30 const [error, setError] = useState("");31 const [form, setForm] = useState({32 tagline: "", description: "", website: "", phone: "", email: "", logo_url: "",33 });3435 useEffect(() => {36 if (me) fetchMyOrg().then((o) => {37 setOrg(o);38 const p = o.profile ?? {};39 setForm({40 tagline: p.tagline ?? "", description: p.description ?? "",41 website: p.website ?? "", phone: p.phone ?? "",42 email: p.email ?? "", logo_url: p.logo_url ?? "",43 });44 }).catch(() => setOrg({ source: null }));45 }, [me]);4647 useEffect(() => {48 if (me && org && !org.source) {49 fetchSources().then((r) => setSources(r.sources)).catch(() => {});50 }51 }, [me, org]);5253 const claimable = useMemo(54 () => sources55 .filter((s) => s.connector && s.active_listings > 0)56 .sort((a, b) => a.name.localeCompare(b.name, "en")),57 [sources]);5859 if (loaded && !me) {60 return (61 <div className="container profil">62 <span className="kicker">Manager space</span>63 <h1>Sign in to manage <span className="hl">your page</span>.</h1>64 <a className="btn btn-primary" href="/api/auth/ka/login">65 Sign in with KA ID66 </a>67 </div>68 );69 }7071 if (me && me.role !== "gestionnaire") {72 return (73 <div className="container profil">74 <span className="kicker">Manager space</span>75 <h1>Reserved for <span className="hl">managers</span>.</h1>76 <p className="lede">77 Your account is in tenant mode. If you manage rentals displayed on78 Rent-Ka, switch profiles.79 </p>80 <Link className="btn btn-primary" to="/welcome">Change profile</Link>81 </div>82 );83 }8485 const doClaim = async () => {86 if (!pick) return;87 setError("");88 const res = await claimOrg(pick);89 if (!res.ok) {90 setError(res.status === 40991 ? "This page is already claimed by another account."92 : "Could not claim this page.");93 return;94 }95 refresh();96 const o = await fetchMyOrg();97 setOrg(o);98 };99100 const doSave = async () => {101 setSaving(true);102 setError("");103 const res = await updateOrg(form);104 setSaving(false);105 if (!res.ok) { setError("Could not save — try again."); return; }106 setSaved(true);107 setTimeout(() => setSaved(false), 2000);108 };109110 return (111 <div className="container profil">112 <span className="kicker">Manager space</span>113114 {org && !org.source && (115 <>116 <h1>Claim the page of <span className="hl">your management</span>.</h1>117 <p className="lede">118 Pick your organization among the sources displayed on Rent-Ka.119 You'll then be able to customize its public page — your inventory120 is already linked automatically.121 </p>122 <div className="claim-row">123 <select value={pick} onChange={(e) => setPick(e.target.value)}>124 <option value="">— Your property management —</option>125 {claimable.map((s) => (126 <option key={s.id} value={s.id}>127 {s.name} ({s.active_listings} rentals)128 </option>129 ))}130 </select>131 <button className="btn btn-primary" disabled={!pick} onClick={doClaim}>132 Claim this page133 </button>134 </div>135 {error && <p className="claim-error">{error}</p>}136 <p className="profil-note">137 Can't find your organization? Write to us at{" "}138 <a href="mailto:contact@groupe-ka.com">contact@groupe-ka.com</a> and139 we'll add your site as a source.140 </p>141 </>142 )}143144 {org && org.source && (145 <>146 <h1><span className="hl">{org.name}</span></h1>147 <div className="org-head">148 <span className="org-stat">149 <IcoBuilding size={15} /> <b>{org.active_listings ?? 0}</b> active linked rentals150 </span>151 <Link className="btn btn-ghost" to={`/g/${org.source}`}>152 See my public page ↗153 </Link>154 </div>155156 <section className="org-form">157 <h3>Customize my page</h3>158 <label>159 <span>Tagline (140 characters)</span>160 <input161 value={form.tagline} maxLength={140}162 placeholder="E.g. Well-maintained rentals in Ottawa since 1998"163 onChange={(e) => setForm({ ...form, tagline: e.target.value })}164 />165 </label>166 <label>167 <span>Description</span>168 <textarea169 value={form.description} rows={5} maxLength={2000}170 placeholder="Introduce your management: areas, services, values…"171 onChange={(e) => setForm({ ...form, description: e.target.value })}172 />173 </label>174 <div className="org-form-row">175 <label>176 <span>Website</span>177 <input178 value={form.website} placeholder="https://…"179 onChange={(e) => setForm({ ...form, website: e.target.value })}180 />181 </label>182 <label>183 <span>Phone</span>184 <input185 value={form.phone} placeholder="416 555-0123"186 onChange={(e) => setForm({ ...form, phone: e.target.value })}187 />188 </label>189 </div>190 <div className="org-form-row">191 <label>192 <span>Public email</span>193 <input194 value={form.email} placeholder="location@…"195 onChange={(e) => setForm({ ...form, email: e.target.value })}196 />197 </label>198 <label>199 <span>Logo (image URL)</span>200 <input201 value={form.logo_url} placeholder="https://…/logo.png"202 onChange={(e) => setForm({ ...form, logo_url: e.target.value })}203 />204 </label>205 </div>206 {error && <p className="claim-error">{error}</p>}207 <button208 className={`btn btn-primary ${saved ? "pc-copy ok" : ""}`}209 disabled={saving} onClick={doSave}210 >211 {saved ? "✓ Saved" : saving ? "Saving…" : "Save"}212 </button>213 </section>214 </>215 )}216 </div>217 );218}219