"use client"; import { useMemo, useState, useEffect } from "react"; import Link from "next/link"; import { useSearchParams, useRouter } from "next/navigation"; import { CATEGORIES } from "@/data/gunbuilderParts"; import type { CategoryId, Part } from "@/types/gunbuilder"; import { CategoryColumn } from "@/components/CategoryColumn"; type GunbuilderProductFromApi = { id: number; // backend numeric id 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"; // Map backend partRole to your existing CategoryId values. const PART_ROLE_TO_CATEGORY: Record = { "upper-receiver": "upper" as CategoryId, barrel: "barrel" as CategoryId, handguard: "handguard" as CategoryId, "charging-handle": "chargingHandle" as CategoryId, "buffer-kit": "buffer" as CategoryId, "lower-parts-kit": "lowerParts" as CategoryId, sight: "sights" as CategoryId, "lower-receiver": "lower" as CategoryId, optic: "optic" as CategoryId, stock: "stock" as CategoryId, }; type BuildState = Partial>; // categoryId -> partId const STORAGE_KEY = "gunbuilder-build-state"; export default function GunbuilderPage() { const searchParams = useSearchParams(); const router = useRouter(); const [parts, setParts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [build, setBuild] = useState(() => { if (typeof window !== "undefined") { const stored = localStorage.getItem(STORAGE_KEY); if (stored) { try { return JSON.parse(stored); } catch { return {}; } } } return {}; }); useEffect(() => { const controller = new AbortController(); async function fetchProducts() { try { setLoading(true); setError(null); const res = await fetch( `${API_BASE_URL}/api/products/gunbuilder?platform=AR-15`, { signal: controller.signal }, ); if (!res.ok) { throw new Error(`Failed to load products (${res.status})`); } const data: GunbuilderProductFromApi[] = await res.json(); const normalized: Part[] = data .map((p): Part | null => { const categoryId = PART_ROLE_TO_CATEGORY[p.partRole]; if (!categoryId) { // Skip any parts we don't know how to map yet return null; } return { id: String(p.id), // <<< ALWAYS string categoryId, name: p.name, brand: p.brand, price: p.price ?? 0, imageUrl: p.mainImageUrl ?? undefined, url: p.buyUrl ?? undefined, notes: undefined, }; }) .filter((p): p is Part => p !== null); setParts(normalized); } catch (err: any) { if (err.name === "AbortError") return; setError(err.message ?? "Failed to load products"); } finally { setLoading(false); } } fetchProducts(); return () => controller.abort(); }, []); // Handle URL query parameter for part selection (?select=upper:165) useEffect(() => { const selectParam = searchParams.get("select"); if (selectParam) { const [categoryId, partId] = selectParam.split(":"); if (categoryId && partId && CATEGORIES.some((c) => c.id === categoryId)) { setBuild((prev) => { const updated = { ...prev, [categoryId as CategoryId]: partId, }; localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); return updated; }); router.replace("/gunbuilder", { scroll: false }); } } }, [searchParams, router]); // Persist build state to localStorage whenever it changes useEffect(() => { if (typeof window !== "undefined") { localStorage.setItem(STORAGE_KEY, JSON.stringify(build)); } }, [build]); const partsByCategory: Record = useMemo(() => { const grouped = {} as Record; for (const category of CATEGORIES) { grouped[category.id] = parts.filter( (p) => p.categoryId === category.id, ); } return grouped; }, [parts]); const selectedParts: Part[] = useMemo(() => { return Object.entries(build) .map(([categoryId, partId]) => parts.find((p) => p.id === partId && p.categoryId === categoryId), ) .filter(Boolean) as Part[]; }, [build, parts]); const totalPrice = useMemo( () => selectedParts.reduce((sum, p) => sum + p.price, 0), [selectedParts], ); const handleSelectPart = (categoryId: CategoryId, partId: string) => { setBuild((prev) => ({ ...prev, [categoryId]: partId, })); }; return (
{/* Header */}

Shadow Standard

The Armory Prototype

Choose one part per category to assemble an AR-15 build. Parts are loaded from the live merchant feed (Aero Precision for now), using your Ballistic backend API.

Current build total
${totalPrice.toFixed(2)}
{selectedParts.length} / {CATEGORIES.length} categories filled
{/* Layout */}
{/* Categories/parts */}
{loading && (

Loading parts from backend…

)} {error && !loading && (

{error} — check that the Ballistic API is running and CORS is configured.

)} {!loading && !error && (
{CATEGORIES.map((category) => (
handleSelectPart(category.id, partId) } />
))}
)}
{/* Build Summary */}
); }