/*eslint no-unused-vars: ["error", { "varsIgnorePattern": "NECTLY_.*|proResetPayout|setupCharity|renderConversationHistoryTable|requestStripeInformation|activateSeeker|proSelectSlot|proSelectSlots|activateProfessional|redirectToStripeConnectLink|startCreditCardAuthorization|logoutUser|sendEarlyAccessEmail|initStripePayment|joinMeeting|onLoad|deleteSlots|proCancelMeeting|seekerCancelMeeting|onProTimeSlotSelectChange|onTimeSlotSelectChange|deleteSlot|proDeleteSlot|proDeleteSlots|proSaveSession|seekerSaveSession|proAppointmentsTabClick|seekerAppointmentsTabClick|proSlotsTabClick|seekerSlotsTabClick|reserveSlot|.*UserRole$" }]*/ /*global google Stripe*/ let calendar = null; let mobileCalendar = null; let seekerCalendar = null; let mobileSeekerCalendar = null; let proCalendar = null; let mobileProCalendar = null; function callApi(url, method = 'GET', data = null) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(method, url); if (data) { xhr.setRequestHeader('Content-Type', 'application/json'); } xhr.onload = function() { if (xhr.status >= 200 && xhr.status < 300) { resolve({ status: xhr.status, response: xhr.responseText }); } else { reject({ status: xhr.status, error: xhr.responseText }); } }; xhr.onerror = function() { reject({ status: 0, error: 'Network error occurred' }); }; xhr.send(data ? JSON.stringify(data) : null); }); } function convertMonthStringToInt(monthString) { const lowerCaseMonth = monthString.toLowerCase(); switch (lowerCaseMonth) { case 'january': return 0; case 'february': return 1; case 'march': return 2; case 'april': return 3; case 'may': return 4; case 'june': return 5; case 'july': return 6; case 'august': return 7; case 'september': return 8; case 'october': return 9; case 'november': return 10; case 'december': return 11; default: console.error(`Error: "${monthString}" is not a valid month string.`); return -1; } } function convertIntMonthToString(monthInt) { const monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; if (monthInt >= 0 && monthInt <= 11) { return monthNames[monthInt]; } else { console.error(`Error: "${monthInt}" is not a valid month integer (0-11).`); return null; } } function getMeta(metaName) { const metas = document.getElementsByTagName('meta'); for (let i = 0; i < metas.length; i++) { if (metas[i].getAttribute('property') === metaName) { return metas[i].getAttribute('content'); } } return ''; } function logoutUser() { return callApi('/api/logout', 'POST') .then(data => { console.log('Logout successful:', data); window.location.href = '/'; return data; }) .catch(error => { console.error('Logout failed:', error); throw error; }); } function initStripePayment(user_id, user_email) { var url = new URL("https://buy.stripe.com/test_28E4gA0aJ4iM0BqaaF7Zu01"); url.searchParams.append("customer_email", user_email); url.searchParams.append("client_reference_id", user_id); url.searchParams.append("prefilled_email", user_email); window.location.href = url.toString(); } let resolveLinkDateElements = []; let linkLastDayInMonth = null; let activeLinkDayElement = null; let resolveMobileLinkDateElements = []; let linkMobileLastDayInMonth = null; let activeMobileLinkDayElement = null; function loopLinkDaysInMonth(self, dateEl) { const leftArrow = self.context.mainElement.querySelectorAll('[data-vc-arrow="prev"]'); const rightArrow = self.context.mainElement.querySelectorAll('[data-vc-arrow="prev"]'); if (leftArrow.length > 0) leftArrow[0].disabled = true; if (rightArrow.length > 0) rightArrow[0].disabled = true; const btnEl = dateEl.querySelector('[data-vc-date-btn]'); const btnDate = new Date(dateEl.getAttribute('data-vc-date')); if (btnDate.getMonth() !== self.context.selectedMonth) { return; } if (self.context.mainElement.id === linkCalendarElementId) { if (activeLinkDayElement === null) { const today = new Date(); if (today.getDate() === btnDate.getDate()) { activeLinkDayElement = btnEl; } } } else { if (activeMobileLinkDayElement === null) { const today = new Date(); if (today.getDate() === btnDate.getDate()) { activeMobileLinkDayElement = btnEl; } } } const day = btnEl.innerText; if (parseInt(day) === 1) { if (self.context.mainElement.id === linkCalendarElementId) { resolveLinkDateElements = [dateEl]; linkLastDayInMonth = getLastDayOfMonth(btnDate.getFullYear(), convertIntMonthToString(btnDate.getMonth())); } else { resolveMobileLinkDateElements = [dateEl]; linkMobileLastDayInMonth = getLastDayOfMonth(btnDate.getFullYear(), convertIntMonthToString(btnDate.getMonth())); } } else { if (self.context.mainElement.id === linkCalendarElementId) { resolveLinkDateElements.push(dateEl); } else { resolveMobileLinkDateElements.push(dateEl); } } let endDayToCompare = null; if (self.context.mainElement.id === linkCalendarElementId) { endDayToCompare = linkLastDayInMonth; } else { endDayToCompare = linkMobileLastDayInMonth; } if (endDayToCompare.getDate() === btnDate.getDate()) { getCurrentMonth(getLinkPageUserId(), endDayToCompare.toISOString(), function(currentMonth) { let dateElements = null; if (self.context.mainElement.id === linkCalendarElementId) { dateElements = resolveLinkDateElements } else { dateElements = resolveMobileLinkDateElements } const busyDaysInMonth = currentMonth.busyDaysInMonth; for (const dateEl of dateElements) { const btnEl = dateEl.querySelector('[data-vc-date-btn]'); const btnDate = new Date(dateEl.getAttribute('data-vc-date')); const today = new Date(); today.setHours(0, 0, 0, 0); btnDate.setHours(0, 0, 0, 0); const day = btnEl.innerText; if (btnDate < today) continue; if (busyDaysInMonth.indexOf(parseInt(day)) !== -1) { setDayElementAsBusy(btnEl); } } if (self.context.mainElement.id === linkCalendarElementId) { resolveLinkDateElements = []; } else { resolveMobileLinkDateElements = []; } if (leftArrow.length > 0) leftArrow[0].disabled = false; if (rightArrow.length > 0) rightArrow[0].disabled = false; console.log("Days in month updated") }, function(error) { console.log("Error fetching slots:", error); if (self.context.mainElement.id === linkCalendarElementId) { resolveLinkDateElements = []; } else { resolveMobileLinkDateElements = []; } if (leftArrow.length > 0) leftArrow[0].disabled = false; if (rightArrow.length > 0) rightArrow[0].disabled = false; }); } } function renderLinkCalendar(onChange, refreshSlots, elementId) { const { Calendar } = window.VanillaCalendarPro; const calendar = new Calendar( `#${elementId}`, { onCreateDateEls: loopLinkDaysInMonth, disableAllDates: true, enableDates:[getEnabledDates()], firstWeekday: 0, type: 'default', onClickArrow(self) { console.log("Month changed to", self.context.selectedYear, self.context.selectedMonth); onChange('month', convertIntMonthToString(self.context.selectedMonth)); }, onClickDate(self, pointerEvent) { const btnEl = pointerEvent.srcElement; console.log("Day clicked", self.context.selectedDates, btnEl); if (self.context.mainElement.id === linkCalendarElementId) { activeLinkDayElement = btnEl; } else { activeMobileLinkDayElement = btnEl; } onChange('value', self.context.selectedDates[0]); }, }); calendar.init(); if (refreshSlots) { const now = new Date(); refreshCalendarSlots("link-calendar-slots-list", now); refreshSlotsHeader(now); } return calendar; } function onLoad() { renderTopMenu(); renderFooter(); setupTogglePairs(); loadFaqToggle(); initLinkButtonGlowEffect(); if (typeof google !== 'undefined') { google.accounts.id.initialize({ client_id: "1089541127844-jciirjufc6pli3d0af9o9s8mq4t0qk0d.apps.googleusercontent.com", callback: function (response) {console.info('Google Auth Response: ', response);}, login_uri: "https://nectly.io/google-oauth-redirect", ux_mode: "redirect", context: "signup" }); google.accounts.id.prompt(); const signUpButton = document.getElementById("nav-signup"); if (signUpButton) { google.accounts.id.renderButton( signUpButton, { type: "standard", shape: "rectangular", theme: "outline", text: "signin_with", size: "large", logo_alignment: "left", locale: "en", } ); } } if (document.getElementById(earlyAccessElementId)) renderEarlyAccess(); setupEarlyAccessToggle(); const calendarElement = document.getElementById(linkCalendarElementId); const mobileCalendarElement = document.getElementById(linkCalendarMobileElementId); if (calendarElement) { calendar = renderLinkCalendar(onCalendarChange, window.innerWidth >= 1024, linkCalendarElementId); } if (mobileCalendarElement) { mobileCalendar = renderLinkCalendar(onMobileCalendarChange, window.innerWidth < 1024, linkCalendarMobileElementId); } if (mobileCalendarElement || calendarElement) { renderTimeZoneString(); } const seekerCalendarElement = document.getElementById(seekerCalendarElementId); const mobileSeekerCalendarElement = document.getElementById(seekerCalendarMobileElementId); if (seekerCalendarElement) { seekerCalendar = renderSeekerCalendar(onSeekerCalendarChange, seekerCalendarElementId); seekerRefreshCalendarSlots(); } if (mobileSeekerCalendarElement) { mobileSeekerCalendar = renderSeekerCalendar(onMobileSeekerCalendarChange, seekerCalendarMobileElementId); seekerRefreshCalendarSlots(); } const proCalendarElement = document.getElementById(proCalendarElementId); const mobileProCalendarElement = document.getElementById(proCalendarMobileElementId); if (proCalendarElement) { proCalendar = renderProCalendar(onProCalendarChange, proCalendarElementId); proRefreshCalendarSlots(); } if (mobileProCalendarElement) { mobileProCalendar = renderProCalendar(onMobileProCalendarChange, proCalendarMobileElementId); proRefreshCalendarSlots(); } if (mobileSeekerCalendarElement || seekerCalendarElement) { renderTimeZoneString(); } setupResponsiveMenuToggle(); setupCancelMeetingToggle(); } const NECTLY_USER_ROLE_SEEKER = "seeker"; const NECTLY_USER_ROLE_PROFESSIONAL = "professional"; const NECTLY_DAILY_SCHEDULE = "daily"; const NECTLY_WEEKLY_SCHEDULE = "weekly"; const NECTLY_MONTHLY_SCHEDULE = "monthly"; const NECTLY_EXACT_PERIOD_SCHEDULE = "exact_period" const postReserveSlotElementId = "link-post-reserve-slot"; const linkEmailInputElementId = 'link-calendar-professional-email'; const linkNameInputElementId = 'link-calendar-professional-name'; const earlyAccessElementId = 'early-access'; const seekerCalendarElementId = 'seeker-calendar'; const seekerCalendarMobileElementId = 'seeker-calendar-mobile'; const proCalendarElementId = 'pro-calendar'; const proCalendarMobileElementId = 'pro-calendar-mobile'; const linkCalendarElementId = 'link-calendar'; const linkCalendarMobileElementId = 'link-calendar-mobile'; function getUserRole(onSuccess, onError) { return callApi('/api/user/roles') .then(data => { console.log('User roles:', data); if (onSuccess) onSuccess(JSON.parse(data.response)); }) .catch(error => { console.error('Error fetching user role:', error); if (onError) onError(error); }); } function addUserRole(user_role, onSuccess, onError) { return callApi('/api/user/roles', 'POST', { role: user_role }) .then(data => { console.log('User role added:', data); if (onSuccess) onSuccess(data.response); }) .catch(error => { console.error('Error adding user role:', error); if (onError) onError(error); }); } function removeUserRole(user_role, onSuccess, onError) { return callApi('/api/user/roles', 'DELETE', { role: user_role }) .then(data => { console.log('User role removed:', data); if (onSuccess) onSuccess(data.response); }) .catch(error => { console.error('Error removing user role:', error); if (onError) onError(error); }); } function getCurrentMonth(seeker_id, selectedDate, onSuccess, onError) { return callApi(`/api/slots?userId=${seeker_id}&date=${selectedDate}`) .then(data => { console.log('Current month:', data); if (onSuccess) onSuccess(JSON.parse(data.response)); }) .catch((error) => { console.error('Error fetching current month:', error); if (onError) onError(error); /*onSuccess(JSON.parse( `{ "slots": [ { "startTime": "2023-10-01T09:00:00Z", "endTime": "2023-10-01T17:05:00Z", "title": "Meeting with John Doe", "description": "Discuss project updates and next steps." } ], "busyDaysInMonth": [ 4, 20, 25, 26 ], "seekerFullName": "John Doe" }` ));*/ }); } function refreshSlotsHeader(value) { const slotsHeader = document.getElementById("link-calendar-picked-date-header"); if (!slotsHeader) { console.error("Element with ID link-calendar-picked-date-header not found."); return; } const selectedDate = value; slotsHeader.innerHTML = selectedDate.toLocaleString('en-US', { weekday: 'long', month: 'short', day: 'numeric', hour12: true }); } function getLastDayOfMonth(year, month) { const intMonth = convertMonthStringToInt(month); const date = new Date(year, intMonth + 1, 0); return date; } function onCalendarChange(calendarParameter, value) { if (window.innerWidth < 1024) return; if (calendarParameter === "value") { console.log("Calendar value changed to", value); refreshCalendarSlots("link-calendar-slots-list", getCalendarSelectedDate(calendar)); refreshSlotsHeader(getCalendarSelectedDate(calendar)); } if (calendarParameter === "month") { console.log("Calendar month changed to", value); resetPostReserveSlotElement(); } } function onMobileCalendarChange(calendarParameter, value) { if (window.innerWidth >= 1024) return; if (calendarParameter === "value") { console.log("Mobile calendar value changed to", value); refreshCalendarSlots("link-calendar-slots-list", getCalendarSelectedDate(mobileCalendar)); refreshSlotsHeader(getCalendarSelectedDate(mobileCalendar)); } if (calendarParameter === "month" && mobileCalendar.month !== value) { console.log("Mobile calendar month changed to", value); resetPostReserveSlotElement(); } } function toggleConfirmationButton(disabled) { document.getElementById(linkEmailInputElementId).disabled = disabled; document.getElementById(linkNameInputElementId).disabled = disabled; document.getElementById("confirmMeetingButton").disabled = disabled; } function getVisibleCalendar() { if (window.innerWidth >= 1024) { return calendar; } else { return mobileCalendar; } } function updateCalendarDays(busyDaysInMonth) { console.log("Updating calendar days with busy days:", busyDaysInMonth); if (activeLinkDayElement) { const day = activeLinkDayElement.innerText; if (busyDaysInMonth.indexOf(parseInt(day)) !== -1) { setDayElementAsBusy(activeLinkDayElement); } } } function updateMobileCalendarDays(busyDaysInMonth) { console.log("Updating calendar days with busy days:", busyDaysInMonth); if (activeMobileLinkDayElement) { const day = activeMobileLinkDayElement.innerText; if (busyDaysInMonth.indexOf(parseInt(day)) !== -1) { setDayElementAsBusy(activeMobileLinkDayElement); } } } function refreshCalendarSlots(targetElementId, selectedDate, calledFromPage) { function renderSlotFromTo(slot) { const startTime = new Date(slot.startTime); const endTime = new Date(slot.endTime); const fromDate = startTime.toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); const toDate = endTime.toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); return `${fromDate} - ${toDate}`; } function renderSlotRow(slot, message) { if (message) { const option = document.createElement('option'); option.value = message; option.textContent = message; return option; } const option = document.createElement('option'); option.value = `${slot.startTime}@${slot.endTime}`; option.textContent = renderSlotFromTo(slot); return option; } resetPostReserveSlotElement(); const slotsElement = document.getElementById(targetElementId); if (!slotsElement) { console.error(`Element with ID ${targetElementId} not found.`); return; } const seekerId = getLinkPageUserId(); if (!selectedDate) { selectedDate = getCalendarSelectedDate(getVisibleCalendar()); } slotsElement.innerHTML = ""; // Clear existing slots getCurrentMonth(seekerId, selectedDate.toISOString(), function(currentMonth) { console.log("Fetched slots:", currentMonth.slots); updateCalendarDays(currentMonth.busyDaysInMonth); updateMobileCalendarDays(currentMonth.busyDaysInMonth); document.getElementById("link-welcome-message").innerHTML = `${currentMonth.seekerFullName} has invited you to 20-minute meeting`; if (currentMonth.slots?.length === 0) { const errorMessage = `
No Available Slots Icon

No available slots for this day.

Try another date.

`; const postReserveSlotElement = document.getElementById(postReserveSlotElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = errorMessage; } else { console.error(`Element with ID ${postReserveSlotElementId} not found.`); } toggleConfirmationButton(true); return; } currentMonth.slots.forEach(slot => { slotsElement.appendChild(renderSlotRow(slot)); toggleConfirmationButton(false); }); if (calledFromPage) { calendar.update(); mobileCalendar.update(); } }, function(error) { console.error("Error fetching slots:", error); if (error.status === 400 && error.error === "400: Selected date cannot be in the past") { selectedDateIsinThePast(); } else { retrievingSlotsFailed() } toggleConfirmationButton(true); }); } function getLinkPageUserId() { return getMeta('user-id'); } function selectedDateIsinThePast() { const errorMessage = `

Selected date cannot be in the past.

Don’t worry, just choose another slot to connect.

`; const postReserveSlotElement = document.getElementById(postReserveSlotElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = errorMessage; } else { console.error(`Element with ID ${postReserveSlotElementId} not found.`); } } function retrievingSlotsFailed() { const errorMessage = `

Retrieving slots failed.

Try to refresh later.

`; const postReserveSlotElement = document.getElementById(postReserveSlotElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = errorMessage; } else { console.error(`Element with ID ${postReserveSlotElementId} not found.`); } } function onReserveSlotError() { const errorMessage = `

That time’s no longer available—someone else grabbed it.

Don’t worry, just choose another slot to connect.

`; const postReserveSlotElement = document.getElementById(postReserveSlotElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = errorMessage; } else { console.error(`Element with ID ${postReserveSlotElementId} not found.`); } } function onReserveSlotSuccess() { const successMessage = `
Email Sent Icon

You’re all set.

Check your inbox for the details—and enjoy the conversation.

`; const postReserveSlotElement = document.getElementById(postReserveSlotElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = successMessage; } else { console.error(`Element with ID ${postReserveSlotElementId} not found.`); } } function resetPostReserveSlotElement() { const postReserveSlotElement = document.getElementById(postReserveSlotElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = ""; } else { console.error(`Element with ID ${postReserveSlotElementId} not found.`); } } function validateEmail(email) { return String(email).toLowerCase().match( /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ ); } function reserveSlot(targetElementId) { function getLinkType() { if (window.location.href.indexOf('/link/') !== -1) { return NECTLY_USER_ROLE_PROFESSIONAL; } else if (window.location.href.indexOf('/connect/') !== -1) { return NECTLY_USER_ROLE_SEEKER; } } const attendee = document.getElementById(linkEmailInputElementId).value; const attendeeName = document.getElementById(linkNameInputElementId).value; const validAttendee = validateEmail(attendee); if (!validAttendee) { console.log("Invalid email address:", attendee); document.getElementById(linkEmailInputElementId).focus(); return; } if (!attendeeName || attendeeName.trim() === "" || attendeeName.length > 60 || attendeeName.indexOf('"') !== -1) { console.log("Invalid name:", attendeeName); document.getElementById(linkNameInputElementId).focus(); return; } const slotsElement = document.getElementById(targetElementId); if (!slotsElement) { console.error(`Element with ID ${targetElementId} not found.`); return; } const selectedOption = slotsElement.options[slotsElement.selectedIndex]; if (!selectedOption) { console.error("No slot selected."); return; } toggleConfirmationButton(true); const startTime = selectedOption.value.split('@')[0]; const endTime = selectedOption.value.split('@')[1]; console.log("Reserving slot from", startTime, "to", endTime); const reserveSlotBody = { "startTime": startTime, "endTime": endTime, "attendee": attendee, "attendeeName": attendeeName, } if (getLinkType() === NECTLY_USER_ROLE_SEEKER) { console.log("Reserve slot body:", reserveSlotBody); const seeker_id = getLinkPageUserId(); console.log("Seeker ID:", seeker_id); callApi(`/api/slots?userId=${seeker_id}`, 'POST', reserveSlotBody) .then(data => { console.log('Current month:', data) onReserveSlotSuccess(); }) .catch((error) => { console.error('Error reserving slot:', error); onReserveSlotError(); }); } else if (getLinkType() === NECTLY_USER_ROLE_PROFESSIONAL) { const professional_id = getLinkPageUserId(); reserveSlotBody["professionalId"] = professional_id; console.log("Professional ID:", professional_id); console.log("Reserve professional slot body:", reserveSlotBody); callApi(`/api/pro-slots?userId=${professional_id}`, 'POST', reserveSlotBody) .then(data => { console.log('Current month:', data) const body = JSON.parse(data.response); var url = new URL("https://buy.stripe.com/test_8x2aEYg9Hg1uesg6Yt7Zu02"); url.searchParams.append("customer_email", attendee); url.searchParams.append("client_reference_id", `${professional_id}---${body.calendarId}---${body.appointmentId}`); url.searchParams.append("prefilled_email", attendee); window.location.href = url.toString(); }) .catch((error) => { console.error('Error reserving slot:', error); onReserveSlotError(); }); } } function setupResponsiveMenuToggle() { const hamburger = document.getElementById('hamburger-btn'); const menuWrapper = document.getElementById('menu-items-wrapper'); if (!hamburger || !menuWrapper) return; let isManuallyOpened = false; function updateMenuDisplay() { if (window.innerWidth > 1024) { menuWrapper.classList.add('active'); hamburger.classList.remove('active'); isManuallyOpened = false; } else { if (!isManuallyOpened) { menuWrapper.classList.remove('active'); hamburger.classList.remove('active'); } } } window.addEventListener('resize', updateMenuDisplay); updateMenuDisplay(); hamburger.addEventListener('click', () => { if (window.innerWidth <= 1024) { const isActive = hamburger.classList.toggle('active'); menuWrapper.classList.toggle('active'); isManuallyOpened = isActive; } }); } function setupEarlyAccessToggle() { const toggle = document.getElementById('earlyAccessToggle'); const close = document.getElementById('closeEarlyAccessToggle'); const content = document.getElementById('earlyAccessContent'); if (!toggle || !close || !content) return; const container = toggle.closest('.early-access-toggle-container'); if (!container) return; toggle.addEventListener('click', () => { if (!container.classList.contains('open')) { content.style.height = content.scrollHeight + 'px'; container.classList.add('open'); } }); close.addEventListener('click', () => { content.style.height = '0px'; container.classList.remove('open'); }); } function renderTimeZoneString() { const timeZoneString = (new Date()).toLocaleDateString("en-US", {timeZoneName: "long"}).split(', ')[1]; const innerHTMLString = `

Time zone

${timeZoneString}

`; const timeZoneElement = document.getElementById("time-zone-container"); if (timeZoneElement) { timeZoneElement.innerHTML = innerHTMLString; } else { console.error("Element with ID time-zone-container not found."); } const timeZoneMobileElement = document.getElementById("time-zone-container-mobile"); if (timeZoneMobileElement) { timeZoneMobileElement.innerHTML = innerHTMLString; } else { console.error("Element with ID time-zone-container-mobile not found."); } } function renderFooter() { const footer = document.getElementById('footer'); if (footer) { footer.innerHTML = ` `; } else { console.error("Element with ID footer not found."); } } function renderTopMenu() { const topMenu = document.getElementById('menu'); if (topMenu) { const loginIsHidden = getMeta('login-is-hidden'); const logoutIsHidden = getMeta('logout-is-hidden'); let hideAvatarMenu = 'style="display:none"'; if (logoutIsHidden.trim().length === 0) hideAvatarMenu = ''; let settingsHtml = ` Settings `; if (getMeta('dashboard') === 'professional-dashboard.html') settingsHtml = ''; topMenu.innerHTML = ` `; if (document.querySelector('.scroll-container')) { topMenu.innerHTML += `
`; safeInitSwipeSections(); } } else { console.error("Element with ID top-menu not found."); } } function renderEarlyAccess() { const earlyAccess = document.getElementById(earlyAccessElementId); if (!earlyAccess) { console.error('Element with ID early-access not found.'); return; } earlyAccess.innerHTML = `
Close Icon

Try it first. Shape what’s next.

We’re launching soon—but a few curious minds get early access. Join our beta, test it out, and help us make Nectly even better.

Join the Beta Waitlist

Your role:

`; } const earlyAccessEmailElementId = "earlyAccessEmail"; const earlyAccessResultMessageElementId = "earlyAccessResultMessage"; const earlySeekerCheckBoxElementId = "earlySeekerCheckBox"; const earlyProfessionalCheckBoxElementId = "earlyProfessionalCheckBox"; const earlyAccessButtonElementId = "earlyAccessButton"; function sendEarlyAccessEmail(earlyAccessSecret) { const successMessage = `

You’re on the list—thank you!

We’ll be in touch soon with early access details. Can’t wait to see where your first Nectly conversation takes you.

`; const errorMessage = `

Joining the Beta Waitlist failed.

Try again later.

`; const earlyAccessResultMessageElement = document.getElementById(earlyAccessResultMessageElementId); if (!earlyAccessResultMessageElement) { console.error(`Element with ID ${earlyAccessResultMessageElementId} not found.`); return; } const attendee = document.getElementById(earlyAccessEmailElementId).value; const validAttendee = validateEmail(attendee); if (!validAttendee) { console.log("Invalid email address:", attendee); document.getElementById(earlyAccessEmailElementId).focus(); return; } const earlyAccessRoleSeeker = document.getElementById(earlySeekerCheckBoxElementId).checked; const earlyAccessRoleProfessional = document.getElementById(earlyProfessionalCheckBoxElementId).checked; if (!earlyAccessRoleSeeker && !earlyAccessRoleProfessional) { console.log("No role selected."); earlyAccessRoleSeeker.focus(); return; } const earlyAccessButtonElement = document.getElementById(earlyAccessButtonElementId); if (!earlyAccessButtonElement) { console.error(`Element with ID ${earlyAccessButtonElementId} not found.`); return; } earlyAccessButtonElement.disabled = true; callApi(`/api/early-access`, 'POST', { "email": attendee, "role": earlyAccessRoleSeeker ? "seeker" : "professional", "secret": earlyAccessSecret }).then(data => { earlyAccessResultMessageElement.innerHTML = successMessage; console.error('Early access email sent successfully:', data); }) .catch((error) => { earlyAccessResultMessageElement.innerHTML = errorMessage; console.error('Error sending early access email:', error); }) } function getProCurrentDate(selectedDate, timezone, onSuccess, onError) { return callApi(`/api/professional-slots?date=${selectedDate}&tzOffsetInMinutes=${timezone}`) .then(data => { console.log('Current month:', data); if (onSuccess) onSuccess(JSON.parse(data.response)); }) .catch((error) => { console.error('Error fetching current month:', error); if (onError) onError(error); }); } function getSeekerCurrentDate(selectedDate, timezone, onSuccess, onError) { return callApi(`/api/seeker-slots?date=${selectedDate}&tzOffsetInMinutes=${timezone}`) .then(data => { console.log('Current month:', data); if (onSuccess) onSuccess(JSON.parse(data.response)); }) .catch((error) => { console.error('Error fetching current month:', error); if (onError) onError(error); /*onSuccess(JSON.parse( `{ "freeSlots": [ { "slotType": "exact_period", "startTime": "2025-05-29T08:00:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T08:20:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T08:30:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T08:50:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T09:00:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T09:20:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T09:30:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T09:50:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T10:00:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T10:20:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T10:30:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T10:50:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T11:00:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T11:20:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T11:30:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T11:50:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T12:00:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T12:20:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T12:30:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T12:50:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T13:00:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T13:20:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T13:30:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T13:50:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T14:00:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T14:20:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T14:30:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T14:50:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T15:00:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T15:20:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T15:30:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T15:50:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T16:00:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T16:20:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T16:30:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T16:50:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T17:30:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T17:50:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T18:00:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T18:20:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T18:30:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T18:50:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T19:00:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T19:20:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-29T19:30:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-29T19:50:00+02:00" } ], "createdSlots": [], "appointments": [ { "createdAt": "2025-05-23T10:56:48.423125+00:00", "attendees": [ "zeman.silvie@gmail.com" ], "appointmentId": "826ac3a9-61cd-43c8-a45f-6dbd35e2d83b", "description": null, "startTime": "2025-05-29T17:00:00+02:00", "location": "", "endTime": "2025-05-29T17:20:00+02:00", "title": "" }, { "createdAt": "2025-05-27T16:15:46.941689+00:00", "attendees": [ "tvavra1@gmail.com" ], "appointmentId": "b2757583-55ca-44bf-8411-9fe7ebd3603a", "description": null, "startTime": "2025-05-30T15:30:00+02:00", "location": "https://meet.google.com/xww-krkr-mso", "endTime": "2025-05-30T15:50:00+02:00", "title": "" }, { "createdAt": "2025-05-23T17:57:37.550312+00:00", "attendees": [ "jakub@zeman.wtf" ], "appointmentId": "57d3199f-320a-44a3-9969-a664e4b20ea3", "description": null, "startTime": "2025-05-30T17:00:00+02:00", "location": "https://meet.google.com/uok-toqp-iqm", "endTime": "2025-05-30T17:20:00+02:00", "title": "" }, { "createdAt": "2025-05-23T16:40:39.399717+00:00", "attendees": [ "jakub@zeman.wtf" ], "appointmentId": "819860f3-8afb-46c7-9543-2aaad2723027", "description": null, "startTime": "2025-05-30T17:30:00+02:00", "location": "https://meet.google.com/vvj-avvn-oop", "endTime": "2025-05-30T17:50:00+02:00", "title": "" } ], "createdSlotsToDate": [], "createdSlotsToday": [], "allCreatedSlots": [ { "slotType": "exact_period", "startTime": "2025-05-30T16:00:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-30T16:20:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-30T16:30:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-30T16:50:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-31T12:30:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-31T12:50:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-05-31T15:30:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-05-31T15:50:00+02:00" }, { "slotType": "exact_period", "startTime": "2025-06-18T13:30:00+02:00", "title": "", "repeatCount": "-1", "endTime": "2025-06-18T13:50:00+02:00" } ], "appointmentsToDate": [ { "createdAt": "2025-05-27T16:15:46.941689+00:00", "attendees": [ "tvavra1@gmail.com" ], "appointmentId": "b2757583-55ca-44bf-8411-9fe7ebd3603a", "description": null, "startTime": "2025-05-30T15:30:00+02:00", "location": "https://meet.google.com/xww-krkr-mso", "endTime": "2025-05-30T15:50:00+02:00", "title": "" }, { "createdAt": "2025-05-23T17:57:37.550312+00:00", "attendees": [ "jakub@zeman.wtf" ], "appointmentId": "57d3199f-320a-44a3-9969-a664e4b20ea3", "description": null, "startTime": "2025-05-30T17:00:00+02:00", "location": "https://meet.google.com/uok-toqp-iqm", "endTime": "2025-05-30T17:20:00+02:00", "title": "" }, { "createdAt": "2025-05-23T16:40:39.399717+00:00", "attendees": [ "jakub@zeman.wtf" ], "appointmentId": "819860f3-8afb-46c7-9543-2aaad2723027", "description": null, "startTime": "2025-05-30T17:30:00+02:00", "location": "https://meet.google.com/vvj-avvn-oop", "endTime": "2025-05-30T17:50:00+02:00", "title": "" } ], "appointmentsToday": [], "appointmentsInMonth": [ 29, 30 ], "slotsInMonth": [ 30, 31 ], "countOfPaidSessions": 86, "countOfUsedSession": 0, "paymentMethodSet": true }` ));*/ }); } const postReserveSlotSeekerElementId = 'seeker-post-reserve-slot'; const seekerNewSessionDateSelect = 'seeker-date-slots-list'; const seekerNewSessionTimeSelect = 'seeker-calendar-slots-list'; const confirmMeetingButtonElementId = 'confirmMeetingButton'; const seekerAppointmentListElementId = 'seeker-appointment-list-container'; const seekerSlotsListElementId = 'seeker-slots-list-container'; const sessionBalanceSummaryElementId = 'session-balance-summary'; const seekerNoAppointmentsElementId = 'seeker-no-appointments'; const seekerNoSlotsElementId = 'seeker-no-slots'; const seekerTodayAppointmentsElementId = "seeker-today-appointments"; const seekerTodaySlotsElementId = "seeker-today-slots"; const seekerAppointmentsElementId = "seeker-appointments"; const seekerSlotsElementId = "seeker-slots"; const seekerAppointmentsDateTabElementId = "seeker-appointments-date-tab"; const seekerAppointmentsAllTabElementId = "seeker-appointments-all-tab"; const seekerSlotsDateTabElementId = "seeker-slots-date-tab"; const seekerSlotsAllTabElementId = "seeker-slots-all-tab"; const seekerRecurringSelectionElementId = "seeker-reccuring-selection"; const seekerBalanceProgressBarElementId = "session-balance-progress-bar"; const seekerSetupYourAccountMessageElementId = "seeker-setup-your-account-message"; const seekerSetPaymentMethodElementId = "seeker-set-payment-method"; const seekerAddYourAvailabilityElementId = "seeker-add-your-availability"; const seekerShareLinkElementId = "seeker-share-link"; const postReserveSlotProElementId = 'pro-post-reserve-slot'; const proNewSessionDateSelect = 'pro-date-slots-list'; const proNewSessionTimeSelect = 'pro-calendar-slots-list'; const proAppointmentListElementId = 'pro-appointment-list-container'; const proSlotsListElementId = 'pro-slots-list-container'; const proNoAppointmentsElementId = 'pro-no-appointments'; const proNoSlotsElementId = 'pro-no-slots'; const proTodayAppointmentsElementId = "pro-today-appointments"; const proTodaySlotsElementId = "pro-today-slots"; const proAppointmentsElementId = "pro-appointments"; const proSlotsElementId = "pro-slots"; const proAppointmentsDateTabElementId = "pro-appointments-date-tab"; const proAppointmentsAllTabElementId = "pro-appointments-all-tab"; const proSlotsDateTabElementId = "pro-slots-date-tab"; const proSlotsAllTabElementId = "pro-slots-all-tab"; const proRecurringSelectionElementId = "pro-reccuring-selection"; const proSetPaymentMethodStripeElementId = "pro-set-payment-method-stripe"; const proSetPaymentMethodStripeOrElementId = "pro-set-payment-method-or"; const proSetPaymentMethodCharityElementId = "pro-set-payment-method-charity"; const proLinkElementId = "pro-link"; const proShareLinkElementId = "pro-share-link"; const proSetupYourAccountMessageElementId = "pro-setup-your-account-message"; const proEarningsElementId = "pro-earnings"; const proAddYourAvailabilityElementId = "pro-add-your-availability"; function resetSeekerSetup() { const seekerSetupYourAccountMessageElement = document.getElementById(seekerSetupYourAccountMessageElementId); if (seekerSetupYourAccountMessageElement) { seekerSetupYourAccountMessageElement.innerHTML = ""; seekerSetupYourAccountMessageElement.className = ""; } const seekerSetPaymentMethodElement = document.getElementById(seekerSetPaymentMethodElementId); if (seekerSetPaymentMethodElement) { seekerSetPaymentMethodElement.innerHTML = ""; seekerSetPaymentMethodElement.className = ""; } const seekerAddYourAvailabilityElement = document.getElementById(seekerAddYourAvailabilityElementId); if (seekerAddYourAvailabilityElement) { seekerAddYourAvailabilityElement.innerHTML = ""; seekerAddYourAvailabilityElement.className = ""; } const seekerShareLinkElement = document.getElementById(seekerShareLinkElementId); if (seekerShareLinkElement) { seekerShareLinkElement.innerHTML = ""; seekerShareLinkElement.className = ""; } } function resetProSetup() { const proSetPaymentMethodStripeElement = document.getElementById(proSetPaymentMethodStripeElementId); if (proSetPaymentMethodStripeElement) { proSetPaymentMethodStripeElement.innerHTML = ""; proSetPaymentMethodStripeElement.className = ""; } const proSetPaymentMethodStripeOrElement = document.getElementById(proSetPaymentMethodStripeOrElementId); if (proSetPaymentMethodStripeOrElement) { proSetPaymentMethodStripeOrElement.innerHTML = ""; proSetPaymentMethodStripeOrElement.className = ""; } const proSetPaymentMethodCharityElement = document.getElementById(proSetPaymentMethodCharityElementId); if (proSetPaymentMethodCharityElement) { proSetPaymentMethodCharityElement.innerHTML = ""; proSetPaymentMethodCharityElement.className = ""; } const proSetupYourAccountMessageElement = document.getElementById(proSetupYourAccountMessageElementId); if (proSetupYourAccountMessageElement) { proSetupYourAccountMessageElement.innerHTML = ""; proSetupYourAccountMessageElement.className = ""; } const proAddYourAvailabilityElement = document.getElementById(proAddYourAvailabilityElementId); if (proAddYourAvailabilityElement) { proAddYourAvailabilityElement.innerHTML = ""; proAddYourAvailabilityElement.className = ""; } const proLinkElement = document.getElementById(proLinkElementId); if (proLinkElement) { proLinkElement.innerHTML = ""; proLinkElement.className = ""; } const proShareLinkElement = document.getElementById(proShareLinkElementId); if (proShareLinkElement) { proShareLinkElement.innerHTML = ""; proShareLinkElement.className = ""; } } function renderProPaymentMethodElement(index) { const proSetPaymentMethodStripeElement = document.getElementById(proSetPaymentMethodStripeElementId); if (proSetPaymentMethodStripeElement) { let indexHeader = ""; if (index > 0) { indexHeader = `

${index}A

`; } proSetPaymentMethodStripeElement.className = "setup-account-container flex flex-column halfgap"; proSetPaymentMethodStripeElement.innerHTML = `
${indexHeader}

Get Paid to Your Account

`; } else { console.error(`Element with ID ${proSetPaymentMethodStripeElementId} not found.`); } const proSetPaymentMethodStripeOrElement = document.getElementById(proSetPaymentMethodStripeOrElementId); if (proSetPaymentMethodStripeOrElement) { proSetPaymentMethodStripeOrElement.className = "text-center"; proSetPaymentMethodStripeOrElement.innerHTML = `OR`; } else { console.error(`Element with ID ${proSetPaymentMethodStripeOrElementId} not found.`); } const proSetPaymentMethodCharityElement = document.getElementById(proSetPaymentMethodCharityElementId); if (proSetPaymentMethodCharityElement) { let indexHeader = ""; if (index > 0) { indexHeader = `

${index}B

`; } proSetPaymentMethodCharityElement.className = "setup-account-container flex flex-column halfgap"; proSetPaymentMethodCharityElement.innerHTML = `
${indexHeader}

Donate to Stand Up To Cancer

All your session payouts will go to Stand Up To Cancer. You’re turning conversations into real contribution—with no extra steps.

You can update this anytime under Account → Payout methods.

Thank you for using your time to do something extraordinary.

`; } else { console.error(`Element with ID ${proSetPaymentMethodCharityElementId} not found.`); } } function renderProLinkElement(index, paymentAccountEligibleForPayout) { const proLinkElement = document.getElementById(proLinkElementId); if (proLinkElement) { proLinkElement.className = "setup-account-container flex flex-column halfgap"; if (index > 0) { proLinkElement.innerHTML = `

${index}

Your link becomes active!

Once payment method is set, you’ll see your personal Nectly link activated here—ready to share.

`; } else { if (paymentAccountEligibleForPayout) { proLinkElement.className = "setup-account-container flex flex-column halfgap"; proLinkElement.innerHTML = `

Add it to LinkedIn. Share it in DMs. Link it on your site. Wherever you connect, your Nectly link helps you say yes — on your terms, with zero friction.

`; setupNectlyLinkButtonAnimation(); } else { proLinkElement.innerHTML = `

Your payment method is set but not eligible for payouts. Please login to your Stripe account and finish the onboarding process.

`; } } } else { console.error(`Element with ID ${proLinkElementId} not found.`); } } function renderProShareLinkElement(index) { const proShareLinkElement = document.getElementById(proShareLinkElementId); if (proShareLinkElement) { let indexHeader = ""; if (index > 0) { indexHeader = `

${index}

`; } proShareLinkElement.className = "setup-account-container flex flex-column halfgap"; proShareLinkElement.innerHTML = `
${indexHeader}

Share Your Link Everywhere You Connect

`; } else { console.error(`Element with ID ${proShareLinkElementId} not found.`); } } function getVisibleProCalendar() { if (window.innerWidth >= 1024) { return proCalendar || mobileProCalendar; } else { return mobileProCalendar || proCalendar; } } function proSlotsFailed() { const errorMessage = `

Retrieving slots failed.

Try to refresh later.

`; const postReserveSlotElement = document.getElementById(postReserveSlotProElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = errorMessage; } else { console.error(`Element with ID ${postReserveSlotProElementId} not found.`); } } function getVisibleSeekerCalendar() { if (window.innerWidth >= 1024) { return seekerCalendar || mobileSeekerCalendar; } else { return mobileSeekerCalendar || seekerCalendar; } } let resolveProDateElements = []; let proLastDayInMonth = null; let activeProDayElement = null; function loopProDaysInMonth(self, dateEl) { const tzOffset = (new Date()).getTimezoneOffset(); const leftArrow = self.context.mainElement.querySelectorAll('[data-vc-arrow="prev"]'); const rightArrow = self.context.mainElement.querySelectorAll('[data-vc-arrow="prev"]'); if (leftArrow.length > 0) leftArrow[0].disabled = true; if (rightArrow.length > 0) rightArrow[0].disabled = true; const btnEl = dateEl.querySelector('[data-vc-date-btn]'); const btnDate = new Date(dateEl.getAttribute('data-vc-date')); btnDate.setTime(btnDate.getTime() + tzOffset * 60000); if (btnDate.getMonth() !== self.context.selectedMonth) { return; } if (activeProDayElement === null) { const today = new Date(); if (today.getDate() === btnDate.getDate()) { activeProDayElement = btnEl; } } const day = btnEl.innerText; if (parseInt(day) === 1) { resolveProDateElements = [dateEl]; proLastDayInMonth = getLastDayOfMonth(btnDate.getFullYear(), convertIntMonthToString(btnDate.getMonth())); } else { resolveProDateElements.push(dateEl); } if (proLastDayInMonth.getDate() === btnDate.getDate()) { getProCurrentDate(proLastDayInMonth.toISOString(), proLastDayInMonth.getTimezoneOffset(), function(currentDay) { const busyDaysInMonth = mergeAppointmentsAndSlots(currentDay); for (const dateEl of resolveProDateElements) { const btnEl = dateEl.querySelector('[data-vc-date-btn]'); const btnDate = new Date(dateEl.getAttribute('data-vc-date')); const today = new Date(); today.setHours(0, 0, 0, 0); btnDate.setHours(0, 0, 0, 0); const day = btnEl.innerText; if (btnDate < today) continue; if (busyDaysInMonth.indexOf(parseInt(day)) !== -1) { setDayElementAsBusy(btnEl); } } resolveProDateElements = []; if (leftArrow.length > 0) leftArrow[0].disabled = false; if (rightArrow.length > 0) rightArrow[0].disabled = false; console.log("Days in month updated") }, function(error) { console.log("Error fetching slots:", error); resolveProDateElements = []; if (leftArrow.length > 0) leftArrow[0].disabled = false; if (rightArrow.length > 0) rightArrow[0].disabled = false; }); } } function renderProCalendar(onChange, elementId) { const { Calendar } = window.VanillaCalendarPro; const calendar = new Calendar( `#${elementId}`, { onCreateDateEls: loopProDaysInMonth, disableAllDates: true, enableDates:[getEnabledDates()], firstWeekday: 0, type: 'default', onClickArrow(self) { console.log("Month changed to", self.context.selectedYear, self.context.selectedMonth); onChange('month', convertIntMonthToString(self.context.selectedMonth)); }, onClickDate(self, pointerEvent) { const btnEl = pointerEvent.srcElement; console.log("Day clicked", self.context.selectedDates, btnEl); activeProDayElement = btnEl; onChange('value', self.context.selectedDates[0]); }, }); calendar.init(); return calendar; } function updateProCalendarDays(busyDaysInMonth) { console.log("Updating calendar days with busy days:", busyDaysInMonth); if (activeProDayElement) { const day = activeProDayElement.innerText; if (busyDaysInMonth.indexOf(parseInt(day)) !== -1) { setDayElementAsBusy(activeProDayElement); } } } function onProCalendarChange(calendarParameter, value) { if (calendarParameter === "value") { console.log("Calendar value changed to", value); resetProAppointmentListContainer(); proRefreshCalendarSlots(); } if (calendarParameter === "month") { console.log("Calendar month changed to", value); } } function onMobileProCalendarChange(calendarParameter, value) { if (calendarParameter === "value") { console.log("Professional Mobile calendar value changed to", value); resetProAppointmentListContainer(); proRefreshCalendarSlots(); } if (calendarParameter === "month") { console.log("Professional Mobile calendar month changed to", value); } } function renderProSetupAvailabilityElement(index) { const proAddYourAvailabilityElement = document.getElementById(proAddYourAvailabilityElementId); if (proAddYourAvailabilityElement) { proAddYourAvailabilityElement.className = "setup-account-container flex flex-column halfgap"; proAddYourAvailabilityElement.innerHTML = `

${index}

Add your availability

`; } else { console.error(`Element with ID ${proAddYourAvailabilityElementId} not found.`); } } function renderProSetup(currentDay) { resetProSetup(); let countOfSetupSteps = 0; if (currentDay.allCreatedSlots.length === 0) countOfSetupSteps++; if (!currentDay.paymentAccount && !currentDay.charityPayout) countOfSetupSteps++; if (countOfSetupSteps === 0) { renderProLinkElement(0, currentDay.paymentAccountEligibleForPayout); } else { const proSetupYourAccountMessageElement = document.getElementById(proSetupYourAccountMessageElementId); if (proSetupYourAccountMessageElement) { proSetupYourAccountMessageElement.innerHTML = `Setup your Account`; } else { console.error(`Element with ID ${proSetupYourAccountMessageElementId} not found.`); } let index = 1; if (!currentDay.paymentAccount && !currentDay.charityPayout) renderProPaymentMethodElement(index++); if (currentDay.allCreatedSlots.length === 0) renderProSetupAvailabilityElement(index++); renderProLinkElement(index++, currentDay.paymentAccountEligibleForPayout); renderProShareLinkElement(index++); } } function proRenderEarnings(payouts) { const proEarningsElement = document.getElementById(proEarningsElementId); if (proEarningsElement) { const inReview = `

In Review

$${payouts.inReview}

This session wrapped recently—your payout will be released within the hour.

`; const availableToWithdraw = `

Available to Withdraw

$${payouts.availableToWithdraw}

These funds are all yours.

`; const totalEarnings = `

Total Earned with Nectly

$${payouts.earned}

You’ve turned time into impact.

Funder
Payment Card Icon

$60

paid out to Funds
`; let earnings = ""; earnings += totalEarnings; earnings += inReview; /*if (parseFloat(payouts.inReview).toFixed(2) > 0) { earnings += inReview; }*/ if (parseFloat(payouts.availableToWithdraw).toFixed(2) > 0) { earnings += availableToWithdraw; } /*if (parseFloat(payouts.earned).toFixed(2) > 0) { earnings += totalEarnings; }*/ if (earnings.length > 0) { proEarningsElement.innerHTML = `

Your Nectly Earnings

${earnings} `; } } else { console.error(`Element with ID ${proEarningsElementId} not found.`); } } function renderProAvailableSessions(currentDay) { const timeSelect = document.getElementById(proNewSessionTimeSelect); timeSelect.innerHTML = ""; currentDay.freeSlots.forEach( slot => { timeSelect.appendChild( new Option(renderSlotFromTo(slot), `${slot.startTime}@${slot.endTime}`) ); } ); } function proRefreshCalendarSlots(calledFromPage) { function resetNewSessionElement() { document.getElementById(proNewSessionDateSelect).innerHTML = ``; document.getElementById(proNewSessionTimeSelect).innerHTML = ``; } function renderNewSessionDate() { const selectedDate = getCalendarSelectedDate(getVisibleProCalendar()); const stringDate = selectedDate.toLocaleString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); document.getElementById(proNewSessionDateSelect).innerHTML = ``; } function renderToday() { const selectedDate = getCalendarSelectedDate(getVisibleProCalendar()); const stringToday = selectedDate.toLocaleString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); const appointmentsTodayElemnet = document.getElementById(proTodayAppointmentsElementId); if (appointmentsTodayElemnet) { appointmentsTodayElemnet.innerHTML = stringToday; } else { console.error(`Element with ID ${proTodayAppointmentsElementId} not found.`); } const slotsTodayElemnet = document.getElementById(proTodaySlotsElementId); if (slotsTodayElemnet) { slotsTodayElemnet.innerHTML = stringToday; } else { console.error(`Element with ID ${proTodaySlotsElementId} not found.`); } } renderToday(); proPostCreateSlot(); resetNewSessionElement(); const selectedDate = getCalendarSelectedDate(getVisibleProCalendar()); getProCurrentDate(selectedDate.toISOString(), selectedDate.getTimezoneOffset(), function(currentDay) { console.log("Current day status:", currentDay); proRenderEarnings(currentDay.payouts); renderProSetup(currentDay); renderProAppointmentList(currentDay); renderProSlotsList(currentDay); renderNewSessionDate(); renderProAvailableSessions(currentDay); updateProCalendarDays(mergeAppointmentsAndSlots(currentDay)); if (currentDay.freeSlots.length > 0) { toggleSeekerConfirmationButton(false); } else { toggleSeekerConfirmationButton(true); } if (calledFromPage) { getVisibleProCalendar().update(); } }, function(error) { console.error("Error fetching slots:", error); proSlotsFailed(); toggleSeekerConfirmationButton(true); }); } function proAppointmentsTabClick(clickedElement) { const activeAppointmentList = getActiveProAppointmentList(); if (activeAppointmentList) { activeAppointmentList.classList.remove('active'); } clickedElement.classList.add('active'); proRefreshCalendarSlots(); } function proSlotsTabClick(clickedElement) { const activeSlotList = getActiveProSlotList(); if (activeSlotList) { activeSlotList.classList.remove('active'); } clickedElement.classList.add('active'); proRefreshCalendarSlots(); } function proCancelMeeting(appointmentId) { console.log("Cancelling meeting:", appointmentId); return callApi(`/api/appointment/${appointmentId}`, 'DELETE') .then(data => { console.log('Appointment cancelled:', data) proRefreshCalendarSlots(); }) .catch((error) => { console.log('Error cancelling appointment:', error); proRefreshCalendarSlots(); }); } function proSelectSlot(startTime, endTime) { console.log("Deleting slot from", startTime, "to", endTime) return callApi(`/api/professional-slots?startTime=${encodeURIComponent(startTime)}&endTime=${encodeURIComponent(endTime)}`, 'DELETE') .then(data => { console.log(`Slot deleted ${startTime}-${endTime}:`, data); proRefreshCalendarSlots(); }) .catch((error) => { console.log(`Slot delete failed ${startTime}-${endTime}:`, error); proRefreshCalendarSlots(); }); } function proSelectSlots(params) { let promises = []; let idx = 0; function deleteOneSlot(slot) { return callApi(`/api/professional-slots?startTime=${encodeURIComponent(slot.startTime)}&endTime=${encodeURIComponent(slot.endTime)}`, 'DELETE') .then(data => { console.log(`Slot deleted`, data); idx += 1; if (params.length > idx) { promises.push(deleteOneSlot(params[idx])); } else { Promise.all(promises).then((values) => { console.log('All slots deleted:', values); proRefreshCalendarSlots(); }); } }) .catch((error) => { console.log(`Slot delete failed`, error); idx += 1; if (params.length > idx) { promises.push(deleteOneSlot(params[idx])); } else { Promise.all(promises).then((values) => { console.log('All slots deleted:', values); proRefreshCalendarSlots(); }); } }); } console.log("Deleting slots:", params); if (params.length > idx) { promises.push(deleteOneSlot(params[0])); } } function renderProSlotsList(currentDay) { function agregateSlotsByDate(slots) { const slotsByDate = new Map(); slots.forEach(slot => { const slotDate = new Date(slot.startTime); const dateKey = `${slotDate.getFullYear()}-${String(slotDate.getMonth() + 1).padStart(2, '0')}-${String(slotDate.getDate()).padStart(2, '0')}`; if (!slotsByDate.has(dateKey)) { slotsByDate.set(dateKey, []); } slotsByDate.get(dateKey).push(slot); }); return slotsByDate; } function displayMaxThreeTimeSlots(slots) { let ret = []; const loopEnd = Math.min(...[3, slots.length]); for (let i = 0; i < loopEnd; i++) { const fromTime = (new Date(slots[i].startTime)).toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); const toTime = (new Date(slots[i].endTime)).toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); ret.push(`${fromTime} – ${toTime}`); } return ret.join('; ') } function renderSlotsDetail(slots) { let ret = ""; slots.forEach(slot => { const fromTime = (new Date(slot.startTime)).toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); const toTime = (new Date(slot.endTime)).toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); ret += `
  • ${fromTime} – ${toTime} Delete Icon
  • `; }); return ret; } function composeOnclickParameters(slots) { return JSON.stringify(slots); } const slotsListElement = document.getElementById(proSlotsListElementId); if (!slotsListElement) { console.error(`Element with ID ${proSlotsListElementId} not found.`); } const slotsTabElement = getActiveProSlotList(); if (!slotsTabElement) { console.error('No active slots tab found.'); return; } let workWithSlots; if (slotsTabElement.id === proSlotsDateTabElementId) { workWithSlots = currentDay.createdSlotsToDate; } else if (slotsTabElement.id === proSlotsAllTabElementId) { workWithSlots = currentDay.allCreatedSlots; } else { workWithSlots = currentDay.createdSlotsToday; } if (workWithSlots.length === 0) { slotsListElement.innerHTML = ""; proRenderNoSlots(); return; } const slotsByDate = agregateSlotsByDate(workWithSlots); let innerHTML = ""; slotsByDate.forEach((slots, date) => { innerHTML += '
    '; innerHTML += `

    ${date.toLocaleString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}

    ${displayMaxThreeTimeSlots(slots)}...

    `; innerHTML += `
      ${renderSlotsDetail(slots)}
    `; innerHTML += '
    '; }); slotsListElement.innerHTML = innerHTML; } function onProTimeSlotSelectChange() { const selectedDate = getCalendarSelectedDate(getVisibleProCalendar()); proPostCreateSlot(); getProCurrentDate(selectedDate.toISOString(), selectedDate.getTimezoneOffset(), function(currentDay) { console.log("Current day status:", currentDay); renderProSetup(currentDay); updateProCalendarDays(mergeAppointmentsAndSlots(currentDay)); getVisibleProCalendar().update(); renderProAppointmentList(currentDay); renderSeekerSlotsList(currentDay); if (currentDay.freeSlots.length > 0) { const timeSelect = document.getElementById(proNewSessionTimeSelect); if (!timeSelect) { console.error(`Element with ID ${proNewSessionTimeSelect} not found.`); return; } if (currentDay.freeSlots.length !== timeSelect.options.length) { updateProCalendarDays(mergeAppointmentsAndSlots(currentDay)); } toggleSeekerConfirmationButton(false); } else { updateProCalendarDays(mergeAppointmentsAndSlots(currentDay)); toggleSeekerConfirmationButton(true); } }, function(error) { console.error("Error fetching professional's slots:", error); if (error.status === 400 && error.error === "400: Selected date cannot be in the past") { proSelectedDateIsinThePast(); } else { proSlotsFailed(); } toggleSeekerConfirmationButton(true); }); } function proSaveSession(slotType, repeatCount) { const subject = ''; const timeSelect = document.getElementById(proNewSessionTimeSelect); if (!timeSelect) { console.error(`Element with ID ${proNewSessionTimeSelect} not found.`); return; } const selectedOption = timeSelect.options[timeSelect.selectedIndex]; if (!selectedOption) { console.error("No slot selected."); return; } toggleSeekerConfirmationButton(true); const startTime = selectedOption.value.split('@')[0]; const endTime = selectedOption.value.split('@')[1]; console.log("Reserving session from", startTime, "to", endTime); const reserveSlotBody = { "startTime": startTime, "endTime": endTime, "title": subject, "slotType": slotType || getProRecurringValue(), "repeatCount": repeatCount || -1, // TODO: implement in UI } const pro_id = getLinkPageUserId(); console.log("Professional ID:", pro_id); console.log("Reserve slot body:", reserveSlotBody); callApi(`/api/professional-slots`, 'POST', reserveSlotBody) .then(data => { console.log('Current month:', data) onProTimeSlotSelectChange(); onReserveProSlotSuccess(); }) .catch((error) => { console.error('Error saving professional session:', error); if (error.status === 400) { if (error.error === "400: Selected date cannot be in the past") { proSelectedDateIsinThePast(); } else if (error.error === "400: Slot is too soon for booking") { proSlotIsTooSoon(); } else { onReserveProSlotError(); } } else { onReserveProSlotError(); } }); } function getProRecurringValue() { const recurringSelect = document.getElementById(proRecurringSelectionElementId); if (!recurringSelect) { console.error(`Element with ID ${proRecurringSelectionElementId} not found.`); return null; } let selectedOption = null; for (const child of recurringSelect.children) { if (child.children[0].checked === true) { selectedOption = child.children[0]; break; } } if (!selectedOption) { console.error("No recurring selected."); return null; } return selectedOption.value; } function renderProAppointmentList(currentDay) { const appointmentListElement = document.getElementById(proAppointmentListElementId); if (!appointmentListElement) { console.error(`Element with ID ${proAppointmentListElementId} not found.`); } const appointmentsTabElement = getActiveProAppointmentList(); if (!appointmentsTabElement) { console.error('No active appointments tab found.'); return; } let workWithAppointments; if (appointmentsTabElement.id === proAppointmentsDateTabElementId) { workWithAppointments = currentDay.appointmentsToDate; } else if (appointmentsTabElement.id === proAppointmentsAllTabElementId) { workWithAppointments = currentDay.appointments; } else { workWithAppointments = currentDay.appointmentsToday; } if (workWithAppointments.length === 0) { appointmentListElement.innerHTML = ""; proRenderNoAppointments(); return; } let innerHTML = ""; workWithAppointments.forEach( (appointment) => { let subject = ""; if (appointment.title) { subject = `

    ${appointment.title}

    `; } innerHTML += `

    Conversation with ${appointment.attendees.join(', ')}

    ${subject}

    ${renderAppointmentDate(appointment)}

    Cancel this conversation?

    This will cancel your confirmed session with XY(gmail@gmail.com).

    They’ll be notified right away.

    `; }); appointmentListElement.innerHTML = innerHTML; setupCancelMeetingToggle(); } function resetProAppointmentListContainer() { const appointmentListElement = document.getElementById(proAppointmentListElementId); if (appointmentListElement) { appointmentListElement.innerHTML = ""; } else { console.error(`Element with ID ${proAppointmentListElementId} not found.`); } } function getActiveProSlotList() { const slotsElement = document.getElementById(proSlotsElementId); if (slotsElement) { for (const child of slotsElement.children) { if (child.classList.contains('active')) { return child; } } } else { console.error(`Element with ID ${proSlotsElementId} not found.`); return null; } } function getActiveProAppointmentList() { const appointmentsElement = document.getElementById(proAppointmentsElementId); if (appointmentsElement) { for (const child of appointmentsElement.children) { if (child.classList.contains('active')) { return child; } } } else { console.error(`Element with ID ${proAppointmentsElementId} not found.`); return null; } } function proPostCreateSlot() { const postReserveSlotElement = document.getElementById(postReserveSlotProElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = ""; } else { console.error(`Element with ID ${postReserveSlotProElementId} not found.`); } const noAppointmentsElement = document.getElementById(proNoAppointmentsElementId); if (noAppointmentsElement) { noAppointmentsElement.innerHTML = ""; } else { console.error(`Element with ID ${proNoAppointmentsElementId} not found.`) } const noSlotsElement = document.getElementById(proNoSlotsElementId); if (noSlotsElement) { noSlotsElement.innerHTML = ""; } else { console.error(`Element with ID ${proNoSlotsElementId} not found.`) } } function proRenderNoSlots() { const noSlotsElement = document.getElementById(proNoSlotsElementId); if (noSlotsElement) { noSlotsElement.innerHTML = ""; noSlotsElement.innerHTML = ` Calendar Icon

    No time slots added yet

    Choose the times you’re available, and they’ll appear here ready to book.

    `; } else { console.error(`Element with ID ${proNoSlotsElementId} not found.`); } } function proRenderNoAppointments() { const noAppointmentsElement = document.getElementById(proNoAppointmentsElementId); if (noAppointmentsElement) { noAppointmentsElement.innerHTML = ""; noAppointmentsElement.innerHTML = ` No Booked Time Slots Icon

    No conversations booked yet

    Once someone books time with you, it’ll appear here. Until then, share your link to start the right conversations — on your terms.

    `; } else { console.error(`Element with ID ${proNoAppointmentsElementId} not found.`); } } function onReserveProSlotError() { const errorMessage = `

    That time’s no longer available.

    Don’t worry, just choose another time for the session.

    `; const postReserveSlotElement = document.getElementById(postReserveSlotProElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = errorMessage; } else { console.error(`Element with ID ${postReserveSlotProElementId} not found.`); } } function onReserveProSlotSuccess() { const successMessage = `
    Email Sent Icon

    You’re all set.

    Your session slot is reserved.

    `; const postReserveSlotElement = document.getElementById(postReserveSlotProElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = successMessage; } else { console.error(`Element with ID ${postReserveSlotProElementId} not found.`); } } function proSelectedDateIsinThePast() { const errorMessage = `

    Selected date cannot be in the past.

    Don’t worry, just choose another slot to create the session.

    `; const postReserveSlotElement = document.getElementById(postReserveSlotProElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = errorMessage; } else { console.error(`Element with ID ${postReserveSlotProElementId} not found.`); } } function proSlotIsTooSoon() { const errorMessage = `

    Selected slot is to soon in the future.

    Don’t worry, just choose another slot to create the session.

    `; const postReserveSlotElement = document.getElementById(postReserveSlotProElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = errorMessage; } else { console.error(`Element with ID ${postReserveSlotProElementId} not found.`); } } function refreshSessionBalanceSummary(countOfPaidSessions, countOfUsedSession) { const summaryElement = document.getElementById(sessionBalanceSummaryElementId); if (summaryElement) { summaryElement.innerHTML = `Remaining: ${countOfPaidSessions - countOfUsedSession} of ${countOfPaidSessions} sessions`; } else { console.error(`Element with ID ${sessionBalanceSummaryElementId} not found.`); } const progressBar = document.getElementById(seekerBalanceProgressBarElementId); if (progressBar) { progressBar.style.width = `${((countOfPaidSessions - countOfUsedSession) / countOfPaidSessions) * 100}%`; } else { console.error(`Element with ID ${seekerBalanceProgressBarElementId} not found.`); } } function seekerSlotsFailed() { const errorMessage = `

    Retrieving slots failed.

    Try to refresh later.

    `; const postReserveSlotElement = document.getElementById(postReserveSlotSeekerElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = errorMessage; } else { console.error(`Element with ID ${postReserveSlotSeekerElementId} not found.`); } } function seekerNoMoreAvailableSlots() { const errorMessage = `
    No Available Slots Icon

    All pre-paid sessions are booked already.

    Add more sessions to continue.

    `; const postReserveSlotElement = document.getElementById(postReserveSlotSeekerElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = errorMessage; } else { console.error(`Element with ID ${postReserveSlotSeekerElementId} not found.`); } } function getCalendarSelectedDate(cal) { if (cal.context.selectedDates.length > 0) { return new Date(cal.context.selectedDates[0]); } return new Date(); } function getEnabledDates() { const today = new Date(); today.setHours(0, 0, 0, 0); const yyyy = today.getFullYear(); const mm = String(today.getMonth() + 1).padStart(2, '0'); const dd = String(today.getDate()).padStart(2, '0'); return `${yyyy}-${mm}-${dd}:2099-12-31` } let resolveSeekerDateElements = []; let seekerLastDayInMonth = null; let activeSeekerDayElement = null; function setDayElementAsBusy(btnEl) { btnEl.style.backgroundColor = "#EBFFD6"; } function loopSeekerDaysInMonth(self, dateEl) { const tzOffset = (new Date()).getTimezoneOffset(); const leftArrow = self.context.mainElement.querySelectorAll('[data-vc-arrow="prev"]'); const rightArrow = self.context.mainElement.querySelectorAll('[data-vc-arrow="prev"]'); if (leftArrow.length > 0) leftArrow[0].disabled = true; if (rightArrow.length > 0) rightArrow[0].disabled = true; const btnEl = dateEl.querySelector('[data-vc-date-btn]'); const btnDate = new Date(dateEl.getAttribute('data-vc-date')); btnDate.setTime(btnDate.getTime() + tzOffset * 60000); if (btnDate.getMonth() !== self.context.selectedMonth) { return; } if (activeSeekerDayElement === null) { const today = new Date(); if (today.getDate() === btnDate.getDate()) { activeSeekerDayElement = btnEl; } } const day = btnEl.innerText; if (parseInt(day) === 1) { resolveSeekerDateElements = [dateEl]; seekerLastDayInMonth = getLastDayOfMonth(btnDate.getFullYear(), convertIntMonthToString(btnDate.getMonth())); } else { resolveSeekerDateElements.push(dateEl); } if (seekerLastDayInMonth.getDate() === btnDate.getDate()) { getSeekerCurrentDate(seekerLastDayInMonth.toISOString(), seekerLastDayInMonth.getTimezoneOffset(), function(currentDay) { const busyDaysInMonth = mergeAppointmentsAndSlots(currentDay); for (const dateEl of resolveSeekerDateElements) { const btnEl = dateEl.querySelector('[data-vc-date-btn]'); const btnDate = new Date(dateEl.getAttribute('data-vc-date')); const today = new Date(); today.setHours(0, 0, 0, 0); btnDate.setHours(0, 0, 0, 0); const day = btnEl.innerText; if (btnDate < today) continue; if (busyDaysInMonth.indexOf(parseInt(day)) !== -1) { setDayElementAsBusy(btnEl); } } resolveSeekerDateElements = []; if (leftArrow.length > 0) leftArrow[0].disabled = false; if (rightArrow.length > 0) rightArrow[0].disabled = false; console.log("Days in month updated") }, function(error) { console.log("Error fetching slots:", error); resolveSeekerDateElements = []; if (leftArrow.length > 0) leftArrow[0].disabled = false; if (rightArrow.length > 0) rightArrow[0].disabled = false; }); } } function renderSeekerChooseHowYouPayElement(index) { const seekerSetPaymentMethodElement = document.getElementById(seekerSetPaymentMethodElementId); if (seekerSetPaymentMethodElement) { seekerSetPaymentMethodElement.className = "setup-account-container flex flex-column halfgap"; seekerSetPaymentMethodElement.innerHTML = `

    ${index}

    Choose how you’ll pay

    `; } else { console.error(`Element with ID ${seekerSetPaymentMethodElementId} not found.`); } } function renderSeekerSetupAvailabilityElement(index) { const seekerAddYourAvailabilityElement = document.getElementById(seekerAddYourAvailabilityElementId); if (seekerAddYourAvailabilityElement) { seekerAddYourAvailabilityElement.className = "setup-account-container flex flex-column halfgap"; seekerAddYourAvailabilityElement.innerHTML = `

    ${index}

    Add your availability

    `; } else { console.error(`Element with ID ${seekerAddYourAvailabilityElementId} not found.`); } } function renderSeekerLinkElement(index) { const seekerShareLinkElement = document.getElementById(seekerShareLinkElementId); if (seekerShareLinkElement) { if (index > 0) { seekerShareLinkElement.className = "setup-account-container flex flex-column halfgap"; seekerShareLinkElement.innerHTML = `

    ${index}

    Your link becomes active!

    Once payment and availability are set, you’ll see your personal Nectly link activated here—ready to share.

    `; } else { seekerShareLinkElement.className = "setup-account-container flex flex-column halfgap"; seekerShareLinkElement.innerHTML = `

    Share it in outreach messages, LinkedIn, or email. It shows your calendar and handles payment automatically.

    `; setupNectlyLinkButtonAnimation(); } } } function renderSeekerSetup(currentDay) { resetSeekerSetup(); let countOfSetupSteps = 0; if (currentDay.allCreatedSlots.length === 0) countOfSetupSteps++; if (!currentDay.paymentMethodSet) countOfSetupSteps++; if (countOfSetupSteps === 0) { renderSeekerLinkElement(0); } else { const seekerSetupYourAccountMessageElement = document.getElementById(seekerSetupYourAccountMessageElementId); if (seekerSetupYourAccountMessageElement) { seekerSetupYourAccountMessageElement.innerHTML = `Setup your Account`; } else { console.error(`Element with ID ${seekerSetupYourAccountMessageElementId} not found.`); } let index = 1; if (!currentDay.paymentMethodSet) renderSeekerChooseHowYouPayElement(index++); if (currentDay.allCreatedSlots.length === 0) renderSeekerSetupAvailabilityElement(index++); renderSeekerLinkElement(index++); } } function renderSeekerCalendar(onChange, elementId) { const { Calendar } = window.VanillaCalendarPro; const calendar = new Calendar( `#${elementId}`, { onCreateDateEls: loopSeekerDaysInMonth, disableAllDates: true, enableDates:[getEnabledDates()], firstWeekday: 0, type: 'default', onClickArrow(self) { console.log("Month changed to", self.context.selectedYear, self.context.selectedMonth); onChange('month', convertIntMonthToString(self.context.selectedMonth)); }, onClickDate(self, pointerEvent) { const btnEl = pointerEvent.srcElement; console.log("Day clicked", self.context.selectedDates, btnEl); activeSeekerDayElement = btnEl; onChange('value', self.context.selectedDates[0]); }, }); calendar.init(); return calendar; } function mergeAppointmentsAndSlots(currentDay) { const appointments = new Set(currentDay.appointmentsInMonth); const slots = new Set(currentDay.slotsInMonth); const busyDaysInMonth = new Set([...appointments, ...slots]); return Array.from(busyDaysInMonth); } function updateSeekerCalendarDays(busyDaysInMonth) { console.log("Updating calendar days with busy days:", busyDaysInMonth); if (activeSeekerDayElement) { const day = activeSeekerDayElement.innerText; if (busyDaysInMonth.indexOf(parseInt(day)) !== -1) { setDayElementAsBusy(activeSeekerDayElement); } } } function onSeekerCalendarChange(calendarParameter, value) { if (calendarParameter === "value") { console.log("Calendar value changed to", value); resetAppointmentListContainer(); seekerRefreshCalendarSlots(); } if (calendarParameter === "month") { console.log("Calendar month changed to", value); } } function onMobileSeekerCalendarChange(calendarParameter, value) { if (calendarParameter === "value") { console.log("Seeker Mobile calendar value changed to", value); resetAppointmentListContainer(); seekerRefreshCalendarSlots(); } if (calendarParameter === "month") { console.log("Seeker Mobile calendar month changed to", value); } } function renderSlotFromTo(slot) { const startTime = new Date(slot.startTime); const endTime = new Date(slot.endTime); const fromDate = startTime.toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); const toDate = endTime.toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); return `${fromDate} - ${toDate}`; } function renderAvailableSessions(currentDay) { const timeSelect = document.getElementById(seekerNewSessionTimeSelect); timeSelect.innerHTML = ""; currentDay.freeSlots.forEach( slot => { timeSelect.appendChild( new Option(renderSlotFromTo(slot), `${slot.startTime}@${slot.endTime}`) ); } ); } function getActiveSeekerAppointmentList() { const appointmentsElement = document.getElementById(seekerAppointmentsElementId); if (appointmentsElement) { for (const child of appointmentsElement.children) { if (child.classList.contains('active')) { return child; } } } else { console.error(`Element with ID ${seekerAppointmentsElementId} not found.`); return null; } } function getActiveSeekerSlotList() { const slotsElement = document.getElementById(seekerSlotsElementId); if (slotsElement) { for (const child of slotsElement.children) { if (child.classList.contains('active')) { return child; } } } else { console.error(`Element with ID ${seekerSlotsElementId} not found.`); return null; } } function seekerAppointmentsTabClick(clickedElement) { const activeAppointmentList = getActiveSeekerAppointmentList(); if (activeAppointmentList) { activeAppointmentList.classList.remove('active'); } clickedElement.classList.add('active'); seekerRefreshCalendarSlots(); } function seekerSlotsTabClick(clickedElement) { const activeSlotList = getActiveSeekerSlotList(); if (activeSlotList) { activeSlotList.classList.remove('active'); } clickedElement.classList.add('active'); seekerRefreshCalendarSlots(); } function seekerRefreshCalendarSlots(calledFromPage) { function resetNewSessionElement() { document.getElementById(seekerNewSessionDateSelect).innerHTML = ``; document.getElementById(seekerNewSessionTimeSelect).innerHTML = ``; } function renderNewSessionDate() { const selectedDate = getCalendarSelectedDate(getVisibleSeekerCalendar()); const stringDate = selectedDate.toLocaleString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); document.getElementById(seekerNewSessionDateSelect).innerHTML = ``; } function renderToday() { const selectedDate = getCalendarSelectedDate(getVisibleSeekerCalendar()); const stringToday = selectedDate.toLocaleString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); const appointmentsTodayElemnet = document.getElementById(seekerTodayAppointmentsElementId); if (appointmentsTodayElemnet) { appointmentsTodayElemnet.innerHTML = stringToday; } else { console.error(`Element with ID ${seekerTodayAppointmentsElementId} not found.`); } const slotsTodayElemnet = document.getElementById(seekerTodaySlotsElementId); if (slotsTodayElemnet) { slotsTodayElemnet.innerHTML = stringToday; } else { console.error(`Element with ID ${seekerTodaySlotsElementId} not found.`); } } renderToday(); resetSeekerPostCreateSlot(); resetNewSessionElement(); const selectedDate = getCalendarSelectedDate(getVisibleSeekerCalendar()); getSeekerCurrentDate(selectedDate.toISOString(), selectedDate.getTimezoneOffset(), function(currentDay) { console.log("Current day status:", currentDay); renderSeekerSetup(currentDay); renderSeekerAppointmentList(currentDay); renderSeekerSlotsList(currentDay); renderNewSessionDate(); if (!currentDay.paymentMethodSet && currentDay.countOfPaidSessions <= currentDay.countOfUsedSession) { seekerNoMoreAvailableSlots(); toggleSeekerConfirmationButton(true); return; } updateSeekerCalendarDays(mergeAppointmentsAndSlots(currentDay)); renderAvailableSessions(currentDay); if (currentDay.freeSlots.length > 0) { toggleSeekerConfirmationButton(false); } else { toggleSeekerConfirmationButton(true); } refreshSessionBalanceSummary(currentDay.countOfPaidSessions, currentDay.countOfUsedSession); if (calledFromPage) { getVisibleSeekerCalendar().update(); } }, function(error) { console.error("Error fetching slots:", error); seekerSlotsFailed(); toggleSeekerConfirmationButton(true); }); } function toggleSeekerConfirmationButton(disabled) { document.getElementById(confirmMeetingButtonElementId).disabled = disabled; } function resetSeekerPostCreateSlot() { const postReserveSlotElement = document.getElementById(postReserveSlotSeekerElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = ""; } else { console.error(`Element with ID ${postReserveSlotSeekerElementId} not found.`); } const noAppointmentsElement = document.getElementById(seekerNoAppointmentsElementId); if (noAppointmentsElement) { noAppointmentsElement.innerHTML = ""; } else { console.error(`Element with ID ${seekerNoAppointmentsElementId} not found.`) } const noSlotsElement = document.getElementById(seekerNoSlotsElementId); if (noSlotsElement) { noSlotsElement.innerHTML = ""; } else { console.error(`Element with ID ${seekerNoSlotsElementId} not found.`) } } function getRecurringValue() { const recurringSelect = document.getElementById(seekerRecurringSelectionElementId); if (!recurringSelect) { console.error(`Element with ID ${seekerRecurringSelectionElementId} not found.`); return null; } let selectedOption = null; for (const child of recurringSelect.children) { if (child.children[0].checked === true) { selectedOption = child.children[0]; break; } } if (!selectedOption) { console.error("No recurring selected."); return null; } return selectedOption.value; } function seekerSaveSession(slotType, repeatCount) { const subject = ''; const timeSelect = document.getElementById(seekerNewSessionTimeSelect); if (!timeSelect) { console.error(`Element with ID ${seekerNewSessionTimeSelect} not found.`); return; } const selectedOption = timeSelect.options[timeSelect.selectedIndex]; if (!selectedOption) { console.error("No slot selected."); return; } toggleSeekerConfirmationButton(true); const startTime = selectedOption.value.split('@')[0]; const endTime = selectedOption.value.split('@')[1]; console.log("Reserving session from", startTime, "to", endTime); const reserveSlotBody = { "startTime": startTime, "endTime": endTime, "title": subject, "slotType": slotType || getRecurringValue(), "repeatCount": repeatCount || -1, // TODO: implement in UI } const seeker_id = getLinkPageUserId(); console.log("Seeker ID:", seeker_id); console.log("Reserve slot body:", reserveSlotBody); callApi(`/api/seeker-slots`, 'POST', reserveSlotBody) .then(data => { console.log('Current month:', data) onTimeSlotSelectChange(); onReserveSeekerSlotSuccess(); }) .catch((error) => { console.error('Error saving seeker session:', error); if (error.status === 400) { if (error.error === "400: Selected date cannot be in the past") { seekerSelectedDateIsinThePast(); } else if (error.error === "400: Slot is too soon for booking") { seekerSlotIsTooSoon(); } else { onReserveSeekerSlotError(); } } else { onReserveSeekerSlotError(); } }); } function onReserveSeekerSlotError() { const errorMessage = `

    That time’s no longer available.

    Don’t worry, just choose another time for the session.

    `; const postReserveSlotElement = document.getElementById(postReserveSlotSeekerElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = errorMessage; } else { console.error(`Element with ID ${postReserveSlotSeekerElementId} not found.`); } } function onReserveSeekerSlotSuccess() { const successMessage = `
    Email Sent Icon

    You’re all set.

    Your session slot is reserved.

    `; const postReserveSlotElement = document.getElementById(postReserveSlotSeekerElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = successMessage; } else { console.error(`Element with ID ${postReserveSlotSeekerElementId} not found.`); } } function seekerSelectedDateIsinThePast() { const errorMessage = `

    Selected date cannot be in the past.

    Don’t worry, just choose another slot to create the session.

    `; const postReserveSlotElement = document.getElementById(postReserveSlotSeekerElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = errorMessage; } else { console.error(`Element with ID ${postReserveSlotSeekerElementId} not found.`); } } function seekerSlotIsTooSoon() { const errorMessage = `

    Selected slot is to soon in the future.

    Don’t worry, just choose another slot to create the session.

    `; const postReserveSlotElement = document.getElementById(postReserveSlotSeekerElementId); if (postReserveSlotElement) { postReserveSlotElement.innerHTML = errorMessage; } else { console.error(`Element with ID ${postReserveSlotSeekerElementId} not found.`); } } function onTimeSlotSelectChange() { const selectedDate = getCalendarSelectedDate(getVisibleSeekerCalendar()); resetSeekerPostCreateSlot(); getSeekerCurrentDate(selectedDate.toISOString(), selectedDate.getTimezoneOffset(), function(currentDay) { renderSeekerSetup(currentDay); updateSeekerCalendarDays(mergeAppointmentsAndSlots(currentDay)); getVisibleSeekerCalendar().update(); console.log("Current day status:", currentDay); if (!currentDay.paymentMethodSet && currentDay.countOfPaidSessions <= currentDay.countOfUsedSession) { seekerNoMoreAvailableSlots(); toggleSeekerConfirmationButton(true); return; } renderSeekerAppointmentList(currentDay); renderSeekerSlotsList(currentDay); if (currentDay.freeSlots.length > 0) { const timeSelect = document.getElementById(seekerNewSessionTimeSelect); if (!timeSelect) { console.error(`Element with ID ${seekerNewSessionTimeSelect} not found.`); return; } if (currentDay.freeSlots.length !== timeSelect.options.length) { renderAvailableSessions(currentDay); } toggleSeekerConfirmationButton(false); } else { renderAvailableSessions(currentDay); toggleSeekerConfirmationButton(true); } }, function(error) { console.error("Error fetching seeker's slots:", error); if (error.status === 400 && error.error === "400: Selected date cannot be in the past") { seekerSelectedDateIsinThePast(); } else { seekerSlotsFailed(); } toggleSeekerConfirmationButton(true); }); } function resetAppointmentListContainer() { const appointmentListElement = document.getElementById(seekerAppointmentListElementId); if (appointmentListElement) { appointmentListElement.innerHTML = ""; } else { console.error(`Element with ID ${seekerAppointmentListElementId} not found.`); } } function renderAppointmentDate(appointment) { const appointmentStart = new Date(appointment.startTime); const appointmentEnd = new Date(appointment.endTime); const fromTime = appointmentStart.toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); const toTime = appointmentEnd.toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); const day = appointmentStart.toLocaleString('en-US', { weekday: 'long', month: 'short', day: 'numeric', hour12: true }); return `${fromTime} – ${toTime} | ${day}`; } function renderNoAppointments() { const noAppointmentsElement = document.getElementById(seekerNoAppointmentsElementId); if (noAppointmentsElement) { noAppointmentsElement.innerHTML = ""; noAppointmentsElement.innerHTML = ` No Booked Time Slots Icon

    No conversations booked yet

    Once an expert books time with you, it’ll appear here. Until then, keep sharing your link to start the right conversations.

    `; } else { console.error(`Element with ID ${seekerNoAppointmentsElementId} not found.`); } } function renderSeekerAppointmentList(currentDay) { const appointmentListElement = document.getElementById(seekerAppointmentListElementId); if (!appointmentListElement) { console.error(`Element with ID ${seekerAppointmentListElementId} not found.`); } const appointmentsTabElement = getActiveSeekerAppointmentList(); if (!appointmentsTabElement) { console.error('No active appointments tab found.'); return; } let workWithAppointments; if (appointmentsTabElement.id === seekerAppointmentsDateTabElementId) { workWithAppointments = currentDay.appointmentsToDate; } else if (appointmentsTabElement.id === seekerAppointmentsAllTabElementId) { workWithAppointments = currentDay.appointments; } else { workWithAppointments = currentDay.appointmentsToday; } if (workWithAppointments.length === 0) { appointmentListElement.innerHTML = ""; renderNoAppointments(); return; } let innerHTML = ""; workWithAppointments.forEach( (appointment) => { let subject = ""; if (appointment.title) { subject = `

    ${appointment.title}

    `; } innerHTML += `

    Conversation with ${appointment.attendees.join(', ')}

    ${subject}

    ${renderAppointmentDate(appointment)}

    Cancel this conversation?

    This will cancel your confirmed session with XY(gmail@gmail.com).

    They’ll be notified right away.

    `; }); appointmentListElement.innerHTML = innerHTML; setupCancelMeetingToggle(); } function renderNoSlots() { const noSlotsElement = document.getElementById(seekerNoSlotsElementId); if (noSlotsElement) { let textBelow = 'Choose the times you’re available, and they’ll appear here—ready for experts to book.'; if (window.location.href.indexOf("professional-dashboard.html") !== -1) { textBelow = 'Choose the times you’re available, and they’ll appear here—ready to book.'; } noSlotsElement.innerHTML = ""; noSlotsElement.innerHTML = ` Calendar Icon

    No time slots added yet

    ${textBelow}

    `; } else { console.error(`Element with ID ${seekerNoSlotsElementId} not found.`); } } function renderSeekerSlotsList(currentDay) { function agregateSlotsByDate(slots) { const slotsByDate = new Map(); slots.forEach(slot => { const slotDate = new Date(slot.startTime); const dateKey = `${slotDate.getFullYear()}-${String(slotDate.getMonth() + 1).padStart(2, '0')}-${String(slotDate.getDate()).padStart(2, '0')}`; if (!slotsByDate.has(dateKey)) { slotsByDate.set(dateKey, []); } slotsByDate.get(dateKey).push(slot); }); return slotsByDate; } function displayMaxThreeTimeSlots(slots) { let ret = []; const loopEnd = Math.min(...[3, slots.length]); for (let i = 0; i < loopEnd; i++) { const fromTime = (new Date(slots[i].startTime)).toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); const toTime = (new Date(slots[i].endTime)).toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); ret.push(`${fromTime} – ${toTime}`); } return ret.join('; ') } function renderSlotsDetail(slots) { let ret = ""; slots.forEach(slot => { const fromTime = (new Date(slot.startTime)).toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); const toTime = (new Date(slot.endTime)).toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); ret += `
  • ${fromTime} – ${toTime} Delete Icon
  • `; }); return ret; } function composeOnclickParameters(slots) { return JSON.stringify(slots); } const slotsListElement = document.getElementById(seekerSlotsListElementId); if (!slotsListElement) { console.error(`Element with ID ${seekerSlotsListElementId} not found.`); } const slotsTabElement = getActiveSeekerSlotList(); if (!slotsTabElement) { console.error('No active slots tab found.'); return; } let workWithSlots; if (slotsTabElement.id === seekerSlotsDateTabElementId) { workWithSlots = currentDay.createdSlotsToDate; } else if (slotsTabElement.id === seekerSlotsAllTabElementId) { workWithSlots = currentDay.allCreatedSlots; } else { workWithSlots = currentDay.createdSlotsToday; } if (workWithSlots.length === 0) { slotsListElement.innerHTML = ""; renderNoSlots(); return; } const slotsByDate = agregateSlotsByDate(workWithSlots); let innerHTML = ''; slotsByDate.forEach((slots, date) => { innerHTML += '
    '; innerHTML += `

    ${date.toLocaleString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}

    ${displayMaxThreeTimeSlots(slots)}...

    `; innerHTML += `
      ${renderSlotsDetail(slots)}
    `; innerHTML += '
    '; }); slotsListElement.innerHTML = innerHTML; } function joinMeeting(meetingUrl) { console.log("Joining meeting:", meetingUrl); window.open(meetingUrl, '_blank'); } function seekerCancelMeeting(appointmentId) { console.log("Cancelling meeting:", appointmentId); return callApi(`/api/appointment/${appointmentId}`, 'DELETE') .then(data => { console.log('Appointment cancelled:', data) seekerRefreshCalendarSlots(); }) .catch((error) => { console.log('Error cancelling appointment:', error); seekerRefreshCalendarSlots(); }); } function deleteSlot(startTime, endTime) { console.log("Deleting slot from", startTime, "to", endTime) return callApi(`/api/seeker-slots?startTime=${encodeURIComponent(startTime)}&endTime=${encodeURIComponent(endTime)}`, 'DELETE') .then(data => { console.log(`Slot deleted ${startTime}-${endTime}:`, data); seekerRefreshCalendarSlots(); }) .catch((error) => { console.log(`Slot delete failed ${startTime}-${endTime}:`, error); seekerRefreshCalendarSlots(); }); } function deleteSlots(params) { let promises = []; let idx = 0; function deleteOneSlot(slot) { return callApi(`/api/seeker-slots?startTime=${encodeURIComponent(slot.startTime)}&endTime=${encodeURIComponent(slot.endTime)}`, 'DELETE') .then(data => { console.log(`Slot deleted`, data); idx += 1; if (params.length > idx) { promises.push(deleteOneSlot(params[idx])); } else { Promise.all(promises).then((values) => { console.log('All slots deleted:', values); seekerRefreshCalendarSlots(); }); } }) .catch((error) => { console.log(`Slot delete failed`, error); idx += 1; if (params.length > idx) { promises.push(deleteOneSlot(params[idx])); } else { Promise.all(promises).then((values) => { console.log('All slots deleted:', values); seekerRefreshCalendarSlots(); }); } }); } console.log("Deleting slots:", params); if (params.length > idx) { promises.push(deleteOneSlot(params[0])); } } const paymentElementId = 'stripe-payment-element'; const paymentSubmitElementId = 'stripe-payment-submit'; const paymentFormElementId = 'stripe-payment-form'; const paymentErrorElementId = 'stripe-error-message'; const popupWrapperElementId = 'payment-popup-wrapper'; function startCreditCardAuthorization(stripePublicKey) { const stripe = new Stripe(stripePublicKey); const elements = stripe.elements({ mode: 'setup', currency: 'usd', }); const paymentElement = elements.create('payment', { layout: 'accordion'}); paymentElement.mount(`#${paymentElementId}`); const submitBtn = document.getElementById(paymentSubmitElementId); submitBtn.style.visibility = 'visible'; const handleError = (error) => { const paymentErrorElement = document.getElementById(paymentErrorElementId); paymentErrorElement.innerHTML = error.message; submitBtn.disabled = false; } const popupWrapper = document.getElementById(popupWrapperElementId); popupWrapper.style.visibility = 'visible'; popupWrapper.style.opacity = '1'; const form = document.getElementById(paymentFormElementId); form.addEventListener('submit', async (event) => { event.preventDefault(); if (submitBtn.disabled) { return; } submitBtn.disabled = true; const {error: submitError} = await elements.submit(); if (submitError) { handleError(submitError); return; } callApi('/api/stripe-subscription', 'POST') .then(data => { console.log('Stripe subscription successful:', data); const response = JSON.parse(data.response); stripe.confirmSetup({ elements: elements, clientSecret: response.clientSecretForFirstPayment, confirmParams: { return_url: 'https://nectly.io/seekers-dashboard.html', }, redirect: 'always' }).then((result) => { if(result.error) { handleError(result.error); console.error('Payment failed:', result.error); } else { console.log('Payment subscription successful:', result); } }); }) .catch(error => { console.error('Stripe subscription failed:', error); }); }); } function closePaymentPopup() { const popupWrapper = document.getElementById("payment-popup-wrapper"); popupWrapper.style.visibility = 'hidden'; popupWrapper.style.opacity = '0'; } const closePaymentPopupElement = document.getElementById("close-payment-popup"); if (closePaymentPopupElement) closePaymentPopupElement.addEventListener("click", function(event) { event.preventDefault(); closePaymentPopup(); }); document.addEventListener("keydown", function(event) { if (event.key === "Escape") { closePaymentPopup(); } }); const paymentPopupWrapperElement = document.getElementById("payment-popup-wrapper"); if (paymentPopupWrapperElement) paymentPopupWrapperElement.addEventListener("click", function(event) { const form = document.querySelector(".popup-form"); if (!form.contains(event.target)) { closePaymentPopup(); } }); function redirectToStripeConnectLink() { callApi('/api/stripe-connect/get-account-link', 'GET') .then(data => { console.log('Get Stripe Connect Link successful:', data); const response = JSON.parse(data.response); window.location.href = response.link; }) .catch(error => { console.error('Stripe subscription failed:', error); }); } function setupCharity(charityPayoutType) { const body = { "charityPayoutType": charityPayoutType }; callApi('/api/professional-setup-charity', 'POST', body) .then(data => { console.log('Setup charity successful:', data); proRefreshCalendarSlots(); }) .catch(error => { console.error('Setup charity failed:', error); }); } function proResetPayout() { const postReserveSlotElement = document.getElementById('account-settings-save-result'); const successMessage = `

    Payout methods reset successfully.

    `; const failedMessage = `

    Failed to reset your payout methods.

    `; callApi('/api/professional-reset-payouts', 'DELETE') .then(data => { console.log('Professional payout reset successful:', data); postReserveSlotElement.innerHTML = successMessage; }) .catch(error => { console.error('Professional payout reset failed:', error); postReserveSlotElement.innerHTML = failedMessage; }); } function requestStripeInformation() { callApi('/api/stripe-connect/request-information', 'GET') .then(data => { console.log('Get Stripe Connect Link successful:', data); const response = JSON.parse(data.response); window.open(response.requestInformationLink, '_blank').focus(); }) .catch(error => { console.error('Stripe subscription failed:', error); }); return false; } function activateProfessional() { addUserRole(NECTLY_USER_ROLE_PROFESSIONAL); window.location = '/professional-dashboard.html'; } function activateSeeker() { addUserRole(NECTLY_USER_ROLE_SEEKER); window.location = '/seekers-dashboard.html'; } const conversationHistoryTableElementId = 'conversation-history-table'; let conversationHistoryPages = []; let conversationHistoryIndex = -1; function renderConversationHistoryTable(action) { /* { "items": [ {"startTime": "2025-05-29T19:30:00+02:00", "seeker": true, "professional": true, "type": "paid", "amount": "$30.00"}, {"startTime": "2025-05-29T20:30:00+02:00", "seeker": true, "professional": true, "type": "paid", "amount": "$30.00"} ], "nextToken": "next-token-id" } */ function clearConversationHistoryTable(conversationHistoryTableElement) { conversationHistoryTableElement.replaceChildren(); conversationHistoryTableElement.innerHTML = ""; } function renderTableHeader(conversationHistoryTableElement) { const headerElement = document.createElement('div'); headerElement.innerHTML = `

    Start time and date

    Seeker

    Professional

    Payment status

    `; headerElement.className = "conversation-history-table-header"; conversationHistoryTableElement.appendChild(headerElement); } function renderNoConversation(conversationHistoryTableElement) { const emptyMessageElement = document.createElement('div'); emptyMessageElement.className = "conversation-history-status-container flex flex-column gap-1"; emptyMessageElement.innerHTML = ` No Conversations Illustration

    No past conversations yet.

    Once you’ve had a few, they’ll appear here—your personal history of meaningful, 20-minute connections.

    `; conversationHistoryTableElement.appendChild(emptyMessageElement); } function renderTableItem(conversationHistoryTableElement, item) { const startTime = new Date(item.startTime); let seeker = 'No'; let seekerClass = 'system-status no'; if (item.seeker) { seeker = 'Yes' seekerClass = 'system-status yes'; } const rowElement = document.createElement('div'); let professional = 'No'; let professionalClass = 'system-status no'; if (item.professional) { professional = 'Yes'; professionalClass = 'system-status yes'; } let paymentStatus = 'Unpaid'; let paymentStatusClass = 'system-status unpaid'; if (item.type === 'paid') { paymentStatus = 'Paid ' + item.amount; paymentStatusClass = 'system-status paid'; } if (item.type === 'charity') { paymentStatus = 'For charity'; paymentStatusClass = 'system-status charity'; } rowElement.innerHTML = `

    ${startTime.toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}

    ${seeker}

    ${professional}

    ${paymentStatus}

    `; rowElement.className = "conversation-history-table-row"; conversationHistoryTableElement.appendChild(rowElement); } function renderPagination(conversationHistoryTableElement, historyPage) { if (historyPage.nextToken) { const paginationElement = document.createElement('div'); paginationElement.className = "pagination"; if (conversationHistoryIndex <= 0) { paginationElement.innerHTML = ` `; } else { paginationElement.innerHTML = ` `; } conversationHistoryTableElement.appendChild(paginationElement); } else if (conversationHistoryIndex > 0) { const paginationElement = document.createElement('div'); paginationElement.className = "pagination"; paginationElement.innerHTML = ` `; conversationHistoryTableElement.appendChild(paginationElement); } else { const paginationElement = document.createElement('div'); paginationElement.className = "pagination"; paginationElement.innerHTML = ` `; conversationHistoryTableElement.appendChild(paginationElement); } } function renderTable(conversationHistoryTableElement, historyPage) { clearConversationHistoryTable(conversationHistoryTableElement); if (historyPage.items.length === 0) { renderNoConversation(conversationHistoryTableElement); return; } renderTableHeader(conversationHistoryTableElement); historyPage.items.forEach(item => { renderTableItem(conversationHistoryTableElement, item); }); renderPagination(conversationHistoryTableElement, historyPage); } const conversationHistoryTableElement = document.getElementById(conversationHistoryTableElementId); if (!conversationHistoryTableElement) { console.error(`Element with ID ${conversationHistoryTableElementId} not found.`); return; } if (action === 'init') { callApi('/api/conversation/history', 'GET') .then(data => { console.log('Get conversation history successful:', data); const historyPage = JSON.parse(data.response); conversationHistoryPages = [historyPage]; conversationHistoryIndex = 0; renderTable(conversationHistoryTableElement, historyPage); }) .catch(error => { console.error('Get conversation history failed:', error); }); } if (action === 'next' && conversationHistoryIndex === conversationHistoryPages.length - 1) { callApi(`/api/conversation/history&nextToken=${conversationHistoryPages[conversationHistoryIndex].nextToken}`, 'GET') .then(data => { console.log('Get conversation history successful:', data); const historyPage = JSON.parse(data.response); conversationHistoryPages.push(historyPage); conversationHistoryIndex += 1; renderTable(conversationHistoryTableElement, historyPage); }) .catch(error => { console.error('Get conversation history failed:', error); }); } if (action === 'next' && conversationHistoryIndex < conversationHistoryPages.length - 1) { conversationHistoryIndex += 1; const historyPage = conversationHistoryPages[conversationHistoryIndex]; renderTable(conversationHistoryTableElement, historyPage); } if (action === 'prev') { if (conversationHistoryIndex > 0) conversationHistoryIndex -= 1; const historyPage = conversationHistoryPages[conversationHistoryIndex]; renderTable(conversationHistoryTableElement, historyPage); } } function initLinkButtonGlowEffect() { const wrapper = document.querySelector('.glow-button-wrapper'); if (wrapper) { wrapper.addEventListener('mousemove', (e) => { const rect = wrapper.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const angle = Math.atan2(y - rect.height / 2, x - rect.width / 2) * (180 / Math.PI); const deg = (angle + 360) % 360; wrapper.style.setProperty('--start', deg); wrapper.style.setProperty('--active', 1); }); wrapper.addEventListener('mouseleave', () => { wrapper.style.setProperty('--active', 0); }); } } const tabs = document.querySelectorAll('.tab'); const contents = document.querySelectorAll('.tab-content'); tabs.forEach(tab => { tab.addEventListener('click', () => { const tabId = tab.getAttribute('data-tab'); tabs.forEach(t => t.classList.remove('active')); tab.classList.add('active'); contents.forEach(c => { c.classList.toggle('active', c.getAttribute('data-content') === tabId); }); }); }); function setupCancelMeetingToggle() { document.querySelectorAll('.slot-list-item').forEach(item => { const cancelBtn = item.querySelector('#cancelMeetingButton'); const confirmBtn = item.querySelector('#cancelMeetingConfirmationButton'); const declineBtn = item.querySelector('#cancelMeetingDeclineButton'); const cancelWindow = item.querySelector('#cancel-conversation-window'); const meetingContainer = item.querySelector('.meeting-button-container'); if (!cancelBtn || !confirmBtn || !declineBtn || !cancelWindow || !meetingContainer) return; // Init styles cancelWindow.style.height = "0px"; cancelWindow.style.opacity = "0"; cancelWindow.style.padding = "0"; cancelWindow.style.overflow = "hidden"; cancelWindow.style.transition = "height 0.3s ease, opacity 0.3s ease, padding 0.3s ease"; meetingContainer.style.transition = "height 0.3s ease, padding 0.3s ease"; // OPEN: Cancel Conversation cancelBtn.addEventListener("click", () => { item.classList.add("cancel-active"); const contentHeight = cancelWindow.scrollHeight; cancelWindow.style.height = contentHeight + "px"; cancelWindow.style.opacity = "1"; cancelWindow.style.padding = "1rem"; meetingContainer.style.height = "0px"; }); // CLOSE: Confirm or Decline const resetView = () => { item.classList.remove("cancel-active"); cancelWindow.style.height = "0px"; cancelWindow.style.opacity = "0"; cancelWindow.style.padding = "0"; meetingContainer.style.height = ""; meetingContainer.style.padding = ""; }; confirmBtn.addEventListener("click", resetView); declineBtn.addEventListener("click", resetView); }); } function setupTogglePairs() { document.querySelectorAll('.toggle-pair').forEach(pair => { const a = pair.querySelector('.toggle-a'); const b = pair.querySelector('.toggle-b'); if (!a || !b) return; a.style.height = a.scrollHeight + 'px'; pair.addEventListener('mouseenter', () => { a.style.height = '0px'; a.style.opacity = '0'; b.style.height = b.scrollHeight + 'px'; b.style.opacity = '1'; }); pair.addEventListener('mouseleave', () => { a.style.height = a.scrollHeight + 'px'; a.style.opacity = '1'; b.style.height = '0px'; b.style.opacity = '0'; }); }); } new ResizeObserver(entries => { for (let entry of entries) { const container = entry.target; const { width, height } = entry.contentRect; if (width > height) { container.classList.add('row-layout'); container.classList.remove('column-layout'); } else { container.classList.add('column-layout'); container.classList.remove('row-layout'); } } }); function loadFaqToggle() { const toggles = document.querySelectorAll('.faq-toggle'); if (!toggles) return; toggles.forEach(btn => { btn.addEventListener('click', () => { const expanded = btn.getAttribute('aria-expanded') === 'true'; const content = document.getElementById(btn.getAttribute('aria-controls')); const icon = btn.querySelector('.faq-icon'); btn.setAttribute('aria-expanded', String(!expanded)); icon.textContent = expanded ? '+' : '–'; content.classList.toggle('open', !expanded); }); }); // delayed auto-toggle (to be safe after scroll & layout) setTimeout(() => { const hash = location.hash.slice(1); if (!hash) return; const section = document.getElementById(hash); if (!section) return; const autoBtn = section.querySelector('.faq-toggle'); if (autoBtn && autoBtn.getAttribute('aria-expanded') === 'false') { autoBtn.click(); } }, 200); } function initSwipeSections() { const menu = document.querySelector('.app-menu'); const container = document.querySelector('.scroll-container'); if (!menu || !container) return; const buttons = menu.querySelectorAll('button'); function scrollToIndex(index) { const containerWidth = container.clientWidth; container.scrollTo({ left: index * containerWidth, behavior: 'smooth', }); } function updateActiveButton() { const index = Math.round(container.scrollLeft / container.clientWidth); buttons.forEach((btn, i) => { btn.classList.toggle('active', i === index); }); } buttons.forEach((button, index) => { button.addEventListener('click', () => { scrollToIndex(index); }); }); container.addEventListener('scroll', () => { updateActiveButton(); }); // Nastaviť správny button na začiatku updateActiveButton(); } function safeInitSwipeSections(attempt = 0) { const menus = document.querySelectorAll('.app-menu'); const containers = document.querySelectorAll('.scroll-container'); if (menus.length > 0 && containers.length > 0) { // overíme, či už majú kontajnery nenulovú šírku const allReady = Array.from(containers).every((c) => c.clientWidth > 0); if (allReady) { initSwipeSections(); } else if (attempt < 20) { setTimeout(() => safeInitSwipeSections(attempt + 1), 100); // opakuj max. 20x } } else if (attempt < 20) { setTimeout(() => safeInitSwipeSections(attempt + 1), 100); } } let isAnimating = false; function setupNectlyLinkButtonAnimation() { const button = document.getElementById('nectly-link-button'); if (!button) return; if (button.dataset.animationInitialized === 'true') return; button.dataset.animationInitialized = 'true'; button.addEventListener('click', () => { if (isAnimating) return; isAnimating = true; button.classList.add('clicked'); setTimeout(() => { button.classList.remove('clicked'); button.classList.add('returning'); setTimeout(() => { button.classList.remove('returning'); isAnimating = false; }, 300); }, 3000); }); }