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:
root
2026-05-05 21:14:09 +00:00
parent 213d4b4832
commit 6c6dd26823
21 changed files with 984 additions and 1 deletions
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Quiz extends Model {
protected $fillable = ['subject_id','title','description','active'];
protected $casts = ['active' => 'boolean'];
public function subject() { return $this->belongsTo(Subject::class); }
public function questions() { return $this->hasMany(QuizQuestion::class)->orderBy('sort_order'); }
public function attempts() { return $this->hasMany(QuizAttempt::class); }
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class QuizAnswerOption extends Model {
public $timestamps = false;
protected $fillable = ['quiz_question_id','text','is_correct','sort_order'];
public function question() { return $this->belongsTo(QuizQuestion::class,'quiz_question_id'); }
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class QuizAttempt extends Model {
protected $fillable = ['user_id','quiz_id','score','current_question','status','started_at','completed_at'];
protected $casts = ['started_at' => 'datetime', 'completed_at' => 'datetime'];
public const POINTS = [1, 1, 2, 2, 3, 3, 4, 6, 8, 10];
public function user() { return $this->belongsTo(User::class); }
public function quiz() { return $this->belongsTo(Quiz::class); }
public function answers() { return $this->hasMany(QuizAttemptAnswer::class); }
public function stars(): string {
$s = $this->score;
if ($s >= 40) return '🌟🌟🌟🌟🌟';
if ($s >= 30) return '⭐⭐⭐⭐';
if ($s >= 20) return '⭐⭐⭐';
if ($s >= 10) return '⭐⭐';
if ($s >= 1) return '⭐';
return '—';
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class QuizAttemptAnswer extends Model {
protected $fillable = ['quiz_attempt_id','quiz_question_id','answer_given','is_correct','points_earned'];
public function attempt() { return $this->belongsTo(QuizAttempt::class,'quiz_attempt_id'); }
public function question() { return $this->belongsTo(QuizQuestion::class,'quiz_question_id'); }
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class QuizQuestion extends Model {
protected $fillable = ['quiz_id','sort_order','type','question_text','time_limit','correct_answer'];
public function quiz() { return $this->belongsTo(Quiz::class); }
public function answerOptions() { return $this->hasMany(QuizAnswerOption::class,'quiz_question_id')->orderBy('sort_order'); }
public function typeLabel(): string {
return match($this->type) {
'multiple_choice' => 'Multiple Choice',
'exclusion' => 'Ausschluss',
'true_false' => 'Wahr/Falsch',
'free_text' => 'Freitext',
default => $this->type,
};
}
}