feat(map): tap drops a pin + confirm card instead of jumping into create-spot

This commit is contained in:
SkateSpots 2026-10-07 10:26:36 +07:00
parent 0a5449d24f
commit 24322b3b1c
5 changed files with 99 additions and 2 deletions

View file

@ -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<HTMLDivElement>(null);
@ -814,6 +817,7 @@ export function SpotMap({
const markersRef = useRef<L.Marker[]>([]);
const btcMapMarkersRef = useRef<L.Marker[]>([]);
const userMarkerRef = useRef<L.Marker | null>(null);
const pendingPinMarkerRef = useRef<L.Marker | null>(null);
// MarkerClusterGroup for spot/shop/gear/tour markers
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const clusterGroupRef = useRef<any>(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:
'<div style="display:flex;flex-direction:column;align-items:center;transform:translateY(-4px)">' +
'<div style="width:22px;height:22px;border-radius:50%;background:#ff0040;border:3px solid #fff;box-shadow:0 2px 10px rgba(0,0,0,.55)"></div>' +
'<div style="width:0;height:0;border-left:7px solid transparent;border-right:7px solid transparent;border-top:11px solid #fff;margin-top:-1px"></div>' +
'</div>',
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;

View file

@ -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',

View file

@ -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',

View file

@ -110,6 +110,8 @@ const thBatch2 = {
addSpot: 'เพิ่มสปอต',
addShop: 'เพิ่มร้าน',
addEvent: 'เพิ่มอีเวนต์',
addSpotHereTitle: 'เพิ่มสปอตที่นี่?',
addSpotHereDesc: 'ปักหมุดตรงจุดที่คุณแตะ',
sellGear: 'ขาย Gear',
offerTour: 'เสนอทัวร์',
closeAddMenu: 'ปิดเมนูเพิ่ม',

View file

@ -606,6 +606,9 @@ export default function MapPage() {
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [selectedSpot, setSelectedSpot] = useState<SkateSpot | null>(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}
/>
</Suspense>
{/* 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 && (
<div className="pointer-events-none absolute inset-x-0 bottom-24 z-[1003] flex justify-center px-4 md:bottom-8">
<div className="pointer-events-auto flex w-full max-w-md items-center gap-3 rounded-2xl border-2 border-[#ff0040]/50 bg-black/90 px-3 py-2.5 shadow-2xl backdrop-blur-md">
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-[#ff0040]/20 text-lg">
📍
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-black uppercase tracking-wide text-white">
{t('mapUi.addSpotHereTitle')}
</p>
<p className="truncate text-xs text-gray-400">
{t('mapUi.addSpotHereDesc')}
</p>
</div>
<div className="flex flex-shrink-0 gap-1.5">
<Button
size="sm"
variant="ghost"
className="h-9 px-2 text-gray-400"
onClick={() => setPendingAddPin(null)}
>
{t('common.cancel')}
</Button>
<Button
size="sm"
className="h-9 gap-1 bg-[#ff0040] font-black uppercase hover:bg-[#ff0040]/90"
onClick={() => {
const pin = pendingAddPin;
setPendingAddPin(null);
navigate(`/create-spot?lat=${pin.lat.toFixed(6)}&lng=${pin.lng.toFixed(6)}`);
}}
>
<MapPin className="h-4 w-4" />
{t('mapUi.addSpot')}
</Button>
</div>
</div>
</div>
)}
{/* 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