import { Controller } from "@hotwired/stimulus"; export default class extends Controller { static targets = ["checkbox", "durationContainer", "durationSelect", "expirationInfo"]; static values = { memberId: Number, enabled: Boolean, familyId: Number, duration: String, expiresAt: String }; connect() { console.log("Location sharing toggle controller connected"); this.updateToggleState(); this.setupExpirationTimer(); } disconnect() { this.clearExpirationTimer(); } toggle() { const newState = !this.enabledValue; const duration = this.hasDurationSelectTarget ? this.durationSelectTarget.value : 'permanent'; // Optimistically update UI this.enabledValue = newState; this.updateToggleState(); // Send the update to server this.updateLocationSharing(newState, duration); } changeDuration() { if (!this.enabledValue) return; // Only allow duration changes when sharing is enabled const duration = this.durationSelectTarget.value; this.durationValue = duration; // Update sharing with new duration this.updateLocationSharing(true, duration); } updateToggleState() { const isEnabled = this.enabledValue; // Update checkbox (DaisyUI toggle) this.checkboxTarget.checked = isEnabled; // Show/hide duration container if (this.hasDurationContainerTarget) { if (isEnabled) { this.durationContainerTarget.classList.remove('hidden'); } else { this.durationContainerTarget.classList.add('hidden'); } } } async updateLocationSharing(enabled, duration = 'permanent') { try { const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); const response = await fetch(`/family/location_sharing`, { method: 'PATCH', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ enabled: enabled, duration: duration }) }); const data = await response.json(); if (data.success) { // Update local values from server response this.durationValue = data.duration; this.expiresAtValue = data.expires_at; // Update duration select if it exists if (this.hasDurationSelectTarget) { this.durationSelectTarget.value = data.duration; } // Update expiration info this.updateExpirationInfo(data.expires_at_formatted); // Show success message this.showFlashMessage('success', data.message); // Setup/clear expiration timer this.setupExpirationTimer(); // Trigger custom event for other controllers to listen to document.dispatchEvent(new CustomEvent('location-sharing:updated', { detail: { userId: this.memberIdValue, enabled: enabled, duration: data.duration, expiresAt: data.expires_at } })); } else { // Revert the UI change if server update failed this.enabledValue = !enabled; this.updateToggleState(); this.showFlashMessage('error', data.message || 'Failed to update location sharing'); } } catch (error) { console.error('Error updating location sharing:', error); // Revert the UI change if request failed this.enabledValue = !enabled; this.updateToggleState(); this.showFlashMessage('error', 'Network error occurred while updating location sharing'); } } setupExpirationTimer() { this.clearExpirationTimer(); if (this.enabledValue && this.expiresAtValue) { const expiresAt = new Date(this.expiresAtValue); const now = new Date(); const msUntilExpiration = expiresAt.getTime() - now.getTime(); if (msUntilExpiration > 0) { // Set timer to automatically disable sharing when it expires this.expirationTimer = setTimeout(() => { this.enabledValue = false; this.updateToggleState(); this.showFlashMessage('info', 'Location sharing has expired'); // Trigger update event document.dispatchEvent(new CustomEvent('location-sharing:expired', { detail: { userId: this.memberIdValue } })); }, msUntilExpiration); // Also set up periodic updates to show countdown this.updateExpirationCountdown(); this.countdownInterval = setInterval(() => { this.updateExpirationCountdown(); }, 60000); // Update every minute } } } clearExpirationTimer() { if (this.expirationTimer) { clearTimeout(this.expirationTimer); this.expirationTimer = null; } if (this.countdownInterval) { clearInterval(this.countdownInterval); this.countdownInterval = null; } } updateExpirationInfo(formattedTime) { if (this.hasExpirationInfoTarget && formattedTime) { this.expirationInfoTarget.textContent = `Expires ${formattedTime}`; this.expirationInfoTarget.style.display = 'block'; } else if (this.hasExpirationInfoTarget) { this.expirationInfoTarget.style.display = 'none'; } } updateExpirationCountdown() { if (!this.hasExpirationInfoTarget || !this.expiresAtValue) return; const expiresAt = new Date(this.expiresAtValue); const now = new Date(); const msUntilExpiration = expiresAt.getTime() - now.getTime(); if (msUntilExpiration <= 0) { this.expirationInfoTarget.textContent = 'Expired'; this.expirationInfoTarget.style.display = 'block'; return; } const hoursLeft = Math.floor(msUntilExpiration / (1000 * 60 * 60)); const minutesLeft = Math.floor((msUntilExpiration % (1000 * 60 * 60)) / (1000 * 60)); let timeText; if (hoursLeft > 0) { timeText = `${hoursLeft}h ${minutesLeft}m remaining`; } else { timeText = `${minutesLeft}m remaining`; } this.expirationInfoTarget.textContent = `Expires in ${timeText}`; } showFlashMessage(type, message) { // Create a flash message element matching the project style (_flash.html.erb) const flashContainer = document.getElementById('flash-messages') || this.createFlashContainer(); const bgClass = this.getFlashClasses(type); const flashElement = document.createElement('div'); flashElement.className = `flex items-center ${bgClass} py-3 px-5 rounded-lg z-[6000]`; flashElement.innerHTML = `
${message}
`; // Add click handler to dismiss button const dismissButton = flashElement.querySelector('button'); dismissButton.addEventListener('click', () => { flashElement.classList.add('fade-out'); setTimeout(() => { flashElement.remove(); // Remove the container if it's empty if (flashContainer && !flashContainer.hasChildNodes()) { flashContainer.remove(); } }, 150); }); flashContainer.appendChild(flashElement); // Auto-remove after 5 seconds setTimeout(() => { if (flashElement.parentNode) { flashElement.classList.add('fade-out'); setTimeout(() => { flashElement.remove(); // Remove the container if it's empty if (flashContainer && !flashContainer.hasChildNodes()) { flashContainer.remove(); } }, 150); } }, 5000); } createFlashContainer() { const container = document.createElement('div'); container.id = 'flash-messages'; container.className = 'fixed top-5 right-5 flex flex-col gap-2 z-50'; document.body.appendChild(container); return container; } getFlashClasses(type) { switch (type) { case 'error': case 'alert': return 'bg-red-100 text-red-700 border-red-300'; default: return 'bg-blue-100 text-blue-700 border-blue-300'; } } // Helper method to check if user's own location sharing is enabled // This can be used by other controllers static getUserLocationSharingStatus() { const toggleController = document.querySelector('[data-controller*="location-sharing-toggle"]'); if (toggleController) { const controller = this.application.getControllerForElementAndIdentifier(toggleController, 'location-sharing-toggle'); return controller?.enabledValue || false; } return false; } }