diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 17117c2..afe3562 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -9,28 +9,39 @@ import FormField from "@/components/FormField"; import { login } from "@/lib/auth"; import DestructibleAlert from "@/components/DestructibleAlert"; import { useAuth } from "@/context/AuthContext"; +import { LoginForm } from "@/types/auth"; -const page = () => { +const LoginPage = () => { const router = useRouter(); const { setToken } = useAuth(); - const [form, setForm] = useState({ + + const [form, setForm] = useState({ email: "", password: "", }); - const [error, setError] = useState(null); + + const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); - // For Rafeed - // Function to login a user. I've kept it in a barebones form right now, but you can just call the login function from /lib/auth.ts and pass on the form. const loginUser = async () => { try { setIsLoading(true); setError(null); - await login(form, setToken); // Call the login function - router.push("/home"); // Redirect on successful login - } catch (error) { - console.log(error); - setError(error.message); // Handle error messages + await login(form, setToken); + router.push("/home"); + } catch (err: unknown) { + console.error(err); + + if ( + typeof err === "object" && + err !== null && + "message" in err && + typeof err.message === "string" + ) { + setError(err.message); + } else { + setError("An unexpected error occurred."); + } } finally { setIsLoading(false); } @@ -63,13 +74,17 @@ const page = () => { title="Email Address" value={form.email} placeholder="Enter your email address..." - handleChangeText={(e) => setForm({ ...form, email: e })} + handleChangeText={(value) => + setForm({ ...form, email: value }) + } /> setForm({ ...form, password: e })} + handleChangeText={(value) => + setForm({ ...form, password: value }) + } /> @@ -94,7 +109,7 @@ const page = () => { className="text-center mb-[70px]" style={{ fontFamily: "Montserrat, sans-serif" }} > - Don't have an account?{" "} + Don't have an account?{" "} Register here. @@ -106,4 +121,4 @@ const page = () => { ); }; -export default page; +export default LoginPage; diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx index 90d6636..7a44260 100644 --- a/app/(auth)/register/page.tsx +++ b/app/(auth)/register/page.tsx @@ -10,11 +10,18 @@ import { useAuth } from "@/context/AuthContext"; import BackgroundWrapper from "@/components/BackgroundWrapper"; import FormField from "@/components/FormField"; import DestructibleAlert from "@/components/DestructibleAlert"; +import { RegisterForm } from "@/types/auth"; + +interface CustomError extends Error { + response?: { + detail?: string; + }; +} export default function RegisterPage() { const { setToken } = useAuth(); const router = useRouter(); - const [form, setForm] = useState({ + const [form, setForm] = useState({ name: "", institution: "", sscRoll: "", @@ -25,13 +32,12 @@ export default function RegisterPage() { }); const [error, setError] = useState(null); - const handleError = (error: any) => { + const handleError = (error: { detail: string }) => { if (error?.detail) { const match = error.detail.match(/Key \((.*?)\)=\((.*?)\)/); if (match) { const field = match[1]; - const value = match[2]; - return `The ${field} already exists. Please use a different value.`; + return `The ${field} already exists. Please try again.`; } } return "An unexpected error occurred. Please try again."; @@ -56,16 +62,30 @@ export default function RegisterPage() { setError(validationError); return; } + try { await register(form, setToken); router.push("/home"); - } catch (error: any) { - console.error("Error:", error.response || error.message); - if (error.response?.detail) { - const decodedError = handleError({ detail: error.response.detail }); - setError(decodedError); + } catch (error) { + // Type guard for built-in Error type + if (error instanceof Error) { + console.error( + "Error:", + (error as CustomError).response || error.message + ); + + const response = (error as CustomError).response; + + if (response?.detail) { + const decodedError = handleError({ detail: response.detail }); + setError(decodedError); + } else { + setError(error.message || "An unexpected error occurred."); + } } else { - setError(error.message || "An unexpected error occurred."); + // Fallback for non-standard errors + console.error("Unexpected error:", error); + setError("An unexpected error occurred."); } } }; @@ -148,7 +168,7 @@ export default function RegisterPage() {

- Already have an account?{" "} + Already have an account? Login here diff --git a/app/(tabs)/bookmark/page.tsx b/app/(tabs)/bookmark/page.tsx index d73fc40..64d64ef 100644 --- a/app/(tabs)/bookmark/page.tsx +++ b/app/(tabs)/bookmark/page.tsx @@ -50,7 +50,7 @@ const QuestionItem = ({ question }: QuestionItemProps) => { const BookmarkPage = () => { const router = useRouter(); - const [questions, setQuestions] = useState(); + const [questions, setQuestions] = useState([]); useEffect(() => { fetch("/data/bookmark.json") @@ -74,7 +74,7 @@ const BookmarkPage = () => { - {questions?.map((question) => ( + {questions.map((question: Question) => ( ))} diff --git a/app/(tabs)/categories/page.tsx b/app/(tabs)/categories/page.tsx deleted file mode 100644 index a54c709..0000000 --- a/app/(tabs)/categories/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -const page = () => { - return

page
; -}; - -export default page; diff --git a/app/(tabs)/home/page.tsx b/app/(tabs)/home/page.tsx index 61005a5..28f31a9 100644 --- a/app/(tabs)/home/page.tsx +++ b/app/(tabs)/home/page.tsx @@ -1,57 +1,56 @@ "use client"; -import React, { useState, useEffect, ReactNode } from "react"; +import React, { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import Image from "next/image"; import Header from "@/components/Header"; import SlidingGallery from "@/components/SlidingGallery"; import BackgroundWrapper from "@/components/BackgroundWrapper"; -import DestructibleAlert from "@/components/DestructibleAlert"; import { ChevronRight } from "lucide-react"; import styles from "@/css/Home.module.css"; import { API_URL } from "@/lib/auth"; import { Avatar } from "@/components/ui/avatar"; import { getLinkedViews } from "@/lib/gallery-views"; import { getTopThree } from "@/lib/leaderboard"; +import DestructibleAlert from "@/components/DestructibleAlert"; +import { GalleryViews } from "@/types/gallery"; -interface LinkedView { +interface LeaderboardEntry { id: string; - content: ReactNode; + name: string; + points: number; } -const page = () => { +const HomePage = () => { const router = useRouter(); - const [boardData, setBoardData] = useState([]); - const [boardError, setBoardError] = useState(null); - const [linkedViews, setLinkedViews] = useState(); - - const performanceData = [ - { label: "Mock Test", progress: 20 }, - { label: "Topic Test", progress: 70 }, - { label: "Subject Test", progress: 50 }, - ]; - - const progressData = [ - { label: "Physics", progress: 25 }, - { label: "Chemistry", progress: 57 }, - ]; + const [boardData, setBoardData] = useState([]); + const [boardError, setBoardError] = useState(null); + const [linkedViews, setLinkedViews] = useState(); useEffect(() => { let isMounted = true; - async function fetchBoardData() { + + const fetchBoardData = async () => { try { const response = await fetch(`${API_URL}/leaderboard`); if (!response.ok) { throw new Error("Failed to fetch leaderboard data"); } - const data = await response.json(); + + const data: LeaderboardEntry[] = await response.json(); if (isMounted) setBoardData(data); - } catch (error) { - if (isMounted) setBoardError(error.message || "An error occurred"); + } catch (err) { + if (isMounted) { + const message = + err instanceof Error ? err.message : "An unexpected error occurred"; + setBoardError(message); + } } - } - const fetchedLinkedViews = getLinkedViews(); + }; + + const fetchedLinkedViews: GalleryViews[] = getLinkedViews(); setLinkedViews(fetchedLinkedViews); + fetchBoardData(); return () => { @@ -157,20 +156,24 @@ const page = () => {
- {getTopThree(boardData).map((student, idx) => ( -
-
- {student.rank} - - - {student.name} + {boardError ? ( + + ) : ( + getTopThree(boardData).map((student, idx) => ( +
+
+ {student.rank} + + + {student.name} + +
+ + {student.points}pt
- - {student.points}pt - -
- ))} + )) + )}
@@ -232,4 +235,4 @@ const page = () => { ); }; -export default page; +export default HomePage; diff --git a/app/(tabs)/leaderboard/page.tsx b/app/(tabs)/leaderboard/page.tsx index 33d2e2b..ab7c524 100644 --- a/app/(tabs)/leaderboard/page.tsx +++ b/app/(tabs)/leaderboard/page.tsx @@ -2,6 +2,7 @@ import BackgroundWrapper from "@/components/BackgroundWrapper"; import Header from "@/components/Header"; +import DestructibleAlert from "@/components/DestructibleAlert"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { API_URL, getToken } from "@/lib/auth"; import { BoardData, getLeaderboard } from "@/lib/leaderboard"; @@ -40,7 +41,14 @@ const LeaderboardPage = () => { setUserData(fetchedUserData); } catch (error) { console.error(error); - setUserData(undefined); + setUserData({ + name: "", + institution: "", + sscRoll: "", + hscRoll: "", + email: "", + phone: "", + }); } } @@ -97,6 +105,33 @@ const LeaderboardPage = () => { return result ? [{ ...result, rank: sortedData.indexOf(result) + 1 }] : []; }; + if (loading) { + return ( + +
+
+
+
+

Loading...

+
+
+
+ ); + } + + if (boardError) { + return ( + +
+
+
+ +
+
+
+ ); + } + return (
diff --git a/app/(tabs)/live/page.tsx b/app/(tabs)/live/page.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/app/(tabs)/paper/page.tsx b/app/(tabs)/paper/page.tsx index 6e3e323..26ce11e 100644 --- a/app/(tabs)/paper/page.tsx +++ b/app/(tabs)/paper/page.tsx @@ -2,7 +2,7 @@ import { useSearchParams } from "next/navigation"; import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; +import { Suspense, useEffect, useState } from "react"; import Header from "@/components/Header"; import DestructibleAlert from "@/components/DestructibleAlert"; import BackgroundWrapper from "@/components/BackgroundWrapper"; @@ -15,7 +15,7 @@ interface Mock { rating: number; } -export default function PaperScreen() { +function PaperPageContent() { const router = useRouter(); const searchParams = useSearchParams(); const name = searchParams.get("name") || ""; @@ -23,7 +23,6 @@ export default function PaperScreen() { const [questions, setQuestions] = useState(null); const [errorMsg, setErrorMsg] = useState(null); const [refreshing, setRefreshing] = useState(false); - const [componentKey, setComponentKey] = useState(0); async function fetchMocks() { try { @@ -45,12 +44,7 @@ export default function PaperScreen() { const onRefresh = async () => { setRefreshing(true); - await fetchMocks(); - setComponentKey((prevKey) => prevKey + 1); - setTimeout(() => { - setRefreshing(false); - }, 1000); }; if (errorMsg) { @@ -124,3 +118,20 @@ export default function PaperScreen() { ); } + +export default function PaperScreen() { + +
+
+
+

Loading...

+
+
+ + } + > + +
; +} diff --git a/app/(tabs)/settings/page.tsx b/app/(tabs)/settings/page.tsx index 4068bd0..212def5 100644 --- a/app/(tabs)/settings/page.tsx +++ b/app/(tabs)/settings/page.tsx @@ -3,6 +3,7 @@ import BackgroundWrapper from "@/components/BackgroundWrapper"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { API_URL, getToken } from "@/lib/auth"; +import { UserData } from "@/types/auth"; import { Bookmark, ChartColumn, @@ -22,7 +23,14 @@ import React, { useEffect, useState } from "react"; const SettingsPage = () => { const router = useRouter(); - const [userData, setUserData] = useState(null); + const [userData, setUserData] = useState({ + name: "", + institution: "", + sscRoll: "", + hscRoll: "", + email: "", + phone: "", + }); useEffect(() => { async function fetchUser() { diff --git a/app/(tabs)/unit/page.tsx b/app/(tabs)/unit/page.tsx index 8821005..47b227e 100644 --- a/app/(tabs)/unit/page.tsx +++ b/app/(tabs)/unit/page.tsx @@ -16,21 +16,18 @@ const units = [ const Unit = () => { const router = useRouter(); - const handleUnitPress = (unit) => { + const handleUnitPress = (unit: { + id?: number; + name: string; + rating?: number; + }) => { router.push(`/paper?name=${encodeURIComponent(unit.name)}`); }; return (
-
+
diff --git a/app/exam/[id]/page.tsx b/app/exam/[id]/page.tsx index 921dfb2..0abd813 100644 --- a/app/exam/[id]/page.tsx +++ b/app/exam/[id]/page.tsx @@ -6,71 +6,11 @@ import { useTimer } from "@/context/TimerContext"; import { useExam } from "@/context/ExamContext"; import { API_URL, getToken } from "@/lib/auth"; import Header from "@/components/Header"; -import { Bookmark, BookmarkCheck } from "lucide-react"; import { useModal } from "@/context/ModalContext"; import Modal from "@/components/ExamModal"; import { Question } from "@/types/exam"; import QuestionItem from "@/components/QuestionItem"; -// Types -// interface Question { -// id: number; -// question: string; -// options: Record; -// } - -// interface QuestionItemProps { -// question: Question; -// selectedAnswer?: string; -// handleSelect: (questionId: number, option: string) => void; -// } - -// const QuestionItem = React.memo( -// ({ question, selectedAnswer, handleSelect }) => { -// const [bookmark, setBookmark] = useState(false); - -// return ( -//
-//

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

-//
-//
-// -//
-//
-// {Object.entries(question.options).map(([key, value]) => ( -// -// ))} -//
-//
-// ); -// } -// ); - -// QuestionItem.displayName = "QuestionItem"; - export default function ExamPage() { // All hooks at the top - no conditional calls const router = useRouter(); @@ -280,13 +220,7 @@ export default function ExamPage() { // Render the main exam interface return (
-
+
{currentAttempt ? (
@@ -301,8 +235,8 @@ export default function ExamPage() {

Skipped:{" "} - {currentExam?.questions.length - - currentAttempt?.answers.length} + {(currentExam?.questions?.length ?? 0) - + (currentAttempt?.answers?.length ?? 0)}

diff --git a/app/exam/pretest/page.tsx b/app/exam/pretest/page.tsx index 760245c..9042e47 100644 --- a/app/exam/pretest/page.tsx +++ b/app/exam/pretest/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useRouter, useSearchParams } from "next/navigation"; -import { useEffect, useState } from "react"; +import { Suspense, useEffect, useState } from "react"; import { ArrowLeft, HelpCircle, Clock, XCircle } from "lucide-react"; import DestructibleAlert from "@/components/DestructibleAlert"; import BackgroundWrapper from "@/components/BackgroundWrapper"; @@ -18,47 +18,46 @@ interface Metadata { }; } -export default function PretestPage() { +function PretestPageContent() { const router = useRouter(); const searchParams = useSearchParams(); const [examData, setExamData] = useState(); const { startExam, setCurrentExam } = useExam(); // Get params from URL search params - const name = searchParams.get("name") || ""; const id = searchParams.get("id") || ""; const title = searchParams.get("title") || ""; const rating = searchParams.get("rating") || ""; const [metadata, setMetadata] = useState(null); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - async function fetchQuestions() { - if (!id) return; - - try { - setLoading(true); - const questionResponse = await fetch(`${API_URL}/mock/${id}`, { - method: "GET", - }); - if (!questionResponse.ok) { - throw new Error("Failed to fetch questions"); - } - const data = await questionResponse.json(); - const fetchedMetadata: Metadata = data; - - setExamData(data); - setMetadata(fetchedMetadata); - } catch (error) { - console.error(error); - setError(error instanceof Error ? error.message : "An error occurred"); - } finally { - setLoading(false); - } - } + const [error, setError] = useState(); + console.log(loading); useEffect(() => { + async function fetchQuestions() { + if (!id) return; + + try { + setLoading(true); + const questionResponse = await fetch(`${API_URL}/mock/${id}`, { + method: "GET", + }); + if (!questionResponse.ok) { + throw new Error("Failed to fetch questions"); + } + const data = await questionResponse.json(); + const fetchedMetadata: Metadata = data; + + setExamData(data); + setMetadata(fetchedMetadata); + } catch (error) { + console.error(error); + setError(error instanceof Error ? error.message : "An error occurred"); + } finally { + setLoading(false); + } + } if (id) { fetchQuestions(); } @@ -74,15 +73,16 @@ export default function PretestPage() {
- {/* */}
); } function handleStartExam() { + if (!examData) return; + setCurrentExam(examData); - startExam(examData); // Pass examData directly + startExam(examData); router.push(`/exam/${id}?time=${metadata?.metadata.duration}`); } return ( @@ -192,3 +192,22 @@ export default function PretestPage() { ); } + +export default function PretestPage() { + return ( + +
+
+
+

Loading...

+
+
+ + } + > + +
+ ); +} diff --git a/app/exam/results/page.tsx b/app/exam/results/page.tsx index 0ade0f8..4052af5 100644 --- a/app/exam/results/page.tsx +++ b/app/exam/results/page.tsx @@ -1,13 +1,14 @@ "use client"; import { useRouter } from "next/navigation"; -import { useExam, useExamResults } from "@/context/ExamContext"; +import { useExam } from "@/context/ExamContext"; import { useEffect, useState } from "react"; import React from "react"; import { ArrowLeft } from "lucide-react"; import SlidingGallery from "@/components/SlidingGallery"; import QuestionItem from "@/components/QuestionItem"; import { getResultViews } from "@/lib/gallery-views"; +import { Question } from "@/types/exam"; export default function ResultsPage() { const router = useRouter(); @@ -58,15 +59,15 @@ export default function ResultsPage() { const apiResponse = getApiResponse(); - const timeTaken = - currentAttempt.endTime && currentAttempt.startTime - ? Math.round( - (currentAttempt.endTime.getTime() - - currentAttempt.startTime.getTime()) / - 1000 / - 60 - ) - : 0; + // const timeTaken = + // currentAttempt.endTime && currentAttempt.startTime + // ? Math.round( + // (currentAttempt.endTime.getTime() - + // currentAttempt.startTime.getTime()) / + // 1000 / + // 60 + // ) + // : 0; const views = getResultViews(currentAttempt); @@ -98,11 +99,13 @@ export default function ResultsPage() { Solutions
- {apiResponse.questions.map((question) => ( + {apiResponse.questions.map((question: Question) => ( ))} diff --git a/app/page.tsx b/app/page.tsx index 805acb6..47d33d3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -53,7 +53,7 @@ export default function Home() { className="text-center font-medium" style={{ fontFamily: "Montserrat, sans-serif" }} > - Don't have an account?{" "} + Don't have an account? Register here diff --git a/bun.lockb b/bun.lockb index 90fb462..ff2e50d 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/BackgroundWrapper.tsx b/components/BackgroundWrapper.tsx index c874760..e7139e0 100644 --- a/components/BackgroundWrapper.tsx +++ b/components/BackgroundWrapper.tsx @@ -1,6 +1,10 @@ -import React from "react"; +import React, { ReactNode } from "react"; -const BackgroundWrapper = ({ children }) => { +interface BackgroundWrapperProps { + children: ReactNode; +} + +const BackgroundWrapper = ({ children }: BackgroundWrapperProps) => { return (
{ >
{children}
diff --git a/components/DestructibleAlert.tsx b/components/DestructibleAlert.tsx index 36e6ad6..20eff0d 100644 --- a/components/DestructibleAlert.tsx +++ b/components/DestructibleAlert.tsx @@ -1,12 +1,14 @@ import React from "react"; +interface DestructibleAlertProps { + text: string; + extraStyles?: string; +} + const DestructibleAlert = ({ text, extraStyles = "", -}: { - text: string; - extraStyles?: string; -}) => { +}: DestructibleAlertProps) => { return (
{ + title: string; + placeholder?: string; + value: string; + handleChangeText: (value: string) => void; +} const FormField = ({ title, @@ -6,68 +13,38 @@ const FormField = ({ value, handleChangeText, ...props -}) => { +}: FormFieldProps) => { const [showPassword, setShowPassword] = useState(false); + const isPasswordField = title.toLowerCase().includes("password"); - const isPasswordField = title === "Password" || title === "Confirm Password"; + const inputId = `input-${title.replace(/\s+/g, "-").toLowerCase()}`; return (
-
+
handleChangeText(e.target.value)} - className="flex-1 bg-transparent outline-none border-none text-blue-950" - style={{ - color: "#0D47A1", - fontSize: 16, - fontFamily: "inherit", - backgroundColor: "transparent", - border: "none", - outline: "none", - }} + className="flex-1 bg-transparent outline-none border-none text-blue-950 text-[16px] font-inherit" {...props} /> {isPasswordField && ( diff --git a/components/Header.tsx b/components/Header.tsx index a9c7f19..198ea1b 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -13,7 +13,7 @@ interface HeaderProps { displayUser?: boolean; displaySubject?: string; displayTabTitle?: string; - examDuration?: string; + examDuration?: string | null; } const Header = ({ @@ -28,7 +28,7 @@ const Header = ({ const [totalSeconds, setTotalSeconds] = useState( examDuration ? parseInt(examDuration) * 60 : 0 ); - const { timeRemaining, stopTimer } = useTimer(); + const { stopTimer } = useTimer(); const [userData, setUserData] = useState(); useEffect(() => { diff --git a/components/QuestionItem.tsx b/components/QuestionItem.tsx index 8ba935e..64557a3 100644 --- a/components/QuestionItem.tsx +++ b/components/QuestionItem.tsx @@ -6,7 +6,7 @@ import { Badge } from "./ui/badge"; interface ResultItemProps { mode: "result"; question: Question; - selectedAnswer: string | undefined; + selectedAnswer: { answer: string } | undefined; } interface ExamItemProps { @@ -20,10 +20,22 @@ type QuestionItemProps = ResultItemProps | ExamItemProps; const QuestionItem = (props: QuestionItemProps) => { const [bookmark, setBookmark] = useState(false); - const { question, selectedAnswer } = props; + + const { question } = props; const isExam = props.mode === "exam"; + // Extract correct type-safe selectedAnswer + const selectedAnswer = isExam + ? props.selectedAnswer + : props.selectedAnswer?.answer; + + const handleOptionSelect = (key: string) => { + if (isExam && props.handleSelect) { + props.handleSelect(parseInt(question.id), key); + } + }; + return (

@@ -45,19 +57,14 @@ const QuestionItem = (props: QuestionItemProps) => { {isExam ? (
- {Object.entries(question.options).map(([key, value]) => { + {Object.entries(question.options ?? {}).map(([key, value]) => { const isSelected = selectedAnswer === key; return (
+
- {Object.entries(question.options).map(([key, value]) => { + {Object.entries(question.options ?? {}).map(([key, value]) => { const isCorrect = key === question.correctAnswer; - const isSelected = key === selectedAnswer?.answer; + const isSelected = key === selectedAnswer; 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) { + } else if (isSelected && !isCorrect) { optionStyle += " bg-red-600 text-white border-red-600"; - } - - if (!isCorrect && !isSelected) { + } else { optionStyle += " border-gray-300 text-gray-700"; } @@ -118,7 +122,9 @@ const QuestionItem = (props: QuestionItemProps) => { ); })}
+
+

Solution:

{question.solution}

diff --git a/components/SlidingGallery.tsx b/components/SlidingGallery.tsx index baa20a7..6abc755 100644 --- a/components/SlidingGallery.tsx +++ b/components/SlidingGallery.tsx @@ -1,13 +1,15 @@ -import React, { useState, useRef, useEffect } from "react"; -import Link from "next/link"; -import Image from "next/image"; +import React, { + useState, + useRef, + useEffect, + useCallback, + UIEvent, +} from "react"; import styles from "../css/SlidingGallery.module.css"; +import { GalleryViews } from "@/types/gallery"; interface SlidingGalleryProps { - views?: { - id: string; - content: React.ReactNode; - }[]; + views: GalleryViews[] | undefined; className?: string; showPagination?: boolean; autoScroll?: boolean; @@ -17,7 +19,7 @@ interface SlidingGalleryProps { } const SlidingGallery = ({ - views = [], + views, className = "", showPagination = true, autoScroll = false, @@ -25,15 +27,47 @@ const SlidingGallery = ({ onSlideChange = () => {}, height = "100vh", }: SlidingGalleryProps) => { - const [activeIdx, setActiveIdx] = useState(0); + 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 scrollRef = useRef(null); + const galleryRef = useRef(null); + const autoScrollRef = useRef(null); + + const handleScroll = (event: UIEvent) => { + handleUserInteraction(); + const scrollLeft = event.currentTarget.scrollLeft; + const slideWidth = dimensions.width; + const index = Math.round(scrollLeft / slideWidth); + + if (index !== activeIdx) { + setActiveIdx(index); + onSlideChange(index); + } + }; + + const goToSlide = useCallback( + (index: number) => { + if (scrollRef.current) { + scrollRef.current.scrollTo({ + left: index * dimensions.width, + behavior: "smooth", + }); + } + }, + [dimensions.width] + ); + + const handleDotClick = (index: number) => { + handleUserInteraction(); + goToSlide(index); + setActiveIdx(index); + onSlideChange(index); + }; // Auto-scroll functionality useEffect(() => { - if (autoScroll && views.length > 1) { + if (autoScroll && views && views.length > 1) { autoScrollRef.current = setInterval(() => { setActiveIdx((prevIdx) => { const nextIdx = (prevIdx + 1) % views.length; @@ -48,15 +82,17 @@ const SlidingGallery = ({ } }; } - }, [autoScroll, autoScrollInterval, views.length]); + }, [autoScroll, autoScrollInterval, views?.length, goToSlide, views]); // Clear auto-scroll on user interaction const handleUserInteraction = () => { if (autoScrollRef.current) { clearInterval(autoScrollRef.current); + autoScrollRef.current = null; } }; + // Update dimensions useEffect(() => { const updateDimensions = () => { if (galleryRef.current) { @@ -67,18 +103,13 @@ const SlidingGallery = ({ } }; - // Initial dimension update updateDimensions(); - - // Add resize listener window.addEventListener("resize", updateDimensions); - - // Cleanup return () => window.removeEventListener("resize", updateDimensions); }, []); + // Recalculate index when dimension changes useEffect(() => { - // Recalculate active index when dimensions change if (scrollRef.current && dimensions.width > 0) { const scrollLeft = scrollRef.current.scrollLeft; const slideWidth = dimensions.width; @@ -87,34 +118,6 @@ const SlidingGallery = ({ } }, [dimensions]); - const handleScroll = (event: { target: { scrollLeft: any } }) => { - 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 (
@@ -138,6 +141,8 @@ const SlidingGallery = ({ style={{ width: "100%", height: "100%", + overflowX: "scroll", + display: "flex", }} > {views.map((item) => ( @@ -154,6 +159,7 @@ const SlidingGallery = ({
))}
+ {showPagination && views.length > 1 && (
{views.map((_, index) => ( diff --git a/context/ExamContext.tsx b/context/ExamContext.tsx index f219fbf..b593920 100644 --- a/context/ExamContext.tsx +++ b/context/ExamContext.tsx @@ -86,7 +86,7 @@ export const ExamProvider: React.FC<{ children: ReactNode }> = ({ setCurrentAttemptState(null); }; - const startExam = (exam?: Exam) => { + const startExam = (exam?: Exam): void => { const examToUse = exam || currentExam; if (!examToUse) { @@ -101,6 +101,7 @@ export const ExamProvider: React.FC<{ children: ReactNode }> = ({ answers: [], startTime: new Date(), totalQuestions: 0, + score: 0, }; setCurrentAttemptState(attempt); diff --git a/context/ModalContext.tsx b/context/ModalContext.tsx index 6c9e49f..b08d85e 100644 --- a/context/ModalContext.tsx +++ b/context/ModalContext.tsx @@ -1,24 +1,41 @@ "use client"; import { createContext, useContext, useState } from "react"; -const ModalContext = createContext(null); +// Define the context type +interface ModalContextType { + isOpen: boolean; + open: () => void; + close: () => void; + toggle: () => void; +} -export function ModalProvider({ children }) { +// Create context with default values (no null) +const ModalContext = createContext({ + isOpen: false, + open: () => {}, + close: () => {}, + toggle: () => {}, +}); + +export function ModalProvider({ children }: { children: React.ReactNode }) { const [isOpen, setIsOpen] = useState(false); const open = () => setIsOpen(true); const close = () => setIsOpen(false); const toggle = () => setIsOpen((prev) => !prev); + const value: ModalContextType = { + isOpen, + open, + close, + toggle, + }; + return ( - - {children} - + {children} ); } -export function useModal() { - const ctx = useContext(ModalContext); - if (!ctx) throw new Error("useModal must be inside "); - return ctx; +export function useModal(): ModalContextType { + return useContext(ModalContext); } diff --git a/eslint.config.mjs b/eslint.config.mjs index c85fb67..c2d45e9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -11,6 +11,15 @@ const compat = new FlatCompat({ const eslintConfig = [ ...compat.extends("next/core-web-vitals", "next/typescript"), + { + rules: { + // Disable the no-explicit-any rule + "@typescript-eslint/no-explicit-any": "off", + + // Alternative: Make it a warning instead of error + // "@typescript-eslint/no-explicit-any": "warn", + }, + }, ]; export default eslintConfig; diff --git a/lib/auth.ts b/lib/auth.ts index f9d4378..72e2347 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,20 +1,37 @@ export const API_URL = "https://examjam-api.pptx704.com"; -// Cookie utility functions -const setCookie = (name, value, days = 7) => { +// Cookie utility function +const setCookie = (name: string, value: string | null, days: number = 7) => { if (typeof document === "undefined") return; if (value === null) { - // Delete cookie by setting expiration to past date document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Strict; Secure`; } else { const expires = new Date(); expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000); - document.cookie = `${name}=${value}; expires=${expires.toUTCString()}; path=/; SameSite=Strict; Secure`; + document.cookie = `${name}=${encodeURIComponent( + value + )}; expires=${expires.toUTCString()}; path=/; SameSite=Strict; Secure`; } }; -export const login = async (form, setToken) => { +interface AuthForm { + email: string; + password: string; + [key: string]: any; // for flexibility +} + +type SetTokenFn = (token: string) => void; + +// Optional: Create a custom error type to carry extra data +interface APIError extends Error { + response?: any; +} + +export const login = async ( + form: AuthForm, + setToken: SetTokenFn +): Promise => { const response = await fetch(`${API_URL}/auth/login`, { method: "POST", headers: { @@ -29,27 +46,14 @@ export const login = async (form, setToken) => { throw new Error(data.message || "Login failed"); } - // Save the token to cookies instead of secure storage setCookie("authToken", data.token); - setToken(data.token); // Update the token in context + setToken(data.token); }; -const handleError = (error) => { - // Check if error has a "detail" property - if (error?.detail) { - // Match the field causing the issue - const match = error.detail.match(/Key \((.*?)\)=\((.*?)\)/); - - if (match) { - const field = match[1]; // The field name, e.g., "phone" - const value = match[2]; // The duplicate value, e.g., "0987654321" - return `The ${field} already exists. Please use a different value.`; - } - } - return "An unexpected error occurred. Please try again."; -}; - -export const register = async (form, setToken) => { +export const register = async ( + form: AuthForm, + setToken: SetTokenFn +): Promise => { const response = await fetch(`${API_URL}/auth/register`, { method: "POST", headers: { @@ -58,22 +62,19 @@ export const register = async (form, setToken) => { body: JSON.stringify(form), }); - const data = await response.json(); // Parse the response JSON + const data = await response.json(); if (!response.ok) { - // Instead of throwing a string, include full error data for debugging - const error = new Error(data?.detail || "Registration failed"); - error.response = data; // Attach the full response for later use + const error: APIError = new Error(data?.detail || "Registration failed"); + error.response = data; throw error; } - // Save the token to cookies instead of secure storage setCookie("authToken", data.token); - setToken(data.token); // Update the token in context + setToken(data.token); }; -// Additional utility function to get token from cookies (if needed elsewhere) -export const getTokenFromCookie = () => { +export const getTokenFromCookie = (): string | null => { if (typeof document === "undefined") return null; const value = `; ${document.cookie}`; @@ -84,17 +85,15 @@ export const getTokenFromCookie = () => { return null; }; -// Utility function to clear auth token (for logout) -export const clearAuthToken = () => { +export const clearAuthToken = (): void => { setCookie("authToken", null); }; -export const getToken = async () => { +export const getToken = async (): Promise => { if (typeof window === "undefined") { return null; } - // Extract authToken from cookies const match = document.cookie.match(/(?:^|;\s*)authToken=([^;]*)/); return match ? decodeURIComponent(match[1]) : null; }; diff --git a/lib/gallery-views.tsx b/lib/gallery-views.tsx index e5f0195..dc69bae 100644 --- a/lib/gallery-views.tsx +++ b/lib/gallery-views.tsx @@ -1,16 +1,14 @@ // lib/gallery-views.tsx import Link from "next/link"; import Image from "next/image"; +import { ExamAnswer } from "@/types/exam"; +import { GalleryViews } from "@/types/gallery"; +// Define the ExamResults type if not already defined interface ExamResults { score: number; totalQuestions: number; - answers: string[]; -} - -interface LinkedViews { - id: string; - content: React.ReactNode; + answers: ExamAnswer[]; // or more specific type based on your answer structure } export const getResultViews = (examResults: ExamResults | null) => [ @@ -104,9 +102,9 @@ export const getResultViews = (examResults: ExamResults | null) => [ }, ]; -export const getLinkedViews = (): LinkedViews[] => [ +export const getLinkedViews = (): GalleryViews[] => [ { - id: "1", + id: 1, content: ( ; type: "multiple-choice" | "text" | "boolean" | undefined; correctAnswer: string | undefined; @@ -18,7 +18,7 @@ export interface Exam { export interface ExamAnswer { questionId: string; - answer: any; + answer: string; timestamp: Date; } @@ -28,7 +28,7 @@ export interface ExamAttempt { answers: ExamAnswer[]; startTime: Date; endTime?: Date; - score?: number; + score: number; passed?: boolean; apiResponse?: any; totalQuestions: number; @@ -42,9 +42,9 @@ export interface ExamContextType { // Actions setCurrentExam: (exam: Exam) => void; - startExam: () => void; + startExam: (exam?: Exam) => void; setAnswer: (questionId: string, answer: any) => void; - submitExam: () => ExamAttempt; + submitExam: () => ExamAttempt | null; clearExam: () => void; setApiResponse: (response: any) => void; diff --git a/types/gallery.d.ts b/types/gallery.d.ts new file mode 100644 index 0000000..581d375 --- /dev/null +++ b/types/gallery.d.ts @@ -0,0 +1,4 @@ +export interface GalleryViews { + id: number; + content: React.JSX.Element; +}