generated from muhtadeetaron/nextjs-template
feat(ui): add avatar and badge components
This commit is contained in:
@ -10,6 +10,7 @@ import BackgroundWrapper from "@/components/BackgroundWrapper";
|
|||||||
import DestructibleAlert from "@/components/DestructibleAlert";
|
import DestructibleAlert from "@/components/DestructibleAlert";
|
||||||
import { ChevronRight } from "lucide-react"; // Using Lucide React for icons
|
import { ChevronRight } from "lucide-react"; // Using Lucide React for icons
|
||||||
import styles from "@/css/Home.module.css";
|
import styles from "@/css/Home.module.css";
|
||||||
|
import facebookStyles from "@/css/SlidingGallery.module.css";
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api";
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api";
|
||||||
|
|
||||||
@ -30,6 +31,37 @@ const page = () => {
|
|||||||
{ label: "Chemistry", progress: 57 },
|
{ label: "Chemistry", progress: 57 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const facebookViews = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
content: (
|
||||||
|
<Link
|
||||||
|
href="https://www.facebook.com/share/g/15jdqESvWV/?mibextid=wwXIfr"
|
||||||
|
className="w-full h-full block text-inherit box-border"
|
||||||
|
>
|
||||||
|
<div className="w-full h-full p-6 flex text-black bg-blue-50 rounded-4xl 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={facebookStyles.logoView}>
|
||||||
|
<Image
|
||||||
|
src="/images/static/facebook-logo.png"
|
||||||
|
alt="Facebook Logo"
|
||||||
|
width={150}
|
||||||
|
height={150}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Fetch function for leaderboard data
|
// Fetch function for leaderboard data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
@ -70,7 +102,7 @@ const page = () => {
|
|||||||
<Header displayTabTitle={null} displayUser image={profileImg} />
|
<Header displayTabTitle={null} displayUser image={profileImg} />
|
||||||
<div className={styles.scrollContainer}>
|
<div className={styles.scrollContainer}>
|
||||||
<div className={styles.contentWrapper}>
|
<div className={styles.contentWrapper}>
|
||||||
<SlidingGallery />
|
<SlidingGallery views={facebookViews} height="23vh" />
|
||||||
<div className={styles.mainContent}>
|
<div className={styles.mainContent}>
|
||||||
{/* Categories Section */}
|
{/* Categories Section */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -2,15 +2,19 @@
|
|||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useExam, useExamResults } from "@/context/ExamContext";
|
import { useExam, useExamResults } from "@/context/ExamContext";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import Image from "next/image";
|
||||||
|
import SlidingGallery from "@/components/SlidingGallery";
|
||||||
|
|
||||||
interface Question {
|
interface Question {
|
||||||
solution: string;
|
correctAnswer: string;
|
||||||
id: number;
|
id: number;
|
||||||
question: string;
|
question: string;
|
||||||
options: Record<string, string>;
|
options: Record<string, string>;
|
||||||
|
solution?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QuestionItemProps {
|
interface QuestionItemProps {
|
||||||
@ -18,49 +22,78 @@ interface QuestionItemProps {
|
|||||||
selectedAnswer: string | undefined;
|
selectedAnswer: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QuestionItem = React.memo<QuestionItemProps>(
|
const QuestionItem = ({ question, selectedAnswer }: QuestionItemProps) => (
|
||||||
({ question, selectedAnswer }) => (
|
<div className="border border-[#8abdff]/50 rounded-2xl p-4 flex flex-col gap-7">
|
||||||
<div className="border border-[#8abdff]/50 rounded-2xl p-4 flex flex-col gap-7">
|
<h3 className="text-xl font-medium">
|
||||||
<h3 className="text-xl font-medium">
|
{question.id}. {question.question}
|
||||||
{question.id}. {question.question}
|
</h3>
|
||||||
</h3>
|
|
||||||
<div className="flex flex-col gap-4 items-start">
|
<div className="flex justify-between items-center">
|
||||||
{Object.entries(question.options).map(([key, value]) => (
|
<div></div>
|
||||||
<button key={key} className="flex items-center gap-3">
|
|
||||||
<span
|
{selectedAnswer?.answer === question.correctAnswer ? (
|
||||||
className={`flex items-center rounded-full border px-1.5 ${
|
<Badge className="bg-green-500 text-white" variant="default">
|
||||||
selectedAnswer === key
|
Correct
|
||||||
? "text-white bg-[#113768] border-[#113768]"
|
</Badge>
|
||||||
: ""
|
) : selectedAnswer?.answer !== question.correctAnswer ? (
|
||||||
}`}
|
<Badge className="bg-red-500 text-white" variant="default">
|
||||||
>
|
Incorrect
|
||||||
{key.toUpperCase()}
|
</Badge>
|
||||||
</span>
|
) : (
|
||||||
<span className="option-description">{value}</span>
|
<Badge className="bg-yellow-500" variant="destructive">
|
||||||
</button>
|
Skipped
|
||||||
))}
|
</Badge>
|
||||||
</div>
|
)}
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<h3 className="text-xl font-bold text-black/40">Solution:</h3>
|
|
||||||
<p className="text-lg font-medium">{question.solution}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
|
<div className="flex flex-col gap-4 items-start">
|
||||||
|
{Object.entries(question.options).map(([key, value]) => {
|
||||||
|
const isCorrect = key === question.correctAnswer;
|
||||||
|
const isSelected = key === selectedAnswer?.answer;
|
||||||
|
|
||||||
|
let optionStyle =
|
||||||
|
"px-2 py-1 flex items-center rounded-full border font-medium text-sm";
|
||||||
|
|
||||||
|
if (isCorrect) {
|
||||||
|
optionStyle += " bg-green-600 text-white border-green-600";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSelected && !isCorrect) {
|
||||||
|
optionStyle += " bg-red-600 text-white border-red-600";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCorrect && !isSelected) {
|
||||||
|
optionStyle += " border-gray-300 text-gray-700";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={key} className="flex items-center gap-3">
|
||||||
|
<span className={optionStyle}>{key.toUpperCase()}</span>
|
||||||
|
<span className="option-description">{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h3 className="text-lg font-bold text-black/40">Solution:</h3>
|
||||||
|
<p className="text-lg">{question.solution}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function ResultsPage() {
|
export default function ResultsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { clearExam, isExamCompleted, getApiResponse } = useExam();
|
const { clearExam, isExamCompleted, getApiResponse } = useExam();
|
||||||
|
let examResults;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Redirect if no completed exam
|
|
||||||
if (!isExamCompleted()) {
|
if (!isExamCompleted()) {
|
||||||
router.push("/unit");
|
router.push("/unit");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}, [isExamCompleted, router]);
|
}, [isExamCompleted, router]);
|
||||||
|
|
||||||
let examResults;
|
|
||||||
try {
|
try {
|
||||||
examResults = useExamResults();
|
examResults = useExamResults();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -98,28 +131,106 @@ export default function ResultsPage() {
|
|||||||
)
|
)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
|
const resultViews = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
content: (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="bg-blue-50/60 border border-[#113678]/50 rounded-4xl h-[170px] flex flex-col items-center justify-center gap-4">
|
||||||
|
<div className="text-xl text-black ">
|
||||||
|
<span className="font-bold">Accuracy</span> Rate:
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Image
|
||||||
|
src="/images/icons/accuracy.png"
|
||||||
|
alt="accuracy"
|
||||||
|
width={60}
|
||||||
|
height={60}
|
||||||
|
/>
|
||||||
|
<h2 className="text-6xl font-bold text-[#113678]">
|
||||||
|
{(
|
||||||
|
(examResults.score / examResults.totalQuestions) *
|
||||||
|
100
|
||||||
|
).toFixed(1)}
|
||||||
|
%
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
content: (
|
||||||
|
<div className=" w-full">
|
||||||
|
<div className="bg-blue-50/60 border border-[#113678]/50 rounded-4xl h-[170px] flex flex-col items-center justify-center gap-3">
|
||||||
|
<div className="text-xl text-black ">
|
||||||
|
<span className="font-bold">Error</span> Rate:
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Image
|
||||||
|
src="/images/icons/error.png"
|
||||||
|
alt="accuracy"
|
||||||
|
width={60}
|
||||||
|
height={60}
|
||||||
|
/>
|
||||||
|
<h2 className="text-6xl font-bold text-[#113678]">
|
||||||
|
{(
|
||||||
|
((examResults.totalQuestions - examResults.score) /
|
||||||
|
examResults.totalQuestions) *
|
||||||
|
100
|
||||||
|
).toFixed(1)}
|
||||||
|
%
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
content: (
|
||||||
|
<div className="my-8 w-full">
|
||||||
|
<div className="bg-blue-50/60 border border-[#113678]/50 rounded-4xl h-[170px] flex flex-col items-center justify-center gap-4">
|
||||||
|
<div className="text-xl text-black">
|
||||||
|
<span className="font-bold">Attempt</span> Rate:
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Image
|
||||||
|
src="/images/icons/attempt.png"
|
||||||
|
alt="accuracy"
|
||||||
|
width={60}
|
||||||
|
height={60}
|
||||||
|
/>
|
||||||
|
<h2 className="text-6xl font-bold text-[#113678]">
|
||||||
|
{(
|
||||||
|
(examResults.answers.length / examResults.totalQuestions) *
|
||||||
|
100
|
||||||
|
).toFixed(1)}
|
||||||
|
%
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
<button className="p-10" onClick={() => router.push("/unit")}>
|
<button className="p-10" onClick={() => router.push("/unit")}>
|
||||||
<ArrowLeft size={30} color="black" />
|
<ArrowLeft size={30} color="black" />
|
||||||
</button>
|
</button>
|
||||||
<div className="bg-white rounded-lg shadow-lg px-10 pb-20">
|
<div className="bg-white rounded-lg shadow-lg px-10 pb-20">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 mb-2 text-center">
|
<h1 className="text-2xl font-bold text-gray-900 mb-4 text-center">
|
||||||
Keep up the good work!
|
Keep up the good work!
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Score Display */}
|
{/* Score Display */}
|
||||||
<div className="mb-8">
|
<SlidingGallery className="my-8" views={resultViews} height="170px" />
|
||||||
<div className="bg-blue-50/60 border border-[#113678]/50 rounded-4xl h-[150px] flex flex-col items-center justify-center">
|
|
||||||
<div className="text-xl text-black mb-2">Accuracy:</div>
|
|
||||||
<div className="text-5xl font-bold text-[#113678]">
|
|
||||||
{((examResults.score / examResults.totalQuestions) * 100).toFixed(
|
|
||||||
1
|
|
||||||
)}
|
|
||||||
%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{apiResponse && (
|
{apiResponse && (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
@ -131,10 +242,20 @@ export default function ResultsPage() {
|
|||||||
<QuestionItem
|
<QuestionItem
|
||||||
key={question.id}
|
key={question.id}
|
||||||
question={question}
|
question={question}
|
||||||
selectedAnswer={undefined}
|
selectedAnswer={examResults.answers?.[question.id]}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-4 items-center mb-6 text-sm text-gray-600">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 bg-green-600 rounded-full"></div>{" "}
|
||||||
|
Correct
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 bg-red-600 rounded-full"></div> Your
|
||||||
|
Answer (Incorrect)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { ChevronLeft, Layers } from "lucide-react";
|
|||||||
import { useTimer } from "@/context/TimerContext";
|
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 { useExam } from "@/context/ExamContext";
|
||||||
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
|
||||||
const API_URL = "https://examjam-api.pptx704.com";
|
const API_URL = "https://examjam-api.pptx704.com";
|
||||||
|
|
||||||
@ -101,15 +102,11 @@ const Header = ({
|
|||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
{displayUser && (
|
{displayUser && (
|
||||||
<div className={styles.profile}>
|
<div className={styles.profile}>
|
||||||
{image && (
|
<Avatar className="bg-gray-200 w-10 h-10">
|
||||||
<Image
|
<AvatarFallback className=" text-lg">
|
||||||
src={image}
|
{userData?.name ? userData.name.charAt(0).toUpperCase() : ""}
|
||||||
alt="Profile"
|
</AvatarFallback>
|
||||||
width={40}
|
</Avatar>
|
||||||
height={40}
|
|
||||||
className={styles.profileImg}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<span className={styles.text}>
|
<span className={styles.text}>
|
||||||
Hello {userData?.name ? userData.name.split(" ")[0] : ""}
|
Hello {userData?.name ? userData.name.split(" ")[0] : ""}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -3,115 +3,158 @@ import Link from "next/link";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import styles from "../css/SlidingGallery.module.css";
|
import styles from "../css/SlidingGallery.module.css";
|
||||||
|
|
||||||
const views = [
|
const SlidingGallery = ({
|
||||||
{
|
views = [],
|
||||||
id: "1",
|
className = "",
|
||||||
content: (
|
showPagination = true,
|
||||||
<Link
|
autoScroll = false,
|
||||||
href="https://www.facebook.com/share/g/15jdqESvWV/?mibextid=wwXIfr"
|
autoScrollInterval = 5000,
|
||||||
className={styles.link}
|
onSlideChange = () => {},
|
||||||
>
|
height = "100vh",
|
||||||
<div className={styles.facebook}>
|
}) => {
|
||||||
<div className={styles.textView}>
|
|
||||||
<h3 className={styles.facebookOne}>Meet, Share, and Learn!</h3>
|
|
||||||
<p className={styles.facebookTwo}>Join Facebook Community</p>
|
|
||||||
</div>
|
|
||||||
<div className={styles.logoView}>
|
|
||||||
<Image
|
|
||||||
src="/images/static/facebook-logo.png"
|
|
||||||
alt="Facebook Logo"
|
|
||||||
width={120}
|
|
||||||
height={120}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
content: (
|
|
||||||
<Link
|
|
||||||
href="https://www.facebook.com/share/g/15jdqESvWV/?mibextid=wwXIfr"
|
|
||||||
className={styles.link}
|
|
||||||
>
|
|
||||||
<div className={styles.facebook}>
|
|
||||||
<div className={styles.textView}>
|
|
||||||
<h3 className={styles.facebookOne}>Meet, Share, and Learn!</h3>
|
|
||||||
<p className={styles.facebookTwo}>Join Facebook Community</p>
|
|
||||||
</div>
|
|
||||||
<div className={styles.logoView}>
|
|
||||||
<Image
|
|
||||||
src="/images/static/facebook-logo.png"
|
|
||||||
alt="Facebook Logo"
|
|
||||||
width={120}
|
|
||||||
height={120}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
content: (
|
|
||||||
<Link
|
|
||||||
href="https://www.facebook.com/share/g/15jdqESvWV/?mibextid=wwXIfr"
|
|
||||||
className={styles.link}
|
|
||||||
>
|
|
||||||
<div className={styles.facebook}>
|
|
||||||
<div className={styles.textView}>
|
|
||||||
<h3 className={styles.facebookOne}>Meet, Share, and Learn!</h3>
|
|
||||||
<p className={styles.facebookTwo}>Join Facebook Community</p>
|
|
||||||
</div>
|
|
||||||
<div className={styles.logoView}>
|
|
||||||
<Image
|
|
||||||
src="/images/static/facebook-logo.png"
|
|
||||||
alt="Facebook Logo"
|
|
||||||
width={120}
|
|
||||||
height={120}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const SlidingGallery = () => {
|
|
||||||
const [activeIdx, setActiveIdx] = useState(0);
|
const [activeIdx, setActiveIdx] = useState(0);
|
||||||
|
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||||
const scrollRef = useRef(null);
|
const scrollRef = useRef(null);
|
||||||
|
const galleryRef = useRef(null);
|
||||||
|
const autoScrollRef = useRef(null);
|
||||||
|
|
||||||
const handleScroll = (event) => {
|
// Auto-scroll functionality
|
||||||
const scrollLeft = event.target.scrollLeft;
|
useEffect(() => {
|
||||||
const slideWidth = event.target.clientWidth;
|
if (autoScroll && views.length > 1) {
|
||||||
const index = Math.round(scrollLeft / slideWidth);
|
autoScrollRef.current = setInterval(() => {
|
||||||
setActiveIdx(index);
|
setActiveIdx((prevIdx) => {
|
||||||
|
const nextIdx = (prevIdx + 1) % views.length;
|
||||||
|
goToSlide(nextIdx);
|
||||||
|
return nextIdx;
|
||||||
|
});
|
||||||
|
}, autoScrollInterval);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (autoScrollRef.current) {
|
||||||
|
clearInterval(autoScrollRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [autoScroll, autoScrollInterval, views.length]);
|
||||||
|
|
||||||
|
// Clear auto-scroll on user interaction
|
||||||
|
const handleUserInteraction = () => {
|
||||||
|
if (autoScrollRef.current) {
|
||||||
|
clearInterval(autoScrollRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateDimensions = () => {
|
||||||
|
if (galleryRef.current) {
|
||||||
|
setDimensions({
|
||||||
|
width: galleryRef.current.clientWidth,
|
||||||
|
height: galleryRef.current.clientHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial dimension update
|
||||||
|
updateDimensions();
|
||||||
|
|
||||||
|
// Add resize listener
|
||||||
|
window.addEventListener("resize", updateDimensions);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => window.removeEventListener("resize", updateDimensions);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Recalculate active index when dimensions change
|
||||||
|
if (scrollRef.current && dimensions.width > 0) {
|
||||||
|
const scrollLeft = scrollRef.current.scrollLeft;
|
||||||
|
const slideWidth = dimensions.width;
|
||||||
|
const index = Math.round(scrollLeft / slideWidth);
|
||||||
|
setActiveIdx(index);
|
||||||
|
}
|
||||||
|
}, [dimensions]);
|
||||||
|
|
||||||
|
const handleScroll = (event) => {
|
||||||
|
handleUserInteraction();
|
||||||
|
const scrollLeft = event.target.scrollLeft;
|
||||||
|
const slideWidth = dimensions.width;
|
||||||
|
const index = Math.round(scrollLeft / slideWidth);
|
||||||
|
if (index !== activeIdx) {
|
||||||
|
setActiveIdx(index);
|
||||||
|
onSlideChange(index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToSlide = (index) => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTo({
|
||||||
|
left: index * dimensions.width,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDotClick = (index) => {
|
||||||
|
handleUserInteraction();
|
||||||
|
goToSlide(index);
|
||||||
|
setActiveIdx(index);
|
||||||
|
onSlideChange(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Early return if no views
|
||||||
|
if (!views || views.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.gallery} ${className}`}>
|
||||||
|
<div className={styles.emptyState}>
|
||||||
|
<p>No content to display</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.gallery}>
|
<div
|
||||||
|
className={`${styles.gallery} ${className}`}
|
||||||
|
ref={galleryRef}
|
||||||
|
style={{ height }}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={styles.scrollContainer}
|
className={styles.scrollContainer}
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{views.map((item) => (
|
{views.map((item) => (
|
||||||
<div key={item.id} className={styles.slide}>
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className={styles.slide}
|
||||||
|
style={{
|
||||||
|
width: dimensions.width,
|
||||||
|
height: "100%",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{item.content}
|
{item.content}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.pagination}>
|
{showPagination && views.length > 1 && (
|
||||||
{views.map((_, index) => (
|
<div className={styles.pagination}>
|
||||||
<div
|
{views.map((_, index) => (
|
||||||
key={index}
|
<div
|
||||||
className={`${styles.dot} ${
|
key={index}
|
||||||
activeIdx === index ? styles.activeDot : styles.inactiveDot
|
className={`${styles.dot} ${
|
||||||
}`}
|
activeIdx === index ? styles.activeDot : styles.inactiveDot
|
||||||
/>
|
}`}
|
||||||
))}
|
onClick={() => handleDotClick(index)}
|
||||||
</div>
|
style={{ cursor: "pointer" }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
53
components/ui/avatar.tsx
Normal file
53
components/ui/avatar.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Avatar({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
data-slot="avatar"
|
||||||
|
className={cn(
|
||||||
|
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarImage({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
data-slot="avatar-image"
|
||||||
|
className={cn("aspect-square size-full", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarFallback({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback }
|
||||||
46
components/ui/badge.tsx
Normal file
46
components/ui/badge.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "span"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
@ -1,120 +1,167 @@
|
|||||||
|
/* SlidingGallery.module.css */
|
||||||
|
|
||||||
.gallery {
|
.gallery {
|
||||||
height: 200px;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid #113768;
|
height: 100vh; /* Default height, can be overridden by props */
|
||||||
border-radius: 25px;
|
overflow: hidden;
|
||||||
position: relative;
|
display: flex;
|
||||||
overflow: hidden;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollContainer {
|
.emptyState {
|
||||||
display: flex;
|
flex: 1;
|
||||||
height: 100%;
|
display: flex;
|
||||||
overflow-x: auto;
|
align-items: center;
|
||||||
scroll-behavior: smooth;
|
justify-content: center;
|
||||||
scroll-snap-type: x mandatory;
|
color: #666;
|
||||||
scrollbar-width: none; /* Firefox */
|
font-size: 1.2rem;
|
||||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
}
|
||||||
}
|
|
||||||
|
.scrollContainer {
|
||||||
.scrollContainer::-webkit-scrollbar {
|
flex: 1;
|
||||||
display: none; /* WebKit */
|
display: flex;
|
||||||
}
|
overflow-x: auto;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
.slide {
|
scrollbar-width: none; /* Firefox */
|
||||||
min-width: 100%;
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
flex-shrink: 0;
|
}
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
.scrollContainer::-webkit-scrollbar {
|
||||||
align-items: center;
|
display: none; /* Chrome, Safari, Opera */
|
||||||
scroll-snap-align: start;
|
}
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
.slide {
|
||||||
|
min-width: 100%;
|
||||||
|
scroll-snap-align: start;
|
||||||
.link {
|
display: flex;
|
||||||
text-decoration: none;
|
align-items: center;
|
||||||
color: inherit;
|
justify-content: center;
|
||||||
width: 100%;
|
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 {
|
.facebook {
|
||||||
display: flex;
|
flex-direction: column;
|
||||||
justify-content: space-evenly;
|
text-align: center;
|
||||||
flex-direction: row;
|
padding: 30px 20px;
|
||||||
height: 100%;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 25px;
|
|
||||||
padding: 40px 40px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.facebookOne {
|
|
||||||
font-family: 'Montserrat', sans-serif;
|
|
||||||
font-weight: 900;
|
|
||||||
color: #113768;
|
|
||||||
font-size: 20px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.facebookTwo {
|
|
||||||
font-family: 'Montserrat', sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #113768;
|
|
||||||
font-size: 13px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 20px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activeDot {
|
|
||||||
background-color: #113768;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inactiveDot {
|
|
||||||
background-color: #ccc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.textView {
|
.textView {
|
||||||
width: 60%;
|
padding-right: 0;
|
||||||
display: flex;
|
margin-bottom: 20px;
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logoView {
|
.slide {
|
||||||
width: 40%;
|
padding: 15px;
|
||||||
display: flex;
|
}
|
||||||
justify-content: flex-end;
|
}
|
||||||
align-items: flex-end;
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.gallery {
|
||||||
|
height: 60vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive adjustments */
|
.facebook {
|
||||||
@media (max-width: 768px) {
|
padding: 20px 15px;
|
||||||
.slide {
|
}
|
||||||
min-width: calc(100% - 40px);
|
|
||||||
padding: 0 20px;
|
.slide {
|
||||||
}
|
padding: 10px;
|
||||||
|
}
|
||||||
.facebookOne {
|
}
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.facebookTwo {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -9,6 +9,8 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"capacitor-secure-storage-plugin": "^0.11.0",
|
"capacitor-secure-storage-plugin": "^0.11.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.523.0",
|
"lucide-react": "^0.523.0",
|
||||||
|
|||||||
BIN
public/images/icons/accuracy.png
Normal file
BIN
public/images/icons/accuracy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
public/images/icons/attempt.png
Normal file
BIN
public/images/icons/attempt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
public/images/icons/error.png
Normal file
BIN
public/images/icons/error.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Reference in New Issue
Block a user