Upgrade Express to version 5.2.1 and update related dependencies in package.json and package-lock.json. Refactor server CORS configuration to accommodate Express 5 changes. Enhance routing logic in Vue components for improved path normalization and menu handling. Update HTML asset references for better loading performance and accessibility improvements in various components.
This commit is contained in:
@@ -13,6 +13,10 @@
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.mg-card:hover {
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
.mg-card--highlight {
|
||||
border-color: var(--color-brand-primary);
|
||||
box-shadow: 0 0 0 1px rgba(148, 0, 255, 0.18);
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
--font-size-h3: clamp(1.05rem, 1.5vw, 1.2rem);
|
||||
|
||||
--shadow-dropdown: 2px 2px 6px rgba(0, 0, 0, 0.22);
|
||||
--transition-fast: 140ms ease;
|
||||
--transition-standard: 220ms ease;
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -88,6 +90,17 @@ body {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
a,
|
||||
button,
|
||||
[role="button"] {
|
||||
transition:
|
||||
color var(--transition-fast),
|
||||
background-color var(--transition-fast),
|
||||
border-color var(--transition-fast),
|
||||
box-shadow var(--transition-standard),
|
||||
transform var(--transition-fast);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<header>
|
||||
<div class="header-title">
|
||||
<h1>Evangelische Miriamgemeinde Frankfurt am Main</h1>
|
||||
<span class="reload-icon" @click="reloadMenu">⟳</span>
|
||||
<button class="reload-icon" type="button" @click="reloadMenu" aria-label="Menü neu laden">
|
||||
⟳
|
||||
</button>
|
||||
</div>
|
||||
<NavbarComponent />
|
||||
<BreadcrumbsComponent />
|
||||
@@ -13,7 +15,6 @@
|
||||
import NavbarComponent from './NavbarComponent.vue';
|
||||
import BreadcrumbsComponent from './BreadcrumbsComponent.vue';
|
||||
import { mapActions } from 'vuex';
|
||||
import router from '@/router';
|
||||
|
||||
export default {
|
||||
name: 'HeaderComponent',
|
||||
@@ -25,16 +26,8 @@ export default {
|
||||
...mapActions(['loadMenuData']),
|
||||
async reloadMenu() {
|
||||
await this.loadMenuData();
|
||||
this.$router.push({ path: '/' });
|
||||
const routes = this.$store.state.menuData.map(item => {
|
||||
if (item.component) {
|
||||
return {
|
||||
path: item.link,
|
||||
component: () => import(`../components/${item.component}.vue`)
|
||||
};
|
||||
}
|
||||
});
|
||||
routes.forEach(route => router.addRoute(route));
|
||||
// Für den Test-Reload den aktuellen Pfad behalten und sauber neu initialisieren.
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -65,18 +58,22 @@ header h1 {
|
||||
}
|
||||
|
||||
.reload-icon {
|
||||
font-size: 1rem;
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
margin-left: var(--space-3);
|
||||
background-color: var(--color-brand-tint);
|
||||
color: var(--color-bg-page);
|
||||
padding: var(--space-1) 6px;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
color: var(--color-text-muted);
|
||||
padding: 2px 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.14);
|
||||
line-height: 1;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.reload-icon:hover,
|
||||
.reload-icon:focus-visible {
|
||||
color: var(--color-brand-primary-hover);
|
||||
border-color: rgba(122, 0, 209, 0.45);
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -185,6 +185,11 @@ export default {
|
||||
background-color: var(--color-brand-primary-hover);
|
||||
}
|
||||
|
||||
.nav-link:active,
|
||||
.menu-toggle:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.navbar a:focus-visible,
|
||||
.menu-toggle:focus-visible {
|
||||
outline-color: #fff;
|
||||
@@ -209,7 +214,7 @@ export default {
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out;
|
||||
transition: opacity var(--transition-standard), visibility var(--transition-standard);
|
||||
box-shadow: var(--shadow-dropdown);
|
||||
border-radius: 4px;
|
||||
padding: var(--space-1);
|
||||
@@ -234,7 +239,7 @@ export default {
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out;
|
||||
transition: opacity var(--transition-standard), visibility var(--transition-standard);
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
|
||||
@@ -9,7 +9,13 @@
|
||||
>
|
||||
<div class="mg-card__grid">
|
||||
<div v-if="hasImage(event)" class="mg-media" aria-hidden="true">
|
||||
<img v-if="imageMap[event.relatedImage]" :src="imageMap[event.relatedImage]" alt="" />
|
||||
<img
|
||||
v-if="imageMap[event.relatedImage]"
|
||||
:src="imageMap[event.relatedImage]"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="mg-media" aria-hidden="true"></div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-html="parsedContent"></div>
|
||||
<div class="content-render" v-html="parsedContent"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -187,5 +187,58 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add styles if needed */
|
||||
.content-render :deep(ul),
|
||||
.content-render :deep(ol) {
|
||||
margin: 0.2rem 0 0.35rem 0;
|
||||
padding-left: 1.05rem;
|
||||
}
|
||||
|
||||
.content-render :deep(li) {
|
||||
margin: 0.08rem 0;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.content-render :deep(ul) {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.content-render :deep(ul > li) {
|
||||
position: relative;
|
||||
padding-left: 1.05rem;
|
||||
}
|
||||
|
||||
.content-render :deep(ul > li)::before {
|
||||
content: "•";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.02em;
|
||||
color: var(--color-brand-primary-hover);
|
||||
font-size: 0.95em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.content-render :deep(ul ul) {
|
||||
margin-top: 0.08rem;
|
||||
margin-bottom: 0.18rem;
|
||||
}
|
||||
|
||||
.content-render :deep(ul ul > li) {
|
||||
padding-left: 0.95rem;
|
||||
}
|
||||
|
||||
.content-render :deep(ul ul > li)::before {
|
||||
content: "◦";
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.content-render :deep(ol) {
|
||||
list-style-position: outside;
|
||||
}
|
||||
|
||||
.content-render :deep(ol > li)::marker {
|
||||
color: var(--color-brand-primary-hover);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,63 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<ContentComponent v-if="isKnownMenuPath" :link="normalizedPath" />
|
||||
<div v-else class="not-found">
|
||||
<h1>Seite existiert nicht</h1>
|
||||
<p>Leider existiert die aufgerufene Seite nicht.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DefaultComponent'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
padding: 20px;
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import ContentComponent from '@/components/ContentComponent.vue';
|
||||
|
||||
function normalizePath(path) {
|
||||
if (!path || typeof path !== 'string') {
|
||||
return '/';
|
||||
}
|
||||
</style>
|
||||
|
||||
let value = path.trim();
|
||||
if (!value.startsWith('/')) {
|
||||
value = `/${value}`;
|
||||
}
|
||||
if (value.length > 1) {
|
||||
value = value.replace(/\/+$/, '');
|
||||
}
|
||||
return value || '/';
|
||||
}
|
||||
|
||||
function findByPath(items, targetPath) {
|
||||
const normalizedTarget = normalizePath(targetPath);
|
||||
for (const item of items || []) {
|
||||
if (normalizePath(item?.link || '') === normalizedTarget) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(item?.submenu) && findByPath(item.submenu, normalizedTarget)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'DefaultComponent',
|
||||
components: {
|
||||
ContentComponent,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['menuData']),
|
||||
normalizedPath() {
|
||||
return normalizePath(this.$route.path);
|
||||
},
|
||||
isKnownMenuPath() {
|
||||
return findByPath(this.menuData, this.normalizedPath);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.not-found {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="side-image">
|
||||
<img :src="currentImage" alt="" />
|
||||
<img :src="currentImage" alt="" loading="lazy" decoding="async" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -33,17 +33,32 @@ function loadComponent(componentName) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePath(path) {
|
||||
if (!path || typeof path !== 'string') {
|
||||
return '/';
|
||||
}
|
||||
let value = path.trim();
|
||||
if (!value.startsWith('/')) {
|
||||
value = `/${value}`;
|
||||
}
|
||||
if (value.length > 1) {
|
||||
value = value.replace(/\/+$/, '');
|
||||
}
|
||||
return value || '/';
|
||||
}
|
||||
|
||||
function generateRoutesFromMenu(menu) {
|
||||
let routes = [];
|
||||
menu.forEach(item => {
|
||||
if (item.link === '/admin/edit-pages') {
|
||||
const normalizedLink = normalizePath(item.link || '');
|
||||
if (normalizedLink === '/admin/edit-pages') {
|
||||
return;
|
||||
}
|
||||
|
||||
let route = null;
|
||||
if (item.link && item.link !== '') {
|
||||
route = {
|
||||
path: item.link,
|
||||
path: normalizedLink,
|
||||
meta: { requiresAuth: item.requiresAuth || false },
|
||||
components: {
|
||||
default: loadComponent(item.component),
|
||||
@@ -63,6 +78,43 @@ function generateRoutesFromMenu(menu) {
|
||||
return routes;
|
||||
}
|
||||
|
||||
function findMenuItemByPath(menu, targetPath) {
|
||||
const wanted = normalizePath(targetPath);
|
||||
for (const item of menu || []) {
|
||||
if (normalizePath(item.link || '') === wanted) {
|
||||
return item;
|
||||
}
|
||||
if (item.submenu && item.submenu.length > 0) {
|
||||
const found = findMenuItemByPath(item.submenu, wanted);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function ensureMenuRouteForPath(path) {
|
||||
const normalizedPath = normalizePath(path);
|
||||
const exists = router.getRoutes().some(r => normalizePath(r.path) === normalizedPath);
|
||||
if (exists) {
|
||||
return true;
|
||||
}
|
||||
const menuItem = findMenuItemByPath(store.state.menuData, normalizedPath);
|
||||
if (!menuItem || !menuItem.link) {
|
||||
return false;
|
||||
}
|
||||
router.addRoute({
|
||||
path: normalizedPath,
|
||||
meta: { requiresAuth: menuItem.requiresAuth || false },
|
||||
components: {
|
||||
default: loadComponent(menuItem.component),
|
||||
rightColumn: loadComponent('ImageContent')
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: []
|
||||
@@ -96,10 +148,24 @@ router.beforeEach(async (to, from, next) => {
|
||||
});
|
||||
}
|
||||
|
||||
const normalizedToPath = normalizePath(to.path);
|
||||
if (normalizedToPath !== to.path) {
|
||||
next({ path: normalizedToPath, query: to.query, hash: to.hash, replace: true });
|
||||
return;
|
||||
}
|
||||
next({ ...to, replace: true });
|
||||
} else {
|
||||
// Sicherstellen, dass Kernrouten immer verfügbar sind
|
||||
ensureCoreRoutes();
|
||||
const normalizedToPath = normalizePath(to.path);
|
||||
if (normalizedToPath !== to.path) {
|
||||
next({ path: normalizedToPath, query: to.query, hash: to.hash, replace: true });
|
||||
return;
|
||||
}
|
||||
if (to.matched.length === 0 && ensureMenuRouteForPath(normalizedToPath)) {
|
||||
next({ path: normalizedToPath, query: to.query, hash: to.hash, replace: true });
|
||||
return;
|
||||
}
|
||||
if (to.matched.some(record => record.meta.requiresAuth) && !store.getters.isLoggedIn) {
|
||||
next('/auth/login');
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user