◆ Portail Antalaha

Annuaire › Code ouvert

Convertisseur Ariary, Euro, Dollar

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.

js/app.js · 14.3 Ko

/* ============================================================
   Fichier : js/app.js
   Rôle    : tout le comportement du convertisseur de monnaie.
   Dépend de : index.html (les id utilisés dans getElementById
               doivent exister dans la page).
   Chargé par : <script src="js/app.js"> en bas de index.html.
   ------------------------------------------------------------
   Plan du fichier :
     1. Données (taux de change)
     2. Stockage sûr (localStorage avec repli en mémoire)
     3. Outils : lire un nombre, formater un nombre
     4. Calcul de la conversion
     5. Affichage (lecture/écriture du DOM)
     6. Événements (clic, envoi de formulaire…)
     7. Démarrage
   ============================================================ */

"use strict"; // Mode strict : JavaScript signale davantage d'erreurs (fautes de frappe, variables oubliées)

/* ============================================================
   1. DONNÉES
   ============================================================ */

/**
 * Taux d'exemple : combien d'Ariary vaut UNE unité de chaque devise.
 * Ce sont des valeurs d'illustration, PAS le taux du jour : le site
 * n'a pas Internet, donc c'est l'utilisateur qui les règle.
 * On utilise l'Ariary comme « devise pivot » : toute conversion passe
 * par l'Ariary (Euro → Ariary → Dollar). Ainsi, 2 taux suffisent
 * au lieu de 6 couples de devises.
 */
const TAUX_PAR_DEFAUT = {
  MGA: 1,      // 1 Ariary vaut 1 Ariary (le pivot)
  EUR: 5000,   // 1 Euro   ≈ 5 000 Ar (exemple)
  USD: 4500    // 1 Dollar ≈ 4 500 Ar (exemple)
};

/** Les taux actuellement utilisés (copie modifiable des taux par défaut). */
let taux = { ...TAUX_PAR_DEFAUT }; // « ... » (opérateur de décomposition) copie l'objet

/** Montants proposés dans le tableau « Repères rapides » (en Ariary). */
const REPERES_ARIARY = [1000, 5000, 10000, 20000, 50000, 100000];

/** Clé sous laquelle on range les taux dans le stockage du navigateur. */
const CLE_STOCKAGE = "convertisseur-taux";

/* ============================================================
   2. STOCKAGE SÛR
   ============================================================ */

/**
 * Mémoire de secours, utilisée quand localStorage est interdit.
 * PIÈGE : sur le Portail, les sites s'exécutent dans un « bac à sable » :
 * le simple fait d'écrire « localStorage » lève une SecurityError.
 * Il FAUT donc toujours entourer l'accès d'un try/catch.
 * Dans ce cas, les taux restent en mémoire tant que la page est ouverte,
 * mais sont oubliés au rechargement.
 */
let memoireDeSecours = null;

/**
 * Enregistre les taux (dans localStorage si possible, sinon en mémoire).
 * @param {{MGA:number, EUR:number, USD:number}} valeurs - taux à garder
 * @returns {void}
 */
function sauvegarderTaux(valeurs) {
  memoireDeSecours = JSON.stringify(valeurs); // JSON.stringify : objet -> texte
  try {
    window.localStorage.setItem(CLE_STOCKAGE, memoireDeSecours);
  } catch (erreur) {
    // Stockage indisponible : ce n'est pas grave, la mémoire de secours suffit.
  }
}

/**
 * Relit les taux sauvegardés.
 * @returns {{MGA:number, EUR:number, USD:number}|null} les taux, ou null s'il n'y en a pas
 */
function chargerTaux() {
  let texte = memoireDeSecours;
  try {
    const lu = window.localStorage.getItem(CLE_STOCKAGE);
    if (lu) { texte = lu; }
  } catch (erreur) {
    // Stockage indisponible : on garde la mémoire de secours.
  }
  if (!texte) { return null; }
  try {
    const objet = JSON.parse(texte); // JSON.parse : texte -> objet (peut échouer si le texte est abîmé)
    if (objet && objet.EUR > 0 && objet.USD > 0) {
      return { MGA: 1, EUR: Number(objet.EUR), USD: Number(objet.USD) };
    }
  } catch (erreur) {
    // Texte abîmé : on l'ignore.
  }
  return null;
}

/* ============================================================
   3. OUTILS : LIRE ET FORMATER UN NOMBRE
   ============================================================ */

/**
 * Transforme le texte tapé par l'utilisateur en nombre.
 * Accepte « 25000 », « 25 000 », « 12,5 » et « 12.5 ».
 * Pourquoi ne pas utiliser <input type="number"> ? Parce qu'en français on
 * tape une virgule, et ce type de champ se comporte différemment selon les
 * navigateurs. Ici, on contrôle tout.
 * @param {string} texte - le texte saisi
 * @returns {number|null} le nombre, ou null si le texte n'est pas un nombre valide
 */
function lireNombre(texte) {
  // Retire tous les espaces (y compris insécables) : « 25 000 » -> « 25000 »
  let propre = texte.replace(/[\s  ]/g, "");
  // Remplace la virgule décimale par un point : « 12,5 » -> « 12.5 »
  propre = propre.replace(",", ".");
  // Expression régulière : uniquement des chiffres, avec éventuellement un point suivi de chiffres.
  // Cela refuse « abc », « -5 », « 1.2.3 » ou le texte vide.
  if (!/^\d+(\.\d+)?$/.test(propre)) {
    return null;
  }
  return Number(propre);
}

/**
 * Formate un montant avec le symbole de sa devise, à la française.
 * Intl.NumberFormat est l'outil intégré au navigateur pour afficher des
 * nombres selon les habitudes d'une langue (espaces, virgule décimale…).
 * @param {number} valeur  - le montant
 * @param {string} devise  - "MGA", "EUR" ou "USD"
 * @returns {string} le texte prêt à afficher, par exemple « 5 000,00 € »
 */
function formaterMontant(valeur, devise) {
  if (devise === "MGA") {
    // L'Ariary s'écrit sans décimales dans la vie courante.
    const entier = new Intl.NumberFormat("fr-FR", { maximumFractionDigits: 0 });
    return entier.format(Math.round(valeur)) + " Ar";
  }
  const format = new Intl.NumberFormat("fr-FR", {
    style: "currency",   // affiche le symbole de la monnaie
    currency: devise     // "EUR" ou "USD"
  });
  return format.format(valeur);
}

/**
 * Formate un nombre simple (sans symbole), pour le champ des taux.
 * @param {number} valeur - le nombre
 * @returns {string} par exemple « 4 500 » ou « 4 512,5 »
 */
function formaterSimple(valeur) {
  return new Intl.NumberFormat("fr-FR", { maximumFractionDigits: 4 }).format(valeur);
}

/* ============================================================
   4. CALCUL
   ============================================================ */

/**
 * Convertit un montant d'une devise vers une autre en passant par l'Ariary.
 * Exemple : 10 € vers $ -> 10 × 5000 = 50 000 Ar -> 50 000 ÷ 4500 ≈ 11,11 $.
 * @param {number} montant - le montant de départ
 * @param {string} source  - devise de départ ("MGA", "EUR", "USD")
 * @param {string} cible   - devise d'arrivée
 * @returns {number} le montant converti
 */
function convertir(montant, source, cible) {
  const enAriary = montant * taux[source]; // étape 1 : tout ramener en Ariary
  return enAriary / taux[cible];           // étape 2 : passer de l'Ariary à la devise voulue
}

/* ============================================================
   5. AFFICHAGE (DOM)
   ============================================================ */
/* Le DOM est l'arbre des éléments de la page. On le lit et on le modifie
   depuis JavaScript. D'abord, on retrouve les éléments UNE fois pour toutes. */

const champMontant      = document.getElementById("montant");
const erreurMontant     = document.getElementById("erreur-montant");
const selectSource      = document.getElementById("devise-source");
const selectCible       = document.getElementById("devise-cible");
const boutonInverser    = document.getElementById("bouton-inverser");
const zoneResultat      = document.getElementById("resultat");
const formConversion    = document.getElementById("formulaire-conversion");

const champTauxEur      = document.getElementById("taux-eur");
const champTauxUsd      = document.getElementById("taux-usd");
const erreurTaux        = document.getElementById("erreur-taux");
const formTaux          = document.getElementById("formulaire-taux");
const boutonReinit      = document.getElementById("bouton-reinitialiser");

const corpsReperes      = document.getElementById("corps-reperes");

/**
 * Affiche (ou efface) un message d'erreur sous un champ.
 * @param {HTMLInputElement} champ   - le champ concerné (sa bordure devient rouge)
 * @param {HTMLElement} zoneMessage  - le paragraphe où écrire le message
 * @param {string} message           - le message ; chaîne vide pour effacer l'erreur
 * @returns {void}
 */
function afficherErreur(champ, zoneMessage, message) {
  zoneMessage.textContent = message; // textContent : écrit du TEXTE (jamais du HTML) -> plus sûr
  if (message) {
    champ.classList.add("invalide");
    champ.setAttribute("aria-invalid", "true"); // informe les lecteurs d'écran
  } else {
    champ.classList.remove("invalide");
    champ.removeAttribute("aria-invalid");
  }
}

/**
 * Affiche le résultat d'une conversion dans la zone prévue.
 * @param {number} montant - montant saisi
 * @param {string} source  - devise de départ
 * @param {string} cible   - devise d'arrivée
 * @returns {void}
 */
function afficherResultat(montant, source, cible) {
  const resultat = convertir(montant, source, cible);
  // On vide la zone puis on la remplit avec des éléments créés « à la main » :
  // createElement + textContent évite d'insérer du HTML venant de l'utilisateur.
  zoneResultat.textContent = "";

  const ligne1 = document.createElement("span");
  ligne1.textContent = formaterMontant(montant, source) + " valent";

  const ligne2 = document.createElement("strong");
  ligne2.textContent = formaterMontant(resultat, cible);

  zoneResultat.append(ligne1, ligne2);
}

/**
 * Remplit les deux champs de taux avec les valeurs actuelles.
 * @returns {void}
 */
function afficherTaux() {
  champTauxEur.value = formaterSimple(taux.EUR);
  champTauxUsd.value = formaterSimple(taux.USD);
}

/**
 * Construit le tableau « Repères rapides » (une ligne par montant en Ariary).
 * @returns {void}
 */
function afficherReperes() {
  corpsReperes.textContent = ""; // on repart d'un tableau vide
  // forEach : exécute la fonction pour chaque élément de la liste
  REPERES_ARIARY.forEach(function (ariary) {
    const ligne = document.createElement("tr");
    // On crée trois cellules à partir d'une petite liste de textes
    [
      formaterMontant(ariary, "MGA"),
      formaterMontant(convertir(ariary, "MGA", "EUR"), "EUR"),
      formaterMontant(convertir(ariary, "MGA", "USD"), "USD")
    ].forEach(function (texte) {
      const cellule = document.createElement("td");
      cellule.textContent = texte;
      ligne.appendChild(cellule);
    });
    corpsReperes.appendChild(ligne);
  });
}

/* ============================================================
   6. ÉVÉNEMENTS
   ============================================================ */
/* Un événement est « quelque chose qui se passe » (clic, envoi, frappe).
   addEventListener dit : « quand cela arrive, exécute cette fonction ». */

/**
 * Gère l'envoi du formulaire de conversion (bouton « Convertir » ou touche Entrée).
 * @param {SubmitEvent} evenement - l'événement d'envoi
 * @returns {void}
 */
function surConversion(evenement) {
  // Par défaut, un formulaire recharge la page : on l'empêche.
  evenement.preventDefault();

  const montant = lireNombre(champMontant.value);
  if (montant === null) {
    afficherErreur(champMontant, erreurMontant,
      "Montant invalide : tapez un nombre positif, par exemple 25000 ou 12,5.");
    zoneResultat.textContent = "";
    champMontant.focus(); // remet le curseur dans le champ fautif
    return;
  }
  afficherErreur(champMontant, erreurMontant, ""); // tout va bien : on efface l'erreur
  afficherResultat(montant, selectSource.value, selectCible.value);
}

/**
 * Échange les devises « De » et « Vers », puis relance le calcul s'il y a un montant.
 * @returns {void}
 */
function surInversion() {
  const ancienneSource = selectSource.value;
  selectSource.value = selectCible.value;
  selectCible.value = ancienneSource;
  // Si un montant valide est déjà saisi, on met le résultat à jour tout de suite.
  const montant = lireNombre(champMontant.value);
  if (montant !== null) {
    afficherResultat(montant, selectSource.value, selectCible.value);
  }
}

/**
 * Gère l'enregistrement de nouveaux taux.
 * @param {SubmitEvent} evenement - l'événement d'envoi
 * @returns {void}
 */
function surChangementTaux(evenement) {
  evenement.preventDefault();

  const eur = lireNombre(champTauxEur.value);
  const usd = lireNombre(champTauxUsd.value);

  // Un taux doit être un nombre STRICTEMENT positif (diviser par 0 donnerait « Infinity »).
  const eurOk = eur !== null && eur > 0;
  const usdOk = usd !== null && usd > 0;
  afficherErreur(champTauxEur, erreurTaux, "");
  afficherErreur(champTauxUsd, erreurTaux, "");

  if (!eurOk || !usdOk) {
    // On marque en rouge le(s) champ(s) fautif(s) puis on écrit le message commun.
    if (!eurOk) { champTauxEur.classList.add("invalide"); }
    if (!usdOk) { champTauxUsd.classList.add("invalide"); }
    erreurTaux.textContent = "Chaque taux doit être un nombre supérieur à zéro (exemple : 4500).";
    (eurOk ? champTauxUsd : champTauxEur).focus();
    return;
  }

  taux = { MGA: 1, EUR: eur, USD: usd };
  sauvegarderTaux(taux);
  afficherTaux();
  afficherReperes();
  // Met à jour le résultat affiché si un montant est saisi
  const montant = lireNombre(champMontant.value);
  if (montant !== null) { afficherResultat(montant, selectSource.value, selectCible.value); }
  erreurTaux.textContent = ""; // pas de message : tout s'est bien passé
}

/**
 * Remet les taux d'exemple.
 * @returns {void}
 */
function surReinitialisation() {
  taux = { ...TAUX_PAR_DEFAUT };
  sauvegarderTaux(taux);
  afficherErreur(champTauxEur, erreurTaux, "");
  afficherErreur(champTauxUsd, erreurTaux, "");
  afficherTaux();
  afficherReperes();
}

// On branche chaque fonction sur son événement.
formConversion.addEventListener("submit", surConversion);
boutonInverser.addEventListener("click", surInversion);
formTaux.addEventListener("submit", surChangementTaux);
boutonReinit.addEventListener("click", surReinitialisation);

/* ============================================================
   7. DÉMARRAGE
   ============================================================ */

/**
 * Prépare la page au chargement : relit d'éventuels taux sauvegardés
 * puis remplit les champs et le tableau.
 * @returns {void}
 */
function demarrer() {
  const sauvegardes = chargerTaux();
  if (sauvegardes) { taux = sauvegardes; }
  afficherTaux();
  afficherReperes();
}

demarrer();