release: dev -> main (P1 conception v0.2 + front P5 + admin shell) #1
4 changed files with 345 additions and 0 deletions
126
src/public/borne/assets/js/page-product.js
Normal file
126
src/public/borne/assets/js/page-product.js
Normal 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);
|
||||
86
src/public/borne/assets/js/page-products.js
Normal file
86
src/public/borne/assets/js/page-products.js
Normal 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);
|
||||
65
src/public/borne/product.html
Normal file
65
src/public/borne/product.html
Normal 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"
|
||||
>
|
||||
← 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">🛒</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>
|
||||
68
src/public/borne/products.html
Normal file
68
src/public/borne/products.html
Normal 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"
|
||||
>
|
||||
← 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">🛒</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>
|
||||
Loading…
Add table
Reference in a new issue