From 002a325dfb597e61d6e1d6f88e5a45f9d67399c5 Mon Sep 17 00:00:00 2001 From: MichalC <103170279+wSBMichalee@users.noreply.github.com> Date: Sun, 28 Jun 2026 00:16:22 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20dodanie=20kalkulatora=20doboru=20klimat?= =?UTF-8?q?yzacji=20i=20zaktualizowanie=20996=20produkt=C3=B3w?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/kalkulator/page.tsx | 52 +- app/sklep/page.tsx | 17 +- components/kalkulator/KalkulatorClient.tsx | 465 ++ components/layout/Header.tsx | 5 - components/sections/AcTypesSection.tsx | 6 +- components/shop/ProductCard.tsx | 31 +- components/shop/ProductsGrid.tsx | 38 +- content/products.ts | 6543 ++++++++++++++++---- parse_transcript.py | 20 + parse_transcript2.py | 14 + 10 files changed, 5740 insertions(+), 1451 deletions(-) create mode 100644 components/kalkulator/KalkulatorClient.tsx create mode 100644 parse_transcript.py create mode 100644 parse_transcript2.py diff --git a/app/kalkulator/page.tsx b/app/kalkulator/page.tsx index 127445d..59054eb 100644 --- a/app/kalkulator/page.tsx +++ b/app/kalkulator/page.tsx @@ -1,51 +1,13 @@ -import { CtaBanner } from "@/components/sections/CtaBanner"; -import { Wrench, Phone, ArrowRight } from "lucide-react"; -import Link from "next/link"; -import { companyDetails } from "@/content/company"; -import type { Metadata } from "next"; +import type { Metadata } from 'next'; +import { KalkulatorClient } from '@/components/kalkulator/KalkulatorClient'; +import { getProducts } from '@/content/products'; export const metadata: Metadata = { - title: 'Kalkulator doboru | ThermCool', - description: "Kalkulator doboru klimatyzacji i pomp ciepła w przygotowaniu.", + title: 'Kalkulator doboru klimatyzacji | ThermCool', + description: 'Dobierz klimatyzator do swojego pomieszczenia w 4 krokach. Kalkulator mocy, dobór kategorii i szacunkowy koszt montażu.', }; export default function KalkulatorPage() { - return ( -
-
-
-
- -
-

- Narzędzie w przygotowaniu -

-

- Pracujemy nad zaawansowanym konfiguratorem, który krok po kroku pomoże Ci dobrać sprzęt i natychmiast oszacuje koszty montażu. Narzędzie zostanie udostępnione już wkrótce! -

- -
- - - - - - -
-
-
- - -
- ); + const products = getProducts(); + return ; } diff --git a/app/sklep/page.tsx b/app/sklep/page.tsx index 09a8a20..7f19f8d 100644 --- a/app/sklep/page.tsx +++ b/app/sklep/page.tsx @@ -9,15 +9,7 @@ export const metadata: Metadata = { alternates: { canonical: '/sklep' }, }; -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 || ''; - +export default function ShopPage() { const products = getProducts(); const brands = getBrands(); const splitCount = products.filter(p => p.category === 'scienne').length; @@ -35,12 +27,7 @@ export default async function ShopPage({ searchParams }: Props) { secondaryCtaLink={undefined} backgroundImage="https://images.unsplash.com/photo-1621905251189-08b45d6a269e?w=1600&q=80" /> - + ); } diff --git a/components/kalkulator/KalkulatorClient.tsx b/components/kalkulator/KalkulatorClient.tsx new file mode 100644 index 0000000..a7824a5 --- /dev/null +++ b/components/kalkulator/KalkulatorClient.tsx @@ -0,0 +1,465 @@ +'use client'; + +import { useState, useMemo } from 'react'; +import Link from 'next/link'; +import Image from 'next/image'; +import { ArrowRight, ArrowLeft, Check, Thermometer, Home, Zap, ShoppingCart, Phone, RotateCcw } from 'lucide-react'; +import { Product } from '@/content/products'; +import { companyDetails } from '@/content/company'; + +interface Props { + products: Product[]; +} + +// Oblicz wymaganą moc chłodniczą w kW +function calculatePower(m2: number, floor: string, sun: string, building: string): number { + let power = m2 * 0.1; // baza: 1 kW na 10 m² + + // Korekta typu budynku i piętra + if (building === 'apartment') { + if (floor === 'top') power *= 1.2; // ostatnie piętro - więcej ciepła z dachu + if (floor === 'ground') power *= 0.95; + } else if (building === 'house') { + if (floor === 'top') power *= 1.1; // poddasze - dodatkowe nagrzewanie + } + + // Korekta nasłonecznienia + if (sun === 'south') power *= 1.15; // południe - duże nasłonecznienie + if (sun === 'west') power *= 1.1; + if (sun === 'north') power *= 0.9; + + return Math.round(power * 10) / 10; +} + +// Szacunkowy koszt montażu +function estimateInstall(pipes: string, rooms: number): { min: number; max: number } { + const base = rooms === 1 ? 1200 : rooms === 2 ? 2800 : rooms === 3 ? 4200 : 5500; + const pipeExtra = pipes === 'short' ? 0 : pipes === 'medium' ? 300 : pipes === 'long' ? 700 : 1200; + return { min: base + pipeExtra, max: base + pipeExtra + 600 }; +} + +const STEPS = ['Pomieszczenie', 'Konfiguracja', 'Instalacja', 'Budżet'] as const; + +export function KalkulatorClient({ products }: Props) { + const [step, setStep] = useState(0); + const [building, setBuilding] = useState('apartment'); + const [m2, setM2] = useState(25); + const [floor, setFloor] = useState('middle'); // ground | middle | top + const [sun, setSun] = useState('south'); // north | east | south | west + const [rooms, setRooms] = useState(1); + const [pipes, setPipes] = useState('medium'); // short(<3m) | medium(3-6m) | long(6-10m) | verylong(>10m) + const [budget, setBudget] = useState(8000); + const [showResults, setShowResults] = useState(false); + + const requiredPower = useMemo(() => calculatePower(m2, floor, sun, building), [m2, floor, sun, building]); + const installCost = useMemo(() => estimateInstall(pipes, rooms), [pipes, rooms]); + const category = rooms === 1 ? 'scienne' : 'multisplit'; + + const results = useMemo(() => { + if (!showResults) return []; + const powerMin = requiredPower * 0.8; + const powerMax = requiredPower * 1.4; + const maxPrice = budget - installCost.min; + + return products + .filter(p => { + if (p.category !== category) return false; + if (p.price === null || p.price > maxPrice) return false; + const moc = parseFloat(String(p.specs?.['Moc chłodnicza'] || '0').replace(',', '.').split('–')[0]); + return moc >= powerMin && moc <= powerMax; + }) + .sort((a, b) => { + // Sortuj po tym jak blisko jest moc do wymaganej + const mocA = parseFloat(String(a.specs?.['Moc chłodnicza'] || '0').replace(',', '.').split('–')[0]); + const mocB = parseFloat(String(b.specs?.['Moc chłodnicza'] || '0').replace(',', '.').split('–')[0]); + return Math.abs(mocA - requiredPower) - Math.abs(mocB - requiredPower); + }) + .slice(0, 6); + }, [showResults, products, requiredPower, category, budget, installCost.min]); + + const handleFinish = () => setShowResults(true); + const handleReset = () => { setShowResults(false); setStep(0); }; + + if (showResults) { + return ( +
+
+ {/* Podsumowanie */} +
+

+ Dopasowane klimatyzatory +

+
+ {[ + { label: 'Wymagana moc', value: `${requiredPower} kW` }, + { label: 'Powierzchnia', value: `${m2} m²` }, + { label: 'Typ instalacji', value: rooms === 1 ? 'Split (1 pokój)' : `Multisplit (${rooms} pokoje)` }, + { label: 'Szac. montaż', value: `${installCost.min.toLocaleString('pl-PL')}–${installCost.max.toLocaleString('pl-PL')} zł` }, + ].map((s, i) => ( +
+

{s.label}

+

{s.value}

+
+ ))} +
+
+ + {results.length > 0 ? ( +
+ {results.map(product => ( +
+
+ {product.name} + {product.specs?.['Klasa energetyczna'] && ( + + {product.specs['Klasa energetyczna'].split('/')[0]} + + )} +
+
+

{product.brand}

+

+ {product.name} +

+
+ {product.specs?.['Moc chłodnicza'] && ( + + {product.specs['Moc chłodnicza']} kW + + )} + {product.specs?.['Hałas wewnątrz (dB)'] && ( + + {product.specs['Hałas wewnątrz (dB)']} dB + + )} +
+
+
+

+ {product.price?.toLocaleString('pl-PL')} zł +

+

+ montaż od {installCost.min.toLocaleString('pl-PL')} zł

+
+ + + +
+
+
+ ))} +
+ ) : ( +
+

+ Nie znaleziono produktów dla podanych kryteriów. Spróbuj zwiększyć budżet lub zadzwoń do nas — dobierzemy ofertę indywidualnie. +

+ + + +
+ )} + +
+ + + + +
+
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+

+ Dobierz klimatyzator +

+

Odpowiedz na 4 pytania — pokażemy Ci najlepsze opcje z naszej oferty

+
+ + {/* Pasek postępu */} +
+ {STEPS.map((s, i) => ( +
+
+ {i < step ? : i + 1} +
+ + {i < STEPS.length - 1 &&
} +
+ ))} +
+ + {/* Karty kroków */} +
+ + {/* Krok 0: Pomieszczenie */} + {step === 0 && ( +
+
+
+ +
+
+

Pomieszczenie

+

Metraż i warunki

+
+
+ +
+ + setM2(Number(e.target.value))} + className="w-full accent-accent" + /> +
+ 10 m²100 m² +
+
+ Wymagana moc: ok. {requiredPower} kW + (dla {m2} m²) +
+
+ +
+ +
+ {[ + { value: 'apartment', label: '🏢 Mieszkanie w bloku', sub: 'Kamienica, wieżowiec, blok' }, + { value: 'house', label: '🏠 Dom jednorodzinny', sub: 'Wolnostojący, szeregowiec, bliźniak' }, + ].map(o => ( + + ))} +
+
+ + {building === 'house' && ( +
+ +
+ {[ + { value: 'ground', label: '🏠 Parter' }, + { value: 'top', label: '🏠 Piętro / poddasze' }, + ].map(o => ( + + ))} +
+
+ )} + + {building === 'apartment' && ( +
+ +
+ {[ + { value: 'ground', label: 'Parter' }, + { value: 'low', label: '1-3 piętro' }, + { value: 'middle', label: '4-8 piętro' }, + { value: 'high', label: '9+ piętro' }, + { value: 'top', label: 'Ostatnie' }, + ].map(o => ( + + ))} +
+
+ )} + +
+ +
+ {[ + { value: 'north', label: '🧭 Północ (chłodna)' }, + { value: 'east', label: '🧭 Wschód' }, + { value: 'west', label: '🧭 Zachód' }, + { value: 'south', label: '🧭 Południe (mocne słońce)' }, + ].map(o => ( + + ))} +
+
+
+ )} + + {/* Krok 1: Konfiguracja */} + {step === 1 && ( +
+
+
+ +
+
+

Ile pomieszczeń?

+

1 pomieszczenie = split, więcej = multisplit

+
+
+ +
+ {[ + { value: 1, label: '1 pomieszczenie', sub: 'Klimatyzator split', icon: '🏠' }, + { value: 2, label: '2 pomieszczenia', sub: 'System multisplit 2+1', icon: '🏡' }, + { value: 3, label: '3 pomieszczenia', sub: 'System multisplit 3+1', icon: '🏘️' }, + { value: 4, label: '4-5 pomieszczeń', sub: 'System multisplit 4+1', icon: '🏗️' }, + ].map(o => ( + + ))} +
+ +
+ {rooms === 1 + ? '✅ Klimatyzator split — jedna jednostka wewnętrzna + jedna zewnętrzna. Idealne do pojedynczego pomieszczenia.' + : `✅ System multisplit — jedna jednostka zewnętrzna obsługuje ${rooms} jednostki wewnętrzne. Ekonomiczne i estetyczne rozwiązanie dla całego domu.`} +
+
+ )} + + {/* Krok 2: Instalacja */} + {step === 2 && ( +
+
+
+ +
+
+

Długość instalacji

+

Rury i kable między jednostkami

+
+
+ +
+ {[ + { value: 'short', label: 'Do 3 metrów', sub: 'Jednostka zewnętrzna tuż obok (balkon, taras)' }, + { value: 'medium', label: '3–6 metrów', sub: 'Typowa instalacja w bloku lub domu' }, + { value: 'long', label: '6–10 metrów', sub: 'Dłuższa trasa, np. przez kilka pomieszczeń' }, + { value: 'verylong', label: 'Powyżej 10 metrów', sub: 'Bardzo długa trasa, wymaga dodatkowych materiałów' }, + ].map(o => ( + + ))} +
+ +
+ Szacowany koszt montażu dla Twojej konfiguracji: + {installCost.min.toLocaleString('pl-PL')}–{installCost.max.toLocaleString('pl-PL')} zł +
+
+ )} + + {/* Krok 3: Budżet */} + {step === 3 && ( +
+
+
+ +
+
+

Całkowity budżet

+

Urządzenie + montaż łącznie

+
+
+ +
+
+ + {budget.toLocaleString('pl-PL')} zł +
+ setBudget(Number(e.target.value))} + className="w-full accent-accent" + /> +
+ 3 000 zł25 000 zł +
+
+ +
+
+

Na urządzenie zostaje

+

+ ok. {Math.max(0, budget - installCost.min).toLocaleString('pl-PL')} zł +

+
+
+

Szac. koszt montażu

+

+ {installCost.min.toLocaleString('pl-PL')}–{installCost.max.toLocaleString('pl-PL')} zł +

+
+
+ +
+ 💡 Podsumowanie: szukamy klimatyzatora {category === 'scienne' ? 'split ściennego' : 'multisplit'} o mocy ok. {requiredPower} kW w cenie do {Math.max(0, budget - installCost.min).toLocaleString('pl-PL')} zł +
+
+ )} + + {/* Nawigacja */} +
+ {step > 0 ? ( + + ) :
} + + {step < 3 ? ( + + ) : ( + + )} +
+
+
+
+ ); +} diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx index e3f5530..d54f016 100644 --- a/components/layout/Header.tsx +++ b/components/layout/Header.tsx @@ -49,11 +49,6 @@ export function Header() { href: "/sklep?category=multisplit", description: "Systemy wielopokojowe (2-5 pomieszczeń)", }, - { - label: "Przenośne", - href: "/sklep?category=przenosne", - description: "Mobilne chłodzenie bez stałej instalacji", - }, { label: "Kasetonowe i konsolowe", href: "/sklep?category=kasetonowe", diff --git a/components/sections/AcTypesSection.tsx b/components/sections/AcTypesSection.tsx index fbd6d2f..b54c9cd 100644 --- a/components/sections/AcTypesSection.tsx +++ b/components/sections/AcTypesSection.tsx @@ -10,21 +10,21 @@ const acTypes = [ description: 'Najpopularniejsze rozwiązanie do jednego pomieszczenia. Składa się z jednej jednostki wewnętrznej i jednej zewnętrznej.', icon: Wind, subtypes: ['Ścienne', 'Konsolowe', 'Kasetonowe', 'Kanałowe'], - link: '/sklep/klimatyzatory-split', + link: '/sklep?category=scienne', }, { title: 'Systemy MULTISPLIT', description: 'Idealne do chłodzenia całego domu. Do jednej jednostki zewnętrznej można podłączyć od 2 do 5 jednostek wewnętrznych.', icon: LayoutGrid, subtypes: ['Dla 2 pomieszczeń', 'Dla 3 pomieszczeń', 'Dla 4-5 pomieszczeń', 'Mieszane typy jednostek'], - link: '/sklep/klimatyzatory-multisplit', + link: '/sklep?category=multisplit', }, { title: 'Pozostałe rozwiązania', description: 'Specjalistyczne urządzenia łączące funkcje chłodzenia z zaawansowanym grzaniem lub oczyszczaniem powietrza.', icon: Sparkles, subtypes: ['Z funkcją ogrzewania (do -25°C)', 'Z zaawansowanym oczyszczaczem', 'Klimatyzatory przenośne', 'Klimatyzatory bez jednostki zewn.'], - link: '/sklep/inne', + link: '/sklep', }, ]; diff --git a/components/shop/ProductCard.tsx b/components/shop/ProductCard.tsx index 7db7f18..990e64b 100644 --- a/components/shop/ProductCard.tsx +++ b/components/shop/ProductCard.tsx @@ -42,7 +42,6 @@ export function ProductCard({ product }: ProductCardProps) { price: product.price, }); setAdded(true); - setTimeout(() => setAdded(false), 1500); }; const handleCompare = (e: React.MouseEvent) => { @@ -161,20 +160,22 @@ export function ProductCard({ product }: ProductCardProps) { )} {product.price !== null ? ( - + added ? ( + e.stopPropagation()} + > + Zobacz koszyk + + ) : ( + + ) ) : (