diff --git a/.env.example b/.env.example index d8f292f7..db14d044 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,12 @@ WORDPRESS_HOSTNAME="wordpress.com" # If using the revalidate plugin # You can generate by running `openssl rand -base64 32` in the terminal WORDPRESS_WEBHOOK_SECRET="your-secret-key-here" + +# Terminannahme (server-only, niemals im Browser verwenden) +APPOINTMENT_API_TOKEN="your-appointment-api-token" +APPOINTMENT_SMTP_HOST="smtp.example.de" +APPOINTMENT_SMTP_USER="termin@example.de" +APPOINTMENT_SMTP_PASSWORD="your-smtp-password" + +# Öffentliche kanonische URL +NEXT_PUBLIC_SITE_URL="https://www.fahrschule-bz-ucar.de" diff --git a/app/api/appointments/route.ts b/app/api/appointments/route.ts new file mode 100644 index 00000000..66eb6c89 --- /dev/null +++ b/app/api/appointments/route.ts @@ -0,0 +1,22 @@ +import { NextResponse } from "next/server"; + +const requiredFields = ["name", "email", "phone", "license", "date"] as const; + +export async function POST(request: Request) { + const payload = (await request.json().catch(() => null)) as Record | null; + if (!payload || requiredFields.some((field) => typeof payload[field] !== "string" || !payload[field])) { + return NextResponse.json({ message: "Bitte fülle alle Pflichtfelder aus." }, { status: 400 }); + } + + // TODO: WordPress REST endpoint, SMTP and calendar credentials belong in server-only env vars. + // Example boundary: POST `${WORDPRESS_URL}/wp-json/bz-ucar/v1/appointments`. + if (!process.env.WORDPRESS_URL || !process.env.APPOINTMENT_API_TOKEN) { + return NextResponse.json( + { message: "Die Terminannahme ist noch nicht konfiguriert. Bitte ruf uns direkt an: 030 123 45 67." }, + { status: 503 }, + ); + } + + // TODO: Send the validated payload to WordPress, notify via SMTP and persist admin tracking ID. + return NextResponse.json({ message: "Anfrage erfolgreich übermittelt." }, { status: 202 }); +} diff --git a/app/datenschutz/page.tsx b/app/datenschutz/page.tsx new file mode 100644 index 00000000..d8d606cc --- /dev/null +++ b/app/datenschutz/page.tsx @@ -0,0 +1,3 @@ +export default function Datenschutz() { + return

Datenschutzerklärung

Hier informieren wir dich darüber, welche personenbezogenen Daten bei einer Terminanfrage verarbeitet werden.

Platzhalter für die finale Datenschutzerklärung: Bitte vor dem Livegang durch eine rechtlich geprüfte Fassung und die tatsächlichen WordPress-, SMTP- und Kalenderanbieter ersetzen.

; +} diff --git a/app/globals.css b/app/globals.css index e1dc4f9b..696baf1c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -176,3 +176,159 @@ @apply bg-background text-foreground; } } + +html { + scroll-behavior: smooth; +} + +.form-field { + width: 100%; + border: 1px solid #d8e2dd; + border-radius: 0.85rem; + background: #fbfcfa; + padding: 0.8rem 0.95rem; + color: #09090b; + outline: none; + transition: border-color 150ms ease, box-shadow 150ms ease; +} + +.form-field:focus { + border-color: #09090b; + box-shadow: 0 0 0 3px rgb(29 95 87 / 12%); +} + +.modern-site { + font-family: var(--font-sans), "Inter", ui-sans-serif, system-ui, sans-serif; +} + +.modern-site h1, +.modern-site h2, +.modern-site h3 { + text-wrap: balance; +} + +.course-card { + min-height: 26rem; + transition: transform 180ms ease, box-shadow 180ms ease; +} + +.course-card:hover { + transform: translateY(-0.35rem); + box-shadow: 0 1.5rem 3rem rgb(16 35 43 / 12%); +} + +.course-card-lime { + background: #ef233c; + color: #09090b; +} + +.course-card-coral { + background: #d90429; + color: #fff; +} + +.course-card-blue { + background: #f4f4f5; + color: #09090b; +} + +.scene-city { + background: + linear-gradient(90deg, transparent 0 3%, #27272a 3% 10%, transparent 10% 12%, #27272a 12% 20%, transparent 20% 23%, #27272a 23% 31%, transparent 31% 34%, #27272a 34% 44%, transparent 44% 47%, #27272a 47% 57%, transparent 57% 60%, #27272a 60% 69%, transparent 69% 73%, #27272a 73% 84%, transparent 84% 87%, #27272a 87% 96%, transparent 96%); + clip-path: polygon(0 40%, 3% 40%, 3% 8%, 10% 8%, 10% 34%, 12% 34%, 12% 14%, 20% 14%, 20% 44%, 23% 44%, 23% 23%, 31% 23%, 31% 38%, 34% 38%, 34% 5%, 44% 5%, 44% 35%, 47% 35%, 47% 17%, 57% 17%, 57% 45%, 60% 45%, 60% 10%, 69% 10%, 69% 38%, 73% 38%, 73% 19%, 84% 19%, 84% 43%, 87% 43%, 87% 7%, 96% 7%, 96% 38%, 100% 38%, 100% 100%, 0 100%); +} + +.scene-cloud { + position: absolute; + height: 3rem; + width: 9rem; + border-radius: 999px; + background: rgb(255 255 255 / 10%); + filter: blur(1px); +} + +.scene-cloud::before, +.scene-cloud::after { + position: absolute; + bottom: 0; + content: ""; + border-radius: 999px; + background: inherit; +} + +.scene-cloud::before { + left: 1.5rem; + height: 4rem; + width: 4rem; +} + +.scene-cloud::after { + right: 1.3rem; + height: 3.5rem; + width: 3.5rem; +} + +.scene-cloud-one { + left: 18%; + top: 22%; + animation: cloud-drift 18s ease-in-out infinite alternate; +} + +.scene-cloud-two { + right: 12%; + top: 36%; + transform: scale(.65); + animation: cloud-drift 23s ease-in-out -5s infinite alternate-reverse; +} + +.scene-bus { + animation: bus-arrival 12s ease-in-out infinite; +} + +.scene-person { + animation: passenger-exit 12s ease-in-out infinite; +} + +.scene-car { + animation: training-car-exit 12s ease-in-out infinite; +} + +.hero-reference-video { + filter: saturate(.9) contrast(1.05); +} + +.hero-video-overlay { + background: linear-gradient(135deg, rgb(13 39 48 / 30%), rgb(8 26 32 / 62%)); +} + +@keyframes bus-arrival { + 0%, 8% { transform: translateX(0); } + 25%, 58% { transform: translateX(190%); } + 70%, 100% { transform: translateX(480%); } +} + +@keyframes passenger-exit { + 0%, 24% { opacity: 0; transform: translateY(1rem); } + 34%, 55% { opacity: 1; transform: translateY(0); } + 65%, 100% { opacity: 0; transform: translate(4rem, 0); } +} + +@keyframes training-car-exit { + 0%, 48% { transform: translateX(0); } + 62%, 100% { transform: translateX(300%); } +} + +@keyframes cloud-drift { + from { transform: translateX(-1rem); } + to { transform: translateX(2rem); } +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + .scene-bus, .scene-person, .scene-car, .scene-cloud-one, .scene-cloud-two { + animation: none; + } + .scene-bus { transform: translateX(190%); } + .scene-person { opacity: 1; } + .scene-car { transform: translateX(80%); } +} diff --git a/app/impressum/page.tsx b/app/impressum/page.tsx new file mode 100644 index 00000000..193392e9 --- /dev/null +++ b/app/impressum/page.tsx @@ -0,0 +1,3 @@ +export default function Impressum() { + return

Impressum

Fahrschule Bz Ucar
Musterstraße 12
12345 Berlin

Vertreten durch: [Name der verantwortlichen Person]

Telefon: [Telefonnummer]
E-Mail: [E-Mail-Adresse]

Hinweis: Diese Platzhalter müssen vor dem produktiven Start mit den vollständigen Anbieterangaben ergänzt werden.

; +} diff --git a/app/layout.tsx b/app/layout.tsx index 2fc94497..b445a8c0 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -17,9 +17,11 @@ const font = FontSans({ }); export const metadata: Metadata = { - title: "WordPress & Next.js Starter by 9d8", - description: - "A starter template for Next.js with WordPress as a headless CMS.", + title: { + default: "Fahrschule Bz Ucar | Sicher ans Ziel", + template: "%s | Fahrschule Bz Ucar", + }, + description: siteConfig.site_description, metadataBase: new URL(siteConfig.site_domain), alternates: { canonical: "/", @@ -32,7 +34,7 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - +
-

404 - Page Not Found

+

404 – Seite nicht gefunden

- Sorry, the page you are looking for does not exist. + Die gesuchte Seite existiert leider nicht.

diff --git a/app/page.tsx b/app/page.tsx index c6290b24..43d58ba5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,146 +1,83 @@ -// Craft Imports -import { Section, Container, Prose } from "@/components/craft"; - -// Next.js Imports import Link from "next/link"; +import { ArrowDownRight, ArrowRight, Check, MessageCircle } from "lucide-react"; +import { BookingForm } from "@/components/home/booking-form"; +import { HeroScene } from "@/components/home/hero-scene"; + +const courses = [ + { number: "01", title: "Klasse B", description: "Dein Weg zum Pkw-Führerschein. Klar strukturiert, flexibel geplant und mit modernen Ausbildungsfahrzeugen.", tone: "lime" }, + { number: "02", title: "Klasse A", description: "Mehr Freiheit auf zwei Rädern. Sicher trainieren, selbstbewusst starten und jede Kurve genießen.", tone: "coral" }, + { number: "03", title: "Klasse BE", description: "Mehr Möglichkeiten im Alltag und Beruf. Anhänger fahren lernen, ohne Stress und mit System.", tone: "blue" }, +]; -// Icons -import { File, Pen, Tag, Diamond, User, Folder } from "lucide-react"; -import { WordPressIcon } from "@/components/icons/wordpress"; -import { NextJsIcon } from "@/components/icons/nextjs"; +const benefits = [ + { title: "Persönliche Begleitung", text: "Wir kennen deinen Namen, deinen Fortschritt und dein Ziel." }, + { title: "Dein Rhythmus", text: "Theorie und Fahrstunden passen in deinen Alltag." }, + { title: "Sicher lernen", text: "Ruhige Erklärungen, moderne Fahrzeuge und klare Schritte." }, +]; -// This page is using the craft.tsx component and design system export default function Home() { return ( -
- - - -
- ); -} +
+
+
+
+
+ + Fahrschule Bz Ucar. + +
+ Führerscheine + Informationen + Kontakt +
+ Anfrage senden +
+
+
+

Fahrschule in Berlin

+

Fahr los.
Wachs über dich hinaus.

+

Dein Führerschein ist mehr als ein Stück Papier. Er ist der Moment, in dem du selbst entscheidest, wohin es geht.

+
+ Probestunde buchen + 4.9 aus 180+ Bewertungen +
+
+
+ +
Nächster SchrittKontakt
+
+
+
+
-// This is just some example TSX -const ToDelete = () => { - return ( -
- -

Headless WordPress built with the Next.js

+
+
+ 12+ Jahre ErfahrungFlexible FahrstundenModerne FahrzeugeBerlin & Umgebung +
+
-

- This is next-wp, - created as a way to build WordPress sites with Next.js at rapid speed. - This starter is designed with{" "} - shadcn/ui,{" "} - craft-ds, and Tailwind CSS. Use{" "} - brijr/components to build your - site with prebuilt components. The data fetching and typesafety is - handled in lib/wordpress.ts and{" "} - lib/wordpress.d.ts. -

-
+
+
+

Dein Weg

Welcher Führerschein
passt zu dir?

Wir finden gemeinsam die passende Ausbildung für deine Ziele und deinen Alltag.

+
{courses.map((course) =>
{course.number}

{course.title}

{course.description}

Kontakt
)}
+
+
-
- {/* Vercel Clone Starter */} -
- - {/* eslint-disable-next-line */} - Deploy with Vercel - -

- Deploy with Vercel in seconds. -

+
+
+

Warum Fahrschule Bz Ucar?

Nicht nur
bestehen.
Verstehen.

Eine gute Fahrschule bringt dir nicht nur bei, wie du die Prüfung bestehst. Sie gibt dir Sicherheit für alle Wege danach.

+
{benefits.map(({ title, text }, index) =>
0{index + 1}

{title}

{text}

)}
+
-
- - +
+
+

Dein erster Schritt

Lust auf
Führerschein?

Vereinbare jetzt deine kostenlose Probestunde. Ganz unverbindlich - wir freuen uns auf dich!

Persönliche Beratung inklusive

Antwort innerhalb von 24 Stunden

Keine versteckten Kosten

+
-
+ -
- - - - Posts{" "} - - All posts from your WordPress - - - - - - - Pages{" "} - - Custom pages from your WordPress - - - - - - - Authors{" "} - - List of the authors from your WordPress - - - - - - - Tags{" "} - - Content by tags from your WordPress - - - - - - - Categories{" "} - - Categories from your WordPress - - - - - - - Documentation{" "} - - How to use `next-wp` - - - -
+

Du hast noch Fragen?

Schreib uns. Wir sind da.

Kontakt aufnehmen
); -}; +} diff --git a/components/home/booking-form.tsx b/components/home/booking-form.tsx new file mode 100644 index 00000000..8415dc94 --- /dev/null +++ b/components/home/booking-form.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { FormEvent, useState } from "react"; + +type FormState = "idle" | "sending" | "success" | "error"; + +const initialForm = { name: "", email: "", phone: "", license: "B", date: "", message: "" }; + +export function BookingForm() { + const [form, setForm] = useState(initialForm); + const [state, setState] = useState("idle"); + const [error, setError] = useState(""); + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + setState("sending"); + setError(""); + + try { + const response = await fetch("/api/appointments", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(form), + }); + const result = (await response.json()) as { message?: string }; + if (!response.ok) throw new Error(result.message ?? "Bitte prüfe deine Angaben."); + setState("success"); + setForm(initialForm); + } catch (submissionError) { + setState("error"); + setError(submissionError instanceof Error ? submissionError.message : "Die Anfrage konnte nicht gesendet werden."); + } + } + + function updateField(field: keyof typeof initialForm, value: string) { + setForm((current) => ({ ...current, [field]: value })); + } + + if (state === "success") { + return ( +
+ ✓ +

Danke für deine Anfrage!

+

Wir melden uns schnellstmöglich persönlich bei dir, um deinen Termin zu bestätigen.

+ +
+ ); + } + + return ( +
+
+ + +
+
+ + +
+ +