React ND Project 2 - Would You Rather

Project Notes by James Priest

Code Notes

Live Demo GitHub Repo

wyr77
Would You Rather Game

This site contains code notes for how I built project 2 of my Udacity React Nanodegree Program.


1. Project Requirements

1.1 Overview

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

1.1.1 Why this project

This project will solidify your understanding of React and Redux while giving you a chance to express your creativity. You’ll practice

1.1.2 Starter Code

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.

1.2 App Functionality

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.

1.3 App Architecture

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.4 Project Instructions

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.

1.5 Sample Project UI

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.

wyr1
Login page

wyr2
Home page

wyr3
Poll page

wyr4
Poll Results

wyr5
Create Question

wyr6
Leader Board

1.6 Step-by-Step Guide

1.6.1 Planning Stage 📐

Step 1 - Draw All of the Views of the App (Visual Design Stage)

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.

Step 2 - Break Each View Into a Hierarchy of Components

For this step,

Step 3 - Determine What Events Happen in the App

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!

Step 4 - Determine What Data Lives in the Store

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

1.6.2 Coding Stage🔨

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.

2. Mockup Creation

2.1 Semantic UI React

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.

2.2 UI Testing

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.

2.2.1 UITest1

Here I tested a handful of random components that you can see from the import list.

wyr7
UITest1.js

2.2.2 UITest2

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.

wyr8
UITest2.js

2.3 UI Mockups

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.

2.3.1 Sample Page

wyr9
Live Demo: Would You Rather@4-ui-mockups on CodeSandbox

2.4 Mockup - Nav

I split out the navigation into a Nav component that is responsive and scales well on mobile devices.

wyr12
Live Demo: Would You Rather@5-mockup-nav on CodeSandbox

2.5 Mockup - Home

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.

wyr10
Unanswered Questions

wyr11
Answered Questions

Live Demo: Would You Rather@6-mockup-home on CodeSandbox

2.6 Mockup - Framework

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.

2.6.1 App.js

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.

wyr14
Unanswered Questions Tab

wyr15
Answered Questions Tab

Live Demo: Would You Rather@7-mockup-framework on CodeSandbox

2.7 Mockup - Poll Question

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.

wyr18
Poll Question

Live Demo: Would You Rather@8-mockup-poll-question on CodeSandbox

2.8 Mockup - Poll Result

The next step was to add in the PollResult.js component.

wyr19
Poll Result

Live Demo: Would You Rather@9-mockup-poll-result on CodeSandbox

2.9 Mockup - New Poll

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.

wyr23
New Poll - Incomplete fields

wyr21
New Poll - Valid fields

wyr22
New Poll - Loader on Submission

Live Demo: Would You Rather@10-mockup-new-poll on CodeSandbox

2.10 Mockup - Leaderboard

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.

wyr24
Leaderboard

Live Demo: Would You Rather@11-mockup-leaderboard on CodeSandbox

2.11 Mockup - Login

The last mockup is of the Login screen. This needs to do the following.

Here are screenshots of the login page.

wyr25
Login 1

wyr27
Login 2

Live Demo: Would You Rather@12-mockup-login on CodeSandbox

3. Application Design

3.1 Step 1 - View Requirements

Now that we have mockups created I can list those here along with the requirements for each application view.

3.1.1 Login view

wyr28
Login

Login View Requirements:

3.1.2 Home view

wyr29
Home - Unanswered Polls

Home View Requirements:

wyr34
Home - Answered Polls

3.1.3 Poll Question view

wyr30
Poll Question

Poll Question View Requirements:

When someone votes in a poll:

3.1.4 Poll Result view

wyr31
Poll Result

Poll Result View Requirements:

3.1.5 Poll 404 view

wyr37
Poll 404

Poll 404 View Requirements:

3.1.6 New Poll view

wyr32
New Poll

New Poll View Requirements:

3.1.7 Leaderboard view

wyr33
Leaderboard

Leaderboard View Requirements:

3.1.8 Additional Requirements

3.2 Step 2 - Component Hierarchy

The 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).

3.2.1 Login view components

wyr35
Login

This view is broken down into the following React components:

3.2.2 Home view components

wyr36
Home

3.2.3 Poll Question view components

wyr38
Poll Question

3.2.4 Poll Result view components

wyr39
Poll Result

3.2.5 Poll 404 view components

wyr40
Poll 404

3.2.6 New Poll view components

wyr41
New Poll

3.2.7 Leaderboard view components

wyr42
Leaderboard

3.2.8 Master list of all 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.

3.3 Step 3 - App Events

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:

3.3.1 App component events

When the app first loads we need to fill the store with data.

wyr43
App component

Action and data:

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 and questions 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 creator
    • getQuestions(questions) action creator
  • Invoke handleInitialData() from App’s componentDidMount() lifecycle event.

3.3.2 Login component events

Data used to populate form.

wyr44
Login component

Action and data:

3.3.3 Home component events

Data used to populate tab panes.

wyr45
Home component

3.3.4 UserCard component events

wyr46
UserCard component

3.3.5 PollTeaser component events

wyr47
PollTeaser component

3.3.6 PollQuestion component events

wyr48
PollQuestion component

3.3.7 PollAnswer component events

wyr49
PollAnswer component

3.3.8 NewPoll component events

wyr50
NewPoll component

3.3.9 Leaderboard component events

wyr51
Leaderboard component

3.3.10 LeaderboardCard component events

wyr52
LeaderboardCard component

3.4 Step 4 - Store Data

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.

3.4.1 Data elements

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.

3.4.2 Data Normalization

The Redux documentation recommends normalizing our state according to the following normalization principles.

3.4.3 Store properties

Our store will consist of the following data “tables”.

wyr53
Redux Store Entities

4. Coding Phase

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.

4.1 API Functions

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.

4.1.1 api.js

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

4.2 Actions

The next step is to create a set of actions and action creators.

We start by creating the following files in /src/actions/.

4.2.1 authUser.js

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

4.2.2 questions.js

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

4.2.3 users.js

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

4.2.4 shared.js

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

4.3 Reducers

The next step is to create our reducers.

We start by creating the following files in /src/reducers/;

4.3.1 authUser.js

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

4.3.2 questions.js

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

4.3.3 users.js

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

4.3.4 index.js

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.

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

4.3.5 Add Redux Provider code to entry point

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')
);

4.3.6 Testing

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.

wyr54
React Tools showing storeState

4.4 Middleware

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/.

4.4.1 logger.js

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;

4.4.2 index.js

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.

4.4.3 Add Redux Middleware to code entry point

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')
);

4.5 Initialize App Data

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.

4.5.1 App.js

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.

wyr55
React Tools showing storeState

We can also see our logger is working properly because it outputs both the RECEIVE_QUESTIONS and RECEIVE_USERS actions.

wyr56
DevTools Console with Logger output

4.6 Login & Navigation

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.

4.6.1 App 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.

4.6.2 Login Component

Next we update the Login component to do the following:

We 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;

4.6.3 Nav Component

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.

wyr57
Login Form Component showing user accounts

wyr58
Nav Component showing logged in user

4.7 Home View

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:

4.7.1 Home Component

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

4.8.2 UserCard Component

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

4.8.3 PollTeaser Component

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.

wyr59
Home View showing Unanswered Questions Tab

wyr60
Home View showing Answered Questions Tab

4.8 UserCard Component

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.

4.8.1 Home Component

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.

4.8.2 UserCard Component

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.

4.8.3 PollTeaser Component

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.

wyr61
Home View showing Unanswered Questions Tab

wyr62
Home View showing Answered Questions Tab

4.9 Answer Poll Question

This step involved creating actions, action creators, & reducers for both the users and questions store slices.

4.9.1 Questions Action

This 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
  };
}

4.9.2 Questions Reducer

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

4.9.3 Users Action

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

4.9.4 Users Reducer

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
          }
        }
      };
    ...
  }
}

4.9.5 API code

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

4.9.6 PollQuestion Component

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.

wyr63
Answer Question View

The Redux logger shows that both actions were dispatched as well as updated state info.

wyr64
Redux Logger

4.10 Display Poll Results

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:

4.10.1 PollResult Component

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

4.10.2 Helper code

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.

wyr65
Poll Results View

The Redux logger shows that both actions were dispatched as well as updated state info.

wyr66
Poll Results with tie

4.11 Create New Poll

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.

4.11.1 Users Action

This 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
  };
}

4.11.2 Users Reducer

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

4.11.3 Questions Actions

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

4.11.4 Questions Reducer

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
      };
    ...
  }
}

4.11.5 API code

This is located at /src/utils/api.js.

// api.js
import {
  _saveQuestion,
} from './_DATA';

export function saveQuestion(question) {
  return _saveQuestion(question);
}

4.11.6 NewPoll Component

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.

wyr67
Create New Poll View

The Redux logger shows that both actions were dispatched as well as updated state info.

wyr71
Redux Logger

The poll question is then added to the Home view.

wyr68
Home View with added poll question

The question can then be answered normally.

wyr69
Answer Poll Question

The poll results are then displayed.

wyr70
Poll Results

4.12 Display Leaderboard

The leaderboard consists of data transformations to user in order to display user scores.

4.12.1 Leaderboard Component

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.

wyr72
Leaderboard component

4.13 No Match 404 Routing

This last part deals with displaying a 404 error when a bad path is used.

4.13.1 NoMatch Component

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;

4.13.2 App Component

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

4.13.3 UserCard Component

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.

wyr73
NoMatch on bad path

wyr74
NoMatch on bad question_id

5. Project Submission

5.1 Clean-up Code

The next thing I needed to do was prep my code for submission. This involved:

5.2 Project Review

I received the results from my project review and passed on my first submission.

Here’s a screenshot of the Summary.

wyr75
Review Summary

5.3 Update README

The last step is to update the README with screenshots of the app including the following.

wyr77
Glamour Shot #1

wyr76
Glamour Shot #2