Refactor homepage layout to dynamically render sections based on configuration
This commit updates the homepage component to utilize a dynamic rendering approach for sections, allowing for configuration-based display of components. It introduces computed properties to manage section visibility and mapping of section IDs to their respective components, enhancing flexibility and maintainability of the homepage structure.
This commit is contained in:
@@ -183,6 +183,27 @@
|
||||
</p>
|
||||
</NuxtLink>
|
||||
|
||||
<!-- Startseite -->
|
||||
<NuxtLink
|
||||
to="/cms/startseite"
|
||||
class="bg-white p-6 rounded-xl shadow-lg border border-gray-100 hover:shadow-xl transition-all group"
|
||||
>
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-12 h-12 bg-cyan-100 rounded-lg flex items-center justify-center group-hover:bg-cyan-600 transition-colors">
|
||||
<Layout
|
||||
:size="24"
|
||||
class="text-cyan-600 group-hover:text-white"
|
||||
/>
|
||||
</div>
|
||||
<h2 class="ml-4 text-xl font-semibold text-gray-900">
|
||||
Startseite
|
||||
</h2>
|
||||
</div>
|
||||
<p class="text-gray-600">
|
||||
Reihenfolge der Startseiten-Elemente konfigurieren
|
||||
</p>
|
||||
</NuxtLink>
|
||||
|
||||
<!-- Einstellungen -->
|
||||
<NuxtLink
|
||||
to="/cms/einstellungen"
|
||||
@@ -231,7 +252,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Newspaper, Calendar, Users, UserCog, Settings } from 'lucide-vue-next'
|
||||
import { Newspaper, Calendar, Users, UserCog, Settings, Layout } from 'lucide-vue-next'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
|
||||
276
pages/cms/startseite.vue
Normal file
276
pages/cms/startseite.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div class="min-h-full py-16 bg-gray-50">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-2">
|
||||
Startseite konfigurieren
|
||||
</h1>
|
||||
<div class="w-24 h-1 bg-primary-600 mb-4" />
|
||||
<p class="text-gray-600">
|
||||
Legen Sie die Reihenfolge der Elemente auf der Startseite fest.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
|
||||
:disabled="isSaving"
|
||||
@click="saveConfig"
|
||||
>
|
||||
<Loader2
|
||||
v-if="isSaving"
|
||||
:size="20"
|
||||
class="animate-spin mr-2"
|
||||
/>
|
||||
<span>{{ isSaving ? 'Speichert...' : 'Speichern' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="flex items-center justify-center py-12"
|
||||
>
|
||||
<Loader2
|
||||
:size="40"
|
||||
class="animate-spin text-primary-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Config Form -->
|
||||
<div
|
||||
v-else
|
||||
class="bg-white rounded-xl shadow-lg p-6"
|
||||
>
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-2">
|
||||
Verfügbare Elemente
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
Ziehen Sie die Elemente per Drag & Drop oder verwenden Sie die Pfeil-Buttons, um die Reihenfolge zu ändern.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="(section, index) in sections"
|
||||
:key="section.id"
|
||||
class="flex items-center gap-3 p-4 border border-gray-200 rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<!-- Drag Handle -->
|
||||
<div class="flex flex-col gap-1 cursor-move">
|
||||
<GripVertical :size="16" class="text-gray-400" />
|
||||
</div>
|
||||
|
||||
<!-- Section Info -->
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-gray-900">
|
||||
{{ getSectionLabel(section.id) }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ getSectionDescription(section.id) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Position Controls -->
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Nach oben"
|
||||
:disabled="isSaving || index === 0"
|
||||
@click="moveUp(index)"
|
||||
>
|
||||
<ChevronUp :size="18" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Nach unten"
|
||||
:disabled="isSaving || index === sections.length - 1"
|
||||
@click="moveDown(index)"
|
||||
>
|
||||
<ChevronDown :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Visibility Toggle -->
|
||||
<div class="flex items-center">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
v-model="section.enabled"
|
||||
type="checkbox"
|
||||
class="w-5 h-5 text-primary-600 border-gray-300 rounded focus:ring-primary-500"
|
||||
:disabled="isSaving"
|
||||
>
|
||||
<span class="ml-2 text-sm text-gray-700">
|
||||
Anzeigen
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p class="text-sm text-blue-800">
|
||||
<strong>Hinweis:</strong> Deaktivierte Elemente werden auf der Startseite nicht angezeigt, bleiben aber in der Konfiguration erhalten.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
class="mt-6 flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm"
|
||||
>
|
||||
<AlertCircle
|
||||
:size="20"
|
||||
class="mr-2"
|
||||
/>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Loader2, AlertCircle, ChevronUp, ChevronDown, GripVertical } from 'lucide-vue-next'
|
||||
|
||||
const isLoading = ref(true)
|
||||
const isSaving = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const sections = ref([])
|
||||
|
||||
// Verfügbare Elemente mit Labels und Beschreibungen
|
||||
const availableSections = {
|
||||
banner: {
|
||||
label: 'Banner (Willkommen)',
|
||||
description: 'Hero-Bereich mit Willkommensnachricht'
|
||||
},
|
||||
termine: {
|
||||
label: 'Kommende Termine',
|
||||
description: 'Vorschau der nächsten Vereinstermine'
|
||||
},
|
||||
spiele: {
|
||||
label: 'Nächste Spiele',
|
||||
description: 'Vorschau der kommenden Punktspiele'
|
||||
},
|
||||
aktuelles: {
|
||||
label: 'Aktuelles',
|
||||
description: 'Öffentliche News und Ankündigungen'
|
||||
},
|
||||
kontakt: {
|
||||
label: 'Kontakt-Boxen',
|
||||
description: 'Mitglied werden & Kontakt aufnehmen'
|
||||
}
|
||||
}
|
||||
|
||||
function getSectionLabel(id) {
|
||||
return availableSections[id]?.label || id
|
||||
}
|
||||
|
||||
function getSectionDescription(id) {
|
||||
return availableSections[id]?.description || ''
|
||||
}
|
||||
|
||||
const loadConfig = async () => {
|
||||
isLoading.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const config = await $fetch('/api/config')
|
||||
|
||||
// Standard-Reihenfolge, falls nicht vorhanden
|
||||
const defaultSections = [
|
||||
{ id: 'banner', enabled: true },
|
||||
{ id: 'termine', enabled: true },
|
||||
{ id: 'spiele', enabled: true },
|
||||
{ id: 'aktuelles', enabled: true },
|
||||
{ id: 'kontakt', enabled: true }
|
||||
]
|
||||
|
||||
if (config.homepage && config.homepage.sections && Array.isArray(config.homepage.sections)) {
|
||||
// Validiere und merge: Nur bekannte IDs verwenden, fehlende hinzufügen
|
||||
const knownIds = new Set(config.homepage.sections.map(s => s.id))
|
||||
const merged = [...config.homepage.sections]
|
||||
|
||||
// Füge fehlende Standard-Elemente hinzu
|
||||
for (const defaultSection of defaultSections) {
|
||||
if (!knownIds.has(defaultSection.id)) {
|
||||
merged.push({ ...defaultSection })
|
||||
}
|
||||
}
|
||||
|
||||
sections.value = merged
|
||||
} else {
|
||||
sections.value = [...defaultSections]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Konfiguration:', error)
|
||||
errorMessage.value = 'Fehler beim Laden der Konfiguration'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const moveUp = (index) => {
|
||||
if (index <= 0) return
|
||||
const item = sections.value[index]
|
||||
sections.value.splice(index, 1)
|
||||
sections.value.splice(index - 1, 0, item)
|
||||
}
|
||||
|
||||
const moveDown = (index) => {
|
||||
if (index >= sections.value.length - 1) return
|
||||
const item = sections.value[index]
|
||||
sections.value.splice(index, 1)
|
||||
sections.value.splice(index + 1, 0, item)
|
||||
}
|
||||
|
||||
const saveConfig = async () => {
|
||||
isSaving.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
// Lade aktuelle Config
|
||||
const config = await $fetch('/api/config')
|
||||
|
||||
// Aktualisiere homepage.sections
|
||||
if (!config.homepage) {
|
||||
config.homepage = {}
|
||||
}
|
||||
config.homepage.sections = sections.value
|
||||
|
||||
// Speichere Config
|
||||
await $fetch('/api/config', {
|
||||
method: 'PUT',
|
||||
body: config
|
||||
})
|
||||
|
||||
if (window.showSuccessModal) {
|
||||
window.showSuccessModal('Erfolg', 'Startseiten-Konfiguration wurde erfolgreich gespeichert')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern:', error)
|
||||
errorMessage.value = error?.data?.message || 'Fehler beim Speichern der Konfiguration'
|
||||
if (window.showErrorModal) {
|
||||
window.showErrorModal('Fehler', errorMessage.value)
|
||||
}
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
layout: 'default'
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: 'Startseite konfigurieren - Harheimer TC',
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user