8 Commits

Author SHA1 Message Date
sean 9e5b86967b new admin dashbioard and mapping page 2025-12-07 16:18:28 -05:00
sean 6cf3ea7c94 see /admin/mapping page. 2025-12-07 09:57:46 -05:00
sean caf4817d12 role-mapping-fixes 2025-12-06 20:08:00 -05:00
sean 05b30d8682 fixing url issues with renaming 2025-12-06 09:16:37 -05:00
dstrawsb dbcd06d192 fixing conflicts 2025-12-05 22:25:55 -05:00
sean bab21e4ee3 builds page concept. rename project 2025-12-05 20:14:05 -05:00
sean 2fec870302 new builder layout and filters. pagination 2025-12-05 07:26:27 -05:00
sean cb16698940 fuck if i know 2025-12-04 15:06:57 -05:00
126 changed files with 3973 additions and 10632 deletions
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EditMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="261566a2-3672-4898-b754-916bbdb2063b" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/next-env.d.ts" beforeDir="false" afterPath="$PROJECT_DIR$/next-env.d.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/tsconfig.json" beforeDir="false" afterPath="$PROJECT_DIR$/tsconfig.json" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 1
}</component>
<component name="ProjectId" id="361l08WqyxLtDro87xvcOWn3FhH" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;main&quot;,
&quot;ignore.virus.scanning.warn.message&quot;: &quot;true&quot;,
&quot;js.debugger.nextJs.config.created.client&quot;: &quot;true&quot;,
&quot;js.debugger.nextJs.config.created.server&quot;: &quot;true&quot;,
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;//wsl.localhost/Arch/home/don/code/shadow-gunbuilder-ai-proto&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</component>
<component name="RunManager" selected="npm.Next.js: server-side">
<configuration name="Next.js: debug client-side" type="JavascriptDebugType" uri="http://localhost:3000/">
<method v="2" />
</configuration>
<configuration name="Next.js: server-side" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-jdk-9823dce3aa75-bf35d07a577b-intellij.indexing.shared.core-IU-252.27397.103" />
<option value="bundled-js-predefined-d6986cc7102b-3aa1da707db6-JavaScript-IU-252.27397.103" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="261566a2-3672-4898-b754-916bbdb2063b" name="Changes" comment="" />
<created>1764181733358</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1764181733358</updated>
<workItem from="1764181734592" duration="78000" />
<workItem from="1764181824336" duration="9367000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="github-copilot-workspace">
<instructionFileLocations>
<option value=".github/instructions" />
</instructionFileLocations>
<promptFileLocations>
<option value=".github/prompts" />
</promptFileLocations>
</component>
</project>
-20
View File
@@ -1,20 +0,0 @@
{
"pages": {
"/gunbuilder/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/gunbuilder/page.js"
],
"/layout": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/css/app/layout.css",
"static/chunks/app/layout.js"
],
"/_not-found/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/_not-found/page.js"
]
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"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": []
}
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.
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
View File
@@ -1 +0,0 @@
{"type": "commonjs"}
-1
View File
@@ -1 +0,0 @@
{}
-4
View File
@@ -1,4 +0,0 @@
{
"/_not-found/page": "app/_not-found/page.js",
"/gunbuilder/page": "app/gunbuilder/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 +0,0 @@
self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST="[]"
@@ -1 +0,0 @@
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":[]}
-6
View File
@@ -1,6 +0,0 @@
{
"version": 3,
"middleware": {},
"functions": {},
"sortedMiddleware": []
}
@@ -1 +0,0 @@
self.__REACT_LOADABLE_MANIFEST="{}"
-1
View File
@@ -1 +0,0 @@
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{},\"appUsingSizeAdjust\":false,\"pagesUsingSizeAdjust\":false}"
-1
View File
@@ -1 +0,0 @@
{"pages":{},"app":{},"appUsingSizeAdjust":false,"pagesUsingSizeAdjust":false}
-1
View File
@@ -1 +0,0 @@
{}
@@ -1 +0,0 @@
self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"1xFGN5nb3mbeAQPPAywQxDx1Ue0O/Rg+NJlfm52KEi4=\"\n}"
@@ -1,5 +0,0 @@
{
"node": {},
"edge": {},
"encryptionKey": "1xFGN5nb3mbeAQPPAywQxDx1Ue0O/Rg+NJlfm52KEi4="
}
-75
View File
@@ -1,75 +0,0 @@
"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/).
*/
exports.id = "vendor-chunks/@swc";
exports.ids = ["vendor-chunks/@swc"];
exports.modules = {
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js":
/*!**************************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js ***!
\**************************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _class_private_field_loose_base),\n/* harmony export */ _class_private_field_loose_base: () => (/* binding */ _class_private_field_loose_base)\n/* harmony export */ });\nfunction _class_private_field_loose_base(receiver, privateKey) {\n if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) {\n throw new TypeError(\"attempted to use private field on non-instance\");\n }\n\n return receiver;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9fY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9iYXNlLmpzIiwibWFwcGluZ3MiOiI7Ozs7O0FBQU87QUFDUDtBQUNBO0FBQ0E7O0FBRUE7QUFDQTtBQUNnRCIsInNvdXJjZXMiOlsid2VicGFjazovL2d1bmJ1aWxkZXItcHJvdG90eXBlLy4vbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2NsYXNzX3ByaXZhdGVfZmllbGRfbG9vc2VfYmFzZS5qcz81MTYwIl0sInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBmdW5jdGlvbiBfY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9iYXNlKHJlY2VpdmVyLCBwcml2YXRlS2V5KSB7XG4gICAgaWYgKCFPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwocmVjZWl2ZXIsIHByaXZhdGVLZXkpKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoXCJhdHRlbXB0ZWQgdG8gdXNlIHByaXZhdGUgZmllbGQgb24gbm9uLWluc3RhbmNlXCIpO1xuICAgIH1cblxuICAgIHJldHVybiByZWNlaXZlcjtcbn1cbmV4cG9ydCB7IF9jbGFzc19wcml2YXRlX2ZpZWxkX2xvb3NlX2Jhc2UgYXMgXyB9O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_base.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_key.js":
/*!*************************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_class_private_field_loose_key.js ***!
\*************************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _class_private_field_loose_key),\n/* harmony export */ _class_private_field_loose_key: () => (/* binding */ _class_private_field_loose_key)\n/* harmony export */ });\nvar id = 0;\n\nfunction _class_private_field_loose_key(name) {\n return \"__private_\" + id++ + \"_\" + name;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9fY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9rZXkuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBQTs7QUFFTztBQUNQO0FBQ0E7QUFDK0MiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9ndW5idWlsZGVyLXByb3RvdHlwZS8uL25vZGVfbW9kdWxlcy9Ac3djL2hlbHBlcnMvZXNtL19jbGFzc19wcml2YXRlX2ZpZWxkX2xvb3NlX2tleS5qcz81ODI1Il0sInNvdXJjZXNDb250ZW50IjpbInZhciBpZCA9IDA7XG5cbmV4cG9ydCBmdW5jdGlvbiBfY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9rZXkobmFtZSkge1xuICAgIHJldHVybiBcIl9fcHJpdmF0ZV9cIiArIGlkKysgKyBcIl9cIiArIG5hbWU7XG59XG5leHBvcnQgeyBfY2xhc3NfcHJpdmF0ZV9maWVsZF9sb29zZV9rZXkgYXMgXyB9O1xuIl0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_class_private_field_loose_key.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_interop_require_default.js":
/*!*******************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_interop_require_default.js ***!
\*******************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _interop_require_default),\n/* harmony export */ _interop_require_default: () => (/* binding */ _interop_require_default)\n/* harmony export */ });\nfunction _interop_require_default(obj) {\n return obj && obj.__esModule ? obj : { default: obj };\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBTztBQUNQLDJDQUEyQztBQUMzQztBQUN5QyIsInNvdXJjZXMiOlsid2VicGFjazovL2d1bmJ1aWxkZXItcHJvdG90eXBlLy4vbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2ludGVyb3BfcmVxdWlyZV9kZWZhdWx0LmpzP2FiZTYiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGZ1bmN0aW9uIF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdChvYmopIHtcbiAgICByZXR1cm4gb2JqICYmIG9iai5fX2VzTW9kdWxlID8gb2JqIDogeyBkZWZhdWx0OiBvYmogfTtcbn1cbmV4cG9ydCB7IF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdCBhcyBfIH07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_interop_require_default.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_interop_require_wildcard.js":
/*!********************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_interop_require_wildcard.js ***!
\********************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _interop_require_wildcard),\n/* harmony export */ _interop_require_wildcard: () => (/* binding */ _interop_require_wildcard)\n/* harmony export */ });\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}\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\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX3dpbGRjYXJkLmpzIiwibWFwcGluZ3MiOiI7Ozs7O0FBQUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQSxLQUFLO0FBQ0w7QUFDTztBQUNQO0FBQ0EsdUZBQXVGOztBQUV2Rjs7QUFFQTs7QUFFQSxtQkFBbUI7QUFDbkI7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7O0FBRUE7O0FBRUE7QUFDQTtBQUMwQyIsInNvdXJjZXMiOlsid2VicGFjazovL2d1bmJ1aWxkZXItcHJvdG90eXBlLy4vbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2ludGVyb3BfcmVxdWlyZV93aWxkY2FyZC5qcz8wMWE1Il0sInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIF9nZXRSZXF1aXJlV2lsZGNhcmRDYWNoZShub2RlSW50ZXJvcCkge1xuICAgIGlmICh0eXBlb2YgV2Vha01hcCAhPT0gXCJmdW5jdGlvblwiKSByZXR1cm4gbnVsbDtcblxuICAgIHZhciBjYWNoZUJhYmVsSW50ZXJvcCA9IG5ldyBXZWFrTWFwKCk7XG4gICAgdmFyIGNhY2hlTm9kZUludGVyb3AgPSBuZXcgV2Vha01hcCgpO1xuXG4gICAgcmV0dXJuIChfZ2V0UmVxdWlyZVdpbGRjYXJkQ2FjaGUgPSBmdW5jdGlvbihub2RlSW50ZXJvcCkge1xuICAgICAgICByZXR1cm4gbm9kZUludGVyb3AgPyBjYWNoZU5vZGVJbnRlcm9wIDogY2FjaGVCYWJlbEludGVyb3A7XG4gICAgfSkobm9kZUludGVyb3ApO1xufVxuZXhwb3J0IGZ1bmN0aW9uIF9pbnRlcm9wX3JlcXVpcmVfd2lsZGNhcmQob2JqLCBub2RlSW50ZXJvcCkge1xuICAgIGlmICghbm9kZUludGVyb3AgJiYgb2JqICYmIG9iai5fX2VzTW9kdWxlKSByZXR1cm4gb2JqO1xuICAgIGlmIChvYmogPT09IG51bGwgfHwgdHlwZW9mIG9iaiAhPT0gXCJvYmplY3RcIiAmJiB0eXBlb2Ygb2JqICE9PSBcImZ1bmN0aW9uXCIpIHJldHVybiB7IGRlZmF1bHQ6IG9iaiB9O1xuXG4gICAgdmFyIGNhY2hlID0gX2dldFJlcXVpcmVXaWxkY2FyZENhY2hlKG5vZGVJbnRlcm9wKTtcblxuICAgIGlmIChjYWNoZSAmJiBjYWNoZS5oYXMob2JqKSkgcmV0dXJuIGNhY2hlLmdldChvYmopO1xuXG4gICAgdmFyIG5ld09iaiA9IHsgX19wcm90b19fOiBudWxsIH07XG4gICAgdmFyIGhhc1Byb3BlcnR5RGVzY3JpcHRvciA9IE9iamVjdC5kZWZpbmVQcm9wZXJ0eSAmJiBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yO1xuXG4gICAgZm9yICh2YXIga2V5IGluIG9iaikge1xuICAgICAgICBpZiAoa2V5ICE9PSBcImRlZmF1bHRcIiAmJiBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwob2JqLCBrZXkpKSB7XG4gICAgICAgICAgICB2YXIgZGVzYyA9IGhhc1Byb3BlcnR5RGVzY3JpcHRvciA/IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3Iob2JqLCBrZXkpIDogbnVsbDtcbiAgICAgICAgICAgIGlmIChkZXNjICYmIChkZXNjLmdldCB8fCBkZXNjLnNldCkpIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShuZXdPYmosIGtleSwgZGVzYyk7XG4gICAgICAgICAgICBlbHNlIG5ld09ialtrZXldID0gb2JqW2tleV07XG4gICAgICAgIH1cbiAgICB9XG5cbiAgICBuZXdPYmouZGVmYXVsdCA9IG9iajtcblxuICAgIGlmIChjYWNoZSkgY2FjaGUuc2V0KG9iaiwgbmV3T2JqKTtcblxuICAgIHJldHVybiBuZXdPYmo7XG59XG5leHBvcnQgeyBfaW50ZXJvcF9yZXF1aXJlX3dpbGRjYXJkIGFzIF8gfTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_interop_require_wildcard.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_tagged_template_literal_loose.js":
/*!*************************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_tagged_template_literal_loose.js ***!
\*************************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _tagged_template_literal_loose),\n/* harmony export */ _tagged_template_literal_loose: () => (/* binding */ _tagged_template_literal_loose)\n/* harmony export */ });\nfunction _tagged_template_literal_loose(strings, raw) {\n if (!raw) raw = strings.slice(0);\n\n strings.raw = raw;\n\n return strings;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9fdGFnZ2VkX3RlbXBsYXRlX2xpdGVyYWxfbG9vc2UuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBTztBQUNQOztBQUVBOztBQUVBO0FBQ0E7QUFDK0MiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9ndW5idWlsZGVyLXByb3RvdHlwZS8uL25vZGVfbW9kdWxlcy9Ac3djL2hlbHBlcnMvZXNtL190YWdnZWRfdGVtcGxhdGVfbGl0ZXJhbF9sb29zZS5qcz9jMTdiIl0sInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBmdW5jdGlvbiBfdGFnZ2VkX3RlbXBsYXRlX2xpdGVyYWxfbG9vc2Uoc3RyaW5ncywgcmF3KSB7XG4gICAgaWYgKCFyYXcpIHJhdyA9IHN0cmluZ3Muc2xpY2UoMCk7XG5cbiAgICBzdHJpbmdzLnJhdyA9IHJhdztcblxuICAgIHJldHVybiBzdHJpbmdzO1xufVxuZXhwb3J0IHsgX3RhZ2dlZF90ZW1wbGF0ZV9saXRlcmFsX2xvb3NlIGFzIF8gfTtcbiJdLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_tagged_template_literal_loose.js\n");
/***/ }),
/***/ "(rsc)/./node_modules/@swc/helpers/esm/_interop_require_default.js":
/*!*******************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_interop_require_default.js ***!
\*******************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ _: () => (/* binding */ _interop_require_default),\n/* harmony export */ _interop_require_default: () => (/* binding */ _interop_require_default)\n/* harmony export */ });\nfunction _interop_require_default(obj) {\n return obj && obj.__esModule ? obj : { default: obj };\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHJzYykvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBTztBQUNQLDJDQUEyQztBQUMzQztBQUN5QyIsInNvdXJjZXMiOlsid2VicGFjazovL2d1bmJ1aWxkZXItcHJvdG90eXBlLy4vbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2ludGVyb3BfcmVxdWlyZV9kZWZhdWx0LmpzPzM5NWEiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGZ1bmN0aW9uIF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdChvYmopIHtcbiAgICByZXR1cm4gb2JqICYmIG9iai5fX2VzTW9kdWxlID8gb2JqIDogeyBkZWZhdWx0OiBvYmogfTtcbn1cbmV4cG9ydCB7IF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdCBhcyBfIH07XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(rsc)/./node_modules/@swc/helpers/esm/_interop_require_default.js\n");
/***/ })
};
;
File diff suppressed because one or more lines are too long
-215
View File
@@ -1,215 +0,0 @@
/*
* 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/).
*/
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({});
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ id: moduleId,
/******/ loaded: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ var threw = true;
/******/ try {
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/ threw = false;
/******/ } finally {
/******/ if(threw) delete __webpack_module_cache__[moduleId];
/******/ }
/******/
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
/******/
/************************************************************************/
/******/ /* webpack/runtime/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/create fake namespace object */
/******/ (() => {
/******/ var getProto = Object.getPrototypeOf ? (obj) => (Object.getPrototypeOf(obj)) : (obj) => (obj.__proto__);
/******/ var leafPrototypes;
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 16: return value when it's Promise-like
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = this(value);
/******/ if(mode & 8) return value;
/******/ if(typeof value === 'object' && value) {
/******/ if((mode & 4) && value.__esModule) return value;
/******/ if((mode & 16) && typeof value.then === 'function') return value;
/******/ }
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ var def = {};
/******/ leafPrototypes = leafPrototypes || [null, getProto({}), getProto([]), getProto(getProto)];
/******/ for(var current = mode & 2 && value; typeof current == 'object' && !~leafPrototypes.indexOf(current); current = getProto(current)) {
/******/ Object.getOwnPropertyNames(current).forEach((key) => (def[key] = () => (value[key])));
/******/ }
/******/ def['default'] = () => (value);
/******/ __webpack_require__.d(ns, def);
/******/ return ns;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/ensure chunk */
/******/ (() => {
/******/ __webpack_require__.f = {};
/******/ // This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = (chunkId) => {
/******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
/******/ __webpack_require__.f[key](chunkId, promises);
/******/ return promises;
/******/ }, []));
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/get javascript chunk filename */
/******/ (() => {
/******/ // This function allow to reference async chunks and sibling chunks for the entrypoint
/******/ __webpack_require__.u = (chunkId) => {
/******/ // return url for filenames based on template
/******/ return "" + chunkId + ".js";
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => ("2d054b747f45013d")
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/node module decorator */
/******/ (() => {
/******/ __webpack_require__.nmd = (module) => {
/******/ module.paths = [];
/******/ if (!module.children) module.children = [];
/******/ return module;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/startup entrypoint */
/******/ (() => {
/******/ __webpack_require__.X = (result, chunkIds, fn) => {
/******/ // arguments: chunkIds, moduleId are deprecated
/******/ var moduleId = chunkIds;
/******/ if(!fn) chunkIds = result, fn = () => (__webpack_require__(__webpack_require__.s = moduleId));
/******/ chunkIds.map(__webpack_require__.e, __webpack_require__)
/******/ var r = fn();
/******/ return r === undefined ? result : r;
/******/ }
/******/ })();
/******/
/******/ /* webpack/runtime/require chunk loading */
/******/ (() => {
/******/ // no baseURI
/******/
/******/ // object to store loaded chunks
/******/ // "1" means "loaded", otherwise not loaded yet
/******/ var installedChunks = {
/******/ "webpack-runtime": 1
/******/ };
/******/
/******/ // no on chunks loaded
/******/
/******/ var installChunk = (chunk) => {
/******/ var moreModules = chunk.modules, chunkIds = chunk.ids, runtime = chunk.runtime;
/******/ for(var moduleId in moreModules) {
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(runtime) runtime(__webpack_require__);
/******/ for(var i = 0; i < chunkIds.length; i++)
/******/ installedChunks[chunkIds[i]] = 1;
/******/
/******/ };
/******/
/******/ // require() chunk loading for javascript
/******/ __webpack_require__.f.require = (chunkId, promises) => {
/******/ // "1" is the signal for "already loaded"
/******/ if(!installedChunks[chunkId]) {
/******/ if("webpack-runtime" != chunkId) {
/******/ installChunk(require("./" + __webpack_require__.u(chunkId)));
/******/ } else installedChunks[chunkId] = 1;
/******/ }
/******/ };
/******/
/******/ module.exports = __webpack_require__;
/******/ __webpack_require__.C = installChunk;
/******/
/******/ // no HMR
/******/
/******/ // no HMR manifest
/******/ })();
/******/
/************************************************************************/
/******/
/******/
/******/ })()
;
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
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 it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1 +0,0 @@
self.__BUILD_MANIFEST = {__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},sortedPages:["\u002F_app"]};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()
-1
View File
@@ -1 +0,0 @@
self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()
@@ -1 +0,0 @@
{"c":["app/layout","webpack"],"r":[],"m":[]}
@@ -1 +0,0 @@
{"c":[],"r":[],"m":[]}
@@ -1,22 +0,0 @@
"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\"] = (\"32ab11c94444\");\nif (true) { module.hot.accept() }\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKGFwcC1wYWdlcy1icm93c2VyKS8uL2FwcC9nbG9iYWxzLmNzcyIsIm1hcHBpbmdzIjoiO0FBQUEsK0RBQWUsY0FBYztBQUM3QixJQUFJLElBQVUsSUFBSSxpQkFBaUIiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9fTl9FLy4vYXBwL2dsb2JhbHMuY3NzPzk1YWEiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGRlZmF1bHQgXCIzMmFiMTFjOTQ0NDRcIlxuaWYgKG1vZHVsZS5ob3QpIHsgbW9kdWxlLmhvdC5hY2NlcHQoKSB9XG4iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(app-pages-browser)/./app/globals.css\n"));
/***/ })
});
@@ -1,18 +0,0 @@
"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 "e6c287ee061905c3"; }
/******/ }();
/******/
/******/ }
);
-4
View File
File diff suppressed because one or more lines are too long
-79
View File
@@ -1,79 +0,0 @@
// File: /Users/sean/Dev/gunbuilder-prototype/app/gunbuilder/page.tsx
import * as entry from '../../../../app/gunbuilder/page.js'
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
type TEntry = typeof import('../../../../app/gunbuilder/page.js')
// Check that the entry is a valid entry
checkFields<Diff<{
default: Function
config?: {}
generateStaticParams?: Function
revalidate?: RevalidateRange<TEntry> | false
dynamic?: 'auto' | 'force-dynamic' | 'error' | 'force-static'
dynamicParams?: boolean
fetchCache?: 'auto' | 'force-no-store' | 'only-no-store' | 'default-no-store' | 'default-cache' | 'only-cache' | 'force-cache'
preferredRegion?: 'auto' | 'global' | 'home' | string | string[]
runtime?: 'nodejs' | 'experimental-edge' | 'edge'
maxDuration?: number
metadata?: any
generateMetadata?: Function
viewport?: any
generateViewport?: Function
}, TEntry, ''>>()
// Check the prop type of the entry function
checkFields<Diff<PageProps, FirstArg<TEntry['default']>, 'default'>>()
// Check the arguments and return type of the generateMetadata function
if ('generateMetadata' in entry) {
checkFields<Diff<PageProps, FirstArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
checkFields<Diff<ResolvingMetadata, SecondArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
}
// Check the arguments and return type of the generateViewport function
if ('generateViewport' in entry) {
checkFields<Diff<PageProps, FirstArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
checkFields<Diff<ResolvingViewport, SecondArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
}
// Check the arguments and return type of the generateStaticParams function
if ('generateStaticParams' in entry) {
checkFields<Diff<{ params: PageParams }, FirstArg<MaybeField<TEntry, 'generateStaticParams'>>, 'generateStaticParams'>>()
checkFields<Diff<{ __tag__: 'generateStaticParams', __return_type__: any[] | Promise<any[]> }, { __tag__: 'generateStaticParams', __return_type__: ReturnType<MaybeField<TEntry, 'generateStaticParams'>> }>>()
}
type PageParams = any
export interface PageProps {
params?: any
searchParams?: any
}
export interface LayoutProps {
children?: React.ReactNode
params?: any
}
// =============
// Utility types
type RevalidateRange<T> = T extends { revalidate: any } ? NonNegative<T['revalidate']> : never
// If T is unknown or any, it will be an empty {} type. Otherwise, it will be the same as Omit<T, keyof Base>.
type OmitWithTag<T, K extends keyof any, _M> = Omit<T, K>
type Diff<Base, T extends Base, Message extends string = ''> = 0 extends (1 & T) ? {} : OmitWithTag<T, keyof Base, Message>
type FirstArg<T extends Function> = T extends (...args: [infer T, any]) => any ? unknown extends T ? any : T : never
type SecondArg<T extends Function> = T extends (...args: [any, infer T]) => any ? unknown extends T ? any : T : never
type MaybeField<T, K extends string> = T extends { [k in K]: infer G } ? G extends Function ? G : never : never
function checkFields<_ extends { [k in keyof any]: never }>() {}
// https://github.com/sindresorhus/type-fest
type Numeric = number | bigint
type Zero = 0 | 0n
type Negative<T extends Numeric> = T extends Zero ? never : `${T}` extends `-${string}` ? T : never
type NonNegative<T extends Numeric> = T extends Zero ? T : Negative<T> extends never ? T : '__invalid_negative_number__'
-79
View File
@@ -1,79 +0,0 @@
// File: /Users/sean/Dev/gunbuilder-prototype/app/layout.tsx
import * as entry from '../../../app/layout.js'
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
type TEntry = typeof import('../../../app/layout.js')
// Check that the entry is a valid entry
checkFields<Diff<{
default: Function
config?: {}
generateStaticParams?: Function
revalidate?: RevalidateRange<TEntry> | false
dynamic?: 'auto' | 'force-dynamic' | 'error' | 'force-static'
dynamicParams?: boolean
fetchCache?: 'auto' | 'force-no-store' | 'only-no-store' | 'default-no-store' | 'default-cache' | 'only-cache' | 'force-cache'
preferredRegion?: 'auto' | 'global' | 'home' | string | string[]
runtime?: 'nodejs' | 'experimental-edge' | 'edge'
maxDuration?: number
metadata?: any
generateMetadata?: Function
viewport?: any
generateViewport?: Function
}, TEntry, ''>>()
// Check the prop type of the entry function
checkFields<Diff<LayoutProps, FirstArg<TEntry['default']>, 'default'>>()
// Check the arguments and return type of the generateMetadata function
if ('generateMetadata' in entry) {
checkFields<Diff<LayoutProps, FirstArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
checkFields<Diff<ResolvingMetadata, SecondArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
}
// Check the arguments and return type of the generateViewport function
if ('generateViewport' in entry) {
checkFields<Diff<LayoutProps, FirstArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
checkFields<Diff<ResolvingViewport, SecondArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
}
// Check the arguments and return type of the generateStaticParams function
if ('generateStaticParams' in entry) {
checkFields<Diff<{ params: PageParams }, FirstArg<MaybeField<TEntry, 'generateStaticParams'>>, 'generateStaticParams'>>()
checkFields<Diff<{ __tag__: 'generateStaticParams', __return_type__: any[] | Promise<any[]> }, { __tag__: 'generateStaticParams', __return_type__: ReturnType<MaybeField<TEntry, 'generateStaticParams'>> }>>()
}
type PageParams = any
export interface PageProps {
params?: any
searchParams?: any
}
export interface LayoutProps {
children?: React.ReactNode
params?: any
}
// =============
// Utility types
type RevalidateRange<T> = T extends { revalidate: any } ? NonNegative<T['revalidate']> : never
// If T is unknown or any, it will be an empty {} type. Otherwise, it will be the same as Omit<T, keyof Base>.
type OmitWithTag<T, K extends keyof any, _M> = Omit<T, K>
type Diff<Base, T extends Base, Message extends string = ''> = 0 extends (1 & T) ? {} : OmitWithTag<T, keyof Base, Message>
type FirstArg<T extends Function> = T extends (...args: [infer T, any]) => any ? unknown extends T ? any : T : never
type SecondArg<T extends Function> = T extends (...args: [any, infer T]) => any ? unknown extends T ? any : T : never
type MaybeField<T, K extends string> = T extends { [k in K]: infer G } ? G extends Function ? G : never : never
function checkFields<_ extends { [k in keyof any]: never }>() {}
// https://github.com/sindresorhus/type-fest
type Numeric = number | bigint
type Zero = 0 | 0n
type Negative<T extends Numeric> = T extends Zero ? never : `${T}` extends `-${string}` ? T : never
type NonNegative<T extends Numeric> = T extends Zero ? T : Negative<T> extends never ? T : '__invalid_negative_number__'
-1
View File
@@ -1 +0,0 @@
{"type": "module"}
+125
View File
@@ -0,0 +1,125 @@
// app/admin/categories/page.tsx
"use client";
import { useEffect, useState } from "react";
import { useAuth } from "@/context/AuthContext";
import {
fetchAdminCategories,
type AdminCategory,
} from "@/lib/api/adminCategoryMappings";
export default function AdminCategoriesPage() {
const { token } = useAuth();
const [categories, setCategories] = useState<AdminCategory[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!token) return;
const load = async () => {
try {
setLoading(true);
const cats = await fetchAdminCategories(token);
setCategories(cats);
} catch (e: any) {
console.error(e);
setError(e?.message ?? "Failed to load categories");
} finally {
setLoading(false);
}
};
load();
}, [token]);
if (!token) {
return (
<div className="p-6 text-sm text-red-500">
You must be logged in to view this page.
</div>
);
}
return (
<div className="p-6 space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-lg font-semibold tracking-tight">
Canonical categories
</h1>
<p className="mt-1 text-xs text-zinc-400">
These are your standardized builder categories and their groupings.
</p>
</div>
{/* Placeholder for future actions (add/edit) */}
{/* <button className="rounded-md bg-emerald-600 px-3 py-1 text-xs font-medium text-white hover:bg-emerald-500">
+ Add category
</button> */}
</div>
{/* Error */}
{error && (
<div className="rounded border border-red-500/60 bg-red-950/30 px-3 py-2 text-xs text-red-200">
{error}
</div>
)}
{/* Table */}
{loading ? (
<div className="text-sm text-zinc-400">Loading</div>
) : (
<div className="overflow-x-auto rounded-lg border border-zinc-800 bg-zinc-950/60">
<table className="min-w-full text-left text-xs">
<thead className="bg-zinc-900/80 text-zinc-400">
<tr>
<th className="px-3 py-2 font-medium">Group</th>
<th className="px-3 py-2 font-medium">Name</th>
<th className="px-3 py-2 font-medium">Slug</th>
<th className="px-3 py-2 font-medium">Sort order</th>
<th className="px-3 py-2 font-medium">Description</th>
</tr>
</thead>
<tbody>
{categories.map((cat) => (
<tr
key={cat.id}
className="border-t border-zinc-800/70 hover:bg-zinc-900/60"
>
<td className="px-3 py-2 text-[11px] text-zinc-400">
{cat.groupName ?? "—"}
</td>
<td className="px-3 py-2 text-zinc-100">{cat.name}</td>
<td className="px-3 py-2 text-[11px] text-zinc-500">
<code>{cat.slug}</code>
</td>
<td className="px-3 py-2 text-[11px] text-zinc-400">
{cat.sortOrder ?? "—"}
</td>
<td className="px-3 py-2 text-zinc-300">
{cat.description || (
<span className="text-zinc-500">No description</span>
)}
</td>
</tr>
))}
{categories.length === 0 && (
<tr>
<td
colSpan={5}
className="px-3 py-4 text-center text-xs text-zinc-500"
>
No categories found. Seed the <code>part_categories</code>{" "}
table to get started.
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</div>
);
}
+260
View File
@@ -0,0 +1,260 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
const formatCount = (value: number | null | undefined) =>
(typeof value === "number" ? value : 0).toLocaleString();
type ImportSummary = {
totalProducts: number;
mappedProducts: number;
pendingProducts: number;
};
type RawByMerchantRow = {
merchantId: number;
merchantName: string;
platform: string;
status: string;
count: number;
};
type MerchantImportRow = {
merchantId: number;
merchantName: string;
totalProducts: number;
mappedProducts: number;
pendingProducts: number;
lastImportAt?: string | null;
};
export default function ImportStatusPage() {
const [summary, setSummary] = useState<ImportSummary | null>(null);
const [rows, setRows] = useState<MerchantImportRow[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [importingId, setImportingId] = useState<number | null>(null);
useEffect(() => {
async function load() {
try {
setLoading(true);
setError(null);
const [summaryRes, byMerchantRes] = await Promise.all([
fetch(`${API_BASE_URL}/api/admin/import-status/summary`),
fetch(`${API_BASE_URL}/api/admin/import-status/by-merchant`),
]);
if (!summaryRes.ok || !byMerchantRes.ok) {
throw new Error("Failed to load import status");
}
const summaryJson: ImportSummary = await summaryRes.json();
const byMerchantRaw: RawByMerchantRow[] = await byMerchantRes.json();
// Aggregate per merchant using merchantId as the key
const merchantMap = new Map<number, MerchantImportRow>();
for (const row of byMerchantRaw) {
const id = row.merchantId;
const name = row.merchantName;
const status = row.status;
const count = row.count ?? 0;
if (!merchantMap.has(id)) {
merchantMap.set(id, {
merchantId: id,
merchantName: name,
totalProducts: 0,
mappedProducts: 0,
pendingProducts: 0,
lastImportAt: null, // not provided by API yet
});
}
const agg = merchantMap.get(id)!;
agg.totalProducts += count;
if (status === "MAPPED") {
agg.mappedProducts += count;
} else if (status === "PENDING_MAPPING") {
agg.pendingProducts += count;
}
}
const byMerchantJson: MerchantImportRow[] = Array.from(
merchantMap.values()
);
setSummary(summaryJson);
setRows(byMerchantJson);
} catch (e: any) {
console.error(e);
setError(e.message ?? "Failed to load import status");
} finally {
setLoading(false);
}
}
load();
}, []);
async function handleReimport(merchantId: number) {
try {
setImportingId(merchantId);
const res = await fetch(
`${API_BASE_URL}/api/admin/merchants/${merchantId}/import`,
{
method: "POST",
}
);
if (!res.ok) {
throw new Error(`Re-import failed (${res.status})`);
}
// Optionally re-load data here if you want the table to refresh automatically
} catch (e: any) {
console.error(e);
alert(e.message ?? "Failed to trigger import");
} finally {
setImportingId(null);
}
}
return (
<main className="min-h-screen bg-black text-zinc-50">
<div className="mx-auto max-w-6xl px-4 py-8">
<header className="flex items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold tracking-tight">
Import Status
</h1>
<p className="mt-1 text-sm text-zinc-400">
Per-merchant feed health and mapping coverage.
</p>
</div>
<Link
href="/admin"
className="text-xs text-zinc-400 hover:text-zinc-200"
>
Back to Admin Home
</Link>
</header>
{loading && (
<p className="mt-6 text-sm text-zinc-400">Loading import status</p>
)}
{error && <p className="mt-6 text-sm text-red-400">Error: {error}</p>}
{summary && !loading && !error && (
<>
{/* Summary cards */}
<section className="mt-6 grid gap-4 md:grid-cols-3">
<div className="rounded-lg border border-zinc-800 bg-zinc-950/70 px-4 py-3">
<p className="text-xs uppercase tracking-[0.18em] text-zinc-500">
Total Products
</p>
<p className="mt-2 text-xl font-semibold">
{formatCount(summary.totalProducts)}
</p>
</div>
<div className="rounded-lg border border-zinc-800 bg-zinc-950/70 px-4 py-3">
<p className="text-xs uppercase tracking-[0.18em] text-zinc-500">
Mapped
</p>
<p className="mt-2 text-xl font-semibold text-emerald-400">
{formatCount(summary.mappedProducts)}
</p>
</div>
<div className="rounded-lg border border-zinc-800 bg-zinc-950/70 px-4 py-3">
<p className="text-xs uppercase tracking-[0.18em] text-zinc-500">
Pending Mapping
</p>
<p className="mt-2 text-xl font-semibold text-amber-300">
{formatCount(summary.pendingProducts)}
</p>
</div>
</section>
<section className="mt-8">
<h2 className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-500">
By Merchant
</h2>
<div className="mt-3 overflow-x-auto rounded-lg border border-zinc-800 bg-zinc-950/70">
<table className="min-w-full text-left text-sm">
<thead className="border-b border-zinc-800 bg-zinc-950/80 text-xs uppercase text-zinc-500">
<tr>
<th className="px-4 py-3">Merchant</th>
<th className="px-4 py-3 text-right">Total</th>
<th className="px-4 py-3 text-right">Mapped</th>
<th className="px-4 py-3 text-right">Pending</th>
<th className="px-4 py-3">Last Import</th>
<th className="px-4 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr
key={row.merchantId}
className="border-t border-zinc-900/80"
>
<td className="px-4 py-3">
<span className="font-medium text-zinc-50">
{row.merchantName}
</span>
</td>
<td className="px-4 py-3 text-right text-zinc-300">
{formatCount(row.totalProducts)}
</td>
<td className="px-4 py-3 text-right text-emerald-400">
{formatCount(row.mappedProducts)}
</td>
<td className="px-4 py-3 text-right text-amber-300">
{formatCount(row.pendingProducts)}
</td>
<td className="px-4 py-3 text-xs text-zinc-500">
{row.lastImportAt
? new Date(row.lastImportAt).toLocaleString()
: "—"}
</td>
<td className="px-4 py-3">
<div className="flex justify-end gap-2">
<Link
href={`/admin/mapping?merchantId=${row.merchantId}`}
className="rounded-md border border-zinc-700 bg-zinc-900 px-2.5 py-1 text-xs text-zinc-200 hover:border-amber-400/80 hover:bg-zinc-800"
>
Mapping
</Link>
<button
type="button"
onClick={() => handleReimport(row.merchantId)}
disabled={importingId === row.merchantId}
className="rounded-md border border-zinc-700 bg-zinc-900 px-2.5 py-1 text-xs text-zinc-200 hover:border-amber-400/80 hover:bg-zinc-800 disabled:cursor-not-allowed disabled:opacity-60"
>
{importingId === row.merchantId
? "Re-importing…"
: "Re-import"}
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</>
)}
</div>
</main>
);
}
+323
View File
@@ -0,0 +1,323 @@
"use client";
import { useEffect, useState } from "react";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
type PendingBucket = {
merchantId: number;
merchantName: string;
rawCategoryKey: string;
mappedPartRole: string | null;
productCount: number;
};
const PART_ROLE_OPTIONS: string[] = [
// LOWER receivers
"LOWER_RECEIVER_STRIPPED",
"LOWER_RECEIVER_COMPLETE",
"LOWER_RECEIVER_COMPLETE_PISTOL",
"LOWER_RECEIVER_COMPLETE_RIFLE",
"LOWER_RECEIVER_80",
"LOWER_RECEIVER_BILLET",
"LOWER_RECEIVER_FORGED",
"LOWER_RECEIVER_POLYMER",
// LOWER internals & controls
"LOWER_PARTS_KIT",
"LOWER_PARTS_KIT_ENHANCED",
"TRIGGER",
"TRIGGER_OTHER",
"TRIGGER_COMBAT",
"TRIGGER_MATCH",
"TRIGGER_DROP_IN",
"TRIGGER_SINGLE_STAGE",
"TRIGGER_TWO_STAGE",
"TRIGGER_GUARD",
"PISTOL_GRIP",
"PISTOL_GRIP_ERGO",
"PISTOL_GRIP_VERTICAL",
"SAFETY_SELECTOR",
"SAFETY_SELECTOR_AMBI",
"SAFETY_SELECTOR_45_DEG",
"MAG_RELEASE",
"TAKEDOWN_PINS",
"SPRINGS_PINS_MISC",
// LOWER buffer & stock
"BUFFER_KIT",
"BUFFER_TUBE",
"BUFFER_TUBE_MILSPEC",
"BUFFER_TUBE_COMMERCIAL",
"BUFFER_SPRING",
"BUFFER_WEIGHT",
"STOCK_ADJUSTABLE",
"STOCK_FIXED",
"STOCK_FOLDER",
"STOCK_PRECISION",
"BRACE_PISTOL",
// UPPER receivers & BCG
"UPPER_RECEIVER_STRIPPED",
"UPPER_RECEIVER_BILLET",
"UPPER_RECEIVER_MONOLITHIC",
"UPPER_RECEIVER_COMPLETE",
"UPPER_RECEIVER_COMPLETE_PISTOL",
"UPPER_RECEIVER_COMPLETE_RIFLE",
"BOLT_CARRIER_GROUP",
"BCG_COMPLETE",
"BCG_LIGHTWEIGHT",
"BCG_NICKEL_BORON",
// UPPER barrel & gas
"BARREL",
"BARREL_THREADED",
"BARREL_PENCIL",
"BARREL_MATCH",
"BARREL_HEAVY",
"BARREL_14_5_PINNED",
"GAS_BLOCK",
"GAS_BLOCK_ADJUSTABLE",
"GAS_BLOCK_LOW_PROFILE",
"GAS_TUBE_PISTOL",
"GAS_TUBE_CAR",
"GAS_TUBE_MID",
"GAS_TUBE_RIFLE",
// UPPER front end / muzzle
"HANDGUARD_MLOK",
"HANDGUARD_KEYMOD",
"HANDGUARD_QUAD",
"HANDGUARD_SLICK",
"MUZZLE_BRAKE",
"MUZZLE_COMPENSATOR",
// 👇 generic / backup muzzle roles
"MUZZLE_DEVICE",
"MUZZLE_DEVICE_OTHER",
"FLASH_HIDER",
"MUZZLE_DEVICE_QD_MOUNT",
"SUPPRESSOR_DIRECT_THREAD",
"SUPPRESSOR_QD",
"SUPPRESSOR_RIFLE",
"SUPPRESSOR_PISTOL",
"CHARGING_HANDLE",
"CHARGING_HANDLE_AMBI",
"CHARGING_HANDLE_GAS_BUSTER",
// Sights / optics
"SIGHTS_BACKUP",
"SIGHTS_IRON_FIXED",
"SIGHTS_IRON_FLIP",
"SIGHTS_OFFSET",
"OPTIC_REDDOT",
"OPTIC_LPVO",
"OPTIC_HOLOGRAPHIC",
"OPTIC_PRISM",
"OPTIC_MAGNIFIER",
// Accessories
"MAGAZINE",
"SLING",
"BIPOD",
"MOUNT_SCOPE",
"MOUNT_OFFSET",
"LIGHT_WEAPON",
"LASER_VISIBLE",
"LASER_IR",
"TOOL",
];
export default function MappingAdminPage() {
const [rows, setRows] = useState<PendingBucket[]>([]);
const [loading, setLoading] = useState(true);
const [savingKey, setSavingKey] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function load() {
try {
setLoading(true);
setError(null);
const res = await fetch(
`${API_BASE_URL}/api/admin/mapping/pending-buckets`
);
if (!res.ok) {
throw new Error(`Failed to load pending buckets (${res.status})`);
}
const data: PendingBucket[] = await res.json();
setRows(data);
} catch (e: any) {
console.error(e);
setError(e.message ?? "Failed to load pending buckets");
} finally {
setLoading(false);
}
}
load();
}, []);
const handleChangeRole = (idx: number, value: string) => {
setRows((prev) => {
const next = [...prev];
next[idx] = { ...next[idx], mappedPartRole: value || null };
return next;
});
};
const handleSave = async (row: PendingBucket) => {
if (!row.mappedPartRole) return;
const key = `${row.merchantId}-${row.rawCategoryKey}`;
try {
setSavingKey(key);
setError(null);
const res = await fetch(`${API_BASE_URL}/api/admin/mapping/apply`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
merchantId: row.merchantId,
rawCategoryKey: row.rawCategoryKey,
mappedPartRole: row.mappedPartRole,
}),
});
if (!res.ok) {
throw new Error(`Failed to save mapping (${res.status})`);
}
// After save, remove this bucket from the list
setRows((prev) =>
prev.filter(
(r) =>
!(
r.merchantId === row.merchantId &&
r.rawCategoryKey === row.rawCategoryKey
)
)
);
} catch (e: any) {
console.error(e);
setError(e.message ?? "Failed to save mapping");
} finally {
setSavingKey(null);
}
};
return (
<main className="min-h-screen bg-black text-zinc-50">
<div className="mx-auto max-w-6xl px-4 py-6">
<h1 className="text-xl font-semibold tracking-tight">
Bulk Mapping Pending Categories
</h1>
<p className="mt-1 text-sm text-zinc-400">
Bucketed by merchant + raw category. Map each bucket to a part role to
clean up the builder catalog.
</p>
{loading && (
<p className="mt-4 text-sm text-zinc-400">Loading pending buckets</p>
)}
{error && (
<p className="mt-4 text-sm text-red-400">Error: {error}</p>
)}
{!loading && rows.length === 0 && !error && (
<p className="mt-4 text-sm text-emerald-400">
No pending mapping buckets 🎉
</p>
)}
{!loading && rows.length > 0 && (
<section className="mt-4 rounded-lg border border-zinc-800 bg-zinc-950/80 p-3">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-xs font-semibold uppercase tracking-[0.14em] text-zinc-500">
Pending Buckets
</h2>
<span className="text-xs text-zinc-400">
{rows.length} buckets {rows.reduce((s, r) => s + r.productCount, 0)}{" "}
products
</span>
</div>
<div className="mt-1 overflow-x-auto">
<table className="min-w-full text-xs">
<thead>
<tr className="border-b border-zinc-800 bg-zinc-900/60">
<th className="px-2 py-1 text-left text-zinc-400">
Merchant
</th>
<th className="px-2 py-1 text-left text-zinc-400">
Raw Category
</th>
<th className="px-2 py-1 text-left text-zinc-400">
Products
</th>
<th className="px-2 py-1 text-left text-zinc-400">
Part Role
</th>
<th className="px-2 py-1 text-right text-zinc-400">
Action
</th>
</tr>
</thead>
<tbody>
{rows.map((row, idx) => {
const rowKey = `${row.merchantId}-${row.rawCategoryKey}`;
const isSaving = savingKey === rowKey;
return (
<tr
key={rowKey}
className="border-b border-zinc-900 hover:bg-zinc-900/40"
>
<td className="px-2 py-1 text-zinc-100">
{row.merchantName}
</td>
<td className="px-2 py-1 text-zinc-300">
{row.rawCategoryKey}
</td>
<td className="px-2 py-1 text-zinc-200">{row.productCount}</td>
<td className="px-2 py-1 text-zinc-100">
<select
className="w-full rounded-md border border-zinc-700 bg-zinc-950/80 px-2 py-1 text-xs text-zinc-100 outline-none focus:border-amber-400"
value={row.mappedPartRole ?? ""}
onChange={(e) =>
handleChangeRole(idx, e.target.value)
}
>
<option value="">Select part role</option>
{PART_ROLE_OPTIONS.map((pr) => (
<option key={pr} value={pr}>
{pr}
</option>
))}
</select>
</td>
<td className="px-2 py-1 text-right">
<button
onClick={() => handleSave(row)}
disabled={!row.mappedPartRole || isSaving}
className="rounded-md bg-emerald-500 px-3 py-1 text-[0.7rem] font-semibold text-black disabled:cursor-not-allowed disabled:bg-zinc-700"
>
{isSaving ? "Saving…" : "Save"}
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</section>
)}
</div>
</main>
);
}
@@ -0,0 +1,360 @@
"use client";
import { useEffect, useState } from "react";
import { useApi } from "@/lib/useApi";
type Merchant = {
id: number;
name: string;
};
type CategoryMapping = {
id: number;
merchantId: number;
merchantName: string;
rawCategoryPath: string;
partCategoryId: number | null;
partCategoryName?: string | null;
};
// Matches the Spring MerchantCategoryMappingDto record
type MerchantCategoryMappingDto = {
id: number;
merchantId: number;
merchantName: string;
rawCategoryPath: string;
partCategoryId: number | null;
partCategoryName?: string | null;
};
type AdminCategory = {
id: number;
name: string;
groupName?: string | null;
};
export default function MerchantCategoryMappingPage() {
const { get, post } = useApi();
const [merchants, setMerchants] = useState<Merchant[]>([]);
const [selectedMerchantId, setSelectedMerchantId] = useState<number | null>(
null
);
const [mappings, setMappings] = useState<CategoryMapping[]>([]);
const [loadingMerchants, setLoadingMerchants] = useState(false);
const [loadingMappings, setLoadingMappings] = useState(false);
const [savingId, setSavingId] = useState<number | null>(null);
const [error, setError] = useState<string | null>(null);
const [categories, setCategories] = useState<AdminCategory[]>([]);
const [loadingCategories, setLoadingCategories] = useState(false);
const [dirty, setDirty] = useState<Record<number, CategoryMapping>>({});
// 1) Load merchants for the dropdown
useEffect(() => {
setLoadingMerchants(true);
setError(null);
get<Merchant[]>("/api/admin/category-mappings/merchants")
.then((data) => {
setMerchants(data);
if (data.length > 0 && selectedMerchantId == null) {
setSelectedMerchantId(data[0].id);
}
})
.catch((err: any) => {
console.error("Failed to load merchants", err);
setError(
err instanceof Error ? err.message : "Failed to load merchants"
);
})
.finally(() => setLoadingMerchants(false));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Load global (canonical) part categories for the dropdown
// Load global (canonical) part categories for the dropdown
useEffect(() => {
setLoadingCategories(true);
setError(null);
get<AdminCategory[]>("/api/admin/categories")
.then((data) => {
setCategories(data);
})
.catch((err: any) => {
console.error("Failed to load categories", err);
setError(
err instanceof Error ? err.message : "Failed to load categories"
);
})
.finally(() => setLoadingCategories(false));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 2) Load mappings when merchant changes (THIS sends merchantId)
useEffect(() => {
if (selectedMerchantId == null) return;
setLoadingMappings(true);
setError(null);
get<CategoryMapping[]>(
`/api/admin/category-mappings?merchantId=${selectedMerchantId}`
)
.then((data) => {
setMappings(data);
})
.catch((err: any) => {
console.error("Failed to load category mappings", err);
setError(
err instanceof Error
? err.message
: "Failed to load category mappings"
);
})
.finally(() => setLoadingMappings(false));
}, [selectedMerchantId]);
// 3) Save a single mapping row (adjust endpoint if yours differs)
const handleSaveMapping = async (
mappingId: number,
updated: { partCategoryId: number | null }
) => {
setSavingId(mappingId);
setError(null);
try {
const updatedMapping = await post<MerchantCategoryMappingDto>(
`/api/admin/category-mappings/${mappingId}`,
{
partCategoryId: updated.partCategoryId,
}
);
setMappings((prev) =>
prev.map((m) =>
m.id === mappingId
? {
...m,
// map Java DTO fields back into your UI model
merchantId: updatedMapping.merchantId,
merchantName: updatedMapping.merchantName,
rawCategoryPath: updatedMapping.rawCategoryPath,
partCategoryId: updatedMapping.partCategoryId,
partCategoryName: updatedMapping.partCategoryName,
}
: m
)
);
} catch (err: any) {
console.error("Failed to save mapping", err);
setError(err instanceof Error ? err.message : "Failed to save mapping");
} finally {
setSavingId(null);
}
};
const handleSaveAll = async () => {
if (Object.keys(dirty).length === 0) return;
// special flag to indicate a global save is in progress
setSavingId(-1);
setError(null);
try {
const dirtyRows = Object.values(dirty);
const updatedMappings = await Promise.all(
dirtyRows.map((row) =>
post<MerchantCategoryMappingDto>(
`/api/admin/category-mappings/${row.id}`,
{
partCategoryId: row.partCategoryId ?? null,
}
)
)
);
setMappings((prev) =>
prev.map((m) => {
const updated = updatedMappings.find((u) => u.id === m.id);
if (!updated) return m;
return {
...m,
merchantId: updated.merchantId,
merchantName: updated.merchantName,
rawCategoryPath: updated.rawCategoryPath,
partCategoryId: updated.partCategoryId,
partCategoryName: updated.partCategoryName,
};
})
);
// Clear dirty state after successful batch save
setDirty({});
} catch (err: any) {
console.error("Failed to save all mappings", err);
setError(
err instanceof Error
? err.message
: "Failed to save all mappings"
);
} finally {
setSavingId(null);
}
};
return (
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold text-zinc-50">
Merchant Category Mapping
</h1>
<div className="flex items-center gap-2">
<span className="text-sm text-zinc-400">Merchant:</span>
<select
className="bg-zinc-900 border border-zinc-700 rounded px-2 py-1 text-sm"
value={selectedMerchantId ?? ""}
onChange={(e) =>
setSelectedMerchantId(
e.target.value ? Number(e.target.value) : null
)
}
>
{loadingMerchants && <option value="">Loading merchants</option>}
{!loadingMerchants && merchants.length === 0 && (
<option value="">No merchants</option>
)}
{merchants.map((m) => (
<option key={m.id} value={m.id}>
{m.name}
</option>
))}
</select>
</div>
</div>
{error && (
<div className="rounded bg-red-900/40 border border-red-700 px-3 py-2 text-sm text-red-200">
{error}
</div>
)}
{Object.keys(dirty).length > 0 && (
<div className="flex justify-end">
<button
onClick={handleSaveAll}
disabled={savingId !== null}
className="mb-3 rounded bg-emerald-600 px-4 py-2 text-sm font-medium text-white hover:bg-emerald-500 disabled:opacity-50"
>
Save All Changes ({Object.keys(dirty).length})
</button>
</div>
)}
<div className="border border-zinc-800 rounded-lg overflow-hidden">
<div className="bg-zinc-900 border-b border-zinc-800 px-4 py-2 text-xs uppercase tracking-wide text-zinc-400">
Category Mappings
</div>
{loadingMappings ? (
<div className="p-4 text-sm text-zinc-400">Loading mappings</div>
) : mappings.length === 0 ? (
<div className="p-4 text-sm text-zinc-400">
No mappings found for this merchant.
</div>
) : (
<table className="w-full text-sm">
<thead className="bg-zinc-950/60 border-b border-zinc-800">
<tr>
<th className="text-left px-4 py-2 text-xs uppercase tracking-wide text-zinc-500">
Merchant Category
</th>
<th className="text-left px-4 py-2 text-xs uppercase tracking-wide text-zinc-500">
Global Category
</th>
<th className="px-4 py-2 text-xs uppercase tracking-wide text-zinc-500">
Actions
</th>
</tr>
</thead>
<tbody>
{mappings.map((m) => (
<tr key={m.id} className="border-t border-zinc-800">
<td className="px-4 py-2 text-zinc-100">
{m.rawCategoryPath}
</td>
<td className="px-4 py-2">
{loadingCategories ? (
<span className="text-xs text-zinc-500">
Loading categories
</span>
) : categories.length === 0 ? (
<span className="text-xs text-zinc-500">
No categories
</span>
) : (
<select
className="bg-zinc-950 border border-zinc-700 rounded px-2 py-1 text-sm w-64"
value={m.partCategoryId ?? ""}
onChange={(e) => {
const value = e.target.value
? Number(e.target.value)
: null;
const updatedRow: CategoryMapping = {
...m,
partCategoryId: value,
};
setMappings((prev) =>
prev.map((row) =>
row.id === m.id ? updatedRow : row
)
);
// mark this row as dirty so it can be batch-saved
setDirty((prev) => ({
...prev,
[m.id]: updatedRow,
}));
}}
>
<option value=""> Unmapped </option>
{categories.map((cat) => (
<option key={cat.id} value={cat.id}>
{cat.groupName ? `[${cat.groupName}] ` : ""}
{cat.name}
</option>
))}
</select>
)}
{m.partCategoryName && (
<div className="text-xs text-zinc-500 mt-1">
Current: {m.partCategoryName}
</div>
)}
</td>
<td className="px-4 py-2 text-right">
<button
onClick={() =>
handleSaveMapping(m.id, {
partCategoryId: m.partCategoryId ?? null,
})
}
disabled={savingId === m.id}
className="text-xs px-3 py-1 rounded border border-zinc-600 hover:bg-zinc-800 disabled:opacity-50"
>
{savingId === m.id ? "Saving…" : "Save"}
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}
+178
View File
@@ -0,0 +1,178 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
type AdminOverview = {
totalProducts: number;
mappedProducts: number;
unmappedProducts: number;
merchantCount: number;
categoryMappingCount: number;
};
export default function AdminLandingPage() {
const [overview, setOverview] = useState<AdminOverview | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function load() {
try {
setLoading(true);
setError(null);
const res = await fetch(`${API_BASE_URL}/api/admin/dashboard/overview`);
if (!res.ok) {
throw new Error(`Failed to load dashboard (${res.status})`);
}
const data: AdminOverview = await res.json();
setOverview(data);
} catch (e: any) {
console.error(e);
setError(e.message ?? "Failed to load admin dashboard");
} finally {
setLoading(false);
}
}
load();
}, []);
return (
<main className="min-h-screen bg-black text-zinc-50">
<div className="mx-auto max-w-6xl px-4 py-8">
<header className="flex items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold tracking-tight">
Battl Builder Admin
</h1>
<p className="mt-1 text-sm text-zinc-400">
Control room for feeds, mappings, and catalog health.
</p>
</div>
</header>
{loading && (
<p className="mt-6 text-sm text-zinc-400">Loading dashboard</p>
)}
{error && (
<p className="mt-6 text-sm text-red-400">Error: {error}</p>
)}
{overview && !loading && !error && (
<>
{/* Stats row */}
<section className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div className="rounded-lg border border-zinc-800 bg-zinc-950/70 p-4">
<p className="text-xs uppercase tracking-[0.16em] text-zinc-500">
Total Products
</p>
<p className="mt-2 text-2xl font-semibold">
{overview.totalProducts.toLocaleString()}
</p>
</div>
<div className="rounded-lg border border-zinc-800 bg-zinc-950/70 p-4">
<p className="text-xs uppercase tracking-[0.16em] text-zinc-500">
Mapped Products
</p>
<p className="mt-2 text-2xl font-semibold text-emerald-400">
{overview.mappedProducts.toLocaleString()}
</p>
</div>
<div className="rounded-lg border border-zinc-800 bg-zinc-950/70 p-4">
<p className="text-xs uppercase tracking-[0.16em] text-zinc-500">
Unmapped Products
</p>
<p className="mt-2 text-2xl font-semibold text-amber-400">
{overview.unmappedProducts.toLocaleString()}
</p>
<p className="mt-1 text-[0.7rem] text-zinc-500">
Products still in <code>PENDING_MAPPING</code>.
</p>
</div>
<div className="rounded-lg border border-zinc-800 bg-zinc-950/70 p-4">
<p className="text-xs uppercase tracking-[0.16em] text-zinc-500">
Merchants / Mappings
</p>
<p className="mt-2 text-lg font-semibold">
{overview.merchantCount}{" "}
<span className="text-xs font-normal text-zinc-400">
merchants
</span>
</p>
<p className="text-sm text-zinc-400">
{overview.categoryMappingCount.toLocaleString()} mappings
</p>
</div>
</section>
{/* Nav cards */}
<section className="mt-10">
<h2 className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-500">
Admin Tools
</h2>
<div className="mt-3 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{/* Bulk Mapping */}
<Link
href="/admin/mapping"
className="group rounded-lg border border-zinc-800 bg-zinc-950/70 p-4 transition hover:border-amber-400/80 hover:bg-zinc-900/70"
>
<p className="text-sm font-semibold text-zinc-50">
Bulk Category Mapping
</p>
<p className="mt-1 text-xs text-zinc-400">
Bucket raw merchant categories and map them to part roles to
keep the catalog clean.
</p>
<p className="mt-3 text-xs text-amber-300">
{overview.unmappedProducts.toLocaleString()} products
pending mapping
</p>
</Link>
{/* Import Status */}
<Link
href="/admin/import-status"
className="group rounded-lg border border-zinc-800 bg-zinc-950/70 p-4 transition hover:border-amber-400/80 hover:bg-zinc-900/70"
>
<p className="text-sm font-semibold text-zinc-50">
Import Status
</p>
<p className="mt-1 text-xs text-zinc-400">
Inspect last feed runs, per-merchant health, and errors.
</p>
<p className="mt-3 text-xs text-zinc-500 group-hover:text-zinc-300">
View import summaries &nbsp;
</p>
</Link>
{/* Placeholder for future admin tools */}
<Link
href="/admin/products"
className="group rounded-lg border border-zinc-900 bg-zinc-950/40 p-4 transition hover:border-amber-400/80 hover:bg-zinc-900/70"
>
<p className="text-sm font-semibold text-zinc-50">
Catalog Explorer
</p>
<p className="mt-1 text-xs text-zinc-400">
(Future) Browse normalized products, offers, and compatibility.
</p>
<p className="mt-3 text-xs text-zinc-500 group-hover:text-zinc-300">
Coming soon
</p>
</Link>
</div>
</section>
</>
)}
</div>
</main>
);
}
+228
View File
@@ -0,0 +1,228 @@
// app/admin/category-mappings/page.tsx
"use client";
import { useEffect, useState } from "react";
import { useAuth } from "@/context/AuthContext";
import {
fetchAdminCategories,
fetchPartRoleMappings,
createPartRoleMapping,
updatePartRoleMapping,
deletePartRoleMapping,
type AdminCategory,
type AdminPartRoleMapping,
} from "@/lib/api/adminCategoryMappings";
export default function AdminCategoryMappingsPage() {
const { token } = useAuth();
const [categories, setCategories] = useState<AdminCategory[]>([]);
const [mappings, setMappings] = useState<AdminPartRoleMapping[]>([]);
const [platform, setPlatform] = useState("AR-15");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!token) return;
const load = async () => {
try {
setLoading(true);
const [cats, maps] = await Promise.all([
fetchAdminCategories(token),
fetchPartRoleMappings(token, platform),
]);
setCategories(cats);
setMappings(maps);
} catch (e: any) {
console.error(e);
setError(e?.message ?? "Failed to load mappings");
} finally {
setLoading(false);
}
};
load();
}, [token, platform]);
const handleCreate = async () => {
if (!token) return;
const defaultCategory = categories[0];
if (!defaultCategory) return;
try {
setSaving(true);
const created = await createPartRoleMapping(token, {
platform,
partRole: "NEW_PART_ROLE",
categorySlug: defaultCategory.slug,
});
setMappings((prev) => [...prev, created]);
} catch (e: any) {
console.error(e);
setError(e?.message ?? "Failed to create mapping");
} finally {
setSaving(false);
}
};
const handleUpdate = async (row: AdminPartRoleMapping, updates: Partial<AdminPartRoleMapping>) => {
if (!token) return;
try {
setSaving(true);
const updated = await updatePartRoleMapping(token, row.id, {
platform: updates.platform ?? row.platform,
partRole: updates.partRole ?? row.partRole,
categorySlug: updates.categorySlug ?? row.categorySlug,
notes: updates.notes ?? row.notes ?? undefined,
});
setMappings((prev) => prev.map((m) => (m.id === row.id ? updated : m)));
} catch (e: any) {
console.error(e);
setError(e?.message ?? "Failed to update mapping");
} finally {
setSaving(false);
}
};
const handleDelete = async (row: AdminPartRoleMapping) => {
if (!token) return;
if (!confirm(`Delete mapping for ${row.partRole}?`)) return;
try {
setSaving(true);
await deletePartRoleMapping(token, row.id);
setMappings((prev) => prev.filter((m) => m.id !== row.id));
} catch (e: any) {
console.error(e);
setError(e?.message ?? "Failed to delete mapping");
} finally {
setSaving(false);
}
};
if (!token) {
return <div className="p-6 text-sm text-red-500">You must be logged in to view this page.</div>;
}
return (
<div className="p-6 space-y-4">
<div className="flex items-center justify-between">
<h1 className="text-lg font-semibold tracking-tight">
Part role category mappings
</h1>
<p className="mt-1 text-xs text-zinc-400">
FOR LATER! Advanced mapping: defines which canonical category each builder part role
uses for a given platform.
</p>
<div className="flex items-center gap-3">
<select
value={platform}
onChange={(e) => setPlatform(e.target.value)}
className="rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1 text-sm"
>
<option value="AR-15">AR-15</option>
{/* add other platforms later */}
</select>
<button
onClick={handleCreate}
disabled={saving || loading || categories.length === 0}
className="rounded-md bg-emerald-600 px-3 py-1 text-xs font-medium text-white hover:bg-emerald-500 disabled:opacity-50"
>
+ Add mapping
</button>
</div>
</div>
{error && (
<div className="rounded border border-red-500/60 bg-red-950/30 px-3 py-2 text-xs text-red-200">
{error}
</div>
)}
{loading ? (
<div className="text-sm text-zinc-400">Loading</div>
) : (
<div className="overflow-x-auto rounded-lg border border-zinc-800 bg-zinc-950/60">
<table className="min-w-full text-left text-xs">
<thead className="bg-zinc-900/80 text-zinc-400">
<tr>
<th className="px-3 py-2 font-medium">Platform</th>
<th className="px-3 py-2 font-medium">Part role</th>
<th className="px-3 py-2 font-medium">Category</th>
<th className="px-3 py-2 font-medium">Group</th>
<th className="px-3 py-2 font-medium">Notes</th>
<th className="px-3 py-2 text-right font-medium">Actions</th>
</tr>
</thead>
<tbody>
{mappings.map((row) => (
<tr key={row.id} className="border-t border-zinc-800/70 hover:bg-zinc-900/60">
<td className="px-3 py-2 text-zinc-300">{row.platform}</td>
<td className="px-3 py-2">
<input
className="w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-1 text-xs text-zinc-100"
defaultValue={row.partRole}
onBlur={(e) => {
if (e.target.value !== row.partRole) {
handleUpdate(row, { partRole: e.target.value });
}
}}
/>
</td>
<td className="px-3 py-2">
<select
className="w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-1 text-xs text-zinc-100"
value={row.categorySlug}
onChange={(e) => {
handleUpdate(row, { categorySlug: e.target.value });
}}
>
{categories.map((cat) => (
<option key={cat.id} value={cat.slug}>
{cat.groupName ? `[${cat.groupName}] ` : ""}
{cat.name}
</option>
))}
</select>
</td>
<td className="px-3 py-2 text-zinc-400 text-[11px]">{row.groupName}</td>
<td className="px-3 py-2">
<input
className="w-full rounded border border-zinc-700 bg-zinc-900 px-2 py-1 text-xs text-zinc-100"
defaultValue={row.notes ?? ""}
onBlur={(e) => {
if (e.target.value !== (row.notes ?? "")) {
handleUpdate(row, { notes: e.target.value });
}
}}
/>
</td>
<td className="px-3 py-2 text-right">
<button
onClick={() => handleDelete(row)}
className="rounded border border-red-700/70 px-2 py-1 text-[11px] text-red-300 hover:bg-red-900/40"
>
Delete
</button>
</td>
</tr>
))}
{mappings.length === 0 && (
<tr>
<td
colSpan={6}
className="px-3 py-4 text-center text-xs text-zinc-500"
>
No mappings yet for {platform}. Click Add mapping to get started.
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More