diff --git a/app/gunbuilder/page.tsx b/app/gunbuilder/page.tsx index f31f608..6c071cb 100644 --- a/app/gunbuilder/page.tsx +++ b/app/gunbuilder/page.tsx @@ -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 (
-
-
-
- - Early Access Beta - - - You're using an early-access prototype of The Armory. Data, - pricing, and available parts are still evolving. - -
- - Early-access prototype. Data and pricing may change. - -
-
+
{/* Header */}
diff --git a/app/layout.tsx b/app/layout.tsx index ee92b07..dc9a196 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 ( - + + {/* AuthProvider wraps everything that uses useAuth */} -
- -
{children}
-
+ + +
{children}
diff --git a/app/lib/api/adminCategoryMappings.ts b/app/lib/api/adminCategoryMappings.ts new file mode 100644 index 0000000..5dd60d7 --- /dev/null +++ b/app/lib/api/adminCategoryMappings.ts @@ -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("/api/admin/categories", { + token, + }); +} + +export async function fetchPartRoleMappings(token: string, platform = "AR-15") { + const params = new URLSearchParams({ platform }); + return apiFetch(`/api/admin/category-mappings?${params.toString()}`, { + token, + }); +} + +export async function createPartRoleMapping( + token: string, + payload: { + platform: string; + partRole: string; + categorySlug: string; + notes?: string; + }, +) { + return apiFetch("/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(`/api/admin/category-mappings/${id}`, { + method: "PUT", + token, + body: payload, + }); +} + +export async function deletePartRoleMapping(token: string, id: number) { + await apiFetch(`/api/admin/category-mappings/${id}`, { + method: "DELETE", + token, + }); +} \ No newline at end of file diff --git a/components/BuilderNav.tsx b/components/BuilderNav.tsx new file mode 100644 index 0000000..5aecdcf --- /dev/null +++ b/components/BuilderNav.tsx @@ -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 ( +
+ + +
+
    + {items.map((cat) => { + const isActive = currentCategory === cat.id; + return ( +
  • + + {cat.name} + +
  • + ); + })} +
+
+
+ ); + }; + + return ( +
{/* ← aligns with topnav */} + +
+ ); +} + +export default BuilderNav; \ No newline at end of file diff --git a/components/TopNav.tsx b/components/TopNav.tsx index 035efe6..f848c69 100644 --- a/components/TopNav.tsx +++ b/components/TopNav.tsx @@ -9,6 +9,22 @@ export function TopNav() { return (
+
+
+
+ + Early Access Beta + + + You're using an early-access prototype of The Armory. Data, + pricing, and available parts are still evolving. + +
+ + Early-access prototype. Data and pricing may change. + +
+
{/* Brand / Home link */} = { 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 = Object - .entries(CATEGORY_TO_PART_ROLES) - .reduce((acc, [categoryId, roles]) => { - for (const role of roles) { - acc[role] = categoryId as CategoryId; - } - return acc; - }, {} as Record); \ No newline at end of file +export const PART_ROLE_TO_CATEGORY: Record = Object.entries( + CATEGORY_TO_PART_ROLES, +).reduce((acc, [categoryId, roles]) => { + for (const role of roles) { + acc[role] = categoryId as CategoryId; + } + return acc; +}, {} as Record); \ No newline at end of file diff --git a/types/gunbuilder.ts b/types/gunbuilder.ts index e4fe3e5..33d84f5 100644 --- a/types/gunbuilder.ts +++ b/types/gunbuilder.ts @@ -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; }