Skip to content
29 changes: 19 additions & 10 deletions app/blocks/inventory-item-detail/item-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,55 @@ import styles from "./item-header.module.css";

interface Props {
className?: string;
item: Pick<InventoryItem, "sku" | "name" | "brand" | "size" | "condition" | "imageUrl">;
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 (
<div className={[styles.header, className].filter(Boolean).join(" ")}>
<div className={styles.image}>
{item.imageUrl ? (
<img
src={item.imageUrl}
alt={item.name}
style={{ width: "100%", height: "100%", objectFit: "cover", borderRadius: "inherit" }}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
borderRadius: "inherit",
}}
/>
) : (
"Product Image"
)}
</div>

<div className={styles.info}>
<div className={styles.sku}>{item.sku}</div>

<h1 className={styles.name}>{item.name}</h1>

<div className={styles.meta}>
<span>{item.brand}</span>
{item.size && (
<>
<span>·</span>
<span>{item.size}</span>
</>
)}
<span>·</span>
<span>Size {item.size}</span>
<span>·</span>
<span>{displayCondition}</span>
</div>
</div>

<div className={styles.actions}>
<button className={styles.editBtn}>Edit</button>
<button className={styles.deleteBtn}>Delete</button>
</div>
</div>
);
}
}
22 changes: 17 additions & 5 deletions app/blocks/inventory-item-detail/item-info-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 (
<div className={[styles.card, className].filter(Boolean).join(" ")}>
<div className={styles.title}>Item Details</div>

<div className={styles.field}>
<div className={styles.label}>Purchase Price</div>
<div className={[styles.value, styles.money].join(" ")}>{formatMoney(item.purchasePrice, item.currency)}</div>
<div className={[styles.value, styles.money].join(" ")}>
{formatMoney(item.purchasePrice, item.currency)}
</div>
</div>

<div className={styles.field}>
<div className={styles.label}>Purchase Date</div>
<div className={styles.value}>{formattedPurchaseDate}</div>
</div>

<div className={styles.field}>
<div className={styles.label}>Condition</div>
<div className={styles.value}>{displayCondition}</div>
</div>

<div className={styles.field}>
<div className={styles.label}>Status</div>
<div className={styles.value}>
<span className={styles.badge}>{displayStatus}</span>
</div>
</div>

<div className={styles.field}>
<div className={styles.label}>Asking Price</div>
<div className={[styles.value, styles.money].join(" ")}>{formatMoney(item.askingPrice, item.currency)}</div>
<div className={[styles.value, styles.money].join(" ")}>
{formatMoney(item.askingPrice, item.currency)}
</div>
</div>

<div className={styles.field}>
<div className={styles.label}>Notes</div>
<div className={styles.value}>
{item.notes || <span style={{ color: "var(--color-text-subtle)" }}>None</span>}
</div>
<div className={styles.value}>{item.notes || "No notes available"}</div>
</div>
</div>
);
Expand Down
120 changes: 75 additions & 45 deletions app/blocks/inventory-item-detail/price-history-chart.tsx
Original file line number Diff line number Diff line change
@@ -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<MarketPrice, "marketplace"> & {
fetchedAt: string | Date;
askPrice: number | null;
bidPrice: number | null;
lastSold: number | null;
})[];
priceHistory: PriceHistoryItem[];
}


const marketplaceColors: Record<string, string> = {
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 (
<div className={[styles.card, className].filter(Boolean).join(" ")}>
<div className={styles.title}>30-Day Price History</div>
<p
style={{
fontSize: 14,
color: "var(--color-text-muted)",
textAlign: "center",
padding: "var(--space-6)",
margin: 0,
}}
>
No price history available.
</p>
<p>No price history available.</p>
</div>
);
}
Expand All @@ -54,55 +52,87 @@ 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 (
<div className={[styles.card, className].filter(Boolean).join(" ")}>
<div className={styles.title}>30-Day Price History</div>

<ResponsiveContainer width="100%" height={260}>
<LineChart data={chartData} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" />
<LineChart data={chartData}>
<CartesianGrid
strokeDasharray="3 3"
stroke="rgba(255,255,255,0.05)"
/>

<XAxis
dataKey="date"
tick={{ fill: "var(--color-text-subtle)", fontSize: 11 }}
tick={{
fill: "var(--color-text-subtle)",
fontSize: 11,
}}
axisLine={false}
tickLine={false}
/>

<YAxis
tick={{ fill: "var(--color-text-subtle)", fontSize: 11 }}
tick={{
fill: "var(--color-text-subtle)",
fontSize: 11,
}}
axisLine={false}
tickLine={false}
tickFormatter={(v) => `$${v}`}
/>

<Tooltip
contentStyle={{
background: "var(--color-bg-elevated)",
border: "1px solid var(--color-border)",
borderRadius: 8,
fontSize: 12,
}}
formatter={(v) => [`${Number(v)}`, ""]}
/>

<Legend wrapperStyle={{ fontSize: 11 }} />

{Object.entries(marketplaceColors).map(([key, color]) => (
<Line key={key} type="monotone" dataKey={key} stroke={color} strokeWidth={2} dot={false} />
<Line
key={key}
type="monotone"
dataKey={key}
stroke={color}
strokeWidth={2}
dot={false}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
);
}
}
22 changes: 14 additions & 8 deletions app/blocks/inventory-item-detail/related-items.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={[styles.section, className].filter(Boolean).join(" ")}>
<div className={styles.title}>Related Items</div>

<div className={styles.grid}>
{related.map((item) => (
<Link key={item.id} to={`/app/inventory/${item.id}`} className={styles.card}>
{items.map((item) => (
<Link
key={item.id}
to={`/app/inventory/${item.id}`}
className={styles.card}
>
<div className={styles.image}>Image</div>
<div className={styles.name}>{item.name}</div>
<div className={styles.price}>${item.marketValue}</div>
<div className={styles.price}>
{item.askingPrice != null ? `$${item.askingPrice}` : "N/A"}
</div>
</Link>
))}
</div>
</div>
);
}
}
2 changes: 1 addition & 1 deletion app/routes/app-layout.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
Loading
Loading