From f7328d59eb7d619437a6244fbf89607301adf877 Mon Sep 17 00:00:00 2001 From: Tiago Ramos <2222055@my.ipleiria.pt> Date: Sat, 3 Jan 2026 14:45:14 +0000 Subject: [PATCH] removed comments --- api/app/Http/Controllers/GameController.php | 16 +- api/app/Http/Controllers/MatchController.php | 16 +- .../Http/Controllers/StatisticsController.php | 4 +- api/app/Http/Controllers/UserController.php | 57 +-- api/app/Http/Requests/StoreGameRequest.php | 13 - api/app/Policies/GamePolicy.php | 24 +- api/bootstrap/app.php | 1 - api/config/auth.php | 5 - api/config/cache.php | 1 - api/routes/api.php | 6 +- frontend/src/App.vue | 2 - frontend/src/components/game/DeckArea.vue | 4 +- frontend/src/components/game/FlyingCard.vue | 4 - frontend/src/components/game/GameBoard.vue | 1 - frontend/src/components/game/GameOver.vue | 6 - frontend/src/components/game/ScoreDisplay.vue | 8 +- frontend/src/main.js | 1 - frontend/src/pages/admin/AdminPage.vue | 11 +- .../src/pages/game/MultiplayerGamePage.vue | 338 ++++++++++-------- .../src/pages/game/SinglePlayerGamePage.vue | 51 ++- frontend/src/pages/user/UserPage.vue | 11 +- frontend/src/stores/api.js | 2 - frontend/src/stores/auth.js | 14 +- frontend/src/stores/bisca.js | 1 - frontend/src/stores/socket.js | 2 +- websockets/classes/BiscaMatch.js | 5 - websockets/events/connection.js | 4 +- websockets/events/game.js | 5 - websockets/events/match.js | 4 - 29 files changed, 266 insertions(+), 351 deletions(-) diff --git a/api/app/Http/Controllers/GameController.php b/api/app/Http/Controllers/GameController.php index 949a347..ce5321e 100644 --- a/api/app/Http/Controllers/GameController.php +++ b/api/app/Http/Controllers/GameController.php @@ -17,7 +17,6 @@ class GameController extends Controller $user = Auth::user(); $query = Game::query()->with(["winner", "player1", "player2"]); - // Se não for Admin, mostra apenas os jogos do utilizador if ($user->type !== 'A') { $query->where(function ($q) use ($user) { $q->where('player1_user_id', $user->id) @@ -50,9 +49,6 @@ class GameController extends Controller return response()->json($query->paginate(15)); } - // --- MÉTODOS ADICIONADOS --- - - // Host a new multiplayer Game public function host(Request $request) { $request->validate([ @@ -61,7 +57,6 @@ class GameController extends Controller $user = $request->user(); - // Check for active game $activeGame = Game::where(function ($q) use ($user) { $q->where("player1_user_id", $user->id) ->orWhere("player2_user_id", $user->id); @@ -77,7 +72,7 @@ class GameController extends Controller 'type' => $request->type, 'status' => 'Pending', 'player1_user_id' => $user->id, - 'player2_user_id' => self::GHOST_ID, // Ghost ID = 1 (Waiting for player) + 'player2_user_id' => self::GHOST_ID, 'began_at' => now(), 'player1_points' => 0, 'player2_points' => 0, @@ -86,14 +81,13 @@ class GameController extends Controller return response()->json($game, 201); } - // List open games (Available to join) public function open(Request $request) { $type = $request->query('type', '9'); $GHOST_ID = 1; $games = Game::where('status', 'Pending') - ->where('player2_user_id', $GHOST_ID) // Games waiting for P2 + ->where('player2_user_id', $GHOST_ID) ->where('type', $type) ->with('player1') ->orderBy('began_at', 'asc') @@ -101,7 +95,6 @@ class GameController extends Controller return response()->json($games); } - // --------------------------- public function store(Request $request) { @@ -132,7 +125,6 @@ class GameController extends Controller $initialStatus = $request->input('status', 'Pending'); - // If specific player 2 is provided, auto-start game if ($player2Id !== self::GHOST_ID && !$request->has('status')) { $initialStatus = 'Playing'; } @@ -159,17 +151,14 @@ class GameController extends Controller public function join(Request $request, Game $game) { - // Prevent joining if already full or started if ($game->status !== 'Pending' || $game->player2_user_id !== self::GHOST_ID) { return response()->json(['message' => 'Game is not available'], 400); } - // Prevent joining your own game if ($game->player1_user_id === $request->user()->id) { return response()->json(['message' => 'Cannot join your own game'], 400); } - // Check if joining user has another active game $activeGame = Game::where(function ($q) use ($request) { $q->where("player1_user_id", $request->user()->id) ->orWhere("player2_user_id", $request->user()->id); @@ -247,7 +236,6 @@ class GameController extends Controller public function destroy(Game $game) { - // Ensure only the host can delete, and ONLY if it's still Pending if ($game->player1_user_id !== auth()->id()) { return response()->json(['message' => 'Unauthorized'], 403); } diff --git a/api/app/Http/Controllers/MatchController.php b/api/app/Http/Controllers/MatchController.php index f8c1ca7..7d88d9a 100644 --- a/api/app/Http/Controllers/MatchController.php +++ b/api/app/Http/Controllers/MatchController.php @@ -70,7 +70,7 @@ class MatchController extends Controller $stakeType = CoinTransactionType::firstOrCreate( ["name" => "Match stake"], - ["type" => "D"], // Debit + ["type" => "D"], ); $match = MatchGame::create([ @@ -213,7 +213,7 @@ class MatchController extends Controller ) { $payoutType = CoinTransactionType::firstOrCreate( ["name" => "Match payout"], - ["type" => "C"], // Credit + ["type" => "C"], ); $totalPrize = $match->stake * 2; @@ -273,7 +273,7 @@ class MatchController extends Controller $stakeType = CoinTransactionType::firstOrCreate( ["name" => "Match stake"], - ["type" => "D"], // Debit + ["type" => "D"], ); $match = MatchGame::create([ @@ -307,7 +307,7 @@ class MatchController extends Controller $query = MatchGame::where('status', 'Pending') ->where(function($q) { $q->whereNull('player2_user_id') - ->orWhere('player2_user_id', 1); // 1 = Ghost ID + ->orWhere('player2_user_id', 1); }); if ($request->has('type')) { @@ -321,7 +321,6 @@ class MatchController extends Controller return response()->json($matches); } - // --- FIX: Safely Delete Match by removing dependencies first --- public function destroy(MatchGame $match) { $user = request()->user(); @@ -335,13 +334,10 @@ class MatchController extends Controller } return DB::transaction(function () use ($match, $user) { - // 1. Delete associated Games first (The FK constraint cause) DB::table('games')->where('match_id', $match->id)->delete(); - // 2. Unlink existing Coin Transactions (Set match_id to NULL) CoinTransaction::where('match_id', $match->id)->update(['match_id' => null]); - // 3. Process Refund if ($match->stake > 0) { $user->increment('coins_balance', $match->stake); @@ -350,10 +346,9 @@ class MatchController extends Controller ['type' => 'C'] ); - // Create Refund Transaction with NO LINK to the deleted match CoinTransaction::create([ 'user_id' => $user->id, - 'match_id' => null, // IMPORTANT: Must be null + 'match_id' => null, 'coin_transaction_type_id' => $refundType->id, 'transaction_datetime' => now(), 'coins' => $match->stake, @@ -361,7 +356,6 @@ class MatchController extends Controller ]); } - // 4. Finally delete the match $match->delete(); return response()->json([ diff --git a/api/app/Http/Controllers/StatisticsController.php b/api/app/Http/Controllers/StatisticsController.php index aabba2c..aac96ef 100644 --- a/api/app/Http/Controllers/StatisticsController.php +++ b/api/app/Http/Controllers/StatisticsController.php @@ -65,7 +65,7 @@ class StatisticsController extends Controller ->where('winner_user_id', $user->id) ->count(); - $totalLosses = $totalGames - $totalWins; // Simplest math + $totalLosses = $totalGames - $totalWins; $winRate = 0; if ($totalGames > 0) { @@ -78,7 +78,7 @@ class StatisticsController extends Controller 'total_wins' => $totalWins, 'total_losses' => $totalLosses, 'win_rate' => $winRate . '%', - 'current_balance' => $user->coins_balance, // Extra info + 'current_balance' => $user->coins_balance, ]); } diff --git a/api/app/Http/Controllers/UserController.php b/api/app/Http/Controllers/UserController.php index 29eec09..c20bf09 100644 --- a/api/app/Http/Controllers/UserController.php +++ b/api/app/Http/Controllers/UserController.php @@ -12,10 +12,6 @@ use Illuminate\Validation\Rule; class UserController extends Controller { - /** - * GET /api/users - * Lista todos os utilizadores (Apenas para Admins) - */ public function index(Request $request) { $this->authorize("viewAny", User::class); @@ -41,10 +37,6 @@ class UserController extends Controller return response()->json($query->paginate(15)); } - /** - * POST /api/users - * Registar um novo utilizador (Admin only) - */ public function store(Request $request) { $this->authorize("create", User::class); @@ -63,25 +55,15 @@ class UserController extends Controller return response()->json($user, 201); } - /** - * GET /api/users/me - * Retorna o perfil do utilizador autenticado atual. - * Usado pelo VueJS para saber quem está logado. - */ public function me(Request $request) { return response()->json($request->user()); } - /** - * GET /api/users/{user} - * Mostra um perfil específico. - */ public function show(User $user) { $currentUser = Auth::user(); - // Admins and owners get full profile, others get limited view if ($currentUser->type === "A" || $currentUser->id === $user->id) { return response()->json($user); } else { @@ -95,10 +77,6 @@ class UserController extends Controller } } - /** - * PUT/PATCH /api/users/{user} - * Atualizar perfil (Nome, Nickname, Password, Foto) - */ public function update(Request $request, User $user) { $this->authorize("update", $user); @@ -119,12 +97,6 @@ class UserController extends Controller "password" => "sometimes|string|min:3", ]); - // Lógica de Upload de Foto (Exemplo Básico) - // if ($request->hasFile('photo_avatar')) { - // $path = $request->file('photo_avatar')->store('avatars', 'public'); - // $validated['photo_avatar_filename'] = basename($path); - // } - $user->update($validated); return response()->json($user); @@ -139,7 +111,6 @@ class UserController extends Controller "new_password" => "required|string|min:3|confirmed", ]); - // Verificar se a password atual está correta if (!Hash::check($validated["current_password"], $user->password)) { return response()->json( ["message" => "Current password is incorrect"], @@ -147,22 +118,16 @@ class UserController extends Controller ); } - // Atualizar para a nova password $user->password = Hash::make($validated["new_password"]); $user->save(); return response()->json(["message" => "Password updated successfully"]); } - /** - * DELETE /api/users/{user} - * Apagar conta (Soft Delete) - */ public function destroy(User $user) { $this->authorize("delete", $user); - // Check if user has transactions or games - if so, soft delete $hasTransactions = $user->transactions()->exists(); $hasGames = Game::where(function ($q) use ($user) { $q->where("player1_user_id", $user->id)->orWhere( @@ -172,26 +137,21 @@ class UserController extends Controller })->exists(); if ($hasTransactions || $hasGames) { - $user->delete(); // Soft delete + $user->delete(); $message = "User account deactivated (soft-deleted due to transaction/game history)"; } else { - $user->forceDelete(); // Hard delete + $user->forceDelete(); $message = "User permanently deleted"; } return response()->json(["message" => $message]); } - /** - * GET /api/users/{user}/games - * Histórico de Partidas Individuais (Cartas) - */ public function getGames(Request $request, User $user) { $this->authorize("view", $user); - // Define a query base para GAMES $query = \App\Models\Game::query() ->where(function ($q) use ($user) { $q->where("player1_user_id", $user->id)->orWhere( @@ -209,7 +169,6 @@ class UserController extends Controller $query->where("status", $request->status); } - // Date filtering if ($request->has("date_from")) { $query->whereDate("began_at", ">=", $request->date_from); } @@ -218,11 +177,9 @@ class UserController extends Controller $query->whereDate("began_at", "<=", $request->date_to); } - // Flexible sorting $sortColumn = $request->get("sort_by", "began_at"); $sortDirection = $request->get("sort_direction", "desc"); - // Whitelist sortable columns for security $allowedColumns = [ "began_at", "ended_at", @@ -251,10 +208,6 @@ class UserController extends Controller return response()->json($games); } - /** - * GET /api/users/{user}/matches - * Histórico de Matches (Apostas / Marcas) - */ public function getMatches(Request $request, User $user) { $this->authorize("view", $user); @@ -276,7 +229,6 @@ class UserController extends Controller $query->where("status", $request->status); } - // Date filtering if ($request->has("date_from")) { $query->whereDate("began_at", ">=", $request->date_from); } @@ -285,11 +237,9 @@ class UserController extends Controller $query->whereDate("began_at", "<=", $request->date_to); } - // Flexible sorting $sortColumn = $request->get("sort_by", "began_at"); $sortDirection = $request->get("sort_direction", "desc"); - // Whitelist sortable columns for security $allowedColumns = ["began_at", "ended_at", "stake", "total_time"]; if (in_array($sortColumn, $allowedColumns)) { $query->orderBy($sortColumn, $sortDirection); @@ -308,7 +258,7 @@ class UserController extends Controller ) { $match->outcome = "loss"; } else { - $match->outcome = "interrupted"; // Matches geralmente não empatam (alguém chega a 4 marcas) + $match->outcome = "interrupted"; } return $match; }); @@ -323,7 +273,6 @@ class UserController extends Controller $user->blocked = true; $user->save(); - // Revoke all active tokens for immediate effect $user->tokens()->delete(); return response()->json(["message" => "User blocked successfully"]); diff --git a/api/app/Http/Requests/StoreGameRequest.php b/api/app/Http/Requests/StoreGameRequest.php index 35452e0..7759b73 100644 --- a/api/app/Http/Requests/StoreGameRequest.php +++ b/api/app/Http/Requests/StoreGameRequest.php @@ -7,31 +7,18 @@ use Illuminate\Support\Facades\Auth; class StoreGameRequest extends FormRequest { - /** - * Determine if the user is authorized to make this request. - */ public function authorize(): bool { - // Só utilizadores autenticados podem criar jogos return Auth::check(); } - /** - * Get the validation rules that apply to the request. - * - * @return array|string> - */ public function rules(): array { return [ - // Agora validamos diretamente '3' ou '9' para bater certo com a BD 'type' => 'required|in:3,9', ]; } - /** - * Mensagens personalizadas (Opcional) - */ public function messages(): array { return [ diff --git a/api/app/Policies/GamePolicy.php b/api/app/Policies/GamePolicy.php index 9fbdc9a..a2c563e 100644 --- a/api/app/Policies/GamePolicy.php +++ b/api/app/Policies/GamePolicy.php @@ -7,17 +7,11 @@ use App\Models\User; class GamePolicy { - /** - * Determine whether the user can view any models. - */ public function viewAny(User $user): bool { return true; } - /**2 - * Determine whether the user can view the model. - */ public function view(User $user, Game $game): bool { if ($user->type === "A") { @@ -38,9 +32,6 @@ class GamePolicy return false; } - /** - * Determine whether the user can create models. - */ public function create(User $user): bool { if ($user->type === "A") { @@ -86,9 +77,7 @@ class GamePolicy return true; } - /** - * Determine whether the user can update the model. - */ + public function update(User $user, Game $game): bool { if ($user->type === "A") { @@ -105,26 +94,17 @@ class GamePolicy return true; } - /** - * Determine whether the user can delete the model. - */ + public function delete(User $user, Game $game): bool { - // Only admins can delete games return $user->type === "A"; } - /** - * Determine whether the user can restore the model. - */ public function restore(User $user, Game $game): bool { return false; } - /** - * Determine whether the user can permanently delete the model. - */ public function forceDelete(User $user, Game $game): bool { return false; diff --git a/api/bootstrap/app.php b/api/bootstrap/app.php index 94db66d..242ab3c 100644 --- a/api/bootstrap/app.php +++ b/api/bootstrap/app.php @@ -17,6 +17,5 @@ return Application::configure(basePath: dirname(__DIR__)) ]); }) ->withExceptions(function (Exceptions $exceptions): void { - // }) ->create(); diff --git a/api/config/auth.php b/api/config/auth.php index 7d1eb0d..48e4d3f 100644 --- a/api/config/auth.php +++ b/api/config/auth.php @@ -64,11 +64,6 @@ return [ 'driver' => 'eloquent', 'model' => env('AUTH_MODEL', App\Models\User::class), ], - - // 'users' => [ - // 'driver' => 'database', - // 'table' => 'users', - // ], ], /* diff --git a/api/config/cache.php b/api/config/cache.php index b32aead..68a9475 100644 --- a/api/config/cache.php +++ b/api/config/cache.php @@ -61,7 +61,6 @@ return [ env('MEMCACHED_PASSWORD'), ], 'options' => [ - // Memcached::OPT_CONNECT_TIMEOUT => 2000, ], 'servers' => [ [ diff --git a/api/routes/api.php b/api/routes/api.php index 0a57f82..28dca03 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -11,14 +11,12 @@ use App\Http\Controllers\TransactionController; use Illuminate\Support\Facades\Route; Route::prefix('v1')->group(function () { - // Public Routes Route::post('/login', [AuthController::class, 'login']); Route::post('/register', [AuthController::class, 'register']); Route::get('/statistics/public', [StatisticsController::class, 'getPublicStats']); Route::get('/leaderboard', [StatisticsController::class, 'getLeaderboard']); - // Authenticated Routes Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () { Route::post('/logout', [AuthController::class, 'logout']); Route::post('/coin-purchases', [CoinPurchaseController::class, 'store']); @@ -37,7 +35,6 @@ Route::prefix('v1')->group(function () { Route::get('/transactions', [UserController::class, 'getMyTransactions']); }); - // User Resources Route::prefix('/users')->group(function () { Route::get('/{user}', [UserController::class, 'show']); Route::get('/{user}/transactions', [UserController::class, 'getTransactions']); @@ -61,12 +58,11 @@ Route::prefix('v1')->group(function () { Route::post('host', [MatchController::class, 'host']); Route::get('open', [MatchController::class, 'open']); Route::post('/{match}/join', [MatchController::class, 'join']); - Route::post('/{match}/start', [MatchController::class, 'start']); // Changed to /start to be explicit + Route::post('/{match}/start', [MatchController::class, 'start']); Route::delete('/{match}', [MatchController::class, 'destroy']); Route::apiResource('/', MatchController::class)->parameters(['' => 'match']); }); - // Admin Routes Route::middleware('user.type:A') ->prefix('admin') ->group(function () { diff --git a/frontend/src/App.vue b/frontend/src/App.vue index da16d8c..1bba2f2 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -42,8 +42,6 @@ const logout = () => { onMounted(async () => { await authStore.restoreSession() - // REMOVED - The WebSocket for multiplayer is handled separately in the WebSocketStore - // socketStore.handleConnection() }) diff --git a/frontend/src/components/game/DeckArea.vue b/frontend/src/components/game/DeckArea.vue index d1b45f2..7aa4701 100644 --- a/frontend/src/components/game/DeckArea.vue +++ b/frontend/src/components/game/DeckArea.vue @@ -88,7 +88,6 @@ const props = defineProps({ const deckPulse = ref(false) const trumpReveal = ref(false) -// Pulse animation when cards remaining changes watch( () => props.cardsRemaining, () => { @@ -99,7 +98,6 @@ watch( }, ) -// Trump reveal animation watch( () => props.revealTrump, (newValue) => { @@ -107,7 +105,7 @@ watch( trumpReveal.value = true setTimeout(() => { trumpReveal.value = false - }, 3000) // Show for 3 seconds + }, 3000) } }, ) diff --git a/frontend/src/components/game/FlyingCard.vue b/frontend/src/components/game/FlyingCard.vue index cf92227..f957ce3 100644 --- a/frontend/src/components/game/FlyingCard.vue +++ b/frontend/src/components/game/FlyingCard.vue @@ -54,18 +54,14 @@ const isAnimating = ref(false) const currentPosition = ref({ x: 0, y: 0 }) onMounted(() => { - // Start at the deck position currentPosition.value = { ...props.startPosition } isVisible.value = true - // Wait for delay, then start animation setTimeout(() => { - // Force a reflow to ensure starting position is set requestAnimationFrame(() => { isAnimating.value = true currentPosition.value = { ...props.endPosition } - // After animation completes, emit event and hide setTimeout(() => { emit('complete') isVisible.value = false diff --git a/frontend/src/components/game/GameBoard.vue b/frontend/src/components/game/GameBoard.vue index caf35a6..31f38c2 100644 --- a/frontend/src/components/game/GameBoard.vue +++ b/frontend/src/components/game/GameBoard.vue @@ -61,7 +61,6 @@ const props = defineProps({ cardsRemaining: { type: Number, default: 0 }, deckIsEmpty: { type: Boolean, default: false }, maxCards: { type: Number, default: 3 }, - // REMOVIDO: playableCardIndices (já não precisamos dele) playerScore: { type: Number, default: 0 }, opponentScore: { type: Number, default: 0 }, playerName: { type: String, default: 'You' }, diff --git a/frontend/src/components/game/GameOver.vue b/frontend/src/components/game/GameOver.vue index 98236ee..cde1675 100644 --- a/frontend/src/components/game/GameOver.vue +++ b/frontend/src/components/game/GameOver.vue @@ -218,21 +218,18 @@ const showConfetti = ref(false) const displayPlayerScore = ref(0) const displayOpponentScore = ref(0) -// Computed title based on winner const title = computed(() => { if (props.winner === 'player') return 'Victory!' if (props.winner === 'opponent') return 'Defeat' return 'Draw!' }) -// Computed subtitle const subtitle = computed(() => { if (props.winner === 'player') return 'Congratulations! You won the game!' if (props.winner === 'opponent') return 'Better luck next time!' return 'The game ended in a draw' }) -// Animate score count-up const animateScore = (fromValue, toValue, callback) => { const duration = 1000 const steps = 30 @@ -255,19 +252,16 @@ const animateScore = (fromValue, toValue, callback) => { }, stepDuration) } -// Watch for visibility and trigger animations watch( () => props.isVisible, (newValue) => { if (newValue) { - // Start confetti if player wins if (props.winner === 'player') { setTimeout(() => { showConfetti.value = true }, 300) } - // Animate scores counting up setTimeout(() => { animateScore(0, props.playerScore, (value) => { displayPlayerScore.value = value diff --git a/frontend/src/components/game/ScoreDisplay.vue b/frontend/src/components/game/ScoreDisplay.vue index 6571a10..dd20731 100644 --- a/frontend/src/components/game/ScoreDisplay.vue +++ b/frontend/src/components/game/ScoreDisplay.vue @@ -99,10 +99,9 @@ const turnChanged = ref(false) const displayPlayerScore = ref(props.playerScore) const displayOpponentScore = ref(props.opponentScore) -// Animate score count-up const animateScore = (fromValue, toValue, callback) => { - const duration = 500 // Total animation duration in ms - const steps = 20 // Number of increments + const duration = 500 + const steps = 20 const stepDuration = duration / steps const increment = (toValue - fromValue) / steps @@ -122,7 +121,6 @@ const animateScore = (fromValue, toValue, callback) => { }, stepDuration) } -// Watch for player score changes and trigger count-up animation watch( () => props.playerScore, (newScore, oldScore) => { @@ -143,7 +141,6 @@ watch( { immediate: true }, ) -// Watch for opponent score changes and trigger count-up animation watch( () => props.opponentScore, (newScore, oldScore) => { @@ -164,7 +161,6 @@ watch( { immediate: true }, ) -// Watch for turn changes and trigger pulse animation watch( () => props.currentTurn, () => { diff --git a/frontend/src/main.js b/frontend/src/main.js index d41204b..dc040f0 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -13,7 +13,6 @@ console.log('[main.js] ws connection', wsConnection) const app = createApp(App) -//app.provide('socket', io(wsConnection)) app.provide('serverBaseURL', `http://${apiDomain}`) app.provide('apiBaseURL', `http://${apiDomain}/api/v1`) diff --git a/frontend/src/pages/admin/AdminPage.vue b/frontend/src/pages/admin/AdminPage.vue index 348dd48..8f82846 100644 --- a/frontend/src/pages/admin/AdminPage.vue +++ b/frontend/src/pages/admin/AdminPage.vue @@ -812,7 +812,7 @@ const stats = ref({}) const users = ref([]) const transactions = ref([]) const games = ref([]) -const matches = ref([]) // NEW +const matches = ref([]) const isLoading = ref(false) const loadMoreTrigger = ref(null) @@ -822,7 +822,7 @@ const pagination = reactive({ users: { page: 1, lastPage: 1 }, transactions: { page: 1, lastPage: 1 }, games: { page: 1, lastPage: 1 }, - matches: { page: 1, lastPage: 1 }, // NEW + matches: { page: 1, lastPage: 1 }, }) const filters = reactive({ @@ -832,7 +832,6 @@ const filters = reactive({ sort_coins: null, game_sort_direction: 'desc', game_type: null, - // Match filters match_type: null, match_outcome: null, match_date_from: '', @@ -852,7 +851,7 @@ const tabs = [ { id: 'users', name: 'User Management' }, { id: 'transactions', name: 'Transactions' }, { id: 'games', name: 'Games History' }, - { id: 'matches', name: 'Matches History' }, // NEW + { id: 'matches', name: 'Matches History' }, ] const chartData = computed(() => { @@ -1028,10 +1027,10 @@ const handleSort = async (column) => { filters.match_type = filters.match_type === null ? '9' : filters.match_type === '9' ? '3' : null } else if (column === 'date') { - filters.pot = null // Reset pot sort when sorting by date + filters.pot = null filters.match_sort_direction = filters.match_sort_direction === 'desc' ? 'asc' : 'desc' } else if (column === 'pot') { - filters.match_sort_direction = null // Reset date sort when sorting by pot + filters.match_sort_direction = null if (filters.pot === null) filters.pot = 'desc' else if (filters.pot === 'desc') filters.pot = 'asc' else filters.pot = 'desc' diff --git a/frontend/src/pages/game/MultiplayerGamePage.vue b/frontend/src/pages/game/MultiplayerGamePage.vue index c70bd9f..985a5de 100644 --- a/frontend/src/pages/game/MultiplayerGamePage.vue +++ b/frontend/src/pages/game/MultiplayerGamePage.vue @@ -1,28 +1,44 @@ @@ -94,29 +136,26 @@ const timeLeft = ref(20) const timerInterval = ref(null) const hasConnectedOnce = ref(false) -// --- STATE --- const matchState = computed(() => wsStore.gameState || { id: gameId.value }) const gameMetadata = ref({ p1_id: null, p1_name: 'Player 1', p2_id: null, - p2_name: 'Opponent' + p2_name: 'Opponent', }) const displayedTrick = ref({ playerCard: null, opponentCard: null }) const trickClearTimer = ref(null) -// --- COMPUTED --- - const myUserId = computed(() => authStore.currentUser?.id) const shouldShowBoard = computed(() => { - const s = wsStore.gameState; + const s = wsStore.gameState if (s && s.player2 && s.player2.id > 1) { - return true; + return true } - return false; + return false }) const amIHost = computed(() => { @@ -127,8 +166,12 @@ const amIHost = computed(() => { return String(p1Id) === String(myUserId.value) }) -const myDisplayName = computed(() => amIHost.value ? gameMetadata.value.p1_name : gameMetadata.value.p2_name) -const opponentDisplayName = computed(() => amIHost.value ? gameMetadata.value.p2_name : gameMetadata.value.p1_name) +const myDisplayName = computed(() => + amIHost.value ? gameMetadata.value.p1_name : gameMetadata.value.p2_name, +) +const opponentDisplayName = computed(() => + amIHost.value ? gameMetadata.value.p2_name : gameMetadata.value.p1_name, +) const mappedTrick = computed(() => { const table = wsStore.gameState?.table @@ -157,54 +200,63 @@ const opponentHand = computed(() => { return Array.from({ length: count }, (_, i) => ({ id: `opp-${i}`, suit: 'back', rank: 0 })) }) -// --- WATCHERS --- - -watch(() => wsStore.connected, (isConnected) => { - if (isConnected) { - hasConnectedOnce.value = true - wsStore.joinGame(gameId.value) - } -}, { immediate: true }) - -watch(() => wsStore.gameState, async (newState, oldState) => { - const oldP2 = oldState?.player2?.id; - const newP2 = newState?.player2?.id; - if (newP2 && newP2 > 1 && newP2 !== oldP2) { - await fetchGameMetadata(); - } -}, { deep: true }) - -watch(mappedTrick, (newVal) => { - if (newVal.playerCard || newVal.opponentCard) { - if (trickClearTimer.value) clearTimeout(trickClearTimer.value) - displayedTrick.value = { ...newVal } - } else { - const currentlyShowing = displayedTrick.value.playerCard || displayedTrick.value.opponentCard - if (currentlyShowing) { - trickClearTimer.value = setTimeout(() => { - displayedTrick.value = { playerCard: null, opponentCard: null } - }, 1500) - } else { - displayedTrick.value = { playerCard: null, opponentCard: null } +watch( + () => wsStore.connected, + (isConnected) => { + if (isConnected) { + hasConnectedOnce.value = true + wsStore.joinGame(gameId.value) } - } -}, { deep: true }) + }, + { immediate: true }, +) + +watch( + () => wsStore.gameState, + async (newState, oldState) => { + const oldP2 = oldState?.player2?.id + const newP2 = newState?.player2?.id + if (newP2 && newP2 > 1 && newP2 !== oldP2) { + await fetchGameMetadata() + } + }, + { deep: true }, +) + +watch( + mappedTrick, + (newVal) => { + if (newVal.playerCard || newVal.opponentCard) { + if (trickClearTimer.value) clearTimeout(trickClearTimer.value) + displayedTrick.value = { ...newVal } + } else { + const currentlyShowing = displayedTrick.value.playerCard || displayedTrick.value.opponentCard + if (currentlyShowing) { + trickClearTimer.value = setTimeout(() => { + displayedTrick.value = { playerCard: null, opponentCard: null } + }, 1500) + } else { + displayedTrick.value = { playerCard: null, opponentCard: null } + } + } + }, + { deep: true }, +) -// --- FIX: TIMER GHOSTING --- const startTimer = () => { if (timerInterval.value) clearInterval(timerInterval.value) if (!shouldShowBoard.value && wsStore.gameState?.status !== 'playing') { - timeLeft.value = 20; - return; + timeLeft.value = 20 + return } - // FORCE RESET to avoid showing old time for 1 frame - timeLeft.value = 20; + timeLeft.value = 20 - const startAt = wsStore.gameState?.turnStartedAt ? new Date(wsStore.gameState.turnStartedAt).getTime() : Date.now() + const startAt = wsStore.gameState?.turnStartedAt + ? new Date(wsStore.gameState.turnStartedAt).getTime() + : Date.now() - // Define update function const update = () => { const now = Date.now() const elapsed = Math.floor((now - startAt) / 1000) @@ -212,16 +264,18 @@ const startTimer = () => { if (timeLeft.value <= 0) clearInterval(timerInterval.value) } - // EXECUTE IMMEDIATELY - update(); + update() - // Then set interval timerInterval.value = setInterval(update, 1000) } -watch(() => wsStore.gameState?.currentTurn, (newTurn) => { - if (newTurn) startTimer() -}, { immediate: true }) +watch( + () => wsStore.gameState?.currentTurn, + (newTurn) => { + if (newTurn) startTimer() + }, + { immediate: true }, +) const animateDeal = async (cards) => { if (isDealing.value) return @@ -229,21 +283,23 @@ const animateDeal = async (cards) => { visibleHand.value = [] for (const card of cards) { visibleHand.value.push(card) - await new Promise(r => setTimeout(r, 150)) + await new Promise((r) => setTimeout(r, 150)) } isDealing.value = false } -watch(serverHand, (newHand) => { - if (visibleHand.value.length === 0 && newHand.length > 0 && !hasInitialDealHappened.value) { - animateDeal(newHand) - hasInitialDealHappened.value = true - } else if (!isDealing.value) { - visibleHand.value = [...newHand] - } -}, { deep: true }) - -// --- METHODS --- +watch( + serverHand, + (newHand) => { + if (visibleHand.value.length === 0 && newHand.length > 0 && !hasInitialDealHappened.value) { + animateDeal(newHand) + hasInitialDealHappened.value = true + } else if (!isDealing.value) { + visibleHand.value = [...newHand] + } + }, + { deep: true }, +) const fetchGameMetadata = async () => { try { @@ -253,27 +309,26 @@ const fetchGameMetadata = async () => { p1_id: game.player1_user_id, p1_name: game.player1?.nickname || 'Host', p2_id: game.player2_user_id, - p2_name: game.player2?.nickname || 'Opponent' + p2_name: game.player2?.nickname || 'Opponent', } } catch (e) { - console.error("Failed to load metadata", e) + console.error('Failed to load metadata', e) } } const handlePlayCard = (card) => { if (!isMyTurn.value || gameOver.value) return - visibleHand.value = visibleHand.value.filter(c => c.id !== card.id) + visibleHand.value = visibleHand.value.filter((c) => c.id !== card.id) wsStore.playCard(gameId.value, card.id) } const handleBeforeUnload = (event) => { if (shouldShowBoard.value && !gameOver.value) { - event.preventDefault(); - event.returnValue = ''; + event.preventDefault() + event.returnValue = '' } -}; +} -// --- FIX: SURRENDER LOGIC --- const handleSurrender = () => { if (confirm('Are you sure you want to surrender? You will lose.')) { wsStore.surrender(gameId.value) @@ -281,93 +336,90 @@ const handleSurrender = () => { } const handleCloseModal = async () => { - // 1. If game is still in Lobby (Pending) and I am the Host, DELETE it. if (!shouldShowBoard.value && amIHost.value && !gameOver.value) { try { - // This calls Route::delete('/{game}', ...) in your api.php - await axios.delete(`${API_BASE_URL}/games/${gameId.value}`); - console.log("Lobby cancelled and deleted."); + await axios.delete(`${API_BASE_URL}/games/${gameId.value}`) + console.log('Lobby cancelled and deleted.') } catch (e) { - console.error("Failed to delete pending game:", e); + console.error('Failed to delete pending game:', e) } } - // 2. Notify server of voluntary exit (to skip the 10s reconnect timer) if (wsStore.socket) { - wsStore.socket.emit('notify_disconnect'); + wsStore.socket.emit('notify_disconnect') } - // 3. Disconnect and go home - wsStore.disconnect(); - router.push({ name: 'home' }); + wsStore.disconnect() + router.push({ name: 'home' }) } -// --- LIFECYCLE --- onBeforeRouteLeave(async (to, from, next) => { - // CASE A: Game is Pending (Waiting Room) if (!shouldShowBoard.value && !gameOver.value) { if (amIHost.value) { - // Ask for confirmation to close the lobby - const confirmClose = window.confirm("This will cancel the lobby. Are you sure?"); + const confirmClose = window.confirm('This will cancel the lobby. Are you sure?') if (confirmClose) { try { - // Delete the game from DB so it doesn't get stuck as "Pending" - await axios.delete(`${API_BASE_URL}/games/${gameId.value}`); - } catch (e) { console.error(e) } + await axios.delete(`${API_BASE_URL}/games/${gameId.value}`) + } catch (e) { + console.error(e) + } - if (wsStore.socket) wsStore.socket.emit('notify_disconnect'); - wsStore.disconnect(); - next(); + if (wsStore.socket) wsStore.socket.emit('notify_disconnect') + wsStore.disconnect() + next() } else { - next(false); // Stay on page + next(false) } - return; + return } else { - // If I am NOT host (just a guest waiting), just leave - if (wsStore.socket) wsStore.socket.emit('notify_disconnect'); - wsStore.disconnect(); - next(); - return; + if (wsStore.socket) wsStore.socket.emit('notify_disconnect') + wsStore.disconnect() + next() + return } } - // CASE B: Game is Over (Standard exit) if (gameOver.value) { - next(); - return; + next() + return } - // CASE C: Game is Active/Playing (The Surrender Logic we built before) const answer = window.confirm( - "If you leave, you will be register has 'losing' or 'surrendered', Are you sure you want to leave?" - ); + "If you leave, you will be register has 'losing' or 'surrendered', Are you sure you want to leave?", + ) if (answer) { - // ... (Your existing surrender logic) ... - try { await wsStore.surrender(gameId.value); } catch (e) { } + try { + await wsStore.surrender(gameId.value) + } catch (e) {} if (wsStore.socket) { wsStore.socket.emit('notify_disconnect', () => { - wsStore.disconnect(); - next(); - }); - setTimeout(() => { if (wsStore.connected) { wsStore.disconnect(); next(); } }, 500); - return; + wsStore.disconnect() + next() + }) + setTimeout(() => { + if (wsStore.connected) { + wsStore.disconnect() + next() + } + }, 500) + return } - wsStore.disconnect(); - next(); + wsStore.disconnect() + next() } else { - next(false); + next(false) } -}); +}) onMounted(async () => { await fetchGameMetadata() wsStore.connect() wsStore.onTrickEnd((result) => { - const winnerName = result.winnerId == authStore.currentUser?.id ? "You" : "Opponent" + const winnerName = result.winnerId == authStore.currentUser?.id ? 'You' : 'Opponent' toast.success(`${winnerName} won the trick!`, { position: 'top-center', duration: 1500 }) }) diff --git a/frontend/src/pages/game/SinglePlayerGamePage.vue b/frontend/src/pages/game/SinglePlayerGamePage.vue index 5cabe93..f340cf0 100644 --- a/frontend/src/pages/game/SinglePlayerGamePage.vue +++ b/frontend/src/pages/game/SinglePlayerGamePage.vue @@ -1,35 +1,52 @@