// -- GESTION DU FORMULAIRE PERMETTANT D'AFFICHER ET RÉPONDRE À UN QUIZ /// Il n'est pas nécessaire d'être connecté pour répondre au quiz et voir son résultat. /// Mais si pas connecté, on propose à l'internaute de se connecter ou de créer un compte pour sauvegarder son résultat. /// Dans ce but son résultat est stocké dans son navigateur. /// Si il est connecté, l'enregistrement de son résultat se fait automatiquement côté serveur et ses éventuels précédents résultats sont affichés. // Fichier de configuration tirés du backend : import { apiUrl, availableLangs, theme } from "../../config/instance.js"; const lang=availableLangs[0]; import { getPreviousAnswers, questionnaireRoutes, saveAnswersRoute } from "../../config/questionnaires.js"; const configTemplate = require("../../views/"+theme+"/config/"+lang+".js"); import { checkAnswerOuput, saveAnswer } from "./tools/answers.js"; import { addElement } from "./tools/dom.js"; import { helloDev, updateAccountLink } from "./tools/everywhere.js"; import { getLocaly } from "./tools/clientstorage.js"; import { getDatasFromInputs } from "./tools/forms.js"; import { dateFormat, replaceAll } from "../../tools/main"; import { loadMatomo } from "./tools/matomo.js"; import { checkSession, getTimeDifference } from "./tools/users.js"; // Dictionnaires : const txtServerError = require("../../lang/"+lang+"/general").serverError; const txtAnswers = require("../../lang/"+lang+"/answer"); // Principaux éléments du DOM manipulés : const myForm = document.getElementById("questionnaire"); const divResponse = document.getElementById("response"); const btnShow = document.getElementById("showQuestionnaire"); const btnSubmit = document.getElementById("checkResponses"); const explanationsTitle = document.getElementById("explanationsTitle"); const explanationsContent = document.getElementById("explanationsContent"); let isConnected, user; const initialise = async () => { try { btnShow.style.display="inline";// bouton caché si JS inactif, car JS nécessaire pour vérifier les réponses isConnected=await checkSession(["user"]);// "user" car seuls les utilisateurs de base peuvent enregistrer leurs réponses aux quizs // Si l'utilisateur est connecté et a déjà répondu à ce quiz, on affiche ses précédentes réponses à la place du texte servant à expliquer le topo aux nouveaux if(isConnected) { user=getLocaly("user", true); updateAccountLink(user.status, configTemplate);// lien vers le compte adapté pour les utilisateurs connectés checkPreviousResponses(user); } else loadMatomo(); } catch(e) { console.error(e); } } initialise(); helloDev(); // Affichage du questionnaire quand l'utilisateur clique sur le bouton ou si l'id du formulaire est passée par l'url. // Déclenche en même temps le chronomètre mesurant la durée de la réponse aux questions. const showQuestionnaire = () => { chronoBegin=Date.now(); myForm.style.display="block"; btnShow.style.display="none"; const here=window.location;// window.location à ajouter pour ne pas quitter la page en mode "preview". if(window.location.hash!=="") { window.location.hash="";// ! le "#" reste window.location.assign(here+"questionnaire"); } else window.location.assign(here+"#questionnaire"); } let chronoBegin=0; btnShow.addEventListener("click", function(e) { try { e.preventDefault(); showQuestionnaire(); } catch(e) { addElement(divResponse, "p", txtServerError, "", ["error"]); console.error(e); } }); // Lien passé par mail pour voir directement le quiz if(location.hash!="" && location.hash==="#questionnaire") showQuestionnaire(); // Traitement de l'envoi de la réponse de l'utilisateur : let answer = {}; myForm.addEventListener("submit", function(e) { try { e.preventDefault(); btnSubmit.style.display="none";// seulement un envoi à la fois, SVP :) divResponse.innerHTML="";// supprime les éventuels messages déjà affichés const userResponses=getDatasFromInputs(myForm); answer.duration=Math.round((Date.now()-chronoBegin)/1000); answer.nbQuestions=0; answer.nbCorrectAnswers=0; answer.QuestionnaireId=document.getElementById("questionnaireId").value; // Les réponses sont regroupées par question, donc quand idQuestion change, on connaît le résultat pour la question précédente. // Pour qu'une réponse soit bonne, il faut cocher toutes les bonnes réponses (si QCM) à la question ET cocher aucune des mauvaises. let idChoice, idQuestion="", goodResponse=false; for(let item in userResponses) { if(item.startsWith("isCorrect_response_"))// = Nouvelle réponse possible. { idChoice = item.substring(item.lastIndexOf("_") + 1); // si on change de queston if(userResponses["question_id_response_"+idChoice]!=idQuestion) // on commence à traiter une nouvelle question { idQuestion=userResponses["question_id_response_"+idChoice]; answer.nbQuestions++; if(goodResponse) // résultat de la question précédente answer.nbCorrectAnswers++; goodResponse=true;// réponse bonne jusqu'à la première erreur... } if(userResponses[item]=="true") { document.getElementById("response_"+idChoice).parentNode.classList.add("isCorrect"); if(userResponses["response_"+idChoice]===undefined)// une bonne réponse n'a pas été sélectionnée goodResponse=false; } else { if(userResponses["response_"+idChoice]==="on")// réponse cochée ne faisant pas partie des bonnes { goodResponse=false; document.getElementById("response_"+idChoice).parentNode.classList.add("isNotCorrect"); } } } } // si j'ai bien répondu à la dernière question, il faut le compter ici, car je suis sorti de la boucle : if(goodResponse) answer.nbCorrectAnswers++; // Affichage du résultat, suivant si l'utilisateur est connecté ou pas et son score : let getOuput=checkAnswerOuput(answer); if(isConnected) { // Si l'utilisateur est connecté, on enregistre son résultat sur le serveur. const xhrSaveAnswer = new XMLHttpRequest(); xhrSaveAnswer.open("POST", apiUrl+questionnaireRoutes+saveAnswersRoute); xhrSaveAnswer.onreadystatechange = function() { if (this.readyState == XMLHttpRequest.DONE) { let xhrResponse=JSON.parse(this.responseText); if (this.status === 201 && (xhrResponse.message)) { getOuput+="
"+xhrResponse.message.replace("#URL", configTemplate.userHomePage); checkPreviousResponses(user); } else getOuput+="
"+txtAnswers.responseSavedError.replace("#URL", configTemplate.userHomePage); // on redirige vers le résultat window.location.hash=""; const here=window.location;// window.location à ajouter pour ne pas quitter la page en mode "preview"... window.location.assign(here+"explanations"); } } xhrSaveAnswer.setRequestHeader("Authorization", "Bearer "+user.token); xhrSaveAnswer.setRequestHeader("Content-Type", "application/json"); answer.timeDifference=getTimeDifference();// on en profite pour mettre les pendules à l'heure. xhrSaveAnswer.send(JSON.stringify(answer)); } else { // si pas connecté, on enregistre le résultat côté client pour permettre de le retrouver au moment de la création du compte ou de la connexion. if(saveAnswer(answer)) { getOuput+="

"+txtAnswers.wantToSaveResponses; addElement(divResponse, "p", getOuput, "", ["info"]); document.querySelector(".subscribeBtns").style.display="block"; } else // inutile de proposer de créer un compte si le stockage local ne fonctionne pas addElement(divResponse, "p", getOuput, "", ["info"]); // on redirige vers le résultat window.location.hash=""; const here=window.location;// window.location à ajouter pour ne pas quitter la page en mode "preview"... window.location.assign(here+"response"); } // + affichage des textes d'explications pour chaque question const explanations=document.querySelectorAll(".help"); for(let i in explanations) { if(explanations[i].style!=undefined) // sinon, la console affiche une erreur "TypeError: explanations[i].style is undefined", bien que tout fonctionne (?) explanations[i].style.display="block"; } } catch(e) { addElement(divResponse, "p", txtServerError, "", ["error"]); console.error(e); } }) // Fonction vérifiant les précédentes réponses de l'utilisateur // Utile si connecté lors du premier chargement de la page, puis après une nouvelle réponse const checkPreviousResponses = (user) => { const xhrPreviousRes = new XMLHttpRequest(); xhrPreviousRes.open("GET", apiUrl+questionnaireRoutes+getPreviousAnswers+user.id+"/"+document.getElementById("questionnaireId").value); xhrPreviousRes.onreadystatechange = function() { if (this.readyState == XMLHttpRequest.DONE) { let response=JSON.parse(this.responseText); if (this.status === 200) { const nbResponses=response.length; let previousAnswersContent=""; addElement(explanationsTitle, "span", txtAnswers.previousAnswersTitle.replace("#NOM", user.name)); if(nbResponses!==0) { let totNbQuestions=0, totNbCorrectAnswers=0, totDuration=0, mapLineContent; for(let i in response) { totNbQuestions+=response[i].nbQuestions;// ! on ne peut se baser sur la version actuelle du quiz, car le nombre de questions a pu évoluer. totNbCorrectAnswers+=response[i].nbCorrectAnswers; totDuration+=response[i].duration; mapLineContent = { DATEANSWER : dateFormat(response[i].createdAt, lang), NBCORRECTANSWERS : response[i].nbCorrectAnswers, NBQUESTIONS : response[i].nbQuestions, AVGDURATION : response[i].duration }; previousAnswersContent+="
  • "+replaceAll(txtAnswers.previousAnswersLine, mapLineContent)+"
  • "; } mapLineContent = { AVGDURATION : Math.round(totDuration/nbResponses), AVGCORRECTANSWERS : Math.round(totNbCorrectAnswers/totNbQuestions*100) }; previousAnswersContent="
    "+replaceAll(txtAnswers.previousAnswersStats, mapLineContent)+"
    "+previousAnswersContent; addElement(explanationsContent, "ul", previousAnswersContent); } else addElement(explanationsContent, "ul", txtAnswers.noPreviousAnswer); // dans un cas comme dans l'autre, bouton pour revenir à l'accueil du compte addElement(explanationsContent, "p", ""+configTemplate.userHomePageTxt+"", "", ["btn"], "", false); } } } xhrPreviousRes.setRequestHeader("Authorization", "Bearer "+user.token); xhrPreviousRes.send(); }