133 lines
4.8 KiB
TypeScript
133 lines
4.8 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
import { MARKETPLACE_ITEM_KIND, parseMarketplaceItem, type MarketplaceItem } from '@/lib/types';
|
|
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
|
import { useAdminModeration, filterAdminHidden } from '@/hooks/useAdminModeration';
|
|
|
|
const STALE_TIME = 5 * 60 * 1000; // 5 min
|
|
const GC_TIME = 30 * 60 * 1000; // 30 min
|
|
const TIMEOUT = 8000;
|
|
|
|
export function useMarketplaceItems(limit = 100, options: { includeHidden?: boolean; enabled?: boolean } = {}) {
|
|
const { includeHidden = false, enabled = true } = options;
|
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
|
const hidden = useAdminModeration();
|
|
|
|
return useQuery({
|
|
queryKey: ['marketplace-items', limit, getQueryKeySuffix()],
|
|
enabled,
|
|
// `includeHidden`: admin tooling only (review queue + undo).
|
|
select: (items: MarketplaceItem[]) =>
|
|
includeHidden ? items : filterAdminHidden(items, MARKETPLACE_ITEM_KIND, hidden),
|
|
queryFn: async () => {
|
|
const signal = AbortSignal.timeout(TIMEOUT);
|
|
|
|
const events = await querySkateContent(
|
|
[
|
|
{
|
|
kinds: [MARKETPLACE_ITEM_KIND],
|
|
'#t': ['skateboarding'],
|
|
limit,
|
|
},
|
|
],
|
|
{ signal, requireAny: true }
|
|
);
|
|
|
|
const items: MarketplaceItem[] = [];
|
|
for (const event of events) {
|
|
// Skip showcase-only Stash items (public gear that is NOT for sale):
|
|
// they belong on the profile's public Stash, not the sellable marketplace.
|
|
const isShowcaseOnly = event.tags.some((t) => t[0] === 'showcase' && t[1] === 'true');
|
|
if (isShowcaseOnly) continue;
|
|
const item = parseMarketplaceItem(event);
|
|
if (item) items.push(item);
|
|
}
|
|
|
|
return items.sort((a, b) => b.createdAt - a.createdAt);
|
|
},
|
|
staleTime: STALE_TIME,
|
|
gcTime: GC_TIME,
|
|
refetchOnMount: true,
|
|
});
|
|
}
|
|
|
|
export function useMarketplaceAtSpot(spotPubkey: string, spotIdentifier: string, limit = 20) {
|
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
|
const hidden = useAdminModeration();
|
|
|
|
return useQuery({
|
|
queryKey: ['marketplace-at-spot', spotPubkey, spotIdentifier, getQueryKeySuffix()],
|
|
select: (items: MarketplaceItem[]) => filterAdminHidden(items, MARKETPLACE_ITEM_KIND, hidden),
|
|
queryFn: async () => {
|
|
const signal = AbortSignal.timeout(TIMEOUT);
|
|
|
|
const spotRef = `30818:${spotPubkey}:${spotIdentifier}`;
|
|
|
|
const events = await querySkateContent(
|
|
[
|
|
{
|
|
kinds: [MARKETPLACE_ITEM_KIND],
|
|
'#a': [spotRef],
|
|
'#t': ['skateboarding'],
|
|
limit,
|
|
},
|
|
],
|
|
{ signal }
|
|
);
|
|
|
|
const items: MarketplaceItem[] = [];
|
|
for (const event of events) {
|
|
const isShowcaseOnly = event.tags.some((t) => t[0] === 'showcase' && t[1] === 'true');
|
|
if (isShowcaseOnly) continue;
|
|
const item = parseMarketplaceItem(event);
|
|
if (item && (!item.status || item.status === 'active')) {
|
|
items.push(item);
|
|
}
|
|
}
|
|
|
|
return items.sort((a, b) => b.createdAt - a.createdAt);
|
|
},
|
|
enabled: !!spotPubkey && !!spotIdentifier,
|
|
staleTime: STALE_TIME,
|
|
gcTime: GC_TIME,
|
|
refetchOnMount: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetch a single marketplace listing by author + `d` (including sold/inactive).
|
|
* Admin-hidden listings resolve to null and showcase-only ("not for sale") items
|
|
* are skipped, so a direct `/listing/...` URL cannot surface content the
|
|
* marketplace has removed or that was never for sale.
|
|
*/
|
|
export function useMarketplaceItem(pubkey?: string, identifier?: string) {
|
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
|
const hidden = useAdminModeration();
|
|
|
|
return useQuery({
|
|
queryKey: ['marketplace-item', pubkey, identifier, getQueryKeySuffix()],
|
|
enabled: !!pubkey && !!identifier,
|
|
staleTime: STALE_TIME,
|
|
gcTime: GC_TIME,
|
|
select: (item: MarketplaceItem | null) =>
|
|
item &&
|
|
!hidden.has({
|
|
coord: `${MARKETPLACE_ITEM_KIND}:${item.pubkey}:${item.identifier}`,
|
|
id: item.id,
|
|
})
|
|
? item
|
|
: null,
|
|
queryFn: async (): Promise<MarketplaceItem | null> => {
|
|
if (!pubkey || !identifier) return null;
|
|
const signal = AbortSignal.timeout(TIMEOUT);
|
|
const events = await querySkateContent(
|
|
[{ kinds: [MARKETPLACE_ITEM_KIND], authors: [pubkey], '#d': [identifier], limit: 5 }],
|
|
{ signal, requireAny: true },
|
|
);
|
|
const newest = [...events].sort((a, b) => b.created_at - a.created_at)[0];
|
|
if (!newest) return null;
|
|
// Showcase-only (public Stash gear, NOT for sale) never reaches a listing URL.
|
|
if (newest.tags.some((t) => t[0] === 'showcase' && t[1] === 'true')) return null;
|
|
return parseMarketplaceItem(newest);
|
|
},
|
|
});
|
|
}
|