challenge & leaderboard added

This commit is contained in:
RetasyaSalsabila 2026-03-11 16:21:51 +07:00
parent 9f31e74818
commit 62d15959a6
16 changed files with 2137 additions and 574 deletions

View File

@ -3,99 +3,185 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Challenge;
use App\Models\Kelas;
use App\Models\SoalChallenge;
use App\Models\Kelas;
use App\Models\Badge;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class ChallengeController extends Controller
{
public function index()
public function index(Request $request)
{
$challenges = Challenge::latest()->get();
$kelass = Kelas::all();
return view('admin.challenge.index', compact('challenges', 'kelass'));
}
$query = Challenge::with(['kelas', 'soal'])
->withCount('soal');
public function create()
{
return view('admin.challenge.create');
}
public function store(Request $request)
{
$request->validate([
'judul_challenge' => 'required',
'exp' => 'required|integer|min:1',
'tenggat_waktu' => 'required|date',
'kelas' => 'required|array',
'pertanyaan' => 'required|array|min:1'
]);
DB::transaction(function () use ($request) {
$challenge = Challenge::create([
'id_admin' => auth('admin')->id(),
'judul_challenge' => $request->judul_challenge,
'deskripsi' => $request->deskripsi,
'exp' => $request->exp,
'tenggat_waktu' => $request->tenggat_waktu,
]);
$challenge->kelas()->attach($request->kelas);
$jumlahSoal = count($request->pertanyaan);
$expPerSoal = floor($request->exp / $jumlahSoal);
foreach ($request->pertanyaan as $i => $pertanyaan) {
SoalChallenge::create([
'id_challenge' => $challenge->id_challenge,
'pertanyaan' => $pertanyaan,
'opsi_a' => $request->opsi_a[$i],
'opsi_b' => $request->opsi_b[$i],
'opsi_c' => $request->opsi_c[$i],
'opsi_d' => $request->opsi_d[$i],
'jawaban_benar' => $request->jawaban_benar[$i],
'exp_per_soal' => $expPerSoal,
]);
if ($request->filled('search')) {
$query->where('judul_challenge', 'like', '%' . $request->search . '%');
}
});
$challenges = $query->orderBy('created_at', 'desc')
->paginate(10)
->appends($request->all());
return redirect()->route('admin.challenge.index')
->with('success', 'Challenge & soal berhasil dibuat!');
}
$kelas = Kelas::orderBy('tingkat')->orderBy('nama_kelas')->get();
$badges = Badge::all();
return view('admin.challenge.index', compact('challenges', 'kelas', 'badges'));
}
public function store(Request $request)
{
$request->validate([
'judul_challenge' => 'required|string|max:200',
'deskripsi' => 'nullable|string',
'exp' => 'required|integer|min:0',
'id_badge' => 'nullable|exists:badges,id_badge',
'tenggat_waktu' => 'required|date|after:now',
'id_kelas' => 'required|array|min:1',
'id_kelas.*' => 'exists:kelas,id_kelas',
// Soal
'pertanyaan' => 'required|array|min:1',
'pertanyaan.*' => 'required|string',
'opsi_a.*' => 'required|string',
'opsi_b.*' => 'required|string',
'opsi_c.*' => 'required|string',
'opsi_d.*' => 'required|string',
'jawaban_benar.*' => 'required|in:A,B,C,D',
'exp_per_soal.*' => 'required|integer|min:0',
], [
'tenggat_waktu.after' => 'Tenggat waktu harus lebih dari sekarang.',
'pertanyaan.required' => 'Minimal harus ada 1 soal.',
'id_kelas.required' => 'Pilih minimal 1 kelas.',
]);
DB::transaction(function () use ($request) {
$admin = Auth::guard('admin')->user();
$challenge = Challenge::create([
'id_admin' => $admin->id_admin,
'judul_challenge' => $request->judul_challenge,
'deskripsi' => $request->deskripsi,
'exp' => $request->exp,
'id_badge' => $request->id_badge,
'tenggat_waktu' => $request->tenggat_waktu,
]);
// Attach ke kelas
$challenge->kelas()->sync($request->id_kelas);
// Simpan soal
foreach ($request->pertanyaan as $i => $pertanyaan) {
SoalChallenge::create([
'id_challenge' => $challenge->id_challenge,
'pertanyaan' => $pertanyaan,
'opsi_a' => $request->opsi_a[$i],
'opsi_b' => $request->opsi_b[$i],
'opsi_c' => $request->opsi_c[$i],
'opsi_d' => $request->opsi_d[$i],
'jawaban_benar' => $request->jawaban_benar[$i],
'exp_per_soal' => $request->exp_per_soal[$i],
]);
}
});
return redirect()->route('admin.challenge.index')
->with('success', 'Challenge berhasil dibuat!');
}
public function show($id)
{
$challenge = Challenge::with(['kelas', 'soal'])->findOrFail($id);
return view('admin.challenge.show', compact('challenge'));
}
public function edit($id)
{
$challenge = Challenge::findOrFail($id);
return view('admin.challenge.edit', compact('challenge'));
$challenge = Challenge::with(['kelas', 'soal'])->findOrFail($id);
$kelas = Kelas::orderBy('tingkat')->orderBy('nama_kelas')->get();
$badges = Badge::all();
return view('admin.challenge.edit', compact('challenge', 'kelas', 'badges'));
}
public function update(Request $request, $id)
{
$challenge = Challenge::findOrFail($id);
$challenge->update([
'judul_challenge' => $request->judul_challenge,
'deskripsi' => $request->deskripsi,
'exp' => $request->exp,
'tenggat_waktu' => $request->tenggat_waktu,
$request->validate([
'judul_challenge' => 'required|string|max:200',
'deskripsi' => 'nullable|string',
'exp' => 'required|integer|min:0',
'id_badge' => 'nullable|exists:badges,id_badge',
'tenggat_waktu' => 'required|date',
'id_kelas' => 'required|array|min:1',
'id_kelas.*' => 'exists:kelas,id_kelas',
'pertanyaan' => 'required|array|min:1',
'pertanyaan.*' => 'required|string',
'opsi_a.*' => 'required|string',
'opsi_b.*' => 'required|string',
'opsi_c.*' => 'required|string',
'opsi_d.*' => 'required|string',
'jawaban_benar.*' => 'required|in:A,B,C,D',
'exp_per_soal.*' => 'required|integer|min:0',
]);
DB::transaction(function () use ($request, $challenge) {
$challenge->update([
'judul_challenge' => $request->judul_challenge,
'deskripsi' => $request->deskripsi,
'exp' => $request->exp,
'id_badge' => $request->id_badge,
'tenggat_waktu' => $request->tenggat_waktu,
]);
$challenge->kelas()->sync($request->id_kelas);
// Hapus soal lama, insert ulang
SoalChallenge::where('id_challenge', $challenge->id_challenge)->delete();
foreach ($request->pertanyaan as $i => $pertanyaan) {
SoalChallenge::create([
'id_challenge' => $challenge->id_challenge,
'pertanyaan' => $pertanyaan,
'opsi_a' => $request->opsi_a[$i],
'opsi_b' => $request->opsi_b[$i],
'opsi_c' => $request->opsi_c[$i],
'opsi_d' => $request->opsi_d[$i],
'jawaban_benar' => $request->jawaban_benar[$i],
'exp_per_soal' => $request->exp_per_soal[$i],
]);
}
});
return redirect()->route('admin.challenge.index')
->with('success', 'Challenge berhasil diupdate!');
}
public function destroy($id)
{
$challenge = Challenge::findOrFail($id);
$challenge->delete();
Challenge::findOrFail($id)->delete();
return redirect()->route('admin.challenge.index')
->with('success', 'Challenge berhasil dihapus!');
->with('success', 'Challenge berhasil dihapus.');
}
}
/**
* AJAX return data challenge untuk modal edit
*/
public function editData($id)
{
$challenge = Challenge::with(['kelas', 'soal'])->findOrFail($id);
return response()->json([
'judul_challenge' => $challenge->judul_challenge,
'deskripsi' => $challenge->deskripsi,
'exp' => $challenge->exp,
'id_badge' => $challenge->id_badge,
'tenggat_waktu' => $challenge->tenggat_waktu,
'kelas' => $challenge->kelas->pluck('id_kelas'),
'soal' => $challenge->soal,
]);
}
}

View File

@ -0,0 +1,172 @@
<?php
namespace App\Http\Controllers\Siswa;
use App\Http\Controllers\Controller;
use App\Models\Challenge;
use App\Models\PesertaChallenge;
use App\Models\Leaderboard;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class ChallengeController extends Controller
{
public function index()
{
$siswa = Auth::guard('siswa')->user();
$challenges = Challenge::whereHas('kelas', function ($q) use ($siswa) {
$q->where('challenge_kelas.id_kelas', $siswa->id_kelas);
})
->with(['soal'])
->withCount('soal')
->orderBy('tenggat_waktu', 'asc')
->get();
$sudahDikerjakan = PesertaChallenge::where('id_siswa', $siswa->id_siswa)
->where('status', 'selesai')
->pluck('id_challenge')
->toArray();
return view('siswa.challenge.index', compact('challenges', 'sudahDikerjakan'));
}
public function kerjakan($id_challenge)
{
$siswa = Auth::guard('siswa')->user();
$challenge = Challenge::whereHas('kelas', function ($q) use ($siswa) {
$q->where('challenge_kelas.id_kelas', $siswa->id_kelas);
})
->with('soal')
->findOrFail($id_challenge);
$sudah = PesertaChallenge::where('id_siswa', $siswa->id_siswa)
->where('id_challenge', $id_challenge)
->where('status', 'selesai')
->exists();
if ($sudah) {
return redirect()->route('siswa.challenge.hasil', $id_challenge);
}
if (Carbon::parse($challenge->tenggat_waktu)->isPast()) {
return redirect()->route('siswa.challenge.index')
->with('error', 'Challenge ini sudah melewati tenggat waktu.');
}
if ($challenge->soal->isEmpty()) {
return redirect()->route('siswa.challenge.index')
->with('error', 'Challenge ini belum memiliki soal.');
}
return view('siswa.challenge.kerjakan', compact('challenge'));
}
public function submit(Request $request, $id_challenge)
{
$siswa = Auth::guard('siswa')->user();
$challenge = Challenge::whereHas('kelas', function ($q) use ($siswa) {
$q->where('challenge_kelas.id_kelas', $siswa->id_kelas);
})
->with('soal')
->findOrFail($id_challenge);
$sudah = PesertaChallenge::where('id_siswa', $siswa->id_siswa)
->where('id_challenge', $id_challenge)
->where('status', 'selesai')
->exists();
if ($sudah) {
return redirect()->route('siswa.challenge.hasil', $id_challenge);
}
if (Carbon::parse($challenge->tenggat_waktu)->isPast()) {
return redirect()->route('siswa.challenge.index')
->with('error', 'Challenge sudah melewati tenggat waktu.');
}
$jawaban = $request->input('jawaban', []);
$totalExp = 0;
$jawabanJson = [];
foreach ($challenge->soal as $soal) {
$jwb = $jawaban[$soal->id_soal] ?? null;
$jawabanJson[$soal->id_soal] = $jwb;
if ($jwb && strtoupper($jwb) === strtoupper($soal->jawaban_benar)) {
$totalExp += $soal->exp_per_soal;
}
}
// Semester & tahun ajaran otomatis
$now = Carbon::now();
$semester = $now->month >= 7 ? '1' : '2';
$tahunAjaran = $now->month >= 7
? $now->year . '/' . ($now->year + 1)
: ($now->year - 1) . '/' . $now->year;
DB::transaction(function () use ($siswa, $challenge, $jawabanJson, $totalExp, $semester, $tahunAjaran) {
PesertaChallenge::create([
'id_challenge' => $challenge->id_challenge,
'id_siswa' => $siswa->id_siswa,
'jawaban' => json_encode($jawabanJson),
'waktu_submit' => Carbon::now(),
'exp' => $totalExp,
'status' => 'selesai',
]);
// firstOrCreate dengan semester & tahun_ajaran sebagai key
$lb = Leaderboard::firstOrCreate(
[
'id_siswa' => $siswa->id_siswa,
'id_kelas' => $siswa->id_kelas,
'semester' => $semester,
'tahun_ajaran' => $tahunAjaran,
],
['total_exp' => 0, 'ranking' => 0]
);
$lb->increment('total_exp', $totalExp);
// Recalculate ranking per kelas + semester + tahun ajaran
Leaderboard::where('id_kelas', $siswa->id_kelas)
->where('semester', $semester)
->where('tahun_ajaran', $tahunAjaran)
->orderBy('total_exp', 'desc')
->get()
->each(fn($row, $i) => $row->update(['ranking' => $i + 1]));
});
return redirect()->route('siswa.challenge.hasil', $id_challenge);
}
public function hasil($id_challenge)
{
$siswa = Auth::guard('siswa')->user();
$challenge = Challenge::with('soal')->findOrFail($id_challenge);
$peserta = PesertaChallenge::where('id_siswa', $siswa->id_siswa)
->where('id_challenge', $id_challenge)
->firstOrFail();
$jawabanSiswa = json_decode($peserta->jawaban, true) ?? [];
$benar = 0; $salah = 0;
foreach ($challenge->soal as $soal) {
$jwb = $jawabanSiswa[$soal->id_soal] ?? null;
strtoupper((string)$jwb) === strtoupper($soal->jawaban_benar) ? $benar++ : $salah++;
}
$totalSoal = $challenge->soal->count();
$persentase = $totalSoal > 0 ? round(($benar / $totalSoal) * 100) : 0;
return view('siswa.challenge.hasil', compact(
'challenge', 'peserta', 'jawabanSiswa',
'benar', 'salah', 'totalSoal', 'persentase'
));
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers\Siswa;
use App\Http\Controllers\Controller;
use App\Models\Leaderboard;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
class LeaderboardController extends Controller
{
public function index()
{
$siswa = Auth::guard('siswa')->user();
$now = Carbon::now();
$semester = $now->month >= 7 ? '1' : '2';
$tahunAjaran = $now->month >= 7
? $now->year . '/' . ($now->year + 1)
: ($now->year - 1) . '/' . $now->year;
// Top leaderboard kelas siswa ini
$leaderboard = Leaderboard::with('siswa')
->where('id_kelas', $siswa->id_kelas)
->where('semester', $semester)
->where('tahun_ajaran', $tahunAjaran)
->orderBy('total_exp', 'desc')
->get()
->map(function ($item, $i) {
return [
'ranking' => $i + 1,
'nama' => optional($item->siswa)->nama ?? '-',
'nisn' => optional($item->siswa)->nisn ?? '-',
'exp' => $item->total_exp,
'id_siswa'=> $item->id_siswa,
];
});
// Posisi siswa yang login
$myRank = $leaderboard->firstWhere('id_siswa', $siswa->id_siswa);
return view('siswa.leaderboard.index', compact(
'leaderboard', 'myRank', 'semester', 'tahunAjaran'
));
}
}

View File

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('leaderboards', function (Blueprint $table) {
$table->integer('ranking')->default(0)->change();
$table->string('semester', 50)->default('1')->change();
$table->string('tahun_ajaran', 20)->default('2024/2025')->change();
});
}
public function down(): void
{
Schema::table('leaderboards', function (Blueprint $table) {
$table->integer('ranking')->default(null)->change();
$table->string('semester', 50)->default(null)->change();
$table->string('tahun_ajaran', 20)->default(null)->change();
});
}
};

View File

@ -1,23 +0,0 @@
@extends('admin.layouts.app')
@section('content')
<h2>Tambah Challenge</h2>
<form action="{{ route('admin.challenge.store') }}" method="POST">
@csrf
<label>Judul</label><br>
<input type="text" name="judul_challenge"><br><br>
<label>Deskripsi</label><br>
<textarea name="deskripsi"></textarea><br><br>
<label>EXP</label><br>
<input type="number" name="exp"><br><br>
<label>Tenggat Waktu</label><br>
<input type="datetime-local" name="tenggat_waktu"><br><br>
<button type="submit">Simpan</button>
</form>
@endsection

View File

@ -1,26 +0,0 @@
@extends('admin.layouts.app')
@section('content')
<h2>Edit Challenge</h2>
<form action="{{ route('admin.challenge.update', $challenge->id_challenge) }}" method="POST">
@csrf
@method('PUT')
<label>Judul</label><br>
<input type="text" name="judul_challenge" value="{{ $challenge->judul_challenge }}"><br><br>
<label>Deskripsi</label><br>
<textarea name="deskripsi">{{ $challenge->deskripsi }}</textarea><br><br>
<label>EXP</label><br>
<input type="number" name="exp" value="{{ $challenge->exp }}"><br><br>
<label>Tenggat Waktu</label><br>
<input type="datetime-local"
name="tenggat_waktu"
value="{{ \Carbon\Carbon::parse($challenge->tenggat_waktu)->format('Y-m-d\TH:i') }}"><br><br>
<button type="submit">Update</button>
</form>
@endsection

View File

@ -5,135 +5,299 @@
@section('content')
<style>
.page-title {
font-size: 30px;
font-weight: 800;
margin-bottom: 10px;
margin-top: -20px;
}
.page-title {
font-size: 30px;
font-weight: 800;
margin-bottom: 10px;
margin-top: -20px;
}
.custom-card {
background: white;
border-radius: 20px;
border: 2px solid #e5e5e5;
padding: 25px;
}
.custom-card {
background: white;
border-radius: 20px;
border: 2px solid #e5e5e5;
padding: 25px;
}
.btn-primary-custom {
background: #2b8ef3;
color: white;
border-radius: 10px;
padding: 8px 18px;
border: none;
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 14px;
text-decoration: none;
}
.btn-primary-custom {
background: #2b8ef3;
color: white;
border-radius: 10px;
padding: 8px 18px;
border: none;
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 14px;
text-decoration: none;
cursor: pointer;
font-family: 'Poppins', sans-serif;
}
.table-header {
background: #a5e6ba;
}
.table-header { background: #a5e6ba; }
.search-box {
background: #a5e6ba;
border-radius: 30px;
padding: 6px 15px;
display: flex;
align-items: center;
gap: 8px;
}
.search-box {
background: #a5e6ba;
border-radius: 30px;
padding: 6px 15px;
display: flex;
align-items: center;
gap: 8px;
}
.search-box input {
border: none;
outline: none;
background: transparent;
width: 150px;
}
.search-box input {
border: none;
outline: none;
background: transparent;
width: 160px;
}
.action-icon {
width: 20px;
cursor: pointer;
margin: 0 5px;
}
.action-icon { width: 20px; cursor: pointer; margin: 0 4px; }
.modal-header-pastel {
background: #FFD97D !important;
color: black !important;
border-bottom: none;
padding: 15px 20px;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
}
.deadline-badge {
font-size: 11px;
font-weight: 700;
padding: 3px 8px;
border-radius: 99px;
display: inline-block;
}
.deadline-aktif { background: #dcfce7; color: #16a34a; }
.deadline-lewat { background: #fee2e2; color: #ef4444; }
.kelas-chip {
display: inline-block;
background: #e6f0ff;
color: #1d4ed8;
font-size: 11px;
font-weight: 600;
padding: 2px 8px;
border-radius: 99px;
margin: 1px;
}
.soal-count {
background: #f0fdf4;
color: #16a34a;
font-size: 12px;
font-weight: 700;
padding: 3px 10px;
border-radius: 99px;
}
.alert-success-custom {
background: #dcfce7;
color: #166534;
border-radius: 10px;
padding: 12px 16px;
margin-bottom: 16px;
font-weight: 500;
font-size: 14px;
}
/* MODAL */
.modal-header-challenge {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border-bottom: none;
border-radius: 16px 16px 0 0;
}
.modal-header-challenge .btn-close { filter: brightness(0) invert(1); }
.modal-content {
border-radius: 16px;
border: none;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
.modal-body label { font-weight: 600; font-size: 13px; }
/* SOAL CARD */
.soal-card {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 14px;
padding: 16px 18px;
margin-bottom: 12px;
position: relative;
}
.soal-card .soal-number {
position: absolute;
top: -10px;
left: 16px;
background: #667eea;
color: white;
font-size: 11px;
font-weight: 700;
padding: 2px 10px;
border-radius: 99px;
}
.soal-card .btn-hapus-soal {
position: absolute;
top: 10px;
right: 12px;
background: #fee2e2;
color: #ef4444;
border: none;
border-radius: 8px;
width: 28px;
height: 28px;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.soal-card .btn-hapus-soal:hover { background: #fca5a5; }
.opsi-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 8px;
}
.opsi-item { display: flex; flex-direction: column; gap: 4px; }
.opsi-item label { font-size: 12px; color: #64748b; font-weight: 600; }
.opsi-item input { border-radius: 8px; border: 1px solid #cbd5e1; padding: 6px 10px; font-size: 13px; }
.jawaban-row {
display: flex;
gap: 12px;
align-items: center;
margin-top: 10px;
background: #fff;
border-radius: 8px;
padding: 8px 12px;
border: 1px solid #e2e8f0;
}
.jawaban-row label { font-size: 12px; color: #64748b; font-weight: 600; min-width: 90px; }
.btn-tambah-soal {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 10px;
padding: 9px 18px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
width: 100%;
margin-top: 4px;
font-family: 'Poppins', sans-serif;
}
.kelas-checkbox-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.kelas-check-item {
display: flex;
align-items: center;
gap: 8px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 8px 10px;
cursor: pointer;
transition: all 0.2s;
}
.kelas-check-item:hover { background: #e6f0ff; border-color: #2b8ef3; }
.kelas-check-item input[type="checkbox"] { cursor: pointer; }
.section-divider {
font-size: 13px;
font-weight: 700;
color: #667eea;
border-bottom: 2px solid #e6f0ff;
padding-bottom: 6px;
margin: 16px 0 12px;
}
</style>
<h3 class="page-title">DAFTAR CHALLENGE</h3>
@if(session('success'))
<div class="alert-success-custom"> {{ session('success') }}</div>
@endif
<div class="custom-card">
<div class="d-flex justify-content-between align-items-center mb-3">
<button class="btn-primary-custom" data-bs-toggle="modal" data-bs-target="#modalTambah">
<img src="{{ asset('images/icon/main/add.png') }}" width="18">
Tambah Data
<button class="btn-primary-custom" onclick="openTambahModal()">
<img src="{{ asset('images/icon/main/add.png') }}" width="18"> Tambah Challenge
</button>
<form method="GET">
<div class="search-box">
<input type="text" name="search" placeholder="Cari"
<input type="text" name="search" placeholder="Cari challenge..."
value="{{ request('search') }}">
<button style="border:none;background:none">
<button style="border:none;background:none" type="submit">
<img src="{{ asset('images/icon/main/search.png') }}" width="18">
</button>
</div>
</form>
</div>
{{-- Alert --}}
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show">
{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<table class="table text-center align-middle">
<thead class="table-header">
<tr>
<th>No</th>
<th>Judul Challenge</th>
<th>EXP</th>
<th>Kelas</th>
<th>Soal</th>
<th>Total EXP</th>
<th>Tenggat</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
@forelse($challenges as $index => $challenge)
@forelse($challenges as $i => $ch)
@php $isLewat = \Carbon\Carbon::parse($ch->tenggat_waktu)->isPast(); @endphp
<tr>
<td>{{ $loop->iteration }}</td>
<td>{{ $challenge->judul_challenge }}</td>
<td>{{ $challenge->exp }}</td>
<td>{{ \Carbon\Carbon::parse($challenge->tenggat_waktu)->format('d M Y H:i') }}</td>
<td>{{ $challenges->firstItem() + $i }}</td>
<td style="text-align:left">
<div style="font-weight:700;color:#1e293b">{{ $ch->judul_challenge }}</div>
@if($ch->deskripsi)
<div style="font-size:12px;color:#94a3b8">{{ Str::limit($ch->deskripsi, 50) }}</div>
@endif
</td>
<td>
<button onclick="openEditModal(
'{{ $challenge->id_challenge }}',
'{{ $challenge->judul_challenge }}',
'{{ $challenge->deskripsi }}',
'{{ $challenge->exp }}',
'{{ \Carbon\Carbon::parse($challenge->tenggat_waktu)->format('Y-m-d\TH:i') }}'
)" style="border:none;background:none">
@foreach($ch->kelas as $k)
<span class="kelas-chip">{{ $k->tingkat }} {{ $k->nama_kelas }}</span>
@endforeach
</td>
<td><span class="soal-count">{{ $ch->soal_count }} soal</span></td>
<td style="font-weight:700;color:#667eea">{{ $ch->exp }} EXP</td>
<td>
<span class="deadline-badge {{ $isLewat ? 'deadline-lewat' : 'deadline-aktif' }}">
{{ $isLewat ? '⏰ Lewat' : '✅ Aktif' }}
</span>
<div style="font-size:11px;color:#64748b;margin-top:3px">
{{ \Carbon\Carbon::parse($ch->tenggat_waktu)->format('d M Y, H:i') }}
</div>
</td>
<td>
<button onclick="openEditModal({{ $ch->id_challenge }})"
style="border:none;background:none">
<img src="{{ asset('images/icon/main/edit.png') }}" class="action-icon">
</button>
<form action="{{ route('admin.challenge.destroy', $challenge->id_challenge) }}"
<a href="{{ route('admin.challenge.show', $ch->id_challenge) }}"
style="border:none;background:none;display:inline">
<img src="{{ asset('images/icon/main/search.png') }}" class="action-icon">
</a>
<form action="{{ route('admin.challenge.destroy', $ch->id_challenge) }}"
method="POST" class="d-inline"
onsubmit="return confirm('Yakin ingin menghapus challenge ini?')">
@csrf
@method('DELETE')
onsubmit="return confirm('Yakin hapus challenge ini?')">
@csrf @method('DELETE')
<button type="submit" style="border:none;background:none">
<img src="{{ asset('images/icon/main/del.png') }}" class="action-icon">
</button>
@ -142,234 +306,296 @@
</tr>
@empty
<tr>
<td colspan="5">Belum ada challenge</td>
<td colspan="7" class="text-muted py-4">Belum ada challenge.</td>
</tr>
@endforelse
</tbody>
</table>
<div class="d-flex justify-content-end">{{ $challenges->links() }}</div>
</div>
{{-- MODAL TAMBAH --}}
<div class="modal fade" id="modalTambah">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<form action="{{ route('admin.challenge.store') }}" method="POST">
@csrf
<div class="modal-header modal-header-pastel">
<h5 class="modal-title w-100 text-center">Tambah Challenge</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
{{-- STEP 1 --}}
<div class="modal-body" id="tambahStep1">
<div class="mb-3">
<label>Judul *</label>
<input type="text" name="judul_challenge" class="form-control" required>
</div>
<div class="mb-3">
<label>Deskripsi</label>
<textarea name="deskripsi" class="form-control"></textarea>
</div>
<div class="mb-3">
<label>Total EXP *</label>
<input type="number" name="exp" class="form-control" required>
</div>
<div class="mb-3">
<label>Tenggat *</label>
<input type="datetime-local" name="tenggat_waktu" class="form-control" required>
</div>
<div class="mb-3">
<label>Pilih Kelas *</label>
@foreach($kelass as $kelas)
<div class="form-check">
<input class="form-check-input"
type="checkbox"
name="kelas[]"
value="{{ $kelas->id_kelas }}">
<label class="form-check-label">
{{ $kelas->tingkat }} - {{ $kelas->nama_kelas }}
</label>
</div>
@endforeach
</div>
</div>
{{-- STEP 2 --}}
<div class="modal-body d-none" id="tambahStep2">
<h5 class="mb-3">Tambah Soal</h5>
<div id="soalContainer"></div>
<button type="button"
class="btn btn-primary"
onclick="addSoal()">
+ Tambah Soal
</button>
</div>
<div class="modal-footer">
<button type="button"
class="btn btn-secondary"
data-bs-dismiss="modal">
Cancel
</button>
<button type="button"
class="btn btn-warning d-none"
id="tambahBackBtn"
onclick="backTambahStep()">
Back
</button>
<button type="button"
class="btn btn-primary"
id="tambahNextBtn"
onclick="nextTambahStep()">
Next
</button>
<button type="submit"
class="btn btn-success d-none"
id="tambahSubmitBtn">
Simpan Semua
</button>
</div>
</form>
</div>
</div>
</div>
{{-- MODAL EDIT --}}
<div class="modal fade" id="modalEdit">
<div class="modal-dialog modal-dialog-centered">
{{-- ============================================================ --}}
{{-- MODAL TAMBAH --}}
{{-- ============================================================ --}}
<div class="modal fade" id="modalTambah" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header modal-header-pastel">
<h5 class="modal-title w-100 text-center">Edit Challenge</h5>
<div class="modal-header modal-header-challenge">
<h5 class="modal-title">🏆 Tambah Challenge Baru</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="formEdit" method="POST">
<form action="{{ route('admin.challenge.store') }}" method="POST" id="formTambah">
@csrf
@method('PUT')
<div class="modal-body">
<div class="mb-3">
<label>Judul Challenge *</label>
<input type="text" id="editJudul" name="judul_challenge" class="form-control" required>
{{-- INFO CHALLENGE --}}
<div class="section-divider">📋 Informasi Challenge</div>
<div class="row g-3 mb-2">
<div class="col-8">
<label>Judul Challenge <span class="text-danger">*</span></label>
<input type="text" name="judul_challenge" class="form-control"
placeholder="Contoh: Challenge Matematika Minggu Ini" required>
</div>
<div class="col-4">
<label>Total EXP <span class="text-danger">*</span></label>
<input type="number" name="exp" class="form-control" value="100" min="0" required>
</div>
</div>
<div class="mb-3">
<label>Deskripsi</label>
<textarea id="editDeskripsi" name="deskripsi" class="form-control"></textarea>
<textarea name="deskripsi" class="form-control" rows="2"
placeholder="Deskripsi singkat challenge (opsional)"></textarea>
</div>
<div class="mb-3">
<label>EXP *</label>
<input type="number" id="editExp" name="exp" class="form-control" required>
<div class="row g-3 mb-3">
<div class="col-6">
<label>Tenggat Waktu <span class="text-danger">*</span></label>
<input type="datetime-local" name="tenggat_waktu" class="form-control" required
min="{{ now()->format('Y-m-d\TH:i') }}">
</div>
<div class="col-6">
<label>Badge Reward</label>
<select name="id_badge" class="form-control">
<option value="">-- Tanpa Badge --</option>
@foreach($badges as $badge)
<option value="{{ $badge->id_badge }}">{{ $badge->nama_badge }}</option>
@endforeach
</select>
</div>
</div>
<div class="mb-3">
<label>Tenggat Waktu *</label>
<input type="datetime-local" id="editTenggat" name="tenggat_waktu" class="form-control" required>
{{-- PILIH KELAS --}}
<div class="section-divider">🏫 Target Kelas</div>
<div class="kelas-checkbox-grid mb-3">
@foreach($kelas as $k)
<label class="kelas-check-item">
<input type="checkbox" name="id_kelas[]" value="{{ $k->id_kelas }}">
<span style="font-size:13px;font-weight:600">{{ $k->tingkat }} {{ $k->nama_kelas }}</span>
</label>
@endforeach
</div>
{{-- SOAL --}}
<div class="section-divider">📝 Daftar Soal</div>
<div id="soalContainer"></div>
<button type="button" class="btn-tambah-soal" onclick="tambahSoal('soalContainer')">
+ Tambah Soal
</button>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-success">Update</button>
<button type="submit" class="btn btn-success">💾 Simpan Challenge</button>
</div>
</form>
</div>
</div>
</div>
{{-- ============================================================ --}}
{{-- MODAL EDIT --}}
{{-- ============================================================ --}}
<div class="modal fade" id="modalEdit" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header modal-header-challenge">
<h5 class="modal-title">✏️ Edit Challenge</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="formEdit" method="POST">
@csrf @method('PUT')
<div class="modal-body">
<div class="section-divider">📋 Informasi Challenge</div>
<div class="row g-3 mb-2">
<div class="col-8">
<label>Judul Challenge <span class="text-danger">*</span></label>
<input type="text" name="judul_challenge" id="editJudul" class="form-control" required>
</div>
<div class="col-4">
<label>Total EXP <span class="text-danger">*</span></label>
<input type="number" name="exp" id="editExp" class="form-control" min="0" required>
</div>
</div>
<div class="mb-3">
<label>Deskripsi</label>
<textarea name="deskripsi" id="editDeskripsi" class="form-control" rows="2"></textarea>
</div>
<div class="row g-3 mb-3">
<div class="col-6">
<label>Tenggat Waktu <span class="text-danger">*</span></label>
<input type="datetime-local" name="tenggat_waktu" id="editTenggat" class="form-control" required>
</div>
<div class="col-6">
<label>Badge Reward</label>
<select name="id_badge" id="editBadge" class="form-control">
<option value="">-- Tanpa Badge --</option>
@foreach($badges as $badge)
<option value="{{ $badge->id_badge }}">{{ $badge->nama_badge }}</option>
@endforeach
</select>
</div>
</div>
<div class="section-divider">🏫 Target Kelas</div>
<div class="kelas-checkbox-grid mb-3" id="editKelasContainer">
@foreach($kelas as $k)
<label class="kelas-check-item">
<input type="checkbox" name="id_kelas[]"
value="{{ $k->id_kelas }}" class="edit-kelas-check">
<span style="font-size:13px;font-weight:600">{{ $k->tingkat }} {{ $k->nama_kelas }}</span>
</label>
@endforeach
</div>
<div class="section-divider">📝 Daftar Soal</div>
<div id="editSoalContainer"></div>
<button type="button" class="btn-tambah-soal" onclick="tambahSoal('editSoalContainer')">
+ Tambah Soal
</button>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-warning">💾 Update Challenge</button>
</div>
</form>
</div>
</div>
</div>
<script>
let soalCountTambah = 0;
let soalCountEdit = 0;
function openEditModal(id, judul, deskripsi, exp, tenggat) {
document.getElementById('editJudul').value = judul;
document.getElementById('editDeskripsi').value = deskripsi;
document.getElementById('editExp').value = exp;
document.getElementById('editTenggat').value = tenggat;
document.getElementById('formEdit').action = '/admin/challenge/' + id;
// ===== TAMBAH SOAL BARU =====
function tambahSoal(containerId, data = null) {
const isEdit = containerId === 'editSoalContainer';
const count = isEdit ? ++soalCountEdit : ++soalCountTambah;
const container = document.getElementById(containerId);
var modal = new bootstrap.Modal(document.getElementById('modalEdit'));
modal.show();
}
const card = document.createElement('div');
card.className = 'soal-card';
card.innerHTML = `
<span class="soal-number">Soal ${count}</span>
<button type="button" class="btn-hapus-soal" onclick="hapusSoal(this, '${containerId}')"></button>
function nextTambahStep(){
document.getElementById('tambahStep1').classList.add('d-none');
document.getElementById('tambahStep2').classList.remove('d-none');
document.getElementById('tambahNextBtn').classList.add('d-none');
document.getElementById('tambahSubmitBtn').classList.remove('d-none');
document.getElementById('tambahBackBtn').classList.remove('d-none');
}
function backTambahStep(){
document.getElementById('tambahStep1').classList.remove('d-none');
document.getElementById('tambahStep2').classList.add('d-none');
document.getElementById('tambahNextBtn').classList.remove('d-none');
document.getElementById('tambahSubmitBtn').classList.add('d-none');
document.getElementById('tambahBackBtn').classList.add('d-none');
}
function addSoal(){
let html = `
<div class="border rounded p-3 mb-3">
<div class="mb-2">
<label>Pertanyaan</label>
<textarea name="pertanyaan[]" class="form-control" required></textarea>
<div class="mb-2" style="margin-top:10px">
<label>Pertanyaan <span class="text-danger">*</span></label>
<textarea name="pertanyaan[]" class="form-control" rows="2"
placeholder="Tuliskan pertanyaan..." required>${data ? data.pertanyaan : ''}</textarea>
</div>
<div class="row">
<div class="col-md-6 mb-2">
<input type="text" name="opsi_a[]" class="form-control" placeholder="Opsi A" required>
<div class="opsi-grid">
<div class="opsi-item">
<label>Opsi A</label>
<input type="text" name="opsi_a[]" class="form-control"
placeholder="Opsi A" required value="${data ? data.opsi_a : ''}">
</div>
<div class="col-md-6 mb-2">
<input type="text" name="opsi_b[]" class="form-control" placeholder="Opsi B" required>
<div class="opsi-item">
<label>Opsi B</label>
<input type="text" name="opsi_b[]" class="form-control"
placeholder="Opsi B" required value="${data ? data.opsi_b : ''}">
</div>
<div class="col-md-6 mb-2">
<input type="text" name="opsi_c[]" class="form-control" placeholder="Opsi C" required>
<div class="opsi-item">
<label>Opsi C</label>
<input type="text" name="opsi_c[]" class="form-control"
placeholder="Opsi C" required value="${data ? data.opsi_c : ''}">
</div>
<div class="col-md-6 mb-2">
<input type="text" name="opsi_d[]" class="form-control" placeholder="Opsi D" required>
<div class="opsi-item">
<label>Opsi D</label>
<input type="text" name="opsi_d[]" class="form-control"
placeholder="Opsi D" required value="${data ? data.opsi_d : ''}">
</div>
</div>
<div class="mb-2">
<div class="jawaban-row">
<label>Jawaban Benar</label>
<select name="jawaban_benar[]" class="form-control" required>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
<select name="jawaban_benar[]" class="form-select" style="width:100px" required>
${['A','B','C','D'].map(o =>
`<option value="${o}" ${data && data.jawaban_benar === o ? 'selected' : ''}>${o}</option>`
).join('')}
</select>
<label style="margin-left:16px">EXP per Soal</label>
<input type="number" name="exp_per_soal[]" class="form-control" style="width:90px"
min="0" value="${data ? data.exp_per_soal : 10}" required>
</div>
</div>
`;
document.getElementById('soalContainer').insertAdjacentHTML('beforeend', html);
container.appendChild(card);
renumberSoal(containerId);
}
// ===== HAPUS SOAL =====
function hapusSoal(btn, containerId) {
const container = document.getElementById(containerId);
if (container.querySelectorAll('.soal-card').length <= 1) {
alert('Minimal harus ada 1 soal.');
return;
}
btn.closest('.soal-card').remove();
renumberSoal(containerId);
}
// ===== RENUMBER SOAL =====
function renumberSoal(containerId) {
const cards = document.querySelectorAll(`#${containerId} .soal-card`);
cards.forEach((card, i) => {
const num = card.querySelector('.soal-number');
if (num) num.textContent = `Soal ${i + 1}`;
});
}
// ===== BUKA MODAL TAMBAH =====
function openTambahModal() {
soalCountTambah = 0;
document.getElementById('soalContainer').innerHTML = '';
document.getElementById('formTambah').reset();
tambahSoal('soalContainer'); // default 1 soal
new bootstrap.Modal(document.getElementById('modalTambah')).show();
}
// ===== BUKA MODAL EDIT =====
async function openEditModal(idChallenge) {
soalCountEdit = 0;
document.getElementById('editSoalContainer').innerHTML = '';
// Fetch data challenge via AJAX
const res = await fetch(`/admin/challenge/${idChallenge}/edit-data`);
const data = await res.json();
// Isi form
document.getElementById('formEdit').action = `/admin/challenge/${idChallenge}`;
document.getElementById('editJudul').value = data.judul_challenge;
document.getElementById('editExp').value = data.exp;
document.getElementById('editDeskripsi').value = data.deskripsi ?? '';
document.getElementById('editBadge').value = data.id_badge ?? '';
// Format datetime-local
const dt = new Date(data.tenggat_waktu);
const pad = n => String(n).padStart(2,'0');
document.getElementById('editTenggat').value =
`${dt.getFullYear()}-${pad(dt.getMonth()+1)}-${pad(dt.getDate())}T${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
// Centang kelas
document.querySelectorAll('.edit-kelas-check').forEach(cb => {
cb.checked = data.kelas.includes(parseInt(cb.value));
});
// Isi soal
data.soal.forEach(s => tambahSoal('editSoalContainer', s));
new bootstrap.Modal(document.getElementById('modalEdit')).show();
}
</script>
@endsection

View File

@ -0,0 +1,205 @@
@extends('admin.layouts.app')
@section('title', 'Detail Challenge')
@section('content')
<style>
.back-link {
display: inline-flex;
align-items: center;
gap: 8px;
color: #2b8ef3;
font-weight: 600;
font-size: 14px;
text-decoration: none;
margin-bottom: 20px;
}
.back-link:hover { text-decoration: underline; }
.info-card {
background: white;
border-radius: 20px;
border: 2px solid #e5e5e5;
padding: 24px 28px;
margin-bottom: 20px;
border-left: 5px solid #667eea;
}
.info-title { font-size: 22px; font-weight: 800; color: #1e293b; margin: 0 0 12px; }
.meta-chip {
display: inline-flex;
align-items: center;
gap: 6px;
background: #f1f5f9;
color: #475569;
font-size: 13px;
font-weight: 500;
padding: 5px 12px;
border-radius: 99px;
}
.meta-chip.purple { background: #ede9fe; color: #6d28d9; }
.meta-chip.green { background: #dcfce7; color: #16a34a; }
.meta-chip.red { background: #fee2e2; color: #dc2626; }
.kelas-chip {
display: inline-block;
background: #e6f0ff;
color: #1d4ed8;
font-size: 12px;
font-weight: 600;
padding: 3px 10px;
border-radius: 99px;
margin: 2px;
}
.soal-card {
background: white;
border-radius: 16px;
border: 2px solid #e5e5e5;
padding: 20px;
margin-bottom: 14px;
}
.soal-number-badge {
display: inline-block;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
font-size: 12px;
font-weight: 700;
padding: 3px 12px;
border-radius: 99px;
margin-bottom: 10px;
}
.soal-pertanyaan {
font-weight: 600;
font-size: 15px;
color: #1e293b;
margin-bottom: 12px;
line-height: 1.6;
}
.opsi-list { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.opsi-item {
display: flex;
align-items: center;
gap: 10px;
background: #f8fafc;
border-radius: 10px;
padding: 9px 14px;
font-size: 14px;
border: 2px solid transparent;
}
.opsi-item.benar {
background: #dcfce7;
border-color: #22c55e;
font-weight: 700;
color: #15803d;
}
.opsi-label {
width: 28px;
height: 28px;
border-radius: 50%;
background: #e2e8f0;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 13px;
flex-shrink: 0;
}
.opsi-item.benar .opsi-label {
background: #22c55e;
color: white;
}
.exp-badge {
background: #fef9c3;
color: #b45309;
font-size: 12px;
font-weight: 700;
padding: 3px 10px;
border-radius: 99px;
margin-top: 10px;
display: inline-block;
}
.custom-card {
background: white;
border-radius: 20px;
border: 2px solid #e5e5e5;
padding: 25px;
margin-bottom: 20px;
}
.section-title { font-size: 16px; font-weight: 700; color: #1e293b; margin-bottom: 14px; }
</style>
<a href="{{ route('admin.challenge.index') }}" class="back-link"> Kembali ke Daftar Challenge</a>
@php
$isLewat = \Carbon\Carbon::parse($challenge->tenggat_waktu)->isPast();
@endphp
{{-- INFO --}}
<div class="info-card">
<h2 class="info-title">🏆 {{ $challenge->judul_challenge }}</h2>
<div class="d-flex flex-wrap gap-2 mb-3">
<span class="meta-chip purple"> {{ $challenge->exp }} EXP</span>
<span class="meta-chip {{ $isLewat ? 'red' : 'green' }}">
{{ \Carbon\Carbon::parse($challenge->tenggat_waktu)->format('d M Y, H:i') }}
{{ $isLewat ? 'Sudah lewat' : 'Masih aktif' }}
</span>
<span class="meta-chip">📝 {{ $challenge->soal->count() }} soal</span>
</div>
<div class="mb-2">
@foreach($challenge->kelas as $k)
<span class="kelas-chip">{{ $k->tingkat }} {{ $k->nama_kelas }}</span>
@endforeach
</div>
@if($challenge->deskripsi)
<div style="background:#f8fafc;border-radius:10px;padding:12px 16px;font-size:14px;color:#475569;margin-top:10px;border:1px solid #e2e8f0">
{!! nl2br(e($challenge->deskripsi)) !!}
</div>
@endif
</div>
{{-- DAFTAR SOAL --}}
<div class="custom-card">
<p class="section-title">📝 Daftar Soal Challenge</p>
@forelse($challenge->soal as $i => $soal)
<div class="soal-card">
<span class="soal-number-badge">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
<div class="opsi-item {{ $soal->jawaban_benar === $opsi ? 'benar' : '' }}">
<span class="opsi-label">{{ $opsi }}</span>
<span>{{ $soal->$key }}</span>
@if($soal->jawaban_benar === $opsi)
<span style="margin-left:auto;font-size:16px"></span>
@endif
</div>
@endforeach
</div>
<span class="exp-badge"> {{ $soal->exp_per_soal }} EXP per soal</span>
</div>
@empty
<p class="text-muted text-center py-3">Belum ada soal.</p>
@endforelse
</div>
@endsection

View File

@ -1,174 +0,0 @@
@extends('admin.layouts.app')
@section('title', 'Tambah Soal Challenge')
@section('content')
<style>
.page-title {
font-size: 28px;
font-weight: 800;
margin-top: -20px;
margin-bottom: 10px;
}
.custom-card {
background: white;
border-radius: 20px;
border: 2px solid #e5e5e5;
padding: 25px;
}
.soal-box {
border: 2px solid #e5e5e5;
border-radius: 15px;
padding: 20px;
margin-bottom: 20px;
background: #f9fbff;
}
.btn-add {
background: #2b8ef3;
color: white;
border-radius: 10px;
padding: 8px 18px;
border: none;
}
.btn-remove {
background: #ef4444;
color: white;
border: none;
border-radius: 8px;
padding: 5px 12px;
font-size: 12px;
}
</style>
<h3 class="page-title">
TAMBAH SOAL - {{ $challenge->judul_challenge }}
</h3>
<div class="custom-card">
<form action="{{ route('admin.challenge.soal.store', $challenge->id_challenge) }}" method="POST">
@csrf
<div id="soalContainer">
{{-- SOAL PERTAMA (DEFAULT) --}}
<div class="soal-box">
<h6>Soal 1</h6>
<div class="mb-3">
<label>Pertanyaan *</label>
<textarea name="pertanyaan[]" class="form-control" required></textarea>
</div>
<div class="row">
<div class="col-md-6 mb-2">
<input type="text" name="opsi_a[]" class="form-control" placeholder="Opsi A" required>
</div>
<div class="col-md-6 mb-2">
<input type="text" name="opsi_b[]" class="form-control" placeholder="Opsi B" required>
</div>
<div class="col-md-6 mb-2">
<input type="text" name="opsi_c[]" class="form-control" placeholder="Opsi C" required>
</div>
<div class="col-md-6 mb-2">
<input type="text" name="opsi_d[]" class="form-control" placeholder="Opsi D" required>
</div>
</div>
<div class="mb-2">
<label>Jawaban Benar *</label>
<select name="jawaban_benar[]" class="form-control" required>
<option value="">-- Pilih --</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
</select>
</div>
</div>
</div>
<button type="button" onclick="tambahSoal()" class="btn-add">
+ Tambah Soal
</button>
<hr>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-success">
Simpan Semua Soal
</button>
</div>
</form>
</div>
<script>
let nomorSoal = 1;
function tambahSoal() {
nomorSoal++;
let container = document.getElementById('soalContainer');
let html = `
<div class="soal-box">
<div class="d-flex justify-content-between">
<h6>Soal ${nomorSoal}</h6>
<button type="button" class="btn-remove" onclick="hapusSoal(this)">
Hapus
</button>
</div>
<div class="mb-3">
<label>Pertanyaan *</label>
<textarea name="pertanyaan[]" class="form-control" required></textarea>
</div>
<div class="row">
<div class="col-md-6 mb-2">
<input type="text" name="opsi_a[]" class="form-control" placeholder="Opsi A" required>
</div>
<div class="col-md-6 mb-2">
<input type="text" name="opsi_b[]" class="form-control" placeholder="Opsi B" required>
</div>
<div class="col-md-6 mb-2">
<input type="text" name="opsi_c[]" class="form-control" placeholder="Opsi C" required>
</div>
<div class="col-md-6 mb-2">
<input type="text" name="opsi_d[]" class="form-control" placeholder="Opsi D" required>
</div>
</div>
<div class="mb-2">
<label>Jawaban Benar *</label>
<select name="jawaban_benar[]" class="form-control" required>
<option value="">-- Pilih --</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
</select>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function hapusSoal(button) {
button.closest('.soal-box').remove();
}
</script>
@endsection

View File

@ -111,7 +111,7 @@ class="sidebar-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}"
<span>Dashboard</span>
</a>
<div class="sidebar-section">Master Data</div>
<div class="sidebar-section">Data Master</div>
<a href="{{ route('admin.guru.index') }}"
class="sidebar-link {{ request()->routeIs('admin.guru.*') ? 'active' : '' }}">

View File

@ -0,0 +1,178 @@
@extends('siswa.layouts.app')
@section('title', 'Hasil Challenge')
@push('styles')
<style>
.hasil-wrapper { max-width: 680px; margin: 0 auto; padding: 0 0 40px; }
.hasil-hero {
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 24px;
padding: 36px 28px;
text-align: center;
color: white;
margin-bottom: 24px;
position: relative;
overflow: hidden;
}
.hasil-hero::before {
content: '🏆';
position: absolute;
font-size: 120px;
opacity: 0.08;
top: -10px; right: -10px;
}
.hasil-emoji { font-size: 52px; margin-bottom: 10px; }
.hasil-title { font-size: 22px; font-weight: 800; margin-bottom: 4px; }
.hasil-subtitle { font-size: 14px; opacity: 0.85; margin-bottom: 20px; }
.hasil-exp {
display: inline-block;
background: rgba(255,255,255,0.2);
border-radius: 99px;
padding: 8px 24px;
font-size: 20px;
font-weight: 800;
}
.stat-row { display: grid; grid-template-columns: repeat(3,1fr); gap: 14px; margin-bottom: 24px; }
.stat-box { background: white; border-radius: 16px; border: 2px solid #e5e5e5; padding: 18px 14px; text-align: center; }
.stat-num { font-size: 28px; font-weight: 800; margin: 0; }
.stat-label { font-size: 12px; color: #94a3b8; margin: 4px 0 0; font-weight: 500; }
.custom-card { background: white; border-radius: 20px; border: 2px solid #e5e5e5; padding: 24px; margin-bottom: 16px; }
.section-title { font-size: 16px; font-weight: 700; color: #1e293b; margin-bottom: 16px; }
.soal-review { border: 2px solid #e2e8f0; border-radius: 14px; padding: 18px; margin-bottom: 12px; }
.soal-review.benar { border-color: #22c55e; background: #f0fdf4; }
.soal-review.salah { border-color: #ef4444; background: #fef2f2; }
.review-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.review-number { font-size: 12px; font-weight: 700; color: #64748b; }
.review-status { font-size: 12px; font-weight: 700; padding: 2px 10px; border-radius: 99px; }
.review-status.benar { background: #dcfce7; color: #16a34a; }
.review-status.salah { background: #fee2e2; color: #dc2626; }
.review-pertanyaan { font-size: 14px; font-weight: 600; color: #1e293b; margin-bottom: 12px; line-height: 1.6; }
.opsi-review {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
border-radius: 10px;
font-size: 13px;
margin-bottom: 6px;
background: #f8fafc;
}
.opsi-review.jawaban-benar { background: #dcfce7; font-weight: 700; color: #15803d; }
.opsi-review.salah-dipilih { background: #fee2e2; color: #991b1b; font-weight: 700; }
.opsi-circle {
width: 26px; height: 26px;
border-radius: 50%;
background: #e2e8f0;
display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 700; flex-shrink: 0;
}
.opsi-review.jawaban-benar .opsi-circle { background: #22c55e; color: white; }
.opsi-review.salah-dipilih .opsi-circle { background: #ef4444; color: white; }
.btn-back {
display: block;
text-align: center;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border-radius: 12px;
padding: 12px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
margin-top: 20px;
}
.btn-back:hover { opacity: 0.9; color: white; }
</style>
@endpush
@section('content')
@php
$emoji = $persentase >= 80 ? '🎉' : ($persentase >= 60 ? '👍' : '💪');
$pesan = $persentase >= 80
? 'Luar biasa! Kamu menguasai materi ini!'
: ($persentase >= 60 ? 'Bagus! Terus tingkatkan kemampuanmu!' : 'Jangan menyerah! Terus semangat belajar!');
@endphp
<div class="hasil-wrapper">
<div class="hasil-hero">
<div class="hasil-emoji">{{ $emoji }}</div>
<div class="hasil-title">{{ $challenge->judul_challenge }}</div>
<div class="hasil-subtitle">{{ $pesan }}</div>
<div class="hasil-exp"> +{{ $peserta->exp }} EXP didapat!</div>
</div>
<div class="stat-row">
<div class="stat-box">
<p class="stat-num" style="color:#22c55e">{{ $benar }}</p>
<p class="stat-label">Jawaban Benar</p>
</div>
<div class="stat-box">
<p class="stat-num" style="color:#ef4444">{{ $salah }}</p>
<p class="stat-label">Jawaban Salah</p>
</div>
<div class="stat-box">
<p class="stat-num" style="color:#667eea">{{ $persentase }}%</p>
<p class="stat-label">Skor</p>
</div>
</div>
<div class="custom-card">
<p class="section-title">📋 Pembahasan Jawaban</p>
@foreach($challenge->soal as $i => $soal)
@php
$jwbSiswa = strtoupper($jawabanSiswa[$soal->id_soal] ?? '');
$jwbBenar = strtoupper($soal->jawaban_benar);
$isBenar = $jwbSiswa === $jwbBenar;
@endphp
<div class="soal-review {{ $isBenar ? 'benar' : 'salah' }}">
<div class="review-header">
<span class="review-number">Soal {{ $i + 1 }}</span>
<span class="review-status {{ $isBenar ? 'benar' : 'salah' }}">
{{ $isBenar ? '✅ Benar' : '❌ Salah' }}
</span>
</div>
<p class="review-pertanyaan">{{ $soal->pertanyaan }}</p>
@foreach(['A','B','C','D'] as $opsi)
@php
$key = 'opsi_' . strtolower($opsi);
$isDipilih = $jwbSiswa === $opsi;
$isJwbBenar = $jwbBenar === $opsi;
$cls = $isJwbBenar ? 'jawaban-benar' : ($isDipilih ? 'salah-dipilih' : '');
@endphp
<div class="opsi-review {{ $cls }}">
<span class="opsi-circle">{{ $opsi }}</span>
<span>{{ $soal->$key }}</span>
@if($isJwbBenar)
<span style="margin-left:auto;font-size:12px"> Jawaban benar</span>
@elseif($isDipilih)
<span style="margin-left:auto;font-size:12px"> Jawabanmu</span>
@endif
</div>
@endforeach
</div>
@endforeach
</div>
<a href="{{ route('siswa.challenge.index') }}" class="btn-back">
Kembali ke Daftar Challenge
</a>
</div>
@endsection

View File

@ -0,0 +1,174 @@
@extends('siswa.layouts.app')
@section('title', 'Challenge')
@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; }
.challenge-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.challenge-card {
background: white;
border-radius: 20px;
border: 2px solid #e5e5e5;
padding: 22px;
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
overflow: hidden;
}
.challenge-card:hover { transform: translateY(-3px); box-shadow: 0 8px 24px rgba(0,0,0,0.1); }
.challenge-card.sudah { border-color: #a5e6ba; background: linear-gradient(135deg, #f0fdf4, #fff); }
.challenge-card.lewat { border-color: #fecaca; background: #fffafa; opacity: 0.85; }
.card-badge {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 700;
padding: 3px 10px;
border-radius: 99px;
margin-bottom: 12px;
}
.badge-aktif { background: #dcfce7; color: #16a34a; }
.badge-sudah { background: #e0f2fe; color: #0369a1; }
.badge-lewat { background: #fee2e2; color: #dc2626; }
.card-title { font-size: 16px; font-weight: 700; color: #1e293b; margin-bottom: 8px; line-height: 1.4; }
.card-desc { font-size: 13px; color: #64748b; margin-bottom: 14px; line-height: 1.6; }
.card-meta { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.meta-item {
display: inline-flex;
align-items: center;
gap: 5px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 99px;
padding: 4px 10px;
font-size: 12px;
color: #475569;
font-weight: 500;
}
.meta-item.exp { background: #fef9c3; color: #b45309; border-color: #fde68a; }
.btn-kerjakan {
display: block;
text-align: center;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border-radius: 12px;
padding: 10px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
transition: opacity 0.2s;
}
.btn-kerjakan:hover { opacity: 0.9; color: white; }
.btn-lihat-hasil {
display: block;
text-align: center;
background: #e0f2fe;
color: #0369a1;
border-radius: 12px;
padding: 10px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
}
.btn-lihat-hasil:hover { background: #bae6fd; color: #0369a1; }
.btn-disabled {
display: block;
text-align: center;
background: #f1f5f9;
color: #94a3b8;
border-radius: 12px;
padding: 10px;
font-size: 14px;
font-weight: 600;
cursor: not-allowed;
}
.empty-state { text-align: center; padding: 60px 20px; color: #94a3b8; }
.alert-error { background: #fee2e2; color: #991b1b; border-radius: 12px; padding: 12px 16px; margin-bottom: 20px; font-weight: 500; font-size: 14px; }
.challenge-card::before {
content: '🏆';
position: absolute;
top: 16px; right: 16px;
font-size: 28px;
opacity: 0.12;
}
.challenge-card.sudah::before { content: '✅'; opacity: 0.2; }
.challenge-card.lewat::before { content: '⏰'; opacity: 0.15; }
</style>
@endpush
@section('content')
<h3 class="page-title">🏆 Challenge</h3>
<p class="page-subtitle">Kerjakan challenge untuk mendapatkan EXP dan naik peringkat di leaderboard!</p>
@if(session('error'))
<div class="alert-error"> {{ session('error') }}</div>
@endif
@if($challenges->isEmpty())
<div class="empty-state">
<div style="font-size:56px;margin-bottom:16px">🎯</div>
<p style="font-size:16px;font-weight:600;color:#475569">Belum ada challenge untuk kelasmu.</p>
<p style="font-size:13px">Tunggu admin membuat challenge baru!</p>
</div>
@else
<div class="challenge-grid">
@foreach($challenges as $ch)
@php
$isLewat = \Carbon\Carbon::parse($ch->tenggat_waktu)->isPast();
$isSudah = in_array($ch->id_challenge, $sudahDikerjakan);
$cardClass = $isSudah ? 'sudah' : ($isLewat ? 'lewat' : '');
@endphp
<div class="challenge-card {{ $cardClass }}">
@if($isSudah)
<span class="card-badge badge-sudah"> Sudah Dikerjakan</span>
@elseif($isLewat)
<span class="card-badge badge-lewat"> Tenggat Lewat</span>
@else
<span class="card-badge badge-aktif">🔥 Aktif</span>
@endif
<div class="card-title">{{ $ch->judul_challenge }}</div>
@if($ch->deskripsi)
<div class="card-desc">{{ Str::limit($ch->deskripsi, 80) }}</div>
@endif
<div class="card-meta">
<span class="meta-item">📝 {{ $ch->soal_count }} soal</span>
<span class="meta-item exp"> {{ $ch->exp }} EXP</span>
<span class="meta-item"> {{ \Carbon\Carbon::parse($ch->tenggat_waktu)->format('d M Y, H:i') }}</span>
</div>
@if($isSudah)
<a href="{{ route('siswa.challenge.hasil', $ch->id_challenge) }}" class="btn-lihat-hasil">📊 Lihat Hasil</a>
@elseif($isLewat)
<span class="btn-disabled">Tenggat sudah lewat</span>
@else
<a href="{{ route('siswa.challenge.kerjakan', $ch->id_challenge) }}" class="btn-kerjakan">🚀 Kerjakan Sekarang</a>
@endif
</div>
@endforeach
</div>
@endif
@endsection

View File

@ -0,0 +1,406 @@
@extends('siswa.layouts.app')
@section('title', 'Kerjakan Challenge')
@push('styles')
<style>
/* Sembunyikan sidebar saat kerjakan biar fokus */
.siswa-wrapper .sidebar { display: none !important; }
.sidebar-toggle-btn { display: none !important; }
.quiz-wrapper {
max-width: 680px;
margin: 0 auto;
padding: 10px 0 40px;
}
.quiz-header {
text-align: center;
margin-bottom: 28px;
}
.quiz-title {
font-size: 20px;
font-weight: 800;
color: #1e293b;
margin-bottom: 6px;
}
.quiz-meta {
font-size: 13px;
color: #64748b;
display: flex;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
}
.quiz-meta span {
display: inline-flex;
align-items: center;
gap: 4px;
}
/* Progress bar */
.progress-bar-wrap {
background: #e2e8f0;
border-radius: 99px;
height: 8px;
margin-bottom: 28px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 99px;
transition: width 0.4s ease;
}
.progress-label {
text-align: center;
font-size: 12px;
color: #64748b;
font-weight: 600;
margin-bottom: 8px;
}
/* Soal card */
.soal-card {
background: white;
border-radius: 20px;
border: 2px solid #e5e5e5;
padding: 28px;
margin-bottom: 20px;
display: none; /* sembunyikan semua dulu */
}
.soal-card.active { display: block; }
.soal-number {
display: inline-block;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
font-size: 12px;
font-weight: 700;
padding: 3px 12px;
border-radius: 99px;
margin-bottom: 14px;
}
.soal-pertanyaan {
font-size: 16px;
font-weight: 600;
color: #1e293b;
margin-bottom: 20px;
line-height: 1.7;
}
/* Opsi pilihan */
.opsi-list { display: flex; flex-direction: column; gap: 10px; }
.opsi-item {
display: flex;
align-items: center;
gap: 14px;
background: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 13px 16px;
cursor: pointer;
transition: all 0.18s;
font-size: 14px;
color: #1e293b;
user-select: none;
}
.opsi-item:hover {
border-color: #667eea;
background: #f0eeff;
}
.opsi-item.selected {
border-color: #667eea;
background: linear-gradient(135deg, #ede9fe, #f5f3ff);
font-weight: 600;
color: #4c1d95;
}
.opsi-item input[type="radio"] { display: none; }
.opsi-label-circle {
width: 32px;
height: 32px;
border-radius: 50%;
background: #e2e8f0;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 13px;
flex-shrink: 0;
transition: all 0.18s;
}
.opsi-item.selected .opsi-label-circle {
background: #667eea;
color: white;
}
/* Navigasi */
.nav-buttons {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.btn-nav {
padding: 11px 24px;
border-radius: 12px;
border: none;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
font-family: 'Poppins', sans-serif;
}
.btn-prev {
background: #f1f5f9;
color: #475569;
}
.btn-prev:hover { background: #e2e8f0; }
.btn-prev:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-next {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
margin-left: auto;
}
.btn-next:hover { opacity: 0.9; }
.btn-submit {
background: linear-gradient(135deg, #22c55e, #16a34a);
color: white;
margin-left: auto;
display: none;
}
.btn-submit:hover { opacity: 0.9; }
/* Nomor soal dots */
.soal-dots {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.soal-dot {
width: 32px;
height: 32px;
border-radius: 50%;
background: #e2e8f0;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
color: #64748b;
cursor: pointer;
transition: all 0.2s;
}
.soal-dot.active { background: #667eea; color: white; }
.soal-dot.answered { background: #dcfce7; color: #16a34a; border: 2px solid #22c55e; }
.soal-dot.active.answered { background: #22c55e; color: white; }
/* Warning belum semua dijawab */
.warning-box {
background: #fff7ed;
border: 1px solid #fed7aa;
border-radius: 12px;
padding: 12px 16px;
font-size: 13px;
color: #c2410c;
font-weight: 500;
margin-bottom: 16px;
display: none;
}
</style>
@endpush
@section('content')
<div class="quiz-wrapper">
{{-- Header --}}
<div class="quiz-header">
<div class="quiz-title">🏆 {{ $challenge->judul_challenge }}</div>
<div class="quiz-meta">
<span>📝 {{ $challenge->soal->count() }} Soal</span>
<span> {{ $challenge->exp }} EXP</span>
<span> Tenggat: {{ \Carbon\Carbon::parse($challenge->tenggat_waktu)->format('d M Y, H:i') }}</span>
</div>
</div>
{{-- Dots navigasi soal --}}
<div class="soal-dots" id="soalDots">
@foreach($challenge->soal as $i => $soal)
<div class="soal-dot {{ $i === 0 ? 'active' : '' }}"
id="dot-{{ $i }}"
onclick="goToSoal({{ $i }})">
{{ $i + 1 }}
</div>
@endforeach
</div>
{{-- Progress --}}
<div class="progress-label" id="progressLabel">Soal 1 dari {{ $challenge->soal->count() }}</div>
<div class="progress-bar-wrap">
<div class="progress-bar-fill" id="progressBar"
style="width: {{ round(1 / $challenge->soal->count() * 100) }}%"></div>
</div>
{{-- Form --}}
<form action="{{ route('siswa.challenge.submit', $challenge->id_challenge) }}"
method="POST" id="quizForm">
@csrf
{{-- Soal --}}
@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
{{-- Warning --}}
<div class="warning-box" id="warningBox">
⚠️ Masih ada <span id="warningCount"></span> soal yang belum dijawab. Yakin ingin submit?
</div>
{{-- Navigasi --}}
<div class="nav-buttons">
<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()">
🎯 Selesai & Submit
</button>
</div>
</form>
</div>
@endsection
@push('scripts')
<script>
const totalSoal = {{ $challenge->soal->count() }};
let currentSoal = 0;
let jawaban = {}; // { index: 'A'/'B'/'C'/'D' }
function goToSoal(index) {
// Sembunyikan soal sekarang
document.getElementById(`soal-${currentSoal}`).classList.remove('active');
document.getElementById(`dot-${currentSoal}`).classList.remove('active');
// Tampilkan soal target
currentSoal = index;
document.getElementById(`soal-${currentSoal}`).classList.add('active');
const dot = document.getElementById(`dot-${currentSoal}`);
dot.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;
// Hapus selected dari semua opsi soal ini
['A','B','C','D'].forEach(o => {
document.getElementById(`label-${soalIndex}-${o}`)?.classList.remove('selected');
});
// Tandai yang dipilih
document.getElementById(`label-${soalIndex}-${opsi}`)?.classList.add('selected');
document.getElementById(`radio-${soalIndex}-${opsi}`).checked = true;
// Update dot
const dot = document.getElementById(`dot-${soalIndex}`);
dot.classList.add('answered');
updateProgress();
}
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 = 'block';
} else {
btnNext.style.display = 'block';
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 konfirmasiSubmit() {
const belum = totalSoal - Object.keys(jawaban).length;
const warningBox = document.getElementById('warningBox');
if (belum > 0) {
document.getElementById('warningCount').textContent = belum + ' soal';
warningBox.style.display = 'block';
return confirm(`Masih ada ${belum} soal yang belum dijawab. Yakin ingin submit?`);
}
return confirm('Yakin ingin submit jawaban? Jawaban tidak bisa diubah setelah submit.');
}
// Init
updateNav();
updateProgress();
</script>
@endpush

View File

@ -228,13 +228,13 @@ class="sidebar-link {{ request()->routeIs('siswa.tugas*') ? 'active' : '' }}">
<span>Tugas</span>
</a>
<a href="#"
<a href="{{ route('siswa.challenge.index') }}"
class="sidebar-link {{ request()->routeIs('siswa.challenge*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/challenge.png') }}" class="sidebar-icon" alt="">
<span>Challenge</span>
</a>
<a href="#"
<a href="{{ route('siswa.leaderboard.index') }}"
class="sidebar-link {{ request()->routeIs('siswa.leaderboard*') ? 'active' : '' }}">
<img src="{{ asset('images/icon/sidebar/lb.png') }}" class="sidebar-icon" alt="">
<span>Leaderboard</span>

View File

@ -0,0 +1,254 @@
@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 top 3 */
.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 {
width: 56px;
height: 56px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
font-weight: 800;
color: white;
position: relative;
}
.podium-crown {
position: absolute;
top: -16px;
font-size: 18px;
}
.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 */
.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-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; }
/* Tabel */
.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; }
.lb-row {
display: flex;
align-items: center;
gap: 14px;
padding: 12px 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-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>
@if($leaderboard->isEmpty())
<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>
@else
{{-- Podium Top 3 --}}
@php
$top3 = $leaderboard->take(3);
$first = $top3->firstWhere('ranking', 1);
$second = $top3->firstWhere('ranking', 2);
$third = $top3->firstWhere('ranking', 3);
@endphp
@if($first)
<div class="podium-wrap">
{{-- Rank 2 --}}
@if($second)
<div class="podium-item rank-2">
<div class="podium-avatar">{{ strtoupper(substr($second['nama'], 0, 1)) }}</div>
<div class="podium-name">{{ $second['nama'] }}</div>
<div class="podium-exp"> {{ number_format($second['exp']) }}</div>
<div class="podium-bar">2</div>
</div>
@endif
{{-- Rank 1 --}}
<div class="podium-item rank-1">
<div class="podium-avatar">
<span class="podium-crown">👑</span>
{{ strtoupper(substr($first['nama'], 0, 1)) }}
</div>
<div class="podium-name">{{ $first['nama'] }}</div>
<div class="podium-exp"> {{ number_format($first['exp']) }}</div>
<div class="podium-bar">1</div>
</div>
{{-- Rank 3 --}}
@if($third)
<div class="podium-item rank-3">
<div class="podium-avatar">{{ strtoupper(substr($third['nama'], 0, 1)) }}</div>
<div class="podium-name">{{ $third['nama'] }}</div>
<div class="podium-exp"> {{ number_format($third['exp']) }}</div>
<div class="podium-bar">3</div>
</div>
@endif
</div>
@endif
{{-- My Rank Banner --}}
@if($myRank)
<div class="my-rank-banner">
<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_format($myRank['exp']) }} EXP</div>
</div>
<div style="font-size:32px">🎯</div>
</div>
@endif
{{-- Tabel semua --}}
<div class="custom-card">
<p class="section-title">📋 Semua Peringkat</p>
@foreach($leaderboard as $item)
@php
$isMe = $item['id_siswa'] === $siswaLogin->id_siswa;
$rankClass = match($item['ranking']) {
1 => 'gold', 2 => 'silver', 3 => 'bronze', default => ''
};
@endphp
<div class="lb-row {{ $isMe ? 'highlight' : '' }}">
<div class="lb-rank {{ $rankClass }}">
@if($item['ranking'] === 1) 🥇
@elseif($item['ranking'] === 2) 🥈
@elseif($item['ranking'] === 3) 🥉
@else {{ $item['ranking'] }}
@endif
</div>
<div style="flex:1">
<div class="lb-nama">
{{ $item['nama'] }}
@if($isMe) <span style="background:#c4b5fd;color:#4c1d95;font-size:10px;padding:2px 7px;border-radius:99px;font-weight:700;margin-left:4px">Kamu</span> @endif
</div>
<div class="lb-nisn">{{ $item['nisn'] }}</div>
</div>
<div class="lb-exp"> {{ number_format($item['exp']) }}</div>
</div>
@endforeach
</div>
@endif
@endsection

View File

@ -76,7 +76,6 @@
Route::post('/siswa/login', [SiswaLoginController::class, 'login'])
->name('siswa.login.submit');
// =======================================================
// ADMIN AREA (HARUS LOGIN ADMIN)
// =======================================================
@ -90,35 +89,40 @@
Route::get('/profil', function () {
return view('admin.profil');
})->name('profil');
})->name('profil');
// CRUD AREA
// ── GURU ──────────────────────────────────────────────
Route::get('/guru/kelas-by-mapel', [AdminGuruController::class, 'getKelasByMapel'])
->name('guru.kelasByMapel');
// History Materi
Route::get('/materi/history', [AdminMateriTugasController::class, 'historyMateri'])->name('materi.history');
Route::delete('/materi/{id}', [AdminMateriTugasController::class, 'destroyMateri'])->name('materi.destroy');
// History Tugas
Route::get('/tugas/history', [AdminMateriTugasController::class, 'historyTugas'])->name('tugas.history');
Route::get('/tugas/{id}/detail', [AdminMateriTugasController::class, 'detailTugas'])->name('tugas.detail');
Route::delete('/tugas/{id}', [AdminMateriTugasController::class, 'destroyTugas'])->name('tugas.destroy');
->name('guru.kelasByMapel');
Route::resource('guru', AdminGuruController::class);
// ── SISWA / KELAS / MAPEL ─────────────────────────────
Route::resource('siswa', AdminSiswaController::class);
Route::resource('kelas', AdminKelasController::class);
Route::resource('mapel', AdminMapelController::class);
Route::resource('leaderboard', AdminLeaderboardController::class)
->only(['index']);
// ── HISTORY MATERI ────────────────────────────────────
Route::get('/materi/history', [AdminMateriTugasController::class, 'historyMateri'])->name('materi.history');
Route::delete('/materi/{id}', [AdminMateriTugasController::class, 'destroyMateri'])->name('materi.destroy');
// ── HISTORY TUGAS ─────────────────────────────────────
Route::get('/tugas/history', [AdminMateriTugasController::class, 'historyTugas'])->name('tugas.history');
Route::get('/tugas/{id}/detail', [AdminMateriTugasController::class, 'detailTugas'])->name('tugas.detail');
Route::delete('/tugas/{id}', [AdminMateriTugasController::class, 'destroyTugas'])->name('tugas.destroy');
// ── CHALLENGE ─────────────────────────────────────────
// WAJIB di atas Route::resource agar tidak konflik dengan {challenge} wildcard
Route::get('/challenge/{id}/edit-data', [AdminChallengeController::class, 'editData'])
->name('challenge.editData');
Route::resource('challenge', AdminChallengeController::class);
// LOGOUT ADMIN
Route::post('/logout', [LoginController::class, 'logout'])
->name('logout');
});
// ── LEADERBOARD ───────────────────────────────────────
Route::resource('leaderboard', AdminLeaderboardController::class)->only(['index']);
// ── LOGOUT ────────────────────────────────────────────
Route::post('/logout', [LoginController::class, 'logout'])->name('logout');
});
// =======================================================
// GURU AREA (HARUS LOGIN GURU)
@ -176,6 +180,15 @@
Route::get('/tugas/{id_tugas}', [SiswaTugasController::class, 'show'])->name('tugas.show');
Route::post('/tugas/{id_tugas}/submit', [SiswaTugasController::class, 'submit'])->name('tugas.submit');
// CHALLENGE SISWA
Route::get('/challenge', [SiswaChallengeController::class, 'index'])->name('challenge.index');
Route::get('/challenge/{id}/kerjakan', [SiswaChallengeController::class, 'kerjakan'])->name('challenge.kerjakan');
Route::post('/challenge/{id}/submit', [SiswaChallengeController::class, 'submit'])->name('challenge.submit');
Route::get('/challenge/{id}/hasil', [SiswaChallengeController::class, 'hasil'])->name('challenge.hasil');
//LEADERBOARD SISWA
Route::get('/leaderboard', [SiswaLeaderboardController::class, 'index'])->name('leaderboard.index');
// LOGOUT SISWA
Route::post('/logout', [SiswaLoginController::class, 'logout'])->name('logout');