fix(borne): panier unique = panneau persistant (retrait cart.html + product.html)
All checks were successful
CI / php-lint (pull_request) Successful in 32s
CI / secret-scan (pull_request) Successful in 11s
CI / static-tests (pull_request) Successful in 1m14s
CI / js-tests (pull_request) Successful in 42s
CI / secret-scan (push) Successful in 13s
CI / php-lint (push) Successful in 33s
CI / static-tests (push) Successful in 1m12s
CI / js-tests (push) Successful in 37s
All checks were successful
CI / php-lint (pull_request) Successful in 32s
CI / secret-scan (pull_request) Successful in 11s
CI / static-tests (pull_request) Successful in 1m14s
CI / js-tests (pull_request) Successful in 42s
CI / secret-scan (push) Successful in 13s
CI / php-lint (push) Successful in 33s
CI / static-tests (push) Successful in 1m12s
CI / js-tests (push) Successful in 37s
Le panier avait deux vues divergentes : la page cart.html et le panneau de droite. On garde le PANNEAU comme unique vue panier et on supprime : - cart.html + page-cart.js (page panier separee) ; - product.html + page-product.js (fiche produit fantome non atteinte : le clic produit ouvre une modale) ; - l'icone panier de l'en-tete (le panneau montre la commande). Le panneau devient un panier complet : stepper +/- par ligne (decrementer a 0 retire la ligne), en plus du bouton retrait. Cibles tactiles 44px. Repointe le lien retour de payment.html et la redirection panier-vide vers categories.html ; neutralise le href de repli des cartes produit ; nettoie le CSS mort (cart-*, product-detail*, site-header__cart). E2E reecrit (modale options + panneau au lieu de product.html / cart.html). JS 101 verts.
This commit is contained in:
parent
0968a98668
commit
3f026d96c6
16 changed files with 166 additions and 950 deletions
|
|
@ -292,7 +292,6 @@ button {
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-header__cart,
|
|
||||||
.site-header__mode {
|
.site-header__mode {
|
||||||
justify-self: end;
|
justify-self: end;
|
||||||
}
|
}
|
||||||
|
|
@ -454,49 +453,6 @@ button {
|
||||||
7. SHARED COMPONENTS — header extensions + badges + buttons
|
7. SHARED COMPONENTS — header extensions + badges + buttons
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
/* Cart link in header (products / product / cart pages) */
|
|
||||||
.site-header__cart {
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
transition: background var(--transition-fast);
|
|
||||||
font-size: 1.6rem;
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-header__cart:hover,
|
|
||||||
.site-header__cart:focus-visible {
|
|
||||||
background: rgba(255, 199, 44, 0.18);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-icon {
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 4px;
|
|
||||||
right: 4px;
|
|
||||||
min-width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
padding: 0 4px;
|
|
||||||
border-radius: var(--radius-pill);
|
|
||||||
background: var(--color-brand-red);
|
|
||||||
color: #fff;
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mode badge — shown in header for context */
|
/* Mode badge — shown in header for context */
|
||||||
.mode-badge {
|
.mode-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
@ -744,215 +700,9 @@ button {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
9. COMPONENT — PRODUCT DETAIL (product.html)
|
10. COMPONENT — QUANTITY CONTROLS (modale options produit)
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
.product-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: var(--color-bg-page);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-main {
|
|
||||||
flex: 1;
|
|
||||||
padding: var(--space-6);
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-error {
|
|
||||||
color: var(--color-brand-red);
|
|
||||||
font-size: var(--font-size-md);
|
|
||||||
padding: var(--space-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-detail {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-detail__skeleton {
|
|
||||||
/* Placeholder shown while JS loads data */
|
|
||||||
width: 100%;
|
|
||||||
height: 300px;
|
|
||||||
background: var(--color-bg-card);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
animation: skeleton-pulse 1.4s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes skeleton-pulse {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.4; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-detail__image-wrap {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 480px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: var(--color-bg-card);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
overflow: hidden;
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-detail__image {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
padding: var(--space-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-detail__info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-detail__name {
|
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-detail__price {
|
|
||||||
font-size: var(--font-size-2xl);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
color: var(--color-brand-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-detail__composition {
|
|
||||||
background: var(--color-bg-card);
|
|
||||||
border-left: 4px solid var(--color-brand-yellow);
|
|
||||||
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
|
||||||
padding: var(--space-4) var(--space-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-detail__composition-title {
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-detail__composition-text {
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-detail__add {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
10. COMPONENT — CART (cart.html)
|
|
||||||
============================================================ */
|
|
||||||
|
|
||||||
.cart-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: var(--color-bg-page);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-main {
|
|
||||||
flex: 1;
|
|
||||||
padding: var(--space-6);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-5);
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-main__heading {
|
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-empty {
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--space-10) var(--space-6);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-empty__message {
|
|
||||||
font-size: var(--font-size-lg);
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-3);
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* One cart line */
|
|
||||||
.cart-line {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-4);
|
|
||||||
background: var(--color-bg-card);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
padding: var(--space-3) var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-line__image {
|
|
||||||
width: 72px;
|
|
||||||
height: 72px;
|
|
||||||
object-fit: contain;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
background: var(--color-bg-page);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-line__info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-line__name {
|
|
||||||
display: block;
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-line__unit-price {
|
|
||||||
display: block;
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Quantity controls */
|
|
||||||
.cart-line__qty {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qty-btn {
|
.qty-btn {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
|
|
@ -984,73 +734,6 @@ button {
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-line__total {
|
|
||||||
font-size: var(--font-size-md);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
flex-shrink: 0;
|
|
||||||
min-width: 80px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-line__remove {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: var(--space-2);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
transition: background var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-line__remove:hover,
|
|
||||||
.cart-line__remove:focus-visible {
|
|
||||||
background: rgba(218, 2, 14, 0.10);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Order summary block */
|
|
||||||
.cart-summary {
|
|
||||||
background: var(--color-bg-card);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
padding: var(--space-5);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-summary__line {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-summary__line--total {
|
|
||||||
padding-top: var(--space-3);
|
|
||||||
border-top: 2px solid var(--color-border-default);
|
|
||||||
font-size: var(--font-size-lg);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cart action row */
|
|
||||||
.cart-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-4);
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding-bottom: var(--space-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-actions .btn {
|
|
||||||
flex: 1 1 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
11. COMPONENT — PAYMENT (payment.html)
|
11. COMPONENT — PAYMENT (payment.html)
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
@ -1268,15 +951,6 @@ button {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-line {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-line__info {
|
|
||||||
flex: 1 1 calc(100% - 80px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment-methods {
|
.payment-methods {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
@ -1292,14 +966,10 @@ button {
|
||||||
.products-main {
|
.products-main {
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-main {
|
|
||||||
padding: var(--space-4);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
14. COMPONENT — MENU COMPOSER MODAL (product.html, type=menu)
|
14. COMPONENT — MENU COMPOSER MODAL (modale ouverte depuis la grille produits)
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -1515,10 +1185,6 @@ button {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-detail__info .allergen-info-btn {
|
|
||||||
margin-top: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.allergen-modal-overlay {
|
.allergen-modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|
@ -1788,31 +1454,6 @@ button {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Cart line composition display -------------------- */
|
|
||||||
|
|
||||||
.cart-line__composition {
|
|
||||||
list-style: none;
|
|
||||||
padding: var(--space-1) 0 0;
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-line__comp-item {
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-line__comp-suppl {
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
color: var(--color-brand-yellow-dk);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
line-height: 1.4;
|
|
||||||
padding-top: var(--space-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Responsive — narrow screens --------------------- */
|
/* ---------- Responsive — narrow screens --------------------- */
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
|
|
@ -1920,8 +1561,7 @@ button {
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-panel__line {
|
.order-panel__line {
|
||||||
position: relative;
|
padding: var(--space-3) 0;
|
||||||
padding: var(--space-3) var(--space-6) var(--space-3) 0;
|
|
||||||
border-bottom: 1px solid var(--color-border-default);
|
border-bottom: 1px solid var(--color-border-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1952,14 +1592,49 @@ button {
|
||||||
content: "+ ";
|
content: "+ ";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ligne de controles : stepper de quantite a gauche, retrait a droite. */
|
||||||
|
.order-panel__line-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-panel__qty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Boutons +/- : cible tactile confortable (borne). */
|
||||||
|
.order-panel__qty-btn {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
border: 2px solid var(--color-border-default);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-panel__qty-value {
|
||||||
|
min-width: 2ch;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
.order-panel__remove {
|
.order-panel__remove {
|
||||||
position: absolute;
|
display: flex;
|
||||||
top: var(--space-3);
|
align-items: center;
|
||||||
right: 0;
|
justify-content: center;
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: var(--space-1);
|
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
/*
|
/*
|
||||||
* order-panel.js — Panneau de commande persistant (maquette : recap a droite de
|
* order-panel.js — Panneau de commande persistant (maquette : recap a droite de
|
||||||
* l'ecran de commande). Rendu sur les ecrans de commande (products, product) pour
|
* l'ecran de commande). Rendu sur l'ecran de commande (products) pour que le panier
|
||||||
* que le panier reste visible en permanence, comme sur la maquette borne.
|
* reste visible en permanence, comme sur la maquette borne.
|
||||||
*
|
*
|
||||||
* C'est un miroir COMPACT de page-cart.js : meme modele d'item, meme rendu de la
|
* C'est l'UNIQUE vue panier : il montre lignes + total + Abandon/Payer, permet
|
||||||
* composition de menu. La page panier (cart.html) reste la vue detaillee (TVA, +/-) ;
|
* d'ajuster la quantite de chaque ligne (+/-) et de la retirer. La logique de mise
|
||||||
* le panneau, lui, montre lignes + total + Abandon/Payer et permet de retirer une
|
* en forme est extraite en fonctions PURES (buildPanelModel, compositionLabels)
|
||||||
* ligne. La logique de mise en forme est extraite en fonctions PURES (buildPanelModel,
|
* pour etre testable sans DOM.
|
||||||
* compositionLabels) pour etre testable sans DOM.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getCart,
|
getCart,
|
||||||
removeFromCart,
|
removeFromCart,
|
||||||
|
updateQuantity,
|
||||||
computeMenuLineCents,
|
computeMenuLineCents,
|
||||||
clearCart,
|
clearCart,
|
||||||
formatPrice,
|
formatPrice,
|
||||||
|
|
@ -35,8 +35,8 @@ export function lineCents(item) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construit les libelles des options d'un menu (puces sous le nom de ligne).
|
* Construit les libelles des options d'un menu (puces sous le nom de ligne).
|
||||||
* Miroir de renderCompositionBlock() de page-cart.js, sans le supplement (le panneau
|
* Sans le supplement (le panneau affiche le total de ligne, pas le detail TVA).
|
||||||
* affiche le total de ligne, pas le detail TVA). Tolerant aux composants absents.
|
* Tolerant aux composants absents.
|
||||||
* @param {Object|undefined} c — objet composition de l'item menu
|
* @param {Object|undefined} c — objet composition de l'item menu
|
||||||
* @returns {string[]}
|
* @returns {string[]}
|
||||||
*/
|
*/
|
||||||
|
|
@ -93,7 +93,7 @@ function modeLabel() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construit le HTML d'une ligne du panneau. Toute valeur derivee du catalogue est
|
* Construit le HTML d'une ligne du panneau. Toute valeur derivee du catalogue est
|
||||||
* echappee (RG-T15 anti-XSS), comme dans page-cart.js.
|
* echappee (RG-T15 anti-XSS).
|
||||||
* @param {Object} line — element de buildPanelModel().lines
|
* @param {Object} line — element de buildPanelModel().lines
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
|
|
@ -106,18 +106,37 @@ function lineHtml(line) {
|
||||||
return `
|
return `
|
||||||
<li class="order-panel__line">
|
<li class="order-panel__line">
|
||||||
<div class="order-panel__line-main">
|
<div class="order-panel__line-main">
|
||||||
<span class="order-panel__line-name">${line.quantite}× ${escHtml(line.libelle)}</span>
|
<span class="order-panel__line-name">${escHtml(line.libelle)}</span>
|
||||||
<span class="order-panel__line-price">${formatPrice(line.lineCents)}</span>
|
<span class="order-panel__line-price">${formatPrice(line.lineCents)}</span>
|
||||||
</div>
|
</div>
|
||||||
${options}
|
${options}
|
||||||
<button
|
<div class="order-panel__line-controls">
|
||||||
class="order-panel__remove"
|
<div class="order-panel__qty" role="group" aria-label="Quantite de ${escHtml(line.libelle)}">
|
||||||
data-index="${line.index}"
|
<button
|
||||||
type="button"
|
class="order-panel__qty-btn"
|
||||||
aria-label="Retirer ${escHtml(line.libelle)} de la commande"
|
data-action="dec"
|
||||||
>
|
data-index="${line.index}"
|
||||||
<img src="assets/images/ui/trash.png" alt="" aria-hidden="true" width="20" height="20">
|
type="button"
|
||||||
</button>
|
aria-label="Diminuer la quantite de ${escHtml(line.libelle)}"
|
||||||
|
>−</button>
|
||||||
|
<span class="order-panel__qty-value">${line.quantite}</span>
|
||||||
|
<button
|
||||||
|
class="order-panel__qty-btn"
|
||||||
|
data-action="inc"
|
||||||
|
data-index="${line.index}"
|
||||||
|
type="button"
|
||||||
|
aria-label="Augmenter la quantite de ${escHtml(line.libelle)}"
|
||||||
|
>+</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="order-panel__remove"
|
||||||
|
data-index="${line.index}"
|
||||||
|
type="button"
|
||||||
|
aria-label="Retirer ${escHtml(line.libelle)} de la commande"
|
||||||
|
>
|
||||||
|
<img src="assets/images/ui/trash.png" alt="" aria-hidden="true" width="20" height="20">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
@ -165,6 +184,19 @@ export function renderOrderPanel(container) {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Stepper +/- : ajuste la quantite de la ligne. Decrementer a 0 retire la ligne
|
||||||
|
// (updateQuantity supprime quand qty <= 0). Couvre produits ET menus (un menu a
|
||||||
|
// quantite > 1 = N menus identiques, facture par quantite cote serveur).
|
||||||
|
container.querySelectorAll('.order-panel__qty-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const index = parseInt(btn.dataset.index, 10);
|
||||||
|
const cart = getCart();
|
||||||
|
const current = cart[index] ? cart[index].quantite : 0;
|
||||||
|
updateQuantity(index, btn.dataset.action === 'inc' ? current + 1 : current - 1);
|
||||||
|
renderOrderPanel(container);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
container.querySelectorAll('.order-panel__remove').forEach(btn => {
|
container.querySelectorAll('.order-panel__remove').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
removeFromCart(parseInt(btn.dataset.index, 10));
|
removeFromCart(parseInt(btn.dataset.index, 10));
|
||||||
|
|
@ -181,7 +213,7 @@ export function renderOrderPanel(container) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payer desactive sur panier vide : un <a> ignore `disabled`, on bloque le clic
|
// Payer desactive sur panier vide : un <a> ignore `disabled`, on bloque le clic
|
||||||
// via aria-disabled (meme parade que page-cart.js / le fix a11y E2E #45).
|
// via aria-disabled (parade a11y, cf. fix E2E #45).
|
||||||
const pay = container.querySelector('.order-panel__pay');
|
const pay = container.querySelector('.order-panel__pay');
|
||||||
if (pay) {
|
if (pay) {
|
||||||
pay.addEventListener('click', e => {
|
pay.addEventListener('click', e => {
|
||||||
|
|
|
||||||
|
|
@ -1,194 +0,0 @@
|
||||||
/*
|
|
||||||
* page-cart.js — Shopping cart screen.
|
|
||||||
*
|
|
||||||
* Displays all cart lines with quantity controls and totals.
|
|
||||||
* Handles two item shapes:
|
|
||||||
* - Simple product: { id, type, libelle, prix_cents, quantite, image }
|
|
||||||
* - Composed menu: { ...above, composition: {...}, supplement_cents: number }
|
|
||||||
*
|
|
||||||
* Menu lines render a composition breakdown beneath the product name.
|
|
||||||
* Simple product lines render as before (no composition block).
|
|
||||||
*
|
|
||||||
* TVA: 10% (taux normal restauration, France 2024 — simplification MVP).
|
|
||||||
* TODO: verify exact applicable TVA rate with an accountant in P3.
|
|
||||||
* The real rate depends on sur-place vs a-emporter, alcohol content, etc.
|
|
||||||
*
|
|
||||||
* The total displayed is TTC (tax inclusive) because French consumer law
|
|
||||||
* requires prices shown to end-consumers to include all taxes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { getCart, removeFromCart, updateQuantity, getTotalCents, computeMenuLineCents, clearCart, formatPrice, escHtml } from './state.js';
|
|
||||||
import { refreshCartBadge } from './nav.js';
|
|
||||||
|
|
||||||
/* TVA rate used for display breakdown only — stored prices are already TTC */
|
|
||||||
const TVA_RATE = 0.10;
|
|
||||||
|
|
||||||
const cartList = document.getElementById('cart-list');
|
|
||||||
const emptyBlock = document.getElementById('cart-empty');
|
|
||||||
const summaryBlock= document.getElementById('cart-summary');
|
|
||||||
const totalTTC = document.getElementById('total-ttc');
|
|
||||||
const totalHT = document.getElementById('total-ht');
|
|
||||||
const totalTVA = document.getElementById('total-tva');
|
|
||||||
const payBtn = document.getElementById('pay-btn');
|
|
||||||
const abandonBtn = document.getElementById('abandon-btn');
|
|
||||||
|
|
||||||
function renderCart() {
|
|
||||||
const items = getCart();
|
|
||||||
refreshCartBadge();
|
|
||||||
|
|
||||||
if (!items.length) {
|
|
||||||
cartList.innerHTML = '';
|
|
||||||
emptyBlock.hidden = false;
|
|
||||||
summaryBlock.hidden = true;
|
|
||||||
// pay-btn est un <a> : `.disabled` n'existe pas dessus, il faut piloter
|
|
||||||
// aria-disabled (sinon le bouton reste annonce desactive panier rempli).
|
|
||||||
if (payBtn) payBtn.setAttribute('aria-disabled', 'true');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emptyBlock.hidden = true;
|
|
||||||
summaryBlock.hidden = false;
|
|
||||||
if (payBtn) payBtn.setAttribute('aria-disabled', 'false');
|
|
||||||
|
|
||||||
cartList.innerHTML = '';
|
|
||||||
items.forEach((item, index) => {
|
|
||||||
const isMenu = item.type === 'menu';
|
|
||||||
const lineTotalCents = isMenu
|
|
||||||
? computeMenuLineCents(item)
|
|
||||||
: item.prix_cents * item.quantite;
|
|
||||||
|
|
||||||
const row = document.createElement('li');
|
|
||||||
row.className = 'cart-line';
|
|
||||||
row.setAttribute('aria-label', `${item.libelle}, quantite ${item.quantite}`);
|
|
||||||
|
|
||||||
row.innerHTML = `
|
|
||||||
<img
|
|
||||||
class="cart-line__image"
|
|
||||||
src="${escHtml(item.image)}"
|
|
||||||
alt="${escHtml(item.libelle)}"
|
|
||||||
onerror="this.src='assets/images/ui/logo.png'; this.alt='Image non disponible';"
|
|
||||||
>
|
|
||||||
<div class="cart-line__info">
|
|
||||||
<span class="cart-line__name">${escHtml(item.libelle)}</span>
|
|
||||||
<span class="cart-line__unit-price">${formatPrice(item.prix_cents)} / unite${isMenu && (item.supplement_cents ?? 0) > 0 ? ` + ${formatPrice(item.supplement_cents)} suppl.` : ''}</span>
|
|
||||||
${isMenu && item.composition ? renderCompositionBlock(item) : ''}
|
|
||||||
</div>
|
|
||||||
<div class="cart-line__qty" role="group" aria-label="Quantite de ${escHtml(item.libelle)}">
|
|
||||||
<button
|
|
||||||
class="qty-btn qty-btn--minus"
|
|
||||||
data-index="${index}"
|
|
||||||
aria-label="Diminuer la quantite de ${escHtml(item.libelle)}"
|
|
||||||
type="button"
|
|
||||||
>-</button>
|
|
||||||
<span class="qty-value" aria-live="polite">${item.quantite}</span>
|
|
||||||
<button
|
|
||||||
class="qty-btn qty-btn--plus"
|
|
||||||
data-index="${index}"
|
|
||||||
aria-label="Augmenter la quantite de ${escHtml(item.libelle)}"
|
|
||||||
type="button"
|
|
||||||
>+</button>
|
|
||||||
</div>
|
|
||||||
<span class="cart-line__total">${formatPrice(lineTotalCents)}</span>
|
|
||||||
<button
|
|
||||||
class="cart-line__remove"
|
|
||||||
data-index="${index}"
|
|
||||||
aria-label="Supprimer ${escHtml(item.libelle)} du panier"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<img src="assets/images/ui/trash.png" alt="" aria-hidden="true" width="24" height="24">
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
cartList.appendChild(row);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Attach event listeners after render */
|
|
||||||
cartList.querySelectorAll('.qty-btn--minus').forEach(btn => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
const idx = parseInt(btn.dataset.index, 10);
|
|
||||||
const cart = getCart();
|
|
||||||
updateQuantity(idx, cart[idx].quantite - 1);
|
|
||||||
renderCart();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cartList.querySelectorAll('.qty-btn--plus').forEach(btn => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
const idx = parseInt(btn.dataset.index, 10);
|
|
||||||
const cart = getCart();
|
|
||||||
updateQuantity(idx, cart[idx].quantite + 1);
|
|
||||||
renderCart();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cartList.querySelectorAll('.cart-line__remove').forEach(btn => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
const idx = parseInt(btn.dataset.index, 10);
|
|
||||||
removeFromCart(idx);
|
|
||||||
renderCart();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Update totals */
|
|
||||||
const ttcCents = getTotalCents();
|
|
||||||
/* Back-calculate HT from TTC (prices assumed to be TTC already) */
|
|
||||||
const htCents = Math.round(ttcCents / (1 + TVA_RATE));
|
|
||||||
const tvaCents = ttcCents - htCents;
|
|
||||||
|
|
||||||
if (totalTTC) totalTTC.textContent = formatPrice(ttcCents);
|
|
||||||
if (totalHT) totalHT.textContent = formatPrice(htCents);
|
|
||||||
if (totalTVA) totalTVA.textContent = formatPrice(tvaCents);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the composition breakdown HTML for a menu cart line.
|
|
||||||
* Renders burger (with personalisation options), accompagnement, boisson, sauce,
|
|
||||||
* and the supplement summary if applicable. Le format Maxi se lit dans le libelle de
|
|
||||||
* l'accompagnement (variante "Grande ...") et la ligne de supplement, pas un suffixe.
|
|
||||||
*
|
|
||||||
* @param {Object} item — cart item with type === 'menu' and composition object
|
|
||||||
* @returns {string} HTML string
|
|
||||||
*/
|
|
||||||
function renderCompositionBlock(item) {
|
|
||||||
const c = item.composition;
|
|
||||||
if (!c) return '';
|
|
||||||
|
|
||||||
// Tolerant aux champs absents : depuis L2 (composeur slot-driven), un menu peut
|
|
||||||
// ne pas avoir tous les slots (ex. pas de sauce) -> ne pas supposer leur presence.
|
|
||||||
const parts = [];
|
|
||||||
if (c.burger) {
|
|
||||||
const burgerOpts = c.burger.options && c.burger.options.length
|
|
||||||
? ` (${c.burger.options.map(o => o === 'sans-oignon' ? 'sans oignon' : 'avec fromage').join(', ')})`
|
|
||||||
: '';
|
|
||||||
parts.push(`${escHtml(c.burger.libelle)}${burgerOpts}`);
|
|
||||||
}
|
|
||||||
// libelle fait foi : en Maxi l'accompagnement porte deja sa variante par nom
|
|
||||||
// ("Grande Frite"). Plus de suffixe taille -- il doublait le nom ("Grande Frite
|
|
||||||
// grande") et "normale"/"grande" mentait pour la boisson (le Maxi ne l'agrandit pas).
|
|
||||||
if (c.accompagnement) {
|
|
||||||
parts.push(escHtml(c.accompagnement.libelle));
|
|
||||||
}
|
|
||||||
if (c.boisson) {
|
|
||||||
parts.push(escHtml(c.boisson.libelle));
|
|
||||||
}
|
|
||||||
if (c.sauce) {
|
|
||||||
parts.push(escHtml(c.sauce.libelle));
|
|
||||||
}
|
|
||||||
|
|
||||||
const supplTotal = item.supplement_cents ?? 0;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<ul class="cart-line__composition" aria-label="Composition du menu">
|
|
||||||
${parts.map(t => `<li class="cart-line__comp-item">+ ${t}</li>`).join('')}
|
|
||||||
${supplTotal > 0 ? `<li class="cart-line__comp-suppl">Format Maxi : +${formatPrice(supplTotal)}</li>` : ''}
|
|
||||||
</ul>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (abandonBtn) {
|
|
||||||
abandonBtn.addEventListener('click', () => {
|
|
||||||
clearCart();
|
|
||||||
window.location.href = 'categories.html';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', renderCart);
|
|
||||||
|
|
@ -181,7 +181,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
const items = getCart();
|
const items = getCart();
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
window.location.href = 'cart.html';
|
window.location.href = 'categories.html';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (recap) {
|
if (recap) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* page-product-menu.js — Composeur de menu PILOTE PAR LES SLOTS (P5 L2).
|
* page-product-menu.js — Composeur de menu PILOTE PAR LES SLOTS (P5 L2).
|
||||||
*
|
*
|
||||||
* Importe par page-product.js quand le produit charge est un menu (type === 'menu').
|
* Importe par page-products.js quand le produit clique est un menu (type === 'menu').
|
||||||
*
|
*
|
||||||
* Avant L2 : le composeur composait LIBREMENT a partir des categories (burgers,
|
* Avant L2 : le composeur composait LIBREMENT a partir des categories (burgers,
|
||||||
* frites, boissons, sauces) sans tenir compte du menu reel. Desormais il consomme
|
* frites, boissons, sauces) sans tenir compte du menu reel. Desormais il consomme
|
||||||
|
|
@ -12,9 +12,9 @@
|
||||||
* Etapes : Format (Normal/Maxi, burger impose affiche) -> 1 pas par slot (dans
|
* Etapes : Format (Normal/Maxi, burger impose affiche) -> 1 pas par slot (dans
|
||||||
* l'ordre display_order ; requis = choix obligatoire, optionnel = "sans") -> recap.
|
* l'ordre display_order ; requis = choix obligatoire, optionnel = "sans") -> recap.
|
||||||
*
|
*
|
||||||
* La forme de `composition` produite reste compatible avec page-cart.js et
|
* La forme de `composition` produite reste compatible avec order-panel.js (burger /
|
||||||
* order-panel.js (burger / accompagnement / boisson / sauce + taille), le slot_type
|
* accompagnement / boisson / sauce + taille), le slot_type mappant vers le bon champ ;
|
||||||
* mappant vers le bon champ ; Maxi pose taille 'G' + supplement = prix_maxi - prix_normal.
|
* Maxi pose taille 'G' + supplement = prix_maxi - prix_normal.
|
||||||
*
|
*
|
||||||
* A11y : role=dialog, aria-modal, focus-trap, ESC annule, focus au 1er interactif.
|
* A11y : role=dialog, aria-modal, focus-trap, ESC annule, focus au 1er interactif.
|
||||||
*/
|
*/
|
||||||
|
|
@ -155,7 +155,7 @@ export function composerIsViable(model) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Entree publique — appelee par page-product.js */
|
/* Entree publique — appelee par page-products.js */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
/*
|
|
||||||
* page-product.js — Product detail screen.
|
|
||||||
*
|
|
||||||
* Reads ?id=<int>&category=<slug> from the query string.
|
|
||||||
*
|
|
||||||
* Branch on product type:
|
|
||||||
* - type === 'menu' → open the multi-step composer modal (page-product-menu.js).
|
|
||||||
* The standard detail layout is bypassed because a menu
|
|
||||||
* cannot be added to the cart without composition choices.
|
|
||||||
* - type === 'produit' → render the standard detail card with "Ajouter au panier".
|
|
||||||
*
|
|
||||||
* After "Ajouter au panier" (simple product):
|
|
||||||
* 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, loadAllergens } from './data.js';
|
|
||||||
import { addToCart, formatPrice, escHtml } from './state.js';
|
|
||||||
import { refreshCartBadge } from './nav.js';
|
|
||||||
import { openMenuComposer } from './page-product-menu.js';
|
|
||||||
import { openProductOptions, productSizes } from './product-options.js';
|
|
||||||
import { buildAllergenInfoButton, openAllergenModal } from './allergens.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, categorySlug);
|
|
||||||
if (!product) {
|
|
||||||
showError('Ce produit n\'existe pas.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.title = `Wakdo - ${product.nom}`;
|
|
||||||
|
|
||||||
if (product.type === 'menu') {
|
|
||||||
/* Hide the standard product detail area; the composer will overlay the page.
|
|
||||||
* The container stays in the DOM so the skeleton does not flash. */
|
|
||||||
container.hidden = true;
|
|
||||||
await openMenuComposer(product, categorySlug);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Produit a tailles multiples (R4, ex. boisson 30/50 cl) : on delegue a la
|
|
||||||
* modale d'options (meme picker que la grille) plutot que de dupliquer la
|
|
||||||
* selection de taille dans la fiche -> un seul chemin pour choisir la taille. */
|
|
||||||
if (productSizes(product).length) {
|
|
||||||
container.hidden = true;
|
|
||||||
openProductOptions(product, categorySlug);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="product-detail__image-wrap">
|
|
||||||
<img
|
|
||||||
class="product-detail__image"
|
|
||||||
src="${escHtml(product.image)}"
|
|
||||||
alt="${escHtml(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">${escHtml(product.nom)}</h1>
|
|
||||||
<p class="product-detail__price">${formatPrice(product.prix)}</p>
|
|
||||||
<button
|
|
||||||
class="btn btn--primary btn--large product-detail__add"
|
|
||||||
id="add-to-cart-btn"
|
|
||||||
aria-label="Ajouter ${escHtml(product.nom)} au panier"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Ajouter au panier
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Bouton "i" allergenes (modale generale) dans le bloc info de la fiche.
|
|
||||||
// Echec de chargement non bloquant : la fiche reste fonctionnelle.
|
|
||||||
try {
|
|
||||||
const allergens = await loadAllergens();
|
|
||||||
const info = container.querySelector('.product-detail__info');
|
|
||||||
if (info) {
|
|
||||||
info.appendChild(buildAllergenInfoButton(() => openAllergenModal(allergens)));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('loadAllergens error:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(msg) {
|
|
||||||
if (errorBlock) {
|
|
||||||
errorBlock.hidden = false;
|
|
||||||
errorBlock.textContent = msg;
|
|
||||||
}
|
|
||||||
if (container) {
|
|
||||||
container.hidden = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', renderProduct);
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
*
|
*
|
||||||
* Reads ?category=<id> from the query string, maps to a slug via
|
* Reads ?category=<id> from the query string, maps to a slug via
|
||||||
* CATEGORY_ID_TO_SLUG, then fetches the matching product array.
|
* CATEGORY_ID_TO_SLUG, then fetches the matching product array.
|
||||||
* On product card click, navigates to product.html?id=<id>&category=<slug>.
|
* On product card click, opens an in-page modal (composer for a menu, options
|
||||||
|
* for a simple product) above the grid ; the order panel reflects the addition.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getProductsByCategory, getCategoryById, CATEGORY_ID_TO_SLUG, loadAllergens } from './data.js';
|
import { getProductsByCategory, getCategoryById, CATEGORY_ID_TO_SLUG, loadAllergens } from './data.js';
|
||||||
|
|
@ -68,7 +69,9 @@ async function renderProducts() {
|
||||||
const orderable = product.commandable !== false;
|
const orderable = product.commandable !== false;
|
||||||
const card = document.createElement('a');
|
const card = document.createElement('a');
|
||||||
card.className = orderable ? 'product-card' : 'product-card product-card--unavailable';
|
card.className = orderable ? 'product-card' : 'product-card product-card--unavailable';
|
||||||
card.href = `product.html?id=${product.id}&category=${categorySlug}`;
|
// Le <a> reste pour le focus/clavier (a11y) ; href='#' inerte, le handler
|
||||||
|
// click ci-dessous fait foi (preventDefault + ouverture de la modale).
|
||||||
|
card.href = '#';
|
||||||
card.setAttribute('aria-label', `${product.nom} - ${formatPrice(product.prix)}${orderable ? '' : ' - indisponible'}`);
|
card.setAttribute('aria-label', `${product.nom} - ${formatPrice(product.prix)}${orderable ? '' : ' - indisponible'}`);
|
||||||
if (!orderable) card.setAttribute('aria-disabled', 'true');
|
if (!orderable) card.setAttribute('aria-disabled', 'true');
|
||||||
|
|
||||||
|
|
@ -94,10 +97,10 @@ async function renderProducts() {
|
||||||
const infoBtn = buildAllergenInfoButton(() => openAllergenModal(allergens));
|
const infoBtn = buildAllergenInfoButton(() => openAllergenModal(allergens));
|
||||||
card.querySelector('.product-card__image-wrap').appendChild(infoBtn);
|
card.querySelector('.product-card__image-wrap').appendChild(infoBtn);
|
||||||
|
|
||||||
// Clic produit -> modale au-dessus de la grille (paradigme maquette) au lieu
|
// Clic produit -> modale au-dessus de la grille (paradigme maquette) :
|
||||||
// de naviguer vers product.html : menu -> composeur (L2), produit -> options
|
// menu -> composeur (L2), produit -> options (L3). Le panneau de droite est
|
||||||
// (L3). Le <a href> reste un repli (lien direct / sans JS). Une tuile en
|
// l'unique vue panier ; pas de navigation au clic. Une tuile en rupture ne
|
||||||
// rupture ne fait rien (ni navigation ni modale).
|
// fait rien (ni navigation ni modale).
|
||||||
card.addEventListener('click', (e) => {
|
card.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!orderable) return;
|
if (!orderable) return;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
/*
|
/*
|
||||||
* product-options.js — Modale d'options produit (P5 L3, taille R4).
|
* product-options.js — Modale d'options produit (P5 L3, taille R4).
|
||||||
*
|
*
|
||||||
* Remplace la navigation vers product.html : cliquer un produit simple ouvre une
|
* Ouvre une modale d'options au clic produit, au lieu d'une navigation : cliquer un
|
||||||
* modale (image, prix unitaire, stepper de quantite, total) au-dessus de la grille,
|
* produit simple ouvre une modale (image, prix unitaire, stepper de quantite, total)
|
||||||
* facon maquette ("Une petite soif ?"). A l'ajout, le panneau de commande persistant
|
* au-dessus de la grille, facon maquette ("Une petite soif ?"). A l'ajout, le panneau
|
||||||
* (L1) est re-rendu pour refleter immediatement la commande -> pas de navigation.
|
* de commande persistant (L1) est re-rendu pour refleter immediatement la commande.
|
||||||
*
|
*
|
||||||
* Taille (R4) : la dimension 30/50 cl de la maquette existe desormais en base sous
|
* Taille (R4) : la dimension 30/50 cl de la maquette existe desormais en base sous
|
||||||
* forme de LIGNES produit distinctes (product.sizes : [{product_id, size_cl,
|
* forme de LIGNES produit distinctes (product.sizes : [{product_id, size_cl,
|
||||||
|
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
<!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 - Votre panier">
|
|
||||||
<title>Wakdo - Panier</title>
|
|
||||||
<link rel="canonical" href="https://corentin-wakdo.stark.a3n.fr/cart.html">
|
|
||||||
<link rel="icon" type="image/svg+xml" href="assets/images/favicon.svg">
|
|
||||||
<link rel="stylesheet" href="assets/css/style.css">
|
|
||||||
</head>
|
|
||||||
<body class="cart-page">
|
|
||||||
|
|
||||||
<!--
|
|
||||||
cart.html — Shopping cart.
|
|
||||||
page-cart.js renders all cart lines, handles qty controls and removal.
|
|
||||||
|
|
||||||
TVA: 10% (restauration France 2024 — simplified rate).
|
|
||||||
TODO: verify exact rate with accountant in P3 — actual rate depends
|
|
||||||
on sur-place vs a-emporter and product type (alcohol, etc.).
|
|
||||||
|
|
||||||
The stored prices are TTC. HT is back-calculated at display time only.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<header class="site-header">
|
|
||||||
<a
|
|
||||||
class="site-header__back"
|
|
||||||
href="categories.html"
|
|
||||||
aria-label="Continuer mes achats"
|
|
||||||
>
|
|
||||||
← Continuer
|
|
||||||
</a>
|
|
||||||
<img
|
|
||||||
class="site-header__logo"
|
|
||||||
src="assets/images/ui/logo.png"
|
|
||||||
alt="Wakdo"
|
|
||||||
>
|
|
||||||
<span class="mode-badge site-header__mode" data-mode-badge aria-label="Mode de consommation">Sur place</span>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="cart-main" aria-label="Votre panier">
|
|
||||||
|
|
||||||
<h1 class="cart-main__heading">Votre panier</h1>
|
|
||||||
|
|
||||||
<!-- Empty cart state -->
|
|
||||||
<div id="cart-empty" class="cart-empty" hidden>
|
|
||||||
<p class="cart-empty__message">Votre panier est vide.</p>
|
|
||||||
<a class="btn btn--secondary" href="categories.html">Decouvrir nos produits</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Cart lines -->
|
|
||||||
<ul id="cart-list" class="cart-list" aria-label="Lignes du panier">
|
|
||||||
<!-- Filled by page-cart.js -->
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<!-- Order summary -->
|
|
||||||
<aside id="cart-summary" class="cart-summary" hidden aria-label="Recapitulatif de commande">
|
|
||||||
<div class="cart-summary__line">
|
|
||||||
<span>Total HT</span>
|
|
||||||
<span id="total-ht">—</span>
|
|
||||||
</div>
|
|
||||||
<div class="cart-summary__line">
|
|
||||||
<!-- TVA 10% — taux restauration FR 2024 (simplifie, voir commentaire ci-dessus) -->
|
|
||||||
<span>TVA (10%)</span>
|
|
||||||
<span id="total-tva">—</span>
|
|
||||||
</div>
|
|
||||||
<div class="cart-summary__line cart-summary__line--total">
|
|
||||||
<span>Total TTC</span>
|
|
||||||
<strong id="total-ttc">—</strong>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- Actions -->
|
|
||||||
<div class="cart-actions">
|
|
||||||
<button
|
|
||||||
id="abandon-btn"
|
|
||||||
class="btn btn--secondary"
|
|
||||||
type="button"
|
|
||||||
aria-label="Abandonner la commande et retourner aux categories"
|
|
||||||
>
|
|
||||||
Abandonner
|
|
||||||
</button>
|
|
||||||
<a
|
|
||||||
id="pay-btn"
|
|
||||||
class="btn btn--primary"
|
|
||||||
href="payment.html"
|
|
||||||
role="button"
|
|
||||||
aria-label="Passer au paiement"
|
|
||||||
aria-disabled="true"
|
|
||||||
>
|
|
||||||
Valider ma commande
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script type="module" src="assets/js/nav.js"></script>
|
|
||||||
<script type="module" src="assets/js/page-cart.js"></script>
|
|
||||||
<script type="module" src="assets/js/a11y.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -21,10 +21,10 @@
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<a
|
<a
|
||||||
class="site-header__back"
|
class="site-header__back"
|
||||||
href="cart.html"
|
href="categories.html"
|
||||||
aria-label="Retour au panier"
|
aria-label="Retour aux categories"
|
||||||
>
|
>
|
||||||
← Panier
|
← Retour
|
||||||
</a>
|
</a>
|
||||||
<img
|
<img
|
||||||
class="site-header__logo"
|
class="site-header__logo"
|
||||||
|
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
<!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="canonical" href="https://corentin-wakdo.stark.a3n.fr/product.html">
|
|
||||||
<link rel="icon" type="image/svg+xml" href="assets/images/favicon.svg">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="order-layout">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<aside class="order-panel" data-order-panel aria-label="Ma commande" aria-live="polite"></aside>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="module" src="assets/js/nav.js"></script>
|
|
||||||
<script type="module" src="assets/js/page-product.js"></script>
|
|
||||||
<script type="module" src="assets/js/order-panel.js"></script>
|
|
||||||
<script type="module" src="assets/js/a11y.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -33,15 +33,6 @@
|
||||||
src="assets/images/ui/logo.png"
|
src="assets/images/ui/logo.png"
|
||||||
alt="Wakdo"
|
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>
|
</header>
|
||||||
|
|
||||||
<div class="order-layout">
|
<div class="order-layout">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// Parcours E2E borne : welcome -> categories -> produit -> ajout panier -> panier
|
// Parcours E2E borne : welcome -> categories -> produits (grille) -> modale d'options
|
||||||
// -> paiement -> confirmation. La stack est montee a part (run.sh) ; le panier vit
|
// au clic produit -> ajout -> le panneau de commande persistant (unique vue panier)
|
||||||
// dans localStorage (meme origine), donc on peut naviguer par goto sans perdre l'etat.
|
// reflete la ligne -> paiement -> confirmation. La stack est montee a part (run.sh) ;
|
||||||
|
// le panier vit dans localStorage (meme origine), on navigue sans perdre l'etat.
|
||||||
|
// Il n'existe plus de page panier ni de page produit separees (lot F3 panier unique).
|
||||||
const { test, expect } = require('@playwright/test');
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
test('parcours borne : de l\'accueil a la confirmation de commande', async ({ page }) => {
|
test('parcours borne : de l\'accueil a la confirmation de commande', async ({ page }) => {
|
||||||
|
|
@ -16,40 +18,46 @@ test('parcours borne : de l\'accueil a la confirmation de commande', async ({ pa
|
||||||
|
|
||||||
await test.step('categories -> produits', async () => {
|
await test.step('categories -> produits', async () => {
|
||||||
await expect(page.locator('h1.categories-main__heading')).toBeVisible();
|
await expect(page.locator('h1.categories-main__heading')).toBeVisible();
|
||||||
// Categorie 2 = boissons : produits SIMPLES (la categorie 1 = menus, qui rendent
|
// Categorie 2 = boissons : produits SIMPLES (la categorie 1 = menus, qui ouvrent
|
||||||
// un autre gabarit a slots, sans bouton d'ajout direct).
|
// le composeur a slots, un autre parcours). Un produit simple ouvre la modale options.
|
||||||
await page.locator('a[href="products.html?category=2"]').click();
|
await page.locator('a[href="products.html?category=2"]').click();
|
||||||
await expect(page).toHaveURL(/products\.html\?category=2/);
|
await expect(page).toHaveURL(/products\.html\?category=2/);
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('produits -> fiche produit', async () => {
|
await test.step('clic produit -> modale options', async () => {
|
||||||
// Cartes rendues par JS depuis le JSON : auto-wait sur la 1re carte.
|
// Cartes rendues par JS depuis le JSON : auto-wait sur la 1re carte commandable.
|
||||||
const firstCard = page.locator('#products-grid a.product-card').first();
|
const firstCard = page.locator('#products-grid a.product-card:not(.product-card--unavailable)').first();
|
||||||
await expect(firstCard).toBeVisible();
|
await expect(firstCard).toBeVisible();
|
||||||
await firstCard.click();
|
await firstCard.click();
|
||||||
await expect(page).toHaveURL(/product\.html\?id=/);
|
// product-options.js monte une modale (.composer-overlay) avec le bouton d'ajout #po-add.
|
||||||
|
await expect(page.locator('.composer-overlay [role="dialog"]')).toBeVisible();
|
||||||
|
await expect(page.locator('#po-add')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('ajout au panier', async () => {
|
await test.step('ajout -> le panneau de commande reflete la ligne', async () => {
|
||||||
const addBtn = page.locator('#add-to-cart-btn');
|
await page.locator('#po-add').click();
|
||||||
await expect(addBtn).toBeVisible();
|
// La modale se ferme et le panneau persistant (unique vue panier) montre la ligne.
|
||||||
await addBtn.click();
|
await expect(page.locator('.composer-overlay')).toHaveCount(0);
|
||||||
// Feedback visuel "Ajoute !" (page-product.js) ; l'ecriture localStorage est synchrone.
|
const panel = page.locator('[data-order-panel]');
|
||||||
await expect(addBtn).toHaveText(/Ajoute/);
|
await expect(panel.locator('.order-panel__line')).toHaveCount(1);
|
||||||
|
// Total calcule (le panneau affiche un montant, plus le placeholder vide).
|
||||||
|
await expect(panel.locator('.order-panel__total-value')).not.toHaveText('');
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('panier : recapitulatif', async () => {
|
await test.step('panneau Payer -> paiement', async () => {
|
||||||
await page.goto('/cart.html');
|
const payLink = page.locator('[data-order-panel] .order-panel__pay');
|
||||||
await expect(page.locator('#cart-summary')).toBeVisible();
|
await expect(payLink).toHaveAttribute('aria-disabled', 'false');
|
||||||
await expect(page.locator('#cart-list li')).toHaveCount(1);
|
await payLink.click();
|
||||||
// Total calcule, plus le placeholder "—".
|
|
||||||
await expect(page.locator('#total-ttc')).not.toHaveText('—');
|
|
||||||
await page.locator('#pay-btn').click();
|
|
||||||
await expect(page).toHaveURL(/payment\.html/);
|
await expect(page).toHaveURL(/payment\.html/);
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('paiement -> confirmation', async () => {
|
await test.step('paiement -> confirmation', async () => {
|
||||||
await page.locator('#pay-card').click();
|
await page.locator('#pay-card').click();
|
||||||
|
// Sur-place : page-payment.js ouvre la modale chevalet (numero de table) avant de
|
||||||
|
// soumettre. On saisit un numero puis on enregistre pour declencher le checkout.
|
||||||
|
await expect(page.locator('#chevalet-input')).toBeVisible();
|
||||||
|
await page.locator('#chevalet-input').fill('12');
|
||||||
|
await page.locator('#chevalet-ok').click();
|
||||||
await expect(page).toHaveURL(/confirmation\.html/);
|
await expect(page).toHaveURL(/confirmation\.html/);
|
||||||
await expect(page.locator('.confirmation-banner__title')).toHaveText(/Commande confirmee/);
|
await expect(page.locator('.confirmation-banner__title')).toHaveText(/Commande confirmee/);
|
||||||
// Numero de commande genere (plus le placeholder).
|
// Numero de commande genere (plus le placeholder).
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { JSDOM } from 'jsdom';
|
||||||
let buildComposerSteps, buildMenuCartItem, selectionsComplete, composerIsViable, optionLabel;
|
let buildComposerSteps, buildMenuCartItem, selectionsComplete, composerIsViable, optionLabel;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', { url: 'https://kiosk.test/product.html' });
|
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', { url: 'https://kiosk.test/products.html' });
|
||||||
global.window = dom.window;
|
global.window = dom.window;
|
||||||
global.document = dom.window.document;
|
global.document = dom.window.document;
|
||||||
global.localStorage = dom.window.localStorage;
|
global.localStorage = dom.window.localStorage;
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ test('modeLabel: libelle humain ; vide si mode absent ou inconnu (ne ment pas)',
|
||||||
test('needsModeRedirect: page profonde SANS mode valide -> redirige', () => {
|
test('needsModeRedirect: page profonde SANS mode valide -> redirige', () => {
|
||||||
assert.equal(needsModeRedirect('/payment.html', null), true);
|
assert.equal(needsModeRedirect('/payment.html', null), true);
|
||||||
assert.equal(needsModeRedirect('/products.html', undefined), true);
|
assert.equal(needsModeRedirect('/products.html', undefined), true);
|
||||||
assert.equal(needsModeRedirect('/cart.html', 'bidon'), true);
|
assert.equal(needsModeRedirect('/payment.html', 'bidon'), true);
|
||||||
assert.equal(needsModeRedirect('/categories.html', ''), true);
|
assert.equal(needsModeRedirect('/categories.html', ''), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,24 @@ test('renderOrderPanel: clic corbeille retire la ligne et re-rend', () => {
|
||||||
assert.equal(JSON.parse(localStorage.getItem('wakdo_cart')).length, 1);
|
assert.equal(JSON.parse(localStorage.getItem('wakdo_cart')).length, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('renderOrderPanel: stepper + augmente la quantite et re-rend', () => {
|
||||||
|
localStorage.setItem('wakdo_cart', JSON.stringify([simple({ quantite: 1 })]));
|
||||||
|
const el = document.createElement('aside');
|
||||||
|
renderOrderPanel(el);
|
||||||
|
el.querySelector('.order-panel__qty-btn[data-action="inc"]').click();
|
||||||
|
assert.equal(JSON.parse(localStorage.getItem('wakdo_cart'))[0].quantite, 2);
|
||||||
|
assert.equal(el.querySelector('.order-panel__qty-value').textContent, '2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renderOrderPanel: stepper - a quantite 1 retire la ligne', () => {
|
||||||
|
localStorage.setItem('wakdo_cart', JSON.stringify([simple({ quantite: 1 }), menu()]));
|
||||||
|
const el = document.createElement('aside');
|
||||||
|
renderOrderPanel(el);
|
||||||
|
el.querySelector('.order-panel__qty-btn[data-action="dec"]').click(); // 1re ligne 1 -> 0 -> retiree
|
||||||
|
assert.equal(el.querySelectorAll('.order-panel__line').length, 1);
|
||||||
|
assert.equal(JSON.parse(localStorage.getItem('wakdo_cart')).length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
test('renderOrderPanel: libelle de ligne echappe (anti-XSS RG-T15)', () => {
|
test('renderOrderPanel: libelle de ligne echappe (anti-XSS RG-T15)', () => {
|
||||||
localStorage.setItem('wakdo_cart', JSON.stringify([simple({ libelle: '<img src=x onerror=alert(1)>' })]));
|
localStorage.setItem('wakdo_cart', JSON.stringify([simple({ libelle: '<img src=x onerror=alert(1)>' })]));
|
||||||
const el = document.createElement('aside');
|
const el = document.createElement('aside');
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue