reactored code to use formfields and formselects

This commit is contained in:
Md Asif 2024-12-24 19:10:20 +05:30
parent 962102d44c
commit 9f4059e2c6
8 changed files with 175 additions and 204 deletions

View File

@ -0,0 +1,21 @@
import PropTypes from "prop-types";
import { motion } from "motion/react";
function FieldError({ text }) {
return (
<motion.div
className="text-sm text-error ml-3 pt-1"
initial={{ y: 15, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 15, opacity: 0 }}
>
{text}
</motion.div>
);
}
FieldError.propTypes = {
text: PropTypes.string.isRequired,
};
export default FieldError;

View File

@ -2,13 +2,16 @@ import PropTypes from "prop-types";
import { motion } from "motion/react"; import { motion } from "motion/react";
import clsx from "clsx"; import clsx from "clsx";
function FormField({ label, children, icon }) { function FormField({ label, children, icon, variant }) {
return ( return (
<div className="flex"> <div className="flex items-center">
<label className="mr-4 text-lg text-black dark:text-primary-dark md:w-[30%] xl:w-[20%] 2xl:w-[17%]"> <label className={clsx(
"mr-20 text-lg text-black dark:text-primary-dark whitespace-nowrap",
variant === 'long' && "sm:w-[5%]"
)}>
{label} {label}
</label> </label>
<div className="flex w-full gap-4 items-center"> <div className={clsx("flex w-full gap-4 items-center", variant === 'long' && 'gap-10')}>
{children} {children}
{icon && ( {icon && (
<motion.div <motion.div
@ -29,7 +32,8 @@ function FormField({ label, children, icon }) {
FormField.propTypes = { FormField.propTypes = {
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
icon: PropTypes.object.isRequired, icon: PropTypes.object,
variant: PropTypes.string
}; };
export default FormField; export default FormField;

View File

@ -0,0 +1,11 @@
import PropTypes from "prop-types";
function FormHeader({ text }) {
return <h1 className="text-2xl font-medium text-primary py-2">{text}</h1>;
}
FormHeader.propTypes = {
text: PropTypes.string.isRequired
}
export default FormHeader;

View File

@ -1,8 +1,11 @@
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { motion, AnimatePresence } from "motion/react"; import { motion, AnimatePresence } from "motion/react";
import clsx from "clsx";
function FormInput({ function FormInput({
value, value,
onChange, onChange,
placeholder,
valid = true, valid = true,
maxLength = 17, maxLength = 17,
readOnly = false, readOnly = false,
@ -14,10 +17,14 @@ function FormInput({
<input <input
readOnly={readOnly} readOnly={readOnly}
value={value} value={value}
className={`w-72 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 text-grey focus:outline-grey ${className}`} className={clsx(
`w-72 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 text-grey focus:outline-grey ${className}`,
!valid && "border-error"
)}
onChange={onChange} onChange={onChange}
type={type} type={type}
maxLength={maxLength} maxLength={maxLength}
placeholder={placeholder}
/> />
<AnimatePresence> <AnimatePresence>
{!valid && ( {!valid && (
@ -44,6 +51,7 @@ FormInput.propTypes = {
type: PropTypes.string, type: PropTypes.string,
maxLength: PropTypes.number, maxLength: PropTypes.number,
valid: PropTypes.bool, valid: PropTypes.bool,
placeholder: PropTypes.string,
}; };
export default FormInput; export default FormInput;

View File

@ -1,40 +1,32 @@
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { motion, AnimatePresence } from "motion/react"; import { AnimatePresence } from "motion/react";
import clsx from "clsx";
import FieldError from "./FieldError";
function FormSelect({ value, onChange, options, className, valid = true }) { function FormSelect({ value, onChange, options, className, valid = true }) {
return ( return (
<div> <div>
<select <select
value={value} value={value}
className={ className={clsx(
"w-72 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 text-grey focus:outline-grey " + `w-72 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 text-grey focus:outline-grey ${className}`,
className !valid && "border-error"
}
onChange={onChange}
>
<option disabled value="">
Select
</option>
{options.map(({ value, label }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
<AnimatePresence>
{!valid && (
<motion.div
className="text-sm text-error ml-3 pt-1"
initial={{ y: 15, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 15, opacity: 0 }}
key="cabinetIdError"
>
Invalid Value
</motion.div>
)} )}
onChange={onChange}
>
<option disabled value="">
Select
</option>
{options.map(({ value, label }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
<AnimatePresence>
{!valid && <FieldError text={"Invalid value"} />}
</AnimatePresence> </AnimatePresence>
</div> </div>
); );
} }

View File

@ -0,0 +1,35 @@
import { motion } from "motion/react";
import ProductListTable from "./ProductListTable";
import PropTypes from "prop-types";
function ProductModal({ productInfo, handleProductSelect }) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
key="productList"
className="fixed z-50 inset-0 flex items-center justify-center bg-black/50"
>
<motion.div
initial={{ y: 15, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 15, opacity: 0 }}
className="flex flex-col items-center bg-white p-4 py-8 rounded-3xl w-[60%] max-h-[80%] overflow-auto font-body"
>
<h2 className="text-xl mb-4">Select Product</h2>
<ProductListTable
productInfo={productInfo}
onSelectProduct={handleProductSelect}
/>
</motion.div>
</motion.div>
);
}
ProductModal.propTypes = {
productInfo: PropTypes.object.isRequired,
handleProductSelect: PropTypes.func.isRequired,
};
export default ProductModal;

View File

@ -1,8 +1,6 @@
import { AnimatePresence } from "motion/react"; import { AnimatePresence } from "motion/react";
import clsx from "clsx"; import { PackageSearch, UserSearch } from "lucide-react";
import { PackageSearch, Copy, UserSearch } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { motion } from "motion/react";
import FormBox from "../components/FormBox"; import FormBox from "../components/FormBox";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import FormField from "../components/FormField"; import FormField from "../components/FormField";
@ -10,17 +8,14 @@ import FormInput from "../components/FormInput";
import FormSelect from "../components/FormSelect"; import FormSelect from "../components/FormSelect";
import Button from "../components/Button"; import Button from "../components/Button";
import productInfo from "../util/productList"; import productInfo from "../util/productList";
import ProductListTable from "../components/ProductListTable"; import Notification from "../components/Notification";
import ProductModal from "../components/ProductModal";
import FormHeader from "../components/FormHeader";
function AccountCreation() { function AccountCreation() {
const { t } = useTranslation(); const { t } = useTranslation();
const [notification] = useState({ const [notification] = useState({ message: "", type: "" });
visible: false,
message: "",
type: "",
});
const [showProductModal, setShowProductModal] = useState(false); const [showProductModal, setShowProductModal] = useState(false);
const [submitting] = useState(false);
const [accountDetails, setAccountDetails] = useState({ const [accountDetails, setAccountDetails] = useState({
productCode: "", productCode: "",
interestCategory: "", interestCategory: "",
@ -48,6 +43,8 @@ function AccountCreation() {
const newAccountDetails = { ...accountDetails }; const newAccountDetails = { ...accountDetails };
newAccountDetails.productCode = product.productCode; newAccountDetails.productCode = product.productCode;
newAccountDetails.interestCategory = product.interestCategory; newAccountDetails.interestCategory = product.interestCategory;
newAccountDetails.productCodeValid = true;
newAccountDetails.interestCategoryValid = true;
setAccountDetails(newAccountDetails); setAccountDetails(newAccountDetails);
setShowProductModal(false); setShowProductModal(false);
}; };
@ -181,36 +178,8 @@ function AccountCreation() {
} }
console.log("Form is valid", accountDetails); console.log("Form is valid", accountDetails);
}; };
const renderProductModal = () => {
return (
<AnimatePresence mode="popLayout">
{showProductModal && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
key="productList"
className="fixed z-50 inset-0 flex items-center justify-center bg-black/50"
>
<motion.div
initial={{ y: 15, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 15, opacity: 0 }}
className="flex flex-col items-center bg-white p-4 py-8 rounded-3xl w-[60%] max-h-[80%] overflow-auto font-body"
>
<h2 className="text-xl mb-4">Select Product</h2>
<ProductListTable
productInfo={productInfo}
onSelectProduct={handleProductSelect}
/>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
};
const renerField = (field) => { const renderField = (field) => {
const commonProps = { const commonProps = {
value: accountDetails[field.name], value: accountDetails[field.name],
valid: accountDetails[`${field.name}Valid`], valid: accountDetails[`${field.name}Valid`],
@ -221,7 +190,6 @@ function AccountCreation() {
setAccountDetails(newAccountDetails); setAccountDetails(newAccountDetails);
}, },
maxLength: field.maxLength, maxLength: field.maxLength,
className: clsx(!accountDetails[`${field.name}Valid`] && "border-error"),
}; };
return ( return (
@ -242,59 +210,32 @@ function AccountCreation() {
return ( return (
<div> <div>
<AnimatePresence> <AnimatePresence>
{notification.visible && ( {notification.message !== "" && <Notification {...notification} />}
<motion.div </AnimatePresence>
initial={{ y: 15, opacity: 0 }}
animate={{ y: 0, opacity: 1 }} <AnimatePresence>
exit={{ y: 15, opacity: 0 }} {showProductModal && (
className={clsx( <ProductModal
"p-4 mb-8 font-body text-center text-xl rounded-2xl flex items-center justify-center gap-2", productInfo={productInfo}
notification.type === "error" handleProductSelect={handleProductSelect}
? "bg-error-surface text-error" />
: "bg-success-surface text-success"
)}
>
{notification.message.split(":").map((msg, index) => {
return index === 1 ? (
<span key={index} className="border-b border-dashed">
{msg}
</span>
) : (
<span key={index}>{msg}</span>
);
})}
<Copy
cursor={"pointer"}
size={15}
onClick={navigator.clipboard.writeText(
notification.message.split(":")[1].trim()
)}
/>
</motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
{renderProductModal()}
<FormBox title="Account Creation" disabled={submitting}> <FormBox title="Account Creation" >
<div className="p-2 pt-7 "> <div className="p-2 pt-7 ">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<h1 className="text-2xl font-medium text-primary py-2"> <FormHeader text={"Account Details"}/>
Account Details {accountDetailsFields.map(renderField)}
</h1>
{accountDetailsFields.map(renerField)}
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<h1 className="text-2xl font-medium text-primary py-2 pt-6"> <FormHeader text={"Additional Details"}/>
Additional Details {additionalDetailsFields.map(renderField)}
</h1>
{additionalDetailsFields.map(renerField)}
</div> </div>
</div> </div>
<Button <Button text={t("submit")} onClick={handleSubmit} />
text={t("submit")}
onClick={handleSubmit}
disabled={submitting}
/>
</FormBox> </FormBox>
</div> </div>
); );
} }

View File

@ -1,25 +1,23 @@
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { useState } from "react"; import { useState } from "react";
import clsx from "clsx"; import { useToast } from "../hooks/useToast";
import { useLoading } from "../hooks/useLoading";
import FormBox from "../components/FormBox"; import FormBox from "../components/FormBox";
import Button from "../components/Button"; import Button from "../components/Button";
import { useToast } from "../hooks/useToast";
import { lockerService } from "../services/locker.service"; import { lockerService } from "../services/locker.service";
import { AnimatePresence } from "motion/react"; import { AnimatePresence } from "motion/react";
import { useLoading } from "../hooks/useLoading";
import Notification from "../components/Notification"; import Notification from "../components/Notification";
import FormField from "../components/FormField";
import FormInput from "../components/FormInput";
import FormSelect from "../components/FormSelect";
function LockersRegistration() { function LockersRegistration() {
const location = useLocation(); const location = useLocation();
const showToast = useToast();
const { setIsLoading } = useLoading(); const { setIsLoading } = useLoading();
const noOfLockers = location.state?.noOfLockers; const [notification, setNotification] = useState({ message: "", type: "" });
const noOfLockers = location.state?.noOfLockers;
const cabinetId = location.state?.cabinetId; const cabinetId = location.state?.cabinetId;
const [submitting, setSubmitting] = useState(false);
const [notification, setNotification] = useState({
message: "",
type: "",
});
const initLockers = Array(noOfLockers ? parseInt(noOfLockers) : 0) const initLockers = Array(noOfLockers ? parseInt(noOfLockers) : 0)
.fill() .fill()
@ -31,12 +29,14 @@ function LockersRegistration() {
sizeValid: true, sizeValid: true,
keyIdValid: true, keyIdValid: true,
})); }));
const [lockerValues, setLockerValues] = useState(initLockers); const [lockerValues, setLockerValues] = useState(initLockers);
if(!location.state) {
return <></> if (!location.state) {
return <></>;
} }
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
console.log("submitting");
e.preventDefault(); e.preventDefault();
const idRegex = /^[A-Z]{2}[0-9]{4}$/; const idRegex = /^[A-Z]{2}[0-9]{4}$/;
let valid = true; let valid = true;
@ -92,16 +92,10 @@ function LockersRegistration() {
setLockerValues(newValues); setLockerValues(newValues);
if (!valid) { if (!valid) {
const errorMessage =
duplicateLockerIds.length || duplicateKeyIds.length
? "Please ensure all IDs are unique."
: "Inavlid Ids";
showToast(errorMessage, "error");
return; return;
} }
try { try {
setSubmitting(true);
setIsLoading(true); setIsLoading(true);
const response = await lockerService.registerLockers( const response = await lockerService.registerLockers(
cabinetId, cabinetId,
@ -120,84 +114,50 @@ function LockersRegistration() {
return; return;
} finally { } finally {
setIsLoading(false); setIsLoading(false);
setSubmitting(false);
} }
}; };
const lockerDetails = lockerValues.map((locker, index) => { const lockerDetails = lockerValues.map((locker, index) => {
return ( return (
<div key={index} className="flex gap-12 items-center"> <FormField key={index} label={`Locker ${index + 1}`} variant="long">
<label className="text-lg text-bold dark:text-primary-dark">{`Locker ${ <FormInput
index + 1
}`}</label>
<input
value={locker.id} value={locker.id}
disabled={submitting} onChange={ e => {
className={clsx(
"w-64 h-9 px-2 rounded-full dark:bg-white dark:text-grey border-2 text-grey focus:outline-grey",
!locker.idValid && "border-error text-error"
)}
onChange={(e) => {
const newValues = [...lockerValues]; const newValues = [...lockerValues];
newValues[index] = { newValues[index].id = e.target.value;
...newValues[index], newValues[index].idValid = true;
id: e.target.value.toUpperCase(),
idValid: true,
};
setLockerValues(newValues); setLockerValues(newValues);
}} }}
placeholder="Locker ID" valid={locker.idValid}
type="text" placeholder={"Locker Id"}
maxLength={6}
/> />
<FormSelect
<select
value={locker.size} value={locker.size}
disabled={submitting} onChange={e => {
className={clsx(
"w-64 h-9 px-2 rounded-full dark:bg-white dark:text-grey border-2 text-grey focus:outline-grey",
!locker.sizeValid && "border-error"
)}
onChange={(e) => {
const newValues = [...lockerValues]; const newValues = [...lockerValues];
newValues[index] = { newValues[index].size = e.target.value;
...newValues[index], newValues[index].sizeValid = true;
size: e.target.value,
sizeValid: true,
};
setLockerValues(newValues); setLockerValues(newValues);
}} }}
> valid={locker.sizeValid}
<option value="" disabled> options={[
Size {label: 'Small', value: '1'},
</option> {label: 'Medium', value: '2'},
<option value="small">Small</option> {label: 'Large', value: '3'}
<option value="medium">Medium</option> ]}
<option value="large">Large</option>
</select>
<input
value={locker.keyId}
disabled={submitting}
className={clsx(
"w-64 h-9 px-2 rounded-full dark:bg-white dark:text-grey border-2 text-grey focus:outline-grey",
!locker.keyIdValid && "border-error"
)}
onChange={(e) => {
const newValues = [...lockerValues];
newValues[index] = {
...newValues[index],
keyId: e.target.value.toUpperCase(),
keyIdValid: true,
};
setLockerValues(newValues);
}}
placeholder="Key ID"
type="text"
maxLength={6}
/> />
</div> <FormInput
value={locker.keyId}
onChange={ e => {
const newValues = [...lockerValues];
newValues[index].keyId = e.target.value;
newValues[index].keyIdValid = true;
setLockerValues(newValues);
}}
valid={locker.keyIdValid}
placeholder={"Locker Key Id"}
/>
</FormField>
); );
}); });
@ -210,13 +170,12 @@ function LockersRegistration() {
{notification.type === "success" && ( {notification.type === "success" && (
<div className="absolute inset-0 bg-[#fff]/50 z-10 rounded-3xl" /> <div className="absolute inset-0 bg-[#fff]/50 z-10 rounded-3xl" />
)} )}
<FormBox title="Locker Registration" disabled={submitting}> <FormBox title="Locker Registration">
<div className="px-4 pt-7 text-2xl font-bold text-primary dark:text-primary-dark"> <div className="px-4 pt-7 text-2xl font-bold text-primary dark:text-primary-dark">
{cabinetId} {cabinetId}
</div> </div>
<div className="flex flex-col gap-4 p-4">{lockerDetails}</div> <div className="flex flex-col gap-4 p-4">{lockerDetails}</div>
<Button <Button
disabled={submitting}
text="Register" text="Register"
onClick={(e) => { onClick={(e) => {
handleSubmit(e); handleSubmit(e);