391 lines
11 KiB
Vue
391 lines
11 KiB
Vue
<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>
|
|
</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="/mannschaften/spielplaene"
|
|
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="/mannschaften/spielplaene"
|
|
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.Termin}-${game.HeimMannschaft}`"
|
|
class="bg-white rounded-xl shadow-lg border border-gray-200 hover:shadow-xl transition-shadow"
|
|
>
|
|
<div class="p-6">
|
|
<!-- Date and Time -->
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center">
|
|
<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.Termin) }}</span>
|
|
</div>
|
|
<div class="flex items-center text-sm text-gray-600">
|
|
<svg
|
|
class="w-4 h-4 text-gray-400 mr-1"
|
|
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.Termin) }}
|
|
</div>
|
|
</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">
|
|
Heim
|
|
</p>
|
|
<p class="font-semibold text-gray-900">
|
|
{{ formatTeamName(game.HeimMannschaft, game.HeimMannschaftAltersklasse) }}
|
|
</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">
|
|
Gast
|
|
</p>
|
|
<p class="font-semibold text-gray-900">
|
|
{{ formatTeamName(game.GastMannschaft, game.GastMannschaftAltersklasse) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Competition Info -->
|
|
<div
|
|
v-if="game.Runde"
|
|
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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
{{ formatRunde(game.Runde) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- View All Button -->
|
|
<div
|
|
v-if="upcomingGames && upcomingGames.length > 0"
|
|
class="text-center mt-8"
|
|
>
|
|
<NuxtLink
|
|
to="/mannschaften/spielplaene"
|
|
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)
|
|
|
|
const in7Days = new Date(today)
|
|
in7Days.setDate(in7Days.getDate() + 7)
|
|
|
|
return spielplanData.value
|
|
.filter(game => {
|
|
if (!game.Termin) return false
|
|
|
|
// Parse deutsches Datumsformat: "27.10.2025 20:00"
|
|
let gameDate
|
|
try {
|
|
if (game.Termin.includes(' ')) {
|
|
// Uhrzeit entfernen: "27.10.2025 20:00" -> "27.10.2025"
|
|
const datumTeil = game.Termin.split(' ')[0]
|
|
|
|
// Deutsches Format parsen: "27.10.2025" -> Date
|
|
const [tag, monat, jahr] = datumTeil.split('.')
|
|
gameDate = new Date(jahr, monat - 1, tag) // Monat ist 0-basiert
|
|
} else {
|
|
// Fallback für andere Formate
|
|
gameDate = new Date(game.Termin)
|
|
}
|
|
|
|
if (isNaN(gameDate.getTime())) return false
|
|
|
|
gameDate.setHours(0, 0, 0, 0)
|
|
|
|
// Nur Spiele der nächsten 7 Tage
|
|
return gameDate >= today && gameDate <= in7Days
|
|
} catch (error) {
|
|
return false
|
|
}
|
|
})
|
|
.sort((a, b) => {
|
|
// Sortierung nach Datum
|
|
const dateA = parseGameDate(a.Termin)
|
|
const dateB = parseGameDate(b.Termin)
|
|
return dateA - dateB
|
|
})
|
|
.slice(0, 6) // Zeige maximal 6 Spiele
|
|
})
|
|
|
|
// Hilfsfunktion zum Parsen von Spielterminen
|
|
const parseGameDate = (terminString) => {
|
|
if (!terminString) return new Date()
|
|
|
|
try {
|
|
if (terminString.includes(' ')) {
|
|
// Uhrzeit entfernen: "27.10.2025 20:00" -> "27.10.2025"
|
|
const datumTeil = terminString.split(' ')[0]
|
|
|
|
// Deutsches Format parsen: "27.10.2025" -> Date
|
|
const [tag, monat, jahr] = datumTeil.split('.')
|
|
return new Date(jahr, monat - 1, tag) // Monat ist 0-basiert
|
|
} else {
|
|
return new Date(terminString)
|
|
}
|
|
} catch {
|
|
return new Date()
|
|
}
|
|
}
|
|
|
|
const formatDate = (terminString) => {
|
|
if (!terminString) return '-'
|
|
|
|
try {
|
|
const date = parseGameDate(terminString)
|
|
if (isNaN(date.getTime())) {
|
|
return terminString
|
|
}
|
|
|
|
// Wochentag-Kürzel
|
|
const wochentagKuerzel = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']
|
|
const wochentag = wochentagKuerzel[date.getDay()]
|
|
|
|
// Deutsches Datum
|
|
const datum = date.toLocaleDateString('de-DE', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
})
|
|
|
|
return `${wochentag} ${datum}`
|
|
} catch {
|
|
return terminString
|
|
}
|
|
}
|
|
|
|
const formatTime = (terminString) => {
|
|
if (!terminString) return '-'
|
|
|
|
try {
|
|
if (terminString.includes(' ')) {
|
|
// Uhrzeit extrahieren: "27.10.2025 20:00" -> "20:00"
|
|
const zeitTeil = terminString.split(' ')[1]
|
|
return zeitTeil || '-'
|
|
}
|
|
|
|
return terminString
|
|
} catch {
|
|
return terminString
|
|
}
|
|
}
|
|
|
|
const formatTeamName = (teamName, ageGroup) => {
|
|
if (!teamName) return 'Nicht angegeben'
|
|
|
|
// Prüfe ob es Nachwuchs ist
|
|
const isNachwuchs = ageGroup && (
|
|
ageGroup.toLowerCase().includes('jugend') ||
|
|
teamName.toLowerCase().includes('jugend')
|
|
)
|
|
|
|
// Füge (J) für Nachwuchs hinzu
|
|
return isNachwuchs ? `(J) ${teamName}` : teamName
|
|
}
|
|
|
|
const formatRunde = (runde) => {
|
|
if (!runde) return ''
|
|
|
|
const rundeLower = runde.toLowerCase()
|
|
|
|
if (rundeLower === 'vr' || rundeLower.includes('vorrunde')) {
|
|
return 'Vorrunde'
|
|
} else if (rundeLower === 'rr' || rundeLower.includes('rückrunde')) {
|
|
return 'Rückrunde'
|
|
} else if (rundeLower.includes('pokal')) {
|
|
return 'Pokal'
|
|
}
|
|
|
|
return runde
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadData()
|
|
})
|
|
</script>
|