part category grouping UI
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
@@ -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
@@ -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"));
|
||||
|
||||
/***/ })
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@
|
||||
/******/
|
||||
/******/ /* webpack/runtime/getFullHash */
|
||||
/******/ !function() {
|
||||
/******/ __webpack_require__.h = function() { return "d4224caeb46d3863"; }
|
||||
/******/ __webpack_require__.h = function() { return "a6f46b17097acafc"; }
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/global */
|
||||
|
||||
@@ -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"; }
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ }
|
||||
);
|
||||
File diff suppressed because one or more lines are too long
+131
-31
@@ -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're using an early-access prototype of The Armory. Data, pricing, and available parts are still evolving.
|
||||
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">
|
||||
@@ -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'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'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>
|
||||
|
||||
Reference in New Issue
Block a user