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