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 */}
WypożyczalniaPRO
Sprzęt
setView('service')} className={`hover:text-amber-400 transition ${view === 'service' ? 'text-amber-500' : ''}`}>Serwis i Naprawa
Regulamin
setView('cart')}
className="bg-amber-500 hover:bg-amber-600 text-slate-900 px-4 py-2 rounded-lg font-semibold flex items-center transition"
>
Rezerwacje {cart.length > 0 && `(${cart.length})`}
setView('cart')} className="mr-4 relative">
{cart.length > 0 && (
{cart.length}
)}
setMobileMenuOpen(!mobileMenuOpen)}>
{mobileMenuOpen ? : }
{mobileMenuOpen && (
{ goToCatalog(); setMobileMenuOpen(false); }} className="block w-full text-left px-3 py-2 rounded-md text-base font-medium text-white hover:bg-slate-700">Sprzęt
{ setView('service'); setMobileMenuOpen(false); }} className="block w-full text-left px-3 py-2 rounded-md text-base font-medium text-white hover:bg-slate-700">Serwis i Naprawa
Regulamin
)}
{/* 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.pricePerDay} zł / doba
Dostępne sztuki: {item.units.length}
{item.category}
{item.name}
{item.shortDesc}
Sprawdź kalendarz
))}
);
}
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
Zadzwoń do serwisu
);
}
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 (
Powrót do listy
{itemGroup.category}
{itemGroup.name}
{itemGroup.fullDesc}
{itemGroup.units.length > 1 && (
Wybierz konkretny egzemplarz:
{itemGroup.units.map(unit => (
{ setSelectedUnitId(unit.id); setStartDate(null); setEndDate(null); }} className={`px-4 py-2 rounded-lg text-sm font-medium transition ${selectedUnitId === unit.id ? 'bg-amber-500 text-slate-900 shadow-md' : 'bg-gray-100 text-slate-600 hover:bg-gray-200'}`}>
{unit.label}
))}
)}
Cena wynajmu
{itemGroup.pricePerDay} zł / dobę
Nie pobieramy kaucji. Płatność z dołu.
Kalendarz
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1))} className="px-3 py-1.5 border rounded-lg hover:bg-slate-50 text-sm font-medium">← Poprz.
{currentMonth.toLocaleDateString('pl-PL', { month: 'long', year: 'numeric' })}
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1))} className="px-3 py-1.5 border rounded-lg hover:bg-slate-50 text-sm font-medium">Nast. →
{['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
handleDateClick(d)} className={classes}>{d}
;
})}
Precyzyjne godziny
Odbiór (Od)
setStartTime(e.target.value)} className="w-full p-2.5 rounded-lg border border-gray-300 bg-white text-sm font-medium">
{Array.from({length: 12}).map((_, i) => {`${i + 8}:00`.padStart(5, '0')} )}
Zwrot (Do)
setEndTime(e.target.value)} className="w-full p-2.5 rounded-lg border border-gray-300 bg-white text-sm font-medium">
{Array.from({length: 12}).map((_, i) => {`${i + 8}:00`.padStart(5, '0')} )}
0) ? 'bg-amber-500 text-slate-900 hover:bg-amber-400 shadow-md' : 'bg-slate-800 text-slate-500 cursor-not-allowed'}`}>
Potwierdź tę rezerwację
);
}
// --- 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 (
Dobierz kolejny sprzęt
{cart.length === 0 ? (
Brak rezerwacji
Wybierz sprzęt z katalogu, aby rozpocząć.
Przejdź do sprzętu
) : (
{/* 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) */}
)}
);
}
// --- 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.
Wróć do strony głównej
);
}