- 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.
91 lines
3.6 KiB
TypeScript
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;
|