"use client"; import { useMemo, useStat, 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 { buildDetailHref } from "@/app/parts/_components/buildDetailHref"; 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 (align with builder categories) 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", }; // Inverse: CategoryId -> backend partRole (for building detail URLs) const CATEGORY_TO_PART_ROLE: Partial> = Object.fromEntries( Object.entries(PART_ROLE_TO_CATEGORY).map(([role, cat]) => [cat, role]), ) as Partial>; // 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(); // ✅ Option A: platform is a query param and is passed through to backend const platform = searchParams.get("platform") ?? "AR-15"; 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); try { localStorage.setItem(STORAGE_KEY, JSON.stringify(decoded)); } catch { // ignore storage failures (private mode, etc.) } } else if (typeof window !== "undefined") { const stored = localStorage.getItem(STORAGE_KEY); if (stored) { try { const parsed = JSON.parse(stored); setBuild(parsed); } catch { // ignore invalid stored data } } } }, [searchParams]); // Fetch live parts from backend (uses selected platform) useEffect(() => { const controller = new AbortController(); async function fetchProducts() { try { setLoadingParts(true); setPartsError(null); const res = await fetch( `${API_BASE_URL}/api/v1/products?platform=${encodeURIComponent(platform)}`, { 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, 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(); }, [platform]); // Generate shareable URL (includes platform) useEffect(() => { if (typeof window !== "undefined" && Object.keys(build).length > 0) { const encoded = encodeBuildState(build); const url = `${window.location.origin}/builder/build?platform=${encodeURIComponent( platform, )}&build=${encoded}`; setShareUrl(url); } else if (typeof window !== "undefined") { setShareUrl(""); } }, [build, platform]); 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) return; try { await navigator.clipboard.writeText(shareUrl); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error("Failed to copy:", err); } }; const handleShare = async () => { if (!shareUrl) return; if (navigator.share) { try { await navigator.share({ title: `My ${platform} Build`, text: `Check out my ${platform} build: $${totalPrice.toFixed(2)}`, url: shareUrl, }); return; } catch { // fall back to copy } } await 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.

Platform: {platform}

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), ); const partRole = (CATEGORY_TO_PART_ROLE[category.id] as string | undefined) ?? "unknown"; 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
); }