feat: quiz

This commit is contained in:
mehedi-hasan 2024-04-07 22:53:09 +06:00
parent 7b680bc196
commit f7ad1ac2ab
15 changed files with 902 additions and 0 deletions

View File

@ -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 (
<>
<StoreProvider lastUpdate={new Date().getTime()}>
<Header />
<div className="flex h-screen overflow-hidden">
<Sidebar />
<main className="w-full pt-16">{children}</main>
</div>
</StoreProvider>
</>
);
}

View File

@ -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<typeof formSchema>;
export default function Page() {
const [initialData, setInitialData] = useState(null);
const action = initialData ? "Save changes" : "Create";
const defaultValues = initialData
? initialData
: {
imgUrl: [],
};
const form = useForm<FileFormValues>({
resolver: zodResolver(formSchema),
defaultValues,
});
const onSubmit = async (data: FileFormValues) => {};
return (
<ScrollArea className="h-full">
<div className="flex-1 space-y-4 p-4 md:p-8 pt-6">
<div className="max-w-3xl mx-auto">
<h2 className="text-3xl font-bold tracking-tight mb-4">
Hi, Welcome back 👋
</h2>
<Alert className="mb-4">
<AlertDescription className="text-base">
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!
</AlertDescription>
</Alert>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className=" w-full">
<FormField
control={form.control}
name="imgUrl"
render={({ field }) => (
<FormItem>
<FormLabel className="mb-0">
Upload your resume or CV
</FormLabel>
<FormControl>
<FileUpload
onChange={field.onChange}
value={field.value}
onRemove={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
{/* <div className="flex items-center justify-between space-y-2">
<h2 className="text-3xl font-bold tracking-tight">
Hi, Welcome back 👋
</h2>
<div className="hidden md:flex items-center space-x-2">
<CalendarDateRangePicker />
<Button>Download</Button>
</div>
</div>
<Tabs defaultValue="overview" className="space-y-4">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics" disabled>
Analytics
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Revenue
</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$45,231.89</div>
<p className="text-xs text-muted-foreground">
+20.1% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Subscriptions
</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+2350</div>
<p className="text-xs text-muted-foreground">
+180.1% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Sales</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<rect width="20" height="14" x="2" y="5" rx="2" />
<path d="M2 10h20" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+12,234</div>
<p className="text-xs text-muted-foreground">
+19% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Active Now
</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+573</div>
<p className="text-xs text-muted-foreground">
+201 since last hour
</p>
</CardContent>
</Card>
</div>
<div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<CardTitle>Overview</CardTitle>
</CardHeader>
<CardContent className="pl-2">
<Overview />
</CardContent>
</Card>
<Card className="col-span-4 md:col-span-3">
<CardHeader>
<CardTitle>Recent Sales</CardTitle>
<CardDescription>
You made 265 sales this month.
</CardDescription>
</CardHeader>
<CardContent>
<RecentSales />
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs> */}
</div>
</ScrollArea>
);
}

View File

@ -0,0 +1,12 @@
import { StartScreen } from "@/components/quiz/start-screen";
import { Question } from "@/components/quiz/question";
const Page = () => {
return (
<div>
{/* <StartScreen /> */}
<Question difficulty={"easy"} />
</div>
);
};
export default Page;

View File

@ -0,0 +1,12 @@
import { FinishedScreen } from "@/components/quiz/finished-screen";
import React from "react";
const Page = () => {
return (
<div>
<FinishedScreen />
</div>
);
};
export default Page;

View File

@ -0,0 +1,9 @@
import React from 'react'
export const ErrorMessage = () => {
return (
<p className="error">
Oh no! There was an error fecthing questions.
</p>
)
}

View File

@ -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 (
<Card className="max-w-2xl mx-auto mt-8">
{/* <p className="result">
{congrats} You scored <strong>{points}</strong> out of 300 ({percentage}
%)
</p> */}
{/* <p className="highscore">(Highscore: {highscore} points)</p> */}
<CardHeader>
<CardTitle>Quiz Results</CardTitle>
</CardHeader>
<CardContent>
<p className="text-green-500">Correct: {totalCorrectAns}</p>
<p className="text-rose-500">
Incorrect: {questionsArray.length - totalCorrectAns}
</p>
</CardContent>
<CardFooter>
<Button
onClick={() => {
reset();
router.push(`/dashboard/quiz`);
}}
>
Reset
</Button>
</CardFooter>
{/* <div className="reset-btns">
<Button className="btn" onClick={() => router.push(`/dashboard/quiz`)}>
Main Menu
</Button>
<Button
className="btn"
onClick={() => router.push(`/dashboard/quiz/${gameMode}`)}
>
Reset
</Button>
</div> */}
{/* <SocialMedia /> */}
</Card>
);
};

View File

@ -0,0 +1,13 @@
import React from 'react'
// import { Outlet } from 'react-router-dom'
export const Header = () => {
return (
<>
<header className='app-header'>
<h1>The trivia <span>Quiz</span></h1>
</header>
{/* <Outlet/> */}
</>
)
}

View File

@ -0,0 +1,10 @@
import React from 'react'
export const Loader = () => {
return (
<div className="loader-container">
<div className="loader"></div>
<h3>Loading questions...</h3>
</div>
)
}

View File

@ -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 (
<Button className="" onClick={nextQuestion}>
Next
</Button>
);
return (
<Button className="" onClick={handleFinish}>
Finish
</Button>
);
};

View File

@ -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 (
<div className="space-y-2">
<Progress
value={(100 / questionsArray.length) * index}
className="w-full h-4"
/>
{/* <progress max="15" value={index + Number(answer !== null)} /> */}
{/* <div className="flex justify-between"> */}
<p>
<strong>{index + 1}</strong> / {questionsArray.length}
</p>
{/* <p>
<strong>{points}</strong> / 300
</p> */}
{/* </div> */}
</div>
);
};

View File

@ -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 (
<div className=" max-w-4xl mx-auto mt-8 px-4">
{status === "loading" && <Loader />}
{status === "error" && <ErrorMessage />}
{status === "ready" && (
<>
<ProgressBar />
<div className="space-y-4 mt-8">
{/* <div
className="category"
// style={difficulty !== "easy" ? styleCat : {}}
>
{difficulty} quiz
</div> */}
<h4 className="text-xl font-semibold">{statement}</h4>
<div className="flex flex-col gap-1">
{options?.map((option: any, index: number) => {
return (
<Button
variant={`${answer === option ? "default" : "secondary"}`}
key={index}
className={cn("justify-start text-lg py-6 disabled:pointer-events-auto disabled:cursor-not-allowed")}
// className={cn(answer === option ? "answer" : "")}
// className={`${answer === option ? "answer" : ""}
// ${
// hasAnswered
// ? currentQuestion.correctAnswer === option
// ? "correct"
// : ""
// : ""
// }
// `}
disabled={hasAnswered}
onClick={() => newAnswer(option)}
>
{option}
</Button>
);
})}
</div>
</div>
<div className="flex justify-between mt-6 items-center">
<div>{answer && <Next />}</div>
<Timer />
</div>
</>
)}
</div>
);
};

View File

@ -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 (
<div className="">
<Link href="/dashboard/quiz" >
<Github />
</Link>
<Link href="/dashboard/quiz">
<Linkedin />
</Link>
</div>
);
};

View File

@ -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 (
<div className="text-center mt-20">
<h2 className="font-bold text-4xl">Welcome to The Trivia Quiz!</h2>
<h3 className="font-bold text-xl mt-2">15 question to test your general knowledge</h3>
<h4 className="mt-4 mb-2">First, choose the test difficulty:</h4>
<div className="flex gap-2 justify-center">
<Button
// className="btn2"
value="easy"
onClick={handleClick}
// style={{ backgroundColor: "#0ee32a" }}
>
Easy
</Button>
<Button
// className="btn2"
value="medium"
onClick={handleClick}
// style={{ backgroundColor: "#e3ce0e" }}
>
Medium
</Button>
<Button
// className="btn2"
value="hard"
onClick={handleClick}
// style={{ backgroundColor: "#fc2121" }}
>
Hard
</Button>
</div>
{/* <SocialMedia /> */}
</div>
);
};

View File

@ -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 (
<div className={buttonVariants({ variant: "outline" })}>
{mins < 10 && "0"}
{mins}:{sec < 10 && "0"}
{sec}
</div>
);
};

171
src/lib/quiz-store.ts Normal file
View File

@ -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<typeof initializeStore>;
const storeContext = createContext<StoreType | null>(null);
export const Provider = storeContext.Provider;
export function useQuizStore<T>(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<StoreInterface>((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,
// }),
}));
}