migration wip
This commit is contained in:
163
components/layout/Footer.tsx
Normal file
163
components/layout/Footer.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import Link from 'next/link';
|
||||
import { Container } from '@/components/ui/Container';
|
||||
import { Navigation } from './Navigation';
|
||||
|
||||
interface FooterProps {
|
||||
locale: string;
|
||||
siteName?: string;
|
||||
}
|
||||
|
||||
export function Footer({ locale, siteName = 'KLZ Cables' }: FooterProps) {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
// Quick links
|
||||
const quickLinks = [
|
||||
{ title: 'About Us', path: `/${locale}/about` },
|
||||
{ title: 'Blog', path: `/${locale}/blog` },
|
||||
{ title: 'Products', path: `/${locale}/products` },
|
||||
{ title: 'Contact', path: `/${locale}/contact` }
|
||||
];
|
||||
|
||||
// Product categories
|
||||
const productCategories = [
|
||||
{ title: 'Medium Voltage Cables', path: `/${locale}/product-category/medium-voltage` },
|
||||
{ title: 'Low Voltage Cables', path: `/${locale}/product-category/low-voltage` },
|
||||
{ title: 'Cable Accessories', path: `/${locale}/product-category/accessories` },
|
||||
{ title: 'Special Solutions', path: `/${locale}/product-category/special` }
|
||||
];
|
||||
|
||||
// Legal links
|
||||
const legalLinks = [
|
||||
{ title: 'Privacy Policy', path: `/${locale}/privacy` },
|
||||
{ title: 'Terms of Service', path: `/${locale}/terms` },
|
||||
{ title: 'Imprint', path: `/${locale}/imprint` }
|
||||
];
|
||||
|
||||
return (
|
||||
<footer className="bg-gray-900 text-gray-300 border-t border-gray-800">
|
||||
<Container maxWidth="6xl" padding="lg">
|
||||
{/* Main Footer Content */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-8">
|
||||
{/* Company Info */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">KLZ</span>
|
||||
</div>
|
||||
<span className="font-bold text-white text-lg">{siteName}</span>
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed text-gray-400">
|
||||
Professional cable solutions for industrial applications.
|
||||
Quality, reliability, and innovation since 1990.
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
{/* Social Media Links */}
|
||||
<a href="#" className="text-gray-400 hover:text-white transition-colors" aria-label="LinkedIn">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" className="text-gray-400 hover:text-white transition-colors" aria-label="Twitter">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" className="text-gray-400 hover:text-white transition-colors" aria-label="Facebook">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v3.385z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div>
|
||||
<h3 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider">
|
||||
Quick Links
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{quickLinks.map((link) => (
|
||||
<li key={link.path}>
|
||||
<Link
|
||||
href={link.path}
|
||||
className="text-sm hover:text-white transition-colors"
|
||||
>
|
||||
{link.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Product Categories */}
|
||||
<div>
|
||||
<h3 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider">
|
||||
Products
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{productCategories.map((link) => (
|
||||
<li key={link.path}>
|
||||
<Link
|
||||
href={link.path}
|
||||
className="text-sm hover:text-white transition-colors"
|
||||
>
|
||||
{link.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Contact Info */}
|
||||
<div>
|
||||
<h3 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider">
|
||||
Contact
|
||||
</h3>
|
||||
<ul className="space-y-3 text-sm">
|
||||
<li className="flex items-start gap-2">
|
||||
<svg className="w-4 h-4 mt-0.5 text-primary flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span>info@klz-cables.com</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<svg className="w-4 h-4 mt-0.5 text-primary flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
<span>+49 (0) 123 456 789</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<svg className="w-4 h-4 mt-0.5 text-primary flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span>
|
||||
Industrial Street 123<br />
|
||||
12345 Berlin, Germany
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<div className="border-t border-gray-800 pt-6 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<div className="text-sm text-gray-400">
|
||||
© {currentYear} {siteName}. All rights reserved.
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
{legalLinks.map((link) => (
|
||||
<Link
|
||||
key={link.path}
|
||||
href={link.path}
|
||||
className="hover:text-white transition-colors"
|
||||
>
|
||||
{link.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
60
components/layout/Header.tsx
Normal file
60
components/layout/Header.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import Link from 'next/link';
|
||||
import { Container } from '@/components/ui/Container';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Navigation } from './Navigation';
|
||||
import { LocaleSwitcher } from '@/components/LocaleSwitcher';
|
||||
import { MobileMenu } from './MobileMenu';
|
||||
|
||||
interface HeaderProps {
|
||||
locale: string;
|
||||
siteName?: string;
|
||||
logo?: string;
|
||||
}
|
||||
|
||||
export function Header({ locale, siteName = 'KLZ Cables', logo }: HeaderProps) {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 bg-white border-b border-gray-200 shadow-sm">
|
||||
<Container maxWidth="6xl" padding="md">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
{/* Logo and Branding */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href={`/${locale}`}
|
||||
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
|
||||
>
|
||||
{logo ? (
|
||||
<img src={logo} alt={siteName} className="h-8 w-auto" />
|
||||
) : (
|
||||
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">KLZ</span>
|
||||
</div>
|
||||
)}
|
||||
<span className="hidden sm:block font-bold text-lg text-gray-900">
|
||||
{siteName}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex items-center gap-6">
|
||||
<Navigation locale={locale} variant="header" />
|
||||
<LocaleSwitcher />
|
||||
<Link href={`/${locale}/contact`}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
>
|
||||
Contact Us
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
<div className="flex items-center gap-2">
|
||||
<MobileMenu locale={locale} siteName={siteName} />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
78
components/layout/Layout.tsx
Normal file
78
components/layout/Layout.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ReactNode } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Header } from './Header';
|
||||
import { Footer } from './Footer';
|
||||
import { Container } from '@/components/ui/Container';
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode;
|
||||
locale: string;
|
||||
siteName?: string;
|
||||
logo?: string;
|
||||
showSidebar?: boolean;
|
||||
breadcrumb?: Array<{ title: string; path: string }>;
|
||||
}
|
||||
|
||||
export function Layout({
|
||||
children,
|
||||
locale,
|
||||
siteName = 'KLZ Cables',
|
||||
logo,
|
||||
showSidebar = false,
|
||||
breadcrumb
|
||||
}: LayoutProps) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* Header */}
|
||||
<Header
|
||||
locale={locale}
|
||||
siteName={siteName}
|
||||
logo={logo}
|
||||
/>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="flex-1">
|
||||
{/* Breadcrumb */}
|
||||
{breadcrumb && breadcrumb.length > 0 && (
|
||||
<div className="bg-gray-50 border-b border-gray-200">
|
||||
<Container maxWidth="6xl" padding="md">
|
||||
<nav className="flex items-center gap-2 text-sm text-gray-600 py-3" aria-label="Breadcrumb">
|
||||
<Link
|
||||
href={`/${locale}`}
|
||||
className="hover:text-primary transition-colors"
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
{breadcrumb.map((item, index) => (
|
||||
<div key={item.path} className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
{index === breadcrumb.length - 1 ? (
|
||||
<span className="text-gray-900 font-medium">{item.title}</span>
|
||||
) : (
|
||||
<Link
|
||||
href={item.path}
|
||||
className="hover:text-primary transition-colors"
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</Container>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<Container maxWidth="6xl" padding="md" className="py-8 md:py-12">
|
||||
{children}
|
||||
</Container>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<Footer locale={locale} siteName={siteName} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
213
components/layout/MobileMenu.tsx
Normal file
213
components/layout/MobileMenu.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { LocaleSwitcher } from '@/components/LocaleSwitcher';
|
||||
import { getLocaleFromPath } from '@/lib/i18n';
|
||||
|
||||
interface MobileMenuProps {
|
||||
locale: string;
|
||||
siteName: string;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function MobileMenu({ locale, siteName, onClose }: MobileMenuProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const currentLocale = getLocaleFromPath(pathname);
|
||||
|
||||
// Main navigation menu
|
||||
const mainMenu = [
|
||||
{ title: 'Home', path: `/${locale}` },
|
||||
{ title: 'Blog', path: `/${locale}/blog` },
|
||||
{ title: 'Products', path: `/${locale}/products` },
|
||||
{ title: 'Contact', path: `/${locale}/contact` }
|
||||
];
|
||||
|
||||
// Product categories (could be dynamic from data)
|
||||
const productCategories = [
|
||||
{ title: 'Medium Voltage', path: `/${locale}/product-category/medium-voltage` },
|
||||
{ title: 'Low Voltage', path: `/${locale}/product-category/low-voltage` },
|
||||
{ title: 'Accessories', path: `/${locale}/product-category/accessories` }
|
||||
];
|
||||
|
||||
// Close on route change
|
||||
useEffect(() => {
|
||||
setIsOpen(false);
|
||||
if (onClose) onClose();
|
||||
}, [pathname, onClose]);
|
||||
|
||||
const toggleMenu = () => {
|
||||
setIsOpen(!isOpen);
|
||||
if (!isOpen && onClose) onClose();
|
||||
};
|
||||
|
||||
const closeMenu = () => {
|
||||
setIsOpen(false);
|
||||
if (onClose) onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile Toggle Button */}
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
className="md:hidden p-3 rounded-lg hover:bg-gray-100 active:bg-gray-200 transition-colors touch-target-sm"
|
||||
aria-label="Toggle mobile menu"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6 text-gray-700"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
{isOpen ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Mobile Menu Overlay */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 z-50 md:hidden"
|
||||
onClick={closeMenu}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Mobile Menu Drawer */}
|
||||
<div
|
||||
className={`fixed top-0 right-0 h-full w-full max-w-md bg-white shadow-2xl z-50 transform transition-transform duration-300 ease-in-out md:hidden safe-area-p ${
|
||||
isOpen ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Mobile navigation menu"
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 safe-area-p">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center shadow-sm">
|
||||
<span className="text-white font-bold text-sm">KLZ</span>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 text-lg">{siteName}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeMenu}
|
||||
className="p-3 rounded-lg hover:bg-gray-100 active:bg-gray-200 transition-colors touch-target-sm"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{/* Main Navigation */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">
|
||||
Navigation
|
||||
</h3>
|
||||
<nav className="space-y-1">
|
||||
{mainMenu.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
href={item.path}
|
||||
className="flex items-center justify-between px-4 py-3 rounded-lg text-gray-700 hover:bg-gray-100 hover:text-primary active:bg-gray-200 transition-colors touch-target-md"
|
||||
onClick={closeMenu}
|
||||
>
|
||||
<span className="font-medium text-base">{item.title}</span>
|
||||
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Product Categories */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">
|
||||
Product Categories
|
||||
</h3>
|
||||
<nav className="space-y-1">
|
||||
{productCategories.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
href={item.path}
|
||||
className="flex items-center justify-between px-4 py-3 rounded-lg text-gray-700 hover:bg-gray-100 hover:text-primary active:bg-gray-200 transition-colors touch-target-md"
|
||||
onClick={closeMenu}
|
||||
>
|
||||
<span className="font-medium text-base">{item.title}</span>
|
||||
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Language Switcher */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">
|
||||
Language
|
||||
</h3>
|
||||
<div className="px-3">
|
||||
<LocaleSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Information */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">
|
||||
Contact
|
||||
</h3>
|
||||
<div className="space-y-2 px-4 text-sm text-gray-600">
|
||||
<a
|
||||
href="mailto:info@klz-cables.com"
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors touch-target-sm"
|
||||
>
|
||||
<svg className="w-5 h-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="font-medium">info@klz-cables.com</span>
|
||||
</a>
|
||||
<a
|
||||
href="tel:+490123456789"
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors touch-target-sm"
|
||||
>
|
||||
<svg className="w-5 h-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
<span className="font-medium">+49 (0) 123 456 789</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer CTA */}
|
||||
<div className="p-4 border-t border-gray-200 bg-gray-50">
|
||||
<Link href={`/${locale}/contact`} onClick={closeMenu} className="block w-full">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="md"
|
||||
fullWidth
|
||||
>
|
||||
Get in Touch
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
59
components/layout/Navigation.tsx
Normal file
59
components/layout/Navigation.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { getLocaleFromPath } from '@/lib/i18n';
|
||||
|
||||
interface NavigationProps {
|
||||
locale: string;
|
||||
variant?: 'header' | 'footer';
|
||||
}
|
||||
|
||||
export function Navigation({ locale, variant = 'header' }: NavigationProps) {
|
||||
const pathname = usePathname();
|
||||
const currentLocale = getLocaleFromPath(pathname);
|
||||
|
||||
// Main navigation menu
|
||||
const mainMenu = [
|
||||
{ title: 'Home', path: `/${locale}` },
|
||||
{ title: 'Blog', path: `/${locale}/blog` },
|
||||
{ title: 'Products', path: `/${locale}/products` },
|
||||
{ title: 'Contact', path: `/${locale}/contact` }
|
||||
];
|
||||
|
||||
// Determine styles based on variant
|
||||
const isHeader = variant === 'header';
|
||||
const baseClasses = isHeader
|
||||
? 'hidden md:flex items-center gap-1'
|
||||
: 'flex flex-col gap-2';
|
||||
|
||||
const linkClasses = isHeader
|
||||
? 'px-3 py-2 text-sm font-medium text-gray-700 hover:text-primary hover:bg-primary-light rounded-lg transition-colors relative'
|
||||
: 'text-sm text-gray-600 hover:text-primary transition-colors';
|
||||
|
||||
const activeClasses = isHeader
|
||||
? 'text-primary bg-primary-light font-semibold'
|
||||
: 'text-primary font-medium';
|
||||
|
||||
return (
|
||||
<nav className={baseClasses}>
|
||||
{mainMenu.map((item) => {
|
||||
const isActive = pathname === item.path ||
|
||||
(item.path !== `/${locale}` && pathname.startsWith(item.path));
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.path}
|
||||
href={item.path}
|
||||
className={`${linkClasses} ${isActive ? activeClasses : ''}`}
|
||||
>
|
||||
{item.title}
|
||||
{isActive && isHeader && (
|
||||
<span className="absolute bottom-0 left-3 right-3 h-0.5 bg-primary rounded-full" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
337
components/layout/ResponsiveWrapper.tsx
Normal file
337
components/layout/ResponsiveWrapper.tsx
Normal file
@@ -0,0 +1,337 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getViewport } from '@/lib/responsive';
|
||||
|
||||
interface ResponsiveWrapperProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
// Visibility control
|
||||
showOn?: ('mobile' | 'tablet' | 'desktop' | 'largeDesktop')[];
|
||||
hideOn?: ('mobile' | 'tablet' | 'desktop' | 'largeDesktop')[];
|
||||
// Mobile-specific behavior
|
||||
stackOnMobile?: boolean;
|
||||
centerOnMobile?: boolean;
|
||||
// Padding control
|
||||
padding?: 'none' | 'sm' | 'md' | 'lg' | 'responsive';
|
||||
// Container control
|
||||
container?: boolean;
|
||||
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full';
|
||||
}
|
||||
|
||||
/**
|
||||
* ResponsiveWrapper Component
|
||||
* Provides comprehensive responsive behavior for any content
|
||||
*/
|
||||
export function ResponsiveWrapper({
|
||||
children,
|
||||
className = '',
|
||||
showOn,
|
||||
hideOn,
|
||||
stackOnMobile = false,
|
||||
centerOnMobile = false,
|
||||
padding = 'md',
|
||||
container = false,
|
||||
maxWidth = 'xl',
|
||||
}: ResponsiveWrapperProps) {
|
||||
// Get visibility classes
|
||||
const getVisibilityClasses = () => {
|
||||
let classes = '';
|
||||
|
||||
if (showOn) {
|
||||
// Hide by default, show only on specified breakpoints
|
||||
classes += 'hidden ';
|
||||
if (showOn.includes('mobile')) classes += 'xs:block ';
|
||||
if (showOn.includes('tablet')) classes += 'md:block ';
|
||||
if (showOn.includes('desktop')) classes += 'lg:block ';
|
||||
if (showOn.includes('largeDesktop')) classes += 'xl:block ';
|
||||
}
|
||||
|
||||
if (hideOn) {
|
||||
// Show by default, hide on specified breakpoints
|
||||
if (hideOn.includes('mobile')) classes += 'xs:hidden ';
|
||||
if (hideOn.includes('tablet')) classes += 'md:hidden ';
|
||||
if (hideOn.includes('desktop')) classes += 'lg:hidden ';
|
||||
if (hideOn.includes('largeDesktop')) classes += 'xl:hidden ';
|
||||
}
|
||||
|
||||
return classes;
|
||||
};
|
||||
|
||||
// Get mobile-specific classes
|
||||
const getMobileClasses = () => {
|
||||
let classes = '';
|
||||
|
||||
if (stackOnMobile) {
|
||||
classes += 'flex-col xs:flex-row ';
|
||||
}
|
||||
|
||||
if (centerOnMobile) {
|
||||
classes += 'items-center xs:items-start text-center xs:text-left ';
|
||||
}
|
||||
|
||||
return classes;
|
||||
};
|
||||
|
||||
// Get padding classes
|
||||
const getPaddingClasses = () => {
|
||||
switch (padding) {
|
||||
case 'none':
|
||||
return '';
|
||||
case 'sm':
|
||||
return 'px-3 py-2 xs:px-4 xs:py-3';
|
||||
case 'md':
|
||||
return 'px-4 py-3 xs:px-6 xs:py-4';
|
||||
case 'lg':
|
||||
return 'px-5 py-4 xs:px-8 xs:py-6';
|
||||
case 'responsive':
|
||||
return 'px-4 py-3 xs:px-6 xs:py-4 md:px-8 md:py-6 lg:px-10 lg:py-8';
|
||||
default:
|
||||
return 'px-4 py-3';
|
||||
}
|
||||
};
|
||||
|
||||
// Get container classes if needed
|
||||
const getContainerClasses = () => {
|
||||
if (!container) return '';
|
||||
|
||||
const maxWidthClasses = {
|
||||
sm: 'max-w-sm',
|
||||
md: 'max-w-md',
|
||||
lg: 'max-w-lg',
|
||||
xl: 'max-w-xl',
|
||||
'2xl': 'max-w-2xl',
|
||||
'3xl': 'max-w-3xl',
|
||||
full: 'max-w-full',
|
||||
};
|
||||
|
||||
return `mx-auto ${maxWidthClasses[maxWidth]} w-full`;
|
||||
};
|
||||
|
||||
const wrapperClasses = cn(
|
||||
// Base classes
|
||||
'responsive-wrapper',
|
||||
// Visibility
|
||||
getVisibilityClasses(),
|
||||
// Mobile behavior
|
||||
getMobileClasses(),
|
||||
// Padding
|
||||
getPaddingClasses(),
|
||||
// Container
|
||||
getContainerClasses(),
|
||||
// Custom classes
|
||||
className
|
||||
);
|
||||
|
||||
return <div className={wrapperClasses}>{children}</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* ResponsiveGrid Wrapper
|
||||
* Creates responsive grid layouts with mobile-first approach
|
||||
*/
|
||||
interface ResponsiveGridProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
// Column configuration
|
||||
columns?: {
|
||||
mobile?: number;
|
||||
tablet?: number;
|
||||
desktop?: number;
|
||||
largeDesktop?: number;
|
||||
};
|
||||
gap?: 'none' | 'sm' | 'md' | 'lg' | 'responsive';
|
||||
// Mobile stacking
|
||||
stackMobile?: boolean;
|
||||
// Alignment
|
||||
alignItems?: 'start' | 'center' | 'end' | 'stretch';
|
||||
justifyItems?: 'start' | 'center' | 'end' | 'stretch';
|
||||
}
|
||||
|
||||
export function ResponsiveGrid({
|
||||
children,
|
||||
className = '',
|
||||
columns = {},
|
||||
gap = 'md',
|
||||
stackMobile = false,
|
||||
alignItems = 'start',
|
||||
justifyItems = 'start',
|
||||
}: ResponsiveGridProps) {
|
||||
const getGridColumns = () => {
|
||||
if (stackMobile) {
|
||||
return `grid-cols-1 sm:grid-cols-2 md:grid-cols-${columns.tablet || 3} lg:grid-cols-${columns.desktop || 4}`;
|
||||
}
|
||||
|
||||
const mobile = columns.mobile || 1;
|
||||
const tablet = columns.tablet || 2;
|
||||
const desktop = columns.desktop || 3;
|
||||
const largeDesktop = columns.largeDesktop || 4;
|
||||
|
||||
return `grid-cols-${mobile} sm:grid-cols-${tablet} md:grid-cols-${desktop} lg:grid-cols-${largeDesktop}`;
|
||||
};
|
||||
|
||||
const getGapClasses = () => {
|
||||
switch (gap) {
|
||||
case 'none':
|
||||
return 'gap-0';
|
||||
case 'sm':
|
||||
return 'gap-2 sm:gap-3 md:gap-4';
|
||||
case 'md':
|
||||
return 'gap-3 sm:gap-4 md:gap-6 lg:gap-8';
|
||||
case 'lg':
|
||||
return 'gap-4 sm:gap-6 md:gap-8 lg:gap-12';
|
||||
case 'responsive':
|
||||
return 'gap-2 xs:gap-3 sm:gap-4 md:gap-6 lg:gap-8 xl:gap-12';
|
||||
default:
|
||||
return 'gap-4';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'grid',
|
||||
'w-full',
|
||||
getGridColumns(),
|
||||
getGapClasses(),
|
||||
alignItems && `items-${alignItems}`,
|
||||
justifyItems && `justify-items-${justifyItems}`,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ResponsiveStack Wrapper
|
||||
* Creates vertical stack that becomes horizontal on larger screens
|
||||
*/
|
||||
interface ResponsiveStackProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
gap?: 'none' | 'sm' | 'md' | 'lg' | 'responsive';
|
||||
reverseOnMobile?: boolean;
|
||||
wrap?: boolean;
|
||||
}
|
||||
|
||||
export function ResponsiveStack({
|
||||
children,
|
||||
className = '',
|
||||
gap = 'md',
|
||||
reverseOnMobile = false,
|
||||
wrap = false,
|
||||
}: ResponsiveStackProps) {
|
||||
const getGapClasses = () => {
|
||||
switch (gap) {
|
||||
case 'none':
|
||||
return 'gap-0';
|
||||
case 'sm':
|
||||
return 'gap-2 sm:gap-3 md:gap-4';
|
||||
case 'md':
|
||||
return 'gap-3 sm:gap-4 md:gap-6 lg:gap-8';
|
||||
case 'lg':
|
||||
return 'gap-4 sm:gap-6 md:gap-8 lg:gap-12';
|
||||
case 'responsive':
|
||||
return 'gap-2 xs:gap-3 sm:gap-4 md:gap-6 lg:gap-8';
|
||||
default:
|
||||
return 'gap-4';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex',
|
||||
// Mobile-first: column, then row
|
||||
'flex-col',
|
||||
'xs:flex-row',
|
||||
// Gap
|
||||
getGapClasses(),
|
||||
// Wrap
|
||||
wrap ? 'flex-wrap xs:flex-nowrap' : 'flex-nowrap',
|
||||
// Reverse on mobile
|
||||
reverseOnMobile && 'flex-col-reverse xs:flex-row',
|
||||
// Ensure proper spacing
|
||||
'w-full',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ResponsiveSection Wrapper
|
||||
* Optimized section with responsive padding and max-width
|
||||
*/
|
||||
interface ResponsiveSectionProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'responsive';
|
||||
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | 'full';
|
||||
centered?: boolean;
|
||||
safeArea?: boolean;
|
||||
}
|
||||
|
||||
export function ResponsiveSection({
|
||||
children,
|
||||
className = '',
|
||||
padding = 'responsive',
|
||||
maxWidth = '6xl',
|
||||
centered = true,
|
||||
safeArea = false,
|
||||
}: ResponsiveSectionProps) {
|
||||
const getPaddingClasses = () => {
|
||||
switch (padding) {
|
||||
case 'none':
|
||||
return '';
|
||||
case 'sm':
|
||||
return 'px-3 py-4 xs:px-4 xs:py-6 md:px-6 md:py-8';
|
||||
case 'md':
|
||||
return 'px-4 py-6 xs:px-6 xs:py-8 md:px-8 md:py-12';
|
||||
case 'lg':
|
||||
return 'px-5 py-8 xs:px-8 xs:py-12 md:px-12 md:py-16';
|
||||
case 'xl':
|
||||
return 'px-6 py-10 xs:px-10 xs:py-14 md:px-16 md:py-20';
|
||||
case 'responsive':
|
||||
return 'px-4 py-6 xs:px-6 xs:py-8 md:px-8 md:py-12 lg:px-12 lg:py-16 xl:px-16 xl:py-20';
|
||||
default:
|
||||
return 'px-4 py-6';
|
||||
}
|
||||
};
|
||||
|
||||
const getMaxWidthClasses = () => {
|
||||
const maxWidthMap = {
|
||||
sm: 'max-w-sm',
|
||||
md: 'max-w-md',
|
||||
lg: 'max-w-lg',
|
||||
xl: 'max-w-xl',
|
||||
'2xl': 'max-w-2xl',
|
||||
'3xl': 'max-w-3xl',
|
||||
'4xl': 'max-w-4xl',
|
||||
'5xl': 'max-w-5xl',
|
||||
'6xl': 'max-w-6xl',
|
||||
full: 'max-w-full',
|
||||
};
|
||||
return maxWidthMap[maxWidth];
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'w-full',
|
||||
centered && 'mx-auto',
|
||||
getMaxWidthClasses(),
|
||||
getPaddingClasses(),
|
||||
safeArea && 'safe-area-p',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResponsiveWrapper;
|
||||
6
components/layout/index.ts
Normal file
6
components/layout/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// Layout Components
|
||||
export { Header } from './Header';
|
||||
export { Footer } from './Footer';
|
||||
export { Layout } from './Layout';
|
||||
export { MobileMenu } from './MobileMenu';
|
||||
export { Navigation } from './Navigation';
|
||||
Reference in New Issue
Block a user