├── .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 |
29 |
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 |
--------------------------------------------------------------------------------