diff --git a/src/components/SpotMap.tsx b/src/components/SpotMap.tsx index de68094..3bf49b3 100644 --- a/src/components/SpotMap.tsx +++ b/src/components/SpotMap.tsx @@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { MapPin } from 'lucide-react'; -import type { SkateSpot, MarketplaceItem, SkateTour } from '@/lib/types'; +import type { SkateSpot, MarketplaceItem, SkateTour, SkateEvent } from '@/lib/types'; import { SPOT_TYPE_LABELS, formatPrice } from '@/lib/types'; import type { SpotWeatherData } from '@/hooks/useSpotWeather'; import type { BtcMapPlace } from '@/hooks/useBtcMapPlaces'; @@ -209,6 +209,32 @@ const createTourIcon = () => L.divIcon({ popupAnchor: [0, -40], }); +// Custom event marker icon (lime β€” matches the Events section + Add Event action) +const createEventIcon = () => L.divIcon({ + className: 'event-marker', + html: ` +
+ + + +
+ `, + iconSize: [38, 38], + iconAnchor: [19, 38], + popupAnchor: [0, -38], +}); + // Custom BTC Map marker icon (for Bitcoin-accepting places) const createBtcMapIcon = (isBoosted?: boolean) => L.divIcon({ className: 'btcmap-marker', @@ -305,6 +331,7 @@ interface SpotMapProps { spots: SkateSpot[]; marketplaceItems?: MarketplaceItem[]; tours?: SkateTour[]; + events?: SkateEvent[]; btcMapPlaces?: BtcMapPlace[]; center?: [number, number]; zoom?: number; @@ -320,6 +347,7 @@ interface SpotMapProps { showShops?: boolean; showGear?: boolean; showTours?: boolean; + showEvents?: boolean; showBtcMap?: boolean; flyTo?: { lat: number; lng: number; zoom?: number } | null; } @@ -547,6 +575,7 @@ export function SpotMap({ spots, marketplaceItems = [], tours = [], + events = [], btcMapPlaces = [], center = [40.7128, -74.006], // Default to NYC zoom = 3, @@ -562,6 +591,7 @@ export function SpotMap({ showShops = true, showGear = true, showTours = true, + showEvents = false, showBtcMap = false, flyTo = null, }: SpotMapProps) { @@ -1029,7 +1059,73 @@ export function SpotMap({ markersRef.current.push(tourMarker); }); } - }, [locationGroups, tours, showTours, onSpotClick, handleViewDetails, weatherEnabled, weatherData, navigate]); + + // Add event markers (upcoming, geo-located events β€” lime pins) + if (showEvents && events.length > 0) { + const nowSec = Math.floor(Date.now() / 1000); + events.forEach((evt) => { + // Only plot events with a valid geohash that haven't ended yet + if (!evt.geohash) return; + if ((evt.end ?? evt.start) < nowSec) return; + + const decoded = ngeohash.decode(evt.geohash); + const eventMarker = L.marker([decoded.latitude, decoded.longitude], { + icon: createEventIcon(), + }); + + const container = document.createElement('div'); + container.className = 'p-3 min-w-[240px] max-w-[300px]'; + + // Header + const header = document.createElement('div'); + header.className = 'flex items-start gap-2 mb-3'; + header.innerHTML = ` +
+ πŸ“… +
+
+

${evt.title}

+

UPCOMING EVENT

+
+ `; + container.appendChild(header); + + // Details + const details = document.createElement('div'); + details.className = 'space-y-1.5 mb-3 text-xs text-gray-300'; + const startDate = new Date(evt.start * 1000); + const dateStr = startDate.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }); + const timeStr = startDate.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }); + details.innerHTML += `
πŸ—“οΈ ${dateStr} Β· ${timeStr}
`; + if (evt.location) { + details.innerHTML += `
πŸ“ ${evt.location}
`; + } + container.appendChild(details); + + // View button + const button = document.createElement('button'); + button.className = 'w-full inline-flex items-center justify-center rounded-md text-xs font-bold bg-[#ccff00] text-black hover:bg-[#ccff00]/90 h-8 px-3 transition-colors'; + button.textContent = 'View Event β†’'; + button.onclick = (e) => { + e.preventDefault(); + navigate(`/event/${evt.pubkey}/${evt.identifier}`); + }; + container.appendChild(button); + + eventMarker.bindPopup(container, { + maxWidth: 320, + className: 'custom-popup event-popup', + }); + + if (clusterGroupRef.current) { + clusterGroupRef.current.addLayer(eventMarker); + } else { + eventMarker.addTo(map); + } + markersRef.current.push(eventMarker); + }); + } + }, [locationGroups, tours, showTours, events, showEvents, onSpotClick, handleViewDetails, weatherEnabled, weatherData, navigate]); // Update BTC Map markers useEffect(() => { diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx index 53fb517..175e178 100644 --- a/src/pages/MapPage.tsx +++ b/src/pages/MapPage.tsx @@ -25,6 +25,7 @@ import { useSkateSpots } from '@/hooks/useSkateSpots'; import { useShops } from '@/hooks/useShops'; import { useMarketplaceItems } from '@/hooks/useMarketplace'; import { useTours } from '@/hooks/useTours'; +import { useUpcomingSkateEvents } from '@/hooks/useSkateEvents'; import { useBtcMapPlaces } from '@/hooks/useBtcMapPlaces'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useFollowing } from '@/hooks/useFollowing'; @@ -544,6 +545,8 @@ export default function MapPage() { const { data: shops, isLoading: shopsLoading } = useShops(200); const { data: marketplaceItems, isLoading: marketplaceLoading } = useMarketplaceItems(200); const { data: tours, isLoading: toursLoading } = useTours(100); + // Upcoming events for the (opt-in) Events map layer + const { data: upcomingEvents } = useUpcomingSkateEvents(100); // Check if coming with ?view=gear to focus on marketplace const viewParam = searchParams.get('view'); @@ -569,6 +572,8 @@ export default function MapPage() { const [showShops, setShowShops] = useState(!focusGear); const [showGear, setShowGear] = useState(true); const [showTours, setShowTours] = useState(!focusGear); + // Events layer β€” off by default; users opt in via the Layers control + const [showEvents, setShowEvents] = useState(false); const [showBtcMap, setShowBtcMap] = useState(false); // Mobile filters sheet @@ -898,8 +903,9 @@ export default function MapPage() { // Count active layer filters for mobile badge const activeLayerCount = [showSpots, showShops, showGear, showTours, showBtcMap].filter(Boolean).length; - // Count of the 4 primary layers active β€” shown as a badge on the mobile Layers button - const mobileLayerCount = [showSpots, showShops, showGear, showTours].filter(Boolean).length; + // Count of the primary layers active β€” shown as a badge on the Layers button + const mobileLayerCount = [showSpots, showShops, showGear, showTours, showEvents].filter(Boolean).length; + const totalPrimaryLayers = 5; const activeCategoryCount = selectedCategories.length; const totalActiveFilters = activeCategoryCount + (trustFilter !== 'all' ? 1 : 0) + (sortBy !== 'recent' ? 1 : 0); @@ -1038,6 +1044,17 @@ export default function MapPage() { > πŸ—ΊοΈ Tours +