// ContactCalendar.jsx - booking calendar that replaces the contact form on the Deezer homepage. // Flow: pick date → pick time → form (name/email/company/topic/notes) → thanks. // Dostępność i rezerwacja idą przez window.BookingAPI (booking-api.js, ładowany przed tym plikiem). function ContactCalendar() { const MONTHS_PL = ['styczeń','luty','marzec','kwiecień','maj','czerwiec','lipiec','sierpień','wrzesień','październik','listopad','grudzień']; const MONTHS_PL_DM = ['stycznia','lutego','marca','kwietnia','maja','czerwca','lipca','sierpnia','września','października','listopada','grudnia']; const DOW_PL = ['Pn','Wt','Śr','Cz','Pt','Sb','Nd']; const DOW_PL_FULL = ['Niedziela','Poniedziałek','Wtorek','Środa','Czwartek','Piątek','Sobota']; const TOPICS = ['Bezpłatny audyt','Automatyzacja','Aplikacja webowa','System rezerwacji','Agent AI','Inne']; const today = React.useMemo(() => { const d = new Date(); d.setHours(0,0,0,0); return d; }, []); const [step, setStep] = React.useState(0); // 0 kalendarz, 1 formularz, 2 podziękowanie const [date, setDate] = React.useState(null); const [time, setTime] = React.useState(null); const [month, setMonth] = React.useState(() => new Date(today.getFullYear(), today.getMonth(), 1)); const [form, setForm] = React.useState({ name:'', email:'', company:'', topic:'', notes:'', _hp:'' }); // dostępność z backendu const [availability, setAvailability] = React.useState({}); // { 'YYYY-MM-DD': ['10:30', ...] } const [loadState, setLoadState] = React.useState('idle'); // 'idle'|'loading'|'ok'|'error' const [meetLink, setMeetLink] = React.useState(null); const [submitting, setSubmitting] = React.useState(false); const [bookingError, setBookingError] = React.useState(null); // 'slot_taken'|'error'|null React.useEffect(() => { if (window.lucide) lucide.createIcons(); }); // helpers const pad = (n) => String(n).padStart(2,'0'); const fmtKey = (d) => `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; const monthKey = (mDate) => `${mDate.getFullYear()}-${pad(mDate.getMonth()+1)}`; const fmtPrettyDate = (d) => `${DOW_PL_FULL[d.getDay()]}, ${d.getDate()} ${MONTHS_PL_DM[d.getMonth()]} ${d.getFullYear()}`; // pobierz dostępność dla widocznego miesiąca const loadMonth = React.useCallback((mDate) => { if (!window.BookingAPI) { setLoadState('error'); return; } setLoadState('loading'); window.BookingAPI.fetchAvailability(monthKey(mDate)) .then((res) => { setAvailability((res && res.days) || {}); setLoadState('ok'); }) .catch(() => { setLoadState('error'); }); }, []); React.useEffect(() => { loadMonth(month); }, [month, loadMonth]); const slotsForKey = (key) => availability[key] || []; const daysGrid = React.useMemo(() => { const year = month.getFullYear(), m = month.getMonth(); const offset = (new Date(year, m, 1).getDay() + 6) % 7; const daysInMonth = new Date(year, m + 1, 0).getDate(); const cells = []; for (let i = 0; i < offset; i++) cells.push(null); for (let d = 1; d <= daysInMonth; d++) cells.push(new Date(year, m, d)); return cells; }, [month]); const monthIsCurrent = month.getFullYear() === today.getFullYear() && month.getMonth() === today.getMonth(); const prevMonth = () => { const prev = new Date(month.getFullYear(), month.getMonth() - 1, 1); if (prev < new Date(today.getFullYear(), today.getMonth(), 1)) return; setMonth(prev); }; const nextMonth = () => setMonth(new Date(month.getFullYear(), month.getMonth() + 1, 1)); const selectedDateObj = date ? new Date(date) : null; const slots = date ? slotsForKey(date) : []; const pickDate = (d) => { setDate(fmtKey(d)); setTime(null); }; const pickTime = (t) => { setTime(t); setBookingError(null); setStep(1); }; const goBackToCal = () => setStep(0); const submit = (e) => { if (e) e.preventDefault(); if (submitting) return; setBookingError(null); setSubmitting(true); const payload = { date, time, name: form.name, email: form.email, company: form.company, topic: form.topic, notes: form.notes, _hp: form._hp }; window.BookingAPI.createBooking(payload) .then((res) => { setMeetLink((res && res.meetLink) || null); setStep(2); }) .catch((err) => { if (err && err.reason === 'slot_taken') { setBookingError('slot_taken'); setStep(0); setTime(null); loadMonth(month); // odśwież — slot mógł zniknąć } else { setBookingError('error'); } }) .finally(() => setSubmitting(false)); }; const restart = () => { setStep(0); setDate(null); setTime(null); setMeetLink(null); setBookingError(null); setForm({ name:'', email:'', company:'', topic:'', notes:'', _hp:'' }); loadMonth(month); }; const canSubmit = form.name.trim() && form.email.trim() && form.company.trim() && form.topic && !submitting; // ── renderery ── const renderProgress = () => (
0 ? ' is-done' : ' is-active')}> {step > 0 ? : 01} 1 ? ' is-done' : step === 1 ? ' is-active' : '')}> {step > 1 ? : 02} 03
); const renderCalendar = () => ( <>
{MONTHS_PL[month.getMonth()]} {month.getFullYear()}
{bookingError === 'slot_taken' && (
Ten termin właśnie zniknął — wybierz inny.
)} {loadState === 'error' && (
Nie udało się pobrać terminów.
)}
{DOW_PL.map((d) =>
{d}
)} {daysGrid.map((d, i) => { if (!d) return ; const key = fmtKey(d); const isPast = d < today; const hasSlots = (availability[key] || []).length > 0; const isToday = key === fmtKey(today); const isSel = key === date; const disabled = isPast || !hasSlots || loadState !== 'ok'; return ( ); })}
{selectedDateObj ? 'Godzina' : (loadState === 'loading' ? 'Ładowanie…' : 'Wybierz datę')}
{selectedDateObj ? `${DOW_PL_FULL[selectedDateObj.getDay()]}, ${selectedDateObj.getDate()} ${MONTHS_PL_DM[selectedDateObj.getMonth()]}` : ''}
{loadState === 'loading' &&
Pobieram dostępne terminy…
} {loadState === 'ok' && !selectedDateObj &&
Po wybraniu daty pojawią się dostępne godziny.
} {loadState === 'ok' && selectedDateObj && slots.length === 0 &&
Brak dostępnych godzin w wybranym dniu.
} {loadState === 'ok' && selectedDateObj && slots.map((t) => ( ))}
); const renderForm = () => ( <>
{fmtPrettyDate(selectedDateObj)} · godz. {time}
setForm((f) => ({ ...f, name: e.target.value }))} placeholder="Anna Nowak" />
setForm((f) => ({ ...f, email: e.target.value }))} placeholder="anna@firma.pl" />
setForm((f) => ({ ...f, company: e.target.value }))} placeholder="Firma sp. z o.o." />
{/* honeypot — ukryte pole; boty je wypełniają, ludzie nie */}
{bookingError === 'error' && (

Nie udało się zarezerwować. Spróbuj ponownie za chwilę.

)}

Pola oznaczone * są wymagane.

); const renderThanks = () => (

Termin zarezerwowany

Wysłałem Ci zaproszenie na maila{meetLink ? ' z linkiem do spotkania' : ''}. Do zobaczenia!

Termin
{fmtPrettyDate(selectedDateObj)} · godz. {time}
Temat
{form.topic}
Kontakt
{form.name} · {form.email}
{meetLink && <>
Spotkanie
{meetLink}
}
); return (
Umów rozmowę {renderProgress()}
{step === 0 && renderCalendar()} {step === 1 && renderForm()} {step === 2 && renderThanks()}
); } function ContactDeezer() { React.useEffect(() => { if (window.lucide) lucide.createIcons(); }); return (

Umów bezpłatny audyt.

30 minut, w których poznaję Twój biznes i mówię wprost, jak mogę Ci pomóc.

W ciągu 30 minut:
  • Poznam Twoje procesy i zobaczę, co dziś zjada Twoją marżę albo czas.
  • Sprawdzimy razem, gdzie automatyzacja albo AI mają sens - a gdzie tylko skomplikują sprawę.
  • Powiem Ci wprost, czy realnie mogę Ci pomóc - jeśli nie, oszczędzę Twój czas.
  • Wyjdziesz z konkretnym następnym krokiem.
Mail
kontakt@maciejmatera.pl
); } window.ContactCalendar = ContactCalendar; window.ContactDeezer = ContactDeezer;