add loader overlay

This commit is contained in:
dmsanhrProject 2025-11-06 14:59:34 +07:00
parent d85beefd8b
commit 1462a1ff54
9 changed files with 122 additions and 12 deletions

49
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -0,0 +1,43 @@
import { motion } from "framer-motion";
export default function LoadingOverlay({ show = false, text = "Memproses data..." }) {
if (!show) return null;
return (
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-white/30">
{/* Animated Dots */}
<motion.div
className="flex gap-3 mb-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6 }}
>
{[0, 1, 2].map((i) => (
<motion.span
key={i}
className="w-4 h-4 bg-blue-600 rounded-full"
animate={{
y: [0, -8, 0],
}}
transition={{
repeat: Infinity,
duration: 0.8,
delay: i * 0.2,
ease: "easeInOut",
}}
/>
))}
</motion.div>
{/* Text with subtle fade animation */}
<motion.p
className="text-gray-700 text-sm font-medium"
initial={{ opacity: 0 }}
animate={{ opacity: [0.3, 1, 0.3] }}
transition={{ duration: 1.5, repeat: Infinity }}
>
{text}
</motion.p>
</div>
);
}

View File

@ -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]);

View File

@ -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);
}
};

View File

@ -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;

View File

@ -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 (
<div className="max-w-4xl mx-auto py-10">
<LoadingOverlay show={loading} text="Processing..." />
<div className="mb-6 flex justify-between items-center">
<h1 className="text-2xl font-bold text-gray-800">Upload Data</h1>
<p className="text-lg text-gray-600">

View File

@ -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() {
/>
)}
<LoadingOverlay show={loading} text="Upload to database..." />
<h1 className="text-2xl font-bold mb-4"> Validasi & Konfirmasi Data</h1>
<div className="w-full mx-auto mt-6">

View File

@ -31,6 +31,8 @@ const uploadSlice = createSlice({
state.file = null;
state.result = null;
state.validatedData = null;
state.pdfPageCount = null;
state.selectedPages = null;
},
},
});