// --- 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 = `

Loading slots...

`; } 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); } });