SkateSpots/src/hooks/useSellerInfo.ts
shakespeare.diy e29d35f1e4 fix: restore full skate-spots source tree from misplaced src/src/ copy
The entire skate-spots codebase was accidentally copied into src/src/ and
public/public/ instead of src/ and public/ directly. This left the real
src/ directory as blank MKStack template stubs, explaining why the landing
page showed the default template instead of SkateSpots.

Changes made:
- Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx,
  all pages, components, hooks, contexts, lib, i18n, test)
- Copied all files from public/public/ up to public/ (images, manifest, etc.)
- Deleted src/src/ and public/public/ nested directories
- Removed HelloWorld.tsx template stub from pages/
- Retained the previously fixed index.css (Tailwind v3 directives)

Full skate-spots feature set is now active:
- All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.)
- NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler
- Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.)
- Outfit Variable font, Amber pre-check in main.tsx
- All hooks (useSkateSpots, useCoaches, useZaps, etc.)
- i18n support (en/es)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 22:18:28 +07:00

161 lines
5 KiB
TypeScript

import { useNostr } from '@nostrify/react';
import { useQuery } from '@tanstack/react-query';
import { MARKETPLACE_ITEM_KIND } from '@/lib/types';
import { useAppContext } from '@/hooks/useAppContext';
export interface SellerInfo {
pubkey: string;
accountAge: number; // Days since first event
listingCount: number;
activeListingCount: number;
hasNip05: boolean;
nip05?: string;
firstSeen?: number; // Unix timestamp of first event
}
/**
* Hook to fetch seller information including account age and listing count.
*/
export function useSellerInfo(pubkey: string | undefined) {
const { nostr } = useNostr();
const { config } = useAppContext();
const relayUrls = config.relayMetadata.relays
.filter(r => r.read)
.map(r => r.url)
.sort()
.join(',');
return useQuery({
queryKey: ['seller-info', pubkey, relayUrls],
queryFn: async (c) => {
if (!pubkey) return null;
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(10000)]);
try {
const readRelays = config.relayMetadata.relays
.filter(r => r.read)
.map(r => r.url);
// Query profile (kind 0) and marketplace listings in parallel
const profileEventsPromise: Promise<any[]>[] = [];
const listingEventsPromise: Promise<any[]>[] = [];
for (const relayUrl of readRelays) {
try {
const relay = nostr.relay(relayUrl);
profileEventsPromise.push(
relay.query(
[{ kinds: [0], authors: [pubkey], limit: 1 }],
{ signal }
).catch(() => [])
);
listingEventsPromise.push(
relay.query(
[{
kinds: [MARKETPLACE_ITEM_KIND],
authors: [pubkey],
'#t': ['skateboarding'],
limit: 100
}],
{ signal }
).catch(() => [])
);
} catch (error) {
// Ignore individual relay errors
}
}
const [profileResults, listingResults] = await Promise.all([
Promise.all(profileEventsPromise),
Promise.all(listingEventsPromise),
]);
// Flatten results
const profileEvents = profileResults.flat();
const listingEvents = listingResults.flat();
// Get profile metadata
let hasNip05 = false;
let nip05: string | undefined;
let profileCreatedAt: number | undefined;
if (profileEvents.length > 0) {
const profile = profileEvents.reduce((latest, current) =>
current.created_at > latest.created_at ? current : latest
);
try {
const metadata = JSON.parse(profile.content);
hasNip05 = !!metadata.nip05;
nip05 = metadata.nip05;
} catch (e) {
// Ignore JSON parse errors
}
profileCreatedAt = profile.created_at;
}
// Deduplicate listings by d-tag
const listingMap = new Map<string, any>();
for (const event of listingEvents) {
const d = event.tags.find(([name]: [string]) => name === 'd')?.[1];
if (d) {
const key = `${event.pubkey}:${event.kind}:${d}`;
const existing = listingMap.get(key);
if (!existing || event.created_at > existing.created_at) {
listingMap.set(key, event);
}
}
}
const uniqueListings = Array.from(listingMap.values());
// Count active listings (not expired, status = active)
const now = Math.floor(Date.now() / 1000);
const activeListings = uniqueListings.filter(event => {
const status = event.tags.find(([name]: [string]) => name === 'status')?.[1];
const expiration = event.tags.find(([name]: [string]) => name === 'expiration')?.[1];
if (status && status !== 'active') return false;
if (expiration && parseInt(expiration) < now) return false;
return true;
});
// Find oldest event timestamp (approximation of account age)
const allTimestamps = [
...uniqueListings.map(e => e.created_at),
...(profileCreatedAt ? [profileCreatedAt] : []),
];
const firstSeen = allTimestamps.length > 0
? Math.min(...allTimestamps)
: undefined;
const accountAge = firstSeen
? Math.floor((now - firstSeen) / (24 * 60 * 60)) // Days
: 0;
return {
pubkey,
accountAge,
listingCount: uniqueListings.length,
activeListingCount: activeListings.length,
hasNip05,
nip05,
firstSeen,
} as SellerInfo;
} catch (error) {
console.warn('[useSellerInfo] Error fetching seller info:', error);
return null;
}
},
enabled: !!pubkey,
staleTime: 300000, // Cache for 5 minutes
gcTime: 600000, // Keep in cache for 10 minutes
});
}