feat: implement compare functionality, checkout flow, and add 30 blog posts

This commit is contained in:
MichalC
2026-06-27 18:09:22 +02:00
parent 6bdfe18e18
commit 0c59086a6b
63 changed files with 9766 additions and 1874 deletions
+104 -119
View File
@@ -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>
);
}
+17 -30
View File
@@ -1,46 +1,33 @@
import { Metadata } from 'next';
import { HeroSection } from '@/components/sections/HeroSection';
import { getProducts, getBrands } from '@/content/products';
import { ProductsGrid } from '@/components/shop/ProductsGrid';
import { getProducts } from '@/content/products';
import { HeroSection } from '@/components/sections/HeroSection';
export const metadata: Metadata = {
title: 'Sklep z klimatyzatorami | AirProject',
description: 'Wybierz idealny klimatyzator do swojego domu. Oferujemy urządzenia renomowanych marek z możliwością profesjonalnego montażu.',
alternates: {
canonical: '/sklep',
}
title: 'Sklep z klimatyzatorami | ThermCool',
description: 'Klimatyzatory split i multisplit — Gree, Rotenso, Haier, Daikin, LG, Samsung i inne. Szeroki wybór, konkurencyjne ceny, profesjonalny montaż.',
alternates: { canonical: '/sklep' },
};
export default function SklepPage() {
export default function ShopPage() {
const products = getProducts();
const brands = getBrands();
const splitCount = products.filter(p => p.category === 'scienne').length;
const multiCount = products.filter(p => p.category === 'multisplit').length;
return (
<main>
<HeroSection
eyebrow="E-commerce"
title="Sklep z klimatyzatorami"
subtitle="Znajdź urządzenie idealnie dopasowane do Twoich potrzeb. Zamów samo urządzenie lub wybierz kompleksową usługę z naszym profesjonalnym montażem."
ctaText="Skontaktuj się z doradcą"
eyebrow="Sklep ThermCool"
title="Klimatyzatory split i multisplit"
subtitle={`${splitCount} klimatyzatorów split i ${multiCount} jednostek multisplit. Gree, Rotenso, Haier, Daikin, LG, Samsung i inne marki. Z montażem lub bez.`}
ctaText="Zapytaj o montaż"
ctaLink="/kontakt"
backgroundImage="https://images.unsplash.com/photo-1527689638836-411945a2b57c?w=1600&q=80"
secondaryCtaText={undefined}
secondaryCtaLink={undefined}
backgroundImage="https://images.unsplash.com/photo-1621905251189-08b45d6a269e?w=1600&q=80"
/>
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ProductsGrid products={products} />
</section>
{/* SEO Blok na dole */}
<section className="bg-slate-50 py-16 border-t border-border mt-12">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 prose prose-slate">
<h2 className="text-2xl font-bold font-heading text-primary">Klimatyzatory ścienne, multisplit i przenośne</h2>
<p>
Wybór odpowiedniego klimatyzatora to decyzja na lata. W naszym sklepie oferujemy wyłącznie sprawdzone modele od liderów rynku, takich jak Rotenso, Daikin, Mitsubishi, Gree czy LG. Niezależnie od tego, czy szukasz prostego urządzenia ściennego (split), zaawansowanego systemu dla kilku pomieszczeń (multisplit), czy rozwiązania do biura (klimatyzatory kasetonowe), znajdziesz je u nas w atrakcyjnych cenach.
</p>
<p>
Pamiętaj, że gwarancją niezawodności jest nie tylko sam sprzęt, ale przede wszystkim jego <strong>poprawny montaż</strong>. Zachęcamy do wyboru opcji "Kup z montażem" – nasi certyfikowani instalatorzy F-gaz zapewnią pełen profesjonalizm i bezpieczeństwo Twojej nowej instalacji, a Ty zyskasz spokój i pełną gwarancję producenta.
</p>
</div>
</section>
<ProductsGrid products={products} brands={brands} />
</main>
);
}