Map loading: replace fuzzy ghost-pin blobs with a clean top bar + pill

The cold-start loading state scattered fuzzy pink "ghost pins" across the map
(the same blob look that was disliked on real markers) which read as broken.
Removed MapSkeletonPins entirely.

New loading state, per the agreed plan (keep the map instant, never cover it):
- A slim animated top progress bar ("ss-loadbar" sweep on a faint track) that
  sits above the map while spots stream in.
- During a genuine cold start (no markers yet), a small labeled "Loading spots"
  pill with a spinner near the top — honest about what's happening (querying
  relays), not fake pins on the ocean.
- Reduced-motion respected (bar becomes static).

The map itself is untouched and still renders immediately; only the placeholder
changed. Dropped the now-unused MapSkeletonPins import. Build clean, no console
errors.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-14 09:54:07 +07:00
parent a40c86e655
commit 46fbb0df43
2 changed files with 35 additions and 10 deletions

View file

@ -395,6 +395,20 @@
} }
} }
/* Indeterminate top loading bar for the map ("Loading spots"). A short segment
sweeps left→right on a faint track, so it reads as active work without
pretending to know real progress. Disabled under reduced-motion. */
@keyframes ssLoadBar {
0% { transform: translateX(-120%); }
100% { transform: translateX(420%); }
}
.ss-loadbar {
animation: ssLoadBar 1.1s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
@media (prefers-reduced-motion: reduce) {
.ss-loadbar { animation: none; width: 100%; opacity: 0.7; }
}
.animate-fadeIn { .animate-fadeIn {
animation: fadeIn 0.4s ease-out forwards; animation: fadeIn 0.4s ease-out forwards;
} }

View file

@ -85,7 +85,6 @@ import { useToast } from '@/hooks/useToast';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { getCurrentPosition, GeolocationError, isNativeGeolocation } from '@/lib/geolocation'; import { getCurrentPosition, GeolocationError, isNativeGeolocation } from '@/lib/geolocation';
import { getLastLocation } from '@/lib/lastLocation'; import { getLastLocation } from '@/lib/lastLocation';
import { MapSkeletonPins } from '@/components/MapSkeletonPins';
type ViewMode = 'map' | 'list'; type ViewMode = 'map' | 'list';
@ -1615,18 +1614,30 @@ export default function MapPage() {
userLocation={userLocation ? { lat: userLocation.lat, lng: userLocation.lon } : null} userLocation={userLocation ? { lat: userLocation.lat, lng: userLocation.lon } : null}
/> />
</Suspense> </Suspense>
{/* Loading indicator — slim top edge bar */} {/* Loading indicator — a slim, honest top bar. The map stays
visible underneath and spots stream in; we NEVER cover the map
with fake placeholder pins. An animated sweep sits on top of a
faint track so it reads as "working", plus a small labeled pill
during a genuine cold start (no markers yet) so the user knows
we're talking to the relays. */}
{isLoading && ( {isLoading && (
<div className="absolute top-0 left-0 right-0 z-[1000] h-0.5"> <>
<div className="h-full bg-gradient-to-r from-[#ff0040] via-[#ff6600] to-[#ccff00] animate-pulse" /> <div className="absolute top-0 left-0 right-0 z-[1001] h-[3px] overflow-hidden bg-white/5">
</div> <div className="ss-loadbar h-full w-1/3 bg-gradient-to-r from-[#ff0040] via-[#ff6600] to-[#ccff00]" />
</div>
{mapSpots.length === 0 && (
<div className="absolute top-3 left-1/2 -translate-x-1/2 z-[1001]">
<div className="flex items-center gap-2 rounded-full bg-black/60 px-3.5 py-1.5 backdrop-blur-md ring-1 ring-white/10 shadow-lg">
<span className="h-3.5 w-3.5 rounded-full border-2 border-[#ccff00]/30 border-t-[#ccff00] animate-spin" />
<span className="font-mono text-[10px] uppercase tracking-[0.22em] text-white/70">
Loading spots
</span>
</div>
</div>
)}
</>
)} )}
{/* Cold-start skeleton: only while genuinely loading with NO
markers on the map yet. The instant real (or cached) spots
render, mapSpots.length > 0 and this disappears — no blank map. */}
{isLoading && mapSpots.length === 0 && <MapSkeletonPins />}
{/* Map attribution — bottom-left, sits ABOVE the mobile bottom {/* Map attribution — bottom-left, sits ABOVE the mobile bottom