fixing platform routing. not done. updated logo on builder

This commit is contained in:
2025-12-10 14:32:09 -05:00
parent 54c30b1d8a
commit b26dcb947e
8 changed files with 255 additions and 265 deletions
@@ -27,7 +27,10 @@ const API_BASE_URL =
export default function PartDetailPage() {
const params = useParams();
const categoryId = params.categoryId as CategoryId;
const partId = params.partId as string; // this is the UUID we passed in the link
// Support URLs like /builder/lower/25969-m5-complete-lower-receiver
const rawPartParam = params.partId as string;
const partId = rawPartParam.split("-")[0]; // "25969-m5-..." -> "25969"
const [product, setProduct] = useState<GunbuilderProductFromApi | null>(null);
const [loading, setLoading] = useState(true);
+54 -9
View File
@@ -2,7 +2,7 @@
import { useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { CATEGORIES } from "@/data/gunbuilderParts";
import type { CategoryId, Part } from "@/types/gunbuilder";
@@ -26,6 +26,8 @@ type UiPart = Part & {
inStock?: boolean;
};
const PLATFORMS = ["AR-15", "AR-10"];
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
@@ -37,10 +39,18 @@ type SortOption = "relevance" | "price-asc" | "price-desc" | "brand-asc";
type BuildState = Partial<Record<CategoryId, string>>;
const STORAGE_KEY = "gunbuilder-build-state";
// support for url id-slug
const slugify = (str: string) =>
str
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
export default function CategoryPage() {
const params = useParams();
const categoryId = params.categoryId as CategoryId;
const router = useRouter();
const searchParams = useSearchParams();
const [viewMode, setViewMode] = useState<ViewMode>("list");
const [parts, setParts] = useState<UiPart[]>([]);
@@ -70,6 +80,10 @@ export default function CategoryPage() {
}
});
const initialPlatform =
(searchParams.get("platform") as string) || "AR-15";
const [platform, setPlatform] = useState<string>(initialPlatform);
const category = useMemo(
() => CATEGORIES.find((c) => c.id === categoryId),
[categoryId],
@@ -86,10 +100,9 @@ export default function CategoryPage() {
setError(null);
const search = new URLSearchParams();
search.set("platform", "AR-15");
search.set("categorySlug", categoryId);
search.set("platform", platform);
const url = `${API_BASE_URL}/api/gunbuilder/products/by-category?${search.toString()}`;
const url = `${API_BASE_URL}/api/products?${search.toString()}`;
const res = await fetch(url, { signal: controller.signal });
@@ -123,7 +136,7 @@ export default function CategoryPage() {
fetchCategoryParts();
return () => controller.abort();
}, [categoryId]);
}, [categoryId, platform]);
// persist build to localStorage whenever it changes
useEffect(() => {
@@ -275,10 +288,16 @@ export default function CategoryPage() {
return filteredParts.slice(startIndex, startIndex + PAGE_SIZE);
}, [filteredParts, currentPage, PAGE_SIZE]);
useEffect(() => {
useEffect(() => {
// whenever the category or filters change, jump back to page 1
setCurrentPage(1);
}, [categoryId, brandFilter, sortBy, searchQuery, priceRange, inStockOnly]);
}, [categoryId, brandFilter, sortBy, searchQuery, priceRange, inStockOnly, platform]);
useEffect(() => {
const nextPlatform =
(searchParams.get("platform") as string) || "AR-15";
setPlatform(nextPlatform);
}, [searchParams]);
const visibleRange = useMemo(() => {
if (filteredParts.length === 0) {
@@ -588,6 +607,26 @@ export default function CategoryPage() {
)}
</div>
</div>
<div className="flex items-center gap-2">
<label
htmlFor="platform-select"
className="text-xs text-zinc-500"
>
Platform
</label>
<select
id="platform-select"
value={platform}
onChange={(e) => setPlatform(e.target.value)}
className="rounded-md border border-zinc-700 bg-zinc-900/70 px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:ring-1 focus:ring-amber-400"
>
{PLATFORMS.map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label
htmlFor="sort-by"
@@ -660,7 +699,10 @@ export default function CategoryPage() {
</div>
<div className="mt-2 flex gap-2">
<Link
href={`/builder/${categoryId}/${part.id}`}
href={{
pathname: `/builder/${categoryId}/${part.id}-${slugify(part.name)}`,
query: { platform },
}}
className="flex-1 rounded-md border border-zinc-700 bg-zinc-800/50 px-3 py-2 text-center text-xs font-medium text-zinc-300 transition-colors hover:border-zinc-600 hover:bg-zinc-700"
>
View Details
@@ -723,7 +765,10 @@ export default function CategoryPage() {
</div>
<div className="flex justify-end gap-2">
<Link
href={`/builder/${categoryId}/${part.id}`}
href={{
pathname: `/builder/${categoryId}/${part.id}-${slugify(part.name)}`,
query: { platform },
}}
className="whitespace-nowrap rounded-md border border-zinc-700 bg-zinc-800/50 px-3 py-1.5 text-xs font-medium text-zinc-300 transition-colors hover:border-zinc-600 hover:bg-zinc-700"
>
View Details
-32
View File
@@ -24,35 +24,3 @@ export default function BuilderLayout({ children }: { children: ReactNode }) {
);
}
//
// Original homepage. will delete after testing
//
// import "./globals.css";
// import type { ReactNode } from "react";
// import { AuthProvider } from "@/context/AuthContext";
// import { TopNav } from "@/components/TopNav";
// import { BuilderNav } from "@/components/BuilderNav";
// export const metadata = {
// title: {
// default: "Battl Builder",
// template: "%s | Battl Builder",
// },
// description: "Battl Builder — the smarter, faster, datadriven way to build your next firearm.",
// };
// export default function RootLayout({ children }: { children: ReactNode }) {
// return (
// <html lang="en">
// <body className="bg-black text-zinc-100">
// <AuthProvider>
// <TopNav />
// <BuilderNav />
// <main className="min-h-screen">{children}</main>
// </AuthProvider>
// </body>
// </html>
// );
// }
+90 -61
View File
@@ -20,6 +20,8 @@ type GunbuilderProductFromApi = {
buyUrl: string | null;
};
const PLATFORMS = ["AR-15", "AR-10", "AR-9"] as const;
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
@@ -85,6 +87,7 @@ export default function GunbuilderPage() {
const [parts, setParts] = useState<Part[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [platform, setPlatform] = useState<(typeof PLATFORMS)[number]>("AR-15");
const [build, setBuild] = useState<BuildState>(() => {
if (typeof window !== "undefined") {
const stored = localStorage.getItem(STORAGE_KEY);
@@ -110,8 +113,10 @@ export default function GunbuilderPage() {
setError(null);
const res = await fetch(
`${API_BASE_URL}/api/products/gunbuilder?platform=AR-15`,
{ signal: controller.signal },
`${API_BASE_URL}/api/products?platform=${encodeURIComponent(
platform
)}`,
{ signal: controller.signal }
);
if (!res.ok) {
@@ -121,29 +126,28 @@ export default function GunbuilderPage() {
const data: GunbuilderProductFromApi[] = await res.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;
}
.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;
}
const buyUrl = p.buyUrl ?? undefined;
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);
const buyUrl = p.buyUrl ?? undefined;
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);
setParts(normalized);
} catch (err: any) {
@@ -157,7 +161,16 @@ export default function GunbuilderPage() {
fetchProducts();
return () => controller.abort();
}, []);
}, [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);
}
}, [platform]);
// Handle URL query parameter for part selection (?select=upper:165)
useEffect(() => {
@@ -191,9 +204,7 @@ export default function GunbuilderPage() {
const partsByCategory: Record<CategoryId, Part[]> = useMemo(() => {
const grouped = {} as Record<CategoryId, Part[]>;
for (const category of CATEGORIES) {
grouped[category.id] = parts.filter(
(p) => p.categoryId === category.id,
);
grouped[category.id] = parts.filter((p) => p.categoryId === category.id);
}
return grouped;
}, [parts]);
@@ -201,7 +212,7 @@ export default function GunbuilderPage() {
const selectedParts: Part[] = useMemo(() => {
return Object.entries(build)
.map(([categoryId, partId]) =>
parts.find((p) => p.id === partId && p.categoryId === categoryId),
parts.find((p) => p.id === partId && p.categoryId === categoryId)
)
.filter(Boolean) as Part[];
}, [build, parts]);
@@ -212,14 +223,14 @@ export default function GunbuilderPage() {
Object.entries(build).map(([categoryId, partId]) => [
categoryId as CategoryId,
partId ? true : false,
]),
])
) as Record<CategoryId, unknown>,
[build],
[build]
);
const totalPrice = useMemo(
() => selectedParts.reduce((sum, p) => sum + p.price, 0),
[selectedParts],
[selectedParts]
);
useEffect(() => {
@@ -236,7 +247,7 @@ export default function GunbuilderPage() {
const encoded = window.btoa(payload);
const origin = window.location?.origin ?? "";
const url = `${origin}/builder/build?build=${encodeURIComponent(
encoded,
encoded
)}`;
setShareUrl(url);
} catch {
@@ -254,10 +265,10 @@ export default function GunbuilderPage() {
// Use group ordering for the summary as well
const summaryCategoryOrder: CategoryId[] = useMemo(
() =>
CATEGORY_GROUPS.flatMap((group) => group.categoryIds).filter(
(id) => CATEGORIES.some((c) => c.id === id),
CATEGORY_GROUPS.flatMap((group) => group.categoryIds).filter((id) =>
CATEGORIES.some((c) => c.id === id)
),
[],
[]
);
const handleShare = async () => {
@@ -278,14 +289,14 @@ export default function GunbuilderPage() {
const selectedPartId = build[cat.id];
const part = parts.find(
(p) => p.id === selectedPartId && p.categoryId === cat.id,
(p) => p.id === selectedPartId && p.categoryId === cat.id
);
if (part) {
lines.push(
`${cat.name}: ${part.brand}${part.name} ($${part.price.toFixed(
2,
)})`,
2
)})`
);
} else {
lines.push(`${cat.name}: [Not selected]`);
@@ -299,7 +310,7 @@ export default function GunbuilderPage() {
setShareStatus("Build summary copied to clipboard.");
} catch {
setShareStatus(
"Could not access clipboard. You can copy from the build summary page.",
"Could not access clipboard. You can copy from the build summary page."
);
}
};
@@ -334,7 +345,9 @@ export default function GunbuilderPage() {
setShareStatus("Share dialog opened.");
} catch {
// User canceled or share failed; keep it quiet but let them know
setShareStatus("Share canceled or unavailable. You can copy the link instead.");
setShareStatus(
"Share canceled or unavailable. You can copy the link instead."
);
}
} else {
// Fallback to copying the link
@@ -344,7 +357,6 @@ export default function GunbuilderPage() {
return (
<main className="min-h-screen bg-black text-zinc-50">
<div className="mx-auto max-w-6xl px-4 py-6 lg:py-10">
{/* Header */}
<header className="mb-6">
@@ -361,10 +373,31 @@ export default function GunbuilderPage() {
update as you go. This early-access builder keeps your setup saved
locally so you can come back and refine it anytime.
</p>
<div className="mt-4 flex flex-wrap items-center gap-3">
<label className="text-xs font-medium text-zinc-400 flex items-center gap-2">
Platform
<select
value={platform}
onChange={(e) =>
setPlatform(e.target.value as (typeof PLATFORMS)[number])
}
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"
>
{PLATFORMS.map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</select>
</label>
<p className="text-[0.7rem] text-zinc-500">
Parts list updates automatically when you change platforms.
</p>
</div>
</div>
</header>
{/* Build summary panel */}
<section className="mb-6 rounded-lg border border-zinc-800 bg-zinc-950/80 p-3 md:p-4 flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
{/* Left: title + totals + primary actions */}
@@ -416,7 +449,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
@@ -432,8 +467,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">
@@ -468,9 +503,7 @@ export default function GunbuilderPage() {
)}
{shareStatus && (
<p className="mt-1 text-[0.7rem] text-zinc-500">
{shareStatus}
</p>
<p className="mt-1 text-[0.7rem] text-zinc-500">{shareStatus}</p>
)}
</div>
</section>
@@ -511,15 +544,12 @@ export default function GunbuilderPage() {
<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
linkor a quick way to choose a part if you haven&apos;t picked
one yet.
current pick for that part type, with price and a direct buy linkor
a quick way to choose a part if you haven&apos;t picked one yet.
</p>
{loading && (
<p className="text-sm text-zinc-500">
Loading parts from backend
</p>
<p className="text-sm text-zinc-500">Loading parts from backend</p>
)}
{error && !loading && (
<p className="text-sm text-red-400">
@@ -532,7 +562,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 CategoryId)
);
if (groupCategories.length === 0) {
@@ -584,7 +614,7 @@ export default function GunbuilderPage() {
partsByCategory[category.id] ?? [];
const selectedPartId = build[category.id];
const selectedPart = categoryParts.find(
(p) => p.id === selectedPartId,
(p) => p.id === selectedPartId
);
const hasParts = categoryParts.length > 0;
@@ -663,10 +693,9 @@ export default function GunbuilderPage() {
</>
) : hasParts ? (
<Link
href={`/builder/${category.id}`}
className="inline-flex rounded-md border border-zinc-700 bg-zinc-900/70 px-2.5 py-1 text-[0.7rem] font-medium text-zinc-200 hover:bg-zinc-800 hover:border-zinc-600 transition-colors"
onClick={() => {
// If you want to pre-select something later, you can handle here.
href={{
pathname: `/builder/${category.id}`,
query: { platform },
}}
>
Choose a Part
@@ -729,4 +758,4 @@ export default function GunbuilderPage() {
</div>
</main>
);
}
}
-1
View File
@@ -96,7 +96,6 @@ export default function HomePage() {
{/* Top Bar / Logo */}
<header className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
{/* Logo slot replace src with your actual logo file */}
<div className="flex items-center">
<Image
src="/battl/battl-logo-mark-2.svg"
+10 -1
View File
@@ -2,6 +2,8 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { useAuth } from "@/context/AuthContext";
export function TopNav() {
@@ -19,7 +21,14 @@ export function TopNav() {
href="/"
className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-400"
>
Battl Builder
<Image
src="/battl/battl-logo-mark-2.svg"
alt="Battl Builders Logo"
width={260} // adjust to taste
height={48} // adjust to taste
className="block"
/>
</Link>
{/* Right side actions */}
+65 -160
View File
@@ -1,163 +1,68 @@
import type { Category, Part } from "@/types/gunbuilder";
import type { Category } from "@/types/gunbuilder";
export const CATEGORY_SLUGS = {
lower: "lower",
"complete-lower": "complete-lower",
"lower-parts": "lower-parts",
trigger: "trigger",
grip: "grip",
safety: "safety",
buffer: "buffer",
stock: "stock",
upper: "upper",
"complete-upper": "complete-upper",
barrel: "barrel",
handguard: "handguard",
"gas-block": "gas-block",
"gas-tube": "gas-tube",
"muzzle-device": "muzzle-device",
bcg: "bcg",
sights: "sights",
"charging-handle": "charging-handle",
suppressor: "suppressor",
optic: "optic",
magazine: "magazine",
"weapon-light": "weapon-light",
foregrip: "foregrip",
bipod: "bipod",
sling: "sling",
"rail-accessory": "rail-accessory",
tools: "tools",
} as const;
export const CATEGORIES: Category[] = [
// LOWER
{
id: "lower",
name: "Stripped Lowers",
description: "Serialized lower the foundation of the build.",
group: "lower",
},
{
id: "completeLower",
name: "Complete Lowers",
description: "Factory-built lower with stock, buffer system, and controls.",
group: "lower",
},
{
id: "lowerParts",
name: "Lower Parts Kits",
description: "Pins, springs, and controls for the lower.",
group: "lower",
},
{
id: "trigger",
name: "Trigger / Fire Control",
description: "Single-stage, two-stage, and match triggers.",
group: "lower",
},
{
id: "grip",
name: "Pistol Grip",
description: "Grips that shape how the rifle handles.",
group: "lower",
},
{
id: "safety",
name: "Safety / Selector",
description: "Ambi selectors and control upgrades.",
group: "lower",
},
{
id: "buffer",
name: "Buffer System",
description: "Buffer tube, buffer, spring, and related parts.",
group: "lower",
},
{
id: "stock",
name: "Stock / Brace",
description: "Adjustable, fixed, and minimalist stocks.",
group: "lower",
},
// 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: "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 },
{ id: "buffer", name: "Buffer System", group: "lower", slug: CATEGORY_SLUGS.buffer },
{ id: "stock", name: "Stock / Brace", group: "lower", slug: CATEGORY_SLUGS.stock },
// UPPER
{
id: "upper",
name: "Stripped Uppers",
description: "The core of the top half of your build.",
group: "upper",
},
{
id: "completeUpper",
name: "Complete Uppers",
description: "Pre-assembled uppers ready to pin and shoot.",
group: "upper",
},
{
id: "bcg",
name: "Bolt Carriers",
description: "The heartbeat of the rifles cycling.",
group: "upper",
},
{
id: "barrel",
name: "Barrels",
description: "Different lengths, profiles, and calibers.",
group: "upper",
},
{
id: "gasBlock",
name: "Gas Blocks",
description: "Standard and adjustable gas blocks.",
group: "upper",
},
{
id: "gasTube",
name: "Gas Tubes",
description: "Carbine, mid, rifle, and more.",
group: "upper",
},
{
id: "muzzleDevice",
name: "Muzzle Devices",
description: "Brakes, comps, and flash hiders.",
group: "upper",
},
{
id: "suppressor",
name: "Suppressors",
description: "Hearing-safe setups and hosts.",
group: "upper",
},
{
id: "handguard",
name: "Handguards / Rails",
description: "M-LOK rails and front-end furniture.",
group: "upper",
},
{
id: "chargingHandle",
name: "Charging Handles",
description: "Standard and ambi charging handles.",
group: "upper",
},
{
id: "sights",
name: "Iron Sights",
description: "Backup and primary irons.",
group: "upper",
},
{
id: "optic",
name: "Optics",
description: "LPVOs, red dots, and magnifiers.",
group: "upper",
},
// ACCESSORIES GROUP
{
id: "magazine",
name: "Magazines",
group: "accessories",
},
{
id: "weaponLight",
name: "Weapon Lights & Lasers",
group: "accessories",
},
{
id: "foregrip",
name: "Vertical / Angled Grips",
group: "accessories",
},
{
id: "bipod",
name: "Bipods",
group: "accessories",
},
{
id: "sling",
name: "Slings & Mounts",
group: "accessories",
},
{
id: "railAccessory",
name: "Rail Sections & Covers",
group: "accessories",
},
{
id: "tools",
name: "Tools & Maintenance",
group: "accessories",
},
];
// 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: "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: "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: "suppressor", name: "Suppressors", group: "upper", slug: CATEGORY_SLUGS.suppressor },
{ id: "optic", name: "Optics", group: "upper", slug: CATEGORY_SLUGS.optic },
// Accessories
{ 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: "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: "tools", name: "Tools & Maintenance", group: "accessories", slug: CATEGORY_SLUGS.tools },
];
+32
View File
@@ -50,3 +50,35 @@ export interface Part {
imageUrl?: string;
notes?: string;
}
export const CATEGORY_SLUGS: Record<CategoryId, 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",
sights: "sights",
bcg: "bcg",
chargingHandle: "charging-handle",
suppressor: "suppressor",
optic: "optic",
magazine: "magazine",
weaponLight: "weapon-light",
foregrip: "foregrip",
bipod: "bipod",
sling: "sling",
railAccessory: "rail-accessory",
tools: "tools",
};