diff --git a/src/components/GearSwapItemCard.tsx b/src/components/GearSwapItemCard.tsx index 1a550bd..c25870e 100644 --- a/src/components/GearSwapItemCard.tsx +++ b/src/components/GearSwapItemCard.tsx @@ -127,12 +127,12 @@ export function GearSwapItemCard({ item, trustScore, salesCount }: GearSwapItemC return ( navigate(`/messages/${npub}`)} + onClick={() => navigate(`/listing/${item.pubkey}/${item.identifier}`)} > {/* Image */} diff --git a/src/components/LazyMount.tsx b/src/components/LazyMount.tsx new file mode 100644 index 0000000..ef00b23 --- /dev/null +++ b/src/components/LazyMount.tsx @@ -0,0 +1,60 @@ +import { useEffect, useRef, useState, type ReactNode } from 'react'; + +/** + * Defer mounting `children` until the wrapper scrolls near the viewport. + * + * Long grids (e.g. the marketplace) otherwise mount every card — and every + * hook/query/DOM node each one pulls in — up front. With this, off-screen cards + * stay as a cheap placeholder until they're ~600px away, then mount once and + * stay mounted. Falls back to immediate render when IntersectionObserver is + * unavailable. + */ +export function LazyMount({ + children, + minHeight = 300, + rootMargin = '600px', + className, +}: { + children: ReactNode; + /** Reserve height so the page doesn't jump when the card mounts. */ + minHeight?: number; + rootMargin?: string; + className?: string; +}) { + const ref = useRef(null); + const [visible, setVisible] = useState(false); + + useEffect(() => { + if (visible) return; + const el = ref.current; + if (!el) return; + + if (typeof IntersectionObserver === 'undefined') { + setVisible(true); + return; + } + + const observer = new IntersectionObserver( + (entries) => { + if (entries.some((e) => e.isIntersecting)) { + setVisible(true); + observer.disconnect(); + } + }, + { rootMargin }, + ); + observer.observe(el); + return () => observer.disconnect(); + }, [visible, rootMargin]); + + return ( +
+ {visible ? children : null} +
+ ); +} diff --git a/src/pages/GearSwapPage.tsx b/src/pages/GearSwapPage.tsx index 8a6168d..7e0af3c 100644 --- a/src/pages/GearSwapPage.tsx +++ b/src/pages/GearSwapPage.tsx @@ -13,6 +13,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMe import { useMarketplaceItems } from '@/hooks/useMarketplace'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useBatchTrustScores } from '@/hooks/useBatchTrustScores'; +import { LazyMount } from '@/components/LazyMount'; import { useBatchSellerSales } from '@/hooks/useSellerSales'; import { useFollowList } from '@/hooks/useFollowList'; import { GearSwapItemCard } from '@/components/GearSwapItemCard'; @@ -410,13 +411,14 @@ export default function GearSwapPage() { {!isLoading && !error && filteredItems.length > 0 && (
{filteredItems.map((item) => ( - + + + ))}
)}