Files
GearBox/src/client/hooks/useGlobalItems.ts
Jean-Luc Makiola 37edd0edfd feat(33-06): add market prices section to catalog detail page
- Add useGlobalItemPrices and useGlobalItemCommunityStats hooks
- Add MarketPricesSection component with user's market MSRP prominent
- Show community price stats per market with median and report count
- Collapsible "Other Markets" section (collapsed by default)
- Import useCurrency, useExchangeRates, formatPrice for market display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:09:56 +02:00

118 lines
2.9 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ApiError, apiDelete, apiGet, apiPost } from "../lib/api";
interface GlobalItem {
id: number;
brand: string;
model: string;
category: string | null;
weightGrams: number | null;
priceCents: number | null;
imageUrl: string | null;
description: string | null;
sourceUrl: string | null;
imageCredit: string | null;
imageSourceUrl: string | null;
createdAt: string;
}
interface GlobalItemWithOwnerCount extends GlobalItem {
ownerCount: number;
}
interface ItemGlobalLink {
id: number;
itemId: number;
globalItemId: number;
}
export function useGlobalItems(query?: string, tags?: string[]) {
const params = new URLSearchParams();
if (query) params.set("q", query);
if (tags && tags.length > 0) params.set("tags", tags.join(","));
const qs = params.toString();
return useQuery({
queryKey: ["global-items", query ?? "", tags ?? []],
queryFn: () =>
apiGet<GlobalItem[]>(`/api/global-items${qs ? `?${qs}` : ""}`),
});
}
export function useGlobalItem(id: number | null) {
return useQuery({
queryKey: ["global-items", id],
queryFn: () => apiGet<GlobalItemWithOwnerCount>(`/api/global-items/${id}`),
enabled: id != null,
retry: (count, error) =>
error instanceof ApiError && error.status === 404 ? false : count < 3,
});
}
export function useLinkItem() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
itemId,
globalItemId,
}: {
itemId: number;
globalItemId: number;
}) =>
apiPost<ItemGlobalLink>(`/api/items/${itemId}/link`, { globalItemId }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["items"] });
queryClient.invalidateQueries({ queryKey: ["global-items"] });
},
});
}
interface MarketPriceData {
id: number;
globalItemId: number;
market: string;
currency: string;
priceCents: number;
source: string | null;
createdAt: string;
}
interface CommunityPriceStat {
market: string;
currency: string;
medianPrice: number;
reportCount: number;
}
export function useGlobalItemPrices(globalItemId: number) {
return useQuery({
queryKey: ["global-item-prices", globalItemId],
queryFn: () =>
apiGet<{ marketPrices: MarketPriceData[] }>(
`/api/market-prices/global-items/${globalItemId}/prices`,
),
enabled: globalItemId > 0,
});
}
export function useGlobalItemCommunityStats(globalItemId: number) {
return useQuery({
queryKey: ["global-item-community-stats", globalItemId],
queryFn: () =>
apiGet<CommunityPriceStat[]>(`/api/community-prices/${globalItemId}`),
enabled: globalItemId > 0,
});
}
export function useUnlinkItem() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (itemId: number) =>
apiDelete<{ success: boolean }>(`/api/items/${itemId}/link`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["items"] });
queryClient.invalidateQueries({ queryKey: ["global-items"] });
},
});
}