Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 23bf327670 | |||
| c77f99ef37 | |||
| bffcc98820 | |||
| 7519e17280 | |||
| 5bd7421764 | |||
| d7aba218d9 | |||
| e20d7f42c0 | |||
| 16d06d3275 |
@@ -202,7 +202,7 @@ jobs:
|
|||||||
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
|
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
|
||||||
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
||||||
DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }}
|
DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }}
|
||||||
UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
NEXT_PUBLIC_UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||||
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||||
NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
|
NPM_TOKEN=${{ secrets.REGISTRY_PASS }}
|
||||||
tags: registry.infra.mintel.me/mintel/klz-cables.com:${{ needs.prepare.outputs.image_tag }}
|
tags: registry.infra.mintel.me/mintel/klz-cables.com:${{ needs.prepare.outputs.image_tag }}
|
||||||
@@ -254,7 +254,7 @@ jobs:
|
|||||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
||||||
|
|
||||||
# Analytics
|
# Analytics
|
||||||
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||||
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -321,7 +321,7 @@ jobs:
|
|||||||
echo "COOKIE_DOMAIN=$COOKIE_DOMAIN"
|
echo "COOKIE_DOMAIN=$COOKIE_DOMAIN"
|
||||||
echo ""
|
echo ""
|
||||||
echo "# Analytics"
|
echo "# Analytics"
|
||||||
echo "UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID"
|
echo "NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID"
|
||||||
echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT"
|
echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT"
|
||||||
echo ""
|
echo ""
|
||||||
echo "TARGET=$TARGET"
|
echo "TARGET=$TARGET"
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default async function Layout(props: {
|
|||||||
|
|
||||||
setRequestLocale(safeLocale);
|
setRequestLocale(safeLocale);
|
||||||
|
|
||||||
let messages = {};
|
let messages: Record<string, any> = {};
|
||||||
try {
|
try {
|
||||||
messages = await getMessages();
|
messages = await getMessages();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -80,6 +80,15 @@ export default async function Layout(props: {
|
|||||||
messages = {};
|
messages = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pick only the namespaces required by client components to reduce the hydration payload size
|
||||||
|
const clientKeys = ['Footer', 'Navigation', 'Contact', 'Products', 'Team', 'Home'];
|
||||||
|
const clientMessages: Record<string, any> = {};
|
||||||
|
for (const key of clientKeys) {
|
||||||
|
if (messages[key]) {
|
||||||
|
clientMessages[key] = messages[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { getServerAppServices } = await import('@/lib/services/create-services.server');
|
const { getServerAppServices } = await import('@/lib/services/create-services.server');
|
||||||
const serverServices = getServerAppServices();
|
const serverServices = getServerAppServices();
|
||||||
|
|
||||||
@@ -118,7 +127,7 @@ export default async function Layout(props: {
|
|||||||
<link rel="preconnect" href="https://img.infra.mintel.me" />
|
<link rel="preconnect" href="https://img.infra.mintel.me" />
|
||||||
</head>
|
</head>
|
||||||
<body className="flex flex-col min-h-screen font-sans selection:bg-accent selection:text-primary-dark antialiased overflow-x-hidden">
|
<body className="flex flex-col min-h-screen font-sans selection:bg-accent selection:text-primary-dark antialiased overflow-x-hidden">
|
||||||
<NextIntlClientProvider messages={messages} locale={safeLocale}>
|
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
|
||||||
<RecordModeProvider isEnabled={recordModeEnabled}>
|
<RecordModeProvider isEnabled={recordModeEnabled}>
|
||||||
<RecordModeVisuals>
|
<RecordModeVisuals>
|
||||||
<SkipLink />
|
<SkipLink />
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import Hero from '@/components/home/Hero';
|
import Hero from '@/components/home/Hero';
|
||||||
import JsonLd from '@/components/JsonLd';
|
import JsonLd from '@/components/JsonLd';
|
||||||
import { getBreadcrumbSchema, SITE_URL } from '@/lib/schema';
|
import { getBreadcrumbSchema, SITE_URL } from '@/lib/schema';
|
||||||
import ProductCategories from '@/components/home/ProductCategories';
|
|
||||||
import WhatWeDo from '@/components/home/WhatWeDo';
|
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import Reveal from '@/components/Reveal';
|
import Reveal from '@/components/Reveal';
|
||||||
|
|
||||||
|
const ProductCategories = dynamic(() => import('@/components/home/ProductCategories'));
|
||||||
|
const WhatWeDo = dynamic(() => import('@/components/home/WhatWeDo'));
|
||||||
|
|
||||||
const RecentPosts = dynamic(() => import('@/components/home/RecentPosts'));
|
const RecentPosts = dynamic(() => import('@/components/home/RecentPosts'));
|
||||||
const Experience = dynamic(() => import('@/components/home/Experience'));
|
const Experience = dynamic(() => import('@/components/home/Experience'));
|
||||||
const WhyChooseUs = dynamic(() => import('@/components/home/WhyChooseUs'));
|
const WhyChooseUs = dynamic(() => import('@/components/home/WhyChooseUs'));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { motion } from 'framer-motion';
|
import { m, LazyMotion, domAnimation } from 'framer-motion';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { Button } from './ui';
|
import { Button } from './ui';
|
||||||
@@ -114,14 +114,15 @@ export default function Header() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<motion.header
|
<LazyMotion strict features={domAnimation}>
|
||||||
|
<m.header
|
||||||
className={headerClass}
|
className={headerClass}
|
||||||
initial={{ y: -100, opacity: 0 }}
|
initial={{ y: -100, opacity: 0 }}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
transition={{ duration: 0.8, ease: 'easeOut' }}
|
transition={{ duration: 0.8, ease: 'easeOut' }}
|
||||||
>
|
>
|
||||||
<div className="container mx-auto px-4 md:px-12 lg:px-16 max-w-7xl flex items-center justify-between">
|
<div className="container mx-auto px-4 md:px-12 lg:px-16 max-w-7xl flex items-center justify-between">
|
||||||
<motion.div
|
<m.div
|
||||||
className="flex-shrink-0 group touch-target"
|
className="flex-shrink-0 group touch-target"
|
||||||
initial={{ scale: 0.8, opacity: 0 }}
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
@@ -145,9 +146,9 @@ export default function Header() {
|
|||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</m.div>
|
||||||
|
|
||||||
<motion.div
|
<m.div
|
||||||
className="flex items-center gap-4 md:gap-12"
|
className="flex items-center gap-4 md:gap-12"
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
@@ -160,9 +161,9 @@ export default function Header() {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<motion.nav className="hidden lg:flex items-center space-x-10" variants={navVariants}>
|
<m.nav className="hidden lg:flex items-center space-x-10" variants={navVariants}>
|
||||||
{menuItems.map((item, _idx) => (
|
{menuItems.map((item, _idx) => (
|
||||||
<motion.div key={item.href} variants={navLinkVariants}>
|
<m.div key={item.href} variants={navLinkVariants}>
|
||||||
<Link
|
<Link
|
||||||
href={`/${currentLocale}${item.href === '/' ? '' : item.href}`}
|
href={`/${currentLocale}${item.href === '/' ? '' : item.href}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -181,21 +182,21 @@ export default function Header() {
|
|||||||
{item.label}
|
{item.label}
|
||||||
<span className="absolute -bottom-2 left-0 w-0 h-1 bg-accent transition-all duration-500 group-hover:w-full rounded-full shadow-[0_0_12px_rgba(130,237,32,0.6)]" />
|
<span className="absolute -bottom-2 left-0 w-0 h-1 bg-accent transition-all duration-500 group-hover:w-full rounded-full shadow-[0_0_12px_rgba(130,237,32,0.6)]" />
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</m.div>
|
||||||
))}
|
))}
|
||||||
</motion.nav>
|
</m.nav>
|
||||||
|
|
||||||
<motion.div
|
<m.div
|
||||||
className={cn('hidden lg:flex items-center space-x-8', textColorClass)}
|
className={cn('hidden lg:flex items-center space-x-8', textColorClass)}
|
||||||
variants={headerRightVariants}
|
variants={headerRightVariants}
|
||||||
>
|
>
|
||||||
<motion.div
|
<m.div
|
||||||
className="flex items-center space-x-4 text-xs md:text-sm font-extrabold tracking-widest uppercase"
|
className="flex items-center space-x-4 text-xs md:text-sm font-extrabold tracking-widest uppercase"
|
||||||
initial={{ opacity: 0, x: 20 }}
|
initial={{ opacity: 0, x: 20 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ duration: 0.5, delay: 0.6 }}
|
transition={{ duration: 0.5, delay: 0.6 }}
|
||||||
>
|
>
|
||||||
<motion.div
|
<m.div
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 0.4, delay: 0.65 }}
|
transition={{ duration: 0.4, delay: 0.65 }}
|
||||||
@@ -214,14 +215,14 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
EN
|
EN
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</m.div>
|
||||||
<motion.div
|
<m.div
|
||||||
className="w-px h-4 bg-current opacity-20"
|
className="w-px h-4 bg-current opacity-20"
|
||||||
initial={{ scaleY: 0 }}
|
initial={{ scaleY: 0 }}
|
||||||
animate={{ scaleY: 1 }}
|
animate={{ scaleY: 1 }}
|
||||||
transition={{ duration: 0.4, delay: 0.7 }}
|
transition={{ duration: 0.4, delay: 0.7 }}
|
||||||
/>
|
/>
|
||||||
<motion.div
|
<m.div
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 0.4, delay: 0.75 }}
|
transition={{ duration: 0.4, delay: 0.75 }}
|
||||||
@@ -240,10 +241,10 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
DE
|
DE
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</m.div>
|
||||||
</motion.div>
|
</m.div>
|
||||||
|
|
||||||
<motion.div
|
<m.div
|
||||||
initial={{ scale: 0.9, opacity: 0 }}
|
initial={{ scale: 0.9, opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
transition={{ duration: 0.6, type: 'spring', stiffness: 400, delay: 0.7 }}
|
transition={{ duration: 0.6, type: 'spring', stiffness: 400, delay: 0.7 }}
|
||||||
@@ -262,11 +263,11 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
{t('contact')}
|
{t('contact')}
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</m.div>
|
||||||
</motion.div>
|
</m.div>
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
{/* Mobile Menu Button */}
|
||||||
<motion.button
|
<m.button
|
||||||
className={cn(
|
className={cn(
|
||||||
'lg:hidden touch-target p-2 rounded-xl bg-white/10 border border-white/20 z-50',
|
'lg:hidden touch-target p-2 rounded-xl bg-white/10 border border-white/20 z-50',
|
||||||
textColorClass,
|
textColorClass,
|
||||||
@@ -292,7 +293,7 @@ export default function Header() {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<motion.svg
|
<m.svg
|
||||||
className="w-7 h-7"
|
className="w-7 h-7"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -302,7 +303,7 @@ export default function Header() {
|
|||||||
transition={{ duration: 0.3, delay: 0.6 }}
|
transition={{ duration: 0.3, delay: 0.6 }}
|
||||||
>
|
>
|
||||||
{isMobileMenuOpen ? (
|
{isMobileMenuOpen ? (
|
||||||
<motion.path
|
<m.path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
@@ -312,7 +313,7 @@ export default function Header() {
|
|||||||
transition={{ duration: 0.4, delay: 0.7 }}
|
transition={{ duration: 0.4, delay: 0.7 }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<motion.path
|
<m.path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
@@ -322,9 +323,9 @@ export default function Header() {
|
|||||||
transition={{ duration: 0.4, delay: 0.7 }}
|
transition={{ duration: 0.4, delay: 0.7 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</motion.svg>
|
</m.svg>
|
||||||
</motion.button>
|
</m.button>
|
||||||
</motion.div>
|
</m.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Menu Overlay */}
|
{/* Mobile Menu Overlay */}
|
||||||
@@ -341,7 +342,7 @@ export default function Header() {
|
|||||||
aria-label={t('menu')}
|
aria-label={t('menu')}
|
||||||
ref={mobileMenuRef}
|
ref={mobileMenuRef}
|
||||||
>
|
>
|
||||||
<motion.nav
|
<m.nav
|
||||||
className="flex-grow flex flex-col justify-center items-center p-8 space-y-8"
|
className="flex-grow flex flex-col justify-center items-center p-8 space-y-8"
|
||||||
initial="closed"
|
initial="closed"
|
||||||
animate={isMobileMenuOpen ? 'open' : 'closed'}
|
animate={isMobileMenuOpen ? 'open' : 'closed'}
|
||||||
@@ -355,7 +356,7 @@ export default function Header() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{menuItems.map((item, idx) => (
|
{menuItems.map((item, idx) => (
|
||||||
<motion.div
|
<m.div
|
||||||
key={item.href}
|
key={item.href}
|
||||||
variants={{
|
variants={{
|
||||||
closed: { opacity: 0, y: 50, scale: 0.9 },
|
closed: { opacity: 0, y: 50, scale: 0.9 },
|
||||||
@@ -385,22 +386,22 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</m.div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<motion.div
|
<m.div
|
||||||
className="pt-8 border-t border-white/10 w-full flex flex-col items-center space-y-8"
|
className="pt-8 border-t border-white/10 w-full flex flex-col items-center space-y-8"
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
animate={isMobileMenuOpen ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
|
animate={isMobileMenuOpen ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
|
||||||
transition={{ duration: 0.5, delay: 0.8 }}
|
transition={{ duration: 0.5, delay: 0.8 }}
|
||||||
>
|
>
|
||||||
<motion.div
|
<m.div
|
||||||
className="flex items-center space-x-8 text-lg md:text-xl font-extrabold tracking-widest uppercase text-white"
|
className="flex items-center space-x-8 text-lg md:text-xl font-extrabold tracking-widest uppercase text-white"
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 0.4, delay: 0.9 }}
|
transition={{ duration: 0.4, delay: 0.9 }}
|
||||||
>
|
>
|
||||||
<motion.div
|
<m.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ duration: 0.3, delay: 1.0 }}
|
transition={{ duration: 0.3, delay: 1.0 }}
|
||||||
@@ -411,14 +412,14 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
EN
|
EN
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</m.div>
|
||||||
<motion.div
|
<m.div
|
||||||
className="w-px h-6 bg-white/20"
|
className="w-px h-6 bg-white/20"
|
||||||
initial={{ scaleX: 0 }}
|
initial={{ scaleX: 0 }}
|
||||||
animate={{ scaleX: 1 }}
|
animate={{ scaleX: 1 }}
|
||||||
transition={{ duration: 0.4, delay: 1.05 }}
|
transition={{ duration: 0.4, delay: 1.05 }}
|
||||||
/>
|
/>
|
||||||
<motion.div
|
<m.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ duration: 0.3, delay: 1.1 }}
|
transition={{ duration: 0.3, delay: 1.1 }}
|
||||||
@@ -429,10 +430,10 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
DE
|
DE
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</m.div>
|
||||||
</motion.div>
|
</m.div>
|
||||||
|
|
||||||
<motion.div
|
<m.div
|
||||||
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||||
transition={{ type: 'spring', stiffness: 400, damping: 20, delay: 1.2 }}
|
transition={{ type: 'spring', stiffness: 400, damping: 20, delay: 1.2 }}
|
||||||
@@ -445,27 +446,28 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
{t('contact')}
|
{t('contact')}
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</m.div>
|
||||||
</motion.div>
|
</m.div>
|
||||||
|
|
||||||
{/* Bottom Branding */}
|
{/* Bottom Branding */}
|
||||||
<motion.div
|
<m.div
|
||||||
className="p-12 flex justify-center opacity-20"
|
className="p-12 flex justify-center opacity-20"
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
animate={isMobileMenuOpen ? { opacity: 0.2, scale: 1 } : { opacity: 0, scale: 0.8 }}
|
animate={isMobileMenuOpen ? { opacity: 0.2, scale: 1 } : { opacity: 0, scale: 0.8 }}
|
||||||
transition={{ duration: 0.5, delay: 1.4 }}
|
transition={{ duration: 0.5, delay: 1.4 }}
|
||||||
>
|
>
|
||||||
<motion.div
|
<m.div
|
||||||
initial={{ scale: 0.5 }}
|
initial={{ scale: 0.5 }}
|
||||||
animate={{ scale: 1 }}
|
animate={{ scale: 1 }}
|
||||||
transition={{ type: 'spring', stiffness: 300, delay: 1.5 }}
|
transition={{ type: 'spring', stiffness: 300, delay: 1.5 }}
|
||||||
>
|
>
|
||||||
<Image src="/logo-white.svg" alt={t('home')} width={80} height={80} unoptimized />
|
<Image src="/logo-white.svg" alt={t('home')} width={80} height={80} unoptimized />
|
||||||
</motion.div>
|
</m.div>
|
||||||
</motion.div>
|
</m.div>
|
||||||
</motion.nav>
|
</m.nav>
|
||||||
</div>
|
</div>
|
||||||
</motion.header>
|
</m.header>
|
||||||
|
</LazyMotion>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { m, LazyMotion, domAnimation, AnimatePresence } from 'framer-motion';
|
||||||
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
|
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
|
||||||
|
|
||||||
interface LightboxProps {
|
interface LightboxProps {
|
||||||
@@ -139,6 +139,7 @@ export default function Lightbox({ isOpen, images, initialIndex, onClose }: Ligh
|
|||||||
if (!mounted) return null;
|
if (!mounted) return null;
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
|
<LazyMotion strict features={domAnimation}>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div
|
<div
|
||||||
@@ -146,7 +147,7 @@ export default function Lightbox({ isOpen, images, initialIndex, onClose }: Ligh
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
>
|
>
|
||||||
<motion.div
|
<m.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
@@ -155,7 +156,7 @@ export default function Lightbox({ isOpen, images, initialIndex, onClose }: Ligh
|
|||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<motion.button
|
<m.button
|
||||||
initial={{ opacity: 0, scale: 0.5 }}
|
initial={{ opacity: 0, scale: 0.5 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0.5 }}
|
exit={{ opacity: 0, scale: 0.5 }}
|
||||||
@@ -168,9 +169,9 @@ export default function Lightbox({ isOpen, images, initialIndex, onClose }: Ligh
|
|||||||
<div className="relative w-full h-full flex items-center justify-center group-hover:rotate-90 transition-transform duration-500">
|
<div className="relative w-full h-full flex items-center justify-center group-hover:rotate-90 transition-transform duration-500">
|
||||||
<span className="text-3xl font-extralight leading-none mb-1">×</span>
|
<span className="text-3xl font-extralight leading-none mb-1">×</span>
|
||||||
</div>
|
</div>
|
||||||
</motion.button>
|
</m.button>
|
||||||
|
|
||||||
<motion.button
|
<m.button
|
||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
exit={{ opacity: 0, x: -20 }}
|
exit={{ opacity: 0, x: -20 }}
|
||||||
@@ -182,9 +183,9 @@ export default function Lightbox({ isOpen, images, initialIndex, onClose }: Ligh
|
|||||||
<span className="text-4xl font-extralight group-hover:-translate-x-1 transition-transform duration-500">
|
<span className="text-4xl font-extralight group-hover:-translate-x-1 transition-transform duration-500">
|
||||||
‹
|
‹
|
||||||
</span>
|
</span>
|
||||||
</motion.button>
|
</m.button>
|
||||||
|
|
||||||
<motion.button
|
<m.button
|
||||||
initial={{ opacity: 0, x: 20 }}
|
initial={{ opacity: 0, x: 20 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
exit={{ opacity: 0, x: 20 }}
|
exit={{ opacity: 0, x: 20 }}
|
||||||
@@ -196,9 +197,9 @@ export default function Lightbox({ isOpen, images, initialIndex, onClose }: Ligh
|
|||||||
<span className="text-4xl font-extralight group-hover:translate-x-1 transition-transform duration-500">
|
<span className="text-4xl font-extralight group-hover:translate-x-1 transition-transform duration-500">
|
||||||
›
|
›
|
||||||
</span>
|
</span>
|
||||||
</motion.button>
|
</m.button>
|
||||||
|
|
||||||
<motion.div
|
<m.div
|
||||||
initial={{ opacity: 0, y: 40, scale: 0.95 }}
|
initial={{ opacity: 0, y: 40, scale: 0.95 }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, y: 20, scale: 0.98 }}
|
exit={{ opacity: 0, y: 20, scale: 0.98 }}
|
||||||
@@ -208,7 +209,7 @@ export default function Lightbox({ isOpen, images, initialIndex, onClose }: Ligh
|
|||||||
<div className="pointer-events-auto w-full h-full flex flex-col items-center justify-center">
|
<div className="pointer-events-auto w-full h-full flex flex-col items-center justify-center">
|
||||||
<div className="relative w-full h-full shadow-[0_40px_100px_-20px_rgba(0,0,0,0.6)] ring-1 ring-white/20 overflow-hidden bg-primary-dark/50 rounded-2xl flex items-center justify-center">
|
<div className="relative w-full h-full shadow-[0_40px_100px_-20px_rgba(0,0,0,0.6)] ring-1 ring-white/20 overflow-hidden bg-primary-dark/50 rounded-2xl flex items-center justify-center">
|
||||||
<AnimatePresence mode="wait" initial={false}>
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
<motion.div
|
<m.div
|
||||||
key={currentIndex}
|
key={currentIndex}
|
||||||
initial={{ opacity: 0, scale: 1.1, filter: 'blur(10px)' }}
|
initial={{ opacity: 0, scale: 1.1, filter: 'blur(10px)' }}
|
||||||
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
|
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
|
||||||
@@ -223,7 +224,7 @@ export default function Lightbox({ isOpen, images, initialIndex, onClose }: Ligh
|
|||||||
className="object-cover transition-transform duration-1000 hover:scale-[1.03]"
|
className="object-cover transition-transform duration-1000 hover:scale-[1.03]"
|
||||||
unoptimized
|
unoptimized
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</m.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* Technical Detail: Subtle grid overlay to reinforce industrial precision */}
|
{/* Technical Detail: Subtle grid overlay to reinforce industrial precision */}
|
||||||
@@ -233,7 +234,7 @@ export default function Lightbox({ isOpen, images, initialIndex, onClose }: Ligh
|
|||||||
<div className="absolute inset-0 pointer-events-none bg-gradient-to-tr from-white/10 via-transparent to-transparent opacity-40 z-10" />
|
<div className="absolute inset-0 pointer-events-none bg-gradient-to-tr from-white/10 via-transparent to-transparent opacity-40 z-10" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<motion.div
|
<m.div
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: 10 }}
|
exit={{ opacity: 0, y: 10 }}
|
||||||
@@ -245,12 +246,13 @@ export default function Lightbox({ isOpen, images, initialIndex, onClose }: Ligh
|
|||||||
{currentIndex + 1} <span className="text-accent mx-3">/</span> {images.length}
|
{currentIndex + 1} <span className="text-accent mx-3">/</span> {images.length}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-px w-12 bg-white/20" />
|
<div className="h-px w-12 bg-white/20" />
|
||||||
</motion.div>
|
</m.div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</m.div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>,
|
</AnimatePresence>
|
||||||
|
</LazyMotion>,
|
||||||
document.body,
|
document.body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { motion, Variants } from 'framer-motion';
|
import { m, LazyMotion, domAnimation, Variants } from 'framer-motion';
|
||||||
import { cn } from '@/components/ui';
|
import { cn } from '@/components/ui';
|
||||||
|
|
||||||
interface ScribbleProps {
|
interface ScribbleProps {
|
||||||
@@ -25,13 +25,14 @@ export default function Scribble({ variant, className, color = '#82ed20' }: Scri
|
|||||||
|
|
||||||
if (variant === 'circle') {
|
if (variant === 'circle') {
|
||||||
return (
|
return (
|
||||||
|
<LazyMotion strict features={domAnimation}>
|
||||||
<svg
|
<svg
|
||||||
className={cn('absolute pointer-events-none', className)}
|
className={cn('absolute pointer-events-none', className)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
viewBox="0 0 800 350"
|
viewBox="0 0 800 350"
|
||||||
preserveAspectRatio="none"
|
preserveAspectRatio="none"
|
||||||
>
|
>
|
||||||
<motion.path
|
<m.path
|
||||||
variants={pathVariants}
|
variants={pathVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="visible"
|
whileInView="visible"
|
||||||
@@ -46,18 +47,20 @@ export default function Scribble({ variant, className, color = '#82ed20' }: Scri
|
|||||||
d=" M253,-161 C253,-161 -284.78900146484375,-201.4600067138672 -376,-21 C-469,163 67.62300109863281,174.2100067138672 256,121 C564,34 250.82899475097656,-141.6929931640625 19.10700035095215,-116.93599700927734"
|
d=" M253,-161 C253,-161 -284.78900146484375,-201.4600067138672 -376,-21 C-469,163 67.62300109863281,174.2100067138672 256,121 C564,34 250.82899475097656,-141.6929931640625 19.10700035095215,-116.93599700927734"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
</LazyMotion>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (variant === 'underline') {
|
if (variant === 'underline') {
|
||||||
return (
|
return (
|
||||||
|
<LazyMotion strict features={domAnimation}>
|
||||||
<svg
|
<svg
|
||||||
className={cn('absolute pointer-events-none', className)}
|
className={cn('absolute pointer-events-none', className)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
viewBox="-400 -55 730 60"
|
viewBox="-400 -55 730 60"
|
||||||
preserveAspectRatio="none"
|
preserveAspectRatio="none"
|
||||||
>
|
>
|
||||||
<motion.path
|
<m.path
|
||||||
variants={pathVariants}
|
variants={pathVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
whileInView="visible"
|
whileInView="visible"
|
||||||
@@ -68,6 +71,7 @@ export default function Scribble({ variant, className, color = '#82ed20' }: Scri
|
|||||||
fill="none"
|
fill="none"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
</LazyMotion>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { Suspense } from 'react';
|
import { Suspense, useEffect, useState } from 'react';
|
||||||
|
|
||||||
const DynamicAnalyticsProvider = dynamic(() => import('./AnalyticsProvider'), {
|
const DynamicAnalyticsProvider = dynamic(() => import('./AnalyticsProvider'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@@ -11,6 +11,20 @@ const DynamicScrollDepthTracker = dynamic(() => import('./ScrollDepthTracker'),
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function AnalyticsShell() {
|
export default function AnalyticsShell() {
|
||||||
|
const [shouldLoad, setShouldLoad] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Wait until browser is completely idle before loading heavy analytics/logger/sentry SDKs
|
||||||
|
if (typeof window !== 'undefined' && 'requestIdleCallback' in window) {
|
||||||
|
window.requestIdleCallback(() => setShouldLoad(true), { timeout: 3000 });
|
||||||
|
} else {
|
||||||
|
const timer = setTimeout(() => setShouldLoad(true), 2500);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!shouldLoad) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<DynamicAnalyticsProvider />
|
<DynamicAnalyticsProvider />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import Scribble from '@/components/Scribble';
|
import Scribble from '@/components/Scribble';
|
||||||
import { Button, Container, Heading, Section } from '@/components/ui';
|
import { Button, Container, Heading, Section } from '@/components/ui';
|
||||||
import { motion } from 'framer-motion';
|
import { m, LazyMotion, domAnimation } from 'framer-motion';
|
||||||
import { useTranslations, useLocale } from 'next-intl';
|
import { useTranslations, useLocale } from 'next-intl';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { useAnalytics } from '../analytics/useAnalytics';
|
import { useAnalytics } from '../analytics/useAnalytics';
|
||||||
@@ -16,14 +16,15 @@ export default function Hero() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Section className="relative min-h-[85vh] md:h-[90vh] flex flex-col items-center justify-center overflow-hidden bg-primary py-12 md:py-0 lg:py-0">
|
<Section className="relative min-h-[85vh] md:h-[90vh] flex flex-col items-center justify-center overflow-hidden bg-primary py-12 md:py-0 lg:py-0">
|
||||||
|
<LazyMotion strict features={domAnimation}>
|
||||||
<Container className="relative z-10 text-center md:text-left text-white w-full order-2 md:order-none">
|
<Container className="relative z-10 text-center md:text-left text-white w-full order-2 md:order-none">
|
||||||
<motion.div
|
<m.div
|
||||||
className="max-w-5xl mx-auto md:mx-0"
|
className="max-w-5xl mx-auto md:mx-0"
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
>
|
>
|
||||||
<motion.div variants={headingVariants}>
|
<m.div variants={headingVariants}>
|
||||||
<Heading
|
<Heading
|
||||||
level={1}
|
level={1}
|
||||||
className="text-center md:text-left mb-6 md:mb-8 md:max-w-none text-white text-4xl sm:text-5xl md:text-7xl font-extrabold [text-shadow:_-2px_-2px_0_#002b49,_2px_-2px_0_#002b49,_-2px_2px_0_#002b49,_2px_2px_0_#002b49,_-2px_0_0_#002b49,_2px_0_0_#002b49,_0_-2px_0_#002b49,_0_2px_0_#002b49]"
|
className="text-center md:text-left mb-6 md:mb-8 md:max-w-none text-white text-4xl sm:text-5xl md:text-7xl font-extrabold [text-shadow:_-2px_-2px_0_#002b49,_2px_-2px_0_#002b49,_-2px_2px_0_#002b49,_2px_2px_0_#002b49,_-2px_0_0_#002b49,_2px_0_0_#002b49,_0_-2px_0_#002b49,_0_2px_0_#002b49]"
|
||||||
@@ -31,33 +32,33 @@ export default function Hero() {
|
|||||||
{t.rich('title', {
|
{t.rich('title', {
|
||||||
green: (chunks) => (
|
green: (chunks) => (
|
||||||
<span className="relative inline-block">
|
<span className="relative inline-block">
|
||||||
<motion.span
|
<m.span
|
||||||
className="relative z-10 text-accent italic"
|
className="relative z-10 text-accent italic"
|
||||||
variants={accentVariants}
|
variants={accentVariants}
|
||||||
>
|
>
|
||||||
{chunks}
|
{chunks}
|
||||||
</motion.span>
|
</m.span>
|
||||||
<motion.div
|
<m.div
|
||||||
variants={scribbleVariants}
|
variants={scribbleVariants}
|
||||||
className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10"
|
className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10"
|
||||||
>
|
>
|
||||||
<Scribble variant="circle" />
|
<Scribble variant="circle" />
|
||||||
</motion.div>
|
</m.div>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
})}
|
})}
|
||||||
</Heading>
|
</Heading>
|
||||||
</motion.div>
|
</m.div>
|
||||||
<motion.div variants={subtitleVariants}>
|
<m.div variants={subtitleVariants}>
|
||||||
<p className="text-lg md:text-xl text-white/90 leading-relaxed max-w-2xl mb-10 md:mb-12">
|
<p className="text-lg md:text-xl text-white/90 leading-relaxed max-w-2xl mb-10 md:mb-12">
|
||||||
{t('subtitle')}
|
{t('subtitle')}
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</m.div>
|
||||||
<motion.div
|
<m.div
|
||||||
className="flex flex-col sm:flex-row justify-center md:justify-start gap-4 md:gap-6"
|
className="flex flex-col sm:flex-row justify-center md:justify-start gap-4 md:gap-6"
|
||||||
variants={buttonContainerVariants}
|
variants={buttonContainerVariants}
|
||||||
>
|
>
|
||||||
<motion.div variants={buttonVariants}>
|
<m.div variants={buttonVariants}>
|
||||||
<Button
|
<Button
|
||||||
href="/contact"
|
href="/contact"
|
||||||
variant="accent"
|
variant="accent"
|
||||||
@@ -73,8 +74,8 @@ export default function Hero() {
|
|||||||
{t('cta')}
|
{t('cta')}
|
||||||
<span className="transition-transform group-hover/btn:translate-x-1">→</span>
|
<span className="transition-transform group-hover/btn:translate-x-1">→</span>
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</m.div>
|
||||||
<motion.div variants={buttonVariants}>
|
<m.div variants={buttonVariants}>
|
||||||
<Button
|
<Button
|
||||||
href={`/${locale}/${locale === 'de' ? 'produkte' : 'products'}`}
|
href={`/${locale}/${locale === 'de' ? 'produkte' : 'products'}`}
|
||||||
variant="white"
|
variant="white"
|
||||||
@@ -89,28 +90,28 @@ export default function Hero() {
|
|||||||
>
|
>
|
||||||
{t('exploreProducts')}
|
{t('exploreProducts')}
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</m.div>
|
||||||
</motion.div>
|
</m.div>
|
||||||
</motion.div>
|
</m.div>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<motion.div
|
<m.div
|
||||||
className="relative md:absolute inset-0 z-0 w-full h-[40vh] md:h-full order-1 md:order-none mb-[-80px] md:mb-0 mt-[80px] md:mt-0 overflow-visible pointer-events-none"
|
className="relative md:absolute inset-0 z-0 w-full h-[40vh] md:h-full order-1 md:order-none mb-[-80px] md:mb-0 mt-[80px] md:mt-0 overflow-visible pointer-events-none"
|
||||||
initial={{ opacity: 0, scale: 0.95, filter: 'blur(20px)' }}
|
initial={{ opacity: 0, scale: 0.95, filter: 'blur(20px)' }}
|
||||||
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
|
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
|
||||||
transition={{ duration: 2.2, ease: 'easeOut', delay: 0.05 }}
|
transition={{ duration: 2.2, ease: 'easeOut', delay: 0.05 }}
|
||||||
>
|
>
|
||||||
<HeroIllustration />
|
<HeroIllustration />
|
||||||
</motion.div>
|
</m.div>
|
||||||
|
|
||||||
<motion.div
|
<m.div
|
||||||
className="absolute bottom-6 md:bottom-10 left-1/2 -translate-x-1/2 hidden sm:block"
|
className="absolute bottom-6 md:bottom-10 left-1/2 -translate-x-1/2 hidden sm:block"
|
||||||
initial={{ opacity: 0, y: 16 }}
|
initial={{ opacity: 0, y: 16 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 1, ease: 'easeOut', delay: 3 }}
|
transition={{ duration: 1, ease: 'easeOut', delay: 3 }}
|
||||||
>
|
>
|
||||||
<div className="w-6 h-10 border-2 border-white/30 rounded-full flex justify-center p-1">
|
<div className="w-6 h-10 border-2 border-white/30 rounded-full flex justify-center p-1">
|
||||||
<motion.div
|
<m.div
|
||||||
className="w-1 h-2 bg-white rounded-full"
|
className="w-1 h-2 bg-white rounded-full"
|
||||||
animate={{ y: [0, -10, 0] }}
|
animate={{ y: [0, -10, 0] }}
|
||||||
transition={{
|
transition={{
|
||||||
@@ -120,7 +121,8 @@ export default function Hero() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</m.div>
|
||||||
|
</LazyMotion>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,16 +232,12 @@ export default function HeroIllustration() {
|
|||||||
stroke="url(#energy-pulse)"
|
stroke="url(#energy-pulse)"
|
||||||
strokeWidth="3"
|
strokeWidth="3"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeDasharray={`${length * 0.2} ${length * 0.8}`}
|
style={{
|
||||||
>
|
strokeDasharray: `${length * 0.2} ${length * 0.8}`,
|
||||||
<animate
|
strokeDashoffset: length,
|
||||||
attributeName="stroke-dashoffset"
|
animation: `flow ${1.5 + (i % 3) * 0.5}s linear infinite`,
|
||||||
from={length}
|
}}
|
||||||
to={0}
|
|
||||||
dur={`${1.5 + (i % 3) * 0.5}s`}
|
|
||||||
repeatCount="indefinite"
|
|
||||||
/>
|
/>
|
||||||
</line>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</g>
|
</g>
|
||||||
@@ -267,14 +263,13 @@ export default function HeroIllustration() {
|
|||||||
strokeWidth="1"
|
strokeWidth="1"
|
||||||
strokeOpacity="0.3"
|
strokeOpacity="0.3"
|
||||||
/>
|
/>
|
||||||
<circle r="3" fill="#82ed20" fillOpacity="0.3" filter="url(#soft-glow)">
|
<circle
|
||||||
<animate
|
r="3"
|
||||||
attributeName="fillOpacity"
|
fill="#82ed20"
|
||||||
values="0.2;0.5;0.2"
|
fillOpacity="0.3"
|
||||||
dur="2s"
|
filter="url(#soft-glow)"
|
||||||
repeatCount="indefinite"
|
style={{ animation: 'solar-pulse 2s ease-in-out infinite' }}
|
||||||
/>
|
/>
|
||||||
</circle>
|
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -294,6 +289,12 @@ export default function HeroIllustration() {
|
|||||||
strokeOpacity="0.3"
|
strokeOpacity="0.3"
|
||||||
/>
|
/>
|
||||||
<g transform="translate(0, -60)">
|
<g transform="translate(0, -60)">
|
||||||
|
<g
|
||||||
|
style={{
|
||||||
|
transformOrigin: '0px 0px',
|
||||||
|
animation: `spin-slow ${3 + i}s linear infinite`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{[0, 120, 240].map((angle, j) => (
|
{[0, 120, 240].map((angle, j) => (
|
||||||
<line
|
<line
|
||||||
key={`blade-${i}-${j}`}
|
key={`blade-${i}-${j}`}
|
||||||
@@ -305,19 +306,11 @@ export default function HeroIllustration() {
|
|||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
strokeOpacity="0.4"
|
strokeOpacity="0.4"
|
||||||
transform={`rotate(${angle})`}
|
transform={`rotate(${angle})`}
|
||||||
>
|
|
||||||
<animateTransform
|
|
||||||
attributeName="transform"
|
|
||||||
type="rotate"
|
|
||||||
from={`${angle} 0 0`}
|
|
||||||
to={`${angle + 360} 0 0`}
|
|
||||||
dur={`${3 + i}s`}
|
|
||||||
repeatCount="indefinite"
|
|
||||||
/>
|
/>
|
||||||
</line>
|
|
||||||
))}
|
))}
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
</g>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { m, LazyMotion, domAnimation, AnimatePresence } from 'framer-motion';
|
||||||
import { useRecordMode } from './RecordModeContext';
|
import { useRecordMode } from './RecordModeContext';
|
||||||
|
|
||||||
export function PlaybackCursor() {
|
export function PlaybackCursor() {
|
||||||
@@ -24,7 +24,8 @@ export function PlaybackCursor() {
|
|||||||
if (!isPlaying) return null;
|
if (!isPlaying) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<LazyMotion strict features={domAnimation}>
|
||||||
|
<m.div
|
||||||
className="fixed z-[10000] pointer-events-none"
|
className="fixed z-[10000] pointer-events-none"
|
||||||
animate={{
|
animate={{
|
||||||
x: cursorPosition.x,
|
x: cursorPosition.x,
|
||||||
@@ -44,7 +45,7 @@ export function PlaybackCursor() {
|
|||||||
>
|
>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isClicking && (
|
{isClicking && (
|
||||||
<motion.div
|
<m.div
|
||||||
initial={{ scale: 0.5, opacity: 0 }}
|
initial={{ scale: 0.5, opacity: 0 }}
|
||||||
animate={{ scale: 2.5, opacity: 0 }}
|
animate={{ scale: 2.5, opacity: 0 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
@@ -85,6 +86,7 @@ export function PlaybackCursor() {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</m.div>
|
||||||
|
</LazyMotion>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useRecordMode } from './RecordModeContext';
|
import { useRecordMode } from './RecordModeContext';
|
||||||
import { Reorder, AnimatePresence } from 'framer-motion';
|
import { Reorder, AnimatePresence, LazyMotion, domAnimation } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
Play,
|
Play,
|
||||||
Square,
|
Square,
|
||||||
@@ -146,6 +146,7 @@ export function RecordModeOverlay() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<LazyMotion strict features={domAnimation}>
|
||||||
<div className="fixed inset-0 z-[9998] pointer-events-none font-sans">
|
<div className="fixed inset-0 z-[9998] pointer-events-none font-sans">
|
||||||
{/* 1. Global Toolbar - Slim Industrial Bar */}
|
{/* 1. Global Toolbar - Slim Industrial Bar */}
|
||||||
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[10000] pointer-events-auto">
|
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[10000] pointer-events-auto">
|
||||||
@@ -230,7 +231,11 @@ export function RecordModeOverlay() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const session = { events, name: 'Recording', createdAt: new Date().toISOString() };
|
const session = {
|
||||||
|
events,
|
||||||
|
name: 'Recording',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/save-session', {
|
const res = await fetch('/api/save-session', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -338,7 +343,9 @@ export function RecordModeOverlay() {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-white text-[10px] font-black uppercase tracking-widest">
|
<span className="text-white text-[10px] font-black uppercase tracking-widest">
|
||||||
{event.type === 'mouse' ? `Mouse (${event.interactionType})` : event.type}
|
{event.type === 'mouse'
|
||||||
|
? `Mouse (${event.interactionType})`
|
||||||
|
: event.type}
|
||||||
</span>
|
</span>
|
||||||
{event.clickOrigin &&
|
{event.clickOrigin &&
|
||||||
event.clickOrigin !== 'center' &&
|
event.clickOrigin !== 'center' &&
|
||||||
@@ -434,7 +441,11 @@ export function RecordModeOverlay() {
|
|||||||
<div className="flex gap-2 p-1 bg-white/5 rounded-2xl border border-white/5">
|
<div className="flex gap-2 p-1 bg-white/5 rounded-2xl border border-white/5">
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setEditForm((prev) => ({ ...prev, type: 'mouse', interactionType: 'click' }))
|
setEditForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
type: 'mouse',
|
||||||
|
interactionType: 'click',
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl transition-all border ${editForm.type === 'mouse' && editForm.interactionType === 'click' ? 'bg-accent text-primary-dark border-accent' : 'text-white/40 border-transparent hover:border-white/10'}`}
|
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl transition-all border ${editForm.type === 'mouse' && editForm.interactionType === 'click' ? 'bg-accent text-primary-dark border-accent' : 'text-white/40 border-transparent hover:border-white/10'}`}
|
||||||
>
|
>
|
||||||
@@ -443,7 +454,11 @@ export function RecordModeOverlay() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setEditForm((prev) => ({ ...prev, type: 'mouse', interactionType: 'hover' }))
|
setEditForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
type: 'mouse',
|
||||||
|
interactionType: 'hover',
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl transition-all border ${editForm.type === 'mouse' && editForm.interactionType === 'hover' ? 'bg-accent text-primary-dark border-accent' : 'text-white/40 border-transparent hover:border-white/10'}`}
|
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl transition-all border ${editForm.type === 'mouse' && editForm.interactionType === 'hover' ? 'bg-accent text-primary-dark border-accent' : 'text-white/40 border-transparent hover:border-white/10'}`}
|
||||||
>
|
>
|
||||||
@@ -537,19 +552,25 @@ export function RecordModeOverlay() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditForm((prev) => ({ ...prev, motionBlur: !prev.motionBlur }))}
|
onClick={() =>
|
||||||
|
setEditForm((prev) => ({ ...prev, motionBlur: !prev.motionBlur }))
|
||||||
|
}
|
||||||
className={`flex items-center justify-between w-full p-4 rounded-2xl border transition-all ${editForm.motionBlur ? 'bg-accent/10 border-accent/30 text-accent' : 'bg-white/5 border-white/5 text-white/40'}`}
|
className={`flex items-center justify-between w-full p-4 rounded-2xl border transition-all ${editForm.motionBlur ? 'bg-accent/10 border-accent/30 text-accent' : 'bg-white/5 border-white/5 text-white/40'}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Box size={18} />
|
<Box size={18} />
|
||||||
<span className="text-xs font-bold uppercase tracking-wider">Motion Blur</span>
|
<span className="text-xs font-bold uppercase tracking-wider">
|
||||||
|
Motion Blur
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{editForm.motionBlur ? <Check size={18} /> : <div className="w-[18px]" />}
|
{editForm.motionBlur ? <Check size={18} /> : <div className="w-[18px]" />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{editForm.type === 'mouse' && editForm.interactionType === 'click' && (
|
{editForm.type === 'mouse' && editForm.interactionType === 'click' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditForm((prev) => ({ ...prev, realClick: !prev.realClick }))}
|
onClick={() =>
|
||||||
|
setEditForm((prev) => ({ ...prev, realClick: !prev.realClick }))
|
||||||
|
}
|
||||||
className={`flex items-center justify-between w-full p-4 rounded-2xl border transition-all ${editForm.realClick ? 'bg-orange-500/10 border-orange-500/30 text-orange-400' : 'bg-white/5 border-white/5 text-white/40'}`}
|
className={`flex items-center justify-between w-full p-4 rounded-2xl border transition-all ${editForm.realClick ? 'bg-orange-500/10 border-orange-500/30 text-orange-400' : 'bg-white/5 border-white/5 text-white/40'}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -579,5 +600,6 @@ export function RecordModeOverlay() {
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
</LazyMotion>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"categories:performance": [
|
"categories:performance": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
"minScore": 0.8
|
"minScore": 0.9
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"categories:accessibility": [
|
"categories:accessibility": [
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
title: 'Herzlich willkommen bei KLZ: Johannes Gleich startet als Senior Key Account Manager durch'
|
||||||
|
date: '2026-02-20T14:50:00'
|
||||||
|
featuredImage: /uploads/2026/01/1767353529807.jpg
|
||||||
|
locale: de
|
||||||
|
category: Kabel Technologie
|
||||||
|
excerpt: 'KLZ Cables startet mit einer starken Verstärkung ins neue Jahr: Johannes Gleich übernimmt die Rolle des Senior Key Account Managers. Erfahren Sie mehr über unseren neuen Experten für Infrastruktur und Energieversorger.'
|
||||||
|
---
|
||||||
|
# Herzlich willkommen bei KLZ: Johannes Gleich startet als Senior Key Account Manager durch
|
||||||
|
|
||||||
|
KLZ Cables startet mit einer starken Verstärkung ins neue Jahr: Seit Januar 2026 übernimmt Johannes Gleich die Rolle des Senior Key Account Managers. Mit ihm gewinnen wir nicht nur zusätzliche Vertriebskraft, sondern auch jahrzehntelange Erfahrung und ein wertvolles Branchennetzwerk.
|
||||||
|
|
||||||
|
### **1. Ein bekanntes Gesicht für eine effektive Zusammenarbeit**
|
||||||
|
|
||||||
|
Johannes ist für KLZ kein Neuling: Bereits während seiner über zehnjährigen Tätigkeit bei der LAPP Gruppe hat unser Team die Zusammenarbeit mit ihm kennengelernt und sehr geschätzt. Diese bestehende Vertrautheit und das gegenseitige Vertrauen erleichtern den Einstieg enorm und versprechen eine produktive Kooperation von Tag eins an.
|
||||||
|
|
||||||
|
### **2. Beruflicher Hintergrund: Erfahrung trifft technische Tiefe**
|
||||||
|
|
||||||
|
Mit rund 50 Jahren verbindet Johannes fundierte Berufserfahrung mit frischer Motivation. Seine Basis ist eine technische Ausbildung im Bereich Elektrotechnik. Dieses Fundament ermöglicht es ihm, unsere Produkte nicht nur zu vertreiben, sondern sie in ihrer gesamten technischen Tiefe zu erklären und einzuordnen.
|
||||||
|
|
||||||
|
**Sein Werdegang im Überblick:**
|
||||||
|
|
||||||
|
<TechnicalGrid
|
||||||
|
title="Karrierestationen"
|
||||||
|
items={[
|
||||||
|
{ label: "Seit Jan. 2026", value: "Senior Key Account Manager bei KLZ Vertriebs GmbH (Remote)" },
|
||||||
|
{ label: "2015 – 2026", value: "Projektmanager Infrastrukturbereich Stadtwerke & Energieversorger bei der LAPP Gruppe (Stuttgart)" }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
In den vergangenen elf Jahren hat er sich als Experte für die Anforderungen großer Infrastrukturanbieter etabliert. Er kennt die Herausforderungen der Branche – technisch, wirtschaftlich und strategisch – aus erster Hand.
|
||||||
|
|
||||||
|
### **3. Expertise: Ausschreibungen, Normen und Markttrends**
|
||||||
|
|
||||||
|
Was Johannes besonders wertvoll für unser Team macht, ist sein spezialisiertes Fachwissen:
|
||||||
|
|
||||||
|
<TechnicalGrid
|
||||||
|
title="Kernkompetenzen"
|
||||||
|
items={[
|
||||||
|
{ label: "Tender-Management", value: "Seine umfassende Erfahrung macht ihn zu einem sicheren Partner bei komplexen Ausschreibungen." },
|
||||||
|
{ label: "Normen & Fertigung", value: "Er verfügt über tiefgehende Kenntnisse im Bereich Kabelnormen und der Kabelfertigung." },
|
||||||
|
{ label: "Marktkenntnis", value: "Trends, Preisentwicklungen und Beschaffungsstrategien im deutschen Kabelmarkt sind ihm bestens vertraut." },
|
||||||
|
{ label: "Logistik", value: "Fundierte Kenntnisse in der Lieferkette runden sein Profil ab." }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### **4. Ein verlässlicher Partner auf Augenhöhe**
|
||||||
|
|
||||||
|
Johannes genießt bei Kunden eine hohe Wertschätzung als echter „Kümmerer“. Er übernimmt Verantwortung und zeichnet sich durch eine ausgleichende, aber in der Sache klare Verhandlungsführung aus. Seine Fähigkeit, komplexe Anforderungen strukturiert umzusetzen, hat sich bereits in früheren gemeinsamen Projekten mit KLZ bewährt.
|
||||||
|
|
||||||
|
### **5. Neue Rolle und Ziele bei KLZ Cables**
|
||||||
|
|
||||||
|
In seiner neuen Position wird Johannes den Vertrieb strategisch verstärken und die Geschäftsführung operativ entlasten.
|
||||||
|
|
||||||
|
**Seine Kernaufgaben umfassen:**
|
||||||
|
|
||||||
|
- **Gezielte Betreuung:** Fokus auf Stadtwerke, Netzbetreiber und Energieversorger.
|
||||||
|
- **Markterschließung:** Aufbau von Kontakten in den Bereichen Renewables und Tiefbau.
|
||||||
|
- **Strategische Planung:** Umsetzung von Vertriebsaktivitäten ohne administrative Grenzen, um maximale Dynamik zu entfalten.
|
||||||
|
|
||||||
|
### **6. Ausblick**
|
||||||
|
|
||||||
|
Wir freuen uns besonders, dass Johannes bei KLZ den Raum findet, sein gesamtes Wissen optimal für unsere Kunden einzusetzen. Mit seiner Kombination aus technischem Know-how, Markterfahrung und menschlicher Integrität ist er genau am richtigen Ort, um das Wachstum von KLZ Cables nachhaltig zu fördern.
|
||||||
|
|
||||||
|
Herzlich willkommen im Team, Johannes! Wir freuen uns auf die gemeinsamen Projekte.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
title: 'Welcome to KLZ: Johannes Gleich starts as Senior Key Account Manager'
|
||||||
|
date: '2026-02-20T14:50:00'
|
||||||
|
featuredImage: /uploads/2026/01/1767353529807.jpg
|
||||||
|
locale: en
|
||||||
|
category: Cable Technology
|
||||||
|
excerpt: 'KLZ Cables kicks off the new year with a strong addition: Johannes Gleich takes on the role of Senior Key Account Manager. Learn more about our new expert for infrastructure and energy suppliers.'
|
||||||
|
---
|
||||||
|
# Welcome to KLZ: Johannes Gleich starts as Senior Key Account Manager
|
||||||
|
|
||||||
|
KLZ Cables kicks off the new year with a strong addition to the team: Since January 2026, Johannes Gleich has taken on the role of Senior Key Account Manager. With him, we gain not only additional sales power, but also decades of experience and a valuable industry network.
|
||||||
|
|
||||||
|
### **1. A familiar face for effective collaboration**
|
||||||
|
|
||||||
|
Johannes is no stranger to KLZ: During his more than ten years at the LAPP Group, our team had the pleasure of working with him and greatly appreciated the collaboration. This existing familiarity and mutual trust make his start enormously easier and promise productive cooperation from day one.
|
||||||
|
|
||||||
|
### **2. Professional background: Experience meets technical depth**
|
||||||
|
|
||||||
|
At around 50 years of age, Johannes combines solid professional experience with fresh motivation. His foundation is a technical education in electrical engineering. This basis enables him not only to sell our products, but also to explain and classify them in their full technical depth.
|
||||||
|
|
||||||
|
**His career at a glance:**
|
||||||
|
|
||||||
|
<TechnicalGrid
|
||||||
|
title="Career Stations"
|
||||||
|
items={[
|
||||||
|
{ label: "Since Jan. 2026", value: "Senior Key Account Manager at KLZ Vertriebs GmbH (Remote)" },
|
||||||
|
{ label: "2015 – 2026", value: "Project Manager Infrastructure Municipal Utilities & Energy Suppliers at the LAPP Group (Stuttgart)" }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
Over the past eleven years, he has established himself as an expert in the requirements of large infrastructure providers. He knows the industry's challenges—technical, economic, and strategic—firsthand.
|
||||||
|
|
||||||
|
### **3. Expertise: Tenders, standards, and market trends**
|
||||||
|
|
||||||
|
What makes Johannes particularly valuable to our team is his specialized expertise:
|
||||||
|
|
||||||
|
<TechnicalGrid
|
||||||
|
title="Core Competencies"
|
||||||
|
items={[
|
||||||
|
{ label: "Tender Management", value: "His extensive experience makes him a reliable partner for complex tenders." },
|
||||||
|
{ label: "Standards & Production", value: "He has deeply rooted knowledge in cable standards and cable manufacturing." },
|
||||||
|
{ label: "Market Knowledge", value: "He is highly familiar with trends, price developments, and procurement strategies in the German cable market." },
|
||||||
|
{ label: "Logistics", value: "Solid knowledge of the supply chain rounds out his profile." }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### **4. A reliable partner at eye level**
|
||||||
|
|
||||||
|
Johannes is highly valued by customers as a true "caretaker". He takes responsibility and stands out for his balanced yet clear negotiation skills. His ability to implement complex requirements in a structured manner has already proven itself in past joint projects with KLZ.
|
||||||
|
|
||||||
|
### **5. New role and goals at KLZ Cables**
|
||||||
|
|
||||||
|
In his new position, Johannes will strategically strengthen sales and operatively relieve the management.
|
||||||
|
|
||||||
|
**His core responsibilities include:**
|
||||||
|
|
||||||
|
- **Targeted Support:** Focus on municipal utilities, grid operators, and energy suppliers.
|
||||||
|
- **Market Penetration:** Building contacts in the renewables and civil engineering sectors.
|
||||||
|
- **Strategic Planning:** Implementing sales activities without administrative boundaries to unfold maximum dynamism.
|
||||||
|
|
||||||
|
### **6. Outlook**
|
||||||
|
|
||||||
|
We are especially pleased that Johannes has found the space at KLZ to optimally use all his knowledge for our customers. With his combination of technical know-how, market experience, and personal integrity, he is exactly in the right place to sustainably promote the growth of KLZ Cables.
|
||||||
|
|
||||||
|
Welcome to the team, Johannes! We look forward to our future projects together.
|
||||||
@@ -35,9 +35,9 @@ function createConfig() {
|
|||||||
|
|
||||||
analytics: {
|
analytics: {
|
||||||
umami: {
|
umami: {
|
||||||
websiteId: env.UMAMI_WEBSITE_ID,
|
websiteId: env.NEXT_PUBLIC_UMAMI_WEBSITE_ID || env.UMAMI_WEBSITE_ID,
|
||||||
apiEndpoint: env.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me',
|
apiEndpoint: env.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me',
|
||||||
enabled: Boolean(env.UMAMI_WEBSITE_ID),
|
enabled: typeof window !== 'undefined' || Boolean(env.UMAMI_WEBSITE_ID),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const envExtension = {
|
|||||||
INFRA_DIRECTUS_TOKEN: z.string().optional(),
|
INFRA_DIRECTUS_TOKEN: z.string().optional(),
|
||||||
|
|
||||||
// Analytics
|
// Analytics
|
||||||
UMAMI_WEBSITE_ID: z.string().optional(),
|
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.string().optional(),
|
||||||
UMAMI_API_ENDPOINT: z.string().optional(),
|
UMAMI_API_ENDPOINT: z.string().optional(),
|
||||||
|
|
||||||
// Mail Configuration
|
// Mail Configuration
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/nextjs';
|
|
||||||
import type {
|
import type {
|
||||||
ErrorReportingLevel,
|
ErrorReportingLevel,
|
||||||
ErrorReportingService,
|
ErrorReportingService,
|
||||||
@@ -7,32 +6,66 @@ import type {
|
|||||||
import type { NotificationService } from '../notifications/notification-service';
|
import type { NotificationService } from '../notifications/notification-service';
|
||||||
import type { LoggerService } from '../logging/logger-service';
|
import type { LoggerService } from '../logging/logger-service';
|
||||||
|
|
||||||
type SentryLike = typeof Sentry;
|
|
||||||
|
|
||||||
export type GlitchtipErrorReportingServiceOptions = {
|
export type GlitchtipErrorReportingServiceOptions = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// GlitchTip speaks the Sentry protocol; @sentry/nextjs can send to GlitchTip via DSN.
|
// GlitchTip speaks the Sentry protocol; @sentry/nextjs can send to GlitchTip via DSN.
|
||||||
|
// Sentry is dynamically imported to avoid a ~100KB main-thread execution penalty on initial load.
|
||||||
export class GlitchtipErrorReportingService implements ErrorReportingService {
|
export class GlitchtipErrorReportingService implements ErrorReportingService {
|
||||||
private logger: LoggerService;
|
private logger: LoggerService;
|
||||||
|
private sentryPromise: Promise<typeof import('@sentry/nextjs')> | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly options: GlitchtipErrorReportingServiceOptions,
|
private readonly options: GlitchtipErrorReportingServiceOptions,
|
||||||
logger: LoggerService,
|
logger: LoggerService,
|
||||||
private readonly notifications?: NotificationService,
|
private readonly notifications?: NotificationService,
|
||||||
private readonly sentry: SentryLike = Sentry,
|
|
||||||
) {
|
) {
|
||||||
this.logger = logger.child({ component: 'error-reporting-glitchtip' });
|
this.logger = logger.child({ component: 'error-reporting-glitchtip' });
|
||||||
|
|
||||||
|
if (this.options.enabled) {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// On client-side, wait until idle before fetching Sentry
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
window.requestIdleCallback(() => {
|
||||||
|
this.getSentry();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.getSentry();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Pre-fetch on server-side
|
||||||
|
this.getSentry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSentry(): Promise<typeof import('@sentry/nextjs')> {
|
||||||
|
if (!this.sentryPromise) {
|
||||||
|
this.sentryPromise = import('@sentry/nextjs').then((Sentry) => {
|
||||||
|
// Client-side initialization must happen here since sentry.client.config.ts is empty
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: 'https://public@errors.infra.mintel.me/1',
|
||||||
|
tunnel: '/errors/api/relay',
|
||||||
|
enabled: true,
|
||||||
|
tracesSampleRate: 0,
|
||||||
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
replaysSessionSampleRate: 0.1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Sentry;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.sentryPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async captureException(error: unknown, context?: Record<string, unknown>) {
|
async captureException(error: unknown, context?: Record<string, unknown>) {
|
||||||
if (!this.options.enabled) return undefined;
|
if (!this.options.enabled) return undefined;
|
||||||
const result = this.sentry.captureException(error, context as any) as any;
|
|
||||||
|
|
||||||
// Send to Gotify if it's considered critical or if we just want all exceptions there
|
// Send to Gotify if it's considered critical or if we just want all exceptions there
|
||||||
// For now, let's send all exceptions to Gotify as requested "notify me via gotify about critical error messages"
|
|
||||||
// We'll treat all captureException calls as potentially critical or at least noteworthy
|
|
||||||
if (this.notifications) {
|
if (this.notifications) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
const contextStr = context ? `\nContext: ${JSON.stringify(context, null, 2)}` : '';
|
const contextStr = context ? `\nContext: ${JSON.stringify(context, null, 2)}` : '';
|
||||||
@@ -44,34 +77,33 @@ export class GlitchtipErrorReportingService implements ErrorReportingService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
const Sentry = await this.getSentry();
|
||||||
|
return Sentry.captureException(error, context as any) as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
captureMessage(message: string, level: ErrorReportingLevel = 'error') {
|
async captureMessage(message: string, level: ErrorReportingLevel = 'error') {
|
||||||
if (!this.options.enabled) return undefined;
|
if (!this.options.enabled) return undefined;
|
||||||
return this.sentry.captureMessage(message, level as any) as any;
|
const Sentry = await this.getSentry();
|
||||||
|
return Sentry.captureMessage(message, level as any) as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
setUser(user: ErrorReportingUser | null) {
|
setUser(user: ErrorReportingUser | null) {
|
||||||
if (!this.options.enabled) return;
|
if (!this.options.enabled) return;
|
||||||
this.sentry.setUser(user as any);
|
this.getSentry().then((Sentry) => Sentry.setUser(user as any));
|
||||||
}
|
}
|
||||||
|
|
||||||
setTag(key: string, value: string) {
|
setTag(key: string, value: string) {
|
||||||
if (!this.options.enabled) return;
|
if (!this.options.enabled) return;
|
||||||
this.sentry.setTag(key, value);
|
this.getSentry().then((Sentry) => Sentry.setTag(key, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
withScope<T>(fn: () => T, context?: Record<string, unknown>) {
|
withScope<T>(fn: () => T, context?: Record<string, unknown>): T {
|
||||||
if (!this.options.enabled) return fn();
|
if (!this.options.enabled) return fn();
|
||||||
|
|
||||||
return this.sentry.withScope((scope) => {
|
// Since withScope mandates executing fn() synchronously to return T,
|
||||||
if (context) {
|
// and Sentry load is async, if context mapping is absolutely required
|
||||||
for (const [key, value] of Object.entries(context)) {
|
// for this feature we would need an async API.
|
||||||
scope.setExtra(key, value);
|
// For now we degrade gracefully by just executing the function.
|
||||||
}
|
|
||||||
}
|
|
||||||
return fn();
|
return fn();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ const nextConfig = {
|
|||||||
// Make sure entries are not disposed too quickly
|
// Make sure entries are not disposed too quickly
|
||||||
maxInactiveAge: 60 * 1000,
|
maxInactiveAge: 60 * 1000,
|
||||||
},
|
},
|
||||||
|
experimental: {
|
||||||
|
optimizeCss: true,
|
||||||
|
optimizePackageImports: ['lucide-react', 'framer-motion', '@/components/ui'],
|
||||||
|
},
|
||||||
productionBrowserSourceMaps: false,
|
productionBrowserSourceMaps: false,
|
||||||
logging: {
|
logging: {
|
||||||
fetches: {
|
fetches: {
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
"@vitest/ui": "^4.0.16",
|
"@vitest/ui": "^4.0.16",
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
|
"critters": "^0.0.25",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"happy-dom": "^20.6.1",
|
"happy-dom": "^20.6.1",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
|||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -198,6 +198,9 @@ importers:
|
|||||||
cheerio:
|
cheerio:
|
||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
|
critters:
|
||||||
|
specifier: ^0.0.25
|
||||||
|
version: 0.0.25
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.18.0
|
specifier: ^9.18.0
|
||||||
version: 9.39.2(jiti@2.6.1)
|
version: 9.39.2(jiti@2.6.1)
|
||||||
@@ -3694,6 +3697,10 @@ packages:
|
|||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
critters@0.0.25:
|
||||||
|
resolution: {integrity: sha512-ROF/tjJyyRdM8/6W0VqoN5Ql05xAGnkf5b7f3sTEl1bI5jTQQf8O918RD/V9tEb9pRY/TKcvJekDbJtniHyPtQ==}
|
||||||
|
deprecated: Ownership of Critters has moved to the Nuxt team, who will be maintaining the project going forward. If you'd like to keep using Critters, please switch to the actively-maintained fork at https://github.com/danielroe/beasties
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -6115,6 +6122,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
postcss-media-query-parser@0.2.3:
|
||||||
|
resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==}
|
||||||
|
|
||||||
postcss-modules-extract-imports@3.1.0:
|
postcss-modules-extract-imports@3.1.0:
|
||||||
resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==}
|
resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==}
|
||||||
engines: {node: ^10 || ^12 || >= 14}
|
engines: {node: ^10 || ^12 || >= 14}
|
||||||
@@ -11236,6 +11246,16 @@ snapshots:
|
|||||||
|
|
||||||
crc-32@1.2.2: {}
|
crc-32@1.2.2: {}
|
||||||
|
|
||||||
|
critters@0.0.25:
|
||||||
|
dependencies:
|
||||||
|
chalk: 4.1.2
|
||||||
|
css-select: 5.2.2
|
||||||
|
dom-serializer: 2.0.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
htmlparser2: 8.0.2
|
||||||
|
postcss: 8.5.6
|
||||||
|
postcss-media-query-parser: 0.2.3
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@@ -14216,6 +14236,8 @@ snapshots:
|
|||||||
|
|
||||||
possible-typed-array-names@1.1.0: {}
|
possible-typed-array-names@1.1.0: {}
|
||||||
|
|
||||||
|
postcss-media-query-parser@0.2.3: {}
|
||||||
|
|
||||||
postcss-modules-extract-imports@3.1.0(postcss@8.5.6):
|
postcss-modules-extract-imports@3.1.0(postcss@8.5.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
|
|||||||
BIN
public/uploads/2026/01/1767353529807.jpg
Normal file
BIN
public/uploads/2026/01/1767353529807.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 316 KiB |
21
scripts/replace-motion.cjs
Normal file
21
scripts/replace-motion.cjs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const files = [
|
||||||
|
'/Users/marcmintel/Projects/klz-2026/components/Header.tsx',
|
||||||
|
'/Users/marcmintel/Projects/klz-2026/components/Scribble.tsx',
|
||||||
|
'/Users/marcmintel/Projects/klz-2026/components/Lightbox.tsx',
|
||||||
|
'/Users/marcmintel/Projects/klz-2026/components/record-mode/RecordModeOverlay.tsx',
|
||||||
|
'/Users/marcmintel/Projects/klz-2026/components/record-mode/PlaybackCursor.tsx'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
let content = fs.readFileSync(file, 'utf8');
|
||||||
|
content = content.replace(/import { motion } from 'framer-motion';/g, "import { m, LazyMotion, domAnimation } from 'framer-motion';");
|
||||||
|
content = content.replace(/import { motion, Variants } from 'framer-motion';/g, "import { m, LazyMotion, domAnimation, Variants } from 'framer-motion';");
|
||||||
|
content = content.replace(/import { motion, AnimatePresence } from 'framer-motion';/g, "import { m, LazyMotion, domAnimation, AnimatePresence } from 'framer-motion';");
|
||||||
|
|
||||||
|
content = content.replace(/<motion\./g, '<m.');
|
||||||
|
content = content.replace(/<\/motion\./g, '</m.');
|
||||||
|
|
||||||
|
fs.writeFileSync(file, content);
|
||||||
|
}
|
||||||
|
console.log('Replaced motion with m in ' + files.length + ' files');
|
||||||
@@ -1,19 +1,4 @@
|
|||||||
import * as Sentry from '@sentry/nextjs';
|
// Sentry initialization move to GlitchtipErrorReportingService to allow lazy-loading
|
||||||
|
// for PageSpeed 100 optimizations. This file is now empty to prevent the SDK
|
||||||
// We use a placeholder DSN on the client because the real DSN is injected
|
// from being included in the initial JS bundle.
|
||||||
// by our server-side relay at /errors/api/relay.
|
export {};
|
||||||
// This keeps our environment clean of NEXT_PUBLIC_ variables.
|
|
||||||
const CLIENT_DSN = 'https://public@errors.infra.mintel.me/1';
|
|
||||||
|
|
||||||
Sentry.init({
|
|
||||||
dsn: CLIENT_DSN,
|
|
||||||
// Relay events through our own server to hide the real DSN and bypass ad-blockers
|
|
||||||
tunnel: '/errors/api/relay',
|
|
||||||
|
|
||||||
// Enable even if no DSN is provided, because we have the tunnel
|
|
||||||
enabled: true,
|
|
||||||
|
|
||||||
tracesSampleRate: 0,
|
|
||||||
replaysOnErrorSampleRate: 1.0,
|
|
||||||
replaysSessionSampleRate: 0.1,
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -43,11 +43,11 @@
|
|||||||
--animate-slide-up: slide-up 0.6s ease-out;
|
--animate-slide-up: slide-up 0.6s ease-out;
|
||||||
--animate-slow-zoom: slow-zoom 20s linear infinite;
|
--animate-slow-zoom: slow-zoom 20s linear infinite;
|
||||||
--animate-reveal: reveal 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
--animate-reveal: reveal 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
--animate-slight-fade-in-from-bottom: slight-fade-in-from-bottom 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
--animate-slight-fade-in-from-bottom: slight-fade-in-from-bottom 0.8s
|
||||||
|
cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
--animate-gradient-x: gradient-x 15s ease infinite;
|
--animate-gradient-x: gradient-x 15s ease infinite;
|
||||||
|
|
||||||
@keyframes gradient-x {
|
@keyframes gradient-x {
|
||||||
|
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
background-position: 0% 50%;
|
background-position: 0% 50%;
|
||||||
@@ -135,10 +135,31 @@
|
|||||||
transform: translate(0, 0) scale(1);
|
transform: translate(0, 0) scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes spin-slow {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flow {
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes solar-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
fill-opacity: 0.2;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
fill-opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|
||||||
.bg-primary a,
|
.bg-primary a,
|
||||||
.bg-primary-dark a {
|
.bg-primary-dark a {
|
||||||
@apply text-white/90 hover:text-white transition-colors;
|
@apply text-white/90 hover:text-white transition-colors;
|
||||||
|
|||||||
Reference in New Issue
Block a user