diff --git a/src/meta/play-meta.js b/src/meta/play-meta.js index f701e1c8ad..c4f72248b7 100644 --- a/src/meta/play-meta.js +++ b/src/meta/play-meta.js @@ -15,6 +15,7 @@ import { PasswordGenerator, WhyTypescript, NetlifyCardGame, +FunQuiz, //import play here } from "plays"; @@ -230,5 +231,18 @@ export const plays = [ video: '', language: 'js', featured: true, + }, { + id: 'pl-fun-quiz', + name: 'Fun Quiz', + description: 'Its a Fun Quiz app which lets player to choose desirable category to answer 20 unique question with 4 options and pick the correct one.', + component: () => {return }, + path: '/plays/fun-quiz', + level: 'Intermediate', + tags: 'QuizApp,FunQuiz,API', + github: 'Angryman18', + cover: 'https://cdn.pixabay.com/photo/2019/05/22/22/28/brainstorm-4222728_960_720.jpg', + blog: 'https://hashnode.com/@imsmahanta', + video: '', + language: 'js' }, //replace new play item here ]; diff --git a/src/plays/fun-quiz/EndScreen.jsx b/src/plays/fun-quiz/EndScreen.jsx new file mode 100644 index 0000000000..f8fdaf908c --- /dev/null +++ b/src/plays/fun-quiz/EndScreen.jsx @@ -0,0 +1,91 @@ +// vendors +import { Fragment, useState } from "react"; + +//css +import "./FrontScreen.scss"; + +const EndScreen = ({ quizSummary, redirectHome }) => { + const { correctAnswers, cheatedAnswers, wrongAnswers, result } = quizSummary; + const [currentQuestion, setCurrentQuestion] = useState({}); + + const ShowCurrentQuestionDetails = ({ currentQuestion }) => { + if (!Object.keys(currentQuestion).length) return false; + return ( +
+
Question: {currentQuestion?.qNo}
+
  • + Ans: ${currentQuestion?.correct_answer}
    `, + }} + /> + Your Answer: ${currentQuestion?.your_answer}`, + }} + /> +
  • + ); + }; + + return ( + +
    +
    +

    Quiz Summary

    + {!cheatedAnswers ? ( +

    Congratulations!

    + ) : ( +

    You Cheated!

    + )} +
    +
    +

    + {correctAnswers} + Correct Answers +

    + {!!cheatedAnswers && ( +

    ({cheatedAnswers} cheated)

    + )} +
    +
    + {wrongAnswers} + Wrong Answers +
    +
    +
    + +
    +
    + {result.map((item, index) => { + return ( +
    + setCurrentQuestion({ ...item, qNo: index + 1 }) + } + > + {index + 1} +
    + ); + })} +
    + +
    +
    +
    + ); +}; + +export default EndScreen; diff --git a/src/plays/fun-quiz/FrontScreen.jsx b/src/plays/fun-quiz/FrontScreen.jsx new file mode 100644 index 0000000000..6f9d091502 --- /dev/null +++ b/src/plays/fun-quiz/FrontScreen.jsx @@ -0,0 +1,100 @@ +import { useState } from "react"; + +// css +import "./FrontScreen.scss"; +import options from './options.json' + +const CATEGORY_SELECTION = "CATEGORY_SELECTION"; +const RULES_DISPLAY = "RULES_DISPLAY"; + +const QuizSelectionScreen = ({ getSelectedCategory }) => { + const [selectedOption, setSelectedOption] = useState(""); + const [view, setView] = useState(CATEGORY_SELECTION); + + const letMeInHandler = () => { + if (!selectedOption) return; + setView(RULES_DISPLAY); + }; + + const RulesComponent = () => { + return ( + <> +

    1. There will be 20 Unique Questions.

    +

    2. Every Question will have 4 multiple choices to chooose.

    +

    + 3. Among 4 options only one option will be correct answer of the + Question. +

    +

    + 4. Answer selection isn't mandatory. You can skip choosing any answer + and it will be counted as incorrect answer. +

    +

    + 5. After answer confirmation you cannot go back to previous question + or donot refresh the page otherwise you will lose you progress. +

    +

    + 6. You will be given 30 seconds to answer each question and timeup is + considered as an incorrect answer and next question will be displayed. +

    +

    + 7. After selecting an option you can click on selected option to unselect it. +

    +

    8. You can use cheats to cheat the answer.

    +
    + +
    +
    + +
    + + ); + }; + + const CategorySelector = () => { + return ( + <> +

    + The Quiz app requires to have a specefic category in order to start + with. Select one of the below options in which you have expertise in. +

    +
    + {options.map((option) => { + return ( +
    setSelectedOption(option.id)} + className={`single-selection ${ + selectedOption === option.id && "active-selected" + }`} + > + {option.name} +
    + ); + })} +
    +
    + +
    + + ); + }; + + const renderView = view === CATEGORY_SELECTION; + + return ( +
    +
    +

    {!renderView ? "Quiz Rules" : "Quiz App"}

    + {renderView && } + {!renderView && } +
    +
    + ); +}; + +export default QuizSelectionScreen; diff --git a/src/plays/fun-quiz/FrontScreen.scss b/src/plays/fun-quiz/FrontScreen.scss new file mode 100644 index 0000000000..8a5c3dd1bc --- /dev/null +++ b/src/plays/fun-quiz/FrontScreen.scss @@ -0,0 +1,229 @@ + +@import './variables'; + +.fun-quiz-main { + overflow: hidden; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .main-child { + width: 500px; + + .quiz-summary div:nth-child(1) { + font-size: 1.1rem; + line-height: 1; + margin: 5px 0; + display: flex; + align-items: center; + gap: 10px; + + h4:nth-of-type(1) { + margin: 0; + display: flex; + align-items: center; + gap: 10px; + + span:nth-of-type(1) { + font-size: 4rem; + line-height: 1; + padding: 0; + margin: 0; + font-weight: bold; + } + } + + h4:nth-of-type(2) { + margin: 0; + font-size: 1.5rem; + line-height: 1; + color: $quiz-app-raw-red; + } + + @media screen and (max-width: 600px) { + font-size: 1rem; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + } + } + + .quiz-summary div:nth-child(2) { + display: flex; + gap: 10px; + align-items: center; + font-size: 1.1rem; + + span:nth-of-type(1) { + font-size: 4rem; + font-weight: bold; + color: $quiz-app-raw-red; + line-height: 1; + } + + span:nth-of-type(2) { + font-weight: bold; + } + } + + h1 { + margin-top: 20px; + display: inline-block; + margin-bottom: 40px; + + &::after { + content: ""; + display: block; + width: 100%; + margin-top: 5px; + border: 2px solid $quiz-app-light-red; + border-radius: 20px; + } + } + + p { + font-size: 1rem; + font-weight: normal; + margin-top: -25px; + margin-bottom: 25px; + color: $quiz-app-green-color2; + line-height: 1.5; + } + + .selectable-options { + display: flex; + gap: 10px 15px; + width: 100%; + flex-wrap: wrap; + + .single-selection { + background-color: $quiz-app-light-gray; + box-shadow: inset 0px 0px 1px 1px rgba(218, 218, 218, 0.4); + padding: 8px 16px; + cursor: pointer; + border-radius: 20px; + color: $quiz-app-black-color; + font-weight: bold; + + &:hover { + background-color: $quiz-app-black-color; + color: $quiz-app-white-color; + box-shadow: none; + transition: 75ms ease-in; + } + } + + .active-selected { + background-color: $quiz-app-black-color; + color: $quiz-app-white-color; + box-shadow: none; + } + } + + .front-footer { + margin: 2rem 0; + + button { + outline: none; + border: none; + cursor: pointer; + background: $quiz-app-black-color; + width: 100%; + padding: 12px 0; + font-size: 1rem; + color: $quiz-app-white-color; + border-radius: 40px; + font-weight: bold; + box-shadow: 0px 0px 10px $quiz-app-transparent-black; + font-family: 'Quicksand', sans-serif; + + &:hover { + background-color: $quiz-app-black-color2; + } + } + } + + .congrats { + color: $quiz-app-raw-green; + } + .cheated { + color: $quiz-app-raw-red; + } + + .circle-area { + display: flex; + flex-wrap: wrap; + gap: 10px; + + .circle-correct { + width: 50px; + aspect-ratio: 1/1; + border-radius: 50%; + background-color: $quiz-app-raw-green; + color: $quiz-app-white-color; + display: grid; + place-items: center; + cursor: pointer; + } + + .circle-incorrect { + width: 50px; + aspect-ratio: 1/1; + border-radius: 50%; + background-color: $quiz-app-light-red2; + color: $quiz-app-white-color; + display: grid; + place-items: center; + cursor: pointer; + } + + } + + .display-question { + margin: 1.5rem 0; + width: 100%; + padding: 12px; + border-radius: 10px; + border: 1px solid $quiz-app-border-color; + color: $quiz-app-white-color; + position: relative; + } + + .correct { + background-color: $quiz-app-raw-green; + } + + .incorrect { + background-color: $quiz-app-light-red2; + } + + .question-number { + position: absolute; + top: -10px; + right: 20px; + background-color: $quiz-app-black-color; + padding: 0 10px; + border-radius: 20px; + color: $quiz-app-white-color; + } + + .back { + outline: none; + border: none; + background-color: transparent; + font-weight: bold; + font-size: 1rem; + color: $quiz-app-deep-blue; + cursor: pointer; + } + + } + + @media screen and (max-width: 600px) { + .main-child { + width: calc(100% - 2rem); + } + } +} + diff --git a/src/plays/fun-quiz/FunQuiz.jsx b/src/plays/fun-quiz/FunQuiz.jsx new file mode 100644 index 0000000000..fd2467f71b --- /dev/null +++ b/src/plays/fun-quiz/FunQuiz.jsx @@ -0,0 +1,74 @@ +import { getPlayById } from "meta/play-meta-util"; +import { useState } from "react"; + +import PlayHeader from "common/playlists/PlayHeader"; +import QuizScreen from "./QuizScreen"; +import QuizSelectionScreen from "./FrontScreen"; +import EndScreen from "./EndScreen"; + +// css +import './FunQuiz.scss' + +function FunQuiz(props) { + // Do not remove the below lines. + // The following code is to fetch the current play from the URL + const { id } = props; + const play = getPlayById(id); + + // Your Code Start below. + const [category, setCategory] = useState(""); + const [quizCompleted, setQuizCompleted] = useState(false); + const [quizSummary, setQuizSummary] = useState({}); + + const [maintenance] = useState(false); + + const calculateBooleanValues = (array = [], key) => { + return array.reduce((a, b) => (a += b[key] ? 1 : 0), 0); + }; + + const getQuizSummary = (result = []) => { + if (result.length === 20) { + setQuizCompleted(true); + const correctAnswers = calculateBooleanValues(result, "correct"); + const cheatedAnswers = calculateBooleanValues(result, "cheated"); + const wrongAnswers = 20 - correctAnswers; + return setQuizSummary({ + correctAnswers, + cheatedAnswers, + wrongAnswers, + result, + }); + } + return; + }; + + const redirectHome = () => { + setQuizCompleted(false); + setCategory(""); + }; + + if (maintenance) return

    This Page is under Maintenance

    ; + + return ( + <> +
    + +
    + {/* Your Code Starts Here */} + {!category && !quizCompleted && ( + + )} + {category && !quizCompleted && ( + + )} + {quizCompleted && ( + + )} + {/* Your Code Ends Here */} +
    +
    + + ); +} + +export default FunQuiz; diff --git a/src/plays/fun-quiz/FunQuiz.scss b/src/plays/fun-quiz/FunQuiz.scss new file mode 100644 index 0000000000..49cfd474cc --- /dev/null +++ b/src/plays/fun-quiz/FunQuiz.scss @@ -0,0 +1,6 @@ + + +.fun-quiz { + width: 100%; + overflow: hidden; +} \ No newline at end of file diff --git a/src/plays/fun-quiz/QuizScreen.jsx b/src/plays/fun-quiz/QuizScreen.jsx new file mode 100644 index 0000000000..0289ab5c72 --- /dev/null +++ b/src/plays/fun-quiz/QuizScreen.jsx @@ -0,0 +1,185 @@ +import { useEffect, useState, useCallback } from "react"; + +import "./QuizScreen.scss"; + +// assets +import confuseIcon from "./confuse.gif"; + +const answerState = { + answer: "", + cheat: false, + cheated: false, +}; + +function QuizScreen({ category, getQuizSummary }) { + const [quizData, setQuizData] = useState({ loading: false, data: [], error: false }); + const [answer, setAnswer] = useState({ ...answerState }); + const [result, setResult] = useState([]); + const [questionNumber, setQuestionNumber] = useState(0); + const [timer, setTimer] = useState(30); + + const formatCategoryText = category === "all" ? "" : `&category=${category}`; + const currentQuestion = quizData?.data?.[questionNumber]; + + useEffect(() => { + (async () => { + try { + setQuizData({ ...quizData, loading: true }); + const response = await fetch( + `https://opentdb.com/api.php?amount=20${formatCategoryText}&type=multiple` + ); + const { results } = await response.json(); + // if there is no data coming from api but status 200 is returned then we want to end up in catch block + if (!results.length) throw new Error(); + const createOptions = results.map((result) => { + const { incorrect_answers, correct_answer } = result; + const options = [...incorrect_answers]; + options?.splice( + Math.floor(Math.random() * (options.length + 1)), + 0, + correct_answer + ); + return { ...result, options }; + }); + return setQuizData({ data: createOptions, loading: false, error: false }); + } catch (err) { + setQuizData({ ...quizData, loading: false, error: true }); + } + })(); + }, []); + + // select and deselect the answer + const handleAnswerClick = (val) => (e) => { + setAnswer( + !!answer.answer && answer.answer === val + ? answerState + : { ...answer, answer: val } + ); + }; + + // handling the confirm button click + const handleConfirm = useCallback( + (skipped = false) => { + const updateResult = () => { + const manageSkippedAnswer = !skipped ? answer.answer : ""; + setResult((pre) => [ + ...pre, + { + question: currentQuestion.question, + correct: currentQuestion.correct_answer === manageSkippedAnswer, + your_answer: manageSkippedAnswer, + correct_answer: currentQuestion.correct_answer, + cheated: answer.cheated, + }, + ]); + }; + + if (questionNumber === 19) { + updateResult(); + return getQuizSummary([ + ...result, + { + question: currentQuestion.question, + correct: currentQuestion.correct_answer === answer.answer, + your_answer: answer.answer, + correct_answer: currentQuestion.correct_answer, + cheated: answer.cheated, + }, + ]); + } + updateResult(); + setAnswer(answerState); + setTimer(30); + setQuestionNumber(questionNumber + 1); + }, + [answer, questionNumber, currentQuestion, result, getQuizSummary] + ); + + useEffect(() => { + if (timer !== -1 && !!quizData.data.length) { + const setTiming = setInterval(() => { + setTimer(timer - 1); + }, 1000); + return () => clearInterval(setTiming); + } else if (!!quizData.data.length) { + setAnswer('') + handleConfirm(true); + } + }, [timer, handleConfirm, quizData.data]); + + const cheatHandler = (e) => { + setAnswer({ + cheat: true, + cheated: true, + answer: currentQuestion.correct_answer, + }); + const showCheat = setTimeout(() => { + setAnswer({ ...answerState, cheated: true }); + clearTimeout(showCheat); + }, 500); + }; + + const itemClassDisplayController = (option) => { + if (answer.cheat && answer.answer === option) + return "option-button blinking-options"; + if (answer.answer === option && !answer.cheat) + return "option-button active-option"; + return "option-button"; + }; + + // if there is an no data we display this message. + if (quizData?.error) { + return (
    We Apologize! Something Error Occured!
    ) + } + + return ( +
    + {quizData.loading && ( +
    + loading +
    + )} + {!quizData.loading && ( +
    +
    {timer}
    +
    Question: {questionNumber + 1}
    +
    +

    +

    +
    + {currentQuestion?.options?.map((option, index) => { + return ( +
    +
    +
    + ); + })} +
    +
    + + {answer.answer && !answer.cheat && ( + + )} + {!answer.answer && ( + + )} +
    +
    + )} +
    + ); +} + +export default QuizScreen; diff --git a/src/plays/fun-quiz/QuizScreen.scss b/src/plays/fun-quiz/QuizScreen.scss new file mode 100644 index 0000000000..2970cc4225 --- /dev/null +++ b/src/plays/fun-quiz/QuizScreen.scss @@ -0,0 +1,220 @@ +@import './variables'; + + +.fun-quiz { + &-screen { + margin-top: 4rem; + width: 100%; + overflow: hidden; + position: relative; + color: $quiz-app-raw-black; + display: block; + position: relative; + + @media screen and (max-width: 1366px) { + .App { + margin-top: 0rem; + } + } + + .loading-overlay { + display: flex; + justify-content: center; + align-items: center; + + } + + .timer { + border: 1px solid $quiz-app-black-color; + border-radius: 50%; + width: 100px; + aspect-ratio: 1/1; + display: grid; + place-items: center; + font-family: "Russo One", sans-serif; + font-size: 3rem; + color: $quiz-app-black-color; + margin-bottom: 2rem; + } + + .caution { + color: $quiz-app-raw-red; + } + + .video { + min-height: 100%; + min-width: 100%; + width: 100%; + object-position: center; + object-fit: cover; + aspect-ratio: 16/9; + } + + .section { + height: inherit; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + + .question-info { + background-color: $quiz-app-black-color; + color: $quiz-app-white-color; + padding: 8px 1rem; + margin: 1rem; + border-radius: 30px; + margin-top: -8px; + font-weight: bold; + } + + .question { + max-width: calc(820px - 50px); + margin: 0 10px 1rem 10px; + background-color: $quiz-app-light-gray2; + box-shadow: inset 0px 0px 10px 1px $quiz-app-light-gray3; + border-radius: 20px; + border: 1px solid $quiz-app-border-color; + + + h1 { + margin: 0.5rem 2rem; + color: $quiz-app-text-color; + font-size: clamp(1rem, 5vw, 1.5rem); + } + } + + .options { + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(2, 1fr); + row-gap: 30px; + column-gap: 30px; + margin-top: 1rem; + + .single-opt { + margin: 0; + padding: 0; + width: 250px; + display: grid; + place-items: center; + } + + .option-button { + display: flex; + align-items: center; + min-width: 250px; + min-height: 60px; + font-size: 1rem; + padding: 0.5rem 1.5rem; + background-color: rgba(234,227,227,1); + border-radius: 40px; + transition: 100ms ease-in-out; + text-align: left; + vertical-align: middle; + cursor: pointer; + + &:hover { + background-color: $quiz-app-green-color; + color: $quiz-app-white-color; + } + } + + .blinking-options { + animation: blinker 0.2s linear infinite; + background-color: $quiz-app-light-gray; + color: $quiz-app-black-color; + + @keyframes blinker { + from { + background-color: $quiz-app-light-gray; + color: $quiz-app-black-color; + } + 50% { + background-color: $quiz-app-green-color; + color: $quiz-app-white-color; + } + to { + background-color: $quiz-app-light-gray; + color: $quiz-app-black-color; + } + } + } + + .active-option { + background-color: $quiz-app-green-color; + box-shadow: inset 0px 0px 10px 2px $quiz-app-transparent-black; + color: $quiz-app-white-color; + } + } + + .confirm-button { + outline: none; + padding: 10px 2rem; + font-size: 1.1rem; + background-color: $quiz-app-deep-blue; + cursor: pointer; + border: none; + border-radius: 30px; + color: $quiz-app-white-color; + color: $quiz-app-white-color; + font-weight: 600; + + &:hover { + background-color: $quiz-app-deep-blue2; + box-shadow: inset 0px 0px 10px 2px $quiz-app-transparent-black; + } + } + + .cheat-button { + outline: none; + border: none; + background-color: transparent; + font-weight: normal; + color: $quiz-app-deep-blue; + text-decoration: underline; + cursor: pointer; + } + + @media screen and (max-width: 600px) { + .options { + grid-template-columns: 0fr; + grid-template-rows: repeat(4, 1fr); + row-gap: 20px; + column-gap: 20px; + } + .question { + width: calc(100% - 30px); + overflow: hidden; + } + } + + .footer { + margin: 1rem 0; + width: 540px; + height: 50px; + display: flex; + justify-content: space-between; + align-items: center; + + + + .link { + outline: none; + border: none; + background-color: transparent; + font-size: 1rem; + color: $quiz-app-deep-blue; + cursor: pointer; + font-weight: 600; + } + @media screen and (max-width: 600px) { + width: 100%; + height: 100px; + flex-direction: column; + justify-content: center; + gap: 1rem; + } + } + } + } +} diff --git a/src/plays/fun-quiz/Readme.md b/src/plays/fun-quiz/Readme.md new file mode 100644 index 0000000000..18e406382f --- /dev/null +++ b/src/plays/fun-quiz/Readme.md @@ -0,0 +1,11 @@ +# Fun Quiz + +The `Fun Quiz` is a very fun play build with ReactJS & moreover its a quiz game where you will be asked 20 unique question and player have to choose one answer out of 4 options. + + +There is some key things we can also learn from projects are + +- Handling complex logics in ReactJS. +- using useEffect for api call and handling data using useState hook. +- Managing Complex and Nested States. +- use of scss in ReactJS. diff --git a/src/plays/fun-quiz/_variables.scss b/src/plays/fun-quiz/_variables.scss new file mode 100644 index 0000000000..820dcc28eb --- /dev/null +++ b/src/plays/fun-quiz/_variables.scss @@ -0,0 +1,27 @@ +@import url("https://fonts.googleapis.com/css2?family=Russo+One&display=swap"); // russo one + + +$quiz-app-green-color2: rgba(0, 154, 49, 1); +$quiz-app-text-color: rgba(32, 73, 105, 1); +$quiz-app-black-color: #444444; +$quiz-app-black-color2: #333333; +$quiz-app-white-color: #ffffff; +$quiz-app-green-color: rgba(0, 160, 62, 1); + +$quiz-app-light-gray: rgba(242, 244, 246, 1); +$quiz-app-light-gray2: rgba(255, 247, 247, 1); +$quiz-app-light-gray3: rgba(255, 247, 255, 1); + +$quiz-app-border-color: #ccc; + +$quiz-app-transparent-black: rgba(0, 0, 0, 0.1); + +$quiz-app-deep-blue: #0a66c2; +$quiz-app-deep-blue2: #16437e; + +$quiz-app-light-red: rgba(242,49,127,1); +$quiz-app-light-red2: rgba(246,0,60,1); + +$quiz-app-raw-green: green; +$quiz-app-raw-red: red; +$quiz-app-raw-black: black; \ No newline at end of file diff --git a/src/plays/fun-quiz/confuse.gif b/src/plays/fun-quiz/confuse.gif new file mode 100644 index 0000000000..12ee813601 Binary files /dev/null and b/src/plays/fun-quiz/confuse.gif differ diff --git a/src/plays/fun-quiz/options.json b/src/plays/fun-quiz/options.json new file mode 100644 index 0000000000..499e3ae75d --- /dev/null +++ b/src/plays/fun-quiz/options.json @@ -0,0 +1,62 @@ +[ + { + "name": "All", + "id": "all" + }, + { + "name": "Books", + "id": 10 + }, + { + "name": "General Knwoledge", + "id": 9 + }, + { + "name": "Films", + "id": 11 + }, + { + "name": "Musics", + "id": 12 + }, + { + "name": "Television", + "id": 14 + }, + { + "name": "Video Games", + "id": 15 + }, + { + "name": "Computers", + "id": 18 + }, + { + "name": "Mathematics", + "id": 19 + }, + { + "name": "Sports", + "id": 21 + }, + { + "name": "Geography", + "id": 22 + }, + { + "name": "History", + "id": 23 + }, + { + "name": "Politics", + "id": 24 + }, + { + "name": "Celebrities", + "id": 26 + }, + { + "name": "Science & Nature", + "id": 17 + } +] diff --git a/src/plays/index.js b/src/plays/index.js index 1c7b1d71ce..d6acba309d 100644 --- a/src/plays/index.js +++ b/src/plays/index.js @@ -17,4 +17,5 @@ export { default as AnalogClock } from 'plays/analog-clock/AnalogClock'; export { default as PasswordGenerator } from 'plays/password-generator/PasswordGenerator'; export { default as WhyTypescript } from 'plays/why-typescript/WhyTypescript'; export { default as NetlifyCardGame } from 'plays/memory-game/NetlifyCardGame'; +export { default as FunQuiz } from 'plays/fun-quiz/FunQuiz'; //add export here