Add 'Spielplan' links to Navigation component; update index page to include 'Spielplan' section; enhance 'spielplaene' page with filtering, loading states, and error handling for improved user experience.

This commit is contained in:
Torsten Schulz (local)
2025-10-24 00:55:04 +02:00
parent c6ce26773a
commit 7660f7cf7b
13 changed files with 2457 additions and 228 deletions

View File

@@ -57,6 +57,12 @@
Termine
</NuxtLink>
<NuxtLink to="/spielplan" @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">
Spielplan
</NuxtLink>
<NuxtLink
v-if="hasGalleryImages"
to="/galerie"
@@ -246,6 +252,11 @@
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Termine
</NuxtLink>
<NuxtLink to="/cms/spielplaene"
@click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
Spielpläne
</NuxtLink>
<NuxtLink to="/mitgliederbereich/mitglieder"
@click="showCmsDropdown = false"
class="block px-4 py-2 text-sm text-gray-300 hover:bg-primary-600 hover:text-white transition-colors">
@@ -403,6 +414,11 @@
Termine
</NuxtLink>
<NuxtLink to="/spielplan" @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">
Spielplan
</NuxtLink>
<NuxtLink
v-if="hasGalleryImages"
to="/galerie"
@@ -450,6 +466,10 @@
class="block px-4 py-2 text-sm text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
Termine
</NuxtLink>
<NuxtLink to="/cms/spielplaene" @click="isMobileMenuOpen = false"
class="block px-4 py-2 text-sm text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
Spielpläne
</NuxtLink>
<NuxtLink to="/mitgliederbereich/mitglieder" @click="isMobileMenuOpen = false"
class="block px-4 py-2 text-sm text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
Mitglieder

218
components/Spielplan.vue Normal file
View File

@@ -0,0 +1,218 @@
<template>
<section class="py-16 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-gray-900 mb-4">Nächste Spiele</h2>
<p class="text-lg text-gray-600">Aktuelle Termine und Spiele des Vereins</p>
</div>
<!-- Loading State -->
<div v-if="isLoading" class="text-center py-8">
<svg class="w-8 h-8 text-gray-400 mx-auto mb-4 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>
<p class="text-gray-600">Spielplan wird geladen...</p>
</div>
<!-- Error State -->
<div v-else-if="error" class="text-center py-8">
<svg class="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<p class="text-gray-600 mb-4">{{ error }}</p>
<NuxtLink to="/spielplan" class="inline-flex items-center px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors">
Zum Spielplan
</NuxtLink>
</div>
<!-- Empty State -->
<div v-else-if="!upcomingGames || upcomingGames.length === 0" class="text-center py-8">
<svg class="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<h3 class="text-lg font-medium text-gray-900 mb-2">Keine kommenden Spiele</h3>
<p class="text-gray-600 mb-4">Derzeit sind keine Spiele geplant.</p>
<NuxtLink to="/spielplan" class="inline-flex items-center px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors">
Zum Spielplan
</NuxtLink>
</div>
<!-- Games Grid -->
<div v-else class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<div v-for="game in upcomingGames" :key="`${game.Datum}-${game.Mannschaft}`"
class="bg-white rounded-xl shadow-lg border border-gray-200 hover:shadow-xl transition-shadow">
<div class="p-6">
<!-- Date -->
<div class="flex items-center mb-4">
<svg class="w-5 h-5 text-primary-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span class="text-sm font-medium text-gray-900">{{ formatDate(game.Datum) }}</span>
</div>
<!-- Teams -->
<div class="mb-4">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-sm text-gray-600 mb-1">Mannschaft</p>
<p class="font-semibold text-gray-900">{{ game.Mannschaft || 'Nicht angegeben' }}</p>
</div>
<div class="text-center mx-4">
<div class="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
<span class="text-primary-600 font-bold text-sm">vs</span>
</div>
</div>
<div class="flex-1 text-right">
<p class="text-sm text-gray-600 mb-1">Gegner</p>
<p class="font-semibold text-gray-900">{{ game.Gegner || 'Nicht angegeben' }}</p>
</div>
</div>
</div>
<!-- Details -->
<div class="space-y-2">
<div v-if="game.Ort" class="flex items-center text-sm text-gray-600">
<svg class="w-4 h-4 text-gray-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
{{ game.Ort }}
</div>
<div v-if="game.Uhrzeit" class="flex items-center text-sm text-gray-600">
<svg class="w-4 h-4 text-gray-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{{ formatTime(game.Uhrzeit) }}
</div>
<div v-if="game.Status" class="flex items-center text-sm">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="getStatusClass(game.Status)">
{{ game.Status }}
</span>
</div>
</div>
<!-- Result (if available) -->
<div v-if="game.Ergebnis" class="mt-4 pt-4 border-t border-gray-200">
<div class="text-center">
<p class="text-sm text-gray-600 mb-1">Ergebnis</p>
<p class="text-lg font-bold text-primary-600">{{ game.Ergebnis }}</p>
</div>
</div>
</div>
</div>
</div>
<!-- View All Button -->
<div v-if="upcomingGames && upcomingGames.length > 0" class="text-center mt-8">
<NuxtLink to="/spielplan"
class="inline-flex items-center px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors">
Alle Spiele anzeigen
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</NuxtLink>
</div>
</div>
</section>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
const spielplanData = ref([])
const isLoading = ref(false)
const error = ref(null)
const loadData = async () => {
isLoading.value = true
error.value = null
try {
const response = await fetch('/api/spielplan')
const result = await response.json()
if (result.success) {
spielplanData.value = result.data
} else {
error.value = result.message
}
} catch (err) {
console.error('Fehler beim Laden des Spielplans:', err)
error.value = 'Fehler beim Laden der Daten'
} finally {
isLoading.value = false
}
}
const upcomingGames = computed(() => {
if (!spielplanData.value || spielplanData.value.length === 0) return []
const today = new Date()
today.setHours(0, 0, 0, 0)
return spielplanData.value
.filter(game => {
if (!game.Datum) return false
const gameDate = new Date(game.Datum)
if (isNaN(gameDate.getTime())) return false
return gameDate >= today
})
.sort((a, b) => {
const dateA = new Date(a.Datum)
const dateB = new Date(b.Datum)
return dateA - dateB
})
.slice(0, 6) // Zeige nur die nächsten 6 Spiele
})
const formatDate = (dateString) => {
if (!dateString) return '-'
try {
const date = new Date(dateString)
if (isNaN(date.getTime())) {
return dateString
}
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
} catch {
return dateString
}
}
const formatTime = (timeString) => {
if (!timeString) return '-'
// Einfache Zeitformatierung (HH:MM)
if (timeString.match(/^\d{1,2}:\d{2}$/)) {
return timeString
}
return timeString
}
const getStatusClass = (status) => {
const statusMap = {
'Geplant': 'bg-blue-100 text-blue-800',
'Abgesagt': 'bg-red-100 text-red-800',
'Verschoben': 'bg-yellow-100 text-yellow-800',
'Beendet': 'bg-green-100 text-green-800',
'Läuft': 'bg-purple-100 text-purple-800'
}
return statusMap[status] || 'bg-gray-100 text-gray-800'
}
onMounted(() => {
loadData()
})
</script>