full app is using live data now,

This commit is contained in:
2025-11-30 21:08:13 -05:00
parent d53a58e994
commit c858835044
111 changed files with 1551 additions and 644 deletions
+94 -12
View File
@@ -5,13 +5,15 @@ import Link from "next/link";
import { useParams } from "next/navigation";
import { CATEGORIES } from "@/data/gunbuilderParts";
import type { CategoryId } from "@/types/gunbuilder";
import {
getRetailerOffers,
type RetailerOffer,
} from "./data";
type GunbuilderProductFromApi = {
id: number;
uuid: string;
slug: string;
id: string; // backend UUID string
name: string;
brandName: string;
brand: string;
platform: string;
partRole: string;
price: number | null;
@@ -31,11 +33,16 @@ export default function PartDetailPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [offers, setOffers] = useState<RetailerOffer[]>([]);
const [offersLoading, setOffersLoading] = useState(true);
const [offersError, setOffersError] = useState<string | null>(null);
const category = useMemo(
() => CATEGORIES.find((c) => c.id === categoryId),
[categoryId],
);
// 1) Load product (same as before)
useEffect(() => {
if (!partId) return;
@@ -47,7 +54,7 @@ export default function PartDetailPage() {
setError(null);
// For now, pull the full AR-15 list and find by UUID.
// We can optimize with a dedicated /api/products/gunbuilder/{uuid} later.
// We can optimize with a dedicated /api/products/{uuid} later.
const url = `${API_BASE_URL}/api/products/gunbuilder?platform=AR-15`;
const res = await fetch(url, { signal: controller.signal });
@@ -56,7 +63,7 @@ export default function PartDetailPage() {
}
const data: GunbuilderProductFromApi[] = await res.json();
const found = data.find((p) => p.uuid === partId) ?? null;
const found = data.find((p) => p.id === partId) ?? null;
if (!found) {
setError("Product not found");
@@ -76,6 +83,48 @@ export default function PartDetailPage() {
return () => controller.abort();
}, [partId]);
// 2) Load offers for this product from Ballistic backend
useEffect(() => {
if (!partId) return;
let cancelled = false;
async function fetchOffers() {
try {
setOffersLoading(true);
setOffersError(null);
const data = await getRetailerOffers(partId);
if (!cancelled) {
setOffers(data);
}
} catch (err: any) {
if (!cancelled) {
setOffersError(err.message ?? "Failed to load retailer offers");
}
} finally {
if (!cancelled) {
setOffersLoading(false);
}
}
}
fetchOffers();
return () => {
cancelled = true;
};
}, [partId]);
// Best price: prefer live offers, fall back to summary product.price
const bestPrice = useMemo(() => {
if (offers.length > 0) {
return offers[0].price;
}
return product?.price ?? null;
}, [offers, product]);
if (!category) {
return (
<main className="min-h-screen bg-black text-zinc-50">
@@ -148,13 +197,14 @@ export default function PartDetailPage() {
</div>
)}
<div className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-500">
{product.brandName}
{product.brand}
</div>
<h1 className="text-2xl md:text-3xl font-semibold tracking-tight">
{product.name}
</h1>
<p className="text-xs text-zinc-500">
Platform: <span className="text-zinc-300">{product.platform}</span>
Platform:{" "}
<span className="text-zinc-300">{product.platform}</span>
</p>
<p className="text-xs text-zinc-500">
Role: <span className="text-zinc-300">{product.partRole}</span>
@@ -168,11 +218,11 @@ export default function PartDetailPage() {
Price
</p>
<p className="text-2xl font-semibold text-amber-300">
{product.price && product.price > 0
? `$${product.price.toFixed(2)}`
{bestPrice && bestPrice > 0
? `$${bestPrice.toFixed(2)}`
: "—"}
</p>
{!product.price && (
{!bestPrice && (
<p className="mt-1 text-xs text-zinc-500">
Pricing not available yet. Live pricing will appear once
offers are imported.
@@ -180,9 +230,41 @@ export default function PartDetailPage() {
)}
</div>
{/* Offers list */}
<div className="space-y-2">
{offersLoading && (
<p className="text-xs text-zinc-500">Loading offers</p>
)}
{offersError && (
<p className="text-xs text-red-400">
{offersError}
</p>
)}
{!offersLoading && !offersError && offers.length > 0 && (
<div className="space-y-1">
{offers.map((offer) => (
<a
key={offer.id}
href={offer.affiliateUrl}
target="_blank"
rel="noreferrer"
className="flex items-center justify-between rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-xs hover:bg-zinc-800 transition-colors"
>
<span className="text-zinc-200">
{offer.retailerName}
</span>
<span className="font-semibold text-amber-300">
${offer.price.toFixed(2)}
</span>
</a>
))}
</div>
)}
</div>
<div className="space-y-2">
<Link
href={`/gunbuilder?select=${categoryId}:${product.uuid}`}
href={`/gunbuilder?select=${categoryId}:${product.id}`}
className="block w-full rounded-md bg-amber-400 text-black text-sm font-semibold text-center py-2.5 hover:bg-amber-300 transition-colors"
>
Add to Build