feat: Add user level and experience attributes, and implement reward, task, and quest models with corresponding controllers and migrations

This commit is contained in:
Jonas Pfalzgraf 2024-12-16 09:00:57 +01:00
parent aa50740dcb
commit fd20278948
17 changed files with 963 additions and 112 deletions

View file

@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
public function register(Request $request)
{
$request->validate([
'name' => 'required',
'email' => 'required|email|unique:users',
'password' => 'required|min:6'
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'level' => 1,
'xp' => 0
]);
$token = $user->createToken('api_token')->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token
], 201);
}
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required'
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->json(['message' => 'Invalid credentials'], 401);
}
$token = $user->createToken('api_token')->plainTextToken;
return response()->json([
'token' => $token,
'user' => $user
]);
}
public function me(Request $request)
{
return response()->json(['user' => $request->user()]);
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers;
use App\Models\Quest;
use App\Models\Task;
use Illuminate\Http\Request;
class QuestController extends Controller
{
public function index(Request $request)
{
$quests = Quest::where('user_id', $request->user()->id)->where('is_completed', false)->get();
return response()->json($quests);
}
public function generate(Request $request)
{
// Vereinfachte Logik: Nimmt die letzten 5 offenen Tasks und macht daraus Quests
$tasks = Task::where('user_id', $request->user()->id)->limit(5)->get();
$quests = [];
foreach ($tasks as $task) {
$quests[] = Quest::create([
'user_id' => $request->user()->id,
'task_id' => $task->id,
'title' => $task->title,
'category' => $task->category,
'due_date' => $task->due_date,
'xp_reward' => 50,
'is_completed' => false
]);
}
return response()->json($quests, 201);
}
public function complete(Request $request, $id)
{
$quest = Quest::where('user_id', $request->user()->id)->findOrFail($id);
if ($quest->is_completed) {
return response()->json(['message' => 'Quest already completed'], 400);
}
// Mark as completed
$quest->update(['is_completed' => true]);
$user = $request->user();
$user->xp += $quest->xp_reward;
// Level-Up Check (vereinfachte Logik)
$xpForNextLevel = $user->level * 100;
$levelUp = false;
while ($user->xp >= $xpForNextLevel) {
$user->level += 1;
$user->xp = $user->xp - $xpForNextLevel;
$xpForNextLevel = $user->level * 100;
$levelUp = true;
}
$user->save();
return response()->json([
'quest' => $quest,
'xp_gained' => $quest->xp_reward,
'new_level' => $user->level,
'level_up' => $levelUp
]);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers;
use App\Models\Reward;
use Illuminate\Http\Request;
class RewardController extends Controller
{
public function index(Request $request)
{
$rewards = Reward::all();
return response()->json($rewards);
}
public function unlocked(Request $request)
{
$user = $request->user();
$unlocked = $user->belongsToMany(Reward::class, 'reward_user')->get();
return response()->json($unlocked);
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use Illuminate\Http\Request;
class TaskController extends Controller
{
public function index(Request $request)
{
$tasks = Task::where('user_id', $request->user()->id)->get();
return response()->json($tasks);
}
public function store(Request $request)
{
$request->validate([
'title' => 'required',
'category' => 'in:daily,weekend,vacation,work',
'priority' => 'in:low,medium,high'
]);
$task = Task::create([
'user_id' => $request->user()->id,
'title' => $request->title,
'description' => $request->description,
'category' => $request->category ?? 'daily',
'priority' => $request->priority ?? 'medium',
'due_date' => $request->due_date
]);
return response()->json($task, 201);
}
public function show(Request $request, $id)
{
$task = Task::where('user_id', $request->user()->id)->findOrFail($id);
return response()->json($task);
}
public function update(Request $request, $id)
{
$task = Task::where('user_id', $request->user()->id)->findOrFail($id);
$task->update($request->only('title', 'description', 'category', 'priority', 'due_date'));
return response()->json($task);
}
public function destroy(Request $request, $id)
{
$task = Task::where('user_id', $request->user()->id)->findOrFail($id);
$task->delete();
return response()->noContent();
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers;
use App\Models\Reward;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function status(Request $request)
{
$user = $request->user();
$xpForNextLevel = $user->level * 100;
// Finde noch nicht freigeschaltete Rewards, die in Reichweite sind
$upcomingRewards = Reward::where('unlocked_at_level', '>', $user->level)->limit(5)->get();
return response()->json([
'level' => $user->level,
'xp' => $user->xp,
'xp_for_next_level' => $xpForNextLevel,
'upcoming_rewards' => $upcomingRewards
]);
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Quest extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'task_id',
'title',
'category',
'due_date',
'xp_reward',
'is_completed'
];
public function user()
{
return $this->belongsTo(User::class);
}
public function task()
{
return $this->belongsTo(Task::class);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Reward extends Model
{
use HasFactory;
protected $fillable = [
'name',
'description',
'unlocked_at_level'
];
public function users()
{
return $this->belongsToMany(User::class, 'reward_user');
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'title',
'description',
'category',
'priority',
'due_date'
];
public function user()
{
return $this->belongsTo(User::class);
}
}

View file

@ -20,6 +20,8 @@ class User extends Authenticatable
'name',
'email',
'password',
'level',
'xp'
];
/**

View file

@ -13,7 +13,7 @@ return [
|
*/
'name' => env('APP_NAME', 'Laravel'),
'name' => env('APP_NAME', 'ADHD Home Quest'),
/*
|--------------------------------------------------------------------------
@ -26,7 +26,7 @@ return [
|
*/
'env' => env('APP_ENV', 'production'),
'env' => env('APP_ENV', 'development'),
/*
|--------------------------------------------------------------------------
@ -39,7 +39,7 @@ return [
|
*/
'debug' => (bool) env('APP_DEBUG', false),
'debug' => (bool) env('APP_DEBUG', true),
/*
|--------------------------------------------------------------------------
@ -65,7 +65,7 @@ return [
|
*/
'timezone' => env('APP_TIMEZONE', 'UTC'),
'timezone' => env('APP_TIMEZONE', 'CET'),
/*
|--------------------------------------------------------------------------

View file

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->integer('level')->default(1);
$table->integer('xp')->default(0);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
}
};

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->string('title');
$table->text('description')->nullable();
$table->enum('category', ['daily', 'weekend', 'vacation', 'work'])->default('daily');
$table->enum('priority', ['low', 'medium', 'high'])->default('medium');
$table->dateTime('due_date')->nullable();
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('tasks');
}
};

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up()
{
Schema::create('quests', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->unsignedBigInteger('task_id');
$table->string('title');
$table->string('category');
$table->dateTime('due_date')->nullable();
$table->integer('xp_reward')->default(50);
$table->boolean('is_completed')->default(false);
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('task_id')->references('id')->on('tasks')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('quests');
}
};

View file

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up()
{
Schema::create('rewards', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description')->nullable();
$table->integer('unlocked_at_level')->default(2);
$table->timestamps();
});
// Optionale Pivot-Tabelle für freigeschaltete Rewards
Schema::create('reward_user', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->unsignedBigInteger('reward_id');
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('reward_id')->references('id')->on('rewards')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('rewards');
Schema::dropIfExists('reward_user');
}
};

View file

@ -55,111 +55,7 @@
</header>
<main class="mt-6">
<div class="grid gap-6 lg:grid-cols-2 lg:gap-8">
<a
href="https://laravel.com/docs"
id="docs-card"
class="flex flex-col items-start gap-6 overflow-hidden rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] md:row-span-3 lg:p-10 lg:pb-10 dark:bg-zinc-900 dark:ring-zinc-800 dark:hover:text-white/70 dark:hover:ring-zinc-700 dark:focus-visible:ring-[#FF2D20]"
>
<div id="screenshot-container" class="relative flex w-full flex-1 items-stretch">
<img
src="https://laravel.com/assets/img/welcome/docs-light.svg"
alt="Laravel documentation screenshot"
class="aspect-video h-full w-full flex-1 rounded-[10px] object-top object-cover drop-shadow-[0px_4px_34px_rgba(0,0,0,0.06)] dark:hidden"
onerror="
document.getElementById('screenshot-container').classList.add('!hidden');
document.getElementById('docs-card').classList.add('!row-span-1');
document.getElementById('docs-card-content').classList.add('!flex-row');
document.getElementById('background').classList.add('!hidden');
"
/>
<img
src="https://laravel.com/assets/img/welcome/docs-dark.svg"
alt="Laravel documentation screenshot"
class="hidden aspect-video h-full w-full flex-1 rounded-[10px] object-top object-cover drop-shadow-[0px_4px_34px_rgba(0,0,0,0.25)] dark:block"
/>
<div
class="absolute -bottom-16 -left-16 h-40 w-[calc(100%+8rem)] bg-gradient-to-b from-transparent via-white to-white dark:via-zinc-900 dark:to-zinc-900"
></div>
</div>
<div class="relative flex items-center gap-6 lg:items-end">
<div id="docs-card-content" class="flex items-start gap-6 lg:flex-col">
<div class="flex size-12 shrink-0 items-center justify-center rounded-full bg-[#FF2D20]/10 sm:size-16">
<svg class="size-5 sm:size-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#FF2D20" d="M23 4a1 1 0 0 0-1.447-.894L12.224 7.77a.5.5 0 0 1-.448 0L2.447 3.106A1 1 0 0 0 1 4v13.382a1.99 1.99 0 0 0 1.105 1.79l9.448 4.728c.14.065.293.1.447.1.154-.005.306-.04.447-.105l9.453-4.724a1.99 1.99 0 0 0 1.1-1.789V4ZM3 6.023a.25.25 0 0 1 .362-.223l7.5 3.75a.251.251 0 0 1 .138.223v11.2a.25.25 0 0 1-.362.224l-7.5-3.75a.25.25 0 0 1-.138-.22V6.023Zm18 11.2a.25.25 0 0 1-.138.224l-7.5 3.75a.249.249 0 0 1-.329-.099.249.249 0 0 1-.033-.12V9.772a.251.251 0 0 1 .138-.224l7.5-3.75a.25.25 0 0 1 .362.224v11.2Z"/><path fill="#FF2D20" d="m3.55 1.893 8 4.048a1.008 1.008 0 0 0 .9 0l8-4.048a1 1 0 0 0-.9-1.785l-7.322 3.706a.506.506 0 0 1-.452 0L4.454.108a1 1 0 0 0-.9 1.785H3.55Z"/></svg>
</div>
<div class="pt-3 sm:pt-5 lg:pt-0">
<h2 class="text-xl font-semibold text-black dark:text-white">Documentation</h2>
<p class="mt-4 text-sm/relaxed">
Laravel has wonderful documentation covering every aspect of the framework. Whether you are a newcomer or have prior experience with Laravel, we recommend reading our documentation from beginning to end.
</p>
</div>
</div>
<svg class="size-6 shrink-0 stroke-[#FF2D20]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75"/></svg>
</div>
</a>
<a
href="https://laracasts.com"
class="flex items-start gap-4 rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] lg:pb-10 dark:bg-zinc-900 dark:ring-zinc-800 dark:hover:text-white/70 dark:hover:ring-zinc-700 dark:focus-visible:ring-[#FF2D20]"
>
<div class="flex size-12 shrink-0 items-center justify-center rounded-full bg-[#FF2D20]/10 sm:size-16">
<svg class="size-5 sm:size-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><g fill="#FF2D20"><path d="M24 8.25a.5.5 0 0 0-.5-.5H.5a.5.5 0 0 0-.5.5v12a2.5 2.5 0 0 0 2.5 2.5h19a2.5 2.5 0 0 0 2.5-2.5v-12Zm-7.765 5.868a1.221 1.221 0 0 1 0 2.264l-6.626 2.776A1.153 1.153 0 0 1 8 18.123v-5.746a1.151 1.151 0 0 1 1.609-1.035l6.626 2.776ZM19.564 1.677a.25.25 0 0 0-.177-.427H15.6a.106.106 0 0 0-.072.03l-4.54 4.543a.25.25 0 0 0 .177.427h3.783c.027 0 .054-.01.073-.03l4.543-4.543ZM22.071 1.318a.047.047 0 0 0-.045.013l-4.492 4.492a.249.249 0 0 0 .038.385.25.25 0 0 0 .14.042h5.784a.5.5 0 0 0 .5-.5v-2a2.5 2.5 0 0 0-1.925-2.432ZM13.014 1.677a.25.25 0 0 0-.178-.427H9.101a.106.106 0 0 0-.073.03l-4.54 4.543a.25.25 0 0 0 .177.427H8.4a.106.106 0 0 0 .073-.03l4.54-4.543ZM6.513 1.677a.25.25 0 0 0-.177-.427H2.5A2.5 2.5 0 0 0 0 3.75v2a.5.5 0 0 0 .5.5h1.4a.106.106 0 0 0 .073-.03l4.54-4.543Z"/></g></svg>
</div>
<div class="pt-3 sm:pt-5">
<h2 class="text-xl font-semibold text-black dark:text-white">Laracasts</h2>
<p class="mt-4 text-sm/relaxed">
Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process.
</p>
</div>
<svg class="size-6 shrink-0 self-center stroke-[#FF2D20]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75"/></svg>
</a>
<a
href="https://laravel-news.com"
class="flex items-start gap-4 rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] lg:pb-10 dark:bg-zinc-900 dark:ring-zinc-800 dark:hover:text-white/70 dark:hover:ring-zinc-700 dark:focus-visible:ring-[#FF2D20]"
>
<div class="flex size-12 shrink-0 items-center justify-center rounded-full bg-[#FF2D20]/10 sm:size-16">
<svg class="size-5 sm:size-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><g fill="#FF2D20"><path d="M8.75 4.5H5.5c-.69 0-1.25.56-1.25 1.25v4.75c0 .69.56 1.25 1.25 1.25h3.25c.69 0 1.25-.56 1.25-1.25V5.75c0-.69-.56-1.25-1.25-1.25Z"/><path d="M24 10a3 3 0 0 0-3-3h-2V2.5a2 2 0 0 0-2-2H2a2 2 0 0 0-2 2V20a3.5 3.5 0 0 0 3.5 3.5h17A3.5 3.5 0 0 0 24 20V10ZM3.5 21.5A1.5 1.5 0 0 1 2 20V3a.5.5 0 0 1 .5-.5h14a.5.5 0 0 1 .5.5v17c0 .295.037.588.11.874a.5.5 0 0 1-.484.625L3.5 21.5ZM22 20a1.5 1.5 0 1 1-3 0V9.5a.5.5 0 0 1 .5-.5H21a1 1 0 0 1 1 1v10Z"/><path d="M12.751 6.047h2a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-.75.75h-2A.75.75 0 0 1 12 7.3v-.5a.75.75 0 0 1 .751-.753ZM12.751 10.047h2a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-.75.75h-2A.75.75 0 0 1 12 11.3v-.5a.75.75 0 0 1 .751-.753ZM4.751 14.047h10a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-.75.75h-10A.75.75 0 0 1 4 15.3v-.5a.75.75 0 0 1 .751-.753ZM4.75 18.047h7.5a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-.75.75h-7.5A.75.75 0 0 1 4 19.3v-.5a.75.75 0 0 1 .75-.753Z"/></g></svg>
</div>
<div class="pt-3 sm:pt-5">
<h2 class="text-xl font-semibold text-black dark:text-white">Laravel News</h2>
<p class="mt-4 text-sm/relaxed">
Laravel News is a community driven portal and newsletter aggregating all of the latest and most important news in the Laravel ecosystem, including new package releases and tutorials.
</p>
</div>
<svg class="size-6 shrink-0 self-center stroke-[#FF2D20]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75"/></svg>
</a>
<div class="flex items-start gap-4 rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] lg:pb-10 dark:bg-zinc-900 dark:ring-zinc-800 dark:hover:text-white/70 dark:hover:ring-zinc-700 dark:focus-visible:ring-[#FF2D20]">
<div class="flex size-12 shrink-0 items-center justify-center rounded-full bg-[#FF2D20]/10 sm:size-16">
<svg class="size-5 sm:size-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<g fill="#FF2D20">
<path
d="M16.597 12.635a.247.247 0 0 0-.08-.237 2.234 2.234 0 0 1-.769-1.68c.001-.195.03-.39.084-.578a.25.25 0 0 0-.09-.267 8.8 8.8 0 0 0-4.826-1.66.25.25 0 0 0-.268.181 2.5 2.5 0 0 1-2.4 1.824.045.045 0 0 0-.045.037 12.255 12.255 0 0 0-.093 3.86.251.251 0 0 0 .208.214c2.22.366 4.367 1.08 6.362 2.118a.252.252 0 0 0 .32-.079 10.09 10.09 0 0 0 1.597-3.733ZM13.616 17.968a.25.25 0 0 0-.063-.407A19.697 19.697 0 0 0 8.91 15.98a.25.25 0 0 0-.287.325c.151.455.334.898.548 1.328.437.827.981 1.594 1.619 2.28a.249.249 0 0 0 .32.044 29.13 29.13 0 0 0 2.506-1.99ZM6.303 14.105a.25.25 0 0 0 .265-.274 13.048 13.048 0 0 1 .205-4.045.062.062 0 0 0-.022-.07 2.5 2.5 0 0 1-.777-.982.25.25 0 0 0-.271-.149 11 11 0 0 0-5.6 2.815.255.255 0 0 0-.075.163c-.008.135-.02.27-.02.406.002.8.084 1.598.246 2.381a.25.25 0 0 0 .303.193 19.924 19.924 0 0 1 5.746-.438ZM9.228 20.914a.25.25 0 0 0 .1-.393 11.53 11.53 0 0 1-1.5-2.22 12.238 12.238 0 0 1-.91-2.465.248.248 0 0 0-.22-.187 18.876 18.876 0 0 0-5.69.33.249.249 0 0 0-.179.336c.838 2.142 2.272 4 4.132 5.353a.254.254 0 0 0 .15.048c1.41-.01 2.807-.282 4.117-.802ZM18.93 12.957l-.005-.008a.25.25 0 0 0-.268-.082 2.21 2.21 0 0 1-.41.081.25.25 0 0 0-.217.2c-.582 2.66-2.127 5.35-5.75 7.843a.248.248 0 0 0-.09.299.25.25 0 0 0 .065.091 28.703 28.703 0 0 0 2.662 2.12.246.246 0 0 0 .209.037c2.579-.701 4.85-2.242 6.456-4.378a.25.25 0 0 0 .048-.189 13.51 13.51 0 0 0-2.7-6.014ZM5.702 7.058a.254.254 0 0 0 .2-.165A2.488 2.488 0 0 1 7.98 5.245a.093.093 0 0 0 .078-.062 19.734 19.734 0 0 1 3.055-4.74.25.25 0 0 0-.21-.41 12.009 12.009 0 0 0-10.4 8.558.25.25 0 0 0 .373.281 12.912 12.912 0 0 1 4.826-1.814ZM10.773 22.052a.25.25 0 0 0-.28-.046c-.758.356-1.55.635-2.365.833a.25.25 0 0 0-.022.48c1.252.43 2.568.65 3.893.65.1 0 .2 0 .3-.008a.25.25 0 0 0 .147-.444c-.526-.424-1.1-.917-1.673-1.465ZM18.744 8.436a.249.249 0 0 0 .15.228 2.246 2.246 0 0 1 1.352 2.054c0 .337-.08.67-.23.972a.25.25 0 0 0 .042.28l.007.009a15.016 15.016 0 0 1 2.52 4.6.25.25 0 0 0 .37.132.25.25 0 0 0 .096-.114c.623-1.464.944-3.039.945-4.63a12.005 12.005 0 0 0-5.78-10.258.25.25 0 0 0-.373.274c.547 2.109.85 4.274.901 6.453ZM9.61 5.38a.25.25 0 0 0 .08.31c.34.24.616.561.8.935a.25.25 0 0 0 .3.127.631.631 0 0 1 .206-.034c2.054.078 4.036.772 5.69 1.991a.251.251 0 0 0 .267.024c.046-.024.093-.047.141-.067a.25.25 0 0 0 .151-.23A29.98 29.98 0 0 0 15.957.764a.25.25 0 0 0-.16-.164 11.924 11.924 0 0 0-2.21-.518.252.252 0 0 0-.215.076A22.456 22.456 0 0 0 9.61 5.38Z"
/>
</g>
</svg>
</div>
<div class="pt-3 sm:pt-5">
<h2 class="text-xl font-semibold text-black dark:text-white">Vibrant Ecosystem</h2>
<p class="mt-4 text-sm/relaxed">
Laravel's robust library of first-party tools and libraries, such as <a href="https://forge.laravel.com" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white dark:focus-visible:ring-[#FF2D20]">Forge</a>, <a href="https://vapor.laravel.com" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Vapor</a>, <a href="https://nova.laravel.com" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Nova</a>, <a href="https://envoyer.io" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Envoyer</a>, and <a href="https://herd.laravel.com" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Herd</a> help you take your projects to the next level. Pair them with powerful open source libraries like <a href="https://laravel.com/docs/billing" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Cashier</a>, <a href="https://laravel.com/docs/dusk" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Dusk</a>, <a href="https://laravel.com/docs/broadcasting" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Echo</a>, <a href="https://laravel.com/docs/horizon" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Horizon</a>, <a href="https://laravel.com/docs/sanctum" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Sanctum</a>, <a href="https://laravel.com/docs/telescope" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Telescope</a>, and more.
</p>
</div>
</div>
</div>
</main>
<footer class="py-16 text-center text-sm text-black dark:text-white/70">

View file

@ -1,9 +1,30 @@
<?php
use Illuminate\Http\Request;
use App\Http\Controllers\AuthController;
use App\Http\Controllers\TaskController;
use App\Http\Controllers\QuestController;
use App\Http\Controllers\UserController;
use App\Http\Controllers\RewardController;
use Illuminate\Support\Facades\Route;
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
Route::post('/auth/register', [AuthController::class, 'register']);
Route::post('/auth/login', [AuthController::class, 'login']);
Route::middleware(['auth:sanctum'])->group(function () {
Route::get('/auth/me', [AuthController::class, 'me']);
Route::get('/tasks', [TaskController::class, 'index']);
Route::post('/tasks', [TaskController::class, 'store']);
Route::get('/tasks/{id}', [TaskController::class, 'show']);
Route::put('/tasks/{id}', [TaskController::class, 'update']);
Route::delete('/tasks/{id}', [TaskController::class, 'destroy']);
Route::get('/quests', [QuestController::class, 'index']);
Route::post('/quests', [QuestController::class, 'generate']);
Route::post('/quests/{id}/complete', [QuestController::class, 'complete']);
Route::get('/user/status', [UserController::class, 'status']);
Route::get('/rewards', [RewardController::class, 'index']);
Route::get('/rewards/unlocked', [RewardController::class, 'unlocked']);
});

480
swagger.yml Normal file
View file

@ -0,0 +1,480 @@
openapi: 3.0.0
info:
title: ADHD Home Quest API
version: 1.0.0
description: >
Eine REST-API für die ADHD Home Quest App.
Diese API bietet Endpunkte für:
- User-Registrierung und -Login
- Aufgabenverwaltung (Tasks)
- Quest-Generierung (Quests)
- XP & Levelsystem
- Belohnungen (Rewards) & Achievements
contact:
name: ADHD Home Quest Team
email: adhdhomequest@josunlp.de
servers:
- url: https://api.adhdhomequest.josunlp.de
description: Produktions-Server
- url: https://staging.api.adhdhomequest.josunlp.de
description: Staging-Server
tags:
- name: Auth
description: Endpunkte zur Authentifizierung und User-Verwaltung
- name: Tasks
description: Endpunkte zur Aufgabenverwaltung
- name: Quests
description: Generierung und Management von Quests
- name: User
description: Userbezogene Daten (XP, Level, Achievements)
- name: Rewards
description: Rewards und Achievements
paths:
/auth/register:
post:
tags:
- Auth
summary: Registriert einen neuen Nutzer
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterRequest'
responses:
'201':
description: Nutzer erfolgreich registriert
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
'400':
description: Ungültige Eingaben
/auth/login:
post:
tags:
- Auth
summary: Loggt einen bestehenden Nutzer ein
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
responses:
'200':
description: Erfolgreich eingeloggt, Access Token wird zurückgegeben
content:
application/json:
schema:
$ref: '#/components/schemas/LoginResponse'
'401':
description: Ungültige Login-Daten
/auth/me:
get:
tags:
- Auth
summary: Liefert Informationen über den eingeloggten Nutzer
security:
- BearerAuth: []
responses:
'200':
description: Nutzerinformationen
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
'401':
description: Nicht autorisiert
/tasks:
get:
tags:
- Tasks
summary: Liste alle Tasks des eingeloggten Nutzers auf
security:
- BearerAuth: []
responses:
'200':
description: Liste von Tasks
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Task'
post:
tags:
- Tasks
summary: Erstelle einen neuen Task
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateTaskRequest'
responses:
'201':
description: Task erstellt
content:
application/json:
schema:
$ref: '#/components/schemas/Task'
'400':
description: Ungültige Daten
/tasks/{id}:
get:
tags:
- Tasks
summary: Hole einen einzelnen Task
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Task gefunden
content:
application/json:
schema:
$ref: '#/components/schemas/Task'
'404':
description: Task nicht gefunden
put:
tags:
- Tasks
summary: Aktualisiere einen bestehenden Task
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateTaskRequest'
responses:
'200':
description: Task aktualisiert
content:
application/json:
schema:
$ref: '#/components/schemas/Task'
'400':
description: Ungültige Daten
'404':
description: Task nicht gefunden
delete:
tags:
- Tasks
summary: Lösche einen bestehenden Task
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'204':
description: Task gelöscht
'404':
description: Task nicht gefunden
/quests:
get:
tags:
- Quests
summary: Liste aller aktuell generierten Quests für den Nutzer
security:
- BearerAuth: []
responses:
'200':
description: Liste von Quests
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Quest'
post:
tags:
- Quests
summary: Generiere neue Quests auf Basis der vorhandenen Tasks
security:
- BearerAuth: []
responses:
'201':
description: Neue Quests generiert
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Quest'
/quests/{id}/complete:
post:
tags:
- Quests
summary: Markiert eine Quest als erledigt und vergibt XP
security:
- BearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Quest erledigt, XP vergeben
content:
application/json:
schema:
$ref: '#/components/schemas/QuestCompletionResponse'
'404':
description: Quest nicht gefunden
/user/status:
get:
tags:
- User
summary: Liefert den aktuellen Level, XP und nächste Rewards
security:
- BearerAuth: []
responses:
'200':
description: User-Status mit XP, Level und Rewards
content:
application/json:
schema:
$ref: '#/components/schemas/UserStatus'
/rewards:
get:
tags:
- Rewards
summary: Liste aller möglichen Rewards / Achievements
security:
- BearerAuth: []
responses:
'200':
description: Liste aller Rewards/Achievements
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Reward'
/rewards/unlocked:
get:
tags:
- Rewards
summary: Liste der freigeschalteten Rewards für den aktuellen Nutzer
security:
- BearerAuth: []
responses:
'200':
description: Liste der freigeschalteten Rewards
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Reward'
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
RegisterRequest:
type: object
required:
- name
- email
- password
properties:
name:
type: string
email:
type: string
format: email
password:
type: string
LoginRequest:
type: object
required:
- email
- password
properties:
email:
type: string
format: email
password:
type: string
LoginResponse:
type: object
properties:
token:
type: string
user:
$ref: '#/components/schemas/User'
UserResponse:
type: object
properties:
user:
$ref: '#/components/schemas/User'
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
level:
type: integer
xp:
type: integer
UserStatus:
type: object
properties:
level:
type: integer
xp:
type: integer
xp_for_next_level:
type: integer
upcoming_rewards:
type: array
items:
$ref: '#/components/schemas/Reward'
Task:
type: object
properties:
id:
type: integer
title:
type: string
description:
type: string
category:
type: string
enum: [daily, weekend, vacation, work]
priority:
type: string
enum: [low, medium, high]
due_date:
type: string
format: date-time
CreateTaskRequest:
type: object
required:
- title
properties:
title:
type: string
description:
type: string
category:
type: string
enum: [daily, weekend, vacation, work]
priority:
type: string
enum: [low, medium, high]
due_date:
type: string
format: date-time
UpdateTaskRequest:
type: object
properties:
title:
type: string
description:
type: string
category:
type: string
enum: [daily, weekend, vacation, work]
priority:
type: string
enum: [low, medium, high]
due_date:
type: string
format: date-time
Quest:
type: object
properties:
id:
type: integer
task_id:
type: integer
title:
type: string
category:
type: string
due_date:
type: string
format: date-time
xp_reward:
type: integer
QuestCompletionResponse:
type: object
properties:
quest:
$ref: '#/components/schemas/Quest'
xp_gained:
type: integer
new_level:
type: integer
level_up:
type: boolean
Reward:
type: object
properties:
id:
type: integer
name:
type: string
description:
type: string
unlocked_at_level:
type: integer