feat: optymalizacja GlobalSearch, nawigacji oraz filtrów w sklepie
This commit is contained in:
+28
-102
@@ -1,11 +1,8 @@
|
||||
import { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
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';
|
||||
import { ProductPageClient } from '@/components/shop/ProductPageClient';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ slug: string }>;
|
||||
@@ -31,104 +28,33 @@ export default async function ProductPage({ params }: Props) {
|
||||
const product = getProductBySlug(slug);
|
||||
if (!product) notFound();
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://thermcool.pl';
|
||||
|
||||
const productJsonLd = product.price ? {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Product",
|
||||
"name": product.name,
|
||||
"description": product.shortDescription,
|
||||
"sku": product.sku,
|
||||
"brand": { "@type": "Brand", "name": product.brand },
|
||||
"image": product.image.startsWith('http') ? product.image : `${baseUrl}${product.image}`,
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"url": `${baseUrl}/sklep/${product.slug}`,
|
||||
"priceCurrency": "PLN",
|
||||
"price": product.price,
|
||||
"priceValidUntil": "2027-12-31",
|
||||
"availability": "https://schema.org/InStock",
|
||||
"seller": { "@type": "Organization", "name": "ThermCool" },
|
||||
},
|
||||
} : null;
|
||||
|
||||
return (
|
||||
<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">
|
||||
|
||||
{/* 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>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{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>
|
||||
<>
|
||||
{productJsonLd && (
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }} />
|
||||
)}
|
||||
<ProductPageClient product={product} companyPhone={companyDetails.phone} companyPhoneRaw={companyDetails.phoneRaw} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+15
-2
@@ -9,7 +9,15 @@ export const metadata: Metadata = {
|
||||
alternates: { canonical: '/sklep' },
|
||||
};
|
||||
|
||||
export default function ShopPage() {
|
||||
interface Props {
|
||||
searchParams: Promise<{ search?: string; brand?: string }>;
|
||||
}
|
||||
|
||||
export default async function ShopPage({ searchParams }: Props) {
|
||||
const params = await searchParams;
|
||||
const initialSearch = params.search || '';
|
||||
const initialBrand = params.brand || '';
|
||||
|
||||
const products = getProducts();
|
||||
const brands = getBrands();
|
||||
const splitCount = products.filter(p => p.category === 'scienne').length;
|
||||
@@ -27,7 +35,12 @@ export default function ShopPage() {
|
||||
secondaryCtaLink={undefined}
|
||||
backgroundImage="https://images.unsplash.com/photo-1621905251189-08b45d6a269e?w=1600&q=80"
|
||||
/>
|
||||
<ProductsGrid products={products} brands={brands} />
|
||||
<ProductsGrid
|
||||
products={products}
|
||||
brands={brands}
|
||||
initialSearch={initialSearch}
|
||||
initialBrand={initialBrand}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user