Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Branch1 #48

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions INTRODUCTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Upgrade Challenge

## Introduction

Hi, my name is Elvis Goncalves and this is my version of the Upgrade Challenge.

For the sake of UI simplicity i decided to start it off with bootstrap and react-strap but have also added some custom sass. You will see that i customized the look of the checkbox on step 2.

I also took the liberty to give the application a look and feel that is somewhat similar to the Upgrade.com site. I'm using the same logo and fonts.

### Note: My local server runs on port 5173, therefore i had to update the server/index.js file to support API calls from this port.
### URL for app: http://localhost:5173/

## Libraries

As you will see in the package.json file i added a few libraries to help me build this application:

- "bootstrap": For general UI layout
- "reactstrap": For the Modal component
- "react-redux": For the application state management
- "@reduxjs/toolkit": To create a slice and actions for each reducer function
- "react-router-dom": To define the different application routes
- "react-hook-form": For form validations
- "classnames": For conditionally joining classNames together
- "react-overlay-loader": For loading image during API calls
- "sass": For SASS support

## General Approach

Here are the high level steps that i took to build this app:

- Implemented the different routes for each step of the app.
- Configured a redux store and made it available to the App component.
- Used the redux store for the User information, available colors and the loading feature on the API calls.
- Implemented form validations on the steps that required user input with the help of react-hook-forms.
- Once the form is valid, the user can move on to the next step. The forms data is dispatched to the redux store.
- As the user moves through the first 2 steps, i persisted the User and Colors state in the localStorage in order not to lose the information on page refresh.
- Once the colors are fetched from the API they are persisted in the localStorage in order not to make that same API call again.
- If the user enters "Error" as a first name, the server will return an error and will redirect to the error page.
- I implemented an error 404 page in case a route does not exist.

### Unit Testing

I added some test in the App.test.jsx file to test if each of the pages can load properly.

I also added a SignUp.test.jsx to trigger the form validations and see if it would display the error messages.
4 changes: 4 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
/>
<meta name="theme-color" content="#000000" />
<link rel="shortcut icon" href="/favicon.ico" />
<link href="https://static.upgrade.com/assets/fonts/Graphik-Regular-Web.woff2" as="font" type="font/woff2" crossorigin="true">
<link href="https://static.upgrade.com/assets/fonts/Graphik-Black-Web.woff2" as="font" type="font/woff2" crossorigin="true">
<link href="https://static.upgrade.com/assets/fonts/Graphik-Medium-Web.woff2" as="font" type="font/woff2" crossorigin="true">
<link href="https://static.upgrade.com/assets/fonts/Graphik-Light-Web.woff2" as="font" type="font/woff2" crossorigin="true">
<title>Upgrade challenge</title>
</head>
<body>
Expand Down
14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,21 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^1.9.5",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"bootstrap": "^5.3.0",
"classnames": "^2.3.2",
"express": "^4.18.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-hook-form": "^7.45.2",
"react-overlay-loader": "^0.0.3",
"react-redux": "^8.1.1",
"react-router-dom": "^6.14.2",
"reactstrap": "^9.2.0",
"sass": "^1.64.1"
},
"devDependencies": {
"@babel/preset-env": "^7.22.9",
Expand Down
2 changes: 1 addition & 1 deletion server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const express = require('express');
const app = express();

app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "http://localhost:3000");
res.header("Access-Control-Allow-Origin", "http://localhost:5173");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
Expand Down
39 changes: 27 additions & 12 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
import React, { Component } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Layout from "./pages/Layout";
import SignUp from "./pages/SignUp";
import AdditionalInfo from "./pages/AdditionalInfo";
import Confirmation from "./pages/Confirmation";
import SuccessMessage from "./pages/SuccessMessage";
import ErrorMessage from "./pages/ErrorMessage";
import Page404 from "./pages/Page404";

const App = () => {
return (
<div>
<header>
<h1>Welcome to Upgrade challenge</h1>
</header>
<p>
To get started, edit <code>src/App.jsx</code> and save to reload.
</p>
</div>
);
};
class App extends Component {

render() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<SignUp />} />
<Route path="more-info" element={<AdditionalInfo />} />
<Route path="confirmation" element={<Confirmation />} />
<Route path="success" element={<SuccessMessage />} />
<Route path="error" element={<ErrorMessage />} />
<Route path="*" element={<Page404 />} />
</Route>
</Routes>
</BrowserRouter>
);
}
}

export default App;
64 changes: 56 additions & 8 deletions src/App.test.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,60 @@
import React from "react";
import ReactDOM from "react-dom";
import { createRoot } from "react-dom/client";
import App from "./App";
import React from 'react';
import ReactDOM from "react-dom/client";
import App from './App';
import { Provider } from 'react-redux'
import store from './store'
import SignUp from './pages/SignUp';
import AdditionalInfo from './pages/AdditionalInfo';
import Confirmation from './pages/Confirmation';
import SuccessMessage from './pages/SuccessMessage';
import ErrorMessage from './pages/ErrorMessage';
import Page404 from './pages/Page404';

it("renders without crashing", () => {
const div = document.createElement("div");
const root = createRoot(div);
it('App renders without crashing', () => {
const div = document.createElement('div');
const root = ReactDOM.createRoot(div);
root.render(<Provider store={store}><App /></Provider>);
root.unmount(div);
});

it('SignUp renders without crashing', () => {
const div = document.createElement('div');
const root = ReactDOM.createRoot(div);
root.render(<Provider store={store}><SignUp /></Provider>);
root.unmount(div);
});

it('AdditionalInfo renders without crashing', () => {
const div = document.createElement('div');
const root = ReactDOM.createRoot(div);
root.render(<Provider store={store}><AdditionalInfo /></Provider>);
root.unmount(div);
});

it('Confirmation renders without crashing', () => {
const div = document.createElement('div');
const root = ReactDOM.createRoot(div);
root.render(<Provider store={store}><Confirmation /></Provider>);
root.unmount(div);
});

it('SuccessMessage renders without crashing', () => {
const div = document.createElement('div');
const root = ReactDOM.createRoot(div);
root.render(<Provider store={store}><SuccessMessage /></Provider>);
root.unmount(div);
});

it('ErrorMessage renders without crashing', () => {
const div = document.createElement('div');
const root = ReactDOM.createRoot(div);
root.render(<Provider store={store}><ErrorMessage /></Provider>);
root.unmount(div);
});

root.render(<App />);
it('Pagge404 renders without crashing', () => {
const div = document.createElement('div');
const root = ReactDOM.createRoot(div);
root.render(<Provider store={store}><Page404 /></Provider>);
root.unmount(div);
});
13 changes: 11 additions & 2 deletions src/index.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux'
import store from './store'

ReactDOM.render(<App />, document.getElementById('root'));
import './scss/custom.scss';

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}><App /></Provider>
</React.StrictMode>
);
131 changes: 131 additions & 0 deletions src/pages/AdditionalInfo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**********************************************************************************/
/* AdditionalInfo.jsx:
/* Second step in the application.
/* @author: Elvis Goncalves
/**********************************************************************************/

import React, { useEffect, useState } from 'react'
import { Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import { useDispatch, useSelector } from 'react-redux'
import { useForm } from "react-hook-form";
import { setColors, setLoading, setUser } from '../slices/appSlice'
import classNames from "classnames";

export default function AdditionalInfo() {

// Change in branch1

const dispatch = useDispatch();

// Get the user state from the redux store
const user = useSelector(state => state.appReducer.user);

// Initialize the react-hook-form methods. Will be used to validate the form.
const { register, handleSubmit, watch, formState: { errors } } = useForm({
defaultValues: {
favoriteColor: user.color
}
});

// Get the colors state from the redux store. We could've kept it as a local state
// but i wanted to keep all the localStorage interaction in the slice.
const colors = useSelector(state => state.appReducer.colors);

// Initialize the state variables
const [modal, setModal] = useState(false);

// To toggle the terms and condition modal
const toggleTermsConditions = () => setModal(!modal);

const handleBack = (formData) => {
// Dispatch the user information to the redux store via the setUser action to set the favorite color and terms.
dispatch(setUser({...user, color: formData.favoriteColor, terms: formData.terms}));
// Redirect to first step.
window.location.href = "/";
}

const submitForm = (formData) => {
// Dispatch the user information to the redux store via the setUser action to set the favorite color and terms.
dispatch(setUser({...user, color: formData.favoriteColor, terms: formData.terms}));
// Redirect to confirmation step.
window.location.href = "/confirmation";
};

/**********************************************************************************/
/* Defining the useEffect hook to fetch the color list
/**********************************************************************************/
useEffect(() => {
// Do not call the API if we already have the color list.
if (colors.length === 0) {
dispatch(setLoading(true));

// Fetch the colors from the API call.
fetch("http://localhost:3001/api/colors", {
redirect: 'follow',
method: 'GET'
})
.then(response => {
return response.json()
})
.then(data => {
dispatch(setColors(data));
dispatch(setLoading(false));
});
}
}, []);

return (
<>
<header>
<h1 className="text-center">Additional Info</h1>
</header>
<form onSubmit={handleSubmit(submitForm)} noValidate autoComplete="off" className="mt-4">
<div className="form-group my-3">
<div className="position-relative">
<select className={classNames({"form-select form-select-lg text-capitalize mb-1": true, "error": errors.favoriteColor, "has-data": watch("favoriteColor")?.length > 0})}
{...register("favoriteColor", { required: true })} defaultValue={user.color}>
<option hidden="hidden" value=""></option>
{colors.map((color, index) => {
return <option key={"color-" + index} className="text-capitalize" value={color}>{color}</option>
})}
</select>
<span className="placeholder-label">Select your favorite color</span>
</div>
{errors.favoriteColor && <p className="text-danger">The Favorite Color field is required</p>}
</div>
<div className="form-group position-relative my-3">
<div className="checkbox">
<label>
<input type="checkbox"
defaultChecked={user.terms}
{...register("terms", { required: true })} />
<span className={classNames({"checkbox": true, "error": errors.terms})}></span>
<span className="d-flex align-items-center">
I agree to the <a href="#" className="ps-1" onClick={toggleTermsConditions}>terms and conditions</a>
</span>
</label>
</div>
{errors.terms && <p className="text-danger">You must agree to the Terms and Conditions.</p>}
</div>
<div className="d-flex justify-content-end mt-4">
<button type="button" className="btn btn-secondary btn-lg me-3" onClick={handleSubmit(handleBack)}>Back</button>
<button type="submit" className="btn btn-success btn-lg">Next</button>
</div>
</form>
<Modal isOpen={modal} toggle={toggleTermsConditions}>
<ModalHeader toggle={toggleTermsConditions}>
Terms and Conditions
</ModalHeader>
<ModalBody>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
</ModalBody>
<ModalFooter>
<button type="button" className="btn btn-success btn-lg" onClick={toggleTermsConditions}>
Close
</button>
</ModalFooter>
</Modal>
</>
);
};
Loading