import React, { useState, useEffect } from 'react'; import { Calendar as CalendarIcon, Wrench, CheckCircle, ArrowLeft, Info, ShoppingCart, Menu, X, Clock, Settings, User, Phone, ShieldCheck, AlertTriangle } from 'lucide-react'; // --- MOCK DATA: SKONSOLIDOWANA BAZA SPRZĘTU --- const EQUIPMENT_GROUPS = [ { id: 'grp-odk-pior', name: 'Odkurzacz Piorący Karcher', category: 'Odkurzacze', shortDesc: 'Profesjonalny sprzęt do ekstrakcyjnego prania tapicerki i dywanów.', fullDesc: 'Wydajny odkurzacz piorący, idealny do dogłębnego czyszczenia wykładzin, dywanów oraz tapicerki samochodowej i meblowej. Pozostawia minimalną wilgoć resztkową, dzięki czemu powierzchnie są szybko gotowe do użytku. W zestawie standardowa dysza do tapicerki.', pricePerDay: 60, imageUrl: 'https://images.unsplash.com/photo-1558317374-067fb5f30001?auto=format&fit=crop&q=80&w=800', units: Array.from({ length: 5 }).map((_, i) => ({ id: `odk-pior-${i + 1}`, label: `Egzemplarz #${i + 1}` })) }, { id: 'grp-osuszacz', name: 'Osuszacz powietrza Master', category: 'Budowa', shortDesc: 'Przemysłowy osuszacz kondensacyjny do zwalczania wilgoci.', fullDesc: 'Bardzo wydajny osuszacz powietrza, niezbędny przy pracach tynkarskich, malarskich oraz po zalaniach. Przyspiesza proces schnięcia budynków, zapobiega powstawaniu pleśni. Posiada duży zbiornik na wodę z możliwością podłączenia stałego odpływu.', pricePerDay: 40, imageUrl: 'https://images.unsplash.com/photo-1616880014022-297c554e20eb?auto=format&fit=crop&q=80&w=800', units: Array.from({ length: 2 }).map((_, i) => ({ id: `osuszacz-${i + 1}`, label: `Egzemplarz #${i + 1}` })) }, { id: 'grp-kosa', name: 'Kosa spalinowa STIHL/Husqvarna', category: 'Ogród', shortDesc: 'Mocna kosa do wycinania twardej trawy i zarośli.', fullDesc: 'Profesjonalna kosa spalinowa przeznaczona do intensywnych prac w trudnym terenie. Doskonale radzi sobie z wysoką trawą, chwastami oraz drobnymi zaroślami. Wyposażona w wygodne szelki odciążające kręgosłup i system antywibracyjny.', pricePerDay: 80, imageUrl: 'https://images.unsplash.com/photo-1592424001807-686967f6b7c5?auto=format&fit=crop&q=80&w=800', units: [{ id: 'kosa-1', label: 'Egzemplarz #1' }] }, { id: 'grp-termo', name: 'Kamera termowizyjna FLIR', category: 'Pomiary', shortDesc: 'Precyzyjna kamera do wykrywania strat ciepła i wilgoci.', fullDesc: 'Zaawansowana kamera termowizyjna o wysokiej rozdzielczości. Umożliwia szybką i bezinwazyjną diagnostykę budynków: wykrywanie mostków termicznych, nieszczelności w izolacji, awarii ogrzewania podłogowego oraz zawilgocenia ścian.', pricePerDay: 120, imageUrl: 'https://images.unsplash.com/photo-1581092334651-ddf26d9a09d0?auto=format&fit=crop&q=80&w=800', units: [{ id: 'termo-1', label: 'Egzemplarz #1' }] }, { id: 'grp-odk-bud', name: 'Odkurzacz budowlany Karcher', category: 'Budowa', shortDesc: 'Odkurzacz do pracy na sucho/mokro z otrzepywaczem.', fullDesc: 'Uniwersalny odkurzacz warsztatowo-budowlany. Niezbędny przy współpracy z elektronarzędziami (żyrafa, bruzdownica). Posiada automatyczny system oczyszczania filtra, co gwarantuje stałą siłę ssącą nawet przy drobnym pyle.', pricePerDay: 50, imageUrl: 'https://images.unsplash.com/photo-1527515637462-cff94eecc1ac?auto=format&fit=crop&q=80&w=800', units: [{ id: 'odk-bud-1', label: 'Egzemplarz #1' }] }, { id: 'grp-komp', name: 'Kompresor olejowy 50L', category: 'Warsztat', shortDesc: 'Kompresor do narzędzi pneumatycznych i malowania.', fullDesc: 'Wydajna sprężarka powietrza ze zbiornikiem 50 litrów. Zapewnia stabilne ciśnienie niezbędne do pracy z kluczami pneumatycznymi, pistoletami lakierniczymi, tynkarskimi oraz do przedmuchiwania układów.', pricePerDay: 45, imageUrl: 'https://images.unsplash.com/photo-1621905252507-b35492d90eb4?auto=format&fit=crop&q=80&w=800', units: [{ id: 'komp-1', label: 'Egzemplarz #1' }] }, { id: 'grp-pila', name: 'Piła stołowa do drewna', category: 'Warsztat', shortDesc: 'Precyzyjna pilarka do cięcia drewna i płyt.', fullDesc: 'Profesjonalna piła stołowa z mocnym silnikiem. Posiada regulację kąta i głębokości cięcia oraz stabilną prowadnicę równoległą. Idealna do prac stolarskich, układania paneli i docinania desek szalunkowych.', pricePerDay: 70, imageUrl: 'https://images.unsplash.com/photo-1504148455328-c376907d081c?auto=format&fit=crop&q=80&w=800', units: [{ id: 'pila-1', label: 'Egzemplarz #1' }] } ]; // --- UTILS --- const getDatesInRange = (startDate, endDate) => { const dates = []; let current = new Date(startDate); current.setHours(0,0,0,0); const end = new Date(endDate); end.setHours(0,0,0,0); while (current <= end) { dates.push(current.toISOString().split('T')[0]); current.setDate(current.getDate() + 1); } return dates; }; // --- KOMPONENT GŁÓWNY --- export default function App() { const [view, setView] = useState('catalog'); // 'catalog', 'item', 'cart', 'service', 'success' const [selectedItemGroup, setSelectedItemGroup] = useState(null); const [bookings, setBookings] = useState({ 'odk-pior-1': ['2026-04-15', '2026-04-16'], }); const [cart, setCart] = useState([]); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const goToCatalog = () => { setView('catalog'); setSelectedItemGroup(null); }; const goToItem = (itemGroup) => { setSelectedItemGroup(itemGroup); setView('item'); window.scrollTo(0,0); }; const handleBookingComplete = (itemGroup, unit, startDate, startTime, endDate, endTime, calculatedDays, totalCost) => { const dates = getDatesInRange(startDate, endDate || startDate); setCart([...cart, { itemGroup, unit, startDate, startTime, endDate: endDate || startDate, endTime, calculatedDays, totalCost, dates }]); setBookings(prev => ({ ...prev, [unit.id]: [...(prev[unit.id] || []), ...dates] })); setView('cart'); }; const handleCheckoutSuccess = () => { setCart([]); // Czyszczenie koszyka po udanej rezerwacji setView('success'); window.scrollTo(0,0); }; return (
{/* NAVBAR */} {/* MAIN CONTENT AREA */}
{view === 'catalog' && } {view === 'service' && } {view === 'item' && selectedItemGroup && ( )} {view === 'cart' && } {view === 'success' && }
); } // --- KOMPONENTY WIDOKÓW (Catalog, Service, ItemDetails) pozostają bez zmian w logice --- function Catalog({ items, onSelect }) { return (

Katalog Sprzętu

Wybierz maszynę, aby sprawdzić dostępność konkretnych egzemplarzy.

{items.map((item) => (
onSelect(item)}>
{item.name}
{item.pricePerDay} zł / doba
Dostępne sztuki: {item.units.length}
{item.category}

{item.name}

{item.shortDesc}

))}
); } function ServiceView() { return (

Profesjonalny Serwis Sprzętu

Naprawiamy to, co inni spisali na straty.

Co serwisujemy?

  • Odkurzacze piorące i budowlane (m.in. Karcher, Profi)
  • Kosy i kosiarki spalinowe (Stihl, Husqvarna)
  • Elektronarzędzia (wiertarki, szlifierki, piły)
  • Kompresory i nagrzewnice

Zasady serwisu

Przed przystąpieniem do naprawy zawsze wykonujemy darmową diagnozę i wycenę. Ty decydujesz, czy naprawiamy.

Godziny przyjęć sprzętu Pn - Pt: 8:00 - 16:00
); } function ItemDetails({ itemGroup, onBack, bookings, onBook }) { const [selectedUnitId, setSelectedUnitId] = useState(itemGroup.units[0].id); const [startDate, setStartDate] = useState(null); const [startTime, setStartTime] = useState('08:00'); const [endDate, setEndDate] = useState(null); const [endTime, setEndTime] = useState('08:00'); const [currentMonth, setCurrentMonth] = useState(new Date()); const selectedUnit = itemGroup.units.find(u => u.id === selectedUnitId); const unitBookedDates = bookings[selectedUnitId] || []; const daysInMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0).getDate(); const firstDayOfMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1).getDay(); const startOffset = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1; const handleDateClick = (day) => { const clickedDate = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day, 12, 0, 0); const dateString = clickedDate.toISOString().split('T')[0]; const today = new Date(); today.setHours(0,0,0,0); if (clickedDate < today || unitBookedDates.includes(dateString)) return; if (!startDate || (startDate && endDate)) { setStartDate(clickedDate); setEndDate(null); } else { if (clickedDate < startDate) { setStartDate(clickedDate); } else { const range = getDatesInRange(startDate, clickedDate); if (range.some(d => unitBookedDates.includes(d))) { alert('Wybrany zakres obejmuje daty, w których ten egzemplarz jest zajęty.'); setStartDate(clickedDate); } else { setEndDate(clickedDate); } } } }; const getDayStatus = (day) => { if (!day) return 'empty'; const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day, 12, 0, 0); const dateStr = date.toISOString().split('T')[0]; const today = new Date(); today.setHours(0,0,0,0); if (date < today) return 'past'; if (unitBookedDates.includes(dateStr)) return 'booked'; if (startDate && !endDate && dateStr === startDate.toISOString().split('T')[0]) return 'selected-start'; if (startDate && endDate && date >= startDate && date <= endDate) return 'selected-range'; return 'available'; }; const calculateDays = () => { if (!startDate) return 0; const startD = new Date(startDate); const [sH, sM] = startTime.split(':').map(Number); startD.setHours(sH, sM, 0, 0); const endD = new Date(endDate || startDate); const [eH, eM] = endTime.split(':').map(Number); endD.setHours(eH, eM, 0, 0); const diffMs = endD - startD; if (diffMs < 0) return 0; const totalHours = diffMs / (1000 * 60 * 60); if (totalHours === 0 && !endDate) return 1; if (totalHours <= 32) return 1; return Math.ceil((totalHours - 8) / 24); }; const calculatedDays = calculateDays(); const totalCost = calculatedDays * itemGroup.pricePerDay; const handleConfirm = () => { if (!startDate) return alert("Proszę wybrać datę z kalendarza."); if (calculatedDays === 0) return alert("Czas zwrotu nie może być wcześniejszy niż czas odbioru."); onBook(itemGroup, selectedUnit, startDate, startTime, endDate, endTime, calculatedDays, totalCost); }; return (
{itemGroup.category}

{itemGroup.name}

{itemGroup.fullDesc}

{itemGroup.units.length > 1 && (
{itemGroup.units.map(unit => ( ))}
)}
Cena wynajmu {itemGroup.pricePerDay} zł / dobę

Nie pobieramy kaucji. Płatność z dołu.

Kalendarz

{currentMonth.toLocaleDateString('pl-PL', { month: 'long', year: 'numeric' })}

{['Pn', 'Wt', 'Śr', 'Cz', 'Pt', 'So', 'Nd'].map(n =>
{n}
)} {Array.from({length: startOffset}).map((_, i) =>
)} {Array.from({length: daysInMonth}).map((_, i) => { const d = i + 1; const status = getDayStatus(d); let classes = "h-10 w-full flex items-center justify-center text-sm rounded-md transition-all cursor-pointer border border-transparent "; if(status === 'past') classes += "text-gray-300 cursor-not-allowed"; else if(status === 'booked') classes += "bg-red-50 text-red-400 line-through cursor-not-allowed"; else if(status === 'selected-start') classes += "bg-amber-500 text-white font-bold shadow-md"; else if(status === 'selected-range') classes += "bg-amber-400 text-white font-bold"; else classes += "hover:border-amber-400 hover:bg-amber-50 text-slate-700"; return
; })}

Precyzyjne godziny

); } // --- NOWY / ZMODYFIKOWANY KOMPONENT: KOSZYK Z FORMULARZEM I ZABEZPIECZENIAMI --- function CartView({ cart, onBack, onCheckoutSuccess }) { const sum = cart.reduce((acc, curr) => acc + curr.totalCost, 0); // Stany formularza const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [phone, setPhone] = useState(''); // Stany Captcha (zabezpieczenie anty-bot) const [num1, setNum1] = useState(0); const [num2, setNum2] = useState(0); const [captchaAnswer, setCaptchaAnswer] = useState(''); const [captchaError, setCaptchaError] = useState(false); // Inicjalizacja prostego równania matematycznego useEffect(() => { setNum1(Math.floor(Math.random() * 9) + 1); setNum2(Math.floor(Math.random() * 9) + 1); }, []); const handleSubmit = (e) => { e.preventDefault(); // Walidacja formatu telefonu (dokładnie 9 cyfr, bez spacji) const phoneRegex = /^[0-9]{9}$/; if (!phoneRegex.test(phone.replace(/\s/g, ''))) { alert("Proszę podać poprawny, 9-cyfrowy polski numer telefonu bez spacji i kierunkowego."); return; } // Walidacja Captcha if (parseInt(captchaAnswer) !== (num1 + num2)) { setCaptchaError(true); setCaptchaAnswer(''); // Zmieńmy pytanie po błędzie setNum1(Math.floor(Math.random() * 9) + 1); setNum2(Math.floor(Math.random() * 9) + 1); return; } setCaptchaError(false); // Sukces - tu w prawdziwej aplikacji wysłalibyśmy dane na backend console.log("Wysłano rezerwację:", { firstName, lastName, phone, cart }); onCheckoutSuccess(); }; return (
{cart.length === 0 ? (

Brak rezerwacji

Wybierz sprzęt z katalogu, aby rozpocząć.

) : (
{/* LEWA STRONA: Lista sprzętów (Zajmuje 3 kolumny) */}

Twój Koszyk

{cart.map((booking, index) => (

{booking.itemGroup.name}

{booking.unit.label}
{booking.startDate.toLocaleDateString()} {booking.startTime} → {booking.endDate.toLocaleDateString()} {booking.endTime}
{booking.calculatedDays} doby
{booking.totalCost} zł
))}
Do zapłaty (przy zwrocie): {sum} zł
{/* PRAWA STRONA: Formularz i zabezpieczenia (Zajmuje 2 kolumny) */}

Dane kontaktowe

setFirstName(e.target.value)} className="w-full p-3 rounded-xl border border-gray-300 focus:border-amber-500 focus:ring-2 focus:ring-amber-200 outline-none transition" placeholder="np. Jan" />
setLastName(e.target.value)} className="w-full p-3 rounded-xl border border-gray-300 focus:border-amber-500 focus:ring-2 focus:ring-amber-200 outline-none transition" placeholder="np. Kowalski" />
setPhone(e.target.value)} className="w-full p-3 rounded-xl border border-gray-300 focus:border-amber-500 focus:ring-2 focus:ring-amber-200 outline-none transition" placeholder="123456789" maxLength="9" minLength="9" />

Wpisz 9 cyfr bez spacji.

{/* ZABEZPIECZENIE: Informacja o wstępnej rezerwacji */}

Zabezpieczenie rezerwacji

Złożenie tej prośby tworzy rezerwację wstępną. Ze względów bezpieczeństwa ostateczne zablokowanie kalendarza nastąpi dopiero po naszym kontakcie telefonicznym.

{/* ZABEZPIECZENIE: Math Captcha */}
{num1} + {num2} = setCaptchaAnswer(e.target.value)} className={`w-20 p-2 text-center text-lg font-bold rounded-lg border focus:outline-none focus:ring-2 ${captchaError ? 'border-red-500 bg-red-50 focus:ring-red-200' : 'border-gray-300 focus:border-amber-500 focus:ring-amber-200'}`} />
{captchaError &&

Błędny wynik. Spróbuj ponownie.

}
)}
); } // --- NOWY KOMPONENT: WIDOK SUKCESU PO ZŁOŻENIU ZAMÓWIENIA --- function SuccessView({ onBack }) { return (

Rezerwacja Wstępna Przyjęta!

Dziękujemy. Otrzymaliśmy Twoje zgłoszenie. W celu weryfikacji i ostatecznego potwierdzenia wynajmu, skontaktujemy się z Tobą telefonicznie w najbliższym czasie.

Co dalej?

  • Oczekuj na połączenie z naszego biura.
  • Przygotuj dowód osobisty do okazania przy odbiorze.
  • Płatność zrealizujesz przy zwrocie maszyny.
); }