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%
3.7 KB · 102 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// Rent-Ka — Rental listings aggregator (Canada, outside Québec)3// Author: Simon-Pierre Boucher — contact@spboucher.ai4// pages/Passerelle.tsx: gateway page to the original listing.5//   Opened in a new tab by the listing CTAs: shows Rent-Ka → manager with a6//   progress bar, then redirects to the manager's ad after ~2.2 s.7//   Security: we receive a uid (never a raw URL) — the destination URL comes8//   from our API, not from the parameter. No open redirect possible.9// -----------------------------------------------------------------------------10import { useEffect, useRef, useState } from "react";11import { Link, useParams } from "react-router-dom";12import { fetchListing, fetchSources, registerSourceNames, sourceName } from "../api";1314const DELAI_MS = 2200;1516export default function PasserellePage() {17  const { uid } = useParams<{ uid: string }>();18  const [dest, setDest] = useState<{ url: string; source: string } | null>(null);19  const [error, setError] = useState(false);20  const timer = useRef<number | null>(null);2122  useEffect(() => {23    if (!uid) return;24    let cancelled = false;25    Promise.all([fetchSources(), fetchListing(uid)])26      .then(([src, l]) => {27        if (cancelled) return;28        registerSourceNames(src.sources);29        if (!l.url || !/^https?:\/\//.test(l.url)) { setError(true); return; }30        setDest({ url: l.url, source: l.source });31        timer.current = window.setTimeout(() => {32          window.location.replace(l.url);33        }, DELAI_MS);34      })35      .catch(() => !cancelled && setError(true));36    return () => {37      cancelled = true;38      if (timer.current) window.clearTimeout(timer.current);39    };40  }, [uid]);4142  if (error)43    return (44      <div className="notice container">45        <div className="big">⚠️</div>46        <h2>Listing not found</h2>47        <p>Could not find the original listing.</p>48        <Link className="btn btn-primary" to="/">Back to rentals</Link>49      </div>50    );5152  const nom = dest ? sourceName(dest.source) : "…";5354  return (55    <div className="passerelle" role="status" aria-live="polite">56      <div className="pass-card">57        <div className="pass-brands">58          <span className="pass-brand">59            Rent<span className="ka">·Ka</span>60          </span>61          <span className="pass-trajet" aria-hidden="true">62            <span className="pass-dot" /><span className="pass-dot" /><span className="pass-dot" />63            <span className="pass-fleche">➔</span>64          </span>65          <span className="pass-gestionnaire">66            <span className="pass-gest-ico" aria-hidden="true">🏢</span>67            {nom}68          </span>69        </div>7071        <h1>Gateway to the original listing</h1>72        <p className="pass-texte">73          {dest74            ? <>We are taking you to <b>{nom}</b> — prices, visits and75                signatures happen directly with the manager.</>76            : "Preparing the redirect…"}77        </p>7879        <div className="pass-progress" aria-hidden="true">80          <span className={`pass-progress-fill ${dest ? "go" : ""}`} />81        </div>8283        <div className="pass-actions">84          {dest && (85            <a className="btn btn-primary" href={dest.url} rel="noopener noreferrer">86              Go now ↗87            </a>88          )}89          <button className="btn btn-ghost" onClick={() => window.close()}>90            Stay on Rent-Ka91          </button>92        </div>9394        <div className="fine">95          Rent-Ka is an independent aggregator: every listing links back to96          the ad published by the property manager.97        </div>98      </div>99    </div>100  );101}102