fixing platform routing. not done. updated logo on builder
This commit is contained in:
@@ -27,7 +27,10 @@ const API_BASE_URL =
|
||||
export default function PartDetailPage() {
|
||||
const params = useParams();
|
||||
const categoryId = params.categoryId as CategoryId;
|
||||
const partId = params.partId as string; // this is the UUID we passed in the link
|
||||
|
||||
// Support URLs like /builder/lower/25969-m5-complete-lower-receiver
|
||||
const rawPartParam = params.partId as string;
|
||||
const partId = rawPartParam.split("-")[0]; // "25969-m5-..." -> "25969"
|
||||
|
||||
const [product, setProduct] = useState<GunbuilderProductFromApi | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { CATEGORIES } from "@/data/gunbuilderParts";
|
||||
import type { CategoryId, Part } from "@/types/gunbuilder";
|
||||
|
||||
@@ -26,6 +26,8 @@ type UiPart = Part & {
|
||||
inStock?: boolean;
|
||||
};
|
||||
|
||||
const PLATFORMS = ["AR-15", "AR-10"];
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
|
||||
|
||||
@@ -37,10 +39,18 @@ type SortOption = "relevance" | "price-asc" | "price-desc" | "brand-asc";
|
||||
type BuildState = Partial<Record<CategoryId, string>>;
|
||||
const STORAGE_KEY = "gunbuilder-build-state";
|
||||
|
||||
// support for url id-slug
|
||||
const slugify = (str: string) =>
|
||||
str
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)/g, "");
|
||||
|
||||
export default function CategoryPage() {
|
||||
const params = useParams();
|
||||
const categoryId = params.categoryId as CategoryId;
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [viewMode, setViewMode] = useState<ViewMode>("list");
|
||||
const [parts, setParts] = useState<UiPart[]>([]);
|
||||
@@ -70,6 +80,10 @@ export default function CategoryPage() {
|
||||
}
|
||||
});
|
||||
|
||||
const initialPlatform =
|
||||
(searchParams.get("platform") as string) || "AR-15";
|
||||
const [platform, setPlatform] = useState<string>(initialPlatform);
|
||||
|
||||
const category = useMemo(
|
||||
() => CATEGORIES.find((c) => c.id === categoryId),
|
||||
[categoryId],
|
||||
@@ -86,10 +100,9 @@ export default function CategoryPage() {
|
||||
setError(null);
|
||||
|
||||
const search = new URLSearchParams();
|
||||
search.set("platform", "AR-15");
|
||||
search.set("categorySlug", categoryId);
|
||||
search.set("platform", platform);
|
||||
|
||||
const url = `${API_BASE_URL}/api/gunbuilder/products/by-category?${search.toString()}`;
|
||||
const url = `${API_BASE_URL}/api/products?${search.toString()}`;
|
||||
|
||||
const res = await fetch(url, { signal: controller.signal });
|
||||
|
||||
@@ -123,7 +136,7 @@ export default function CategoryPage() {
|
||||
fetchCategoryParts();
|
||||
|
||||
return () => controller.abort();
|
||||
}, [categoryId]);
|
||||
}, [categoryId, platform]);
|
||||
|
||||
// persist build to localStorage whenever it changes
|
||||
useEffect(() => {
|
||||
@@ -275,10 +288,16 @@ export default function CategoryPage() {
|
||||
return filteredParts.slice(startIndex, startIndex + PAGE_SIZE);
|
||||
}, [filteredParts, currentPage, PAGE_SIZE]);
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() => {
|
||||
// whenever the category or filters change, jump back to page 1
|
||||
setCurrentPage(1);
|
||||
}, [categoryId, brandFilter, sortBy, searchQuery, priceRange, inStockOnly]);
|
||||
}, [categoryId, brandFilter, sortBy, searchQuery, priceRange, inStockOnly, platform]);
|
||||
|
||||
useEffect(() => {
|
||||
const nextPlatform =
|
||||
(searchParams.get("platform") as string) || "AR-15";
|
||||
setPlatform(nextPlatform);
|
||||
}, [searchParams]);
|
||||
|
||||
const visibleRange = useMemo(() => {
|
||||
if (filteredParts.length === 0) {
|
||||
@@ -588,6 +607,26 @@ export default function CategoryPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label
|
||||
htmlFor="platform-select"
|
||||
className="text-xs text-zinc-500"
|
||||
>
|
||||
Platform
|
||||
</label>
|
||||
<select
|
||||
id="platform-select"
|
||||
value={platform}
|
||||
onChange={(e) => setPlatform(e.target.value)}
|
||||
className="rounded-md border border-zinc-700 bg-zinc-900/70 px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:ring-1 focus:ring-amber-400"
|
||||
>
|
||||
{PLATFORMS.map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{p}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label
|
||||
htmlFor="sort-by"
|
||||
@@ -660,7 +699,10 @@ export default function CategoryPage() {
|
||||
</div>
|
||||
<div className="mt-2 flex gap-2">
|
||||
<Link
|
||||
href={`/builder/${categoryId}/${part.id}`}
|
||||
href={{
|
||||
pathname: `/builder/${categoryId}/${part.id}-${slugify(part.name)}`,
|
||||
query: { platform },
|
||||
}}
|
||||
className="flex-1 rounded-md border border-zinc-700 bg-zinc-800/50 px-3 py-2 text-center text-xs font-medium text-zinc-300 transition-colors hover:border-zinc-600 hover:bg-zinc-700"
|
||||
>
|
||||
View Details
|
||||
@@ -723,7 +765,10 @@ export default function CategoryPage() {
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Link
|
||||
href={`/builder/${categoryId}/${part.id}`}
|
||||
href={{
|
||||
pathname: `/builder/${categoryId}/${part.id}-${slugify(part.name)}`,
|
||||
query: { platform },
|
||||
}}
|
||||
className="whitespace-nowrap rounded-md border border-zinc-700 bg-zinc-800/50 px-3 py-1.5 text-xs font-medium text-zinc-300 transition-colors hover:border-zinc-600 hover:bg-zinc-700"
|
||||
>
|
||||
View Details
|
||||
|
||||
@@ -24,35 +24,3 @@ export default function BuilderLayout({ children }: { children: ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Original homepage. will delete after testing
|
||||
//
|
||||
|
||||
// import "./globals.css";
|
||||
// import type { ReactNode } from "react";
|
||||
// import { AuthProvider } from "@/context/AuthContext";
|
||||
// import { TopNav } from "@/components/TopNav";
|
||||
// import { BuilderNav } from "@/components/BuilderNav";
|
||||
|
||||
// export const metadata = {
|
||||
// title: {
|
||||
// default: "Battl Builder",
|
||||
// template: "%s | Battl Builder",
|
||||
// },
|
||||
// description: "Battl Builder — the smarter, faster, data‑driven way to build your next firearm.",
|
||||
// };
|
||||
|
||||
// export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
// return (
|
||||
// <html lang="en">
|
||||
// <body className="bg-black text-zinc-100">
|
||||
// <AuthProvider>
|
||||
// <TopNav />
|
||||
// <BuilderNav />
|
||||
// <main className="min-h-screen">{children}</main>
|
||||
// </AuthProvider>
|
||||
// </body>
|
||||
// </html>
|
||||
// );
|
||||
// }
|
||||
|
||||
@@ -20,6 +20,8 @@ type GunbuilderProductFromApi = {
|
||||
buyUrl: string | null;
|
||||
};
|
||||
|
||||
const PLATFORMS = ["AR-15", "AR-10", "AR-9"] as const;
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
|
||||
|
||||
@@ -85,6 +87,7 @@ export default function GunbuilderPage() {
|
||||
const [parts, setParts] = useState<Part[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [platform, setPlatform] = useState<(typeof PLATFORMS)[number]>("AR-15");
|
||||
const [build, setBuild] = useState<BuildState>(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
@@ -110,8 +113,10 @@ export default function GunbuilderPage() {
|
||||
setError(null);
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/products/gunbuilder?platform=AR-15`,
|
||||
{ signal: controller.signal },
|
||||
`${API_BASE_URL}/api/products?platform=${encodeURIComponent(
|
||||
platform
|
||||
)}`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -121,29 +126,28 @@ 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);
|
||||
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);
|
||||
|
||||
setParts(normalized);
|
||||
} catch (err: any) {
|
||||
@@ -157,7 +161,16 @@ export default function GunbuilderPage() {
|
||||
fetchProducts();
|
||||
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
}, [platform]);
|
||||
|
||||
useEffect(() => {
|
||||
// When the platform changes, reset the current build so we don't
|
||||
// show stale part selections from a different platform.
|
||||
setBuild({});
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
}, [platform]);
|
||||
|
||||
// Handle URL query parameter for part selection (?select=upper:165)
|
||||
useEffect(() => {
|
||||
@@ -191,9 +204,7 @@ export default function GunbuilderPage() {
|
||||
const partsByCategory: Record<CategoryId, Part[]> = useMemo(() => {
|
||||
const grouped = {} as Record<CategoryId, Part[]>;
|
||||
for (const category of CATEGORIES) {
|
||||
grouped[category.id] = parts.filter(
|
||||
(p) => p.categoryId === category.id,
|
||||
);
|
||||
grouped[category.id] = parts.filter((p) => p.categoryId === category.id);
|
||||
}
|
||||
return grouped;
|
||||
}, [parts]);
|
||||
@@ -201,7 +212,7 @@ export default function GunbuilderPage() {
|
||||
const selectedParts: Part[] = useMemo(() => {
|
||||
return Object.entries(build)
|
||||
.map(([categoryId, partId]) =>
|
||||
parts.find((p) => p.id === partId && p.categoryId === categoryId),
|
||||
parts.find((p) => p.id === partId && p.categoryId === categoryId)
|
||||
)
|
||||
.filter(Boolean) as Part[];
|
||||
}, [build, parts]);
|
||||
@@ -212,14 +223,14 @@ export default function GunbuilderPage() {
|
||||
Object.entries(build).map(([categoryId, partId]) => [
|
||||
categoryId as CategoryId,
|
||||
partId ? true : false,
|
||||
]),
|
||||
])
|
||||
) as Record<CategoryId, unknown>,
|
||||
[build],
|
||||
[build]
|
||||
);
|
||||
|
||||
const totalPrice = useMemo(
|
||||
() => selectedParts.reduce((sum, p) => sum + p.price, 0),
|
||||
[selectedParts],
|
||||
[selectedParts]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -236,7 +247,7 @@ export default function GunbuilderPage() {
|
||||
const encoded = window.btoa(payload);
|
||||
const origin = window.location?.origin ?? "";
|
||||
const url = `${origin}/builder/build?build=${encodeURIComponent(
|
||||
encoded,
|
||||
encoded
|
||||
)}`;
|
||||
setShareUrl(url);
|
||||
} catch {
|
||||
@@ -254,10 +265,10 @@ export default function GunbuilderPage() {
|
||||
// Use group ordering for the summary as well
|
||||
const summaryCategoryOrder: CategoryId[] = useMemo(
|
||||
() =>
|
||||
CATEGORY_GROUPS.flatMap((group) => group.categoryIds).filter(
|
||||
(id) => CATEGORIES.some((c) => c.id === id),
|
||||
CATEGORY_GROUPS.flatMap((group) => group.categoryIds).filter((id) =>
|
||||
CATEGORIES.some((c) => c.id === id)
|
||||
),
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
const handleShare = async () => {
|
||||
@@ -278,14 +289,14 @@ export default function GunbuilderPage() {
|
||||
|
||||
const selectedPartId = build[cat.id];
|
||||
const part = parts.find(
|
||||
(p) => p.id === selectedPartId && p.categoryId === cat.id,
|
||||
(p) => p.id === selectedPartId && p.categoryId === cat.id
|
||||
);
|
||||
|
||||
if (part) {
|
||||
lines.push(
|
||||
`${cat.name}: ${part.brand} — ${part.name} ($${part.price.toFixed(
|
||||
2,
|
||||
)})`,
|
||||
2
|
||||
)})`
|
||||
);
|
||||
} else {
|
||||
lines.push(`${cat.name}: [Not selected]`);
|
||||
@@ -299,7 +310,7 @@ export default function GunbuilderPage() {
|
||||
setShareStatus("Build summary copied to clipboard.");
|
||||
} catch {
|
||||
setShareStatus(
|
||||
"Could not access clipboard. You can copy from the build summary page.",
|
||||
"Could not access clipboard. You can copy from the build summary page."
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -334,7 +345,9 @@ export default function GunbuilderPage() {
|
||||
setShareStatus("Share dialog opened.");
|
||||
} catch {
|
||||
// User canceled or share failed; keep it quiet but let them know
|
||||
setShareStatus("Share canceled or unavailable. You can copy the link instead.");
|
||||
setShareStatus(
|
||||
"Share canceled or unavailable. You can copy the link instead."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Fallback to copying the link
|
||||
@@ -344,7 +357,6 @@ export default function GunbuilderPage() {
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-black text-zinc-50">
|
||||
|
||||
<div className="mx-auto max-w-6xl px-4 py-6 lg:py-10">
|
||||
{/* Header */}
|
||||
<header className="mb-6">
|
||||
@@ -361,10 +373,31 @@ export default function GunbuilderPage() {
|
||||
update as you go. This early-access builder keeps your setup saved
|
||||
locally so you can come back and refine it anytime.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3">
|
||||
<label className="text-xs font-medium text-zinc-400 flex items-center gap-2">
|
||||
Platform
|
||||
<select
|
||||
value={platform}
|
||||
onChange={(e) =>
|
||||
setPlatform(e.target.value as (typeof PLATFORMS)[number])
|
||||
}
|
||||
className="rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1 text-xs text-zinc-100 focus:outline-none focus:ring-1 focus:ring-amber-400"
|
||||
>
|
||||
{PLATFORMS.map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{p}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<p className="text-[0.7rem] text-zinc-500">
|
||||
Parts list updates automatically when you change platforms.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
{/* Build summary panel */}
|
||||
<section className="mb-6 rounded-lg border border-zinc-800 bg-zinc-950/80 p-3 md:p-4 flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
{/* Left: title + totals + primary actions */}
|
||||
@@ -416,7 +449,9 @@ export default function GunbuilderPage() {
|
||||
}}
|
||||
disabled={selectedParts.length === 0}
|
||||
className={`w-full sm:w-auto rounded-md border border-zinc-700 bg-zinc-900/50 px-3 py-2 text-sm font-medium text-zinc-300 hover:bg-zinc-800 hover:border-zinc-600 transition-colors ${
|
||||
selectedParts.length === 0 ? "opacity-40 cursor-not-allowed" : ""
|
||||
selectedParts.length === 0
|
||||
? "opacity-40 cursor-not-allowed"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
Clear Build
|
||||
@@ -432,8 +467,8 @@ export default function GunbuilderPage() {
|
||||
Share Your Build
|
||||
</h3>
|
||||
<p className="mt-1 text-[0.7rem] text-zinc-500">
|
||||
Share this link to let others view your build or bookmark it to come
|
||||
back later.
|
||||
Share this link to let others view your build or bookmark it
|
||||
to come back later.
|
||||
</p>
|
||||
<div className="mt-2 flex flex-col gap-2 md:flex-row md:items-center">
|
||||
<div className="flex-1">
|
||||
@@ -468,9 +503,7 @@ export default function GunbuilderPage() {
|
||||
)}
|
||||
|
||||
{shareStatus && (
|
||||
<p className="mt-1 text-[0.7rem] text-zinc-500">
|
||||
{shareStatus}
|
||||
</p>
|
||||
<p className="mt-1 text-[0.7rem] text-zinc-500">{shareStatus}</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
@@ -511,15 +544,12 @@ export default function GunbuilderPage() {
|
||||
<section className="rounded-lg border border-zinc-800 bg-zinc-950/60 p-3 md:p-4">
|
||||
<p className="mb-4 text-xs text-zinc-500">
|
||||
Work top-down through the major sections. Each row shows your
|
||||
current pick for that part type, with price and a direct buy
|
||||
link—or a quick way to choose a part if you haven't picked
|
||||
one yet.
|
||||
current pick for that part type, with price and a direct buy link—or
|
||||
a quick way to choose a part if you haven't picked one yet.
|
||||
</p>
|
||||
|
||||
{loading && (
|
||||
<p className="text-sm text-zinc-500">
|
||||
Loading parts from backend…
|
||||
</p>
|
||||
<p className="text-sm text-zinc-500">Loading parts from backend…</p>
|
||||
)}
|
||||
{error && !loading && (
|
||||
<p className="text-sm text-red-400">
|
||||
@@ -532,7 +562,7 @@ export default function GunbuilderPage() {
|
||||
<div className="space-y-6">
|
||||
{CATEGORY_GROUPS.map((group) => {
|
||||
const groupCategories = CATEGORIES.filter((c) =>
|
||||
group.categoryIds.includes(c.id as CategoryId),
|
||||
group.categoryIds.includes(c.id as CategoryId)
|
||||
);
|
||||
|
||||
if (groupCategories.length === 0) {
|
||||
@@ -584,7 +614,7 @@ export default function GunbuilderPage() {
|
||||
partsByCategory[category.id] ?? [];
|
||||
const selectedPartId = build[category.id];
|
||||
const selectedPart = categoryParts.find(
|
||||
(p) => p.id === selectedPartId,
|
||||
(p) => p.id === selectedPartId
|
||||
);
|
||||
const hasParts = categoryParts.length > 0;
|
||||
|
||||
@@ -663,10 +693,9 @@ export default function GunbuilderPage() {
|
||||
</>
|
||||
) : hasParts ? (
|
||||
<Link
|
||||
href={`/builder/${category.id}`}
|
||||
className="inline-flex rounded-md border border-zinc-700 bg-zinc-900/70 px-2.5 py-1 text-[0.7rem] font-medium text-zinc-200 hover:bg-zinc-800 hover:border-zinc-600 transition-colors"
|
||||
onClick={() => {
|
||||
// If you want to pre-select something later, you can handle here.
|
||||
href={{
|
||||
pathname: `/builder/${category.id}`,
|
||||
query: { platform },
|
||||
}}
|
||||
>
|
||||
Choose a Part
|
||||
@@ -729,4 +758,4 @@ export default function GunbuilderPage() {
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,6 @@ export default function HomePage() {
|
||||
{/* Top Bar / Logo */}
|
||||
<header className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Logo slot – replace src with your actual logo file */}
|
||||
<div className="flex items-center">
|
||||
<Image
|
||||
src="/battl/battl-logo-mark-2.svg"
|
||||
|
||||
+10
-1
@@ -2,6 +2,8 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
|
||||
export function TopNav() {
|
||||
@@ -19,7 +21,14 @@ export function TopNav() {
|
||||
href="/"
|
||||
className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-400"
|
||||
>
|
||||
Battl Builder
|
||||
|
||||
<Image
|
||||
src="/battl/battl-logo-mark-2.svg"
|
||||
alt="Battl Builders Logo"
|
||||
width={260} // adjust to taste
|
||||
height={48} // adjust to taste
|
||||
className="block"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Right side actions */}
|
||||
|
||||
+65
-160
@@ -1,163 +1,68 @@
|
||||
import type { Category, Part } from "@/types/gunbuilder";
|
||||
import type { Category } from "@/types/gunbuilder";
|
||||
|
||||
export const CATEGORY_SLUGS = {
|
||||
lower: "lower",
|
||||
"complete-lower": "complete-lower",
|
||||
"lower-parts": "lower-parts",
|
||||
trigger: "trigger",
|
||||
grip: "grip",
|
||||
safety: "safety",
|
||||
buffer: "buffer",
|
||||
stock: "stock",
|
||||
|
||||
upper: "upper",
|
||||
"complete-upper": "complete-upper",
|
||||
barrel: "barrel",
|
||||
handguard: "handguard",
|
||||
"gas-block": "gas-block",
|
||||
"gas-tube": "gas-tube",
|
||||
"muzzle-device": "muzzle-device",
|
||||
bcg: "bcg",
|
||||
sights: "sights",
|
||||
"charging-handle": "charging-handle",
|
||||
suppressor: "suppressor",
|
||||
optic: "optic",
|
||||
|
||||
magazine: "magazine",
|
||||
"weapon-light": "weapon-light",
|
||||
foregrip: "foregrip",
|
||||
bipod: "bipod",
|
||||
sling: "sling",
|
||||
"rail-accessory": "rail-accessory",
|
||||
tools: "tools",
|
||||
} as const;
|
||||
|
||||
export const CATEGORIES: Category[] = [
|
||||
// LOWER
|
||||
{
|
||||
id: "lower",
|
||||
name: "Stripped Lowers",
|
||||
description: "Serialized lower – the foundation of the build.",
|
||||
group: "lower",
|
||||
},
|
||||
{
|
||||
id: "completeLower",
|
||||
name: "Complete Lowers",
|
||||
description: "Factory-built lower with stock, buffer system, and controls.",
|
||||
group: "lower",
|
||||
},
|
||||
{
|
||||
id: "lowerParts",
|
||||
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",
|
||||
},
|
||||
// Lower group
|
||||
{ id: "lower", name: "Stripped Lowers", group: "lower", slug: CATEGORY_SLUGS.lower },
|
||||
{ id: "complete-lower", name: "Complete Lower", group: "lower", slug: CATEGORY_SLUGS["complete-lower"] },
|
||||
{ id: "lower-parts", name: "Lower Parts Kit", group: "lower", slug: CATEGORY_SLUGS["lower-parts"] },
|
||||
{ id: "trigger", name: "Trigger / Fire Control", group: "lower", slug: CATEGORY_SLUGS.trigger },
|
||||
{ id: "grip", name: "Pistol Grip", group: "lower", slug: CATEGORY_SLUGS.grip },
|
||||
{ id: "safety", name: "Safety / Selector", group: "lower", slug: CATEGORY_SLUGS.safety },
|
||||
{ id: "buffer", name: "Buffer System", group: "lower", slug: CATEGORY_SLUGS.buffer },
|
||||
{ id: "stock", name: "Stock / Brace", group: "lower", slug: CATEGORY_SLUGS.stock },
|
||||
|
||||
// UPPER
|
||||
{
|
||||
id: "upper",
|
||||
name: "Stripped Uppers",
|
||||
description: "The core of the top half of your build.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "completeUpper",
|
||||
name: "Complete Uppers",
|
||||
description: "Pre-assembled uppers ready to pin and shoot.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "bcg",
|
||||
name: "Bolt Carriers",
|
||||
description: "The heartbeat of the rifle’s cycling.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "barrel",
|
||||
name: "Barrels",
|
||||
description: "Different lengths, profiles, and calibers.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "gasBlock",
|
||||
name: "Gas Blocks",
|
||||
description: "Standard and adjustable gas blocks.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "gasTube",
|
||||
name: "Gas Tubes",
|
||||
description: "Carbine, mid, rifle, and more.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "muzzleDevice",
|
||||
name: "Muzzle Devices",
|
||||
description: "Brakes, comps, and flash hiders.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "suppressor",
|
||||
name: "Suppressors",
|
||||
description: "Hearing-safe setups and hosts.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "handguard",
|
||||
name: "Handguards / Rails",
|
||||
description: "M-LOK rails and front-end furniture.",
|
||||
group: "upper",
|
||||
},
|
||||
{
|
||||
id: "chargingHandle",
|
||||
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: "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",
|
||||
},
|
||||
];
|
||||
// Upper group
|
||||
{ id: "upper", name: "Stripped Upper", group: "upper", slug: CATEGORY_SLUGS.upper },
|
||||
{ id: "complete-upper", name: "Complete Upper", group: "upper", slug: CATEGORY_SLUGS["complete-upper"] },
|
||||
{ id: "barrel", name: "Barrels", group: "upper", slug: CATEGORY_SLUGS.barrel },
|
||||
{ id: "handguard", name: "Handguards / Rails", group: "upper", slug: CATEGORY_SLUGS.handguard },
|
||||
{ id: "gas-block", name: "Gas Block", group: "upper", slug: CATEGORY_SLUGS["gas-block"] },
|
||||
{ id: "gas-tube", name: "Gas Tube", group: "upper", slug: CATEGORY_SLUGS["gas-tube"] },
|
||||
{ id: "muzzle-device", name: "Muzzle Device", group: "upper", slug: CATEGORY_SLUGS["muzzle-device"] },
|
||||
{ id: "sights", name: "Iron Sights", group: "upper", slug: CATEGORY_SLUGS.sights },
|
||||
{ id: "bcg", name: "Bolt Carrier Group", group: "upper", slug: CATEGORY_SLUGS.bcg },
|
||||
{ id: "charging-handle", name: "Charging Handle", group: "upper", slug: CATEGORY_SLUGS["charging-handle"] },
|
||||
{ id: "suppressor", name: "Suppressors", group: "upper", slug: CATEGORY_SLUGS.suppressor },
|
||||
{ id: "optic", name: "Optics", group: "upper", slug: CATEGORY_SLUGS.optic },
|
||||
|
||||
// Accessories
|
||||
{ id: "magazine", name: "Magazines", group: "accessories", slug: CATEGORY_SLUGS.magazine },
|
||||
{ id: "weapon-light", name: "Weapon Lights & Lasers", group: "accessories", slug: CATEGORY_SLUGS["weapon-light"] },
|
||||
{ id: "foregrip", name: "Vertical / Angled Grips", group: "accessories", slug: CATEGORY_SLUGS.foregrip },
|
||||
{ id: "bipod", name: "Bipods", group: "accessories", slug: CATEGORY_SLUGS.bipod },
|
||||
{ id: "sling", name: "Slings & Mounts", group: "accessories", slug: CATEGORY_SLUGS.sling },
|
||||
{ id: "rail-accessory", name: "Rail Accessories", group: "accessories", slug: CATEGORY_SLUGS["rail-accessory"] },
|
||||
{ id: "tools", name: "Tools & Maintenance", group: "accessories", slug: CATEGORY_SLUGS.tools },
|
||||
];
|
||||
@@ -50,3 +50,35 @@ export interface Part {
|
||||
imageUrl?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export const CATEGORY_SLUGS: Record<CategoryId, string> = {
|
||||
lower: "lower",
|
||||
completeLower: "complete-lower",
|
||||
lowerParts: "lower-parts",
|
||||
trigger: "trigger",
|
||||
grip: "grip",
|
||||
safety: "safety",
|
||||
buffer: "buffer",
|
||||
stock: "stock",
|
||||
|
||||
upper: "upper",
|
||||
completeUpper: "complete-upper",
|
||||
barrel: "barrel",
|
||||
handguard: "handguard",
|
||||
gasBlock: "gas-block",
|
||||
gasTube: "gas-tube",
|
||||
muzzleDevice: "muzzle-device",
|
||||
sights: "sights",
|
||||
bcg: "bcg",
|
||||
chargingHandle: "charging-handle",
|
||||
suppressor: "suppressor",
|
||||
optic: "optic",
|
||||
|
||||
magazine: "magazine",
|
||||
weaponLight: "weapon-light",
|
||||
foregrip: "foregrip",
|
||||
bipod: "bipod",
|
||||
sling: "sling",
|
||||
railAccessory: "rail-accessory",
|
||||
tools: "tools",
|
||||
};
|
||||
Reference in New Issue
Block a user