import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = [ "timezoneDisplay", "timezoneOffset", "timezoneNote", "timezoneStatus", "currentLocalTime", "timezoneConversionStatus", "userTimezoneInput", "selectedDatetimeInput", "selectedTimeDisplay", "selectedTimeText", "submitButton", "durationInput", "timezoneSelect" ] // Common timezone list with readable names static timezones = [ { value: "America/New_York", label: "Eastern Time (ET)" }, { value: "America/Chicago", label: "Central Time (CT)" }, { value: "America/Denver", label: "Mountain Time (MT)" }, { value: "America/Los_Angeles", label: "Pacific Time (PT)" }, { value: "America/Anchorage", label: "Alaska Time (AKT)" }, { value: "Pacific/Honolulu", label: "Hawaii Time (HST)" }, { value: "Europe/London", label: "Greenwich Mean Time (GMT)" }, { value: "Europe/Paris", label: "Central European Time (CET)" }, { value: "Europe/Berlin", label: "Central European Time (CET)" }, { value: "Europe/Rome", label: "Central European Time (CET)" }, { value: "Europe/Madrid", label: "Central European Time (CET)" }, { value: "Europe/Amsterdam", label: "Central European Time (CET)" }, { value: "Europe/Brussels", label: "Central European Time (CET)" }, { value: "Europe/Vienna", label: "Central European Time (CET)" }, { value: "Europe/Zurich", label: "Central European Time (CET)" }, { value: "Europe/Stockholm", label: "Central European Time (CET)" }, { value: "Europe/Oslo", label: "Central European Time (CET)" }, { value: "Europe/Copenhagen", label: "Central European Time (CET)" }, { value: "Europe/Helsinki", label: "Eastern European Time (EET)" }, { value: "Europe/Athens", label: "Eastern European Time (EET)" }, { value: "Europe/Bucharest", label: "Eastern European Time (EET)" }, { value: "Europe/Sofia", label: "Eastern European Time (EET)" }, { value: "Europe/Kiev", label: "Eastern European Time (EET)" }, { value: "Europe/Moscow", label: "Moscow Time (MSK)" }, { value: "Asia/Tokyo", label: "Japan Standard Time (JST)" }, { value: "Asia/Shanghai", label: "China Standard Time (CST)" }, { value: "Asia/Hong_Kong", label: "Hong Kong Time (HKT)" }, { value: "Asia/Singapore", label: "Singapore Time (SGT)" }, { value: "Asia/Seoul", label: "Korea Standard Time (KST)" }, { value: "Asia/Kolkata", label: "India Standard Time (IST)" }, { value: "Asia/Dubai", label: "Gulf Standard Time (GST)" }, { value: "Asia/Jakarta", label: "Western Indonesian Time (WIB)" }, { value: "Australia/Sydney", label: "Australian Eastern Time (AEST)" }, { value: "Australia/Melbourne", label: "Australian Eastern Time (AEST)" }, { value: "Australia/Perth", label: "Australian Western Time (AWST)" }, { value: "Pacific/Auckland", label: "New Zealand Standard Time (NZST)" }, { value: "UTC", label: "Coordinated Universal Time (UTC)" } ] static values = { maxAttempts: { type: Number, default: 10 } } connect() { console.log('Timezone conversion controller connected') // Populate the timezone dropdown first this.populateTimezoneDropdown() // Then detect user timezone this.detectUserTimezone() this.updateCurrentTime() this.startTimeUpdates() // Set up load more button listener this.setupLoadMoreButton() // Start attempting to convert time slots after a short delay setTimeout(() => { this.attemptConversion() }, 100) } disconnect() { if (this.timeUpdateInterval) { clearInterval(this.timeUpdateInterval) } } // Detect user's timezone detectUserTimezone() { console.log("Starting timezone detection...") try { // Try to get the timezone name (e.g., "America/New_York") const timezoneName = Intl.DateTimeFormat().resolvedOptions().timeZone console.log("Detected timezone name:", timezoneName) if (timezoneName && timezoneName !== 'UTC') { console.log("Using detected timezone name:", timezoneName) this.setTimezoneDisplay(timezoneName, timezoneName) return } } catch (e) { console.log('Could not detect timezone name, using offset:', e) } // Fallback to offset calculation console.log("Using offset-based timezone detection...") const now = new Date() const offset = now.getTimezoneOffset() const hoursOffset = Math.abs(offset / 60) const minutesOffset = Math.abs(offset % 60) const sign = offset < 0 ? '+' : '-' const offsetString = `UTC${sign}${hoursOffset.toString().padStart(2, '0')}:${minutesOffset.toString().padStart(2, '0')}` console.log("Using offset-based timezone:", offsetString) this.setTimezoneDisplay(offsetString, offsetString) } // Set timezone display elements setTimezoneDisplay(timezone, displayName) { console.log("Setting timezone display:", { timezone, displayName }) if (this.hasUserTimezoneInputTarget) { this.userTimezoneInputTarget.value = timezone console.log("Set userTimezoneInput value to:", timezone) } if (this.hasTimezoneDisplayTarget) { this.timezoneDisplayTarget.textContent = displayName.replace(/_/g, ' ') console.log("Set timezoneDisplay text to:", displayName.replace(/_/g, ' ')) } if (this.hasTimezoneOffsetTarget) { this.timezoneOffsetTarget.textContent = `(${timezone})` console.log("Set timezoneOffset text to:", `(${timezone})`) } if (this.hasTimezoneNoteTarget) { this.timezoneNoteTarget.textContent = displayName.replace(/_/g, ' ') } if (this.hasTimezoneStatusTarget) { this.timezoneStatusTarget.classList.remove('hidden') } // Update dropdown selection if it exists if (this.hasTimezoneSelectTarget) { this.timezoneSelectTarget.value = timezone } // Load time slots with the detected timezone this.loadTimeSlots(timezone) } // Update current local time updateCurrentTime() { if (this.hasCurrentLocalTimeTarget) { const now = new Date() const options = { hour: 'numeric', minute: '2-digit', second: '2-digit', timeZoneName: 'short' } this.currentLocalTimeTarget.textContent = now.toLocaleTimeString('en-US', options) } } // Start updating time every second startTimeUpdates() { this.updateCurrentTime() this.timeUpdateInterval = setInterval(() => { this.updateCurrentTime() }, 1000) } // Load time slots using Turbo loadTimeSlots(timezone) { if (this.hasTimezoneConversionStatusTarget) { this.timezoneConversionStatusTarget.textContent = "Loading available time slots..." this.timezoneConversionStatusTarget.className = 'text-xs text-blue-600 mb-4' } // Get the current URL and add the timezone parameter const currentUrl = new URL(window.location.href) currentUrl.searchParams.set('timezone', timezone) // Use fetch to load the content and manually update the turbo frame fetch(currentUrl.toString(), { headers: { 'Accept': 'text/html', 'Turbo-Frame': 'time-slots' } }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } return response.text() }) .then(html => { // The response is just the partial content, not wrapped in #time-slots // So we can use the HTML directly if (html.trim() !== '') { const turboFrame = document.getElementById('time-slots') if (turboFrame) { turboFrame.innerHTML = html // Update status if (this.hasTimezoneConversionStatusTarget) { this.timezoneConversionStatusTarget.textContent = "Time slots loaded successfully!" this.timezoneConversionStatusTarget.className = 'text-xs text-green-600 mb-4' } // Set up time slot listeners this.setupTimeSlotListeners() } } else { if (this.hasTimezoneConversionStatusTarget) { this.timezoneConversionStatusTarget.textContent = "No time slots available for the selected timezone" this.timezoneConversionStatusTarget.className = 'text-xs text-yellow-600 mb-4' } } }) .catch(error => { console.error("Error loading time slots:", error) if (this.hasTimezoneConversionStatusTarget) { this.timezoneConversionStatusTarget.textContent = "Error loading time slots. Please try again." this.timezoneConversionStatusTarget.className = 'text-xs text-red-600 mb-4' } }) } // Convert all time slots to user's timezone convertTimeSlotsToUserTimezone() { const userTimezone = this.hasUserTimezoneInputTarget ? this.userTimezoneInputTarget.value : 'UTC' const timeSlots = document.querySelectorAll('.time-slot') let convertedCount = 0 let totalCount = timeSlots.length if (totalCount === 0) { return { convertedCount: 0, totalCount: 0 } } timeSlots.forEach((slot) => { const datetime = slot.dataset.datetime if (datetime) { const date = new Date(datetime) try { const options = { hour: 'numeric', minute: '2-digit', hour12: true } const localTime = date.toLocaleTimeString('en-US', { ...options, timeZone: userTimezone }) const timeDisplay = slot.querySelector('.time-display') if (timeDisplay) { timeDisplay.textContent = localTime convertedCount++ } } catch (e) { } } }) return { convertedCount, totalCount } } // Set up time slot event listeners setupTimeSlotListeners() { const timeSlots = document.querySelectorAll('.time-slot') timeSlots.forEach((slot) => { slot.addEventListener('click', (event) => { this.handleTimeSlotClick(event, slot) }) }) } // Handle time slot click handleTimeSlotClick(event, slot) { // Remove active class from all slots document.querySelectorAll('.time-slot').forEach(s => { s.classList.remove('bg-blue-500', 'text-white', 'border-blue-500') }) // Add active class to clicked slot slot.classList.add('bg-blue-500', 'text-white', 'border-blue-500') // Set the selected datetime const datetime = slot.dataset.datetime const duration = parseInt(slot.dataset.duration) / 60 // Convert to minutes if (this.hasSelectedDatetimeInputTarget) { this.selectedDatetimeInputTarget.value = datetime } if (this.hasDurationInputTarget) { this.durationInputTarget.value = duration } // Show selected time in user's timezone this.updateSelectedTimeDisplay(datetime) // Enable submit button this.enableSubmitButton() } // Update selected time display updateSelectedTimeDisplay(datetime) { if (!this.hasSelectedTimeTextTarget || !this.hasSelectedTimeDisplayTarget) return const date = new Date(datetime) const userTimezone = this.hasUserTimezoneInputTarget ? this.userTimezoneInputTarget.value : 'UTC' try { const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZoneName: 'short', timeZone: userTimezone } const formattedTime = date.toLocaleDateString('en-US', options) this.selectedTimeTextTarget.textContent = formattedTime } catch (e) { // Fallback to local timezone if the detected timezone is invalid const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZoneName: 'short' } const fallbackTime = date.toLocaleDateString('en-US', options) this.selectedTimeTextTarget.textContent = fallbackTime } this.selectedTimeDisplayTarget.classList.remove('hidden') } // Enable submit button enableSubmitButton() { if (!this.hasSubmitButtonTarget) return this.submitButtonTarget.disabled = false this.submitButtonTarget.classList.remove('bg-gray-300', 'cursor-not-allowed') this.submitButtonTarget.classList.add('bg-blue-600', 'hover:bg-blue-700') } // Check for time slots and run conversion checkAndRunConversion() { const timeSlots = document.querySelectorAll('.time-slot') if (timeSlots.length> 0) { this.setupTimeSlotListeners() this.runTimezoneConversion() return true } else { return false } } // Run timezone conversion runTimezoneConversion() { const result = this.convertTimeSlotsToUserTimezone() if (this.hasTimezoneConversionStatusTarget) { if (result.convertedCount === result.totalCount) { this.timezoneConversionStatusTarget.textContent = `βœ“ All ${result.totalCount} times converted to your timezone` this.timezoneConversionStatusTarget.className = 'text-xs text-green-600 mb-4' } else { this.timezoneConversionStatusTarget.textContent = `⚠ ${result.convertedCount}/${result.totalCount} times converted to your timezone` this.timezoneConversionStatusTarget.className = 'text-xs text-yellow-600 mb-4' } } } // Populate the timezone dropdown populateTimezoneDropdown() { try { if (!this.hasTimezoneSelectTarget) { console.log('No timezone select target found') return } console.log('Populating timezone dropdown...') // Clear the loading option this.timezoneSelectTarget.innerHTML = '' // Add the detected timezone first (if available) const detectedTimezone = this.hasUserTimezoneInputTarget ? this.userTimezoneInputTarget.value : null if (detectedTimezone && detectedTimezone !== 'UTC') { const detectedOption = document.createElement('option') detectedOption.value = detectedTimezone detectedOption.textContent = `πŸ“ ${this.getTimezoneLabel(detectedTimezone)} (Detected)` detectedOption.selected = true this.timezoneSelectTarget.appendChild(detectedOption) // Add a separator const separator = document.createElement('option') separator.disabled = true separator.textContent = '──────────' this.timezoneSelectTarget.appendChild(separator) } // Add all available timezones if (this.constructor.timezones && this.constructor.timezones.length> 0) { this.constructor.timezones.forEach(timezone => { const option = document.createElement('option') option.value = timezone.value option.textContent = timezone.label // Don't duplicate the detected timezone if (timezone.value !== detectedTimezone) { this.timezoneSelectTarget.appendChild(option) } }) console.log('Timezone dropdown populated with', this.constructor.timezones.length, 'options') } else { console.error('No timezones available') } } catch (error) { console.error('Error populating timezone dropdown:', error) } } // Get a readable label for a timezone getTimezoneLabel(timezoneValue) { const timezone = this.constructor.timezones.find(tz => tz.value === timezoneValue) return timezone ? timezone.label : timezoneValue.replace(/_/g, ' ') } // Handle timezone dropdown change onTimezoneChange(event) { const selectedTimezone = event.target.value if (!selectedTimezone) return // Update the display this.setTimezoneDisplay(selectedTimezone, selectedTimezone) // Reload time slots with the new timezone this.loadTimeSlots(selectedTimezone) // Update the status if (this.hasTimezoneConversionStatusTarget) { this.timezoneConversionStatusTarget.textContent = `Switched to ${this.getTimezoneLabel(selectedTimezone)}` this.timezoneConversionStatusTarget.className = 'text-xs text-blue-600 mb-4' } } // Manual load time slots (for button click) manualLoadTimeSlots() { const timezone = this.hasUserTimezoneInputTarget ? this.userTimezoneInputTarget.value : 'UTC'; this.loadTimeSlots(timezone); } // Attempt conversion with retries attemptConversion() { let attempts = 0 const attempt = () => { if (attempts>= this.maxAttemptsValue) { if (this.hasTimezoneConversionStatusTarget) { this.timezoneConversionStatusTarget.textContent = '⚠ Could not find time slots to convert' this.timezoneConversionStatusTarget.className = 'text-xs text-yellow-600 mb-4' } return } attempts++ if (!this.checkAndRunConversion()) { // Retry with exponential backoff: 500ms, 1s, 2s, 4s, etc. const delay = Math.min(500 * Math.pow(2, attempts - 1), 5000) setTimeout(attempt, delay) } } attempt() } // Set up load more button listener setupLoadMoreButton() { // Use event delegation to handle dynamically added buttons document.addEventListener('click', (event) => { if (event.target && event.target.id === 'load-more-days-btn') { event.preventDefault() this.loadMoreDays(event.target) } }) } // Load more days loadMoreDays(button) { const lastLoadedDate = button.dataset.lastLoadedDate const timezone = button.dataset.timezone || 'UTC' // Disable button and show loading state button.disabled = true button.innerHTML = 'Loading...' // Get the current URL path (without query params) const currentPath = window.location.pathname const loadMoreUrl = `${currentPath}/load_more?last_loaded_date=${lastLoadedDate}&timezone=${timezone}` fetch(loadMoreUrl, { headers: { 'Accept': 'application/json' } }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } return response.json() }) .then(data => { if (data.slots_by_date && Object.keys(data.slots_by_date).length> 0) { // Append new days to the existing time slots this.appendNewDays(data.slots_by_date, data.next_start_date, data.next_end_date, timezone) // Update button for next load if (data.has_more) { // Find the last loaded date from the new slots const newDates = Object.keys(data.slots_by_date).map(dateStr => new Date(dateStr)).sort((a, b) => a - b) const lastLoadedDate = newDates[newDates.length - 1] button.dataset.lastLoadedDate = lastLoadedDate.toISOString().split('T')[0] button.innerHTML = 'Load More Days' button.disabled = false } else { // Hide button if no more days available button.style.display = 'none' } // Update status if (this.hasTimezoneConversionStatusTarget) { const newDaysCount = Object.keys(data.slots_by_date).length this.timezoneConversionStatusTarget.textContent = `βœ“ Loaded ${newDaysCount} more days!` this.timezoneConversionStatusTarget.className = 'text-xs text-green-600 mb-4' } } else { // No more slots available button.style.display = 'none' if (this.hasTimezoneConversionStatusTarget) { this.timezoneConversionStatusTarget.textContent = "No more available time slots" this.timezoneConversionStatusTarget.className = 'text-xs text-yellow-600 mb-4' } } }) .catch(error => { console.error("Error loading more days:", error) // Reset button button.innerHTML = 'Load More Days' button.disabled = false if (this.hasTimezoneConversionStatusTarget) { this.timezoneConversionStatusTarget.textContent = "Error loading more days. Please try again." this.timezoneConversionStatusTarget.className = 'text-xs text-red-600 mb-4' } }) } // Append new days to the existing time slots appendNewDays(slotsByDate, nextStartDate, nextEndDate, timezone) { const timeSlotsContainer = document.getElementById('time-slots') if (!timeSlotsContainer) { console.error("Time slots container not found") return } // Find the existing days container let daysContainer = timeSlotsContainer.querySelector('.space-y-4') if (!daysContainer) { console.error("Days container not found") return } // Find the load more button to insert before it const loadMoreButton = document.getElementById('load-more-days-btn') const loadMoreButtonContainer = loadMoreButton ? loadMoreButton.parentElement : null // Convert date strings back to Date objects for processing const processedSlotsByDate = {} Object.keys(slotsByDate).forEach(dateStr => { const date = new Date(dateStr) processedSlotsByDate[date] = slotsByDate[dateStr] }) // Sort dates const sortedDates = Object.keys(processedSlotsByDate).sort((a, b) => new Date(a) - new Date(b)) // Create HTML for new days and insert them before the load more button sortedDates.forEach(dateStr => { const date = new Date(dateStr) const slots = processedSlotsByDate[dateStr] if (slots && slots.length> 0) { const dayHtml = this.createDayHtml(date, slots, timezone) // Insert before the load more button if it exists, otherwise append to end if (loadMoreButtonContainer) { loadMoreButtonContainer.insertAdjacentHTML('beforebegin', dayHtml) } else { daysContainer.insertAdjacentHTML('beforeend', dayHtml) } } }) // Update the load more button data attributes if (loadMoreButton) { // Find the last loaded date from the new slots const newDates = Object.keys(slotsByDate).map(dateStr => new Date(dateStr)).sort((a, b) => a - b) const lastLoadedDate = newDates[newDates.length - 1] loadMoreButton.dataset.lastLoadedDate = lastLoadedDate.toISOString().split('T')[0] } // Set up event listeners for new time slots this.setupTimeSlotListeners() } // Create HTML for a single day createDayHtml(date, slots, timezone) { const dayName = date.toLocaleDateString('en-US', { weekday: 'long' }) const monthDay = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) let slotsHtml = '' slots.forEach(slot => { const startTime = new Date(slot.start_time) const timeStr = startTime.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, timeZone: timezone }) slotsHtml += ` ` }) return `

${dayName}, ${monthDay}

${slotsHtml}
` } }

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /