sanbon / frontend /src /pages /StudentExams.jsx
Seth0330's picture
Update frontend/src/pages/StudentExams.jsx
b8236a8 verified
// frontend/src/pages/StudentExams.jsx
import React, { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import {
Menu,
Calendar,
Clock,
MapPin,
LayoutDashboard,
Users as UsersIcon,
FileText,
Trophy,
ShoppingBag,
DollarSign,
CheckCircle2,
AlertCircle,
ArrowRight,
CreditCard,
} from "lucide-react";
import { format } from "date-fns";
import api from "../api/client";
import UserMenu from "../components/UserMenu";
import dojoLogo from "../assets/dojo-logo.png";
export default function StudentExams() {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const stored = JSON.parse(localStorage.getItem("karateStudent") || "{}");
const studentName = stored.name || "Student";
const studentEmail = stored.email || "student1@example.com";
const [registrations, setRegistrations] = useState([]);
const [loading, setLoading] = useState(true);
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const [paymentStatus, setPaymentStatus] = useState(null);
// Check for payment status in URL params
useEffect(() => {
const payment = searchParams.get("payment");
if (payment === "success") {
setPaymentStatus("success");
setTimeout(() => {
setSearchParams({});
window.location.reload();
}, 2000);
} else if (payment === "cancelled") {
setPaymentStatus("cancelled");
setSearchParams({});
setTimeout(() => setPaymentStatus(null), 5000);
}
}, [searchParams, setSearchParams]);
useEffect(() => {
if (!studentEmail) return;
async function loadData() {
setLoading(true);
try {
const res = await api.get("/student/exam-registrations", {
params: { email: studentEmail },
});
setRegistrations(Array.isArray(res.data) ? res.data : []);
} catch (err) {
console.error("Error loading exam registrations:", err);
setRegistrations([]);
} finally {
setLoading(false);
}
}
loadData();
}, [studentEmail]);
const handlePayment = async (registration) => {
try {
// Create dynamic checkout session
const res = await api.post(`/exam/${registration.id}/checkout`);
if (res.data?.checkout_url) {
// Redirect to Stripe Checkout
window.location.href = res.data.checkout_url;
} else {
alert("Failed to create payment session. Please try again.");
}
} catch (err) {
console.error("Error creating checkout session:", err);
alert(err.response?.data?.detail || "Failed to create payment session. Please try again.");
}
};
function handleLogout() {
localStorage.removeItem("karateStudent");
navigate("/login");
}
const navItems = [
{ label: "Dashboard", to: "/student", icon: LayoutDashboard },
{ label: "Classes", to: "/student/classes", icon: UsersIcon },
{ label: "Exams", to: "/student/exams", icon: FileText },
{ label: "Competitions", to: "/student/competitions", icon: Trophy },
{ label: "Shop", to: "/student/products", icon: ShoppingBag },
];
const handleNavClick = (to) => {
navigate(to);
setMobileNavOpen(false);
};
const isActivePath = (to) => {
if (typeof window === "undefined") return false;
return window.location.pathname === to;
};
// Count pending exam invites (unpaid registrations)
const pendingExams = registrations.filter(
(r) => r.payment_status === "unpaid" || r.payment_status === "pending" || r.registration_status === "invited"
);
return (
<div className="min-h-screen bg-stone-50 flex flex-col">
{/* TOP HEADER */}
<header className="border-b border-stone-100 bg-white flex items-center justify-between px-4 sm:px-6 lg:px-10 py-3">
<div className="flex items-center gap-3">
<button
type="button"
className="inline-flex md:hidden items-center justify-center h-9 w-9 rounded-full border border-stone-200 text-stone-700 hover:bg-stone-50"
onClick={() => setMobileNavOpen(true)}
aria-label="Open navigation"
>
<Menu className="w-4 h-4" />
</button>
<img
src={dojoLogo}
alt="Dojo logo"
className="h-9 w-9 rounded-full border border-stone-200 bg-white object-cover"
/>
<div className="hidden sm:block">
<div className="text-sm font-semibold text-stone-900">Karate Dojo</div>
<div className="text-xs text-stone-500">Student Portal</div>
</div>
</div>
<UserMenu name={studentName} email={studentEmail} onLogout={handleLogout} />
</header>
{/* BODY: sidebar + main content */}
<div className="flex flex-1">
{/* DESKTOP SIDEBAR */}
<aside className="hidden md:flex w-64 flex-col bg-white">
<nav className="flex-1 px-3 pt-4 pb-2 space-y-1">
{navItems.map((item) => {
const Icon = item.icon;
const isActive = isActivePath(item.to);
const hasNotification = item.to === "/student/exams" && pendingExams.length > 0;
return (
<button
key={item.to}
type="button"
onClick={() => handleNavClick(item.to)}
className={`w-full flex items-center gap-3 rounded-xl px-3 py-2 text-sm text-left relative ${
isActive
? "bg-rose-50 text-rose-700 font-semibold"
: "text-stone-600 hover:bg-stone-50"
}`}
>
<span
className={`inline-flex h-8 w-8 items-center justify-center rounded-xl border ${
isActive
? "border-rose-100 bg-rose-50 text-rose-600"
: "border-stone-200 bg-white text-stone-500"
}`}
>
<Icon className="w-4 h-4" />
</span>
<span>{item.label}</span>
{hasNotification && (
<span className="absolute right-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center w-5 h-5 rounded-full bg-red-500 text-white text-xs font-bold">
{pendingExams.length}
</span>
)}
</button>
);
})}
</nav>
<div className="px-4 pb-5">
<div className="rounded-2xl border border-rose-100 bg-rose-50 px-3 py-2">
<div className="text-[11px] font-semibold text-rose-700">Student Mode</div>
<div className="text-[11px] text-rose-500">Limited access view</div>
</div>
</div>
</aside>
{/* MAIN CONTENT */}
<div className="flex-1 border-l border-stone-100">
<main className="px-4 sm:px-6 lg:px-10 py-6 sm:py-8 pb-10">
{/* Header */}
<div className="mb-8">
<h1 className="text-2xl sm:text-3xl font-bold text-stone-900">Exams</h1>
<p className="text-stone-600 mt-1">View and manage your exam registrations</p>
</div>
{/* Payment Status Messages */}
{paymentStatus === "success" && (
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg flex items-center gap-2">
<CheckCircle2 className="w-5 h-5 text-green-600" />
<p className="text-sm text-green-800">Payment successful! Your registration is being processed.</p>
</div>
)}
{paymentStatus === "cancelled" && (
<div className="mb-6 p-4 bg-amber-50 border border-amber-200 rounded-lg flex items-center gap-2">
<AlertCircle className="w-5 h-5 text-amber-600" />
<p className="text-sm text-amber-800">Payment was cancelled. You can try again when ready.</p>
</div>
)}
{/* Exams List */}
{loading ? (
<div className="bg-white border border-stone-200 rounded-2xl shadow-sm">
<div className="px-4 sm:px-6 py-12 text-center text-stone-500">
<p>Loading exams...</p>
</div>
</div>
) : registrations.length === 0 ? (
<div className="bg-white border border-stone-200 rounded-2xl shadow-sm">
<div className="px-4 sm:px-6 py-12 text-center text-stone-500">
<FileText className="w-12 h-12 mx-auto mb-3 opacity-30" />
<p>No exam registrations found</p>
</div>
</div>
) : (
<div className="space-y-4">
{registrations.map((reg) => {
const isPending = reg.payment_status === "unpaid" || reg.registration_status === "invited";
const isPaid = reg.payment_status === "paid";
const exam = reg.exam || {};
const batch = reg.batch || {};
return (
<div
key={reg.id}
className={`bg-white border-2 rounded-2xl shadow-sm overflow-hidden ${
isPending ? "border-amber-200 bg-gradient-to-br from-amber-50 to-white" : "border-stone-200"
}`}
>
<div className="p-5 sm:p-6">
<div className="flex flex-col sm:flex-row justify-between items-start gap-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<h3 className="text-xl font-bold text-stone-900">{exam.name || "Exam"}</h3>
{isPending && (
<span className="inline-flex items-center rounded-full bg-amber-100 text-amber-700 text-xs font-medium px-2 py-0.5">
Pending Payment
</span>
)}
{isPaid && (
<span className="inline-flex items-center rounded-full bg-green-100 text-green-700 text-xs font-medium px-2 py-0.5">
Registered
</span>
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4 text-sm text-stone-600">
{exam.exam_date && (
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
<span>{format(new Date(exam.exam_date), "MMM d, yyyy")}</span>
</div>
)}
{batch.time_from && batch.time_to && (
<div className="flex items-center gap-2">
<Clock className="w-4 h-4" />
<span>
{batch.batch_name} • {batch.time_from.substring(0, 5)} - {batch.time_to.substring(0, 5)}
</span>
</div>
)}
{exam.location && (
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4" />
<span>{exam.location}</span>
</div>
)}
<div className="flex items-center gap-2">
<DollarSign className="w-4 h-4" />
<span className="font-medium">${exam.exam_fee || 0}</span>
</div>
</div>
{exam.description && (
<div className="mb-4 p-3 bg-stone-50 rounded-lg text-sm text-stone-700">
{exam.description}
</div>
)}
<div className="text-sm text-stone-600">
<div className="mb-1">
<span className="font-medium">Student:</span> {reg.student_name}
</div>
<div>
<span className="font-medium">Target Level:</span>{" "}
<span className="text-blue-600 font-medium">{reg.target_level || "N/A"}</span>
</div>
{reg.current_level && (
<div>
<span className="font-medium">Current Level:</span> {reg.current_level}
</div>
)}
</div>
</div>
<div className="flex flex-col gap-2 w-full sm:w-auto">
{isPending && (
<button
type="button"
onClick={() => handlePayment(reg)}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-red-600 hover:bg-red-700 text-white text-sm font-medium px-6 py-2.5 shadow-sm"
>
<CreditCard className="w-4 h-4" />
Register and Pay
</button>
)}
{isPaid && (
<div className="inline-flex items-center gap-2 rounded-lg bg-green-50 text-green-700 text-sm font-medium px-6 py-2.5">
<CheckCircle2 className="w-4 h-4" />
Payment Complete
</div>
)}
</div>
</div>
</div>
</div>
);
})}
</div>
)}
</main>
</div>
</div>
{/* MOBILE DRAWER NAV */}
{mobileNavOpen && (
<>
<div
className="fixed inset-0 z-40 bg-black/20 md:hidden"
onClick={() => setMobileNavOpen(false)}
/>
<div className="fixed inset-y-0 left-0 z-50 w-72 max-w-full bg-white border-r border-stone-100 shadow-lg flex flex-col md:hidden">
<div className="px-5 pt-5 pb-4 flex items-center gap-3 border-b border-stone-100">
<img
src={dojoLogo}
alt="Dojo logo"
className="h-9 w-9 rounded-full border border-stone-200 bg-white object-cover"
/>
<div>
<div className="text-sm font-semibold text-stone-900">Karate Dojo</div>
<div className="text-xs text-stone-500">Student Portal</div>
</div>
</div>
<nav className="flex-1 px-3 pt-4 pb-2 space-y-1 overflow-y-auto">
{navItems.map((item) => {
const Icon = item.icon;
const isActive = isActivePath(item.to);
const hasNotification = item.to === "/student/exams" && pendingExams.length > 0;
return (
<button
key={item.to}
type="button"
onClick={() => handleNavClick(item.to)}
className={`w-full flex items-center gap-3 rounded-xl px-3 py-2 text-sm text-left relative ${
isActive
? "bg-rose-50 text-rose-700 font-semibold"
: "text-stone-600 hover:bg-stone-50"
}`}
>
<span
className={`inline-flex h-8 w-8 items-center justify-center rounded-xl border ${
isActive
? "border-rose-100 bg-rose-50 text-rose-600"
: "border-stone-200 bg-white text-stone-500"
}`}
>
<Icon className="w-4 h-4" />
</span>
<span>{item.label}</span>
{hasNotification && (
<span className="absolute right-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center w-5 h-5 rounded-full bg-red-500 text-white text-xs font-bold">
{pendingExams.length}
</span>
)}
</button>
);
})}
</nav>
<div className="px-4 pb-5">
<div className="rounded-2xl border border-rose-100 bg-rose-50 px-3 py-2">
<div className="text-[11px] font-semibold text-rose-700">Student Mode</div>
<div className="text-[11px] text-rose-500">Limited access view</div>
</div>
</div>
</div>
</>
)}
</div>
);
}