Refactor navigation and game schedule components; remove 'Spielplan' links from Navigation.vue, update links to point to '/mannschaften/spielplaene' in Spielplan.vue, and enhance date and time formatting for improved display. Add PDF metadata and security headers in the API for better document handling.
This commit is contained in:
@@ -57,12 +57,6 @@
|
||||
Termine
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink to="/spielplan" @click="currentSubmenu = null"
|
||||
class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50"
|
||||
active-class="text-white bg-primary-600">
|
||||
Spielplan
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink
|
||||
v-if="hasGalleryImages"
|
||||
to="/galerie"
|
||||
@@ -414,11 +408,6 @@
|
||||
Termine
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink to="/spielplan" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-3 text-gray-300 hover:text-white hover:bg-primary-700/50 rounded-lg font-medium transition-colors">
|
||||
Spielplan
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink
|
||||
v-if="hasGalleryImages"
|
||||
to="/galerie"
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<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="/spielplan" class="inline-flex items-center px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors">
|
||||
<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>
|
||||
@@ -32,30 +32,38 @@
|
||||
</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="/spielplan" class="inline-flex items-center px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors">
|
||||
<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.Datum}-${game.Mannschaft}`"
|
||||
<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 -->
|
||||
<div class="flex items-center mb-4">
|
||||
<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.Datum) }}</span>
|
||||
<!-- 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">Mannschaft</p>
|
||||
<p class="font-semibold text-gray-900">{{ game.Mannschaft || 'Nicht angegeben' }}</p>
|
||||
<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">
|
||||
@@ -63,43 +71,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 text-right">
|
||||
<p class="text-sm text-gray-600 mb-1">Gegner</p>
|
||||
<p class="font-semibold text-gray-900">{{ game.Gegner || 'Nicht angegeben' }}</p>
|
||||
<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>
|
||||
|
||||
<!-- Details -->
|
||||
<div class="space-y-2">
|
||||
<div v-if="game.Ort" 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="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
{{ game.Ort }}
|
||||
</div>
|
||||
|
||||
<div v-if="game.Uhrzeit" 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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{{ formatTime(game.Uhrzeit) }}
|
||||
</div>
|
||||
|
||||
<div v-if="game.Status" class="flex items-center text-sm">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
||||
:class="getStatusClass(game.Status)">
|
||||
{{ game.Status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result (if available) -->
|
||||
<div v-if="game.Ergebnis" class="mt-4 pt-4 border-t border-gray-200">
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-600 mb-1">Ergebnis</p>
|
||||
<p class="text-lg font-bold text-primary-600">{{ game.Ergebnis }}</p>
|
||||
</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>
|
||||
@@ -107,7 +90,7 @@
|
||||
|
||||
<!-- View All Button -->
|
||||
<div v-if="upcomingGames && upcomingGames.length > 0" class="text-center mt-8">
|
||||
<NuxtLink to="/spielplan"
|
||||
<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">
|
||||
@@ -153,63 +136,136 @@ const upcomingGames = computed(() => {
|
||||
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.Datum) return false
|
||||
if (!game.Termin) return false
|
||||
|
||||
const gameDate = new Date(game.Datum)
|
||||
if (isNaN(gameDate.getTime())) 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]
|
||||
|
||||
return gameDate >= today
|
||||
// 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) => {
|
||||
const dateA = new Date(a.Datum)
|
||||
const dateB = new Date(b.Datum)
|
||||
// Sortierung nach Datum
|
||||
const dateA = parseGameDate(a.Termin)
|
||||
const dateB = parseGameDate(b.Termin)
|
||||
return dateA - dateB
|
||||
})
|
||||
.slice(0, 6) // Zeige nur die nächsten 6 Spiele
|
||||
.slice(0, 6) // Zeige maximal 6 Spiele
|
||||
})
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '-'
|
||||
// Hilfsfunktion zum Parsen von Spielterminen
|
||||
const parseGameDate = (terminString) => {
|
||||
if (!terminString) return new Date()
|
||||
|
||||
try {
|
||||
const date = new Date(dateString)
|
||||
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 dateString
|
||||
return terminString
|
||||
}
|
||||
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
// 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 dateString
|
||||
return terminString
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timeString) => {
|
||||
if (!timeString) return '-'
|
||||
const formatTime = (terminString) => {
|
||||
if (!terminString) return '-'
|
||||
|
||||
// Einfache Zeitformatierung (HH:MM)
|
||||
if (timeString.match(/^\d{1,2}:\d{2}$/)) {
|
||||
return timeString
|
||||
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
|
||||
}
|
||||
|
||||
return timeString
|
||||
}
|
||||
|
||||
const getStatusClass = (status) => {
|
||||
const statusMap = {
|
||||
'Geplant': 'bg-blue-100 text-blue-800',
|
||||
'Abgesagt': 'bg-red-100 text-red-800',
|
||||
'Verschoben': 'bg-yellow-100 text-yellow-800',
|
||||
'Beendet': 'bg-green-100 text-green-800',
|
||||
'Läuft': 'bg-purple-100 text-purple-800'
|
||||
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 statusMap[status] || 'bg-gray-100 text-gray-800'
|
||||
return runde
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -255,6 +255,21 @@ export default defineEventHandler(async (event) => {
|
||||
\\author{Harheimer TC}
|
||||
\\date{Saison 2025/26 - Stand: ${currentDate}}
|
||||
|
||||
% PDF-Metadaten für Sicherheit
|
||||
\\usepackage[pdftex]{hyperref}
|
||||
\\hypersetup{
|
||||
pdfauthor={Harheimer TC},
|
||||
pdftitle={Spielplan ${teamName} - Saison 2025/26},
|
||||
pdfsubject={Vereinsspielplan},
|
||||
pdfkeywords={Tischtennis, Spielplan, Harheimer TC, ${teamName}},
|
||||
pdfcreator={Harheimer TC CMS},
|
||||
pdfproducer={LaTeX mit pdflatex},
|
||||
pdfcreationdate={${new Date().toISOString()}},
|
||||
pdfmoddate={${new Date().toISOString()}},
|
||||
colorlinks=false,
|
||||
pdfborder={0 0 0}
|
||||
}
|
||||
|
||||
\\begin{document}
|
||||
\\maketitle
|
||||
|
||||
@@ -376,6 +391,14 @@ ${hallenListe.map(halle => {
|
||||
setHeader(event, 'Content-Disposition', `attachment; filename="spielplan_${team}.pdf"`)
|
||||
setHeader(event, 'Content-Length', pdfBuffer.length.toString())
|
||||
|
||||
// Füge Sicherheits-Header hinzu
|
||||
setHeader(event, 'X-Content-Type-Options', 'nosniff')
|
||||
setHeader(event, 'X-Frame-Options', 'DENY')
|
||||
setHeader(event, 'X-XSS-Protection', '1; mode=block')
|
||||
setHeader(event, 'Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
setHeader(event, 'Pragma', 'no-cache')
|
||||
setHeader(event, 'Expires', '0')
|
||||
|
||||
return pdfBuffer
|
||||
|
||||
} catch (compileError) {
|
||||
|
||||
0
temp/spielplan_all_1761261967839.out
Normal file
0
temp/spielplan_all_1761261967839.out
Normal file
Reference in New Issue
Block a user