feat(ui): add modal functionality, leaderboard view

This commit is contained in:
shafin-r
2025-07-10 19:28:50 +06:00
parent 64fc4d9a9a
commit 71eeafdaee
9 changed files with 358 additions and 83 deletions

91
components/ExamModal.tsx Normal file
View File

@ -0,0 +1,91 @@
"use client";
import { useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { X } from "lucide-react";
interface ModalProps {
/** Control visibility */
open: boolean;
/** Callback for both explicit and implicit close actions */
onClose: () => void;
/** Optional heading */
title?: string;
children: React.ReactNode;
/** Center horizontally and vertically? (default true) */
center?: boolean;
}
export default function Modal({
open,
onClose,
title,
children,
center = true,
}: ModalProps) {
const dialogRef = useRef<HTMLDialogElement | null>(null);
// Open / close imperatively to keep <dialog> in sync with prop
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (open && !dialog.open) dialog.showModal();
if (!open && dialog.open) dialog.close();
}, [open]);
// Close on native <dialog> close event
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
const handleClose = () => onClose();
dialog.addEventListener("close", handleClose);
return () => dialog.removeEventListener("close", handleClose);
}, [onClose]);
// ESC -> close (for browsers without builtin <dialog> handling)
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open, onClose]);
if (typeof window === "undefined") return null; // SSR guard
return createPortal(
<dialog
ref={dialogRef}
className={`fixed top-0 right-0 h-[110vh] z-50 w-[87vw]
ml-auto transform-none /* 1 & 2 */
backdrop:bg-black/20
transition-[opacity,transform] duration-300
${
open
? "opacity-100 translate-x-0 translate-y-0"
: "opacity-0 translate-x-4"
}
${center ? "rounded-l-xl" : ""}
`}
>
{/* Card */}
<div className="bg-white rounded-xl overflow-hidden pb-10">
{title && (
<header className="flex items-center justify-between px-6 pt-10 pb-4 dark:border-zinc-700">
<h2 className="text-2xl font-semibold">{title}</h2>
<button
aria-label="Close"
onClick={onClose}
className="p-1 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-full"
>
<X className="h-5 w-5" />
</button>
</header>
)}
<section className="px-6 ">{children}</section>
</div>
</dialog>,
document.body
);
}

View File

@ -6,6 +6,7 @@ import { useTimer } from "@/context/TimerContext";
import styles from "@/css/Header.module.css";
import { useExam } from "@/context/ExamContext";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { useModal } from "@/context/ModalContext";
const API_URL = "https://examjam-api.pptx704.com";
@ -28,6 +29,7 @@ const Header = ({
examDuration,
}) => {
const router = useRouter();
const { open } = useModal();
const { clearExam } = useExam();
const [totalSeconds, setTotalSeconds] = useState(
examDuration ? parseInt(examDuration) * 60 : 0
@ -147,7 +149,7 @@ const Header = ({
</span>
<span className={styles.timeLabel}>Mins</span>
</div>
<div className={styles.timeUnit}>
<div className={styles.timeUnit} style={{ borderRight: "none" }}>
<span className={styles.timeValue}>
{String(seconds).padStart(2, "0")}
</span>
@ -156,8 +158,7 @@ const Header = ({
</div>
<button
disabled
onClick={() => router.push("/exam/modal")}
onClick={open}
className={`${styles.iconButton} ${styles.disabled}`}
>
<Layers size={30} color="white" />