Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 51s
This commit introduces a new function, getSortedKategorien, to sort competition categories based on a predefined order. The rendering of categories in the Vereinsmeisterschaften component is updated to utilize this new sorting function, improving the organization and display of results. This change enhances the user experience by ensuring categories are presented in a consistent and logical order.
451 lines
14 KiB
Vue
451 lines
14 KiB
Vue
<template>
|
|
<div class="min-h-full py-16 bg-gray-50">
|
|
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<h1 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-6">
|
|
Vereinsmeisterschaften
|
|
</h1>
|
|
<div class="w-24 h-1 bg-primary-600 mb-8" />
|
|
|
|
<p class="text-xl text-gray-600 mb-12">
|
|
Die Ergebnisse unserer Vereinsmeisterschaften der letzten Jahre
|
|
</p>
|
|
|
|
<!-- Filter -->
|
|
<div class="mb-8 flex flex-wrap gap-4">
|
|
<button
|
|
v-for="jahr in verfuegbareJahre"
|
|
:key="jahr"
|
|
:class="[
|
|
'px-4 py-2 rounded-lg font-medium transition-colors',
|
|
selectedYear === jahr
|
|
? 'bg-primary-600 text-white'
|
|
: 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300'
|
|
]"
|
|
@click="selectedYear = jahr"
|
|
>
|
|
{{ jahr }}
|
|
</button>
|
|
<button
|
|
:class="[
|
|
'px-4 py-2 rounded-lg font-medium transition-colors',
|
|
selectedYear === 'alle'
|
|
? 'bg-primary-600 text-white'
|
|
: 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300'
|
|
]"
|
|
@click="selectedYear = 'alle'"
|
|
>
|
|
Alle Jahre
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Ergebnisse -->
|
|
<div
|
|
v-if="filteredResults.length > 0"
|
|
class="space-y-8"
|
|
>
|
|
<div
|
|
v-for="jahr in sortedJahre"
|
|
:key="jahr"
|
|
class="bg-white rounded-xl shadow-lg p-6"
|
|
>
|
|
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6 flex items-center">
|
|
<Trophy
|
|
:size="28"
|
|
class="text-primary-600 mr-3"
|
|
/>
|
|
{{ jahr }}
|
|
</h2>
|
|
|
|
<!-- Besondere Bemerkungen -->
|
|
<div
|
|
v-if="sortedGroupedResults[jahr]?.bemerkungen"
|
|
class="mb-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg"
|
|
>
|
|
<p class="text-yellow-800 font-medium">
|
|
{{ sortedGroupedResults[jahr].bemerkungen }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Kategorien -->
|
|
<div
|
|
v-if="sortedGroupedResults[jahr]?.kategorien"
|
|
class="space-y-6"
|
|
>
|
|
<div
|
|
v-for="kategorie in getSortedKategorien(sortedGroupedResults[jahr].kategorien)"
|
|
:key="kategorie"
|
|
class="border-l-4 border-primary-600 pl-4"
|
|
>
|
|
<h3 class="text-xl font-semibold text-gray-900 mb-4">
|
|
{{ kategorie }}
|
|
</h3>
|
|
|
|
<div class="grid gap-3">
|
|
<div
|
|
v-for="(ergebnis, index) in sortedGroupedResults[jahr].kategorien[kategorie]"
|
|
:key="index"
|
|
:class="[
|
|
'flex items-center justify-between p-3 rounded-lg',
|
|
ergebnis.platz === '1' ? 'bg-yellow-50 border border-yellow-200' :
|
|
ergebnis.platz === '2' ? 'bg-gray-50 border border-gray-200' :
|
|
ergebnis.platz === '3' ? 'bg-orange-50 border border-orange-200' :
|
|
'bg-gray-100'
|
|
]"
|
|
>
|
|
<div class="flex items-center">
|
|
<div
|
|
:class="[
|
|
'w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold mr-3',
|
|
ergebnis.platz === '1' ? 'bg-yellow-500 text-white' :
|
|
ergebnis.platz === '2' ? 'bg-gray-400 text-white' :
|
|
ergebnis.platz === '3' ? 'bg-orange-500 text-white' :
|
|
'bg-gray-300 text-gray-700'
|
|
]"
|
|
>
|
|
{{ ergebnis.platz }}
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<div
|
|
v-if="ergebnis.imageFilename1"
|
|
class="flex-shrink-0"
|
|
>
|
|
<img
|
|
:src="`/api/personen/${ergebnis.imageFilename1}?width=40&height=40`"
|
|
:alt="ergebnis.spieler1"
|
|
class="w-10 h-10 rounded-full object-cover border-2 border-gray-300 cursor-pointer hover:border-primary-500 transition-colors"
|
|
loading="lazy"
|
|
@click="openLightbox(ergebnis.imageFilename1, ergebnis.spieler1)"
|
|
>
|
|
</div>
|
|
<div>
|
|
<span class="font-semibold text-gray-900">
|
|
{{ ergebnis.spieler1 }}
|
|
</span>
|
|
<span
|
|
v-if="ergebnis.spieler2"
|
|
class="text-gray-600"
|
|
>
|
|
<span
|
|
v-if="ergebnis.imageFilename2"
|
|
class="ml-2 inline-flex items-center gap-2"
|
|
>
|
|
/
|
|
<img
|
|
:src="`/api/personen/${ergebnis.imageFilename2}?width=40&height=40`"
|
|
:alt="ergebnis.spieler2"
|
|
class="w-10 h-10 rounded-full object-cover border-2 border-gray-300 cursor-pointer hover:border-primary-500 transition-colors"
|
|
loading="lazy"
|
|
@click="openLightbox(ergebnis.imageFilename2, ergebnis.spieler2)"
|
|
>
|
|
{{ ergebnis.spieler2 }}
|
|
</span>
|
|
<span
|
|
v-else
|
|
class="text-gray-600"
|
|
>
|
|
/ {{ ergebnis.spieler2 }}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="text-sm text-gray-500">
|
|
{{ ergebnis.platz === '1' ? 'Vereinsmeister' : ergebnis.platz + '. Platz' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-else
|
|
class="text-center py-12 bg-white rounded-xl shadow-lg"
|
|
>
|
|
<Trophy
|
|
:size="48"
|
|
class="text-gray-400 mx-auto mb-4"
|
|
/>
|
|
<p class="text-gray-600">
|
|
Keine Ergebnisse für das ausgewählte Jahr gefunden.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Statistik -->
|
|
<div class="mt-12 bg-gradient-to-r from-primary-600 to-primary-700 rounded-xl p-8 text-white">
|
|
<h3 class="text-2xl font-display font-bold mb-6">
|
|
Statistik
|
|
</h3>
|
|
<div class="grid md:grid-cols-3 gap-6">
|
|
<div class="text-center">
|
|
<div class="text-3xl font-bold mb-2">
|
|
{{ verfuegbareJahre.length }}
|
|
</div>
|
|
<div class="text-primary-100">
|
|
Jahre mit Meisterschaften
|
|
</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="text-3xl font-bold mb-2">
|
|
{{ totalWinners }}
|
|
</div>
|
|
<div class="text-primary-100">
|
|
Einzelgewinner
|
|
</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="text-3xl font-bold mb-2">
|
|
{{ totalDoubles }}
|
|
</div>
|
|
<div class="text-primary-100">
|
|
Doppelgewinner
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gratulation -->
|
|
<div class="mt-8 text-center">
|
|
<div class="bg-white rounded-xl shadow-lg p-8 border-l-4 border-primary-600">
|
|
<h3 class="text-2xl font-display font-bold text-gray-900 mb-4 flex items-center justify-center">
|
|
<Trophy
|
|
:size="32"
|
|
class="text-primary-600 mr-3"
|
|
/>
|
|
Herzlichen Glückwunsch!
|
|
</h3>
|
|
<p class="text-lg text-gray-700 leading-relaxed">
|
|
Wir gratulieren allen Teilnehmern und Gewinnern der Vereinsmeisterschaften zu ihren großartigen Leistungen!
|
|
</p>
|
|
<p class="text-lg text-gray-700 leading-relaxed mt-4">
|
|
Besonders stolz sind wir auf die kontinuierliche Teilnahme und den fairen Wettkampfgeist unserer Mitglieder.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Lightbox für Bilder -->
|
|
<div
|
|
v-if="lightboxImage"
|
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-90 p-4"
|
|
tabindex="0"
|
|
@click="closeLightbox"
|
|
@keydown="handleLightboxKeydown"
|
|
>
|
|
<div
|
|
class="relative max-w-5xl max-h-full"
|
|
@click.stop
|
|
>
|
|
<!-- Close Button -->
|
|
<button
|
|
class="absolute top-4 right-4 text-white hover:text-gray-300 z-10 bg-black bg-opacity-50 rounded-full p-3"
|
|
aria-label="Schließen"
|
|
@click="closeLightbox"
|
|
>
|
|
<svg
|
|
class="w-8 h-8"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
|
|
<img
|
|
:src="`/api/personen/${lightboxImage.filename}`"
|
|
:alt="lightboxImage.name"
|
|
class="max-w-[90%] max-h-[90vh] object-contain mx-auto"
|
|
>
|
|
<div class="mt-4 text-white text-center">
|
|
<h3 class="text-xl font-semibold">
|
|
{{ lightboxImage.name }}
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { Trophy } from 'lucide-vue-next'
|
|
|
|
const results = ref([])
|
|
const selectedYear = ref('alle')
|
|
const lightboxImage = ref(null)
|
|
|
|
const loadResults = async () => {
|
|
try {
|
|
// Verwende API-Endpoint statt statische Datei, um Cache-Probleme zu vermeiden
|
|
const response = await fetch('/api/vereinsmeisterschaften')
|
|
if (!response.ok) return
|
|
|
|
const csv = await response.text()
|
|
const lines = csv.split('\n').filter(line => line.trim() !== '')
|
|
|
|
if (lines.length < 2) return
|
|
|
|
results.value = lines.slice(1).map(line => {
|
|
// CSV-Parser: Respektiert Anführungszeichen
|
|
const values = []
|
|
let current = ''
|
|
let inQuotes = false
|
|
|
|
for (let i = 0; i < line.length; i++) {
|
|
const char = line[i]
|
|
|
|
if (char === '"') {
|
|
inQuotes = !inQuotes
|
|
} else if (char === ',' && !inQuotes) {
|
|
values.push(current.trim())
|
|
current = ''
|
|
} else {
|
|
current += char
|
|
}
|
|
}
|
|
values.push(current.trim())
|
|
|
|
// Mindestens 6 Spalten erforderlich (die neuen Bildspalten sind optional)
|
|
if (values.length < 6) return null
|
|
|
|
return {
|
|
jahr: values[0].trim(),
|
|
kategorie: values[1].trim(),
|
|
platz: values[2].trim(),
|
|
spieler1: values[3].trim(),
|
|
spieler2: values[4].trim(),
|
|
bemerkung: values[5].trim(),
|
|
imageFilename1: values[6]?.trim() || '',
|
|
imageFilename2: values[7]?.trim() || ''
|
|
}
|
|
}).filter(result => result !== null)
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Vereinsmeisterschaften:', error)
|
|
}
|
|
}
|
|
|
|
const verfuegbareJahre = computed(() => {
|
|
const jahre = [...new Set(results.value.map(r => r.jahr).filter(j => j !== ''))]
|
|
return jahre.sort((a, b) => b - a) // Neueste zuerst
|
|
})
|
|
|
|
const filteredResults = computed(() => {
|
|
if (selectedYear.value === 'alle') {
|
|
return results.value
|
|
}
|
|
return results.value.filter(r => r.jahr === selectedYear.value)
|
|
})
|
|
|
|
const groupedResults = computed(() => {
|
|
const grouped = {}
|
|
|
|
filteredResults.value.forEach(result => {
|
|
if (!grouped[result.jahr]) {
|
|
grouped[result.jahr] = {
|
|
kategorien: {},
|
|
bemerkungen: null
|
|
}
|
|
}
|
|
|
|
// Besondere Bemerkungen (z.B. coronabedingter Ausfall)
|
|
if (result.bemerkung && result.bemerkung !== '') {
|
|
grouped[result.jahr].bemerkungen = result.bemerkung
|
|
return
|
|
}
|
|
|
|
// Normale Ergebnisse
|
|
if (result.kategorie && result.kategorie !== '') {
|
|
if (!grouped[result.jahr].kategorien[result.kategorie]) {
|
|
grouped[result.jahr].kategorien[result.kategorie] = []
|
|
}
|
|
grouped[result.jahr].kategorien[result.kategorie].push(result)
|
|
}
|
|
})
|
|
|
|
return grouped
|
|
})
|
|
|
|
const sortedGroupedResults = computed(() => {
|
|
const sorted = {}
|
|
const jahre = Object.keys(groupedResults.value).sort((a, b) => b - a) // Neueste zuerst
|
|
|
|
jahre.forEach(jahr => {
|
|
sorted[jahr] = groupedResults.value[jahr]
|
|
})
|
|
|
|
return sorted
|
|
})
|
|
|
|
const sortedJahre = computed(() => {
|
|
return Object.keys(groupedResults.value).sort((a, b) => b - a) // Neueste zuerst
|
|
})
|
|
|
|
const totalWinners = computed(() => {
|
|
return results.value.filter(r => r.kategorie === 'Einzel' && r.platz === '1').length
|
|
})
|
|
|
|
const totalDoubles = computed(() => {
|
|
return results.value.filter(r => r.kategorie === 'Doppel' && r.platz === '1').length
|
|
})
|
|
|
|
// Sortierreihenfolge für Kategorien
|
|
const kategorieReihenfolge = ['Einzel', 'Doppel', 'Mixed', 'Senioren', 'Jugend']
|
|
|
|
function getSortedKategorien(kategorien) {
|
|
if (!kategorien) return []
|
|
|
|
// Sortiere Kategorien nach vordefinierter Reihenfolge
|
|
const sorted = Object.keys(kategorien).sort((a, b) => {
|
|
const indexA = kategorieReihenfolge.indexOf(a)
|
|
const indexB = kategorieReihenfolge.indexOf(b)
|
|
|
|
// Wenn beide in der Liste sind, sortiere nach Index
|
|
if (indexA !== -1 && indexB !== -1) {
|
|
return indexA - indexB
|
|
}
|
|
// Wenn nur eine in der Liste ist, kommt sie zuerst
|
|
if (indexA !== -1) return -1
|
|
if (indexB !== -1) return 1
|
|
// Wenn keine in der Liste ist, alphabetisch sortieren
|
|
return a.localeCompare(b)
|
|
})
|
|
|
|
return sorted
|
|
}
|
|
|
|
function openLightbox(filename, name) {
|
|
lightboxImage.value = { filename, name }
|
|
document.body.style.overflow = 'hidden'
|
|
setTimeout(() => {
|
|
const modal = document.querySelector('[tabindex="0"]')
|
|
if (modal) modal.focus()
|
|
}, 100)
|
|
}
|
|
|
|
function closeLightbox() {
|
|
lightboxImage.value = null
|
|
document.body.style.overflow = ''
|
|
}
|
|
|
|
function handleLightboxKeydown(event) {
|
|
if (event.key === 'Escape') {
|
|
closeLightbox()
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadResults()
|
|
})
|
|
|
|
useHead({
|
|
title: 'Vereinsmeisterschaften - Harheimer TC',
|
|
})
|
|
</script>
|