"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"; type BuildState = Partial>; // categoryId -> partId const STORAGE_KEY = "gunbuilder-build-state"; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"; type GunbuilderProductFromApi = { id: string; // backend UUID string name: string; brand: string; platform: string; partRole: string; price: number | null; mainImageUrl: string | null; buyUrl: string | null; }; // Map backend partRole -> CategoryId (same as /gunbuilder page) const PART_ROLE_TO_CATEGORY: Record = { "upper-receiver": "upper", barrel: "barrel", handguard: "handguard", "charging-handle": "chargingHandle", "buffer-kit": "buffer", "lower-parts-kit": "lowerParts", sight: "sights", }; // Encode build state to URL-friendly string function encodeBuildState(build: BuildState): string { const entries = Object.entries(build) .filter(([_, partId]) => partId) .map(([categoryId, partId]) => `${categoryId}:${partId}`) .join(","); return btoa(entries); } // Decode URL string to build state function decodeBuildState(encoded: string): BuildState { try { const decoded = atob(encoded); const build: BuildState = {}; decoded.split(",").forEach((entry) => { const [categoryId, partId] = entry.split(":"); if (categoryId && partId) { build[categoryId as CategoryId] = partId; } }); return build; } catch { return {}; } } export default function BuildDetailsPage() { const searchParams = useSearchParams(); const router = useRouter(); const [build, setBuild] = useState({}); const [shareUrl, setShareUrl] = useState(""); const [copied, setCopied] = useState(false); const [parts, setParts] = useState([]); const [loadingParts, setLoadingParts] = useState(true); const [partsError, setPartsError] = useState(null); // Load build from URL params or localStorage useEffect(() => { const buildParam = searchParams.get("build"); if (buildParam) { const decoded = decodeBuildState(buildParam); setBuild(decoded); // Also save to localStorage localStorage.setItem(STORAGE_KEY, JSON.stringify(decoded)); } else if (typeof window !== "undefined") { // Load from localStorage const stored = localStorage.getItem(STORAGE_KEY); if (stored) { try { const parsed = JSON.parse(stored); setBuild(parsed); } catch { // Invalid stored data } } } }, [searchParams]); // Fetch live parts from backend (same source as /gunbuilder) useEffect(() => { const controller = new AbortController(); async function fetchProducts() { try { setLoadingParts(true); setPartsError(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) return null; return { id: p.id, // already a string UUID categoryId, name: p.name, brand: p.brand, price: p.price ?? 0, imageUrl: p.mainImageUrl ?? undefined, url: p.buyUrl ?? undefined, notes: undefined, }; }) .filter(Boolean) as Part[]; setParts(normalized); } catch (err: any) { if (err.name === "AbortError") return; setPartsError(err.message ?? "Failed to load products"); } finally { setLoadingParts(false); } } fetchProducts(); return () => controller.abort(); }, []); // Generate shareable URL useEffect(() => { if (typeof window !== "undefined" && Object.keys(build).length > 0) { const encoded = encodeBuildState(build); const url = `${window.location.origin}/builder/build?build=${encoded}`; setShareUrl(url); } }, [build]); const selectedParts: Part[] = useMemo(() => { if (parts.length === 0) return []; return Object.entries(build) .map(([categoryId, partId]) => parts.find( (p) => p.id === partId && p.categoryId === (categoryId as CategoryId), ), ) .filter(Boolean) as Part[]; }, [build, parts]); const totalPrice = useMemo( () => selectedParts.reduce((sum, p) => sum + p.price, 0), [selectedParts], ); const handleCopyLink = async () => { if (shareUrl) { try { await navigator.clipboard.writeText(shareUrl); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error("Failed to copy:", err); } } }; const handleShare = async () => { if (navigator.share && shareUrl) { try { await navigator.share({ title: "My AR-15 Build", text: `Check out my AR-15 build: $${totalPrice.toFixed(2)}`, url: shareUrl, }); } catch { handleCopyLink(); } } else { handleCopyLink(); } }; if (!loadingParts && selectedParts.length === 0) { return (

No Build Selected

You need to select at least one part to view your build.

Go to Builder
); } return (
{/* Header */}
← Back to The Armory

Shadow Standard

Build Summary

Review your build, purchase parts from our affiliate partners, or share your build with others.

Total Build Price
${totalPrice.toFixed(2)}
{selectedParts.length} / {CATEGORIES.length} parts selected
{/* Share Section */}

Share Your Build

Share this link to let others view your build or bookmark it to come back later.

{shareUrl && (

Shareable URL:

{shareUrl}

)}
{/* Build Parts */}

Selected Parts

{loadingParts && (

Loading parts…

)} {partsError && (

{partsError} — check the Ballistic API.

)}
{CATEGORIES.map((category) => { const selectedPartId = build[category.id]; const part = parts.find( (p) => p.id === selectedPartId && p.categoryId === (category.id as CategoryId), ); return (
{category.name}
{part ? ( <>
{part.brand}{" "} {part.name}
{part.url && ( Purchase from Retailer )} View Details
) : (
Not selected
)}
{part && (
${part.price.toFixed(2)}
)}
); })}
{/* Actions */}
Edit Build
); }