Give me the code
Here is the complete code for all files in the quiz application:
app/globals.css@import "tailwindcss";
@theme inline {
--font-sans: "Inter", "Inter Fallback";
--font-mono: "Fira Code", "Fira Code Fallback";
--color-background: oklch(0.13 0.02 260);
--color-foreground: oklch(0.98 0.01 260);
--color-card: oklch(0.18 0.02 260);
--color-card-foreground: oklch(0.98 0.01 260);
--color-primary: oklch(0.75 0.18 180);
--color-primary-foreground: oklch(0.13 0.02 260);
--color-secondary: oklch(0.25 0.02 260);
--color-secondary-foreground: oklch(0.90 0.01 260);
--color-muted: oklch(0.22 0.02 260);
--color-muted-foreground: oklch(0.65 0.02 260);
--color-accent: oklch(0.80 0.15 85);
--color-accent-foreground: oklch(0.13 0.02 260);
--color-destructive: oklch(0.65 0.20 25);
--color-destructive-foreground: oklch(0.98 0.01 260);
--color-success: oklch(0.70 0.18 150);
--color-success-foreground: oklch(0.13 0.02 260);
--color-border: oklch(0.28 0.02 260);
--color-ring: oklch(0.75 0.18 180);
--radius: 1rem;
}
* {
border-color: var(--color-border);
}
body {
background: var(--color-background);
color: var(--color-foreground);
font-family: var(--font-sans);
}app/layout.tsximport type { Metadata, Viewport } from "next";
import { Inter, Fira_Code } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
const firaCode = Fira_Code({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "QuizMaster - Test Your Knowledge",
description: "A fun and engaging quiz application to test your knowledge",
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 1,
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className="bg-background">
<body className={`${inter.className} antialiased`}>{children}</body>
</html>
);
}app/page.tsx"use client";
import { useState } from "react";
import { QuizStart } from "@/components/quiz-start";
import { QuizQuestion } from "@/components/quiz-question";
import { QuizResults } from "@/components/quiz-results";
export type Question = {
id: number;
category: string;
question: string;
options: string[];
correctAnswer: number;
};
export type Answer = {
questionId: number;
selectedAnswer: number;
isCorrect: boolean;
};
const questions: Question[] = [
{
id: 1,
category: "Science",
question: "What is the chemical symbol for gold?",
options: ["Go", "Au", "Ag", "Gd"],
correctAnswer: 1,
},
{
id: 2,
category: "Geography",
question: "What is the capital city of Japan?",
options: ["Seoul", "Beijing", "Tokyo", "Bangkok"],
correctAnswer: 2,
},
{
id: 3,
category: "History",
question: "In which year did World War II end?",
options: ["1943", "1944", "1945", "1946"],
correctAnswer: 2,
},
{
id: 4,
category: "Technology",
question: "Who is the co-founder of Apple Inc.?",
options: ["Bill Gates", "Steve Jobs", "Elon Musk", "Jeff Bezos"],
correctAnswer: 1,
},
{
id: 5,
category: "Nature",
question: "What is the largest mammal on Earth?",
options: ["African Elephant", "Blue Whale", "Giraffe", "Polar Bear"],
correctAnswer: 1,
},
];
export default function QuizApp() {
const [gameState, setGameState] = useState<"start" | "playing" | "results">("start");
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [answers, setAnswers] = useState<Answer[]>([]);
const [score, setScore] = useState(0);
const handleStartQuiz = () => {
setGameState("playing");
setCurrentQuestionIndex(0);
setAnswers([]);
setScore(0);
};
const handleAnswer = (selectedAnswer: number) => {
const currentQuestion = questions[currentQuestionIndex];
const isCorrect = selectedAnswer === currentQuestion.correctAnswer;
const newAnswer: Answer = {
questionId: currentQuestion.id,
selectedAnswer,
isCorrect,
};
setAnswers((prev) => [...prev, newAnswer]);
if (isCorrect) {
setScore((prev) => prev + 1);
}
setTimeout(() => {
if (currentQuestionIndex < questions.length - 1) {
setCurrentQuestionIndex((prev) => prev + 1);
} else {
setGameState("results");
}
}, 1500);
};
const handleTimeUp = () => {
const currentQuestion = questions[currentQuestionIndex];
const newAnswer: Answer = {
questionId: currentQuestion.id,
selectedAnswer: -1,
isCorrect: false,
};
setAnswers((prev) => [...prev, newAnswer]);
setTimeout(() => {
if (currentQuestionIndex < questions.length - 1) {
setCurrentQuestionIndex((prev) => prev + 1);
} else {
setGameState("results");
}
}, 1000);
};
const handleRestart = () => {
setGameState("start");
};
return (
<main className="min-h-screen flex flex-col bg-background">
{gameState === "start" && (
<QuizStart
totalQuestions={questions.length}
timePerQuestion={15}
onStart={handleStartQuiz}
/>
)}
{gameState === "playing" && (
<QuizQuestion
question={questions[currentQuestionIndex]}
questionNumber={currentQuestionIndex + 1}
totalQuestions={questions.length}
onAnswer={handleAnswer}
onTimeUp={handleTimeUp}
/>
)}
{gameState === "results" && (
<QuizResults
score={score}
totalQuestions={questions.length}
answers={answers}
questions={questions}
onRestart={handleRestart}
/>
)}
</main>
);
}components/quiz-start.tsx"use client";
import { Button } from "@/components/ui/button";
import { Brain, Clock, HelpCircle } from "lucide-react";
type QuizStartProps = {
totalQuestions: number;
timePerQuestion: number;
onStart: () => void;
};
export function QuizStart({ totalQuestions, timePerQuestion, onStart }: QuizStartProps) {
return (
<div className="flex-1 flex flex-col items-center justify-center px-6 py-12">
<div className="w-24 h-24 rounded-full bg-primary/20 flex items-center justify-center mb-8">
<Brain className="w-12 h-12 text-primary" />
</div>
<h1 className="text-3xl font-bold text-foreground text-center mb-3">
QuizMaster
</h1>
<p className="text-muted-foreground text-center mb-10 text-balance">
Test your knowledge across various topics and challenge yourself!
</p>
<div className="w-full max-w-xs space-y-4 mb-12">
<div className="flex items-center gap-4 p-4 rounded-xl bg-card border border-border">
<div className="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center">
<HelpCircle className="w-5 h-5 text-primary" />
</div>
<div>
<p className="text-sm text-muted-foreground">Questions</p>
<p className="text-lg font-semibold text-foreground">{totalQuestions}</p>
</div>
</div>
<div className="flex items-center gap-4 p-4 rounded-xl bg-card border border-border">
<div className="w-10 h-10 rounded-full bg-accent/20 flex items-center justify-center">
<Clock className="w-5 h-5 text-accent" />
</div>
<div>
<p className="text-sm text-muted-foreground">Time per question</p>
<p className="text-lg font-semibold text-foreground">{timePerQuestion} seconds</p>
</div>
</div>
</div>
<Button
onClick={onStart}
size="lg"
className="w-full max-w-xs h-14 text-lg font-semibold bg-primary text-primary-foreground hover:bg-primary/90 rounded-xl"
>
Start Quiz
</Button>
</div>
);
}components/quiz-question.tsx"use client";
import { useState, useEffect } from "react";
import { Question } from "@/app/page";
import { CircularTimer } from "@/components/circular-timer";
import { ProgressBar } from "@/components/progress-bar";
import { cn } from "@/lib/utils";
type QuizQuestionProps = {
question: Question;
questionNumber: number;
totalQuestions: number;
onAnswer: (selectedAnswer: number) => void;
onTimeUp: () => void;
};
export function QuizQuestion({
question,
questionNumber,
totalQuestions,
onAnswer,
onTimeUp,
}: QuizQuestionProps) {
const [selectedAnswer, setSelectedAnswer] = useState<number | null>(null);
const [timeLeft, setTimeLeft] = useState(15);
const [isAnswered, setIsAnswered] = useState(false);
useEffect(() => {
setSelectedAnswer(null);
setTimeLeft(15);
setIsAnswered(false);
}, [question.id]);
useEffect(() => {
if (isAnswered) return;
const timer = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
clearInterval(timer);
onTimeUp();
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}, [question.id, isAnswered, onTimeUp]);
const handleSelectAnswer = (index: number) => {
if (isAnswered) return;
setSelectedAnswer(index);
setIsAnswered(true);
onAnswer(index);
};
const getOptionStyles = (index: number) => {
if (!isAnswered) {
return "bg-card border-border hover:border-primary hover:bg-primary/5";
}
if (index === question.correctAnswer) {
return "bg-success/20 border-success";
}
if (index === selectedAnswer && index !== question.correctAnswer) {
return "bg-destructive/20 border-destructive";
}
return "bg-card border-border opacity-50";
};
return (
<div className="flex-1 flex flex-col px-6 py-8">
<div className="flex items-center justify-between mb-6">
<span className="text-sm font-medium text-muted-foreground">
Question {questionNumber}/{totalQuestions}
</span>
<span className="px-3 py-1 text-xs font-medium rounded-full bg-primary/20 text-primary">
{question.category}
</span>
</div>
<ProgressBar current={questionNumber} total={totalQuestions} />
<div className="flex justify-center my-8">
<CircularTimer timeLeft={timeLeft} totalTime={15} />
</div>
<h2 className="text-xl font-semibold text-foreground text-center mb-8 text-balance">
{question.question}
</h2>
<div className="space-y-3 mt-auto">
{question.options.map((option, index) => (
<button
key={index}
onClick={() => handleSelectAnswer(index)}
disabled={isAnswered}
className={cn(
"w-full p-4 rounded-xl border-2 text-left font-medium transition-all duration-200 min-h-[56px]",
getOptionStyles(index)
)}
>
<span className="flex items-center gap-3">
<span className="w-8 h-8 rounded-full bg-secondary flex items-center justify-center text-sm font-semibold text-secondary-foreground">
{String.fromCharCode(65 + index)}
</span>
<span className="text-foreground">{option}</span>
</span>
</button>
))}
</div>
</div>
);
}components/circular-timer.tsx"use client";
import { cn } from "@/lib/utils";
type CircularTimerProps = {
timeLeft: number;
totalTime: number;
};
export function CircularTimer({ timeLeft, totalTime }: CircularTimerProps) {
const percentage = (timeLeft / totalTime) * 100;
const circumference = 2 * Math.PI * 45;
const strokeDashoffset = circumference - (percentage / 100) * circumference;
const getTimerColor = () => {
if (timeLeft > 10) return "text-primary stroke-primary";
if (timeLeft > 5) return "text-accent stroke-accent";
return "text-destructive stroke-destructive";
};
return (
<div className="relative w-28 h-28">
<svg className="w-full h-full -rotate-90" viewBox="0 0 100 100">
<circle
cx="50"
cy="50"
r="45"
fill="none"
stroke="currentColor"
strokeWidth="8"
className="text-secondary"
/>
<circle
cx="50"
cy="50"
r="45"
fill="none"
strokeWidth="8"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
className={cn("transition-all duration-1000 ease-linear", getTimerColor())}
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className={cn("text-3xl font-bold", getTimerColor().split(" ")[0])}>
{timeLeft}
</span>
</div>
</div>
);
}components/progress-bar.tsx"use client";
import { cn } from "@/lib/utils";
type ProgressBarProps = {
current: number;
total: number;
};
export function ProgressBar({ current, total }: ProgressBarProps) {
return (
<div className="flex gap-2">
{Array.from({ length: total }).map((_, index) => (
<div
key={index}
className={cn(
"h-2 flex-1 rounded-full transition-all duration-300",
index < current ? "bg-primary" : "bg-secondary"
)}
/>
))}
</div>
);
}components/quiz-results.tsx"use client";
import { Button } from "@/components/ui/button";
import { Question, Answer } from "@/app/page";
import { CheckCircle2, XCircle, RotateCcw } from "lucide-react";
import { cn } from "@/lib/utils";
type QuizResultsProps = {
score: number;
totalQuestions: number;
answers: Answer[];
questions: Question[];
onRestart: () => void;
};
export function QuizResults({
score,
totalQuestions,
answers,
questions,
onRestart,
}: QuizResultsProps) {
const percentage = Math.round((score / totalQuestions) * 100);
const getScoreEmoji = () => {
if (percentage >= 80) return "🎉";
if (percentage >= 60) return "👍";
if (percentage >= 40) return "😊";
return "💪";
};
const getScoreMessage = () => {
if (percentage >= 80) return "Excellent!";
if (percentage >= 60) return "Good Job!";
if (percentage >= 40) return "Not Bad!";
return "Keep Practicing!";
};
return (
<div className="flex-1 flex flex-col px-6 py-8">
<div className="text-center mb-8">
<span className="text-6xl mb-4 block">{getScoreEmoji()}</span>
<h2 className="text-2xl font-bold text-foreground mb-2">
{getScoreMessage()}
</h2>
<p className="text-muted-foreground">
You scored {score} out of {totalQuestions}
</p>
</div>
<div className="w-32 h-32 rounded-full border-8 border-primary mx-auto flex items-center justify-center mb-8">
<span className="text-4xl font-bold text-primary">{percentage}%</span>
</div>
<div className="flex-1 overflow-auto">
<h3 className="text-lg font-semibold text-foreground mb-4">Summary</h3>
<div className="space-y-3">
{questions.map((question, index) => {
const answer = answers[index];
const isCorrect = answer?.isCorrect;
return (
<div
key={question.id}
className={cn(
"p-4 rounded-xl border-2",
isCorrect
? "bg-success/10 border-success/30"
: "bg-destructive/10 border-destructive/30"
)}
>
<div className="flex items-start gap-3">
{isCorrect ? (
<CheckCircle2 className="w-5 h-5 text-success mt-0.5 shrink-0" />
) : (
<XCircle className="w-5 h-5 text-destructive mt-0.5 shrink-0" />
)}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground line-clamp-2">
{question.question}
</p>
<p className="text-xs text-muted-foreground mt-1">
Correct: {question.options[question.correctAnswer]}
</p>
</div>
</div>
</div>
);
})}
</div>
</div>
<Button
onClick={onRestart}
size="lg"
className="w-full h-14 text-lg font-semibold bg-primary text-primary-foreground hover:bg-primary/90 rounded-xl mt-6"
>
<RotateCcw className="w-5 h-5 mr-2" />
Play Again
</Button>
</div>
);
}This is the complete source code for the quiz application. It features a dark theme with teal primary and amber accent colors, a circular countdown timer that changes color as time runs out, a segmented progress bar, and animated feedback for correct/incorrect answers. All touch targets are 44px+ for mobile optimization.
Great thank you can you make the same application in react native and can show me the code
I can provide you with the complete React Native code for the quiz application. Note that React Native apps cannot be previewed in v0 (which runs Next.js), but here is the full code you can use in your own React Native/Expo project:
app/
├── _layout.tsx # Root layout with fonts
├── index.tsx # Main quiz screen
components/
├── QuizStart.tsx # Start screen
├── QuizQuestion.tsx # Question with timer
├── CircularTimer.tsx # Animated circular timer
├── ProgressBar.tsx # Progress indicator
├── QuizResults.tsx # Results summary
constants/
└── Colors.ts # Theme colorsconstants/Colors.tsexport const Colors = {
background: '#1a1a2e',
foreground: '#f8f8fc',
card: '#252541',
cardForeground: '#f8f8fc',
primary: '#2dd4bf',
primaryForeground: '#1a1a2e',
secondary: '#3a3a5c',
secondaryForeground: '#e0e0e8',
muted: '#2f2f4a',
mutedForeground: '#9898a8',
accent: '#fbbf24',
accentForeground: '#1a1a2e',
destructive: '#ef4444',
destructiveForeground: '#f8f8fc',
success: '#22c55e',
successForeground: '#1a1a2e',
border: '#404060',
};app/_layout.tsximport { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { Colors } from '@/constants/Colors';
export default function RootLayout() {
return (
<>
<StatusBar style="light" />
<Stack
screenOptions={{
headerShown: false,
contentStyle: { backgroundColor: Colors.background },
}}
/>
</>
);
}app/index.tsximport { useState } from 'react';
import { View, StyleSheet } from 'react-native';
import { QuizStart } from '@/components/QuizStart';
import { QuizQuestion } from '@/components/QuizQuestion';
import { QuizResults } from '@/components/QuizResults';
import { Colors } from '@/constants/Colors';
export type Question = {
id: number;
category: string;
question: string;
options: string[];
correctAnswer: number;
};
export type Answer = {
questionId: number;
selectedAnswer: number;
isCorrect: boolean;
};
const questions: Question[] = [
{
id: 1,
category: 'Science',
question: 'What is the chemical symbol for gold?',
options: ['Go', 'Au', 'Ag', 'Gd'],
correctAnswer: 1,
},
{
id: 2,
category: 'Geography',
question: 'What is the capital city of Japan?',
options: ['Seoul', 'Beijing', 'Tokyo', 'Bangkok'],
correctAnswer: 2,
},
{
id: 3,
category: 'History',
question: 'In which year did World War II end?',
options: ['1943', '1944', '1945', '1946'],
correctAnswer: 2,
},
{
id: 4,
category: 'Technology',
question: 'Who is the co-founder of Apple Inc.?',
options: ['Bill Gates', 'Steve Jobs', 'Elon Musk', 'Jeff Bezos'],
correctAnswer: 1,
},
{
id: 5,
category: 'Nature',
question: 'What is the largest mammal on Earth?',
options: ['African Elephant', 'Blue Whale', 'Giraffe', 'Polar Bear'],
correctAnswer: 1,
},
];
export default function QuizApp() {
const [gameState, setGameState] = useState<'start' | 'playing' | 'results'>('start');
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [answers, setAnswers] = useState<Answer[]>([]);
const [score, setScore] = useState(0);
const handleStartQuiz = () => {
setGameState('playing');
setCurrentQuestionIndex(0);
setAnswers([]);
setScore(0);
};
const handleAnswer = (selectedAnswer: number) => {
const currentQuestion = questions[currentQuestionIndex];
const isCorrect = selectedAnswer === currentQuestion.correctAnswer;
const newAnswer: Answer = {
questionId: currentQuestion.id,
selectedAnswer,
isCorrect,
};
setAnswers((prev) => [...prev, newAnswer]);
if (isCorrect) {
setScore((prev) => prev + 1);
}
setTimeout(() => {
if (currentQuestionIndex < questions.length - 1) {
setCurrentQuestionIndex((prev) => prev + 1);
} else {
setGameState('results');
}
}, 1500);
};
const handleTimeUp = () => {
const currentQuestion = questions[currentQuestionIndex];
const newAnswer: Answer = {
questionId: currentQuestion.id,
selectedAnswer: -1,
isCorrect: false,
};
setAnswers((prev) => [...prev, newAnswer]);
setTimeout(() => {
if (currentQuestionIndex < questions.length - 1) {
setCurrentQuestionIndex((prev) => prev + 1);
} else {
setGameState('results');
}
}, 1000);
};
const handleRestart = () => {
setGameState('start');
};
return (
<View style={styles.container}>
{gameState === 'start' && (
<QuizStart
totalQuestions={questions.length}
timePerQuestion={15}
onStart={handleStartQuiz}
/>
)}
{gameState === 'playing' && (
<QuizQuestion
question={questions[currentQuestionIndex]}
questionNumber={currentQuestionIndex + 1}
totalQuestions={questions.length}
onAnswer={handleAnswer}
onTimeUp={handleTimeUp}
/>
)}
{gameState === 'results' && (
<QuizResults
score={score}
totalQuestions={questions.length}
answers={answers}
questions={questions}
onRestart={handleRestart}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: Colors.background,
},
});components/QuizStart.tsximport { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { Colors } from '@/constants/Colors';
type QuizStartProps = {
totalQuestions: number;
timePerQuestion: number;
onStart: () => void;
};
export function QuizStart({ totalQuestions, timePerQuestion, onStart }: QuizStartProps) {
return (
<View style={styles.container}>
<View style={styles.iconContainer}>
<Ionicons name="bulb" size={48} color={Colors.primary} />
</View>
<Text style={styles.title}>QuizMaster</Text>
<Text style={styles.subtitle}>
Test your knowledge across various topics and challenge yourself!
</Text>
<View style={styles.infoContainer}>
<View style={styles.infoCard}>
<View style={[styles.infoIcon, { backgroundColor: Colors.primary + '30' }]}>
<Ionicons name="help-circle" size={24} color={Colors.primary} />
</View>
<View>
<Text style={styles.infoLabel}>Questions</Text>
<Text style={styles.infoValue}>{totalQuestions}</Text>
</View>
</View>
<View style={styles.infoCard}>
<View style={[styles.infoIcon, { backgroundColor: Colors.accent + '30' }]}>
<Ionicons name="time" size={24} color={Colors.accent} />
</View>
<View>
<Text style={styles.infoLabel}>Time per question</Text>
<Text style={styles.infoValue}>{timePerQuestion} seconds</Text>
</View>
</View>
</View>
<TouchableOpacity style={styles.startButton} onPress={onStart} activeOpacity={0.8}>
<Text style={styles.startButtonText}>Start Quiz</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 24,
paddingVertical: 48,
},
iconContainer: {
width: 96,
height: 96,
borderRadius: 48,
backgroundColor: Colors.primary + '20',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 32,
},
title: {
fontSize: 32,
fontWeight: 'bold',
color: Colors.foreground,
textAlign: 'center',
marginBottom: 12,
},
subtitle: {
fontSize: 16,
color: Colors.mutedForeground,
textAlign: 'center',
marginBottom: 40,
lineHeight: 24,
},
infoContainer: {
width: '100%',
maxWidth: 320,
gap: 16,
marginBottom: 48,
},
infoCard: {
flexDirection: 'row',
alignItems: 'center',
gap: 16,
padding: 16,
borderRadius: 16,
backgroundColor: Colors.card,
borderWidth: 1,
borderColor: Colors.border,
},
infoIcon: {
width: 40,
height: 40,
borderRadius: 20,
alignItems: 'center',
justifyContent: 'center',
},
infoLabel: {
fontSize: 14,
color: Colors.mutedForeground,
},
infoValue: {
fontSize: 18,
fontWeight: '600',
color: Colors.foreground,
},
startButton: {
width: '100%',
maxWidth: 320,
height: 56,
backgroundColor: Colors.primary,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
startButtonText: {
fontSize: 18,
fontWeight: '600',
color: Colors.primaryForeground,
},
});components/QuizQuestion.tsximport { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { Question } from '@/app/index';
import { CircularTimer } from './CircularTimer';
import { ProgressBar } from './ProgressBar';
import { Colors } from '@/constants/Colors';
type QuizQuestionProps = {
question: Question;
questionNumber: number;
totalQuestions: number;
onAnswer: (selectedAnswer: number) => void;
onTimeUp: () => void;
};
export function QuizQuestion({
question,
questionNumber,
totalQuestions,
onAnswer,
onTimeUp,
}: QuizQuestionProps) {
const [selectedAnswer, setSelectedAnswer] = useState<number | null>(null);
const [timeLeft, setTimeLeft] = useState(15);
const [isAnswered, setIsAnswered] = useState(false);
useEffect(() => {
setSelectedAnswer(null);
setTimeLeft(15);
setIsAnswered(false);
}, [question.id]);
useEffect(() => {
if (isAnswered) return;
const timer = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
clearInterval(timer);
onTimeUp();
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}, [question.id, isAnswered, onTimeUp]);
const handleSelectAnswer = (index: number) => {
if (isAnswered) return;
setSelectedAnswer(index);
setIsAnswered(true);
onAnswer(index);
};
const getOptionStyles = (index: number) => {
if (!isAnswered) {
return {
backgroundColor: Colors.card,
borderColor: Colors.border,
};
}
if (index === question.correctAnswer) {
return {
backgroundColor: Colors.success + '30',
borderColor: Colors.success,
};
}
if (index === selectedAnswer && index !== question.correctAnswer) {
return {
backgroundColor: Colors.destructive + '30',
borderColor: Colors.destructive,
};
}
return {
backgroundColor: Colors.card,
borderColor: Colors.border,
opacity: 0.5,
};
};
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.questionCount}>
Question {questionNumber}/{totalQuestions}
</Text>
<View style={styles.categoryBadge}>
<Text style={styles.categoryText}>{question.category}</Text>
</View>
</View>
<ProgressBar current={questionNumber} total={totalQuestions} />
<View style={styles.timerContainer}>
<CircularTimer timeLeft={timeLeft} totalTime={15} />
</View>
<Text style={styles.questionText}>{question.question}</Text>
<View style={styles.optionsContainer}>
{question.options.map((option, index) => (
<TouchableOpacity
key={index}
onPress={() => handleSelectAnswer(index)}
disabled={isAnswered}
activeOpacity={0.7}
style={[styles.optionButton, getOptionStyles(index)]}
>
<View style={styles.optionContent}>
<View style={styles.optionLetter}>
<Text style={styles.optionLetterText}>
{String.fromCharCode(65 + index)}
</Text>
</View>
<Text style={styles.optionText}>{option}</Text>
</View>
</TouchableOpacity>
))}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 24,
paddingVertical: 32,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 24,
},
questionCount: {
fontSize: 14,
fontWeight: '500',
color: Colors.mutedForeground,
},
categoryBadge: {
paddingHorizontal: 12,
paddingVertical: 4,
borderRadius: 12,
backgroundColor: Colors.primary + '30',
},
categoryText: {
fontSize: 12,
fontWeight: '500',
color: Colors.primary,
},
timerContainer: {
alignItems: 'center',
marginVertical: 32,
},
questionText: {
fontSize: 20,
fontWeight: '600',
color: Colors.foreground,
textAlign: 'center',
marginBottom: 32,
lineHeight: 28,
},
optionsContainer: {
flex: 1,
justifyContent: 'flex-end',
gap: 12,
},
optionButton: {
padding: 16,
borderRadius: 16,
borderWidth: 2,
minHeight: 56,
},
optionContent: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
optionLetter: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: Colors.secondary,
alignItems: 'center',
justifyContent: 'center',
},
optionLetterText: {
fontSize: 14,
fontWeight: '600',
color: Colors.secondaryForeground,
},
optionText: {
flex: 1,
fontSize: 16,
fontWeight: '500',
color: Colors.foreground,
},
});components/CircularTimer.tsximport { View, Text, StyleSheet } from 'react-native';
import Svg, { Circle } from 'react-native-svg';
import { Colors } from '@/constants/Colors';
type CircularTimerProps = {
timeLeft: number;
totalTime: number;
};
export function CircularTimer({ timeLeft, totalTime }: CircularTimerProps) {
const size = 112;
const strokeWidth = 8;
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const percentage = (timeLeft / totalTime) * 100;
const strokeDashoffset = circumference - (percentage / 100) * circumference;
const getTimerColor = () => {
if (timeLeft > 10) return Colors.primary;
if (timeLeft > 5) return Colors.accent;
return Colors.destructive;
};
const timerColor = getTimerColor();
return (
<View style={styles.container}>
<Svg width={size} height={size} style={styles.svg}>
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={Colors.secondary}
strokeWidth={strokeWidth}
/>
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={timerColor}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
rotation="-90"
origin={`${size / 2}, ${size / 2}`}
/>
</Svg>
<View style={styles.textContainer}>
<Text style={[styles.timeText, { color: timerColor }]}>{timeLeft}</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
width: 112,
height: 112,
position: 'relative',
},
svg: {
transform: [{ rotateZ: '0deg' }],
},
textContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center',
},
timeText: {
fontSize: 36,
fontWeight: 'bold',
},
});components/ProgressBar.tsximport { View, StyleSheet } from 'react-native';
import { Colors } from '@/constants/Colors';
type ProgressBarProps = {
current: number;
total: number;
};
export function ProgressBar({ current, total }: ProgressBarProps) {
return (
<View style={styles.container}>
{Array.from({ length: total }).map((_, index) => (
<View
key={index}
style={[
styles.segment,
{
backgroundColor: index < current ? Colors.primary : Colors.secondary,
},
]}
/>
))}
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
gap: 8,
},
segment: {
flex: 1,
height: 8,
borderRadius: 4,
},
});components/QuizResults.tsximport { View, Text, TouchableOpacity, ScrollView, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { Question, Answer } from '@/app/index';
import { Colors } from '@/constants/Colors';
type QuizResultsProps = {
score: number;
totalQuestions: number;
answers: Answer[];
questions: Question[];
onRestart: () => void;
};
export function QuizResults({
score,
totalQuestions,
answers,
questions,
onRestart,
}: QuizResultsProps) {
const percentage = Math.round((score / totalQuestions) * 100);
const getScoreEmoji = () => {
if (percentage >= 80) return '🎉';
if (percentage >= 60) return '👍';
if (percentage >= 40) return '😊';
return '💪';
};
const getScoreMessage = () => {
if (percentage >= 80) return 'Excellent!';
if (percentage >= 60) return 'Good Job!';
if (percentage >= 40) return 'Not Bad!';
return 'Keep Practicing!';
};
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.emoji}>{getScoreEmoji()}</Text>
<Text style={styles.message}>{getScoreMessage()}</Text>
<Text style={styles.scoreText}>
You scored {score} out of {totalQuestions}
</Text>
</View>
<View style={styles.percentageCircle}>
<Text style={styles.percentageText}>{percentage}%</Text>
</View>
<ScrollView style={styles.summaryContainer} showsVerticalScrollIndicator={false}>
<Text style={styles.summaryTitle}>Summary</Text>
{questions.map((question, index) => {
const answer = answers[index];
const isCorrect = answer?.isCorrect;
return (
<View
key={question.id}
style={[
styles.summaryCard,
{
backgroundColor: isCorrect ? Colors.success + '15' : Colors.destructive + '15',
borderColor: isCorrect ? Colors.success + '40' : Colors.destructive + '40',
},
]}
>
<Ionicons
name={isCorrect ? 'checkmark-circle' : 'close-circle'}
size={20}
color={isCorrect ? Colors.success : Colors.destructive}
style={styles.summaryIcon}
/>
<View style={styles.summaryContent}>
<Text style={styles.summaryQuestion} numberOfLines={2}>
{question.question}
</Text>
<Text style={styles.summaryAnswer}>
Correct: {question.options[question.correctAnswer]}
</Text>
</View>
</View>
);
})}
</ScrollView>
<TouchableOpacity style={styles.restartButton} onPress={onRestart} activeOpacity={0.8}>
<Ionicons name="refresh" size={20} color={Colors.primaryForeground} />
<Text style={styles.restartButtonText}>Play Again</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 24,
paddingVertical: 32,
},
header: {
alignItems: 'center',
marginBottom: 24,
},
emoji: {
fontSize: 56,
marginBottom: 16,
},
message: {
fontSize: 24,
fontWeight: 'bold',
color: Colors.foreground,
marginBottom: 8,
},
scoreText: {
fontSize: 16,
color: Colors.mutedForeground,
},
percentageCircle: {
width: 128,
height: 128,
borderRadius: 64,
borderWidth: 8,
borderColor: Colors.primary,
alignItems: 'center',
justifyContent: 'center',
alignSelf: 'center',
marginBottom: 32,
},
percentageText: {
fontSize: 36,
fontWeight: 'bold',
color: Colors.primary,
},
summaryContainer: {
flex: 1,
},
summaryTitle: {
fontSize: 18,
fontWeight: '600',
color: Colors.foreground,
marginBottom: 16,
},
summaryCard: {
flexDirection: 'row',
padding: 16,
borderRadius: 16,
borderWidth: 2,
marginBottom: 12,
},
summaryIcon: {
marginRight: 12,
marginTop: 2,
},
summaryContent: {
flex: 1,
},
summaryQuestion: {
fontSize: 14,
fontWeight: '500',
color: Colors.foreground,
marginBottom: 4,
},
summaryAnswer: {
fontSize: 12,
color: Colors.mutedForeground,
},
restartButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
height: 56,
backgroundColor: Colors.primary,
borderRadius: 16,
marginTop: 24,
},
restartButtonText: {
fontSize: 18,
fontWeight: '600',
color: Colors.primaryForeground,
},
});To use this code in your React Native project:
npx create-expo-app quiz-appnpx expo install react-native-svgnpx expo startThe React Native version mirrors the Next.js app with the same dark theme, circular timer with color transitions, segmented progress bar, and answer feedback. It uses react-native-svg for the circular timer and @expo/vector-icons for icons.
The code returns the following error:
Cannot update a component (`QuizApp`) while rendering a different component (`QuizQuestion`). To locate the bad setState() call inside `QuizQuestion`, follow the stack trace as described in https://react.dev/link/setstate-in-render
Revise the code to address the error.