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 { login } from "@/lib/auth";
|
||||
import DestructibleAlert from "@/components/DestructibleAlert";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { LoginForm } from "@/types/auth";
|
||||
import { CircleAlert } from "lucide-react";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
|
||||
@ -6,7 +6,6 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { register } from "@/lib/auth";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||
import FormField from "@/components/FormField";
|
||||
import DestructibleAlert from "@/components/DestructibleAlert";
|
||||
|
||||
@ -2,16 +2,8 @@
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||
import {
|
||||
Bookmark,
|
||||
BookmarkCheck,
|
||||
Check,
|
||||
ListFilter,
|
||||
MoveLeft,
|
||||
OctagonX,
|
||||
} from "lucide-react";
|
||||
import { Bookmark, BookmarkCheck, ListFilter, MoveLeft } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import DestructibleAlert from "@/components/DestructibleAlert";
|
||||
|
||||
interface Question {
|
||||
id: number;
|
||||
|
||||
@ -7,7 +7,6 @@ import DestructibleAlert from "@/components/DestructibleAlert";
|
||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||
import { API_URL, getToken } from "@/lib/auth";
|
||||
import { Loader, RefreshCw } from "lucide-react";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
|
||||
type Mock = {
|
||||
@ -74,7 +73,7 @@ export default function MockScreen() {
|
||||
<Header displayTabTitle="Mocks" />
|
||||
<div className="overflow-y-auto">
|
||||
<div className="mt-5 px-5">
|
||||
<DestructibleAlert text={errorMsg} extraStyles="" />
|
||||
<DestructibleAlert text={errorMsg} />
|
||||
</div>
|
||||
<div className="flex justify-center mt-4">
|
||||
<button
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import Header from "@/components/Header";
|
||||
@ -8,8 +7,6 @@ import DestructibleAlert from "@/components/DestructibleAlert";
|
||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||
import { API_URL, getToken } from "@/lib/auth";
|
||||
import { Loader, RefreshCw } from "lucide-react";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { Question } from "@/types/exam";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
|
||||
type Subject = {
|
||||
|
||||
@ -7,7 +7,6 @@ import DestructibleAlert from "@/components/DestructibleAlert";
|
||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||
import { API_URL, getToken } from "@/lib/auth";
|
||||
import { Loader, RefreshCw } from "lucide-react";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
|
||||
type Topic = {
|
||||
|
||||
@ -8,68 +8,41 @@ import SlidingGallery from "@/components/SlidingGallery";
|
||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||
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 LeaderboardEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
points: number;
|
||||
}
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
import DestructibleAlert from "@/components/DestructibleAlert";
|
||||
|
||||
const HomePage = () => {
|
||||
const router = useRouter();
|
||||
const [boardData, setBoardData] = useState<LeaderboardEntry[]>([]);
|
||||
const [boardError, setBoardError] = useState<string | null>(null);
|
||||
const [linkedViews, setLinkedViews] = useState<GalleryViews[]>();
|
||||
const { user } = useAuthStore();
|
||||
|
||||
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();
|
||||
setLinkedViews(fetchedLinkedViews);
|
||||
|
||||
// fetchBoardData();
|
||||
|
||||
// return () => {
|
||||
// isMounted = false;
|
||||
// };
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BackgroundWrapper>
|
||||
<div className={styles.container}>
|
||||
<div className="flex-1 min-h-screen">
|
||||
<Header displayUser />
|
||||
<div className={styles.scrollContainer}>
|
||||
<div className={styles.contentWrapper}>
|
||||
<div className="overflow-y-auto pt-4 h-[calc(100vh-80px)]">
|
||||
<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" />
|
||||
<div className={styles.mainContent}>
|
||||
<div className="flex flex-col gap-9">
|
||||
{/* Categories Section */}
|
||||
<div>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h2 className={styles.sectionTitle}>Categories</h2>
|
||||
<div className="flex item-scenter justify-between">
|
||||
<h2 className="text-2xl font-bold text-[#113768]">
|
||||
Categories
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => router.push("/categories")}
|
||||
className={styles.arrowButton}
|
||||
|
||||
@ -1,186 +1,9 @@
|
||||
"use client";
|
||||
|
||||
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";
|
||||
import { UserData } from "@/types/auth";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import React from "react";
|
||||
|
||||
const LeaderboardPage = () => {
|
||||
const [boardError, setBoardError] = useState<string | null>(null);
|
||||
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>
|
||||
);
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default LeaderboardPage;
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
import {
|
||||
Bookmark,
|
||||
|
||||
@ -1,33 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useCallback, useState } from "react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import Header from "@/components/Header";
|
||||
import QuestionItem from "@/components/QuestionItem";
|
||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||
import { useExamStore } from "@/stores/examStore";
|
||||
import { useTimerStore } from "@/stores/timerStore";
|
||||
import { useExamExitGuard } from "@/hooks/useExamExitGuard";
|
||||
|
||||
export default function ExamPage() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const test_id = searchParams.get("test_id") || "";
|
||||
const type = searchParams.get("type") || "";
|
||||
|
||||
const {
|
||||
setStatus,
|
||||
test,
|
||||
answers,
|
||||
startExam,
|
||||
setAnswer,
|
||||
submitExam,
|
||||
cancelExam,
|
||||
status,
|
||||
} = useExamStore();
|
||||
const { setStatus, test, answers, startExam, setAnswer, submitExam } =
|
||||
useExamStore();
|
||||
const { resetTimer, stopTimer } = useTimerStore();
|
||||
const { showExitDialog } = useExamExitGuard(type);
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
@ -126,7 +126,7 @@ function PretestPageContent() {
|
||||
setStatus("in-progress");
|
||||
|
||||
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 (
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { ExamProvider } from "@/context/ExamContext";
|
||||
import { TimerProvider } from "@/context/TimerContext";
|
||||
import { AuthProvider } from "@/context/AuthContext";
|
||||
import { ModalProvider } from "@/context/ModalContext";
|
||||
|
||||
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 = {
|
||||
appId: "com.examjam.solanine",
|
||||
appId: "com.examjam.omukk",
|
||||
appName: "ExamJam",
|
||||
webDir: "public",
|
||||
webDir: "out", // ✅ point to your Next.js static export
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@ -1,18 +1,38 @@
|
||||
import React, { JSX } from "react";
|
||||
|
||||
interface DestructibleAlertProps {
|
||||
variant?: "error" | "warning" | "alert";
|
||||
text: string;
|
||||
icon?: JSX.Element;
|
||||
}
|
||||
|
||||
const DestructibleAlert: React.FC<DestructibleAlertProps> = ({
|
||||
variant,
|
||||
text,
|
||||
icon,
|
||||
}) => {
|
||||
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>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,13 +2,10 @@
|
||||
|
||||
import React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ChevronLeft, Layers, Loader } from "lucide-react";
|
||||
import { useTimer } from "@/context/TimerContext";
|
||||
import { ChevronLeft, Layers } from "lucide-react";
|
||||
import styles from "@/css/Header.module.css";
|
||||
import { useExam } from "@/context/ExamContext";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { useModal } from "@/context/ModalContext";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
import { useTimerStore } from "@/stores/timerStore";
|
||||
import { useExamStore } from "@/stores/examStore";
|
||||
|
||||
@ -9,9 +9,9 @@ interface QuestionItemProps {
|
||||
index: number;
|
||||
selectedAnswer: Answer;
|
||||
onSelect: (answer: Answer) => void;
|
||||
userAnswer?: Answer; // new
|
||||
correctAnswer?: Answer; // new
|
||||
showResults?: boolean; // control whether to highlight or not
|
||||
userAnswer?: Answer;
|
||||
correctAnswer?: Answer;
|
||||
showResults?: boolean;
|
||||
}
|
||||
|
||||
const letters = ["A", "B", "C", "D"]; // extend if needed
|
||||
@ -40,60 +40,29 @@ const QuestionItem: React.FC<QuestionItemProps> = ({
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{question.options.map((opt, optIdx) => {
|
||||
const isSelected =
|
||||
question.type === "Single"
|
||||
? selectedAnswer === optIdx
|
||||
: Array.isArray(selectedAnswer) &&
|
||||
selectedAnswer.includes(optIdx);
|
||||
const isSelected = selectedAnswer === optIdx;
|
||||
|
||||
// ✅ logic for coloring after results
|
||||
// ✅ logic for coloring
|
||||
let btnClasses = "bg-gray-100 text-gray-900 border-gray-400";
|
||||
if (isSelected) {
|
||||
btnClasses = "bg-blue-600 text-white border-blue-600";
|
||||
}
|
||||
|
||||
if (showResults && correctAnswer !== undefined) {
|
||||
if (question.type === "Single") {
|
||||
if (userAnswer === optIdx && userAnswer !== correctAnswer) {
|
||||
btnClasses = "bg-red-500 text-white border-red-600"; // wrong
|
||||
}
|
||||
if (correctAnswer === optIdx) {
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={optIdx} className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (showResults) return; // disable changes in results mode
|
||||
if (question.type === "Single") {
|
||||
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);
|
||||
}
|
||||
if (showResults) return; // disable selection in results mode
|
||||
onSelect(optIdx); // always a number
|
||||
}}
|
||||
className={`w-7 h-7 rounded-full border font-bold
|
||||
flex items-center justify-center
|
||||
|
||||
@ -5,7 +5,6 @@ import React, {
|
||||
useCallback,
|
||||
UIEvent,
|
||||
} from "react";
|
||||
import styles from "../css/SlidingGallery.module.css";
|
||||
import { GalleryViews } from "@/types/gallery";
|
||||
|
||||
interface SlidingGalleryProps {
|
||||
@ -120,8 +119,10 @@ const SlidingGallery = ({
|
||||
|
||||
if (!views || views.length === 0) {
|
||||
return (
|
||||
<div className={`${styles.gallery} ${className}`}>
|
||||
<div className={styles.emptyState}>
|
||||
<div
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@ -130,12 +131,12 @@ const SlidingGallery = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.gallery} ${className}`}
|
||||
className={`relative w-full h-screen overflow-hidden flex flex-col ${className}`}
|
||||
ref={galleryRef}
|
||||
style={{ height }}
|
||||
>
|
||||
<div
|
||||
className={styles.scrollContainer}
|
||||
className="flex-1 flex overflow-x-auto"
|
||||
ref={scrollRef}
|
||||
onScroll={handleScroll}
|
||||
style={{
|
||||
@ -143,16 +144,20 @@ const SlidingGallery = ({
|
||||
height: "100%",
|
||||
overflowX: "scroll",
|
||||
display: "flex",
|
||||
scrollSnapType: "x mandatory",
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none",
|
||||
}}
|
||||
>
|
||||
{views.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={styles.slide}
|
||||
className="min-w-full flex items-center justify-center px-2 box-border"
|
||||
style={{
|
||||
width: dimensions.width,
|
||||
height: "100%",
|
||||
flexShrink: 0,
|
||||
scrollSnapAlign: "start",
|
||||
}}
|
||||
>
|
||||
{item.content}
|
||||
@ -161,15 +166,14 @@ const SlidingGallery = ({
|
||||
</div>
|
||||
|
||||
{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) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`${styles.dot} ${
|
||||
activeIdx === index ? styles.activeDot : styles.inactiveDot
|
||||
className={`w-2 h-2 rounded-full transition-all duration-300 ease-in ${
|
||||
activeIdx === index ? "bg-[#113768]" : "bg-[#b1d3ff]"
|
||||
}`}
|
||||
onClick={() => handleDotClick(index)}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
))}
|
||||
</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: (
|
||||
<Link
|
||||
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="">
|
||||
<h3 className="text-2xl text-[#113768] font-black">
|
||||
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-slot": "^1.2.3",
|
||||
"capacitor-secure-storage-plugin": "^0.11.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.523.0",
|
||||
"next": "15.3.2",
|
||||
@ -3081,6 +3082,18 @@
|
||||
"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": {
|
||||
"version": "0.0.1",
|
||||
"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-slot": "^1.2.3",
|
||||
"capacitor-secure-storage-plugin": "^0.11.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.523.0",
|
||||
"next": "15.3.2",
|
||||
|
||||
Reference in New Issue
Block a user