This commit is contained in:
egnbkts committed 2026-05-13 13:13:57 +02:00
1 parent 8757431c79
commit b2a409e6b7
14 files changed
+228 -117

No files matched your search

+2 -2
View File
@@ -1,10 +1,10 @@
import CookieConsentBanner from "@/components/CookieConsent";
import type { Metadata, Viewport } from "next";
import { Manrope, Nunito_Sans } from "next/font/google";
import "./globals.css";
import Footer from "@/components/Footer";
import Header from "@/components/Header";
import CookieConsentLoader from "@/components/CookieConsentLoader";
const nunitoSans = Nunito_Sans({
subsets: ["latin"],
@@ -94,10 +94,10 @@ export default function RootLayout({
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<CookieConsentLoader />
<Header />
{children}
<Footer />
<CookieConsentBanner />
</body>
</html>
);
+1 -1
View File
@@ -29,5 +29,5 @@
}
:global(#cc-main .cm) {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08) !important;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.08) !important;
}
+53 -25
View File
@@ -1,12 +1,31 @@
"use client";
import { useEffect } from "react";
import * as CookieConsent from "vanilla-cookieconsent";
import "vanilla-cookieconsent/dist/cookieconsent.css";
import "./CookieConsent.module.css";
export default function CookieConsentBanner() {
useEffect(() => {
let mounted = true;
let idleId: number | undefined;
let timeoutId: ReturnType<typeof setTimeout> | undefined;
const loadConsent = async () => {
const [cookieConsentModule] = await Promise.all([
import("vanilla-cookieconsent"),
import("vanilla-cookieconsent/dist/cookieconsent.css"),
]);
const mod = cookieConsentModule as {
run?: (config: unknown) => void;
default?: {
run?: (config: unknown) => void;
};
};
const CookieConsent = mod.run ? mod : mod.default;
if (!mounted || !CookieConsent?.run) return;
CookieConsent.run({
categories: {
necessary: {
@@ -23,10 +42,7 @@ export default function CookieConsentBanner() {
consentModal: {
title: "Ihre Privatsphäre ist uns wichtig",
description:
"Wir verwenden Cookies, um Ihnen die bestmögliche Nutzung unserer Website zu ermöglichen. " +
"Einige Cookies sind technisch notwendig, andere helfen uns, die Website zu verbessern " +
"und Ihnen relevante Inhalte anzuzeigen. Sie können Ihre Auswahl jederzeit unter " +
"<a href='/datenschutz'>Datenschutz</a> anpassen oder widerrufen.",
"Wir setzen notwendige und optionale Cookies ein. Ihre Auswahl können Sie jederzeit unter <a href='/datenschutz'>Datenschutz</a> ändern.",
acceptAllBtn: "Alle akzeptieren",
acceptNecessaryBtn: "Nur notwendige",
showPreferencesBtn: "Einstellungen anpassen",
@@ -44,44 +60,30 @@ export default function CookieConsentBanner() {
{
title: "Ihre Privatsphäre",
description:
"Wir setzen Cookies ein, um die Funktionalität unserer Website sicherzustellen " +
"und unser Angebot kontinuierlich zu verbessern. Sie entscheiden selbst, " +
"welche Kategorien Sie zulassen möchten.",
"Wir setzen Cookies ein, um die Funktionalität unserer Website sicherzustellen und unser Angebot kontinuierlich zu verbessern. Sie entscheiden selbst, welche Kategorien Sie zulassen möchten.",
},
{
title: "Technisch notwendig",
description:
"Diese Cookies sind für den reibungslosen Betrieb der Website unbedingt erforderlich " +
"und können nicht deaktiviert werden. Sie speichern keine personenbezogenen Daten " +
"und werden ausschließlich als Reaktion auf von Ihnen durchgeführte Aktionen gesetzt " +
"(z. B. Spracheinstellungen oder Sicherheits-Token).",
"Diese Cookies sind für den reibungslosen Betrieb der Website unbedingt erforderlich und können nicht deaktiviert werden.",
linkedCategory: "necessary",
},
{
title: "Analyse & Statistik",
description:
"Diese Cookies helfen uns zu verstehen, wie Besucher mit unserer Website interagieren. " +
"Alle Daten werden anonymisiert gesammelt und dienen ausschließlich zur Verbesserung " +
"unseres Angebots. Wir nutzen Google Analytics (GA4) – Ihre IP-Adresse wird dabei " +
"anonymisiert und nicht gespeichert.",
"Diese Cookies helfen uns zu verstehen, wie Besucher mit unserer Website interagieren.",
linkedCategory: "analytics",
},
{
title: "Marketing & externe Medien",
description:
"Diese Cookies ermöglichen die Einbindung externer Inhalte wie YouTube-Videos, " +
"Google Maps, Instagram-, Facebook- und X (Twitter)-Inhalte. " +
"Ohne Ihre Zustimmung werden diese Inhalte nicht geladen und keine Daten übertragen. " +
"Bitte beachten Sie, dass bei Aktivierung personenbezogene Daten " +
"an Drittanbieter (ggf. auch in die USA) übertragen werden können.",
"Diese Cookies ermöglichen die Einbindung externer Inhalte wie YouTube-Videos, Google Maps, Instagram-, Facebook- und X-Inhalte.",
linkedCategory: "marketing",
},
{
title: "Weitere Informationen",
description:
"Bei Fragen zu unserer Cookie-Richtlinie wenden Sie sich bitte an uns über das " +
"<a href='/kontakt'>Kontaktformular</a>. Mehr Details finden Sie in unserer " +
"<a href='/datenschutz'>Datenschutzerklärung</a>.",
"Bei Fragen wenden Sie sich bitte über das <a href='/kontakt'>Kontaktformular</a> an uns. Mehr Details finden Sie in unserer <a href='/datenschutz'>Datenschutzerklärung</a>.",
},
],
},
@@ -89,6 +91,32 @@ export default function CookieConsentBanner() {
},
},
});
};
if (window.requestIdleCallback) {
idleId = window.requestIdleCallback(
() => {
void loadConsent();
},
{ timeout: 2000 },
);
} else {
timeoutId = setTimeout(() => {
void loadConsent();
}, 300);
}
return () => {
mounted = false;
if (idleId && window.cancelIdleCallback) {
window.cancelIdleCallback(idleId);
}
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, []);
return null;
+11
View File
@@ -0,0 +1,11 @@
"use client";
import dynamic from "next/dynamic";
const CookieConsentBanner = dynamic(() => import("./CookieConsent"), {
ssr: false,
});
export default function CookieConsentLoader() {
return <CookieConsentBanner />;
}
+1 -1
View File
@@ -4,7 +4,7 @@
*/
:root {
--footer-bg: #171717;
--footer-bg: #272727 ;
--footer-text: #a1a1aa;
--footer-text-hover: #ffffff;
--footer-title: #ffffff;
+1 -1
View File
@@ -62,7 +62,7 @@ export default function Footer() {
{/* Bottom: Copyright + Social */}
<div className={styles.footer__bottom}>
<p className={styles.footer__copy}>
© {currentYear} ENVU. Alle Rechte vorbehalten.
© {currentYear}. Alle Rechte vorbehalten.
</p>
<div className={styles.footer__social}>
+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 (
+21 -36
View File
@@ -2,9 +2,12 @@
position: relative;
background: var(--color-background-linear);
padding-block: var(--space-16);
overflow: hidden;
}
.inner {
position: relative;
z-index: 1;
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
@@ -14,68 +17,45 @@
position: absolute;
top: 50%;
left: 50%;
right: auto;
transform: translate(-50%, -50%);
right: var(--space-4);
width: 100%;
height: 80%;
opacity: 0.08;
width: min(78vw, 360px);
height: auto;
opacity: 0.05;
pointer-events: none;
user-select: none;
z-index: 0;
}
.content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.heading {
max-width: 15ch;
}
.lead {
max-width: 40ch;
}
.actions {
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--space-4);
margin-top: var(--space-2);
}
.actions :global(.btn) {
width: fit-content;
font-weight: var(--font-weight-semibold);
}
.actions :global(.btn--tertiary) {
padding-top: 0;
padding-bottom: 0;
color: var(--font-body-color);
}
/* Tablet */
@media (min-width: 768px) {
.hero {
padding-block: var(--space-28);
}
.heroDecoration {
position: absolute;
top: 50%;
left: auto;
right: 0;
transform: translateY(-50%);
height: 90%;
width: 50%;
width: min(42vw, 420px);
opacity: 0.06;
}
.actions {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: var(--space-6);
}
.heading {
max-width: 24ch;
}
@@ -85,12 +65,17 @@
}
}
/* Desktop */
@media (min-width: 1024px) {
.hero {
padding-block: var(--space-36);
}
.inner {
padding-inline: var(--space-6);
}
.heroDecoration {
width: min(44vw, 560px);
opacity: 0.08;
}
}
+12 -7
View File
@@ -1,17 +1,22 @@
import { ArrowRightIcon, PhoneIcon } from "@phosphor-icons/react/dist/ssr";
import Link from "next/link";
import styles from "./HeroSection.module.css";
import { companyData } from "@/data/companyData";
import Image from "next/image";
export default function HeroSection() {
return (
<section className={styles.hero} aria-labelledby="hero-heading">
<img
<div className={styles.inner}>
<Image
src="/logo/logo-mininmal-basiclean-grey.svg"
alt=""
aria-hidden="true"
className={styles.heroDecoration}
width={620}
height={620}
unoptimized
/>
<div className={styles.inner}>
<div className={styles.content}>
<h1 id="hero-heading" className={styles.heading}>
Gebäudereinigung für ein makelloses Arbeitsumfeld
@@ -28,18 +33,18 @@ export default function HeroSection() {
Jetzt Angebot anfragen
</Link>
<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">
<a
href={`tel:${companyData.contact.phone.value}`}
className="btn btn--tertiary"
>
<span className="btn-icon" aria-hidden="true">
<PhoneIcon size={18} weight="bold" />
</span>
0172 6210606
{companyData.contact.phone.display}
</a>
</div>
</div>
+18 -11
View File
@@ -5,17 +5,23 @@ import {
ClockIcon,
} from "@phosphor-icons/react/dist/ssr";
export type ContactInfo = {
export type CompanyContactKey = "phone" | "email" | "openingHours";
export type CompanyContactItem = {
type: "company";
contactKey: CompanyContactKey;
icon: ElementType;
};
export type StaticContactItem = {
type: "static";
icon: ElementType;
label: string;
href?: string;
obfuscated?: {
user?: string;
domain?: string;
tel?: string;
};
};
export type ContactInfo = CompanyContactItem | StaticContactItem;
export type ContactCtaData = {
heading: string;
lead: string;
@@ -31,18 +37,19 @@ export const contactCtaData: ContactCtaData = {
buttonHref: "/kontakt",
contacts: [
{
type: "company",
contactKey: "phone",
icon: PhoneIcon,
label: "0172 621 0606",
obfuscated: { tel: "01726210606" },
},
{
type: "company",
contactKey: "email",
icon: EnvelopeSimpleIcon,
label: "info@basiclean.de",
obfuscated: { user: "info", domain: "basiclean.de" },
},
{
type: "company",
contactKey: "openingHours",
icon: ClockIcon,
label: "Mo. - Fr. 08:00 - 17:00 Uhr",
},
],
};
+65 -28
View File
@@ -1,32 +1,35 @@
"use client";
import Link from "next/link";
import { useEffect, useRef } from "react";
import { ArrowRightIcon } from "@phosphor-icons/react/dist/ssr";
import { companyData } from "@/data/companyData";
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}`;
function resolveCompanyContact(
item: Extract<ContactInfo, { type: "company" }>,
) {
if (item.contactKey === "phone") {
return {
label: companyData.contact.phone.display,
href: `tel:${companyData.contact.phone.value}`,
};
}
}, [item.obfuscated]);
const Icon = item.icon;
if (item.contactKey === "email") {
const email = `${companyData.contact.email.user}@${companyData.contact.email.domain}`;
return (
<a ref={ref} href="#" className={styles.contactItem}>
<Icon size={20} weight="regular" aria-hidden="true" />
{item.label}
</a>
);
return {
label: email,
href: `mailto:${email}`,
};
}
if (item.contactKey === "openingHours") {
return {
label: companyData.contact.openingHours.display,
};
}
return null;
}
export default function ContactCtaSection() {
@@ -55,16 +58,50 @@ export default function ContactCtaSection() {
<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}>
{contacts.map((item) => {
const icon = (
<item.icon size={20} weight="regular" aria-hidden="true" />
);
if (item.type === "company") {
const resolved = resolveCompanyContact(item);
if (!resolved) return null;
return (
<a
key={item.contactKey}
href={resolved.href}
className={styles.contactItem}
aria-label={resolved.label}
>
{icon}
{resolved.label}
</a>
);
}
if (item.href) {
return (
<a
key={item.label}
href={item.href}
className={styles.contactItem}
aria-label={item.label}
>
{icon}
{item.label}
</a>
);
}
return (
<span key={item.label} className={styles.contactItem}>
{icon}
{item.label}
</span>
),
)}
);
})}
</div>
</div>
</div>
+18
View File
@@ -0,0 +1,18 @@
export const companyData = {
name: "Basiclean",
legalName: "Basiclean",
slogan: "Wo Sauberkeit Standard ist",
contact: {
email: {
user: "info",
domain: "basiclean.de",
},
phone: {
display: "0172 621 0606",
value: "+491726210606",
},
openingHours: {
display: "Mo. - Fr. 08:00 - 17:00 Uhr",
},
},
} as const;
+17
View File
@@ -0,0 +1,17 @@
import { companyData } from "./companyData";
export function getCompanyEmail() {
return `${companyData.contact.email.user}@${companyData.contact.email.domain}`;
}
export function getCompanyEmailHref() {
return `mailto:${getCompanyEmail()}`;
}
export function getCompanyPhoneDisplay() {
return companyData.contact.phone.display;
}
export function getCompanyPhoneHref() {
return `tel:${companyData.contact.phone.value}`;
}
+7 -4
View File
@@ -1,12 +1,15 @@
import type { NextConfig } from "next";
const allowedOrigin = process.env.NEXT_ALLOWED_DEV_ORIGIN;
const nextConfig: NextConfig = {
/* config options here */
output: "standalone",
trailingSlash: false,
poweredByHeader: false,
allowedDevOrigins: [
"192.168.178.59", // deine PC-IP
],
};
if (allowedOrigin) {
nextConfig.allowedDevOrigins = [allowedOrigin];
}
export default nextConfig;