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>
This commit is contained in:
2026-04-13 18:09:56 +02:00
parent 02fcae12f0
commit 37edd0edfd
2 changed files with 145 additions and 1 deletions

View File

@@ -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<CommunityPriceStat[]>(`/api/community-prices/${globalItemId}`),
enabled: globalItemId > 0,
});
}
export function useUnlinkItem() {
const queryClient = useQueryClient();
return useMutation({

View File

@@ -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() {
</div>
)}
{/* Market Prices Section */}
<MarketPricesSection globalItemId={Number(globalItemId)} />
{/* Product page link */}
{item.sourceUrl && (
<div className="mt-4">
@@ -251,3 +262,99 @@ function GlobalItemDetail() {
</div>
);
}
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 (
<div className="mt-6 mb-6">
<h3 className="text-sm font-medium text-gray-900 mb-3">Price</h3>
{/* User's market MSRP */}
{userMarketPrice && (
<div className="mb-2">
<span className="text-lg font-semibold text-gray-900">
{formatPrice(userMarketPrice.priceCents, userMarketPrice.currency as Currency)}
</span>
<span className="text-xs text-gray-500 ml-2">
MSRP ({userMarketPrice.market})
</span>
</div>
)}
{/* Community stats for user's market */}
{userMarketStats.map((stat) => (
<p key={`${stat.market}-${stat.currency}`} className="text-sm text-gray-700 mb-1">
Community ({stat.market}):{" "}
{formatPrice(stat.medianPrice, stat.currency as Currency)} median{" "}
<span className="text-xs text-gray-400">
({stat.reportCount} reports)
</span>
</p>
))}
{/* Other Markets collapsible */}
{(otherMarketPrices.length > 0 || otherMarketStats.length > 0) && (
<div className="mt-3">
<button
type="button"
onClick={() => setShowOtherMarkets(!showOtherMarkets)}
className="text-sm text-gray-500 cursor-pointer hover:text-gray-700 flex items-center gap-1"
>
<LucideIcon
name={showOtherMarkets ? "chevron-down" : "chevron-right"}
size={14}
/>
Other Markets
</button>
{showOtherMarkets && (
<div className="pl-4 mt-2 space-y-1">
{otherMarketPrices.map((p) => (
<div key={`${p.market}-${p.currency}`}>
<span className="text-sm font-medium text-gray-900">
{formatPrice(p.priceCents, p.currency as Currency)}
</span>
<span className="text-xs text-gray-500 ml-2">
MSRP ({p.market})
</span>
</div>
))}
{otherMarketStats.map((stat) => (
<p
key={`${stat.market}-${stat.currency}`}
className="text-sm text-gray-700"
>
Community ({stat.market}):{" "}
{formatPrice(stat.medianPrice, stat.currency as Currency)}{" "}
median{" "}
<span className="text-xs text-gray-400">
({stat.reportCount} reports)
</span>
</p>
))}
</div>
)}
</div>
)}
</div>
);
}