Add motion animations to Button and CabinetMaintenance components; update form validation and styling

This commit is contained in:
Md Asif 2024-12-22 01:38:09 +05:30
parent 442b8e52dd
commit bfe22a61a5
8 changed files with 291 additions and 80 deletions

72
package-lock.json generated
View File

@ -13,6 +13,7 @@
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.6.1", "i18next-http-backend": "^2.6.1",
"lucide-react": "^0.446.0", "lucide-react": "^0.446.0",
"motion": "^11.15.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -2857,6 +2858,33 @@
"url": "https://github.com/sponsors/rawify" "url": "https://github.com/sponsors/rawify"
} }
}, },
"node_modules/framer-motion": {
"version": "11.15.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz",
"integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==",
"license": "MIT",
"dependencies": {
"motion-dom": "^11.14.3",
"motion-utils": "^11.14.3",
"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": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -3928,6 +3956,44 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/motion": {
"version": "11.15.0",
"resolved": "https://registry.npmjs.org/motion/-/motion-11.15.0.tgz",
"integrity": "sha512-iZ7dwADQJWGsqsSkBhNHdI2LyYWU+hA1Nhy357wCLZq1yHxGImgt3l7Yv0HT/WOskcYDq9nxdedyl4zUv7UFFw==",
"license": "MIT",
"dependencies": {
"framer-motion": "^11.15.0",
"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/motion-dom": {
"version": "11.14.3",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz",
"integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==",
"license": "MIT"
},
"node_modules/motion-utils": {
"version": "11.14.3",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz",
"integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==",
"license": "MIT"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -5327,6 +5393,12 @@
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"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": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@ -15,6 +15,7 @@
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.6.1", "i18next-http-backend": "^2.6.1",
"lucide-react": "^0.446.0", "lucide-react": "^0.446.0",
"motion": "^11.15.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",

View File

@ -1,19 +1,38 @@
import { Outlet } from "react-router-dom"; import { useOutlet } from "react-router";
import { useState } from "react";
import Header from "./components/Header"; import Header from "./components/Header";
import Footer from "./components/Footer"; import Footer from "./components/Footer";
import { ToastProvider } from "./components/Toast"; import { AnimatePresence } from "motion/react";
import { motion } from "motion/react";
import { useLocation } from "react-router";
const AnimatedOutlet = () => {
const o = useOutlet();
const [outlet] = useState(o);
return <div>{outlet}</div>;
};
function App() { function App() {
const location = useLocation();
return ( return (
<ToastProvider>
<div className="flex flex-col min-h-screen"> <div className="flex flex-col min-h-screen">
<Header /> <Header />
<main className="flex flex-grow transition-color-mode md:p-7 2xl:p-12 bg-surface dark:bg-surface-dark"> <main className="overflow-hidden flex flex-grow transition-color-mode md:p-7 2xl:p-12 bg-surface dark:bg-surface-dark">
<Outlet /> <AnimatePresence mode="popLayout">
<motion.div
className="w-full ovwerflow-hidden"
key={location.pathname}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 50 }}
>
<AnimatedOutlet />
</motion.div>
</AnimatePresence>
</main> </main>
<Footer /> <Footer />
</div> </div>
</ToastProvider>
); );
} }

View File

@ -1,13 +1,16 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { motion } from 'motion/react';
function Button({text, onClick}) { function Button({text, onClick}) {
return ( return (
<button <motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-12 py-2 text-lg text-white dark:text-primary-dark rounded-full bg-primary dark:bg-secondary-dark" className="px-12 py-2 text-lg text-white dark:text-primary-dark rounded-full bg-primary dark:bg-secondary-dark"
onClick={onClick} onClick={onClick}
> >
{text} {text}
</button> </motion.button>
) )
} }

View File

@ -1,10 +1,23 @@
import { PropTypes } from 'prop-types'; import { PropTypes } from "prop-types";
import clsx from 'clsx'; import clsx from "clsx";
function FormBox({ title, children, alt = false }) { function FormBox({ title, children, alt = false }) {
return ( return (
<form className={clsx(alt ? 'bg-secondary-variant dark:bg-secondary-variant-dark border-secondary-variant dark:border-secondary-variant-dark' : 'bg-surface-variant dark:bg-surface-variant-dark', 'transition-color-mode font-body border-secondary dark:border-secondary-dark border-2 p-4 rounded-3xl relative h-full')}> <form
<label 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')}> className={clsx(
alt
? "bg-secondary-variant dark:bg-secondary-variant-dark border-secondary-variant dark:border-secondary-variant-dark"
: "bg-surface-variant dark:bg-surface-variant-dark",
"transition-color-mode font-body border-secondary dark:border-secondary-dark border-2 p-4 rounded-3xl relative h-full"
)}
>
<label
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"
)}
>
{title} {title}
</label> </label>
{children} {children}
@ -18,4 +31,4 @@ FormBox.propTypes = {
alt: PropTypes.bool, alt: PropTypes.bool,
}; };
export default FormBox; export default FormBox;

View File

@ -1,33 +1,56 @@
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import clsx from "clsx";
import { useState } from "react";
function SubMenu({ items }) { function SubMenu({ items, isVisible, onLinkClick }) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="absolute left-0 z-50 invisible transition-all duration-200 -translate-y-6 shadow-sm opacity-0 top-full border-t-3 border-primary dark:border-primary-dark bg-secondary dark:bg-secondary-dark rounded-2xl shadow-surface-variant-dark dark:shadow-primary group-hover:visible group-hover:translate-y-0 group-hover:opacity-100"> <div
className={clsx(
"absolute left-0 z-50 shadow-sm top-full border-t-3 border-primary dark:border-primary-dark bg-secondary dark:bg-secondary-dark rounded-2xl shadow-surface-variant-dark dark:shadow-primary",
isVisible ? "block" : "hidden"
)}
>
{items.map((subItem, index) => ( {items.map((subItem, index) => (
<div key={index} className="px-6 py-2 cursor-pointer text-nowrap hover:bg-white dark:hover:bg-secondary-variant-dark first:rounded-t-2xl second-last:rounded-b-2xl first:pt-4 last:pb-4"> <div
key={index}
className="px-6 py-2 cursor-pointer text-nowrap hover:bg-white dark:hover:bg-secondary-variant-dark first:rounded-t-2xl second-last:rounded-b-2xl first:pt-4 last:pb-4"
onClick={onLinkClick}
>
<Link to={subItem.path}>{t(subItem.name)}</Link> <Link to={subItem.path}>{t(subItem.name)}</Link>
</div> </div>
))} ))}
<svg className="absolute top-0 left-6 mt-[-13px] fill-primary dark:fill-primary-dark" width="16" height="13" viewBox="0 0 16 13" xmlns="http://www.w3.org/2000/svg"> <svg
className="absolute top-0 left-6 mt-[-13px] fill-primary dark:fill-primary-dark"
width="16"
height="13"
viewBox="0 0 16 13"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M6.26795 1C7.03775 -0.333332 8.96225 -0.333334 9.73205 0.999999L14.9282 10C15.698 11.3333 14.7358 13 13.1962 13H2.80385C1.26425 13 0.301996 11.3333 1.0718 10L6.26795 1Z" /> <path d="M6.26795 1C7.03775 -0.333332 8.96225 -0.333334 9.73205 0.999999L14.9282 10C15.698 11.3333 14.7358 13 13.1962 13H2.80385C1.26425 13 0.301996 11.3333 1.0718 10L6.26795 1Z" />
</svg> </svg>
</div> </div>
); );
}; }
function MenuBar({ menuItems }) { function MenuBar({ menuItems }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [activeMenu, setActiveMenu] = useState(null);
return ( return (
<div className="flex justify-between pt-5 pb-2 text-base font-body"> <div className="flex justify-between pt-5 pb-2 text-base font-body">
{menuItems.map((item, index) => {menuItems.map((item, index) => (
<div key={index} className="relative pb-3 cursor-pointer group"> <div
{t(item.name)} key={index}
{item.submenu.length > 0 && <SubMenu items={item.submenu} />} className="relative pb-3 cursor-pointer"
onMouseEnter={() => setActiveMenu(index)}
onMouseLeave={() => setActiveMenu(null)}
>
<Link to={item.path}>{t(item.name)}</Link>
{item.submenu.length > 0 && <SubMenu items={item.submenu} isVisible={ activeMenu === index } onLinkClick={() => setActiveMenu(null)}/>}
</div> </div>
)} ))}
</div> </div>
); );
} }
@ -42,18 +65,20 @@ MenuBar.propTypes = {
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,
}) })
), ),
path: PropTypes.string path: PropTypes.string,
}) })
).isRequired ).isRequired,
}; };
SubMenu.propTypes = { SubMenu.propTypes = {
items: PropTypes.arrayOf( items: PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
path: PropTypes.string.isRequired path: PropTypes.string.isRequired,
}) })
).isRequired ).isRequired,
isVisible: PropTypes.bool.isRequired,
onLinkClick: PropTypes.func.isRequired,
}; };
export default MenuBar; export default MenuBar;

View File

@ -3,33 +3,40 @@ import { useTranslation } from "react-i18next";
import FormBox from "../components/FormBox"; import FormBox from "../components/FormBox";
import Button from "../components/Button"; import Button from "../components/Button";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { AnimatePresence, motion } from "motion/react";
import clsx from "clsx"; import clsx from "clsx";
function CabinetCreation() { function CabinetCreation() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const [cabinetId, setCabinetId] = useState({id: "", valid: true}); const [cabinetId, setCabinetId] = useState({ id: "", valid: true });
const [cabinetKeyId, setCabinetKeyId] = useState({id: "", valid: true}); const [cabinetKeyId, setCabinetKeyId] = useState({ id: "", valid: true });
const [noOfLockers, setNoOfLockers] = useState({number: 0, valid: true}); const [noOfLockers, setNoOfLockers] = useState({ number: 0, valid: true });
const handleNext = (e) => { const handleNext = (e) => {
e.preventDefault(); e.preventDefault();
const idRegex = /^[A-Z]{2}[0-9]{4}$/; const idRegex = /^[A-Z]{2}[0-9]{4}$/;
if (!idRegex.test(cabinetId.id)) { if (!idRegex.test(cabinetId.id)) {
setCabinetId({id: cabinetId.id, valid: false}); setCabinetId({ id: cabinetId.id, valid: false });
return; return;
} else if (!idRegex.test(cabinetKeyId.id)) { } else if (!idRegex.test(cabinetKeyId.id)) {
setCabinetKeyId({id: cabinetKeyId.id, valid: false}); setCabinetKeyId({ id: cabinetKeyId.id, valid: false });
return; return;
} else if (noOfLockers.number === 0) { } else if (noOfLockers.number === 0) {
setNoOfLockers({number: noOfLockers.number, valid: false}); setNoOfLockers({ number: noOfLockers.number, valid: false });
return; return;
} }
navigate("register-lockers", {state: {cabinetId: cabinetId.id, cabinetKeyId: cabinetKeyId.id, noOfLockers: noOfLockers.number}}); navigate("register-lockers", {
state: {
cabinetId: cabinetId.id,
cabinetKeyId: cabinetKeyId.id,
noOfLockers: noOfLockers.number,
},
});
}; };
return ( return (
<div className="w-full h-fit"> <motion.div className="w-full h-fit" initial={{ scale: 0.9 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
<FormBox title={t("cabinetCreation")}> <FormBox title={t("cabinetCreation")}>
<div className="p-2 pt-7 flex flex-col gap-4"> <div className="p-2 pt-7 flex flex-col gap-4">
<div className="flex"> <div className="flex">
@ -39,12 +46,32 @@ function CabinetCreation() {
<div className="w-full"> <div className="w-full">
<input <input
value={cabinetId.id} value={cabinetId.id}
className={clsx("w-1/5 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 border-grey text-grey focus:outline-grey", !cabinetId.valid && "border-error")} className={clsx(
onChange={(e) => setCabinetId({id: e.target.value.toUpperCase(), valid: true })} "w-1/5 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 border-grey text-grey focus:outline-grey",
!cabinetId.valid && "border-error"
)}
onChange={(e) =>
setCabinetId({
id: e.target.value.toUpperCase(),
valid: true,
})
}
type="text" type="text"
maxLength={6} maxLength={6}
/> />
{!cabinetId.valid && <div className="text-sm text-error ml-3 pt-1">Invalid Cabinet Id</div>} <AnimatePresence initial={false}>
{!cabinetId.valid && (
<motion.div
className="w-1/5 text-sm text-error ml-3 pt-1"
initial={{ opacity: 0,scale: 0 }}
animate={{ opacity: 1,scale: 1 }}
exit={{ opacity: 0,scale: 0 }}
key="cabinetIdError"
>
Invalid Cabinet Id
</motion.div>
)}
</AnimatePresence>
</div> </div>
</div> </div>
<div className="flex"> <div className="flex">
@ -52,14 +79,26 @@ function CabinetCreation() {
{t("cabinetKeyId")} {t("cabinetKeyId")}
</label> </label>
<div className="w-full"> <div className="w-full">
<input <input
value={cabinetKeyId.id} value={cabinetKeyId.id}
className={clsx("w-1/5 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 border-grey text-grey focus:outline-grey", !cabinetKeyId.valid && "border-error")} className={clsx(
onChange={(e) => setCabinetKeyId({id: e.target.value.toUpperCase(), valid: true })} "w-1/5 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 border-grey text-grey focus:outline-grey",
type="text" !cabinetKeyId.valid && "border-error"
maxLength={6} )}
/> onChange={(e) =>
{!cabinetKeyId.valid && <div className="text-sm text-error ml-3 pt-1">Invalid Key Id</div>} setCabinetKeyId({
id: e.target.value.toUpperCase(),
valid: true,
})
}
type="text"
maxLength={6}
/>
{!cabinetKeyId.valid && (
<div className="text-sm text-error ml-3 pt-1">
Invalid Key Id
</div>
)}
</div> </div>
</div> </div>
<div className="flex"> <div className="flex">
@ -67,19 +106,33 @@ function CabinetCreation() {
{t("noOfLockers")} {t("noOfLockers")}
</label> </label>
<div className="w-full"> <div className="w-full">
<input <input
value={noOfLockers.number} value={noOfLockers.number}
className={clsx("w-1/5 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 border-grey text-grey focus:outline-grey", !noOfLockers.valid && "border-error")} className={clsx(
onChange={(e) => setNoOfLockers({number: e.target.value, valid: true })} "w-1/5 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 border-grey text-grey focus:outline-grey",
type="number" !noOfLockers.valid && "border-error"
/> )}
{!noOfLockers.valid && <div className="text-sm text-error ml-3 pt-1">Invalid Number of Lockers</div>} onChange={(e) =>
setNoOfLockers({ number: e.target.value, valid: true })
}
type="number"
/>
{!noOfLockers.valid && (
<div className="text-sm text-error ml-3 pt-1">
Invalid Number of Lockers
</div>
)}
</div> </div>
</div> </div>
</div> </div>
<Button text={t("next")} onClick={(e) => {handleNext(e)}}/> <Button
text={t("next")}
onClick={(e) => {
handleNext(e);
}}
/>
</FormBox> </FormBox>
</div> </motion.div>
); );
} }

View File

@ -1,43 +1,68 @@
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useToast } from "../hooks/useToast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import FormBox from "../components/FormBox"; import FormBox from "../components/FormBox";
import Button from "../components/Button"; import Button from "../components/Button";
import { motion } from "motion/react";
import clsx from "clsx";
import { AnimatePresence } from "motion/react";
function CabinetMaintenace() { function CabinetMaintenace() {
const navigate = useNavigate(); const navigate = useNavigate();
const showToast = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
const [operation, setOperation] = useState(""); const [operation, setOperation] = useState({ value: "", valid: true });
const handleNext = () => { const handleNext = (e) => {
if (operation === "") { e.preventDefault();
showToast("Please select an operation", "error"); if (operation.value === "") {
return; setOperation({ value: operation.value, valid: false });
} }
navigate(operation); navigate(operation.value);
}; };
return ( return (
<div className="w-full h-fit"> <div>
<FormBox title={t('cabinetMaintenance')}> <FormBox title={t("cabinetMaintenance")}>
<div className="p-2"> <div className="p-2 pt-7">
<div className="my-5"> <div className="flex">
<label className="mr-4 text-lg text-black dark:text-white">{t('operation')}</label> <label className="mr-4 text-lg text-black dark:text-primary-dark w-[10%]">
<select {t("operation")}
className="w-1/5 h-10 px-2 rounded-full dark:bg-grey dark:text-primary-dark border-2 border-grey text-grey focus:border-none focus:outline-none" </label>
onChange={(e) => setOperation(e.target.value)} <div className="w-full">
defaultValue={operation} <select
> className={clsx(
<option value="" disabled> "w-1/5 h-10 px-2 rounded-full dark:bg-grey dark:text-primary-dark border-2 focus:outline-grey border-grey",
{t('select')} !operation.valid && "border-error"
</option> )}
<option value="create">{t('create')}</option> onChange={(e) =>
</select> setOperation({ value: e.target.value, valid: true })
}
defaultValue={operation.value}
value={operation.value}
>
<option value="" disabled>
{t("select")}
</option>
<option value="create">{t("create")}</option>
</select>
<AnimatePresence initial={false}>
{!operation.valid && (
<motion.div
className="w-1/5 text-sm text-error ml-3 pt-1"
initial={{ opacity: 0,scale: 0 }}
animate={{ opacity: 1,scale: 1 }}
exit={{ opacity: 0,scale: 0 }}
key="cabinetIdError"
>
Invalid Operation
</motion.div>
)}
</AnimatePresence>
</div>
</div> </div>
<Button text={t('next')} onClick={handleNext} />
</div> </div>
<Button text={t("next")} onClick={(e) => handleNext(e)} />
</FormBox> </FormBox>
</div> </div>
); );