Skip to content
Open
17,216 changes: 17,216 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@100&display=swap"
rel="stylesheet"
/>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
Expand Down
24 changes: 18 additions & 6 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

:root {
--font-primary: "Roboto", sans-serif;
--border-radius-default: 4px;
--border-radius-xl: 15px;
--color-accent: #cacfdd;
Expand All @@ -16,7 +17,7 @@
--color-brand: #413ef7;
--color-text: #141925;
--color-text-light: #72787d;
--width-input: 20rem;
/* --width-input: 20rem; */
--width-content: 30rem;
}

Expand All @@ -29,6 +30,7 @@ body {
font-size: 18px;
font-family: var(--font-primary);
background: var(--color-accent);
text-align: center;
}

main {
Expand All @@ -55,11 +57,21 @@ legend {
color: var(--color-text-light);
}

.error {
color: red;
padding: 0.5em 0;
}

.form-row {
margin-bottom: 0.5em;
}

@media screen and (max-width: 400px) {
body {
font-size: 15px;
}
}

@media screen and (max-width: 320px) {
body {
font-size: 10px;
}
fieldset {
margin: 0;
}
}
145 changes: 75 additions & 70 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ 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 ErrorMessage from "./ui/components/ErrorMessage/ErrorMessage";
import transformAddress from "./core/models/address";
import useAddressBook from "./ui/hooks/useAddressBook";
import useFormFields from "./ui/hooks/useFormFields";

import "./App.css";
import Button from "./ui/components/Button/Button";
import LoadingIndicator from "./ui/components/LoadingIndicator/LoadingIndicator";

function App() {
/**
Expand All @@ -20,34 +23,26 @@ 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, clearFormFields } =
useFormFields(initialValues);
/**
* Results states
*/
const [error, setError] = React.useState(undefined);
const [addresses, setAddresses] = React.useState([]);
const [isLoading, setIsLoading] = React.useState(false);
/**
* Redux actions
*/
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();

Expand All @@ -58,23 +53,56 @@ 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 houseNumber = e.target.elements.houseNumber.value;
setIsLoading(true);
const res = await fetch(
`http://api.postcodedata.nl/v1/postcode/?postcode=${zipCode}&streetnumber=${houseNumber}&ref=domeinnaam&type=json`
);
const data = await res.json();
setIsLoading(false);
if (data.status === "error") return setError(data.errormessage);
// remove any potential previous errors if current request has no errors
setError(undefined);

const address = transformAddress({ ...data.details[0], houseNumber });

if (
addresses.some(
(entry) =>
entry.postcode === address.postcode &&
entry.houseNumber === address.houseNumber
)
)
return setError("House already added.");

setAddresses((prevAddresses) => [...prevAddresses, address]);
};

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"
);
return;
}

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,
});
};

const handleClearForm = () => {
clearFormFields();
setAddresses([]);
};

return (
Expand All @@ -88,71 +116,48 @@ function App() {
</small>
</h1>
{/* TODO: Create generic <Form /> component to display form rows, legend and a submit button */}
<form onSubmit={handleAddressSubmit}>
<fieldset>
<legend>🏠 Find an address</legend>
<div className="form-row">
<InputText
name="zipCode"
onChange={handleZipCodeChange}
placeholder="Zip Code"
value={zipCode}
/>
</div>
<div className="form-row">
<InputText
name="houseNumber"
onChange={handleHouseNumberChange}
value={houseNumber}
placeholder="House number"
/>
</div>
<Button type="submit">Find</Button>
</fieldset>
</form>
<Form
initialValues={values}
formFieldNames={["zipCode", "houseNumber"]}
caption="🏠 Find an address"
buttonTitle={"Find"}
onSubmit={handleAddressSubmit}
onChange={handleChange}
/>
{addresses.length > 0 &&
addresses.map((address) => {
return (
<Radio
name="selectedAddress"
id={address.id}
key={address.id}
onChange={handleSelectedAddressChange}
onChange={handleChange}
>
<Address address={address} />
</Radio>
);
})}
{/* TODO: Create generic <Form /> component to display form rows, legend and a submit button */}
{selectedAddress && (
<form onSubmit={handlePersonSubmit}>
<fieldset>
<legend>✏️ Add personal info to address</legend>
<div className="form-row">
<InputText
name="firstName"
placeholder="First name"
onChange={handleFirstNameChange}
value={firstName}
/>
</div>
<div className="form-row">
<InputText
name="lastName"
placeholder="Last name"
onChange={handleLastNameChange}
value={lastName}
/>
</div>
<Button type="submit">Add to addressbook</Button>
</fieldset>
</form>
{values.selectedAddress && (
<Form
initialValues={values}
formFieldNames={["firstName", "lastName"]}
caption="✏️ Add personal info to address"
buttonTitle={"Add to addressbook"}
onSubmit={handlePersonSubmit}
onChange={handleChange}
/>
)}

{/* TODO: Create an <ErrorMessage /> component for displaying an error message */}
{error && <div className="error">{error}</div>}
{error && <ErrorMessage error={error} />}

<LoadingIndicator isLoading={isLoading} />

{/* TODO: Add a button to clear all form fields. Button must look different from the default primary button, see design. */}
<Button variant="secondary" onClick={handleClearForm}>
Clear all fields
</Button>
</Section>

<Section variant="dark">
Expand Down
16 changes: 14 additions & 2 deletions src/core/reducers/addressBook.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Expand Down
4 changes: 3 additions & 1 deletion src/ui/components/Button/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ const Button = ({
type = "button",
variant = "primary", // or 'secondary'
}) => {
const classNames = `${$.button} ${variant === "primary" ? $.primary : $.secondary }`;

return (
<button
// TODO: Add conditional classNames
// - Must have a condition to set the '.primary' className
// - Must have a condition to set the '.secondary' className
className={$.button}
className={classNames}
type={type}
onClick={onClick}
>
Expand Down
12 changes: 12 additions & 0 deletions src/ui/components/Button/Button.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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-brand);
}

.secondary {
color: var(--color-brand);
background-color: var(--color-background);
border-color: var(--color-brand);
}
11 changes: 11 additions & 0 deletions src/ui/components/ErrorMessage/ErrorMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from "react";

import $ from "./ErrorMessage.module.css";

const ErrorMessage = ({ error }) => {
return (
<div className={$.error}>{error}</div>
);
};

export default ErrorMessage;
4 changes: 4 additions & 0 deletions src/ui/components/ErrorMessage/ErrorMessage.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.error {
color: red;
padding: 0.5em 0;
}
Loading