feat: some fixes to the ui, imlemented user and guest user, ans used the dahboard

This commit is contained in:
2026-08-16 16:24:25 +02:00
parent 4639baa9ae
commit 50e9fbd0b0
37 changed files with 1657 additions and 194 deletions
@@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers;
use App\Models\Room;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class DashboardController extends Controller
{
public function __invoke(Request $request): Response
{
$rooms = $request->user()
->ownedRooms()
->withCount('players')
->orderByRaw('closed_at IS NOT NULL')
->orderByDesc('last_activity_at')
->get()
->map(fn (Room $room) => [
'id' => $room->id,
'title' => $room->title,
'join_code' => $room->join_code,
'current_round' => $room->current_round,
'players_count' => $room->players_count,
'last_activity_at' => $room->last_activity_at?->toIso8601String(),
'closed_at' => $room->closed_at?->toIso8601String(),
]);
return Inertia::render('Dashboard', [
'rooms' => $rooms,
]);
}
}
+133 -6
View File
@@ -6,13 +6,15 @@ use App\Http\Requests\StoreRoomRequest;
use App\Models\Deck;
use App\Models\Player;
use App\Models\Room;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Inertia\Response;
class RoomController extends Controller
{
public function create(Request $request)
public function create(Request $request): Response
{
$user = $request->user();
@@ -37,7 +39,7 @@ class RoomController extends Controller
]);
}
public function store(Request $request, StoreRoomRequest $roomStoreRequest)
public function store(Request $request, StoreRoomRequest $roomStoreRequest): RedirectResponse
{
$validated = $roomStoreRequest->validated();
$user = $request->user();
@@ -59,7 +61,9 @@ class RoomController extends Controller
$user,
) {
$room = Room::create([
'owner_id' => $user?->id,
'title' => $validated['title'] ?? null,
'last_activity_at' => now(),
'deck_config' => [
'id' => $deck->id,
'name' => $deck->name,
@@ -82,10 +86,19 @@ class RoomController extends Controller
$player->id,
);
return to_route('room.show', $room);
$redirect = to_route('room.show', $room);
if ($user === null) {
$redirect->with('room_notice', [
'guest_retention_days' => config('rooms.guest_retention_days'),
'account_retention_days' => config('rooms.account_retention_days'),
]);
}
return $redirect;
}
public function show(Room $room, Request $request)
public function show(Room $room, Request $request): Response
{
$currentPlayer = $request->attributes->get('currentPlayer');
@@ -117,6 +130,9 @@ class RoomController extends Controller
->firstWhere('id', $currentPlayer->id)
?->votes
->first();
$statistics = $room->revealed_at !== null
? $this->currentRoundStatistics($room)
: null;
return Inertia::render('rooms/Show', [
'room' => [
@@ -126,6 +142,7 @@ class RoomController extends Controller
'current_round' => $room->current_round,
'revealed' => $room->revealed_at !== null,
'state_version' => $room->state_version,
'statistics' => $statistics,
'deck' => [
'name' => $room->deck_config['name'],
'cards' => $room->deck_config['cards'],
@@ -141,10 +158,120 @@ class RoomController extends Controller
]);
}
public function join(Request $request)
public function join(Request $request): Response
{
$joinCode = strtoupper(trim((string) $request->query('join_code')));
$room = $joinCode !== ''
? Room::query()->where('join_code', $joinCode)->first()
: null;
$existingPlayer = $room !== null && $request->user() !== null
? $room->players()->where('user_id', $request->user()->id)->first()
: null;
return Inertia::render('rooms/Join', [
'join_code' => $request->query('join_code'),
'join_code' => $joinCode,
'defaultPlayerName' => $existingPlayer instanceof Player
? $existingPlayer->name
: $request->user()?->name,
'closedRoom' => $room?->isClosed() ? [
'title' => $room->title,
'join_code' => $room->join_code,
] : null,
]);
}
public function reopen(Room $room, Request $request): RedirectResponse
{
$user = $request->user();
abort_if($room->trashed(), 404);
abort_unless($user !== null && $user->id === $room->owner_id, 403);
$player = DB::transaction(function () use ($room, $user) {
$lockedRoom = Room::query()->lockForUpdate()->findOrFail($room->id);
abort_unless($user->id === $lockedRoom->owner_id, 403);
if ($lockedRoom->isClosed()) {
$lockedRoom->update([
'current_round' => $lockedRoom->current_round + 1,
'revealed_at' => null,
'closed_at' => null,
'last_activity_at' => now(),
'state_version' => $lockedRoom->state_version + 1,
]);
}
return $lockedRoom->players()
->where('user_id', $user->id)
->firstOrFail();
});
$request->session()->put("room_players.{$room->id}", $player->id);
return to_route('room.show', $room);
}
public function destroy(Room $room, Request $request): RedirectResponse
{
$user = $request->user();
abort_if($room->trashed(), 404);
abort_unless($user !== null && $user->id === $room->owner_id, 403);
DB::transaction(function () use ($room, $user): void {
$lockedRoom = Room::withTrashed()
->lockForUpdate()
->findOrFail($room->id);
abort_if($lockedRoom->trashed(), 404);
abort_unless($user->id === $lockedRoom->owner_id, 403);
$lockedRoom->update([
'state_version' => $lockedRoom->state_version + 1,
]);
$lockedRoom->delete();
});
$request->session()->forget("room_players.{$room->id}");
Inertia::flash('toast', [
'type' => 'success',
'message' => 'Der Raum wurde gelöscht.',
]);
return to_route('dashboard');
}
/**
* @return array{average: string|null, median: string|null}
*/
private function currentRoundStatistics(Room $room): array
{
$values = [];
foreach ($room->players as $player) {
$vote = $player->votes->first();
if ($vote !== null && is_numeric($vote->value)) {
$values[] = (float) $vote->value;
}
}
sort($values, SORT_NUMERIC);
$count = count($values);
if ($count === 0) {
return ['average' => null, 'median' => null];
}
$middle = intdiv($count, 2);
$median = $count % 2 === 1
? $values[$middle]
: ($values[$middle - 1] + $values[$middle]) / 2;
return [
'average' => number_format(array_sum($values) / $count, 2, ',', ''),
'median' => number_format($median, 2, ',', ''),
];
}
}
+50 -8
View File
@@ -3,23 +3,51 @@
namespace App\Http\Controllers;
use App\Http\Requests\JoinRoomRequest;
use App\Models\Player;
use App\Models\Room;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class RoomPlayerController extends Controller
{
public function join(JoinRoomRequest $request)
public function join(JoinRoomRequest $request): RedirectResponse
{
$validated = $request->validated();
$user = $request->user();
$room = Room::where('join_code', '=', $validated['join_code'])->firstOrFail();
[$room, $player] = DB::transaction(function () use ($request, $validated, $user): array {
$room = Room::query()
->where('join_code', $validated['join_code'])
->lockForUpdate()
->firstOrFail();
$player = $room->players()->create([
'user_id' => $user?->id,
'name' => $validated['player_name'],
'is_moderator' => false,
'last_seen_at' => now(),
]);
if ($room->isClosed()) {
throw ValidationException::withMessages([
'join_code' => 'Dieser Raum ist geschlossen.',
]);
}
$player = $this->existingPlayer($request, $room);
if ($player === null) {
$player = $room->players()->create([
'user_id' => $user?->id,
'name' => $validated['player_name'],
'is_moderator' => false,
'last_seen_at' => now(),
]);
} else {
$player->update(['last_seen_at' => now()]);
}
$room->update([
'last_activity_at' => now(),
'state_version' => $room->state_version + 1,
]);
return [$room, $player];
});
$request->session()->put(
"room_players.{$room->id}",
@@ -27,6 +55,20 @@ class RoomPlayerController extends Controller
);
return to_route('room.show', $room);
}
private function existingPlayer(JoinRoomRequest $request, Room $room): ?Player
{
if ($request->user() !== null) {
return $room->players()
->where('user_id', $request->user()->id)
->first();
}
$playerId = $request->session()->get("room_players.{$room->id}");
return is_numeric($playerId)
? $room->players()->find((int) $playerId)
: null;
}
}
+18
View File
@@ -30,6 +30,8 @@ class RoundController extends Controller
->lockForUpdate()
->findOrFail($room->id);
$this->ensureRoomIsOpen($lockedRoom);
Vote::query()
->where('room_id', $lockedRoom->id)
->where('round', $lockedRoom->current_round)
@@ -37,6 +39,7 @@ class RoundController extends Controller
$lockedRoom->update([
'revealed_at' => null,
'last_activity_at' => now(),
'state_version' => $lockedRoom->state_version + 1,
]);
});
@@ -62,6 +65,8 @@ class RoundController extends Controller
->lockForUpdate()
->findOrFail($room->id);
$this->ensureRoomIsOpen($lockedRoom);
if ($lockedRoom->revealed_at !== null) {
throw ValidationException::withMessages([
'room' => 'Die Karten sind bereits aufgedeckt.',
@@ -70,6 +75,7 @@ class RoundController extends Controller
$lockedRoom->update([
'revealed_at' => now(),
'last_activity_at' => now(),
'state_version' => $lockedRoom->state_version + 1,
]);
});
@@ -95,6 +101,8 @@ class RoundController extends Controller
->lockForUpdate()
->findOrFail($room->id);
$this->ensureRoomIsOpen($lockedRoom);
if ($lockedRoom->revealed_at === null) {
throw ValidationException::withMessages([
'room' => 'Die aktuelle Runde muss zuerst aufgedeckt werden.',
@@ -104,10 +112,20 @@ class RoundController extends Controller
$lockedRoom->update([
'current_round' => $lockedRoom->current_round + 1,
'revealed_at' => null,
'last_activity_at' => now(),
'state_version' => $lockedRoom->state_version + 1,
]);
});
return back();
}
private function ensureRoomIsOpen(Room $room): void
{
if ($room->isClosed()) {
throw ValidationException::withMessages([
'room' => 'Dieser Raum ist geschlossen.',
]);
}
}
}
+10 -1
View File
@@ -31,6 +31,12 @@ class VoteController extends Controller
->lockForUpdate()
->findOrFail($room->id);
if ($lockedRoom->isClosed()) {
throw ValidationException::withMessages([
'value' => 'Dieser Raum ist geschlossen.',
]);
}
if ($lockedRoom->revealed_at !== null) {
throw ValidationException::withMessages([
'value' => 'Die Karten wurden bereits aufgedeckt.',
@@ -48,7 +54,10 @@ class VoteController extends Controller
],
);
$lockedRoom->increment('state_version');
$lockedRoom->update([
'last_activity_at' => now(),
'state_version' => $lockedRoom->state_version + 1,
]);
});
return back();
+31 -5
View File
@@ -5,6 +5,7 @@ namespace App\Http\Middleware;
use App\Models\Room;
use Closure;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Symfony\Component\HttpFoundation\Response;
class EnsureRoomParticipant
@@ -23,23 +24,48 @@ class EnsureRoomParticipant
}
$sessionKey = "room_players.{$room->id}";
$playerId = $request->session()->get($sessionKey);
if (! $playerId) {
if ($room->trashed()) {
$request->session()->forget($sessionKey);
Inertia::flash('toast', [
'type' => 'info',
'message' => 'Dieser Raum wurde gelöscht.',
]);
return to_route('home');
}
if ($room->isClosed()) {
return to_route('room.join', [
'join_code' => $room->join_code,
])->withErrors([
'join_code' => 'Du musst diesen Raum erst beitreten.',
'join_code' => 'Dieser Raum ist geschlossen.',
]);
}
$player = $room->players()->find($playerId);
$playerId = $request->session()->get($sessionKey);
if (! $player) {
$player = $playerId ? $room->players()->find($playerId) : null;
if ($player === null && $playerId) {
$request->session()->forget($sessionKey);
}
if ($player === null && $request->user() !== null) {
$player = $room->players()
->where('user_id', $request->user()->id)
->first();
if ($player !== null) {
$request->session()->put($sessionKey, $player->id);
}
}
if ($player === null) {
return to_route('room.join', [
'join_code' => $room->join_code,
])->withErrors([
'join_code' => 'Du musst diesem Raum erst beitreten.',
]);
}
@@ -41,6 +41,7 @@ class HandleInertiaRequests extends Middleware
'auth' => [
'user' => $request->user(),
],
'roomNotice' => fn () => $request->session()->get('room_notice'),
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
];
}
+48 -2
View File
@@ -7,6 +7,7 @@ use App\Models\Room;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
class JoinRoomRequest extends FormRequest
{
@@ -37,9 +38,54 @@ class JoinRoomRequest extends FormRequest
->where('join_code', $this->input('join_code'))
->value('id');
$existingPlayerId = null;
if ($roomId !== null && $this->user() !== null) {
$existingPlayerId = Player::query()
->where('room_id', $roomId)
->where('user_id', $this->user()->id)
->value('id');
} elseif ($roomId !== null) {
$sessionPlayerId = $this->session()->get("room_players.{$roomId}");
$existingPlayerId = is_numeric($sessionPlayerId)
? Player::query()
->where('room_id', $roomId)
->whereKey((int) $sessionPlayerId)
->value('id')
: null;
}
$playerNameRules = ['required', 'string', 'max:50'];
if ($existingPlayerId === null) {
$playerNameRules[] = Rule::unique(Player::class, 'name')->where('room_id', $roomId);
}
return [
'join_code' => ['bail', 'string', 'required', 'size:6', Rule::exists(Room::class, 'join_code')],
'player_name' => ['required', 'string', 'max:50', Rule::unique(Player::class, 'name')->where('room_id', $roomId)],
'join_code' => [
'bail',
'string',
'required',
'size:6',
Rule::exists(Room::class, 'join_code')->whereNull('deleted_at'),
],
'player_name' => $playerNameRules,
];
}
/** @return array<callable(Validator): void> */
public function after(): array
{
return [
function (Validator $validator): void {
$room = Room::query()
->where('join_code', $this->input('join_code'))
->first();
if ($room?->isClosed()) {
$validator->errors()->add('join_code', 'Dieser Raum ist geschlossen.');
}
},
];
}
+5
View File
@@ -2,6 +2,7 @@
namespace App\Http\Requests;
use App\Models\Room;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
@@ -25,6 +26,10 @@ class SubmitVoteRequest extends FormRequest
{
$room = $this->route('room');
if (! $room instanceof Room) {
return ['value' => ['prohibited']];
}
return [
'value' => ['bail', 'required', 'string', Rule::in(array_column($room->deck_config['cards'], 'key'))],
];