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:
Generated
+47
-8
@@ -8,9 +8,12 @@
|
||||
"name": "-",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^5.2.10",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.31",
|
||||
"vue-router": "^5.0.4"
|
||||
"vue-router": "^5.0.4",
|
||||
"vuetify": "^4.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node24": "^24.0.4",
|
||||
@@ -59,7 +62,6 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -515,10 +517,20 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/roboto": {
|
||||
"version": "5.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.10.tgz",
|
||||
"integrity": "sha512-8HlA5FtSfz//oFSr2eL7GFXAiE7eIkcGOtx7tjsLKq+as702x9+GU7K95iDeWFapHC4M2hv9RrpXKRTGGBI8Zg==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
@@ -564,6 +576,12 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mdi/font": {
|
||||
"version": "7.4.47",
|
||||
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
|
||||
"integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
|
||||
@@ -886,7 +904,6 @@
|
||||
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -1342,7 +1359,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
@@ -2304,7 +2320,6 @@
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^7.7.7"
|
||||
},
|
||||
@@ -2616,7 +2631,6 @@
|
||||
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -2705,7 +2719,6 @@
|
||||
"integrity": "sha512-baBr4jUVSLJ0RPyZ2nK0zS2+W8hNHbM4hEzfvllukmRPVS3xDG5ATTNtbRXrKIOE2b8/FsPWJAOnuIxcs7g3cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
@@ -2921,7 +2934,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
|
||||
"integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.32",
|
||||
"@vue/compiler-sfc": "3.5.32",
|
||||
@@ -3033,6 +3045,33 @@
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vuetify": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.0.5.tgz",
|
||||
"integrity": "sha512-pFysKOHuY3dROTVh9PdlhVz50ZR0E5/goY5ecTXc8F8tajUA2ee3xZ8Lqs1WtEw/X3w93wx/LogyjgaQCAL/Ig==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/johnleider"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.7",
|
||||
"vite-plugin-vuetify": ">=2.1.0",
|
||||
"vue": "^3.5.0",
|
||||
"webpack-plugin-vuetify": ">=3.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
},
|
||||
"vite-plugin-vuetify": {
|
||||
"optional": true
|
||||
},
|
||||
"webpack-plugin-vuetify": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-virtual-modules": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
|
||||
|
||||
+4
-1
@@ -12,9 +12,12 @@
|
||||
"format": "prettier --write --experimental-cli src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^5.2.10",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.31",
|
||||
"vue-router": "^5.0.4"
|
||||
"vue-router": "^5.0.4",
|
||||
"vuetify": "^4.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node24": "^24.0.4",
|
||||
|
||||
@@ -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>
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"include": ["env.d.ts", "src/**/*.vue", "src/**/*.ts"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
// Extra safety for array and object lookups, but may have false positives.
|
||||
|
||||
Reference in New Issue
Block a user