feat: implement compare functionality, checkout flow, and add 30 blog posts
This commit is contained in:
+104
-119
@@ -1,149 +1,134 @@
|
||||
import { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { getProducts, getProductBySlug } from '@/content/products';
|
||||
import { ProductGallery } from '@/components/shop/ProductGallery';
|
||||
import { ProductInfo } from '@/components/shop/ProductInfo';
|
||||
import { Tabs } from '@/components/shop/Tabs';
|
||||
import { ProductCard } from '@/components/shop/ProductCard';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { Phone, ArrowLeft, ShoppingBag } from 'lucide-react';
|
||||
import { getProductBySlug, getProducts } from '@/content/products';
|
||||
import { CtaBanner } from '@/components/sections/CtaBanner';
|
||||
import { companyDetails } from '@/content/company';
|
||||
|
||||
interface ProductPageProps {
|
||||
params: {
|
||||
slug: string;
|
||||
};
|
||||
interface Props {
|
||||
params: Promise<{ slug: string }>;
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const products = getProducts();
|
||||
return products.map((product) => ({
|
||||
slug: product.slug,
|
||||
}));
|
||||
return getProducts().map(p => ({ slug: p.slug }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: ProductPageProps): Promise<Metadata> {
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const product = getProductBySlug(slug);
|
||||
|
||||
if (!product) {
|
||||
return { title: 'Produkt nie znaleziony' };
|
||||
}
|
||||
|
||||
if (!product) return { title: 'Produkt nie znaleziony' };
|
||||
return {
|
||||
title: `${product.brand} ${product.name} | Sklep AirProject`,
|
||||
description: product.description,
|
||||
alternates: {
|
||||
canonical: `/sklep/${product.slug}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: `${product.brand} ${product.name}`,
|
||||
description: product.description,
|
||||
type: 'website',
|
||||
images: [
|
||||
{
|
||||
url: product.image || '/hero.jpg',
|
||||
},
|
||||
],
|
||||
},
|
||||
title: `${product.brand} ${product.name} | Sklep ThermCool`,
|
||||
description: product.shortDescription || `Klimatyzator ${product.name} — sprawdź specyfikację i cenę. Profesjonalny montaż na Dolnym Śląsku.`,
|
||||
alternates: { canonical: `/sklep/${product.slug}` },
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ProductPage({ params }: ProductPageProps) {
|
||||
export default async function ProductPage({ params }: Props) {
|
||||
const { slug } = await params;
|
||||
const product = getProductBySlug(slug);
|
||||
|
||||
if (!product) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const allProducts = getProducts();
|
||||
// Dobieramy 4 podobne produkty
|
||||
const relatedProducts = allProducts
|
||||
.filter(p => p.category === product.category && p.slug !== product.slug)
|
||||
.slice(0, 4);
|
||||
|
||||
// JSON-LD
|
||||
const jsonLdProduct = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Product',
|
||||
name: `${product.brand} ${product.name}`,
|
||||
image: product.image,
|
||||
description: product.description,
|
||||
brand: {
|
||||
'@type': 'Brand',
|
||||
name: product.brand,
|
||||
},
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
url: `https://thermcool.pl/sklep/${product.slug}`,
|
||||
priceCurrency: 'PLN',
|
||||
price: product.price,
|
||||
itemCondition: 'https://schema.org/NewCondition',
|
||||
availability: 'https://schema.org/InStock',
|
||||
},
|
||||
};
|
||||
|
||||
const jsonLdBreadcrumb = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 1,
|
||||
name: 'Sklep',
|
||||
item: 'https://thermcool.pl/sklep',
|
||||
},
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 2,
|
||||
name: product.name,
|
||||
item: `https://thermcool.pl/sklep/${product.slug}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
if (!product) notFound();
|
||||
|
||||
return (
|
||||
<main className="bg-slate-50 pt-24 pb-16 min-h-screen">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLdProduct) }}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLdBreadcrumb) }}
|
||||
/>
|
||||
<main className="bg-slate-50 min-h-screen">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Nawigacja powrotu */}
|
||||
<Link
|
||||
href="/sklep"
|
||||
className="inline-flex items-center gap-2 text-sm font-semibold text-text-muted hover:text-primary transition-colors mb-8"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
{/* Breadcrumb */}
|
||||
<Link href="/sklep" className="inline-flex items-center gap-2 text-sm text-text-muted hover:text-accent transition-colors mb-6">
|
||||
<ArrowLeft className="size-4" />
|
||||
Wróć do sklepu
|
||||
</Link>
|
||||
|
||||
{/* Górna sekcja */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16">
|
||||
<ProductGallery image={product.image || ''} name={product.name} />
|
||||
<ProductInfo product={product} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
|
||||
{/* Zakładki z detalami */}
|
||||
<Tabs product={product} />
|
||||
{/* Zdjęcie */}
|
||||
<div className="relative aspect-square bg-white rounded-2xl border border-border overflow-hidden">
|
||||
<Image
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
fill
|
||||
className="object-contain p-8"
|
||||
priority
|
||||
unoptimized={product.image.startsWith('http')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Podobne produkty */}
|
||||
{relatedProducts.length > 0 && (
|
||||
<section className="mt-24">
|
||||
<h2 className="text-2xl font-bold text-primary font-heading mb-8">Podobne produkty</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{relatedProducts.map(relProduct => (
|
||||
<ProductCard key={relProduct.slug} product={relProduct} />
|
||||
))}
|
||||
{/* Info */}
|
||||
<div className="flex flex-col gap-6">
|
||||
<div>
|
||||
<span className="text-sm font-semibold text-accent uppercase tracking-wider">{product.brand}</span>
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-primary font-heading mt-1 leading-tight">
|
||||
{product.name}
|
||||
</h1>
|
||||
<p className="text-xs text-text-muted mt-1">SKU: {product.sku}</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{product.shortDescription && (
|
||||
<p className="text-text-muted leading-relaxed">{product.shortDescription}</p>
|
||||
)}
|
||||
|
||||
{/* Cena / CTA */}
|
||||
<div className="p-5 rounded-xl bg-white border border-border">
|
||||
{product.price !== null ? (
|
||||
<div>
|
||||
<p className="text-3xl font-bold text-primary font-heading">
|
||||
{product.price.toLocaleString('pl-PL')} zł
|
||||
</p>
|
||||
<p className="text-sm text-text-muted mt-1">cena urządzenia brutto (23% VAT)</p>
|
||||
<p className="text-xs text-text-muted mt-1">Montaż wyceniamy indywidualnie — zadzwoń lub napisz</p>
|
||||
<div className="flex flex-col sm:flex-row gap-3 mt-4">
|
||||
<a href={`tel:${companyDetails.phoneRaw}`} className="btn btn-primary font-bold text-white flex items-center gap-2 justify-center">
|
||||
<Phone className="size-4" />
|
||||
Zadzwoń i zamów
|
||||
</a>
|
||||
<Link href="/kontakt" className="btn btn-outline btn-primary bg-white font-bold flex items-center gap-2 justify-center">
|
||||
<ShoppingBag className="size-4" />
|
||||
Zapytaj o montaż
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className="text-lg font-semibold text-primary font-heading">Cena na zapytanie</p>
|
||||
<p className="text-sm text-text-muted mt-1">Multisplit — cena zależy od wybranych jednostek wewnętrznych i konfiguracji instalacji.</p>
|
||||
<a href={`tel:${companyDetails.phoneRaw}`} className="btn btn-primary font-bold text-white flex items-center gap-2 mt-4">
|
||||
<Phone className="size-4" />
|
||||
Zapytaj o wycenę
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Specyfikacja */}
|
||||
{Object.keys(product.specs).length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-base font-bold text-primary font-heading mb-3">Specyfikacja</h2>
|
||||
<div className="rounded-xl border border-border overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<tbody>
|
||||
{Object.entries(product.specs).map(([key, val], idx) => (
|
||||
<tr key={key} className={idx % 2 === 0 ? 'bg-white' : 'bg-slate-50'}>
|
||||
<td className="px-4 py-2.5 text-text-muted font-medium w-1/2">{key}</td>
|
||||
<td className="px-4 py-2.5 text-primary">{val}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CtaBanner
|
||||
title="Potrzebujesz montażu?"
|
||||
subtitle="Sprzedajemy z montażem na terenie Dolnego Śląska. Zadzwoń i ustal termin."
|
||||
ctaText="Umów montaż"
|
||||
ctaLink="/kontakt"
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user