From 1d5100fbdf59539a21cf222335c8ff1ef01ecaab Mon Sep 17 00:00:00 2001 From: AfonsoCMSousa Date: Thu, 11 Dec 2025 21:14:54 +0000 Subject: [PATCH 1/6] feat:initial commit --- frontend/src/pages/home/HomePage.vue | 2 +- frontend/src/pages/user/UserPage.vue | 37 ++++++++++++++++++++++++++ frontend/src/router/index.js | 5 ++++ frontend/src/stores/user.js | 39 ++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/user/UserPage.vue create mode 100644 frontend/src/stores/user.js diff --git a/frontend/src/pages/home/HomePage.vue b/frontend/src/pages/home/HomePage.vue index 42f95c7..939fae6 100644 --- a/frontend/src/pages/home/HomePage.vue +++ b/frontend/src/pages/home/HomePage.vue @@ -8,4 +8,4 @@ -` \ No newline at end of file +` diff --git a/frontend/src/pages/user/UserPage.vue b/frontend/src/pages/user/UserPage.vue new file mode 100644 index 0000000..e952503 --- /dev/null +++ b/frontend/src/pages/user/UserPage.vue @@ -0,0 +1,37 @@ + + + + + + diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index ee115e2..0c65ab4 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -2,6 +2,7 @@ import HomePage from '@/pages/home/HomePage.vue' import LoginPage from '@/pages/login/LoginPage.vue' import LaravelPage from '@/pages/testing/LaravelPage.vue' import WebsocketsPage from '@/pages/testing/WebsocketsPage.vue' +import UserPage from '@/pages/user/UserPage.vue' import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ @@ -15,6 +16,10 @@ const router = createRouter({ path: '/login', component: LoginPage, }, + { + path: '/user', + component: UserPage, + }, { path: '/testing', children: [ diff --git a/frontend/src/stores/user.js b/frontend/src/stores/user.js new file mode 100644 index 0000000..2a678ea --- /dev/null +++ b/frontend/src/stores/user.js @@ -0,0 +1,39 @@ +import { defineStore } from 'pinia' +import axios from 'axios' + +export const useUserStore = defineStore('user', { + state: () => ({ + user: null, + token: localStorage.getItem('token') || null, + }), + + actions: { + async fetchUser() { + if (!this.token) return + + try { + const response = await axios.get('/api/users/me', { + headers: { + Authorization: `Bearer ${this.token}` + } + }) + + this.user = response.data + } catch (error) { + console.error("Failed to fetch user") + } + }, + + setToken(token) { + this.token = token + localStorage.setItem('token', token) + }, + + logout() { + this.token = null + this.user = null + localStorage.removeItem('token') + } + } +}) + -- 2.54.0 From d1dd2c00f6b89b110f70e235dfcae53262cccebe Mon Sep 17 00:00:00 2001 From: AfonsoCMSousa Date: Thu, 18 Dec 2025 21:04:49 +0000 Subject: [PATCH 2/6] SyntaxSquad/DADProject#9 feat: User info dysplayed in /users/ --- frontend/src/components/layout/NavBar.vue | 3 + frontend/src/main.js | 4 + frontend/src/pages/login/LoginPage.vue | 111 +++++++-------- frontend/src/pages/user/UserPage.vue | 159 +++++++++++++++++++--- frontend/src/stores/api.js | 12 ++ frontend/src/stores/auth.js | 62 +++++++-- frontend/src/stores/user.js | 47 +++++-- 7 files changed, 305 insertions(+), 93 deletions(-) diff --git a/frontend/src/components/layout/NavBar.vue b/frontend/src/components/layout/NavBar.vue index cc66d7a..533d0c4 100644 --- a/frontend/src/components/layout/NavBar.vue +++ b/frontend/src/components/layout/NavBar.vue @@ -24,6 +24,9 @@ Logout + + Profile + diff --git a/frontend/src/main.js b/frontend/src/main.js index 27177c5..a14e65a 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,6 +1,7 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' import { io } from 'socket.io-client' +import { useAuthStore } from '@/stores/auth' import App from './App.vue' import router from './router' @@ -20,4 +21,7 @@ app.provide('apiBaseURL', `http://${apiDomain}/api`) app.use(createPinia()) app.use(router) +const authStore = useAuthStore() +authStore.initAuth() + app.mount('#app') diff --git a/frontend/src/pages/login/LoginPage.vue b/frontend/src/pages/login/LoginPage.vue index 7ddc2e5..e1afd46 100644 --- a/frontend/src/pages/login/LoginPage.vue +++ b/frontend/src/pages/login/LoginPage.vue @@ -1,55 +1,55 @@ \ No newline at end of file + diff --git a/frontend/src/pages/user/UserPage.vue b/frontend/src/pages/user/UserPage.vue index e952503..41570ef 100644 --- a/frontend/src/pages/user/UserPage.vue +++ b/frontend/src/pages/user/UserPage.vue @@ -2,36 +2,163 @@

My Profile

-
-

Name: {{ userStore.user.name }}

-

Email: {{ userStore.user.email }}

-

Joined: {{ new Date(userStore.user.created_at).toLocaleString() }}

+ +
+

Loading your profile...

-
-

Loading... 読み込み中

+ +
+

Error: {{ error }}

+ +
+ + + + + +
+

No user data or not logged in. Please log in to view your profile.

- - diff --git a/frontend/src/stores/api.js b/frontend/src/stores/api.js index 3920e25..8ff3ba8 100644 --- a/frontend/src/stores/api.js +++ b/frontend/src/stores/api.js @@ -1,3 +1,4 @@ +// src/stores/api.js import { defineStore } from 'pinia' import axios from 'axios' import { inject, ref } from 'vue' @@ -21,7 +22,10 @@ export const useAPIStore = defineStore('api', () => { const response = await axios.post(`${API_BASE_URL}/login`, credentials) 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 @@ -53,11 +57,19 @@ export const useAPIStore = defineStore('api', () => { return axios.get(`${API_BASE_URL}/games?${queryParams}`) } + const initToken = (storedToken) => { + if (storedToken) { + token.value = storedToken + axios.defaults.headers.common['Authorization'] = `Bearer ${storedToken}` + } + } + return { postLogin, postLogout, getAuthUser, getGames, gameQueryParameters, + initToken, } }) diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js index c5bef31..1cec1b3 100644 --- a/frontend/src/stores/auth.js +++ b/frontend/src/stores/auth.js @@ -1,6 +1,8 @@ +// src/stores/auth.js import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { useAPIStore } from './api' +import axios from 'axios' export const useAuthStore = defineStore('auth', () => { const apiStore = useAPIStore() @@ -20,8 +22,10 @@ export const useAuthStore = defineStore('auth', () => { token.value = jwtToken if (jwtToken) { localStorage.setItem('token', jwtToken) + axios.defaults.headers.common['Authorization'] = `Bearer ${jwtToken}` } else { localStorage.removeItem('token') + delete axios.defaults.headers.common['Authorization'] } } @@ -30,20 +34,57 @@ 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 + try { + // postLogin already sets the token and axios headers, and returns the response + const loginResponse = await apiStore.postLogin(credentials) - setToken(jwtToken) - currentUser.value = response.data - return response.data + // Extract the token from the response + const jwtToken = loginResponse.data.token + + // Store token in auth store (for localStorage) + setToken(jwtToken) + + // Use the user data from login response (it's already there!) + currentUser.value = loginResponse.data.user + + return currentUser.value + } catch (error) { + // Clear everything on login failure + setToken(null) + currentUser.value = undefined + throw error + } } const logout = async () => { - await apiStore.postLogout() - currentUser.value = undefined - setToken(null) + try { + await apiStore.postLogout() + } catch (error) { + console.error('Logout API call failed:', error) + } finally { + currentUser.value = undefined + setToken(null) + } + } + + // Initialize auth from localStorage on app start + const initAuth = async () => { + if (token.value) { + axios.defaults.headers.common['Authorization'] = `Bearer ${token.value}` + apiStore.initToken(token.value) + + // Try to fetch current user if we don't have it + if (!currentUser.value) { + try { + const userResponse = await apiStore.getAuthUser() + currentUser.value = userResponse.data + } catch (error) { + console.error('Failed to restore user session:', error) + // Clear invalid token + setToken(null) + } + } + } } return { @@ -55,5 +96,6 @@ export const useAuthStore = defineStore('auth', () => { getToken, login, logout, + initAuth, } }) diff --git a/frontend/src/stores/user.js b/frontend/src/stores/user.js index 2a678ea..ea53013 100644 --- a/frontend/src/stores/user.js +++ b/frontend/src/stores/user.js @@ -1,39 +1,58 @@ import { defineStore } from 'pinia' import axios from 'axios' +import { useAuthStore } from './auth' export const useUserStore = defineStore('user', { state: () => ({ user: null, - token: localStorage.getItem('token') || null, + loading: false, + error: null }), + getters: { + currentUser: (state) => state.user + }, + actions: { - async fetchUser() { - if (!this.token) return + async fetchUserProfile() { + const authStore = useAuthStore() + const token = authStore.getToken() + + if (!token) { + this.error = 'No authentication token found' + throw new Error('No authentication token found') + } + + this.loading = true + this.error = null try { const response = await axios.get('/api/users/me', { headers: { - Authorization: `Bearer ${this.token}` + Authorization: `Bearer ${token}`, + Accept: 'application/json' } }) this.user = response.data + return response.data } catch (error) { - console.error("Failed to fetch user") + this.error = error.response?.data?.message || 'Failed to fetch user profile' + + if (error.response?.status === 401) { + authStore.logout() + } + + throw error + } finally { + this.loading = false } }, - setToken(token) { - this.token = token - localStorage.setItem('token', token) - }, - - logout() { - this.token = null + clearUser() { this.user = null - localStorage.removeItem('token') + this.error = null + this.loading = false } } }) - -- 2.54.0 From d903f96d57987a54b451ce3c4ea2abf8f97742a5 Mon Sep 17 00:00:00 2001 From: AfonsoCMSousa Date: Tue, 23 Dec 2025 17:24:25 +0000 Subject: [PATCH 3/6] SyntaxSquad/DADProject#9 Actuall User View --- frontend/src/pages/user/UserPage.vue | 461 ++++++++++++++++++++++----- 1 file changed, 388 insertions(+), 73 deletions(-) diff --git a/frontend/src/pages/user/UserPage.vue b/frontend/src/pages/user/UserPage.vue index 41570ef..2af39e8 100644 --- a/frontend/src/pages/user/UserPage.vue +++ b/frontend/src/pages/user/UserPage.vue @@ -1,40 +1,149 @@ @@ -53,7 +162,12 @@ const loading = ref(false) const error = ref(null) const formatDate = (dateString) => { - return new Date(dateString).toLocaleString() + const date = new Date(dateString) + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }) } const fetchProfile = async () => { @@ -94,71 +208,272 @@ onMounted(async () => { -- 2.54.0 From b82b4d5b592e09cf393beffbfcbe1e646949f736 Mon Sep 17 00:00:00 2001 From: AfonsoCMSousa Date: Tue, 23 Dec 2025 18:21:04 +0000 Subject: [PATCH 4/6] SyntaxSquad/DADProject#9 feat: Added profile change and name/password changes. --- api/routes/api.php | 61 +++ frontend/src/main.js | 5 +- frontend/src/pages/user/UserPage.vue | 590 +++++++++++++++++++++++---- frontend/src/stores/auth.js | 2 + 4 files changed, 572 insertions(+), 86 deletions(-) diff --git a/api/routes/api.php b/api/routes/api.php index 6aeaa71..e3a0fe3 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -18,6 +18,67 @@ Route::middleware('auth:sanctum')->group(function () { Route::get('/me', function (Request $request) { return $request->user(); }); + + // Update user profile (name and nickname) + Route::put('/me', function (Request $request) { + $request->validate([ + 'name' => 'required|string|max:255', + 'nickname' => 'required|string|max:255|unique:users,nickname,' . $request->user()->id, + ]); + + $user = $request->user(); + $user->name = $request->name; + $user->nickname = $request->nickname; + $user->save(); + + return response()->json($user); + }); + + // Change password + Route::put('/me/password', function (Request $request) { + $request->validate([ + 'current_password' => 'required', + 'new_password' => 'required|min:8|confirmed', + ]); + + $user = $request->user(); + + if (!\Hash::check($request->current_password, $user->password)) { + return response()->json([ + 'message' => 'Current password is incorrect' + ], 422); + } + + $user->password = \Hash::make($request->new_password); + $user->save(); + + return response()->json([ + 'message' => 'Password changed successfully' + ]); + }); + + // Upload avatar + Route::post('/me/avatar', function (Request $request) { + $request->validate([ + 'avatar' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048', + ]); + + $user = $request->user(); + + if ($user->photo_avatar_filename) { + \Storage::disk('public')->delete('photos_avatars/' . $user->photo_avatar_filename); + } + + $file = $request->file('avatar'); + $filename = str_pad($user->id, 5, '0', STR_PAD_LEFT) . '_' . \Str::random(10) . '.' . $file->extension(); + $file->storeAs('photos_avatars', $filename, 'public'); + + $user->photo_avatar_filename = $filename; + $user->save(); + + return response()->json($user); + }); + Route::get('/{user}/matches', [UserController::class, 'getMatches']); }); diff --git a/frontend/src/main.js b/frontend/src/main.js index a14e65a..2d5c398 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,7 +1,6 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' import { io } from 'socket.io-client' -import { useAuthStore } from '@/stores/auth' import App from './App.vue' import router from './router' @@ -21,7 +20,5 @@ app.provide('apiBaseURL', `http://${apiDomain}/api`) app.use(createPinia()) app.use(router) -const authStore = useAuthStore() -authStore.initAuth() - app.mount('#app') + diff --git a/frontend/src/pages/user/UserPage.vue b/frontend/src/pages/user/UserPage.vue index 2af39e8..147a64d 100644 --- a/frontend/src/pages/user/UserPage.vue +++ b/frontend/src/pages/user/UserPage.vue @@ -30,13 +30,26 @@
{{ authStore.currentUser.name.charAt(0).toUpperCase() }}
+ +

{{ authStore.currentUser.name }}

@@ -52,91 +65,209 @@
-
-
-
- - - - -
-
- -

{{ authStore.currentUser.email }}

-
-
+ +
+ + + +
-
-
- - - - + +
+
+
+
+ + + + +
+
+ +

{{ authStore.currentUser.email }}

+
-
- -

{{ authStore.currentUser.type === 'P' ? 'Player' : 'Other' }}

-
-
-
-
- - - - - - +
+
+ + + + +
+
+ +

{{ authStore.currentUser.type === 'P' ? 'Player' : 'Other' }}

+
-
- -

{{ formatDate(authStore.currentUser.created_at) }}

-
-
-
-
- - - - +
+
+ + + + + + +
+
+ +

{{ formatDate(authStore.currentUser.created_at) }}

+
-
- -

- - {{ authStore.currentUser.blocked ? 'Blocked' : 'Active' }} - -

-
-
-
-
- - - - +
+
+ + + + +
+
+ +

+ + {{ authStore.currentUser.blocked ? 'Blocked' : 'Active' }} + +

+
-
- -

{{ formatDate(authStore.currentUser.email_verified_at) }}

-
-
-
-
- - - - +
+
+ + + + +
+
+ +

{{ formatDate(authStore.currentUser.email_verified_at) }}

+
-
- -

{{ formatDate(authStore.currentUser.updated_at) }}

+ +
+
+ + + + +
+
+ +

{{ formatDate(authStore.currentUser.updated_at) }}

+
+ + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ {{ profileMessage }} +
+
+
+ + +
+
+
+ + +
+ +
+ + + Password must be at least 8 characters long +
+ +
+ + +
+ +
+ + +
+ +
+ {{ passwordMessage }} +
+
+
@@ -149,17 +280,40 @@ @@ -210,14 +465,12 @@ onMounted(async () => { diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js index 43c8434..14f60ea 100644 --- a/frontend/src/stores/auth.js +++ b/frontend/src/stores/auth.js @@ -5,6 +5,8 @@ import { useAPIStore } from './api' export const useAuthStore = defineStore('auth', () => { const apiStore = useAPIStore() + + const currentUser = ref(undefined) const token = ref(localStorage.getItem('token') || null) -- 2.54.0 From 18cef8da4b9a848dd4cbe079cd15469cb7b0a001 Mon Sep 17 00:00:00 2001 From: AfonsoCMSousa Date: Tue, 23 Dec 2025 18:42:09 +0000 Subject: [PATCH 5/6] SyntaxSquad/DADProject#9 fix: Used Tailwind Classes and Exported inline CSS into "UserPage.css" --- frontend/src/pages/user/UserPage.css | 19 + frontend/src/pages/user/UserPage.vue | 661 +++++---------------------- 2 files changed, 135 insertions(+), 545 deletions(-) create mode 100644 frontend/src/pages/user/UserPage.css diff --git a/frontend/src/pages/user/UserPage.css b/frontend/src/pages/user/UserPage.css new file mode 100644 index 0000000..fd10a93 --- /dev/null +++ b/frontend/src/pages/user/UserPage.css @@ -0,0 +1,19 @@ +/* UserPage.css - Custom styles that can't be done with Tailwind */ + +.spinner { + width: 40px; + height: 40px; + border: 3px solid #f0f0f0; + border-top: 3px solid #000; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/frontend/src/pages/user/UserPage.vue b/frontend/src/pages/user/UserPage.vue index 147a64d..05b6db6 100644 --- a/frontend/src/pages/user/UserPage.vue +++ b/frontend/src/pages/user/UserPage.vue @@ -1,22 +1,24 @@ @@ -463,443 +472,5 @@ onMounted(async () => { -- 2.54.0 From c34d74db46769f7a1accaa29de3f0632795f85b6 Mon Sep 17 00:00:00 2001 From: AfonsoCMSousa Date: Tue, 23 Dec 2025 18:53:11 +0000 Subject: [PATCH 6/6] SyntaxSquad/DADProject#9 fixforthefix: Remove anoying gray bg --- frontend/src/pages/user/UserPage.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/user/UserPage.vue b/frontend/src/pages/user/UserPage.vue index 05b6db6..0de4d6c 100644 --- a/frontend/src/pages/user/UserPage.vue +++ b/frontend/src/pages/user/UserPage.vue @@ -1,5 +1,5 @@