-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Implement quiz API with enhanced functionality and UI #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
3f1d20f
feat: Initiate quiz api
Alvalens cfb5751
feat: Revise quiz functionality and UI components
Alvalens 415e5f4
feat: Refactor quiz data structure and implement encryption for quiz …
Alvalens a8ecc08
feat: Enhance quiz functionality with result saving and history page
Alvalens 20c6eb1
Merge branch 'main' of https://github.com/gdsc-um/homesite-refactor i…
Alvalens 677d01d
feat: Add quiz history page and improve quiz client navigation
Alvalens c66cc1b
Merge branch 'main' of https://github.com/gdsc-um/homesite-refactor i…
Alvalens 451f53a
feat: Update environment variables and move secret key
Alvalens 830dc17
feat: Add isPublished field to Quiz model and update related components
Alvalens c5f69cc
Merge branches 'backend/member-quiz' and 'main' of https://github.com…
Alvalens File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,4 @@ | ||
| DATABASE_URL="mysql://username:password@host:port/database" | ||
| NEXTAUTH_SECRET="your-secret-key" | ||
| NEXT_PUBLIC_API_BASE_URL="http://example.com" | ||
| NEXT_PUBLIC_QUIZ_SECRET="your-secret-key" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| "use client"; | ||
|
|
||
| import React from "react"; | ||
| import { toast } from "react-hot-toast"; | ||
|
|
||
| interface Option { | ||
| answer: string; | ||
| isCorrect: boolean; | ||
| } | ||
|
|
||
| interface Question { | ||
| id: string; | ||
| question: string; | ||
| answer: { | ||
| options: Option[]; | ||
| }; | ||
| } | ||
|
|
||
| interface Quiz { | ||
| id: string; | ||
| title: string; | ||
| questions: Question[]; | ||
| } | ||
|
|
||
| interface QuizClientProps { | ||
| quizData: Quiz; | ||
| } | ||
|
|
||
| const QuizClient: React.FC<QuizClientProps> = ({ quizData }) => { | ||
| const [currentQuestion, setCurrentQuestion] = React.useState<number>(0); | ||
| const [score, setScore] = React.useState<number>(0); | ||
| const [showScore, setShowScore] = React.useState<boolean>(false); | ||
| const [startTime, setStartTime] = React.useState<Date | null>(null); | ||
| const [endTime, setEndTime] = React.useState<Date | null>(null); | ||
|
|
||
| React.useEffect(() => { | ||
| if (!startTime) { | ||
| setStartTime(new Date()); | ||
| } | ||
| }, [startTime]); | ||
|
|
||
| const handleQuestion = (selectedOption: Option) => { | ||
| const correct = selectedOption.isCorrect; | ||
| const newScore = correct ? score + 1 : score; | ||
| setScore(newScore); | ||
|
|
||
| if (currentQuestion + 1 === quizData.questions.length) { | ||
| setEndTime(new Date()); | ||
| setShowScore(true); | ||
| saveResult(newScore); | ||
| } else { | ||
| setCurrentQuestion((prev) => prev + 1); | ||
| } | ||
| }; | ||
|
|
||
| const saveResult = async (currentScore: number) => { | ||
| let adjustedScore = currentScore / quizData.questions.length; | ||
| adjustedScore = Math.round(adjustedScore * 100); | ||
| const result = { | ||
| score: adjustedScore, | ||
| timestamp: new Date(), | ||
| }; | ||
|
|
||
| try { | ||
| const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/user/quizzes/${quizData.id}/submit`, { | ||
| method: "POST", | ||
| body: JSON.stringify(result), | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| }); | ||
|
|
||
| if (response.ok) { | ||
| toast.success("Quiz result submitted successfully!"); | ||
| } else { | ||
| const err = await response.json(); | ||
| toast.error(err.error || "Failed to submit quiz result."); | ||
| } | ||
| } catch (error: any) { | ||
| console.log("Error submitting quiz result:", error); | ||
| toast.error("An unexpected error occurred while submitting the quiz result."); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="w-full min-h-screen flex flex-col items-center justify-center py-8 px-4 bg-gray-50"> | ||
| {showScore ? ( | ||
| <div className="flex flex-col items-center w-full max-w-md mx-auto bg-white p-6 rounded-lg shadow-lg"> | ||
| <h1 className="text-3xl font-semibold text-blue-600 mb-4"> | ||
| Your Score: {score} / {quizData.questions.length} | ||
| </h1> | ||
| <p className="text-xl text-gray-700"> | ||
| {score === quizData.questions.length | ||
| ? "Perfect score! Great job!" | ||
| : `You scored ${score} out of ${quizData.questions.length}`} | ||
| </p> | ||
| <p className="text-xl text-gray-700"> | ||
| Time Taken:{" "} | ||
| {endTime && startTime | ||
| ? ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(2) | ||
| : "N/A"}{" "} | ||
| seconds | ||
| </p> | ||
| <button | ||
| onClick={() => window.location.reload()} | ||
| className="mt-8 px-6 py-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 transition" | ||
| > | ||
| Retry Quiz | ||
| </button> | ||
|
|
||
| <button | ||
| onClick={() => window.location.href = "/quiz"} | ||
| className="mt-4 px-6 py-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 transition" | ||
| > | ||
| Back to Quiz | ||
| </button> | ||
|
|
||
| </div> | ||
| ) : ( | ||
| <div className="w-full max-w-md mx-auto bg-white p-6 rounded-lg shadow-lg"> | ||
| <h1 className="text-2xl font-semibold text-center mb-6">{quizData.title}</h1> | ||
| <div className="mb-6"> | ||
| <h2 className="text-lg font-medium text-gray-800"> | ||
| {quizData.questions[currentQuestion].question} | ||
| </h2> | ||
| <div className="mt-4 space-y-3"> | ||
| {quizData.questions[currentQuestion].answer.options.map( | ||
| (option, index) => ( | ||
| <button | ||
| key={index} | ||
| onClick={() => handleQuestion(option)} | ||
| className="w-full py-2 px-4 bg-gray-100 text-gray-800 border border-gray-300 rounded-lg shadow-sm hover:bg-gray-200 transition-all" | ||
| > | ||
| {option.answer} | ||
| </button> | ||
| ) | ||
| )} | ||
| </div> | ||
| </div> | ||
| <div className="text-center mt-6"> | ||
| <span className="text-sm text-gray-500"> | ||
| Question {currentQuestion + 1} of {quizData.questions.length} | ||
| </span> | ||
| </div> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default QuizClient; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| "use client"; | ||
| import { useParams } from 'next/navigation'; | ||
| import React from 'react'; | ||
| import CryptoJS from 'crypto-js'; | ||
|
|
||
|
|
||
| const decryptData = (encryptedData: string) => { | ||
| const secret = process.env.NEXT_PUBLIC_QUIZ_SECRET; | ||
| if (!secret) { | ||
| throw new Error('QUIZ_SECRET is not defined'); | ||
| } | ||
| const bytes = CryptoJS.AES.decrypt(encryptedData, secret); | ||
| const decryptedData = bytes.toString(CryptoJS.enc.Utf8); | ||
| return JSON.parse(decryptedData); | ||
| }; | ||
|
|
||
| export default function QuizPage() { | ||
| const [quizData, setQuizData] = React.useState(null); | ||
| const params = useParams(); | ||
| const id = params?.id as string; | ||
|
|
||
| React.useEffect(() => { | ||
| async function fetchQuizData() { | ||
| const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/user/quizzes/${id}`); | ||
| const { data } = await response.json(); | ||
| if (data) { | ||
| const decryptedQuiz = decryptData(data); | ||
| setQuizData(decryptedQuiz); | ||
| } else { | ||
| console.error('Failed to fetch quiz data'); | ||
| } | ||
| } | ||
|
|
||
| fetchQuizData(); | ||
| }, [id]); | ||
|
|
||
| if (!quizData) { | ||
| return <div>Loading...</div>; // TODO add loading spinner | ||
| } | ||
|
|
||
| // Load client-side component dynamically | ||
| const QuizClient = React.lazy(() => import('./QuizClient')); | ||
|
|
||
| return ( | ||
| <div> | ||
| <React.Suspense fallback={<div>Loading...</div>}> | ||
| <QuizClient quizData={quizData} /> | ||
| </React.Suspense> | ||
| </div> | ||
| ); | ||
| } |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.