Compare commits
5 Commits
v2.2.0-rc.
...
v2.2.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 089ce13c59 | |||
| a2cf9791ae | |||
| aa4e3aab4f | |||
| ce719a1d70 | |||
| bd2f92125b |
1
.env
1
.env
@@ -7,6 +7,7 @@ SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@errors.infra.mintel.me/1
|
||||
LOG_LEVEL=info
|
||||
NEXT_PUBLIC_FEEDBACK_ENABLED=false
|
||||
NEXT_PUBLIC_RECORD_MODE_ENABLED=false
|
||||
NPM_TOKEN=263e7f75d8ada27f3a2e71fd6bd9d95298d48a4d
|
||||
|
||||
# SMTP Configuration
|
||||
MAIL_HOST=smtp.eu.mailgun.org
|
||||
|
||||
4
.npmrc
4
.npmrc
@@ -1,2 +1,2 @@
|
||||
@mintel:registry=https://npm.infra.mintel.me/
|
||||
//npm.infra.mintel.me/:_authToken=${NPM_TOKEN}
|
||||
@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/
|
||||
//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=${NPM_TOKEN}
|
||||
|
||||
@@ -322,6 +322,8 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[DEBUG PAGE] Slug: ${productSlug}, children count: ${descriptionChildren.length}`);
|
||||
|
||||
const descriptionContent = {
|
||||
root: {
|
||||
...product.content.root,
|
||||
@@ -353,29 +355,31 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
categories={product.frontmatter.categories}
|
||||
sku={product.frontmatter.sku}
|
||||
/>
|
||||
<section className="relative pt-40 pb-24 overflow-hidden bg-primary-dark">
|
||||
<section className="relative pt-28 md:pt-40 pb-12 md:pb-24 overflow-hidden bg-primary-dark">
|
||||
{/* Background Decorative Elements */}
|
||||
<div className="absolute top-0 right-0 w-1/2 h-full bg-gradient-to-l from-accent/5 to-transparent pointer-events-none" />
|
||||
<div className="absolute -top-24 -right-24 w-96 h-96 bg-accent/10 rounded-full blur-3xl pointer-events-none" />
|
||||
|
||||
<Container className="relative z-10">
|
||||
<div className="max-w-4xl animate-slide-up">
|
||||
<nav className="flex items-center mb-12 text-white/40 text-[10px] font-black uppercase tracking-[0.2em]">
|
||||
<nav className="flex flex-wrap items-center gap-y-1 mb-6 md:mb-12 text-white/40 text-[10px] font-black uppercase tracking-[0.2em]">
|
||||
<Link
|
||||
href={`/${locale}/${productsSlug}`}
|
||||
className="hover:text-accent transition-colors"
|
||||
className="hover:text-accent transition-colors shrink-0"
|
||||
>
|
||||
{t.has('breadcrumb') ? t('breadcrumb') : 'Products'}
|
||||
</Link>
|
||||
<span className="mx-4 opacity-20">/</span>
|
||||
<span className="mx-2 md:mx-4 opacity-20">/</span>
|
||||
<Link
|
||||
href={`/${locale}/${productsSlug}/${categorySlug}`}
|
||||
className="hover:text-accent transition-colors"
|
||||
className="hover:text-accent transition-colors shrink-0 max-w-[140px] truncate"
|
||||
>
|
||||
{categoryTitle}
|
||||
</Link>
|
||||
<span className="mx-4 opacity-20">/</span>
|
||||
<span className="text-white/90">{product.frontmatter.title}</span>
|
||||
<span className="mx-2 md:mx-4 opacity-20">/</span>
|
||||
<span className="text-white/90 truncate max-w-[140px] md:max-w-none">
|
||||
{product.frontmatter.title}
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-12">
|
||||
@@ -386,7 +390,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
{t('englishVersion')}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-3 mb-8">
|
||||
<div className="flex flex-wrap gap-2 mb-4 md:mb-8">
|
||||
{product.frontmatter.categories.map((cat, idx) => (
|
||||
<Badge
|
||||
key={idx}
|
||||
@@ -397,10 +401,10 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<Heading level={1} className="text-white mb-8 uppercase">
|
||||
<Heading level={1} className="text-white mb-4 md:mb-8 uppercase">
|
||||
{product.frontmatter.title}
|
||||
</Heading>
|
||||
<p className="text-xl md:text-2xl text-white/60 max-w-2xl leading-relaxed font-medium">
|
||||
<p className="text-base md:text-xl lg:text-2xl text-white/60 max-w-2xl leading-relaxed font-medium">
|
||||
{product.frontmatter.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -414,11 +418,11 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
{/* Large Product Image Section */}
|
||||
{product.frontmatter.images && product.frontmatter.images.length > 0 && (
|
||||
<div
|
||||
className="relative -mt-32 mb-32 animate-slide-up"
|
||||
className="relative md:-mt-32 mb-8 md:mb-32 animate-slide-up"
|
||||
style={{ animationDelay: '200ms' }}
|
||||
>
|
||||
<div className="bg-white shadow-[0_32px_64px_-12px_rgba(0,0,0,0.1)] rounded-[48px] border border-neutral-dark/5 overflow-hidden p-12 md:p-20 lg:p-24">
|
||||
<div className="relative w-full aspect-[21/9]">
|
||||
<div className="bg-white shadow-[0_32px_64px_-12px_rgba(0,0,0,0.1)] rounded-[24px] md:rounded-[48px] border border-neutral-dark/5 overflow-hidden p-6 md:p-20 lg:p-24">
|
||||
<div className="relative w-full aspect-[4/3] md:aspect-[21/9]">
|
||||
<Image
|
||||
src={product.frontmatter.images[0]}
|
||||
alt={product.frontmatter.title}
|
||||
@@ -453,10 +457,10 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-20">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-20">
|
||||
{/* Description Area Next to Sidebar */}
|
||||
<div className="lg:col-span-8">
|
||||
<div className="max-w-none prose prose-primary prose-lg md:prose-xl mb-16 pb-16 border-b border-neutral-dark/5">
|
||||
<div className="max-w-none prose prose-primary prose-base md:prose-lg xl:prose-xl mb-8 md:mb-16 pb-8 md:pb-16 border-b border-neutral-dark/5">
|
||||
{descriptionChildren.length > 0 ? (
|
||||
<PayloadRichText data={descriptionContent} />
|
||||
) : product.frontmatter.description ? (
|
||||
@@ -464,6 +468,12 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
{product.frontmatter.description}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{product.application?.root?.children?.length > 0 && (
|
||||
<div className="mt-12">
|
||||
<PayloadRichText data={product.application} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -472,7 +482,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
</div>
|
||||
|
||||
{/* Full-width Technical Data Below */}
|
||||
<div className="mt-16 pt-16 border-t-0">
|
||||
<div className="mt-8 md:mt-16 pt-8 md:pt-16 border-t-0">
|
||||
<div className="max-w-none prose prose-primary prose-lg md:prose-xl">
|
||||
<PayloadRichText data={technicalContent} />
|
||||
</div>
|
||||
@@ -530,7 +540,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
</div>
|
||||
|
||||
{/* Related Products Section */}
|
||||
<div className="mt-16 pt-16 border-t border-neutral-dark/5">
|
||||
<div className="mt-10 md:mt-16 pt-10 md:pt-16 border-t border-neutral-dark/5">
|
||||
<RelatedProducts
|
||||
currentSlug={productSlug}
|
||||
categories={product.frontmatter.categories}
|
||||
|
||||
@@ -15,14 +15,14 @@ export default function Footer() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="bg-primary text-white py-24 relative overflow-hidden content-visibility-auto">
|
||||
<footer className="bg-primary text-white py-14 md:py-24 relative overflow-hidden content-visibility-auto">
|
||||
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
||||
|
||||
<Container>
|
||||
<h2 className="sr-only">Footer Navigation</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-12 gap-16 mb-20">
|
||||
{/* Brand Column */}
|
||||
<div className="lg:col-span-4 space-y-8">
|
||||
<div className="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-12 gap-10 md:gap-16 mb-12 md:mb-20">
|
||||
{/* Brand Column – full width on mobile */}
|
||||
<div className="col-span-2 md:col-span-2 lg:col-span-4 space-y-6 md:space-y-8">
|
||||
<Link
|
||||
href={`/${locale}`}
|
||||
className="inline-block group"
|
||||
@@ -67,9 +67,9 @@ export default function Footer() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Links Columns */}
|
||||
<div className="lg:col-span-2">
|
||||
<h3 className="text-accent font-bold uppercase tracking-widest text-xs md:text-sm mb-8">
|
||||
{/* Legal Column */}
|
||||
<div className="col-span-1 lg:col-span-2">
|
||||
<h3 className="text-accent font-bold uppercase tracking-widest text-xs mb-5 md:mb-8">
|
||||
{t('legal')}
|
||||
</h3>
|
||||
<ul className="space-y-4 text-white/70 list-none m-0 p-0">
|
||||
@@ -121,8 +121,9 @@ export default function Footer() {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2">
|
||||
<h3 className="text-accent font-bold uppercase tracking-widest text-xs md:text-sm mb-8">
|
||||
{/* Company Column */}
|
||||
<div className="col-span-1 lg:col-span-2">
|
||||
<h3 className="text-accent font-bold uppercase tracking-widest text-xs mb-5 md:mb-8">
|
||||
{t('company')}
|
||||
</h3>
|
||||
<ul className="space-y-4 text-white/70 list-none m-0 p-0">
|
||||
@@ -189,9 +190,9 @@ export default function Footer() {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Recent Posts Column */}
|
||||
<div className="lg:col-span-4">
|
||||
<h3 className="text-accent font-bold uppercase tracking-widest text-xs md:text-sm mb-8">
|
||||
{/* Recent Posts Column – full width on mobile */}
|
||||
<div className="col-span-2 md:col-span-2 lg:col-span-4">
|
||||
<h3 className="text-accent font-bold uppercase tracking-widest text-xs mb-5 md:mb-8">
|
||||
{t('recentPosts')}
|
||||
</h3>
|
||||
<ul className="space-y-6 list-none m-0 p-0">
|
||||
@@ -242,7 +243,7 @@ export default function Footer() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-12 border-t border-white/10 flex flex-col md:flex-row justify-between items-center gap-8 text-white/70 text-xs md:text-sm font-medium">
|
||||
<div className="pt-8 md:pt-12 border-t border-white/10 flex flex-row justify-between items-center gap-4 text-white/70 text-xs md:text-sm font-medium">
|
||||
<p>{t('copyright', { year: currentYear })}</p>
|
||||
<div className="flex gap-8">
|
||||
<Link
|
||||
|
||||
@@ -141,7 +141,8 @@ export default function Header() {
|
||||
{
|
||||
'bg-primary/95 backdrop-blur-md md:bg-transparent py-3 md:py-8 shadow-2xl md:shadow-none':
|
||||
isHomePage && !isScrolled && !isMobileMenuOpen,
|
||||
'bg-primary py-3 md:py-4 shadow-2xl': !isHomePage || isScrolled || isMobileMenuOpen,
|
||||
'bg-primary/90 backdrop-blur-md py-3 md:py-4 shadow-2xl':
|
||||
!isHomePage || isScrolled || isMobileMenuOpen,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -152,9 +153,7 @@ export default function Header() {
|
||||
<>
|
||||
<header className={headerClass} style={{ animationDuration: '800ms' }}>
|
||||
<div className="container mx-auto px-4 md:px-12 lg:px-16 max-w-7xl flex items-center justify-between">
|
||||
<div
|
||||
className="flex-shrink-0 group touch-target fill-mode-both"
|
||||
>
|
||||
<div className="flex-shrink-0 group touch-target fill-mode-both">
|
||||
<Link
|
||||
href={`/${currentLocale}`}
|
||||
onClick={() =>
|
||||
@@ -336,115 +335,138 @@ export default function Header() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Mobile Menu Overlay */}
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-0 bg-primary z-[60] lg:hidden transition-all duration-500 ease-in-out flex flex-col',
|
||||
isMobileMenuOpen
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 -translate-y-full pointer-events-none',
|
||||
)}
|
||||
id="mobile-menu"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t('menu')}
|
||||
ref={mobileMenuRef}
|
||||
inert={isMobileMenuOpen ? undefined : true}
|
||||
>
|
||||
<nav className="flex-grow flex flex-col justify-center items-center p-8 space-y-8">
|
||||
{menuItems.map((item, idx) => (
|
||||
<div
|
||||
key={item.href}
|
||||
className={cn(
|
||||
'transition-all duration-500 transform',
|
||||
isMobileMenuOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8',
|
||||
)}
|
||||
style={{ transitionDelay: `${isMobileMenuOpen ? 200 + idx * 80 : 0}ms` }}
|
||||
>
|
||||
<Link
|
||||
href={`/${currentLocale}${item.href === '/' ? '' : item.href}`}
|
||||
aria-current={
|
||||
(
|
||||
item.href === '/'
|
||||
? pathname === `/${currentLocale}` || pathname === '/'
|
||||
: pathname.startsWith(`/${currentLocale}${item.href}`)
|
||||
)
|
||||
? 'page'
|
||||
: undefined
|
||||
}
|
||||
onClick={() => {
|
||||
setIsMobileMenuOpen(false);
|
||||
trackEvent(AnalyticsEvents.LINK_CLICK, {
|
||||
label: item.label,
|
||||
href: item.href,
|
||||
location: 'mobile_menu',
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
'text-2xl md:text-3xl font-extrabold text-white hover:text-accent transition-all transform block py-4',
|
||||
(item.href === '/'
|
||||
? pathname === `/${currentLocale}` || pathname === '/'
|
||||
: pathname.startsWith(`/${currentLocale}${item.href}`)) && 'text-accent',
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Mobile Menu Overlay */}
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-0 bg-primary/95 backdrop-blur-3xl z-[60] lg:hidden transition-all duration-500 ease-in-out flex flex-col',
|
||||
isMobileMenuOpen
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 -translate-y-full pointer-events-none',
|
||||
)}
|
||||
id="mobile-menu"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t('menu')}
|
||||
ref={mobileMenuRef}
|
||||
inert={isMobileMenuOpen ? undefined : true}
|
||||
>
|
||||
{/* Close Button inside overlay */}
|
||||
<div className="flex justify-end p-6 pt-8">
|
||||
<button
|
||||
className="touch-target p-2 rounded-xl bg-white/10 border border-white/20 text-white hover:bg-white/20 transition-all duration-300"
|
||||
aria-label={t('toggleMenu')}
|
||||
onClick={() => {
|
||||
setIsMobileMenuOpen(false);
|
||||
trackEvent(AnalyticsEvents.BUTTON_CLICK, {
|
||||
type: 'mobile_menu',
|
||||
action: 'close',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<svg className="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<nav className="flex-grow flex flex-col justify-center items-center p-8 space-y-8">
|
||||
{menuItems.map((item, idx) => (
|
||||
<div
|
||||
key={item.href}
|
||||
className={cn(
|
||||
'pt-8 border-t border-white/10 w-full flex flex-col items-center space-y-8 transition-all duration-500',
|
||||
'transition-all duration-500 transform',
|
||||
isMobileMenuOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8',
|
||||
)}
|
||||
style={{ transitionDelay: isMobileMenuOpen ? '600ms' : '0ms' }}
|
||||
style={{ transitionDelay: `${isMobileMenuOpen ? 200 + idx * 80 : 0}ms` }}
|
||||
>
|
||||
<div className="flex items-center space-x-8 text-lg md:text-xl font-extrabold tracking-widest uppercase text-white">
|
||||
<div>
|
||||
<Link
|
||||
href={getPathForLocale('en')}
|
||||
className={`hover:text-accent transition-colors ${currentLocale === 'en' ? 'text-accent' : 'opacity-80'}`}
|
||||
>
|
||||
EN
|
||||
</Link>
|
||||
</div>
|
||||
<div className="w-px h-6 bg-white/30" />
|
||||
<div>
|
||||
<Link
|
||||
href={getPathForLocale('de')}
|
||||
className={`hover:text-accent transition-colors ${currentLocale === 'de' ? 'text-accent' : 'opacity-80'}`}
|
||||
>
|
||||
DE
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href={`/${currentLocale}${item.href === '/' ? '' : item.href}`}
|
||||
aria-current={
|
||||
(
|
||||
item.href === '/'
|
||||
? pathname === `/${currentLocale}` || pathname === '/'
|
||||
: pathname.startsWith(`/${currentLocale}${item.href}`)
|
||||
)
|
||||
? 'page'
|
||||
: undefined
|
||||
}
|
||||
onClick={() => {
|
||||
setIsMobileMenuOpen(false);
|
||||
trackEvent(AnalyticsEvents.LINK_CLICK, {
|
||||
label: item.label,
|
||||
href: item.href,
|
||||
location: 'mobile_menu',
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
'text-2xl md:text-3xl font-extrabold text-white hover:text-accent transition-all transform block py-4',
|
||||
(item.href === '/'
|
||||
? pathname === `/${currentLocale}` || pathname === '/'
|
||||
: pathname.startsWith(`/${currentLocale}${item.href}`)) && 'text-accent',
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="w-full max-w-xs">
|
||||
<Button
|
||||
href={`/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`}
|
||||
variant="accent"
|
||||
size="lg"
|
||||
className="w-full py-6 text-lg md:text-xl shadow-2xl hover:scale-105 transition-transform"
|
||||
<div
|
||||
className={cn(
|
||||
'pt-8 border-t border-white/10 w-full flex flex-col items-center space-y-8 transition-all duration-500',
|
||||
isMobileMenuOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8',
|
||||
)}
|
||||
style={{ transitionDelay: isMobileMenuOpen ? '600ms' : '0ms' }}
|
||||
>
|
||||
<div className="flex items-center space-x-8 text-lg md:text-xl font-extrabold tracking-widest uppercase text-white">
|
||||
<div>
|
||||
<Link
|
||||
href={getPathForLocale('en')}
|
||||
className={`hover:text-accent transition-colors ${currentLocale === 'en' ? 'text-accent' : 'opacity-80'}`}
|
||||
>
|
||||
{t('contact')}
|
||||
</Button>
|
||||
EN
|
||||
</Link>
|
||||
</div>
|
||||
<div className="w-px h-6 bg-white/30" />
|
||||
<div>
|
||||
<Link
|
||||
href={getPathForLocale('de')}
|
||||
className={`hover:text-accent transition-colors ${currentLocale === 'de' ? 'text-accent' : 'opacity-80'}`}
|
||||
>
|
||||
DE
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Branding */}
|
||||
<div
|
||||
className={cn(
|
||||
'p-12 flex justify-center transition-all duration-700',
|
||||
isMobileMenuOpen ? 'opacity-20 scale-100' : 'opacity-0 scale-75',
|
||||
)}
|
||||
style={{ transitionDelay: isMobileMenuOpen ? '800ms' : '0ms' }}
|
||||
>
|
||||
<Image src="/logo-white.svg" alt={t('home')} width={80} height={80} unoptimized />
|
||||
<div className="w-full max-w-xs">
|
||||
<Button
|
||||
href={`/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`}
|
||||
variant="accent"
|
||||
size="lg"
|
||||
className="w-full py-6 text-lg md:text-xl shadow-2xl hover:scale-105 transition-transform"
|
||||
>
|
||||
{t('contact')}
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
{/* Bottom Branding */}
|
||||
<div
|
||||
className={cn(
|
||||
'p-12 flex justify-center transition-all duration-700',
|
||||
isMobileMenuOpen ? 'opacity-20 scale-100' : 'opacity-0 scale-75',
|
||||
)}
|
||||
style={{ transitionDelay: isMobileMenuOpen ? '800ms' : '0ms' }}
|
||||
>
|
||||
<Image src="/logo-white.svg" alt={t('home')} width={80} height={80} unoptimized />
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,95 +39,17 @@ import CTA from '@/components/home/CTA';
|
||||
const jsxConverters: JSXConverters = {
|
||||
...defaultJSXConverters,
|
||||
// Let the default converters handle text nodes to preserve valid formatting
|
||||
// If the text node contains raw HTML (from messy migrations), render it as HTML instead of escaping it
|
||||
text: ({ node }: any) => {
|
||||
const text = node.text;
|
||||
// Handle markdown-style lists embedded in text nodes from Markdown migration
|
||||
if (text && text.includes('\n- ')) {
|
||||
const parts = text.split('\n- ').filter((p: string) => p.trim() !== '');
|
||||
// If first part doesn't start with "- ", it's a prefix paragraph
|
||||
const startsWithDash = text.trimStart().startsWith('- ');
|
||||
const prefix = startsWithDash ? null : parts.shift();
|
||||
return (
|
||||
<div className="my-4">
|
||||
{prefix && (
|
||||
<div dangerouslySetInnerHTML={prefix.includes('<') ? { __html: prefix } : undefined}>
|
||||
{!prefix.includes('<') ? prefix : undefined}
|
||||
</div>
|
||||
)}
|
||||
<ul className="list-disc pl-6 my-4 space-y-2">
|
||||
{parts.map((item: string, i: number) => {
|
||||
const cleanItem = item.trim();
|
||||
if (cleanItem.includes('<')) {
|
||||
return <li key={i} dangerouslySetInnerHTML={{ __html: cleanItem }} />;
|
||||
}
|
||||
return <li key={i}>{cleanItem}</li>;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (text && (text.includes('<') || text.includes('data-start'))) {
|
||||
return <span dangerouslySetInnerHTML={{ __html: text }} />;
|
||||
}
|
||||
|
||||
// Handle markdown-style links [text](url) from Markdown migration
|
||||
if (text && /\[([^\]]+)\]\(([^)]+)\)/.test(text)) {
|
||||
const parts: React.ReactNode[] = [];
|
||||
const remaining = text;
|
||||
let key = 0;
|
||||
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||
let match;
|
||||
let lastIndex = 0;
|
||||
while ((match = linkRegex.exec(remaining)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
parts.push(<span key={key++}>{remaining.slice(lastIndex, match.index)}</span>);
|
||||
}
|
||||
parts.push(
|
||||
<a
|
||||
key={key++}
|
||||
href={match[2]}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline decoration-primary/30 hover:decoration-primary transition-colors"
|
||||
>
|
||||
{match[1]}
|
||||
</a>,
|
||||
);
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
if (lastIndex < remaining.length) {
|
||||
parts.push(<span key={key++}>{remaining.slice(lastIndex)}</span>);
|
||||
}
|
||||
return <>{parts}</>;
|
||||
}
|
||||
|
||||
// Handle newlines in text nodes — convert to <br> for proper line breaks
|
||||
if (text && text.includes('\n')) {
|
||||
const lines = text.split('\n');
|
||||
return (
|
||||
<>
|
||||
{lines.map((line: string, i: number) => (
|
||||
<span key={i}>
|
||||
{line}
|
||||
{i < lines.length - 1 && <br />}
|
||||
</span>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.format === 1) return <strong key="bold">{text}</strong>;
|
||||
if (node.format === 2) return <em key="italic">{text}</em>;
|
||||
return <span key="text">{text}</span>;
|
||||
},
|
||||
// Use div instead of p for paragraphs to allow nested block elements (like the lists above)
|
||||
paragraph: ({ children }: any) => (
|
||||
<div className="mb-6 leading-relaxed text-text-secondary">{children}</div>
|
||||
),
|
||||
paragraph: ({ node, nodesToJSX }: any) => {
|
||||
return (
|
||||
<div className="mb-6 leading-relaxed text-text-secondary">
|
||||
{nodesToJSX({ nodes: node.children })}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
// Scale headings to prevent multiple H1s (H1 -> H2, etc) and style natively
|
||||
heading: ({ node, children }: any) => {
|
||||
heading: ({ node, nodesToJSX }: any) => {
|
||||
const children = nodesToJSX({ nodes: node.children });
|
||||
const tag = node?.tag;
|
||||
if (tag === 'h1')
|
||||
return (
|
||||
@@ -151,7 +73,8 @@ const jsxConverters: JSXConverters = {
|
||||
);
|
||||
return <h6 className="text-base font-bold mt-6 mb-4 text-text-primary">{children}</h6>;
|
||||
},
|
||||
list: ({ node, children }: any) => {
|
||||
list: ({ node, nodesToJSX }: any) => {
|
||||
const children = nodesToJSX({ nodes: node.children });
|
||||
if (node?.listType === 'number') {
|
||||
return (
|
||||
<ol className="list-decimal pl-6 my-6 space-y-2 text-text-secondary marker:text-primary marker:font-bold">
|
||||
@@ -168,7 +91,8 @@ const jsxConverters: JSXConverters = {
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
listitem: ({ node, children }: any) => {
|
||||
listitem: ({ node, nodesToJSX }: any) => {
|
||||
const children = nodesToJSX({ nodes: node.children });
|
||||
if (node?.checked != null) {
|
||||
return (
|
||||
<li className="flex items-center gap-3 mb-2 leading-relaxed">
|
||||
@@ -184,12 +108,16 @@ const jsxConverters: JSXConverters = {
|
||||
}
|
||||
return <li className="mb-2 leading-relaxed">{children}</li>;
|
||||
},
|
||||
quote: ({ children }: any) => (
|
||||
<blockquote className="border-l-4 border-primary bg-primary/5 rounded-r-2xl pl-6 py-4 my-8 italic text-text-secondary shadow-sm">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
link: ({ node, children }: any) => {
|
||||
quote: ({ node, nodesToJSX }: any) => {
|
||||
const children = nodesToJSX({ nodes: node.children });
|
||||
return (
|
||||
<blockquote className="border-l-4 border-primary bg-primary/5 rounded-r-2xl pl-6 py-4 my-8 italic text-text-secondary shadow-sm">
|
||||
{children}
|
||||
</blockquote>
|
||||
);
|
||||
},
|
||||
link: ({ node, nodesToJSX }: any) => {
|
||||
const children = nodesToJSX({ nodes: node.children });
|
||||
// Handling Payload CMS link nodes
|
||||
const href = node?.fields?.url || node?.url || '#';
|
||||
const newTab = node?.fields?.newTab || node?.newTab;
|
||||
@@ -1090,6 +1018,10 @@ export default function PayloadRichText({
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
if (data.root?.children?.length > 0) {
|
||||
console.log('[PayloadRichText DEBUG] received children', data.root.children.length);
|
||||
}
|
||||
|
||||
const dynamicConverters: JSXConverters = {
|
||||
...jsxConverters,
|
||||
blocks: {
|
||||
|
||||
@@ -38,14 +38,14 @@ export default function ProductTechnicalData({ data }: ProductTechnicalDataProps
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-16">
|
||||
<div className="space-y-8 md:space-y-16">
|
||||
{technicalItems.length > 0 && (
|
||||
<div className="bg-white p-8 md:p-12 rounded-[32px] shadow-sm border border-neutral-dark/5">
|
||||
<div className="bg-white p-5 md:p-12 rounded-[20px] md:rounded-[32px] shadow-sm border border-neutral-dark/5">
|
||||
<h3 className="text-2xl font-bold text-primary mb-8 flex items-center gap-3">
|
||||
<div className="w-2 h-8 bg-accent rounded-full" />
|
||||
General Data
|
||||
</h3>
|
||||
<dl className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-12 gap-y-8">
|
||||
<dl className="grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-6 md:gap-x-12 md:gap-y-8">
|
||||
{technicalItems.map((item, idx) => (
|
||||
<div key={idx} className="flex flex-col group">
|
||||
<dt className="text-sm font-bold uppercase tracking-widest text-primary/40 mb-2 group-hover:text-accent transition-colors">
|
||||
@@ -72,7 +72,7 @@ export default function ProductTechnicalData({ data }: ProductTechnicalDataProps
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-white p-8 md:p-12 rounded-[32px] shadow-sm border border-neutral-dark/5 overflow-hidden"
|
||||
className="bg-white p-5 md:p-12 rounded-[20px] md:rounded-[32px] shadow-sm border border-neutral-dark/5 overflow-hidden"
|
||||
>
|
||||
<h3 className="text-2xl font-bold text-primary mb-8 flex items-center gap-3">
|
||||
<div className="w-2 h-8 bg-accent rounded-full" />
|
||||
@@ -83,7 +83,7 @@ export default function ProductTechnicalData({ data }: ProductTechnicalDataProps
|
||||
</h3>
|
||||
|
||||
{table.metaItems.length > 0 && (
|
||||
<dl className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8 mb-12 bg-neutral-light/50 p-8 rounded-2xl border border-neutral-dark/5">
|
||||
<dl className="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 md:gap-8 mb-6 md:mb-12 bg-neutral-light/50 p-4 md:p-8 rounded-xl md:rounded-2xl border border-neutral-dark/5">
|
||||
{table.metaItems.map((item, mIdx) => (
|
||||
<div key={mIdx}>
|
||||
<dt className="text-[10px] font-black uppercase tracking-[0.2em] text-primary/40 mb-1">
|
||||
@@ -98,9 +98,11 @@ export default function ProductTechnicalData({ data }: ProductTechnicalDataProps
|
||||
)}
|
||||
|
||||
<div className="relative">
|
||||
{/* Scroll hint gradient on right edge for mobile */}
|
||||
<div className="pointer-events-none absolute right-0 top-0 h-full w-8 bg-gradient-to-l from-white to-transparent z-20 md:hidden" />
|
||||
<div
|
||||
id={`voltage-table-${idx}`}
|
||||
className={`overflow-x-auto -mx-8 md:-mx-12 px-8 md:px-12 transition-all duration-500 ease-in-out ${
|
||||
className={`overflow-x-auto -mx-5 md:-mx-12 px-5 md:px-12 transition-all duration-500 ease-in-out ${
|
||||
!isExpanded && hasManyRows ? 'max-h-[400px] overflow-y-hidden' : 'max-h-[none]'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -29,7 +29,7 @@ services:
|
||||
NEXT_TELEMETRY_DISABLED: "1"
|
||||
POSTGRES_URI: postgres://${PAYLOAD_DB_USER:-payload}:${PAYLOAD_DB_PASSWORD:-120in09oenaoinsd9iaidon}@klz-db:5432/${PAYLOAD_DB_NAME:-payload}
|
||||
PAYLOAD_SECRET: ${PAYLOAD_SECRET:-fallback-secret-for-dev}
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||
UV_THREADPOOL_SIZE: "4"
|
||||
NPM_TOKEN: ${NPM_TOKEN:-}
|
||||
CI: "true"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Sentry initialization move to GlitchtipErrorReportingService to allow lazy-loading
|
||||
// for PageSpeed 100 optimizations. This file is now empty to prevent the SDK
|
||||
// from being included in the initial JS bundle.
|
||||
export {};
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
|
||||
@@ -18,6 +18,7 @@ export interface ProductData {
|
||||
slug: string;
|
||||
frontmatter: ProductFrontmatter;
|
||||
content: any; // Lexical AST from Payload
|
||||
application?: any; // Lexical AST for Application field
|
||||
}
|
||||
|
||||
export async function getProductMetadata(
|
||||
@@ -113,6 +114,7 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
|
||||
: 50,
|
||||
},
|
||||
content: doc.content,
|
||||
application: doc.application,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -195,6 +197,7 @@ export async function getAllProducts(locale: string): Promise<ProductData[]> {
|
||||
: 50,
|
||||
},
|
||||
content: null,
|
||||
application: null,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -17,6 +17,7 @@ const nextConfig = {
|
||||
workerThreads: false,
|
||||
},
|
||||
reactStrictMode: false,
|
||||
swcMinify: true,
|
||||
productionBrowserSourceMaps: false,
|
||||
logging: {
|
||||
fetches: {
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -1692,7 +1692,7 @@ packages:
|
||||
resolution: {integrity: sha512-RraNY9SCcx4KZV0Dh6BEW6XEW2swkqYca74pkFFRw6hHItSHiy+O/xMnpbofjYbzXj0tSpBGthUF1hHTsr3vIQ==}
|
||||
|
||||
'@mintel/eslint-config@1.8.21':
|
||||
resolution: {integrity: sha512-PsPxQk3fsUGLwQCVvHaiNNt0WcjwU/eU9xMxEGGm4SCcPw/ED4UZbaCEYwR78lp9BBGAKSqTFMWBhXfY4PjU8g==}
|
||||
resolution: {integrity: sha512-GH5tm1y89AhD+Lxf95BGCOdy7Nv1OPNLWrUpaTR6jsuKfH2dm9fU66LF7sDH5THmrkfAZ8zSzHJsKPjintv3IA==}
|
||||
|
||||
'@mintel/mail@1.8.21':
|
||||
resolution: {integrity: sha512-leZV9gINmxD4eVJ3Ij9KdrQoyib67NVHgL/93J7KcWSUWKbr2HVuKUBpiWImeeEZn3JO0f7JwRbVUzXPBRVeQA==}
|
||||
@@ -1701,10 +1701,10 @@ packages:
|
||||
react-dom: ^19.0.0
|
||||
|
||||
'@mintel/next-config@1.8.21':
|
||||
resolution: {integrity: sha512-Nwnp32h+eAjZwY9YHXHo2eIWkGrNWAqF6vT8RvyeehU1uJtoajrEpBIQPAf5dWmWSWkIdPSu9vlzEUORu39pBA==}
|
||||
resolution: {integrity: sha512-K4jb9Glf84a212BRZ/zmOUueBphmsikvStFCuDc5lxyFT+Hkj4w8ChmtI7gaUxHMrftooduGPXJ1+NFpKkvc/Q==}
|
||||
|
||||
'@mintel/next-feedback@1.8.21':
|
||||
resolution: {integrity: sha512-7WUpX/GMUBO+DYrnCm1Xb3mRQAaWDDaA1DgwavlV2m0lYiwqlPsLGafsBOY9MdGrTFxp2oFuz8lUK8/fkB2/SQ==}
|
||||
resolution: {integrity: sha512-n2KzGDbOvAskuzjbt8h5EOMSEnISxHrsXxJwDdMxCXEgmzfJSvWpP2mAqb684dimOwo1UWHE6DMSAFc1FXeYwg==}
|
||||
peerDependencies:
|
||||
react: ^19.0.0
|
||||
react-dom: ^19.0.0
|
||||
@@ -1713,7 +1713,7 @@ packages:
|
||||
resolution: {integrity: sha512-sr0yDtySGou+3DNvrqY6HWSHCiVIc8nnoRbckyPMSE21AGxk2aJineXGy9BO9tulSBdhStm2SgdC7McMFTszug==}
|
||||
|
||||
'@mintel/tsconfig@1.8.21':
|
||||
resolution: {integrity: sha512-V5sY+sZlUv7i5OTqoLph+k7s0hMOzE8G7kB1snFGVuhE71zc8ooi+0WDeP++lwJz3xlFLhQTv/iRznfBnYOCew==}
|
||||
resolution: {integrity: sha512-ePBfBZiijyXKOS6nLIyxkg7QDZEEC1TugzNhmvwwpc0Yh7BmVHyNpvjg6zKsoGj2rok+9Kc8mLH1WihQIs8SKg==}
|
||||
|
||||
'@monaco-editor/loader@1.7.0':
|
||||
resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==}
|
||||
|
||||
@@ -66,16 +66,29 @@ async function main() {
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// 3. Inject Gatekeeper session bypassing auth screens
|
||||
console.log(`\n🛡️ Injecting Gatekeeper Session...`);
|
||||
await page.setCookie({
|
||||
name: 'klz_gatekeeper_session',
|
||||
value: gatekeeperPassword,
|
||||
domain: new URL(targetUrl).hostname,
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: targetUrl.startsWith('https://'),
|
||||
});
|
||||
// 3. Authenticate through Gatekeeper login form
|
||||
console.log(`\n🛡️ Authenticating through Gatekeeper...`);
|
||||
try {
|
||||
// Navigate to a protected page so Gatekeeper redirects us to the login screen
|
||||
await page.goto(contactUrl, { waitUntil: 'networkidle0', timeout: 30000 });
|
||||
|
||||
// Check if we landed on the Gatekeeper login page
|
||||
const isGatekeeperPage = await page.$('input[name="password"]');
|
||||
if (isGatekeeperPage) {
|
||||
await page.type('input[name="password"]', gatekeeperPassword);
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle0', timeout: 30000 }),
|
||||
page.click('button[type="submit"]'),
|
||||
]);
|
||||
console.log(`✅ Gatekeeper authentication successful!`);
|
||||
} else {
|
||||
console.log(`✅ Already authenticated (no Gatekeeper gate detected).`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`❌ Gatekeeper authentication failed: ${err.message}`);
|
||||
await browser.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let hasErrors = false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user