Styles/global #1

Merged
egnbkts merged 35 commits from styles/global into main 2026-08-12 10:14:58 +02:00
14 changed files with 228 additions and 117 deletions
Showing only changes of commit b2a409e6b7 - Show all commits

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 type { Metadata, Viewport } from "next";
import { Manrope, Nunito_Sans } from "next/font/google"; import { Manrope, Nunito_Sans } from "next/font/google";
import "./globals.css"; import "./globals.css";
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
import Header from "@/components/Header"; import Header from "@/components/Header";
import CookieConsentLoader from "@/components/CookieConsentLoader";
const nunitoSans = Nunito_Sans({ const nunitoSans = Nunito_Sans({
subsets: ["latin"], subsets: ["latin"],
@@ -94,10 +94,10 @@ export default function RootLayout({
type="application/ld+json" type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/> />
<CookieConsentLoader />
<Header /> <Header />
{children} {children}
<Footer /> <Footer />
<CookieConsentBanner />
</body> </body>
</html> </html>
); );
+1 -1
View File
@@ -29,5 +29,5 @@
} }
:global(#cc-main .cm) { :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"; "use client";
import { useEffect } from "react"; import { useEffect } from "react";
import * as CookieConsent from "vanilla-cookieconsent";
import "vanilla-cookieconsent/dist/cookieconsent.css";
import "./CookieConsent.module.css"; import "./CookieConsent.module.css";
export default function CookieConsentBanner() { export default function CookieConsentBanner() {
useEffect(() => { 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({ CookieConsent.run({
categories: { categories: {
necessary: { necessary: {
@@ -23,10 +42,7 @@ export default function CookieConsentBanner() {
consentModal: { consentModal: {
title: "Ihre Privatsphäre ist uns wichtig", title: "Ihre Privatsphäre ist uns wichtig",
description: description:
"Wir verwenden Cookies, um Ihnen die bestmögliche Nutzung unserer Website zu ermöglichen. " + "Wir setzen notwendige und optionale Cookies ein. Ihre Auswahl können Sie jederzeit unter <a href='/datenschutz'>Datenschutz</a> ändern.",
"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.",
acceptAllBtn: "Alle akzeptieren", acceptAllBtn: "Alle akzeptieren",
acceptNecessaryBtn: "Nur notwendige", acceptNecessaryBtn: "Nur notwendige",
showPreferencesBtn: "Einstellungen anpassen", showPreferencesBtn: "Einstellungen anpassen",
@@ -44,44 +60,30 @@ export default function CookieConsentBanner() {
{ {
title: "Ihre Privatsphäre", title: "Ihre Privatsphäre",
description: description:
"Wir setzen Cookies ein, um die Funktionalität unserer Website sicherzustellen " + "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.",
"und unser Angebot kontinuierlich zu verbessern. Sie entscheiden selbst, " +
"welche Kategorien Sie zulassen möchten.",
}, },
{ {
title: "Technisch notwendig", title: "Technisch notwendig",
description: description:
"Diese Cookies sind für den reibungslosen Betrieb der Website unbedingt erforderlich " + "Diese Cookies sind für den reibungslosen Betrieb der Website unbedingt erforderlich und können nicht deaktiviert werden.",
"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).",
linkedCategory: "necessary", linkedCategory: "necessary",
}, },
{ {
title: "Analyse & Statistik", title: "Analyse & Statistik",
description: description:
"Diese Cookies helfen uns zu verstehen, wie Besucher mit unserer Website interagieren. " + "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.",
linkedCategory: "analytics", linkedCategory: "analytics",
}, },
{ {
title: "Marketing & externe Medien", title: "Marketing & externe Medien",
description: description:
"Diese Cookies ermöglichen die Einbindung externer Inhalte wie YouTube-Videos, " + "Diese Cookies ermöglichen die Einbindung externer Inhalte wie YouTube-Videos, Google Maps, Instagram-, Facebook- und X-Inhalte.",
"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.",
linkedCategory: "marketing", linkedCategory: "marketing",
}, },
{ {
title: "Weitere Informationen", title: "Weitere Informationen",
description: description:
"Bei Fragen zu unserer Cookie-Richtlinie wenden Sie sich bitte an uns über das " + "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>.",
"<a href='/kontakt'>Kontaktformular</a>. 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; 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 { :root {
--footer-bg: #171717; --footer-bg: #272727 ;
--footer-text: #a1a1aa; --footer-text: #a1a1aa;
--footer-text-hover: #ffffff; --footer-text-hover: #ffffff;
--footer-title: #ffffff; --footer-title: #ffffff;
+1 -1
View File
@@ -62,7 +62,7 @@ export default function Footer() {
{/* Bottom: Copyright + Social */} {/* Bottom: Copyright + Social */}
<div className={styles.footer__bottom}> <div className={styles.footer__bottom}>
<p className={styles.footer__copy}> <p className={styles.footer__copy}>
© {currentYear} ENVU. Alle Rechte vorbehalten. © {currentYear}. Alle Rechte vorbehalten.
</p> </p>
<div className={styles.footer__social}> <div className={styles.footer__social}>
+1 -1
View File
@@ -1,5 +1,5 @@
import styles from "./Header.module.css"; import styles from "./Header.module.css";
import Navbar from "./navbar/Navbar"; import Navbar from "./Navbar/Navbar";
export default function Header() { export default function Header() {
return ( return (
+21 -36
View File
@@ -2,9 +2,12 @@
position: relative; position: relative;
background: var(--color-background-linear); background: var(--color-background-linear);
padding-block: var(--space-16); padding-block: var(--space-16);
overflow: hidden;
} }
.inner { .inner {
position: relative;
z-index: 1;
max-width: var(--container-width); max-width: var(--container-width);
margin-inline: auto; margin-inline: auto;
padding-inline: var(--space-4); padding-inline: var(--space-4);
@@ -14,68 +17,45 @@
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
right: auto;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
right: var(--space-4); width: min(78vw, 360px);
width: 100%; height: auto;
height: 80%; opacity: 0.05;
opacity: 0.08;
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
z-index: 0; z-index: 0;
} }
.content { .content {
position: relative;
z-index: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-5); 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) { @media (min-width: 768px) {
.hero { .hero {
padding-block: var(--space-28); padding-block: var(--space-28);
} }
.heroDecoration { .heroDecoration {
position: absolute;
top: 50%; top: 50%;
left: auto;
right: 0;
transform: translateY(-50%); transform: translateY(-50%);
height: 90%; width: min(42vw, 420px);
width: 50%; opacity: 0.06;
} }
.actions { .actions {
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
gap: var(--space-6); gap: var(--space-6);
} }
.heading { .heading {
max-width: 24ch; max-width: 24ch;
} }
@@ -85,12 +65,17 @@
} }
} }
/* Desktop */
@media (min-width: 1024px) { @media (min-width: 1024px) {
.hero { .hero {
padding-block: var(--space-36); padding-block: var(--space-36);
} }
.inner { .inner {
padding-inline: var(--space-6); 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 { ArrowRightIcon, PhoneIcon } from "@phosphor-icons/react/dist/ssr";
import Link from "next/link"; import Link from "next/link";
import styles from "./HeroSection.module.css"; import styles from "./HeroSection.module.css";
import { companyData } from "@/data/companyData";
import Image from "next/image";
export default function HeroSection() { export default function HeroSection() {
return ( return (
<section className={styles.hero} aria-labelledby="hero-heading"> <section className={styles.hero} aria-labelledby="hero-heading">
<img <div className={styles.inner}>
<Image
src="/logo/logo-mininmal-basiclean-grey.svg" src="/logo/logo-mininmal-basiclean-grey.svg"
alt="" alt=""
aria-hidden="true" aria-hidden="true"
className={styles.heroDecoration} className={styles.heroDecoration}
width={620}
height={620}
unoptimized
/> />
<div className={styles.inner}>
<div className={styles.content}> <div className={styles.content}>
<h1 id="hero-heading" className={styles.heading}> <h1 id="hero-heading" className={styles.heading}>
Gebäudereinigung für ein makelloses Arbeitsumfeld Gebäudereinigung für ein makelloses Arbeitsumfeld
@@ -28,18 +33,18 @@ export default function HeroSection() {
Jetzt Angebot anfragen Jetzt Angebot anfragen
</Link> </Link>
<Link href="/Leistungen" 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 Leistungen ansehen
</Link> </Link>
</div> </div>
<div className={styles.actions}> <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"> <span className="btn-icon" aria-hidden="true">
<PhoneIcon size={18} weight="bold" /> <PhoneIcon size={18} weight="bold" />
</span> </span>
0172 6210606 {companyData.contact.phone.display}
</a> </a>
</div> </div>
</div> </div>
+18 -11
View File
@@ -5,17 +5,23 @@ import {
ClockIcon, ClockIcon,
} from "@phosphor-icons/react/dist/ssr"; } 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; icon: ElementType;
label: string; label: string;
href?: string; href?: string;
obfuscated?: {
user?: string;
domain?: string;
tel?: string;
};
}; };
export type ContactInfo = CompanyContactItem | StaticContactItem;
export type ContactCtaData = { export type ContactCtaData = {
heading: string; heading: string;
lead: string; lead: string;
@@ -31,18 +37,19 @@ export const contactCtaData: ContactCtaData = {
buttonHref: "/kontakt", buttonHref: "/kontakt",
contacts: [ contacts: [
{ {
type: "company",
contactKey: "phone",
icon: PhoneIcon, icon: PhoneIcon,
label: "0172 621 0606",
obfuscated: { tel: "01726210606" },
}, },
{ {
type: "company",
contactKey: "email",
icon: EnvelopeSimpleIcon, icon: EnvelopeSimpleIcon,
label: "info@basiclean.de",
obfuscated: { user: "info", domain: "basiclean.de" },
}, },
{ {
type: "company",
contactKey: "openingHours",
icon: ClockIcon, 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 Link from "next/link";
import { useEffect, useRef } from "react";
import { ArrowRightIcon } from "@phosphor-icons/react/dist/ssr"; import { ArrowRightIcon } from "@phosphor-icons/react/dist/ssr";
import { companyData } from "@/data/companyData";
import styles from "./ContactCtaSection.module.css"; import styles from "./ContactCtaSection.module.css";
import { contactCtaData, type ContactInfo } from "./ContactCtaData"; import { contactCtaData, type ContactInfo } from "./ContactCtaData";
function ObfuscatedLink({ item }: { item: ContactInfo }) { function resolveCompanyContact(
const ref = useRef<HTMLAnchorElement>(null); item: Extract<ContactInfo, { type: "company" }>,
) {
useEffect(() => { if (item.contactKey === "phone") {
if (!ref.current) return; return {
label: companyData.contact.phone.display,
if (item.obfuscated?.tel) { href: `tel:${companyData.contact.phone.value}`,
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; if (item.contactKey === "email") {
const email = `${companyData.contact.email.user}@${companyData.contact.email.domain}`;
return ( return {
<a ref={ref} href="#" className={styles.contactItem}> label: email,
<Icon size={20} weight="regular" aria-hidden="true" /> href: `mailto:${email}`,
{item.label} };
</a> }
);
if (item.contactKey === "openingHours") {
return {
label: companyData.contact.openingHours.display,
};
}
return null;
} }
export default function ContactCtaSection() { export default function ContactCtaSection() {
@@ -55,16 +58,50 @@ export default function ContactCtaSection() {
<hr className={styles.divider} aria-hidden="true" /> <hr className={styles.divider} aria-hidden="true" />
<div className={styles.contactRow}> <div className={styles.contactRow}>
{contacts.map((item: ContactInfo) => {contacts.map((item) => {
item.obfuscated ? ( const icon = (
<ObfuscatedLink key={item.label} item={item} />
) : (
<span key={item.label} className={styles.contactItem}>
<item.icon size={20} weight="regular" aria-hidden="true" /> <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} {item.label}
</span> </span>
), );
)} })}
</div> </div>
</div> </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"; import type { NextConfig } from "next";
const allowedOrigin = process.env.NEXT_ALLOWED_DEV_ORIGIN;
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ output: "standalone",
trailingSlash: false, trailingSlash: false,
poweredByHeader: false, poweredByHeader: false,
allowedDevOrigins: [
"192.168.178.59", // deine PC-IP
],
}; };
if (allowedOrigin) {
nextConfig.allowedDevOrigins = [allowedOrigin];
}
export default nextConfig; export default nextConfig;