fixing role ,apping. added accessories to ui

This commit is contained in:
2025-12-13 06:02:05 -05:00
parent 3ddb48fe58
commit 978b9df96c
4 changed files with 243 additions and 194 deletions
+123 -58
View File
@@ -29,6 +29,20 @@ 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)
const UNIVERSAL_CATEGORIES = new Set<CategoryId>([
"magazine",
"weapon-light",
"foregrip",
"bipod",
"sling",
"rail-accessory",
"tools",
// Optics & sights are intentionally treated as universal
"optic",
"sights",
]);
// Category groups for the main grid
const CATEGORY_GROUPS: {
id: string;
@@ -43,8 +57,8 @@ const CATEGORY_GROUPS: {
"Everything from the serialized lower to small parts, fire control, and core controls.",
categoryIds: [
"lower",
"completeLower",
"lowerParts",
"complete-lower",
"lower-parts",
"trigger",
"grip",
"safety",
@@ -59,26 +73,34 @@ const CATEGORY_GROUPS: {
"Barrel, upper, gas system, and the parts that keep the rifle cycling.",
categoryIds: [
"upper",
"completeUpper",
"complete-upper",
"bcg",
"barrel",
"gasBlock",
"gasTube",
"muzzleDevice",
"gas-block",
"gas-tube",
"muzzle-device",
"suppressor",
"handguard",
"chargingHandle",
"charging-handle",
"sights",
"optic",
],
},
// Optional: future group for accessories/furniture if needed
// {
// id: "accessories-group",
// label: "Accessories & Furniture",
// description: "Optics, lights, slings, and other supporting gear.",
// categoryIds: ["optic", "sights", ...] as CategoryId[],
// },
{
id: "accessories-group",
label: "Accessories & Gear",
description:
"Universal add-ons that work across platforms — mags, lights, slings, tools, and more.",
categoryIds: [
"magazine",
"weapon-light",
"foregrip",
"bipod",
"sling",
"rail-accessory",
"tools",
],
},
];
export default function GunbuilderPage() {
@@ -121,47 +143,78 @@ export default function GunbuilderPage() {
setLoading(true);
setError(null);
const res = await fetch(
`${API_BASE_URL}/api/products?platform=${encodeURIComponent(
platform
)}`,
{ signal: controller.signal }
);
// 1) Platform-scoped products
const scopedUrl = `${API_BASE_URL}/api/products?platform=${encodeURIComponent(
platform
)}`;
if (!res.ok) {
throw new Error(`Failed to load products (${res.status})`);
// 2) Universal products (no platform filter)
// NOTE: This assumes your backend supports /api/products with no platform param.
// If it doesn't yet, this call will fail quietly and you'll still see scoped results.
const universalUrl = `${API_BASE_URL}/api/products`;
const [scopedRes, universalRes] = await Promise.all([
fetch(scopedUrl, { signal: controller.signal }),
fetch(universalUrl, { signal: controller.signal }).catch(() => null as any),
]);
if (!scopedRes || !scopedRes.ok) {
const status = scopedRes?.status ?? "";
throw new Error(`Failed to load products (${status})`);
}
const data: GunbuilderProductFromApi[] = await res.json();
const scopedData: GunbuilderProductFromApi[] = await scopedRes.json();
const normalized: Part[] = data
.map((p): Part | null => {
const categoryId = PART_ROLE_TO_CATEGORY[p.partRole];
if (!categoryId) {
// Skip any parts we don't know how to map yet
return null;
}
let universalData: GunbuilderProductFromApi[] = [];
if (universalRes && universalRes.ok) {
universalData = await universalRes.json();
}
const buyUrl = p.buyUrl ?? undefined;
// Normalize both lists
const normalize = (data: GunbuilderProductFromApi[]): Part[] =>
data
.map((p): Part | null => {
const categoryId = PART_ROLE_TO_CATEGORY[p.partRole];
if (!categoryId) return null;
return {
id: String(p.id), // ALWAYS string
categoryId,
name: p.name,
brand: p.brand,
price: p.price ?? 0,
imageUrl: p.mainImageUrl ?? undefined,
affiliateUrl: buyUrl, // main link
url: buyUrl, // 👈optional alias if you still reference .url. maybe pretty url?
notes: undefined,
};
})
.filter((p): p is Part => p !== 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)) {
return null;
}
setParts(normalized);
const buyUrl = p.buyUrl ?? undefined;
return {
id: String(p.id),
categoryId,
name: p.name,
brand: p.brand,
price: p.price ?? 0,
imageUrl: p.mainImageUrl ?? undefined,
affiliateUrl: buyUrl,
url: buyUrl,
notes: undefined,
};
})
.filter((p): p is Part => p !== null);
const scopedParts = normalize(scopedData);
const universalParts = normalize(universalData);
// Merge + de-dupe by (categoryId + id)
const seen = new Set<string>();
const merged: Part[] = [];
for (const p of [...scopedParts, ...universalParts]) {
const key = `${p.categoryId}:${p.id}`;
if (seen.has(key)) continue;
seen.add(key);
merged.push(p);
}
setParts(merged);
} catch (err: any) {
if (err.name === "AbortError") return;
setError(err.message ?? "Failed to load products");
if (err?.name === "AbortError") return;
setError(err?.message ?? "Failed to load products");
} finally {
setLoading(false);
}
@@ -173,12 +226,18 @@ export default function GunbuilderPage() {
}, [platform]);
useEffect(() => {
// When the platform changes, reset the current build so we don't
// show stale part selections from a different platform.
setBuild({});
if (typeof window !== "undefined") {
localStorage.removeItem(STORAGE_KEY);
}
// When the platform changes, clear ONLY platform-scoped selections.
// Keep universal accessories (optic/light/sling/etc) so the user doesn't lose them.
setBuild((prev) => {
const next: BuildState = {};
for (const [categoryId, partId] of Object.entries(prev)) {
const cid = categoryId as CategoryId;
if (UNIVERSAL_CATEGORIES.has(cid)) {
next[cid] = partId;
}
}
return next;
});
}, [platform]);
// Handle URL query parameter for part selection (?select=upper:165)
@@ -280,10 +339,16 @@ export default function GunbuilderPage() {
}, [build]);
const handleSelectPart = (categoryId: CategoryId, partId: string) => {
setBuild((prev) => ({
...prev,
[categoryId]: partId,
}));
setBuild((prev) => {
const updated = {
...prev,
[categoryId]: partId,
};
if (typeof window !== "undefined") {
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
}
return updated;
});
};
// Use group ordering for the summary as well
@@ -302,7 +367,7 @@ export default function GunbuilderPage() {
}
const lines: string[] = [];
lines.push("Shadow Standard — The Armory Build");
lines.push("B Build");
lines.push("");
lines.push(`Total: $${totalPrice.toFixed(2)}`);
lines.push("");