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%
4.7 KB · 122 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// Rent-Ka — Rental listings aggregator (Canada, outside Québec)3// Author: Simon-Pierre Boucher — contact@spboucher.ai4// components/CookieConsent.tsx: consent banner (cookies/storage)5//   Honest by design: Rent-Ka uses NO third-party cookies or trackers.6//   The selector covers local storage (preferences) and possible future7//   anonymous audience measurement. Choice saved in localStorage,8//   re-openable via the "rentka:openConsent" event (footer link).9// -----------------------------------------------------------------------------10import { useEffect, useState } from "react";11import { Link } from "react-router-dom";1213const KEY = "rentka-consent-v1";1415export interface Consent {16  essential: true;          // always on (site operation)17  preferences: boolean;     // remember filters and list/map view18  statistics: boolean;      // anonymous audience measurement (if ever enabled)19  ts: number;20}2122export function getConsent(): Consent | null {23  try {24    const raw = localStorage.getItem(KEY);25    return raw ? (JSON.parse(raw) as Consent) : null;26  } catch {27    return null;28  }29}3031function save(preferences: boolean, statistics: boolean): Consent {32  const c: Consent = { essential: true, preferences, statistics, ts: Date.now() };33  try { localStorage.setItem(KEY, JSON.stringify(c)); } catch { /* storage blocked */ }34  return c;35}3637export default function CookieConsent() {38  const [visible, setVisible] = useState(false);39  const [custom, setCustom] = useState(false);40  const [prefs, setPrefs] = useState(true);41  const [stats, setStats] = useState(false);4243  useEffect(() => {44    if (getConsent() === null) setVisible(true);45    const open = () => { setVisible(true); setCustom(true); };46    window.addEventListener("rentka:openConsent", open);47    return () => window.removeEventListener("rentka:openConsent", open);48  }, []);4950  // the banner must never cover the floating controls (Filters button,51  // sticky CTA of the listing page): publish its height as a CSS variable52  // so they shift above it while it is displayed53  useEffect(() => {54    const root = document.documentElement;55    if (!visible) {56      root.style.setProperty("--consent-h", "0px");57      return;58    }59    const el = document.querySelector(".cookie-banner");60    const apply = () =>61      root.style.setProperty("--consent-h", `${(el?.getBoundingClientRect().height ?? 0) + 12}px`);62    apply();63    const ro = el ? new ResizeObserver(apply) : null;64    if (el && ro) ro.observe(el);65    return () => { ro?.disconnect(); root.style.setProperty("--consent-h", "0px"); };66  }, [visible, custom]);6768  if (!visible) return null;6970  const close = (p: boolean, s: boolean) => { save(p, s); setVisible(false); setCustom(false); };7172  return (73    <div className="cookie-banner" role="dialog" aria-modal="false" aria-label="Cookie preferences">74      <div className="cookie-inner">75        <div className="cookie-text">76          <b>🍪 Your cookies, your choice.</b>{" "}77          Rent-Ka uses no advertising trackers or third-party cookies. The site78          locally remembers your preferences (filters, map view) and your79          consent choice. <Link to="/privacy">Privacy policy</Link>80        </div>8182        {custom && (83          <div className="cookie-options">84            <label className="cookie-opt">85              <input type="checkbox" checked disabled />86              <span><b>Essential</b> — site operation (always on)</span>87            </label>88            <label className="cookie-opt">89              <input type="checkbox" checked={prefs} onChange={(e) => setPrefs(e.target.checked)} />90              <span><b>Preferences</b> — remember your filters and list/map view</span>91            </label>92            <label className="cookie-opt">93              <input type="checkbox" checked={stats} onChange={(e) => setStats(e.target.checked)} />94              <span><b>Statistics</b> — anonymous audience measurement, no profiling</span>95            </label>96          </div>97        )}9899        <div className="cookie-actions">100          {!custom && (101            <button className="btn btn-ghost" onClick={() => setCustom(true)}>102              Customize103            </button>104          )}105          <button className="btn btn-ghost" onClick={() => close(false, false)}>106            Essential only107          </button>108          {custom ? (109            <button className="btn btn-primary" onClick={() => close(prefs, stats)}>110              Save my choices111            </button>112          ) : (113            <button className="btn btn-primary" onClick={() => close(true, true)}>114              Accept all115            </button>116          )}117        </div>118      </div>119    </div>120  );121}122