mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Show family members on the map
This commit is contained in:
parent
fa3d926a92
commit
f6b32371ec
14 changed files with 1075 additions and 32 deletions
File diff suppressed because one or more lines are too long
30
app/controllers/api/v1/families_controller.rb
Normal file
30
app/controllers/api/v1/families_controller.rb
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::FamiliesController < ApiController
|
||||
before_action :ensure_family_feature_enabled!
|
||||
before_action :ensure_user_in_family!
|
||||
|
||||
def locations
|
||||
family_locations = Families::Locations.new(current_api_user).call
|
||||
|
||||
render json: {
|
||||
locations: family_locations,
|
||||
updated_at: Time.current.iso8601,
|
||||
sharing_enabled: current_api_user.family_sharing_enabled?
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_family_feature_enabled!
|
||||
unless DawarichSettings.family_feature_enabled?
|
||||
render json: { error: 'Family feature is not enabled' }, status: :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_user_in_family!
|
||||
unless current_api_user.in_family?
|
||||
render json: { error: 'User is not part of a family' }, status: :forbidden
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
class FamiliesController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :ensure_family_feature_enabled!
|
||||
before_action :set_family, only: %i[show edit update destroy leave]
|
||||
before_action :set_family, only: %i[show edit update destroy leave update_location_sharing]
|
||||
|
||||
def index
|
||||
redirect_to family_path(current_user.family) if current_user.in_family?
|
||||
|
|
@ -96,8 +96,60 @@ class FamiliesController < ApplicationController
|
|||
alert: 'You cannot leave the family while you are the owner and there are other members. Remove all members first or transfer ownership.'
|
||||
end
|
||||
|
||||
def update_location_sharing
|
||||
# No authorization needed - users can control their own location sharing
|
||||
enabled = ActiveModel::Type::Boolean.new.cast(params[:enabled])
|
||||
duration = params[:duration]
|
||||
|
||||
if current_user.update_family_location_sharing!(enabled, duration: duration)
|
||||
response_data = {
|
||||
success: true,
|
||||
enabled: enabled,
|
||||
duration: current_user.family_sharing_duration,
|
||||
message: build_sharing_message(enabled, duration)
|
||||
}
|
||||
|
||||
# Add expiration info if sharing is time-limited
|
||||
if enabled && current_user.family_sharing_expires_at.present?
|
||||
response_data[:expires_at] = current_user.family_sharing_expires_at.iso8601
|
||||
response_data[:expires_at_formatted] = current_user.family_sharing_expires_at.strftime('%b %d at %I:%M %p')
|
||||
end
|
||||
|
||||
render json: response_data
|
||||
else
|
||||
render json: {
|
||||
success: false,
|
||||
message: 'Failed to update location sharing setting'
|
||||
}, status: :unprocessable_content
|
||||
end
|
||||
rescue => e
|
||||
render json: {
|
||||
success: false,
|
||||
message: 'An error occurred while updating location sharing'
|
||||
}, status: :internal_server_error
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_sharing_message(enabled, duration)
|
||||
return 'Location sharing disabled' unless enabled
|
||||
|
||||
case duration
|
||||
when '1h'
|
||||
'Location sharing enabled for 1 hour'
|
||||
when '6h'
|
||||
'Location sharing enabled for 6 hours'
|
||||
when '12h'
|
||||
'Location sharing enabled for 12 hours'
|
||||
when '24h'
|
||||
'Location sharing enabled for 24 hours'
|
||||
when 'permanent', nil
|
||||
'Location sharing enabled'
|
||||
else
|
||||
duration.to_i > 0 ? "Location sharing enabled for #{duration.to_i} hours" : 'Location sharing enabled'
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_family_feature_enabled!
|
||||
unless DawarichSettings.family_feature_enabled?
|
||||
redirect_to root_path, alert: 'Family feature is not available'
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class MapController < ApplicationController
|
|||
@years = years_range
|
||||
@points_number = points_count
|
||||
@features = DawarichSettings.features
|
||||
@family_member_locations = family_member_locations
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -93,4 +94,8 @@ class MapController < ApplicationController
|
|||
def points_from_user
|
||||
current_user.points.without_raw_data.order(timestamp: :asc)
|
||||
end
|
||||
|
||||
def family_member_locations
|
||||
Families::Locations.new(current_user).call
|
||||
end
|
||||
end
|
||||
|
|
|
|||
320
app/javascript/controllers/family_members_controller.js
Normal file
320
app/javascript/controllers/family_members_controller.js
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
import { Controller } from "@hotwired/stimulus";
|
||||
import L from "leaflet";
|
||||
import { showFlashMessage } from "../maps/helpers";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [];
|
||||
|
||||
static values = {
|
||||
familyMemberLocations: Array,
|
||||
features: Object,
|
||||
userTheme: String
|
||||
}
|
||||
|
||||
connect() {
|
||||
console.log("Family members controller connected");
|
||||
|
||||
// Wait for maps controller to be ready
|
||||
this.waitForMap();
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.cleanup();
|
||||
console.log("Family members controller disconnected");
|
||||
}
|
||||
|
||||
waitForMap() {
|
||||
// Find the maps controller element
|
||||
const mapElement = document.querySelector('[data-controller*="maps"]');
|
||||
if (!mapElement) {
|
||||
console.warn('Maps controller element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for the maps controller to be initialized
|
||||
const checkMapReady = () => {
|
||||
if (window.mapsController && window.mapsController.map) {
|
||||
this.initializeFamilyFeatures();
|
||||
} else {
|
||||
setTimeout(checkMapReady, 100);
|
||||
}
|
||||
};
|
||||
|
||||
checkMapReady();
|
||||
}
|
||||
|
||||
initializeFamilyFeatures() {
|
||||
this.map = window.mapsController.map;
|
||||
|
||||
if (!this.map) {
|
||||
console.warn('Map not available for family members controller');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize family member markers layer
|
||||
this.familyMarkersLayer = L.layerGroup();
|
||||
this.createFamilyMarkers();
|
||||
|
||||
// Add to layer control if available
|
||||
this.addToLayerControl();
|
||||
|
||||
// Add markers to map if layer control integration doesn't work initially
|
||||
if (this.familyMarkersLayer.getLayers().length > 0) {
|
||||
this.familyMarkersLayer.addTo(this.map);
|
||||
}
|
||||
|
||||
// Listen for family data updates
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
createFamilyMarkers() {
|
||||
// Clear existing family markers
|
||||
if (this.familyMarkersLayer) {
|
||||
this.familyMarkersLayer.clearLayers();
|
||||
}
|
||||
|
||||
// Only proceed if family feature is enabled and we have family member locations
|
||||
if (!this.featuresValue.family ||
|
||||
!this.familyMemberLocationsValue ||
|
||||
this.familyMemberLocationsValue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.familyMemberLocationsValue.forEach((location) => {
|
||||
if (!location || !location.latitude || !location.longitude) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the first letter of the email or use '?' as fallback
|
||||
const emailInitial = location.email_initial || location.email?.charAt(0)?.toUpperCase() || '?';
|
||||
|
||||
// Create a distinct marker for family members with email initial
|
||||
const familyMarker = L.marker([location.latitude, location.longitude], {
|
||||
icon: L.divIcon({
|
||||
html: `<div style="background-color: #10B981; color: white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border: 2px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.2); font-size: 14px; font-weight: bold; font-family: system-ui, -apple-system, sans-serif;">${emailInitial}</div>`,
|
||||
iconSize: [24, 24],
|
||||
iconAnchor: [12, 12],
|
||||
className: 'family-member-marker'
|
||||
})
|
||||
});
|
||||
|
||||
// Format timestamp for display
|
||||
const lastSeen = new Date(location.updated_at).toLocaleString();
|
||||
|
||||
// Create popup content with theme-aware styling
|
||||
const popupContent = this.createPopupContent(location, lastSeen);
|
||||
|
||||
familyMarker.bindPopup(popupContent);
|
||||
this.familyMarkersLayer.addLayer(familyMarker);
|
||||
});
|
||||
}
|
||||
|
||||
createPopupContent(location, lastSeen) {
|
||||
const isDark = this.userThemeValue === 'dark';
|
||||
const bgColor = isDark ? '#1f2937' : '#ffffff';
|
||||
const textColor = isDark ? '#f9fafb' : '#111827';
|
||||
const mutedColor = isDark ? '#9ca3af' : '#6b7280';
|
||||
|
||||
const emailInitial = location.email_initial || location.email?.charAt(0)?.toUpperCase() || '?';
|
||||
|
||||
return `
|
||||
<div class="family-member-popup" style="background-color: ${bgColor}; color: ${textColor}; padding: 8px; border-radius: 6px; min-width: 200px;">
|
||||
<h3 style="margin: 0 0 8px 0; color: #10B981; font-size: 14px; font-weight: bold; display: flex; align-items: center; gap: 6px;">
|
||||
<span style="background-color: #10B981; color: white; border-radius: 50%; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: bold;">${emailInitial}</span>
|
||||
Family Member
|
||||
</h3>
|
||||
<p style="margin: 0 0 4px 0; font-size: 12px;">
|
||||
<strong>Email:</strong> ${location.email || 'Unknown'}
|
||||
</p>
|
||||
<p style="margin: 0 0 4px 0; font-size: 12px;">
|
||||
<strong>Location:</strong> ${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}
|
||||
</p>
|
||||
<p style="margin: 0; font-size: 12px; color: ${mutedColor};">
|
||||
<strong>Last seen:</strong> ${lastSeen}
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
addToLayerControl() {
|
||||
// Add family markers layer to the maps controller's layer control
|
||||
if (window.mapsController && window.mapsController.layerControl && this.familyMarkersLayer) {
|
||||
// We need to recreate the layer control to include our new layer
|
||||
this.updateMapsControllerLayerControl();
|
||||
}
|
||||
}
|
||||
|
||||
updateMapsControllerLayerControl() {
|
||||
const mapsController = window.mapsController;
|
||||
if (!mapsController || typeof mapsController.updateLayerControl !== 'function') return;
|
||||
|
||||
// Use the maps controller's helper method to update layer control
|
||||
mapsController.updateLayerControl({
|
||||
"Family Members": this.familyMarkersLayer
|
||||
});
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Listen for family data updates (for real-time updates in the future)
|
||||
document.addEventListener('family:locations:updated', (event) => {
|
||||
this.familyMemberLocationsValue = event.detail.locations;
|
||||
this.createFamilyMarkers();
|
||||
});
|
||||
|
||||
// Listen for theme changes
|
||||
document.addEventListener('theme:changed', (event) => {
|
||||
this.userThemeValue = event.detail.theme;
|
||||
// Recreate popups with new theme
|
||||
this.createFamilyMarkers();
|
||||
});
|
||||
|
||||
// Listen for layer control events
|
||||
this.setupLayerControlEvents();
|
||||
}
|
||||
|
||||
setupLayerControlEvents() {
|
||||
if (!this.map) return;
|
||||
|
||||
// Listen for when the Family Members layer is added
|
||||
this.map.on('overlayadd', (event) => {
|
||||
if (event.name === 'Family Members' && event.layer === this.familyMarkersLayer) {
|
||||
console.log('Family Members layer enabled - refreshing locations');
|
||||
this.refreshFamilyLocations();
|
||||
|
||||
// Set up periodic refresh while layer is active
|
||||
this.startPeriodicRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for when the Family Members layer is removed
|
||||
this.map.on('overlayremove', (event) => {
|
||||
if (event.name === 'Family Members' && event.layer === this.familyMarkersLayer) {
|
||||
// Stop periodic refresh when layer is disabled
|
||||
this.stopPeriodicRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startPeriodicRefresh() {
|
||||
// Clear any existing refresh interval
|
||||
this.stopPeriodicRefresh();
|
||||
|
||||
// Refresh family locations every 30 seconds while layer is active
|
||||
this.refreshInterval = setInterval(() => {
|
||||
if (this.map && this.map.hasLayer(this.familyMarkersLayer)) {
|
||||
this.refreshFamilyLocations();
|
||||
} else {
|
||||
// Layer is no longer active, stop refreshing
|
||||
this.stopPeriodicRefresh();
|
||||
}
|
||||
}, 30000); // 30 seconds
|
||||
}
|
||||
|
||||
stopPeriodicRefresh() {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
this.refreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to manually update family member locations (for API calls)
|
||||
updateFamilyLocations(locations) {
|
||||
this.familyMemberLocationsValue = locations;
|
||||
this.createFamilyMarkers();
|
||||
|
||||
// Dispatch event for other controllers that might be interested
|
||||
document.dispatchEvent(new CustomEvent('family:locations:updated', {
|
||||
detail: { locations: locations }
|
||||
}));
|
||||
}
|
||||
|
||||
// Method to refresh family locations from API
|
||||
async refreshFamilyLocations() {
|
||||
if (!window.mapsController?.apiKey) {
|
||||
console.warn('API key not available for family locations refresh');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/families/locations?api_key=${window.mapsController.apiKey}`, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
console.warn('Family feature not enabled or user not in family');
|
||||
return;
|
||||
}
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.updateFamilyLocations(data.locations || []);
|
||||
|
||||
// Show user feedback if this was a manual refresh
|
||||
if (this.showUserFeedback) {
|
||||
const count = data.locations?.length || 0;
|
||||
this.showFlashMessageToUser('notice', `Family locations updated (${count} members)`);
|
||||
this.showUserFeedback = false; // Reset flag
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error refreshing family locations:', error);
|
||||
|
||||
// Show error to user if this was a manual refresh
|
||||
if (this.showUserFeedback) {
|
||||
this.showFlashMessageToUser('error', 'Failed to refresh family locations');
|
||||
this.showUserFeedback = false; // Reset flag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to show flash messages using the imported helper
|
||||
showFlashMessageToUser(type, message) {
|
||||
showFlashMessage(type, message);
|
||||
}
|
||||
|
||||
// Method for manual refresh with user feedback
|
||||
async manualRefreshFamilyLocations() {
|
||||
this.showUserFeedback = true; // Enable user feedback for this refresh
|
||||
await this.refreshFamilyLocations();
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
// Stop periodic refresh
|
||||
this.stopPeriodicRefresh();
|
||||
|
||||
// Remove family markers layer from map if it exists
|
||||
if (this.familyMarkersLayer && this.map && this.map.hasLayer(this.familyMarkersLayer)) {
|
||||
this.map.removeLayer(this.familyMarkersLayer);
|
||||
}
|
||||
|
||||
// Remove map event listeners
|
||||
if (this.map) {
|
||||
this.map.off('overlayadd');
|
||||
this.map.off('overlayremove');
|
||||
}
|
||||
|
||||
// Remove document event listeners
|
||||
document.removeEventListener('family:locations:updated', this.handleLocationUpdates);
|
||||
document.removeEventListener('theme:changed', this.handleThemeChange);
|
||||
}
|
||||
|
||||
// Expose layer for external access
|
||||
getFamilyMarkersLayer() {
|
||||
return this.familyMarkersLayer;
|
||||
}
|
||||
|
||||
// Check if family features are enabled
|
||||
isFamilyFeatureEnabled() {
|
||||
return this.featuresValue.family === true;
|
||||
}
|
||||
|
||||
// Get family marker count
|
||||
getFamilyMemberCount() {
|
||||
return this.familyMemberLocationsValue ? this.familyMemberLocationsValue.length : 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["indicator"];
|
||||
static values = {
|
||||
enabled: Boolean
|
||||
};
|
||||
|
||||
connect() {
|
||||
console.log("Family navbar indicator controller connected");
|
||||
this.updateIndicator();
|
||||
|
||||
// Listen for location sharing updates
|
||||
document.addEventListener('location-sharing:updated', this.handleSharingUpdate.bind(this));
|
||||
document.addEventListener('location-sharing:expired', this.handleSharingExpired.bind(this));
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
document.removeEventListener('location-sharing:updated', this.handleSharingUpdate.bind(this));
|
||||
document.removeEventListener('location-sharing:expired', this.handleSharingExpired.bind(this));
|
||||
}
|
||||
|
||||
handleSharingUpdate(event) {
|
||||
// Only update if this is the current user's sharing change
|
||||
// (we're only showing the current user's status in navbar)
|
||||
this.enabledValue = event.detail.enabled;
|
||||
this.updateIndicator();
|
||||
}
|
||||
|
||||
handleSharingExpired(event) {
|
||||
this.enabledValue = false;
|
||||
this.updateIndicator();
|
||||
}
|
||||
|
||||
updateIndicator() {
|
||||
if (!this.hasIndicatorTarget) return;
|
||||
|
||||
if (this.enabledValue) {
|
||||
// Green pulsing indicator for enabled
|
||||
this.indicatorTarget.className = "w-2 h-2 bg-green-500 rounded-full animate-pulse";
|
||||
this.indicatorTarget.title = "Location sharing enabled";
|
||||
} else {
|
||||
// Gray indicator for disabled
|
||||
this.indicatorTarget.className = "w-2 h-2 bg-gray-400 rounded-full";
|
||||
this.indicatorTarget.title = "Location sharing disabled";
|
||||
}
|
||||
}
|
||||
}
|
||||
276
app/javascript/controllers/location_sharing_toggle_controller.js
Normal file
276
app/javascript/controllers/location_sharing_toggle_controller.js
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
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(`/families/${this.familyIdValue}/update_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;
|
||||
}
|
||||
}
|
||||
|
|
@ -207,6 +207,9 @@ export default class extends BaseController {
|
|||
// Expose visits manager globally for location search integration
|
||||
window.visitsManager = this.visitsManager;
|
||||
|
||||
// Expose maps controller globally for family integration
|
||||
window.mapsController = this;
|
||||
|
||||
// Initialize layers for the layer control
|
||||
const controlsLayer = {
|
||||
Points: this.markersLayer,
|
||||
|
|
@ -1841,4 +1844,77 @@ export default class extends BaseController {
|
|||
this.locationSearch = new LocationSearch(this.map, this.apiKey, this.userTheme);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method for family controller to update layer control
|
||||
updateLayerControl(additionalLayers = {}) {
|
||||
if (!this.layerControl) return;
|
||||
|
||||
// Store which base and overlay layers are currently visible
|
||||
const overlayStates = {};
|
||||
let activeBaseLayer = null;
|
||||
let activeBaseLayerName = null;
|
||||
|
||||
if (this.layerControl._layers) {
|
||||
Object.values(this.layerControl._layers).forEach(layerObj => {
|
||||
if (layerObj.overlay && layerObj.layer) {
|
||||
// Store overlay layer states
|
||||
overlayStates[layerObj.name] = this.map.hasLayer(layerObj.layer);
|
||||
} else if (!layerObj.overlay && this.map.hasLayer(layerObj.layer)) {
|
||||
// Store the currently active base layer
|
||||
activeBaseLayer = layerObj.layer;
|
||||
activeBaseLayerName = layerObj.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove existing layer control
|
||||
this.map.removeControl(this.layerControl);
|
||||
|
||||
// Create base controls layer object
|
||||
const baseControlsLayer = {
|
||||
Points: this.markersLayer || L.layerGroup(),
|
||||
Routes: this.polylinesLayer || L.layerGroup(),
|
||||
Tracks: this.tracksLayer || L.layerGroup(),
|
||||
Heatmap: this.heatmapLayer || L.heatLayer([]),
|
||||
"Fog of War": this.fogOverlay,
|
||||
"Scratch map": this.scratchLayerManager?.getLayer() || L.layerGroup(),
|
||||
Areas: this.areasLayer || L.layerGroup(),
|
||||
Photos: this.photoMarkers || L.layerGroup(),
|
||||
"Suggested Visits": this.visitsManager?.getVisitCirclesLayer() || L.layerGroup(),
|
||||
"Confirmed Visits": this.visitsManager?.getConfirmedVisitCirclesLayer() || L.layerGroup()
|
||||
};
|
||||
|
||||
// Merge with additional layers (like family members)
|
||||
const controlsLayer = { ...baseControlsLayer, ...additionalLayers };
|
||||
|
||||
// Get base maps and re-add the layer control
|
||||
const baseMaps = this.baseMaps();
|
||||
this.layerControl = L.control.layers(baseMaps, controlsLayer).addTo(this.map);
|
||||
|
||||
// Restore the active base layer if we had one
|
||||
if (activeBaseLayer && activeBaseLayerName) {
|
||||
console.log(`Restoring base layer: ${activeBaseLayerName}`);
|
||||
// Make sure the base layer is added to the map
|
||||
if (!this.map.hasLayer(activeBaseLayer)) {
|
||||
activeBaseLayer.addTo(this.map);
|
||||
}
|
||||
} else {
|
||||
// If no active base layer was found, ensure we have a default one
|
||||
console.log('No active base layer found, adding default');
|
||||
const defaultBaseLayer = Object.values(baseMaps)[0];
|
||||
if (defaultBaseLayer && !this.map.hasLayer(defaultBaseLayer)) {
|
||||
defaultBaseLayer.addTo(this.map);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore overlay layer visibility states
|
||||
Object.entries(overlayStates).forEach(([name, wasVisible]) => {
|
||||
const layer = controlsLayer[name];
|
||||
if (layer && wasVisible && !this.map.hasLayer(layer)) {
|
||||
layer.addTo(this.map);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
|
|||
devise :database_authenticatable, :registerable,
|
||||
:recoverable, :rememberable, :validatable, :trackable
|
||||
|
||||
has_many :points, dependent: :destroy, counter_cache: true
|
||||
has_many :points, dependent: :destroy
|
||||
has_many :imports, dependent: :destroy
|
||||
has_many :stats, dependent: :destroy
|
||||
has_many :exports, dependent: :destroy
|
||||
|
|
@ -217,13 +217,89 @@ inverse_of: :invited_by, dependent: :destroy
|
|||
end
|
||||
|
||||
def family_sharing_enabled?
|
||||
in_family?
|
||||
# User must be in a family and have explicitly enabled location sharing
|
||||
return false unless in_family?
|
||||
|
||||
sharing_settings = settings.dig('family', 'location_sharing')
|
||||
return false if sharing_settings.blank?
|
||||
|
||||
# If it's a boolean (legacy support), return it
|
||||
return sharing_settings if [true, false].include?(sharing_settings)
|
||||
|
||||
# If it's time-limited sharing, check if it's still active
|
||||
if sharing_settings.is_a?(Hash)
|
||||
return false unless sharing_settings['enabled'] == true
|
||||
|
||||
# Check if sharing has an expiration
|
||||
expires_at = sharing_settings['expires_at']
|
||||
return expires_at.blank? || Time.parse(expires_at) > Time.current
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def update_family_location_sharing!(enabled, duration: nil)
|
||||
return false unless in_family?
|
||||
|
||||
current_settings = settings || {}
|
||||
current_settings['family'] ||= {}
|
||||
|
||||
if enabled
|
||||
sharing_config = { 'enabled' => true }
|
||||
|
||||
# Add expiration if duration is specified
|
||||
if duration.present?
|
||||
expiration_time = case duration
|
||||
when '1h'
|
||||
1.hour.from_now
|
||||
when '6h'
|
||||
6.hours.from_now
|
||||
when '12h'
|
||||
12.hours.from_now
|
||||
when '24h'
|
||||
24.hours.from_now
|
||||
when 'permanent'
|
||||
nil # No expiration
|
||||
else
|
||||
# Custom duration in hours
|
||||
duration.to_i.hours.from_now if duration.to_i > 0
|
||||
end
|
||||
|
||||
sharing_config['expires_at'] = expiration_time.iso8601 if expiration_time
|
||||
sharing_config['duration'] = duration
|
||||
end
|
||||
|
||||
current_settings['family']['location_sharing'] = sharing_config
|
||||
else
|
||||
current_settings['family']['location_sharing'] = { 'enabled' => false }
|
||||
end
|
||||
|
||||
update!(settings: current_settings)
|
||||
end
|
||||
|
||||
def family_sharing_expires_at
|
||||
sharing_settings = settings.dig('family', 'location_sharing')
|
||||
return nil unless sharing_settings.is_a?(Hash)
|
||||
|
||||
expires_at = sharing_settings['expires_at']
|
||||
Time.parse(expires_at) if expires_at.present?
|
||||
rescue ArgumentError
|
||||
nil
|
||||
end
|
||||
|
||||
def family_sharing_duration
|
||||
settings.dig('family', 'location_sharing', 'duration') || 'permanent'
|
||||
end
|
||||
|
||||
def latest_location_for_family
|
||||
return nil unless family_sharing_enabled?
|
||||
|
||||
latest_point = points.order(timestamp: :desc).first
|
||||
# Use select to only fetch needed columns and limit to 1 for efficiency
|
||||
latest_point = points.select(:latitude, :longitude, :timestamp)
|
||||
.order(timestamp: :desc)
|
||||
.limit(1)
|
||||
.first
|
||||
|
||||
return nil unless latest_point
|
||||
|
||||
{
|
||||
|
|
|
|||
49
app/services/families/locations.rb
Normal file
49
app/services/families/locations.rb
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Families::Locations
|
||||
attr_reader :user
|
||||
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
def call
|
||||
return [] unless family_feature_enabled?
|
||||
return [] unless user.in_family?
|
||||
|
||||
sharing_members = family_members_with_sharing_enabled
|
||||
return [] unless sharing_members.any?
|
||||
|
||||
build_family_locations(sharing_members)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def family_feature_enabled?
|
||||
DawarichSettings.family_feature_enabled?
|
||||
end
|
||||
|
||||
def family_members_with_sharing_enabled
|
||||
user.family.members
|
||||
.where.not(id: user.id)
|
||||
.select(&:family_sharing_enabled?)
|
||||
end
|
||||
|
||||
def build_family_locations(sharing_members)
|
||||
latest_points = sharing_members.map { |member| member.points.last }.compact
|
||||
|
||||
latest_points.map do |point|
|
||||
next unless point
|
||||
|
||||
{
|
||||
user_id: point.user_id,
|
||||
email: point.user.email,
|
||||
email_initial: point.user.email.first.upcase,
|
||||
latitude: point.lat.to_f,
|
||||
longitude: point.lon.to_f,
|
||||
timestamp: point.timestamp.to_i,
|
||||
updated_at: Time.at(point.timestamp.to_i)
|
||||
}
|
||||
end.compact
|
||||
end
|
||||
end
|
||||
|
|
@ -42,9 +42,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Family Members -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg p-6">
|
||||
<!-- Family Members -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg p-6 mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
<%= t('families.show.members_title', default: 'Family Members') %>
|
||||
|
|
@ -58,29 +57,116 @@
|
|||
|
||||
<div class="space-y-3">
|
||||
<% @members.each do |member| %>
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<div>
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100"><%= member.email %></div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
<%= member.family_membership.role.humanize %>
|
||||
<% if member.family_membership.role == 'owner' %>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 ml-2">
|
||||
<%= t('families.show.owner_badge', default: 'Owner') %>
|
||||
</span>
|
||||
<% end %>
|
||||
<!-- DaisyUI Card for each member -->
|
||||
<div class="card bg-base-200 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-grow">
|
||||
<!-- Member Info -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-primary text-primary-content rounded-full w-12">
|
||||
<span class="text-lg font-semibold">
|
||||
<%= member.email&.first&.upcase || '?' %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="card-title text-base"><%= member.email %></h3>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<% if member.family_membership.role == 'owner' %>
|
||||
<div class="badge badge-warning badge-sm">
|
||||
<%= t('families.show.owner_badge', default: 'Owner') %>
|
||||
</div>
|
||||
<% else %>
|
||||
<span class="text-sm opacity-60">
|
||||
<%= member.family_membership.role.humanize %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="text-xs opacity-50 mt-1">
|
||||
<%= t('families.show.joined_on', default: 'Joined') %>
|
||||
<%= member.family_membership.created_at.strftime('%b %d, %Y') %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location Sharing Controls - More Compact -->
|
||||
<div class="ml-auto flex items-center gap-4">
|
||||
<% if member == current_user %>
|
||||
<!-- Own toggle - interactive (consolidated controller) -->
|
||||
<div data-controller="location-sharing-toggle"
|
||||
data-location-sharing-toggle-member-id-value="<%= member.id %>"
|
||||
data-location-sharing-toggle-enabled-value="<%= member.family_sharing_enabled? %>"
|
||||
data-location-sharing-toggle-family-id-value="<%= @family.id %>"
|
||||
data-location-sharing-toggle-duration-value="<%= member.family_sharing_duration %>"
|
||||
data-location-sharing-toggle-expires-at-value="<%= member.family_sharing_expires_at&.iso8601 %>"
|
||||
class="flex items-center gap-3">
|
||||
|
||||
<span class="text-sm opacity-60">Location:</span>
|
||||
|
||||
<!-- Toggle Switch -->
|
||||
<input type="checkbox"
|
||||
class="toggle toggle-primary toggle-sm"
|
||||
<%= 'checked' if member.family_sharing_enabled? %>
|
||||
data-location-sharing-toggle-target="checkbox"
|
||||
data-action="change->location-sharing-toggle#toggle">
|
||||
|
||||
<!-- Duration Dropdown (only visible when enabled) -->
|
||||
<div class="<%= 'hidden' unless member.family_sharing_enabled? %>"
|
||||
data-location-sharing-toggle-target="durationContainer">
|
||||
<select class="select select-bordered select-xs w-28 h-full"
|
||||
data-location-sharing-toggle-target="durationSelect"
|
||||
data-action="change->location-sharing-toggle#changeDuration">
|
||||
<option value="permanent" <%= 'selected' if member.family_sharing_duration == 'permanent' %>>Always</option>
|
||||
<option value="1h" <%= 'selected' if member.family_sharing_duration == '1h' %>>1 hour</option>
|
||||
<option value="6h" <%= 'selected' if member.family_sharing_duration == '6h' %>>6 hours</option>
|
||||
<option value="12h" <%= 'selected' if member.family_sharing_duration == '12h' %>>12 hours</option>
|
||||
<option value="24h" <%= 'selected' if member.family_sharing_duration == '24h' %>>24 hours</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Expiration Info (inline) -->
|
||||
<% if member.family_sharing_enabled? && member.family_sharing_expires_at.present? %>
|
||||
<div class="text-xs opacity-50"
|
||||
data-location-sharing-toggle-target="expirationInfo">
|
||||
• Expires <%= time_ago_in_words(member.family_sharing_expires_at) %> from now
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% else %>
|
||||
<!-- Other member's status - read-only indicator -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm opacity-60">Location:</span>
|
||||
<% if member.family_sharing_enabled? %>
|
||||
<div class="w-3 h-3 bg-success rounded-full animate-pulse"></div>
|
||||
<span class="text-xs text-success font-medium">
|
||||
<%= member.family_sharing_duration == 'permanent' ? 'Always' : member.family_sharing_duration&.upcase %>
|
||||
</span>
|
||||
<% if member.family_sharing_expires_at.present? %>
|
||||
<span class="text-xs opacity-50">
|
||||
• Expires <%= time_ago_in_words(member.family_sharing_expires_at) %> from now
|
||||
</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="w-3 h-3 bg-base-300 rounded-full"></div>
|
||||
<span class="text-xs opacity-50">Disabled</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
<%= t('families.show.joined_on', default: 'Joined') %>
|
||||
<%= member.family_membership.created_at.strftime('%b %d, %Y') %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pending Invitations -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg p-6">
|
||||
<!-- Pending Invitations -->
|
||||
<div class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700 rounded-lg p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
<%= t('families.show.invitations_title', default: 'Pending Invitations') %>
|
||||
|
|
@ -166,7 +252,6 @@
|
|||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@
|
|||
<div
|
||||
id='map'
|
||||
class="w-full z-0"
|
||||
data-controller="maps points add-visit"
|
||||
data-controller="maps points add-visit family-members"
|
||||
data-points-target="map"
|
||||
data-api_key="<%= current_user.api_key %>"
|
||||
data-self_hosted="<%= @self_hosted %>"
|
||||
|
|
@ -74,7 +74,10 @@
|
|||
data-distance="<%= @distance %>"
|
||||
data-points_number="<%= @points_number %>"
|
||||
data-timezone="<%= Rails.configuration.time_zone %>"
|
||||
data-features='<%= @features.to_json.html_safe %>'>
|
||||
data-features='<%= @features.to_json.html_safe %>'
|
||||
data-family-members-family-member-locations-value='<%= @family_member_locations.to_json.html_safe %>'
|
||||
data-family-members-features-value='<%= @features.to_json.html_safe %>'
|
||||
data-family-members-user-theme-value="<%= current_user&.theme || 'dark' %>">
|
||||
<div data-maps-target="container" class="h-[25rem] rounded-lg w-full min-h-screen z-0">
|
||||
<div id="fog" class="fog"></div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,15 @@
|
|||
<% if user_signed_in? && DawarichSettings.family_feature_enabled? %>
|
||||
<li>
|
||||
<% if current_user.in_family? %>
|
||||
<%= link_to 'Family', family_path(current_user.family), class: "#{active_class?(families_path)}" %>
|
||||
<div data-controller="family-navbar-indicator"
|
||||
data-family-navbar-indicator-enabled-value="<%= current_user.family_sharing_enabled? %>">
|
||||
<%= link_to family_path(current_user.family), class: "#{active_class?(families_path)} flex items-center space-x-2" do %>
|
||||
<span>Family</span>
|
||||
<div data-family-navbar-indicator-target="indicator"
|
||||
class="w-2 h-2 <%= current_user.family_sharing_enabled? ? 'bg-green-500 animate-pulse' : 'bg-gray-400' %> rounded-full"
|
||||
title="<%= current_user.family_sharing_enabled? ? 'Location sharing enabled' : 'Location sharing disabled' %>"></div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= link_to 'Family', families_path, class: "#{active_class?(families_path)}" %>
|
||||
<% end %>
|
||||
|
|
@ -68,7 +76,15 @@
|
|||
<% if user_signed_in? && DawarichSettings.family_feature_enabled? %>
|
||||
<li>
|
||||
<% if current_user.in_family? %>
|
||||
<%= link_to 'Family', family_path(current_user.family), class: "mx-1 #{active_class?(families_path)}" %>
|
||||
<div data-controller="family-navbar-indicator"
|
||||
data-family-navbar-indicator-enabled-value="<%= current_user.family_sharing_enabled? %>">
|
||||
<%= link_to family_path(current_user.family), class: "mx-1 #{active_class?(families_path)} flex items-center space-x-2" do %>
|
||||
<span>Family</span>
|
||||
<div data-family-navbar-indicator-target="indicator"
|
||||
class="w-2 h-2 <%= current_user.family_sharing_enabled? ? 'bg-green-500 animate-pulse' : 'bg-gray-400' %> rounded-full"
|
||||
title="<%= current_user.family_sharing_enabled? ? 'Location sharing enabled' : 'Location sharing disabled' %>"></div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= link_to 'Family', families_path, class: "mx-1 #{active_class?(families_path)}" %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ Rails.application.routes.draw do
|
|||
resources :families do
|
||||
member do
|
||||
delete :leave
|
||||
patch :update_location_sharing
|
||||
end
|
||||
resources :invitations, except: %i[edit update], controller: 'family_invitations' do
|
||||
member do
|
||||
|
|
@ -170,6 +171,12 @@ Rails.application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
resources :families, only: [] do
|
||||
collection do
|
||||
get :locations
|
||||
end
|
||||
end
|
||||
|
||||
post 'subscriptions/callback', to: 'subscriptions#callback'
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue