feat: quiz
This commit is contained in:
parent
7b680bc196
commit
f7ad1ac2ab
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1,12 @@
|
|||
import { FinishedScreen } from "@/components/quiz/finished-screen";
|
||||
import React from "react";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div>
|
||||
<FinishedScreen />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react'
|
||||
|
||||
export const ErrorMessage = () => {
|
||||
return (
|
||||
<p className="error">
|
||||
Oh no! There was an error fecthing questions.
|
||||
</p>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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/> */}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
// }),
|
||||
}));
|
||||
}
|
Loading…
Reference in New Issue