From 848dfd4c319c3c45d76aedb0a17a06166bdde23f Mon Sep 17 00:00:00 2001 From: Norddin Date: Wed, 1 Mar 2023 15:33:34 +0100 Subject: [PATCH 1/9] Added the 'Roboto' font from Google fonts and added it as a global CSS var called `--font-primary` --- public/index.html | 6 ++++++ src/App.css | 1 + 2 files changed, 7 insertions(+) diff --git a/public/index.html b/public/index.html index 69cddec..e8b8e82 100644 --- a/public/index.html +++ b/public/index.html @@ -1,6 +1,12 @@ + + + diff --git a/src/App.css b/src/App.css index c53df1f..2fb3d23 100644 --- a/src/App.css +++ b/src/App.css @@ -8,6 +8,7 @@ */ :root { + --font-primary: "Roboto", sans-serif; --border-radius-default: 4px; --border-radius-xl: 15px; --color-accent: #cacfdd; From ec6d10405eb2ddbe2f23f3a79efc2b10b916322b Mon Sep 17 00:00:00 2001 From: Norddin Date: Wed, 1 Mar 2023 16:44:11 +0100 Subject: [PATCH 2/9] Created separate styles for .primary and .secondary variants of the button component. --- src/ui/components/Button/Button.module.css | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/ui/components/Button/Button.module.css b/src/ui/components/Button/Button.module.css index c10325a..fcbfbc3 100644 --- a/src/ui/components/Button/Button.module.css +++ b/src/ui/components/Button/Button.module.css @@ -23,3 +23,15 @@ /** TODO: Create separate styles for .primary and .secondary variants of this button: * - Must use the CSS variables declared in App.css */ + +.primary { + color: var(--color-background); + background-color: var(--color-brand); + border-color: var(--color-border); +} + +.secondary { + color: var(--color-brand); + background-color: var(--color-background); + border-color: var(--color-border); +} From 38990a3d28cf9b1305c0cf8a4c7c050c9313b98c Mon Sep 17 00:00:00 2001 From: Norddin Date: Wed, 1 Mar 2023 18:57:22 +0100 Subject: [PATCH 3/9] Made a custom hook to set form fields in a more generic way. --- src/App.js | 49 ++++++++++++----------------------- src/ui/hooks/useFormFields.js | 12 +++++++++ 2 files changed, 29 insertions(+), 32 deletions(-) create mode 100644 src/ui/hooks/useFormFields.js diff --git a/src/App.js b/src/App.js index 4050ce0..ba3ede5 100644 --- a/src/App.js +++ b/src/App.js @@ -10,6 +10,7 @@ import transformAddress from "./core/models/address"; import useAddressBook from "./ui/hooks/useAddressBook"; import "./App.css"; +import useFormFields from "./ui/hooks/useFormFields"; function App() { /** @@ -20,11 +21,8 @@ function App() { * - Remove all individual React.useState * - Remove all individual onChange handlers, like handleZipCodeChange for example */ - const [zipCode, setZipCode] = React.useState(""); - const [houseNumber, setHouseNumber] = React.useState(""); - const [firstName, setFirstName] = React.useState(""); - const [lastName, setLastName] = React.useState(""); - const [selectedAddress, setSelectedAddress] = React.useState(""); + const initialValues = {zipCode: "", houseNumber: "", firstName: "", lastName: "", selectedAddress: ""} + const { values, handleChange } = useFormFields(initialValues); /** * Results states */ @@ -35,19 +33,6 @@ function App() { */ const { addAddress } = useAddressBook(); - /** - * Text fields onChange handlers - */ - const handleZipCodeChange = (e) => setZipCode(e.target.value); - - const handleHouseNumberChange = (e) => setHouseNumber(e.target.value); - - const handleFirstNameChange = (e) => setFirstName(e.target.value); - - const handleLastNameChange = (e) => setLastName(e.target.value); - - const handleSelectedAddressChange = (e) => setSelectedAddress(e.target.value); - const handleAddressSubmit = async (e) => { e.preventDefault(); @@ -63,7 +48,7 @@ function App() { const handlePersonSubmit = (e) => { e.preventDefault(); - if (!selectedAddress || !addresses.length) { + if (!values.selectedAddress || !addresses.length) { setError( "No address selected, try to select an address or find one if you haven't" ); @@ -71,10 +56,10 @@ function App() { } const foundAddress = addresses.find( - (address) => address.id === selectedAddress + (address) => address.id === values.selectedAddress ); - addAddress({ ...foundAddress, firstName, lastName }); + addAddress({ ...foundAddress, firstName: values.firstName, lastName: values.lastName }); }; return ( @@ -94,16 +79,16 @@ function App() {
@@ -117,14 +102,14 @@ function App() { name="selectedAddress" id={address.id} key={address.id} - onChange={handleSelectedAddressChange} + onChange={handleChange} > -
+
); })} {/* TODO: Create generic
component to display form rows, legend and a submit button */} - {selectedAddress && ( + {values.selectedAddress && (
✏️ Add personal info to address @@ -132,16 +117,16 @@ function App() {
diff --git a/src/ui/hooks/useFormFields.js b/src/ui/hooks/useFormFields.js new file mode 100644 index 0000000..c9c2a47 --- /dev/null +++ b/src/ui/hooks/useFormFields.js @@ -0,0 +1,12 @@ +import React, { useState } from "react"; + +export default function useFormFields(initialValues) { + const [values, setValues] = useState(initialValues) + + const handleChange = (e) => { + const { name, value } = e.target; + setValues((prevValues) => ({...prevValues, [name]: value })) + } + + return { values, handleChange }; +} From 9559d6268939a5c90e9ecf46355887e8717b9a3e Mon Sep 17 00:00:00 2001 From: Norddin Date: Thu, 2 Mar 2023 10:05:01 +0100 Subject: [PATCH 4/9] Added code to fetch addresses based on houseNumber and zipCode. --- src/App.js | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/App.js b/src/App.js index ba3ede5..311af8b 100644 --- a/src/App.js +++ b/src/App.js @@ -21,7 +21,13 @@ function App() { * - Remove all individual React.useState * - Remove all individual onChange handlers, like handleZipCodeChange for example */ - const initialValues = {zipCode: "", houseNumber: "", firstName: "", lastName: "", selectedAddress: ""} + const initialValues = { + zipCode: "", + houseNumber: "", + firstName: "", + lastName: "", + selectedAddress: "", + }; const { values, handleChange } = useFormFields(initialValues); /** * Results states @@ -43,6 +49,20 @@ function App() { * - Make sure to add the houseNumber to each found address in the response using `transformAddress()` function * - Bonus: Add a loading state in the UI while fetching addresses */ + const zipCode = e.target.elements.zipCode.value; + const houseNr = e.target.elements.houseNumber.value; + + const res = await fetch( + `http://api.postcodedata.nl/v1/postcode/?postcode=${zipCode}&streetnumber=${houseNr}&ref=domeinnaam&type=json` + ); + const data = await res.json(); + + if (data.status === "error") return setError(data.errormessage); + // remove any potential previous errors if current request has no errors + setError(undefined); + + const transformedAddress = transformAddress({...data.details[0], houseNr}); + setAddresses((prevAddresses) => [...prevAddresses, transformedAddress]); }; const handlePersonSubmit = (e) => { @@ -59,7 +79,11 @@ function App() { (address) => address.id === values.selectedAddress ); - addAddress({ ...foundAddress, firstName: values.firstName, lastName: values.lastName }); + addAddress({ + ...foundAddress, + firstName: values.firstName, + lastName: values.lastName, + }); }; return ( @@ -104,7 +128,7 @@ function App() { key={address.id} onChange={handleChange} > -
+
); })} From f1fb1d759cd33a3e3cad2a8dd6e777f7f9fb7d0e Mon Sep 17 00:00:00 2001 From: Norddin Date: Thu, 2 Mar 2023 11:47:07 +0100 Subject: [PATCH 5/9] Created generic component to display form rows, legend and a submit button. --- src/App.js | 74 ++++++++------------------ src/ui/components/Form/Form.js | 43 +++++++++++++++ src/ui/components/Form/Form.module.css | 0 3 files changed, 66 insertions(+), 51 deletions(-) create mode 100644 src/ui/components/Form/Form.js create mode 100644 src/ui/components/Form/Form.module.css diff --git a/src/App.js b/src/App.js index 311af8b..a78b257 100644 --- a/src/App.js +++ b/src/App.js @@ -2,15 +2,14 @@ import React from "react"; import Address from "./ui/components/Address/Address"; import AddressBook from "./ui/components/AddressBook/AddressBook"; -import Button from "./ui/components/Button/Button"; -import InputText from "./ui/components/InputText/InputText"; import Radio from "./ui/components/Radio/Radio"; import Section from "./ui/components/Section/Section"; +import Form from "./ui/components/Form/Form"; import transformAddress from "./core/models/address"; import useAddressBook from "./ui/hooks/useAddressBook"; +import useFormFields from "./ui/hooks/useFormFields"; import "./App.css"; -import useFormFields from "./ui/hooks/useFormFields"; function App() { /** @@ -50,10 +49,10 @@ function App() { * - Bonus: Add a loading state in the UI while fetching addresses */ const zipCode = e.target.elements.zipCode.value; - const houseNr = e.target.elements.houseNumber.value; + const houseNumber = e.target.elements.houseNumber.value; const res = await fetch( - `http://api.postcodedata.nl/v1/postcode/?postcode=${zipCode}&streetnumber=${houseNr}&ref=domeinnaam&type=json` + `http://api.postcodedata.nl/v1/postcode/?postcode=${zipCode}&streetnumber=${houseNumber}&ref=domeinnaam&type=json` ); const data = await res.json(); @@ -61,8 +60,9 @@ function App() { // remove any potential previous errors if current request has no errors setError(undefined); - const transformedAddress = transformAddress({...data.details[0], houseNr}); - setAddresses((prevAddresses) => [...prevAddresses, transformedAddress]); + const address = transformAddress({ ...data.details[0], houseNumber }); + + setAddresses((prevAddresses) => [...prevAddresses, address]); }; const handlePersonSubmit = (e) => { @@ -97,28 +97,14 @@ function App() { {/* TODO: Create generic component to display form rows, legend and a submit button */} - -
- 🏠 Find an address -
- -
-
- -
- -
- +
{addresses.length > 0 && addresses.map((address) => { return ( @@ -134,28 +120,14 @@ function App() { })} {/* TODO: Create generic component to display form rows, legend and a submit button */} {values.selectedAddress && ( - -
- ✏️ Add personal info to address -
- -
-
- -
- -
-
+
)} {/* TODO: Create an component for displaying an error message */} diff --git a/src/ui/components/Form/Form.js b/src/ui/components/Form/Form.js new file mode 100644 index 0000000..6332781 --- /dev/null +++ b/src/ui/components/Form/Form.js @@ -0,0 +1,43 @@ +import React from "react"; +import Button from "../Button/Button"; +import InputText from "../InputText/InputText"; + +const Form = ({ + initialValues, + formFieldNames, + caption, + buttonTitle, + onSubmit, + onChange, +}) => { + + const formatPlaceholder = (text) => { + // split text based on capital letters + const str = text.split(/(?=[A-Z])/).join(" ").toLowerCase(); + // Capitalize the first letter and return entire string + return str.charAt(0).toUpperCase() + str.slice(1); + }; + + return ( + +
+ {caption} + {formFieldNames.map((formFieldName) => { + return ( +
+ +
+ ); + })} + +
+ + ); +}; + +export default Form; diff --git a/src/ui/components/Form/Form.module.css b/src/ui/components/Form/Form.module.css new file mode 100644 index 0000000..e69de29 From c1269f6cc0a6b4df8b1240f8b2b7c800286c9790 Mon Sep 17 00:00:00 2001 From: Norddin Date: Thu, 2 Mar 2023 11:57:09 +0100 Subject: [PATCH 6/9] Created an component for displaying an error message. --- src/App.js | 3 ++- src/ui/components/ErrorMessage/ErrorMessage.js | 11 +++++++++++ .../components/ErrorMessage/ErrorMessage.module.css | 4 ++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 src/ui/components/ErrorMessage/ErrorMessage.js create mode 100644 src/ui/components/ErrorMessage/ErrorMessage.module.css diff --git a/src/App.js b/src/App.js index a78b257..a273502 100644 --- a/src/App.js +++ b/src/App.js @@ -5,6 +5,7 @@ import AddressBook from "./ui/components/AddressBook/AddressBook"; import Radio from "./ui/components/Radio/Radio"; import Section from "./ui/components/Section/Section"; import Form from "./ui/components/Form/Form"; +import ErrorMessage from "./ui/components/ErrorMessage/ErrorMessage"; import transformAddress from "./core/models/address"; import useAddressBook from "./ui/hooks/useAddressBook"; import useFormFields from "./ui/hooks/useFormFields"; @@ -131,7 +132,7 @@ function App() { )} {/* TODO: Create an component for displaying an error message */} - {error &&
{error}
} + {error && } {/* TODO: Add a button to clear all form fields. Button must look different from the default primary button, see design. */} diff --git a/src/ui/components/ErrorMessage/ErrorMessage.js b/src/ui/components/ErrorMessage/ErrorMessage.js new file mode 100644 index 0000000..68e0add --- /dev/null +++ b/src/ui/components/ErrorMessage/ErrorMessage.js @@ -0,0 +1,11 @@ +import React from "react"; + +import $ from "./ErrorMessage.module.css"; + +const ErrorMessage = ({ error }) => { + return ( +
{error}
+ ); +}; + +export default ErrorMessage; diff --git a/src/ui/components/ErrorMessage/ErrorMessage.module.css b/src/ui/components/ErrorMessage/ErrorMessage.module.css new file mode 100644 index 0000000..ce94c30 --- /dev/null +++ b/src/ui/components/ErrorMessage/ErrorMessage.module.css @@ -0,0 +1,4 @@ +.error { + color: red; + padding: 0.5em 0; + } \ No newline at end of file From eba68e761ed0a5cc1c5b8d86d35722adfe0b62c1 Mon Sep 17 00:00:00 2001 From: Norddin Date: Thu, 2 Mar 2023 12:23:30 +0100 Subject: [PATCH 7/9] - TODO: Added a button, according to the design, to clear all form fields. - TODO: Added conditional classNames for primary and secondary variant in
diff --git a/src/ui/components/Button/Button.js b/src/ui/components/Button/Button.js index dc73e26..d57238a 100644 --- a/src/ui/components/Button/Button.js +++ b/src/ui/components/Button/Button.js @@ -7,14 +7,16 @@ const Button = ({ children, onClick, type = "button", - variant = "primary", // or 'secondary' + isPrimary = true }) => { + const classNames = `${$.button} ${isPrimary ? $.primary : $.secondary }`; + return ( +
diff --git a/src/core/reducers/addressBook.js b/src/core/reducers/addressBook.js index 6d3980a..e417dfc 100644 --- a/src/core/reducers/addressBook.js +++ b/src/core/reducers/addressBook.js @@ -6,10 +6,22 @@ const reducer = (state = defaultState, action) => { switch (action.type) { case "address/add": /** TODO: Prevent duplicate addresses */ - return { ...state, addresses: [...state.addresses, action.payload] }; + if ( + state.addresses.some( + (address) => + address.houseNumber === action.payload.houseNumber && + address.postcode === action.payload.postcode + ) + ) + return state; + else return { ...state, addresses: [...state.addresses, action.payload] }; + case "address/remove": /** TODO: Write a state update which removes an address from the addresses array. */ - return state; + const newAddresses = state.addresses.filter( + (address) => address.id !== action.payload + ); + return { ...state, addresses: newAddresses }; case "addresses/add": { return { ...state, addresses: action.payload }; } diff --git a/src/ui/components/Button/Button.js b/src/ui/components/Button/Button.js index d57238a..ac3d419 100644 --- a/src/ui/components/Button/Button.js +++ b/src/ui/components/Button/Button.js @@ -7,9 +7,9 @@ const Button = ({ children, onClick, type = "button", - isPrimary = true + variant = "primary", // or 'secondary' }) => { - const classNames = `${$.button} ${isPrimary ? $.primary : $.secondary }`; + const classNames = `${$.button} ${variant === "primary" ? $.primary : $.secondary }`; return (