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