From 84c3f8f611a9f1f0d12ee0f6007bfd281c70f3e1 Mon Sep 17 00:00:00 2001 From: Rupinder Date: Sun, 13 Aug 2023 10:26:29 +1000 Subject: [PATCH 1/7] moved add customer page to separate route --- public/index.html | 2 +- src/App.tsx | 6 ++++- .../AddCustomer/AddCustomerForm.tsx | 25 ++++++++++++++++--- .../AddCustomer/StyledAddCustomerForm.ts | 17 +++++++++++++ src/views/Home.tsx | 23 +++++++++++++++-- 5 files changed, 65 insertions(+), 8 deletions(-) diff --git a/public/index.html b/public/index.html index e6cc647..9b05d89 100644 --- a/public/index.html +++ b/public/index.html @@ -24,7 +24,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - Customer List + Customer Management diff --git a/src/App.tsx b/src/App.tsx index 9b36496..28f0e67 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { StyledMain, StyledHeader, StyledHeaderText } from "./StyledApp"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import Home from "./views/Home"; +import { AddCustomerForm } from "./components/AddCustomer/AddCustomerForm"; const App: React.FC = () => { return ( @@ -11,9 +12,12 @@ const App: React.FC = () => { - + + + + diff --git a/src/components/AddCustomer/AddCustomerForm.tsx b/src/components/AddCustomer/AddCustomerForm.tsx index 52ebf9e..adb2ed1 100644 --- a/src/components/AddCustomer/AddCustomerForm.tsx +++ b/src/components/AddCustomer/AddCustomerForm.tsx @@ -6,13 +6,29 @@ import { StyledInput, StyledLabel, StyledAddButton, + StyledCancelButton, } from "./StyledAddCustomerForm"; +import { addCustomer } from "../../redux/actions/customerActions"; +import { Dispatch } from "redux"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router-dom"; -type Props = { - saveCustomer: (customer: ICustomer | any) => void; -}; +export const AddCustomerForm: React.FC = () => { + let history = useHistory(); + const dispatch: Dispatch = useDispatch(); + + const saveCustomer = React.useCallback( + (customer: ICustomer | any) => { + dispatch(addCustomer(customer)); + history.push("/"); + }, + [dispatch] + ); + + const cancel = () => { + history.push("/"); + } -export const AddCustomerForm: React.FC = ({ saveCustomer }) => { return ( = ({ saveCustomer }) => { /> Add Customer + Cancel ); diff --git a/src/components/AddCustomer/StyledAddCustomerForm.ts b/src/components/AddCustomer/StyledAddCustomerForm.ts index a7a4bb9..fd33141 100644 --- a/src/components/AddCustomer/StyledAddCustomerForm.ts +++ b/src/components/AddCustomer/StyledAddCustomerForm.ts @@ -30,3 +30,20 @@ export const StyledAddButton = styled.button` border-radius: 4px; font-size: 16px; `; + +export const StyledCancelButton = styled.button` + padding: 1rem; + background-color: gray; + color: white; + border: none; + border-radius: 4px; + font-size: 16px; + cursor: pointer; + margin-top: 5px +`; + +export const StyledError = styled.div` + float: right; + font-size: 14px; + color: red; +`; diff --git a/src/views/Home.tsx b/src/views/Home.tsx index 8911b39..b087016 100644 --- a/src/views/Home.tsx +++ b/src/views/Home.tsx @@ -1,10 +1,23 @@ import * as React from "react"; import { useSelector, shallowEqual, useDispatch } from "react-redux"; import { Customer } from "../components/Customer/Customer"; -import { AddCustomerForm } from "../components/AddCustomer/AddCustomerForm"; import { Dispatch } from "redux"; import { CustomerState, ICustomer } from "../types/types"; import { addCustomer, removeCustomer } from "../redux/actions/customerActions"; +import styled from "styled-components"; +import { Link } from "react-router-dom"; + +const StyledAddCustomerButton = styled(Link)` + padding: 1rem; + background-color: rgb(4, 121, 205); + color: white; + border: none; + border-radius: 4px; + font-size: 16px; + width: 100%; + text-align: center; + text-decoration: auto; +`; const Home: React.FC = () => { const customers: readonly ICustomer[] = useSelector( @@ -21,7 +34,13 @@ const Home: React.FC = () => { return ( <> - +

Customer List

+ + New Customer + {customers.map((customer: ICustomer) => ( Date: Sun, 13 Aug 2023 10:39:50 +1000 Subject: [PATCH 2/7] added customer birthday --- src/components/AddCustomer/AddCustomerForm.tsx | 11 ++++++++++- src/components/Customer/Customer.tsx | 3 +++ src/redux/reducers/customerReducers.tsx | 4 ++++ src/types/types.ts | 1 + 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/components/AddCustomer/AddCustomerForm.tsx b/src/components/AddCustomer/AddCustomerForm.tsx index adb2ed1..199720e 100644 --- a/src/components/AddCustomer/AddCustomerForm.tsx +++ b/src/components/AddCustomer/AddCustomerForm.tsx @@ -27,7 +27,7 @@ export const AddCustomerForm: React.FC = () => { const cancel = () => { history.push("/"); - } + }; return ( { firstName: "", lastName: "", phoneNumber: "", + birthday: "", }} onSubmit={( values: Customer, @@ -70,6 +71,14 @@ export const AddCustomerForm: React.FC = () => { type="tel" /> + + Add Customer Cancel diff --git a/src/components/Customer/Customer.tsx b/src/components/Customer/Customer.tsx index 8fd093a..f8e84d7 100644 --- a/src/components/Customer/Customer.tsx +++ b/src/components/Customer/Customer.tsx @@ -30,6 +30,9 @@ export const Customer: React.FC = ({ customer, removeCustomer }) => { Phone number: {customer.phoneNumber} + + Birthday: {customer.birthday} + deleteCustomer(customer)}> Delete diff --git a/src/redux/reducers/customerReducers.tsx b/src/redux/reducers/customerReducers.tsx index d6d837e..d0c93f9 100644 --- a/src/redux/reducers/customerReducers.tsx +++ b/src/redux/reducers/customerReducers.tsx @@ -8,18 +8,21 @@ export const initialState: CustomerState = { firstName: "Charles", lastName: "Babbage", phoneNumber: "0412 123 123", + birthday: "1992-10-05", }, { id: 2, firstName: "Alan", lastName: "Turing", phoneNumber: "(03) 9599 1234", + birthday: "1990-11-02", }, { id: 3, firstName: "Ada", lastName: "Lovelace", phoneNumber: "+61 423 345 567", + birthday: "1984-04-09", }, ], }; @@ -35,6 +38,7 @@ export const customerReducer = ( firstName: action.customer.firstName, lastName: action.customer.lastName, phoneNumber: action.customer.phoneNumber, + birthday: action.customer.birthday, }; return { ...state, diff --git a/src/types/types.ts b/src/types/types.ts index 83e2415..3c4cde2 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -2,6 +2,7 @@ export interface Customer { firstName: string; lastName: string; phoneNumber: string; + birthday: string; } export interface ICustomer extends Customer { From 8a78bdade53ed5e0bdb7fc33e6653c6697845a90 Mon Sep 17 00:00:00 2001 From: Rupinder Date: Sun, 13 Aug 2023 11:53:23 +1000 Subject: [PATCH 3/7] added form validations --- .../AddCustomer/AddCustomerForm.tsx | 109 ++++++++++++------ 1 file changed, 76 insertions(+), 33 deletions(-) diff --git a/src/components/AddCustomer/AddCustomerForm.tsx b/src/components/AddCustomer/AddCustomerForm.tsx index 199720e..bdcb606 100644 --- a/src/components/AddCustomer/AddCustomerForm.tsx +++ b/src/components/AddCustomer/AddCustomerForm.tsx @@ -7,11 +7,28 @@ import { StyledLabel, StyledAddButton, StyledCancelButton, + StyledError, } from "./StyledAddCustomerForm"; import { addCustomer } from "../../redux/actions/customerActions"; import { Dispatch } from "redux"; import { useDispatch } from "react-redux"; import { useHistory } from "react-router-dom"; +import * as Yup from "yup"; + +const CustomerSchema = Yup.object().shape({ + firstName: Yup.string() + .min(2, "Too Short!") + .max(50, "Too Long!") + .required("Required"), + lastName: Yup.string() + .min(2, "Too Short!") + .max(50, "Too Long!") + .required("Required"), + phoneNumber: Yup.string() + .matches(/^(\(0[1-9]\)|0[1-9])?( ?-?[0-9]){10,10}$/, "Invalid phone number") + .required("Required"), + birthday: Yup.date().required("Required"), +}); export const AddCustomerForm: React.FC = () => { let history = useHistory(); @@ -37,6 +54,7 @@ export const AddCustomerForm: React.FC = () => { phoneNumber: "", birthday: "", }} + validationSchema={CustomerSchema} onSubmit={( values: Customer, { setSubmitting }: FormikHelpers @@ -45,43 +63,68 @@ export const AddCustomerForm: React.FC = () => { setSubmitting(false); }} > - - First Name - + {({ errors, touched }) => ( + + + First Name*{" "} + {errors.firstName && touched.firstName ? ( + {errors.firstName} + ) : null} + + + + + Last Name*{" "} + {errors.lastName && touched.lastName ? ( + {errors.lastName} + ) : null} + + + + + Phone Number*{" "} + {errors.phoneNumber && touched.phoneNumber ? ( + {errors.phoneNumber} + ) : null} + - Last Name - + - Phone Number - + + Birthday*{" "} + {errors.birthday && touched.birthday ? ( + {errors.birthday} + ) : null} + - + - Add Customer - Cancel - + Add Customer + Cancel + + )} ); }; From 2e354652b15e7aa36af91a8009c13a1dfe80c33a Mon Sep 17 00:00:00 2001 From: Rupinder Date: Sun, 13 Aug 2023 12:02:39 +1000 Subject: [PATCH 4/7] added accessibility aria-* attributes --- src/components/AddCustomer/AddCustomerForm.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/AddCustomer/AddCustomerForm.tsx b/src/components/AddCustomer/AddCustomerForm.tsx index bdcb606..fbdee8e 100644 --- a/src/components/AddCustomer/AddCustomerForm.tsx +++ b/src/components/AddCustomer/AddCustomerForm.tsx @@ -76,6 +76,8 @@ export const AddCustomerForm: React.FC = () => { id="firstName" name="firstName" placeholder="John" + aria-label={'First Name'} + aria-required="true" /> @@ -89,6 +91,8 @@ export const AddCustomerForm: React.FC = () => { id="lastName" name="lastName" placeholder="Doe" + aria-label={'Last Name'} + aria-required="true" /> @@ -104,6 +108,8 @@ export const AddCustomerForm: React.FC = () => { name="phoneNumber" placeholder="03 333 333 333" type="tel" + aria-label={'Phone'} + aria-required="true" /> @@ -119,6 +125,8 @@ export const AddCustomerForm: React.FC = () => { name="birthday" placeholder="DD/MM/YYYY" type="date" + aria-label={'Birthday'} + aria-required="true" /> Add Customer From 2d4a27160f86e5f0c247fab1904fc098c61480e6 Mon Sep 17 00:00:00 2001 From: Rupinder Date: Sun, 13 Aug 2023 13:12:31 +1000 Subject: [PATCH 5/7] added grid to display customers --- package.json | 2 + src/components/Customer/StyledCustomer.ts | 3 +- src/components/Grid/ButtonCellRenderer.tsx | 21 +++++++++ src/views/Home.tsx | 55 +++++++++++++++------- 4 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 src/components/Grid/ButtonCellRenderer.tsx diff --git a/package.json b/package.json index 07b580b..ae92a60 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "@types/react-router-dom": "^5.1.7", "@types/styled-components": "^5.1.10", "@types/yup": "^0.29.11", + "ag-grid-community": "30.0.6", + "ag-grid-react": "30.0.6", "formik": "^2.2.9", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/src/components/Customer/StyledCustomer.ts b/src/components/Customer/StyledCustomer.ts index f6e8885..079073a 100644 --- a/src/components/Customer/StyledCustomer.ts +++ b/src/components/Customer/StyledCustomer.ts @@ -19,7 +19,8 @@ export const StyledCustomerInfo = styled.p` `; export const StyledCustomerDelete = styled.button` - padding: 1rem; + padding: 6px; + height: 30px; background-color: #dc1616; color: white; border: none; diff --git a/src/components/Grid/ButtonCellRenderer.tsx b/src/components/Grid/ButtonCellRenderer.tsx new file mode 100644 index 0000000..d297772 --- /dev/null +++ b/src/components/Grid/ButtonCellRenderer.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { StyledCustomerDelete } from "../Customer/StyledCustomer"; +import { Dispatch } from "redux"; +import { useDispatch } from "react-redux"; +import { ICustomer } from "../../types/types"; +import { removeCustomer } from "../../redux/actions/customerActions"; + +export const ButtonCellRenderer: React.FC = (params) => { + const dispatch: Dispatch = useDispatch(); + + const deleteCustomer = React.useCallback( + (customer: ICustomer) => dispatch(removeCustomer(customer)), + [dispatch, removeCustomer] + ); + + return ( + deleteCustomer(params.data)}> + Delete + + ); +}; diff --git a/src/views/Home.tsx b/src/views/Home.tsx index b087016..83e0c74 100644 --- a/src/views/Home.tsx +++ b/src/views/Home.tsx @@ -1,11 +1,14 @@ import * as React from "react"; -import { useSelector, shallowEqual, useDispatch } from "react-redux"; -import { Customer } from "../components/Customer/Customer"; -import { Dispatch } from "redux"; +import { useSelector, shallowEqual } from "react-redux"; import { CustomerState, ICustomer } from "../types/types"; -import { addCustomer, removeCustomer } from "../redux/actions/customerActions"; import styled from "styled-components"; import { Link } from "react-router-dom"; +import { AgGridReact } from "ag-grid-react"; +import { ButtonCellRenderer } from "../components/Grid/ButtonCellRenderer" + +import "ag-grid-community/styles/ag-grid.css"; +import "ag-grid-community/styles/ag-theme-alpine.css"; +import { ColDef } from "ag-grid-community"; const StyledAddCustomerButton = styled(Link)` padding: 1rem; @@ -17,20 +20,34 @@ const StyledAddCustomerButton = styled(Link)` width: 100%; text-align: center; text-decoration: auto; + margin-bottom: 5px; `; +const filterConfig = { + filter: true, + floatingFilter: true, +}; const Home: React.FC = () => { - const customers: readonly ICustomer[] = useSelector( + const gridRef = React.useRef>(null); + + const customers: ICustomer[] = useSelector( (state: CustomerState) => state.customers, shallowEqual ); - const dispatch: Dispatch = useDispatch(); + const colDefs: ColDef[] = [ + { field: "firstName", ...filterConfig }, + { field: "lastName", ...filterConfig }, + { field: "phoneNumber", ...filterConfig }, + { field: "birthday", ...filterConfig }, + { field: "", cellRenderer: ButtonCellRenderer} + ]; - const saveCustomer = React.useCallback( - (customer: ICustomer) => dispatch(addCustomer(customer)), - [dispatch] - ); + const onFirstDataRendered = React.useCallback((params) => { + gridRef?.current?.api.sizeColumnsToFit(); + }, []); + + const [rowData, setRowData] = React.useState(customers); return ( <> @@ -41,13 +58,17 @@ const Home: React.FC = () => { > New Customer - {customers.map((customer: ICustomer) => ( - - ))} +
+ + ref={gridRef} + rowData={rowData} + columnDefs={colDefs} + onFirstDataRendered={onFirstDataRendered} + > +
); }; From 4c5c11fc5dbd8501a52f6c1eea21d52fc4cf9ba2 Mon Sep 17 00:00:00 2001 From: Rupinder Date: Sun, 13 Aug 2023 13:18:52 +1000 Subject: [PATCH 6/7] enabed sorting --- src/views/Home.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/views/Home.tsx b/src/views/Home.tsx index 83e0c74..8d1d1f3 100644 --- a/src/views/Home.tsx +++ b/src/views/Home.tsx @@ -36,10 +36,10 @@ const Home: React.FC = () => { ); const colDefs: ColDef[] = [ - { field: "firstName", ...filterConfig }, - { field: "lastName", ...filterConfig }, - { field: "phoneNumber", ...filterConfig }, - { field: "birthday", ...filterConfig }, + { field: "firstName", ...filterConfig, sortable: true }, + { field: "lastName", ...filterConfig, sortable: true }, + { field: "phoneNumber", ...filterConfig, sortable: true }, + { field: "birthday", ...filterConfig, sortable: true }, { field: "", cellRenderer: ButtonCellRenderer} ]; @@ -47,8 +47,6 @@ const Home: React.FC = () => { gridRef?.current?.api.sizeColumnsToFit(); }, []); - const [rowData, setRowData] = React.useState(customers); - return ( <>

Customer List

@@ -64,7 +62,7 @@ const Home: React.FC = () => { > ref={gridRef} - rowData={rowData} + rowData={customers} columnDefs={colDefs} onFirstDataRendered={onFirstDataRendered} > From 7085e86e6bc4d856709ddd477b404373b4857491 Mon Sep 17 00:00:00 2001 From: Rupinder Date: Sun, 13 Aug 2023 19:20:15 +1000 Subject: [PATCH 7/7] added validation if customer already exist --- .../AddCustomer/AddCustomerForm.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/AddCustomer/AddCustomerForm.tsx b/src/components/AddCustomer/AddCustomerForm.tsx index fbdee8e..1878f71 100644 --- a/src/components/AddCustomer/AddCustomerForm.tsx +++ b/src/components/AddCustomer/AddCustomerForm.tsx @@ -1,6 +1,6 @@ import { Field, Formik, FormikHelpers } from "formik"; import * as React from "react"; -import { ICustomer, Customer } from "../../types/types"; +import { ICustomer, Customer, CustomerState } from "../../types/types"; import { StyledForm, StyledInput, @@ -11,7 +11,7 @@ import { } from "./StyledAddCustomerForm"; import { addCustomer } from "../../redux/actions/customerActions"; import { Dispatch } from "redux"; -import { useDispatch } from "react-redux"; +import { shallowEqual, useDispatch, useSelector } from "react-redux"; import { useHistory } from "react-router-dom"; import * as Yup from "yup"; @@ -34,10 +34,22 @@ export const AddCustomerForm: React.FC = () => { let history = useHistory(); const dispatch: Dispatch = useDispatch(); + const customers: ICustomer[] = useSelector( + (state: CustomerState) => state.customers, + shallowEqual + ); + const saveCustomer = React.useCallback( (customer: ICustomer | any) => { - dispatch(addCustomer(customer)); - history.push("/"); + let isCustomerFound = customers.find(cust => cust.firstName === customer.firstName && cust.lastName === customer.lastName && cust.phoneNumber === customer.phoneNumber) + if(!isCustomerFound) { + dispatch(addCustomer(customer)); + history.push("/"); + } else { + alert('Customer already exist') + } + + }, [dispatch] );