mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 14:51:45 +00:00
v2.5.4 — Smart map zoom & place files in reservations
- Map auto-fits to day places when selecting a day or place - Dynamic padding accounts for sidebars and place inspector overlay - Place-based reservations now show linked files in the bookings tab - Increased max zoom to 16 for closer detail on nearby places
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nomad-client",
|
"name": "nomad-client",
|
||||||
"version": "2.5.3",
|
"version": "2.5.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState, useMemo } from 'react'
|
||||||
import { MapContainer, TileLayer, Marker, Tooltip, Polyline, useMap } from 'react-leaflet'
|
import { MapContainer, TileLayer, Marker, Tooltip, Polyline, useMap } from 'react-leaflet'
|
||||||
import MarkerClusterGroup from 'react-leaflet-cluster'
|
import MarkerClusterGroup from 'react-leaflet-cluster'
|
||||||
import L from 'leaflet'
|
import L from 'leaflet'
|
||||||
@@ -89,19 +89,26 @@ function createPlaceIcon(place, orderNumber, isSelected) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectionController({ places, selectedPlaceId }) {
|
function SelectionController({ places, selectedPlaceId, dayPlaces, paddingOpts }) {
|
||||||
const map = useMap()
|
const map = useMap()
|
||||||
const prev = useRef(null)
|
const prev = useRef(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedPlaceId && selectedPlaceId !== prev.current) {
|
if (selectedPlaceId && selectedPlaceId !== prev.current) {
|
||||||
const place = places.find(p => p.id === selectedPlaceId)
|
// Fit all day places into view (so you see context), but ensure selected is visible
|
||||||
if (place?.lat && place?.lng) {
|
const toFit = dayPlaces.length > 0 ? dayPlaces : places.filter(p => p.id === selectedPlaceId)
|
||||||
map.panTo([place.lat, place.lng], { animate: true, duration: 0.5 })
|
const withCoords = toFit.filter(p => p.lat && p.lng)
|
||||||
|
if (withCoords.length > 0) {
|
||||||
|
try {
|
||||||
|
const bounds = L.latLngBounds(withCoords.map(p => [p.lat, p.lng]))
|
||||||
|
if (bounds.isValid()) {
|
||||||
|
map.fitBounds(bounds, { ...paddingOpts, maxZoom: 16, animate: true })
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prev.current = selectedPlaceId
|
prev.current = selectedPlaceId
|
||||||
}, [selectedPlaceId, places, map])
|
}, [selectedPlaceId, places, dayPlaces, paddingOpts, map])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -121,7 +128,7 @@ function MapController({ center, zoom }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fit bounds when places change (fitKey triggers re-fit)
|
// Fit bounds when places change (fitKey triggers re-fit)
|
||||||
function BoundsController({ places, fitKey }) {
|
function BoundsController({ places, fitKey, paddingOpts }) {
|
||||||
const map = useMap()
|
const map = useMap()
|
||||||
const prevFitKey = useRef(-1)
|
const prevFitKey = useRef(-1)
|
||||||
|
|
||||||
@@ -131,9 +138,9 @@ function BoundsController({ places, fitKey }) {
|
|||||||
if (places.length === 0) return
|
if (places.length === 0) return
|
||||||
try {
|
try {
|
||||||
const bounds = L.latLngBounds(places.map(p => [p.lat, p.lng]))
|
const bounds = L.latLngBounds(places.map(p => [p.lat, p.lng]))
|
||||||
if (bounds.isValid()) map.fitBounds(bounds, { padding: [60, 60], maxZoom: 15, animate: true })
|
if (bounds.isValid()) map.fitBounds(bounds, { ...paddingOpts, maxZoom: 16, animate: true })
|
||||||
} catch {}
|
} catch {}
|
||||||
}, [fitKey, places, map])
|
}, [fitKey, places, paddingOpts, map])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -153,6 +160,7 @@ const mapPhotoCache = new Map()
|
|||||||
|
|
||||||
export function MapView({
|
export function MapView({
|
||||||
places = [],
|
places = [],
|
||||||
|
dayPlaces = [],
|
||||||
route = null,
|
route = null,
|
||||||
selectedPlaceId = null,
|
selectedPlaceId = null,
|
||||||
onMarkerClick,
|
onMarkerClick,
|
||||||
@@ -162,7 +170,20 @@ export function MapView({
|
|||||||
tileUrl = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
|
tileUrl = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
|
||||||
fitKey = 0,
|
fitKey = 0,
|
||||||
dayOrderMap = {},
|
dayOrderMap = {},
|
||||||
|
leftWidth = 0,
|
||||||
|
rightWidth = 0,
|
||||||
|
hasInspector = false,
|
||||||
}) {
|
}) {
|
||||||
|
// Dynamic padding: account for sidebars + bottom inspector
|
||||||
|
const paddingOpts = useMemo(() => {
|
||||||
|
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
|
||||||
|
if (isMobile) return { padding: [40, 20] }
|
||||||
|
const top = 60
|
||||||
|
const bottom = hasInspector ? 320 : 60
|
||||||
|
const left = leftWidth + 40
|
||||||
|
const right = rightWidth + 40
|
||||||
|
return { paddingTopLeft: [left, top], paddingBottomRight: [right, bottom] }
|
||||||
|
}, [leftWidth, rightWidth, hasInspector])
|
||||||
const [photoUrls, setPhotoUrls] = useState({})
|
const [photoUrls, setPhotoUrls] = useState({})
|
||||||
|
|
||||||
// Fetch Google photos for places that have google_place_id but no image_url
|
// Fetch Google photos for places that have google_place_id but no image_url
|
||||||
@@ -200,8 +221,8 @@ export function MapView({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<MapController center={center} zoom={zoom} />
|
<MapController center={center} zoom={zoom} />
|
||||||
<BoundsController places={places} fitKey={fitKey} />
|
<BoundsController places={dayPlaces.length > 0 ? dayPlaces : places} fitKey={fitKey} paddingOpts={paddingOpts} />
|
||||||
<SelectionController places={places} selectedPlaceId={selectedPlaceId} />
|
<SelectionController places={places} selectedPlaceId={selectedPlaceId} dayPlaces={dayPlaces} paddingOpts={paddingOpts} />
|
||||||
<MapClickHandler onClick={onMapClick} />
|
<MapClickHandler onClick={onMapClick} />
|
||||||
|
|
||||||
<MarkerClusterGroup
|
<MarkerClusterGroup
|
||||||
|
|||||||
@@ -245,13 +245,14 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function PlaceReservationCard({ item, tripId }) {
|
function PlaceReservationCard({ item, tripId, files = [], onNavigateToFiles }) {
|
||||||
const { updatePlace } = useTripStore()
|
const { updatePlace } = useTripStore()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { t, locale } = useTranslation()
|
const { t, locale } = useTranslation()
|
||||||
const timeFormat = useSettingsStore(s => s.settings.time_format) || '24h'
|
const timeFormat = useSettingsStore(s => s.settings.time_format) || '24h'
|
||||||
const [editing, setEditing] = useState(false)
|
const [editing, setEditing] = useState(false)
|
||||||
const confirmed = item.status === 'confirmed'
|
const confirmed = item.status === 'confirmed'
|
||||||
|
const placeFiles = files.filter(f => f.place_id === item.placeId)
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!confirm(t('reservations.confirm.remove', { name: item.title }))) return
|
if (!confirm(t('reservations.confirm.remove', { name: item.title }))) return
|
||||||
@@ -322,6 +323,26 @@ function PlaceReservationCard({ item, tripId }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{item.notes && <p style={{ margin: '7px 0 0', fontSize: 11.5, color: 'var(--text-muted)', lineHeight: 1.5, borderTop: '1px solid var(--border-secondary)', paddingTop: 7 }}>{item.notes}</p>}
|
{item.notes && <p style={{ margin: '7px 0 0', fontSize: 11.5, color: 'var(--text-muted)', lineHeight: 1.5, borderTop: '1px solid var(--border-secondary)', paddingTop: 7 }}>{item.notes}</p>}
|
||||||
|
|
||||||
|
{/* Files attached to the place */}
|
||||||
|
{placeFiles.length > 0 && (
|
||||||
|
<div style={{ marginTop: 8, borderTop: '1px solid var(--border-secondary)', paddingTop: 8, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
|
{placeFiles.map(f => (
|
||||||
|
<div key={f.id} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
<FileText size={11} style={{ color: 'var(--text-faint)', flexShrink: 0 }} />
|
||||||
|
<span style={{ fontSize: 11.5, color: 'var(--text-secondary)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</span>
|
||||||
|
<a href={f.url} target="_blank" rel="noreferrer" style={{ display: 'flex', color: 'var(--text-faint)', flexShrink: 0 }} title={t('common.open')}>
|
||||||
|
<ExternalLink size={11} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{onNavigateToFiles && (
|
||||||
|
<button onClick={onNavigateToFiles} style={{ alignSelf: 'flex-start', fontSize: 11, color: 'var(--text-muted)', background: 'none', border: 'none', cursor: 'pointer', padding: 0, textDecoration: 'underline', fontFamily: 'inherit' }}>
|
||||||
|
{t('reservations.showFiles')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -388,7 +409,7 @@ export default function ReservationsPanel({ tripId, reservations, days, assignme
|
|||||||
const total = allPending.length + allConfirmed.length
|
const total = allPending.length + allConfirmed.length
|
||||||
|
|
||||||
function renderCard(r) {
|
function renderCard(r) {
|
||||||
if (r._placeRes) return <PlaceReservationCard key={r.id} item={r} tripId={tripId} />
|
if (r._placeRes) return <PlaceReservationCard key={r.id} item={r} tripId={tripId} files={files} onNavigateToFiles={onNavigateToFiles} />
|
||||||
return <ReservationCard key={r.id} r={r} tripId={tripId} onEdit={onEdit} onDelete={onDelete} files={files} onNavigateToFiles={onNavigateToFiles} />
|
return <ReservationCard key={r.id} r={r} tripId={tripId} onEdit={onEdit} onDelete={onDelete} files={files} onNavigateToFiles={onNavigateToFiles} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -258,6 +258,13 @@ export default function TripPlannerPage() {
|
|||||||
return map
|
return map
|
||||||
}, [selectedDayId, assignments])
|
}, [selectedDayId, assignments])
|
||||||
|
|
||||||
|
// Places assigned to selected day (with coords) — used for map fitting
|
||||||
|
const dayPlaces = useMemo(() => {
|
||||||
|
if (!selectedDayId) return []
|
||||||
|
const da = assignments[String(selectedDayId)] || []
|
||||||
|
return da.map(a => a.place).filter(p => p?.lat && p?.lng)
|
||||||
|
}, [selectedDayId, assignments])
|
||||||
|
|
||||||
const mapTileUrl = settings.map_tile_url || 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
|
const mapTileUrl = settings.map_tile_url || 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
|
||||||
const defaultCenter = [settings.default_lat || 48.8566, settings.default_lng || 2.3522]
|
const defaultCenter = [settings.default_lat || 48.8566, settings.default_lng || 2.3522]
|
||||||
const defaultZoom = settings.default_zoom || 10
|
const defaultZoom = settings.default_zoom || 10
|
||||||
@@ -323,6 +330,7 @@ export default function TripPlannerPage() {
|
|||||||
<div style={{ position: 'absolute', inset: 0 }}>
|
<div style={{ position: 'absolute', inset: 0 }}>
|
||||||
<MapView
|
<MapView
|
||||||
places={mapPlaces()}
|
places={mapPlaces()}
|
||||||
|
dayPlaces={dayPlaces}
|
||||||
route={route}
|
route={route}
|
||||||
selectedPlaceId={selectedPlaceId}
|
selectedPlaceId={selectedPlaceId}
|
||||||
onMarkerClick={handleMarkerClick}
|
onMarkerClick={handleMarkerClick}
|
||||||
@@ -332,6 +340,9 @@ export default function TripPlannerPage() {
|
|||||||
tileUrl={mapTileUrl}
|
tileUrl={mapTileUrl}
|
||||||
fitKey={fitKey}
|
fitKey={fitKey}
|
||||||
dayOrderMap={dayOrderMap}
|
dayOrderMap={dayOrderMap}
|
||||||
|
leftWidth={leftCollapsed ? 0 : leftWidth}
|
||||||
|
rightWidth={rightCollapsed ? 0 : rightWidth}
|
||||||
|
hasInspector={!!selectedPlace}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{routeInfo && (
|
{routeInfo && (
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nomad-server",
|
"name": "nomad-server",
|
||||||
"version": "2.5.3",
|
"version": "2.5.4",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/index.js",
|
"start": "node src/index.js",
|
||||||
|
|||||||
Reference in New Issue
Block a user