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// 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