SkateSpots/src/components/MapSkeletonPins.tsx
SkateSpots 09735375c2 feat(i18n): Spanish (Castellano) — infra + Map/Spots, Marketplace, Lab (batch 1)
- First-visit language auto-detect (Spanish devices land in es; stored choice wins).
- Sasquatch AI replies in the app language (skate trick names kept English).
- i18n safety net: scripts/check-i18n.mjs wired into npm test — fails on any t()
  key missing from en/es or any en/es key drift.
- ~1,680 new keys (2,297 total) across mapUi / spotUi / marketUi / labUi and
  ~65 files wired to t(): Map, spots (detail/create/edit/chat), marketplace,
  trades, offers, escrow, shipping, The Lab, /become/* forms, coaches, skaters.
- Policy: only skate TRICK names stay English; user-generated content untouched.
2026-10-01 23:57:20 +07:00

91 lines
3.6 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.
*/
import { useTranslation } from '@/contexts/language';
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() {
const { t } = useTranslation();
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">
{t('mapUi.findingSpots')}
</span>
</div>
</div>
</div>
);
}
export default MapSkeletonPins;