mad cleanup.

This commit is contained in:
2025-12-29 13:59:17 -05:00
parent 0dd89a751b
commit cc386d14f4
14 changed files with 1054 additions and 315 deletions
+53 -40
View File
@@ -29,7 +29,7 @@ import { useAuth } from "@/context/AuthContext";
import { CATEGORIES } from "@/data/gunbuilderParts";
import { BUILDER_SLOTS, isSlotSatisfied } from "@/data/builderSlots";
import type { CategoryId, Part } from "@/types/gunbuilder";
import type { BuilderSlotKey, Part } from "@/types/builderSlots";
import { PART_ROLE_TO_CATEGORY } from "@/lib/catalogMappings";
// ✅ Centralized overlap rules + helpers
@@ -64,7 +64,7 @@ const API_BASE_URL =
const STORAGE_KEY = "gunbuilder-build-state";
// Categories that should NOT be filtered by platform (universal accessories / gear)
const UNIVERSAL_CATEGORIES = new Set<CategoryId>([
const UNIVERSAL_CATEGORIES = new Set<BuilderSlotKey>([
"magazine",
"weapon-light",
"foregrip",
@@ -82,7 +82,7 @@ const CATEGORY_GROUPS: {
id: string;
label: string;
description?: string;
categoryIds: CategoryId[];
categoryIds: BuilderSlotKey[];
}[] = [
{
id: "lower-group",
@@ -92,10 +92,10 @@ const CATEGORY_GROUPS: {
categoryIds: [
"lower-receiver",
"complete-lower",
"lower-parts",
"lower-parts-kit", // ✅ was lower-parts
"trigger",
"grip",
"safety",
"safety-selector", // ✅ was safety
"buffer",
"stock",
],
@@ -219,8 +219,8 @@ export default function GunbuilderPage() {
// -----------------------------
// Derived collections
// -----------------------------
const partsByCategory: Record<CategoryId, Part[]> = useMemo(() => {
const grouped = {} as Record<CategoryId, Part[]>;
const partsByCategory: Record<BuilderSlotKey, Part[]> = useMemo(() => {
const grouped = {} as Record<BuilderSlotKey, Part[]>;
for (const category of CATEGORIES) {
grouped[category.id] = parts.filter((p) => p.categoryId === category.id);
}
@@ -242,14 +242,14 @@ export default function GunbuilderPage() {
});
}, [build, parts]);
const selectedByCategory: Record<CategoryId, unknown> = useMemo(
const selectedByCategory: Record<BuilderSlotKey, unknown> = useMemo(
() =>
Object.fromEntries(
Object.entries(build).map(([categoryId, partId]) => [
categoryId as CategoryId,
categoryId as BuilderSlotKey,
partId ? true : false,
])
) as Record<CategoryId, unknown>,
) as Record<BuilderSlotKey, unknown>,
[build]
);
@@ -262,7 +262,7 @@ export default function GunbuilderPage() {
// Role -> Category resolver
// NOTE: defensive while backend roles/mappings evolve
// -----------------------------
const resolveCategoryId = (normalizedRole: string): CategoryId | null => {
const resolveCategoryId = (normalizedRole: string): BuilderSlotKey | null => {
if (normalizedRole === "upper-receiver") return "upper-receiver";
if (normalizedRole.includes("complete-upper")) return "complete-upper";
@@ -311,15 +311,23 @@ export default function GunbuilderPage() {
if (normalizedRole.includes("suppress")) return "suppressor";
// ✅ FIX: Lower Parts Kit canonical key
if (
normalizedRole.includes("lower-parts") ||
normalizedRole.includes("lpk")
)
return "lower-parts";
return "lower-parts-kit";
if (normalizedRole.includes("trigger")) return "trigger";
if (normalizedRole.includes("grip")) return "grip";
if (normalizedRole.includes("safety")) return "safety";
// ✅ FIX: Safety canonical key
if (
normalizedRole.includes("safety") ||
normalizedRole.includes("selector")
)
return "safety-selector";
if (normalizedRole.includes("buffer")) return "buffer";
if (normalizedRole.includes("stock")) return "stock";
@@ -328,6 +336,7 @@ export default function GunbuilderPage() {
if (normalizedRole.includes("sight")) return "sights";
if (normalizedRole.includes("mag")) return "magazine";
if (
normalizedRole.includes("weapon-light") ||
(normalizedRole.includes("light") && !normalizedRole.includes("flight"))
@@ -354,27 +363,6 @@ export default function GunbuilderPage() {
return (PART_ROLE_TO_CATEGORY as any)[normalizedRole] ?? null;
};
// -----------------------------
// Selection handler (with hint warnings)
// -----------------------------
const handleSelectPart = useCallback(
(categoryId: CategoryId, partId: string, _opts?: { confirm?: boolean }) => {
const warn = (msg: string) => {
setShareStatus(msg);
window.setTimeout(() => setShareStatus(null), 4000);
};
const hints = getSelectionHints(categoryId, build);
if (hints.length > 0) warn(hints[0]);
setBuild((prev) => ({
...prev,
[categoryId]: partId,
}));
},
[build]
);
// -----------------------------
// Fetch products (scoped + universal merge)
// -----------------------------
@@ -501,7 +489,7 @@ export default function GunbuilderPage() {
setBuild((prevBuild) => {
const next: BuildState = {};
for (const [categoryId, partId] of Object.entries(prevBuild)) {
const cid = categoryId as CategoryId;
const cid = categoryId as BuilderSlotKey;
if (UNIVERSAL_CATEGORIES.has(cid)) {
next[cid] = partId;
}
@@ -661,6 +649,31 @@ export default function GunbuilderPage() {
}
};
// -----------------------------
// Selection handler (with hint warnings)
// -----------------------------
const handleSelectPart = useCallback(
(
categoryId: BuilderSlotKey,
partId: string,
_opts?: { confirm?: boolean }
) => {
const warn = (msg: string) => {
setShareStatus(msg);
window.setTimeout(() => setShareStatus(null), 4000);
};
const hints = getSelectionHints(categoryId, build);
if (hints.length > 0) warn(hints[0]);
setBuild((prev) => ({
...prev,
[categoryId]: partId,
}));
},
[build]
);
// -----------------------------
// Handle URL query parameters:
// - ?select=categoryId:partId
@@ -685,7 +698,7 @@ export default function GunbuilderPage() {
if (selectParam) {
const [categoryIdRaw, partId] = selectParam.split(":");
const requestedCategoryId = categoryIdRaw as CategoryId;
const requestedCategoryId = categoryIdRaw as BuilderSlotKey;
if (requestedCategoryId && partId) {
handleSelectPart(requestedCategoryId, partId, { confirm: false });
@@ -696,7 +709,7 @@ export default function GunbuilderPage() {
if (CATEGORIES.some((c) => c.id === removeParam)) {
setBuild((prev) => {
const next: BuildState = { ...prev };
delete next[removeParam as CategoryId];
delete next[removeParam as BuilderSlotKey];
return next;
});
}
@@ -744,7 +757,7 @@ export default function GunbuilderPage() {
}
}, [build]);
const summaryCategoryOrder: CategoryId[] = useMemo(
const summaryCategoryOrder: BuilderSlotKey[] = useMemo(
() =>
CATEGORY_GROUPS.flatMap((group) => group.categoryIds).filter((id) =>
CATEGORIES.some((c) => c.id === id)
@@ -1164,7 +1177,7 @@ export default function GunbuilderPage() {
<div className="space-y-6">
{CATEGORY_GROUPS.map((group) => {
const groupCategories = CATEGORIES.filter((c) =>
group.categoryIds.includes(c.id as CategoryId)
group.categoryIds.includes(c.id as BuilderSlotKey)
);
if (groupCategories.length === 0) return null;
@@ -1260,7 +1273,7 @@ export default function GunbuilderPage() {
{/* Overlap chips help explain conflicts / dependencies */}
<div className="mt-1 flex flex-wrap gap-1.5">
{getOverlapChipsForCategory(
category.id as CategoryId,
category.id as BuilderSlotKey,
build
).map((c) => (
<OverlapChip
@@ -3,7 +3,7 @@
import { useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import type { CategoryId } from "@/types/gunbuilder";
import type { CategoryId } from "@/types/builderSlots";
import {
PART_ROLE_TO_CATEGORY,
normalizePartRole,
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from "react";
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 type { CategoryId, Part } from "@/types/builderSlots";
import { CATEGORY_TO_PART_ROLES } from "@/data/partRoleMappings";
type ViewMode = "card" | "list";
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import type { CategoryId } from "@/types/gunbuilder";
import type { CategoryId } from "@/types/builderSlots";
import { PART_ROLE_TO_CATEGORY, normalizePartRole } from "@/lib/catalogMappings";
/**