feat(map): tap drops a pin + confirm card instead of jumping into create-spot
This commit is contained in:
parent
0a5449d24f
commit
24322b3b1c
5 changed files with 99 additions and 2 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ const thBatch2 = {
|
|||
addSpot: 'เพิ่มสปอต',
|
||||
addShop: 'เพิ่มร้าน',
|
||||
addEvent: 'เพิ่มอีเวนต์',
|
||||
addSpotHereTitle: 'เพิ่มสปอตที่นี่?',
|
||||
addSpotHereDesc: 'ปักหมุดตรงจุดที่คุณแตะ',
|
||||
sellGear: 'ขาย Gear',
|
||||
offerTour: 'เสนอทัวร์',
|
||||
closeAddMenu: 'ปิดเมนูเพิ่ม',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue