feat: Integrate Directus CMS, add i18n with next-intl, and configure project tooling with pnpm, husky, and commitlint.**
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, m, LazyMotion, domAnimation } from 'framer-motion';
|
||||
import { ArrowUp, Home, Info, Menu, X } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button } from './Button';
|
||||
import { Reveal } from './Reveal';
|
||||
import { AnimatePresence, m, LazyMotion, domAnimation } from "framer-motion";
|
||||
import { ArrowUp, Home, Info, Menu, X } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button } from "./Button";
|
||||
import { Reveal } from "./Reveal";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
const t = useTranslations("Layout");
|
||||
const pathname = usePathname();
|
||||
const [showScrollTop, setShowScrollTop] = useState(false);
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
@@ -20,9 +22,9 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
setShowScrollTop(window.scrollY > 400);
|
||||
setIsScrolled(window.scrollY > 20);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -30,124 +32,131 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
}, [pathname]);
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
};
|
||||
|
||||
const isActive = (path: string) => pathname === path;
|
||||
const isActive = (path: string) =>
|
||||
pathname === path || pathname === `/en${path}` || pathname === `/de${path}`;
|
||||
|
||||
const navLinks = [
|
||||
{ href: '/', label: 'Startseite', icon: Home },
|
||||
{ href: '/ueber-uns', label: 'Über uns', icon: Info },
|
||||
{ href: "/", label: t("nav.home"), icon: Home },
|
||||
{ href: "/ueber-uns", label: t("nav.about"), icon: Info },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col font-sans">
|
||||
<Reveal direction="down" fullWidth trigger="mount" className="fixed top-0 left-0 right-0 z-[100]">
|
||||
<Reveal
|
||||
direction="down"
|
||||
fullWidth
|
||||
trigger="mount"
|
||||
className="fixed top-0 left-0 right-0 z-[100]"
|
||||
>
|
||||
<header
|
||||
className={`transition-all duration-300 flex items-center py-1 ${
|
||||
isScrolled
|
||||
? 'bg-white/90 backdrop-blur-lg border-b border-slate-200 shadow-sm'
|
||||
: 'bg-gradient-to-b from-white/80 via-white/40 to-transparent'
|
||||
? "bg-white/90 backdrop-blur-lg border-b border-slate-200 shadow-sm"
|
||||
: "bg-gradient-to-b from-white/80 via-white/40 to-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="container-custom flex justify-between items-center w-full relative z-10">
|
||||
<Link
|
||||
href="/"
|
||||
className="relative z-10 flex items-center group"
|
||||
aria-label="MB Grid Solutions - Zur Startseite"
|
||||
>
|
||||
<div className={`relative transition-all duration-300 ${isScrolled ? 'h-[50px] md:h-[80px] w-[120px] md:w-[200px] mt-0 mb-[-10px]' : 'h-[80px] md:h-[140px] w-[180px] md:w-[320px] mt-2 md:mt-4 mb-[-20px] md:mb-[-40px]'}`}>
|
||||
<Image
|
||||
src="/assets/logo.png"
|
||||
alt="MB Grid Solutions"
|
||||
fill
|
||||
className="object-contain"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center gap-8" aria-label="Hauptnavigation">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`relative px-2 py-1 text-sm font-bold tracking-tight transition-all group ${
|
||||
isActive(link.href)
|
||||
? 'text-primary'
|
||||
: `${isScrolled ? 'text-slate-600' : 'text-slate-900'} hover:text-primary`
|
||||
}`}
|
||||
>
|
||||
{link.label}
|
||||
<span className={`absolute -bottom-1 left-0 w-full h-0.5 bg-accent transition-transform duration-300 origin-left ${isActive(link.href) ? 'scale-x-100' : 'scale-x-0 group-hover:scale-x-100'}`} />
|
||||
</Link>
|
||||
))}
|
||||
<Button
|
||||
href="/kontakt"
|
||||
className="ml-4 !py-2 !px-5 !text-[10px]"
|
||||
<div className="container-custom flex justify-between items-center w-full relative z-10">
|
||||
<Link
|
||||
href="/"
|
||||
className="relative z-10 flex items-center group"
|
||||
aria-label={`${t("nav.home")} - Zur Startseite`}
|
||||
>
|
||||
Projekt anfragen
|
||||
</Button>
|
||||
</nav>
|
||||
<div
|
||||
className={`relative transition-all duration-300 ${isScrolled ? "h-[50px] md:h-[80px] w-[120px] md:w-[200px] mt-0 mb-[-10px]" : "h-[80px] md:h-[140px] w-[180px] md:w-[320px] mt-2 md:mt-4 mb-[-20px] md:mb-[-40px]"}`}
|
||||
>
|
||||
<Image
|
||||
src="/assets/logo.png"
|
||||
alt="MB Grid Solutions"
|
||||
fill
|
||||
className="object-contain"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Mobile Menu Toggle */}
|
||||
<button
|
||||
className="md:hidden p-2 text-slate-600 hover:text-primary transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
aria-label="Menü öffnen"
|
||||
>
|
||||
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
{/* Desktop Navigation */}
|
||||
<nav
|
||||
className="hidden md:flex items-center gap-8"
|
||||
aria-label="Hauptnavigation"
|
||||
>
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`relative px-2 py-1 text-sm font-bold tracking-tight transition-all group ${
|
||||
isActive(link.href)
|
||||
? "text-primary"
|
||||
: `${isScrolled ? "text-slate-600" : "text-slate-900"} hover:text-primary`
|
||||
}`}
|
||||
>
|
||||
{link.label}
|
||||
<span
|
||||
className={`absolute -bottom-1 left-0 w-full h-0.5 bg-accent transition-transform duration-300 origin-left ${isActive(link.href) ? "scale-x-100" : "scale-x-0 group-hover:scale-x-100"}`}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
<Button href="/kontakt" className="ml-4 !py-2 !px-5 !text-[10px]">
|
||||
{t("nav.cta")}
|
||||
</Button>
|
||||
</nav>
|
||||
|
||||
{/* Mobile Menu Toggle */}
|
||||
<button
|
||||
className="md:hidden p-2 text-slate-600 hover:text-primary transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
aria-label="Menü öffnen"
|
||||
>
|
||||
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
</Reveal>
|
||||
|
||||
{/* Mobile Menu Overlay */}
|
||||
<LazyMotion features={domAnimation}>
|
||||
<AnimatePresence>
|
||||
{isMobileMenuOpen && (
|
||||
<m.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className="fixed inset-0 z-[90] bg-white pt-32 px-6 md:hidden"
|
||||
>
|
||||
<nav className="flex flex-col gap-4">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`flex items-center gap-4 p-4 rounded-xl text-lg font-semibold transition-all ${
|
||||
isActive(link.href)
|
||||
? 'text-accent bg-accent/5'
|
||||
: 'text-slate-600 hover:text-primary hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<link.icon size={24} />
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
<Button
|
||||
href="/kontakt"
|
||||
className="mt-4 w-full"
|
||||
>
|
||||
Projekt anfragen
|
||||
</Button>
|
||||
</nav>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<AnimatePresence>
|
||||
{isMobileMenuOpen && (
|
||||
<m.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className="fixed inset-0 z-[90] bg-white pt-32 px-6 md:hidden"
|
||||
>
|
||||
<nav className="flex flex-col gap-4">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`flex items-center gap-4 p-4 rounded-xl text-lg font-semibold transition-all ${
|
||||
isActive(link.href)
|
||||
? "text-accent bg-accent/5"
|
||||
: "text-slate-600 hover:text-primary hover:bg-slate-50"
|
||||
}`}
|
||||
>
|
||||
<link.icon size={24} />
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
<Button href="/kontakt" className="mt-4 w-full">
|
||||
{t("nav.cta")}
|
||||
</Button>
|
||||
</nav>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</LazyMotion>
|
||||
|
||||
<main className="flex-grow">
|
||||
{children}
|
||||
</main>
|
||||
<main className="flex-grow">{children}</main>
|
||||
|
||||
<button
|
||||
onClick={scrollToTop}
|
||||
className={`fixed bottom-8 right-8 w-12 h-12 bg-primary text-white rounded-full flex items-center justify-center cursor-pointer z-[80] shadow-xl transition-all duration-300 hover:-translate-y-1 hover:bg-accent ${
|
||||
showScrollTop ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4 pointer-events-none'
|
||||
showScrollTop
|
||||
? "opacity-100 translate-y-0"
|
||||
: "opacity-0 translate-y-4 pointer-events-none"
|
||||
}`}
|
||||
aria-label="Nach oben scrollen"
|
||||
>
|
||||
@@ -157,87 +166,128 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
<Reveal fullWidth>
|
||||
<footer className="bg-slate-900 text-slate-300 py-16 md:py-24 relative overflow-hidden group">
|
||||
<div className="absolute inset-0 grid-pattern opacity-[0.08] pointer-events-none" />
|
||||
|
||||
|
||||
{/* Animated Tech Lines */}
|
||||
<LazyMotion features={domAnimation}>
|
||||
<m.div
|
||||
animate={{ x: ['-100%', '100%'] }}
|
||||
transition={{ duration: 15, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-accent/30 to-transparent"
|
||||
/>
|
||||
<m.div
|
||||
animate={{ x: ['100%', '-100%'] }}
|
||||
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-accent/20 to-transparent"
|
||||
/>
|
||||
<m.div
|
||||
animate={{ x: ["-100%", "100%"] }}
|
||||
transition={{ duration: 15, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-accent/30 to-transparent"
|
||||
/>
|
||||
<m.div
|
||||
animate={{ x: ["100%", "-100%"] }}
|
||||
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-accent/20 to-transparent"
|
||||
/>
|
||||
</LazyMotion>
|
||||
|
||||
{/* Corner Accents */}
|
||||
<div className="tech-corner top-8 left-8 border-t border-l border-white/10 group-hover:border-accent/30 transition-colors duration-700" />
|
||||
<div className="tech-corner bottom-8 right-8 border-b border-r border-white/10 group-hover:border-accent/30 transition-colors duration-700" />
|
||||
|
||||
|
||||
<div className="container-custom relative z-10">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10 md:gap-12 mb-12 md:mb-16">
|
||||
<div className="lg:col-span-2">
|
||||
<Link href="/" className="inline-block mb-6 md:mb-8 group">
|
||||
<div className="relative h-16 md:h-20 w-48 brightness-0 invert opacity-80 group-hover:opacity-100 transition-opacity">
|
||||
<Image
|
||||
src="/assets/logo.png"
|
||||
alt="MB Grid Solutions"
|
||||
fill
|
||||
className="object-contain object-left"
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10 md:gap-12 mb-12 md:mb-16">
|
||||
<div className="lg:col-span-2">
|
||||
<Link href="/" className="inline-block mb-6 md:mb-8 group">
|
||||
<div className="relative h-16 md:h-20 w-48 brightness-0 invert opacity-80 group-hover:opacity-100 transition-opacity">
|
||||
<Image
|
||||
src="/assets/logo.png"
|
||||
alt="MB Grid Solutions"
|
||||
fill
|
||||
className="object-contain object-left"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<p className="text-slate-400 max-w-md leading-relaxed mb-8">
|
||||
{t("footer.description")}
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
{/* Social links could go here */}
|
||||
</div>
|
||||
</Link>
|
||||
<p className="text-slate-400 max-w-md leading-relaxed mb-8">
|
||||
Ihr spezialisierter Partner für herstellerneutrale technische Beratung und Projektbegleitung bei Energiekabelprojekten bis 110 kV.
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
{/* Social links could go here */}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-bold mb-6">
|
||||
{t("footer.navigation")}
|
||||
</h4>
|
||||
<nav className="flex flex-col gap-4">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="hover:text-accent transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
<Link
|
||||
href="/kontakt"
|
||||
className="hover:text-accent transition-colors"
|
||||
>
|
||||
{t("nav.contact")}
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-bold mb-6">
|
||||
{t("footer.legal")}
|
||||
</h4>
|
||||
<nav className="flex flex-col gap-4">
|
||||
<Link
|
||||
href="/impressum"
|
||||
className="hover:text-accent transition-colors"
|
||||
>
|
||||
{t("footer.impressum")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/datenschutz"
|
||||
className="hover:text-accent transition-colors"
|
||||
>
|
||||
{t("footer.datenschutz")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/agb"
|
||||
className="hover:text-accent transition-colors"
|
||||
>
|
||||
{t("footer.agb")}
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-bold mb-6">Navigation</h4>
|
||||
<nav className="flex flex-col gap-4">
|
||||
{navLinks.map((link) => (
|
||||
<Link key={link.href} href={link.href} className="hover:text-accent transition-colors">
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
<Link href="/kontakt" className="hover:text-accent transition-colors">Kontakt</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-bold mb-6">Rechtliches</h4>
|
||||
<nav className="flex flex-col gap-4">
|
||||
<Link href="/impressum" className="hover:text-accent transition-colors">Impressum</Link>
|
||||
<Link href="/datenschutz" className="hover:text-accent transition-colors">Datenschutz</Link>
|
||||
<Link href="/agb" className="hover:text-accent transition-colors">AGB</Link>
|
||||
</nav>
|
||||
<div className="pt-8 border-t border-slate-800 flex flex-col md:flex-row justify-between items-center gap-6 md:gap-4 text-sm text-slate-500 relative text-center md:text-left">
|
||||
<div className="absolute -top-px left-1/2 -translate-x-1/2 md:left-0 md:translate-x-0 w-12 h-px bg-accent/50" />
|
||||
<p>
|
||||
© {new Date().getFullYear()} MB Grid Solutions & Services
|
||||
GmbH. <br className="md:hidden" /> {t("footer.rights")}
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-accent opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-accent"></span>
|
||||
</span>
|
||||
{t("footer.madeWith")}{" "}
|
||||
<span className="text-accent">{t("footer.precision")}</span>{" "}
|
||||
{t("footer.inGermany")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-8 border-t border-slate-800 flex flex-col md:flex-row justify-between items-center gap-6 md:gap-4 text-sm text-slate-500 relative text-center md:text-left">
|
||||
<div className="absolute -top-px left-1/2 -translate-x-1/2 md:left-0 md:translate-x-0 w-12 h-px bg-accent/50" />
|
||||
<p>© {new Date().getFullYear()} MB Grid Solutions & Services GmbH. <br className="md:hidden" /> Alle Rechte vorbehalten.</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-accent opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-accent"></span>
|
||||
</span>
|
||||
Made with <span className="text-accent">precision</span> in Germany
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</Reveal>
|
||||
|
||||
<div className="bg-slate-950 py-6 border-t border-white/5">
|
||||
<div className="container-custom">
|
||||
<p className="text-[10px] uppercase tracking-[0.2em] text-slate-600 text-center md:text-left">
|
||||
Website developed by <a href="https://mintel.me" target="_blank" rel="noopener noreferrer" className="text-slate-500 hover:text-accent transition-colors duration-300">mintel.me</a>
|
||||
Website developed by{" "}
|
||||
<a
|
||||
href="https://mintel.me"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-slate-500 hover:text-accent transition-colors duration-300"
|
||||
>
|
||||
mintel.me
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user