deleteing .next
This commit is contained in:
+23
-34
@@ -119,25 +119,29 @@ 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);
|
||||
|
||||
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);
|
||||
} catch (err: any) {
|
||||
@@ -327,22 +331,7 @@ export default function GunbuilderPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
{/* Header */}
|
||||
<header className="mb-6">
|
||||
|
||||
+9
-14
@@ -1,25 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import "./globals.css";
|
||||
import { Inter } from "next/font/google";
|
||||
import type { ReactNode } from "react";
|
||||
import { AuthProvider } from "@/context/AuthContext";
|
||||
import { TopNav } from "@/components/TopNav";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata = {
|
||||
title: "Shadow Standard Co.",
|
||||
description: "The Armory — Early Access",
|
||||
};
|
||||
import { TopNav } from "@/components/TopNav"; // or default import if that's how you exported it
|
||||
import { BuilderNav } from "@/components/BuilderNav";
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<body className="bg-black text-zinc-100">
|
||||
{/* AuthProvider wraps everything that uses useAuth */}
|
||||
<AuthProvider>
|
||||
<div className="min-h-screen flex flex-col bg-black text-zinc-50">
|
||||
<TopNav />
|
||||
<main className="flex-1">{children}</main>
|
||||
</div>
|
||||
<TopNav />
|
||||
<BuilderNav />
|
||||
<main className="min-h-screen">{children}</main>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</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 (
|
||||
<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">
|
||||
{/* Brand / Home link */}
|
||||
<Link
|
||||
|
||||
+70
-14
@@ -4,104 +4,160 @@ export const CATEGORIES: Category[] = [
|
||||
// LOWER
|
||||
{
|
||||
id: "lower",
|
||||
name: "Stripped Lower Receiver",
|
||||
name: "Stripped Lowers",
|
||||
description: "Serialized lower – the foundation of the build.",
|
||||
group: "lower",
|
||||
},
|
||||
{
|
||||
id: "completeLower",
|
||||
name: "Complete Lower",
|
||||
name: "Complete Lowers",
|
||||
description: "Factory-built lower with stock, buffer system, and controls.",
|
||||
group: "lower",
|
||||
},
|
||||
{
|
||||
id: "lowerParts",
|
||||
name: "Lower Parts Kit",
|
||||
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",
|
||||
},
|
||||
|
||||
// UPPER
|
||||
{
|
||||
id: "upper",
|
||||
name: "Stripped Upper Receiver",
|
||||
name: "Stripped Uppers",
|
||||
description: "The core of the top half of your build.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "completeUpper",
|
||||
name: "Complete Upper",
|
||||
name: "Complete Uppers",
|
||||
description: "Pre-assembled uppers ready to pin and shoot.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "bcg",
|
||||
name: "Bolt Carrier Group",
|
||||
name: "Bolt Carriers",
|
||||
description: "The heartbeat of the rifle’s cycling.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "barrel",
|
||||
name: "Barrel",
|
||||
name: "Barrels",
|
||||
description: "Different lengths, profiles, and calibers.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "gasBlock",
|
||||
name: "Gas Block",
|
||||
name: "Gas Blocks",
|
||||
description: "Standard and adjustable gas blocks.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "gasTube",
|
||||
name: "Gas Tube",
|
||||
name: "Gas Tubes",
|
||||
description: "Carbine, mid, rifle, and more.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "muzzleDevice",
|
||||
name: "Muzzle Device",
|
||||
name: "Muzzle Devices",
|
||||
description: "Brakes, comps, and flash hiders.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "suppressor",
|
||||
name: "Suppressor",
|
||||
name: "Suppressors",
|
||||
description: "Hearing-safe setups and hosts.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "handguard",
|
||||
name: "Handguard / Rail",
|
||||
name: "Handguards / Rails",
|
||||
description: "M-LOK rails and front-end furniture.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "chargingHandle",
|
||||
name: "Charging Handle",
|
||||
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: "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",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -27,14 +27,76 @@ export const CATEGORY_TO_PART_ROLES: Record<CategoryId, string[]> = {
|
||||
gasTube: ["gas-tube"],
|
||||
muzzleDevice: ["muzzle-device", "compensator", "brake"],
|
||||
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
|
||||
export const PART_ROLE_TO_CATEGORY: Record<string, CategoryId> = Object
|
||||
.entries(CATEGORY_TO_PART_ROLES)
|
||||
.reduce((acc, [categoryId, roles]) => {
|
||||
for (const role of roles) {
|
||||
acc[role] = categoryId as CategoryId;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, CategoryId>);
|
||||
export const PART_ROLE_TO_CATEGORY: Record<string, CategoryId> = Object.entries(
|
||||
CATEGORY_TO_PART_ROLES,
|
||||
).reduce((acc, [categoryId, roles]) => {
|
||||
for (const role of roles) {
|
||||
acc[role] = categoryId as CategoryId;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, CategoryId>);
|
||||
+16
-2
@@ -1,3 +1,6 @@
|
||||
// Grouping for nav + layout
|
||||
export type CategoryGroup = "lower" | "upper" | "accessories";
|
||||
|
||||
export type CategoryId =
|
||||
| "upper"
|
||||
| "completeUpper"
|
||||
@@ -18,12 +21,22 @@ export type CategoryId =
|
||||
| "safety"
|
||||
| "sights"
|
||||
| "optic"
|
||||
| "stock";
|
||||
| "stock"
|
||||
// NEW ACCESSORY CATEGORIES
|
||||
| "magazine"
|
||||
| "weaponLight"
|
||||
| "foregrip"
|
||||
| "bipod"
|
||||
| "sling"
|
||||
| "railAccessory"
|
||||
| "tools";
|
||||
|
||||
export interface Category {
|
||||
id: CategoryId;
|
||||
name: string;
|
||||
description?: string;
|
||||
/** used for nav & grouping */
|
||||
group?: CategoryGroup;
|
||||
}
|
||||
|
||||
export interface Part {
|
||||
@@ -32,7 +45,8 @@ export interface Part {
|
||||
brand: string;
|
||||
categoryId: CategoryId;
|
||||
price: number;
|
||||
affiliateUrl: string;
|
||||
affiliateUrl?: string;
|
||||
url?: string;
|
||||
imageUrl?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user