From 19d2c22d154c9efe62912640db156349a2c88a4b Mon Sep 17 00:00:00 2001 From: Vasiliki Doropoulou Date: Sun, 16 Feb 2025 15:19:36 +0100 Subject: [PATCH 1/9] feat: improve quiz loading with rate limiting and enhanced error handling - Add rate limiting mechanism to prevent rapid API requests - Implement comprehensive error handling for question loading - Add isWaiting state to manage request throttling - Enhance logging and error tracking during question retrieval - Improve custom mode question selection logic - Add more robust validation for API responses --- src/hooks/useQuiz.ts | 240 +++++++++++++++++++++++++++++++------------ 1 file changed, 177 insertions(+), 63 deletions(-) diff --git a/src/hooks/useQuiz.ts b/src/hooks/useQuiz.ts index 4c6e3f5..3a5b568 100644 --- a/src/hooks/useQuiz.ts +++ b/src/hooks/useQuiz.ts @@ -1,5 +1,5 @@ // src/hooks/useQuiz.ts -import { useState, useCallback, useEffect } from "react"; +import { useState, useCallback, useEffect, useRef } from "react"; import { Question, Topic, Difficulty } from "@/types/quiz"; interface TopicProgress { @@ -80,6 +80,13 @@ export const useQuiz = () => { AnsweredQuestion[] >([]); + // Add a ref to track the last request time + const lastRequestTime = useRef(0); + const RATE_LIMIT_DELAY = 2000; // 2 seconds between requests + + // Add a new state for rate limit waiting + const [isWaiting, setIsWaiting] = useState(false); + // Load saved state from localStorage after component mounts useEffect(() => { try { @@ -130,50 +137,183 @@ export const useQuiz = () => { }, [state, answeredQuestions, isInitialized]); const loadQuestion = useCallback(async () => { + // Add check for custom mode + if (state.isCustomMode && state.customQuestions.length > 0) { + // Use existing custom questions instead of loading new ones + const randomIndex = Math.floor( + Math.random() * state.customQuestions.length + ); + setState((prev) => ({ + ...prev, + questions: [state.customQuestions[randomIndex]], + currentQuestionIndex: 0, + error: null, + })); + return; + } + try { + // Check if enough time has passed since last request + const now = Date.now(); + const timeSinceLastRequest = now - lastRequestTime.current; + + if (timeSinceLastRequest < RATE_LIMIT_DELAY) { + setIsWaiting(true); + const waitTime = RATE_LIMIT_DELAY - timeSinceLastRequest; + console.log(`Waiting ${waitTime}ms before next request...`); + await new Promise((resolve) => setTimeout(resolve, waitTime)); + setIsWaiting(false); + } + setLoading(true); setError(null); + // Update last request time + lastRequestTime.current = Date.now(); + const getNextTopic = (): Topic => { + // Ensure we're using the correct difficulty level const levelTopics = topics[state.difficulty]; + if (!levelTopics || levelTopics.length === 0) { + throw new Error( + `No topics found for difficulty: ${state.difficulty}` + ); + } const randomIndex = Math.floor(Math.random() * levelTopics.length); return levelTopics[randomIndex]; }; - const response = await fetch("/api/quiz", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - topic: getNextTopic(), - difficulty: state.difficulty, - excludePatterns: state.questions - .map((q) => q.question) - .join(" ") - .substring(0, 100), - }), - }); - - if (!response.ok) { - throw new Error("Failed to load question"); + // Add validation to ensure we have a valid difficulty and topic + const selectedTopic = getNextTopic(); + if (!selectedTopic || !state.difficulty) { + throw new Error("Invalid topic or difficulty"); } - const question = await response.json(); + // // Clean and format the exclude patterns + // const excludePatterns = state.questions + // .slice(-3) + // .map((q) => q.question) + // .map((question) => + // // Remove special characters and escape quotes + // question + // .replace(/[\n\r\t]/g, " ") + // .replace(/"/g, '\\"') + // .trim() + // ) + // .join(" ") + // .substring(0, 100); + + const requestBody = { + topic: selectedTopic, + difficulty: state.difficulty, + // excludePatterns, + }; + + console.log("Sending request with:", requestBody); - setState((prev) => ({ - ...prev, - questions: [question], - currentQuestionIndex: 0, - })); + try { + const response = await fetch("/api/quiz", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }); + + let data; + try { + data = await response.json(); + } catch (parseError) { + console.error("Failed to parse response:", parseError); + throw new Error("Server returned invalid JSON"); + } + + // Check for error responses first + if (!response.ok) { + const errorDetails = { + status: response.status, + statusText: response.statusText, + data: data || "No error data", + }; + + console.error("Server error details:", errorDetails); + + // Try to extract a meaningful error message + let errorMessage = "Unknown server error"; + if (typeof data === "object" && data !== null) { + errorMessage = + data.error || + data.message || + `Error ${response.status}: ${response.statusText}`; + } else if (typeof data === "string") { + errorMessage = data; + } + + throw new Error(errorMessage); + } + + // Validate the response data + if (!data || typeof data !== "object") { + console.error("Invalid data structure received:", data); + throw new Error("Server returned invalid data structure"); + } + + if (!data.topic || !data.difficulty || !data.question) { + console.error("Missing required fields in response:", data); + throw new Error("Server returned incomplete question data"); + } + + // Validate difficulty match + if (data.difficulty !== state.difficulty) { + console.error("Difficulty mismatch:", { + expected: state.difficulty, + received: data.difficulty, + }); + throw new Error( + `Received question with wrong difficulty: ${data.difficulty}` + ); + } + + console.log("Successfully loaded question:", { + topic: data.topic, + difficulty: data.difficulty, + }); + + setState((prev) => ({ + ...prev, + questions: [data], + currentQuestionIndex: 0, + error: null, + })); + } catch (fetchError) { + console.error("Fetch error details:", { + error: fetchError, + message: + fetchError instanceof Error + ? fetchError.message + : String(fetchError), + }); + + throw new Error( + fetchError instanceof Error + ? fetchError.message + : "Failed to communicate with server" + ); + } } catch (error) { + console.error("Error in loadQuestion:", error); setError( error instanceof Error ? error.message : "Failed to load question" ); + setState((prev) => ({ + ...prev, + questions: [], + currentQuestionIndex: 0, + })); } finally { setLoading(false); } - }, [state.questions, state.difficulty]); + }, [state.difficulty, state.isCustomMode, state.customQuestions]); const handleAnswer = useCallback( (answer: string) => { @@ -251,51 +391,18 @@ export const useQuiz = () => { const hasNextQuestion = prev.questions && nextIndex < prev.questions.length; - // If we're in custom mode and completed all questions - if (!hasNextQuestion && prev.isCustomMode) { - // Track the current question as used - const currentQuestion = prev.questions[prev.currentQuestionIndex]; - const updatedUsedQuestions = new Set(prev.usedCustomQuestions); - updatedUsedQuestions.add(currentQuestion.question); - - // If we've used all questions, reset the used questions set - if (updatedUsedQuestions.size >= prev.customQuestions.length) { - return { - ...prev, - currentQuestionIndex: 0, - usedCustomQuestions: new Set(), - }; - } - - // Find the next unused question - const unusedQuestions = prev.customQuestions.filter( - (q) => !updatedUsedQuestions.has(q.question) - ); - - if (unusedQuestions.length > 0) { - // Randomly select an unused question - const randomIndex = Math.floor( - Math.random() * unusedQuestions.length - ); - const nextQuestions = [unusedQuestions[randomIndex]]; - - return { - ...prev, - questions: nextQuestions, - currentQuestionIndex: 0, - usedCustomQuestions: updatedUsedQuestions, - totalQuestions: prev.customQuestions.length, - }; - } + if (!hasNextQuestion) { + // If no next question, trigger a load with rate limiting + loadQuestion(); + return prev; } - // Regular mode or has next question return { ...prev, currentQuestionIndex: nextIndex, }; }); - }, []); + }, [loadQuestion]); const toggleCustomMode = useCallback( (enabled: boolean) => { @@ -328,7 +435,13 @@ export const useQuiz = () => { difficulty: newDifficulty, currentQuestionIndex: 0, questions: [], + score: 0, + streak: 0, + correctAnswersInRow: 0, + error: null, + loading: false, })); + loadQuestion(); }, [loadQuestion] @@ -357,6 +470,7 @@ export const useQuiz = () => { isCustomMode: state.isCustomMode, toggleCustomMode, setDifficulty, + isWaiting, // submitQuiz, }; }; From 9aabbf6cec91d25eea302dddf421c0ea34782e67 Mon Sep 17 00:00:00 2001 From: Vasiliki Doropoulou Date: Sun, 16 Feb 2025 15:38:03 +0100 Subject: [PATCH 2/9] feat: update LoadingCard to display dynamic waiting message - Add isWaiting prop to LoadingCard to show context-specific loading text - Modify QuizApp to pass custom loading message based on quiz state --- src/components/QuizApp.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/QuizApp.tsx b/src/components/QuizApp.tsx index 3a4cd6f..e9b94fd 100644 --- a/src/components/QuizApp.tsx +++ b/src/components/QuizApp.tsx @@ -36,6 +36,7 @@ export default function QuizApp() { isCustomMode, toggleCustomMode, setDifficulty, + isWaiting, } = useQuiz(); const [mounted, setMounted] = useState(false); @@ -50,7 +51,12 @@ export default function QuizApp() { }, [currentQuestion, loadQuestion]); if (!mounted) return null; - if (loading) return ; + if (loading) + return ( + + ); if (error) return ; if (!currentQuestion) return null; From 7881b75457ede902f8e40d81a0497a4cbee52515 Mon Sep 17 00:00:00 2001 From: Vasiliki Doropoulou Date: Sun, 16 Feb 2025 15:39:38 +0100 Subject: [PATCH 3/9] refactor: simplify LoadingCard component with improved styling --- src/components/quiz/LoadingCard.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/quiz/LoadingCard.tsx b/src/components/quiz/LoadingCard.tsx index fe4cef5..692f80c 100644 --- a/src/components/quiz/LoadingCard.tsx +++ b/src/components/quiz/LoadingCard.tsx @@ -1,13 +1,13 @@ -import { Card, CardContent } from "@/components/ui/card"; -import { Loader } from "lucide-react"; +interface LoadingCardProps { + message?: string; +} -export function LoadingCard() { +export const LoadingCard = ({ message = "Loading..." }: LoadingCardProps) => { return ( - - - - Loading question... - - +
+
+

{message}

+
+
); -} +}; From 58934b3f487add21c9fc13e050174065857a4c0c Mon Sep 17 00:00:00 2001 From: Vasiliki Doropoulou Date: Sun, 16 Feb 2025 16:01:07 +0100 Subject: [PATCH 4/9] feat: add code syntax highlighting for quiz answer options - Implement code rendering for options wrapped in backticks - Use SyntaxHighlighter with OneDark theme for code options - Update prompt generation guidelines to support code-formatted answers - Improve accessibility and visual presentation of answer options --- src/app/api/quiz/prompt.ts | 29 ++++++++++++++------- src/components/quiz/AnswerOptions.tsx | 36 +++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/app/api/quiz/prompt.ts b/src/app/api/quiz/prompt.ts index 1842421..27ec9f8 100644 --- a/src/app/api/quiz/prompt.ts +++ b/src/app/api/quiz/prompt.ts @@ -57,19 +57,30 @@ export function generatePrompt( 9. Do not combine multiple options into one answer 10. Each option should be a complete, standalone answer 11. MUST provide EXACTLY 4 options - no more, no less - + 12. For optimization/improvement questions, options should contain actual code solutions + 13. When showing code in options, use proper formatting with backticks + Example of INCORRECT format: ❌ options: ["12", "65", "18", "70", "55"] // Wrong - has 5 options ❌ correctAnswer: "12 and 65" // Wrong - combines options - - Example of CORRECT format: + + Example of CORRECT format for code-based answers: + ✓ options: [ + "\`const user = { name: 'John', age: Number('30') };\`", + "\`const user = { name: 'John', age: parseInt('30') };\`", + "\`const user = { name: 'John', age: +'30' };\`", + "\`const user = { name: 'John', age: '30' };\`" + ] + ✓ correctAnswer: "\`const user = { name: 'John', age: Number('30') };\`" + + Example of CORRECT format for text-based answers: ✓ options: [ - "Age under 12 or over 65", - "Age under 18", - "Age over 21", - "None of the above" - ] // Correct - exactly 4 options - ✓ correctAnswer: "Age under 12 or over 65" // Correct - matches exactly one option + "Use the Number() function to convert strings to numbers", + "Leave the value as a string", + "Use JSON.stringify() on the entire object", + "Remove the age property entirely" + ] + ✓ correctAnswer: "Use the Number() function to convert strings to numbers" The response must be pure JSON only.`; } diff --git a/src/components/quiz/AnswerOptions.tsx b/src/components/quiz/AnswerOptions.tsx index 55eb095..640dac1 100644 --- a/src/components/quiz/AnswerOptions.tsx +++ b/src/components/quiz/AnswerOptions.tsx @@ -1,5 +1,7 @@ import { motion } from "framer-motion"; import { Button } from "@/components/ui/button"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; interface AnswerOptionsProps { options: string[]; @@ -16,6 +18,28 @@ export function AnswerOptions({ showExplanation, onAnswerSelect, }: AnswerOptionsProps) { + const renderOptionContent = (option: string) => { + // Check if the option is wrapped in backticks + const codeMatch = option.match(/^`(.+)`$/); + if (codeMatch) { + return ( + + {codeMatch[1]} + + ); + } + return option; + }; + return (
{options.map((option, index) => ( @@ -47,12 +71,14 @@ export function AnswerOptions({ }`} role="radio" aria-checked={option === selectedAnswer} - aria-label={option} + aria-label={option.replace(/`/g, "")} > - - {String.fromCharCode(65 + index)} - - {option} +
+ + {String.fromCharCode(65 + index)} + +
{renderOptionContent(option)}
+
))} From d5e27247365e08dcc945ed99e81b39e3252c6c00 Mon Sep 17 00:00:00 2001 From: Vasiliki Doropoulou Date: Sun, 16 Feb 2025 16:18:17 +0100 Subject: [PATCH 5/9] refactor: switch from OneDark to VSC Dark Plus theme for code syntax highlighting --- src/components/quiz/AnswerOptions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/quiz/AnswerOptions.tsx b/src/components/quiz/AnswerOptions.tsx index 640dac1..3d90d15 100644 --- a/src/components/quiz/AnswerOptions.tsx +++ b/src/components/quiz/AnswerOptions.tsx @@ -1,7 +1,7 @@ import { motion } from "framer-motion"; import { Button } from "@/components/ui/button"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; -import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; +import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"; interface AnswerOptionsProps { options: string[]; @@ -25,7 +25,7 @@ export function AnswerOptions({ return ( Date: Sun, 16 Feb 2025 16:34:46 +0100 Subject: [PATCH 6/9] refactor: optimize question loading and error handling in useQuiz hook - Simplify question loading logic with early return for existing questions - Streamline error message generation - Add small delay after successful question loading - Implement retry mechanism for network errors - Remove custom mode specific loading logic - Improve error state management and question progression --- src/hooks/useQuiz.ts | 78 +++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 44 deletions(-) diff --git a/src/hooks/useQuiz.ts b/src/hooks/useQuiz.ts index 3a5b568..09acecc 100644 --- a/src/hooks/useQuiz.ts +++ b/src/hooks/useQuiz.ts @@ -137,18 +137,8 @@ export const useQuiz = () => { }, [state, answeredQuestions, isInitialized]); const loadQuestion = useCallback(async () => { - // Add check for custom mode - if (state.isCustomMode && state.customQuestions.length > 0) { - // Use existing custom questions instead of loading new ones - const randomIndex = Math.floor( - Math.random() * state.customQuestions.length - ); - setState((prev) => ({ - ...prev, - questions: [state.customQuestions[randomIndex]], - currentQuestionIndex: 0, - error: null, - })); + // Add check to prevent loading if there's already a valid question + if (state.questions[state.currentQuestionIndex]) { return; } @@ -237,19 +227,15 @@ export const useQuiz = () => { }; console.error("Server error details:", errorDetails); - - // Try to extract a meaningful error message - let errorMessage = "Unknown server error"; - if (typeof data === "object" && data !== null) { - errorMessage = - data.error || - data.message || - `Error ${response.status}: ${response.statusText}`; - } else if (typeof data === "string") { - errorMessage = data; - } - - throw new Error(errorMessage); + throw new Error( + typeof data === "object" && data !== null + ? data.error || + data.message || + `Error ${response.status}: ${response.statusText}` + : typeof data === "string" + ? data + : "Unknown server error" + ); } // Validate the response data @@ -279,6 +265,9 @@ export const useQuiz = () => { difficulty: data.difficulty, }); + // After successfully loading the question, add a small delay + await new Promise((resolve) => setTimeout(resolve, 500)); + setState((prev) => ({ ...prev, questions: [data], @@ -286,19 +275,8 @@ export const useQuiz = () => { error: null, })); } catch (fetchError) { - console.error("Fetch error details:", { - error: fetchError, - message: - fetchError instanceof Error - ? fetchError.message - : String(fetchError), - }); - - throw new Error( - fetchError instanceof Error - ? fetchError.message - : "Failed to communicate with server" - ); + console.error("Fetch error details:", fetchError); + throw fetchError; // Let the outer catch handle this } } catch (error) { console.error("Error in loadQuestion:", error); @@ -307,13 +285,22 @@ export const useQuiz = () => { ); setState((prev) => ({ ...prev, - questions: [], + questions: [], // Clear questions on error currentQuestionIndex: 0, })); + // Add delay before retrying to prevent rapid requests on error + await new Promise((resolve) => setTimeout(resolve, 2000)); + // Only retry if there was a network error, not a validation error + if ( + error instanceof Error && + error.message.includes("Failed to communicate") + ) { + loadQuestion(); + } } finally { setLoading(false); } - }, [state.difficulty, state.isCustomMode, state.customQuestions]); + }, [state.difficulty, state.currentQuestionIndex, state.questions]); const handleAnswer = useCallback( (answer: string) => { @@ -392,9 +379,12 @@ export const useQuiz = () => { prev.questions && nextIndex < prev.questions.length; if (!hasNextQuestion) { - // If no next question, trigger a load with rate limiting - loadQuestion(); - return prev; + // Instead of immediately loading, clear the current question first + return { + ...prev, + questions: [], + currentQuestionIndex: 0, + }; } return { @@ -402,7 +392,7 @@ export const useQuiz = () => { currentQuestionIndex: nextIndex, }; }); - }, [loadQuestion]); + }, []); const toggleCustomMode = useCallback( (enabled: boolean) => { From 9a5e61446a5195c88e6f4186ff78700ff2b6c3a0 Mon Sep 17 00:00:00 2001 From: Vasiliki Doropoulou Date: Sun, 16 Feb 2025 16:54:29 +0100 Subject: [PATCH 7/9] refactor: improve AnswerOptions component with memoization and dynamic styling - Extract option rendering logic into a memoized function - Add dynamic button variant and className generation methods - Maintain existing functionality with cleaner implementation --- src/components/quiz/AnswerOptions.tsx | 84 +++++++++++++++------------ 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/src/components/quiz/AnswerOptions.tsx b/src/components/quiz/AnswerOptions.tsx index 3d90d15..7cfc8b8 100644 --- a/src/components/quiz/AnswerOptions.tsx +++ b/src/components/quiz/AnswerOptions.tsx @@ -2,6 +2,7 @@ import { motion } from "framer-motion"; import { Button } from "@/components/ui/button"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"; +import { useMemo } from "react"; interface AnswerOptionsProps { options: string[]; @@ -11,6 +12,13 @@ interface AnswerOptionsProps { onAnswerSelect: (answer: string) => void; } +const syntaxHighlighterStyle = { + background: "transparent", + padding: "0.5rem", + margin: 0, + fontSize: "0.875rem", +} as const; + export function AnswerOptions({ options, correctAnswer, @@ -18,26 +26,43 @@ export function AnswerOptions({ showExplanation, onAnswerSelect, }: AnswerOptionsProps) { - const renderOptionContent = (option: string) => { - // Check if the option is wrapped in backticks - const codeMatch = option.match(/^`(.+)`$/); - if (codeMatch) { - return ( - - {codeMatch[1]} - - ); - } - return option; + const renderOptionContent = useMemo(() => { + const render = (option: string) => { + const codeMatch = option.match(/^`(.+)`$/); + if (codeMatch) { + return ( + + {codeMatch[1]} + + ); + } + return option; + }; + render.displayName = "RenderOptionContent"; + return render; + }, []); + + const getButtonVariant = (option: string) => { + if (!showExplanation) return "outline"; + if (option === correctAnswer) return "default"; + if (option === selectedAnswer) return "destructive"; + return "outline"; + }; + + const getButtonClassName = (option: string) => { + const baseClasses = + "w-full text-left justify-start p-6 h-auto transition-colors"; + if (!showExplanation) + return `${baseClasses} hover:bg-muted bg-background text-foreground border`; + if (option === correctAnswer) + return `${baseClasses} bg-green-600 hover:bg-green-600 text-white font-medium`; + if (option === selectedAnswer) + return `${baseClasses} bg-red-600 text-white border-red-700`; + return `${baseClasses} hover:bg-muted bg-background text-foreground border`; }; return ( @@ -52,23 +77,8 @@ export function AnswerOptions({ +
+ ) +); +ExplanationSection.displayName = "ExplanationSection"; + +export const FeedbackSection = memo(function FeedbackSection({ selectedAnswer, currentQuestion, difficulty, @@ -25,22 +73,15 @@ export function FeedbackSection({ onNextQuestion, }: FeedbackSectionProps) { const isCorrect = selectedAnswer === currentQuestion?.correctAnswer; + const feedbackClasses = isCorrect + ? "bg-green-50 border border-green-200 text-green-800" + : "bg-red-50 border border-red-200 text-red-800"; return (
-
+
- {isCorrect ? ( - - ) : ( - - )} +

{getFeedbackMessage(isCorrect, difficulty)} @@ -61,41 +102,4 @@ export function FeedbackSection({ />

); -} - -function ExplanationSection({ - currentQuestion, - selectedAnswer, - onNextQuestion, -}: ExplanationSectionProps) { - return ( -
-
-

- Detailed Explanation: -

-

- {currentQuestion?.explanation} -

-
- - {selectedAnswer !== currentQuestion?.correctAnswer && ( -
-

Pro Tip:

-

- Remember to {currentQuestion?.topic.toLowerCase()} concepts. Try - practicing with similar examples to reinforce your understanding. -

-
- )} - - -
- ); -} +}); From fddbca40392c01d8d8b5500c4d2927a5dab7f74e Mon Sep 17 00:00:00 2001 From: Vasiliki Doropoulou Date: Sun, 16 Feb 2025 17:15:10 +0100 Subject: [PATCH 9/9] refactor: optimize QuestionCard with memoization and component extraction - Extract QuestionHeader and CodeDisplay as memoized components - Replace useEffect with useMemo for progress calculation - Improve performance and readability of QuestionCard rendering - Remove unnecessary console.log --- src/components/quiz/QuestionCard.tsx | 106 +++++++++++++++++---------- 1 file changed, 67 insertions(+), 39 deletions(-) diff --git a/src/components/quiz/QuestionCard.tsx b/src/components/quiz/QuestionCard.tsx index bf60fb0..01f7370 100644 --- a/src/components/quiz/QuestionCard.tsx +++ b/src/components/quiz/QuestionCard.tsx @@ -6,7 +6,7 @@ import { Trophy } from "lucide-react"; import { StatsDisplay } from "./StatsDisplay"; import { AnswerOptions } from "./AnswerOptions"; import { FeedbackSection } from "./FeedbackSection"; -import { useEffect } from "react"; +import { memo, useMemo } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"; @@ -24,7 +24,60 @@ interface QuestionCardProps { totalQuestions: number; } -export function QuestionCard({ +const syntaxHighlighterStyle = { + background: "rgba(0, 0, 0, 0.2)", + padding: "1rem", + borderRadius: "0.5rem", + fontSize: "0.875rem", +} as const; + +const QuestionHeader = memo( + ({ + questionNumber, + totalQuestions, + streak, + progress, + }: { + questionNumber: number; + totalQuestions: number; + streak: number; + progress: number; + }) => ( +
+
+
+
+ + Question {questionNumber} of {totalQuestions} + + {streak >= 3 && ( +
+ + Hot Streak! +
+ )} +
+
+ +
+
+ ) +); +QuestionHeader.displayName = "QuestionHeader"; + +const CodeDisplay = memo(({ code }: { code: string }) => ( + + {code} + +)); +CodeDisplay.displayName = "CodeDisplay"; + +export const QuestionCard = memo(function QuestionCard({ currentQuestion, score, streak, @@ -37,11 +90,10 @@ export function QuestionCard({ questionNumber, totalQuestions, }: QuestionCardProps) { - const progress = (questionNumber / totalQuestions) * 100; - - useEffect(() => { - console.log("Rendering CodeBlock with:", currentQuestion.code); - }, [currentQuestion.code]); + const progress = useMemo( + () => (questionNumber / totalQuestions) * 100, + [questionNumber, totalQuestions] + ); return ( -
-
-
- - Question {questionNumber} of {totalQuestions} - - {streak >= 3 && ( -
- - Hot Streak! -
- )} -
-
- -
+ - {currentQuestion?.code && ( - - {currentQuestion.code} - - )} + {currentQuestion?.code && }
); -} +});