// // This page is most likely temporary. We probably do not need this page // // // "use client"; import { useEffect, useMemo, useState } 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 { Pencil, Link2, Square, CheckSquare, } from "lucide-react"; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"; const STORAGE_KEY = "gunbuilder-build-state"; type BuildState = Partial>; type GunbuilderProductFromApi = { id: number; name: string; brand: string; platform: string; partRole: string; price: number | null; imageUrl: string | null; buyUrl: string | null; }; const PLATFORMS = ["AR-15", "AR-10", "AR-9"] as const; const UNIVERSAL_CATEGORIES = new Set([ "magazine", "weapon-light", "foregrip", "bipod", "sling", "rail-accessory", "tools", "optic", "sights", ]); const CATEGORY_GROUPS: { id: string; label: string; description?: string; categoryIds: CategoryId[]; }[] = [ { id: "lower-group", label: "Lower Receiver Parts", description: "Everything from the serialized lower to small parts, fire control, and core controls.", categoryIds: [ "lower-receiver", "complete-lower", "lower-parts", "trigger", "grip", "safety", "buffer", "stock", ], }, { id: "upper-group", label: "Upper Receiver Parts", description: "Barrel, upper, gas system, and the parts that keep the rifle cycling.", categoryIds: [ "upper-receiver", "complete-upper", "bcg", "barrel", "gas-block", "gas-tube", "muzzle-device", "suppressor", "handguard", "charging-handle", "sights", "optic", ], }, { id: "accessories-group", label: "Accessories & Gear", description: "Universal add-ons that work across platforms — mags, lights, slings, tools, and more.", categoryIds: [ "magazine", "weapon-light", "foregrip", "bipod", "sling", "rail-accessory", "tools", ], }, ]; function isValidPlatform( value: string | null ): value is (typeof PLATFORMS)[number] { return !!value && (PLATFORMS as readonly string[]).includes(value); } function safeDecodeBuildParam(buildParam: string | null): BuildState | null { if (!buildParam) return null; try { // build param is base64(json) const json = window.atob(buildParam); const parsed = JSON.parse(json) as BuildState; return parsed && typeof parsed === "object" ? parsed : null; } catch { return null; } } function formatPrice(n: number) { return `$${n.toFixed(2)}`; } export default function BuildSummaryPage() { const searchParams = useSearchParams(); const router = useRouter(); const qpPlatform = searchParams.get("platform"); const platform = isValidPlatform(qpPlatform) ? qpPlatform : "AR-15"; const [build, setBuild] = useState({}); const [parts, setParts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Load build from ?build=... OR localStorage useEffect(() => { if (typeof window === "undefined") return; const buildParam = searchParams.get("build"); const decoded = safeDecodeBuildParam(buildParam); if (decoded) { setBuild(decoded); return; } try { const stored = localStorage.getItem(STORAGE_KEY); setBuild(stored ? (JSON.parse(stored) as BuildState) : {}); } catch { setBuild({}); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Fetch product data so we can resolve ids -> Part info useEffect(() => { const controller = new AbortController(); async function fetchProducts() { try { setLoading(true); setError(null); // scoped (platform) const scopedUrl = `${API_BASE_URL}/api/v1/products?platform=${encodeURIComponent( platform )}`; // universal (optional) const universalUrl = `${API_BASE_URL}/api/v1/products`; const [scopedRes, universalRes] = await Promise.all([ fetch(scopedUrl, { signal: controller.signal }), fetch(universalUrl, { signal: controller.signal }).catch( () => null as any ), ]); if (!scopedRes || !scopedRes.ok) { throw new Error(`Failed to load products (${scopedRes?.status ?? ""})`); } const scopedData: GunbuilderProductFromApi[] = await scopedRes.json(); let universalData: GunbuilderProductFromApi[] = []; if (universalRes && universalRes.ok) { universalData = await universalRes.json(); } // NOTE: We only need enough to render the summary. // We will map “everything we can” and ignore unknown categories. const toCategoryId = (rawRole: string): CategoryId | null => { const role = (rawRole ?? "").trim().toLowerCase().replace(/_/g, "-"); // quick defensive mapping (mirrors your builder page strategy) if (role.includes("complete-upper")) return "complete-upper"; if (role.includes("complete-lower")) return "complete-lower"; if (role === "upper-receiver" || role === "upper") return "upper-receiver"; if (role === "lower-receiver" || role === "lower") return "lower-receiver"; if (role.includes("charging")) return "charging-handle"; if (role.includes("handguard")) return "handguard"; if (role.includes("bcg") || role.includes("bolt-carrier")) return "bcg"; if (role.includes("barrel")) return "barrel"; if (role.includes("gas-block") || role.includes("gasblock")) return "gas-block"; if (role.includes("gas-tube") || role.includes("gastube")) return "gas-tube"; if (role.includes("muzzle") || role.includes("flash") || role.includes("brake") || role.includes("comp")) return "muzzle-device"; if (role.includes("suppress")) return "suppressor"; if (role.includes("lower-parts") || role.includes("lpk")) return "lower-parts"; if (role.includes("trigger")) return "trigger"; if (role.includes("grip")) return "grip"; if (role.includes("safety")) return "safety"; if (role.includes("buffer")) return "buffer"; if (role.includes("stock")) return "stock"; if (role.includes("optic") || role.includes("scope")) return "optic"; if (role.includes("sight")) return "sights"; if (role.includes("mag")) return "magazine"; if (role.includes("weapon-light") || (role.includes("light") && !role.includes("flight"))) return "weapon-light"; if (role.includes("foregrip") || role.includes("grip-vertical")) return "foregrip"; if (role.includes("bipod")) return "bipod"; if (role.includes("sling")) return "sling"; if (role.includes("rail-accessory") || role.includes("rail-attachment")) return "rail-accessory"; if (role.includes("tool")) return "tools"; return null; }; const normalize = (data: GunbuilderProductFromApi[], isUniversal: boolean): Part[] => data .map((p): Part | null => { const categoryId = toCategoryId(p.partRole); if (!categoryId) return null; if (isUniversal && !UNIVERSAL_CATEGORIES.has(categoryId)) return null; const buyUrl = p.buyUrl ?? undefined; return { id: String(p.id), categoryId, name: p.name, brand: p.brand, price: p.price ?? 0, imageUrl: (p.imageUrl as any) ?? (p as any).mainImageUrl ?? undefined, affiliateUrl: buyUrl, url: buyUrl, notes: undefined, }; }) .filter((p): p is Part => p !== null); const scopedParts = normalize(scopedData, false); const universalParts = normalize(universalData, true); const seen = new Set(); const merged: Part[] = []; for (const p of [...scopedParts, ...universalParts]) { const key = `${p.categoryId}:${p.id}`; if (seen.has(key)) continue; seen.add(key); merged.push(p); } setParts(merged); } catch (err: any) { if (err?.name === "AbortError") return; setError(err?.message ?? "Failed to load products"); } finally { setLoading(false); } } fetchProducts(); return () => controller.abort(); }, [platform]); const summaryCategoryOrder: CategoryId[] = useMemo( () => CATEGORY_GROUPS.flatMap((g) => g.categoryIds).filter((id) => CATEGORIES.some((c) => c.id === id) ), [] ); const selectedParts = useMemo(() => { const out: { categoryId: CategoryId; part?: Part }[] = []; for (const cid of summaryCategoryOrder) { const partId = build[cid]; const part = partId ? parts.find((p) => p.id === partId && p.categoryId === cid) : undefined; out.push({ categoryId: cid, part }); } return out; }, [build, parts, summaryCategoryOrder]); const total = useMemo( () => selectedParts.reduce((sum, row) => sum + (row.part?.price ?? 0), 0), [selectedParts] ); const filledCount = useMemo( () => selectedParts.filter((r) => !!r.part).length, [selectedParts] ); return (
{/* Header */}

BATTL BUILDERS

Build Summary

Snapshot of your current selections. (V1: data resolves from your local build state + live products.)

Platform: {platform}
Total
{formatPrice(total)}
{filledCount} / {summaryCategoryOrder.length} selected
Edit Build
{/* Body */}
{loading ? (

Loading…

) : error ? (

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

) : (
{CATEGORY_GROUPS.map((group) => { const rows = group.categoryIds .filter((cid) => summaryCategoryOrder.includes(cid)) .map((cid) => { const cat = CATEGORIES.find((c) => c.id === cid); const partId = build[cid]; const part = partId ? parts.find((p) => p.id === partId && p.categoryId === cid) : undefined; return { cid, cat, part }; }); return (
{group.label}
{group.description && (
{group.description}
)}
{rows.map(({ cid, cat, part }) => (
{cat?.name ?? cid}
{part ? (
{part.brand}{" "} {part.name}
) : (
Not selected
)}
Price
{part ? formatPrice(part.price) : "—"}
{part?.url ? ( ) : ( )}
))}
); })}
)}
Tip: If you landed here from a shared link, it uses ?build= first; otherwise it reads your local build.
); }