馃 distributed transcription service
thistle.dunkirk.sh
1import { css, html, LitElement } from "lit";
2import { customElement, state } from "lit/decorators.js";
3import type { MeetingTime } from "./meeting-time-picker";
4import "./meeting-time-picker";
5
6interface Class {
7 id: string;
8 course_code: string;
9 name: string;
10 professor: string;
11 semester: string;
12 year: number;
13 archived: boolean;
14 created_at: number;
15 student_count?: number;
16 transcript_count?: number;
17}
18
19interface WaitlistEntry {
20 id: string;
21 user_id: number;
22 course_code: string;
23 course_name: string;
24 professor: string;
25 semester: string;
26 year: number;
27 additional_info: string | null;
28 meeting_times: string | null;
29 created_at: number;
30}
31
32@customElement("admin-classes")
33export class AdminClasses extends LitElement {
34 @state() classes: Class[] = [];
35 @state() waitlist: WaitlistEntry[] = [];
36 @state() isLoading = true;
37 @state() error = "";
38 @state() searchTerm = "";
39
40 @state() activeTab: "classes" | "waitlist" = "classes";
41 @state() approvingEntry: WaitlistEntry | null = null;
42 @state() showModal = false;
43 @state() meetingTimes: MeetingTime[] = [];
44 @state() editingClass = {
45 courseCode: "",
46 courseName: "",
47 professor: "",
48 semester: "",
49 year: new Date().getFullYear(),
50 };
51 @state() deleteState: {
52 id: string;
53 type: "class" | "waitlist";
54 clicks: number;
55 timeout: number | null;
56 } | null = null;
57
58 static override styles = css`
59 :host {
60 display: block;
61 }
62
63 .header {
64 display: flex;
65 justify-content: space-between;
66 align-items: center;
67 margin-bottom: 1.5rem;
68 gap: 1rem;
69 }
70
71 .search {
72 flex: 1;
73 max-width: 30rem;
74 padding: 0.5rem 0.75rem;
75 border: 2px solid var(--secondary);
76 border-radius: 4px;
77 font-size: 1rem;
78 font-family: inherit;
79 background: var(--background);
80 color: var(--text);
81 }
82
83 .search:focus {
84 outline: none;
85 border-color: var(--primary);
86 }
87
88 .create-btn {
89 padding: 0.75rem 1.5rem;
90 background: var(--primary);
91 color: white;
92 border: 2px solid var(--primary);
93 border-radius: 6px;
94 font-size: 1rem;
95 font-weight: 500;
96 cursor: pointer;
97 transition: all 0.2s;
98 font-family: inherit;
99 white-space: nowrap;
100 }
101
102 .create-btn:hover {
103 background: var(--gunmetal);
104 border-color: var(--gunmetal);
105 }
106
107 .classes-grid {
108 display: grid;
109 gap: 1rem;
110 }
111
112 .class-card {
113 background: var(--background);
114 border: 2px solid var(--secondary);
115 border-radius: 8px;
116 padding: 1.25rem;
117 transition: all 0.2s;
118 }
119
120 .class-card:hover {
121 border-color: var(--primary);
122 }
123
124 .class-card.archived {
125 opacity: 0.6;
126 border-style: dashed;
127 }
128
129 .class-header {
130 display: flex;
131 justify-content: space-between;
132 align-items: flex-start;
133 gap: 1rem;
134 margin-bottom: 0.75rem;
135 }
136
137 .class-info {
138 flex: 1;
139 }
140
141 .course-code {
142 font-size: 0.875rem;
143 font-weight: 600;
144 color: var(--accent);
145 text-transform: uppercase;
146 }
147
148 .class-name {
149 font-size: 1.125rem;
150 font-weight: 600;
151 margin: 0.25rem 0;
152 color: var(--text);
153 }
154
155 .class-meta {
156 display: flex;
157 gap: 1rem;
158 font-size: 0.875rem;
159 color: var(--paynes-gray);
160 flex-wrap: wrap;
161 }
162
163 .badge {
164 display: inline-block;
165 padding: 0.25rem 0.5rem;
166 border-radius: 4px;
167 font-size: 0.75rem;
168 font-weight: 600;
169 text-transform: uppercase;
170 }
171
172 .badge.archived {
173 background: var(--secondary);
174 color: var(--text);
175 }
176
177 .actions {
178 display: flex;
179 gap: 0.5rem;
180 flex-wrap: wrap;
181 }
182
183 button {
184 padding: 0.5rem 1rem;
185 border: 2px solid;
186 border-radius: 6px;
187 font-size: 0.875rem;
188 font-weight: 500;
189 cursor: pointer;
190 transition: all 0.2s;
191 font-family: inherit;
192 }
193
194 .btn-archive {
195 background: transparent;
196 color: var(--paynes-gray);
197 border-color: var(--secondary);
198 }
199
200 .btn-archive:hover {
201 border-color: var(--paynes-gray);
202 }
203
204 .btn-delete {
205 background: transparent;
206 color: #dc2626;
207 border-color: #dc2626;
208 }
209
210 .btn-delete:hover {
211 background: #dc2626;
212 color: white;
213 }
214
215 button:disabled {
216 opacity: 0.6;
217 cursor: not-allowed;
218 }
219
220 .empty-state {
221 text-align: center;
222 padding: 3rem 2rem;
223 color: var(--paynes-gray);
224 }
225
226 .loading {
227 text-align: center;
228 padding: 3rem 2rem;
229 color: var(--paynes-gray);
230 }
231
232 .error-banner {
233 background: #fecaca;
234 border: 2px solid rgba(220, 38, 38, 0.8);
235 border-radius: 6px;
236 padding: 1rem;
237 margin-bottom: 1.5rem;
238 color: #dc2626;
239 font-weight: 500;
240 }
241
242 .tabs {
243 display: flex;
244 gap: 1rem;
245 margin-bottom: 2rem;
246 border-bottom: 2px solid var(--secondary);
247 }
248
249 .tab {
250 padding: 0.75rem 1.5rem;
251 background: transparent;
252 border: none;
253 border-radius: 0;
254 color: var(--text);
255 cursor: pointer;
256 font-size: 1rem;
257 font-weight: 500;
258 font-family: inherit;
259 border-bottom: 2px solid transparent;
260 margin-bottom: -2px;
261 transition: all 0.2s;
262 }
263
264 .tab:hover {
265 color: var(--primary);
266 }
267
268 .tab.active {
269 color: var(--primary);
270 border-bottom-color: var(--primary);
271 }
272
273 .tab-badge {
274 display: inline-block;
275 margin-left: 0.5rem;
276 padding: 0.125rem 0.5rem;
277 background: var(--accent);
278 color: white;
279 border-radius: 12px;
280 font-size: 0.75rem;
281 font-weight: 600;
282 }
283
284 .modal-overlay {
285 position: fixed;
286 top: 0;
287 left: 0;
288 right: 0;
289 bottom: 0;
290 background: rgba(0, 0, 0, 0.5);
291 display: flex;
292 align-items: center;
293 justify-content: center;
294 z-index: 1000;
295 padding: 1rem;
296 }
297
298 .modal {
299 background: var(--background);
300 border: 2px solid var(--secondary);
301 border-radius: 12px;
302 padding: 2rem;
303 max-width: 32rem;
304 width: 100%;
305 max-height: 90vh;
306 overflow-y: auto;
307 }
308
309 .modal-title {
310 margin: 0 0 1.5rem 0;
311 color: var(--text);
312 font-size: 1.5rem;
313 }
314
315 .form-group {
316 margin-bottom: 1rem;
317 }
318
319 .form-group label {
320 display: block;
321 margin-bottom: 0.5rem;
322 font-weight: 500;
323 color: var(--text);
324 font-size: 0.875rem;
325 }
326
327 .form-group input {
328 width: 100%;
329 padding: 0.75rem;
330 border: 2px solid var(--secondary);
331 border-radius: 6px;
332 font-size: 1rem;
333 font-family: inherit;
334 background: var(--background);
335 color: var(--text);
336 box-sizing: border-box;
337 }
338
339 .form-group input:focus,
340 .form-group select:focus {
341 outline: none;
342 border-color: var(--primary);
343 }
344
345 .form-group select {
346 width: 100%;
347 padding: 0.75rem;
348 border: 2px solid var(--secondary);
349 border-radius: 6px;
350 font-size: 1rem;
351 font-family: inherit;
352 background: var(--background);
353 color: var(--text);
354 box-sizing: border-box;
355 }
356
357 .form-grid {
358 display: grid;
359 grid-template-columns: 1fr 1fr;
360 gap: 1rem;
361 margin-bottom: 1rem;
362 }
363
364 .form-group-full {
365 grid-column: 1 / -1;
366 }
367
368 .meeting-times-list {
369 display: flex;
370 flex-direction: column;
371 gap: 0.5rem;
372 }
373
374 .meeting-time-row {
375 display: flex;
376 gap: 0.5rem;
377 align-items: center;
378 }
379
380 .meeting-time-row input {
381 flex: 1;
382 }
383
384 .btn-remove {
385 padding: 0.5rem;
386 background: transparent;
387 color: #dc2626;
388 border: 2px solid #dc2626;
389 border-radius: 6px;
390 cursor: pointer;
391 font-size: 0.875rem;
392 font-weight: 500;
393 transition: all 0.2s;
394 }
395
396 .btn-remove:hover {
397 background: #dc2626;
398 color: white;
399 }
400
401 .btn-add {
402 margin-top: 0.5rem;
403 padding: 0.5rem 1rem;
404 background: transparent;
405 color: var(--primary);
406 border: 2px solid var(--primary);
407 border-radius: 6px;
408 cursor: pointer;
409 font-size: 0.875rem;
410 font-weight: 500;
411 transition: all 0.2s;
412 }
413
414 .btn-add:hover {
415 background: var(--primary);
416 color: white;
417 }
418
419 .modal-actions {
420 display: flex;
421 gap: 0.75rem;
422 justify-content: flex-end;
423 margin-top: 1.5rem;
424 }
425
426 .btn-submit {
427 padding: 0.75rem 1.5rem;
428 background: var(--primary);
429 color: white;
430 border: 2px solid var(--primary);
431 border-radius: 6px;
432 font-size: 1rem;
433 font-weight: 500;
434 cursor: pointer;
435 transition: all 0.2s;
436 font-family: inherit;
437 }
438
439 .btn-submit:hover:not(:disabled) {
440 background: var(--gunmetal);
441 border-color: var(--gunmetal);
442 }
443
444 .btn-submit:disabled {
445 opacity: 0.6;
446 cursor: not-allowed;
447 }
448
449 .btn-cancel {
450 padding: 0.75rem 1.5rem;
451 background: transparent;
452 color: var(--text);
453 border: 2px solid var(--secondary);
454 border-radius: 6px;
455 font-size: 1rem;
456 font-weight: 500;
457 cursor: pointer;
458 transition: all 0.2s;
459 font-family: inherit;
460 }
461
462 .btn-cancel:hover {
463 border-color: var(--primary);
464 color: var(--primary);
465 }
466 `;
467
468 override async connectedCallback() {
469 super.connectedCallback();
470
471 // Check for subtab query parameter
472 const params = new URLSearchParams(window.location.search);
473 const subtab = params.get("subtab");
474 if (subtab && this.isValidSubtab(subtab)) {
475 this.activeTab = subtab as "classes" | "waitlist";
476 } else {
477 // Set default subtab in URL if on classes tab
478 this.setActiveTab(this.activeTab);
479 }
480
481 await this.loadData();
482 }
483
484 private isValidSubtab(subtab: string): boolean {
485 return ["classes", "waitlist"].includes(subtab);
486 }
487
488 private setActiveTab(tab: "classes" | "waitlist") {
489 this.activeTab = tab;
490 // Update URL without reloading page
491 const url = new URL(window.location.href);
492 url.searchParams.set("subtab", tab);
493 window.history.pushState({}, "", url);
494 }
495
496 private async loadData() {
497 this.isLoading = true;
498 this.error = "";
499
500 try {
501 const [classesRes, waitlistRes] = await Promise.all([
502 fetch("/api/admin/classes"),
503 fetch("/api/admin/waitlist"),
504 ]);
505
506 if (!classesRes.ok || !waitlistRes.ok) {
507 throw new Error("Failed to load data");
508 }
509
510 const classesData = await classesRes.json();
511 const waitlistData = await waitlistRes.json();
512
513 // Flatten grouped classes into array
514 const groupedClasses = classesData.classes || {};
515 this.classes = Object.values(groupedClasses).flat();
516 this.waitlist = waitlistData.waitlist || [];
517 } catch {
518 this.error = "Failed to load data. Please try again.";
519 } finally {
520 this.isLoading = false;
521 }
522 }
523
524 private handleSearch(e: Event) {
525 this.searchTerm = (e.target as HTMLInputElement).value.toLowerCase();
526 }
527
528 private async handleToggleArchive(classId: string) {
529 try {
530 // Find the class to toggle its archived state
531 const classToToggle = this.classes.find((c) => c.id === classId);
532 if (!classToToggle) return;
533
534 const response = await fetch(`/api/classes/${classId}/archive`, {
535 method: "PUT",
536 headers: { "Content-Type": "application/json" },
537 body: JSON.stringify({ archived: !classToToggle.archived }),
538 });
539
540 if (!response.ok) {
541 throw new Error("Failed to update class");
542 }
543
544 // Update local state instead of reloading
545 this.classes = this.classes.map((c) =>
546 c.id === classId ? { ...c, archived: !c.archived } : c,
547 );
548 } catch {
549 this.error = "Failed to update class. Please try again.";
550 }
551 }
552
553 private handleDeleteClick(id: string, type: "class" | "waitlist") {
554 // If this is a different item or timeout expired, reset
555 if (
556 !this.deleteState ||
557 this.deleteState.id !== id ||
558 this.deleteState.type !== type
559 ) {
560 // Clear any existing timeout
561 if (this.deleteState?.timeout) {
562 clearTimeout(this.deleteState.timeout);
563 }
564
565 // Set first click
566 const timeout = window.setTimeout(() => {
567 this.deleteState = null;
568 }, 1000);
569
570 this.deleteState = { id, type, clicks: 1, timeout };
571 return;
572 }
573
574 // Increment clicks
575 const newClicks = this.deleteState.clicks + 1;
576
577 // Clear existing timeout
578 if (this.deleteState.timeout) {
579 clearTimeout(this.deleteState.timeout);
580 }
581
582 // Third click - actually delete
583 if (newClicks === 3) {
584 this.deleteState = null;
585 if (type === "class") {
586 this.performDeleteClass(id);
587 } else {
588 this.performDeleteWaitlist(id);
589 }
590 return;
591 }
592
593 // Second click - reset timeout
594 const timeout = window.setTimeout(() => {
595 this.deleteState = null;
596 }, 1000);
597
598 this.deleteState = { id, type, clicks: newClicks, timeout };
599 }
600
601 private getDeleteButtonText(id: string, type: "class" | "waitlist"): string {
602 if (
603 !this.deleteState ||
604 this.deleteState.id !== id ||
605 this.deleteState.type !== type
606 ) {
607 return "Delete";
608 }
609
610 if (this.deleteState.clicks === 1) {
611 return "Are you sure?";
612 }
613
614 if (this.deleteState.clicks === 2) {
615 return "Final warning!";
616 }
617
618 return "Delete";
619 }
620
621 private async performDeleteClass(classId: string) {
622 try {
623 const response = await fetch(`/api/classes/${classId}`, {
624 method: "DELETE",
625 });
626
627 if (!response.ok) {
628 throw new Error("Failed to delete class");
629 }
630
631 await this.loadData();
632 } catch {
633 this.error = "Failed to delete class. Please try again.";
634 }
635 }
636
637 private async performDeleteWaitlist(id: string) {
638 try {
639 const response = await fetch(`/api/admin/waitlist/${id}`, {
640 method: "DELETE",
641 });
642
643 if (!response.ok) {
644 throw new Error("Failed to delete waitlist entry");
645 }
646
647 await this.loadData();
648 } catch {
649 this.error = "Failed to delete waitlist entry. Please try again.";
650 }
651 }
652
653 private handleCreateClass() {
654 // Set empty form for creating new class
655 this.approvingEntry = null;
656 this.editingClass = {
657 courseCode: "",
658 courseName: "",
659 professor: "",
660 semester: "",
661 year: new Date().getFullYear(),
662 };
663 this.meetingTimes = [];
664 this.showModal = true;
665 }
666
667 private getFilteredClasses() {
668 if (!this.searchTerm) return this.classes;
669
670 return this.classes.filter((cls) => {
671 const searchStr = this.searchTerm;
672 return (
673 cls.course_code.toLowerCase().includes(searchStr) ||
674 cls.name.toLowerCase().includes(searchStr) ||
675 cls.professor.toLowerCase().includes(searchStr)
676 );
677 });
678 }
679
680 override render() {
681 if (this.isLoading) {
682 return html`<div class="loading">Loading...</div>`;
683 }
684
685 const filteredClasses = this.getFilteredClasses();
686
687 return html`
688 ${this.error ? html`<div class="error-banner">${this.error}</div>` : ""}
689
690 <div class="tabs">
691 <button
692 class="tab ${this.activeTab === "classes" ? "active" : ""}"
693 @click=${() => {
694 this.setActiveTab("classes");
695 }}
696 >
697 Classes
698 </button>
699 <button
700 class="tab ${this.activeTab === "waitlist" ? "active" : ""}"
701 @click=${() => {
702 this.setActiveTab("waitlist");
703 }}
704 >
705 Waitlist
706 ${this.waitlist.length > 0 ? html`<span class="tab-badge">${this.waitlist.length}</span>` : ""}
707 </button>
708 </div>
709
710 ${
711 this.activeTab === "classes"
712 ? this.renderClasses(filteredClasses)
713 : this.renderWaitlist()
714 }
715
716 ${this.showModal ? this.renderApprovalModal() : ""}
717 `;
718 }
719
720 private renderClasses(filteredClasses: Class[]) {
721 return html`
722 <div class="header">
723 <input
724 type="text"
725 class="search"
726 placeholder="Search classes..."
727 @input=${this.handleSearch}
728 .value=${this.searchTerm}
729 />
730 <button class="create-btn" @click=${this.handleCreateClass}>
731 + Create Class
732 </button>
733 </div>
734
735 ${
736 filteredClasses.length === 0
737 ? html`
738 <div class="empty-state">
739 ${this.searchTerm ? "No classes found matching your search" : "No classes yet"}
740 </div>
741 `
742 : html`
743 <div class="classes-grid">
744 ${filteredClasses.map(
745 (cls) => html`
746 <div class="class-card ${cls.archived ? "archived" : ""}">
747 <div class="class-header">
748 <div class="class-info">
749 <div class="course-code">${cls.course_code}</div>
750 <div class="class-name">${cls.name}</div>
751 <div class="class-meta">
752 <span>馃懁 ${cls.professor}</span>
753 <span>馃搮 ${cls.semester} ${cls.year}</span>
754 <span>馃懃 ${cls.student_count || 0} students</span>
755 <span>馃搫 ${cls.transcript_count || 0} transcripts</span>
756 ${cls.archived ? html`<span class="badge archived">Archived</span>` : ""}
757 </div>
758 </div>
759 <div class="actions">
760 <button
761 class="btn-archive"
762 @click=${() => this.handleToggleArchive(cls.id)}
763 >
764 ${cls.archived ? "Unarchive" : "Archive"}
765 </button>
766 <button
767 class="btn-delete"
768 @click=${() => this.handleDeleteClick(cls.id, "class")}
769 >
770 ${this.getDeleteButtonText(cls.id, "class")}
771 </button>
772 </div>
773 </div>
774 </div>
775 `,
776 )}
777 </div>
778 `
779 }
780 `;
781 }
782
783 private renderWaitlist() {
784 return html`
785 ${
786 this.waitlist.length === 0
787 ? html`
788 <div class="empty-state">No waitlist requests yet</div>
789 `
790 : html`
791 <div class="classes-grid">
792 ${this.waitlist.map(
793 (entry) => html`
794 <div class="class-card">
795 <div class="class-header">
796 <div class="class-info">
797 <div class="course-code">${entry.course_code}</div>
798 <div class="class-name">${entry.course_name}</div>
799 <div class="class-meta">
800 <span>馃懁 ${entry.professor}</span>
801 <span>馃搮 ${entry.semester} ${entry.year}</span>
802 </div>
803 ${
804 entry.additional_info
805 ? html`
806 <p style="margin-top: 0.75rem; font-size: 0.875rem; color: var(--paynes-gray);">
807 ${entry.additional_info}
808 </p>
809 `
810 : ""
811 }
812 </div>
813 <div class="actions">
814 <button
815 class="btn-archive"
816 @click=${() => this.handleApproveWaitlist(entry)}
817 >
818 Approve & Create Class
819 </button>
820 <button
821 class="btn-delete"
822 @click=${() => this.handleDeleteClick(entry.id, "waitlist")}
823 >
824 ${this.getDeleteButtonText(entry.id, "waitlist")}
825 </button>
826 </div>
827 </div>
828 </div>
829 `,
830 )}
831 </div>
832 `
833 }
834 `;
835 }
836
837 private handleApproveWaitlist(entry: WaitlistEntry) {
838 this.approvingEntry = entry;
839
840 // Pre-fill form with waitlist data
841 this.editingClass = {
842 courseCode: entry.course_code,
843 courseName: entry.course_name,
844 professor: entry.professor,
845 semester: entry.semester,
846 year: entry.year,
847 };
848
849 // Parse meeting times from JSON if available, otherwise use empty array
850 if (entry.meeting_times) {
851 try {
852 const parsed = JSON.parse(entry.meeting_times);
853 this.meetingTimes =
854 Array.isArray(parsed) && parsed.length > 0 ? parsed : [];
855 } catch {
856 this.meetingTimes = [];
857 }
858 } else {
859 this.meetingTimes = [];
860 }
861 this.showModal = true;
862 }
863
864 private handleMeetingTimesChange(e: CustomEvent) {
865 this.meetingTimes = e.detail;
866 }
867
868 private handleClassFieldInput(field: string, e: Event) {
869 const value = (e.target as HTMLInputElement | HTMLSelectElement).value;
870 this.editingClass = { ...this.editingClass, [field]: value };
871 }
872
873 private cancelApproval() {
874 this.showModal = false;
875 this.approvingEntry = null;
876 this.meetingTimes = [];
877 this.editingClass = {
878 courseCode: "",
879 courseName: "",
880 professor: "",
881 semester: "",
882 year: new Date().getFullYear(),
883 };
884 }
885
886 private async submitApproval() {
887 if (this.meetingTimes.length === 0) {
888 this.error = "Please add at least one meeting time";
889 return;
890 }
891
892 // Convert MeetingTime objects to label strings
893 const labels = this.meetingTimes.map((t) => t.label);
894
895 try {
896 const response = await fetch("/api/classes", {
897 method: "POST",
898 headers: { "Content-Type": "application/json" },
899 body: JSON.stringify({
900 course_code: this.editingClass.courseCode,
901 name: this.editingClass.courseName,
902 professor: this.editingClass.professor,
903 semester: this.editingClass.semester,
904 year: this.editingClass.year,
905 meeting_times: labels,
906 }),
907 });
908
909 if (!response.ok) {
910 const data = await response.json();
911 console.error("Failed to create class:", data);
912 throw new Error(data.error || "Failed to create class");
913 }
914
915 // If approving from waitlist, delete the waitlist entry
916 if (this.approvingEntry) {
917 await fetch(`/api/admin/waitlist/${this.approvingEntry.id}`, {
918 method: "DELETE",
919 });
920 }
921
922 await this.loadData();
923
924 this.setActiveTab("classes");
925 this.showModal = false;
926 this.approvingEntry = null;
927 this.meetingTimes = [];
928 this.editingClass = {
929 courseCode: "",
930 courseName: "",
931 professor: "",
932 semester: "",
933 year: new Date().getFullYear(),
934 };
935 } catch (error) {
936 console.error("Error in submitApproval:", error);
937 this.error =
938 error instanceof Error
939 ? error.message
940 : "Failed to create class. Please try again.";
941 }
942 }
943
944 private renderApprovalModal() {
945 const isApproving = !!this.approvingEntry;
946 const title = isApproving ? "Review & Create Class" : "Create New Class";
947 const description = isApproving
948 ? "Review the class details and make any edits before creating"
949 : "Enter the class details below";
950
951 return html`
952 <div class="modal-overlay" @click=${this.cancelApproval}>
953 <div class="modal" @click=${(e: Event) => e.stopPropagation()}>
954 <h2 class="modal-title">${title}</h2>
955
956 <p style="margin-bottom: 1.5rem; color: var(--paynes-gray);">
957 ${description}
958 </p>
959
960 ${this.error ? html`<div class="error-banner">${this.error}</div>` : ""}
961
962 <div class="form-grid">
963 <div class="form-group">
964 <label>Course Code *</label>
965 <input
966 type="text"
967 required
968 .value=${this.editingClass.courseCode}
969 @input=${(e: Event) => this.handleClassFieldInput("courseCode", e)}
970 />
971 </div>
972 <div class="form-group">
973 <label>Course Name *</label>
974 <input
975 type="text"
976 required
977 .value=${this.editingClass.courseName}
978 @input=${(e: Event) => this.handleClassFieldInput("courseName", e)}
979 />
980 </div>
981 <div class="form-group">
982 <label>Professor *</label>
983 <input
984 type="text"
985 required
986 .value=${this.editingClass.professor}
987 @input=${(e: Event) => this.handleClassFieldInput("professor", e)}
988 />
989 </div>
990 <div class="form-group">
991 <label>Semester *</label>
992 <select
993 required
994 .value=${this.editingClass.semester}
995 @change=${(e: Event) => this.handleClassFieldInput("semester", e)}
996 >
997 <option value="">Select semester</option>
998 <option value="Spring">Spring</option>
999 <option value="Summer">Summer</option>
1000 <option value="Fall">Fall</option>
1001 <option value="Winter">Winter</option>
1002 </select>
1003 </div>
1004 <div class="form-group">
1005 <label>Year *</label>
1006 <input
1007 type="number"
1008 required
1009 min="2020"
1010 max="2030"
1011 .value=${this.editingClass.year.toString()}
1012 @input=${(e: Event) => this.handleClassFieldInput("year", e)}
1013 />
1014 </div>
1015 <div class="form-group form-group-full">
1016 <label>Meeting Times *</label>
1017 <meeting-time-picker
1018 .value=${this.meetingTimes}
1019 @change=${this.handleMeetingTimesChange}
1020 ></meeting-time-picker>
1021 </div>
1022 </div>
1023
1024 <div class="modal-actions">
1025 <button class="btn-cancel" @click=${this.cancelApproval}>
1026 Cancel
1027 </button>
1028 <button
1029 class="btn-submit"
1030 @click=${this.submitApproval}
1031 ?disabled=${this.meetingTimes.length === 0}
1032 >
1033 Create Class
1034 </button>
1035 </div>
1036 </div>
1037 </div>
1038 `;
1039 }
1040}