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
-121
View File
@@ -1,121 +0,0 @@
'use client';
import React, { useState, useRef, useEffect, MouseEvent, TouchEvent } from 'react';
import Image from 'next/image';
import { projectsData } from '@/content/projects';
import { GripVertical } from 'lucide-react';
export const BeforeAfterSlider = () => {
const [sliderPosition, setSliderPosition] = useState(50);
const [isDragging, setIsDragging] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Pobierz pierwszy projekt z kategorii remonty, który ma beforeAfter
const project = projectsData.find(p => p.category === 'remonty' && p.beforeAfter);
if (!project || !project.beforeAfter) return null;
const handleMove = (clientX: number) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = Math.max(0, Math.min(clientX - rect.left, rect.width));
const percentage = (x / rect.width) * 100;
setSliderPosition(percentage);
};
const onMouseMove = (e: globalThis.MouseEvent) => {
if (!isDragging) return;
handleMove(e.clientX);
};
const onTouchMove = (e: globalThis.TouchEvent) => {
if (!isDragging) return;
handleMove(e.touches[0].clientX);
};
const stopDragging = () => {
setIsDragging(false);
};
useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', stopDragging);
window.addEventListener('touchmove', onTouchMove);
window.addEventListener('touchend', stopDragging);
} else {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', stopDragging);
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('touchend', stopDragging);
}
return () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', stopDragging);
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('touchend', stopDragging);
};
}, [isDragging]);
return (
<section className="py-16 bg-white">
<div className="container mx-auto px-4 md:px-8 max-w-5xl">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-primary mb-4">Przed i Po</h2>
<p className="text-gray-600">Zobacz metamorfozę na jednym z naszych ostatnich projektów.</p>
</div>
<div
ref={containerRef}
className="relative w-full aspect-[4/3] md:aspect-[16/9] rounded-2xl overflow-hidden select-none cursor-ew-resize bg-gray-100"
onMouseDown={() => setIsDragging(true)}
onTouchStart={() => setIsDragging(true)}
>
{/* Obrazek PO (na spodzie) */}
<div className="absolute inset-0">
<Image
src={project.beforeAfter.afterUrl}
alt="Wygląd po remoncie"
fill
className="object-cover pointer-events-none"
/>
<div className="absolute top-4 right-4 bg-white/80 backdrop-blur text-primary font-semibold px-4 py-1.5 rounded-full text-sm">
Po
</div>
</div>
{/* Obrazek PRZED (na wierzchu, obcięty) */}
<div
className="absolute inset-0 overflow-hidden"
style={{ width: `${sliderPosition}%` }}
>
<div className="absolute inset-0 w-[100vw] min-w-full">
{/* Używamy native img dla prostszego manipulowania wymiarami wewnątrz overflow */}
<img
src={project.beforeAfter.beforeUrl}
alt="Wygląd przed remontem"
className="absolute inset-0 w-full h-full object-cover pointer-events-none"
style={{ width: containerRef.current?.getBoundingClientRect().width || '100%' }}
/>
</div>
<div className="absolute top-4 left-4 bg-white/80 backdrop-blur text-gray-700 font-semibold px-4 py-1.5 rounded-full text-sm">
Przed
</div>
</div>
{/* Suwak (linia i uchwyt) */}
<div
className="absolute top-0 bottom-0 w-1 bg-white cursor-ew-resize flex items-center justify-center shadow-[0_0_10px_rgba(0,0,0,0.3)]"
style={{ left: `calc(${sliderPosition}% - 2px)` }}
>
<div className="w-10 h-10 bg-white rounded-full flex items-center justify-center shadow-lg border border-gray-100 text-primary hover:scale-110 transition-transform">
<GripVertical className="w-5 h-5 text-gray-400" />
</div>
</div>
</div>
</div>
</section>
);
};
+3 -3
View File
@@ -15,18 +15,18 @@ export interface HeroSectionProps {
}
export function HeroSection({
eyebrow = "HVAC & Usługi Wykończeniowe",
eyebrow = "Klimatyzacja i Pompy Ciepła",
title = (
<>
Klimatyzacja, pompy ciepła<br className="hidden sm:inline" />
<span className="text-accent"> i usługi domowe</span>
</>
),
subtitle = "Oferujemy profesjonalny dobór, czysty montaż i sprawny serwis. Wykończymy Twoje wnętrza i naprawimy instalacje. Solidnie i z polecenia.",
subtitle = "Oferujemy profesjonalny dobór, czysty montaż i sprawny serwis klimatyzacji oraz pomp ciepła. Solidnie, estetycznie i z pełną gwarancją.",
ctaText = "Darmowa wycena",
ctaLink = "/kontakt",
secondaryCtaText = "Zobacz ofertę",
secondaryCtaLink = "/uslugi-domowe",
secondaryCtaLink = "/sklep",
backgroundImage = "/hero.jpg",
features
}: HeroSectionProps = {}) {
+10 -8
View File
@@ -47,11 +47,8 @@ export function ProcessSteps({
{/* Process Steps Timeline */}
<div className="relative mt-12">
{/* Desktop connecting line (only if few steps, otherwise wrap) */}
{steps.length <= 4 && (
<div className="hidden lg:block absolute top-10 left-[12%] right-[12%] h-0.5 bg-slate-200 z-0" />
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-y-12 gap-x-8 relative z-10">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-y-12 gap-x-8 relative z-10">
{steps.map((step, idx) => {
const Icon = iconMap[step.icon || ""] || HelpCircle;
return (
@@ -59,10 +56,15 @@ export function ProcessSteps({
{/* Circle Badge with Icon & Number */}
<div className="relative size-20 rounded-full bg-slate-50 border-2 border-slate-200 flex items-center justify-center text-primary group-hover:border-accent group-hover:bg-accent-light group-hover:text-accent transition-all duration-300 shadow-sm z-10">
{/* Step Number */}
<span className="absolute -top-1.5 -right-1.5 size-6 rounded-full bg-cta text-white text-xs font-bold flex items-center justify-center font-heading border-2 border-white shadow-sm">
{step.number}
</span>
<Icon className="size-8 group-hover:scale-110 transition-transform duration-300" />
{step.icon && iconMap[step.icon] && (
<span className="absolute -top-1.5 -right-1.5 size-6 rounded-full bg-accent text-white text-xs font-bold flex items-center justify-center font-heading border-2 border-white shadow-sm">
{step.number}
</span>
)}
{step.icon && iconMap[step.icon]
? <Icon className="size-8 group-hover:scale-110 transition-transform duration-300" />
: <span className="text-2xl font-extrabold font-heading text-accent">{step.number}</span>
}
</div>
{/* Title & Description */}
-2
View File
@@ -13,8 +13,6 @@ const categories: { id: Category; label: string }[] = [
{ id: 'wszystkie', label: 'Wszystkie' },
{ id: 'klimatyzacja', label: 'Klimatyzacja' },
{ id: 'pompy-ciepla', label: 'Pompy ciepła' },
{ id: 'remonty', label: 'Remonty' },
{ id: 'hydraulika', label: 'Hydraulika' },
];
export function ProjectsGrid() {
+1 -1
View File
@@ -9,7 +9,7 @@ export function Testimonials() {
<SectionHeading
eyebrow="Opinie Klientów"
title="Co mówią o nas nasi klienci?"
subtitle="Zadowolenie klientów to nasz najwyższy cel. Zobacz opinie osób, które zaufały naszym instalatorom i ekipom budowlanym."
subtitle="Zadowolenie klientów to nasz najwyższy cel. Zobacz opinie osób, które zaufały naszym montażystom i serwisantom."
align="center"
/>
+4 -4
View File
@@ -5,8 +5,8 @@ export function WhyUs() {
const advantages = [
{
icon: Award,
title: "Uprawnienia F-Gaz i certyfikaty",
description: "Posiadamy wszystkie wymagane certyfikaty UDT oraz autoryzacje wiodących producentów klimatyzacji.",
title: "Doświadczenie i certyfikaty",
description: "Posiadamy wszystkie wymagane certyfikaty i autoryzacje. Działamy na rynku od ponad 15 lat.",
},
{
icon: FileText,
@@ -32,8 +32,8 @@ export function WhyUs() {
{/* Left Column: Title, Intro and Statistics */}
<div className="lg:col-span-5 flex flex-col gap-6">
<div>
<span className="text-xs md:text-sm font-semibold uppercase tracking-wider text-cta font-heading">
Dlaczego ThermCool
<span className="text-xs md:text-sm font-semibold uppercase tracking-wider text-accent font-heading">
{`Dlaczego ${companyDetails.shortName}`}
</span>
<h2 className="text-3xl md:text-4xl font-bold text-primary font-heading leading-tight mt-2">
Zaufany partner w instalacjach domowych