SkateSpots/src/App.tsx
shakespeare.diy a984084464 Instant cold-start: query persistence, optimistic geolocation, skeleton pins
Three UX wins so the app feels "already loaded" the moment it opens:

1. IndexedDB React Query persistence (lib/queryPersister.ts)
   - PersistQueryClientProvider rehydrates the last-seen spots, shops,
     events, tours, crews, coaches, listings and author profiles from
     IndexedDB on cold start, so returning users land on a populated map
     instead of a blank one; fresh data refetches quietly in the
     background.
   - Strict allow-list / deny-list: only public, JSON-serializable,
     cold-start-relevant queries are persisted. Private/encrypted data
     (stash, DMs, contacts, wallet/nwc/cashu/bitcoin, signer) is NEVER
     written to disk. 24h maxAge + version buster keep it fresh & safe.
   - Pinned @tanstack/react-query + query-core + persist-client to a
     single 5.102.8 to avoid a query-core mismatch (partialMatchKey).

2. Optimistic geolocation (lib/lastLocation.ts)
   - getCurrentPosition now caches every successful fix.
   - The map opens ALREADY centered on the user's last-known area
     (city zoom) instead of the world view; "you are here" + proximity
     sort seed from the cache instantly.
   - "Near Me" flies to the cached location immediately on tap, then
     refines to the fresh GPS fix. Silent background warm-up on web map
     mount so a later tap resolves instantly.

3. Cold-start skeleton (components/MapSkeletonPins.tsx)
   - When the map is genuinely empty + loading, show elegant pulsing
     ghost pins + a "Finding spots" pill instead of a blank map; they
     vanish the instant real (or cached) spots render. Splash now
     signals content-ready when spots actually resolve.

Respects prefers-reduced-motion throughout. Build + runtime verified
clean (no console errors).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 21:58:22 +07:00

113 lines
4.1 KiB
TypeScript

// NOTE: This file should normally not be modified unless you are adding a new provider.
// To add new routes, edit the AppRouter.tsx file.
import { QueryClient } from "@tanstack/react-query";
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { createHead, UnheadProvider } from '@unhead/react/client';
import { InferSeoMetaPlugin } from '@unhead/addons';
import { Suspense } from 'react';
import {
createIDBPersister,
shouldDehydrateQuery,
PERSIST_BUSTER,
PERSIST_MAX_AGE,
} from '@/lib/queryPersister';
import NostrProvider from '@/components/NostrProvider';
import { NostrSync } from '@/components/NostrSync';
import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
import { NostrLoginProvider } from '@nostrify/react/login';
import { AppProvider } from '@/components/AppProvider';
import { NWCProvider } from '@/contexts/NWCContext';
import { LanguageProvider } from '@/contexts/LanguageContext';
import { AppConfig } from '@/contexts/AppContext';
import { PWAInstallPrompt } from '@/components/PWAInstallPrompt';
import { BetaDisclaimerDialog } from '@/components/BetaDisclaimerDialog';
import { SplashScreen } from '@/components/SplashScreen';
import { AmberCallbackHandler } from '@/components/auth/AmberCallbackHandler';
import AppRouter from './AppRouter';
const head = createHead({
plugins: [
InferSeoMetaPlugin(),
],
});
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
staleTime: 60000, // 1 minute
gcTime: Infinity,
},
},
});
// IndexedDB-backed persistence so the app rehydrates the last-seen spots,
// shops, events and profiles instantly on a cold start (see lib/queryPersister).
const persister = createIDBPersister();
const defaultConfig: AppConfig = {
theme: "dark",
relayMetadata: {
relays: [
{ url: 'wss://auth.nostr1.com', read: true, write: true },
{ url: 'wss://wotr.relatr.xyz', read: true, write: true },
{ url: 'wss://relay.ditto.pub', read: true, write: true },
{ url: 'wss://goskatespots.nostr1.com', read: true, write: true },
{ url: 'wss://purplepag.es', read: true, write: false }, // Profile aggregator - read only
],
updatedAt: 0,
},
useSkateRelay: true, // Use community relay by default for skate content
};
export function App() {
return (
<UnheadProvider head={head}>
<LanguageProvider>
<AppProvider storageKey="nostr:app-config" defaultConfig={defaultConfig}>
<PersistQueryClientProvider
client={queryClient}
persistOptions={{
persister,
// Discard persisted content older than 24h so nothing stale lingers.
maxAge: PERSIST_MAX_AGE,
// Invalidate the whole cache when the persisted data shape changes.
buster: PERSIST_BUSTER,
dehydrateOptions: {
// Only persist public, cold-start-relevant queries — never
// private/encrypted data. See lib/queryPersister.
shouldDehydrateQuery,
},
}}
>
<NostrLoginProvider storageKey='nostr:login'>
<NostrProvider>
<SplashScreen />
<NostrSync />
<AmberCallbackHandler />
<NWCProvider>
<TooltipProvider>
<Toaster />
<PWAInstallPrompt />
<BetaDisclaimerDialog />
<Suspense>
{/* Gentle opacity reveal so content eases in under the
departing splash (pure opacity — never shifts the map). */}
<div className="animate-app-reveal motion-reduce:animate-none">
<AppRouter />
</div>
</Suspense>
</TooltipProvider>
</NWCProvider>
</NostrProvider>
</NostrLoginProvider>
</PersistQueryClientProvider>
</AppProvider>
</LanguageProvider>
</UnheadProvider>
);
}
export default App;