MIF_E31230356/resources/views/siswa/leaderboard/index.blade.php

242 lines
11 KiB
PHP

@extends('siswa.layouts.app')
@section('title', 'Leaderboard')
@push('styles')
<style>
.page-title { font-size: 28px; font-weight: 800; margin-top: -20px; margin-bottom: 6px; }
.page-subtitle { font-size: 14px; color: #64748b; margin-bottom: 24px; }
.podium-wrap { display: flex; align-items: flex-end; justify-content: center; gap: 12px; margin-bottom: 32px; }
.podium-item { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.podium-avatar { border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 22px; font-weight: 800; color: white; position: relative; overflow: hidden; flex-shrink: 0; width: 56px; height: 56px; }
.podium-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.podium-crown { position: absolute; top: -16px; font-size: 18px; z-index: 1; pointer-events: none; }
.rank-1 .podium-avatar { background: linear-gradient(135deg,#f59e0b,#d97706); width:68px; height:68px; font-size:26px; }
.rank-2 .podium-avatar { background: linear-gradient(135deg,#94a3b8,#64748b); }
.rank-3 .podium-avatar { background: linear-gradient(135deg,#f97316,#ea580c); }
.podium-name { font-size:13px; font-weight:700; color:#1e293b; text-align:center; max-width:90px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.podium-exp { font-size:12px; color:#64748b; }
.podium-bar { border-radius:12px 12px 0 0; width:80px; display:flex; align-items:center; justify-content:center; font-size:20px; font-weight:800; color:white; }
.rank-1 .podium-bar { height:80px; background:linear-gradient(135deg,#f59e0b,#d97706); }
.rank-2 .podium-bar { height:60px; background:linear-gradient(135deg,#94a3b8,#64748b); }
.rank-3 .podium-bar { height:44px; background:linear-gradient(135deg,#f97316,#ea580c); }
.my-rank-banner { background:linear-gradient(135deg,#667eea,#764ba2); border-radius:16px; padding:16px 20px; color:white; display:flex; align-items:center; gap:16px; margin-bottom:20px; }
.my-rank-avatar { width:48px; height:48px; border-radius:50%; object-fit:cover; border:2px solid rgba(255,255,255,0.5); flex-shrink:0; }
.my-rank-avatar-placeholder { width:48px; height:48px; border-radius:50%; background:rgba(255,255,255,0.2); display:flex; align-items:center; justify-content:center; font-size:20px; font-weight:800; flex-shrink:0; }
.my-rank-num { font-size:36px; font-weight:800; line-height:1; }
.my-rank-info { flex:1; }
.my-rank-label { font-size:12px; opacity:0.8; margin-bottom:2px; }
.my-rank-nama { font-size:16px; font-weight:700; }
.my-rank-exp { font-size:13px; opacity:0.9; }
.custom-card { background:white; border-radius:20px; border:2px solid #e5e5e5; padding:22px; }
.section-title { font-size:15px; font-weight:700; color:#1e293b; margin-bottom:16px; display:flex; align-items:center; justify-content:space-between; }
.live-dot { width:8px; height:8px; border-radius:50%; background:#22c55e; box-shadow:0 0 6px #22c55e; animation: pulse 2s ease-in-out infinite; display:inline-block; margin-right:6px; }
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:0.5;transform:scale(0.8)} }
.lb-row { display:flex; align-items:center; gap:12px; padding:10px 14px; border-radius:12px; margin-bottom:8px; transition:background 0.15s; }
.lb-row:hover { background:#f8fafc; }
.lb-row.highlight { background:#f0eeff; border:2px solid #c4b5fd; }
.lb-rank { width:32px; height:32px; border-radius:50%; background:#e2e8f0; display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; color:#64748b; flex-shrink:0; }
.lb-rank.gold { background:#fef3c7; color:#d97706; }
.lb-rank.silver { background:#f1f5f9; color:#64748b; }
.lb-rank.bronze { background:#ffedd5; color:#ea580c; }
.lb-avatar { width:36px; height:36px; border-radius:50%; object-fit:cover; flex-shrink:0; border:2px solid #e2e8f0; }
.lb-avatar-placeholder { width:36px; height:36px; border-radius:50%; background:#e6f0ff; display:flex; align-items:center; justify-content:center; font-size:14px; font-weight:700; color:#2b8ef3; flex-shrink:0; }
.lb-nama { flex:1; font-size:14px; font-weight:600; color:#1e293b; }
.lb-nisn { font-size:12px; color:#94a3b8; }
.lb-exp { font-size:14px; font-weight:700; color:#667eea; }
.semester-badge { display:inline-block; background:#f0eeff; color:#667eea; font-size:12px; font-weight:700; padding:4px 12px; border-radius:99px; margin-bottom:20px; }
.empty-state { text-align:center; padding:40px 20px; color:#94a3b8; }
</style>
@endpush
@section('content')
@php $siswaLogin = Auth::guard('siswa')->user(); @endphp
<h3 class="page-title">🏅 Leaderboard</h3>
<p class="page-subtitle">Peringkat siswa berdasarkan total EXP yang dikumpulkan.</p>
<span class="semester-badge">Semester {{ $semester }} · {{ $tahunAjaran }}</span>
{{-- Container di-render real-time via JavaScript --}}
<div id="leaderboard-container">
<div style="text-align:center;padding:40px;color:#94a3b8;">
<div style="font-size:32px;margin-bottom:8px"></div>
<p style="font-size:13px">Memuat data...</p>
</div>
</div>
@endsection
@push('scripts')
<script>
const currentSiswaId = {{ $siswaLogin->id_siswa }};
const jsonUrl = '{{ route("siswa.leaderboard.json") }}';
function avatarHtml(item, size = 'normal') {
const isLarge = size === 'large';
const w = isLarge ? 48 : 36;
const fontSize = isLarge ? 20 : 14;
if (item.foto_url) {
const cls = isLarge ? 'my-rank-avatar' : 'lb-avatar';
return `<img src="${item.foto_url}?t=${Date.now()}" class="${cls}" alt="">`;
}
const initial = item.nama ? item.nama.charAt(0).toUpperCase() : '?';
if (isLarge) {
return `<div class="my-rank-avatar-placeholder">${initial}</div>`;
}
return `<div class="lb-avatar-placeholder">${initial}</div>`;
}
function podiumAvatarHtml(item, rankClass) {
if (item.foto_url) {
return `<img src="${item.foto_url}?t=${Date.now()}" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">`;
}
return item.nama ? item.nama.charAt(0).toUpperCase() : '?';
}
function rankBadge(ranking) {
if (ranking === 1) return '🥇';
if (ranking === 2) return '🥈';
if (ranking === 3) return '🥉';
return ranking;
}
function rankClass(ranking) {
if (ranking === 1) return 'gold';
if (ranking === 2) return 'silver';
if (ranking === 3) return 'bronze';
return '';
}
function renderLeaderboard(data) {
const { leaderboard, myRank } = data;
if (!leaderboard || leaderboard.length === 0) {
document.getElementById('leaderboard-container').innerHTML = `
<div class="empty-state">
<div style="font-size:52px;margin-bottom:12px">📊</div>
<p style="font-size:15px;font-weight:600;color:#475569">Belum ada data leaderboard.</p>
<p style="font-size:13px">Kerjakan challenge untuk masuk leaderboard!</p>
</div>`;
return;
}
const first = leaderboard.find(x => x.ranking === 1);
const second = leaderboard.find(x => x.ranking === 2);
const third = leaderboard.find(x => x.ranking === 3);
// Podium
let podiumHtml = '';
if (first) {
podiumHtml = `<div class="podium-wrap">`;
if (second) podiumHtml += `
<div class="podium-item rank-2">
<div class="podium-avatar">${podiumAvatarHtml(second)}</div>
<div class="podium-name">${second.nama}</div>
<div class="podium-exp">⭐ ${Number(second.exp).toLocaleString('id')}</div>
<div class="podium-bar">2</div>
</div>`;
podiumHtml += `
<div class="podium-item rank-1">
<div class="podium-avatar">
<span class="podium-crown">👑</span>
${podiumAvatarHtml(first)}
</div>
<div class="podium-name">${first.nama}</div>
<div class="podium-exp">⭐ ${Number(first.exp).toLocaleString('id')}</div>
<div class="podium-bar">1</div>
</div>`;
if (third) podiumHtml += `
<div class="podium-item rank-3">
<div class="podium-avatar">${podiumAvatarHtml(third)}</div>
<div class="podium-name">${third.nama}</div>
<div class="podium-exp">⭐ ${Number(third.exp).toLocaleString('id')}</div>
<div class="podium-bar">3</div>
</div>`;
podiumHtml += `</div>`;
}
// My rank banner
let bannerHtml = '';
if (myRank) {
bannerHtml = `
<div class="my-rank-banner">
${avatarHtml(myRank, 'large')}
<div class="my-rank-num">#${myRank.ranking}</div>
<div class="my-rank-info">
<div class="my-rank-label">Posisimu saat ini</div>
<div class="my-rank-nama">${myRank.nama}</div>
<div class="my-rank-exp">⭐ ${Number(myRank.exp).toLocaleString('id')} EXP</div>
</div>
<div style="font-size:32px">🎯</div>
</div>`;
}
// Table rows
let rowsHtml = leaderboard.map(item => {
const isMe = item.id_siswa === currentSiswaId;
return `
<div class="lb-row ${isMe ? 'highlight' : ''}">
<div class="lb-rank ${rankClass(item.ranking)}">${rankBadge(item.ranking)}</div>
${avatarHtml(item)}
<div style="flex:1">
<div class="lb-nama">
${item.nama}
${isMe ? '<span style="background:#c4b5fd;color:#4c1d95;font-size:10px;padding:2px 7px;border-radius:99px;font-weight:700;margin-left:4px">Kamu</span>' : ''}
</div>
<div class="lb-nisn">${item.nisn}</div>
</div>
<div class="lb-exp">⭐ ${Number(item.exp).toLocaleString('id')}</div>
</div>`;
}).join('');
document.getElementById('leaderboard-container').innerHTML = `
${podiumHtml}
${bannerHtml}
<div class="custom-card">
<p class="section-title">
<span>📋 Semua Peringkat</span>
<span><span class="live-dot"></span><span style="font-size:11px;color:#22c55e;font-weight:600">Live</span></span>
</p>
${rowsHtml}
</div>`;
}
// Polling setiap 10 detik
async function pollLeaderboard() {
try {
const res = await fetch(jsonUrl);
const data = await res.json();
renderLeaderboard(data);
} catch (e) {
// Diam-diam gagal, coba lagi nanti
}
}
// Mulai polling
setInterval(pollLeaderboard, 10000);
// Load langsung saat halaman buka
pollLeaderboard();
</script>
@endpush