keeai / frontend /src /pages /StudentDashboard.jsx
Seth0330's picture
Update frontend/src/pages/StudentDashboard.jsx
9d9a28b verified
// frontend/src/pages/StudentDashboard.jsx
import React, { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import {
Menu,
MessageCircle,
Calendar,
Users,
Clock,
LayoutDashboard,
Users as UsersIcon,
FileText,
Trophy,
ShoppingBag,
X,
CreditCard,
CheckCircle2,
ArrowRight,
Plus,
TrendingUp,
} from "lucide-react";
import { format, differenceInDays, differenceInYears } from "date-fns";
import api from "../api/client";
import UserMenu from "../components/UserMenu";
import StudentAIAssistant from "../components/student/StudentAIAssistant";
import InviteAcceptance from "./InviteAcceptance";
import PlanAcceptance from "./PlanAcceptance";
import dojoLogo from "../assets/dojo-logo.png";
export default function StudentDashboard() {
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 firstName = studentName.split(" ")[0];
const [plans, setPlans] = useState([]);
const [memberships, setMemberships] = useState([]);
const [pendingInvites, setPendingInvites] = useState([]);
const [requests, setRequests] = useState([]);
const [myStudents, setMyStudents] = useState([]);
const [enrollments, setEnrollments] = useState([]);
const [examRegistrations, setExamRegistrations] = useState([]);
const [loading, setLoading] = useState(true);
const [aiOpen, setAiOpen] = useState(false);
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const [selectedInvite, setSelectedInvite] = useState(null);
const [selectedPlan, setSelectedPlan] = useState(null); // For direct plan subscription
const [paymentStatus, setPaymentStatus] = useState(null);
const [showAddStudent, setShowAddStudent] = useState(false);
const [showManageMembership, setShowManageMembership] = useState(false);
const [showPauseConfirm, setShowPauseConfirm] = useState(false);
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
const [pauseMessage, setPauseMessage] = useState(null);
const [cancelMessage, setCancelMessage] = useState(null);
const [isPausing, setIsPausing] = useState(false);
const [isCancelling, setIsCancelling] = useState(false);
const [withdrawPauseMessage, setWithdrawPauseMessage] = useState(null);
const [withdrawCancelMessage, setWithdrawCancelMessage] = useState(null);
const [resumeMessage, setResumeMessage] = useState(null);
const [reactivateMessage, setReactivateMessage] = useState(null);
const [studentMessage, setStudentMessage] = useState(null);
const [studentForm, setStudentForm] = useState({
first_name: "",
last_name: "",
date_of_birth: "",
gender: "",
medical_notes: "",
});
// Check for payment status in URL params
useEffect(() => {
const payment = searchParams.get("payment");
const inviteToken = searchParams.get("invite");
if (payment === "success") {
setPaymentStatus("success");
setSelectedInvite(null);
setTimeout(() => {
setSearchParams({});
window.location.reload();
}, 2000);
} else if (payment === "cancelled") {
setPaymentStatus("cancelled");
setSearchParams({});
setTimeout(() => setPaymentStatus(null), 5000);
}
}, [searchParams, setSearchParams]);
// Auto-scroll when invite form container is opened (focus is handled inside InviteAcceptance)
useEffect(() => {
if (selectedInvite) {
// Scroll to container after a short delay to let React render
setTimeout(() => {
const container = document.getElementById('invite-acceptance-container');
if (container) {
container.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 200);
}
}, [selectedInvite]);
function handleLogout() {
localStorage.removeItem("karateStudent");
navigate("/login");
}
// Move loadData outside useEffect so it can be called from event handlers
const loadData = React.useCallback(async () => {
if (!studentEmail) return;
setLoading(true);
try {
// Available plans
const plansRes = await api.get("/student/plans");
setPlans(Array.isArray(plansRes.data) ? plansRes.data : []);
// Memberships for this student
const membershipsRes = await api.get("/student/memberships", {
params: { email: studentEmail },
});
setMemberships(Array.isArray(membershipsRes.data) ? membershipsRes.data : []);
// Pending invites for this student
try {
const invitesRes = await api.get("/student/invites", {
params: { email: studentEmail },
});
setPendingInvites(Array.isArray(invitesRes.data) ? invitesRes.data : []);
} catch (err) {
setPendingInvites([]);
}
// Get students for this membership
try {
const studentsRes = await api.get("/student/students", {
params: { email: studentEmail },
});
setMyStudents(Array.isArray(studentsRes.data) ? studentsRes.data : []);
} catch (err) {
setMyStudents([]);
}
// Get enrollments for these students
try {
const enrollmentsRes = await api.get("/student/enrollments", {
params: { email: studentEmail },
});
setEnrollments(Array.isArray(enrollmentsRes.data) ? enrollmentsRes.data : []);
} catch (err) {
setEnrollments([]);
}
// Get exam registrations
try {
const examRes = await api.get("/student/exam-registrations", {
params: { email: studentEmail },
});
setExamRegistrations(Array.isArray(examRes.data) ? examRes.data : []);
} catch (err) {
setExamRegistrations([]);
}
// Get membership requests
try {
const requestsRes = await api.get("/student/requests", {
params: { email: studentEmail },
});
setRequests(Array.isArray(requestsRes.data) ? requestsRes.data : []);
} catch (err) {
setRequests([]);
}
} finally {
setLoading(false);
}
}, [studentEmail]);
useEffect(() => {
loadData();
}, [loadData]);
// Get active membership
// Find active membership, paused membership, or cancelled membership that hasn't reached renewal date yet
const today = new Date();
today.setHours(0, 0, 0, 0);
const activeMembership = memberships.find((m) => {
// Skip history entries (cancellation/pause entries)
if (m.is_cancellation_entry || m.is_pause_entry) return false;
if (m.status === "active" || m.status === "paused") return true;
// If cancelled but renewal date hasn't passed, treat as active for access
if (m.status === "cancelled" && m.renewal_date) {
const renewalDate = new Date(m.renewal_date);
renewalDate.setHours(0, 0, 0, 0);
return renewalDate >= today;
}
return false;
}) || null;
const daysUntilRenewal = activeMembership?.renewal_date
? differenceInDays(new Date(activeMembership.renewal_date), new Date())
: null;
// Check for pending requests
const pendingPauseRequest = activeMembership ? requests.find(
(r) => r.membership_id === activeMembership.id && r.request_type === "pause" && r.status === "pending"
) : null;
const pendingCancelRequest = activeMembership ? requests.find(
(r) => r.membership_id === activeMembership.id && r.request_type === "cancel" && r.status === "pending"
) : null;
// Calculate resume date for paused memberships (paused_at + 30 days)
const resumeDate = activeMembership?.status === "paused" && activeMembership?.paused_at
? (() => {
const pausedDate = new Date(activeMembership.paused_at);
const resume = new Date(pausedDate);
resume.setDate(resume.getDate() + 30); // Add 30 days for resume date
return resume;
})()
: null;
// Check if pause has been used in the current membership period
// Pause can only be used once per period (until renewal date passes)
const hasPausedBefore = activeMembership?.paused_at && activeMembership?.renewal_date
? (() => {
const renewalDate = new Date(activeMembership.renewal_date);
renewalDate.setHours(0, 0, 0, 0);
// If paused_at exists and we're still before renewal date, pause was used in this period
return renewalDate >= today;
})()
: false;
// Check if reactivate is available (cancelled and renewal date hasn't passed)
const canReactivate = activeMembership?.status === "cancelled" && activeMembership?.renewal_date &&
new Date(activeMembership.renewal_date) >= today;
// Calculate active classes count (unique classes)
const activeClassesCount = new Set(enrollments.map((e) => e.class_id)).size;
// Count pending exam invites (unpaid registrations)
const pendingExamInvites = examRegistrations.filter(
(r) => r.payment_status === "unpaid" || r.payment_status === "pending" || r.registration_status === "invited"
);
// Total pending invites (membership + exam)
const totalPendingInvites = pendingInvites.length + pendingExamInvites.length;
// Get default plans (up to 2)
const defaultPlans = plans.filter((p) => p.is_default).slice(0, 2);
// Handle direct plan subscription - show form first
const handleSelectPlan = (plan) => {
setSelectedPlan(plan);
};
// Group enrollments by class
const enrollmentsByClass = enrollments.reduce((acc, e) => {
if (!acc[e.class_id]) {
acc[e.class_id] = {
class_id: e.class_id,
class_name: e.class_name,
students: [],
};
}
acc[e.class_id].students.push({
student_id: e.student_id,
student_name: e.student_name,
});
return acc;
}, {});
// Calculate age for students
const calculateAge = (dob) => {
if (!dob) return null;
try {
return differenceInYears(new Date(), new Date(dob));
} catch {
return null;
}
};
// Dummy exam progress (until we have real data)
const examProgress = myStudents.length > 0
? myStudents.map((student, idx) => ({
student: `${student.first_name} ${student.last_name}`,
currentLevel: idx === 0 ? 3 : 1,
levelName: idx === 0 ? "Yellow Belt" : "White Belt",
nextLevel: idx === 0 ? "Orange Belt" : "Yellow Belt",
progress: idx === 0 ? 75 : 40,
}))
: [
{
student: "Alex Johnson",
currentLevel: 3,
levelName: "Yellow Belt",
nextLevel: "Orange Belt",
progress: 75,
},
{
student: "Emma Johnson",
currentLevel: 1,
levelName: "White Belt",
nextLevel: "Yellow Belt",
progress: 40,
},
];
// Filter nav items based on active membership
// Only show Dashboard if no active membership, show all items if there's an active membership
const allNavItems = [
{ 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 navItems = activeMembership
? allNavItems
: allNavItems.filter(item => item.label === "Dashboard");
const handleNavClick = (to) => {
navigate(to);
setMobileNavOpen(false);
};
const isActivePath = (to) => {
if (typeof window === "undefined") return false;
return window.location.pathname === to;
};
const canAddMoreStudents =
activeMembership && myStudents.length < (activeMembership.max_students || 1);
const handleAddStudent = async (e) => {
e.preventDefault();
if (!activeMembership) return;
try {
await api.post("/admin/students", {
membership_id: activeMembership.id,
first_name: studentForm.first_name,
last_name: studentForm.last_name,
date_of_birth: studentForm.date_of_birth || null,
gender: studentForm.gender || null,
medical_notes: studentForm.medical_notes || null,
});
// Reload students
const studentsRes = await api.get("/student/students", {
params: { email: studentEmail },
});
setMyStudents(Array.isArray(studentsRes.data) ? studentsRes.data : []);
setStudentMessage({
type: "success",
text: "Student added successfully!"
});
setTimeout(() => {
setShowAddStudent(false);
setStudentForm({
first_name: "",
last_name: "",
date_of_birth: "",
gender: "",
medical_notes: "",
});
setStudentMessage(null);
}, 1500);
} catch (err) {
console.error("Failed to add student:", err);
setStudentMessage({
type: "error",
text: err.response?.data?.detail || "Failed to add student. Please try again."
});
}
};
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 object-cover"
/>
<div className="hidden sm:block">
<div className="text-sm font-semibold text-stone-900">Arun Martial Arts</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" && pendingExamInvites.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">
{pendingExamInvites.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-8 py-6 sm:py-8">
{/* Payment Status Messages */}
{paymentStatus === "success" && (
<div className="mb-6 rounded-lg border border-green-200 bg-green-50 px-4 py-3">
<div className="flex items-center gap-2">
<div className="w-5 h-5 rounded-full bg-green-600 flex items-center justify-center">
<span className="text-white text-xs font-bold"></span>
</div>
<div>
<p className="text-sm font-semibold text-green-900">Payment Successful!</p>
<p className="text-xs text-green-700">
Your membership has been activated. Refreshing your dashboard...
</p>
</div>
</div>
</div>
)}
{paymentStatus === "cancelled" && (
<div className="mb-6 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3">
<div className="flex items-center gap-2">
<div className="w-5 h-5 rounded-full bg-amber-600 flex items-center justify-center">
<span className="text-white text-xs font-bold">!</span>
</div>
<div>
<p className="text-sm font-semibold text-amber-900">Payment Cancelled</p>
<p className="text-xs text-amber-700">
Your payment was cancelled. You can try again anytime.
</p>
</div>
</div>
</div>
)}
{/* Header */}
<div className="mb-8">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-stone-900">
Welcome back, {firstName}
</h1>
<p className="text-stone-600 mt-1">Track your membership and training progress</p>
</div>
<button
type="button"
onClick={() => setAiOpen(true)}
className="inline-flex items-center gap-2 rounded-lg bg-red-600 hover:bg-red-700 text-white text-sm font-medium px-4 py-2 shadow-sm"
>
<MessageCircle className="w-4 h-4" />
Ask the Coach
</button>
</div>
</div>
{/* Plan Acceptance Form - Show when a plan is selected */}
{selectedPlan && (
<PlanAcceptance
plan={selectedPlan}
email={studentEmail}
onComplete={() => {
setSelectedPlan(null);
loadData();
}}
onCancel={() => setSelectedPlan(null)}
/>
)}
{/* Choose Your Perfect Plan Section - Show default plans only when no active membership and no selected plan */}
{!activeMembership && !selectedPlan && defaultPlans.length > 0 && (
<div className="mb-6 bg-gradient-to-br from-blue-50 to-white border-2 border-blue-200 rounded-2xl shadow-sm">
<div className="px-5 py-4 border-b border-blue-100">
<h2 className="text-2xl font-bold text-stone-900">Choose Your Perfect Plan</h2>
<p className="text-stone-600 mt-1">Start your karate journey today</p>
</div>
<div className="px-5 py-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{defaultPlans.map((plan) => (
<div
key={plan.id}
className="p-6 bg-white rounded-lg border-2 border-stone-200 hover:border-red-400 transition-all"
>
<div className="mb-4">
<h3 className="text-xl font-bold text-stone-900">{plan.name}</h3>
<div className="mt-2 flex items-baseline gap-1">
<span className="text-3xl font-bold text-red-600">${plan.price}</span>
<span className="text-stone-500">/ {plan.billing_period}</span>
</div>
</div>
<div className="space-y-2 mb-4">
<div className="flex items-center gap-2 text-sm text-stone-600">
<CheckCircle2 className="w-4 h-4 text-green-600" />
Up to {plan.max_students} student{plan.max_students > 1 ? "s" : ""}
</div>
{plan.description && (
<p className="text-sm text-stone-600">{plan.description}</p>
)}
</div>
<button
type="button"
onClick={() => handleSelectPlan(plan)}
className="w-full rounded-lg bg-red-600 hover:bg-red-700 text-white text-sm font-medium py-2.5"
>
Select Plan
</button>
</div>
))}
</div>
{/* Help Section */}
<div className="p-6 bg-gradient-to-r from-purple-50 to-pink-50 rounded-lg border border-purple-200">
<div className="flex flex-col md:flex-row items-center gap-4">
<div className="flex-1 text-center md:text-left">
<h4 className="font-semibold text-stone-900 mb-2">
Need help choosing a plan that suits you?
</h4>
<p className="text-sm text-stone-600">
Talk to us, and we provide a free consultation to start your journey
</p>
</div>
<button
type="button"
onClick={() => setAiOpen(true)}
className="inline-flex items-center gap-2 rounded-lg bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium px-4 py-2 whitespace-nowrap"
>
<MessageCircle className="w-4 h-4" />
Get Free Consultation
</button>
</div>
</div>
</div>
</div>
)}
{/* Current Membership Status Card */}
{activeMembership && (
<div className="bg-white border border-red-200 rounded-2xl shadow-sm mb-6 bg-gradient-to-br from-white to-red-50">
<div className="px-5 py-4 border-b border-stone-100">
<div className="flex flex-col sm:flex-row justify-between items-start gap-4">
<div>
<h2 className="text-lg font-semibold text-stone-900 flex items-center gap-2">
Your Membership
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
activeMembership.status === "active"
? "bg-green-50 text-green-700"
: activeMembership.status === "paused"
? "bg-blue-50 text-blue-700"
: activeMembership.status === "cancelled"
? "bg-amber-50 text-amber-700"
: "bg-stone-50 text-stone-700"
}`}>
{activeMembership.status === "cancelled"
? "Cancelled (Active until renewal)"
: activeMembership.status === "paused"
? "Paused"
: activeMembership.status}
</span>
</h2>
<p className="text-sm text-stone-500 mt-2">
{activeMembership.plan_name} • {activeMembership.billing_period}
</p>
</div>
<div className="flex flex-col sm:flex-row gap-2 items-start sm:items-center">
<div className="text-left sm:text-right">
<div className="text-sm text-stone-500">Member since</div>
<div className="font-semibold text-stone-900">
{activeMembership.start_date
? format(new Date(activeMembership.start_date), "MMMM yyyy")
: "-"}
</div>
</div>
<button
type="button"
onClick={() => setShowManageMembership(!showManageMembership)}
className="inline-flex items-center justify-center rounded-lg border border-red-300 px-3 py-1.5 text-sm text-red-700 hover:bg-red-50 whitespace-nowrap"
>
{showManageMembership ? "Close" : "Manage"}
</button>
</div>
</div>
</div>
<div className="px-5 py-4">
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4 mb-4">
{/* Renewal Date / Resume Date */}
<div className="flex items-center gap-3 p-4 bg-white rounded-lg border border-stone-200">
<div className="w-10 h-10 bg-red-100 rounded-full flex items-center justify-center">
<Calendar className="w-5 h-5 text-red-600" />
</div>
<div>
<div className="text-xs text-stone-500">
{activeMembership.status === "paused" ? "Resume Date" : "Renewal Date"}
</div>
<div className="font-semibold text-stone-900">
{activeMembership.status === "paused" && resumeDate
? format(resumeDate, "MMM d, yyyy")
: activeMembership.renewal_date
? format(new Date(activeMembership.renewal_date), "MMM d, yyyy")
: "-"}
</div>
</div>
</div>
{/* Days Remaining */}
<div className="flex items-center gap-3 p-4 bg-white rounded-lg border border-stone-200">
<div className="w-10 h-10 bg-amber-100 rounded-full flex items-center justify-center">
<Clock className="w-5 h-5 text-amber-600" />
</div>
<div>
<div className="text-xs text-stone-500">Days Remaining</div>
<div className="font-semibold text-stone-900">
{daysUntilRenewal !== null ? `${daysUntilRenewal} days` : "-"}
</div>
</div>
</div>
{/* Amount */}
<div className="flex items-center gap-3 p-4 bg-white rounded-lg border border-stone-200">
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<CreditCard className="w-5 h-5 text-green-600" />
</div>
<div>
<div className="text-xs text-stone-500">Amount</div>
<div className="font-semibold text-stone-900">
${Number(activeMembership.price).toFixed(2)}
</div>
</div>
</div>
{/* Students */}
<div className="flex items-center gap-3 p-4 bg-white rounded-lg border border-stone-200">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<Users className="w-5 h-5 text-blue-600" />
</div>
<div>
<div className="text-xs text-stone-500">Students</div>
<div className="font-semibold text-stone-900">
{myStudents.length} / {activeMembership.max_students || 1}
</div>
</div>
</div>
</div>
{/* Manage Membership Section */}
{showManageMembership && (
<div className="border-t pt-4 mt-4 space-y-4">
<h3 className="font-semibold text-stone-900">Manage Your Membership</h3>
{/* Pause Membership - Only show if not already paused and no pending request */}
{!hasPausedBefore && !pendingPauseRequest && activeMembership?.status !== "paused" && (
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
{!showPauseConfirm ? (
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<h4 className="font-semibold text-stone-900 mb-1">Pause Membership</h4>
<p className="text-sm text-stone-600 mb-3">
Need a break? Pause your membership for 1 month. Your renewal date will be pushed by 30 days.
</p>
<p className="text-xs text-stone-500">
Current renewal: {activeMembership.renewal_date
? format(new Date(activeMembership.renewal_date), "MMM d, yyyy")
: "-"}
</p>
</div>
<button
type="button"
onClick={() => {
setShowPauseConfirm(true);
setPauseMessage(null);
}}
className="inline-flex items-center justify-center rounded-lg border border-blue-600 px-3 py-1.5 text-sm text-blue-700 hover:bg-blue-100 whitespace-nowrap"
>
Pause for 1 Month
</button>
</div>
) : (
<div>
<h4 className="font-semibold text-blue-900 mb-2 flex items-center gap-2">
<span className="text-yellow-600">⚠️</span> Confirm Pause
</h4>
<div className="bg-white p-4 rounded border border-blue-300 mb-4">
<p className="text-sm text-stone-700 mb-2">Your membership will be paused for 1 month:</p>
<ul className="text-sm text-stone-600 space-y-1 list-disc list-inside">
<li>During this period you will not be able to participate in classes and activities</li>
<li>You can only pause once during your current active membership</li>
<li>You can continue your classes after the pause period is over</li>
<li>
Your renewal date will be pushed to{" "}
{activeMembership.renewal_date
? format(
new Date(
new Date(activeMembership.renewal_date).setDate(
new Date(activeMembership.renewal_date).getDate() + 30
)
),
"MMM d, yyyy"
)
: "-"}
</li>
</ul>
</div>
{pauseMessage && (
<div className={`mb-4 p-3 rounded-lg text-sm ${
pauseMessage.type === "success"
? "bg-green-50 text-green-900 border border-green-200"
: "bg-red-50 text-red-900 border border-red-200"
}`}>
{pauseMessage.text}
</div>
)}
<div className="flex gap-2">
<button
type="button"
onClick={async () => {
setIsPausing(true);
setPauseMessage(null);
try {
const response = await api.post(`/student/memberships/${activeMembership.id}/pause-request`);
setPauseMessage({
type: "success",
text: "Pause request submitted! Waiting for admin approval."
});
// Wait for user to read the message, then refresh data and close dialog
setTimeout(async () => {
await loadData();
setTimeout(() => {
setShowPauseConfirm(false);
setShowManageMembership(false);
setPauseMessage(null);
}, 500);
}, 4000);
} catch (err) {
setPauseMessage({
type: "error",
text: err.response?.data?.detail || "Failed to pause membership. Please try again."
});
} finally {
setIsPausing(false);
}
}}
disabled={isPausing}
className="inline-flex items-center justify-center rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium px-4 py-2 disabled:opacity-60"
>
{isPausing ? "Pausing..." : "Yes, Pause"}
</button>
<button
type="button"
onClick={() => {
setShowPauseConfirm(false);
setPauseMessage(null);
}}
className="inline-flex items-center justify-center rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-700 hover:bg-stone-50"
>
No, Don&apos;t Pause
</button>
</div>
</div>
)}
</div>
)}
{/* Cancel Membership - Only show if not cancelled and no pending request */}
{activeMembership?.status !== "cancelled" && !pendingCancelRequest && (
<div className="p-4 bg-red-50 rounded-lg border border-red-200">
{!showCancelConfirm ? (
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<h4 className="font-semibold text-stone-900 mb-1">Cancel Membership</h4>
<p className="text-sm text-stone-600">
Cancel your subscription and stop future auto-payments. You can continue using classes until your current period ends.
</p>
</div>
<button
type="button"
onClick={() => {
setShowCancelConfirm(true);
setCancelMessage(null);
}}
className="inline-flex items-center justify-center rounded-lg border border-red-600 px-3 py-1.5 text-sm text-red-700 hover:bg-red-100 whitespace-nowrap"
>
Cancel Subscription
</button>
</div>
) : (
<div>
<h4 className="font-semibold text-red-900 mb-2 flex items-center gap-2">
<span className="text-yellow-600">⚠️</span> Confirm Cancellation
</h4>
<div className="bg-white p-4 rounded border border-red-300 mb-4">
<p className="text-sm text-stone-700 mb-2">Are you sure you want to cancel your subscription?</p>
<ul className="text-sm text-stone-600 space-y-1 list-disc list-inside">
<li>Your next auto-payment will be stopped</li>
<li>
You can continue using classes until{" "}
{activeMembership.renewal_date
? format(new Date(activeMembership.renewal_date), "MMM d, yyyy")
: "current period ends"}
</li>
<li>You can reactivate your membership anytime</li>
</ul>
</div>
{cancelMessage && (
<div className={`mb-4 p-3 rounded-lg text-sm ${
cancelMessage.type === "success"
? "bg-green-50 text-green-900 border border-green-200"
: "bg-red-50 text-red-900 border border-red-200"
}`}>
{cancelMessage.text}
</div>
)}
<div className="flex gap-2">
<button
type="button"
onClick={async () => {
setIsCancelling(true);
setCancelMessage(null);
try {
const response = await api.post(`/student/memberships/${activeMembership.id}/cancel-request`);
// Check if response is successful (status 200-299)
if (response.status >= 200 && response.status < 300) {
setCancelMessage({
type: "success",
text: "Cancellation request submitted! Waiting for admin approval."
});
// Wait for user to read the message, then refresh data and close dialog
setTimeout(async () => {
await loadData();
setTimeout(() => {
setShowCancelConfirm(false);
setShowManageMembership(false);
setCancelMessage(null);
}, 500);
}, 4000);
} else {
setCancelMessage({
type: "error",
text: response.data?.detail || "Failed to cancel membership. Please try again."
});
}
} catch (err) {
// If error but status is 200, it might be a false error
if (err.response?.status >= 200 && err.response?.status < 300) {
setCancelMessage({
type: "success",
text: `Subscription cancelled! Auto-payment stopped. You can use classes until ${format(
new Date(activeMembership.renewal_date),
"MMM d, yyyy"
)}.`
});
// Wait for user to read the message, then refresh data and close dialog
setTimeout(async () => {
await loadData();
setTimeout(() => {
setShowCancelConfirm(false);
setShowManageMembership(false);
setCancelMessage(null);
}, 500);
}, 4000);
} else {
setCancelMessage({
type: "error",
text: err.response?.data?.detail || "Failed to submit cancellation request. Please try again."
});
}
} finally {
setIsCancelling(false);
}
}}
disabled={isCancelling}
className="inline-flex items-center justify-center rounded-lg bg-red-600 hover:bg-red-700 text-white text-sm font-medium px-4 py-2 disabled:opacity-60"
>
{isCancelling ? "Submitting..." : "Yes, Cancel Subscription"}
</button>
<button
type="button"
onClick={() => {
setShowCancelConfirm(false);
setCancelMessage(null);
}}
className="inline-flex items-center justify-center rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-700 hover:bg-stone-50"
>
No, Keep My Membership
</button>
</div>
</div>
)}
</div>
)}
{/* Pending Requests */}
{(pendingPauseRequest || pendingCancelRequest) && (
<div className="p-4 bg-amber-50 rounded-lg border border-amber-200">
<h4 className="font-semibold text-stone-900 mb-2">Pending Requests</h4>
{pendingPauseRequest && (
<div className="mb-2 p-3 bg-white rounded border border-amber-300">
<div className="flex items-center justify-between">
<div>
<span className="text-sm font-medium text-stone-900">Pause Request</span>
<p className="text-xs text-stone-600 mt-1">Waiting for admin approval</p>
</div>
<button
type="button"
onClick={async () => {
setWithdrawPauseMessage(null);
try {
await api.post(`/student/memberships/${activeMembership.id}/requests/${pendingPauseRequest.id}/withdraw`);
setWithdrawPauseMessage({
type: "success",
text: "Pause request withdrawn successfully."
});
// Wait for user to read the message, then refresh data
setTimeout(async () => {
await loadData();
setTimeout(() => {
setWithdrawPauseMessage(null);
}, 500);
}, 4000);
} catch (err) {
setWithdrawPauseMessage({
type: "error",
text: err.response?.data?.detail || "Failed to withdraw request. Please try again."
});
}
}}
className="inline-flex items-center justify-center rounded-lg border border-amber-300 px-3 py-1 text-sm text-amber-700 hover:bg-amber-100"
>
Withdraw
</button>
</div>
{withdrawPauseMessage && (
<div className={`mt-2 p-2 rounded-lg text-xs ${
withdrawPauseMessage.type === "success"
? "bg-green-50 text-green-800 border border-green-200"
: "bg-red-50 text-red-800 border border-red-200"
}`}>
{withdrawPauseMessage.text}
</div>
)}
</div>
)}
{pendingCancelRequest && (
<div className="p-3 bg-white rounded border border-amber-300">
<div className="flex items-center justify-between">
<div>
<span className="text-sm font-medium text-stone-900">Cancellation Request</span>
<p className="text-xs text-stone-600 mt-1">Waiting for admin approval</p>
</div>
<button
type="button"
onClick={async () => {
setWithdrawCancelMessage(null);
try {
await api.post(`/student/memberships/${activeMembership.id}/requests/${pendingCancelRequest.id}/withdraw`);
setWithdrawCancelMessage({
type: "success",
text: "Cancellation request withdrawn successfully."
});
// Wait for user to read the message, then refresh data
setTimeout(async () => {
await loadData();
setTimeout(() => {
setWithdrawCancelMessage(null);
}, 500);
}, 4000);
} catch (err) {
setWithdrawCancelMessage({
type: "error",
text: err.response?.data?.detail || "Failed to withdraw request. Please try again."
});
}
}}
className="inline-flex items-center justify-center rounded-lg border border-amber-300 px-3 py-1 text-sm text-amber-700 hover:bg-amber-100"
>
Withdraw
</button>
</div>
{withdrawCancelMessage && (
<div className={`mt-2 p-2 rounded-lg text-xs ${
withdrawCancelMessage.type === "success"
? "bg-green-50 text-green-800 border border-green-200"
: "bg-red-50 text-red-800 border border-red-200"
}`}>
{withdrawCancelMessage.text}
</div>
)}
</div>
)}
</div>
)}
{/* Resume Membership (for paused) */}
{activeMembership?.status === "paused" && (
<div className="p-4 bg-green-50 rounded-lg border border-green-200">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<h4 className="font-semibold text-stone-900 mb-1">Resume Membership</h4>
<p className="text-sm text-stone-600">
Resume your membership immediately. Your renewal date will be auto adjusted based on the number of days paused.
</p>
{resumeMessage && (
<div className={`mt-2 p-2 rounded-lg text-xs ${
resumeMessage.type === "success"
? "bg-green-50 text-green-800 border border-green-200"
: "bg-red-50 text-red-800 border border-red-200"
}`}>
{resumeMessage.text}
</div>
)}
</div>
<button
type="button"
onClick={async () => {
setResumeMessage(null);
try {
await api.post(`/student/memberships/${activeMembership.id}/resume`);
setResumeMessage({
type: "success",
text: "Membership resumed successfully!"
});
setTimeout(() => {
loadData();
setShowManageMembership(false);
setResumeMessage(null);
}, 1500);
} catch (err) {
setResumeMessage({
type: "error",
text: err.response?.data?.detail || "Failed to resume membership. Please try again."
});
}
}}
className="inline-flex items-center justify-center rounded-lg bg-green-600 hover:bg-green-700 text-white text-sm font-medium px-4 py-2 whitespace-nowrap"
>
Resume
</button>
</div>
</div>
)}
{/* Reactivate Membership (for cancelled) */}
{canReactivate && (
<div className="p-4 bg-green-50 rounded-lg border border-green-200">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<h4 className="font-semibold text-stone-900 mb-1">Reactivate Membership</h4>
<p className="text-sm text-stone-600">
Reactivate your membership. Payment required. Membership will start from {activeMembership.renewal_date
? format(new Date(activeMembership.renewal_date), "MMM d, yyyy")
: "-"}.
</p>
{reactivateMessage && (
<div className={`mt-2 p-2 rounded-lg text-xs ${
reactivateMessage.type === "error"
? "bg-red-50 text-red-800 border border-red-200"
: ""
}`}>
{reactivateMessage.text}
</div>
)}
</div>
<button
type="button"
onClick={async () => {
setReactivateMessage(null);
try {
const response = await api.post(`/student/memberships/${activeMembership.id}/reactivate`);
if (response.data?.checkout_url) {
window.location.href = response.data.checkout_url;
}
} catch (err) {
setReactivateMessage({
type: "error",
text: err.response?.data?.detail || "Failed to create reactivation payment. Please try again."
});
}
}}
className="inline-flex items-center justify-center rounded-lg bg-green-600 hover:bg-green-700 text-white text-sm font-medium px-4 py-2 whitespace-nowrap"
>
Reactivate
</button>
</div>
</div>
)}
</div>
)}
</div>
</div>
)}
{/* Pending Invitations - Show at top if there are any */}
{(pendingInvites.length > 0 || pendingExamInvites.length > 0) && (
<div className="bg-white border border-amber-200 rounded-2xl shadow-sm mb-6 bg-gradient-to-br from-amber-50 to-white">
<div className="px-5 py-4 border-b border-stone-100">
<h2 className="text-lg font-semibold text-stone-900 flex items-center gap-2">
<Clock className="w-5 h-5 text-amber-600" />
Pending Invitations
</h2>
<p className="text-sm text-stone-500 mt-1">
You have pending invitations - pay to activate
</p>
</div>
<div className="px-5 py-4">
<div className="space-y-4">
{/* Membership Invites */}
{pendingInvites.map((invite) => (
<div key={invite.id} className="p-4 bg-white rounded-lg border border-stone-200">
<div className="flex flex-col sm:flex-row justify-between items-start gap-4">
<div className="flex-1">
<div className="font-semibold text-lg text-stone-900">{invite.plan_name}</div>
<div className="text-sm text-stone-500 mt-1">
${invite.plan_price} • Up to {invite.max_students} student(s)
</div>
{invite.class_details && (
<div className="mt-3 p-3 bg-stone-50 rounded text-sm">
<div className="font-medium text-stone-700 mb-1">Classes Included:</div>
<p className="text-stone-600">{invite.class_details}</p>
</div>
)}
</div>
<button
type="button"
onClick={() => {
setSelectedInvite(invite);
}}
className="inline-flex items-center gap-2 rounded-lg bg-amber-600 hover:bg-amber-700 text-white text-sm font-medium px-4 py-2 w-full sm:w-auto"
>
Complete Invitation
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
))}
{/* Exam Invites */}
{pendingExamInvites.map((reg) => {
const exam = reg.exam || {};
const batch = reg.batch || {};
return (
<div key={reg.id} className="p-4 bg-white rounded-lg border border-stone-200">
<div className="flex flex-col sm:flex-row justify-between items-start gap-4">
<div className="flex-1">
<div className="font-semibold text-lg text-stone-900">{exam.name || "Exam"}</div>
<div className="text-sm text-stone-500 mt-1">
${exam.exam_fee || 0} • {batch.batch_name || "Batch"}
</div>
{exam.exam_date && (
<div className="text-sm text-stone-600 mt-1">
Exam Date: {format(new Date(exam.exam_date), "MMM d, yyyy")}
</div>
)}
</div>
<button
type="button"
onClick={() => navigate("/student/exams")}
className="inline-flex items-center gap-2 rounded-lg bg-amber-600 hover:bg-amber-700 text-white text-sm font-medium px-4 py-2 w-full sm:w-auto"
>
View Exam Details
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
);
})}
</div>
</div>
</div>
)}
{/* Invite Acceptance Inline */}
{selectedInvite && (
<div id="invite-acceptance-container" className="mb-6 bg-white border border-stone-200 rounded-2xl shadow-sm">
<div className="px-4 sm:px-6 py-4 border-b border-stone-100 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h2 className="text-base font-semibold text-stone-900">Complete Your Membership</h2>
<p className="text-sm text-stone-500">
Add student information and complete payment for {selectedInvite.plan_name}
</p>
</div>
<button
type="button"
onClick={() => setSelectedInvite(null)}
className="inline-flex items-center justify-center rounded-lg border border-stone-200 px-2.5 py-1.5 text-sm text-stone-600 hover:bg-stone-50"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="px-4 sm:px-6 py-4">
<InviteAcceptance
key={selectedInvite.id}
invite={selectedInvite}
onComplete={() => {
setSelectedInvite(null);
window.location.reload();
}}
onCancel={() => {
setSelectedInvite(null);
}}
/>
</div>
</div>
)}
{/* Belt Progression Path - Only show with active membership */}
{activeMembership && (
<div className="bg-white border border-stone-200 rounded-2xl shadow-sm mb-6">
<div className="px-5 py-4 border-b border-stone-100">
<h2 className="text-lg font-semibold text-stone-900 flex items-center gap-2">
🥋 Belt Progression Path
</h2>
<p className="text-sm text-stone-500 mt-1">Track your students' journey to black belt</p>
</div>
<div className="px-5 py-4">
<div className="space-y-6">
{examProgress.map((progress, idx) => (
<div key={idx} className="p-4 bg-stone-50 rounded-lg">
<div className="flex justify-between items-start mb-3">
<div>
<div className="font-semibold text-stone-900">{progress.student}</div>
<div className="text-sm text-stone-500">Current: {progress.levelName}</div>
</div>
<span className="inline-flex items-center rounded-full bg-amber-100 px-2 py-1 text-xs font-medium text-amber-800">
Level {progress.currentLevel}
</span>
</div>
{/* Progress Bar */}
<div className="mb-3">
<div className="flex justify-between text-xs text-stone-500 mb-1">
<span>{progress.levelName}</span>
<span>{progress.nextLevel}</span>
</div>
<div className="h-3 bg-stone-200 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-amber-500 to-orange-500 rounded-full transition-all"
style={{ width: `${progress.progress}%` }}
/>
</div>
<div className="text-xs text-stone-500 mt-1 text-right">
{progress.progress}% to next belt
</div>
</div>
{/* Belt Path Visualization */}
<div className="flex items-center gap-2 overflow-x-auto py-2">
{["White", "Yellow", "Orange", "Green", "Blue", "Brown", "Black"].map(
(belt, beltIdx) => {
const isCompleted = beltIdx < progress.currentLevel;
const isCurrent = beltIdx === progress.currentLevel - 1;
const beltColors = {
White: "bg-gray-100 border-gray-300",
Yellow: "bg-yellow-100 border-yellow-400",
Orange: "bg-orange-100 border-orange-400",
Green: "bg-green-100 border-green-400",
Blue: "bg-blue-100 border-blue-400",
Brown: "bg-amber-800 border-amber-900 text-white",
Black: "bg-stone-900 border-stone-950 text-white",
};
return (
<div key={belt} className="flex flex-col items-center">
<div
className={`w-10 h-10 rounded-full border-2 flex items-center justify-center text-xs font-medium ${
beltColors[belt]
} ${
isCurrent ? "ring-2 ring-red-500 ring-offset-2" : ""
} ${isCompleted ? "opacity-100" : "opacity-40"}`}
>
{isCompleted ? "✓" : beltIdx + 1}
</div>
<span className="text-xs mt-1 text-stone-500">{belt}</span>
</div>
);
}
)}
</div>
</div>
))}
</div>
</div>
</div>
)}
{/* My Students */}
{activeMembership && (
<div className="bg-white border border-stone-200 rounded-2xl shadow-sm mb-6">
<div className="px-5 py-4 border-b border-stone-100">
<div className="flex justify-between items-center">
<div>
<h2 className="text-lg font-semibold text-stone-900">My Students</h2>
<p className="text-sm text-stone-500 mt-1">
{canAddMoreStudents
? `You can add ${(activeMembership.max_students || 1) - myStudents.length} more student(s)`
: "Maximum students reached"}
</p>
</div>
{canAddMoreStudents && (
<button
type="button"
onClick={() => setShowAddStudent(!showAddStudent)}
className="inline-flex items-center gap-2 rounded-lg bg-red-600 hover:bg-red-700 text-white text-sm font-medium px-4 py-2"
>
<Plus className="w-4 h-4" />
Add Student
</button>
)}
</div>
</div>
<div className="px-5 py-4">
{showAddStudent && (
<form onSubmit={handleAddStudent} className="mb-6 p-4 bg-stone-50 rounded-lg space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-stone-700 mb-1">
First Name *
</label>
<input
type="text"
value={studentForm.first_name}
onChange={(e) =>
setStudentForm({ ...studentForm, first_name: e.target.value })
}
required
className="w-full px-3 py-2 border border-stone-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-stone-700 mb-1">
Last Name *
</label>
<input
type="text"
value={studentForm.last_name}
onChange={(e) =>
setStudentForm({ ...studentForm, last_name: e.target.value })
}
required
className="w-full px-3 py-2 border border-stone-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-stone-700 mb-1">
Date of Birth
</label>
<input
type="date"
value={studentForm.date_of_birth}
onChange={(e) =>
setStudentForm({ ...studentForm, date_of_birth: e.target.value })
}
className="w-full px-3 py-2 border border-stone-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-stone-700 mb-1">Gender</label>
<select
value={studentForm.gender}
onChange={(e) =>
setStudentForm({ ...studentForm, gender: e.target.value })
}
className="w-full px-3 py-2 border border-stone-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
>
<option value="">Select gender</option>
<option value="Male">Male</option>
<option value="Female">Female</option>
<option value="Other">Other</option>
<option value="Prefer not to say">Prefer not to say</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-stone-700 mb-1">
Medical Notes
</label>
<textarea
value={studentForm.medical_notes}
onChange={(e) =>
setStudentForm({ ...studentForm, medical_notes: e.target.value })
}
placeholder="Any allergies, conditions, or special needs..."
rows={2}
className="w-full px-3 py-2 border border-stone-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
/>
</div>
{studentMessage && (
<div className={`p-3 rounded-lg text-sm ${
studentMessage.type === "success"
? "bg-green-50 text-green-800 border border-green-200"
: "bg-red-50 text-red-800 border border-red-200"
}`}>
{studentMessage.text}
</div>
)}
<div className="flex gap-2">
<button
type="submit"
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm font-medium"
>
Save Student
</button>
<button
type="button"
onClick={() => {
setShowAddStudent(false);
setStudentForm({
first_name: "",
last_name: "",
date_of_birth: "",
gender: "",
medical_notes: "",
});
setStudentMessage(null);
}}
className="px-4 py-2 border border-stone-200 rounded-lg hover:bg-stone-50 text-sm font-medium"
>
Cancel
</button>
</div>
</form>
)}
<div className="space-y-3">
{myStudents.length === 0 ? (
<div className="text-center py-8 text-stone-500">
<Users className="w-12 h-12 mx-auto mb-3 opacity-30" />
<p>No students added yet</p>
</div>
) : (
myStudents.map((student) => {
const studentEnrollments = enrollments.filter(
(e) => e.student_id === student.id
);
const age = calculateAge(student.date_of_birth);
return (
<div key={student.id} className="p-4 bg-stone-50 rounded-lg">
<div className="flex justify-between items-start">
<div>
<div className="font-semibold text-stone-900">
{student.first_name} {student.last_name}
</div>
<div className="text-sm text-stone-500">
{age && `Age: ${age}`}
{student.gender && ` • Gender: ${student.gender}`}
</div>
</div>
<span className="inline-flex items-center rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700">
Active
</span>
</div>
{studentEnrollments.length > 0 && (
<div className="mt-2 pt-2 border-t text-sm text-stone-600">
Classes: {studentEnrollments.map((e) => e.class_name).join(", ")}
</div>
)}
</div>
);
})
)}
</div>
</div>
</div>
)}
{/* Active Classes Summary */}
{enrollments.length > 0 && (
<div className="bg-white border border-stone-200 rounded-2xl shadow-sm mb-6">
<div className="px-5 py-4 border-b border-stone-100">
<h2 className="text-lg font-semibold text-stone-900 flex items-center gap-2">
<CheckCircle2 className="w-5 h-5 text-green-600" />
Active Classes
</h2>
</div>
<div className="px-5 py-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{Object.values(enrollmentsByClass).map((classData) => (
<div key={classData.class_id} className="p-4 bg-stone-50 rounded-lg">
<div className="font-semibold text-stone-900">{classData.class_name}</div>
<div className="text-sm text-stone-500 mt-1">
Students: {classData.students.map((s) => s.student_name).join(", ")}
</div>
</div>
))}
</div>
</div>
</div>
)}
{/* What's Happening (replaces Available Membership Plans) */}
<div className="mb-8">
<div className="flex items-center gap-2 mb-4">
<TrendingUp className="w-5 h-5 text-stone-600" />
<h2 className="text-xl font-bold text-stone-900">What&apos;s Happening</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white border border-stone-200 rounded-xl p-4">
<h3 className="font-semibold text-stone-900 mb-2">New Advanced Kata Program</h3>
<p className="text-sm text-stone-600 mb-4">
Master advanced kata forms with our new intensive program. Perfect for students
preparing for black belt exams.
</p>
<button
type="button"
className="inline-flex items-center gap-1 text-sm font-medium text-red-600 hover:text-red-700"
>
Know More <ArrowRight className="w-4 h-4" />
</button>
</div>
<div className="bg-white border border-stone-200 rounded-xl p-4">
<h3 className="font-semibold text-stone-900 mb-2">Competition Training Camp</h3>
<p className="text-sm text-stone-600 mb-4">
Join our specialized competition training camp. Learn tournament strategies and
improve your competitive performance.
</p>
<button
type="button"
className="inline-flex items-center gap-1 text-sm font-medium text-red-600 hover:text-red-700"
>
Know More <ArrowRight className="w-4 h-4" />
</button>
</div>
<div className="bg-white border border-stone-200 rounded-xl p-4">
<h3 className="font-semibold text-stone-900 mb-2">Self-Defense Workshop</h3>
<p className="text-sm text-stone-600 mb-4">
Practical self-defense techniques for all ages. Learn real-world applications of
karate in a safe, controlled environment.
</p>
<button
type="button"
className="inline-flex items-center gap-1 text-sm font-medium text-red-600 hover:text-red-700"
>
Know More <ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* AI Insights */}
<div className="bg-white border border-purple-200 rounded-2xl shadow-sm mb-6 bg-gradient-to-br from-purple-50 to-white">
<div className="px-5 py-4 border-b border-stone-100">
<h2 className="text-lg font-semibold text-stone-900 flex items-center gap-2">
<span className="text-2xl">🤖</span>
AI Training Insights
</h2>
</div>
<div className="px-5 py-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-4 bg-white rounded-lg border border-stone-200">
<div className="text-2xl mb-2">🔥</div>
<div className="font-semibold text-stone-900">Great Progress!</div>
<p className="text-sm text-stone-600 mt-1">
{myStudents.length > 0
? `${myStudents[0].first_name} has attended 12 classes this month - 20% more than last month!`
: "Alex has attended 12 classes this month - 20% more than last month!"}
</p>
</div>
<div className="p-4 bg-white rounded-lg border border-stone-200">
<div className="text-2xl mb-2">🎯</div>
<div className="font-semibold text-stone-900">Focus Area</div>
<p className="text-sm text-stone-600 mt-1">
{myStudents.length > 1
? `${myStudents[1].first_name} should practice kata forms before the next grading exam.`
: "Emma should practice kata forms before the next grading exam."}
</p>
</div>
<div className="p-4 bg-white rounded-lg border border-stone-200">
<div className="text-2xl mb-2"></div>
<div className="font-semibold text-stone-900">Achievement</div>
<p className="text-sm text-stone-600 mt-1">
{myStudents.length > 0
? `${myStudents[0].first_name} is ready for Orange Belt exam! Schedule it soon.`
: "Alex is ready for Orange Belt exam! Schedule it soon."}
</p>
</div>
</div>
</div>
</div>
{/* Membership History */}
<div className="bg-white border border-stone-200 rounded-2xl shadow-sm">
<div className="px-5 py-4 border-b border-stone-100">
<h2 className="text-lg font-semibold text-stone-900">Membership History</h2>
<p className="text-sm text-stone-500 mt-1">Payment activities and membership records</p>
</div>
<div className="px-5 py-4">
{loading ? (
<p className="text-sm text-stone-500">Loading membership history…</p>
) : memberships.length === 0 ? (
<p className="text-sm text-stone-500">
You don&apos;t have any membership records yet.
</p>
) : (
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="text-left text-xs text-stone-500 uppercase border-b border-stone-100">
<th className="py-2 pr-4">Plan</th>
<th className="py-2 pr-4">Status</th>
<th className="py-2 pr-4">{(memberships.some(m => m.is_cancellation_entry) || memberships.some(m => m.is_pause_entry)) ? "Date" : "Started"}</th>
<th className="py-2 pr-4">{memberships.some(m => m.is_pause_entry) ? "Resume/Renewal" : "Renewal"}</th>
<th className="py-2 pr-4">Price</th>
</tr>
</thead>
<tbody>
{memberships
.sort((a, b) => {
// Sort by date descending (most recent first)
const dateA = new Date(a.is_cancellation_entry || a.is_pause_entry ? a.start_date : a.start_date);
const dateB = new Date(b.is_cancellation_entry || b.is_pause_entry ? b.start_date : b.start_date);
return dateB - dateA;
})
.map((m) => (
<tr key={m.id} className="border-b border-stone-50">
<td className="py-2 pr-4 text-stone-900">
{m.is_cancellation_entry ? (
<span className="text-stone-600 italic">Cancelled</span>
) : m.is_pause_entry ? (
<span className="text-stone-600 italic">Paused</span>
) : (
m.plan_name
)}
</td>
<td className="py-2 pr-4">
<span
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
m.status === "active"
? "bg-green-50 text-green-700"
: m.status === "paused"
? "bg-blue-50 text-blue-700"
: m.status === "expired"
? "bg-red-50 text-red-700"
: m.status === "cancelled"
? "bg-amber-50 text-amber-700"
: "bg-stone-50 text-stone-700"
}`}
>
{m.status}
</span>
</td>
<td className="py-2 pr-4 text-stone-900">
{m.is_cancellation_entry ? (
<span>
Cancelled: {m.start_date
? format(new Date(m.start_date), "MMM d, yyyy")
: "-"}
</span>
) : m.is_pause_entry ? (
<span>
Paused: {m.start_date
? format(new Date(m.start_date), "MMM d, yyyy")
: "-"}
</span>
) : (
m.start_date
? format(new Date(m.start_date), "MMM d, yyyy")
: "-"
)}
</td>
<td className="py-2 pr-4 text-stone-900">
{m.renewal_date
? format(new Date(m.renewal_date), "MMM d, yyyy")
: "-"}
</td>
<td className="py-2 pr-4 text-stone-900">
${Number(m.price).toFixed(2)} / {m.billing_period}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
{/* No membership and no invite message */}
{!activeMembership && pendingInvites.length === 0 && !loading && (
<div className="bg-white border border-stone-200 rounded-2xl shadow-sm mt-6">
<div className="px-5 py-12 text-center">
<Users className="w-12 h-12 mx-auto mb-3 text-stone-300" />
<p className="text-stone-500">
You don&apos;t have an active membership or pending invitation.
</p>
<p className="text-sm text-stone-400 mt-2">
Contact the dojo admin to get an invitation.
</p>
</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 object-cover"
/>
<div>
<div className="text-sm font-semibold text-stone-900">Arun Martial Arts</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);
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 ${
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>
</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>
</>
)}
<StudentAIAssistant open={aiOpen} onClose={() => setAiOpen(false)} />
</div>
);
}