feat: implement compare functionality, checkout flow, and add 30 blog posts
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 = {}) {
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user