◆ Portail Antalaha

Annuaire › Code ouvert

Calculateur de prix de vanille (Alpine.js)

tutorial · Voir le site ↗
Connectez-vous pour copier ce site

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>