perf: lazy loading, faster timeouts, better caching, chunk splitting
- AppRouter: lazy load all 40+ pages (only Index is eager) → initial JS bundle no longer includes code for pages never visited - MapPage: lazy load SpotMap/Leaflet (~150KB) behind Suspense → Leaflet only downloads when user navigates to /map - useSkateSpots/useShops/useMarketplace/useTours: → timeout: 30s → 8s (fail fast, show error sooner) → staleTime: 30s → 5min (trust cache, fewer relay round-trips) → gcTime: 5min → 30min (keep data in memory longer) → refetchOnMount: 'always' → true (use cache when data is fresh) - Remove console.log/warn/debug spam from production hooks - vite.config: manual chunk splitting for leaflet, nostr-tools, bitcoin libs, radix-ui, react-router — each chunk cached independently by browser, only re-downloaded when that library updates - vite.config: esbuild drops console.log/warn/debug in prod build Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
d844e151bd
commit
bd3e4bee4e
7 changed files with 258 additions and 226 deletions
|
|
@ -1,106 +1,124 @@
|
||||||
|
import { lazy, Suspense } from "react";
|
||||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||||
import { ScrollToTop } from "./components/ScrollToTop";
|
import { ScrollToTop } from "./components/ScrollToTop";
|
||||||
|
|
||||||
|
// Eagerly load only the most-visited pages
|
||||||
import Index from "./pages/Index";
|
import Index from "./pages/Index";
|
||||||
import MapPage from "./pages/MapPage";
|
|
||||||
import EventsPage from "./pages/EventsPage";
|
// Lazy load everything else — each page becomes its own JS chunk
|
||||||
import FoodPage from "./pages/FoodPage";
|
const MapPage = lazy(() => import("./pages/MapPage"));
|
||||||
import WalletPage from "./pages/WalletPage";
|
const EventsPage = lazy(() => import("./pages/EventsPage"));
|
||||||
import SettingsPage from "./pages/SettingsPage";
|
const LabPage = lazy(() => import("./pages/LabPage"));
|
||||||
import CreateSpotPage from "./pages/CreateSpotPage";
|
const WalletPage = lazy(() => import("./pages/WalletPage"));
|
||||||
import CreateShopPage from "./pages/CreateShopPage";
|
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
|
||||||
import CreateEventPage from "./pages/CreateEventPage";
|
const CreateSpotPage = lazy(() => import("./pages/CreateSpotPage"));
|
||||||
import CreateListingPage from "./pages/CreateListingPage";
|
const CreateShopPage = lazy(() => import("./pages/CreateShopPage"));
|
||||||
import CreateTourPage from "./pages/CreateTourPage";
|
const CreateEventPage = lazy(() => import("./pages/CreateEventPage"));
|
||||||
import EditSpotPage from "./pages/EditSpotPage";
|
const CreateListingPage = lazy(() => import("./pages/CreateListingPage"));
|
||||||
import EditShopPage from "./pages/EditShopPage";
|
const CreateTourPage = lazy(() => import("./pages/CreateTourPage"));
|
||||||
import EditEventPage from "./pages/EditEventPage";
|
const EditSpotPage = lazy(() => import("./pages/EditSpotPage"));
|
||||||
import GearSwapPage from "./pages/GearSwapPage";
|
const EditShopPage = lazy(() => import("./pages/EditShopPage"));
|
||||||
import SpotDetailPage from "./pages/SpotDetailPage";
|
const EditEventPage = lazy(() => import("./pages/EditEventPage"));
|
||||||
import ShopDetailPage from "./pages/ShopDetailPage";
|
const GearSwapPage = lazy(() => import("./pages/GearSwapPage"));
|
||||||
import EventDetailPage from "./pages/EventDetailPage";
|
const SpotDetailPage = lazy(() => import("./pages/SpotDetailPage"));
|
||||||
import OnboardingPage from "./pages/OnboardingPage";
|
const ShopDetailPage = lazy(() => import("./pages/ShopDetailPage"));
|
||||||
import PyramidPage from "./pages/PyramidPage";
|
const EventDetailPage = lazy(() => import("./pages/EventDetailPage"));
|
||||||
import LabPage from "./pages/LabPage";
|
const OnboardingPage = lazy(() => import("./pages/OnboardingPage"));
|
||||||
import BecomeCoachPage from "./pages/BecomeCoachPage";
|
const PyramidPage = lazy(() => import("./pages/PyramidPage"));
|
||||||
import BecomeFilmerPage from "./pages/BecomeFilmerPage";
|
const BecomeCoachPage = lazy(() => import("./pages/BecomeCoachPage"));
|
||||||
import BecomeEditorPage from "./pages/BecomeEditorPage";
|
const BecomeFilmerPage = lazy(() => import("./pages/BecomeFilmerPage"));
|
||||||
import CoachDetailPage from "./pages/CoachDetailPage";
|
const BecomeEditorPage = lazy(() => import("./pages/BecomeEditorPage"));
|
||||||
import TourDetailPage from "./pages/TourDetailPage";
|
const CoachDetailPage = lazy(() => import("./pages/CoachDetailPage"));
|
||||||
import AboutPage from "./pages/AboutPage";
|
const TourDetailPage = lazy(() => import("./pages/TourDetailPage"));
|
||||||
import FAQPage from "./pages/FAQPage";
|
const AboutPage = lazy(() => import("./pages/AboutPage"));
|
||||||
import CommunityStatsPage from "./pages/CommunityStatsPage";
|
const FAQPage = lazy(() => import("./pages/FAQPage"));
|
||||||
import SpotCollectionsPage from "./pages/SpotCollectionsPage";
|
const FoodPage = lazy(() => import("./pages/FoodPage"));
|
||||||
import CollectionDetailPage from "./pages/CollectionDetailPage";
|
const CommunityStatsPage = lazy(() => import("./pages/CommunityStatsPage"));
|
||||||
import DirectMessagesPage from "./pages/DirectMessagesPage";
|
const SpotCollectionsPage = lazy(() => import("./pages/SpotCollectionsPage"));
|
||||||
import ConversationPage from "./pages/ConversationPage";
|
const CollectionDetailPage = lazy(() => import("./pages/CollectionDetailPage"));
|
||||||
import BountyLeaderboardPage from "./pages/BountyLeaderboardPage";
|
const DirectMessagesPage = lazy(() => import("./pages/DirectMessagesPage"));
|
||||||
import SignersPage from "./pages/SignersPage";
|
const ConversationPage = lazy(() => import("./pages/ConversationPage"));
|
||||||
import StashPage from "./pages/StashPage";
|
const BountyLeaderboardPage = lazy(() => import("./pages/BountyLeaderboardPage"));
|
||||||
import { NIP19Page } from "./pages/NIP19Page";
|
const SignersPage = lazy(() => import("./pages/SignersPage"));
|
||||||
import LicensePage from "./pages/LicensePage";
|
const StashPage = lazy(() => import("./pages/StashPage"));
|
||||||
import SupportersPage from "./pages/SupportersPage";
|
const NIP19Page = lazy(() => import("./pages/NIP19Page").then(m => ({ default: m.NIP19Page })));
|
||||||
import RiderCardPage from "./pages/RiderCardPage";
|
const LicensePage = lazy(() => import("./pages/LicensePage"));
|
||||||
import AmberCallbackPage from "./pages/AmberCallbackPage";
|
const SupportersPage = lazy(() => import("./pages/SupportersPage"));
|
||||||
import NotFound from "./pages/NotFound";
|
const RiderCardPage = lazy(() => import("./pages/RiderCardPage"));
|
||||||
|
const AmberCallbackPage = lazy(() => import("./pages/AmberCallbackPage"));
|
||||||
|
const MarketplacePage = lazy(() => import("./pages/MarketplacePage"));
|
||||||
|
const NotFound = lazy(() => import("./pages/NotFound"));
|
||||||
|
|
||||||
|
// Minimal inline fallback — no extra component needed
|
||||||
|
function PageLoader() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-black flex items-center justify-center">
|
||||||
|
<div className="w-8 h-8 border-2 border-[#ff0040] border-t-transparent rounded-full animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function AppRouter() {
|
export function AppRouter() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
<Routes>
|
<Suspense fallback={<PageLoader />}>
|
||||||
<Route path="/" element={<Index />} />
|
<Routes>
|
||||||
<Route path="/map" element={<MapPage />} />
|
<Route path="/" element={<Index />} />
|
||||||
<Route path="/events" element={<EventsPage />} />
|
<Route path="/map" element={<MapPage />} />
|
||||||
<Route path="/lab" element={<LabPage />} />
|
<Route path="/events" element={<EventsPage />} />
|
||||||
{/* Legacy route redirect */}
|
<Route path="/lab" element={<LabPage />} />
|
||||||
<Route path="/coaches" element={<LabPage />} />
|
{/* Legacy route redirect */}
|
||||||
<Route path="/become-coach" element={<BecomeCoachPage />} />
|
<Route path="/coaches" element={<LabPage />} />
|
||||||
<Route path="/become-filmer" element={<BecomeFilmerPage />} />
|
<Route path="/become-coach" element={<BecomeCoachPage />} />
|
||||||
<Route path="/become-editor" element={<BecomeEditorPage />} />
|
<Route path="/become-filmer" element={<BecomeFilmerPage />} />
|
||||||
<Route path="/coach/:pubkey/:identifier" element={<CoachDetailPage />} />
|
<Route path="/become-editor" element={<BecomeEditorPage />} />
|
||||||
<Route path="/onboarding" element={<OnboardingPage />} />
|
<Route path="/coach/:pubkey/:identifier" element={<CoachDetailPage />} />
|
||||||
<Route path="/pyramid" element={<PyramidPage />} />
|
<Route path="/onboarding" element={<OnboardingPage />} />
|
||||||
<Route path="/marketplace" element={<GearSwapPage />} />
|
<Route path="/pyramid" element={<PyramidPage />} />
|
||||||
<Route path="/stats" element={<CommunityStatsPage />} />
|
<Route path="/marketplace" element={<GearSwapPage />} />
|
||||||
<Route path="/about" element={<AboutPage />} />
|
<Route path="/stats" element={<CommunityStatsPage />} />
|
||||||
<Route path="/faq" element={<FAQPage />} />
|
<Route path="/about" element={<AboutPage />} />
|
||||||
<Route path="/food" element={<FoodPage />} />
|
<Route path="/faq" element={<FAQPage />} />
|
||||||
<Route path="/wallet" element={<WalletPage />} />
|
<Route path="/food" element={<FoodPage />} />
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
<Route path="/wallet" element={<WalletPage />} />
|
||||||
<Route path="/create-spot" element={<CreateSpotPage />} />
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
<Route path="/create-shop" element={<CreateShopPage />} />
|
<Route path="/create-spot" element={<CreateSpotPage />} />
|
||||||
<Route path="/create-event" element={<CreateEventPage />} />
|
<Route path="/create-shop" element={<CreateShopPage />} />
|
||||||
<Route path="/create-listing" element={<CreateListingPage />} />
|
<Route path="/create-event" element={<CreateEventPage />} />
|
||||||
<Route path="/create-tour" element={<CreateTourPage />} />
|
<Route path="/create-listing" element={<CreateListingPage />} />
|
||||||
<Route path="/edit-spot/:pubkey/:identifier" element={<EditSpotPage />} />
|
<Route path="/create-tour" element={<CreateTourPage />} />
|
||||||
<Route path="/edit-shop/:pubkey/:identifier" element={<EditShopPage />} />
|
<Route path="/edit-spot/:pubkey/:identifier" element={<EditSpotPage />} />
|
||||||
<Route path="/edit-event/:pubkey/:identifier" element={<EditEventPage />} />
|
<Route path="/edit-shop/:pubkey/:identifier" element={<EditShopPage />} />
|
||||||
<Route path="/spot/:pubkey/:identifier" element={<SpotDetailPage />} />
|
<Route path="/edit-event/:pubkey/:identifier" element={<EditEventPage />} />
|
||||||
<Route path="/shop/:pubkey/:identifier" element={<ShopDetailPage />} />
|
<Route path="/spot/:pubkey/:identifier" element={<SpotDetailPage />} />
|
||||||
<Route path="/event/:pubkey/:identifier" element={<EventDetailPage />} />
|
<Route path="/shop/:pubkey/:identifier" element={<ShopDetailPage />} />
|
||||||
<Route path="/tour/:pubkey/:identifier" element={<TourDetailPage />} />
|
<Route path="/event/:pubkey/:identifier" element={<EventDetailPage />} />
|
||||||
<Route path="/collections" element={<SpotCollectionsPage />} />
|
<Route path="/tour/:pubkey/:identifier" element={<TourDetailPage />} />
|
||||||
<Route path="/collections/:pubkey/:identifier" element={<CollectionDetailPage />} />
|
<Route path="/collections" element={<SpotCollectionsPage />} />
|
||||||
<Route path="/messages" element={<DirectMessagesPage />} />
|
<Route path="/collections/:pubkey/:identifier" element={<CollectionDetailPage />} />
|
||||||
<Route path="/messages/:npub" element={<ConversationPage />} />
|
<Route path="/messages" element={<DirectMessagesPage />} />
|
||||||
{/* Bounty System */}
|
<Route path="/messages/:npub" element={<ConversationPage />} />
|
||||||
<Route path="/bounties" element={<BountyLeaderboardPage />} />
|
{/* Bounty System */}
|
||||||
{/* Security & Signers */}
|
<Route path="/bounties" element={<BountyLeaderboardPage />} />
|
||||||
<Route path="/signers" element={<SignersPage />} />
|
{/* Security & Signers */}
|
||||||
{/* Stash - Private Inventory */}
|
<Route path="/signers" element={<SignersPage />} />
|
||||||
<Route path="/stash" element={<StashPage />} />
|
{/* Stash - Private Inventory */}
|
||||||
{/* Info pages */}
|
<Route path="/stash" element={<StashPage />} />
|
||||||
<Route path="/card" element={<RiderCardPage />} />
|
{/* Info pages */}
|
||||||
<Route path="/license" element={<LicensePage />} />
|
<Route path="/card" element={<RiderCardPage />} />
|
||||||
<Route path="/supporters" element={<SupportersPage />} />
|
<Route path="/license" element={<LicensePage />} />
|
||||||
{/* Amber NIP-55 callback — Amber redirects here after signing/login */}
|
<Route path="/supporters" element={<SupportersPage />} />
|
||||||
<Route path="/amber-callback" element={<AmberCallbackPage />} />
|
{/* Amber NIP-55 callback — Amber redirects here after signing/login */}
|
||||||
{/* NIP-19 route for npub1, note1, naddr1, nevent1, nprofile1 */}
|
<Route path="/amber-callback" element={<AmberCallbackPage />} />
|
||||||
<Route path="/:nip19" element={<NIP19Page />} />
|
{/* Marketplace */}
|
||||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
<Route path="/marketplace-listings" element={<MarketplacePage />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
{/* NIP-19 route for npub1, note1, naddr1, nevent1, nprofile1 */}
|
||||||
</Routes>
|
<Route path="/:nip19" element={<NIP19Page />} />
|
||||||
|
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,17 @@ import { useQuery } from '@tanstack/react-query';
|
||||||
import { MARKETPLACE_ITEM_KIND, parseMarketplaceItem, type MarketplaceItem } from '@/lib/types';
|
import { MARKETPLACE_ITEM_KIND, parseMarketplaceItem, type MarketplaceItem } from '@/lib/types';
|
||||||
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
||||||
|
|
||||||
|
const STALE_TIME = 5 * 60 * 1000; // 5 min
|
||||||
|
const GC_TIME = 30 * 60 * 1000; // 30 min
|
||||||
|
const TIMEOUT = 8000;
|
||||||
|
|
||||||
export function useMarketplaceItems(limit = 100) {
|
export function useMarketplaceItems(limit = 100) {
|
||||||
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['marketplace-items', limit, getQueryKeySuffix()],
|
queryKey: ['marketplace-items', limit, getQueryKeySuffix()],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const signal = AbortSignal.timeout(10000);
|
const signal = AbortSignal.timeout(TIMEOUT);
|
||||||
|
|
||||||
console.log('[useMarketplaceItems] Querying for marketplace items...');
|
|
||||||
|
|
||||||
const events = await querySkateContent(
|
const events = await querySkateContent(
|
||||||
[
|
[
|
||||||
|
|
@ -23,20 +25,17 @@ export function useMarketplaceItems(limit = 100) {
|
||||||
{ signal }
|
{ signal }
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`[useMarketplaceItems] Received ${events.length} events`);
|
|
||||||
|
|
||||||
const items: MarketplaceItem[] = [];
|
const items: MarketplaceItem[] = [];
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const item = parseMarketplaceItem(event);
|
const item = parseMarketplaceItem(event);
|
||||||
if (item) items.push(item);
|
if (item) items.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[useMarketplaceItems] Parsed items: ${items.length}`);
|
|
||||||
|
|
||||||
return items.sort((a, b) => b.createdAt - a.createdAt);
|
return items.sort((a, b) => b.createdAt - a.createdAt);
|
||||||
},
|
},
|
||||||
staleTime: 30000,
|
staleTime: STALE_TIME,
|
||||||
refetchOnMount: 'always',
|
gcTime: GC_TIME,
|
||||||
|
refetchOnMount: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,13 +45,10 @@ export function useMarketplaceAtSpot(spotPubkey: string, spotIdentifier: string,
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['marketplace-at-spot', spotPubkey, spotIdentifier, getQueryKeySuffix()],
|
queryKey: ['marketplace-at-spot', spotPubkey, spotIdentifier, getQueryKeySuffix()],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const signal = AbortSignal.timeout(10000);
|
const signal = AbortSignal.timeout(TIMEOUT);
|
||||||
|
|
||||||
// Build spot reference (naddr format without encoding)
|
|
||||||
const spotRef = `30818:${spotPubkey}:${spotIdentifier}`;
|
const spotRef = `30818:${spotPubkey}:${spotIdentifier}`;
|
||||||
|
|
||||||
console.log(`[useMarketplaceAtSpot] Querying for items at spot: ${spotRef}`);
|
|
||||||
|
|
||||||
const events = await querySkateContent(
|
const events = await querySkateContent(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
@ -65,23 +61,19 @@ export function useMarketplaceAtSpot(spotPubkey: string, spotIdentifier: string,
|
||||||
{ signal }
|
{ signal }
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`[useMarketplaceAtSpot] Received ${events.length} events`);
|
|
||||||
|
|
||||||
const items: MarketplaceItem[] = [];
|
const items: MarketplaceItem[] = [];
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const item = parseMarketplaceItem(event);
|
const item = parseMarketplaceItem(event);
|
||||||
// Only show items that are active or don't have a status
|
|
||||||
if (item && (!item.status || item.status === 'active')) {
|
if (item && (!item.status || item.status === 'active')) {
|
||||||
items.push(item);
|
items.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[useMarketplaceAtSpot] Parsed active items: ${items.length}`);
|
|
||||||
|
|
||||||
return items.sort((a, b) => b.createdAt - a.createdAt);
|
return items.sort((a, b) => b.createdAt - a.createdAt);
|
||||||
},
|
},
|
||||||
enabled: !!spotPubkey && !!spotIdentifier,
|
enabled: !!spotPubkey && !!spotIdentifier,
|
||||||
staleTime: 30000,
|
staleTime: STALE_TIME,
|
||||||
refetchOnMount: 'always',
|
gcTime: GC_TIME,
|
||||||
|
refetchOnMount: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ export function useShops(limit = 100) {
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const signal = AbortSignal.timeout(10000);
|
const signal = AbortSignal.timeout(10000);
|
||||||
|
|
||||||
console.log('[useShops] Querying for shops');
|
|
||||||
|
|
||||||
// Query new shop kind (30017)
|
// Query new shop kind (30017)
|
||||||
const newShops = await querySkateContent(
|
const newShops = await querySkateContent(
|
||||||
[
|
[
|
||||||
|
|
@ -23,8 +21,6 @@ export function useShops(limit = 100) {
|
||||||
],
|
],
|
||||||
{ signal }
|
{ signal }
|
||||||
);
|
);
|
||||||
console.log(`[useShops] Received ${newShops.length} new shops (kind 30017)`);
|
|
||||||
|
|
||||||
// Query old shops (kind 30818 with 'shop' type) for backward compatibility
|
// Query old shops (kind 30818 with 'shop' type) for backward compatibility
|
||||||
const oldShops = await querySkateContent(
|
const oldShops = await querySkateContent(
|
||||||
[
|
[
|
||||||
|
|
@ -36,7 +32,7 @@ export function useShops(limit = 100) {
|
||||||
],
|
],
|
||||||
{ signal }
|
{ signal }
|
||||||
);
|
);
|
||||||
console.log(`[useShops] Received ${oldShops.length} old shops (kind 30818)`);
|
|
||||||
|
|
||||||
const allEvents = [...newShops, ...oldShops];
|
const allEvents = [...newShops, ...oldShops];
|
||||||
|
|
||||||
|
|
@ -72,19 +68,15 @@ export function useShops(limit = 100) {
|
||||||
|
|
||||||
if (shop) {
|
if (shop) {
|
||||||
shops.push(shop);
|
shops.push(shop);
|
||||||
} else {
|
|
||||||
console.warn('[useShops] Failed to parse event:', event);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[useShops] Parsed shops:', shops.length);
|
|
||||||
|
|
||||||
// Sort by created_at descending
|
// Sort by created_at descending
|
||||||
return shops.sort((a, b) => b.createdAt - a.createdAt);
|
return shops.sort((a, b) => b.createdAt - a.createdAt);
|
||||||
},
|
},
|
||||||
staleTime: 30000,
|
staleTime: 5 * 60 * 1000,
|
||||||
gcTime: 300000,
|
gcTime: 30 * 60 * 1000,
|
||||||
refetchOnMount: 'always',
|
refetchOnMount: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,8 +88,6 @@ export function useShop(identifier: string, pubkey?: string) {
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const signal = AbortSignal.timeout(10000);
|
const signal = AbortSignal.timeout(10000);
|
||||||
|
|
||||||
console.log(`[useShop] Querying for shop: ${identifier}`);
|
|
||||||
|
|
||||||
// Query new shop format (kind 30017)
|
// Query new shop format (kind 30017)
|
||||||
const newFilter: {
|
const newFilter: {
|
||||||
kinds: number[];
|
kinds: number[];
|
||||||
|
|
@ -112,8 +102,6 @@ export function useShop(identifier: string, pubkey?: string) {
|
||||||
if (pubkey) newFilter.authors = [pubkey];
|
if (pubkey) newFilter.authors = [pubkey];
|
||||||
|
|
||||||
const newShops = await querySkateContent([newFilter], { signal });
|
const newShops = await querySkateContent([newFilter], { signal });
|
||||||
console.log(`[useShop] Received ${newShops.length} new shops`);
|
|
||||||
|
|
||||||
// Query old shop format (kind 30818 with 'shop' type) for backward compatibility
|
// Query old shop format (kind 30818 with 'shop' type) for backward compatibility
|
||||||
const oldFilter: {
|
const oldFilter: {
|
||||||
kinds: number[];
|
kinds: number[];
|
||||||
|
|
@ -130,14 +118,9 @@ export function useShop(identifier: string, pubkey?: string) {
|
||||||
if (pubkey) oldFilter.authors = [pubkey];
|
if (pubkey) oldFilter.authors = [pubkey];
|
||||||
|
|
||||||
const oldShops = await querySkateContent([oldFilter], { signal });
|
const oldShops = await querySkateContent([oldFilter], { signal });
|
||||||
console.log(`[useShop] Received ${oldShops.length} old shops`);
|
|
||||||
|
|
||||||
const allEvents = [...newShops, ...oldShops];
|
const allEvents = [...newShops, ...oldShops];
|
||||||
|
|
||||||
if (allEvents.length === 0) {
|
if (allEvents.length === 0) return null;
|
||||||
console.warn(`[useShop] No events found for identifier: ${identifier}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If multiple events, take the most recent
|
// If multiple events, take the most recent
|
||||||
const mostRecent = allEvents.reduce((latest, current) =>
|
const mostRecent = allEvents.reduce((latest, current) =>
|
||||||
|
|
@ -174,8 +157,9 @@ export function useShop(identifier: string, pubkey?: string) {
|
||||||
return shop;
|
return shop;
|
||||||
},
|
},
|
||||||
enabled: !!identifier,
|
enabled: !!identifier,
|
||||||
staleTime: 30000,
|
staleTime: 5 * 60 * 1000,
|
||||||
refetchOnMount: 'always',
|
gcTime: 30 * 60 * 1000,
|
||||||
|
refetchOnMount: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,7 +192,8 @@ export function useUserShops(pubkey: string) {
|
||||||
return shops.sort((a, b) => b.createdAt - a.createdAt);
|
return shops.sort((a, b) => b.createdAt - a.createdAt);
|
||||||
},
|
},
|
||||||
enabled: !!pubkey,
|
enabled: !!pubkey,
|
||||||
staleTime: 30000,
|
staleTime: 5 * 60 * 1000,
|
||||||
refetchOnMount: 'always',
|
gcTime: 30 * 60 * 1000,
|
||||||
|
refetchOnMount: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,11 @@ import { SKATE_SPOT_KIND, parseSkateSpot, type SkateSpot } from '@/lib/types';
|
||||||
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
||||||
import { cacheSpots, getAllCachedSpots, isOffline } from '@/lib/offlineCache';
|
import { cacheSpots, getAllCachedSpots, isOffline } from '@/lib/offlineCache';
|
||||||
|
|
||||||
|
// Spots change infrequently — cache for 5 minutes, keep for 30 minutes
|
||||||
|
const SPOTS_STALE_TIME = 5 * 60 * 1000; // 5 min
|
||||||
|
const SPOTS_GC_TIME = 30 * 60 * 1000; // 30 min
|
||||||
|
const QUERY_TIMEOUT = 8000; // 8s — fail fast, show error sooner
|
||||||
|
|
||||||
export function useSkateSpots(limit = 100) {
|
export function useSkateSpots(limit = 100) {
|
||||||
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
||||||
|
|
||||||
|
|
@ -11,12 +16,11 @@ export function useSkateSpots(limit = 100) {
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
// If offline, return cached spots
|
// If offline, return cached spots
|
||||||
if (isOffline()) {
|
if (isOffline()) {
|
||||||
console.log('[useSkateSpots] Offline, returning cached spots');
|
|
||||||
const cachedSpots = await getAllCachedSpots();
|
const cachedSpots = await getAllCachedSpots();
|
||||||
return cachedSpots.sort((a, b) => b.createdAt - a.createdAt).slice(0, limit);
|
return cachedSpots.sort((a, b) => b.createdAt - a.createdAt).slice(0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
const signal = AbortSignal.timeout(30000);
|
const signal = AbortSignal.timeout(QUERY_TIMEOUT);
|
||||||
|
|
||||||
const events = await querySkateContent(
|
const events = await querySkateContent(
|
||||||
[
|
[
|
||||||
|
|
@ -31,32 +35,26 @@ export function useSkateSpots(limit = 100) {
|
||||||
|
|
||||||
const spots: SkateSpot[] = [];
|
const spots: SkateSpot[] = [];
|
||||||
const spotsWithEvents: Array<{ event: typeof events[0]; spot: SkateSpot }> = [];
|
const spotsWithEvents: Array<{ event: typeof events[0]; spot: SkateSpot }> = [];
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const spot = parseSkateSpot(event);
|
const spot = parseSkateSpot(event);
|
||||||
if (spot) {
|
if (spot) {
|
||||||
spots.push(spot);
|
spots.push(spot);
|
||||||
spotsWithEvents.push({ event, spot });
|
spotsWithEvents.push({ event, spot });
|
||||||
} else {
|
|
||||||
console.warn('[useSkateSpots] Failed to parse event:', event);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[useSkateSpots] Parsed spots:', spots.length);
|
|
||||||
|
|
||||||
// Cache spots in the background for offline use
|
// Cache spots in the background for offline use
|
||||||
if (spotsWithEvents.length > 0) {
|
if (spotsWithEvents.length > 0) {
|
||||||
cacheSpots(spotsWithEvents).catch(err =>
|
cacheSpots(spotsWithEvents).catch(() => {});
|
||||||
console.warn('[useSkateSpots] Failed to cache spots:', err)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by created_at descending
|
|
||||||
return spots.sort((a, b) => b.createdAt - a.createdAt);
|
return spots.sort((a, b) => b.createdAt - a.createdAt);
|
||||||
},
|
},
|
||||||
staleTime: 30000,
|
staleTime: SPOTS_STALE_TIME,
|
||||||
gcTime: 300000,
|
gcTime: SPOTS_GC_TIME,
|
||||||
refetchOnMount: 'always',
|
// Don't force a relay round-trip on every navigation — use cache if fresh
|
||||||
|
refetchOnMount: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,16 +66,12 @@ export function useSkateSpot(identifier: string, pubkey?: string) {
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
// If offline, try to get from cache first
|
// If offline, try to get from cache first
|
||||||
if (isOffline() && pubkey) {
|
if (isOffline() && pubkey) {
|
||||||
console.log('[useSkateSpot] Offline, checking cache');
|
|
||||||
const { getCachedSpot } = await import('@/lib/offlineCache');
|
const { getCachedSpot } = await import('@/lib/offlineCache');
|
||||||
const cached = await getCachedSpot(pubkey, identifier);
|
const cached = await getCachedSpot(pubkey, identifier);
|
||||||
if (cached) {
|
if (cached) return cached;
|
||||||
console.log('[useSkateSpot] Found in cache:', identifier);
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const signal = AbortSignal.timeout(30000);
|
const signal = AbortSignal.timeout(QUERY_TIMEOUT);
|
||||||
|
|
||||||
const filter: {
|
const filter: {
|
||||||
kinds: number[];
|
kinds: number[];
|
||||||
|
|
@ -96,26 +90,22 @@ export function useSkateSpot(identifier: string, pubkey?: string) {
|
||||||
|
|
||||||
const events = await querySkateContent([filter], { signal });
|
const events = await querySkateContent([filter], { signal });
|
||||||
|
|
||||||
if (events.length === 0) {
|
if (events.length === 0) return null;
|
||||||
console.warn(`[useSkateSpot] No events found for identifier: ${identifier}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const spot = parseSkateSpot(events[0]);
|
const spot = parseSkateSpot(events[0]);
|
||||||
|
|
||||||
// Cache the spot for offline use
|
// Cache the spot for offline use
|
||||||
if (spot) {
|
if (spot) {
|
||||||
const { cacheSpot } = await import('@/lib/offlineCache');
|
const { cacheSpot } = await import('@/lib/offlineCache');
|
||||||
cacheSpot(events[0], spot).catch(err =>
|
cacheSpot(events[0], spot).catch(() => {});
|
||||||
console.warn('[useSkateSpot] Failed to cache spot:', err)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return spot;
|
return spot;
|
||||||
},
|
},
|
||||||
enabled: !!identifier,
|
enabled: !!identifier,
|
||||||
staleTime: 30000,
|
staleTime: SPOTS_STALE_TIME,
|
||||||
refetchOnMount: 'always',
|
gcTime: SPOTS_GC_TIME,
|
||||||
|
refetchOnMount: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,7 +115,7 @@ export function useUserSkateSpots(pubkey: string) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['user-skate-spots', pubkey, getQueryKeySuffix()],
|
queryKey: ['user-skate-spots', pubkey, getQueryKeySuffix()],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const signal = AbortSignal.timeout(30000);
|
const signal = AbortSignal.timeout(QUERY_TIMEOUT);
|
||||||
|
|
||||||
const events = await querySkateContent(
|
const events = await querySkateContent(
|
||||||
[
|
[
|
||||||
|
|
@ -148,7 +138,8 @@ export function useUserSkateSpots(pubkey: string) {
|
||||||
return spots.sort((a, b) => b.createdAt - a.createdAt);
|
return spots.sort((a, b) => b.createdAt - a.createdAt);
|
||||||
},
|
},
|
||||||
enabled: !!pubkey,
|
enabled: !!pubkey,
|
||||||
staleTime: 30000,
|
staleTime: SPOTS_STALE_TIME,
|
||||||
refetchOnMount: 'always',
|
gcTime: SPOTS_GC_TIME,
|
||||||
|
refetchOnMount: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,17 @@ import { useQuery } from '@tanstack/react-query';
|
||||||
import { SKATE_TOUR_KIND, parseSkateTour, type SkateTour } from '@/lib/types';
|
import { SKATE_TOUR_KIND, parseSkateTour, type SkateTour } from '@/lib/types';
|
||||||
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
||||||
|
|
||||||
|
const STALE_TIME = 5 * 60 * 1000; // 5 min
|
||||||
|
const GC_TIME = 30 * 60 * 1000; // 30 min
|
||||||
|
const TIMEOUT = 8000;
|
||||||
|
|
||||||
export function useTours(limit = 100) {
|
export function useTours(limit = 100) {
|
||||||
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['skate-tours', limit, getQueryKeySuffix()],
|
queryKey: ['skate-tours', limit, getQueryKeySuffix()],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const signal = AbortSignal.timeout(10000);
|
const signal = AbortSignal.timeout(TIMEOUT);
|
||||||
|
|
||||||
console.log('[useTours] Querying for tours');
|
|
||||||
|
|
||||||
const events = await querySkateContent(
|
const events = await querySkateContent(
|
||||||
[
|
[
|
||||||
|
|
@ -23,26 +25,17 @@ export function useTours(limit = 100) {
|
||||||
{ signal }
|
{ signal }
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`[useTours] Received ${events.length} events`);
|
|
||||||
|
|
||||||
const tours: SkateTour[] = [];
|
const tours: SkateTour[] = [];
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const tour = parseSkateTour(event);
|
const tour = parseSkateTour(event);
|
||||||
if (tour) {
|
if (tour) tours.push(tour);
|
||||||
tours.push(tour);
|
|
||||||
} else {
|
|
||||||
console.warn('[useTours] Failed to parse event:', event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[useTours] Parsed tours:', tours.length);
|
|
||||||
|
|
||||||
// Sort by created_at descending
|
|
||||||
return tours.sort((a, b) => b.createdAt - a.createdAt);
|
return tours.sort((a, b) => b.createdAt - a.createdAt);
|
||||||
},
|
},
|
||||||
staleTime: 30000,
|
staleTime: STALE_TIME,
|
||||||
gcTime: 300000,
|
gcTime: GC_TIME,
|
||||||
refetchOnMount: 'always',
|
refetchOnMount: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +45,7 @@ export function useTour(identifier: string, pubkey?: string) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['skate-tour', identifier, pubkey, getQueryKeySuffix()],
|
queryKey: ['skate-tour', identifier, pubkey, getQueryKeySuffix()],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const signal = AbortSignal.timeout(10000);
|
const signal = AbortSignal.timeout(TIMEOUT);
|
||||||
|
|
||||||
const filter: {
|
const filter: {
|
||||||
kinds: number[];
|
kinds: number[];
|
||||||
|
|
@ -69,22 +62,16 @@ export function useTour(identifier: string, pubkey?: string) {
|
||||||
filter.authors = [pubkey];
|
filter.authors = [pubkey];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[useTour] Querying for tour: ${identifier}`);
|
|
||||||
|
|
||||||
const events = await querySkateContent([filter], { signal });
|
const events = await querySkateContent([filter], { signal });
|
||||||
|
|
||||||
console.log(`[useTour] Received ${events.length} events`);
|
if (events.length === 0) return null;
|
||||||
|
|
||||||
if (events.length === 0) {
|
|
||||||
console.warn(`[useTour] No events found for identifier: ${identifier}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseSkateTour(events[0]);
|
return parseSkateTour(events[0]);
|
||||||
},
|
},
|
||||||
enabled: !!identifier,
|
enabled: !!identifier,
|
||||||
staleTime: 30000,
|
staleTime: STALE_TIME,
|
||||||
refetchOnMount: 'always',
|
gcTime: GC_TIME,
|
||||||
|
refetchOnMount: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,7 +81,7 @@ export function useUserTours(pubkey: string) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['user-skate-tours', pubkey, getQueryKeySuffix()],
|
queryKey: ['user-skate-tours', pubkey, getQueryKeySuffix()],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const signal = AbortSignal.timeout(10000);
|
const signal = AbortSignal.timeout(TIMEOUT);
|
||||||
|
|
||||||
const events = await querySkateContent(
|
const events = await querySkateContent(
|
||||||
[
|
[
|
||||||
|
|
@ -117,7 +104,8 @@ export function useUserTours(pubkey: string) {
|
||||||
return tours.sort((a, b) => b.createdAt - a.createdAt);
|
return tours.sort((a, b) => b.createdAt - a.createdAt);
|
||||||
},
|
},
|
||||||
enabled: !!pubkey,
|
enabled: !!pubkey,
|
||||||
staleTime: 30000,
|
staleTime: STALE_TIME,
|
||||||
refetchOnMount: 'always',
|
gcTime: GC_TIME,
|
||||||
|
refetchOnMount: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { useState, useMemo, useEffect } from 'react';
|
import { useState, useMemo, useEffect, lazy, Suspense } from 'react';
|
||||||
import { useSeoMeta } from '@unhead/react';
|
import { useSeoMeta } from '@unhead/react';
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { Layout } from '@/components/Layout';
|
import { Layout } from '@/components/Layout';
|
||||||
import { SpotMap } from '@/components/SpotMap';
|
|
||||||
|
// Lazy-load the map — Leaflet is large (~150KB) and only needed on this page
|
||||||
|
const SpotMap = lazy(() => import('@/components/SpotMap').then(m => ({ default: m.SpotMap })));
|
||||||
import { SpotCard, SpotCardSkeleton } from '@/components/SpotCard';
|
import { SpotCard, SpotCardSkeleton } from '@/components/SpotCard';
|
||||||
import { WeatherToggle } from '@/components/WeatherToggle';
|
import { WeatherToggle } from '@/components/WeatherToggle';
|
||||||
import { TrustFilter, type TrustFilterLevel, filterByTrustLevel } from '@/components/TrustFilter';
|
import { TrustFilter, type TrustFilterLevel, filterByTrustLevel } from '@/components/TrustFilter';
|
||||||
|
|
@ -1061,25 +1063,34 @@ export default function MapPage() {
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
{viewMode === 'map' ? (
|
{viewMode === 'map' ? (
|
||||||
<>
|
<>
|
||||||
<SpotMap
|
<Suspense fallback={
|
||||||
spots={filteredSpots}
|
<div className="h-full bg-black/50 flex items-center justify-center">
|
||||||
marketplaceItems={marketplaceItems || []}
|
<div className="flex items-center gap-3">
|
||||||
tours={tours || []}
|
<div className="w-6 h-6 border-2 border-[#ff0040] border-t-transparent rounded-full animate-spin" />
|
||||||
btcMapPlaces={btcMapPlaces || []}
|
<span className="text-sm text-white font-bold">Loading map...</span>
|
||||||
selectedSpot={selectedSpot}
|
</div>
|
||||||
onSpotClick={setSelectedSpot}
|
</div>
|
||||||
onMapClick={handleMapClick}
|
}>
|
||||||
onMapMove={setMapCenter}
|
<SpotMap
|
||||||
enableClickToAdd={!!user}
|
spots={filteredSpots}
|
||||||
className="h-full"
|
marketplaceItems={marketplaceItems || []}
|
||||||
weatherEnabled={weatherEnabled}
|
tours={tours || []}
|
||||||
weatherData={weatherData}
|
btcMapPlaces={btcMapPlaces || []}
|
||||||
showSpots={showSpots}
|
selectedSpot={selectedSpot}
|
||||||
showShops={showShops}
|
onSpotClick={setSelectedSpot}
|
||||||
showGear={showGear}
|
onMapClick={handleMapClick}
|
||||||
showTours={showTours}
|
onMapMove={setMapCenter}
|
||||||
showBtcMap={showBtcMap}
|
enableClickToAdd={!!user}
|
||||||
/>
|
className="h-full"
|
||||||
|
weatherEnabled={weatherEnabled}
|
||||||
|
weatherData={weatherData}
|
||||||
|
showSpots={showSpots}
|
||||||
|
showShops={showShops}
|
||||||
|
showGear={showGear}
|
||||||
|
showTours={showTours}
|
||||||
|
showBtcMap={showBtcMap}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
{/* Loading indicator */}
|
{/* Loading indicator */}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="absolute top-4 left-1/2 -translate-x-1/2 z-[1000] bg-black/90 backdrop-blur px-4 py-2 rounded-lg shadow-lg border border-[#ff0040]/30">
|
<div className="absolute top-4 left-1/2 -translate-x-1/2 z-[1000] bg-black/90 backdrop-blur px-4 py-2 rounded-lg shadow-lg border border-[#ff0040]/30">
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export default defineConfig(() => ({
|
||||||
return !log.includes("React Router Future Flag Warning");
|
return !log.includes("React Router Future Flag Warning");
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
DEBUG_PRINT_LIMIT: '0', // Suppress DOM output that exceeds AI context windows
|
DEBUG_PRINT_LIMIT: '0',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|
@ -32,8 +32,55 @@ export default defineConfig(() => ({
|
||||||
exclude: ['@lightninglabs/lnc-web'],
|
exclude: ['@lightninglabs/lnc-web'],
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
// Strip console.log/warn/debug in production (keeps console.error for real errors)
|
||||||
|
minify: 'esbuild',
|
||||||
commonjsOptions: {
|
commonjsOptions: {
|
||||||
transformMixedEsModules: true,
|
transformMixedEsModules: true,
|
||||||
},
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
// Split large vendor libraries into separate cacheable chunks.
|
||||||
|
// Users only re-download a chunk when that specific library updates.
|
||||||
|
manualChunks(id) {
|
||||||
|
// Map — Leaflet is large (~150KB), isolate it so non-map pages don't pay for it
|
||||||
|
if (id.includes('leaflet') || id.includes('ngeohash')) {
|
||||||
|
return 'vendor-map';
|
||||||
|
}
|
||||||
|
// Nostr core — changes infrequently, worth caching separately
|
||||||
|
if (id.includes('nostr-tools') || id.includes('@nostrify') || id.includes('@noble')) {
|
||||||
|
return 'vendor-nostr';
|
||||||
|
}
|
||||||
|
// Bitcoin / Lightning — large, rarely changes
|
||||||
|
if (
|
||||||
|
id.includes('@cashu') ||
|
||||||
|
id.includes('bitcoinjs-lib') ||
|
||||||
|
id.includes('@scure') ||
|
||||||
|
id.includes('@bitcoinerlab') ||
|
||||||
|
id.includes('ecpair') ||
|
||||||
|
id.includes('@getalby') ||
|
||||||
|
id.includes('npubcash')
|
||||||
|
) {
|
||||||
|
return 'vendor-bitcoin';
|
||||||
|
}
|
||||||
|
// React ecosystem — very stable, long cache life
|
||||||
|
if (id.includes('react-dom') || id.includes('react-router')) {
|
||||||
|
return 'vendor-react';
|
||||||
|
}
|
||||||
|
// Radix UI components — large but stable
|
||||||
|
if (id.includes('@radix-ui')) {
|
||||||
|
return 'vendor-radix';
|
||||||
|
}
|
||||||
|
// TanStack Query
|
||||||
|
if (id.includes('@tanstack')) {
|
||||||
|
return 'vendor-query';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}));
|
// Strip console.log/warn/debug calls from production build
|
||||||
|
esbuild: {
|
||||||
|
drop: ['debugger'],
|
||||||
|
pure: ['console.log', 'console.warn', 'console.debug'],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue