289 lines
9.6 KiB
JavaScript
289 lines
9.6 KiB
JavaScript
// ==============================
|
||
// Cuisine / Lecteur de recettes
|
||
// ==============================
|
||
(() => {
|
||
"use strict";
|
||
|
||
// ---------- Sélecteurs ----------
|
||
const $ = (sel) => document.querySelector(sel);
|
||
|
||
const els = {
|
||
root: $("#cuisineApp"),
|
||
scroller: $("#recipeScroller"),
|
||
title: $("#currentName"),
|
||
btnOpen: $("#openPdf"),
|
||
btnToggle: $("#toggleSide"),
|
||
iframePrev: $("#pdfPreview"),
|
||
placeholder: $("#pdfPlaceholder"),
|
||
overlay: $("#viewerOverlay"),
|
||
overlayTit: $("#viewerTitle"),
|
||
iframeFull: $("#pdfFrameFull"),
|
||
btnClose: $("#closeViewer"),
|
||
};
|
||
|
||
// ---------- Etat ----------
|
||
const PDF_DEFAULT_ZOOM = "page-fit"; // "page-fit" | "page-width" | "100"
|
||
const LS_KEY_COLLAPSED = "cui_sidebar_collapsed";
|
||
|
||
/** @type {{id:string,name:string,image:string,tempsDePreparation:number|null,pdf:string,portions:number}[]} */
|
||
let RECIPES = [];
|
||
let CUR = -1; // index courant
|
||
|
||
// ---------- Utils ----------
|
||
const normalize = (s) => (s ?? "").toString().trim().toLowerCase();
|
||
const proxify = (url) => url ? `/HelloFresh/ProxyPdf?url=${encodeURIComponent(url)}` : "";
|
||
|
||
|
||
function buildPdfSrc(rawUrl) {
|
||
if (!rawUrl) return "";
|
||
const base = `/HelloFresh/ProxyPdf?url=${encodeURIComponent(rawUrl)}`;
|
||
// cache la barre (toolbar=0) + fixe le zoom
|
||
return `${base}#toolbar=0&zoom=${encodeURIComponent(PDF_DEFAULT_ZOOM)}`;
|
||
}
|
||
|
||
// Dédup par nom (on garde la 1ère qui a un pdf / image)
|
||
function dedupByName(rows) {
|
||
const map = new Map(); // key: normalized name -> row
|
||
for (const r of rows) {
|
||
const key = normalize(r.name);
|
||
if (!key) continue;
|
||
if (!map.has(key)) {
|
||
map.set(key, r);
|
||
} else {
|
||
const kept = map.get(key);
|
||
// privilégier une entrée avec PDF / image
|
||
if (!kept.pdf && r.pdf) map.set(key, r);
|
||
else if (!kept.image && r.image) map.set(key, r);
|
||
}
|
||
}
|
||
return [...map.values()];
|
||
}
|
||
|
||
// ---------- Rendu de la liste ----------
|
||
function renderList(list) {
|
||
const wrap = els.scroller;
|
||
if (!wrap) return;
|
||
wrap.innerHTML = "";
|
||
|
||
list.forEach((r, idx) => {
|
||
const card = document.createElement("div");
|
||
card.className = "cui-card";
|
||
card.tabIndex = 0;
|
||
card.dataset.idx = String(idx);
|
||
|
||
card.innerHTML = `
|
||
<img class="cui-thumb" src="${r.image || ""}" alt="">
|
||
<div class="cui-info">
|
||
<div class="cui-name" title="${r.name}">${r.name}</div>
|
||
<div class="cui-meta">${r.tempsDePreparation ? (r.tempsDePreparation + " min") : ""}</div>
|
||
</div>
|
||
`;
|
||
|
||
// clic / entrée -> sélection
|
||
card.addEventListener("click", () => selectIdx(idx));
|
||
card.addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter" || e.key === " ") {
|
||
e.preventDefault();
|
||
selectIdx(idx);
|
||
}
|
||
});
|
||
|
||
wrap.appendChild(card);
|
||
});
|
||
|
||
markActive();
|
||
}
|
||
|
||
function markActive() {
|
||
els.scroller?.querySelectorAll(".cui-card").forEach((el, i) => {
|
||
el.classList.toggle("active", i === CUR);
|
||
});
|
||
}
|
||
|
||
// ---------- Sélection / Toolbar / Preview ----------
|
||
function selectIdx(idx) {
|
||
if (idx < 0 || idx >= RECIPES.length) return;
|
||
CUR = idx;
|
||
const r = RECIPES[CUR];
|
||
|
||
// Titre (ellipsis géré par CSS)
|
||
if (els.title) els.title.textContent = r.name || "Recette";
|
||
|
||
// Bouton ouvrir
|
||
if (els.btnOpen) {
|
||
const can = !!r.pdf;
|
||
els.btnOpen.disabled = !can;
|
||
els.btnOpen.setAttribute("aria-disabled", String(!can));
|
||
els.btnOpen.title = can ? "Ouvrir le PDF" : "PDF indisponible";
|
||
}
|
||
|
||
// Aperçu PDF
|
||
loadPreview(r.pdf);
|
||
|
||
// Etat actif visuel
|
||
markActive();
|
||
}
|
||
|
||
function loadPreview(url) {
|
||
if (!els.iframePrev || !els.placeholder) return;
|
||
|
||
if (url) {
|
||
els.iframePrev.src = buildPdfSrc(url);
|
||
els.iframePrev.style.display = "block";
|
||
els.placeholder.style.display = "none";
|
||
} else {
|
||
els.iframePrev.removeAttribute("src");
|
||
els.iframePrev.style.display = "none";
|
||
els.placeholder.style.display = "grid";
|
||
}
|
||
}
|
||
|
||
// ---------- Plein écran ----------
|
||
function openViewer() {
|
||
if (CUR < 0) return;
|
||
const r = RECIPES[CUR];
|
||
if (!r.pdf || !els.overlay || !els.iframeFull || !els.overlayTit) return;
|
||
|
||
els.overlayTit.textContent = r.name || "PDF";
|
||
els.iframeFull.src = buildPdfSrc(r.pdf);
|
||
els.overlay.classList.remove("hidden");
|
||
els.overlay.setAttribute("aria-hidden", "false");
|
||
// tente le plein écran (si autorisé par l’UA)
|
||
try { els.overlay.requestFullscreen(); } catch { }
|
||
}
|
||
|
||
function closeViewer() {
|
||
if (!els.overlay || !els.iframeFull) return;
|
||
els.iframeFull.removeAttribute("src");
|
||
els.overlay.classList.add("hidden");
|
||
els.overlay.setAttribute("aria-hidden", "true");
|
||
if (document.fullscreenElement) {
|
||
try { document.exitFullscreen(); } catch { }
|
||
}
|
||
}
|
||
|
||
// Navigation dans le viewer
|
||
function next() {
|
||
if (CUR < RECIPES.length - 1) {
|
||
selectIdx(CUR + 1);
|
||
if (!els.overlay?.classList.contains("hidden") && els.iframeFull) {
|
||
const r = RECIPES[CUR];
|
||
els.iframeFull.src = buildPdfSrc(r.pdf || "");
|
||
}
|
||
}
|
||
}
|
||
function prev() {
|
||
if (CUR > 0) {
|
||
selectIdx(CUR - 1);
|
||
if (!els.overlay?.classList.contains("hidden") && els.iframeFull) {
|
||
const r = RECIPES[CUR];
|
||
els.iframeFull.src = buildPdfSrc(r.pdf || "");
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------- Données ----------
|
||
async function fetchRecipes() {
|
||
const res = await fetch("/HelloFresh/GetRecipesOwned", { credentials: "same-origin" });
|
||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||
const raw = await res.json();
|
||
|
||
// Normalisation des propriétés
|
||
const norm = (r) => ({
|
||
id: r.id ?? r.Id ?? "",
|
||
name: r.name ?? r.Name ?? "",
|
||
image: r.image ?? r.Image ?? "",
|
||
tempsDePreparation: r.tempsDePreparation ?? r.TempsDePreparation ?? null,
|
||
pdf: r.pdf ?? r.Pdf ?? "",
|
||
portions: Number(r.portions ?? r.Portions ?? 0) || 0
|
||
});
|
||
|
||
let rows = Array.isArray(raw) ? raw.map(norm) : [];
|
||
rows = dedupByName(rows);
|
||
|
||
// Tri par nom (français)
|
||
rows.sort((a, b) => String(a.name).localeCompare(String(b.name), "fr", { sensitivity: "base" }));
|
||
|
||
return rows;
|
||
}
|
||
|
||
async function loadRecipes() {
|
||
try {
|
||
RECIPES = await fetchRecipes();
|
||
renderList(RECIPES);
|
||
|
||
// Auto-sélection : d'abord la 1ère avec PDF, sinon la 1ère tout court
|
||
const firstPdf = RECIPES.findIndex(r => !!r.pdf);
|
||
const idx = firstPdf >= 0 ? firstPdf : (RECIPES.length ? 0 : -1);
|
||
if (idx >= 0) selectIdx(idx);
|
||
else selectIdx(-1);
|
||
} catch (err) {
|
||
console.error("[Cuisine] GetRecipesOwned failed:", err);
|
||
RECIPES = [];
|
||
renderList([]);
|
||
selectIdx(-1);
|
||
}
|
||
}
|
||
|
||
// ---------- Sidebar collapse (persisté) ----------
|
||
function setCollapsed(collapsed) {
|
||
if (!els.root) return;
|
||
els.root.classList.toggle("collapsed", !!collapsed);
|
||
els.btnToggle?.setAttribute("aria-expanded", String(!collapsed));
|
||
try { localStorage.setItem(LS_KEY_COLLAPSED, JSON.stringify(!!collapsed)); } catch { }
|
||
}
|
||
function getCollapsed() {
|
||
try {
|
||
const v = localStorage.getItem(LS_KEY_COLLAPSED);
|
||
return v ? JSON.parse(v) : false;
|
||
} catch { return false; }
|
||
}
|
||
|
||
// ---------- Gestes / Clavier (viewer) ----------
|
||
(function wireGestures() {
|
||
if (!els.overlay) return;
|
||
|
||
// Swipe horizontal (tablette)
|
||
let touchStartX = 0;
|
||
const SWIPE_MIN = 60;
|
||
els.overlay.addEventListener("touchstart", (e) => {
|
||
if (!e.changedTouches?.length) return;
|
||
touchStartX = e.changedTouches[0].clientX;
|
||
}, { passive: true });
|
||
|
||
els.overlay.addEventListener("touchend", (e) => {
|
||
if (!e.changedTouches?.length) return;
|
||
const dx = e.changedTouches[0].clientX - touchStartX;
|
||
if (Math.abs(dx) > SWIPE_MIN) (dx < 0 ? next : prev)();
|
||
}, { passive: true });
|
||
|
||
// Clavier (visible uniquement dans le viewer)
|
||
document.addEventListener("keydown", (e) => {
|
||
if (els.overlay?.classList.contains("hidden")) return;
|
||
if (e.key === "ArrowRight") next();
|
||
else if (e.key === "ArrowLeft") prev();
|
||
else if (e.key === "Escape") closeViewer();
|
||
});
|
||
})();
|
||
|
||
// ---------- Wiring des boutons ----------
|
||
function wireUI() {
|
||
els.btnOpen?.addEventListener("click", openViewer);
|
||
els.btnClose?.addEventListener("click", closeViewer);
|
||
|
||
els.btnToggle?.addEventListener("click", () => {
|
||
setCollapsed(!els.root?.classList.contains("collapsed"));
|
||
});
|
||
}
|
||
|
||
// ---------- Boot ----------
|
||
document.addEventListener("DOMContentLoaded", async () => {
|
||
// Appliquer l’état collapsé mémorisé
|
||
setCollapsed(getCollapsed());
|
||
|
||
wireUI();
|
||
await loadRecipes();
|
||
});
|
||
|
||
})();
|