From 6c6dd26823e0d7b943512f587f80d397f184ec00 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 5 May 2026 21:14:09 +0000 Subject: [PATCH] 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 --- app/Http/Controllers/Admin/QuizController.php | 104 +++++++++++++++ .../Admin/QuizQuestionController.php | 89 +++++++++++++ app/Http/Controllers/Child/QuizController.php | 96 ++++++++++++++ app/Models/Quiz.php | 10 ++ app/Models/QuizAnswerOption.php | 8 ++ app/Models/QuizAttempt.php | 20 +++ app/Models/QuizAttemptAnswer.php | 8 ++ app/Models/QuizQuestion.php | 17 +++ ...2026_05_05_220000_create_quizzes_table.php | 38 ++++++ ...5_05_220001_create_quiz_attempts_table.php | 32 +++++ .../admin/quiz_questions/create.blade.php | 71 ++++++++++ .../views/admin/quiz_questions/edit.blade.php | 80 ++++++++++++ .../views/admin/quizzes/create.blade.php | 28 ++++ resources/views/admin/quizzes/edit.blade.php | 72 ++++++++++ resources/views/admin/quizzes/index.blade.php | 61 +++++++++ resources/views/child/quiz/index.blade.php | 46 +++++++ resources/views/child/quiz/question.blade.php | 123 ++++++++++++++++++ resources/views/child/quiz/result.blade.php | 63 +++++++++ resources/views/layouts/admin.blade.php | 3 + resources/views/layouts/child.blade.php | 1 + routes/web.php | 15 ++- 21 files changed, 984 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/Admin/QuizController.php create mode 100644 app/Http/Controllers/Admin/QuizQuestionController.php create mode 100644 app/Http/Controllers/Child/QuizController.php create mode 100644 app/Models/Quiz.php create mode 100644 app/Models/QuizAnswerOption.php create mode 100644 app/Models/QuizAttempt.php create mode 100644 app/Models/QuizAttemptAnswer.php create mode 100644 app/Models/QuizQuestion.php create mode 100644 database/migrations/2026_05_05_220000_create_quizzes_table.php create mode 100644 database/migrations/2026_05_05_220001_create_quiz_attempts_table.php create mode 100644 resources/views/admin/quiz_questions/create.blade.php create mode 100644 resources/views/admin/quiz_questions/edit.blade.php create mode 100644 resources/views/admin/quizzes/create.blade.php create mode 100644 resources/views/admin/quizzes/edit.blade.php create mode 100644 resources/views/admin/quizzes/index.blade.php create mode 100644 resources/views/child/quiz/index.blade.php create mode 100644 resources/views/child/quiz/question.blade.php create mode 100644 resources/views/child/quiz/result.blade.php diff --git a/app/Http/Controllers/Admin/QuizController.php b/app/Http/Controllers/Admin/QuizController.php new file mode 100644 index 0000000..a36cef0 --- /dev/null +++ b/app/Http/Controllers/Admin/QuizController.php @@ -0,0 +1,104 @@ +withCount('questions')->latest()->get(); + return view('admin.quizzes.index', compact('quizzes')); + } + + public function create() { + $subjects = Subject::all(); + return view('admin.quizzes.create', compact('subjects')); + } + + public function store(Request $r) { + $r->validate(['title'=>'required|string|max:120','subject_id'=>'required|exists:subjects,id','description'=>'nullable|string|max:500']); + $quiz = Quiz::create($r->only('title','subject_id','description') + ['active'=>true]); + return redirect()->route('admin.quizzes.edit', $quiz)->with('success','Quiz erstellt – jetzt Fragen hinzufügen.'); + } + + public function edit(Quiz $quiz) { + $subjects = Subject::all(); + $questions = $quiz->questions()->with('answerOptions')->get(); + return view('admin.quizzes.edit', compact('quiz','subjects','questions')); + } + + public function update(Request $r, Quiz $quiz) { + $r->validate(['title'=>'required|string|max:120','subject_id'=>'required|exists:subjects,id','description'=>'nullable|string|max:500']); + $quiz->update($r->only('title','subject_id','description') + ['active'=>$r->boolean('active')]); + return back()->with('success','Quiz gespeichert.'); + } + + public function destroy(Quiz $quiz) { + $quiz->delete(); + return redirect()->route('admin.quizzes.index')->with('success','Quiz gelöscht.'); + } + + public function export(Quiz $quiz) { + $quiz->load('subject','questions.answerOptions'); + $data = [ + 'subject' => $quiz->subject->slug, + 'title' => $quiz->title, + 'description' => $quiz->description, + 'questions' => $quiz->questions->map(fn($q) => [ + 'type' => $q->type, + 'question_text' => $q->question_text, + 'time_limit' => $q->time_limit, + 'correct_answer' => $q->correct_answer, + 'options' => $q->answerOptions->map(fn($o) => [ + 'text' => $o->text, + 'is_correct' => (bool)$o->is_correct, + ])->values(), + ])->values(), + ]; + $filename = 'quiz-'.now()->format('Y-m-d').'-'.str($quiz->title)->slug().'.json'; + return response()->streamDownload( + fn() => print(json_encode($data, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)), + $filename, ['Content-Type'=>'application/json'] + ); + } + + public function import(Request $r) { + $r->validate(['file'=>'required|file|mimes:json|max:4096']); + $raw = json_decode(file_get_contents($r->file('file')->getRealPath()), true); + if (!is_array($raw) || !isset($raw['title'],$raw['subject'],$raw['questions'])) { + return back()->with('error','Ungültiges JSON-Format. Felder: title, subject, questions erwartet.'); + } + $subject = Subject::where('slug',$raw['subject'])->first(); + if (!$subject) return back()->with('error','Unbekanntes Fach: '.$raw['subject']); + + DB::transaction(function() use ($raw,$subject) { + $quiz = Quiz::create([ + 'subject_id' => $subject->id, + 'title' => $raw['title'], + 'description' => $raw['description'] ?? null, + 'active' => true, + ]); + foreach (array_slice($raw['questions'],0,10) as $i => $item) { + $q = QuizQuestion::create([ + 'quiz_id' => $quiz->id, + 'sort_order' => $i, + 'type' => $item['type'] ?? 'multiple_choice', + 'question_text' => $item['question_text'], + 'time_limit' => $item['time_limit'] ?? null, + 'correct_answer' => $item['correct_answer'] ?? null, + ]); + foreach (($item['options'] ?? []) as $j => $opt) { + QuizAnswerOption::create([ + 'quiz_question_id' => $q->id, + 'text' => $opt['text'], + 'is_correct' => $opt['is_correct'] ?? false, + 'sort_order' => $j, + ]); + } + } + }); + return redirect()->route('admin.quizzes.index')->with('success','Quiz "'.$raw['title'].'" importiert.'); + } +} diff --git a/app/Http/Controllers/Admin/QuizQuestionController.php b/app/Http/Controllers/Admin/QuizQuestionController.php new file mode 100644 index 0000000..c8a0b9a --- /dev/null +++ b/app/Http/Controllers/Admin/QuizQuestionController.php @@ -0,0 +1,89 @@ +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.'); + } +} diff --git a/app/Http/Controllers/Child/QuizController.php b/app/Http/Controllers/Child/QuizController.php new file mode 100644 index 0000000..bdcdbce --- /dev/null +++ b/app/Http/Controllers/Child/QuizController.php @@ -0,0 +1,96 @@ +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, + }; + } +} diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php new file mode 100644 index 0000000..661128a --- /dev/null +++ b/app/Models/Quiz.php @@ -0,0 +1,10 @@ + '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); } +} diff --git a/app/Models/QuizAnswerOption.php b/app/Models/QuizAnswerOption.php new file mode 100644 index 0000000..b99d7cf --- /dev/null +++ b/app/Models/QuizAnswerOption.php @@ -0,0 +1,8 @@ +belongsTo(QuizQuestion::class,'quiz_question_id'); } +} diff --git a/app/Models/QuizAttempt.php b/app/Models/QuizAttempt.php new file mode 100644 index 0000000..bc16baa --- /dev/null +++ b/app/Models/QuizAttempt.php @@ -0,0 +1,20 @@ + '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 '—'; + } +} diff --git a/app/Models/QuizAttemptAnswer.php b/app/Models/QuizAttemptAnswer.php new file mode 100644 index 0000000..44e63a1 --- /dev/null +++ b/app/Models/QuizAttemptAnswer.php @@ -0,0 +1,8 @@ +belongsTo(QuizAttempt::class,'quiz_attempt_id'); } + public function question() { return $this->belongsTo(QuizQuestion::class,'quiz_question_id'); } +} diff --git a/app/Models/QuizQuestion.php b/app/Models/QuizQuestion.php new file mode 100644 index 0000000..9914e48 --- /dev/null +++ b/app/Models/QuizQuestion.php @@ -0,0 +1,17 @@ +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, + }; + } +} diff --git a/database/migrations/2026_05_05_220000_create_quizzes_table.php b/database/migrations/2026_05_05_220000_create_quizzes_table.php new file mode 100644 index 0000000..5ff4b07 --- /dev/null +++ b/database/migrations/2026_05_05_220000_create_quizzes_table.php @@ -0,0 +1,38 @@ +id(); + $table->foreignId('subject_id')->constrained()->cascadeOnDelete(); + $table->string('title'); + $table->text('description')->nullable(); + $table->boolean('active')->default(true); + $table->timestamps(); + }); + Schema::create('quiz_questions', function (Blueprint $table) { + $table->id(); + $table->foreignId('quiz_id')->constrained()->cascadeOnDelete(); + $table->unsignedSmallInteger('sort_order')->default(0); + $table->enum('type', ['multiple_choice','exclusion','true_false','free_text']); + $table->text('question_text'); + $table->unsignedSmallInteger('time_limit')->nullable(); + $table->string('correct_answer')->nullable(); + $table->timestamps(); + }); + Schema::create('quiz_answer_options', function (Blueprint $table) { + $table->id(); + $table->foreignId('quiz_question_id')->constrained()->cascadeOnDelete(); + $table->string('text'); + $table->boolean('is_correct')->default(false); + $table->unsignedSmallInteger('sort_order')->default(0); + }); + } + public function down(): void { + Schema::dropIfExists('quiz_answer_options'); + Schema::dropIfExists('quiz_questions'); + Schema::dropIfExists('quizzes'); + } +}; diff --git a/database/migrations/2026_05_05_220001_create_quiz_attempts_table.php b/database/migrations/2026_05_05_220001_create_quiz_attempts_table.php new file mode 100644 index 0000000..fd12f6a --- /dev/null +++ b/database/migrations/2026_05_05_220001_create_quiz_attempts_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('quiz_id')->constrained()->cascadeOnDelete(); + $table->unsignedSmallInteger('score')->default(0); + $table->unsignedSmallInteger('current_question')->default(0); + $table->enum('status', ['in_progress','completed'])->default('in_progress'); + $table->timestamp('started_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); + }); + Schema::create('quiz_attempt_answers', function (Blueprint $table) { + $table->id(); + $table->foreignId('quiz_attempt_id')->constrained()->cascadeOnDelete(); + $table->foreignId('quiz_question_id')->constrained()->cascadeOnDelete(); + $table->string('answer_given')->nullable(); + $table->boolean('is_correct')->default(false); + $table->unsignedSmallInteger('points_earned')->default(0); + $table->timestamps(); + }); + } + public function down(): void { + Schema::dropIfExists('quiz_attempt_answers'); + Schema::dropIfExists('quiz_attempts'); + } +}; diff --git a/resources/views/admin/quiz_questions/create.blade.php b/resources/views/admin/quiz_questions/create.blade.php new file mode 100644 index 0000000..47da9ed --- /dev/null +++ b/resources/views/admin/quiz_questions/create.blade.php @@ -0,0 +1,71 @@ +@extends('layouts.admin') +@section('title','Frage hinzufügen') +@section('content') +
+ ← Zurück zu {{ $quiz->title }} +
+

Neue Frage hinzufügen

+
+ @csrf + @php $qtype='multiple_choice'; $correctIdx=0; $question_text=''; $time_limit=''; $correct_answer=''; $options=[]; @endphp + +
+
+ + +
+
+ + +
+
+ + +
+ + {{-- MC / Exclusion options --}} +
+ + @for($i = 0; $i < 4; $i++) +
+ + +
+ @endfor +

Klicke den Radio-Button links neben der richtigen Antwort.

+
+ + {{-- True / False --}} +
+ + +
+ + {{-- Free text --}} +
+ + +
+
+ +
+
+
+@endsection diff --git a/resources/views/admin/quiz_questions/edit.blade.php b/resources/views/admin/quiz_questions/edit.blade.php new file mode 100644 index 0000000..f515358 --- /dev/null +++ b/resources/views/admin/quiz_questions/edit.blade.php @@ -0,0 +1,80 @@ +@extends('layouts.admin') +@section('title','Frage bearbeiten') +@section('content') +
+ ← Zurück zu {{ $question->quiz->title }} +
+

Frage bearbeiten

+
+ @csrf @method('PUT') + @php + $qtype = $question->type; + $question_text = $question->question_text; + $time_limit = $question->time_limit; + $correct_answer = $question->correct_answer; + $opts = $question->answerOptions->toArray(); + $options = array_column($opts, null); + $correctIdx = 0; + foreach ($opts as $i => $o) { if ($o['is_correct']) { $correctIdx = $i; break; } } + @endphp + +
+
+ + +
+
+ + +
+
+ + +
+ + {{-- MC / Exclusion options --}} +
+ + @for($i = 0; $i < 4; $i++) +
+ + +
+ @endfor +

Klicke den Radio-Button links neben der richtigen Antwort.

+
+ + {{-- True / False --}} +
+ + +
+ + {{-- Free text --}} +
+ + +
+
+ +
+
+
+@endsection diff --git a/resources/views/admin/quizzes/create.blade.php b/resources/views/admin/quizzes/create.blade.php new file mode 100644 index 0000000..85f33c1 --- /dev/null +++ b/resources/views/admin/quizzes/create.blade.php @@ -0,0 +1,28 @@ +@extends('layouts.admin') +@section('title','Neues Quiz') +@section('content') +
+ ← Zurück +
+

Neues Quiz

+
+ @csrf +
+ + +
+
+ + +
+
+ + +
+ +
+
+
+@endsection diff --git a/resources/views/admin/quizzes/edit.blade.php b/resources/views/admin/quizzes/edit.blade.php new file mode 100644 index 0000000..fecb3aa --- /dev/null +++ b/resources/views/admin/quizzes/edit.blade.php @@ -0,0 +1,72 @@ +@extends('layouts.admin') +@section('title','Quiz bearbeiten') +@section('content') +
+ ← Zurück + + {{-- Metadata --}} +
+

Quiz-Einstellungen

+
+ @csrf @method('PUT') +
+
+ + +
+
+ + +
+
+
+ + +
+
+ active?'checked':'' }} class="w-4 h-4 text-violet-600"> + +
+ +
+
+ + {{-- Questions --}} +
+

Fragen + ({{ $questions->count() }}/10) +

+ @if($questions->count() < 10) + + Frage hinzufügen + @else + ✅ 10 Fragen vollständig + @endif +
+ + @forelse($questions as $i => $q) +
+
+ {{ $i+1 }}. +
+ {{ $q->typeLabel() }} + @if($q->time_limit)⏱ {{ $q->time_limit }}s@endif +

{{ $q->question_text }}

+
+
+
+ Bearbeiten +
+ @csrf @method('DELETE') + +
+
+
+ @empty +
+ Noch keine Fragen. Füge bis zu 10 Fragen hinzu. +
+ @endforelse +
+@endsection diff --git a/resources/views/admin/quizzes/index.blade.php b/resources/views/admin/quizzes/index.blade.php new file mode 100644 index 0000000..b838cb2 --- /dev/null +++ b/resources/views/admin/quizzes/index.blade.php @@ -0,0 +1,61 @@ +@extends('layouts.admin') +@section('title','Quizzes') +@section('content') +
+

Quizzes

+
+ + + Neues Quiz +
+
+
+

JSON im gleichen Format wie der Export. Erstellt immer ein neues Quiz.

+
+ @csrf + + +
+
+
+
+
+ + + + + + + + + + + + @forelse($quizzes as $quiz) + + + + + + + + @empty + + @endforelse + +
FachTitelFragenStatus
{{ $quiz->subject->icon }} {{ $quiz->subject->name }}{{ $quiz->title }} + + {{ $quiz->questions_count }}/10 + + {{ $quiz->active ? '✅' : '⏸️' }} + ⬇ Export + Bearbeiten +
+ @csrf @method('DELETE') + +
+
Noch keine Quizzes. Erstes Quiz erstellen →
+
+@endsection diff --git a/resources/views/child/quiz/index.blade.php b/resources/views/child/quiz/index.blade.php new file mode 100644 index 0000000..21df699 --- /dev/null +++ b/resources/views/child/quiz/index.blade.php @@ -0,0 +1,46 @@ +@extends('layouts.child') +@section('content') +

🧠 Quiz

+

Beantworte 10 Fragen – max. 40 Münzen zu gewinnen!

+ +@foreach($subjects as $subjectId => $subject) +@php $group = $quizzes->get($subjectId, collect()) @endphp +@if($group->isNotEmpty()) +
+

{{ $subject->icon }} {{ $subject->name }}

+
+ @foreach($group as $quiz) +
+
+
+

{{ $quiz->title }}

+ @if($quiz->description)

{{ $quiz->description }}

@endif +
+ 📝 {{ $quiz->questions_count }} Fragen + 🏆 max. 40 Münzen + @if(isset($bestScores[$quiz->id])) + Bisher: {{ $bestScores[$quiz->id] }}/40 🪙 + @endif +
+
+
+ @csrf + +
+
+
+ @endforeach +
+
+@endif +@endforeach + +@if($quizzes->isEmpty()) +
+

🧩

+

Noch keine Quizzes verfügbar. Schau später nochmal vorbei!

+
+@endif +@endsection diff --git a/resources/views/child/quiz/question.blade.php b/resources/views/child/quiz/question.blade.php new file mode 100644 index 0000000..19a08bb --- /dev/null +++ b/resources/views/child/quiz/question.blade.php @@ -0,0 +1,123 @@ +@extends('layouts.child') +@section('content') +@php $tl = $question->time_limit ?? 0; @endphp +
+ + {{-- Header bar --}} +
+ ← Quiz +
+
+
+ + {{ $attempt->current_question + 1 }} / {{ $attempt->quiz->questions()->count() }} + +
+ + {{-- Timer bar --}} + + + {{-- Last answer flash --}} + @if(session('last_answer')) + @php $la = session('last_answer'); @endphp +
+ @if($la['timeout']) ⏱ Zeit abgelaufen – 0 Münzen + @elseif($la['correct']) ✅ Richtig! +{{ $la['points'] }} 🪙 + @else ❌ Leider falsch – 0 Münzen + @endif +
+ @endif + + {{-- Points badge --}} +
+ + +{{ $pointsIfCorrect }} 🪙 bei richtiger Antwort + +
+ + {{-- Question card --}} +
+

{{ $question->question_text }}

+ +
+ @csrf + + + @if(in_array($question->type, ['multiple_choice','exclusion'])) +
+ @foreach($question->answerOptions as $opt) + + @endforeach +
+ + @elseif($question->type === 'true_false') +
+ + +
+ + @elseif($question->type === 'free_text') +
+ +
+ @endif + + +
+
+
+@endsection diff --git a/resources/views/child/quiz/result.blade.php b/resources/views/child/quiz/result.blade.php new file mode 100644 index 0000000..e8216a8 --- /dev/null +++ b/resources/views/child/quiz/result.blade.php @@ -0,0 +1,63 @@ +@extends('layouts.child') +@section('content') +
+
{{ $attempt->stars() }}
+

{{ $attempt->score }} / 40 Münzen

+

{{ $attempt->quiz->title }}

+
+ +{{ $attempt->score }} 🪙 verdient! +
+
+ +{{-- Per-question breakdown --}} +
+ + + + + + + + + + + @foreach($answers as $i => $a) + @php + $display = $a->answer_given; + if ($display === '__timeout__') { + $display = '⏱ Zeit abgelaufen'; + } elseif (in_array($a->question->type, ['multiple_choice','exclusion'])) { + $opt = $a->question->answerOptions->firstWhere('id', (int)$a->answer_given); + $display = $opt ? $opt->text : '–'; + } elseif ($a->question->type === 'true_false') { + $display = $display === 'true' ? 'Wahr' : 'Falsch'; + } + $points_scale = \App\Models\QuizAttempt::POINTS; + @endphp + + + + + + + @endforeach + +
#FrageErgebnisMünzen
{{ $i+1 }}{{ Str::limit($a->question->question_text, 55) }} + @if($a->is_correct) + ✅ {{ $display }} + @else + ❌ {{ $display }} + @endif + + {{ $a->is_correct ? '+' . $a->points_earned : '0' }} +
+
+ +
+
+ @csrf + +
+ ← Zur Quiz-Übersicht +
+@endsection diff --git a/resources/views/layouts/admin.blade.php b/resources/views/layouts/admin.blade.php index 3ea213d..3a44f54 100644 --- a/resources/views/layouts/admin.blade.php +++ b/resources/views/layouts/admin.blade.php @@ -23,6 +23,9 @@ Fragen + + 🧠 Quizzes + 🎁 Belohnungen diff --git a/resources/views/layouts/child.blade.php b/resources/views/layouts/child.blade.php index 139eb4a..8da73df 100644 --- a/resources/views/layouts/child.blade.php +++ b/resources/views/layouts/child.blade.php @@ -13,6 +13,7 @@
Lernen + 🧠 Quiz 🪙 Belohnungen 💡 Erinnerung
diff --git a/routes/web.php b/routes/web.php index d0a501f..ee4802a 100755 --- a/routes/web.php +++ b/routes/web.php @@ -22,6 +22,14 @@ Route::middleware(['auth','admin'])->prefix('admin')->name('admin.')->group(func Route::resource('questions',Admin\QuestionController::class); Route::resource('rewards', Admin\RewardController::class)->except('show'); Route::resource('reference', Admin\ReferenceController::class)->except('show'); + Route::get ('quizzes/{quiz}/export', [Admin\QuizController::class,'export'])->name('quizzes.export'); + Route::post('quizzes/import', [Admin\QuizController::class,'import'])->name('quizzes.import'); + Route::resource('quizzes', Admin\QuizController::class); + Route::get ('quizzes/{quiz}/questions/create',[Admin\QuizQuestionController::class,'create'])->name('quizzes.questions.create'); + Route::post('quizzes/{quiz}/questions', [Admin\QuizQuestionController::class,'store']) ->name('quizzes.questions.store'); + Route::get ('quiz-questions/{question}/edit', [Admin\QuizQuestionController::class,'edit']) ->name('quiz-questions.edit'); + Route::put ('quiz-questions/{question}', [Admin\QuizQuestionController::class,'update']) ->name('quiz-questions.update'); + Route::delete('quiz-questions/{question}', [Admin\QuizQuestionController::class,'destroy'])->name('quiz-questions.destroy'); Route::get ('redemptions', [Admin\RedemptionController::class,'index']) ->name('redemptions.index'); Route::patch ('redemptions/{redemption}/approve', [Admin\RedemptionController::class,'approve'])->name('redemptions.approve'); Route::patch ('redemptions/{redemption}/reject', [Admin\RedemptionController::class,'reject']) ->name('redemptions.reject'); @@ -34,7 +42,12 @@ Route::middleware(['auth','child'])->group(function () { Route::post('lernen/{subject:slug}/antwort', [\App\Http\Controllers\Child\LearnController::class,'answer']) ->name('learn.answer'); Route::get ('belohnungen', [\App\Http\Controllers\Child\RewardController::class,'index']) ->name('rewards.index'); Route::post('belohnungen/{reward}/einloesen', [\App\Http\Controllers\Child\RewardController::class,'redeem']) ->name('rewards.redeem'); - Route::get ('erinnerung', [\App\Http\Controllers\Child\ReferenceController::class,'index'])->name('reference.index'); + Route::get ('quiz', [\App\Http\Controllers\Child\QuizController::class,'index']) ->name('quiz.index'); + Route::get ('quiz/versuch/{attempt}', [\App\Http\Controllers\Child\QuizController::class,'question'])->name('quiz.question'); + Route::post('quiz/versuch/{attempt}/antwort', [\App\Http\Controllers\Child\QuizController::class,'answer']) ->name('quiz.answer'); + Route::get ('quiz/ergebnis/{attempt}', [\App\Http\Controllers\Child\QuizController::class,'result']) ->name('quiz.result'); + Route::post('quiz/{quiz}/start', [\App\Http\Controllers\Child\QuizController::class,'start']) ->name('quiz.start'); + Route::get ('erinnerung', [\App\Http\Controllers\Child\ReferenceController::class,'index'])->name('reference.index'); Route::get ('erinnerung/{reference:slug}', [\App\Http\Controllers\Child\ReferenceController::class,'show']) ->name('reference.show'); });