<?php
|
/**
|
* WPCode Snippet 3/4 — Interview Page Shortcode + Video Upload
|
* Name: IM - Interview Page
|
* Type: PHP Snippet
|
* Location: Run everywhere
|
*
|
* Dependency: Snippets 1 & 2 must be enabled first
|
*
|
* Usage:
|
* [im_interview] → Interview page (embed in a WordPress page)
|
* Access via: /interview/?im_token=xxxxxx
|
*/
|
|
defined('ABSPATH') || exit;
|
|
/* ============================================================
|
Disable cache for interview pages
|
============================================================ */
|
add_action('template_redirect', function () {
|
if (!empty($_GET['im_token'])) {
|
nocache_headers();
|
}
|
}, 1);
|
|
/* ============================================================
|
Subject → Interview Questions Mapping
|
============================================================ */
|
function im_get_questions($candidate): array
|
{
|
$subject_ids = json_decode($candidate->subjects ?? '[]', true) ?: [];
|
if (empty($subject_ids))
|
return [];
|
|
$disciplines = [];
|
foreach ($subject_ids as $sid) {
|
if (!is_numeric($sid))
|
continue;
|
$terms = wp_get_post_terms((int) $sid, 'discipline');
|
if (!is_wp_error($terms) && !empty($terms)) {
|
foreach ($terms as $t) {
|
$order_val = get_field('order', $t);
|
if ($order_val === false || $order_val === null) {
|
$order_val = get_term_meta($t->term_id, 'order', true);
|
}
|
if (!isset($disciplines[$t->term_id])) {
|
$disciplines[$t->term_id] = [
|
'term' => $t,
|
'order' => (int) $order_val
|
];
|
}
|
}
|
}
|
}
|
|
if (empty($disciplines))
|
return [];
|
|
uasort($disciplines, function ($a, $b) {
|
return $a['order'] <=> $b['order'];
|
});
|
|
$top_disciplines = array_slice($disciplines, 0, 3, true);
|
|
$questions_to_show = [];
|
foreach ($top_disciplines as $term_id => $data) {
|
$term = $data['term'];
|
|
$q_posts = get_posts([
|
'post_type' => 'interview_question',
|
'posts_per_page' => -1,
|
'tax_query' => [
|
[
|
'taxonomy' => 'discipline',
|
'field' => 'term_id',
|
'terms' => $term_id
|
]
|
],
|
'fields' => 'ids'
|
]);
|
|
if (!empty($q_posts)) {
|
$random_q_id = $q_posts[array_rand($q_posts)];
|
$gallery = get_field('content', $random_q_id);
|
$images = [];
|
if (is_array($gallery)) {
|
foreach ($gallery as $img) {
|
if (is_string($img)) {
|
$images[] = $img;
|
} elseif (is_array($img) && !empty($img['url'])) {
|
$images[] = $img['url'];
|
}
|
}
|
}
|
if (!empty($images)) {
|
$q_items = [];
|
foreach ($images as $url) {
|
$q_items[] = ['type' => 'image', 'url' => $url];
|
}
|
$questions_to_show[$term->name] = $q_items;
|
}
|
}
|
}
|
|
return $questions_to_show;
|
}
|
|
/* ============================================================
|
Interview CSS (output once)
|
============================================================ */
|
function im_enqueue_interview_styles()
|
{
|
static $done = false;
|
if ($done)
|
return;
|
$done = true;
|
echo '<style>
|
.im-iv-wrap{max-width:820px;margin:0 auto;padding:24px 0 60px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif;color:#1f2937;font-size:16px;line-height:1.7}
|
.im-iv-wrap *,.im-iv-wrap *::before,.im-iv-wrap *::after{box-sizing:border-box}
|
.im-iv-page{background:#fff;border-radius:12px;box-shadow:0 2px 16px rgba(0,0,0,.07);overflow:hidden}
|
.im-iv-header{background:linear-gradient(135deg,#009dff,#3de1fe,#53eee0);padding:36px 40px 28px;color:#fff}
|
.im-iv-header h1{margin:0 0 8px;font-size:26px;font-weight:700}
|
.im-iv-header p{margin:0 0 16px;opacity:.9;font-size:15px}
|
.im-iv-countdown{background:rgba(255,255,255,1);border-radius:8px;padding:10px 16px;font-size:14px;display:inline-block;color: rgba(215, 0, 0, 1);}
|
.im-iv-timer{font-weight:700;color:#fff;color: rgba(215, 0, 0, 1);}
|
.im-iv-section{padding:32px 40px;border-bottom:1px solid #f3f4f6}
|
.im-iv-section:last-child{border-bottom:none}
|
.im-iv-section h2{margin:0 0 16px;font-size:18px;font-weight:700;color:#111827}
|
.im-iv-hint{color:#6b7280;font-size:14px;margin:0 0 16px}
|
.im-iv-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}
|
.im-iv-subject{background:#f9fafb;border:1px solid #e5e7eb;border-radius:10px;padding:20px 24px;margin-bottom:16px}
|
.im-iv-subject h3{margin:0 0 12px;font-size:16px;font-weight:700;color:#009dff}
|
.im-iv-subject ol{margin:0;padding-left:20px}
|
.im-iv-subject li{margin-bottom:8px;font-size:15px;color:#374151}
|
.im-iv-drop{border:2px dashed #cbd5e1;border-radius:10px;padding:40px 24px;text-align:center;cursor:pointer;background:#f8fafc;position:relative;transition:all .2s;margin-bottom:20px}
|
.im-iv-drop:hover,.im-iv-drop.drag{border-color:#009dff;background:#e6f9ff}
|
.im-iv-drop-icon{font-size:36px;color:#94a3b8;margin-bottom:10px}
|
.im-iv-drop p{margin:0;color:#64748b;font-size:15px}
|
.im-iv-drop input[type=file]{position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%}
|
.im-iv-fname{margin-top:10px!important;font-size:14px;font-weight:600;color:#009dff}
|
.im-iv-progress{display:flex;align-items:center;gap:12px;margin-bottom:16px;background:#f1f5f9;border-radius:8px;padding:12px 16px}
|
.im-iv-bar{flex:1;height:8px;background:linear-gradient(135deg,#009dff,#3de1fe,#53eee0);border-radius:4px;transition:width .3s;width:0}
|
#im-iv-pct{font-size:14px;font-weight:700;color:#009dff;min-width:36px;text-align:right}
|
.im-iv-btn{display:block;width:100%;padding:14px;background:linear-gradient(135deg,#009dff,#3de1fe,#53eee0);color:#fff;border:none;border-radius:8px;font-size:16px;font-weight:700;cursor:pointer;transition:all .3s;text-shadow:0 1px 2px rgba(0,0,0,.1);font-family:inherit}
|
.im-iv-btn:hover{background:linear-gradient(135deg,#0088e0,#30cce8,#45ddd2);transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,157,255,.3)}
|
.im-iv-btn:disabled{background:#94a3b8;cursor:not-allowed;transform:none;box-shadow:none}
|
.im-iv-center{text-align:center;padding:60px 40px}
|
.im-iv-icon{width:72px;height:72px;border-radius:50%;font-size:36px;line-height:72px;margin:0 auto 24px;font-weight:700}
|
.im-iv-icon.ok{background:#d1fae5;color:#065f46}
|
.im-iv-icon.err{background:#fee2e2;color:#991b1b}
|
.im-iv-center h1{font-size:24px;margin:0 0 12px}
|
.im-iv-center p{color:#6b7280;font-size:15px}
|
@media(max-width:600px){.im-iv-header,.im-iv-section{padding:24px 20px}}
|
</style>';
|
echo '<script>var imIvAjaxUrl="' . esc_url(admin_url('admin-ajax.php')) . '";</script>';
|
}
|
|
/* ============================================================
|
AJAX: Video Upload Handler
|
============================================================ */
|
add_action('wp_ajax_im_interview_upload', 'im_ajax_interview_upload');
|
add_action('wp_ajax_nopriv_im_interview_upload', 'im_ajax_interview_upload');
|
function im_ajax_interview_upload()
|
{
|
$token_str = sanitize_text_field($_POST['im_token'] ?? '');
|
$token_row = $token_str ? IM_Token::validate($token_str) : null;
|
if (!$token_row) {
|
wp_send_json_error(['message' => 'Invalid or expired interview link. Please refresh the page.']);
|
}
|
$candidate = IM_Candidate::get($token_row->candidate_id);
|
if (!$candidate) {
|
wp_send_json_error(['message' => 'Candidate not found.']);
|
}
|
if (!wp_verify_nonce($_POST['im_nonce'] ?? '', 'im_interview_' . $token_row->token)) {
|
wp_send_json_error(['message' => 'Security verification failed. Please refresh and try again.']);
|
}
|
$file = $_FILES['im_video'] ?? null;
|
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
|
wp_send_json_error(['message' => 'Video upload failed. Please check the file size (max 500MB) and try again.']);
|
}
|
$allowed = [
|
'video/mp4',
|
'video/quicktime',
|
'video/x-msvideo',
|
'video/avi',
|
'video/webm',
|
'video/x-ms-wmv',
|
'application/zip',
|
'application/x-zip-compressed',
|
'application/x-rar-compressed',
|
'application/vnd.rar',
|
'application/x-7z-compressed',
|
'application/gzip',
|
'application/x-tar'
|
];
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
$mime = finfo_file($finfo, $file['tmp_name']);
|
finfo_close($finfo);
|
if (!in_array($mime, $allowed, true)) {
|
wp_send_json_error(['message' => 'Supported formats: mp4, mov, avi, webm, wmv, zip, rar, 7z.']);
|
}
|
if ($file['size'] > IM_MAX_VIDEO_SIZE) {
|
wp_send_json_error(['message' => 'Video file cannot exceed 500MB. Please compress and try again.']);
|
}
|
$upload_dir = wp_upload_dir();
|
$save_dir = $upload_dir['basedir'] . '/interviews/' . $candidate->id;
|
wp_mkdir_p($save_dir);
|
file_put_contents($save_dir . '/index.html', '');
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
$filename = 'interview_' . $token_row->id . '_' . time() . '.' . $ext;
|
$dest = $save_dir . '/' . $filename;
|
if (!move_uploaded_file($file['tmp_name'], $dest)) {
|
wp_send_json_error(['message' => 'File save failed. Please contact the administrator.']);
|
}
|
IM_Token::mark_used($token_row->id, $dest, $filename);
|
IM_Candidate::update_status($candidate->id, 'completed');
|
$name = esc_html($candidate->preferred_name ?: $candidate->first_name);
|
wp_send_json_success(['message' => 'ok', 'name' => $name]);
|
}
|
|
/* ============================================================
|
Shortcode: [im_interview]
|
============================================================ */
|
add_shortcode('im_interview', function () {
|
im_enqueue_interview_styles();
|
ob_start();
|
|
$token_str = sanitize_text_field($_GET['im_token'] ?? '');
|
|
// No token
|
if (!$token_str): ?>
|
<div class="im-iv-wrap">
|
<div class="im-iv-page im-iv-center">
|
<div class="im-iv-icon err">!</div>
|
<h1>Link Expired</h1>
|
<p>This link has expired. Please fill out the Join Us form again, and we’ll send you a new application link
|
shortly.</p>
|
</div>
|
</div>
|
<?php return ob_get_clean();
|
endif;
|
|
$token_row = IM_Token::validate($token_str);
|
|
// Token invalid (used or expired)
|
if (!$token_row):
|
$used = IM_Token::get_by_token($token_str);
|
if ($used && $used->is_used): ?>
|
<div class="im-iv-wrap">
|
<div class="im-iv-page im-iv-center">
|
<div class="im-iv-icon ok">✓</div>
|
<h1>Already Submitted</h1>
|
<p>You have already submitted your interview video. No further action is needed.</p>
|
<p>Thank you for your participation!</p>
|
</div>
|
</div>
|
<?php else: ?>
|
<div class="im-iv-wrap">
|
<div class="im-iv-page im-iv-center">
|
<div class="im-iv-icon err">!</div>
|
<h1>Link Expired</h1>
|
<p>This interview link has expired. Please contact us if you need a new one.</p>
|
</div>
|
</div>
|
<?php endif;
|
return ob_get_clean();
|
endif;
|
|
// Mark opened & reset to 24h
|
if (empty($token_row->opened_at)) {
|
IM_Token::mark_opened($token_row->id);
|
$token_row = IM_Token::validate($token_str);
|
}
|
|
$candidate = IM_Candidate::get($token_row->candidate_id);
|
$questions = im_get_questions($candidate);
|
|
$name = esc_html($candidate->preferred_name ?: $candidate->first_name);
|
$nonce = wp_create_nonce('im_interview_' . $token_row->token);
|
$expires_dt = new DateTimeImmutable($token_row->expires_at, wp_timezone());
|
$expires_ms = $expires_dt->getTimestamp() * 1000;
|
|
$q_html = '';
|
foreach ($questions as $subject => $qs) {
|
$q_html .= '<div class="im-iv-subject"><h3>' . esc_html($subject) . ' Exam question</h3><div class="im-iv-gallery" style="margin-top:12px">';
|
foreach ($qs as $q) {
|
if (isset($q['type']) && $q['type'] === 'image') {
|
$q_html .= '<img src="' . esc_url($q['url']) . '" style="max-width:100%;border-radius:6px;margin-bottom:12px;display:block">';
|
}
|
}
|
$q_html .= '</div></div>';
|
}
|
if (!$q_html)
|
$q_html = '<p class="im-iv-notice">No questions available. Please contact the recruitment team.</p>';
|
?>
|
<div class="im-iv-wrap">
|
<div class="im-iv-page">
|
<div class="im-iv-header">
|
<h1>Video Interview</h1>
|
<p>Welcome, <strong><?= $name ?></strong></p>
|
<p style="opacity:.9;font-size:14px;margin:0 0 16px">Please review the questions below, record your
|
responses, and upload your video.</p>
|
<div class="im-iv-countdown" data-exp="<?= $expires_ms ?>">
|
Time remaining: <span class="im-iv-timer">—</span>
|
</div>
|
</div>
|
|
<div class="im-iv-section">
|
<h2>Interview Questions</h2>
|
<div class="im-iv-notice" style="line-height:1.8">
|
<p style="margin:0 0 8px">Please answer all questions in one video, in order. Only one upload is allowed.</p>
|
<p style="margin:0 0 8px;color:#ef4444;font-weight:700">Please record your responses in English.</p>
|
<p style="margin:0 0 8px">Walk us through your thinking as if you’re teaching a student — we’d love to see how you explain and communicate your ideas. Most candidates spend around 5–10 minutes per question, but there’s no strict time limit. Don’t worry about having perfect answers — we’re much more interested in how you think and how you teach.</p>
|
|
</div>
|
<?= $q_html ?>
|
</div>
|
|
<div class="im-iv-section">
|
<h2>Upload Interview Video</h2>
|
<p class="im-iv-hint">Supported formats: mp4, mov, avi, webm, wmv, zip, rar, 7z | Max 500MB</p>
|
<form method="post" enctype="multipart/form-data" id="im-iv-form">
|
<input type="hidden" name="im_token" value="<?= esc_attr($token_str) ?>">
|
<input type="hidden" name="im_nonce" value="<?= esc_attr($nonce) ?>">
|
<div class="im-iv-drop" id="im-iv-drop">
|
<div class="im-iv-drop-icon">▶</div>
|
<p>Click to select a video or archive file, or drag and drop here</p>
|
<input type="file" name="im_video" id="im-iv-file" accept="video/*,.zip,.rar,.7z,.gz,.tar" required>
|
<p class="im-iv-fname" id="im-iv-fname"></p>
|
</div>
|
<div class="im-iv-progress" id="im-iv-prog" style="display:none">
|
<div class="im-iv-bar" id="im-iv-bar"></div>
|
<span id="im-iv-pct">0%</span>
|
</div>
|
<button type="submit" class="im-iv-btn" id="im-iv-btn">Submit Interview Video</button>
|
</form>
|
</div>
|
</div>
|
</div>
|
|
<script>
|
(function () {
|
var exp = parseInt(document.querySelector('[data-exp]').dataset.exp);
|
function tick() {
|
var d = Math.floor((exp - Date.now()) / 1000), el = document.querySelector('.im-iv-timer');
|
if (d <= 0) { el.textContent = 'Expired'; return; }
|
el.textContent = Math.floor(d / 3600) + 'h ' + String(Math.floor(d % 3600 / 60)).padStart(2, '0') + 'm ' + String(d % 60).padStart(2, '0') + 's';
|
setTimeout(tick, 1000);
|
}
|
tick();
|
document.getElementById('im-iv-file').addEventListener('change', function () {
|
var f = this.files[0];
|
if (f) document.getElementById('im-iv-fname').textContent = 'Selected: ' + f.name + ' (' + (f.size / 1024 / 1024).toFixed(1) + ' MB)';
|
});
|
var drop = document.getElementById('im-iv-drop');
|
drop.addEventListener('dragover', function (e) { e.preventDefault(); drop.classList.add('drag'); });
|
drop.addEventListener('dragleave', function () { drop.classList.remove('drag'); });
|
drop.addEventListener('drop', function (e) {
|
e.preventDefault(); drop.classList.remove('drag');
|
var f = e.dataTransfer.files[0];
|
if (f) {
|
document.getElementById('im-iv-file').files = e.dataTransfer.files;
|
document.getElementById('im-iv-fname').textContent = 'Selected: ' + f.name + ' (' + (f.size / 1024 / 1024).toFixed(1) + ' MB)';
|
}
|
});
|
document.getElementById('im-iv-form').addEventListener('submit', function (e) {
|
e.preventDefault();
|
var btn = document.getElementById('im-iv-btn');
|
btn.disabled = true; btn.textContent = 'Uploading, please do not close this page...';
|
document.getElementById('im-iv-prog').style.display = 'flex';
|
var fd = new FormData(document.getElementById('im-iv-form'));
|
fd.append('action', 'im_interview_upload');
|
var xhr = new XMLHttpRequest();
|
xhr.open('POST', imIvAjaxUrl);
|
xhr.upload.onprogress = function (e) {
|
if (e.lengthComputable) {
|
var p = Math.round(e.loaded / e.total * 100);
|
document.getElementById('im-iv-bar').style.width = p + '%';
|
document.getElementById('im-iv-pct').textContent = p + '%';
|
}
|
};
|
xhr.onload = function () {
|
try {
|
var res = JSON.parse(xhr.responseText);
|
if (res.success) {
|
var page = document.querySelector('.im-iv-page');
|
page.className = 'im-iv-page im-iv-center';
|
page.innerHTML = '<div class="im-iv-icon ok">✓</div>'
|
+ '<h1>Submission Successful!</h1>'
|
+ '<p style="margin-bottom:0">Dear <strong>' + (res.data.name || '') + '</strong>, your interview video has been successfully uploaded.</p>'
|
+ '<p style="margin-top:10px">Our team will contact you after review. Thank you for your patience!</p>';
|
} else {
|
btn.disabled = false; btn.textContent = 'Submit Interview Video';
|
document.getElementById('im-iv-prog').style.display = 'none';
|
document.getElementById('im-iv-bar').style.width = '0';
|
document.getElementById('im-iv-pct').textContent = '0%';
|
alert(res.data.message || 'An error occurred. Please try again.');
|
}
|
} catch (ex) {
|
btn.disabled = false; btn.textContent = 'Submit Interview Video';
|
alert('An error occurred. Please try again.');
|
}
|
};
|
xhr.onerror = function () { btn.disabled = false; btn.textContent = 'Submit Interview Video'; alert('Network error. Please try again.'); };
|
xhr.send(fd);
|
});
|
})();
|
</script>
|
<?php
|
return ob_get_clean();
|
});
|