Téléverser les fichiers vers "/"
This commit is contained in:
commit
fa62e6e01e
410
index.html
Normal file
410
index.html
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Cours -vPrendre en main Brevo</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&family=Inter:wght@400;500;600;700;800&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="hero">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Arinfo · Niveau débutant à intermédiaire</p>
|
||||||
|
<h1>Prendre en main Brevo : contacts, campagnes, formulaires et automatisations</h1>
|
||||||
|
<p class="lead">Un cours step by step pour créer une première base de contacts propre, envoyer une campagne email simple et construire un scénario d’accueil automatisé.</p>
|
||||||
|
<div class="actions no-print">
|
||||||
|
<button id="printBtn">Exporter en PDF / imprimer</button>
|
||||||
|
<button id="mdBtn" class="secondary">Exporter en Markdown</button>
|
||||||
|
<button id="resetBtn" class="ghost">Réinitialiser</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<aside class="course-card">
|
||||||
|
<strong>Durée estimée</strong><span>2h30 à 3h30</span>
|
||||||
|
<strong>Modalité</strong><span>Atelier guidé + exercices</span>
|
||||||
|
<strong>Pré-requis</strong><span>Compte Brevo, navigateur, fichier CSV de test</span>
|
||||||
|
</aside>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<nav class="progress no-print" aria-label="Progression du cours">
|
||||||
|
<span>Progression</span>
|
||||||
|
<progress id="progress" value="0" max="100"></progress>
|
||||||
|
<strong id="progressLabel">0%</strong>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section class="module" id="positionnement">
|
||||||
|
<h2>0. Quiz de positionnement</h2>
|
||||||
|
<p>Objectif : identifier le niveau de départ avant de lancer le cours.</p>
|
||||||
|
<div class="quiz" data-quiz="positioning"></div>
|
||||||
|
<button class="check no-print" data-check="positioning">Corriger le quiz de positionnement</button>
|
||||||
|
<p class="result" id="positioningResult"></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="module">
|
||||||
|
<h2>1. Objectifs pédagogiques</h2>
|
||||||
|
<ul class="checklist">
|
||||||
|
<li><label><input type="checkbox" class="task"> Comprendre le rôle de Brevo dans une stratégie marketing.</label></li>
|
||||||
|
<li><label><input type="checkbox" class="task"> Créer et organiser une liste de contacts.</label></li>
|
||||||
|
<li><label><input type="checkbox" class="task"> Concevoir une campagne email simple et professionnelle.</label></li>
|
||||||
|
<li><label><input type="checkbox" class="task"> Mettre en place un formulaire d’inscription.</label></li>
|
||||||
|
<li><label><input type="checkbox" class="task"> Construire une automatisation d’accueil.</label></li>
|
||||||
|
<li><label><input type="checkbox" class="task"> Lire les indicateurs de performance de base.</label></li>
|
||||||
|
</ul>
|
||||||
|
<div class="note"><strong>Point de vigilance RGPD :</strong> n’importez jamais de contacts sans preuve de consentement. Une base sale détruit la délivrabilité et l’image de marque.</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="module">
|
||||||
|
<h2>2. Comprendre Brevo en 10 minutes</h2>
|
||||||
|
<p>Brevo sert à gérer des contacts, envoyer des campagnes marketing, créer des formulaires, automatiser des actions et suivre les performances. Les emails transactionnels sont différents : ils sont déclenchés par une action utilisateur, par exemple une création de compte, une confirmation de commande ou une réinitialisation de mot de passe.</p>
|
||||||
|
<div class="grid">
|
||||||
|
<article><h3>Contacts</h3><p>Base de données : emails, attributs, listes, segments.</p></article>
|
||||||
|
<article><h3>Campagnes</h3><p>Emails marketing envoyés à une audience ciblée.</p></article>
|
||||||
|
<article><h3>Formulaires</h3><p>Collecte propre de contacts avec consentement.</p></article>
|
||||||
|
<article><h3>Automatisations</h3><p>Scénarios déclenchés par des événements ou comportements.</p></article>
|
||||||
|
</div>
|
||||||
|
<div class="exercise" id="exercise2">
|
||||||
|
<h3>Exercice 2 : Classifier les types d'emails (Drag and Drop)</h3>
|
||||||
|
<p>Glisse et dépose les exemples d'emails dans leur catégorie correspondante, ou clique sur les cartes pour les déplacer :</p>
|
||||||
|
|
||||||
|
<!-- Cartes à classer -->
|
||||||
|
<div class="drag-cards-container" id="ex2_cards">
|
||||||
|
<div class="drag-card" draggable="true" id="ex2_card_newsletter" data-item="newsletter">Newsletter mensuelle</div>
|
||||||
|
<div class="drag-card" draggable="true" id="ex2_card_bienvenue" data-item="bienvenue">Email de bienvenue</div>
|
||||||
|
<div class="drag-card" draggable="true" id="ex2_card_commande" data-item="commande">Confirmation de commande</div>
|
||||||
|
<div class="drag-card" draggable="true" id="ex2_card_panier" data-item="panier">Relance panier abandonné</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonnes cibles -->
|
||||||
|
<div class="drop-zones-container">
|
||||||
|
<div class="drop-zone" data-category="campagne">
|
||||||
|
<h4>Campagnes (Marketing)</h4>
|
||||||
|
<div class="drop-zone-content" id="ex2_zone_campagne"></div>
|
||||||
|
</div>
|
||||||
|
<div class="drop-zone" data-category="automatisation">
|
||||||
|
<h4>Automatisations</h4>
|
||||||
|
<div class="drop-zone-content" id="ex2_zone_automatisation"></div>
|
||||||
|
</div>
|
||||||
|
<div class="drop-zone" data-category="transactionnel">
|
||||||
|
<h4>Transactionnel</h4>
|
||||||
|
<div class="drop-zone-content" id="ex2_zone_transactionnel"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="exercise-actions no-print">
|
||||||
|
<button type="button" class="btn-check" onclick="checkExercise2()">Corriger l'exercice 2</button>
|
||||||
|
<button type="button" class="btn-reset secondary" onclick="resetExercise2()">Réinitialiser</button>
|
||||||
|
</div>
|
||||||
|
<div class="exercise-feedback" id="exercise2Feedback"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="module">
|
||||||
|
<h2>3. Step by step - Créer une base de contacts propre</h2>
|
||||||
|
<ol class="steps">
|
||||||
|
<li><strong>Préparer le fichier CSV.</strong> Colonnes minimales : email, prénom, source, consentement, date d’inscription.</li>
|
||||||
|
<li><strong>Créer une liste.</strong> Exemple : “Prospects - Atelier IA”.</li>
|
||||||
|
<li><strong>Importer les contacts.</strong> Mapper chaque colonne avec le bon attribut Brevo.</li>
|
||||||
|
<li><strong>Contrôler la qualité.</strong> Supprimer doublons, emails génériques douteux et contacts sans consentement.</li>
|
||||||
|
<li><strong>Segmenter.</strong> Exemple : source = formulaire LinkedIn, intérêt = formation IA.</li>
|
||||||
|
</ol>
|
||||||
|
<div class="exercise" id="exercise3">
|
||||||
|
<h3>Exercice 3 : Nettoyer et valider une liste de contacts avant import</h3>
|
||||||
|
<p>Voici 5 contacts extraits d'un fichier CSV d'inscription. Décide pour chaque contact s'il est conforme aux règles RGPD et de délivrabilité de Brevo en choisissant <strong>"Importer"</strong> ou <strong>"Rejeter"</strong> :</p>
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="cleaning-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Source</th>
|
||||||
|
<th>Consentement</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="ex3_tbody">
|
||||||
|
<tr data-row="1">
|
||||||
|
<td class="cell-email">jean.dupont@gmail.com</td>
|
||||||
|
<td>Formulaire Newsletter</td>
|
||||||
|
<td><span class="badge badge-success">Oui (Double Opt-In)</span></td>
|
||||||
|
<td class="cell-action">
|
||||||
|
<label class="radio-label"><input type="radio" name="ex3_row1" value="import"> Importer</label>
|
||||||
|
<label class="radio-label"><input type="radio" name="ex3_row1" value="reject"> Rejeter</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr data-row="2">
|
||||||
|
<td class="cell-email">contact@entreprise.com</td>
|
||||||
|
<td>Achat de base de données B2B</td>
|
||||||
|
<td><span class="badge badge-error">Non</span></td>
|
||||||
|
<td class="cell-action">
|
||||||
|
<label class="radio-label"><input type="radio" name="ex3_row2" value="import"> Importer</label>
|
||||||
|
<label class="radio-label"><input type="radio" name="ex3_row2" value="reject"> Rejeter</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr data-row="3">
|
||||||
|
<td class="cell-email">support@societe.fr</td>
|
||||||
|
<td>Formulaire de contact général</td>
|
||||||
|
<td><span class="badge badge-success">Oui</span></td>
|
||||||
|
<td class="cell-action">
|
||||||
|
<label class="radio-label"><input type="radio" name="ex3_row3" value="import"> Importer</label>
|
||||||
|
<label class="radio-label"><input type="radio" name="ex3_row3" value="reject"> Rejeter</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr data-row="4">
|
||||||
|
<td class="cell-email">pierre.durand@gmail</td>
|
||||||
|
<td>Formulaire Newsletter</td>
|
||||||
|
<td><span class="badge badge-success">Oui</span></td>
|
||||||
|
<td class="cell-action">
|
||||||
|
<label class="radio-label"><input type="radio" name="ex3_row4" value="import"> Importer</label>
|
||||||
|
<label class="radio-label"><input type="radio" name="ex3_row4" value="reject"> Rejeter</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr data-row="5">
|
||||||
|
<td class="cell-email">sophie.martin@outlook.fr</td>
|
||||||
|
<td>Téléchargement Livre Blanc</td>
|
||||||
|
<td><span class="badge badge-success">Oui</span></td>
|
||||||
|
<td class="cell-action">
|
||||||
|
<label class="radio-label"><input type="radio" name="ex3_row5" value="import"> Importer</label>
|
||||||
|
<label class="radio-label"><input type="radio" name="ex3_row5" value="reject"> Rejeter</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="exercise-actions no-print">
|
||||||
|
<button type="button" class="btn-check" onclick="checkExercise3()">Corriger l'exercice 3</button>
|
||||||
|
<button type="button" class="btn-reset secondary" onclick="resetExercise3()">Réinitialiser</button>
|
||||||
|
</div>
|
||||||
|
<div class="exercise-feedback" id="exercise3Feedback"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="module">
|
||||||
|
<h2>4. Step by step - Créer une campagne email</h2>
|
||||||
|
<ol class="steps">
|
||||||
|
<li><strong>Définir l’objectif.</strong> Exemple : inscription à un webinaire, téléchargement d’un guide, prise de rendez-vous.</li>
|
||||||
|
<li><strong>Choisir l’audience.</strong> Liste complète ou segment précis.</li>
|
||||||
|
<li><strong>Rédiger l’objet.</strong> Court, concret, sans promesse floue.</li>
|
||||||
|
<li><strong>Construire l’email.</strong> Une accroche, une valeur, une preuve, un appel à l’action.</li>
|
||||||
|
<li><strong>Tester.</strong> Envoyer un test, vérifier mobile, liens, fautes, expéditeur.</li>
|
||||||
|
<li><strong>Programmer.</strong> Choisir une date cohérente avec l’audience.</li>
|
||||||
|
</ol>
|
||||||
|
<div class="template-box">
|
||||||
|
<h3>Structure recommandée</h3>
|
||||||
|
<p><strong>Objet :</strong> [Bénéfice clair] en [temps court]</p>
|
||||||
|
<p><strong>Corps :</strong> problème → solution → bénéfice → preuve → bouton unique.</p>
|
||||||
|
<p><strong>CTA :</strong> “Réserver ma place”, “Télécharger le guide”, “Demander une démo”.</p>
|
||||||
|
</div>
|
||||||
|
<div class="exercise" id="exercise4">
|
||||||
|
<h3>Exercice 4 : Rédiger un objet d'email percutant</h3>
|
||||||
|
<p>Rédige un objet d'email pour promouvoir une formation <strong>"Découvrir l’IA générative en entreprise"</strong>. Évite les termes spam, soigne la longueur et cherche à capter l'intérêt.</p>
|
||||||
|
<input type="text" id="ex4_input" placeholder="Ex: [Prénom], gagnez 2 heures par jour grâce à l'IA" />
|
||||||
|
|
||||||
|
<div class="exercise-actions no-print">
|
||||||
|
<button type="button" class="btn-check" onclick="checkExercise4()">Analyser l'objet</button>
|
||||||
|
<button type="button" class="btn-reset secondary" onclick="resetExercise4()">Réinitialiser</button>
|
||||||
|
</div>
|
||||||
|
<div class="exercise-feedback" id="exercise4Feedback"></div>
|
||||||
|
|
||||||
|
<details class="correction-accordion no-print">
|
||||||
|
<summary>💡 Voir des exemples d'objets recommandés</summary>
|
||||||
|
<div class="accordion-content">
|
||||||
|
<ul>
|
||||||
|
<li><strong>Approche Curiosité / Directe :</strong> <code>L'IA générative dans votre entreprise : par où commencer ?</code> (Clair, va droit au but).</li>
|
||||||
|
<li><strong>Approche Bénéfice / Urgence :</strong> <code>{{ contact.PRENOM }}, gagnez 2h par jour grâce à l'IA</code> (Personnalisé, quantifie le gain de temps).</li>
|
||||||
|
<li><strong>Approche Chiffre / Preuve :</strong> <code>3 cas d'usage réels de l'IA dans votre secteur</code> (Donne envie de découvrir les exemples concrets).</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="module">
|
||||||
|
<h2>5. Step by step - Créer un formulaire d’inscription</h2>
|
||||||
|
<ol class="steps">
|
||||||
|
<li><strong>Définir la promesse.</strong> Pourquoi la personne laisserait-elle son email ?</li>
|
||||||
|
<li><strong>Limiter les champs.</strong> Email + prénom suffisent souvent au départ.</li>
|
||||||
|
<li><strong>Ajouter le consentement.</strong> Formulation claire, non ambiguë.</li>
|
||||||
|
<li><strong>Associer à une liste.</strong> Exemple : “Inscrits newsletter”.</li>
|
||||||
|
<li><strong>Tester l’inscription.</strong> Vérifier l’ajout dans la liste et le message de confirmation.</li>
|
||||||
|
</ol>
|
||||||
|
<div class="exercise" id="exercise5">
|
||||||
|
<h3>Exercice 5 : Rédiger la clause de consentement RGPD</h3>
|
||||||
|
<p>Écris la phrase de consentement (opt-in) qui accompagnera la case à cocher de ton formulaire d'inscription. Elle doit indiquer ce que l'abonné recevra et comment il peut se désabonner.</p>
|
||||||
|
<textarea id="ex5_textarea" placeholder="J'accepte de recevoir..." rows="3"></textarea>
|
||||||
|
|
||||||
|
<div class="exercise-actions no-print">
|
||||||
|
<button type="button" class="btn-check" onclick="checkExercise5()">Valider la clause RGPD</button>
|
||||||
|
<button type="button" class="btn-reset secondary" onclick="resetExercise5()">Réinitialiser</button>
|
||||||
|
</div>
|
||||||
|
<div class="exercise-feedback" id="exercise5Feedback"></div>
|
||||||
|
|
||||||
|
<details class="correction-accordion no-print">
|
||||||
|
<summary>💡 Voir la clause de consentement type (conforme RGPD)</summary>
|
||||||
|
<div class="accordion-content">
|
||||||
|
<p class="model-text"><em>"En renseignant votre adresse email, vous acceptez de recevoir nos actualités et offres promotionnelles par courrier électronique. Vous pouvez vous désabonner à tout moment à l'aide des liens de désinscription intégrés dans nos emails."</em></p>
|
||||||
|
<p><strong>Pourquoi cette clause est conforme :</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Précision :</strong> Elle définit la nature des envois (actualités, offres).</li>
|
||||||
|
<li><strong>Libre arbitre :</strong> Elle rappelle que le désabonnement est possible à tout moment et facilement.</li>
|
||||||
|
<li><strong>Actif :</strong> Elle doit être associée à une case à cocher non pré-cochée.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="module">
|
||||||
|
<h2>6. Step by step - Créer une automatisation d’accueil</h2>
|
||||||
|
<p>Brevo permet de créer des automatisations marketing simples, comme des messages de bienvenue, de l’onboarding, des relances panier abandonné, des anniversaires, du lead scoring ou de la gestion de listes.</p>
|
||||||
|
<ol class="steps">
|
||||||
|
<li><strong>Déclencheur.</strong> Contact ajouté à la liste “Inscrits newsletter”.</li>
|
||||||
|
<li><strong>Délai.</strong> Attendre 5 minutes pour éviter l’effet robot immédiat.</li>
|
||||||
|
<li><strong>Email 1.</strong> Bienvenue + rappel de la promesse.</li>
|
||||||
|
<li><strong>Délai.</strong> Attendre 2 jours.</li>
|
||||||
|
<li><strong>Email 2.</strong> Ressource utile ou cas pratique.</li>
|
||||||
|
<li><strong>Condition.</strong> Si clic sur le lien : tag “intéressé”. Sinon : relance légère.</li>
|
||||||
|
</ol>
|
||||||
|
<div class="scenario">
|
||||||
|
<span>Entrée liste</span><b>→</b><span>Attente</span><b>→</b><span>Email 1</span><b>→</b><span>Attente</span><b>→</b><span>Condition clic</span>
|
||||||
|
</div>
|
||||||
|
<div class="exercise" id="exercise6">
|
||||||
|
<h3>Exercice 6 : Ordonner les étapes d'un scénario d'automatisation</h3>
|
||||||
|
<p>Organise ces 5 actions pour construire un scénario d'onboarding cohérent et efficace en cliquant sur les flèches pour déplacer les étapes vers le haut ou le bas :</p>
|
||||||
|
|
||||||
|
<ul class="sortable-list" id="ex6_list">
|
||||||
|
<li class="sortable-item" data-step="attente1">
|
||||||
|
<div class="item-content">
|
||||||
|
<span class="step-badge text-delay">Délai</span> Attendre 5 minutes avant d'envoyer
|
||||||
|
</div>
|
||||||
|
<div class="item-actions no-print">
|
||||||
|
<button type="button" class="btn-move-up" onclick="moveStepUp(this)">↑</button>
|
||||||
|
<button type="button" class="btn-move-down" onclick="moveStepDown(this)">↓</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="sortable-item" data-step="email2">
|
||||||
|
<div class="item-content">
|
||||||
|
<span class="step-badge text-action">Action</span> Envoyer l'Email 2 (Ressource pratique & conseils)
|
||||||
|
</div>
|
||||||
|
<div class="item-actions no-print">
|
||||||
|
<button type="button" class="btn-move-up" onclick="moveStepUp(this)">↑</button>
|
||||||
|
<button type="button" class="btn-move-down" onclick="moveStepDown(this)">↓</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="sortable-item" data-step="declencheur">
|
||||||
|
<div class="item-content">
|
||||||
|
<span class="step-badge text-trigger">Déclencheur</span> Le contact s'inscrit au formulaire "Newsletter"
|
||||||
|
</div>
|
||||||
|
<div class="item-actions no-print">
|
||||||
|
<button type="button" class="btn-move-up" onclick="moveStepUp(this)">↑</button>
|
||||||
|
<button type="button" class="btn-move-down" onclick="moveStepDown(this)">↓</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="sortable-item" data-step="attente2">
|
||||||
|
<div class="item-content">
|
||||||
|
<span class="step-badge text-delay">Délai</span> Attendre 2 jours
|
||||||
|
</div>
|
||||||
|
<div class="item-actions no-print">
|
||||||
|
<button type="button" class="btn-move-up" onclick="moveStepUp(this)">↑</button>
|
||||||
|
<button type="button" class="btn-move-down" onclick="moveStepDown(this)">↓</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="sortable-item" data-step="email1">
|
||||||
|
<div class="item-content">
|
||||||
|
<span class="step-badge text-action">Action</span> Envoyer l'Email 1 de bienvenue (Rappel de la promesse)
|
||||||
|
</div>
|
||||||
|
<div class="item-actions no-print">
|
||||||
|
<button type="button" class="btn-move-up" onclick="moveStepUp(this)">↑</button>
|
||||||
|
<button type="button" class="btn-move-down" onclick="moveStepDown(this)">↓</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="exercise-actions no-print">
|
||||||
|
<button type="button" class="btn-check" onclick="checkExercise6()">Valider le scénario</button>
|
||||||
|
<button type="button" class="btn-reset secondary" onclick="resetExercise6()">Réinitialiser</button>
|
||||||
|
</div>
|
||||||
|
<div class="exercise-feedback" id="exercise6Feedback"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="module">
|
||||||
|
<h2>7. Lire les résultats sans se noyer</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Indicateur</th><th>Ce qu’il dit</th><th>Action si mauvais résultat</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Taux d’ouverture</td><td>Attractivité de l’objet et confiance envers l’expéditeur.</td><td>Tester un objet plus clair, segmenter mieux.</td></tr>
|
||||||
|
<tr><td>Taux de clic</td><td>Intérêt pour le contenu et le CTA.</td><td>Réduire le contenu, rendre le bouton plus visible.</td></tr>
|
||||||
|
<tr><td>Désinscriptions</td><td>Écart entre promesse et contenu reçu.</td><td>Revoir fréquence, ciblage et valeur de l’email.</td></tr>
|
||||||
|
<tr><td>Rebonds</td><td>Qualité technique de la base email.</td><td>Nettoyer la base, supprimer emails invalides.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="module">
|
||||||
|
<h2>8. Atelier final</h2>
|
||||||
|
<p><strong>Mission :</strong> préparer une mini-stratégie Brevo pour promouvoir une formation courte.</p>
|
||||||
|
<ol>
|
||||||
|
<li>Créer une liste cible.</li>
|
||||||
|
<li>Définir un formulaire d’inscription.</li>
|
||||||
|
<li>Rédiger un objet de campagne.</li>
|
||||||
|
<li>Créer un scénario d’accueil en 3 emails.</li>
|
||||||
|
<li>Définir 3 indicateurs de suivi.</li>
|
||||||
|
</ol>
|
||||||
|
<textarea id="ex8_textarea" class="large" placeholder="Ton plan d’action Brevo..."></textarea>
|
||||||
|
|
||||||
|
<details class="correction-accordion no-print">
|
||||||
|
<summary>💡 Afficher un exemple de plan d'action type pour s'auto-évaluer</summary>
|
||||||
|
<div class="accordion-content">
|
||||||
|
<h4>Exemple de plan d'action recommandé :</h4>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Créer une liste cible :</strong> Liste <code>"Prospects - Formation Courte"</code>.</li>
|
||||||
|
<li><strong>Formulaire d'inscription :</strong> Intégré sur le site, demandant <em>Prénom</em> et <em>Email</em>, avec une clause de consentement claire (opt-in actif).</li>
|
||||||
|
<li><strong>Objet de campagne :</strong> <code>"{{ contact.PRENOM }}, maîtrisez [Sujet] en 3 jours"</code>.</li>
|
||||||
|
<li><strong>Scénario d'accueil en 3 emails :</strong>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Déclencheur :</strong> Inscription au formulaire.</li>
|
||||||
|
<li><strong>Email 1 (immédiat) :</strong> Contenu promis + liens utiles.</li>
|
||||||
|
<li><strong>Email 2 (J+1) :</strong> Étude de cas ou témoignage client pour asseoir la crédibilité.</li>
|
||||||
|
<li><strong>Email 3 (J+3) :</strong> Offre commerciale avec appel à l'action direct (réserver un appel).</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><strong>3 indicateurs de suivi :</strong> Taux d'ouverture (qualité de l'objet), Taux de clic (intérêt pour l'offre/CTA), et Taux de désinscription (pertinence globale).</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="module" id="quiz-final">
|
||||||
|
<h2>9. Quiz final</h2>
|
||||||
|
<div class="quiz" data-quiz="final"></div>
|
||||||
|
<button class="check no-print" data-check="final">Corriger le quiz final</button>
|
||||||
|
<p class="result" id="finalResult"></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="module end-course no-print" id="completion-section">
|
||||||
|
<h2>Félicitations ! Vous avez complété le cours 🎉</h2>
|
||||||
|
<p>Vous êtes arrivé au bout de ce cours interactif Brevo. Vous pouvez exporter l'ensemble du cours incluant vos réponses, vos notes et vos corrections au format PDF pour le conserver ou le partager.</p>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" id="finalPrintBtn" class="btn-check">Télécharger mon cours complété (PDF)</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="module sources">
|
||||||
|
<h2>Sources utilisées pour la conception du cours</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Documentation Brevo : automatisations, templates, emails transactionnels, SMTP, migration vers le nouvel éditeur d’automatisation.</li>
|
||||||
|
<li>Le cours évite volontairement les captures d’écran figées : l’interface Brevo évolue, les libellés peuvent changer.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>Cours interactif Brevo - version imprimable et exportable Markdown.</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
906
script.js
Normal file
906
script.js
Normal file
@ -0,0 +1,906 @@
|
|||||||
|
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();
|
||||||
Loading…
Reference in New Issue
Block a user