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