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%' },
+ }
},
},
},