mirror of
https://github.com/Freika/dawarich.git
synced 2025-12-21 04:49:46 -06:00
* Implement OmniAuth GitHub authentication * Fix omniauth GitHub scope to include user email access * Remove margin-bottom * Implement Google OAuth2 authentication * Implement OIDC authentication for Dawarich using omniauth_openid_connect gem. * Add patreon account linking and patron checking service * Update docker-compose.yml to use boolean values instead of strings * Add support for KML files * Add tests * Update changelog * Remove patreon OAuth integration * Move omniauthable to a concern * Update an icon in integrations * Update changelog * Update app version * Fix family location sharing toggle * Move family location sharing to its own controller * Update changelog * Implement basic tagging functionality for places, allowing users to categorize and label places with custom tags. * Add places management API and tags feature * Add some changes related to places management feature * Fix some tests * Fix sometests * Add places layer * Update places layer to use Leaflet.Control.Layers.Tree for hierarchical layer control * Rework tag form * Add hashtag * Add privacy zones to tags * Add notes to places and manage place tags * Update changelog * Update e2e tests * Extract tag serializer to its own file * Fix some tests * Fix tags request specs * Fix some tests * Fix rest of the tests * Revert some changes * Add missing specs * Revert changes in place export/import code * Fix some specs * Fix PlaceFinder to only consider global places when finding existing places * Fix few more specs * Fix visits creator spec * Fix last tests * Update place creating modal * Add home location based on "Home" tagged place * Save enabled tag layers * Some fixes * Fix bug where enabling place tag layers would trigger saving enabled layers, overwriting with incomplete data * Update migration to use disable_ddl_transaction! and add up/down methods * Fix tag layers restoration and filtering logic * Update OIDC auto-registration and email/password registration settings * Fix potential xss
277 lines
8.6 KiB
JavaScript
277 lines
8.6 KiB
JavaScript
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 = `
|
|
<div class="mr-4">${message}</div>
|
|
<button type="button">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
`;
|
|
|
|
// 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;
|
|
}
|
|
}
|