diff --git a/package-lock.json b/package-lock.json index 16c5883..c38264f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@reduxjs/toolkit": "^2.9.2", "@tailwindcss/vite": "^4.1.16", "axios": "^1.13.0", + "framer-motion": "^12.23.24", "jwt-decode": "^4.0.0", "pdfjs-dist": "^5.4.394", "react": "^19.1.1", @@ -2489,6 +2490,33 @@ "node": ">=0.8" } }, + "node_modules/framer-motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", + "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3125,6 +3153,21 @@ "node": "*" } }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3648,6 +3691,12 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 9249dfa..66c494f 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@reduxjs/toolkit": "^2.9.2", "@tailwindcss/vite": "^4.1.16", "axios": "^1.13.0", + "framer-motion": "^12.23.24", "jwt-decode": "^4.0.0", "pdfjs-dist": "^5.4.394", "react": "^19.1.1", diff --git a/src/components/LoadingOverlay.jsx b/src/components/LoadingOverlay.jsx new file mode 100644 index 0000000..1cd1a78 --- /dev/null +++ b/src/components/LoadingOverlay.jsx @@ -0,0 +1,43 @@ +import { motion } from "framer-motion"; + +export default function LoadingOverlay({ show = false, text = "Memproses data..." }) { + if (!show) return null; + + return ( +
+ {/* Animated Dots */} + + {[0, 1, 2].map((i) => ( + + ))} + + + {/* Text with subtle fade animation */} + + {text} + +
+ ); +} diff --git a/src/components/Notification.jsx b/src/components/Notification.jsx index 3a24102..ed4a30f 100644 --- a/src/components/Notification.jsx +++ b/src/components/Notification.jsx @@ -11,7 +11,7 @@ export default function Notification({ message, type = "info", onClose }) { useEffect(() => { const timer = setTimeout(() => { onClose(); - }, 3000); // auto close 3 detik + }, 2000); // auto close 3 detik return () => clearTimeout(timer); }, [onClose]); diff --git a/src/pages/admin/upload/controller_admin_upload.jsx b/src/pages/admin/upload/controller_admin_upload.jsx index f6bfee2..9d5b896 100644 --- a/src/pages/admin/upload/controller_admin_upload.jsx +++ b/src/pages/admin/upload/controller_admin_upload.jsx @@ -64,15 +64,16 @@ export function useUploadController() { if (!file) return; setLoading(true); try { - console.log(selectedPages); const res = await uploadFile(file, selectedPages, selectedSheet); dispatch(setResult(res)); if (res.file_type !== ".pdf" || (res.file_type === ".pdf" && !res.tables)) { navigate("/admin/upload/validate"); + }else{ + setLoading(false); } } finally { - setLoading(false); + // setLoading(false); } }; @@ -84,7 +85,7 @@ export function useUploadController() { dispatch(setResult(res)); navigate("/admin/upload/validate"); } finally { - setLoading(false); + // setLoading(false); } }; @@ -100,7 +101,7 @@ export function useUploadController() { dispatch(setValidatedData(res)); navigate("/admin/upload/success"); } finally { - setLoading(false); + // setLoading(false); } }; diff --git a/src/pages/admin/upload/service_admin_upload.jsx b/src/pages/admin/upload/service_admin_upload.jsx index 73f8392..b22964a 100644 --- a/src/pages/admin/upload/service_admin_upload.jsx +++ b/src/pages/admin/upload/service_admin_upload.jsx @@ -35,7 +35,7 @@ export async function uploadFile(file, page = null, sheet = null) { } try { - const response = await api.post("/upload", formData, { + const response = await api.post("/upload/file", formData, { headers: { "Content-Type": "multipart/form-data" }, }); return response.data; @@ -47,7 +47,7 @@ export async function uploadFile(file, page = null, sheet = null) { export async function uploadPdf(data) { try { - const response = await api.post("/process-pdf", data, { + const response = await api.post("/upload/process-pdf", data, { headers: { "Content-Type": "application/json" }, }); return response.data; @@ -60,7 +60,7 @@ export async function uploadPdf(data) { export async function saveToDatabase(data) { console.log("send:", data); try { - const response = await api.post("/upload_to_postgis", data, { + const response = await api.post("/upload/to-postgis", data, { headers: { "Content-Type": "application/json" }, }); return response.data; diff --git a/src/pages/admin/upload/views_admin_upload.jsx b/src/pages/admin/upload/views_admin_upload.jsx index 0be7452..8d505cf 100644 --- a/src/pages/admin/upload/views_admin_upload.jsx +++ b/src/pages/admin/upload/views_admin_upload.jsx @@ -1,8 +1,11 @@ import { useUploadController } from "./controller_admin_upload"; import { useDispatch } from "react-redux"; +import LoadingOverlay from "../../../components/LoadingOverlay"; import FileDropzone from "../../../components/FileDropzone"; import PdfPageSelector from "../../../components/PdfPageSelector"; import { Link } from "react-router-dom"; +import { useEffect } from "react"; +import { reset } from "../../../store/slices/uploadSlice"; export default function ViewsAdminUploadStep1() { const dispatch = useDispatch(); @@ -32,8 +35,16 @@ export default function ViewsAdminUploadStep1() { const ext = file ? file.name.split(".").pop().toLowerCase() : ""; + useEffect(() => { + dispatch(reset()) + }, []) + + return (
+ + +

Upload Data

diff --git a/src/pages/admin/upload/views_admin_validate_upload.jsx b/src/pages/admin/upload/views_admin_validate_upload.jsx index a84fe1a..a940c39 100644 --- a/src/pages/admin/upload/views_admin_validate_upload.jsx +++ b/src/pages/admin/upload/views_admin_validate_upload.jsx @@ -1,6 +1,7 @@ +import { useState, useEffect } from "react"; import { useUploadController } from "./controller_admin_upload"; import { useSelector } from "react-redux"; -import { useState, useEffect } from "react"; +import LoadingOverlay from "../../../components/LoadingOverlay"; import Notification from "../../../components/Notification"; import { Navigate } from "react-router-dom"; import FilePreview from "../../../components/upload/FilePreview"; @@ -66,9 +67,9 @@ export default function ViewsAdminUploadValidate() { return; } - setAlertMessage("Mengunggah ke database..."); - setAlertType("info"); - setShowAlert(true); + // setAlertMessage("Mengunggah ke database..."); + // setAlertType("info"); + // setShowAlert(true); handleConfirmUpload(); }; @@ -82,6 +83,8 @@ export default function ViewsAdminUploadValidate() { /> )} + +

✅ Validasi & Konfirmasi Data

diff --git a/src/store/slices/uploadSlice.js b/src/store/slices/uploadSlice.js index 03063a4..889649d 100644 --- a/src/store/slices/uploadSlice.js +++ b/src/store/slices/uploadSlice.js @@ -31,6 +31,8 @@ const uploadSlice = createSlice({ state.file = null; state.result = null; state.validatedData = null; + state.pdfPageCount = null; + state.selectedPages = null; }, }, });