+ {/* Quick actions */}
+
+
+
{/* Category filter pills */}
@@ -171,7 +185,6 @@ export function ShopPage() {
return (
- {/* Badge image */}
{heroImage ? (
- {/* Name */}
{badge.name}
- {/* Description */}
{badge.description && (
{badge.description}
)}
- {/* Price / Owned + Supply */}
{owned ? (
@@ -232,3 +242,167 @@ export function ShopPage() {
);
}
+
+// ─── NoteCard Skeleton ─────────────────────────────────────────────────────────
+
+function NoteCardSkeleton() {
+ return (
+
+ );
+}
+
+// ─── Page ──────────────────────────────────────────────────────────────────────
+
+export function ShopPage() {
+ const { config } = useAppContext();
+ const { user } = useCurrentUser();
+ const queryClient = useQueryClient();
+
+ const [activeTab, setActiveTab] = useState(() => {
+ try {
+ const stored = sessionStorage.getItem('ditto:feed-tab:shop');
+ if (stored === 'shop' || stored === 'follows' || stored === 'global') return stored;
+ } catch { /* ignore */ }
+ return 'shop';
+ });
+
+ const handleSetTab = useCallback((tab: ShopTab) => {
+ setActiveTab(tab);
+ try { sessionStorage.setItem('ditto:feed-tab:shop', tab); } catch { /* ignore */ }
+ }, []);
+
+ useSeoMeta({
+ title: `Badge Shop | ${config.appName}`,
+ description: 'Collect badges, discover new ones, and show them off on your profile',
+ });
+
+ // Feed query for follows/global tabs
+ const feedTab = activeTab === 'follows' ? 'follows' : 'global';
+ const feedQuery = useBadgeFeed(feedTab);
+
+ const {
+ data: rawData,
+ isPending,
+ isLoading,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ } = feedQuery;
+
+ // Auto-fetch page 2 for smoother scrolling
+ useEffect(() => {
+ if (activeTab !== 'shop' && hasNextPage && !isFetchingNextPage && rawData?.pages?.length === 1) {
+ fetchNextPage();
+ }
+ }, [activeTab, hasNextPage, isFetchingNextPage, rawData?.pages?.length, fetchNextPage]);
+
+ // Intersection observer for infinite scroll
+ const { ref: scrollRef, inView } = useInView({
+ threshold: 0,
+ rootMargin: '400px',
+ });
+
+ useEffect(() => {
+ if (inView && hasNextPage && !isFetchingNextPage) {
+ fetchNextPage();
+ }
+ }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
+
+ // Flatten and deduplicate feed events
+ const feedEvents = useMemo(() => {
+ if (!rawData?.pages) return [];
+ const seen = new Set();
+ return (rawData.pages as NostrEvent[][])
+ .flat()
+ .filter((event) => {
+ if (seen.has(event.id)) return false;
+ seen.add(event.id);
+ return true;
+ });
+ }, [rawData?.pages]);
+
+ const handleRefresh = useCallback(async () => {
+ await queryClient.invalidateQueries({ queryKey: ['badge-feed', feedTab] });
+ }, [queryClient, feedTab]);
+
+ const showSkeleton = activeTab !== 'shop' && (isPending || (isLoading && !rawData));
+
+ return (
+
+ {/* Page header */}
+
+
+
+
+
+
+
Badge Shop
+
+
+
+ {/* Tabs */}
+
+ handleSetTab('shop')} />
+ handleSetTab('follows')} disabled={!user} />
+ handleSetTab('global')} />
+
+
+ {/* Tab content */}
+ {activeTab === 'shop' ? (
+
+ ) : (
+
+ {showSkeleton ? (
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+ ) : feedEvents.length > 0 ? (
+
+ {feedEvents.map((event) => (
+
+ ))}
+ {hasNextPage && (
+
+ {isFetchingNextPage && (
+
+
+
+ )}
+
+ )}
+
+ ) : (
+ handleSetTab('global') : undefined}
+ />
+ )}
+
+ )}
+
+ );
+}