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%
4.6 KB · 126 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// Rent-Ka — Rental listings aggregator (Canada, outside Québec)3// Author: Simon-Pierre Boucher — contact@spboucher.ai4// components/HistoriqueLouka.tsx: "Listing history" block (detail page)5//   Timeline of REAL sync observations: price, description, area,6//   availability, inclusions, photo changes, removals/returns. + "Past7//   lives": likely recycled listings of the same unit (multi-signal score,8//   explainable inference).9// -----------------------------------------------------------------------------10import { useEffect, useState } from "react";11import {12  HistoriqueLouka as HistoT, Recyclees,13  fetchHistorique, fetchRecyclees, fmtPrice,14} from "../api";1516const NBSP = " ";1718const fmtTs = (ts: number | null | undefined): string =>19  ts20    ? new Date(ts * 1000).toLocaleDateString("en-CA",21        { day: "numeric", month: "short", year: "numeric" })22    : "—";2324const EVENT_LABEL: Record<string, string> = {25  prix: "Price",26  description: "Description changed",27  superficie: "Area changed",28  dispo: "Availability changed",29  inclusions: "Inclusions changed",30  photos: "Photos changed",31  disparition: "Listing removed",32  reapparition: "Listing republished",33};3435export default function HistoriqueLouka({ uid }: { uid: string }) {36  const [d, setD] = useState<HistoT | null>(null);37  const [rec, setRec] = useState<Recyclees | null>(null);38  useEffect(() => {39    setD(null); setRec(null);40    fetchHistorique(uid).then(setD).catch(() => setD(null));41    fetchRecyclees(uid).then(setRec).catch(() => setRec(null));42  }, [uid]);43  if (!d) return null;4445  const variationPct = d.variation != null46    ? `${d.variation > 0 ? "+" : "−"}${Math.abs(Math.round(d.variation * 100))}${NBSP}%`47    : null;48  const items = d.timeline.slice(0, 12);49  const matches = rec?.matches ?? [];5051  return (52    <section className="f-bloc f-histolk" id="historique-louka">53      <h2>Listing history</h2>54      <div className="kv">55        <div className="cell">56          <div className="k">Tracked since</div>57          <div className="v">{fmtTs(d.premiere_observation)}</div>58        </div>59        <div className="cell">60          <div className="k">Online</div>61          <div className="v">{d.jours_en_ligne}{NBSP}day{d.jours_en_ligne > 1 ? "s" : ""}</div>62        </div>63        {d.prix_initial != null && d.prix_actuel != null && d.prix_initial !== d.prix_actuel && (64          <div className="cell">65            <div className="k">Initial → current price</div>66            <div className="v">67              {fmtPrice(d.prix_initial)} → {fmtPrice(d.prix_actuel)}68              {variationPct && ` (${variationPct})`}69            </div>70          </div>71        )}72        <div className="cell">73          <div className="k">Changes observed</div>74          <div className="v">{d.modifications}</div>75        </div>76      </div>7778      {items.length > 0 && (79        <ul className="hl-timeline">80          {items.map((it, i) => (81            <li key={`${it.ts}-${it.type}-${i}`}82                className={`hl-item hl-${it.type}`}>83              <span className="hl-date">{fmtTs(it.ts)}</span>84              <span className="hl-texte">85                {it.type === "prix" ? (86                  it.prix_avant != null87                    ? <>Price {it.prix_avant! > (it.prix ?? 0) ? "lowered" : "raised"} from {fmtPrice(it.prix_avant ?? null)} to <b>{fmtPrice(it.prix ?? null)}</b></>88                    : <>First observed price: <b>{fmtPrice(it.prix ?? null)}</b></>89                ) : (90                  EVENT_LABEL[it.type] ?? it.type91                )}92              </span>93            </li>94          ))}95        </ul>96      )}97      {items.length === 0 && (98        <p className="fine">99          No change observed since this listing's first sync.100        </p>101      )}102103      {matches.length > 0 && (104        <div className="hl-recyclees">105          <h3>Likely past lives of this unit</h3>106          {matches.slice(0, 3).map((m) => (107            <div className="hl-match" key={m.uid}>108              <div>109                <span className="zi-badge zi-modere">110                  likely repost ({m.confiance}{NBSP}% confidence)111                </span>{" "}112                {m.prix != null && <>advertised at {fmtPrice(m.prix)}</>}113                {m.derniere_observation != null && <> until {fmtTs(m.derniere_observation)}</>}114              </div>115              <div className="hl-signaux">{m.signaux.join(" · ")}</div>116            </div>117          ))}118          <p className="fine">{rec?.methode} — inference, no automatic merging.</p>119        </div>120      )}121122      <p className="fine">{d.methode}</p>123    </section>124  );125}126