diff --git a/src/components/SpotMap.tsx b/src/components/SpotMap.tsx index cc44d80..c1270f2 100644 --- a/src/components/SpotMap.tsx +++ b/src/components/SpotMap.tsx @@ -540,6 +540,8 @@ interface SpotMapProps { userLocation?: { lat: number; lng: number } | null; /** The current user's Nostr profile picture, used on the "you are here" pin. */ userPicture?: string | null; + /** A pending spot location the user tapped to add (shown before confirming). */ + pendingPin?: { lat: number; lng: number } | null; } function createPopupContent( @@ -807,6 +809,7 @@ export function SpotMap({ flyTo = null, userLocation = null, userPicture = null, + pendingPin = null, }: SpotMapProps) { const { t } = useTranslation(); const mapRef = useRef(null); @@ -814,6 +817,7 @@ export function SpotMap({ const markersRef = useRef([]); const btcMapMarkersRef = useRef([]); const userMarkerRef = useRef(null); + const pendingPinMarkerRef = useRef(null); // MarkerClusterGroup for spot/shop/gear/tour markers // eslint-disable-next-line @typescript-eslint/no-explicit-any const clusterGroupRef = useRef(null); @@ -1072,6 +1076,44 @@ export function SpotMap({ } }, [userLocation, userPicture, t]); + // Render / update the tap-to-add pending pin. The user taps the map, a pin + // drops here, and MapPage shows an "Add a spot here?" card — only on confirm + // do we leave the map. Without this a logged-in tap yanked you straight into + // the create form, which made the map feel broken. + useEffect(() => { + const map = mapInstanceRef.current; + if (!map) return; + + if (!pendingPin) { + if (pendingPinMarkerRef.current) { + pendingPinMarkerRef.current.remove(); + pendingPinMarkerRef.current = null; + } + return; + } + + const latlng: [number, number] = [pendingPin.lat, pendingPin.lng]; + if (pendingPinMarkerRef.current) { + pendingPinMarkerRef.current.setLatLng(latlng); + } else { + pendingPinMarkerRef.current = L.marker(latlng, { + icon: L.divIcon({ + className: '', + html: + '
' + + '
' + + '
' + + '
', + iconSize: [22, 34], + iconAnchor: [11, 34], + }), + zIndexOffset: 1200, + keyboard: false, + }).addTo(map); + } + map.panTo(latlng, { animate: true }); + }, [pendingPin]); + // Update markers useEffect(() => { const map = mapInstanceRef.current; diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 21e8557..6ab2786 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -980,6 +980,8 @@ mapUi: { addSpot: 'Add Spot', addShop: 'Add Shop', addEvent: 'Add Event', + addSpotHereTitle: 'Add a spot here?', + addSpotHereDesc: 'Drop a pin where you tapped', sellGear: 'Sell Gear', offerTour: 'Offer Tour', closeAddMenu: 'Close add menu', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index faf1371..60abdb7 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -980,6 +980,8 @@ mapUi: { addSpot: 'Añadir spot', addShop: 'Añadir tienda', addEvent: 'Añadir evento', + addSpotHereTitle: '¿Añadir un spot aquí?', + addSpotHereDesc: 'Coloca el pin donde tocaste', sellGear: 'Vender equipo', offerTour: 'Ofrecer ruta', closeAddMenu: 'Cerrar menú de añadir', diff --git a/src/i18n/th/batch2.ts b/src/i18n/th/batch2.ts index c39e13d..330a071 100644 --- a/src/i18n/th/batch2.ts +++ b/src/i18n/th/batch2.ts @@ -110,6 +110,8 @@ const thBatch2 = { addSpot: 'เพิ่มสปอต', addShop: 'เพิ่มร้าน', addEvent: 'เพิ่มอีเวนต์', + addSpotHereTitle: 'เพิ่มสปอตที่นี่?', + addSpotHereDesc: 'ปักหมุดตรงจุดที่คุณแตะ', sellGear: 'ขาย Gear', offerTour: 'เสนอทัวร์', closeAddMenu: 'ปิดเมนูเพิ่ม', diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx index 0fa0e43..b7f0fbb 100644 --- a/src/pages/MapPage.tsx +++ b/src/pages/MapPage.tsx @@ -606,6 +606,9 @@ export default function MapPage() { const [searchQuery, setSearchQuery] = useState(''); const [selectedCategories, setSelectedCategories] = useState([]); const [selectedSpot, setSelectedSpot] = useState(null); + // Tap-to-add: a logged-in tap drops this pin and shows a confirm card instead + // of yanking the user into the create form (which made the map feel broken). + const [pendingAddPin, setPendingAddPin] = useState<{ lat: number; lng: number } | null>(null); // Directions target lives at page level (NOT inside the spot Sheet) so the // RouteDialog is never nested inside another modal. const [routeTarget, setRouteTarget] = useState<{ lat: number; lng: number; label: string } | null>(null); @@ -916,8 +919,10 @@ export default function MapPage() { const handleMapClick = (lat: number, lng: number) => { if (!user) return; - // Navigate to create spot with coordinates pre-filled - navigate(`/create-spot?lat=${lat.toFixed(6)}&lng=${lng.toFixed(6)}`); + // Drop a pin and ask first — do NOT navigate immediately. The user confirms + // with the card below, which is the only thing that opens the create form. + setSelectedSpot(null); + setPendingAddPin({ lat, lng }); }; // Filter combined spots and shops @@ -1690,8 +1695,52 @@ export default function MapPage() { flyTo={flyTo} userLocation={isMobile && userLocation ? { lat: userLocation.lat, lng: userLocation.lon } : null} userPicture={userPicture} + pendingPin={pendingAddPin} /> + + {/* Tap-to-add confirm. Tapping the map drops a pin (above) and + shows this card; only "Add spot" opens the create form, so a + stray tap no longer kicks you off the map. */} + {pendingAddPin && ( +
+
+
+ 📍 +
+
+

+ {t('mapUi.addSpotHereTitle')} +

+

+ {t('mapUi.addSpotHereDesc')} +

+
+
+ + +
+
+
+ )} {/* Loading indicator — a slim, honest top bar. The map stays visible underneath and spots stream in; we NEVER cover the map with fake placeholder pins. An animated sweep sits on top of a