reactored code to use formfields and formselects
This commit is contained in:
parent
962102d44c
commit
9f4059e2c6
21
src/components/FieldError.jsx
Normal file
21
src/components/FieldError.jsx
Normal 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;
|
@ -2,13 +2,16 @@ import PropTypes from "prop-types";
|
||||
import { motion } from "motion/react";
|
||||
import clsx from "clsx";
|
||||
|
||||
function FormField({ label, children, icon }) {
|
||||
function FormField({ label, children, icon, variant }) {
|
||||
return (
|
||||
<div className="flex">
|
||||
<label className="mr-4 text-lg text-black dark:text-primary-dark md:w-[30%] xl:w-[20%] 2xl:w-[17%]">
|
||||
<div className="flex items-center">
|
||||
<label className={clsx(
|
||||
"mr-20 text-lg text-black dark:text-primary-dark whitespace-nowrap",
|
||||
variant === 'long' && "sm:w-[5%]"
|
||||
)}>
|
||||
{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}
|
||||
{icon && (
|
||||
<motion.div
|
||||
@ -29,7 +32,8 @@ function FormField({ label, children, icon }) {
|
||||
FormField.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
icon: PropTypes.object.isRequired,
|
||||
icon: PropTypes.object,
|
||||
variant: PropTypes.string
|
||||
};
|
||||
|
||||
export default FormField;
|
||||
|
11
src/components/FormHeader.jsx
Normal file
11
src/components/FormHeader.jsx
Normal 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;
|
@ -1,8 +1,11 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import clsx from "clsx";
|
||||
|
||||
function FormInput({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
valid = true,
|
||||
maxLength = 17,
|
||||
readOnly = false,
|
||||
@ -14,10 +17,14 @@ function FormInput({
|
||||
<input
|
||||
readOnly={readOnly}
|
||||
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}
|
||||
type={type}
|
||||
maxLength={maxLength}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{!valid && (
|
||||
@ -44,6 +51,7 @@ FormInput.propTypes = {
|
||||
type: PropTypes.string,
|
||||
maxLength: PropTypes.number,
|
||||
valid: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
};
|
||||
|
||||
export default FormInput;
|
||||
|
@ -1,40 +1,32 @@
|
||||
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 }) {
|
||||
return (
|
||||
<div>
|
||||
<select
|
||||
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
|
||||
}
|
||||
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>
|
||||
<select
|
||||
value={value}
|
||||
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}
|
||||
>
|
||||
<option disabled value="">
|
||||
Select
|
||||
</option>
|
||||
{options.map(({ value, label }) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<AnimatePresence>
|
||||
{!valid && <FieldError text={"Invalid value"} />}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
35
src/components/ProductModal.jsx
Normal file
35
src/components/ProductModal.jsx
Normal 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;
|
@ -1,8 +1,6 @@
|
||||
import { AnimatePresence } from "motion/react";
|
||||
import clsx from "clsx";
|
||||
import { PackageSearch, Copy, UserSearch } from "lucide-react";
|
||||
import { PackageSearch, UserSearch } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { motion } from "motion/react";
|
||||
import FormBox from "../components/FormBox";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FormField from "../components/FormField";
|
||||
@ -10,17 +8,14 @@ import FormInput from "../components/FormInput";
|
||||
import FormSelect from "../components/FormSelect";
|
||||
import Button from "../components/Button";
|
||||
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() {
|
||||
const { t } = useTranslation();
|
||||
const [notification] = useState({
|
||||
visible: false,
|
||||
message: "",
|
||||
type: "",
|
||||
});
|
||||
const [notification] = useState({ message: "", type: "" });
|
||||
const [showProductModal, setShowProductModal] = useState(false);
|
||||
const [submitting] = useState(false);
|
||||
const [accountDetails, setAccountDetails] = useState({
|
||||
productCode: "",
|
||||
interestCategory: "",
|
||||
@ -48,6 +43,8 @@ function AccountCreation() {
|
||||
const newAccountDetails = { ...accountDetails };
|
||||
newAccountDetails.productCode = product.productCode;
|
||||
newAccountDetails.interestCategory = product.interestCategory;
|
||||
newAccountDetails.productCodeValid = true;
|
||||
newAccountDetails.interestCategoryValid = true;
|
||||
setAccountDetails(newAccountDetails);
|
||||
setShowProductModal(false);
|
||||
};
|
||||
@ -181,36 +178,8 @@ function AccountCreation() {
|
||||
}
|
||||
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 = {
|
||||
value: accountDetails[field.name],
|
||||
valid: accountDetails[`${field.name}Valid`],
|
||||
@ -221,7 +190,6 @@ function AccountCreation() {
|
||||
setAccountDetails(newAccountDetails);
|
||||
},
|
||||
maxLength: field.maxLength,
|
||||
className: clsx(!accountDetails[`${field.name}Valid`] && "border-error"),
|
||||
};
|
||||
|
||||
return (
|
||||
@ -242,59 +210,32 @@ function AccountCreation() {
|
||||
return (
|
||||
<div>
|
||||
<AnimatePresence>
|
||||
{notification.visible && (
|
||||
<motion.div
|
||||
initial={{ y: 15, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 15, opacity: 0 }}
|
||||
className={clsx(
|
||||
"p-4 mb-8 font-body text-center text-xl rounded-2xl flex items-center justify-center gap-2",
|
||||
notification.type === "error"
|
||||
? "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>
|
||||
{notification.message !== "" && <Notification {...notification} />}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{showProductModal && (
|
||||
<ProductModal
|
||||
productInfo={productInfo}
|
||||
handleProductSelect={handleProductSelect}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{renderProductModal()}
|
||||
<FormBox title="Account Creation" disabled={submitting}>
|
||||
|
||||
<FormBox title="Account Creation" >
|
||||
<div className="p-2 pt-7 ">
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1 className="text-2xl font-medium text-primary py-2">
|
||||
Account Details
|
||||
</h1>
|
||||
{accountDetailsFields.map(renerField)}
|
||||
<FormHeader text={"Account Details"}/>
|
||||
{accountDetailsFields.map(renderField)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1 className="text-2xl font-medium text-primary py-2 pt-6">
|
||||
Additional Details
|
||||
</h1>
|
||||
{additionalDetailsFields.map(renerField)}
|
||||
<FormHeader text={"Additional Details"}/>
|
||||
{additionalDetailsFields.map(renderField)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
text={t("submit")}
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting}
|
||||
/>
|
||||
<Button text={t("submit")} onClick={handleSubmit} />
|
||||
</FormBox>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,25 +1,23 @@
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { useToast } from "../hooks/useToast";
|
||||
import { useLoading } from "../hooks/useLoading";
|
||||
import FormBox from "../components/FormBox";
|
||||
import Button from "../components/Button";
|
||||
import { useToast } from "../hooks/useToast";
|
||||
import { lockerService } from "../services/locker.service";
|
||||
import { AnimatePresence } from "motion/react";
|
||||
import { useLoading } from "../hooks/useLoading";
|
||||
import Notification from "../components/Notification";
|
||||
import FormField from "../components/FormField";
|
||||
import FormInput from "../components/FormInput";
|
||||
import FormSelect from "../components/FormSelect";
|
||||
|
||||
function LockersRegistration() {
|
||||
const location = useLocation();
|
||||
const showToast = useToast();
|
||||
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 [submitting, setSubmitting] = useState(false);
|
||||
const [notification, setNotification] = useState({
|
||||
message: "",
|
||||
type: "",
|
||||
});
|
||||
|
||||
const initLockers = Array(noOfLockers ? parseInt(noOfLockers) : 0)
|
||||
.fill()
|
||||
@ -31,12 +29,14 @@ function LockersRegistration() {
|
||||
sizeValid: true,
|
||||
keyIdValid: true,
|
||||
}));
|
||||
|
||||
const [lockerValues, setLockerValues] = useState(initLockers);
|
||||
if(!location.state) {
|
||||
return <></>
|
||||
|
||||
if (!location.state) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
console.log("submitting");
|
||||
e.preventDefault();
|
||||
const idRegex = /^[A-Z]{2}[0-9]{4}$/;
|
||||
let valid = true;
|
||||
@ -92,16 +92,10 @@ function LockersRegistration() {
|
||||
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 lockerService.registerLockers(
|
||||
cabinetId,
|
||||
@ -120,84 +114,50 @@ function LockersRegistration() {
|
||||
return;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const lockerDetails = lockerValues.map((locker, index) => {
|
||||
return (
|
||||
<div key={index} className="flex gap-12 items-center">
|
||||
<label className="text-lg text-bold dark:text-primary-dark">{`Locker ${
|
||||
index + 1
|
||||
}`}</label>
|
||||
|
||||
<input
|
||||
<FormField key={index} label={`Locker ${index + 1}`} variant="long">
|
||||
<FormInput
|
||||
value={locker.id}
|
||||
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.idValid && "border-error text-error"
|
||||
)}
|
||||
onChange={(e) => {
|
||||
onChange={ e => {
|
||||
const newValues = [...lockerValues];
|
||||
newValues[index] = {
|
||||
...newValues[index],
|
||||
id: e.target.value.toUpperCase(),
|
||||
idValid: true,
|
||||
};
|
||||
newValues[index].id = e.target.value;
|
||||
newValues[index].idValid = true;
|
||||
setLockerValues(newValues);
|
||||
}}
|
||||
placeholder="Locker ID"
|
||||
type="text"
|
||||
maxLength={6}
|
||||
valid={locker.idValid}
|
||||
placeholder={"Locker Id"}
|
||||
/>
|
||||
|
||||
<select
|
||||
<FormSelect
|
||||
value={locker.size}
|
||||
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.sizeValid && "border-error"
|
||||
)}
|
||||
onChange={(e) => {
|
||||
onChange={e => {
|
||||
const newValues = [...lockerValues];
|
||||
newValues[index] = {
|
||||
...newValues[index],
|
||||
size: e.target.value,
|
||||
sizeValid: true,
|
||||
};
|
||||
newValues[index].size = e.target.value;
|
||||
newValues[index].sizeValid = true;
|
||||
setLockerValues(newValues);
|
||||
}}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Size
|
||||
</option>
|
||||
<option value="small">Small</option>
|
||||
<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}
|
||||
valid={locker.sizeValid}
|
||||
options={[
|
||||
{label: 'Small', value: '1'},
|
||||
{label: 'Medium', value: '2'},
|
||||
{label: 'Large', value: '3'}
|
||||
]}
|
||||
/>
|
||||
</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" && (
|
||||
<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">
|
||||
{cabinetId}
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 p-4">{lockerDetails}</div>
|
||||
<Button
|
||||
disabled={submitting}
|
||||
text="Register"
|
||||
onClick={(e) => {
|
||||
handleSubmit(e);
|
||||
|
Loading…
x
Reference in New Issue
Block a user