This commit is contained in:
@@ -1,135 +0,0 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getProductBySlug } from '@/lib/mdx';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export default async function Image({ params: { locale, slug } }: { params: { locale: string, slug: string[] } }) {
|
||||
const productSlug = slug[slug.length - 1];
|
||||
const product = await getProductBySlug(productSlug, locale);
|
||||
|
||||
if (!product) {
|
||||
return new ImageResponse(
|
||||
<div style={{ display: 'flex', width: '100%', height: '100%', backgroundColor: '#001a4d' }} />
|
||||
);
|
||||
}
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: 'white',
|
||||
padding: '60px',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* Left Side: Content */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', width: '55%' }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: 'bold',
|
||||
color: '#001a4d',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.2em',
|
||||
marginBottom: '20px',
|
||||
opacity: 0.6,
|
||||
}}
|
||||
>
|
||||
KLZ Cables | {product.frontmatter.categories[0]}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '72px',
|
||||
fontWeight: '900',
|
||||
color: '#001a4d',
|
||||
lineHeight: '1.1',
|
||||
marginBottom: '30px',
|
||||
}}
|
||||
>
|
||||
{product.frontmatter.title}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '28px',
|
||||
color: '#4a5568',
|
||||
lineHeight: '1.4',
|
||||
}}
|
||||
>
|
||||
{product.frontmatter.description}
|
||||
</div>
|
||||
|
||||
{/* Accent Line */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: '40px',
|
||||
width: '100px',
|
||||
height: '8px',
|
||||
backgroundColor: '#00ff99',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Side: Product Image */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '40%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{product.frontmatter.images?.[0] && (
|
||||
<img
|
||||
src={product.frontmatter.images[0].startsWith('http') ? product.frontmatter.images[0] : `https://klz-cables.com${product.frontmatter.images[0]}`}
|
||||
alt=""
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '80%',
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* Subtle shadow under product */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '10%',
|
||||
width: '60%',
|
||||
height: '20px',
|
||||
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||
borderRadius: '50%',
|
||||
filter: 'blur(10px)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Branding Corner */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '40px',
|
||||
right: '60px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold', color: '#001a4d' }}>KLZ</div>
|
||||
<div style={{ fontSize: '24px', fontWeight: 'normal', color: '#00ff99', marginLeft: '4px' }}>Cables</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,337 +0,0 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { MDXRemote } from 'next-mdx-remote/rsc';
|
||||
import { getProductBySlug, getAllProducts } from '@/lib/mdx';
|
||||
import ProductTechnicalData from '@/components/ProductTechnicalData';
|
||||
import ProductTabs from '@/components/ProductTabs';
|
||||
import RequestQuoteForm from '@/components/RequestQuoteForm';
|
||||
import RelatedProducts from '@/components/RelatedProducts';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { Section, Container, Heading, Badge, Button } from '@/components/ui';
|
||||
import Scribble from '@/components/Scribble';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
interface ProductPageProps {
|
||||
params: {
|
||||
locale: string;
|
||||
slug: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: ProductPageProps): Promise<Metadata> {
|
||||
const { locale, slug } = params;
|
||||
const productSlug = slug[slug.length - 1];
|
||||
const t = await getTranslations('Products');
|
||||
|
||||
// Check if it's a category page
|
||||
const categories = ['low-voltage-cables', 'medium-voltage-cables', 'high-voltage-cables', 'solar-cables'];
|
||||
if (categories.includes(productSlug)) {
|
||||
const categoryKey = productSlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
||||
const categoryTitle = t(`categories.${categoryKey}.title`);
|
||||
const categoryDesc = t(`categories.${categoryKey}.description`);
|
||||
|
||||
return {
|
||||
title: categoryTitle,
|
||||
description: categoryDesc,
|
||||
openGraph: {
|
||||
title: categoryTitle,
|
||||
description: categoryDesc,
|
||||
url: `https://klz-cables.com/${locale}/products/${productSlug}`,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const product = await getProductBySlug(productSlug, locale);
|
||||
if (!product) return {};
|
||||
|
||||
return {
|
||||
title: product.frontmatter.title,
|
||||
description: product.frontmatter.description,
|
||||
openGraph: {
|
||||
title: product.frontmatter.title,
|
||||
description: product.frontmatter.description,
|
||||
type: 'website',
|
||||
url: `https://klz-cables.com/${locale}/products/${slug.join('/')}`,
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: product.frontmatter.title,
|
||||
description: product.frontmatter.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const components = {
|
||||
ProductTechnicalData,
|
||||
ProductTabs,
|
||||
p: (props: any) => <p {...props} className="text-lg md:text-xl text-text-secondary leading-relaxed mb-8" />,
|
||||
h2: (props: any) => (
|
||||
<div className="relative mt-20 mb-10">
|
||||
<h2 {...props} className="text-3xl md:text-4xl lg:text-5xl font-extrabold text-primary tracking-tight" />
|
||||
<div className="absolute -bottom-4 left-0 w-24 h-1.5 bg-accent rounded-full" />
|
||||
</div>
|
||||
),
|
||||
h3: (props: any) => <h3 {...props} className="text-2xl md:text-3xl lg:text-4xl font-bold text-primary mt-16 mb-6 tracking-tight" />,
|
||||
ul: (props: any) => <ul {...props} className="list-disc pl-8 mb-10 space-y-4 text-text-secondary text-lg md:text-xl" />,
|
||||
li: (props: any) => <li {...props} className="pl-2 marker:text-accent marker:font-bold" />,
|
||||
table: (props: any) => (
|
||||
<div className="overflow-x-auto my-16 rounded-3xl 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-xs 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" />,
|
||||
hr: () => <hr className="my-20 border-t-2 border-neutral-dark/5" />,
|
||||
blockquote: (props: any) => (
|
||||
<div className="my-12 p-8 md:p-12 bg-primary-dark rounded-3xl relative overflow-hidden group">
|
||||
<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 group-hover:bg-accent/20 transition-colors duration-700" />
|
||||
<div className="relative z-10 italic text-2xl md:text-3xl text-white/90 leading-relaxed font-medium" {...props} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export default async function ProductPage({ params }: ProductPageProps) {
|
||||
const { locale, slug } = params;
|
||||
const productSlug = slug[slug.length - 1];
|
||||
const t = await getTranslations('Products');
|
||||
|
||||
// Check if it's a category page
|
||||
const categories = ['low-voltage-cables', 'medium-voltage-cables', 'high-voltage-cables', 'solar-cables'];
|
||||
if (categories.includes(productSlug)) {
|
||||
const allProducts = await getAllProducts(locale);
|
||||
const categoryKey = productSlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
||||
const categoryTitle = t(`categories.${categoryKey}.title`);
|
||||
|
||||
// Filter products for this category
|
||||
const filteredProducts = allProducts.filter(p =>
|
||||
p.frontmatter.categories.some(cat => cat.toLowerCase().replace(/\s+/g, '-') === productSlug)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-white">
|
||||
<section className="relative min-h-[50vh] flex items-center pt-32 pb-20 overflow-hidden bg-primary-dark">
|
||||
<Container className="relative z-10">
|
||||
<div className="max-w-4xl animate-slide-up">
|
||||
<nav className="flex items-center mb-8 text-white/40 text-sm font-bold uppercase tracking-widest">
|
||||
<Link href={`/${locale}/products`} className="hover:text-accent transition-colors">{t('title')}</Link>
|
||||
<span className="mx-3 opacity-30">/</span>
|
||||
<span className="text-white/90">{categoryTitle}</span>
|
||||
</nav>
|
||||
<h1 className="text-5xl md:text-7xl lg:text-8xl font-extrabold text-white mb-8 tracking-tight leading-[1.05]">
|
||||
{categoryTitle}
|
||||
</h1>
|
||||
<div className="h-1.5 w-24 bg-accent rounded-full" />
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
<Section className="bg-neutral-light relative z-20 -mt-12 rounded-t-[48px] md:rounded-t-[64px]">
|
||||
<Container>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{filteredProducts.map((product) => (
|
||||
<Link
|
||||
key={product.slug}
|
||||
href={`/${locale}/products/${productSlug}/${product.slug}`}
|
||||
className="group block bg-white rounded-[32px] overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-500 border border-neutral-dark/5"
|
||||
>
|
||||
<div className="aspect-[4/3] relative bg-neutral-light/30 p-12 overflow-hidden">
|
||||
{product.frontmatter.images?.[0] && (
|
||||
<>
|
||||
<Image
|
||||
src={product.frontmatter.images[0]}
|
||||
alt={product.frontmatter.title}
|
||||
fill
|
||||
className="object-contain p-8 transition-transform duration-700 group-hover:scale-110 z-10"
|
||||
/>
|
||||
{/* Subtle reflection/shadow effect */}
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 w-2/3 h-4 bg-black/5 blur-xl rounded-full" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-8 md:p-10">
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{product.frontmatter.categories.map((cat, i) => (
|
||||
<span key={i} className="text-[10px] font-bold uppercase tracking-widest text-primary/40">
|
||||
{cat}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-text-primary group-hover:text-primary transition-colors mb-4 leading-tight">
|
||||
{product.frontmatter.title}
|
||||
</h2>
|
||||
<p className="text-text-secondary line-clamp-2 text-base leading-relaxed mb-8">
|
||||
{product.frontmatter.description}
|
||||
</p>
|
||||
<div className="flex items-center text-primary font-bold group-hover:text-accent-dark transition-colors">
|
||||
<span className="border-b-2 border-primary/10 group-hover:border-accent-dark transition-colors pb-1">
|
||||
{t('details')}
|
||||
</span>
|
||||
<svg className="w-5 h-5 ml-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>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const product = await getProductBySlug(productSlug, locale);
|
||||
|
||||
if (!product) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const isFallback = (product.frontmatter as any).isFallback;
|
||||
const categorySlug = slug[0];
|
||||
const categoryKey = categorySlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
||||
const categoryTitle = t(`categories.${categoryKey}.title`);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-neutral-light">
|
||||
{/* Product Hero */}
|
||||
<section className="relative pt-40 pb-32 overflow-hidden bg-primary-dark">
|
||||
<Container className="relative z-10">
|
||||
<div className="max-w-5xl animate-slide-up">
|
||||
<nav className="flex items-center mb-8 text-white/40 text-sm font-bold uppercase tracking-widest">
|
||||
<Link href={`/${locale}/products`} className="hover:text-accent transition-colors">{t('title')}</Link>
|
||||
<span className="mx-3 opacity-30">/</span>
|
||||
<Link href={`/${locale}/products/${categorySlug}`} className="hover:text-accent transition-colors">{categoryTitle}</Link>
|
||||
<span className="mx-3 opacity-30">/</span>
|
||||
<span className="text-white/90">{product.frontmatter.title}</span>
|
||||
</nav>
|
||||
|
||||
<div className="flex flex-col lg:flex-row lg:items-end justify-between gap-12">
|
||||
<div className="flex-1">
|
||||
{isFallback && (
|
||||
<div className="mb-6 inline-flex items-center px-3 py-1 rounded-md bg-accent/10 border border-accent/20 text-accent text-[10px] font-bold uppercase tracking-widest">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent mr-2 animate-pulse" />
|
||||
{t('englishVersion')}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-3 mb-8">
|
||||
{product.frontmatter.categories.map((cat, idx) => (
|
||||
<Badge key={idx} variant="accent" className="bg-white/10 text-white/90 border-white/10 backdrop-blur-md px-4 py-1.5">
|
||||
{cat}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<h1 className="text-5xl md:text-7xl lg:text-8xl font-extrabold text-white mb-0 tracking-tight leading-[1.05]">
|
||||
{product.frontmatter.title}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
<Section className="bg-neutral-light relative z-20 -mt-16 pt-0">
|
||||
<Container>
|
||||
{/* Large Product Image Section */}
|
||||
{product.frontmatter.images && product.frontmatter.images.length > 0 && (
|
||||
<div className="relative -mt-24 mb-24 animate-slide-up" style={{ animationDelay: '200ms' }}>
|
||||
<div className="bg-white shadow-2xl border border-neutral-dark/5 overflow-hidden p-10 md:p-16 lg:p-20">
|
||||
<div className="relative w-full aspect-[21/9]">
|
||||
<Image
|
||||
src={product.frontmatter.images[0]}
|
||||
alt={product.frontmatter.title}
|
||||
fill
|
||||
className="object-contain transition-transform duration-1000 hover:scale-105"
|
||||
priority
|
||||
/>
|
||||
{/* Subtle reflection/shadow effect */}
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-3/4 h-12 bg-black/5 blur-3xl rounded-[100%]" />
|
||||
</div>
|
||||
|
||||
{product.frontmatter.images.length > 1 && (
|
||||
<div className="flex justify-center gap-6 mt-16">
|
||||
{product.frontmatter.images.slice(0, 5).map((img, idx) => (
|
||||
<div key={idx} className="relative w-24 h-24 md:w-32 md:h-32 border-2 border-neutral-dark/10 rounded-xl overflow-hidden bg-neutral-light/20 hover:border-accent transition-all duration-300 cursor-pointer group">
|
||||
<Image src={img} alt="" fill className="object-cover transition-transform duration-500 group-hover:scale-110" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-16 lg:gap-24 relative items-start overflow-visible">
|
||||
<div className="flex-1 w-full">
|
||||
{/* Main Content Area */}
|
||||
<div className="prose prose-lg md:prose-xl prose-slate max-w-none prose-headings:text-primary prose-headings:font-extrabold prose-a:text-primary prose-strong:text-primary prose-img:rounded-3xl prose-img:shadow-2xl">
|
||||
<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('/')}`,
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Related Products */}
|
||||
<RelatedProducts
|
||||
currentSlug={productSlug}
|
||||
categories={product.frontmatter.categories}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full lg:w-[450px] xl:w-[500px] lg:sticky lg:top-32">
|
||||
<div className="lg:translate-x-12 xl:translate-x-20">
|
||||
{/* Request Quote Form */}
|
||||
<div className="bg-white rounded-3xl shadow-2xl border border-neutral-dark/5 overflow-hidden">
|
||||
<div className="bg-primary-dark p-6 md:p-8 text-white relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-40 h-40 bg-accent/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-3xl" />
|
||||
|
||||
{/* Product Thumbnail for Sticky State */}
|
||||
{product.frontmatter.images?.[0] && (
|
||||
<div className="relative w-full aspect-[16/9] mb-6 rounded-xl overflow-hidden bg-white/10 backdrop-blur-md p-3 border border-white/10 z-10">
|
||||
<Image
|
||||
src={product.frontmatter.images[0]}
|
||||
alt=""
|
||||
fill
|
||||
className="object-contain p-1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h3 className="text-xl md:text-2xl font-extrabold mb-2 relative z-10 tracking-tight">{t('requestQuote')}</h3>
|
||||
<p className="text-white/60 text-sm relative z-10 leading-relaxed">{t('requestQuoteDesc')}</p>
|
||||
</div>
|
||||
<div className="p-6 md:p-8">
|
||||
<RequestQuoteForm productName={product.frontmatter.title} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user