cours-brevo/script.js

907 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 dimporter une liste de contacts, il faut vérifier…",
a: ["Le consentement et la qualité des emails", "La couleur du logo", "Le nombre dimages dans lemail"],
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 dune campagne est toujours…",
a: ["Le taux douverture seul", "Le chiffre daffaires 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 dune 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 demail 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 dun formulaire ?",
a: ["Pour réduire la friction dinscription", "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 daccueil, 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: ["Linté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 demail 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 lenvoi ?",
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 dautomatisation Brevo peut servir à…",
a: ["Messages de bienvenue, relance, onboarding", "Créer un logo automatiquement", "Corriger les fautes dun 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 leffort sur automatisations et analyse.' : 'Bon niveau : viser latelier final et loptimisation.'}`
: `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();