"use client"; import { useEffect, useMemo, useState } from "react"; 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: string; // backend UUID string name: string; brand: string; platform: string; partRole: string; price: number | null; mainImageUrl: string | null; buyUrl: string | null; }; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"; export default function PartDetailPage() { const params = useParams(); const categoryId = params.categoryId as CategoryId; const partId = params.partId as string; // this is the UUID we passed in the link const [product, setProduct] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [offers, setOffers] = useState([]); const [offersLoading, setOffersLoading] = useState(true); const [offersError, setOffersError] = useState(null); const category = useMemo( () => CATEGORIES.find((c) => c.id === categoryId), [categoryId], ); // 1) Load product (same as before) useEffect(() => { if (!partId) return; const controller = new AbortController(); async function fetchProduct() { try { setLoading(true); setError(null); // For now, pull the full AR-15 list and find by UUID. // 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 }); if (!res.ok) { throw new Error(`Failed to load product (${res.status})`); } const data: GunbuilderProductFromApi[] = await res.json(); const found = data.find((p) => p.id === partId) ?? null; if (!found) { setError("Product not found"); } setProduct(found); } catch (err: any) { if (err.name === "AbortError") return; setError(err.message ?? "Failed to load product"); } finally { setLoading(false); } } fetchProduct(); 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 (

Category Not Found

Return to Gunbuilder
); } return (
{/* Breadcrumbs */} {loading ? (

Loading product…

) : error || !product ? (

Product Unavailable

{error ?? "We couldn’t find this product."}

Back to {category.name} parts
) : (
{/* Left: image + meta */}
{product.mainImageUrl && (
{/* eslint-disable-next-line @next/next/no-img-element */} {product.name}
)}
{product.brand}

{product.name}

Platform:{" "} {product.platform}

Role: {product.partRole}

{/* Right: pricing + actions */}
)}
); }