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

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