MIF_E31230356/resources/views/siswa/challenge/kerjakan.blade.php

684 lines
22 KiB
PHP

@extends('siswa.layouts.app')
@section('title', 'Kerjakan Challenge')
@push('styles')
<style>
/* Sembunyikan sidebar navigasi app */
.siswa-wrapper .nav-sidebar,
.siswa-wrapper aside.sidebar,
.sidebar-toggle-btn { display: none !important; }
.siswa-wrapper .main,
.siswa-wrapper .main-content {
margin-left: 0 !important;
width: 100% !important;
max-width: 100% !important;
}
/* ─────────────────────────────────────────
LAYOUT
───────────────────────────────────────── */
.quiz-page {
display: grid;
grid-template-columns: 1fr 272px;
gap: 20px;
max-width: 1080px;
margin: 0 auto;
padding: 20px 16px 48px;
align-items: start;
}
.quiz-main {
display: flex;
flex-direction: column;
gap: 14px;
min-width: 0;
}
/* Header */
.quiz-header {
background: #fff;
border: 1.5px solid #e2e8f0;
border-radius: 18px;
padding: 20px 24px;
text-align: center;
}
.quiz-title {
font-size: 19px;
font-weight: 800;
color: #1e293b;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.quiz-title img { width: 22px; height: 22px; object-fit: contain; }
.quiz-meta {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
}
.quiz-meta span {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: #475569;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 99px;
padding: 4px 12px;
font-weight: 500;
}
.quiz-meta img { width: 13px; height: 13px; object-fit: contain; }
/* Durasi badge di header */
.durasi-meta-badge {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 700;
color: #5b21b6;
background: #ede9fe;
border: 1px solid #c4b5fd;
border-radius: 99px;
padding: 4px 12px;
}
/* Progress */
.progress-card {
background: #fff;
border: 1.5px solid #e2e8f0;
border-radius: 14px;
padding: 12px 20px;
display: flex;
align-items: center;
gap: 14px;
}
.progress-label {
font-size: 12px;
font-weight: 600;
color: #64748b;
white-space: nowrap;
flex-shrink: 0;
}
.progress-bar-wrap {
flex: 1;
background: #e2e8f0;
border-radius: 99px;
height: 7px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 99px;
transition: width 0.4s ease;
}
/* Soal card */
.soal-card {
background: #fff;
border: 1.5px solid #e2e8f0;
border-radius: 18px;
padding: 24px 26px;
display: none;
}
.soal-card.active { display: block; }
.soal-number {
display: inline-block;
background: linear-gradient(135deg, #667eea, #764ba2);
color: #fff;
font-size: 11px;
font-weight: 700;
padding: 3px 13px;
border-radius: 99px;
margin-bottom: 14px;
}
.soal-pertanyaan {
font-size: 15px;
font-weight: 600;
color: #1e293b;
line-height: 1.7;
margin-bottom: 18px;
}
.opsi-list { display: flex; flex-direction: column; gap: 9px; }
.opsi-item {
display: flex;
align-items: center;
gap: 12px;
background: #f8fafc;
border: 1.5px solid #e2e8f0;
border-radius: 12px;
padding: 12px 16px;
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
font-size: 14px;
color: #334155;
user-select: none;
}
.opsi-item:hover { border-color: #667eea; background: #f0eeff; }
.opsi-item.selected {
border-color: #667eea;
background: #ede9fe;
color: #4c1d95;
font-weight: 600;
}
.opsi-item input[type="radio"] { display: none; }
.opsi-label-circle {
width: 30px; height: 30px;
border-radius: 50%;
background: #e2e8f0;
display: flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: 700;
flex-shrink: 0;
transition: background 0.15s, color 0.15s;
}
.opsi-item.selected .opsi-label-circle { background: #667eea; color: #fff; }
/* Nav buttons */
.nav-buttons-card {
background: #fff;
border: 1.5px solid #e2e8f0;
border-radius: 14px;
padding: 14px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.btn-nav {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 22px;
border-radius: 10px;
border: none;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.18s;
font-family: 'Poppins', sans-serif;
}
.btn-prev { background: #f1f5f9; color: #475569; }
.btn-prev:hover:not(:disabled) { background: #e2e8f0; }
.btn-prev:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-next { background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; }
.btn-next:hover { opacity: 0.88; }
.btn-submit { background: linear-gradient(135deg, #22c55e, #16a34a); color: #fff; display: none; }
.btn-submit:hover { opacity: 0.88; }
.target-selesai{ width: 15px; height: 15px; object-fit: contain; }
/* Warning */
.warning-box {
display: none;
align-items: center;
gap: 8px;
background: #fff7ed;
border: 1px solid #fed7aa;
border-radius: 11px;
padding: 11px 15px;
font-size: 13px;
color: #c2410c;
font-weight: 500;
margin-bottom: 4px;
}
.warning-box img { width: 15px; height: 15px; object-fit: contain; flex-shrink: 0; }
/* ─────────────────────────────────────────
SIDEBAR QUIZ
───────────────────────────────────────── */
.quiz-sidebar-panel {
display: flex;
flex-direction: column;
gap: 14px;
position: sticky;
top: 20px;
}
/* Timer */
.timer-card {
background: #fff;
border: 1.5px solid #e2e8f0;
border-radius: 18px;
padding: 18px 20px;
text-align: center;
}
.timer-label {
font-size: 10px;
font-weight: 700;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 4px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.timer-label img { width: 13px; height: 13px; object-fit: contain; }
/* Baris total durasi (kecil, di bawah label) */
.timer-total-info {
font-size: 11px;
color: #a78bfa;
font-weight: 600;
margin-bottom: 8px;
}
.timer-display {
font-size: 34px;
font-weight: 800;
color: #1e293b;
letter-spacing: 3px;
font-variant-numeric: tabular-nums;
transition: color 0.3s;
}
.timer-display.warning { color: #d97706; }
.timer-display.danger { color: #dc2626; animation: heartbeat 0.8s ease-in-out infinite; }
@keyframes heartbeat {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.06); }
}
.timer-bar-wrap {
background: #e2e8f0;
border-radius: 99px;
height: 5px;
margin-top: 10px;
overflow: hidden;
}
.timer-bar-fill {
height: 100%;
background: linear-gradient(90deg, #22c55e, #667eea);
border-radius: 99px;
transition: width 1s linear, background 0.3s;
}
.timer-bar-fill.warning { background: linear-gradient(90deg, #f59e0b, #d97706); }
.timer-bar-fill.danger { background: linear-gradient(90deg, #ef4444, #dc2626); }
/* Nav card */
.nav-card {
background: #fff;
border: 1.5px solid #e2e8f0;
border-radius: 18px;
padding: 18px 20px;
}
.nav-card-label {
font-size: 10px; font-weight: 700;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 12px;
display: flex; align-items: center; gap: 6px;
}
.nav-card-label img { width: 13px; height: 13px; object-fit: contain; }
.nav-summary {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px; margin-bottom: 14px;
}
.summary-item {
text-align: center;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 8px 4px;
}
.summary-num { font-size: 22px; font-weight: 800; }
.summary-label { font-size: 10px; color: #94a3b8; font-weight: 500; margin-top: 2px; }
.summary-item.answered-sum .summary-num { color: #22c55e; }
.summary-item.unanswered-sum .summary-num { color: #94a3b8; }
.nav-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 7px; margin-bottom: 14px;
}
.nav-btn {
aspect-ratio: 1;
border-radius: 9px;
border: 1.5px solid #e2e8f0;
background: #f8fafc;
font-size: 12px; font-weight: 700;
color: #64748b; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: all 0.15s;
font-family: 'Poppins', sans-serif;
}
.nav-btn:hover { border-color: #667eea; background: #f0eeff; color: #667eea; }
.nav-btn.active { background: #667eea; border-color: #667eea; color: #fff; }
.nav-btn.answered { background: #dcfce7; border-color: #22c55e; color: #16a34a; }
.nav-btn.active.answered { background: #22c55e; border-color: #22c55e; color: #fff; }
.nav-legend {
display: flex; flex-direction: column;
gap: 5px; padding-top: 12px;
border-top: 1px solid #f1f5f9;
}
.legend-item {
display: flex; align-items: center;
gap: 7px; font-size: 11px; color: #64748b;
}
.legend-dot { width: 13px; height: 13px; border-radius: 4px; flex-shrink: 0; }
.legend-dot.current { background: #667eea; }
.legend-dot.done { background: #dcfce7; border: 1.5px solid #22c55e; }
.legend-dot.undone { background: #f8fafc; border: 1.5px solid #e2e8f0; }
@media (max-width: 768px) {
.quiz-page { grid-template-columns: 1fr; }
.quiz-sidebar-panel { position: static; order: -1; }
.nav-grid { grid-template-columns: repeat(8, 1fr); }
}
</style>
@endpush
@section('content')
<div class="quiz-page">
{{-- KOLOM KIRI --}}
<div class="quiz-main">
<div class="quiz-header">
<div class="quiz-title">
<img src="{{ asset('images/icon/siswac/piala.png') }}" alt="">
{{ $challenge->judul_challenge }}
</div>
<div class="quiz-meta">
<span>
<img src="{{ asset('images/icon/siswac/buku1.png') }}" alt="">
{{ $challenge->soal->count() }} Soal
</span>
<span>
<img src="{{ asset('images/icon/siswac/star.png') }}" alt="">
{{ $challenge->exp }} EXP
</span>
<span>
<img src="{{ asset('images/icon/siswac/alarm.png') }}" alt="">
Tenggat: {{ \Carbon\Carbon::parse($challenge->tenggat_waktu)->format('d M Y, H:i') }}
</span>
{{-- Badge durasi --}}
@if($challenge->durasi_pengerjaan)
<span class="durasi-meta-badge">
{{ $challenge->durasi_pengerjaan }} menit pengerjaan
</span>
@endif
</div>
</div>
<div class="progress-card">
<span class="progress-label" id="progressLabel">
Soal 1 dari {{ $challenge->soal->count() }}
</span>
<div class="progress-bar-wrap">
<div class="progress-bar-fill" id="progressBar"
style="width: {{ round(1 / $challenge->soal->count() * 100) }}%">
</div>
</div>
</div>
<form action="{{ route('siswa.challenge.submit', $challenge->id_challenge) }}"
method="POST" id="quizForm">
@csrf
@foreach($challenge->soal as $i => $soal)
<div class="soal-card {{ $i === 0 ? 'active' : '' }}" id="soal-{{ $i }}">
<span class="soal-number">Soal {{ $i + 1 }}</span>
<p class="soal-pertanyaan">{{ $soal->pertanyaan }}</p>
<div class="opsi-list">
@foreach(['A','B','C','D'] as $opsi)
@php $key = 'opsi_' . strtolower($opsi); @endphp
<label class="opsi-item" id="label-{{ $i }}-{{ $opsi }}"
onclick="pilihJawaban({{ $i }}, '{{ $opsi }}', {{ $soal->id_soal }})">
<input type="radio" name="jawaban[{{ $soal->id_soal }}]"
value="{{ $opsi }}" id="radio-{{ $i }}-{{ $opsi }}">
<span class="opsi-label-circle">{{ $opsi }}</span>
<span>{{ $soal->$key }}</span>
</label>
@endforeach
</div>
</div>
@endforeach
<div class="warning-box" id="warningBox">
<img src="{{ asset('images/icon/siswac/alert.png') }}" alt="">
Masih ada <span id="warningCount"></span> soal yang belum dijawab. Yakin ingin submit?
</div>
<div class="nav-buttons-card">
<button type="button" class="btn-nav btn-prev" id="btnPrev"
onclick="prevSoal()" disabled> Sebelumnya</button>
<button type="button" class="btn-nav btn-next" id="btnNext"
onclick="nextSoal()">Selanjutnya </button>
<button type="submit" class="btn-nav btn-submit" id="btnSubmit"
onclick="return konfirmasiSubmit()">
<img src="{{ asset('images/icon/siswac/target.png') }}" class="target-selesai" alt="Submit">
Selesai &amp; Submit
</button>
</div>
</form>
</div>{{-- /quiz-main --}}
{{-- KOLOM KANAN --}}
<div class="quiz-sidebar-panel">
<div class="timer-card">
<div class="timer-label">
<img src="{{ asset('images/icon/siswac/alarm.png') }}" alt="">
Sisa Waktu
</div>
{{-- Info total durasi --}}
@if($challenge->durasi_pengerjaan)
<div class="timer-total-info">
dari {{ $challenge->durasi_pengerjaan }} menit
</div>
@endif
<div class="timer-display" id="timerDisplay">--:--</div>
<div class="timer-bar-wrap">
<div class="timer-bar-fill" id="timerBar" style="width:100%"></div>
</div>
</div>
<div class="nav-card">
<div class="nav-card-label">
<img src="{{ asset('images/icon/siswac/buku1.png') }}" alt="">
Navigasi Soal
</div>
<div class="nav-summary">
<div class="summary-item answered-sum">
<div class="summary-num" id="summaryAnswered">0</div>
<div class="summary-label">Terjawab</div>
</div>
<div class="summary-item unanswered-sum">
<div class="summary-num" id="summaryUnanswered">{{ $challenge->soal->count() }}</div>
<div class="summary-label">Belum</div>
</div>
</div>
<div class="nav-grid" id="navGrid">
@foreach($challenge->soal as $i => $soal)
<button type="button"
class="nav-btn {{ $i === 0 ? 'active' : '' }}"
id="navBtn-{{ $i }}"
onclick="goToSoal({{ $i }})">
{{ $i + 1 }}
</button>
@endforeach
</div>
<div class="nav-legend">
<div class="legend-item"><div class="legend-dot current"></div><span>Soal aktif</span></div>
<div class="legend-item"><div class="legend-dot done"></div><span>Sudah dijawab</span></div>
<div class="legend-item"><div class="legend-dot undone"></div><span>Belum dijawab</span></div>
</div>
</div>
</div>{{-- /quiz-sidebar-panel --}}
</div>{{-- /quiz-page --}}
@endsection
@push('scripts')
<script>
const totalSoal = {{ $challenge->soal->count() }};
let currentSoal = 0;
let jawaban = {};
// ═══════════════════════════════════════════════════════════════════
// TIMER — menggunakan durasi_pengerjaan (menit) dari database
//
// Logika:
// 1. Jika ada durasi_pengerjaan → hitung mundur dari durasi tsb
// 2. Jika tidak ada durasi_pengerjaan (null) → fallback ke tenggat
// 3. Ambil waktu mulai dari sessionStorage agar konsisten
// jika halaman di-refresh (timer tidak reset)
// ═══════════════════════════════════════════════════════════════════
@if($challenge->durasi_pengerjaan)
// Durasi dalam detik
const DURASI_DETIK = {{ (int)$challenge->durasi_pengerjaan * 60 }};
// Simpan waktu mulai ke sessionStorage agar tidak reset saat refresh
const SESSION_KEY = 'quiz_start_{{ $challenge->id_challenge }}';
let startTime = parseInt(sessionStorage.getItem(SESSION_KEY) || '0');
if (!startTime) {
startTime = Date.now();
sessionStorage.setItem(SESSION_KEY, startTime);
}
// Hitung sisa detik berdasarkan waktu mulai
const totalDetik = DURASI_DETIK;
let sisaDetik = Math.max(0, DURASI_DETIK - Math.floor((Date.now() - startTime) / 1000));
@else
// Fallback: pakai tenggat waktu
const tenggatMs = {{ \Carbon\Carbon::parse($challenge->tenggat_waktu)->timestamp }} * 1000;
const totalDetik = Math.max(0, Math.floor((tenggatMs - Date.now()) / 1000));
let sisaDetik = totalDetik;
@endif
const elTimer = document.getElementById('timerDisplay');
const elTimerBar = document.getElementById('timerBar');
function formatWaktu(s) {
if (s <= 0) return '00:00';
const j = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const d = s % 60;
const mm = String(m).padStart(2, '0');
const ss = String(d).padStart(2, '0');
if (j > 0) return `${String(j).padStart(2,'0')}:${mm}:${ss}`;
return `${mm}:${ss}`;
}
function updateTimer() {
elTimer.textContent = formatWaktu(sisaDetik);
const pct = totalDetik > 0 ? (sisaDetik / totalDetik) * 100 : 0;
elTimerBar.style.width = pct + '%';
if (sisaDetik <= 60) {
elTimer.className = 'timer-display danger';
elTimerBar.className = 'timer-bar-fill danger';
} else if (sisaDetik <= Math.floor(totalDetik * 0.25)) {
// Kuning saat sisa ≤ 25% dari total durasi
elTimer.className = 'timer-display warning';
elTimerBar.className = 'timer-bar-fill warning';
} else {
elTimer.className = 'timer-display';
elTimerBar.className = 'timer-bar-fill';
}
if (sisaDetik <= 0) {
clearInterval(timerInterval);
elTimer.textContent = '00:00';
@if($challenge->durasi_pengerjaan)
sessionStorage.removeItem('quiz_start_{{ $challenge->id_challenge }}');
@endif
alert('Waktu habis! Jawaban kamu akan otomatis dikumpulkan.');
document.getElementById('quizForm').submit();
return;
}
sisaDetik--;
}
updateTimer();
const timerInterval = setInterval(updateTimer, 1000);
// ═══════════════════════════════════════════════════════════════════
// NAVIGASI SOAL
// ═══════════════════════════════════════════════════════════════════
function goToSoal(index) {
document.getElementById(`soal-${currentSoal}`).classList.remove('active');
document.getElementById(`navBtn-${currentSoal}`).classList.remove('active');
currentSoal = index;
document.getElementById(`soal-${currentSoal}`).classList.add('active');
document.getElementById(`navBtn-${currentSoal}`).classList.add('active');
updateNav();
updateProgress();
}
function nextSoal() { if (currentSoal < totalSoal - 1) goToSoal(currentSoal + 1); }
function prevSoal() { if (currentSoal > 0) goToSoal(currentSoal - 1); }
function pilihJawaban(soalIndex, opsi, idSoal) {
jawaban[soalIndex] = opsi;
['A','B','C','D'].forEach(o => {
document.getElementById(`label-${soalIndex}-${o}`)?.classList.remove('selected');
});
document.getElementById(`label-${soalIndex}-${opsi}`)?.classList.add('selected');
document.getElementById(`radio-${soalIndex}-${opsi}`).checked = true;
document.getElementById(`navBtn-${soalIndex}`)?.classList.add('answered');
updateProgress();
updateSummary();
}
function updateNav() {
const btnPrev = document.getElementById('btnPrev');
const btnNext = document.getElementById('btnNext');
const btnSubmit = document.getElementById('btnSubmit');
btnPrev.disabled = (currentSoal === 0);
if (currentSoal === totalSoal - 1) {
btnNext.style.display = 'none';
btnSubmit.style.display = 'inline-flex';
} else {
btnNext.style.display = 'inline-flex';
btnSubmit.style.display = 'none';
}
}
function updateProgress() {
const answered = Object.keys(jawaban).length;
const pct = Math.round(((currentSoal + 1) / totalSoal) * 100);
document.getElementById('progressBar').style.width = pct + '%';
document.getElementById('progressLabel').textContent =
`Soal ${currentSoal + 1} dari ${totalSoal} · ${answered} terjawab`;
}
function updateSummary() {
const answered = Object.keys(jawaban).length;
document.getElementById('summaryAnswered').textContent = answered;
document.getElementById('summaryUnanswered').textContent = totalSoal - answered;
}
function konfirmasiSubmit() {
const belum = totalSoal - Object.keys(jawaban).length;
if (belum > 0) {
document.getElementById('warningCount').textContent = `${belum} soal`;
document.getElementById('warningBox').style.display = 'flex';
return confirm(`Masih ada ${belum} soal yang belum dijawab. Yakin ingin submit?`);
}
@if($challenge->durasi_pengerjaan)
// Bersihkan session timer setelah submit
sessionStorage.removeItem('quiz_start_{{ $challenge->id_challenge }}');
@endif
return confirm('Yakin ingin submit jawaban? Jawaban tidak bisa diubah setelah submit.');
}
// Init
updateNav();
updateProgress();
updateSummary();
</script>
@endpush