part category grouping UI

This commit is contained in:
2025-12-02 16:01:40 -05:00
parent 4e3d1206cb
commit c51ce862a9
19 changed files with 216 additions and 38 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -1,7 +1,7 @@
{
"/gunbuilder/page": "app/gunbuilder/page.js",
"/gunbuilder/admin/merchants/page": "app/gunbuilder/admin/merchants/page.js",
"/gunbuilder/[categoryId]/page": "app/gunbuilder/[categoryId]/page.js",
"/gunbuilder/page": "app/gunbuilder/page.js",
"/gunbuilder/build/page": "app/gunbuilder/build/page.js",
"/gunbuilder/[categoryId]/page": "app/gunbuilder/[categoryId]/page.js",
"/gunbuilder/admin/category-mappings/page": "app/gunbuilder/admin/category-mappings/page.js"
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -125,7 +125,7 @@
/******/
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => ("c6d6d43de26b07e0")
/******/ __webpack_require__.h = () => ("f15ab610ff54783e")
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -25,7 +25,7 @@ eval(__webpack_require__.ts("Promise.resolve(/*! import() eager */).then(__webpa
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval(__webpack_require__.ts("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (\"e2bb5f76af4b\");\nif (true) { module.hot.accept() }\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKGFwcC1wYWdlcy1icm93c2VyKS8uL2FwcC9nbG9iYWxzLmNzcyIsIm1hcHBpbmdzIjoiO0FBQUEsK0RBQWUsY0FBYztBQUM3QixJQUFJLElBQVUsSUFBSSxpQkFBaUIiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9fTl9FLy4vYXBwL2dsb2JhbHMuY3NzPzk1YWEiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGRlZmF1bHQgXCJlMmJiNWY3NmFmNGJcIlxuaWYgKG1vZHVsZS5ob3QpIHsgbW9kdWxlLmhvdC5hY2NlcHQoKSB9XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(app-pages-browser)/./app/globals.css\n"));
eval(__webpack_require__.ts("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (\"e222be0d16ad\");\nif (true) { module.hot.accept() }\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKGFwcC1wYWdlcy1icm93c2VyKS8uL2FwcC9nbG9iYWxzLmNzcyIsIm1hcHBpbmdzIjoiO0FBQUEsK0RBQWUsY0FBYztBQUM3QixJQUFJLElBQVUsSUFBSSxpQkFBaUIiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9fTl9FLy4vYXBwL2dsb2JhbHMuY3NzPzk1YWEiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGRlZmF1bHQgXCJlMjIyYmUwZDE2YWRcIlxuaWYgKG1vZHVsZS5ob3QpIHsgbW9kdWxlLmhvdC5hY2NlcHQoKSB9XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(app-pages-browser)/./app/globals.css\n"));
/***/ })
+1 -1
View File
@@ -192,7 +192,7 @@
/******/
/******/ /* webpack/runtime/getFullHash */
/******/ !function() {
/******/ __webpack_require__.h = function() { return "d4224caeb46d3863"; }
/******/ __webpack_require__.h = function() { return "a6f46b17097acafc"; }
/******/ }();
/******/
/******/ /* webpack/runtime/global */
+13
View File
@@ -672,6 +672,9 @@ video {
.items-center {
align-items: center;
}
.items-baseline {
align-items: baseline;
}
.justify-end {
justify-content: flex-end;
}
@@ -711,6 +714,16 @@ video {
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(1rem * var(--tw-space-y-reverse));
}
.space-y-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
}
.space-y-6 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
}
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
"use strict";
/*
* ATTENTION: An "eval-source-map" devtool has been used.
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
self["webpackHotUpdate_N_E"]("app/layout",{
/***/ "(app-pages-browser)/./app/globals.css":
/*!*************************!*\
!*** ./app/globals.css ***!
\*************************/
/***/ (function(module, __webpack_exports__, __webpack_require__) {
eval(__webpack_require__.ts("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (\"e222be0d16ad\");\nif (true) { module.hot.accept() }\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKGFwcC1wYWdlcy1icm93c2VyKS8uL2FwcC9nbG9iYWxzLmNzcyIsIm1hcHBpbmdzIjoiO0FBQUEsK0RBQWUsY0FBYztBQUM3QixJQUFJLElBQVUsSUFBSSxpQkFBaUIiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9fTl9FLy4vYXBwL2dsb2JhbHMuY3NzPzk1YWEiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGRlZmF1bHQgXCJlMjIyYmUwZDE2YWRcIlxuaWYgKG1vZHVsZS5ob3QpIHsgbW9kdWxlLmhvdC5hY2NlcHQoKSB9XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(app-pages-browser)/./app/globals.css\n"));
/***/ })
});
@@ -0,0 +1 @@
{"c":["app/layout","app/gunbuilder/page","webpack"],"r":[],"m":[]}
@@ -0,0 +1,18 @@
"use strict";
/*
* ATTENTION: An "eval-source-map" devtool has been used.
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
self["webpackHotUpdate_N_E"]("webpack",{},
/******/ function(__webpack_require__) { // webpackRuntimeModules
/******/ /* webpack/runtime/getFullHash */
/******/ !function() {
/******/ __webpack_require__.h = function() { return "a6f46b17097acafc"; }
/******/ }();
/******/
/******/ }
);
+2
View File
File diff suppressed because one or more lines are too long
+131 -31
View File
@@ -8,7 +8,7 @@ import type { CategoryId, Part } from "@/types/gunbuilder";
import { CategoryColumn } from "@/components/CategoryColumn";
type GunbuilderProductFromApi = {
id: number; // backend numeric id
id: number; // backend numeric id
name: string;
brand: string;
platform: string;
@@ -39,6 +39,40 @@ type BuildState = Partial<Record<CategoryId, string>>; // categoryId -> partId
const STORAGE_KEY = "gunbuilder-build-state";
// Group categories into logical sections.
//
// IMPORTANT:
// These IDs must match the ids you have in CATEGORIES.
// Anything that doesn't exist will just be skipped.
const CATEGORY_GROUPS: {
id: string;
label: string;
description?: string;
categoryIds: CategoryId[];
}[] = [
{
id: "lower-group",
label: "Lower Receiver Parts",
description:
"Everything from the serialized lower to small parts and core controls.",
categoryIds: ["lower", "lowerParts", "stock", "buffer"] as CategoryId[],
},
{
id: "upper-group",
label: "Upper Receiver Parts",
description:
"Barrel, upper, handguard, and the parts that keep the rifle cycling.",
categoryIds: [
"upper",
"barrel",
"handguard",
"chargingHandle",
"sights",
"optic",
] as CategoryId[],
},
];
export default function GunbuilderPage() {
const searchParams = useSearchParams();
const router = useRouter();
@@ -87,7 +121,7 @@ export default function GunbuilderPage() {
}
return {
id: String(p.id), // <<< ALWAYS string
id: String(p.id), // ALWAYS string
categoryId,
name: p.name,
brand: p.brand,
@@ -124,7 +158,9 @@ export default function GunbuilderPage() {
...prev,
[categoryId as CategoryId]: partId,
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
if (typeof window !== "undefined") {
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
}
return updated;
});
@@ -170,6 +206,15 @@ 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),
),
[],
);
return (
<main className="min-h-screen bg-black text-zinc-50">
<div className="border-b border-amber-500/20 bg-amber-500/5">
@@ -179,7 +224,8 @@ export default function GunbuilderPage() {
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.
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">
@@ -198,7 +244,10 @@ export default function GunbuilderPage() {
The Armory: <span className="text-amber-300">Early Access</span>
</h1>
<p className="mt-2 text-sm text-zinc-400 max-w-xl">
Explore components from trusted brands, choose one part per category, track live prices, and watch your total build cost update as you go. This early-access builder keeps your setup saved locally so you can come back and refine it anytime.
Explore components from trusted brands, choose one part per
category, track live prices, and watch your total build cost
update as you go. This early-access builder keeps your setup saved
locally so you can come back and refine it anytime.
</p>
</div>
@@ -218,7 +267,9 @@ export default function GunbuilderPage() {
{/* Categories/parts */}
<section className="rounded-lg border border-zinc-800 bg-zinc-950/60 p-3 md:p-4">
{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">
@@ -227,22 +278,51 @@ export default function GunbuilderPage() {
</p>
)}
{!loading && !error && (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{CATEGORIES.map((category) => (
<div
key={category.id}
className="border border-zinc-800 rounded-md p-2 md:p-3 bg-zinc-950/60"
>
<CategoryColumn
category={category}
parts={partsByCategory[category.id]}
selectedPartId={build[category.id]}
onSelectPart={(partId) =>
handleSelectPart(category.id, partId)
}
/>
</div>
))}
<div className="space-y-6">
{CATEGORY_GROUPS.map((group) => {
const groupCategories = CATEGORIES.filter((c) =>
group.categoryIds.includes(c.id as CategoryId),
);
if (groupCategories.length === 0) {
return null;
}
return (
<div key={group.id} className="space-y-3">
<div className="flex items-baseline justify-between gap-2">
<div>
<h2 className="text-sm font-semibold text-zinc-100">
{group.label}
</h2>
{group.description && (
<p className="text-xs text-zinc-500 mt-1 max-w-xl">
{group.description}
</p>
)}
</div>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{groupCategories.map((category) => (
<div
key={category.id}
className="border border-zinc-800 rounded-md p-2 md:p-3 bg-zinc-950/60"
>
<CategoryColumn
category={category}
parts={partsByCategory[category.id]}
selectedPartId={build[category.id]}
onSelectPart={(partId) =>
handleSelectPart(category.id, partId)
}
/>
</div>
))}
</div>
</div>
);
})}
</div>
)}
</section>
@@ -261,11 +341,16 @@ export default function GunbuilderPage() {
)}
<ul className="space-y-2">
{CATEGORIES.map((cat) => {
{summaryCategoryOrder.map((categoryId) => {
const cat = CATEGORIES.find((c) => c.id === categoryId);
if (!cat) return null;
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,
);
return (
<li key={cat.id}>
<div className="text-[0.7rem] uppercase tracking-[0.16em] text-zinc-500">
@@ -310,7 +395,9 @@ export default function GunbuilderPage() {
type="button"
onClick={() => {
setBuild({});
localStorage.removeItem(STORAGE_KEY);
if (typeof window !== "undefined") {
localStorage.removeItem(STORAGE_KEY);
}
}}
disabled={selectedParts.length === 0}
className={`w-full 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 ${
@@ -327,27 +414,40 @@ export default function GunbuilderPage() {
</div>
</aside>
</div>
{/* What's New */}
<section className="mt-8 rounded-lg border border-zinc-800 bg-zinc-950/80 p-3 md:p-4">
<h2 className="text-xs font-semibold uppercase tracking-[0.16em] text-amber-300">
What&apos;s New in Early Access
</h2>
<ul className="mt-2 space-y-1 text-sm text-zinc-400">
<li> Live parts and pricing pulled from multiple merchants.</li>
<li> Category-based layout so you can see at a glance which parts of your build are still missing.</li>
<li> Running build total that updates automatically as you add or swap components.</li>
<li> Local build persistence so your selections stick around between visits on this device.</li>
<li>
Grouped layout for lower and upper receiver parts so you can see
at a glance which sections of your build are still missing.
</li>
<li>
Running build total that updates automatically as you add or
swap components.
</li>
<li>
Local build persistence so your selections stick around between
visits on this device.
</li>
</ul>
</section>
{/* Roadmap */}
<section className="mt-8 rounded-lg border border-zinc-800 bg-zinc-950/80 p-3 md:p-4">
<h2 className="text-xs font-semibold uppercase tracking-[0.16em] text-amber-300">
Whats on our roadmap
What&apos;s on our roadmap
</h2>
<ul className="mt-2 space-y-1 text-sm text-zinc-400">
<li> Platform filters (AR-15, AR-9, AR-10)</li>
<li> More part categories</li>
<li> More part categories and furniture</li>
<li> Richer pricing/stock sync</li>
<li> Smarter compatibility checks between components</li>
<li> AR9/AR10 support And more</li>
<li> AR9/AR10 support and more</li>
</ul>
</section>
</div>