This commit is contained in:
egnbkts committed 2026-05-13 09:08:53 +02:00
1 parent 0b775a938d
commit 8757431c79
35 files changed
+1507 -60

No files matched your search

+1 -1
View File
@@ -34,7 +34,7 @@
}
.label {
font-size: var(--font-size-small);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
letter-spacing: var(--letter-spacing-wide);
text-transform: uppercase;
+1 -17
View File
@@ -19,19 +19,12 @@ html.has-menu-open {
scrollbar-gutter: stable;
}
html,
body {
max-width: 100vw;
}
body {
min-height: 100%;
display: flex;
flex-direction: column;
color: var(--font-body-color);
background: var(--color-background);
font-family: var(--font-body);
font-size: 1rem;
font-size: var(--font-size-base);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: var(--line-height-body);
@@ -40,19 +33,10 @@ body {
main {
width: 100%;
max-width: 1280px;
margin-inline: auto;
padding: 0 var(--space-4);
}
@media (min-width: 768px) {
main {
padding: 0 var(--space-4);
}
}
@media (min-width: 1024px) {
main {
padding: 0 var(--space-6);
}
}
+1 -1
View File
@@ -34,7 +34,7 @@
}
.label {
font-size: var(--font-size-small);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
letter-spacing: var(--letter-spacing-wide);
text-transform: uppercase;
+16
View File
@@ -1,9 +1,25 @@
import AboutUsSection from "@/components/AboutUsSection";
import BenefitsSection from "@/components/benefitsSection";
import ContactCtaSection from "@/components/contact/ContactCtaSection";
import { homepageFaq } from "@/components/faq/faqData";
import FaqSection from "@/components/faq/FaqSection";
import HeroSection from "@/components/HeroSection";
import IndustrySolutionsSection from "@/components/IndustrySolutionsSection";
import ServiceSection from "@/components/ServiceSection";
export default function Home() {
return (
<main>
<HeroSection />
<ServiceSection />
<BenefitsSection />
<IndustrySolutionsSection />
<FaqSection
title="In drei Schritten zu planbarer Sauberkeit"
items={homepageFaq}
/>
<AboutUsSection />
<ContactCtaSection />
</main>
);
}
+32
View File
@@ -0,0 +1,32 @@
export type AboutUsMember = {
name: string;
imageSrc: string;
imageAlt: string;
};
export type AboutUsData = {
title: string;
paragraphs: string[];
members: AboutUsMember[];
};
export const aboutUsData: AboutUsData = {
title: "Ihre Reinigungsfirma für professionelle Ansprüche",
paragraphs: [
"Basiclean ist ein junges Reinigungsunternehmen mit Fokus auf gewerbliche Kunden in Hannover und der umliegenden Region.",
"Unser Ziel ist es, Unternehmen mit planbarer Sauberkeit, klaren Abläufen und zuverlässiger Zusammenarbeit zu entlasten. Statt starrer Standards bieten wir individuelle Reinigungslösungen, abgestimmt auf Ihr Objekt und Ihre Anforderungen.",
"Persönlicher Kontakt, transparente Leistungen und geschulte Reinigungskräfte stehen dabei im Mittelpunkt.",
],
members: [
{
name: "Abdulkabi Dastan",
imageSrc: "/images/portrait/abdulkabi-dastan.jpg",
imageAlt: "Porträt von Abdulkabi Dastan",
},
{
name: "Gökhan Özsayiner",
imageSrc: "/images/portrait/goekhan-oezsayiner.jpg",
imageAlt: "Porträt von Gökhan Özsayiner",
},
],
};
+120
View File
@@ -0,0 +1,120 @@
.section {
padding-block: var(--space-16);
}
.inner {
width: 100%;
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
display: grid;
grid-template-columns: 1fr;
gap: var(--space-8);
justify-items: center;
}
.heading {
max-width: 20ch;
text-align: center;
}
.team {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-4);
max-width: 420px;
width: 100%;
}
.card {
display: flex;
flex-direction: column;
gap: var(--space-3);
align-items: center;
}
.imageWrap {
position: relative;
aspect-ratio: 4 / 5;
overflow: hidden;
border-radius: 20px;
background: #e9e9e9;
width: 100%;
}
.image {
object-fit: cover;
}
.name {
font-size: var(----font-size-xs);
color: var(--color-text);
text-align: center;
}
.textBlock {
display: flex;
flex-direction: column;
gap: var(--space-5);
align-items: center;
}
.text {
max-width: 56ch;
color: var(--color-text-muted);
font-size: var(----font-size-xs);
text-align: center;
}
@media (min-width: 768px) {
.section {
padding-block: var(--space-24);
}
.inner {
grid-template-columns: 1fr 1fr;
column-gap: var(--space-16);
row-gap: var(--space-8);
align-items: start;
justify-items: stretch;
}
.heading {
grid-column: 1 / -1;
text-align: left;
}
.team {
grid-column: 1;
grid-row: 2;
}
.card {
align-items: stretch;
}
.textBlock {
grid-column: 2;
grid-row: 2;
align-self: center;
align-items: flex-start;
}
.text {
text-align: left;
}
}
@media (min-width: 1024px) {
.section {
padding-block: var(--space-32);
}
.inner {
padding-inline: var(--space-6);
}
.team {
max-width: 460px;
}
}
+45
View File
@@ -0,0 +1,45 @@
"use client";
import Image from "next/image";
import styles from "./AboutUsSection.module.css";
import { aboutUsData } from "./AboutUsData";
export default function AboutUsSection() {
const { title, paragraphs, members } = aboutUsData;
return (
<section className={styles.section} aria-labelledby="about-us-heading">
<div className={styles.inner}>
<h2 id="about-us-heading" className={styles.heading}>
{title}
</h2>
<div className={styles.team}>
{members.map((member) => (
<article key={member.name} className={styles.card}>
<div className={styles.imageWrap}>
<Image
src={member.imageSrc}
alt={member.imageAlt}
fill
className={styles.image}
sizes="(max-width: 767px) 42vw, 180px"
/>
</div>
<p className={styles.name}>{member.name}</p>
</article>
))}
</div>
<div className={styles.textBlock}>
{paragraphs.map((paragraph, index) => (
<p key={index} className={styles.text}>
{paragraph}
</p>
))}
</div>
</div>
</section>
);
}
+1 -1
View File
@@ -26,7 +26,7 @@
}
.footer__inner {
max-width: 1280px;
max-width: var(--container-width);
margin-inline: auto;
padding: var(--footer-padding-y) clamp(1rem, 5vw, 2rem);
}
+1 -1
View File
@@ -1,5 +1,5 @@
import styles from "./Header.module.css";
import Navbar from "./Navbar/Navbar";
import Navbar from "./navbar/Navbar";
export default function Header() {
return (
+20 -12
View File
@@ -1,15 +1,11 @@
.hero {
position: relative;
background: linear-gradient(to bottom, #f9f9f9 5%, #ffffff 50%, #f9f9f9 100%);
background: var(--color-background-linear);
padding-block: var(--space-16);
width: 100vw;
margin-left: calc(50% - 50vw);
margin-right: calc(50% - 50vw);
}
.inner {
width: 100%;
max-width: 1280px;
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
}
@@ -35,25 +31,30 @@
}
.heading {
max-width: 20ch;
max-width: 15ch;
}
.lead {
max-width: 56ch;
max-width: 40ch;
}
.actions {
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--space-3);
gap: var(--space-4);
margin-top: var(--space-2);
}
.actions :global(.btn) {
width: fit-content;
justify-content: center;
min-height: 44px;
font-weight: var(--font-weight-semibold);
}
.actions :global(.btn--tertiary) {
padding-top: 0;
padding-bottom: 0;
color: var(--font-body-color);
}
/* Tablet */
@@ -67,7 +68,7 @@
top: 50%;
transform: translateY(-50%);
height: 90%;
width:50%;
width: 50%;
}
.actions {
flex-direction: row;
@@ -75,6 +76,13 @@
align-items: center;
gap: var(--space-6);
}
.heading {
max-width: 24ch;
}
.lead {
max-width: 48ch;
}
}
/* Desktop */
+13 -5
View File
@@ -14,11 +14,11 @@ export default function HeroSection() {
<div className={styles.inner}>
<div className={styles.content}>
<h1 id="hero-heading" className={styles.heading}>
Mehr Sauberkeit, weniger Aufwand für ein sauberes Arbeitsumfeld
Gebäudereinigung für ein makelloses Arbeitsumfeld
</h1>
<p className={`lead ${styles.lead}`}>
Professionelle Gebäudereinigung für Büros, Praxen und Gewerbeflächen
nach festen Abläufen und höchsten Qualitätsstandards.
Mit festen Abläufen und hohen Qualitätsstandards – für ein sauberes,
professionelles Arbeitsumfeld.
</p>
<div className={styles.actions}>
<Link href="/kontakt" className="btn btn--filled">
@@ -27,11 +27,19 @@ export default function HeroSection() {
</span>
Jetzt Angebot anfragen
</Link>
<a href="tel:+495112345678" className="btn btn--outline">
<Link href="/Leistungen" className="btn btn--outline">
{/* <span className="btn-icon animate" aria-hidden="true">
<ArrowRightIcon size={18} weight="bold" />
</span> */}
Leistungen ansehen
</Link>
</div>
<div className={styles.actions}>
<a href="tel:+491726210606" className="btn btn--tertiary">
<span className="btn-icon" aria-hidden="true">
<PhoneIcon size={18} weight="bold" />
</span>
0172 621 0606
0172 6210606
</a>
</div>
</div>
@@ -0,0 +1,166 @@
.section {
padding-block: var(--space-16);
}
.inner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
}
.intro {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
margin-bottom: var(--space-8);
text-align: center;
}
.heading {
max-width: 18ch;
}
.lead {
max-width: 58ch;
color: var(--color-text-muted);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--space-4);
justify-content: center;
}
.card {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
background-color: #e2ebe5;
color: var(--color-accent);
border-radius: var(--radius-lg);
padding: var(--space-6) var(--space-6);
text-decoration: none;
min-height: 88px;
transition:
background-color 0.25s ease,
color 0.25s ease,
box-shadow 0.25s ease;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
}
.card:hover,
.card:focus-visible {
background-color: var(--color-accent);
color: #ffffff;
}
.cardText {
font-size: var(--font-size-body);
line-height: 1.2;
font-weight: var(--font-weight-semibold);
max-width: 18ch;
}
.iconWrap {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: 50px;
background-color: var(--color-accent);
color: #ffffff;
transition:
background-color 0.25s ease,
color 0.25s ease;
}
.card:hover .iconWrap,
.card:focus-visible .iconWrap {
background-color: #ffffff;
color: var(--color-accent);
}
.icon {
display: block;
}
@media (min-width: 768px) {
.section {
padding-block: var(--space-24);
}
.intro {
margin-bottom: var(--space-10);
}
.heading {
max-width: 18ch;
}
.grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.card {
min-height: 96px;
}
.card:nth-child(4),
.card:nth-child(5) {
grid-column: span 1;
}
}
@media (min-width: 1024px) {
.section {
padding-block: var(--space-24);
}
.inner {
padding-inline: var(--space-6);
}
.grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
max-width: 860px;
margin-inline: auto;
}
.card {
padding: var(--space-12) var(--space-6);
}
.card:nth-child(4) {
grid-column: 1 / 2;
justify-self: end;
width: 100%;
max-width: 100%;
}
.card:nth-child(5) {
grid-column: 2 / 3;
justify-self: start;
width: 100%;
max-width: 100%;
}
}
@media (hover: none) {
.card:hover,
.card:focus-visible {
background-color: #e2ebe5;
color: var(--color-accent);
transform: none;
}
.card:hover .iconWrap,
.card:focus-visible .iconWrap {
background-color: var(--color-accent);
color: #ffffff;
}
}
+79
View File
@@ -0,0 +1,79 @@
import Link from "next/link";
import styles from "./IndustrySolutionsSection.module.css";
import {
BuildingsIcon,
StethoscopeIcon,
WarehouseIcon,
HouseLineIcon,
ArrowRightIcon,
} from "@phosphor-icons/react/dist/ssr";
const industries = [
{
title: "Büros",
href: "/branchen/bueros",
icon: BuildingsIcon,
},
{
title: "Arztpraxen",
href: "/branchen/arztpraxen",
icon: StethoscopeIcon,
},
{
title: "Hallen und Lagerflächen",
href: "/branchen/hallen-lagerflaechen",
icon: WarehouseIcon,
},
{
title: "Wohn- und Geschäftshäuser",
href: "/branchen/wohn-geschaeftshaeuser",
icon: HouseLineIcon,
},
{
title: "Zum Kontaktformular",
href: "/kontakt",
icon: ArrowRightIcon,
},
];
export default function IndustrySolutionsSection() {
return (
<section
className={styles.section}
aria-labelledby="industry-solutions-heading"
>
<div className={styles.inner}>
<div className={styles.intro}>
<h2 id="industry-solutions-heading" className={styles.heading}>
Reinigungsleistungen für verschiedene Branchen
</h2>
<p className={styles.lead}>
Als Reinigungsfirma betreuen wir Unternehmen aus unterschiedlichen
Branchen. Unsere Leistungen passen wir individuell an Nutzung,
Objektgröße und branchenspezifische Anforderungen an.
</p>
</div>
<div className={styles.grid}>
{industries.map((industry) => {
const Icon = industry.icon;
return (
<Link
key={industry.title}
href={industry.href}
className={styles.card}
>
<span className={styles.cardText}>{industry.title}</span>
<span className={styles.iconWrap} aria-hidden="true">
<Icon size={18} weight="bold" className={styles.icon} />
</span>
</Link>
);
})}
</div>
</div>
</section>
);
}
+4 -1
View File
@@ -211,7 +211,10 @@
padding: 24px 12px;
}
.nav-links > li:first-child,
.nav-links > li:first-child {
padding: 0;
}
.nav-links > li:last-child {
padding: 0;
}
@@ -104,6 +104,20 @@
box-shadow: 0 4px 6px -6px rgb(0 0 0 / 30%);
opacity: 0;
transform: translateY(-8px);
transition:
max-height 300ms ease,
opacity 300ms ease,
transform 300ms ease,
visibility 0s linear 300ms;
background-color: var(--nav-bg);
}
.nav-submenu.open {
transition:
max-height 300ms ease,
opacity 300ms ease,
transform 300ms ease,
visibility 0s linear 0s;
}
.nav-submenu-list li a:focus-visible {
+137
View File
@@ -0,0 +1,137 @@
.services {
padding-block: var(--space-16);
}
.inner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
}
.intro {
display: flex;
flex-direction: column;
gap: var(--space-4);
margin-bottom: var(--space-8);
}
.heading,
.lead {
text-align: center;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
justify-content: center;
gap: var(--space-8);
}
.card {
display: flex;
flex-direction: column;
min-height: 100%;
background: var(--color-surface);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.05);
}
.imageWrap {
position: relative;
width: 100%;
aspect-ratio: 16 / 7;
background: var(--color-surface-alt);
overflow: hidden;
border-bottom: 4px solid var(--color-accent);
}
.image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.cardBody {
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
gap: var(--space-5);
padding: var(--space-5);
}
.cardContent {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.cardTitle {
font-size: var(--font-size-s);
background-color: var(--color-accent);
width: fit-content;
padding: 8px;
color: #ffffff;
letter-spacing: var(--letter-spacing-normal);
border-radius: 4px;
font-weight: var(--font-weight-regular);
}
.cardText {
max-width: 34ch;
color: var(--color-text-muted);
font-size: var(--font-size-xs);
}
.cardBody :global(.btn) {
width: fit-content;
padding: 0;
color: var(--font-body-color);
font-weight: var(--font-weight-semibold);
}
/* Tablet */
@media (min-width: 768px) {
.services {
padding-block: var(--space-24);
}
.intro {
margin-bottom: var(--space-10);
}
.heading {
max-width: 22ch;
text-align: left;
}
.lead {
max-width: 62ch;
text-align: left;
}
/* .card {
max-width: 100%;
} */
.grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
/* Desktop */
@media (min-width: 1024px) {
.services {
padding-block: var(--space-24);
}
.inner {
padding-inline: var(--space-6);
}
.grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
align-items: stretch;
}
}
+94
View File
@@ -0,0 +1,94 @@
import { ArrowRightIcon } from "@phosphor-icons/react/dist/ssr";
import Link from "next/link";
import styles from "./ServiceSection.module.css";
import Image from "next/image";
const services = [
{
title: "Grundreinigung",
description:
"Regelmäßige Reinigung von Arbeitsplätzen, Besprechungsräumen und Gemeinschaftsbereichen für einen gepflegten Eindruck im Alltag.",
image: "/images/services/grundreinigung.jpg",
imageAlt: "Sauberer Büroarbeitsplatz nach der Reinigung",
href: "/leistungen/bueroreinigung",
},
{
title: "Glas- und Fensterreinigung",
description:
"Streifenfreie Reinigung von Fenstern, Glaselementen und Eingangsbereichen für einen professionellen ersten Eindruck.",
image: "/images/services/Fensterreinigung.jpg",
imageAlt: "Große gereinigte Fensterfront eines Geschäftsgebäudes",
href: "/leistungen/glasreinigung",
},
{
title: "Treppenhausreinigung",
description:
"Saubere Eingangs- und Treppenbereiche als Visitenkarte für Mitarbeitende, Kundschaft und Besucher.",
image: "/images/services/treppenhausreinigung.jpg",
imageAlt: "Sauberes Treppenhaus in einem Gewerbeobjekt",
href: "/leistungen/treppenhausreinigung",
},
{
title: "Unterhaltsreinigung",
description:
"Individuell abgestimmte Reinigungsintervalle für dauerhaft gepflegte Büro-, Praxis- und Gewerbeflächen.",
image: "/images/services/unterhaltsreinigung.jpg",
imageAlt: "Reinigungskraft bei der Unterhaltsreinigung in einem Büro",
href: "/leistungen/unterhaltsreinigung",
},
{
title: "Hausmeisterdienste",
description:
"Hausmeisterdienste für Kleinreparaturen bis zur Objektbetreuung – für reibungslose Abläufe.",
image: "/images/services/Hausmeisterdienste.jpg",
imageAlt: "Hausmeisterdienste für Gartenarbeit",
href: "/leistungen/Hausmeisterdienste",
},
];
export default function ServiceSection() {
return (
<section className={styles.services} aria-labelledby="services-heading">
<div className={styles.inner}>
<div className={styles.intro}>
<h2 id="services-heading" className={styles.heading}>
Professionelle Reinigung für Büros, Praxen und Geschäftsräume
</h2>
<p className={`lead ${styles.lead}`}>
Individuelle Reinigungskonzepte für Unternehmen – damit Ihre Räume sauber, zuverlässig und professions sorgenfrei bleiben.
</p>
</div>
<div className={styles.grid}>
{services.map((service) => (
<article key={service.title} className={styles.card}>
<div className={styles.imageWrap}>
<Image
src={service.image}
alt={service.imageAlt}
fill
sizes="(min-width: 1024px) 400px, (min-width: 768px) 50vw, 100vw"
className={styles.image}
/>
</div>
<div className={styles.cardBody}>
<div className={styles.cardContent}>
<h3 className={styles.cardTitle}>{service.title}</h3>
<p className={styles.cardText}>{service.description}</p>
</div>
<Link href={service.href} className="btn btn--tertiary">
Mehr
<span className="btn-icon animate" aria-hidden="true">
<ArrowRightIcon size={16} weight="bold" />
</span>
</Link>
</div>
</article>
))}
</div>
</div>
</section>
);
}
+122
View File
@@ -0,0 +1,122 @@
.benefits {
background: var(--color-background-linear);
padding-block: var(--space-16);
}
.inner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
}
/* Mobile: Intro oben, Grid unten */
.intro {
display: flex;
flex-direction: column;
gap: var(--space-4);
margin-bottom: var(--space-8);
}
.heading,
.lead {
text-align: center;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: var(--space-5);
}
.card {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: var(--space-4);
background: var(--color-surface);
border-radius: 12px;
padding: var(--space-5);
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.05);
}
.iconWrap {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 40px;
height: 40px;
color: var(--color-accent);
}
.cardContent {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.cardTitle {
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
color: var(--font-heading-color);
}
.cardText {
color: var(--color-text-muted);
font-size: var(--font-size-xs);
max-width: 34ch;
}
/* Tablet */
@media (min-width: 768px) {
.benefits {
padding-block: var(--space-24);
}
.intro {
margin-bottom: var(--space-10);
}
.heading {
max-width: 16ch;
text-align: left;
}
.lead {
max-width: 48ch;
text-align: left;
}
.grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
/* Desktop: Intro links, Grid rechts — nebeneinander */
@media (min-width: 1024px) {
.benefits {
padding-block: var(--space-36);
}
.inner {
display: grid;
grid-template-columns: 280px 1fr;
gap: var(--space-16);
align-items: start;
padding-inline: var(--space-6);
}
.intro {
position: sticky;
top: var(--space-8);
margin-bottom: 0;
}
.heading {
text-align: left;
}
.lead {
text-align: left;
}
}
+83
View File
@@ -0,0 +1,83 @@
import {
UsersThreeIcon,
GraduationCapIcon,
ClockIcon,
SealCheckIcon,
LeafIcon,
CurrencyCircleDollarIcon,
} from "@phosphor-icons/react/dist/ssr";
import styles from "./benefitsSection.module.css";
const benefits = [
{
icon: UsersThreeIcon,
title: "Feste Ansprechpartner",
description:
"Keine wechselnden Zuständigkeiten – wir sind immer persönlich für Sie da.",
},
{
icon: GraduationCapIcon,
title: "Geschultes Personal",
description:
"Unsere Teams werden kontinuierlich in modernen Reinigungstechniken geschult.",
},
{
icon: ClockIcon,
title: "Flexible Zeiten",
description:
"Wir reinigen dann, wenn es Ihren Betriebsablauf am wenigsten stört.",
},
{
icon: SealCheckIcon,
title: "Höchste Qualität",
description:
"Strenges Qualitätsmanagement durch regelmäßige Objektkontrollen.",
},
{
icon: LeafIcon,
title: "Umweltfreundlich",
description:
"Einsatz von biologisch abbaubaren Reinigungsmitteln für die Umwelt.",
},
{
icon: CurrencyCircleDollarIcon,
title: "Transparente Kosten",
description:
"Klare Preisgestaltung ohne versteckte Nebenkosten für Ihre Planung.",
},
];
export default function BenefitsSection() {
return (
<section className={styles.benefits} aria-labelledby="benefits-heading">
<div className={styles.inner}>
<div className={styles.intro}>
<h2 id="benefits-heading" className={styles.heading}>
Ihre Vorteile mit uns
</h2>
<p className={`lead ${styles.lead}`}>
Wir verstehen die Anforderungen moderner Unternehmen an einen
Facility-Partner: Verlässlichkeit, Professionalität und Transparenz.
</p>
</div>
<div className={styles.grid}>
{benefits.map((benefit) => {
const Icon = benefit.icon;
return (
<article key={benefit.title} className={styles.card}>
<span className={styles.iconWrap} aria-hidden="true">
<Icon size={24} weight="duotone" />
</span>
<div className={styles.cardContent}>
<h3 className={styles.cardTitle}>{benefit.title}</h3>
<p className={styles.cardText}>{benefit.description}</p>
</div>
</article>
);
})}
</div>
</div>
</section>
);
}
+48
View File
@@ -0,0 +1,48 @@
import type { ElementType } from "react";
import {
PhoneIcon,
EnvelopeSimpleIcon,
ClockIcon,
} from "@phosphor-icons/react/dist/ssr";
export type ContactInfo = {
icon: ElementType;
label: string;
href?: string;
obfuscated?: {
user?: string;
domain?: string;
tel?: string;
};
};
export type ContactCtaData = {
heading: string;
lead: string;
buttonLabel: string;
buttonHref: string;
contacts: ContactInfo[];
};
export const contactCtaData: ContactCtaData = {
heading: "Lassen Sie uns über Ihre Anforderungen sprechen",
lead: "Wir erstellen ein individuelles Reinigungskonzept für Ihr Unternehmen in Hannover und Umgebung. Unverbindlich, transparent und exakt auf Ihre Anforderung abgestimmt.",
buttonLabel: "Angebot anfragen",
buttonHref: "/kontakt",
contacts: [
{
icon: PhoneIcon,
label: "0172 621 0606",
obfuscated: { tel: "01726210606" },
},
{
icon: EnvelopeSimpleIcon,
label: "info@basiclean.de",
obfuscated: { user: "info", domain: "basiclean.de" },
},
{
icon: ClockIcon,
label: "Mo. - Fr. 08:00 - 17:00 Uhr",
},
],
};
@@ -0,0 +1,108 @@
.section {
background: #ffffff;
padding-bottom: var(--space-16);
}
.inner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
}
.card {
background-color: var(--color-accent);
border-radius: var(--radius-lg);
padding: var(--space-8) var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-6);
}
.top {
display: flex;
flex-direction: column;
gap: var(--space-6);
align-items: flex-start;
}
.top :global(.btn) {
font-weight: var(--font-weight-semibold);
}
.text {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.heading {
color: #ffffff;
max-width: 24ch;
}
.lead {
color: rgba(255, 255, 255);
font-size: var(--font-size-xs);
max-width: 72ch;
}
.divider {
border: none;
border-top: 1px solid rgba(255, 255, 255, 0.2);
margin: 0;
}
.contactRow {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.contactItem {
display: inline-flex;
align-items: center;
gap: var(--space-2);
color: #ffffff;
font-size: var(--font-size-xs);
}
@media (min-width: 768px) {
.section {
padding-bottom: var(--space-24);
}
.card {
padding: var(--space-10) var(--space-10);
}
.top {
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: var(--space-8);
}
.contactRow {
flex-direction: row;
gap: var(--space-8);
flex-wrap: wrap;
}
}
@media (min-width: 1024px) {
.inner {
padding-inline: var(--space-6);
}
.card {
padding: var(--space-12) var(--space-12);
}
}
@media (hover: none) {
.button:hover,
.button:focus-visible {
background: #ffffff;
box-shadow: none;
}
}
+73
View File
@@ -0,0 +1,73 @@
"use client";
import Link from "next/link";
import { useEffect, useRef } from "react";
import { ArrowRightIcon } from "@phosphor-icons/react/dist/ssr";
import styles from "./ContactCtaSection.module.css";
import { contactCtaData, type ContactInfo } from "./ContactCtaData";
function ObfuscatedLink({ item }: { item: ContactInfo }) {
const ref = useRef<HTMLAnchorElement>(null);
useEffect(() => {
if (!ref.current) return;
if (item.obfuscated?.tel) {
ref.current.href = `tel:${item.obfuscated.tel}`;
} else if (item.obfuscated?.user && item.obfuscated?.domain) {
ref.current.href = `mailto:${item.obfuscated.user}@${item.obfuscated.domain}`;
}
}, [item.obfuscated]);
const Icon = item.icon;
return (
<a ref={ref} href="#" className={styles.contactItem}>
<Icon size={20} weight="regular" aria-hidden="true" />
{item.label}
</a>
);
}
export default function ContactCtaSection() {
const { heading, lead, buttonLabel, buttonHref, contacts } = contactCtaData;
return (
<section className={styles.section} aria-labelledby="contact-cta-heading">
<div className={styles.inner}>
<div className={styles.card}>
<div className={styles.top}>
<div className={styles.text}>
<h2 id="contact-cta-heading" className={styles.heading}>
{heading}
</h2>
<p className={styles.lead}>{lead}</p>
</div>
<Link href={buttonHref} className="btn btn--filled-inverse">
<span className="btn-icon animate" aria-hidden="true">
<ArrowRightIcon size={16} weight="bold" />
</span>
{buttonLabel}
</Link>
</div>
<hr className={styles.divider} aria-hidden="true" />
<div className={styles.contactRow}>
{contacts.map((item: ContactInfo) =>
item.obfuscated ? (
<ObfuscatedLink key={item.label} item={item} />
) : (
<span key={item.label} className={styles.contactItem}>
<item.icon size={20} weight="regular" aria-hidden="true" />
{item.label}
</span>
),
)}
</div>
</div>
</div>
</section>
);
}
+137
View File
@@ -0,0 +1,137 @@
.section {
background: var(--color-background-linear);
padding-block: var(--space-16);
}
.inner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
display: grid;
grid-template-columns: 1fr;
gap: var(--space-8);
align-items: center;
}
.content {
display: flex;
flex-direction: column;
justify-content: center;
}
.heading {
max-width: 11ch;
text-align: left;
color: var(--color-accent);
}
.accordion {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.item {
width: 100%;
}
.itemHeading {
margin: 0;
}
.trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
width: 100%;
min-height: 52px;
padding: var(--space-3) var(--space-5);
background: var(--color-accent);
color: #ffffff;
border: 0;
border-radius: 9999px;
text-align: left;
cursor: pointer;
}
.trigger:hover,
.trigger:focus-visible {
background: #1f6152;
}
.triggerText {
font-size: var(--font-size-s);
font-weight: var(--font-weight-medium);
line-height: 1.2;
}
.iconWrap {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 24px;
height: 24px;
transition: transform 0.25s ease;
}
.trigger[aria-expanded="true"] .iconWrap {
transform: rotate(180deg);
}
.panel {
max-height: 0;
overflow: hidden;
transition: max-height 0.4s ease;
}
.panelInner {
padding: var(--space-4) var(--space-5) var(--space-2);
}
.answer {
max-width: 52ch;
color: var(--color-text-muted);
font-size: var(----font-size-xs);
line-height: 1.5;
}
@media (min-width: 768px) {
.section {
padding-block: var(--space-24);
}
.inner {
grid-template-columns: 280px 1fr;
gap: var(--space-16);
}
.trigger {
min-height: 56px;
}
}
@media (min-width: 1024px) {
.section {
padding-block: var(--space-36);
}
.inner {
padding-inline: var(--space-6);
}
.accordion {
max-width: 560px;
justify-self: end;
width: 100%;
}
}
@media (hover: none) {
.trigger:hover,
.trigger:focus-visible {
background: var(--color-accent);
box-shadow: 0 4px 14px rgba(38, 115, 97, 0.14);
}
}
+113
View File
@@ -0,0 +1,113 @@
"use client";
import { useId, useRef, useState } from "react";
import { CaretDownIcon } from "@phosphor-icons/react/dist/ssr";
import styles from "./FaqSection.module.css";
export type FaqItem = {
question: string;
answer: string;
};
type FaqSectionProps = {
title: string;
items: FaqItem[];
};
type AccordionItemProps = {
item: FaqItem;
isOpen: boolean;
buttonId: string;
panelId: string;
onToggle: () => void;
};
function AccordionItem({
item,
isOpen,
buttonId,
panelId,
onToggle,
}: AccordionItemProps) {
const panelRef = useRef<HTMLDivElement | null>(null);
const handleToggle = () => {
const panel = panelRef.current;
if (!panel) return;
if (panel.style.maxHeight) {
panel.style.maxHeight = "";
} else {
panel.style.maxHeight = panel.scrollHeight + "px";
}
onToggle();
};
return (
<div className={styles.item}>
<h3 className={styles.itemHeading}>
<button
id={buttonId}
type="button"
className={styles.trigger}
aria-expanded={isOpen}
aria-controls={panelId}
onClick={handleToggle}
>
<span className={styles.triggerText}>{item.question}</span>
<span className={styles.iconWrap} aria-hidden="true">
<CaretDownIcon size={14} weight="bold" />
</span>
</button>
</h3>
<div
id={panelId}
ref={panelRef}
role="region"
aria-labelledby={buttonId}
className={styles.panel}
>
<div className={styles.panelInner}>
<p className={styles.answer}>{item.answer}</p>
</div>
</div>
</div>
);
}
export default function FaqSection({ title, items }: FaqSectionProps) {
const baseId = useId();
const [openIndex, setOpenIndex] = useState<number | null>(null);
return (
<section className={styles.section} aria-labelledby="faq-heading">
<div className={styles.inner}>
<div className={styles.content}>
<h2 id="faq-heading" className={styles.heading}>
{title}
</h2>
</div>
<div className={styles.accordion}>
{items.map((item, index) => {
const isOpen = openIndex === index;
return (
<AccordionItem
key={item.question}
item={item}
isOpen={isOpen}
buttonId={`${baseId}-btn-${index}`}
panelId={`${baseId}-panel-${index}`}
onToggle={() => setOpenIndex(isOpen ? null : index)}
/>
);
})}
</div>
</div>
</section>
);
}
+17
View File
@@ -0,0 +1,17 @@
export const homepageFaq = [
{
question: "Kostenlose Beratung und Angebotserstellung",
answer:
"Wir besprechen Ihr Objekt, den Reinigungsbedarf und die gewünschten Intervalle. Anschließend erhalten Sie ein transparentes Angebot.",
},
{
question: "Individuelle Reinigungslösungen",
answer:
"Unsere Leistungen passen wir an Ihre Branche, Objektgröße und Anforderungen an. So entsteht ein Reinigungsplan, der wirklich zu Ihrem Alltag passt.",
},
{
question: "Zeitverfügbare Umsetzung",
answer:
"Wir planen flexibel und reinigen dann, wenn es Ihren Betrieb am wenigsten stört. Auf Wunsch auch früh morgens, abends oder am Wochenende.",
},
];
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

+42 -11
View File
@@ -1,13 +1,13 @@
:root {
/* Colors */
--btn-bg-primary: #267361;
--btn-bg-primary-hover: #267361;
--btn-bg-primary: var(--color-accent);
--btn-bg-primary-hover: var(--color-accent);
--btn-bg-primary-active: #353030;
--btn-text-light: #fefcfb;
--btn-text-dark: #267361;
--btn-text-dark: var(--color-accent);
--btn-border-primary: #267361;
--btn-border-primary: var(--color-accent);
--btn-border-ghost: #e9e9e9;
--btn-border-ghost-hover: #ececec;
--btn-border-ghost-active: #e1e1e1;
@@ -25,20 +25,21 @@
--btn-focus-offset: 2px;
/* Layout */
--btn-py: 12px;
--btn-px: 16px;
--btn-py: 8px;
--btn-px: 12px;
--btn-py-lg: 16px;
--btn-px-lg: 24px;
--btn-px-lg: 20px;
--btn-radius: 50px;
--btn-gap: 8px;
--btn-max-height: 52px;
--btn-max-height: 46px;
--btn-min-height: 38px;
--btn-icon-size: 18px;
--btn-tertiary-px: 4px;
/* Typography */
--btn-font-family: var(--font-body);
--btn-font-size: 0.889rem;
--btn-font-weight: var(--font-weight-bold);
--btn-font-size: var(--font-size-xs);
--btn-font-weight: var(--font-weight-medium);
--btn-line-height: 1;
/* Shadow */
@@ -122,12 +123,37 @@
transition: none;
}
/* Filled Inverse */
.btn--filled-inverse {
background-color: #ffffff;
color: var(--color-accent);
border-color: #ffffff;
box-shadow: var(--btn-shadow);
}
.btn--filled-inverse .btn-icon {
color: currentColor;
}
.btn--filled-inverse:hover {
background-color: rgba(255, 255, 255, 0.9);
border-color: rgba(255, 255, 255, 0.9);
color: var(--color-accent);
}
.btn--filled-inverse:active {
background-color: rgba(255, 255, 255, 0.75);
border-color: rgba(255, 255, 255, 0.75);
transition: none;
}
/* Outline */
.btn--outline {
background-color: transparent;
color: var(--btn-text-dark);
border-color: var(--btn-border-primary);
box-shadow: var(--btn-shadow-none);
min-height: var(--btn-min-height);
}
.btn--outline .btn-icon {
@@ -218,6 +244,12 @@
border-color: var(--btn-bg-primary);
}
.btn--filled-inverse:hover {
background-color: #ffffff;
border-color: #ffffff;
color: var(--color-accent);
}
.btn--outline:hover {
background-color: transparent;
border-color: var(--btn-border-primary);
@@ -241,6 +273,5 @@
@media (min-width: 768px) {
.btn {
padding: var(--btn-py-lg) var(--btn-px-lg);
font-size: 1rem;
}
}
+8 -1
View File
@@ -47,10 +47,17 @@
--color-surface-alt: #f9f9f9;
--color-background: #ffffff;
--color-background-alt: #f9f9f9;
--color-background-linear: linear-gradient(
to bottom,
#f9f9f9 5%,
#ffffff 50%,
#f9f9f9 100%
);
--color-text: #171717;
--color-text-muted: #5f5f5f;
--color-text-muted: #4a4a4a;
--color-text-soft: #7a7a7a;
--color-heading: #171717;
--color-accent: #267361;
/* Radius */
--radius-none: 0;
+11 -9
View File
@@ -1,8 +1,9 @@
:root {
/* Font sizes */
--font-size-body: 1rem;
--font-size-small: 0.875rem;
--font-size-lead: clamp(1rem, 2.15vw, 1.125rem);
--font-size-xs: clamp(0.79rem, 2vw, 0.889rem);
--font-size-s: 0.889rem;
--font-size-base: clamp(0.889rem, 2.2vw, 1rem);
--font-size-m: clamp(1rem, 2.15vw, 1.125rem);
/* Fluid typography (768px to 1024px) */
--font-size-h1: clamp(1.802rem, 5.71vw, 3.247rem);
@@ -12,7 +13,8 @@
--font-size-h5: clamp(1.125rem, 2.82vw, 1.602rem);
--line-height-body: 1.5;
--line-height-heading: 1.2;
--line-height-heading: 1.1;
--line-height-lead: 1.2;
--letter-spacing-tight: -0.02em;
--letter-spacing-normal: 0;
@@ -36,20 +38,20 @@ a {
p,
li {
font-size: var(--font-size-body);
font-size: var(--font-size-base);
line-height: var(--line-height-body);
}
small,
.small,
.text-small {
font-size: var(--font-size-small);
font-size: var(--font-size-xs);
line-height: 1.4;
}
.lead,
.text-lead {
font-size: var(--font-size-lead);
line-height: 1.4;
font-size: var(--font-size-m);
line-height: var(--line-height-lead);
}
h1,