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// live.ts: shared "live data" hooks (home)5// -----------------------------------------------------------------------------6import { useEffect, useState } from "react";78/** Animated hero counter (~0.9 s, cubic easing) — the feel of a real engine9 indexing. Respects prefers-reduced-motion (direct value). */10export function useCountUp(target: number | null | undefined, ms = 900): string | null {11 const [v, setV] = useState<number | null>(null);12 useEffect(() => {13 if (target == null) return;14 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setV(target); return; }15 let raf = 0;16 const t0 = performance.now();17 const step = (t: number) => {18 const p = Math.min(1, (t - t0) / ms);19 setV(Math.round(target * (1 - Math.pow(1 - p, 3))));20 if (p < 1) raf = requestAnimationFrame(step);21 };22 raf = requestAnimationFrame(step);23 return () => cancelAnimationFrame(raf);24 }, [target, ms]);25 return v == null ? null : v.toLocaleString("en-CA");26}2728/** Live "X s ago" — re-rendered every second while the ts exists. */29export function useAgo(ts: number | null): string | null {30 const [, tick] = useState(0);31 useEffect(() => {32 if (ts == null) return;33 const id = setInterval(() => tick((x) => x + 1), 1000);34 return () => clearInterval(id);35 }, [ts]);36 if (ts == null) return null;37 const s = Math.max(0, Math.floor(Date.now() / 1000 - ts));38 if (s < 90) return `${s} s ago`;39 if (s < 5400) return `${Math.round(s / 60)} min ago`;40 return `${Math.round(s / 3600)} h ago`;41}42