From 3bda7d6eb6d1b9e6228aaf85392c4aa7ce0a3cf8 Mon Sep 17 00:00:00 2001 From: Engin Date: Thu, 10 Sep 2026 14:31:05 +0200 Subject: [PATCH] update --- app/branchenloesungen/[slug]/page.module.css | 343 +++++++++++++++++++ app/branchenloesungen/[slug]/page.tsx | 226 +++++++++++- app/branchenloesungen/data.ts | 272 ++++++++++++++- app/kontakt/actions.ts | 93 +++++ app/kontakt/page.module.css | 189 ++++++++++ app/kontakt/page.tsx | 82 ++++- app/leistungen/[slug]/page.tsx | 74 +++- app/leistungen/data.ts | 34 ++ app/ueberuns/loading.tsx | 7 - app/ueberuns/page.module.css | 207 +++++++++++ app/ueberuns/page.tsx | 128 ++++++- components/BentoGallery.module.css | 105 ++++++ components/BentoGallery.tsx | 38 ++ components/ContactForm.module.css | 100 ++++++ components/ContactForm.tsx | 213 ++++++++++++ components/StaggeredGallery.module.css | 95 +++++ components/StaggeredGallery.tsx | 38 ++ components/faq/faqData.ts | 111 ++++++ lib/mailer.ts | 13 + package-lock.json | 10 + package.json | 1 + public/images/branchen/gallery/1.jpg | Bin 0 -> 1943176 bytes public/images/branchen/gallery/2.jpg | Bin 0 -> 82000 bytes public/images/branchen/gallery/3.jpg | Bin 0 -> 24527 bytes public/images/branchen/gallery/4.jpg | Bin 0 -> 42714 bytes public/images/branchen/gallery/5.jpg | Bin 0 -> 46397 bytes public/images/branchen/gallery/6.jpg | Bin 0 -> 46582 bytes public/images/contact.jpg | Bin 0 -> 57610 bytes 28 files changed, 2330 insertions(+), 49 deletions(-) create mode 100644 app/kontakt/actions.ts create mode 100644 app/kontakt/page.module.css delete mode 100644 app/ueberuns/loading.tsx create mode 100644 components/BentoGallery.module.css create mode 100644 components/BentoGallery.tsx create mode 100644 components/ContactForm.module.css create mode 100644 components/ContactForm.tsx create mode 100644 components/StaggeredGallery.module.css create mode 100644 components/StaggeredGallery.tsx create mode 100644 lib/mailer.ts create mode 100644 public/images/branchen/gallery/1.jpg create mode 100644 public/images/branchen/gallery/2.jpg create mode 100644 public/images/branchen/gallery/3.jpg create mode 100644 public/images/branchen/gallery/4.jpg create mode 100644 public/images/branchen/gallery/5.jpg create mode 100644 public/images/branchen/gallery/6.jpg create mode 100644 public/images/contact.jpg diff --git a/app/branchenloesungen/[slug]/page.module.css b/app/branchenloesungen/[slug]/page.module.css index e69de29..b964425 100644 --- a/app/branchenloesungen/[slug]/page.module.css +++ b/app/branchenloesungen/[slug]/page.module.css @@ -0,0 +1,343 @@ +.hero { + background: var(--color-background-linear); + padding-block: var(--space-16); + position: relative; + overflow: hidden; +} + +.inner { + max-width: var(--container-width); + margin-inline: auto; + padding-inline: var(--space-4); + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + gap: var(--space-2); + align-items: left; +} + +.decoration { + position: absolute; + top: var(--space-8); + right: var(--space-8); + display: none; + grid-template-columns: repeat(6, 1fr); + grid-template-rows: repeat(3, 1fr); + gap: var(--space-3); + pointer-events: none; + z-index: 0; +} + +.dot { + width: 14px; + height: 14px; + border-radius: var(--radius-full); + background-color: var(--color-accent); + opacity: 0.8; +} + +.heading { + color: var(--color-heading); + max-width: 20ch; + display: flex; + flex-direction: column; + gap: var(--space-1); + text-align: left; + align-self: left; + position: relative; + z-index: 2; +} + +.headingSub { + font-size: var(--font-size-subhead); + font-weight: var(--font-weight-medium); + letter-spacing: var(--letter-spacing-tight); +} + +.content { + display: flex; + flex-direction: column; + gap: var(--space-6); +} + +.imageWrapper { + display: none; + position: relative; + width: 100%; + margin-inline: auto; + aspect-ratio: 21 / 9; + flex-shrink: 0; + z-index: 1; +} + +.imageWrapper::before { + content: ""; + position: absolute; + left: 8%; + right: 8%; + bottom: -4px; + height: 28%; + background: color-mix(in srgb, var(--color-accent) 55%, transparent); + filter: blur(22px); + opacity: 0.45; + border-radius: 999px; + z-index: -1; + pointer-events: none; +} + +.imageFrame { + position: relative; + width: 100%; + height: 100%; + border-radius: var(--radius-lg); + overflow: hidden; + box-shadow: + 0 10px 30px rgba(0, 0, 0, 0.08), + 0 24px 60px rgba(0, 0, 0, 0.06); +} + +.image { + object-fit: cover; +} + +.body { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--space-6); + text-align: left; +} + +.lead { + font-size: var(--font-size-base); + text-align: left; + width: min(100%, 48ch); +} + +/* --- Challenges Section: 1:1 Optik von MaintenanceCleaningSection --- */ +.challengesSection { + background: var(--color-background-linear); + padding-block: var(--space-16); +} + +.challengesInner { + max-width: var(--container-width); + margin-inline: auto; + padding-inline: var(--space-4); +} + +.challengesHeading { + max-width: 24ch; + padding: 0 0 8px 0; + color: var(--color-heading); +} + +.challengesLead { + margin-top: var(--space-3); + font-size: var(--font-size-base); + max-width: 48ch; +} + +.challengesList { + margin-top: var(--space-6); + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: var(--space-5); + list-style: none; + padding: 0; +} + +.challengesItem { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: var(--space-4); + background: var(--color-surface-muted, #e6ede9); + border-radius: 12px; + padding: var(--space-5); +} + +.challengesIcon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 24px; + height: 24px; + color: var(--color-accent); +} + +.challengesText { + color: var(--font-heading-color); + font-size: var(--font-size-xs); +} + +/* --- Related Services Section --- */ +.servicesSection { + padding-block: var(--space-16); +} + +.servicesInner { + max-width: var(--container-width); + margin-inline: auto; + padding-inline: var(--space-4); +} + +.servicesHeading { + max-width: 32ch; + color: var(--color-heading); +} + +.servicesLead { + margin-top: var(--space-3); + max-width: 60ch; + font-size: var(--font-size-base); +} + +.servicesGrid { + margin-top: var(--space-8); + display: grid; + grid-template-columns: 1fr; + gap: var(--space-6); +} + +.serviceCard { + display: flex; + flex-direction: column; + border-radius: var(--radius-lg); + overflow: hidden; + background: #fff; + box-shadow: + 0 10px 30px rgba(0, 0, 0, 0.06), + 0 2px 8px rgba(0, 0, 0, 0.04); + text-decoration: none; + color: inherit; + transition: + transform 0.2s ease, + box-shadow 0.2s ease; +} + +.serviceCard:hover { + transform: translateY(-4px); + box-shadow: + 0 16px 40px rgba(0, 0, 0, 0.1), + 0 4px 12px rgba(0, 0, 0, 0.06); +} + +.serviceCardImageWrapper { + position: relative; + width: 100%; + aspect-ratio: 4 / 3; +} + +.serviceCardImage { + object-fit: cover; +} + +.serviceCardBody { + padding: var(--space-4); + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.serviceCardTitle { + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + color: var(--color-heading); +} + +.serviceCardLink { + display: inline-flex; + align-items: center; + gap: var(--space-1); + font-size: var(--font-size-small, 0.875rem); + font-weight: var(--font-weight-medium); + color: var(--color-accent); +} + +/* Tablet: 768px */ +@media (min-width: 768px) { + .content { + flex-direction: row; + align-items: flex-start; + gap: var(--space-8); + } + + .inner { + align-items: flex-start; + gap: var(--space-8); + } + + .heading { + text-align: left; + align-self: flex-start; + } + + .body { + align-items: flex-start; + text-align: left; + } + + .lead { + text-align: left; + margin-inline: 0; + } + + .decoration { + display: grid; + top: -24px; + } + + .imageWrapper { + display: block; + width: 260px; + aspect-ratio: 4 / 3; + } + + .dot { + width: 24px; + height: 24px; + } + + .challengesSection { + padding-block: var(--space-24); + } + + .challengesHeading, + .challengesLead { + text-align: left; + } + + .challengesList { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .servicesSection { + padding-block: var(--space-24); + } + + .servicesGrid { + grid-template-columns: repeat(3, 1fr); + } +} + +/* Desktop: 1024px */ +@media (min-width: 1024px) { + .hero { + padding-block: var(--space-36); + } + + .decoration { + top: -42px; + } + + .challengesInner { + padding-inline: var(--space-6); + } + + .challengesList { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} diff --git a/app/branchenloesungen/[slug]/page.tsx b/app/branchenloesungen/[slug]/page.tsx index aca9816..d18c285 100644 --- a/app/branchenloesungen/[slug]/page.tsx +++ b/app/branchenloesungen/[slug]/page.tsx @@ -1,34 +1,234 @@ import { notFound } from "next/navigation"; +import { branchen } from "../data"; +import { leistungen } from "../../leistungen/data"; import Image from "next/image"; -import { branchenloesungen } from "../data"; +import Link from "next/link"; +import { + ArrowRightIcon, + CheckCircleIcon, +} from "@phosphor-icons/react/dist/ssr"; +import styles from "./page.module.css"; +import StaggeredGallery from "@/components/StaggeredGallery"; +import FaqSection from "@/components/faq/FaqSection"; +import { branchenFaq } from "@/components/faq/faqData"; +import ContactCtaSection from "@/components/contact/ContactCtaSection"; +import BentoGallery from "@/components/BentoGallery"; + +import type { Metadata } from "next"; + +const siteUrl = "https://basiclean.de"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }>; +}): Promise { + const { slug } = await params; + const branche = branchen.find((item) => item.slug === slug); + + if (!branche) { + return { + title: "Nicht gefunden", + robots: { index: false, follow: false }, + }; + } + + const hasCustomSeo = Boolean(branche.metaTitle); + + const title = hasCustomSeo + ? { absolute: branche.metaTitle as string } + : branche.titleMain; + + const flatTitle = hasCustomSeo + ? (branche.metaTitle as string) + : branche.titleMain; + const description = branche.metaDescription ?? branche.text; + const url = `${siteUrl}/branchenloesungen/${branche.slug}`; + const ogImageUrl = branche.ogImage ?? branche.image; + + return { + title, + description, + alternates: { + canonical: url, + }, + openGraph: { + type: "website", + siteName: "Basiclean", + locale: "de_DE", + title: flatTitle, + description, + url, + images: [ + { + url: ogImageUrl, + alt: branche.imageAlt, + width: 1200, + height: 628, + }, + ], + }, + twitter: { + card: "summary_large_image", + site: "@basiclean", + title: flatTitle, + description, + images: [{ url: ogImageUrl, alt: branche.imageAlt }], + }, + }; +} export function generateStaticParams() { - return branchenloesungen.map((item) => ({ + return branchen.map((item) => ({ slug: item.slug, })); } -export default async function LeistungDetail({ +export default async function BrancheDetail({ params, }: { params: Promise<{ slug: string }>; }) { const { slug } = await params; - const leistung = branchenloesungen.find((item) => item.slug === slug); - if (!leistung) notFound(); + const branche = branchen.find((item) => item.slug === slug); + const faqEntry = branchenFaq.find((item) => item.slug === branche?.faqSlug); + + if (!branche) { + notFound(); + } + + const empfohleneLeistungen = branche.relatedServices.slugs + .map((leistungSlug) => + leistungen.find((item) => item.slug === leistungSlug), + ) + .filter((item): item is (typeof leistungen)[number] => Boolean(item)); return (
-

{leistung.title}

-

{leistung.text}

+
+
+ - {leistung.title} +

+ {branche.titleMain} + {branche.titleSub} +

+ +
+
+
+ {branche.imageAlt} +
+
+ +
+

{branche.text}

+ + + + Jetzt Angebot anfragen + +
+
+
+
+ + + +
+
+

+ {branche.challengesSection.heading} +

+

+ {branche.challengesSection.lead} +

+ +
+ {branche.challengesSection.points.map((point) => ( +
+ +

{point}

+
+ ))} +
+
+
+ + {empfohleneLeistungen.length > 0 && ( +
+
+

+ {branche.relatedServices.heading} +

+

+ {branche.relatedServices.lead} +

+ +
+ {empfohleneLeistungen.map((leistung) => ( + +
+ {leistung.imageAlt} +
+
+

+ {leistung.titleMain} +

+ + Mehr erfahren + +
+ + ))} +
+
+
+ )} + +
+ +
+ +
+ +
); } diff --git a/app/branchenloesungen/data.ts b/app/branchenloesungen/data.ts index 24c30e6..73ca879 100644 --- a/app/branchenloesungen/data.ts +++ b/app/branchenloesungen/data.ts @@ -1,26 +1,266 @@ -export const branchenloesungen = [ +export const branchen = [ { slug: "bueros", - title: "Büros", - text: "Glas- und Fensterreinigung", - image: "/images/leistungen/webdesign.jpg", - }, - { - slug: "kanzlei", - title: "Kanzlei", - text: "Unterhaltsreinigung", - image: "/images/leistungen/seo.jpg", + titleMain: "Reinigung für Büros", + titleSub: "in Hannover und Umgebung", + text: "Ein gepflegtes Büro prägt den Eindruck bei Kunden, Bewerbern und dem eigenen Team. Wir reinigen Arbeitsplätze, Besprechungsräume, Empfangsbereiche und Sanitäranlagen zuverlässig und abgestimmt auf Ihre Betriebsabläufe.", + image: "/images/branchen/buero.jpg", + imageAlt: "Gepflegter Büroarbeitsplatz nach der Reinigung", + + // --- SEO --- + metaTitle: "Büroreinigung Hannover – für Unternehmen jeder Größe", + metaDescription: + "Professionelle Reinigung für Büros in Hannover: Arbeitsplätze, Empfang und Sanitärbereiche zuverlässig gepflegt. Individuelle Konzepte. Jetzt anfragen.", + ogImage: "/images/branchen/buero.jpg", + + challengesSection: { + heading: "Typische Anforderungen in Büroumgebungen", + lead: "Büros werden täglich stark frequentiert – von Mitarbeitenden, Kunden und Besuchern. Das stellt besondere Anforderungen an Sauberkeit, Hygiene und diskrete Abstimmung mit dem laufenden Betrieb.", + points: [ + "Hohe Publikumsfrequenz an Empfang und in Besprechungsräumen", + "Sensible Bereiche wie Serverräume oder Aktenlager erfordern besondere Sorgfalt", + "Reinigungszeiten müssen sich in bestehende Arbeitsabläufe einfügen", + "Regelmäßige Hygiene an Kontaktflächen wie Türgriffen und Tastaturen", + "Repräsentative Sauberkeit in Kunden- und Empfangsbereichen", + ], + }, + + relatedServices: { + heading: "Passende Leistungen für Ihr Büro", + lead: "Diese Leistungen kombinieren wir je nach Bedarf für Ihr Bürogebäude.", + slugs: [ + "unterhaltsreinigung", + "fensterreinigung", + "treppenhausreinigung", + ], + }, + + faqSlug: "bueros", + + contactCtaSection: { + heading: "Lassen Sie uns über die Reinigung Ihres Büros sprechen", + lead: "Wir erstellen ein individuelles Reinigungskonzept für Ihre Büroräume in Hannover und Umgebung. Unverbindlich, transparent und exakt auf Ihre Anforderung abgestimmt.", + }, + gallery: [ + { + src: "/images/branchen/gallery/1.jpg", + alt: "Modernes Büro mit Arbeitsplätzen", + }, + { + src: "/images/branchen/gallery/2.jpg", + alt: "Empfangsbereich eines Bürogebäudes", + }, + { + src: "/images/branchen/gallery/3.jpg", + alt: "Besprechungsraum in einem Büro", + }, + { + src: "/images/branchen/gallery/4.jpg", + alt: "Gemeinschaftsküche im Büro", + }, + { + src: "/images/branchen/gallery/5.jpg", + alt: "Flur mit Glasfronten im Bürogebäude", + }, + { + src: "/images/branchen/gallery/6.jpg", + alt: "Sanitärbereich in einem Bürogebäude", + }, + ], }, { slug: "hallen-und-lagerflaechen", - title: "Hallen und Lagerflächen", - text: "Treppenhausreinigung", - image: "/images/leistungen/branding.jpg", + titleMain: "Reinigung für Hallen und Lagerflächen", + titleSub: "in Hannover und Umgebung", + text: "Große Flächen, hohe Belastung und industrielle Anforderungen: Wir reinigen Hallen, Lager und Produktionsflächen zuverlässig und mit dem passenden Equipment für großflächige und stark beanspruchte Böden.", + image: "/images/branchen/hallen-lagerflaechen.jpg", + imageAlt: "Saubere Lagerhalle nach professioneller Reinigung", + + // --- SEO --- + metaTitle: "Hallenreinigung Hannover – Lager & Produktionsflächen", + metaDescription: + "Reinigung von Hallen und Lagerflächen in Hannover: großflächig, belastbar und termingerecht. Professionelles Equipment für Industrieböden. Jetzt anfragen.", + ogImage: "/images/branchen/hallen-lagerflaechen.jpg", + + challengesSection: { + heading: "Typische Anforderungen in Hallen und Lagerflächen", + lead: "Hallen und Lagerflächen unterliegen anderen Bedingungen als klassische Büroräume – große Flächen, Staub, Verkehr durch Gabelstapler und wechselnde Verschmutzungsgrade erfordern ein angepasstes Konzept.", + points: [ + "Großflächige Böden erfordern effizientes, maschinelles Vorgehen", + "Staub- und Schmutzbelastung durch Warenumschlag und Betriebsabläufe", + "Reinigungszeiten müssen sich an Schichtbetrieb und Lieferverkehr anpassen", + "Sicherheitsvorgaben bei laufendem Gabelstapler- und Warenverkehr", + "Unterschiedliche Bodenbeläge von Industrieboden bis Estrich", + ], + }, + + relatedServices: { + heading: "Passende Leistungen für Ihre Halle", + lead: "Diese Leistungen kombinieren wir je nach Bedarf für Ihre Hallen- und Lagerflächen.", + slugs: ["grundreinigung", "unterhaltsreinigung", "hausmeisterdienste"], + }, + + faqSlug: "hallen-und-lagerflaechen", + + contactCtaSection: { + heading: "Lassen Sie uns über die Reinigung Ihrer Halle sprechen", + lead: "Wir erstellen ein individuelles Reinigungskonzept für Ihre Hallen- und Lagerflächen in Hannover und Umgebung. Unverbindlich, transparent und exakt auf Ihre Anforderung abgestimmt.", + }, + gallery: [ + { + src: "/images/branchen/gallery/1.jpg", + alt: "Lagerhalle mit Regalsystemen", + }, + { + src: "/images/branchen/gallery/2.jpg", + alt: "Industrieboden in einer Produktionshalle", + }, + { + src: "/images/branchen/gallery/3.jpg", + alt: "Gabelstapler in einer Lagerhalle", + }, + { + src: "/images/branchen/gallery/4.jpg", + alt: "Verladebereich einer Lagerhalle", + }, + { + src: "/images/branchen/gallery/5.jpg", + alt: "Hochregallager von innen", + }, + { + src: "/images/branchen/gallery/halle-6.jpg", + alt: "Produktionsfläche in einer Industriehalle", + }, + ], }, { slug: "wohn-und-geschaeftshaeuser", - title: "Wohn- und Geschäftshäuse", - text: "Performante E-Commerce-Lösungen.", - image: "/images/leistungen/shop.jpg", + titleMain: "Reinigung für Wohn- und Geschäftshäuser", + titleSub: "in Hannover und Umgebung", + text: "Gepflegte Treppenhäuser, Eingangsbereiche und Gemeinschaftsflächen sind die Visitenkarte jeder Immobilie. Wir betreuen Wohn- und Geschäftshäuser zuverlässig und mit einem festen Ansprechpartner für Verwaltungen und Eigentümer.", + image: "/images/branchen/wohn-geschaeftshaeuser.jpg", + imageAlt: "Gepflegtes Treppenhaus in einem Wohn- und Geschäftshaus", + + // --- SEO --- + metaTitle: "Hausreinigung Hannover – Wohn- & Geschäftshäuser", + metaDescription: + "Reinigung für Wohn- und Geschäftshäuser in Hannover: Treppenhäuser, Eingänge und Gemeinschaftsflächen gepflegt. Fester Ansprechpartner. Jetzt anfragen.", + ogImage: "/images/branchen/wohn-geschaeftshaeuser.jpg", + + challengesSection: { + heading: "Typische Anforderungen bei Wohn- und Geschäftshäusern", + lead: "In Mehrparteienhäusern treffen unterschiedliche Interessen von Bewohnern, Mietern und Eigentümern aufeinander. Das erfordert verlässliche Abläufe und klare Kommunikation mit der Hausverwaltung.", + points: [ + "Regelmäßige Betreuung von Treppenhäusern, Fluren und Eingangsbereichen", + "Abstimmung mit Hausverwaltungen und Eigentümergemeinschaften", + "Unterschiedliche Erwartungen von Mietern und Gewerbetreibenden im selben Objekt", + "Saisonale Aufgaben wie Laubentfernung oder Winterdienst im Außenbereich", + "Diskrete Reinigungszeiten mit möglichst geringer Störung der Bewohner", + ], + }, + + relatedServices: { + heading: "Passende Leistungen für Ihre Immobilie", + lead: "Diese Leistungen kombinieren wir je nach Bedarf für Ihr Wohn- oder Geschäftshaus.", + slugs: ["treppenhausreinigung", "hausmeisterdienste", "fensterreinigung"], + }, + + faqSlug: "wohn-und-geschaeftshaeuser", + + contactCtaSection: { + heading: "Lassen Sie uns über die Reinigung Ihrer Immobilie sprechen", + lead: "Wir erstellen ein individuelles Reinigungskonzept für Ihr Wohn- oder Geschäftshaus in Hannover und Umgebung. Unverbindlich, transparent und exakt auf Ihre Anforderung abgestimmt.", + }, + gallery: [ + { + src: "/images/branchen/gallery/1.jpg", + alt: "Treppenhaus in einem Wohnhaus", + }, + { + src: "/images/branchen/gallery/2.jpg", + alt: "Eingangsbereich eines Geschäftshauses", + }, + { + src: "/images/branchen/gallery/3.jpg", + alt: "Fassade eines Wohn- und Geschäftshauses", + }, + { + src: "/images/branchen/gallery/4.jpg", + alt: "Gemeinschaftsflur in einem Mehrfamilienhaus", + }, + { + src: "/images/branchen/gallery/5.jpg", + alt: "Briefkastenanlage im Eingangsbereich", + }, + { + src: "/images/branchen/gallery/6.jpg", + alt: "Außenanlage eines Wohnhauses", + }, + ], + }, + { + slug: "kanzlei", + titleMain: "Reinigung für Kanzleien", + titleSub: "in Hannover und Umgebung", + text: "In Kanzleien zählen Diskretion, Sorgfalt und ein makelloses Erscheinungsbild. Wir reinigen Empfangsbereiche, Besprechungsräume und Büros zuverlässig – mit geschultem Personal und höchster Sorgfalt im Umgang mit vertraulichen Unterlagen.", + image: "/images/branchen/kanzlei.jpg", + imageAlt: "Gepflegter Empfangsbereich einer Kanzlei", + + // --- SEO --- + metaTitle: "Kanzleireinigung Hannover – diskret & zuverlässig", + metaDescription: + "Reinigung für Kanzleien in Hannover: Empfang, Besprechungsräume und Büros diskret gepflegt. Geschultes Personal, feste Ansprechpartner. Jetzt anfragen.", + ogImage: "/images/branchen/kanzlei.jpg", + + challengesSection: { + heading: "Typische Anforderungen in Kanzleien", + lead: "Kanzleien stellen besondere Anforderungen an Vertraulichkeit und Zuverlässigkeit. Reinigungskräfte bewegen sich in sensiblen Räumen mit vertraulichen Unterlagen und benötigen entsprechend geschultes Personal.", + points: [ + "Vertrauliche Unterlagen erfordern besondere Sorgfalt und Diskretion", + "Repräsentatives Erscheinungsbild in Empfang und Besprechungsräumen", + "Feste, verlässliche Ansprechpartner für langfristige Zusammenarbeit", + "Reinigungszeiten außerhalb der Kanzleistunden zur Wahrung der Vertraulichkeit", + "Sorgfältiger Umgang mit hochwertiger Ausstattung und Mobiliar", + ], + }, + + relatedServices: { + heading: "Passende Leistungen für Ihre Kanzlei", + lead: "Diese Leistungen kombinieren wir je nach Bedarf für Ihre Kanzleiräume.", + slugs: ["unterhaltsreinigung", "fensterreinigung", "grundreinigung"], + }, + + faqSlug: "kanzlei", + + contactCtaSection: { + heading: "Lassen Sie uns über die Reinigung Ihrer Kanzlei sprechen", + lead: "Wir erstellen ein individuelles Reinigungskonzept für Ihre Kanzleiräume in Hannover und Umgebung. Unverbindlich, transparent und exakt auf Ihre Anforderung abgestimmt.", + }, + gallery: [ + { + src: "/images/branchen/gallery/1.jpg", + alt: "Empfangsbereich einer Kanzlei", + }, + { + src: "/images/branchen/gallery/2.jpg", + alt: "Besprechungsraum in einer Kanzlei", + }, + { + src: "/images/branchen/gallery/3.jpg", + alt: "Büro mit Aktenregalen in einer Kanzlei", + }, + { + src: "/images/branchen/gallery/4.jpg", + alt: "Wartebereich in einer Kanzlei", + }, + { + src: "/images/branchen/gallery/5.jpg", + alt: "Schreibtisch in einem Kanzleibüro", + }, + { + src: "/images/branchen/gallery/6.jpg", + alt: "Konferenzraum einer Kanzlei", + }, + ], }, ]; diff --git a/app/kontakt/actions.ts b/app/kontakt/actions.ts new file mode 100644 index 0000000..5845d45 --- /dev/null +++ b/app/kontakt/actions.ts @@ -0,0 +1,93 @@ +"use server"; + +import { transporter } from "@/lib/mailer"; + +export interface ContactFormState { + success: boolean; + error?: string; + fieldErrors?: Partial>; +} + +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +// Erlaubt Ziffern, Leerzeichen, +, -, /, Klammern - deckt gängige Telefonformate ab. +const PHONE_REGEX = /^[0-9+\-/() ]{6,20}$/; + +export async function sendContactMessage( + _prevState: ContactFormState, + formData: FormData, +): Promise { + const unternehmen = (formData.get("unternehmen") as string | null)?.trim() ?? ""; + const vorname = (formData.get("vorname") as string | null)?.trim() ?? ""; + const nachname = (formData.get("nachname") as string | null)?.trim() ?? ""; + const email = (formData.get("email") as string | null)?.trim() ?? ""; + const telefon = (formData.get("telefon") as string | null)?.trim() ?? ""; + const nachricht = (formData.get("nachricht") as string | null)?.trim() ?? ""; + const datenschutz = formData.get("datenschutz"); + + const fieldErrors: ContactFormState["fieldErrors"] = {}; + + if (!vorname) fieldErrors.vorname = "Bitte geben Sie Ihren Vornamen an."; + if (!nachname) fieldErrors.nachname = "Bitte geben Sie Ihren Nachnamen an."; + + if (!email) { + fieldErrors.email = "Bitte geben Sie Ihre E-Mail-Adresse an."; + } else if (!EMAIL_REGEX.test(email)) { + fieldErrors.email = "Bitte geben Sie eine gültige E-Mail-Adresse an."; + } + + if (!telefon) { + fieldErrors.telefon = "Bitte geben Sie Ihre Telefonnummer an."; + } else if (!PHONE_REGEX.test(telefon)) { + fieldErrors.telefon = "Bitte geben Sie eine gültige Telefonnummer an."; + } + + if (!nachricht) { + fieldErrors.nachricht = "Bitte geben Sie eine Nachricht ein."; + } + + if (datenschutz !== "on") { + fieldErrors.datenschutz = "Bitte bestätigen Sie die Datenschutzhinweise."; + } + + if (Object.keys(fieldErrors).length > 0) { + return { success: false, fieldErrors }; + } + + try { + await transporter.sendMail({ + from: `"Basiclean Kontaktformular" <${process.env.SMTP_USER}>`, + to: process.env.CONTACT_RECEIVER, + replyTo: email, + subject: `Neue Kontaktanfrage von ${vorname} ${nachname}`, + text: [ + `Unternehmen: ${unternehmen || "-"}`, + `Name: ${vorname} ${nachname}`, + `E-Mail: ${email}`, + `Telefon: ${telefon}`, + "", + "Nachricht:", + nachricht, + ].join("\n"), + html: ` +

Neue Kontaktanfrage

+

Unternehmen: ${unternehmen || "-"}

+

Name: ${vorname} ${nachname}

+

E-Mail: ${email}

+

Telefon: ${telefon}

+

Nachricht:

+

${nachricht.replace(/\n/g, "
")}

+ `, + }); + + return { success: true }; + } catch (error) { + console.error("Fehler beim Senden der Kontaktanfrage:", error); + return { + success: false, + error: "Die Nachricht konnte nicht gesendet werden. Bitte versuchen Sie es später erneut.", + }; + } +} \ No newline at end of file diff --git a/app/kontakt/page.module.css b/app/kontakt/page.module.css new file mode 100644 index 0000000..fe77347 --- /dev/null +++ b/app/kontakt/page.module.css @@ -0,0 +1,189 @@ +.hero { + background: var(--color-background-linear); + padding-block: var(--space-16); + position: relative; + overflow: hidden; +} + +.inner { + max-width: var(--container-width); + margin-inline: auto; + padding-inline: var(--space-4); + position: relative; + z-index: 1; +} + +.decoration { + position: absolute; + top: var(--space-8); + right: var(--space-8); + display: none; + grid-template-columns: repeat(6, 1fr); + grid-template-rows: repeat(3, 1fr); + gap: var(--space-3); + pointer-events: none; + z-index: 0; +} + +.dot { + width: 14px; + height: 14px; + border-radius: var(--radius-full); + background-color: var(--color-accent); + opacity: 0.8; +} + +.heading { + color: var(--color-heading); + max-width: 24ch; + position: relative; + z-index: 2; +} + +.lead { + margin-top: var(--space-3); + font-size: var(--font-size-base); + max-width: 60ch; + position: relative; + z-index: 2; +} + +/* --- Form Section --- */ +.formSection { + padding-block: var(--space-16); +} + +.formInner { + max-width: var(--container-width); + margin-inline: auto; + padding-inline: var(--space-4); + display: flex; + flex-direction: column; + overflow: hidden; + border-radius: var(--space-2); + padding: var(--space-2) var(--space-2) var(--space-2) 0; +} + +/* Links: kein eigener Hintergrund - bleibt leer/weiß */ +.formPanel { + padding: var(--space-8) var(--space-6) var(--space-6); +} + +/* + Rechts: Hintergrundbild mit dunklem Verlauf zur Lesbarkeit. + Inhalt wird als Flex-Column mit space-between verteilt: + Überschrift oben links, Kontaktdaten unten links. +*/ +.infoPanel { + position: relative; + background-image: + linear-gradient( + 180deg, + rgb(0 0 0 / 35%) 0%, + rgb(0 0 0 / 20%) 45%, + rgb(0 0 0 / 65%) 100% + ), + url("/images/contact.jpg"); + background-size: cover; + background-position: center; + padding: var(--space-8) var(--space-6) var(--space-6); + color: #fff; + display: flex; + flex-direction: column; + justify-content: space-between; + min-height: 420px; + border-radius: var(--space-2); +} + +.infoHeading { + color: #fff; + max-width: 20ch; +} + +.infoList { + display: flex; + flex-direction: column; + gap: var(--space-3); + list-style: none; + padding: 0; +} + +.infoItem { + display: flex; + flex-direction: column; + gap: 2px; +} + +.infoLabel { + font-size: var(--font-size-small, 0.875rem); + font-weight: var(--font-weight-medium); + color: rgb(255 255 255 / 80%); +} + +.infoValue { + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + color: #fff; + text-decoration: none; +} + +a.infoValue:hover { + text-decoration: underline; +} + +/* Tablet: 768px – 50/50 Aufteilung */ +@media (min-width: 768px) { + .hero, + .formSection { + padding-block: var(--space-24); + } + + .decoration { + display: grid; + top: -24px; + } + + .dot { + width: 24px; + height: 24px; + } + + .formInner { + flex-direction: row; + align-items: stretch; + background: var(--color-background-linear); + } + + .formPanel { + width: 50%; + padding: var(--space-8); + } + + .infoPanel { + width: 50%; + padding: var(--space-10) var(--space-8) var(--space-8); + min-height: 480px; + } +} + +/* Desktop: 1024px – zurück auf 40/60 */ +@media (min-width: 1024px) { + .hero { + padding-block: var(--space-36); + } + + .decoration { + top: -42px; + } + + .formPanel { + width: 40%; + padding: var(--space-10); + } + + .infoPanel { + width: 60%; + padding: var(--space-12) var(--space-10) var(--space-10); + min-height: 560px; + } +} diff --git a/app/kontakt/page.tsx b/app/kontakt/page.tsx index 4d3428b..4f8558d 100644 --- a/app/kontakt/page.tsx +++ b/app/kontakt/page.tsx @@ -1,10 +1,74 @@ -export default function Kontakt() { - return <> -

Gebäudereinigung für ein makelloses Arbeitsumfeld

-

Gebäudereinigung für ein makelloses Arbeitsumfeld

-

Gebäudereinigung für ein makelloses Arbeitsumfeld

-

Gebäudereinigung für ein makelloses Arbeitsumfeld

-
Gebäudereinigung für ein makelloses Arbeitsumfeld
-
Gebäudereinigung für ein makelloses Arbeitsumfeld
- ; +import type { Metadata } from "next"; + +import styles from "./page.module.css"; +import ContactForm from "@/components/ContactForm"; +import FaqSection from "@/components/faq/FaqSection"; +import { homepageFaq } from "@/components/faq/faqData"; + +export const metadata: Metadata = { + title: "Kontakt", + description: + "Kontaktieren Sie Basiclean für ein individuelles Angebot zu Reinigung und Hausmeisterdiensten in Hannover und Umgebung.", +}; + +export default function Page() { + return ( +
+
+
+ + +

+ Kontaktieren Sie uns +

+

+ Sie haben Fragen oder möchten ein individuelles Angebot anfragen? + Füllen Sie das Formular aus – wir melden uns schnellstmöglich bei + Ihnen zurück. +

+
+
+ +
+
+
+ +
+ +
+

So erreichen Sie uns

+ + +
+
+
+ +
+ +
+
+ ); } diff --git a/app/leistungen/[slug]/page.tsx b/app/leistungen/[slug]/page.tsx index 751f430..32c7f97 100644 --- a/app/leistungen/[slug]/page.tsx +++ b/app/leistungen/[slug]/page.tsx @@ -13,6 +13,74 @@ import FaqSection from "@/components/faq/FaqSection"; import { leistungenFaq } from "@/components/faq/faqData"; import ContactCtaSection from "@/components/contact/ContactCtaSection"; +import type { Metadata } from "next"; + +const siteUrl = "https://basiclean.de"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }>; +}): Promise { + const { slug } = await params; + const leistung = leistungen.find((item) => item.slug === slug); + + if (!leistung) { + return { + title: "Nicht gefunden", + robots: { index: false, follow: false }, + }; + } + + const hasCustomSeo = Boolean(leistung.metaTitle); + + // Eigener metaTitle vorhanden -> Suffix "| Basiclean" unterdrücken (title.absolute). + // Kein metaTitle gesetzt -> Fallback auf titleMain, Template aus dem Layout greift normal. + const title = hasCustomSeo + ? { absolute: leistung.metaTitle as string } + : leistung.titleMain; + + const description = leistung.metaDescription ?? leistung.text; + const url = `${siteUrl}/leistungen/${leistung.slug}`; + const ogImageUrl = leistung.ogImage ?? leistung.image; + + // Für OpenGraph/Twitter braucht es einen reinen String-Titel (kein title.absolute-Objekt). + const flatTitle = hasCustomSeo + ? (leistung.metaTitle as string) + : leistung.titleMain; + + return { + title, + description, + alternates: { + canonical: url, + }, + openGraph: { + type: "website", + siteName: "Basiclean", + locale: "de_DE", + title: flatTitle, + description, + url, + images: [ + { + url: ogImageUrl, + alt: leistung.imageAlt, + width: 1200, + height: 628, + }, + ], + }, + twitter: { + card: "summary_large_image", + site: "@basiclean", + title: flatTitle, + description, + images: [{ url: ogImageUrl, alt: leistung.imageAlt }], + }, + }; +} + export function generateStaticParams() { return leistungen.map((item) => ({ slug: item.slug, @@ -110,9 +178,9 @@ export default async function LeistungDetail({ /> -
- -
+
+ +
); } diff --git a/app/leistungen/data.ts b/app/leistungen/data.ts index 8f2138a..d17535c 100644 --- a/app/leistungen/data.ts +++ b/app/leistungen/data.ts @@ -8,6 +8,12 @@ export const leistungen = [ imageAlt: "Gereinigte Fensterfront eines Geschäftsgebäudes", titleSlides: "Warum eine professionelle Unterhaltsreinigung wichtig ist", + // --- SEO --- + metaTitle: "Fensterreinigung Hannover – streifenfrei & zuverlässig", + metaDescription: + "Professionelle Fensterreinigung in Hannover: Fenster, Rahmen und Glasfassaden streifenfrei gereinigt. Individuelle Konzepte, faire Preise. Jetzt anfragen.", + ogImage: "/images/services/Fensterreinigung.jpg", + slides: [ { heading: "Klare Sicht für Ihren guten ersten Eindruck", @@ -118,6 +124,13 @@ export const leistungen = [ image: "/images/services/unterhaltsreinigung.jpg", imageAlt: "Reinigungskraft bei der Unterhaltsreinigung in einem Büro", titleSlides: "Warum professionelle Unterhaltsreinigung wichtig ist", + + // --- SEO --- + metaTitle: "Unterhaltsreinigung Hannover – Büros, Praxen & Schulen", + metaDescription: + "Regelmäßige Unterhaltsreinigung in Hannover für Büros, Praxen und Schulen. Feste Termine, individuelle Intervalle, geschultes Personal. Jetzt anfragen.", + ogImage: "/images/slides/unterhaltsreinigung-1.jpg", + slides: [ { heading: "Konstante Hygiene im Alltag", @@ -228,6 +241,13 @@ export const leistungen = [ image: "/images/services/treppenhausreinigung.jpg", imageAlt: "Sauberes Treppenhaus in einem Gewerbeobjekt", titleSlides: "Warum professionelle Unterhaltsreinigung wichtig ist", + + // --- SEO --- + metaTitle: "Treppenhausreinigung Hannover – gepflegte Zugänge", + metaDescription: + "Treppenhausreinigung in Hannover für Wohn- und Gewerbeobjekte: Böden, Handläufe und Türen gepflegt in festen Intervallen. Jetzt Angebot anfragen.", + ogImage: "/images/slides/treppenhausreinigung-1.jpg", + slides: [ { heading: "Ein gepflegter Empfang beginnt im Treppenhaus", @@ -339,6 +359,13 @@ export const leistungen = [ image: "/images/services/grundreinigung.jpg", imageAlt: "Sauberer Büroarbeitsplatz nach der Grundreinigung", titleSlides: "Warum professionelle Unterhaltsreinigung wichtig ist", + + // --- SEO --- + metaTitle: "Grundreinigung Hannover – Tiefenreinigung vom Profi", + metaDescription: + "Professionelle Grundreinigung in Hannover: intensive Tiefenreinigung nach Umzug, Umbau oder Neueröffnung. Passendes Equipment, faire Preise. Jetzt anfragen.", + ogImage: "/images/slides/grundreinigung-1.jpg", + slides: [ { heading: "Gründliche Sauberkeit bis ins Detail", @@ -450,6 +477,13 @@ export const leistungen = [ image: "/images/services/Hausmeisterdienste.jpg", imageAlt: "Hausmeister bei der Außenpflege eines Gebäudes", titleSlides: "Warum professionelle Unterhaltsreinigung wichtig ist", + + // --- SEO --- + metaTitle: "Hausmeisterdienste Hannover – Objektbetreuung", + metaDescription: + "Hausmeisterdienste in Hannover: Winterdienst, Grünpflege, Kleinreparaturen und Kontrollgänge aus einer Hand. Fester Ansprechpartner. Jetzt anfragen.", + ogImage: "/images/slides/hausmeisterdienste-1.jpg", + slides: [ { heading: "Zuverlässige Betreuung für Ihr Objekt", diff --git a/app/ueberuns/loading.tsx b/app/ueberuns/loading.tsx deleted file mode 100644 index 2fdcf96..0000000 --- a/app/ueberuns/loading.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Loading() { - return ( -
-

Laden...

-
- ); -} diff --git a/app/ueberuns/page.module.css b/app/ueberuns/page.module.css index e69de29..f73ef98 100644 --- a/app/ueberuns/page.module.css +++ b/app/ueberuns/page.module.css @@ -0,0 +1,207 @@ +.hero { + background: var(--color-background-linear); + padding-block: var(--space-16); + position: relative; + overflow: hidden; +} + +.inner { + max-width: var(--container-width); + margin-inline: auto; + padding-inline: var(--space-4); + position: relative; + z-index: 1; +} + +.decoration { + position: absolute; + top: var(--space-8); + right: var(--space-8); + display: none; + grid-template-columns: repeat(6, 1fr); + grid-template-rows: repeat(3, 1fr); + gap: var(--space-3); + pointer-events: none; + z-index: 0; +} + +.dot { + width: 14px; + height: 14px; + border-radius: var(--radius-full); + background-color: var(--color-accent); + opacity: 0.8; +} + +.heading { + color: var(--color-heading); + max-width: 26ch; + position: relative; + z-index: 2; +} + +.lead { + margin-top: var(--space-3); + font-size: var(--font-size-base); + max-width: 65ch; + position: relative; + z-index: 2; +} + +/* --- Story Section --- */ +.storySection { + padding-block: var(--space-16); +} + +.storyInner { + max-width: var(--container-width); + margin-inline: auto; + padding-inline: var(--space-4); +} + +.storyText { + max-width: 70ch; +} + +.storyHeading { + color: var(--color-heading); + margin-bottom: var(--space-4); +} + +.storyLead { + font-size: var(--font-size-base); + margin-bottom: var(--space-4); +} + +.storyLead:last-child { + margin-bottom: 0; +} + +/* --- Werte Section (Kachel-Design wie MaintenanceCleaningSection) --- */ +.werteSection { + background: var(--color-background-linear); + padding-block: var(--space-16); +} + +.werteInner { + max-width: var(--container-width); + margin-inline: auto; + padding-inline: var(--space-4); +} + +.werteHeading { + max-width: 24ch; + color: var(--color-heading); +} + +.werteLead { + margin-top: var(--space-3); + font-size: var(--font-size-base); + max-width: 48ch; +} + +.werteGrid { + margin-top: var(--space-8); + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: var(--space-5); +} + +.werteCard { + display: flex; + flex-direction: column; + gap: var(--space-3); + background: var(--color-surface-muted, #e6ede9); + border-radius: 12px; + padding: var(--space-5); +} + +.werteIcon { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + color: var(--color-accent); +} + +.werteCardHeading { + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + color: var(--color-heading); +} + +.werteCardText { + font-size: var(--font-size-xs); + color: var(--font-heading-color); +} + +/* --- Philosophy Section --- */ +.philosophySection { + padding-block: var(--space-16); +} + +.philosophyInner { + max-width: var(--container-width); + margin-inline: auto; + padding-inline: var(--space-4); + display: flex; + flex-direction: column; + gap: var(--space-8); +} + +.philosophyCol { + flex: 1; +} + +.philosophyHeading { + color: var(--color-heading); + margin-bottom: var(--space-3); +} + +.philosophyText { + font-size: var(--font-size-base); +} + +/* Tablet: 768px */ +@media (min-width: 768px) { + .storySection, + .werteSection, + .philosophySection { + padding-block: var(--space-24); + } + + .decoration { + display: grid; + top: -24px; + } + + .dot { + width: 24px; + height: 24px; + } + + .werteGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .philosophyInner { + flex-direction: row; + gap: var(--space-10); + } +} + +/* Desktop: 1024px */ +@media (min-width: 1024px) { + .hero { + padding-block: var(--space-36); + } + + .decoration { + top: -42px; + } + + .werteGrid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} diff --git a/app/ueberuns/page.tsx b/app/ueberuns/page.tsx index e7a5db9..5e18b51 100644 --- a/app/ueberuns/page.tsx +++ b/app/ueberuns/page.tsx @@ -1,8 +1,134 @@ +import type { Metadata } from "next"; +import { CheckCircleIcon } from "@phosphor-icons/react/dist/ssr"; +import styles from "./page.module.css"; +import ContactCtaSection from "@/components/contact/ContactCtaSection"; + +export const metadata: Metadata = { + title: "Über uns", + description: + "Basiclean ist Ihr Partner für professionelle Gebäudereinigung in Hannover. Erfahren Sie mehr über unsere Werte, Ziele und unser Verständnis von Qualität.", +}; + +const werte = [ + { + heading: "Zuverlässigkeit", + text: "Wir halten Termine ein und sorgen für gleichbleibende Qualität – Woche für Woche, ohne Ausnahmen.", + }, + { + heading: "Transparenz", + text: "Faire, nachvollziehbare Preise ohne versteckte Kosten. Sie wissen jederzeit, wofür Sie bezahlen.", + }, + { + heading: "Sorgfalt", + text: "Wir behandeln Ihr Objekt so, als wäre es unser eigenes – mit Aufmerksamkeit für Details.", + }, + { + heading: "Nähe", + text: "Als regionales Unternehmen sind wir persönlich erreichbar und kennen die Anforderungen vor Ort.", + }, +]; export default function UeberUns() { return (
-

Über Uns

+
+
+ + +

+ Über Basiclean +

+

+ Basiclean wurde mit dem Anspruch gegründet, Gebäudereinigung in + Hannover verlässlicher, transparenter und persönlicher zu gestalten. + Wir sind ein junges Unternehmen mit klarem Fokus: saubere Ergebnisse + und langfristige Zusammenarbeit statt austauschbarer + Standarddienstleistung. +

+
+
+ +
+
+
+

Wie wir angefangen haben

+

+ Basiclean ist ein neu gegründetes Unternehmen für Gebäudereinigung + mit Sitz in Hannover. Der Anstoß war die Beobachtung, dass viele + Reinigungsdienstleistungen unpersönlich und wenig flexibel + organisiert sind. Wir wollten das anders machen: mit festen + Ansprechpartnern, klaren Absprachen und einem Team, das sich + wirklich für das Ergebnis verantwortlich fühlt. +

+

+ Von Anfang an haben wir uns auf Unterhalts-, Fenster- und + Grundreinigung sowie Hausmeisterdienste für Büros, Wohn- und + Geschäftshäuser, Kanzleien und Gewerbeflächen spezialisiert – + statt alles anzubieten, konzentrieren wir uns auf das, was wir + wirklich gut können. +

+
+
+
+ +
+
+

Was uns wichtig ist

+

+ Diese Grundsätze bestimmen, wie wir arbeiten – bei jedem Auftrag, + unabhängig von Objektgröße oder Branche. +

+ +
+ {werte.map((wert) => ( +
+ +

{wert.heading}

+

{wert.text}

+
+ ))} +
+
+
+ +
+
+
+

Unsere Philosophie

+

+ Wir verstehen Reinigung nicht als reine Dienstleistung nach + Checkliste, sondern als Teil der Wahrnehmung Ihres Unternehmens. + Ein gepflegtes Gebäude schafft Vertrauen – bei Kunden, + Mitarbeitenden und Besuchern gleichermaßen. Deshalb passen wir + unsere Konzepte an das jeweilige Objekt an, statt starre + Standardpakete zu verkaufen. +

+
+
+

Unsere Ziele

+

+ Kurzfristig wollen wir in Hannover und Umgebung als verlässlicher + Partner für Gebäudereinigung wahrgenommen werden. Langfristig + setzen wir auf nachhaltiges Wachstum durch zufriedene, langjährige + Kunden statt kurzfristiger Einzelaufträge – getragen von einem + Team, das fair behandelt wird und entsprechend motiviert arbeitet. +

+
+
+
+ +
+ +
); } diff --git a/components/BentoGallery.module.css b/components/BentoGallery.module.css new file mode 100644 index 0000000..c80418c --- /dev/null +++ b/components/BentoGallery.module.css @@ -0,0 +1,105 @@ +.gallerySection { + padding-block: var(--space-10); +} + +.inner { + max-width: var(--container-width); + margin-inline: auto; + padding-inline: var(--space-4); +} + +/* Mobile: einfache Stapelung */ +.grid { + display: grid; + grid-template-columns: 1fr; + gap: var(--space-3); +} + +.item { + position: relative; +} + +.frame { + position: relative; + width: 100%; + aspect-ratio: 4 / 3; + border-radius: var(--radius-lg); + overflow: hidden; + box-shadow: + 0 10px 30px rgba(0, 0, 0, 0.08), + 0 24px 60px rgba(0, 0, 0, 0.06); +} + +.image { + object-fit: cover; + transition: transform 0.4s ease; +} + +.frame:hover .image { + transform: scale(1.04); +} + +/* + Tablet & Desktop ab 768px: Bento-Grid mit benannten Areas. + 6 Bilder, ein großes Hauptbild (a) oben links, Rest gestaffelt drumherum. + Jede Area erscheint genau einmal im Template -> kein Bild geht verloren. +*/ +@media (min-width: 768px) { + .gallerySection { + padding-block: var(--space-16); + } + + .grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-template-rows: repeat(3, minmax(140px, 1fr)); + grid-template-areas: + "a a b c" + "a a d d" + "e e f f"; + gap: var(--space-4); + } + + .item { + height: 100%; + } + + .frame { + height: 100%; + aspect-ratio: auto; + } + + .item[data-index="0"] { + grid-area: a; + } + .item[data-index="1"] { + grid-area: b; + } + .item[data-index="2"] { + grid-area: c; + } + .item[data-index="3"] { + grid-area: d; + } + .item[data-index="4"] { + grid-area: e; + } + .item[data-index="5"] { + grid-area: f; + } +} + +/* Desktop: 1024px – großzügigere, breitere Variante desselben Musters */ +@media (min-width: 1024px) { + .gallerySection { + padding-block: var(--space-20); + } + + .grid { + grid-template-columns: repeat(6, 1fr); + grid-template-rows: repeat(2, minmax(200px, 1fr)); + grid-template-areas: + "a a a b b c" + "a a a d e c"; + } +} diff --git a/components/BentoGallery.tsx b/components/BentoGallery.tsx new file mode 100644 index 0000000..20f4395 --- /dev/null +++ b/components/BentoGallery.tsx @@ -0,0 +1,38 @@ +import Image from "next/image"; +import styles from "./BentoGallery.module.css"; + +interface GalleryImage { + src: string; + alt: string; +} + +interface BentoGalleryProps { + images: GalleryImage[]; +} + +export default function BentoGallery({ images }: BentoGalleryProps) { + const items = images.slice(0, 6); + + return ( +
+
+
+ {items.map((image, index) => ( +
+
+ {image.alt} +
+
+ ))} +
+
+
+ ); +} diff --git a/components/ContactForm.module.css b/components/ContactForm.module.css new file mode 100644 index 0000000..4d27590 --- /dev/null +++ b/components/ContactForm.module.css @@ -0,0 +1,100 @@ +.form { + display: flex; + flex-direction: column; + gap: var(--space-5); + max-width: 640px; +} + +.row { + display: grid; + grid-template-columns: 1fr; + gap: var(--space-5); +} + +.field { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.label { + font-size: var(--font-size-small, 0.9rem); + font-weight: var(--font-weight-medium); + color: var(--color-heading); +} + +.optional { + font-weight: var(--font-weight-regular, 400); + color: var(--color-text-muted, #6b7280); +} + +.required { + color: var(--color-accent); +} + +.input, +.textarea { + border: 1px solid color-mix(in srgb, var(--color-heading) 15%, transparent); + border-radius: var(--radius-md, 8px); + padding: var(--space-3) var(--space-4); + font-size: var(--font-size-base); + font-family: inherit; + background: #fff; + transition: border-color 0.2s ease; +} + +.input:focus, +.textarea:focus { + outline: none; + border-color: var(--color-accent); +} + +.textarea { + resize: vertical; + min-height: 140px; +} + +.checkboxField { + display: flex; + align-items: flex-start; + gap: var(--space-3); +} + +.checkbox { + margin-top: 4px; + width: 18px; + height: 18px; + flex-shrink: 0; + accent-color: var(--color-accent); +} + +.checkboxLabel { + font-size: var(--font-size-small, 0.9rem); + line-height: 1.5; +} + +.checkboxLabel a { + color: var(--color-accent); + text-decoration: underline; +} + +.errorText { + font-size: var(--font-size-small, 0.85rem); + color: #d92d20; + margin: 0; +} + +.successBox { + max-width: 640px; + padding: var(--space-6); + border-radius: var(--radius-lg); + background: color-mix(in srgb, var(--color-accent) 12%, white); + color: var(--color-heading); +} + +/* Tablet: 768px */ +@media (min-width: 768px) { + .row { + grid-template-columns: repeat(2, 1fr); + } +} diff --git a/components/ContactForm.tsx b/components/ContactForm.tsx new file mode 100644 index 0000000..d31bf24 --- /dev/null +++ b/components/ContactForm.tsx @@ -0,0 +1,213 @@ +"use client"; + +import { useActionState, useMemo, useState } from "react"; +import { useFormStatus } from "react-dom"; +import { + sendContactMessage, + type ContactFormState, +} from "@/app/kontakt/actions"; +import styles from "./ContactForm.module.css"; + +const initialState: ContactFormState = { success: false }; + +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +const PHONE_REGEX = /^[0-9+\-/() ]{6,20}$/; + +function SubmitButton({ disabled }: { disabled: boolean }) { + const { pending } = useFormStatus(); + + return ( + + ); +} + +export default function ContactForm() { + const [state, formAction] = useActionState(sendContactMessage, initialState); + + const [values, setValues] = useState({ + unternehmen: "", + vorname: "", + nachname: "", + email: "", + telefon: "", + nachricht: "", + datenschutz: false, + }); + + const isValid = useMemo(() => { + return ( + values.vorname.trim().length > 0 && + values.nachname.trim().length > 0 && + EMAIL_REGEX.test(values.email.trim()) && + PHONE_REGEX.test(values.telefon.trim()) && + values.nachricht.trim().length > 0 && + values.datenschutz + ); + }, [values]); + + if (state.success) { + return ( +
+

+ Vielen Dank für Ihre Nachricht! Wir melden uns schnellstmöglich bei + Ihnen zurück. +

+
+ ); + } + + return ( +
+
+ + + setValues((v) => ({ ...v, unternehmen: e.target.value })) + } + /> +
+ +
+
+ + + setValues((v) => ({ ...v, vorname: e.target.value })) + } + /> + {state.fieldErrors?.vorname && ( +

{state.fieldErrors.vorname}

+ )} +
+ +
+ + + setValues((v) => ({ ...v, nachname: e.target.value })) + } + /> + {state.fieldErrors?.nachname && ( +

{state.fieldErrors.nachname}

+ )} +
+
+ +
+ + setValues((v) => ({ ...v, email: e.target.value }))} + /> + {state.fieldErrors?.email && ( +

{state.fieldErrors.email}

+ )} +
+ +
+ + + setValues((v) => ({ ...v, telefon: e.target.value })) + } + /> + {state.fieldErrors?.telefon && ( +

{state.fieldErrors.telefon}

+ )} +
+ +
+ +