diff --git a/src/components/MapSkeletonPins.tsx b/src/components/MapSkeletonPins.tsx index 0c36826..57b7b64 100644 --- a/src/components/MapSkeletonPins.tsx +++ b/src/components/MapSkeletonPins.tsx @@ -68,9 +68,10 @@ export function MapSkeletonPins() { ))} - {/* Centered, quiet status label */} -
-
+ {/* Quiet status label — slim pill near the TOP so it never blocks the + map's center where the spots are. */} +
+
diff --git a/src/components/SpotMap.tsx b/src/components/SpotMap.tsx index 7cb0ef3..ac0a0e2 100644 --- a/src/components/SpotMap.tsx +++ b/src/components/SpotMap.tsx @@ -63,51 +63,100 @@ const defaultIcon = L.icon({ L.Marker.prototype.options.icon = defaultIcon; -// Custom skate spot marker icon (for regular spots) -const createSkateSpotIcon = (weatherColor?: string, weatherIcon?: string) => { - const gradient = weatherColor +/** Emoji glyph per spot category — turns the map into a readable language. */ +const CATEGORY_GLYPH: Record = { + street: '🛹', + park: '🏞️', + diy: '🔧', + plaza: '🏛️', + transition: '🌊', +}; + +/** + * Crisp skate-spot marker: a solid neon teardrop with a TIGHT glow halo (the + * glow is an accent, not the whole blob), a category emoji inside so the pin is + * readable at a glance, and — for covered/indoor spots — a small roof "cap" so + * dry spots literally stand out on a rainy day. + * + * `weatherColor`/`weatherIcon` (when the weather layer is on) still override the + * fill + add the condition badge, exactly as before. + */ +const createSkateSpotIcon = (opts?: { + weatherColor?: string; + weatherIcon?: string; + category?: string; + shelter?: 'outdoor' | 'partial' | 'covered' | 'indoor'; +}) => { + const { weatherColor, weatherIcon, category, shelter } = opts ?? {}; + + const fill = weatherColor ? `background: ${weatherColor};` : 'background: linear-gradient(135deg, #ff0040, #ff6600);'; + const glowColor = weatherColor || '#ff0040'; + const glyph = (category && CATEGORY_GLYPH[category]) || '🛹'; + const isSheltered = shelter === 'covered' || shelter === 'indoor'; - const borderColor = weatherColor ? weatherColor : '#fff'; + // Roof cap for covered/indoor spots — a small cyan "dry" marker that sits + // above the pin so it reads even when zoomed out. + const roofCap = isSheltered + ? ` +
` + : ''; return L.divIcon({ className: 'skate-spot-marker', html: ` -
- ${weatherIcon ? ` -
+ ${roofCap} +
+ ${weatherIcon ? ` +
${weatherIcon}
+ ` : ''} + ${weatherIcon}
- ` : ''} - - - - + font-size: 16px; + line-height: 1; + filter: drop-shadow(0 1px 1px rgba(0,0,0,0.5)); + ">${glyph} +
`, iconSize: [36, 36], @@ -902,8 +951,8 @@ export function SpotMap({ color: white; font-weight: 900; font-size: ${count > 99 ? '11px' : count > 9 ? '13px' : '15px'}; - box-shadow: 0 4px 14px rgba(255,0,64,0.55); - border: 3px solid rgba(255,255,255,0.85); + box-shadow: 0 0 0 1px rgba(0,0,0,0.35), 0 0 12px rgba(255,0,64,0.9), 0 3px 8px rgba(0,0,0,0.45); + border: 3px solid #fff; font-family: Impact, 'Arial Black', sans-serif; letter-spacing: -0.5px; ">${count}
@@ -1046,22 +1095,24 @@ export function SpotMap({ } else if (hasSpots && !hasGear) { // Regular spot only const spotWeather = weatherEnabled && weatherData && primarySpot ? weatherData.get(primarySpot.geohash) : undefined; - if (spotWeather) { - icon = createSkateSpotIcon(spotWeather.skateability.color, spotWeather.weather.weatherIcon); - } else { - icon = createSkateSpotIcon(); - } + icon = createSkateSpotIcon({ + weatherColor: spotWeather?.skateability.color, + weatherIcon: spotWeather?.weather.weatherIcon, + category: primarySpot?.category, + shelter: primarySpot?.shelter, + }); } else if (!hasSpots && hasGear) { // Gear only (no spot) icon = createMarketplaceIcon(gearCount); } else { // Spot + gear - regular spot icon const spotWeather = weatherEnabled && weatherData && primarySpot ? weatherData.get(primarySpot.geohash) : undefined; - if (spotWeather) { - icon = createSkateSpotIcon(spotWeather.skateability.color, spotWeather.weather.weatherIcon); - } else { - icon = createSkateSpotIcon(); - } + icon = createSkateSpotIcon({ + weatherColor: spotWeather?.skateability.color, + weatherIcon: spotWeather?.weather.weatherIcon, + category: primarySpot?.category, + shelter: primarySpot?.shelter, + }); } const marker = L.marker([group.lat, group.lng], { diff --git a/src/index.css b/src/index.css index 3406dcc..b2e925b 100644 --- a/src/index.css +++ b/src/index.css @@ -360,6 +360,41 @@ animation: ghostPing 2.4s ease-in-out infinite; } +/* ── Live map markers: crisp pins that feel alive ───────────────────────────── + A new pin drops in with a quick spring; hovering/tapping lifts it and pulses + its neon halo once. Purely presentational, GPU-friendly (transform/opacity), + and fully disabled under reduced-motion. */ +@keyframes ssPinDrop { + 0% { transform: translateY(-14px) scale(0.6); opacity: 0; } + 60% { transform: translateY(2px) scale(1.06); opacity: 1; } + 100% { transform: translateY(0) scale(1); opacity: 1; } +} +@keyframes ssPinPulse { + 0% { filter: drop-shadow(0 0 2px rgba(255,0,64,0.0)); } + 50% { filter: drop-shadow(0 0 10px rgba(255,0,64,0.9)); } + 100% { filter: drop-shadow(0 0 2px rgba(255,0,64,0.0)); } +} + +/* The teardrop wrapper inside our divIcon. */ +.skate-spot-marker .ss-pin { + transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1); + transform-origin: 50% 100%; + will-change: transform; +} +@media (prefers-reduced-motion: no-preference) { + .skate-spot-marker .ss-pin { + animation: ssPinDrop 0.32s cubic-bezier(0.34, 1.56, 0.64, 1) both; + } + /* Lift + one halo pulse on hover/focus/press. Leaflet keeps the marker + focusable, so this also covers keyboard + tap. */ + .skate-spot-marker:hover .ss-pin, + .skate-spot-marker:focus .ss-pin, + .skate-spot-marker:active .ss-pin { + transform: translateY(-4px) scale(1.12); + animation: ssPinPulse 0.6s ease-out; + } +} + .animate-fadeIn { animation: fadeIn 0.4s ease-out forwards; } diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx index ee0e302..50f0e6f 100644 --- a/src/pages/MapPage.tsx +++ b/src/pages/MapPage.tsx @@ -1580,7 +1580,7 @@ export default function MapPage() { {viewMode === 'map' ? ( <> +
Loading map...