"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; uuid: string; slug: string; name: string; brandName: 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. // Make sure these string values match the `id` fields in CATEGORIES. 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, }; 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(() => { // Initialize from localStorage if available 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) => { const categoryId = PART_ROLE_TO_CATEGORY[p.partRole]; if (!categoryId) { // Skip any parts we don't know how to map yet return null; } return { // Use the backend UUID as the stable id for the UI id: p.uuid, categoryId, name: p.name, brand: p.brandName, price: p.price ?? 0, imageUrl: p.mainImageUrl ?? undefined, url: p.buyUrl ?? undefined, } as Part; }) .filter(Boolean) as Part[]; 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 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]: partId, }; // Persist to localStorage localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); return updated; }); // Remove query param from URL 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

Gunbuilder 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 */}
); }