diff --git a/README.md b/README.md index 60b569e..b00ee47 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ See [https://opentdb.com/api\_config.php](https://opentdb.com/api_config.php) fo ## Instructions -* Fork and clone this repo. Run `npm install` to get all dependencies. + -* Make the app responsive and look good at all screen sizes. + ## Stretch goals - feel free to pick -* Show the user a 'happy' animated gif on a correct answer and a 'sad' gif on incorrect answer. + * Allow user to select question category -* Allow user to select difficulty level + * Add some unit tests -* Implement a scoring system that gives higher scores for more difficult questions + -* Create a high score table. When a user finishes the game, for example answers a question incorrectly, allow them to enter their name and add it along with score to a high score table. + * Display statistics about player performance such as total questions played, average score, most popular category, category with highest percentage of correct etc. @@ -71,7 +71,7 @@ See [https://opentdb.com/api\_config.php](https://opentdb.com/api_config.php) fo * Make frequent commits * Create a pull request at the end -## Redux getting started guide + diff --git a/index.html b/index.html index 20c7415..048f512 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,19 @@ - - Redux intro - - - -
- -
- - - + + + Quizness Time + + + + + + + +
+ +
+ + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 90154a0..e506249 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4674,6 +4674,11 @@ "minimalistic-assert": "^1.0.1" } }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -9127,6 +9132,11 @@ "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", "dev": true }, + "shuffle-array": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/shuffle-array/-/shuffle-array-1.0.1.tgz", + "integrity": "sha1-xP88/nTRb5NzBZIwGyXmV3sSiYs=" + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", diff --git a/package.json b/package.json index 7102a0e..9680d09 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,14 @@ "classnames": "^2.2.6", "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.1.1", + "he": "^1.2.0", "prop-types": "^15.6.2", "react": "^16.2.0", "react-dom": "^16.2.0", "react-redux": "^5.0.7", "redux": "^4.0.0", - "redux-thunk": "^2.3.0" + "redux-thunk": "^2.3.0", + "shuffle-array": "^1.0.1" }, "devDependencies": { "babel-jest": "^22.4.1", diff --git a/src/actions/index.js b/src/actions/index.js index a1d9430..35ba0b4 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -1,5 +1,97 @@ -export function fetchQuestion(){ - return function(dispatch){ +import shuffle from "shuffle-array"; +export function fetchQuestion(difficulty) { + return function(dispatch) { + fetch(`https://opentdb.com/api.php?amount=1&type=multiple${difficulty}`) + .then(response => response.json()) + .then(result => { + const questionObj = result.results[0]; + dispatch(receiveQuestion(questionObj)); + }) + .catch(error => console.log(error)); + }; +} + +export function receiveQuestion(question) { + question.answerArr = shuffle( + question.incorrect_answers.concat(question.correct_answer) + ); + return { + type: "RECEIVE_QUESTION", + question, + correct: "" + }; +} + +export function receiveAnswer(answer, question, quizDifficulty) { + const correctAnswer = question.correct_answer; + if (answer === correctAnswer) { + return { + type: "CORRECT_ANSWER", + quizDifficulty, + questionDifficulty: question.difficulty + }; + } else { + return { + type: "INCORRECT_ANSWER" + }; + } +} + +export function receiveView(view) { + return { + type: "RECEIVE_VIEW", + view + }; +} + +export function receiveDifficulty(difficulty) { + return { + type: "RECEIVE_DIFFICULTY", + difficulty + }; +} + +export function receivePlayerName(name) { + return { + type: "RECEIVE_PLAYER_NAME", + name + }; +} + + +export function fetchScoreboard(difficulty) { + const scoreboard = JSON.parse(localStorage.getItem(difficulty)) + return { + scoreboard + } +} + +export function initializeStateScoreboard(difficulty){ + const scoreboard = JSON.parse(localStorage.getItem(difficulty)) + return { + type: "RECEIVE_SCOREBOARD", + scoreboard: scoreboard, + formVisible: "yes" } } + +export function submitScore(name, points, difficulty) { + const quizDifficulty = !difficulty ? "random" : difficulty; + const scoreboardArray = fetchScoreboard(quizDifficulty); + const playerScoreObject = { name: name, points: points }; + if (scoreboardArray.scoreboard === null) { + localStorage.setItem(quizDifficulty, JSON.stringify([playerScoreObject])); + } else if (scoreboardArray) { + const newScoreboard = scoreboardArray.scoreboard.concat(playerScoreObject) + newScoreboard.sort((a, b) => b.points - a.points); + localStorage.setItem(difficulty, JSON.stringify(newScoreboard)) + } + const updatedScoreboard = fetchScoreboard(quizDifficulty); + return { + type: "RECEIVE_SCOREBOARD", + scoreboard: updatedScoreboard.scoreboard, + formVisible: "no" + } +} + diff --git a/src/components/App.js b/src/components/App.js index ef2abb3..44c24ed 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -1,10 +1,11 @@ import React from 'react'; +import ContentContainer from '../containers/ContentContainer' class App extends React.Component { render(){ return (
- App contents go here +
) } diff --git a/src/components/Content.js b/src/components/Content.js new file mode 100644 index 0000000..3f3cb85 --- /dev/null +++ b/src/components/Content.js @@ -0,0 +1,21 @@ +import React from "react"; +import QuestionContainer from "../containers/QuestionContainer"; +import PointsContainer from "../containers/PointsContainer"; +import MenuContainer from "../containers/MenuContainer"; +import ScoreboardContainer from "../containers/ScoreboardContainer"; + +function Content({ view }) { + return ( +
+ {view === "menu" && } + {view === "quiz" && ( + + + + )} + {view === "scoreboard" && } +
+ ); +} + +export default Content; diff --git a/src/components/Menu.js b/src/components/Menu.js new file mode 100644 index 0000000..61445e3 --- /dev/null +++ b/src/components/Menu.js @@ -0,0 +1,43 @@ +import React from "react"; + +function Menu({ difficulty, receiveView, receiveDifficulty }) { + return ( +
+

It's Quizness Time!

+ + + {difficulty === "&difficulty=easy" && ( +

Just the basics, easy questions for easy going players.

+ )} + {difficulty === "&difficulty=medium" && ( +

Not quite easy, not quite hard.

+ )} + {difficulty === "&difficulty=hard" && ( +

Challenging to say the least.

+ )} + {difficulty === "" && ( +

+ A mixed bag of question difficulties! Easy questions are worth 1 + point, medium questions are worth 2 points and hard questions are + worth 3 points. +

+ )} + +
+ ); +} + +export default Menu; diff --git a/src/components/Points.js b/src/components/Points.js new file mode 100644 index 0000000..16f0a3f --- /dev/null +++ b/src/components/Points.js @@ -0,0 +1,12 @@ +import React from 'react'; + +function Points({ points }) { + + return( +
+

POINTS: {points}

+
+ ) +} + +export default Points; \ No newline at end of file diff --git a/src/components/Question.js b/src/components/Question.js new file mode 100644 index 0000000..a1f3211 --- /dev/null +++ b/src/components/Question.js @@ -0,0 +1,95 @@ +import React from "react"; +import question from "../reducers/question"; +import { decode } from "he"; + +class Question extends React.Component { + constructor() { + super(); + } + + componentDidMount() { + this.props.fetchQuestion(this.props.difficulty); + this.props.initializeStateScoreboard(this.props.difficulty); + } + + fetchNextQuestion() { + setTimeout(() => this.props.fetchQuestion(this.props.difficulty), 3000); + } + + goToScoreboard() { + setTimeout(() => this.props.receiveView("scoreboard"), 3000); + } + + render() { + const correctAnswer = this.props.question.correct_answer; + + return ( +
+

+ {this.props.numberOfQuestions} + /20 +

+ {this.props.question.question && ( +
+

+ {decode(this.props.question.question)}{" "} +

+ {this.props.difficulty === "" && ( +

Difficulty: {this.props.question.difficulty}

+ )} + {this.props.question.answerArr.map(answer => ( + + ))} + {this.props.correct === "yes" && ( +
+ {" "} +

+ Correct! Well Done +

{" "} + +
+ )} + {this.props.correct === "no" && ( +
+

+ Incorrect! The correct answer was: {decode(correctAnswer)}{" "} +

+ +
+ )} +
+ )} +
+ ); + } +} + +export default Question; diff --git a/src/components/Scoreboard.js b/src/components/Scoreboard.js new file mode 100644 index 0000000..53d84f1 --- /dev/null +++ b/src/components/Scoreboard.js @@ -0,0 +1,59 @@ +import React from "react"; + +class Scoreboard extends React.Component { + constructor() { + super(); + } + + componentDidMount() {} + + render() { + return ( +
+

+ Well done, you scored {this.props.points} points! +

+
    + {this.props.scoreboard.map(score => ( +
  1. + {" "} + {score.name}: {score.points}{" "} +
  2. + ))} +
+ {this.props.formVisible == "yes" && ( +
{ + event.preventDefault(); + this.props.submitScore( + this.props.name, + this.props.points, + this.props.difficulty + ); + }} + > +

+ Enter your name below to add your score to the scoreboard +

+ + this.props.receivePlayerName(event.target.value) + } + placeholder="Please enter your name here" + /> + +
+ )} +
+ ); + } +} + +export default Scoreboard; diff --git a/src/containers/ContentContainer.js b/src/containers/ContentContainer.js new file mode 100644 index 0000000..800f376 --- /dev/null +++ b/src/containers/ContentContainer.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import Content from '../components/Content'; + +const mapStateToProps = state => { + return { + view: state.content.view + } +} + +export default connect( + mapStateToProps +)(Content) \ No newline at end of file diff --git a/src/containers/MenuContainer.js b/src/containers/MenuContainer.js new file mode 100644 index 0000000..0997efd --- /dev/null +++ b/src/containers/MenuContainer.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import Menu from '../components/Menu'; +import { receiveView, receiveDifficulty } from '../actions'; + +const mapStateToProps = state => { + return { + difficulty: state.menu.difficulty + } +} + +const mapDispatchToProps = dispatch => { + return { + receiveView: (view) => dispatch(receiveView(view)), + receiveDifficulty: (difficulty) => dispatch(receiveDifficulty(difficulty)) + } +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Menu) \ No newline at end of file diff --git a/src/containers/PointsContainer.js b/src/containers/PointsContainer.js new file mode 100644 index 0000000..2ed1153 --- /dev/null +++ b/src/containers/PointsContainer.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import Points from '../components/Points'; +import {} from '../actions' + +const mapStateToProps = state => { + return { + points: state.points.points + } +} + +export default connect( + mapStateToProps +)(Points) \ No newline at end of file diff --git a/src/containers/QuestionContainer.js b/src/containers/QuestionContainer.js new file mode 100644 index 0000000..05a071d --- /dev/null +++ b/src/containers/QuestionContainer.js @@ -0,0 +1,29 @@ +import { connect } from "react-redux"; +import Question from "../components/Question"; +import { fetchQuestion, receiveAnswer, receiveView, initializeStateScoreboard } from "../actions"; + +const mapStateToProps = state => { + return { + question: state.question.question, + numberOfQuestions: state.question.numberOfQuestions, + correct: state.points.correct, + difficulty: state.menu.difficulty + }; +}; + +const mapDispatchToProps = dispatch => { + return { + fetchQuestion: difficulty => dispatch(fetchQuestion(difficulty)), + receiveAnswer: (answer, question, quizDifficulty) => + dispatch( + receiveAnswer(answer, question, quizDifficulty) + ), + receiveView: view => dispatch(receiveView(view)), + initializeStateScoreboard: difficulty => dispatch(initializeStateScoreboard(difficulty)) + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Question); diff --git a/src/containers/ScoreboardContainer.js b/src/containers/ScoreboardContainer.js new file mode 100644 index 0000000..280dd4b --- /dev/null +++ b/src/containers/ScoreboardContainer.js @@ -0,0 +1,25 @@ +import { connect } from "react-redux"; +import Scoreboard from "../components/Scoreboard"; +import { receivePlayerName, submitScore } from "../actions"; + +const mapStateToProps = state => { + return { + points: state.points.points, + difficulty: state.menu.difficulty, + name: state.scoreboard.name, + scoreboard: state.scoreboard.scoreboard, + formVisible: state.scoreboard.formVisible + }; +}; + +const mapDispatchToProps = dispatch => { + return { + receivePlayerName: name => dispatch(receivePlayerName(name)), + submitScore: (name, points, difficulty) => dispatch(submitScore(name, points, difficulty)) + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Scoreboard); diff --git a/src/index.js b/src/index.js index 846662e..8213dfc 100644 --- a/src/index.js +++ b/src/index.js @@ -3,13 +3,17 @@ import ReactDOM from 'react-dom'; import App from './components/App'; import thunkMiddleware from 'redux-thunk'; -import { createStore, applyMiddleware } from 'redux'; +import { createStore, applyMiddleware, compose } from 'redux'; import { Provider } from 'react-redux'; import rootReducer from './reducers'; -const store = createStore(rootReducer, applyMiddleware( - thunkMiddleware -)); + +const enhansers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + + +const store = createStore(rootReducer, enhansers(applyMiddleware( + thunkMiddleware) +)) ReactDOM.render( diff --git a/src/reducers/content.js b/src/reducers/content.js new file mode 100644 index 0000000..e9364d1 --- /dev/null +++ b/src/reducers/content.js @@ -0,0 +1,12 @@ +function content(state = {view: "menu"}, action){ + switch(action.type) { + case 'RECEIVE_VIEW': + return { + view: action.view + } + default: + return state; + } +} + +export default content; \ No newline at end of file diff --git a/src/reducers/index.js b/src/reducers/index.js index 8e738e8..3a89590 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,6 +1,14 @@ import { combineReducers } from 'redux'; -import placeholder from './placeholder'; +import question from './question'; +import points from './points'; +import content from './content'; +import menu from './menu'; +import scoreboard from './scoreboard'; export default combineReducers({ - placeholder + question, + points, + content, + menu, + scoreboard }); diff --git a/src/reducers/menu.js b/src/reducers/menu.js new file mode 100644 index 0000000..29d845f --- /dev/null +++ b/src/reducers/menu.js @@ -0,0 +1,12 @@ +function menu(state = {difficulty: "&difficulty=easy"}, action) { + switch(action.type) { + case 'RECEIVE_DIFFICULTY': + return { + difficulty: action.difficulty + } + default: + return state; + } +} + +export default menu; \ No newline at end of file diff --git a/src/reducers/placeholder.js b/src/reducers/placeholder.js deleted file mode 100644 index 25755c5..0000000 --- a/src/reducers/placeholder.js +++ /dev/null @@ -1,8 +0,0 @@ -function placeholder(state = '', action){ - switch (action.type) { - default: - return state - } -} - -export default placeholder; diff --git a/src/reducers/points.js b/src/reducers/points.js new file mode 100644 index 0000000..c279b9d --- /dev/null +++ b/src/reducers/points.js @@ -0,0 +1,52 @@ +const initialState = { + points : 0, + correct: "" +} + + +function points(state = initialState, action){ + const addOnePoint = state.points + 1; + const addTwoPoints = state.points + 2; + const addThreePoints = state.points + 3; + switch (action.type) { + case 'RECEIVE_QUESTION': + return { + points: state.points, + correct: action.correct + } + case 'CORRECT_ANSWER': + if (action.quizDifficulty !== "") { + return { + points: addOnePoint, + correct: 'yes' + } + } else if (action.quizDifficulty === "") { + if(action.questionDifficulty === "easy") { + return { + points: addOnePoint, + correct: 'yes' + } + } else if (action.questionDifficulty === "medium") { + return { + points: addTwoPoints, + correct: 'yes' + } + } else if (action.questionDifficulty === "hard") { + return { + points: addThreePoints, + correct: 'yes' + } + } + } + + case 'INCORRECT_ANSWER': + return { + points: state.points, + correct: 'no' + } + default: + return state; + } +} + +export default points; \ No newline at end of file diff --git a/src/reducers/question.js b/src/reducers/question.js new file mode 100644 index 0000000..0ec1b8e --- /dev/null +++ b/src/reducers/question.js @@ -0,0 +1,14 @@ +function question(state = {question: {}, numberOfQuestions: 0}, action){ + switch (action.type) { + case 'RECEIVE_QUESTION': + const incrementNumberOfQuestions = state.numberOfQuestions + 1; + return{ + question: action.question, + numberOfQuestions: incrementNumberOfQuestions + } + default: + return state + } +} + +export default question; diff --git a/src/reducers/scoreboard.js b/src/reducers/scoreboard.js new file mode 100644 index 0000000..aca09d9 --- /dev/null +++ b/src/reducers/scoreboard.js @@ -0,0 +1,21 @@ +function scoreboard(state = {name: "", scoreboard: [], formVisible: "yes"}, action) { + switch(action.type){ + case 'RECEIVE_PLAYER_NAME': + return { + name: action.name, + scoreboard: state.scoreboard, + formVisible: state.formVisible + } + + case 'RECEIVE_SCOREBOARD': + return { + name: state.name, + scoreboard: action.scoreboard, + formVisible: action.formVisible + } + default: + return state; + } +} + +export default scoreboard; \ No newline at end of file diff --git a/style.css b/style.css index d9bbbe0..9307152 100644 --- a/style.css +++ b/style.css @@ -1,15 +1,210 @@ -.voting-button { - color: #550527; - font-size: 24px; - padding: 10px; - margin: 10px; +:root { + --heading-font: 'Lobster', cursive; + --question-font: 'Encode Sans Condensed', sans-serif; +} + +html, body, ul, ol { + margin: 0; + padding: 0; + background-color: #1A8FE3; +} + +.menu__container { + height: 100vh; + width: 100vw; + display: flex; + flex-direction: column; + justify-content: center; +} + +.menu__title { + font-family: var(--heading-font); + display: flex; + justify-content: center; + color: #DE541E; + -webkit-text-stroke: 0.1px #F7F7FF; + font-size: 4.5em; +} + +.menu__select__label { + margin-left: 10px; + color: #F7F7FF; + font-family: var(--question-font); + margin-left: 7.5vw; +} + +.menu__select__dropdown{ + width: 85vw; + display: flex; + align-self: center; + margin-top: 10px; + font-family: var(--question-font); + font-size: 1.5em; +} + +.menu__select__description{ + margin-left: 10px; + color: #F7F7FF; + font-family: var(--question-font); + display: flex; + justify-content: center; + font-size: 1.5em; +} + +.menu__select__button { + width: 50vw; + display: flex; + align-self: center; + justify-content: center; + height: 50px; border-radius: 5px; - background-color: white; - border: 2px solid #550527; + border: 2px solid #F7F7FF; + font-size: 100%; + font-family: var(--question-font); + background-color: #DE541E; + color: #F7F7FF +} + +.menu__select__button:hover{ + background-color: #F7F7FF; + color: #DE541E; + border: 2px solid #DE541E; +} + + +.question__container{ + display: flex; + flex-direction: column; + font-family: var(--question-font); + color: #F7F7FF; + height: 90vh; + width: 100vw; +} + +.question__number{ + font-size: 1.5em; + margin-right: 10px; + margin-bottom: 0; + align-self: flex-end; +} + +.question__question__container { + display: flex; + flex-direction: column; + justify-content: center; +} + +.question__question__question { + align-self: center; + font-size: 1.5em; + margin-left: 10px; + text-align: center; +} + +.question__answer__button, +.question__answer__button--correct, +.question__answer__button--incorrect { + display: flex; + width: 50vw; + align-self: center; + height: 50px; + font-size: 100%; + margin-top: 10px; + justify-content: center; +} + + +.question__answer__button{ + color: #F7F7FF; + background-color: #DE541E; +} + +.question__answer__button--correct { + background-color: green; + color: #F7F7FF; +} + +.question__answer__button--incorrect { + background-color: red; + color: #F7F7FF; +} + +.question__correct, +.question__incorrect{ + display: flex; + flex-direction: column; +} + +.question__correct__text, +.question__incorrect__text{ + align-self: center; + font-size: 1.2em; + margin-left: 10px; +} + +.question__correct__image, +.question__incorrect__image { + max-height: 25vh; + max-width: 45vw; + align-self: center; +} + +.points__container { + position: fixed; + display: flex; + height: 5vh; + flex-direction: column; + bottom: 0; + width: 100vw; +} + +.points__number{ + align-self: center; + margin: 0; + color: #F7F7FF; + font-family: var(--question-font); + font-size: 1.5em; +} + +.scoreboard__container { + font-family: var(--question-font); + color: #F7F7FF; + width: 100vw; + display: flex; + flex-direction: column; + align-items: center; + font-size: 1.7em; + margin-left: 10px; } -.voting-button--selected { - color: #FFF; - background-color: #550527; - border: 2px solid white; +.scoreboard__form{ + display: flex; + flex-direction: column; + width: 80vw; } + +.scoreboard__form__input{ + height: 5vh; + font-size: 100%; +} + +.scoreboard__form__button{ + margin-top: 15px; + width: 50vw; + display: flex; + align-self: center; + justify-content: center; + height: 50px; + border-radius: 5px; + border: 2px solid #F7F7FF; + font-size: 1.3em; + font-family: var(--question-font); + background-color: #DE541E; + color: #F7F7FF +} + +.scoreboard__form__button:hover{ + background-color: #F7F7FF; + color: #DE541E; + border: 2px solid #DE541E; +} \ No newline at end of file