| | |
| | | ============================================================ */ |
| | | add_action('template_redirect', function () { |
| | | if (!empty($_GET['im_training_token'])) { |
| | | if (!defined('DONOTCACHEPAGE')) { |
| | | define('DONOTCACHEPAGE', true); |
| | | } |
| | | nocache_headers(); |
| | | header('Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0'); |
| | | header('Pragma: no-cache'); |
| | | } |
| | | }, 1); |
| | | |
| | |
| | | .im-tr-section h2 .im-tr-step{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;background:linear-gradient(135deg,#009dff,#3de1fe);color:#fff;border-radius:50%;font-size:14px;font-weight:700;flex-shrink:0} |
| | | .im-tr-video-wrap{position:relative;width:100%;padding-bottom:56.25%;border-radius:10px;overflow:hidden;background:#0f172a;margin-bottom:20px} |
| | | .im-tr-video-wrap iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:none} |
| | | .im-tr-dl-btn{display:inline-flex;align-items:center;gap:8px;background:linear-gradient(135deg,#009dff,#3de1fe,#53eee0);color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-size:15px;font-weight:700;transition:all .3s;border:none;cursor:pointer;font-family:inherit} |
| | | .im-tr-dl-btn:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,157,255,.3);color:#fff} |
| | | .im-tr-dl-section{display:block} |
| | | .im-tr-dl-btn{display:inline-flex;align-items:center;justify-content:center;gap:12px;background:linear-gradient(135deg,#009dff,#3de1fe,#53eee0);color:#fff;padding:18px 36px;border-radius:12px;text-decoration:none;font-size:18px;font-weight:700;min-height:56px;box-shadow:0 4px 14px rgba(0,157,255,.35);transition:all .3s;border:none;cursor:pointer;font-family:inherit;max-width:100%;box-sizing:border-box;word-break:break-word;text-align:center} |
| | | .im-tr-dl-btn:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,157,255,.45);color:#fff} |
| | | .im-tr-dl-btn svg{flex-shrink:0} |
| | | .im-tr-quiz{margin-top:28px;border-top:2px solid #e5e7eb;padding-top:24px} |
| | | .im-tr-quiz-title{font-size:16px;font-weight:700;color:#009dff;margin:0 0 20px;display:flex;align-items:center;gap:8px} |
| | | .im-tr-q{background:#f9fafb;border:1px solid #e5e7eb;border-radius:10px;padding:20px 24px;margin-bottom:16px;transition:border-color .3s} |
| | |
| | | .im-tr-center h1{font-size:24px;margin:0 0 12px} |
| | | .im-tr-center p{color:#6b7280;font-size:15px;margin-bottom:5px;} |
| | | .im-tr-notice{background:#e6f9ff;border-left:4px solid #009dff;padding:12px 16px;border-radius:0 8px 8px 0;color:#007acc;font-size:14px;margin-bottom:20px} |
| | | @media(max-width:600px){.im-tr-header,.im-tr-section{padding:24px 20px}.im-tr-q{padding:16px 18px}} |
| | | @media(max-width:600px){.im-tr-header,.im-tr-section{padding:24px 20px}.im-tr-q{padding:16px 18px}.im-tr-dl-section{width:100%}.im-tr-dl-btn{width:100%;padding:15px 18px;font-size:16px;min-height:52px;gap:10px;line-height:1.35}.im-tr-dl-btn svg{width:22px;height:22px}} |
| | | @keyframes imTrFadeIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}} |
| | | .im-tr-fade-in{animation:imTrFadeIn .5s ease} |
| | | @keyframes imTrSpin{to{transform:rotate(360deg)}} |
| | |
| | | .im-tr-btn-loading::after{content:"";position:absolute;width:20px;height:20px;top:50%;left:50%;margin:-10px 0 0 -10px;border:3px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:imTrSpin .6s linear infinite} |
| | | </style>'; |
| | | echo '<script>var imTrAjaxUrl="' . esc_url(admin_url('admin-ajax.php')) . '";</script>'; |
| | | } |
| | | |
| | | /* ============================================================ |
| | | AJAX: Fresh nonce (avoids stale nonce from full-page cache / long sessions) |
| | | ============================================================ */ |
| | | add_action('wp_ajax_im_training_refresh_nonce', 'im_ajax_training_refresh_nonce'); |
| | | add_action('wp_ajax_nopriv_im_training_refresh_nonce', 'im_ajax_training_refresh_nonce'); |
| | | function im_ajax_training_refresh_nonce() |
| | | { |
| | | $token = sanitize_text_field($_POST['im_training_token'] ?? ''); |
| | | if (!$token || !IM_Candidate::get_by_training_token($token)) { |
| | | wp_send_json_error(['message' => 'Invalid or expired training link.']); |
| | | } |
| | | wp_send_json_success(['nonce' => wp_create_nonce('im_training_' . $token)]); |
| | | } |
| | | |
| | | /* ============================================================ |
| | |
| | | endif; |
| | | |
| | | $name = esc_html($candidate->preferred_name ?: $candidate->first_name); |
| | | $nonce = wp_create_nonce('im_training_' . $token_str); |
| | | $total = count($training_posts); |
| | | |
| | | // Build training data for JS |
| | |
| | | <?php if ($td['download_url']): ?> |
| | | <div class="im-tr-dl-section im-tr-hidden" id="im-tr-dl-<?= $idx ?>"> |
| | | <a href="<?= esc_url($td['download_url']) ?>" target="_blank" class="im-tr-dl-btn"> |
| | | <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| | | <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| | | <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3" /> |
| | | </svg> |
| | | <?= esc_html($td['download_btn_text']) ?> |
| | |
| | | <input type="radio" name="im_tr_q_<?= $idx ?>_<?= $qi ?>" value="<?= esc_attr($key) ?>"> |
| | | <span class="im-tr-ck"></span> |
| | | <span class="im-tr-letter"><?= $letter ?>.</span> |
| | | <span><?= esc_html($q[$key]) ?></span> |
| | | <span><?= wp_kses_post(nl2br(esc_html((string) ($q[$key] ?? '')))) ?></span> |
| | | </label> |
| | | <?php endforeach; ?> |
| | | <div class="im-tr-q-feedback" id="im-tr-fb-<?= $idx ?>-<?= $qi ?>"></div> |
| | |
| | | document.getElementById('im-tr-progress-text').textContent = 'Progress: ' + completed + ' / ' + TOTAL; |
| | | } |
| | | |
| | | function submitTrainingComplete() { |
| | | var fd = new FormData(); |
| | | fd.append('action', 'im_training_complete'); |
| | | fd.append('im_training_token', '<?= esc_js($token_str) ?>'); |
| | | fd.append('im_nonce', '<?= esc_js($nonce) ?>'); |
| | | var imTrToken = '<?= esc_js($token_str) ?>'; |
| | | |
| | | fetch(imTrAjaxUrl, { method: 'POST', body: fd }) |
| | | function submitTrainingComplete() { |
| | | var refresh = new FormData(); |
| | | refresh.append('action', 'im_training_refresh_nonce'); |
| | | refresh.append('im_training_token', imTrToken); |
| | | |
| | | fetch(imTrAjaxUrl, { method: 'POST', body: refresh, credentials: 'same-origin' }) |
| | | .then(function (r) { return r.json(); }) |
| | | .then(function (nr) { |
| | | if (!nr.success || !nr.data || !nr.data.nonce) { |
| | | alert((nr.data && nr.data.message) || 'Security check failed. Please refresh the page and try again.'); |
| | | return; |
| | | } |
| | | var fd = new FormData(); |
| | | fd.append('action', 'im_training_complete'); |
| | | fd.append('im_training_token', imTrToken); |
| | | fd.append('im_nonce', nr.data.nonce); |
| | | |
| | | return fetch(imTrAjaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' }); |
| | | }) |
| | | .then(function (r) { return r ? r.json() : null; }) |
| | | .then(function (res) { |
| | | if (!res) return; |
| | | if (res.success) { |
| | | var page = document.getElementById('im-tr-page'); |
| | | page.innerHTML = '<div class="im-tr-center im-tr-fade-in">' |