badge fixed
This commit is contained in:
parent
f84402aff7
commit
3b21f82135
|
|
@ -3,9 +3,11 @@
|
||||||
namespace App\Http\Controllers\Siswa;
|
namespace App\Http\Controllers\Siswa;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Badge;
|
||||||
use App\Models\Challenge;
|
use App\Models\Challenge;
|
||||||
use App\Models\PesertaChallenge;
|
use App\Models\PesertaChallenge;
|
||||||
use App\Models\Leaderboard;
|
use App\Models\Leaderboard;
|
||||||
|
use App\Models\SiswaBadge;
|
||||||
use App\Services\BadgeService;
|
use App\Services\BadgeService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
@ -108,6 +110,11 @@ public function submit(Request $request, $id_challenge)
|
||||||
? $now->year . '/' . ($now->year + 1)
|
? $now->year . '/' . ($now->year + 1)
|
||||||
: ($now->year - 1) . '/' . $now->year;
|
: ($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) {
|
DB::transaction(function () use ($siswa, $challenge, $jawabanJson, $totalExp, $semester, $tahunAjaran) {
|
||||||
PesertaChallenge::create([
|
PesertaChallenge::create([
|
||||||
'id_challenge' => $challenge->id_challenge,
|
'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]));
|
->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);
|
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);
|
return redirect()->route('siswa.challenge.hasil', $id_challenge);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,9 +185,13 @@ public function hasil($id_challenge)
|
||||||
$totalSoal = $challenge->soal->count();
|
$totalSoal = $challenge->soal->count();
|
||||||
$persentase = $totalSoal > 0 ? round(($benar / $totalSoal) * 100) : 0;
|
$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(
|
return view('siswa.challenge.hasil', compact(
|
||||||
'challenge', 'peserta', 'jawabanSiswa',
|
'challenge', 'peserta', 'jawabanSiswa',
|
||||||
'benar', 'salah', 'totalSoal', 'persentase'
|
'benar', 'salah', 'totalSoal', 'persentase',
|
||||||
|
'badgeBaru'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Leaderboard;
|
use App\Models\Leaderboard;
|
||||||
|
use App\Models\SiswaBadge;
|
||||||
use App\Services\BadgeService;
|
use App\Services\BadgeService;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
@ -48,28 +49,42 @@ private function getData()
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$data = $this->getData();
|
$data = $this->getData();
|
||||||
// Keluarkan 'siswa' dari data yang dikirim ke view (tidak dibutuhkan di view)
|
|
||||||
unset($data['siswa']);
|
unset($data['siswa']);
|
||||||
return view('siswa.leaderboard.index', $data);
|
return view('siswa.leaderboard.index', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Endpoint JSON untuk polling real-time.
|
* Endpoint JSON untuk polling real-time.
|
||||||
* Setiap kali dipanggil, badge leaderboard siswa yang sedang login
|
* Mengevaluasi badge leaderboard siswa, lalu return badge yang dimiliki
|
||||||
* dievaluasi ulang (diberikan atau dicabut sesuai ranking saat ini).
|
* saat ini agar JS bisa mendeteksi badge baru via localStorage.
|
||||||
*/
|
*/
|
||||||
public function json()
|
public function json()
|
||||||
{
|
{
|
||||||
$data = $this->getData();
|
$data = $this->getData();
|
||||||
$siswa = $data['siswa'];
|
$siswa = $data['siswa'];
|
||||||
|
|
||||||
// Evaluasi badge leaderboard secara real-time untuk siswa yang sedang login
|
// Evaluasi badge leaderboard (grant/revoke)
|
||||||
app(BadgeService::class)->checkLeaderboardBadges(
|
app(BadgeService::class)->checkLeaderboardBadges(
|
||||||
$siswa->id_siswa,
|
$siswa->id_siswa,
|
||||||
$siswa->id_kelas
|
$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']);
|
unset($data['siswa']);
|
||||||
|
$data['badgeSiswa'] = $badgeSiswa;
|
||||||
|
|
||||||
return response()->json($data);
|
return response()->json($data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
namespace App\Http\Controllers\Siswa;
|
namespace App\Http\Controllers\Siswa;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Badge;
|
||||||
|
use App\Models\SiswaBadge;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
@ -10,6 +12,27 @@
|
||||||
|
|
||||||
class ProfileController extends Controller
|
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)
|
public function updateAjax(Request $request)
|
||||||
{
|
{
|
||||||
$siswa = Auth::guard('siswa')->user();
|
$siswa = Auth::guard('siswa')->user();
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@
|
||||||
namespace App\Http\Controllers\Siswa;
|
namespace App\Http\Controllers\Siswa;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Badge;
|
||||||
|
use App\Models\SiswaBadge;
|
||||||
|
use App\Services\BadgeService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use App\Models\Tugas;
|
use App\Models\Tugas;
|
||||||
use App\Models\PengumpulanTugas;
|
use App\Models\PengumpulanTugas;
|
||||||
use App\Models\Mengajar;
|
|
||||||
use App\Services\BadgeService;
|
|
||||||
|
|
||||||
class TugasController extends Controller
|
class TugasController extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -137,9 +138,26 @@ public function submit(Request $request, $id_tugas)
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// --- Cek & berikan badge tugas hanya jika tepat waktu ---
|
// Cek & berikan badge tugas hanya jika tepat waktu
|
||||||
if ($status === 'dikumpulkan') {
|
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);
|
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'
|
$pesan = $status === 'terlambat'
|
||||||
|
|
|
||||||
|
|
@ -450,8 +450,9 @@ .other-portals a {
|
||||||
color: #5b3fc0;
|
color: #5b3fc0;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 3px 8px;
|
padding: 5px 12px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
font-size: 15px;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,98 @@
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
.btn-back:hover { opacity: 0.9; color: white; }
|
.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>
|
</style>
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
|
|
@ -146,6 +238,33 @@
|
||||||
: ($persentase >= 60 ? 'Bagus! Terus tingkatkan kemampuanmu!' : 'Jangan menyerah! Terus semangat belajar!');
|
: ($persentase >= 60 ? 'Bagus! Terus tingkatkan kemampuanmu!' : 'Jangan menyerah! Terus semangat belajar!');
|
||||||
@endphp
|
@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-wrapper">
|
||||||
|
|
||||||
<div class="hasil-hero">
|
<div class="hasil-hero">
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@
|
||||||
.sidebar-link.active { background: #e6f0ff; color: #1d4ed8; }
|
.sidebar-link.active { background: #e6f0ff; color: #1d4ed8; }
|
||||||
.sidebar-icon { width: 20px; height: 20px; flex-shrink: 0; object-fit: contain; }
|
.sidebar-icon { width: 20px; height: 20px; flex-shrink: 0; object-fit: contain; }
|
||||||
|
|
||||||
/* Logout — disamain dengan layout guru */
|
|
||||||
.sidebar-logout {
|
.sidebar-logout {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
padding-top: 16px;
|
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 { 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.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; }
|
.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>
|
</style>
|
||||||
@stack('styles')
|
@stack('styles')
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -222,6 +368,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- PROFILE MODAL --}}
|
{{-- 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-overlay" id="profileModalOverlay" onclick="closeOnOverlay(event)">
|
||||||
<div class="profile-modal">
|
<div class="profile-modal">
|
||||||
<button class="modal-close" onclick="closeProfileModal()">
|
<button class="modal-close" onclick="closeProfileModal()">
|
||||||
|
|
@ -266,6 +419,44 @@
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn-save-modal" id="btn-save">Simpan Perubahan</button>
|
<button type="submit" class="btn-save-modal" id="btn-save">Simpan Perubahan</button>
|
||||||
</form>
|
</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>
|
||||||
</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);});
|
toggleBtn.addEventListener('click',function(){const c=sidebar.classList.contains('collapsed');sidebar.classList.toggle('collapsed');updateTogglePosition(!c);});
|
||||||
|
|
||||||
const NOTIF_URL='{{ route("siswa.notifikasi") }}';
|
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 = {
|
const notifIcons = {
|
||||||
materi: '<img src="{{ $iconMateri }}" alt="Materi baru" class="icon-md">',
|
materi: '<img src="{{ $iconMateri }}" alt="Materi baru" class="icon-md">',
|
||||||
tugas: '<img src="{{ $iconTugas }}" alt="Tugas 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');});
|
document.addEventListener('click',e=>{if(!document.getElementById('notifWrap').contains(e.target))document.getElementById('notifDropdown').classList.remove('show');});
|
||||||
fetchNotif();setInterval(fetchNotif,30000);
|
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 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 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();}
|
function closeOnOverlay(e){if(e.target===document.getElementById('profileModalOverlay'))closeProfileModal();}
|
||||||
|
|
|
||||||
|
|
@ -207,11 +207,129 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
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>
|
</style>
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
@section('content')
|
@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">
|
<h1 class="page-title">
|
||||||
<img src="{{ asset('images/icon/siswat/buku1.png') }}" alt="Ikon tugas" class="icon-md">
|
<img src="{{ asset('images/icon/siswat/buku1.png') }}" alt="Ikon tugas" class="icon-md">
|
||||||
Tugas
|
Tugas
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue