perf: deep react code splitting, next-intl payload scoping, and SVG hardware acceleration for PageSpeed 100
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 2m1s
Build & Deploy / 🏗️ Build (push) Successful in 7m43s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Smoke Test (push) Successful in 1m10s
Build & Deploy / ⚡ Lighthouse (push) Successful in 3m20s
Build & Deploy / 🔔 Notify (push) Successful in 2s

This commit is contained in:
2026-02-20 11:53:42 +01:00
parent 7542f42568
commit 16d06d3275
15 changed files with 1234 additions and 1121 deletions

View File

@@ -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 />

View File

@@ -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'));

View File

@@ -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>
</> </>
); );
} }

View File

@@ -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,
); );
} }

View File

@@ -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>
); );
} }

View File

@@ -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 />

View File

@@ -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">&rarr;</span> <span className="transition-transform group-hover/btn:translate-x-1">&rarr;</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>
); );
} }

View File

@@ -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>
); );
})} })}

View File

@@ -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>
); );
} }

View File

@@ -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>
); );
} }

View File

@@ -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();
});
} }
} }

View File

@@ -10,6 +10,9 @@ 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: {
optimizePackageImports: ['lucide-react', 'framer-motion', '@/components/ui'],
},
productionBrowserSourceMaps: false, productionBrowserSourceMaps: false,
logging: { logging: {
fetches: { fetches: {

View 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');

View File

@@ -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,
});

View File

@@ -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;