Add Quiz feature: 10-question quizzes with progressive scoring (max 40 pts)
- Quizzes table with questions, answer options, attempts, answers - Question types: multiple_choice, exclusion, true_false, free_text - Progressive scoring: [1,1,2,2,3,3,4,6,8,10] = max 40 per quiz - Alpine.js countdown timer per question with auto-submit on timeout - Admin: CRUD for quizzes + per-question editor, JSON export/import - Child: quiz overview with best scores, question view, result breakdown - Nav: Quiz link in child header and admin sidebar
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Child;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\{Quiz, QuizAttempt, QuizAttemptAnswer, QuizQuestion, Subject};
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class QuizController extends Controller {
|
||||
|
||||
public function index() {
|
||||
$subjects = Subject::all()->keyBy('id');
|
||||
$quizzes = Quiz::with('subject')->where('active',true)
|
||||
->withCount('questions')->get()->groupBy('subject_id');
|
||||
$bestScores = QuizAttempt::where('user_id',auth()->id())
|
||||
->where('status','completed')
|
||||
->selectRaw('quiz_id, MAX(score) as best')
|
||||
->groupBy('quiz_id')->pluck('best','quiz_id');
|
||||
return view('child.quiz.index', compact('subjects','quizzes','bestScores'));
|
||||
}
|
||||
|
||||
public function start(Quiz $quiz) {
|
||||
abort_unless($quiz->active, 403);
|
||||
$attempt = QuizAttempt::firstOrCreate(
|
||||
['user_id'=>auth()->id(),'quiz_id'=>$quiz->id,'status'=>'in_progress'],
|
||||
['score'=>0,'current_question'=>0,'started_at'=>now()]
|
||||
);
|
||||
return redirect()->route('quiz.question', $attempt);
|
||||
}
|
||||
|
||||
public function question(QuizAttempt $attempt) {
|
||||
abort_unless($attempt->user_id === auth()->id(), 403);
|
||||
if ($attempt->status === 'completed') return redirect()->route('quiz.result', $attempt);
|
||||
$question = $attempt->quiz->questions()->skip($attempt->current_question)->first();
|
||||
$pointsIfCorrect = QuizAttempt::POINTS[$attempt->current_question];
|
||||
return view('child.quiz.question', compact('attempt','question','pointsIfCorrect'));
|
||||
}
|
||||
|
||||
public function answer(Request $r, QuizAttempt $attempt) {
|
||||
abort_unless($attempt->user_id === auth()->id(), 403);
|
||||
abort_if($attempt->status === 'completed', 400);
|
||||
|
||||
$question = $attempt->quiz->questions()->skip($attempt->current_question)->first();
|
||||
$possible = QuizAttempt::POINTS[$attempt->current_question];
|
||||
$timedOut = $r->input('timeout') === '1';
|
||||
$answerGiven = $r->input('answer','');
|
||||
$isCorrect = false;
|
||||
$pointsEarned = 0;
|
||||
|
||||
if (!$timedOut && $answerGiven !== '') {
|
||||
$isCorrect = $this->check($question, $answerGiven);
|
||||
$pointsEarned = $isCorrect ? $possible : 0;
|
||||
}
|
||||
|
||||
QuizAttemptAnswer::create([
|
||||
'quiz_attempt_id' => $attempt->id,
|
||||
'quiz_question_id' => $question->id,
|
||||
'answer_given' => $timedOut ? '__timeout__' : $answerGiven,
|
||||
'is_correct' => $isCorrect,
|
||||
'points_earned' => $pointsEarned,
|
||||
]);
|
||||
|
||||
session()->flash('last_answer', [
|
||||
'correct' => $isCorrect,
|
||||
'points' => $pointsEarned,
|
||||
'possible' => $possible,
|
||||
'timeout' => $timedOut,
|
||||
]);
|
||||
|
||||
$next = $attempt->current_question + 1;
|
||||
$totalQuestions = $attempt->quiz->questions()->count();
|
||||
|
||||
if ($next >= $totalQuestions || $next >= 10) {
|
||||
$total = QuizAttemptAnswer::where('quiz_attempt_id',$attempt->id)->sum('points_earned');
|
||||
$attempt->update(['score'=>$total,'current_question'=>$next,'status'=>'completed','completed_at'=>now()]);
|
||||
auth()->user()->increment('points', $total);
|
||||
return redirect()->route('quiz.result', $attempt);
|
||||
}
|
||||
$attempt->update(['current_question'=>$next]);
|
||||
return redirect()->route('quiz.question', $attempt);
|
||||
}
|
||||
|
||||
public function result(QuizAttempt $attempt) {
|
||||
abort_unless($attempt->user_id === auth()->id(), 403);
|
||||
abort_unless($attempt->status === 'completed', 400);
|
||||
$answers = $attempt->answers()->with('question.answerOptions')->get();
|
||||
return view('child.quiz.result', compact('attempt','answers'));
|
||||
}
|
||||
|
||||
private function check(QuizQuestion $q, string $answer): bool {
|
||||
return match($q->type) {
|
||||
'multiple_choice','exclusion' => ($opt = $q->answerOptions()->where('is_correct',true)->first())
|
||||
&& (string)$opt->id === $answer,
|
||||
'true_false','free_text' => strtolower(trim($answer)) === strtolower(trim((string)$q->correct_answer)),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user