fixed part role mapping to categories. set a const for categories

This commit is contained in:
2025-12-02 21:13:17 -05:00
parent c51ce862a9
commit 4b823f49cf
196 changed files with 2820 additions and 164 deletions
+4 -14
View File
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { CATEGORIES } from "@/data/gunbuilderParts";
import { CATEGORY_TO_PART_ROLES } from "@/data/partRoleMappings";
import type { CategoryId, Part } from "@/types/gunbuilder";
type ViewMode = "card" | "list";
@@ -22,18 +23,6 @@ type GunbuilderProductFromApi = {
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
const CATEGORY_TO_PART_ROLES: Record<CategoryId, string[]> = {
upper: ["upper-receiver"],
barrel: ["barrel"],
handguard: ["handguard"],
chargingHandle: ["charging-handle"],
buffer: ["buffer-kit"],
lowerParts: ["lower-parts-kit"],
sights: ["sight"],
lower: ["lower-receiver"],
optic: ["optic"],
stock: ["stock"],
};
// sort options
type SortOption = "relevance" | "price-asc" | "price-desc" | "brand-asc";
@@ -80,10 +69,11 @@ export default function CategoryPage() {
async function fetchCategoryParts() {
try {
setLoading(true);
setLoading(true);
setError(null);
const roles = CATEGORY_TO_PART_ROLES[categoryId] ?? [];
// FIX: determine which backend partRoles map to this category
const roles = CATEGORY_TO_PART_ROLES[categoryId] ?? [];
const search = new URLSearchParams();
search.set("platform", "AR-15");
@@ -27,16 +27,27 @@ type UpsertRequest = {
// Keep this in sync with your backend `partRole` values
const PART_ROLE_OPTIONS = [
{ value: "", label: "— Unmapped —" },
// Lower / fire control
{ value: "lower-receiver", label: "Lower Receiver" },
{ value: "lower-parts-kit", label: "Lower Parts Kit" },
{ value: "trigger", label: "Trigger / Trigger Kit" },
{ value: "pistol-grip", label: "Pistol Grip" },
{ value: "safety-selector", label: "Safety Selector" },
// Upper
{ value: "upper-receiver", label: "Upper Receiver" },
{ value: "barrel", label: "Barrel" },
{ value: "handguard", label: "Handguard" },
{ value: "charging-handle", label: "Charging Handle" },
{ value: "buffer-kit", label: "Buffer Kit" },
{ value: "lower-parts-kit", label: "Lower Parts Kit" },
{ value: "sight", label: "Sight / Optic" },
{ value: "lower-receiver", label: "Lower Receiver" },
{ value: "optic", label: "Optic" },
{ value: "gas-system", label: "Gas System / Gas Block" },
{ value: "muzzle-device", label: "Muzzle Device" },
{ value: "suppressor", label: "Suppressor" },
// Furniture / sights / optics
{ value: "stock", label: "Stock" },
{ value: "buffer-kit", label: "Buffer Kit" },
{ value: "charging-handle", label: "Charging Handle" },
{ value: "sight", label: "Iron / Backup Sight" },
{ value: "optic", label: "Optic" },
{ value: "other", label: "Other / Ignore" },
];
+87 -34
View File
@@ -6,6 +6,7 @@ import { useSearchParams, useRouter } from "next/navigation";
import { CATEGORIES } from "@/data/gunbuilderParts";
import type { CategoryId, Part } from "@/types/gunbuilder";
import { CategoryColumn } from "@/components/CategoryColumn";
import { PART_ROLE_TO_CATEGORY } from "@/data/partRoleMappings";
type GunbuilderProductFromApi = {
id: number; // backend numeric id
@@ -21,19 +22,6 @@ type GunbuilderProductFromApi = {
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
// Map backend partRole to your existing CategoryId values.
const PART_ROLE_TO_CATEGORY: Record<string, CategoryId> = {
"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,
"lower-receiver": "lower" as CategoryId,
optic: "optic" as CategoryId,
stock: "stock" as CategoryId,
};
type BuildState = Partial<Record<CategoryId, string>>; // categoryId -> partId
@@ -54,22 +42,37 @@ const CATEGORY_GROUPS: {
id: "lower-group",
label: "Lower Receiver Parts",
description:
"Everything from the serialized lower to small parts and core controls.",
categoryIds: ["lower", "lowerParts", "stock", "buffer"] as CategoryId[],
"Everything from the serialized lower to small parts, fire control, and core controls.",
categoryIds: [
"lower",
"completeLower",
"lowerParts",
"trigger",
"grip",
"safety",
"buffer",
"stock",
],
},
{
id: "upper-group",
label: "Upper Receiver Parts",
description:
"Barrel, upper, handguard, and the parts that keep the rifle cycling.",
"Barrel, upper, gas system, and the parts that keep the rifle cycling.",
categoryIds: [
"upper",
"completeUpper",
"bcg",
"barrel",
"gasBlock",
"gasTube",
"muzzleDevice",
"suppressor",
"handguard",
"chargingHandle",
"sights",
"optic",
] as CategoryId[],
],
},
];
@@ -92,6 +95,7 @@ export default function GunbuilderPage() {
}
return {};
});
const [layoutMode, setLayoutMode] = useState<"cards" | "list">("cards");
useEffect(() => {
const controller = new AbortController();
@@ -266,6 +270,35 @@ export default function GunbuilderPage() {
<div className="grid gap-4 lg:grid-cols-[3fr,1.25fr]">
{/* Categories/parts */}
<section className="rounded-lg border border-zinc-800 bg-zinc-950/60 p-3 md:p-4">
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<p className="text-xs text-zinc-500">
Choose one part per category. Switch layouts to browse by cards or a tighter list view.
</p>
<div className="inline-flex items-center rounded-md border border-zinc-700 bg-zinc-900/70 text-[0.7rem]">
<button
type="button"
onClick={() => setLayoutMode("cards")}
className={`px-2.5 py-1 rounded-l-md border-r border-zinc-700 transition-colors ${
layoutMode === "cards"
? "bg-amber-500/20 text-amber-200"
: "text-zinc-400 hover:bg-zinc-800"
}`}
>
Card view
</button>
<button
type="button"
onClick={() => setLayoutMode("list")}
className={`px-2.5 py-1 rounded-r-md transition-colors ${
layoutMode === "list"
? "bg-amber-500/20 text-amber-200"
: "text-zinc-400 hover:bg-zinc-800"
}`}
>
List view
</button>
</div>
</div>
{loading && (
<p className="text-sm text-zinc-500">
Loading parts from backend
@@ -303,23 +336,43 @@ export default function GunbuilderPage() {
</div>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{groupCategories.map((category) => (
<div
key={category.id}
className="border border-zinc-800 rounded-md p-2 md:p-3 bg-zinc-950/60"
>
<CategoryColumn
category={category}
parts={partsByCategory[category.id]}
selectedPartId={build[category.id]}
onSelectPart={(partId) =>
handleSelectPart(category.id, partId)
}
/>
</div>
))}
</div>
{layoutMode === "cards" ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{groupCategories.map((category) => (
<div
key={category.id}
className="border border-zinc-800 rounded-md p-2 md:p-3 bg-zinc-950/60"
>
<CategoryColumn
category={category}
parts={partsByCategory[category.id]}
selectedPartId={build[category.id]}
onSelectPart={(partId) =>
handleSelectPart(category.id, partId)
}
/>
</div>
))}
</div>
) : (
<div className="space-y-3">
{groupCategories.map((category) => (
<div
key={category.id}
className="border border-zinc-800 rounded-md p-2 md:p-3 bg-zinc-950/60 flex flex-col md:flex-row md:items-start md:justify-between gap-3"
>
<CategoryColumn
category={category}
parts={partsByCategory[category.id]}
selectedPartId={build[category.id]}
onSelectPart={(partId) =>
handleSelectPart(category.id, partId)
}
/>
</div>
))}
</div>
)}
</div>
);
})}