From 3e969a6f49977f0fca0297a2480ae4591b6eb422 Mon Sep 17 00:00:00 2001 From: jordynelson-2 Date: Fri, 5 May 2023 10:21:11 +1000 Subject: [PATCH] Changes made by Jordan Nelson - Added Birthday field to Add Customer Form - Added Field level validation using Formik's documentation - Added toast notifications to show if a New User is Added, Deleted or Already Exists - Added a styled div to show the errors on fields --- package.json | 1 + .../AddCustomer/AddCustomerForm.tsx | 137 ++++++++++++++---- .../AddCustomer/StyledAddCustomerForm.ts | 8 + src/components/Customer/Customer.tsx | 33 +++-- src/components/Customer/StyledCustomer.ts | 2 + src/redux/reducers/customerReducers.spec.ts | 8 + src/redux/reducers/customerReducers.tsx | 40 ++++- src/types/types.ts | 3 +- src/views/Home.tsx | 6 +- yarn.lock | 12 ++ 10 files changed, 196 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 07b580b..a66f3f8 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "formik": "^2.2.9", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-hot-toast": "^2.4.1", "react-redux": "^7.2.4", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", diff --git a/src/components/AddCustomer/AddCustomerForm.tsx b/src/components/AddCustomer/AddCustomerForm.tsx index 52ebf9e..db2ff86 100644 --- a/src/components/AddCustomer/AddCustomerForm.tsx +++ b/src/components/AddCustomer/AddCustomerForm.tsx @@ -6,19 +6,67 @@ import { StyledInput, StyledLabel, StyledAddButton, + StyledErrorDiv, } from "./StyledAddCustomerForm"; type Props = { saveCustomer: (customer: ICustomer | any) => void; }; -export const AddCustomerForm: React.FC = ({ saveCustomer }) => { +function validateEmail(value: string) { + let error; + if (!value) { + error = "Required"; + } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)) { + error = "Invalid email address"; + } + return error; +} + +function validateFirstName(value: string) { + let error; + if (!value) { + error = "Required"; + } else if (!/^[a-zA-Z0-9]{2,50}$/i.test(value)) { + error = "Please enter a valid name"; + } + return error; +} + +function validateLastName(value: string) { + let error; + if (!value) { + error = "Required"; + } else if (!/^[a-zA-Z0-9]{2,50}$/i.test(value)) { + error = "Please enter a valid Last Name"; + } + return error; +} + +function validateBirthDate(value: string) { + let error; + if (!value) { + error = "Required"; + } else if (/^[0-9]{2}\/[0-9]{2}\/[0-9]{4}$/i.test(value)) { + error = "Please enter a valid date"; + } else { + const inputDate = new Date(value); + const currentDate = new Date(); + if (inputDate.getTime() >= currentDate.getTime()) { + error = "Please enter a date before today's date"; + } + } + return error; +} + +export const AddCustomerForm = ({ saveCustomer }: Props) => { return ( = ({ saveCustomer }) => { setSubmitting(false); }} > - - First Name - - - Last Name - - - Phone Number - - - Add Customer - + {({ errors, touched, validateForm }) => ( + + First Name + + {errors.firstName && touched.firstName && ( + {errors.firstName} + )} + + Last Name + + {errors.lastName && touched.lastName && ( + {errors.lastName} + )} + + Email + + {errors.email && touched.email && ( + {errors.email} + )} + + Birthday + + {errors.birthDate && touched.birthDate && ( + {errors.birthDate} + )} + + validateForm()}> + Add Customer + + + )} ); }; diff --git a/src/components/AddCustomer/StyledAddCustomerForm.ts b/src/components/AddCustomer/StyledAddCustomerForm.ts index a7a4bb9..12a9097 100644 --- a/src/components/AddCustomer/StyledAddCustomerForm.ts +++ b/src/components/AddCustomer/StyledAddCustomerForm.ts @@ -29,4 +29,12 @@ export const StyledAddButton = styled.button` border: none; border-radius: 4px; font-size: 16px; + cursor: pointer; +`; + +export const StyledErrorDiv = styled.div` + color: red; + font-size: 16px; + margin: 0 0 1rem; + padding: 0; `; diff --git a/src/components/Customer/Customer.tsx b/src/components/Customer/Customer.tsx index 8fd093a..cc2dac1 100644 --- a/src/components/Customer/Customer.tsx +++ b/src/components/Customer/Customer.tsx @@ -8,31 +8,36 @@ import { StyledCustomerInfo, StyledCustomerName, } from "./StyledCustomer"; +import { Toaster } from "react-hot-toast"; type Props = { customer: ICustomer; removeCustomer: (customer: ICustomer) => void; }; -export const Customer: React.FC = ({ customer, removeCustomer }) => { +export const Customer = ({ customer, removeCustomer }: Props) => { const dispatch: Dispatch = useDispatch(); const deleteCustomer = React.useCallback( - (customer: ICustomer) => dispatch(removeCustomer(customer)), - [dispatch, removeCustomer] + (customer: ICustomer) => { + dispatch(removeCustomer(customer)); + }, + [dispatch] ); return ( - - - {customer.firstName} {customer.lastName} - - - Phone number: {customer.phoneNumber} - - deleteCustomer(customer)}> - Delete - - + <> + + + {customer.firstName} {customer.lastName} + + Email: {customer.email} + Birthday: {customer.birthDate} + deleteCustomer(customer)}> + Delete + + + + ); }; diff --git a/src/components/Customer/StyledCustomer.ts b/src/components/Customer/StyledCustomer.ts index f6e8885..17192c7 100644 --- a/src/components/Customer/StyledCustomer.ts +++ b/src/components/Customer/StyledCustomer.ts @@ -6,6 +6,7 @@ export const StyledCustomer = styled.div` border: 1px solid #ccc; margin: 1rem; padding: 1rem; + border-radius: 4px; `; export const StyledCustomerName = styled.h2` @@ -25,4 +26,5 @@ export const StyledCustomerDelete = styled.button` border: none; border-radius: 4px; font-size: 16px; + cursor: pointer; `; diff --git a/src/redux/reducers/customerReducers.spec.ts b/src/redux/reducers/customerReducers.spec.ts index 3b03881..2d88bb7 100644 --- a/src/redux/reducers/customerReducers.spec.ts +++ b/src/redux/reducers/customerReducers.spec.ts @@ -8,18 +8,21 @@ const initialState = { id: 1, lastName: "Babbage", phoneNumber: "0412 123 123", + birthDate: "1791-12-26", }, { firstName: "Alan", id: 2, lastName: "Turing", phoneNumber: "(03) 9599 1234", + birthDate: "1791-12-26", }, { firstName: "Ada", id: 3, lastName: "Lovelace", phoneNumber: "+61 423 345 567", + birthDate: "1791-12-26", }, ], }; @@ -40,6 +43,7 @@ describe("customer reducer", () => { id: 1, lastName: "Dummy", phoneNumber: "000 000 000", + birthDate: "1791-12-26", }, }) ).toEqual({ @@ -49,6 +53,7 @@ describe("customer reducer", () => { id: 1, lastName: "Dummy", phoneNumber: "000 000 000", + birthDate: "1791-12-26", }, ], }); @@ -63,6 +68,7 @@ describe("customer reducer", () => { id: 1, lastName: "Babbage", phoneNumber: "0412 123 123", + birthDate: "1791-12-26", }, }) ).toEqual({ @@ -72,12 +78,14 @@ describe("customer reducer", () => { id: 2, lastName: "Turing", phoneNumber: "(03) 9599 1234", + birthDate: "1791-12-26", }, { firstName: "Ada", id: 3, lastName: "Lovelace", phoneNumber: "+61 423 345 567", + birthDate: "1791-12-26", }, ], }); diff --git a/src/redux/reducers/customerReducers.tsx b/src/redux/reducers/customerReducers.tsx index d6d837e..899cda3 100644 --- a/src/redux/reducers/customerReducers.tsx +++ b/src/redux/reducers/customerReducers.tsx @@ -1,5 +1,6 @@ import { CustomerAction, CustomerState, ICustomer } from "../../types/types"; import { ADD_CUSTOMER, REMOVE_CUSTOMER } from "../actions/customerTypes"; +import toast from "react-hot-toast"; export const initialState: CustomerState = { customers: [ @@ -7,23 +8,33 @@ export const initialState: CustomerState = { id: 1, firstName: "Charles", lastName: "Babbage", - phoneNumber: "0412 123 123", + email: "cb@test.com", + birthDate: "1791-12-26", }, { id: 2, firstName: "Alan", lastName: "Turing", - phoneNumber: "(03) 9599 1234", + email: "at@test.com", + birthDate: "1912-06-23", }, { id: 3, firstName: "Ada", lastName: "Lovelace", - phoneNumber: "+61 423 345 567", + email: "al@test.com", + birthDate: "1815-12-10", }, ], }; +const notifyExists = () => + toast("Customer already exists!", { + icon: "🙅", + }); +const notifySuccess = () => toast.success("Customer added !"); +const notifyDelete = () => toast.error("Customer Deleted!"); + export const customerReducer = ( state: CustomerState = initialState, action: CustomerAction @@ -34,16 +45,29 @@ export const customerReducer = ( id: action.customer.id ?? Math.random(), // not really unique but it's just an example firstName: action.customer.firstName, lastName: action.customer.lastName, - phoneNumber: action.customer.phoneNumber, - }; - return { - ...state, - customers: state.customers.concat(newCustomer), + email: action.customer.email, + birthDate: action.customer.birthDate, }; + + const customerExists = state.customers.find( + (customer) => customer.firstName === newCustomer.firstName + ); + + if (customerExists) { + notifyExists(); + return state; + } else { + notifySuccess(); + return { + ...state, + customers: state.customers.concat(newCustomer), + }; + } case REMOVE_CUSTOMER: const updatedCustomers: ICustomer[] = state.customers.filter( (customer) => customer.id !== action.customer.id ); + notifyDelete(); return { ...state, customers: updatedCustomers, diff --git a/src/types/types.ts b/src/types/types.ts index 83e2415..6c36045 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,7 +1,8 @@ export interface Customer { firstName: string; lastName: string; - phoneNumber: string; + email: string; + birthDate: string; } export interface ICustomer extends Customer { diff --git a/src/views/Home.tsx b/src/views/Home.tsx index 8911b39..e9a1349 100644 --- a/src/views/Home.tsx +++ b/src/views/Home.tsx @@ -5,6 +5,7 @@ import { AddCustomerForm } from "../components/AddCustomer/AddCustomerForm"; import { Dispatch } from "redux"; import { CustomerState, ICustomer } from "../types/types"; import { addCustomer, removeCustomer } from "../redux/actions/customerActions"; +import { Toaster } from "react-hot-toast"; const Home: React.FC = () => { const customers: readonly ICustomer[] = useSelector( @@ -15,7 +16,9 @@ const Home: React.FC = () => { const dispatch: Dispatch = useDispatch(); const saveCustomer = React.useCallback( - (customer: ICustomer) => dispatch(addCustomer(customer)), + (customer: ICustomer) => { + dispatch(addCustomer(customer)); + }, [dispatch] ); @@ -29,6 +32,7 @@ const Home: React.FC = () => { removeCustomer={removeCustomer} /> ))} + ); }; diff --git a/yarn.lock b/yarn.lock index 64dc156..409ebfe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5553,6 +5553,11 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" +goober@^2.1.10: + version "2.1.13" + resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.13.tgz#e3c06d5578486212a76c9eba860cbc3232ff6d7c" + integrity sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ== + graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.6" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" @@ -9298,6 +9303,13 @@ react-fast-compare@^2.0.1: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== +react-hot-toast@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.4.1.tgz#df04295eda8a7b12c4f968e54a61c8d36f4c0994" + integrity sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ== + dependencies: + goober "^2.1.10" + react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"