mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
commit
07216e00dd
27 changed files with 830 additions and 381 deletions
|
|
@ -17,6 +17,7 @@ In this release we're introducing family features that allow users to create fam
|
|||
## Fixed
|
||||
|
||||
- Sign out button works again. #1844
|
||||
- Fixed user deletion bug where user could not be deleted due to counter cache on points.
|
||||
- Users always have default distance unit set to kilometers. #1832
|
||||
|
||||
## Changed
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -34,6 +34,7 @@
|
|||
color: var(--leaflet-text-color) !important;
|
||||
border-color: var(--leaflet-border-color) !important;
|
||||
box-shadow: 0 1px 4px var(--leaflet-shadow-color) !important;
|
||||
|
||||
}
|
||||
|
||||
/* Leaflet zoom buttons */
|
||||
|
|
@ -51,6 +52,32 @@
|
|||
.leaflet-control-layers-toggle {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
/* Replace default icon with custom SVG */
|
||||
background-image: none !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-toggle::before {
|
||||
content: '' !important;
|
||||
display: block !important;
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"/><path d="M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12"/><path d="M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17"/></svg>') !important;
|
||||
background-size: contain !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: center !important;
|
||||
}
|
||||
|
||||
/* Dark theme - use white stroke for the icon */
|
||||
[data-theme="dark"] .leaflet-control-layers-toggle::before {
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"/><path d="M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12"/><path d="M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17"/></svg>') !important;
|
||||
}
|
||||
|
||||
/* Light theme - use black stroke for the icon */
|
||||
[data-theme="light"] .leaflet-control-layers-toggle::before {
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"/><path d="M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12"/><path d="M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17"/></svg>') !important;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-expanded {
|
||||
|
|
@ -186,4 +213,24 @@
|
|||
.family-member-marker-recent .leaflet-marker-icon > div {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2), 0 0 0 0 rgba(16, 185, 129, 0.7);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fix bottom controls being cut off */
|
||||
.leaflet-bottom {
|
||||
padding-bottom: 10px !important;
|
||||
transition: padding-bottom 0.3s ease;
|
||||
}
|
||||
|
||||
.leaflet-bottom.leaflet-left {
|
||||
padding-left: 10px !important;
|
||||
}
|
||||
|
||||
.leaflet-bottom.leaflet-right {
|
||||
padding-right: 10px !important;
|
||||
}
|
||||
|
||||
/* DaisyUI tooltips on map buttons - ensure they appear above date navigation (z-index: 9999) */
|
||||
.tooltip:before,
|
||||
.tooltip:after {
|
||||
z-index: 10000 !important;
|
||||
}
|
||||
|
|
|
|||
1
app/assets/svg/icons/lucide/outline/chevron-down.svg
Normal file
1
app/assets/svg/icons/lucide/outline/chevron-down.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down-icon lucide-chevron-down"><path d="m6 9 6 6 6-6"/></svg>
|
||||
|
After Width: | Height: | Size: 272 B |
1
app/assets/svg/icons/lucide/outline/chevron-up.svg
Normal file
1
app/assets/svg/icons/lucide/outline/chevron-up.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-up-icon lucide-chevron-up"><path d="m18 15-6-6-6 6"/></svg>
|
||||
|
After Width: | Height: | Size: 270 B |
1
app/assets/svg/icons/lucide/outline/search.svg
Normal file
1
app/assets/svg/icons/lucide/outline/search.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
|
||||
|
After Width: | Height: | Size: 295 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-dashed-mouse-pointer-icon lucide-square-dashed-mouse-pointer"><path d="M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"/><path d="M5 3a2 2 0 0 0-2 2"/><path d="M19 3a2 2 0 0 1 2 2"/><path d="M5 21a2 2 0 0 1-2-2"/><path d="M9 3h1"/><path d="M9 21h2"/><path d="M14 3h1"/><path d="M3 9v1"/><path d="M21 9v2"/><path d="M3 14v1"/></svg>
|
||||
|
After Width: | Height: | Size: 623 B |
1
app/assets/svg/icons/lucide/outline/triangle-alert.svg
Normal file
1
app/assets/svg/icons/lucide/outline/triangle-alert.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert-icon lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
|
||||
|
After Width: | Height: | Size: 377 B |
|
|
@ -1,7 +1,10 @@
|
|||
import { Controller } from "@hotwired/stimulus";
|
||||
import L from "leaflet";
|
||||
import { showFlashMessage } from "../maps/helpers";
|
||||
import { applyThemeToButton } from "../maps/theme_utils";
|
||||
import {
|
||||
setAddVisitButtonActive,
|
||||
setAddVisitButtonInactive
|
||||
} from "../maps/map_controls";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [""];
|
||||
|
|
@ -71,39 +74,26 @@ export default class extends Controller {
|
|||
setupAddVisitButton() {
|
||||
if (!this.map || this.addVisitButton) return;
|
||||
|
||||
// Create the Add Visit control
|
||||
const AddVisitControl = L.Control.extend({
|
||||
onAdd: (map) => {
|
||||
const button = L.DomUtil.create('button', 'leaflet-control-button add-visit-button');
|
||||
button.innerHTML = '➕';
|
||||
button.title = 'Add a visit';
|
||||
// The Add Visit button is now created centrally by maps_controller.js
|
||||
// via addTopRightButtons(). We just need to find it and attach our handler.
|
||||
setTimeout(() => {
|
||||
this.addVisitButton = document.querySelector('.add-visit-button');
|
||||
|
||||
// Style the button with theme-aware styling
|
||||
applyThemeToButton(button, this.userThemeValue || 'dark');
|
||||
button.style.width = '48px';
|
||||
button.style.height = '48px';
|
||||
button.style.borderRadius = '4px';
|
||||
button.style.padding = '0';
|
||||
button.style.lineHeight = '48px';
|
||||
button.style.fontSize = '18px';
|
||||
button.style.textAlign = 'center';
|
||||
button.style.transition = 'all 0.2s ease';
|
||||
|
||||
// Disable map interactions when clicking the button
|
||||
L.DomEvent.disableClickPropagation(button);
|
||||
|
||||
// Toggle add visit mode on button click
|
||||
L.DomEvent.on(button, 'click', () => {
|
||||
this.toggleAddVisitMode(button);
|
||||
});
|
||||
|
||||
this.addVisitButton = button;
|
||||
return button;
|
||||
if (this.addVisitButton) {
|
||||
// Attach our click handler to the existing button
|
||||
// Use event capturing and stopPropagation to prevent map click
|
||||
this.addVisitButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.toggleAddVisitMode(this.addVisitButton);
|
||||
}, true); // Use capture phase
|
||||
} else {
|
||||
console.warn('Add visit button not found, retrying...');
|
||||
// Retry if button hasn't been created yet
|
||||
this.addVisitButton = null;
|
||||
setTimeout(() => this.setupAddVisitButton(), 200);
|
||||
}
|
||||
});
|
||||
|
||||
// Add the control to the map (top right, below existing buttons)
|
||||
this.map.addControl(new AddVisitControl({ position: 'topright' }));
|
||||
}, 100);
|
||||
}
|
||||
|
||||
toggleAddVisitMode(button) {
|
||||
|
|
@ -120,15 +110,18 @@ export default class extends Controller {
|
|||
this.isAddingVisit = true;
|
||||
|
||||
// Update button style to show active state
|
||||
button.style.backgroundColor = '#dc3545';
|
||||
button.style.color = 'white';
|
||||
button.innerHTML = '✕';
|
||||
setAddVisitButtonActive(button);
|
||||
|
||||
// Change cursor to crosshair
|
||||
this.map.getContainer().style.cursor = 'crosshair';
|
||||
|
||||
// Add map click listener
|
||||
this.map.on('click', this.onMapClick, this);
|
||||
// Add map click listener with a small delay to prevent immediate trigger
|
||||
// This ensures the button click doesn't propagate to the map
|
||||
setTimeout(() => {
|
||||
if (this.isAddingVisit) {
|
||||
this.map.on('click', this.onMapClick, this);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
showFlashMessage('notice', 'Click on the map to place a visit');
|
||||
}
|
||||
|
|
@ -136,9 +129,8 @@ export default class extends Controller {
|
|||
exitAddVisitMode(button) {
|
||||
this.isAddingVisit = false;
|
||||
|
||||
// Reset button style with theme-aware styling
|
||||
applyThemeToButton(button, this.userThemeValue || 'dark');
|
||||
button.innerHTML = '➕';
|
||||
// Reset button style to inactive state
|
||||
setAddVisitButtonInactive(button, this.userThemeValue || 'dark');
|
||||
|
||||
// Reset cursor
|
||||
this.map.getContainer().style.cursor = '';
|
||||
|
|
@ -185,6 +177,12 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
showVisitForm(lat, lng) {
|
||||
// Close any existing popup first to ensure only one popup is open
|
||||
if (this.currentPopup) {
|
||||
this.map.closePopup(this.currentPopup);
|
||||
this.currentPopup = null;
|
||||
}
|
||||
|
||||
// Get current date/time for default values
|
||||
const now = new Date();
|
||||
const oneHourLater = new Date(now.getTime() + (60 * 60 * 1000));
|
||||
|
|
@ -290,7 +288,8 @@ export default class extends Controller {
|
|||
started_at: formData.get('started_at'),
|
||||
ended_at: formData.get('ended_at'),
|
||||
latitude: formData.get('latitude'),
|
||||
longitude: formData.get('longitude')
|
||||
longitude: formData.get('longitude'),
|
||||
status: 'confirmed' // Manually created visits should be confirmed
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -324,15 +323,14 @@ export default class extends Controller {
|
|||
|
||||
if (response.ok) {
|
||||
showFlashMessage('notice', `Visit "${visitData.visit.name}" created successfully!`);
|
||||
|
||||
// Store the created visit data
|
||||
const createdVisit = data;
|
||||
|
||||
this.exitAddVisitMode(this.addVisitButton);
|
||||
|
||||
// Refresh visits layer - this will clear and refetch data
|
||||
this.refreshVisitsLayer();
|
||||
|
||||
// Ensure confirmed visits layer is enabled (with a small delay for the API call to complete)
|
||||
setTimeout(() => {
|
||||
this.ensureVisitsLayersEnabled();
|
||||
}, 300);
|
||||
// Add the newly created visit marker immediately to the map
|
||||
this.addCreatedVisitToMap(createdVisit, visitData.visit.latitude, visitData.visit.longitude);
|
||||
} else {
|
||||
const errorMessage = data.error || data.message || 'Failed to create visit';
|
||||
showFlashMessage('error', errorMessage);
|
||||
|
|
@ -347,96 +345,92 @@ export default class extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
refreshVisitsLayer() {
|
||||
console.log('Attempting to refresh visits layer...');
|
||||
addCreatedVisitToMap(visitData, latitude, longitude) {
|
||||
console.log('Adding newly created visit to map immediately', { latitude, longitude, visitData });
|
||||
|
||||
// Try multiple approaches to refresh the visits layer
|
||||
const mapsController = document.querySelector('[data-controller*="maps"]');
|
||||
if (mapsController) {
|
||||
// Try to get the Stimulus controller instance
|
||||
const stimulusController = this.application.getControllerForElementAndIdentifier(mapsController, 'maps');
|
||||
|
||||
if (stimulusController && stimulusController.visitsManager) {
|
||||
console.log('Found maps controller with visits manager');
|
||||
|
||||
// Clear existing visits and fetch fresh data
|
||||
if (stimulusController.visitsManager.visitCircles) {
|
||||
stimulusController.visitsManager.visitCircles.clearLayers();
|
||||
}
|
||||
if (stimulusController.visitsManager.confirmedVisitCircles) {
|
||||
stimulusController.visitsManager.confirmedVisitCircles.clearLayers();
|
||||
}
|
||||
|
||||
// Refresh the visits data
|
||||
if (typeof stimulusController.visitsManager.fetchAndDisplayVisits === 'function') {
|
||||
console.log('Refreshing visits data...');
|
||||
stimulusController.visitsManager.fetchAndDisplayVisits();
|
||||
}
|
||||
} else {
|
||||
console.log('Could not find maps controller or visits manager');
|
||||
|
||||
// Fallback: Try to dispatch a custom event
|
||||
const refreshEvent = new CustomEvent('visits:refresh', { bubbles: true });
|
||||
mapsController.dispatchEvent(refreshEvent);
|
||||
}
|
||||
} else {
|
||||
if (!mapsController) {
|
||||
console.log('Could not find maps controller element');
|
||||
return;
|
||||
}
|
||||
|
||||
const stimulusController = this.application.getControllerForElementAndIdentifier(mapsController, 'maps');
|
||||
if (!stimulusController || !stimulusController.visitsManager) {
|
||||
console.log('Could not find maps controller or visits manager');
|
||||
return;
|
||||
}
|
||||
|
||||
const visitsManager = stimulusController.visitsManager;
|
||||
|
||||
// Create a circle for the newly created visit (always confirmed)
|
||||
const circle = L.circle([latitude, longitude], {
|
||||
color: '#4A90E2', // Border color for confirmed visits
|
||||
fillColor: '#4A90E2', // Fill color for confirmed visits
|
||||
fillOpacity: 0.5,
|
||||
radius: 110, // Confirmed visit size
|
||||
weight: 2,
|
||||
interactive: true,
|
||||
bubblingMouseEvents: false,
|
||||
pane: 'confirmedVisitsPane'
|
||||
});
|
||||
|
||||
// Add the circle to the confirmed visits layer
|
||||
visitsManager.confirmedVisitCircles.addLayer(circle);
|
||||
console.log('✅ Added newly created confirmed visit circle to layer');
|
||||
console.log('Confirmed visits layer info:', {
|
||||
layerCount: visitsManager.confirmedVisitCircles.getLayers().length,
|
||||
isOnMap: this.map.hasLayer(visitsManager.confirmedVisitCircles)
|
||||
});
|
||||
|
||||
// Make sure the layer is visible on the map
|
||||
if (!this.map.hasLayer(visitsManager.confirmedVisitCircles)) {
|
||||
this.map.addLayer(visitsManager.confirmedVisitCircles);
|
||||
console.log('✅ Added confirmed visits layer to map');
|
||||
}
|
||||
|
||||
// Check if the layer control has the confirmed visits layer enabled
|
||||
this.ensureConfirmedVisitsLayerEnabled();
|
||||
}
|
||||
|
||||
ensureVisitsLayersEnabled() {
|
||||
console.log('Ensuring visits layers are enabled...');
|
||||
|
||||
const mapsController = document.querySelector('[data-controller*="maps"]');
|
||||
if (mapsController) {
|
||||
const stimulusController = this.application.getControllerForElementAndIdentifier(mapsController, 'maps');
|
||||
|
||||
if (stimulusController && stimulusController.map && stimulusController.visitsManager) {
|
||||
const map = stimulusController.map;
|
||||
const visitsManager = stimulusController.visitsManager;
|
||||
|
||||
// Get the confirmed visits layer (newly created visits are always confirmed)
|
||||
const confirmedVisitsLayer = visitsManager.getConfirmedVisitCirclesLayer();
|
||||
|
||||
// Ensure confirmed visits layer is added to map since we create confirmed visits
|
||||
if (confirmedVisitsLayer && !map.hasLayer(confirmedVisitsLayer)) {
|
||||
console.log('Adding confirmed visits layer to map');
|
||||
map.addLayer(confirmedVisitsLayer);
|
||||
|
||||
// Update the layer control checkbox to reflect the layer is now active
|
||||
this.updateLayerControlCheckbox('Confirmed Visits', true);
|
||||
}
|
||||
|
||||
// Refresh visits data to include the new visit
|
||||
if (typeof visitsManager.fetchAndDisplayVisits === 'function') {
|
||||
console.log('Final refresh of visits to show new visit...');
|
||||
visitsManager.fetchAndDisplayVisits();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateLayerControlCheckbox(layerName, isEnabled) {
|
||||
// Find the layer control input for the specified layer
|
||||
ensureConfirmedVisitsLayerEnabled() {
|
||||
// Find the layer control and check/enable the "Confirmed Visits" checkbox
|
||||
const layerControlContainer = document.querySelector('.leaflet-control-layers');
|
||||
if (!layerControlContainer) {
|
||||
console.log('Layer control container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const inputs = layerControlContainer.querySelectorAll('input[type="checkbox"]');
|
||||
inputs.forEach(input => {
|
||||
const label = input.nextElementSibling;
|
||||
if (label && label.textContent.trim() === layerName) {
|
||||
console.log(`Updating ${layerName} checkbox to ${isEnabled}`);
|
||||
input.checked = isEnabled;
|
||||
// Expand the layer control if it's collapsed
|
||||
const layerControlExpand = layerControlContainer.querySelector('.leaflet-control-layers-toggle');
|
||||
if (layerControlExpand) {
|
||||
layerControlExpand.click();
|
||||
}
|
||||
|
||||
// Trigger change event to ensure proper state management
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
const inputs = layerControlContainer.querySelectorAll('input[type="checkbox"]');
|
||||
inputs.forEach(input => {
|
||||
const label = input.nextElementSibling;
|
||||
if (label && label.textContent.trim().includes('Confirmed Visits')) {
|
||||
console.log('Found Confirmed Visits checkbox, current state:', input.checked);
|
||||
if (!input.checked) {
|
||||
console.log('Enabling Confirmed Visits layer via checkbox');
|
||||
input.checked = true;
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
refreshVisitsLayer() {
|
||||
// Don't auto-refresh after creating a visit
|
||||
// The visit is already visible on the map from addCreatedVisitToMap()
|
||||
// Auto-refresh would clear it because fetchAndDisplayVisits uses URL date params
|
||||
// which might not include the newly created visit
|
||||
console.log('Skipping auto-refresh - visit already added to map');
|
||||
}
|
||||
|
||||
|
||||
cleanup() {
|
||||
if (this.map) {
|
||||
this.map.off('click', this.onMapClick, this);
|
||||
|
|
|
|||
43
app/javascript/controllers/clipboard_controller.js
Normal file
43
app/javascript/controllers/clipboard_controller.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { Controller } from "@hotwired/stimulus"
|
||||
import { showFlashMessage } from "../maps/helpers"
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
text: String
|
||||
}
|
||||
|
||||
static targets = ["icon", "text"]
|
||||
|
||||
copy() {
|
||||
navigator.clipboard.writeText(this.textValue).then(() => {
|
||||
this.showButtonFeedback()
|
||||
showFlashMessage('notice', 'Link copied to clipboard!')
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy text: ', err)
|
||||
showFlashMessage('error', 'Failed to copy link')
|
||||
})
|
||||
}
|
||||
|
||||
showButtonFeedback() {
|
||||
const button = this.element
|
||||
const originalClasses = button.className
|
||||
const originalHTML = button.innerHTML
|
||||
|
||||
// Change button appearance
|
||||
button.className = 'btn btn-success btn-xs'
|
||||
button.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Copied!
|
||||
`
|
||||
button.disabled = true
|
||||
|
||||
// Reset after 2 seconds
|
||||
setTimeout(() => {
|
||||
button.className = originalClasses
|
||||
button.innerHTML = originalHTML
|
||||
button.disabled = false
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
|
@ -109,7 +109,7 @@ export default class extends Controller {
|
|||
const lastSeen = new Date(location.updated_at).toLocaleString();
|
||||
|
||||
// Create small tooltip that shows automatically
|
||||
const tooltipContent = this.createTooltipContent(lastSeen);
|
||||
const tooltipContent = this.createTooltipContent(lastSeen, location.battery);
|
||||
const tooltip = familyMarker.bindTooltip(tooltipContent, {
|
||||
permanent: true,
|
||||
direction: 'top',
|
||||
|
|
@ -177,7 +177,7 @@ export default class extends Controller {
|
|||
|
||||
// Update tooltip content
|
||||
const lastSeen = new Date(locationData.updated_at).toLocaleString();
|
||||
const tooltipContent = this.createTooltipContent(lastSeen);
|
||||
const tooltipContent = this.createTooltipContent(lastSeen, locationData.battery);
|
||||
existingMarker.setTooltipContent(tooltipContent);
|
||||
|
||||
// Update popup content
|
||||
|
|
@ -216,7 +216,7 @@ export default class extends Controller {
|
|||
|
||||
const lastSeen = new Date(location.updated_at).toLocaleString();
|
||||
|
||||
const tooltipContent = this.createTooltipContent(lastSeen);
|
||||
const tooltipContent = this.createTooltipContent(lastSeen, location.battery);
|
||||
familyMarker.bindTooltip(tooltipContent, {
|
||||
permanent: true,
|
||||
direction: 'top',
|
||||
|
|
@ -238,8 +238,9 @@ export default class extends Controller {
|
|||
this.familyMarkers[location.user_id] = familyMarker;
|
||||
}
|
||||
|
||||
createTooltipContent(lastSeen) {
|
||||
return `Last seen: ${lastSeen}`;
|
||||
createTooltipContent(lastSeen, battery) {
|
||||
const batteryInfo = battery !== null && battery !== undefined ? ` | Battery: ${battery}%` : '';
|
||||
return `Last seen: ${lastSeen}${batteryInfo}`;
|
||||
}
|
||||
|
||||
createPopupContent(location, lastSeen) {
|
||||
|
|
@ -250,6 +251,59 @@ export default class extends Controller {
|
|||
|
||||
const emailInitial = location.email_initial || location.email?.charAt(0)?.toUpperCase() || '?';
|
||||
|
||||
// Battery display with icon
|
||||
const battery = location.battery;
|
||||
const batteryStatus = location.battery_status;
|
||||
let batteryDisplay = '';
|
||||
|
||||
if (battery !== null && battery !== undefined) {
|
||||
// Determine battery color based on level and status
|
||||
let batteryColor = '#10B981'; // green
|
||||
if (batteryStatus === 'charging') {
|
||||
batteryColor = battery <= 50 ? '#F59E0B' : '#10B981'; // orange if low, green if high
|
||||
} else if (battery <= 20) {
|
||||
batteryColor = '#EF4444'; // red
|
||||
} else if (battery <= 50) {
|
||||
batteryColor = '#F59E0B'; // orange
|
||||
}
|
||||
|
||||
// Helper function to get appropriate Lucide battery icon
|
||||
const getBatteryIcon = (battery, batteryStatus, batteryColor) => {
|
||||
const baseAttrs = `width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="${batteryColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 4px;"`;
|
||||
|
||||
// Charging icon
|
||||
if (batteryStatus === 'charging') {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" ${baseAttrs}><path d="m11 7-3 5h4l-3 5"/><path d="M14.856 6H16a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.935"/><path d="M22 14v-4"/><path d="M5.14 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.936"/></svg>`;
|
||||
}
|
||||
|
||||
// Full battery
|
||||
if (battery === 100 || batteryStatus === 'full') {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" ${baseAttrs}><path d="M10 10v4"/><path d="M14 10v4"/><path d="M22 14v-4"/><path d="M6 10v4"/><rect x="2" y="6" width="16" height="12" rx="2"/></svg>`;
|
||||
}
|
||||
|
||||
// Low battery (≤20%)
|
||||
if (battery <= 20) {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" ${baseAttrs}><path d="M22 14v-4"/><path d="M6 14v-4"/><rect x="2" y="6" width="16" height="12" rx="2"/></svg>`;
|
||||
}
|
||||
|
||||
// Medium battery (21-50%)
|
||||
if (battery <= 50) {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" ${baseAttrs}><path d="M10 14v-4"/><path d="M22 14v-4"/><path d="M6 14v-4"/><rect x="2" y="6" width="16" height="12" rx="2"/></svg>`;
|
||||
}
|
||||
|
||||
// High battery (>50%, default to full)
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" ${baseAttrs}><path d="M10 10v4"/><path d="M14 10v4"/><path d="M22 14v-4"/><path d="M6 10v4"/><rect x="2" y="6" width="16" height="12" rx="2"/></svg>`;
|
||||
};
|
||||
|
||||
const batteryIcon = getBatteryIcon(battery, batteryStatus, batteryColor);
|
||||
|
||||
batteryDisplay = `
|
||||
<p style="margin: 0 0 8px 0; font-size: 13px;">
|
||||
${batteryIcon}<strong>Battery:</strong> ${battery}%${batteryStatus ? ` (${batteryStatus})` : ''}
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="family-member-popup" style="background-color: ${bgColor}; color: ${textColor}; padding: 12px; border-radius: 8px; min-width: 220px;">
|
||||
<h3 style="margin: 0 0 12px 0; color: #10B981; font-size: 15px; font-weight: bold; display: flex; align-items: center; gap: 8px;">
|
||||
|
|
@ -263,6 +317,7 @@ export default class extends Controller {
|
|||
<strong>Coordinates:</strong><br/>
|
||||
${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}
|
||||
</p>
|
||||
${batteryDisplay}
|
||||
<p style="margin: 0; font-size: 12px; color: ${mutedColor}; padding-top: 8px; border-top: 1px solid ${isDark ? '#374151' : '#e5e7eb'};">
|
||||
<strong>Last seen:</strong> ${lastSeen}
|
||||
</p>
|
||||
|
|
|
|||
45
app/javascript/controllers/map_controls_controller.js
Normal file
45
app/javascript/controllers/map_controls_controller.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["panel", "toggleIcon"]
|
||||
|
||||
connect() {
|
||||
// Restore panel state from sessionStorage on page load
|
||||
const panelState = sessionStorage.getItem('mapControlsPanelState')
|
||||
if (panelState === 'visible') {
|
||||
this.showPanel()
|
||||
}
|
||||
}
|
||||
|
||||
toggle() {
|
||||
const isHidden = this.panelTarget.classList.contains("hidden")
|
||||
|
||||
if (isHidden) {
|
||||
this.showPanel()
|
||||
sessionStorage.setItem('mapControlsPanelState', 'visible')
|
||||
} else {
|
||||
this.hidePanel()
|
||||
sessionStorage.setItem('mapControlsPanelState', 'hidden')
|
||||
}
|
||||
}
|
||||
|
||||
showPanel() {
|
||||
this.panelTarget.classList.remove("hidden")
|
||||
|
||||
// Update icon to chevron-up
|
||||
const currentIcon = this.toggleIconTarget.querySelector('svg')
|
||||
currentIcon.classList.remove('lucide-chevron-down')
|
||||
currentIcon.classList.add('lucide-chevron-up')
|
||||
currentIcon.innerHTML = '<path d="m18 15-6-6-6 6"/>'
|
||||
}
|
||||
|
||||
hidePanel() {
|
||||
this.panelTarget.classList.add("hidden")
|
||||
|
||||
// Update icon to chevron-down
|
||||
const currentIcon = this.toggleIconTarget.querySelector('svg')
|
||||
currentIcon.classList.remove('lucide-chevron-up')
|
||||
currentIcon.classList.add('lucide-chevron-down')
|
||||
currentIcon.innerHTML = '<path d="m6 9 6 6 6-6"/>'
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ import { TileMonitor } from "../maps/tile_monitor";
|
|||
import BaseController from "./base_controller";
|
||||
import { createAllMapLayers } from "../maps/layers";
|
||||
import { applyThemeToControl, applyThemeToButton, applyThemeToPanel } from "../maps/theme_utils";
|
||||
import { addTopRightButtons } from "../maps/map_controls";
|
||||
|
||||
export default class extends BaseController {
|
||||
static targets = ["container"];
|
||||
|
|
@ -112,7 +113,7 @@ export default class extends BaseController {
|
|||
this.map = L.map(this.containerTarget).setView([this.center[0], this.center[1]], 14);
|
||||
|
||||
// Add scale control
|
||||
L.control.scale({
|
||||
this.scaleControl = L.control.scale({
|
||||
position: 'bottomright',
|
||||
imperial: this.distanceUnit === 'mi',
|
||||
metric: this.distanceUnit === 'km',
|
||||
|
|
@ -145,7 +146,7 @@ export default class extends BaseController {
|
|||
}
|
||||
});
|
||||
|
||||
new StatsControl().addTo(this.map);
|
||||
this.statsControl = new StatsControl().addTo(this.map);
|
||||
|
||||
// Set the maximum bounds to prevent infinite scroll
|
||||
var southWest = L.latLng(-120, -210);
|
||||
|
|
@ -200,6 +201,9 @@ export default class extends BaseController {
|
|||
this.addSettingsButton();
|
||||
}
|
||||
|
||||
// Add info toggle button
|
||||
this.addInfoToggleButton();
|
||||
|
||||
// Initialize the visits manager
|
||||
this.visitsManager = new VisitsManager(this.map, this.apiKey, this.userTheme);
|
||||
|
||||
|
|
@ -209,22 +213,6 @@ export default class extends BaseController {
|
|||
// Expose maps controller globally for family integration
|
||||
window.mapsController = this;
|
||||
|
||||
// Initialize layers for the layer control
|
||||
const controlsLayer = {
|
||||
Points: this.markersLayer,
|
||||
Routes: this.polylinesLayer,
|
||||
Tracks: this.tracksLayer,
|
||||
Heatmap: this.heatmapLayer,
|
||||
"Fog of War": this.fogOverlay,
|
||||
"Scratch map": this.scratchLayerManager?.getLayer() || L.layerGroup(),
|
||||
Areas: this.areasLayer,
|
||||
Photos: this.photoMarkers,
|
||||
"Suggested Visits": this.visitsManager.getVisitCirclesLayer(),
|
||||
"Confirmed Visits": this.visitsManager.getConfirmedVisitCirclesLayer()
|
||||
};
|
||||
|
||||
this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
|
||||
|
||||
// Initialize tile monitor
|
||||
this.tileMonitor = new TileMonitor(this.map, this.apiKey);
|
||||
|
||||
|
|
@ -250,11 +238,25 @@ export default class extends BaseController {
|
|||
// Preload areas
|
||||
fetchAndDrawAreas(this.areasLayer, this.apiKey);
|
||||
|
||||
// Add right panel toggle
|
||||
this.addTogglePanelButton();
|
||||
// Add all top-right buttons in the correct order
|
||||
this.initializeTopRightButtons();
|
||||
|
||||
// Initialize layers for the layer control
|
||||
const controlsLayer = {
|
||||
Points: this.markersLayer,
|
||||
Routes: this.polylinesLayer,
|
||||
Tracks: this.tracksLayer,
|
||||
Heatmap: this.heatmapLayer,
|
||||
"Fog of War": this.fogOverlay,
|
||||
"Scratch map": this.scratchLayerManager?.getLayer() || L.layerGroup(),
|
||||
Areas: this.areasLayer,
|
||||
Photos: this.photoMarkers,
|
||||
"Suggested Visits": this.visitsManager.getVisitCirclesLayer(),
|
||||
"Confirmed Visits": this.visitsManager.getConfirmedVisitCirclesLayer()
|
||||
};
|
||||
|
||||
this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
|
||||
|
||||
// Add visits buttons after calendar button to position them below
|
||||
this.visitsManager.addDrawerButton();
|
||||
|
||||
// Initialize Live Map Handler
|
||||
this.initializeLiveMapHandler();
|
||||
|
|
@ -800,13 +802,19 @@ export default class extends BaseController {
|
|||
// Define the custom control
|
||||
const SettingsControl = L.Control.extend({
|
||||
onAdd: (map) => {
|
||||
const button = L.DomUtil.create('button', 'map-settings-button');
|
||||
button.innerHTML = '⚙️'; // Gear icon
|
||||
const button = L.DomUtil.create('button', 'map-settings-button tooltip tooltip-right');
|
||||
button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cog-icon lucide-cog"><path d="M11 10.27 7 3.34"/><path d="m11 13.73-4 6.93"/><path d="M12 22v-2"/><path d="M12 2v2"/><path d="M14 12h8"/><path d="m17 20.66-1-1.73"/><path d="m17 3.34-1 1.73"/><path d="M2 12h2"/><path d="m20.66 17-1.73-1"/><path d="m20.66 7-1.73 1"/><path d="m3.34 17 1.73-1"/><path d="m3.34 7 1.73 1"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="12" r="8"/></svg>'; // Gear icon
|
||||
button.setAttribute('data-tip', 'Settings');
|
||||
|
||||
// Style the button with theme-aware styling
|
||||
applyThemeToButton(button, this.userTheme);
|
||||
button.style.width = '32px';
|
||||
button.style.height = '32px';
|
||||
button.style.width = '30px';
|
||||
button.style.height = '30px';
|
||||
button.style.display = 'flex';
|
||||
button.style.alignItems = 'center';
|
||||
button.style.justifyContent = 'center';
|
||||
button.style.padding = '0';
|
||||
button.style.borderRadius = '4px';
|
||||
|
||||
// Disable map interactions when clicking the button
|
||||
L.DomEvent.disableClickPropagation(button);
|
||||
|
|
@ -825,6 +833,104 @@ export default class extends BaseController {
|
|||
this.settingsButtonAdded = true;
|
||||
}
|
||||
|
||||
addInfoToggleButton() {
|
||||
// Store reference to the controller instance for use in the control
|
||||
const controller = this;
|
||||
|
||||
const InfoToggleControl = L.Control.extend({
|
||||
options: {
|
||||
position: 'bottomleft'
|
||||
},
|
||||
onAdd: function(map) {
|
||||
const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
|
||||
const button = L.DomUtil.create('button', 'map-info-toggle-button tooltip tooltip-right', container);
|
||||
button.setAttribute('data-tip', 'Toggle footer visibility');
|
||||
|
||||
// Lucide info icon
|
||||
button.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M12 16v-4"></path>
|
||||
<path d="M12 8h.01"></path>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Style the button with theme-aware styling
|
||||
applyThemeToButton(button, controller.userTheme);
|
||||
button.style.width = '34px';
|
||||
button.style.height = '34px';
|
||||
button.style.display = 'flex';
|
||||
button.style.alignItems = 'center';
|
||||
button.style.justifyContent = 'center';
|
||||
button.style.cursor = 'pointer';
|
||||
button.style.border = 'none';
|
||||
button.style.borderRadius = '4px';
|
||||
|
||||
// Disable map interactions when clicking the button
|
||||
L.DomEvent.disableClickPropagation(container);
|
||||
|
||||
// Toggle footer visibility on button click
|
||||
L.DomEvent.on(button, 'click', () => {
|
||||
controller.toggleFooterVisibility();
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
});
|
||||
|
||||
// Add the control to the map
|
||||
this.map.addControl(new InfoToggleControl());
|
||||
}
|
||||
|
||||
toggleFooterVisibility() {
|
||||
// Toggle the page footer
|
||||
const footer = document.getElementById('map-footer');
|
||||
if (!footer) return;
|
||||
|
||||
const isCurrentlyHidden = footer.classList.contains('hidden');
|
||||
|
||||
// Toggle Tailwind's hidden class
|
||||
footer.classList.toggle('hidden');
|
||||
|
||||
// Adjust bottom controls position based on footer visibility
|
||||
if (isCurrentlyHidden) {
|
||||
// Footer is being shown - move controls up
|
||||
setTimeout(() => {
|
||||
const footerHeight = footer.offsetHeight;
|
||||
// Add extra 20px margin above footer
|
||||
this.adjustBottomControls(footerHeight + 20);
|
||||
}, 10); // Small delay to ensure footer is rendered
|
||||
} else {
|
||||
// Footer is being hidden - reset controls position
|
||||
this.adjustBottomControls(10); // Back to default padding
|
||||
}
|
||||
|
||||
// Add click event to close footer when clicking on it (only add once)
|
||||
if (!footer.dataset.clickHandlerAdded) {
|
||||
footer.addEventListener('click', (e) => {
|
||||
// Only close if clicking the footer itself, not its contents
|
||||
if (e.target === footer) {
|
||||
footer.classList.add('hidden');
|
||||
this.adjustBottomControls(10); // Reset controls position
|
||||
}
|
||||
});
|
||||
footer.dataset.clickHandlerAdded = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
adjustBottomControls(paddingBottom) {
|
||||
// Adjust all bottom Leaflet controls
|
||||
const bottomLeftControls = this.map.getContainer().querySelector('.leaflet-bottom.leaflet-left');
|
||||
const bottomRightControls = this.map.getContainer().querySelector('.leaflet-bottom.leaflet-right');
|
||||
|
||||
if (bottomLeftControls) {
|
||||
bottomLeftControls.style.setProperty('padding-bottom', `${paddingBottom}px`, 'important');
|
||||
}
|
||||
if (bottomRightControls) {
|
||||
bottomRightControls.style.setProperty('padding-bottom', `${paddingBottom}px`, 'important');
|
||||
}
|
||||
}
|
||||
|
||||
toggleSettingsMenu() {
|
||||
// If the settings panel already exists, just show/hide it
|
||||
if (this.settingsPanel) {
|
||||
|
|
@ -1161,48 +1267,35 @@ export default class extends BaseController {
|
|||
}
|
||||
}
|
||||
|
||||
initializeTopRightButtons() {
|
||||
// Add all top-right buttons in the correct order:
|
||||
// 1. Select Area, 2. Add Visit, 3. Open Calendar, 4. Open Drawer
|
||||
// Note: Layer control is added separately and appears at the top
|
||||
|
||||
addTogglePanelButton() {
|
||||
// Store reference to the controller instance for use in the control
|
||||
const controller = this;
|
||||
this.topRightControls = addTopRightButtons(
|
||||
this.map,
|
||||
{
|
||||
onSelectArea: () => this.visitsManager.toggleSelectionMode(),
|
||||
// onAddVisit is intentionally null - the add_visit_controller will attach its handler
|
||||
onAddVisit: null,
|
||||
onToggleCalendar: () => this.toggleRightPanel(),
|
||||
onToggleDrawer: () => this.visitsManager.toggleDrawer()
|
||||
},
|
||||
this.userTheme
|
||||
);
|
||||
|
||||
const TogglePanelControl = L.Control.extend({
|
||||
onAdd: function(map) {
|
||||
const button = L.DomUtil.create('button', 'toggle-panel-button');
|
||||
button.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8 2v4" />
|
||||
<path d="M16 2v4" />
|
||||
<path d="M21 14V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8" />
|
||||
<path d="M3 10h18" />
|
||||
<path d="m16 20 2 2 4-4" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Style the button with theme-aware styling
|
||||
applyThemeToButton(button, controller.userTheme);
|
||||
button.style.width = '48px';
|
||||
button.style.height = '48px';
|
||||
button.style.borderRadius = '4px';
|
||||
button.style.padding = '0';
|
||||
button.style.display = 'flex';
|
||||
button.style.alignItems = 'center';
|
||||
button.style.justifyContent = 'center';
|
||||
|
||||
// Disable map interactions when clicking the button
|
||||
L.DomEvent.disableClickPropagation(button);
|
||||
|
||||
// Toggle panel on button click
|
||||
L.DomEvent.on(button, 'click', () => {
|
||||
controller.toggleRightPanel();
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
});
|
||||
|
||||
// Add the control to the map
|
||||
this.map.addControl(new TogglePanelControl({ position: 'topright' }));
|
||||
// Add CSS for selection button active state (needed by visits manager)
|
||||
if (!document.getElementById('selection-tool-active-style')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'selection-tool-active-style';
|
||||
style.textContent = `
|
||||
#selection-tool-button.active {
|
||||
border: 2px dashed #3388ff !important;
|
||||
box-shadow: 0 0 8px rgba(51, 136, 255, 0.5) !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
shouldShowTracksSelector() {
|
||||
|
|
|
|||
|
|
@ -99,13 +99,6 @@ export default class extends BaseController {
|
|||
console.log('📊 After fitBounds overlay display:', afterFitBoundsElement?.style.display || 'default');
|
||||
}
|
||||
|
||||
console.log('🎯 Public sharing: using manual hexagon loading');
|
||||
console.log('🔍 Debug values:');
|
||||
console.log(' dataBounds:', dataBounds);
|
||||
console.log(' point_count:', dataBounds?.point_count);
|
||||
console.log(' hexagonsAvailableValue:', this.hexagonsAvailableValue);
|
||||
console.log(' hexagonsAvailableValue type:', typeof this.hexagonsAvailableValue);
|
||||
|
||||
// Load hexagons only if they are pre-calculated and data exists
|
||||
if (dataBounds && dataBounds.point_count > 0 && this.hexagonsAvailableValue) {
|
||||
await this.loadStaticHexagons();
|
||||
|
|
@ -140,7 +133,7 @@ export default class extends BaseController {
|
|||
|
||||
// Ensure loading overlay is visible and disable map interaction
|
||||
const loadingElement = document.getElementById('map-loading');
|
||||
console.log('🔍 Loading element found:', !!loadingElement);
|
||||
|
||||
if (loadingElement) {
|
||||
loadingElement.style.display = 'flex';
|
||||
loadingElement.style.visibility = 'visible';
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@ export default class extends Controller {
|
|||
// Show temporary success feedback
|
||||
const button = this.sharingLinkTarget.nextElementSibling
|
||||
const originalText = button.innerHTML
|
||||
button.innerHTML = "✅ Copied!"
|
||||
button.classList.add("btn-success")
|
||||
button.innerHTML = "✅ Link Copied!"
|
||||
button.classList.add("btn-outline btn-success")
|
||||
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalText
|
||||
|
|
|
|||
|
|
@ -125,14 +125,16 @@ export function showFlashMessage(type, message) {
|
|||
if (!flashContainer) {
|
||||
flashContainer = document.createElement('div');
|
||||
flashContainer.id = 'flash-messages';
|
||||
flashContainer.className = 'fixed top-5 right-5 flex flex-col-reverse gap-2 z-50';
|
||||
// Use z-[9999] to ensure flash messages appear above navbar (z-50)
|
||||
flashContainer.className = 'fixed top-20 right-5 flex flex-col-reverse gap-2';
|
||||
flashContainer.style.zIndex = '9999';
|
||||
document.body.appendChild(flashContainer);
|
||||
}
|
||||
|
||||
// Create the flash message div
|
||||
const flashDiv = document.createElement('div');
|
||||
flashDiv.setAttribute('data-controller', 'removals');
|
||||
flashDiv.className = `flex items-center justify-between ${classesForFlash(type)} py-3 px-5 rounded-lg z-50`;
|
||||
flashDiv.className = `flex items-center justify-between ${classesForFlash(type)} py-3 px-5 rounded-lg shadow-lg`;
|
||||
|
||||
// Create the message div
|
||||
const messageDiv = document.createElement('div');
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class LocationSearch {
|
|||
const SearchToggleControl = L.Control.extend({
|
||||
onAdd: function(map) {
|
||||
const button = L.DomUtil.create('button', 'location-search-toggle');
|
||||
button.innerHTML = '🔍';
|
||||
button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>';
|
||||
// Style the button with theme-aware styling
|
||||
applyThemeToButton(button, this.userTheme);
|
||||
button.style.width = '48px';
|
||||
|
|
@ -33,6 +33,9 @@ class LocationSearch {
|
|||
button.style.padding = '0';
|
||||
button.style.fontSize = '18px';
|
||||
button.style.marginTop = '10px'; // Space below settings button
|
||||
button.style.display = 'flex';
|
||||
button.style.alignItems = 'center';
|
||||
button.style.justifyContent = 'center';
|
||||
button.title = 'Search locations';
|
||||
button.id = 'location-search-toggle';
|
||||
return button;
|
||||
|
|
@ -174,8 +177,6 @@ class LocationSearch {
|
|||
container.addEventListener('DOMMouseScroll', (e) => {
|
||||
e.stopPropagation();
|
||||
}, { passive: false });
|
||||
|
||||
console.log('LocationSearch: Added scroll prevention to container', container.id || 'search-bar');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
193
app/javascript/maps/map_controls.js
Normal file
193
app/javascript/maps/map_controls.js
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// Map control buttons and utilities
|
||||
// This file contains all button controls that are positioned on the top-right corner of the map
|
||||
import L from "leaflet";
|
||||
import { applyThemeToButton } from "./theme_utils";
|
||||
|
||||
/**
|
||||
* Creates a standardized button element for map controls
|
||||
* @param {String} className - CSS class name for the button
|
||||
* @param {String} svgIcon - SVG icon HTML
|
||||
* @param {String} title - Button title/tooltip
|
||||
* @param {String} userTheme - User's theme preference ('dark' or 'light')
|
||||
* @param {Function} onClickCallback - Callback function to execute when button is clicked
|
||||
* @returns {HTMLElement} Button element with tooltip
|
||||
*/
|
||||
function createStandardButton(className, svgIcon, title, userTheme, onClickCallback) {
|
||||
const button = L.DomUtil.create('button', `${className} tooltip tooltip-left`);
|
||||
button.innerHTML = svgIcon;
|
||||
button.setAttribute('data-tip', title);
|
||||
|
||||
// Apply standard button styling
|
||||
applyThemeToButton(button, userTheme);
|
||||
button.style.width = '48px';
|
||||
button.style.height = '48px';
|
||||
button.style.borderRadius = '4px';
|
||||
button.style.padding = '0';
|
||||
button.style.display = 'flex';
|
||||
button.style.alignItems = 'center';
|
||||
button.style.justifyContent = 'center';
|
||||
button.style.fontSize = '18px';
|
||||
button.style.transition = 'all 0.2s ease';
|
||||
|
||||
// Disable map interactions when clicking the button
|
||||
L.DomEvent.disableClickPropagation(button);
|
||||
|
||||
// Attach click handler if provided
|
||||
// Note: Some buttons (like Add Visit) have their handlers attached separately
|
||||
if (onClickCallback && typeof onClickCallback === 'function') {
|
||||
L.DomEvent.on(button, 'click', () => {
|
||||
onClickCallback(button);
|
||||
});
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a "Toggle Panel" button control for the map
|
||||
* @param {Function} onClickCallback - Callback function to execute when button is clicked
|
||||
* @param {String} userTheme - User's theme preference ('dark' or 'light')
|
||||
* @returns {L.Control} Leaflet control instance
|
||||
*/
|
||||
export function createTogglePanelControl(onClickCallback, userTheme = 'dark') {
|
||||
const TogglePanelControl = L.Control.extend({
|
||||
onAdd: function(map) {
|
||||
const svgIcon = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8 2v4" />
|
||||
<path d="M16 2v4" />
|
||||
<path d="M21 14V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8" />
|
||||
<path d="M3 10h18" />
|
||||
<path d="m16 20 2 2 4-4" />
|
||||
</svg>
|
||||
`;
|
||||
return createStandardButton('toggle-panel-button', svgIcon, 'Toggle Panel', userTheme, onClickCallback);
|
||||
}
|
||||
});
|
||||
|
||||
return TogglePanelControl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a "Visits Drawer" button control for the map
|
||||
* @param {Function} onClickCallback - Callback function to execute when button is clicked
|
||||
* @param {String} userTheme - User's theme preference ('dark' or 'light')
|
||||
* @returns {L.Control} Leaflet control instance
|
||||
*/
|
||||
export function createVisitsDrawerControl(onClickCallback, userTheme = 'dark') {
|
||||
const DrawerControl = L.Control.extend({
|
||||
onAdd: function(map) {
|
||||
const svgIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-right-open-icon lucide-panel-right-open"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/><path d="m10 15-3-3 3-3"/></svg>';
|
||||
return createStandardButton('leaflet-control-button drawer-button', svgIcon, 'Toggle Visits Drawer', userTheme, onClickCallback);
|
||||
}
|
||||
});
|
||||
|
||||
return DrawerControl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an "Area Selection" button control for the map
|
||||
* @param {Function} onClickCallback - Callback function to execute when button is clicked
|
||||
* @param {String} userTheme - User's theme preference ('dark' or 'light')
|
||||
* @returns {L.Control} Leaflet control instance
|
||||
*/
|
||||
export function createAreaSelectionControl(onClickCallback, userTheme = 'dark') {
|
||||
const SelectionControl = L.Control.extend({
|
||||
onAdd: function(map) {
|
||||
const svgIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-dashed-mouse-pointer-icon lucide-square-dashed-mouse-pointer"><path d="M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"/><path d="M5 3a2 2 0 0 0-2 2"/><path d="M19 3a2 2 0 0 1 2 2"/><path d="M5 21a2 2 0 0 1-2-2"/><path d="M9 3h1"/><path d="M9 21h2"/><path d="M14 3h1"/><path d="M3 9v1"/><path d="M21 9v2"/><path d="M3 14v1"/></svg>';
|
||||
const button = createStandardButton('leaflet-bar leaflet-control leaflet-control-custom', svgIcon, 'Select Area', userTheme, onClickCallback);
|
||||
button.id = 'selection-tool-button';
|
||||
return button;
|
||||
}
|
||||
});
|
||||
|
||||
return SelectionControl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an "Add Visit" button control for the map
|
||||
* @param {Function} onClickCallback - Callback function to execute when button is clicked
|
||||
* @param {String} userTheme - User's theme preference ('dark' or 'light')
|
||||
* @returns {L.Control} Leaflet control instance
|
||||
*/
|
||||
export function createAddVisitControl(onClickCallback, userTheme = 'dark') {
|
||||
const AddVisitControl = L.Control.extend({
|
||||
onAdd: function(map) {
|
||||
const svgIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin-check-icon lucide-map-pin-check"><path d="M19.43 12.935c.357-.967.57-1.955.57-2.935a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32.197 32.197 0 0 0 .813-.728"/><circle cx="12" cy="10" r="3"/><path d="m16 18 2 2 4-4"/></svg>';
|
||||
return createStandardButton('leaflet-control-button add-visit-button', svgIcon, 'Add a visit', userTheme, onClickCallback);
|
||||
}
|
||||
});
|
||||
|
||||
return AddVisitControl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all top-right corner buttons to the map in the correct order
|
||||
* Order: 1. Select Area, 2. Add Visit, 3. Open Calendar, 4. Open Drawer
|
||||
* Note: Layer control is added separately by Leaflet and appears at the top
|
||||
*
|
||||
* @param {Object} map - Leaflet map instance
|
||||
* @param {Object} callbacks - Object containing callback functions for each button
|
||||
* @param {Function} callbacks.onSelectArea - Callback for select area button
|
||||
* @param {Function} callbacks.onAddVisit - Callback for add visit button
|
||||
* @param {Function} callbacks.onToggleCalendar - Callback for toggle calendar/panel button
|
||||
* @param {Function} callbacks.onToggleDrawer - Callback for toggle drawer button
|
||||
* @param {String} userTheme - User's theme preference ('dark' or 'light')
|
||||
* @returns {Object} Object containing references to all created controls
|
||||
*/
|
||||
export function addTopRightButtons(map, callbacks, userTheme = 'dark') {
|
||||
const controls = {};
|
||||
|
||||
// 1. Select Area button
|
||||
if (callbacks.onSelectArea) {
|
||||
const SelectionControl = createAreaSelectionControl(callbacks.onSelectArea, userTheme);
|
||||
controls.selectionControl = new SelectionControl({ position: 'topright' });
|
||||
map.addControl(controls.selectionControl);
|
||||
}
|
||||
|
||||
// 2. Add Visit button
|
||||
// Note: Button is always created, callback is optional (add_visit_controller attaches its own handler)
|
||||
const AddVisitControl = createAddVisitControl(callbacks.onAddVisit, userTheme);
|
||||
controls.addVisitControl = new AddVisitControl({ position: 'topright' });
|
||||
map.addControl(controls.addVisitControl);
|
||||
|
||||
// 3. Open Calendar (Toggle Panel) button
|
||||
if (callbacks.onToggleCalendar) {
|
||||
const TogglePanelControl = createTogglePanelControl(callbacks.onToggleCalendar, userTheme);
|
||||
controls.togglePanelControl = new TogglePanelControl({ position: 'topright' });
|
||||
map.addControl(controls.togglePanelControl);
|
||||
}
|
||||
|
||||
// 4. Open Drawer button
|
||||
if (callbacks.onToggleDrawer) {
|
||||
const DrawerControl = createVisitsDrawerControl(callbacks.onToggleDrawer, userTheme);
|
||||
controls.drawerControl = new DrawerControl({ position: 'topright' });
|
||||
map.addControl(controls.drawerControl);
|
||||
}
|
||||
|
||||
return controls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the Add Visit button to show active state
|
||||
* @param {HTMLElement} button - The button element to update
|
||||
*/
|
||||
export function setAddVisitButtonActive(button) {
|
||||
if (!button) return;
|
||||
|
||||
button.style.backgroundColor = '#dc3545';
|
||||
button.style.color = 'white';
|
||||
button.innerHTML = '✕';
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the Add Visit button to show inactive/default state
|
||||
* @param {HTMLElement} button - The button element to update
|
||||
* @param {String} userTheme - User's theme preference ('dark' or 'light')
|
||||
*/
|
||||
export function setAddVisitButtonInactive(button, userTheme = 'dark') {
|
||||
if (!button) return;
|
||||
|
||||
applyThemeToButton(button, userTheme);
|
||||
button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin-check-icon lucide-map-pin-check"><path d="M19.43 12.935c.357-.967.57-1.955.57-2.935a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32.197 32.197 0 0 0 .813-.728"/><circle cx="12" cy="10" r="3"/><path d="m16 18 2 2 4-4"/></svg>';
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import L from "leaflet";
|
||||
import { showFlashMessage } from "./helpers";
|
||||
import { applyThemeToButton } from "./theme_utils";
|
||||
|
||||
/**
|
||||
* Manages visits functionality including displaying, fetching, and interacting with visits
|
||||
|
|
@ -65,74 +64,14 @@ export class VisitsManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Adds a button to toggle the visits drawer
|
||||
* Note: Drawer and selection buttons are now added centrally via addTopRightButtons()
|
||||
* in maps_controller.js to ensure correct button ordering.
|
||||
*
|
||||
* The methods below are kept for backwards compatibility but are no longer called
|
||||
* during initialization. Button callbacks are wired directly in maps_controller.js:
|
||||
* - onSelectArea -> this.toggleSelectionMode()
|
||||
* - onToggleDrawer -> this.toggleDrawer()
|
||||
*/
|
||||
addDrawerButton() {
|
||||
const DrawerControl = L.Control.extend({
|
||||
onAdd: (map) => {
|
||||
const button = L.DomUtil.create('button', 'leaflet-control-button drawer-button');
|
||||
button.innerHTML = '⬅️'; // Left arrow icon
|
||||
// Style the button with theme-aware styling
|
||||
applyThemeToButton(button, this.userTheme);
|
||||
button.style.width = '48px';
|
||||
button.style.height = '48px';
|
||||
button.style.borderRadius = '4px';
|
||||
button.style.padding = '0';
|
||||
button.style.lineHeight = '48px';
|
||||
button.style.fontSize = '18px';
|
||||
button.style.textAlign = 'center';
|
||||
|
||||
L.DomEvent.disableClickPropagation(button);
|
||||
L.DomEvent.on(button, 'click', () => {
|
||||
this.toggleDrawer();
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
});
|
||||
|
||||
this.map.addControl(new DrawerControl({ position: 'topright' }));
|
||||
|
||||
// Add the selection tool button
|
||||
this.addSelectionButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a button to enable/disable the area selection tool
|
||||
*/
|
||||
addSelectionButton() {
|
||||
const SelectionControl = L.Control.extend({
|
||||
onAdd: (map) => {
|
||||
const button = L.DomUtil.create('button', 'leaflet-bar leaflet-control leaflet-control-custom');
|
||||
button.innerHTML = '⚓️';
|
||||
button.title = 'Select Area';
|
||||
button.id = 'selection-tool-button';
|
||||
// Style the button with theme-aware styling
|
||||
applyThemeToButton(button, this.userTheme);
|
||||
button.style.width = '48px';
|
||||
button.style.height = '48px';
|
||||
button.style.borderRadius = '4px';
|
||||
button.style.padding = '0';
|
||||
button.style.lineHeight = '48px';
|
||||
button.style.fontSize = '18px';
|
||||
button.style.textAlign = 'center';
|
||||
button.onclick = () => this.toggleSelectionMode();
|
||||
return button;
|
||||
}
|
||||
});
|
||||
|
||||
new SelectionControl({ position: 'topright' }).addTo(this.map);
|
||||
|
||||
// Add CSS for selection button active state
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
#selection-tool-button.active {
|
||||
border: 2px dashed #3388ff !important;
|
||||
box-shadow: 0 0 8px rgba(51, 136, 255, 0.5) !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the area selection mode
|
||||
|
|
@ -482,7 +421,7 @@ export class VisitsManager {
|
|||
|
||||
const drawerButton = document.querySelector('.drawer-button');
|
||||
if (drawerButton) {
|
||||
drawerButton.innerHTML = this.drawerOpen ? '➡️' : '⬅️';
|
||||
drawerButton.innerHTML = this.drawerOpen ? '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-right-close-icon lucide-panel-right-close"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/><path d="m8 9 3 3-3 3"/></svg>' : '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-right-open-icon lucide-panel-right-open"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/><path d="m10 15-3-3 3-3"/></svg>';
|
||||
}
|
||||
|
||||
const controls = document.querySelectorAll('.leaflet-control-layers, .toggle-panel-button, .leaflet-right-panel, .drawer-button, #selection-tool-button');
|
||||
|
|
|
|||
|
|
@ -5,7 +5,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
|
||||
|
|
|
|||
|
|
@ -41,7 +41,9 @@ class Families::Locations
|
|||
latitude: point.lat,
|
||||
longitude: point.lon,
|
||||
timestamp: point.timestamp.to_i,
|
||||
updated_at: Time.zone.at(point.timestamp.to_i)
|
||||
updated_at: Time.zone.at(point.timestamp.to_i),
|
||||
battery: point.battery,
|
||||
battery_status: point.battery_status
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@
|
|||
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>
|
||||
<span class="text-sm opacity-60">Location sharing:</span>
|
||||
|
||||
<!-- Toggle Switch -->
|
||||
<input type="checkbox"
|
||||
|
|
@ -136,7 +136,7 @@
|
|||
<% else %>
|
||||
<!-- Other member's status - read-only indicator -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm opacity-60">Location:</span>
|
||||
<span class="text-sm opacity-60">Location sharing:</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">
|
||||
|
|
@ -174,7 +174,7 @@
|
|||
<div class="space-y-3 mb-4">
|
||||
<% @pending_invitations.each do |invitation| %>
|
||||
<div class="flex items-center justify-between p-3 bg-base-100 rounded-lg">
|
||||
<div>
|
||||
<div class="flex-grow">
|
||||
<div class="font-medium text-base-content"><%= invitation.email %></div>
|
||||
<div class="text-sm text-base-content opacity-60">
|
||||
<%= t('families.show.invited_on', default: 'Invited') %>
|
||||
|
|
@ -184,14 +184,26 @@
|
|||
<%= t('families.show.expires_on', default: 'Expires') %>
|
||||
<%= invitation.expires_at.strftime('%b %d, %Y at %I:%M %p') %>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button data-controller="clipboard"
|
||||
data-clipboard-text-value="<%= public_invitation_url(invitation.token) %>"
|
||||
data-action="click->clipboard#copy"
|
||||
class="btn btn-outline btn-info btn-xs"
|
||||
title="Copy invitation link">
|
||||
<%= icon 'copy', class: "inline-block w-3" %>
|
||||
Copy Invitation Link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% if policy(@family).manage_invitations? %>
|
||||
<%= link_to family_invitation_path(invitation.token),
|
||||
method: :delete,
|
||||
data: { confirm: 'Are you sure you want to cancel this invitation?', turbo_confirm: 'Are you sure you want to cancel this invitation?' },
|
||||
class: "btn btn-outline btn-warning btn-sm opacity-70" do %>
|
||||
Cancel
|
||||
<% end %>
|
||||
<div class="ml-3">
|
||||
<%= link_to family_invitation_path(invitation.token),
|
||||
method: :delete,
|
||||
data: { confirm: 'Are you sure you want to cancel this invitation?', turbo_confirm: 'Are you sure you want to cancel this invitation?' },
|
||||
class: "btn btn-outline btn-warning btn-sm" do %>
|
||||
Cancel
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -227,9 +239,7 @@
|
|||
<!-- Family at capacity message -->
|
||||
<div class="border-t pt-4">
|
||||
<div class="alert alert-warning">
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<%= icon 'triangle-alert', class: "inline-block w-6 mr-2 flex-shrink-0" %>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium">
|
||||
Family at Capacity
|
||||
|
|
|
|||
|
|
@ -28,6 +28,17 @@
|
|||
</div>
|
||||
|
||||
<div class="flex space-x-2">
|
||||
<button type="button"
|
||||
data-controller="clipboard"
|
||||
data-clipboard-text-value="<%= public_invitation_url(invitation.token) %>"
|
||||
data-action="click->clipboard#copy"
|
||||
class="btn btn-ghost btn-sm text-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<%= t('family_invitations.index.copy_link', default: 'Copy Link') %>
|
||||
</button>
|
||||
|
||||
<%= link_to public_invitation_path(invitation.token),
|
||||
class: "btn btn-ghost btn-sm text-info" do %>
|
||||
<%= t('family_invitations.index.view_invitation', default: 'View') %>
|
||||
|
|
|
|||
|
|
@ -24,26 +24,29 @@
|
|||
|
||||
<body class='h-screen overflow-hidden relative'>
|
||||
<!-- Fixed Navbar -->
|
||||
<div class='fixed w-full z-50 bg-base-100 shadow-md h-16'>
|
||||
<div class='fixed w-full z-40 bg-base-100 shadow-md h-16'>
|
||||
<div class='container mx-auto h-full w-full flex items-center'>
|
||||
<%= render 'shared/navbar' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flash Messages - Fixed below navbar -->
|
||||
<div class='fixed top-16 w-full z-40 h-8'>
|
||||
<div class='fixed top-16 w-full z-50'>
|
||||
<div class='container mx-auto px-5'>
|
||||
<%= render 'shared/flash' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Navigation - Fixed below flash messages -->
|
||||
|
||||
<!-- Full Screen Map Container -->
|
||||
<div class='absolute top-40 left-0 right-0 bottom-0 w-full z-10 overflow-auto'>
|
||||
<div class='absolute top-16 left-0 right-0 w-full z-20' style='height: calc(100vh - 4rem);'>
|
||||
<%= yield %>
|
||||
</div>
|
||||
|
||||
<!-- Fixed Footer (hidden by default) -->
|
||||
<div id='map-footer' class='fixed bottom-0 left-0 right-0 z-30 hidden'>
|
||||
<%= render 'shared/legal_footer' %>
|
||||
</div>
|
||||
|
||||
<%= render 'map/onboarding_modal' %>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,87 +1,99 @@
|
|||
<% content_for :title, 'Map' %>
|
||||
|
||||
<div class="flex flex-col lg:flex-row lg:space-x-4 w-full">
|
||||
<div class='w-full'>
|
||||
<div class="flex flex-col space-y-4 mb-4 w-full" style="margin-top: 5rem;">
|
||||
<%= form_with url: map_path(import_id: params[:import_id]), method: :get do |f| %>
|
||||
<div class="flex flex-col space-y-4 sm:flex-row sm:space-y-0 sm:space-x-4 sm:items-end">
|
||||
<div class="w-full sm:w-1/12 md:w-1/12 lg:w-1/12">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<span class="tooltip" data-tip="<%= human_date(@start_at - 1.day) %>">
|
||||
<%= link_to map_path(start_at: @start_at - 1.day, end_at: @end_at - 1.day, import_id: params[:import_id]), class: "btn border border-base-300 hover:btn-ghost w-full" do %>
|
||||
<%= icon 'chevron-left' %>
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full sm:w-2/12 md:w-1/12 lg:w-2/12">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<%= f.label :start_at, class: "text-sm font-semibold" %>
|
||||
<%= f.datetime_local_field :start_at, include_seconds: false, class: "input input-bordered hover:cursor-pointer hover:input-primary", value: @start_at %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full sm:w-2/12 md:w-1/12 lg:w-2/12">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<%= f.label :end_at, class: "text-sm font-semibold" %>
|
||||
<%= f.datetime_local_field :end_at, include_seconds: false, class: "input input-bordered hover:cursor-pointer hover:input-primary", value: @end_at %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full sm:w-1/12 md:w-1/12 lg:w-1/12">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<span class="tooltip" data-tip="<%= human_date(@start_at + 1.day) %>">
|
||||
<%= link_to map_path(start_at: @start_at + 1.day, end_at: @end_at + 1.day, import_id: params[:import_id]), class: "btn border border-base-300 hover:btn-ghost w-full" do %>
|
||||
<%= icon 'chevron-right' %>
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full sm:w-6/12 md:w-2/12 lg:w-1/12">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<%= f.submit "Search", class: "btn btn-primary hover:btn-info" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full sm:w-6/12 md:w-2/12 lg:w-1/12">
|
||||
<div class="flex flex-col space-y-2 text-center">
|
||||
<%= link_to "Today",
|
||||
map_path(start_at: Time.current.beginning_of_day, end_at: Time.current.end_of_day, import_id: params[:import_id]),
|
||||
class: "btn border border-base-300 hover:btn-ghost" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full sm:w-6/12 md:w-3/12 lg:w-2/12">
|
||||
<div class="flex flex-col space-y-2 text-center">
|
||||
<%= link_to "Last 7 days", map_path(start_at: 1.week.ago.beginning_of_day, end_at: Time.current.end_of_day, import_id: params[:import_id]), class: "btn border border-base-300 hover:btn-ghost" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full sm:w-6/12 md:w-3/12 lg:w-2/12">
|
||||
<div class="flex flex-col space-y-2 text-center">
|
||||
<%= link_to "Last month", map_path(start_at: 1.month.ago.beginning_of_day, end_at: Time.current.end_of_day, import_id: params[:import_id]), class: "btn border border-base-300 hover:btn-ghost" %>
|
||||
</div>
|
||||
<!-- Floating Date Navigation Controls -->
|
||||
<div class="fixed top-20 left-0 right-0 flex justify-center" style="z-index: 9999; margin-left: 80px; margin-right: 80px;">
|
||||
<div style="width: 1500px; max-width: 100%;" data-controller="map-controls">
|
||||
<!-- Mobile: Compact Toggle Button -->
|
||||
<div class="lg:hidden justify-center flex">
|
||||
<button
|
||||
type="button"
|
||||
data-action="click->map-controls#toggle"
|
||||
class="btn btn-primary w-96 shadow-lg">
|
||||
<span data-map-controls-target="toggleIcon">
|
||||
<%= icon 'chevron-down' %>
|
||||
</span>
|
||||
<span class="ml-2"><%= human_date(@start_at) %></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Expandable Panel (hidden on mobile by default, always visible on desktop) -->
|
||||
<div
|
||||
data-map-controls-target="panel"
|
||||
class="hidden lg:!block bg-base-100 bg-opacity-95 rounded-lg shadow-lg p-4 mt-2 lg:mt-0 scale-80">
|
||||
<%= form_with url: map_path(import_id: params[:import_id]), method: :get do |f| %>
|
||||
<div class="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4 lg:items-end">
|
||||
<div class="w-full lg:w-1/12">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<span class="tooltip" data-tip="<%= human_date(@start_at - 1.day) %>">
|
||||
<%= link_to map_path(start_at: @start_at - 1.day, end_at: @end_at - 1.day, import_id: params[:import_id]), class: "btn border border-base-300 hover:btn-ghost w-full" do %>
|
||||
<%= icon 'chevron-left' %>
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div
|
||||
id='map'
|
||||
class="w-full z-0"
|
||||
data-controller="maps points add-visit family-members"
|
||||
data-points-target="map"
|
||||
data-api_key="<%= current_user.api_key %>"
|
||||
data-self_hosted="<%= @self_hosted %>"
|
||||
data-user_settings='<%= current_user.safe_settings.settings.to_json.html_safe %>'
|
||||
data-user_theme="<%= current_user&.theme || 'dark' %>"
|
||||
data-coordinates='<%= @coordinates.to_json.html_safe %>'
|
||||
data-tracks='<%= @tracks.to_json.html_safe %>'
|
||||
data-distance="<%= @distance %>"
|
||||
data-points_number="<%= @points_number %>"
|
||||
data-timezone="<%= Rails.configuration.time_zone %>"
|
||||
data-features='<%= @features.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 class="w-full lg:w-2/12 tooltip tooltip-bottom" data-tip="Start date and time">
|
||||
<%= f.datetime_local_field :start_at, include_seconds: false, class: "input input-bordered hover:cursor-pointer hover:input-primary w-full", value: @start_at %>
|
||||
</div>
|
||||
<div class="w-full lg:w-2/12 tooltip tooltip-bottom" data-tip="End date and time">
|
||||
<%= f.datetime_local_field :end_at, include_seconds: false, class: "input input-bordered hover:cursor-pointer hover:input-primary w-full", value: @end_at %>
|
||||
</div>
|
||||
<div class="w-full lg:w-1/12">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<span class="tooltip" data-tip="<%= human_date(@start_at + 1.day) %>">
|
||||
<%= link_to map_path(start_at: @start_at + 1.day, end_at: @end_at + 1.day, import_id: params[:import_id]), class: "btn border border-base-300 hover:btn-ghost w-full" do %>
|
||||
<%= icon 'chevron-right' %>
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full lg:w-1/12">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<%= f.submit "Search", class: "btn btn-primary hover:btn-info w-full" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full lg:w-1/12">
|
||||
<div class="flex flex-col space-y-2 text-center">
|
||||
<%= link_to "Today",
|
||||
map_path(start_at: Time.current.beginning_of_day, end_at: Time.current.end_of_day, import_id: params[:import_id]),
|
||||
class: "btn border border-base-300 hover:btn-ghost w-full" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full lg:w-2/12">
|
||||
<div class="flex flex-col space-y-2 text-center">
|
||||
<%= link_to "Last 7 days", map_path(start_at: 1.week.ago.beginning_of_day, end_at: Time.current.end_of_day, import_id: params[:import_id]), class: "btn border border-base-300 hover:btn-ghost w-full" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full lg:w-2/12">
|
||||
<div class="flex flex-col space-y-2 text-center">
|
||||
<%= link_to "Last month", map_path(start_at: 1.month.ago.beginning_of_day, end_at: Time.current.end_of_day, import_id: params[:import_id]), class: "btn border border-base-300 hover:btn-ghost w-full" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full Screen Map -->
|
||||
<div
|
||||
id='map'
|
||||
class="absolute inset-0 w-full h-full z-0"
|
||||
data-controller="maps points add-visit family-members"
|
||||
data-points-target="map"
|
||||
data-api_key="<%= current_user.api_key %>"
|
||||
data-self_hosted="<%= @self_hosted %>"
|
||||
data-user_settings='<%= current_user.safe_settings.settings.to_json %>'
|
||||
data-user_theme="<%= current_user&.theme || 'dark' %>"
|
||||
data-coordinates='<%= @coordinates.to_json.html_safe %>'
|
||||
data-tracks='<%= @tracks.to_json.html_safe %>'
|
||||
data-distance="<%= @distance %>"
|
||||
data-points_number="<%= @points_number %>"
|
||||
data-timezone="<%= Rails.configuration.time_zone %>"
|
||||
data-features='<%= @features.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="w-full h-full">
|
||||
<div id="fog" class="fog"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="navbar bg-base-100">
|
||||
<div class="navbar bg-base-100 h-16">
|
||||
<div class="navbar-start">
|
||||
<div class="dropdown">
|
||||
<label tabindex="0" class="btn btn-ghost lg:hidden">
|
||||
|
|
|
|||
|
|
@ -525,7 +525,7 @@ test.describe('Map Functionality', () => {
|
|||
|
||||
// Verify it's actually a clickable button with gear icon
|
||||
const buttonText = await settingsButton.textContent();
|
||||
expect(buttonText).toBe('⚙️');
|
||||
expect(buttonText).toBe('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cog-icon lucide-cog"><path d="M11 10.27 7 3.34"/><path d="m11 13.73-4 6.93"/><path d="M12 22v-2"/><path d="M12 2v2"/><path d="M14 12h8"/><path d="m17 20.66-1-1.73"/><path d="m17 3.34-1 1.73"/><path d="M2 12h2"/><path d="m20.66 17-1.73-1"/><path d="m20.66 7-1.73 1"/><path d="m3.34 17 1.73-1"/><path d="m3.34 7 1.73 1"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="12" r="8"/></svg>');
|
||||
|
||||
// Test opening settings panel
|
||||
await settingsButton.click();
|
||||
|
|
|
|||
Loading…
Reference in a new issue