907 lines
33 KiB
JavaScript
907 lines
33 KiB
JavaScript
const quizzes = {
|
||
positioning: [
|
||
{
|
||
q: "Brevo sert principalement à…",
|
||
a: ["Créer uniquement des sites web", "Gérer contacts, campagnes et automatisations", "Remplacer un CRM complet dans tous les cas"],
|
||
c: 1,
|
||
e: "Brevo est une plateforme marketing relationnelle tout-en-un pour gérer contacts, campagnes et scénarios automatisés."
|
||
},
|
||
{
|
||
q: "Avant d’importer une liste de contacts, il faut vérifier…",
|
||
a: ["Le consentement et la qualité des emails", "La couleur du logo", "Le nombre d’images dans l’email"],
|
||
c: 0,
|
||
e: "Le RGPD et les règles de délivrabilité imposent de vérifier le consentement explicite et d'assainir la liste."
|
||
},
|
||
{
|
||
q: "Un email transactionnel est déclenché par…",
|
||
a: ["Une action utilisateur ou un événement", "Une envie de communiquer une promotion", "Un post LinkedIn"],
|
||
c: 0,
|
||
e: "Les emails transactionnels répondent à une action directe (création de compte, achat) et sont attendus immédiatement."
|
||
},
|
||
{
|
||
q: "Le meilleur premier indicateur d’une campagne est toujours…",
|
||
a: ["Le taux d’ouverture seul", "Le chiffre d’affaires seul", "Un ensemble ouverture + clic + désinscription + rebonds"],
|
||
c: 2,
|
||
e: "Analyser un ensemble d'indicateurs croisés donne une vision réelle des performances et de la santé de la délivrabilité."
|
||
},
|
||
{
|
||
q: "Une automatisation commence généralement par…",
|
||
a: ["Un déclencheur", "Un PDF", "Une facture"],
|
||
c: 0,
|
||
e: "Tout scénario automatisé débute par un déclencheur (trigger) lié à une action ou un attribut de contact."
|
||
}
|
||
],
|
||
final: [
|
||
{
|
||
q: "Quel est le risque d’une base de contacts non qualifiée ?",
|
||
a: ["Améliorer la délivrabilité", "Dégrader la délivrabilité et la confiance", "Augmenter automatiquement les ventes"],
|
||
c: 1,
|
||
e: "Importer des adresses non consentantes ou invalides détruit la réputation d'expéditeur et peut mener au blocage du compte."
|
||
},
|
||
{
|
||
q: "Quelle structure d’email est la plus efficace ?",
|
||
a: ["Long texte sans bouton", "Problème, solution, bénéfice, preuve, CTA", "Images uniquement"],
|
||
c: 1,
|
||
e: "La structure AIDA (Attention, Intérêt, Désir, Action) structure la valeur apportée et amène naturellement au clic."
|
||
},
|
||
{
|
||
q: "Pourquoi limiter les champs d’un formulaire ?",
|
||
a: ["Pour réduire la friction d’inscription", "Pour collecter moins de consentement", "Pour éviter les statistiques"],
|
||
c: 0,
|
||
e: "Moins il y a d'étapes et de champs, plus le taux de conversion du formulaire d'inscription sera élevé."
|
||
},
|
||
{
|
||
q: "Dans un scénario d’accueil, une condition de clic sert à…",
|
||
a: ["Supprimer tous les contacts", "Adapter la suite selon le comportement", "Changer le mot de passe"],
|
||
c: 1,
|
||
e: "Les branches conditionnelles adaptent la communication selon le niveau d'engagement de l'apprenant."
|
||
},
|
||
{
|
||
q: "Le taux de clic mesure surtout…",
|
||
a: ["L’intérêt pour le contenu et le CTA", "La qualité du serveur DNS uniquement", "Le nombre de contacts importés"],
|
||
c: 0,
|
||
e: "Le clic démontre la pertinence du contenu proposé et la clarté du bouton d'appel à l'action."
|
||
},
|
||
{
|
||
q: "Un template d’email sert à…",
|
||
a: ["Créer une structure réutilisable", "Bloquer les désinscriptions", "Importer un CSV"],
|
||
c: 0,
|
||
e: "Les templates assurent l'homogénéité graphique de la marque et font gagner du temps à chaque envoi."
|
||
},
|
||
{
|
||
q: "Quel élément doit être testé avant l’envoi ?",
|
||
a: ["Liens, mobile, expéditeur, fautes", "Uniquement la taille du logo", "Rien si le design est joli"],
|
||
c: 0,
|
||
e: "Valider l'objet, l'expéditeur, l'affichage sur mobile et les liens évite les erreurs critiques de communication."
|
||
},
|
||
{
|
||
q: "Une bonne segmentation permet de…",
|
||
a: ["Envoyer le même message à tout le monde", "Cibler selon des critères pertinents", "Éviter le consentement"],
|
||
c: 1,
|
||
e: "Segmenter permet d'envoyer des messages ciblés selon les besoins ou comportements, maximisant l'engagement."
|
||
},
|
||
{
|
||
q: "Une phrase de consentement doit être…",
|
||
a: ["Floue", "Claire et explicite", "Cachée en bas de page"],
|
||
c: 1,
|
||
e: "Le RGPD impose une clause explicite, sans ambiguïté, informant l'utilisateur de l'usage fait de ses données."
|
||
},
|
||
{
|
||
q: "Un workflow d’automatisation Brevo peut servir à…",
|
||
a: ["Messages de bienvenue, relance, onboarding", "Créer un logo automatiquement", "Corriger les fautes d’un site web"],
|
||
c: 0,
|
||
e: "L'automatisation gère l'onboarding, l'engagement et les rappels tout au long de la vie du contact."
|
||
}
|
||
]
|
||
};
|
||
|
||
// --- INITIALISATION DES QUIZ ---
|
||
function renderQuiz(name) {
|
||
const box = document.querySelector(`[data-quiz="${name}"]`);
|
||
box.innerHTML = quizzes[name].map((item, i) => `
|
||
<div class="quiz-item">
|
||
<strong>${i + 1}. ${item.q}</strong>
|
||
${item.a.map((x, j) => `
|
||
<label><input type="radio" name="${name}_${i}" value="${j}"> ${x}</label>
|
||
`).join("")}
|
||
</div>
|
||
`).join("");
|
||
}
|
||
|
||
renderQuiz('positioning');
|
||
renderQuiz('final');
|
||
|
||
function checkQuiz(name) {
|
||
let score = 0;
|
||
const quizItems = document.querySelectorAll(`[data-quiz="${name}"] .quiz-item`);
|
||
|
||
quizzes[name].forEach((item, i) => {
|
||
const quizItem = quizItems[i];
|
||
quizItem.classList.remove('correct-quiz', 'incorrect-quiz');
|
||
const oldExplanation = quizItem.querySelector('.quiz-explanation');
|
||
if (oldExplanation) oldExplanation.remove();
|
||
|
||
const picked = document.querySelector(`input[name="${name}_${i}"]:checked`);
|
||
const isCorrect = picked && Number(picked.value) === item.c;
|
||
|
||
if (isCorrect) {
|
||
score++;
|
||
quizItem.classList.add('correct-quiz');
|
||
} else {
|
||
quizItem.classList.add('incorrect-quiz');
|
||
}
|
||
|
||
const exp = document.createElement('div');
|
||
exp.className = 'quiz-explanation';
|
||
exp.innerHTML = `${isCorrect ? '✓' : '✗'} <strong>Correction :</strong> ${item.e}`;
|
||
quizItem.appendChild(exp);
|
||
});
|
||
|
||
const pct = Math.round(score / quizzes[name].length * 100);
|
||
const msg = name === 'positioning'
|
||
? `Score : ${score}/${quizzes[name].length}. ${pct < 50 ? 'Niveau débutant : suivre toutes les étapes sans sauter les exercices.' : pct < 80 ? 'Base correcte : concentrer l’effort sur automatisations et analyse.' : 'Bon niveau : viser l’atelier final et l’optimisation.'}`
|
||
: `Score final : ${score}/${quizzes[name].length} (${pct}%). ${pct >= 80 ? 'Acquis validés.' : 'Revoir les modules contacts, campagnes et automatisations.'}`;
|
||
|
||
const resultEl = document.getElementById(name === 'positioning' ? 'positioningResult' : 'finalResult');
|
||
resultEl.textContent = msg;
|
||
resultEl.style.display = 'block';
|
||
resultEl.className = `result ${pct >= 80 ? 'badge-success' : pct >= 50 ? 'warning' : 'badge-error'}`;
|
||
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
|
||
document.querySelectorAll('[data-check]').forEach(b => b.addEventListener('click', () => checkQuiz(b.dataset.check)));
|
||
|
||
// --- EXERCICE 2 : DRAG & DROP & CLICK ---
|
||
let selectedDragCard = null;
|
||
let ex2Validated = false;
|
||
const ex2Solutions = {
|
||
"ex2_card_newsletter": "campagne",
|
||
"ex2_card_bienvenue": "automatisation",
|
||
"ex2_card_commande": "transactionnel",
|
||
"ex2_card_panier": "automatisation"
|
||
};
|
||
|
||
function setupDragAndDrop() {
|
||
const cards = document.querySelectorAll('#exercise2 .drag-card');
|
||
const zones = document.querySelectorAll('#exercise2 .drop-zone');
|
||
const sourceContainer = document.getElementById('ex2_cards');
|
||
|
||
cards.forEach(card => {
|
||
card.addEventListener('dragstart', (e) => {
|
||
card.classList.add('dragging');
|
||
e.dataTransfer.setData('text/plain', card.id);
|
||
});
|
||
|
||
card.addEventListener('dragend', () => {
|
||
card.classList.remove('dragging');
|
||
saveState();
|
||
updateProgress();
|
||
});
|
||
|
||
card.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
cards.forEach(c => {
|
||
if (c !== card) c.style.outline = 'none';
|
||
});
|
||
|
||
if (selectedDragCard === card) {
|
||
selectedDragCard.style.outline = 'none';
|
||
selectedDragCard = null;
|
||
} else {
|
||
selectedDragCard = card;
|
||
selectedDragCard.style.outline = '3px solid var(--brand)';
|
||
}
|
||
});
|
||
});
|
||
|
||
sourceContainer.addEventListener('dragover', (e) => e.preventDefault());
|
||
sourceContainer.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
const cardId = e.dataTransfer.getData('text/plain');
|
||
const card = document.getElementById(cardId);
|
||
if (card) {
|
||
sourceContainer.appendChild(card);
|
||
card.classList.remove('correct-card', 'incorrect-card');
|
||
ex2Validated = false;
|
||
document.getElementById('exercise2Feedback').className = 'exercise-feedback';
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
});
|
||
|
||
sourceContainer.addEventListener('click', () => {
|
||
if (selectedDragCard) {
|
||
sourceContainer.appendChild(selectedDragCard);
|
||
selectedDragCard.classList.remove('correct-card', 'incorrect-card');
|
||
selectedDragCard.style.outline = 'none';
|
||
selectedDragCard = null;
|
||
ex2Validated = false;
|
||
document.getElementById('exercise2Feedback').className = 'exercise-feedback';
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
});
|
||
|
||
zones.forEach(zone => {
|
||
const content = zone.querySelector('.drop-zone-content');
|
||
|
||
zone.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
zone.classList.add('dragover');
|
||
});
|
||
|
||
zone.addEventListener('dragleave', () => {
|
||
zone.classList.remove('dragover');
|
||
});
|
||
|
||
zone.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
zone.classList.remove('dragover');
|
||
const cardId = e.dataTransfer.getData('text/plain');
|
||
const card = document.getElementById(cardId);
|
||
if (card) {
|
||
content.appendChild(card);
|
||
card.classList.remove('correct-card', 'incorrect-card');
|
||
ex2Validated = false;
|
||
document.getElementById('exercise2Feedback').className = 'exercise-feedback';
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
});
|
||
|
||
zone.addEventListener('click', () => {
|
||
if (selectedDragCard) {
|
||
content.appendChild(selectedDragCard);
|
||
selectedDragCard.classList.remove('correct-card', 'incorrect-card');
|
||
selectedDragCard.style.outline = 'none';
|
||
selectedDragCard = null;
|
||
ex2Validated = false;
|
||
document.getElementById('exercise2Feedback').className = 'exercise-feedback';
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function checkExercise2() {
|
||
const cards = document.querySelectorAll('#exercise2 .drag-card');
|
||
let correctCount = 0;
|
||
const totalCards = cards.length;
|
||
let missing = 0;
|
||
|
||
cards.forEach(card => {
|
||
const parentZone = card.closest('.drop-zone');
|
||
card.classList.remove('correct-card', 'incorrect-card');
|
||
|
||
if (!parentZone) {
|
||
missing++;
|
||
return;
|
||
}
|
||
|
||
const category = parentZone.dataset.category;
|
||
const expected = ex2Solutions[card.id];
|
||
|
||
if (category === expected) {
|
||
card.classList.add('correct-card');
|
||
correctCount++;
|
||
} else {
|
||
card.classList.add('incorrect-card');
|
||
}
|
||
});
|
||
|
||
const feedback = document.getElementById('exercise2Feedback');
|
||
feedback.className = 'exercise-feedback show';
|
||
|
||
if (missing > 0) {
|
||
feedback.className = 'exercise-feedback show warning';
|
||
feedback.innerHTML = `<p>⚠️ <strong>Attention :</strong> Il reste ${missing} carte(s) non classée(s). Glisse-les toutes dans une colonne avant de lancer la correction.</p>`;
|
||
return;
|
||
}
|
||
|
||
ex2Validated = true;
|
||
if (correctCount === totalCards) {
|
||
feedback.className = 'exercise-feedback show correct';
|
||
feedback.innerHTML = `<p>🎉 <strong>Félicitations ! Toutes les cartes sont bien classées.</strong></p>
|
||
<ul>
|
||
<li><strong>Campagne Marketing :</strong> La newsletter est envoyée ponctuellement à une audience ciblée pour communiquer une offre ou actualité.</li>
|
||
<li><strong>Automatisation (Workflow) :</strong> Les emails de bienvenue (onboarding) et les paniers abandonnés sont déclenchés de manière programmée selon le comportement de l'utilisateur.</li>
|
||
<li><strong>Transactionnel :</strong> La confirmation de commande est émise de façon immédiate et unique pour valider un acte technique ou un achat initié par l'internaute.</li>
|
||
</ul>`;
|
||
} else {
|
||
feedback.className = 'exercise-feedback show incorrect';
|
||
feedback.innerHTML = `<p>❌ <strong>Il y a des erreurs (${correctCount}/${totalCards} correctes).</strong></p>
|
||
<p>Consulte la couleur des cartes : celles en vert sont correctes, celles en rouge sont incorrectes. Déplace à nouveau les cartes incorrectes pour résoudre l'exercice !</p>`;
|
||
}
|
||
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
|
||
function resetExercise2() {
|
||
const sourceContainer = document.getElementById('ex2_cards');
|
||
const cards = document.querySelectorAll('#exercise2 .drag-card');
|
||
cards.forEach(card => {
|
||
card.classList.remove('correct-card', 'incorrect-card');
|
||
card.style.outline = 'none';
|
||
sourceContainer.appendChild(card);
|
||
});
|
||
selectedDragCard = null;
|
||
ex2Validated = false;
|
||
|
||
const feedback = document.getElementById('exercise2Feedback');
|
||
feedback.className = 'exercise-feedback';
|
||
feedback.innerHTML = '';
|
||
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
|
||
// --- EXERCICE 3 : SIMULATEUR DE NETTOYAGE ---
|
||
let ex3Validated = false;
|
||
const ex3Solutions = {
|
||
"ex3_row1": "import",
|
||
"ex3_row2": "reject",
|
||
"ex3_row3": "reject",
|
||
"ex3_row4": "reject",
|
||
"ex3_row5": "import"
|
||
};
|
||
const ex3Explanations = {
|
||
"ex3_row1": {
|
||
correct: "Correct. Contact valide et double opt-in conforme RGPD.",
|
||
incorrect: "Erreur. Ce contact est parfaitement conforme et doit être importé."
|
||
},
|
||
"ex3_row2": {
|
||
correct: "Correct. Rejeté à juste titre : l'achat de bases de données est strictement illégal sous le RGPD et banni par Brevo.",
|
||
incorrect: "Erreur critique. L'achat de contacts est interdit par le RGPD et les CGU de Brevo. Cela conduit au blocage de votre compte."
|
||
},
|
||
"ex3_row3": {
|
||
correct: "Correct. Rejeté : les adresses génériques de rôles (support@, info@) polluent les listes et dégradent la délivrabilité.",
|
||
incorrect: "Erreur. L'adresse support@ est une adresse de rôle/générique à rejeter pour le marketing direct."
|
||
},
|
||
"ex3_row4": {
|
||
correct: "Correct. Rejeté : l'adresse email est invalide (absence de domaine valide comme .com ou .fr), ce qui causerait un rebond dur.",
|
||
incorrect: "Erreur. L'adresse ne contient pas d'extension de domaine (@gmail) et provoquera un rebond critique (hard bounce)."
|
||
},
|
||
"ex3_row5": {
|
||
correct: "Correct. Contact valide : l'inscription via livre blanc avec opt-in est une source légitime.",
|
||
incorrect: "Erreur. Ce contact est individuel et a donné son consentement via le livre blanc, il doit être importé."
|
||
}
|
||
};
|
||
|
||
function checkExercise3() {
|
||
let score = 0;
|
||
let answeredRows = 0;
|
||
const tbody = document.getElementById('ex3_tbody');
|
||
|
||
document.querySelectorAll('.row-explanation').forEach(el => el.remove());
|
||
document.querySelectorAll('#ex3_tbody tr').forEach(tr => tr.className = '');
|
||
|
||
for (let i = 1; i <= 5; i++) {
|
||
const rowId = `ex3_row${i}`;
|
||
const selected = document.querySelector(`input[name="${rowId}"]:checked`);
|
||
const tr = tbody.querySelector(`tr[data-row="${i}"]`);
|
||
|
||
if (selected) {
|
||
answeredRows++;
|
||
const val = selected.value;
|
||
const expected = ex3Solutions[rowId];
|
||
const isCorrect = val === expected;
|
||
|
||
const expDiv = document.createElement('div');
|
||
expDiv.className = `row-explanation ${isCorrect ? 'text-success' : 'text-error'}`;
|
||
|
||
if (isCorrect) {
|
||
tr.classList.add('correct-row');
|
||
expDiv.innerHTML = `✓ ${ex3Explanations[rowId].correct}`;
|
||
score++;
|
||
} else {
|
||
tr.classList.add('incorrect-row');
|
||
expDiv.innerHTML = `✗ ${ex3Explanations[rowId].incorrect}`;
|
||
}
|
||
tr.querySelector('.cell-action').appendChild(expDiv);
|
||
}
|
||
}
|
||
|
||
const feedback = document.getElementById('exercise3Feedback');
|
||
feedback.className = 'exercise-feedback show';
|
||
|
||
if (answeredRows < 5) {
|
||
feedback.className = 'exercise-feedback show warning';
|
||
feedback.innerHTML = `<p>⚠️ <strong>Attention :</strong> Fais un choix (Importer/Rejeter) pour les 5 contacts avant de valider.</p>`;
|
||
return;
|
||
}
|
||
|
||
ex3Validated = true;
|
||
if (score === 5) {
|
||
feedback.className = 'exercise-feedback show correct';
|
||
feedback.innerHTML = `<p>🎉 <strong>Félicitations ! 5/5 correct.</strong> Tu as nettoyé ta liste comme un professionnel et protégé ta délivrabilité.</p>`;
|
||
} else {
|
||
feedback.className = 'exercise-feedback show incorrect';
|
||
feedback.innerHTML = `<p>❌ <strong>Score : ${score}/5.</strong> Tu as commis des erreurs de qualification. Relis le détail sous chaque adresse pour comprendre les règles.</p>`;
|
||
}
|
||
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
|
||
function resetExercise3() {
|
||
document.querySelectorAll('input[name^="ex3_row"]').forEach(r => r.checked = false);
|
||
document.querySelectorAll('.row-explanation').forEach(el => el.remove());
|
||
document.querySelectorAll('#ex3_tbody tr').forEach(tr => tr.className = '');
|
||
|
||
const feedback = document.getElementById('exercise3Feedback');
|
||
feedback.className = 'exercise-feedback';
|
||
feedback.innerHTML = '';
|
||
ex3Validated = false;
|
||
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
|
||
// --- EXERCICE 4 : ANALYSEUR D'OBJET D'EMAIL ---
|
||
let ex4Validated = false;
|
||
|
||
function checkExercise4() {
|
||
const input = document.getElementById('ex4_input');
|
||
const text = input.value.trim();
|
||
const feedback = document.getElementById('exercise4Feedback');
|
||
feedback.className = 'exercise-feedback show';
|
||
|
||
if (!text) {
|
||
feedback.className = 'exercise-feedback show warning';
|
||
feedback.innerHTML = `<p>⚠️ <strong>Attention :</strong> Rédige un objet d'email avant de lancer l'analyseur.</p>`;
|
||
return;
|
||
}
|
||
|
||
ex4Validated = true;
|
||
let score = 5;
|
||
const issues = [];
|
||
const successes = [];
|
||
|
||
if (text.length < 15) {
|
||
score -= 1.5;
|
||
issues.push("L'objet est trop court. Il manque d'impact descriptif.");
|
||
} else if (text.length > 55) {
|
||
score -= 1.5;
|
||
issues.push(`L'objet est trop long (${text.length} car.). Il sera tronqué sur la majorité des téléphones mobiles (limite à ~50 car.).`);
|
||
} else {
|
||
successes.push(`Longueur optimale de l'objet (${text.length} caractères) !`);
|
||
}
|
||
|
||
const hasPersonalization = /\{\{|contact\.|prenom|prénom|\[\w+\]/i.test(text);
|
||
if (hasPersonalization) {
|
||
successes.push("Présence d'un tag de personnalisation détectée. Excellent pour booster l'ouverture !");
|
||
} else {
|
||
issues.push("Aucune variable de personnalisation détectée (ex: <code>{{ contact.PRENOM }}</code>). C'est un atout d'attention.");
|
||
}
|
||
|
||
const spamWords = ["gratuit", "offert", "vite", "100%", "argent", "gagner", "promotion", "promo", "urgent", "cadeau", "!!!"];
|
||
const foundSpam = [];
|
||
spamWords.forEach(word => {
|
||
if (text.toLowerCase().includes(word)) foundSpam.push(word);
|
||
});
|
||
|
||
if (foundSpam.length > 0) {
|
||
score -= foundSpam.length * 1.0;
|
||
issues.push(`Mots risqués identifiés : <strong>${foundSpam.join(', ')}</strong>. Ils risquent de déclencher les filtres anti-spam.`);
|
||
} else {
|
||
successes.push("Pas de spam-words flagrants détectés.");
|
||
}
|
||
|
||
score = Math.max(1, Math.min(5, score));
|
||
const stars = "★".repeat(Math.round(score)) + "☆".repeat(5 - Math.round(score));
|
||
|
||
if (score >= 4) {
|
||
feedback.className = 'exercise-feedback show correct';
|
||
} else if (score >= 2.5) {
|
||
feedback.className = 'exercise-feedback show warning';
|
||
} else {
|
||
feedback.className = 'exercise-feedback show incorrect';
|
||
}
|
||
|
||
let html = `<p><strong>Optimisation de l'objet : <span style="font-size:1.15rem">${stars}</span> (${score.toFixed(1)}/5)</strong></p>`;
|
||
if (successes.length > 0) {
|
||
html += `<p style="color:#065f46;margin-bottom:4px"><strong>Points forts :</strong><br>` + successes.map(s => `✓ ${s}`).join('<br>') + `</p>`;
|
||
}
|
||
if (issues.length > 0) {
|
||
html += `<p style="color:#b91c1c;margin:4px 0 0"><strong>Recommandations :</strong><br>` + issues.map(i => `✗ ${i}`).join('<br>') + `</p>`;
|
||
}
|
||
|
||
feedback.innerHTML = html;
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
|
||
function resetExercise4() {
|
||
document.getElementById('ex4_input').value = '';
|
||
const feedback = document.getElementById('exercise4Feedback');
|
||
feedback.className = 'exercise-feedback';
|
||
feedback.innerHTML = '';
|
||
ex4Validated = false;
|
||
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
|
||
// --- EXERCICE 5 : CLAUSE RGPD ---
|
||
let ex5Validated = false;
|
||
|
||
function checkExercise5() {
|
||
const textarea = document.getElementById('ex5_textarea');
|
||
const text = textarea.value.trim();
|
||
const feedback = document.getElementById('exercise5Feedback');
|
||
feedback.className = 'exercise-feedback show';
|
||
|
||
if (!text) {
|
||
feedback.className = 'exercise-feedback show warning';
|
||
feedback.innerHTML = `<p>⚠️ <strong>Attention :</strong> Saisis une clause de consentement avant de lancer la validation.</p>`;
|
||
return;
|
||
}
|
||
|
||
ex5Validated = true;
|
||
let score = 0;
|
||
const findings = [];
|
||
|
||
const serviceCheck = /recevoir|envoi|email|newsletter|courriel|communication|actualité|actualite|offre|lettre/i.test(text);
|
||
if (serviceCheck) {
|
||
score += 50;
|
||
findings.push("✓ <strong>Description des communications :</strong> Conforme. L'utilisateur sait quel contenu il va recevoir.");
|
||
} else {
|
||
findings.push("✗ <strong>Description du service manquante :</strong> Tu dois mentionner précisément le but de la collecte (ex : 'recevoir nos conseils hebdomadaires').");
|
||
}
|
||
|
||
const unsubCheck = /désabonner|desabonner|désinscription|desinscription|lien|retirer|moment/i.test(text);
|
||
if (unsubCheck) {
|
||
score += 50;
|
||
findings.push("✓ <strong>Droit de retrait (Désabonnement) :</strong> Conforme. La méthode pour se rétracter simplement et à tout moment est mentionnée.");
|
||
} else {
|
||
findings.push("✗ <strong>Droit de retrait manquant :</strong> Précise impérativement la possibilité de retrait (ex : 'Vous pouvez vous désabonner à tout moment via le lien de désinscription').");
|
||
}
|
||
|
||
if (score === 100) {
|
||
feedback.className = 'exercise-feedback show correct';
|
||
feedback.innerHTML = `<p>🎉 <strong>Clause de consentement 100% conforme au RGPD !</strong></p>` + findings.map(f => `<p>${f}</p>`).join('');
|
||
} else if (score === 50) {
|
||
feedback.className = 'exercise-feedback show warning';
|
||
feedback.innerHTML = `<p>⚠️ <strong>Clause partiellement conforme (50% de conformité) :</strong></p>` + findings.map(f => `<p>${f}</p>`).join('');
|
||
} else {
|
||
feedback.className = 'exercise-feedback show incorrect';
|
||
feedback.innerHTML = `<p>❌ <strong>Clause non conforme (0% de conformité) :</strong></p>` + findings.map(f => `<p>${f}</p>`).join('');
|
||
}
|
||
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
|
||
function resetExercise5() {
|
||
document.getElementById('ex5_textarea').value = '';
|
||
const feedback = document.getElementById('exercise5Feedback');
|
||
feedback.className = 'exercise-feedback';
|
||
feedback.innerHTML = '';
|
||
ex5Validated = false;
|
||
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
|
||
// --- EXERCICE 6 : SÉQUENCEUR DE WORKFLOW ---
|
||
const ex6SolutionOrder = ["declencheur", "attente1", "email1", "attente2", "email2"];
|
||
let ex6Validated = false;
|
||
|
||
function moveStepUp(button) {
|
||
const item = button.closest('.sortable-item');
|
||
const previous = item.previousElementSibling;
|
||
if (previous) {
|
||
item.parentNode.insertBefore(item, previous);
|
||
clearEx6Feedback();
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
}
|
||
|
||
function moveStepDown(button) {
|
||
const item = button.closest('.sortable-item');
|
||
const next = item.nextElementSibling;
|
||
if (next) {
|
||
item.parentNode.insertBefore(next, item);
|
||
clearEx6Feedback();
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
}
|
||
|
||
function clearEx6Feedback() {
|
||
const items = document.querySelectorAll('#ex6_list .sortable-item');
|
||
items.forEach(item => {
|
||
item.classList.remove('correct-item', 'incorrect-item');
|
||
});
|
||
const feedback = document.getElementById('exercise6Feedback');
|
||
feedback.className = 'exercise-feedback';
|
||
feedback.innerHTML = '';
|
||
ex6Validated = false;
|
||
}
|
||
|
||
function checkExercise6() {
|
||
const items = document.querySelectorAll('#ex6_list .sortable-item');
|
||
let correctCount = 0;
|
||
|
||
items.forEach((item, index) => {
|
||
const step = item.dataset.step;
|
||
const expected = ex6SolutionOrder[index];
|
||
item.classList.remove('correct-item', 'incorrect-item');
|
||
|
||
if (step === expected) {
|
||
item.classList.add('correct-item');
|
||
correctCount++;
|
||
} else {
|
||
item.classList.add('incorrect-item');
|
||
}
|
||
});
|
||
|
||
const feedback = document.getElementById('exercise6Feedback');
|
||
feedback.className = 'exercise-feedback show';
|
||
ex6Validated = true;
|
||
|
||
if (correctCount === 5) {
|
||
feedback.className = 'exercise-feedback show correct';
|
||
feedback.innerHTML = `<p>🎉 <strong>Scénario d'accueil 100% correct !</strong> Tu as configuré le flux logique idéal :</p>
|
||
<ol>
|
||
<li><strong>Déclencheur :</strong> Le client s'inscrit au formulaire (entrée de données).</li>
|
||
<li><strong>Délai (5 minutes) :</strong> Pour temporiser l'envoi et paraître naturel.</li>
|
||
<li><strong>Action (Email 1) :</strong> L'email de bienvenue confirme immédiatement la promesse.</li>
|
||
<li><strong>Délai (2 jours) :</strong> Laisse le temps d'assimiler l'accueil avant la suite.</li>
|
||
<li><strong>Action (Email 2) :</strong> Offre du contenu de valeur pour relancer la relation commerciale.</li>
|
||
</ol>`;
|
||
} else {
|
||
feedback.className = 'exercise-feedback show incorrect';
|
||
feedback.innerHTML = `<p>❌ <strong>Erreurs détectées (${correctCount}/5 étapes bien placées).</strong></p>
|
||
<p>Rappels : Les déclencheurs lancent le scénario, puis on alterne des temps d'attente et des actions. Utilise les flèches pour corriger les blocs rouges.</p>`;
|
||
}
|
||
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
|
||
function resetExercise6() {
|
||
const list = document.getElementById('ex6_list');
|
||
const items = Array.from(list.children);
|
||
const initialOrder = ["attente1", "email2", "declencheur", "attente2", "email1"];
|
||
|
||
initialOrder.forEach(step => {
|
||
const item = items.find(el => el.dataset.step === step);
|
||
if (item) list.appendChild(item);
|
||
});
|
||
|
||
clearEx6Feedback();
|
||
saveState();
|
||
updateProgress();
|
||
}
|
||
|
||
// --- PERSISTANCE ET SAUVEGARDE ---
|
||
function saveState() {
|
||
const tasks = [...document.querySelectorAll('.task')].map(x => x.checked);
|
||
|
||
// Quiz radios
|
||
const radios = [...document.querySelectorAll('input[name^="positioning_"], input[name^="final_"]')].map(x => x.checked);
|
||
|
||
// Text inputs
|
||
const texts = {
|
||
ex4_input: document.getElementById('ex4_input')?.value || '',
|
||
ex5_textarea: document.getElementById('ex5_textarea')?.value || '',
|
||
ex8_textarea: document.getElementById('ex8_textarea')?.value || ''
|
||
};
|
||
|
||
// Drag-and-drop state
|
||
const ex2State = {};
|
||
document.querySelectorAll('#exercise2 .drag-card').forEach(card => {
|
||
const parent = card.parentNode;
|
||
ex2State[card.id] = parent ? parent.id : 'ex2_cards';
|
||
});
|
||
|
||
// Cleaning table radios
|
||
const ex3State = {};
|
||
for (let i = 1; i <= 5; i++) {
|
||
const checked = document.querySelector(`input[name="ex3_row${i}"]:checked`);
|
||
ex3State[`ex3_row${i}`] = checked ? checked.value : null;
|
||
}
|
||
|
||
// Automation sequence state
|
||
const ex6State = [...document.querySelectorAll('#ex6_list .sortable-item')].map(item => item.dataset.step);
|
||
|
||
const state = {
|
||
tasks,
|
||
radios,
|
||
texts,
|
||
ex2State,
|
||
ex3State,
|
||
ex6State,
|
||
ex2Validated,
|
||
ex3Validated,
|
||
ex4Validated,
|
||
ex5Validated,
|
||
ex6Validated
|
||
};
|
||
|
||
localStorage.setItem('brevoCourseState', JSON.stringify(state));
|
||
}
|
||
|
||
function loadState() {
|
||
const raw = localStorage.getItem('brevoCourseState');
|
||
if (!raw) return;
|
||
const d = JSON.parse(raw);
|
||
|
||
// Restore task checkboxes
|
||
if (d.tasks) {
|
||
document.querySelectorAll('.task').forEach((x, i) => {
|
||
if (i < d.tasks.length) x.checked = !!d.tasks[i];
|
||
});
|
||
}
|
||
|
||
// Restore quiz radios
|
||
const quizRadios = document.querySelectorAll('input[name^="positioning_"], input[name^="final_"]');
|
||
if (d.radios && quizRadios.length > 0) {
|
||
quizRadios.forEach((x, i) => {
|
||
if (i < d.radios.length) x.checked = !!d.radios[i];
|
||
});
|
||
}
|
||
|
||
// Restore inputs & textareas
|
||
if (d.texts) {
|
||
if (d.texts.ex4_input !== undefined && document.getElementById('ex4_input')) {
|
||
document.getElementById('ex4_input').value = d.texts.ex4_input;
|
||
}
|
||
if (d.texts.ex5_textarea !== undefined && document.getElementById('ex5_textarea')) {
|
||
document.getElementById('ex5_textarea').value = d.texts.ex5_textarea;
|
||
}
|
||
if (d.texts.ex8_textarea !== undefined && document.getElementById('ex8_textarea')) {
|
||
document.getElementById('ex8_textarea').value = d.texts.ex8_textarea;
|
||
}
|
||
}
|
||
|
||
// Restore Exercise 2 drag elements
|
||
if (d.ex2State) {
|
||
Object.keys(d.ex2State).forEach(cardId => {
|
||
const card = document.getElementById(cardId);
|
||
const parentId = d.ex2State[cardId];
|
||
const parent = document.getElementById(parentId);
|
||
if (card && parent) {
|
||
parent.appendChild(card);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Restore Exercise 3 radio choices
|
||
if (d.ex3State) {
|
||
Object.keys(d.ex3State).forEach(name => {
|
||
const val = d.ex3State[name];
|
||
if (val) {
|
||
const input = document.querySelector(`input[name="${name}"][value="${val}"]`);
|
||
if (input) input.checked = true;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Restore Exercise 6 sequence
|
||
if (d.ex6State) {
|
||
const list = document.getElementById('ex6_list');
|
||
if (list) {
|
||
const items = Array.from(list.children);
|
||
d.ex6State.forEach(step => {
|
||
const item = items.find(el => el.dataset.step === step);
|
||
if (item) list.appendChild(item);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Restore validation triggers
|
||
ex2Validated = !!d.ex2Validated;
|
||
ex3Validated = !!d.ex3Validated;
|
||
ex4Validated = !!d.ex4Validated;
|
||
ex5Validated = !!d.ex5Validated;
|
||
ex6Validated = !!d.ex6Validated;
|
||
|
||
// Run validation overlays if previously checked
|
||
if (ex2Validated) checkExercise2();
|
||
if (ex3Validated) checkExercise3();
|
||
if (ex4Validated) checkExercise4();
|
||
if (ex5Validated) checkExercise5();
|
||
if (ex6Validated) checkExercise6();
|
||
|
||
updateProgress();
|
||
}
|
||
|
||
function updateProgress() {
|
||
const tasks = [...document.querySelectorAll('.task')];
|
||
const checkedTasks = tasks.filter(t => t.checked).length;
|
||
|
||
let answeredQuizQuestions = 0;
|
||
for (let i = 0; i < quizzes.positioning.length; i++) {
|
||
if (document.querySelector(`input[name="positioning_${i}"]:checked`)) answeredQuizQuestions++;
|
||
}
|
||
for (let i = 0; i < quizzes.final.length; i++) {
|
||
if (document.querySelector(`input[name="final_${i}"]:checked`)) answeredQuizQuestions++;
|
||
}
|
||
|
||
let exercisePoints = 0;
|
||
if (ex2Validated) exercisePoints++;
|
||
if (ex3Validated) exercisePoints++;
|
||
if (ex4Validated) exercisePoints++;
|
||
if (ex5Validated) exercisePoints++;
|
||
if (ex6Validated) exercisePoints++;
|
||
|
||
const finalActText = document.getElementById('ex8_textarea')?.value.trim() || '';
|
||
if (finalActText.length > 10) exercisePoints++;
|
||
|
||
const earned = checkedTasks + answeredQuizQuestions + exercisePoints;
|
||
const total = tasks.length + quizzes.positioning.length + quizzes.final.length + 6;
|
||
|
||
const pct = Math.round(earned / total * 100);
|
||
const progEl = document.getElementById('progress');
|
||
const labelEl = document.getElementById('progressLabel');
|
||
|
||
if (progEl) progEl.value = pct;
|
||
if (labelEl) labelEl.textContent = pct + '%';
|
||
}
|
||
|
||
document.addEventListener('input', () => {
|
||
saveState();
|
||
updateProgress();
|
||
});
|
||
document.addEventListener('change', () => {
|
||
saveState();
|
||
updateProgress();
|
||
});
|
||
|
||
// print
|
||
document.getElementById('printBtn').addEventListener('click', () => window.print());
|
||
document.getElementById('finalPrintBtn').addEventListener('click', () => window.print());
|
||
|
||
// reset
|
||
document.getElementById('resetBtn').addEventListener('click', () => {
|
||
localStorage.removeItem('brevoCourseState');
|
||
location.reload();
|
||
});
|
||
|
||
// md export
|
||
function htmlToMd() {
|
||
let md = '# Cours interactif — Prendre en main Brevo\n\n';
|
||
document.querySelectorAll('section.module').forEach(sec => {
|
||
const title = sec.querySelector('h2')?.innerText || '';
|
||
md += '## ' + title + '\n\n';
|
||
sec.querySelectorAll('p, li, h3').forEach(el => {
|
||
if (
|
||
el.closest('.quiz') ||
|
||
el.closest('.exercise-feedback') ||
|
||
el.closest('.exercise-actions') ||
|
||
el.closest('.item-actions') ||
|
||
el.closest('details')
|
||
) return;
|
||
|
||
const tag = el.tagName.toLowerCase();
|
||
const txt = el.innerText.trim();
|
||
if (!txt) return;
|
||
if (tag === 'h3') md += '### ' + txt + '\n\n';
|
||
else if (tag === 'li') md += '- ' + txt + '\n';
|
||
else md += txt + '\n\n';
|
||
});
|
||
});
|
||
return md;
|
||
}
|
||
|
||
document.getElementById('mdBtn').addEventListener('click', () => {
|
||
const blob = new Blob([htmlToMd()], { type: 'text/markdown;charset=utf-8' });
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = 'cours-brevo.md';
|
||
a.click();
|
||
URL.revokeObjectURL(a.href);
|
||
});
|
||
|
||
// Setup drag and drop events & load saved states on DOMContentLoaded
|
||
setupDragAndDrop();
|
||
loadState();
|
||
updateProgress();
|