// --- Debounce Utility Helper ---
function debounce(func, wait) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
function getAppointmentDateTimes(selectedDate, selectedTime, selectedTimezone, startISO, endISO) {
if (startISO && endISO) {
return {
startDateTime: startISO,
endDateTime: endISO,
timeZone: selectedTimezone
};
}
if (!selectedDate || !selectedTime) return null;
const dateParts = selectedDate.match(/([A-Za-z]+)\s+(\d+),\s+(\d{4})/);
if (!dateParts) return null;
const months = {
'January': 0, 'February': 1, 'March': 2, 'April': 3, 'May': 4, 'June': 5,
'July': 6, 'August': 7, 'September': 8, 'October': 9, 'November': 10, 'December': 11
};
const monthIndex = months[dateParts[1]];
const day = parseInt(dateParts[2], 10);
const year = parseInt(dateParts[3], 10);
const timeParts = selectedTime.match(/(\d+):(\d+)\s*(AM|PM)/i);
if (!timeParts) return null;
let hours = parseInt(timeParts[1], 10);
const minutes = parseInt(timeParts[2], 10);
const ampm = timeParts[3].toUpperCase();
if (ampm === 'PM' && hours < 12) hours += 12;
if (ampm === 'AM' && hours === 12) hours = 0;
const startDate = new Date(year, monthIndex, day, hours, minutes, 0);
const pad = (num) => num.toString().padStart(2, '0');
const startStr = `${startDate.getFullYear()}-${pad(startDate.getMonth() + 1)}-${pad(startDate.getDate())}T${pad(startDate.getHours())}:${pad(startDate.getMinutes())}:00`;
const slotDuration = window.apiSlotDuration || (typeof GOOGLE_CALENDAR_CONFIG !== 'undefined' ? GOOGLE_CALENDAR_CONFIG.availableSlotTime : null) || 30;
const endDate = new Date(startDate.getTime() + Number(slotDuration) * 60 * 1000);
const endStr = `${endDate.getFullYear()}-${pad(endDate.getMonth() + 1)}-${pad(endDate.getDate())}T${pad(endDate.getHours())}:${pad(endDate.getMinutes())}:00`;
return {
startDateTime: startStr,
endDateTime: endStr,
timeZone: selectedTimezone
};
}
function humanizeStringOrArray(val) {
if (!val) return val;
if (Array.isArray(val)) {
return val.map(humanizeStringOrArray);
}
if (typeof val === 'string') {
return val.replace(/[_-]/g, ' ');
}
return val;
}
/**
* Creates a Google Calendar Event from the multi-step form data.
* @param {Object} formData Raw form data (arrays/strings).
* @param {Object} contactInfo Contact/appointment data.
* @param {Function} onSuccess Callback on successful creation.
* @param {Function} onFailure Callback with string error message on failure.
*/
window.createGoogleCalendarEvent = function (formData, contactInfo, onSuccess, onFailure) {
const clientTimezone = window.adminCalendarTimezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
const userTimezone = contactInfo.selectedTimezone || window.userCalendarTimezone;
const dateTimes = getAppointmentDateTimes(contactInfo.selectedDate, contactInfo.selectedTime, contactInfo.selectedTimezone, contactInfo.startISO, contactInfo.endISO);
if (!dateTimes) {
onFailure("Could not retrieve appointment date and time. Please re-select your slot.");
return;
}
const payload = {
firstName: contactInfo.firstName,
lastName: contactInfo.lastName,
email: contactInfo.email,
company: contactInfo.company,
website: contactInfo.website,
role: contactInfo.role,
audienceDetails: contactInfo.audienceDetails,
budgetContext: contactInfo.budgetContext,
create: humanizeStringOrArray(formData.create || ''),
goals: humanizeStringOrArray(formData.goals || []),
audience: humanizeStringOrArray(formData.audience || []),
timeline: formData.timeline || '',
planning_process: humanizeStringOrArray(formData.planning_process || ''),
startDateTime: dateTimes.startDateTime,
endDateTime: dateTimes.endDateTime,
timeZone: dateTimes.timeZone,
calendarId: GOOGLE_CALENDAR_CONFIG.calendarId,
footprint: window.location.origin || window.location.href
};
// Step 1: Fetch AI-generated description, then proceed to book
function proceedWithBooking(descriptionStr) {
if (GOOGLE_CALENDAR_CONFIG.useGoogleAppsScript) {
if (!GOOGLE_CALENDAR_CONFIG.googleAppsScriptUrlForNew) {
console.warn("Google Apps Script URL is empty. Simulating API event creation...");
setTimeout(() => {
onSuccess("https://meet.google.com/mock-meet-link");
}, 1500);
return;
}
// Get a fresh token for the final submission to avoid duplicate/used token errors
getRecaptchaToken().then(newToken => {
const payloadWithDescription = Object.assign({}, payload, {
description: descriptionStr,
recaptchaToken: newToken || payload.recaptchaToken
});
fetch(GOOGLE_CALENDAR_CONFIG.googleAppsScriptUrlForNew, {
method: 'POST',
body: JSON.stringify(payloadWithDescription)
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return response.json();
})
.then(result => {
if (result && result.status === 'success') {
onSuccess(result.meetLink);
} else {
onFailure(result && result.message ? result.message : "Failed to book event.");
}
})
.catch((err) => {
console.error("Google Calendar Apps Script Error:", err);
onFailure("Failed to book appointment. ");
console.error("Please make sure the Apps Script is deployed with access set to 'Anyone'.");
});
});
} else {
// Option B: Direct Google Calendar API
if (!GOOGLE_CALENDAR_CONFIG.accessToken) {
console.warn("Google Calendar OAuth Access Token is empty. Simulating direct API event creation...");
setTimeout(() => {
onSuccess("https://meet.google.com/mock-meet-link");
}, 1500);
return;
}
const eventPayload = {
summary: `Inquiry: ${payload.firstName} ${payload.lastName} (${payload.company})`,
description: descriptionStr,
start: {
dateTime: payload.startDateTime,
timeZone: payload.timeZone
},
end: {
dateTime: payload.endDateTime,
timeZone: payload.timeZone
},
attendees: [
{ email: payload.email }
],
conferenceData: {
createRequest: {
requestId: "meet_" + new Date().getTime(),
conferenceSolutionKey: {
type: "hangoutsMeet"
}
}
}
};
const url = `https://www.googleapis.com/calendar/v3/calendars/${GOOGLE_CALENDAR_CONFIG.calendarId}/events?conferenceDataVersion=1`;
fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${GOOGLE_CALENDAR_CONFIG.accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(eventPayload)
})
.then(async (response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error?.message || `HTTP error ${response.status}`);
}
return response.json();
})
.then((data) => {
onSuccess(data.hangoutLink);
})
.catch((err) => {
console.error("Google Calendar API Error:", err);
onFailure(`Failed to book appointment: ${err.message}`);
});
}
}
function runBookingPipeline() {
// Call the AI Description Apps Script endpoint first to generate a human-readable description
const aiDescriptionUrl = GOOGLE_CALENDAR_CONFIG.googleAppsScriptUrlForAiDescription;
if (aiDescriptionUrl) {
fetch(aiDescriptionUrl, {
method: 'POST',
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(result => {
// console.group('%cš„ AI Description Response', 'color: #6366f1; font-weight: bold;');
const descriptionStr = (result && result.description)
? result.description
: JSON.stringify(payload, null, 2);
if (!result || !result.description) {
console.warn('ā ļø No description in response ā falling back to raw JSON');
}
// console.groupEnd();
proceedWithBooking(descriptionStr);
})
.catch(err => {
console.warn('%cā AI Description fetch failed ā falling back to raw JSON', 'color: #ef4444;', err);
proceedWithBooking(JSON.stringify(payload, null, 2));
});
} else {
proceedWithBooking(JSON.stringify(payload, null, 2));
}
}
// Step 0: Resolve reCAPTCHA v3 token first, then run the booking pipeline
getRecaptchaToken().then(token => {
if (token) {
payload.recaptchaToken = token;
}
runBookingPipeline();
});
};
// --- Google reCAPTCHA v3 Helper Functions ---
document.addEventListener('DOMContentLoaded', () => {
const siteKey = GOOGLE_CALENDAR_CONFIG.recaptchaSiteKey;
if (siteKey) {
const script = document.createElement('script');
script.src = `https://www.google.com/recaptcha/api.js?render=${siteKey}`;
document.head.appendChild(script);
}
});
function getRecaptchaToken() {
const siteKey = GOOGLE_CALENDAR_CONFIG.recaptchaSiteKey;
if (!siteKey) return Promise.resolve('');
return new Promise((resolve) => {
const timeoutId = setTimeout(() => {
console.warn("reCAPTCHA execution timed out (Safety JS Timeout).");
resolve('');
}, 8000);
if (typeof grecaptcha === 'undefined') {
clearTimeout(timeoutId);
resolve('');
return;
}
grecaptcha.ready(() => {
grecaptcha.execute(siteKey, { action: 'submit_booking' })
.then((token) => {
clearTimeout(timeoutId);
resolve(token);
})
.catch((err) => {
console.warn("reCAPTCHA execution error:", err);
clearTimeout(timeoutId);
resolve('');
});
});
});
}
// āāā DOMContentLoaded Listener (Form controller) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
document.addEventListener('DOMContentLoaded', function () {
const stepwrp = document.querySelector('.step-form-wrapper');
if (stepwrp) {
// --- DOM Elements Cache ---
const stepper = stepwrp.querySelector('.sf-stepper');
const steps = stepwrp.querySelectorAll('.sf-step');
const lines = stepwrp.querySelectorAll('.sf-step-line');
const form = stepwrp.querySelector('#sf-multistep-form');
const successScreen = stepwrp.querySelector('#sf-success-screen');
const calendarGrid = stepwrp.querySelector('#sf-calendar-grid');
const monthYearLabel = stepwrp.querySelector('#sf-cal-month-year');
const timeSlotsContainer = stepwrp.querySelector('#sf-time-slots-container');
const appointmentValueText = stepwrp.querySelector('#sf-appointment-value-text');
const appointmentInput = stepwrp.querySelector('#sf-appointment-input');
const stepContents = stepwrp.querySelectorAll('.sf-step-content');
const loadingOverlay = stepwrp.querySelector('#sf-loading-overlay');
const prevBtn = stepwrp.querySelector('#sf-prev-btn');
const nextBtn = stepwrp.querySelector('#sf-next-btn');
const restartBtn = stepwrp.querySelector('#sf-restart-btn');
// --- Configuration & Constants ---
const TOTAL_STEPS = steps.length;
const TRANSITION_DURATION = 200; // ms
const ANIMATION_DELAY = 30; // ms
function setSubmitLoading(isLoading) {
if (isLoading) {
nextBtn.textContent = 'Submitting...';
nextBtn.classList.add('disabled');
nextBtn.style.pointerEvents = 'none';
nextBtn.style.opacity = '0.7';
prevBtn.style.pointerEvents = 'none';
prevBtn.style.opacity = '0.5';
if (loadingOverlay) {
loadingOverlay.classList.add('active');
}
} else {
nextBtn.textContent = 'Submit Inquiry';
nextBtn.classList.remove('disabled');
nextBtn.style.pointerEvents = 'auto';
nextBtn.style.opacity = '1';
prevBtn.style.pointerEvents = 'auto';
prevBtn.style.opacity = '1';
if (loadingOverlay) {
loadingOverlay.classList.remove('active');
}
}
}
function showSubmitError(msg) {
const step6Error = stepwrp.querySelector('#sf-step6-error');
if (step6Error) {
step6Error.textContent = msg;
step6Error.style.display = 'block';
step6Error.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} else {
alert(msg);
}
}
function executeSuccessScreenTransition(firstName, lastName, meetLink) {
stepwrp.querySelector('#sf-success-username').textContent = `${firstName} ${lastName}`;
stepwrp.querySelector('#sf-success-time-text').textContent = appointmentValueText.textContent;
// Handle Google Meet link display
const meetPill = stepwrp.querySelector('#sf-success-meet-pill');
const meetLinkAnchor = stepwrp.querySelector('#sf-success-meet-link');
if (meetLink && meetPill && meetLinkAnchor) {
meetLinkAnchor.href = meetLink;
meetPill.style.display = 'inline-flex';
} else if (meetPill) {
meetPill.style.display = 'none';
}
form.style.display = 'none';
stepwrp.querySelector('.sf-card-footer').style.display = 'none';
steps.forEach((step) => {
step.classList.remove('active');
step.classList.add('completed');
});
lines.forEach((line) => {
line.classList.add('active');
});
successScreen.style.display = 'block';
setTimeout(() => {
successScreen.classList.add('active');
}, 50);
}
let timezoneData = [];
const TIMEZONES = {};
function loadTimezones() {
return fetch(TIMEZONE_DATA_PATH)
.then(response => response.json())
.then(data => {
if (Array.isArray(data)) {
timezoneData = data;
} else {
throw new Error("Invalid timezone data format");
}
})
.catch(err => {
console.error("Failed to load timezones from timezones.json, using fallback", err);
timezoneData = [
{ name: 'America/New_York', utcOffset: '-05:00', label: '(GMT-05:00) Eastern Time - New York' },
{ name: 'America/Chicago', utcOffset: '-06:00', label: '(GMT-06:00) Central Time - Chicago' },
{ name: 'America/Denver', utcOffset: '-07:00', label: '(GMT-07:00) Mountain Time - Denver' },
{ name: 'America/Los_Angeles', utcOffset: '-08:00', label: '(GMT-08:00) Pacific Time - Los Angeles' },
{ name: 'America/Phoenix', utcOffset: '-07:00', label: '(GMT-07:00) Mountain Time - Phoenix' },
{ name: 'America/Anchorage', utcOffset: '-09:00', label: '(GMT-09:00) Alaska Time - Anchorage' },
{ name: 'Pacific/Honolulu', utcOffset: '-10:00', label: '(GMT-10:00) Hawaii Time - Honolulu' },
{ name: 'Europe/London', utcOffset: '+00:00', label: '(GMT+00:00) Greenwich Mean Time - London' },
{ name: 'Europe/Paris', utcOffset: '+01:00', label: '(GMT+01:00) Central European Time - Paris' },
{ name: 'Europe/Berlin', utcOffset: '+01:00', label: '(GMT+01:00) Central European Time - Berlin' },
{ name: 'Europe/Moscow', utcOffset: '+03:00', label: '(GMT+03:00) Moscow Standard Time - Moscow' },
{ name: 'Asia/Dubai', utcOffset: '+04:00', label: '(GMT+04:00) Gulf Standard Time - Dubai' },
{ name: 'Asia/Kolkata', utcOffset: '+05:30', label: '(GMT+05:30) India Standard Time - Kolkata' },
{ name: 'Asia/Kathmandu', utcOffset: '+05:45', label: '(GMT+05:45) Nepal Time - Kathmandu' },
{ name: 'Asia/Dhaka', utcOffset: '+06:00', label: '(GMT+06:00) Bangladesh Standard Time - Dhaka' },
{ name: 'Asia/Bangkok', utcOffset: '+07:00', label: '(GMT+07:00) Indochina Time - Bangkok' },
{ name: 'Asia/Shanghai', utcOffset: '+08:00', label: '(GMT+08:00) China Standard Time - Shanghai' },
{ name: 'Asia/Tokyo', utcOffset: '+09:00', label: '(GMT+09:00) Japan Standard Time - Tokyo' },
{ name: 'Asia/Seoul', utcOffset: '+09:00', label: '(GMT+09:00) Korea Standard Time - Seoul' },
{ name: 'Australia/Sydney', utcOffset: '+10:00', label: '(GMT+10:00) Eastern Time - Sydney' },
{ name: 'Australia/Perth', utcOffset: '+08:00', label: '(GMT+08:00) Western Time - Perth' },
{ name: 'Pacific/Auckland', utcOffset: '+12:00', label: '(GMT+12:00) New Zealand Time - Auckland' }
];
})
.finally(() => {
timezoneData.forEach((tz) => {
TIMEZONES[tz.name] = parseUtcOffset(tz.utcOffset);
});
selectedTimezone = detectAndMapTimezone();
detectedLocalTimezoneName = selectedTimezone;
});
}
let BASE_SLOTS = [];
let apiStartTime = null;
let apiEndTime = null;
let apiSlotDuration = null;
let apiWorkingDays = null;
let apiHolidays = [];
function parseUtcOffset(offsetStr) {
const sign = offsetStr.startsWith('-') ? -1 : 1;
const parts = offsetStr.substring(1).split(':');
const hours = parseInt(parts[0], 10);
const minutes = parseInt(parts[1], 10);
return sign * (hours + minutes / 60);
}
function detectAndMapTimezone() {
try {
let tzName = Intl.DateTimeFormat().resolvedOptions().timeZone;
const aliases = {
'Asia/Calcutta': 'Asia/Kolkata',
'Asia/Saigon': 'Asia/Ho_Chi_Minh',
'Europe/Kiev': 'Europe/Kyiv'
};
if (aliases[tzName]) {
tzName = aliases[tzName];
}
const found = timezoneData.find((tz) => tz.name === tzName);
if (found) {
return found.name;
}
const offsetMinutes = -new Date().getTimezoneOffset();
const offsetHours = offsetMinutes / 60;
const sign = offsetHours >= 0 ? '+' : '-';
const absHours = Math.floor(Math.abs(offsetHours));
const absMins = Math.round((Math.abs(offsetHours) - absHours) * 60);
const offsetStr = `${sign}${absHours.toString().padStart(2, '0')}:${absMins.toString().padStart(2, '0')}`;
const matchedOffset = timezoneData.find((tz) => tz.utcOffset === offsetStr);
if (matchedOffset) {
return matchedOffset.name;
}
if (tzName) {
const newTz = {
name: tzName,
utcOffset: offsetStr,
label: `(GMT${offsetStr}) ${tzName}`
};
timezoneData.push(newTz);
TIMEZONES[tzName] = parseUtcOffset(offsetStr);
return tzName;
}
const defaultTz = timezoneData.find((tz) => tz.name === 'Asia/Kolkata') || timezoneData[0];
return defaultTz ? defaultTz.name : 'Asia/Kolkata';
} catch (e) {
const defaultTz = timezoneData.find((tz) => tz.name === 'Asia/Kolkata') || timezoneData[0];
return defaultTz ? defaultTz.name : 'Asia/Kolkata';
}
}
// --- State Variables ---
let currentStep = 1;
let selectedDate = null;
let selectedTime = null;
let selectedTimezone = null;
let detectedLocalTimezoneName = null;
let selectedSlotIndex = -1;
let calendarYear = new Date().getFullYear();
let calendarMonth = new Date().getMonth();
const monthNames = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
const formData = {};
let rawAvailableSlots = [];
let bookedSlotsData = [];
let slotsByLocalDate = {};
let selectableDateKeys = new Set();
// --- Debounced Action Handlers ---
const debouncedFetchCalendarData = debounce(function (date) {
fetchCalendarData(date);
}, 300);
const debouncedUpdateSlotSelection = debounce(function () {
updateAppointmentPreview();
hideElementById('sf-step5-error');
}, 150);
const firstNameInput = stepwrp.querySelector('#sf-first-name');
const lastNameInput = stepwrp.querySelector('#sf-last-name');
const emailInput = stepwrp.querySelector('#sf-email-address');
const companyInput = stepwrp.querySelector('#sf-company-name');
const websiteInput = stepwrp.querySelector('#sf-website-url');
const roleInput = stepwrp.querySelector('#sf-role');
const audienceDetailsInput = stepwrp.querySelector('#sf-audience-details');
const budgetContextInput = stepwrp.querySelector('#sf-budget-context');
// --- Initialize Form ---
initApp();
function initApp() {
loadTimezones().then(() => {
initOptionCards();
initPillOptions();
initInputListeners();
selectedDate = null;
initCalendar();
renderTimeSlots();
initTimezoneSelector();
updateStepIndicator();
syncFormData();
setupEventListeners();
fetchCalendarInit();
fetchCalendarData();
});
}
function formatLocalTime(hours, minutes) {
let ampm = 'AM';
let displayHours = hours;
if (displayHours >= 12) {
ampm = 'PM';
if (displayHours > 12) displayHours -= 12;
}
if (displayHours === 0) displayHours = 12;
return `${displayHours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')} ${ampm}`;
}
function generateBaseSlots(startTime, endTime, duration) {
const slots = [];
let currentMinutes = startTime * 60;
const endMinutes = endTime * 60;
while (currentMinutes < endMinutes) {
const hours = Math.floor(currentMinutes / 60);
const minutes = currentMinutes % 60;
let ampm = 'AM';
let displayHours = hours;
if (displayHours >= 12) {
ampm = 'PM';
if (displayHours > 12) displayHours -= 12;
}
if (displayHours === 0) displayHours = 12;
const timeStr = `${displayHours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')} ${ampm}`;
slots.push({ time: timeStr, disabled: false });
currentMinutes += duration;
}
return slots;
}
function fetchCalendarInit() {
const initUrl = typeof GOOGLE_CALENDAR_CONFIG !== 'undefined' ? (GOOGLE_CALENDAR_CONFIG.googleAppsScriptUrlForCalendarInit || (GOOGLE_CALENDAR_CONFIG.googleAppsScriptUrlForSlot ? GOOGLE_CALENDAR_CONFIG.googleAppsScriptUrlForSlot.replace('action=slots', 'action=calendar_init') : '')) : '';
if (!initUrl) return Promise.resolve();
return fetch(initUrl)
.then(r => r.json())
.then(data => {
//console.log("Calendar Init API Response:", data);
if (data) {
if (Array.isArray(data.working_days)) {
apiWorkingDays = data.working_days;
}
if (Array.isArray(data.holidays)) {
apiHolidays = data.holidays;
}
renderCalendar(calendarYear, calendarMonth);
}
})
.catch(err => {
console.error("Error fetching calendar init config:", err);
});
}
function fetchCalendarData(dateStr) {
let slotUrl = typeof GOOGLE_CALENDAR_CONFIG !== 'undefined' ? GOOGLE_CALENDAR_CONFIG.googleAppsScriptUrlForSlot : '';
const bookingUrl = typeof GOOGLE_CALENDAR_CONFIG !== 'undefined' ? GOOGLE_CALENDAR_CONFIG.googleAppsScriptUrlForFindBooking : '';
if (!slotUrl) {
console.warn("Google Calendar Slots URL is not configured. Slots will not be fetched.");
return;
}
if (dateStr) {
slotUrl += '&date=' + encodeURIComponent(dateStr);
}
if (selectedTimezone) {
slotUrl += '&timezone=' + encodeURIComponent(selectedTimezone);
}
let bookingUrlWithDate = bookingUrl;
if (bookingUrlWithDate && dateStr) {
bookingUrlWithDate += '&date=' + encodeURIComponent(dateStr);
}
const fetchBookings = bookingUrlWithDate ? fetch(bookingUrlWithDate).then(r => r.json()).catch(err => {
console.error("Error fetching booked slots from Apps Script:", err);
return [];
}) : Promise.resolve([]);
if (timeSlotsContainer) {
timeSlotsContainer.innerHTML = `
`;
}
Promise.all([
fetch(slotUrl).then(r => {
if (!r.ok) throw new Error(`HTTP error ${r.status}`);
return r.json();
}),
fetchBookings
])
.then(([slots, bookings]) => {
if (slots && slots.timezone) {
window.adminCalendarTimezone = slots.timezone;
}
const slotsArray = slots && Array.isArray(slots.data) ? slots.data : (Array.isArray(slots) ? slots : []);
let startTime = slots && typeof slots.start_time === 'number' ? slots.start_time : null;
let endTime = slots && typeof slots.end_time === 'number' ? slots.end_time : null;
let duration = slots && typeof slots.slot_duration === 'number' ? slots.slot_duration : null;
let workingDays = slots && Array.isArray(slots.working_days) ? slots.working_days : null;
if (startTime === null || endTime === null || duration === null) {
startTime = 9;
endTime = 17;
duration = 30;
}
if (workingDays) {
apiWorkingDays = workingDays;
}
apiStartTime = startTime;
apiEndTime = endTime;
apiSlotDuration = duration;
window.apiSlotDuration = duration;
BASE_SLOTS = generateBaseSlots(apiStartTime, apiEndTime, apiSlotDuration);
rawAvailableSlots = slotsArray;
const bookingsArray = bookings && Array.isArray(bookings.bookings) ? bookings.bookings : (Array.isArray(bookings) ? bookings : []);
bookedSlotsData = bookingsArray;
processSlotsData();
})
.catch(err => {
console.error("Error fetching calendar data:", err);
if (timeSlotsContainer) {
timeSlotsContainer.innerHTML = `
Error loading slots.
`;
}
});
}
function processSlotsData() {
selectableDateKeys = new Set();
const targetOffset = TIMEZONES[selectedTimezone] !== undefined ? TIMEZONES[selectedTimezone] : 5.5;
const now = Date.now();
const bookingRanges = [];
bookedSlotsData.forEach(day => {
if (Array.isArray(day.slot)) {
day.slot.forEach(s => {
if (s.startISO && s.endISO) {
const startMs = Date.parse(s.startISO);
const endMs = Date.parse(s.endISO);
if (!isNaN(startMs) && !isNaN(endMs)) {
bookingRanges.push({ start: startMs, end: endMs });
}
}
});
}
});
rawAvailableSlots.forEach(day => {
if (Array.isArray(day.slot)) {
day.slot.forEach(s => {
if (s.startISO) {
const utcMs = Date.parse(s.startISO);
if (!isNaN(utcMs)) {
const isBooked = bookingRanges.some(b => utcMs >= b.start && utcMs < b.end);
const isPast = utcMs < now;
if (!isBooked && !isPast) {
const localMs = utcMs + (targetOffset * 60 * 60 * 1000);
const targetDateObj = new Date(localMs);
const localYear = targetDateObj.getUTCFullYear();
const localMonth = (targetDateObj.getUTCMonth() + 1).toString().padStart(2, '0');
const localDay = targetDateObj.getUTCDate().toString().padStart(2, '0');
const localDateKey = `${localYear}-${localMonth}-${localDay}`;
selectableDateKeys.add(localDateKey);
}
}
}
});
}
});
renderCalendar(calendarYear, calendarMonth);
renderTimeSlots();
}
// --- Event Listeners Setup ---
function setupEventListeners() {
nextBtn.addEventListener('click', handleNextStep);
prevBtn.addEventListener('click', handlePrevStep);
restartBtn.addEventListener('click', handleRestart);
stepwrp.querySelectorAll('.sf-edit-btn').forEach((btn) => {
btn.addEventListener('click', function () {
const target = parseInt(btn.getAttribute('data-edit-step'), 10);
if (target >= 1 && target <= 5) {
transitionStep(currentStep, target);
}
});
});
form.addEventListener('submit', (e) => e.preventDefault());
form.addEventListener('input', syncFormData);
form.addEventListener('change', syncFormData);
}
// --- Option Card Interaction (Steps 1, 2, 3) ---
function initOptionCards() {
const cards = stepwrp.querySelectorAll('.sf-option-card');
cards.forEach((card) => {
const input = card.querySelector('input');
if (!input) return;
input.addEventListener('change', function () {
if (card.classList.contains('multi-select')) {
card.classList.toggle('selected', input.checked);
} else {
const name = input.getAttribute('name');
stepwrp.querySelectorAll(`.sf-option-card input[name="${name}"]`).forEach((itemInput) => {
itemInput.closest('.sf-option-card').classList.remove('selected');
});
card.classList.add('selected');
}
if (input.name === 'audience' && input.value === 'other') {
toggleOtherAudience();
}
hideElementById('sf-step1-error');
hideElementById('sf-step2-error');
hideElementById('sf-step3-error');
hideElementById('sf-step3-other-error');
syncFormData();
});
});
}
// --- Pill Option Interaction (Step 4) ---
function initPillOptions() {
const pills = stepwrp.querySelectorAll('.sf-pill-option');
pills.forEach((pill) => {
const input = pill.querySelector('input');
if (!input) return;
input.addEventListener('change', function () {
const name = input.getAttribute('name');
stepwrp.querySelectorAll(`.sf-pill-option input[name="${name}"]`).forEach((itemInput) => {
itemInput.closest('.sf-pill-option').classList.remove('selected');
});
pill.classList.add('selected');
if (name === 'timeline') {
hideElementById('sf-timeline-error');
} else if (name === 'planning_process') {
hideElementById('sf-planning-error');
}
syncFormData();
});
});
}
// --- Inline Input Listener (Step 5) ---
function initInputListeners() {
const inputConfigs = [
{ el: firstNameInput, errId: 'sf-first-name-error' },
{ el: lastNameInput, errId: 'sf-last-name-error' },
{ el: emailInput, errId: 'sf-email-error' },
{ el: companyInput, errId: 'sf-company-error' },
{ el: websiteInput, errId: 'sf-website-error' },
{ el: roleInput, errId: null },
{ el: audienceDetailsInput, errId: null },
{ el: budgetContextInput, errId: null },
];
inputConfigs.forEach(({ el, errId }) => {
if (!el) return;
el.addEventListener('input', function () {
const wpr = el.closest('.sf-custom-input-wpr, .sf-custom-textarea-wpr');
if (wpr) wpr.classList.remove('error');
if (errId) {
hideElementById(errId);
}
if (el === emailInput) {
nextBtn.classList.remove('disabled');
nextBtn.style.pointerEvents = 'auto';
nextBtn.style.opacity = '1';
hideElementById('sf-step5-error');
}
syncFormData();
});
});
}
// --- Calendar State & Helpers ---
function getSlotUtcTimestamp(dateStr, timeStr, timezone) {
const dateParts = dateStr.match(/([A-Za-z]+)\s+(\d+),\s+(\d{4})/);
if (!dateParts) return 0;
const months = {
'January': 0, 'February': 1, 'March': 2, 'April': 3, 'May': 4, 'June': 5,
'July': 6, 'August': 7, 'September': 8, 'October': 9, 'November': 10, 'December': 11
};
const monthIndex = months[dateParts[1]];
const day = parseInt(dateParts[2], 10);
const year = parseInt(dateParts[3], 10);
const timeParts = timeStr.match(/(\d+):(\d+)\s*(AM|PM)/i);
if (!timeParts) return 0;
let hours = parseInt(timeParts[1], 10);
const minutes = parseInt(timeParts[2], 10);
const ampm = timeParts[3].toUpperCase();
if (ampm === 'PM' && hours < 12) hours += 12;
if (ampm === 'AM' && hours === 12) hours = 0;
const localUtcMs = Date.UTC(year, monthIndex, day, hours, minutes, 0);
const offsetHours = TIMEZONES[timezone] !== undefined ? TIMEZONES[timezone] : 5.5;
const offsetMs = offsetHours * 60 * 60 * 1000;
return localUtcMs - offsetMs;
}
function getSlotUtcTimestampWithOffset(dateStr, timeStr, offsetHours) {
const dateParts = dateStr.match(/([A-Za-z]+)\s+(\d+),\s+(\d{4})/);
if (!dateParts) return 0;
const months = {
'January': 0, 'February': 1, 'March': 2, 'April': 3, 'May': 4, 'June': 5,
'July': 6, 'August': 7, 'September': 8, 'October': 9, 'November': 10, 'December': 11
};
const monthIndex = months[dateParts[1]];
const day = parseInt(dateParts[2], 10);
const year = parseInt(dateParts[3], 10);
const timeParts = timeStr.match(/(\d+):(\d+)\s*(AM|PM)/i);
if (!timeParts) return 0;
let hours = parseInt(timeParts[1], 10);
const minutes = parseInt(timeParts[2], 10);
const ampm = timeParts[3].toUpperCase();
if (ampm === 'PM' && hours < 12) hours += 12;
if (ampm === 'AM' && hours === 12) hours = 0;
const localUtcMs = Date.UTC(year, monthIndex, day, hours, minutes, 0);
const offsetMs = offsetHours * 60 * 60 * 1000;
return localUtcMs - offsetMs;
}
function getYYYYMMDD(dateStr) {
const dateParts = dateStr.match(/([A-Za-z]+)\s+(\d+),\s+(\d{4})/);
if (!dateParts) return '';
const months = {
'January': '01', 'February': '02', 'March': '03', 'April': '04', 'May': '05', 'June': '06',
'July': '07', 'August': '08', 'September': '09', 'October': '10', 'November': '11', 'December': '12'
};
const month = months[dateParts[1]];
const day = dateParts[2].padStart(2, '0');
const year = dateParts[3];
return `${year}-${month}-${day}`;
}
function updateCalNavButtons() {
const systemDate = new Date();
const systemToday = new Date(systemDate.getFullYear(), systemDate.getMonth(), systemDate.getDate());
const maxSelectableDate = new Date(systemToday.getTime() + 90 * 24 * 60 * 60 * 1000);
const calPrevBtn = stepwrp.querySelector('#sf-cal-prev-btn');
const calNextBtn = stepwrp.querySelector('#sf-cal-next-btn');
if (!calPrevBtn || !calNextBtn) return;
const canGoPrev = (calendarYear > systemDate.getFullYear()) ||
(calendarYear === systemDate.getFullYear() && calendarMonth > systemDate.getMonth());
if (canGoPrev) {
calPrevBtn.classList.remove('disabled');
calPrevBtn.style.opacity = '1';
calPrevBtn.style.pointerEvents = 'auto';
calPrevBtn.style.cursor = 'pointer';
} else {
calPrevBtn.classList.add('disabled');
calPrevBtn.style.opacity = '0.3';
calPrevBtn.style.pointerEvents = 'none';
calPrevBtn.style.cursor = 'not-allowed';
}
const canGoNext = (calendarYear < maxSelectableDate.getFullYear()) ||
(calendarYear === maxSelectableDate.getFullYear() && calendarMonth < maxSelectableDate.getMonth());
if (canGoNext) {
calNextBtn.classList.remove('disabled');
calNextBtn.style.opacity = '1';
calNextBtn.style.pointerEvents = 'auto';
calNextBtn.style.cursor = 'pointer';
} else {
calNextBtn.classList.add('disabled');
calNextBtn.style.opacity = '0.3';
calNextBtn.style.pointerEvents = 'none';
calNextBtn.style.cursor = 'not-allowed';
}
}
function renderCalendar(year, month) {
monthYearLabel.textContent = `${monthNames[month]} ${year}`;
const headers = calendarGrid.querySelectorAll('.sf-calendar-day-label');
calendarGrid.innerHTML = '';
headers.forEach((h) => calendarGrid.appendChild(h));
const firstDayIndex = new Date(year, month, 1).getDay();
const numDays = new Date(year, month + 1, 0).getDate();
const prevMonthNumDays = new Date(year, month, 0).getDate();
const systemDate = new Date();
const systemToday = new Date(systemDate.getFullYear(), systemDate.getMonth(), systemDate.getDate());
const maxSelectableDate = new Date(systemToday.getTime() + 90 * 24 * 60 * 60 * 1000);
// Previous month filler days
for (let i = firstDayIndex - 1; i >= 0; i--) {
const dayVal = prevMonthNumDays - i;
const dayEl = document.createElement('div');
dayEl.classList.add('sf-calendar-day', 'disabled');
dayEl.textContent = dayVal;
calendarGrid.appendChild(dayEl);
}
// Current month days
for (let day = 1; day <= numDays; day++) {
const dayEl = document.createElement('div');
dayEl.classList.add('sf-calendar-day');
const dateOfChoice = new Date(year, month, day);
const todayMidnight = new Date(systemToday.getFullYear(), systemToday.getMonth(), systemToday.getDate());
const dateOfChoiceMidnight = new Date(dateOfChoice.getFullYear(), dateOfChoice.getMonth(), dateOfChoice.getDate());
const dayOfWeek = dateOfChoice.getDay();
const isWorkingDay = Array.isArray(apiWorkingDays) ? (apiWorkingDays.indexOf(dayOfWeek) !== -1) : true;
const localDateKey = `${year}-${(month + 1).toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
const isHoliday = Array.isArray(apiHolidays) && apiHolidays.indexOf(localDateKey) !== -1;
let isSelectable = (dateOfChoiceMidnight >= todayMidnight && dateOfChoiceMidnight <= maxSelectableDate) && isWorkingDay && !isHoliday;
if (!isSelectable) {
dayEl.classList.add('disabled');
}
const dateStr = `${monthNames[month]} ${day}, ${year}`;
if (selectedDate === dateStr) {
dayEl.classList.add('selected');
}
dayEl.innerHTML = `${day}`;
if (dateOfChoice.getFullYear() === systemToday.getFullYear() &&
dateOfChoice.getMonth() === systemToday.getMonth() &&
dateOfChoice.getDate() === systemToday.getDate()) {
dayEl.classList.add('today', 'current-date');
}
dayEl.addEventListener('click', function () {
if (!isSelectable) return;
const selected = calendarGrid.querySelector('.sf-calendar-day.selected');
if (selected) {
selected.classList.remove('selected');
}
dayEl.classList.add('selected');
selectedDate = dateStr;
debouncedFetchCalendarData(selectedDate);
updateAppointmentPreview();
hideElementById('sf-step5-error');
nextBtn.classList.remove('disabled');
nextBtn.style.pointerEvents = 'auto';
nextBtn.style.opacity = '1';
});
calendarGrid.appendChild(dayEl);
}
// Next month days filler (complete 42 grid cells)
const totalCells = firstDayIndex + numDays;
const nextFillerCount = 42 - totalCells;
for (let day = 1; day <= nextFillerCount; day++) {
const dayEl = document.createElement('div');
dayEl.classList.add('sf-calendar-day', 'disabled');
dayEl.textContent = day;
calendarGrid.appendChild(dayEl);
}
updateCalNavButtons();
}
// --- Calendar Initialization (Step 5) ---
function initCalendar() {
const systemDate = new Date();
calendarYear = systemDate.getFullYear();
calendarMonth = systemDate.getMonth();
const calPrevBtn = stepwrp.querySelector('#sf-cal-prev-btn');
const calNextBtn = stepwrp.querySelector('#sf-cal-next-btn');
if (calPrevBtn) {
calPrevBtn.addEventListener('click', function (e) {
e.preventDefault();
calendarMonth--;
if (calendarMonth < 0) {
calendarMonth = 11;
calendarYear--;
}
renderCalendar(calendarYear, calendarMonth);
});
}
if (calNextBtn) {
calNextBtn.addEventListener('click', function (e) {
e.preventDefault();
calendarMonth++;
if (calendarMonth > 11) {
calendarMonth = 0;
calendarYear++;
}
renderCalendar(calendarYear, calendarMonth);
});
}
renderCalendar(calendarYear, calendarMonth);
}
// --- Timezone Conversion Helper ---
function convertTime(timeStr, targetTimezone) {
const parts = timeStr.match(/(\d+):(\d+)\s*(AM|PM)/i);
if (!parts) return timeStr;
let hours = parseInt(parts[1], 10);
const minutes = parseInt(parts[2], 10);
const ampm = parts[3].toUpperCase();
if (ampm === 'PM' && hours < 12) hours += 12;
if (ampm === 'AM' && hours === 12) hours = 0;
const baseOffset = 5.5; // Calcutta is UTC + 5.5
const targetOffset = TIMEZONES[targetTimezone] !== undefined ? TIMEZONES[targetTimezone] : 5.5;
let baseMinutes = hours * 60 + minutes;
let utcMinutes = baseMinutes - baseOffset * 60;
let targetMinutes = utcMinutes + targetOffset * 60;
targetMinutes = ((targetMinutes % 1440) + 1440) % 1440;
let targetHours = Math.floor(targetMinutes / 60);
const targetMins = targetMinutes % 60;
let targetAmpm = 'AM';
if (targetHours >= 12) {
targetAmpm = 'PM';
if (targetHours > 12) targetHours -= 12;
}
if (targetHours === 0) targetHours = 12;
const padMin = targetMins.toString().padStart(2, '0');
return `${targetHours.toString().padStart(2, '0')}:${padMin} ${targetAmpm}`;
}
function getSlotsForSelectedDate(selectedDateStr, targetTimezone) {
const resultSlots = [];
const now = Date.now();
if (!selectedDateStr) return resultSlots;
const selectedDateKey = getYYYYMMDD(selectedDateStr); // e.g. "2026-07-10"
if (!selectedDateKey) return resultSlots;
const dateParts = selectedDateStr.match(/([A-Za-z]+)\s+(\d+),\s+(\d{4})/);
if (!dateParts) return resultSlots;
const months = {
'January': 0, 'February': 1, 'March': 2, 'April': 3, 'May': 4, 'June': 5,
'July': 6, 'August': 7, 'September': 8, 'October': 9, 'November': 10, 'December': 11
};
const monthIndex = months[dateParts[1]];
const day = parseInt(dateParts[2], 10);
const year = parseInt(dateParts[3], 10);
const bookingRanges = [];
bookedSlotsData.forEach(day => {
if (Array.isArray(day.slot)) {
day.slot.forEach(s => {
if (s.startISO && s.endISO) {
const startMs = Date.parse(s.startISO);
const endMs = Date.parse(s.endISO);
if (!isNaN(startMs) && !isNaN(endMs)) {
bookingRanges.push({ start: startMs, end: endMs });
}
}
});
}
});
const allAvailableSlotUtc = new Set();
rawAvailableSlots.forEach(day => {
if (Array.isArray(day.slot)) {
day.slot.forEach(s => {
if (s.startISO) {
const ms = Date.parse(s.startISO);
if (!isNaN(ms)) {
allAvailableSlotUtc.add(ms);
}
}
});
}
});
let detectedScriptOffset = 5.5; // default/fallback to Asia/Kolkata
if (rawAvailableSlots.length > 0 && Array.isArray(rawAvailableSlots[0].slot) && rawAvailableSlots[0].slot.length > 0) {
const dayObj = rawAvailableSlots[0];
const sampleSlot = dayObj.slot[0];
if (dayObj.date && sampleSlot.start && sampleSlot.startISO) {
const [yr, mo, dy] = dayObj.date.split('-').map(Number);
const [hrs, mins] = sampleSlot.start.split(':').map(Number);
const localUtcMs = Date.UTC(yr, mo - 1, dy, hrs, mins, 0);
const actualUtcMs = Date.parse(sampleSlot.startISO);
if (!isNaN(localUtcMs) && !isNaN(actualUtcMs)) {
detectedScriptOffset = (localUtcMs - actualUtcMs) / (60 * 60 * 1000);
}
}
}
const targetOffset = TIMEZONES[targetTimezone] !== undefined ? TIMEZONES[targetTimezone] : 5.5;
const todayTargetMs = now + (targetOffset * 60 * 60 * 1000);
const todayTargetObj = new Date(todayTargetMs);
const todayTargetKey = `${todayTargetObj.getUTCFullYear()}-${(todayTargetObj.getUTCMonth() + 1).toString().padStart(2, '0')}-${todayTargetObj.getUTCDate().toString().padStart(2, '0')}`;
const centerDate = new Date(year, monthIndex, day);
const datesToProcess = [
new Date(centerDate.getTime() - 24 * 60 * 60 * 1000), // yesterday
centerDate, // selected day
new Date(centerDate.getTime() + 24 * 60 * 60 * 1000) // tomorrow
];
datesToProcess.forEach(baseDateObj => {
const baseDateStr = `${monthNames[baseDateObj.getMonth()]} ${baseDateObj.getDate()}, ${baseDateObj.getFullYear()}`;
BASE_SLOTS.forEach(templateSlot => {
const slotUtc = getSlotUtcTimestampWithOffset(baseDateStr, templateSlot.time, detectedScriptOffset);
const localMs = slotUtc + (targetOffset * 60 * 60 * 1000);
const targetDateObj = new Date(localMs);
const localYear = targetDateObj.getUTCFullYear();
const localMonth = targetDateObj.getUTCMonth();
const localDay = targetDateObj.getUTCDate();
const localDateKey = `${localYear}-${(localMonth + 1).toString().padStart(2, '0')}-${localDay.toString().padStart(2, '0')}`;
if (localDateKey === selectedDateKey) {
const localHours = targetDateObj.getUTCHours();
const localMinutes = targetDateObj.getUTCMinutes();
const localTimeStr = formatLocalTime(localHours, localMinutes);
const slotStartISO = new Date(slotUtc).toISOString();
const isAvailable = allAvailableSlotUtc.has(slotUtc);
const isBooked = bookingRanges.some(booking => {
return slotUtc >= booking.start && slotUtc < booking.end;
});
const isSlotToday = (localDateKey === todayTargetKey);
let isSlotDisabled = !isAvailable || isBooked || (isSlotToday && slotUtc < now);
let isSlotBookedStatus = isBooked;
resultSlots.push({
time: localTimeStr,
startISO: slotStartISO,
isBooked: isSlotBookedStatus,
disabled: isSlotDisabled,
utcTimestamp: slotUtc
});
}
});
});
resultSlots.sort((a, b) => a.utcTimestamp - b.utcTimestamp);
return resultSlots;
}
// --- Render Dynamic Time Slots ---
function renderTimeSlots() {
timeSlotsContainer.innerHTML = '';
if (!selectedDate) {
const infoMsg = document.createElement('div');
infoMsg.style.textAlign = 'center';
infoMsg.style.color = 'var(--sf-gray-muted)';
infoMsg.style.padding = '20px';
infoMsg.textContent = 'Please select a date from the calendar.';
timeSlotsContainer.appendChild(infoMsg);
return;
}
const daySlots = getSlotsForSelectedDate(selectedDate, selectedTimezone);
if (daySlots.length === 0) {
const infoMsg = document.createElement('div');
infoMsg.style.textAlign = 'center';
infoMsg.style.color = 'var(--sf-gray-muted)';
infoMsg.style.padding = '20px';
infoMsg.textContent = 'No available slots for this date.';
timeSlotsContainer.appendChild(infoMsg);
return;
}
let selectionCleared = false;
daySlots.forEach((slot, index) => {
let isSlotDisabled = slot.disabled;
let isSlotBooked = slot.isBooked;
const slotEl = document.createElement('div');
slotEl.classList.add('sf-time-slot');
if (isSlotDisabled) {
slotEl.classList.add('disabled');
if (isSlotBooked) {
slotEl.classList.add('booked');
}
if (selectedSlotIndex === index) {
selectedTime = null;
selectedSlotIndex = -1;
selectionCleared = true;
}
}
const displayLabel = isSlotBooked ? `${slot.time} (Booked)` : slot.time;
slotEl.setAttribute('data-time', slot.time);
slotEl.textContent = displayLabel;
if (selectedSlotIndex === index && !isSlotDisabled) {
slotEl.classList.add('selected');
selectedTime = slot.time;
}
slotEl.addEventListener('click', function () {
if (isSlotDisabled) return;
const selected = timeSlotsContainer.querySelector('.sf-time-slot.selected');
if (selected) {
selected.classList.remove('selected');
}
slotEl.classList.add('selected');
selectedTime = slot.time;
selectedSlotIndex = index;
debouncedUpdateSlotSelection();
});
timeSlotsContainer.appendChild(slotEl);
});
if (selectionCleared) {
updateAppointmentPreview();
}
}
// --- Render Dynamic Timezone Dropdown Options ---
function renderTimezoneOptions(filterQuery = '') {
const tzOptionsContainer = stepwrp.querySelector('#sf-timezone-options-container');
if (!tzOptionsContainer) return;
tzOptionsContainer.innerHTML = '';
const query = filterQuery.toLowerCase();
const filteredData = timezoneData.filter(tz => {
const label = (tz.label || '').toLowerCase();
const name = (tz.name || '').toLowerCase();
return label.includes(query) || name.includes(query);
});
if (filteredData.length === 0) {
const emptyEl = document.createElement('div');
emptyEl.className = 'sf-timezone-option disabled';
emptyEl.style.cursor = 'default';
emptyEl.style.justifyContent = 'center';
emptyEl.style.color = 'var(--sf-gray-muted)';
emptyEl.innerHTML = 'No timezones found';
tzOptionsContainer.appendChild(emptyEl);
return;
}
filteredData.forEach((tz) => {
const isSelected = tz.name === selectedTimezone;
const checkmark = isSelected ? 'ā' : '';
const selectedClass = isSelected ? 'selected' : '';
const optEl = document.createElement('div');
optEl.className = `sf-timezone-option ${selectedClass}`;
optEl.setAttribute('data-value', tz.name);
const displayText = tz.label || `${tz.name} (GMT${tz.utcOffset})`;
optEl.innerHTML = `
${checkmark}
${displayText}
`;
optEl.addEventListener('click', function (e) {
e.stopPropagation();
selectTimezone(tz.name);
});
tzOptionsContainer.appendChild(optEl);
});
}
// --- Select Timezone Action ---
function selectTimezone(tzName) {
selectedTimezone = tzName;
window.userCalendarTimezone = tzName;
const tzCurrentLabel = stepwrp.querySelector('#sf-timezone-current-label');
const tzInput = stepwrp.querySelector('#sf-timezone-input');
const tzInfo = timezoneData.find((tz) => tz.name === tzName);
const displayLabel = tzInfo ? (tzInfo.label || `${tzInfo.name} (GMT${tzInfo.utcOffset})`) : tzName;
if (tzCurrentLabel) tzCurrentLabel.textContent = displayLabel;
if (tzInput) tzInput.value = tzName;
const tzOptions = stepwrp.querySelectorAll('.sf-timezone-option');
tzOptions.forEach((opt) => {
const val = opt.getAttribute('data-value');
const check = opt.querySelector('.sf-timezone-check-mark');
if (val === tzName) {
opt.classList.add('selected');
if (check) check.textContent = 'ā';
} else {
opt.classList.remove('selected');
if (check) check.textContent = '';
}
});
processSlotsData();
updateAppointmentPreview();
closeAllDropdowns();
}
// --- Timezone Selector Interaction ---
function initTimezoneSelector() {
const tzSelectBtn = stepwrp.querySelector('#sf-timezone-select-btn');
const tzDropdownList = stepwrp.querySelector('#sf-timezone-dropdown-list');
const searchInput = stepwrp.querySelector('#sf-timezone-search-input');
const searchWpr = stepwrp.querySelector('.sf-timezone-search-wpr');
if (!tzSelectBtn || !tzDropdownList) return;
renderTimezoneOptions();
selectTimezone(selectedTimezone);
tzSelectBtn.addEventListener('click', function (e) {
e.stopPropagation();
const isOpen = tzDropdownList.classList.contains('show');
closeAllDropdowns();
if (!isOpen) {
tzDropdownList.classList.add('show');
tzSelectBtn.classList.add('active');
if (searchInput) {
searchInput.value = '';
renderTimezoneOptions('');
setTimeout(() => searchInput.focus(), 50);
}
}
});
if (searchWpr) {
searchWpr.addEventListener('click', function (e) {
e.stopPropagation();
});
}
if (searchInput) {
searchInput.addEventListener('input', function () {
renderTimezoneOptions(this.value.trim());
});
}
document.addEventListener('click', function () {
closeAllDropdowns();
});
}
function closeAllDropdowns() {
const tzSelectBtn = stepwrp.querySelector('#sf-timezone-select-btn');
const tzDropdownList = stepwrp.querySelector('#sf-timezone-dropdown-list');
if (tzDropdownList) tzDropdownList.classList.remove('show');
if (tzSelectBtn) tzSelectBtn.classList.remove('active');
}
// --- Update Review Step (Step 6) ---
function updateAppointmentPreview() {
if (appointmentValueText) {
if (selectedDate && selectedTime) {
const dateObj = new Date(selectedDate);
const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const dayName = isNaN(dateObj.getDay()) ? 'Monday' : daysOfWeek[dateObj.getDay()];
const tzInfo = timezoneData.find((tz) => tz.name === selectedTimezone);
const tzDisplay = tzInfo ? (tzInfo.label || `${tzInfo.name} (GMT${tzInfo.utcOffset})`) : selectedTimezone;
const val = `${dayName}, ${selectedDate} At ${selectedTime} (${tzDisplay})`;
appointmentValueText.textContent = val;
if (appointmentInput) appointmentInput.value = val;
} else {
appointmentValueText.textContent = 'Please choose a date and time slot';
if (appointmentInput) appointmentInput.value = '';
}
syncFormData();
}
}
function updateReviewStep() {
const firstName = firstNameInput.value.trim();
const lastName = lastNameInput.value.trim();
stepwrp.querySelector('#sf-review-name').textContent = `${firstName} ${lastName}`;
stepwrp.querySelector('#sf-review-email').textContent = emailInput.value.trim();
stepwrp.querySelector('#sf-review-company').textContent = companyInput.value.trim();
const role = roleInput.value.trim();
stepwrp.querySelector('#sf-review-role').textContent = role || '-';
const website = websiteInput.value.trim();
stepwrp.querySelector('#sf-review-website').textContent = website || '-';
const createChecked = stepwrp.querySelector('input[name="create"]:checked');
const createLabel = createChecked ? createChecked.closest('.sf-option-card').querySelector('.sf-card-label').textContent : '-';
stepwrp.querySelector('#sf-review-create').textContent = createLabel;
const goalsChecked = stepwrp.querySelectorAll('input[name="goals"]:checked');
const goalsLabels = Array.from(goalsChecked).map((el) => el.closest('.sf-option-card').querySelector('.sf-card-label').textContent);
stepwrp.querySelector('#sf-review-goals').textContent = goalsLabels.length > 0 ? goalsLabels.join(', ') : '-';
const audienceChecked = stepwrp.querySelectorAll('input[name="audience"]:checked');
const audienceLabels = Array.from(audienceChecked).map((el) => {
const labelText = el.closest('.sf-option-card').querySelector('.sf-card-label').textContent;
if (el.value === 'other') {
const extraVal = stepwrp.querySelector('#sf-audience-details') ? stepwrp.querySelector('#sf-audience-details').value.trim() : '';
if (extraVal) {
return `others: ${extraVal}`;
}
}
return labelText;
});
stepwrp.querySelector('#sf-review-audience').textContent = audienceLabels.length > 0 ? audienceLabels.join(', ') : '-';
const timelineChecked = stepwrp.querySelector('input[name="timeline"]:checked');
const planningChecked = stepwrp.querySelector('input[name="planning_process"]:checked');
const timelineVal = timelineChecked ? timelineChecked.closest('.sf-pill-option').querySelector('span').textContent : '';
const planningVal = planningChecked ? planningChecked.closest('.sf-pill-option').querySelector('span').textContent : '';
stepwrp.querySelector('#sf-review-scope').textContent = timelineVal && planningVal ? `${timelineVal} / ${planningVal}` : '-';
stepwrp.querySelector('#sf-review-appointment').textContent = appointmentValueText.textContent;
}
// --- Sync Form Data to JSON Object ---
function syncFormData() {
const elements = form.querySelectorAll('[name]');
const processed = new Set();
elements.forEach((el) => {
const name = el.name;
if (!name || processed.has(name)) return;
if (el.type === 'checkbox') {
const checked = Array.from(form.querySelectorAll(`input[name="${name}"]:checked`)).map((c) => {
if (name === 'audience' && c.value === 'other') {
const extraVal = stepwrp.querySelector('#sf-audience-details') ? stepwrp.querySelector('#sf-audience-details').value.trim() : '';
if (extraVal) {
return `others:${extraVal}`;
}
}
return c.value;
});
formData[name] = checked;
processed.add(name);
} else if (el.type === 'radio') {
const checkedEl = form.querySelector(`input[name="${name}"]:checked`);
formData[name] = checkedEl ? checkedEl.value : '';
processed.add(name);
} else {
formData[name] = el.value.trim();
processed.add(name);
}
});
}
// --- Step Indicator & Progress updates ---
function updateStepIndicator() {
steps.forEach((step, idx) => {
const stepNum = idx + 1;
step.classList.remove('active', 'completed');
if (stepNum === currentStep) {
step.classList.add('active');
} else if (stepNum < currentStep) {
step.classList.add('completed');
}
});
lines.forEach((line, idx) => {
const lineNum = idx + 1;
line.classList.remove('active');
if (lineNum < currentStep) {
line.classList.add('active');
}
});
if (currentStep === 1 || currentStep === TOTAL_STEPS) {
prevBtn.style.visibility = 'hidden';
} else {
prevBtn.style.visibility = 'visible';
}
if (stepper) {
if (currentStep === TOTAL_STEPS) {
stepper.style.display = 'none';
} else {
stepper.style.display = 'flex';
}
}
if (currentStep === TOTAL_STEPS) {
nextBtn.textContent = 'Submit Inquiry';
} else {
nextBtn.textContent = 'Next';
}
}
// --- Step Transitions (Slide and Fade) ---
function transitionStep(fromStep, toStep) {
const currentEl = stepwrp.querySelector(`.sf-step-content[data-step-content="${fromStep}"]`);
const nextEl = stepwrp.querySelector(`.sf-step-content[data-step-content="${toStep}"]`);
if (!currentEl || !nextEl) return;
currentEl.classList.remove('fade-in');
setTimeout(() => {
currentEl.classList.remove('active');
nextEl.classList.add('active');
setTimeout(() => {
nextEl.classList.add('fade-in');
}, ANIMATION_DELAY);
currentStep = toStep;
updateStepIndicator();
}, TRANSITION_DURATION);
}
// --- Navigation Handlers ---
function handleNextStep() {
if (!validateStep(currentStep)) return;
if (currentStep === 5) {
const emailVal = emailInput.value.trim().toLowerCase();
const checkUrlBase = typeof GOOGLE_CALENDAR_CONFIG !== 'undefined' ? GOOGLE_CALENDAR_CONFIG.googleAppsScriptUrlForCheckEmail : '';
if (checkUrlBase) {
const checkUrl = checkUrlBase +
'&email=' + encodeURIComponent(emailVal) +
'&date=' + encodeURIComponent(selectedDate) +
'&timezone=' + encodeURIComponent(selectedTimezone);
nextBtn.textContent = 'Checking booking...';
nextBtn.classList.add('disabled');
nextBtn.style.pointerEvents = 'none';
nextBtn.style.opacity = '0.7';
// console.log("Checking booking for email:", emailVal, "on date:", selectedDate, "timezone:", selectedTimezone);
fetch(checkUrl)
.then(response => response.json())
.then(res => {
// console.log("Fetched value from check_email API:", res);
nextBtn.textContent = 'Next Step';
nextBtn.classList.remove('disabled');
nextBtn.style.pointerEvents = 'auto';
nextBtn.style.opacity = '1';
if (res && res.status === 'found') {
const startDate = new Date(res.startTime);
const formattedTime = startDate.toLocaleString([], { dateStyle: 'full', timeStyle: 'short' });
// let meetLinkHtml = '';
// if (res.meetLink) {
// meetLinkHtml = ` Google Meet Link`;
// }
const err = stepwrp.querySelector('#sf-step5-error');
if (err) {
err.innerHTML = `You already have an appointment scheduled for: ${formattedTime}`;
err.style.display = 'block';
err.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
nextBtn.textContent = 'Next Step';
nextBtn.classList.add('disabled');
nextBtn.style.pointerEvents = 'none';
nextBtn.style.opacity = '0.5';
} else {
const nextStep = currentStep + 1;
updateReviewStep();
transitionStep(currentStep, nextStep);
stepwrp.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
})
.catch(err => {
console.error("Error checking email booking status:", err);
nextBtn.textContent = 'Next Step';
nextBtn.classList.remove('disabled');
nextBtn.style.pointerEvents = 'auto';
nextBtn.style.opacity = '1';
const nextStep = currentStep + 1;
updateReviewStep();
transitionStep(currentStep, nextStep);
stepwrp.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
return;
}
}
if (currentStep < TOTAL_STEPS) {
const nextStep = currentStep + 1;
if (nextStep === 6) {
updateReviewStep();
}
transitionStep(currentStep, nextStep);
stepwrp.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else {
submitBookingForm();
}
}
function hideElementById(id) {
const el = stepwrp.querySelector(`#${id}`);
if (el) el.style.display = 'none';
}
function handlePrevStep() {
if (currentStep > 1) {
transitionStep(currentStep, currentStep - 1);
stepwrp.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
function clearErrors() {
stepwrp.querySelectorAll('.sf-step-error-msg, .sf-input-error-msg').forEach((el) => {
el.style.display = 'none';
});
stepwrp.querySelectorAll('.sf-custom-input-wpr, .sf-custom-textarea-wpr').forEach((el) => {
el.classList.remove('error');
});
}
function showFieldError(inputEl, errorElId, message) {
const wpr = inputEl.closest('.sf-custom-input-wpr, .sf-custom-textarea-wpr');
if (wpr) wpr.classList.add('error');
const err = stepwrp.querySelector(`#${errorElId}`);
if (err) {
if (message) err.textContent = message;
err.style.display = 'block';
}
}
function validateStep(step) {
clearErrors();
let isValid = true;
if (step === 1) {
if (!stepwrp.querySelector('input[name="create"]:checked')) {
const err = stepwrp.querySelector('#sf-step1-error');
if (err) err.style.display = 'block';
isValid = false;
}
}
if (step === 2) {
if (stepwrp.querySelectorAll('input[name="goals"]:checked').length === 0) {
const err = stepwrp.querySelector('#sf-step2-error');
if (err) err.style.display = 'block';
isValid = false;
}
}
if (step === 3) {
if (stepwrp.querySelectorAll('input[name="audience"]:checked').length === 0) {
const err = stepwrp.querySelector('#sf-step3-error');
if (err) err.style.display = 'block';
isValid = false;
} else {
const otherCheckbox = stepwrp.querySelector('input[name="audience"][value="other"]');
if (otherCheckbox && otherCheckbox.checked) {
const audienceDetails = stepwrp.querySelector('#sf-audience-details');
if (!audienceDetails || !audienceDetails.value.trim()) {
const err = stepwrp.querySelector('#sf-step3-other-error');
if (err) err.style.display = 'block';
isValid = false;
}
}
}
}
if (step === 4) {
if (!stepwrp.querySelector('input[name="timeline"]:checked')) {
const err = stepwrp.querySelector('#sf-timeline-error');
if (err) err.style.display = 'block';
isValid = false;
}
if (!stepwrp.querySelector('input[name="planning_process"]:checked')) {
const err = stepwrp.querySelector('#sf-planning-error');
if (err) err.style.display = 'block';
isValid = false;
}
}
if (step === 5) {
const firstNameVal = firstNameInput.value.trim();
const lastNameVal = lastNameInput.value.trim();
const emailVal = emailInput.value.trim();
const companyVal = companyInput.value.trim();
const websiteVal = websiteInput.value.trim();
if (!firstNameVal) {
showFieldError(firstNameInput, 'sf-first-name-error', 'First name is required.');
isValid = false;
} else if (!validateNameFormat(firstNameVal)) {
showFieldError(firstNameInput, 'sf-first-name-error', 'First name can only contain letters.');
isValid = false;
}
if (!lastNameVal) {
showFieldError(lastNameInput, 'sf-last-name-error', 'Last name is required.');
isValid = false;
} else if (!validateNameFormat(lastNameVal)) {
showFieldError(lastNameInput, 'sf-last-name-error', 'Last name can only contain letters.');
isValid = false;
}
if (!emailVal) {
showFieldError(emailInput, 'sf-email-error', 'Email address is required.');
isValid = false;
} else if (/[A-Z]/.test(emailVal)) {
showFieldError(emailInput, 'sf-email-error', 'Email address must not contain capital letters.');
isValid = false;
} else if (!validateEmail(emailVal)) {
showFieldError(emailInput, 'sf-email-error', 'Please enter a valid email address.');
isValid = false;
}
if (!companyVal) {
showFieldError(companyInput, 'sf-company-error', 'Company name is required.');
isValid = false;
}
if (websiteVal && !validateURL(websiteVal)) {
showFieldError(websiteInput, 'sf-website-error', 'Please enter a valid website URL.');
isValid = false;
}
if (!selectedDate || !selectedTime) {
const err = stepwrp.querySelector('#sf-step5-error');
if (err) err.style.display = 'block';
isValid = false;
}
}
return isValid;
}
function validateEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
function validateNameFormat(name) {
const re = /^[a-zA-Z\s'-]+$/;
return re.test(name);
}
function validateURL(url) {
const re = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/.*)?$/;
return re.test(url);
}
// --- Final Booking Submission ---
function submitBookingForm() {
syncFormData();
setSubmitLoading(true);
const step6Error = stepwrp.querySelector('#sf-step6-error');
if (step6Error) step6Error.style.display = 'none';
const firstName = firstNameInput.value.trim();
const lastName = lastNameInput.value.trim();
let startISO = '';
let endISO = '';
if (selectedDate && selectedSlotIndex !== -1) {
const daySlots = getSlotsForSelectedDate(selectedDate, selectedTimezone);
const selectedSlot = daySlots[selectedSlotIndex];
if (selectedSlot) {
startISO = selectedSlot.startISO;
const startUtc = Date.parse(startISO);
const endUtc = startUtc + apiSlotDuration * 60 * 1000;
endISO = new Date(endUtc).toISOString();
}
}
if (typeof window.createGoogleCalendarEvent === 'function') {
window.createGoogleCalendarEvent(
formData,
{
firstName: firstName,
lastName: lastName,
email: emailInput.value.trim(),
company: companyInput.value.trim(),
website: websiteInput.value.trim(),
role: roleInput.value.trim(),
audienceDetails: audienceDetailsInput.value.trim(),
budgetContext: budgetContextInput.value.trim(),
selectedDate: selectedDate,
selectedTime: selectedTime,
selectedTimezone: selectedTimezone,
startISO: startISO,
endISO: endISO
},
function (meetLink) {
setSubmitLoading(false);
executeSuccessScreenTransition(firstName, lastName, meetLink);
},
function (errorMsg) {
setSubmitLoading(false);
showSubmitError(errorMsg);
}
);
} else {
console.warn("window.createGoogleCalendarEvent is not defined. Simulating API event creation...");
setTimeout(() => {
setSubmitLoading(false);
const meetLink = "https://meet.google.com/mock-meet-link";
executeSuccessScreenTransition(firstName, lastName, meetLink);
}, 1500);
}
}
// --- Restart Form Handler ---
function handleRestart() {
currentStep = 1;
selectedDate = null;
selectedTime = null;
selectedSlotIndex = -1;
selectTimezone(detectedLocalTimezoneName);
const systemDate = new Date();
calendarYear = systemDate.getFullYear();
calendarMonth = systemDate.getMonth();
renderCalendar(calendarYear, calendarMonth);
renderTimeSlots();
form.reset();
clearErrors();
stepwrp.querySelectorAll('.sf-option-card').forEach((card) => {
card.classList.remove('selected');
const input = card.querySelector('input');
if (input) input.checked = false;
});
stepwrp.querySelectorAll('.sf-pill-option').forEach((pill) => {
pill.classList.remove('selected');
const input = pill.querySelector('input');
if (input) input.checked = false;
});
calendarGrid.querySelectorAll('.sf-calendar-day.selected').forEach((day) => day.classList.remove('selected'));
timeSlotsContainer.querySelectorAll('.sf-time-slot.selected').forEach((slot) => slot.classList.remove('selected'));
updateAppointmentPreview();
toggleOtherAudience();
successScreen.classList.remove('active');
setTimeout(() => {
successScreen.style.display = 'none';
form.style.display = 'block';
stepwrp.querySelector('.sf-card-footer').style.display = 'flex';
stepContents.forEach((content) => content.classList.remove('active', 'fade-in'));
const firstStepEl = stepwrp.querySelector('.sf-step-content[data-step-content="1"]');
firstStepEl.classList.add('active', 'fade-in');
updateStepIndicator();
}, 300);
}
function toggleOtherAudience() {
const otherCheckbox = stepwrp.querySelector('input[name="audience"][value="other"]');
const container = stepwrp.querySelector('#sf-other-audience-container');
if (otherCheckbox && container) {
if (otherCheckbox.checked) {
container.style.display = 'block';
const textarea = container.querySelector('textarea');
if (textarea && !textarea._errorListenerAttached) {
textarea._errorListenerAttached = true;
textarea.addEventListener('input', function () {
if (textarea.value.trim()) {
hideElementById('sf-step3-other-error');
}
});
}
} else {
container.style.display = 'none';
hideElementById('sf-step3-other-error');
const textarea = container.querySelector('textarea');
if (textarea) {
textarea.value = '';
}
}
}
}
setTimeout(() => {
stepwrp.querySelector('.sf-step-content[data-step-content="1"]').classList.add('fade-in');
}, 100);
}
});