subnav and other stuff
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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user