Annuaire › Code ouvert
Code publié par son auteur, en lecture seule. « Copier dans mon espace » crée un nouveau site à vous (non publié) que vous pouvez modifier librement.
index.html · 9.5 Ko
<!DOCTYPE html>
<!--
==========================================================================
index.html — Calculateur de prix de vanille (tutoriel Alpine.js, DÉBUTANT)
--------------------------------------------------------------------------
Cas d'usage : un vendeur de vanille d'Antalaha veut donner un prix à ses
clients : qualité + poids + remises + livraison = total en Ariary.
Alpine.js en une phrase : on ajoute des attributs spéciaux (x-data,
x-model, x-text…) aux balises HTML, et la page devient interactive sans
écrire de JavaScript de manipulation du DOM.
==========================================================================
-->
<html lang="fr">
<head>
<meta charset="utf-8">
<!-- Viewport : indispensable pour un affichage correct sur téléphone -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Prix de la vanille — Antalaha</title>
<!-- Chemin RELATIF (sans « / » au début) : le site est servi dans un
sous-dossier du portail, un chemin absolu casserait le lien. -->
<link rel="stylesheet" href="css/style.css">
<!--
ORDRE DES SCRIPTS (important) :
1) notre fichier, qui définit la fonction calculateur() ;
2) Alpine, avec « defer » : il attend que le HTML soit entièrement lu,
puis parcourt la page pour activer les attributs x-…
Le chemin /libs/alpine.min.js est fourni par le portail (pas de CDN :
le site tourne dans un bac à sable sans accès au réseau externe).
-->
<script src="js/calculateur.js" defer></script>
<script src="/libs/alpine.min.js" defer></script>
</head>
<body>
<header class="entete">
<h1>Combien coûte ma vanille ?</h1>
<p>Choisissez la qualité et le poids : le prix se met à jour tout seul, en Ariary.</p>
</header>
<!--
x-data="calculateur()" : DÉCLARE un « composant ». Tout ce qui est à
l'intérieur de cette balise <main> peut lire/modifier les données
retournées par calculateur() (voir js/calculateur.js).
Sans x-data sur un parent, aucun autre attribut x-… ne fonctionne.
-->
<main x-data="calculateur()">
<!-- ===================== COLONNE 1 : LE FORMULAIRE ===================== -->
<section class="carte" aria-labelledby="titre-form">
<h2 id="titre-form">1. Votre commande</h2>
<div class="champ">
<label for="qualite">Qualité de la vanille</label>
<!--
x-model="qualite" : liaison BIDIRECTIONNELLE. Quand on change la
liste, la propriété « qualite » change ; si « qualite » change
(ex. : bouton Réinitialiser), la liste se met à jour.
-->
<select id="qualite" x-model="qualite">
<!--
x-for : répète la balise <template> pour chaque élément.
Object.keys(qualites) donne la liste des clés de l'objet
(« gourmet », « standard », « fendue ») : « cle » prend chaque
valeur à tour de rôle, et qualites[cle] retrouve les détails.
:key est obligatoire pour que le navigateur reconnaisse chaque ligne.
x-for doit être sur un <template> qui a UN SEUL élément enfant.
-->
<template x-for="cle in Object.keys(qualites)" :key="cle">
<!-- :value (= x-bind:value) : met la clé dans l'attribut value.
:selected : PIÈGE — Alpine applique x-model sur le <select> AVANT
que x-for ait créé les <option>. Sans :selected, la valeur de
départ (« standard ») ne serait pas sélectionnée à l'écran. -->
<option :value="cle" :selected="cle === qualite" x-text="qualites[cle].nom + ' — ' + ariary(qualites[cle].prixGramme) + '/g'"></option>
</template>
</select>
</div>
<div class="champ">
<label for="poids">Poids souhaité (en grammes)</label>
<div class="rangee">
<!-- @click (= x-on:click) : exécute une méthode au clic -->
<button type="button" class="secondaire" @click="ajuster(-50)" aria-label="Retirer 50 grammes">−50</button>
<!--
x-model.number : le modificateur « .number » convertit le texte
saisi en vrai nombre JavaScript. Sans lui, poids vaudrait "250"
(du texte) et "250" + 50 donnerait "25050" !
Si le champ est vidé, la valeur devient '' : c'est pourquoi le
getter poidsValide existe dans calculateur.js.
:class ajoute la classe « invalide » (bordure rouge) si erreur.
:aria-invalid informe les lecteurs d'écran.
-->
<input id="poids" type="number" min="0" step="10" inputmode="numeric"
x-model.number="poids"
:class="{ 'invalide': erreurPoids !== '' }"
:aria-invalid="erreurPoids !== ''"
aria-describedby="aide-poids">
<button type="button" class="secondaire" @click="ajuster(50)" aria-label="Ajouter 50 grammes">+50</button>
</div>
<!-- x-show : affiche l'élément seulement si l'expression est vraie
(il reste dans la page mais prend « display: none »). -->
<p class="erreur" role="alert" x-show="erreurPoids !== ''" x-text="erreurPoids" x-cloak></p>
<p class="aide" id="aide-poids">Environ 8 à 10 gousses pour 50 g. Remise volume dès 250 g.</p>
</div>
<div class="champ">
<label for="zone">Zone de livraison</label>
<select id="zone" x-model="zone">
<template x-for="cle in Object.keys(zones)" :key="cle">
<option :value="cle" :selected="cle === zone" x-text="zones[cle].nom"></option>
</template>
</select>
</div>
<!-- Case à cocher : x-model donne true/false, pas du texte -->
<div class="case">
<input id="fidele" type="checkbox" x-model="clientFidele">
<label for="fidele">Je suis un client fidèle (−5 %)</label>
</div>
<div class="case">
<input id="cadeau" type="checkbox" x-model="cadeau">
<label for="cadeau">Emballage cadeau (+3 000 Ar)</label>
</div>
<button type="button" class="secondaire" @click="reinitialiser()">Tout remettre à zéro</button>
</section>
<!-- ===================== COLONNE 2 : LE RÉSULTAT ===================== -->
<section class="carte" aria-labelledby="titre-res">
<h2 id="titre-res">2. Votre prix</h2>
<!-- aria-live : les lecteurs d'écran annoncent les changements -->
<div aria-live="polite">
<!--
x-show avec une comparaison : le bandeau n'apparaît que s'il y a
une remise. « x-text » remplace le contenu texte de la balise par
le résultat de l'expression JavaScript.
-->
<p class="bandeau" x-show="remisePourcent > 0" x-cloak>
Bonne nouvelle : <strong x-text="remisePourcent + ' %'"></strong> de remise appliquée !
<span x-show="remiseVolume > 0" x-text="'(dont ' + remiseVolume + ' % pour la quantité)'"></span>
</p>
<dl>
<div class="ligne">
<dt>Prix au gramme</dt>
<!-- On appelle la méthode ariary() pour mettre en forme -->
<dd x-text="ariary(prixGramme)"></dd>
</div>
<div class="ligne">
<dt>Sous-total (<span x-text="poidsValide"></span> g)</dt>
<dd x-text="ariary(sousTotal)"></dd>
</div>
<div class="ligne remise" x-show="remiseMontant > 0" x-cloak>
<dt>Remise</dt>
<dd x-text="'− ' + ariary(remiseMontant)"></dd>
</div>
<div class="ligne">
<dt>Livraison</dt>
<!-- Opérateur ternaire : condition ? si vrai : si faux -->
<dd x-text="livraison === 0 ? 'Gratuite' : ariary(livraison)"></dd>
</div>
<div class="ligne" x-show="cadeau" x-cloak>
<dt>Emballage cadeau</dt>
<dd x-text="ariary(emballage)"></dd>
</div>
<div class="ligne">
<dt>Délai estimé</dt>
<dd x-text="delai"></dd>
</div>
</dl>
<div class="total">
<span>Total à payer</span>
<strong x-text="ariary(total)"></strong>
</div>
</div>
<!--
Ouverture/fermeture d'un bloc avec un simple booléen :
@click inverse la valeur (! = « non »), x-show réagit.
:aria-expanded reflète l'état pour l'accessibilité.
-->
<p>
<button type="button" class="secondaire" @click="detailOuvert = !detailOuvert"
:aria-expanded="detailOuvert" aria-controls="detail">
<!-- Le texte du bouton dépend de l'état -->
<span x-text="detailOuvert ? 'Masquer le détail du calcul' : 'Voir le détail du calcul'"></span>
</button>
</p>
<div id="detail" x-show="detailOuvert" x-cloak>
<p class="aide">
Total = (prix au gramme × poids) − remise + livraison + emballage.<br>
Livraison = frais de base + supplément par tranche de 100 g entamée.
</p>
<p class="aide" x-text="ariary(sousTotal) + ' − ' + ariary(remiseMontant) + ' + ' + ariary(livraison) + ' + ' + ariary(emballage) + ' = ' + ariary(total)"></p>
</div>
</section>
</main>
<footer>
<!-- Icône en ligne (SVG) : elle hérite de la couleur du texte grâce à currentColor -->
<svg class="icone" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 3c8 2 12 8 14 18M7 3c6 3 9 8 10 15M4 5l3-2M17 21l3-2"/></svg>
Prix fictifs, donnés à titre d'exemple pour apprendre Alpine.js.
</footer>
</body>
</html>