feat: add homepage components and API for settings and spielplan options
- 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.
This commit is contained in:
20
components/HomeLinksTeaser.vue
Normal file
20
components/HomeLinksTeaser.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<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 shadow-sm p-8 md:p-10 border border-gray-200">
|
||||
<h2 class="text-2xl md:text-3xl font-display font-bold text-gray-900 mb-3">
|
||||
Nützliche Links
|
||||
</h2>
|
||||
<p class="text-gray-600 mb-6 max-w-3xl">
|
||||
Direkter Zugang zu Verbänden, Ergebnisdiensten und weiteren hilfreichen Portalen.
|
||||
</p>
|
||||
<NuxtLink
|
||||
to="/links"
|
||||
class="inline-flex items-center px-6 py-3 rounded-lg border border-primary-600 text-primary-700 hover:bg-primary-50 font-semibold transition-colors"
|
||||
>
|
||||
Links öffnen
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
187
components/HomeSpielplanTeamWidget.vue
Normal file
187
components/HomeSpielplanTeamWidget.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<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>
|
||||
20
components/HomeTrainingTeaser.vue
Normal file
20
components/HomeTrainingTeaser.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<section class="py-16 bg-gray-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-white rounded-xl shadow-lg p-8 md:p-10 border border-gray-100">
|
||||
<h2 class="text-2xl md:text-3xl font-display font-bold text-gray-900 mb-3">
|
||||
Training & Einstieg
|
||||
</h2>
|
||||
<p class="text-gray-600 mb-6 max-w-3xl">
|
||||
Alle Infos zu Trainingszeiten, Trainern und unserem Anfängerangebot auf einen Blick.
|
||||
</p>
|
||||
<NuxtLink
|
||||
to="/training"
|
||||
class="inline-flex items-center px-6 py-3 rounded-lg bg-primary-600 hover:bg-primary-700 text-white font-semibold transition-colors"
|
||||
>
|
||||
Zum Training
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
20
components/HomeVereinsmeisterschaftenTeaser.vue
Normal file
20
components/HomeVereinsmeisterschaftenTeaser.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<section class="py-16 bg-gray-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-white rounded-xl shadow-lg p-8 md:p-10 border border-gray-100">
|
||||
<h2 class="text-2xl md:text-3xl font-display font-bold text-gray-900 mb-3">
|
||||
Vereinsmeisterschaften
|
||||
</h2>
|
||||
<p class="text-gray-600 mb-6 max-w-3xl">
|
||||
Ergebnisse, Historie und Einblicke in die Vereinsmeisterschaften.
|
||||
</p>
|
||||
<NuxtLink
|
||||
to="/vereinsmeisterschaften"
|
||||
class="inline-flex items-center px-6 py-3 rounded-lg bg-primary-600 hover:bg-primary-700 text-white font-semibold transition-colors"
|
||||
>
|
||||
Ergebnisse ansehen
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -7,8 +7,12 @@
|
||||
>
|
||||
<div class="pointer-events-auto mx-4 shadow-lg rounded-md overflow-hidden">
|
||||
<div class="px-4 py-3 bg-green-50 text-green-800 text-sm">
|
||||
<div class="font-medium">{{ toastTitle }}</div>
|
||||
<div class="mt-1">{{ toastMessage }}</div>
|
||||
<div class="font-medium">
|
||||
{{ toastTitle }}
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
{{ toastMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user