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// components/GestionnaireBloc.tsx: "Who manages this rental?" block (detail)5// Manager profile (direct source) + Google reputation: real rating6// distribution, recent average, trend and recurring themes computed over7// the reviews synced in OUR database (rentka/managers.py) — not just the8// average Google shows, and zero API calls on load.9// The Google profile is only shown when the match is confident.10// -----------------------------------------------------------------------------11import { useEffect, useState } from "react";12import { Gestionnaire, fetchGestionnaire } from "../api";1314const NBSP = " ";1516const SENT_CLS: Record<string, string> = {17 "négatif": "zi-eleve", "neutre": "zi-nc", "positif": "zi-ok",18 "negative": "zi-eleve", "neutral": "zi-nc", "positive": "zi-ok",19};2021export default function GestionnaireBloc({ source }: { source: string }) {22 const [d, setD] = useState<Gestionnaire | null>(null);23 useEffect(() => {24 setD(null);25 fetchGestionnaire(source).then(setD).catch(() => setD(null));26 }, [source]);27 // without an associated Google profile, the "Practical details" block is enough28 if (!d || !d.google_maps || d.google_maps.statut === "non_associe") return null;2930 const g = d.google_maps;31 const avis = d.avis;32 const dist = avis?.distribution;33 const total = dist ? Object.values(dist).reduce((a, b) => a + b, 0) : 0;34 const recents = (d.avis_recents ?? []).filter((a) => a.texte).slice(0, 3);3536 return (37 <section className="f-bloc f-gest" id="gestionnaire">38 <h2>Who manages this rental?</h2>39 <div className="gest-head">40 <div className="gest-nom">{d.nom}</div>41 <div className="gest-meta">42 {d.annonces_actives}{NBSP}active listing{d.annonces_actives > 1 ? "s" : ""} on Rent-Ka43 {d.site_web && (44 <>45 {" · "}46 <a href={d.site_web} target="_blank" rel="noopener noreferrer">website ↗</a>47 </>48 )}49 </div>50 </div>5152 {g.note != null && (53 <div className="gest-google">54 <span className="gest-note">★ {g.note.toFixed(1)}</span>55 <span className="gest-navis">56 {g.nombre_avis}{NBSP}Google reviews — profile “{g.nom}”57 </span>58 </div>59 )}6061 {avis && avis.n > 0 && (62 <>63 {dist && total > 0 && (64 <div className="gest-bars" aria-label="Rating distribution (analyzed reviews)">65 {[5, 4, 3, 2, 1].map((n) => {66 const c = dist[String(n)] ?? 0;67 return (68 <div className="gest-bar" key={n}>69 <span className="gb-n">{n}★</span>70 <span className="gb-track">71 <span className={`gb-fill ${n <= 2 ? "neg" : n >= 4 ? "pos" : ""}`}72 style={{ width: `${Math.round((100 * c) / total)}%` }} />73 </span>74 <span className="gb-c">{c}</span>75 </div>76 );77 })}78 </div>79 )}80 <div className="gest-stats">81 {avis.moyenne_12m != null && (82 <span>Last 12 months average: <b>{avis.moyenne_12m.toFixed(1)}</b> ({avis.n_12m} reviews)</span>83 )}84 {avis.tendance && (85 <span className={`zi-badge ${avis.tendance === "en amélioration" || avis.tendance === "improving" ? "zi-ok" : avis.tendance === "en dégradation" || avis.tendance === "declining" ? "zi-modere" : "zi-nc"}`}>86 {avis.tendance}87 </span>88 )}89 </div>90 {(avis.plaintes_frequentes?.length ?? 0) > 0 && (91 <div className="gest-themes">92 <span className="k">Recurring complaints in reviews:</span>{" "}93 {avis.plaintes_frequentes!.map((t) => (94 <span className="zi-badge zi-modere" key={t}>{t}</span>95 ))}96 </div>97 )}98 {recents.length > 0 && (99 <details className="gest-avis">100 <summary>Recent review excerpts ({recents.length})</summary>101 {recents.map((a, i) => (102 <blockquote className="gest-citation" key={i}>103 <span className={`zi-badge ${SENT_CLS[a.analyse.sentiment ?? ""] ?? "zi-nc"}`}>104 {a.note != null ? `${a.note}★` : "—"}105 </span>{" "}106 {a.texte}107 <footer>108 {a.date ? new Date(a.date).toLocaleDateString("en-CA", { month: "long", year: "numeric" }) : ""}109 {a.reponse_proprietaire && " · the manager replied"}110 </footer>111 </blockquote>112 ))}113 </details>114 )}115 </>116 )}117118 <p className="fine">119 Google Maps profile matched automatically ({Math.round((g.confiance_association ?? 0) * 100)}{NBSP}% confidence —{" "}120 {g.methode}). Statistics computed over the {avis?.n ?? 0} reviews121 synced in the Rent-Ka database122 {g.nombre_avis && avis && avis.n < g.nombre_avis123 ? ` (sample of the most recent; Google reports ${g.nombre_avis})`124 : ""}. Themes detected by lexicon — indicative inference, not a125 human reading.126 </p>127 </section>128 );129}130