feat(ui): add avatar and badge components

This commit is contained in:
shafin-r
2025-07-10 14:51:45 +06:00
parent d42a42a8d1
commit 64fc4d9a9a
12 changed files with 597 additions and 256 deletions

View File

@ -2,15 +2,19 @@
import { useRouter } from "next/navigation";
import { useExam, useExamResults } from "@/context/ExamContext";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import React from "react";
import { ArrowLeft } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import Image from "next/image";
import SlidingGallery from "@/components/SlidingGallery";
interface Question {
solution: string;
correctAnswer: string;
id: number;
question: string;
options: Record<string, string>;
solution?: string;
}
interface QuestionItemProps {
@ -18,49 +22,78 @@ interface QuestionItemProps {
selectedAnswer: string | undefined;
}
const QuestionItem = React.memo<QuestionItemProps>(
({ question, selectedAnswer }) => (
<div className="border border-[#8abdff]/50 rounded-2xl p-4 flex flex-col gap-7">
<h3 className="text-xl font-medium">
{question.id}. {question.question}
</h3>
<div className="flex flex-col gap-4 items-start">
{Object.entries(question.options).map(([key, value]) => (
<button key={key} className="flex items-center gap-3">
<span
className={`flex items-center rounded-full border px-1.5 ${
selectedAnswer === key
? "text-white bg-[#113768] border-[#113768]"
: ""
}`}
>
{key.toUpperCase()}
</span>
<span className="option-description">{value}</span>
</button>
))}
</div>
<div className="flex flex-col gap-4">
<h3 className="text-xl font-bold text-black/40">Solution:</h3>
<p className="text-lg font-medium">{question.solution}</p>
</div>
const QuestionItem = ({ question, selectedAnswer }: QuestionItemProps) => (
<div className="border border-[#8abdff]/50 rounded-2xl p-4 flex flex-col gap-7">
<h3 className="text-xl font-medium">
{question.id}. {question.question}
</h3>
<div className="flex justify-between items-center">
<div></div>
{selectedAnswer?.answer === question.correctAnswer ? (
<Badge className="bg-green-500 text-white" variant="default">
Correct
</Badge>
) : selectedAnswer?.answer !== question.correctAnswer ? (
<Badge className="bg-red-500 text-white" variant="default">
Incorrect
</Badge>
) : (
<Badge className="bg-yellow-500" variant="destructive">
Skipped
</Badge>
)}
</div>
)
<div className="flex flex-col gap-4 items-start">
{Object.entries(question.options).map(([key, value]) => {
const isCorrect = key === question.correctAnswer;
const isSelected = key === selectedAnswer?.answer;
let optionStyle =
"px-2 py-1 flex items-center rounded-full border font-medium text-sm";
if (isCorrect) {
optionStyle += " bg-green-600 text-white border-green-600";
}
if (isSelected && !isCorrect) {
optionStyle += " bg-red-600 text-white border-red-600";
}
if (!isCorrect && !isSelected) {
optionStyle += " border-gray-300 text-gray-700";
}
return (
<div key={key} className="flex items-center gap-3">
<span className={optionStyle}>{key.toUpperCase()}</span>
<span className="option-description">{value}</span>
</div>
);
})}
</div>
<div className="flex flex-col gap-2">
<h3 className="text-lg font-bold text-black/40">Solution:</h3>
<p className="text-lg">{question.solution}</p>
</div>
</div>
);
export default function ResultsPage() {
const router = useRouter();
const { clearExam, isExamCompleted, getApiResponse } = useExam();
let examResults;
useEffect(() => {
// Redirect if no completed exam
if (!isExamCompleted()) {
router.push("/unit");
return;
}
}, [isExamCompleted, router]);
let examResults;
try {
examResults = useExamResults();
} catch (error) {
@ -98,28 +131,106 @@ export default function ResultsPage() {
)
: 0;
const resultViews = [
{
id: 1,
content: (
<div className="w-full">
<div className="bg-blue-50/60 border border-[#113678]/50 rounded-4xl h-[170px] flex flex-col items-center justify-center gap-4">
<div className="text-xl text-black ">
<span className="font-bold">Accuracy</span> Rate:
</div>
<div className="flex gap-4">
<Image
src="/images/icons/accuracy.png"
alt="accuracy"
width={60}
height={60}
/>
<h2 className="text-6xl font-bold text-[#113678]">
{(
(examResults.score / examResults.totalQuestions) *
100
).toFixed(1)}
%
</h2>
</div>
<div></div>
</div>
</div>
),
},
{
id: 2,
content: (
<div className=" w-full">
<div className="bg-blue-50/60 border border-[#113678]/50 rounded-4xl h-[170px] flex flex-col items-center justify-center gap-3">
<div className="text-xl text-black ">
<span className="font-bold">Error</span> Rate:
</div>
<div className="flex gap-4">
<Image
src="/images/icons/error.png"
alt="accuracy"
width={60}
height={60}
/>
<h2 className="text-6xl font-bold text-[#113678]">
{(
((examResults.totalQuestions - examResults.score) /
examResults.totalQuestions) *
100
).toFixed(1)}
%
</h2>
</div>
<div></div>
</div>
</div>
),
},
{
id: 3,
content: (
<div className="my-8 w-full">
<div className="bg-blue-50/60 border border-[#113678]/50 rounded-4xl h-[170px] flex flex-col items-center justify-center gap-4">
<div className="text-xl text-black">
<span className="font-bold">Attempt</span> Rate:
</div>
<div className="flex gap-4">
<Image
src="/images/icons/attempt.png"
alt="accuracy"
width={60}
height={60}
/>
<h2 className="text-6xl font-bold text-[#113678]">
{(
(examResults.answers.length / examResults.totalQuestions) *
100
).toFixed(1)}
%
</h2>
</div>
<div></div>
</div>
</div>
),
},
];
return (
<div className="min-h-screen bg-white">
<button className="p-10" onClick={() => router.push("/unit")}>
<ArrowLeft size={30} color="black" />
</button>
<div className="bg-white rounded-lg shadow-lg px-10 pb-20">
<h1 className="text-2xl font-bold text-gray-900 mb-2 text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4 text-center">
Keep up the good work!
</h1>
{/* Score Display */}
<div className="mb-8">
<div className="bg-blue-50/60 border border-[#113678]/50 rounded-4xl h-[150px] flex flex-col items-center justify-center">
<div className="text-xl text-black mb-2">Accuracy:</div>
<div className="text-5xl font-bold text-[#113678]">
{((examResults.score / examResults.totalQuestions) * 100).toFixed(
1
)}
%
</div>
</div>
</div>
<SlidingGallery className="my-8" views={resultViews} height="170px" />
{apiResponse && (
<div className="mb-8">
@ -131,10 +242,20 @@ export default function ResultsPage() {
<QuestionItem
key={question.id}
question={question}
selectedAnswer={undefined}
selectedAnswer={examResults.answers?.[question.id]}
/>
))}
</div>
<div className="flex gap-4 items-center mb-6 text-sm text-gray-600">
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-green-600 rounded-full"></div>{" "}
Correct
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-red-600 rounded-full"></div> Your
Answer (Incorrect)
</div>
</div>
</div>
)}