feat: complete website implementation
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
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 Link from 'next/link';
|
||||
|
||||
interface ProductPageProps {
|
||||
params: {
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const products = getProducts();
|
||||
return products.map((product) => ({
|
||||
slug: product.slug,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: ProductPageProps): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const product = getProductBySlug(slug);
|
||||
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ProductPage({ params }: ProductPageProps) {
|
||||
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://aeroservice-demo.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://aeroservice-demo.pl/sklep',
|
||||
},
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 2,
|
||||
name: product.name,
|
||||
item: `https://aeroservice-demo.pl/sklep/${product.slug}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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) }}
|
||||
/>
|
||||
|
||||
<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" />
|
||||
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>
|
||||
|
||||
{/* Zakładki z detalami */}
|
||||
<Tabs product={product} />
|
||||
|
||||
{/* 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} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user