Overview
A custom booking flow typically involves:- Display available meeting types
- Show available time slots
- Collect attendee information
- Create the booking
- Show confirmation
Step 1: Fetch Meeting Types
Get available meeting types for a user:Copy
async function getMeetingTypes(userId) {
const response = await fetch(
'https://api.meetergo.com/v4/meeting-type',
{
headers: {
'Authorization': `Bearer ${API_KEY}`,
'x-meetergo-api-user-id': userId
}
}
);
return response.json();
}
Display Meeting Types
Copy
function MeetingTypeSelector({ meetingTypes, onSelect }) {
return (
<div className="meeting-types">
{meetingTypes.map(type => (
<button
key={type.id}
onClick={() => onSelect(type)}
className="meeting-type-card"
>
<h3>{type.name}</h3>
<p>{type.duration} minutes</p>
{type.description && <p>{type.description}</p>}
</button>
))}
</div>
);
}
Step 2: Display Available Slots
Fetch and display available time slots:Copy
async function getAvailableSlots(userId, meetingTypeId, startDate, endDate) {
const params = new URLSearchParams({
startDate,
endDate,
meetingTypeId
});
const response = await fetch(
`https://api.meetergo.com/v4/user/${userId}/bookable-windows?${params}`,
{
headers: {
'Authorization': `Bearer ${API_KEY}`,
'x-meetergo-api-user-id': userId
}
}
);
const data = await response.json();
return data.windows;
}
Group Slots by Day
Copy
function groupSlotsByDay(slots) {
const grouped = {};
for (const slot of slots) {
const date = new Date(slot);
const dayKey = date.toISOString().split('T')[0];
if (!grouped[dayKey]) {
grouped[dayKey] = [];
}
grouped[dayKey].push(slot);
}
return grouped;
}
Calendar Component
Copy
function SlotPicker({ slots, onSelect, selectedSlot }) {
const groupedSlots = groupSlotsByDay(slots);
const days = Object.keys(groupedSlots).sort();
return (
<div className="slot-picker">
{days.map(day => (
<div key={day} className="day-column">
<h4>{formatDate(day)}</h4>
<div className="slots">
{groupedSlots[day].map(slot => (
<button
key={slot}
onClick={() => onSelect(slot)}
className={`slot ${selectedSlot === slot ? 'selected' : ''}`}
>
{formatTime(slot)}
</button>
))}
</div>
</div>
))}
</div>
);
}
function formatDate(dateString) {
return new Date(dateString).toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric'
});
}
function formatTime(isoString) {
return new Date(isoString).toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit'
});
}
Step 3: Collect Attendee Information
Copy
function BookingForm({ onSubmit, isLoading }) {
const [formData, setFormData] = useState({
email: '',
firstname: '',
lastname: '',
phone: '',
company: '',
notes: ''
});
const handleChange = (e) => {
setFormData(prev => ({
...prev,
[e.target.name]: e.target.value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(formData);
};
return (
<form onSubmit={handleSubmit} className="booking-form">
<div className="form-row">
<input
type="email"
name="email"
placeholder="Email *"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<div className="form-row form-row-split">
<input
type="text"
name="firstname"
placeholder="First name"
value={formData.firstname}
onChange={handleChange}
/>
<input
type="text"
name="lastname"
placeholder="Last name"
value={formData.lastname}
onChange={handleChange}
/>
</div>
<div className="form-row">
<input
type="tel"
name="phone"
placeholder="Phone"
value={formData.phone}
onChange={handleChange}
/>
</div>
<div className="form-row">
<input
type="text"
name="company"
placeholder="Company"
value={formData.company}
onChange={handleChange}
/>
</div>
<div className="form-row">
<textarea
name="notes"
placeholder="Notes or questions"
value={formData.notes}
onChange={handleChange}
rows={3}
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Booking...' : 'Confirm Booking'}
</button>
</form>
);
}
Step 4: Create the Booking
Copy
async function createBooking({ meetingTypeId, start, hostIds, attendee }) {
const response = await fetch('https://api.meetergo.com/v4/booking', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
meetingTypeId,
start,
hostIds,
attendee: {
email: attendee.email,
firstname: attendee.firstname,
lastname: attendee.lastname,
phone: attendee.phone,
customFields: {
company: attendee.company,
notes: attendee.notes
}
}
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return response.json();
}
Step 5: Complete Flow Component
Copy
function CustomBookingFlow({ userId, hostIds }) {
const [step, setStep] = useState('select-type');
const [meetingTypes, setMeetingTypes] = useState([]);
const [selectedType, setSelectedType] = useState(null);
const [slots, setSlots] = useState([]);
const [selectedSlot, setSelectedSlot] = useState(null);
const [booking, setBooking] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// Load meeting types on mount
useEffect(() => {
getMeetingTypes(userId).then(setMeetingTypes);
}, [userId]);
// Load slots when meeting type selected
useEffect(() => {
if (selectedType) {
const today = new Date().toISOString().split('T')[0];
const nextMonth = new Date();
nextMonth.setMonth(nextMonth.getMonth() + 1);
const endDate = nextMonth.toISOString().split('T')[0];
getAvailableSlots(userId, selectedType.id, today, endDate)
.then(setSlots);
}
}, [selectedType, userId]);
const handleTypeSelect = (type) => {
setSelectedType(type);
setStep('select-time');
};
const handleSlotSelect = (slot) => {
setSelectedSlot(slot);
setStep('enter-details');
};
const handleFormSubmit = async (attendee) => {
setIsLoading(true);
setError(null);
try {
const result = await createBooking({
meetingTypeId: selectedType.id,
start: selectedSlot,
hostIds,
attendee
});
setBooking(result);
setStep('confirmation');
} catch (err) {
setError(err.message);
// If slot unavailable, go back to time selection
if (err.message.includes('no longer available')) {
setStep('select-time');
// Refresh available slots
const today = new Date().toISOString().split('T')[0];
const nextMonth = new Date();
nextMonth.setMonth(nextMonth.getMonth() + 1);
getAvailableSlots(userId, selectedType.id, today, nextMonth.toISOString().split('T')[0])
.then(setSlots);
}
} finally {
setIsLoading(false);
}
};
return (
<div className="booking-flow">
{/* Progress indicator */}
<div className="steps">
<span className={step === 'select-type' ? 'active' : ''}>1. Meeting</span>
<span className={step === 'select-time' ? 'active' : ''}>2. Time</span>
<span className={step === 'enter-details' ? 'active' : ''}>3. Details</span>
<span className={step === 'confirmation' ? 'active' : ''}>4. Done</span>
</div>
{error && <div className="error">{error}</div>}
{step === 'select-type' && (
<MeetingTypeSelector
meetingTypes={meetingTypes}
onSelect={handleTypeSelect}
/>
)}
{step === 'select-time' && (
<>
<button onClick={() => setStep('select-type')}>← Back</button>
<h2>Select a time for {selectedType.name}</h2>
<SlotPicker
slots={slots}
selectedSlot={selectedSlot}
onSelect={handleSlotSelect}
/>
</>
)}
{step === 'enter-details' && (
<>
<button onClick={() => setStep('select-time')}>← Back</button>
<h2>Enter your details</h2>
<p>
{selectedType.name} on {formatDate(selectedSlot)} at {formatTime(selectedSlot)}
</p>
<BookingForm
onSubmit={handleFormSubmit}
isLoading={isLoading}
/>
</>
)}
{step === 'confirmation' && (
<div className="confirmation">
<h2>Booking Confirmed!</h2>
<p>
Your {selectedType.name} is scheduled for{' '}
{new Date(selectedSlot).toLocaleString()}
</p>
<p>A confirmation email has been sent.</p>
<p className="booking-id">
Booking ID: {booking.appointmentId}
</p>
</div>
)}
</div>
);
}
Styling
Copy
.booking-flow {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.steps {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 1px solid #e5e7eb;
}
.steps span {
color: #9ca3af;
font-size: 14px;
}
.steps span.active {
color: #2563eb;
font-weight: 600;
}
.meeting-type-card {
display: block;
width: 100%;
padding: 20px;
margin-bottom: 10px;
border: 2px solid #e5e7eb;
border-radius: 8px;
background: white;
text-align: left;
cursor: pointer;
transition: border-color 0.2s;
}
.meeting-type-card:hover {
border-color: #2563eb;
}
.slot-picker {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 20px;
}
.day-column h4 {
font-size: 14px;
margin-bottom: 10px;
}
.slot {
display: block;
width: 100%;
padding: 8px;
margin-bottom: 5px;
border: 1px solid #e5e7eb;
border-radius: 4px;
background: white;
cursor: pointer;
}
.slot:hover {
background: #f3f4f6;
}
.slot.selected {
background: #2563eb;
color: white;
border-color: #2563eb;
}
.booking-form input,
.booking-form textarea {
width: 100%;
padding: 12px;
border: 1px solid #e5e7eb;
border-radius: 6px;
font-size: 16px;
}
.form-row {
margin-bottom: 15px;
}
.form-row-split {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.booking-form button {
width: 100%;
padding: 14px;
background: #2563eb;
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
}
.booking-form button:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.error {
padding: 12px;
background: #fef2f2;
color: #dc2626;
border-radius: 6px;
margin-bottom: 20px;
}
.confirmation {
text-align: center;
padding: 40px 20px;
}
Best Practices
Handle race conditions - Slots can be booked while the user fills the form
Show loading states - Provide feedback during API calls
Validate client-side - Check required fields before API call
Allow going back - Let users change their selections
Display timezone - Show times in the user’s local timezone