"use client"; import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useParams } from "next/navigation"; import { CATEGORIES } from "@/data/gunbuilderParts"; import type { CategoryId, Part } from "@/types/gunbuilder"; type ViewMode = "card" | "list"; 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 from category id -> Ballistic partRole(s) * Keep this in sync with whatever you’re using on the main /gunbuilder page. */ const CATEGORY_TO_PART_ROLES: Record = { upper: ["upper-receiver"], barrel: ["barrel"], handguard: ["handguard"], chargingHandle: ["charging-handle"], buffer: ["buffer-kit"], lowerParts: ["lower-parts-kit"], sights: ["sight"], }; export default function CategoryPage() { const params = useParams(); const categoryId = params.categoryId as CategoryId; const [viewMode, setViewMode] = useState("list"); const [parts, setParts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const category = useMemo( () => CATEGORIES.find((c) => c.id === categoryId), [categoryId], ); useEffect(() => { if (!categoryId) return; const controller = new AbortController(); async function fetchCategoryParts() { try { setLoading(true); setError(null); const roles = CATEGORY_TO_PART_ROLES[categoryId] ?? []; const search = new URLSearchParams(); search.set("platform", "AR-15"); roles.forEach((r) => search.append("partRoles", r)); const url = `${API_BASE_URL}/api/products/gunbuilder${ roles.length ? `?${search.toString()}` : "" }`; const res = await fetch(url, { 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) => ({ id: p.uuid, categoryId, name: p.name, brand: p.brandName, price: p.price ?? 0, // these fields exist in the Part type even if you’re not using them yet imageUrl: p.mainImageUrl ?? undefined, url: p.buyUrl ?? undefined, notes: undefined, })); setParts(normalized); } catch (err: any) { if (err.name === "AbortError") return; setError(err.message ?? "Failed to load products"); } finally { setLoading(false); } } fetchCategoryParts(); return () => controller.abort(); }, [categoryId]); if (!category) { return (

Category Not Found

Return to Gunbuilder
); } return (
{/* Header */}
← Back to Gunbuilder

Shadow Standard

{category.name} Parts

Browse all available {category.name.toLowerCase()} options for your build, pulled live from your Ballistic backend.

{!loading && !error && parts.length > 0 && (
)}
{/* Parts Grid/List */}
{loading ? (

Loading {category.name.toLowerCase()}…

) : error ? (

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

) : parts.length === 0 ? (

No parts available for this category yet.

) : viewMode === "card" ? (
{parts.map((part) => (
{part.brand}{" "} — {part.name}
{part.notes && (

{part.notes}

)}
${part.price.toFixed(2)}
View Details {/* Hover Overlay */}
Add to Build
))}
) : (
{parts.map((part) => (
{part.brand}{" "} — {part.name}
{part.notes && (

{part.notes}

)}
${part.price.toFixed(2)}
View Details
{/* Hover Overlay */}
Add to Build
))}
)}
); }