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>
149 lines
4.6 KiB
TypeScript
149 lines
4.6 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
import { useNostr } from '@nostrify/react';
|
|
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
|
|
|
/**
|
|
* Hook to track completed sales for a seller.
|
|
*
|
|
* Sales are tracked via status changes in marketplace listings (kind 30402).
|
|
* A "sale" is counted when a listing has status = 'sold'.
|
|
*
|
|
* Future enhancement: Could also track via kind 1111 comments with specific tags
|
|
* to confirm receipt/completion of sales.
|
|
*/
|
|
export function useSellerSales(pubkey: string | undefined) {
|
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
|
|
|
return useQuery({
|
|
queryKey: ['seller-sales', pubkey, getQueryKeySuffix()],
|
|
queryFn: async () => {
|
|
if (!pubkey) return { totalSales: 0, recentSales: [] };
|
|
|
|
const signal = AbortSignal.timeout(8000);
|
|
|
|
try {
|
|
// Query all marketplace listings by this seller
|
|
const events = await querySkateContent(
|
|
[
|
|
{
|
|
kinds: [30402], // NIP-99 Classified Listings
|
|
authors: [pubkey],
|
|
'#t': ['skateboarding'],
|
|
limit: 200,
|
|
},
|
|
],
|
|
{ signal }
|
|
);
|
|
|
|
// Deduplicate by d-tag (keep latest version of each listing)
|
|
const listingMap = new Map<string, typeof events[0]>();
|
|
for (const event of events) {
|
|
const d = event.tags.find(([name]) => name === 'd')?.[1];
|
|
if (d) {
|
|
const existing = listingMap.get(d);
|
|
if (!existing || event.created_at > existing.created_at) {
|
|
listingMap.set(d, event);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Count listings with status = 'sold'
|
|
const allListings = Array.from(listingMap.values());
|
|
const soldListings = allListings.filter(event => {
|
|
const status = event.tags.find(([name]) => name === 'status')?.[1];
|
|
return status === 'sold';
|
|
});
|
|
|
|
// Get recent sales (last 5)
|
|
const recentSales = soldListings
|
|
.sort((a, b) => b.created_at - a.created_at)
|
|
.slice(0, 5)
|
|
.map(event => ({
|
|
id: event.id,
|
|
title: event.tags.find(([name]) => name === 'title')?.[1] || 'Unknown item',
|
|
soldAt: event.created_at,
|
|
}));
|
|
|
|
return {
|
|
totalSales: soldListings.length,
|
|
recentSales,
|
|
};
|
|
} catch (error) {
|
|
console.warn('[useSellerSales] Error:', error);
|
|
return { totalSales: 0, recentSales: [] };
|
|
}
|
|
},
|
|
enabled: !!pubkey,
|
|
staleTime: 300000, // 5 minutes
|
|
gcTime: 600000, // 10 minutes
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Batch hook to get sales for multiple sellers at once
|
|
*/
|
|
export function useBatchSellerSales(pubkeys: string[]) {
|
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
|
|
|
return useQuery({
|
|
queryKey: ['batch-seller-sales', pubkeys.sort().join(','), getQueryKeySuffix()],
|
|
queryFn: async () => {
|
|
if (pubkeys.length === 0) return new Map<string, number>();
|
|
|
|
const signal = AbortSignal.timeout(10000);
|
|
|
|
try {
|
|
// Query listings for all sellers at once
|
|
const events = await querySkateContent(
|
|
[
|
|
{
|
|
kinds: [30402],
|
|
authors: pubkeys,
|
|
'#t': ['skateboarding'],
|
|
limit: 500,
|
|
},
|
|
],
|
|
{ signal }
|
|
);
|
|
|
|
// Group by pubkey and count sold items
|
|
const salesByPubkey = new Map<string, number>();
|
|
|
|
// Initialize all pubkeys with 0
|
|
for (const pk of pubkeys) {
|
|
salesByPubkey.set(pk, 0);
|
|
}
|
|
|
|
// Deduplicate by pubkey+d-tag (keep latest version of each listing)
|
|
const listingMap = new Map<string, typeof events[0]>();
|
|
for (const event of events) {
|
|
const d = event.tags.find(([name]) => name === 'd')?.[1];
|
|
if (d) {
|
|
const key = `${event.pubkey}:${d}`;
|
|
const existing = listingMap.get(key);
|
|
if (!existing || event.created_at > existing.created_at) {
|
|
listingMap.set(key, event);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Count sold listings per pubkey
|
|
for (const event of listingMap.values()) {
|
|
const status = event.tags.find(([name]) => name === 'status')?.[1];
|
|
if (status === 'sold') {
|
|
const current = salesByPubkey.get(event.pubkey) || 0;
|
|
salesByPubkey.set(event.pubkey, current + 1);
|
|
}
|
|
}
|
|
|
|
return salesByPubkey;
|
|
} catch (error) {
|
|
console.warn('[useBatchSellerSales] Error:', error);
|
|
return new Map<string, number>();
|
|
}
|
|
},
|
|
enabled: pubkeys.length > 0,
|
|
staleTime: 300000,
|
|
gcTime: 600000,
|
|
});
|
|
}
|