Compare commits
8 Commits
fb3c23fa46
...
it-works
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e5b86967b | |||
| 6cf3ea7c94 | |||
| caf4817d12 | |||
| 05b30d8682 | |||
| dbcd06d192 | |||
| bab21e4ee3 | |||
| 2fec870302 | |||
| cb16698940 |
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -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
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Ask2AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -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
@@ -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>
|
||||||
Generated
+93
@@ -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">{
|
||||||
|
"associatedIndex": 1
|
||||||
|
}</component>
|
||||||
|
<component name="ProjectId" id="361l08WqyxLtDro87xvcOWn3FhH" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent">{
|
||||||
|
"keyToString": {
|
||||||
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
|
"git-widget-placeholder": "main",
|
||||||
|
"ignore.virus.scanning.warn.message": "true",
|
||||||
|
"js.debugger.nextJs.config.created.client": "true",
|
||||||
|
"js.debugger.nextJs.config.created.server": "true",
|
||||||
|
"kotlin-language-version-configured": "true",
|
||||||
|
"last_opened_file_path": "//wsl.localhost/Arch/home/don/code/shadow-gunbuilder-ai-proto",
|
||||||
|
"node.js.detected.package.eslint": "true",
|
||||||
|
"node.js.detected.package.tslint": "true",
|
||||||
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
|
"nodejs_package_manager_path": "npm",
|
||||||
|
"vue.rearranger.settings.migration": "true"
|
||||||
|
}
|
||||||
|
}</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>
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
{"type": "commonjs"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -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":[]}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 3,
|
|
||||||
"middleware": {},
|
|
||||||
"functions": {},
|
|
||||||
"sortedMiddleware": []
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
self.__REACT_LOADABLE_MANIFEST="{}"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{},\"appUsingSizeAdjust\":false,\"pagesUsingSizeAdjust\":false}"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"pages":{},"app":{},"appUsingSizeAdjust":false,"pagesUsingSizeAdjust":false}
|
|
||||||
@@ -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="
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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 +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"; }
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ }
|
|
||||||
);
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -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__'
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
{"type": "module"}
|
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 →
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
Reference in New Issue
Block a user