Refactor homepage layout to dynamically render sections based on configuration
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 56s

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:
Torsten Schulz (local)
2026-02-05 17:40:02 +01:00
parent e52fc7dffc
commit 55c4e77848
3 changed files with 341 additions and 15 deletions

View File

@@ -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
View 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>