chore(borne): memoisation des loaders data.js + contraste a11y des etats selectionnes
Cloture deux findings differes de la revue L1. - data.js : loadCategories/loadProducts/loadProductsById/loadAllergens memoisent desormais la PROMESSE (et non le resultat). Les appelants concurrents au meme chargement (products.html : page-products + bandeau categories + panneau) partagent une seule requete /api/* au lieu d'en declencher plusieurs. Sur echec, la promesse est reinitialisee -> reessai possible (pas de cache d'erreur permanent). - style.css : l'indicateur d'etat selectionne (.composer-card--selected) passe au jaune fonce + anneau renforce (le bandeau categories l'avait deja en L1) pour le contraste non-texte WCAG 1.4.11. Tests JS inchanges : 64/64 verts (data.test.js couvre les loaders memoises).
This commit is contained in:
parent
7575d0458a
commit
f1f96b1278
2 changed files with 63 additions and 75 deletions
|
|
@ -1427,8 +1427,8 @@ button {
|
|||
|
||||
/* Selected state — mirrors aria-pressed=true */
|
||||
.composer-card--selected {
|
||||
border-color: var(--color-border-active);
|
||||
box-shadow: 0 0 0 3px rgba(255, 199, 44, 0.45), var(--shadow-card);
|
||||
border-color: var(--color-brand-yellow-dk); /* jaune fonce : contraste a11y 1.4.11 */
|
||||
box-shadow: 0 0 0 3px rgba(230, 168, 0, 0.55), var(--shadow-card);
|
||||
background: rgba(255, 199, 44, 0.06);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,14 +21,13 @@ const MENUS_URL = '/api/menus';
|
|||
* encore en place : bascule sur '/api/allergens' differee. */
|
||||
const ALLERGENS_URL = 'data/allergens.json';
|
||||
|
||||
/** @type {Array|null} — in-memory cache to avoid repeated fetches */
|
||||
let _categoriesCache = null;
|
||||
|
||||
/** @type {Object|null} */
|
||||
let _productsCache = null;
|
||||
|
||||
/** @type {Array|null} */
|
||||
let _allergensCache = null;
|
||||
/* Memoisation par PROMESSE (pas par resultat) : N appelants concurrents au meme
|
||||
* chargement partagent UNE seule requete reseau (evite les fetch /api/* redondants
|
||||
* au DOMContentLoaded de products.html). Sur echec, la promesse est reinitialisee
|
||||
* pour autoriser un nouvel essai. */
|
||||
let _categoriesPromise = null;
|
||||
let _productsPromise = null;
|
||||
let _allergensPromise = null;
|
||||
|
||||
/**
|
||||
* Recupere une collection enveloppee de l'API et renvoie le tableau `data`.
|
||||
|
|
@ -46,16 +45,13 @@ async function fetchCollection(url) {
|
|||
* Fetches and caches the categories list (forme borne : id, title, slug, image).
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
export async function loadCategories() {
|
||||
if (_categoriesCache) return _categoriesCache;
|
||||
const rows = await fetchCollection(CATEGORIES_URL);
|
||||
_categoriesCache = rows.map(c => ({
|
||||
id: c.id,
|
||||
title: c.name,
|
||||
slug: c.slug,
|
||||
image: c.image_path,
|
||||
}));
|
||||
return _categoriesCache;
|
||||
export function loadCategories() {
|
||||
if (!_categoriesPromise) {
|
||||
_categoriesPromise = fetchCollection(CATEGORIES_URL)
|
||||
.then(rows => rows.map(c => ({ id: c.id, title: c.name, slug: c.slug, image: c.image_path })))
|
||||
.catch(e => { _categoriesPromise = null; throw e; });
|
||||
}
|
||||
return _categoriesPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -65,52 +61,38 @@ export async function loadCategories() {
|
|||
* prix NORMAL (le supplement Maxi est gere par le composeur cote borne).
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function loadProducts() {
|
||||
if (_productsCache) return _productsCache;
|
||||
export function loadProducts() {
|
||||
if (_productsPromise) return _productsPromise;
|
||||
|
||||
const [categories, products, menus] = await Promise.all([
|
||||
_productsPromise = Promise.all([
|
||||
loadCategories(),
|
||||
fetchCollection(PRODUCTS_URL),
|
||||
fetchCollection(MENUS_URL),
|
||||
]);
|
||||
|
||||
]).then(([categories, products, menus]) => {
|
||||
const slugByCategoryId = {};
|
||||
const bySlug = {};
|
||||
for (const cat of categories) {
|
||||
slugByCategoryId[cat.id] = cat.slug;
|
||||
bySlug[cat.slug] = [];
|
||||
}
|
||||
|
||||
for (const p of products) {
|
||||
const slug = slugByCategoryId[p.category_id];
|
||||
if (slug === undefined) continue;
|
||||
bySlug[slug].push({
|
||||
id: p.id,
|
||||
nom: p.name,
|
||||
prix: p.price_cents,
|
||||
image: p.image_path,
|
||||
type: 'produit',
|
||||
});
|
||||
bySlug[slug].push({ id: p.id, nom: p.name, prix: p.price_cents, image: p.image_path, type: 'produit' });
|
||||
}
|
||||
|
||||
for (const m of menus) {
|
||||
const slug = slugByCategoryId[m.category_id];
|
||||
if (slug === undefined) continue;
|
||||
bySlug[slug].push({
|
||||
id: m.id,
|
||||
nom: m.name,
|
||||
prix: m.price_normal_cents,
|
||||
image: m.image_path,
|
||||
type: 'menu',
|
||||
});
|
||||
bySlug[slug].push({ id: m.id, nom: m.name, prix: m.price_normal_cents, image: m.image_path, type: 'menu' });
|
||||
}
|
||||
return bySlug;
|
||||
}).catch(e => { _productsPromise = null; throw e; });
|
||||
|
||||
_productsCache = bySlug;
|
||||
return _productsCache;
|
||||
return _productsPromise;
|
||||
}
|
||||
|
||||
/** @type {Object|null} — cache id->produit (type 'produit' uniquement) */
|
||||
let _productsByIdCache = null;
|
||||
/** @type {Promise|null} — index id->produit memoise (type 'produit' uniquement) */
|
||||
let _productsByIdPromise = null;
|
||||
|
||||
/**
|
||||
* Index des PRODUITS par id (type 'produit' seulement : exclut les menus, dont
|
||||
|
|
@ -119,17 +101,19 @@ let _productsByIdCache = null;
|
|||
* affichables. Derive de loadProducts() : aucune requete reseau supplementaire.
|
||||
* @returns {Promise<Object<number, Object>>}
|
||||
*/
|
||||
export async function loadProductsById() {
|
||||
if (_productsByIdCache) return _productsByIdCache;
|
||||
const bySlug = await loadProducts();
|
||||
export function loadProductsById() {
|
||||
if (!_productsByIdPromise) {
|
||||
_productsByIdPromise = loadProducts().then(bySlug => {
|
||||
const byId = {};
|
||||
for (const slug of Object.keys(bySlug)) {
|
||||
for (const item of bySlug[slug]) {
|
||||
if (item.type === 'produit') byId[item.id] = item;
|
||||
}
|
||||
}
|
||||
_productsByIdCache = byId;
|
||||
return _productsByIdCache;
|
||||
return byId;
|
||||
}).catch(e => { _productsByIdPromise = null; throw e; });
|
||||
}
|
||||
return _productsByIdPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -153,12 +137,16 @@ export async function loadMenu(id) {
|
|||
* la reponse est un tableau nu (pas d'enveloppe), conserve tel quel.
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
export async function loadAllergens() {
|
||||
if (_allergensCache) return _allergensCache;
|
||||
const res = await fetch(ALLERGENS_URL);
|
||||
export function loadAllergens() {
|
||||
if (!_allergensPromise) {
|
||||
_allergensPromise = fetch(ALLERGENS_URL)
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error(`Failed to load allergens: HTTP ${res.status}`);
|
||||
_allergensCache = await res.json();
|
||||
return _allergensCache;
|
||||
return res.json();
|
||||
})
|
||||
.catch(e => { _allergensPromise = null; throw e; });
|
||||
}
|
||||
return _allergensPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue