From 2ecc6242af91e6bd63b11d4acecb8e8772055b46 Mon Sep 17 00:00:00 2001 From: Rogaix Date: Sat, 23 Aug 2025 00:49:05 +0200 Subject: [PATCH 01/12] fix(ui/components): remove grid-template-columns from #app styles to center router views --- ui/apps/portal/src/assets/main.css | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/apps/portal/src/assets/main.css b/ui/apps/portal/src/assets/main.css index 90bc218..9d60e79 100644 --- a/ui/apps/portal/src/assets/main.css +++ b/ui/apps/portal/src/assets/main.css @@ -38,7 +38,6 @@ a, #app { display: grid; - grid-template-columns: 1fr 1fr; padding: 0 2rem; } } From cdebc898dbfc527bd6e4ca1ceb01f58fa242a5d8 Mon Sep 17 00:00:00 2001 From: Rogaix Date: Sat, 23 Aug 2025 01:01:59 +0200 Subject: [PATCH 02/12] feat: add UsersView component with user loading and error handling --- ui/apps/portal/src/views/users/UsersView.vue | 50 ++++++++++++++++++++ ui/apps/portal/src/views/users/index.ts | 1 + ui/apps/portal/src/vue-shims.d.ts | 5 ++ 3 files changed, 56 insertions(+) create mode 100644 ui/apps/portal/src/views/users/UsersView.vue create mode 100644 ui/apps/portal/src/views/users/index.ts create mode 100644 ui/apps/portal/src/vue-shims.d.ts diff --git a/ui/apps/portal/src/views/users/UsersView.vue b/ui/apps/portal/src/views/users/UsersView.vue new file mode 100644 index 0000000..635ab7a --- /dev/null +++ b/ui/apps/portal/src/views/users/UsersView.vue @@ -0,0 +1,50 @@ + + + diff --git a/ui/apps/portal/src/views/users/index.ts b/ui/apps/portal/src/views/users/index.ts new file mode 100644 index 0000000..d6e35d6 --- /dev/null +++ b/ui/apps/portal/src/views/users/index.ts @@ -0,0 +1 @@ +export { default as UsersView } from './UsersView.vue' diff --git a/ui/apps/portal/src/vue-shims.d.ts b/ui/apps/portal/src/vue-shims.d.ts new file mode 100644 index 0000000..ac1ded7 --- /dev/null +++ b/ui/apps/portal/src/vue-shims.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} From 957b8dd5be100e3b5dcae2b1ea542316b4b915ba Mon Sep 17 00:00:00 2001 From: Rogaix Date: Sat, 23 Aug 2025 01:02:23 +0200 Subject: [PATCH 03/12] feat: add routing for UsersView and update HomeView with navigation link --- ui/apps/portal/src/router/index.ts | 7 +++++++ ui/apps/portal/src/views/HomeView.vue | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/ui/apps/portal/src/router/index.ts b/ui/apps/portal/src/router/index.ts index 29788a1..ba95e29 100644 --- a/ui/apps/portal/src/router/index.ts +++ b/ui/apps/portal/src/router/index.ts @@ -1,5 +1,7 @@ import { createRouter, createWebHistory } from 'vue-router' + import HomeView from '../views/HomeView.vue' +import { UsersView } from '../views/users' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -9,6 +11,11 @@ const router = createRouter({ name: 'home', component: HomeView, }, + { + path: '/users', + name: 'users', + component: UsersView, + }, ], }) diff --git a/ui/apps/portal/src/views/HomeView.vue b/ui/apps/portal/src/views/HomeView.vue index c9997f4..5d769ed 100644 --- a/ui/apps/portal/src/views/HomeView.vue +++ b/ui/apps/portal/src/views/HomeView.vue @@ -1,3 +1,8 @@ From 20f35bb3df1c2a3acb80e294a1a59e75206c826b Mon Sep 17 00:00:00 2001 From: Rogaix Date: Sat, 23 Aug 2025 01:46:34 +0200 Subject: [PATCH 04/12] feat: implement user creation functionality and refactor user fetching logic into composable --- ui/apps/portal/src/views/users/UsersView.vue | 27 +++------- .../src/views/users/create/CreateUser.vue | 38 ++++++++++++++ .../portal/src/views/users/create/index.ts | 1 + ui/apps/portal/src/views/users/index.ts | 1 + ui/apps/portal/src/views/users/useUsers.ts | 51 +++++++++++++++++++ 5 files changed, 98 insertions(+), 20 deletions(-) create mode 100644 ui/apps/portal/src/views/users/create/CreateUser.vue create mode 100644 ui/apps/portal/src/views/users/create/index.ts create mode 100644 ui/apps/portal/src/views/users/useUsers.ts diff --git a/ui/apps/portal/src/views/users/UsersView.vue b/ui/apps/portal/src/views/users/UsersView.vue index 635ab7a..fa4eb84 100644 --- a/ui/apps/portal/src/views/users/UsersView.vue +++ b/ui/apps/portal/src/views/users/UsersView.vue @@ -22,29 +22,16 @@
No users found.
+ +onMounted(fetchUsers) + \ No newline at end of file diff --git a/ui/apps/portal/src/views/users/create/CreateUser.vue b/ui/apps/portal/src/views/users/create/CreateUser.vue new file mode 100644 index 0000000..951107b --- /dev/null +++ b/ui/apps/portal/src/views/users/create/CreateUser.vue @@ -0,0 +1,38 @@ + + + diff --git a/ui/apps/portal/src/views/users/create/index.ts b/ui/apps/portal/src/views/users/create/index.ts new file mode 100644 index 0000000..cc0d2e3 --- /dev/null +++ b/ui/apps/portal/src/views/users/create/index.ts @@ -0,0 +1 @@ +export { default as CreateUser } from './CreateUser.vue' diff --git a/ui/apps/portal/src/views/users/index.ts b/ui/apps/portal/src/views/users/index.ts index d6e35d6..432bbf0 100644 --- a/ui/apps/portal/src/views/users/index.ts +++ b/ui/apps/portal/src/views/users/index.ts @@ -1 +1,2 @@ export { default as UsersView } from './UsersView.vue' +export { CreateUser } from './create' diff --git a/ui/apps/portal/src/views/users/useUsers.ts b/ui/apps/portal/src/views/users/useUsers.ts new file mode 100644 index 0000000..d4bf78e --- /dev/null +++ b/ui/apps/portal/src/views/users/useUsers.ts @@ -0,0 +1,51 @@ +import { ref } from 'vue' +import { apiService, type User } from '@/services/api' + +export function useUsers() { + const users = ref([]) + const loading = ref(true) + const error = ref('') + + const fetchUsers = async () => { + loading.value = true + try { + users.value = await apiService.getUsers() + } catch (e: any) { + error.value = e.message || 'Failed to load users.' + } finally { + loading.value = false + } + } + + const createUser = async (name: string, email: string) => { + error.value = '' + + try { + if (!name.trim()) { + throw new Error('Name is required.') + } + + if (!email.trim()) { + throw new Error('Email is required.') + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(email)) { + throw new Error('Invalid email address.') + } + + await apiService.createUser({ name, email }) + await fetchUsers() + } catch (e: any) { + error.value = e.message || 'Failed to create user.' + } + } + + return { + users, + loading, + error, + fetchUsers, + createUser, + } +} From 707ff924d43f60ac7cd30b2dca476fddef899e7a Mon Sep 17 00:00:00 2001 From: Rogaix Date: Sat, 23 Aug 2025 01:59:07 +0200 Subject: [PATCH 05/12] feat: add delete user functionality and handle no content response in API --- ui/apps/portal/src/services/api.ts | 4 +++ ui/apps/portal/src/views/users/UsersView.vue | 28 +++++++++++++++++--- ui/apps/portal/src/views/users/useUsers.ts | 10 +++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/ui/apps/portal/src/services/api.ts b/ui/apps/portal/src/services/api.ts index 8486cb8..046aa36 100644 --- a/ui/apps/portal/src/services/api.ts +++ b/ui/apps/portal/src/services/api.ts @@ -25,6 +25,10 @@ export class ApiService { throw new Error(`API request failed: ${response.statusText}`) } + if (response.status === 204) { + // No content to parse + return undefined as T + } return response.json() } diff --git a/ui/apps/portal/src/views/users/UsersView.vue b/ui/apps/portal/src/views/users/UsersView.vue index fa4eb84..32e0b28 100644 --- a/ui/apps/portal/src/views/users/UsersView.vue +++ b/ui/apps/portal/src/views/users/UsersView.vue @@ -14,8 +14,30 @@ - {{ user.name }} - {{ user.email }} + + {{ user.name }} + + + {{ user.email }} @@ -31,7 +53,7 @@ import { onMounted } from 'vue' import { Card } from '@quixsi/components' import { CreateUser } from './create' import { useUsers } from './useUsers' -const { users, loading, error, fetchUsers } = useUsers() +const { users, loading, error, fetchUsers, deleteUser } = useUsers() onMounted(fetchUsers) \ No newline at end of file diff --git a/ui/apps/portal/src/views/users/useUsers.ts b/ui/apps/portal/src/views/users/useUsers.ts index d4bf78e..f81fcef 100644 --- a/ui/apps/portal/src/views/users/useUsers.ts +++ b/ui/apps/portal/src/views/users/useUsers.ts @@ -41,11 +41,21 @@ export function useUsers() { } } + const deleteUser = async (id: string) => { + try { + await apiService.deleteUser(id) + await fetchUsers() + } catch (e: any) { + error.value = e.message || 'Failed to delete user.' + } + } + return { users, loading, error, fetchUsers, createUser, + deleteUser, } } From 3bcc69dd7a2bc030c4099144949f7f2e14292927 Mon Sep 17 00:00:00 2001 From: Rogaix Date: Sat, 23 Aug 2025 02:06:10 +0200 Subject: [PATCH 06/12] refactor: change useUsers to a singleton composable. A proper store should be considered for a bigger prod application --- ui/apps/portal/src/views/users/useUsers.ts | 78 ++++++++++------------ 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/ui/apps/portal/src/views/users/useUsers.ts b/ui/apps/portal/src/views/users/useUsers.ts index f81fcef..46b242d 100644 --- a/ui/apps/portal/src/views/users/useUsers.ts +++ b/ui/apps/portal/src/views/users/useUsers.ts @@ -1,55 +1,51 @@ import { ref } from 'vue' import { apiService, type User } from '@/services/api' -export function useUsers() { - const users = ref([]) - const loading = ref(true) - const error = ref('') +const users = ref([]) +const loading = ref(true) +const error = ref('') - const fetchUsers = async () => { - loading.value = true - try { - users.value = await apiService.getUsers() - } catch (e: any) { - error.value = e.message || 'Failed to load users.' - } finally { - loading.value = false - } +const fetchUsers = async () => { + loading.value = true + try { + users.value = await apiService.getUsers() + } catch (e: any) { + error.value = e.message || 'Failed to load users.' + } finally { + loading.value = false } +} - const createUser = async (name: string, email: string) => { - error.value = '' - - try { - if (!name.trim()) { - throw new Error('Name is required.') - } - - if (!email.trim()) { - throw new Error('Email is required.') - } - - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - if (!emailRegex.test(email)) { - throw new Error('Invalid email address.') - } - - await apiService.createUser({ name, email }) - await fetchUsers() - } catch (e: any) { - error.value = e.message || 'Failed to create user.' +const createUser = async (name: string, email: string) => { + error.value = '' + try { + if (!name.trim()) { + throw new Error('Name is required.') + } + if (!email.trim()) { + throw new Error('Email is required.') } + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(email)) { + throw new Error('Invalid email address.') + } + await apiService.createUser({ name, email }) + await fetchUsers() + } catch (e: any) { + error.value = e.message || 'Failed to create user.' } +} - const deleteUser = async (id: string) => { - try { - await apiService.deleteUser(id) - await fetchUsers() - } catch (e: any) { - error.value = e.message || 'Failed to delete user.' - } +const deleteUser = async (id: string) => { + try { + await apiService.deleteUser(id) + await fetchUsers() + } catch (e: any) { + error.value = e.message || 'Failed to delete user.' } +} +export function useUsers() { return { users, loading, From eec81b8cf06c94a60cd0842aba7c7d7d5a50b634 Mon Sep 17 00:00:00 2001 From: Rogaix Date: Sat, 23 Aug 2025 02:11:03 +0200 Subject: [PATCH 07/12] fix: reset error state before fetching users and creating a user feat: add a basic check if an email already exists. --- ui/apps/portal/src/views/users/useUsers.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ui/apps/portal/src/views/users/useUsers.ts b/ui/apps/portal/src/views/users/useUsers.ts index 46b242d..44464cc 100644 --- a/ui/apps/portal/src/views/users/useUsers.ts +++ b/ui/apps/portal/src/views/users/useUsers.ts @@ -7,6 +7,8 @@ const error = ref('') const fetchUsers = async () => { loading.value = true + error.value = '' + try { users.value = await apiService.getUsers() } catch (e: any) { @@ -25,10 +27,16 @@ const createUser = async (name: string, email: string) => { if (!email.trim()) { throw new Error('Email is required.') } + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ if (!emailRegex.test(email)) { throw new Error('Invalid email address.') } + + if (users.value.some(u => u.email.toLowerCase() === email.trim().toLowerCase())) { + throw new Error('Email is already in use.') + } + await apiService.createUser({ name, email }) await fetchUsers() } catch (e: any) { @@ -37,6 +45,8 @@ const createUser = async (name: string, email: string) => { } const deleteUser = async (id: string) => { + error.value = '' + try { await apiService.deleteUser(id) await fetchUsers() From 310f1999a6f966be7be9138acb95c5036cf73324 Mon Sep 17 00:00:00 2001 From: Rogaix Date: Sat, 23 Aug 2025 02:19:35 +0200 Subject: [PATCH 08/12] fix: adjust error message display and layout in UsersView and CreateUser components --- ui/apps/portal/src/views/users/UsersView.vue | 4 ++-- ui/apps/portal/src/views/users/create/CreateUser.vue | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ui/apps/portal/src/views/users/UsersView.vue b/ui/apps/portal/src/views/users/UsersView.vue index 32e0b28..8491c55 100644 --- a/ui/apps/portal/src/views/users/UsersView.vue +++ b/ui/apps/portal/src/views/users/UsersView.vue @@ -1,10 +1,9 @@ diff --git a/ui/apps/portal/src/views/users/create/CreateUser.vue b/ui/apps/portal/src/views/users/create/CreateUser.vue index 951107b..ce8a815 100644 --- a/ui/apps/portal/src/views/users/create/CreateUser.vue +++ b/ui/apps/portal/src/views/users/create/CreateUser.vue @@ -1,7 +1,6 @@ -
{{ error }}
@@ -53,7 +52,8 @@ import { onMounted } from 'vue' import { Card } from '@quixsi/components' import { CreateUser } from './create' import { useUsers } from './useUsers' -const { users, loading, error, fetchUsers, deleteUser } = useUsers() + +const { users, loading, fetchUsers, deleteUser } = useUsers() onMounted(fetchUsers) \ No newline at end of file diff --git a/ui/apps/portal/src/views/users/useUsers.ts b/ui/apps/portal/src/views/users/useUsers.ts index 44464cc..5b6967c 100644 --- a/ui/apps/portal/src/views/users/useUsers.ts +++ b/ui/apps/portal/src/views/users/useUsers.ts @@ -1,6 +1,8 @@ import { ref } from 'vue' import { apiService, type User } from '@/services/api' +import { useToast } from '@/composables/useToast' +const { showSuccess, showError } = useToast() const users = ref([]) const loading = ref(true) const error = ref('') @@ -13,6 +15,7 @@ const fetchUsers = async () => { users.value = await apiService.getUsers() } catch (e: any) { error.value = e.message || 'Failed to load users.' + showError(error.value) } finally { loading.value = false } @@ -39,19 +42,23 @@ const createUser = async (name: string, email: string) => { await apiService.createUser({ name, email }) await fetchUsers() + showSuccess('User created successfully!') } catch (e: any) { error.value = e.message || 'Failed to create user.' + showError(error.value) } } const deleteUser = async (id: string) => { error.value = '' - + try { await apiService.deleteUser(id) await fetchUsers() + showSuccess('User deleted successfully!') } catch (e: any) { error.value = e.message || 'Failed to delete user.' + showError(error.value) } } From 3669fb13e384f293f4d45489c875f17f6a830fbb Mon Sep 17 00:00:00 2001 From: Rogaix Date: Sat, 23 Aug 2025 03:17:04 +0200 Subject: [PATCH 11/12] refactor: moved useUsers composable to correct folder --- ui/apps/portal/src/{views/users => composables}/useUsers.ts | 0 ui/apps/portal/src/views/users/UsersView.vue | 2 +- ui/apps/portal/src/views/users/create/CreateUser.vue | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename ui/apps/portal/src/{views/users => composables}/useUsers.ts (100%) diff --git a/ui/apps/portal/src/views/users/useUsers.ts b/ui/apps/portal/src/composables/useUsers.ts similarity index 100% rename from ui/apps/portal/src/views/users/useUsers.ts rename to ui/apps/portal/src/composables/useUsers.ts diff --git a/ui/apps/portal/src/views/users/UsersView.vue b/ui/apps/portal/src/views/users/UsersView.vue index b6b00d9..07e6c67 100644 --- a/ui/apps/portal/src/views/users/UsersView.vue +++ b/ui/apps/portal/src/views/users/UsersView.vue @@ -51,7 +51,7 @@ import { onMounted } from 'vue' import { Card } from '@quixsi/components' import { CreateUser } from './create' -import { useUsers } from './useUsers' +import { useUsers } from '@/composables/useUsers' const { users, loading, fetchUsers, deleteUser } = useUsers() diff --git a/ui/apps/portal/src/views/users/create/CreateUser.vue b/ui/apps/portal/src/views/users/create/CreateUser.vue index ce8a815..e0e54d2 100644 --- a/ui/apps/portal/src/views/users/create/CreateUser.vue +++ b/ui/apps/portal/src/views/users/create/CreateUser.vue @@ -25,7 +25,7 @@ diff --git a/ui/apps/portal/src/composables/useToast.ts b/ui/apps/portal/src/composables/useToast.ts index f2fa3de..ad5e415 100644 --- a/ui/apps/portal/src/composables/useToast.ts +++ b/ui/apps/portal/src/composables/useToast.ts @@ -2,26 +2,52 @@ import { ref } from 'vue' export type ToastType = 'success' | 'error' -interface ToastState { +export interface ToastState { message: string type: ToastType key: number + duration?: number + class?: string } -const toast = ref({ message: '', type: 'success', key: 0 }) +const toasts = ref([]) +const timers = new Map>() export function useToast() { - const showSuccess = (message: string) => { - toast.value = { message, type: 'success', key: Date.now() } + const scheduleRemoval = (key: number, duration = 3000) => { + if (timers.has(key)) return + const t = setTimeout(() => { + removeToast(key) + }, duration) + timers.set(key, t) } - const showError = (message: string) => { - toast.value = { message, type: 'error', key: Date.now() } + const showSuccess = (message: string, duration = 3000) => { + const key = Date.now() + Math.random() + toasts.value.push({ message, type: 'success', key, duration }) + scheduleRemoval(key, duration) } - const clearToast = () => { - toast.value.message = '' + const showError = (message: string, duration = 3000) => { + const key = Date.now() + Math.random() + toasts.value.push({ message, type: 'error', key, duration }) + scheduleRemoval(key, duration) } - - return { toast, showSuccess, showError, clearToast } + + const removeToast = (key: string | number) => { + toasts.value = toasts.value.filter(t => t.key !== key) + const timer = timers.get(Number(key)) + if (timer) { + clearTimeout(timer) + timers.delete(Number(key)) + } + } + + const clearToasts = () => { + toasts.value = [] + timers.forEach(t => clearTimeout(t)) + timers.clear() + } + + return { toasts, showSuccess, showError, removeToast, clearToasts } } diff --git a/ui/packages/components/src/ui/toast/Toast.vue b/ui/packages/components/src/ui/toast/Toast.vue index 733f031..ac97b07 100644 --- a/ui/packages/components/src/ui/toast/Toast.vue +++ b/ui/packages/components/src/ui/toast/Toast.vue @@ -1,60 +1,44 @@