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%
7.5 KB · 219 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/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