From d1c7c1333f70b1ca5193d19085f685fce2f0efeb Mon Sep 17 00:00:00 2001 From: erenturan90 <211200127+erenturan90@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:33:52 +0200 Subject: [PATCH 1/7] feat: build Fahrschule Bz Ucar website Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .env.example | 9 ++ app/api/appointments/route.ts | 22 ++++ app/datenschutz/page.tsx | 3 + app/globals.css | 107 +++++++++++++++++++ app/impressum/page.tsx | 3 + app/layout.tsx | 10 +- app/not-found.tsx | 6 +- app/page.tsx | 175 ++++++++----------------------- components/home/booking-form.tsx | 83 +++++++++++++++ components/home/hero-scene.tsx | 82 +++++++++++++++ components/layout/footer.tsx | 32 ++---- components/layout/nav.tsx | 21 ++-- components/nav/mobile-nav.tsx | 8 +- menu.config.ts | 12 +-- site.config.ts | 7 +- 15 files changed, 390 insertions(+), 190 deletions(-) create mode 100644 app/api/appointments/route.ts create mode 100644 app/datenschutz/page.tsx create mode 100644 app/impressum/page.tsx create mode 100644 components/home/booking-form.tsx create mode 100644 components/home/hero-scene.tsx 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..7224f210 100644 --- a/app/globals.css +++ b/app/globals.css @@ -176,3 +176,110 @@ @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: #10232b; + outline: none; + transition: border-color 150ms ease, box-shadow 150ms ease; +} + +.form-field:focus { + border-color: #1d5f57; + box-shadow: 0 0 0 3px rgb(29 95 87 / 12%); +} + +.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 10s ease-in-out infinite; +} + +.scene-person { + animation: passenger-exit 10s ease-in-out infinite; +} + +.scene-car { + animation: training-car-exit 10s ease-in-out infinite; +} + +@keyframes bus-arrival { + 0%, 8% { transform: translateX(0); } + 25%, 58% { transform: translateX(155%); } + 70%, 100% { transform: translateX(430%); } +} + +@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(270%); } +} + +@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(155%); } + .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..baf4faf9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,146 +1,53 @@ -// Craft Imports -import { Section, Container, Prose } from "@/components/craft"; - -// Next.js Imports import Link from "next/link"; +import { ArrowRight, Check, Clock3, HeartHandshake, Star, Users } from "lucide-react"; +import { BookingForm } from "@/components/home/booking-form"; +import { HeroScene } from "@/components/home/hero-scene"; -// Icons -import { File, Pen, Tag, Diamond, User, Folder } from "lucide-react"; -import { WordPressIcon } from "@/components/icons/wordpress"; -import { NextJsIcon } from "@/components/icons/nextjs"; +const courses = [ + { label: "Klasse B", title: "Dein Pkw-Führerschein", text: "Flexibel lernen, sicher fahren und mit einem guten Gefühl in die Prüfung.", color: "bg-[#e8f4ee]" }, + { label: "Klasse A", title: "Freiheit auf zwei Rädern", text: "Motorradfahren mit erfahrenen Fahrlehrerinnen und Fahrlehrern.", color: "bg-[#fff2d7]" }, + { label: "Klasse BE", title: "Mehr Platz, mehr Möglichkeiten", text: "Anhänger sicher bewegen – im Alltag und im Beruf.", color: "bg-[#e9eff7]" }, +]; -// This page is using the craft.tsx component and design system export default function Home() { return ( -
- - - -
- ); -} - -// This is just some example TSX -const ToDelete = () => { - return ( -
- -

Headless WordPress built with the Next.js

- -

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

-
+
+
+
+

Deine Fahrschule in Berlin

+

Sicher ans Ziel.
Mit Bz Ucar.

+

Führerschein machen, Freiheit gewinnen. Wir begleiten dich persönlich, geduldig und mit einem modernen Ausbildungskonzept.

+
+ Probestunde buchen + Führerscheine entdecken +
+
+
4.9/5 aus über 180 Bewertungen
+
12+Jahre Erfahrung
+
+
+ +
-
- {/* Vercel Clone Starter */} -
- - {/* eslint-disable-next-line */} - Deploy with Vercel - -

- Deploy with Vercel in seconds. -

+
+
+

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.label}

{course.title}

{course.text}

Mehr erfahren
)}
+
-
- - +
+
+

Warum Bz Ucar?

Lernen, das
zu dir passt.

Wir glauben: Gute Ausbildung beginnt mit Vertrauen. Deshalb nehmen wir uns Zeit, hören zu und bringen dich Schritt für Schritt sicher ans Ziel.

+
{[{ icon: HeartHandshake, title: "Persönlich", text: "Fahrlehrer, die dich wirklich kennen und individuell begleiten." }, { icon: Clock3, title: "Flexibel", text: "Theorie und Praxis, die in deinen Alltag und deinen Rhythmus passen." }, { icon: ShieldCheckIcon, title: "Sicher", text: "Moderne Fahrzeuge und eine Ausbildung mit klarer Struktur." }, { icon: Users, title: "Gemeinsam", text: "Eine starke Community, die dich motiviert und unterstützt." }].map(({ icon: Icon, title, text }) =>

{title}

{text}

)}
-
+ -
- - - - 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` - - - -
+

Dein erster Schritt

Lust auf
Führerschein?

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

Persönliche Beratung inklusive

Keine versteckten Kosten

Antwort innerhalb von 24 Stunden

); -}; +} + +function ShieldCheckIcon(props: { className?: string; size?: number }) { + return ; +} diff --git a/components/home/booking-form.tsx b/components/home/booking-form.tsx new file mode 100644 index 00000000..2947b4fe --- /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 ( +
+
+ + +
+
+ + +
+ +