From 37edd0edfdbbc5b104ba5a367f7be33e2b7ac701 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 13 Apr 2026 18:09:56 +0200 Subject: [PATCH] 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) --- src/client/hooks/useGlobalItems.ts | 37 ++++++ .../routes/global-items/$globalItemId.tsx | 109 +++++++++++++++++- 2 files changed, 145 insertions(+), 1 deletion(-) diff --git a/src/client/hooks/useGlobalItems.ts b/src/client/hooks/useGlobalItems.ts index ecfff5b..d492346 100644 --- a/src/client/hooks/useGlobalItems.ts +++ b/src/client/hooks/useGlobalItems.ts @@ -67,6 +67,43 @@ export function useLinkItem() { }); } +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(`/api/community-prices/${globalItemId}`), + enabled: globalItemId > 0, + }); +} + export function useUnlinkItem() { const queryClient = useQueryClient(); return useMutation({ diff --git a/src/client/routes/global-items/$globalItemId.tsx b/src/client/routes/global-items/$globalItemId.tsx index 609ddcb..7dc9f1c 100644 --- a/src/client/routes/global-items/$globalItemId.tsx +++ b/src/client/routes/global-items/$globalItemId.tsx @@ -1,8 +1,16 @@ import { createFileRoute, Link } from "@tanstack/react-router"; +import { useState } from "react"; import { GearImage, imageContainerBg } from "../../components/GearImage"; import { useAuth } from "../../hooks/useAuth"; +import { useCurrency } from "../../hooks/useCurrency"; +import { useExchangeRates } from "../../hooks/useExchangeRates"; import { useFormatters } from "../../hooks/useFormatters"; -import { useGlobalItem } from "../../hooks/useGlobalItems"; +import { + useGlobalItem, + useGlobalItemCommunityStats, + useGlobalItemPrices, +} from "../../hooks/useGlobalItems"; +import { formatPrice, type Currency } from "../../lib/formatters"; import { LucideIcon } from "../../lib/iconData"; import { useUIStore } from "../../stores/uiStore"; @@ -235,6 +243,9 @@ function GlobalItemDetail() { )} + {/* Market Prices Section */} + + {/* Product page link */} {item.sourceUrl && (
@@ -251,3 +262,99 @@ function GlobalItemDetail() {
); } + +function MarketPricesSection({ + globalItemId, +}: { globalItemId: number }) { + const { market: userMarket } = useCurrency(); + const { price } = useFormatters(); + const { data: pricesData } = useGlobalItemPrices(globalItemId); + const { data: communityStats } = useGlobalItemCommunityStats(globalItemId); + const [showOtherMarkets, setShowOtherMarkets] = useState(false); + + const marketPrices = pricesData?.marketPrices ?? []; + const stats = communityStats ?? []; + + // No data at all — don't render section + if (marketPrices.length === 0 && stats.length === 0) return null; + + const userMarketPrice = marketPrices.find((p) => p.market === userMarket); + const otherMarketPrices = marketPrices.filter( + (p) => p.market !== userMarket, + ); + const userMarketStats = stats.filter((s) => s.market === userMarket); + const otherMarketStats = stats.filter((s) => s.market !== userMarket); + + return ( +
+

Price

+ + {/* User's market MSRP */} + {userMarketPrice && ( +
+ + {formatPrice(userMarketPrice.priceCents, userMarketPrice.currency as Currency)} + + + MSRP ({userMarketPrice.market}) + +
+ )} + + {/* Community stats for user's market */} + {userMarketStats.map((stat) => ( +

+ Community ({stat.market}):{" "} + {formatPrice(stat.medianPrice, stat.currency as Currency)} median{" "} + + ({stat.reportCount} reports) + +

+ ))} + + {/* Other Markets collapsible */} + {(otherMarketPrices.length > 0 || otherMarketStats.length > 0) && ( +
+ + {showOtherMarkets && ( +
+ {otherMarketPrices.map((p) => ( +
+ + {formatPrice(p.priceCents, p.currency as Currency)} + + + MSRP ({p.market}) + +
+ ))} + {otherMarketStats.map((stat) => ( +

+ Community ({stat.market}):{" "} + {formatPrice(stat.medianPrice, stat.currency as Currency)}{" "} + median{" "} + + ({stat.reportCount} reports) + +

+ ))} +
+ )} +
+ )} +
+ ); +}