diff --git a/.eslintrc.json b/.eslintrc.json index c9c0675c..90119e00 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -26,6 +26,7 @@ "react-hooks" ], "rules": { + "linebreak-style": "off", "react/function-component-definition": [ 2, { diff --git a/README.md b/README.md index d1f22b80..7a2567a9 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,26 @@ Describe how you approached to problem, and what tools and techniques you used t ## View it live -Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about. +https://rococo-pony-134b75.netlify.app/ + +# Running Locally + +This is a short guide on how to run a Redux quiz project locally. + +## Prerequisites + +- [Node.js](https://nodejs.org/en/) installed on your machine +- A code editor like [VSCode](https://code.visualstudio.com/) or [Sublime Text](https://www.sublimetext.com/) + +## Installation + +1. Clone the repository or download the project files to your local machine. +2. Open a terminal and navigate to the project directory. +3. Run `npm install` to install all the project dependencies including `react-redux`, `@reduxjs/toolkit`, and `react-router-dom`. + +## Running the Project + +1. After the installation is complete, run `npm start` to start the development server. +2. Once the server is started, you can view the project in your web browser by navigating to [http://localhost:3000](http://localhost:3000). + +That's it! You now have the Redux project up and running on your local machine with all the required dependencies installed. diff --git a/package-lock.json b/package-lock.json index 8bd1eecb..59e14584 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@babel/eslint-parser": "^7.18.9", - "@reduxjs/toolkit": "^1.8.3", + "@reduxjs/toolkit": "^1.9.3", "eslint": "^8.21.0", "eslint-config-airbnb": "^19.0.4", "eslint-plugin-import": "^2.26.0", @@ -18,7 +18,8 @@ "eslint-plugin-react-hooks": "^4.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-redux": "^8.0.2", + "react-redux": "^8.0.5", + "react-router-dom": "^6.10.0", "react-scripts": "^5.0.1" }, "devDependencies": { @@ -3128,14 +3129,14 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.3.tgz", - "integrity": "sha512-lU/LDIfORmjBbyDLaqFN2JB9YmAT1BElET9y0ZszwhSBa5Ef3t6o5CrHupw5J1iOXwd+o92QfQZ8OJpwXvsssg==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz", + "integrity": "sha512-GU2TNBQVofL09VGmuSioNPQIu6Ml0YLf4EJhgj0AvBadRlCGzUWet8372LjvO4fqKZF2vH1xU0htAa7BrK9pZg==", "dependencies": { - "immer": "^9.0.7", - "redux": "^4.1.2", - "redux-thunk": "^2.4.1", - "reselect": "^4.1.5" + "immer": "^9.0.16", + "redux": "^4.2.0", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.7" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18", @@ -3150,6 +3151,14 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.5.0.tgz", + "integrity": "sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg==", + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -8873,9 +8882,9 @@ } }, "node_modules/immer": { - "version": "9.0.15", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.15.tgz", - "integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==", + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -14448,9 +14457,9 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-redux": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.2.tgz", - "integrity": "sha512-nBwiscMw3NoP59NFCXFf02f8xdo+vSHT/uZ1ldDwF7XaTpzm+Phk97VT4urYBl5TYAPNVaFm12UHAEyzkpNzRA==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz", + "integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==", "dependencies": { "@babel/runtime": "^7.12.1", "@types/hoist-non-react-statics": "^3.3.1", @@ -14499,6 +14508,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.10.0.tgz", + "integrity": "sha512-Nrg0BWpQqrC3ZFFkyewrflCud9dio9ME3ojHCF/WLsprJVzkq3q3UeEhMCAW1dobjeGbWgjNn/PVF6m46ANxXQ==", + "dependencies": { + "@remix-run/router": "1.5.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.10.0.tgz", + "integrity": "sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg==", + "dependencies": { + "@remix-run/router": "1.5.0", + "react-router": "6.10.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -14655,9 +14694,9 @@ } }, "node_modules/redux-thunk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", - "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", "peerDependencies": { "redux": "^4" } @@ -14818,9 +14857,9 @@ "dev": true }, "node_modules/reselect": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.6.tgz", - "integrity": "sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ==" + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz", + "integrity": "sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==" }, "node_modules/resolve": { "version": "1.22.1", @@ -19654,16 +19693,21 @@ } }, "@reduxjs/toolkit": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.3.tgz", - "integrity": "sha512-lU/LDIfORmjBbyDLaqFN2JB9YmAT1BElET9y0ZszwhSBa5Ef3t6o5CrHupw5J1iOXwd+o92QfQZ8OJpwXvsssg==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz", + "integrity": "sha512-GU2TNBQVofL09VGmuSioNPQIu6Ml0YLf4EJhgj0AvBadRlCGzUWet8372LjvO4fqKZF2vH1xU0htAa7BrK9pZg==", "requires": { - "immer": "^9.0.7", - "redux": "^4.1.2", - "redux-thunk": "^2.4.1", - "reselect": "^4.1.5" + "immer": "^9.0.16", + "redux": "^4.2.0", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.7" } }, + "@remix-run/router": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.5.0.tgz", + "integrity": "sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg==" + }, "@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -23941,9 +23985,9 @@ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" }, "immer": { - "version": "9.0.15", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.15.tgz", - "integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==" + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" }, "import-fresh": { "version": "3.3.0", @@ -27880,9 +27924,9 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "react-redux": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.2.tgz", - "integrity": "sha512-nBwiscMw3NoP59NFCXFf02f8xdo+vSHT/uZ1ldDwF7XaTpzm+Phk97VT4urYBl5TYAPNVaFm12UHAEyzkpNzRA==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz", + "integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==", "requires": { "@babel/runtime": "^7.12.1", "@types/hoist-non-react-statics": "^3.3.1", @@ -27905,6 +27949,23 @@ "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "dev": true }, + "react-router": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.10.0.tgz", + "integrity": "sha512-Nrg0BWpQqrC3ZFFkyewrflCud9dio9ME3ojHCF/WLsprJVzkq3q3UeEhMCAW1dobjeGbWgjNn/PVF6m46ANxXQ==", + "requires": { + "@remix-run/router": "1.5.0" + } + }, + "react-router-dom": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.10.0.tgz", + "integrity": "sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg==", + "requires": { + "@remix-run/router": "1.5.0", + "react-router": "6.10.0" + } + }, "react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -28030,9 +28091,9 @@ } }, "redux-thunk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", - "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", "requires": {} }, "regenerate": { @@ -28160,9 +28221,9 @@ "dev": true }, "reselect": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.6.tgz", - "integrity": "sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ==" + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz", + "integrity": "sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==" }, "resolve": { "version": "1.22.1", diff --git a/package.json b/package.json index 58da74eb..9b4d7349 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@babel/eslint-parser": "^7.18.9", - "@reduxjs/toolkit": "^1.8.3", + "@reduxjs/toolkit": "^1.9.3", "eslint": "^8.21.0", "eslint-config-airbnb": "^19.0.4", "eslint-plugin-import": "^2.26.0", @@ -13,7 +13,8 @@ "eslint-plugin-react-hooks": "^4.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-redux": "^8.0.2", + "react-redux": "^8.0.5", + "react-router-dom": "^6.10.0", "react-scripts": "^5.0.1" }, "scripts": { diff --git a/public/assets/linkedinlogo.png b/public/assets/linkedinlogo.png new file mode 100644 index 00000000..50aeee7c Binary files /dev/null and b/public/assets/linkedinlogo.png differ diff --git a/src/App.js b/src/App.js index 690bd373..bcd97f4a 100644 --- a/src/App.js +++ b/src/App.js @@ -1,9 +1,11 @@ import React from 'react'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { Provider } from 'react-redux'; import { combineReducers, configureStore } from '@reduxjs/toolkit'; import { quiz } from 'reducers/quiz'; +import { WelcomePage } from 'pages/WelcomePage'; -import { CurrentQuestion } from 'components/CurrentQuestion'; +import { Question } from 'pages/Question'; const reducer = combineReducers({ quiz: quiz.reducer @@ -14,7 +16,12 @@ const store = configureStore({ reducer }); export const App = () => { return ( - + + + } /> + } /> + + ); } diff --git a/src/components/AnswerButton.js b/src/components/AnswerButton.js new file mode 100644 index 00000000..ce36f233 --- /dev/null +++ b/src/components/AnswerButton.js @@ -0,0 +1,47 @@ +/* eslint-disable max-len */ +/* eslint-disable no-nested-ternary */ +/* eslint-disable no-shadow */ +import React, { useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { quiz } from '../reducers/quiz'; + +export const AnswerButton = ({ index, option, setGoToNextButton }) => { + const dispatch = useDispatch(); + const [activeBtn, setActiveBtn] = useState(false); + + // Gets all question in the store + const question = useSelector((state) => state.quiz.questions[state.quiz.currentQuestionIndex]); + + // Gets the answers the user has given + const usersAnswer = useSelector((state) => state.quiz.answers[state.quiz.currentQuestionIndex]); + + const onAnswerSubmit = (questionId, answerIndex) => { + dispatch(quiz.actions.submitAnswer({ + questionId, answerIndex + })); + if (question.correctAnswerIndex === answerIndex) { + window.alert('Yay YOU!') + dispatch(quiz.actions.goToNextQuestion()) + } else { + window.alert('Wrong answer!') + } + setActiveBtn(true); + setGoToNextButton(true); + } + + const correctAnswer = usersAnswer && index === question.correctAnswerIndex; + + return ( + /* If activeBtn is true, it checks whether the answer is correct or incorrect based on the correctAnswer variable. + If correctAnswer is true, it sets the class to 'correct', which applies the appropriate styles to indicate a correct answer. + If correctAnswer is false, it sets the class to 'wrong', which applies the appropriate styles to indicate an incorrect answer. + If activeBtn is false, it sets the class to 'default', which applies the default styles to the button. */ + + ); +}; diff --git a/src/components/CurrentQuestion.css b/src/components/CurrentQuestion.css new file mode 100644 index 00000000..1d91b898 --- /dev/null +++ b/src/components/CurrentQuestion.css @@ -0,0 +1,32 @@ +.eslint-hater h1 { + font-family: futura-pt, 'sans-serif'; + font-weight: 900; + letter-spacing: -0.01em; + color: rgb(0, 24, 164); + text-align: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.answerButton { + place-self: center; +} + +.answer-buttons { + display: flex; + flex-direction: column; + gap: 10px; +} + +.eslint-hater-bottom { + display: flex; + flex-direction: column; + gap: 11px; +} + +.eslint-hater { + display: flex; + flex-direction: column; + align-items: center; +} diff --git a/src/components/CurrentQuestion.js b/src/components/CurrentQuestion.js index 36ee2224..2055b0c9 100644 --- a/src/components/CurrentQuestion.js +++ b/src/components/CurrentQuestion.js @@ -1,16 +1,51 @@ -import React from 'react' -import { useSelector } from 'react-redux' +import React, { useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { NextButton } from './NextButton'; +import { ProgressBar } from './ProgressBar'; +import { AnswerButton } from './AnswerButton'; +import { SummaryPage } from '../pages/Summary'; +import { quiz } from '../reducers/quiz'; +import './CurrentQuestion.css'; export const CurrentQuestion = () => { - const question = useSelector((state) => state.quiz.questions[state.quiz.currentQuestionIndex]) + console.log(quiz); + const dispatch = useDispatch(); + const [goToNextButton, setGoToNextButton] = useState(false); + const question = useSelector( + (state) => state.quiz.questions[state.quiz.currentQuestionIndex] + ); + const quizOver = useSelector((state) => state.quiz.quizOver); if (!question) { - return

Oh no! I could not find the current question!

+ return

Oh no! I could not find the current question!

; } + const moveToNext = () => { + dispatch(quiz.actions.goToNextQuestion()); + setGoToNextButton(false); + }; return ( -
-

Question: {question.questionText}

-
- ) -} + <> + {quizOver && } + {!quizOver && ( +
+

Question: {question.questionText}

+
+ {question.options.map((option, index) => ( + + ))} +
+
+ {goToNextButton && } + +
+
+ )} + + ); +}; \ No newline at end of file diff --git a/src/components/Footer.css b/src/components/Footer.css new file mode 100644 index 00000000..40abe06d --- /dev/null +++ b/src/components/Footer.css @@ -0,0 +1,51 @@ +/* Footer */ + +footer { + /* margin-top: 20px; + height: 120px; + padding: 5px; */ + display: flex; + flex-direction: column; + align-self: flex-end; + border-color: rgb(0, 37, 255); + border-width: 0px 16px 16px 16px; + border-style: solid; + } + + footer p { + text-align: center; + color: black; + text-shadow: 1px 1px 2px white; + } + + .contact-me { + display:flex; + justify-content: center; + align-items: center; + gap: 5px; + padding: 5px; + /* + margin-top: 10px; */ + } + + .contact-logo{ + width: 20px; + height: 20px; + object-fit: cover; + opacity: .75; + } + + .contact-logo:hover{ + transform: scale(1.2); + opacity: 1; + } + + .about-me { + display: flex; + justify-content: center; + align-items: center; + font-size: 15px; + color: rgb(0, 24, 164); + text-shadow: 1px 1px 2px white; + text-align: center; + } \ No newline at end of file diff --git a/src/components/NextButton.css b/src/components/NextButton.css new file mode 100644 index 00000000..40a86b90 --- /dev/null +++ b/src/components/NextButton.css @@ -0,0 +1,27 @@ +.nextButton { + cursor: pointer; + text-underline-offset: 6px; + font-family: futura-pt, "sans-serif"; + font-weight: 600; + transition: all 0.2s ease 0s; + /* display: inline-block; */ + padding: 18px 30px; + border-radius: 50px; + text-decoration: none; + font-size: 18px; + border: 2px solid rgb(10, 37, 255); + /* place-self: flex-start; */ + color: rgb(10, 37, 255); + background: transparent; + align-self: center; + margin-top:40px; + margin-bottom:40px; + /* this is the important stuff 👇 */ +animation: bounce 1s infinite alternate; +} + +@keyframes bounce { + 0% {transform: translateY(0);} + 100% {transform: translateY(50px);} + } + diff --git a/src/components/NextButton.js b/src/components/NextButton.js new file mode 100644 index 00000000..faf7886b --- /dev/null +++ b/src/components/NextButton.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { quiz } from '../reducers/quiz'; +import './NextButton.css'; + +export const NextButton = () => { + const dispatch = useDispatch(); + const goToNextQuestion = () => { + dispatch(quiz.actions.goToNextQuestion()); + }; + return ( + + ); +}; diff --git a/src/components/ProgressBar.css b/src/components/ProgressBar.css new file mode 100644 index 00000000..09fa4692 --- /dev/null +++ b/src/components/ProgressBar.css @@ -0,0 +1,25 @@ +.progress-bar { + font-family: futura-pt, 'sans-serif'; + letter-spacing: -0.01em; + font-size: 32px; + line-height: min(max(50px, 6vw), 53px); + color: rgb(0, 24, 164); +} +.progress { + width: 100%; + height: 10px; + appearance: none; + background-color: #f4f4f4; + border-radius: 20px; + overflow: hidden; +} + +.progress::-webkit-progress-bar { + background-color: #f4f4f4; + border-radius: 20px; +} + +.progress::-webkit-progress-value { + background-color: rgba(0, 37, 255, 1); + border-radius: 20px; +} diff --git a/src/components/ProgressBar.js b/src/components/ProgressBar.js new file mode 100644 index 00000000..12eec768 --- /dev/null +++ b/src/components/ProgressBar.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import './ProgressBar.css'; + +export const ProgressBar = () => { + const questions = useSelector((state) => state.quiz.questions); + const currentQuestionIndex = useSelector((state) => { + return state.quiz.questions[state.quiz.currentQuestionIndex]; + }); + + const progress = (currentQuestionIndex.id / questions.length) * 100; + + return ( +
+

+ Question: {currentQuestionIndex.id} / {questions.length} +

+ + {progress}% + +
+ ); +}; diff --git a/src/components/ResetButton.css b/src/components/ResetButton.css new file mode 100644 index 00000000..2374b707 --- /dev/null +++ b/src/components/ResetButton.css @@ -0,0 +1,35 @@ +.resetButton { + cursor: pointer; + text-underline-offset: 6px; + /*font-family: futura-pt, 'sans-serif'; */ + font-weight: 600; + transition: all 0.2s ease 0s; + display: inline-block; + padding: 18px 30px; + border-radius: 50px; + text-decoration: none; + font-size: 18px; + border: 2px solid rgb(10, 37, 255); + place-self: flex-start; + color: rgb(255, 255, 255); + background: rgb(10, 37, 255); + + /* this is the important stuff 👇 */ + animation: bounce 1s infinite alternate; +} + +@keyframes bounce { + 0% { + transform: translateY(0); + } + 100% { + transform: translateY(50px); + } +} +.resetButtonContainer { + display: flex; + justify-content: center; + align-items: center; + margin-top: 40px; + margin-bottom: 40px; +} diff --git a/src/components/ResetButton.js b/src/components/ResetButton.js new file mode 100644 index 00000000..3849fdbb --- /dev/null +++ b/src/components/ResetButton.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { quiz } from '../reducers/quiz'; +import './ResetButton.css'; + +export const ResetButton = () => { + const dispatch = useDispatch(); + const restart = () => { + dispatch(quiz.actions.restart()); + }; + return ( +
+ +
+ ); +}; diff --git a/src/components/answerbutton.css b/src/components/answerbutton.css new file mode 100644 index 00000000..6257da33 --- /dev/null +++ b/src/components/answerbutton.css @@ -0,0 +1,30 @@ +/* button { + background-color: #3498db; + color: #fff; + font-size: 16px; + padding: 12px 24px; + border-radius: 12px; + border: none; + cursor: pointer; +} + +button.default:hover { + background-color: #2980b9; +} + +button.default:active { + background-color: #21618c; +} + +button.correct { + background-color: #2ecc71; +} + +button.wrong { + background-color: #e74c3c; +} + +button.default { + background-color: #bdc3c7; +} +*/ \ No newline at end of file diff --git a/src/components/footer.js b/src/components/footer.js new file mode 100644 index 00000000..5896e69a --- /dev/null +++ b/src/components/footer.js @@ -0,0 +1,17 @@ +import React from 'react'; +import './Footer.css'; + +export const Footer = () => { + return ( + + ) +} \ No newline at end of file diff --git a/src/img/tech.png b/src/img/tech.png new file mode 100644 index 00000000..0d2911e9 Binary files /dev/null and b/src/img/tech.png differ diff --git a/src/img/technigo.jpg b/src/img/technigo.jpg new file mode 100644 index 00000000..2e16f73c Binary files /dev/null and b/src/img/technigo.jpg differ diff --git a/src/pages/Question.js b/src/pages/Question.js new file mode 100644 index 00000000..7aaf870c --- /dev/null +++ b/src/pages/Question.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { CurrentQuestion } from 'components/CurrentQuestion'; +import './question.css'; + +export const Question = () => { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/src/pages/Summary.js b/src/pages/Summary.js new file mode 100644 index 00000000..804853aa --- /dev/null +++ b/src/pages/Summary.js @@ -0,0 +1,39 @@ +/* eslint-disable implicit-arrow-linebreak */ +/* eslint-disable react/jsx-no-useless-fragment */ +import React from 'react'; +import { ResetButton } from 'components/ResetButton'; +import { useSelector } from 'react-redux'; +import './summary.css'; + +export const SummaryPage = () => { + // Variable to calculate how many correct answer the user has given + const correctAnswer = useSelector( + (state) => + state.quiz.answers.filter((answer) => answer.isCorrect === true).length + ); + const answers = useSelector((state) => state.quiz.answers); + + const userSummary = () => { + if (correctAnswer === 5) { + return 'Youa are a genius 🧠'; + } else if (correctAnswer === 4) { + return 'Well done 👌'; + } else if (correctAnswer === 3) { + return 'Well done 👌'; + } else { + return 'You need to study more 🤦'; + } + }; + + // console.log(correctAnswer); + return ( +
+

Oh wow, what a ride!

+

+ Your final score was: {correctAnswer} out of {answers.length} +

+ +

{userSummary()}

+
+ ); +}; diff --git a/src/pages/WelcomePage.js b/src/pages/WelcomePage.js new file mode 100644 index 00000000..47624151 --- /dev/null +++ b/src/pages/WelcomePage.js @@ -0,0 +1,26 @@ +import React from 'react'; +import './welcomepage.css'; +import { useNavigate } from 'react-router-dom'; +import { Footer } from 'components/footer'; + +export const WelcomePage = () => { + const navigate = useNavigate(); + const goToQuiz = () => { + navigate('/quiz'); + }; + return ( +
+
+
+

Hello there friend 👋

+

Ready to Rumble with React?

+
+ +
+
+
+ ); +}; diff --git a/src/pages/question.css b/src/pages/question.css new file mode 100644 index 00000000..3f97b3cd --- /dev/null +++ b/src/pages/question.css @@ -0,0 +1,6 @@ +.questions-background { + height: 100vh; + border-color: rgb(0, 37, 255); + border-width: 16px 16px 0px 16px; + border-style: solid; +} \ No newline at end of file diff --git a/src/pages/summary.css b/src/pages/summary.css new file mode 100644 index 00000000..ee85ec66 --- /dev/null +++ b/src/pages/summary.css @@ -0,0 +1,22 @@ +.summary { + height: 100vh; + border-color: rgb(0, 37, 255); + border-width: 16px 16px 16px 16px; + border-style: solid; + display: flex; + flex-direction: column; +} + +.summary h1, h3 { + font-family: futura-pt, "sans-serif"; + font-weight: 900; + letter-spacing: -0.01em; + color: rgb(0, 24, 164); + text-align: center; + padding-top: 40px; +} + +.summary p { + padding-top: 40px; + text-align: center; +} \ No newline at end of file diff --git a/src/pages/welcomepage.css b/src/pages/welcomepage.css new file mode 100644 index 00000000..d2ed2ad1 --- /dev/null +++ b/src/pages/welcomepage.css @@ -0,0 +1,53 @@ +.welcome-background { + height: 400px; + background-image: url(../img/tech.png); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + /* background-position: center -550px; */ +} + +.content { + text-align: center; + padding-top: 16px; + border-color: rgb(0, 37, 255); + border-width: 16px 16px 0px 16px; + border-style: solid; +} +.welcome-text-container h1 { + font-family: futura-pt, 'sans-serif'; + font-weight: 900; + letter-spacing: -0.01em; + color: rgb(0, 24, 164); +} + +button { + cursor: pointer; + text-underline-offset: 6px; + /*font-family: futura-pt, 'sans-serif'; */ + font-weight: 600; + transition: all 0.2s ease 0s; + display: inline-block; + padding: 18px 30px; + border-radius: 50px; + text-decoration: none; + font-size: 18px; + border: 2px solid rgb(10, 37, 255); + /* place-self: flex-start; */ + color: rgb(255, 255, 255); + background: rgb(10, 37, 255); + align-self: center; +} +.welcomeButton { + /* this is the important stuff 👇 */ + animation: bounce 1s infinite alternate; +} + +@keyframes bounce { + 0% { + transform: translateY(0); + } + 100% { + transform: translateY(50px); + } +} diff --git a/src/reducers/quiz.js b/src/reducers/quiz.js index a38bbf68..cf8d92d4 100644 --- a/src/reducers/quiz.js +++ b/src/reducers/quiz.js @@ -1,23 +1,76 @@ -import { createSlice } from '@reduxjs/toolkit' +/* eslint-disable comma-dangle */ +import { createSlice } from '@reduxjs/toolkit'; // Change these to your own questions! const questions = [ - { id: 1, questionText: 'Who set the Olympic record for the 100m dash in 2012?', options: ['Usain Bolt', 'Justin Gatlin', 'Tyson Gay', 'Asafa Powell'], correctAnswerIndex: 0 }, - { id: 2, questionText: 'When was Michael Phelps last named male World Swimmer of the Year?', options: ['2012', '2014', '2016', '2018'], correctAnswerIndex: 2 } -] + { + id: 1, + questionText: 'What is React?', + options: [ + 'A front-end JavaScript library', + 'A back-end programming language', + 'A mobile application development platform', + 'A database management system', + ], + correctAnswerIndex: 0, + }, + { + id: 2, + questionText: 'What is JSX?', + options: [ + 'A type of HTML used in React components', + 'A JavaScript extension for writing React code', + 'A CSS framework for styling React components', + 'A backend server for React applications', + ], + correctAnswerIndex: 1, + }, + { + id: 3, + questionText: 'What is the Virtual DOM?', + options: [ + 'A real-time representation of the HTML DOM used for performance optimization', + 'A way to manage multiple React components on a page', + 'A tool for debugging React applications', + 'A database used to store React component data', + ], + correctAnswerIndex: 0, + }, + { + id: 4, + questionText: 'What is a React component?', + options: [ + 'A small, reusable piece of code that can be rendered on a web page', + 'A programming language used to create React applications', + 'A database used to store React component data', + 'A tool for debugging React applications', + ], + correctAnswerIndex: 0, + }, + { + id: 5, + questionText: 'What is the purpose of state in React?', + options: [ + 'To store and manage data that can change over time', + 'To render components on the page', + 'To handle user events like clicks and keyboard inputs', + 'To control the overall layout of the web page', + ], + correctAnswerIndex: 0, + }, +]; const initialState = { questions, answers: [], currentQuestionIndex: 0, - quizOver: false -} + quizOver: false, +}; export const quiz = createSlice({ name: 'quiz', initialState, reducers: { - /** * Use this action when a user selects an answer to the question. * The answer will be stored in the `quiz.answers` state with the @@ -34,15 +87,19 @@ export const quiz = createSlice({ * and `answerIndex` keys. See the readme for more details. */ submitAnswer: (state, action) => { - const { questionId, answerIndex } = action.payload - const question = state.questions.find((q) => q.id === questionId) + const { questionId, answerIndex } = action.payload; + const question = state.questions.find((q) => q.id === questionId); if (!question) { - throw new Error('Could not find question! Check to make sure you are passing the question id correctly.') + throw new Error( + 'Could not find question! Check to make sure you are passing the question id correctly.' + ); } if (question.options[answerIndex] === undefined) { - throw new Error(`You passed answerIndex ${answerIndex}, but it is not in the possible answers array!`) + throw new Error( + `You passed answerIndex ${answerIndex}, but it is not in the possible answers array!` + ); } state.answers.push({ @@ -50,8 +107,8 @@ export const quiz = createSlice({ answerIndex, question, answer: question.options[answerIndex], - isCorrect: question.correctAnswerIndex === answerIndex - }) + isCorrect: question.correctAnswerIndex === answerIndex, + }); }, /** @@ -63,9 +120,9 @@ export const quiz = createSlice({ */ goToNextQuestion: (state) => { if (state.currentQuestionIndex + 1 === state.questions.length) { - state.quizOver = true + state.quizOver = true; } else { - state.currentQuestionIndex += 1 + state.currentQuestionIndex += 1; } }, @@ -77,8 +134,7 @@ export const quiz = createSlice({ * This action does not require a payload. */ restart: () => { - return initialState - } - - } -}) + return initialState; + }, + }, +});