Files
harheimertc/pages/vereinsmeisterschaften.vue
Torsten Schulz (local) 6df276ede2
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 51s
Refactor category sorting logic in Vereinsmeisterschaften component
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.
2026-01-17 18:22:25 +01:00

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>