SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
4.5 KB · 97 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// Lou-Ka — Location court terme3// components/CtPriceAnalysis.tsx : bloc « Analyse du prix / nuit » (fiche CT)4//   Position du tarif dans la distribution des hébergements comparables5//   (segment : type + capacité + région touristique, calculé par l'API).6// -----------------------------------------------------------------------------7import { CtPriceContext, fmtNight } from "../ctapi";89export function ctDealBadge(p: CtPriceContext | null) {10  if (!p || p.deviation == null || p.verdict == null) return null;11  const pct = Math.round(Math.abs(p.deviation) * 100);12  if (p.verdict === "sous")13    return { cls: "deal-good", txt: `≈ ${pct} % sous le prix médian du segment` };14  if (p.verdict === "dessus")15    return { cls: "deal-high", txt: `≈ ${pct} % au-dessus du prix médian` };16  return { cls: "deal-ok", txt: "Dans les prix du segment" };17}1819function Histo({ p, price }: { p: CtPriceContext; price: number }) {20  const bins = p.histogram;21  if (!bins || bins.length < 4) return null;22  const W = 320, H = 96, top = 18, bottom = 16;23  const lo = bins[0].x0, hi = bins[bins.length - 1].x1;24  if (hi <= lo) return null;25  const max = Math.max(...bins.map((b) => b.n), 1);26  const x = (v: number) => ((Math.min(Math.max(v, lo), hi) - lo) / (hi - lo)) * W;27  const bw = W / bins.length;28  const plotH = H - top - bottom;29  const priceX = x(price), medX = x(p.median);30  const close = Math.abs(priceX - medX) < 64;31  const lblAnchor = (px: number) => (px < 56 ? "start" : px > W - 56 ? "end" : "middle");32  return (33    <svg className="fv-histo" viewBox={`0 0 ${W} ${H}`} role="img"34         aria-label={`Position du prix (${price} $/nuit) parmi ${p.n} hébergements comparables`}>35      {bins.map((b, i) => {36        const h = Math.max(1.5, (b.n / max) * plotH);37        const inBin = price >= b.x0 && price < b.x1;38        return (39          <rect key={i} x={i * bw + 1} y={H - bottom - h} rx="2"40                width={Math.max(1, bw - 2)} height={h}41                fill={inBin ? "var(--accent, #ff6a00)" : "rgba(204, 85, 0, 0.28)"}>42            <title>{`${b.x0} $ – ${b.x1} $ : ${b.n} hébergement${b.n > 1 ? "s" : ""}`}</title>43          </rect>44        );45      })}46      {/* fourchette interquartile (bande) */}47      <rect x={x(p.p25)} y={H - bottom} width={Math.max(2, x(p.p75) - x(p.p25))}48            height="3.5" rx="1.5" fill="rgba(204, 85, 0, 0.45)" />49      {/* marqueur médiane */}50      <line x1={medX} x2={medX} y1={top - 2} y2={H - bottom}51            stroke="var(--accent-deep, #cc5500)" strokeWidth="1.6" strokeDasharray="3 3" />52      {!close && (53        <text x={medX} y={top - 7} textAnchor={lblAnchor(medX)}54              className="fv-histo-lbl fv-histo-lbl-fv">Médiane</text>55      )}56      {/* marqueur du prix demandé */}57      <line x1={priceX} x2={priceX} y1={top - 2} y2={H - bottom}58            stroke="var(--ink, #141814)" strokeWidth="2" />59      <text x={priceX} y={close ? top - 7 : H - 4} textAnchor={lblAnchor(priceX)}60            className="fv-histo-lbl">Ce prix{close ? " / médiane" : ""}</text>61      <text x="1" y={H - 4} className="fv-histo-axis" textAnchor="start">{lo} $</text>62      <text x={W - 1} y={H - 4} className="fv-histo-axis" textAnchor="end">{hi} $</text>63    </svg>64  );65}6667export default function CtPriceAnalysis({ p, price }:68    { p: CtPriceContext | null; price: number | null }) {69  if (!p || price == null) return null;70  const badge = ctDealBadge(p);71  return (72    <section className="f-bloc f-fairvalue" id="analyse-prix">73      <h2>Analyse du prix Lou-Ka</h2>74      <div className="fv-head">75        <div>76          <div className="fv-value">{fmtNight(p.median)} <small>/ nuit</small></div>77          <div className="fv-range">78            Prix médian du segment · 50 % des prix entre {fmtNight(p.p25)} et {fmtNight(p.p75)}79          </div>80        </div>81        {badge && <div className={`fv-badge ${badge.cls}`}>{badge.txt}</div>}82      </div>83      <Histo p={p} price={price} />84      <div className="fv-meta">85        <span>Segment : <b>{p.segment}</b></span>86        <span>{p.n.toLocaleString("fr-CA")} hébergements comparables</span>87        <span>Moins cher que {100 - p.percentile} % du segment</span>88      </div>89      <p className="fine">90        Comparaison indicative des tarifs affichés (« à partir de ») par les91        sources pour des hébergements du même type dans la même région — les92        prix réels varient selon les dates et la durée du séjour.93      </p>94    </section>95  );96}97