deleteing .next
This commit is contained in:
+23
-34
@@ -119,25 +119,29 @@ export default function GunbuilderPage() {
|
|||||||
const data: GunbuilderProductFromApi[] = await res.json();
|
const data: GunbuilderProductFromApi[] = await res.json();
|
||||||
|
|
||||||
const normalized: Part[] = data
|
const normalized: Part[] = data
|
||||||
.map((p): Part | null => {
|
.map((p): Part | null => {
|
||||||
const categoryId = PART_ROLE_TO_CATEGORY[p.partRole];
|
const categoryId = PART_ROLE_TO_CATEGORY[p.partRole];
|
||||||
if (!categoryId) {
|
if (!categoryId) {
|
||||||
// Skip any parts we don't know how to map yet
|
// Skip any parts we don't know how to map yet
|
||||||
return null;
|
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);
|
||||||
|
|
||||||
return {
|
|
||||||
id: String(p.id), // ALWAYS string
|
|
||||||
categoryId,
|
|
||||||
name: p.name,
|
|
||||||
brand: p.brand,
|
|
||||||
price: p.price ?? 0,
|
|
||||||
imageUrl: p.mainImageUrl ?? undefined,
|
|
||||||
url: p.buyUrl ?? undefined,
|
|
||||||
notes: undefined,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((p): p is Part => p !== null);
|
|
||||||
|
|
||||||
setParts(normalized);
|
setParts(normalized);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -327,22 +331,7 @@ export default function GunbuilderPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-black text-zinc-50">
|
<main className="min-h-screen bg-black text-zinc-50">
|
||||||
<div className="border-b border-amber-500/20 bg-amber-500/5">
|
|
||||||
<div className="mx-auto flex max-w-6xl flex-col gap-1 px-4 py-2 text-[0.75rem] text-amber-100 md:flex-row md:items-center md:justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="rounded-sm bg-amber-500/20 px-2 py-0.5 text-[0.65rem] font-semibold uppercase tracking-[0.18em] text-amber-300">
|
|
||||||
Early Access Beta
|
|
||||||
</span>
|
|
||||||
<span className="hidden text-amber-100/90 md:inline">
|
|
||||||
You're using an early-access prototype of The Armory. Data,
|
|
||||||
pricing, and available parts are still evolving.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-amber-100/90 md:hidden">
|
|
||||||
Early-access prototype. Data and pricing may change.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto max-w-6xl px-4 py-6 lg:py-10">
|
<div className="mx-auto max-w-6xl px-4 py-6 lg:py-10">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="mb-6">
|
<header className="mb-6">
|
||||||
|
|||||||
+9
-14
@@ -1,25 +1,20 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { Inter } from "next/font/google";
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { AuthProvider } from "@/context/AuthContext";
|
import { AuthProvider } from "@/context/AuthContext";
|
||||||
import { TopNav } from "@/components/TopNav";
|
import { TopNav } from "@/components/TopNav"; // or default import if that's how you exported it
|
||||||
|
import { BuilderNav } from "@/components/BuilderNav";
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: "Shadow Standard Co.",
|
|
||||||
description: "The Armory — Early Access",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={inter.className}>
|
<body className="bg-black text-zinc-100">
|
||||||
|
{/* AuthProvider wraps everything that uses useAuth */}
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<div className="min-h-screen flex flex-col bg-black text-zinc-50">
|
<TopNav />
|
||||||
<TopNav />
|
<BuilderNav />
|
||||||
<main className="flex-1">{children}</main>
|
<main className="min-h-screen">{children}</main>
|
||||||
</div>
|
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { apiFetch } from "@/lib/useApi";
|
||||||
|
|
||||||
|
export type AdminCategory = {
|
||||||
|
id: number;
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
groupName?: string | null;
|
||||||
|
sortOrder?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdminPartRoleMapping = {
|
||||||
|
id: number;
|
||||||
|
platform: string;
|
||||||
|
partRole: string;
|
||||||
|
categorySlug: string;
|
||||||
|
categoryName: string;
|
||||||
|
groupName?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function fetchAdminCategories(token: string) {
|
||||||
|
return apiFetch<AdminCategory[]>("/api/admin/categories", {
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPartRoleMappings(token: string, platform = "AR-15") {
|
||||||
|
const params = new URLSearchParams({ platform });
|
||||||
|
return apiFetch<AdminPartRoleMapping[]>(`/api/admin/category-mappings?${params.toString()}`, {
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPartRoleMapping(
|
||||||
|
token: string,
|
||||||
|
payload: {
|
||||||
|
platform: string;
|
||||||
|
partRole: string;
|
||||||
|
categorySlug: string;
|
||||||
|
notes?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return apiFetch<AdminPartRoleMapping>("/api/admin/category-mappings", {
|
||||||
|
method: "POST",
|
||||||
|
token,
|
||||||
|
body: payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePartRoleMapping(
|
||||||
|
token: string,
|
||||||
|
id: number,
|
||||||
|
payload: {
|
||||||
|
platform: string;
|
||||||
|
partRole: string;
|
||||||
|
categorySlug: string;
|
||||||
|
notes?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return apiFetch<AdminPartRoleMapping>(`/api/admin/category-mappings/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
token,
|
||||||
|
body: payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePartRoleMapping(token: string, id: number) {
|
||||||
|
await apiFetch<void>(`/api/admin/category-mappings/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { CATEGORIES } from "@/data/gunbuilderParts";
|
||||||
|
import type { Category } from "@/types/gunbuilder";
|
||||||
|
|
||||||
|
type BuilderNavProps = {
|
||||||
|
activeCategoryId?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to group categories for the dropdowns
|
||||||
|
function groupCategories(group: "lower" | "upper" | "accessories"): Category[] {
|
||||||
|
return CATEGORIES.filter((c) => c.group === group);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BuilderNav({ activeCategoryId }: BuilderNavProps) {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const lower = groupCategories("lower");
|
||||||
|
const upper = groupCategories("upper");
|
||||||
|
const accessories = groupCategories("accessories");
|
||||||
|
|
||||||
|
const currentCategory =
|
||||||
|
activeCategoryId ?? searchParams.get("category") ?? undefined;
|
||||||
|
|
||||||
|
const baseHref = "/gunbuilder";
|
||||||
|
|
||||||
|
const renderDropdown = (label: string, items: Category[]) => {
|
||||||
|
if (!items.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative group">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`inline-flex items-center gap-1 font-medium tracking-wide transition-colors uppercase text-[11px] ${
|
||||||
|
items.some(cat => cat.id === currentCategory)
|
||||||
|
? "text-white"
|
||||||
|
: "text-neutral-200 hover:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
<span className="text-[9px]">▾</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="invisible opacity-0 group-hover:visible group-hover:opacity-100 transition-all absolute left-0 mt-2 rounded-md border border-neutral-800 bg-black/95 shadow-xl z-40 min-w-[220px]">
|
||||||
|
<ul className="py-2 text-sm">
|
||||||
|
{items.map((cat) => {
|
||||||
|
const isActive = currentCategory === cat.id;
|
||||||
|
return (
|
||||||
|
<li key={cat.id}>
|
||||||
|
<Link
|
||||||
|
href={`${baseHref}/${cat.id}`}
|
||||||
|
className={[
|
||||||
|
"block w-full px-3 py-1.5 text-left text-sm transition-colors",
|
||||||
|
isActive
|
||||||
|
? "bg-neutral-800 text-white"
|
||||||
|
: "text-neutral-300 hover:bg-neutral-800 hover:text-white",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{cat.name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-6xl mx-auto w-full"> {/* ← aligns with topnav */}
|
||||||
|
<nav className="flex items-center gap-6 border-b border-neutral-900 px-6 py-3 text-sm bg-black/95 backdrop-blur-sm">
|
||||||
|
{/* Primary builder link */}
|
||||||
|
<Link
|
||||||
|
href={baseHref}
|
||||||
|
className="font-semibold text-neutral-100 hover:text-white tracking-[0.25em] uppercase text-[11px]"
|
||||||
|
>
|
||||||
|
Builder
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{renderDropdown("Lower Parts", lower)}
|
||||||
|
{renderDropdown("Upper Parts", upper)}
|
||||||
|
{renderDropdown("Accessories", accessories)}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/builds"
|
||||||
|
className="font-medium text-neutral-300 hover:text-white tracking-[0.25em] uppercase text-[11px]"
|
||||||
|
>
|
||||||
|
Builds
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Right → Current filter */}
|
||||||
|
{currentCategory && (
|
||||||
|
<div className="ml-auto flex items-center gap-2 text-neutral-400 text-xs">
|
||||||
|
<span className="uppercase tracking-wide text-[10px]">Viewing</span>
|
||||||
|
<span className="font-medium text-neutral-100">
|
||||||
|
{CATEGORIES.find((c) => c.id === currentCategory)?.name ??
|
||||||
|
currentCategory}
|
||||||
|
</span>
|
||||||
|
<Link
|
||||||
|
href={baseHref}
|
||||||
|
className="rounded border border-neutral-700 px-2 py-0.5 text-[10px] uppercase tracking-wide hover:bg-neutral-800 hover:text-white"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BuilderNav;
|
||||||
@@ -9,6 +9,22 @@ export function TopNav() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="border-b border-zinc-800 bg-black/95 backdrop-blur">
|
<header className="border-b border-zinc-800 bg-black/95 backdrop-blur">
|
||||||
|
<div className="border-b border-amber-500/20 bg-amber-500/5">
|
||||||
|
<div className="mx-auto flex max-w-6xl flex-col gap-1 px-4 py-2 text-[0.75rem] text-amber-100 md:flex-row md:items-center md:justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="rounded-sm bg-amber-500/20 px-2 py-0.5 text-[0.65rem] font-semibold uppercase tracking-[0.18em] text-amber-300">
|
||||||
|
Early Access Beta
|
||||||
|
</span>
|
||||||
|
<span className="hidden text-amber-100/90 md:inline">
|
||||||
|
You're using an early-access prototype of The Armory. Data,
|
||||||
|
pricing, and available parts are still evolving.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-amber-100/90 md:hidden">
|
||||||
|
Early-access prototype. Data and pricing may change.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
||||||
{/* Brand / Home link */}
|
{/* Brand / Home link */}
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
+70
-14
@@ -4,104 +4,160 @@ export const CATEGORIES: Category[] = [
|
|||||||
// LOWER
|
// LOWER
|
||||||
{
|
{
|
||||||
id: "lower",
|
id: "lower",
|
||||||
name: "Stripped Lower Receiver",
|
name: "Stripped Lowers",
|
||||||
description: "Serialized lower – the foundation of the build.",
|
description: "Serialized lower – the foundation of the build.",
|
||||||
|
group: "lower",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "completeLower",
|
id: "completeLower",
|
||||||
name: "Complete Lower",
|
name: "Complete Lowers",
|
||||||
description: "Factory-built lower with stock, buffer system, and controls.",
|
description: "Factory-built lower with stock, buffer system, and controls.",
|
||||||
|
group: "lower",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "lowerParts",
|
id: "lowerParts",
|
||||||
name: "Lower Parts Kit",
|
name: "Lower Parts Kits",
|
||||||
description: "Pins, springs, and controls for the lower.",
|
description: "Pins, springs, and controls for the lower.",
|
||||||
|
group: "lower",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "trigger",
|
id: "trigger",
|
||||||
name: "Trigger / Fire Control",
|
name: "Trigger / Fire Control",
|
||||||
description: "Single-stage, two-stage, and match triggers.",
|
description: "Single-stage, two-stage, and match triggers.",
|
||||||
|
group: "lower",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "grip",
|
id: "grip",
|
||||||
name: "Pistol Grip",
|
name: "Pistol Grip",
|
||||||
description: "Grips that shape how the rifle handles.",
|
description: "Grips that shape how the rifle handles.",
|
||||||
|
group: "lower",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "safety",
|
id: "safety",
|
||||||
name: "Safety / Selector",
|
name: "Safety / Selector",
|
||||||
description: "Ambi selectors and control upgrades.",
|
description: "Ambi selectors and control upgrades.",
|
||||||
|
group: "lower",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "buffer",
|
id: "buffer",
|
||||||
name: "Buffer System",
|
name: "Buffer System",
|
||||||
description: "Buffer tube, buffer, spring, and related parts.",
|
description: "Buffer tube, buffer, spring, and related parts.",
|
||||||
|
group: "lower",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "stock",
|
id: "stock",
|
||||||
name: "Stock / Brace",
|
name: "Stock / Brace",
|
||||||
description: "Adjustable, fixed, and minimalist stocks.",
|
description: "Adjustable, fixed, and minimalist stocks.",
|
||||||
|
group: "lower",
|
||||||
},
|
},
|
||||||
|
|
||||||
// UPPER
|
// UPPER
|
||||||
{
|
{
|
||||||
id: "upper",
|
id: "upper",
|
||||||
name: "Stripped Upper Receiver",
|
name: "Stripped Uppers",
|
||||||
description: "The core of the top half of your build.",
|
description: "The core of the top half of your build.",
|
||||||
|
group: "upper",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "completeUpper",
|
id: "completeUpper",
|
||||||
name: "Complete Upper",
|
name: "Complete Uppers",
|
||||||
description: "Pre-assembled uppers ready to pin and shoot.",
|
description: "Pre-assembled uppers ready to pin and shoot.",
|
||||||
|
group: "upper",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "bcg",
|
id: "bcg",
|
||||||
name: "Bolt Carrier Group",
|
name: "Bolt Carriers",
|
||||||
description: "The heartbeat of the rifle’s cycling.",
|
description: "The heartbeat of the rifle’s cycling.",
|
||||||
|
group: "upper",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "barrel",
|
id: "barrel",
|
||||||
name: "Barrel",
|
name: "Barrels",
|
||||||
description: "Different lengths, profiles, and calibers.",
|
description: "Different lengths, profiles, and calibers.",
|
||||||
|
group: "upper",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "gasBlock",
|
id: "gasBlock",
|
||||||
name: "Gas Block",
|
name: "Gas Blocks",
|
||||||
description: "Standard and adjustable gas blocks.",
|
description: "Standard and adjustable gas blocks.",
|
||||||
|
group: "upper",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "gasTube",
|
id: "gasTube",
|
||||||
name: "Gas Tube",
|
name: "Gas Tubes",
|
||||||
description: "Carbine, mid, rifle, and more.",
|
description: "Carbine, mid, rifle, and more.",
|
||||||
|
group: "upper",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "muzzleDevice",
|
id: "muzzleDevice",
|
||||||
name: "Muzzle Device",
|
name: "Muzzle Devices",
|
||||||
description: "Brakes, comps, and flash hiders.",
|
description: "Brakes, comps, and flash hiders.",
|
||||||
|
group: "upper",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "suppressor",
|
id: "suppressor",
|
||||||
name: "Suppressor",
|
name: "Suppressors",
|
||||||
description: "Hearing-safe setups and hosts.",
|
description: "Hearing-safe setups and hosts.",
|
||||||
|
group: "upper",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "handguard",
|
id: "handguard",
|
||||||
name: "Handguard / Rail",
|
name: "Handguards / Rails",
|
||||||
description: "M-LOK rails and front-end furniture.",
|
description: "M-LOK rails and front-end furniture.",
|
||||||
|
group: "upper",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "chargingHandle",
|
id: "chargingHandle",
|
||||||
name: "Charging Handle",
|
name: "Charging Handles",
|
||||||
description: "Standard and ambi charging handles.",
|
description: "Standard and ambi charging handles.",
|
||||||
|
group: "upper",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "sights",
|
id: "sights",
|
||||||
name: "Iron Sights",
|
name: "Iron Sights",
|
||||||
description: "Backup and primary irons.",
|
description: "Backup and primary irons.",
|
||||||
|
group: "upper",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "optic",
|
id: "optic",
|
||||||
name: "Optic",
|
name: "Optics",
|
||||||
description: "LPVOs, red dots, and magnifiers.",
|
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",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -27,14 +27,76 @@ export const CATEGORY_TO_PART_ROLES: Record<CategoryId, string[]> = {
|
|||||||
gasTube: ["gas-tube"],
|
gasTube: ["gas-tube"],
|
||||||
muzzleDevice: ["muzzle-device", "compensator", "brake"],
|
muzzleDevice: ["muzzle-device", "compensator", "brake"],
|
||||||
suppressor: ["suppressor"],
|
suppressor: ["suppressor"],
|
||||||
|
|
||||||
|
// ===== ACCESSORIES =====
|
||||||
|
|
||||||
|
// Magazines
|
||||||
|
magazine: [
|
||||||
|
"magazine",
|
||||||
|
"mag",
|
||||||
|
"magazine-ar15",
|
||||||
|
"magazine-308",
|
||||||
|
"drum-magazine",
|
||||||
|
],
|
||||||
|
|
||||||
|
// 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",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Invert to get partRole -> CategoryId
|
// Invert to get partRole -> CategoryId
|
||||||
export const PART_ROLE_TO_CATEGORY: Record<string, CategoryId> = Object
|
export const PART_ROLE_TO_CATEGORY: Record<string, CategoryId> = Object.entries(
|
||||||
.entries(CATEGORY_TO_PART_ROLES)
|
CATEGORY_TO_PART_ROLES,
|
||||||
.reduce((acc, [categoryId, roles]) => {
|
).reduce((acc, [categoryId, roles]) => {
|
||||||
for (const role of roles) {
|
for (const role of roles) {
|
||||||
acc[role] = categoryId as CategoryId;
|
acc[role] = categoryId as CategoryId;
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, CategoryId>);
|
}, {} as Record<string, CategoryId>);
|
||||||
+16
-2
@@ -1,3 +1,6 @@
|
|||||||
|
// Grouping for nav + layout
|
||||||
|
export type CategoryGroup = "lower" | "upper" | "accessories";
|
||||||
|
|
||||||
export type CategoryId =
|
export type CategoryId =
|
||||||
| "upper"
|
| "upper"
|
||||||
| "completeUpper"
|
| "completeUpper"
|
||||||
@@ -18,12 +21,22 @@ export type CategoryId =
|
|||||||
| "safety"
|
| "safety"
|
||||||
| "sights"
|
| "sights"
|
||||||
| "optic"
|
| "optic"
|
||||||
| "stock";
|
| "stock"
|
||||||
|
// NEW ACCESSORY CATEGORIES
|
||||||
|
| "magazine"
|
||||||
|
| "weaponLight"
|
||||||
|
| "foregrip"
|
||||||
|
| "bipod"
|
||||||
|
| "sling"
|
||||||
|
| "railAccessory"
|
||||||
|
| "tools";
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
id: CategoryId;
|
id: CategoryId;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
/** used for nav & grouping */
|
||||||
|
group?: CategoryGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Part {
|
export interface Part {
|
||||||
@@ -32,7 +45,8 @@ export interface Part {
|
|||||||
brand: string;
|
brand: string;
|
||||||
categoryId: CategoryId;
|
categoryId: CategoryId;
|
||||||
price: number;
|
price: number;
|
||||||
affiliateUrl: string;
|
affiliateUrl?: string;
|
||||||
|
url?: string;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user