diff --git a/package-lock.json b/package-lock.json index f74dd62..2cc98c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,7 +57,9 @@ "@radix-ui/react-tooltip": "^1.1.4", "@scure/bip32": "^2.0.1", "@scure/bip39": "1.6.0", + "@tanstack/query-core": "^5.102.8", "@tanstack/react-query": "^5.56.2", + "@tanstack/react-query-persist-client": "^5.102.8", "@unhead/addons": "^2.0.10", "@unhead/react": "^2.0.10", "bitcoinjs-lib": "^7.0.0", @@ -76,6 +78,7 @@ "html2canvas": "^1.4.1", "html5-qrcode": "^2.3.8", "idb": "^8.0.3", + "idb-keyval": "^6.3.0", "input-otp": "^1.2.4", "leaflet": "^1.9.4", "leaflet.markercluster": "^1.5.3", @@ -3520,13 +3523,13 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.75.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.75.0.tgz", - "integrity": "sha512-rk8KQuCdhoRkzjRVF3QxLgAfFUyS0k7+GCQjlGEpEGco+qazJ0eMH6aO1DjDjibH7/ik383nnztua3BG+lOnwg==", + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.102.8.tgz", + "integrity": "sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg==", "license": "MIT", "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "url": "https://github.com/sponsors/tannerlinsley", + "type": "github" } }, "node_modules/@tanstack/react-query": { @@ -3545,6 +3548,16 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-query-persist-client": { + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.102.8.tgz", + "integrity": "sha512-WOZReC+m9klekPtG7eeZ3txJeMA9lg2R0RytSG3yXcZgJItQAVIyoobJdbdIQtlpgcU2tSzsOEBtJacs2vlCOA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/tannerlinsley", + "type": "github" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -5993,6 +6006,12 @@ "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", "license": "ISC" }, + "node_modules/idb-keyval": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.3.0.tgz", + "integrity": "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==", + "license": "Apache-2.0" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", diff --git a/package.json b/package.json index 00253d7..c52c543 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,9 @@ "@radix-ui/react-tooltip": "^1.1.4", "@scure/bip32": "^2.0.1", "@scure/bip39": "1.6.0", - "@tanstack/react-query": "^5.56.2", + "@tanstack/query-core": "^5.102.8", + "@tanstack/react-query": "5.102.8", + "@tanstack/react-query-persist-client": "5.102.8", "@unhead/addons": "^2.0.10", "@unhead/react": "^2.0.10", "bitcoinjs-lib": "^7.0.0", @@ -83,6 +85,7 @@ "html2canvas": "^1.4.1", "html5-qrcode": "^2.3.8", "idb": "^8.0.3", + "idb-keyval": "^6.3.0", "input-otp": "^1.2.4", "leaflet": "^1.9.4", "leaflet.markercluster": "^1.5.3", diff --git a/src/App.tsx b/src/App.tsx index afb2218..2a2ccfc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,17 @@ // 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, QueryClientProvider } from "@tanstack/react-query"; +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"; @@ -36,6 +43,10 @@ const queryClient = new QueryClient({ }, }); +// 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: { @@ -56,7 +67,21 @@ export function App() { - + @@ -78,7 +103,7 @@ export function App() { - + diff --git a/src/components/MapSkeletonPins.tsx b/src/components/MapSkeletonPins.tsx new file mode 100644 index 0000000..0c36826 --- /dev/null +++ b/src/components/MapSkeletonPins.tsx @@ -0,0 +1,87 @@ +/** + * 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 ( +