fixing role ,apping. added accessories to ui
This commit is contained in:
+123
-58
@@ -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("");
|
||||
|
||||
+21
-21
@@ -1,9 +1,9 @@
|
||||
import type { Category } from "@/types/gunbuilder";
|
||||
|
||||
export const CATEGORY_SLUGS = {
|
||||
export const CATEGORY_SLUGS: Record<string, string> = {
|
||||
lower: "lower",
|
||||
"complete-lower": "complete-lower",
|
||||
"lower-parts": "lower-parts",
|
||||
completeLower: "complete-lower",
|
||||
lowerParts: "lower-parts",
|
||||
trigger: "trigger",
|
||||
grip: "grip",
|
||||
safety: "safety",
|
||||
@@ -11,32 +11,32 @@ export const CATEGORY_SLUGS = {
|
||||
stock: "stock",
|
||||
|
||||
upper: "upper",
|
||||
"complete-upper": "complete-upper",
|
||||
completeUpper: "complete-upper",
|
||||
barrel: "barrel",
|
||||
handguard: "handguard",
|
||||
"gas-block": "gas-block",
|
||||
"gas-tube": "gas-tube",
|
||||
"muzzle-device": "muzzle-device",
|
||||
gasBlock: "gas-block",
|
||||
gasTube: "gas-tube",
|
||||
muzzleDevice: "muzzle-device",
|
||||
bcg: "bcg",
|
||||
sights: "sights",
|
||||
"charging-handle": "charging-handle",
|
||||
chargingHandle: "charging-handle",
|
||||
suppressor: "suppressor",
|
||||
optic: "optic",
|
||||
|
||||
magazine: "magazine",
|
||||
"weapon-light": "weapon-light",
|
||||
weaponLight: "weapon-light",
|
||||
foregrip: "foregrip",
|
||||
bipod: "bipod",
|
||||
sling: "sling",
|
||||
"rail-accessory": "rail-accessory",
|
||||
railAccessory: "rail-accessory",
|
||||
tools: "tools",
|
||||
} as const;
|
||||
};
|
||||
|
||||
export const CATEGORIES: Category[] = [
|
||||
// Lower group
|
||||
{ id: "lower", name: "Stripped Lowers", group: "lower", slug: CATEGORY_SLUGS.lower },
|
||||
{ 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: "completeLower", name: "Complete Lower", group: "lower", slug: CATEGORY_SLUGS.completeLower },
|
||||
{ id: "lowerParts", name: "Lower Parts Kit", group: "lower", slug: CATEGORY_SLUGS.lowerParts },
|
||||
{ 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 +45,24 @@ export const CATEGORIES: Category[] = [
|
||||
|
||||
// Upper group
|
||||
{ id: "upper", name: "Stripped Upper", group: "upper", slug: CATEGORY_SLUGS.upper },
|
||||
{ id: "complete-upper", name: "Complete Upper", group: "upper", slug: CATEGORY_SLUGS["complete-upper"] },
|
||||
{ id: "completeUpper", name: "Complete Upper", group: "upper", slug: CATEGORY_SLUGS.completeUpper },
|
||||
{ id: "barrel", name: "Barrels", group: "upper", slug: CATEGORY_SLUGS.barrel },
|
||||
{ id: "handguard", name: "Handguards / Rails", group: "upper", slug: CATEGORY_SLUGS.handguard },
|
||||
{ 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: "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: "sights", name: "Iron Sights", group: "upper", slug: CATEGORY_SLUGS.sights },
|
||||
{ id: "bcg", name: "Bolt Carrier Group", group: "upper", slug: CATEGORY_SLUGS.bcg },
|
||||
{ id: "charging-handle", name: "Charging Handle", group: "upper", slug: CATEGORY_SLUGS["charging-handle"] },
|
||||
{ id: "chargingHandle", name: "Charging Handle", group: "upper", slug: CATEGORY_SLUGS.chargingHandle },
|
||||
{ id: "suppressor", name: "Suppressors", group: "upper", slug: CATEGORY_SLUGS.suppressor },
|
||||
{ id: "optic", name: "Optics", group: "upper", slug: CATEGORY_SLUGS.optic },
|
||||
|
||||
// Accessories
|
||||
// Accessories (platform-agnostic)
|
||||
{ id: "magazine", name: "Magazines", group: "accessories", slug: CATEGORY_SLUGS.magazine },
|
||||
{ id: "weapon-light", name: "Weapon Lights & Lasers", group: "accessories", slug: CATEGORY_SLUGS["weapon-light"] },
|
||||
{ id: "weaponLight", name: "Weapon Lights & Lasers", group: "accessories", slug: CATEGORY_SLUGS.weaponLight },
|
||||
{ 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: "rail-accessory", name: "Rail Accessories", group: "accessories", slug: CATEGORY_SLUGS["rail-accessory"] },
|
||||
{ id: "railAccessory", name: "Rail Accessories", group: "accessories", slug: CATEGORY_SLUGS.railAccessory },
|
||||
{ id: "tools", name: "Tools & Maintenance", group: "accessories", slug: CATEGORY_SLUGS.tools },
|
||||
];
|
||||
+24
-85
@@ -1,102 +1,41 @@
|
||||
import type { CategoryId } from "@/types/gunbuilder";
|
||||
|
||||
// CategoryId -> partRoles
|
||||
export const CATEGORY_TO_PART_ROLES: Record<CategoryId, string[]> = {
|
||||
// CORE / EXISTING
|
||||
upper: ["upper-receiver"],
|
||||
barrel: ["barrel"],
|
||||
handguard: ["handguard"],
|
||||
chargingHandle: ["charging-handle"],
|
||||
buffer: ["buffer-kit"],
|
||||
lowerParts: ["lower-parts-kit"],
|
||||
sights: ["sight"],
|
||||
lower: ["LOWER_RECEIVER_STRIPPED"],
|
||||
optic: ["optic"],
|
||||
stock: ["stock"],
|
||||
|
||||
// NEW LOWER PARTS
|
||||
trigger: ["trigger", "trigger-kit"],
|
||||
grip: ["pistol-grip", "grip"],
|
||||
safety: ["safety", "safety-selector"],
|
||||
completeLower: ["complete-lower"],
|
||||
|
||||
// NEW UPPER PARTS
|
||||
upper: ["upper-receiver", "upper"],
|
||||
completeUpper: ["complete-upper"],
|
||||
bcg: ["bcg", "bolt-carrier-group"],
|
||||
barrel: ["barrel"],
|
||||
gasBlock: ["gas-block"],
|
||||
gasTube: ["gas-tube"],
|
||||
muzzleDevice: ["muzzle-device", "compensator", "brake"],
|
||||
suppressor: ["suppressor"],
|
||||
handguard: ["handguard"],
|
||||
chargingHandle: ["charging-handle"],
|
||||
bcg: ["bcg", "bolt-carrier-group"],
|
||||
|
||||
// ===== ACCESSORIES =====
|
||||
lower: ["lower-receiver", "lower", "LOWER_RECEIVER_STRIPPED"],
|
||||
completeLower: ["complete-lower"],
|
||||
lowerParts: ["lower-parts-kit", "lower-parts"],
|
||||
trigger: ["trigger", "trigger-kit"],
|
||||
grip: ["pistol-grip", "grip"],
|
||||
safety: ["safety", "safety-selector"],
|
||||
buffer: ["buffer-kit", "buffer"],
|
||||
stock: ["stock"],
|
||||
|
||||
// Magazines
|
||||
magazine: [
|
||||
"magazine",
|
||||
"mag",
|
||||
"magazine-ar15",
|
||||
"magazine-308",
|
||||
"drum-magazine",
|
||||
],
|
||||
sights: ["sight", "sights", "iron-sights"],
|
||||
optic: ["optic", "optics"],
|
||||
|
||||
// Lights / Lasers
|
||||
weaponLight: [
|
||||
"weapon-light",
|
||||
"light",
|
||||
"weapon-light-laser",
|
||||
"light-laser-combo",
|
||||
"laser",
|
||||
],
|
||||
|
||||
// Foregrips
|
||||
foregrip: [
|
||||
"vertical-grip",
|
||||
"angled-foregrip",
|
||||
"foregrip",
|
||||
"handstop",
|
||||
],
|
||||
|
||||
// Bipods
|
||||
bipod: [
|
||||
"bipod",
|
||||
],
|
||||
|
||||
// Slings & mounts
|
||||
sling: [
|
||||
"sling",
|
||||
"sling-mount",
|
||||
"sling-swivel",
|
||||
"qd-sling-mount",
|
||||
],
|
||||
|
||||
// Rail sections & covers
|
||||
railAccessory: [
|
||||
"rail-section",
|
||||
"picatinny-rail-section",
|
||||
"m-lok-rail-section",
|
||||
"keymod-rail-section",
|
||||
"rail-cover",
|
||||
"rail-panel",
|
||||
],
|
||||
|
||||
// Tools & maintenance
|
||||
tools: [
|
||||
"tool",
|
||||
"armorer-tool",
|
||||
"armorer-wrench",
|
||||
"cleaning-kit",
|
||||
"bore-snake",
|
||||
"vise-block",
|
||||
"torque-wrench",
|
||||
],
|
||||
magazine: ["magazine", "mag", "magazine-ar15", "magazine-308", "drum-magazine"],
|
||||
weaponLight: ["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"],
|
||||
tools: ["tool", "armorer-tool", "armorer-wrench", "cleaning-kit", "bore-snake", "vise-block", "torque-wrench"],
|
||||
};
|
||||
|
||||
// Invert to get partRole -> CategoryId
|
||||
export const PART_ROLE_TO_CATEGORY: Record<string, CategoryId> = Object.entries(
|
||||
CATEGORY_TO_PART_ROLES,
|
||||
CATEGORY_TO_PART_ROLES
|
||||
).reduce((acc, [categoryId, roles]) => {
|
||||
for (const role of roles) {
|
||||
acc[role] = categoryId as CategoryId;
|
||||
}
|
||||
for (const role of roles) acc[role] = categoryId as CategoryId;
|
||||
return acc;
|
||||
}, {} as Record<string, CategoryId>);
|
||||
+75
-30
@@ -1,35 +1,58 @@
|
||||
// Grouping for nav + layout
|
||||
export type CategoryGroup = "lower" | "upper" | "accessories";
|
||||
|
||||
/**
|
||||
* CategoryId is the canonical identifier used throughout the UI + localStorage.
|
||||
*
|
||||
* ✅ Canonical IDs are kebab-case to match route segments and `gunbuilderParts.ts`.
|
||||
*
|
||||
* 🧯 Legacy camelCase IDs are temporarily supported to avoid breaking older
|
||||
* localStorage/build-state and older code paths. Prefer kebab-case going forward.
|
||||
*/
|
||||
export type CategoryId =
|
||||
| "upper"
|
||||
| "completeUpper"
|
||||
| "barrel"
|
||||
| "gasBlock"
|
||||
| "gasTube"
|
||||
| "muzzleDevice"
|
||||
| "suppressor"
|
||||
| "handguard"
|
||||
| "chargingHandle"
|
||||
| "bcg"
|
||||
| "buffer"
|
||||
// ===== LOWER =====
|
||||
| "lower"
|
||||
| "completeLower"
|
||||
| "lowerParts"
|
||||
| "complete-lower"
|
||||
| "lower-parts"
|
||||
| "trigger"
|
||||
| "grip"
|
||||
| "safety"
|
||||
| "buffer"
|
||||
| "stock"
|
||||
|
||||
// ===== UPPER =====
|
||||
| "upper"
|
||||
| "complete-upper"
|
||||
| "barrel"
|
||||
| "handguard"
|
||||
| "gas-block"
|
||||
| "gas-tube"
|
||||
| "muzzle-device"
|
||||
| "bcg"
|
||||
| "charging-handle"
|
||||
| "suppressor"
|
||||
| "sights"
|
||||
| "optic"
|
||||
| "stock"
|
||||
// NEW ACCESSORY CATEGORIES
|
||||
|
||||
// ===== ACCESSORIES =====
|
||||
| "magazine"
|
||||
| "weaponLight"
|
||||
| "weapon-light"
|
||||
| "foregrip"
|
||||
| "bipod"
|
||||
| "sling"
|
||||
| "railAccessory"
|
||||
| "tools";
|
||||
| "rail-accessory"
|
||||
| "tools"
|
||||
|
||||
// ===== LEGACY (temporary) =====
|
||||
| "completeUpper"
|
||||
| "gasBlock"
|
||||
| "gasTube"
|
||||
| "muzzleDevice"
|
||||
| "chargingHandle"
|
||||
| "completeLower"
|
||||
| "lowerParts"
|
||||
| "weaponLight"
|
||||
| "railAccessory";
|
||||
|
||||
export interface Category {
|
||||
id: CategoryId;
|
||||
@@ -37,6 +60,8 @@ export interface Category {
|
||||
description?: string;
|
||||
/** used for nav & grouping */
|
||||
group?: CategoryGroup;
|
||||
/** optional slug override (mostly for legacy IDs) */
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
export interface Part {
|
||||
@@ -46,39 +71,59 @@ export interface Part {
|
||||
categoryId: CategoryId;
|
||||
price: number;
|
||||
affiliateUrl?: string;
|
||||
url?: string;
|
||||
url?: string;
|
||||
imageUrl?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export const CATEGORY_SLUGS: Record<CategoryId, string> = {
|
||||
/**
|
||||
* Route / UI slug for each CategoryId.
|
||||
*
|
||||
* - Canonical kebab-case ids map 1:1.
|
||||
* - Legacy camelCase ids map to the same slug.
|
||||
*/
|
||||
export const CATEGORY_SLUGS = {
|
||||
// LOWER
|
||||
lower: "lower",
|
||||
completeLower: "complete-lower",
|
||||
lowerParts: "lower-parts",
|
||||
"complete-lower": "complete-lower",
|
||||
"lower-parts": "lower-parts",
|
||||
trigger: "trigger",
|
||||
grip: "grip",
|
||||
safety: "safety",
|
||||
buffer: "buffer",
|
||||
stock: "stock",
|
||||
|
||||
// UPPER
|
||||
upper: "upper",
|
||||
completeUpper: "complete-upper",
|
||||
"complete-upper": "complete-upper",
|
||||
barrel: "barrel",
|
||||
handguard: "handguard",
|
||||
gasBlock: "gas-block",
|
||||
gasTube: "gas-tube",
|
||||
muzzleDevice: "muzzle-device",
|
||||
"gas-block": "gas-block",
|
||||
"gas-tube": "gas-tube",
|
||||
"muzzle-device": "muzzle-device",
|
||||
sights: "sights",
|
||||
bcg: "bcg",
|
||||
chargingHandle: "charging-handle",
|
||||
"charging-handle": "charging-handle",
|
||||
suppressor: "suppressor",
|
||||
optic: "optic",
|
||||
|
||||
// ACCESSORIES
|
||||
magazine: "magazine",
|
||||
weaponLight: "weapon-light",
|
||||
"weapon-light": "weapon-light",
|
||||
foregrip: "foregrip",
|
||||
bipod: "bipod",
|
||||
sling: "sling",
|
||||
railAccessory: "rail-accessory",
|
||||
"rail-accessory": "rail-accessory",
|
||||
tools: "tools",
|
||||
};
|
||||
|
||||
// LEGACY (temporary)
|
||||
completeUpper: "complete-upper",
|
||||
gasBlock: "gas-block",
|
||||
gasTube: "gas-tube",
|
||||
muzzleDevice: "muzzle-device",
|
||||
chargingHandle: "charging-handle",
|
||||
completeLower: "complete-lower",
|
||||
lowerParts: "lower-parts",
|
||||
weaponLight: "weapon-light",
|
||||
railAccessory: "rail-accessory",
|
||||
} as const satisfies Record<CategoryId, string>;
|
||||
Reference in New Issue
Block a user