badge fixed
This commit is contained in:
parent
f84402aff7
commit
3b21f82135
|
|
@ -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'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
</style>
|
||||
@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())
|
||||
<div class="badge-modal-overlay active" id="badgeModal">
|
||||
<div class="badge-modal">
|
||||
<div class="badge-modal-label">Badge Baru Diraih!</div>
|
||||
|
||||
<div class="badge-modal-list">
|
||||
@foreach($badgeBaru as $b)
|
||||
<div class="badge-modal-item">
|
||||
<img
|
||||
src="{{ asset($b->icon_badge) }}"
|
||||
alt="{{ $b->nama_badge }}"
|
||||
class="badge-modal-icon"
|
||||
>
|
||||
<div class="badge-modal-info-nama">{{ $b->nama_badge }}</div>
|
||||
<div class="badge-modal-info-desc">{{ $b->deskripsi }}</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<button class="badge-modal-btn" onclick="document.getElementById('badgeModal').classList.remove('active')">
|
||||
Sip, lanjut! 🚀
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="hasil-wrapper">
|
||||
|
||||
<div class="hasil-hero">
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
</style>
|
||||
@stack('styles')
|
||||
</head>
|
||||
|
|
@ -222,6 +368,13 @@
|
|||
</div>
|
||||
|
||||
{{-- 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
|
||||
|
||||
<div class="profile-modal-overlay" id="profileModalOverlay" onclick="closeOnOverlay(event)">
|
||||
<div class="profile-modal">
|
||||
<button class="modal-close" onclick="closeProfileModal()">
|
||||
|
|
@ -266,6 +419,44 @@
|
|||
</div>
|
||||
<button type="submit" class="btn-save-modal" id="btn-save">Simpan Perubahan</button>
|
||||
</form>
|
||||
|
||||
{{-- ── Badge Koleksi ── --}}
|
||||
@if($semuaBadgeLayout->isNotEmpty())
|
||||
<div class="modal-badge-section">
|
||||
<hr class="modal-divider">
|
||||
<div class="modal-badge-title">🏅 Badge Koleksiku</div>
|
||||
<div class="modal-badge-grid">
|
||||
@foreach($semuaBadgeLayout as $b)
|
||||
@php $dimiliki = in_array($b->id_badge, $idBadgeDimiliki); @endphp
|
||||
<div class="modal-badge-item {{ $dimiliki ? 'dimiliki' : 'belum' }}">
|
||||
@if(!$dimiliki)
|
||||
<svg class="modal-badge-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
@endif
|
||||
<img src="{{ asset($b->icon_badge) }}" alt="{{ $b->nama_badge }}" class="modal-badge-icon">
|
||||
<div class="modal-badge-nama">{{ $b->nama_badge }}</div>
|
||||
<div class="modal-badge-desc">{{ $b->deskripsi }}</div>
|
||||
@if($dimiliki)
|
||||
<span class="modal-badge-tag">✓ Diraih</span>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Modal Badge Leaderboard Global --}}
|
||||
<div class="lb-badge-modal-overlay" id="lbBadgeModal">
|
||||
<div class="lb-badge-modal">
|
||||
<div class="lb-badge-modal-label">Badge Baru Diraih!</div>
|
||||
<div class="lb-badge-modal-list" id="lbBadgeList"></div>
|
||||
<button class="lb-badge-modal-btn" onclick="dismissLbBadgeModal()">
|
||||
Sip, lanjut! 🚀
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -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: '<img src="{{ $iconMateri }}" alt="Materi baru" class="icon-md">',
|
||||
tugas: '<img src="{{ $iconTugas }}" alt="Tugas baru" class="icon-md">'
|
||||
|
|
@ -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 => `
|
||||
<div class="lb-badge-modal-item">
|
||||
<img src="${b.icon_url}" alt="${b.nama_badge}" class="lb-badge-modal-icon">
|
||||
<div class="lb-badge-modal-nama">${b.nama_badge}</div>
|
||||
<div class="lb-badge-modal-desc">${b.deskripsi}</div>
|
||||
</div>`).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();}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- ── Pop-up Modal Badge Baru (tugas) ── --}}
|
||||
@if(session('badge_baru') && session('badge_baru')->isNotEmpty())
|
||||
<div class="badge-modal-overlay active" id="badgeModal">
|
||||
<div class="badge-modal">
|
||||
<div class="badge-modal-label">Badge Baru Diraih!</div>
|
||||
|
||||
<div class="badge-modal-list">
|
||||
@foreach(session('badge_baru') as $b)
|
||||
<div class="badge-modal-item">
|
||||
<img
|
||||
src="{{ asset($b->icon_badge) }}"
|
||||
alt="{{ $b->nama_badge }}"
|
||||
class="badge-modal-icon"
|
||||
>
|
||||
<div class="badge-modal-info-nama">{{ $b->nama_badge }}</div>
|
||||
<div class="badge-modal-info-desc">{{ $b->deskripsi }}</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<button class="badge-modal-btn" onclick="document.getElementById('badgeModal').classList.remove('active')">
|
||||
Sip, lanjut! 🚀
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<h1 class="page-title">
|
||||
<img src="{{ asset('images/icon/siswat/buku1.png') }}" alt="Ikon tugas" class="icon-md">
|
||||
Tugas
|
||||
|
|
|
|||
Loading…
Reference in New Issue