Initial commit: Harheimer TC Website
- Vue 3 + Nuxt 3 Framework - Tailwind CSS Styling - Responsive Design mit schwarz-roten Vereinsfarben - Dynamische Galerie mit Lightbox - Event-Management über CSV-Dateien - Mannschaftsübersicht mit dynamischen Seiten - SMTP-Kontaktformular - Google Maps Integration - Mobile-optimierte Navigation mit Submenus - Trainer-Übersicht - Vereinsmeisterschaften, Spielsysteme, TT-Regeln - Impressum mit Datenschutzerklärung
This commit is contained in:
106
components/About.vue
Normal file
106
components/About.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<section id="about" class="py-16 sm:py-20 bg-gradient-to-b from-white to-gray-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-4">
|
||||
Über uns
|
||||
</h2>
|
||||
<div class="w-24 h-1 bg-primary-600 mx-auto mb-6" />
|
||||
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
Seit über 70 Jahren wird in unserem Harheimer Verein Tischtennis gespielt
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid lg:grid-cols-2 gap-12 items-center mb-20">
|
||||
<div class="relative h-[400px] sm:h-[500px] rounded-2xl overflow-hidden shadow-2xl">
|
||||
<div
|
||||
class="w-full h-full bg-cover bg-center hover:scale-110 transition-transform duration-700"
|
||||
style="background-image: url('https://images.unsplash.com/photo-1611004275469-8583ed5d7b8d?q=80&w=2070')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<h3 class="text-3xl font-display font-bold text-gray-900">
|
||||
Ein familiärer Verein mit Tradition
|
||||
</h3>
|
||||
<p class="text-lg text-gray-600 leading-relaxed">
|
||||
Wir sind ein kleiner, selbständiger, familiärer Verein mit ca. 40 Mitgliedern.
|
||||
Wir nehmen zurzeit mit fünf Herrenmannschaften an der Punktspielrunde teil.
|
||||
</p>
|
||||
<p class="text-lg text-gray-600 leading-relaxed">
|
||||
Ab der Saison 2025/26 werden wir auch wieder mit einer Jugendmannschaft aktiv.
|
||||
</p>
|
||||
<p class="text-lg text-gray-600 leading-relaxed">
|
||||
Wir trainieren zweimal wöchentlich in der Turnhalle der Grundschule Harheim mit
|
||||
anschließendem gemütlichem Beisammensein in einer der lokalen Gaststätten.
|
||||
Jährlich finden außerdem unsere Vereinsmeisterschaften statt.
|
||||
</p>
|
||||
<div class="bg-primary-50 border-l-4 border-primary-600 p-6 rounded-lg">
|
||||
<h4 class="text-xl font-semibold text-primary-800 mb-3">Wir suchen Verstärkung!</h4>
|
||||
<p class="text-primary-700 mb-4">
|
||||
Wir suchen ständig Verstärkungen für unsere Damen- und Herrenmannschaften!
|
||||
</p>
|
||||
<p class="text-primary-700 font-medium">
|
||||
Alle Tischtennis-Begeisterten sind herzlich zu einem Probetraining eingeladen!
|
||||
</p>
|
||||
</div>
|
||||
<div class="pt-4">
|
||||
<NuxtLink
|
||||
to="/kontakt"
|
||||
class="inline-flex items-center px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
|
||||
>
|
||||
Kontakt aufnehmen
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Values -->
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<div
|
||||
v-for="value in values"
|
||||
:key="value.title"
|
||||
class="bg-white p-6 rounded-xl shadow-lg hover:shadow-xl transition-shadow border border-gray-100"
|
||||
>
|
||||
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<component :is="value.icon" :size="24" class="text-primary-600" />
|
||||
</div>
|
||||
<h4 class="text-xl font-display font-bold text-gray-900 mb-2">
|
||||
{{ value.title }}
|
||||
</h4>
|
||||
<p class="text-gray-600">
|
||||
{{ value.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Heart, Award, Target, Users2 } from 'lucide-vue-next'
|
||||
|
||||
const values = [
|
||||
{
|
||||
icon: Heart,
|
||||
title: '70+ Jahre Tradition',
|
||||
description: 'Seit 1954 spielen wir Tischtennis in Harheim',
|
||||
},
|
||||
{
|
||||
icon: Users2,
|
||||
title: 'Familiärer Verein',
|
||||
description: 'Ca. 40 Mitglieder in einer herzlichen Gemeinschaft',
|
||||
},
|
||||
{
|
||||
icon: Award,
|
||||
title: '5 Herrenmannschaften',
|
||||
description: 'Aktive Teilnahme an der Punktspielrunde',
|
||||
},
|
||||
{
|
||||
icon: Target,
|
||||
title: 'Jugendförderung',
|
||||
description: 'Ab 2025/26 wieder eine Jugendmannschaft',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
124
components/Calendar.vue
Normal file
124
components/Calendar.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<section id="calendar" class="py-16 sm:py-20 bg-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-4">
|
||||
Termine & Events
|
||||
</h2>
|
||||
<div class="w-24 h-1 bg-primary-600 mx-auto mb-6" />
|
||||
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
Entdecken Sie unsere kommenden Veranstaltungen und Turniere
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<!-- Timeline line -->
|
||||
<div class="hidden lg:block absolute left-1/2 transform -translate-x-1/2 h-full w-0.5 bg-gradient-to-b from-primary-200 via-primary-400 to-primary-200" />
|
||||
|
||||
<div class="space-y-12">
|
||||
<div
|
||||
v-for="(event, index) in events"
|
||||
:key="event.title"
|
||||
:class="[
|
||||
'relative flex items-center',
|
||||
index % 2 === 0 ? 'lg:flex-row' : 'lg:flex-row-reverse'
|
||||
]"
|
||||
>
|
||||
<div :class="['w-full lg:w-5/12', index % 2 === 0 ? 'lg:pr-12' : 'lg:pl-12']">
|
||||
<div class="bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-shadow p-6 border border-gray-100">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div :class="['flex-shrink-0 w-14 h-14 bg-gradient-to-br rounded-xl flex items-center justify-center', event.color]">
|
||||
<component :is="event.icon" :size="28" class="text-white" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-semibold text-primary-600 mb-1">
|
||||
{{ event.date }}
|
||||
</div>
|
||||
<h3 class="text-xl font-display font-bold text-gray-900 mb-2">
|
||||
{{ event.title }}
|
||||
</h3>
|
||||
<p class="text-gray-600">
|
||||
{{ event.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline dot -->
|
||||
<div class="hidden lg:block absolute left-1/2 transform -translate-x-1/2">
|
||||
<div :class="['w-4 h-4 bg-gradient-to-br rounded-full border-4 border-white shadow-lg', event.color]" />
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:block w-5/12" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-16 text-center">
|
||||
<div class="bg-gray-50 rounded-2xl p-8 max-w-2xl mx-auto">
|
||||
<CalendarIcon :size="48" class="text-primary-600 mx-auto mb-4" />
|
||||
<h3 class="text-2xl font-display font-bold text-gray-900 mb-3">
|
||||
Regelmäßige Angebote
|
||||
</h3>
|
||||
<div class="space-y-2 text-gray-700">
|
||||
<p><strong>Dienstag & Donnerstag:</strong> Jugendtraining (17:00 - 19:00 Uhr)</p>
|
||||
<p><strong>Mittwoch:</strong> Hobbygruppe (19:00 - 21:00 Uhr)</p>
|
||||
<p><strong>Freitag:</strong> Wettkampftraining (18:00 - 20:00 Uhr)</p>
|
||||
<p><strong>Wochenende:</strong> Punktspiele & Ligabetrieb</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Calendar as CalendarIcon, Trophy, Users, PartyPopper } from 'lucide-vue-next'
|
||||
|
||||
const events = [
|
||||
{
|
||||
date: '15. Mai 2025',
|
||||
title: 'Saisoneröffnung',
|
||||
description: 'Offizieller Start in die Tennissaison mit großem Eröffnungsfest',
|
||||
icon: PartyPopper,
|
||||
color: 'from-pink-500 to-rose-500',
|
||||
},
|
||||
{
|
||||
date: '20-22. Juni 2025',
|
||||
title: 'Clubmeisterschaft',
|
||||
description: 'Spannende Matches um den Clubmeister-Titel',
|
||||
icon: Trophy,
|
||||
color: 'from-yellow-500 to-orange-500',
|
||||
},
|
||||
{
|
||||
date: '15. Juli 2025',
|
||||
title: 'Jugend-Turnier',
|
||||
description: 'Nachwuchsturnier für alle Altersklassen',
|
||||
icon: Users,
|
||||
color: 'from-blue-500 to-cyan-500',
|
||||
},
|
||||
{
|
||||
date: '10. August 2025',
|
||||
title: 'Sommerfest',
|
||||
description: 'Geselliges Beisammensein mit Grillen und Live-Musik',
|
||||
icon: PartyPopper,
|
||||
color: 'from-green-500 to-emerald-500',
|
||||
},
|
||||
{
|
||||
date: '1-3. September 2025',
|
||||
title: 'Vereinsmeisterschaft',
|
||||
description: 'Das Highlight der Saison - Vereinsmeisterschaft in allen Kategorien',
|
||||
icon: Trophy,
|
||||
color: 'from-purple-500 to-indigo-500',
|
||||
},
|
||||
{
|
||||
date: '30. September 2025',
|
||||
title: 'Saisonabschluss',
|
||||
description: 'Gemütlicher Ausklang der Saison mit Siegerehrung',
|
||||
icon: CalendarIcon,
|
||||
color: 'from-red-500 to-pink-500',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
238
components/Contact.vue
Normal file
238
components/Contact.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<section id="contact" class="py-16 sm:py-20 bg-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-4">
|
||||
Kontakt
|
||||
</h2>
|
||||
<div class="w-24 h-1 bg-primary-600 mx-auto mb-6" />
|
||||
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
Wir freuen uns auf Ihre Nachricht - Kontaktieren Sie uns!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid lg:grid-cols-2 gap-12 items-start">
|
||||
<!-- Contact Information -->
|
||||
<div class="space-y-6">
|
||||
<div
|
||||
v-for="info in contactInfo"
|
||||
:key="info.title"
|
||||
class="flex items-start space-x-4 bg-gray-50 p-6 rounded-xl hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<div :class="['flex-shrink-0 w-12 h-12 bg-gradient-to-br rounded-lg flex items-center justify-center', info.color]">
|
||||
<component :is="info.icon" :size="24" class="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-display font-bold text-gray-900 mb-2">
|
||||
{{ info.title }}
|
||||
</h3>
|
||||
<p v-for="(line, i) in info.content" :key="i" class="text-gray-600">
|
||||
{{ line }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map & Link -->
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-2xl overflow-hidden shadow-xl h-64">
|
||||
<iframe
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2554.5!2d8.660947!3d50.187044!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x47bd0e5e5e5e5e5e%3A0x5e5e5e5e5e5e5e5e!2sIn%20den%20Schafg%C3%A4rten%2025%2C%2060437%20Frankfurt%20am%20Main!5e0!3m2!1sde!2sde!4v1234567890"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style="border: 0"
|
||||
loading="lazy"
|
||||
allowfullscreen
|
||||
referrerpolicy="no-referrer-when-downgrade"
|
||||
title="Sporthalle der Grundschule Harheim"
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
href="https://www.google.com/maps/search/?api=1&query=In+den+Schafgärten+25+60437+Frankfurt"
|
||||
target="_blank"
|
||||
class="block text-center px-4 py-3 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg transition-colors"
|
||||
>
|
||||
In Google Maps öffnen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Form -->
|
||||
<div class="bg-gradient-to-br from-primary-50 to-primary-100/50 rounded-2xl p-8 shadow-xl">
|
||||
<h3 class="text-2xl font-display font-bold text-gray-900 mb-6">
|
||||
Senden Sie uns eine Nachricht
|
||||
</h3>
|
||||
<form class="space-y-4" @submit.prevent="sendEmail">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
v-model="formData.name"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all"
|
||||
placeholder="Ihr Name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
E-Mail *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
v-model="formData.email"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all"
|
||||
placeholder="ihre@email.de"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="phone" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Telefon
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
v-model="formData.phone"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all"
|
||||
placeholder="+49 123 456789"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="subject" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Betreff *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="subject"
|
||||
v-model="formData.subject"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all"
|
||||
placeholder="Worum geht es?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="message" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nachricht *
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
v-model="formData.message"
|
||||
required
|
||||
rows="5"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all resize-none"
|
||||
placeholder="Ihre Nachricht..."
|
||||
/>
|
||||
</div>
|
||||
<!-- Status Message -->
|
||||
<div v-if="submitStatus" class="p-4 rounded-lg" :class="submitStatus === 'success' ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'">
|
||||
<div class="flex items-center">
|
||||
<CheckCircle v-if="submitStatus === 'success'" :size="20" class="text-green-600 mr-2" />
|
||||
<AlertCircle v-else :size="20" class="text-red-600 mr-2" />
|
||||
<p :class="submitStatus === 'success' ? 'text-green-800' : 'text-red-800'" class="text-sm font-medium">
|
||||
{{ submitMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isSubmitting"
|
||||
class="w-full px-6 py-4 bg-primary-600 hover:bg-primary-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-300 flex items-center justify-center"
|
||||
>
|
||||
<Send v-if="!isSubmitting" :size="20" class="mr-2" />
|
||||
<div v-else class="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
||||
{{ isSubmitting ? 'Wird gesendet...' : 'E-Mail senden' }}
|
||||
</button>
|
||||
<p class="text-sm text-gray-600 text-center">
|
||||
* Pflichtfelder
|
||||
</p>
|
||||
</form>
|
||||
<p class="mt-4 text-sm text-gray-600 text-center">
|
||||
Ihre Nachricht wird direkt an j.dichmann@gmx.de gesendet
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { MapPin, Phone, Mail, Clock, Send, CheckCircle, AlertCircle } from 'lucide-vue-next'
|
||||
|
||||
const formData = ref({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
subject: '',
|
||||
message: ''
|
||||
})
|
||||
|
||||
const isSubmitting = ref(false)
|
||||
const submitStatus = ref(null) // 'success', 'error', null
|
||||
const submitMessage = ref('')
|
||||
|
||||
const sendEmail = async () => {
|
||||
isSubmitting.value = true
|
||||
submitStatus.value = null
|
||||
submitMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await $fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
body: formData.value
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
submitStatus.value = 'success'
|
||||
submitMessage.value = 'E-Mail wurde erfolgreich gesendet! Wir melden uns bald bei Ihnen.'
|
||||
|
||||
// Formular zurücksetzen
|
||||
formData.value = {
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
subject: '',
|
||||
message: ''
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Senden:', error)
|
||||
submitStatus.value = 'error'
|
||||
submitMessage.value = error.data?.message || 'Fehler beim Senden der E-Mail. Bitte versuchen Sie es später erneut.'
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const contactInfo = [
|
||||
{
|
||||
icon: MapPin,
|
||||
title: 'Trainingsort',
|
||||
content: ['Sporthalle der Grundschule Harheim', 'In den Schafgärten 25', '60437 Frankfurt/Main'],
|
||||
color: 'from-red-500 to-pink-500',
|
||||
},
|
||||
{
|
||||
icon: Phone,
|
||||
title: 'Telefon',
|
||||
content: ['06101-4992227'],
|
||||
color: 'from-green-500 to-emerald-500',
|
||||
},
|
||||
{
|
||||
icon: Mail,
|
||||
title: 'E-Mail',
|
||||
content: ['j.dichmann@gmx.de'],
|
||||
color: 'from-blue-500 to-cyan-500',
|
||||
},
|
||||
{
|
||||
icon: Clock,
|
||||
title: 'Trainingszeiten',
|
||||
content: ['Dienstag: 19:30 - 22:30 Uhr', 'Donnerstag: 19:30 - 22:30 Uhr'],
|
||||
color: 'from-purple-500 to-indigo-500',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
102
components/Facilities.vue
Normal file
102
components/Facilities.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<section id="facilities" class="py-16 sm:py-20 bg-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-4">
|
||||
Unsere Anlagen
|
||||
</h2>
|
||||
<div class="w-24 h-1 bg-primary-600 mx-auto mb-6" />
|
||||
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
Moderne Ausstattung und erstklassige Einrichtungen für ein perfektes Tischtenniserlebnis
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-8 mb-16">
|
||||
<div
|
||||
v-for="facility in facilities"
|
||||
:key="facility.title"
|
||||
class="group relative bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 overflow-hidden border border-gray-100"
|
||||
>
|
||||
<div :class="['absolute top-0 left-0 right-0 h-1 bg-gradient-to-r opacity-0 group-hover:opacity-100 transition-opacity', facility.color]" />
|
||||
<div class="p-8">
|
||||
<div :class="['w-16 h-16 bg-gradient-to-br rounded-xl flex items-center justify-center mb-4 group-hover:scale-110 transition-transform', facility.color]">
|
||||
<component :is="facility.icon" :size="32" class="text-white" />
|
||||
</div>
|
||||
<h3 class="text-2xl font-display font-bold text-gray-900 mb-3">
|
||||
{{ facility.title }}
|
||||
</h3>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
{{ facility.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Gallery -->
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div class="relative h-[300px] rounded-2xl overflow-hidden shadow-xl group">
|
||||
<div
|
||||
class="w-full h-full bg-cover bg-center group-hover:scale-110 transition-transform duration-700"
|
||||
style="background-image: url('https://images.unsplash.com/photo-1534438097545-77fef53fe2e8?q=80&w=2070')"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent flex items-end">
|
||||
<p class="text-white font-semibold text-xl p-6">Hochwertige Wettkampftische</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-[300px] rounded-2xl overflow-hidden shadow-xl group">
|
||||
<div
|
||||
class="w-full h-full bg-cover bg-center group-hover:scale-110 transition-transform duration-700"
|
||||
style="background-image: url('https://images.unsplash.com/photo-1611004275469-8583ed5d7b8d?q=80&w=2070')"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent flex items-end">
|
||||
<p class="text-white font-semibold text-xl p-6">Moderne Tischtennishalle</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Sun, CloudRain, Dumbbell, Utensils, Wifi, Droplets } from 'lucide-vue-next'
|
||||
|
||||
const facilities = [
|
||||
{
|
||||
icon: Sun,
|
||||
title: '8 Tischtennisplatten',
|
||||
description: 'Hochwertige Wettkampftische für optimales Spielvergnügen',
|
||||
color: 'from-yellow-400 to-orange-500',
|
||||
},
|
||||
{
|
||||
icon: CloudRain,
|
||||
title: 'Klimatisierte Halle',
|
||||
description: 'Optimale Bedingungen bei jedem Wetter in unserer modernen Halle',
|
||||
color: 'from-blue-400 to-blue-600',
|
||||
},
|
||||
{
|
||||
icon: Dumbbell,
|
||||
title: 'Trainingsbereich',
|
||||
description: 'Ballmaschinen und Trainingsgeräte für gezieltes Training',
|
||||
color: 'from-red-400 to-red-600',
|
||||
},
|
||||
{
|
||||
icon: Utensils,
|
||||
title: 'Clubhaus',
|
||||
description: 'Gemütliches Clubhaus mit Aufenthaltsraum und Küche',
|
||||
color: 'from-green-400 to-green-600',
|
||||
},
|
||||
{
|
||||
icon: Wifi,
|
||||
title: 'Kostenloses WLAN',
|
||||
description: 'Schnelles Internet auf der gesamten Anlage',
|
||||
color: 'from-purple-400 to-purple-600',
|
||||
},
|
||||
{
|
||||
icon: Droplets,
|
||||
title: 'Umkleiden & Duschen',
|
||||
description: 'Moderne, saubere Umkleideräume mit Duschen',
|
||||
color: 'from-cyan-400 to-cyan-600',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
23
components/Footer.vue
Normal file
23
components/Footer.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<footer class="fixed bottom-0 left-0 right-0 z-40 bg-gray-900 border-t border-gray-800 shadow-2xl">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-center space-y-2 sm:space-y-0">
|
||||
<p class="text-sm text-gray-400">
|
||||
© {{ currentYear }} Harheimer TC
|
||||
</p>
|
||||
<div class="flex items-center space-x-6 text-sm">
|
||||
<NuxtLink to="/impressum" class="text-gray-400 hover:text-primary-400 transition-colors">
|
||||
Impressum
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/kontakt" class="text-gray-400 hover:text-primary-400 transition-colors">
|
||||
Kontakt
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const currentYear = new Date().getFullYear()
|
||||
</script>
|
||||
106
components/Gallery.vue
Normal file
106
components/Gallery.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<section v-if="images.length > 0" id="gallery" class="py-16 sm:py-20 bg-gradient-to-b from-white to-gray-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-4">
|
||||
Galerie
|
||||
</h2>
|
||||
<div class="w-24 h-1 bg-primary-600 mx-auto mb-6" />
|
||||
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
Eindrücke von unserem Verein
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid sm:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-2">
|
||||
<div
|
||||
v-for="image in images"
|
||||
:key="image.filename"
|
||||
class="group relative w-20 h-20 rounded-md overflow-hidden shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer"
|
||||
@click="openLightbox(image)"
|
||||
>
|
||||
<img
|
||||
:src="`/galerie/${image.filename}`"
|
||||
:alt="image.title"
|
||||
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end">
|
||||
<p class="text-white font-semibold text-xs p-1 truncate">{{ image.title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lightbox Modal -->
|
||||
<div
|
||||
v-if="lightboxImage"
|
||||
class="fixed inset-0 z-50 bg-black/90 flex items-center justify-center p-4"
|
||||
@click="closeLightbox"
|
||||
>
|
||||
<div class="relative w-full h-full flex items-center justify-center">
|
||||
<button
|
||||
@click.stop="closeLightbox"
|
||||
class="absolute top-4 right-4 z-10 w-10 h-10 bg-white/20 hover:bg-white/30 rounded-full flex items-center justify-center text-white transition-colors"
|
||||
>
|
||||
<X :size="24" />
|
||||
</button>
|
||||
<img
|
||||
:src="`/galerie/${lightboxImage.filename}`"
|
||||
:alt="lightboxImage.title"
|
||||
class="max-w-[80vw] max-h-[80vh] object-contain rounded-lg"
|
||||
@click.stop
|
||||
/>
|
||||
<div class="absolute bottom-4 left-4 right-4 text-center">
|
||||
<p class="text-white font-semibold text-lg bg-black/50 rounded-lg px-4 py-2">
|
||||
{{ lightboxImage.title }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { X } from 'lucide-vue-next'
|
||||
|
||||
const images = ref([])
|
||||
const lightboxImage = ref(null)
|
||||
|
||||
const loadImages = async () => {
|
||||
try {
|
||||
const response = await $fetch('/api/galerie')
|
||||
images.value = response || []
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Galerie-Bilder:', error)
|
||||
images.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const openLightbox = (image) => {
|
||||
lightboxImage.value = image
|
||||
document.body.style.overflow = 'hidden' // Verhindert Scrollen im Hintergrund
|
||||
}
|
||||
|
||||
const closeLightbox = () => {
|
||||
lightboxImage.value = null
|
||||
document.body.style.overflow = 'auto' // Erlaubt wieder Scrollen
|
||||
}
|
||||
|
||||
// ESC-Taste zum Schließen
|
||||
const handleKeydown = (event) => {
|
||||
if (event.key === 'Escape' && lightboxImage.value) {
|
||||
closeLightbox()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadImages()
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
document.body.style.overflow = 'auto' // Cleanup
|
||||
})
|
||||
</script>
|
||||
|
||||
86
components/Hero.vue
Normal file
86
components/Hero.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<section id="home" class="relative min-h-full flex items-center justify-center overflow-hidden py-20 bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
<!-- Decorative Elements -->
|
||||
<div class="absolute inset-0 z-0">
|
||||
<div class="absolute top-0 right-0 w-96 h-96 bg-primary-200/30 rounded-full blur-3xl" />
|
||||
<div class="absolute bottom-0 left-0 w-96 h-96 bg-gray-300/30 rounded-full blur-3xl" />
|
||||
<div
|
||||
class="absolute inset-0 opacity-5"
|
||||
style="background-image: url('https://images.unsplash.com/photo-1609710228159-0fa9bd7c0827?q=80&w=2070'); background-size: cover; background-position: center;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="relative z-20 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 sm:py-32">
|
||||
<div class="text-center">
|
||||
<h1 class="text-5xl sm:text-6xl lg:text-7xl font-display font-bold text-gray-900 mb-6 leading-tight animate-fade-in">
|
||||
Willkommen beim<br />
|
||||
<span class="text-primary-600">Harheimer TC</span>
|
||||
</h1>
|
||||
|
||||
<p class="text-xl sm:text-2xl text-gray-700 mb-8 max-w-3xl mx-auto animate-fade-in-delay-1">
|
||||
Tradition trifft Moderne - Ihr Tischtennisverein in Frankfurt-Harheim seit über 45 Jahren
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center animate-fade-in-delay-2">
|
||||
<NuxtLink
|
||||
to="/mitgliedschaft"
|
||||
class="group px-8 py-4 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 flex items-center space-x-2"
|
||||
>
|
||||
<span>Mitglied werden</span>
|
||||
<ArrowRight :size="20" class="group-hover:translate-x-1 transition-transform" />
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
to="/kontakt"
|
||||
class="px-8 py-4 bg-white hover:bg-gray-50 text-gray-900 font-semibold rounded-xl border-2 border-gray-300 hover:border-primary-600 shadow-lg transition-all duration-300"
|
||||
>
|
||||
Kontakt aufnehmen
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Termine -->
|
||||
<div class="mt-16 max-w-4xl mx-auto">
|
||||
<TermineVorschau />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scroll Indicator -->
|
||||
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 z-20 animate-bounce">
|
||||
<div class="w-6 h-10 border-2 border-gray-400 rounded-full flex justify-center pt-2">
|
||||
<div class="w-1.5 h-3 bg-primary-600 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ArrowRight } from 'lucide-vue-next'
|
||||
import TermineVorschau from './TermineVorschau.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.8s ease-out;
|
||||
}
|
||||
|
||||
.animate-fade-in-delay-1 {
|
||||
animation: fadeIn 0.8s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
.animate-fade-in-delay-2 {
|
||||
animation: fadeIn 0.8s ease-out 0.4s both;
|
||||
}
|
||||
</style>
|
||||
|
||||
207
components/MannschaftenUebersicht.vue
Normal file
207
components/MannschaftenUebersicht.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="mannschaften.length > 0" class="space-y-8">
|
||||
<div
|
||||
v-for="(mannschaft, index) in mannschaften"
|
||||
:key="index"
|
||||
class="bg-white rounded-xl shadow-lg border border-gray-100 overflow-hidden"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="bg-gradient-to-r from-primary-600 to-primary-700 p-6">
|
||||
<h2 class="text-2xl font-display font-bold text-white mb-2">
|
||||
{{ mannschaft.mannschaft }}
|
||||
</h2>
|
||||
<p class="text-primary-100 text-lg">{{ mannschaft.liga }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-6">
|
||||
<!-- Liga-Info -->
|
||||
<div class="grid md:grid-cols-2 gap-6 mb-6">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-2 h-2 bg-primary-600 rounded-full"></div>
|
||||
<span class="text-gray-600">Staffelleiter:</span>
|
||||
<span class="font-semibold text-gray-900">{{ mannschaft.staffelleiter }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-2 h-2 bg-primary-600 rounded-full"></div>
|
||||
<span class="text-gray-600">Telefon:</span>
|
||||
<span class="font-semibold text-gray-900">{{ mannschaft.telefon }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-2 h-2 bg-primary-600 rounded-full"></div>
|
||||
<span class="text-gray-600">Heimspieltag:</span>
|
||||
<span class="font-semibold text-gray-900">{{ mannschaft.heimspieltag }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-2 h-2 bg-primary-600 rounded-full"></div>
|
||||
<span class="text-gray-600">Spielsystem:</span>
|
||||
<span class="font-semibold text-gray-900">{{ mannschaft.spielsystem }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mannschaftsaufstellung -->
|
||||
<div class="border-t border-gray-200 pt-6">
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-4">
|
||||
Mannschaftsaufstellung Saison 2025/26 (Hinrunde)
|
||||
</h3>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div
|
||||
v-for="(spieler, spielerIndex) in getSpielerListe(mannschaft)"
|
||||
:key="spielerIndex"
|
||||
class="bg-gray-50 rounded-lg p-4 text-center"
|
||||
:class="spieler === mannschaft.mannschaftsfuehrer ? 'ring-2 ring-primary-500 bg-primary-50' : ''"
|
||||
>
|
||||
<div class="font-semibold text-gray-900">{{ spieler }}</div>
|
||||
<div v-if="spieler === mannschaft.mannschaftsfuehrer" class="text-xs text-primary-600 font-medium mt-1">
|
||||
Mannschaftsführer
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Links -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="text-center">
|
||||
<a
|
||||
v-if="mannschaft.weitere_informationen_link && mannschaft.weitere_informationen_link !== ''"
|
||||
:href="mannschaft.weitere_informationen_link"
|
||||
target="_blank"
|
||||
class="inline-flex items-center px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
|
||||
>
|
||||
<BarChart :size="20" class="mr-2" />
|
||||
Weitere Informationen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Letzte Aktualisierung -->
|
||||
<div class="border-t border-gray-200 pt-4 mt-6">
|
||||
<p class="text-sm text-gray-500 text-center">
|
||||
Zuletzt aktualisiert am: {{ formatDate(mannschaft.letzte_aktualisierung) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center py-12 bg-gray-50 rounded-xl">
|
||||
<Users :size="48" class="text-gray-400 mx-auto mb-4" />
|
||||
<p class="text-gray-600">Keine Mannschaftsdaten geladen</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Calendar, Users, BarChart } from 'lucide-vue-next'
|
||||
|
||||
const mannschaften = ref([])
|
||||
|
||||
const loadMannschaften = async () => {
|
||||
try {
|
||||
console.log('Lade Mannschaften...')
|
||||
const response = await fetch('/data/mannschaften.csv')
|
||||
console.log('Response:', response)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const csv = await response.text()
|
||||
console.log('CSV Text:', csv)
|
||||
|
||||
// Vereinfachter CSV-Parser
|
||||
const lines = csv.split('\n').filter(line => line.trim() !== '')
|
||||
console.log('CSV Lines:', lines)
|
||||
|
||||
if (lines.length < 2) {
|
||||
console.log('Keine Datenzeilen gefunden')
|
||||
return
|
||||
}
|
||||
|
||||
mannschaften.value = lines.slice(1).map((line, index) => {
|
||||
// Besserer 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())
|
||||
|
||||
if (values.length < 10) {
|
||||
console.log(`Zeile ${index + 2} hat zu wenige Werte:`, values)
|
||||
return null
|
||||
}
|
||||
|
||||
const mannschaft = {
|
||||
mannschaft: values[0].trim(),
|
||||
liga: values[1].trim(),
|
||||
staffelleiter: values[2].trim(),
|
||||
telefon: values[3].trim(),
|
||||
heimspieltag: values[4].trim(),
|
||||
spielsystem: values[5].trim(),
|
||||
mannschaftsfuehrer: values[6].trim(),
|
||||
spieler: values[7].trim(),
|
||||
weitere_informationen_link: values[8].trim(),
|
||||
letzte_aktualisierung: values[9] ? values[9].trim() : ''
|
||||
}
|
||||
|
||||
console.log(`Mannschaft ${index + 1}:`, mannschaft)
|
||||
console.log(`Parsed values count: ${values.length}`)
|
||||
console.log(`Letzte Aktualisierung raw: "${values[9]}"`)
|
||||
console.log(`Letzte Aktualisierung trimmed: "${values[9] ? values[9].trim() : 'undefined'}")`)
|
||||
return mannschaft
|
||||
}).filter(mannschaft => mannschaft !== null)
|
||||
|
||||
console.log('Alle geparsten Mannschaften:', mannschaften.value)
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Mannschaften:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getSpielerListe = (mannschaft) => {
|
||||
if (!mannschaft.spieler) return []
|
||||
return mannschaft.spieler.split(';').map(s => s.trim()).filter(s => s !== '')
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
|
||||
// Wenn bereits im Format DD.MM.YYYY, direkt zurückgeben
|
||||
if (/^\d{2}\.\d{2}\.\d{4}$/.test(dateString)) {
|
||||
return dateString
|
||||
}
|
||||
|
||||
// Versuche, das Datum zu parsen
|
||||
const date = new Date(dateString)
|
||||
if (isNaN(date.getTime())) {
|
||||
return dateString // Falls ungültig, Original zurückgeben
|
||||
}
|
||||
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadMannschaften()
|
||||
})
|
||||
</script>
|
||||
169
components/Membership.vue
Normal file
169
components/Membership.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<section id="membership" class="py-16 sm:py-20 bg-gradient-to-b from-gray-50 to-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-4">
|
||||
Mitgliedschaft
|
||||
</h2>
|
||||
<div class="w-24 h-1 bg-primary-600 mx-auto mb-6" />
|
||||
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
Werden Sie Teil unserer Tischtennis-Familie - Wählen Sie die passende Mitgliedschaft für sich
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto">
|
||||
<div
|
||||
v-for="plan in plans"
|
||||
:key="plan.name"
|
||||
:class="[
|
||||
'relative bg-white rounded-2xl shadow-xl overflow-hidden',
|
||||
plan.popular ? 'ring-4 ring-primary-500 scale-105' : ''
|
||||
]"
|
||||
>
|
||||
<div v-if="plan.popular" class="absolute top-0 right-0 bg-primary-600 text-white px-4 py-1 text-sm font-semibold rounded-bl-lg">
|
||||
Beliebt
|
||||
</div>
|
||||
|
||||
<div :class="['h-2 bg-gradient-to-r', plan.gradient]" />
|
||||
|
||||
<div class="p-8">
|
||||
<div :class="['w-12 h-12 bg-gradient-to-br rounded-xl flex items-center justify-center mb-4', plan.gradient]">
|
||||
<component :is="plan.icon" :size="24" class="text-white" />
|
||||
</div>
|
||||
|
||||
<h3 class="text-2xl font-display font-bold text-gray-900 mb-2">
|
||||
{{ plan.name }}
|
||||
</h3>
|
||||
<p class="text-gray-600 mb-6 min-h-[3rem]">
|
||||
{{ plan.description }}
|
||||
</p>
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="flex items-baseline">
|
||||
<span class="text-5xl font-bold text-gray-900">{{ plan.price }}€</span>
|
||||
<span class="text-gray-600 ml-2">/ {{ plan.period }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="space-y-3 mb-8">
|
||||
<li v-for="feature in plan.features" :key="feature" class="flex items-start">
|
||||
<Check :size="20" class="text-primary-600 mr-3 flex-shrink-0 mt-0.5" />
|
||||
<span class="text-gray-700">{{ feature }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<NuxtLink
|
||||
to="/kontakt"
|
||||
:class="[
|
||||
'block w-full text-center px-6 py-3 rounded-lg font-semibold transition-all duration-300',
|
||||
plan.popular
|
||||
? 'bg-primary-600 hover:bg-primary-700 text-white shadow-lg hover:shadow-xl'
|
||||
: 'bg-gray-100 hover:bg-gray-200 text-gray-900'
|
||||
]"
|
||||
>
|
||||
Jetzt beitreten
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Satzung Download -->
|
||||
<div class="mt-16 bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
|
||||
<div class="text-center mb-8">
|
||||
<h3 class="text-3xl font-display font-bold text-gray-900 mb-4">
|
||||
Vereinsatzung
|
||||
</h3>
|
||||
<p class="text-xl text-gray-600">
|
||||
Laden Sie unsere aktuelle Vereinsatzung herunter
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<a
|
||||
href="/documents/satzung.pdf"
|
||||
target="_blank"
|
||||
class="inline-flex items-center px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
|
||||
>
|
||||
<FileText :size="20" class="mr-2" />
|
||||
Satzung herunterladen (PDF)
|
||||
</a>
|
||||
<span class="text-sm text-gray-500">oder</span>
|
||||
<NuxtLink
|
||||
to="/satzung"
|
||||
class="inline-flex items-center px-6 py-3 bg-gray-100 hover:bg-gray-200 text-gray-900 font-semibold rounded-lg transition-colors"
|
||||
>
|
||||
<Eye :size="20" class="mr-2" />
|
||||
Online ansehen
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-16 bg-gradient-to-r from-primary-600 to-primary-700 rounded-2xl p-8 sm:p-12 text-center">
|
||||
<h3 class="text-3xl font-display font-bold text-white mb-4">
|
||||
Noch Fragen zur Mitgliedschaft?
|
||||
</h3>
|
||||
<p class="text-xl text-primary-100 mb-6">
|
||||
Kontaktieren Sie uns - wir beraten Sie gerne persönlich
|
||||
</p>
|
||||
<NuxtLink
|
||||
to="/kontakt"
|
||||
class="inline-flex items-center px-8 py-4 bg-white text-primary-600 font-semibold rounded-lg hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
Jetzt Kontakt aufnehmen
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Check, Star, Heart, FileText, Eye } from 'lucide-vue-next'
|
||||
|
||||
const plans = [
|
||||
{
|
||||
name: 'Kinder/Jugend',
|
||||
price: '72',
|
||||
period: 'Jahr',
|
||||
description: 'Perfekt für junge Tischtennisspieler bis 18 Jahre',
|
||||
features: [
|
||||
'Unbegrenzte Hallennutzung',
|
||||
'Kostenfreies Jugendtraining',
|
||||
'Teilnahme an Jugendturnieren',
|
||||
'Clubveranstaltungen',
|
||||
'Gäste mitbringen',
|
||||
],
|
||||
icon: Star,
|
||||
gradient: 'from-blue-500 to-cyan-500',
|
||||
},
|
||||
{
|
||||
name: 'Erwachsene',
|
||||
price: '120',
|
||||
period: 'Jahr',
|
||||
description: 'Vollmitgliedschaft für Erwachsene',
|
||||
features: [
|
||||
'Unbegrenzte Hallennutzung',
|
||||
'Freies Spielen nach Verfügbarkeit',
|
||||
'Clubveranstaltungen',
|
||||
'Gäste mitbringen',
|
||||
'Zugang Trainingsbereich',
|
||||
],
|
||||
icon: Check,
|
||||
gradient: 'from-primary-500 to-green-600',
|
||||
popular: true,
|
||||
},
|
||||
{
|
||||
name: 'Passiv',
|
||||
price: '30',
|
||||
period: 'Jahr',
|
||||
description: 'Unterstützen Sie Ihren Lieblingsverein',
|
||||
features: [
|
||||
'Vereinsunterstützung',
|
||||
'Vereinsinformationen',
|
||||
'Keine Spielberechtigung',
|
||||
],
|
||||
icon: Heart,
|
||||
gradient: 'from-orange-500 to-red-500',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
414
components/Navigation.vue
Normal file
414
components/Navigation.vue
Normal file
@@ -0,0 +1,414 @@
|
||||
<template>
|
||||
<nav
|
||||
class="fixed top-0 left-0 right-0 z-50 bg-gradient-to-r from-gray-900 via-primary-900 to-gray-900 shadow-xl h-20">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-full">
|
||||
<div class="flex flex-col justify-between h-full py-2">
|
||||
<!-- Hauptmenü -->
|
||||
<div class="flex justify-between items-center">
|
||||
<!-- Logo -->
|
||||
<NuxtLink to="/" class="flex items-center space-x-3 hover:scale-105 transition-transform">
|
||||
<img
|
||||
src="~/assets/images/logos/Harheimer TC.svg"
|
||||
alt="Harheimer TC Logo"
|
||||
class="w-12 h-12"
|
||||
/>
|
||||
<div class="hidden sm:block">
|
||||
<span class="text-xl font-display font-bold text-white">Harheimer <span
|
||||
class="text-primary-400">TC</span></span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
|
||||
<div style="display:flex;flex-direction:column;">
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden lg:flex items-center space-x-1">
|
||||
<NuxtLink to="/"
|
||||
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">
|
||||
Start
|
||||
</NuxtLink>
|
||||
|
||||
<button @click="toggleSubmenu('verein')"
|
||||
class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50"
|
||||
:class="(route.path.startsWith('/ueber-uns') || route.path.startsWith('/vorstand') || route.path.startsWith('/geschichte') || route.path.startsWith('/satzung') || route.path.startsWith('/vereinsmeisterschaften') || currentSubmenu === 'verein') ? 'text-white bg-primary-600' : ''">
|
||||
Verein
|
||||
</button>
|
||||
|
||||
<button @click="toggleSubmenu('mannschaften')"
|
||||
class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50"
|
||||
:class="(route.path.startsWith('/mannschaften') || route.path.startsWith('/spielsysteme') || currentSubmenu === 'mannschaften') ? 'text-white bg-primary-600' : ''">
|
||||
Mannschaften
|
||||
</button>
|
||||
|
||||
<button @click="toggleSubmenu('training')"
|
||||
class="px-4 py-2 text-gray-300 hover:text-white font-medium transition-all rounded-lg hover:bg-primary-700/50"
|
||||
:class="(route.path.startsWith('/training') || route.path.startsWith('/tt-regeln') || currentSubmenu === 'training') ? 'text-white bg-primary-600' : ''">
|
||||
Training
|
||||
</button>
|
||||
|
||||
<NuxtLink to="/mitgliedschaft"
|
||||
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">
|
||||
Mitgliedschaft
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink to="/termine" @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">
|
||||
Termine
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink
|
||||
v-if="hasGalleryImages"
|
||||
to="/galerie"
|
||||
@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">
|
||||
Galerie
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink to="/kontakt" @click="currentSubmenu = null"
|
||||
class="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold transition-all rounded-lg shadow-lg">
|
||||
Kontakt
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:flex items-center h-6 border-t border-primary-700/20">
|
||||
<div v-if="currentSubmenu" class="flex items-center space-x-1">
|
||||
<!-- Verein Submenu -->
|
||||
<template v-if="currentSubmenu === 'verein'">
|
||||
<NuxtLink to="/ueber-uns"
|
||||
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
|
||||
active-class="text-white bg-primary-600">
|
||||
Über uns
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/vorstand"
|
||||
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
|
||||
active-class="text-white bg-primary-600">
|
||||
Vorstand
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/geschichte"
|
||||
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
|
||||
active-class="text-white bg-primary-600">
|
||||
Geschichte
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/satzung"
|
||||
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
|
||||
active-class="text-white bg-primary-600">
|
||||
Satzung
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/vereinsmeisterschaften"
|
||||
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
|
||||
active-class="text-white bg-primary-600">
|
||||
Vereinsmeisterschaften
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<!-- Mannschaften Submenu -->
|
||||
<template v-if="currentSubmenu === 'mannschaften'">
|
||||
<NuxtLink to="/mannschaften"
|
||||
class="px-2.5 py-1 text-xs font-semibold text-white hover:bg-primary-700/50 rounded transition-all"
|
||||
active-class="bg-primary-600">
|
||||
Übersicht
|
||||
</NuxtLink>
|
||||
<div class="h-3 w-px bg-primary-700" />
|
||||
<template v-for="mannschaft in mannschaften" :key="mannschaft.slug">
|
||||
<NuxtLink
|
||||
:to="`/mannschaften/${mannschaft.slug}`"
|
||||
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
|
||||
active-class="text-white bg-primary-600">
|
||||
{{ mannschaft.mannschaft }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<div class="h-3 w-px bg-primary-700" />
|
||||
<NuxtLink to="/mannschaften/spielplaene"
|
||||
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
|
||||
active-class="text-white bg-primary-600">
|
||||
Spielpläne
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/spielsysteme"
|
||||
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
|
||||
active-class="text-white bg-primary-600">
|
||||
Spielsysteme
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<!-- Training Submenu -->
|
||||
<template v-if="currentSubmenu === 'training'">
|
||||
<NuxtLink to="/training"
|
||||
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
|
||||
active-class="text-white bg-primary-600">
|
||||
Trainingszeiten
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/training/trainer"
|
||||
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
|
||||
active-class="text-white bg-primary-600">
|
||||
Trainer
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/training/anfaenger"
|
||||
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
|
||||
active-class="text-white bg-primary-600">
|
||||
Anfänger
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/tt-regeln"
|
||||
class="px-2.5 py-1 text-xs text-gray-300 hover:text-white hover:bg-primary-700/50 rounded transition-all"
|
||||
active-class="text-white bg-primary-600">
|
||||
TT-Regeln
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<button @click="isMobileMenuOpen = !isMobileMenuOpen"
|
||||
class="lg:hidden p-2 rounded-lg hover:bg-primary-700/50 transition-colors" aria-label="Toggle menu">
|
||||
<X v-if="isMobileMenuOpen" :size="24" class="text-white" />
|
||||
<Menu v-else :size="24" class="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Untermenü (Desktop) - im gleichen Block, immer gleiche Höhe -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<Transition enter-active-class="transition duration-200 ease-out"
|
||||
enter-from-class="opacity-0 transform -translate-y-2" enter-to-class="opacity-100 transform translate-y-0"
|
||||
leave-active-class="transition duration-150 ease-in" leave-from-class="opacity-100 transform translate-y-0"
|
||||
leave-to-class="opacity-0 transform -translate-y-2">
|
||||
<div v-if="isMobileMenuOpen"
|
||||
class="lg:hidden bg-gray-800 border-t border-primary-700/30 max-h-[80vh] overflow-y-auto">
|
||||
<div class="px-4 py-4 space-y-2">
|
||||
<NuxtLink to="/" @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">
|
||||
Start
|
||||
</NuxtLink>
|
||||
|
||||
<!-- Verein Mobile -->
|
||||
<div>
|
||||
<button @click="toggleMobileSubmenu('verein')"
|
||||
class="w-full flex items-center justify-between px-4 py-3 text-gray-300 hover:text-white hover:bg-primary-700/50 rounded-lg font-medium transition-colors">
|
||||
Verein
|
||||
<ChevronDown :size="16"
|
||||
:class="['transition-transform', mobileSubmenu === 'verein' ? 'rotate-180' : '']" />
|
||||
</button>
|
||||
<div v-if="mobileSubmenu === 'verein'" class="pl-4 space-y-1 mt-1 bg-primary-900/30 rounded-lg p-2">
|
||||
<NuxtLink to="/ueber-uns" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
Über uns
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/vorstand" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
Vorstand
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/geschichte" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
Geschichte
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/satzung" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
Satzung
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/vereinsmeisterschaften" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
Vereinsmeisterschaften
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mannschaften Mobile -->
|
||||
<div>
|
||||
<button @click="toggleMobileSubmenu('mannschaften')"
|
||||
class="w-full flex items-center justify-between px-4 py-3 text-gray-300 hover:text-white hover:bg-primary-700/50 rounded-lg font-medium transition-colors">
|
||||
Mannschaften
|
||||
<ChevronDown :size="16"
|
||||
:class="['transition-transform', mobileSubmenu === 'mannschaften' ? 'rotate-180' : '']" />
|
||||
</button>
|
||||
<div v-if="mobileSubmenu === 'mannschaften'" class="pl-4 space-y-1 mt-1 bg-primary-900/30 rounded-lg p-2">
|
||||
<NuxtLink to="/mannschaften" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-2 text-sm font-semibold text-gray-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
Übersicht
|
||||
</NuxtLink>
|
||||
<template v-for="mannschaft in mannschaften" :key="mannschaft.slug">
|
||||
<NuxtLink
|
||||
:to="`/mannschaften/${mannschaft.slug}`"
|
||||
@click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
{{ mannschaft.mannschaft }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<div class="border-t border-primary-700/20 my-2" />
|
||||
<NuxtLink to="/mannschaften/spielplaene" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
Spielpläne
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/spielsysteme" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
Spielsysteme
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Training Mobile -->
|
||||
<div>
|
||||
<button @click="toggleMobileSubmenu('training')"
|
||||
class="w-full flex items-center justify-between px-4 py-3 text-gray-300 hover:text-white hover:bg-primary-700/50 rounded-lg font-medium transition-colors">
|
||||
Training
|
||||
<ChevronDown :size="16"
|
||||
:class="['transition-transform', mobileSubmenu === 'training' ? 'rotate-180' : '']" />
|
||||
</button>
|
||||
<div v-if="mobileSubmenu === 'training'" class="pl-4 space-y-1 mt-1 bg-primary-900/30 rounded-lg p-2">
|
||||
<NuxtLink to="/training" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
Trainingszeiten
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/training/trainer" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
Trainer
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/training/anfaenger" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
Anfänger
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/tt-regeln" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-2 text-sm text-gray-400 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors">
|
||||
TT-Regeln
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NuxtLink to="/mitgliedschaft" @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">
|
||||
Mitgliedschaft
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink to="/termine" @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">
|
||||
Termine
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink
|
||||
v-if="hasGalleryImages"
|
||||
to="/galerie"
|
||||
@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">
|
||||
Galerie
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink to="/kontakt" @click="isMobileMenuOpen = false"
|
||||
class="block px-4 py-3 bg-primary-600 hover:bg-primary-700 text-white rounded-lg font-semibold transition-colors">
|
||||
Kontakt
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Menu, X, ChevronDown } from 'lucide-vue-next'
|
||||
|
||||
const route = useRoute()
|
||||
const isMobileMenuOpen = ref(false)
|
||||
const mobileSubmenu = ref(null)
|
||||
const mannschaften = ref([])
|
||||
const hasGalleryImages = ref(false)
|
||||
|
||||
// Automatisches Setzen des Submenus basierend auf der Route
|
||||
const currentSubmenu = computed(() => {
|
||||
const path = route.path
|
||||
if (path.startsWith('/ueber-uns') || path.startsWith('/vorstand') ||
|
||||
path.startsWith('/geschichte') || path.startsWith('/satzung') ||
|
||||
path.startsWith('/vereinsmeisterschaften')) {
|
||||
return 'verein'
|
||||
}
|
||||
if (path.startsWith('/mannschaften') || path.startsWith('/spielsysteme')) {
|
||||
return 'mannschaften'
|
||||
}
|
||||
if (path.startsWith('/training') || path.startsWith('/tt-regeln')) {
|
||||
return 'training'
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
// Manuelles Toggle für Click-Events
|
||||
const manualSubmenu = ref(null)
|
||||
|
||||
const toggleMobileSubmenu = (menu) => {
|
||||
mobileSubmenu.value = mobileSubmenu.value === menu ? null : menu
|
||||
}
|
||||
|
||||
const loadMannschaften = async () => {
|
||||
try {
|
||||
const response = await fetch('/data/mannschaften.csv')
|
||||
if (!response.ok) return
|
||||
|
||||
const csv = await response.text()
|
||||
const lines = csv.split('\n').filter(line => line.trim() !== '')
|
||||
|
||||
if (lines.length < 2) return
|
||||
|
||||
mannschaften.value = lines.slice(1).map(line => {
|
||||
// Besserer 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())
|
||||
|
||||
if (values.length < 10) return null
|
||||
|
||||
return {
|
||||
mannschaft: values[0].trim(),
|
||||
slug: values[0].trim().toLowerCase().replace(/\s+/g, '-')
|
||||
}
|
||||
}).filter(mannschaft => mannschaft !== null)
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Mannschaften:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const checkGalleryImages = async () => {
|
||||
try {
|
||||
const response = await $fetch('/api/galerie')
|
||||
hasGalleryImages.value = response && response.length > 0
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Prüfen der Galerie-Bilder:', error)
|
||||
hasGalleryImages.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadMannschaften()
|
||||
checkGalleryImages()
|
||||
})
|
||||
|
||||
const toggleSubmenu = (menu) => {
|
||||
// Wenn wir schon im richtigen Bereich sind, nichts tun (Submenu bleibt offen)
|
||||
// Wenn nicht, zur Hauptseite navigieren
|
||||
const path = route.path
|
||||
|
||||
if (menu === 'verein' && !path.startsWith('/ueber-uns') && !path.startsWith('/vorstand') &&
|
||||
!path.startsWith('/geschichte') && !path.startsWith('/satzung') && !path.startsWith('/vereinsmeisterschaften')) {
|
||||
navigateTo('/ueber-uns')
|
||||
} else if (menu === 'mannschaften' && !path.startsWith('/mannschaften') && !path.startsWith('/spielsysteme')) {
|
||||
navigateTo('/mannschaften')
|
||||
} else if (menu === 'training' && !path.startsWith('/training') && !path.startsWith('/tt-regeln')) {
|
||||
navigateTo('/training')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
141
components/TermineVorschau.vue
Normal file
141
components/TermineVorschau.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-center mb-6">
|
||||
<h2 class="text-2xl font-display font-bold text-gray-900 mb-2">
|
||||
Kommende Termine
|
||||
</h2>
|
||||
<div class="w-16 h-0.5 bg-primary-600 mx-auto" />
|
||||
</div>
|
||||
|
||||
<div v-if="naechsteTermine.length > 0" class="space-y-2 mb-6">
|
||||
<div
|
||||
v-for="(termin, index) in naechsteTermine"
|
||||
:key="index"
|
||||
class="bg-gray-50 rounded-lg p-3 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-10 h-10 bg-primary-600 rounded-lg flex flex-col items-center justify-center text-white text-xs font-bold">
|
||||
<span>{{ formatDay(termin.datum) }}</span>
|
||||
<span>{{ formatMonth(termin.datum) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900">{{ termin.titel }}</h3>
|
||||
<p class="text-sm text-gray-600">{{ termin.beschreibung }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span :class="[
|
||||
'px-2 py-1 text-xs font-medium rounded-full',
|
||||
termin.kategorie === 'Turnier' ? 'bg-yellow-100 text-yellow-800' : 'bg-blue-100 text-blue-800'
|
||||
]">
|
||||
{{ termin.kategorie }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center py-8 bg-gray-50 rounded-lg">
|
||||
<Calendar :size="32" class="text-gray-400 mx-auto mb-2" />
|
||||
<p class="text-gray-600 text-sm">Keine kommenden Termine</p>
|
||||
</div>
|
||||
|
||||
<div v-if="naechsteTermine.length > 0" class="text-center">
|
||||
<NuxtLink
|
||||
to="/termine"
|
||||
class="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white text-sm font-medium rounded-lg transition-colors"
|
||||
>
|
||||
Alle Termine anzeigen
|
||||
<ArrowRight :size="16" class="ml-1" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Calendar, ArrowRight } from 'lucide-vue-next'
|
||||
|
||||
const termine = ref([])
|
||||
|
||||
const naechsteTermine = computed(() => {
|
||||
const heute = new Date()
|
||||
console.log('Heute ist:', heute.toISOString().split('T')[0])
|
||||
|
||||
const kommende = termine.value
|
||||
.filter(t => {
|
||||
const terminDatum = new Date(t.datum)
|
||||
const istKommend = terminDatum >= heute
|
||||
console.log(`Termin ${t.titel} (${t.datum}): ${istKommend ? 'KOMMEND' : 'VERSTRICHEN'}`)
|
||||
return istKommend
|
||||
})
|
||||
.sort((a, b) => new Date(a.datum) - new Date(b.datum))
|
||||
|
||||
console.log('Kommende Termine:', kommende)
|
||||
return kommende
|
||||
})
|
||||
|
||||
const formatDay = (dateString) => {
|
||||
const date = new Date(dateString)
|
||||
return date.getDate()
|
||||
}
|
||||
|
||||
const formatMonth = (dateString) => {
|
||||
const date = new Date(dateString)
|
||||
const monate = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
|
||||
return monate[date.getMonth()]
|
||||
}
|
||||
|
||||
const loadTermine = async () => {
|
||||
try {
|
||||
console.log('Lade Termine...')
|
||||
const response = await fetch('/data/termine.csv')
|
||||
console.log('Response:', response)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const csv = await response.text()
|
||||
console.log('CSV Text:', csv)
|
||||
|
||||
// Vereinfachter CSV-Parser
|
||||
const lines = csv.split('\n').filter(line => line.trim() !== '')
|
||||
console.log('CSV Lines:', lines)
|
||||
|
||||
if (lines.length < 2) {
|
||||
console.log('Keine Datenzeilen gefunden')
|
||||
return
|
||||
}
|
||||
|
||||
termine.value = lines.slice(1).map((line, index) => {
|
||||
// Entferne Anführungszeichen und teile bei Kommas
|
||||
const cleanLine = line.replace(/"/g, '')
|
||||
const values = cleanLine.split(',')
|
||||
|
||||
if (values.length < 4) {
|
||||
console.log(`Zeile ${index + 2} hat zu wenige Werte:`, values)
|
||||
return null
|
||||
}
|
||||
|
||||
const termin = {
|
||||
datum: values[0].trim(),
|
||||
titel: values[1].trim(),
|
||||
beschreibung: values[2].trim(),
|
||||
kategorie: values[3].trim()
|
||||
}
|
||||
|
||||
console.log(`Termin ${index + 1}:`, termin)
|
||||
return termin
|
||||
}).filter(termin => termin !== null)
|
||||
|
||||
console.log('Alle geparsten Termine:', termine.value)
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Termine:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTermine()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user