Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,293 changes: 1,207 additions & 86 deletions package-lock.json

Large diffs are not rendered by default.

77 changes: 69 additions & 8 deletions src/components/CreateWalletModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState, useCallback } from "react";
import { X, CheckCircle, AlertCircle, Wallet, ShieldCheck } from "lucide-react";
import { useWallet } from "@/providers/WalletProvider";

Expand All @@ -11,12 +11,16 @@ interface Props {
onClose: () => void;
}

const FOCUSABLE_SELECTORS = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';

export default function CreateWalletModal({ open, onClose }: Props) {
const { connect, address, walletType, error: walletError } = useWallet();
const [state, setState] = useState<ModalState>("idle");
const [method, setMethod] = useState<string | null>(null);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const backdropRef = useRef<HTMLDivElement | null>(null);
const modalRef = useRef<HTMLDivElement>(null);
const previousActiveElement = useRef<Element | null>(null);

useEffect(() => {
if (open) {
Expand All @@ -26,13 +30,62 @@ export default function CreateWalletModal({ open, onClose }: Props) {
}
}, [open]);

// Focus trap handler
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.key === "Escape") {
onClose();
return;
}

if (event.key === "Tab" && modalRef.current) {
const focusableElements = modalRef.current.querySelectorAll(FOCUSABLE_SELECTORS);
const firstElement = focusableElements[0] as HTMLElement | undefined;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement | undefined;

if (event.shiftKey) {
if (document.activeElement === firstElement) {
event.preventDefault();
lastElement?.focus();
}
} else {
if (document.activeElement === lastElement) {
event.preventDefault();
firstElement?.focus();
}
}
}
},
[onClose]
);

// Manage focus and event listeners when modal opens/closes
useEffect(() => {
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") onClose();
if (open) {
previousActiveElement.current = document.activeElement;
document.body.style.overflow = "hidden";
window.addEventListener("keydown", handleKeyDown);

// Focus first focusable element
setTimeout(() => {
const focusableElements = modalRef.current?.querySelectorAll(FOCUSABLE_SELECTORS);
if (focusableElements && focusableElements.length > 0) {
(focusableElements[0] as HTMLElement).focus();
}
}, 0);
} else {
document.body.style.overflow = "unset";
window.removeEventListener("keydown", handleKeyDown);
if (previousActiveElement.current instanceof HTMLElement) {
previousActiveElement.current.focus();
}
}
if (open) window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open, onClose]);

return () => {
document.body.style.overflow = "unset";
window.removeEventListener("keydown", handleKeyDown);
};
}, [open, handleKeyDown]);

if (!open) return null;

Expand All @@ -56,18 +109,26 @@ export default function CreateWalletModal({ open, onClose }: Props) {
}
}

const modalTitleId = "connect-wallet-modal-title";

return (
<div
ref={backdropRef}
onMouseDown={clickOutside}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"
>
<div className="relative w-full max-w-md bg-card border border-border/80 rounded-2xl p-6 shadow-2xl space-y-5">
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby={modalTitleId}
className="relative w-full max-w-md bg-card border border-border/80 rounded-2xl p-6 shadow-2xl space-y-5"
>
{/* Header */}
<div className="flex items-center justify-between pb-2 border-b border-border/40">
<div className="flex items-center gap-2">
<Wallet className="w-5 h-5 text-purple-400" />
<h3 className="text-lg font-bold text-foreground">Connect Wallet</h3>
<h3 id={modalTitleId} className="text-lg font-bold text-foreground">Connect Wallet</h3>
</div>
<button
onClick={onClose}
Expand Down
2 changes: 2 additions & 0 deletions src/components/dashboard/AvailabilityToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export default function AvailabilityToggle({
isAvailable ? "bg-green-500/30" : "bg-slate-700/30"
}`}
aria-label="Toggle availability"
aria-pressed={isAvailable}
role="switch"
>
<span
className={`inline-block h-6 w-6 transform rounded-full bg-white transition-transform ${
Expand Down
11 changes: 9 additions & 2 deletions src/components/dashboard/DashboardTopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,22 @@ export default function DashboardTopBar({ onToggleMenu }: { onToggleMenu?: () =>
const pathname = usePathname()

const isProfilePage = pathname === "/dashboard/profile"
const [isMenuOpen, setIsMenuOpen] = React.useState(false)

const handleToggleMenu = () => {
setIsMenuOpen(!isMenuOpen)
onToggleMenu?.()
}

return (
<header className="w-full bg-background border-b border-white/5 px-4 md:px-8 py-3 flex items-center justify-between sticky top-0 z-50">
{/* Left: Hamburger, Logo and Breadcrumbs */}
<div className="flex justify-between items-center md:min-w-[380px] gap-4 md:gap-12">
<button
onClick={onToggleMenu}
onClick={handleToggleMenu}
className="lg:hidden p-2 -ml-2 text-white/60 hover:text-white transition-colors"
aria-label="Toggle Menu"
aria-expanded={isMenuOpen}
>
<Menu className="w-6 h-6" />
</button>
Expand All @@ -41,7 +48,7 @@ export default function DashboardTopBar({ onToggleMenu }: { onToggleMenu?: () =>
{/* Right: Actions and User */}
<div className="flex items-center gap-5 md:min-w-[780px]">
{/* Notification Icon */}
<button className="p-2 text-white/40 hover:text-white transition-colors relative">
<button className="p-2 text-white/40 hover:text-white transition-colors relative" aria-label="Notifications">
<Bell className="w-5 h-5" />
<span className="absolute top-2.5 right-2.5 w-1.5 h-1.5 bg-purple-500 rounded-full border border-[#05010d]" />
</button>
Expand Down
80 changes: 70 additions & 10 deletions src/components/dashboard/DownloadModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React, { useEffect, useRef } from "react";
import React, { useEffect, useRef, useCallback } from "react";
import { X, Download, FileText, Clock, HardDrive } from "lucide-react";
import { Button } from "@/components/ui/Button";

Expand All @@ -12,6 +12,8 @@ interface Props {
fileName?: string;
}

const FOCUSABLE_SELECTORS = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';

export default function DownloadModal({
open,
onClose,
Expand All @@ -20,15 +22,65 @@ export default function DownloadModal({
fileName = "submission.pdf",
}: Props) {
const backdropRef = useRef<HTMLDivElement | null>(null);
const modalRef = useRef<HTMLDivElement>(null);
const previousActiveElement = useRef<Element | null>(null);

// Focus trap handler
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.key === "Escape") {
onClose();
return;
}

if (event.key === "Tab" && modalRef.current) {
const focusableElements = modalRef.current.querySelectorAll(FOCUSABLE_SELECTORS);
const firstElement = focusableElements[0] as HTMLElement | undefined;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement | undefined;

if (event.shiftKey) {
if (document.activeElement === firstElement) {
event.preventDefault();
lastElement?.focus();
}
} else {
if (document.activeElement === lastElement) {
event.preventDefault();
firstElement?.focus();
}
}
}
},
[onClose]
);

// Escape to close
// Manage focus and event listeners when modal opens/closes
useEffect(() => {
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") onClose();
if (open) {
previousActiveElement.current = document.activeElement;
document.body.style.overflow = "hidden";
window.addEventListener("keydown", handleKeyDown);

// Focus first focusable element
setTimeout(() => {
const focusableElements = modalRef.current?.querySelectorAll(FOCUSABLE_SELECTORS);
if (focusableElements && focusableElements.length > 0) {
(focusableElements[0] as HTMLElement).focus();
}
}, 0);
} else {
document.body.style.overflow = "unset";
window.removeEventListener("keydown", handleKeyDown);
if (previousActiveElement.current instanceof HTMLElement) {
previousActiveElement.current.focus();
}
}
if (open) window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open, onClose]);

return () => {
document.body.style.overflow = "unset";
window.removeEventListener("keydown", handleKeyDown);
};
}, [open, handleKeyDown]);

if (!open) return null;

Expand All @@ -42,25 +94,33 @@ export default function DownloadModal({
onClose();
}

const modalTitleId = "download-modal-title";

return (
<div
ref={backdropRef}
onMouseDown={clickOutside}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
>
<div className="relative w-[min(520px,94%)] bg-card border border-border rounded-xl p-6 space-y-6">
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby={modalTitleId}
className="relative w-[min(520px,94%)] bg-card border border-border rounded-xl p-6 space-y-6"
>
{/* Close Button */}
<button
onClick={onClose}
aria-label="close"
aria-label="Close modal"
className="absolute right-3 top-3 p-2 rounded-md hover:bg-accent transition-colors"
>
<X className="w-5 h-5 text-foreground" />
</button>

{/* Header */}
<div>
<h3 className="text-lg font-semibold text-foreground">Download Submission</h3>
<h3 id={modalTitleId} className="text-lg font-semibold text-foreground">Download Submission</h3>
<p className="text-sm text-foreground/60 mt-1">
Review and download the student submission
</p>
Expand Down
5 changes: 5 additions & 0 deletions src/components/dashboard/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ export default function Sidebar() {
<div className="md:hidden mb-4">
<button
onClick={() => setOpen(!open)}
aria-expanded={open}
aria-haspopup="menu"
aria-label="Toggle navigation menu"
className="w-full flex items-center justify-between px-4 py-3 rounded-xl bg-card text-foreground"
>
{activeItem}
Expand Down Expand Up @@ -140,6 +143,8 @@ export default function Sidebar() {
onClick={() => setDesktopProfileOpen((v) => !v)}
className="w-full flex items-center justify-between bg-card rounded-xl px-3 py-2"
aria-expanded={desktopProfileOpen}
aria-haspopup="menu"
aria-label="Profile selection menu"
>
<div className="flex items-center gap-2">
<img src={`/assets/${encodeURIComponent("Avatar Placeholder.png")}`} alt="avatar" className="w-8 h-8 rounded-full object-cover" />
Expand Down
53 changes: 31 additions & 22 deletions src/components/dashboard/SupportAccordionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,42 +22,51 @@ export const SupportAccordionItem: React.FC<SupportAccordionItemProps> = ({
isOpen,
onToggle,
}) => {
const contentId = `accordion-content-${title.replace(/\s+/g, '-').toLowerCase()}`

return (
<div className="border-b border-border">
<div
className={cn(
"flex items-center justify-between py-[12px] px-[24px] cursor-pointer transition-colors duration-200 group hover:bg-card",
isOpen && "bg-card/50"
)}
onClick={onToggle}
>
<div className="flex flex-col gap-[4px] flex-1 mr-4">
<div className="flex items-center gap-2">
<span className="text-[14px] font-medium text-[#CCCCCC] font-ubuntu leading-[17px]">
{title}
<span className="text-[#888888] ml-2 font-normal">({subtitle})</span>
</span>
</div>
</div>

<h3 className="m-0">
<button
className={cn(
"flex items-center justify-center px-[12px] py-[6px] rounded-[48px] border border-border min-w-[64px] h-[26px] transition-all duration-200 hover:border-[#444444]",
isOpen && "bg-white border-white"
"w-full flex items-center justify-between py-[12px] px-[24px] cursor-pointer transition-colors duration-200 group hover:bg-card text-left bg-transparent border-none",
isOpen && "bg-card/50"
)}
onClick={onToggle}
aria-expanded={isOpen}
aria-controls={contentId}
>
<div className="flex flex-col gap-[4px] flex-1 mr-4">
<div className="flex items-center gap-2">
<span className="text-[14px] font-medium text-[#CCCCCC] font-ubuntu leading-[17px]">
{title}
<span className="text-[#888888] ml-2 font-normal">({subtitle})</span>
</span>
</div>
</div>

<span
className={cn(
"text-[12px] font-normal leading-[14px]",
isOpen ? "text-black" : "text-foreground"
"inline-flex items-center justify-center px-[12px] py-[6px] rounded-[48px] border border-border min-w-[64px] h-[26px] transition-all duration-200",
isOpen && "bg-white border-white"
)}
>
{isOpen ? "Close" : "Expand"}
<span
className={cn(
"text-[12px] font-normal leading-[14px]",
isOpen ? "text-black" : "text-foreground"
)}
>
{isOpen ? "Close" : "Expand"}
</span>
</span>
</button>
</div>
</h3>

<div
id={contentId}
role="region"
aria-labelledby={contentId}
className={cn(
"grid transition-all duration-300 ease-in-out overflow-hidden",
isOpen ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"
Expand Down
Loading
Loading