This commit is contained in:
2026-01-19 19:09:32 +01:00
parent e8b6b13a3b
commit 5c9b2e3f5a
5 changed files with 173 additions and 205 deletions

View File

@@ -2,7 +2,6 @@ import ProductSidebar from '@/components/ProductSidebar';
import ProductTabs from '@/components/ProductTabs';
import ProductTechnicalData from '@/components/ProductTechnicalData';
import RelatedProducts from '@/components/RelatedProducts';
import RequestQuoteForm from '@/components/RequestQuoteForm';
import { Badge, Container, Section } from '@/components/ui';
import { getDatasheetPath } from '@/lib/datasheets';
import { getAllProducts, getProductBySlug } from '@/lib/mdx';
@@ -66,33 +65,33 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
const components = {
ProductTechnicalData,
ProductTabs,
p: (props: any) => <p {...props} className="text-lg md:text-xl text-text-secondary leading-relaxed mb-6 font-medium block !mb-6" />,
p: (props: any) => <p {...props} className="text-lg md:text-xl text-text-secondary leading-relaxed mb-8 font-medium" />,
h2: (props: any) => (
<div className="relative mt-20 mb-10 block !mt-20 !mb-10">
<h2 {...props} className="text-4xl md:text-5xl font-black text-primary tracking-tighter uppercase mb-6 block !mb-6" />
<div className="relative mb-16">
<h2 {...props} className="text-4xl md:text-5xl font-black text-primary tracking-tighter uppercase mb-6" />
<div className="w-20 h-1.5 bg-accent rounded-full" />
</div>
),
h3: (props: any) => <h3 {...props} className="text-2xl md:text-3xl font-black text-primary mt-16 mb-6 tracking-tight uppercase block !mt-16 !mb-6" />,
ul: (props: any) => <ul {...props} className="list-none pl-0 mt-4 mb-8 space-y-4 block !mt-4 !mb-8" />,
h3: (props: any) => <h3 {...props} className="text-2xl md:text-3xl font-black text-primary mb-10 tracking-tight uppercase" />,
ul: (props: any) => <ul {...props} className="list-none pl-0 mb-10" />,
section: (props: any) => <div {...props} className="block" />,
li: (props: any) => (
<li className="flex items-start gap-4 group">
<li className="flex items-start gap-4 group mb-4 last:mb-0">
<div className="mt-2.5 w-2 h-2 rounded-full bg-accent flex-shrink-0 group-hover:scale-125 transition-transform" />
<span {...props} className="text-lg md:text-xl text-text-secondary leading-relaxed font-medium" />
</li>
),
strong: (props: any) => <strong {...props} className="font-black text-primary" />,
table: (props: any) => (
<div className="overflow-x-auto my-16 rounded-[32px] border border-neutral-dark/10 shadow-xl bg-white p-1">
<div className="overflow-x-auto my-20 rounded-[32px] border border-neutral-dark/10 shadow-xl bg-white p-1">
<table {...props} className="min-w-full divide-y divide-neutral-dark/10" />
</div>
),
th: (props: any) => <th {...props} className="px-8 py-6 bg-neutral-light/50 text-left text-[10px] font-black uppercase tracking-[0.25em] text-primary/60" />,
td: (props: any) => <td {...props} className="px-8 py-6 text-text-secondary border-t border-neutral-dark/5 text-lg md:text-xl font-medium" />,
hr: () => <hr className="my-20 border-t-2 border-neutral-dark/5" />,
hr: () => <hr className="my-24 border-t-2 border-neutral-dark/5" />,
blockquote: (props: any) => (
<div className="my-16 p-10 md:p-16 bg-primary-dark rounded-[40px] relative overflow-hidden group">
<div className="my-20 p-10 md:p-16 bg-primary-dark rounded-[40px] relative overflow-hidden group">
<div className="absolute top-0 right-0 w-64 h-64 bg-accent/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-3xl group-hover:bg-accent/20 transition-colors duration-700" />
<div className="relative z-10 italic text-2xl md:text-4xl text-white/90 leading-relaxed font-black tracking-tight" {...props} />
</div>
@@ -201,6 +200,30 @@ export default async function ProductPage({ params }: ProductPageProps) {
const categoryKey = categorySlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t(`categories.${categoryKey}.title`);
const sidebar = (
<ProductSidebar
productName={product.frontmatter.title}
productImage={product.frontmatter.images?.[0]}
datasheetPath={datasheetPath}
/>
);
const productComponents = {
...components,
ProductTabs: (props: any) => <ProductTabs {...props} sidebar={sidebar} />,
};
// Pre-process content to convert raw HTML tags to Markdown so they use our custom components
const processedContent = product.content
.replace(/<h2[^>]*>(.*?)<\/h2>/g, '\n## $1\n')
.replace(/<h3[^>]*>(.*?)<\/h3>/g, '\n### $1\n')
.replace(/<p[^>]*>(.*?)<\/p>/g, '\n$1\n')
.replace(/<ul[^>]*>(.*?)<\/ul>/gs, '\n$1\n')
.replace(/<li[^>]*>(.*?)<\/li>/g, '\n- $1\n')
.replace(/<strong[^>]*>(.*?)<\/strong>/g, '**$1**')
.replace(/<section[^>]*>/g, '')
.replace(/<\/section>/g, '');
return (
<div className="flex flex-col min-h-screen bg-white relative">
{/* Product Hero */}
@@ -278,84 +301,37 @@ export default async function ProductPage({ params }: ProductPageProps) {
)}
<div className="relative">
<div className="space-y-20">
{/* Sidebar on Mobile (Above Content) */}
<div className="w-full lg:hidden">
<div className="space-y-10">
{/* Request Quote Form */}
<div className="bg-white rounded-[40px] shadow-[0_32px_64px_-12px_rgba(0,0,0,0.08)] border border-neutral-dark/5 overflow-hidden">
<div className="bg-primary-dark p-8 md:p-10 text-white relative overflow-hidden">
<div className="absolute top-0 right-0 w-48 h-48 bg-accent/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-3xl" />
<h3 className="text-2xl md:text-3xl font-black mb-3 relative z-10 tracking-tight uppercase">{t('requestQuote')}</h3>
<p className="text-white/50 text-base relative z-10 leading-relaxed font-medium">{t('requestQuoteDesc')}</p>
</div>
<div className="p-8 md:p-10">
<RequestQuoteForm productName={product.frontmatter.title} />
</div>
</div>
{/* Datasheet Download */}
{datasheetPath && (
<a
href={datasheetPath}
target="_blank"
rel="noopener noreferrer"
className="block bg-neutral-light/30 rounded-[40px] border border-neutral-dark/5 overflow-hidden group hover:bg-white hover:shadow-2xl transition-all duration-700"
>
<div className="p-8 md:p-10 flex items-center gap-8">
<div className="w-20 h-20 rounded-3xl bg-white shadow-sm flex items-center justify-center flex-shrink-0 group-hover:bg-accent group-hover:text-white transition-all duration-700 text-accent">
<svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div className="flex-1">
<h3 className="text-xl font-black text-primary mb-2 uppercase tracking-tight">{t('downloadDatasheet')}</h3>
<p className="text-text-secondary text-sm font-medium leading-relaxed">{t('downloadDatasheetDesc')}</p>
</div>
</div>
</a>
)}
</div>
<div className="w-full">
{/* Main Content Area */}
<div className="max-w-none">
<MDXRemote source={processedContent} components={productComponents} />
</div>
<div className="w-full">
{/* Main Content Area */}
<div className="max-w-none [&_h3]:mt-16 [&_h3]:mb-6 [&_p]:mb-6 [&_ul]:mt-4 [&_ul]:mb-8 [&_li]:mb-4">
<MDXRemote source={product.content} components={components} />
</div>
{/* Structured Data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Product',
name: product.frontmatter.title,
description: product.frontmatter.description,
sku: product.frontmatter.sku,
image: product.frontmatter.images?.[0] ? `https://klz-cables.com${product.frontmatter.images[0]}` : undefined,
brand: {
'@type': 'Brand',
name: 'KLZ Cables',
},
offers: {
'@type': 'Offer',
availability: 'https://schema.org/InStock',
priceCurrency: 'EUR',
url: `https://klz-cables.com/${locale}/products/${slug.join('/')}`,
},
}),
}}
/>
</div>
{/* Structured Data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Product',
name: product.frontmatter.title,
description: product.frontmatter.description,
sku: product.frontmatter.sku,
image: product.frontmatter.images?.[0] ? `https://klz-cables.com${product.frontmatter.images[0]}` : undefined,
brand: {
'@type': 'Brand',
name: 'KLZ Cables',
},
offers: {
'@type': 'Offer',
availability: 'https://schema.org/InStock',
priceCurrency: 'EUR',
url: `https://klz-cables.com/${locale}/products/${slug.join('/')}`,
},
}),
}}
/>
</div>
<ProductSidebar
productName={product.frontmatter.title}
productImage={product.frontmatter.images?.[0]}
datasheetPath={datasheetPath}
/>
</div>
{/* Related Products Section */}

View File

@@ -1,123 +1,97 @@
'use client';
import { useEffect, useRef } from 'react';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import RequestQuoteForm from '@/components/RequestQuoteForm';
import Scribble from '@/components/Scribble';
import { cn } from '@/components/ui/utils';
interface ProductSidebarProps {
productName: string;
productImage?: string;
datasheetPath?: string | null;
className?: string;
}
export default function ProductSidebar({ productName, productImage, datasheetPath }: ProductSidebarProps) {
export default function ProductSidebar({ productName, productImage, datasheetPath, className }: ProductSidebarProps) {
const t = useTranslations('Products');
const sidebarRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleScroll = () => {
if (!sidebarRef.current) return;
const sidebar = sidebarRef.current;
const container = sidebar.parentElement;
if (!container) return;
const containerRect = container.getBoundingClientRect();
const sidebarHeight = sidebar.offsetHeight;
// Offset from top of viewport when sticky
const stickyOffset = 128; // 8rem = top-32
let translateY = 0;
// If the top of the container has scrolled past our sticky offset
if (containerRect.top < stickyOffset) {
translateY = stickyOffset - containerRect.top;
}
// Don't let it go past the bottom of the container
const maxTranslateY = containerRect.height - sidebarHeight;
if (translateY > maxTranslateY) {
translateY = maxTranslateY;
}
// Ensure translateY is never negative
if (translateY < 0) translateY = 0;
sidebar.style.transform = `translateY(${translateY}px)`;
};
let rafId: number;
const onScroll = () => {
rafId = requestAnimationFrame(handleScroll);
};
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', handleScroll);
// Initial call
handleScroll();
return () => {
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', handleScroll);
cancelAnimationFrame(rafId);
};
}, []);
return (
<div
ref={sidebarRef}
className="hidden lg:block absolute left-full ml-12 top-0 w-[350px] xl:w-[400px] z-30 will-change-transform"
>
<div className="space-y-6">
{/* Request Quote Form */}
<div className="bg-white rounded-[32px] shadow-2xl border border-neutral-dark/5 overflow-hidden">
<div className="bg-primary-dark p-6 text-white relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-accent/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl" />
{/* Product Thumbnail */}
{productImage && (
<div className="relative w-full aspect-[21/9] mb-4 rounded-2xl overflow-hidden bg-white/5 backdrop-blur-xl p-4 border border-white/10 z-10">
<div className={cn("flex flex-col gap-4 animate-slight-fade-in-from-bottom", className)}>
{/* Request Quote Form Card */}
<div className="bg-white rounded-3xl border border-neutral-medium shadow-sm transition-all duration-500 hover:shadow-2xl hover:-translate-y-1 overflow-hidden group/card">
<div className="bg-primary p-6 text-white relative overflow-hidden">
{/* Background Accent - Saturated Blue Glow */}
<div className="absolute top-0 right-0 w-40 h-40 bg-saturated/30 rounded-full -translate-y-1/2 translate-x-1/2 blur-[80px] pointer-events-none" />
{/* Product Thumbnail with Reflection */}
{productImage && (
<div className="relative w-full aspect-[16/10] mb-6 rounded-2xl overflow-hidden bg-white/5 backdrop-blur-md p-4 border border-white/10 z-10 group">
<div className="relative w-full h-full transition-transform duration-1000 ease-out group-hover:scale-105">
<Image
src={productImage}
alt=""
alt={productName}
fill
className="object-contain p-1"
className="object-contain p-2 drop-shadow-[0_20px_30px_rgba(0,0,0,0.4)]"
/>
</div>
)}
<h3 className="text-xl font-black mb-2 relative z-10 tracking-tight uppercase">{t('requestQuote')}</h3>
<p className="text-white/50 text-sm relative z-10 leading-relaxed font-medium">{t('requestQuoteDesc')}</p>
</div>
<div className="p-6">
<RequestQuoteForm productName={productName} />
</div>
</div>
{/* Datasheet Download */}
{datasheetPath && (
<a
href={datasheetPath}
target="_blank"
rel="noopener noreferrer"
className="block bg-white/90 backdrop-blur-md rounded-[32px] border border-neutral-dark/5 overflow-hidden group hover:bg-white hover:shadow-xl transition-all duration-500 shadow-lg"
>
<div className="p-6 flex items-center gap-6">
<div className="w-14 h-14 rounded-2xl bg-neutral-light shadow-sm flex items-center justify-center flex-shrink-0 group-hover:bg-accent group-hover:text-white transition-all duration-500 text-accent">
<svg className="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div className="flex-1">
<h3 className="text-lg font-black text-primary mb-1 uppercase tracking-tight">{t('downloadDatasheet')}</h3>
<p className="text-text-secondary text-xs font-medium leading-relaxed">{t('downloadDatasheetDesc')}</p>
{/* Subtle Reflection Overlay */}
<div className="absolute inset-0 bg-gradient-to-tr from-white/20 via-transparent to-transparent opacity-30 pointer-events-none" />
</div>
</div>
</a>
)}
)}
<div className="relative z-10">
<div className="inline-block relative mb-2">
<h3 className="text-xl font-heading font-black m-0 tracking-tighter uppercase leading-none">
{t('requestQuote')}
</h3>
<Scribble
variant="underline"
className="w-full h-3 -bottom-3 left-0 text-accent/80"
color="var(--color-accent)"
/>
</div>
<p className="text-white/60 text-xs m-0 mt-2 leading-relaxed font-medium max-w-[90%]">
{t('requestQuoteDesc')}
</p>
</div>
</div>
<div className="p-6 bg-neutral-light/50">
<RequestQuoteForm productName={productName} />
</div>
</div>
{/* Datasheet Download */}
{datasheetPath && (
<a
href={datasheetPath}
target="_blank"
rel="noopener noreferrer"
className="block bg-white rounded-2xl border border-neutral-medium overflow-hidden group transition-all duration-500 hover:shadow-xl hover:border-saturated/30 hover:-translate-y-0.5"
>
<div className="p-4 flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-neutral-medium/20 flex items-center justify-center flex-shrink-0 group-hover:bg-saturated group-hover:text-white transition-all duration-500 text-saturated border border-transparent group-hover:border-white/20">
<svg className="w-6 h-6 transition-transform duration-500 group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-heading font-black text-neutral-dark m-0 uppercase tracking-tighter leading-tight group-hover:text-saturated transition-colors duration-300">
{t('downloadDatasheet')}
</h3>
<p className="text-text-secondary text-[10px] m-0 mt-0.5 font-semibold leading-tight truncate uppercase tracking-widest opacity-60">
{t('downloadDatasheetDesc')}
</p>
</div>
<div className="text-neutral-dark/20 group-hover:text-saturated transition-all duration-500 transform group-hover:translate-x-1">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</a>
)}
</div>
);
}

View File

@@ -3,13 +3,29 @@ import React from 'react';
interface ProductTabsProps {
children: React.ReactNode;
technicalData?: React.ReactNode;
sidebar?: React.ReactNode;
}
export default function ProductTabs({ children, technicalData }: ProductTabsProps) {
export default function ProductTabs({ children, technicalData, sidebar }: ProductTabsProps) {
return (
<div className="space-y-24">
<div className="max-w-none">
{children}
<div className="flex flex-col lg:flex-row gap-12 items-start">
<div className="flex-1 min-w-0">
{sidebar && (
<div className="lg:hidden mb-12">
{sidebar}
</div>
)}
<div className="max-w-none">
{children}
</div>
</div>
{sidebar && (
<div className="hidden lg:block sticky top-32 w-[350px] xl:w-[400px] flex-shrink-0">
{sidebar}
</div>
)}
</div>
{technicalData && (

View File

@@ -31,19 +31,19 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
if (status === 'success') {
return (
<div className="bg-accent/5 border border-accent/20 text-primary-dark p-6 rounded-[32px] text-center animate-fade-in">
<div className="w-12 h-12 bg-accent rounded-full flex items-center justify-center mx-auto mb-4 shadow-lg shadow-accent/20">
<svg className="w-6 h-6 text-primary-dark" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div className="bg-accent/5 border border-accent/20 text-primary-dark p-4 rounded-xl text-center animate-fade-in !mt-0">
<div className="w-10 h-10 bg-accent rounded-full flex items-center justify-center mx-auto mb-3 shadow-lg shadow-accent/20">
<svg className="w-5 h-5 text-primary-dark" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
</div>
<h3 className="text-xl font-extrabold mb-2 tracking-tight">{t('successTitle')}</h3>
<p className="text-text-secondary text-sm leading-relaxed mb-6">
<h3 className="text-base font-extrabold mb-1 tracking-tight !mt-0">{t('successTitle')}</h3>
<p className="text-text-secondary text-xs leading-tight mb-4 !mt-0">
{t('successDesc', { productName })}
</p>
<button
onClick={() => setStatus('idle')}
className="inline-flex items-center text-[10px] font-bold uppercase tracking-[0.2em] text-primary hover:text-accent transition-colors group"
className="inline-flex items-center text-[9px] font-bold uppercase tracking-[0.2em] text-primary hover:text-accent transition-colors group"
>
<span className="border-b-2 border-primary/10 group-hover:border-accent transition-colors pb-1">
{t('sendAnother')}
@@ -54,9 +54,9 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-4">
<div className="space-y-1.5">
<form onSubmit={handleSubmit} className="space-y-3 !mt-0">
<div className="space-y-2 !mt-0">
<div className="space-y-1 !mt-0">
<Input
type="email"
id="email"
@@ -64,46 +64,48 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={t('email')}
className="h-9 text-xs !mt-0"
/>
</div>
<div className="space-y-1.5">
<div className="space-y-1 !mt-0">
<Textarea
id="request"
required
rows={4}
rows={3}
value={request}
onChange={(e) => setRequest(e.target.value)}
placeholder={t('message')}
className="text-xs !mt-0"
/>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2 !mt-0">
<Button
type="submit"
disabled={status === 'submitting'}
className="w-full py-3.5 rounded-xl flex items-center justify-center gap-2 group"
className="w-full py-2 rounded-lg flex items-center justify-center gap-2 group !mt-0"
>
{status === 'submitting' ? (
<>
<svg className="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<svg className="animate-spin h-3 w-3 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span className="text-sm">{t('submitting')}</span>
<span className="text-xs">{t('submitting')}</span>
</>
) : (
<>
<span className="text-sm">{t('submit')}</span>
<svg className="w-4 h-4 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<span className="text-xs">{t('submit')}</span>
<svg className="w-3 h-3 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</>
)}
</Button>
<p className="text-[8px] text-center text-text-secondary/40 uppercase tracking-[0.15em] font-medium px-2">
<p className="text-[7px] text-center text-text-secondary/40 uppercase tracking-[0.15em] font-medium px-2 !mt-1 !mb-0">
{t('privacyNote')}
</p>
</div>

File diff suppressed because one or more lines are too long