feat: Lernapp mit Mathe/Deutsch/Englisch, Münzsystem und Belohnungen

This commit is contained in:
root
2026-05-05 14:41:09 +00:00
parent 21e40cd2da
commit bd1640994c
45 changed files with 1522 additions and 58 deletions
@@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\QuestionAttempt;
use App\Models\RewardRedemption;
class DashboardController extends Controller {
public function index() {
$children = User::where('role','child')->count();
$questions = \App\Models\Question::count();
$attempts_today = QuestionAttempt::whereDate('created_at', today())->count();
$pending = RewardRedemption::where('status','pending')->count();
$topKids = User::where('role','child')->orderByDesc('points')->take(5)->get();
$recentAttempts = QuestionAttempt::with(['user','question.subject'])
->latest('id')->take(10)->get();
return view('admin.dashboard', compact(
'children','questions','attempts_today','pending','topKids','recentAttempts'
));
}
}
@@ -0,0 +1,100 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Question;
use App\Models\Subject;
use App\Models\AnswerOption;
use Illuminate\Http\Request;
class QuestionController extends Controller {
public function index(Request $r) {
$subjects = Subject::all();
$query = Question::with('subject')->latest();
if ($r->filled('subject')) $query->where('subject_id', $r->subject);
$questions = $query->paginate(20)->withQueryString();
return view('admin.questions.index', compact('questions','subjects'));
}
public function create() {
$subjects = Subject::all();
return view('admin.questions.create', compact('subjects'));
}
public function store(Request $r) {
$r->validate([
'subject_id' => 'required|exists:subjects,id',
'question_text' => 'required|string',
'type' => 'required|in:multiple_choice,number_input',
'difficulty' => 'required|in:1,2,3',
'options' => 'required_if:type,multiple_choice|array|min:2',
'options.*' => 'required|string',
'correct' => 'required_if:type,multiple_choice|integer',
'number_answer' => 'required_if:type,number_input',
]);
$pts = [1=>5, 2=>10, 3=>20][$r->difficulty];
$q = Question::create([
'subject_id' => $r->subject_id,
'question_text' => $r->question_text,
'type' => $r->type,
'difficulty' => $r->difficulty,
'points_value' => $pts,
'active' => true,
]);
if ($r->type === 'multiple_choice') {
foreach ($r->options as $i => $text) {
if (trim($text) === '') continue;
AnswerOption::create([
'question_id' => $q->id,
'text' => $text,
'is_correct' => ($i == $r->correct),
'sort_order' => $i,
]);
}
} else {
AnswerOption::create([
'question_id' => $q->id,
'text' => $r->number_answer,
'is_correct' => true,
'sort_order' => 0,
]);
}
return redirect()->route('admin.questions.index')->with('success','Frage gespeichert.');
}
public function edit(Question $question) {
$subjects = Subject::all();
$question->load('answerOptions');
return view('admin.questions.edit', compact('question','subjects'));
}
public function update(Request $r, Question $question) {
$r->validate([
'subject_id' => 'required|exists:subjects,id',
'question_text' => 'required|string',
'difficulty' => 'required|in:1,2,3',
'options' => 'array',
'options.*' => 'string',
'correct' => 'integer',
]);
$pts = [1=>5, 2=>10, 3=>20][$r->difficulty];
$question->update([
'subject_id' => $r->subject_id,
'question_text' => $r->question_text,
'difficulty' => $r->difficulty,
'points_value' => $pts,
'active' => $r->boolean('active', true),
]);
if ($r->filled('options')) {
$question->answerOptions()->delete();
foreach ($r->options as $i => $text) {
if (trim($text) === '') continue;
AnswerOption::create([
'question_id' => $question->id,
'text' => $text,
'is_correct' => ($i == $r->correct),
'sort_order' => $i,
]);
}
}
return redirect()->route('admin.questions.index')->with('success','Gespeichert.');
}
public function destroy(Question $question) {
$question->delete();
return redirect()->route('admin.questions.index')->with('success','Frage gelöscht.');
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\RewardRedemption;
use Illuminate\Http\Request;
class RedemptionController extends Controller {
public function index() {
$pending = RewardRedemption::with(['user','reward'])->where('status','pending')->latest()->get();
$resolved = RewardRedemption::with(['user','reward','resolver'])->whereIn('status',['approved','rejected'])->latest()->take(50)->get();
return view('admin.redemptions.index', compact('pending','resolved'));
}
public function approve(RewardRedemption $redemption) {
if ($redemption->status !== 'pending') abort(422);
$redemption->update([
'status' => 'approved',
'resolved_at' => now(),
'resolved_by' => auth()->id(),
]);
return back()->with('success','Freigegeben!');
}
public function reject(Request $r, RewardRedemption $redemption) {
if ($redemption->status !== 'pending') abort(422);
$redemption->update([
'status' => 'rejected',
'note' => $r->note,
'resolved_at' => now(),
'resolved_by' => auth()->id(),
]);
// Punkte zurückgeben
$redemption->user->increment('points', $redemption->points_spent);
return back()->with('success','Abgelehnt, Punkte zurückgebucht.');
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Reward;
use Illuminate\Http\Request;
class RewardController extends Controller {
public function index() {
$rewards = Reward::orderBy('points_cost')->get();
return view('admin.rewards.index', compact('rewards'));
}
public function create() { return view('admin.rewards.create'); }
public function store(Request $r) {
$r->validate([
'name' => 'required|string|max:80',
'description' => 'nullable|string|max:200',
'icon' => 'required|string|max:10',
'points_cost' => 'required|integer|min:1',
'minutes' => 'nullable|integer|min:1',
]);
Reward::create(array_merge($r->only('name','description','icon','points_cost','minutes'), ['active'=>true]));
return redirect()->route('admin.rewards.index')->with('success','Belohnung erstellt.');
}
public function edit(Reward $reward) { return view('admin.rewards.edit', compact('reward')); }
public function update(Request $r, Reward $reward) {
$r->validate([
'name' => 'required|string|max:80',
'description' => 'nullable|string|max:200',
'icon' => 'required|string|max:10',
'points_cost' => 'required|integer|min:1',
'minutes' => 'nullable|integer|min:1',
]);
$reward->update(array_merge(
$r->only('name','description','icon','points_cost','minutes'),
['active' => $r->boolean('active', true)]
));
return redirect()->route('admin.rewards.index')->with('success','Gespeichert.');
}
public function destroy(Reward $reward) {
$reward->delete();
return redirect()->route('admin.rewards.index')->with('success','Belohnung gelöscht.');
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class UserController extends Controller {
public function index() {
$users = User::where('role','child')->withCount('attempts')->orderBy('name')->get();
return view('admin.users.index', compact('users'));
}
public function create() { return view('admin.users.create'); }
public function store(Request $r) {
$r->validate([
'name' => 'required|string|max:60',
'email' => 'required|email|unique:users',
'password' => 'required|min:6',
]);
User::create([
'name' => $r->name,
'email' => $r->email,
'password' => Hash::make($r->password),
'role' => 'child',
'points' => 0,
]);
return redirect()->route('admin.users.index')->with('success','Kind-Konto erstellt.');
}
public function edit(User $user) { return view('admin.users.edit', compact('user')); }
public function update(Request $r, User $user) {
$r->validate([
'name' => 'required|string|max:60',
'email' => 'required|email|unique:users,email,'.$user->id,
'password' => 'nullable|min:6',
'points' => 'required|integer|min:0',
]);
$user->fill(['name'=>$r->name,'email'=>$r->email,'points'=>$r->points]);
if ($r->filled('password')) $user->password = Hash::make($r->password);
$user->save();
return redirect()->route('admin.users.index')->with('success','Gespeichert.');
}
public function destroy(User $user) {
$user->delete();
return redirect()->route('admin.users.index')->with('success','Konto gelöscht.');
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Child;
use App\Http\Controllers\Controller;
use App\Models\Subject;
use App\Models\QuestionAttempt;
class DashboardController extends Controller {
public function index() {
$user = auth()->user();
$level = $user->level();
$streak = $user->currentStreak();
$subjects = Subject::withCount(['questions as total' => fn($q) => $q->where('active',true)])->get()
->map(function($s) use ($user) {
$s->correct = QuestionAttempt::whereHas('question', fn($q) => $q->where('subject_id',$s->id))
->where('user_id',$user->id)->where('is_correct',true)->count();
return $s;
});
$recentAttempts = QuestionAttempt::with('question.subject')
->where('user_id',$user->id)->latest('id')->take(5)->get();
$pendingRedemptions = $user->redemptions()->where('status','pending')->count();
return view('child.dashboard', compact('user','level','streak','subjects','recentAttempts','pendingRedemptions'));
}
}
@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\Child;
use App\Http\Controllers\Controller;
use App\Models\Subject;
use App\Models\Question;
use App\Models\QuestionAttempt;
use Illuminate\Http\Request;
class LearnController extends Controller {
public function subjects() {
$user = auth()->user();
$subjects = Subject::all()->map(function($s) use ($user) {
$s->correct = QuestionAttempt::whereHas('question', fn($q) => $q->where('subject_id',$s->id))
->where('user_id',$user->id)->where('is_correct',true)->count();
$s->total = $s->questions()->where('active',true)->count();
return $s;
});
return view('child.learn.subjects', compact('subjects'));
}
public function quiz(Subject $subject) {
$user = auth()->user();
// Bevorzuge Fragen die heute noch nicht beantwortet wurden
$answeredToday = QuestionAttempt::where('user_id',$user->id)
->whereDate('created_at', today())
->pluck('question_id');
$question = $subject->activeQuestions()
->whereNotIn('id', $answeredToday)
->with('answerOptions')
->inRandomOrder()
->first();
// Falls alle beantwortet: ganz random
if (!$question) {
$question = $subject->activeQuestions()->with('answerOptions')->inRandomOrder()->first();
}
if (!$question) {
return redirect()->route('learn.subjects')->with('info','Noch keine Fragen für dieses Fach.');
}
// Optionen mischen
$question->answerOptions = $question->answerOptions->shuffle();
return view('child.learn.quiz', compact('subject','question'));
}
public function answer(Request $r, Subject $subject) {
$r->validate(['question_id'=>'required|exists:questions,id','answer'=>'required']);
$user = auth()->user();
$question = Question::with('answerOptions')->findOrFail($r->question_id);
$correct = $question->answerOptions->where('is_correct',true)->first();
$isRight = (string)$r->answer === (string)$correct->id;
$earned = 0;
if ($isRight) {
$earned = $question->points_value;
// Streak-Bonus: 3+ in Folge = +5
$streak = $user->currentStreak();
if ($streak >= 2) $earned += 5;
$user->increment('points', $earned);
}
QuestionAttempt::create([
'user_id' => $user->id,
'question_id' => $question->id,
'is_correct' => $isRight,
'points_earned'=> $earned,
]);
$newStreak = $user->fresh()->currentStreak();
return view('child.learn.result', compact('subject','question','correct','isRight','earned','newStreak'));
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Child;
use App\Http\Controllers\Controller;
use App\Models\Reward;
use App\Models\RewardRedemption;
use Illuminate\Http\Request;
class RewardController extends Controller {
public function index() {
$user = auth()->user();
$rewards = Reward::where('active',true)->orderBy('points_cost')->get();
$history = RewardRedemption::with('reward')->where('user_id',$user->id)->latest()->take(20)->get();
return view('child.rewards.index', compact('user','rewards','history'));
}
public function redeem(Request $r, Reward $reward) {
$user = auth()->user();
if (!$reward->active) abort(422);
if ($user->points < $reward->points_cost) {
return back()->with('error','Nicht genug Münzen!');
}
$user->decrement('points', $reward->points_cost);
RewardRedemption::create([
'user_id' => $user->id,
'reward_id' => $reward->id,
'status' => 'pending',
'points_spent'=> $reward->points_cost,
]);
return back()->with('success','Eingelöst! Warte auf Freigabe.');
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class EnsureAdmin {
public function handle(Request $request, Closure $next) {
if (!$request->user() || !$request->user()->isAdmin()) {
abort(403);
}
return $next($request);
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class EnsureChild {
public function handle(Request $request, Closure $next) {
if (!$request->user() || !$request->user()->isChild()) {
abort(403);
}
return $next($request);
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AnswerOption extends Model {
public $timestamps = false;
protected $fillable = ['question_id','text','is_correct','sort_order'];
protected $casts = ['is_correct' => 'boolean'];
public function question() { return $this->belongsTo(Question::class); }
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Question extends Model {
protected $fillable = ['subject_id','question_text','type','difficulty','points_value','active'];
protected $casts = ['active' => 'boolean'];
public function subject() { return $this->belongsTo(Subject::class); }
public function answerOptions() { return $this->hasMany(AnswerOption::class)->orderBy('sort_order'); }
public function attempts() { return $this->hasMany(QuestionAttempt::class); }
public function difficultyStars(): string {
return str_repeat('⭐', $this->difficulty);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class QuestionAttempt extends Model {
public $timestamps = false;
protected $fillable = ['user_id','question_id','is_correct','points_earned'];
protected $casts = ['is_correct' => 'boolean', 'created_at' => 'datetime'];
public function user() { return $this->belongsTo(User::class); }
public function question() { return $this->belongsTo(Question::class); }
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Reward extends Model {
protected $fillable = ['name','description','icon','points_cost','minutes','active'];
protected $casts = ['active' => 'boolean'];
public function redemptions() { return $this->hasMany(RewardRedemption::class); }
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class RewardRedemption extends Model {
protected $fillable = ['user_id','reward_id','status','points_spent','note','resolved_at','resolved_by'];
protected $casts = ['resolved_at' => 'datetime'];
public function user() { return $this->belongsTo(User::class); }
public function reward() { return $this->belongsTo(Reward::class); }
public function resolver() { return $this->belongsTo(User::class, 'resolved_by'); }
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Subject extends Model {
protected $fillable = ['name','slug','icon','color'];
public function questions() { return $this->hasMany(Question::class); }
public function activeQuestions() { return $this->questions()->where('active', true); }
}
+26 -23
View File
@@ -1,32 +1,35 @@
<?php <?php
namespace App\Models; namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
class User extends Authenticatable {
#[Fillable(['name', 'email', 'password'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable; use HasFactory, Notifiable;
protected $fillable = ['name','email','password','role','points'];
protected $hidden = ['password','remember_token'];
protected $casts = ['email_verified_at' => 'datetime', 'password' => 'hashed'];
/** public function isAdmin(): bool { return $this->role === 'admin'; }
* Get the attributes that should be cast. public function isChild(): bool { return $this->role === 'child'; }
*
* @return array<string, string> public function attempts() { return $this->hasMany(QuestionAttempt::class); }
*/ public function redemptions() { return $this->hasMany(RewardRedemption::class); }
protected function casts(): array
{ public function level(): array {
return [ $p = $this->points;
'email_verified_at' => 'datetime', if ($p >= 600) return ['label' => 'Meister', 'icon' => '👑', 'color' => 'text-yellow-500', 'next' => null, 'progress' => 100];
'password' => 'hashed', if ($p >= 300) return ['label' => 'Wissensprofi', 'icon' => '🌟', 'color' => 'text-purple-500', 'next' => 600, 'progress' => (int)(($p-300)/3)];
]; if ($p >= 100) return ['label' => 'Lernstar', 'icon' => '⭐', 'color' => 'text-blue-500', 'next' => 300, 'progress' => (int)(($p-100)/2)];
return ['label' => 'Anfänger', 'icon' => '🌱', 'color' => 'text-green-500', 'next' => 100, 'progress' => $p];
}
public function currentStreak(): int {
$last = $this->attempts()->latest('id')->take(10)->get();
$streak = 0;
foreach ($last as $a) {
if (!$a->is_correct) break;
$streak++;
}
return $streak;
} }
} }
+7 -6
View File
@@ -1,5 +1,4 @@
<?php <?php
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
@@ -10,9 +9,11 @@ return Application::configure(basePath: dirname(__DIR__))
commands: __DIR__.'/../routes/console.php', commands: __DIR__.'/../routes/console.php',
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware) {
// $middleware->alias([
'admin' => \App\Http\Middleware\EnsureAdmin::class,
'child' => \App\Http\Middleware\EnsureChild::class,
]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions) {})
// ->create();
})->create();
@@ -0,0 +1,15 @@
<?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('users', function (Blueprint $table) {
$table->enum('role', ['admin', 'child'])->default('child')->after('email');
$table->unsignedInteger('points')->default(0)->after('role');
});
}
public function down(): void {
Schema::table('users', fn(Blueprint $t) => $t->dropColumn(['role','points']));
}
};
@@ -0,0 +1,17 @@
<?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::create('subjects', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('icon')->default('📚');
$table->string('color')->default('blue');
$table->timestamps();
});
}
public function down(): void { Schema::dropIfExists('subjects'); }
};
@@ -0,0 +1,19 @@
<?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::create('questions', function (Blueprint $table) {
$table->id();
$table->foreignId('subject_id')->constrained()->onDelete('cascade');
$table->text('question_text');
$table->enum('type', ['multiple_choice', 'number_input'])->default('multiple_choice');
$table->unsignedTinyInteger('difficulty')->default(1);
$table->unsignedTinyInteger('points_value')->default(5);
$table->boolean('active')->default(true);
$table->timestamps();
});
}
public function down(): void { Schema::dropIfExists('questions'); }
};
@@ -0,0 +1,16 @@
<?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::create('answer_options', function (Blueprint $table) {
$table->id();
$table->foreignId('question_id')->constrained()->onDelete('cascade');
$table->string('text');
$table->boolean('is_correct')->default(false);
$table->unsignedTinyInteger('sort_order')->default(0);
});
}
public function down(): void { Schema::dropIfExists('answer_options'); }
};
@@ -0,0 +1,17 @@
<?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::create('question_attempts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('question_id')->constrained()->onDelete('cascade');
$table->boolean('is_correct');
$table->unsignedTinyInteger('points_earned')->default(0);
$table->timestamp('created_at')->useCurrent();
});
}
public function down(): void { Schema::dropIfExists('question_attempts'); }
};
@@ -0,0 +1,19 @@
<?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::create('rewards', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('description')->nullable();
$table->string('icon')->default('🎁');
$table->unsignedInteger('points_cost');
$table->unsignedInteger('minutes')->nullable();
$table->boolean('active')->default(true);
$table->timestamps();
});
}
public function down(): void { Schema::dropIfExists('rewards'); }
};
@@ -0,0 +1,20 @@
<?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::create('reward_redemptions', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('reward_id')->constrained()->onDelete('cascade');
$table->enum('status', ['pending','approved','rejected'])->default('pending');
$table->unsignedInteger('points_spent');
$table->string('note')->nullable();
$table->timestamp('resolved_at')->nullable();
$table->foreignId('resolved_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
});
}
public function down(): void { Schema::dropIfExists('reward_redemptions'); }
};
+107 -15
View File
@@ -1,25 +1,117 @@
<?php <?php
namespace Database\Seeders; namespace Database\Seeders;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents; use App\Models\Subject;
use App\Models\Question;
use App\Models\AnswerOption;
use App\Models\Reward;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class DatabaseSeeder extends Seeder class DatabaseSeeder extends Seeder {
{ public function run(): void {
use WithoutModelEvents; User::create([
'name' => 'Admin',
'email' => 'admin@lernapp.de',
'password' => Hash::make('Admin1234!'),
'role' => 'admin',
'points' => 0,
]);
/** $subjects = [
* Seed the application's database. ['name'=>'Mathe', 'slug'=>'mathe', 'icon'=>'🔢', 'color'=>'green'],
*/ ['name'=>'Deutsch', 'slug'=>'deutsch', 'icon'=>'📖', 'color'=>'blue'],
public function run(): void ['name'=>'Englisch','slug'=>'englisch', 'icon'=>'🌍', 'color'=>'orange'],
{ ];
// User::factory(10)->create(); foreach ($subjects as $s) {
Subject::create($s);
}
User::factory()->create([ $questions = [
'name' => 'Test User', // Mathe
'email' => 'test@example.com', ['subject'=>'mathe','text'=>'7 × 8 = ?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['56',true],['48',false],['54',false],['64',false]]],
['subject'=>'mathe','text'=>'144 ÷ 12 = ?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['12',true],['10',false],['11',false],['14',false]]],
['subject'=>'mathe','text'=>'234 + 567 = ?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['801',true],['791',false],['800',false],['811',false]]],
['subject'=>'mathe','text'=>'1000 387 = ?','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['613',true],['623',false],['713',false],['587',false]]],
['subject'=>'mathe','text'=>'Eine Schachtel hat 24 Pralinen. Wie viele Schachteln brauchst du für 96 Pralinen?','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['4',true],['3',false],['5',false],['6',false]]],
['subject'=>'mathe','text'=>'Was ist die Hälfte von 136?','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['68',true],['64',false],['70',false],['72',false]]],
['subject'=>'mathe','text'=>'Ein Rechteck ist 9 cm lang und 6 cm breit. Wie groß ist sein Flächeninhalt?','type'=>'multiple_choice','diff'=>3,'pts'=>20,
'options'=>[['54 cm²',true],['30 cm²',false],['45 cm²',false],['60 cm²',false]]],
['subject'=>'mathe','text'=>'3 × 4 + 2 × 5 = ?','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['22',true],['34',false],['26',false],['20',false]]],
// Deutsch
['subject'=>'deutsch','text'=>'Welches Wort ist ein Nomen?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['Hund',true],['laufen',false],['schnell',false],['grün',false]]],
['subject'=>'deutsch','text'=>'Was ist die Einzahl von „Häuser"?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['Haus',true],['Häusen',false],['Häuse',false],['Hausung',false]]],
['subject'=>'deutsch','text'=>'Welche Zeitform ist: „Er spielte Fußball."?','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['Präteritum',true],['Präsens',false],['Futur',false],['Perfekt',false]]],
['subject'=>'deutsch','text'=>'Welche Satzart ist das: „Wie schön der Tag ist!"?','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['Ausrufesatz',true],['Aussagesatz',false],['Fragesatz',false],['Aufforderungssatz',false]]],
['subject'=>'deutsch','text'=>'Was ist das Gegenteil von „fröhlich"?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['traurig',true],['lustig',false],['mutig',false],['fleißig',false]]],
['subject'=>'deutsch','text'=>'Welches Wort ist ein Verb?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['rennen',true],['Tisch',false],['blau',false],['schnell',false]]],
['subject'=>'deutsch','text'=>'Wie heißt der Plural von „das Kind"?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['die Kinder',true],['die Kinde',false],['die Kinds',false],['die Kindern',false]]],
['subject'=>'deutsch','text'=>'Welches Komma ist richtig? „Ich esse ___ Brot, Butter und Käse."','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['kein Komma nötig',true],['nach „esse"',false],['nach „Brot"',false],['nach „Butter"',false]]],
// Englisch
['subject'=>'englisch','text'=>'What is "der Hund" in English?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['the dog',true],['the cat',false],['the horse',false],['the pig',false]]],
['subject'=>'englisch','text'=>'Which word means "groß"?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['big',true],['small',false],['old',false],['fast',false]]],
['subject'=>'englisch','text'=>'"I ___ to school every day." Which word fits?','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['go',true],['going',false],['goes',false],['gone',false]]],
['subject'=>'englisch','text'=>'What is the plural of "child"?','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['children',true],['childs',false],['childes',false],['child',false]]],
['subject'=>'englisch','text'=>'What color is "gelb"?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['yellow',true],['red',false],['green',false],['blue',false]]],
['subject'=>'englisch','text'=>'How do you say "Guten Morgen"?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['Good morning',true],['Good night',false],['Good evening',false],['Good afternoon',false]]],
['subject'=>'englisch','text'=>'"She ___ a book yesterday." Which word fits?','type'=>'multiple_choice','diff'=>3,'pts'=>20,
'options'=>[['read',true],['reads',false],['reading',false],['is reading',false]]],
['subject'=>'englisch','text'=>'What is the opposite of "hot"?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['cold',true],['warm',false],['cool',false],['fast',false]]],
];
$subjectMap = Subject::pluck('id','slug')->all();
foreach ($questions as $i => $q) {
$question = Question::create([
'subject_id' => $subjectMap[$q['subject']],
'question_text' => $q['text'],
'type' => $q['type'],
'difficulty' => $q['diff'],
'points_value' => $q['pts'],
'active' => true,
]);
foreach ($q['options'] as $j => [$text, $correct]) {
AnswerOption::create([
'question_id' => $question->id,
'text' => $text,
'is_correct' => $correct,
'sort_order' => $j,
]); ]);
} }
} }
$rewards = [
['name'=>'30 Min. Fernsehen', 'description'=>'30 Minuten Fernsehzeit', 'icon'=>'📺','points_cost'=>50, 'minutes'=>30],
['name'=>'1 Std. Fernsehen', 'description'=>'Eine Stunde Fernsehzeit', 'icon'=>'📺','points_cost'=>90, 'minutes'=>60],
['name'=>'30 Min. Konsole', 'description'=>'30 Minuten Spielkonsole', 'icon'=>'🎮','points_cost'=>75, 'minutes'=>30],
['name'=>'1 Std. Konsole', 'description'=>'Eine Stunde Spielkonsole', 'icon'=>'🎮','points_cost'=>130,'minutes'=>60],
['name'=>'Wunschdessert', 'description'=>'Ein Dessert deiner Wahl', 'icon'=>'🍰','points_cost'=>80, 'minutes'=>null],
['name'=>'1 Std. länger aufbleiben','description'=>'Heute eine Stunde später ins Bett','icon'=>'🌙','points_cost'=>120,'minutes'=>null],
];
foreach ($rewards as $r) {
Reward::create(array_merge($r, ['active' => true]));
}
}
}
+51
View File
@@ -0,0 +1,51 @@
@extends('layouts.admin')
@section('title','Dashboard')
@section('content')
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200">
<div class="text-2xl font-bold text-slate-800">{{ $children }}</div>
<div class="text-sm text-slate-500 mt-1">👦 Kinder</div>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200">
<div class="text-2xl font-bold text-slate-800">{{ $questions }}</div>
<div class="text-sm text-slate-500 mt-1"> Fragen</div>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200">
<div class="text-2xl font-bold text-slate-800">{{ $attempts_today }}</div>
<div class="text-sm text-slate-500 mt-1">📝 Antworten heute</div>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200 {{ $pending > 0 ? 'border-red-300 bg-red-50' : '' }}">
<div class="text-2xl font-bold {{ $pending > 0 ? 'text-red-600' : 'text-slate-800' }}">{{ $pending }}</div>
<div class="text-sm text-slate-500 mt-1">🎁 Offene Einlösungen</div>
</div>
</div>
<div class="grid lg:grid-cols-2 gap-6">
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
<h2 class="font-semibold text-slate-800 mb-4">🏆 Bestenliste</h2>
@forelse($topKids as $i => $kid)
<div class="flex items-center justify-between py-2 {{ !$loop->last ? 'border-b border-slate-100' : '' }}">
<div class="flex items-center gap-3">
<span class="text-lg">{{ ['🥇','🥈','🥉','4.','5.'][$i] }}</span>
<span class="font-medium text-slate-700">{{ $kid->name }}</span>
</div>
<span class="text-amber-600 font-bold">🪙 {{ $kid->points }}</span>
</div>
@empty<p class="text-slate-400 text-sm">Noch keine Kinder angelegt.</p>@endforelse
</div>
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
<h2 class="font-semibold text-slate-800 mb-4">📝 Letzte Aktivität</h2>
<div class="space-y-2">
@forelse($recentAttempts as $a)
<div class="flex items-center justify-between text-sm">
<div class="flex items-center gap-2">
<span>{{ $a->is_correct ? '✅' : '❌' }}</span>
<span class="text-slate-600">{{ $a->user->name }}</span>
<span class="text-slate-400"> {{ $a->question->subject->name }}</span>
</div>
<span class="text-xs text-slate-400">{{ $a->created_at->diffForHumans() }}</span>
</div>
@empty<p class="text-slate-400 text-sm">Noch keine Aktivität.</p>@endforelse
</div>
</div>
</div>
@endsection
@@ -0,0 +1,54 @@
@extends('layouts.admin')
@section('title','Neue Frage')
@section('content')
<div class="max-w-2xl">
<a href="{{ route('admin.questions.index') }}" class="text-sm text-slate-500 hover:text-slate-700 mb-4 inline-block"> Zurück</a>
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6" x-data="{type:'multiple_choice'}">
<h2 class="font-semibold text-slate-800 mb-5">Neue Frage erstellen</h2>
<form method="POST" action="{{ route('admin.questions.store') }}" class="space-y-5">
@csrf
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Fach</label>
<select name="subject_id" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
@foreach($subjects as $s)<option value="{{ $s->id }}">{{ $s->icon }} {{ $s->name }}</option>@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Schwierigkeit</label>
<select name="difficulty" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
<option value="1"> Leicht (5 Münzen)</option>
<option value="2">⭐⭐ Mittel (10 Münzen)</option>
<option value="3">⭐⭐⭐ Schwer (20 Münzen)</option>
</select>
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Typ</label>
<select name="type" x-model="type" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
<option value="multiple_choice">Multiple Choice</option>
<option value="number_input">Zahlenantwort</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Frage</label>
<textarea name="question_text" required rows="2" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">{{ old('question_text') }}</textarea>
</div>
<div x-show="type==='multiple_choice'">
<label class="block text-sm font-medium text-slate-700 mb-2">Antwortmöglichkeiten <span class="text-slate-400">(Richtige markieren)</span></label>
@for($i=0;$i<4;$i++)
<div class="flex items-center gap-3 mb-2">
<input type="radio" name="correct" value="{{ $i }}" {{ $i===0?'checked':'' }} class="accent-violet-600">
<input type="text" name="options[]" placeholder="Antwort {{ $i+1 }}" class="flex-1 border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
</div>
@endfor
</div>
<div x-show="type==='number_input'">
<label class="block text-sm font-medium text-slate-700 mb-1">Richtige Antwort (Zahl)</label>
<input type="text" name="number_answer" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
</div>
<button type="submit" class="w-full bg-violet-600 hover:bg-violet-700 text-white py-2 rounded-lg font-medium text-sm">Frage speichern</button>
</form>
</div>
</div>
@endsection
@@ -0,0 +1,49 @@
@extends('layouts.admin')
@section('title','Frage bearbeiten')
@section('content')
<div class="max-w-2xl">
<a href="{{ route('admin.questions.index') }}" class="text-sm text-slate-500 hover:text-slate-700 mb-4 inline-block"> Zurück</a>
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h2 class="font-semibold text-slate-800 mb-5">Frage bearbeiten</h2>
<form method="POST" action="{{ route('admin.questions.update',$question) }}" class="space-y-5">
@csrf @method('PUT')
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Fach</label>
<select name="subject_id" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
@foreach($subjects as $s)<option value="{{ $s->id }}" {{ $s->id==$question->subject_id?'selected':'' }}>{{ $s->icon }} {{ $s->name }}</option>@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Schwierigkeit</label>
<select name="difficulty" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
@foreach([1=>'⭐ Leicht (5 Münzen)',2=>'⭐⭐ Mittel (10 Münzen)',3=>'⭐⭐⭐ Schwer (20 Münzen)'] as $v=>$l)
<option value="{{ $v }}" {{ $question->difficulty==$v?'selected':'' }}>{{ $l }}</option>
@endforeach
</select>
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Frage</label>
<textarea name="question_text" required rows="2" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">{{ old('question_text',$question->question_text) }}</textarea>
</div>
@if($question->type==='multiple_choice')
<div>
<label class="block text-sm font-medium text-slate-700 mb-2">Antwortmöglichkeiten</label>
@foreach($question->answerOptions as $i=>$opt)
<div class="flex items-center gap-3 mb-2">
<input type="radio" name="correct" value="{{ $i }}" {{ $opt->is_correct?'checked':'' }} class="accent-violet-600">
<input type="text" name="options[]" value="{{ $opt->text }}" class="flex-1 border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
</div>
@endforeach
</div>
@endif
<div class="flex items-center gap-3">
<input type="checkbox" name="active" id="active" value="1" {{ $question->active?'checked':'' }} class="accent-violet-600">
<label for="active" class="text-sm font-medium text-slate-700">Frage aktiv</label>
</div>
<button type="submit" class="w-full bg-violet-600 hover:bg-violet-700 text-white py-2 rounded-lg font-medium text-sm">Speichern</button>
</form>
</div>
</div>
@endsection
@@ -0,0 +1,47 @@
@extends('layouts.admin')
@section('title','Fragen')
@section('content')
<div class="flex flex-wrap gap-3 items-center justify-between mb-6">
<form method="GET" class="flex gap-2">
<select name="subject" class="border border-slate-300 rounded-lg px-3 py-2 text-sm">
<option value="">Alle Fächer</option>
@foreach($subjects as $s)<option value="{{ $s->id }}" {{ request('subject')==$s->id?'selected':'' }}>{{ $s->icon }} {{ $s->name }}</option>@endforeach
</select>
<button class="bg-slate-700 text-white px-4 py-2 rounded-lg text-sm">Filtern</button>
</form>
<a href="{{ route('admin.questions.create') }}" class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg text-sm font-medium">+ Neue Frage</a>
</div>
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-slate-50 border-b border-slate-200">
<tr>
<th class="text-left px-4 py-3 font-medium text-slate-600">Frage</th>
<th class="text-left px-4 py-3 font-medium text-slate-600">Fach</th>
<th class="text-center px-4 py-3 font-medium text-slate-600">Schwierigkeit</th>
<th class="text-center px-4 py-3 font-medium text-slate-600">Aktiv</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
@forelse($questions as $q)
<tr class="hover:bg-slate-50">
<td class="px-4 py-3 text-slate-700 max-w-xs truncate">{{ $q->question_text }}</td>
<td class="px-4 py-3"><span class="text-base">{{ $q->subject->icon }}</span> {{ $q->subject->name }}</td>
<td class="px-4 py-3 text-center">{{ $q->difficultyStars() }}</td>
<td class="px-4 py-3 text-center">{{ $q->active ? '✅' : '⏸️' }}</td>
<td class="px-4 py-3 text-right whitespace-nowrap">
<a href="{{ route('admin.questions.edit',$q) }}" class="text-violet-600 hover:underline mr-3">Bearbeiten</a>
<form method="POST" action="{{ route('admin.questions.destroy',$q) }}" class="inline" onsubmit="return confirm('Löschen?')">
@csrf @method('DELETE')
<button class="text-red-500 hover:underline">Löschen</button>
</form>
</td>
</tr>
@empty
<tr><td colspan="5" class="px-4 py-8 text-center text-slate-400">Keine Fragen vorhanden.</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-4">{{ $questions->links() }}</div>
@endsection
@@ -0,0 +1,63 @@
@extends('layouts.admin')
@section('title','Einlösungen')
@section('content')
@if($pending->count())
<h2 class="font-semibold text-slate-800 mb-4"> Warten auf Freigabe</h2>
<div class="space-y-3 mb-8">
@foreach($pending as $red)
<div class="bg-white rounded-xl shadow-sm border border-amber-200 p-5 flex items-center justify-between gap-4">
<div class="flex items-center gap-4">
<span class="text-3xl">{{ $red->reward->icon }}</span>
<div>
<div class="font-semibold text-slate-800">{{ $red->user->name }} möchte: {{ $red->reward->name }}</div>
<div class="text-sm text-slate-500">🪙 {{ $red->points_spent }} Münzen · {{ $red->created_at->diffForHumans() }}</div>
</div>
</div>
<div class="flex gap-2 shrink-0">
<form method="POST" action="{{ route('admin.redemptions.approve',$red) }}">
@csrf @method('PATCH')
<button class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium"> Freigeben</button>
</form>
<form method="POST" action="{{ route('admin.redemptions.reject',$red) }}" x-data x-on:submit.prevent="if(confirm('Ablehnen?')) $el.submit()">
@csrf @method('PATCH')
<button class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg text-sm font-medium"> Ablehnen</button>
</form>
</div>
</div>
@endforeach
</div>
@else
<div class="bg-green-50 border border-green-200 rounded-xl p-5 mb-8 text-green-700 font-medium"> Keine offenen Einlösungen.</div>
@endif
<h2 class="font-semibold text-slate-800 mb-4">📋 Verlauf</h2>
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-slate-50 border-b border-slate-200">
<tr>
<th class="text-left px-4 py-3 font-medium text-slate-600">Kind</th>
<th class="text-left px-4 py-3 font-medium text-slate-600">Belohnung</th>
<th class="text-center px-4 py-3 font-medium text-slate-600">Status</th>
<th class="text-right px-4 py-3 font-medium text-slate-600">Münzen</th>
<th class="text-right px-4 py-3 font-medium text-slate-600">Datum</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
@forelse($resolved as $red)
<tr>
<td class="px-4 py-3 font-medium text-slate-700">{{ $red->user->name }}</td>
<td class="px-4 py-3">{{ $red->reward->icon }} {{ $red->reward->name }}</td>
<td class="px-4 py-3 text-center">
<span class="{{ $red->status==='approved' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700' }} px-2 py-0.5 rounded-full text-xs font-medium">
{{ $red->status==='approved' ? 'Freigegeben' : 'Abgelehnt' }}
</span>
</td>
<td class="px-4 py-3 text-right text-amber-600 font-medium">🪙 {{ $red->points_spent }}</td>
<td class="px-4 py-3 text-right text-slate-400 text-xs">{{ $red->created_at->format('d.m.Y') }}</td>
</tr>
@empty
<tr><td colspan="5" class="px-4 py-6 text-center text-slate-400">Noch keine Einlösungen.</td></tr>
@endforelse
</tbody>
</table>
</div>
@endsection
@@ -0,0 +1,38 @@
@extends('layouts.admin')
@section('title','Neue Belohnung')
@section('content')
<div class="max-w-lg">
<a href="{{ route('admin.rewards.index') }}" class="text-sm text-slate-500 hover:text-slate-700 mb-4 inline-block"> Zurück</a>
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h2 class="font-semibold text-slate-800 mb-5">Neue Belohnung</h2>
<form method="POST" action="{{ route('admin.rewards.store') }}" class="space-y-4">
@csrf
<div class="grid grid-cols-4 gap-3">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Icon</label>
<input name="icon" value="{{ old('icon','🎁') }}" required maxlength="4" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-center text-xl focus:ring-2 focus:ring-violet-500 outline-none">
</div>
<div class="col-span-3">
<label class="block text-sm font-medium text-slate-700 mb-1">Name</label>
<input name="name" value="{{ old('name') }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Beschreibung</label>
<input name="description" value="{{ old('description') }}" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">🪙 Münzenpreis</label>
<input name="points_cost" type="number" min="1" value="{{ old('points_cost') }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Minuten <span class="text-slate-400">(optional)</span></label>
<input name="minutes" type="number" min="1" value="{{ old('minutes') }}" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
</div>
</div>
<button type="submit" class="w-full bg-violet-600 hover:bg-violet-700 text-white py-2 rounded-lg font-medium text-sm">Belohnung erstellen</button>
</form>
</div>
</div>
@endsection
@@ -0,0 +1,42 @@
@extends('layouts.admin')
@section('title','Belohnung bearbeiten')
@section('content')
<div class="max-w-lg">
<a href="{{ route('admin.rewards.index') }}" class="text-sm text-slate-500 hover:text-slate-700 mb-4 inline-block"> Zurück</a>
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h2 class="font-semibold text-slate-800 mb-5">Belohnung bearbeiten</h2>
<form method="POST" action="{{ route('admin.rewards.update',$reward) }}" class="space-y-4">
@csrf @method('PUT')
<div class="grid grid-cols-4 gap-3">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Icon</label>
<input name="icon" value="{{ old('icon',$reward->icon) }}" required maxlength="4" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-center text-xl focus:ring-2 focus:ring-violet-500 outline-none">
</div>
<div class="col-span-3">
<label class="block text-sm font-medium text-slate-700 mb-1">Name</label>
<input name="name" value="{{ old('name',$reward->name) }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Beschreibung</label>
<input name="description" value="{{ old('description',$reward->description) }}" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">🪙 Münzenpreis</label>
<input name="points_cost" type="number" min="1" value="{{ old('points_cost',$reward->points_cost) }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Minuten</label>
<input name="minutes" type="number" min="1" value="{{ old('minutes',$reward->minutes) }}" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
</div>
</div>
<div class="flex items-center gap-3">
<input type="checkbox" name="active" id="active" value="1" {{ $reward->active?'checked':'' }} class="accent-violet-600">
<label for="active" class="text-sm font-medium text-slate-700">Belohnung aktiv</label>
</div>
<button type="submit" class="w-full bg-violet-600 hover:bg-violet-700 text-white py-2 rounded-lg font-medium text-sm">Speichern</button>
</form>
</div>
</div>
@endsection
@@ -0,0 +1,33 @@
@extends('layouts.admin')
@section('title','Belohnungen')
@section('content')
<div class="flex justify-between items-center mb-6">
<h2 class="text-lg font-semibold text-slate-700">Belohnungen</h2>
<a href="{{ route('admin.rewards.create') }}" class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg text-sm font-medium">+ Neue Belohnung</a>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
@forelse($rewards as $r)
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
<div class="flex items-start justify-between mb-3">
<span class="text-3xl">{{ $r->icon }}</span>
<span class="{{ $r->active ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500' }} text-xs px-2 py-0.5 rounded-full font-medium">{{ $r->active ? 'Aktiv' : 'Inaktiv' }}</span>
</div>
<div class="font-semibold text-slate-800">{{ $r->name }}</div>
@if($r->description)<div class="text-xs text-slate-500 mt-0.5">{{ $r->description }}</div>@endif
<div class="mt-3 flex items-center justify-between">
<span class="font-bold text-amber-600">🪙 {{ $r->points_cost }} Münzen</span>
@if($r->minutes)<span class="text-xs text-slate-500">{{ $r->minutes }} Min.</span>@endif
</div>
<div class="mt-4 flex gap-2">
<a href="{{ route('admin.rewards.edit',$r) }}" class="flex-1 text-center bg-slate-100 hover:bg-slate-200 text-slate-700 py-1.5 rounded-lg text-xs font-medium">Bearbeiten</a>
<form method="POST" action="{{ route('admin.rewards.destroy',$r) }}" onsubmit="return confirm('Löschen?')">
@csrf @method('DELETE')
<button class="bg-red-50 hover:bg-red-100 text-red-600 py-1.5 px-3 rounded-lg text-xs font-medium">Löschen</button>
</form>
</div>
</div>
@empty
<p class="text-slate-400 col-span-3">Noch keine Belohnungen.</p>
@endforelse
</div>
@endsection
@@ -0,0 +1,29 @@
@extends('layouts.admin')
@section('title','Neues Kind')
@section('content')
<div class="max-w-lg">
<a href="{{ route('admin.users.index') }}" class="text-sm text-slate-500 hover:text-slate-700 mb-4 inline-block"> Zurück</a>
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h2 class="font-semibold text-slate-800 mb-5">Neues Kind-Konto</h2>
<form method="POST" action="{{ route('admin.users.store') }}" class="space-y-4">
@csrf
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Name</label>
<input name="name" value="{{ old('name') }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 focus:border-transparent outline-none">
@error('name')<p class="text-red-500 text-xs mt-1">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">E-Mail</label>
<input name="email" type="email" value="{{ old('email') }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 focus:border-transparent outline-none">
@error('email')<p class="text-red-500 text-xs mt-1">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Passwort</label>
<input name="password" type="password" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 focus:border-transparent outline-none">
@error('password')<p class="text-red-500 text-xs mt-1">{{ $message }}</p>@enderror
</div>
<button type="submit" class="w-full bg-violet-600 hover:bg-violet-700 text-white py-2 rounded-lg font-medium text-sm">Konto erstellen</button>
</form>
</div>
</div>
@endsection
@@ -0,0 +1,30 @@
@extends('layouts.admin')
@section('title','Kind bearbeiten')
@section('content')
<div class="max-w-lg">
<a href="{{ route('admin.users.index') }}" class="text-sm text-slate-500 hover:text-slate-700 mb-4 inline-block"> Zurück</a>
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h2 class="font-semibold text-slate-800 mb-5">{{ $user->name }} bearbeiten</h2>
<form method="POST" action="{{ route('admin.users.update',$user) }}" class="space-y-4">
@csrf @method('PUT')
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Name</label>
<input name="name" value="{{ old('name',$user->name) }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">E-Mail</label>
<input name="email" type="email" value="{{ old('email',$user->email) }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Neues Passwort <span class="text-slate-400">(leer = unverändert)</span></label>
<input name="password" type="password" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">🪙 Münzen</label>
<input name="points" type="number" min="0" value="{{ old('points',$user->points) }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
</div>
<button type="submit" class="w-full bg-violet-600 hover:bg-violet-700 text-white py-2 rounded-lg font-medium text-sm">Speichern</button>
</form>
</div>
</div>
@endsection
@@ -0,0 +1,40 @@
@extends('layouts.admin')
@section('title','Kinder')
@section('content')
<div class="flex justify-between items-center mb-6">
<h2 class="text-lg font-semibold text-slate-700">Alle Kinder-Konten</h2>
<a href="{{ route('admin.users.create') }}" class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg text-sm font-medium">+ Neues Kind</a>
</div>
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-slate-50 border-b border-slate-200">
<tr>
<th class="text-left px-4 py-3 font-medium text-slate-600">Name</th>
<th class="text-left px-4 py-3 font-medium text-slate-600">E-Mail</th>
<th class="text-right px-4 py-3 font-medium text-slate-600">🪙 Münzen</th>
<th class="text-right px-4 py-3 font-medium text-slate-600">Antworten</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
@forelse($users as $u)
<tr class="hover:bg-slate-50">
<td class="px-4 py-3 font-medium text-slate-800">{{ $u->name }}</td>
<td class="px-4 py-3 text-slate-500">{{ $u->email }}</td>
<td class="px-4 py-3 text-right font-bold text-amber-600">{{ $u->points }}</td>
<td class="px-4 py-3 text-right text-slate-500">{{ $u->attempts_count }}</td>
<td class="px-4 py-3 text-right">
<a href="{{ route('admin.users.edit',$u) }}" class="text-violet-600 hover:underline mr-3">Bearbeiten</a>
<form method="POST" action="{{ route('admin.users.destroy',$u) }}" class="inline" onsubmit="return confirm('Wirklich löschen?')">
@csrf @method('DELETE')
<button class="text-red-500 hover:underline">Löschen</button>
</form>
</td>
</tr>
@empty
<tr><td colspan="5" class="px-4 py-8 text-center text-slate-400">Noch keine Kinder angelegt.</td></tr>
@endforelse
</tbody>
</table>
</div>
@endsection
+72
View File
@@ -0,0 +1,72 @@
@extends('layouts.child')
@section('content')
@php $level = auth()->user()->level() @endphp
<div class="text-center mb-8">
<div class="text-5xl mb-2">{{ $level['icon'] }}</div>
<h1 class="text-2xl font-bold text-indigo-700">Hallo, {{ auth()->user()->name }}! 👋</h1>
<p class="text-slate-500 mt-1">Du bist: <span class="{{ $level['color'] }} font-bold">{{ $level['label'] }}</span></p>
@if($streak >= 3)
<div class="mt-3 inline-flex items-center gap-2 bg-orange-100 text-orange-700 rounded-full px-4 py-1.5 text-sm font-bold">
🔥 {{ $streak }}er-Serie! +5 Bonusmünzen pro Antwort
</div>
@elseif($streak > 0)
<div class="mt-3 inline-flex items-center gap-2 bg-sky-100 text-sky-700 rounded-full px-4 py-1.5 text-sm font-medium">
{{ $streak }} richtige Antworten in Folge!
</div>
@endif
</div>
<div class="grid grid-cols-2 gap-4 mb-8">
<div class="bg-white rounded-2xl shadow-sm border border-amber-200 p-5 text-center">
<div class="text-4xl font-black text-amber-500">{{ auth()->user()->points }}</div>
<div class="text-sm text-slate-500 mt-1 font-medium">🪙 Münzen</div>
@if($level['next'])
<div class="mt-3">
<div class="text-xs text-slate-400 mb-1">Nächstes Level in {{ $level['next'] - auth()->user()->points }} Münzen</div>
<div class="bg-slate-200 rounded-full h-2"><div class="bg-amber-400 h-2 rounded-full transition-all" style="width:{{ $level['progress'] }}%"></div></div>
</div>
@endif
</div>
@if($pendingRedemptions > 0)
<a href="{{ route('rewards.index') }}" class="bg-green-50 border border-green-300 rounded-2xl p-5 text-center hover:bg-green-100 transition">
<div class="text-4xl font-black text-green-600">{{ $pendingRedemptions }}</div>
<div class="text-sm text-slate-500 mt-1 font-medium">🎁 Einlösung wartet</div>
</a>
@else
<a href="{{ route('rewards.index') }}" class="bg-white rounded-2xl shadow-sm border border-slate-200 p-5 text-center hover:border-indigo-300 transition">
<div class="text-4xl">🎁</div>
<div class="text-sm text-slate-500 mt-1 font-medium">Belohnungen</div>
</a>
@endif
</div>
<h2 class="font-bold text-slate-700 text-lg mb-4">📚 Fächer</h2>
<div class="grid grid-cols-3 gap-4 mb-8">
@foreach($subjects as $s)
@php $colors = ['green'=>'from-green-400 to-emerald-500','blue'=>'from-blue-400 to-indigo-500','orange'=>'from-orange-400 to-amber-500'] @endphp
<a href="{{ route('learn.quiz', $s->slug) }}" class="bg-gradient-to-br {{ $colors[$s->color] ?? 'from-violet-400 to-purple-500' }} rounded-2xl p-5 text-white text-center hover:scale-105 transition-transform shadow-sm">
<div class="text-3xl mb-2">{{ $s->icon }}</div>
<div class="font-bold text-sm">{{ $s->name }}</div>
<div class="text-white/80 text-xs mt-1">{{ $s->correct }} </div>
</a>
@endforeach
</div>
@if($recentAttempts->count())
<h2 class="font-bold text-slate-700 text-lg mb-4">⏱️ Zuletzt gespielt</h2>
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
@foreach($recentAttempts as $a)
<div class="flex items-center justify-between px-5 py-3 {{ !$loop->last ? 'border-b border-slate-100' : '' }}">
<div class="flex items-center gap-3">
<span class="text-lg">{{ $a->is_correct ? '✅' : '❌' }}</span>
<div>
<div class="text-sm font-medium text-slate-700 line-clamp-1">{{ Str::limit($a->question->question_text, 40) }}</div>
<div class="text-xs text-slate-400">{{ $a->question->subject->name }}</div>
</div>
</div>
@if($a->points_earned > 0)<span class="text-xs font-bold text-amber-500">+{{ $a->points_earned }} 🪙</span>@endif
</div>
@endforeach
</div>
@endif
@endsection
@@ -0,0 +1,34 @@
@extends('layouts.child')
@section('content')
<div class="mb-4 flex items-center gap-3">
<a href="{{ route('learn.subjects') }}" class="text-slate-400 hover:text-slate-600 text-sm"> Fächer</a>
<span class="text-lg">{{ $subject->icon }}</span>
<span class="font-bold text-slate-700">{{ $subject->name }}</span>
<span class="ml-auto text-sm text-slate-400">{{ $question->difficultyStars() }}</span>
</div>
<div class="bg-white rounded-2xl shadow-md border border-slate-200 p-6 mb-6">
<div class="text-xs text-slate-400 mb-3 font-medium">
{{ ['','⭐ Leicht — 5 Münzen','⭐⭐ Mittel — 10 Münzen','⭐⭐⭐ Schwer — 20 Münzen'][$question->difficulty] }}
</div>
<p class="text-xl font-bold text-slate-800 leading-snug">{{ $question->question_text }}</p>
</div>
<form method="POST" action="{{ route('learn.answer', $subject->slug) }}">
@csrf
<input type="hidden" name="question_id" value="{{ $question->id }}">
<div class="grid gap-3">
@foreach($question->answerOptions as $opt)
<label class="cursor-pointer">
<input type="radio" name="answer" value="{{ $opt->id }}" class="sr-only peer" required>
<div class="bg-white border-2 border-slate-200 rounded-2xl px-5 py-4 text-base font-medium text-slate-700
peer-checked:border-indigo-500 peer-checked:bg-indigo-50 peer-checked:text-indigo-700
hover:border-indigo-300 hover:bg-slate-50 transition-all">
{{ $opt->text }}
</div>
</label>
@endforeach
</div>
<button type="submit" class="mt-6 w-full bg-indigo-600 hover:bg-indigo-700 text-white text-lg font-bold py-4 rounded-2xl shadow-md transition-colors">
Antworten
</button>
</form>
@endsection
@@ -0,0 +1,41 @@
@extends('layouts.child')
@section('content')
<div class="text-center">
@if($isRight)
<div class="text-7xl mb-4 animate-bounce">🎉</div>
<h1 class="text-3xl font-black text-green-600 mb-2">Richtig!</h1>
<p class="text-slate-600 mb-4">Super gemacht!</p>
<div class="inline-flex items-center gap-2 bg-amber-100 text-amber-700 font-black text-2xl rounded-2xl px-6 py-3 mb-4">
+{{ $earned }} 🪙
</div>
@if($earned > $question->points_value)
<p class="text-orange-600 font-bold text-sm mb-4">🔥 Serien-Bonus inklusive!</p>
@endif
@if($newStreak >= 3)
<div class="bg-orange-50 border border-orange-200 rounded-xl px-4 py-2 text-orange-700 font-bold text-sm mb-4 inline-block">
🔥 {{ $newStreak }}er-Serie! Weiter so!
</div>
@endif
@else
<div class="text-7xl mb-4">😅</div>
<h1 class="text-3xl font-black text-red-500 mb-2">Leider falsch</h1>
<p class="text-slate-600 mb-4">Die richtige Antwort war:</p>
<div class="bg-green-100 border border-green-300 text-green-800 font-bold text-lg rounded-2xl px-6 py-3 mb-4 inline-block">
{{ $correct->text }}
</div>
@endif
<div class="mt-6 flex gap-3 justify-center">
<a href="{{ route('learn.quiz', $subject->slug) }}" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold px-8 py-3 rounded-2xl text-base shadow-md transition-colors">
Nächste Frage
</a>
<a href="{{ route('learn.subjects') }}" class="bg-slate-200 hover:bg-slate-300 text-slate-700 font-bold px-6 py-3 rounded-2xl text-base transition-colors">
Fächer
</a>
</div>
<div class="mt-6 text-slate-500 text-sm">
Dein Kontostand: <span class="font-bold text-amber-600">🪙 {{ auth()->user()->fresh()->points }}</span>
</div>
</div>
@endsection
@@ -0,0 +1,19 @@
@extends('layouts.child')
@section('content')
<h1 class="text-2xl font-bold text-indigo-700 mb-6 text-center">📚 Welches Fach?</h1>
<div class="grid gap-5">
@foreach($subjects as $s)
@php $colors = ['green'=>'from-green-400 to-emerald-500','blue'=>'from-blue-400 to-indigo-500','orange'=>'from-orange-400 to-amber-500'] @endphp
<a href="{{ route('learn.quiz', $s->slug) }}" class="bg-gradient-to-r {{ $colors[$s->color] ?? 'from-violet-400 to-purple-500' }} rounded-2xl p-6 text-white flex items-center justify-between hover:scale-[1.02] transition-transform shadow-md">
<div class="flex items-center gap-5">
<span class="text-5xl">{{ $s->icon }}</span>
<div>
<div class="text-2xl font-black">{{ $s->name }}</div>
<div class="text-white/80 text-sm mt-0.5">{{ $s->total }} Fragen · {{ $s->correct }} richtig beantwortet</div>
</div>
</div>
<div class="text-4xl"></div>
</a>
@endforeach
</div>
@endsection
@@ -0,0 +1,60 @@
@extends('layouts.child')
@section('content')
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-indigo-700">🎁 Belohnungen</h1>
<div class="bg-amber-100 text-amber-700 font-black rounded-full px-4 py-2">🪙 {{ auth()->user()->points }}</div>
</div>
<div class="grid gap-4 mb-8">
@foreach($rewards as $r)
@php $canAfford = auth()->user()->points >= $r->points_cost @endphp
<div class="bg-white rounded-2xl shadow-sm border {{ $canAfford ? 'border-slate-200 hover:border-indigo-300' : 'border-slate-100 opacity-60' }} p-5 transition-all">
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-4">
<span class="text-4xl">{{ $r->icon }}</span>
<div>
<div class="font-bold text-slate-800 text-base">{{ $r->name }}</div>
@if($r->description)<div class="text-sm text-slate-500">{{ $r->description }}</div>@endif
@if($r->minutes)<div class="text-xs text-slate-400 mt-0.5"> {{ $r->minutes }} Minuten</div>@endif
</div>
</div>
<div class="text-right shrink-0">
<div class="font-black text-amber-600 text-lg">🪙 {{ $r->points_cost }}</div>
@if($canAfford)
<form method="POST" action="{{ route('rewards.redeem', $r) }}" onsubmit="return confirm('Möchtest du {{ $r->name }} für {{ $r->points_cost }} Münzen einlösen?')">
@csrf
<button class="mt-2 bg-indigo-600 hover:bg-indigo-700 text-white font-bold px-4 py-2 rounded-xl text-sm transition-colors">
Einlösen
</button>
</form>
@else
<div class="mt-2 text-xs text-slate-400 font-medium">Noch {{ $r->points_cost - auth()->user()->points }} fehlen</div>
@endif
</div>
</div>
</div>
@endforeach
</div>
@if($history->count())
<h2 class="font-bold text-slate-700 text-lg mb-4">📋 Meine Einlösungen</h2>
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
@foreach($history as $red)
<div class="flex items-center justify-between px-5 py-3 {{ !$loop->last ? 'border-b border-slate-100' : '' }}">
<div class="flex items-center gap-3">
<span class="text-xl">{{ $red->reward->icon }}</span>
<div>
<div class="text-sm font-medium text-slate-700">{{ $red->reward->name }}</div>
<div class="text-xs text-slate-400">{{ $red->created_at->format('d.m.Y') }}</div>
</div>
</div>
<span class="text-xs font-bold px-2.5 py-1 rounded-full
{{ $red->status==='approved' ? 'bg-green-100 text-green-700' :
($red->status==='rejected' ? 'bg-red-100 text-red-600' : 'bg-amber-100 text-amber-700') }}">
{{ $red->status==='approved' ? '✅ Freigegeben' : ($red->status==='rejected' ? '❌ Abgelehnt' : '⏳ Warten...') }}
</span>
</div>
@endforeach
</div>
@endif
@endsection
+58
View File
@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="de" class="h-full">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Lernapp Admin</title>
@vite(['resources/css/app.css','resources/js/app.js'])
</head>
<body class="h-full bg-slate-100 font-sans" x-data="{open:false}">
<div class="flex h-full">
<!-- Sidebar -->
<aside class="hidden md:flex flex-col w-60 bg-slate-900 text-slate-100 shrink-0">
<div class="flex items-center gap-3 px-6 py-5 border-b border-slate-700">
<span class="text-2xl">🎓</span>
<div><div class="font-bold text-white text-sm">Lernapp</div><div class="text-xs text-slate-400">Admin-Bereich</div></div>
</div>
<nav class="flex-1 px-3 py-4 space-y-1 text-sm">
<a href="{{ route('admin.dashboard') }}" class="flex items-center gap-3 px-3 py-2 rounded-lg {{ request()->routeIs('admin.dashboard') ? 'bg-violet-600 text-white' : 'text-slate-300 hover:bg-slate-800' }}">
<span>📊</span> Dashboard
</a>
<a href="{{ route('admin.users.index') }}" class="flex items-center gap-3 px-3 py-2 rounded-lg {{ request()->routeIs('admin.users.*') ? 'bg-violet-600 text-white' : 'text-slate-300 hover:bg-slate-800' }}">
<span>👦</span> Kinder
</a>
<a href="{{ route('admin.questions.index') }}" class="flex items-center gap-3 px-3 py-2 rounded-lg {{ request()->routeIs('admin.questions.*') ? 'bg-violet-600 text-white' : 'text-slate-300 hover:bg-slate-800' }}">
<span></span> Fragen
</a>
<a href="{{ route('admin.rewards.index') }}" class="flex items-center gap-3 px-3 py-2 rounded-lg {{ request()->routeIs('admin.rewards.*') ? 'bg-violet-600 text-white' : 'text-slate-300 hover:bg-slate-800' }}">
<span>🎁</span> Belohnungen
</a>
<a href="{{ route('admin.redemptions.index') }}" class="flex items-center gap-3 px-3 py-2 rounded-lg {{ request()->routeIs('admin.redemptions.*') ? 'bg-violet-600 text-white' : 'text-slate-300 hover:bg-slate-800' }}">
<span></span> Einlösungen
@php $p = \App\Models\RewardRedemption::where('status','pending')->count() @endphp
@if($p > 0)<span class="ml-auto bg-red-500 text-white text-xs rounded-full px-1.5">{{ $p }}</span>@endif
</a>
</nav>
<div class="px-3 py-4 border-t border-slate-700">
<form method="POST" action="{{ route('logout') }}">
@csrf
<button class="flex items-center gap-3 px-3 py-2 w-full text-sm text-slate-400 hover:text-white rounded-lg hover:bg-slate-800">
<span>🚪</span> Abmelden
</button>
</form>
</div>
</aside>
<!-- Main -->
<div class="flex-1 flex flex-col overflow-hidden">
<header class="bg-white border-b border-slate-200 px-6 py-4 flex items-center justify-between">
<h1 class="font-semibold text-slate-800">@yield('title','Dashboard')</h1>
<span class="text-sm text-slate-500">{{ auth()->user()->name }}</span>
</header>
<main class="flex-1 overflow-y-auto p-6">
@if(session('success'))<div class="mb-4 bg-green-100 border border-green-300 text-green-800 rounded-lg px-4 py-3 text-sm">{{ session('success') }}</div>@endif
@if(session('error'))<div class="mb-4 bg-red-100 border border-red-300 text-red-800 rounded-lg px-4 py-3 text-sm">{{ session('error') }}</div>@endif
@yield('content')
</main>
</div>
</div>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Lernapp</title>
@vite(['resources/css/app.css','resources/js/app.js'])
</head>
<body class="min-h-screen bg-gradient-to-br from-sky-100 to-indigo-100 font-sans">
<nav class="bg-white shadow-sm sticky top-0 z-10">
<div class="max-w-3xl mx-auto px-4 py-3 flex items-center justify-between">
<a href="{{ route('dashboard') }}" class="flex items-center gap-2 font-bold text-indigo-600 text-lg">
🎓 Lernapp
</a>
<div class="flex items-center gap-4">
<a href="{{ route('learn.subjects') }}" class="text-sm font-medium text-slate-600 hover:text-indigo-600">Lernen</a>
<a href="{{ route('rewards.index') }}" class="text-sm font-medium text-slate-600 hover:text-indigo-600">🪙 Belohnungen</a>
<div class="flex items-center gap-1 bg-amber-100 text-amber-700 font-bold rounded-full px-3 py-1 text-sm">
🪙 {{ auth()->user()->points }}
</div>
<form method="POST" action="{{ route('logout') }}">@csrf
<button class="text-xs text-slate-400 hover:text-slate-600">Abmelden</button>
</form>
</div>
</div>
</nav>
<main class="max-w-3xl mx-auto px-4 py-8">
@if(session('success'))<div class="mb-4 bg-green-100 border border-green-300 text-green-800 rounded-xl px-4 py-3 text-sm font-medium"> {{ session('success') }}</div>@endif
@if(session('error'))<div class="mb-4 bg-red-100 border border-red-300 text-red-800 rounded-xl px-4 py-3 text-sm font-medium"> {{ session('error') }}</div>@endif
@if(session('info'))<div class="mb-4 bg-blue-100 border border-blue-300 text-blue-800 rounded-xl px-4 py-3 text-sm font-medium"> {{ session('info') }}</div>@endif
@yield('content')
</main>
</body>
</html>
+27 -12
View File
@@ -1,20 +1,35 @@
<?php <?php
use App\Http\Controllers\Admin;
use App\Http\Controllers\ProfileController; use App\Http\Controllers\Child;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/', function () { Route::get('/', fn() => redirect()->route('dashboard'));
return view('welcome');
// Role-aware dashboard redirect
Route::middleware(['auth','verified'])->get('/dashboard', function () {
return auth()->user()->isAdmin()
? redirect()->route('admin.dashboard')
: app(\App\Http\Controllers\Child\DashboardController::class)->index();
})->name('dashboard');
// ── ADMIN ──────────────────────────────────────────────────
Route::middleware(['auth','admin'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/', [Admin\DashboardController::class, 'index'])->name('dashboard');
Route::resource('users', Admin\UserController::class);
Route::resource('questions',Admin\QuestionController::class);
Route::resource('rewards', Admin\RewardController::class)->except('show');
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');
}); });
Route::get('/dashboard', function () { // ── CHILD ──────────────────────────────────────────────────
return view('dashboard'); Route::middleware(['auth','child'])->group(function () {
})->middleware(['auth', 'verified'])->name('dashboard'); Route::get ('lernen', [\App\Http\Controllers\Child\LearnController::class,'subjects'])->name('learn.subjects');
Route::get ('lernen/{subject:slug}', [\App\Http\Controllers\Child\LearnController::class,'quiz']) ->name('learn.quiz');
Route::middleware('auth')->group(function () { Route::post('lernen/{subject:slug}/antwort', [\App\Http\Controllers\Child\LearnController::class,'answer']) ->name('learn.answer');
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::get ('belohnungen', [\App\Http\Controllers\Child\RewardController::class,'index']) ->name('rewards.index');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::post('belohnungen/{reward}/einloesen', [\App\Http\Controllers\Child\RewardController::class,'redeem']) ->name('rewards.redeem');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
}); });
require __DIR__.'/auth.php'; require __DIR__.'/auth.php';