From f7ad1ac2abd19bdd9a25b4ec0ba9b525f9ba600e Mon Sep 17 00:00:00 2001 From: mehedi-hasan Date: Sun, 7 Apr 2024 22:53:09 +0600 Subject: [PATCH] feat: quiz --- .../(dashboard)/dashboard/(quiz)/layout.tsx | 29 +++ src/app/(dashboard)/dashboard/(quiz)/page.tsx | 242 ++++++++++++++++++ .../dashboard/(quiz)/quiz/page.tsx | 12 + .../dashboard/(quiz)/quiz/results/page.tsx | 12 + src/components/quiz/error-message.tsx | 9 + src/components/quiz/finished-screen.tsx | 90 +++++++ src/components/quiz/header.tsx | 13 + src/components/quiz/loader.tsx | 10 + src/components/quiz/next.tsx | 44 ++++ src/components/quiz/progress.tsx | 34 +++ src/components/quiz/question.tsx | 106 ++++++++ src/components/quiz/social-media.tsx | 17 ++ src/components/quiz/start-screen.tsx | 62 +++++ src/components/quiz/timer.tsx | 51 ++++ src/lib/quiz-store.ts | 171 +++++++++++++ 15 files changed, 902 insertions(+) create mode 100644 src/app/(dashboard)/dashboard/(quiz)/layout.tsx create mode 100644 src/app/(dashboard)/dashboard/(quiz)/page.tsx create mode 100644 src/app/(dashboard)/dashboard/(quiz)/quiz/page.tsx create mode 100644 src/app/(dashboard)/dashboard/(quiz)/quiz/results/page.tsx create mode 100644 src/components/quiz/error-message.tsx create mode 100644 src/components/quiz/finished-screen.tsx create mode 100644 src/components/quiz/header.tsx create mode 100644 src/components/quiz/loader.tsx create mode 100644 src/components/quiz/next.tsx create mode 100644 src/components/quiz/progress.tsx create mode 100644 src/components/quiz/question.tsx create mode 100644 src/components/quiz/social-media.tsx create mode 100644 src/components/quiz/start-screen.tsx create mode 100644 src/components/quiz/timer.tsx create mode 100644 src/lib/quiz-store.ts diff --git a/src/app/(dashboard)/dashboard/(quiz)/layout.tsx b/src/app/(dashboard)/dashboard/(quiz)/layout.tsx new file mode 100644 index 0000000..f106d24 --- /dev/null +++ b/src/app/(dashboard)/dashboard/(quiz)/layout.tsx @@ -0,0 +1,29 @@ +import Header from "@/components/layout/header"; +import Sidebar from "@/components/layout/sidebar"; +import StoreProvider from "@/lib/store-provider"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Dashboard | Skilled Ai", + description: + "Empower your coding journey at Skilled Ai. Access tutorials, challenges, and expert-led courses to master programming languages and tools. Join a supportive community for discussions and code reviews. Elevate your skills and stay ahead in the tech world with us!", +}; + +export default function DashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + <> + +
+ +
+ +
{children}
+
+ + + ); +} diff --git a/src/app/(dashboard)/dashboard/(quiz)/page.tsx b/src/app/(dashboard)/dashboard/(quiz)/page.tsx new file mode 100644 index 0000000..28a465f --- /dev/null +++ b/src/app/(dashboard)/dashboard/(quiz)/page.tsx @@ -0,0 +1,242 @@ +"use client"; + +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { z } from "zod"; +import FileUpload from "@/components/file-upload"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useState } from "react"; +import { Alert, AlertDescription } from "@/components/ui/alert"; + +const ImgSchema = z.object({ + fileName: z.string(), + name: z.string(), + fileSize: z.number(), + size: z.number(), + fileKey: z.string(), + key: z.string(), + fileUrl: z.string(), + url: z.string(), +}); + +const IMG_MAX_LIMIT = 1; +const formSchema = z.object({ + imgUrl: z + .array(ImgSchema) + .max(IMG_MAX_LIMIT, { message: "You can only add up to 3 images" }) + .min(1, { message: "At least one image must be added." }), +}); + +type FileFormValues = z.infer; + +export default function Page() { + const [initialData, setInitialData] = useState(null); + const action = initialData ? "Save changes" : "Create"; + const defaultValues = initialData + ? initialData + : { + imgUrl: [], + }; + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues, + }); + + const onSubmit = async (data: FileFormValues) => {}; + return ( + +
+
+

+ Hi, Welcome back 👋 +

+ + + Empower your coding journey at Skilled Ai. Access tutorials, + challenges, and expert-led courses to master programming languages + and tools. Join a supportive community for discussions and code + reviews. Elevate your skills and stay ahead in the tech world with + us! + + +
+ + ( + + + Upload your resume or CV + + + + + + + )} + /> + + +
+ + {/*
+

+ Hi, Welcome back 👋 +

+
+ + +
+
+ + + Overview + + Analytics + + + +
+ + + + Total Revenue + + + + + + +
$45,231.89
+

+ +20.1% from last month +

+
+
+ + + + Subscriptions + + + + + + + + +
+2350
+

+ +180.1% from last month +

+
+
+ + + Sales + + + + + + +
+12,234
+

+ +19% from last month +

+
+
+ + + + Active Now + + + + + + +
+573
+

+ +201 since last hour +

+
+
+
+
+ + + Overview + + + + + + + + Recent Sales + + You made 265 sales this month. + + + + + + +
+
+
*/} +
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/(quiz)/quiz/page.tsx b/src/app/(dashboard)/dashboard/(quiz)/quiz/page.tsx new file mode 100644 index 0000000..b387219 --- /dev/null +++ b/src/app/(dashboard)/dashboard/(quiz)/quiz/page.tsx @@ -0,0 +1,12 @@ +import { StartScreen } from "@/components/quiz/start-screen"; +import { Question } from "@/components/quiz/question"; +const Page = () => { + return ( +
+ {/* */} + +
+ ); +}; + +export default Page; diff --git a/src/app/(dashboard)/dashboard/(quiz)/quiz/results/page.tsx b/src/app/(dashboard)/dashboard/(quiz)/quiz/results/page.tsx new file mode 100644 index 0000000..94f8ba1 --- /dev/null +++ b/src/app/(dashboard)/dashboard/(quiz)/quiz/results/page.tsx @@ -0,0 +1,12 @@ +import { FinishedScreen } from "@/components/quiz/finished-screen"; +import React from "react"; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export default Page; diff --git a/src/components/quiz/error-message.tsx b/src/components/quiz/error-message.tsx new file mode 100644 index 0000000..26ce9ad --- /dev/null +++ b/src/components/quiz/error-message.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +export const ErrorMessage = () => { + return ( +

+ Oh no! There was an error fecthing questions. +

+ ) +} \ No newline at end of file diff --git a/src/components/quiz/finished-screen.tsx b/src/components/quiz/finished-screen.tsx new file mode 100644 index 0000000..7825e9a --- /dev/null +++ b/src/components/quiz/finished-screen.tsx @@ -0,0 +1,90 @@ +"use client"; + +import React from "react"; +// import { useNavigate } from 'react-router-dom' +// import { useSelector } from 'react-redux' +import { useQuizStore } from "@/lib/quiz-store"; +import { useShallow } from "zustand/react/shallow"; +import { SocialMedia } from "./social-media"; +import { useRouter } from "next/navigation"; +import { Button } from "../ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "../ui/card"; + +export const FinishedScreen = () => { + const router = useRouter(); + const { + points, + highscore, + gameMode, + totalCorrectAns, + questionsArray, + reset, + } = useQuizStore( + useShallow((store) => ({ + points: store.points, + highscore: store.highscore, + gameMode: store.gameMode, + totalCorrectAns: store.totalCorrectAns, + questionsArray: store.questionsArray, + reset: store.reset, + })), + ); + // const {points, highscore} = useSelector(store => store.questions) + // const {gameMode} = useSelector(store => store.difficulty) + const percentage = Math.ceil((points * 100) / 300); + // const navigate = useNavigate() + + let congrats; + if (percentage === 100) congrats = "Perfect!"; + if (percentage >= 80 && percentage < 100) congrats = "Excellent!"; + if (percentage >= 50 && percentage < 80) congrats = "Good!"; + if (percentage > 0 && percentage < 50) congrats = "Bad luck!"; + if (percentage === 0) congrats = "Oh no!"; + + return ( + + {/*

+ {congrats} You scored {points} out of 300 ({percentage} + %) +

*/} + {/*

(Highscore: {highscore} points)

*/} + + Quiz Results + + +

Correct: {totalCorrectAns}

+

+ Incorrect: {questionsArray.length - totalCorrectAns} +

+
+ + + + {/*
+ + +
*/} + {/* */} +
+ ); +}; diff --git a/src/components/quiz/header.tsx b/src/components/quiz/header.tsx new file mode 100644 index 0000000..4ed8942 --- /dev/null +++ b/src/components/quiz/header.tsx @@ -0,0 +1,13 @@ +import React from 'react' +// import { Outlet } from 'react-router-dom' + +export const Header = () => { + return ( + <> +
+

The trivia Quiz

+
+ {/* */} + + ) +} \ No newline at end of file diff --git a/src/components/quiz/loader.tsx b/src/components/quiz/loader.tsx new file mode 100644 index 0000000..788ca28 --- /dev/null +++ b/src/components/quiz/loader.tsx @@ -0,0 +1,10 @@ +import React from 'react' + +export const Loader = () => { + return ( +
+
+

Loading questions...

+
+ ) +} \ No newline at end of file diff --git a/src/components/quiz/next.tsx b/src/components/quiz/next.tsx new file mode 100644 index 0000000..c56c70f --- /dev/null +++ b/src/components/quiz/next.tsx @@ -0,0 +1,44 @@ +import { useQuizStore } from "@/lib/quiz-store"; +import { useRouter } from "next/navigation"; +import React from "react"; +import { useShallow } from "zustand/react/shallow"; +import { Button } from "../ui/button"; +// import { useNavigate } from 'react-router-dom' +// import { useDispatch, useSelector } from 'react-redux' +// import { gameEnded, nextQuestion } from '../features/questions/questionsSlice' + +export const Next = () => { + const router = useRouter(); + + const { index, gameEnded, nextQuestion, questionsArray } = useQuizStore( + useShallow((store) => ({ + index: store.index, + gameEnded: store.gameEnded, + nextQuestion: store.nextQuestion, + questionsArray: store.questionsArray, + })), + ); + + // const {index} = useSelector(store => store.questions) + + // const dispatch = useDispatch() + // const navigate = useNavigate() + + const handleFinish = () => { + gameEnded(); + router.push("/dashboard/quiz/results"); + }; + + if (index < questionsArray.length - 1) + return ( + + ); + + return ( + + ); +}; diff --git a/src/components/quiz/progress.tsx b/src/components/quiz/progress.tsx new file mode 100644 index 0000000..f0faa96 --- /dev/null +++ b/src/components/quiz/progress.tsx @@ -0,0 +1,34 @@ +import { useQuizStore } from "@/lib/quiz-store"; +import React from "react"; +import { useShallow } from "zustand/react/shallow"; +import { Progress } from "../ui/progress"; + +export const ProgressBar = () => { + const { index, points, answer, questionsArray } = useQuizStore( + useShallow((store) => ({ + index: store.index, + points: store.points, + answer: store.answer, + questionsArray: store.questionsArray, + })), + ); + + return ( +
+ + + {/* */} + {/*
*/} +

+ {index + 1} / {questionsArray.length} +

+ {/*

+ {points} / 300 +

*/} + {/*
*/} +
+ ); +}; diff --git a/src/components/quiz/question.tsx b/src/components/quiz/question.tsx new file mode 100644 index 0000000..2e0f546 --- /dev/null +++ b/src/components/quiz/question.tsx @@ -0,0 +1,106 @@ +"use client"; + +import React, { useEffect } from "react"; +// import { getQuestions, newAnswer } from "../features/questions/questionsSlice"; +import { Loader } from "./loader"; +import { ErrorMessage } from "./error-message"; +import { ProgressBar } from "./progress"; +import { Timer } from "./timer"; +import { Next } from "./next"; +import { useQuizStore } from "@/lib/quiz-store"; +import { useShallow } from "zustand/react/shallow"; +import { Button } from "../ui/button"; +import { cn } from "@/lib/utils"; + +export const Question = ({ difficulty }: { difficulty: string }) => { + // const { difficulty } = useParams(); + // const { gameMode } = useSelector((store) => store.difficulty); + // const { status, index, currentQuestion, answer } = useSelector( + // (store) => store.questions, + // ); + + const { + restartTimer, + // gameMode, + status, + index, + currentQuestion, + answer, + newAnswer, + } = useQuizStore( + useShallow((store) => ({ + restartTimer: store.restartTimer, + gameMode: store.gameMode, + status: store.status, + index: store.index, + currentQuestion: store.currentQuestion, + answer: store.answer, + newAnswer: store.newAnswer, + })), + ); + + useEffect(() => { + restartTimer(); + // getQuestions(gameMode); + }, []); + + useEffect(() => { + window.scrollTo(0, 0); + }, [index]); + + const statement = currentQuestion?.question; + const options = currentQuestion?.options; + const hasAnswered = answer !== null; + // const styleCat = { + // backgroundColor: difficulty === "medium" ? "#e3ce0e" : "#fc2121", + // }; + return ( +
+ {status === "loading" && } + {status === "error" && } + {status === "ready" && ( + <> + +
+ {/*
+ {difficulty} quiz +
*/} +

{statement}

+
+ {options?.map((option: any, index: number) => { + return ( + + ); + })} +
+
+
+
{answer && }
+ +
+ + )} +
+ ); +}; diff --git a/src/components/quiz/social-media.tsx b/src/components/quiz/social-media.tsx new file mode 100644 index 0000000..dce3b33 --- /dev/null +++ b/src/components/quiz/social-media.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import Link from "next/link"; +import { Github, Linkedin } from "lucide-react"; +// import { Link } from 'react-router-dom'; + +export const SocialMedia = () => { + return ( +
+ + + + + + +
+ ); +}; diff --git a/src/components/quiz/start-screen.tsx b/src/components/quiz/start-screen.tsx new file mode 100644 index 0000000..00ec697 --- /dev/null +++ b/src/components/quiz/start-screen.tsx @@ -0,0 +1,62 @@ +"use client"; + +// import { useNavigate } from "react-router-dom"; +// import { SocialMedia } from "./SocialMedia"; +// import { selectGameMode } from "../features/difficulty/difficultySlice"; +// import { useDispatch } from "react-redux"; +import { useRouter } from "next/navigation"; +import { useQuizStore } from "@/lib/quiz-store"; +import { useShallow } from "zustand/react/shallow"; +import { Button } from "../ui/button"; + +export const StartScreen = () => { + // const dispatch = useDispatch(); + // const navigate = useNavigate(); + const router = useRouter(); + + const { selectGameMode } = useQuizStore( + useShallow((store) => ({ + selectGameMode: store.selectGameMode, + })), + ); + + const handleClick = (e: any) => { + selectGameMode(e.target.value); + router.push(`/dashboard/quiz/${e.target.value}`); + }; + + return ( +
+

Welcome to The Trivia Quiz!

+

15 question to test your general knowledge

+

First, choose the test difficulty:

+
+ + + +
+ {/* */} +
+ ); +}; diff --git a/src/components/quiz/timer.tsx b/src/components/quiz/timer.tsx new file mode 100644 index 0000000..d063e54 --- /dev/null +++ b/src/components/quiz/timer.tsx @@ -0,0 +1,51 @@ +import React, { useEffect } from "react"; +// import { useNavigate } from 'react-router-dom' +// import { useDispatch, useSelector } from 'react-redux' +// import { lessSeconds, restartTimer } from '../features/timer/timerSlice' +// import { gameEnded } from '../features/questions/questionsSlice' +import { useQuizStore } from "@/lib/quiz-store"; +import { useShallow } from "zustand/react/shallow"; +import { useRouter } from "next/navigation"; +import { buttonVariants } from "../ui/button"; + +export const Timer = () => { + const router = useRouter(); + const { secondsRemaining, gameEnded, lessSeconds, restartTimer } = + useQuizStore( + useShallow((store) => ({ + secondsRemaining: store.secondsRemaining, + gameEnded: store.gameEnded, + lessSeconds: store.lessSeconds, + restartTimer: store.restartTimer, + })), + ); + // const {secondsRemaining} = useSelector(store => store.timer) + const mins = Math.floor(secondsRemaining / 60); + const sec = secondsRemaining % 60; + + // const dispatch = useDispatch(); + // const navigate = useNavigate(); + + useEffect(() => { + if (secondsRemaining === 0) { + gameEnded(); + restartTimer(); + router.push("/dashboard/quiz/results"); + } + }, [secondsRemaining]); + + useEffect(() => { + const timer = setInterval(() => { + lessSeconds(); + }, 1000); + return () => clearInterval(timer); + }, []); + + return ( +
+ {mins < 10 && "0"} + {mins}:{sec < 10 && "0"} + {sec} +
+ ); +}; diff --git a/src/lib/quiz-store.ts b/src/lib/quiz-store.ts new file mode 100644 index 0000000..173e338 --- /dev/null +++ b/src/lib/quiz-store.ts @@ -0,0 +1,171 @@ +"use client"; + +import { createContext, useContext } from "react"; +import { createStore, useStore as useZustandStore } from "zustand"; +import { PreloadedStoreInterface } from "./store-provider"; +import { questions } from "@/constants/data"; + +function shuffleArray(array: any) { + const newArray = [...array]; + // for (let i = newArray.length - 1; i > 0; i--) { + // const j = Math.floor(Math.random() * (i + 1)); + // [newArray[i], newArray[j]] = [newArray[j], newArray[i]]; + // } + return newArray; +} + +export interface StoreInterface { + gameMode: null | string; + questionsArray: any[]; + //loading, error, ready + status: string; + index: number; + currentQuestion: any; + answer: null; + points: number; + highscore: number; + secondsRemaining: number; + totalCorrectAns: number; + lessSeconds: () => void; + restartTimer: () => void; + selectGameMode: (gameMode: string) => void; + newAnswer: (answer: any) => void; + nextQuestion: () => void; + gameEnded: () => void; + lastUpdate: number; + light: boolean; + count: number; + // tick: (lastUpdate: number) => void; + // increment: () => void; + // decrement: () => void; + reset: () => void; +} + +function getDefaultInitialState() { + return { + lastUpdate: new Date(1970, 1, 1).getTime(), + light: false, + count: 0, + gameMode: null, + questionsArray: questions, + //loading, error, ready + status: "ready", + index: 0, + currentQuestion: { + id: questions[0].id, + correctAnswer: questions[0].correctAnswer, + question: questions[0].question.text, + options: shuffleArray([ + ...questions[0].incorrectAnswers, + questions[0].correctAnswer, + ]), + }, + answer: null, + points: 0, + highscore: 0, + secondsRemaining: 210, + totalCorrectAns: 0, + }; +} + +export type StoreType = ReturnType; + +const storeContext = createContext(null); + +export const Provider = storeContext.Provider; + +export function useQuizStore(selector: (state: StoreInterface) => T) { + const store = useContext(storeContext); + + if (!store) throw new Error("Store is missing the provider"); + + return useZustandStore(store, selector); +} + +export function initializeStore(preloadedState: PreloadedStoreInterface) { + return createStore((set, get) => ({ + ...getDefaultInitialState(), + ...preloadedState, + selectGameMode: (gameMode) => + set({ + gameMode, + }), + newAnswer: (answer) => { + // console.log(get().totalCorrectAns); + set({ + answer, + points: + answer === get().currentQuestion.correctAnswer + ? get().points + 20 + : get().points, + totalCorrectAns: + answer === get().currentQuestion.correctAnswer + ? get().totalCorrectAns + 1 + : get().totalCorrectAns, + }); + }, + nextQuestion: () => { + let temp = get().questionsArray[get().index + 1]; + let newArray = { + id: temp.id, + correctAnswer: temp.correctAnswer, + question: temp.question.text, + options: shuffleArray([...temp.incorrectAnswers, temp.correctAnswer]), + }; + set({ + index: (get().index += 1), + currentQuestion: newArray, + answer: null, + }); + }, + gameEnded: () => + set({ + highscore: + get().points > get().highscore ? get().points : get().highscore, + }), + lessSeconds: () => set({ secondsRemaining: (get().secondsRemaining -= 1) }), + restartTimer: () => set({ secondsRemaining: 210 }), + reset: () => + set({ + lastUpdate: new Date(1970, 1, 1).getTime(), + light: false, + count: 0, + gameMode: null, + questionsArray: questions, + //loading, error, ready + status: "ready", + index: 0, + currentQuestion: { + id: questions[0].id, + correctAnswer: questions[0].correctAnswer, + question: questions[0].question.text, + options: shuffleArray([ + ...questions[0].incorrectAnswers, + questions[0].correctAnswer, + ]), + }, + answer: null, + points: 0, + highscore: 0, + secondsRemaining: 210, + totalCorrectAns: 0, + }), + // tick: (lastUpdate) => + // set({ + // lastUpdate, + // light: !get().light, + // }), + // increment: () => + // set({ + // count: get().count + 1, + // }), + // decrement: () => + // set({ + // count: get().count - 1, + // }), + // reset: () => + // set({ + // count: getDefaultInitialState().count, + // }), + })); +}