challenge & leaderboard added
This commit is contained in:
parent
9f31e74818
commit
62d15959a6
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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' : '' }}">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue