Add modules, more page formatting.

This commit is contained in:
Andrew Lalis 2025-08-02 21:28:20 -04:00
parent 1f78983038
commit c0835383df
9 changed files with 197 additions and 15 deletions

View File

@ -59,3 +59,23 @@ a:hover {
font-size: 28px; font-size: 28px;
font-weight: 500; 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;
}

View File

@ -0,0 +1,48 @@
<script setup lang="ts">
import { ref, type Ref } from 'vue';
const dialog: Ref<HTMLDialogElement | null> = ref(null)
function show() {
dialog.value?.showModal()
}
defineExpose({ show })
</script>
<template>
<Teleport to="body">
<dialog ref="dialog" class="app-modal-dialog">
<slot></slot>
<div class="app-modal-dialog-actions">
<span @click="dialog?.close()" class="app-modal-dialog-close-button">
<font-awesome-icon icon="fa-xmark"></font-awesome-icon>
Close
</span>
</div>
</dialog>
</Teleport>
</template>
<style lang="css">
.app-modal-dialog {
background-color: var(--bg-secondary);
color: inherit;
border-radius: 1em;
border-width: 0;
min-width: 300px;
max-width: 500px;
}
.app-modal-dialog-actions {
text-align: right;
}
.app-modal-dialog-close-button {
cursor: pointer;
}
.app-modal-dialog-close-button:hover {
color: var(--theme-secondary);
}
</style>

View File

@ -1,12 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { ProfileApiClient, type Profile } from '@/api/profile'; import { ProfileApiClient, type Profile } from '@/api/profile';
import ModalWrapper from '@/components/ModalWrapper.vue';
import { useAuthStore } from '@/stores/auth-store'; import { useAuthStore } from '@/stores/auth-store';
import { onMounted, type Ref, ref } from 'vue'; import { useProfileStore } from '@/stores/profile-store';
import { onMounted, type Ref, ref, useTemplateRef } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
const authStore = useAuthStore() const authStore = useAuthStore()
const profileStore = useProfileStore()
const router = useRouter() const router = useRouter()
const profiles: Ref<Profile[]> = ref([]) const profiles: Ref<Profile[]> = ref([])
const testModal = useTemplateRef('testModal')
onMounted(async () => { onMounted(async () => {
authStore.$subscribe(async (_, state) => { authStore.$subscribe(async (_, state) => {
@ -22,15 +26,26 @@ onMounted(async () => {
profiles.value = [] profiles.value = []
} }
}) })
function selectProfile(profile: Profile) {
profileStore.onProfileSelected(profile)
router.push('/')
}
</script> </script>
<template> <template>
<div class="app-page-container"> <div class="app-page-container">
<h1 class="app-page-title">Profiles</h1> <h1 class="app-page-title">Select a Profile</h1>
<div class="profile-card" v-for="profile in profiles" :key="profile.name" <div class="profile-card" v-for="profile in profiles" :key="profile.name" @click="selectProfile(profile)">
@click="router.push('/profiles/' + profile.name)">
<span>{{ profile.name }}</span> <span>{{ profile.name }}</span>
</div> </div>
<div class="profile-card" @click="testModal?.show()">
<span>Add a new profile...</span>
</div>
<ModalWrapper ref="testModal">
<p>This is my modal!</p>
</ModalWrapper>
</div> </div>
</template> </template>
<style lang="css"> <style lang="css">

View File

@ -1,13 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import ProfileModule from './home/ProfileModule.vue';
import AccountsModule from './home/AccountsModule.vue';
</script> </script>
<template> <template>
<div class="app-page-container"> <div class="app-module-container">
<h1 class="app-page-title">Homepage</h1> <ProfileModule />
<p>This is your user's homepage.</p> <AccountsModule />
<RouterLink to="/profiles">
<font-awesome-icon icon="fa-folder-open"></font-awesome-icon>
View your profiles
</RouterLink>
</div> </div>
</template> </template>

View File

@ -0,0 +1,44 @@
<script setup lang="ts">
import { AccountApiClient, type Account } from '@/api/account'
import { useProfileStore } from '@/stores/profile-store'
import { onMounted, ref, type Ref } from 'vue'
const profileStore = useProfileStore()
const accounts: Ref<Account[]> = ref([])
onMounted(async () => {
if (!profileStore.state) {
console.warn('No profile is selected.')
return
}
const accountApi = new AccountApiClient(profileStore.state)
accountApi.getAccounts().then(result => accounts.value = result)
.catch(err => console.error(err))
})
</script>
<template>
<div class="app-module">
<h2>Accounts</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Currency</th>
<th>Number</th>
<th>Type</th>
</tr>
</thead>
<tbody>
<tr v-for="account in accounts" :key="account.id">
<td>{{ account.name }}</td>
<td>{{ account.currency }}</td>
<td>...{{ account.numberSuffix }}</td>
<td>{{ account.type }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<style lang="css"></style>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import { useProfileStore } from '@/stores/profile-store';
const profileStore = useProfileStore()
</script>
<template>
<div class="app-module">
<h2>Profile</h2>
<p>Your currently selected profile is: {{ profileStore.state?.name }}</p>
<RouterLink to="/profiles">
<font-awesome-icon icon="fa-folder-open"></font-awesome-icon>
Select a different profile
</RouterLink>
</div>
</template>

View File

@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/auth-store'
import { createRouter, createWebHistory, type RouteLocationNormalized } from 'vue-router' import { createRouter, createWebHistory, type RouteLocationNormalized } from 'vue-router'
import UserHomePage from '@/pages/UserHomePage.vue' import UserHomePage from '@/pages/UserHomePage.vue'
import ProfilesPage from '@/pages/ProfilesPage.vue' import ProfilesPage from '@/pages/ProfilesPage.vue'
import { useProfileStore } from '@/stores/profile-store'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -23,6 +24,7 @@ const router = createRouter({
path: '', path: '',
component: async () => UserHomePage, component: async () => UserHomePage,
meta: { title: 'Home' }, meta: { title: 'Home' },
beforeEnter: profileSelected,
}, },
{ {
path: 'profiles', path: 'profiles',
@ -57,4 +59,10 @@ function onlyAuthenticated(to: RouteLocationNormalized) {
return '/login?next=' + encodeURIComponent(to.path) 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 export default router

View File

@ -7,26 +7,28 @@ export interface AuthenticatedData {
token: string token: string
} }
const LOCAL_STORAGE_KEY = 'token'
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const state: Ref<AuthenticatedData | null> = ref(getStateFromLocalStorage()) const state: Ref<AuthenticatedData | null> = ref(getStateFromLocalStorage())
function onUserLoggedIn(username: string, token: string) { function onUserLoggedIn(username: string, token: string) {
state.value = { username, token } state.value = { username, token }
localStorage.setItem('token', token) localStorage.setItem(LOCAL_STORAGE_KEY, token)
} }
function onUserLoggedOut() { function onUserLoggedOut() {
state.value = null state.value = null
localStorage.clear() localStorage.removeItem(LOCAL_STORAGE_KEY)
} }
return { state, onUserLoggedIn, onUserLoggedOut } return { state, onUserLoggedIn, onUserLoggedOut }
}) })
function getStateFromLocalStorage(): AuthenticatedData | null { function getStateFromLocalStorage(): AuthenticatedData | null {
const token = localStorage.getItem('token') const token = localStorage.getItem(LOCAL_STORAGE_KEY)
if (token === null || token.length === 0 || isExpired(token)) { if (token === null || token.length === 0 || isExpired(token)) {
localStorage.clear() localStorage.removeItem(LOCAL_STORAGE_KEY)
return null return null
} }
const username = parseSubject(token) const username = parseSubject(token)

View File

@ -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<Profile | null> = 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
}