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:
Torsten Schulz (local)
2025-10-21 00:41:12 +02:00
commit 737c3064bd
61 changed files with 25816 additions and 0 deletions

141
.gitignore vendored Normal file
View File

@@ -0,0 +1,141 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Temporary files
*.tmp
*.temp

94
README.md Normal file
View File

@@ -0,0 +1,94 @@
# Harheimer TC Website
Moderne Website für den Harheimer Tischtennis Club (HTC) in Frankfurt-Harheim.
## Technologie-Stack
- **Framework**: Vue 3 + Nuxt 3
- **Styling**: Tailwind CSS
- **Icons**: Lucide Vue Next
- **Build Tool**: Vite
- **Sprache**: JavaScript (ES6)
## Features
- 🏓 **Responsive Design** - Optimiert für alle Geräte
- 📱 **Mobile-First** - Perfekte Darstellung auf Smartphones
- 🎨 **Moderne UI** - Schwarze-rote Vereinsfarben
- 📸 **Dynamische Galerie** - Zeigt nur Bilder an, wenn vorhanden
- 📅 **Event-Management** - Termine aus CSV-Dateien
- 👥 **Mannschaftsübersicht** - Dynamische Team-Seiten
- 📋 **Kontaktformular** - SMTP-basierte E-Mail-Versendung
- 🗺️ **Kartenintegration** - Google Maps für Trainingsort
## Projektstruktur
```
harheimertc/
├── components/ # Vue-Komponenten
├── pages/ # Seiten-Routing
├── public/ # Statische Dateien
│ ├── data/ # CSV-Dateien (Termine, Mannschaften)
│ ├── documents/ # PDF-Dokumente
│ └── galerie/ # Galerie-Bilder
├── server/ # API-Endpunkte
└── assets/ # CSS und Bilder
```
## Installation
```bash
# Dependencies installieren
npm install
# Entwicklungsserver starten (Port 3100)
npm run dev
# Produktions-Build
npm run build
# Preview des Builds
npm run preview
```
## Konfiguration
### SMTP-Einstellungen
Für das Kontaktformular müssen folgende Umgebungsvariablen gesetzt werden:
```bash
SMTP_HOST=your-smtp-host
SMTP_PORT=587
SMTP_USER=your-email@domain.com
SMTP_PASS=your-password
SMTP_FROM=your-email@domain.com
SMTP_TO=club@harheimertc.de
```
### Datenverwaltung
- **Termine**: `public/data/termine.csv`
- **Mannschaften**: `public/data/mannschaften.csv`
- **Galerie**: Bilder in `public/galerie/` ablegen
## Entwicklung
### Lokale Entwicklung
```bash
npm run dev
```
Die Website ist dann unter `http://localhost:3100` erreichbar.
### Deployment
```bash
npm run build
npm run preview
```
## Lizenz
© 2025 Harheimer Tischtennis Club. Alle Rechte vorbehalten.

14
app.vue Normal file
View File

@@ -0,0 +1,14 @@
<template>
<div class="h-screen flex flex-col overflow-hidden">
<Navigation />
<main class="flex-1 overflow-y-auto pt-20">
<NuxtPage />
</main>
<Footer />
</div>
</template>
<script setup>
import Navigation from '~/components/Navigation.vue'
import Footer from '~/components/Footer.vue'
</script>

39
assets/css/main.css Normal file
View File

@@ -0,0 +1,39 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
font-family: 'Inter', system-ui, sans-serif;
scroll-behavior: smooth;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Montserrat', system-ui, sans-serif;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #dc2626;
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: #b91c1c;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.8 MiB

106
components/About.vue Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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>

View 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
View 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
View 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>

View 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>

16
env.example Normal file
View File

@@ -0,0 +1,16 @@
# SMTP-Konfiguration für E-Mail-Versand
# Diese Datei sollte in .env umbenannt werden und nicht in Git committet werden
# SMTP-Server (z.B. Gmail, GMX, etc.)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=j.dichmann@gmx.de
SMTP_PASS=your_email_password_here
# Alternative für GMX:
# SMTP_HOST=mail.gmx.net
# SMTP_PORT=587
# Alternative für andere Provider:
# SMTP_HOST=smtp.your-provider.com
# SMTP_PORT=587

5
next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

37
nuxt.config.js Normal file
View File

@@ -0,0 +1,37 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
modules: ['@nuxtjs/tailwindcss'],
app: {
head: {
title: 'Harheimer TC - Tischtennis in Frankfurt-Harheim',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
name: 'description',
content: 'Willkommen beim Harheimer Tischtennis Club - Ihr Tischtennisverein in Frankfurt-Harheim. Mitglied werden, Training buchen, Turniere und mehr.'
},
{
name: 'keywords',
content: 'Tischtennis, Tischtennisclub, Frankfurt, Harheim, Sport, Verein, Mitgliedschaft, Pingpong'
}
],
link: [
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' },
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@700;800;900&display=swap'
}
]
}
},
css: ['~/assets/css/main.css'],
compatibilityDate: '2024-04-03'
})

11849
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "harheimertc-website",
"version": "1.0.0",
"description": "Moderne Webseite für den Harheimer Tischtennis Club",
"private": true,
"type": "module",
"scripts": {
"dev": "nuxt dev --port 3100",
"build": "nuxt build",
"generate": "nuxt generate",
"preview": "nuxt preview --port 3100",
"postinstall": "nuxt prepare"
},
"dependencies": {
"nodemailer": "^7.0.9",
"nuxt": "^3.11.0",
"vue": "^3.4.0"
},
"devDependencies": {
"@nuxtjs/tailwindcss": "^6.11.0",
"autoprefixer": "^10.4.0",
"lucide-vue-next": "^0.344.0",
"postcss": "^8.4.0",
"tailwindcss": "^3.4.0"
}
}

14
pages/anlagen.vue Normal file
View File

@@ -0,0 +1,14 @@
<template>
<div class="min-h-screen">
<Facilities />
</div>
</template>
<script setup>
import Facilities from '~/components/Facilities.vue'
useHead({
title: 'Anlagen - Harheimer TC',
})
</script>

14
pages/galerie.vue Normal file
View File

@@ -0,0 +1,14 @@
<template>
<div class="min-h-screen">
<Gallery />
</div>
</template>
<script setup>
import Gallery from '~/components/Gallery.vue'
useHead({
title: 'Galerie - Harheimer TC',
})
</script>

96
pages/geschichte.vue Normal file
View File

@@ -0,0 +1,96 @@
<template>
<div class="min-h-full py-16 bg-gray-50">
<div class="max-w-7xl 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">
Vereinsgeschichte
</h1>
<div class="w-24 h-1 bg-primary-600 mb-8" />
<div class="prose prose-lg max-w-none">
<p class="text-xl text-gray-600 mb-8">
Die bewegte Geschichte des Harheimer Tischtennis Clubs seit 1954.
</p>
<div class="space-y-6 mb-8">
<p class="text-lg text-gray-700">
Nach dem zweiten Weltkrieg entwickelte sich sprunghaft der Tischtennissport in der Bundesrepublik.
Auch in der damaligen Gemeinde Harheim gab es junge Menschen, die an diesem neuen Sport Gefallen fanden,
so dass am <strong>10.05.1950</strong> durch deren Initiative eine Tischtennisabteilung innerhalb der
Sportgemeinschaft Harheim (SGH) gegründet wurde.
</p>
<p class="text-lg text-gray-700">
Zu Anfang waren es nur wenige TT-Begeisterte und nur durch deren Idealismus, Opfer und Gemeinschaftssinn
wurden die Anfangsschwierigkeiten überwunden. Im Laufe der Zeit kamen auch die Kritiker innerhalb der SGH
nicht umhin, die damaligen Tischtennisspieler mit ihrer neuen Sportart anzuerkennen.
</p>
</div>
<div class="space-y-6">
<div class="bg-white p-6 rounded-xl shadow-lg border-l-4 border-primary-600">
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">10.06.1954 - Gründung des HTC</h3>
<p class="text-gray-600 mb-3">
Bei der am 20.05.1954 stattgefundenen Sitzung der SGH wurde die Trennung der einzelnen Abteilungen beschlossen.
Somit sah sich die TT-Abteilung veranlasst, ihren Sportbetrieb in eigener Regie weiterzuführen.
</p>
<p class="text-gray-600">
Am <strong>10.06.1954</strong> trafen sich 6 Damen und 22 Herren zur Gründungsversammlung in der Gaststätte Zum Löwen".
Der neu gegründete Verein wurde unter dem Namen "Harheimer Tischtennis-Club" Mitglied des Landessportbundes Hessen.
</p>
</div>
<div class="bg-white p-6 rounded-xl shadow-lg border-l-4 border-primary-600">
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">1964 - Neue Trainingsstätte</h3>
<p class="text-gray-600">
Mit der Erbauung der Schulturnhalle im Jahre 1964 stand eine für die damaligen Verhältnisse recht moderne
Übungsstätte zur Verfügung, die dem HTC für einen Tag in der Woche überlassen wurde. Damit waren viele
Probleme gelöst und es gab einen Aufschwung, der sich in einer steigenden Spielerzahl bemerkbar machte.
</p>
</div>
<div class="bg-white p-6 rounded-xl shadow-lg border-l-4 border-primary-600">
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">1974 - Bürgerhaus</h3>
<p class="text-gray-600">
Mit der Erstellung des Bürgerhauses wurde wiederum neuer Trainingsraum geschaffen, der besonders für den
Tischtennissport geeignet ist. Der HTC nahm die Gelegenheit war und hielt ab Mai 1974 seine Übungsabende
im großen Saal des Bürgerhauses ab.
</p>
</div>
<div class="bg-white p-6 rounded-xl shadow-lg border-l-4 border-primary-600">
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">1976 - Eintragung ins Vereinsregister</h3>
<p class="text-gray-600">
Die Eintragung in das Vereinsregister (e. V.) erfolgte im Jahre 1976 und gleichzeitig wurde dem Verein
die Gemeinnützigkeit zuerkannt.
</p>
</div>
<div class="bg-white p-6 rounded-xl shadow-lg border-l-4 border-primary-600">
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">1978/79 - Sportlicher Höhepunkt</h3>
<p class="text-gray-600">
Ein besonderes Geschenk machten die Spieler des HTC im Jubiläumsjahr ihrem Verein: Die 1. Herrenmannschaft
wurde Meister der Bezirksklasse Ffm.-Ost und die 2. Herrenmannschaft Meister der Kreisklasse-A Ffm.-Nord.
Nachdem auch die Schülermannschaft Meister ihrer Klasse wurde, ist die Saison 78/79 als absolut sportlicher
Höhepunkt in der Vereinsgeschichte zu werten.
</p>
</div>
<div class="bg-white p-6 rounded-xl shadow-lg border-l-4 border-primary-600">
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">Heute</h3>
<p class="text-gray-600">
Der HTC hat sich auch in Zukunft zur Aufgabe gemacht, allen interessierten Bürgern und Jugendlichen im
Rahmen seiner Möglichkeiten das Tischtennisspielen als Leistungssport oder zur Freizeitgestaltung zu ermöglichen.
</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
useHead({
title: 'Geschichte - Harheimer TC',
})
</script>

124
pages/impressum.vue Normal file
View File

@@ -0,0 +1,124 @@
<template>
<div class="min-h-full py-16 px-4 sm:px-6 lg:px-8 bg-gray-50">
<div class="max-w-4xl mx-auto">
<h1 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-6">
Impressum
</h1>
<div class="w-24 h-1 bg-primary-600 mb-8" />
<div class="bg-white p-8 rounded-xl shadow-lg space-y-6">
<div>
<h2 class="text-xl font-display font-bold text-gray-900 mb-2">Angaben gemäß § 5 TMG</h2>
<p class="text-gray-700">
Harheimer Tischtennis-Club 1954 e. V. (HTC)<br />
In der Fuchskaut 4<br />
60437 Frankfurt am Main
</p>
</div>
<div>
<h2 class="text-xl font-display font-bold text-gray-900 mb-2">Kontakt</h2>
<p class="text-gray-700">
Telefon: 06101-4992227<br />
E-Mail: j.dichmann@gmx.de<br />
Internet: www.harheimertc.de
</p>
</div>
<div>
<h2 class="text-xl font-display font-bold text-gray-900 mb-2">Vertretungsberechtigter Vorstand</h2>
<p class="text-gray-700">
Roger Dichmann, Vorsitzender<br />
Jürgen Kratz, Stellvertreter des Vorsitzenden<br />
Olaf Nüßlein, Kassenwart<br />
Jürgen Dichmann, Schriftführer
</p>
</div>
<div>
<h2 class="text-xl font-display font-bold text-gray-900 mb-2">Registereintrag</h2>
<p class="text-gray-700">
lsb h-Vereinsnummer: 24091<br />
Registereintrag: Amtsgericht Frankfurt am Main, Registergericht<br />
Registernummer: VR 6835
</p>
</div>
<div>
<h2 class="text-xl font-display font-bold text-gray-900 mb-2">Vereinsatzung</h2>
<p class="text-gray-700 mb-4">
Unsere aktuelle Vereinsatzung können Sie hier herunterladen oder online einsehen:
</p>
<div class="flex flex-col sm:flex-row gap-3">
<a
href="/documents/satzung.pdf"
target="_blank"
class="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg transition-colors"
>
<FileText :size="16" class="mr-2" />
Satzung herunterladen (PDF)
</a>
<NuxtLink
to="/satzung"
class="inline-flex items-center px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-900 font-medium rounded-lg transition-colors"
>
<Eye :size="16" class="mr-2" />
Online ansehen
</NuxtLink>
</div>
</div>
<div>
<h2 class="text-xl font-display font-bold text-gray-900 mb-2">Verantwortlich für den Inhalt</h2>
<p class="text-gray-700">
Roger Dichmann<br />
Reginastr. 46<br />
60437 Frankfurt
</p>
</div>
<div>
<h2 class="text-xl font-display font-bold text-gray-900 mb-2">Haftungsausschluss</h2>
<h3 class="font-semibold text-gray-900 mt-4 mb-2">Haftung für Inhalte</h3>
<p class="text-gray-700 mb-4">
Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen. Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen. Grundsätzlich sind alle unsere Informationen ohne Gewähr. Auch für den Fall das unzutreffende oder falsche Informationen enthalten sind, wird vom HTC jegliche Haftung ausgeschlossen.
</p>
<h3 class="font-semibold text-gray-900 mt-4 mb-2">Haftung für Links</h3>
<p class="text-gray-700 mb-4">
Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar. Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Eine Haftung für Schäden, die ggf. durch das Aufrufen dieser Seiten, bzw. deren Inhalte entstehen, wird vom HTC nicht übernommen. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.
</p>
<h3 class="font-semibold text-gray-900 mt-4 mb-2">Urheberrecht</h3>
<p class="text-gray-700 mb-4">
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers. Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet. Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.
</p>
</div>
<div>
<h2 class="text-xl font-display font-bold text-gray-900 mb-2">Datenschutzerklärung</h2>
<h3 class="font-semibold text-gray-900 mt-4 mb-2">Datenschutz</h3>
<p class="text-gray-700 mb-4">
Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend der gesetzlichen Datenschutzvorschriften sowie dieser Datenschutzerklärung. Die Nutzung unserer Website ist in der Regel ohne Angabe personenbezogener Daten möglich. Soweit auf unseren Seiten personenbezogene Daten (beispielsweise Name, Anschrift oder E-Mail-Adressen) erhoben werden, erfolgt dies, soweit möglich, stets auf freiwilliger Basis. Diese Daten werden ohne Ihre ausdrückliche Zustimmung nicht an Dritte weitergegeben. Wir weisen darauf hin, dass die Datenübertragung im Internet (z.B. bei der Kommunikation per E-Mail) Sicherheitslücken aufweisen kann. Ein lückenloser Schutz der Daten vor dem Zugriff durch Dritte ist nicht möglich.
</p>
<h3 class="font-semibold text-gray-900 mt-4 mb-2">Widerspruch Werbe-Mails</h3>
<p class="text-gray-700">
Der Nutzung von im Rahmen der Impressumspflicht veröffentlichten Kontaktdaten zur Übersendung von nicht ausdrücklich angeforderter Werbung und Informationsmaterialien wird hiermit widersprochen. Die Betreiber der Seiten behalten sich ausdrücklich rechtliche Schritte im Falle der unverlangten Zusendung von Werbeinformationen, etwa durch Spam-E-Mails, vor.
</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { FileText, Eye } from 'lucide-vue-next'
useHead({
title: 'Impressum - Harheimer TC',
})
</script>

9
pages/index.vue Normal file
View File

@@ -0,0 +1,9 @@
<template>
<div class="min-h-full">
<Hero />
</div>
</template>
<script setup>
import Hero from '~/components/Hero.vue'
</script>

14
pages/kontakt.vue Normal file
View File

@@ -0,0 +1,14 @@
<template>
<div class="min-h-screen">
<Contact />
</div>
</template>
<script setup>
import Contact from '~/components/Contact.vue'
useHead({
title: 'Kontakt - Harheimer TC',
})
</script>

View File

@@ -0,0 +1,209 @@
<template>
<div class="min-h-full py-16 bg-gray-50">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div v-if="mannschaft" class="space-y-8">
<!-- Header -->
<div class="bg-gradient-to-r from-primary-600 to-primary-700 rounded-xl p-8 text-white">
<h1 class="text-4xl font-display font-bold mb-2">
{{ mannschaft.mannschaft }}
</h1>
<p class="text-primary-100 text-xl">{{ mannschaft.liga }}</p>
</div>
<!-- Liga-Info -->
<div class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-semibold text-gray-900 mb-6">Liga-Informationen</h2>
<div class="grid md:grid-cols-2 gap-6">
<div class="space-y-4">
<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-4">
<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>
</div>
<!-- Mannschaftsaufstellung -->
<div class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-semibold text-gray-900 mb-6">
Mannschaftsaufstellung Saison 2025/26 (Hinrunde)
</h2>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div
v-for="(spieler, index) in getSpielerListe(mannschaft)"
:key="index"
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="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-semibold text-gray-900 mb-6">Weitere Informationen</h2>
<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-8 py-4 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
>
<BarChart :size="24" class="mr-3" />
Weitere Informationen
</a>
</div>
</div>
<!-- Letzte Aktualisierung -->
<div class="bg-white rounded-xl shadow-lg p-6">
<p class="text-sm text-gray-500 text-center">
Zuletzt aktualisiert am: {{ formatDate(mannschaft.letzte_aktualisierung) }}
</p>
</div>
<!-- Zurück-Button -->
<div class="text-center">
<NuxtLink
to="/mannschaften"
class="inline-flex items-center px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
>
Zurück zur Übersicht
</NuxtLink>
</div>
</div>
<div v-else class="text-center py-16">
<h1 class="text-4xl font-display font-bold text-gray-900 mb-4">Mannschaft nicht gefunden</h1>
<p class="text-gray-600 mb-8">Die angeforderte Mannschaft konnte nicht gefunden werden.</p>
<NuxtLink
to="/mannschaften"
class="inline-flex items-center px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
>
Zur Mannschaftsübersicht
</NuxtLink>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Calendar, Users, BarChart } from 'lucide-vue-next'
const route = useRoute()
const mannschaft = ref(null)
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
const mannschaften = 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(),
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].trim(),
slug: values[0].trim().toLowerCase().replace(/\s+/g, '-')
}
}).filter(mannschaft => mannschaft !== null)
// Finde die Mannschaft basierend auf dem Slug
const currentSlug = route.params.slug
mannschaft.value = mannschaften.find(m => m.slug === currentSlug) || null
if (mannschaft.value) {
useHead({
title: `${mannschaft.value.mannschaft} - Harheimer TC`,
})
}
} 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>

View File

@@ -0,0 +1,36 @@
<template>
<div class="min-h-full py-16 bg-gray-50">
<div class="max-w-7xl 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">
Damenmannschaft
</h1>
<div class="w-24 h-1 bg-primary-600 mb-8" />
<div class="bg-white p-8 rounded-xl shadow-lg">
<h3 class="text-2xl font-display font-bold text-gray-900 mb-4">1. Damen</h3>
<p class="text-gray-600 mb-4">Liga: Bezirksliga</p>
<p class="text-gray-600 mb-6">Mannschaftsführerin: Name folgt</p>
<div class="mt-8">
<h4 class="text-lg font-semibold text-gray-900 mb-4">Wir suchen Verstärkung!</h4>
<p class="text-gray-600 mb-4">
Unsere Damenmannschaft freut sich über neue Spielerinnen. Interessiert? Dann melde dich bei uns!
</p>
<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>
</div>
</template>
<script setup>
useHead({
title: 'Damenmannschaft - Harheimer TC',
})
</script>

View File

@@ -0,0 +1,37 @@
<template>
<div class="min-h-full py-16 bg-gray-50">
<div class="max-w-7xl 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">
Herrenmannschaften
</h1>
<div class="w-24 h-1 bg-primary-600 mb-8" />
<div class="space-y-8">
<div class="bg-white p-8 rounded-xl shadow-lg">
<h3 class="text-2xl font-display font-bold text-gray-900 mb-4">1. Herren</h3>
<p class="text-gray-600 mb-4">Liga: Bezirksoberliga</p>
<p class="text-gray-600">Mannschaftsführer: Name folgt</p>
</div>
<div class="bg-white p-8 rounded-xl shadow-lg">
<h3 class="text-2xl font-display font-bold text-gray-900 mb-4">2. Herren</h3>
<p class="text-gray-600 mb-4">Liga: Bezirksliga</p>
<p class="text-gray-600">Mannschaftsführer: Name folgt</p>
</div>
<div class="bg-white p-8 rounded-xl shadow-lg">
<h3 class="text-2xl font-display font-bold text-gray-900 mb-4">3. Herren</h3>
<p class="text-gray-600 mb-4">Liga: Kreisliga</p>
<p class="text-gray-600">Mannschaftsführer: Name folgt</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
useHead({
title: 'Herrenmannschaften - Harheimer TC',
})
</script>

View File

@@ -0,0 +1,93 @@
<template>
<div class="min-h-full py-16 bg-gray-50">
<div class="max-w-7xl 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">
Unsere Mannschaften
</h1>
<div class="w-24 h-1 bg-primary-600 mb-8" />
<p class="text-xl text-gray-600 mb-12">
Unsere aktiven Mannschaften in der Saison 2025/26
</p>
<MannschaftenUebersicht />
<div class="mt-16">
<h2 class="text-3xl font-display font-bold text-gray-900 mb-8 text-center">
Weitere Informationen
</h2>
<div class="grid md:grid-cols-3 gap-8">
<NuxtLink
to="/mannschaften/herren"
class="group bg-white p-8 rounded-xl shadow-lg hover:shadow-2xl transition-all border border-gray-100 hover:border-primary-600"
>
<div class="w-16 h-16 bg-gradient-to-br from-primary-500 to-primary-700 rounded-xl flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
<Users :size="32" class="text-white" />
</div>
<h3 class="text-2xl font-display font-bold text-gray-900 mb-2 group-hover:text-primary-600 transition-colors">
Herren
</h3>
<p class="text-gray-600">
3 Mannschaften in verschiedenen Ligen
</p>
</NuxtLink>
<NuxtLink
to="/mannschaften/damen"
class="group bg-white p-8 rounded-xl shadow-lg hover:shadow-2xl transition-all border border-gray-100 hover:border-primary-600"
>
<div class="w-16 h-16 bg-gradient-to-br from-primary-500 to-primary-700 rounded-xl flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
<Users :size="32" class="text-white" />
</div>
<h3 class="text-2xl font-display font-bold text-gray-900 mb-2 group-hover:text-primary-600 transition-colors">
Damen
</h3>
<p class="text-gray-600">
1 Mannschaft in der Bezirksliga
</p>
</NuxtLink>
<NuxtLink
to="/mannschaften/jugend"
class="group bg-white p-8 rounded-xl shadow-lg hover:shadow-2xl transition-all border border-gray-100 hover:border-primary-600"
>
<div class="w-16 h-16 bg-gradient-to-br from-primary-500 to-primary-700 rounded-xl flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
<Users :size="32" class="text-white" />
</div>
<h3 class="text-2xl font-display font-bold text-gray-900 mb-2 group-hover:text-primary-600 transition-colors">
Jugend
</h3>
<p class="text-gray-600">
2 Jugendmannschaften
</p>
</NuxtLink>
</div>
<div class="mt-12 bg-primary-50 p-8 rounded-xl border border-primary-100">
<h3 class="text-2xl font-display font-bold text-gray-900 mb-4">
Spielpläne & Ergebnisse
</h3>
<p class="text-gray-600 mb-6">
Alle aktuellen Spielpläne und Ergebnisse unserer Mannschaften finden Sie hier.
</p>
<NuxtLink
to="/mannschaften/spielplaene"
class="inline-flex items-center px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
>
Zu den Spielplänen
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { Users } from 'lucide-vue-next'
import MannschaftenUebersicht from '~/components/MannschaftenUebersicht.vue'
useHead({
title: 'Mannschaften - Harheimer TC',
})
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div class="min-h-full py-16 bg-gray-50">
<div class="max-w-7xl 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">
Jugendmannschaften
</h1>
<div class="w-24 h-1 bg-primary-600 mb-8" />
<div class="space-y-8">
<div class="bg-white p-8 rounded-xl shadow-lg">
<h3 class="text-2xl font-display font-bold text-gray-900 mb-4">Jugend 1 (U18)</h3>
<p class="text-gray-600 mb-4">Liga: Bezirksliga</p>
<p class="text-gray-600">Betreuer: Name folgt</p>
</div>
<div class="bg-white p-8 rounded-xl shadow-lg">
<h3 class="text-2xl font-display font-bold text-gray-900 mb-4">Jugend 2 (U15)</h3>
<p class="text-gray-600 mb-4">Liga: Kreisliga</p>
<p class="text-gray-600">Betreuer: Name folgt</p>
</div>
<div class="bg-primary-50 p-8 rounded-xl border border-primary-100">
<h3 class="text-2xl font-display font-bold text-gray-900 mb-4">
Jugendtraining
</h3>
<p class="text-gray-600 mb-6">
<strong>Dienstag & Donnerstag:</strong> 17:00 - 19:00 Uhr<br />
Für Kinder und Jugendliche von 8-18 Jahren
</p>
<NuxtLink
to="/training"
class="inline-flex items-center px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
>
Mehr zum Training
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup>
useHead({
title: 'Jugendmannschaften - Harheimer TC',
})
</script>

View File

@@ -0,0 +1,268 @@
<template>
<div class="min-h-full py-16 bg-gray-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<h1 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-4">
Spielpläne
</h1>
<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">
Aktuelle Spielpläne der Saison {{ aktuellesSaisonLabel }}
</p>
</div>
<!-- Spielpläne -->
<div v-if="spielplaene.length > 0" class="space-y-4 max-w-4xl mx-auto">
<div
v-for="(plan, index) in spielplaene"
:key="index"
class="bg-white rounded-xl shadow-lg border border-gray-100 p-6 hover:shadow-xl transition-shadow"
>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center">
<FileText :size="24" class="text-primary-600" />
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900">{{ plan.titel }}</h3>
<p class="text-sm text-gray-500">Saison {{ plan.saison }}</p>
</div>
</div>
<a
:href="plan.url"
download
class="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg transition-colors"
>
<Download :size="18" class="mr-2" />
Download
</a>
</div>
</div>
</div>
<!-- Keine Spielpläne -->
<div v-else class="text-center py-16 bg-white rounded-xl shadow-lg max-w-4xl mx-auto">
<FileText :size="48" class="text-gray-400 mx-auto mb-4" />
<h3 class="text-xl font-semibold text-gray-900 mb-2">Keine Spielpläne verfügbar</h3>
<p class="text-gray-600">
Für die aktuelle Saison {{ aktuellesSaisonLabel }} sind noch keine Spielpläne verfügbar.
</p>
</div>
<!-- Online Spielpläne und Tabellen -->
<div class="mt-12 max-w-4xl mx-auto">
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6 text-center">
Online Spielpläne & Tabellen
</h2>
<div v-if="mannschaftenMitLinks.length > 0" class="space-y-3">
<div
v-for="(mannschaft, index) in mannschaftenMitLinks"
:key="index"
class="bg-white rounded-lg shadow border border-gray-100 p-4 hover:shadow-md transition-shadow"
>
<div class="flex items-center justify-between">
<div>
<h3 class="font-semibold text-gray-900">{{ mannschaft.mannschaft }}</h3>
<p class="text-sm text-gray-500">{{ mannschaft.liga }}</p>
</div>
<a
:href="mannschaft.weitere_informationen_link"
target="_blank"
class="inline-flex items-center px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors text-sm"
>
<ExternalLink :size="16" class="mr-2" />
Online ansehen
</a>
</div>
</div>
</div>
</div>
<!-- Info-Box -->
<div class="mt-12 max-w-4xl mx-auto bg-primary-50 border border-primary-100 rounded-xl p-6">
<h3 class="text-lg font-semibold text-primary-900 mb-2">
Hinweis
</h3>
<p class="text-primary-800">
Die Spielpläne werden automatisch für die aktuelle Saison angezeigt.
Ältere Spielpläne können auf Anfrage bereitgestellt werden.
</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { FileText, Download, ExternalLink } from 'lucide-vue-next'
const spielplaene = ref([])
const mannschaftenMitLinks = ref([])
// Berechne die aktuelle Saison
const aktuellesSaison = computed(() => {
const jetzt = new Date()
const monat = jetzt.getMonth() + 1 // 1-12
const jahr = jetzt.getFullYear()
// Saison wechselt im Juli/August
if (monat >= 7) {
return { start: jahr, ende: jahr + 1 }
} else {
return { start: jahr - 1, ende: jahr }
}
})
const aktuellesSaisonLabel = computed(() => {
return `${aktuellesSaison.value.start}/${aktuellesSaison.value.ende}`
})
// Funktion zum Extrahieren der Saison aus dem Dateinamen
const extractSaison = (filename) => {
console.log('extractSaison für:', filename)
// Normalisiere alle möglichen Trennzeichen zu einem einzigen Zeichen
// Suche nach 4 Ziffern, gefolgt von irgendeinem Nicht-Ziffer-Zeichen, gefolgt von 4 Ziffern
let match = filename.match(/(\d{4})[^0-9](\d{4})/)
if (match) {
const start = parseInt(match[1])
const ende = parseInt(match[2])
console.log(' Gefunden (4-stellig):', start, ende)
return { start, ende, label: `${start}/${ende}` }
}
// Suche nach 2 Ziffern, gefolgt von irgendeinem Nicht-Ziffer-Zeichen, gefolgt von 2 Ziffern
match = filename.match(/(\d{2})[^0-9](\d{2})/)
if (match) {
let start = parseInt(match[1])
let ende = parseInt(match[2])
// Wenn Kurzform (25-26), zu Langform konvertieren
if (start < 100) {
start = 2000 + start
ende = 2000 + ende
}
console.log(' Gefunden (2-stellig):', start, ende)
return { start, ende, label: `${start}/${ende}` }
}
console.log(' Keine Saison gefunden')
return null
}
// Prüfe, ob eine Saison zur aktuellen Saison passt
const istAktuellesSaison = (saison) => {
if (!saison) return false
return saison.start === aktuellesSaison.value.start &&
saison.ende === aktuellesSaison.value.ende
}
// Lade Spielpläne
const loadSpielplaene = async () => {
try {
console.log('=== SPIELPLÄNE LADEN ===')
console.log('Aktuelle Saison:', aktuellesSaison.value)
console.log('Saison Label:', aktuellesSaisonLabel.value)
// Lade Dateien vom Server
const response = await fetch('/api/spielplaene')
if (!response.ok) {
console.error('Fehler beim Laden der Spielpläne:', response.status)
return
}
const dateien = await response.json()
console.log('Geladene Dateien:', dateien)
const gefiltert = dateien
.map(filename => {
console.log('Verarbeite Datei:', filename)
const saison = extractSaison(filename)
console.log(' Extrahierte Saison:', saison)
console.log(' Ist aktuelle Saison?', saison ? istAktuellesSaison(saison) : false)
if (!saison || !istAktuellesSaison(saison)) {
return null
}
// Extrahiere Titel aus Dateiname
const titel = filename
.replace(/\.(pdf|PDF|xlsx|XLSX|xls|XLS)$/, '')
.replace(/[-_]/g, ' ')
.replace(/\d{2,4}[-_\/]\d{2,4}/, '')
.trim()
return {
filename,
titel: titel || filename,
saison: saison.label,
url: `/spielplaene/${filename}`
}
})
.filter(item => item !== null)
spielplaene.value = gefiltert
console.log('Aktuelle Saison:', aktuellesSaisonLabel.value)
console.log('Gefundene Spielpläne:', spielplaene.value)
} catch (error) {
console.error('Fehler beim Laden der Spielpläne:', error)
}
}
// Lade Mannschaften aus CSV
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
mannschaftenMitLinks.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(),
liga: values[1].trim(),
weitere_informationen_link: values[8].trim()
}
}).filter(mannschaft => mannschaft !== null && mannschaft.weitere_informationen_link !== '')
console.log('Mannschaften mit Links:', mannschaftenMitLinks.value)
} catch (error) {
console.error('Fehler beim Laden der Mannschaften:', error)
}
}
onMounted(() => {
loadSpielplaene()
loadMannschaften()
})
useHead({
title: 'Spielpläne - Harheimer TC',
})
</script>

14
pages/mitgliedschaft.vue Normal file
View File

@@ -0,0 +1,14 @@
<template>
<div class="min-h-screen">
<Membership />
</div>
</template>
<script setup>
import Membership from '~/components/Membership.vue'
useHead({
title: 'Mitgliedschaft - Harheimer TC',
})
</script>

149
pages/satzung.vue Normal file
View File

@@ -0,0 +1,149 @@
<template>
<div class="min-h-full py-16 bg-gray-50">
<div class="max-w-4xl 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">
Vereinssatzung
</h1>
<div class="w-24 h-1 bg-primary-600 mb-8" />
<div class="bg-white p-8 rounded-xl shadow-lg">
<p class="text-lg text-gray-600 mb-8">
Die Satzung des Harheimer Tischtennis Clubs regelt die Grundlagen unseres Vereins.
</p>
<div class="prose prose-lg max-w-none">
<div class="space-y-8">
<div>
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">§ 1 Name, Sitz und Geschäftsjahr</h3>
<div class="space-y-2 text-gray-700">
<p><strong>(1)</strong> Der Verein führt den Namen "Harheimer Tischtennis-Club 1954 e.V." (HTC).</p>
<p><strong>(2)</strong> Der Verein hat seinen Sitz in Frankfurt am Main.</p>
<p><strong>(3)</strong> Das Geschäftsjahr ist das Kalenderjahr.</p>
</div>
</div>
<div>
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">§ 2 Zweck des Vereins</h3>
<div class="space-y-2 text-gray-700">
<p><strong>(1)</strong> Der Verein bezweckt die Förderung des Tischtennissports und die Pflege der Geselligkeit seiner Mitglieder.</p>
<p><strong>(2)</strong> Der Verein ist selbstlos tätig; er verfolgt nicht in erster Linie eigenwirtschaftliche Zwecke.</p>
</div>
</div>
<div>
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">§ 3 Mitgliedschaft</h3>
<div class="space-y-2 text-gray-700">
<p><strong>(1)</strong> Mitglied des Vereins kann jede natürliche Person werden, die die Ziele des Vereins unterstützt.</p>
<p><strong>(2)</strong> Der Antrag auf Mitgliedschaft ist schriftlich an den Vorstand zu richten.</p>
<p><strong>(3)</strong> Über die Aufnahme entscheidet der Vorstand.</p>
</div>
</div>
<div>
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">§ 4 Rechte und Pflichten der Mitglieder</h3>
<div class="space-y-2 text-gray-700">
<p><strong>(1)</strong> Die Mitglieder haben das Recht, an den Veranstaltungen des Vereins teilzunehmen und die Einrichtungen des Vereins zu benutzen.</p>
<p><strong>(2)</strong> Die Mitglieder sind verpflichtet, die Satzung und die Beschlüsse der Vereinsorgane zu beachten und den Mitgliedsbeitrag zu entrichten.</p>
</div>
</div>
<div>
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">§ 5 Mitgliedsbeiträge</h3>
<div class="space-y-2 text-gray-700">
<p><strong>(1)</strong> Die Höhe der Mitgliedsbeiträge wird von der Mitgliederversammlung festgesetzt.</p>
<p><strong>(2)</strong> Die Mitgliedsbeiträge sind im Voraus zu entrichten.</p>
</div>
</div>
<div>
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">§ 6 Beendigung der Mitgliedschaft</h3>
<div class="space-y-2 text-gray-700">
<p><strong>(1)</strong> Die Mitgliedschaft endet durch Austritt, Ausschluss oder Tod.</p>
<p><strong>(2)</strong> Der Austritt erfolgt durch schriftliche Erklärung gegenüber dem Vorstand.</p>
<p><strong>(3)</strong> Ein Mitglied kann aus wichtigem Grund ausgeschlossen werden.</p>
</div>
</div>
<div>
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">§ 7 Organe des Vereins</h3>
<div class="space-y-2 text-gray-700">
<p>Organe des Vereins sind:</p>
<ul class="list-disc list-inside ml-4 space-y-1">
<li>die Mitgliederversammlung</li>
<li>der Vorstand</li>
</ul>
</div>
</div>
<div>
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">§ 8 Mitgliederversammlung</h3>
<div class="space-y-2 text-gray-700">
<p><strong>(1)</strong> Die Mitgliederversammlung ist das oberste Organ des Vereins.</p>
<p><strong>(2)</strong> Sie wird vom Vorsitzenden mindestens einmal im Jahr einberufen.</p>
<p><strong>(3)</strong> Die Mitgliederversammlung beschließt über alle wichtigen Angelegenheiten des Vereins.</p>
</div>
</div>
<div>
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">§ 9 Vorstand</h3>
<div class="space-y-2 text-gray-700">
<p><strong>(1)</strong> Der Vorstand besteht aus:</p>
<ul class="list-disc list-inside ml-4 space-y-1">
<li>dem Vorsitzenden</li>
<li>dem stellvertretenden Vorsitzenden</li>
<li>dem Kassenwart</li>
<li>dem Schriftführer</li>
</ul>
<p><strong>(2)</strong> Der Vorstand wird von der Mitgliederversammlung gewählt.</p>
<p><strong>(3)</strong> Der Vorstand führt die Geschäfte des Vereins.</p>
</div>
</div>
<div>
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">§ 10 Satzungsänderungen</h3>
<div class="space-y-2 text-gray-700">
<p>Satzungsänderungen können nur in einer Mitgliederversammlung mit einer Mehrheit von zwei Dritteln der anwesenden Mitglieder beschlossen werden.</p>
</div>
</div>
<div>
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">§ 11 Auflösung des Vereins</h3>
<div class="space-y-2 text-gray-700">
<p><strong>(1)</strong> Die Auflösung des Vereins kann nur in einer Mitgliederversammlung mit einer Mehrheit von drei Vierteln der anwesenden Mitglieder beschlossen werden.</p>
<p><strong>(2)</strong> Bei Auflösung des Vereins fällt das Vereinsvermögen an eine gemeinnützige Organisation.</p>
</div>
</div>
</div>
<div class="mt-12 p-6 bg-primary-50 rounded-lg border border-primary-200">
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
<div>
<h4 class="text-lg font-semibold text-primary-800 mb-2">Satzung als PDF herunterladen</h4>
<p class="text-primary-700 text-sm">
Laden Sie die vollständige Satzung als PDF-Dokument herunter.
</p>
</div>
<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" />
PDF herunterladen
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { FileText } from 'lucide-vue-next'
useHead({
title: 'Satzung - Harheimer TC',
})
</script>

214
pages/spielsysteme.vue Normal file
View File

@@ -0,0 +1,214 @@
<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">
Spielsysteme
</h1>
<div class="w-24 h-1 bg-primary-600 mb-8" />
<p class="text-xl text-gray-600 mb-12">
Übersicht der verschiedenen Mannschafts-Spielsysteme im Tischtennis
</p>
<!-- Filter -->
<div class="mb-8 flex flex-wrap gap-4">
<button
v-for="kategorie in verfuegbareKategorien"
:key="kategorie"
@click="selectedCategory = kategorie"
:class="[
'px-4 py-2 rounded-lg font-medium transition-colors',
selectedCategory === kategorie
? 'bg-primary-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300'
]"
>
{{ kategorie }}
</button>
<button
@click="selectedCategory = 'alle'"
:class="[
'px-4 py-2 rounded-lg font-medium transition-colors',
selectedCategory === 'alle'
? 'bg-primary-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300'
]"
>
Alle Kategorien
</button>
</div>
<!-- Spielsysteme -->
<div v-if="filteredSystems.length > 0" class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="system in filteredSystems"
:key="system.name"
class="bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow border border-gray-100"
>
<div class="flex items-start justify-between mb-4">
<div class="flex-1">
<h3 class="text-xl font-display font-bold text-gray-900 mb-2">
{{ system.name }}
</h3>
<div class="flex items-center mb-3">
<Users :size="16" class="text-primary-600 mr-2" />
<span class="text-sm font-medium text-gray-600">{{ system.mannschaftsgroesse }}</span>
</div>
</div>
<div
:class="[
'px-3 py-1 rounded-full text-xs font-medium',
getCategoryColor(system.kategorie)
]"
>
{{ system.kategorie }}
</div>
</div>
<p class="text-gray-700 mb-4 leading-relaxed">
{{ system.description }}
</p>
<div class="space-y-2 text-sm">
<div v-if="system.spielabfolge" class="flex items-center">
<Calendar :size="14" class="text-primary-600 mr-2 flex-shrink-0" />
<span class="text-gray-600"><strong>Spielabfolge:</strong> {{ system.spielabfolge }}</span>
</div>
<div v-if="system.anzahl_spiele" class="flex items-center">
<Hash :size="14" class="text-primary-600 mr-2 flex-shrink-0" />
<span class="text-gray-600"><strong>Anzahl Spiele:</strong> {{ system.anzahl_spiele }}</span>
</div>
<div v-if="system.besonderheiten" class="flex items-center">
<Star :size="14" class="text-primary-600 mr-2 flex-shrink-0" />
<span class="text-gray-600"><strong>Besonderheiten:</strong> {{ system.besonderheiten }}</span>
</div>
</div>
</div>
</div>
<div v-else class="text-center py-12 bg-white rounded-xl shadow-lg">
<Settings :size="48" class="text-gray-400 mx-auto mb-4" />
<p class="text-gray-600">Keine Spielsysteme für die ausgewählte Kategorie gefunden.</p>
</div>
<!-- Zusätzliche Informationen -->
<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 flex items-center">
<BookOpen :size="28" class="mr-3" />
Weitere Informationen
</h3>
<div class="space-y-4">
<p class="text-primary-100 leading-relaxed">
Die Spielsysteme werden je nach Liga und Verband unterschiedlich eingesetzt.
Die meisten regionalen Ligen verwenden das Bundessystem oder das Braunschweiger System.
</p>
<p class="text-primary-100 leading-relaxed">
Internationale Wettkämpfe folgen meist den FIT-Systemen (Corbillon-Cup für Damen,
Swaythling-Cup für Herren).
</p>
<div class="mt-6">
<a
href="https://www.wikiwand.com/de/Tischtennis#Spielsysteme"
target="_blank"
class="inline-flex items-center px-6 py-3 bg-white text-primary-600 font-semibold rounded-lg hover:bg-gray-100 transition-colors"
>
<ExternalLink :size="20" class="mr-2" />
Detaillierte Erklärungen auf Wikiwand
</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { Users, Settings, BookOpen, ExternalLink, Calendar, Hash, Star } from 'lucide-vue-next'
const systems = ref([])
const selectedCategory = ref('alle')
const loadSystems = async () => {
try {
const response = await fetch('/data/spielsysteme.csv')
if (!response.ok) return
const csv = await response.text()
const lines = csv.split('\n').filter(line => line.trim() !== '')
if (lines.length < 2) return
systems.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())
if (values.length < 8) return null
return {
name: values[0].trim(),
description: values[1].trim(),
mannschaftsgroesse: values[2].trim(),
kategorie: values[3].trim(),
details: values[4].trim(),
spielabfolge: values[5].trim(),
anzahl_spiele: values[6].trim(),
besonderheiten: values[7].trim()
}
}).filter(system => system !== null)
} catch (error) {
console.error('Fehler beim Laden der Spielsysteme:', error)
}
}
const verfuegbareKategorien = computed(() => {
const kategorien = [...new Set(systems.value.map(s => s.kategorie).filter(k => k !== ''))]
return kategorien.sort()
})
const filteredSystems = computed(() => {
if (selectedCategory.value === 'alle') {
return systems.value
}
return systems.value.filter(s => s.kategorie === selectedCategory.value)
})
const getCategoryColor = (kategorie) => {
const colors = {
'Klassisch': 'bg-blue-100 text-blue-800',
'Flexibel': 'bg-green-100 text-green-800',
'Strukturiert': 'bg-purple-100 text-purple-800',
'Modifiziert': 'bg-orange-100 text-orange-800',
'International': 'bg-red-100 text-red-800',
'Standard': 'bg-gray-100 text-gray-800',
'Professionell': 'bg-yellow-100 text-yellow-800'
}
return colors[kategorie] || 'bg-gray-100 text-gray-800'
}
onMounted(() => {
loadSystems()
})
useHead({
title: 'Spielsysteme - Harheimer TC',
})
</script>

159
pages/termine.vue Normal file
View File

@@ -0,0 +1,159 @@
<template>
<div class="min-h-full py-16 bg-gray-50">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12">
<h1 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-4">
Termine & Events
</h1>
<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">
Alle kommenden Termine und Veranstaltungen des Harheimer TC
</p>
</div>
<div v-if="naechsteTermine.length > 0" class="space-y-4">
<div
v-for="(termin, index) in naechsteTermine"
:key="index"
class="bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow"
>
<div class="flex items-start space-x-4">
<div class="flex-shrink-0 w-16 h-16 bg-primary-600 rounded-xl flex flex-col items-center justify-center text-white">
<span class="text-2xl font-bold">{{ formatDay(termin.datum) }}</span>
<span class="text-xs">{{ formatMonth(termin.datum) }}</span>
</div>
<div class="flex-1">
<div class="flex items-start justify-between">
<div>
<h3 class="text-xl font-semibold text-gray-900 mb-1">{{ termin.titel }}</h3>
<p class="text-gray-600 mb-2">{{ termin.beschreibung }}</p>
<p class="text-sm text-gray-500">{{ formatFullDate(termin.datum) }}</p>
</div>
<span :class="[
'px-3 py-1 text-sm 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>
</div>
<div v-else class="text-center py-16 bg-white rounded-xl shadow-lg">
<Calendar :size="64" class="text-gray-400 mx-auto mb-4" />
<h3 class="text-2xl font-semibold text-gray-900 mb-2">Keine kommenden Termine</h3>
<p class="text-gray-600">
Aktuell sind keine Termine geplant. Schauen Sie bald wieder vorbei!
</p>
</div>
<div class="mt-12 bg-primary-50 border border-primary-100 rounded-xl p-6">
<h3 class="text-lg font-semibold text-primary-900 mb-2">
Hinweis
</h3>
<p class="text-primary-800">
Alle Termine sind vorbehaltlich kurzfristiger Änderungen.
Bei Fragen zu einzelnen Veranstaltungen kontaktieren Sie uns gerne.
</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { Calendar } from 'lucide-vue-next'
const termine = ref([])
const naechsteTermine = computed(() => {
const heute = new Date()
heute.setHours(0, 0, 0, 0)
return termine.value
.filter(t => {
const terminDatum = new Date(t.datum)
return terminDatum >= heute
})
.sort((a, b) => new Date(a.datum) - new Date(b.datum))
})
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 formatFullDate = (dateString) => {
const date = new Date(dateString)
const wochentage = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']
const monate = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']
return `${wochentage[date.getDay()]}, ${date.getDate()}. ${monate[date.getMonth()]} ${date.getFullYear()}`
}
const loadTermine = async () => {
try {
const response = await fetch('/data/termine.csv')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const csv = await response.text()
const lines = csv.split('\n').filter(line => line.trim() !== '')
if (lines.length < 2) {
return
}
termine.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 < 4) {
return null
}
return {
datum: values[0].trim(),
titel: values[1].trim(),
beschreibung: values[2].trim(),
kategorie: values[3].trim()
}
}).filter(termin => termin !== null)
} catch (error) {
console.error('Fehler beim Laden der Termine:', error)
}
}
onMounted(() => {
loadTermine()
})
useHead({
title: 'Termine & Events - Harheimer TC',
})
</script>

View File

@@ -0,0 +1,76 @@
<template>
<div class="min-h-full py-16 bg-gray-50">
<div class="max-w-7xl 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">
Tischtennis für Anfänger
</h1>
<div class="w-24 h-1 bg-primary-600 mb-8" />
<div class="prose prose-lg max-w-none">
<p class="text-xl text-gray-600 mb-8">
Du möchtest mit Tischtennis anfangen? Perfekt! Bei uns bist du richtig.
</p>
<div class="bg-white p-8 rounded-xl shadow-lg not-prose mb-8">
<h3 class="text-2xl font-display font-bold text-gray-900 mb-4">
Was du wissen solltest
</h3>
<ul class="space-y-3">
<li class="flex items-start">
<Check :size="24" class="text-primary-600 mr-3 flex-shrink-0 mt-0.5" />
<span class="text-gray-700">Keine Vorkenntnisse nötig</span>
</li>
<li class="flex items-start">
<Check :size="24" class="text-primary-600 mr-3 flex-shrink-0 mt-0.5" />
<span class="text-gray-700">Schläger und Material werden gestellt</span>
</li>
<li class="flex items-start">
<Check :size="24" class="text-primary-600 mr-3 flex-shrink-0 mt-0.5" />
<span class="text-gray-700">Sportkleidung und Hallenschuhe mitbringen</span>
</li>
<li class="flex items-start">
<Check :size="24" class="text-primary-600 mr-3 flex-shrink-0 mt-0.5" />
<span class="text-gray-700">3x kostenlos Probetraining</span>
</li>
<li class="flex items-start">
<Check :size="24" class="text-primary-600 mr-3 flex-shrink-0 mt-0.5" />
<span class="text-gray-700">Einstieg jederzeit möglich</span>
</li>
</ul>
</div>
<div class="bg-primary-50 p-8 rounded-xl border border-primary-100 not-prose">
<h3 class="text-2xl font-display font-bold text-gray-900 mb-4">
Anfängergruppen
</h3>
<div class="space-y-4 mb-6">
<div>
<h4 class="font-semibold text-gray-900 mb-1">Schüler/Jugend (ab 6 Jahre)</h4>
<p class="text-gray-600">Dienstag, 17:30 - 19:30 Uhr</p>
</div>
<div>
<h4 class="font-semibold text-gray-900 mb-1">Damen und Herren</h4>
<p class="text-gray-600">Dienstag & Donnerstag, 19:30 - 22:30 Uhr</p>
</div>
</div>
<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"
>
Zum Probetraining anmelden
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { Check } from 'lucide-vue-next'
useHead({
title: 'Für Anfänger - Harheimer TC',
})
</script>

102
pages/training/index.vue Normal file
View File

@@ -0,0 +1,102 @@
<template>
<div class="min-h-full py-16 bg-gray-50">
<div class="max-w-7xl 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">
Trainingszeiten
</h1>
<div class="w-24 h-1 bg-primary-600 mb-8" />
<!-- Trainingsort -->
<div class="bg-white rounded-xl shadow-lg p-8 mb-12">
<div class="flex items-start space-x-4 mb-6">
<MapPin :size="32" class="text-primary-600 flex-shrink-0" />
<div>
<h2 class="text-2xl font-display font-bold text-gray-900 mb-4">Trainingsort</h2>
<h3 class="text-lg font-semibold text-gray-900 mb-2">
Sporthalle der Grundschule Harheim
</h3>
<p class="text-gray-700 mb-1">In den Schafgärten 25</p>
<p class="text-gray-700 mb-4">60437 Frankfurt/Main</p>
<a
href="https://www.google.com/maps/search/?api=1&query=In+den+Schafgärten+25+60437+Frankfurt"
target="_blank"
class="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg transition-colors text-sm"
>
<MapPin :size="16" class="mr-2" />
Anfahrtsplan anzeigen
</a>
</div>
</div>
</div>
<!-- Trainingszeiten -->
<h2 class="text-2xl font-display font-bold text-gray-900 mb-6">
Trainingszeiten
</h2>
<div class="grid gap-6 mb-12">
<div class="bg-white p-6 rounded-xl shadow-lg border-l-4 border-primary-600">
<div class="flex items-start justify-between">
<div>
<h3 class="text-xl font-display font-bold text-gray-900 mb-2">Damen und Herren</h3>
<div class="space-y-2">
<p class="text-lg font-semibold text-primary-600">
Dienstag: 19:30 - 22:30 Uhr
</p>
<p class="text-lg font-semibold text-primary-600">
Donnerstag: 19:30 - 22:30 Uhr
</p>
</div>
</div>
<Clock :size="32" class="text-primary-600" />
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-lg border-l-4 border-primary-600">
<div class="flex items-start justify-between">
<div>
<h3 class="text-xl font-display font-bold text-gray-900 mb-2">Schüler/Jugend</h3>
<p class="text-gray-600 mb-2">Ab 6 Jahre</p>
<p class="text-lg font-semibold text-primary-600">
Dienstag: 17:30 - 19:30 Uhr
</p>
</div>
<Clock :size="32" class="text-primary-600" />
</div>
</div>
</div>
<div class="mt-12 bg-primary-50 p-8 rounded-xl border border-primary-100">
<h3 class="text-2xl font-display font-bold text-gray-900 mb-4">
Interessiert?
</h3>
<p class="text-gray-600 mb-6">
Komm einfach zum Schnuppertraining vorbei oder kontaktiere uns für weitere Informationen!
</p>
<div class="flex flex-wrap gap-4">
<NuxtLink
to="/training/anfaenger"
class="inline-flex items-center px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition-colors"
>
Infos für Anfänger
</NuxtLink>
<NuxtLink
to="/kontakt"
class="inline-flex items-center px-6 py-3 bg-white hover:bg-gray-50 text-primary-600 border-2 border-primary-600 font-semibold rounded-lg transition-colors"
>
Kontakt
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { Clock, MapPin } from 'lucide-vue-next'
useHead({
title: 'Trainingszeiten - Harheimer TC',
})
</script>

View File

@@ -0,0 +1,52 @@
<template>
<div class="min-h-full py-16 bg-gray-50">
<div class="max-w-7xl 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">
Unsere Trainer
</h1>
<div class="w-24 h-1 bg-primary-600 mb-8" />
<p class="text-xl text-gray-600 mb-12">
Erfahrene und qualifizierte Trainer für alle Leistungsstufen
</p>
<div class="grid md:grid-cols-3 gap-8">
<div class="bg-white p-8 rounded-xl shadow-lg">
<h3 class="text-2xl font-display font-bold text-gray-900 mb-2">C-Trainer</h3>
<p class="text-gray-600 mb-4">Torsten Schulz</p>
<p class="text-sm text-gray-500">
Lizenz: C-Trainer<br />
Schwerpunkt: Nachwuchsförderung<br />
Erwachsenen bei Wunsch zur Verfügung
</p>
</div>
<div class="bg-white p-8 rounded-xl shadow-lg">
<h3 class="text-2xl font-display font-bold text-gray-900 mb-2">Kindertrainer</h3>
<p class="text-gray-600 mb-4">Thomas Steinbrech</p>
<p class="text-sm text-gray-500">
Lizenz: Kindertrainer<br />
Schwerpunkt: Nachwuchsförderung
</p>
</div>
<div class="bg-white p-8 rounded-xl shadow-lg">
<h3 class="text-2xl font-display font-bold text-gray-900 mb-2">Assistenztrainerin</h3>
<p class="text-gray-600 mb-4">Magda Schwallbach</p>
<p class="text-sm text-gray-500">
Lizenz: Assistenztrainerin<br />
Schwerpunkt: Unterstützung & Betreuung
</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
useHead({
title: 'Trainer - Harheimer TC',
})
</script>

200
pages/tt-regeln.vue Normal file
View File

@@ -0,0 +1,200 @@
<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">
Tischtennis-Regeln
</h1>
<div class="w-24 h-1 bg-primary-600 mb-8" />
<p class="text-xl text-gray-600 mb-12">
Offizielle Regeln und Bestimmungen für den Tischtennissport
</p>
<!-- Offizielle Regeln -->
<div class="grid md:grid-cols-2 gap-8 mb-12 items-stretch">
<!-- ITTF-Reglement -->
<div class="bg-white rounded-xl shadow-lg p-8 border border-gray-100 flex flex-col h-full">
<div class="flex items-center mb-6">
<div class="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center mr-4">
<Globe :size="24" class="text-white" />
</div>
<div>
<h2 class="text-2xl font-display font-bold text-gray-900">Offizielles ITTF-Reglement</h2>
<p class="text-gray-600">Internationale Tischtennis-Regeln</p>
</div>
</div>
<p class="text-gray-700 mb-6 leading-relaxed flex-grow">
Die offiziellen Regeln des Internationalen Tischtennis-Verbands (ITTF)
gelten weltweit für alle Wettkämpfe und Turniere.
</p>
<div class="space-y-4 mt-auto">
<a
href="https://www.tischtennis.de/dttb/regeln-satzung/satzung-ordnungen.html"
target="_blank"
class="block w-full px-6 py-4 bg-primary-600 hover:bg-primary-700 text-white font-bold rounded-lg transition-colors text-center text-lg border-2 border-primary-600 shadow-lg"
>
🔗 Offizielle ITTF-Regeln aufrufen
</a>
<div class="text-center">
<p class="text-sm text-gray-600 font-medium">
Deutsche Übersetzung auf tischtennis.de
</p>
<p class="text-xs text-gray-500 mt-1">
Internationale Tischtennis-Regeln A & B
</p>
</div>
</div>
</div>
<!-- Vereinfachte Regeln -->
<div class="bg-white rounded-xl shadow-lg p-8 border border-gray-100 flex flex-col h-full">
<div class="flex items-center mb-6">
<div class="w-12 h-12 bg-gradient-to-br from-primary-500 to-primary-600 rounded-xl flex items-center justify-center mr-4">
<FileText :size="24" class="text-white" />
</div>
<div>
<h2 class="text-2xl font-display font-bold text-gray-900">Tischtennis-Regeln Light</h2>
<p class="text-gray-600">Vereinfachte Übersicht</p>
</div>
</div>
<p class="text-gray-700 mb-6 leading-relaxed flex-grow">
Eine kompakte Übersicht der wichtigsten Tischtennis-Regeln
für Einsteiger und Hobbyspieler. Diese vereinfachte Version
erklärt die Grundlagen verständlich und übersichtlich.
</p>
<div class="space-y-3 mt-auto">
<a
href="/documents/Tischtennisregeln light.pdf"
target="_blank"
download
class="block w-full px-6 py-4 bg-primary-600 hover:bg-primary-700 text-white font-bold rounded-lg transition-colors text-center text-lg border-2 border-primary-600 shadow-lg"
>
Regeln Light herunterladen
</a>
<p class="text-sm text-gray-500 text-center">
PDF-Dokument (vereinfachte Fassung)
</p>
</div>
</div>
</div>
<!-- Grundregeln Übersicht -->
<div class="bg-white rounded-xl shadow-lg p-8 mb-12">
<h2 class="text-3xl font-display font-bold text-gray-900 mb-8 text-center">
Grundregeln im Überblick
</h2>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="text-center p-6 bg-gray-50 rounded-lg">
<div class="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Target :size="32" class="text-primary-600" />
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Spielfeld</h3>
<p class="text-gray-600 text-sm">
Tisch: 2,74m × 1,525m, Höhe: 76cm<br>
Netz: 15,25cm hoch
</p>
</div>
<div class="text-center p-6 bg-gray-50 rounded-lg">
<div class="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Circle :size="32" class="text-primary-600" />
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Ball</h3>
<p class="text-gray-600 text-sm">
Durchmesser: 40mm<br>
Gewicht: 2,7g
</p>
</div>
<div class="text-center p-6 bg-gray-50 rounded-lg">
<div class="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Zap :size="32" class="text-primary-600" />
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Schläger</h3>
<p class="text-gray-600 text-sm">
Belag: schwarz + farbig<br>
(rot, grün, pink, blau, gelb, lila)<br>
Holz: mindestens 85%
</p>
</div>
<div class="text-center p-6 bg-gray-50 rounded-lg">
<div class="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Play :size="32" class="text-primary-600" />
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Aufschlag</h3>
<p class="text-gray-600 text-sm">
Ball muss sichtbar hochgeworfen werden<br>
Mindestens 16cm Höhe
</p>
</div>
<div class="text-center p-6 bg-gray-50 rounded-lg">
<div class="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Trophy :size="32" class="text-primary-600" />
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Satz</h3>
<p class="text-gray-600 text-sm">
Gewinn bei 11 Punkten<br>
Mindestens 2 Punkte Vorsprung
</p>
</div>
<div class="text-center p-6 bg-gray-50 rounded-lg">
<div class="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Users :size="32" class="text-primary-600" />
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Spiel</h3>
<p class="text-gray-600 text-sm">
Best of 5 oder 7 Sätze<br>
Wechsel alle 2 Punkte
</p>
</div>
</div>
</div>
<!-- Zusätzliche Informationen -->
<div class="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 flex items-center">
<BookOpen :size="28" class="mr-3" />
Weitere Informationen
</h3>
<div class="space-y-4">
<p class="text-primary-100 leading-relaxed">
Die offiziellen ITTF-Regeln werden regelmäßig aktualisiert und gelten für alle
internationalen Wettkämpfe. Für regionale Turniere können abweichende
Bestimmungen gelten.
</p>
<p class="text-primary-100 leading-relaxed">
Bei Fragen zu spezifischen Regeln wenden Sie sich an den
<a href="https://www.tischtennis.de" target="_blank" class="underline hover:text-white">
Deutschen Tischtennis-Bund (DTTB)
</a> oder Ihren regionalen Verband.
</p>
<div class="mt-6 text-center">
<a
href="https://www.tischtennis.de/dttb/regeln-satzung/satzung-ordnungen.html"
target="_blank"
class="inline-flex items-center px-8 py-4 bg-primary-600 hover:bg-primary-700 text-white font-bold rounded-lg transition-colors text-lg border-2 border-primary-600 shadow-lg"
>
🔗 Alle DTTB-Regeln und Ordnungen
</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { Globe, FileText, Download, ExternalLink, Target, Circle, Zap, Play, Trophy, Users, BookOpen } from 'lucide-vue-next'
useHead({
title: 'TT-Regeln - Harheimer TC',
})
</script>

14
pages/ueber-uns.vue Normal file
View File

@@ -0,0 +1,14 @@
<template>
<div class="min-h-full">
<About />
</div>
</template>
<script setup>
import About from '~/components/About.vue'
useHead({
title: 'Über uns - Harheimer TC',
})
</script>

View File

@@ -0,0 +1,277 @@
<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"
@click="selectedYear = 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'
]"
>
{{ jahr }}
</button>
<button
@click="selectedYear = 'alle'"
: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'
]"
>
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="(kategorieData, kategorie) in 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 kategorieData"
: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>
<span class="font-semibold text-gray-900">
{{ ergebnis.spieler1 }}
<span v-if="ergebnis.spieler2" class="text-gray-600">
/ {{ ergebnis.spieler2 }}
</span>
</span>
</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>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { Trophy } from 'lucide-vue-next'
const results = ref([])
const selectedYear = ref('alle')
const loadResults = async () => {
try {
const response = await fetch('/data/vereinsmeisterschaften.csv')
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())
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()
}
}).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
})
onMounted(() => {
loadResults()
})
useHead({
title: 'Vereinsmeisterschaften - Harheimer TC',
})
</script>

65
pages/vorstand.vue Normal file
View File

@@ -0,0 +1,65 @@
<template>
<div class="min-h-full py-16 bg-gray-50">
<div class="max-w-7xl 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">
Vorstand
</h1>
<div class="w-24 h-1 bg-primary-600 mb-8" />
<div class="prose prose-lg max-w-none">
<p class="text-xl text-gray-600 mb-8">
Unser engagiertes Vorstandsteam leitet den Harheimer TC mit Herz und Sachverstand.
</p>
<div class="grid md:grid-cols-2 gap-8 not-prose">
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
<h3 class="text-xl font-display font-bold text-gray-900 mb-2">Vorsitzender</h3>
<h4 class="text-lg font-semibold text-primary-600 mb-3">Roger Dichmann</h4>
<div class="space-y-1 text-gray-600">
<p>Reginastr. 46</p>
<p>60437 Frankfurt</p>
<p>Tel. 06101-9953015</p>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
<h3 class="text-xl font-display font-bold text-gray-900 mb-2">Stellvertreter des Vorsitzenden</h3>
<h4 class="text-lg font-semibold text-primary-600 mb-3">Jürgen Kratz</h4>
<div class="space-y-1 text-gray-600">
<p>Bürgerstr. 68</p>
<p>60437 Frankfurt</p>
<p>Tel. 06101-43221</p>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
<h3 class="text-xl font-display font-bold text-gray-900 mb-2">Kassenwart</h3>
<h4 class="text-lg font-semibold text-primary-600 mb-3">Olaf Nüßlein</h4>
<div class="space-y-1 text-gray-600">
<p>Am Eschbachtal 52</p>
<p>60437 Frankfurt</p>
<p>Tel. 06101-47469</p>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
<h3 class="text-xl font-display font-bold text-gray-900 mb-2">Schriftführer</h3>
<h4 class="text-lg font-semibold text-primary-600 mb-3">Jürgen Dichmann</h4>
<div class="space-y-1 text-gray-600">
<p>In der Fuchskaut 4</p>
<p>60437 Frankfurt</p>
<p>Tel. 06101-4992227</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
useHead({
title: 'Vorstand - Harheimer TC',
})
</script>

View File

@@ -0,0 +1,7 @@
"mannschaft","liga","staffelleiter","telefon","heimspieltag","spielsystem","mannschaftsfuehrer","spieler","weitere_informationen_link","letzte_aktualisierung"
"Erwachsene 1","1.Kreisklasse Frankfurt, Gruppe 1","Michael Heck","069-40807763","Donnerstag, 20:15 Uhr","Bundessystem (4er-Mannschaft)","André Gilzinger","Josias Strobel; André Gilzinger; Ulf Heinzerling; Sven Baublies","https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/1._Kreisklasse_Gr._1/gruppe/496101/tabelle/gesamt","16.07.2025"
"Erwachsene 2","1.Kreisklasse Frankfurt, Gruppe 2","Michael Heck","069-40807763","Dienstag, 20:15 Uhr","Bundessystem (4er-Mannschaft)","Michael Koch","Bernd Meyer; Detlef Alt; Michael Koch; Marco Reininger","https://click-tt.de/mannschaft/erwachsene-2","16.07.2025"
"Erwachsene 3","2.Kreisklasse Frankfurt, Gruppe 1","Michael Walter","0160-97800518","Donnerstag, 20:15 Uhr","Bundessystem (4er Mannschaft)","Jonas Völker","Olaf Nüßlein; Jürgen Kratz; Jonas Völker; Arno Krauß","https://click-tt.de/mannschaft/erwachsene-3","16.07.2025"
"Erwachsene 4","2.Kreisklasse Frankfurt, Gruppe 2","Michael Walter","0160-97800518","Dienstag, 20:15 Uhr","Bundessystem (4er Mannschaft)","Mark Möllenbruck","Melanie Bayer; Thomas Steinbrech; Mark Möllenbruck; Jacob Waltenberger","https://click-tt.de/mannschaft/erwachsene-4","16.07.2025"
"Erwachsene 5","3.Kreisklasse Frankfurt, Gruppe 1","Christian von Tresckow","0172 8858913","Donnerstag, 20:15 Uhr","Braunschweiger System (3er oder 4er Mannschaft möglich)","Johannes Binder","Torsten Schulz; Kristin von Rauchhaupt; Johannes Binder; Roger Dichmann; Matthias Schmidt; André Schindler; Sebastian Renker; Helge Stefan; Georg Gilzinger; Zhehao Shi; Birgit Haas-Schrödter; Jürgen Dichmann; Paul Fremer","https://click-tt.de/mannschaft/erwachsene-5","16.07.2025"
"Jugendmannschaft","Jungen (J 11), 1.Kreisklasse Frankfurt","Thosten Scherz","0171-9370881","Dienstag, 18:00 Uhr","Braunschweiger System (3/4er-Mannschaft)","Timo Wolf","Timo Wolf; Emilian Völker; Lukas Rusu Cara; Daniel Rusu Cara; Joschua Koch; Fred Swyter","https://click-tt.de/mannschaft/jugendmannschaft","16.07.2025"
1 mannschaft liga staffelleiter telefon heimspieltag spielsystem mannschaftsfuehrer spieler weitere_informationen_link letzte_aktualisierung
2 Erwachsene 1 1.Kreisklasse Frankfurt, Gruppe 1 Michael Heck 069-40807763 Donnerstag, 20:15 Uhr Bundessystem (4er-Mannschaft) André Gilzinger Josias Strobel; André Gilzinger; Ulf Heinzerling; Sven Baublies https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/1._Kreisklasse_Gr._1/gruppe/496101/tabelle/gesamt 16.07.2025
3 Erwachsene 2 1.Kreisklasse Frankfurt, Gruppe 2 Michael Heck 069-40807763 Dienstag, 20:15 Uhr Bundessystem (4er-Mannschaft) Michael Koch Bernd Meyer; Detlef Alt; Michael Koch; Marco Reininger https://click-tt.de/mannschaft/erwachsene-2 16.07.2025
4 Erwachsene 3 2.Kreisklasse Frankfurt, Gruppe 1 Michael Walter 0160-97800518 Donnerstag, 20:15 Uhr Bundessystem (4er Mannschaft) Jonas Völker Olaf Nüßlein; Jürgen Kratz; Jonas Völker; Arno Krauß https://click-tt.de/mannschaft/erwachsene-3 16.07.2025
5 Erwachsene 4 2.Kreisklasse Frankfurt, Gruppe 2 Michael Walter 0160-97800518 Dienstag, 20:15 Uhr Bundessystem (4er Mannschaft) Mark Möllenbruck Melanie Bayer; Thomas Steinbrech; Mark Möllenbruck; Jacob Waltenberger https://click-tt.de/mannschaft/erwachsene-4 16.07.2025
6 Erwachsene 5 3.Kreisklasse Frankfurt, Gruppe 1 Christian von Tresckow 0172 8858913 Donnerstag, 20:15 Uhr Braunschweiger System (3er oder 4er Mannschaft möglich) Johannes Binder Torsten Schulz; Kristin von Rauchhaupt; Johannes Binder; Roger Dichmann; Matthias Schmidt; André Schindler; Sebastian Renker; Helge Stefan; Georg Gilzinger; Zhehao Shi; Birgit Haas-Schrödter; Jürgen Dichmann; Paul Fremer https://click-tt.de/mannschaft/erwachsene-5 16.07.2025
7 Jugendmannschaft Jungen (J 11), 1.Kreisklasse Frankfurt Thosten Scherz 0171-9370881 Dienstag, 18:00 Uhr Braunschweiger System (3/4er-Mannschaft) Timo Wolf Timo Wolf; Emilian Völker; Lukas Rusu Cara; Daniel Rusu Cara; Joschua Koch; Fred Swyter https://click-tt.de/mannschaft/jugendmannschaft 16.07.2025

View File

@@ -0,0 +1,12 @@
"name","description","mannschaftsgroesse","kategorie","details","spielabfolge","anzahl_spiele","besonderheiten"
"Sechser-Paarkreuz-System","Klassisches System für größere Mannschaften mit 3 Doppeln und 6 Einzeln","6er-Mannschaft","Klassisch","Paarweise Kreuzung der Spieler in drei Paarkreuzen","16 Spiele: 3 Doppel + 12 Einzel + 1 Doppel","16","9 Siege zum Gewinn"
"Braunschweiger System","Flexibles System für kleinere Mannschaften mit verschiedenen Varianten","3er oder 4er Mannschaft","Flexibel","Anpassbar an Mannschaftsgröße, immer 10 Spiele","10 Spiele: 1-2 Doppel + Einzel","10","Verschiedene Varianten möglich"
"Werner-Scheffler-System","Strukturiertes System für 4er-Mannschaften mit 2 Doppeln und 12 Einzeln","4er Mannschaft","Strukturiert","Systematische Paarung, auch Kombisystem des WTTV","14 Spiele: 2 Doppel + 12 Einzel","14","Seit 1968 in DTTB-Wettspielordnung"
"Modifiziertes Werner-Scheffler-System","Erweiterte Version des Werner-Scheffler-Systems","4er Mannschaft","Modifiziert","Verbesserte Paarungslogik","Variiert","Variiert","Anpassungen an moderne Anforderungen"
"Corbillon-Cup-System","Internationales System für Damenmannschaften","2er Mannschaft","International","FIT-System für Damen, benannt nach Marcel Corbillon","5 Spiele: 4 Einzel + 1 Doppel","5","3 Siege zum Gewinn"
"Swaythling-Cup-System","Internationales System für Herrenmannschaften","3er Mannschaft","International","FIT-System für Herren, Best of 9 Matches","9 Spiele: nur Einzel","9","5 Siege zum Gewinn"
"Modifiziertes Swaythling-Cup-System","Angepasste Version des Swaythling-Cup-Systems","3er Mannschaft","Modifiziert","Flexiblere Regeln, Best of 7 Matches","7 Spiele: 3 Einzel + 1 Doppel + 3 Einzel","7","4 Siege zum Gewinn"
"Bundessystem","Standard-System des DTTB für 4er-Mannschaften","4er Mannschaft","Standard","Deutscher Tischtennis-Bund Standard","10 Spiele: 2 Doppel + 8 Einzel","10","Alle Spiele werden ausgetragen"
"Tischtennis-Bundesliga-System","Professionelles System der Bundesliga","3er Mannschaft","Professionell","Höchste deutsche Spielklasse","5 Spiele: 5 Einzel","5","Seit 2011/12 in TTBL"
"Schweden-Liga-System","Skandinavisches Spielsystem für 3er-Mannschaften","3er Mannschaft","International","Schwedisches Ligasystem mit Doppel","10 Spiele: 9 Einzel + 1 Doppel","10","Doppel nach 3. Einzel"
"Schweizer System","VR-Cup System aus der Schweiz","Variabel","International","Schweizer Verbandssystem","Variiert","Variiert","Anpassbar an verschiedene Größen"
1 name description mannschaftsgroesse kategorie details spielabfolge anzahl_spiele besonderheiten
2 Sechser-Paarkreuz-System Klassisches System für größere Mannschaften mit 3 Doppeln und 6 Einzeln 6er-Mannschaft Klassisch Paarweise Kreuzung der Spieler in drei Paarkreuzen 16 Spiele: 3 Doppel + 12 Einzel + 1 Doppel 16 9 Siege zum Gewinn
3 Braunschweiger System Flexibles System für kleinere Mannschaften mit verschiedenen Varianten 3er oder 4er Mannschaft Flexibel Anpassbar an Mannschaftsgröße, immer 10 Spiele 10 Spiele: 1-2 Doppel + Einzel 10 Verschiedene Varianten möglich
4 Werner-Scheffler-System Strukturiertes System für 4er-Mannschaften mit 2 Doppeln und 12 Einzeln 4er Mannschaft Strukturiert Systematische Paarung, auch Kombisystem des WTTV 14 Spiele: 2 Doppel + 12 Einzel 14 Seit 1968 in DTTB-Wettspielordnung
5 Modifiziertes Werner-Scheffler-System Erweiterte Version des Werner-Scheffler-Systems 4er Mannschaft Modifiziert Verbesserte Paarungslogik Variiert Variiert Anpassungen an moderne Anforderungen
6 Corbillon-Cup-System Internationales System für Damenmannschaften 2er Mannschaft International FIT-System für Damen, benannt nach Marcel Corbillon 5 Spiele: 4 Einzel + 1 Doppel 5 3 Siege zum Gewinn
7 Swaythling-Cup-System Internationales System für Herrenmannschaften 3er Mannschaft International FIT-System für Herren, Best of 9 Matches 9 Spiele: nur Einzel 9 5 Siege zum Gewinn
8 Modifiziertes Swaythling-Cup-System Angepasste Version des Swaythling-Cup-Systems 3er Mannschaft Modifiziert Flexiblere Regeln, Best of 7 Matches 7 Spiele: 3 Einzel + 1 Doppel + 3 Einzel 7 4 Siege zum Gewinn
9 Bundessystem Standard-System des DTTB für 4er-Mannschaften 4er Mannschaft Standard Deutscher Tischtennis-Bund Standard 10 Spiele: 2 Doppel + 8 Einzel 10 Alle Spiele werden ausgetragen
10 Tischtennis-Bundesliga-System Professionelles System der Bundesliga 3er Mannschaft Professionell Höchste deutsche Spielklasse 5 Spiele: 5 Einzel 5 Seit 2011/12 in TTBL
11 Schweden-Liga-System Skandinavisches Spielsystem für 3er-Mannschaften 3er Mannschaft International Schwedisches Ligasystem mit Doppel 10 Spiele: 9 Einzel + 1 Doppel 10 Doppel nach 3. Einzel
12 Schweizer System VR-Cup System aus der Schweiz Variabel International Schweizer Verbandssystem Variiert Variiert Anpassbar an verschiedene Größen

11
public/data/termine.csv Normal file
View File

@@ -0,0 +1,11 @@
"datum","titel","beschreibung","kategorie"
"2025-10-25","Herbstturnier","Offenes Turnier für alle Leistungsklassen","Turnier"
"2025-11-02","Halloween-Special","Spooky Training mit Kostümen und Süßigkeiten","Event"
"2025-11-15","Vereinsmeisterschaft","Das Highlight der Saison - Vereinsmeisterschaft in allen Kategorien","Turnier"
"2025-12-06","Nikolaus-Turnier","Weihnachtliches Turnier mit kleinen Geschenken","Turnier"
"2025-12-20","Weihnachtsfeier","Gemütlicher Jahresabschluss mit Siegerehrung","Event"
"2026-01-10","Neujahrstraining","Erstes Training im neuen Jahr","Event"
"2026-02-14","Valentinstag-Special","Paar-Turnier für Verliebte","Turnier"
"2026-03-15","Frühlingsturnier","Saisoneröffnung mit großem Turnier","Turnier"
1 datum titel beschreibung kategorie
2 2025-10-25 Herbstturnier Offenes Turnier für alle Leistungsklassen Turnier
3 2025-11-02 Halloween-Special Spooky Training mit Kostümen und Süßigkeiten Event
4 2025-11-15 Vereinsmeisterschaft Das Highlight der Saison - Vereinsmeisterschaft in allen Kategorien Turnier
5 2025-12-06 Nikolaus-Turnier Weihnachtliches Turnier mit kleinen Geschenken Turnier
6 2025-12-20 Weihnachtsfeier Gemütlicher Jahresabschluss mit Siegerehrung Event
7 2026-01-10 Neujahrstraining Erstes Training im neuen Jahr Event
8 2026-02-14 Valentinstag-Special Paar-Turnier für Verliebte Turnier
9 2026-03-15 Frühlingsturnier Saisoneröffnung mit großem Turnier Turnier

View File

@@ -0,0 +1,48 @@
"jahr","kategorie","platz","spieler1","spieler2","bemerkung"
"2024","Einzel","1","Michael Koch","",""
"2024","Einzel","2","Olaf Nüßlein","",""
"2024","Einzel","3","Bernd Meyer","",""
"2024","Doppel","1","Sven Baublies","Johannes Binder",""
"2024","Doppel","2","Bernd Meyer","Jürgen Dichmann",""
"2024","Doppel","3","Michael Koch","Jacob Waltenberger",""
"2023","Einzel","1","André Gilzinger","",""
"2023","Einzel","2","Olaf Nüßlein","",""
"2023","Einzel","3","Michael Koch","",""
"2023","Doppel","1","Olaf Nüßlein","Johannes Binder",""
"2023","Doppel","2","Renate Nebel","André Gilzinger",""
"2023","Doppel","3","Ute Puschmann","Jürgen Kratz",""
"2022","Einzel","1","Sven Baublies","",""
"2022","Einzel","2","Thomas Steinbrech","",""
"2022","Einzel","3","André Gilzinger","",""
"2022","Doppel","1","Sven Baublies","Kristin von Rauchhaupt",""
"2022","Doppel","2","Michael Weber","Johannes Binder",""
"2022","Doppel","3","Michael Koch","Renate Nebel",""
"2021","","","","","coronabedingter Ausfall"
"2020","","","","","coronabedingter Ausfall"
"2019","Einzel","1","André Gilzinger","",""
"2019","Einzel","2","Thomas Steinbrech","",""
"2019","Einzel","3","Jürgen Kratz","",""
"2019","Doppel","1","André Gilzinger","Volker Marx",""
"2019","Doppel","2","Jürgen Kratz","Marko Wiedau",""
"2019","Doppel","3","Bernd Meyer","Kristin von Rauchhaupt",""
"2018","Einzel","1","André Gilzinger","",""
"2018","Einzel","2","Jürgen Kratz","",""
"2018","Einzel","3","Sven Baublies","",""
"2018","Doppel","1","André Gilzinger","Volker Marx",""
"2018","Doppel","2","Sven Baublies","Helge Stefan",""
"2018","Doppel","3","Jürgen Kratz","Renate Nebel",""
"2017","Einzel","1","André Gilzinger","",""
"2017","Einzel","2","Sven Baublies","",""
"2017","Einzel","3","Olaf Nüßlein","",""
"2017","Doppel","1","Olaf Nüßlein","Helge Stefan",""
"2017","Doppel","2","André Gilzinger","Renate Nebel",""
"2017","Doppel","3","Jürgen Kratz","Kristin von Rauchhaupt",""
"2016","Herren-Einzel","1","André Gilzinger","",""
"2016","Herren-Einzel","2","Sven Baublies","",""
"2016","Herren-Einzel","3","Olaf Nüßlein","",""
"2016","Damen-Einzel","1","Birgit Haas-Schrödter","",""
"2016","Damen-Einzel","2","Kristin von Rauchhaupt","",""
"2016","Damen-Einzel","3","Renate Nebel","",""
"2016","Doppel","1","Jürgen Kratz","Matthias Schmidt",""
"2016","Doppel","2","André Gilzinger","Bernd Meyer",""
"2016","Doppel","3","Sven Baublies","Dagmar Bereksasi",""
1 jahr kategorie platz spieler1 spieler2 bemerkung
2 2024 Einzel 1 Michael Koch
3 2024 Einzel 2 Olaf Nüßlein
4 2024 Einzel 3 Bernd Meyer
5 2024 Doppel 1 Sven Baublies Johannes Binder
6 2024 Doppel 2 Bernd Meyer Jürgen Dichmann
7 2024 Doppel 3 Michael Koch Jacob Waltenberger
8 2023 Einzel 1 André Gilzinger
9 2023 Einzel 2 Olaf Nüßlein
10 2023 Einzel 3 Michael Koch
11 2023 Doppel 1 Olaf Nüßlein Johannes Binder
12 2023 Doppel 2 Renate Nebel André Gilzinger
13 2023 Doppel 3 Ute Puschmann Jürgen Kratz
14 2022 Einzel 1 Sven Baublies
15 2022 Einzel 2 Thomas Steinbrech
16 2022 Einzel 3 André Gilzinger
17 2022 Doppel 1 Sven Baublies Kristin von Rauchhaupt
18 2022 Doppel 2 Michael Weber Johannes Binder
19 2022 Doppel 3 Michael Koch Renate Nebel
20 2021 coronabedingter Ausfall
21 2020 coronabedingter Ausfall
22 2019 Einzel 1 André Gilzinger
23 2019 Einzel 2 Thomas Steinbrech
24 2019 Einzel 3 Jürgen Kratz
25 2019 Doppel 1 André Gilzinger Volker Marx
26 2019 Doppel 2 Jürgen Kratz Marko Wiedau
27 2019 Doppel 3 Bernd Meyer Kristin von Rauchhaupt
28 2018 Einzel 1 André Gilzinger
29 2018 Einzel 2 Jürgen Kratz
30 2018 Einzel 3 Sven Baublies
31 2018 Doppel 1 André Gilzinger Volker Marx
32 2018 Doppel 2 Sven Baublies Helge Stefan
33 2018 Doppel 3 Jürgen Kratz Renate Nebel
34 2017 Einzel 1 André Gilzinger
35 2017 Einzel 2 Sven Baublies
36 2017 Einzel 3 Olaf Nüßlein
37 2017 Doppel 1 Olaf Nüßlein Helge Stefan
38 2017 Doppel 2 André Gilzinger Renate Nebel
39 2017 Doppel 3 Jürgen Kratz Kristin von Rauchhaupt
40 2016 Herren-Einzel 1 André Gilzinger
41 2016 Herren-Einzel 2 Sven Baublies
42 2016 Herren-Einzel 3 Olaf Nüßlein
43 2016 Damen-Einzel 1 Birgit Haas-Schrödter
44 2016 Damen-Einzel 2 Kristin von Rauchhaupt
45 2016 Damen-Einzel 3 Renate Nebel
46 2016 Doppel 1 Jürgen Kratz Matthias Schmidt
47 2016 Doppel 2 André Gilzinger Bernd Meyer
48 2016 Doppel 3 Sven Baublies Dagmar Bereksasi

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

108
server/api/contact.post.js Normal file
View File

@@ -0,0 +1,108 @@
import nodemailer from 'nodemailer'
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event)
// Validierung der Eingabedaten
if (!body.name || !body.email || !body.subject || !body.message) {
throw createError({
statusCode: 400,
statusMessage: 'Alle Pflichtfelder müssen ausgefüllt werden'
})
}
// E-Mail-Validierung
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(body.email)) {
throw createError({
statusCode: 400,
statusMessage: 'Ungültige E-Mail-Adresse'
})
}
// SMTP-Konfiguration (hier können Sie Ihre SMTP-Daten eintragen)
const transporter = nodemailer.createTransporter({
host: process.env.SMTP_HOST || 'smtp.gmail.com',
port: process.env.SMTP_PORT || 587,
secure: false, // true für 465, false für andere Ports
auth: {
user: process.env.SMTP_USER || 'j.dichmann@gmx.de',
pass: process.env.SMTP_PASS || process.env.EMAIL_PASSWORD
}
})
// E-Mail-Template
const emailHtml = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #dc2626; border-bottom: 2px solid #dc2626; padding-bottom: 10px;">
Neue Kontaktanfrage - Harheimer TC
</h2>
<div style="background-color: #f9fafb; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h3 style="color: #374151; margin-top: 0;">Kontaktdaten:</h3>
<p><strong>Name:</strong> ${body.name}</p>
<p><strong>E-Mail:</strong> ${body.email}</p>
<p><strong>Telefon:</strong> ${body.phone || 'Nicht angegeben'}</p>
<p><strong>Betreff:</strong> ${body.subject}</p>
</div>
<div style="background-color: #ffffff; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px;">
<h3 style="color: #374151; margin-top: 0;">Nachricht:</h3>
<p style="white-space: pre-wrap; line-height: 1.6;">${body.message}</p>
</div>
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 14px;">
<p>Diese Nachricht wurde über das Kontaktformular der Harheimer TC Website gesendet.</p>
<p>Zeitstempel: ${new Date().toLocaleString('de-DE')}</p>
</div>
</div>
`
const emailText = `
Neue Kontaktanfrage - Harheimer TC
Kontaktdaten:
Name: ${body.name}
E-Mail: ${body.email}
Telefon: ${body.phone || 'Nicht angegeben'}
Betreff: ${body.subject}
Nachricht:
${body.message}
---
Diese Nachricht wurde über das Kontaktformular der Harheimer TC Website gesendet.
Zeitstempel: ${new Date().toLocaleString('de-DE')}
`
// E-Mail senden
const mailOptions = {
from: `"Harheimer TC Website" <${process.env.SMTP_USER || 'j.dichmann@gmx.de'}>`,
to: 'j.dichmann@gmx.de',
replyTo: body.email,
subject: `Kontaktanfrage: ${body.subject}`,
text: emailText,
html: emailHtml
}
await transporter.sendMail(mailOptions)
return {
success: true,
message: 'E-Mail wurde erfolgreich gesendet!'
}
} catch (error) {
console.error('Fehler beim Senden der E-Mail:', error)
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
statusMessage: 'Fehler beim Senden der E-Mail. Bitte versuchen Sie es später erneut.'
})
}
})

41
server/api/galerie.get.js Normal file
View File

@@ -0,0 +1,41 @@
import { promises as fs } from 'fs'
import path from 'path'
export default defineEventHandler(async (event) => {
try {
const galerieDir = path.join(process.cwd(), 'public', 'galerie')
// Prüfe, ob das Verzeichnis existiert
try {
await fs.access(galerieDir)
} catch {
return []
}
// Lese alle Dateien im Verzeichnis
const dateien = await fs.readdir(galerieDir)
// Filtere nur Bilddateien
const erlaubteExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']
const bilder = dateien.filter(datei => {
const ext = path.extname(datei).toLowerCase()
return erlaubteExtensions.includes(ext)
})
// Erstelle Bildobjekte mit Titel basierend auf Dateiname
return bilder.map(filename => {
const nameWithoutExt = path.parse(filename).name
const title = nameWithoutExt
.replace(/[-_]/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase())
return {
filename,
title
}
})
} catch (error) {
console.error('Fehler beim Lesen der Galerie:', error)
return []
}
})

View File

@@ -0,0 +1,31 @@
import { promises as fs } from 'fs'
import path from 'path'
export default defineEventHandler(async (event) => {
try {
const spielplaeneDir = path.join(process.cwd(), 'public', 'spielplaene')
// Prüfe, ob das Verzeichnis existiert
try {
await fs.access(spielplaeneDir)
} catch {
return []
}
// Lese alle Dateien im Verzeichnis
const dateien = await fs.readdir(spielplaeneDir)
// Filtere nur relevante Dateitypen
const erlaubteExtensions = ['.pdf', '.xlsx', '.xls', '.doc', '.docx']
const gefiltert = dateien.filter(datei => {
const ext = path.extname(datei).toLowerCase()
return erlaubteExtensions.includes(ext)
})
return gefiltert
} catch (error) {
console.error('Fehler beim Lesen der Spielpläne:', error)
return []
}
})

45
tailwind.config.js Normal file
View File

@@ -0,0 +1,45 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./components/**/*.{js,vue,ts}',
'./layouts/**/*.vue',
'./pages/**/*.vue',
'./plugins/**/*.{js,ts}',
'./app.vue',
],
theme: {
extend: {
colors: {
primary: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
},
accent: {
50: '#fafafa',
100: '#f4f4f5',
200: '#e4e4e7',
300: '#d4d4d8',
400: '#a1a1aa',
500: '#71717a',
600: '#52525b',
700: '#3f3f46',
800: '#27272a',
900: '#18181b',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
display: ['Montserrat', 'system-ui', 'sans-serif'],
},
},
},
plugins: [],
}