Update dependencies to include TinyMCE and Quill, enhance Navigation component with a new Newsletter submenu, and implement role-based access control for CMS features. Refactor user role handling to support multiple roles and improve user management functionality across various API endpoints.
This commit is contained in:
@@ -53,11 +53,17 @@
|
||||
Termine
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink v-if="hasGalleryImages" to="/galerie" @click="currentSubmenu = null"
|
||||
class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50"
|
||||
active-class="text-white bg-primary-600">
|
||||
Galerie
|
||||
</NuxtLink>
|
||||
<NuxtLink v-if="hasGalleryImages" to="/verein/galerie" @click="currentSubmenu = null"
|
||||
class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50"
|
||||
active-class="text-white bg-primary-600">
|
||||
Galerie
|
||||
</NuxtLink>
|
||||
|
||||
<button @click="toggleSubmenu('newsletter')"
|
||||
class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50"
|
||||
:class="(route.path.startsWith('/newsletter') || currentSubmenu === 'newsletter') ? 'text-white bg-primary-600' : ''">
|
||||
Newsletter
|
||||
</button>
|
||||
|
||||
<button v-if="isLoggedIn" @click="toggleSubmenu('intern')"
|
||||
class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50"
|
||||
@@ -73,6 +79,19 @@
|
||||
|
||||
<div class="hidden lg:flex items-center h-6 border-t border-primary-700/20">
|
||||
<div v-if="currentSubmenu" class="flex items-center space-x-1">
|
||||
<!-- Newsletter Submenu -->
|
||||
<template v-if="currentSubmenu === 'newsletter'">
|
||||
<NuxtLink to="/newsletter/subscribe"
|
||||
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
|
||||
active-class="text-white bg-primary-600">
|
||||
Abonnieren
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/newsletter/unsubscribe"
|
||||
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
|
||||
active-class="text-white bg-primary-600">
|
||||
Abmelden
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<!-- Verein Submenu -->
|
||||
<template v-if="currentSubmenu === 'verein'">
|
||||
<NuxtLink to="/verein/ueber-uns"
|
||||
@@ -188,6 +207,14 @@
|
||||
active-class="text-white bg-primary-600">
|
||||
API-Dokumentation
|
||||
</NuxtLink>
|
||||
<template v-if="canAccessNewsletter">
|
||||
<div class="h-3 w-px bg-primary-700" />
|
||||
<NuxtLink to="/cms/newsletter"
|
||||
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
|
||||
active-class="text-white bg-primary-600">
|
||||
Newsletter
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template v-if="isAdmin">
|
||||
<div class="h-3 w-px bg-primary-700" />
|
||||
<div class="relative inline-block">
|
||||
@@ -323,6 +350,14 @@
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
Galerie
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/newsletter/subscribe" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
Newsletter abonnieren
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/newsletter/unsubscribe" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
Newsletter abmelden
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -395,10 +430,18 @@
|
||||
Termine
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink v-if="hasGalleryImages" to="/galerie" @click="isMobileMenuOpen = false"
|
||||
<NuxtLink v-if="hasGalleryImages" to="/verein/galerie" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-3 text-gray-300 hover:text-white hover:bg-primary-700/50 rounded-lg font-medium transition-colors">
|
||||
Galerie
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/newsletter/subscribe" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-3 text-gray-300 hover:text-white hover:bg-primary-700/50 rounded-lg font-medium transition-colors">
|
||||
Newsletter abonnieren
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/newsletter/unsubscribe" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-3 text-gray-300 hover:text-white hover:bg-primary-700/50 rounded-lg font-medium transition-colors">
|
||||
Newsletter abmelden
|
||||
</NuxtLink>
|
||||
|
||||
<!-- Intern Mobile -->
|
||||
<div v-if="isLoggedIn">
|
||||
@@ -425,6 +468,13 @@
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
Mein Profil
|
||||
</NuxtLink>
|
||||
<template v-if="canAccessNewsletter">
|
||||
<div class="border-t border-primary-700/20 my-2" />
|
||||
<NuxtLink to="/cms/newsletter" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
Newsletter
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template v-if="isAdmin">
|
||||
<div class="border-t border-primary-700/20 my-2" />
|
||||
<NuxtLink to="/cms" @click="isMobileMenuOpen = false"
|
||||
@@ -509,6 +559,7 @@ const showCmsDropdown = ref(false)
|
||||
// Reactive auth state from store
|
||||
const isLoggedIn = computed(() => authStore.isLoggedIn)
|
||||
const isAdmin = computed(() => authStore.isAdmin)
|
||||
const canAccessNewsletter = computed(() => authStore.hasAnyRole('admin', 'vorstand', 'newsletter'))
|
||||
|
||||
// Automatisches Setzen des Submenus basierend auf der Route
|
||||
const currentSubmenu = computed(() => {
|
||||
@@ -526,6 +577,9 @@ const currentSubmenu = computed(() => {
|
||||
if (path.startsWith('/mitgliederbereich') || path.startsWith('/cms')) {
|
||||
return 'intern'
|
||||
}
|
||||
if (path.startsWith('/newsletter')) {
|
||||
return 'newsletter'
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
@@ -613,7 +667,9 @@ const toggleSubmenu = (menu) => {
|
||||
// Wenn nicht, zur Hauptseite navigieren
|
||||
const path = route.path
|
||||
|
||||
if (menu === 'verein' && !path.startsWith('/verein/') && !path.startsWith('/vorstand') && !path.startsWith('/vereinsmeisterschaften')) {
|
||||
if (menu === 'newsletter' && !path.startsWith('/newsletter')) {
|
||||
navigateTo('/newsletter/subscribe')
|
||||
} else if (menu === 'verein' && !path.startsWith('/verein/') && !path.startsWith('/vorstand') && !path.startsWith('/vereinsmeisterschaften')) {
|
||||
navigateTo('/verein/ueber-uns')
|
||||
} else if (menu === 'mannschaften' && !path.startsWith('/mannschaften') && !path.startsWith('/spielsysteme')) {
|
||||
navigateTo('/mannschaften')
|
||||
|
||||
147
components/RichTextEditor.vue
Normal file
147
components/RichTextEditor.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div>
|
||||
<label v-if="label" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
{{ label }}
|
||||
<span v-if="required" class="text-red-500">*</span>
|
||||
</label>
|
||||
<div ref="editorContainer" class="border border-gray-300 rounded-lg bg-white"></div>
|
||||
<input type="hidden" :value="modelValue" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const editorContainer = ref(null)
|
||||
let quill = null
|
||||
|
||||
onMounted(async () => {
|
||||
if (process.client && editorContainer.value) {
|
||||
// Dynamisch Quill nur im Client laden
|
||||
const Quill = (await import('quill')).default
|
||||
await import('quill/dist/quill.snow.css')
|
||||
|
||||
quill = new Quill(editorContainer.value, {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'header': [1, 2, 3, false] }],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
||||
[{ 'align': [] }],
|
||||
['link', 'image'],
|
||||
['blockquote', 'code-block'],
|
||||
['clean']
|
||||
]
|
||||
},
|
||||
placeholder: 'Newsletter-Inhalt eingeben...'
|
||||
})
|
||||
|
||||
// Setze initialen Inhalt
|
||||
if (props.modelValue) {
|
||||
quill.root.innerHTML = props.modelValue
|
||||
}
|
||||
|
||||
// Emitiere Änderungen
|
||||
quill.on('text-change', () => {
|
||||
const html = quill.root.innerHTML
|
||||
// Prüfe ob Inhalt wirklich geändert wurde (nicht nur leere Tags)
|
||||
const textContent = quill.getText().trim()
|
||||
if (textContent || html !== '<p><br></p>') {
|
||||
emit('update:modelValue', html)
|
||||
} else {
|
||||
emit('update:modelValue', '')
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (quill && quill.root.innerHTML !== newValue) {
|
||||
// Temporär Event-Listener entfernen um Endlosschleife zu vermeiden
|
||||
const currentContent = quill.root.innerHTML
|
||||
if (currentContent !== newValue) {
|
||||
quill.root.innerHTML = newValue || ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (quill) {
|
||||
quill = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Quill Editor Styles */
|
||||
.ql-container {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.ql-editor {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.ql-editor.ql-blank::before {
|
||||
color: #9ca3af;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.ql-toolbar {
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.ql-container {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.ql-snow .ql-stroke {
|
||||
stroke: #374151;
|
||||
}
|
||||
|
||||
.ql-snow .ql-fill {
|
||||
fill: #374151;
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker-label {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.ql-snow .ql-tooltip {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.ql-snow .ql-tooltip input[type=text] {
|
||||
border: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.ql-snow .ql-tooltip a.ql-action::after {
|
||||
border-right: 1px solid #d1d5db;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user