Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1#!/usr/bin/env node2// -----------------------------------------------------------------------------3// Lou-Ka — kit Figma · shots.mjs4// Capture le site live www.lou-ka.com (desktop 1440 + mobile 390) et rapatrie5// quelques photos d'annonces réelles pour les maquettes.6// · references/fold/*.jpg : au-dessus du pli (embarqué dans le plugin)7// · references/full/*.jpg : pleine page (à glisser dans Figma au besoin)8// · references/photos/*.jpg : photos d'annonces réduites à 640 px9// Usage : node shots.mjs (Playwright de ~/ka-screenshots réutilisé)10// -----------------------------------------------------------------------------11import { createRequire } from "node:module";12import { mkdirSync, writeFileSync } from "node:fs";13import { execSync } from "node:child_process";14import path from "node:path";1516const require = createRequire(path.join(process.env.HOME, "ka-screenshots/node_modules/"));17const { chromium } = require("playwright");1819const ROOT = new URL(".", import.meta.url).pathname;20const REF = path.join(ROOT, "references");21for (const d of ["fold", "full", "photos"]) mkdirSync(path.join(REF, d), { recursive: true });2223const BASE = "https://www.lou-ka.com";2425// ---- 1. Choisir une vraie annonce riche (photos, prix, secteur) -------------26async function pickListing() {27 const r = await fetch(`${BASE}/api/search?city=Montr%C3%A9al&page_size=60&sort=recent`);28 const d = await r.json();29 const ok = d.listings.filter(30 (l) => (l.images?.length ?? 0) >= 6 && l.price >= 1100 && l.price <= 2600 && l.sector && l.unit_type,31 );32 ok.sort((a, b) => (b.fv_verdict ? 1 : 0) - (a.fv_verdict ? 1 : 0) || b.images.length - a.images.length);33 return { pick: ok[0] ?? d.listings[0], pool: d.listings };34}3536// ---- 2. Photos d'annonces (pour les cartes de la maquette) ------------------37async function grabPhotos(pool, pick) {38 const urls = [];39 for (const im of (pick.images ?? []).slice(0, 5)) urls.push(im);40 for (const l of pool) {41 if (urls.length >= 12) break;42 if (l.uid === pick.uid) continue;43 if (l.images?.[0] && l.price >= 900) urls.push(l.images[0]);44 }45 let i = 0;46 const kept = [];47 for (const u of urls) {48 try {49 const res = await fetch(u, { headers: { "User-Agent": "Mozilla/5.0" } });50 if (!res.ok) continue;51 const buf = Buffer.from(await res.arrayBuffer());52 if (buf.length < 8000) continue;53 const raw = path.join(REF, "photos", `raw-${i}.img`);54 writeFileSync(raw, buf);55 const out = path.join(REF, "photos", `photo-${String(i + 1).padStart(2, "0")}.jpg`);56 execSync(`sips -s format jpeg -s formatOptions 78 -Z 640 "${raw}" --out "${out}" >/dev/null 2>&1 && rm -f "${raw}"`);57 kept.push(out);58 i++;59 } catch (e) {60 /* on passe */61 }62 }63 return kept;64}6566// ---- 3. Captures d'écran --------------------------------------------------67const HIDE_CSS = `68 .cookie-banner, .ka-agent, #ka-agent, .ka-agent-btn, [class*="agent-fab"] { display: none !important; }69 .ticker-track { animation: none !important; }70 *, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; }71`;7273async function shoot(browser, { name, url, w, h, mobile }) {74 const ctx = await browser.newContext({75 viewport: { width: w, height: h },76 deviceScaleFactor: 2,77 isMobile: !!mobile,78 hasTouch: !!mobile,79 locale: "fr-CA",80 userAgent: mobile81 ? "Mozilla/5.0 (iPhone; CPU iPhone OS 26_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1"82 : undefined,83 });84 const page = await ctx.newPage();85 await page.addInitScript(() => {86 try { localStorage.setItem("louka_consent", JSON.stringify({ analytics: false, ts: Date.now() })); } catch {}87 });88 await page.goto(url, { waitUntil: "networkidle", timeout: 60000 }).catch(() => {});89 await page.addStyleTag({ content: HIDE_CSS });90 await page.waitForTimeout(1800);91 // fermer un éventuel bandeau de consentement resté visible92 for (const sel of ["button:has-text('Tout accepter')", "button:has-text('Accepter')", "button:has-text('Refuser')"]) {93 const b = page.locator(sel).first();94 if (await b.count()) { await b.click({ timeout: 1000 }).catch(() => {}); break; }95 }96 await page.evaluate(() => window.scrollTo(0, 0));97 await page.waitForTimeout(400);98 const fold = path.join(REF, "fold", `${name}.jpg`);99 await page.screenshot({ path: fold, type: "jpeg", quality: 80, fullPage: false });100 const full = path.join(REF, "full", `${name}.jpg`);101 await page.screenshot({ path: full, type: "jpeg", quality: 80, fullPage: true }).catch(() => {});102 await ctx.close();103 return fold;104}105106const { pick, pool } = await pickListing();107console.log("Annonce de référence :", pick.uid, "—", pick.title, pick.price, pick.city);108const photos = await grabPhotos(pool, pick);109console.log("Photos récupérées :", photos.length);110111const pages = [112 ["accueil", "/"],113 ["logement", `/logement/${encodeURIComponent(pick.uid)}`],114 ["stats", "/stats"],115 ["villes", "/villes"],116 ["court-terme", "/court-terme"],117];118const browser = await chromium.launch();119const done = [];120for (const [name, p] of pages) {121 done.push(await shoot(browser, { name: `desktop-${name}`, url: BASE + p, w: 1440, h: 900 }));122 done.push(await shoot(browser, { name: `mobile-${name}`, url: BASE + p, w: 390, h: 844, mobile: true }));123 console.log("✓", name);124}125await browser.close();126127writeFileSync(128 path.join(REF, "manifest.json"),129 JSON.stringify(130 {131 capturedAt: new Date().toISOString(),132 listing: { uid: pick.uid, title: pick.title, address: pick.address, sector: pick.sector, city: pick.city,133 unit_type: pick.unit_type, price: pick.price, price_label: pick.price_label, source: pick.source,134 availability: pick.availability, amenities: pick.amenities, bedrooms: pick.bedrooms, bathrooms: pick.bathrooms,135 fv_verdict: pick.fv_verdict, fv_deviation: pick.fv_deviation, ks_global: pick.ks_global, nImages: pick.images?.length },136 photos: photos.map((p) => path.basename(p)),137 shots: done.map((p) => path.basename(p)),138 },139 null,140 2,141 ),142);143console.log("Terminé →", REF);144