From a6a67d69d5f8e9dbd432cb9975eed3dbfaf4d53a Mon Sep 17 00:00:00 2001 From: Md Asif Date: Sun, 22 Dec 2024 15:59:24 +0530 Subject: [PATCH] Add loading context and loading bar component; integrate axios for API calls and update routing for locker registration --- package-lock.json | 100 ++++++++++ package.json | 1 + src/App.jsx | 41 ++-- src/components/FormBox.jsx | 2 +- src/components/LoadingBar.jsx | 15 ++ src/contexts/Loading.jsx | 19 ++ src/{components => contexts}/Toast.jsx | 0 src/hooks/useLoading.js | 4 + src/hooks/useToast.js | 2 +- src/main.jsx | 51 ++--- src/pages/LockersRegistration.jsx | 262 +++++++++++++++++++++++++ src/services/api.js | 10 + src/services/locker.service.js | 14 ++ tailwind.config.js | 6 + 14 files changed, 487 insertions(+), 40 deletions(-) create mode 100644 src/components/LoadingBar.jsx create mode 100644 src/contexts/Loading.jsx rename src/{components => contexts}/Toast.jsx (100%) create mode 100644 src/hooks/useLoading.js create mode 100644 src/pages/LockersRegistration.jsx create mode 100644 src/services/api.js create mode 100644 src/services/locker.service.js diff --git a/package-lock.json b/package-lock.json index 6b9fe30..f9381cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "osaka", "version": "0.0.0", "dependencies": { + "axios": "^1.7.9", "clsx": "^2.1.1", "i18next": "^23.15.1", "i18next-browser-languagedetector": "^8.0.0", @@ -1664,6 +1665,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -1718,6 +1725,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1934,6 +1952,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -2117,6 +2147,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2817,6 +2856,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -2844,6 +2903,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3933,6 +4006,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4572,6 +4666,12 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 292e45e..ae9f08e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.7.9", "clsx": "^2.1.1", "i18next": "^23.15.1", "i18next-browser-languagedetector": "^8.0.0", diff --git a/src/App.jsx b/src/App.jsx index 0642603..17edcc4 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,10 +1,13 @@ -import { useOutlet } from "react-router"; +import { useLocation, useOutlet } from "react-router"; import { useState } from "react"; import Header from "./components/Header"; import Footer from "./components/Footer"; import { AnimatePresence } from "motion/react"; import { motion } from "motion/react"; -import { useLocation } from "react-router"; +import { ToastProvider } from "./contexts/Toast"; +import { useLoading } from "./hooks/useLoading"; +import LoadingBar from "./components/LoadingBar"; +import { LoadingProvider } from "./contexts/Loading"; const AnimatedOutlet = () => { const o = useOutlet(); @@ -13,26 +16,36 @@ const AnimatedOutlet = () => { return
{outlet}
; }; +function LoadingBarWrapper() { + const { isLoading } = useLoading(); + return isLoading ? : null; +} + function App() { const location = useLocation(); return ( +
-
- - - - - + +
+ + + + + + +
+
); } diff --git a/src/components/FormBox.jsx b/src/components/FormBox.jsx index ec5c987..a508467 100644 --- a/src/components/FormBox.jsx +++ b/src/components/FormBox.jsx @@ -15,7 +15,7 @@ function FormBox({ title, children, alt = false }) { className={clsx( alt && "bg-surface dark:bg-surface-dark border-3 border-secondary-variant dark:border-secondary-variant-dark", - "font-body absolute left-11 -top-4 bg-secondary dark:bg-secondary-dark text-primary dark:text-primary-dark font-medium py-1 px-4 rounded-full" + "font-body absolute left-11 -top-4 bg-secondary dark:bg-secondary-dark text-primary dark:text-primary-dark font-medium py-1 px-4 rounded-full z-20" )} > {title} diff --git a/src/components/LoadingBar.jsx b/src/components/LoadingBar.jsx new file mode 100644 index 0000000..f993143 --- /dev/null +++ b/src/components/LoadingBar.jsx @@ -0,0 +1,15 @@ +import { motion } from "framer-motion"; + +function LoadingBar() { + return ( +
+ +
+ ); +} + +export default LoadingBar; diff --git a/src/contexts/Loading.jsx b/src/contexts/Loading.jsx new file mode 100644 index 0000000..707e5ae --- /dev/null +++ b/src/contexts/Loading.jsx @@ -0,0 +1,19 @@ +import { createContext, useState } from 'react'; +import PropTypes from 'prop-types'; + +export const LoadingContext = createContext(); + +export function LoadingProvider({ children }) { + const [isLoading, setIsLoading] = useState(false); + + return ( + + {children} + + ); +} + +LoadingProvider.propTypes = { + children: PropTypes.node.isRequired, +}; + diff --git a/src/components/Toast.jsx b/src/contexts/Toast.jsx similarity index 100% rename from src/components/Toast.jsx rename to src/contexts/Toast.jsx diff --git a/src/hooks/useLoading.js b/src/hooks/useLoading.js new file mode 100644 index 0000000..5236d07 --- /dev/null +++ b/src/hooks/useLoading.js @@ -0,0 +1,4 @@ +import { useContext } from "react"; +import { LoadingContext } from "../contexts/Loading"; + +export const useLoading = () => useContext(LoadingContext); \ No newline at end of file diff --git a/src/hooks/useToast.js b/src/hooks/useToast.js index a658696..7d49462 100644 --- a/src/hooks/useToast.js +++ b/src/hooks/useToast.js @@ -1,4 +1,4 @@ import { useContext } from "react"; -import { ToastContext } from "../components/Toast"; +import { ToastContext } from "../contexts/Toast"; export const useToast = () => useContext(ToastContext); diff --git a/src/main.jsx b/src/main.jsx index 98e8547..28a628a 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,38 +1,41 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import App from './App.jsx' -import './index.css' -import './i18n' -import { createBrowserRouter, RouterProvider } from 'react-router-dom'; -import Home from './pages/Home.jsx' -import CabinetMaintenace from './pages/CabinetMaintenance.jsx' -import CabinetCreation from './pages/CabinetCreation.jsx' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App.jsx"; +import "./index.css"; +import "./i18n"; +import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import Home from "./pages/Home.jsx"; +import CabinetMaintenace from "./pages/CabinetMaintenance.jsx"; +import CabinetCreation from "./pages/CabinetCreation.jsx"; +import LockersRegistration from "./pages/LockersRegistration.jsx"; const router = createBrowserRouter([ { path: "/", - element: , + element: , children: [ { index: true, - element: + element: , }, { - path: 'operation/cabinet', - element: + path: "operation/cabinet", + element: , }, { - path: 'operation/cabinet/create', - element: - } - ] - } -] -); + path: "operation/cabinet/create", + element: , + }, + { + path: "operation/cabinet/create/register-lockers", + element: , + }, + ], + }, +]); - -createRoot(document.getElementById('root')).render( +createRoot(document.getElementById("root")).render( - , -) + +); diff --git a/src/pages/LockersRegistration.jsx b/src/pages/LockersRegistration.jsx new file mode 100644 index 0000000..2ced813 --- /dev/null +++ b/src/pages/LockersRegistration.jsx @@ -0,0 +1,262 @@ +import { useLocation } from "react-router-dom"; +import { useState } from "react"; +import clsx from "clsx"; +import FormBox from "../components/FormBox"; +import Button from "../components/Button"; +import { useToast } from "../hooks/useToast"; +import { lockersService } from "../services/locker.service"; +import { Copy } from "lucide-react"; +import { AnimatePresence } from "motion/react"; +import { motion } from "motion/react"; +import { useLoading } from "../hooks/useLoading"; + +function LockersRegistration() { + const location = useLocation(); + const showToast = useToast(); + const { setIsLoading } = useLoading(); + + const { noOfLockers, cabinetId } = location.state; + const [submitting, setSubmitting] = useState(false); + const [notification, setNotification] = useState({ + visible: false, + message: "", + type: "", + }); + + const initLockers = Array(parseInt(noOfLockers)) + .fill() + .map(() => ({ + id: "", + size: "", + keyId: "", + idValid: true, + sizeValid: true, + keyIdValid: true, + })); + const [lockerValues, setLockerValues] = useState(initLockers); + + const handleSubmit = async (e) => { + console.log("submitting"); + e.preventDefault(); + const idRegex = /^[A-Z]{2}[0-9]{4}$/; + let valid = true; + + // Helper function to find duplicates + const findDuplicates = (arr, key) => { + const values = arr.map((item) => item[key]); + return values.filter((item, index) => values.indexOf(item) !== index); + }; + + // Find duplicates + const duplicateLockerIds = findDuplicates(lockerValues, "id"); + const duplicateKeyIds = findDuplicates(lockerValues, "keyId"); + + const newValues = lockerValues.map((locker) => { + const newLocker = { ...locker }; + + // Check ID + if ( + locker.id === "" || + !idRegex.test(locker.id) || + duplicateLockerIds.includes(locker.id) + ) { + newLocker.idValid = false; + valid = false; + } else { + newLocker.idValid = true; + } + + // Check size + if (locker.size === "") { + newLocker.sizeValid = false; + valid = false; + } else { + newLocker.sizeValid = true; + } + + // Check keyId + if ( + locker.keyId === "" || + !idRegex.test(locker.keyId) || + duplicateKeyIds.includes(locker.keyId) + ) { + newLocker.keyIdValid = false; + valid = false; + } else { + newLocker.keyIdValid = true; + } + + return newLocker; + }); + + setLockerValues(newValues); + + if (!valid) { + const errorMessage = + duplicateLockerIds.length || duplicateKeyIds.length + ? "Please ensure all IDs are unique." + : "Inavlid Ids"; + showToast(errorMessage, "error"); + return; + } + + try { + setSubmitting(true); + setIsLoading(true); + const response = await lockersService.registerLockers( + cabinetId, + lockerValues + ); + setNotification({ + visible: true, + message: `Cabinet creation successful. Cabinet ID: ${response.data.cabinetId}`, + type: "success", + }); + } catch (error) { + console.error(error); + setNotification({ + visible: true, + message: `Error registering lockers. ${error.message}`, + type: "error", + }); + return; + } finally { + setIsLoading(false); + setSubmitting(false); + } + }; + + const lockerDetails = lockerValues.map((locker, index) => { + return ( +
+ + + { + const newValues = [...lockerValues]; + newValues[index] = { + ...newValues[index], + id: e.target.value.toUpperCase(), + idValid: true, + }; + setLockerValues(newValues); + }} + placeholder="Locker ID" + type="text" + maxLength={6} + /> + + + + { + const newValues = [...lockerValues]; + newValues[index] = { + ...newValues[index], + keyId: e.target.value.toUpperCase(), + keyIdValid: true, + }; + setLockerValues(newValues); + }} + placeholder="Key ID" + type="text" + maxLength={6} + /> +
+ ); + }); + + return ( +
+ + {notification.visible && ( + + {notification.message.split(":").map((msg, index) => { + return index === 1 ? ( + + {msg} + + ) : ( + {msg} + ); + })} + + + )} + +
+ {notification.type === "success" && ( +
+ )} + +
+ {cabinetId} +
+
{lockerDetails}
+
+
+ ); +} + +export default LockersRegistration; diff --git a/src/services/api.js b/src/services/api.js new file mode 100644 index 0000000..4aa5678 --- /dev/null +++ b/src/services/api.js @@ -0,0 +1,10 @@ +import axios from 'axios'; + +const api = axios.create({ + baseURL: "http://localhost:8081/api/v1", + headers: { + 'Content-Type': 'application/json' + } +}); + +export default api; \ No newline at end of file diff --git a/src/services/locker.service.js b/src/services/locker.service.js new file mode 100644 index 0000000..8e8c0ac --- /dev/null +++ b/src/services/locker.service.js @@ -0,0 +1,14 @@ +import api from './api'; + +export const lockersService = { + registerLockers: async (cabinetId, lockers) => { + return api.post('/cabinet', { + cabinetId, + lockers: lockers.map(({ id, size, keyId }) => ({ + id, + size, + keyId + })) + }); + }, +}; \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index 7591d98..da19a89 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -70,6 +70,12 @@ export default { '0%': { opacity: '1' }, '100%': { opacity: '0' }, }, + loading: { + '0%': { left: '-10%' }, + '30%': { width: '30%' }, + '80%': { width: '10%' }, + '100%': { left: '100%' }, + } }, }, },