/**
* Course Manager — Frontend JS v3.0
*/
(function ($) {
'use strict';
// ── Session-based lesson navigation ─────────────────
// Navigates to a level/lesson by saving to session then reloading — no URL change
function navigateToLesson( level, lesson, callback ) {
if ( ! level && ! lesson ) {
if ( typeof callback === 'function' ) callback();
return;
}
$.ajax({
url: CM.ajax_url,
method: 'POST',
data: {
action: 'cm_navigate_lesson',
nonce: CM.nonce,
level: level || 1,
lesson: lesson || 1,
},
complete: function() {
if ( typeof callback === 'function' ) {
callback();
} else {
window.location.reload();
}
}
});
}
// ── Sidebar lesson link clicks ────────────────────────
$(document).on('click', '.cm-nav-lesson', function(e) {
e.preventDefault();
var level = parseInt( $(this).data('level') );
var lesson = parseInt( $(this).data('lesson') );
navigateToLesson( level, lesson );
});
// ── Enrollment modal ────────────────────────────────
function openEnrollModal() {
$('#cm-enroll-overlay').fadeIn(250);
}
$(document).on('click', '#cm-enroll-trigger, #cm-enroll-trigger-inline', function(e) {
e.preventDefault();
openEnrollModal();
});
$(document).on('click', '#cm-enroll-close', function() {
$('#cm-enroll-overlay').fadeOut(200);
});
$(document).on('click', '#cm-enroll-overlay', function(e) {
if ( $(e.target).is('#cm-enroll-overlay') ) $('#cm-enroll-overlay').fadeOut(200);
});
$(document).on('click', '#cm-enroll-agree', function() {
var $btn = $(this);
$btn.prop('disabled', true).text('Enrolling…');
$.ajax({
url: CM.ajax_url,
method: 'POST',
data: { action: 'cm_enroll_user', nonce: CM.nonce },
success: function(res) {
if ( res.success ) {
// Reload page — shortcode will re-render in enrolled state
window.location.reload();
} else {
alert( res.data.message || 'Something went wrong. Please try again.' );
$btn.prop('disabled', false).text('Agree & Enroll');
}
},
error: function() {
alert('Network error. Please try again.');
$btn.prop('disabled', false).text('Agree & Enroll');
}
});
});
// ── Sidebar level accordion ─────────────────────────
$(document).on('click', '.cm-level-header', function () {
var $group = $(this).closest('.cm-level-group');
if ( $group.hasClass('cm-level-locked') ) return;
var $list = $group.find('.cm-lesson-list');
var $chevron = $(this).find('.cm-level-chevron');
var isOpen = $list.hasClass('cm-level-open');
// Close all others
$('.cm-lesson-list.cm-level-open').not($list).slideUp(250).removeClass('cm-level-open');
$('.cm-level-chevron').not($chevron).text('▼');
// Toggle this one
if ( isOpen ) {
$list.slideUp(250).removeClass('cm-level-open');
$chevron.text('▼');
} else {
$list.slideDown(250).addClass('cm-level-open');
$chevron.text('▲');
}
});
// ── Take Quiz button ────────────────────────────────
$(document).on('click', '#cm-take-quiz-btn', function () {
var $btn = $(this);
// Guest on gated lesson — show login popup
if ( $btn.data('logged-in') === 0 || $btn.data('logged-in') === '0' ) {
$('#cm-login-overlay').fadeIn(250);
return;
}
var $section = $('#cm-quiz-section');
if ( $section.hasClass('cm-quiz-open') ) {
$section.removeClass('cm-quiz-open');
$btn.html( $btn.data('original') || $btn.html() );
} else {
if ( ! $btn.data('original') ) $btn.data('original', $btn.html());
$btn.html('▲ Hide Quiz');
$section.addClass('cm-quiz-open');
$('html, body').animate({ scrollTop: $section.offset().top - 80 }, 400);
}
});
// ── Login popup close ───────────────────────────────
$(document).on('click', '#cm-login-modal-close', function () {
$('#cm-login-overlay').fadeOut(200);
});
$(document).on('click', '#cm-login-overlay', function (e) {
if ( $(e.target).is('#cm-login-overlay') ) $('#cm-login-overlay').fadeOut(200);
});
$(document).on('keydown', function (e) {
if ( e.key === 'Escape' ) $('#cm-login-overlay').fadeOut(200);
});
// ── No-quiz Complete Lesson button ─────────────────
$(document).on('click', '.cm-no-quiz-complete', function () {
var $btn = $(this);
var lessonId = $btn.data('lesson-id');
var nextUrl = $btn.data('next-url') || '';
var isLast = $btn.data('is-last-in-level') === 1 || $btn.data('is-last-in-level') === '1';
var currentLevel = $btn.data('current-level') || '';
var nextLevel = $btn.data('next-level') || '';
var alreadyDone = $btn.data('already-done') === 1 || $btn.data('already-done') === '1';
function showLevelCompleteOrRedirect() {
var navLevel = parseInt( $btn.data('nav-level') || 0 );
var navLesson = parseInt( $btn.data('nav-lesson') || 0 );
if ( isLast ) {
$('#cm-level-complete-overlay')
.data('nav-level', navLevel)
.data('nav-lesson', navLesson)
.data('level-title', currentLevel)
.data('next-level', nextLevel)
.data('quiz-passed', 0)
.data('quiz-total', 0);
showLevelCompleteModal( navLevel, navLesson, currentLevel, nextLevel, 0, 0 );
} else if ( navLevel ) {
navigateToLesson( navLevel, navLesson );
}
}
if ( alreadyDone ) {
showLevelCompleteOrRedirect();
return;
}
// Mark lesson done: confirmation animation → AJAX → lock → navigate
$btn.prop('disabled', true)
.removeClass('cm-btn-active-complete')
.addClass('cm-btn-confirming')
.html('✔ Saving…');
$.ajax({
url: CM.ajax_url,
method: 'POST',
data: {
action: 'cm_complete_lesson',
nonce: CM.nonce,
lesson_id: lessonId,
},
success: function () {
// Brief "done" state before navigating
$btn.removeClass('cm-btn-confirming')
.addClass('cm-btn-done')
.html('✅ Lesson Complete');
// Advance progress bar
advanceProgressBar();
// Short pause so user sees the locked state, then navigate/modal
setTimeout(function() {
showLevelCompleteOrRedirect();
}, 800);
},
error: function () {
$btn.prop('disabled', false)
.removeClass('cm-btn-confirming')
.addClass('cm-btn-active-complete')
.text('Mark Lesson Complete');
alert('Something went wrong. Please try again.');
},
});
});
// ── Advance master tracker + sidebar bar ────────────
function advanceProgressBar() {
// 1. Master tracker dots
var $current = $('.cm-mpt-dot--current');
if ( $current.length ) {
$current.removeClass('cm-mpt-dot--current').addClass('cm-mpt-dot--done');
$current.find('.cm-mpt-dot-inner').text('✓');
$current.next('.cm-mpt-connector').addClass('cm-mpt-connector--done');
var $row = $current.closest('.cm-mpt-row');
var $badge = $row.find('.cm-mpt-badge');
var doneCnt = $row.find('.cm-mpt-dot--done').length;
var totCnt = $row.find('.cm-mpt-dot').length;
if ( doneCnt === totCnt ) {
$row.removeClass('cm-mpt-row--active').addClass('cm-mpt-row--done');
var levelTitle = $row.find('.cm-mpt-level-label').text();
$badge.addClass('cm-mpt-badge--done').html('✅ ' + levelTitle + ' Complete');
if ( $('.cm-mpt-row').length === $('.cm-mpt-row--done').length ) {
$('.cm-mpt-final').addClass('cm-mpt-final--done');
}
} else {
$badge.find('.cm-mpt-badge-progress').text( doneCnt + '/' + totCnt );
}
}
// 2. Sidebar progress bar
var $wrap = $('.cm-progress-wrap[data-total]');
var $bar = $wrap.find('.cm-progress-bar-fill');
var $label = $wrap.find('.cm-progress-label');
if ( $wrap.length ) {
var total = parseInt( $wrap.data('total') ) || 0;
var passed = parseInt( $wrap.data('passed') ) || 0;
var newPassed = passed + 1;
var newPct = total > 0 ? Math.min( 100, Math.round( (newPassed / total) * 100 ) ) : 0;
$bar.css('width', newPct + '%');
$label.text( newPassed + '/' + total + ' completed' );
$wrap.data('passed', newPassed);
}
}
// Points config: map level position → points awarded
// This mirrors CM_WPLOYALTY_LEVEL_CAMPAIGNS points in rewards.php
var CM_LEVEL_POINTS = CM.level_points || { 1: 500, 2: 1000, 3: 1500, 4: 2000 };
// ── Shared level-complete modal launcher ────────────
function showLevelCompleteModal( navLevel, navLesson, levelTitle, nextLevel, qPassed, qTotal ) {
var isCourse = ( !navLevel );
var currentLevelPos = parseInt( $('#cm-level-complete-overlay').data('current-level-pos') || 0 );
$('#cm-lc-title').text( levelTitle );
var scoreHtml = '';
if ( qTotal > 0 ) {
var pct = Math.round( (qPassed / qTotal) * 100 );
scoreHtml = '
' + qPassed + '/' + qTotal + 'Quizzes Passed
';
scoreHtml += '' + pct + '%Level Score
';
} else {
scoreHtml = '✅All Lessons Completed
';
}
$('#cm-lc-score').html( scoreHtml );
var $btn = $('#cm-lc-next-btn');
// All buttons go through points popup first via cm-lc-show-points class
$btn.removeClass('cm-lc-course-done cm-lc-show-points');
if ( isCourse ) {
$btn.text('🎓 Course Complete!').addClass('cm-lc-course-done')
.data('nav-level', 0).data('nav-lesson', 0)
.data('current-level-pos', currentLevelPos);
$('#cm-lc-badge').text('🎓');
} else if ( nextLevel ) {
$btn.text('Start ' + nextLevel + ' →').addClass('cm-lc-show-points')
.data('nav-level', navLevel).data('nav-lesson', navLesson)
.data('current-level-pos', currentLevelPos);
} else {
$btn.text('Continue →').addClass('cm-lc-show-points')
.data('nav-level', navLevel).data('nav-lesson', navLesson)
.data('current-level-pos', currentLevelPos);
}
$('#cm-level-complete-overlay').fadeIn(300);
launchConfetti();
}
// ── Inline continue button (same-level, no modal) ─────
$(document).on('click', '.cm-nav-continue', function(e) {
e.preventDefault();
var navLevel = parseInt( $(this).data('nav-level') || 0 );
var navLesson = parseInt( $(this).data('nav-lesson') || 0 );
navigateToLesson( navLevel, navLesson );
});
// ── Option selection ────────────────────────────────
$(document).on('click', '.cm-option-label', function () {
var $q = $(this).closest('.cm-question');
$q.find('.cm-option-label').removeClass('cm-selected');
$(this).addClass('cm-selected');
$(this).find('.cm-option-radio').prop('checked', true);
});
// ── Quiz submission ─────────────────────────────────
$(document).on('submit', '#cm-quiz-form', function (e) {
e.preventDefault();
var $form = $(this);
var lessonId = $form.data('lesson-id');
var total = parseInt( $form.data('total') );
var nextUrl = $form.data('next-url') || '';
var navLevel = parseInt( $form.data('nav-level') || 0 );
var navLesson = parseInt( $form.data('nav-lesson') || 0 );
var $btn = $('#cm-submit-btn');
var answers = {};
for ( var i = 0; i < total; i++ ) {
answers[i] = $form.find('input[name="cm_q_' + i + '"]:checked').val() || '';
}
// Validate all answered
var unanswered = 0;
for ( var j = 0; j < total; j++ ) { if ( ! answers[j] ) unanswered++; }
if ( unanswered > 0 ) {
showInlineError('Please answer all ' + unanswered + ' remaining question(s) before submitting.');
return;
}
$btn.prop('disabled', true).html(' Checking…');
$.ajax({
url: CM.ajax_url,
method: 'POST',
data: {
action: 'cm_submit_quiz',
nonce: CM.nonce,
lesson_id: lessonId,
answers: answers,
},
success: function (res) {
if ( res.success ) {
renderResults( res.data, nextUrl );
lockForm();
} else {
showInlineError( res.data.message || 'Something went wrong. Please try again.' );
$btn.prop('disabled', false).text('Submit Quiz');
}
},
error: function () {
showInlineError('Network error. Please check your connection and try again.');
$btn.prop('disabled', false).text('Submit Quiz');
},
});
});
// ── Render results ──────────────────────────────────
function renderResults( data, nextUrl ) {
var $results = $('#cm-quiz-results');
var $section = $('#cm-quiz-section');
var passed = data.passed;
var score = data.correct + '/' + data.total + ' (' + data.score_pct + '%)';
var isLastInLevel = $section.data('is-last-in-level') === 1 || $section.data('is-last-in-level') === '1';
var currentLevel = $section.data('current-level') || '';
var nextLevel = $section.data('next-level') || '';
var levelQuizPassed = parseInt( $section.data('level-quiz-passed') ) || 0;
var levelQuizTotal = parseInt( $section.data('level-quiz-total') ) || 0;
// Store results for Reveal Answers
$('#cm-quiz-form').data('results', data.results);
// Mark questions right/wrong (no option highlight yet)
$.each(data.results, function (idx, r) {
$('.cm-question[data-index="' + idx + '"]').addClass( r.is_correct ? 'cm-correct-q' : 'cm-wrong-q' );
});
var html = '';
if ( passed ) {
html += '✅ You Passed!
';
html += 'You scored ' + score + '. Great work!
';
} else {
html += '❌ Not Quite…
';
html += 'You scored ' + score + '. You need at least ' + data.passing_pct + '% to pass.
';
}
html += '';
if ( passed && isLastInLevel ) {
html += '';
} else if ( passed && navLevel ) {
html += '';
} else {
html += '';
}
html += '';
html += '
';
$results.html(html)
.addClass( passed ? 'cm-result-pass' : 'cm-result-fail' )
.slideDown(400);
$('html, body').animate({ scrollTop: $results.offset().top - 100 }, 500);
$('.cm-quiz-footer').slideUp(300);
// Store level context for the modal
if ( passed && isLastInLevel ) {
$('#cm-level-complete-overlay')
.data('nav-level', navLevel)
.data('nav-lesson', navLesson)
.data('level-title', currentLevel)
.data('next-level', nextLevel)
.data('quiz-passed', levelQuizPassed + ( data.passed ? 1 : 0 ) )
.data('quiz-total', levelQuizTotal);
}
}
// ── Level complete modal trigger (quiz path) ────────
$(document).on('click', '#cm-continue-level-btn', function () {
var $overlay = $('#cm-level-complete-overlay');
var navLevel = parseInt( $(this).data('nav-level') || $overlay.data('nav-level') || 0 );
var navLesson = parseInt( $(this).data('nav-lesson') || $overlay.data('nav-lesson') || 0 );
showLevelCompleteModal(
navLevel,
navLesson,
$overlay.data('level-title') || '',
$overlay.data('next-level') || '',
$overlay.data('quiz-passed') || 0,
$overlay.data('quiz-total') || 0
);
});
// ── Points awarded popup ─────────────────────────────
// Points map: level position -> points (must match rewards.php config)
var cmLevelPoints = CM.level_points || { 1: 500, 2: 1000, 3: 1500, 4: 2000 };
function showPointsPopup( currentLevelPos, navLevel, navLesson, afterCallback ) {
var pts = cmLevelPoints[ currentLevelPos ] || 0;
// Check if this level was already rewarded (repeat completion)
var rewardedMap = {};
try {
rewardedMap = JSON.parse( $('#cm-level-complete-overlay').data('level-rewarded') || '{}' );
} catch(e) {}
var alreadyRewarded = rewardedMap[ currentLevelPos ] === true;
if ( ! pts || alreadyRewarded ) {
// Show "first time only" message instead of points amount
$('#cm-pts-amount').hide();
$('#cm-pts-icon').text('ℹ️');
$('#cm-pts-label').text('Heads Up');
$('#cm-pts-msg').text(
'Reward points are earned for the first time only. ' +
'You've already earned your points for this level.'
).css('color', '#374151');
$('#cm-pts-close').text('Got it');
$('#cm-points-overlay').fadeIn(300);
// No confetti for repeat completions
$('#cm-pts-close').data('callback', afterCallback);
return;
}
// First-time completion — show points awarded
$('#cm-pts-amount').show().text( '+' + pts.toLocaleString() );
$('#cm-pts-icon').text('🎉');
$('#cm-pts-label').text('Points Awarded!');
$('#cm-pts-msg').text( pts.toLocaleString() + ' points added to your rewards balance' ).css('color', '#6b7280');
$('#cm-pts-close').text('Awesome!');
$('#cm-points-overlay').fadeIn(300);
launchPointsConfetti();
// Store callback for dismiss
$('#cm-pts-close').data('callback', afterCallback);
}
$(document).on('click', '#cm-pts-close', function() {
$('#cm-points-overlay').fadeOut(200, function() {
var cb = $('#cm-pts-close').data('callback');
if ( typeof cb === 'function' ) cb();
});
});
function launchPointsConfetti() {
var $c = $('#cm-points-overlay .cm-pts-confetti');
if ( ! $c.length ) {
$c = $('');
$('#cm-points-overlay').prepend($c);
}
$c.empty();
var colors = ['#fbbf24','#f87171','#34d399','#60a5fa','#c084fc','#fff','#36604e'];
for ( var i = 0; i < 60; i++ ) {
var $p = $('');
$p.css({
'--x': ( Math.random() * 200 - 100 ) + 'vw',
'--y': ( Math.random() * -120 - 20 ) + 'px',
'--rot': ( Math.random() * 720 ) + 'deg',
'--delay': ( Math.random() * 0.5 ) + 's',
'--size': ( Math.random() * 8 + 6 ) + 'px',
'background': colors[ Math.floor( Math.random() * colors.length ) ],
'left': ( Math.random() * 100 ) + '%',
});
$c.append($p);
}
}
// Patch: after level modal fades in, hook the next button to show points first
// We use a delegated pre-handler on the overlay button
$(document).on('click', '.cm-lc-show-points', function(e) {
e.preventDefault();
var navLevel = parseInt( $(this).data('nav-level') || 0 );
var navLesson = parseInt( $(this).data('nav-lesson') || 0 );
var currentLvlPos = parseInt( $(this).data('current-level-pos') || 0 );
$('#cm-level-complete-overlay').fadeOut(200, function() {
showPointsPopup( currentLvlPos, navLevel, navLesson, function() {
if ( navLevel ) {
navigateToLesson( navLevel, navLesson );
} else {
// Course complete — show course modal
var now = new Date();
var opts = { year: 'numeric', month: 'long', day: 'numeric' };
$('#cm-course-complete-date').text( now.toLocaleDateString( undefined, opts ) );
$('#cm-course-complete-overlay').fadeIn(300);
launchCourseConfetti();
}
});
});
});
// ── Course completion modal ─────────────────────────
$(document).on('click', '.cm-lc-course-done', function(e) {
e.preventDefault();
// Build date string
var now = new Date();
var opts = { year: 'numeric', month: 'long', day: 'numeric' };
var dateStr = now.toLocaleDateString( undefined, opts );
$('#cm-course-complete-date').text( dateStr );
// Hide level modal, show course modal
$('#cm-level-complete-overlay').fadeOut(200, function() {
$('#cm-course-complete-overlay').fadeIn(300);
launchCourseConfetti();
});
});
// Close course complete overlay
$(document).on('click', '#cm-course-complete-close', function() {
$('#cm-course-complete-overlay').fadeOut(200);
});
$(document).on('click', '#cm-course-complete-overlay', function(e) {
if ( $(e.target).is('#cm-course-complete-overlay') ) {
$('#cm-course-complete-overlay').fadeOut(200);
}
});
// ── Course confetti (longer, denser burst) ───────────
function launchCourseConfetti() {
var $container = $('#cm-course-confetti');
$container.empty();
var colors = ['#36604e','#a8d5b5','#fbbf24','#f87171','#60a5fa','#c084fc','#fff','#fb923c'];
for ( var i = 0; i < 120; i++ ) {
var $p = $('');
var shape = Math.random() > 0.5 ? '50%' : '2px';
$p.css({
'--x': ( Math.random() * 220 - 110 ) + 'vw',
'--y': ( Math.random() * -160 - 20 ) + 'px',
'--rot': ( Math.random() * 1080 ) + 'deg',
'--delay': ( Math.random() * 1.0 ) + 's',
'--size': ( Math.random() * 10 + 6 ) + 'px',
'background': colors[ Math.floor( Math.random() * colors.length ) ],
'left': ( Math.random() * 100 ) + '%',
'border-radius': shape,
'animation-duration': ( Math.random() * 1 + 2 ) + 's',
});
$container.append($p);
}
}
// ── Confetti ────────────────────────────────────────
function launchConfetti() {
var $container = $('#cm-lc-confetti');
$container.empty();
var colors = ['#36604e','#a8d5b5','#fbbf24','#f87171','#60a5fa','#c084fc','#fff'];
for ( var i = 0; i < 72; i++ ) {
var $p = $('');
$p.css({
'--x': ( Math.random() * 200 - 100 ) + 'vw',
'--y': ( Math.random() * -120 - 20 ) + 'px',
'--rot': ( Math.random() * 720 ) + 'deg',
'--delay': ( Math.random() * 0.6 ) + 's',
'--size': ( Math.random() * 8 + 6 ) + 'px',
'background': colors[ Math.floor( Math.random() * colors.length ) ],
'left': ( Math.random() * 100 ) + '%',
});
$container.append($p);
}
}
// ── Reveal / Hide answers ───────────────────────────
$(document).on('click', '#cm-reveal-btn', function () {
var results = $('#cm-quiz-form').data('results');
if ( ! results ) return;
$.each(results, function (idx, r) {
$('.cm-question[data-index="' + idx + '"]').find('.cm-option-label').each(function () {
var val = $(this).find('.cm-option-radio').val();
if ( val === r.correct ) $(this).addClass('cm-answer-correct');
if ( val === r.selected && ! r.is_correct ) $(this).addClass('cm-answer-wrong');
});
});
$(this).text('▲ Hide Answers').attr('id', 'cm-hide-btn');
});
$(document).on('click', '#cm-hide-btn', function () {
$('.cm-option-label').removeClass('cm-answer-correct cm-answer-wrong');
$(this).text('👁 Reveal Answers').attr('id', 'cm-reveal-btn');
});
// ── Retry ───────────────────────────────────────────
$(document).on('click', '#cm-retry-btn', function () {
$('#cm-quiz-form')[0].reset();
$('.cm-option-label').removeClass('cm-selected cm-answer-correct cm-answer-wrong');
$('.cm-question').removeClass('cm-correct-q cm-wrong-q');
$('#cm-quiz-form').removeData('results');
$('#cm-quiz-results').slideUp(300, function () {
$(this).html('').removeClass('cm-result-pass cm-result-fail');
});
$('.cm-quiz-footer').slideDown(300);
$('#cm-submit-btn').prop('disabled', false).text('Submit Quiz');
$('html, body').animate({ scrollTop: $('#cm-quiz-section').offset().top - 80 }, 400);
});
// ── Lock form after submission ──────────────────────
function lockForm() {
$('#cm-quiz-form .cm-option-radio').prop('disabled', true);
$('#cm-quiz-form .cm-option-label').css('cursor', 'default');
}
// ── Inline error ────────────────────────────────────
function showInlineError(msg) {
var $err = $('#cm-inline-error');
if ( ! $err.length ) {
$err = $('');
$('.cm-quiz-footer').append($err);
}
$err.text(msg).hide().fadeIn(300);
setTimeout(function () { $err.fadeOut(400); }, 4000);
}
}(jQuery));