From c0835383dfb1b9a3d46a343cc643ee04a615bec0 Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Sat, 2 Aug 2025 21:28:20 -0400 Subject: [PATCH] Add modules, more page formatting. --- web-app/src/assets/main.css | 20 ++++++++++ web-app/src/components/ModalWrapper.vue | 48 +++++++++++++++++++++++ web-app/src/pages/ProfilesPage.vue | 23 +++++++++-- web-app/src/pages/UserHomePage.vue | 12 +++--- web-app/src/pages/home/AccountsModule.vue | 44 +++++++++++++++++++++ web-app/src/pages/home/ProfileModule.vue | 15 +++++++ web-app/src/router/index.ts | 8 ++++ web-app/src/stores/auth-store.ts | 10 +++-- web-app/src/stores/profile-store.ts | 32 +++++++++++++++ 9 files changed, 197 insertions(+), 15 deletions(-) create mode 100644 web-app/src/components/ModalWrapper.vue create mode 100644 web-app/src/pages/home/AccountsModule.vue create mode 100644 web-app/src/pages/home/ProfileModule.vue create mode 100644 web-app/src/stores/profile-store.ts diff --git a/web-app/src/assets/main.css b/web-app/src/assets/main.css index 5c47a10..736615d 100644 --- a/web-app/src/assets/main.css +++ b/web-app/src/assets/main.css @@ -59,3 +59,23 @@ a:hover { font-size: 28px; font-weight: 500; } + +.app-module-container { + display: flex; + flex-wrap: wrap; + gap: 20px; + padding: 1em; +} + +.app-module { + min-width: 300px; + min-height: 200px; + flex-grow: 1; + background-color: var(--bg-secondary); + border-radius: 0.5em; + padding: 0.5em; +} + +.app-module > h2 { + margin: 0; +} diff --git a/web-app/src/components/ModalWrapper.vue b/web-app/src/components/ModalWrapper.vue new file mode 100644 index 0000000..704537d --- /dev/null +++ b/web-app/src/components/ModalWrapper.vue @@ -0,0 +1,48 @@ + + + diff --git a/web-app/src/pages/ProfilesPage.vue b/web-app/src/pages/ProfilesPage.vue index f4b1209..4bc00d1 100644 --- a/web-app/src/pages/ProfilesPage.vue +++ b/web-app/src/pages/ProfilesPage.vue @@ -1,12 +1,16 @@ diff --git a/web-app/src/pages/home/ProfileModule.vue b/web-app/src/pages/home/ProfileModule.vue new file mode 100644 index 0000000..b53fc8f --- /dev/null +++ b/web-app/src/pages/home/ProfileModule.vue @@ -0,0 +1,15 @@ + + diff --git a/web-app/src/router/index.ts b/web-app/src/router/index.ts index e990dc2..955cd48 100644 --- a/web-app/src/router/index.ts +++ b/web-app/src/router/index.ts @@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/auth-store' import { createRouter, createWebHistory, type RouteLocationNormalized } from 'vue-router' import UserHomePage from '@/pages/UserHomePage.vue' import ProfilesPage from '@/pages/ProfilesPage.vue' +import { useProfileStore } from '@/stores/profile-store' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -23,6 +24,7 @@ const router = createRouter({ path: '', component: async () => UserHomePage, meta: { title: 'Home' }, + beforeEnter: profileSelected, }, { path: 'profiles', @@ -57,4 +59,10 @@ function onlyAuthenticated(to: RouteLocationNormalized) { return '/login?next=' + encodeURIComponent(to.path) } +function profileSelected() { + const profileStore = useProfileStore() + if (profileStore.state) return true + return '/profiles' // Send the user to /profiles to select one before continuing. +} + export default router diff --git a/web-app/src/stores/auth-store.ts b/web-app/src/stores/auth-store.ts index 4b128c2..541963c 100644 --- a/web-app/src/stores/auth-store.ts +++ b/web-app/src/stores/auth-store.ts @@ -7,26 +7,28 @@ export interface AuthenticatedData { token: string } +const LOCAL_STORAGE_KEY = 'token' + export const useAuthStore = defineStore('auth', () => { const state: Ref = ref(getStateFromLocalStorage()) function onUserLoggedIn(username: string, token: string) { state.value = { username, token } - localStorage.setItem('token', token) + localStorage.setItem(LOCAL_STORAGE_KEY, token) } function onUserLoggedOut() { state.value = null - localStorage.clear() + localStorage.removeItem(LOCAL_STORAGE_KEY) } return { state, onUserLoggedIn, onUserLoggedOut } }) function getStateFromLocalStorage(): AuthenticatedData | null { - const token = localStorage.getItem('token') + const token = localStorage.getItem(LOCAL_STORAGE_KEY) if (token === null || token.length === 0 || isExpired(token)) { - localStorage.clear() + localStorage.removeItem(LOCAL_STORAGE_KEY) return null } const username = parseSubject(token) diff --git a/web-app/src/stores/profile-store.ts b/web-app/src/stores/profile-store.ts new file mode 100644 index 0000000..0334c08 --- /dev/null +++ b/web-app/src/stores/profile-store.ts @@ -0,0 +1,32 @@ +import type { Profile } from '@/api/profile' +import { defineStore } from 'pinia' +import { ref, type Ref } from 'vue' + +const LOCAL_STORAGE_KEY = 'profile' + +export const useProfileStore = defineStore('profile', () => { + const state: Ref = ref(getStateFromLocalStorage()) + + function onProfileSelected(profile: Profile) { + state.value = profile + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(profile)) + } + + function onProfileSelectionCleared() { + state.value = null + localStorage.removeItem(LOCAL_STORAGE_KEY) + } + + return { state, onProfileSelected, onProfileSelectionCleared } +}) + +function getStateFromLocalStorage(): Profile | null { + const jsonText = localStorage.getItem(LOCAL_STORAGE_KEY) + if (jsonText === null || jsonText.length === 0) { + localStorage.removeItem(LOCAL_STORAGE_KEY) + return null + } + const profile = JSON.parse(jsonText) as Profile + console.info('Loaded profile', profile) + return profile +}