Seth0330 commited on
Commit
aace498
·
verified ·
1 Parent(s): a31b152

Update frontend/src/components/admin/ClassStudentManager.jsx

Browse files
frontend/src/components/admin/ClassStudentManager.jsx CHANGED
@@ -1,6 +1,6 @@
1
  // frontend/src/components/admin/ClassStudentManager.jsx
2
- import React, { useState, useMemo } from "react";
3
- import { Users2, X } from "lucide-react";
4
  import client from "../../api/client";
5
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
6
 
@@ -41,6 +41,11 @@ export default function ClassStudentManager({ classData, onClose }) {
41
  });
42
 
43
  const [selectedStudentId, setSelectedStudentId] = useState("");
 
 
 
 
 
44
 
45
  const availableStudents = useMemo(() => {
46
  const enrolledStudentIds = new Set(
@@ -64,13 +69,94 @@ export default function ClassStudentManager({ classData, onClose }) {
64
  });
65
  }, [students, enrollments]);
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  const enrollMutation = useMutation({
68
  // POST /api/admin/classes/{class_id}/enroll
69
  mutationFn: (payload) =>
70
  client.post(`/admin/classes/${classId}/enroll`, payload),
71
  onSuccess: () => {
72
  queryClient.invalidateQueries({ queryKey: ["class-enrollments", classId] });
73
- setSelectedEmail("");
 
 
74
  },
75
  });
76
 
@@ -94,7 +180,7 @@ export default function ClassStudentManager({ classData, onClose }) {
94
  enrollMutation.mutate({
95
  student_id: student.id,
96
  student_email: student.membership_email || "",
97
- student_name: student.name,
98
  });
99
  };
100
 
@@ -128,22 +214,69 @@ export default function ClassStudentManager({ classData, onClose }) {
128
  <div className="px-4 sm:px-6 py-4 space-y-4">
129
  {/* Add student row */}
130
  <div className="flex flex-wrap gap-3 items-center">
131
- <select
132
- className="flex-1 min-w-[220px] rounded-lg border border-stone-200 px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-red-500 focus:border-red-500"
133
- value={selectedStudentId}
134
- onChange={(e) => setSelectedStudentId(e.target.value)}
135
- disabled={isLoadingStudents || enrollMutation.isPending}
136
- >
137
- <option value="">Select a student to add...</option>
138
- {availableStudents.map((student) => (
139
- <option key={student.id} value={student.id}>
140
- {student.first_name} {student.last_name}
141
- {student.gender ? ` (${student.gender})` : ""}
142
- {student.membership_plan_name ? ` - ${student.membership_plan_name}` : ""}
143
- {student.membership_email ? ` [${student.membership_email}]` : ""}
144
- </option>
145
- ))}
146
- </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  <button
148
  type="button"
149
  onClick={handleAddStudent}
 
1
  // frontend/src/components/admin/ClassStudentManager.jsx
2
+ import React, { useState, useMemo, useRef, useEffect } from "react";
3
+ import { Users2, X, ChevronDown, Search } from "lucide-react";
4
  import client from "../../api/client";
5
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
6
 
 
41
  });
42
 
43
  const [selectedStudentId, setSelectedStudentId] = useState("");
44
+ const [searchQuery, setSearchQuery] = useState("");
45
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
46
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
47
+ const dropdownRef = useRef(null);
48
+ const inputRef = useRef(null);
49
 
50
  const availableStudents = useMemo(() => {
51
  const enrolledStudentIds = new Set(
 
69
  });
70
  }, [students, enrollments]);
71
 
72
+ // Filter students based on search query
73
+ const filteredStudents = useMemo(() => {
74
+ if (!searchQuery.trim()) return availableStudents;
75
+ const query = searchQuery.toLowerCase();
76
+ return availableStudents.filter((student) => {
77
+ const fullName = `${student.first_name} ${student.last_name}`.toLowerCase();
78
+ const email = (student.membership_email || "").toLowerCase();
79
+ const planName = (student.membership_plan_name || "").toLowerCase();
80
+ return (
81
+ fullName.includes(query) ||
82
+ email.includes(query) ||
83
+ planName.includes(query) ||
84
+ (student.gender && student.gender.toLowerCase().includes(query))
85
+ );
86
+ });
87
+ }, [availableStudents, searchQuery]);
88
+
89
+ // Close dropdown when clicking outside
90
+ useEffect(() => {
91
+ const handleClickOutside = (event) => {
92
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
93
+ setIsDropdownOpen(false);
94
+ setHighlightedIndex(-1);
95
+ }
96
+ };
97
+
98
+ document.addEventListener("mousedown", handleClickOutside);
99
+ return () => {
100
+ document.removeEventListener("mousedown", handleClickOutside);
101
+ };
102
+ }, []);
103
+
104
+ // Handle keyboard navigation
105
+ const handleKeyDown = (e) => {
106
+ if (!isDropdownOpen && (e.key === "ArrowDown" || e.key === "Enter")) {
107
+ setIsDropdownOpen(true);
108
+ return;
109
+ }
110
+
111
+ if (e.key === "ArrowDown") {
112
+ e.preventDefault();
113
+ setHighlightedIndex((prev) =>
114
+ prev < filteredStudents.length - 1 ? prev + 1 : prev
115
+ );
116
+ } else if (e.key === "ArrowUp") {
117
+ e.preventDefault();
118
+ setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : -1));
119
+ } else if (e.key === "Enter" && highlightedIndex >= 0) {
120
+ e.preventDefault();
121
+ const student = filteredStudents[highlightedIndex];
122
+ if (student) {
123
+ handleSelectStudent(student);
124
+ }
125
+ } else if (e.key === "Escape") {
126
+ setIsDropdownOpen(false);
127
+ setHighlightedIndex(-1);
128
+ }
129
+ };
130
+
131
+ const handleSelectStudent = (student) => {
132
+ setSelectedStudentId(student.id.toString());
133
+ setSearchQuery(`${student.first_name} ${student.last_name}`);
134
+ setIsDropdownOpen(false);
135
+ setHighlightedIndex(-1);
136
+ };
137
+
138
+ const handleInputChange = (e) => {
139
+ setSearchQuery(e.target.value);
140
+ setIsDropdownOpen(true);
141
+ setHighlightedIndex(-1);
142
+ if (!e.target.value) {
143
+ setSelectedStudentId("");
144
+ }
145
+ };
146
+
147
+ const handleInputFocus = () => {
148
+ setIsDropdownOpen(true);
149
+ };
150
+
151
  const enrollMutation = useMutation({
152
  // POST /api/admin/classes/{class_id}/enroll
153
  mutationFn: (payload) =>
154
  client.post(`/admin/classes/${classId}/enroll`, payload),
155
  onSuccess: () => {
156
  queryClient.invalidateQueries({ queryKey: ["class-enrollments", classId] });
157
+ setSelectedStudentId("");
158
+ setSearchQuery("");
159
+ setIsDropdownOpen(false);
160
  },
161
  });
162
 
 
180
  enrollMutation.mutate({
181
  student_id: student.id,
182
  student_email: student.membership_email || "",
183
+ student_name: `${student.first_name} ${student.last_name}`,
184
  });
185
  };
186
 
 
214
  <div className="px-4 sm:px-6 py-4 space-y-4">
215
  {/* Add student row */}
216
  <div className="flex flex-wrap gap-3 items-center">
217
+ <div className="flex-1 min-w-[220px] relative" ref={dropdownRef}>
218
+ <div className="relative">
219
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-stone-400 pointer-events-none" />
220
+ <input
221
+ ref={inputRef}
222
+ type="text"
223
+ value={searchQuery}
224
+ onChange={handleInputChange}
225
+ onFocus={handleInputFocus}
226
+ onKeyDown={handleKeyDown}
227
+ placeholder="Search and select a student to add..."
228
+ disabled={isLoadingStudents || enrollMutation.isPending}
229
+ className="w-full pl-9 pr-10 py-2 rounded-lg border border-stone-200 text-sm focus:outline-none focus:ring-1 focus:ring-red-500 focus:border-red-500 disabled:opacity-50 disabled:cursor-not-allowed"
230
+ />
231
+ <ChevronDown
232
+ className={`absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-stone-400 pointer-events-none transition-transform ${
233
+ isDropdownOpen ? "rotate-180" : ""
234
+ }`}
235
+ />
236
+ </div>
237
+
238
+ {/* Dropdown list */}
239
+ {isDropdownOpen && filteredStudents.length > 0 && (
240
+ <div className="absolute z-50 w-full mt-1 bg-white border border-stone-200 rounded-lg shadow-lg max-h-60 overflow-auto">
241
+ {filteredStudents.map((student, index) => {
242
+ const fullName = `${student.first_name} ${student.last_name}`;
243
+ const isHighlighted = index === highlightedIndex;
244
+ return (
245
+ <button
246
+ key={student.id}
247
+ type="button"
248
+ onClick={() => handleSelectStudent(student)}
249
+ onMouseEnter={() => setHighlightedIndex(index)}
250
+ className={`w-full text-left px-3 py-2 text-sm hover:bg-stone-50 transition-colors ${
251
+ isHighlighted ? "bg-stone-100" : ""
252
+ }`}
253
+ >
254
+ <div className="font-medium text-stone-900">{fullName}</div>
255
+ <div className="text-xs text-stone-500 mt-0.5">
256
+ {student.gender && `${student.gender} • `}
257
+ {student.membership_plan_name && `${student.membership_plan_name} • `}
258
+ {student.membership_email}
259
+ </div>
260
+ </button>
261
+ );
262
+ })}
263
+ </div>
264
+ )}
265
+
266
+ {/* No results message */}
267
+ {isDropdownOpen && searchQuery && filteredStudents.length === 0 && (
268
+ <div className="absolute z-50 w-full mt-1 bg-white border border-stone-200 rounded-lg shadow-lg px-3 py-2 text-sm text-stone-500">
269
+ No students found matching "{searchQuery}"
270
+ </div>
271
+ )}
272
+
273
+ {/* Empty state */}
274
+ {isDropdownOpen && !searchQuery && availableStudents.length === 0 && (
275
+ <div className="absolute z-50 w-full mt-1 bg-white border border-stone-200 rounded-lg shadow-lg px-3 py-2 text-sm text-stone-500">
276
+ No available students to add
277
+ </div>
278
+ )}
279
+ </div>
280
  <button
281
  type="button"
282
  onClick={handleAddStudent}