fixed part role mapping to categories. set a const for categories
This commit is contained in:
@@ -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
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user