builds page concept. rename project
This commit is contained in:
@@ -11,11 +11,6 @@
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/admin/page.js"
|
||||
],
|
||||
"/gunbuilder/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/gunbuilder/page.js"
|
||||
],
|
||||
"/admin/part-role-mappings/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
@@ -31,20 +26,30 @@
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/admin/merchant-category-mappings/page.js"
|
||||
],
|
||||
"/gunbuilder/[categoryId]/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/gunbuilder/[categoryId]/page.js"
|
||||
],
|
||||
"/gunbuilder/[categoryId]/[partId]/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/gunbuilder/[categoryId]/[partId]/page.js"
|
||||
],
|
||||
"/admin/merchants/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/admin/merchants/page.js"
|
||||
],
|
||||
"/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/page.js"
|
||||
],
|
||||
"/builds/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/builds/page.js"
|
||||
],
|
||||
"/builder/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/builder/page.js"
|
||||
],
|
||||
"/_not-found/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/_not-found/page.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
"polyfillFiles": [
|
||||
"static/chunks/polyfills.js"
|
||||
],
|
||||
"devFiles": [],
|
||||
"devFiles": [
|
||||
"static/chunks/react-refresh.js"
|
||||
],
|
||||
"ampDevFiles": [],
|
||||
"lowPriorityFiles": [
|
||||
"static/development/_buildManifest.js",
|
||||
@@ -13,7 +15,16 @@
|
||||
"static/chunks/main-app.js"
|
||||
],
|
||||
"pages": {
|
||||
"/_app": []
|
||||
"/_app": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main.js",
|
||||
"static/chunks/pages/_app.js"
|
||||
],
|
||||
"/_error": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main.js",
|
||||
"static/chunks/pages/_error.js"
|
||||
]
|
||||
},
|
||||
"ampFirstPages": []
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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,8 +1,7 @@
|
||||
{
|
||||
"/_not-found/page": "app/_not-found/page.js",
|
||||
"/page": "app/page.js",
|
||||
"/admin/merchant-category-mappings/page": "app/admin/merchant-category-mappings/page.js",
|
||||
"/gunbuilder/page": "app/gunbuilder/page.js",
|
||||
"/admin/merchants/page": "app/admin/merchants/page.js",
|
||||
"/admin/page": "app/admin/page.js",
|
||||
"/gunbuilder/[categoryId]/[partId]/page": "app/gunbuilder/[categoryId]/[partId]/page.js",
|
||||
"/gunbuilder/[categoryId]/page": "app/gunbuilder/[categoryId]/page.js"
|
||||
"/builds/page": "app/builds/page.js",
|
||||
"/builder/page": "app/builder/page.js"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
self.__BUILD_MANIFEST={"polyfillFiles":["static/chunks/polyfills.js"],"devFiles":[],"ampDevFiles":[],"lowPriorityFiles":["static/development/_buildManifest.js","static/development/_ssgManifest.js"],"rootMainFiles":["static/chunks/webpack.js","static/chunks/main-app.js"],"pages":{"/_app":[]},"ampFirstPages":[]}
|
||||
self.__BUILD_MANIFEST={"polyfillFiles":["static/chunks/polyfills.js"],"devFiles":["static/chunks/react-refresh.js"],"ampDevFiles":[],"lowPriorityFiles":["static/development/_buildManifest.js","static/development/_ssgManifest.js"],"rootMainFiles":["static/chunks/webpack.js","static/chunks/main-app.js"],"pages":{"/_app":["static/chunks/webpack.js","static/chunks/main.js","static/chunks/pages/_app.js"],"/_error":["static/chunks/webpack.js","static/chunks/main.js","static/chunks/pages/_error.js"]},"ampFirstPages":[]}
|
||||
@@ -1 +1,5 @@
|
||||
{}
|
||||
{
|
||||
"/_app": "pages/_app.js",
|
||||
"/_error": "pages/_error.js",
|
||||
"/_document": "pages/_document.js"
|
||||
}
|
||||
@@ -11,6 +11,26 @@ exports.id = "vendor-chunks/@swc";
|
||||
exports.ids = ["vendor-chunks/@swc"];
|
||||
exports.modules = {
|
||||
|
||||
/***/ "./node_modules/@swc/helpers/cjs/_interop_require_default.cjs":
|
||||
/*!********************************************************************!*\
|
||||
!*** ./node_modules/@swc/helpers/cjs/_interop_require_default.cjs ***!
|
||||
\********************************************************************/
|
||||
/***/ ((__unused_webpack_module, exports) => {
|
||||
|
||||
eval("\n\nexports._ = exports._interop_require_default = _interop_require_default;\nfunction _interop_require_default(obj) {\n return obj && obj.__esModule ? obj : { default: obj };\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2Nqcy9faW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQuY2pzIiwibWFwcGluZ3MiOiJBQUFhOztBQUViLFNBQVMsR0FBRyxnQ0FBZ0M7QUFDNUM7QUFDQSwyQ0FBMkM7QUFDM0MiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9ndW5idWlsZGVyLXByb3RvdHlwZS8uL25vZGVfbW9kdWxlcy9Ac3djL2hlbHBlcnMvY2pzL19pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdC5janM/MGQyMyJdLCJzb3VyY2VzQ29udGVudCI6WyJcInVzZSBzdHJpY3RcIjtcblxuZXhwb3J0cy5fID0gZXhwb3J0cy5faW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQgPSBfaW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQ7XG5mdW5jdGlvbiBfaW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQob2JqKSB7XG4gICAgcmV0dXJuIG9iaiAmJiBvYmouX19lc01vZHVsZSA/IG9iaiA6IHsgZGVmYXVsdDogb2JqIH07XG59XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///./node_modules/@swc/helpers/cjs/_interop_require_default.cjs\n");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "./node_modules/@swc/helpers/cjs/_interop_require_wildcard.cjs":
|
||||
/*!*********************************************************************!*\
|
||||
!*** ./node_modules/@swc/helpers/cjs/_interop_require_wildcard.cjs ***!
|
||||
\*********************************************************************/
|
||||
/***/ ((__unused_webpack_module, exports) => {
|
||||
|
||||
eval("\n\nfunction _getRequireWildcardCache(nodeInterop) {\n if (typeof WeakMap !== \"function\") return null;\n\n var cacheBabelInterop = new WeakMap();\n var cacheNodeInterop = new WeakMap();\n\n return (_getRequireWildcardCache = function(nodeInterop) {\n return nodeInterop ? cacheNodeInterop : cacheBabelInterop;\n })(nodeInterop);\n}\nexports._ = exports._interop_require_wildcard = _interop_require_wildcard;\nfunction _interop_require_wildcard(obj, nodeInterop) {\n if (!nodeInterop && obj && obj.__esModule) return obj;\n if (obj === null || typeof obj !== \"object\" && typeof obj !== \"function\") return { default: obj };\n\n var cache = _getRequireWildcardCache(nodeInterop);\n\n if (cache && cache.has(obj)) return cache.get(obj);\n\n var newObj = { __proto__: null };\n var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;\n\n for (var key in obj) {\n if (key !== \"default\" && Object.prototype.hasOwnProperty.call(obj, key)) {\n var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;\n if (desc && (desc.get || desc.set)) Object.defineProperty(newObj, key, desc);\n else newObj[key] = obj[key];\n }\n }\n\n newObj.default = obj;\n\n if (cache) cache.set(obj, newObj);\n\n return newObj;\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2Nqcy9faW50ZXJvcF9yZXF1aXJlX3dpbGRjYXJkLmNqcyIsIm1hcHBpbmdzIjoiQUFBYTs7QUFFYjtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBLEtBQUs7QUFDTDtBQUNBLFNBQVMsR0FBRyxpQ0FBaUM7QUFDN0M7QUFDQTtBQUNBLHVGQUF1Rjs7QUFFdkY7O0FBRUE7O0FBRUEsbUJBQW1CO0FBQ25COztBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBOztBQUVBOztBQUVBO0FBQ0EiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9ndW5idWlsZGVyLXByb3RvdHlwZS8uL25vZGVfbW9kdWxlcy9Ac3djL2hlbHBlcnMvY2pzL19pbnRlcm9wX3JlcXVpcmVfd2lsZGNhcmQuY2pzP2Q1MWMiXSwic291cmNlc0NvbnRlbnQiOlsiXCJ1c2Ugc3RyaWN0XCI7XG5cbmZ1bmN0aW9uIF9nZXRSZXF1aXJlV2lsZGNhcmRDYWNoZShub2RlSW50ZXJvcCkge1xuICAgIGlmICh0eXBlb2YgV2Vha01hcCAhPT0gXCJmdW5jdGlvblwiKSByZXR1cm4gbnVsbDtcblxuICAgIHZhciBjYWNoZUJhYmVsSW50ZXJvcCA9IG5ldyBXZWFrTWFwKCk7XG4gICAgdmFyIGNhY2hlTm9kZUludGVyb3AgPSBuZXcgV2Vha01hcCgpO1xuXG4gICAgcmV0dXJuIChfZ2V0UmVxdWlyZVdpbGRjYXJkQ2FjaGUgPSBmdW5jdGlvbihub2RlSW50ZXJvcCkge1xuICAgICAgICByZXR1cm4gbm9kZUludGVyb3AgPyBjYWNoZU5vZGVJbnRlcm9wIDogY2FjaGVCYWJlbEludGVyb3A7XG4gICAgfSkobm9kZUludGVyb3ApO1xufVxuZXhwb3J0cy5fID0gZXhwb3J0cy5faW50ZXJvcF9yZXF1aXJlX3dpbGRjYXJkID0gX2ludGVyb3BfcmVxdWlyZV93aWxkY2FyZDtcbmZ1bmN0aW9uIF9pbnRlcm9wX3JlcXVpcmVfd2lsZGNhcmQob2JqLCBub2RlSW50ZXJvcCkge1xuICAgIGlmICghbm9kZUludGVyb3AgJiYgb2JqICYmIG9iai5fX2VzTW9kdWxlKSByZXR1cm4gb2JqO1xuICAgIGlmIChvYmogPT09IG51bGwgfHwgdHlwZW9mIG9iaiAhPT0gXCJvYmplY3RcIiAmJiB0eXBlb2Ygb2JqICE9PSBcImZ1bmN0aW9uXCIpIHJldHVybiB7IGRlZmF1bHQ6IG9iaiB9O1xuXG4gICAgdmFyIGNhY2hlID0gX2dldFJlcXVpcmVXaWxkY2FyZENhY2hlKG5vZGVJbnRlcm9wKTtcblxuICAgIGlmIChjYWNoZSAmJiBjYWNoZS5oYXMob2JqKSkgcmV0dXJuIGNhY2hlLmdldChvYmopO1xuXG4gICAgdmFyIG5ld09iaiA9IHsgX19wcm90b19fOiBudWxsIH07XG4gICAgdmFyIGhhc1Byb3BlcnR5RGVzY3JpcHRvciA9IE9iamVjdC5kZWZpbmVQcm9wZXJ0eSAmJiBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yO1xuXG4gICAgZm9yICh2YXIga2V5IGluIG9iaikge1xuICAgICAgICBpZiAoa2V5ICE9PSBcImRlZmF1bHRcIiAmJiBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwob2JqLCBrZXkpKSB7XG4gICAgICAgICAgICB2YXIgZGVzYyA9IGhhc1Byb3BlcnR5RGVzY3JpcHRvciA/IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3Iob2JqLCBrZXkpIDogbnVsbDtcbiAgICAgICAgICAgIGlmIChkZXNjICYmIChkZXNjLmdldCB8fCBkZXNjLnNldCkpIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShuZXdPYmosIGtleSwgZGVzYyk7XG4gICAgICAgICAgICBlbHNlIG5ld09ialtrZXldID0gb2JqW2tleV07XG4gICAgICAgIH1cbiAgICB9XG5cbiAgICBuZXdPYmouZGVmYXVsdCA9IG9iajtcblxuICAgIGlmIChjYWNoZSkgY2FjaGUuc2V0KG9iaiwgbmV3T2JqKTtcblxuICAgIHJldHVybiBuZXdPYmo7XG59XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///./node_modules/@swc/helpers/cjs/_interop_require_wildcard.cjs\n");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js":
|
||||
/*!**************************************************************************!*\
|
||||
!*** ./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js ***!
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -125,7 +125,7 @@
|
||||
/******/
|
||||
/******/ /* webpack/runtime/getFullHash */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.h = () => ("3dc90c63c330aadc")
|
||||
/******/ __webpack_require__.h = () => ("22ee6fa100f81721")
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -192,7 +192,7 @@
|
||||
/******/
|
||||
/******/ /* webpack/runtime/getFullHash */
|
||||
/******/ !function() {
|
||||
/******/ __webpack_require__.h = function() { return "df6e8d5447f13d0a"; }
|
||||
/******/ __webpack_require__.h = function() { return "e7544d75888cd7f5"; }
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/global */
|
||||
|
||||
@@ -575,6 +575,9 @@ video {
|
||||
.mb-1\.5 {
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
.mt-10 {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
.line-clamp-1 {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
@@ -623,6 +626,18 @@ video {
|
||||
.h-3 {
|
||||
height: 0.75rem;
|
||||
}
|
||||
.h-3\.5 {
|
||||
height: 0.875rem;
|
||||
}
|
||||
.h-2 {
|
||||
height: 0.5rem;
|
||||
}
|
||||
.h-6 {
|
||||
height: 1.5rem;
|
||||
}
|
||||
.h-24 {
|
||||
height: 6rem;
|
||||
}
|
||||
.max-h-80 {
|
||||
max-height: 20rem;
|
||||
}
|
||||
@@ -656,6 +671,18 @@ video {
|
||||
.w-3 {
|
||||
width: 0.75rem;
|
||||
}
|
||||
.w-3\.5 {
|
||||
width: 0.875rem;
|
||||
}
|
||||
.w-2 {
|
||||
width: 0.5rem;
|
||||
}
|
||||
.w-6 {
|
||||
width: 1.5rem;
|
||||
}
|
||||
.w-40 {
|
||||
width: 10rem;
|
||||
}
|
||||
.min-w-0 {
|
||||
min-width: 0px;
|
||||
}
|
||||
@@ -705,6 +732,15 @@ video {
|
||||
--tw-translate-y: -50%;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
@keyframes pulse {
|
||||
|
||||
50% {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
.animate-pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
.cursor-not-allowed {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -717,6 +753,9 @@ video {
|
||||
.grid-cols-\[minmax\(0\2c 3fr\)_minmax\(0\2c 1fr\)_minmax\(0\2c 1fr\)_auto\] {
|
||||
grid-template-columns: minmax(0,3fr) minmax(0,1fr) minmax(0,1fr) auto;
|
||||
}
|
||||
.grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -762,6 +801,13 @@ video {
|
||||
.gap-6 {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.gap-x-4 {
|
||||
-moz-column-gap: 1rem;
|
||||
column-gap: 1rem;
|
||||
}
|
||||
.gap-y-2 {
|
||||
row-gap: 0.5rem;
|
||||
}
|
||||
.space-y-1 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
@@ -1004,10 +1050,37 @@ video {
|
||||
.bg-red-700\/50 {
|
||||
background-color: rgb(185 28 28 / 0.5);
|
||||
}
|
||||
.bg-amber-300 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(252 211 77 / var(--tw-bg-opacity));
|
||||
}
|
||||
.bg-gradient-to-r {
|
||||
background-image: linear-gradient(to right, var(--tw-gradient-stops));
|
||||
}
|
||||
.from-amber-400 {
|
||||
--tw-gradient-from: #fbbf24 var(--tw-gradient-from-position);
|
||||
--tw-gradient-to: rgb(251 191 36 / 0) var(--tw-gradient-to-position);
|
||||
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
||||
}
|
||||
.via-amber-200 {
|
||||
--tw-gradient-to: rgb(253 230 138 / 0) var(--tw-gradient-to-position);
|
||||
--tw-gradient-stops: var(--tw-gradient-from), #fde68a var(--tw-gradient-via-position), var(--tw-gradient-to);
|
||||
}
|
||||
.to-amber-400 {
|
||||
--tw-gradient-to: #fbbf24 var(--tw-gradient-to-position);
|
||||
}
|
||||
.bg-clip-text {
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
}
|
||||
.object-contain {
|
||||
-o-object-fit: contain;
|
||||
object-fit: contain;
|
||||
}
|
||||
.object-cover {
|
||||
-o-object-fit: cover;
|
||||
object-fit: cover;
|
||||
}
|
||||
.p-1 {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
@@ -1173,6 +1246,10 @@ video {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
.text-4xl {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -1182,6 +1259,9 @@ video {
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
.font-extrabold {
|
||||
font-weight: 800;
|
||||
}
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -1314,6 +1394,9 @@ video {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(82 82 91 / var(--tw-text-opacity));
|
||||
}
|
||||
.text-transparent {
|
||||
color: transparent;
|
||||
}
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
@@ -1343,6 +1426,10 @@ video {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.drop-shadow-\[0_2px_6px_rgba\(255\2c 193\2c 7\2c 0\.25\)\] {
|
||||
--tw-drop-shadow: drop-shadow(0 2px 6px rgba(255,193,7,0.25));
|
||||
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||
}
|
||||
.filter {
|
||||
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||
}
|
||||
@@ -1539,6 +1626,10 @@ body {
|
||||
|
||||
@media (min-width: 640px) {
|
||||
|
||||
.sm\:block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sm\:inline {
|
||||
display: inline;
|
||||
}
|
||||
@@ -1626,6 +1717,14 @@ body {
|
||||
grid-template-columns: minmax(0,3fr) minmax(0,1fr) minmax(0,1fr) auto;
|
||||
}
|
||||
|
||||
.md\:grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.md\:grid-cols-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.md\:flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -1650,6 +1749,10 @@ body {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.md\:gap-3 {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.md\:p-4 {
|
||||
padding: 1rem;
|
||||
}
|
||||
@@ -1685,6 +1788,16 @@ body {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.md\:text-5xl {
|
||||
font-size: 3rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.md\:text-xl {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
|
||||
@@ -1 +1 @@
|
||||
self.__BUILD_MANIFEST = {__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},sortedPages:["\u002F_app"]};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()
|
||||
self.__BUILD_MANIFEST = {__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/_error":["static\u002Fchunks\u002Fpages\u002F_error.js"],sortedPages:["\u002F_app","\u002F_error"]};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()
|
||||
+53
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
// File: /Users/sean/Dev/gunbuilder-prototype/app/gunbuilder/page.tsx
|
||||
import * as entry from '../../../../app/gunbuilder/page.js'
|
||||
import * as entry from '../../../../app/builder/page.jsx'
|
||||
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
|
||||
|
||||
type TEntry = typeof import('../../../../app/gunbuilder/page.js')
|
||||
type TEntry = typeof import('../../../../app/builder/page.jsx')
|
||||
|
||||
// Check that the entry is a valid entry
|
||||
checkFields<Diff<{
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { CATEGORIES } from "@/data/gunbuilderParts";
|
||||
import type { CategoryId } from "@/types/gunbuilder";
|
||||
import {
|
||||
getRetailerOffers,
|
||||
type RetailerOffer,
|
||||
} from "./data";
|
||||
|
||||
type GunbuilderProductFromApi = {
|
||||
id: string; // backend UUID string
|
||||
name: string;
|
||||
brand: string;
|
||||
platform: string;
|
||||
partRole: string;
|
||||
price: number | null;
|
||||
mainImageUrl: string | null;
|
||||
buyUrl: string | null;
|
||||
};
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
|
||||
|
||||
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
|
||||
|
||||
const [product, setProduct] = useState<GunbuilderProductFromApi | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [offers, setOffers] = useState<RetailerOffer[]>([]);
|
||||
const [offersLoading, setOffersLoading] = useState(true);
|
||||
const [offersError, setOffersError] = useState<string | null>(null);
|
||||
|
||||
const category = useMemo(
|
||||
() => CATEGORIES.find((c) => c.id === categoryId),
|
||||
[categoryId],
|
||||
);
|
||||
|
||||
// 1) Load product (same as before)
|
||||
useEffect(() => {
|
||||
if (!partId) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
async function fetchProduct() {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// For now, pull the full AR-15 list and find by UUID.
|
||||
// We can optimize with a dedicated /api/products/{uuid} later.
|
||||
const url = `${API_BASE_URL}/api/products/gunbuilder?platform=AR-15`;
|
||||
const res = await fetch(url, { signal: controller.signal });
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to load product (${res.status})`);
|
||||
}
|
||||
|
||||
const data: GunbuilderProductFromApi[] = await res.json();
|
||||
const found = data.find((p) => p.id === partId) ?? null;
|
||||
|
||||
if (!found) {
|
||||
setError("Product not found");
|
||||
}
|
||||
|
||||
setProduct(found);
|
||||
} catch (err: any) {
|
||||
if (err.name === "AbortError") return;
|
||||
setError(err.message ?? "Failed to load product");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchProduct();
|
||||
|
||||
return () => controller.abort();
|
||||
}, [partId]);
|
||||
|
||||
// 2) Load offers for this product from Ballistic backend
|
||||
useEffect(() => {
|
||||
if (!partId) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function fetchOffers() {
|
||||
try {
|
||||
setOffersLoading(true);
|
||||
setOffersError(null);
|
||||
|
||||
const data = await getRetailerOffers(partId);
|
||||
|
||||
if (!cancelled) {
|
||||
setOffers(data);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (!cancelled) {
|
||||
setOffersError(err.message ?? "Failed to load retailer offers");
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setOffersLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchOffers();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [partId]);
|
||||
|
||||
// Best price: prefer live offers, fall back to summary product.price
|
||||
const bestPrice = useMemo(() => {
|
||||
if (offers.length > 0) {
|
||||
return offers[0].price;
|
||||
}
|
||||
return product?.price ?? null;
|
||||
}, [offers, product]);
|
||||
|
||||
if (!category) {
|
||||
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">
|
||||
<h1 className="text-2xl font-semibold mb-4">Category Not Found</h1>
|
||||
<Link
|
||||
href="/gunbuilder"
|
||||
className="text-amber-300 hover:text-amber-200 underline"
|
||||
>
|
||||
Return to Gunbuilder
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-black text-zinc-50">
|
||||
<div className="mx-auto max-w-5xl px-4 py-6 lg:py-10">
|
||||
{/* Breadcrumbs */}
|
||||
<nav className="mb-4 text-xs text-zinc-500">
|
||||
<Link
|
||||
href="/gunbuilder"
|
||||
className="hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
Gunbuilder
|
||||
</Link>
|
||||
<span className="mx-1">/</span>
|
||||
<Link
|
||||
href={`/gunbuilder/${categoryId}`}
|
||||
className="hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
{category.name}
|
||||
</Link>
|
||||
{product && (
|
||||
<>
|
||||
<span className="mx-1">/</span>
|
||||
<span className="text-zinc-300">{product.name}</span>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{loading ? (
|
||||
<p className="text-sm text-zinc-500">Loading product…</p>
|
||||
) : error || !product ? (
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold mb-2">Product Unavailable</h1>
|
||||
<p className="text-sm text-zinc-500 mb-4">
|
||||
{error ?? "We couldn’t find this product."}
|
||||
</p>
|
||||
<Link
|
||||
href={`/gunbuilder/${categoryId}`}
|
||||
className="text-amber-300 hover:text-amber-200 underline text-sm"
|
||||
>
|
||||
Back to {category.name} parts
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-6 md:grid-cols-[minmax(0,2fr)_minmax(0,1.3fr)] items-start">
|
||||
{/* Left: image + meta */}
|
||||
<div className="space-y-4">
|
||||
{product.mainImageUrl && (
|
||||
<div className="overflow-hidden rounded-lg border border-zinc-800 bg-zinc-950">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={product.mainImageUrl}
|
||||
alt={product.name}
|
||||
className="w-full object-contain max-h-80 bg-zinc-950"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-500">
|
||||
{product.brand}
|
||||
</div>
|
||||
<h1 className="text-2xl md:text-3xl font-semibold tracking-tight">
|
||||
{product.name}
|
||||
</h1>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||
<p className="text-zinc-500">
|
||||
Platform:{" "}
|
||||
<span className="text-zinc-300">{product.platform}</span>
|
||||
</p>
|
||||
<p className="text-zinc-500">
|
||||
Role:{" "}
|
||||
<span className="text-zinc-300">{product.partRole}</span>
|
||||
</p>
|
||||
<p className="text-zinc-500">
|
||||
Category:{" "}
|
||||
<span className="text-zinc-300">{category.name}</span>
|
||||
</p>
|
||||
<p className="text-zinc-500">
|
||||
Offers:{" "}
|
||||
<span className="text-zinc-300">
|
||||
{offers.length > 0
|
||||
? `${offers.length} live offer${
|
||||
offers.length === 1 ? "" : "s"
|
||||
}`
|
||||
: "No live offers yet"}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Placeholder content until long-form fields are wired up */}
|
||||
<section className="mt-4 space-y-3 text-sm text-zinc-300">
|
||||
<h2 className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-500">
|
||||
Product Overview
|
||||
</h2>
|
||||
<p className="text-zinc-400">
|
||||
This is placeholder copy for the product overview. Once
|
||||
the Ballistic backend exposes richer metadata (short
|
||||
description, feature bullets, etc.), we'll swap this
|
||||
text out and surface the real content here.
|
||||
</p>
|
||||
<p className="text-zinc-500 text-xs">
|
||||
Use this section to highlight what makes this part worth
|
||||
a slot in your build: materials, intended use
|
||||
(duty/range/competition), and any standout features.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mt-4 rounded-lg border border-zinc-800 bg-zinc-950/70 p-3">
|
||||
<h2 className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-500 mb-2">
|
||||
Quick Specs (Placeholder)
|
||||
</h2>
|
||||
<dl className="grid grid-cols-2 gap-x-4 gap-y-2 text-xs text-zinc-400">
|
||||
<div>
|
||||
<dt className="text-zinc-500">Configuration</dt>
|
||||
<dd className="text-zinc-200">
|
||||
TBD (Stripped / Complete / Kit)
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-zinc-500">Caliber</dt>
|
||||
<dd className="text-zinc-200">TBD from feed</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-zinc-500">Finish</dt>
|
||||
<dd className="text-zinc-200">TBD from merchant data</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-zinc-500">Weight</dt>
|
||||
<dd className="text-zinc-200">TBD (oz)</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p className="mt-2 text-[11px] text-zinc-500">
|
||||
Specs are placeholders for now. As we normalize more
|
||||
structured attributes in the importer, this block will
|
||||
auto-populate per product.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Right: pricing + actions */}
|
||||
<aside className="rounded-lg border border-zinc-800 bg-zinc-950/70 p-4 space-y-4">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-zinc-500 mb-1">
|
||||
Price
|
||||
</p>
|
||||
<p className="text-2xl font-semibold text-amber-300">
|
||||
{bestPrice && bestPrice > 0
|
||||
? `$${bestPrice.toFixed(2)}`
|
||||
: "—"}
|
||||
</p>
|
||||
{!bestPrice && (
|
||||
<p className="mt-1 text-xs text-zinc-500">
|
||||
Pricing not available yet. Live pricing will appear once
|
||||
offers are imported.
|
||||
</p>
|
||||
)}
|
||||
{offers.length > 0 && (
|
||||
<p className="mt-1 text-[11px] text-zinc-500">
|
||||
Showing the best available price across all live
|
||||
offers for this part.
|
||||
</p>
|
||||
)}
|
||||
{offers.length > 1 && (
|
||||
<p className="mt-1 text-[11px] text-zinc-500">
|
||||
Price range across retailers:{" "}
|
||||
<span className="text-zinc-300">
|
||||
${Math.min(...offers.map(o => o.price)).toFixed(2)} – ${Math.max(...offers.map(o => o.price)).toFixed(2)}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Offers list */}
|
||||
<div className="space-y-2">
|
||||
{offersLoading && (
|
||||
<p className="text-xs text-zinc-500">Loading offers…</p>
|
||||
)}
|
||||
{offersError && (
|
||||
<p className="text-xs text-red-400">{offersError}</p>
|
||||
)}
|
||||
{!offersLoading && !offersError && offers.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
{offers.map((offer) => (
|
||||
<a
|
||||
key={offer.id}
|
||||
href={offer.affiliateUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center justify-between rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-xs hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
<span className="text-zinc-200">
|
||||
{offer.retailerName}
|
||||
</span>
|
||||
<span className="font-semibold text-amber-300">
|
||||
${offer.price.toFixed(2)}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!offersLoading &&
|
||||
!offersError &&
|
||||
offers.length === 0 && (
|
||||
<p className="text-xs text-zinc-500">
|
||||
No live retailer offers yet. As feeds are imported,
|
||||
merchants and pricing will show up here.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Link
|
||||
href={`/gunbuilder?select=${categoryId}:${product.id}`}
|
||||
className="block w-full rounded-md bg-amber-400 text-black text-sm font-semibold text-center py-2.5 hover:bg-amber-300 transition-colors"
|
||||
>
|
||||
Add to Build
|
||||
</Link>
|
||||
|
||||
{product.buyUrl ? (
|
||||
<a
|
||||
href={product.buyUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="block w-full rounded-md border border-zinc-700 bg-zinc-900 text-xs font-medium text-zinc-200 text-center py-2 hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
View on Merchant Site
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="block w-full rounded-md border border-zinc-800 bg-zinc-900/60 text-xs font-medium text-zinc-500 text-center py-2 cursor-not-allowed"
|
||||
>
|
||||
External link coming soon
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{/* Lower content: builder-focused helper copy (placeholder) */}
|
||||
<section className="mt-10 grid gap-6 md:grid-cols-3 text-sm md:grid-cols-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-500">
|
||||
Why We Like This Part
|
||||
</h2>
|
||||
<p className="text-zinc-400">
|
||||
Drop in a short blurb here about what makes this part
|
||||
worth picking over similar options — think reliability,
|
||||
track record, and value for the money.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-500">
|
||||
Best For
|
||||
</h2>
|
||||
<p className="text-zinc-400">
|
||||
Use this block to call out ideal use cases:{" "}
|
||||
<span className="text-zinc-200">
|
||||
duty rifle, home defense, range toy, competition, night
|
||||
work
|
||||
</span>
|
||||
, etc.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-500">
|
||||
Builder Notes
|
||||
</h2>
|
||||
<p className="text-zinc-400">
|
||||
Add any compatibility quirks or install tips here once
|
||||
the compatibility engine is wired up — gas system length,
|
||||
buffer recommendations, known fitment notes, and more.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-500">
|
||||
Compatibility
|
||||
</h2>
|
||||
<div className="inline-flex items-center gap-2 rounded-md border border-amber-400/30 bg-amber-400/10 px-2 py-1 text-[11px] text-amber-300">
|
||||
<span className="inline-block h-2 w-2 rounded-full bg-amber-300 animate-pulse"></span>
|
||||
Compatibility engine coming online soon
|
||||
</div>
|
||||
<p className="text-zinc-500 text-xs">
|
||||
Fitment checks, caliber validation, buffer/gas system logic, and platform rules will appear here once the engine is active.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { CATEGORIES } from "@/data/gunbuilderParts";
|
||||
import type { CategoryId, Part } from "@/types/gunbuilder";
|
||||
import { PART_ROLE_TO_CATEGORY } from "@/data/partRoleMappings";
|
||||
import { Pencil, Link2 } from "lucide-react";
|
||||
|
||||
type GunbuilderProductFromApi = {
|
||||
id: number; // backend numeric id
|
||||
@@ -598,17 +599,21 @@ export default function GunbuilderPage() {
|
||||
<>
|
||||
<Link
|
||||
href={`/gunbuilder/${category.id}`}
|
||||
className="hidden md:inline-flex rounded-md border border-zinc-700 bg-zinc-900/70 px-2.5 py-1 text-[0.7rem] font-medium text-zinc-300 hover:bg-zinc-800 hover:border-zinc-600 transition-colors"
|
||||
className="hidden md:inline-flex items-center justify-center rounded-md border border-zinc-700 bg-zinc-900/70 px-2 py-1 hover:bg-zinc-800 hover:border-zinc-600 transition-colors"
|
||||
aria-label="Change part"
|
||||
title="Change part"
|
||||
>
|
||||
Change Part
|
||||
<Pencil className="h-3.5 w-3.5 text-zinc-300" />
|
||||
</Link>
|
||||
<a
|
||||
href={selectedPart.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex rounded-md bg-amber-400 px-2.5 py-1 text-[0.7rem] font-semibold text-black hover:bg-amber-300 transition-colors"
|
||||
className="inline-flex items-center justify-center rounded-md bg-amber-400 px-2 py-1 hover:bg-amber-300 transition-colors"
|
||||
aria-label="Buy part"
|
||||
title="Buy part"
|
||||
>
|
||||
Buy
|
||||
<Link2 className="h-3.5 w-3.5 text-black" />
|
||||
</a>
|
||||
</>
|
||||
) : hasParts ? (
|
||||
@@ -0,0 +1,366 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { CATEGORIES } from "@/data/gunbuilderParts";
|
||||
import type { CategoryId } from "@/types/gunbuilder";
|
||||
import {
|
||||
getRetailerOffers,
|
||||
type RetailerOffer,
|
||||
} from "./data";
|
||||
|
||||
type GunbuilderProductFromApi = {
|
||||
id: string; // backend UUID string
|
||||
name: string;
|
||||
brand: string;
|
||||
platform: string;
|
||||
partRole: string;
|
||||
price: number | null;
|
||||
mainImageUrl: string | null;
|
||||
buyUrl: string | null;
|
||||
};
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
|
||||
|
||||
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
|
||||
|
||||
const [product, setProduct] = useState<GunbuilderProductFromApi | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [offers, setOffers] = useState<RetailerOffer[]>([]);
|
||||
const [offersLoading, setOffersLoading] = useState(true);
|
||||
const [offersError, setOffersError] = useState<string | null>(null);
|
||||
|
||||
const category = useMemo(
|
||||
() => CATEGORIES.find((c) => c.id === categoryId),
|
||||
[categoryId],
|
||||
);
|
||||
|
||||
// 1) Load product (same as before)
|
||||
useEffect(() => {
|
||||
if (!partId) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
async function fetchProduct() {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// For now, pull the full AR-15 list and find by UUID.
|
||||
// We can optimize with a dedicated /api/products/{uuid} later.
|
||||
const url = `${API_BASE_URL}/api/products/gunbuilder?platform=AR-15`;
|
||||
const res = await fetch(url, { signal: controller.signal });
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to load product (${res.status})`);
|
||||
}
|
||||
|
||||
const data: GunbuilderProductFromApi[] = await res.json();
|
||||
const found = data.find((p) => p.id === partId) ?? null;
|
||||
|
||||
if (!found) {
|
||||
setError("Product not found");
|
||||
}
|
||||
|
||||
setProduct(found);
|
||||
} catch (err: any) {
|
||||
if (err.name === "AbortError") return;
|
||||
setError(err.message ?? "Failed to load product");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchProduct();
|
||||
|
||||
return () => controller.abort();
|
||||
}, [partId]);
|
||||
|
||||
// 2) Load offers for this product from Ballistic backend
|
||||
useEffect(() => {
|
||||
if (!partId) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function fetchOffers() {
|
||||
try {
|
||||
setOffersLoading(true);
|
||||
setOffersError(null);
|
||||
|
||||
const data = await getRetailerOffers(partId);
|
||||
|
||||
if (!cancelled) {
|
||||
setOffers(data);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (!cancelled) {
|
||||
setOffersError(err.message ?? "Failed to load retailer offers");
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setOffersLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchOffers();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [partId]);
|
||||
|
||||
// Best price: prefer live offers, fall back to summary product.price
|
||||
const bestPrice = useMemo(() => {
|
||||
if (offers.length > 0) {
|
||||
return offers[0].price;
|
||||
}
|
||||
return product?.price ?? null;
|
||||
}, [offers, product]);
|
||||
|
||||
if (!category) {
|
||||
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">
|
||||
<h1 className="text-2xl font-semibold mb-4">Category Not Found</h1>
|
||||
<Link
|
||||
href="/gunbuilder"
|
||||
className="text-amber-300 hover:text-amber-200 underline"
|
||||
>
|
||||
Return to Gunbuilder
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-black text-zinc-50">
|
||||
<div className="mx-auto max-w-5xl px-4 py-6 lg:py-10">
|
||||
{/* Breadcrumbs */}
|
||||
<nav className="mb-4 text-xs text-zinc-500">
|
||||
<Link
|
||||
href="/gunbuilder"
|
||||
className="hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
Gunbuilder
|
||||
</Link>
|
||||
<span className="mx-1">/</span>
|
||||
<Link
|
||||
href={`/gunbuilder/${categoryId}`}
|
||||
className="hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
{category.name}
|
||||
</Link>
|
||||
{product && (
|
||||
<>
|
||||
<span className="mx-1">/</span>
|
||||
<span className="text-zinc-300">{product.name}</span>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{loading ? (
|
||||
<p className="text-sm text-zinc-500">Loading product…</p>
|
||||
) : error || !product ? (
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold mb-2">Product Unavailable</h1>
|
||||
<p className="text-sm text-zinc-500 mb-4">
|
||||
{error ?? "We couldn’t find this product."}
|
||||
</p>
|
||||
<Link
|
||||
href={`/gunbuilder/${categoryId}`}
|
||||
className="text-amber-300 hover:text-amber-200 underline text-sm"
|
||||
>
|
||||
Back to {category.name} parts
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-6 md:grid-cols-[minmax(0,2fr)_minmax(0,1.3fr)] items-start">
|
||||
{/* Left: image + meta */}
|
||||
<div className="space-y-4">
|
||||
{product.mainImageUrl && (
|
||||
<div className="overflow-hidden rounded-lg border border-zinc-800 bg-zinc-950">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={product.mainImageUrl}
|
||||
alt={product.name}
|
||||
className="w-full object-contain max-h-80 bg-zinc-950"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-500">
|
||||
{product.brand}
|
||||
</div>
|
||||
<h1 className="text-2xl md:text-3xl font-semibold tracking-tight">
|
||||
{product.name}
|
||||
</h1>
|
||||
<p className="text-xs text-zinc-500">
|
||||
Platform:{" "}
|
||||
<span className="text-zinc-300">{product.platform}</span>
|
||||
</p>
|
||||
<p className="text-xs text-zinc-500">
|
||||
Role: <span className="text-zinc-300">{product.partRole}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Right: pricing + actions */}
|
||||
<aside className="rounded-lg border border-zinc-800 bg-zinc-950/70 p-4 space-y-4">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-zinc-500 mb-1">
|
||||
Price
|
||||
</p>
|
||||
<p className="text-2xl font-semibold text-amber-300">
|
||||
{bestPrice && bestPrice > 0
|
||||
? `$${bestPrice.toFixed(2)}`
|
||||
: "—"}
|
||||
</p>
|
||||
{!bestPrice && (
|
||||
<p className="mt-1 text-xs text-zinc-500">
|
||||
Pricing not available yet. Live pricing will appear once
|
||||
offers are imported.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Offers list */}
|
||||
<div className="space-y-2">
|
||||
{offersLoading && (
|
||||
<p className="text-xs text-zinc-500">Loading offers…</p>
|
||||
)}
|
||||
{offersError && (
|
||||
<p className="text-xs text-red-400">
|
||||
{offersError}
|
||||
</p>
|
||||
)}
|
||||
{!offersLoading && !offersError && offers.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
{offers.map((offer) => (
|
||||
<a
|
||||
key={offer.id}
|
||||
href={offer.affiliateUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center justify-between rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-xs hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
<span className="text-zinc-200">
|
||||
{offer.retailerName}
|
||||
</span>
|
||||
<span className="font-semibold text-amber-300">
|
||||
${offer.price.toFixed(2)}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Link
|
||||
href={`/gunbuilder?select=${categoryId}:${product.id}`}
|
||||
className="block w-full rounded-md bg-amber-400 text-black text-sm font-semibold text-center py-2.5 hover:bg-amber-300 transition-colors"
|
||||
>
|
||||
Add to Build
|
||||
</Link>
|
||||
|
||||
{product.buyUrl ? (
|
||||
<a
|
||||
href={product.buyUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="block w-full rounded-md border border-zinc-700 bg-zinc-900 text-xs font-medium text-zinc-200 text-center py-2 hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
View on Merchant Site
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="block w-full rounded-md border border-zinc-800 bg-zinc-900/60 text-xs font-medium text-zinc-500 text-center py-2 cursor-not-allowed"
|
||||
>
|
||||
External link coming soon
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+8
-1
@@ -1,4 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import "./globals.css";
|
||||
import type { ReactNode } from "react";
|
||||
@@ -6,6 +5,14 @@ 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">
|
||||
|
||||
+12
-12
@@ -5,27 +5,27 @@ export default function HomePage() {
|
||||
<main className="min-h-screen bg-black text-zinc-50 flex items-center justify-center px-4">
|
||||
<div className="max-w-xl text-center">
|
||||
<p className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-500">
|
||||
Shadow Standard Co.
|
||||
For Those Who Build With Intent.
|
||||
</p>
|
||||
<h1 className="mt-2 text-3xl md:text-4xl font-semibold tracking-tight">
|
||||
The Build Bench
|
||||
<h1
|
||||
className="mt-2 text-4xl md:text-5xl font-extrabold tracking-tight
|
||||
bg-gradient-to-r from-amber-400 via-amber-200 to-amber-400
|
||||
text-transparent uppercase bg-clip-text drop-shadow-[0_2px_6px_rgba(255,193,7,0.25)]"
|
||||
>
|
||||
Battl Builder
|
||||
</h1>
|
||||
<p className="mt-3 text-sm md:text-base text-zinc-400">
|
||||
Shadow Standard’s Armory Build Bench isn’t a catalog — it’s a
|
||||
workbench for people who take their rifles, and their craft,
|
||||
seriously. We pull live parts and pricing from the brands that matter,
|
||||
lay everything out with purpose, and let you shape a rifle the same
|
||||
way you’d shape a hunt or a mission: one smart choice at a time. Track
|
||||
your build cost, swap components on the fly, save your setup, and
|
||||
share it with the crew. No noise. No gimmicks. Just clean information
|
||||
and the tools to build something worth carrying.
|
||||
Battl Builder is your command center for rifle builds. We surface live
|
||||
parts and hard-use brands, giving you a mission-ready workbench to
|
||||
design smarter, track cost, swap components, and save setups. No
|
||||
fluff. Just clean, trustworthy data.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<Link
|
||||
href="/gunbuilder"
|
||||
className="inline-flex items-center justify-center rounded-md border border-amber-400/70 bg-amber-400/10 px-4 py-2 text-sm font-medium text-amber-200 hover:bg-amber-400/15"
|
||||
>
|
||||
Open The Build Bench
|
||||
Launch The Builder
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import type { PriceHistoryPoint } from "@/app/gunbuilder/[categoryId]/[partId]/data";
|
||||
import type { PriceHistoryPoint } from "@/app/builder/[categoryId]/[partId]/data";
|
||||
|
||||
interface PricingHistoryGraphProps {
|
||||
data: PriceHistoryPoint[];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import type { RetailerOffer } from "@/app/gunbuilder/[categoryId]/[partId]/data";
|
||||
import type { RetailerOffer } from "@/app/builder/[categoryId]/[partId]/data";
|
||||
|
||||
interface RetailersListProps {
|
||||
retailers: RetailerOffer[];
|
||||
|
||||
@@ -36,7 +36,7 @@ export function TopNav() {
|
||||
href="/"
|
||||
className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-400"
|
||||
>
|
||||
The Build Bench
|
||||
Battl Builder
|
||||
</Link>
|
||||
|
||||
{/* Center: main nav (Builder / Admin) */}
|
||||
|
||||
Generated
+10
@@ -9,6 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.555.0",
|
||||
"next": "14.2.3",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
@@ -3830,6 +3831,15 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.555.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.555.0.tgz",
|
||||
"integrity": "sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.555.0",
|
||||
"next": "14.2.3",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
|
||||
Reference in New Issue
Block a user