Forum Discussion

VirginieBergon's avatar
VirginieBergon
Community Member
2 months ago

Custom Interactive Product Match – Built with Code Snippet (Accessible & Responsive)

Hi everyone!

For this Build-a-Thon, I wanted to push the boundaries of the Rise 360 Code Block by creating a custom-coded matching activity.

In this challenge, learners must match product benefits with the correct item. Beyond the clean, modern look, I focused on two key pillars:

  • Dual-Interaction Accessibility: To ensure a great experience for everyone, I’ve implemented both Drag-and-Drop AND Click-to-Select functionality. This makes the activity fully accessible for users on touch devices or those who prefer clicking.
  • Gamification & Feedback: I added a dynamic progress bar and a custom CSS confetti celebration upon completion to reward the learner.

 

Check out the demo here: 

https://360.articulate.com/review/content/c1f2b467-0d8e-4b86-92db-d62456b87e7e/review

I’d love to hear your thoughts! 

5 Replies

  • I like this gamification pretty much. Maybe you would likte to share the code also?

  • Hi everyone, 

    Here's the code :

    <!DOCTYPE html>

    <html lang="fr">

    <head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <style>

    @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800&display=swap');

     

    :root {

    --carrefour-blue: #003896;

    --carrefour-green: #27ae60;

    --progress-orange: #f39c12;

    --bg-soft-green: #f2f7f2;

    --accent-light: #e8f5e9;

    }

     

    body {

    font-family: 'Inter', sans-serif;

    background-color: var(--bg-soft-green);

    margin: 0; padding: 30px 15px;

    display: flex; flex-direction: column; align-items: center;

    color: #2c3e50; overflow-x: hidden;

    -webkit-font-smoothing: antialiased;

    user-select: none;

    }

     

    h2 { font-weight: 800; font-size: 1.2rem; margin-bottom: 25px; letter-spacing: -0.02em; text-align: center; color: #1a3c1a; }

     

    .progress-container {

    width: 100%; max-width: 400px; height: 8px;

    background: #e0eadd; border-radius: 10px; margin-bottom: 40px;

    box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);

    }

    .progress-bar {

    height: 100%; width: 0%; background: var(--progress-orange);

    border-radius: 10px; transition: width 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);

    }

     

    .zones-container {

    display: grid; grid-template-columns: repeat(5, 1fr);

    gap: 12px; width: 100%; max-width: 1100px; margin-bottom: 50px;

    }

     

    .target-box {

    background: #ffffff; border-radius: 16px;

    border: 2px solid #e1e8e1; display: flex; flex-direction: column;

    transition: all 0.3s ease; cursor: pointer;

    box-shadow: 0 4px 6px rgba(0,0,0,0.02);

    }

    .target-box.selected-target { border-color: var(--carrefour-blue); background: var(--accent-light); transform: translateY(-2px); }

     

    .label-product {

    padding: 10px 5px; text-align: center; font-size: 0.7rem; font-weight: 800;

    color: var(--carrefour-blue); border-bottom: 1px solid #f2f2f2;

    text-transform: uppercase; min-height: 40px; display: flex; align-items: center; justify-content: center;

    }

     

    .drop-zone {

    height: 100px; display: flex; align-items: center; justify-content: center;

    padding: 12px; font-size: 0.65rem; color: #adb5bd; text-align: center;

    }

    .drop-zone.correct { color: #2c3e50; font-weight: 600; font-size: 0.75rem; background: #fff; border-radius: 0 0 16px 16px; }

     

    .sources-container {

    display: flex; flex-wrap: wrap; justify-content: center;

    gap: 15px; width: 100%; max-width: 900px;

    }

     

    .item {

    width: 185px; padding: 18px; border-radius: 12px; background: #ffffff;

    cursor: grab; font-size: 0.8rem; line-height: 1.5; font-weight: 500;

    box-shadow: 0 10px 15px -3px rgba(0,0,0,0.05);

    border: 1px solid transparent; touch-action: none;

    transition: all 0.3s ease;

    }

    .item:hover { transform: translateY(-3px); border-color: #d1d8d1; }

    .item.selected-item { border: 2px solid var(--progress-orange); background: #fffdfa; }

    .item.hidden { display: none !important; }

     

    .confetti {

    position: fixed; width: 8px; height: 8px; top: -10px; z-index: 9999;

    animation: fall 3s linear forwards;

    }

    @keyframes fall { to { transform: translateY(105vh) rotate(720deg); opacity: 0; } }

     

    #feedback {

    margin-top: 30px; font-weight: 800; color: var(--carrefour-green);

    display: none; text-align: center; font-size: 1.1rem;

    }

     

    @keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } }

     

    MeDia (max-width: 850px) {

    .zones-container { grid-template-columns: repeat(2, 1fr); }

    .zones-container > :last-child { grid-column: span 2; }

    .item { width: 100%; max-width: 300px; }

    }

    </style>

    </head>

    <body>

     

    <h2>🎯 Associez chaque bénéfice au bon produit</h2>

     

    <div class="progress-container">

    <div class="progress-bar" id="progressBar"></div>

    </div>

     

    <div class="zones-container">

    <div class="target-box" data-id="prod1"><div class="label-product">Œufs FQC</div><div class="drop-zone">Glisser ou cliquer</div></div>

    <div class="target-box" data-id="prod2"><div class="label-product">Pâtes Bio</div><div class="drop-zone">Glisser ou cliquer</div></div>

    <div class="target-box" data-id="prod3"><div class="label-product">Lentilles</div><div class="drop-zone">Glisser ou cliquer</div></div>

    <div class="target-box" data-id="prod4"><div class="label-product">Confiture</div><div class="drop-zone">Glisser ou cliquer</div></div>

    <div class="target-box" data-id="prod5"><div class="label-product">Pommes Bio</div><div class="drop-zone">Glisser ou cliquer</div></div>

    </div>

     

    <div class="sources-container" id="sourceBox">

    <div class="item" draggable="true" id="arg1" data-match="prod1">"Riche en Oméga 3 naturels et issu d'une filière sans OGM."</div>

    <div class="item" draggable="true" id="arg2" data-match="prod2">"Énergie longue durée grâce à un index glycémique bas."</div>

    <div class="item" draggable="true" id="arg3" data-match="prod3">"Alternative végétale riche en protéines, sans additifs."</div>

    <div class="item" draggable="true" id="arg4" data-match="prod4">"Le plaisir gourmand avec 30% de sucre en moins."</div>

    <div class="item" draggable="true" id="arg5" data-match="prod5">"Zéro résidu de pesticides pour une consommation brute."</div>

    </div>

     

    <div id="feedback">🌿 Mission réussie ! Expertise validée.</div>

     

    <script>

    let selectedItem = null;

    let solved = 0;

    const total = 5;

     

    const sourceBox = document.getElementById('sourceBox');

    for (let i = sourceBox.children.length; i >= 0; i--) {

    sourceBox.appendChild(sourceBox.children[Math.random() * i | 0]);

    }

     

    document.querySelectorAll('.item').forEach(item => {

    item.addEventListener('dragstart', (e) => {

    e.dataTransfer.setData('text/plain', e.target.id);

    selectedItem = item;

    });

    item.addEventListener('click', () => {

    document.querySelectorAll('.item').forEach(i => i.classList.remove('selected-item'));

    selectedItem = item;

    item.classList.add('selected-item');

    });

    });

     

    document.querySelectorAll('.target-box').forEach(box => {

    box.addEventListener('dragover', (e) => { e.preventDefault(); box.classList.add('selected-target'); });

    box.addEventListener('dragleave', () => box.classList.remove('selected-target'));

    box.addEventListener('drop', (e) => {

    e.preventDefault();

    box.classList.remove('selected-target');

    const id = e.dataTransfer.getData('text/plain');

    validate(document.getElementById(id), box);

    });

    box.addEventListener('click', () => { if (selectedItem) validate(selectedItem, box); });

    });

     

    function validate(item, box) {

    if (!item || box.querySelector('.drop-zone').classList.contains('correct')) return;

    if (item.getAttribute('data-match') === box.getAttribute('data-id')) {

    const dz = box.querySelector('.drop-zone');

    dz.innerText = item.innerText;

    dz.classList.add('correct');

    item.classList.add('hidden');

    solved++;

    document.getElementById('progressBar').style.width = (solved/total)*100 + "%";

    selectedItem = null;

    if(solved === total) finish();

    } else {

    item.style.animation = "shake 0.4s ease";

    setTimeout(() => item.style.animation = "", 400);

    item.classList.remove('selected-item');

    selectedItem = null;

    }

    }

     

    function finish() {

    document.getElementById('feedback').style.display = 'block';

    createConfetti();

     

    // MÉTHODE DE SIGNAL MULTI-PROTOCOLE POUR RISE 360

    const signals = [

    { type: 'livelesson-complete', value: true },

    { type: 'complete', value: true },

    { message: 'submit-answer', status: 'completed' }

    ];

     

    signals.forEach(sig => {

    window.parent.postMessage(sig, '*');

    });

     

    document.getElementById('feedback').scrollIntoView({ behavior: 'smooth' });

    }

     

    function createConfetti() {

    const colors = ['#f39c12', '#27ae60', '#003896', '#ed1c24'];

    for (let i = 0; i < 70; i++) {

    const c = document.createElement('div');

    c.className = 'confetti';

    c.style.left = Math.random() * 100 + 'vw';

    c.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];

    c.style.animationDuration = (Math.random() * 2 + 1) + 's';

    document.body.appendChild(c);

    setTimeout(() => c.remove(), 3000);

    }

    }

    </script>

    </body>

    </html>

     

    Enjoy !