From e224befde7fdb5c11f2fa7524e98b25dce9b4102 Mon Sep 17 00:00:00 2001 From: Maurice <61554723+mauriceboe@users.noreply.github.com> Date: Fri, 12 Jun 2026 20:23:34 +0200 Subject: [PATCH] Map/planner/dashboard polish and small community features (#1155) * feat(planner): reorder days in a modal instead of a dropdown The day-reorder control opened a small anchored dropdown; move it into the shared Modal (portal, dimmed backdrop, Esc/backdrop close) so it matches the Add activity dialog. Drag handles, up/down arrows and the day badges are unchanged. * feat(map): explore reliability, Mapbox popups + compass, region-biased search POI explore: clamp oversized viewports, query the Overpass mirrors in parallel (first valid response wins) with a per-request timeout and a short-lived cache, and surface a retry when every mirror fails - so it returns results at any zoom instead of timing out. Mapbox renderer: add the place/POI hover popups (name, category, address, photo) the Leaflet map already had, plus a compass pill next to the explore pill that resets the view to north. /api/maps/search: accept an optional locationBias to fix foreign-region bias and expose Google's place types in the result. * feat(dashboard): list-view and mobile polish Use the Archived status label for the filter and show Open dates for trips without dates; drop the unused settings button next to the view toggle. Desktop list view renders the date as a stat-style block separated from the counts. Mobile list rows are stacked (slim cover banner + centred date), trip actions stay visible (touch has no hover), and the hero card's hover lift is disabled on touch; small spacing fix under the sidebar. * feat: small community-requested options Raise the plan-note subtitle limit to 250 characters and add more note icons. Expose is_archived and cover_image on the update_trip MCP tool. Add place coordinates to the PDF export. Allow creating a category from an existing to-do, and add a show/hide toggle on the admin password fields. * test(shared): bump day-note subtitle limit assertion to 250 * test: align specs with the new search param order and archive label Keep lang as the 3rd positional arg of the maps search controller so the existing unit test stays valid, and forward locationBias as the 4th. Add the now-used Popup to the MapViewGL mapbox mock, switch the dashboard archive-filter query to the Archived label, and expect the 4-arg search call. --- client/src/api/client.ts | 4 +- client/src/components/Map/MapCompassPill.tsx | 48 +++++ client/src/components/Map/MapViewGL.test.tsx | 6 + client/src/components/Map/MapViewGL.tsx | 36 +++- client/src/components/Map/PoiCategoryPill.tsx | 22 ++- client/src/components/Map/placePopup.ts | 68 ++++++++ client/src/components/Map/usePoiExplore.ts | 49 +++++- client/src/components/PDF/TripPDF.tsx | 1 + .../Planner/DayPlanSidebar.constants.ts | 13 ++ .../Planner/DayPlanSidebarNoteModal.tsx | 4 +- .../Planner/DayPlanSidebarToolbar.tsx | 19 +- .../components/Planner/DayReorderPopup.tsx | 164 +++++++++--------- client/src/components/Todo/TodoListPanel.tsx | 62 +++++-- client/src/index.css | 17 ++ client/src/pages/DashboardPage.test.tsx | 10 +- client/src/pages/DashboardPage.tsx | 7 +- client/src/pages/TripPlannerPage.tsx | 12 +- client/src/pages/admin/AdminUserModals.tsx | 54 ++++-- client/src/styles/dashboard.css | 33 +++- server/src/mcp/tools/trips.ts | 6 +- server/src/nest/maps/maps.controller.ts | 7 +- server/src/nest/maps/maps.service.ts | 4 +- server/src/services/mapsService.ts | 149 +++++++++++++--- .../tests/unit/nest/maps.controller.test.ts | 2 +- shared/src/day/day.schema.spec.ts | 4 +- shared/src/day/day.schema.ts | 4 +- shared/src/i18n/de/dashboard.ts | 2 +- shared/src/i18n/en/dashboard.ts | 2 +- 28 files changed, 623 insertions(+), 186 deletions(-) create mode 100644 client/src/components/Map/MapCompassPill.tsx create mode 100644 client/src/components/Map/placePopup.ts diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 5fe940ea..f74fa9d8 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -559,8 +559,10 @@ export const mapsApi = { reverse: (lat: number, lng: number, lang?: string) => apiClient.get('/maps/reverse', { params: { lat, lng, lang } }).then(r => checkInDev(mapsReverseResultSchema, r.data, 'maps.reverse')), resolveUrl: (url: string) => apiClient.post('/maps/resolve-url', { url }).then(r => checkInDev(mapsResolveUrlResultSchema, r.data, 'maps.resolveUrl')), // OSM-only POI explore: places of a category within the current map viewport bbox. + // Overpass can be slow on a fresh (uncached) area, so this call gets a longer + // timeout than the global default instead of aborting at 8s and showing nothing. pois: (category: string, bbox: { south: number; west: number; north: number; east: number }, signal?: AbortSignal) => - apiClient.get('/maps/pois', { params: { category, ...bbox }, signal }).then(r => r.data as { pois: import('../components/Map/poiCategories').Poi[]; source: string; truncated: boolean }), + apiClient.get('/maps/pois', { params: { category, ...bbox }, signal, timeout: 20000 }).then(r => r.data as { pois: import('../components/Map/poiCategories').Poi[]; source: string; truncated: boolean; clamped?: boolean }), } export const airportsApi = { diff --git a/client/src/components/Map/MapCompassPill.tsx b/client/src/components/Map/MapCompassPill.tsx new file mode 100644 index 00000000..65dc3289 --- /dev/null +++ b/client/src/components/Map/MapCompassPill.tsx @@ -0,0 +1,48 @@ +import { useEffect, useState } from 'react' +import { Navigation } from 'lucide-react' +import type mapboxgl from 'mapbox-gl' + +/** + * Round compass pill for the Mapbox planner map. The Mapbox map can be rotated and + * pitched, so this shows the current bearing (the arrow points to north) and snaps + * the camera back to north + flat on click. Rendered next to the POI "explore" pill + * (Mapbox only) and built as the SAME frosted shell (padding 4 around a 34px button) + * so its height and transparency match the POI pill exactly. + */ +export function MapCompassPill({ map }: { map: mapboxgl.Map }) { + const [bearing, setBearing] = useState(() => map.getBearing()) + + useEffect(() => { + const update = () => setBearing(map.getBearing()) + update() + map.on('rotate', update) + return () => { map.off('rotate', update) } + }, [map]) + + return ( +
+ +
+ ) +} diff --git a/client/src/components/Map/MapViewGL.test.tsx b/client/src/components/Map/MapViewGL.test.tsx index 5a305a70..5cd83aa6 100644 --- a/client/src/components/Map/MapViewGL.test.tsx +++ b/client/src/components/Map/MapViewGL.test.tsx @@ -40,6 +40,12 @@ vi.mock('mapbox-gl', () => ({ })), LngLatBounds: vi.fn(() => ({ extend: vi.fn().mockReturnThis() })), NavigationControl: vi.fn(), + Popup: vi.fn(() => ({ + setLngLat: vi.fn().mockReturnThis(), + setHTML: vi.fn().mockReturnThis(), + addTo: vi.fn().mockReturnThis(), + remove: vi.fn(), + })), }, })) vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({})) diff --git a/client/src/components/Map/MapViewGL.tsx b/client/src/components/Map/MapViewGL.tsx index ac7b593a..8c6ece11 100644 --- a/client/src/components/Map/MapViewGL.tsx +++ b/client/src/components/Map/MapViewGL.tsx @@ -13,6 +13,7 @@ import LocationButton from './LocationButton' import { useGeolocation } from '../../hooks/useGeolocation' import type { Place, Reservation } from '../../types' import { POI_CATEGORY_BY_KEY, type Poi } from './poiCategories' +import { buildPlacePopupHtml, buildPoiPopupHtml } from './placePopup' function categoryIconSvg(iconName: string | null | undefined, size: number): string { const IconComponent = (iconName && CATEGORY_ICON_MAP[iconName]) || CATEGORY_ICON_MAP['MapPin'] @@ -53,6 +54,7 @@ interface Props { pois?: Poi[] onPoiClick?: (poi: Poi) => void onViewportChange?: (bbox: { south: number; west: number; north: number; east: number }) => void + onMapReady?: (map: mapboxgl.Map | null) => void } function createMarkerElement(place: Place & { category_color?: string; category_icon?: string }, photoUrl: string | null, orderNumbers: number[] | null, selected: boolean): HTMLDivElement { @@ -167,6 +169,7 @@ export function MapViewGL({ pois = [], onPoiClick, onViewportChange, + onMapReady, }: Props) { const mapboxStyle = useSettingsStore(s => s.settings.mapbox_style || 'mapbox://styles/mapbox/standard') const mapboxToken = useSettingsStore(s => s.settings.mapbox_access_token || '') @@ -186,10 +189,15 @@ export function MapViewGL({ const onReservationClickRef = useRef(onReservationClick) onReservationClickRef.current = onReservationClick const poiMarkersRef = useRef([]) + // Single reusable hover popup (name/category/address card) shared by planned + // places and POI markers — mirrors the Leaflet map's hover tooltip. + const popupRef = useRef(null) const onPoiClickRef = useRef(onPoiClick) onPoiClickRef.current = onPoiClick const onViewportChangeRef = useRef(onViewportChange) onViewportChangeRef.current = onViewportChange + const onMapReadyRef = useRef(onMapReady) + onMapReadyRef.current = onMapReady const { position: userPosition, mode: trackingMode, error: trackingError, cycleMode: cycleTrackingMode, setMode: setTrackingMode } = useGeolocation() const onClickRefs = useRef({ marker: onMarkerClick, map: onMapClick, context: onMapContextMenu }) onClickRefs.current.marker = onMarkerClick @@ -212,6 +220,16 @@ export function MapViewGL({ projection: mapboxQuality ? 'globe' : 'mercator', }) mapRef.current = map + popupRef.current = new mapboxgl.Popup({ + closeButton: false, + closeOnClick: false, + offset: 18, + maxWidth: '240px', + className: 'trek-map-popup', + }) + // Hand the map out so the trip planner can render its own compass pill next to + // the POI pill (a custom round control instead of Mapbox's default top-right one). + onMapReadyRef.current?.(map) // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(window as any).__trek_map = map @@ -357,6 +375,8 @@ export function MapViewGL({ canvas.removeEventListener('auxclick', onAuxClick) markersRef.current.forEach(m => m.remove()) markersRef.current.clear() + if (popupRef.current) { popupRef.current.remove(); popupRef.current = null } + onMapReadyRef.current?.(null) if (reservationOverlayRef.current) { reservationOverlayRef.current.destroy() reservationOverlayRef.current = null @@ -430,6 +450,10 @@ export function MapViewGL({ useEffect(() => { const map = mapRef.current if (!map) return + // Markers are about to be rebuilt; drop any open hover popup first. A marker + // recreated under the pointer (e.g. when its photo streams in) never fires + // mouseleave, which would otherwise leave the popup orphaned on the map. + popupRef.current?.remove() const ids = new Set(places.map(p => p.id)) markersRef.current.forEach((marker, id) => { @@ -450,6 +474,12 @@ export function MapViewGL({ ev.stopPropagation() onClickRefs.current.marker?.(place.id) }) + el.addEventListener('mouseenter', () => { + popupRef.current?.setLngLat([place.lng, place.lat]) + .setHTML(buildPlacePopupHtml(place as Place & { category_color?: string; category_icon?: string; category_name?: string }, photoUrl)) + .addTo(map) + }) + el.addEventListener('mouseleave', () => { popupRef.current?.remove() }) // Recreate marker each time rather than patching internal state — // mapbox-gl's internal _element bookkeeping breaks under DOM swaps. const existing = markersRef.current.get(place.id) @@ -471,11 +501,15 @@ export function MapViewGL({ useEffect(() => { const map = mapRef.current if (!map || !mapReady) return + popupRef.current?.remove() // same orphan-popup guard as the place markers poiMarkersRef.current.forEach(m => m.remove()) poiMarkersRef.current = [] for (const poi of (pois as Poi[])) { const el = createPoiMarkerElement(poi.category) - el.title = poi.name + el.addEventListener('mouseenter', () => { + popupRef.current?.setLngLat([poi.lng, poi.lat]).setHTML(buildPoiPopupHtml(poi)).addTo(map) + }) + el.addEventListener('mouseleave', () => { popupRef.current?.remove() }) el.addEventListener('click', (ev) => { ev.stopPropagation(); onPoiClickRef.current?.(poi) }) const m = new mapboxgl.Marker({ element: el, anchor: 'center' }).setLngLat([poi.lng, poi.lat]).addTo(map) poiMarkersRef.current.push(m) diff --git a/client/src/components/Map/PoiCategoryPill.tsx b/client/src/components/Map/PoiCategoryPill.tsx index f68fbd81..2eaf4c0e 100644 --- a/client/src/components/Map/PoiCategoryPill.tsx +++ b/client/src/components/Map/PoiCategoryPill.tsx @@ -1,4 +1,4 @@ -import { RotateCw } from 'lucide-react' +import { RotateCw, AlertTriangle } from 'lucide-react' import { useTranslation } from '../../i18n' import { Tooltip } from '../shared/Tooltip' import { POI_CATEGORIES } from './poiCategories' @@ -7,6 +7,8 @@ interface Props { active: Set onToggle: (key: string) => void loadingKeys?: Set + /** categories whose last fetch failed → show a retry affordance */ + errorKeys?: Set /** true when the map moved since the last search → offer "search this area" */ moved?: boolean onSearchArea?: () => void @@ -15,8 +17,9 @@ interface Props { // Frosted, icon-only segmented control that floats over the map. Active segments // fill with the category colour (matching their markers); the label shows in a // custom tooltip on hover so the pill stays compact and never needs to scroll. -export default function PoiCategoryPill({ active, onToggle, loadingKeys, moved, onSearchArea }: Props) { +export default function PoiCategoryPill({ active, onToggle, loadingKeys, errorKeys, moved, onSearchArea }: Props) { const { t } = useTranslation() + const anyError = !!errorKeys && Array.from(active).some(k => errorKeys.has(k)) const frosted: React.CSSProperties = { background: 'var(--sidebar-bg)', @@ -40,6 +43,7 @@ export default function PoiCategoryPill({ active, onToggle, loadingKeys, moved, aria-label={t(cat.labelKey)} className={on ? '' : 'text-content-muted'} style={{ + position: 'relative', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 34, height: 34, borderRadius: 999, border: 'none', cursor: 'pointer', background: on ? cat.color : 'transparent', @@ -61,13 +65,19 @@ export default function PoiCategoryPill({ active, onToggle, loadingKeys, moved, ) : ( )} + {on && !loading && errorKeys?.has(cat.key) && ( + + )} ) })} - {moved && active.size > 0 && ( + {(moved || anyError) && active.size > 0 && ( )} diff --git a/client/src/components/Map/placePopup.ts b/client/src/components/Map/placePopup.ts new file mode 100644 index 00000000..ae79577e --- /dev/null +++ b/client/src/components/Map/placePopup.ts @@ -0,0 +1,68 @@ +import { createElement } from 'react' +import { renderToStaticMarkup } from 'react-dom/server' +import { CATEGORY_ICON_MAP } from '../shared/categoryIcons' +import { POI_CATEGORY_BY_KEY, type Poi } from './poiCategories' +import type { Place } from '../../types' + +// HTML builders for the Mapbox GL hover popup. The Leaflet map already shows a +// name/category/address card on hover (a cursor-following overlay); Mapbox GL has +// no equivalent, so these produce the same card as an HTML string for a +// mapboxgl.Popup. Kept framework-agnostic (plain strings) on purpose. + +type PlaceWithCategory = Place & { + category_color?: string | null + category_icon?: string | null + category_name?: string | null +} + +function esc(s: string | null | undefined): string { + if (!s) return '' + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') +} + +// Render a lucide category icon to an inline SVG string in the given colour. +function iconSvg(iconName: string | null | undefined, size: number, color: string): string { + const Icon = (iconName && CATEGORY_ICON_MAP[iconName]) || CATEGORY_ICON_MAP['MapPin'] + try { + return renderToStaticMarkup(createElement(Icon, { size, color, strokeWidth: 2 })) + } catch { + return '' + } +} + +// Only data: thumbnails and our own photo-proxy URLs are safe to drop straight +// into an — everything else is a fetch seed, not a displayable URL. +function isDisplayablePhoto(url: string | null | undefined): url is string { + return !!url && (url.startsWith('data:') || url.startsWith('/api/maps/place-photo/')) +} + +const CARD_OPEN = '
' +const NAME_STYLE = 'font-weight:600;font-size:12.5px;color:#111827;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' +const ADDR_STYLE = 'font-size:11px;color:#9ca3af;margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' + +/** Hover-popup card for a planned place: optional photo, name, category row, address. */ +export function buildPlacePopupHtml(place: PlaceWithCategory, photoUrl: string | null): string { + const img = isDisplayablePhoto(photoUrl) + ? `
` + : '' + const category = + place.category_name && place.category_icon + ? `
${iconSvg(place.category_icon, 11, place.category_color || '#6b7280')}${esc(place.category_name)}
` + : '' + const address = place.address ? `
${esc(place.address)}
` : '' + return `${CARD_OPEN}${img}
${esc(place.name)}
${category}${address}
` +} + +/** Hover-popup card for an OSM "explore" POI: category-coloured icon, name, address. */ +export function buildPoiPopupHtml(poi: Poi): string { + const cat = POI_CATEGORY_BY_KEY[poi.category] + const color = cat?.color || '#6b7280' + const icon = cat ? renderToStaticMarkup(createElement(cat.Icon, { size: 12, color, strokeWidth: 2 })) : '' + const head = `
${icon}${esc(poi.name)}
` + const address = poi.address ? `
${esc(poi.address)}
` : '' + return `${CARD_OPEN}${head}${address}` +} diff --git a/client/src/components/Map/usePoiExplore.ts b/client/src/components/Map/usePoiExplore.ts index d3c21a71..793d3c0f 100644 --- a/client/src/components/Map/usePoiExplore.ts +++ b/client/src/components/Map/usePoiExplore.ts @@ -4,6 +4,12 @@ import type { Poi } from './poiCategories' export interface Bbox { south: number; west: number; north: number; east: number } +// A request we cancelled on purpose (newer search superseded it) — not a failure. +function isAbortError(err: unknown): boolean { + const e = err as { name?: string; code?: string } | null + return e?.name === 'CanceledError' || e?.code === 'ERR_CANCELED' || e?.name === 'AbortError' +} + /** * State for the map POI "explore" pill. Toggling a category fetches its OSM POIs * for the current viewport; panning/zooming does NOT auto-refetch — it just marks @@ -15,12 +21,18 @@ export function usePoiExplore() { const [byCat, setByCat] = useState>({}) const [loadingKeys, setLoadingKeys] = useState>(() => new Set()) const [moved, setMoved] = useState(false) + // Categories whose last fetch genuinely failed (all Overpass mirrors down), so + // the pill can offer a retry instead of looking like "no places here". + const [errorKeys, setErrorKeys] = useState>(() => new Set()) const bboxRef = useRef(null) // activeRef always mirrors the latest active set so async callbacks (fetch // completions) can check whether a category is still wanted. const activeRef = useRef(active) activeRef.current = active + // One in-flight AbortController per category, so re-toggling / re-searching + // cancels the previous (possibly slow) Overpass request instead of racing it. + const abortRef = useRef>({}) const setLoading = useCallback((key: string, on: boolean) => setLoadingKeys(prev => { const next = new Set(prev) @@ -28,19 +40,41 @@ export function usePoiExplore() { return next }), []) + const setError = useCallback((key: string, on: boolean) => setErrorKeys(prev => { + if (on === prev.has(key)) return prev + const next = new Set(prev) + if (on) next.add(key); else next.delete(key) + return next + }), []) + const fetchCat = useCallback(async (key: string, bbox: Bbox) => { + abortRef.current[key]?.abort() + const ctrl = new AbortController() + abortRef.current[key] = ctrl setLoading(key, true) + setError(key, false) try { - const res = await mapsApi.pois(key, bbox) + const res = await mapsApi.pois(key, bbox, ctrl.signal) // Drop the result if the user toggled this category off while the (slow) // Overpass request was in flight — otherwise stale results re-appear. setByCat(prev => (activeRef.current.has(key) ? { ...prev, [key]: res.pois } : prev)) - } catch { + } catch (err) { + // A superseded request was aborted on purpose — leave its state untouched + // so the newer request owns the spinner and results. + if (isAbortError(err)) return + // A real failure (every Overpass mirror down/timed out): surface it instead + // of a silent empty so the user can retry rather than assume "no places". setByCat(prev => (activeRef.current.has(key) ? { ...prev, [key]: [] } : prev)) + if (activeRef.current.has(key)) setError(key, true) } finally { - setLoading(key, false) + // Only the latest controller for this key clears the spinner; a superseded + // one must not, or it would hide the newer request's in-flight state. + if (abortRef.current[key] === ctrl) { + setLoading(key, false) + delete abortRef.current[key] + } } - }, [setLoading]) + }, [setLoading, setError]) const onViewportChange = useCallback((bbox: Bbox) => { bboxRef.current = bbox @@ -53,6 +87,11 @@ export function usePoiExplore() { const toggle = useCallback((key: string) => { const isOnlyActive = activeRef.current.has(key) && activeRef.current.size === 1 setMoved(false) + setErrorKeys(new Set()) + // Switching to another category (or turning off) — cancel any in-flight + // fetches so their results can't land after the selection changed. + Object.values(abortRef.current).forEach(c => c.abort()) + abortRef.current = {} if (isOnlyActive) { setActive(new Set()) setByCat({}) @@ -72,5 +111,5 @@ export function usePoiExplore() { const pois = useMemo(() => Object.values(byCat).flat(), [byCat]) - return { active, pois, loadingKeys, moved, toggle, searchArea, onViewportChange } + return { active, pois, loadingKeys, errorKeys, moved, toggle, searchArea, onViewportChange } } diff --git a/client/src/components/PDF/TripPDF.tsx b/client/src/components/PDF/TripPDF.tsx index ccc95d9a..77ebf78c 100644 --- a/client/src/components/PDF/TripPDF.tsx +++ b/client/src/components/PDF/TripPDF.tsx @@ -293,6 +293,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor ${cat ? `${escHtml(cat.name)}` : ''} ${place.address ? `
${svgPin}${escHtml(place.address)}
` : ''} + ${(place.lat != null && place.lng != null) ? `
${Number(place.lat).toFixed(5)}, ${Number(place.lng).toFixed(5)}
` : ''} ${place.description ? `
${escHtml(place.description)}
` : ''} ${chips ? `
${chips}
` : ''} ${place.notes ? `
${escHtml(place.notes)}
` : ''} diff --git a/client/src/components/Planner/DayPlanSidebar.constants.ts b/client/src/components/Planner/DayPlanSidebar.constants.ts index 1c1f2cee..bfbc6307 100644 --- a/client/src/components/Planner/DayPlanSidebar.constants.ts +++ b/client/src/components/Planner/DayPlanSidebar.constants.ts @@ -2,6 +2,7 @@ import { FileText, Info, Clock, MapPin, Navigation, Train, Plane, Bus, Car, Ship, Coffee, Ticket, Star, Heart, Camera, Flag, Lightbulb, AlertTriangle, ShoppingBag, Bookmark, Hotel, Utensils, Users, Sailboat, Bike, CarTaxiFront, Route, + Wine, ParkingSquare, Fuel, Footprints, Mountain, Waves, Sun, Umbrella, Music, Landmark, Gift, } from 'lucide-react' export const RES_ICONS = { flight: Plane, hotel: Hotel, restaurant: Utensils, train: Train, car: Car, cruise: Ship, bus: Bus, ferry: Sailboat, bicycle: Bike, taxi: CarTaxiFront, transport_other: Route, event: Ticket, tour: Users, other: FileText } @@ -27,6 +28,18 @@ export const NOTE_ICONS = [ { id: 'AlertTriangle', Icon: AlertTriangle }, { id: 'ShoppingBag', Icon: ShoppingBag }, { id: 'Bookmark', Icon: Bookmark }, + { id: 'Utensils', Icon: Utensils }, + { id: 'Wine', Icon: Wine }, + { id: 'ParkingSquare', Icon: ParkingSquare }, + { id: 'Fuel', Icon: Fuel }, + { id: 'Footprints', Icon: Footprints }, + { id: 'Mountain', Icon: Mountain }, + { id: 'Waves', Icon: Waves }, + { id: 'Sun', Icon: Sun }, + { id: 'Umbrella', Icon: Umbrella }, + { id: 'Music', Icon: Music }, + { id: 'Landmark', Icon: Landmark }, + { id: 'Gift', Icon: Gift }, ] const NOTE_ICON_MAP = Object.fromEntries(NOTE_ICONS.map(({ id, Icon }) => [id, Icon])) export function getNoteIcon(iconId) { return NOTE_ICON_MAP[iconId] || FileText } diff --git a/client/src/components/Planner/DayPlanSidebarNoteModal.tsx b/client/src/components/Planner/DayPlanSidebarNoteModal.tsx index 87fc82e6..21dfd276 100644 --- a/client/src/components/Planner/DayPlanSidebarNoteModal.tsx +++ b/client/src/components/Planner/DayPlanSidebarNoteModal.tsx @@ -58,7 +58,7 @@ export function DayPlanSidebarNoteModal({ noteUi, setNoteUi, noteInputRef, cance />