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:
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user