| |
| import React, { useState } from "react"; |
| import { useParams, useNavigate, Link } from "react-router-dom"; |
| import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; |
| import { |
| LayoutDashboard, |
| Users2, |
| FileText, |
| Trophy, |
| ShoppingBag, |
| Menu, |
| ArrowLeft, |
| User, |
| Calendar, |
| Phone, |
| Edit2, |
| BookOpen, |
| Target, |
| Award, |
| Medal, |
| Activity, |
| Stethoscope, |
| FileText as FileTextIcon, |
| } from "lucide-react"; |
| import api from "../api/client"; |
| import { differenceInYears, format } from "date-fns"; |
| import UserMenu from "../components/UserMenu"; |
| import dojoLogo from "../assets/dojo-logo.png"; |
|
|
| const BELT_COLORS = { |
| "white belt": "bg-gray-100 text-gray-800 border-gray-300", |
| "yellow belt": "bg-yellow-100 text-yellow-800 border-yellow-400", |
| "orange belt": "bg-orange-100 text-orange-800 border-orange-400", |
| "green belt": "bg-green-100 text-green-800 border-green-400", |
| "blue belt": "bg-blue-100 text-blue-800 border-blue-400", |
| "brown belt": "bg-amber-700 text-white border-amber-800", |
| "black belt": "bg-stone-900 text-white border-stone-950", |
| }; |
|
|
| export default function StudentProfile() { |
| const { id } = useParams(); |
| const navigate = useNavigate(); |
| const queryClient = useQueryClient(); |
| const [mobileNavOpen, setMobileNavOpen] = useState(false); |
| const [editingProfile, setEditingProfile] = useState(false); |
| const [commentText, setCommentText] = useState(""); |
|
|
| const storedAdmin = JSON.parse(sessionStorage.getItem("admin") || "{}"); |
| const storedCoach = JSON.parse(localStorage.getItem("karateCoach") || "{}"); |
| const isCoach = !!storedCoach.email; |
| const isAdmin = !!storedAdmin.email; |
| |
| const adminName = storedAdmin.name || "Admin"; |
| const adminEmail = storedAdmin.email || "admin@dojo.com"; |
| const coachName = storedCoach.name || "Coach"; |
| const coachEmail = storedCoach.email || ""; |
|
|
| function handleAdminLogout() { |
| sessionStorage.removeItem("admin"); |
| navigate("/login"); |
| } |
|
|
| function handleCoachLogout() { |
| localStorage.removeItem("karateCoach"); |
| navigate("/coach/login"); |
| } |
|
|
| |
| const { data: student, isLoading: studentLoading } = useQuery({ |
| queryKey: ["student", id], |
| queryFn: async () => { |
| const res = await api.get(`/admin/students`); |
| const students = Array.isArray(res.data) ? res.data : []; |
| return students.find((s) => s.id === parseInt(id)); |
| }, |
| enabled: !!id, |
| }); |
|
|
| |
| const { data: enrollments = [] } = useQuery({ |
| queryKey: ["student-enrollments", id], |
| queryFn: async () => { |
| const res = await api.get("/admin/classes"); |
| const classes = Array.isArray(res.data) ? res.data : []; |
| const allEnrollments = []; |
| for (const cls of classes) { |
| const enrollRes = await api.get(`/admin/classes/${cls.id}/enrollments`); |
| const classEnrollments = Array.isArray(enrollRes.data) ? enrollRes.data : []; |
| const studentEnrollments = classEnrollments.filter( |
| (e) => e.student_id === parseInt(id) && e.status !== "removed" |
| ); |
| allEnrollments.push(...studentEnrollments); |
| } |
| return allEnrollments; |
| }, |
| enabled: !!id, |
| }); |
|
|
| |
| const { data: studentComments = [], isLoading: commentsLoading } = useQuery({ |
| queryKey: ["student-comments", id], |
| queryFn: async () => { |
| if (isCoach && coachEmail) { |
| const res = await api.get(`/coach/students/${id}`, { |
| params: { email: coachEmail }, |
| }); |
| return res.data?.comments || []; |
| } else if (isAdmin) { |
| const res = await api.get(`/admin/students/${id}/comments`); |
| return Array.isArray(res.data) ? res.data : []; |
| } |
| return []; |
| }, |
| enabled: !!id && (isCoach || isAdmin), |
| }); |
|
|
| |
| const updateStudentMutation = useMutation({ |
| mutationFn: (data) => api.put(`/admin/students/${id}`, data), |
| onSuccess: () => { |
| queryClient.invalidateQueries({ queryKey: ["student", id] }); |
| queryClient.invalidateQueries({ queryKey: ["students"] }); |
| setEditingProfile(false); |
| }, |
| }); |
|
|
| |
| const commentMutation = useMutation({ |
| mutationFn: async (comment) => { |
| const res = await api.post(`/coach/students/${id}/comments`, { |
| coach_email: coachEmail, |
| comment: comment, |
| }); |
| return res.data; |
| }, |
| onSuccess: () => { |
| queryClient.invalidateQueries({ queryKey: ["student-comments", id] }); |
| setCommentText(""); |
| }, |
| }); |
|
|
| const navItems = isCoach |
| ? [{ to: "/coach", label: "Classes", icon: Users2 }] |
| : [ |
| { to: "/admin", label: "Dashboard", icon: LayoutDashboard }, |
| { to: "/admin/classes", label: "Classes", icon: Users2 }, |
| { to: "#", label: "Exams", icon: FileText }, |
| { to: "#", label: "Competitions", icon: Trophy }, |
| { to: "#", label: "Shop", icon: ShoppingBag }, |
| ]; |
|
|
| function isActivePath(path) { |
| return window.location.pathname === path; |
| } |
|
|
| function handleNavClick(path) { |
| if (path === "#") return; |
| navigate(path); |
| setMobileNavOpen(false); |
| } |
|
|
| const calculateAge = (dob) => { |
| if (!dob) return null; |
| try { |
| return differenceInYears(new Date(), new Date(dob)); |
| } catch { |
| return null; |
| } |
| }; |
|
|
| const getBeltStyle = (belt) => { |
| if (!belt) return "bg-gray-100 text-gray-800 border-gray-300"; |
| return BELT_COLORS[belt.toLowerCase()] || "bg-gray-100 text-gray-800 border-gray-300"; |
| }; |
|
|
| |
| const classAttends = enrollments.length; |
| const competitionsParticipated = 0; |
| const totalMedals = 0; |
| const goldMedals = 0; |
| const silverMedals = 0; |
| const bronzeMedals = 0; |
| const currentBelt = "yellow belt"; |
|
|
| if (studentLoading) { |
| return ( |
| <div className="min-h-screen bg-stone-50 flex items-center justify-center"> |
| <div className="text-stone-600">Loading student profile...</div> |
| </div> |
| ); |
| } |
|
|
| if (!student) { |
| return ( |
| <div className="min-h-screen bg-stone-50 flex items-center justify-center"> |
| <div className="text-stone-600">Student not found</div> |
| </div> |
| ); |
| } |
|
|
| const age = calculateAge(student.date_of_birth); |
| const studentName = `${student.first_name} ${student.last_name}`; |
|
|
| return ( |
| <div className="min-h-screen bg-stone-50 flex flex-col"> |
| {/* HEADER */} |
| <header className="sticky top-0 z-30 bg-white border-b border-stone-100"> |
| <div className="px-4 sm:px-6 lg:px-10 py-3 flex items-center justify-between"> |
| <div className="flex items-center gap-3"> |
| <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">{isCoach ? "Coach Portal" : "Admin Portal"}</div> |
| </div> |
| </div> |
| <UserMenu |
| name={isCoach ? coachName : adminName} |
| email={isCoach ? coachEmail : adminEmail} |
| onLogout={isCoach ? handleCoachLogout : handleAdminLogout} |
| /> |
| </div> |
| </header> |
| |
| <div className="flex flex-1"> |
| {/* SIDEBAR */} |
| <aside className="hidden md:flex flex-col w-64 border-r border-stone-100 bg-white"> |
| <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">{isCoach ? "Coach Mode" : "Admin Mode"}</div> |
| <div className="text-[11px] text-rose-500">{isCoach ? "Class Management" : "Full Access"}</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"> |
| {/* Back Button */} |
| {isCoach ? ( |
| <button |
| type="button" |
| onClick={() => navigate("/coach")} |
| className="inline-flex items-center gap-2 text-sm text-stone-600 hover:text-stone-900 mb-4" |
| > |
| <ArrowLeft className="w-4 h-4" /> |
| Back to Classes |
| </button> |
| ) : ( |
| <Link |
| to="/admin/members" |
| className="inline-flex items-center gap-2 text-sm text-stone-600 hover:text-stone-900 mb-4" |
| > |
| <ArrowLeft className="w-4 h-4" /> |
| Back to Members |
| </Link> |
| )} |
| |
| {/* Student Profile Header */} |
| <div className="bg-gradient-to-r from-red-600 to-red-700 rounded-2xl p-6 mb-6 text-white"> |
| <div className="flex items-start justify-between"> |
| <div className="flex items-center gap-4"> |
| <div className="w-20 h-20 bg-white/20 rounded-full flex items-center justify-center"> |
| <User className="w-10 h-10 text-white" /> |
| </div> |
| <div> |
| <h1 className="text-2xl sm:text-3xl font-bold mb-2">{studentName}</h1> |
| <div className="flex items-center gap-4 text-sm"> |
| {age && <span>{age} years old</span>} |
| {currentBelt && ( |
| <span |
| className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-medium border ${getBeltStyle( |
| currentBelt |
| )}`} |
| > |
| {currentBelt.charAt(0).toUpperCase() + currentBelt.slice(1)} |
| </span> |
| )} |
| </div> |
| </div> |
| </div> |
| {!isCoach && ( |
| <button |
| type="button" |
| onClick={() => setEditingProfile(!editingProfile)} |
| className="inline-flex items-center gap-2 px-4 py-2 bg-white/20 hover:bg-white/30 rounded-lg text-white text-sm font-medium" |
| > |
| <Edit2 className="w-4 h-4" /> |
| Edit Profile |
| </button> |
| )} |
| </div> |
| </div> |
| |
| {/* Stats Cards */} |
| <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6"> |
| <div className="bg-white border border-stone-200 rounded-lg p-4"> |
| <div className="flex items-center gap-3"> |
| <div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center"> |
| <BookOpen className="w-5 h-5 text-blue-600" /> |
| </div> |
| <div> |
| <div className="text-2xl font-bold text-stone-900">{classAttends}</div> |
| <div className="text-xs text-stone-500">Classes Enrolled</div> |
| </div> |
| </div> |
| </div> |
| <div className="bg-white border border-stone-200 rounded-lg p-4"> |
| <div className="flex items-center gap-3"> |
| <div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center"> |
| <Trophy className="w-5 h-5 text-purple-600" /> |
| </div> |
| <div> |
| <div className="text-2xl font-bold text-stone-900">{competitionsParticipated}</div> |
| <div className="text-xs text-stone-500">Competitions</div> |
| </div> |
| </div> |
| </div> |
| <div className="bg-white border border-stone-200 rounded-lg p-4"> |
| <div className="flex items-center gap-3"> |
| <div className="w-10 h-10 bg-amber-100 rounded-full flex items-center justify-center"> |
| <Medal className="w-5 h-5 text-amber-600" /> |
| </div> |
| <div> |
| <div className="text-2xl font-bold text-stone-900">{totalMedals}</div> |
| <div className="text-xs text-stone-500">Total Medals</div> |
| </div> |
| </div> |
| </div> |
| <div className="bg-white border border-stone-200 rounded-lg p-4"> |
| <div className="flex items-center gap-3"> |
| <div className="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center"> |
| <Award className="w-5 h-5 text-yellow-600" /> |
| </div> |
| <div> |
| <div className="text-2xl font-bold text-stone-900">{goldMedals}</div> |
| <div className="text-xs text-stone-500">Gold</div> |
| </div> |
| </div> |
| </div> |
| <div className="bg-white border border-stone-200 rounded-lg p-4"> |
| <div className="flex items-center gap-3"> |
| <div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center"> |
| <Award className="w-5 h-5 text-gray-600" /> |
| </div> |
| <div> |
| <div className="text-2xl font-bold text-stone-900">{silverMedals}</div> |
| <div className="text-xs text-stone-500">Silver</div> |
| </div> |
| </div> |
| </div> |
| <div className="bg-white border border-stone-200 rounded-lg p-4"> |
| <div className="flex items-center gap-3"> |
| <div className="w-10 h-10 bg-orange-100 rounded-full flex items-center justify-center"> |
| <Award className="w-5 h-5 text-orange-600" /> |
| </div> |
| <div> |
| <div className="text-2xl font-bold text-stone-900">{bronzeMedals}</div> |
| <div className="text-xs text-stone-500">Bronze</div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| {/* Personal Details */} |
| <div className="bg-white border border-stone-200 rounded-lg p-4 mb-6"> |
| <h3 className="text-sm font-semibold text-stone-900 mb-3">Personal Details</h3> |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm"> |
| {student.date_of_birth && ( |
| <div className="flex items-center gap-2"> |
| <Calendar className="w-4 h-4 text-stone-400" /> |
| <span className="text-stone-600">Date of Birth:</span> |
| <span className="font-medium text-stone-900"> |
| {format(new Date(student.date_of_birth), "MMM d, yyyy")} |
| </span> |
| </div> |
| )} |
| {student.membership_email && ( |
| <div className="flex items-center gap-2"> |
| <Phone className="w-4 h-4 text-stone-400" /> |
| <span className="text-stone-600">Contact:</span> |
| <span className="font-medium text-stone-900">{student.membership_email}</span> |
| </div> |
| )} |
| {student.gender && ( |
| <div className="flex items-center gap-2"> |
| <User className="w-4 h-4 text-stone-400" /> |
| <span className="text-stone-600">Gender:</span> |
| <span className="font-medium text-stone-900">{student.gender}</span> |
| </div> |
| )} |
| </div> |
| </div> |
| |
| {/* Edit Student Profile Form - Inline */} |
| {editingProfile && ( |
| <div className="bg-white border border-stone-200 rounded-lg p-4 mb-6"> |
| <div className="mb-4"> |
| <h3 className="text-lg font-semibold text-stone-900">Edit Student Profile</h3> |
| <p className="text-sm text-stone-500 mt-1">Update student information</p> |
| </div> |
| <form |
| onSubmit={(e) => { |
| e.preventDefault(); |
| const formData = new FormData(e.target); |
| updateStudentMutation.mutate({ |
| first_name: formData.get("first_name"), |
| last_name: formData.get("last_name"), |
| date_of_birth: formData.get("date_of_birth") || null, |
| gender: formData.get("gender") || null, |
| medical_notes: formData.get("medical_notes") || null, |
| }); |
| }} |
| className="space-y-4" |
| > |
| <div className="grid grid-cols-2 gap-4"> |
| <div> |
| <label className="block text-sm font-medium text-stone-700 mb-1">First Name</label> |
| <input |
| type="text" |
| name="first_name" |
| defaultValue={student.first_name} |
| 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" |
| name="last_name" |
| defaultValue={student.last_name} |
| 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> |
| <div className="grid grid-cols-2 gap-4"> |
| <div> |
| <label className="block text-sm font-medium text-stone-700 mb-1">Date of Birth</label> |
| <input |
| type="date" |
| name="date_of_birth" |
| defaultValue={ |
| student.date_of_birth ? student.date_of_birth.split("T")[0] : "" |
| } |
| 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 |
| name="gender" |
| defaultValue={student.gender || ""} |
| 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 |
| name="medical_notes" |
| defaultValue={student.medical_notes || ""} |
| rows={4} |
| 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 className="flex gap-2 justify-end pt-4"> |
| <button |
| type="button" |
| onClick={() => setEditingProfile(false)} |
| className="px-4 py-2 text-sm border border-stone-200 rounded-lg hover:bg-stone-50" |
| > |
| Cancel |
| </button> |
| <button |
| type="submit" |
| className="px-4 py-2 text-sm bg-red-600 text-white rounded-lg hover:bg-red-700" |
| > |
| Save |
| </button> |
| </div> |
| </form> |
| </div> |
| )} |
| |
| {/* Tabs */} |
| <div className="border-b border-stone-200 mb-6"> |
| <div className="flex gap-4"> |
| <button |
| type="button" |
| className="pb-3 px-1 border-b-2 border-red-600 text-red-600 font-medium text-sm" |
| > |
| Training & Progress |
| </button> |
| <button |
| type="button" |
| className="pb-3 px-1 border-b-2 border-transparent text-stone-600 hover:text-stone-900 text-sm" |
| > |
| Competitions |
| </button> |
| <button |
| type="button" |
| className="pb-3 px-1 border-b-2 border-transparent text-stone-600 hover:text-stone-900 text-sm" |
| > |
| Health & Safety |
| </button> |
| </div> |
| </div> |
| |
| {/* Content Sections */} |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> |
| {/* Exam History */} |
| <div className="bg-white border border-stone-200 rounded-lg p-4"> |
| <div className="flex items-center justify-between mb-4"> |
| <h3 className="text-sm font-semibold text-stone-900 flex items-center gap-2"> |
| <Target className="w-4 h-4" /> |
| Exam History |
| </h3> |
| </div> |
| <div className="text-center py-8"> |
| <Target className="w-12 h-12 mx-auto mb-3 text-stone-300" /> |
| <p className="text-sm text-stone-500">No exam attempts yet</p> |
| </div> |
| </div> |
| |
| {/* Enrolled Classes */} |
| <div className="bg-white border border-stone-200 rounded-lg p-4"> |
| <div className="flex items-center justify-between mb-4"> |
| <h3 className="text-sm font-semibold text-stone-900 flex items-center gap-2"> |
| <BookOpen className="w-4 h-4" /> |
| Enrolled Classes |
| </h3> |
| </div> |
| {enrollments.length > 0 ? ( |
| <div className="space-y-2"> |
| {enrollments.map((enrollment) => ( |
| <div |
| key={enrollment.id} |
| className="p-3 border border-stone-200 rounded-lg text-sm" |
| > |
| <div className="font-medium text-stone-900">{enrollment.class_name || "Class"}</div> |
| <div className="text-xs text-stone-500 mt-1"> |
| Status: {enrollment.status || "joined"} |
| </div> |
| </div> |
| ))} |
| </div> |
| ) : ( |
| <div className="text-center py-8"> |
| <BookOpen className="w-12 h-12 mx-auto mb-3 text-stone-300" /> |
| <p className="text-sm text-stone-500">Not enrolled in any classes</p> |
| </div> |
| )} |
| </div> |
| </div> |
| |
| {/* Medical Notes & Performance Notes */} |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> |
| <div className="bg-white border border-stone-200 rounded-lg p-4"> |
| <h3 className="text-sm font-semibold text-stone-900 mb-3 flex items-center gap-2"> |
| <Stethoscope className="w-4 h-4" /> |
| Medical Notes |
| </h3> |
| {!isCoach && editingProfile ? ( |
| <form |
| onSubmit={(e) => { |
| e.preventDefault(); |
| const formData = new FormData(e.target); |
| updateStudentMutation.mutate({ |
| medical_notes: formData.get("medical_notes") || null, |
| }); |
| }} |
| className="space-y-3" |
| > |
| <textarea |
| name="medical_notes" |
| defaultValue={student.medical_notes || ""} |
| rows={4} |
| className="w-full px-3 py-2 border border-stone-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 text-sm" |
| /> |
| <div className="flex gap-2"> |
| <button |
| type="submit" |
| className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm" |
| > |
| Save |
| </button> |
| <button |
| type="button" |
| onClick={() => setEditingProfile(false)} |
| className="px-4 py-2 border border-stone-200 rounded-lg hover:bg-stone-50 text-sm" |
| > |
| Cancel |
| </button> |
| </div> |
| </form> |
| ) : ( |
| <p className="text-sm text-stone-600 whitespace-pre-wrap"> |
| {student.medical_notes || "No medical notes"} |
| </p> |
| )} |
| </div> |
| |
| <div className="bg-white border border-stone-200 rounded-lg p-4"> |
| <h3 className="text-sm font-semibold text-stone-900 mb-3 flex items-center gap-2"> |
| <FileTextIcon className="w-4 h-4" /> |
| Performance Notes |
| </h3> |
| |
| {/* Coach can add comments */} |
| {isCoach && ( |
| <form |
| onSubmit={(e) => { |
| e.preventDefault(); |
| if (!commentText.trim()) return; |
| commentMutation.mutate(commentText); |
| }} |
| className="mb-4 space-y-3" |
| > |
| <textarea |
| value={commentText} |
| onChange={(e) => setCommentText(e.target.value)} |
| placeholder="Add a comment about this student..." |
| rows={3} |
| className="w-full px-3 py-2 border border-stone-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 text-sm" |
| /> |
| <button |
| type="submit" |
| disabled={commentMutation.isPending || !commentText.trim()} |
| className="inline-flex items-center justify-center px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm font-medium disabled:opacity-60" |
| > |
| {commentMutation.isPending ? "Adding..." : "Add Comment"} |
| </button> |
| </form> |
| )} |
| |
| {/* Comments History */} |
| {commentsLoading ? ( |
| <p className="text-sm text-stone-500">Loading comments...</p> |
| ) : studentComments.length === 0 ? ( |
| <p className="text-sm text-stone-600">No performance notes</p> |
| ) : ( |
| <div className="space-y-3"> |
| {studentComments.map((comment) => ( |
| <div |
| key={comment.id} |
| className="border border-stone-200 rounded-lg p-3 bg-stone-50" |
| > |
| <div className="flex items-start justify-between mb-2"> |
| <div> |
| <div className="font-medium text-stone-900 text-sm"> |
| {comment.coach_name} |
| </div> |
| <div className="text-xs text-stone-500"> |
| {format(new Date(comment.created_at), "MMM d, yyyy 'at' h:mm a")} |
| </div> |
| </div> |
| <FileTextIcon className="w-4 h-4 text-stone-400" /> |
| </div> |
| <p className="text-sm text-stone-700 whitespace-pre-wrap">{comment.comment}</p> |
| </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 object-cover" |
| /> |
| <div> |
| <div className="text-sm font-semibold text-stone-900">Arun Martial Arts</div> |
| <div className="text-xs text-stone-500">{isCoach ? "Coach Portal" : "Admin 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">{isCoach ? "Coach Mode" : "Admin Mode"}</div> |
| <div className="text-[11px] text-rose-500">{isCoach ? "Class Management" : "Full Access"}</div> |
| </div> |
| </div> |
| </div> |
| </> |
| )} |
| </div> |
| ); |
| } |
|
|
|
|