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
@@ -0,0 +1,89 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\{Quiz, QuizQuestion, QuizAnswerOption};
use Illuminate\Http\Request;
class QuizQuestionController extends Controller {
public function create(Quiz $quiz) {
return view('admin.quiz_questions.create', compact('quiz'));
}
public function store(Request $r, Quiz $quiz) {
$r->validate([
'type' => 'required|in:multiple_choice,exclusion,true_false,free_text',
'question_text' => 'required|string',
'time_limit' => 'nullable|integer|min:5|max:120',
'correct_answer' => 'nullable|string|max:200',
'correct_option' => 'nullable|integer|min:0|max:3',
'options' => 'array',
'options.*.text' => 'nullable|string|max:200',
]);
$q = QuizQuestion::create([
'quiz_id' => $quiz->id,
'sort_order' => ($quiz->questions()->max('sort_order') ?? -1) + 1,
'type' => $r->type,
'question_text' => $r->question_text,
'time_limit' => $r->time_limit ?: null,
'correct_answer' => $r->correct_answer,
]);
if (in_array($r->type,['multiple_choice','exclusion'])) {
$correctIdx = (int)$r->input('correct_option', 0);
foreach (($r->options ?? []) as $i => $opt) {
if (!empty($opt['text'])) {
QuizAnswerOption::create([
'quiz_question_id' => $q->id,
'text' => $opt['text'],
'is_correct' => $i === $correctIdx,
'sort_order' => $i,
]);
}
}
}
return redirect()->route('admin.quizzes.edit', $quiz)->with('success','Frage hinzugefügt.');
}
public function edit(QuizQuestion $question) {
return view('admin.quiz_questions.edit', compact('question'));
}
public function update(Request $r, QuizQuestion $question) {
$r->validate([
'type' => 'required|in:multiple_choice,exclusion,true_false,free_text',
'question_text' => 'required|string',
'time_limit' => 'nullable|integer|min:5|max:120',
'correct_answer' => 'nullable|string|max:200',
'correct_option' => 'nullable|integer|min:0|max:3',
'options' => 'array',
'options.*.text' => 'nullable|string|max:200',
]);
$question->update([
'type' => $r->type,
'question_text' => $r->question_text,
'time_limit' => $r->time_limit ?: null,
'correct_answer' => $r->correct_answer,
]);
if (in_array($r->type,['multiple_choice','exclusion'])) {
$question->answerOptions()->delete();
$correctIdx = (int)$r->input('correct_option', 0);
foreach (($r->options ?? []) as $i => $opt) {
if (!empty($opt['text'])) {
QuizAnswerOption::create([
'quiz_question_id' => $question->id,
'text' => $opt['text'],
'is_correct' => $i === $correctIdx,
'sort_order' => $i,
]);
}
}
}
return redirect()->route('admin.quizzes.edit', $question->quiz)->with('success','Frage gespeichert.');
}
public function destroy(QuizQuestion $question) {
$quiz = $question->quiz;
$question->delete();
return redirect()->route('admin.quizzes.edit', $quiz)->with('success','Frage gelöscht.');
}
}