lots of fixes. cant remember them all
This commit is contained in:
+68
-138
@@ -6,9 +6,17 @@ import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { CATEGORIES } from "@/data/gunbuilderParts";
|
||||
import { BUILDER_SLOTS, isSlotSatisfied } from "@/data/builderSlots";
|
||||
import type { CategoryId, Part } from "@/types/gunbuilder";
|
||||
import { PART_ROLE_TO_CATEGORY } from "@/data/partRoleMappings";
|
||||
import { PART_ROLE_TO_CATEGORY } from "@/lib/catalogMappings";
|
||||
import { Pencil, Link2, Square, CheckSquare } from "lucide-react";
|
||||
|
||||
// ✅ Centralized overlap rules + helpers
|
||||
import OverlapChip from "@/components/builder/OverlapChip";
|
||||
import {
|
||||
getOverlapChipsForCategory,
|
||||
getSelectionHints,
|
||||
type BuildState,
|
||||
} from "@/lib/buildOverlaps";
|
||||
|
||||
type GunbuilderProductFromApi = {
|
||||
id: number; // backend numeric id
|
||||
name: string;
|
||||
@@ -25,8 +33,6 @@ const PLATFORMS = ["AR-15", "AR-10", "AR-9"] as const;
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
|
||||
|
||||
type BuildState = Partial<Record<CategoryId, string>>; // categoryId -> partId
|
||||
|
||||
const STORAGE_KEY = "gunbuilder-build-state";
|
||||
|
||||
// Categories that should NOT be filtered by platform (universal accessories / gear)
|
||||
@@ -103,33 +109,6 @@ const CATEGORY_GROUPS: {
|
||||
},
|
||||
];
|
||||
|
||||
// ===== Build rules: complete assemblies make sub-parts unnecessary =====
|
||||
const COMPLETE_UPPER_CATEGORY: CategoryId = "complete-upper";
|
||||
const COMPLETE_LOWER_CATEGORY: CategoryId = "complete-lower";
|
||||
|
||||
// If a complete upper is selected, these categories are considered "included".
|
||||
const UPPER_INCLUDED_CATEGORIES = new Set<CategoryId>([
|
||||
"upper-receiver",
|
||||
"bcg",
|
||||
"barrel",
|
||||
"gas-block",
|
||||
"gas-tube",
|
||||
"muzzle-device",
|
||||
"suppressor",
|
||||
"handguard",
|
||||
"charging-handle",
|
||||
]);
|
||||
|
||||
const LOWER_INCLUDED_CATEGORIES = new Set<CategoryId>([
|
||||
"lower-receiver",
|
||||
"lower-parts",
|
||||
"trigger",
|
||||
"grip",
|
||||
"safety",
|
||||
"buffer",
|
||||
"stock",
|
||||
]);
|
||||
|
||||
export default function GunbuilderPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
@@ -305,97 +284,25 @@ export default function GunbuilderPage() {
|
||||
[selectedParts]
|
||||
);
|
||||
|
||||
// ---------- Selection logic ----------
|
||||
const handleSelectPart = useCallback(
|
||||
(
|
||||
categoryId: CategoryId,
|
||||
partId: string,
|
||||
opts?: { confirm?: boolean }
|
||||
) => {
|
||||
const shouldConfirm = opts?.confirm !== false;
|
||||
(categoryId: CategoryId, partId: string, _opts?: { confirm?: boolean }) => {
|
||||
// NOTE:
|
||||
// - We DO NOT auto-remove any existing selections.
|
||||
// - We show subtle overlap guidance (chips + an optional “heads up” toast).
|
||||
|
||||
if (shouldConfirm && typeof window !== "undefined") {
|
||||
if (categoryId === COMPLETE_UPPER_CATEGORY) {
|
||||
const toClear = Array.from(UPPER_INCLUDED_CATEGORIES).filter(
|
||||
(cid) => !!build[cid]
|
||||
);
|
||||
if (toClear.length > 0) {
|
||||
const labels = toClear
|
||||
.map((cid) => CATEGORIES.find((c) => c.id === cid)?.name ?? cid)
|
||||
.join(", ");
|
||||
const warn = (msg: string) => {
|
||||
setShareStatus(msg);
|
||||
window.setTimeout(() => setShareStatus(null), 4000);
|
||||
};
|
||||
|
||||
const ok = window.confirm(
|
||||
`Selecting a Complete Upper will remove your currently selected upper parts:\n\n${labels}\n\nContinue?`
|
||||
);
|
||||
if (!ok) return;
|
||||
}
|
||||
}
|
||||
const hints = getSelectionHints(categoryId, build);
|
||||
if (hints.length > 0) warn(hints[0]);
|
||||
|
||||
if (categoryId === COMPLETE_LOWER_CATEGORY) {
|
||||
const toClear = Array.from(LOWER_INCLUDED_CATEGORIES).filter(
|
||||
(cid) => !!build[cid]
|
||||
);
|
||||
if (toClear.length > 0) {
|
||||
const labels = toClear
|
||||
.map((cid) => CATEGORIES.find((c) => c.id === cid)?.name ?? cid)
|
||||
.join(", ");
|
||||
|
||||
const ok = window.confirm(
|
||||
`Selecting a Complete Lower will remove your currently selected lower parts:\n\n${labels}\n\nContinue?`
|
||||
);
|
||||
if (!ok) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
UPPER_INCLUDED_CATEGORIES.has(categoryId) &&
|
||||
!!build[COMPLETE_UPPER_CATEGORY]
|
||||
) {
|
||||
const ok = window.confirm(
|
||||
`Selecting this part will remove your selected Complete Upper. Continue?`
|
||||
);
|
||||
if (!ok) return;
|
||||
}
|
||||
|
||||
if (
|
||||
LOWER_INCLUDED_CATEGORIES.has(categoryId) &&
|
||||
!!build[COMPLETE_LOWER_CATEGORY]
|
||||
) {
|
||||
const ok = window.confirm(
|
||||
`Selecting this part will remove your selected Complete Lower. Continue?`
|
||||
);
|
||||
if (!ok) return;
|
||||
}
|
||||
}
|
||||
|
||||
setBuild((prev) => {
|
||||
const updated: BuildState = {
|
||||
...prev,
|
||||
[categoryId]: partId,
|
||||
};
|
||||
|
||||
if (categoryId === COMPLETE_UPPER_CATEGORY) {
|
||||
for (const cid of UPPER_INCLUDED_CATEGORIES) {
|
||||
delete updated[cid];
|
||||
}
|
||||
}
|
||||
|
||||
if (UPPER_INCLUDED_CATEGORIES.has(categoryId)) {
|
||||
delete updated[COMPLETE_UPPER_CATEGORY];
|
||||
}
|
||||
|
||||
if (categoryId === COMPLETE_LOWER_CATEGORY) {
|
||||
for (const cid of LOWER_INCLUDED_CATEGORIES) {
|
||||
delete updated[cid];
|
||||
}
|
||||
}
|
||||
|
||||
if (LOWER_INCLUDED_CATEGORIES.has(categoryId)) {
|
||||
delete updated[COMPLETE_LOWER_CATEGORY];
|
||||
}
|
||||
|
||||
return updated;
|
||||
});
|
||||
// ✅ Only set the selection. No deletes. No clearing.
|
||||
setBuild((prev) => ({
|
||||
...prev,
|
||||
[categoryId]: partId,
|
||||
}));
|
||||
},
|
||||
[build]
|
||||
);
|
||||
@@ -536,22 +443,18 @@ export default function GunbuilderPage() {
|
||||
const removeParam = searchParams.get("remove");
|
||||
const qpPlatform = searchParams.get("platform");
|
||||
|
||||
// Build a unique key for this action so we only apply it once.
|
||||
const actionKey = `${selectParam ?? ""}|${removeParam ?? ""}|${
|
||||
qpPlatform ?? ""
|
||||
}`;
|
||||
|
||||
// If no action, clear the ref and do nothing.
|
||||
if (!selectParam && !removeParam) {
|
||||
processedActionKeyRef.current = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent infinite loops: if we already processed this exact actionKey, stop.
|
||||
if (processedActionKeyRef.current === actionKey) return;
|
||||
processedActionKeyRef.current = actionKey;
|
||||
|
||||
// Apply actions
|
||||
if (selectParam) {
|
||||
const [categoryIdRaw, partId] = selectParam.split(":");
|
||||
const requestedCategoryId = categoryIdRaw as CategoryId;
|
||||
@@ -573,7 +476,6 @@ export default function GunbuilderPage() {
|
||||
|
||||
const nextPlatform = isValidPlatform(qpPlatform) ? qpPlatform : platform;
|
||||
|
||||
// Clean URL (remove select/remove) but ONLY if needed.
|
||||
const nextUrl = `/builder?platform=${encodeURIComponent(nextPlatform)}`;
|
||||
if (typeof window !== "undefined") {
|
||||
const current = `${window.location.pathname}${window.location.search}`;
|
||||
@@ -613,7 +515,6 @@ export default function GunbuilderPage() {
|
||||
}
|
||||
}, [build]);
|
||||
|
||||
// Use group ordering for the summary as well
|
||||
const summaryCategoryOrder: CategoryId[] = useMemo(
|
||||
() =>
|
||||
CATEGORY_GROUPS.flatMap((group) => group.categoryIds).filter((id) =>
|
||||
@@ -812,7 +713,9 @@ export default function GunbuilderPage() {
|
||||
}}
|
||||
disabled={selectedParts.length === 0}
|
||||
className={`w-full sm:w-auto rounded-md border border-zinc-700 bg-zinc-900/50 px-3 py-2 text-sm font-medium text-zinc-300 hover:bg-zinc-800 hover:border-zinc-600 transition-colors ${
|
||||
selectedParts.length === 0 ? "opacity-40 cursor-not-allowed" : ""
|
||||
selectedParts.length === 0
|
||||
? "opacity-40 cursor-not-allowed"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
Clear Build
|
||||
@@ -828,7 +731,8 @@ export default function GunbuilderPage() {
|
||||
Share Your Build
|
||||
</h3>
|
||||
<p className="mt-1 text-[0.7rem] text-zinc-500">
|
||||
Share this link to let others view your build or bookmark it to come back later.
|
||||
Share this link to let others view your build or bookmark it
|
||||
to come back later.
|
||||
</p>
|
||||
<div className="mt-2 flex flex-col gap-2 md:flex-row md:items-center">
|
||||
<div className="flex-1">
|
||||
@@ -903,8 +807,9 @@ export default function GunbuilderPage() {
|
||||
{/* Layout */}
|
||||
<section className="rounded-lg border border-zinc-800 bg-zinc-950/60 p-3 md:p-4">
|
||||
<p className="mb-4 text-xs text-zinc-500">
|
||||
Work top-down through the major sections. Each row shows your current pick for that part type,
|
||||
with price and a direct buy link—or a quick way to choose a part if you haven't picked one yet.
|
||||
Work top-down through the major sections. Each row shows your current
|
||||
pick for that part type, with price and a direct buy link—or a quick
|
||||
way to choose a part if you haven't picked one yet.
|
||||
</p>
|
||||
|
||||
{loading && (
|
||||
@@ -912,7 +817,8 @@ export default function GunbuilderPage() {
|
||||
)}
|
||||
{error && !loading && (
|
||||
<p className="text-sm text-red-400">
|
||||
{error} — check that the Ballistic API is running and CORS is configured.
|
||||
{error} — check that the Ballistic API is running and CORS is
|
||||
configured.
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -967,12 +873,14 @@ export default function GunbuilderPage() {
|
||||
|
||||
<tbody>
|
||||
{groupCategories.map((category) => {
|
||||
const categoryParts = partsByCategory[category.id] ?? [];
|
||||
const categoryParts =
|
||||
partsByCategory[category.id] ?? [];
|
||||
const selectedPartId = build[category.id];
|
||||
|
||||
const selectedPart = selectedPartId
|
||||
? categoryParts.find((p) => p.id === selectedPartId) ??
|
||||
parts.find((p) => p.id === selectedPartId)
|
||||
? categoryParts.find(
|
||||
(p) => p.id === selectedPartId
|
||||
) ?? parts.find((p) => p.id === selectedPartId)
|
||||
: undefined;
|
||||
|
||||
const hasParts = categoryParts.length > 0;
|
||||
@@ -987,10 +895,28 @@ export default function GunbuilderPage() {
|
||||
<div className="font-medium text-zinc-100">
|
||||
{category.name}
|
||||
</div>
|
||||
|
||||
{selectedPart ? (
|
||||
<div className="text-[0.7rem] text-zinc-500 line-clamp-1">
|
||||
{selectedPart.name}
|
||||
</div>
|
||||
<>
|
||||
<div className="text-[0.7rem] text-zinc-500 line-clamp-1">
|
||||
{selectedPart.name}
|
||||
</div>
|
||||
|
||||
{/* ✅ Category overlap chips */}
|
||||
<div className="mt-1 flex flex-wrap gap-1.5">
|
||||
{getOverlapChipsForCategory(
|
||||
category.id as CategoryId,
|
||||
build
|
||||
).map((c) => (
|
||||
<OverlapChip
|
||||
key={c.key}
|
||||
tone={c.tone}
|
||||
label={c.label}
|
||||
detail={c.detail}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : hasParts ? (
|
||||
<div className="text-[0.7rem] text-zinc-600">
|
||||
No part selected
|
||||
@@ -1031,8 +957,10 @@ export default function GunbuilderPage() {
|
||||
<>
|
||||
<Link
|
||||
href={{
|
||||
pathname: `/parts/${category.id}`,
|
||||
query: platform ? { platform } : {},
|
||||
pathname: `/parts/p/${platform}/${category.id}`,
|
||||
query: platform
|
||||
? { platform }
|
||||
: {},
|
||||
}}
|
||||
className="hidden md:inline-flex items-center justify-center rounded-md border border-zinc-700 bg-zinc-900/70 px-2 py-1 hover:bg-zinc-800 hover:border-zinc-600 transition-colors"
|
||||
aria-label="Change part"
|
||||
@@ -1055,8 +983,10 @@ export default function GunbuilderPage() {
|
||||
) : hasParts ? (
|
||||
<Link
|
||||
href={{
|
||||
pathname: `/parts/${category.id}`,
|
||||
query: platform ? { platform } : {},
|
||||
pathname: `/parts/p/${platform}/${category.id}`,
|
||||
query: platform
|
||||
? { platform }
|
||||
: {},
|
||||
}}
|
||||
className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-md border border-zinc-700 bg-zinc-900/70 px-3 py-1.5 text-xs font-medium text-zinc-300 hover:bg-zinc-800 hover:border-zinc-600 transition-colors"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user