- Introduced new Vue components for homepage teasers: HomeLinksTeaser, HomeSpielplanTeamWidget, HomeTrainingTeaser, and HomeVereinsmeisterschaftenTeaser. - Created XML layout for tablet app window dump. - Implemented API endpoints for fetching and updating homepage settings. - Added API for retrieving spielplan options, including team extraction logic.
187 lines
5.1 KiB
Vue
187 lines
5.1 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="bg-gray-50 rounded-xl border border-gray-200 p-6 md:p-8">
|
|
<div class="flex flex-wrap items-center justify-between gap-3 mb-6">
|
|
<div>
|
|
<h2 class="text-2xl font-bold text-gray-900">
|
|
Spielplan: {{ widgetTitle }}
|
|
</h2>
|
|
<p class="text-sm text-gray-600 mt-1">
|
|
Saison {{ seasonLabel }}
|
|
</p>
|
|
</div>
|
|
<NuxtLink
|
|
to="/spielplan"
|
|
class="inline-flex items-center px-4 py-2 rounded-lg bg-primary-600 hover:bg-primary-700 text-white text-sm font-semibold transition-colors"
|
|
>
|
|
Voller Spielplan
|
|
</NuxtLink>
|
|
</div>
|
|
|
|
<div
|
|
v-if="isLoading"
|
|
class="text-sm text-gray-600"
|
|
>
|
|
Spiele werden geladen...
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="error"
|
|
class="text-sm text-red-700"
|
|
>
|
|
{{ error }}
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="upcomingGames.length === 0"
|
|
class="text-sm text-gray-600"
|
|
>
|
|
Keine kommenden Spiele für diese Mannschaft gefunden.
|
|
</div>
|
|
|
|
<div
|
|
v-else
|
|
class="space-y-3"
|
|
>
|
|
<div
|
|
v-for="game in upcomingGames"
|
|
:key="`${game.Termin}-${game.HeimMannschaft}-${game.GastMannschaft}`"
|
|
class="bg-white rounded-lg border border-gray-200 p-4"
|
|
>
|
|
<div class="flex flex-wrap items-center justify-between gap-2 mb-2">
|
|
<p class="text-sm font-medium text-gray-900">
|
|
{{ formatDate(game.Termin) }} {{ formatTime(game.Termin) }}
|
|
</p>
|
|
<p class="text-xs text-gray-500">
|
|
{{ game.Runde || '-' }}
|
|
</p>
|
|
</div>
|
|
<p class="text-sm text-gray-800">
|
|
{{ game.HeimMannschaft }} vs {{ game.GastMannschaft }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref, watch } from 'vue'
|
|
|
|
const props = defineProps({
|
|
season: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
teamName: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
teamAgeGroup: {
|
|
type: String,
|
|
default: ''
|
|
}
|
|
})
|
|
|
|
const isLoading = ref(false)
|
|
const error = ref('')
|
|
const games = ref([])
|
|
|
|
const widgetTitle = computed(() => {
|
|
if (!props.teamName) return 'Mannschaft'
|
|
const youth = String(props.teamAgeGroup || '').toLowerCase().includes('jugend')
|
|
return youth ? `(J) ${props.teamName}` : props.teamName
|
|
})
|
|
|
|
const seasonLabel = computed(() => {
|
|
const match = String(props.season || '').match(/^(\d{2})--(\d{2})$/)
|
|
if (!match) return props.season || '-'
|
|
return `20${match[1]}/${match[2]}`
|
|
})
|
|
|
|
const upcomingGames = computed(() => {
|
|
const now = new Date()
|
|
now.setHours(0, 0, 0, 0)
|
|
return games.value
|
|
.filter(game => {
|
|
const gameDate = parseDate(game.Termin)
|
|
return gameDate && gameDate >= now
|
|
})
|
|
.sort((a, b) => parseDate(a.Termin) - parseDate(b.Termin))
|
|
.slice(0, 5)
|
|
})
|
|
|
|
function parseDate(termin) {
|
|
const raw = String(termin || '').trim()
|
|
const datePart = raw.split(' ')[0]
|
|
const [day, month, year] = datePart.split('.')
|
|
if (!day || !month || !year) return null
|
|
const parsed = new Date(Number(year), Number(month) - 1, Number(day))
|
|
if (Number.isNaN(parsed.getTime())) return null
|
|
parsed.setHours(0, 0, 0, 0)
|
|
return parsed
|
|
}
|
|
|
|
function formatDate(termin) {
|
|
const parsed = parseDate(termin)
|
|
if (!parsed) return termin || '-'
|
|
return parsed.toLocaleDateString('de-DE', {
|
|
weekday: 'short',
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
})
|
|
}
|
|
|
|
function formatTime(termin) {
|
|
const raw = String(termin || '')
|
|
const timePart = raw.split(' ')[1]
|
|
return timePart || ''
|
|
}
|
|
|
|
function isConfiguredTeamMatch(game) {
|
|
const teamName = String(props.teamName || '').trim()
|
|
const teamAgeGroup = String(props.teamAgeGroup || '').trim()
|
|
if (!teamName) return false
|
|
|
|
const homeMatch = String(game.HeimMannschaft || '').trim() === teamName &&
|
|
(!teamAgeGroup || String(game.HeimMannschaftAltersklasse || '').trim() === teamAgeGroup)
|
|
const awayMatch = String(game.GastMannschaft || '').trim() === teamName &&
|
|
(!teamAgeGroup || String(game.GastMannschaftAltersklasse || '').trim() === teamAgeGroup)
|
|
return homeMatch || awayMatch
|
|
}
|
|
|
|
async function loadData() {
|
|
if (!props.teamName || !props.season) {
|
|
games.value = []
|
|
return
|
|
}
|
|
|
|
isLoading.value = true
|
|
error.value = ''
|
|
try {
|
|
const result = await $fetch('/api/spielplan', {
|
|
query: { season: props.season }
|
|
})
|
|
if (!result?.success) {
|
|
throw new Error(result?.message || 'Spielplan konnte nicht geladen werden.')
|
|
}
|
|
games.value = (result.data || []).filter(isConfiguredTeamMatch)
|
|
} catch (err) {
|
|
games.value = []
|
|
error.value = err?.data?.message || err?.message || 'Spielplan konnte nicht geladen werden.'
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => [props.season, props.teamName, props.teamAgeGroup],
|
|
() => {
|
|
loadData()
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
</script> |