mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Extract some styles
This commit is contained in:
parent
3f847ede4f
commit
af139f988d
9 changed files with 618 additions and 501 deletions
File diff suppressed because one or more lines are too long
120
app/assets/stylesheets/maps_v2.css
Normal file
120
app/assets/stylesheets/maps_v2.css
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/* Maps V2 Styles */
|
||||
|
||||
/* Loading Overlay */
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.loading-overlay.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #e5e7eb;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Popup Styles */
|
||||
.point-popup {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.popup-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.popup-row .label {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.popup-row .value {
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
/* Connection Indicator */
|
||||
.connection-indicator {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 8px 16px;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: none; /* Hidden by default, shown when family sharing is active */
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
z-index: 20;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
/* Show connection indicator when family sharing is active */
|
||||
.connection-indicator.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.indicator-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #ef4444;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.connection-indicator.connected .indicator-dot {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.connection-indicator.connected .indicator-text::before {
|
||||
content: 'Connected';
|
||||
}
|
||||
|
||||
.connection-indicator.disconnected .indicator-text::before {
|
||||
content: 'Connecting...';
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
286
app/assets/stylesheets/maps_v2_panel.css
Normal file
286
app/assets/stylesheets/maps_v2_panel.css
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
/* Maps V2 Control Panel Styles */
|
||||
|
||||
.map-control-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -480px; /* Hidden by default */
|
||||
width: 480px;
|
||||
height: 100%;
|
||||
background: oklch(var(--b1));
|
||||
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
|
||||
z-index: 9999;
|
||||
transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.map-control-panel.open {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* Vertical Tab Bar */
|
||||
.panel-tabs {
|
||||
width: 64px;
|
||||
background: oklch(var(--b2));
|
||||
border-right: 1px solid oklch(var(--bc) / 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
color: oklch(var(--bc) / 0.6);
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: oklch(var(--b3));
|
||||
color: oklch(var(--bc));
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: oklch(var(--p));
|
||||
color: oklch(var(--pc));
|
||||
}
|
||||
|
||||
.tab-btn.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 24px;
|
||||
background: oklch(var(--p));
|
||||
border-radius: 2px 0 0 2px;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Panel Content */
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid oklch(var(--bc) / 0.1);
|
||||
background: oklch(var(--b1));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: oklch(var(--bc));
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* Tab Content */
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
.panel-body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.panel-body::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.panel-body::-webkit-scrollbar-thumb {
|
||||
background: oklch(var(--bc) / 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.panel-body::-webkit-scrollbar-thumb:hover {
|
||||
background: oklch(var(--bc) / 0.3);
|
||||
}
|
||||
|
||||
/* Toggle Focus State - Remove all focus indicators */
|
||||
.toggle:focus,
|
||||
.toggle:focus-visible,
|
||||
.toggle:focus-within {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border-color: inherit !important;
|
||||
}
|
||||
|
||||
/* Override DaisyUI toggle focus styles */
|
||||
.toggle:focus-visible:checked,
|
||||
.toggle:checked:focus,
|
||||
.toggle:checked:focus-visible {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Ensure no outline on the toggle container */
|
||||
.form-control .toggle:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* Prevent indeterminate visual state on toggles */
|
||||
.toggle:indeterminate {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Ensure smooth toggle transitions without intermediate states */
|
||||
.toggle {
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle:checked {
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Remove any active/pressed state that might cause intermediate appearance */
|
||||
.toggle:active,
|
||||
.toggle:active:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Responsive Breakpoints */
|
||||
|
||||
/* Large tablets and smaller desktops (1024px - 1280px) */
|
||||
@media (max-width: 1280px) {
|
||||
.map-control-panel {
|
||||
width: 420px;
|
||||
right: -420px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablets (768px - 1024px) */
|
||||
@media (max-width: 1024px) {
|
||||
.map-control-panel {
|
||||
width: 380px;
|
||||
right: -380px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small tablets and large phones (640px - 768px) */
|
||||
@media (max-width: 768px) {
|
||||
.map-control-panel {
|
||||
width: 95%;
|
||||
right: -95%;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile phones (< 640px) */
|
||||
@media (max-width: 640px) {
|
||||
.map-control-panel {
|
||||
width: 100%;
|
||||
right: -100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.panel-tabs {
|
||||
width: 56px;
|
||||
padding: 12px 0;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* Reduce spacing on mobile */
|
||||
.space-y-4 > * + * {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.space-y-6 > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Very small phones (< 375px) */
|
||||
@media (max-width: 375px) {
|
||||
.panel-tabs {
|
||||
width: 52px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
1
app/assets/svg/icons/lucide/outline/map-pin-check.svg
Normal file
1
app/assets/svg/icons/lucide/outline/map-pin-check.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-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>
|
||||
|
After Width: | Height: | Size: 457 B |
130
app/javascript/controllers/maps_v2/map_data_manager.js
Normal file
130
app/javascript/controllers/maps_v2/map_data_manager.js
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import maplibregl from 'maplibre-gl'
|
||||
import { Toast } from 'maps_v2/components/toast'
|
||||
import { performanceMonitor } from 'maps_v2/utils/performance_monitor'
|
||||
|
||||
/**
|
||||
* Manages data loading and layer setup for the map
|
||||
*/
|
||||
export class MapDataManager {
|
||||
constructor(controller) {
|
||||
this.controller = controller
|
||||
this.map = controller.map
|
||||
this.dataLoader = controller.dataLoader
|
||||
this.layerManager = controller.layerManager
|
||||
this.filterManager = controller.filterManager
|
||||
this.eventHandlers = controller.eventHandlers
|
||||
}
|
||||
|
||||
/**
|
||||
* Load map data from API and setup layers
|
||||
* @param {string} startDate - Start date for data range
|
||||
* @param {string} endDate - End date for data range
|
||||
* @param {Object} options - Loading options
|
||||
*/
|
||||
async loadMapData(startDate, endDate, options = {}) {
|
||||
const {
|
||||
showLoading = true,
|
||||
fitBounds = true,
|
||||
showToast = true,
|
||||
onProgress = null
|
||||
} = options
|
||||
|
||||
performanceMonitor.mark('load-map-data')
|
||||
|
||||
if (showLoading) {
|
||||
this.controller.showLoading()
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch data from API
|
||||
const data = await this.dataLoader.fetchMapData(
|
||||
startDate,
|
||||
endDate,
|
||||
showLoading ? onProgress : null
|
||||
)
|
||||
|
||||
// Store visits for filtering
|
||||
this.filterManager.setAllVisits(data.visits)
|
||||
|
||||
// Setup layers
|
||||
await this._setupLayers(data)
|
||||
|
||||
// Fit bounds if requested
|
||||
if (fitBounds && data.points.length > 0) {
|
||||
this._fitMapToBounds(data.pointsGeoJSON)
|
||||
}
|
||||
|
||||
// Show success message
|
||||
if (showToast) {
|
||||
const pointText = data.points.length === 1 ? 'point' : 'points'
|
||||
Toast.success(`Loaded ${data.points.length} location ${pointText}`)
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('[MapDataManager] Failed to load map data:', error)
|
||||
Toast.error('Failed to load location data. Please try again.')
|
||||
throw error
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
this.controller.hideLoading()
|
||||
}
|
||||
const duration = performanceMonitor.measure('load-map-data')
|
||||
console.log(`[Performance] Map data loaded in ${duration}ms`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup all map layers with loaded data
|
||||
* @private
|
||||
*/
|
||||
async _setupLayers(data) {
|
||||
const addAllLayers = async () => {
|
||||
await this.layerManager.addAllLayers(
|
||||
data.pointsGeoJSON,
|
||||
data.routesGeoJSON,
|
||||
data.visitsGeoJSON,
|
||||
data.photosGeoJSON,
|
||||
data.areasGeoJSON,
|
||||
data.tracksGeoJSON,
|
||||
data.placesGeoJSON
|
||||
)
|
||||
|
||||
this.layerManager.setupLayerEventHandlers({
|
||||
handlePointClick: this.eventHandlers.handlePointClick.bind(this.eventHandlers),
|
||||
handleVisitClick: this.eventHandlers.handleVisitClick.bind(this.eventHandlers),
|
||||
handlePhotoClick: this.eventHandlers.handlePhotoClick.bind(this.eventHandlers),
|
||||
handlePlaceClick: this.eventHandlers.handlePlaceClick.bind(this.eventHandlers)
|
||||
})
|
||||
}
|
||||
|
||||
if (this.map.loaded()) {
|
||||
await addAllLayers()
|
||||
} else {
|
||||
this.map.once('load', async () => {
|
||||
await addAllLayers()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fit map to data bounds
|
||||
* @private
|
||||
*/
|
||||
_fitMapToBounds(geojson) {
|
||||
if (!geojson?.features?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const coordinates = geojson.features.map(f => f.geometry.coordinates)
|
||||
|
||||
const bounds = coordinates.reduce((bounds, coord) => {
|
||||
return bounds.extend(coord)
|
||||
}, new maplibregl.LngLatBounds(coordinates[0], coordinates[0]))
|
||||
|
||||
this.map.fitBounds(bounds, {
|
||||
padding: 50,
|
||||
maxZoom: 15
|
||||
})
|
||||
}
|
||||
}
|
||||
66
app/javascript/controllers/maps_v2/map_initializer.js
Normal file
66
app/javascript/controllers/maps_v2/map_initializer.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import maplibregl from 'maplibre-gl'
|
||||
import { getMapStyle } from 'maps_v2/utils/style_manager'
|
||||
|
||||
/**
|
||||
* Handles map initialization for Maps V2
|
||||
*/
|
||||
export class MapInitializer {
|
||||
/**
|
||||
* Initialize MapLibre map instance
|
||||
* @param {HTMLElement} container - The container element for the map
|
||||
* @param {Object} settings - Map settings (style, center, zoom)
|
||||
* @returns {Promise<maplibregl.Map>} The initialized map instance
|
||||
*/
|
||||
static async initialize(container, settings = {}) {
|
||||
const {
|
||||
mapStyle = 'streets',
|
||||
center = [0, 0],
|
||||
zoom = 2,
|
||||
showControls = true
|
||||
} = settings
|
||||
|
||||
const style = await getMapStyle(mapStyle)
|
||||
|
||||
const map = new maplibregl.Map({
|
||||
container,
|
||||
style,
|
||||
center,
|
||||
zoom
|
||||
})
|
||||
|
||||
if (showControls) {
|
||||
map.addControl(new maplibregl.NavigationControl(), 'top-right')
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* Fit map to bounds of GeoJSON features
|
||||
* @param {maplibregl.Map} map - The map instance
|
||||
* @param {Object} geojson - GeoJSON FeatureCollection
|
||||
* @param {Object} options - Fit bounds options
|
||||
*/
|
||||
static fitToBounds(map, geojson, options = {}) {
|
||||
const {
|
||||
padding = 50,
|
||||
maxZoom = 15
|
||||
} = options
|
||||
|
||||
if (!geojson?.features?.length) {
|
||||
console.warn('[MapInitializer] No features to fit bounds to')
|
||||
return
|
||||
}
|
||||
|
||||
const coordinates = geojson.features.map(f => f.geometry.coordinates)
|
||||
|
||||
const bounds = coordinates.reduce((bounds, coord) => {
|
||||
return bounds.extend(coord)
|
||||
}, new maplibregl.LngLatBounds(coordinates[0], coordinates[0]))
|
||||
|
||||
map.fitBounds(bounds, {
|
||||
padding,
|
||||
maxZoom
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { Controller } from '@hotwired/stimulus'
|
||||
import maplibregl from 'maplibre-gl'
|
||||
import { ApiClient } from 'maps_v2/services/api_client'
|
||||
import { SettingsManager } from 'maps_v2/utils/settings_manager'
|
||||
import { SearchManager } from 'maps_v2/utils/search_manager'
|
||||
import { Toast } from 'maps_v2/components/toast'
|
||||
import { performanceMonitor } from 'maps_v2/utils/performance_monitor'
|
||||
import { CleanupHelper } from 'maps_v2/utils/cleanup_helper'
|
||||
import { getMapStyle } from 'maps_v2/utils/style_manager'
|
||||
import { MapInitializer } from './maps_v2/map_initializer'
|
||||
import { MapDataManager } from './maps_v2/map_data_manager'
|
||||
import { LayerManager } from './maps_v2/layer_manager'
|
||||
import { DataLoader } from './maps_v2/data_loader'
|
||||
import { EventHandlers } from './maps_v2/event_handlers'
|
||||
|
|
@ -90,6 +90,7 @@ export default class extends Controller {
|
|||
this.dataLoader = new DataLoader(this.api, this.apiKeyValue)
|
||||
this.eventHandlers = new EventHandlers(this.map)
|
||||
this.filterManager = new FilterManager(this.dataLoader)
|
||||
this.mapDataManager = new MapDataManager(this)
|
||||
|
||||
// Initialize feature managers
|
||||
this.areaSelectionManager = new AreaSelectionManager(this)
|
||||
|
|
@ -129,16 +130,9 @@ export default class extends Controller {
|
|||
* Initialize MapLibre map
|
||||
*/
|
||||
async initializeMap() {
|
||||
const style = await getMapStyle(this.settings.mapStyle)
|
||||
|
||||
this.map = new maplibregl.Map({
|
||||
container: this.containerTarget,
|
||||
style: style,
|
||||
center: [0, 0],
|
||||
zoom: 2
|
||||
this.map = await MapInitializer.initialize(this.containerTarget, {
|
||||
mapStyle: this.settings.mapStyle
|
||||
})
|
||||
|
||||
this.map.addControl(new maplibregl.NavigationControl(), 'top-right')
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -167,88 +161,14 @@ export default class extends Controller {
|
|||
* Load map data from API
|
||||
*/
|
||||
async loadMapData(options = {}) {
|
||||
const {
|
||||
showLoading = true,
|
||||
fitBounds = true,
|
||||
showToast = true
|
||||
} = options
|
||||
|
||||
performanceMonitor.mark('load-map-data')
|
||||
|
||||
if (showLoading) {
|
||||
this.showLoading()
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await this.dataLoader.fetchMapData(
|
||||
this.startDateValue,
|
||||
this.endDateValue,
|
||||
showLoading ? this.updateLoadingProgress.bind(this) : null
|
||||
)
|
||||
|
||||
this.filterManager.setAllVisits(data.visits)
|
||||
|
||||
const addAllLayers = async () => {
|
||||
await this.layerManager.addAllLayers(
|
||||
data.pointsGeoJSON,
|
||||
data.routesGeoJSON,
|
||||
data.visitsGeoJSON,
|
||||
data.photosGeoJSON,
|
||||
data.areasGeoJSON,
|
||||
data.tracksGeoJSON,
|
||||
data.placesGeoJSON
|
||||
)
|
||||
|
||||
this.layerManager.setupLayerEventHandlers({
|
||||
handlePointClick: this.eventHandlers.handlePointClick.bind(this.eventHandlers),
|
||||
handleVisitClick: this.eventHandlers.handleVisitClick.bind(this.eventHandlers),
|
||||
handlePhotoClick: this.eventHandlers.handlePhotoClick.bind(this.eventHandlers),
|
||||
handlePlaceClick: this.eventHandlers.handlePlaceClick.bind(this.eventHandlers)
|
||||
})
|
||||
return this.mapDataManager.loadMapData(
|
||||
this.startDateValue,
|
||||
this.endDateValue,
|
||||
{
|
||||
...options,
|
||||
onProgress: this.updateLoadingProgress.bind(this)
|
||||
}
|
||||
|
||||
if (this.map.loaded()) {
|
||||
await addAllLayers()
|
||||
} else {
|
||||
this.map.once('load', async () => {
|
||||
await addAllLayers()
|
||||
})
|
||||
}
|
||||
|
||||
if (fitBounds && data.points.length > 0) {
|
||||
this.fitMapToBounds(data.pointsGeoJSON)
|
||||
}
|
||||
|
||||
if (showToast) {
|
||||
Toast.success(`Loaded ${data.points.length} location ${data.points.length === 1 ? 'point' : 'points'}`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load map data:', error)
|
||||
Toast.error('Failed to load location data. Please try again.')
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
this.hideLoading()
|
||||
}
|
||||
const duration = performanceMonitor.measure('load-map-data')
|
||||
console.log(`[Performance] Map data loaded in ${duration}ms`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fit map to data bounds
|
||||
*/
|
||||
fitMapToBounds(geojson) {
|
||||
const coordinates = geojson.features.map(f => f.geometry.coordinates)
|
||||
|
||||
const bounds = coordinates.reduce((bounds, coord) => {
|
||||
return bounds.extend(coord)
|
||||
}, new maplibregl.LngLatBounds(coordinates[0], coordinates[0]))
|
||||
|
||||
this.map.fitBounds(bounds, {
|
||||
padding: 50,
|
||||
maxZoom: 15
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -531,9 +531,9 @@
|
|||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<!-- Create a Visit Button -->
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
class="btn btn-sm btn-outline"
|
||||
data-action="click->maps-v2#startCreateVisit">
|
||||
<%= icon 'map-pin-plus' %>
|
||||
<%= icon 'map-pin-check' %>
|
||||
Create a Visit
|
||||
</button>
|
||||
|
||||
|
|
@ -635,289 +635,3 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.map-control-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -480px; /* Hidden by default */
|
||||
width: 480px;
|
||||
height: 100%;
|
||||
background: oklch(var(--b1));
|
||||
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
|
||||
z-index: 9999;
|
||||
transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.map-control-panel.open {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* Vertical Tab Bar */
|
||||
.panel-tabs {
|
||||
width: 64px;
|
||||
background: oklch(var(--b2));
|
||||
border-right: 1px solid oklch(var(--bc) / 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
color: oklch(var(--bc) / 0.6);
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: oklch(var(--b3));
|
||||
color: oklch(var(--bc));
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: oklch(var(--p));
|
||||
color: oklch(var(--pc));
|
||||
}
|
||||
|
||||
.tab-btn.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 24px;
|
||||
background: oklch(var(--p));
|
||||
border-radius: 2px 0 0 2px;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Panel Content */
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid oklch(var(--bc) / 0.1);
|
||||
background: oklch(var(--b1));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: oklch(var(--bc));
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* Tab Content */
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
.panel-body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.panel-body::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.panel-body::-webkit-scrollbar-thumb {
|
||||
background: oklch(var(--bc) / 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.panel-body::-webkit-scrollbar-thumb:hover {
|
||||
background: oklch(var(--bc) / 0.3);
|
||||
}
|
||||
|
||||
/* Toggle Focus State - Remove all focus indicators */
|
||||
.toggle:focus,
|
||||
.toggle:focus-visible,
|
||||
.toggle:focus-within {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border-color: inherit !important;
|
||||
}
|
||||
|
||||
/* Override DaisyUI toggle focus styles */
|
||||
.toggle:focus-visible:checked,
|
||||
.toggle:checked:focus,
|
||||
.toggle:checked:focus-visible {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Ensure no outline on the toggle container */
|
||||
.form-control .toggle:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* Prevent indeterminate visual state on toggles */
|
||||
.toggle:indeterminate {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Ensure smooth toggle transitions without intermediate states */
|
||||
.toggle {
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle:checked {
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Remove any active/pressed state that might cause intermediate appearance */
|
||||
.toggle:active,
|
||||
.toggle:active:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Responsive Breakpoints */
|
||||
|
||||
/* Large tablets and smaller desktops (1024px - 1280px) */
|
||||
@media (max-width: 1280px) {
|
||||
.map-control-panel {
|
||||
width: 420px;
|
||||
right: -420px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablets (768px - 1024px) */
|
||||
@media (max-width: 1024px) {
|
||||
.map-control-panel {
|
||||
width: 380px;
|
||||
right: -380px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small tablets and large phones (640px - 768px) */
|
||||
@media (max-width: 768px) {
|
||||
.map-control-panel {
|
||||
width: 90%;
|
||||
right: -90%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile phones (< 640px) */
|
||||
@media (max-width: 640px) {
|
||||
.map-control-panel {
|
||||
width: 100%;
|
||||
right: -100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.panel-tabs {
|
||||
width: 56px;
|
||||
padding: 12px 0;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* Reduce spacing on mobile */
|
||||
.space-y-4 > * + * {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.space-y-6 > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Very small phones (< 375px) */
|
||||
@media (max-width: 375px) {
|
||||
.panel-tabs {
|
||||
width: 52px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -47,123 +47,3 @@
|
|||
<!-- Place creation modal (shared) -->
|
||||
<%= render 'shared/place_creation_modal' %>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.loading-overlay.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #e5e7eb;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Popup styles */
|
||||
.point-popup {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.popup-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.popup-row .label {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.popup-row .value {
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
/* Connection indicator styles */
|
||||
.connection-indicator {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 8px 16px;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: none; /* Hidden by default, shown when family sharing is active */
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
z-index: 20;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
/* Show connection indicator when family sharing is active */
|
||||
.connection-indicator.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.indicator-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #ef4444;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.connection-indicator.connected .indicator-dot {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.connection-indicator.connected .indicator-text::before {
|
||||
content: 'Connected';
|
||||
}
|
||||
|
||||
.connection-indicator.disconnected .indicator-text::before {
|
||||
content: 'Connecting...';
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue