diff --git a/components.json b/components.json
new file mode 100644
index 0000000..22b8552
--- /dev/null
+++ b/components.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": false,
+ "tailwind": {
+ "config": "",
+ "css": "src/index.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "registries": {}
+}
diff --git a/jsconfig.json b/jsconfig.json
new file mode 100644
index 0000000..2c8ee2b
--- /dev/null
+++ b/jsconfig.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ }
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index c38264f..9798bce 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,17 +8,25 @@
"name": "upload_otomation_fe",
"version": "0.0.0",
"dependencies": {
+ "@radix-ui/react-accordion": "^1.2.12",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-tabs": "^1.1.13",
"@reduxjs/toolkit": "^2.9.2",
"@tailwindcss/vite": "^4.1.16",
"axios": "^1.13.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
"framer-motion": "^12.23.24",
"jwt-decode": "^4.0.0",
+ "lucide-react": "^0.553.0",
"pdfjs-dist": "^5.4.394",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-redux": "^9.2.0",
"react-router-dom": "^7.9.5",
"stream-browserify": "^3.0.0",
+ "tailwind-merge": "^3.4.0",
+ "uuid": "^13.0.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
@@ -31,6 +39,7 @@
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
"tailwindcss": "^4.1.16",
+ "tw-animate-css": "^1.4.0",
"vite": "^7.1.7"
}
},
@@ -607,6 +616,44 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+ "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
+ "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.4"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+ "license": "MIT"
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -889,6 +936,630 @@
"node": ">= 10"
}
},
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-accordion": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz",
+ "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collapsible": "1.1.12",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collapsible": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
+ "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
+ "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
+ "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
+ "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+ "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+ "license": "MIT"
+ },
"node_modules/@reduxjs/toolkit": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.2.tgz",
@@ -1730,7 +2401,7 @@
"version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
@@ -1831,6 +2502,18 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1919,6 +2602,27 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
@@ -2053,6 +2757,12 @@
"node": ">=8"
}
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2564,6 +3274,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@@ -3101,6 +3820,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lucide-react": {
+ "version": "0.553.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.553.0.tgz",
+ "integrity": "sha512-BRgX5zrWmNy/lkVAe0dXBgd7XQdZ3HTf+Hwe3c9WK6dqgnj9h+hxV+MDncM88xDWlCq27+TKvHGE70ViODNILw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -3411,6 +4139,53 @@
}
}
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
+ "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-router": {
"version": "7.9.5",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz",
@@ -3449,6 +4224,28 @@
"react-dom": ">=18"
}
},
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -3656,6 +4453,16 @@
"node": ">=8"
}
},
+ "node_modules/tailwind-merge": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
+ "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
"node_modules/tailwindcss": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
@@ -3697,6 +4504,16 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/tw-animate-css": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
+ "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Wombosvideo"
+ }
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -3720,6 +4537,49 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
@@ -3735,6 +4595,19 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
+ "node_modules/uuid": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
+ "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist-node/bin/uuid"
+ }
+ },
"node_modules/vite": {
"version": "7.1.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
diff --git a/package.json b/package.json
index 66c494f..7b2723e 100644
--- a/package.json
+++ b/package.json
@@ -10,17 +10,25 @@
"preview": "vite preview"
},
"dependencies": {
+ "@radix-ui/react-accordion": "^1.2.12",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-tabs": "^1.1.13",
"@reduxjs/toolkit": "^2.9.2",
"@tailwindcss/vite": "^4.1.16",
"axios": "^1.13.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
"framer-motion": "^12.23.24",
"jwt-decode": "^4.0.0",
+ "lucide-react": "^0.553.0",
"pdfjs-dist": "^5.4.394",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-redux": "^9.2.0",
"react-router-dom": "^7.9.5",
"stream-browserify": "^3.0.0",
+ "tailwind-merge": "^3.4.0",
+ "uuid": "^13.0.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
@@ -33,6 +41,7 @@
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
"tailwindcss": "^4.1.16",
+ "tw-animate-css": "^1.4.0",
"vite": "^7.1.7"
}
}
diff --git a/src/components/MetaDataForm.jsx b/src/components/MetaDataForm.jsx
new file mode 100644
index 0000000..4549952
--- /dev/null
+++ b/src/components/MetaDataForm.jsx
@@ -0,0 +1,273 @@
+import { useState, useEffect } from "react";
+import { v4 as uuidv4 } from "uuid";
+import {
+ Tabs,
+ TabsList,
+ TabsTrigger,
+ TabsContent,
+} from "./ui/tabs";
+
+/**
+ * ๐ MetadataForm.jsx
+ * Form Metadata Geospasial berbasis ISO 19115 (Simplified)
+ * Menggunakan Tailwind CSS murni untuk tampilan modern dan profesional.
+ */
+
+export default function MetadataForm({ onChange }) {
+ const [formData, setFormData] = useState({
+ // ๐งฉ Identifikasi Dataset
+ title: "",
+ abstract: "",
+ keywords: "",
+ topicCategory: "",
+ dateCreated: "",
+ status: "",
+ language: "eng",
+
+ // ๐งญ Referensi Spasial
+ crs: "EPSG:4326",
+ geometryType: "",
+ xmin: "",
+ xmax: "",
+ ymin: "",
+ ymax: "",
+
+ // ๐ Distribusi / Akses Data
+ downloadLink: "",
+ serviceLink: "",
+ format: "",
+ license: "Copyright",
+
+ // ๐ค Informasi Penanggung Jawab
+ organization: "",
+ contactName: "",
+ contactEmail: "",
+ contactPhone: "",
+ role: "",
+
+ // ๐งพ Metadata Umum
+ metadataStandard: "ISO 19115:2003/19139",
+ metadataVersion: "1.0",
+ metadataUUID: "",
+ metadataDate: "",
+ charset: "utf8",
+ rsIdentifier: "WGS 1984"
+ });
+
+ // Generate UUID & tanggal metadata saat pertama kali load
+ useEffect(() => {
+ setFormData((prev) => ({
+ ...prev,
+ metadataUUID: uuidv4(),
+ metadataDate: new Date().toISOString().split("T")[0],
+ }));
+ }, []);
+
+ // Update handler umum
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ const updated = { ...formData, [name]: value };
+ setFormData(updated);
+ if (onChange) onChange(updated);
+ };
+
+ return (
+
+
+
+
+
+ {/* TAB LIST */}
+
+
+ ๐งฉ Identifikasi Dataset
+
+
+ ๐ค Penanggung Jawab
+
+
+
+ {/* TAB 1: IDENTIFIKASI */}
+
+
+
+
+ {/* TAB 2: PENANGGUNG JAWAB */}
+
+
+
+
+
+
+
+ );
+}
+
+/* ---------------------------------------------------
+ ๐ฆ Subkomponen Reusable untuk Input/Select/Textarea
+--------------------------------------------------- */
+function Section({ title, children }) {
+ return (
+
+ {/*
{title} */}
+
{children}
+
+ );
+}
+
+function Input({ label, name, type = "text", value, onChange, readOnly = false }) {
+ return (
+
+
+ {label}{" "}*
+
+
+
+ );
+}
+
+function Textarea({ label, name, value, onChange }) {
+ return (
+
+
+ {label}{" "}*
+
+
+
+ );
+}
+
+function Select({ label, name, value, onChange, options = [] }) {
+ return (
+
+
+ {label}{" "}*
+
+
+ -- Pilih --
+ {options.map((opt) => (
+
+ {opt}
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/ui/accordion.jsx b/src/components/ui/accordion.jsx
new file mode 100644
index 0000000..6b9f0b0
--- /dev/null
+++ b/src/components/ui/accordion.jsx
@@ -0,0 +1,61 @@
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDownIcon } from "lucide-react"
+import { cn } from "@/lib/utils";
+
+function Accordion({
+ ...props
+}) {
+ return ;
+}
+
+function AccordionItem({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function AccordionTrigger({
+ className,
+ children,
+ ...props
+}) {
+ return (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}>
+ {children}
+
+
+
+ );
+}
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/src/components/ui/dropdown-menu.jsx b/src/components/ui/dropdown-menu.jsx
new file mode 100644
index 0000000..13051d5
--- /dev/null
+++ b/src/components/ui/dropdown-menu.jsx
@@ -0,0 +1,221 @@
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function DropdownMenu({
+ ...props
+}) {
+ return ;
+}
+
+function DropdownMenuPortal({
+ ...props
+}) {
+ return ( );
+}
+
+function DropdownMenuTrigger({
+ ...props
+}) {
+ return ( );
+}
+
+function DropdownMenuContent({
+ className,
+ sideOffset = 4,
+ ...props
+}) {
+ return (
+
+
+
+ );
+}
+
+function DropdownMenuGroup({
+ ...props
+}) {
+ return ( );
+}
+
+function DropdownMenuItem({
+ className,
+ inset,
+ variant = "default",
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function DropdownMenuCheckboxItem({
+ className,
+ children,
+ checked,
+ ...props
+}) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ );
+}
+
+function DropdownMenuRadioGroup({
+ ...props
+}) {
+ return ( );
+}
+
+function DropdownMenuRadioItem({
+ className,
+ children,
+ ...props
+}) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ );
+}
+
+function DropdownMenuLabel({
+ className,
+ inset,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function DropdownMenuSeparator({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function DropdownMenuShortcut({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function DropdownMenuSub({
+ ...props
+}) {
+ return ;
+}
+
+function DropdownMenuSubTrigger({
+ className,
+ inset,
+ children,
+ ...props
+}) {
+ return (
+
+ {children}
+
+
+ );
+}
+
+function DropdownMenuSubContent({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+export {
+ DropdownMenu,
+ DropdownMenuPortal,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuLabel,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubTrigger,
+ DropdownMenuSubContent,
+}
diff --git a/src/components/ui/tabs.jsx b/src/components/ui/tabs.jsx
new file mode 100644
index 0000000..fcc9ce5
--- /dev/null
+++ b/src/components/ui/tabs.jsx
@@ -0,0 +1,89 @@
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+function Tabs({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+// function TabsList({
+// className,
+// ...props
+// }) {
+// return (
+//
+// );
+// }
+
+function TabsList({ className, ...props }) {
+ return (
+
+ )
+}
+
+
+// function TabsTrigger({
+// className,
+// ...props
+// }) {
+// return (
+//
+// );
+// }
+
+function TabsTrigger({ className, ...props }) {
+ return (
+
+ )
+}
+
+
+function TabsContent({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/src/components/upload/FilePreview.jsx b/src/components/upload/FilePreview.jsx
index 9ae8ccc..5fe6dc5 100644
--- a/src/components/upload/FilePreview.jsx
+++ b/src/components/upload/FilePreview.jsx
@@ -10,9 +10,9 @@ export default function FilePreview({ result }) {
} = result;
return (
-
+
{/* Section: Warning Table */}
- {warning_examples?.length > 0 ? (
+ {warning_examples?.length > 0 ?? (
โ ๏ธ Beberapa nama wilayah perlu diperiksa kembali.
@@ -35,19 +35,17 @@ export default function FilePreview({ result }) {
variant="warning"
/>
- ) : (
-
โ
Data 100% valid
)}
{/* Section: File Preview */}
-
๐ Cuplikan Data
+ {/*
๐ Cuplikan Data */}
0 ? 5 : 15}
variant="preview"
/>
@@ -57,10 +55,17 @@ export default function FilePreview({ result }) {
function Table({ title, columns, rows, total, limit = 100, variant = "preview" }) {
const displayedRows = rows.slice(0, limit);
+ const shorten = (text, max = 80) => {
+ if (!text) return "โ";
+ return text.length > max ? text.slice(0, max) + "..." : text;
+ };
+
return (
+
+
-
+
{row[col] !== null && row[col] !== undefined && row[col] !== ""
- ? row[col]
- : โ }
+ ? (
+ col === "geometry" ? (
+ shorten(row[col], 80)
+ ) : (
+ row[col] || โ
+ )
+ ) : (
+ โ
+ )}
))}
@@ -115,13 +127,16 @@ function Table({ title, columns, rows, total, limit = 100, variant = "preview" }
-
+
+
+
Menampilkan {Math.min(limit, displayedRows.length)} dari {total} baris.
{variant === "preview" && (
- Cuplikan sebagian data (maks. {limit} baris)
+ Cuplikan sebagian data
+ {/* (maks. {limit} baris) */}
)}
diff --git a/src/components/upload/MetadataForm_old.jsx b/src/components/upload/MetadataForm_old.jsx
new file mode 100644
index 0000000..925e373
--- /dev/null
+++ b/src/components/upload/MetadataForm_old.jsx
@@ -0,0 +1,205 @@
+import { useState, useEffect } from "react";
+import { v4 as uuidv4 } from "uuid";
+
+/**
+ * ๐ MetadataForm.jsx
+ * Form Metadata Geospasial berbasis ISO 19115 (Simplified)
+ * Menggunakan Tailwind CSS murni untuk tampilan modern dan profesional.
+ */
+
+export default function MetadataForm({ onChange }) {
+ const [formData, setFormData] = useState({
+ // ๐งฉ Identifikasi Dataset
+ title: "",
+ abstract: "",
+ keywords: "",
+ topicCategory: "",
+ dateCreated: "",
+ status: "",
+ language: "ind",
+
+ // ๐งญ Referensi Spasial
+ crs: "",
+ geometryType: "",
+ xmin: "",
+ xmax: "",
+ ymin: "",
+ ymax: "",
+
+ // ๐ Distribusi / Akses Data
+ downloadLink: "",
+ serviceLink: "",
+ format: "",
+ license: "",
+
+ // ๐ค Informasi Penanggung Jawab
+ organization: "",
+ contactName: "",
+ contactEmail: "",
+ contactPhone: "",
+ role: "",
+
+ // ๐งพ Metadata Umum
+ metadataStandard: "ISO 19115:2003/19139",
+ metadataVersion: "1.0",
+ metadataUUID: "",
+ metadataDate: "",
+ charset: "",
+ });
+
+ // Generate UUID & tanggal metadata saat pertama kali load
+ useEffect(() => {
+ setFormData((prev) => ({
+ ...prev,
+ metadataUUID: uuidv4(),
+ metadataDate: new Date().toISOString().split("T")[0],
+ }));
+ }, []);
+
+ // Update handler umum
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ const updated = { ...formData, [name]: value };
+ setFormData(updated);
+ if (onChange) onChange(updated);
+ };
+
+ return (
+
+
+ {/* ๐งฉ Bagian 1 โ Identifikasi Dataset */}
+
+
+ {/* FIXXXX AUTO */}
+ {/* ๐งญ Bagian 2 โ Referensi Spasial */}
+ {/*
*/}
+
+ {/* NANTI WAKTU PUBLIKASI */}
+ {/* ๐ Bagian 3 โ Distribusi / Akses Data */}
+ {/*
*/}
+
+ {/* ๐ค Bagian 4 โ Informasi Penanggung Jawab */}
+
+
+ {/* NANTI WAKTU PUBLIKASI */}
+ {/* ๐งพ Bagian 5 โ Metadata Umum */}
+ {/*
*/}
+
+ );
+}
+
+/* ---------------------------------------------------
+ ๐ฆ Subkomponen Reusable untuk Input/Select/Textarea
+--------------------------------------------------- */
+function Section({ title, children }) {
+ return (
+
+ );
+}
+
+function Input({ label, name, type = "text", value, onChange, readOnly = false }) {
+ return (
+
+
+ {label}
+
+
+
+ );
+}
+
+function Textarea({ label, name, value, onChange }) {
+ return (
+
+
+ {label}
+
+
+
+ );
+}
+
+function Select({ label, name, value, onChange, options = [] }) {
+ return (
+
+
+ {label}
+
+
+ -- Pilih --
+ {options.map((opt) => (
+
+ {opt}
+
+ ))}
+
+
+ );
+}
diff --git a/src/index.css b/src/index.css
index a461c50..7550e24 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1 +1,120 @@
-@import "tailwindcss";
\ No newline at end of file
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+}
+
+:root {
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/utils.js b/src/lib/utils.js
new file mode 100644
index 0000000..b20bf01
--- /dev/null
+++ b/src/lib/utils.js
@@ -0,0 +1,6 @@
+import { clsx } from "clsx";
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs) {
+ return twMerge(clsx(inputs));
+}
diff --git a/src/pages/admin/home/controller_admin_home.jsx b/src/pages/admin/home/controller_admin_home.jsx
index e69de29..d378432 100644
--- a/src/pages/admin/home/controller_admin_home.jsx
+++ b/src/pages/admin/home/controller_admin_home.jsx
@@ -0,0 +1,30 @@
+import { useEffect, useState } from "react";
+import { fetchAllDatasets } from "./service_admin_home";
+
+export function useAdminHomeController() {
+ const [datasets, setDatasets] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [errorMsg, setErrorMsg] = useState("");
+
+ const loadData = async () => {
+ setLoading(true);
+ try {
+ const data = await fetchAllDatasets();
+ setDatasets(data);
+ } catch (err) {
+ setErrorMsg(err?.message || "Terjadi kesalahan saat memuat data.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadData();
+ }, []);
+
+ return {
+ datasets,
+ loading,
+ errorMsg,
+ };
+}
diff --git a/src/pages/admin/home/service_admin_home.jsx b/src/pages/admin/home/service_admin_home.jsx
index e69de29..5346453 100644
--- a/src/pages/admin/home/service_admin_home.jsx
+++ b/src/pages/admin/home/service_admin_home.jsx
@@ -0,0 +1,11 @@
+import api from "../../../services/api";
+
+export async function fetchAllDatasets() {
+ try {
+ const res = await api.get("/dataset/metadata");
+ return res.data?.data || [];
+ } catch (err) {
+ console.error("Fetch datasets error:", err);
+ throw err.response?.data || err;
+ }
+}
diff --git a/src/pages/admin/home/views_admin_home.jsx b/src/pages/admin/home/views_admin_home.jsx
index 0e58ca8..28c1b7d 100644
--- a/src/pages/admin/home/views_admin_home.jsx
+++ b/src/pages/admin/home/views_admin_home.jsx
@@ -1,10 +1,168 @@
-import Sidebar from "../../../components/Sidebar";
+import LoadingOverlay from "../../../components/LoadingOverlay";
+import ErrorNotification from "../../../components/ErrorNotification";
+import { useAdminHomeController } from "./controller_admin_home";
+import {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator
+} from "../../../components/ui/dropdown-menu";
+import { Link } from "react-router-dom";
export default function ViewsAdminHome() {
+ const { datasets, loading, errorMsg } = useAdminHomeController();
+
return (
-
-
Dashboard Home
-
Selamat datang di panel admin upload automation.
+
+
+
+
{}} />
+
+
+
๐ Metadata Dataset
+
+ + Upload Baru
+
+
+
+ {/* Empty State */}
+ {datasets.length === 0 && !loading && (
+
+ Belum ada metadata dataset yang tersimpan.
+
+ )}
+
+ {/* CARD LIST */}
+
+ {datasets.map((item) => (
+
+
+
+ {item.dataset_title}
+
+
+ {/* STATUS BADGE */}
+
+ {item.dataset_status}
+
+
+
+
+ ๐
{new Date(item.created_at).toLocaleString()}
+
+
+
+
+
+ Nama Tabel: {" "}
+ {item.table_title}
+
+
+
+ Kategori: {" "}
+ {item.topic_category}
+
+
+
+ Organisasi: {" "}
+ {item.organization_name}
+
+
+
+ Kontak: {" "}
+ {item.contact_person_name}
+
+
+ {/* GEOM TYPE */}
+
+ Tipe Geometri:
+
+
+ {item.geom_type?.map((g, i) => (
+
+ {g}
+
+ ))}
+
+
+ {/* KEYWORDS */}
+
+
Kata Kunci:
+
+ {item.keywords
+ .split(",")
+ .map((k, i) => (
+
+ #{k.trim()}
+
+ ))}
+
+
+
+
+
+ {/* ACTIONS */}
+
+
+ {/* BUTTON: Buka di QGIS */}
+
+ ๐ Buka di QGIS
+
+
+ {/* MORE MENU */}
+
+
+
+ โฎ
+
+
+
+
+
+
+
+ Lihat Detail
+
+
+
+
+
+ console.log("Hapus:", item.id)}
+ className="text-red-600 cursor-pointer"
+ >
+ Hapus
+
+
+
+
+
+
+
+ ))}
+
);
}
diff --git a/src/pages/admin/upload/controller_admin_upload.jsx b/src/pages/admin/upload/controller_admin_upload.jsx
index d776bfa..7126e10 100644
--- a/src/pages/admin/upload/controller_admin_upload.jsx
+++ b/src/pages/admin/upload/controller_admin_upload.jsx
@@ -19,7 +19,7 @@ export function useUploadController() {
const [loading, setLoading] = useState(false);
const [selectedTable, setSelectedTable] = useState(null);
// const [selectedPages, setSelectedPages] = useState("");
- const [tableTitle, setTableTitle] = useState("");
+ const [tableTitle, setTableTitle] = useState("GTW");
// const [pdfPageCount, setPdfPageCount] = useState(null);
const [selectedSheet, setSelectedSheet] = useState(null);
const [sheetCount, setSheetCount] = useState(null);
@@ -44,19 +44,23 @@ export function useUploadController() {
// } catch (err) {
// console.error("Gagal membaca PDF:", err);
// }
- try {
- const reader = new FileReader();
- reader.onload = async (e) => {
- const typedArray = new Uint8Array(e.target.result);
- const pdf = await pdfjsLib.getDocument({ data: typedArray }).promise;
- dispatch(setPdfPageCount(pdf.numPages));
- console.log(`๐ PDF terdeteksi dengan ${pdf.numPages} halaman`);
- navigate("/admin/upload/pdf"); // ๐ otomatis pindah ke viewer
- };
- reader.readAsArrayBuffer(f);
- } catch (err) {
- console.error("Gagal membaca PDF:", err);
- }
+
+ // try {
+ // const reader = new FileReader();
+ // reader.onload = async (e) => {
+ // const typedArray = new Uint8Array(e.target.result);
+ // const pdf = await pdfjsLib.getDocument({ data: typedArray }).promise;
+ // dispatch(setPdfPageCount(pdf.numPages));
+ // navigate("/admin/upload/pdf");
+ // };
+ // reader.readAsArrayBuffer(f);
+ // } catch (err) {
+ // console.error("Gagal membaca PDF:", err);
+ // }
+
+
+
+ navigate("/admin/upload/pdf");
}
else if (ext === "xlsx" || ext === "xls") {
const data = await f.arrayBuffer();
@@ -104,13 +108,14 @@ export function useUploadController() {
}
};
- const handleConfirmUpload = async () => {
+ const handleConfirmUpload = async (metadata) => {
setLoading(true);
try {
const data = {
- title: tableTitle,
+ title: metadata.title,
columns: result.columns,
rows: result.preview,
+ author: metadata
};
const res = await saveToDatabase(data);
dispatch(setValidatedData(res));
diff --git a/src/pages/admin/upload/pdf_viewer/controller_pdf_viewer.jsx b/src/pages/admin/upload/pdf_viewer/controller_pdf_viewer.jsx
index e7ecfef..18621bd 100644
--- a/src/pages/admin/upload/pdf_viewer/controller_pdf_viewer.jsx
+++ b/src/pages/admin/upload/pdf_viewer/controller_pdf_viewer.jsx
@@ -1,8 +1,8 @@
import { useDispatch, useSelector } from "react-redux";
import { useState } from "react";
import * as pdfjsLib from "pdfjs-dist";
-import { setSelectedPages } from "../../../../store/slices/uploadSlice";
-import { uploadPdf } from "../service_admin_upload";
+import { setSelectedPages, setResult } from "../../../../store/slices/uploadSlice";
+import { uploadFile } from "../service_admin_upload";
import { useNavigate } from "react-router-dom";
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
@@ -13,12 +13,15 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
export function usePdfViewerController() {
const dispatch = useDispatch();
const navigate = useNavigate();
- const { file } = useSelector((state) => state.upload);
+ const { file, selectedPages } = useSelector((state) => state.upload);
const [pages, setPages] = useState([]);
- const [selectedPages, setSelectedPagesLocal] = useState([]);
+ const [selectedPagesLocal, setSelectedPagesLocal] = useState([]);
const [loading, setLoading] = useState(false);
+
+ const [errorMsg, setErrorMsg] = useState("");
+
// Render PDF menjadi gambar
const loadPdfPages = async (pdfFile) => {
setLoading(true);
@@ -55,7 +58,7 @@ export function usePdfViewerController() {
// Toggle halaman yang dipilih
const toggleSelectPage = (pageNum) => {
- let updated = [...selectedPages];
+ let updated = [...selectedPagesLocal];
if (updated.includes(pageNum)) {
updated = updated.filter((p) => p !== pageNum);
} else {
@@ -67,14 +70,21 @@ export function usePdfViewerController() {
};
const handleProcessPdf = async () => {
- if (selectedPages.length === 0) return;
+ if (selectedPagesLocal.length === 0) return;
try {
setLoading(true);
- const res = await uploadPdf({ pages: selectedPages });
- console.log("PDF processed:", res);
- navigate("/admin/upload/validate");
+ const res = await uploadFile(file, selectedPagesLocal);
+ dispatch(setResult(res));
+
+ if (!res.tables) {
+ navigate("/admin/upload/validate");
+ } else if(!Array.isArray(res.tables)) {
+ setErrorMsg(res.message);
+ } else {
+ navigate("/admin/upload/table-selector");
+ }
} catch (err) {
- console.error(err);
+ setErrorMsg(err);
} finally {
setLoading(false);
}
@@ -84,9 +94,10 @@ export function usePdfViewerController() {
file,
pages,
loading,
- selectedPages,
+ selectedPagesLocal,
loadPdfPages,
toggleSelectPage,
handleProcessPdf,
+ errorMsg, setErrorMsg
};
}
diff --git a/src/pages/admin/upload/pdf_viewer/views_admin_pdf_viewer.jsx b/src/pages/admin/upload/pdf_viewer/views_admin_pdf_viewer.jsx
index 9430d43..4e0ca5c 100644
--- a/src/pages/admin/upload/pdf_viewer/views_admin_pdf_viewer.jsx
+++ b/src/pages/admin/upload/pdf_viewer/views_admin_pdf_viewer.jsx
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { usePdfViewerController } from "./controller_pdf_viewer";
import LoadingOverlay from "../../../../components/LoadingOverlay";
+import ErrorNotification from "@/components/ErrorNotification";
import { motion } from "framer-motion";
export default function ViewsAdminPdfViewer() {
@@ -9,10 +10,11 @@ export default function ViewsAdminPdfViewer() {
file,
pages,
loading,
- selectedPages,
+ selectedPagesLocal,
toggleSelectPage,
handleProcessPdf,
loadPdfPages,
+ errorMsg, setErrorMsg
} = usePdfViewerController();
const navigate = useNavigate();
@@ -26,68 +28,74 @@ export default function ViewsAdminPdfViewer() {
return (
- {/* Sidebar kiri */}
-
-
Daftar Halaman
-
- {pages.map((p) => (
-
- toggleSelectPage(p.pageNum)}
- />
- Halaman {p.pageNum}
-
- ))}
-
+
+
setErrorMsg("")}
+ />
-
-
- Dipilih: {" "}
- {selectedPages.length > 0
- ? selectedPages.join(", ")
- : "Belum ada halaman"}
-
-
- Maksimal 3 halaman yang dapat dipilih.
-
+ {/* Left Sidebar */}
+
+
Daftar Halaman
+
+ {pages.map((p) => (
+
+ toggleSelectPage(p.pageNum)}
+ />
+ Halaman {p.pageNum}
+
+ ))}
+
-
- Proses Halaman
-
-
-
-
- {/* Konten kanan (viewer) */}
-
-
-
- {pages.map((p) => (
-
-
-
- Halaman {p.pageNum}
+
+
+ Dipilih: {" "}
+ {selectedPagesLocal.length > 0
+ ? selectedPagesLocal.join(", ")
+ : "Belum ada halaman"}
-
- ))}
+
+ Maksimal 3 halaman yang dapat dipilih.
+
+
+
+ Proses Halaman
+
+
+
+
+ {/* Konten kanan (viewer) */}
+
+
+
+ {pages.map((p) => (
+
+
+
+ Halaman {p.pageNum}
+
+
+ ))}
+
-
);
}
diff --git a/src/pages/admin/upload/service_admin_upload.jsx b/src/pages/admin/upload/service_admin_upload.jsx
index 3630672..48687ed 100644
--- a/src/pages/admin/upload/service_admin_upload.jsx
+++ b/src/pages/admin/upload/service_admin_upload.jsx
@@ -38,9 +38,9 @@ export async function uploadFile(file, page = null, sheet = null) {
const response = await api.post("/upload/file", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
- return response.data;
+ return response.data.data;
} catch (error) {
- throw error.response?.data.detail || "Gagal proses file.";
+ throw error.response?.data.detail.message || "Gagal proses file.";
}
}
@@ -49,9 +49,9 @@ export async function uploadPdf(data) {
const response = await api.post("/upload/process-pdf", data, {
headers: { "Content-Type": "application/json" },
});
- return response.data;
+ return response.data.data;
} catch (error) {
- throw error.response?.data.detail || { message: "Gagal proses file." };
+ throw error.response?.data.detail.message || { message: "Gagal proses file." };
}
}
@@ -61,8 +61,8 @@ export async function saveToDatabase(data) {
const response = await api.post("/upload/to-postgis", data, {
headers: { "Content-Type": "application/json" },
});
- return response.data;
+ return response.data.data;
} catch (error) {
- throw error.response?.data.detail || { message: "Gagal upload data." };
+ throw error.response?.data.detail.message || { message: "Gagal upload data." };
}
}
diff --git a/src/pages/admin/upload/table_picker/controller_admin_table_picker.jsx b/src/pages/admin/upload/table_picker/controller_admin_table_picker.jsx
new file mode 100644
index 0000000..c6406da
--- /dev/null
+++ b/src/pages/admin/upload/table_picker/controller_admin_table_picker.jsx
@@ -0,0 +1,41 @@
+import { useState } from "react";
+import { useSelector, useDispatch } from "react-redux";
+import { useNavigate } from "react-router-dom";
+import { setResult } from "../../../../store/slices/uploadSlice";
+import { uploadPdf } from "../service_admin_upload";
+
+export function useTablePickerController() {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const [loading, setLoading] = useState(false);
+ const { result } = useSelector((state) => state.upload); // result dari BE upload PDF
+ const [selectedTable, setSelectedTableLocal] = useState(
+ result?.tables?.[0] || null
+ );
+
+ const handleSelectTable = (t) => {
+ setSelectedTableLocal(t);
+ // dispatch(setSelectedTable(t));
+ };
+
+ const handleNext = async () => {
+ if (!selectedTable) return;
+ setLoading(true);
+ try {
+ const res = await uploadPdf(selectedTable);
+ dispatch(setResult(res));
+ navigate("/admin/upload/validate");
+ } catch(err){
+ setLoading(false);
+ throw err
+ }
+ };
+
+ return {
+ loading,
+ result,
+ selectedTable,
+ handleSelectTable,
+ handleNext,
+ };
+}
diff --git a/src/pages/admin/upload/table_picker/views_admin_table_picker.jsx b/src/pages/admin/upload/table_picker/views_admin_table_picker.jsx
new file mode 100644
index 0000000..972604e
--- /dev/null
+++ b/src/pages/admin/upload/table_picker/views_admin_table_picker.jsx
@@ -0,0 +1,112 @@
+import { Navigate } from "react-router-dom";
+import { useTablePickerController } from "./controller_admin_table_picker";
+import LoadingOverlay from "../../../../components/LoadingOverlay";
+
+export default function ViewsAdminTablePicker() {
+ const { loading, result, selectedTable, handleSelectTable, handleNext } =
+ useTablePickerController();
+
+ if (!result) {
+ return
;
+ }
+
+ return (
+
+
+ {/* Sidebar kiri */}
+
+
Daftar Tabel
+
+
+ {result.tables.map((t, i) => (
+ handleSelectTable(t)}
+ className={`flex items-center justify-between px-3 py-2 rounded cursor-pointer border transition ${
+ selectedTable?.title === t.title
+ ? "bg-blue-100 border-blue-400 font-semibold"
+ : "bg-white hover:bg-blue-50 border-gray-200"
+ }`}
+ >
+ Tabel {t.title}
+ {selectedTable?.title === t.title && โ
}
+
+ ))}
+
+
+
+
+ Dipilih: {" "}
+ {selectedTable ? `Tabel ${selectedTable.title}` : "Belum ada"}
+
+
+
+ Proses Tabel โ
+
+
+
+
+ {/* Konten kanan (tabel preview) */}
+
+
+ {selectedTable ? (
+
+
+ ๐ Tabel {selectedTable.title}
+
+
+
+
+
+
+ {selectedTable.columns.map((col, idx) => (
+
+ {col}
+
+ ))}
+
+
+
+ {selectedTable.rows.slice(0, 10).map((row, rowIdx) => (
+
+ {selectedTable.columns.map((_, colIdx) => (
+
+ {row[colIdx]}
+
+ ))}
+
+ ))}
+
+
+
+
+
+ Menampilkan {Math.min(10, selectedTable.rows.length)} dari{" "}
+ {selectedTable.rows.length} baris.
+
+
+ ) : (
+
+ Tidak ada tabel yang dipilih.
+
+ )}
+
+
+
+ );
+}
diff --git a/src/pages/admin/upload/views_admin_success_upload.jsx b/src/pages/admin/upload/views_admin_success_upload.jsx
index 25665c8..d0fc52e 100644
--- a/src/pages/admin/upload/views_admin_success_upload.jsx
+++ b/src/pages/admin/upload/views_admin_success_upload.jsx
@@ -4,23 +4,19 @@ import { Navigate } from "react-router-dom";
export default function ViewsAdminUploadSuccess() {
const { validatedData } = useSelector((state) => state.upload);
-
+ const geomIcons = {
+ Point: "๐",
+ MultiPoint: "๐น",
+ LineString: "๐",
+ MultiLineString: "๐ฃ๏ธ",
+ Polygon: "โฌ",
+ MultiPolygon: "๐พ",
+ GeometryCollection: "๐งฉ",
+ };
if (!validatedData) {
- // return (
- //
- //
โ ๏ธ Tidak Ada Data
- //
- // Tidak ditemukan hasil upload yang baru. Silakan unggah data terlebih dahulu.
- //
- //
- // Kembali ke Halaman Upload
- //
- //
- // );
return
;
+ }else{
+ console.log('success', validatedData);
}
return (
@@ -30,7 +26,6 @@ export default function ViewsAdminUploadSuccess() {
Data Anda berhasil disimpan ke database.
- {/* Ringkasan hasil dari backend */}
@@ -57,7 +52,6 @@ export default function ViewsAdminUploadSuccess() {
- {/* Detail List */}
{validatedData.table_name && (
@@ -75,6 +69,18 @@ export default function ViewsAdminUploadSuccess() {
)}
+ {validatedData.geometry_type && (
+
+ ๐งญ Jenis Geometry
+
+ {/* {validatedData.geometry_type.join(", ")} */}
+ {validatedData.geometry_type.map(
+ (g) => `${geomIcons[g] || "โ"} ${g}`
+ ).join(", ")}
+
+
+ )}
+
{validatedData.upload_time && (
๐ Waktu Upload
@@ -90,7 +96,8 @@ export default function ViewsAdminUploadSuccess() {
{validatedData.message && (
- {validatedData.message}
+ {/* {validatedData.message} */}
+ Datasets berhasil di upload
)}
diff --git a/src/pages/admin/upload/views_admin_validate_upload.jsx b/src/pages/admin/upload/views_admin_validate_upload.jsx
index b7eb585..0437c97 100644
--- a/src/pages/admin/upload/views_admin_validate_upload.jsx
+++ b/src/pages/admin/upload/views_admin_validate_upload.jsx
@@ -1,14 +1,183 @@
-import { useState, useEffect } from "react";
+// import { useState, useEffect } from "react";
+// import { useUploadController } from "./controller_admin_upload";
+// import { useSelector } from "react-redux";
+// import LoadingOverlay from "../../../components/LoadingOverlay";
+// import Notification from "../../../components/Notification";
+// import ErrorNotification from "../../../components/ErrorNotification";
+// import { Navigate } from "react-router-dom";
+// import FilePreview from "../../../components/upload/FilePreview";
+// import MetadataForm from "../../../components/MetaDataForm";
+
+// export default function ViewsAdminUploadValidate() {
+// const { result, file } = useSelector((state) => state.upload);
+// const {
+// loading,
+// tableTitle,
+// setTableTitle,
+// handleConfirmUpload,
+// } = useUploadController();
+
+// const [showAlert, setShowAlert] = useState(false);
+// const [alertMessage, setAlertMessage] = useState("");
+// const [alertType, setAlertType] = useState("info");
+// const [errorMsg, setErrorMsg] = useState("");
+
+// useEffect(() => {
+// const handleBeforeUnload = (e) => {
+// if (result && !loading) {
+// e.preventDefault();
+// e.returnValue =
+// "Data upload Anda belum disimpan. Jika Anda meninggalkan halaman ini, data akan hilang.";
+// return e.returnValue;
+// }
+// };
+
+// window.addEventListener("beforeunload", handleBeforeUnload);
+// return () => window.removeEventListener("beforeunload", handleBeforeUnload);
+// }, [result, loading]);
+
+// useEffect(() => {
+// const handleNavigation = (e) => {
+// if (result && !loading) {
+// const confirmLeave = window.confirm(
+// "Data upload Anda belum disimpan. Jika Anda meninggalkan halaman ini, data akan hilang."
+// );
+// if (!confirmLeave) {
+// e.preventDefault();
+// window.history.pushState(null, "", window.location.href); // tetap di halaman
+// }
+// }
+// };
+
+// window.addEventListener("popstate", handleNavigation);
+// return () => window.removeEventListener("popstate", handleNavigation);
+// }, [result, loading]);
+
+// const handleMetadataChange = (data) => {
+// console.log("Metadata Updated:", data);
+// };
+
+// // if (!result)
+// // return
Data belum diupload.
;
+
+// if (!result) return
;
+
+// const handleUploadClick = async () => {
+// if (!tableTitle.trim()) {
+// setAlertMessage(
+// "โJudul tabel belum diisi. Silakan isi sebelum melanjutkan."
+// );
+// setAlertType("error");
+// setShowAlert(true);
+// return;
+// }
+
+// // handleConfirmUpload();
+// try {
+// await handleConfirmUpload();
+// } catch (err) {
+// setErrorMsg(err);
+// }
+// };
+
+// return (
+//
+// {showAlert && (
+//
setShowAlert(false)}
+// />
+// )}
+
+// setErrorMsg("")}
+// />
+
+//
+
+// โ
Validasi & Konfirmasi Data
+
+//
+
+//
+//
+// Judul Tabel
+//
+// setTableTitle(e.target.value)}
+// placeholder="Masukkan judul tabel..."
+// className={`w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 ${
+// !tableTitle ? "border-red-400" : ""
+// }`}
+// />
+//
+// Judul akan dijadikan nama tabel pada database
+//
+//
+
+// {result && }
+
+//
+// history.back()}
+// className="px-5 py-2 text-blue-600 hover:underline"
+// >
+// โ Kembali
+//
+
+//
+// {loading ? "Mengunggah..." : "Upload ke Database"}
+//
+//
+//
+// );
+// }
+
+
+
+
+
+
+ //
+ //
+ //
+
+
+
+
+import { useEffect, useState } from "react";
import { useUploadController } from "./controller_admin_upload";
import { useSelector } from "react-redux";
+import { Navigate } from "react-router-dom";
+
import LoadingOverlay from "../../../components/LoadingOverlay";
import Notification from "../../../components/Notification";
import ErrorNotification from "../../../components/ErrorNotification";
-import { Navigate } from "react-router-dom";
+import MetadataForm from "../../../components/MetaDataForm";
import FilePreview from "../../../components/upload/FilePreview";
+// shadcn accordion (pastikan path sesuai proyekmu)
+import {
+ Accordion,
+ AccordionItem,
+ AccordionTrigger,
+ AccordionContent,
+} from "../../../components/ui/accordion";
+
export default function ViewsAdminUploadValidate() {
- const { result, file } = useSelector((state) => state.upload);
+ const { result } = useSelector((state) => state.upload);
const {
loading,
tableTitle,
@@ -16,67 +185,58 @@ export default function ViewsAdminUploadValidate() {
handleConfirmUpload,
} = useUploadController();
+ const [errorMsg, setErrorMsg] = useState("");
const [showAlert, setShowAlert] = useState(false);
const [alertMessage, setAlertMessage] = useState("");
const [alertType, setAlertType] = useState("info");
- const [errorMsg, setErrorMsg] = useState("");
- useEffect(() => {
- const handleBeforeUnload = (e) => {
- if (result && !loading) {
- e.preventDefault();
- e.returnValue =
- "Data upload Anda belum disimpan. Jika Anda meninggalkan halaman ini, data akan hilang.";
- return e.returnValue;
- }
- };
+ // Local state: index tabel yg dipilih (default 0)
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ // Metadata form state is emitted via onChange from MetadataForm; simpan jika perlu
+ const [metadata, setMetadata] = useState(null);
- window.addEventListener("beforeunload", handleBeforeUnload);
- return () => window.removeEventListener("beforeunload", handleBeforeUnload);
- }, [result, loading]);
-
- useEffect(() => {
- const handleNavigation = (e) => {
- if (result && !loading) {
- const confirmLeave = window.confirm(
- "Data upload Anda belum disimpan. Jika Anda meninggalkan halaman ini, data akan hilang."
- );
- if (!confirmLeave) {
- e.preventDefault();
- window.history.pushState(null, "", window.location.href); // tetap di halaman
- }
- }
- };
-
- window.addEventListener("popstate", handleNavigation);
- return () => window.removeEventListener("popstate", handleNavigation);
- }, [result, loading]);
-
- // if (!result)
- // return
Data belum diupload.
;
-
+ // Guard: jika tidak ada result -> kembalikan ke halaman upload
if (!result) return
;
-
+
+ // Keep selectedIndex valid ketika result berubah
+ useEffect(() => {
+ if (!result || !result.tables || result.tables.length === 0) {
+ setSelectedIndex(0);
+ return;
+ }
+ // clamp index
+ setSelectedIndex((idx) => {
+ if (!result.tables) return 0;
+ if (idx < 0) return 0;
+ if (idx >= result.tables.length) return result.tables.length - 1;
+ return idx;
+ });
+ }, [result]);
+
const handleUploadClick = async () => {
- if (!tableTitle.trim()) {
- setAlertMessage(
- "โJudul tabel belum diisi. Silakan isi sebelum melanjutkan."
- );
+ if (!tableTitle || !tableTitle.trim()) {
+ setAlertMessage("โJudul tabel belum diisi. Silakan isi sebelum melanjutkan.");
setAlertType("error");
setShowAlert(true);
return;
}
-
- // handleConfirmUpload();
try {
- await handleConfirmUpload();
+ await handleConfirmUpload(metadata);
} catch (err) {
- setErrorMsg(err);
+ // tangani error dari controller/service
+ const message =
+ err?.response?.data?.detail ||
+ err?.message ||
+ "Terjadi kesalahan saat mengunggah ke database.";
+ setErrorMsg(message);
}
};
+ const selectedTable = result.tables?.[selectedIndex] || null;
+
return (
-
+
+ {/* Alerts */}
{showAlert && (
setShowAlert(false)}
/>
)}
-
- setErrorMsg("")}
- />
-
+ setErrorMsg("")} />
โ
Validasi & Konfirmasi Data
-
-
- Judul Tabel
-
- setTableTitle(e.target.value)}
- placeholder="Masukkan judul tabel..."
- className={`w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 ${
- !tableTitle ? "border-red-400" : ""
- }`}
- />
-
- Judul akan dijadikan nama tabel pada database
-
-
+ {/* SINGLE ACCORDION */}
+
+
+
+ ๐ Dataset 1
+
- {result && }
+
+
+ {/* LEFT: tabel preview (6 kolom pada layout 12) */}
+
-
-
history.back()}
- className="px-5 py-2 text-blue-600 hover:underline"
- >
- โ Kembali
-
+ {/* RIGHT: metadata form (6 kolom) */}
+
+
๐งพ Metadata
-
- {loading ? "Mengunggah..." : "Upload ke Database"}
-
-
+ {/* MetadataForm menyimpan hasil ke parent via onChange */}
+
setMetadata(data)}/>
+
+
+
+
+ {/* ACTIONS di bawah accordion content */}
+
+
history.back()}
+ className="px-5 py-2 text-blue-600 hover:underline"
+ >
+ โ Kembali
+
+
+
+ {/* optional: show metadata summary brief */}
+ {metadata && (
+
+ Metadata siap โ preview: {metadata.title || "-"}
+
+ )}
+
+
+ {loading ? "Mengunggah..." : "Upload ke Database"}
+
+
+
+
+
+
);
}
diff --git a/src/routes/AppRouter.jsx b/src/routes/AppRouter.jsx
index d715afe..dd46e81 100644
--- a/src/routes/AppRouter.jsx
+++ b/src/routes/AppRouter.jsx
@@ -103,6 +103,7 @@ import ViewsAdminHome from "../pages/admin/home/views_admin_home";
import ViewsAdminUploadStep1 from "../pages/admin/upload/views_admin_upload";
import ViewsAdminUploadValidate from "../pages/admin/upload/views_admin_validate_upload";
import ViewsAdminPdfViewer from "../pages/admin/upload/pdf_viewer/views_admin_pdf_viewer";
+import ViewsAdminTablePicker from "../pages/admin/upload/table_picker/views_admin_table_picker";
import ViewsAdminUploadSuccess from "../pages/admin/upload/views_admin_success_upload";
import ViewsAdminPublikasi from "../pages/admin/publikasi/views_admin_publikasi";
import ViewsAdminUploadRules from "../pages/admin/upload/rules/views_admin_rules_upload";
@@ -129,6 +130,7 @@ const router = createBrowserRouter(
{ path: "upload", element:
},
{ path: "upload/validate", element:
},
{ path: "upload/pdf", element:
},
+ { path: "upload/table-selector", element:
},
{ path: "upload/success", element:
},
{ path: "upload/rules", element:
},
{ path: "publikasi", element:
},
diff --git a/src/services/api.js b/src/services/api.js
index 483d12e..8c20097 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -2,7 +2,9 @@ import axios from "axios";
import { getToken } from "../utils/auth";
const api = axios.create({
- baseURL: "http://labai.polinema.ac.id:808",
+ // baseURL: "http://labai.polinema.ac.id:808",
+ baseURL: "http://localhost:8000",
+ // baseURL:"https://kkqc31ns-8000.asse.devtunnels.ms"
});
api.interceptors.request.use((config) => {
diff --git a/src/store/slices/uploadSlice.js b/src/store/slices/uploadSlice.js
index 889649d..a3dbe76 100644
--- a/src/store/slices/uploadSlice.js
+++ b/src/store/slices/uploadSlice.js
@@ -5,7 +5,8 @@ const initialState = {
result: null,
validatedData: null,
pdfPageCount: null,
- selectedPages: null
+ selectedPages: null,
+ validTable: null
};
const uploadSlice = createSlice({
@@ -27,6 +28,9 @@ const uploadSlice = createSlice({
setSelectedPages: (state, action) => {
state.selectedPages = action.payload;
},
+ setValidTable: (state, action) => {
+ state.validTable = action.payload;
+ },
reset: (state) => {
state.file = null;
state.result = null;
@@ -37,5 +41,5 @@ const uploadSlice = createSlice({
},
});
-export const { setFile, setResult, setValidatedData, reset, setPdfPageCount, setSelectedPages } = uploadSlice.actions;
+export const { setFile, setResult, setValidatedData, reset, setPdfPageCount, setSelectedPages, setValidTable } = uploadSlice.actions;
export default uploadSlice.reducer;
diff --git a/vite.config.js b/vite.config.js
index 8bfd11d..395dc08 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,6 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import tailwindcss from '@tailwindcss/vite'
+import * as path from "path";
// https://vite.dev/config/
export default defineConfig({
@@ -11,6 +12,7 @@ export default defineConfig({
resolve: {
alias: {
stream: 'stream-browserify',
+ "@": path.resolve(__dirname, "./src"),
},
},
})