batch 1 update style & dahsboard for rec.nurinda.co.id

This commit is contained in:
dirga committed 2026-10-14 13:52:12 +07:00
1 parent 9107ffb158
commit f39aa60384
18485 files changed
+843 -2881869

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
.candidate-body{background:#f8f8f5}.candidate-header{max-width:1200px;margin:auto;padding:22px 30px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--line)}.candidate-user{display:flex;gap:20px;align-items:center;font-size:12px;color:#7a8590}.candidate-content{max-width:1160px;margin:auto;padding:40px 30px 60px}.intro{font-size:13px;margin-top:10px}.candidate-footer{text-align:center;font-size:10px;color:#9099a1;padding:20px}.candidate-body .hero{padding:38px 40px}.journey-progress{display:flex;align-items:center;flex-direction:column;width:200px;gap:14px;flex-shrink:0}.journey-progress strong{font-size:70px;line-height:1;letter-spacing:-4px;color:#4d4222}.journey-progress strong span{font-size:26px;color:#a18e47;letter-spacing:-1px}.journey-progress small{letter-spacing:1.5px;font-size:9px;color:#9a8240}progress{appearance:none;width:100%;height:6px;border:0;border-radius:10px;overflow:hidden;background:#e8e7df}progress::-webkit-progress-bar{background:#e8e7df}progress::-webkit-progress-value{background:#d9ac00;border-radius:10px}progress::-moz-progress-bar{background:#d9ac00}.phase-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:17px;margin-top:24px}.phase-card{padding:22px;margin-bottom:24px}.phase-current{border-color:#ead36b;box-shadow:0 0 0 2px #fff6cf}.phase-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:22px}.phase-number{display:grid;place-items:center;width:34px;height:34px;border-radius:9px;background:#fff6ca;color:#967419;font-size:12px;font-weight:650}.phase-card h2{font-size:15px}.phase-card p{font-size:11px;color:var(--muted);margin-top:9px}.phase-card p span{margin:0 6px}.candidate-grid{display:grid;grid-template-columns:1.6fr 1fr;gap:24px}.instructions,.candidate-info{padding:28px}.instructions ol{padding-left:24px;margin:26px 0 0}.instructions li{padding:0 0 24px 10px;font-size:12px;color:#7d8690}.instructions li:last-child{padding-bottom:0}.instructions li strong{display:block;color:#405064;font-size:13px;font-weight:600}.instructions li span{display:block;margin-top:7px;line-height:1.8}.candidate-info h2{margin:16px 0 22px}.candidate-info dl{margin:0}.candidate-info dt{font-size:10px;color:var(--muted);margin-top:16px}.candidate-info dd{font-size:13px;margin:5px 0 0;overflow-wrap:anywhere}.candidate-info>p{font-size:11px;border-top:1px solid var(--line);padding-top:20px;margin-top:22px}.exam-heading{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-bottom:22px}.timer{padding:13px 24px;background:white;border:1px solid #e8d78a;border-radius:10px;min-width:140px;text-align:center;position:sticky;top:10px}.timer small{display:block;color:#9a8a4f;font-size:9px;letter-spacing:1.5px}.timer strong{display:block;font-size:27px;letter-spacing:-.6px;font-variant-numeric:tabular-nums}.timer-warning{background:#fff2ec;border-color:#dc987a;color:#ad5135}.exam-progress{display:flex;align-items:center;gap:7px;font-size:11px;color:#8c9299;margin:0 0 25px}.exam-progress progress{max-width:180px;margin-left:auto}.question-card{padding:27px 30px}.question-meta{display:flex;align-items:center;gap:13px;margin-bottom:18px}.question-meta .eyebrow{margin:0}.question-prompt{font-size:16px;font-weight:550;line-height:1.85;letter-spacing:0;white-space:normal;overflow-wrap:anywhere}.question-card label{margin:20px 0 0}.question-card input[type=text]{max-width:640px;background:#fcfcfa}.choice-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:15px}.choice-grid .choice{display:flex;gap:12px;align-items:flex-start;border:1px solid var(--line);padding:17px;margin:0;border-radius:8px;cursor:pointer;font-size:12px;line-height:1.9;font-weight:400}.choice:hover{background:#fffdf3}.choice:has(input:checked){background:#fff7ce;border-color:#d6b536}.choice input[type=radio]{width:17px;height:17px;min-height:0;margin:4px 0 0;accent-color:#ac8200;flex-shrink:0}.choice span b{display:inline-block;background:#fff2b7;border-radius:4px;padding:0 7px;margin-right:8px}.candidate-body .save-bar{bottom:15px}.candidate-body .auth-main>.notice{width:100%;margin:0 auto 18px}.candidate-body .auth-main{display:flex;flex-direction:column;justify-content:center}.candidate-body .auth-layout{flex-shrink:0}.candidate-body .hero h2{font-size:31px}@media(max-width:900px){.phase-grid{grid-template-columns:1fr 1fr}.candidate-grid{grid-template-columns:1fr}.journey-progress{width:140px}.candidate-body .hero{padding:28px}.candidate-body .hero h2{font-size:27px}}@media(max-width:600px){.candidate-header{padding:18px;gap:10px}.candidate-user>span{display:none}.candidate-header .brand{font-size:13px}.candidate-content{padding:28px 18px 40px}.heading{align-items:flex-start;flex-direction:column}.journey-progress{display:none}.candidate-body .hero h2{font-size:27px}.phase-grid{gap:10px}.phase-card{padding:16px;margin-bottom:8px}.phase-top{margin-bottom:18px;gap:3px}.phase-top .badge{font-size:8px;padding:4px 6px}.phase-Line truncated
.integrity-notice{margin-top:20px;font-size:12px;background:#fff9df;border-color:#eee0a5;color:#79652f}.draft-status{color:#6a816d;font-size:11px;margin-bottom:20px}.draft-status.sync-warning{color:#aa5738;font-weight:600}
+67
View File
@@ -0,0 +1,67 @@
const exam = document.querySelector('#exam-form');
if (exam) {
const questions = [...document.querySelectorAll('[data-question]')];
const status = document.querySelector('#exam-status');
const timer = document.querySelector('[data-remaining]');
let end = performance.now() + Number(timer.dataset.remaining) * 1000;
let queue = Promise.resolve(), closed = false, submitting = false, dirty = false, revision = 0, pending = 0;
const say = (message, warning = false) => { status.textContent = message; status.classList.toggle('sync-warning', warning); };
const updateProgress = () => {
const count = questions.filter(q => [...q.querySelectorAll('input:not([type=hidden])')].some(i => i.type === 'radio' ? i.checked : i.value.trim())).length;
document.querySelector('#answered-count').textContent = count;
document.querySelector('#answer-progress').value = count;
};
const freeze = () => { closed = true; exam.querySelectorAll('input:not([type=hidden]),button').forEach(i => { i.disabled = true; }); };
const send = async (action, eventType = '') => {
if (closed) return;
const snapshotRevision = revision;
const data = new FormData(exam); data.set('action', action);
if (eventType) data.set('event', eventType);
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
let response;
try { response = await fetch(exam.action, {method:'POST', body:data, credentials:'same-origin', cache:'no-store', signal:controller.signal}); } finally { clearTimeout(timeout); }
const result = await response.json();
if (result.stale) { freeze(); say(result.error, true); return; }
if (result.closed) { freeze(); window.location.assign(result.redirect || 'home.php'); return; }
if (!response.ok || !result.ok) throw new Error(result.error || 'Koneksi terputus.');
if (Number.isFinite(result.remaining)) end = performance.now() + result.remaining * 1000;
if (action === 'save') { dirty = revision !== snapshotRevision; say('Draft tersimpan di server · Waktu tetap berjalan.'); }
} catch (error) { dirty = true; say('Belum tersinkron. Periksa koneksi; waktu server tetap berjalan.', true); }
};
// Serialize requests so an older draft cannot overwrite a newer one.
const enqueue = (action, eventType = '') => { pending++; queue = queue.then(() => send(action, eventType)).finally(() => { pending--; }); return queue; };
const log = eventType => {
if (closed || submitting) return;
const data = new FormData();
['csrf','phase','attempt_token'].forEach(name => data.set(name, exam.elements.namedItem(name).value));
data.set('action','event'); data.set('event',eventType);
navigator.sendBeacon(exam.action, data);
};
exam.addEventListener('input', () => { dirty = true; revision++; updateProgress(); });
document.addEventListener('visibilitychange', () => {
if (document.hidden) log('tab_hidden');
else { say('Anda kembali ke halaman tes. Waktu tetap berjalan; perpindahan halaman dicatat untuk peninjauan.'); enqueue('event','page_resumed'); }
});
window.addEventListener('blur', () => log('window_blur'));
['copy','cut','paste','contextmenu'].forEach(type => exam.addEventListener(type, () => log(type)));
exam.addEventListener('submit', async event => {
event.preventDefault();
if (closed || submitting) return;
if (performance.now() < end && !window.confirm('Kirim jawaban fase ini? Jawaban yang dikirim tidak dapat diulang.')) return;
submitting = true;
exam.querySelector('button[type=submit]').disabled = true;
await queue;
if (!closed) HTMLFormElement.prototype.submit.call(exam);
});
const tick = () => {
const remaining = Math.max(0, Math.ceil((end - performance.now()) / 1000));
document.querySelector('#exam-time').textContent = `${String(Math.floor(remaining / 60)).padStart(2,'0')}:${String(remaining % 60).padStart(2,'0')}`;
timer.classList.toggle('timer-warning', remaining <= 60);
if (!remaining && !closed && !submitting) exam.requestSubmit();
};
// Sync even without edits to detect server expiry or another active tab.
setInterval(() => { if (!closed && !submitting && pending === 0) enqueue(dirty ? 'save' : 'event'); }, 3000);
updateProgress(); tick(); setInterval(tick, 1000); enqueue('save');
}