generated from muhtadeetaron/nextjs-template
chore(capacitor): refactor codebase for capacitor entry
This commit is contained in:
@ -8,7 +8,6 @@ import BackgroundWrapper from "@/components/BackgroundWrapper";
|
|||||||
import FormField from "@/components/FormField";
|
import FormField from "@/components/FormField";
|
||||||
import { login } from "@/lib/auth";
|
import { login } from "@/lib/auth";
|
||||||
import DestructibleAlert from "@/components/DestructibleAlert";
|
import DestructibleAlert from "@/components/DestructibleAlert";
|
||||||
import { useAuth } from "@/context/AuthContext";
|
|
||||||
import { LoginForm } from "@/types/auth";
|
import { LoginForm } from "@/types/auth";
|
||||||
import { CircleAlert } from "lucide-react";
|
import { CircleAlert } from "lucide-react";
|
||||||
import { useAuthStore } from "@/stores/authStore";
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
import { register } from "@/lib/auth";
|
import { register } from "@/lib/auth";
|
||||||
import { useAuth } from "@/context/AuthContext";
|
|
||||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||||
import FormField from "@/components/FormField";
|
import FormField from "@/components/FormField";
|
||||||
import DestructibleAlert from "@/components/DestructibleAlert";
|
import DestructibleAlert from "@/components/DestructibleAlert";
|
||||||
|
|||||||
@ -2,16 +2,8 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||||
import {
|
import { Bookmark, BookmarkCheck, ListFilter, MoveLeft } from "lucide-react";
|
||||||
Bookmark,
|
|
||||||
BookmarkCheck,
|
|
||||||
Check,
|
|
||||||
ListFilter,
|
|
||||||
MoveLeft,
|
|
||||||
OctagonX,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import DestructibleAlert from "@/components/DestructibleAlert";
|
|
||||||
|
|
||||||
interface Question {
|
interface Question {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import DestructibleAlert from "@/components/DestructibleAlert";
|
|||||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||||
import { API_URL, getToken } from "@/lib/auth";
|
import { API_URL, getToken } from "@/lib/auth";
|
||||||
import { Loader, RefreshCw } from "lucide-react";
|
import { Loader, RefreshCw } from "lucide-react";
|
||||||
import { useAuth } from "@/context/AuthContext";
|
|
||||||
import { useAuthStore } from "@/stores/authStore";
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
|
|
||||||
type Mock = {
|
type Mock = {
|
||||||
@ -74,7 +73,7 @@ export default function MockScreen() {
|
|||||||
<Header displayTabTitle="Mocks" />
|
<Header displayTabTitle="Mocks" />
|
||||||
<div className="overflow-y-auto">
|
<div className="overflow-y-auto">
|
||||||
<div className="mt-5 px-5">
|
<div className="mt-5 px-5">
|
||||||
<DestructibleAlert text={errorMsg} extraStyles="" />
|
<DestructibleAlert text={errorMsg} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center mt-4">
|
<div className="flex justify-center mt-4">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useSearchParams } from "next/navigation";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Header from "@/components/Header";
|
import Header from "@/components/Header";
|
||||||
@ -8,8 +7,6 @@ import DestructibleAlert from "@/components/DestructibleAlert";
|
|||||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||||
import { API_URL, getToken } from "@/lib/auth";
|
import { API_URL, getToken } from "@/lib/auth";
|
||||||
import { Loader, RefreshCw } from "lucide-react";
|
import { Loader, RefreshCw } from "lucide-react";
|
||||||
import { useAuth } from "@/context/AuthContext";
|
|
||||||
import { Question } from "@/types/exam";
|
|
||||||
import { useAuthStore } from "@/stores/authStore";
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
|
|
||||||
type Subject = {
|
type Subject = {
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import DestructibleAlert from "@/components/DestructibleAlert";
|
|||||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||||
import { API_URL, getToken } from "@/lib/auth";
|
import { API_URL, getToken } from "@/lib/auth";
|
||||||
import { Loader, RefreshCw } from "lucide-react";
|
import { Loader, RefreshCw } from "lucide-react";
|
||||||
import { useAuth } from "@/context/AuthContext";
|
|
||||||
import { useAuthStore } from "@/stores/authStore";
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
|
|
||||||
type Topic = {
|
type Topic = {
|
||||||
|
|||||||
@ -8,68 +8,41 @@ import SlidingGallery from "@/components/SlidingGallery";
|
|||||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||||
import { ChevronRight } from "lucide-react";
|
import { ChevronRight } from "lucide-react";
|
||||||
import styles from "@/css/Home.module.css";
|
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 { getLinkedViews } from "@/lib/gallery-views";
|
||||||
import { getTopThree } from "@/lib/leaderboard";
|
|
||||||
import DestructibleAlert from "@/components/DestructibleAlert";
|
|
||||||
import { GalleryViews } from "@/types/gallery";
|
import { GalleryViews } from "@/types/gallery";
|
||||||
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
interface LeaderboardEntry {
|
import DestructibleAlert from "@/components/DestructibleAlert";
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
points: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [boardData, setBoardData] = useState<LeaderboardEntry[]>([]);
|
|
||||||
const [boardError, setBoardError] = useState<string | null>(null);
|
|
||||||
const [linkedViews, setLinkedViews] = useState<GalleryViews[]>();
|
const [linkedViews, setLinkedViews] = useState<GalleryViews[]>();
|
||||||
|
const { user } = useAuthStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// let isMounted = true;
|
|
||||||
|
|
||||||
// const fetchBoardData = async () => {
|
|
||||||
// try {
|
|
||||||
// const response = await fetch(`${API_URL}/leaderboard`);
|
|
||||||
// if (!response.ok) {
|
|
||||||
// throw new Error("Failed to fetch leaderboard data");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const data: LeaderboardEntry[] = await response.json();
|
|
||||||
// if (isMounted) setBoardData(data);
|
|
||||||
// } catch (err) {
|
|
||||||
// if (isMounted) {
|
|
||||||
// const message =
|
|
||||||
// err instanceof Error ? err.message : "An unexpected error occurred";
|
|
||||||
// setBoardError(message);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
const fetchedLinkedViews: GalleryViews[] = getLinkedViews();
|
const fetchedLinkedViews: GalleryViews[] = getLinkedViews();
|
||||||
setLinkedViews(fetchedLinkedViews);
|
setLinkedViews(fetchedLinkedViews);
|
||||||
|
|
||||||
// fetchBoardData();
|
|
||||||
|
|
||||||
// return () => {
|
|
||||||
// isMounted = false;
|
|
||||||
// };
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BackgroundWrapper>
|
<BackgroundWrapper>
|
||||||
<div className={styles.container}>
|
<div className="flex-1 min-h-screen">
|
||||||
<Header displayUser />
|
<Header displayUser />
|
||||||
<div className={styles.scrollContainer}>
|
<div className="overflow-y-auto pt-4 h-[calc(100vh-80px)]">
|
||||||
<div className={styles.contentWrapper}>
|
<div className="pb-40 mx-6">
|
||||||
|
{!user?.is_verified && (
|
||||||
|
<DestructibleAlert
|
||||||
|
text="Please verify your account. Check your email for the verification link."
|
||||||
|
variant="warning"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<SlidingGallery views={linkedViews} height="23vh" />
|
<SlidingGallery views={linkedViews} height="23vh" />
|
||||||
<div className={styles.mainContent}>
|
<div className="flex flex-col gap-9">
|
||||||
{/* Categories Section */}
|
{/* Categories Section */}
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.sectionHeader}>
|
<div className="flex item-scenter justify-between">
|
||||||
<h2 className={styles.sectionTitle}>Categories</h2>
|
<h2 className="text-2xl font-bold text-[#113768]">
|
||||||
|
Categories
|
||||||
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push("/categories")}
|
onClick={() => router.push("/categories")}
|
||||||
className={styles.arrowButton}
|
className={styles.arrowButton}
|
||||||
|
|||||||
@ -1,186 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
import React from "react";
|
||||||
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";
|
|
||||||
import { UserData } from "@/types/auth";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useAuth } from "@/context/AuthContext";
|
|
||||||
|
|
||||||
const LeaderboardPage = () => {
|
const LeaderboardPage = () => {
|
||||||
const [boardError, setBoardError] = useState<string | null>(null);
|
return <></>;
|
||||||
const [boardData, setBoardData] = useState<BoardData[]>([]);
|
|
||||||
const {user, isLoading} = useAuth()
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
|
|
||||||
async function fetchBoardData() {
|
|
||||||
try {
|
|
||||||
const boardResponse = await fetch(`${API_URL}/leaderboard`, {
|
|
||||||
method: "GET",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!boardResponse.ok) {
|
|
||||||
throw new Error("Failed to fetch leaderboard data");
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchedBoardData = await boardResponse.json();
|
|
||||||
if (Array.isArray(fetchedBoardData) && fetchedBoardData.length > 0) {
|
|
||||||
setBoardData(fetchedBoardData);
|
|
||||||
} else {
|
|
||||||
setBoardError("No leaderboard data available.");
|
|
||||||
setBoardData([]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
setBoardError("Something went wrong. Please try again.");
|
|
||||||
setBoardData([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchBoardData();
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTopThree = (boardData: BoardData[]) => {
|
|
||||||
if (!boardData || !Array.isArray(boardData)) return [];
|
|
||||||
const sortedData = boardData
|
|
||||||
.filter((player) => player?.points !== undefined) // Ensure `points` exists
|
|
||||||
.sort((a, b) => b.points - a.points);
|
|
||||||
|
|
||||||
const topThree = sortedData.slice(0, 3).map((player, index) => ({
|
|
||||||
...player,
|
|
||||||
rank: index + 1,
|
|
||||||
height: index === 0 ? 280 : index === 1 ? 250 : 220,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return [topThree[1], topThree[0], topThree[2]].filter(Boolean); // Handle missing players
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUserData = (boardData: BoardData[], name: string) => {
|
|
||||||
if (!boardData || !Array.isArray(boardData)) return [];
|
|
||||||
const sortedData = boardData
|
|
||||||
.filter((player) => player?.name && player?.points !== undefined)
|
|
||||||
.sort((a, b) => b.points - a.points);
|
|
||||||
|
|
||||||
const result = sortedData.find((player) => player.name === name);
|
|
||||||
return result ? [{ ...result, rank: sortedData.indexOf(result) + 1 }] : [];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<BackgroundWrapper>
|
|
||||||
<section>
|
|
||||||
<Header displayTabTitle="Leaderboard" />
|
|
||||||
<section className="flex flex-col mx-10 pt-10 space-y-4">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-900 mb-4"></div>
|
|
||||||
<p className="text-lg font-medium text-gray-900">Loading...</p>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</BackgroundWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (boardError) {
|
|
||||||
return (
|
|
||||||
<BackgroundWrapper>
|
|
||||||
<section>
|
|
||||||
<Header displayTabTitle="Leaderboard" />
|
|
||||||
<section className="flex flex-col mx-10 pt-10 space-y-4">
|
|
||||||
<DestructibleAlert text={boardError} />
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</BackgroundWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BackgroundWrapper>
|
|
||||||
<section>
|
|
||||||
<Header displayTabTitle={"Leaderboard"} />
|
|
||||||
<section className="flex flex-col mx-10 pt-10 space-y-4">
|
|
||||||
<section className="flex justify-evenly items-end">
|
|
||||||
{getTopThree(boardData).map((student, idx) =>
|
|
||||||
student ? (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className="w-[100px] flex flex-col bg-[#113768] rounded-t-xl items-center justify-start pt-4 space-y-3"
|
|
||||||
style={{ height: student.height }}
|
|
||||||
>
|
|
||||||
<h3 className="font-bold text-xl text-white">
|
|
||||||
{student.rank}
|
|
||||||
</h3>
|
|
||||||
<Avatar className="bg-slate-300 w-12 h-12">
|
|
||||||
<AvatarFallback className="text-xl font-semibold">
|
|
||||||
{student.name.charAt(0).toUpperCase()}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<p className="font-bold text-md text-center text-white">
|
|
||||||
{student.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-white">({student.points}pt)</p>
|
|
||||||
</div>
|
|
||||||
) : null
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
<div className="w-full border-[0.5px] border-[#c5dbf8] bg-[#c5dbf8]"></div>
|
|
||||||
<section className="border-[1px] border-[#c0dafc] w-full rounded-3xl p-6 space-y-4 mb-20">
|
|
||||||
<section>
|
|
||||||
{getUserData(boardData, userData.name).map((user, idx) => (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className="flex bg-[#113768] rounded-[8] py-2 px-4 justify-between items-center"
|
|
||||||
>
|
|
||||||
<div className=" flex gap-3 items-center">
|
|
||||||
<h2 className="font-medium text-sm text-white">
|
|
||||||
{user.rank}
|
|
||||||
</h2>
|
|
||||||
<Avatar className="bg-slate-300 w-6 h-6">
|
|
||||||
<AvatarFallback className="text-sm font-semibold">
|
|
||||||
{user.name.charAt(0).toUpperCase()}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<h3 className="font-medium text-sm text-white">You</h3>
|
|
||||||
</div>
|
|
||||||
<p className="font-medium text-white/70 text-sm">
|
|
||||||
{user.points}pt
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
<div className="w-full border-[0.5px] border-[#c5dbf8] bg-[#c5dbf8]"></div>
|
|
||||||
<section className="space-y-4">
|
|
||||||
{getLeaderboard(boardData)
|
|
||||||
.slice(0, 10)
|
|
||||||
.map((user, idx) => (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className="flex border-2 border-[#c5dbf8] rounded-[8] py-2 px-4 justify-between items-center"
|
|
||||||
>
|
|
||||||
<div className="flex gap-3 items-center">
|
|
||||||
<h2 className="font-medium text-sm">{idx + 1}</h2>
|
|
||||||
<Avatar className="bg-slate-300 w-6 h-6">
|
|
||||||
<AvatarFallback className="text-sm font-semibold">
|
|
||||||
{user.name.charAt(0).toUpperCase()}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<h3 className="font-medium text-sm">
|
|
||||||
{user.name.split(" ").slice(0, 2).join(" ")}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<p className="font-medium text-[#000]/40 text-sm">
|
|
||||||
{user.points}pt
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</BackgroundWrapper>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LeaderboardPage;
|
export default LeaderboardPage;
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
import { useAuth } from "@/context/AuthContext";
|
|
||||||
import { useAuthStore } from "@/stores/authStore";
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
import {
|
import {
|
||||||
Bookmark,
|
Bookmark,
|
||||||
|
|||||||
@ -1,33 +1,22 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useCallback, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import Header from "@/components/Header";
|
import Header from "@/components/Header";
|
||||||
import QuestionItem from "@/components/QuestionItem";
|
import QuestionItem from "@/components/QuestionItem";
|
||||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||||
import { useExamStore } from "@/stores/examStore";
|
import { useExamStore } from "@/stores/examStore";
|
||||||
import { useTimerStore } from "@/stores/timerStore";
|
import { useTimerStore } from "@/stores/timerStore";
|
||||||
import { useExamExitGuard } from "@/hooks/useExamExitGuard";
|
|
||||||
|
|
||||||
export default function ExamPage() {
|
export default function ExamPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const test_id = searchParams.get("test_id") || "";
|
const test_id = searchParams.get("test_id") || "";
|
||||||
const type = searchParams.get("type") || "";
|
const type = searchParams.get("type") || "";
|
||||||
|
|
||||||
const {
|
const { setStatus, test, answers, startExam, setAnswer, submitExam } =
|
||||||
setStatus,
|
useExamStore();
|
||||||
test,
|
|
||||||
answers,
|
|
||||||
startExam,
|
|
||||||
setAnswer,
|
|
||||||
submitExam,
|
|
||||||
cancelExam,
|
|
||||||
status,
|
|
||||||
} = useExamStore();
|
|
||||||
const { resetTimer, stopTimer } = useTimerStore();
|
const { resetTimer, stopTimer } = useTimerStore();
|
||||||
const { showExitDialog } = useExamExitGuard(type);
|
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ function PretestPageContent() {
|
|||||||
setStatus("in-progress");
|
setStatus("in-progress");
|
||||||
|
|
||||||
router.push(
|
router.push(
|
||||||
`/exam/${id}?type=${type}&test_id=${metadata?.test_id}&attempt_id=${metadata?.attempt_id}`
|
`/exam/exam-screen?type=${type}&test_id=${metadata?.test_id}&attempt_id=${metadata?.attempt_id}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ExamProvider } from "@/context/ExamContext";
|
|
||||||
import { TimerProvider } from "@/context/TimerContext";
|
|
||||||
import { AuthProvider } from "@/context/AuthContext";
|
|
||||||
import { ModalProvider } from "@/context/ModalContext";
|
import { ModalProvider } from "@/context/ModalContext";
|
||||||
|
|
||||||
export function Providers({ children }: { children: React.ReactNode }) {
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import type { CapacitorConfig } from "@capacitor/cli";
|
// capacitor.config.ts
|
||||||
|
import { CapacitorConfig } from "@capacitor/cli";
|
||||||
|
|
||||||
const config: CapacitorConfig = {
|
const config: CapacitorConfig = {
|
||||||
appId: "com.examjam.solanine",
|
appId: "com.examjam.omukk",
|
||||||
appName: "ExamJam",
|
appName: "ExamJam",
|
||||||
webDir: "public",
|
webDir: "out", // ✅ point to your Next.js static export
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@ -1,18 +1,38 @@
|
|||||||
import React, { JSX } from "react";
|
import React, { JSX } from "react";
|
||||||
|
|
||||||
interface DestructibleAlertProps {
|
interface DestructibleAlertProps {
|
||||||
|
variant?: "error" | "warning" | "alert";
|
||||||
text: string;
|
text: string;
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DestructibleAlert: React.FC<DestructibleAlertProps> = ({
|
const DestructibleAlert: React.FC<DestructibleAlertProps> = ({
|
||||||
|
variant,
|
||||||
text,
|
text,
|
||||||
icon,
|
icon,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className=" bg-red-200 rounded-3xl py-6 flex flex-col items-center justify-center gap-2 w-full ">
|
<div
|
||||||
|
className={`${
|
||||||
|
variant === "error"
|
||||||
|
? "bg-red-200"
|
||||||
|
: variant === "warning"
|
||||||
|
? "bg-yellow-200"
|
||||||
|
: "bg-green-200"
|
||||||
|
} rounded-3xl py-6 flex flex-col items-center justify-center gap-2 w-full `}
|
||||||
|
>
|
||||||
<div>{icon}</div>
|
<div>{icon}</div>
|
||||||
<p className="text-lg font-bold text-center text-red-800">{text}</p>
|
<p
|
||||||
|
className={`text-lg font-bold text-center ${
|
||||||
|
variant === "error"
|
||||||
|
? "text-red-800"
|
||||||
|
: variant === "warning"
|
||||||
|
? "text-yellow-800"
|
||||||
|
: "text-green-800"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,13 +2,10 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { ChevronLeft, Layers, Loader } from "lucide-react";
|
import { ChevronLeft, Layers } from "lucide-react";
|
||||||
import { useTimer } from "@/context/TimerContext";
|
|
||||||
import styles from "@/css/Header.module.css";
|
import styles from "@/css/Header.module.css";
|
||||||
import { useExam } from "@/context/ExamContext";
|
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
import { useModal } from "@/context/ModalContext";
|
import { useModal } from "@/context/ModalContext";
|
||||||
import { useAuth } from "@/context/AuthContext";
|
|
||||||
import { useAuthStore } from "@/stores/authStore";
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
import { useTimerStore } from "@/stores/timerStore";
|
import { useTimerStore } from "@/stores/timerStore";
|
||||||
import { useExamStore } from "@/stores/examStore";
|
import { useExamStore } from "@/stores/examStore";
|
||||||
|
|||||||
@ -9,9 +9,9 @@ interface QuestionItemProps {
|
|||||||
index: number;
|
index: number;
|
||||||
selectedAnswer: Answer;
|
selectedAnswer: Answer;
|
||||||
onSelect: (answer: Answer) => void;
|
onSelect: (answer: Answer) => void;
|
||||||
userAnswer?: Answer; // new
|
userAnswer?: Answer;
|
||||||
correctAnswer?: Answer; // new
|
correctAnswer?: Answer;
|
||||||
showResults?: boolean; // control whether to highlight or not
|
showResults?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const letters = ["A", "B", "C", "D"]; // extend if needed
|
const letters = ["A", "B", "C", "D"]; // extend if needed
|
||||||
@ -40,39 +40,20 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
|
|||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{question.options.map((opt, optIdx) => {
|
{question.options.map((opt, optIdx) => {
|
||||||
const isSelected =
|
const isSelected = selectedAnswer === optIdx;
|
||||||
question.type === "Single"
|
|
||||||
? selectedAnswer === optIdx
|
|
||||||
: Array.isArray(selectedAnswer) &&
|
|
||||||
selectedAnswer.includes(optIdx);
|
|
||||||
|
|
||||||
// ✅ logic for coloring after results
|
// ✅ logic for coloring
|
||||||
let btnClasses = "bg-gray-100 text-gray-900 border-gray-400";
|
let btnClasses = "bg-gray-100 text-gray-900 border-gray-400";
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
btnClasses = "bg-blue-600 text-white border-blue-600";
|
btnClasses = "bg-blue-600 text-white border-blue-600";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showResults && correctAnswer !== undefined) {
|
if (showResults && correctAnswer !== undefined) {
|
||||||
if (question.type === "Single") {
|
if (userAnswer === optIdx && userAnswer !== correctAnswer) {
|
||||||
if (userAnswer === optIdx && userAnswer !== correctAnswer) {
|
btnClasses = "bg-red-500 text-white border-red-600"; // wrong
|
||||||
btnClasses = "bg-red-500 text-white border-red-600"; // wrong
|
}
|
||||||
}
|
if (correctAnswer === optIdx) {
|
||||||
if (correctAnswer === optIdx) {
|
btnClasses = "bg-green-500 text-white border-green-600"; // correct
|
||||||
btnClasses = "bg-green-500 text-white border-green-600"; // correct
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Multi-select case
|
|
||||||
const userSelected =
|
|
||||||
Array.isArray(userAnswer) && userAnswer.includes(optIdx);
|
|
||||||
const isCorrect =
|
|
||||||
Array.isArray(correctAnswer) && correctAnswer.includes(optIdx);
|
|
||||||
|
|
||||||
if (userSelected && !isCorrect) {
|
|
||||||
btnClasses = "bg-red-500 text-white border-red-600";
|
|
||||||
}
|
|
||||||
if (isCorrect) {
|
|
||||||
btnClasses = "bg-green-500 text-white border-green-600";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,20 +61,8 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
|
|||||||
<div key={optIdx} className="flex items-center gap-3">
|
<div key={optIdx} className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (showResults) return; // disable changes in results mode
|
if (showResults) return; // disable selection in results mode
|
||||||
if (question.type === "Single") {
|
onSelect(optIdx); // always a number
|
||||||
onSelect(optIdx);
|
|
||||||
} else {
|
|
||||||
let newAnswers = Array.isArray(selectedAnswer)
|
|
||||||
? [...selectedAnswer]
|
|
||||||
: [];
|
|
||||||
if (newAnswers.includes(optIdx)) {
|
|
||||||
newAnswers = newAnswers.filter((a) => a !== optIdx);
|
|
||||||
} else {
|
|
||||||
newAnswers.push(optIdx);
|
|
||||||
}
|
|
||||||
onSelect(newAnswers);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
className={`w-7 h-7 rounded-full border font-bold
|
className={`w-7 h-7 rounded-full border font-bold
|
||||||
flex items-center justify-center
|
flex items-center justify-center
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
UIEvent,
|
UIEvent,
|
||||||
} from "react";
|
} from "react";
|
||||||
import styles from "../css/SlidingGallery.module.css";
|
|
||||||
import { GalleryViews } from "@/types/gallery";
|
import { GalleryViews } from "@/types/gallery";
|
||||||
|
|
||||||
interface SlidingGalleryProps {
|
interface SlidingGalleryProps {
|
||||||
@ -120,8 +119,10 @@ const SlidingGallery = ({
|
|||||||
|
|
||||||
if (!views || views.length === 0) {
|
if (!views || views.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.gallery} ${className}`}>
|
<div
|
||||||
<div className={styles.emptyState}>
|
className={`relative w-full h-screen overflow-hidden flex flex-col ${className}`}
|
||||||
|
>
|
||||||
|
<div className="flex-1 flex items-center justify-center text-slate-400 text-lg">
|
||||||
<p>No content to display</p>
|
<p>No content to display</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -130,12 +131,12 @@ const SlidingGallery = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${styles.gallery} ${className}`}
|
className={`relative w-full h-screen overflow-hidden flex flex-col ${className}`}
|
||||||
ref={galleryRef}
|
ref={galleryRef}
|
||||||
style={{ height }}
|
style={{ height }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={styles.scrollContainer}
|
className="flex-1 flex overflow-x-auto"
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
style={{
|
style={{
|
||||||
@ -143,16 +144,20 @@ const SlidingGallery = ({
|
|||||||
height: "100%",
|
height: "100%",
|
||||||
overflowX: "scroll",
|
overflowX: "scroll",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
scrollSnapType: "x mandatory",
|
||||||
|
scrollbarWidth: "none",
|
||||||
|
msOverflowStyle: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{views.map((item) => (
|
{views.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className={styles.slide}
|
className="min-w-full flex items-center justify-center px-2 box-border"
|
||||||
style={{
|
style={{
|
||||||
width: dimensions.width,
|
width: dimensions.width,
|
||||||
height: "100%",
|
height: "100%",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
scrollSnapAlign: "start",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.content}
|
{item.content}
|
||||||
@ -161,15 +166,14 @@ const SlidingGallery = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showPagination && views.length > 1 && (
|
{showPagination && views.length > 1 && (
|
||||||
<div className={styles.pagination}>
|
<div className="absolute bottom-[15px] left-1/2 -translate-x-1/2 flex gap-1.5 z-10">
|
||||||
{views.map((_, index) => (
|
{views.map((_, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`${styles.dot} ${
|
className={`w-2 h-2 rounded-full transition-all duration-300 ease-in ${
|
||||||
activeIdx === index ? styles.activeDot : styles.inactiveDot
|
activeIdx === index ? "bg-[#113768]" : "bg-[#b1d3ff]"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleDotClick(index)}
|
onClick={() => handleDotClick(index)}
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,167 +0,0 @@
|
|||||||
/* SlidingGallery.module.css */
|
|
||||||
|
|
||||||
.gallery {
|
|
||||||
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 {
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
padding: 30px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textView {
|
|
||||||
padding-right: 0;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.gallery {
|
|
||||||
height: 60vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.facebook {
|
|
||||||
padding: 20px 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -104,9 +104,37 @@ export const getLinkedViews = (): GalleryViews[] => [
|
|||||||
content: (
|
content: (
|
||||||
<Link
|
<Link
|
||||||
href="https://www.facebook.com/share/g/15jdqESvWV/?mibextid=wwXIfr"
|
href="https://www.facebook.com/share/g/15jdqESvWV/?mibextid=wwXIfr"
|
||||||
className="w-full h-full block text-inherit box-border"
|
className=" block"
|
||||||
>
|
>
|
||||||
<div className="w-full h-full p-6 flex text-black bg-blue-50 rounded-4xl border-[0.5px] border-[#113768]/30">
|
<div className="w-full h-full p-6 flex text-black bg-blue-50 rounded-3xl border-[0.5px] border-[#113768]/30">
|
||||||
|
<div className="">
|
||||||
|
<h3 className="text-2xl text-[#113768] font-black">
|
||||||
|
Meet, Share, and Learn!
|
||||||
|
</h3>
|
||||||
|
<p className="font-bold text-sm text-[#113768] ">
|
||||||
|
Join Facebook Community
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center items-center shrink-0">
|
||||||
|
<Image
|
||||||
|
src="/images/static/facebook-logo.png"
|
||||||
|
alt="Facebook Logo"
|
||||||
|
width={150}
|
||||||
|
height={150}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
content: (
|
||||||
|
<Link
|
||||||
|
href="https://www.facebook.com/share/g/15jdqESvWV/?mibextid=wwXIfr"
|
||||||
|
className="lock "
|
||||||
|
>
|
||||||
|
<div className="w-full h-full p-6 flex text-black bg-blue-50 rounded-3xl border-[0.5px] border-[#113768]/30">
|
||||||
<div className="">
|
<div className="">
|
||||||
<h3 className="text-2xl text-[#113768] font-black">
|
<h3 className="text-2xl text-[#113768] font-black">
|
||||||
Meet, Share, and Learn!
|
Meet, Share, and Learn!
|
||||||
|
|||||||
13
package-lock.json
generated
13
package-lock.json
generated
@ -14,6 +14,7 @@
|
|||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"capacitor-secure-storage-plugin": "^0.11.0",
|
"capacitor-secure-storage-plugin": "^0.11.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.523.0",
|
"lucide-react": "^0.523.0",
|
||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
@ -3081,6 +3082,18 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/class-variance-authority": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://polar.sh/cva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/client-only": {
|
"node_modules/client-only": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"capacitor-secure-storage-plugin": "^0.11.0",
|
"capacitor-secure-storage-plugin": "^0.11.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.523.0",
|
"lucide-react": "^0.523.0",
|
||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user