366 lines
14 KiB
TypeScript
366 lines
14 KiB
TypeScript
"use client";
|
||
|
||
import { useMemo, useState } from "react";
|
||
import Link from "next/link";
|
||
|
||
type BuildClass = "Rifle" | "Pistol" | "NFA";
|
||
type Caliber = "5.56" | "7.62" | "9mm" | ".300 BLK" | "12ga";
|
||
|
||
type BuildCard = {
|
||
id: string;
|
||
title: string;
|
||
slug: string;
|
||
creator: string;
|
||
caliber: Caliber;
|
||
buildClass: BuildClass;
|
||
price: number;
|
||
votes: number;
|
||
tags: string[];
|
||
};
|
||
|
||
const DUMMY_BUILDS: BuildCard[] = [
|
||
{
|
||
id: "1",
|
||
title: "Duty-Grade 12.5\" AR-15",
|
||
slug: "duty-12-5-ar15",
|
||
creator: "quietpro_01",
|
||
caliber: "5.56",
|
||
buildClass: "Rifle",
|
||
price: 2450,
|
||
votes: 128,
|
||
tags: ["Duty", "NV-Ready", "LPVO"],
|
||
},
|
||
{
|
||
id: "2",
|
||
title: "Home Defense PCC",
|
||
slug: "home-defense-pcc",
|
||
creator: "nerdgunner",
|
||
caliber: "9mm",
|
||
buildClass: "Pistol",
|
||
price: 1650,
|
||
votes: 74,
|
||
tags: ["Home Defense", "Red Dot", "Suppressor-Ready"],
|
||
},
|
||
{
|
||
id: "3",
|
||
title: "Short King .300 BLK SBR",
|
||
slug: "short-king-300blk-sbr",
|
||
creator: "subsonic_six",
|
||
caliber: ".300 BLK",
|
||
buildClass: "NFA",
|
||
price: 3125,
|
||
votes: 201,
|
||
tags: ["SBR", "Suppressed", "Night Work"],
|
||
},
|
||
{
|
||
id: "4",
|
||
title: "Budget 16\" Training Rifle",
|
||
slug: "budget-16-training-rifle",
|
||
creator: "range_rat",
|
||
caliber: "5.56",
|
||
buildClass: "Rifle",
|
||
price: 975,
|
||
votes: 53,
|
||
tags: ["Budget", "Trainer", "Recce-ish"],
|
||
},
|
||
];
|
||
|
||
const CALIBER_FILTERS: (Caliber | "all")[] = [
|
||
"all",
|
||
"5.56",
|
||
"7.62",
|
||
"9mm",
|
||
".300 BLK",
|
||
"12ga",
|
||
];
|
||
|
||
const CLASS_FILTERS: (BuildClass | "all")[] = ["all", "Rifle", "Pistol", "NFA"];
|
||
|
||
const PRICE_FILTERS = [
|
||
{ id: "all", label: "All", min: 0, max: Infinity },
|
||
{ id: "sub1k", label: "< $1k", min: 0, max: 1000 },
|
||
{ id: "1to2k", label: "$1k–$2k", min: 1000, max: 2000 },
|
||
{ id: "2to3k", label: "$2k–$3k", min: 2000, max: 3000 },
|
||
{ id: "3kplus", label: "$3k+", min: 3000, max: Infinity },
|
||
];
|
||
|
||
export default function BuildsPage() {
|
||
const [caliberFilter, setCaliberFilter] = useState<Caliber | "all">("all");
|
||
const [classFilter, setClassFilter] = useState<BuildClass | "all">("all");
|
||
const [priceFilterId, setPriceFilterId] = useState<string>("all");
|
||
const [votes, setVotes] = useState<Record<string, number>>(
|
||
Object.fromEntries(DUMMY_BUILDS.map((b) => [b.id, b.votes])),
|
||
);
|
||
|
||
const activePriceFilter = PRICE_FILTERS.find(
|
||
(p) => p.id === priceFilterId,
|
||
) ?? PRICE_FILTERS[0];
|
||
|
||
const filteredBuilds = useMemo(() => {
|
||
return DUMMY_BUILDS.filter((build) => {
|
||
const matchesCaliber =
|
||
caliberFilter === "all" || build.caliber === caliberFilter;
|
||
|
||
const matchesClass =
|
||
classFilter === "all" || build.buildClass === classFilter;
|
||
|
||
const matchesPrice =
|
||
build.price >= activePriceFilter.min &&
|
||
build.price < activePriceFilter.max;
|
||
|
||
return matchesCaliber && matchesClass && matchesPrice;
|
||
});
|
||
}, [caliberFilter, classFilter, activePriceFilter]);
|
||
|
||
const handleVote = (id: string, delta: 1 | -1) => {
|
||
setVotes((prev) => ({
|
||
...prev,
|
||
[id]: (prev[id] ?? 0) + delta,
|
||
}));
|
||
};
|
||
|
||
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 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||
<div>
|
||
<p className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-500">
|
||
Battl Builder · Build Book
|
||
</p>
|
||
<h1
|
||
className="mt-2 text-3xl md:text-4xl font-extrabold tracking-tight
|
||
bg-gradient-to-r from-amber-400 via-amber-200 to-amber-400
|
||
text-transparent bg-clip-text drop-shadow-[0_2px_6px_rgba(255,193,7,0.25)]"
|
||
>
|
||
Community Builds
|
||
</h1>
|
||
<p className="mt-2 text-sm md:text-base text-zinc-400 max-w-xl">
|
||
Browse community rifle builds, vote them up or down, and steal
|
||
good ideas without shame. This is placeholder content for now —
|
||
real accounts, comments, and saved builds will wire in later.
|
||
</p>
|
||
</div>
|
||
<div className="flex gap-2 md:gap-3">
|
||
<Link
|
||
href="/builder"
|
||
className="inline-flex items-center justify-center rounded-md border border-zinc-700 bg-zinc-900/60 px-4 py-2 text-xs md:text-sm font-medium text-zinc-200 hover:bg-zinc-800 hover:border-zinc-600 transition-colors"
|
||
>
|
||
Open Builder
|
||
</Link>
|
||
<button
|
||
type="button"
|
||
className="inline-flex items-center justify-center rounded-md border border-amber-400/70 bg-amber-400/10 px-4 py-2 text-xs md:text-sm font-medium text-amber-200 hover:bg-amber-400/15 transition-colors"
|
||
>
|
||
+ Submit Build (Coming Soon)
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
{/* Filters */}
|
||
<section className="mb-6 rounded-lg border border-zinc-800 bg-zinc-950/70 p-4 space-y-4">
|
||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||
<div>
|
||
<h2 className="text-xs font-semibold tracking-[0.16em] uppercase text-zinc-400">
|
||
Filter Builds
|
||
</h2>
|
||
<p className="text-xs text-zinc-500 mt-1">
|
||
Filter by caliber, platform class, and ballpark price range.
|
||
All logic is client-side placeholder until the real feed is wired
|
||
into the backend.
|
||
</p>
|
||
</div>
|
||
<div className="text-xs text-zinc-500">
|
||
Showing{" "}
|
||
<span className="text-zinc-200 font-medium">
|
||
{filteredBuilds.length}
|
||
</span>{" "}
|
||
of{" "}
|
||
<span className="text-zinc-200 font-medium">
|
||
{DUMMY_BUILDS.length}
|
||
</span>{" "}
|
||
demo builds
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid gap-3 md:grid-cols-3">
|
||
{/* Caliber filter */}
|
||
<div>
|
||
<div className="text-[0.7rem] uppercase tracking-[0.16em] text-zinc-500 mb-1">
|
||
Caliber
|
||
</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
{CALIBER_FILTERS.map((caliber) => (
|
||
<button
|
||
key={caliber}
|
||
type="button"
|
||
onClick={() =>
|
||
setCaliberFilter(caliber === "all" ? "all" : caliber)
|
||
}
|
||
className={`rounded-full border px-3 py-1 text-xs transition-colors ${
|
||
caliberFilter === caliber
|
||
? "border-amber-400/70 bg-amber-400/10 text-amber-200"
|
||
: "border-zinc-700 bg-zinc-900/60 text-zinc-300 hover:bg-zinc-800 hover:border-zinc-600"
|
||
}`}
|
||
>
|
||
{caliber === "all" ? "All" : caliber}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Class filter */}
|
||
<div>
|
||
<div className="text-[0.7rem] uppercase tracking-[0.16em] text-zinc-500 mb-1">
|
||
Class
|
||
</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
{CLASS_FILTERS.map((cls) => (
|
||
<button
|
||
key={cls}
|
||
type="button"
|
||
onClick={() =>
|
||
setClassFilter(cls === "all" ? "all" : cls)
|
||
}
|
||
className={`rounded-full border px-3 py-1 text-xs transition-colors ${
|
||
classFilter === cls
|
||
? "border-amber-400/70 bg-amber-400/10 text-amber-200"
|
||
: "border-zinc-700 bg-zinc-900/60 text-zinc-300 hover:bg-zinc-800 hover:border-zinc-600"
|
||
}`}
|
||
>
|
||
{cls === "all" ? "All" : cls}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Price filter */}
|
||
<div>
|
||
<div className="text-[0.7rem] uppercase tracking-[0.16em] text-zinc-500 mb-1">
|
||
Price Range
|
||
</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
{PRICE_FILTERS.map((range) => (
|
||
<button
|
||
key={range.id}
|
||
type="button"
|
||
onClick={() => setPriceFilterId(range.id)}
|
||
className={`rounded-full border px-3 py-1 text-xs transition-colors ${
|
||
priceFilterId === range.id
|
||
? "border-amber-400/70 bg-amber-400/10 text-amber-200"
|
||
: "border-zinc-700 bg-zinc-900/60 text-zinc-300 hover:bg-zinc-800 hover:border-zinc-600"
|
||
}`}
|
||
>
|
||
{range.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Build list */}
|
||
<section>
|
||
<div className="space-y-4">
|
||
{filteredBuilds.map((build) => (
|
||
<article
|
||
key={build.id}
|
||
className="flex gap-4 rounded-lg border border-zinc-800 bg-zinc-950/70 p-4"
|
||
>
|
||
{/* Votes column */}
|
||
<div className="flex flex-col items-center justify-center gap-1">
|
||
<button
|
||
type="button"
|
||
onClick={() => handleVote(build.id, 1)}
|
||
className="flex h-6 w-6 items-center justify-center rounded-full border border-zinc-700 bg-zinc-900/70 text-xs text-zinc-300 hover:bg-zinc-800 hover:border-zinc-500"
|
||
>
|
||
▲
|
||
</button>
|
||
<div className="text-xs font-semibold text-amber-200">
|
||
{votes[build.id] ?? build.votes}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleVote(build.id, -1)}
|
||
className="flex h-6 w-6 items-center justify-center rounded-full border border-zinc-700 bg-zinc-900/70 text-xs text-zinc-300 hover:bg-zinc-800 hover:border-zinc-500"
|
||
>
|
||
▼
|
||
</button>
|
||
</div>
|
||
|
||
{/* Placeholder thumbnail */}
|
||
<div className="hidden sm:block">
|
||
<div className="overflow-hidden rounded-md border border-zinc-800 bg-zinc-900/80 h-24 w-40">
|
||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||
<img
|
||
src="https://placehold.co/320x200/png?text=Build+Photo"
|
||
alt={`${build.title} placeholder`}
|
||
className="h-full w-full object-cover"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Main content */}
|
||
<div className="flex-1">
|
||
<div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
|
||
<div>
|
||
<div className="text-[0.7rem] uppercase tracking-[0.16em] text-zinc-500 mb-1">
|
||
{build.buildClass} · {build.caliber}
|
||
</div>
|
||
<h3 className="text-lg md:text-xl font-semibold text-zinc-50">
|
||
<Link
|
||
href={`/builds/${build.slug}`}
|
||
className="hover:text-amber-200 transition-colors"
|
||
>
|
||
{build.title}
|
||
</Link>
|
||
</h3>
|
||
<p className="text-xs text-zinc-500 mt-1">
|
||
Posted by{" "}
|
||
<span className="text-zinc-300">
|
||
b/{build.creator}
|
||
</span>{" "}
|
||
· Placeholder build listing — details, parts list, and
|
||
comments will live on the build detail page later.
|
||
</p>
|
||
</div>
|
||
<div className="text-right mt-1 md:mt-0">
|
||
<div className="text-xs uppercase tracking-[0.16em] text-zinc-500">
|
||
Est. Build Cost
|
||
</div>
|
||
<div className="text-lg font-semibold text-amber-300">
|
||
${build.price.toLocaleString()}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||
{build.tags.map((tag) => (
|
||
<span
|
||
key={tag}
|
||
className="rounded-full border border-zinc-700 bg-zinc-900/60 px-2 py-0.5 text-[0.7rem] text-zinc-400"
|
||
>
|
||
{tag}
|
||
</span>
|
||
))}
|
||
<span className="ml-auto text-[0.7rem] text-zinc-500">
|
||
Comments, save, and share coming soon.
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
))}
|
||
|
||
{filteredBuilds.length === 0 && (
|
||
<div className="rounded-lg border border-zinc-800 bg-zinc-950/70 p-6 text-center text-sm text-zinc-500">
|
||
No builds match those filters yet. Once the real feed is wired
|
||
up, this will update live as the community posts rifles, pistols,
|
||
and NFA builds.
|
||
</div>
|
||
)}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</main>
|
||
);
|
||
} |