fixed part role calls and some other stuff

This commit is contained in:
2025-12-13 10:07:11 -05:00
parent 978b9df96c
commit 0f10ff4e09
4 changed files with 74 additions and 61 deletions
+19 -1
View File
@@ -5,6 +5,7 @@ import Link from "next/link";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { CATEGORIES } from "@/data/gunbuilderParts";
import type { CategoryId, Part } from "@/types/gunbuilder";
import { CATEGORY_TO_PART_ROLES } from "@/data/partRoleMappings";
type ViewMode = "card" | "list";
@@ -44,9 +45,21 @@ const slugify = (str: string) =>
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
const normalizeRole = (role: string) => role.trim().toLowerCase().replace(/_/g, "-");
function getNormalizedPartRolesForCategory(categoryId: string): string[] {
const roles = (CATEGORY_TO_PART_ROLES as any)[categoryId] as string[] | undefined;
if (!roles || roles.length === 0) return [];
return Array.from(new Set(roles.map(normalizeRole)));
}
export default function CategoryPage() {
const params = useParams();
const categoryId = params.categoryId as CategoryId;
const partRoles = useMemo(
() => getNormalizedPartRolesForCategory(String(categoryId)),
[categoryId]
);
const router = useRouter();
const searchParams = useSearchParams();
@@ -102,6 +115,11 @@ export default function CategoryPage() {
const search = new URLSearchParams();
search.set("platform", platform);
// ✅ Scope results to this category's backend roles
for (const r of partRoles) {
search.append("partRoles", r);
}
const url = `${API_BASE_URL}/api/products?${search.toString()}`;
const res = await fetch(url, { signal: controller.signal });
@@ -136,7 +154,7 @@ export default function CategoryPage() {
fetchCategoryParts();
return () => controller.abort();
}, [categoryId, platform]);
}, [categoryId, platform, partRoles]);
// persist build to localStorage whenever it changes
useEffect(() => {
+18 -6
View File
@@ -155,7 +155,9 @@ export default function GunbuilderPage() {
const [scopedRes, universalRes] = await Promise.all([
fetch(scopedUrl, { signal: controller.signal }),
fetch(universalUrl, { signal: controller.signal }).catch(() => null as any),
fetch(universalUrl, { signal: controller.signal }).catch(
() => null as any
),
]);
if (!scopedRes || !scopedRes.ok) {
@@ -174,12 +176,20 @@ export default function GunbuilderPage() {
const normalize = (data: GunbuilderProductFromApi[]): Part[] =>
data
.map((p): Part | null => {
const categoryId = PART_ROLE_TO_CATEGORY[p.partRole];
const normalizedRole = (p.partRole ?? "")
.trim()
.toLowerCase()
.replace(/_/g, "-");
const categoryId = PART_ROLE_TO_CATEGORY[normalizedRole];
if (!categoryId) return null;
// Only keep truly-universal categories from the universal feed
// so we don't accidentally mix platforms for platform-scoped parts.
if (data === universalData && !UNIVERSAL_CATEGORIES.has(categoryId)) {
if (
data === universalData &&
!UNIVERSAL_CATEGORIES.has(categoryId)
) {
return null;
}
@@ -471,13 +481,15 @@ export default function GunbuilderPage() {
onChange={(e) => {
const next = e.target.value as (typeof PLATFORMS)[number];
setPlatform(next);
// Keep URL in sync so navigation + refresh preserve platform
const qp = new URLSearchParams(searchParams.toString());
qp.set("platform", next);
qp.delete("select");
router.replace(`/builder?${qp.toString()}`, { scroll: false });
router.replace(`/builder?${qp.toString()}`, {
scroll: false,
});
}}
className="rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1 text-xs text-zinc-100 focus:outline-none focus:ring-1 focus:ring-amber-400"
>
+12 -43
View File
@@ -1,42 +1,11 @@
import type { Category } from "@/types/gunbuilder";
export const CATEGORY_SLUGS: Record<string, string> = {
lower: "lower",
completeLower: "complete-lower",
lowerParts: "lower-parts",
trigger: "trigger",
grip: "grip",
safety: "safety",
buffer: "buffer",
stock: "stock",
upper: "upper",
completeUpper: "complete-upper",
barrel: "barrel",
handguard: "handguard",
gasBlock: "gas-block",
gasTube: "gas-tube",
muzzleDevice: "muzzle-device",
bcg: "bcg",
sights: "sights",
chargingHandle: "charging-handle",
suppressor: "suppressor",
optic: "optic",
magazine: "magazine",
weaponLight: "weapon-light",
foregrip: "foregrip",
bipod: "bipod",
sling: "sling",
railAccessory: "rail-accessory",
tools: "tools",
};
import type { Category, CategoryId } from "@/types/gunbuilder";
import { CATEGORY_SLUGS } from "@/types/gunbuilder";
export const CATEGORIES: Category[] = [
// Lower group
{ id: "lower", name: "Stripped Lowers", group: "lower", slug: CATEGORY_SLUGS.lower },
{ id: "completeLower", name: "Complete Lower", group: "lower", slug: CATEGORY_SLUGS.completeLower },
{ id: "lowerParts", name: "Lower Parts Kit", group: "lower", slug: CATEGORY_SLUGS.lowerParts },
{ id: "complete-lower", name: "Complete Lower", group: "lower", slug: CATEGORY_SLUGS["complete-lower"] },
{ id: "lower-parts", name: "Lower Parts Kit", group: "lower", slug: CATEGORY_SLUGS["lower-parts"] },
{ id: "trigger", name: "Trigger / Fire Control", group: "lower", slug: CATEGORY_SLUGS.trigger },
{ id: "grip", name: "Pistol Grip", group: "lower", slug: CATEGORY_SLUGS.grip },
{ id: "safety", name: "Safety / Selector", group: "lower", slug: CATEGORY_SLUGS.safety },
@@ -45,24 +14,24 @@ export const CATEGORIES: Category[] = [
// Upper group
{ id: "upper", name: "Stripped Upper", group: "upper", slug: CATEGORY_SLUGS.upper },
{ id: "completeUpper", name: "Complete Upper", group: "upper", slug: CATEGORY_SLUGS.completeUpper },
{ id: "complete-upper", name: "Complete Upper", group: "upper", slug: CATEGORY_SLUGS["complete-upper"] },
{ id: "barrel", name: "Barrels", group: "upper", slug: CATEGORY_SLUGS.barrel },
{ id: "handguard", name: "Handguards / Rails", group: "upper", slug: CATEGORY_SLUGS.handguard },
{ id: "gasBlock", name: "Gas Block", group: "upper", slug: CATEGORY_SLUGS.gasBlock },
{ id: "gasTube", name: "Gas Tube", group: "upper", slug: CATEGORY_SLUGS.gasTube },
{ id: "muzzleDevice", name: "Muzzle Device", group: "upper", slug: CATEGORY_SLUGS.muzzleDevice },
{ id: "gas-block", name: "Gas Block", group: "upper", slug: CATEGORY_SLUGS["gas-block"] },
{ id: "gas-tube", name: "Gas Tube", group: "upper", slug: CATEGORY_SLUGS["gas-tube"] },
{ id: "muzzle-device", name: "Muzzle Device", group: "upper", slug: CATEGORY_SLUGS["muzzle-device"] },
{ id: "sights", name: "Iron Sights", group: "upper", slug: CATEGORY_SLUGS.sights },
{ id: "bcg", name: "Bolt Carrier Group", group: "upper", slug: CATEGORY_SLUGS.bcg },
{ id: "chargingHandle", name: "Charging Handle", group: "upper", slug: CATEGORY_SLUGS.chargingHandle },
{ id: "charging-handle", name: "Charging Handle", group: "upper", slug: CATEGORY_SLUGS["charging-handle"] },
{ id: "suppressor", name: "Suppressors", group: "upper", slug: CATEGORY_SLUGS.suppressor },
{ id: "optic", name: "Optics", group: "upper", slug: CATEGORY_SLUGS.optic },
// Accessories (platform-agnostic)
{ id: "magazine", name: "Magazines", group: "accessories", slug: CATEGORY_SLUGS.magazine },
{ id: "weaponLight", name: "Weapon Lights & Lasers", group: "accessories", slug: CATEGORY_SLUGS.weaponLight },
{ id: "weapon-light", name: "Weapon Lights & Lasers", group: "accessories", slug: CATEGORY_SLUGS["weapon-light"] },
{ id: "foregrip", name: "Vertical / Angled Grips", group: "accessories", slug: CATEGORY_SLUGS.foregrip },
{ id: "bipod", name: "Bipods", group: "accessories", slug: CATEGORY_SLUGS.bipod },
{ id: "sling", name: "Slings & Mounts", group: "accessories", slug: CATEGORY_SLUGS.sling },
{ id: "railAccessory", name: "Rail Accessories", group: "accessories", slug: CATEGORY_SLUGS.railAccessory },
{ id: "rail-accessory", name: "Rail Accessories", group: "accessories", slug: CATEGORY_SLUGS["rail-accessory"] },
{ id: "tools", name: "Tools & Maintenance", group: "accessories", slug: CATEGORY_SLUGS.tools },
];
];
+25 -11
View File
@@ -1,41 +1,55 @@
import type { CategoryId } from "@/types/gunbuilder";
export const CATEGORY_TO_PART_ROLES: Record<CategoryId, string[]> = {
/**
* Maps canonical kebab-case CategoryIds to their associated part roles from the backend.
*/
export const CATEGORY_TO_PART_ROLES: Partial<Record<CategoryId, string[]>> = {
// ===== UPPER =====
upper: ["upper-receiver", "upper"],
completeUpper: ["complete-upper"],
"complete-upper": ["complete-upper"],
barrel: ["barrel"],
gasBlock: ["gas-block"],
gasTube: ["gas-tube"],
muzzleDevice: ["muzzle-device", "compensator", "brake"],
"gas-block": ["gas-block"],
"gas-tube": ["gas-tube"],
"muzzle-device": ["muzzle-device", "compensator", "brake"],
suppressor: ["suppressor"],
handguard: ["handguard"],
chargingHandle: ["charging-handle"],
"charging-handle": ["charging-handle"],
bcg: ["bcg", "bolt-carrier-group"],
// ===== LOWER =====
lower: ["lower-receiver", "lower", "LOWER_RECEIVER_STRIPPED"],
completeLower: ["complete-lower"],
lowerParts: ["lower-parts-kit", "lower-parts"],
"complete-lower": ["complete-lower"],
"lower-parts": ["lower-parts-kit", "lower-parts"],
trigger: ["trigger", "trigger-kit"],
grip: ["pistol-grip", "grip"],
safety: ["safety", "safety-selector"],
buffer: ["buffer-kit", "buffer"],
stock: ["stock"],
// ===== OPTICS =====
sights: ["sight", "sights", "iron-sights"],
optic: ["optic", "optics"],
// ===== ACCESSORIES =====
magazine: ["magazine", "mag", "magazine-ar15", "magazine-308", "drum-magazine"],
weaponLight: ["weapon-light", "light", "weapon-light-laser", "light-laser-combo", "laser"],
"weapon-light": ["weapon-light", "light", "weapon-light-laser", "light-laser-combo", "laser"],
foregrip: ["vertical-grip", "angled-foregrip", "foregrip", "handstop"],
bipod: ["bipod"],
sling: ["sling", "sling-mount", "sling-swivel", "qd-sling-mount"],
railAccessory: ["rail-section", "picatinny-rail-section", "m-lok-rail-section", "keymod-rail-section", "rail-cover", "rail-panel"],
"rail-accessory": ["rail-section", "picatinny-rail-section", "m-lok-rail-section", "keymod-rail-section", "rail-cover", "rail-panel"],
tools: ["tool", "armorer-tool", "armorer-wrench", "cleaning-kit", "bore-snake", "vise-block", "torque-wrench"],
};
export const PART_ROLE_TO_CATEGORY: Record<string, CategoryId> = Object.entries(
CATEGORY_TO_PART_ROLES
).reduce((acc, [categoryId, roles]) => {
for (const role of roles) acc[role] = categoryId as CategoryId;
for (const role of roles) {
// store the original string
acc[role] = categoryId as CategoryId;
// also store a normalized version (handles ENUM_CASE and snake_case)
const normalized = role.trim().toLowerCase().replace(/_/g, "-");
acc[normalized] = categoryId as CategoryId;
}
return acc;
}, {} as Record<string, CategoryId>);