feat: Implement initial website structure with core pages, layout, and reusable UI components, alongside ESLint configuration and SEO setup.

This commit is contained in:
1elle1
2026-01-30 14:45:52 +01:00
parent f0e917ef5d
commit f1cb4ef2cc
20 changed files with 6800 additions and 19 deletions

242
app/aktuelles/page.tsx Normal file
View File

@@ -0,0 +1,242 @@
import { Container } from "@/components/ui/Container";
import { Button } from "@/components/ui/Button";
import { Card } from "@/components/ui/Card";
import { FadeIn } from "@/components/ui/FadeIn";
import { Calendar, ArrowRight } from "lucide-react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Aktuelles",
description:
"Bleiben Sie auf dem Laufenden über Neuigkeiten, Events und Angebote bei Sportbox Reutte.",
openGraph: {
title: "Aktuelles | Sportbox Reutte",
description:
"Neuigkeiten, Events und Angebote bei Sportbox Reutte Ihrem Fitnessstudio in Reutte, Tirol.",
url: "https://sportbox-reutte.at/aktuelles",
},
};
const BLOG_POSTS = [
{
title: "5 Tipps für den perfekten Trainingsstart",
excerpt:
"Sie möchten mit dem Training beginnen, wissen aber nicht wo? Unsere Experten teilen ihre besten Tipps für Einsteiger.",
date: "2026-01-15",
category: "Tipps",
},
{
title: "Neue Gruppenkurse ab Februar",
excerpt:
"Ab Februar erweitern wir unser Kursangebot: HIIT, Functional Training und Pilates neu im Programm.",
date: "2026-01-10",
category: "Neuigkeiten",
},
{
title: "Ernährung und Training: Was wirklich zählt",
excerpt:
"Unsere Ernährungsberaterin Lisa Mayer erklärt, wie Sie Ihre Ernährung optimal auf Ihr Training abstimmen.",
date: "2026-01-05",
category: "Ernährung",
},
{
title: "Rückblick: Community Workout im Dezember",
excerpt:
"Über 40 Teilnehmer beim letzten Community Workout ein Rückblick auf einen großartigen Abend.",
date: "2025-12-20",
category: "Events",
},
{
title: "Krafttraining für Anfänger: Der richtige Einstieg",
excerpt:
"Thomas Huber zeigt die wichtigsten Grundübungen und erklärt, worauf Anfänger achten sollten.",
date: "2025-12-15",
category: "Training",
},
{
title: "Yoga und Fitness: Die perfekte Kombination",
excerpt:
"Warum Yoga die ideale Ergänzung zu Ihrem Krafttraining ist und wie Sie beide Disziplinen verbinden können.",
date: "2025-12-10",
category: "Yoga",
},
] as const;
const EVENTS = [
{
title: "Community Workout Open Air",
date: "15. März 2026",
time: "10:00 12:00 Uhr",
description: "Gemeinsames Training im Freien für alle Fitnesslevel.",
},
{
title: "Ernährungs-Workshop",
date: "22. März 2026",
time: "14:00 16:00 Uhr",
description:
"Lernen Sie, wie Sie Ihre Mahlzeiten optimal für Ihre Ziele planen.",
},
{
title: "Fitness-Challenge: 30 Tage",
date: "1. April 2026",
time: "Start: jederzeit",
description:
"30 Tage, 30 Challenges machen Sie mit und erreichen Sie neue Bestleistungen.",
},
] as const;
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString("de-AT", {
day: "numeric",
month: "long",
year: "numeric",
});
}
export default function AktuellesPage() {
return (
<main>
{/* Hero */}
<section className="pt-32 pb-[var(--spacing-section)] md:pt-40 md:pb-[var(--spacing-4xl)] bg-primary text-secondary">
<Container>
<FadeIn>
<p className="text-sm uppercase tracking-[0.2em] text-secondary/60 mb-4">
Aktuelles
</p>
</FadeIn>
<FadeIn delay={0.1}>
<h1
className="font-bold max-w-2xl"
style={{
fontSize: "clamp(var(--text-3xl), 4vw, var(--text-5xl))",
lineHeight: "1.1",
letterSpacing: "var(--text-5xl-letter-spacing)",
}}
>
Neuigkeiten & Events
</h1>
</FadeIn>
<FadeIn delay={0.2}>
<p className="mt-6 max-w-xl text-secondary/70" style={{ fontSize: "var(--text-lg)" }}>
Bleiben Sie auf dem Laufenden über Neuigkeiten, Events und
Angebote in unserem Fitnessstudio.
</p>
</FadeIn>
</Container>
</section>
{/* Blog Posts */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)]">
<Container>
<FadeIn>
<h2
className="font-bold mb-2"
style={{
fontSize: "var(--text-2xl)",
lineHeight: "var(--text-2xl-line-height)",
}}
>
Beiträge
</h2>
<p className="text-muted mb-10">
Tipps, Neuigkeiten und Wissenswertes rund um Fitness und Gesundheit.
</p>
</FadeIn>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{BLOG_POSTS.map((post, index) => (
<FadeIn key={post.title} delay={index * 0.05}>
<article>
<Card className="h-full flex flex-col">
<div className="flex items-center gap-3 mb-4">
<span className="text-xs uppercase tracking-widest text-muted font-medium">
{post.category}
</span>
<span className="text-xs text-muted">
{formatDate(post.date)}
</span>
</div>
<h3 className="text-base font-bold mb-2">{post.title}</h3>
<p className="text-sm text-muted flex-1">{post.excerpt}</p>
<div className="mt-4 flex items-center gap-1 text-sm font-medium text-primary">
<span>Weiterlesen</span>
<ArrowRight size={14} aria-hidden="true" />
</div>
</Card>
</article>
</FadeIn>
))}
</div>
</Container>
</section>
{/* Events */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)] bg-neutral">
<Container>
<FadeIn>
<h2
className="font-bold mb-2"
style={{
fontSize: "var(--text-2xl)",
lineHeight: "var(--text-2xl-line-height)",
}}
>
Kommende Events
</h2>
<p className="text-muted mb-10">
Workshops, Community-Events und mehr seien Sie dabei.
</p>
</FadeIn>
<div className="space-y-4">
{EVENTS.map((event, index) => (
<FadeIn key={event.title} delay={index * 0.1}>
<Card className="bg-background">
<div className="flex flex-col md:flex-row md:items-center gap-4 md:gap-8">
<div className="flex items-center gap-3 md:min-w-48">
<Calendar size={18} className="text-muted" aria-hidden="true" />
<div>
<p className="text-sm font-bold">{event.date}</p>
<p className="text-xs text-muted">{event.time}</p>
</div>
</div>
<div className="flex-1">
<h3 className="text-base font-bold">{event.title}</h3>
<p className="text-sm text-muted mt-1">{event.description}</p>
</div>
</div>
</Card>
</FadeIn>
))}
</div>
</Container>
</section>
{/* CTA */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)] bg-primary text-secondary">
<Container className="text-center">
<FadeIn>
<h2
className="font-bold mb-4"
style={{
fontSize: "var(--text-3xl)",
lineHeight: "var(--text-3xl-line-height)",
}}
>
Nichts verpassen
</h2>
<p className="text-secondary/70 mb-8 max-w-lg mx-auto">
Kontaktieren Sie uns für aktuelle Informationen zu Events und
Angeboten.
</p>
<Button
href="/leistungen#kontakt"
variant="secondary"
className="border-secondary text-secondary hover:bg-secondary hover:text-primary"
>
Jetzt Termin buchen
</Button>
</FadeIn>
</Container>
</section>
</main>
);
}

View File

@@ -1,9 +1,33 @@
import type { Metadata, Viewport } from "next"; import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import "@/theme/globals.css"; import "@/theme/globals.css";
import "@/theme/stylesheet.css";
import { Header } from "@/components/layout/Header";
import { Footer } from "@/components/layout/Footer";
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Website", title: {
description: "Generated website", default: "Sportbox Reutte Dein Fitnessstudio in Reutte",
template: "%s | Sportbox Reutte",
},
description:
"Willkommen bei Sportbox Reutte Ihr Fitnessstudio für individuelle Trainingspläne und persönliche Betreuung in Reutte, Tirol.",
metadataBase: new URL("https://sportbox-reutte.at"),
openGraph: {
type: "website",
locale: "de_AT",
url: "https://sportbox-reutte.at",
siteName: "Sportbox Reutte",
title: "Sportbox Reutte Dein Fitnessstudio in Reutte",
description:
"Ihr Fitnessstudio für individuelle Trainingspläne und persönliche Betreuung in Reutte, Tirol.",
},
robots: { robots: {
index: true, index: true,
follow: true, follow: true,
@@ -13,7 +37,7 @@ export const metadata: Metadata = {
export const viewport: Viewport = { export const viewport: Viewport = {
width: "device-width", width: "device-width",
initialScale: 1, initialScale: 1,
themeColor: "#ffffff", themeColor: "#000000",
}; };
export default function RootLayout({ export default function RootLayout({
@@ -22,8 +46,12 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="de"> <html lang="de" className={inter.variable}>
<body>{children}</body> <body>
<Header />
{children}
<Footer />
</body>
</html> </html>
); );
} }

227
app/leistungen/page.tsx Normal file
View File

@@ -0,0 +1,227 @@
import { Container } from "@/components/ui/Container";
import { Button } from "@/components/ui/Button";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { Card } from "@/components/ui/Card";
import { FadeIn } from "@/components/ui/FadeIn";
import { ContactForm } from "@/components/ui/ContactForm";
import { FaqSection } from "@/components/sections/FaqSection";
import { Dumbbell, Users, Apple, Flower2, Zap, HeartPulse } from "lucide-react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Leistungen",
description:
"Informieren Sie sich über unser vielfältiges Kursangebot und individuelle Trainingsmöglichkeiten bei Sportbox Reutte.",
openGraph: {
title: "Leistungen | Sportbox Reutte",
description:
"Vielfältiges Kursangebot und individuelle Trainingsmöglichkeiten bei Sportbox Reutte in Reutte, Tirol.",
url: "https://sportbox-reutte.at/leistungen",
},
};
const SERVICES = [
{
icon: Dumbbell,
title: "Personal Training",
description:
"Individuell abgestimmte Trainingspläne mit persönlicher Betreuung durch zertifizierte Trainer. Ideal für gezielte Ziele wie Muskelaufbau, Gewichtsreduktion oder Rehabilitation.",
features: [
"1:1 Betreuung",
"Individueller Trainingsplan",
"Regelmäßige Fortschrittskontrolle",
],
},
{
icon: Users,
title: "Gruppenkurse",
description:
"Vielfältige Kurse für jedes Fitnesslevel von HIIT über Functional Training bis Pilates. Gemeinsam trainieren motiviert und macht Spaß.",
features: [
"Verschiedene Kursformate",
"Für alle Levels",
"Motivierende Gruppenatmosphäre",
],
},
{
icon: Apple,
title: "Ernährungsberatung",
description:
"Professionelle Beratung durch unsere diplomierte Ernährungsberaterin. Wir helfen Ihnen, Ihre Ernährung optimal auf Ihre Trainingsziele abzustimmen.",
features: [
"Persönlicher Ernährungsplan",
"Sportgerechte Ernährung",
"Nachhaltige Umstellung",
],
},
{
icon: Flower2,
title: "Yoga & Mobility",
description:
"Finden Sie Balance zwischen Kraft und Flexibilität. Unsere Yoga- und Mobility-Kurse verbessern Ihre Beweglichkeit und reduzieren Stress.",
features: [
"Verschiedene Yoga-Stile",
"Stretching & Mobility",
"Atemtechniken",
],
},
{
icon: Zap,
title: "Krafttraining",
description:
"Strukturiertes Krafttraining an modernen Geräten und mit freien Gewichten. Von Grundlagen bis Wettkampfvorbereitung.",
features: [
"Moderne Geräte",
"Freie Gewichte",
"Technikschulung",
],
},
{
icon: HeartPulse,
title: "Gesundheitstraining",
description:
"Präventives und rehabilitatives Training für mehr Wohlbefinden im Alltag. Ideal für Rückengesundheit, Haltungsverbesserung und allgemeine Fitness.",
features: [
"Rückentraining",
"Haltungsanalyse",
"Präventives Training",
],
},
] as const;
export default function LeistungenPage() {
return (
<main>
{/* Hero */}
<section className="pt-32 pb-[var(--spacing-section)] md:pt-40 md:pb-[var(--spacing-4xl)] bg-primary text-secondary">
<Container>
<FadeIn>
<p className="text-sm uppercase tracking-[0.2em] text-secondary/60 mb-4">
Leistungen
</p>
</FadeIn>
<FadeIn delay={0.1}>
<h1
className="font-bold max-w-2xl"
style={{
fontSize: "clamp(var(--text-3xl), 4vw, var(--text-5xl))",
lineHeight: "1.1",
letterSpacing: "var(--text-5xl-letter-spacing)",
}}
>
Was wir für Sie tun können.
</h1>
</FadeIn>
<FadeIn delay={0.2}>
<p className="mt-6 max-w-xl text-secondary/70" style={{ fontSize: "var(--text-lg)" }}>
Vielfältige Angebote für Ihre individuellen Fitnessziele
professionell, persönlich und auf Sie abgestimmt.
</p>
</FadeIn>
</Container>
</section>
{/* Services */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)]">
<Container>
<FadeIn>
<SectionHeading
title="Unser Angebot"
subtitle="Von Personal Training bis Ernährungsberatung wir haben für jeden das Richtige."
/>
</FadeIn>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{SERVICES.map((service, index) => (
<FadeIn key={service.title} delay={index * 0.08}>
<Card className="h-full flex flex-col">
<service.icon
size={28}
className="mb-4 text-primary"
aria-hidden="true"
/>
<h3 className="text-lg font-bold mb-2">{service.title}</h3>
<p className="text-sm text-muted mb-4 flex-1">
{service.description}
</p>
<ul className="space-y-1.5">
{service.features.map((feature) => (
<li
key={feature}
className="text-sm text-foreground flex items-start gap-2"
>
<span className="mt-1.5 w-1 h-1 bg-primary rounded-full shrink-0" aria-hidden="true" />
{feature}
</li>
))}
</ul>
</Card>
</FadeIn>
))}
</div>
</Container>
</section>
{/* FAQ */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)] bg-neutral">
<Container>
<FadeIn>
<SectionHeading
title="Häufige Fragen"
subtitle="Antworten auf die häufigsten Fragen rund um Sportbox Reutte."
/>
</FadeIn>
<FadeIn delay={0.1}>
<FaqSection />
</FadeIn>
</Container>
</section>
{/* Contact Form */}
<section id="kontakt" className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)]">
<Container>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
<FadeIn>
<div>
<p className="text-sm uppercase tracking-[0.2em] text-muted mb-4">
Kontakt
</p>
<h2
className="font-bold mb-6"
style={{
fontSize: "var(--text-3xl)",
lineHeight: "var(--text-3xl-line-height)",
letterSpacing: "var(--text-3xl-letter-spacing)",
}}
>
Jetzt Termin vereinbaren.
</h2>
<p className="text-muted leading-relaxed mb-6">
Haben Sie Fragen oder möchten Sie ein Probetraining
vereinbaren? Schreiben Sie uns wir melden uns schnellstmöglich
bei Ihnen.
</p>
<div className="space-y-3 text-sm text-muted">
<p>
<span className="font-medium text-foreground">E-Mail:</span>{" "}
kontakt@sportbox-reutte.at
</p>
<p>
<span className="font-medium text-foreground">Telefon:</span>{" "}
+43 123 456 789
</p>
<p>
<span className="font-medium text-foreground">Adresse:</span>{" "}
Reutte, Tirol, Österreich
</p>
</div>
</div>
</FadeIn>
<FadeIn delay={0.15}>
<ContactForm />
</FadeIn>
</div>
</Container>
</section>
</main>
);
}

View File

@@ -1,7 +1,256 @@
export default function Page() { import { Container } from "@/components/ui/Container";
import { Button } from "@/components/ui/Button";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { Card } from "@/components/ui/Card";
import { FadeIn } from "@/components/ui/FadeIn";
import { Dumbbell, Users, Clock, Heart } from "lucide-react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Sportbox Reutte Dein Fitnessstudio in Reutte",
description:
"Willkommen bei Sportbox Reutte Ihr Fitnessstudio für individuelle Trainingspläne und persönliche Betreuung in Reutte, Tirol.",
openGraph: {
title: "Sportbox Reutte Dein Fitnessstudio in Reutte",
description:
"Ihr Fitnessstudio für individuelle Trainingspläne und persönliche Betreuung in Reutte, Tirol.",
url: "https://sportbox-reutte.at",
},
};
const FEATURES = [
{
icon: Dumbbell,
title: "Individuelle Betreuung",
description:
"Persönliche Trainingspläne, abgestimmt auf Ihre Ziele und Ihr Fitnesslevel.",
},
{
icon: Users,
title: "Starke Community",
description:
"Trainieren Sie in einer motivierenden Gemeinschaft, die Sie unterstützt und inspiriert.",
},
{
icon: Clock,
title: "Flexible Zeiten",
description:
"Großzügige Öffnungszeiten, damit Fitness in Ihren Alltag passt.",
},
{
icon: Heart,
title: "Ganzheitlicher Ansatz",
description:
"Körperliche Fitness, mentale Gesundheit und Ernährung alles unter einem Dach.",
},
] as const;
const SERVICES_PREVIEW = [
{
title: "Personal Training",
description:
"Eins-zu-Eins Betreuung durch zertifizierte Trainer für maximale Ergebnisse.",
},
{
title: "Gruppenkurse",
description:
"Von Yoga bis Krafttraining vielfältige Kurse für jedes Fitnesslevel.",
},
{
title: "Ernährungsberatung",
description:
"Professionelle Beratung für eine Ernährung, die Ihre Trainingsziele unterstützt.",
},
] as const;
export default function HomePage() {
return ( return (
<main> <main>
<p>Ready</p> {/* Hero Section */}
<section className="relative flex items-center min-h-screen bg-primary text-secondary">
<Container className="py-32 md:py-40">
<FadeIn>
<p className="text-sm uppercase tracking-[0.2em] text-secondary/60 mb-6">
Fitnessstudio in Reutte, Tirol
</p>
</FadeIn>
<FadeIn delay={0.1}>
<h1
className="font-bold max-w-3xl"
style={{
fontSize: "clamp(var(--text-4xl), 5vw, var(--text-6xl))",
lineHeight: "1.1",
letterSpacing: "var(--text-5xl-letter-spacing)",
}}
>
Dein Weg zu mehr
<br />
Fitness beginnt hier.
</h1>
</FadeIn>
<FadeIn delay={0.2}>
<p className="mt-6 max-w-xl text-secondary/70" style={{ fontSize: "var(--text-lg)" }}>
Individuelle Trainingspläne, persönliche Betreuung und eine
motivierende Community. Willkommen bei Sportbox Reutte.
</p>
</FadeIn>
<FadeIn delay={0.3}>
<div className="mt-10 flex flex-col sm:flex-row gap-4">
<Button href="/leistungen#kontakt" variant="secondary" className="border-secondary text-secondary hover:bg-secondary hover:text-primary">
Jetzt Termin buchen
</Button>
<Button href="/über-uns" variant="ghost" className="text-secondary/80 hover:text-secondary">
Mehr über uns erfahren
</Button>
</div>
</FadeIn>
</Container>
</section>
{/* Features Section */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)]">
<Container>
<FadeIn>
<SectionHeading
title="Warum Sportbox Reutte?"
subtitle="Wir bieten mehr als nur ein Fitnessstudio wir sind Ihr Partner auf dem Weg zu einem gesünderen Leben."
/>
</FadeIn>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{FEATURES.map((feature, index) => (
<FadeIn key={feature.title} delay={index * 0.1}>
<Card className="text-center h-full">
<feature.icon
size={32}
className="mx-auto mb-4 text-primary"
aria-hidden="true"
/>
<h3 className="text-base font-bold mb-2">{feature.title}</h3>
<p className="text-sm text-muted">{feature.description}</p>
</Card>
</FadeIn>
))}
</div>
</Container>
</section>
{/* About Teaser Section */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)] bg-neutral">
<Container>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<FadeIn>
<div
className="aspect-[4/3] bg-accent/40 flex items-center justify-center"
style={{ borderRadius: "var(--radius-md)" }}
>
<p className="text-muted text-sm">PLACEHOLDER: Studio-Bild</p>
</div>
</FadeIn>
<FadeIn delay={0.15}>
<div>
<p className="text-sm uppercase tracking-[0.2em] text-muted mb-4">
Über uns
</p>
<h2
className="font-bold mb-6"
style={{
fontSize: "var(--text-3xl)",
lineHeight: "var(--text-3xl-line-height)",
letterSpacing: "var(--text-3xl-letter-spacing)",
}}
>
Mehr als nur Training eine Gemeinschaft.
</h2>
<p className="text-muted mb-6 leading-relaxed">
Bei Sportbox Reutte stehen Sie im Mittelpunkt. Unser erfahrenes
Team begleitet Sie auf Ihrem individuellen Weg ob Einsteiger
oder Fortgeschrittener. Gemeinsam erreichen wir Ihre Ziele.
</p>
<Button href="/über-uns" variant="secondary">
Mehr über uns erfahren
</Button>
</div>
</FadeIn>
</div>
</Container>
</section>
{/* Services Preview Section */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)]">
<Container>
<FadeIn>
<SectionHeading
title="Unsere Leistungen"
subtitle="Entdecken Sie unser vielfältiges Angebot für Ihre persönlichen Fitnessziele."
/>
</FadeIn>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{SERVICES_PREVIEW.map((service, index) => (
<FadeIn key={service.title} delay={index * 0.1}>
<Card className="h-full">
<h3 className="text-lg font-bold mb-3">{service.title}</h3>
<p className="text-sm text-muted mb-4">{service.description}</p>
<Button href="/leistungen" variant="ghost" className="text-sm">
Mehr erfahren
</Button>
</Card>
</FadeIn>
))}
</div>
</Container>
</section>
{/* CTA Section */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)] bg-primary text-secondary">
<Container className="text-center">
<FadeIn>
<h2
className="font-bold mb-4"
style={{
fontSize: "var(--text-3xl)",
lineHeight: "var(--text-3xl-line-height)",
}}
>
Bereit für den ersten Schritt?
</h2>
<p className="text-secondary/70 mb-8 max-w-lg mx-auto">
Vereinbaren Sie jetzt einen unverbindlichen Termin und lernen Sie
Sportbox Reutte kennen.
</p>
<Button
href="/leistungen#kontakt"
variant="secondary"
className="border-secondary text-secondary hover:bg-secondary hover:text-primary"
>
Jetzt Termin buchen
</Button>
</FadeIn>
</Container>
</section>
{/* JSON-LD Structured Data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "LocalBusiness",
name: "Sportbox Reutte",
description:
"Fitnessstudio für individuelle Trainingspläne und persönliche Betreuung in Reutte, Tirol.",
url: "https://sportbox-reutte.at",
telephone: "+43123456789",
email: "kontakt@sportbox-reutte.at",
address: {
"@type": "PostalAddress",
addressLocality: "Reutte",
addressRegion: "Tirol",
addressCountry: "AT",
},
sameAs: [],
}),
}}
/>
</main> </main>
); );
} }

222
app/studio/page.tsx Normal file
View File

@@ -0,0 +1,222 @@
import { Container } from "@/components/ui/Container";
import { Button } from "@/components/ui/Button";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { Card } from "@/components/ui/Card";
import { FadeIn } from "@/components/ui/FadeIn";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Studio",
description:
"Entdecken Sie unsere modernen Räumlichkeiten und hochwertigen Fitnessgeräte bei Sportbox Reutte.",
openGraph: {
title: "Studio | Sportbox Reutte",
description:
"Moderne Räumlichkeiten und hochwertige Fitnessgeräte bei Sportbox Reutte in Reutte, Tirol.",
url: "https://sportbox-reutte.at/studio",
},
};
const AREAS = [
{
title: "Kraftbereich",
description:
"Freie Gewichte, Kabelzüge und Kraftgeräte der neuesten Generation für effektives Krafttraining.",
},
{
title: "Cardio-Zone",
description:
"Laufbänder, Ergometer und Rudergeräte für Ihr Ausdauertraining in angenehmer Atmosphäre.",
},
{
title: "Functional Area",
description:
"Offener Trainingsbereich mit Rig, Kettlebells, Medizinbällen und TRX für funktionelles Training.",
},
{
title: "Kursraum",
description:
"Großer, heller Kursraum für Yoga, Pilates, HIIT und weitere Gruppenkurse.",
},
{
title: "Umkleiden & Duschen",
description:
"Moderne Umkleidebereiche mit Schließfächern, Duschen und allem, was Sie brauchen.",
},
{
title: "Lounge",
description:
"Gemütlicher Aufenthaltsbereich zum Ankommen, Austauschen und Entspannen nach dem Training.",
},
] as const;
const GALLERY_ITEMS = [
"Trainingsbereich",
"Cardio-Zone",
"Kursraum",
"Eingangsbereich",
"Functional Area",
"Lounge",
] as const;
export default function StudioPage() {
return (
<main>
{/* Hero */}
<section className="pt-32 pb-[var(--spacing-section)] md:pt-40 md:pb-[var(--spacing-4xl)] bg-primary text-secondary">
<Container>
<FadeIn>
<p className="text-sm uppercase tracking-[0.2em] text-secondary/60 mb-4">
Unser Studio
</p>
</FadeIn>
<FadeIn delay={0.1}>
<h1
className="font-bold max-w-2xl"
style={{
fontSize: "clamp(var(--text-3xl), 4vw, var(--text-5xl))",
lineHeight: "1.1",
letterSpacing: "var(--text-5xl-letter-spacing)",
}}
>
Ihr Trainingsraum in Reutte.
</h1>
</FadeIn>
<FadeIn delay={0.2}>
<p className="mt-6 max-w-xl text-secondary/70" style={{ fontSize: "var(--text-lg)" }}>
Moderne Geräte, durchdachte Räumlichkeiten und eine Atmosphäre,
die zum Training motiviert.
</p>
</FadeIn>
</Container>
</section>
{/* Studio Overview */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)]">
<Container>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<FadeIn>
<div>
<p className="text-sm uppercase tracking-[0.2em] text-muted mb-4">
Räumlichkeiten
</p>
<h2
className="font-bold mb-6"
style={{
fontSize: "var(--text-3xl)",
lineHeight: "var(--text-3xl-line-height)",
letterSpacing: "var(--text-3xl-letter-spacing)",
}}
>
Trainieren mit Blick auf das Wesentliche.
</h2>
<p className="text-muted leading-relaxed mb-4">
Unser Studio verbindet Funktionalität mit Atmosphäre. Auf
großzügiger Fläche finden Sie alles, was Sie für ein
effektives Training brauchen ohne Ablenkung, aber mit Stil.
</p>
<p className="text-muted leading-relaxed">
Hochwertige Geräte namhafter Hersteller, durchdachte
Trainingszonen und eine klare Raumaufteilung sorgen dafür,
dass Sie sich auf das konzentrieren können, was zählt: Ihr
Training.
</p>
</div>
</FadeIn>
<FadeIn delay={0.15}>
<div
className="aspect-[4/3] bg-neutral flex items-center justify-center"
style={{ borderRadius: "var(--radius-md)" }}
>
<p className="text-muted text-sm">PLACEHOLDER: Studio-Übersicht</p>
</div>
</FadeIn>
</div>
</Container>
</section>
{/* Areas */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)] bg-neutral">
<Container>
<FadeIn>
<SectionHeading
title="Unsere Bereiche"
subtitle="Jeder Bereich ist darauf ausgelegt, Ihnen das bestmögliche Trainingserlebnis zu bieten."
/>
</FadeIn>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{AREAS.map((area, index) => (
<FadeIn key={area.title} delay={index * 0.08}>
<Card className="h-full bg-background">
<h3 className="text-base font-bold mb-2">{area.title}</h3>
<p className="text-sm text-muted">{area.description}</p>
</Card>
</FadeIn>
))}
</div>
</Container>
</section>
{/* Gallery */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)]">
<Container>
<FadeIn>
<SectionHeading
title="Einblicke"
subtitle="Werfen Sie einen Blick in unsere Räumlichkeiten."
/>
</FadeIn>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{GALLERY_ITEMS.map((item, index) => (
<FadeIn key={item} delay={index * 0.05}>
<div
className="aspect-[4/3] bg-neutral flex items-center justify-center"
style={{ borderRadius: "var(--radius-md)" }}
>
<p className="text-muted text-sm">PLACEHOLDER: {item}</p>
</div>
</FadeIn>
))}
</div>
</Container>
</section>
{/* CTA */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)] bg-primary text-secondary">
<Container className="text-center">
<FadeIn>
<h2
className="font-bold mb-4"
style={{
fontSize: "var(--text-3xl)",
lineHeight: "var(--text-3xl-line-height)",
}}
>
Überzeugen Sie sich selbst.
</h2>
<p className="text-secondary/70 mb-8 max-w-lg mx-auto">
Vereinbaren Sie einen Termin für ein unverbindliches Probetraining
in unserem Studio.
</p>
<div className="flex flex-col sm:flex-row justify-center gap-4">
<Button
href="/leistungen#kontakt"
variant="secondary"
className="border-secondary text-secondary hover:bg-secondary hover:text-primary"
>
Jetzt Termin buchen
</Button>
<Button
href="/leistungen"
variant="ghost"
className="text-secondary/80 hover:text-secondary"
>
Unsere Leistungen
</Button>
</div>
</FadeIn>
</Container>
</section>
</main>
);
}

231
app/über-uns/page.tsx Normal file
View File

@@ -0,0 +1,231 @@
import { Container } from "@/components/ui/Container";
import { Button } from "@/components/ui/Button";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { Card } from "@/components/ui/Card";
import { FadeIn } from "@/components/ui/FadeIn";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Über Uns",
description:
"Erfahren Sie mehr über unsere Philosophie, unser Team und unsere Community bei Sportbox Reutte.",
openGraph: {
title: "Über Uns | Sportbox Reutte",
description:
"Erfahren Sie mehr über unsere Philosophie, unser Team und unsere Community bei Sportbox Reutte.",
url: "https://sportbox-reutte.at/über-uns",
},
};
const TEAM_MEMBERS = [
{
name: "Max Mustermann",
role: "Gründer & Head Coach",
description:
"Zertifizierter Personal Trainer mit über 10 Jahren Erfahrung in der Fitnessbranche.",
},
{
name: "Anna Berger",
role: "Yoga & Mobility Coach",
description:
"Spezialisiert auf Yoga, Stretching und ganzheitliche Bewegungskonzepte.",
},
{
name: "Thomas Huber",
role: "Kraft- & Konditionstrainer",
description:
"Experte für Krafttraining, Athletiktraining und Wettkampfvorbereitung.",
},
{
name: "Lisa Mayer",
role: "Ernährungsberaterin",
description:
"Diplomierte Ernährungsberaterin mit Fokus auf sportgerechte Ernährung.",
},
] as const;
const VALUES = [
{
title: "Individualität",
description:
"Jeder Mensch ist einzigartig. Unsere Trainingspläne werden individuell auf Sie abgestimmt.",
},
{
title: "Gemeinschaft",
description:
"Gemeinsam erreichen wir mehr. Unsere Community motiviert und unterstützt sich gegenseitig.",
},
{
title: "Qualität",
description:
"Von der Ausbildung unserer Trainer bis zur Auswahl der Geräte wir setzen auf höchste Qualität.",
},
] as const;
export default function UeberUnsPage() {
return (
<main>
{/* Hero */}
<section className="pt-32 pb-[var(--spacing-section)] md:pt-40 md:pb-[var(--spacing-4xl)] bg-primary text-secondary">
<Container>
<FadeIn>
<p className="text-sm uppercase tracking-[0.2em] text-secondary/60 mb-4">
Über uns
</p>
</FadeIn>
<FadeIn delay={0.1}>
<h1
className="font-bold max-w-2xl"
style={{
fontSize: "clamp(var(--text-3xl), 4vw, var(--text-5xl))",
lineHeight: "1.1",
letterSpacing: "var(--text-5xl-letter-spacing)",
}}
>
Wir glauben an die Kraft der Bewegung.
</h1>
</FadeIn>
<FadeIn delay={0.2}>
<p className="mt-6 max-w-xl text-secondary/70" style={{ fontSize: "var(--text-lg)" }}>
Seit der Gründung von Sportbox Reutte verfolgen wir eine klare
Mission: Fitness für jeden zugänglich und persönlich zu gestalten.
</p>
</FadeIn>
</Container>
</section>
{/* Philosophy */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)]">
<Container>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<FadeIn>
<div>
<p className="text-sm uppercase tracking-[0.2em] text-muted mb-4">
Unsere Philosophie
</p>
<h2
className="font-bold mb-6"
style={{
fontSize: "var(--text-3xl)",
lineHeight: "var(--text-3xl-line-height)",
letterSpacing: "var(--text-3xl-letter-spacing)",
}}
>
Fitness ist kein Ziel es ist ein Weg.
</h2>
<p className="text-muted leading-relaxed mb-4">
Bei Sportbox Reutte geht es nicht um kurzfristige Ergebnisse.
Wir begleiten Sie auf einem nachhaltigen Weg zu mehr
Wohlbefinden, Kraft und Lebensqualität.
</p>
<p className="text-muted leading-relaxed">
Unser ganzheitlicher Ansatz verbindet körperliches Training,
mentale Gesundheit und ausgewogene Ernährung. Denn wahre
Fitness entsteht, wenn Körper und Geist im Einklang sind.
</p>
</div>
</FadeIn>
<FadeIn delay={0.15}>
<div
className="aspect-[4/3] bg-neutral flex items-center justify-center"
style={{ borderRadius: "var(--radius-md)" }}
>
<p className="text-muted text-sm">PLACEHOLDER: Team-Bild</p>
</div>
</FadeIn>
</div>
</Container>
</section>
{/* Values */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)] bg-neutral">
<Container>
<FadeIn>
<SectionHeading
title="Unsere Werte"
subtitle="Was uns antreibt und was Sie von uns erwarten können."
/>
</FadeIn>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{VALUES.map((value, index) => (
<FadeIn key={value.title} delay={index * 0.1}>
<Card className="h-full bg-background">
<h3 className="text-lg font-bold mb-3">{value.title}</h3>
<p className="text-sm text-muted">{value.description}</p>
</Card>
</FadeIn>
))}
</div>
</Container>
</section>
{/* Team */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)]">
<Container>
<FadeIn>
<SectionHeading
title="Unser Team"
subtitle="Lernen Sie die Menschen kennen, die Sportbox Reutte ausmachen."
/>
</FadeIn>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{TEAM_MEMBERS.map((member, index) => (
<FadeIn key={member.name} delay={index * 0.1}>
<div className="text-center">
<div
className="w-full aspect-square bg-neutral mb-4 flex items-center justify-center"
style={{ borderRadius: "var(--radius-md)" }}
>
<p className="text-muted text-xs">PLACEHOLDER: Foto</p>
</div>
<h3 className="text-base font-bold">{member.name}</h3>
<p className="text-sm text-muted mt-1">{member.role}</p>
<p className="text-sm text-muted mt-2">
{member.description}
</p>
</div>
</FadeIn>
))}
</div>
</Container>
</section>
{/* CTA */}
<section className="py-[var(--spacing-section)] md:py-[var(--spacing-4xl)] bg-primary text-secondary">
<Container className="text-center">
<FadeIn>
<h2
className="font-bold mb-4"
style={{
fontSize: "var(--text-3xl)",
lineHeight: "var(--text-3xl-line-height)",
}}
>
Werden Sie Teil unserer Community.
</h2>
<p className="text-secondary/70 mb-8 max-w-lg mx-auto">
Kommen Sie vorbei und überzeugen Sie sich selbst von der
Sportbox-Atmosphäre.
</p>
<div className="flex flex-col sm:flex-row justify-center gap-4">
<Button
href="/leistungen#kontakt"
variant="secondary"
className="border-secondary text-secondary hover:bg-secondary hover:text-primary"
>
Jetzt Termin buchen
</Button>
<Button
href="/studio"
variant="ghost"
className="text-secondary/80 hover:text-secondary"
>
Unser Studio entdecken
</Button>
</div>
</FadeIn>
</Container>
</section>
</main>
);
}

View File

@@ -0,0 +1,97 @@
import Link from "next/link";
import { Mail, Phone, MapPin } from "lucide-react";
const NAV_LINKS = [
{ href: "/", label: "Startseite" },
{ href: "/über-uns", label: "Über Uns" },
{ href: "/leistungen", label: "Leistungen" },
{ href: "/studio", label: "Studio" },
{ href: "/aktuelles", label: "Aktuelles" },
] as const;
export function Footer() {
return (
<footer className="bg-primary text-secondary" role="contentinfo">
<div
className="mx-auto px-[var(--spacing-container-padding)] py-16"
style={{ maxWidth: "var(--spacing-container)" }}
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
<div>
<p className="text-xl font-bold tracking-tight mb-4">SPORTBOX</p>
<p className="text-sm text-secondary/70 leading-relaxed">
Ihr Fitnessstudio in Reutte für individuelle Trainingspläne
und persönliche Betreuung.
</p>
</div>
<div>
<p className="text-sm font-bold uppercase tracking-widest mb-4">
Navigation
</p>
<nav aria-label="Footer-Navigation">
<ul className="space-y-2">
{NAV_LINKS.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="text-sm text-secondary/70 transition-colors hover:text-secondary"
>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
</div>
<div>
<p className="text-sm font-bold uppercase tracking-widest mb-4">
Kontakt
</p>
<address className="not-italic space-y-3">
<a
href="mailto:kontakt@sportbox-reutte.at"
className="flex items-center gap-2 text-sm text-secondary/70 transition-colors hover:text-secondary"
>
<Mail size={16} aria-hidden="true" />
kontakt@sportbox-reutte.at
</a>
<a
href="tel:+43123456789"
className="flex items-center gap-2 text-sm text-secondary/70 transition-colors hover:text-secondary"
>
<Phone size={16} aria-hidden="true" />
+43 123 456 789
</a>
<p className="flex items-center gap-2 text-sm text-secondary/70">
<MapPin size={16} aria-hidden="true" />
Reutte, Tirol, Österreich
</p>
</address>
</div>
</div>
<div className="mt-12 pt-8 border-t border-secondary/20 flex flex-col sm:flex-row items-center justify-between gap-4">
<p className="text-xs text-secondary/50">
&copy; {new Date().getFullYear()} Sportbox Reutte. Alle Rechte vorbehalten.
</p>
<div className="flex items-center gap-6">
<Link
href="/impressum"
className="text-xs text-secondary/50 transition-colors hover:text-secondary"
>
Impressum
</Link>
<Link
href="/datenschutz"
className="text-xs text-secondary/50 transition-colors hover:text-secondary"
>
Datenschutz
</Link>
</div>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,97 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Menu, X } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
const NAV_ITEMS = [
{ href: "/", label: "Startseite" },
{ href: "/über-uns", label: "Über Uns" },
{ href: "/leistungen", label: "Leistungen" },
{ href: "/studio", label: "Studio" },
{ href: "/aktuelles", label: "Aktuelles" },
] as const;
export function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const pathname = usePathname();
return (
<header className="fixed top-0 left-0 right-0 z-50 bg-background/95 backdrop-blur-sm border-b border-border">
<div className="mx-auto flex items-center justify-between px-[var(--spacing-container-padding)] py-4" style={{ maxWidth: "var(--spacing-container)" }}>
<Link href="/" className="text-xl font-bold tracking-tight" aria-label="Sportbox Reutte Zur Startseite">
SPORTBOX
</Link>
<nav className="hidden lg:flex items-center gap-8" aria-label="Hauptnavigation">
{NAV_ITEMS.map((item) => (
<Link
key={item.href}
href={item.href}
className={`text-sm tracking-wide transition-colors hover:text-muted ${
pathname === item.href ? "font-bold" : "font-normal"
}`}
>
{item.label}
</Link>
))}
<Link
href="/leistungen#kontakt"
className="bg-primary text-secondary px-5 py-2.5 text-sm font-medium transition-opacity hover:opacity-80"
>
Jetzt Termin buchen
</Link>
</nav>
<button
type="button"
className="lg:hidden p-2 -mr-2"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-expanded={mobileMenuOpen}
aria-controls="mobile-menu"
aria-label={mobileMenuOpen ? "Menü schließen" : "Menü öffnen"}
>
{mobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
<AnimatePresence>
{mobileMenuOpen && (
<motion.nav
id="mobile-menu"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="lg:hidden overflow-hidden border-t border-border bg-background"
aria-label="Mobile Navigation"
>
<div className="flex flex-col px-[var(--spacing-container-padding)] py-4 gap-1">
{NAV_ITEMS.map((item) => (
<Link
key={item.href}
href={item.href}
onClick={() => setMobileMenuOpen(false)}
className={`py-3 text-base transition-colors hover:text-muted ${
pathname === item.href ? "font-bold" : "font-normal"
}`}
>
{item.label}
</Link>
))}
<Link
href="/leistungen#kontakt"
onClick={() => setMobileMenuOpen(false)}
className="mt-2 bg-primary text-secondary px-5 py-3 text-center text-sm font-medium transition-opacity hover:opacity-80"
>
Jetzt Termin buchen
</Link>
</div>
</motion.nav>
)}
</AnimatePresence>
</header>
);
}

View File

@@ -0,0 +1,99 @@
"use client";
import { useState } from "react";
import { ChevronDown } from "lucide-react";
const FAQ_ITEMS = [
{
question: "Brauche ich Vorkenntnisse, um bei Sportbox zu trainieren?",
answer:
"Nein, bei uns sind alle willkommen vom absoluten Anfänger bis zum erfahrenen Sportler. Unsere Trainer erstellen individuelle Trainingspläne und begleiten Sie in Ihrem Tempo.",
},
{
question: "Bieten Sie ein Probetraining an?",
answer:
"Ja, wir bieten ein unverbindliches Probetraining an. Kontaktieren Sie uns einfach über das Kontaktformular oder telefonisch, um einen Termin zu vereinbaren.",
},
{
question: "Welche Öffnungszeiten haben Sie?",
answer:
"Wir haben großzügige Öffnungszeiten, damit Fitness in Ihren Alltag passt. Die aktuellen Öffnungszeiten erfahren Sie bei uns vor Ort oder telefonisch.",
},
{
question: "Welche Kurse bieten Sie an?",
answer:
"Unser Kursangebot umfasst unter anderem Yoga, Pilates, HIIT, Functional Training und Krafttraining. Die Kurse sind für verschiedene Fitnesslevel geeignet. Schauen Sie sich unsere Leistungen an oder fragen Sie direkt bei uns an.",
},
{
question: "Kann ich meinen Trainingsplan anpassen lassen?",
answer:
"Selbstverständlich. Wir passen Ihren Trainingsplan regelmäßig an Ihre Fortschritte und sich verändernde Ziele an. Sprechen Sie einfach Ihren Trainer an.",
},
{
question: "Bieten Sie auch Online-Training an?",
answer:
"Wir arbeiten daran, unser Angebot um Online-Trainingsmöglichkeiten zu erweitern. Kontaktieren Sie uns für aktuelle Informationen.",
},
] as const;
export function FaqSection() {
const [openIndex, setOpenIndex] = useState<number | null>(null);
function toggle(index: number) {
setOpenIndex(openIndex === index ? null : index);
}
return (
<div className="max-w-3xl mx-auto space-y-2">
{FAQ_ITEMS.map((item, index) => (
<div
key={index}
className="border border-border bg-background"
style={{ borderRadius: "var(--radius-md)" }}
>
<button
type="button"
onClick={() => toggle(index)}
aria-expanded={openIndex === index}
className="w-full flex items-center justify-between px-6 py-4 text-left text-sm font-medium"
>
<span>{item.question}</span>
<ChevronDown
size={18}
className={`shrink-0 ml-4 transition-transform duration-200 ${
openIndex === index ? "rotate-180" : ""
}`}
aria-hidden="true"
/>
</button>
{openIndex === index && (
<div className="px-6 pb-4">
<p className="text-sm text-muted leading-relaxed">
{item.answer}
</p>
</div>
)}
</div>
))}
{/* FAQ Schema Markup */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: FAQ_ITEMS.map((item) => ({
"@type": "Question",
name: item.question,
acceptedAnswer: {
"@type": "Answer",
text: item.answer,
},
})),
}),
}}
/>
</div>
);
}

45
components/ui/Button.tsx Normal file
View File

@@ -0,0 +1,45 @@
import Link from "next/link";
interface ButtonProps {
children: React.ReactNode;
href?: string;
variant?: "primary" | "secondary" | "ghost";
className?: string;
type?: "button" | "submit" | "reset";
onClick?: () => void;
}
export function Button({
children,
href,
variant = "primary",
className = "",
type = "button",
onClick,
}: ButtonProps) {
const base =
"inline-flex items-center justify-center px-6 py-3 text-sm font-medium transition-all duration-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary";
const variants = {
primary: "bg-primary text-secondary hover:opacity-80",
secondary:
"border border-primary text-primary hover:bg-primary hover:text-secondary",
ghost: "text-primary underline-offset-4 hover:underline",
};
const classes = `${base} ${variants[variant]} ${className}`;
if (href) {
return (
<Link href={href} className={classes}>
{children}
</Link>
);
}
return (
<button type={type} className={classes} onClick={onClick}>
{children}
</button>
);
}

15
components/ui/Card.tsx Normal file
View File

@@ -0,0 +1,15 @@
interface CardProps {
children: React.ReactNode;
className?: string;
}
export function Card({ children, className = "" }: CardProps) {
return (
<div
className={`border border-border p-6 md:p-8 transition-shadow duration-200 hover:shadow-lg ${className}`}
style={{ borderRadius: "var(--radius-md)" }}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,171 @@
"use client";
import { useState } from "react";
import { Button } from "./Button";
interface FormState {
name: string;
email: string;
phone: string;
message: string;
}
interface FormErrors {
name?: string;
email?: string;
message?: string;
}
export function ContactForm() {
const [form, setForm] = useState<FormState>({
name: "",
email: "",
phone: "",
message: "",
});
const [errors, setErrors] = useState<FormErrors>({});
const [submitted, setSubmitted] = useState(false);
function validate(): FormErrors {
const newErrors: FormErrors = {};
if (!form.name.trim()) newErrors.name = "Bitte geben Sie Ihren Namen ein.";
if (!form.email.trim()) {
newErrors.email = "Bitte geben Sie Ihre E-Mail-Adresse ein.";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) {
newErrors.email = "Bitte geben Sie eine gültige E-Mail-Adresse ein.";
}
if (!form.message.trim())
newErrors.message = "Bitte geben Sie eine Nachricht ein.";
return newErrors;
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const newErrors = validate();
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
setSubmitted(true);
}
}
function handleChange(
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
if (errors[name as keyof FormErrors]) {
setErrors((prev) => ({ ...prev, [name]: undefined }));
}
}
if (submitted) {
return (
<div
className="border border-success p-8 text-center"
style={{ borderRadius: "var(--radius-md)" }}
role="status"
aria-live="polite"
>
<p className="text-lg font-bold mb-2">Vielen Dank für Ihre Nachricht!</p>
<p className="text-muted">
Wir werden uns so schnell wie möglich bei Ihnen melden.
</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} noValidate className="space-y-6">
<div>
<label htmlFor="contact-name" className="block text-sm font-medium mb-1.5">
Name <span aria-hidden="true">*</span>
</label>
<input
id="contact-name"
name="name"
type="text"
autoComplete="name"
aria-required="true"
aria-invalid={!!errors.name}
aria-describedby={errors.name ? "name-error" : undefined}
value={form.name}
onChange={handleChange}
className="w-full border border-border px-4 py-3 text-sm bg-background text-foreground transition-colors focus:border-primary focus:outline-none"
style={{ borderRadius: "var(--radius-sm)" }}
/>
{errors.name && (
<p id="name-error" className="mt-1.5 text-sm text-error" role="alert">
{errors.name}
</p>
)}
</div>
<div>
<label htmlFor="contact-email" className="block text-sm font-medium mb-1.5">
E-Mail <span aria-hidden="true">*</span>
</label>
<input
id="contact-email"
name="email"
type="email"
autoComplete="email"
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? "email-error" : undefined}
value={form.email}
onChange={handleChange}
className="w-full border border-border px-4 py-3 text-sm bg-background text-foreground transition-colors focus:border-primary focus:outline-none"
style={{ borderRadius: "var(--radius-sm)" }}
/>
{errors.email && (
<p id="email-error" className="mt-1.5 text-sm text-error" role="alert">
{errors.email}
</p>
)}
</div>
<div>
<label htmlFor="contact-phone" className="block text-sm font-medium mb-1.5">
Telefon
</label>
<input
id="contact-phone"
name="phone"
type="tel"
autoComplete="tel"
value={form.phone}
onChange={handleChange}
className="w-full border border-border px-4 py-3 text-sm bg-background text-foreground transition-colors focus:border-primary focus:outline-none"
style={{ borderRadius: "var(--radius-sm)" }}
/>
</div>
<div>
<label htmlFor="contact-message" className="block text-sm font-medium mb-1.5">
Nachricht <span aria-hidden="true">*</span>
</label>
<textarea
id="contact-message"
name="message"
rows={5}
aria-required="true"
aria-invalid={!!errors.message}
aria-describedby={errors.message ? "message-error" : undefined}
value={form.message}
onChange={handleChange}
className="w-full border border-border px-4 py-3 text-sm bg-background text-foreground transition-colors focus:border-primary focus:outline-none resize-y"
style={{ borderRadius: "var(--radius-sm)" }}
/>
{errors.message && (
<p id="message-error" className="mt-1.5 text-sm text-error" role="alert">
{errors.message}
</p>
)}
</div>
<Button type="submit" variant="primary">
Nachricht senden
</Button>
</form>
);
}

View File

@@ -0,0 +1,20 @@
interface ContainerProps {
children: React.ReactNode;
className?: string;
as?: "div" | "section" | "article";
}
export function Container({
children,
className = "",
as: Component = "div",
}: ContainerProps) {
return (
<Component
className={`mx-auto px-[var(--spacing-container-padding)] ${className}`}
style={{ maxWidth: "var(--spacing-container)" }}
>
{children}
</Component>
);
}

23
components/ui/FadeIn.tsx Normal file
View File

@@ -0,0 +1,23 @@
"use client";
import { motion } from "framer-motion";
interface FadeInProps {
children: React.ReactNode;
className?: string;
delay?: number;
}
export function FadeIn({ children, className = "", delay = 0 }: FadeInProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.5, delay, ease: "easeOut" }}
className={className}
>
{children}
</motion.div>
);
}

View File

@@ -0,0 +1,31 @@
interface SectionHeadingProps {
title: string;
subtitle?: string;
align?: "left" | "center";
}
export function SectionHeading({
title,
subtitle,
align = "center",
}: SectionHeadingProps) {
return (
<div className={`mb-12 ${align === "center" ? "text-center" : "text-left"}`}>
<h2
className="text-3xl md:text-4xl font-bold tracking-tight"
style={{
fontSize: "var(--text-3xl)",
lineHeight: "var(--text-3xl-line-height)",
letterSpacing: "var(--text-3xl-letter-spacing)",
}}
>
{title}
</h2>
{subtitle && (
<p className="mt-4 text-muted max-w-2xl mx-auto" style={{ fontSize: "var(--text-lg)" }}>
{subtitle}
</p>
)}
</div>
);
}

22
eslint.config.mjs Normal file
View File

@@ -0,0 +1,22 @@
import typescriptParser from "@typescript-eslint/parser";
export default [
{
ignores: [".next/**", "node_modules/**"],
},
{
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaFeatures: { jsx: true },
ecmaVersion: "latest",
sourceType: "module",
},
},
rules: {
"no-console": "warn",
"no-unused-vars": "off",
},
},
];

4904
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,20 +10,24 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"next": "^15.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"framer-motion": "^11.15.0", "framer-motion": "^11.15.0",
"lenis": "^1.1.18", "lenis": "^1.1.18",
"lucide-react": "^0.469.0" "lucide-react": "^0.469.0",
"next": "^15.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.3",
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0", "@types/react-dom": "^19.0.0",
"typescript": "^5.7.0", "@typescript-eslint/parser": "^8.54.0",
"eslint": "^9.39.2",
"eslint-config-next": "^16.1.6",
"tailwindcss": "^4.0.0", "tailwindcss": "^4.0.0",
"@tailwindcss/postcss": "^4.0.0" "typescript": "^5.7.0"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"

4
public/robots.txt Normal file
View File

@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://sportbox-reutte.at/sitemap.xml

View File

@@ -6,6 +6,50 @@
@import "tailwindcss"; @import "tailwindcss";
/* ── Tailwind v4 Theme ───────────────────────────────── */
@theme {
/* Colors */
--color-primary: #000000;
--color-secondary: #FFFFFF;
--color-accent: #d6d6d6;
--color-neutral: #F5F5F5;
--color-background: #FFFFFF;
--color-foreground: #000000;
--color-muted: #B0B0B0;
--color-muted-foreground: #ffffff;
--color-border: #E0E0E0;
--color-success: #4CAF50;
--color-warning: #FF9800;
--color-error: #F44336;
/* Fonts */
--font-heading: 'Inter', sans-serif;
--font-body: 'Inter', sans-serif;
/* Typography Scale override Tailwind defaults to match design tokens */
--text-xl: 1.5rem;
--text-xl--line-height: 2rem;
--text-2xl: 1.75rem;
--text-2xl--line-height: 2.25rem;
--text-3xl: 2rem;
--text-3xl--line-height: 2.5rem;
--text-4xl: 2.5rem;
--text-4xl--line-height: 2.75rem;
--text-5xl: 3rem;
--text-5xl--line-height: 1.1;
--text-6xl: 3.75rem;
--text-6xl--line-height: 1.1;
}
/* ── Font Override (connect next/font to CSS vars) ──── */
/* --font-inter is injected by next/font/google on <html> */
html {
--font-heading: var(--font-inter, 'Inter', sans-serif);
--font-body: var(--font-inter, 'Inter', sans-serif);
}
/* ── Base Resets ───────────────────────────────────── */ /* ── Base Resets ───────────────────────────────────── */
*, *::before, *::after { *, *::before, *::after {
@@ -39,14 +83,10 @@ h1, h2, h3, h4, h5, h6 {
/* ── Links ───────────────────────────────────────── */ /* ── Links ───────────────────────────────────────── */
a { a {
color: var(--color-primary); color: inherit;
text-decoration: none; text-decoration: none;
} }
a:hover {
text-decoration: underline;
}
/* ── Images ──────────────────────────────────────── */ /* ── Images ──────────────────────────────────────── */
img, video, svg { img, video, svg {
@@ -68,3 +108,15 @@ img, video, svg {
background-color: var(--color-primary); background-color: var(--color-primary);
color: var(--color-background); color: var(--color-background);
} }
/* ── Reduced Motion ─────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}