deleteing .next

This commit is contained in:
2025-12-04 10:12:03 -05:00
8 changed files with 392 additions and 72 deletions
+23 -34
View File
@@ -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&apos;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
View File
@@ -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>
+73
View File
@@ -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,
});
}
+115
View File
@@ -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;
+16
View File
@@ -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&apos;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
View File
@@ -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 rifles 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",
},
];
+70 -8
View File
@@ -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
View File
@@ -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;
}