diff --git a/app/blocks/inventory-item-detail/item-header.tsx b/app/blocks/inventory-item-detail/item-header.tsx index 1b82c58..7389bee 100644 --- a/app/blocks/inventory-item-detail/item-header.tsx +++ b/app/blocks/inventory-item-detail/item-header.tsx @@ -3,14 +3,18 @@ import styles from "./item-header.module.css"; interface Props { className?: string; - item: Pick; + item: Pick< + InventoryItem, + "sku" | "name" | "brand" | "size" | "condition" | "imageUrl" + >; } export function ItemHeader({ className, item }: Props) { const displayCondition = item.condition .replace(/_/g, " ") .toLowerCase() - .replace(/\b\w/g, (l) => l.toUpperCase()); + .replace(/\b\w/g, (l: string) => l.toUpperCase()); + return (
@@ -18,31 +22,41 @@ export function ItemHeader({ className, item }: Props) { {item.name} ) : ( "Product Image" )}
+
{item.sku}
+

{item.name}

-
- {item.brand} +
+ {item.brand} + {item.size && ( <> - · - {item.size} - - )} - · - {displayCondition} -
+ · + {item.size} + + )} + + · + {displayCondition} +
+
); -} +} \ No newline at end of file diff --git a/app/blocks/inventory-item-detail/item-info-card.tsx b/app/blocks/inventory-item-detail/item-info-card.tsx index 97b9e48..d1d42c6 100644 --- a/app/blocks/inventory-item-detail/item-info-card.tsx +++ b/app/blocks/inventory-item-detail/item-info-card.tsx @@ -11,6 +11,7 @@ interface Props { } export function ItemInfoCard({ className, item }: Props) { + // Format condition and status (e.g., "new_mint" -> "New Mint") const displayCondition = item.condition .replace(/_/g, " ") .toLowerCase() @@ -21,46 +22,57 @@ export function ItemInfoCard({ className, item }: Props) { .toLowerCase() .replace(/\b\w/g, (l) => l.toUpperCase()); + // Format date to "Jan 5, 2026" const formattedPurchaseDate = new Date(item.purchaseDate).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", }); + // Format currency handling nulls and dynamic currency codes const formatMoney = (val: number | null, currency: string = "USD") => { if (val == null) return "N/A"; return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(val); }; + return (
Item Details
+
Purchase Price
-
{formatMoney(item.purchasePrice, item.currency)}
+
+ {formatMoney(item.purchasePrice, item.currency)} +
+
Purchase Date
{formattedPurchaseDate}
+
Condition
{displayCondition}
+
Status
{displayStatus}
+
Asking Price
-
{formatMoney(item.askingPrice, item.currency)}
+
+ {formatMoney(item.askingPrice, item.currency)} +
+
Notes
-
- {item.notes || None} -
+
{item.notes || "No notes available"}
); diff --git a/app/blocks/inventory-item-detail/price-history-chart.tsx b/app/blocks/inventory-item-detail/price-history-chart.tsx index 248a2c3..a6eba03 100644 --- a/app/blocks/inventory-item-detail/price-history-chart.tsx +++ b/app/blocks/inventory-item-detail/price-history-chart.tsx @@ -1,51 +1,49 @@ -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"; -import type { MarketPrice } from "@prisma/client"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, +} from "recharts"; import styles from "./price-history-chart.module.css"; +import type { MarketPrice } from "@prisma/client"; + +type PriceHistoryItem = Pick< + MarketPrice, + "marketplace" | "fetchedAt" +> & { + askPrice: number | null; + bidPrice: number | null; + lastSold: number | null; +}; + interface Props { className?: string; - priceHistory: (Pick & { - fetchedAt: string | Date; - askPrice: number | null; - bidPrice: number | null; - lastSold: number | null; - })[]; + priceHistory: PriceHistoryItem[]; } + const marketplaceColors: Record = { stockx: "#00FF88", goat: "#7C3AED", ebay: "#3B82F6", flightclub: "#FFB347", stadiumgoods: "#FF4D6A", - amazon: "#FF9900", - mercari: "#E44B3B", - poshmark: "#7B2D8B", - facebook: "#1877F2", - shopify: "#96BF48", - grailed: "#000000", - depop: "#FF2300", - offerup: "#00AB80", - in_person: "#6B7280", - other: "#9CA3AF", }; -export function PriceHistoryChart({ className, priceHistory }: Props) { +export function PriceHistoryChart({ + className, + priceHistory, +}: Props) { if (!priceHistory || priceHistory.length === 0) { return (
30-Day Price History
-

- No price history available. -

+

No price history available.

); } @@ -54,40 +52,64 @@ export function PriceHistoryChart({ className, priceHistory }: Props) { priceHistory.forEach((record) => { const d = new Date(record.fetchedAt); - const dateStr = `${(d.getMonth() + 1).toString().padStart(2, "0")}/${d.getDate().toString().padStart(2, "0")}`; - const sortKey = d.getTime(); + + const dateStr = `${(d.getMonth() + 1) + .toString() + .padStart(2, "0")}/${d + .getDate() + .toString() + .padStart(2, "0")}`; if (!groupedData[dateStr]) { - groupedData[dateStr] = { date: dateStr, _sortTime: sortKey }; + groupedData[dateStr] = { + date: dateStr, + _sortTime: d.getTime(), + }; } const marketKey = record.marketplace.toLowerCase(); - const price = record.lastSold ?? record.askPrice ?? record.bidPrice; - if (price !== null) { - groupedData[dateStr][marketKey] = price; - } + + groupedData[dateStr][marketKey] = + record.askPrice ?? + record.lastSold ?? + record.bidPrice; }); - const chartData = Object.values(groupedData).sort((a, b) => a._sortTime - b._sortTime); + const chartData = Object.values(groupedData).sort( + (a,b) => a._sortTime - b._sortTime +); return (
30-Day Price History
+ - - + + + + `$${v}`} /> + [`${Number(v)}`, ""]} /> + + {Object.entries(marketplaceColors).map(([key, color]) => ( - + ))}
); -} +} \ No newline at end of file diff --git a/app/blocks/inventory-item-detail/related-items.tsx b/app/blocks/inventory-item-detail/related-items.tsx index 4efe518..9d1171d 100644 --- a/app/blocks/inventory-item-detail/related-items.tsx +++ b/app/blocks/inventory-item-detail/related-items.tsx @@ -1,25 +1,31 @@ import { Link } from "react-router"; -import { mockInventoryItems } from "~/data/mock-data"; import styles from "./related-items.module.css"; - +import type { InventoryItem } from "@prisma/client"; interface Props { className?: string; + items: InventoryItem[]; } -export function RelatedItems({ className }: Props) { - const related = mockInventoryItems.slice(1, 5); +export function RelatedItems({ className, items }: Props) { return (
Related Items
+
- {related.map((item) => ( - + {items.map((item) => ( +
Image
{item.name}
-
${item.marketValue}
+
+ {item.askingPrice != null ? `$${item.askingPrice}` : "N/A"} +
))}
); -} +} \ No newline at end of file diff --git a/app/routes/app-layout.tsx b/app/routes/app-layout.tsx index 786b807..f7ea39e 100644 --- a/app/routes/app-layout.tsx +++ b/app/routes/app-layout.tsx @@ -1,6 +1,6 @@ import { Outlet, redirect, useLoaderData } from "react-router"; import { getSupabaseServerClient } from "~/utils/supabase.server"; -import { PrismaClient } from "@prisma/client"; +import { PrismaClient } from "@prisma/client"; import type { Route } from "./+types/app-layout"; import { AppSidebar } from "~/blocks/__global/app-sidebar"; import { BreadcrumbNavigation } from "~/blocks/__global/breadcrumb-navigation"; diff --git a/app/routes/inventory-item-detail.tsx b/app/routes/inventory-item-detail.tsx index 6f6021e..9bf35bc 100644 --- a/app/routes/inventory-item-detail.tsx +++ b/app/routes/inventory-item-detail.tsx @@ -44,6 +44,17 @@ export async function loader({ request, params }: Route.LoaderArgs) { throw new Response("Not Found", { status: 404 }); } + const relatedItems = await prisma.inventoryItem.findMany({ + where: { + userId: user.id, + brand: item.brand, + id: { + not: item.id, + }, + }, + take: 4, + }); + return { item: { ...item, @@ -51,11 +62,11 @@ export async function loader({ request, params }: Route.LoaderArgs) { askingPrice: item.askingPrice ? Number(item.askingPrice) : null, sale: item.sale ? { - ...item.sale, - salePrice: Number(item.sale.salePrice), - platformFee: Number(item.sale.platformFee), - shippingCost: Number(item.sale.shippingCost), - } + ...item.sale, + salePrice: Number(item.sale.salePrice), + platformFee: Number(item.sale.platformFee), + shippingCost: Number(item.sale.shippingCost), + } : null, priceHistory: item.priceHistory.map((ph) => ({ ...ph, @@ -64,14 +75,22 @@ export async function loader({ request, params }: Route.LoaderArgs) { lastSold: ph.lastSold ? Number(ph.lastSold) : null, })), }, + relatedItems, }; } + export default function InventoryItemDetailPage() { - const { item } = useLoaderData(); + const { item, relatedItems } = useLoaderData(); + + if (!item) { + return
Item not found
; + } + return (
+
- + + - +
); }