diff --git a/app/Http/Controllers/Siswa/ChallengeController.php b/app/Http/Controllers/Siswa/ChallengeController.php index b62da20..adb57c3 100644 --- a/app/Http/Controllers/Siswa/ChallengeController.php +++ b/app/Http/Controllers/Siswa/ChallengeController.php @@ -3,9 +3,11 @@ namespace App\Http\Controllers\Siswa; use App\Http\Controllers\Controller; +use App\Models\Badge; use App\Models\Challenge; use App\Models\PesertaChallenge; use App\Models\Leaderboard; +use App\Models\SiswaBadge; use App\Services\BadgeService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -108,6 +110,11 @@ public function submit(Request $request, $id_challenge) ? $now->year . '/' . ($now->year + 1) : ($now->year - 1) . '/' . $now->year; + // Snapshot badge sebelum submit — untuk deteksi badge baru + $badgeSebelum = SiswaBadge::where('id_siswa', $siswa->id_siswa) + ->pluck('id_badge') + ->toArray(); + DB::transaction(function () use ($siswa, $challenge, $jawabanJson, $totalExp, $semester, $tahunAjaran) { PesertaChallenge::create([ 'id_challenge' => $challenge->id_challenge, @@ -138,9 +145,22 @@ public function submit(Request $request, $id_challenge) ->each(fn($row, $i) => $row->update(['ranking' => $i + 1])); }); - // --- Cek & berikan badge challenge (di luar transaksi agar tidak block) --- + // Cek & berikan badge challenge app(BadgeService::class)->checkChallengeBadges($siswa->id_siswa); + // Deteksi badge yang baru didapat (selisih sebelum & sesudah) + $badgeSesudah = SiswaBadge::where('id_siswa', $siswa->id_siswa) + ->pluck('id_badge') + ->toArray(); + + $idBadgeBaru = array_diff($badgeSesudah, $badgeSebelum); + + // Simpan ke session agar bisa ditampilkan di halaman hasil + if (!empty($idBadgeBaru)) { + $badgeBaru = Badge::whereIn('id_badge', $idBadgeBaru)->get(); + session()->flash('badge_baru', $badgeBaru); + } + return redirect()->route('siswa.challenge.hasil', $id_challenge); } @@ -165,9 +185,13 @@ public function hasil($id_challenge) $totalSoal = $challenge->soal->count(); $persentase = $totalSoal > 0 ? round(($benar / $totalSoal) * 100) : 0; + // Ambil badge baru dari session (hanya ada jika baru saja submit) + $badgeBaru = session('badge_baru', collect()); + return view('siswa.challenge.hasil', compact( 'challenge', 'peserta', 'jawabanSiswa', - 'benar', 'salah', 'totalSoal', 'persentase' + 'benar', 'salah', 'totalSoal', 'persentase', + 'badgeBaru' )); } } \ No newline at end of file diff --git a/app/Http/Controllers/Siswa/LeaderboardController.php b/app/Http/Controllers/Siswa/LeaderboardController.php index adb554f..c8b1fe6 100644 --- a/app/Http/Controllers/Siswa/LeaderboardController.php +++ b/app/Http/Controllers/Siswa/LeaderboardController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Models\Leaderboard; +use App\Models\SiswaBadge; use App\Services\BadgeService; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Storage; @@ -48,28 +49,42 @@ private function getData() public function index() { $data = $this->getData(); - // Keluarkan 'siswa' dari data yang dikirim ke view (tidak dibutuhkan di view) unset($data['siswa']); return view('siswa.leaderboard.index', $data); } /** * Endpoint JSON untuk polling real-time. - * Setiap kali dipanggil, badge leaderboard siswa yang sedang login - * dievaluasi ulang (diberikan atau dicabut sesuai ranking saat ini). + * Mengevaluasi badge leaderboard siswa, lalu return badge yang dimiliki + * saat ini agar JS bisa mendeteksi badge baru via localStorage. */ public function json() { $data = $this->getData(); $siswa = $data['siswa']; - // Evaluasi badge leaderboard secara real-time untuk siswa yang sedang login + // Evaluasi badge leaderboard (grant/revoke) app(BadgeService::class)->checkLeaderboardBadges( $siswa->id_siswa, $siswa->id_kelas ); + // Ambil semua badge leaderboard yang dimiliki siswa saat ini + // beserta detail badge (icon, nama, deskripsi) untuk ditampilkan di pop-up + $badgeSiswa = SiswaBadge::with('badge') + ->where('id_siswa', $siswa->id_siswa) + ->whereHas('badge', fn($q) => $q->whereIn('syarat', ['leaderboard_top5', 'leaderboard_top1'])) + ->get() + ->map(fn($sb) => [ + 'id_badge' => $sb->id_badge, + 'nama_badge' => $sb->badge->nama_badge, + 'deskripsi' => $sb->badge->deskripsi, + 'icon_url' => asset($sb->badge->icon_badge), + ]); + unset($data['siswa']); + $data['badgeSiswa'] = $badgeSiswa; + return response()->json($data); } } \ No newline at end of file diff --git a/app/Http/Controllers/Siswa/ProfileController.php b/app/Http/Controllers/Siswa/ProfileController.php index eeadbaa..2736418 100644 --- a/app/Http/Controllers/Siswa/ProfileController.php +++ b/app/Http/Controllers/Siswa/ProfileController.php @@ -3,6 +3,8 @@ namespace App\Http\Controllers\Siswa; use App\Http\Controllers\Controller; +use App\Models\Badge; +use App\Models\SiswaBadge; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -10,6 +12,27 @@ class ProfileController extends Controller { + public function edit() + { + $siswa = Auth::guard('siswa')->user(); + + // Semua badge yang ada di sistem + $semuaBadge = Badge::all(); + + // ID badge yang dimiliki siswa + $idBadgeDimiliki = SiswaBadge::where('id_siswa', $siswa->id_siswa) + ->pluck('id_badge') + ->toArray(); + + // Gabungkan: tiap badge + flag apakah siswa memilikinya + $badges = $semuaBadge->map(function ($badge) use ($idBadgeDimiliki) { + $badge->dimiliki = in_array($badge->id_badge, $idBadgeDimiliki); + return $badge; + }); + + return view('siswa.profile.edit', compact('badges')); + } + public function updateAjax(Request $request) { $siswa = Auth::guard('siswa')->user(); diff --git a/app/Http/Controllers/Siswa/TugasController.php b/app/Http/Controllers/Siswa/TugasController.php index f8e9b93..fe717e3 100644 --- a/app/Http/Controllers/Siswa/TugasController.php +++ b/app/Http/Controllers/Siswa/TugasController.php @@ -3,13 +3,14 @@ namespace App\Http\Controllers\Siswa; use App\Http\Controllers\Controller; +use App\Models\Badge; +use App\Models\SiswaBadge; +use App\Services\BadgeService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Carbon\Carbon; use App\Models\Tugas; use App\Models\PengumpulanTugas; -use App\Models\Mengajar; -use App\Services\BadgeService; class TugasController extends Controller { @@ -137,9 +138,26 @@ public function submit(Request $request, $id_tugas) 'status' => $status, ]); - // --- Cek & berikan badge tugas hanya jika tepat waktu --- + // Cek & berikan badge tugas hanya jika tepat waktu if ($status === 'dikumpulkan') { + // Snapshot badge sebelum pengecekan + $badgeSebelum = SiswaBadge::where('id_siswa', $siswa->id_siswa) + ->pluck('id_badge') + ->toArray(); + app(BadgeService::class)->checkTugasBadges($siswa->id_siswa); + + // Deteksi badge yang baru didapat + $badgeSesudah = SiswaBadge::where('id_siswa', $siswa->id_siswa) + ->pluck('id_badge') + ->toArray(); + + $idBadgeBaru = array_diff($badgeSesudah, $badgeSebelum); + + if (!empty($idBadgeBaru)) { + $badgeBaru = Badge::whereIn('id_badge', $idBadgeBaru)->get(); + session()->flash('badge_baru', $badgeBaru); + } } $pesan = $status === 'terlambat' diff --git a/public/css/login.css b/public/css/login.css index 6eb0e6d..bf1aae5 100644 --- a/public/css/login.css +++ b/public/css/login.css @@ -450,8 +450,9 @@ .other-portals a { color: #5b3fc0; font-weight: 700; text-decoration: none; - padding: 3px 8px; + padding: 5px 12px; border-radius: 6px; + font-size: 15px; transition: background 0.2s; } diff --git a/resources/views/siswa/challenge/hasil.blade.php b/resources/views/siswa/challenge/hasil.blade.php index d03593e..6ff11b4 100644 --- a/resources/views/siswa/challenge/hasil.blade.php +++ b/resources/views/siswa/challenge/hasil.blade.php @@ -124,6 +124,98 @@ margin-top: 20px; } .btn-back:hover { opacity: 0.9; color: white; } + +/* ── Modal Badge ─────────────────────────────────────────── */ +.badge-modal-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0,0,0,0.55); + z-index: 9999; + align-items: center; + justify-content: center; + backdrop-filter: blur(3px); +} + +.badge-modal-overlay.active { + display: flex; +} + +.badge-modal { + background: white; + border-radius: 24px; + padding: 36px 28px 28px; + max-width: 380px; + width: 90%; + text-align: center; + animation: popIn 0.35s cubic-bezier(0.34,1.56,0.64,1); + position: relative; +} + +@keyframes popIn { + from { transform: scale(0.7); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +.badge-modal-label { + font-size: 12px; + font-weight: 700; + color: #7c3aed; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 20px; +} + +.badge-modal-list { + display: flex; + flex-direction: column; + gap: 20px; + margin-bottom: 24px; +} + +.badge-modal-item { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.badge-modal-icon { + width: 100%; + max-width: 220px; + height: auto; + object-fit: contain; + margin-bottom: 14px; +} + +.badge-modal-info-nama { + font-size: 16px; + font-weight: 800; + color: #1e293b; + margin-bottom: 4px; +} + +.badge-modal-info-desc { + font-size: 13px; + color: #64748b; +} + +.badge-modal-btn { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + border: none; + border-radius: 12px; + padding: 12px 32px; + font-size: 14px; + font-weight: 700; + font-family: 'Poppins', sans-serif; + cursor: pointer; + width: 100%; + transition: opacity 0.2s; +} + +.badge-modal-btn:hover { opacity: 0.9; } + @endpush @@ -146,6 +238,33 @@ : ($persentase >= 60 ? 'Bagus! Terus tingkatkan kemampuanmu!' : 'Jangan menyerah! Terus semangat belajar!'); @endphp +{{-- ── Pop-up Modal Badge Baru ── --}} +@if(isset($badgeBaru) && $badgeBaru->isNotEmpty()) +
+
+
Badge Baru Diraih!
+ +
+ @foreach($badgeBaru as $b) +
+ {{ $b->nama_badge }} +
{{ $b->nama_badge }}
+
{{ $b->deskripsi }}
+
+ @endforeach +
+ + +
+
+@endif +
diff --git a/resources/views/siswa/layouts/app.blade.php b/resources/views/siswa/layouts/app.blade.php index f4eaf81..5bd635e 100644 --- a/resources/views/siswa/layouts/app.blade.php +++ b/resources/views/siswa/layouts/app.blade.php @@ -28,7 +28,6 @@ .sidebar-link.active { background: #e6f0ff; color: #1d4ed8; } .sidebar-icon { width: 20px; height: 20px; flex-shrink: 0; object-fit: contain; } - /* Logout — disamain dengan layout guru */ .sidebar-logout { margin-top: auto; padding-top: 16px; @@ -131,6 +130,153 @@ .modal-toast { border-radius: 10px; padding: 10px 14px; font-size: 13px; font-weight: 500; margin-bottom: 13px; display: none; } .modal-toast.success { background: #f0fdf4; border: 1.5px solid #86efac; color: #166534; display: block; } .modal-toast.error { background: #fef2f2; border: 1.5px solid #fca5a5; color: #991b1b; display: block; } + + /* ── Badge Koleksi di Modal ── */ + .modal-badge-section { margin-top: 20px; } + .modal-badge-title { + font-size: 13px; + font-weight: 700; + color: #475569; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 6px; + } + .modal-badge-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + } + .modal-badge-item { + background: #f8fafc; + border: 2px solid #e2e8f0; + border-radius: 14px; + padding: 12px 8px; + text-align: center; + position: relative; + transition: all 0.2s; + } + .modal-badge-item.dimiliki { + background: #faf5ff; + border-color: #c4b5fd; + } + .modal-badge-item.belum { + opacity: 0.45; + filter: grayscale(100%); + } + .modal-badge-icon { + width: 40px; + height: 40px; + object-fit: contain; + display: block; + margin: 0 auto 7px; + } + .modal-badge-nama { + font-size: 11px; + font-weight: 700; + color: #1e293b; + line-height: 1.3; + margin-bottom: 3px; + } + .modal-badge-desc { + font-size: 10px; + color: #94a3b8; + line-height: 1.3; + } + .modal-badge-lock { + position: absolute; + top: 7px; + right: 7px; + width: 13px; + height: 13px; + color: #94a3b8; + } + .modal-badge-tag { + display: inline-block; + background: #ede9fe; + color: #7c3aed; + font-size: 9px; + font-weight: 700; + padding: 2px 6px; + border-radius: 99px; + margin-top: 4px; + } + + /* ── Modal Badge Global (leaderboard) ── */ + .lb-badge-modal-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0,0,0,0.55); + z-index: 99999; + align-items: center; + justify-content: center; + backdrop-filter: blur(3px); + } + .lb-badge-modal-overlay.active { display: flex; } + .lb-badge-modal { + background: white; + border-radius: 24px; + padding: 32px 28px 24px; + max-width: 360px; + width: 90%; + text-align: center; + animation: lbPopIn 0.35s cubic-bezier(0.34,1.56,0.64,1); + } + @keyframes lbPopIn { + from { transform: scale(0.7); opacity: 0; } + to { transform: scale(1); opacity: 1; } + } + .lb-badge-modal-label { + font-size: 12px; + font-weight: 700; + color: #7c3aed; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 20px; + } + .lb-badge-modal-list { + display: flex; + flex-direction: column; + gap: 20px; + margin-bottom: 24px; + } + .lb-badge-modal-item { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + } + .lb-badge-modal-icon { + width: 100%; + max-width: 220px; + height: auto; + object-fit: contain; + margin-bottom: 14px; + } + .lb-badge-modal-nama { + font-size: 16px; + font-weight: 800; + color: #1e293b; + margin-bottom: 4px; + } + .lb-badge-modal-desc { font-size: 13px; color: #64748b; } + .lb-badge-modal-btn { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + border: none; + border-radius: 12px; + padding: 12px 32px; + font-size: 14px; + font-weight: 700; + font-family: 'Poppins', sans-serif; + cursor: pointer; + width: 100%; + transition: opacity 0.2s; + } + .lb-badge-modal-btn:hover { opacity: 0.9; } @stack('styles') @@ -222,6 +368,13 @@
{{-- PROFILE MODAL --}} +@php + use App\Models\Badge; + use App\Models\SiswaBadge; + $semuaBadgeLayout = Badge::all(); + $idBadgeDimiliki = SiswaBadge::where('id_siswa', $siswa->id_siswa)->pluck('id_badge')->toArray(); +@endphp +
+ + {{-- ── Badge Koleksi ── --}} + @if($semuaBadgeLayout->isNotEmpty()) + + @endif + +
+
+ +{{-- Modal Badge Leaderboard Global --}} +
+
+
Badge Baru Diraih!
+
+
@@ -281,6 +472,9 @@ function updateTogglePosition(c){toggleBtn.style.left=c?'0px':SIDEBAR_W+'px';c?t toggleBtn.addEventListener('click',function(){const c=sidebar.classList.contains('collapsed');sidebar.classList.toggle('collapsed');updateTogglePosition(!c);}); const NOTIF_URL='{{ route("siswa.notifikasi") }}'; +const LB_JSON_URL='{{ route("siswa.leaderboard.json") }}'; +const LB_BADGE_STORAGE_KEY='lb_badge_ids_{{ Auth::guard("siswa")->id() }}'; + const notifIcons = { materi: 'Materi baru', tugas: 'Tugas baru' @@ -313,6 +507,83 @@ function toggleNotif(e){e.stopPropagation();document.getElementById('notifDropdo document.addEventListener('click',e=>{if(!document.getElementById('notifWrap').contains(e.target))document.getElementById('notifDropdown').classList.remove('show');}); fetchNotif();setInterval(fetchNotif,30000); +/* ── Polling badge leaderboard ── */ +const LB_BADGE_PENDING_KEY = LB_BADGE_STORAGE_KEY + '_pending'; + +function getSavedBadgeIds() { + try { return JSON.parse(localStorage.getItem(LB_BADGE_STORAGE_KEY) || '[]'); } catch(e) { return []; } +} +function saveBadgeIds(ids) { + try { localStorage.setItem(LB_BADGE_STORAGE_KEY, JSON.stringify(ids)); } catch(e) {} +} +function getPendingBadges() { + try { return JSON.parse(localStorage.getItem(LB_BADGE_PENDING_KEY) || '[]'); } catch(e) { return []; } +} +function savePendingBadges(badges) { + try { localStorage.setItem(LB_BADGE_PENDING_KEY, JSON.stringify(badges)); } catch(e) {} +} +function clearPendingBadges() { + try { localStorage.removeItem(LB_BADGE_PENDING_KEY); } catch(e) {} +} + +function showLbBadgeModal(badges) { + const list = document.getElementById('lbBadgeList'); + list.innerHTML = badges.map(b => ` +
+ ${b.nama_badge} +
${b.nama_badge}
+
${b.deskripsi}
+
`).join(''); + document.getElementById('lbBadgeModal').classList.add('active'); +} + +function dismissLbBadgeModal() { + document.getElementById('lbBadgeModal').classList.remove('active'); + // Baru update storage setelah user dismiss — ini yang fix masalah + const pending = getPendingBadges(); + if (pending.length > 0) { + saveBadgeIds(pending.map(b => b._confirmedIds).flat()); + clearPendingBadges(); + } +} + +async function pollLbBadge() { + try { + const res = await fetch(LB_JSON_URL); + const data = await res.json(); + const currentBadges = data.badgeSiswa || []; + const currentIds = currentBadges.map(b => b.id_badge); + const savedIds = getSavedBadgeIds(); + + // Badge baru = ada di current tapi belum di saved + const newBadges = currentBadges.filter(b => !savedIds.includes(b.id_badge)); + + if (newBadges.length > 0) { + // Simpan sebagai pending — storage belum diupdate sampai user dismiss + savePendingBadges([{ _confirmedIds: currentIds }]); + showLbBadgeModal(newBadges); + } else { + // Tidak ada badge baru, update storage langsung (termasuk handle badge dicabut) + saveBadgeIds(currentIds); + } + } catch(e) {} +} +// Jalankan polling badge leaderboard setiap 10 detik +// Cek dulu apakah ada pending badge yang belum di-dismiss (dari halaman sebelumnya) +(function checkPendingOnLoad() { + const pending = getPendingBadges(); + if (pending.length > 0) { + // Ada badge yang belum di-dismiss, tampilkan ulang + // Ambil dari saved + current ids untuk reconstruct badge list + // Polling akan handle ini, tapi kita perlu tampilkan modal dulu + // Gunakan data dari pending storage untuk reconstruct + pollLbBadge(); // polling akan detect karena storage belum diupdate + return; + } + pollLbBadge(); +})(); +setInterval(pollLbBadge, 10000); + function openProfileModal(){document.getElementById('profileModalOverlay').classList.add('show');document.body.style.overflow='hidden';} function closeProfileModal(){document.getElementById('profileModalOverlay').classList.remove('show');document.body.style.overflow='';const t=document.getElementById('modal-toast');t.className='modal-toast';t.textContent='';} function closeOnOverlay(e){if(e.target===document.getElementById('profileModalOverlay'))closeProfileModal();} diff --git a/resources/views/siswa/tugas/index.blade.php b/resources/views/siswa/tugas/index.blade.php index e4c40e2..25cbaad 100644 --- a/resources/views/siswa/tugas/index.blade.php +++ b/resources/views/siswa/tugas/index.blade.php @@ -207,11 +207,129 @@ align-items: center; gap: 8px; } + + /* ── Modal Badge ─────────────────────────────────────────── */ + .badge-modal-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0,0,0,0.55); + z-index: 9999; + align-items: center; + justify-content: center; + backdrop-filter: blur(3px); + } + + .badge-modal-overlay.active { + display: flex; + } + + .badge-modal { + background: white; + border-radius: 24px; + padding: 36px 28px 28px; + max-width: 380px; + width: 90%; + text-align: center; + animation: popIn 0.35s cubic-bezier(0.34,1.56,0.64,1); + position: relative; + } + + @keyframes popIn { + from { transform: scale(0.7); opacity: 0; } + to { transform: scale(1); opacity: 1; } + } + + .badge-modal-label { + font-size: 12px; + font-weight: 700; + color: #7c3aed; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 20px; + } + + .badge-modal-list { + display: flex; + flex-direction: column; + gap: 20px; + margin-bottom: 24px; + } + + .badge-modal-item { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + } + + .badge-modal-icon { + width: 100%; + max-width: 220px; + height: auto; + object-fit: contain; + margin-bottom: 14px; + } + + .badge-modal-info-nama { + font-size: 16px; + font-weight: 800; + color: #1e293b; + margin-bottom: 4px; + } + + .badge-modal-info-desc { + font-size: 13px; + color: #64748b; + } + + .badge-modal-btn { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + border: none; + border-radius: 12px; + padding: 12px 32px; + font-size: 14px; + font-weight: 700; + font-family: 'Poppins', sans-serif; + cursor: pointer; + width: 100%; + transition: opacity 0.2s; + } + + .badge-modal-btn:hover { opacity: 0.9; } @endpush @section('content') +{{-- ── Pop-up Modal Badge Baru (tugas) ── --}} +@if(session('badge_baru') && session('badge_baru')->isNotEmpty()) +
+
+
Badge Baru Diraih!
+ +
+ @foreach(session('badge_baru') as $b) +
+ {{ $b->nama_badge }} +
{{ $b->nama_badge }}
+
{{ $b->deskripsi }}
+
+ @endforeach +
+ + +
+
+@endif +

Ikon tugas Tugas