// page-book.jsx — Meeting booking calendar
/* ── Mini calendar ────────────────────────────────────────────── */
function BookingCalendar({ date, onSelect }) {
const today = new Date();
today.setHours(0, 0, 0, 0);
const [viewYear, setViewYear] = React.useState(date ? date.getFullYear() : today.getFullYear());
const [viewMonth, setViewMonth] = React.useState(date ? date.getMonth() : today.getMonth());
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const DAYS = ['Su','Mo','Tu','We','Th','Fr','Sa'];
const maxDate = new Date(today);
maxDate.setMonth(maxDate.getMonth() + 3);
const goMonth = (dir) => {
let m = viewMonth + dir, y = viewYear;
if (m < 0) { m = 11; y--; }
if (m > 11) { m = 0; y++; }
setViewYear(y); setViewMonth(m);
};
const canPrev = (viewYear > today.getFullYear()) || (viewYear === today.getFullYear() && viewMonth > today.getMonth());
const firstDow = new Date(viewYear, viewMonth, 1).getDay();
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
return (
{MONTHS[viewMonth]} {viewYear}
{DAYS.map(d =>
{d}
)}
{Array.from({ length: firstDow }, (_, i) =>
)}
{Array.from({ length: daysInMonth }, (_, i) => {
const d = new Date(viewYear, viewMonth, i + 1);
const isPast = d < today;
const isSun = d.getDay() === 0;
const isFar = d > maxDate;
const disabled = isPast || isSun || isFar;
const isSel = date && d.toDateString() === date.toDateString();
const isToday = d.toDateString() === today.toDateString();
return (
);
})}
Mon – Sat · 10 AM – 7 PM IST
);
}
/* ── Main booking page ────────────────────────────────────────── */
function BookingPage() {
const [step, setStep] = React.useState(1);
const [duration, setDuration] = React.useState(null);
const [selDate, setSelDate] = React.useState(null);
const [selSlot, setSelSlot] = React.useState(null);
const [slots, setSlots] = React.useState([]);
const [loadingSlots, setLoadingSlots] = React.useState(false);
const [slotsErr, setSlotsErr] = React.useState(null);
const [form, setForm] = React.useState({ name: '', email: '', note: '' });
const [formErr, setFormErr] = React.useState({});
const [submitting, setSubmitting] = React.useState(false);
const [submitErr, setSubmitErr] = React.useState(null);
const [confirmed, setConfirmed] = React.useState(null);
const API = (window.BOOKING_API_URL || '').replace(/\/$/, '');
/* ── Helpers ── */
const fmtISO = (d) => {
if (!d) return '';
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
};
const fmtTime = (iso) => {
if (!iso) return '';
return new Date(iso).toLocaleTimeString('en-IN', {
hour: 'numeric', minute: '2-digit', hour12: true, timeZone: 'Asia/Kolkata'
});
};
const fmtDateLong = (d) => {
if (!d) return '';
return d.toLocaleDateString('en-IN', {
weekday: 'long', day: 'numeric', month: 'long', year: 'numeric'
});
};
/* ── Fetch slots when date or duration changes ── */
React.useEffect(() => {
if (!selDate || !duration) return;
setLoadingSlots(true);
setSlotsErr(null);
setSelSlot(null);
setSlots([]);
fetch(`${API}/api/booking.php?action=slots&date=${fmtISO(selDate)}&duration=${duration}`)
.then(r => { if (!r.ok) throw new Error('server'); return r.json(); })
.then(data => { setSlots(data.slots || []); setLoadingSlots(false); })
.catch(() => { setSlotsErr('Could not load available times. Please try again.'); setLoadingSlots(false); });
}, [selDate, duration]);
/* ── Handlers ── */
const pickDuration = (d) => { setDuration(d); setSelSlot(null); setStep(2); };
const pickDate = (d) => { setSelDate(d); setSelSlot(null); };
const update = (k) => (e) => setForm({ ...form, [k]: e.target.value });
const validate = () => {
const e = {};
if (!form.name.trim()) e.name = 'Required';
if (!form.email.includes('@')) e.email = 'Valid email required';
return e;
};
const submitBooking = async (e) => {
e.preventDefault();
const errs = validate();
setFormErr(errs);
if (Object.keys(errs).length > 0) return;
setSubmitting(true);
setSubmitErr(null);
try {
const res = await fetch(`${API}/api/booking.php`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: form.name.trim(), email: form.email.trim(), note: form.note.trim(), start: selSlot.start, end: selSlot.end, duration })
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Booking failed');
setConfirmed(data);
setStep(4);
} catch (err) {
setSubmitErr(err.message || 'Something went wrong. Please try again.');
} finally {
setSubmitting(false);
}
};
/* ── Duration options ── */
const DURATIONS = [
{ mins: 15, label: 'Quick Chat', icon: '⚡' },
{ mins: 30, label: 'Consultation', icon: '☕' },
{ mins: 60, label: 'Full Meeting', icon: '📋' },
];
/* ── Step labels ── */
const STEPS = ['Duration', 'Date & Time', 'Your Details'];
/* ── Confirmation screen ── */
if (step === 4 && confirmed) {
return (
You’re booked.
A calendar invite with the Google Meet link has been sent to your inbox. We look forward to speaking with you.
Meeting confirmed
Check your inbox for the calendar invite.
{[
['With', 'Satnam Singh · Harjog Solutions'],
['Date', fmtDateLong(selDate)],
['Time', `${fmtTime(selSlot && selSlot.start)} IST · ${duration} min`],
['For', `${form.name} · ${form.email}`],
].map(([label, val]) => (
{label}
{val}
))}
{confirmed.meetLink && (
)}
);
}
return (
Book a meeting.
Schedule time directly on Satnam’s calendar. Every booking includes a Google Meet link sent to your inbox.
{/* ── Step indicator ── */}
{STEPS.map((s, i) => (
i + 1 ? ' done' : ''}`}>
{step > i + 1 ? '✓' : i + 1}
{s}
))}
{/* ── Step 1: Duration ── */}
{step === 1 && (
Choose meeting type
{DURATIONS.map((d, i) => (
pickDuration(d.mins)}>
{d.icon}
{d.mins}
minutes
{d.label}
))}
)}
{/* ── Step 2: Date & Time ── */}
{step === 2 && (
{!selDate ? (
Select a date to see available times
) : (
<>
{fmtDateLong(selDate)}
{loadingSlots &&
Loading available times…
}
{slotsErr &&
{slotsErr}
}
{!loadingSlots && !slotsErr && slots.length === 0 && (
No slots available on this day. Try another date.
)}
{!loadingSlots && !slotsErr && slots.length > 0 && (
{slots.map(s => (
))}
)}
>
)}
)}
{/* ── Step 3: Details ── */}
{step === 3 && (
)}
);
}
Object.assign(window, { BookingPage });