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 */} + +
+ + + +
+ ); +} + +function Select({ label, name, value, onChange, options = [] }) { + return ( +
+ + +
+ ); +} 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 */} +
+ + +
+ ); +} + +function Select({ label, name, value, onChange, options = [] }) { + return ( +
+ + +
+ ); +} 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) => ( - - ))} -
+ + setErrorMsg("")} + /> -
-

- Dipilih:{" "} - {selectedPages.length > 0 - ? selectedPages.join(", ") - : "Belum ada halaman"} -

-

- Maksimal 3 halaman yang dapat dipilih. -

+ {/* Left Sidebar */} +
+

Daftar Halaman

+
+ {pages.map((p) => ( + + ))} +
- -
-
- - {/* Konten kanan (viewer) */} -
- -
- {pages.map((p) => ( - - {`Halaman -

- Halaman {p.pageNum} +

+

+ Dipilih:{" "} + {selectedPagesLocal.length > 0 + ? selectedPagesLocal.join(", ") + : "Belum ada halaman"}

- - ))} +

+ Maksimal 3 halaman yang dapat dipilih. +

+ + +
+
+ + {/* Konten kanan (viewer) */} +
+ +
+ {pages.map((p) => ( + + {`Halaman +

+ 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) => ( + + ))} +
+ +
+

+ Dipilih:{" "} + {selectedTable ? `Tabel ${selectedTable.title}` : "Belum ada"} +

+ + +
+
+ + {/* Konten kanan (tabel preview) */} +
+
+ {selectedTable ? ( +
+

+ ๐Ÿ“„ Tabel {selectedTable.title} +

+ +
+ + + + {selectedTable.columns.map((col, idx) => ( + + ))} + + + + {selectedTable.rows.slice(0, 10).map((row, rowIdx) => ( + + {selectedTable.columns.map((_, colIdx) => ( + + ))} + + ))} + +
+ {col} +
+ {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

+ +// + +//
+// +// 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 && } + +//
+// + +// +//
+//
+// ); +// } + + + + + + + //
+ // + //
+ + + + +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

-
- - 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) */} +
+

๐Ÿงพ Cuplikan Data

+
+
+ +
+
+
-
- + {/* RIGHT: metadata form (6 kolom) */} +
+

๐Ÿงพ Metadata

- -
+ {/* MetadataForm menyimpan hasil ke parent via onChange */} + setMetadata(data)}/> + +
+
+ + {/* ACTIONS di bawah accordion content */} +
+ + +
+ {/* optional: show metadata summary brief */} + {metadata && ( +
+ Metadata siap โ€” preview: {metadata.title || "-"} +
+ )} + + +
+
+
+
+
); } 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"), }, }, })