Integrate Vuetify layout and routing
Add a Vuetify-powered application shell (Layout.vue) replacing the previous App.vue, including app bar, navigation drawer, theme toggle, footer and localStorage persistence for theme/drawer. Introduce a routesLayout plugin with a Visibility enum and centralized LayoutRoute definitions; add route components (Home, Impressum, Login, 404NotFound) and update the router to build routes from the new layout definitions. Register Vuetify in main.ts and add dependencies (vuetify, @fontsource/roboto, @mdi/font) in package.json; update tsconfig.app.json to include .ts files. Package-lock.json updated accordingly.
This commit is contained in:
@@ -1,11 +0,0 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<h1>You did it!</h1>
|
||||
<p>
|
||||
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
|
||||
documentation
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { Visibility, routes } from '@/plugins/routesLayout'
|
||||
|
||||
const theme = useTheme()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const showDrawer = ref(true)
|
||||
|
||||
function changeTheme() {
|
||||
const nextTheme = theme.global.name.value === 'dark' ? 'light' : 'dark'
|
||||
theme.global.name.value = nextTheme
|
||||
localStorage.setItem('theme', nextTheme)
|
||||
}
|
||||
|
||||
function toggleDrawer() {
|
||||
showDrawer.value = !showDrawer.value
|
||||
localStorage.setItem('drawer', showDrawer.value ? 'Y' : 'N')
|
||||
}
|
||||
|
||||
function changeWebsiteTitle(path: string) {
|
||||
const currentPageInfo = routes.find((x) => x.path === path)
|
||||
if (currentPageInfo) {
|
||||
document.title = `Hoard | ${currentPageInfo.name}`
|
||||
return
|
||||
}
|
||||
|
||||
document.title = 'Hoard'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const storedTheme = localStorage.getItem('theme')
|
||||
const fallbackTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
theme.global.name.value = storedTheme === 'dark' || storedTheme === 'light' ? storedTheme : fallbackTheme
|
||||
|
||||
const storedDrawer = localStorage.getItem('drawer')
|
||||
showDrawer.value = storedDrawer ? storedDrawer.startsWith('Y') : true
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
(newPath) => {
|
||||
changeWebsiteTitle(newPath)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app>
|
||||
<v-app-bar elevation="1">
|
||||
<template #prepend>
|
||||
<v-app-bar-nav-icon
|
||||
v-tooltip="!showDrawer ? 'Menue oeffnen' : 'Menue schliessen'"
|
||||
@click="toggleDrawer()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<v-app-bar-title class="title" @click="router.push({ name: 'Home' })">
|
||||
<span class="pointer">Hoard</span>
|
||||
</v-app-bar-title>
|
||||
|
||||
<v-tooltip v-if="!$vuetify.display.mobile">
|
||||
<template #activator="{ props }">
|
||||
<v-btn icon v-bind="props" to="/login">
|
||||
<v-icon>mdi-account</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
Account
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip>
|
||||
<template #activator="{ props }">
|
||||
<v-btn icon @click="changeTheme()" v-bind="props">
|
||||
<v-icon>mdi-brightness-6</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
{{ theme.global.name.value === 'dark' ? 'Hellen Modus aktivieren' : 'Dunklen Modus aktivieren' }}
|
||||
</v-tooltip>
|
||||
</v-app-bar>
|
||||
|
||||
<v-navigation-drawer
|
||||
v-model="showDrawer"
|
||||
:location="$vuetify.display.mobile ? 'bottom' : undefined"
|
||||
:permanent="!$vuetify.display.mobile"
|
||||
>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="item in routes.filter((x) => x.visible === Visibility.Public)"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
:active="route.path === item.path"
|
||||
link
|
||||
:prepend-icon="item.icon"
|
||||
:title="item.name"
|
||||
class="rounded-lg mr-1 ml-1"
|
||||
/>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-main>
|
||||
<router-view />
|
||||
<v-footer class="d-flex align-center justify-center ga-2 flex-wrap flex-grow-1 py-3">
|
||||
<v-btn
|
||||
v-for="link in routes.filter((x) => x.visible === Visibility.Footer)"
|
||||
:key="link.path"
|
||||
:to="link.path"
|
||||
:text="link.name"
|
||||
variant="text"
|
||||
rounded
|
||||
/>
|
||||
<div class="flex-1-0-100 text-center mt-2">
|
||||
{{ new Date().getFullYear() }} - <strong>Hoard</strong>
|
||||
</div>
|
||||
</v-footer>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
+3
-1
@@ -1,12 +1,14 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import App from './Layout.vue'
|
||||
import router from './router'
|
||||
import vuetify from './plugins/vuetify'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(vuetify)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
import Home from '@/routes/Home.vue'
|
||||
import NotFound from '@/routes/404NotFound.vue'
|
||||
import Login from '@/routes/authentication/Login.vue'
|
||||
import Impressum from '@/routes/Impressum.vue'
|
||||
|
||||
export enum Visibility {
|
||||
Hidden,
|
||||
Authenticated,
|
||||
Unauthenticated,
|
||||
Authorized,
|
||||
Public,
|
||||
Footer,
|
||||
}
|
||||
|
||||
export interface LayoutRoute {
|
||||
path: string
|
||||
name: string
|
||||
description: string
|
||||
icon: string
|
||||
disableFooter?: boolean
|
||||
visible: Visibility
|
||||
meta?: RouteRecordRaw
|
||||
}
|
||||
|
||||
export const routes: LayoutRoute[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Startseite',
|
||||
description: 'Uebersicht der Anwendung',
|
||||
icon: 'mdi-home',
|
||||
visible: Visibility.Public,
|
||||
meta: {
|
||||
name: 'Home',
|
||||
path: '/',
|
||||
component: Home,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
description: 'Logge dich ein',
|
||||
icon: 'mdi-login',
|
||||
visible: Visibility.Hidden,
|
||||
meta: {
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: Login,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/impressum',
|
||||
name: 'Impressum',
|
||||
description: 'Impressum der Anwendung',
|
||||
icon: 'mdi-file-document',
|
||||
visible: Visibility.Footer,
|
||||
meta: {
|
||||
path: '/impressum',
|
||||
name: 'Impressum',
|
||||
component: Impressum,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/notFound',
|
||||
name: 'Nicht gefunden',
|
||||
description: 'Diese Seite wurde nicht gefunden',
|
||||
icon: 'mdi-information-outline',
|
||||
visible: Visibility.Hidden,
|
||||
meta: { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'vuetify/styles'
|
||||
import '@fontsource/roboto/100.css'
|
||||
import '@fontsource/roboto/300.css'
|
||||
import '@fontsource/roboto/400.css'
|
||||
import '@fontsource/roboto/500.css'
|
||||
import '@fontsource/roboto/700.css'
|
||||
import '@fontsource/roboto/900.css'
|
||||
import { createVuetify } from 'vuetify'
|
||||
import * as components from 'vuetify/components'
|
||||
import * as directives from 'vuetify/directives'
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import { aliases, mdi } from 'vuetify/iconsets/mdi'
|
||||
|
||||
export default createVuetify({
|
||||
components,
|
||||
directives,
|
||||
theme: {
|
||||
defaultTheme: 'dark',
|
||||
},
|
||||
icons: {
|
||||
defaultSet: 'mdi',
|
||||
aliases,
|
||||
sets: {
|
||||
mdi,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,8 +1,9 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
||||
import { routes } from '@/plugins/routesLayout'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [],
|
||||
routes: routes.filter((x) => x.meta !== undefined).map((x) => x.meta) as RouteRecordRaw[],
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<v-container fluid class="fill-height d-flex flex-column justify-center align-center">
|
||||
<h1 class="error-title text-h1 font-weight-bold">404</h1>
|
||||
<p class="error-message text-h5">Seite nicht gefunden</p>
|
||||
|
||||
<router-link to="/" class="text-primary text-decoration-none font-weight-medium mt-5">
|
||||
Zurueck zur Startseite
|
||||
</router-link>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.error-title {
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 1.25rem;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<v-container class="py-10">
|
||||
<v-row justify="center">
|
||||
<v-col cols="12" md="10" lg="8">
|
||||
<v-card rounded="lg" elevation="2">
|
||||
<v-card-title class="text-h4">Willkommen bei Hoard</v-card-title>
|
||||
<v-card-text class="text-body-1">
|
||||
Dein Vuetify-Setup ist aktiv. Ueber das Menue links kannst du zu den weiteren Seiten
|
||||
navigieren.
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<v-container class="py-10">
|
||||
<v-row justify="center">
|
||||
<v-col cols="12" md="10" lg="8">
|
||||
<v-card rounded="lg" elevation="2">
|
||||
<v-card-title class="text-h5">Impressum</v-card-title>
|
||||
<v-card-text>
|
||||
Diese Seite ist als Platzhalter angelegt. Trage hier deine Firmen- oder Vereinsdaten
|
||||
ein.
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<v-container class="py-10">
|
||||
<v-row justify="center">
|
||||
<v-col cols="12" sm="10" md="6" lg="4">
|
||||
<v-card rounded="lg" elevation="2">
|
||||
<v-card-title class="text-h5">Login</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field label="E-Mail" prepend-inner-icon="mdi-email-outline" />
|
||||
<v-text-field
|
||||
label="Passwort"
|
||||
type="password"
|
||||
prepend-inner-icon="mdi-lock-outline"
|
||||
/>
|
||||
<v-btn color="primary" block prepend-icon="mdi-login">Anmelden</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user