From 894b3eb045c7d2c2acce847c34a85295c909dcf7 Mon Sep 17 00:00:00 2001 From: boojack Date: Mon, 6 Apr 2026 08:45:54 +0800 Subject: [PATCH] fix(map): refine Leaflet controls and memo map styling --- .../components/UserMemoMap/UserMemoMap.tsx | 78 +++++++++++++------ web/src/components/map/LocationPicker.tsx | 71 ++++++++--------- web/src/components/map/map-utils.tsx | 13 +++- 3 files changed, 98 insertions(+), 64 deletions(-) diff --git a/web/src/components/UserMemoMap/UserMemoMap.tsx b/web/src/components/UserMemoMap/UserMemoMap.tsx index 79fa4de82..ff8ce8edd 100644 --- a/web/src/components/UserMemoMap/UserMemoMap.tsx +++ b/web/src/components/UserMemoMap/UserMemoMap.tsx @@ -25,8 +25,8 @@ interface ClusterGroup { const createClusterCustomIcon = (cluster: ClusterGroup) => { return new DivIcon({ - html: `${cluster.getChildCount()}`, - className: "custom-marker-cluster", + html: `${cluster.getChildCount()}`, + className: "border-none bg-transparent", iconSize: L.point(32, 32, true), }); }; @@ -67,17 +67,41 @@ const UserMemoMap = ({ creator, className }: Props) => { const defaultCenter = { lat: 48.8566, lng: 2.3522 }; return ( -
+
{memosWithLocation.length === 0 && (
-
- -

No location data found

+
+ +

No location data found

)} - +
+
+ + + +
+

Mapped memos

+

{memosWithLocation.length} places pinned

+
+
+
+ + { > {memosWithLocation.map((memo) => ( - -
-
- - {memo.displayTime && - timestampDate(memo.displayTime).toLocaleDateString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - })} - + +
+
+
+ + Memo + + + {memo.displayTime && + timestampDate(memo.displayTime).toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + })} + +
- View - + Open +
-
{memo.snippet || "No content"}
+
+
{memo.snippet || "No content"}
+
+ {memo.location!.latitude.toFixed(2)}°, {memo.location!.longitude.toFixed(2)}° +
+
diff --git a/web/src/components/map/LocationPicker.tsx b/web/src/components/map/LocationPicker.tsx index 3315780dd..cdcf90a54 100644 --- a/web/src/components/map/LocationPicker.tsx +++ b/web/src/components/map/LocationPicker.tsx @@ -1,7 +1,7 @@ import L, { LatLng } from "leaflet"; import { ExternalLinkIcon, MinusIcon, PlusIcon } from "lucide-react"; import { type ReactNode, useEffect, useRef, useState } from "react"; -import { createRoot } from "react-dom/client"; +import { createPortal } from "react-dom"; import { MapContainer, Marker, useMap, useMapEvents } from "react-leaflet"; import { cn } from "@/lib/utils"; import { defaultMarkerIcon, ThemedTileLayer } from "./map-utils"; @@ -135,7 +135,7 @@ interface MapControlsProps { const MapControls = ({ position }: MapControlsProps) => { const map = useMap(); const controlRef = useRef(null); - const rootRef = useRef | null>(null); + const [container, setContainer] = useState(null); const handleOpenInGoogleMaps = () => { if (!position) return; @@ -156,39 +156,25 @@ const MapControls = ({ position }: MapControlsProps) => { const control = new MapControlsContainer({ position: "topright" }); controlRef.current = control; control.addTo(map); - - // Get container and render React component into it - const container = control.getContainer(); - if (container) { - rootRef.current = createRoot(container); - rootRef.current.render( - , - ); - } + setContainer(control.getContainer() ?? null); return () => { - // Cleanup: unmount React component and remove control - if (rootRef.current) { - rootRef.current.unmount(); - rootRef.current = null; - } if (controlRef.current) { controlRef.current.remove(); controlRef.current = null; } + setContainer(null); }; }, [map]); - // Update rendered content when position changes - useEffect(() => { - if (rootRef.current) { - rootRef.current.render( - , - ); - } - }, [position]); + if (!container) { + return null; + } - return null; + return createPortal( + , + container, + ); }; const MapCleanup = () => { @@ -222,21 +208,30 @@ const DEFAULT_CENTER_LAT_LNG = new LatLng(48.8584, 2.2945); const LeafletMap = (props: MapProps) => { const position = props.latlng || DEFAULT_CENTER_LAT_LNG; + const statusLabel = props.readonly ? "Pinned location" : props.latlng ? "Selected location" : "Choose a location"; return ( - - - {}} /> - - - +
+ + + {}} /> + + + + +
+
+ {statusLabel} +
+
+
); }; diff --git a/web/src/components/map/map-utils.tsx b/web/src/components/map/map-utils.tsx index 984cf2e36..fe7c490d7 100644 --- a/web/src/components/map/map-utils.tsx +++ b/web/src/components/map/map-utils.tsx @@ -7,7 +7,7 @@ import { useAuth } from "@/contexts/AuthContext"; import { resolveTheme } from "@/utils/theme"; const TILE_URLS = { - light: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + light: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", dark: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", } as const; @@ -24,12 +24,17 @@ interface MarkerIconOptions { } export const createMarkerIcon = (options?: MarkerIconOptions): DivIcon => { - const { fill = "orange", size = 28, className = "" } = options || {}; + const { fill = "var(--primary)", size = 28, className = "" } = options || {}; return new DivIcon({ - className: "relative border-none", + className: "relative border-none bg-transparent", html: ReactDOMServer.renderToString( - , +
+ +
, ), + iconSize: [size + 8, size + 8], + iconAnchor: [(size + 8) / 2, size + 4], + popupAnchor: [0, -(size * 0.7)], }); };