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:
@@ -46,6 +46,7 @@
|
||||
<option value="mitglied">Mitglied</option>
|
||||
<option value="vorstand">Vorstand</option>
|
||||
<option value="admin">Administrator</option>
|
||||
<option value="newsletter">Newsletter</option>
|
||||
</select>
|
||||
|
||||
<!-- Approve Button -->
|
||||
@@ -112,20 +113,27 @@
|
||||
<div class="text-sm text-gray-600">{{ user.phone || '-' }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<select
|
||||
v-model="user.role"
|
||||
@change="updateUserRole(user)"
|
||||
class="px-3 py-1 border border-gray-300 rounded text-sm"
|
||||
:class="{
|
||||
'bg-red-50 border-red-300': user.role === 'admin',
|
||||
'bg-blue-50 border-blue-300': user.role === 'vorstand',
|
||||
'bg-gray-50 border-gray-300': user.role === 'mitglied'
|
||||
}"
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="role in (user.roles || (user.role ? [user.role] : ['mitglied']))"
|
||||
:key="role"
|
||||
class="px-2 py-1 text-xs font-medium rounded"
|
||||
:class="{
|
||||
'bg-red-100 text-red-800': role === 'admin',
|
||||
'bg-blue-100 text-blue-800': role === 'vorstand',
|
||||
'bg-green-100 text-green-800': role === 'newsletter',
|
||||
'bg-gray-100 text-gray-800': role === 'mitglied'
|
||||
}"
|
||||
>
|
||||
{{ role === 'admin' ? 'Admin' : role === 'vorstand' ? 'Vorstand' : role === 'newsletter' ? 'Newsletter' : 'Mitglied' }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
@click="openRoleModal(user)"
|
||||
class="mt-1 text-xs text-primary-600 hover:text-primary-800"
|
||||
>
|
||||
<option value="mitglied">Mitglied</option>
|
||||
<option value="vorstand">Vorstand</option>
|
||||
<option value="admin">Administrator</option>
|
||||
</select>
|
||||
Bearbeiten
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-600">
|
||||
@@ -162,6 +170,79 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Role Edit Modal -->
|
||||
<div
|
||||
v-if="showRoleModal && editingUser"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
|
||||
@click.self="closeRoleModal"
|
||||
>
|
||||
<div class="bg-white rounded-xl shadow-2xl max-w-md w-full p-6">
|
||||
<h2 class="text-2xl font-display font-bold text-gray-900 mb-4">
|
||||
Rollen bearbeiten: {{ editingUser.name }}
|
||||
</h2>
|
||||
|
||||
<div class="space-y-3 mb-6">
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="selectedRoles"
|
||||
value="mitglied"
|
||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-gray-700">Mitglied</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="selectedRoles"
|
||||
value="vorstand"
|
||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-gray-700">Vorstand</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="selectedRoles"
|
||||
value="newsletter"
|
||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-gray-700">Newsletter</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="selectedRoles"
|
||||
value="admin"
|
||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-gray-700">Administrator</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedRoles.length === 0" class="mb-4 text-sm text-red-600">
|
||||
Mindestens eine Rolle muss ausgewählt werden.
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeRoleModal"
|
||||
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
@click="saveUserRoles"
|
||||
:disabled="selectedRoles.length === 0"
|
||||
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -173,11 +254,17 @@ const allUsers = ref([])
|
||||
const currentUserId = ref(null)
|
||||
const successMessage = ref('')
|
||||
const errorMessage = ref('')
|
||||
const showRoleModal = ref(false)
|
||||
const editingUser = ref(null)
|
||||
const selectedRoles = ref([])
|
||||
|
||||
const pendingUsers = computed(() => {
|
||||
return allUsers.value
|
||||
.filter(u => u.active === false)
|
||||
.map(u => ({ ...u, selectedRole: u.role || 'mitglied' }))
|
||||
.map(u => ({
|
||||
...u,
|
||||
selectedRole: (u.roles && u.roles.length > 0) ? u.roles[0] : (u.role || 'mitglied')
|
||||
}))
|
||||
})
|
||||
|
||||
const activeUsers = computed(() => {
|
||||
@@ -210,7 +297,7 @@ const approveUser = async (user) => {
|
||||
method: 'POST',
|
||||
body: {
|
||||
userId: user.id,
|
||||
role: user.selectedRole
|
||||
roles: [user.selectedRole || 'mitglied']
|
||||
}
|
||||
})
|
||||
|
||||
@@ -224,6 +311,41 @@ const approveUser = async (user) => {
|
||||
}
|
||||
}
|
||||
|
||||
function openRoleModal(user) {
|
||||
editingUser.value = user
|
||||
selectedRoles.value = user.roles || (user.role ? [user.role] : ['mitglied'])
|
||||
showRoleModal.value = true
|
||||
}
|
||||
|
||||
function closeRoleModal() {
|
||||
showRoleModal.value = false
|
||||
editingUser.value = null
|
||||
selectedRoles.value = []
|
||||
}
|
||||
|
||||
async function saveUserRoles() {
|
||||
if (!editingUser.value || selectedRoles.value.length === 0) return
|
||||
|
||||
try {
|
||||
await $fetch('/api/cms/users/update-role', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
userId: editingUser.value.id,
|
||||
roles: selectedRoles.value
|
||||
}
|
||||
})
|
||||
|
||||
successMessage.value = `Rollen von ${editingUser.value.name} wurden aktualisiert`
|
||||
setTimeout(() => successMessage.value = '', 3000)
|
||||
|
||||
closeRoleModal()
|
||||
await loadUsers()
|
||||
} catch (error) {
|
||||
errorMessage.value = 'Fehler beim Aktualisieren der Rollen'
|
||||
setTimeout(() => errorMessage.value = '', 3000)
|
||||
}
|
||||
}
|
||||
|
||||
const rejectUser = async (user) => {
|
||||
window.showConfirmModal('Registrierung ablehnen', `Möchten Sie die Registrierung von ${user.name} wirklich ablehnen?`, async () => {
|
||||
try {
|
||||
@@ -241,24 +363,6 @@ const rejectUser = async (user) => {
|
||||
})
|
||||
}
|
||||
|
||||
const updateUserRole = async (user) => {
|
||||
try {
|
||||
await $fetch('/api/cms/users/update-role', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
userId: user.id,
|
||||
role: user.role
|
||||
}
|
||||
})
|
||||
|
||||
successMessage.value = `Rolle von ${user.name} wurde aktualisiert`
|
||||
setTimeout(() => successMessage.value = '', 3000)
|
||||
} catch (error) {
|
||||
errorMessage.value = 'Fehler beim Aktualisieren der Rolle'
|
||||
setTimeout(() => errorMessage.value = '', 3000)
|
||||
await loadUsers() // Reload to revert changes
|
||||
}
|
||||
}
|
||||
|
||||
const deactivateUser = async (user) => {
|
||||
window.showConfirmModal('Benutzer deaktivieren', `Möchten Sie ${user.name} wirklich deaktivieren?`, async () => {
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
|
||||
<!-- Benutzerverwaltung (nur für Admin) -->
|
||||
<NuxtLink
|
||||
v-if="authStore.role === 'admin'"
|
||||
v-if="authStore.hasRole('admin')"
|
||||
to="/cms/benutzer"
|
||||
class="bg-white p-6 rounded-xl shadow-lg border border-gray-100 hover:shadow-xl transition-all group"
|
||||
>
|
||||
|
||||
884
pages/cms/newsletter.vue
Normal file
884
pages/cms/newsletter.vue
Normal file
@@ -0,0 +1,884 @@
|
||||
<template>
|
||||
<div class="min-h-full bg-gray-50">
|
||||
<!-- Fixed Header -->
|
||||
<div class="fixed top-20 left-0 right-0 z-40 bg-white border-b border-gray-200 shadow-sm">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3 sm:py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl sm:text-4xl font-display font-bold text-gray-900">
|
||||
Newsletter
|
||||
</h1>
|
||||
<div class="w-16 sm:w-24 h-1 bg-primary-600 mt-1 sm:mt-2" />
|
||||
</div>
|
||||
<div class="space-x-3">
|
||||
<button
|
||||
v-if="canCreateGroup"
|
||||
@click="showCreateGroupModal = true"
|
||||
class="inline-flex items-center px-3 py-1.5 sm:px-4 sm:py-2 rounded-lg bg-primary-600 text-white hover:bg-primary-700 text-sm sm:text-base"
|
||||
>
|
||||
<Plus :size="16" class="mr-2" />
|
||||
Neue Newsletter-Gruppe
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="pt-28 sm:pt-32 pb-16">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-12">
|
||||
<Loader2 :size="40" class="animate-spin text-primary-600" />
|
||||
</div>
|
||||
|
||||
<!-- Newsletter Groups List -->
|
||||
<div v-else class="space-y-6">
|
||||
<div
|
||||
v-for="group in groups"
|
||||
:key="group.id"
|
||||
class="bg-white rounded-xl shadow-lg border border-gray-100 overflow-hidden"
|
||||
>
|
||||
<!-- Group Header -->
|
||||
<div class="p-6 border-b border-gray-200">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-3 mb-2">
|
||||
<h3 class="text-xl font-semibold text-gray-900">{{ group.name }}</h3>
|
||||
<span
|
||||
class="px-2 py-1 text-xs font-medium rounded bg-blue-100 text-blue-800"
|
||||
>
|
||||
{{ group.type === 'subscription' ? 'Abonnenten' : 'Gruppe' }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="group.description" class="text-sm text-gray-600 mb-2">
|
||||
{{ group.description }}
|
||||
</p>
|
||||
<div class="flex items-center space-x-4 text-sm text-gray-500">
|
||||
<span>Erstellt: {{ formatDate(group.createdAt) }}</span>
|
||||
<span>{{ group.postCount || 0 }} Posts</span>
|
||||
<span v-if="group.type === 'group'">
|
||||
Zielgruppe: {{ formatTargetGroup(group.targetGroup) }}
|
||||
</span>
|
||||
<span v-if="group.type === 'subscription'">
|
||||
{{ group.sendToExternal ? 'Intern & Extern' : 'Nur Intern' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 ml-4">
|
||||
<button
|
||||
v-if="group.type === 'subscription'"
|
||||
@click="showSubscribersModal(group)"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm"
|
||||
>
|
||||
<Users :size="16" class="inline mr-1" />
|
||||
Abonnenten
|
||||
</button>
|
||||
<button
|
||||
@click="showPostModal(group)"
|
||||
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors text-sm"
|
||||
>
|
||||
<Plus :size="16" class="inline mr-1" />
|
||||
Post hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Posts List Header -->
|
||||
<div v-if="groupPosts[group.id] && groupPosts[group.id].length > 0" class="border-t border-gray-200">
|
||||
<button
|
||||
@click="toggleGroupPosts(group.id)"
|
||||
class="w-full px-6 py-4 flex items-center justify-between hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<span class="text-sm font-medium text-gray-700">
|
||||
Posts ({{ groupPosts[group.id].length }})
|
||||
</span>
|
||||
<svg
|
||||
:class="['w-5 h-5 text-gray-500 transition-transform', expandedGroups[group.id] ? 'rotate-180' : '']"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Collapsible Posts List -->
|
||||
<div v-show="expandedGroups[group.id]" class="divide-y divide-gray-200">
|
||||
<div
|
||||
v-for="post in groupPosts[group.id]"
|
||||
:key="post.id"
|
||||
class="p-6 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h4 class="text-lg font-semibold text-gray-900 mb-2">{{ post.title }}</h4>
|
||||
<div class="flex items-center space-x-4 text-sm text-gray-500 mb-3">
|
||||
<span v-if="post.sentAt">Versendet: {{ formatDate(post.sentAt) }}</span>
|
||||
<span v-else class="text-yellow-600">Nicht versendet</span>
|
||||
<span v-if="post.sentTo && post.sentTo.total > 0">
|
||||
Empfänger: {{ post.sentTo.sent }}/{{ post.sentTo.total }}
|
||||
</span>
|
||||
<span v-else-if="post.sentTo && post.sentTo.total === 0" class="text-gray-400">
|
||||
Keine Empfänger gefunden
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-html="post.content.substring(0, 200) + (post.content.length > 200 ? '...' : '')"
|
||||
class="text-sm text-gray-600 prose prose-sm max-w-none mb-3"
|
||||
></div>
|
||||
|
||||
<!-- Empfängerliste (collapsible) -->
|
||||
<div v-if="post.sentTo && post.sentTo.recipients && post.sentTo.recipients.length > 0" class="border-t border-gray-200 mt-3 pt-3">
|
||||
<button
|
||||
@click="togglePostRecipients(post.id)"
|
||||
class="w-full flex items-center justify-between text-sm text-gray-600 hover:text-gray-900 transition-colors"
|
||||
>
|
||||
<span class="font-medium">
|
||||
Empfänger ({{ post.sentTo.recipients.length }})
|
||||
</span>
|
||||
<svg
|
||||
:class="['w-4 h-4 transition-transform', expandedPosts[post.id] ? 'rotate-180' : '']"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div v-show="expandedPosts[post.id]" class="mt-3 space-y-2">
|
||||
<div
|
||||
v-for="(recipient, idx) in post.sentTo.recipients"
|
||||
:key="idx"
|
||||
class="flex items-center justify-between text-sm py-1 px-2 rounded"
|
||||
:class="recipient.sent ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'"
|
||||
>
|
||||
<div>
|
||||
<span class="font-medium">{{ recipient.email }}</span>
|
||||
<span v-if="recipient.name" class="text-gray-600 ml-2">({{ recipient.name }})</span>
|
||||
</div>
|
||||
<span class="text-xs">
|
||||
{{ recipient.sent ? '✓ Versendet' : '✗ Fehler' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="post.sentTo && post.sentTo.total === 0" class="border-t border-gray-200 mt-3 pt-3 text-sm text-gray-500">
|
||||
Keine Empfänger gefunden
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="p-6 text-center text-gray-500 text-sm border-t border-gray-200">
|
||||
Noch keine Posts in dieser Gruppe
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="groups.length === 0" class="text-center py-12 text-gray-500">
|
||||
Noch keine Newsletter-Gruppen vorhanden.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Group Modal -->
|
||||
<div
|
||||
v-if="showCreateGroupModal"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
|
||||
@click.self="closeGroupModal"
|
||||
>
|
||||
<div class="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] flex flex-col">
|
||||
<div class="p-6 border-b border-gray-200 flex-shrink-0">
|
||||
<h3 class="text-lg font-semibold text-gray-900">
|
||||
Neue Newsletter-Gruppe erstellen
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="overflow-y-auto flex-1 p-6">
|
||||
<form id="group-form" @submit.prevent="saveGroup" class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
v-model="groupFormData.name"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="z.B. Allgemeiner Newsletter"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Beschreibung (optional)
|
||||
</label>
|
||||
<textarea
|
||||
v-model="groupFormData.description"
|
||||
rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="Beschreibung der Newsletter-Gruppe"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Typ *
|
||||
</label>
|
||||
<select
|
||||
v-model="groupFormData.type"
|
||||
required
|
||||
@change="onGroupTypeChange"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
>
|
||||
<option value="">Bitte wählen</option>
|
||||
<option value="subscription">Abonnenten-Newsletter</option>
|
||||
<option value="group">Gruppen-Newsletter</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="groupFormData.type === 'subscription'">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Empfänger
|
||||
</label>
|
||||
<select
|
||||
v-model="groupFormData.sendToExternal"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
>
|
||||
<option :value="false">Nur Intern</option>
|
||||
<option :value="true">Auch Extern</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="groupFormData.type === 'group'">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Zielgruppe *
|
||||
</label>
|
||||
<select
|
||||
v-model="groupFormData.targetGroup"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
>
|
||||
<option value="">Bitte wählen</option>
|
||||
<option value="alle">Alle</option>
|
||||
<option value="erwachsene">Erwachsene</option>
|
||||
<option value="nachwuchs">Nachwuchs</option>
|
||||
<option value="mannschaftsspieler">Mannschaftsspieler</option>
|
||||
<option value="vorstand">Vorstand</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="p-6 border-t border-gray-200 flex justify-end space-x-3 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeGroupModal"
|
||||
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
form="group-form"
|
||||
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
Erstellen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Post Modal -->
|
||||
<div
|
||||
v-if="showPostModalForGroup"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
|
||||
@click.self="closePostModal"
|
||||
>
|
||||
<div class="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] flex flex-col">
|
||||
<div class="p-6 border-b border-gray-200 flex-shrink-0">
|
||||
<h3 class="text-lg font-semibold text-gray-900">
|
||||
Post zu "{{ showPostModalForGroup.name }}" hinzufügen
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Der Post wird automatisch an alle Abonnenten dieser Gruppe versendet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="overflow-y-auto flex-1 p-6">
|
||||
<form id="post-form" @submit.prevent="savePost" class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Titel *
|
||||
</label>
|
||||
<input
|
||||
v-model="postFormData.title"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="Post-Titel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<RichTextEditor
|
||||
v-model="postFormData.content"
|
||||
label="Inhalt *"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="p-6 border-t border-gray-200 flex-shrink-0">
|
||||
<!-- Erfolgsmeldung -->
|
||||
<div v-if="postSuccessMessage" class="space-y-4">
|
||||
<div class="p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm font-medium text-green-800">
|
||||
{{ postSuccessMessage }}
|
||||
</p>
|
||||
<div v-if="postSuccessStats" class="mt-2 text-sm text-green-700">
|
||||
<p>Empfänger: {{ postSuccessStats.sent }}/{{ postSuccessStats.total }} erfolgreich versendet</p>
|
||||
<div v-if="postSuccessStats.failed > 0" class="mt-2">
|
||||
<p class="font-medium">⚠️ {{ postSuccessStats.failed }} Fehler beim Versenden:</p>
|
||||
<ul v-if="postSuccessStats.errorDetails" class="list-disc list-inside mt-1 space-y-1">
|
||||
<li v-for="err in postSuccessStats.errorDetails" :key="err.email">
|
||||
{{ err.email }}: {{ err.error }}
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else-if="postSuccessStats.failedEmails" class="mt-1">
|
||||
{{ postSuccessStats.failedEmails.join(', ') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
@click="closePostModal"
|
||||
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formular-Buttons -->
|
||||
<div v-else class="flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="closePostModal"
|
||||
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
form="post-form"
|
||||
:disabled="isSendingPost"
|
||||
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{{ isSendingPost ? 'Wird versendet...' : 'Erstellen & Versenden' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subscribers Modal -->
|
||||
<div
|
||||
v-if="showSubscribersModalForGroup"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
|
||||
@click.self="closeSubscribersModal"
|
||||
>
|
||||
<div class="bg-white rounded-xl shadow-2xl max-w-4xl w-full max-h-[90vh] flex flex-col">
|
||||
<div class="p-6 border-b border-gray-200 flex-shrink-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-display font-bold text-gray-900">
|
||||
Abonnenten: {{ showSubscribersModalForGroup.name }}
|
||||
</h2>
|
||||
<button
|
||||
@click="showAddSubscriberModal = true"
|
||||
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors text-sm flex items-center"
|
||||
>
|
||||
<Plus :size="16" class="mr-2" />
|
||||
Empfänger hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-y-auto flex-1 p-6">
|
||||
<div v-if="isLoadingSubscribers" class="flex items-center justify-center py-12">
|
||||
<Loader2 :size="40" class="animate-spin text-primary-600" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="subscribers.length === 0" class="text-center py-12 text-gray-500">
|
||||
Keine Abonnenten gefunden.
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div class="bg-gray-50 rounded-lg p-4 mb-4">
|
||||
<p class="text-sm text-gray-600">
|
||||
<strong>{{ subscribers.length }}</strong> Abonnent{{ subscribers.length !== 1 ? 'en' : '' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
E-Mail
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Angemeldet
|
||||
</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Aktionen
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr v-for="subscriber in subscribers" :key="subscriber.id" class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ subscriber.email }}
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-600">
|
||||
{{ subscriber.name || '-' }}
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
<span
|
||||
:class="[
|
||||
'px-2 py-1 text-xs font-medium rounded-full',
|
||||
subscriber.confirmed && !subscriber.unsubscribedAt
|
||||
? 'bg-green-100 text-green-800'
|
||||
: subscriber.unsubscribedAt
|
||||
? 'bg-red-100 text-red-800'
|
||||
: 'bg-yellow-100 text-yellow-800'
|
||||
]"
|
||||
>
|
||||
{{
|
||||
subscriber.confirmed && !subscriber.unsubscribedAt
|
||||
? 'Bestätigt'
|
||||
: subscriber.unsubscribedAt
|
||||
? 'Abgemeldet'
|
||||
: 'Ausstehend'
|
||||
}}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-600">
|
||||
{{ formatDate(subscriber.subscribedAt) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap text-right text-sm font-medium">
|
||||
<button
|
||||
@click="removeSubscriber(subscriber.id)"
|
||||
class="text-red-600 hover:text-red-900"
|
||||
title="Abonnent entfernen"
|
||||
>
|
||||
<Trash2 :size="18" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 border-t border-gray-200 flex justify-end flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeSubscribersModal"
|
||||
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Subscriber Modal -->
|
||||
<div
|
||||
v-if="showAddSubscriberModal"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
|
||||
@click.self="closeAddSubscriberModal"
|
||||
>
|
||||
<div class="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[90vh] flex flex-col">
|
||||
<div class="p-6 border-b border-gray-200 flex-shrink-0">
|
||||
<h2 class="text-2xl font-display font-bold text-gray-900">
|
||||
Empfänger hinzufügen: {{ showSubscribersModalForGroup?.name }}
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Der Empfänger erhält eine Bestätigungsmail mit Ihrer individuellen Nachricht.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="overflow-y-auto flex-1 p-6">
|
||||
<form id="add-subscriber-form" @submit.prevent="addSubscriber" class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
E-Mail-Adresse *
|
||||
</label>
|
||||
<input
|
||||
v-model="addSubscriberForm.email"
|
||||
type="email"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="empfaenger@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Name (optional)
|
||||
</label>
|
||||
<input
|
||||
v-model="addSubscriberForm.name"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="Name des Empfängers"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Individuelle Nachricht (optional)
|
||||
</label>
|
||||
<textarea
|
||||
v-model="addSubscriberForm.customMessage"
|
||||
rows="4"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="Diese Nachricht wird in der Bestätigungsmail angezeigt..."
|
||||
></textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Diese Nachricht wird in der Bestätigungsmail angezeigt, um den Empfänger persönlich anzusprechen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="addSubscriberError" class="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
||||
{{ addSubscriberError }}
|
||||
</div>
|
||||
|
||||
<div v-if="addSubscriberSuccess" class="p-3 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">
|
||||
{{ addSubscriberSuccess }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="p-6 border-t border-gray-200 flex justify-end space-x-3 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeAddSubscriberModal"
|
||||
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
||||
:disabled="isAddingSubscriber"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
form="add-subscriber-form"
|
||||
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors flex items-center disabled:opacity-50"
|
||||
:disabled="isAddingSubscriber"
|
||||
>
|
||||
<Loader2 v-if="isAddingSubscriber" :size="16" class="animate-spin mr-2" />
|
||||
<span>{{ isAddingSubscriber ? 'Wird hinzugefügt...' : 'Hinzufügen' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Plus, Loader2, Users, Trash2 } from 'lucide-vue-next'
|
||||
import RichTextEditor from '~/components/RichTextEditor.vue'
|
||||
|
||||
useHead({
|
||||
title: 'Newsletter-Verwaltung - CMS - Harheimer TC',
|
||||
})
|
||||
|
||||
const groups = ref([])
|
||||
const groupPosts = ref({})
|
||||
const expandedGroups = ref({}) // Track which groups have expanded posts
|
||||
const expandedPosts = ref({}) // Track which posts have expanded recipients
|
||||
const isLoading = ref(true)
|
||||
const showCreateGroupModal = ref(false)
|
||||
const showPostModalForGroup = ref(null)
|
||||
const isSendingPost = ref(false)
|
||||
const postSuccessMessage = ref(null)
|
||||
const postSuccessStats = ref(null)
|
||||
const showSubscribersModalForGroup = ref(null)
|
||||
const subscribers = ref([])
|
||||
const isLoadingSubscribers = ref(false)
|
||||
const showAddSubscriberModal = ref(false)
|
||||
const addSubscriberForm = ref({
|
||||
email: '',
|
||||
name: '',
|
||||
customMessage: ''
|
||||
})
|
||||
const isAddingSubscriber = ref(false)
|
||||
const addSubscriberError = ref('')
|
||||
const addSubscriberSuccess = ref('')
|
||||
|
||||
const groupFormData = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
type: '',
|
||||
targetGroup: '',
|
||||
sendToExternal: false
|
||||
})
|
||||
|
||||
const postFormData = ref({
|
||||
title: '',
|
||||
content: ''
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await loadGroups()
|
||||
})
|
||||
|
||||
async function loadGroups() {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await $fetch('/api/newsletter/groups/list')
|
||||
groups.value = response.groups || []
|
||||
|
||||
// Lade Posts für jede Gruppe
|
||||
for (const group of groups.value) {
|
||||
await loadPostsForGroup(group.id)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Newsletter-Gruppen:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPostsForGroup(groupId) {
|
||||
try {
|
||||
const response = await $fetch(`/api/newsletter/groups/${groupId}/posts/list`)
|
||||
groupPosts.value[groupId] = response.posts || []
|
||||
} catch (error) {
|
||||
console.error(`Fehler beim Laden der Posts für Gruppe ${groupId}:`, error)
|
||||
groupPosts.value[groupId] = []
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
function formatTargetGroup(group) {
|
||||
const groups = {
|
||||
alle: 'Alle',
|
||||
erwachsene: 'Erwachsene',
|
||||
nachwuchs: 'Nachwuchs',
|
||||
mannschaftsspieler: 'Mannschaftsspieler',
|
||||
vorstand: 'Vorstand'
|
||||
}
|
||||
return groups[group] || group
|
||||
}
|
||||
|
||||
function toggleGroupPosts(groupId) {
|
||||
expandedGroups.value[groupId] = !expandedGroups.value[groupId]
|
||||
}
|
||||
|
||||
function togglePostRecipients(postId) {
|
||||
expandedPosts.value[postId] = !expandedPosts.value[postId]
|
||||
}
|
||||
|
||||
function onGroupTypeChange() {
|
||||
if (groupFormData.value.type === 'subscription') {
|
||||
groupFormData.value.targetGroup = ''
|
||||
} else if (groupFormData.value.type === 'group') {
|
||||
groupFormData.value.sendToExternal = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeGroupModal() {
|
||||
showCreateGroupModal.value = false
|
||||
groupFormData.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
type: '',
|
||||
targetGroup: '',
|
||||
sendToExternal: false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveGroup() {
|
||||
try {
|
||||
await $fetch('/api/newsletter/groups/create', {
|
||||
method: 'POST',
|
||||
body: groupFormData.value
|
||||
})
|
||||
|
||||
await loadGroups()
|
||||
closeGroupModal()
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Newsletter-Gruppe:', error)
|
||||
alert(error.data?.statusMessage || 'Fehler beim Erstellen der Newsletter-Gruppe')
|
||||
}
|
||||
}
|
||||
|
||||
function showPostModal(group) {
|
||||
showPostModalForGroup.value = group
|
||||
postFormData.value = {
|
||||
title: '',
|
||||
content: ''
|
||||
}
|
||||
postSuccessMessage.value = null
|
||||
postSuccessStats.value = null
|
||||
}
|
||||
|
||||
function closePostModal() {
|
||||
showPostModalForGroup.value = null
|
||||
postFormData.value = {
|
||||
title: '',
|
||||
content: ''
|
||||
}
|
||||
postSuccessMessage.value = null
|
||||
postSuccessStats.value = null
|
||||
}
|
||||
|
||||
async function showSubscribersModal(group) {
|
||||
showSubscribersModalForGroup.value = group
|
||||
await loadSubscribers(group.id)
|
||||
}
|
||||
|
||||
function closeSubscribersModal() {
|
||||
showSubscribersModalForGroup.value = null
|
||||
subscribers.value = []
|
||||
}
|
||||
|
||||
async function loadSubscribers(groupId) {
|
||||
try {
|
||||
isLoadingSubscribers.value = true
|
||||
const response = await $fetch(`/api/newsletter/groups/${groupId}/subscribers/list`)
|
||||
subscribers.value = response.subscribers || []
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Abonnenten:', error)
|
||||
alert(error.data?.statusMessage || 'Fehler beim Laden der Abonnenten')
|
||||
subscribers.value = []
|
||||
} finally {
|
||||
isLoadingSubscribers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function removeSubscriber(subscriberId) {
|
||||
if (!confirm('Möchten Sie diesen Abonnenten wirklich entfernen?')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await $fetch(`/api/newsletter/groups/${showSubscribersModalForGroup.value.id}/subscribers/remove`, {
|
||||
method: 'POST',
|
||||
body: { subscriberId }
|
||||
})
|
||||
|
||||
await loadSubscribers(showSubscribersModalForGroup.value.id)
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Entfernen des Abonnenten:', error)
|
||||
alert(error.data?.statusMessage || 'Fehler beim Entfernen des Abonnenten')
|
||||
}
|
||||
}
|
||||
|
||||
function closeAddSubscriberModal() {
|
||||
showAddSubscriberModal.value = false
|
||||
addSubscriberForm.value = {
|
||||
email: '',
|
||||
name: '',
|
||||
customMessage: ''
|
||||
}
|
||||
addSubscriberError.value = ''
|
||||
addSubscriberSuccess.value = ''
|
||||
}
|
||||
|
||||
async function addSubscriber() {
|
||||
if (!showSubscribersModalForGroup.value) return
|
||||
|
||||
isAddingSubscriber.value = true
|
||||
addSubscriberError.value = ''
|
||||
addSubscriberSuccess.value = ''
|
||||
|
||||
try {
|
||||
const response = await $fetch(`/api/newsletter/groups/${showSubscribersModalForGroup.value.id}/subscribers/add`, {
|
||||
method: 'POST',
|
||||
body: addSubscriberForm.value
|
||||
})
|
||||
|
||||
addSubscriberSuccess.value = response.message || 'Empfänger erfolgreich hinzugefügt'
|
||||
|
||||
// Nach 2 Sekunden schließen und Liste aktualisieren
|
||||
setTimeout(async () => {
|
||||
await loadSubscribers(showSubscribersModalForGroup.value.id)
|
||||
closeAddSubscriberModal()
|
||||
}, 2000)
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Hinzufügen des Empfängers:', error)
|
||||
addSubscriberError.value = error.data?.statusMessage || error.message || 'Fehler beim Hinzufügen des Empfängers'
|
||||
} finally {
|
||||
isAddingSubscriber.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function savePost() {
|
||||
if (!showPostModalForGroup.value) return
|
||||
|
||||
if (!postFormData.value.title || !postFormData.value.content ||
|
||||
!postFormData.value.content.trim() || postFormData.value.content === '<p><br></p>') {
|
||||
alert('Bitte geben Sie einen Titel und Inhalt ein.')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isSendingPost.value = true
|
||||
|
||||
const response = await $fetch(`/api/newsletter/groups/${showPostModalForGroup.value.id}/posts/create`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
title: postFormData.value.title,
|
||||
content: postFormData.value.content
|
||||
}
|
||||
})
|
||||
|
||||
postSuccessMessage.value = 'Post erfolgreich erstellt und versendet!'
|
||||
postSuccessStats.value = response.stats
|
||||
|
||||
await loadPostsForGroup(showPostModalForGroup.value.id)
|
||||
await loadGroups() // Aktualisiere Post-Count
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Posts:', error)
|
||||
alert(error.data?.statusMessage || 'Fehler beim Erstellen des Posts')
|
||||
} finally {
|
||||
isSendingPost.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -125,7 +125,8 @@ const handleLogin = async () => {
|
||||
|
||||
// Redirect based on role
|
||||
setTimeout(() => {
|
||||
if (response.user.role === 'admin' || response.user.role === 'vorstand') {
|
||||
const roles = response.user.roles || (response.user.role ? [response.user.role] : [])
|
||||
if (roles.includes('admin') || roles.includes('vorstand') || roles.includes('newsletter')) {
|
||||
router.push('/cms')
|
||||
} else {
|
||||
router.push('/mitgliederbereich')
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">E-Mail</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Telefon</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Mannschaft</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th v-if="canEdit" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Aktionen</th>
|
||||
</tr>
|
||||
@@ -79,6 +80,15 @@
|
||||
</template>
|
||||
<span v-else class="text-sm text-gray-400">Nur für Vorstand</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
<span
|
||||
v-if="member.isMannschaftsspieler"
|
||||
class="px-2 py-1 bg-blue-100 text-blue-800 text-xs font-medium rounded-full"
|
||||
>
|
||||
Ja
|
||||
</span>
|
||||
<span v-else class="text-sm text-gray-400">-</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span
|
||||
@@ -153,6 +163,12 @@
|
||||
>
|
||||
Aus Login-System
|
||||
</span>
|
||||
<span
|
||||
v-if="member.isMannschaftsspieler"
|
||||
class="ml-2 px-2 py-1 bg-blue-100 text-blue-800 text-xs font-medium rounded-full"
|
||||
>
|
||||
Mannschaftsspieler
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid sm:grid-cols-2 gap-3 text-gray-600">
|
||||
@@ -296,6 +312,19 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
v-model="formData.isMannschaftsspieler"
|
||||
type="checkbox"
|
||||
id="isMannschaftsspieler"
|
||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||
:disabled="isSaving"
|
||||
/>
|
||||
<label for="isMannschaftsspieler" class="ml-2 block text-sm font-medium text-gray-700">
|
||||
Mannschaftsspieler
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="errorMessage" class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm">
|
||||
<AlertCircle :size="20" class="mr-2" />
|
||||
{{ errorMessage }}
|
||||
@@ -494,18 +523,17 @@ const formData = ref({
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
notes: ''
|
||||
notes: '',
|
||||
isMannschaftsspieler: false
|
||||
})
|
||||
|
||||
const canEdit = computed(() => {
|
||||
return authStore.role === 'admin' || authStore.role === 'vorstand'
|
||||
return authStore.hasAnyRole('admin', 'vorstand')
|
||||
})
|
||||
|
||||
const canViewContactData = computed(() => {
|
||||
// Explicitly check for 'vorstand' role only
|
||||
const role = authStore.role
|
||||
console.log('Current role:', role, 'Can view contact:', role === 'vorstand')
|
||||
return role === 'vorstand'
|
||||
return authStore.hasRole('vorstand')
|
||||
})
|
||||
|
||||
const loadMembers = async () => {
|
||||
@@ -529,7 +557,8 @@ const openAddModal = () => {
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
notes: ''
|
||||
notes: '',
|
||||
isMannschaftsspieler: false
|
||||
}
|
||||
showModal.value = true
|
||||
errorMessage.value = ''
|
||||
@@ -544,7 +573,8 @@ const openEditModal = (member) => {
|
||||
email: member.email || '',
|
||||
phone: member.phone || '',
|
||||
address: member.address || '',
|
||||
notes: member.notes || ''
|
||||
notes: member.notes || '',
|
||||
isMannschaftsspieler: member.isMannschaftsspieler === true
|
||||
}
|
||||
showModal.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
@@ -245,7 +245,7 @@ const formData = ref({
|
||||
})
|
||||
|
||||
const canWrite = computed(() => {
|
||||
return authStore.role === 'admin' || authStore.role === 'vorstand'
|
||||
return authStore.hasAnyRole('admin', 'vorstand')
|
||||
})
|
||||
|
||||
const loadNews = async () => {
|
||||
|
||||
75
pages/newsletter/confirm.vue
Normal file
75
pages/newsletter/confirm.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="min-h-full py-16 bg-gray-50">
|
||||
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-white rounded-xl shadow-lg p-8 text-center">
|
||||
<div v-if="loading" class="py-12">
|
||||
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-8 h-8 text-blue-600 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-lg text-gray-600">Newsletter-Anmeldung wird bestätigt...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="py-12">
|
||||
<div class="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-3xl font-display font-bold text-gray-900 mb-4">
|
||||
Fehler
|
||||
</h1>
|
||||
<p class="text-lg text-gray-600 mb-8">
|
||||
{{ error }}
|
||||
</p>
|
||||
<NuxtLink
|
||||
to="/newsletter/subscribe"
|
||||
class="inline-block px-6 py-3 bg-primary-600 text-white font-semibold rounded-lg hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
Zurück zur Anmeldung
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
useHead({
|
||||
title: 'Newsletter bestätigen - Harheimer TC',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
const token = route.query.token
|
||||
|
||||
if (!token) {
|
||||
error.value = 'Bestätigungstoken fehlt'
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Rufe den API-Endpoint auf, der die Bestätigung durchführt
|
||||
const response = await $fetch(`/api/newsletter/confirm?token=${token}`)
|
||||
|
||||
// Wenn erfolgreich, weiterleiten zur Bestätigungsseite
|
||||
if (response.alreadyConfirmed) {
|
||||
await navigateTo('/newsletter/confirmed?already=true')
|
||||
} else {
|
||||
await navigateTo('/newsletter/confirmed')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler bei Newsletter-Bestätigung:', err)
|
||||
error.value = err.data?.statusMessage || err.message || 'Fehler bei der Newsletter-Bestätigung'
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
40
pages/newsletter/confirmed.vue
Normal file
40
pages/newsletter/confirmed.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="min-h-full py-16 bg-gray-50">
|
||||
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-white rounded-xl shadow-lg p-8 text-center">
|
||||
<div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 class="text-3xl font-display font-bold text-gray-900 mb-4">
|
||||
{{ alreadyConfirmed ? 'Bereits bestätigt' : 'Anmeldung bestätigt!' }}
|
||||
</h1>
|
||||
|
||||
<p class="text-lg text-gray-600 mb-8">
|
||||
{{ alreadyConfirmed
|
||||
? 'Ihre Newsletter-Anmeldung wurde bereits bestätigt.'
|
||||
: 'Vielen Dank! Ihre Newsletter-Anmeldung wurde erfolgreich bestätigt. Sie erhalten ab sofort unseren Newsletter.' }}
|
||||
</p>
|
||||
|
||||
<NuxtLink
|
||||
to="/"
|
||||
class="inline-block px-6 py-3 bg-primary-600 text-white font-semibold rounded-lg hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
Zur Startseite
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const route = useRoute()
|
||||
const alreadyConfirmed = route.query.already === 'true'
|
||||
|
||||
useHead({
|
||||
title: 'Newsletter bestätigt - Harheimer TC',
|
||||
})
|
||||
</script>
|
||||
|
||||
179
pages/newsletter/subscribe.vue
Normal file
179
pages/newsletter/subscribe.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div class="min-h-full py-16 bg-gray-50">
|
||||
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-white rounded-xl shadow-lg p-8">
|
||||
<h1 class="text-3xl font-display font-bold text-gray-900 mb-6">
|
||||
Newsletter abonnieren
|
||||
</h1>
|
||||
<div class="w-24 h-1 bg-primary-600 mb-8" />
|
||||
|
||||
<div v-if="loadingGroups" class="text-center py-8">
|
||||
<p class="text-gray-600">Lade verfügbare Newsletter...</p>
|
||||
</div>
|
||||
|
||||
<form v-else @submit.prevent="subscribe" class="space-y-6">
|
||||
<div>
|
||||
<label for="groupId" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Newsletter auswählen *
|
||||
</label>
|
||||
<select
|
||||
id="groupId"
|
||||
v-model="form.groupId"
|
||||
required
|
||||
@change="checkSubscription"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
>
|
||||
<option value="">Bitte wählen Sie einen Newsletter</option>
|
||||
<option v-for="group in groups" :key="group.id" :value="group.id">
|
||||
{{ group.name }}
|
||||
</option>
|
||||
</select>
|
||||
<p v-if="selectedGroup?.description" class="mt-2 text-sm text-gray-600">
|
||||
{{ selectedGroup.description }}
|
||||
</p>
|
||||
<div v-if="alreadySubscribed" class="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p class="text-sm text-blue-700">
|
||||
✓ Sie sind bereits für diesen Newsletter angemeldet.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
E-Mail-Adresse *
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
required
|
||||
@blur="checkSubscription"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="ihre.email@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Name (optional)
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="Ihr Name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-if="success" class="p-4 bg-green-50 border border-green-200 rounded-lg text-green-700">
|
||||
{{ success }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading || alreadySubscribed || !form.groupId"
|
||||
class="w-full px-6 py-3 bg-primary-600 text-white font-semibold rounded-lg hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{{ loading ? 'Wird verarbeitet...' : alreadySubscribed ? 'Bereits abonniert' : 'Newsletter abonnieren' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
useHead({
|
||||
title: 'Newsletter abonnieren - Harheimer TC',
|
||||
})
|
||||
|
||||
const groups = ref([])
|
||||
const loadingGroups = ref(true)
|
||||
const form = ref({
|
||||
groupId: '',
|
||||
email: '',
|
||||
name: ''
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const checking = ref(false)
|
||||
const error = ref('')
|
||||
const success = ref('')
|
||||
const alreadySubscribed = ref(false)
|
||||
|
||||
const selectedGroup = computed(() => {
|
||||
return groups.value.find(g => g.id === form.value.groupId)
|
||||
})
|
||||
|
||||
async function loadGroups() {
|
||||
try {
|
||||
const response = await $fetch('/api/newsletter/groups/public-list')
|
||||
groups.value = response.groups || []
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der Newsletter-Gruppen:', err)
|
||||
error.value = 'Fehler beim Laden der verfügbaren Newsletter. Bitte versuchen Sie es später erneut.'
|
||||
} finally {
|
||||
loadingGroups.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function checkSubscription() {
|
||||
if (!form.value.groupId || !form.value.email || !form.value.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
|
||||
alreadySubscribed.value = false
|
||||
return
|
||||
}
|
||||
|
||||
checking.value = true
|
||||
try {
|
||||
const response = await $fetch('/api/newsletter/check-subscription', {
|
||||
query: {
|
||||
email: form.value.email,
|
||||
groupId: form.value.groupId
|
||||
}
|
||||
})
|
||||
alreadySubscribed.value = response.subscribed || false
|
||||
} catch (err) {
|
||||
// Fehler ignorieren - könnte bedeuten, dass nicht abonniert ist
|
||||
alreadySubscribed.value = false
|
||||
} finally {
|
||||
checking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function subscribe() {
|
||||
if (alreadySubscribed.value) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
success.value = ''
|
||||
|
||||
try {
|
||||
const response = await $fetch('/api/newsletter/subscribe', {
|
||||
method: 'POST',
|
||||
body: form.value
|
||||
})
|
||||
|
||||
success.value = response.message || 'Eine Bestätigungsmail wurde an Ihre E-Mail-Adresse gesendet.'
|
||||
form.value = { groupId: form.value.groupId, email: '', name: '' }
|
||||
alreadySubscribed.value = false
|
||||
} catch (err) {
|
||||
error.value = err.data?.statusMessage || err.message || 'Fehler bei der Anmeldung. Bitte versuchen Sie es später erneut.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadGroups()
|
||||
})
|
||||
</script>
|
||||
|
||||
128
pages/newsletter/unsubscribe.vue
Normal file
128
pages/newsletter/unsubscribe.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="min-h-full py-16 bg-gray-50">
|
||||
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-white rounded-xl shadow-lg p-8">
|
||||
<h1 class="text-3xl font-display font-bold text-gray-900 mb-6">
|
||||
Newsletter abmelden
|
||||
</h1>
|
||||
<div class="w-24 h-1 bg-primary-600 mb-8" />
|
||||
|
||||
<div v-if="loadingGroups" class="text-center py-8">
|
||||
<p class="text-gray-600">Lade verfügbare Newsletter...</p>
|
||||
</div>
|
||||
|
||||
<form v-else @submit.prevent="unsubscribe" class="space-y-6">
|
||||
<div>
|
||||
<label for="groupId" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Newsletter auswählen *
|
||||
</label>
|
||||
<select
|
||||
id="groupId"
|
||||
v-model="form.groupId"
|
||||
required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
>
|
||||
<option value="">Bitte wählen Sie einen Newsletter</option>
|
||||
<option v-for="group in groups" :key="group.id" :value="group.id">
|
||||
{{ group.name }}
|
||||
</option>
|
||||
</select>
|
||||
<p v-if="selectedGroup?.description" class="mt-2 text-sm text-gray-600">
|
||||
{{ selectedGroup.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
E-Mail-Adresse *
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="ihre.email@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-if="success" class="p-4 bg-green-50 border border-green-200 rounded-lg text-green-700">
|
||||
{{ success }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading || !form.groupId"
|
||||
class="w-full px-6 py-3 bg-primary-600 text-white font-semibold rounded-lg hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{{ loading ? 'Wird verarbeitet...' : 'Newsletter abmelden' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
useHead({
|
||||
title: 'Newsletter abmelden - Harheimer TC',
|
||||
})
|
||||
|
||||
const groups = ref([])
|
||||
const loadingGroups = ref(true)
|
||||
const form = ref({
|
||||
groupId: '',
|
||||
email: ''
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const success = ref('')
|
||||
|
||||
const selectedGroup = computed(() => {
|
||||
return groups.value.find(g => g.id === form.value.groupId)
|
||||
})
|
||||
|
||||
async function loadGroups() {
|
||||
try {
|
||||
const response = await $fetch('/api/newsletter/groups/public-list')
|
||||
groups.value = response.groups || []
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der Newsletter-Gruppen:', err)
|
||||
error.value = 'Fehler beim Laden der verfügbaren Newsletter. Bitte versuchen Sie es später erneut.'
|
||||
} finally {
|
||||
loadingGroups.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function unsubscribe() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
success.value = ''
|
||||
|
||||
try {
|
||||
const response = await $fetch('/api/newsletter/unsubscribe-by-email', {
|
||||
method: 'POST',
|
||||
body: form.value
|
||||
})
|
||||
|
||||
success.value = response.message || 'Sie wurden erfolgreich vom Newsletter abgemeldet.'
|
||||
form.value = { groupId: '', email: '' }
|
||||
} catch (err) {
|
||||
error.value = err.data?.statusMessage || err.message || 'Fehler bei der Abmeldung. Bitte versuchen Sie es später erneut.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadGroups()
|
||||
})
|
||||
</script>
|
||||
|
||||
40
pages/newsletter/unsubscribed.vue
Normal file
40
pages/newsletter/unsubscribed.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="min-h-full py-16 bg-gray-50">
|
||||
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-white rounded-xl shadow-lg p-8 text-center">
|
||||
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 class="text-3xl font-display font-bold text-gray-900 mb-4">
|
||||
{{ alreadyUnsubscribed ? 'Bereits abgemeldet' : 'Erfolgreich abgemeldet' }}
|
||||
</h1>
|
||||
|
||||
<p class="text-lg text-gray-600 mb-8">
|
||||
{{ alreadyUnsubscribed
|
||||
? 'Sie sind bereits vom Newsletter abgemeldet.'
|
||||
: 'Sie wurden erfolgreich vom Newsletter abgemeldet. Sie erhalten keine weiteren Newsletter mehr.' }}
|
||||
</p>
|
||||
|
||||
<NuxtLink
|
||||
to="/"
|
||||
class="inline-block px-6 py-3 bg-primary-600 text-white font-semibold rounded-lg hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
Zur Startseite
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const route = useRoute()
|
||||
const alreadyUnsubscribed = route.query.already === 'true'
|
||||
|
||||
useHead({
|
||||
title: 'Newsletter abgemeldet - Harheimer TC',
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -250,7 +250,10 @@ const uploadForm = ref({
|
||||
})
|
||||
|
||||
const isAdmin = computed(() => authStore.isAdmin)
|
||||
const isVorstand = computed(() => authStore.user?.role === 'vorstand')
|
||||
const isVorstand = computed(() => {
|
||||
const roles = authStore.user?.roles || (authStore.user?.role ? [authStore.user.role] : [])
|
||||
return roles.includes('vorstand')
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: 'Galerie - Harheimer TC',
|
||||
|
||||
Reference in New Issue
Block a user