From f4e2e14fcd4286589c624a13b2ca62b8c64c210a Mon Sep 17 00:00:00 2001 From: Ayazhan Date: Wed, 9 Apr 2025 12:29:27 +0800 Subject: [PATCH] Revert "Feature/quiz progress" --- .../quiz/[userId]/[quizId]/attempts/route.ts | 56 -- .../progress/quiz/[userId]/[quizId]/route.ts | 51 -- app/api/progress/quiz/route.ts | 97 ---- app/classrooms/[id]/page.tsx | 10 +- app/classrooms/[id]/session/[sid]/page.tsx | 11 +- app/classrooms/page.tsx | 10 +- app/dashboard/page.tsx | 13 +- app/page.tsx | 11 +- app/quizzes/[id]/page.tsx | 11 +- app/quizzes/page.tsx | 14 +- components/LoadingSpinner.tsx | 1 - components/ProtectedRoute.tsx | 8 +- components/classrooms/CreateClassroomForm.tsx | 11 +- components/quiz/ActiveQuiz.tsx | 193 ------- components/quiz/QuizFooter.tsx | 72 --- components/quiz/QuizQuestion.tsx | 109 ---- components/quiz/QuizResults.tsx | 202 ------- components/quiz/helper.ts | 58 -- components/quiz/helpers.tsx | 55 -- components/quiz/quiz-card.tsx | 2 +- components/quiz/quiz-component.tsx | 521 +++++++++++++++--- components/ui/button.tsx | 6 +- components/ui/circular-progress-bar.tsx | 4 +- components/ui/mobile-nav.tsx | 4 +- components/ui/responsive-search.tsx | 11 +- components/ui/theme-toggle.tsx | 6 +- components/user/user-menu.tsx | 2 +- hooks/quizzes/useQuizAttempts.ts | 57 -- hooks/quizzes/useQuizProgress.ts | 53 -- types/quiz/quiz.ts | 22 - 30 files changed, 555 insertions(+), 1126 deletions(-) delete mode 100644 app/api/progress/quiz/[userId]/[quizId]/attempts/route.ts delete mode 100644 app/api/progress/quiz/[userId]/[quizId]/route.ts delete mode 100644 app/api/progress/quiz/route.ts delete mode 100644 components/quiz/ActiveQuiz.tsx delete mode 100644 components/quiz/QuizFooter.tsx delete mode 100644 components/quiz/QuizQuestion.tsx delete mode 100644 components/quiz/QuizResults.tsx delete mode 100644 components/quiz/helper.ts delete mode 100644 components/quiz/helpers.tsx delete mode 100644 hooks/quizzes/useQuizAttempts.ts delete mode 100644 hooks/quizzes/useQuizProgress.ts diff --git a/app/api/progress/quiz/[userId]/[quizId]/attempts/route.ts b/app/api/progress/quiz/[userId]/[quizId]/attempts/route.ts deleted file mode 100644 index 27282d7..0000000 --- a/app/api/progress/quiz/[userId]/[quizId]/attempts/route.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { NextResponse } from 'next/server' -import { collection, query, orderBy, limit, getDocs } from 'firebase/firestore' -import { fireStore } from '@/firebase/firebase' - -export async function GET( - request: Request, - { params }: { params: { userId: string; quizId: string } } -) { - try { - const { userId, quizId } = params - const { searchParams } = new URL(request.url) - const limitParam = parseInt(searchParams.get('limit') || '10') - - if (!userId || !quizId) { - return NextResponse.json( - { error: 'Missing userId or quizId' }, - { status: 400 } - ) - } - - const attemptsRef = collection( - fireStore, - 'users', - userId, - 'quizzes', - quizId, - 'attempts' - ) - - const attemptsQuery = query( - attemptsRef, - orderBy('timestamp', 'desc'), - limit(limitParam) - ) - - const attemptsSnapshot = await getDocs(attemptsQuery) - - if (attemptsSnapshot.empty) { - return NextResponse.json([]) - } - - const attempts = attemptsSnapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - timestamp: doc.data().timestamp?.toMillis() || Date.now(), - })) - - return NextResponse.json(attempts) - } catch (error) { - console.error('Error getting quiz attempts:', error) - return NextResponse.json( - { error: 'Failed to fetch quiz attempts' }, - { status: 500 } - ) - } -} diff --git a/app/api/progress/quiz/[userId]/[quizId]/route.ts b/app/api/progress/quiz/[userId]/[quizId]/route.ts deleted file mode 100644 index 49e3031..0000000 --- a/app/api/progress/quiz/[userId]/[quizId]/route.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { NextResponse } from 'next/server' -import { doc, getDoc } from 'firebase/firestore' -import { fireStore } from '@/firebase/firebase' - -export async function GET( - request: Request, - { params }: { params: { userId: string; quizId: string } } -) { - try { - const { userId, quizId } = params - - if (!userId || !quizId) { - return NextResponse.json( - { error: 'Missing userId or quizId' }, - { status: 400 } - ) - } - - const quizRef = doc(fireStore, 'users', userId, 'quizzes', quizId) - const quizDoc = await getDoc(quizRef) - - if (!quizDoc.exists()) { - return NextResponse.json({ - passed: false, - lastTaken: null, - attempts: 0, - successfulAttempts: 0, - }) - } - - const data = quizDoc.data() - - return NextResponse.json({ - passed: data.passed || false, - lastTaken: data.lastTaken || null, - attempts: data.attempts || 0, - successfulAttempts: data.successfulAttempts || 0, - highestScore: data.highestScore || 0, - score: data.score || 0, - correctAnswers: data.correctAnswers || 0, - totalQuestions: data.totalQuestions || 0, - selectedAnswers: data.selectedAnswers || [], - }) - } catch (error) { - console.error('Error getting quiz progress:', error) - return NextResponse.json( - { error: 'Failed to fetch quiz progress' }, - { status: 500 } - ) - } -} diff --git a/app/api/progress/quiz/route.ts b/app/api/progress/quiz/route.ts deleted file mode 100644 index 91298fb..0000000 --- a/app/api/progress/quiz/route.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { NextResponse } from 'next/server' -import { - doc, - collection, - // addDoc, - getDoc, - setDoc, - increment, -} from 'firebase/firestore' -import { fireStore } from '@/firebase/firebase' - -export async function POST(request: Request) { - try { - const { - userId, - quizId, - quizTitle, - tutorialId, - score, - correctAnswers, - totalQuestions, - selectedAnswers, - } = await request.json() - - if (!userId || !quizId) { - return NextResponse.json( - { error: 'Missing userId or quizId' }, - { status: 400 } - ) - } - - const isPassed = score >= 70 // 70% passing threshold - const timestamp = Date.now() - - // Reference to main quiz document - const quizRef = doc(fireStore, 'users', userId, 'quizzes', quizId) - - // Create attempt document - const attemptRef = doc( - collection(fireStore, 'users', userId, 'quizzes', quizId, 'attempts') - ) - await setDoc(attemptRef, { - timestamp: timestamp, - score: score, - correctAnswers: correctAnswers, - totalQuestions: totalQuestions, - passed: isPassed, - selectedAnswers: selectedAnswers, - }) - - // Check if quiz document exists - const quizDoc = await getDoc(quizRef) - - // Update or create quiz progress document - if (quizDoc.exists()) { - const existingData = quizDoc.data() - const currentHighestScore = existingData.highestScore || 0 - await setDoc( - quizRef, - { - lastTaken: timestamp, - attempts: increment(1), - passed: quizDoc.data().passed || isPassed, // Only set to true if not already passed - highestScore: Math.max(currentHighestScore, score), - successfulAttempts: isPassed ? increment(1) : 0, - score: score, - correctAnswers: correctAnswers, - totalQuestions: totalQuestions, - selectedAnswers: selectedAnswers, - }, - { merge: true } - ) - } else { - await setDoc(quizRef, { - quizTitle: quizTitle, - tutorialId: tutorialId, - lastTaken: timestamp, - attempts: 1, - passed: isPassed, - successfulAttempts: isPassed ? 1 : 0, - highestScore: score, - score: score, - correctAnswers: correctAnswers, - totalQuestions: totalQuestions, - selectedAnswers: selectedAnswers, - }) - } - - return NextResponse.json({ success: true }) - } catch (error) { - console.error('Error recording quiz attempt:', error) - return NextResponse.json( - { error: 'Failed to record quiz attempt' }, - { status: 500 } - ) - } -} diff --git a/app/classrooms/[id]/page.tsx b/app/classrooms/[id]/page.tsx index 12018d2..94e0afa 100644 --- a/app/classrooms/[id]/page.tsx +++ b/app/classrooms/[id]/page.tsx @@ -1,5 +1,5 @@ 'use client' -import { LoadingSpinner } from '@/components/LoadingSpinner' +import { Loader2 } from 'lucide-react' import { useAuth } from '@/contexts/AuthContext' import { SessionManagement } from '@/components/session-views/session-management' @@ -14,7 +14,13 @@ const ClassroomLessonPage: React.FC = ({ params }) => { const classroomId = params.id - if (loading) return + if (loading) { + return ( +
+ +
+ ) + } if (!user) { return ( diff --git a/app/classrooms/[id]/session/[sid]/page.tsx b/app/classrooms/[id]/session/[sid]/page.tsx index dad782c..3ebc068 100644 --- a/app/classrooms/[id]/session/[sid]/page.tsx +++ b/app/classrooms/[id]/session/[sid]/page.tsx @@ -1,7 +1,7 @@ 'use client' // import { useState } from 'react' import { useRouter } from 'next/navigation' -import { LoadingSpinner } from '@/components/LoadingSpinner' +import { Loader2 } from 'lucide-react' import { useAuth } from '@/contexts/AuthContext' import { StudentSessionView } from '@/components/session-views/student-session-view' import { TeacherSessionView } from '@/components/session-views/teacher-session-view' @@ -21,7 +21,14 @@ const SessionPage: React.FC = ({ params }) => { const classroomId = params.id const sessionId = params.sid - if (loading) return + // Show loading state while fetching data + if (loading) { + return ( +
+ +
+ ) + } if (!user) { return ( diff --git a/app/classrooms/page.tsx b/app/classrooms/page.tsx index f5388c4..f71e474 100644 --- a/app/classrooms/page.tsx +++ b/app/classrooms/page.tsx @@ -2,7 +2,7 @@ import { useAuth } from '@/contexts/AuthContext' import { TeacherClassroomsView } from '@/components/classrooms/TeacherClassroomsView' import { StudentClassroomsView } from '@/components/classrooms/StudentClassroomsView' -import { LoadingSpinner } from '@/components/LoadingSpinner' +import { Loader2 } from 'lucide-react' const ClassroomPage = () => { const { user, loading } = useAuth() @@ -10,7 +10,13 @@ const ClassroomPage = () => { // to do: go home // const router = useRouter() - if (loading) return + if (loading) { + return ( +
+ +
+ ) + } return ( <> diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 6496b98..349b8e9 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -2,13 +2,22 @@ import { TeacherDashboard } from '@/components//dashboard/TeacherDashboard' import { StudentDashboard } from '@/components/dashboard/StudentDashboard' import { useAuth } from '@/contexts/AuthContext' -import { LoadingSpinner } from '@/components/LoadingSpinner' +import { Loader2 } from 'lucide-react' import ProtectedRoute from '@/components/ProtectedRoute' export default function DashboardPage() { const { user, loading, signOut } = useAuth() - if (loading) return + if (loading) { + return ( +
+
+ +

Loading...

+
+
+ ) + } return ( diff --git a/app/page.tsx b/app/page.tsx index 13f039e..e9fc830 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -13,6 +13,7 @@ import { import { useRouter } from 'next/navigation' import { useAuth } from '@/contexts/AuthContext' import { + Loader2, PlayCircle, BookOpen, Users2, @@ -37,8 +38,6 @@ import { FeatureItem } from '@/components/feature-item' import { HighlightedText } from '@/components/ui/highlighted-text' import { StatsCard } from '@/components/stats-card' import { Section } from '@/components/ui/section' -import { LoadingSpinner } from '@/components/LoadingSpinner' - export default function HomePage() { @@ -49,7 +48,13 @@ export default function HomePage() { const theme = useTheme() const shadowColor = theme.resolvedTheme === 'dark' ? 'white' : 'black' - if (loading) return + if (loading) { + return ( +
+ +
+ ) + } return (
diff --git a/app/quizzes/[id]/page.tsx b/app/quizzes/[id]/page.tsx index f5569ec..c5fc332 100644 --- a/app/quizzes/[id]/page.tsx +++ b/app/quizzes/[id]/page.tsx @@ -4,7 +4,7 @@ import { useQuiz } from '@/hooks/quiz/useQuizzes' import QuizComponent from '@/components/quiz/quiz-component' import { notFound } from 'next/navigation' import { useEffect } from 'react' -import { LoadingSpinner } from '@/components/LoadingSpinner' +import { Loader2 } from 'lucide-react' export default function QuizPage({ params }: { params: { id: string } }) { const { quiz, isLoading, error } = useQuiz(params.id) @@ -16,7 +16,14 @@ export default function QuizPage({ params }: { params: { id: string } }) { }, [error]) if (isLoading) { - return + return ( +
+
+ +

Loading quiz...

+
+
+ ) } if (!quiz) { diff --git a/app/quizzes/page.tsx b/app/quizzes/page.tsx index 8e00472..3999184 100644 --- a/app/quizzes/page.tsx +++ b/app/quizzes/page.tsx @@ -1,13 +1,21 @@ 'use client' import QuizCard from '@/components/quiz/quiz-card' import { useQuizzes } from '@/hooks/quiz/useQuizzes' -import { LoadingSpinner } from '@/components/LoadingSpinner' - +import { Loader2 } from 'lucide-react' export default function QuizzesPage() { const { quizzes, isLoading, error } = useQuizzes() - if (isLoading) return + if (isLoading) { + return ( +
+
+ +

Loading quizzes...

+
+
+ ) + } if (error) { return ( diff --git a/components/LoadingSpinner.tsx b/components/LoadingSpinner.tsx index 3ffb10c..a1b952a 100644 --- a/components/LoadingSpinner.tsx +++ b/components/LoadingSpinner.tsx @@ -4,7 +4,6 @@ export function LoadingSpinner() { return (
-

Loading...

) } diff --git a/components/ProtectedRoute.tsx b/components/ProtectedRoute.tsx index 6315d21..de5e2e0 100644 --- a/components/ProtectedRoute.tsx +++ b/components/ProtectedRoute.tsx @@ -2,7 +2,7 @@ import { useAuth } from '@/contexts/AuthContext' import { useRouter } from 'next/navigation' import { useEffect } from 'react' -import { LoadingSpinner } from '@/components/LoadingSpinner' +import { Loader2 } from 'lucide-react' interface ProtectedRouteProps { children: React.ReactNode @@ -19,7 +19,11 @@ export default function ProtectedRoute({ children }: ProtectedRouteProps) { }, [user, loading, router]) if (loading) { - return + return ( +
+ +
+ ) } if (!user) return null diff --git a/components/classrooms/CreateClassroomForm.tsx b/components/classrooms/CreateClassroomForm.tsx index 7c1c829..632c997 100644 --- a/components/classrooms/CreateClassroomForm.tsx +++ b/components/classrooms/CreateClassroomForm.tsx @@ -30,9 +30,8 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { LoadingSpinner } from '@/components/LoadingSpinner' -import { useToast } from '@/hooks/use-toast' import { Loader2 } from 'lucide-react' +import { useToast } from '@/hooks/use-toast' interface Props { teacherId: string @@ -201,7 +200,13 @@ export function CreateClassroomForm({ teacherId, teacherSchool }: Props) { }) } - if (loading) return + if (loading) { + return ( +
+ +
+ ) + } return (
diff --git a/components/quiz/ActiveQuiz.tsx b/components/quiz/ActiveQuiz.tsx deleted file mode 100644 index f2d068c..0000000 --- a/components/quiz/ActiveQuiz.tsx +++ /dev/null @@ -1,193 +0,0 @@ -'use client' - -import { useState, useEffect } from 'react' -import { Quiz } from '@/types/quiz/quiz' -import Prism from 'prismjs' -import 'prismjs/components/prism-python' -import { useTheme } from 'next-themes' -import { User } from '@/types/firebase' -import { recordQuizAttempt } from './helper' -import QuizQuestion from './QuizQuestion' -import QuizFooter from './QuizFooter' -import { renderQuestionText } from './helpers' - -interface ActiveQuizProps { - quiz: Quiz - currentQuestionIndex: number - setCurrentQuestionIndex: (index: number) => void - selectedAnswers: number[] - setSelectedAnswers: (answers: number[]) => void - answeredQuestions: boolean[] - setAnsweredQuestions: (answered: boolean[]) => void - setShowResults: (show: boolean) => void - isSubmitting: boolean - setIsSubmitting: (isSubmitting: boolean) => void - user: User | null - invalidateCache: () => void - theme: string | undefined -} - -export default function ActiveQuiz({ - quiz, - currentQuestionIndex, - setCurrentQuestionIndex, - selectedAnswers, - setSelectedAnswers, - answeredQuestions, - setAnsweredQuestions, - setShowResults, - isSubmitting, - setIsSubmitting, - user, - invalidateCache, - theme, -}: ActiveQuizProps) { - const { resolvedTheme } = useTheme() - const [mounted, setMounted] = useState(false) - - useEffect(() => { - setMounted(true) - }, []) - - useEffect(() => { - if (mounted) { - const existingThemeLinks = document.querySelectorAll( - 'link[data-prism-theme]' - ) - existingThemeLinks.forEach((link) => link.remove()) - - const link = document.createElement('link') - link.rel = 'stylesheet' - link.setAttribute('data-prism-theme', 'true') - - if (resolvedTheme === 'dark') { - link.href = - 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css' - } else { - link.href = - 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css' - } - - document.head.appendChild(link) - - setTimeout(() => { - Prism.highlightAll() - }, 0) - } - }, [mounted, theme, resolvedTheme, currentQuestionIndex]) - - const currentQuestion = quiz.questions[currentQuestionIndex] - const isLastQuestion = currentQuestionIndex === quiz.questions.length - 1 - const isAnswered = answeredQuestions[currentQuestionIndex] - const selectedAnswer = selectedAnswers[currentQuestionIndex] - const isCorrect = selectedAnswer === currentQuestion.correctAnswer - - const handleAnswerSelection = (answerIndex: number) => { - if (answeredQuestions[currentQuestionIndex]) return - - const newSelectedAnswers = [...selectedAnswers] - newSelectedAnswers[currentQuestionIndex] = answerIndex - setSelectedAnswers(newSelectedAnswers) - } - - const handleSubmitAnswer = () => { - if (selectedAnswers[currentQuestionIndex] === -1) return - - const newAnsweredQuestions = [...answeredQuestions] - newAnsweredQuestions[currentQuestionIndex] = true - setAnsweredQuestions(newAnsweredQuestions) - } - - const handleNextQuestion = () => { - if (currentQuestionIndex < quiz.questions.length - 1) { - setCurrentQuestionIndex(currentQuestionIndex + 1) - } - } - - const handlePreviousQuestion = () => { - if (currentQuestionIndex > 0) { - setCurrentQuestionIndex(currentQuestionIndex - 1) - } - } - - const handleSubmitQuiz = async () => { - if (!user || isSubmitting) return - - setIsSubmitting(true) - const score = calculateScore() - - const success = await recordQuizAttempt( - user, - quiz.id, - quiz.title, - quiz.tutorialId, - score.percentage, - score.correctAnswers, - score.totalQuestions, - selectedAnswers - ) - - if (success) { - invalidateCache() - } - - setShowResults(true) - setIsSubmitting(false) - } - - const calculateScore = () => { - let correctAnswers = 0 - quiz.questions.forEach((question, index) => { - if (selectedAnswers[index] === question.correctAnswer) { - correctAnswers++ - } - }) - return { - correctAnswers, - totalQuestions: quiz.questions.length, - percentage: Math.round((correctAnswers / quiz.questions.length) * 100), - } - } - - const allQuestionsAnswered = answeredQuestions.every((item) => item === true) - - return ( -
-
-
-
-

- {quiz.title} -

-

- Question {currentQuestionIndex + 1} of {quiz.questions.length} -

-
- -
-
-
- - q).length} - onPrevious={handlePreviousQuestion} - onNext={handleNextQuestion} - onSubmitAnswer={handleSubmitAnswer} - onFinishQuiz={handleSubmitQuiz} - /> -
- ) -} diff --git a/components/quiz/QuizFooter.tsx b/components/quiz/QuizFooter.tsx deleted file mode 100644 index cf685b5..0000000 --- a/components/quiz/QuizFooter.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Button } from '@/components/ui/button' -import { ArrowLeft, ArrowRight } from 'lucide-react' - -interface QuizFooterProps { - currentQuestionIndex: number - totalQuestions: number - isAnswered: boolean - isLastQuestion: boolean - selectedAnswer: number - allQuestionsAnswered: boolean - answeredQuestionsCount: number - onPrevious: () => void - onNext: () => void - onSubmitAnswer: () => void - onFinishQuiz: () => void -} - -export default function QuizFooter({ - currentQuestionIndex, - totalQuestions, - isAnswered, - isLastQuestion, - selectedAnswer, - allQuestionsAnswered, - answeredQuestionsCount, - onPrevious, - onNext, - onSubmitAnswer, - onFinishQuiz, -}: QuizFooterProps) { - return ( -
- - -
- - {currentQuestionIndex + 1} / {totalQuestions} - -
- - {!isAnswered ? ( - - ) : isLastQuestion ? ( - - ) : ( - - )} -
- ) -} diff --git a/components/quiz/QuizQuestion.tsx b/components/quiz/QuizQuestion.tsx deleted file mode 100644 index 7167be2..0000000 --- a/components/quiz/QuizQuestion.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' -import { Label } from '@/components/ui/label' -import { CheckCircle, AlertCircle } from 'lucide-react' -import Image from 'next/image' -import { Question } from '@/types/quiz/quiz' - -interface QuizQuestionProps { - question: Question - selectedAnswer: number - isAnswered: boolean - isCorrect: boolean - onSelect: (answerIndex: number) => void - renderQuestionText: (text: string) => React.ReactNode -} - -export default function QuizQuestion({ - question, - selectedAnswer, - isAnswered, - isCorrect, - onSelect, - renderQuestionText, -}: QuizQuestionProps) { - return ( -
-

- {renderQuestionText(question.question)} -

- - {question.imageUrl && ( -
- Question image -
- )} - - onSelect(parseInt(value))} - disabled={isAnswered} - className='space-y-1' - > - {question.options.map((option, index) => ( -
- - - {isAnswered && index === question.correctAnswer && ( - - )} - {isAnswered && index === selectedAnswer && !isCorrect && ( - - )} -
- ))} -
- - {isAnswered && ( -
-

- {isCorrect ? ( -

- - Correct! -
- ) : ( -
- - Let's Review This One! -
- )} -

-
{renderQuestionText(question.explanation)}
-
- )} -
- ) -} diff --git a/components/quiz/QuizResults.tsx b/components/quiz/QuizResults.tsx deleted file mode 100644 index d6c35ec..0000000 --- a/components/quiz/QuizResults.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { CircularProgress } from '@/components/ui/circular-progress-bar' -import { Button } from '@/components/ui/button' -import { CheckCircle, AlertCircle } from 'lucide-react' -import Image from 'next/image' -import Link from 'next/link' -import { Quiz, QuizProgress } from '@/types/quiz/quiz' - -interface ScoreData { - correctAnswers: number - totalQuestions: number - percentage: number -} - -interface QuizResultsProps { - quiz: Quiz - score: ScoreData - selectedAnswers: number[] - progress: QuizProgress | null - theme: string | undefined - onRetakeQuiz: () => void - renderQuestionText: (text: string) => React.ReactNode -} - -export default function QuizResults({ - quiz, - score, - selectedAnswers, - progress, - theme, - onRetakeQuiz, - renderQuestionText, -}: QuizResultsProps) { - const passingScore = 70 - const incorrectAnswers = score.totalQuestions - score.correctAnswers - const isDarkTheme = theme === 'dark' - - return ( -
-
-

- Quiz Summary -

-

- {quiz.title} -

- -
-
- -
- -
-

- {score.percentage >= passingScore - ? 'Congratulations! You passed!' - : `Get ${passingScore}% to pass`} - {progress && ( - <> - {progress.highestScore && ( - <> | Highest Score: {progress.highestScore}% - )} - - )} -

- -
-
- - - {score.correctAnswers} - - correct -
- -
- - - {incorrectAnswers} - - incorrect -
-
- -
- - -
-
-
-
- -
-
-

Review Your Answers

- {quiz.questions.map((question, index) => { - const isCorrect = selectedAnswers[index] === question.correctAnswer - - return ( -
- {/* Top section with icon and question number */} -
-
-
-
- {isCorrect ? ( - - ) : ( - - )} - - Question {index + 1} - -
-
-
-
- -
-
- {renderQuestionText(question.question)} -
-
- - {question.imageUrl && ( -
- {`Question -
- )} - -
-

- Your answer:{' '} - {selectedAnswers[index] !== -1 - ? question.options[selectedAnswers[index]] - : 'Not answered'} -

- {!isCorrect && ( -

- Correct answer: {question.options[question.correctAnswer]} -

- )} -
- - {/* Explanation Card - only shown if there is an explanation */} - {question.explanation && ( -
- Explanation: - {renderQuestionText(question.explanation)} -
- )} -
- ) - })} -
-
- -
- -
-
- ) -} diff --git a/components/quiz/helper.ts b/components/quiz/helper.ts deleted file mode 100644 index a4cd109..0000000 --- a/components/quiz/helper.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { User } from '@/types/firebase' - -export async function recordQuizAttempt( - user: User | null, - quizId: string, - quizTitle: string, - tutorialId: string, - score: number, - correctAnswers: number, - totalQuestions: number, - selectedAnswers: number[] -) { - if (!user) return false - - try { - const response = await fetch('/api/progress/quiz', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - userId: user.uid, - quizId, - quizTitle, - tutorialId, - score, - correctAnswers, - totalQuestions, - selectedAnswers, - }), - }) - - if (!response.ok) { - throw new Error('Failed to record quiz attempt') - } - - return true - } catch (error) { - console.error('Error recording quiz attempt:', error) - return false - } -} - - -// Helper function to calculate score -// export function calculateScore(quiz: Quiz, selectedAnswers: number[]) { -// let correctAnswers = 0 -// quiz.questions.forEach((question, index) => { -// if (selectedAnswers[index] === question.correctAnswer) { -// correctAnswers++ -// } -// }) -// return { -// correctAnswers, -// totalQuestions: quiz.questions.length, -// percentage: Math.round((correctAnswers / quiz.questions.length) * 100), -// } -// } \ No newline at end of file diff --git a/components/quiz/helpers.tsx b/components/quiz/helpers.tsx deleted file mode 100644 index e03f099..0000000 --- a/components/quiz/helpers.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react' -import Prism from 'prismjs' -import 'prismjs/components/prism-python' - -export function renderQuestionText(text: string) { - if (!text || !text.includes('```python')) { - return text - } - - const regex = /```python([\s\S]*?)```/g - let lastIndex = 0 - const elements = [] - let key = 0 - const isDarkMode = document.documentElement.classList.contains('dark') - - let match; - while ((match = regex.exec(text)) !== null) { - if (match.index > lastIndex) { - elements.push( - {text.substring(lastIndex, match.index)} - ) - } - - const code = match[1].trim() - const highlightedCode = Prism.highlight( - code, - Prism.languages.python, - 'python' - ) - - elements.push( -
-        
-      
- ) - - lastIndex = match.index + match[0].length - } - - if (lastIndex < text.length) { - elements.push({text.substring(lastIndex)}) - } - - return <>{elements} -} diff --git a/components/quiz/quiz-card.tsx b/components/quiz/quiz-card.tsx index 3b9802e..3b0be46 100644 --- a/components/quiz/quiz-card.tsx +++ b/components/quiz/quiz-card.tsx @@ -52,7 +52,7 @@ export default function QuizCard({ {questionCount} questions

-
+
diff --git a/components/quiz/quiz-component.tsx b/components/quiz/quiz-component.tsx index 004a37e..9986c32 100644 --- a/components/quiz/quiz-component.tsx +++ b/components/quiz/quiz-component.tsx @@ -1,20 +1,24 @@ 'use client' +import { CircularProgress } from '@/components/ui/circular-progress-bar' import { useState, useEffect } from 'react' import { Quiz } from '@/types/quiz/quiz' +import { Button } from '@/components/ui/button' +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' +import { Label } from '@/components/ui/label' +import { CheckCircle, AlertCircle, ArrowRight, ArrowLeft } from 'lucide-react' +import Image from 'next/image' +import Prism from 'prismjs' +import 'prismjs/components/prism-python' import { useTheme } from 'next-themes' -import { useAuth } from '@/contexts/AuthContext' -import { useQuizProgress } from '@/hooks/quizzes/useQuizProgress' -import QuizResults from './QuizResults' -import ActiveQuiz from './ActiveQuiz' -import { renderQuestionText } from './helpers' -import { LoadingSpinner } from '@/components/LoadingSpinner' +import Link from 'next/link' interface QuizComponentProps { quiz: Quiz } export default function QuizComponent({ quiz }: QuizComponentProps) { + const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0) const [selectedAnswers, setSelectedAnswers] = useState( Array(quiz.questions.length).fill(-1) ) @@ -22,82 +26,473 @@ export default function QuizComponent({ quiz }: QuizComponentProps) { Array(quiz.questions.length).fill(false) ) const [showResults, setShowResults] = useState(false) - const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0) - const [isSubmitting, setIsSubmitting] = useState(false) + const { theme, resolvedTheme } = useTheme() const [mounted, setMounted] = useState(false) - const { theme } = useTheme() - const { user } = useAuth() - const { progress, invalidateCache, isLoading } = useQuizProgress( - quiz.id, - user - ) + useEffect(() => { + setMounted(true) + }, []) useEffect(() => { - if (progress && progress.attempts > 0) { - // If user has taken the quiz before, use their latest answers - setSelectedAnswers( - progress.selectedAnswers || Array(quiz.questions.length).fill(-1) + if (mounted) { + const existingThemeLinks = document.querySelectorAll( + 'link[data-prism-theme]' ) - setShowResults(true) + existingThemeLinks.forEach((link) => link.remove()) + + const link = document.createElement('link') + link.rel = 'stylesheet' + link.setAttribute('data-prism-theme', 'true') + + if (resolvedTheme === 'dark') { + link.href = + 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css' + } else { + link.href = + 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css' + } + + document.head.appendChild(link) + + setTimeout(() => { + Prism.highlightAll() + }, 0) } - }, [progress, quiz.questions.length]) + }, [mounted, theme, resolvedTheme, currentQuestionIndex, showResults]) - useEffect(() => { - setMounted(true) - }, []) + const currentQuestion = quiz.questions[currentQuestionIndex] + const isLastQuestion = currentQuestionIndex === quiz.questions.length - 1 + const isAnswered = answeredQuestions[currentQuestionIndex] + const selectedAnswer = selectedAnswers[currentQuestionIndex] + const isCorrect = selectedAnswer === currentQuestion.correctAnswer + + const handleAnswerSelection = (answerIndex: number) => { + if (answeredQuestions[currentQuestionIndex]) return + + const newSelectedAnswers = [...selectedAnswers] + newSelectedAnswers[currentQuestionIndex] = answerIndex + setSelectedAnswers(newSelectedAnswers) + } + + const handleSubmitAnswer = () => { + if (selectedAnswers[currentQuestionIndex] === -1) return - if (!mounted || isLoading) { - return + const newAnsweredQuestions = [...answeredQuestions] + newAnsweredQuestions[currentQuestionIndex] = true + setAnsweredQuestions(newAnsweredQuestions) } - const handleRetakeQuiz = () => { - setAnsweredQuestions(Array(quiz.questions.length).fill(false)) - setShowResults(false) - setCurrentQuestionIndex(0) - setSelectedAnswers(Array(quiz.questions.length).fill(-1)) + const handleNextQuestion = () => { + if (currentQuestionIndex < quiz.questions.length - 1) { + setCurrentQuestionIndex(currentQuestionIndex + 1) + } } - if (showResults) { - const scoreData = { - correctAnswers: progress?.correctAnswers || 0, - totalQuestions: progress?.totalQuestions || quiz.questions.length, - percentage: progress?.score || 0, + const handlePreviousQuestion = () => { + if (currentQuestionIndex > 0) { + setCurrentQuestionIndex(currentQuestionIndex - 1) + } + } + + const handleSubmitQuiz = () => { + setShowResults(true) + } + + const calculateScore = () => { + let correctAnswers = 0 + quiz.questions.forEach((question, index) => { + if (selectedAnswers[index] === question.correctAnswer) { + correctAnswers++ + } + }) + return { + correctAnswers, + totalQuestions: quiz.questions.length, + percentage: Math.round((correctAnswers / quiz.questions.length) * 100), + } + } + + const isDarkMode = resolvedTheme === 'dark' + + const renderQuestionText = (text: string) => { + if (!text || !text.includes('```python')) { + return text + } + + const regex = /```python([\s\S]*?)```/g + let lastIndex = 0 + const elements = [] + let match + let key = 0 + + while ((match = regex.exec(text)) !== null) { + if (match.index > lastIndex) { + elements.push( + {text.substring(lastIndex, match.index)} + ) + } + + const code = match[1].trim() + const highlightedCode = Prism.highlight( + code, + Prism.languages.python, + 'python' + ) + + elements.push( +
+          
+        
+ ) + + lastIndex = match.index + match[0].length } - const displayAnswers = - progress && progress.selectedAnswers - ? progress.selectedAnswers - : selectedAnswers + if (lastIndex < text.length) { + elements.push({text.substring(lastIndex)}) + } + return <>{elements} + } + + const allQuestionsAnswered = answeredQuestions.every((item) => item === true) + const score = calculateScore() + const isDarkTheme = theme === 'dark' + + if (!mounted) { + return null + } + + if (showResults) { + const passingScore = 70 + const incorrectAnswers = score.totalQuestions - score.correctAnswers return ( - +
+
+

+ Quiz Summary +

+

+ {quiz.title} +

+ + {/* Circular Progress Bar and Details */} +
+ {/* Circular Progress Bar */} +
+ +
+ + {/* Score Details */} +
+

+ {score.percentage >= passingScore + ? 'Congratulations! You passed!' + : `Get ${passingScore}% to pass`} +

+ +
+
+ + + {score.correctAnswers} + + correct +
+ +
+ + + {incorrectAnswers} + + incorrect +
+
+ +
+ + +
+
+
+
+ +
+
+ {quiz.questions.map((question, index) => { + const isCorrect = + selectedAnswers[index] === question.correctAnswer + + return ( +
+ {/* Top section with icon and question number */} +
+
+
+
+ {isCorrect ? ( + + ) : ( + + )} + + Question {index + 1} + +
+
+
+
+ + {/* Question text */} +
+
+ {renderQuestionText(question.question)} +
+
+ + {/* Display image if available */} + {question.imageUrl && ( +
+ {`Question +
+ )} + + {/* Answer Card */} +
+

+ Your answer: {question.options[selectedAnswers[index]]} +

+ {!isCorrect && ( +

+ Correct answer:{' '} + {question.options[question.correctAnswer]} +

+ )} +
+ + {/* Explanation Card - only shown if there is an explanation */} + {question.explanation && ( +
+ Explanation: + {renderQuestionText(question.explanation)} +
+ )} +
+ ) + })} +
+
+
) } return ( - +
+ {/* Main Content */} +
+
+
+

+ {quiz.title} +

+

+ Question {currentQuestionIndex + 1} of {quiz.questions.length} +

+
+
+

+ {renderQuestionText(currentQuestion.question)} +

+ + {/* Display image if available */} + {currentQuestion.imageUrl && ( +
+ Question image +
+ )} + + + handleAnswerSelection(parseInt(value)) + } + disabled={isAnswered} + className='space-y-1' + > + {currentQuestion.options.map((option, index) => ( +
+ + + {isAnswered && index === currentQuestion.correctAnswer && ( + + )} + {isAnswered && index === selectedAnswer && !isCorrect && ( + + )} +
+ ))} +
+ + {isAnswered && ( +
+

+ {isCorrect ? ( +

+ + Correct! +
+ ) : ( +
+ + + Let's Review This One! + +
+ )} +

+
{renderQuestionText(currentQuestion.explanation)}
+
+ )} +
+
+
+
+ + {/* Footer */} +
+ + +
+ + {currentQuestionIndex + 1} / {quiz.questions.length} + +
+ + {!isAnswered ? ( + + ) : isLastQuestion ? ( + + ) : ( + + )} +
+
) } diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 55551f3..e8ad12e 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -12,12 +12,12 @@ const buttonVariants = cva( destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 hover:-translate-y-0.5', outline: - 'border border-input border-teal-400 shadow-sm hover:-translate-y-0.5 active:translate-y-0 transition-all duration-200 hover:bg-accent hover:text-accent-foreground', + 'border border-2 border-teal-400 shadow-sm hover:-translate-y-0.5 active:translate-y-0 transition-all duration-200', secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80 hover:-translate-y-0.5 active:translate-y-0', whiteGray: 'bg-zinc-100 text-secondary-foreground shadow-sm hover:bg-zinc-100/90 dark:bg-zinc-700 dark:hover:bg-zinc-700/90 hover:-translate-y-0.5 active:translate-y-0', - ghost: 'hover:text-accent-foreground', + ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', softBlue: 'bg-gradient-to-r from-purple-100 via-purple-200 to-purple-100 text-primary font-normal shadow-sm border border-purple-200/50 hover:shadow-md hover:from-purple-200 hover:via-purple-300 hover:to-purple-200 hover:-translate-y-0.5 active:translate-y-0 dark:from-purple-900/70 dark:via-purple-800/70 dark:to-purple-900/70 dark:border-purple-700/20 dark:hover:from-purple-800/70 dark:hover:via-purple-700/70 dark:hover:to-purple-800/70', @@ -109,7 +109,7 @@ const buttonVariants = cva( 'relative bg-gradient-to-r from-purple-600 via-purple-500 to-purple-400 text-white shadow-lg hover:from-purple-700 hover:via-purple-600 hover:to-purple-500 transition-all duration-300 hover:shadow-xl hover:-translate-y-0.5 active:translate-y-0', }, size: { - default: 'h-9 px-4 py-2', + default: 'h-9 p-4', sm: 'h-8 rounded-md px-3 text-xs', lg: 'h-10 px-8', icon: 'h-9 w-9', diff --git a/components/ui/circular-progress-bar.tsx b/components/ui/circular-progress-bar.tsx index 145fcf6..223beb7 100644 --- a/components/ui/circular-progress-bar.tsx +++ b/components/ui/circular-progress-bar.tsx @@ -21,7 +21,7 @@ export const CircularProgress = ({ size = 160, strokeWidth = 8, showText = true, - textSize = 'text-2xl', + textSize = 'text-lg', primaryColor = 'stroke-teal-500', secondaryColor = 'stroke-gray-200 dark:stroke-gray-700', className = '', @@ -83,7 +83,7 @@ export const CircularProgress = ({ {showText && (
- + {clampedPercentage}%
diff --git a/components/ui/mobile-nav.tsx b/components/ui/mobile-nav.tsx index 7191896..dc6de4e 100644 --- a/components/ui/mobile-nav.tsx +++ b/components/ui/mobile-nav.tsx @@ -26,8 +26,8 @@ const MobileNav = () => { - - diff --git a/components/user/user-menu.tsx b/components/user/user-menu.tsx index ebfd640..b9334a1 100644 --- a/components/user/user-menu.tsx +++ b/components/user/user-menu.tsx @@ -47,7 +47,7 @@ export function UserMenu() { return ( -