Add modules, more page formatting.
This commit is contained in:
parent
1f78983038
commit
c0835383df
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -1,12 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
import { ProfileApiClient, type Profile } from '@/api/profile';
|
||||
import ModalWrapper from '@/components/ModalWrapper.vue';
|
||||
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';
|
||||
const authStore = useAuthStore()
|
||||
const profileStore = useProfileStore()
|
||||
const router = useRouter()
|
||||
|
||||
const profiles: Ref<Profile[]> = ref([])
|
||||
const testModal = useTemplateRef('testModal')
|
||||
|
||||
onMounted(async () => {
|
||||
authStore.$subscribe(async (_, state) => {
|
||||
|
@ -22,15 +26,26 @@ onMounted(async () => {
|
|||
profiles.value = []
|
||||
}
|
||||
})
|
||||
|
||||
function selectProfile(profile: Profile) {
|
||||
profileStore.onProfileSelected(profile)
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<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"
|
||||
@click="router.push('/profiles/' + profile.name)">
|
||||
<div class="profile-card" v-for="profile in profiles" :key="profile.name" @click="selectProfile(profile)">
|
||||
<span>{{ profile.name }}</span>
|
||||
</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>
|
||||
</template>
|
||||
<style lang="css">
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import ProfileModule from './home/ProfileModule.vue';
|
||||
import AccountsModule from './home/AccountsModule.vue';
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="app-page-container">
|
||||
<h1 class="app-page-title">Homepage</h1>
|
||||
<p>This is your user's homepage.</p>
|
||||
<RouterLink to="/profiles">
|
||||
<font-awesome-icon icon="fa-folder-open"></font-awesome-icon>
|
||||
View your profiles
|
||||
</RouterLink>
|
||||
<div class="app-module-container">
|
||||
<ProfileModule />
|
||||
<AccountsModule />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -7,26 +7,28 @@ export interface AuthenticatedData {
|
|||
token: string
|
||||
}
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'token'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const state: Ref<AuthenticatedData | null> = 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)
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue