SPB Git forge

spb/home-ka

Public
10commits 1branches 0releases
793.0 KBsize
maindefault branch
20 days agolast push
Python 49.6% TypeScript 25.5% CSS 24.1%
3.6 KB · 93 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// Home-Ka — US real-estate aggregator (Groupe KA)3// Author: Simon-Pierre Boucher — contact@spboucher.ai4// pages/Sources.tsx : public registry of data sources (/api/sources)5// -----------------------------------------------------------------------------6import { useEffect, useState } from "react";7import { Link } from "react-router-dom";8import { Source, fetchSources, registerSourceNames } from "../api";910function ago(ts: number | null): string {11  if (ts == null) return "never";12  const s = Math.max(0, Math.floor(Date.now() / 1000 - (ts > 1e12 ? ts / 1000 : ts)));13  if (s < 3600) return `${Math.round(s / 60)} min ago`;14  if (s < 172800) return `${Math.round(s / 3600)} h ago`;15  return `${Math.round(s / 86400)} d ago`;16}1718export default function SourcesPage() {19  const [sources, setSources] = useState<Source[] | null>(null);20  const [error, setError] = useState<string | null>(null);2122  useEffect(() => {23    fetchSources()24      .then((r) => { registerSourceNames(r.sources); setSources(r.sources); })25      .catch((e) => setError(String(e)));26  }, []);2728  const totalListings = (sources ?? []).reduce((s, x) => s + x.active_listings, 0);29  const enabled = (sources ?? []).filter((s) => s.enabled).length;3031  return (32    <div className="container sources">33      <span className="kicker">Registry — data sources</span>34      <h1>Data sources</h1>35      <p className="sub">36        Every listing on Home-Ka comes from a registered source — brokerage37        feeds, MLS/RESO APIs and listing platforms. Each source is synced by a38        dedicated connector and de-duplicated by MLS number and property match.39        Click a count to browse its homes.40      </p>4142      {error && <div className="notice">{error}</div>}43      {!sources && !error && <div className="notice">Loading…</div>}4445      {sources && (46        <>47          <p className="stats-foot" style={{ marginTop: 0 }}>48            <b>{totalListings.toLocaleString("en-US")}</b> active listings ·{" "}49            <b>{sources.length}</b> sources ·{" "}50            <b>{enabled}</b> enabled51          </p>5253          <div className="q-table-wrap">54            <table className="q-table">55              <thead>56                <tr>57                  <th>Source</th><th>Connector</th><th>States</th>58                  <th>Active listings</th><th>Last sync</th>59                </tr>60              </thead>61              <tbody>62                {sources.map((s) => (63                  <tr key={s.id}>64                    <td>65                      {s.name || s.id}66                      {!s.enabled && <span className="adm-badge adm-off"> disabled</span>}67                    </td>68                    <td>{s.connector_type ?? "—"}</td>69                    <td>{s.states?.length ? s.states.join(", ") : "—"}</td>70                    <td className="num">71                      {s.active_listings > 072                        ? <Link className="count-pill" to={`/?source=${encodeURIComponent(s.id)}`}>73                            {s.active_listings.toLocaleString("en-US")}74                          </Link>75                        : "0"}76                    </td>77                    <td>{ago(s.last_sync)}</td>78                  </tr>79                ))}80              </tbody>81            </table>82          </div>8384          <p className="stats-foot">85            Listings published by more than one source are de-duplicated by MLS86            number and by matching the underlying property — no double counting.87          </p>88        </>89      )}90    </div>91  );92}93