From b485e19c9d634c64a4519c1dc35065d64754556e Mon Sep 17 00:00:00 2001 From: Tiago Ramos <2222055@my.ipleiria.pt> Date: Thu, 18 Dec 2025 20:29:10 +0000 Subject: [PATCH 1/6] Added routes, endpoints to get all matches, get a specific match, create a match, join a match, and update the match --- api/app/Http/Controllers/GameController.php | 20 +-- api/app/Http/Controllers/MatchController.php | 134 +++++++++++++++++++ api/app/Http/Requests/StoreMatchRequest.php | 22 +++ api/app/Models/MatchGame.php | 58 ++++++++ api/routes/api.php | 9 ++ 5 files changed, 235 insertions(+), 8 deletions(-) create mode 100644 api/app/Http/Controllers/MatchController.php create mode 100644 api/app/Http/Requests/StoreMatchRequest.php create mode 100644 api/app/Models/MatchGame.php diff --git a/api/app/Http/Controllers/GameController.php b/api/app/Http/Controllers/GameController.php index e731b2a..d798c24 100644 --- a/api/app/Http/Controllers/GameController.php +++ b/api/app/Http/Controllers/GameController.php @@ -6,6 +6,7 @@ use App\Models\Game; use Illuminate\Http\Request; use App\Http\Requests\StoreGameRequest; use Illuminate\Support\Facades\Auth; +use App\Models\MatchGame; class GameController extends Controller { @@ -34,15 +35,18 @@ class GameController extends Controller $user = Auth::user(); - $activeGame = Game::where(function ($query) use ($user) { - $query->where('player1_user_id', $user->id) - ->orWhere('player2_user_id', $user->id); - }) - ->whereIn('status', ['Pending', 'Playing']) - ->exists(); + $activeMatch = MatchGame::where(function ($q) use ($user) { + $q->where('player1_user_id', $user->id) + ->orWhere('player2_user_id', $user->id); + })->whereIn('status', ['Pending', 'Playing'])->exists(); - if ($activeGame) { - return response()->json(['message' => 'You already have an active game.'], 400); + $activeGame = Game::where(function ($q) use ($user) { + $q->where('player1_user_id', $user->id) + ->orWhere('player2_user_id', $user->id); + })->whereIn('status', ['Pending', 'Playing'])->exists(); + + if ($activeMatch || $activeGame) { + return response()->json(['message' => 'You already have an active game or match.'], 400); } $game = Game::create([ diff --git a/api/app/Http/Controllers/MatchController.php b/api/app/Http/Controllers/MatchController.php new file mode 100644 index 0000000..09f5c39 --- /dev/null +++ b/api/app/Http/Controllers/MatchController.php @@ -0,0 +1,134 @@ +with(['winner', 'player1', 'player2']); + + if ($request->has('type')) { + $query->where('type', $request->type); + } + + if ($request->has('status')) { + $query->where('status', $request->status); + } + + $query->orderBy('began_at', 'desc'); + + return response()->json($query->paginate(15)); + } + + public function store(StoreMatchRequest $request) + { + $validated = $request->validated(); + $user = Auth::user(); + + $activeMatch = MatchGame::where(function ($q) use ($user) { + $q->where('player1_user_id', $user->id) + ->orWhere('player2_user_id', $user->id); + })->whereIn('status', ['Pending', 'Playing'])->exists(); + + $activeGame = Game::where(function ($q) use ($user) { + $q->where('player1_user_id', $user->id) + ->orWhere('player2_user_id', $user->id); + })->whereIn('status', ['Pending', 'Playing'])->exists(); + + if ($activeMatch || $activeGame) { + return response()->json(['message' => 'You already have an active game or match.'], 400); + } + + $match = MatchGame::create([ + 'type' => $validated['type'], + 'status' => 'Pending', + 'player1_user_id' => $user->id, + 'player2_user_id' => $user->id, + 'winner_user_id' => $user->id, + 'loser_user_id' => $user->id, + 'stake' => $validated['stake'], + 'began_at' => now(), + 'player1_marks' => 0, + 'player2_marks' => 0, + 'player1_points' => 0, + 'player2_points' => 0, + 'custom' => null + ]); + + return response()->json([ + 'message' => 'Match created successfully', + 'match' => $match + ], 201); + } + + public function show(MatchGame $match) + { + $match->load(['player1', 'player2', 'winner']); + return response()->json($match); + } + + public function join(MatchGame $match) + { + $user = Auth::user(); + + if ($match->player1_user_id == $user->id) { + return response()->json(['message' => 'Waiting for opponent...', 'match' => $match]); + } + + if ($match->status !== 'Pending') { + return response()->json(['message' => 'Match is full or started'], 400); + } + + $activeMatch = MatchGame::where(function ($q) use ($user) { + $q->where('player1_user_id', $user->id) + ->orWhere('player2_user_id', $user->id); + })->whereIn('status', ['Pending', 'Playing'])->exists(); + + if ($activeMatch) { + return response()->json(['message' => 'You are already in another active match.'], 400); + } + + $match->update([ + 'status' => 'Playing', + 'player2_user_id' => $user->id + ]); + + return response()->json([ + 'message' => 'Joined match successfully!', + 'match' => $match + ]); + } + + public function update(Request $request, MatchGame $match) + { + if ($match->status == 'Ended') { + return response()->json(['message' => 'Match already ended'], 400); + } + + $data = $request->validate([ + 'status' => 'in:Playing,Ended,Interrupted', + 'winner_user_id' => 'exists:users,id', + 'loser_user_id' => 'exists:users,id', + 'player1_marks' => 'integer', + 'player2_marks' => 'integer', + 'player1_points' => 'integer', + 'player2_points' => 'integer', + 'total_time' => 'numeric' + ]); + + if (isset($data['status']) && $data['status'] === 'Ended') { + $data['ended_at'] = now(); + } + + $match->update($data); + + return response()->json($match); + } +} \ No newline at end of file diff --git a/api/app/Http/Requests/StoreMatchRequest.php b/api/app/Http/Requests/StoreMatchRequest.php new file mode 100644 index 0000000..b07170d --- /dev/null +++ b/api/app/Http/Requests/StoreMatchRequest.php @@ -0,0 +1,22 @@ + 'required|in:3,9', + 'stake' => 'required|integer|min:1', + ]; + } +} \ No newline at end of file diff --git a/api/app/Models/MatchGame.php b/api/app/Models/MatchGame.php new file mode 100644 index 0000000..5e6a56e --- /dev/null +++ b/api/app/Models/MatchGame.php @@ -0,0 +1,58 @@ + 'datetime', + 'ended_at' => 'datetime', + 'player1_marks' => 'integer', + 'player2_marks' => 'integer', + 'player1_points' => 'integer', + 'player2_points' => 'integer', + 'stake' => 'integer', + ]; + + public function player1() + { + return $this->belongsTo(User::class, 'player1_user_id'); + } + + public function player2() + { + return $this->belongsTo(User::class, 'player2_user_id'); + } + + public function winner() + { + return $this->belongsTo(User::class, 'winner_user_id'); + } +} \ No newline at end of file diff --git a/api/routes/api.php b/api/routes/api.php index 2b9fdcd..d0f9b55 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -3,6 +3,7 @@ use App\Http\Controllers\AuthController; use App\Http\Controllers\GameController; use App\Http\Controllers\UserController; +use App\Http\Controllers\MatchController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; @@ -26,4 +27,12 @@ Route::middleware('auth:sanctum')->group(function () { Route::post('/games/{game}/resign', [GameController::class, 'resign']); Route::get('/users/{user}/matches', [UserController::class, 'getMatches']); Route::post('games/{game}/join', [GameController::class, 'join']); +}); + +Route::middleware('auth:sanctum')->group(function () { + Route::get('matches', [MatchController::class, 'index']); + Route::post('matches', [MatchController::class, 'store']); + Route::get('matches/{match}', [MatchController::class, 'show']); + Route::post('matches/{match}/join', [MatchController::class, 'join']); + Route::put('matches/{match}', [MatchController::class, 'update']); }); \ No newline at end of file From a5663879987946c299078c5d676595c9c2a7bc37 Mon Sep 17 00:00:00 2001 From: Edd Date: Sat, 20 Dec 2025 21:59:33 +0000 Subject: [PATCH 2/6] feature implement login part2 --- api/sample-requests.http | 9 +++++++++ frontend/src/pages/login/LoginPage.vue | 18 ++++++++--------- frontend/src/stores/api.js | 14 ++++++++----- frontend/src/stores/auth.js | 14 ++++++------- frontend/src/stores/socket.js | 28 ++++++++++++++++---------- 5 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 api/sample-requests.http diff --git a/api/sample-requests.http b/api/sample-requests.http new file mode 100644 index 0000000..889ba57 --- /dev/null +++ b/api/sample-requests.http @@ -0,0 +1,9 @@ +### Get All Students +POST http://localhost:8000/api/login +content-Type: application/json +Accept: application/json + +{ + "email": "pa@mail.pt", + "password": "123" +} \ No newline at end of file diff --git a/frontend/src/pages/login/LoginPage.vue b/frontend/src/pages/login/LoginPage.vue index 7ddc2e5..0098fa7 100644 --- a/frontend/src/pages/login/LoginPage.vue +++ b/frontend/src/pages/login/LoginPage.vue @@ -64,16 +64,16 @@ const formData = ref({ const handleSubmit = async () => { - console.log(formData, authStore) - toast.promise(authStore.login(formData.value), { - loading: 'Calling API', - success: (data) => { - return `Login Sucessfull - ${data?.name}` - }, - error: (data) => `[API] Error - ${data?.response?.data?.message}` + const promise = authStore.login(formData.value) + toast.promise(promise, { + loading: 'Calling API', + success: (user) => { + router.push('/') + return `Login Successful - ${user.name}` + }, + error: (error) => + error.response?.data?.message }) - - router.push('/') } \ No newline at end of file diff --git a/frontend/src/stores/api.js b/frontend/src/stores/api.js index 3920e25..8774bd2 100644 --- a/frontend/src/stores/api.js +++ b/frontend/src/stores/api.js @@ -16,12 +16,16 @@ export const useAPIStore = defineStore('api', () => { }, }) - // AUTH const postLogin = async (credentials) => { - const response = await axios.post(`${API_BASE_URL}/login`, credentials) - token.value = response.data.token - axios.defaults.headers.common['Authorization'] = `Bearer ${token.value}` - } + const response = await axios.post(`${API_BASE_URL}/login`, credentials) + + if (!response.data.token) throw response + + token.value = response.data.token + axios.defaults.headers.common['Authorization'] = `Bearer ${token.value}` + + return response +} const postLogout = async () => { await axios.post(`${API_BASE_URL}/logout`) token.value = undefined diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js index c5bef31..43c8434 100644 --- a/frontend/src/stores/auth.js +++ b/frontend/src/stores/auth.js @@ -30,14 +30,12 @@ export const useAuthStore = defineStore('auth', () => { } const login = async (credentials) => { - await apiStore.postLogin(credentials) - const response = await apiStore.getAuthUser() - const jwtToken = response - const user = response.data - - setToken(jwtToken) - currentUser.value = response.data - return response.data + const loginResp = await apiStore.postLogin(credentials) + const userResp = await apiStore.getAuthUser() + const jwtToken = loginResp.data.token + setToken(jwtToken) + currentUser.value = userResp.data + return userResp.data } const logout = async () => { diff --git a/frontend/src/stores/socket.js b/frontend/src/stores/socket.js index 0af311c..f26b116 100644 --- a/frontend/src/stores/socket.js +++ b/frontend/src/stores/socket.js @@ -2,18 +2,24 @@ import { defineStore } from 'pinia' import { inject } from 'vue' export const useSocketStore = defineStore('socket', () => { - const socket = inject('socket') + try { + const socket = inject('socket') - const handleConnection = () => { - socket.on('connect', () => { - console.log(`[Socket] Connected -- ${socket.id}`) - }) - socket.on('disconnect', () => { - console.log(`[Socket] Disconnected -- ${socket.id}`) - }) - } + const handleConnection = () => { + try { + socket.on('connect', () => { + console.log(`[Socket] Connected -- ${socket.id}`) + }) + socket.on('disconnect', () => { + console.log(`[Socket] Disconnected -- ${socket.id}`) + }) + } catch (error) {} + } - return { - handleConnection, + return { + handleConnection, + } + } catch (error) { } + }) From 2734e25cce3af28fc9e3658b4a9e8d3ba427955b Mon Sep 17 00:00:00 2001 From: Edd Date: Sat, 20 Dec 2025 22:26:22 +0000 Subject: [PATCH 3/6] feature implement login final changes --- frontend/src/pages/login/LoginPage.vue | 15 ++++++------ frontend/src/stores/socket.js | 33 +++++++++++++------------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/frontend/src/pages/login/LoginPage.vue b/frontend/src/pages/login/LoginPage.vue index 0098fa7..9dd4ce2 100644 --- a/frontend/src/pages/login/LoginPage.vue +++ b/frontend/src/pages/login/LoginPage.vue @@ -66,14 +66,13 @@ const formData = ref({ const handleSubmit = async () => { const promise = authStore.login(formData.value) toast.promise(promise, { - loading: 'Calling API', - success: (user) => { - router.push('/') - return `Login Successful - ${user.name}` - }, - error: (error) => - error.response?.data?.message + loading: 'Calling API', + success: (user) => { + router.push('/') + return `Login Successful - ${user.name}` + }, + error: (error) => + error.response?.data?.message }) - router.push('/') } \ No newline at end of file diff --git a/frontend/src/stores/socket.js b/frontend/src/stores/socket.js index f26b116..b762d59 100644 --- a/frontend/src/stores/socket.js +++ b/frontend/src/stores/socket.js @@ -2,24 +2,23 @@ import { defineStore } from 'pinia' import { inject } from 'vue' export const useSocketStore = defineStore('socket', () => { - try { - const socket = inject('socket') + const socket = inject('socket') - const handleConnection = () => { - try { - socket.on('connect', () => { - console.log(`[Socket] Connected -- ${socket.id}`) - }) - socket.on('disconnect', () => { - console.log(`[Socket] Disconnected -- ${socket.id}`) - }) - } catch (error) {} + const handleConnection = () => { + try { + socket.on('connect', () => { + console.log(`[Socket] Connected -- ${socket.id}`) + }) + socket.on('disconnect', () => { + console.log(`[Socket] Disconnected -- ${socket.id}`) + }) + } catch (error) { + console.error('[Socket] Connection error:', error) } - - return { - handleConnection, - } - } catch (error) { } - + + return { + handleConnection, + } + }) From c03d49e7e4c99c607b8b26080e9e7bf0fe41564a Mon Sep 17 00:00:00 2001 From: Edd Date: Sun, 21 Dec 2025 02:37:46 +0000 Subject: [PATCH 4/6] removed vscode preferences --- .gitignore | 3 ++- .vscode/settings.json | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 6638d86..ff437dd 100644 --- a/.gitignore +++ b/.gitignore @@ -100,4 +100,5 @@ Desktop.ini # If you have any local secrets or files not to track, add them here: # /path/to/some/file -package-lock.json \ No newline at end of file +package-lock.json +.vscode \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 6b0e5ab..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "postman.settings.dotenv-detection-notification-visibility": false -} \ No newline at end of file From c78c5d2784571221386e3b5e74f9d3a2250d12a0 Mon Sep 17 00:00:00 2001 From: FernandoJVideira <03.pleaser-minster@icloud.com> Date: Tue, 23 Dec 2025 12:34:19 +0000 Subject: [PATCH 5/6] fix: api.php cleanup --- api/routes/api.php | 46 +++++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/api/routes/api.php b/api/routes/api.php index d0f9b55..6aeaa71 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -2,37 +2,33 @@ use App\Http\Controllers\AuthController; use App\Http\Controllers\GameController; -use App\Http\Controllers\UserController; use App\Http\Controllers\MatchController; +use App\Http\Controllers\UserController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; - -# Authentication Routes +// Authentication Routes Route::post('/login', [AuthController::class, 'login']); - -Route::middleware('auth:sanctum')->group(function () { - Route::get('/users/me', function (Request $request) { - return $request->user(); - }); - Route::post('logout', [AuthController::class, 'logout']); -}); - Route::post('/register', [AuthController::class, 'register']); -# Game Routes - Protected Route::middleware('auth:sanctum')->group(function () { - Route::apiResource('games', GameController::class); - Route::post('games', [GameController::class, 'store']); - Route::post('/games/{game}/resign', [GameController::class, 'resign']); - Route::get('/users/{user}/matches', [UserController::class, 'getMatches']); - Route::post('games/{game}/join', [GameController::class, 'join']); -}); + Route::post('logout', [AuthController::class, 'logout']); -Route::middleware('auth:sanctum')->group(function () { - Route::get('matches', [MatchController::class, 'index']); - Route::post('matches', [MatchController::class, 'store']); - Route::get('matches/{match}', [MatchController::class, 'show']); - Route::post('matches/{match}/join', [MatchController::class, 'join']); - Route::put('matches/{match}', [MatchController::class, 'update']); -}); \ No newline at end of file + Route::prefix('users')->group(function () { + Route::get('/me', function (Request $request) { + return $request->user(); + }); + Route::get('/{user}/matches', [UserController::class, 'getMatches']); + }); + + Route::prefix('games')->group(function () { + Route::apiResource('/', GameController::class)->parameters(['' => 'game']); + Route::post('/{game}/resign', [GameController::class, 'resign']); + Route::post('/{game}/join', [GameController::class, 'join']); + }); + + Route::prefix('matches')->group(function () { + Route::apiResource('/', MatchController::class)->parameters(['' => 'match']); + Route::post('/{match}/join', [MatchController::class, 'join']); + }); +}); From 10508cc0b191230b6e44a3d4ade335eab5c7fd73 Mon Sep 17 00:00:00 2001 From: FernandoJVideira <03.pleaser-minster@icloud.com> Date: Tue, 23 Dec 2025 12:36:56 +0000 Subject: [PATCH 6/6] fix: GameControler create game fixed --- api/app/Http/Controllers/GameController.php | 151 ++++++++++++-------- 1 file changed, 93 insertions(+), 58 deletions(-) diff --git a/api/app/Http/Controllers/GameController.php b/api/app/Http/Controllers/GameController.php index d798c24..bc04482 100644 --- a/api/app/Http/Controllers/GameController.php +++ b/api/app/Http/Controllers/GameController.php @@ -12,17 +12,25 @@ class GameController extends Controller { public function index(Request $request) { - $query = Game::query()->with(['winner', 'player1', 'player2']); + $query = Game::query()->with(["winner", "player1", "player2"]); - if ($request->has('type') && in_array($request->type, ['3', '9'])) { - $query->where('type', $request->type); + if ($request->has("type") && in_array($request->type, ["3", "9"])) { + $query->where("type", $request->type); } - if ($request->has('status') && in_array($request->status, ['Pending', 'Playing', 'Ended', 'Interrupted'])) { - $query->where('status', $request->status); + if ( + $request->has("status") && + in_array($request->status, [ + "Pending", + "Playing", + "Ended", + "Interrupted", + ]) + ) { + $query->where("status", $request->status); } - $query->orderBy('began_at', 'desc'); + $query->orderBy("began_at", "desc"); return response()->json($query->paginate(15)); } @@ -30,42 +38,56 @@ class GameController extends Controller public function store(Request $request) { $validated = $request->validate([ - 'type' => 'required|in:3,9', + "type" => "required|in:3,9", ]); $user = Auth::user(); $activeMatch = MatchGame::where(function ($q) use ($user) { - $q->where('player1_user_id', $user->id) - ->orWhere('player2_user_id', $user->id); - })->whereIn('status', ['Pending', 'Playing'])->exists(); + $q->where("player1_user_id", $user->id)->orWhere( + "player2_user_id", + $user->id, + ); + }) + ->whereIn("status", ["Pending", "Playing"]) + ->exists(); $activeGame = Game::where(function ($q) use ($user) { - $q->where('player1_user_id', $user->id) - ->orWhere('player2_user_id', $user->id); - })->whereIn('status', ['Pending', 'Playing'])->exists(); + $q->where("player1_user_id", $user->id)->orWhere( + "player2_user_id", + $user->id, + ); + }) + ->whereIn("status", ["Pending", "Playing"]) + ->exists(); if ($activeMatch || $activeGame) { - return response()->json(['message' => 'You already have an active game or match.'], 400); + return response()->json( + ["message" => "You already have an active game or match."], + 400, + ); } $game = Game::create([ - 'type' => $validated['type'], - 'status' => 'Pending', - 'player1_user_id' => $user->id, - 'player2_user_id' => $user->id, - 'winner_user_id' => $user->id, - 'loser_user_id' => $user->id, - 'began_at' => now(), - 'player1_points' => 0, - 'player2_points' => 0, - 'custom' => null + "type" => $validated["type"], + "status" => "Pending", + "player1_user_id" => $user->id, + "player2_user_id" => null, + "winner_user_id" => null, + "loser_user_id" => null, + "began_at" => now(), + "player1_points" => 0, + "player2_points" => 0, + "custom" => null, ]); - return response()->json([ - 'message' => 'Multiplayer game room created', - 'game' => $game - ], 201); + return response()->json( + [ + "message" => "Multiplayer game room created", + "game" => $game, + ], + 201, + ); } public function join(Game $game) @@ -73,66 +95,79 @@ class GameController extends Controller $user = Auth::user(); if ($game->player1_user_id == $user->id) { - return response()->json([ - 'message' => 'Waiting for opponent...', - 'game' => $game - ], 200); + return response()->json( + [ + "message" => "Waiting for opponent...", + "game" => $game, + ], + 200, + ); } - if ($game->status !== 'Pending') { - return response()->json(['message' => 'Game is full or already started'], 400); + if ($game->status !== "Pending") { + return response()->json( + ["message" => "Game is full or already started"], + 400, + ); } $activeGame = Game::where(function ($query) use ($user) { - $query->where('player1_user_id', $user->id) - ->orWhere('player2_user_id', $user->id); + $query + ->where("player1_user_id", $user->id) + ->orWhere("player2_user_id", $user->id); }) - ->whereIn('status', ['Pending', 'Playing']) - ->exists(); + ->whereIn("status", ["Pending", "Playing"]) + ->exists(); if ($activeGame) { - return response()->json(['message' => 'You are already in another active game.'], 400); + return response()->json( + ["message" => "You are already in another active game."], + 400, + ); } $game->update([ - 'status' => 'Playing', - 'player2_user_id' => $user->id, + "status" => "Playing", + "player2_user_id" => $user->id, ]); - return response()->json([ - 'message' => 'Joined successfully! Game starts now.', - 'game' => $game - ], 200); + return response()->json( + [ + "message" => "Joined successfully! Game starts now.", + "game" => $game, + ], + 200, + ); } public function show(Game $game) { - $game->load(['player1', 'player2', 'winner']); + $game->load(["player1", "player2", "winner"]); return response()->json($game); } public function update(Request $request, Game $game) { - if ($game->status == 'Ended') { - return response()->json(['message' => 'Game already ended'], 400); + if ($game->status == "Ended") { + return response()->json(["message" => "Game already ended"], 400); } $data = $request->validate([ - 'status' => 'in:Playing,Ended,Interrupted', - 'winner_user_id' => 'exists:users,id', - 'loser_user_id' => 'exists:users,id', - 'player1_points' => 'integer', - 'player2_points' => 'integer', - 'is_draw' => 'boolean', - 'total_time' => 'numeric' + "status" => "in:Playing,Ended,Interrupted", + "winner_user_id" => "exists:users,id", + "loser_user_id" => "exists:users,id", + "player1_points" => "integer", + "player2_points" => "integer", + "is_draw" => "boolean", + "total_time" => "numeric", ]); - if (isset($data['status']) && $data['status'] === 'Ended') { - $data['ended_at'] = now(); + if (isset($data["status"]) && $data["status"] === "Ended") { + $data["ended_at"] = now(); } $game->update($data); return response()->json($game); } -} \ No newline at end of file +}