SPB Git forge

spb/food-ka

Public

Food-Ka — agrégateur de produits d'épicerie du Québec — www.food-ka.com

55commits 1branches 0releases
10.2 MBsize
maindefault branch
9 days agolast push
Python 53.9% TypeScript 24% CSS 14.9% JavaScript 5.8% HTML 1.4%

refactor(fiche): ordre des sections = ordre du DOM sur tous les breakpoints

Standard Groupe Ka « Ordre des sections — pages détail » (même correctif
que Lou-Ka a6e30ef et Immo-Ka 38634e6) : plus de réordonnancement via
order:/display:contents — un bloc ajouté sans order retombait à order:0
et passait devant la galerie sur mobile.

- Product.tsx : hero (prix/solde/CTA) déplacé sous la galerie dans la
  colonne principale ; la comparaison de prix devient la colonne droite
- styles.css : suppression des order:/display:contents, colonnes empilées
  en flux naturel sur mobile
- scripts/check-order.mjs : validation Playwright de l ordre visuel

Validé iPhone 14 + desktop 1440px.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 20, 2026) parent de718dd

3 changed files +86 −41

added frontend/scripts/check-order.mjs +41 −0
@@ -0,0 +1,41 @@
1 +// Validation ordre des sections — fiche produit Food-Ka (ordre DOM = ordre visuel)
2 +import { chromium, devices } from "playwright";
3 +
4 +const BASE = process.env.BASE || "http://localhost:18097";
5 +const UID = process.argv[2];
6 +const URL = `${BASE}/produit/${encodeURIComponent(UID)}`;
7 +const SEL = [".f-galerie", ".f-hero", ".f-desc", ".f-pratique", ".f-compare"];
8 +
9 +async function check(name, ctxOpts) {
10 + const browser = await chromium.launch();
11 + const ctx = await browser.newContext(ctxOpts);
12 + const page = await ctx.newPage();
13 + await page.goto(URL, { waitUntil: "networkidle" });
14 + await page.waitForSelector(".f-galerie", { timeout: 15000 });
15 + await page.waitForTimeout(1200);
16 + const data = await page.evaluate((sel) => {
17 + const out = [];
18 + for (const s of sel) {
19 + const el = document.querySelector(s);
20 + if (!el) { out.push({ s, missing: true }); continue; }
21 + const r = el.getBoundingClientRect();
22 + out.push({ s, hidden: r.height === 0 && r.width === 0, top: Math.round(r.top + window.scrollY), left: Math.round(r.left), order: getComputedStyle(el).order });
23 + }
24 + return { out, scrollY: window.scrollY };
25 + }, SEL);
26 + console.log(`\n=== ${name} === scrollY: ${data.scrollY}`);
27 + for (const b of data.out)
28 + console.log(b.missing ? `${b.s.padEnd(14)} (non rendue)` : b.hidden ? `${b.s.padEnd(14)} (vide/masquée)` :
29 + `${b.s.padEnd(14)} top=${String(b.top).padStart(6)} left=${String(b.left).padStart(4)} order=${b.order}`);
30 + await browser.close();
31 + return data;
32 +}
33 +
34 +const mob = await check("iPhone 14 (mobile)", { ...devices["iPhone 14"] });
35 +await check("Desktop 1440px", { viewport: { width: 1440, height: 900 } });
36 +const vis = mob.out.filter(b => !b.missing && !b.hidden);
37 +const sorted = vis.every((b, i) => i === 0 || b.top >= vis[i - 1].top);
38 +const ok = sorted && mob.scrollY === 0 && vis[0].s === ".f-galerie" && vis.every(b => b.order === "0");
39 +console.log(`\nMOBILE: ordre ${sorted ? "CROISSANT ✓" : "DÉSORDONNÉ ✗"} · scrollY=${mob.scrollY} · 1re=${vis[0].s} · sans order=${vis.every(b => b.order === "0")}`);
40 +console.log(ok ? "VALIDATION OK" : "VALIDATION ÉCHEC");
41 +process.exit(ok ? 0 : 1);
modified frontend/src/pages/Product.tsx +38 −36
@@ -189,12 +189,48 @@ export default function ProductPage() {
189 189 </nav>
190 190
191 191 <div className="fiche">
192 − {/* ------- colonne gauche (desktop) : galerie, description, pratique -- */}
192 + {/* ------- colonne gauche (desktop) : galerie, prix, description, ----
193 + ------- pratique — l'ordre du DOM EST l'ordre visuel ------------- */}
193 194 <div className="f-col">
194 195 <section className="f-bloc f-galerie" aria-label="Images du produit">
195 196 <Galerie images={p.images ?? []} titre={p.name} />
196 197 </section>
197 198
199 + <section className="f-bloc f-hero">
200 + <div className="price-fav">
201 + <div className="price">
202 + {fmtPrice(p.price, p.price_label)}
203 + {p.on_sale && p.regular_price != null && (
204 + <s className="price-old big">{fmtPrice(p.regular_price)}</s>
205 + )}
206 + </div>
207 + <FavButton p={p} big />
208 + </div>
209 + {p.on_sale && (
210 + <div className="deal-badge deal-good">
211 + 🔥 En solde{pct != null && <> — épargnez {pct}{NBSP}%</>}
212 + </div>
213 + )}
214 + {p.unit_price_label && <div className="unit-price">{p.unit_price_label}</div>}
215 + <h1>{p.name}</h1>
216 + <div className="loc">
217 + {[p.brand, p.size_label].filter(Boolean).join(" · ")}
218 + </div>
219 + <div className="chips-scroll" role="list" aria-label="Caractéristiques clés">
220 + {chips.map((c) => <span className="chip-key" role="listitem" key={c}>{c}</span>)}
221 + </div>
222 + <a className="cta cta-desktop" href={p.url}
223 + target="_blank" rel="noopener noreferrer">
224 + <SourceLogo source={p.source} size={24} fallback="hide" />
225 + Voir chez {sourceName(p.source)} ↗
226 + </a>
227 + <p className="fine">
228 + Prix affiché par la bannière lors de la dernière synchronisation —
229 + vérifiez en magasin ou sur la fiche originale.
230 + </p>
231 + </section>
232 +
233 +
198 234 <section className="f-bloc f-desc" id="description">
199 235 <h2>Description</h2>
200 236 {p.description
@@ -251,42 +287,8 @@ export default function ProductPage() {
251 287 </section>
252 288 </div>
253 289
254 − {/* ------- colonne droite (desktop) : prix, comparaison --------------- */}
290 + {/* ------- colonne droite (desktop) : comparaison des prix ------------ */}
255 291 <div className="f-col">
256 − <section className="f-bloc f-hero">
257 − <div className="price-fav">
258 − <div className="price">
259 − {fmtPrice(p.price, p.price_label)}
260 − {p.on_sale && p.regular_price != null && (
261 − <s className="price-old big">{fmtPrice(p.regular_price)}</s>
262 − )}
263 − </div>
264 − <FavButton p={p} big />
265 − </div>
266 − {p.on_sale && (
267 − <div className="deal-badge deal-good">
268 − 🔥 En solde{pct != null && <> — épargnez {pct}{NBSP}%</>}
269 − </div>
270 − )}
271 − {p.unit_price_label && <div className="unit-price">{p.unit_price_label}</div>}
272 − <h1>{p.name}</h1>
273 − <div className="loc">
274 − {[p.brand, p.size_label].filter(Boolean).join(" · ")}
275 − </div>
276 − <div className="chips-scroll" role="list" aria-label="Caractéristiques clés">
277 − {chips.map((c) => <span className="chip-key" role="listitem" key={c}>{c}</span>)}
278 − </div>
279 − <a className="cta cta-desktop" href={p.url}
280 − target="_blank" rel="noopener noreferrer">
281 − <SourceLogo source={p.source} size={24} fallback="hide" />
282 − Voir chez {sourceName(p.source)} ↗
283 − </a>
284 − <p className="fine">
285 − Prix affiché par la bannière lors de la dernière synchronisation —
286 − vérifiez en magasin ou sur la fiche originale.
287 − </p>
288 − </section>
289 −
290 292 <section className="f-bloc f-compare" id="comparer">
291 293 <h2>Comparer les prix</h2>
292 294 {compare.length > 0 ? (
modified frontend/src/styles.css +7 −5
@@ -362,14 +362,16 @@ button { font-family: inherit; }
362 362 .crumbs a { border-bottom: 1.5px solid transparent; }
363 363 .crumbs a:hover { color: var(--green); border-color: var(--green); }
364 364
365 −/* mobile : flux unique ordonné ; desktop : 2 colonnes (produit | synthèse) */
365 +/* Ordre DOM = ordre visuel sur TOUS les breakpoints (standard Groupe Ka
366 + « Ordre des sections — pages détail »). Interdit : `order` / `column-reverse`
367 + / `display:contents` pour réordonner — un bloc sans `order` retombe à
368 + order:0 et passe devant la galerie sur mobile (bug corrigé sur Lou-Ka).
369 + Mobile : colonnes empilées ; desktop : grid 2 colonnes (produit | comparaison). */
366 370 .fiche { display: flex; flex-direction: column; gap: 22px; }
367 −.f-col { display: contents; }
368 −.f-galerie { order: 1; } .f-hero { order: 2; } .f-compare { order: 3; }
369 −.f-desc { order: 4; } .f-pratique { order: 5; }
371 +.f-col { display: flex; flex-direction: column; gap: 22px; min-width: 0; }
370 372 @media (min-width: 900px) {
371 373 .fiche { display: grid; grid-template-columns: 1.6fr 1fr; gap: 30px; align-items: start; }
372 − .f-col { display: flex; flex-direction: column; gap: 26px; min-width: 0; }
374 + .f-col { gap: 26px; }
373 375 }
374 376 .f-bloc { min-width: 0; }
375 377 .f-bloc h2 { font-size: 21px; letter-spacing: -0.02em; margin-bottom: 10px; }
376 378