SPB Git

spb/qwhpi Public

QHPI — Quebec Housing Price Index: quality-adjusted, hierarchically pooled housing price indexes.

Python 63.9% TypeScript 25.4% CSS 5.5% TeX 3.5% SQL 0.8% Makefile 0.5% Dockerfile 0.5%
6.6 KB · 179 lines tsx
Raw Blame History
1/**2 * =============================================================================3 * QWHPI — Quebec Weekly Housing Price Index4 * Author  : Simon-Pierre Boucher5 * Contact : contact@spboucher.ai6 * File    : web/app/api-docs/page.tsx7 * Purpose : API docs — copyable examples per endpoint + live playground.8 * =============================================================================9 */10"use client";1112import { useState } from "react";13import { API_BASE } from "../../lib/api";1415interface Endpoint {16  path: string;17  desc: string;18  example: string;19}2021const ENDPOINTS: Endpoint[] = [22  { path: "/v1/index", desc: "Full monthly history for one series (CSV via &format=csv)",23    example: "/v1/index?geography=quebec-city&type=condo&from=2023-01&to=latest" },24  { path: "/v1/index/latest", desc: "Latest complete month for one series",25    example: "/v1/index/latest?geography=montreal&type=condo" },26  { path: "/v1/stats", desc: "Derived metrics: peak, drawdown, CAGR, volatility, momentum, ranks, volumes",27    example: "/v1/stats?geography=quebec&type=all" },28  { path: "/v1/stats/overview", desc: "One rich row per geography (heatmap data)",29    example: "/v1/stats/overview?level=region&type=all" },30  { path: "/v1/compare", desc: "Aligned multi-series comparison, optional rebasing",31    example: "/v1/compare?series=montreal:condo,quebec-city:condo&rebase_period=2022-01" },32  { path: "/v1/map", desc: "One metric per geography for a chosen month",33    example: "/v1/map?metric=yoy&level=region&period=2025-06" },34  { path: "/v1/report", desc: "Automatic PDF report (1-5 series, &format=json for data)",35    example: "/v1/report?series=montreal:condo&format=json" },36  { path: "/v1/vintages", desc: "First release vs current vintage (revision tracking)",37    example: "/v1/vintages?geography=quebec&type=all" },38  { path: "/v1/geographies", desc: "Published hierarchy + coverage matrix",39    example: "/v1/geographies" },40  { path: "/v1/meta", desc: "Model version, vintage, methodology summary",41    example: "/v1/meta" },42];4344function CopyButton({ text }: { text: string }) {45  const [copied, setCopied] = useState(false);46  return (47    <button48      className="ctrl"49      style={{ padding: "4px 10px", fontSize: 12.5 }}50      aria-label="Copy request"51      onClick={() => {52        navigator.clipboard.writeText(text);53        setCopied(true);54        setTimeout(() => setCopied(false), 1500);55      }}56    >57      {copied ? "✓ Copied" : "Copy"}58    </button>59  );60}6162export default function ApiDocs() {63  const [query, setQuery] = useState(ENDPOINTS[0].example);64  const [result, setResult] = useState<string>("");65  const [status, setStatus] = useState<string>("");66  const [running, setRunning] = useState(false);6768  const run = async () => {69    setRunning(true);70    setStatus("");71    const t0 = performance.now();72    try {73      const res = await fetch(`${API_BASE}${query}`, {74        headers: { Accept: "application/json" },75      });76      const ms = Math.round(performance.now() - t0);77      const type = res.headers.get("content-type") ?? "";78      setStatus(`${res.status} ${res.statusText} · ${ms} ms · ${type.split(";")[0]}`);79      if (type.includes("json")) {80        const body = await res.json();81        setResult(JSON.stringify(body, null, 2).slice(0, 20000));82      } else {83        const body = await res.text();84        setResult(body.slice(0, 20000));85      }86    } catch (e) {87      setStatus("network error");88      setResult(String(e));89    }90    setRunning(false);91  };9293  return (94    <>95      <h1>API</h1>96      <p className="lede">97        Every published series, statistic and report is available98        programmatically — confidence intervals and reliability grades on every99        observation, never hidden.{" "}100        <a href={`${API_BASE}/docs`} target="_blank" rel="noreferrer">101          Full OpenAPI reference ↗102        </a>103      </p>104105      <h2>Playground</h2>106      <div className="card fade-up">107        <div className="controls" style={{ margin: 0 }}>108          <select109            aria-label="Endpoint template"110            value=""111            onChange={(e) => {112              if (e.target.value) setQuery(e.target.value);113            }}114          >115            <option value="">Insert an example…</option>116            {ENDPOINTS.map((ep) => (117              <option key={ep.path} value={ep.example}>{ep.path}</option>118            ))}119          </select>120          <input121            type="text"122            value={query}123            aria-label="Request path"124            onChange={(e) => setQuery(e.target.value)}125            onKeyDown={(e) => e.key === "Enter" && run()}126            style={{127              flex: "1 1 340px", background: "var(--surface-1)",128              color: "var(--text-primary)",129              border: "1px solid var(--border-strong)", borderRadius: 10,130              padding: "8px 12px", fontSize: 13.5,131              fontFamily: "ui-monospace, monospace",132            }}133          />134          <button className="primary" onClick={run} disabled={running}>135            {running ? "Running…" : "Run ▸"}136          </button>137        </div>138        {status && (139          <p className="note" style={{ margin: "10px 0 6px" }}>{status}</p>140        )}141        {result && (142          <pre style={{143            background: "var(--surface-2)", borderRadius: 10, padding: 14,144            fontSize: 12, maxHeight: 380, overflow: "auto", margin: "8px 0 0",145          }}>{result}</pre>146        )}147      </div>148149      <h2>Endpoints</h2>150      <div className="grid" style={{ gap: 12 }}>151        {ENDPOINTS.map((ep) => {152          const curl = `curl "${API_BASE}${ep.example}"`;153          return (154            <div className="card hoverable" key={ep.path}155                 style={{ padding: "14px 18px" }}>156              <div style={{ display: "flex", justifyContent: "space-between",157                            alignItems: "baseline", gap: 10, flexWrap: "wrap" }}>158                <code style={{ fontSize: 14, fontWeight: 700 }}>{ep.path}</code>159                <span style={{ display: "flex", gap: 8 }}>160                  <button className="ctrl" style={{ padding: "4px 10px", fontSize: 12.5 }}161                          onClick={() => setQuery(ep.example)}>162                    Try ↑163                  </button>164                  <CopyButton text={curl} />165                </span>166              </div>167              <p className="note" style={{ margin: "6px 0" }}>{ep.desc}</p>168              <code style={{ fontSize: 12, color: "var(--text-secondary)",169                             display: "block", overflowX: "auto" }}>170                {curl}171              </code>172            </div>173          );174        })}175      </div>176    </>177  );178}179