Project Notes by James Priest
Live Demo | GitHub Repo |
This site contains code notes for how I built project 2 of my Udacity React Nanodegree Program.
The assignment is to build an app called “Would You Rather”.
The “Would You Rather” app is a game that presents the user with a poll in the form of:
Would you rather…
- Option A
- Option B
The user will be able to answer the question and upon answering will be able to see the polling results.
The app will allow users to
This project will solidify your understanding of React and Redux while giving you a chance to express your creativity. You’ll practice
The starter code will consist of a _DATA.js
file, which represents a fake database and contains methods that let you access the data. The README file outlines how the data is stored and details the methods you can use to access the database.
The only thing you need to edit in the _DATA.js
file is the value of avatarURL
. Each user should have an avatar, so you’ll need to add a path to each user’s avatar.
Here is the data provided in _DATA.js
.
// _DATA.js
let users = {
sarahedo: {
id: 'sarahedo',
name: 'Sarah Edo',
avatarURL: ,
answers: {
"8xf0y6ziyjabvozdd253nd": 'optionOne',
"6ni6ok3ym7mf1p33lnez": 'optionTwo',
"am8ehyc8byjqgar0jgpub9": 'optionTwo',
"loxhs1bqm25b708cmbf3g": 'optionTwo'
},
questions: ['8xf0y6ziyjabvozdd253nd', 'am8ehyc8byjqgar0jgpub9']
},
tylermcginnis: {
id: 'tylermcginnis',
name: 'Tyler McGinnis',
avatarURL: ,
answers: {
"vthrdm985a262al8qx3do": 'optionOne',
"xj352vofupe1dqz9emx13r": 'optionTwo',
},
questions: ['loxhs1bqm25b708cmbf3g', 'vthrdm985a262al8qx3do'],
},
johndoe: {
id: 'johndoe',
name: 'John Doe',
avatarURL: ,
answers: {
"xj352vofupe1dqz9emx13r": 'optionOne',
"vthrdm985a262al8qx3do": 'optionTwo',
"6ni6ok3ym7mf1p33lnez": 'optionTwo'
},
questions: ['6ni6ok3ym7mf1p33lnez', 'xj352vofupe1dqz9emx13r'],
}
}
let questions = {
"8xf0y6ziyjabvozdd253nd": {
id: '8xf0y6ziyjabvozdd253nd',
author: 'sarahedo',
timestamp: 1467166872634,
optionOne: {
votes: ['sarahedo'],
text: 'have horrible short term memory',
},
optionTwo: {
votes: [],
text: 'have horrible long term memory'
}
},
"6ni6ok3ym7mf1p33lnez": {
id: '6ni6ok3ym7mf1p33lnez',
author: 'johndoe',
timestamp: 1468479767190,
optionOne: {
votes: [],
text: 'become a superhero',
},
optionTwo: {
votes: ['johndoe', 'sarahedo'],
text: 'become a supervillain'
}
},
"am8ehyc8byjqgar0jgpub9": {
id: 'am8ehyc8byjqgar0jgpub9',
author: 'sarahedo',
timestamp: 1488579767190,
optionOne: {
votes: [],
text: 'be telekinetic',
},
optionTwo: {
votes: ['sarahedo'],
text: 'be telepathic'
}
},
"loxhs1bqm25b708cmbf3g": {
id: 'loxhs1bqm25b708cmbf3g',
author: 'tylermcginnis',
timestamp: 1482579767190,
optionOne: {
votes: [],
text: 'be a front-end developer',
},
optionTwo: {
votes: ['sarahedo'],
text: 'be a back-end developer'
}
},
"vthrdm985a262al8qx3do": {
id: 'vthrdm985a262al8qx3do',
author: 'tylermcginnis',
timestamp: 1489579767190,
optionOne: {
votes: ['tylermcginnis'],
text: 'find $50 yourself',
},
optionTwo: {
votes: ['johndoe'],
text: 'have your best friend find $500'
}
},
"xj352vofupe1dqz9emx13r": {
id: 'xj352vofupe1dqz9emx13r',
author: 'johndoe',
timestamp: 1493579767190,
optionOne: {
votes: ['johndoe'],
text: 'write JavaScript',
},
optionTwo: {
votes: ['tylermcginnis'],
text: 'write Swift'
}
},
}
Here are the existing database methods which we’ll want to create an API wrapper around.
// _DATA.js
function generateUID () {
return Math.random().toString(36).substring(2, 15) + Math.random()
.toString(36).substring(2, 15)
}
export function _getUsers () {
return new Promise((res, rej) => {
setTimeout(() => res({...users}), 1000)
})
}
export function _getQuestions () {
return new Promise((res, rej) => {
setTimeout(() => res({...questions}), 1000)
})
}
function formatQuestion ({ optionOneText, optionTwoText, author }) {
return {
id: generateUID(),
timestamp: Date.now(),
author,
optionOne: {
votes: [],
text: optionOneText,
},
optionTwo: {
votes: [],
text: optionTwoText,
}
}
}
export function _saveQuestion (question) {
return new Promise((res, rej) => {
const authedUser = question.author;
const formattedQuestion = formatQuestion(question);
setTimeout(() => {
questions = {
...questions,
[formattedQuestion.id]: formattedQuestion
}
users = {
...users,
[authedUser]: {
...users[authedUser],
questions: users[authedUser].questions.concat([formattedQuestion.id])
}
}
res(formattedQuestion)
}, 1000)
})
}
export function _saveQuestionAnswer ({ authedUser, qid, answer }) {
return new Promise((res, rej) => {
setTimeout(() => {
users = {
...users,
[authedUser]: {
...users[authedUser],
answers: {
...users[authedUser].answers,
[qid]: answer
}
}
}
questions = {
...questions,
[qid]: {
...questions[qid],
[answer]: {
...questions[qid][answer],
votes: questions[qid][answer].votes.concat([authedUser])
}
}
}
res()
}, 500)
})
}
Using the provided starter code, you will build a React/Redux front end for the application.
The person using your application should have a way of impersonating/logging in as an existing user. (This could be as simple as having a login box that appears at the root of the application that lets the user select a name from the list of existing users. Alternatively, you could create your own account creation process to allow a user to sign up for an account.) Your application should work correctly regardless of which user is selected. Once the user logs in, the home page should be shown.
We always want to make sure we know who the logged in user is, so information about the logged in user should appear on the page. If someone tries to navigate anywhere by entering the address in the address bar, the user is asked to sign in and then the requested page is shown. The application allows the user to log out and log back in.
Once the user logs in, the user should be able to toggle between his/her answered and unanswered polls on the home page, which is located at the root. The polls in both categories are arranged from the most recently created (top) to the least recently created (bottom). The unanswered questions should be shown by default, and the name of the logged in user should be visible on the page.
What would be the point of seeing answered and unanswered polling questions if we couldn’t actually vote or see the results? Each polling question should link to the details of that poll. The details of each poll should be available at questions/:question_id
.
When a poll is clicked on the home page, the following is shown:
For answered polls, each of the two answer options contains the following:
The option selected by the logged-in user should be clearly marked.
Since we want to make sure our application creates a good user experience, the application should show a 404 page if the user is trying to access a poll that does not exist. (Please keep in mind that newly created polls will not be accessible at their url because of the way the backend is set up in this application.) It should also display a navigation bar so that the user can easily navigate anywhere in the application.
So what happens when someone votes in a poll? Upon voting in a poll, all of the information of an answered poll should be displayed. The user’s response should be recorded and clearly visible on the poll details page. Users can only vote once per poll; they shouldn’t be allowed to change their answer after they’ve voted – no cheating allowed! When the user comes back to the home page, the polling question should appear in the “Answered” column.
It would be no fun to vote in polls if we couldn’t post our own questions! The form for posting new polling questions should be available at the /add
route. The application should show the text “Would You Rather” and have a form for creating two options. Upon submitting the form, a new poll should be created, the user should be taken to the home page, and the new polling question should appear in the correct category on the home page.
But how can we know how many questions each user has asked and answered? Let’s get some healthy competition going here! The application should have a leaderboard that’s available at the /leaderboard
route. Each entry on the leaderboard should contain the following:
Users should be ordered in descending order based on the sum of the number of questions they’ve asked and the number of questions they’ve answered. The more questions you ask and answer, the higher up you move.
The user should be able to navigate to the leaderboard, to a specific question, and to the form that allows the user to create a new poll both from within the app and by typing in the address into the address bar. To make sure we’re showing the data that is relevant to the user, the application should require the user to be signed in order to access those pages.
By walking through the Planning Stage and the Coding Stage of the Chirper Project, we’ve given you a useful template for building Redux applications. We recommend using this template for building your “Would You Rather?” Project. Remember that planning your project and its architecture before starting to code will save you a lot of debugging time later on!
For this application, most of the application’s state should be managed by Redux. You’ll find that there are situations where it makes sense to store state outside of the Redux store. Check out what Dan Abramov, the creator of Redux, thinks about choosing between Redux’s store and React’s state.
Your application’s store should be the source of truth, and components should read the necessary state from the store instead of having their own versions of the same state. There should be no direct API calls in components’ lifecycle methods, and updates should be triggered by dispatching action creators.
Your application’s code should be structured and organized in a logical way, and your components should be modular and reusable.
1) Use React to build your application’s UI. Remember that composition is key. It’s rarely a mistake to break a component into smaller pieces. Look for opportunities to reuse your components.
2) We recommend using Create React App to generate your submission since it’s the easiest way to ensure you have everything the project reviewer will need to install and run your app.
3) By walking through the Planning Stage and the Coding Stage of the Chirper Project, we’ve given you a useful template for building Redux applications. We recommend using this template for building your “Would You Rather?” Project. Remember that planning your project and its architecture before starting to code will save you a lot of debugging time later on!
4) Use Redux to manage your application state. For this application, most of the application’s state should be managed by Redux. You may use component state to handle form input fields and controlled components. Otherwise, the rest of the state for your application should be controlled by your reducers.
5) While the focus (and specification) of this project is based on functionality rather than styling, please ensure that your app is presentable and easy to navigate.
6) Please carefully test your app against the rubric to make sure all of the rubric requirements are met. Your project must meet all of the rubric criteria in order to pass.
We encourage you to get creative with your projects by adding additional functionality and/or styling (after you’ve ensured that the project meets all of the specifications in the rubric)!
Here’s a set of screenshots that show a sample project.
We need to determine the look and functionality of each view in your app. One of the best approaches is to draw each view of the app on paper so that you’ll have a good idea of what information and data you’re planning to have on each page.
Instead of paper and pencil, you can be a bit more digital and use software for creating mockups. If you were given project specifications, check your mock against them to make sure that you have all of the required features.
For this step,
We need to take a look at what is happening in each component. Let’s determine what actions the app or the user is performing on the data. Is the data being set, modified, or deleted?…then we’ll need an action to keep track of that event!
Remember that the main problems that Redux (and the react-redux bindings!) was meant to solve were:
According to Dan Abramov, the creator of Redux, we should follow the following principle for determining whether to store a piece of data in the store or in a React component:
“Use Redux for state that matters globally or is mutated in complex ways… The rule of thumb is: do whatever is less awkward.”
For further information about this take a look at
Step 1 - Design the shape of the state and create reducers.
Step 2 - Create a Redux store. Connect logger middleware (optional) and Redux Thunk middleware (alternatively, you can use Redux Saga, etc.).
Step 3 - For each view that needs access to the store, create the component and connect it to the store.
Step 4 - For the component you created in the last step, create actions and action creators. Check that everything works correctly.
Step 5 - Repeat Step 3 & Step 4 for each component that needs access to the store.
Step 6 - Create presentational components and confirm that everything works correctly.
Step 7 - Add React Router.
Step 8 - Add finishing touches and make sure the project meets the rubric.
Remember, this is just a template. As you build more projects, you’ll modify this template to suit your needs. You may also find it more intuitive to use a different approach. Regardless of the approach you take, however, planning out your app is imperative to success.
This stage of the process consists of drawing or mocking up each of the app’s views.
I started with pencil and paper and then began looking for a React UI library to accommodate the look and feel.
I settled on Sematic UI React. It comes pre-baked with many of the UI layouts and controls necessary to meet the requirements.
Additionally, the docs are awesome. They have samples and CodeSandbox instances embedded in every page for easy testing and modification.
I played around with various components from the Semantic-UI-React library and tested how involved the implementation was going to be. It turned out to be pretty straight-forward.
Here I tested a handful of random components that you can see from the import list.
I also did a second test to see what the menu and nav system looked like. I wanted to make sure these were responsive and adjusted well on mobile.
I had to bump up the font size a bit but overall the components scaled well.
One of the things that Semantic UI React has is a Responsive control that can be used to show a different UI based on min-width
or max-width
.
I created three breakpoint width for the navigation menu to use. Here’s the UI.
For the visual design stage I took a pencil to paper in order to draw out a rough sketch of each View. This showed me that I had seven views to create.
From there I decided to go straight into building my mockups using Semantic-UI-React in order to get up to speed on the UI framework.
This would give me a chance to see which layouts, controls, and components work best to display the data.
Live Demo: Would You Rather@4-ui-mockups on CodeSandbox
I split out the navigation into a Nav component that is responsive and scales well on mobile devices.
Live Demo: Would You Rather@5-mockup-nav on CodeSandbox
This next mockup employs a Tab component and uses a hard-coded object structure to mimic data from the database.
I also an using composition to break these components into smaller chunks
Here’s are two screenshots of the working mockup - one shows the Unanswered Questions tab and the other shows the Answered Questions tab.
Live Demo: Would You Rather@6-mockup-home on CodeSandbox
The next thing I did was to build out a framework for the app so I could click each of the menu items and navigate to every page. This required adding the following.
This component imports the following
State is used to manage a redirect to the login page and well as whether to show a poll’s question or results based on whether the question has been answered or not.
Live Demo: Would You Rather@7-mockup-framework on CodeSandbox
The next set of changes happens in PollContainer. This component is responsible for rendering the poll question or poll result based on a showResult
flag we pass down.
Live Demo: Would You Rather@8-mockup-poll-question on CodeSandbox
The next step was to add in the PollResult.js component.
Live Demo: Would You Rather@9-mockup-poll-result on CodeSandbox
The New Poll page allows you to create a poll.
For this mockup I implemented the following.
I used state to manage the form which makes this a controlled component.
Here are screenshots of the form in different states.
New Poll - Loader on Submission
Live Demo: Would You Rather@10-mockup-new-poll on CodeSandbox
The leaderboard shows the top three users ranked by score. Score is determined by number of questions answered plus number of questions asked for a total score.
Here are the elements of the mockup
Here’s a screenshot of the leaderboard.
Live Demo: Would You Rather@11-mockup-leaderboard on CodeSandbox
The last mockup is of the Login screen. This needs to do the following.
Here are screenshots of the login page.
Live Demo: Would You Rather@12-mockup-login on CodeSandbox
Now that we have mockups created I can list those here along with the requirements for each application view.
Login View Requirements:
Home View Requirements:
/
) and shown the Home viewquestions/:question_id
Poll Question View Requirements:
When someone votes in a poll:
Poll Result View Requirements:
Poll 404 View Requirements:
New Poll View Requirements:
/add
routeLeaderboard View Requirements:
/leaderboard
routeThe views will now be used in creating the component hierarchy. This is done by drawing boxes around every component and sub-component in the mock and giving them names.
Determining what should be a component should follow the same rules for deciding if a new function or object should be created. It should follow the single responsibility principle. A component should ideally only do one thing.
Components used in the hierarchies that come from the Semantic-UI-React library will be identified with the acronym (SUIR).
This view is broken down into the following React components:
Here’s the master list of components according to how I broke things up. This does not include components from the Sematic-UI-React library, some of which were outlined above.
The component hierarchies tells us which components will be used inside of other components. It gives us the skeleton of our app.
One thing to bear in mind is that each of these components are presentational components.
Right now, we don’t care which components will be upgraded to containers. As we start building out the store, we’ll create additional components that will become container components.
Remember that container components get data from the store and pass it to the presentational components that need the data.
This is where we determine what events happen in the app.
We do this by taking a look at what can happen in each component. Specifically, what actions the app or user is performing on the data in each component.
Is the data being set, modified, or deleted? If so, we’ll need an action to keep track of that event.
We’ll do the following in the analysis of of each component:
When the app first loads we need to fill the store with data.
Action and data:
RECEIVE_USERS
RECEIVE_QUESTIONS
3.3.1.1 Preview of Redux steps
The first thing we need to do is fill the Redux store with all initial data when App loads. This is the
users
andquestions
data sets.Here are the steps to do so:
- Create
handleInitialData()
in ‘actions/shared.js’.- Make async call to data API receiving back both
users
andquestions
.- Then returned promise should
.then()
dispatch
getUsers(users)
action creatorgetQuestions(questions)
action creator- Invoke
handleInitialData()
from App’scomponentDidMount()
lifecycle event.
Data used to populate form.
Action and data:
RECEIVE_USERS
SET_AUTH_USER
Data used to populate tab panes.
The next step is to determine what data lives in the store and what should live in the component.
The main problems Redux (and react-redux bindings) was meant to solve were:
According to Dan Abramov we should use the following principle for determining whether to store a piece of data in the store or in a React component:
“Use Redux for state that matters globally or is mutated in complex ways… The rule of thumb is: do whatever is less awkward.”
See the following for more information on this.
For each piece of data from Step 3, let’s see whether it’s used by multiple components or mutated in a complex way.
Here’s the list of data elements we’ll be using in this app.
Next we indicate in which component each data element will be used.
Users | Questions | AuthUser | Text | Option | |
---|---|---|---|---|---|
App | x | ||||
Login | x | x | |||
Nav | x | x | |||
Home | x | ||||
UserCard | x | ||||
PollTeaser | x | ||||
PollQuestion | x | x | x | ||
PollAnswer | x | ||||
NewPoll | x | x | |||
Leaderboard | x | ||||
LeaderboardCard | x |
Users and questions are used throughout the app and is perfectly suited for the store. AuthUser is used globally and should also be part of the store.
Text and option data can be kept as component state.
Here’s the list of data elements we’ll be using in this app separated in by store or by component state.
The Redux documentation recommends normalizing our state according to the following normalization principles.
Our store will consist of the following data “tables”.
Now that the design phase is done I can jump into the coding phase with a clear roadmap of what components need to be built and how to build them.
The first step is to create a set of function wrappers to our async data requests. The async methods would be direct database fetch requests and include:
We’ll want to create the following API function wrappers for these.
getInitialData
is one spot we might use to normalize the state once we get the data back from the fetch request and prior to it populating the store.
This is located at /src/utils/api.js
.
// api.js
import {
_getUsers,
_getQuestions,
_saveQuestion,
_saveQuestionAnswer
} from './_DATA';
export function getInitialData() {
return Promise.all([_getUsers(), _getQuestions()]).then(
([users, questions]) => ({
users,
questions
})
);
}
export function saveQuestion(info) {
return _saveQuestion(info);
}
export function saveQuestionAnswer(info) {
return _saveQuestionAnswer(info);
}
The next step is to create a set of actions and action creators.
We start by creating the following files in /src/actions/
.
This is located at: /src/actions/authUser.js
.
// authUser.js
export const SET_AUTH_USER = 'SET_AUTH_USER';
export function setAuthUser(id) {
return {
type: SET_AUTH_USER,
id
};
}
This is located at: /src/actions/questions.js
.
// questions.js
export const RECEIVE_QUESTIONS = 'RECEIVE_QUESTIONS';
export function receiveQuestions(questions) {
return {
type: RECEIVE_QUESTIONS,
questions
};
}
This is located at: /src/actions/users.js
.
// user.js
export const RECEIVE_USERS = 'RECEIVE_USERS';
export function receiveUsers(users) {
return {
type: RECEIVE_USERS,
users
};
}
This is located at: /src/actions/shared.js
.
// shared.js
import { getInitialData } from '../utils/api';
import { receiveQuestions } from '../actions/questions';
import { receiveUsers } from '../actions/users';
export function handleInitialData() {
return dispatch => {
return getInitialData().then(({ users, questions }) => {
dispatch(receiveQuestions(questions));
dispatch(receiveUsers(users));
});
};
}
The next step is to create our reducers.
We start by creating the following files in /src/reducers/
;
This is located at /src/reducers/authUser.js
.
// authUser.js
import { SET_AUTH_USER } from '../actions/authUser';
export default function authUser(state = null, action) {
if (action.type === SET_AUTH_USER) {
return action.id;
}
return state;
}
This is located at /src/reducers/questions.js
.
// questions.js
import { RECEIVE_QUESTIONS } from '../actions/questions';
export default function questions(state = {}, action) {
switch (action.type) {
case RECEIVE_QUESTIONS:
return {
...state,
...action.questions
};
default:
return state;
}
}
This is located at /src/reducers/users.js
.
// users.js
import { RECEIVE_USERS } from '../actions/users';
export default function users(state = {}, action) {
switch (action.type) {
case RECEIVE_USERS:
return {
...state,
...action.users
};
default:
return state;
}
}
This is located at /src/reducers/index.js
.
What we do here is combine the reducers into one main root reducer which will combine the following into a single state object.
authUser
reducerquestions
reducerusers
reducerRemember, we need to do this because the createStore
function only accepts a single reducer.
// index.js
import { combineReducers } from 'redux';
import authUser from '../reducers/authUser';
import questions from '../reducers/questions';
import users from '../reducers/users';
export default combineReducers({
authUser,
questions,
users
});
This step instantiates the store and passes it to Provider which wraps App and acts as a Context.
This happens in /src/index.js
.
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import './index.css';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './reducers/index';
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
Now we can test to make sure that our Redux store objects are in place by opening Chrome DevTools and going to the React Developer Tools tab.
React Tools showing storeState
The next step is to create the middleware functions.
All middleware follows this currying pattern.
const logger = (store) => (next) => (action) => {
// ...
}
The first middleware function will be a logger that will output the following:
We start by creating the following files in /src/middleware/
.
This is located at /src/middleware/logger.js
.
// logger.js
const logger = store => next => action => {
console.group(action.type);
console.log('The action:', action);
const returnValue = next(action);
console.log('The new state: ', store.getState());
console.groupEnd();
return returnValue;
};
export default logger;
This is located at /src/middleware/index.js
.
// index.js
import thunk from 'redux-thunk';
import logger from './logger';
import { applyMiddleware } from 'redux';
export default applyMiddleware(thunk, logger);
One thing to note is that middleware gets run after the action creator returns an object or function but before getting sent to the reducer.
Middleware also gets run in the order we apply it. Thunk needs to be run first so that it can properly handle logger.
This is located at /src/index.js
.
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import './index.css';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './reducers/index';
import middleware from './middleware';
const store = createStore(rootReducer, middleware);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
The next step is to invoke our handleInitialData()
thunk action creator that was created in /src/actions/shared.js
.
Here’s what that code looks like.
// shared.js
import { getInitialData } from '../utils/api';
import { receiveQuestions } from '../actions/questions';
import { receiveUsers } from '../actions/users';
export function handleInitialData() {
return dispatch => {
return getInitialData().then(({ users, questions }) => {
dispatch(receiveQuestions(questions));
dispatch(receiveUsers(users));
});
};
}
It uses the thunk signature of function xyz() { return dispatch => {...} }
.
Inside it invokes our Promise-based getInitialData()
async request, then it dispatches the resulting data entities in order to fill our Redux store.
We should invoke this from App since this is the entry point to our application. Now in order to invoke this we need to expose the action as props by using ‘react-redux’ connect
method.
We do this in /src/components/App.js
.
// App.js
import React, { Component } from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { Grid } from 'semantic-ui-react';
import { handleInitialData } from '../actions/shared'; // <- new
import { connect } from 'react-redux'; // <- new
class App extends Component {
componentDidMount() { // <- new
this.props.handleInitialData(); // <- new
} // <- new
render() {
return (
<Router>
<div className="App">
<ContentGrid>
<p>New Start...</p>
</ContentGrid>
</div>
</Router>
);
}
}
const ContentGrid = ({ children }) => (
<Grid padded="vertically" columns={1} centered>
<Grid.Row>
<Grid.Column style={{ maxWidth: 550 }}>{children}</Grid.Column>
</Grid.Row>
</Grid>
);
export default connect( // <- new
null, // <- new
{ handleInitialData } // <- new
)(App); // <- new
When we run the app we can now see the that it is wrapped by the connect component.
React Tools showing storeState
We can also see our logger is working properly because it outputs both the RECEIVE_QUESTIONS and RECEIVE_USERS actions.
DevTools Console with Logger output
Next we’ll work on the login and navigation components. These go hand-in-hand to authenticate the user in the login component and then display the authenticated user in the navigation component.
First we need to modify App to render the Login component if the user has not been authenticated yet. This is done in /src/components/App.js
.
// App.js
import React, { Component, Fragment } from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import { Grid } from 'semantic-ui-react';
import { handleInitialData } from '../actions/shared';
import { connect } from 'react-redux';
import Login from './Login';
import Nav from './Nav';
import Home from './Home';
class App extends Component {
componentDidMount() {
this.props.handleInitialData();
}
render() {
const { authUser } = this.props;
return (
<Router>
<div className="App">
{authUser === null ? (
<Route
render={() => (
<ContentGrid>
<Login />
</ContentGrid>
)}
/>
) : (
<Fragment>
<Nav />
<ContentGrid>
<Route exact path="/" component={Home} />
</ContentGrid>
</Fragment>
)}
</div>
</Router>
);
}
}
const ContentGrid = ({ children }) => (
<Grid padded="vertically" columns={1} centered>
<Grid.Row>
<Grid.Column style={{ maxWidth: 550 }}>{children}</Grid.Column>
</Grid.Row>
</Grid>
);
function mapStateToProps({ authUser }) {
return {
authUser
};
}
export default connect(
mapStateToProps,
{ handleInitialData }
)(App);
We use mapStateToProps
to get the store’s authUser
state.
Next we update the Login component to do the following:
users
to populate Login Form dropdown componentWe do this in the Login component which is located at /src/components/Login.js
.
The code in ‘Login.js’ is split into multiple components following this hierarchy:
// Login.js
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
Segment,
Grid,
Header,
Image,
Form,
Loader,
Dimmer
} from 'semantic-ui-react';
import { setAuthUser } from '../actions/authUser';
export class Login extends Component {
state = {
loading: false
};
handleLoading = () => {
this.setState({ loading: true });
};
render() {
return (
<Fragment>
<Segment.Group>
<LoginHeader />
<LoginGridLayout
image={<BrandImage />}
form={<ConnectedLoginForm onLoading={this.handleLoading} />}
loading={this.state.loading}
/>
</Segment.Group>
<footer className="footer">
<a href="https://www.freepik.com/free-photos-vectors/design">
Avatar characters created by freepik - www.freepik.com
</a>
</footer>
</Fragment>
);
}
}
const LoginHeader = () => (
<Header as="h4" block attached="top" textAlign="center">
<Header.Content>Welcome to the Would You Rather App!</Header.Content>
<Header.Subheader>Please sign in to continue</Header.Subheader>
</Header>
);
const LoginGridLayout = ({ image, form, loading }) => (
<div>
<Grid padded textAlign="center">
<Grid.Row className="login">
<Grid.Column width={16}>
{loading === true && (
<Dimmer active inverted>
<Loader inverted content="Loading" />
</Dimmer>
)}
{image}
<br />
{form}
</Grid.Column>
</Grid.Row>
</Grid>
</div>
);
const BrandImage = () => (
<Image src="/images/avatars/animals.png" size="medium" centered />
);
class LoginForm extends Component {
static propTypes = {
onLoading: PropTypes.func.isRequired
};
state = {
value: ''
};
onChange = (e, { value }) => {
this.setState({ value });
};
handleSubmit = e => {
e.preventDefault();
const { onLoading, setAuthUser } = this.props;
const authUser = this.state.value;
new Promise((res, rej) => {
onLoading();
setTimeout(() => res(), 500);
}).then(() => setAuthUser(authUser));
};
generateDropdownData = () => {
const { users } = this.props;
return users.map(user => ({
key: user.id,
text: user.name,
value: user.id,
image: { avatar: true, src: user.avatarURL }
}));
};
render() {
const { value } = this.state;
const disabled = value === '' ? true : false;
return (
<Form onSubmit={this.handleSubmit}>
<Header as="h2" color="green">
Sign In
</Header>
<Form.Dropdown
placeholder="Select a Friend"
fluid
selection
scrolling
options={this.generateDropdownData()}
value={value}
onChange={this.onChange}
required
/>
<Form.Button content="Login" positive disabled={disabled} fluid />
</Form>
);
}
}
const ConnectedLoginForm = connect(
mapStateToProps,
{ setAuthUser }
)(LoginForm);
function mapStateToProps({ users }) {
return {
users: Object.values(users)
};
}
export default Login;
The nav component will display who is logged in. It is located in /src/components/Nav.js
.
// Nav.js
import React, { Component, Fragment } from 'react';
import { NavLink } from 'react-router-dom';
import { connect } from 'react-redux';
import {
Menu,
Responsive,
Image,
Grid,
Button,
Container
} from 'semantic-ui-react';
import { setAuthUser } from '../actions/authUser';
class Nav extends Component {
handleLogout = e => {
e.preventDefault();
this.props.setAuthUser(null);
};
render() {
const { authUser, users } = this.props;
return (
<Container>
<Responsive as={Menu} minWidth={651} pointing secondary>
<Menu.Item name="home" as={NavLink} to="/" exact />
<Menu.Item name="new poll" as={NavLink} to="/add" />
<Menu.Item name="leader board" as={NavLink} to="/leaderboard" />
<Menu.Menu position="right">
<Menu.Item>
<span>
<Image
src={users[authUser].avatarURL}
avatar
spaced="right"
verticalAlign="bottom"
/>
{users[authUser].name}
</span>
</Menu.Item>
<Menu.Item>
<Button
content="Logout"
labelPosition="right"
basic
compact
icon="log out"
size="mini"
onClick={this.handleLogout}
/>
</Menu.Item>
</Menu.Menu>
</Responsive>
<Responsive as={Fragment} minWidth={375} maxWidth={650}>
...
</Responsive>
<Responsive as={Fragment} maxWidth={374}>
...
</Responsive>
</Container>
);
}
}
function mapStateToProps({ users, authUser }) {
return {
authUser,
users
};
}
export default connect(
mapStateToProps,
{ setAuthUser }
)(Nav);
The Login component displays all users in the dropdown which is now coming from our Redux store.
Login Form Component showing user accounts
Nav Component showing logged in user
The home view consists of multiple components. Tab and Pane are from Semantic-UI-React. The rest are custom components. These are in the following hierarchy and include:
This is located in /src/components/Home.js
.
// Home.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Tab } from 'semantic-ui-react';
import UserCard from './UserCard';
import PollTeaser from './PollTeaser';
const color = {
green: {
name: 'green',
hex: '#21ba45'
},
blue: {
name: 'blue',
hex: '#2185d0'
}
};
export class Home extends Component {
static propTypes = {
userQuestionData: PropTypes.object.isRequired
};
render() {
const { userQuestionData } = this.props;
return <Tab panes={panes({ userQuestionData })} className="tab" />;
}
}
const panes = props => {
const { userQuestionData } = props;
return [
{
menuItem: 'Unanswered',
render: () => (
<Tab.Pane>
{userQuestionData.answered.map(question => (
<UserCard
key={question.id}
userId={question.author}
color={color.green.hex}
>
<PollTeaser
question={question}
unanswered={true}
color={color.green.name}
/>
</UserCard>
))}
</Tab.Pane>
)
},
{
menuItem: 'Answered',
render: () => (
<Tab.Pane>
{userQuestionData.unanswered.map(question => (
<UserCard
key={question.id}
userId={question.author}
color={color.blue.hex}
>
<PollTeaser
question={question}
unanswered={false}
color={color.blue.name}
/>
</UserCard>
))}
</Tab.Pane>
)
}
];
};
function mapStateToProps({ authUser, users, questions }) {
const answeredIds = Object.keys(users[authUser].answers);
const answered = Object.values(questions)
.filter(question => answeredIds.includes(question.id))
.sort((a, b) => b.timestamp - a.timestamp);
const unanswered = Object.values(questions)
.filter(question => !answeredIds.includes(question.id))
.sort((a, b) => b.timestamp - a.timestamp);
return {
userQuestionData: {
answered,
unanswered
}
};
}
export default connect(mapStateToProps)(Home);
This file is located at /src/components/UserCard.js
.
// UserCard.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Segment, Header, Grid, Image } from 'semantic-ui-react';
export class UserCard extends Component {
static propTypes = {
userId: PropTypes.string.isRequired,
color: PropTypes.string
};
render() {
const { user, children, color } = this.props;
return (
<Segment.Group>
<Header
as="h5"
textAlign="left"
block
attached="top"
style={{
borderTop: `2px solid ${color}`
}}
content={`${user.name} asks:`}
/>
<Grid divided padded>
<Grid.Row>
<Grid.Column width={5}>
<Image src={user.avatarURL} />
</Grid.Column>
<Grid.Column width={11}>{children}</Grid.Column>
</Grid.Row>
</Grid>
</Segment.Group>
);
}
}
function mapStateToProps({ users }, props) {
const user = users[props.userId];
return {
user
};
}
export default connect(mapStateToProps)(UserCard);
This file is located at /src/components/PollTeaser.js
.
// PollTeaser.js
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Redirect } from 'react-router-dom';
import { Header, Button } from 'semantic-ui-react';
export class PollTeaser extends Component {
static propTypes = {
question: PropTypes.object.isRequired,
unanswered: PropTypes.bool.isRequired,
color: PropTypes.string
};
state = {
viewPoll: false
};
handleClick = e => {
this.setState(prevState => ({
viewPoll: !prevState.viewPoll
}));
};
render() {
const { question, unanswered, color } = this.props;
if (this.state.viewPoll === true) {
return <Redirect push to={`/questions/${question.id}`} />;
}
return (
<Fragment>
<Header as="h5" textAlign="left">
Would you rather
</Header>
<p style={{ textAlign: 'center' }}>
{question.optionOne.text}
<br />
or...
</p>
<Button
color={color}
size="tiny"
fluid
onClick={this.handleClick}
content={unanswered === true ? 'Answer Poll' : 'Results'}
/>
</Fragment>
);
}
}
export default PollTeaser;
Here are screenshots with the provided data.
Home View showing Unanswered Questions Tab
Home View showing Answered Questions Tab
The UserCard component is responsible for displaying each of the following child components based on the context.
For that reason it was necessary to refactor it to account for each option. Our component hierarchy goes like this.
The first step in this refactor was is reduce the overall footprint of our Home Component. It is responsible for displaying a tab control with a pane for answered questions and another for unanswered questions.
This file is located at /src/components/Home.js
.
// Home.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Tab } from 'semantic-ui-react';
import UserCard from './UserCard';
export class Home extends Component {
static propTypes = {
userQuestionData: PropTypes.object.isRequired
};
render() {
const { userQuestionData } = this.props;
return <Tab panes={panes({ userQuestionData })} className="tab" />;
}
}
const panes = props => {
const { userQuestionData } = props;
return [
{
menuItem: 'Unanswered',
render: () => (
<Tab.Pane>
{userQuestionData.answered.map(question => (
<UserCard
key={question.id}
question_id={question.id}
unanswered={true}
/>
))}
</Tab.Pane>
)
},
{
menuItem: 'Answered',
render: () => (
<Tab.Pane>
{userQuestionData.unanswered.map(question => (
<UserCard
key={question.id}
question_id={question.id}
unanswered={false}
/>
))}
</Tab.Pane>
)
}
];
};
function mapStateToProps({ authUser, users, questions }) {
const answeredIds = Object.keys(users[authUser].answers);
const answered = Object.values(questions)
.filter(question => !answeredIds.includes(question.id))
.sort((a, b) => b.timestamp - a.timestamp);
const unanswered = Object.values(questions)
.filter(question => answeredIds.includes(question.id))
.sort((a, b) => b.timestamp - a.timestamp);
return {
userQuestionData: {
answered,
unanswered
}
};
}
export default connect(mapStateToProps)(Home);
Here most of the complex transformation is done in the mapStateToProps
function. This is were we determine answered vs. unanswered questions based on an array of answered question id’s in the user record.
The next step is to refactor the UserCard component.
This has the most involved code since it is a container for one of three different children.
This file is located at /src/components/UserCard.js
.
// UserCard.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Segment, Header, Grid, Image } from 'semantic-ui-react';
import PollQuestion from './PollQuestion';
import PollResult from './PollResult';
import PollTeaser from './PollTeaser';
import { colors } from '../utils/helpers';
const pollTypes = {
POLL_TEASER: 'POLL_TEASER',
POLL_QUESTION: 'POLL_QUESTION',
POLL_RESULT: 'POLL_RESULT'
};
const PollContent = props => {
const { pollType, question, unanswered } = props;
switch (pollType) {
case pollTypes.POLL_TEASER:
return <PollTeaser question={question} unanswered={unanswered} />;
case pollTypes.POLL_QUESTION:
return <PollQuestion question={question} />;
case pollTypes.POLL_RESULT:
return <PollResult question={question} />;
default:
return;
}
};
export class UserCard extends Component {
static propTypes = {
question: PropTypes.object.isRequired,
author: PropTypes.object.isRequired,
pollType: PropTypes.string.isRequired,
unanswered: PropTypes.bool,
question_id: PropTypes.string
};
render() {
const { author, question, pollType, unanswered = null } = this.props;
const tabColor = unanswered === true ? colors.green : colors.blue;
const borderTop =
unanswered === null
? `1px solid ${colors.grey}`
: `2px solid ${tabColor.hex}`;
return (
<Segment.Group>
<Header
as="h5"
textAlign="left"
block
attached="top"
style={{ borderTop: borderTop }}
>
{author.name} asks:
</Header>
<Grid divided padded>
<Grid.Row>
<Grid.Column width={5}>
<Image src={author.avatarURL} />
</Grid.Column>
<Grid.Column width={11}>
<PollContent
pollType={pollType}
question={question}
unanswered={unanswered}
/>
</Grid.Column>
</Grid.Row>
</Grid>
</Segment.Group>
);
}
}
function mapStateToProps(
{ users, questions, authUser },
{ match, question_id }
) {
let question, pollType;
if (question_id !== undefined) {
question = questions[question_id];
pollType = pollTypes.POLL_TEASER;
} else {
const { question_id } = match.params;
question = questions[question_id];
const user = users[authUser];
pollType = pollTypes.POLL_QUESTION;
if (Object.keys(user.answers).includes(question.id)) {
pollType = pollTypes.POLL_RESULT;
}
}
const author = users[question.author];
return {
question,
author,
pollType
};
}
export default connect(mapStateToProps)(UserCard);
What’s happening here is the pollType
(child component) is being set based on the parameters received or the what the data dictates.
If we don’t receive a question_id
then we know to display the PollTeaser component. If a question_id
is received (meaning it is a param in the URL path) then we check if the question has been answered already to know whether to display PollQuestion or PollResult.
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Redirect } from 'react-router-dom';
import { Header, Button } from 'semantic-ui-react';
import { colors } from '../utils/helpers';
export class PollTeaser extends Component {
static propTypes = {
question: PropTypes.object.isRequired,
unanswered: PropTypes.bool.isRequired
};
state = {
viewPoll: false
};
handleClick = e => {
this.setState(prevState => ({
viewPoll: !prevState.viewPoll
}));
};
render() {
const { question, unanswered } = this.props;
const buttonColor = unanswered === true ? colors.green : colors.blue;
const buttonContent = unanswered === true ? 'Answer Poll' : 'Results'
if (this.state.viewPoll === true) {
return <Redirect push to={`/questions/${question.id}`} />;
}
return (
<Fragment>
<Header as="h5" textAlign="left">
Would you rather
</Header>
<p style={{ textAlign: 'center' }}>
{question.optionOne.text}
<br />
or...
</p>
<Button
color={buttonColor.name}
size="tiny"
fluid
onClick={this.handleClick}
content={buttonContent}
/>
</Fragment>
);
}
}
export default PollTeaser;
The main thing happening here is that the color scheme and button text is being changed based on whether the question has been answered or not.
Here are the updated screenshots which should look the same.
Home View showing Unanswered Questions Tab
Home View showing Answered Questions Tab
This step involved creating actions, action creators, & reducers for both the users and questions store slices.
addAnswerToQuestion
action creatoraddAnswerToUser
action creatorhandleSaveQuestionAnswer
middleware thunk methodsaveQuestionAnswer
async API functionconnect
to expose store state and dispatch actionsThis is located in /src/actions/questions.js
.
// questions.js
export const ADD_ANSWER_TO_QUESTION = 'ADD_ANSWER_TO_QUESTION';
export function addAnswerToQuestion(authUser, qid, answer) {
return {
type: ADD_ANSWER_TO_QUESTION,
authUser,
qid,
answer
};
}
This is located in /src/reducers/questions.js
.
// questions.js
import {
RECEIVE_QUESTIONS,
ADD_ANSWER_TO_QUESTION
} from '../actions/questions';
export default function questions(state = {}, action) {
switch (action.type) {
...
case ADD_ANSWER_TO_QUESTION:
const { authUser, qid, answer } = action;
return {
...state,
[qid]: {
...state[qid],
[answer]: {
...state[qid][answer],
votes: state[qid][answer].votes.concat(authUser)
}
}
};
...
}
}
This file contains both our action creator and thunk middleware function. This is located at /src/actions/users.js
.
// users.js
import { saveQuestionAnswer } from '../utils/api';
import { addAnswerToQuestion } from '../actions/questions';
export const ADD_ANSWER_TO_USER = 'ADD_ANSWER_TO_USER';
function addAnswerToUser(authUser, qid, answer) {
return {
type: ADD_ANSWER_TO_USER,
authUser,
qid,
answer
};
}
export function handleSaveQuestionAnswer(authUser, qid, answer) {
return dispatch => {
dispatch(addAnswerToUser(authUser, qid, answer));
dispatch(addAnswerToQuestion(authUser, qid, answer));
return saveQuestionAnswer(authUser, qid, answer).catch(e => {
console.warn('Error in handleSaveQuestionAnswer:', e);
});
};
}
This file is located at /src/reducers/users.js
.
// users.js
import {
ADD_ANSWER_TO_USER,
} from '../actions/users';
export default function users(state = {}, action) {
switch (action.type) {
...
case ADD_ANSWER_TO_USER:
const { authUser, qid, answer } = action;
return {
...state,
[authUser]: {
...state[authUser],
answers: {
...state[authUser].answers,
[qid]: answer
}
}
};
...
}
}
This is located at /src/utils/api.js
.
// api.js
import { _saveQuestionAnswer } from './_DATA';
export function saveQuestionAnswer(authUser, qid, answer) {
return _saveQuestionAnswer({ authUser, qid, answer });
}
This becomes a container component so we can get state as props and dispatch actions based on user interaction.
// PollQuestion.js
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Header, Button, Form, Radio } from 'semantic-ui-react';
import { handleSaveQuestionAnswer } from '../actions/users';
export class PollQuestion extends Component {
static propTypes = {
authUser: PropTypes.string.isRequired,
handleSaveQuestionAnswer: PropTypes.func.isRequired,
question: PropTypes.object.isRequired
};
state = {
value: ''
};
handleChange = (e, { value }) => this.setState({ value });
handleSubmit = e => {
e.preventDefault();
if (this.state.value !== '') {
const { authUser, question, handleSaveQuestionAnswer } = this.props;
handleSaveQuestionAnswer(authUser, question.id, this.state.value);
}
};
render() {
const { question } = this.props;
const disabled = this.state.value === '' ? true : false;
return (
<Fragment>
<Header as="h4">Would you rather</Header>
<Form onSubmit={this.handleSubmit}>
<Form.Field>
<Radio
label={question.optionOne.text}
name="radioGroup"
value="optionOne"
checked={this.state.value === 'optionOne'}
onChange={this.handleChange}
/>
<br />
<Radio
label={question.optionTwo.text}
name="radioGroup"
value="optionTwo"
checked={this.state.value === 'optionTwo'}
onChange={this.handleChange}
/>
</Form.Field>
<Form.Field>
<Button
color="green"
size="tiny"
fluid
positive
disabled={disabled}
content="Submit"
/>
</Form.Field>
</Form>
</Fragment>
);
}
}
function mapStateToProps({ authUser }, { match }) {
return {
authUser
};
}
export default connect(
mapStateToProps,
{ handleSaveQuestionAnswer }
)(PollQuestion);
Here’s a screenshot of the UI.
The Redux logger shows that both actions were dispatched as well as updated state info.
The next step is to connect the PollResult component to the store so we can use the data to calculate the numbers for display.
The items we need to calculate are:
This file is located at /src/components/PollResult.js
.
// PollResult.js
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import {
Header,
Segment,
Progress,
Label,
Button,
Icon
} from 'semantic-ui-react';
import { styles } from '../utils/helpers';
const YourVoteLabel = () => (
<Label color="orange" ribbon="right" className="vote">
<Icon name="check circle outline" size="big" className="compact" />
<div style={{ float: 'right' }}>
Your
<br />
Vote
</div>
</Label>
);
export class PollResult extends Component {
static propTypes = {
history: PropTypes.object.isRequired,
question: PropTypes.object.isRequired,
user: PropTypes.object.isRequired
};
handleClick = () => {
this.props.history.push('/');
};
render() {
const { question, user } = this.props;
const optionOneVotes = question.optionOne.votes.length;
const optionTwoVotes = question.optionTwo.votes.length;
const votesTotal = optionOneVotes + optionTwoVotes;
const userVote = user.answers[question.id];
let option1 = styles.secondary,
option2 = styles.secondary;
if (optionOneVotes > optionTwoVotes) {
option1 = styles.primary;
} else if (optionTwoVotes > optionOneVotes) {
option2 = styles.primary;
}
return (
<Fragment>
<Header as="h3">
Results:
<Header.Subheader style={{ fontWeight: 'bold' }}>
Would you rather
</Header.Subheader>
</Header>
<Segment
color={option1.color}
style={{ backgroundColor: `${option1.bgColor}` }}
>
{userVote === 'optionOne' && <YourVoteLabel />}
<p style={{ fontWeight: 'bold' }}>{question.optionOne.text}</p>
<Progress
percent={((optionOneVotes / votesTotal) * 100).toFixed(2)}
progress
color={option1.color}
>
{optionOneVotes} out of {votesTotal} votes
</Progress>
</Segment>
<Segment
color={option2.color}
style={{ backgroundColor: `${option2.bgColor}` }}
>
{userVote === 'optionTwo' && <YourVoteLabel />}
<p style={{ fontWeight: 'bold' }}>{question.optionTwo.text}</p>
<Progress
percent={((optionTwoVotes / votesTotal) * 100).toFixed(2)}
progress
color={option2.color}
>
{optionTwoVotes} out of {votesTotal} votes
</Progress>
</Segment>
<Button size="tiny" floated="right" onClick={this.handleClick}>
Back
</Button>
</Fragment>
);
}
}
function mapStateToProps({ users, authUser }) {
const user = users[authUser];
return {
user
};
}
export default withRouter(connect(mapStateToProps)(PollResult));
The helper code contains constants so they can be moved out of the component code. This is located at /src/utils/helpers.js
.
// helper.js
export const styles = {
primary: {
color: 'green',
bgColor: 'honeydew'
},
secondary: {
color: 'grey',
bgColor: '#f4f4f4'
}
};
Here are some screenshots of the UI.
The Redux logger shows that both actions were dispatched as well as updated state info.
The Add Question View allows a new poll to be created. This involves creating actions, action creators, & reducers for both the questions and users store slices.
addQuestionToUser
action creatoraddQuestion
action creatorhandleSaveQuestion
middleware thunk methodsaveQuestion
async API functionconnect
to map state and dispatch actionsThis file is located in /src/actions/users.js
.
// users.js
export const ADD_QUESTION_TO_USER = 'ADD_QUESTION_TO_USER';
export function addQuestionToUser({ id, author }) {
return {
type: ADD_QUESTION_TO_USER,
id,
author
};
}
This file is located in /src/reducers/users.js
.
// users.js
import {
RECEIVE_USERS,
ADD_ANSWER_TO_USER,
ADD_QUESTION_TO_USER
} from '../actions/users';
export default function users(state = {}, action) {
switch (action.type) {
...
case ADD_QUESTION_TO_USER:
const { id, author } = action;
return {
...state,
[author]: {
...state[author],
questions: state[author].questions.concat(id)
}
};
...
}
}
This file contains our action creator and thunk middleware function. This is located in /src/actions/questions.js
.
// questions.js
import { saveQuestion } from '../utils/api';
import { addQuestionToUser } from '../actions/users';
export const ADD_QUESTION = 'ADD_QUESTION';
function addQuestion(question) {
return {
type: ADD_QUESTION,
question
};
}
export function handleSaveQuestion(optionOneText, optionTwoText, author) {
return dispatch => {
return saveQuestion({ optionOneText, optionTwoText, author }).then(
question => {
dispatch(addQuestion(question));
dispatch(addQuestionToUser(question));
}
);
};
}
This file is located at /src/reducers/questions.js
.
// questions.js
import {
ADD_QUESTION
} from '../actions/questions';
export default function questions(state = {}, action) {
switch (action.type) {
...
case ADD_QUESTION:
const { question } = action;
return {
...state,
[question.id]: question
};
...
}
}
This is located at /src/utils/api.js
.
// api.js
import {
_saveQuestion,
} from './_DATA';
export function saveQuestion(question) {
return _saveQuestion(question);
}
This becomes a container component so we can get state as props and dispatch actions based on user interaction.
// NewPoll.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import {
Segment,
Header,
Grid,
Divider,
Form,
Dimmer,
Loader
} from 'semantic-ui-react';
import { handleSaveQuestion } from '../actions/questions';
export class NewPoll extends Component {
static propTypes = {
authUser: PropTypes.string.isRequired,
handleSaveQuestion: PropTypes.func.isRequired
};
state = {
validSubmit: false,
isLoading: false,
option1: '',
option2: ''
};
handleChange = e => {
this.setState({ [e.target.id]: e.target.value });
};
handleSubmit = e => {
e.preventDefault();
const { authUser, handleSaveQuestion } = this.props;
const { option1, option2 } = this.state;
new Promise((res, rej) => {
this.setState({ isLoading: true });
handleSaveQuestion(option1, option2, authUser);
setTimeout(() => res('success'), 1000);
}).then(() => {
this.setState({
option1: '',
option2: ''
});
this.setState({ validSubmit: true });
});
};
render() {
console.log('this.props', this.props);
const disabled = this.state.option1 === '' || this.state.option2 === '';
if (this.state.validSubmit === true) {
return <Redirect to="/" />;
}
return (
<Segment.Group>
<Header as="h3" textAlign="left" block attached="top">
Create a New Poll
</Header>
<Grid padded>
<Grid.Column>
{this.state.isLoading && (
<Dimmer active inverted>
<Loader content="Updating" />
</Dimmer>
)}
<p>Complete the question:</p>
<p>
<strong>Would you rather...</strong>
</p>
<Form onSubmit={this.handleSubmit}>
<Form.Input
id="option1"
placeholder="Enter option one..."
value={this.state.option1}
onChange={this.handleChange}
required
/>
<Divider horizontal>Or</Divider>
<Form.Input
id="option2"
placeholder="Enter option two..."
value={this.state.option2}
onChange={this.handleChange}
required
/>
<Form.Button positive size="tiny" fluid disabled={disabled}>
Submit
</Form.Button>
</Form>
</Grid.Column>
</Grid>
</Segment.Group>
);
}
}
function mapStateToProps({ authUser }) {
return {
authUser
};
}
export default connect(
mapStateToProps,
{ handleSaveQuestion }
)(NewPoll);
Here are some screenshots showing the added question.
The Redux logger shows that both actions were dispatched as well as updated state info.
The poll question is then added to the Home view.
Home View with added poll question
The question can then be answered normally.
The poll results are then displayed.
The leaderboard consists of data transformations to user in order to display user scores.
This file is located at /src/components/Leaderboard.js
.
// Leaderboard.js
import React, { Component, Fragment } from 'react';
import PropType from 'prop-types';
import { connect } from 'react-redux';
import {
Segment,
Grid,
Header,
Image,
Label,
Divider
} from 'semantic-ui-react';
const trophyColor = ['yellow', 'grey', 'orange'];
export class Leaderboard extends Component {
static propType = {
leaderboardData: PropType.array.isRequired
};
render() {
const { leaderboardData } = this.props;
return (
<Fragment>
{leaderboardData.map((user, idx) => (
<Segment.Group key={user.id}>
<Label corner="left" icon="trophy" color={trophyColor[idx]} />
<Grid divided padded>
<Grid.Row>
<Grid.Column width={4} verticalAlign="middle">
<Image src={user.avatarURL} />
</Grid.Column>
<Grid.Column width={8}>
<Header as="h3" textAlign="left">
{user.name}
</Header>
<Grid>
<Grid.Column width={12}>Answered questions</Grid.Column>
<Grid.Column width={4}>{user.answerCount}</Grid.Column>
</Grid>
<Divider />
<Grid>
<Grid.Column width={12}>Created questions</Grid.Column>
<Grid.Column width={4}>{user.questionCount}</Grid.Column>
</Grid>
</Grid.Column>
<Grid.Column width={4} textAlign="center">
<Segment.Group>
<Header as="h5" block attached="top" content="Score" />
<Segment>
<Label circular color="green" size="big">
{user.questionCount + user.answerCount}
</Label>
</Segment>
</Segment.Group>
</Grid.Column>
</Grid.Row>
</Grid>
</Segment.Group>
))}
</Fragment>
);
}
}
function mapStateToProps({ users }) {
const leaderboardData = Object.values(users)
.map(user => ({
id: user.id,
name: user.name,
avatarURL: user.avatarURL,
answerCount: Object.values(user.answers).length,
questionCount: user.questions.length,
total: Object.values(user.answers).length + user.questions.length
}))
.sort((a, b) => a.total - b.total)
.reverse()
.slice(0, 3);
console.log('leaderboardData', leaderboardData);
return {
leaderboardData
};
}
export default connect(mapStateToProps)(Leaderboard);
Here’s the screenshot.
This last part deals with displaying a 404 error when a bad path is used.
This file is located at /src/components/NoMatch.js
.
// NoMatch.js
import React, { Component } from 'react';
import { Container, Header } from 'semantic-ui-react';
export class NoMatch extends Component {
render() {
return (
<Container textAlign="center">
<Header as="h3">No Match 404 Error</Header>
<p>Nothing to see here. Please use the menu to try again.</p>
</Container>
);
}
}
export default NoMatch;
The final change involved updating the Routes in App. This file is located at /src/components/App.js
.
// App.js
import React, { Component, Fragment } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { Grid } from 'semantic-ui-react';
import { handleInitialData } from '../actions/shared';
import { connect } from 'react-redux';
import Login from './Login';
import Nav from './Nav';
import Home from './Home';
import UserCard from './UserCard';
import NewPoll from './NewPoll';
import Leaderboard from './Leaderboard';
import NoMatch from './NoMatch';
class App extends Component {
componentDidMount() {
this.props.handleInitialData();
}
render() {
const { authUser } = this.props;
return (
<Router>
<div className="App">
{authUser === null ? (
<Route
render={() => (
<ContentGrid>
<Login />
</ContentGrid>
)}
/>
) : (
<Fragment>
<Nav />
<ContentGrid>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/questions/bad_id" component={NoMatch} />
<Route path="/questions/:question_id" component={UserCard} />
<Route path="/add" component={NewPoll} />
<Route path="/leaderboard" component={Leaderboard} />
<Route component={NoMatch} />
</Switch>
</ContentGrid>
</Fragment>
)}
</div>
</Router>
);
}
}
const ContentGrid = ({ children }) => (
<Grid padded="vertically" columns={1} centered>
<Grid.Row>
<Grid.Column style={{ maxWidth: 550 }}>{children}</Grid.Column>
</Grid.Row>
</Grid>
);
function mapStateToProps({ authUser }) {
return {
authUser
};
}
export default connect(
mapStateToProps,
{ handleInitialData }
)(App);
Lastly I had to make an update to the UserCard component to redirect when a nonexistent question_id is used in the path. This file is located at /src/components/UserCard.js
.
// UserCard.js
import { Redirect } from 'react-router-dom';
export class UserCard extends Component {
static propTypes = {
question: PropTypes.object,
author: PropTypes.object,
pollType: PropTypes.string,
unanswered: PropTypes.bool,
question_id: PropTypes.string
};
render() {
const {
author,
question,
pollType,
badPath,
unanswered = null
} = this.props;
if (badPath === true) {
return <Redirect to="/questions/bad_id" />;
}
render(...)
}
}
function mapStateToProps(
{ users, questions, authUser },
{ match, question_id }
) {
let question,
author,
pollType,
badPath = false;
if (question_id !== undefined) {
question = questions[question_id];
author = users[question.author];
pollType = pollTypes.POLL_TEASER;
} else {
const { question_id } = match.params;
question = questions[question_id];
const user = users[authUser];
if (question === undefined) {
badPath = true;
} else {
author = users[question.author];
pollType = pollTypes.POLL_QUESTION;
if (Object.keys(user.answers).includes(question.id)) {
pollType = pollTypes.POLL_RESULT;
}
}
}
return {
badPath,
question,
author,
pollType
};
}
Here are the screenshots.
The next thing I needed to do was prep my code for submission. This involved:
console.log
statementsI received the results from my project review and passed on my first submission.
Here’s a screenshot of the Summary.
The last step is to update the README with screenshots of the app including the following.