release: dev -> main (P1 conception v0.2 + front P5 + admin shell) #1

Merged
Corentin merged 34 commits from dev into main 2026-06-04 17:44:30 +02:00
4 changed files with 345 additions and 0 deletions
Showing only changes of commit cd6e05c353 - Show all commits

View file

@ -0,0 +1,126 @@
/*
* page-product.js Product detail screen.
*
* Reads ?id=<int>&category=<slug> from the query string.
* For menus (type === 'menu'): shows a fixed composition note rather than
* a detailed breakdown the school JSON does not include composition data.
* Decision: composition_libre_pour_MVP=non (fixed menu composition message).
*
* After "Ajouter au panier":
* 1. Item added to cart via state.addToCart()
* 2. Button changes to "Ajoute !" for 1 second (visual feedback)
* 3. Redirect to products.html?category=<slug>
*/
import { findProduct } from './data.js';
import { addToCart, formatPrice } from './state.js';
import { refreshCartBadge } from './nav.js';
const params = new URLSearchParams(window.location.search);
const productId = parseInt(params.get('id'), 10);
const categorySlug = params.get('category') ?? 'menus';
const container = document.getElementById('product-detail');
const errorBlock = document.getElementById('product-error');
const backBtn = document.getElementById('back-to-products');
if (backBtn) {
backBtn.href = `products.html?category=${categorySlug}`;
}
async function renderProduct() {
if (!productId) {
showError('Produit introuvable.');
return;
}
try {
const product = await findProduct(productId);
if (!product) {
showError('Ce produit n\'existe pas.');
return;
}
document.title = `Wakdo - ${product.nom}`;
const isMenu = product.type === 'menu';
container.innerHTML = `
<div class="product-detail__image-wrap">
<img
class="product-detail__image"
src="${product.image}"
alt="${product.nom}"
onerror="this.src='assets/images/ui/logo.png'; this.alt='Image non disponible';"
>
</div>
<div class="product-detail__info">
<h1 class="product-detail__name">${product.nom}</h1>
<p class="product-detail__price">${formatPrice(product.prix)}</p>
${isMenu ? renderMenuComposition() : ''}
<button
class="btn btn--primary btn--large product-detail__add"
id="add-to-cart-btn"
aria-label="Ajouter ${product.nom} au panier"
type="button"
>
Ajouter au panier
</button>
</div>
`;
document.getElementById('add-to-cart-btn').addEventListener('click', () => {
addToCart({
id: product.id,
type: product.type,
categorie: product.categorie ?? categorySlug,
libelle: product.nom,
prix_cents: product.prix,
quantite: 1,
image: product.image
});
refreshCartBadge();
const btn = document.getElementById('add-to-cart-btn');
btn.textContent = 'Ajoute !';
btn.disabled = true;
/* Redirect after brief confirmation pause */
setTimeout(() => {
window.location.href = `products.html?category=${categorySlug}`;
}, 1000);
});
} catch (err) {
showError('Erreur lors du chargement du produit.');
console.error('renderProduct error:', err);
}
}
/**
* Returns the HTML block for the menu composition note.
* The school JSON does not contain detailed composition this is the
* intentional simplification for MVP (composition_libre_pour_MVP=non).
*/
function renderMenuComposition() {
return `
<div class="product-detail__composition">
<h2 class="product-detail__composition-title">Composition du menu</h2>
<p class="product-detail__composition-text">
Menu compose : choix burger + accompagnement + boisson composition fixe pour ce MVP.
</p>
</div>
`;
}
function showError(msg) {
if (errorBlock) {
errorBlock.hidden = false;
errorBlock.textContent = msg;
}
if (container) {
container.hidden = true;
}
}
document.addEventListener('DOMContentLoaded', renderProduct);

View file

@ -0,0 +1,86 @@
/*
* page-products.js Products list screen.
*
* Reads ?category=<id> from the query string, maps to a slug via
* CATEGORY_ID_TO_SLUG, then fetches the matching product array.
* On product card click, navigates to product.html?id=<id>&category=<slug>.
*/
import { getProductsByCategory, getCategoryById, CATEGORY_ID_TO_SLUG } from './data.js';
import { formatPrice } from './state.js';
const params = new URLSearchParams(window.location.search);
const categoryId = parseInt(params.get('category'), 10) || 1;
const categorySlug = CATEGORY_ID_TO_SLUG[categoryId] ?? 'menus';
const grid = document.getElementById('products-grid');
const heading = document.getElementById('products-heading');
const backBtn = document.getElementById('back-to-categories');
const errorBlock = document.getElementById('products-error');
/* Build back URL preserving mode query param if present */
const modeParam = params.get('mode');
function buildBackURL() {
const base = 'categories.html';
return modeParam ? `${base}?mode=${modeParam}` : base;
}
if (backBtn) {
backBtn.href = buildBackURL();
}
async function renderProducts() {
try {
const [products, category] = await Promise.all([
getProductsByCategory(categorySlug),
getCategoryById(categoryId)
]);
if (heading && category) {
/* Capitalize first letter of the category title */
const title = category.title.charAt(0).toUpperCase() + category.title.slice(1);
heading.textContent = `Nos ${title}`;
document.title = `Wakdo - ${title}`;
}
if (!products.length) {
grid.innerHTML = '<p class="products-empty">Aucun produit disponible dans cette categorie.</p>';
return;
}
grid.innerHTML = '';
products.forEach(product => {
const card = document.createElement('a');
card.className = 'product-card';
card.href = `product.html?id=${product.id}&category=${categorySlug}`;
card.setAttribute('aria-label', `${product.nom} - ${formatPrice(product.prix)}`);
card.innerHTML = `
<div class="product-card__image-wrap">
<img
class="product-card__image"
src="${product.image}"
alt="${product.nom}"
loading="lazy"
onerror="this.src='assets/images/ui/logo.png'; this.alt='Image non disponible';"
>
</div>
<div class="product-card__body">
<span class="product-card__name">${product.nom}</span>
<span class="product-card__price">${formatPrice(product.prix)}</span>
</div>
`;
grid.appendChild(card);
});
} catch (err) {
if (errorBlock) {
errorBlock.hidden = false;
errorBlock.textContent = 'Impossible de charger les produits. Veuillez reessayer.';
}
console.error('renderProducts error:', err);
}
}
document.addEventListener('DOMContentLoaded', renderProducts);

View file

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<meta name="description" content="Wakdo - Detail du produit">
<title>Wakdo - Produit</title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="product-page">
<!--
product.html — Product detail screen.
Reads ?id=<int>&category=<slug>.
JS (page-product.js) fetches the product, renders detail and handles
the "Ajouter au panier" action.
Menu composition is shown as a fixed note (MVP: no composition selection).
-->
<header class="site-header">
<a
id="back-to-products"
class="site-header__back"
href="products.html"
aria-label="Retour a la liste des produits"
>
&#8592; Retour
</a>
<img
class="site-header__logo"
src="assets/images/ui/logo.png"
alt="Wakdo"
>
<a
class="site-header__cart"
href="cart.html"
aria-label="Voir le panier"
>
<span class="cart-icon" aria-hidden="true">&#128722;</span>
<span class="cart-badge" data-cart-count hidden aria-live="polite">0</span>
<span class="sr-only">Panier</span>
</a>
</header>
<main class="product-main" aria-label="Detail du produit">
<!-- Error block: hidden unless fetch fails or id invalid -->
<p id="product-error" class="product-error" hidden role="alert"></p>
<!--
Container filled by page-product.js.
The JS replaces innerHTML once data is ready.
-->
<div id="product-detail" class="product-detail" aria-live="polite">
<!-- Skeleton placeholder visible during fetch -->
<div class="product-detail__skeleton" aria-hidden="true"></div>
</div>
</main>
<script type="module" src="assets/js/nav.js"></script>
<script type="module" src="assets/js/page-product.js"></script>
</body>
</html>

View file

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<meta name="description" content="Wakdo - Produits de la categorie selectionnee">
<title>Wakdo - Produits</title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="products-page">
<!--
products.html — List of products in a category.
Category is determined at runtime from ?category=<id>.
JS (page-products.js) fetches data/produits.json and renders cards.
In P4: swap fetch URL in data.js to point to GET /api/products?category=<slug>.
-->
<header class="site-header">
<a
id="back-to-categories"
class="site-header__back"
href="categories.html"
aria-label="Retour aux categories"
>
&#8592; Categories
</a>
<img
class="site-header__logo"
src="assets/images/ui/logo.png"
alt="Wakdo"
>
<a
class="site-header__cart"
href="cart.html"
aria-label="Voir le panier"
>
<span class="cart-icon" aria-hidden="true">&#128722;</span>
<span class="cart-badge" data-cart-count hidden aria-live="polite">0</span>
<span class="sr-only">Panier</span>
</a>
</header>
<main class="products-main" aria-label="Liste des produits">
<div class="products-header">
<!--
Heading is updated by page-products.js once the category
data is loaded — default text shown during load.
-->
<h1 id="products-heading" class="products-main__heading">Nos produits</h1>
<span class="mode-badge" data-mode-badge aria-label="Mode de consommation">Sur place</span>
</div>
<!-- Error block: hidden by default, shown if fetch fails -->
<p id="products-error" class="products-error" hidden role="alert"></p>
<ul id="products-grid" class="products-grid" aria-label="Grille de produits">
<!-- Product cards injected by page-products.js -->
</ul>
</main>
<script type="module" src="assets/js/nav.js"></script>
<script type="module" src="assets/js/page-products.js"></script>
</body>
</html>