@unified-api/react-calendar-scheduler
Version:
A React calendar scheduler component for booking appointments
155 lines (151 loc) • 11.3 kB
JavaScript
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var react = require('react');
const CalendarScheduler = ({ busy = [], available = [], connection_id, api_token, duration = 60, months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], type = 'Appointment', bookFn, defaultTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone, work_days = [1, 2, 3, 4, 5], // Monday to Friday
dc = 'us', start_hour = 9, end_hour = 17, }) => {
const [selectedDate, setSelectedDate] = react.useState(new Date());
const [selectedTime, setSelectedTime] = react.useState('');
const [currentMonth, setCurrentMonth] = react.useState(new Date());
const [showBookingForm, setShowBookingForm] = react.useState(false);
const [busySlots, setBusySlots] = react.useState(busy);
const [availableSlots, setAvailableSlots] = react.useState(available);
const [loading, setLoading] = react.useState(false);
// Fetch busy/available slots from API if connection_id is provided
react.useEffect(() => {
const fetchCalendarData = async () => {
if (connection_id && api_token) {
setLoading(true);
try {
const baseUrl = dc === 'eu' ? 'https://eu.unified.to' : dc === 'au' ? 'https://au.unified.to' : 'https://unified.to';
const response = await fetch(`${baseUrl}/calendar/busy`, {
method: 'POST',
headers: {
Authorization: `Bearer ${api_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
connection_id,
start_at: new Date().toISOString(),
end_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
}),
});
const data = await response.json();
setBusySlots(data.busy || []);
}
catch (error) {
console.error('Error fetching calendar data:', error);
}
finally {
setLoading(false);
}
}
};
fetchCalendarData();
}, [connection_id, api_token, dc]);
// Generate calendar days for current month
const calendarDays = react.useMemo(() => {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startDate = new Date(firstDay);
// Start from the beginning of the week
startDate.setDate(startDate.getDate() - startDate.getDay());
const days = [];
const endDate = new Date(lastDay);
endDate.setDate(endDate.getDate() + (6 - endDate.getDay()));
for (let date = new Date(startDate); date <= endDate; date.setDate(date.getDate() + 1)) {
days.push(new Date(date));
}
return days;
}, [currentMonth]);
// Generate available time slots for selected date
const timeSlots = react.useMemo(() => {
if (!selectedDate)
return [];
const dayOfWeek = selectedDate.getDay();
if (!work_days.includes(dayOfWeek))
return [];
const slots = [];
const dateStr = selectedDate.toISOString().split('T')[0];
for (let hour = start_hour; hour < end_hour; hour++) {
for (let minute = 0; minute < 60; minute += duration) {
const timeStr = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
const slotStart = new Date(`${dateStr}T${timeStr}:00`);
const slotEnd = new Date(slotStart.getTime() + duration * 60 * 1000);
// Check if slot conflicts with busy times
const isConflict = busySlots.some((busy) => {
const busyStart = new Date(busy.start_at);
const busyEnd = new Date(busy.end_at);
return slotStart < busyEnd && slotEnd > busyStart;
});
// Check if slot is in available times (if specified)
const isAvailable = availableSlots.length === 0 ||
availableSlots.some((avail) => {
const availStart = new Date(avail.start_at);
const availEnd = new Date(avail.end_at);
return slotStart >= availStart && slotEnd <= availEnd;
});
if (!isConflict && isAvailable) {
slots.push(timeStr);
}
}
}
return slots;
}, [selectedDate, busySlots, availableSlots, work_days, start_hour, end_hour, duration]);
const handleDateSelect = (date) => {
setSelectedDate(date);
setSelectedTime('');
};
const handleTimeSelect = (time) => {
setSelectedTime(time);
setShowBookingForm(true);
};
const handleBooking = (formData) => {
if (bookFn) {
const bookingData = {
...formData,
date: selectedDate.toISOString().split('T')[0],
time: selectedTime,
duration,
timezone: defaultTimezone,
};
bookFn(bookingData);
}
setShowBookingForm(false);
setSelectedTime('');
};
const isDateAvailable = (date) => {
const dayOfWeek = date.getDay();
return work_days.includes(dayOfWeek);
};
const isCurrentMonth = (date) => {
return date.getMonth() === currentMonth.getMonth();
};
return (jsxRuntime.jsxs("div", { className: "calendar-scheduler", children: [jsxRuntime.jsxs("div", { className: "calendar-scheduler-grid", children: [jsxRuntime.jsxs("div", { className: "calendar-section", children: [jsxRuntime.jsxs("div", { className: "calendar-header", children: [jsxRuntime.jsx("button", { onClick: () => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1)), children: "\u2190" }), jsxRuntime.jsxs("h2", { children: [months[currentMonth.getMonth()], " ", currentMonth.getFullYear()] }), jsxRuntime.jsx("button", { onClick: () => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1)), children: "\u2192" })] }), jsxRuntime.jsxs("div", { className: "calendar-grid", children: [jsxRuntime.jsx("div", { className: "calendar-days-header", children: days.map((day) => (jsxRuntime.jsx("div", { children: day.slice(0, 3) }, day))) }), jsxRuntime.jsx("div", { className: "calendar-days-grid", children: calendarDays.map((date, index) => {
const isAvailable = isDateAvailable(date);
const isCurrent = isCurrentMonth(date);
const isSelected = selectedDate && date.toDateString() === selectedDate.toDateString();
return (jsxRuntime.jsx("button", { onClick: () => isAvailable && isCurrent && handleDateSelect(date), disabled: !isAvailable || !isCurrent, className: `calendar-day-button ${isSelected ? 'selected' : ''} ${!isCurrent ? 'other-month' : ''}`, children: date.getDate() }, index));
}) })] })] }), jsxRuntime.jsxs("div", { className: "time-slots-section", children: [jsxRuntime.jsx("h3", { children: selectedDate ? `Available times for ${selectedDate.toLocaleDateString()}` : 'Select a date' }), loading && (jsxRuntime.jsxs("div", { className: "loading-container", children: [jsxRuntime.jsx("div", { className: "loading-spinner" }), jsxRuntime.jsx("p", { className: "loading-text", children: "Loading available times..." })] })), selectedDate && !loading && (jsxRuntime.jsx("div", { className: "time-slots-grid", children: timeSlots.length > 0 ? (timeSlots.map((time) => (jsxRuntime.jsx("button", { onClick: () => handleTimeSelect(time), className: "time-slot-button", children: time }, time)))) : (jsxRuntime.jsx("p", { className: "no-times-message", children: "No available times for this date" })) }))] })] }), showBookingForm && (jsxRuntime.jsx("div", { className: "modal-overlay", children: jsxRuntime.jsxs("div", { className: "modal-content", children: [jsxRuntime.jsxs("h3", { className: "modal-title", children: ["Book ", type] }), jsxRuntime.jsx(BookingForm, { selectedDate: selectedDate, selectedTime: selectedTime, duration: duration, onSubmit: handleBooking, onCancel: () => setShowBookingForm(false) })] }) }))] }));
};
const BookingForm = ({ selectedDate, selectedTime, duration, onSubmit, onCancel }) => {
const [formData, setFormData] = react.useState({
name: '',
email: '',
notes: '',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
});
const handleSubmit = () => {
onSubmit(formData);
};
const handleChange = (e) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
return (jsxRuntime.jsxs("div", { className: "booking-form", children: [jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { className: "form-label", children: "Date & Time" }), jsxRuntime.jsxs("p", { className: "form-text", children: [selectedDate.toLocaleDateString(), " at ", selectedTime, " (", duration, " minutes)"] })] }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { className: "form-label", children: "Name *" }), jsxRuntime.jsx("input", { type: "text", name: "name", required: true, value: formData.name, onChange: handleChange, className: "form-input" })] }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { className: "form-label", children: "Email *" }), jsxRuntime.jsx("input", { type: "email", name: "email", required: true, value: formData.email, onChange: handleChange, className: "form-input" })] }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { className: "form-label", children: "Timezone" }), jsxRuntime.jsx("input", { type: "text", name: "timezone", value: formData.timezone, onChange: handleChange, className: "form-input" })] }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { className: "form-label", children: "Notes" }), jsxRuntime.jsx("textarea", { name: "notes", rows: 3, value: formData.notes, onChange: handleChange, className: "form-textarea" })] }), jsxRuntime.jsxs("div", { className: "form-buttons", children: [jsxRuntime.jsx("button", { onClick: handleSubmit, disabled: !formData.name || !formData.email, className: "btn btn-primary", children: "Book Appointment" }), jsxRuntime.jsx("button", { type: "button", onClick: onCancel, className: "btn btn-secondary", children: "Cancel" })] })] }));
};
exports.CalendarScheduler = CalendarScheduler;
//# sourceMappingURL=index.js.map