Correction PDF

This commit is contained in:
2025-09-16 22:20:21 +02:00
parent d9fca86145
commit 41bd8254e7
17 changed files with 591 additions and 110 deletions

View File

@@ -37,6 +37,19 @@ const splitTags = (str) => (str || '').split(/[,•]/).map(t => t.trim()).filter
const esc = (s) => { const d = document.createElement('div'); d.textContent = (s ?? ''); return d.innerHTML; };
function capFirst(s) { if (s == null) return ''; s = s.toString().trim(); if (!s) return ''; const f = s[0].toLocaleUpperCase('fr-FR'); return f + s.slice(1); }
const DEBOUNCE_MS = 250;
function debounce(fn, wait = DEBOUNCE_MS) {
let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
}
function onlySearchActive() {
return normalize(currentSearchTerm) !== '' &&
appliedFilters.ingredients.length === 0 &&
appliedFilters.tags.length === 0 &&
appliedFilters.timeMin == null &&
appliedFilters.timeMax == null;
}
/***********************
* COLORS for tags (persistées localStorage)
***********************/
@@ -326,28 +339,44 @@ function buildRecipeCardOwned(recipe) {
async function loadRecipes(page = 1) {
try {
const url = new URL('/HelloFresh/GetRecipesByPage', window.location.origin);
url.searchParams.set('page', page); url.searchParams.set('count', countPerPage);
const res = await fetch(url.toString()); if (!res.ok) throw new Error(`HTTP ${res.status}`);
url.searchParams.set('page', page);
url.searchParams.set('count', countPerPage);
// 👉 On laisse le serveur faire la recherche quand il n'y a QUE la recherche active
if (onlySearchActive()) {
url.searchParams.set('search', currentSearchTerm || '');
}
const res = await fetch(url.toString());
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const { recipes, currentPage: serverPage, lastPage } = data;
currentPage = serverPage; lastPageGlobal = lastPage;
const grid = document.getElementById('recipeGrid'); grid.innerHTML = '';
if (!recipes || recipes.length === 0) {
grid.innerHTML = '<p class="placeholder">Aucune recette disponible pour le moment.</p>';
renderPagination(currentPage, lastPageGlobal); return;
renderPagination(currentPage, lastPageGlobal);
return;
}
recipes.forEach(r => grid.appendChild(buildRecipeCardList(r)));
if (window.tippy) {
tippy('[data-tippy-content]', { placement: 'top', arrow: true, delay: [100, 0], theme: 'light-border', maxWidth: 250, allowHTML: true });
}
await hydrateIngredientsForPage(recipes);
refreshSelectedBorders();
if ((currentSearchTerm ?? '').trim() !== '') applySearchFilter();
// ⚠️ si tu avais une ancienne fonction applySearchFilter(), on ne l'appelle plus ici.
// if ((currentSearchTerm ?? '').trim() !== '' && typeof applySearchFilter === 'function') applySearchFilter();
refreshDoneFlagsOnVisibleCards();
renderPagination(currentPage, lastPageGlobal);
} catch (e) { console.error('loadRecipes error', e); }
}
async function loadRecipesOwned({ onEmpty = 'placeholder' } = {}) {
const gridOwned = document.getElementById('recipeGridOwned'); if (!gridOwned) return false;
const wasOpen = document.getElementById('recipeOwnedWrap')?.classList.contains('open');
@@ -450,19 +479,40 @@ function anyFilterActive() {
appliedFilters.timeMin != null ||
appliedFilters.timeMax != null;
}
async function fetchAllRecipes() {
if (ALL_RECIPES_CACHE) return ALL_RECIPES_CACHE;
async function fetchAllRecipes(opts = {}) {
const search = (opts.search || '').trim();
if (!search && ALL_RECIPES_CACHE) return ALL_RECIPES_CACHE;
let page = 1, last = 1, all = [];
do {
const url = new URL('/HelloFresh/GetRecipesByPage', window.location.origin);
url.searchParams.set('page', page); url.searchParams.set('count', countPerPage);
const res = await fetch(url.toString()); if (!res.ok) throw new Error(`HTTP ${res.status}`);
url.searchParams.set('page', page);
url.searchParams.set('count', countPerPage);
if (search) url.searchParams.set('search', search);
const res = await fetch(url.toString());
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
all.push(...(data.recipes || []));
last = data.lastPage || 1; page++;
last = data.lastPage || 1;
page++;
} while (page <= last);
ALL_RECIPES_CACHE = all; return all;
if (!search) ALL_RECIPES_CACHE = all;
return all;
}
async function fetchDetailsBatched(ids, batchSize = 150) {
const out = [];
for (let i = 0; i < ids.length; i += batchSize) {
const part = ids.slice(i, i + batchSize);
const res = await fetch(`/HelloFresh/GetRecipesDetails?ids=${encodeURIComponent(part.join(','))}`);
if (res.ok) out.push(...(await res.json()));
}
return out;
}
function recipeMatchesFilters(cardMeta) {
const term = normalize(currentSearchTerm);
if (term && !cardMeta.name.includes(term)) return false;
@@ -487,14 +537,16 @@ function extractIngredientNames(ingredientsJson) {
} catch { }
return [...out];
}
async function buildMetaFor(recipes) {
const ids = recipes.map(r => r.id).join(',');
const ids = recipes.map(r => r.id);
const metaById = {};
try {
const res = await fetch(`/HelloFresh/GetRecipesDetails?ids=${encodeURIComponent(ids)}`);
const details = await res.json(); // [{id, ingredientsJson}]
const details = await fetchDetailsBatched(ids, 150); // ← chunks pour éviter les URLs géantes
details.forEach(d => { metaById[d.id] = { ing: extractIngredientNames(d.ingredientsJson).map(normalize) }; });
} catch (e) { console.warn('details fail', e); }
recipes.forEach(r => {
const tagsVal = (r.tags ?? r.Tags ?? '').trim();
const tags = splitTags(tagsVal).map(normalize);
@@ -502,24 +554,45 @@ async function buildMetaFor(recipes) {
const name = normalize(r.name || '');
metaById[r.id] = Object.assign({ tags, time, name, ing: [] }, metaById[r.id] || {});
});
return metaById;
}
async function applyAllFilters() {
if (!anyFilterActive()) {
const hasSearch = normalize(currentSearchTerm) !== '';
const hasOtherFilters = appliedFilters.ingredients.length || appliedFilters.tags.length ||
appliedFilters.timeMin != null || appliedFilters.timeMax != null;
// A) Rien d'actif → liste classique
if (!hasSearch && !hasOtherFilters) {
CURRENT_CLIENT_LIST = null;
await loadRecipes(1);
updateActiveFilterBadge(); // peut masquer le badge si rien dactif
updateActiveFilterBadge();
return;
}
const all = await fetchAllRecipes();
// B) Uniquement la recherche → serveur (rapide, paginé)
if (hasSearch && !hasOtherFilters) {
CURRENT_CLIENT_LIST = null; // on revient au flux serveur paginé
currentPage = 1;
await loadRecipes(1); // loadRecipes enverra ?search=...
updateActiveFilterBadge();
return;
}
// C) Recherche + (tags/ingrédients/temps) OU filtres sans recherche
// → on réduit d'abord côté serveur avec ?search=..., puis filtre côté client
const all = await fetchAllRecipes({ search: hasSearch ? currentSearchTerm : '' });
const meta = await buildMetaFor(all);
const filtered = all.filter(r => recipeMatchesFilters(meta[r.id] || {}));
CURRENT_CLIENT_LIST = filtered;
currentPage = 1;
lastPageGlobal = Math.max(1, Math.ceil(filtered.length / countPerPage));
await renderClientPage();
updateActiveFilterBadge(); // badge = basé sur appliedFilters UNIQUEMENT
updateActiveFilterBadge();
}
async function renderClientPage() {
const list = CURRENT_CLIENT_LIST || [];
const start = (currentPage - 1) * countPerPage;
@@ -809,9 +882,10 @@ document.addEventListener('DOMContentLoaded', async () => {
// Live search
const searchInput = document.querySelector('#divSearch input');
if (searchInput) {
searchInput.addEventListener('input', async (e) => {
const debounced = debounce(async () => { await applyAllFilters(); }, 250);
searchInput.addEventListener('input', (e) => {
currentSearchTerm = e.target.value;
await applyAllFilters(); // search est immédiat
debounced();
});
}