SkateSpots/src/components/MapSkeletonPins.tsx
shakespeare.diy 345af97788 Map Pass 1: crisp pins, spot-type & shelter glyphs, floating loading, micro-interactions
Visual foundation for the map (ideas #2, #3, #4, #6 — features #5/#7 intentionally
deferred to a later pass):

#2 Crisp pins, not blobs:
- Reworked createSkateSpotIcon — solid neon teardrop with a TIGHT glow halo
  (inset dark ring + focused shadow) instead of the soft box-shadow that read as
  a fuzzy cloud on light tiles. Cluster badges given the same crisp treatment.

#3 Marker = spot type (a readable language):
- Category emoji inside each pin: 🛹 street / 🏞️ park / 🔧 DIY / 🏛️ plaza / 🌊 transition.
- Covered/indoor spots get a cyan roof "cap" so dry spots stand out at a glance —
  the payoff of the shelter data living on the map, not just a badge.
- createSkateSpotIcon now takes an options object (weatherColor/weatherIcon/
  category/shelter); all call sites updated. Weather-layer colouring unchanged.

#4 Less scrim, floating loading:
- Suspense map-load fallback dimming reduced (bg-black/50 → /20).
- "Finding spots" pill moved from dead-centre (top 46%) to a slim top pill so it
  never blocks the map where the spots are.

#6 Micro-interactions (CSS, GPU-friendly, reduced-motion aware):
- New pins drop in with a spring; hover/focus/tap lifts the pin and pulses its
  neon halo once.

Build clean, no console errors. No tile/theme change — the "og" light map is
untouched per request.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-10 18:46:15 +07:00

88 lines
3.5 KiB
TypeScript

/**
* MapSkeletonPins
* ─────────────────────────────────────────────────────────────────────────────
* A tasteful "spots are loading" overlay shown ONLY on a genuinely cold, empty
* map (no cached spots yet + still fetching). Instead of a blank map that reads
* as broken, we scatter a few softly-pulsing ghost pins in the SkateSpots pin
* shape, so the moment reads as "finding spots near you…".
*
* It's purely decorative and non-interactive (`pointer-events-none`), sits above
* the tiles but below every real control, and fully respects reduced-motion.
* The instant real spots arrive the parent unmounts this — no layout shift.
*/
interface Ghost {
/** left/top as viewport percentages so it scales on any screen. */
left: number;
top: number;
/** teardrop scale + animation delay for an organic, staggered feel. */
scale: number;
delay: number;
}
// Hand-placed so they feel scattered-but-balanced (not a rigid grid).
const GHOSTS: Ghost[] = [
{ left: 22, top: 34, scale: 1.0, delay: 0 },
{ left: 48, top: 22, scale: 0.82, delay: 0.35 },
{ left: 71, top: 38, scale: 1.1, delay: 0.15 },
{ left: 34, top: 58, scale: 0.9, delay: 0.6 },
{ left: 62, top: 62, scale: 1.0, delay: 0.45 },
{ left: 82, top: 66, scale: 0.78, delay: 0.8 },
];
export function MapSkeletonPins() {
return (
<div
className="pointer-events-none absolute inset-0 z-[900] overflow-hidden"
aria-hidden="true"
>
{GHOSTS.map((g, i) => (
<div
key={i}
className="absolute motion-safe:animate-ghost-pin"
style={{
left: `${g.left}%`,
top: `${g.top}%`,
animationDelay: `${g.delay}s`,
}}
>
{/* Soft ground shadow / radar ping under each pin */}
<span
className="absolute left-1/2 top-full h-2 w-8 -translate-x-1/2 -translate-y-1 rounded-[50%] bg-[#ff0040]/20 blur-[3px] motion-safe:animate-ghost-ping"
style={{ animationDelay: `${g.delay}s` }}
/>
{/* SkateSpots teardrop pin outline */}
<div
style={{
width: 30 * g.scale,
height: 30 * g.scale,
borderRadius: '50% 50% 50% 0',
transform: 'rotate(-45deg)',
background:
'linear-gradient(135deg, rgba(255,0,64,0.18), rgba(255,102,0,0.14))',
border: '2px solid rgba(255,255,255,0.14)',
boxShadow: '0 4px 14px rgba(255,0,64,0.15)',
backdropFilter: 'blur(1px)',
}}
/>
</div>
))}
{/* Quiet status label — slim pill near the TOP so it never blocks the
map's center where the spots are. */}
<div className="absolute left-1/2 top-3 -translate-x-1/2">
<div className="flex items-center gap-2 rounded-full bg-black/55 px-3.5 py-1.5 backdrop-blur-md ring-1 ring-white/10 shadow-lg">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full rounded-full bg-[#ff0040] opacity-70 motion-safe:animate-ping" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-[#ff0040]" />
</span>
<span className="font-mono text-[10px] uppercase tracking-[0.22em] text-white/70">
Finding spots
</span>
</div>
</div>
</div>
);
}
export default MapSkeletonPins;