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-http-backend": "^2.6.1",
"lucide-react": "^0.446.0",
"motion": "^11.15.0",
"prop-types": "^15.8.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@ -2857,6 +2858,33 @@
"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": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -3928,6 +3956,44 @@
"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": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -5327,6 +5393,12 @@
"dev": true,
"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": {
"version": "0.4.0",
"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-http-backend": "^2.6.1",
"lucide-react": "^0.446.0",
"motion": "^11.15.0",
"prop-types": "^15.8.1",
"react": "^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 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() {
const location = useLocation();
return (
<ToastProvider>
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex flex-grow transition-color-mode md:p-7 2xl:p-12 bg-surface dark:bg-surface-dark">
<Outlet />
<main className="overflow-hidden flex flex-grow transition-color-mode md:p-7 2xl:p-12 bg-surface dark:bg-surface-dark">
<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>
<Footer />
</div>
</ToastProvider>
);
}

View File

@ -1,13 +1,16 @@
import PropTypes from 'prop-types';
import { motion } from 'motion/react';
function Button({text, onClick}) {
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"
onClick={onClick}
>
{text}
</button>
</motion.button>
)
}

View File

@ -1,10 +1,23 @@
import { PropTypes } from 'prop-types';
import clsx from 'clsx';
import { PropTypes } from "prop-types";
import clsx from "clsx";
function FormBox({ title, children, alt = false }) {
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')}>
<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')}>
<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"
)}
>
<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}
</label>
{children}

View File

@ -1,33 +1,56 @@
import { Link } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import { useState } from "react";
function SubMenu({ items }) {
function SubMenu({ items, isVisible, onLinkClick }) {
const { t } = useTranslation();
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) => (
<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>
</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" />
</svg>
</div>
);
};
}
function MenuBar({ menuItems }) {
const { t } = useTranslation();
const [activeMenu, setActiveMenu] = useState(null);
return (
<div className="flex justify-between pt-5 pb-2 text-base font-body">
{menuItems.map((item, index) =>
<div key={index} className="relative pb-3 cursor-pointer group">
{t(item.name)}
{item.submenu.length > 0 && <SubMenu items={item.submenu} />}
{menuItems.map((item, index) => (
<div
key={index}
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>
);
}
@ -42,18 +65,20 @@ MenuBar.propTypes = {
path: PropTypes.string.isRequired,
})
),
path: PropTypes.string
path: PropTypes.string,
})
).isRequired
).isRequired,
};
SubMenu.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
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;

View File

@ -3,33 +3,40 @@ import { useTranslation } from "react-i18next";
import FormBox from "../components/FormBox";
import Button from "../components/Button";
import { useNavigate } from "react-router-dom";
import { AnimatePresence, motion } from "motion/react";
import clsx from "clsx";
function CabinetCreation() {
const { t } = useTranslation();
const navigate = useNavigate();
const [cabinetId, setCabinetId] = useState({id: "", valid: true});
const [cabinetKeyId, setCabinetKeyId] = useState({id: "", valid: true});
const [noOfLockers, setNoOfLockers] = useState({number: 0, valid: true});
const [cabinetId, setCabinetId] = useState({ id: "", valid: true });
const [cabinetKeyId, setCabinetKeyId] = useState({ id: "", valid: true });
const [noOfLockers, setNoOfLockers] = useState({ number: 0, valid: true });
const handleNext = (e) => {
e.preventDefault();
const idRegex = /^[A-Z]{2}[0-9]{4}$/;
if (!idRegex.test(cabinetId.id)) {
setCabinetId({id: cabinetId.id, valid: false});
setCabinetId({ id: cabinetId.id, valid: false });
return;
} else if (!idRegex.test(cabinetKeyId.id)) {
setCabinetKeyId({id: cabinetKeyId.id, valid: false});
setCabinetKeyId({ id: cabinetKeyId.id, valid: false });
return;
} else if (noOfLockers.number === 0) {
setNoOfLockers({number: noOfLockers.number, valid: false});
setNoOfLockers({ number: noOfLockers.number, valid: false });
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 (
<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")}>
<div className="p-2 pt-7 flex flex-col gap-4">
<div className="flex">
@ -39,12 +46,32 @@ function CabinetCreation() {
<div className="w-full">
<input
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")}
onChange={(e) => setCabinetId({id: e.target.value.toUpperCase(), valid: true })}
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"
)}
onChange={(e) =>
setCabinetId({
id: e.target.value.toUpperCase(),
valid: true,
})
}
type="text"
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 className="flex">
@ -52,14 +79,26 @@ function CabinetCreation() {
{t("cabinetKeyId")}
</label>
<div className="w-full">
<input
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")}
onChange={(e) => 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>}
<input
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"
)}
onChange={(e) =>
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 className="flex">
@ -67,19 +106,33 @@ function CabinetCreation() {
{t("noOfLockers")}
</label>
<div className="w-full">
<input
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")}
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>}
<input
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"
)}
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>
<Button text={t("next")} onClick={(e) => {handleNext(e)}}/>
<Button
text={t("next")}
onClick={(e) => {
handleNext(e);
}}
/>
</FormBox>
</div>
</motion.div>
);
}

View File

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