"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; imageUrl: 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/products/${partId}`; 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(); if (!data) { setError("Product not found"); } setProduct(data); } 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 Builder
); } 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.imageUrl && (
{/* eslint-disable-next-line @next/next/no-img-element */} {product.name}
)}
{product.brand}

{product.name}

Platform:{" "} {product.platform}

Role:{" "} {product.partRole}

Category:{" "} {category.name}

Offers:{" "} {offers.length > 0 ? `${offers.length} live offer${ offers.length === 1 ? "" : "s" }` : "No live offers yet"}

{/* Placeholder content until long-form fields are wired up */}

Product Overview

This is placeholder copy for the product overview. Once the Ballistic backend exposes richer metadata (short description, feature bullets, etc.), we'll swap this text out and surface the real content here.

Use this section to highlight what makes this part worth a slot in your build: materials, intended use (duty/range/competition), and any standout features.

Quick Specs (Placeholder)

Configuration
TBD (Stripped / Complete / Kit)
Caliber
TBD from feed
Finish
TBD from merchant data
Weight
TBD (oz)

Specs are placeholders for now. As we normalize more structured attributes in the importer, this block will auto-populate per product.

{/* Right: pricing + actions */}
{/* Lower content: builder-focused helper copy (placeholder) */}

Why We Like This Part

Drop in a short blurb here about what makes this part worth picking over similar options — think reliability, track record, and value for the money.

Best For

Use this block to call out ideal use cases:{" "} duty rifle, home defense, range toy, competition, night work , etc.

Builder Notes

Add any compatibility quirks or install tips here once the compatibility engine is wired up — gas system length, buffer recommendations, known fitment notes, and more.

Compatibility

Compatibility engine coming online soon

Fitment checks, caliber validation, buffer/gas system logic, and platform rules will appear here once the engine is active.

)}
); }