├── .babelrc ├── .eslintrc.json ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── index.html └── style.css └── src ├── actions └── actions.js ├── components ├── app.js ├── button.js ├── card.js ├── score.js └── timer.js ├── containers └── appContainer.js ├── index.js ├── reducers └── reducers.js ├── store └── index.js └── utils └── getQuiz.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"], 3 | "plugins": ["transform-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .cache/ 4 | .vscode/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quiz Game (in React!) 2 | 3 | ### Play the Quiz at: http://taha-quiz.netlify.com/ 4 | 5 | ### A timed quiz app built by Haydn n' Tammy 6 | * Built with React.js 7 | * Mobile-first design approach 8 | * API calls to opentdb - the [Open Trivia Database](https://opentdb.com/) 9 | * Set a timer and answer questions 10 | * Choose from a predefined list of categories 11 | * Set the difficulty level 12 | 13 | ### Categories: 14 | * Science and Nature 15 | * Celebrities 16 | * General Knowlege 17 | * Sports 18 | * Animals 19 | * Mythology 20 | 21 | ### User Journey 22 | * land on page 23 | * select a category 24 | * start quiz: 25 | * 10 questions appear in sequence 26 | * answer each question in turn before timeout 27 | * difficulty level increases as decrease of time per question? 28 | * see your score on display/dashboard 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mad-skillzzzz", 3 | "version": "1.0.0", 4 | "description": "a timed quiz app - Haydn n' Tammy", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "test:watch": "jest --watch", 9 | "test:coverage": "jest --coverage", 10 | "start": "parcel public/index.html", 11 | "build": "parcel build public/index.html" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/fac-13/mad-skillzzzz.git" 16 | }, 17 | "author": "Haydn Appleby, Tammy Speed", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/fac-13/mad-skillzzzz/issues" 21 | }, 22 | "homepage": "https://github.com/fac-13/mad-skillzzzz#readme", 23 | "devDependencies": { 24 | "babel-core": "^6.26.3", 25 | "babel-eslint": "^8.2.3", 26 | "babel-jest": "^22.4.4", 27 | "babel-plugin-transform-class-properties": "^6.24.1", 28 | "babel-preset-env": "^1.7.0", 29 | "babel-preset-react": "^6.24.1", 30 | "eslint": "^4.19.1", 31 | "eslint-config-recommended": "^2.0.0", 32 | "eslint-plugin-react": "^7.8.2", 33 | "jest": "^22.4.4", 34 | "parcel-bundler": "^1.8.1" 35 | }, 36 | "dependencies": { 37 | "react": "^16.4.1", 38 | "react-dom": "^16.3.2", 39 | "react-redux": "^5.0.7", 40 | "redux": "^4.0.0", 41 | "style.css": "^1.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Document 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | font-family: 'Mako', sans-serif; 4 | } 5 | 6 | body { 7 | background-color: rgb(33, 62, 105); 8 | height: 100%; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | .app { 14 | position: absolute; 15 | top: 0; 16 | bottom: 0; 17 | left: 0; 18 | right: 0; 19 | display: flex; 20 | flex-direction: column; 21 | justify-content: center; 22 | align-items: center; 23 | } 24 | 25 | h1 { 26 | color: white; 27 | font-size: 2rem; 28 | } 29 | 30 | .card { 31 | display: flex; 32 | flex-direction: column; 33 | justify-content: center; 34 | align-items: center; 35 | color: #fff; 36 | max-width: 80%; 37 | } 38 | 39 | .card header { 40 | align-self: flex-end; 41 | text-transform: uppercase; 42 | } 43 | 44 | .card p { 45 | font-weight: bolder; 46 | font-size: 1.5rem; 47 | word-wrap: none; 48 | } 49 | 50 | .scoreCard { 51 | color: white; 52 | text-align: center; 53 | font-size: 2rem; 54 | } 55 | 56 | .button { 57 | display: block; 58 | width: 15rem; 59 | padding: 3px; 60 | background-color: rgb(223, 223, 223); 61 | margin: 0.75rem; 62 | padding: 0.7rem; 63 | border-radius: 5px; 64 | color: black; 65 | font-size: 1.3rem; 66 | border: none; 67 | transition: padding 0.3s; 68 | font-family: 'Mako', sans-serif; 69 | } 70 | 71 | button:focus, 72 | button:hover { 73 | background-color: rgb(38, 100, 32); 74 | color: white; 75 | outline: none; 76 | } 77 | -------------------------------------------------------------------------------- /src/actions/actions.js: -------------------------------------------------------------------------------- 1 | export const setQuizQuestions = data => { 2 | return { 3 | type: 'SET_QUIZ_QUESTIONS', 4 | quizData: data 5 | }; 6 | }; 7 | 8 | export const incrementRightAnswers = () => { 9 | return { 10 | type: 'INCREMENT_RIGHT_ANSWERS' 11 | }; 12 | }; 13 | 14 | export const updateCurrentQuestion = currentQuestion => { 15 | return { 16 | type: 'UPDATE_CURRENT_QUESTION', 17 | currentQuestion 18 | }; 19 | }; 20 | 21 | export const markCategorySelected = () => { 22 | return { 23 | type: 'MARK_CATEGORY_SELECTED' 24 | }; 25 | }; 26 | 27 | export const resetGame = () => { 28 | return { 29 | type: 'RESET_GAME' 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from './button'; 3 | import Card from './card'; 4 | import { getQuiz, getSession } from '../utils/getQuiz'; 5 | import Score from './score'; 6 | 7 | const quizzes = [ 8 | { id: 17, title: 'Science and Nature' }, 9 | { id: 26, title: 'Celebrities' }, 10 | { id: 21, title: 'Sports' }, 11 | { id: 27, title: 'Animals' }, 12 | { id: 20, title: 'Mythology' }, 13 | { id: 9, title: 'General Knowledge' } 14 | ]; 15 | 16 | export default class App extends React.Component { 17 | constructor(props) { 18 | super(props); 19 | this.checkAnswer = this.checkAnswer.bind(this); 20 | this.restartGame = this.restartGame.bind(this); 21 | this.sessionToken = null; 22 | } 23 | 24 | componentDidMount() { 25 | getSession().then(session => { 26 | this.sessionToken = session.token; 27 | }); 28 | } 29 | 30 | fetchCategory(categoryId) { 31 | return () => { 32 | const { setQuizData, markCategorySelected } = this.props; 33 | getQuiz(categoryId, this.sessionToken) 34 | .then(quizData => setQuizData(quizData.results)) 35 | .then(() => markCategorySelected()); 36 | }; 37 | } 38 | 39 | restartGame() { 40 | const { resetGame } = this.props; 41 | resetGame(); 42 | } 43 | 44 | checkAnswer(answer, correctAnswer) { 45 | const { 46 | incrementRightAnswers, 47 | updateCurrentQuestion, 48 | currentQuestion 49 | } = this.props; 50 | return () => { 51 | if (answer === correctAnswer) { 52 | incrementRightAnswers(); 53 | } 54 | updateCurrentQuestion(currentQuestion); 55 | }; 56 | } 57 | 58 | populateQuizCard = (record, index) => { 59 | const { correct_answer, incorrect_answers, difficulty, question } = record; 60 | return ( 61 | atob(x))} 69 | /> 70 | ); 71 | }; 72 | 73 | render() { 74 | const { 75 | quizData, 76 | categorySelected, 77 | rightAnswers, 78 | currentQuestion 79 | } = this.props; 80 | return ( 81 |
82 | {!categorySelected &&

Pick a Category

} 83 | {!categorySelected && 84 | quizzes.map((item, i) => { 85 | return ( 86 | 93 | ); 94 | })} 95 | {quizData && currentQuestion < 10 96 | ? this.populateQuizCard(quizData[currentQuestion], currentQuestion) 97 | : ''} 98 | {quizData && currentQuestion === 10 ? ( 99 | 100 | ) : ( 101 | '' 102 | )} 103 |
104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/components/button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Button = ({ onClick, children, id }) => { 4 | return ( 5 | 8 | ); 9 | }; 10 | 11 | export default Button; 12 | -------------------------------------------------------------------------------- /src/components/card.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from './button'; 3 | import Timer from './timer'; 4 | 5 | export default class Card extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | const { 12 | difficulty, 13 | question, 14 | duration, 15 | wrongAnswers, 16 | correctAnswer, 17 | checkAnswerFn 18 | } = this.props; 19 | const answers = [correctAnswer].concat(wrongAnswers).sort(); 20 | return ( 21 |
22 |
23 | {difficulty} | 24 | 25 |
26 |
27 |

{question}

28 |
29 |
30 | {answers.map((answer, i) => { 31 | return ( 32 | 35 | ); 36 | })} 37 |
38 |
39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/score.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from './button'; 3 | 4 | const Score = ({ refresh, score }) => { 5 | return ( 6 |
7 |

Your Score

8 |

{score}

9 | 10 |
11 | ) 12 | } 13 | 14 | export default Score; -------------------------------------------------------------------------------- /src/components/timer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Timer extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | seconds: 0 8 | }; 9 | } 10 | 11 | tick() { 12 | const { duration, timeoutFn } = this.props; 13 | if (this.state.seconds === duration) { 14 | timeoutFn(); 15 | } else { 16 | this.setState((prevState) => ({ 17 | seconds: prevState.seconds + 1 18 | })); 19 | } 20 | } 21 | 22 | componentDidMount() { 23 | this.interval = setInterval(() => this.tick(), 1000); 24 | } 25 | 26 | componentWillUnmount() { 27 | clearInterval(this.interval); 28 | } 29 | 30 | render() { 31 | const { duration } = this.props; 32 | let timeLeft = duration - this.state.seconds; 33 | return Time Left: {timeLeft}; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/containers/appContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from '../components/app'; 3 | import { connect } from 'react-redux'; 4 | import { 5 | setQuizQuestions, 6 | markCategorySelected, 7 | resetGame, 8 | incrementRightAnswers, 9 | updateCurrentQuestion, 10 | currentQuestion 11 | } from '../actions/actions'; 12 | 13 | const mapStateToProps = state => { 14 | return { 15 | quizData: state.quizData, 16 | categorySelected: state.categorySelected, 17 | currentQuestion: state.currentQuestion, 18 | rightAnswers: state.rightAnswers 19 | }; 20 | }; 21 | 22 | const mapDispatchToProps = dispatch => { 23 | return { 24 | setQuizData: quizData => dispatch(setQuizQuestions(quizData)), 25 | markCategorySelected: () => dispatch(markCategorySelected()), 26 | resetGame: () => dispatch(resetGame()), 27 | incrementRightAnswers: () => dispatch(incrementRightAnswers()), 28 | updateCurrentQuestion: () => 29 | dispatch(updateCurrentQuestion(currentQuestion)) 30 | }; 31 | }; 32 | 33 | const AppContainer = connect( 34 | mapStateToProps, 35 | mapDispatchToProps 36 | )(App); 37 | 38 | export default AppContainer; 39 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import '../public/style.css'; 4 | 5 | import { store } from './store/index.js'; 6 | import AppContainer from './containers/appContainer'; 7 | 8 | ReactDOM.render( 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /src/reducers/reducers.js: -------------------------------------------------------------------------------- 1 | import { 2 | setQuizQuestions, 3 | incrementRightAnswers, 4 | updateCurrentQuestion, 5 | markCategorySelected, 6 | resetGame 7 | } from '../actions/actions'; 8 | 9 | export const initialState = { 10 | quizData: null, 11 | rightAnswers: 0, 12 | currentQuestion: 0, 13 | categorySelected: false 14 | }; 15 | 16 | export const updateState = (state = initialState, action) => { 17 | switch (action.type) { 18 | case 'SET_QUIZ_QUESTIONS': 19 | return Object.assign({}, state, { 20 | quizData: action.quizData 21 | }); 22 | case 'INCREMENT_RIGHT_ANSWERS': 23 | return Object.assign({}, state, { 24 | rightAnswers: state.rightAnswers + 1 25 | }); 26 | case 'UPDATE_CURRENT_QUESTION': 27 | return Object.assign({}, state, { 28 | currentQuestion: state.currentQuestion + 1 29 | }); 30 | case 'MARK_CATEGORY_SELECTED': 31 | return Object.assign({}, state, { 32 | categorySelected: true 33 | }); 34 | case 'RESET_GAME': 35 | return Object.assign({}, initialState); 36 | default: 37 | return state; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import { updateState } from '../reducers/reducers'; 3 | 4 | export const store = createStore(updateState); 5 | -------------------------------------------------------------------------------- /src/utils/getQuiz.js: -------------------------------------------------------------------------------- 1 | const checkResponse = (response) => { 2 | if (response.status !== 200) { 3 | console.log(`Error with the request! ${response.status}`); 4 | } 5 | return response.json(); 6 | }; 7 | 8 | export const getSession = () => { 9 | return fetch(`https://opentdb.com/api_token.php?command=request`) 10 | .then(checkResponse) 11 | .catch((err) => { 12 | throw new Error(`fetching session token failed ${err}`); 13 | }); 14 | }; 15 | 16 | export const getQuiz = (categoryId, sessionToken) => { 17 | return fetch( 18 | `https://opentdb.com/api.php?amount=10&category=${categoryId}&encode=base64&token=${sessionToken}` 19 | ) 20 | .then(checkResponse) 21 | .catch((err) => { 22 | throw new Error(`fetching the quiz failed ${err}`); 23 | }); 24 | }; 25 | --------------------------------------------------------------------------------