chore(capacitor): refactor codebase for capacitor entry

This commit is contained in:
shafin-r
2025-09-08 13:42:15 +06:00
parent 3b2488054c
commit 99d6c15e38
21 changed files with 123 additions and 491 deletions

View File

@ -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";

View File

@ -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";

View File

@ -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;

View File

@ -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

View File

@ -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 = {

View File

@ -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 = {

View File

@ -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}

View File

@ -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;

View File

@ -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,

View File

@ -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);

View File

@ -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 (

View File

@ -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 }) {

View File

@ -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;

View File

@ -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>
); );
}; };

View File

@ -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";

View File

@ -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

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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
View File

@ -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",

View File

@ -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",