fix: product texts, mobile nav background, mobile product page layout
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 2m20s
Build & Deploy / 🏗️ Build (push) Successful in 4m22s
Build & Deploy / 🚀 Deploy (push) Successful in 23s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 5m16s
Build & Deploy / 🔔 Notify (push) Successful in 1s
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 2m20s
Build & Deploy / 🏗️ Build (push) Successful in 4m22s
Build & Deploy / 🚀 Deploy (push) Successful in 23s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 5m16s
Build & Deploy / 🔔 Notify (push) Successful in 1s
- Fix PayloadRichText: migrate custom JSX converters to Lexical v3 nodesToJSX API - paragraph, heading, list, listitem, quote, link converters now use nodesToJSX - Resolves missing product texts since PayloadCMS migration - Fix mobile navigation: move overlay outside <header> to prevent fixed-position clipping - Header transform/backdrop-filter was containing the fixed overlay - Use bg-primary/95 backdrop-blur-3xl for premium blue background - Fix product image mobile layout: use md:-mt-32 responsive prefix - Negative margin only applies on md+ to avoid overlap on mobile - Improve mobile product page UX: - Breadcrumbs: flex-wrap, truncate, reduced separator spacing - Hero: reduced top padding pt-28 on mobile - Product image card: 4/3 aspect ratio and smaller padding on mobile - Section spacing: use responsive md: prefixes throughout - Data tables: 2-col grid on mobile, smaller card padding/radius - Tables: add right-edge scroll hint gradient on mobile
This commit is contained in:
1
.env
1
.env
@@ -7,6 +7,7 @@ SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@errors.infra.mintel.me/1
|
|||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
NEXT_PUBLIC_FEEDBACK_ENABLED=false
|
NEXT_PUBLIC_FEEDBACK_ENABLED=false
|
||||||
NEXT_PUBLIC_RECORD_MODE_ENABLED=false
|
NEXT_PUBLIC_RECORD_MODE_ENABLED=false
|
||||||
|
NPM_TOKEN=263e7f75d8ada27f3a2e71fd6bd9d95298d48a4d
|
||||||
|
|
||||||
# SMTP Configuration
|
# SMTP Configuration
|
||||||
MAIL_HOST=smtp.eu.mailgun.org
|
MAIL_HOST=smtp.eu.mailgun.org
|
||||||
|
|||||||
@@ -322,6 +322,8 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[DEBUG PAGE] Slug: ${productSlug}, children count: ${descriptionChildren.length}`);
|
||||||
|
|
||||||
const descriptionContent = {
|
const descriptionContent = {
|
||||||
root: {
|
root: {
|
||||||
...product.content.root,
|
...product.content.root,
|
||||||
@@ -353,29 +355,31 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
categories={product.frontmatter.categories}
|
categories={product.frontmatter.categories}
|
||||||
sku={product.frontmatter.sku}
|
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 */}
|
{/* 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-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" />
|
<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">
|
<Container className="relative z-10">
|
||||||
<div className="max-w-4xl animate-slide-up">
|
<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
|
<Link
|
||||||
href={`/${locale}/${productsSlug}`}
|
href={`/${locale}/${productsSlug}`}
|
||||||
className="hover:text-accent transition-colors"
|
className="hover:text-accent transition-colors shrink-0"
|
||||||
>
|
>
|
||||||
{t.has('breadcrumb') ? t('breadcrumb') : 'Products'}
|
{t.has('breadcrumb') ? t('breadcrumb') : 'Products'}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="mx-4 opacity-20">/</span>
|
<span className="mx-2 md:mx-4 opacity-20">/</span>
|
||||||
<Link
|
<Link
|
||||||
href={`/${locale}/${productsSlug}/${categorySlug}`}
|
href={`/${locale}/${productsSlug}/${categorySlug}`}
|
||||||
className="hover:text-accent transition-colors"
|
className="hover:text-accent transition-colors shrink-0 max-w-[140px] truncate"
|
||||||
>
|
>
|
||||||
{categoryTitle}
|
{categoryTitle}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="mx-4 opacity-20">/</span>
|
<span className="mx-2 md:mx-4 opacity-20">/</span>
|
||||||
<span className="text-white/90">{product.frontmatter.title}</span>
|
<span className="text-white/90 truncate max-w-[140px] md:max-w-none">
|
||||||
|
{product.frontmatter.title}
|
||||||
|
</span>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-12">
|
<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')}
|
{t('englishVersion')}
|
||||||
</div>
|
</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) => (
|
{product.frontmatter.categories.map((cat, idx) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={idx}
|
key={idx}
|
||||||
@@ -397,10 +401,10 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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}
|
{product.frontmatter.title}
|
||||||
</Heading>
|
</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}
|
{product.frontmatter.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -414,11 +418,11 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
{/* Large Product Image Section */}
|
{/* Large Product Image Section */}
|
||||||
{product.frontmatter.images && product.frontmatter.images.length > 0 && (
|
{product.frontmatter.images && product.frontmatter.images.length > 0 && (
|
||||||
<div
|
<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' }}
|
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="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-[21/9]">
|
<div className="relative w-full aspect-[4/3] md:aspect-[21/9]">
|
||||||
<Image
|
<Image
|
||||||
src={product.frontmatter.images[0]}
|
src={product.frontmatter.images[0]}
|
||||||
alt={product.frontmatter.title}
|
alt={product.frontmatter.title}
|
||||||
@@ -453,10 +457,10 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
</div>
|
</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 */}
|
{/* Description Area Next to Sidebar */}
|
||||||
<div className="lg:col-span-8">
|
<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 ? (
|
{descriptionChildren.length > 0 ? (
|
||||||
<PayloadRichText data={descriptionContent} />
|
<PayloadRichText data={descriptionContent} />
|
||||||
) : product.frontmatter.description ? (
|
) : product.frontmatter.description ? (
|
||||||
@@ -464,6 +468,12 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
{product.frontmatter.description}
|
{product.frontmatter.description}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{product.application?.root?.children?.length > 0 && (
|
||||||
|
<div className="mt-12">
|
||||||
|
<PayloadRichText data={product.application} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -472,7 +482,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Full-width Technical Data Below */}
|
{/* 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">
|
<div className="max-w-none prose prose-primary prose-lg md:prose-xl">
|
||||||
<PayloadRichText data={technicalContent} />
|
<PayloadRichText data={technicalContent} />
|
||||||
</div>
|
</div>
|
||||||
@@ -530,7 +540,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Related Products Section */}
|
{/* 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
|
<RelatedProducts
|
||||||
currentSlug={productSlug}
|
currentSlug={productSlug}
|
||||||
categories={product.frontmatter.categories}
|
categories={product.frontmatter.categories}
|
||||||
|
|||||||
@@ -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':
|
'bg-primary/95 backdrop-blur-md md:bg-transparent py-3 md:py-8 shadow-2xl md:shadow-none':
|
||||||
isHomePage && !isScrolled && !isMobileMenuOpen,
|
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' }}>
|
<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="container mx-auto px-4 md:px-12 lg:px-16 max-w-7xl flex items-center justify-between">
|
||||||
<div
|
<div className="flex-shrink-0 group touch-target fill-mode-both">
|
||||||
className="flex-shrink-0 group touch-target fill-mode-both"
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
href={`/${currentLocale}`}
|
href={`/${currentLocale}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -336,115 +335,115 @@ export default function Header() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
{/* Mobile Menu Overlay */}
|
{/* Mobile Menu Overlay */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed inset-0 bg-primary z-[60] lg:hidden transition-all duration-500 ease-in-out flex flex-col',
|
'fixed inset-0 bg-primary/95 backdrop-blur-3xl z-[60] lg:hidden transition-all duration-500 ease-in-out flex flex-col',
|
||||||
isMobileMenuOpen
|
isMobileMenuOpen
|
||||||
? 'opacity-100 translate-y-0'
|
? 'opacity-100 translate-y-0'
|
||||||
: 'opacity-0 -translate-y-full pointer-events-none',
|
: 'opacity-0 -translate-y-full pointer-events-none',
|
||||||
)}
|
)}
|
||||||
id="mobile-menu"
|
id="mobile-menu"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label={t('menu')}
|
aria-label={t('menu')}
|
||||||
ref={mobileMenuRef}
|
ref={mobileMenuRef}
|
||||||
inert={isMobileMenuOpen ? undefined : true}
|
inert={isMobileMenuOpen ? undefined : true}
|
||||||
>
|
>
|
||||||
<nav className="flex-grow flex flex-col justify-center items-center p-8 space-y-8">
|
<nav className="flex-grow flex flex-col justify-center items-center p-8 space-y-8">
|
||||||
{menuItems.map((item, idx) => (
|
{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>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
key={item.href}
|
||||||
className={cn(
|
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',
|
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">
|
<Link
|
||||||
<div>
|
href={`/${currentLocale}${item.href === '/' ? '' : item.href}`}
|
||||||
<Link
|
aria-current={
|
||||||
href={getPathForLocale('en')}
|
(
|
||||||
className={`hover:text-accent transition-colors ${currentLocale === 'en' ? 'text-accent' : 'opacity-80'}`}
|
item.href === '/'
|
||||||
>
|
? pathname === `/${currentLocale}` || pathname === '/'
|
||||||
EN
|
: pathname.startsWith(`/${currentLocale}${item.href}`)
|
||||||
</Link>
|
)
|
||||||
</div>
|
? 'page'
|
||||||
<div className="w-px h-6 bg-white/30" />
|
: undefined
|
||||||
<div>
|
}
|
||||||
<Link
|
onClick={() => {
|
||||||
href={getPathForLocale('de')}
|
setIsMobileMenuOpen(false);
|
||||||
className={`hover:text-accent transition-colors ${currentLocale === 'de' ? 'text-accent' : 'opacity-80'}`}
|
trackEvent(AnalyticsEvents.LINK_CLICK, {
|
||||||
>
|
label: item.label,
|
||||||
DE
|
href: item.href,
|
||||||
</Link>
|
location: 'mobile_menu',
|
||||||
</div>
|
});
|
||||||
</div>
|
}}
|
||||||
|
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">
|
<div
|
||||||
<Button
|
className={cn(
|
||||||
href={`/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`}
|
'pt-8 border-t border-white/10 w-full flex flex-col items-center space-y-8 transition-all duration-500',
|
||||||
variant="accent"
|
isMobileMenuOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8',
|
||||||
size="lg"
|
)}
|
||||||
className="w-full py-6 text-lg md:text-xl shadow-2xl hover:scale-105 transition-transform"
|
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')}
|
EN
|
||||||
</Button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Branding */}
|
<div className="w-full max-w-xs">
|
||||||
<div
|
<Button
|
||||||
className={cn(
|
href={`/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`}
|
||||||
'p-12 flex justify-center transition-all duration-700',
|
variant="accent"
|
||||||
isMobileMenuOpen ? 'opacity-20 scale-100' : 'opacity-0 scale-75',
|
size="lg"
|
||||||
)}
|
className="w-full py-6 text-lg md:text-xl shadow-2xl hover:scale-105 transition-transform"
|
||||||
style={{ transitionDelay: isMobileMenuOpen ? '800ms' : '0ms' }}
|
>
|
||||||
>
|
{t('contact')}
|
||||||
<Image src="/logo-white.svg" alt={t('home')} width={80} height={80} unoptimized />
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</div>
|
||||||
</div>
|
|
||||||
</header>
|
{/* 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 = {
|
const jsxConverters: JSXConverters = {
|
||||||
...defaultJSXConverters,
|
...defaultJSXConverters,
|
||||||
// Let the default converters handle text nodes to preserve valid formatting
|
// 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)
|
// Use div instead of p for paragraphs to allow nested block elements (like the lists above)
|
||||||
paragraph: ({ children }: any) => (
|
paragraph: ({ node, nodesToJSX }: any) => {
|
||||||
<div className="mb-6 leading-relaxed text-text-secondary">{children}</div>
|
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
|
// 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;
|
const tag = node?.tag;
|
||||||
if (tag === 'h1')
|
if (tag === 'h1')
|
||||||
return (
|
return (
|
||||||
@@ -151,7 +73,8 @@ const jsxConverters: JSXConverters = {
|
|||||||
);
|
);
|
||||||
return <h6 className="text-base font-bold mt-6 mb-4 text-text-primary">{children}</h6>;
|
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') {
|
if (node?.listType === 'number') {
|
||||||
return (
|
return (
|
||||||
<ol className="list-decimal pl-6 my-6 space-y-2 text-text-secondary marker:text-primary marker:font-bold">
|
<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>
|
</ul>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
listitem: ({ node, children }: any) => {
|
listitem: ({ node, nodesToJSX }: any) => {
|
||||||
|
const children = nodesToJSX({ nodes: node.children });
|
||||||
if (node?.checked != null) {
|
if (node?.checked != null) {
|
||||||
return (
|
return (
|
||||||
<li className="flex items-center gap-3 mb-2 leading-relaxed">
|
<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>;
|
return <li className="mb-2 leading-relaxed">{children}</li>;
|
||||||
},
|
},
|
||||||
quote: ({ children }: any) => (
|
quote: ({ node, nodesToJSX }: 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">
|
const children = nodesToJSX({ nodes: node.children });
|
||||||
{children}
|
return (
|
||||||
</blockquote>
|
<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}
|
||||||
link: ({ node, children }: any) => {
|
</blockquote>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
link: ({ node, nodesToJSX }: any) => {
|
||||||
|
const children = nodesToJSX({ nodes: node.children });
|
||||||
// Handling Payload CMS link nodes
|
// Handling Payload CMS link nodes
|
||||||
const href = node?.fields?.url || node?.url || '#';
|
const href = node?.fields?.url || node?.url || '#';
|
||||||
const newTab = node?.fields?.newTab || node?.newTab;
|
const newTab = node?.fields?.newTab || node?.newTab;
|
||||||
@@ -1090,6 +1018,10 @@ export default function PayloadRichText({
|
|||||||
|
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
|
if (data.root?.children?.length > 0) {
|
||||||
|
console.log('[PayloadRichText DEBUG] received children', data.root.children.length);
|
||||||
|
}
|
||||||
|
|
||||||
const dynamicConverters: JSXConverters = {
|
const dynamicConverters: JSXConverters = {
|
||||||
...jsxConverters,
|
...jsxConverters,
|
||||||
blocks: {
|
blocks: {
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ export default function ProductTechnicalData({ data }: ProductTechnicalDataProps
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-16">
|
<div className="space-y-8 md:space-y-16">
|
||||||
{technicalItems.length > 0 && (
|
{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">
|
<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" />
|
<div className="w-2 h-8 bg-accent rounded-full" />
|
||||||
General Data
|
General Data
|
||||||
</h3>
|
</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) => (
|
{technicalItems.map((item, idx) => (
|
||||||
<div key={idx} className="flex flex-col group">
|
<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">
|
<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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
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">
|
<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" />
|
<div className="w-2 h-8 bg-accent rounded-full" />
|
||||||
@@ -83,7 +83,7 @@ export default function ProductTechnicalData({ data }: ProductTechnicalDataProps
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{table.metaItems.length > 0 && (
|
{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) => (
|
{table.metaItems.map((item, mIdx) => (
|
||||||
<div key={mIdx}>
|
<div key={mIdx}>
|
||||||
<dt className="text-[10px] font-black uppercase tracking-[0.2em] text-primary/40 mb-1">
|
<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">
|
<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
|
<div
|
||||||
id={`voltage-table-${idx}`}
|
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]'
|
!isExpanded && hasManyRows ? 'max-h-[400px] overflow-y-hidden' : 'max-h-[none]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ services:
|
|||||||
NEXT_TELEMETRY_DISABLED: "1"
|
NEXT_TELEMETRY_DISABLED: "1"
|
||||||
POSTGRES_URI: postgres://${PAYLOAD_DB_USER:-payload}:${PAYLOAD_DB_PASSWORD:-120in09oenaoinsd9iaidon}@klz-db:5432/${PAYLOAD_DB_NAME:-payload}
|
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}
|
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"
|
UV_THREADPOOL_SIZE: "4"
|
||||||
NPM_TOKEN: ${NPM_TOKEN:-}
|
NPM_TOKEN: ${NPM_TOKEN:-}
|
||||||
CI: "true"
|
CI: "true"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Sentry initialization move to GlitchtipErrorReportingService to allow lazy-loading
|
// Sentry initialization move to GlitchtipErrorReportingService to allow lazy-loading
|
||||||
// for PageSpeed 100 optimizations. This file is now empty to prevent the SDK
|
// for PageSpeed 100 optimizations. This file is now empty to prevent the SDK
|
||||||
// from being included in the initial JS bundle.
|
// 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;
|
slug: string;
|
||||||
frontmatter: ProductFrontmatter;
|
frontmatter: ProductFrontmatter;
|
||||||
content: any; // Lexical AST from Payload
|
content: any; // Lexical AST from Payload
|
||||||
|
application?: any; // Lexical AST for Application field
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProductMetadata(
|
export async function getProductMetadata(
|
||||||
@@ -113,6 +114,7 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
|
|||||||
: 50,
|
: 50,
|
||||||
},
|
},
|
||||||
content: doc.content,
|
content: doc.content,
|
||||||
|
application: doc.application,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +197,7 @@ export async function getAllProducts(locale: string): Promise<ProductData[]> {
|
|||||||
: 50,
|
: 50,
|
||||||
},
|
},
|
||||||
content: null,
|
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" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <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
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const nextConfig = {
|
|||||||
workerThreads: false,
|
workerThreads: false,
|
||||||
},
|
},
|
||||||
reactStrictMode: false,
|
reactStrictMode: false,
|
||||||
|
swcMinify: true,
|
||||||
productionBrowserSourceMaps: false,
|
productionBrowserSourceMaps: false,
|
||||||
logging: {
|
logging: {
|
||||||
fetches: {
|
fetches: {
|
||||||
|
|||||||
Reference in New Issue
Block a user