Add modules, more page formatting.
This commit is contained in:
parent
1f78983038
commit
c0835383df
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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">
|
<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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 { 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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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