diff --git a/app/error.module.css b/app/error.module.css index c2b8c7a..7075712 100644 --- a/app/error.module.css +++ b/app/error.module.css @@ -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; diff --git a/app/globals.css b/app/globals.css index 785b825..3ca7dfb 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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); - } } diff --git a/app/not-found.module.css b/app/not-found.module.css index 32368c7..3e6ca0d 100644 --- a/app/not-found.module.css +++ b/app/not-found.module.css @@ -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; diff --git a/app/page.tsx b/app/page.tsx index f9921a8..c7e1ede 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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 (
+ + + + + +
); } diff --git a/components/AboutUsData.ts b/components/AboutUsData.ts new file mode 100644 index 0000000..2168926 --- /dev/null +++ b/components/AboutUsData.ts @@ -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", + }, + ], +}; diff --git a/components/AboutUsSection.module.css b/components/AboutUsSection.module.css new file mode 100644 index 0000000..453a985 --- /dev/null +++ b/components/AboutUsSection.module.css @@ -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; + } +} diff --git a/components/AboutUsSection.tsx b/components/AboutUsSection.tsx new file mode 100644 index 0000000..6fbe0d9 --- /dev/null +++ b/components/AboutUsSection.tsx @@ -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 ( +
+
+

+ {title} +

+ +
+ {members.map((member) => ( +
+
+ {member.imageAlt} +
+ +

{member.name}

+
+ ))} +
+ +
+ {paragraphs.map((paragraph, index) => ( +

+ {paragraph} +

+ ))} +
+
+
+ ); +} diff --git a/components/Footer.module.css b/components/Footer.module.css index 4016256..cdf785c 100644 --- a/components/Footer.module.css +++ b/components/Footer.module.css @@ -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); } diff --git a/components/Header.tsx b/components/Header.tsx index e69f3aa..285724e 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -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 ( diff --git a/components/HeroSection.module.css b/components/HeroSection.module.css index 548f1bc..3f28fb9 100644 --- a/components/HeroSection.module.css +++ b/components/HeroSection.module.css @@ -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 */ diff --git a/components/HeroSection.tsx b/components/HeroSection.tsx index 80e9e4d..2cf6eb2 100644 --- a/components/HeroSection.tsx +++ b/components/HeroSection.tsx @@ -14,11 +14,11 @@ export default function HeroSection() {

- Mehr Sauberkeit, weniger Aufwand für ein sauberes Arbeitsumfeld + Gebäudereinigung für ein makelloses Arbeitsumfeld

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

@@ -27,11 +27,19 @@ export default function HeroSection() { Jetzt Angebot anfragen - + + {/* */} + Leistungen ansehen + +
+
+ - 0172 621 0606 + 0172 6210606
diff --git a/components/IndustrySolutionsSection.module.css b/components/IndustrySolutionsSection.module.css new file mode 100644 index 0000000..80ba5e8 --- /dev/null +++ b/components/IndustrySolutionsSection.module.css @@ -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; + } +} diff --git a/components/IndustrySolutionsSection.tsx b/components/IndustrySolutionsSection.tsx new file mode 100644 index 0000000..a227bcd --- /dev/null +++ b/components/IndustrySolutionsSection.tsx @@ -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 ( +
+
+
+

+ Reinigungsleistungen für verschiedene Branchen +

+

+ Als Reinigungsfirma betreuen wir Unternehmen aus unterschiedlichen + Branchen. Unsere Leistungen passen wir individuell an Nutzung, + Objektgröße und branchenspezifische Anforderungen an. +

+
+ +
+ {industries.map((industry) => { + const Icon = industry.icon; + + return ( + + {industry.title} + + + + ); + })} +
+
+
+ ); +} diff --git a/components/Navbar/Navbar.module.css b/components/Navbar/Navbar.module.css index 28b5b53..894c8e1 100644 --- a/components/Navbar/Navbar.module.css +++ b/components/Navbar/Navbar.module.css @@ -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; } diff --git a/components/Navbar/NavbarSubMenu.module.css b/components/Navbar/NavbarSubMenu.module.css index f838184..188c308 100644 --- a/components/Navbar/NavbarSubMenu.module.css +++ b/components/Navbar/NavbarSubMenu.module.css @@ -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 { diff --git a/components/ServiceSection.module.css b/components/ServiceSection.module.css new file mode 100644 index 0000000..f409a63 --- /dev/null +++ b/components/ServiceSection.module.css @@ -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; + } +} diff --git a/components/ServiceSection.tsx b/components/ServiceSection.tsx new file mode 100644 index 0000000..2742ca5 --- /dev/null +++ b/components/ServiceSection.tsx @@ -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 ( +
+
+
+

+ Professionelle Reinigung für Büros, Praxen und Geschäftsräume +

+

+ Individuelle Reinigungskonzepte für Unternehmen – damit Ihre Räume sauber, zuverlässig und professions sorgenfrei bleiben. +

+
+ +
+ {services.map((service) => ( +
+
+ {service.imageAlt} +
+ +
+
+

{service.title}

+

{service.description}

+
+ + + Mehr + + +
+
+ ))} +
+
+
+ ); +} diff --git a/components/benefitsSection.module.css b/components/benefitsSection.module.css new file mode 100644 index 0000000..6f7f97e --- /dev/null +++ b/components/benefitsSection.module.css @@ -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; + } +} diff --git a/components/benefitsSection.tsx b/components/benefitsSection.tsx new file mode 100644 index 0000000..e74b15c --- /dev/null +++ b/components/benefitsSection.tsx @@ -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 ( +
+
+
+

+ Ihre Vorteile mit uns +

+

+ Wir verstehen die Anforderungen moderner Unternehmen an einen + Facility-Partner: Verlässlichkeit, Professionalität und Transparenz. +

+
+ +
+ {benefits.map((benefit) => { + const Icon = benefit.icon; + return ( +
+ +
+

{benefit.title}

+

{benefit.description}

+
+
+ ); + })} +
+
+
+ ); +} diff --git a/components/contact/ContactCtaData.ts b/components/contact/ContactCtaData.ts new file mode 100644 index 0000000..b4cc7fe --- /dev/null +++ b/components/contact/ContactCtaData.ts @@ -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", + }, + ], +}; diff --git a/components/contact/ContactCtaSection.module.css b/components/contact/ContactCtaSection.module.css new file mode 100644 index 0000000..9d27a52 --- /dev/null +++ b/components/contact/ContactCtaSection.module.css @@ -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; + } +} diff --git a/components/contact/ContactCtaSection.tsx b/components/contact/ContactCtaSection.tsx new file mode 100644 index 0000000..ab73207 --- /dev/null +++ b/components/contact/ContactCtaSection.tsx @@ -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(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 ( + + + ); +} + +export default function ContactCtaSection() { + const { heading, lead, buttonLabel, buttonHref, contacts } = contactCtaData; + + return ( +
+
+
+
+
+

+ {heading} +

+

{lead}

+
+ + + + {buttonLabel} + +
+ + + +
+ {contacts.map((item: ContactInfo) => + item.obfuscated ? ( + + ) : ( + + + ), + )} +
+
+
+
+ ); +} diff --git a/components/faq/FaqSection.module.css b/components/faq/FaqSection.module.css new file mode 100644 index 0000000..9489caf --- /dev/null +++ b/components/faq/FaqSection.module.css @@ -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); + } +} diff --git a/components/faq/FaqSection.tsx b/components/faq/FaqSection.tsx new file mode 100644 index 0000000..a88f4e1 --- /dev/null +++ b/components/faq/FaqSection.tsx @@ -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(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 ( +
+

+ +

+ +
+
+

{item.answer}

+
+
+
+ ); +} + +export default function FaqSection({ title, items }: FaqSectionProps) { + const baseId = useId(); + const [openIndex, setOpenIndex] = useState(null); + + return ( +
+
+
+

+ {title} +

+
+ +
+ {items.map((item, index) => { + const isOpen = openIndex === index; + + return ( + setOpenIndex(isOpen ? null : index)} + /> + ); + })} +
+
+
+ ); +} diff --git a/components/faq/faqData.ts b/components/faq/faqData.ts new file mode 100644 index 0000000..ee7d558 --- /dev/null +++ b/components/faq/faqData.ts @@ -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.", + }, +]; diff --git a/public/images/portrait/abdulkabi-dastan.jpg b/public/images/portrait/abdulkabi-dastan.jpg new file mode 100644 index 0000000..a1d87d3 Binary files /dev/null and b/public/images/portrait/abdulkabi-dastan.jpg differ diff --git a/public/images/portrait/goekhan-oezsayiner.jpg b/public/images/portrait/goekhan-oezsayiner.jpg new file mode 100644 index 0000000..7625f0b Binary files /dev/null and b/public/images/portrait/goekhan-oezsayiner.jpg differ diff --git a/public/images/services/fensterreinigung.jpg b/public/images/services/fensterreinigung.jpg new file mode 100644 index 0000000..0f186d5 Binary files /dev/null and b/public/images/services/fensterreinigung.jpg differ diff --git a/public/images/services/grundreinigung.jpg b/public/images/services/grundreinigung.jpg new file mode 100644 index 0000000..cdeafde Binary files /dev/null and b/public/images/services/grundreinigung.jpg differ diff --git a/public/images/services/hausmeisterdienste.jpg b/public/images/services/hausmeisterdienste.jpg new file mode 100644 index 0000000..5a36b4d Binary files /dev/null and b/public/images/services/hausmeisterdienste.jpg differ diff --git a/public/images/services/treppenhausreinigung.jpg b/public/images/services/treppenhausreinigung.jpg new file mode 100644 index 0000000..a068c84 Binary files /dev/null and b/public/images/services/treppenhausreinigung.jpg differ diff --git a/public/images/services/unterhaltsreinigung.jpg b/public/images/services/unterhaltsreinigung.jpg new file mode 100644 index 0000000..aa9b09b Binary files /dev/null and b/public/images/services/unterhaltsreinigung.jpg differ diff --git a/styles/buttons.css b/styles/buttons.css index 3b6c74b..0365866 100644 --- a/styles/buttons.css +++ b/styles/buttons.css @@ -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; } } diff --git a/styles/tokens.css b/styles/tokens.css index c8aa1f7..671f287 100644 --- a/styles/tokens.css +++ b/styles/tokens.css @@ -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; diff --git a/styles/typography.css b/styles/typography.css index e8df95f..4f337b2 100644 --- a/styles/typography.css +++ b/styles/typography.css @@ -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,