diff --git a/app/(tabs)/home/page.tsx b/app/(tabs)/home/page.tsx index dd07ed6..a842136 100644 --- a/app/(tabs)/home/page.tsx +++ b/app/(tabs)/home/page.tsx @@ -10,6 +10,7 @@ import BackgroundWrapper from "@/components/BackgroundWrapper"; import DestructibleAlert from "@/components/DestructibleAlert"; import { ChevronRight } from "lucide-react"; // Using Lucide React for icons import styles from "@/css/Home.module.css"; +import facebookStyles from "@/css/SlidingGallery.module.css"; const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api"; @@ -30,6 +31,37 @@ const page = () => { { label: "Chemistry", progress: 57 }, ]; + const facebookViews = [ + { + id: "1", + content: ( + +
+
+

+ Meet, Share, and Learn! +

+

+ Join Facebook Community +

+
+
+ Facebook Logo +
+
+ + ), + }, + ]; + // Fetch function for leaderboard data useEffect(() => { let isMounted = true; @@ -70,7 +102,7 @@ const page = () => {
- +
{/* Categories Section */}
diff --git a/app/exam/results/page.tsx b/app/exam/results/page.tsx index 86312b3..0572042 100644 --- a/app/exam/results/page.tsx +++ b/app/exam/results/page.tsx @@ -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; + solution?: string; } interface QuestionItemProps { @@ -18,49 +22,78 @@ interface QuestionItemProps { selectedAnswer: string | undefined; } -const QuestionItem = React.memo( - ({ question, selectedAnswer }) => ( -
-

- {question.id}. {question.question} -

-
- {Object.entries(question.options).map(([key, value]) => ( - - ))} -
-
-

Solution:

-

{question.solution}

-
+const QuestionItem = ({ question, selectedAnswer }: QuestionItemProps) => ( +
+

+ {question.id}. {question.question} +

+ +
+
+ + {selectedAnswer?.answer === question.correctAnswer ? ( + + Correct + + ) : selectedAnswer?.answer !== question.correctAnswer ? ( + + Incorrect + + ) : ( + + Skipped + + )}
- ) + +
+ {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 ( +
+ {key.toUpperCase()} + {value} +
+ ); + })} +
+ +
+

Solution:

+

{question.solution}

+
+
); 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: ( +
+
+
+ Accuracy Rate: +
+
+ accuracy +

+ {( + (examResults.score / examResults.totalQuestions) * + 100 + ).toFixed(1)} + % +

+
+
+
+
+ ), + }, + { + id: 2, + content: ( +
+
+
+ Error Rate: +
+
+ accuracy +

+ {( + ((examResults.totalQuestions - examResults.score) / + examResults.totalQuestions) * + 100 + ).toFixed(1)} + % +

+
+
+
+
+ ), + }, + { + id: 3, + content: ( +
+
+
+ Attempt Rate: +
+
+ accuracy +

+ {( + (examResults.answers.length / examResults.totalQuestions) * + 100 + ).toFixed(1)} + % +

+
+
+
+
+ ), + }, + ]; + return (
-

+

Keep up the good work!

{/* Score Display */} -
-
-
Accuracy:
-
- {((examResults.score / examResults.totalQuestions) * 100).toFixed( - 1 - )} - % -
-
-
+ {apiResponse && (
@@ -131,10 +242,20 @@ export default function ResultsPage() { ))}
+
+
+
{" "} + Correct +
+
+
Your + Answer (Incorrect) +
+
)} diff --git a/bun.lockb b/bun.lockb index a0d1506..053d8be 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/Header.tsx b/components/Header.tsx index 3f506c3..2904164 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -5,6 +5,7 @@ import { ChevronLeft, Layers } from "lucide-react"; import { useTimer } from "@/context/TimerContext"; import styles from "@/css/Header.module.css"; import { useExam } from "@/context/ExamContext"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; const API_URL = "https://examjam-api.pptx704.com"; @@ -101,15 +102,11 @@ const Header = ({
{displayUser && (
- {image && ( - Profile - )} + + + {userData?.name ? userData.name.charAt(0).toUpperCase() : ""} + + Hello {userData?.name ? userData.name.split(" ")[0] : ""} diff --git a/components/SlidingGallery.tsx b/components/SlidingGallery.tsx index 105683d..bf6bd31 100644 --- a/components/SlidingGallery.tsx +++ b/components/SlidingGallery.tsx @@ -3,115 +3,158 @@ import Link from "next/link"; import Image from "next/image"; import styles from "../css/SlidingGallery.module.css"; -const views = [ - { - id: "1", - content: ( - -
-
-

Meet, Share, and Learn!

-

Join Facebook Community

-
-
- Facebook Logo -
-
- - ), - }, - { - id: "2", - content: ( - -
-
-

Meet, Share, and Learn!

-

Join Facebook Community

-
-
- Facebook Logo -
-
- - ), - }, - { - id: "3", - content: ( - -
-
-

Meet, Share, and Learn!

-

Join Facebook Community

-
-
- Facebook Logo -
-
- - ), - }, -]; - -const SlidingGallery = () => { +const SlidingGallery = ({ + views = [], + className = "", + showPagination = true, + autoScroll = false, + autoScrollInterval = 5000, + onSlideChange = () => {}, + height = "100vh", +}) => { const [activeIdx, setActiveIdx] = useState(0); + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); const scrollRef = useRef(null); + const galleryRef = useRef(null); + const autoScrollRef = useRef(null); - const handleScroll = (event) => { - const scrollLeft = event.target.scrollLeft; - const slideWidth = event.target.clientWidth; - const index = Math.round(scrollLeft / slideWidth); - setActiveIdx(index); + // Auto-scroll functionality + useEffect(() => { + if (autoScroll && views.length > 1) { + autoScrollRef.current = setInterval(() => { + setActiveIdx((prevIdx) => { + const nextIdx = (prevIdx + 1) % views.length; + goToSlide(nextIdx); + return nextIdx; + }); + }, autoScrollInterval); + + return () => { + if (autoScrollRef.current) { + clearInterval(autoScrollRef.current); + } + }; + } + }, [autoScroll, autoScrollInterval, views.length]); + + // Clear auto-scroll on user interaction + const handleUserInteraction = () => { + if (autoScrollRef.current) { + clearInterval(autoScrollRef.current); + } }; + useEffect(() => { + const updateDimensions = () => { + if (galleryRef.current) { + setDimensions({ + width: galleryRef.current.clientWidth, + height: galleryRef.current.clientHeight, + }); + } + }; + + // Initial dimension update + updateDimensions(); + + // Add resize listener + window.addEventListener("resize", updateDimensions); + + // Cleanup + return () => window.removeEventListener("resize", updateDimensions); + }, []); + + useEffect(() => { + // Recalculate active index when dimensions change + if (scrollRef.current && dimensions.width > 0) { + const scrollLeft = scrollRef.current.scrollLeft; + const slideWidth = dimensions.width; + const index = Math.round(scrollLeft / slideWidth); + setActiveIdx(index); + } + }, [dimensions]); + + const handleScroll = (event) => { + handleUserInteraction(); + const scrollLeft = event.target.scrollLeft; + const slideWidth = dimensions.width; + const index = Math.round(scrollLeft / slideWidth); + if (index !== activeIdx) { + setActiveIdx(index); + onSlideChange(index); + } + }; + + const goToSlide = (index) => { + if (scrollRef.current) { + scrollRef.current.scrollTo({ + left: index * dimensions.width, + behavior: "smooth", + }); + } + }; + + const handleDotClick = (index) => { + handleUserInteraction(); + goToSlide(index); + setActiveIdx(index); + onSlideChange(index); + }; + + // Early return if no views + if (!views || views.length === 0) { + return ( +
+
+

No content to display

+
+
+ ); + } + return ( -
+
{views.map((item) => ( -
+
{item.content}
))}
-
- {views.map((_, index) => ( -
- ))} -
+ {showPagination && views.length > 1 && ( +
+ {views.map((_, index) => ( +
handleDotClick(index)} + style={{ cursor: "pointer" }} + /> + ))} +
+ )}
); }; diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..71e428b --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/css/SlidingGallery.module.css b/css/SlidingGallery.module.css index 43f024f..d60a608 100644 --- a/css/SlidingGallery.module.css +++ b/css/SlidingGallery.module.css @@ -1,120 +1,167 @@ +/* SlidingGallery.module.css */ + .gallery { - height: 200px; - width: 100%; - border: 1px solid #113768; - border-radius: 25px; - position: relative; - overflow: hidden; - } - - .scrollContainer { - display: flex; - height: 100%; - overflow-x: auto; - scroll-behavior: smooth; - scroll-snap-type: x mandatory; - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* Internet Explorer 10+ */ - } - - .scrollContainer::-webkit-scrollbar { - display: none; /* WebKit */ - } - - .slide { - min-width: 100%; - flex-shrink: 0; - display: flex; - justify-content: center; - align-items: center; - scroll-snap-align: start; - box-sizing: border-box; - } - - - .link { - text-decoration: none; - color: inherit; - width: 100%; + position: relative; + width: 100%; + height: 100vh; /* Default height, can be overridden by props */ + overflow: hidden; + display: flex; + flex-direction: column; +} + +.emptyState { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + color: #666; + font-size: 1.2rem; +} + +.scrollContainer { + flex: 1; + display: flex; + overflow-x: auto; + scroll-snap-type: x mandatory; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE and Edge */ +} + +.scrollContainer::-webkit-scrollbar { + display: none; /* Chrome, Safari, Opera */ +} + +.slide { + min-width: 100%; + scroll-snap-align: start; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + box-sizing: border-box; +} + +.link { + width: 100%; + height: 100%; + display: block; + text-decoration: none; + color: inherit; +} + +.facebook { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 40px; + box-sizing: border-box; + background: linear-gradient(135deg, #1877f2 0%, #42a5f5 100%); + border-radius: 20px; + color: white; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; +} + +.facebook:hover { + transform: translateY(-5px); +} + +.textView { + flex: 1; + padding-right: 20px; +} + +.facebookOne { + font-size: clamp(1.5rem, 4vw, 2.5rem); + font-weight: 700; + margin: 0 0 16px 0; + line-height: 1.2; +} + +.facebookTwo { + font-size: clamp(1rem, 2.5vw, 1.25rem); + margin: 0; + opacity: 0.9; + line-height: 1.4; +} + +.logoView { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.logoView img { + width: clamp(120px, 15vw, 120px); + height: clamp(120px, 15vw, 120px); + object-fit: contain; +} + +.pagination { + position: absolute; + bottom: 15px; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 6px; + z-index: 10; +} + +.dot { + width: 8px; + height: 8px; + border-radius: 50%; + transition: all 0.3s ease; + cursor: pointer; +} + +.activeDot { + background-color: #113768; +} + +.inactiveDot { + background-color: #b1d3ff; +} + +.inactiveDot:hover { + background-color: rgba(255, 255, 255, 0.8); +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .gallery { + height: 70vh; /* Adjust for mobile */ } .facebook { - display: flex; - justify-content: space-evenly; - flex-direction: row; - height: 100%; - background-color: #fff; - border-radius: 25px; - padding: 40px 40px; - box-sizing: border-box; - } - - .facebookOne { - font-family: 'Montserrat', sans-serif; - font-weight: 900; - color: #113768; - font-size: 20px; - margin: 0; - } - - .facebookTwo { - font-family: 'Montserrat', sans-serif; - font-weight: 600; - color: #113768; - font-size: 13px; - margin: 0; - } - - .pagination { - display: flex; - flex-direction: row; - position: absolute; - bottom: 20px; - left: 50%; - transform: translateX(-50%); - } - - .dot { - width: 10px; - height: 10px; - border-radius: 5px; - margin: 0 5px; - } - - .activeDot { - background-color: #113768; - } - - .inactiveDot { - background-color: #ccc; + flex-direction: column; + text-align: center; + padding: 30px 20px; } .textView { - width: 60%; - display: flex; - flex-direction: column; - justify-content: space-between; + padding-right: 0; + margin-bottom: 20px; } - .logoView { - width: 40%; - display: flex; - justify-content: flex-end; - align-items: flex-end; + .slide { + padding: 15px; + } +} + +@media (max-width: 480px) { + .gallery { + height: 60vh; } - /* Responsive adjustments */ - @media (max-width: 768px) { - .slide { - min-width: calc(100% - 40px); - padding: 0 20px; - } - - .facebookOne { - font-size: 18px; - } - - .facebookTwo { - font-size: 12px; - } - } \ No newline at end of file + .facebook { + padding: 20px 15px; + } + + .slide { + padding: 10px; + } +} \ No newline at end of file diff --git a/package.json b/package.json index 4efd5db..caabb55 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-slot": "^1.2.3", "capacitor-secure-storage-plugin": "^0.11.0", "clsx": "^2.1.1", "lucide-react": "^0.523.0", diff --git a/public/images/icons/accuracy.png b/public/images/icons/accuracy.png new file mode 100644 index 0000000..4ac1956 Binary files /dev/null and b/public/images/icons/accuracy.png differ diff --git a/public/images/icons/attempt.png b/public/images/icons/attempt.png new file mode 100644 index 0000000..ad7906d Binary files /dev/null and b/public/images/icons/attempt.png differ diff --git a/public/images/icons/error.png b/public/images/icons/error.png new file mode 100644 index 0000000..b4af7ea Binary files /dev/null and b/public/images/icons/error.png differ