From d6194d84aad46421c91fe9a96b793484dfb3ce3c Mon Sep 17 00:00:00 2001 From: qingyuan Date: Fri, 30 Jan 2026 11:18:07 -0500 Subject: [PATCH 1/6] added supervised teaching page fuctionality --- src/app/applications/applicationSections.tsx | 7 + .../applications/supervisedTeaching/page.tsx | 438 ++++++++++++++++++ src/hooks/useGetItems.ts | 6 + src/types/navigation.ts | 2 +- 4 files changed, 452 insertions(+), 1 deletion(-) create mode 100644 src/app/applications/supervisedTeaching/page.tsx diff --git a/src/app/applications/applicationSections.tsx b/src/app/applications/applicationSections.tsx index 487bfce..114601b 100644 --- a/src/app/applications/applicationSections.tsx +++ b/src/app/applications/applicationSections.tsx @@ -43,8 +43,15 @@ export default function ApplicationSections({ .map(({ label, to, icon: Icon }: NavbarItem) => ( ))} + + {navItems + .filter((item) => item.type === 'supervised-teaching') + .map(({ label, to, icon: Icon }: NavbarItem) => ( + + ))} +

Research

No available applications at this time.

diff --git a/src/app/applications/supervisedTeaching/page.tsx b/src/app/applications/supervisedTeaching/page.tsx new file mode 100644 index 0000000..ad278db --- /dev/null +++ b/src/app/applications/supervisedTeaching/page.tsx @@ -0,0 +1,438 @@ +'use client'; +import * as React from 'react'; +import Button from '@mui/material/Button'; +import CssBaseline from '@mui/material/CssBaseline'; +import TextField from '@mui/material/TextField'; +import Grid from '@mui/material/Grid'; +import Box from '@mui/material/Box'; +import DepartmentSelect from '@/component/FormUtil/DepartmentSelect'; +import GPA_Select from '@/component/FormUtil/GPASelect'; +import Typography from '@mui/material/Typography'; +import DegreeSelect from '@/component/FormUtil/DegreeSelect'; +import SemesterStatusSelect from '@/component/FormUtil/SemesterStatusSelect'; +import AvailabilityCheckbox from '@/component/FormUtil/AvailabilityCheckbox'; +import AdditionalSemesterPrompt from '@/component/FormUtil/AddtlSemesterPrompt'; +import UpdateRole from '@/firebase/util/UpdateUserRole'; +import { useAuth } from '@/firebase/auth/auth_context'; +import { Toaster, toast } from 'react-hot-toast'; +import Snackbar from '@mui/material/Snackbar'; +import MuiAlert, { AlertProps } from '@mui/material/Alert'; +import 'firebase/firestore'; +import firebase from '@/firebase/firebase_config'; +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import FormControl from '@mui/material/FormControl'; +import Autocomplete from '@mui/material/Autocomplete'; +import HeaderCard from '@/components/HeaderCard/HeaderCard'; +import { + fetchClosestSemesters, + parseCoursesMinimal, +} from '@/hooks/useSemesterOptions'; +import { CourseOption } from '@/hooks/useSemesterOptions'; + +export default function SupervisedTeachingApplication() { + const router = useRouter(); + const { user } = useAuth(); + const userId = user.uid; + + const current = new Date(); + const current_date = `${ + current.getMonth() + 1 + }-${current.getDate()}-${current.getFullYear()}`; + + const [nationality, setNationality] = React.useState(null); + const [additionalPromptValue, setAdditionalPromptValue] = React.useState(''); + const handleAdditionalPromptChange = (newValue: string) => { + setAdditionalPromptValue(newValue); + }; + const [loading, setLoading] = useState(false); + + const handleSubmit = async (event: React.FormEvent) => { + setLoading(true); + event.preventDefault(); + const formData = new FormData(event.currentTarget); + + const availabilityCheckbox_seven = + formData.get('availabilityCheckbox_seven') === 'on'; + const availabilityCheckbox_fourteen = + formData.get('availabilityCheckbox_fourteen') === 'on'; + const availabilityCheckbox_twenty = + formData.get('availabilityCheckbox_twenty') === 'on'; + + const availabilityArray: string[] = []; + if (availabilityCheckbox_seven) availabilityArray.push('7'); + if (availabilityCheckbox_fourteen) availabilityArray.push('14'); + if (availabilityCheckbox_twenty) availabilityArray.push('20'); + + const semesterArray: string[] = []; + semesterArray.push(...(await fetchClosestSemesters(1))); + + const coursesArray = selectedCourses; + let coursesMap: { [key: string]: string } = {}; + for (let i = 0; i < coursesArray.length; i++) { + coursesMap[coursesArray[i]] = 'applied'; + } + + const applicationData = { + firstname: formData.get('firstName') as string, + lastname: formData.get('lastName') as string, + email: formData.get('email') as string, + ufid: formData.get('ufid') as string, + phonenumber: formData.get('phone-number') as string, + gpa: formData.get('gpa-select') as string, + department: formData.get('department-select') as string, + degree: formData.get('degrees-radio-group') as string, + semesterstatus: formData.get('semstatus-radio-group') as string, + additionalprompt: additionalPromptValue, + nationality: nationality as string, + englishproficiency: 'NA', + position: 'Supervised Teaching', + available_hours: availabilityArray as string[], + available_semesters: semesterArray as string[], + courses: coursesMap, + qualifications: formData.get('qualifications-prompt') as string, + uid: userId, + date: current_date, + status: 'Submitted', + resume_link: formData.get('resumeLink') as string, + }; + + if (!applicationData.email.includes('ufl.edu')) { + toast.error('Please enter a valid ufl email!'); + setLoading(false); + return; + } else if (applicationData.firstname === '') { + toast.error('Please enter a valid first name!'); + setLoading(false); + return; + } else if (applicationData.lastname === '') { + toast.error('Please enter a valid last name!'); + setLoading(false); + return; + } else if (applicationData.ufid == '') { + toast.error('Please enter a valid ufid!'); + setLoading(false); + return; + } else if (applicationData.phonenumber === '') { + toast.error('Please enter a valid phone number!'); + setLoading(false); + return; + } else if ( + applicationData.degree === null || + applicationData.degree === '' + ) { + toast.error('Please select a degree!'); + setLoading(false); + return; + } else if ( + applicationData.department === null || + applicationData.department === '' + ) { + toast.error('Please select a department!'); + setLoading(false); + return; + } else if ( + applicationData.semesterstatus === null || + applicationData.semesterstatus === '' + ) { + toast.error('Please select a semester status!'); + setLoading(false); + return; + } else if ( + applicationData.resume_link === null || + applicationData.resume_link === '' + ) { + toast.error('Please provide a resume link!'); + setLoading(false); + return; + } else if (applicationData.available_hours.length == 0) { + toast.error('Please enter your available hours!'); + setLoading(false); + return; + } else if (applicationData.available_semesters.length == 0) { + toast.error('Please enter your available semesters!'); + setLoading(false); + return; + } else if (coursesArray.length == 0) { + toast.error('Please enter your courses!'); + setLoading(false); + return; + } else { + const toastId = toast.loading('Processing application', { + duration: 30000, + }); + await firebase.firestore().collection('assignments').doc(userId).delete(); + + const response = await fetch( + 'https://us-central1-courseconnect-c6a7b.cloudfunctions.net/processApplicationForm', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(applicationData), + } + ); + + if (response.ok) { + toast.dismiss(toastId); + toast.success('Application submitted!'); + await UpdateRole(userId, 'student_applied'); + router.push('/'); + } else { + toast.dismiss(toastId); + toast.error('Application data failed to send to server!'); + console.log('ERROR: Application data failed to send to server'); + } + setLoading(false); + } + }; + + const [success, setSuccess] = React.useState(false); + const Alert = React.forwardRef(function Alert( + props, + ref + ) { + return ; + }); + + const handleSuccess = ( + event?: React.SyntheticEvent | Event, + reason?: string + ) => { + if (reason === 'clickaway') return; + setSuccess(false); + }; + + const [selectedCourses, setSelectedCourses] = React.useState([]); + const [names, setNames] = useState([]); + + React.useEffect(() => { + async function fetchData() { + try { + let data: string[] = []; + let visibleSems: string[] = await fetchClosestSemesters(1); + await firebase + .firestore() + .collection('semesters') + .doc(visibleSems[0]) + .collection('courses') + .get() + .then((snapshot) => + snapshot.docs.map((doc) => { + if (visibleSems.includes(doc.data().semester)) { + data.push(doc.id); + } + }) + ); + setNames(data); + } catch (err) { + console.log(err); + } + } + fetchData(); + }, []); + + const courseOptions = parseCoursesMinimal(names); + + return ( + <> + + + + + Application submitted successfully! + + + + + + + + Personal Information + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Supervision Information + +
+ + + + Please list the course(s) for which you are interested in + supervised teaching. Ensure that you select the courses with + your desired semester and instructor. + +
+ + + multiple + disableCloseOnSelect + options={courseOptions.sort( + (a, b) => -b.code.localeCompare(a.code) + )} + groupBy={(o) => o.department} + getOptionLabel={(o) => o.name} + value={courseOptions.filter((o) => + selectedCourses.includes(o.value) + )} + onChange={(_, vals) => + setSelectedCourses(vals.map((v) => v.value)) + } + isOptionEqualToValue={(opt, val) => opt.value === val.value} + renderInput={(params) => ( + + )} + /> + +
+ + + Please provide your most recently calculated cumulative UF + GPA. + +
+ +
+ + + Please upload a google drive link to your resume. + + + + + + Please describe your qualifications for supervised teaching, + including any teaching, grading, or tutoring experience. If + applicable, mention courses and instructors you have worked + with. + + + +
+ + +
+
+
+ + ); +} diff --git a/src/hooks/useGetItems.ts b/src/hooks/useGetItems.ts index 243fac0..14113a1 100644 --- a/src/hooks/useGetItems.ts +++ b/src/hooks/useGetItems.ts @@ -95,6 +95,12 @@ export const getApplications = (userRole: Role): NavbarItem[] => { icon: FolderOutlinedIcon, type: 'ta', }, + { + label: 'Supervised Teaching', + to: '/applications/supervisedTeaching', + icon: FolderOutlinedIcon, + type: 'supervised-teaching', + }, ]; /* ────────────────────────────────── Faculty ────────────────────────────────── */ diff --git a/src/types/navigation.ts b/src/types/navigation.ts index 4e7c13c..4185820 100644 --- a/src/types/navigation.ts +++ b/src/types/navigation.ts @@ -1,6 +1,6 @@ import { SvgIconComponent } from '@mui/icons-material'; -export type CardType = 'research' | 'ta'; +export type CardType = 'research' | 'ta' | 'supervised-teaching'; export type QueryParams = Record; export type NavbarItem = { From 13a7860ab36d7f3dd88c489b81b64b5271f7788b Mon Sep 17 00:00:00 2001 From: ThomasOli Date: Sat, 31 Jan 2026 14:22:28 -0500 Subject: [PATCH 2/6] add new settings --- .claude/settings.local.json | 3 +- .gitignore | 3 +- CLAUDE.md | 76 +++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 CLAUDE.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e587e28..910d483 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,8 @@ { "permissions": { "allow": [ - "Bash(cd:*)" + "Bash(cd:*)", + "Bash(git -C \"C:\\\\Users\\\\Oling\\\\documents\\\\courseconnect\" log --oneline -20)" ] } } diff --git a/.gitignore b/.gitignore index fb3a957..66361ae 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,5 @@ courseconnect-c6a7b-firebase-adminsdk-dqqis-af57e2e045.json playwright-report test-results playwright/.auth -playwright.accounts.json \ No newline at end of file +playwright.accounts.json +nul diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..724698e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,76 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +CourseConnect is a web application for managing teaching position applications (TAs, UPIs, Graders) and course information. It serves students (submit/track applications), faculty (review applications, manage courses), and department leaders (manage users/courses, upload data via spreadsheets). + +## Tech Stack + +- **Frontend:** React 19, Next.js 15 (App Router), TypeScript +- **UI:** Material UI (MUI) 6, Tailwind CSS 4 +- **Backend:** Firebase/Firestore, Firebase Cloud Functions +- **State:** React Query (TanStack), React Context +- **Forms:** React Hook Form + +## Common Commands + +```bash +# Development +npm run dev # Start dev server at localhost:3000 +npm run build # Production build +npm run start # Start production server +npm run lint # Run ESLint + +# Testing (Playwright E2E) +npm run test:e2e # Run all E2E tests +npm run test:e2e:ui # Run tests with Playwright UI +npm run test:e2e:debug # Debug tests +npx playwright show-report # View test report after failure + +# Firebase Functions (from /functions directory) +npm run build # Compile TypeScript +npm run serve # Run functions locally with emulator +npm run deploy # Deploy functions to Firebase +``` + +## Architecture + +``` +src/ +├── app/ # Next.js App Router pages +│ ├── admin-applications/ # Admin: manage all applications +│ ├── admincourses/ # Admin: course management +│ ├── announcements/ # Announcements feature +│ ├── applications/ # Student application submission +│ ├── courses/ # Course viewing +│ ├── dashboard/ # Main dashboard +│ ├── faculty/ # Faculty management +│ ├── users/ # Admin: user management +│ └── layout.tsx # Root layout with providers +├── components/ # Reusable React components +├── contexts/ # React contexts (Auth, Announcements) +├── firebase/ # Firebase config and utilities +├── hooks/ # Custom React hooks (useGetItems, etc.) +├── types/ # TypeScript type definitions +└── utils/ # Utility functions + +functions/ # Firebase Cloud Functions (email notifications) +tests/e2e/ # Playwright E2E tests +``` + +## Key Patterns + +- **Path alias:** Use `@/*` to import from `src/*` +- **Providers:** AuthProvider and AnnouncementsProvider wrap the app in `layout.tsx` +- **Data fetching:** React Query hooks in `src/hooks/` for Firestore operations +- **E2E test mode:** Feature flags via `NEXT_PUBLIC_E2E` env var and localStorage + +## Pre-commit Hooks + +Husky runs ESLint and Prettier on staged files automatically. If commits fail, check lint errors with `npm run lint`. + +## Firebase Functions + +Cloud Functions in `functions/src/index.ts` handle email notifications (application confirmations, status updates, faculty notifications). Uses Nodemailer with configured SMTP credentials. From dd4c20daa7b93ba483a900e3cd11e7550d461a22 Mon Sep 17 00:00:00 2001 From: ThomasOli Date: Sat, 31 Jan 2026 14:24:27 -0500 Subject: [PATCH 3/6] Revert "Merge branch 'main' into develop" This reverts commit 225a3cbaf1113a7eedd4a43a7512472c1b9d1a7a, reversing changes made to 67a613b613611de91ef71d07f9cdbc02091b20b7. --- .../announcements/AnnouncementSections.tsx | 38 +- src/app/announcements/page.tsx | 3 +- src/app/layout.tsx | 9 +- src/app/users/page.tsx | 206 +++++--- .../Dashboard/Users/ApprovalGrid.tsx | 444 +++++++++-------- src/component/Dashboard/Users/UserGrid.tsx | 460 ++++++++++-------- src/components/TopBar/TopBar.tsx | 24 +- src/contexts/AnnouncementsContext.tsx | 204 -------- 8 files changed, 693 insertions(+), 695 deletions(-) delete mode 100644 src/contexts/AnnouncementsContext.tsx diff --git a/src/app/announcements/AnnouncementSections.tsx b/src/app/announcements/AnnouncementSections.tsx index bada238..898fae8 100644 --- a/src/app/announcements/AnnouncementSections.tsx +++ b/src/app/announcements/AnnouncementSections.tsx @@ -1,17 +1,24 @@ /* components/DashboardSections.tsx */ import { PrimaryButton } from '@/components/Buttons/PrimaryButton'; +import { NavbarItem } from '@/types/navigation'; import { Role } from '@/types/User'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import AnnouncementDialog from './AnnouncementDialogue'; import AnnouncementsRow from './AnnouncementsRow'; import { usePostAnnouncement } from '@/hooks/Announcements/usePostAnnouncement'; -import { useAnnouncements } from '@/contexts/AnnouncementsContext'; +import { useFetchAnnouncementsForAccount } from '@/hooks/Announcements/useFetchAnnouncements'; -import { Announcement } from '@/types/announcement'; +import { Announcement, AudienceRole } from '@/types/announcement'; -export default function AnnouncementSections({ role }: { role: Role }) { +export default function AnnouncementSections({ + role, + uemail, +}: { + role: Role; + uemail: string; +}) { const [open, setOpen] = useState(false); const { postAnnouncement, posting, error: postError } = usePostAnnouncement(); @@ -19,9 +26,18 @@ export default function AnnouncementSections({ role }: { role: Role }) { read, unread, loading, + loadingMore, + hasMore, error: fetchError, refresh, - } = useAnnouncements(); + loadMore, + } = useFetchAnnouncementsForAccount({ + userRole: role, + userEmail: uemail, + userDepartment: 'ECE', // TODO: make real + channel: 'inApp', + realtime: true, + }); async function handleSubmit(draft: Announcement) { await postAnnouncement({ @@ -125,6 +141,18 @@ export default function AnnouncementSections({ role }: { role: Role }) { )}
)} + + {hasMore ? ( +
+ +
+ ) : null} ); } diff --git a/src/app/announcements/page.tsx b/src/app/announcements/page.tsx index af8be83..5f72826 100644 --- a/src/app/announcements/page.tsx +++ b/src/app/announcements/page.tsx @@ -9,6 +9,7 @@ import { markAnnouncementsSeen } from '@/hooks/Announcements/markAnnouncementAsS const AnnouncementsPage: FC = () => { const [user, role, loading, error] = useUserInfo(); + const uemail = user?.email; // prevent double-call in dev Strict Mode const didMarkRef = useRef(false); @@ -26,7 +27,7 @@ const AnnouncementsPage: FC = () => { return ( - + ); }; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6a67f9f..3ec96b6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,5 @@ 'use client'; import { AuthProvider } from '@/firebase/auth/auth_context'; -import { AnnouncementsProvider } from '@/contexts/AnnouncementsContext'; import React, { useEffect } from 'react'; import CssBaseline from '@mui/material/CssBaseline'; import { ThemeProvider, createTheme } from '@mui/material/styles'; @@ -68,11 +67,9 @@ export default function RootLayout({ theme={mode === 'dark' ? darkThemeChosen : lightThemeChosen} > - - - {/*
*/} - {children} - + + {/*
*/} + {children} diff --git a/src/app/users/page.tsx b/src/app/users/page.tsx index 2c9acf2..952a61c 100644 --- a/src/app/users/page.tsx +++ b/src/app/users/page.tsx @@ -1,94 +1,152 @@ 'use client'; import * as React from 'react'; -import { FC } from 'react'; - +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; import CssBaseline from '@mui/material/CssBaseline'; +import TextField from '@mui/material/TextField'; +import Grid from '@mui/material/Grid'; import Box from '@mui/material/Box'; -import Paper from '@mui/material/Paper'; -import Tabs from '@mui/material/Tabs'; -import Tab from '@mui/material/Tab'; +import EditNoteIcon from '@mui/icons-material/EditNote'; +import DepartmentSelect from '@/component/FormUtil/DepartmentSelect'; +import GPA_Select from '@/component/FormUtil/GPASelect'; import Typography from '@mui/material/Typography'; - -import { Toaster } from 'react-hot-toast'; - -import PageLayout from '@/components/PageLayout/PageLayout'; -import { getNavItems } from '@/hooks/useGetItems'; - +import Container from '@mui/material/Container'; +import DegreeSelect from '@/component/FormUtil/DegreeSelect'; +import SemesterStatusSelect from '@/component/FormUtil/SemesterStatusSelect'; +import NationalitySelect from '@/component/FormUtil/NationalitySelect'; +import ProficiencySelect from '@/component/FormUtil/ProficiencySelect'; +import PositionSelect from '@/component/FormUtil/PositionSelect'; +import AvailabilityCheckbox from '@/component/FormUtil/AvailabilityCheckbox'; +import SemesterCheckbox from '@/component/FormUtil/SemesterCheckbox'; +import AdditionalSemesterPrompt from '@/component/FormUtil/AddtlSemesterPrompt'; +import UpdateRole from '@/firebase/util/UpdateUserRole'; import { useAuth } from '@/firebase/auth/auth_context'; +import { Toaster, toast } from 'react-hot-toast'; +import { LinearProgress } from '@mui/material'; +import Snackbar from '@mui/material/Snackbar'; +import MuiAlert, { AlertProps } from '@mui/material/Alert'; +import { ApplicationStatusCard } from '@/components/ApplicationStatusCard/ApplicationStatusCard'; +import { useState } from 'react'; +import { TopNavBarSigned } from '@/component/TopNavBarSigned/TopNavBarSigned'; +import { EceLogoPng } from '@/component/EceLogoPng/EceLogoPng'; +import Users from '@/component/Dashboard/Users/Users'; + import GetUserRole from '@/firebase/util/GetUserRole'; -import UserGrid from '@/component/Dashboard/Users/UserGrid'; -import ApprovalGrid from '@/component/Dashboard/Users/ApprovalGrid'; +import GetUserUfid from '@/firebase/util/GetUserUfid'; +import { ApplicationStatusCardDenied } from '@/components/ApplicationStatusCardDenied/ApplicationStatusCardDenied'; -interface PageProps { } +import { ApplicationStatusCardAccepted } from '@/components/ApplicationStatusCardAccepted/ApplicationStatusCardAccepted'; +import styles from './style.module.css'; +import 'firebase/firestore'; -const User: FC = () => { +import firebase from '@/firebase/firebase_config'; +import { read, utils, writeFile, readFile } from 'xlsx'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import FormControl from '@mui/material/FormControl'; +import Select, { SelectChangeEvent } from '@mui/material/Select'; +import HeaderCard from '@/component/HeaderCard/HeaderCard'; + +export default function User() { const { user } = useAuth(); const [role, loading, error] = GetUserRole(user?.uid); + const [activeComponent, setActiveComponent] = React.useState('welcome'); + const [semester, setSemester] = React.useState('Fall 2024'); + + const handleChange = (event: SelectChangeEvent) => { + setSemester(event.target.value as string); + }; + + const readExcelFile = async (e) => { + // https://docs.sheetjs.com/docs/demos/local/file/ + console.log('ACTIVE'); + + try { + const val = e.target.files[0]; + const ab = await val.arrayBuffer(); + let data = []; + var file = read(ab); - // 0 = All Users, 1 = Unapproved Users - const [tab, setTab] = React.useState(0); + const sheets = file.SheetNames; - if (loading) return
Loading…
; + for (let i = 0; i < sheets.length; i++) { + const temp = utils.sheet_to_json(file.Sheets[file.SheetNames[i]]); + + temp.forEach((res) => { + data.push(res); + }); + } + for (let i = 0; i < data.length; i++) { + await firebase + .firestore() + .collection('courses') + .doc( + data[i]['__EMPTY_5'] + + ' (' + + semester + + ') ' + + ': ' + + data[i]['__EMPTY_22'] + ) + .set({ + professor_emails: + data[i]['__EMPTY_23'] == undefined + ? 'undef' + : data[i]['__EMPTY_23'], + professor_names: + data[i]['__EMPTY_22'] == undefined + ? 'undef' + : data[i]['__EMPTY_22'], + code: + data[i]['__EMPTY_5'] == undefined + ? 'undef' + : data[i]['__EMPTY_5'], + credits: + data[i]['__EMPTY_9'] == undefined + ? 'undef' + : data[i]['__EMPTY_9'], + enrollment_cap: + data[i]['__EMPTY_24'] == undefined + ? 'undef' + : data[i]['__EMPTY_24'], + enrolled: + data[i]['__EMPTY_26'] == undefined + ? 'undef' + : data[i]['__EMPTY_26'], + title: + data[i]['__EMPTY_21'] == undefined + ? 'undef' + : data[i]['__EMPTY_21'], + semester: semester, + }); + } + } catch (err) { + console.log(err); + } + }; + if (loading) return
Loadingƒ?İ
; if (error) return
Error loading role
; - if (role !== 'admin') return
Forbidden
; + if (role !== 'admin') return
Forbidden
; return ( - - + <> + - {/* Tabs Container */} - - - setTab(newValue)} - aria-label="users view switch" - TabIndicatorProps={{ - style: { - height: 3, - borderRadius: 2, - }, - }} - > - - - - - - - {/* Content */} - {tab === 0 ? ( - - - - ) : ( - - + + + + - )} - + + ); -}; - -export default User; - +} diff --git a/src/component/Dashboard/Users/ApprovalGrid.tsx b/src/component/Dashboard/Users/ApprovalGrid.tsx index f8cbe9e..d4e24ec 100644 --- a/src/component/Dashboard/Users/ApprovalGrid.tsx +++ b/src/component/Dashboard/Users/ApprovalGrid.tsx @@ -1,13 +1,11 @@ - 'use client'; - import * as React from 'react'; import Box from '@mui/material/Box'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/DeleteOutlined'; +import ThumbUpOffAltIcon from '@mui/icons-material/ThumbUpOffAlt'; import CancelIcon from '@mui/icons-material/Close'; import SaveIcon from '@mui/icons-material/Save'; -import { ThumbDownOffAlt, ThumbUpOffAlt } from '@mui/icons-material'; import { GridRowModesModel, @@ -24,16 +22,17 @@ import { GridRowId, GridRowModel, GridRowEditStopReasons, + useGridApiContext, gridClasses, } from '@mui/x-data-grid'; - -import { LinearProgress } from '@mui/material'; -import { styled } from '@mui/material/styles'; - +import { Button } from '@mui/material'; import { deleteUserHTTPRequest } from '@/firebase/auth/auth_delete_user'; import firebase from '@/firebase/firebase_config'; import 'firebase/firestore'; - +import { LinearProgress } from '@mui/material'; +import { alpha, styled } from '@mui/material/styles'; +import CheckIcon from '@mui/icons-material/Check'; +import { ThumbDownOffAlt, ThumbUp, ThumbUpOffAlt } from '@mui/icons-material'; interface User { id: string; firstname: string; @@ -55,13 +54,17 @@ interface EditToolbarProps { ) => void; } -function EditToolbar(_props: EditToolbarProps) { - // ✅ match CourseGrid toolbar (default styling) +function EditToolbar(props: EditToolbarProps) { + const { setApplicationData, setRowModesModel } = props; + + // Add state to control the dialog open status + const [open, setOpen] = React.useState(false); + return ( - - - + + + ); } @@ -72,8 +75,6 @@ interface ApprovalGridProps { export default function ApprovalGrid(props: ApprovalGridProps) { const { userRole } = props; - - const [loading, setLoading] = React.useState(false); const [userData, setUserData] = React.useState([]); React.useEffect(() => { @@ -81,15 +82,14 @@ export default function ApprovalGrid(props: ApprovalGridProps) { .firestore() .collection('users') .where('role', '==', 'unapproved'); - const unsubscribe = usersRef.onSnapshot((querySnapshot) => { const data = querySnapshot.docs.map( (doc) => - ({ - id: doc.id, - fullname: `${doc.data().firstname ?? ''} ${doc.data().lastname ?? ''}`, - ...doc.data(), - } as User) + ({ + id: doc.id, + fullname: doc.data().firstname + ' ' + doc.data().lastname, + ...doc.data(), + } as User) ); setUserData(data); @@ -112,127 +112,164 @@ export default function ApprovalGrid(props: ApprovalGridProps) { }; const handleEditClick = (id: GridRowId) => () => { - setLoading(true); setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); - setLoading(false); }; - const handleSaveClick = (id: GridRowId) => async () => { - setLoading(true); - try { - const updatedRow = userData.find((row) => row.id === id); - if (!updatedRow) throw new Error(`No matching user data found for id: ${id}`); - - await firebase.firestore().collection('users').doc(id.toString()).update(updatedRow); - - setRowModesModel({ - ...rowModesModel, - [id]: { mode: GridRowModes.View }, - }); - } catch (error) { - console.error('Error updating document: ', error); - } finally { - setLoading(false); + const handleSaveClick = (id: GridRowId) => () => { + const updatedRow = userData.find((row) => row.id === id); + if (updatedRow) { + firebase + .firestore() + .collection('users') + .doc(id.toString()) + .update(updatedRow) + .then(() => { + setRowModesModel({ + ...rowModesModel, + [id]: { mode: GridRowModes.View }, + }); + }) + .catch((error) => { + console.error('Error updating document: ', error); + }); + } else { + console.error('No matching user data found for id: ', id); } }; - const handleApproveClick = (id: GridRowId) => async () => { - setLoading(true); - try { - await firebase.firestore().collection('users').doc(id.toString()).update({ role: 'faculty' }); - setRowModesModel({ - ...rowModesModel, - [id]: { mode: GridRowModes.View }, - }); - } catch (error) { - console.error('Error updating document: ', error); - } finally { - setLoading(false); + const handleApproveClick = (id: GridRowId) => () => { + const updatedRow = userData.find((row) => row.id === id); + if (updatedRow) { + firebase + .firestore() + .collection('users') + .doc(id.toString()) + .update({ role: 'faculty' }) + .then(() => { + setRowModesModel({ + ...rowModesModel, + [id]: { mode: GridRowModes.View }, + }); + }) + .catch((error) => { + console.error('Error updating document: ', error); + }); + } else { + console.error('No matching user data found for id: ', id); } }; - const handleDenyClick = (id: GridRowId) => async () => { - setLoading(true); - try { - await firebase.firestore().collection('users').doc(id.toString()).update({ role: 'denied' }); - setRowModesModel({ - ...rowModesModel, - [id]: { mode: GridRowModes.View }, - }); - } catch (error) { - console.error('Error updating document: ', error); - } finally { - setLoading(false); + const handleDenyClick = (id: GridRowId) => () => { + const updatedRow = userData.find((row) => row.id === id); + if (updatedRow) { + firebase + .firestore() + .collection('users') + .doc(id.toString()) + .update({ role: 'denied' }) + .then(() => { + setRowModesModel({ + ...rowModesModel, + [id]: { mode: GridRowModes.View }, + }); + }) + .catch((error) => { + console.error('Error updating document: ', error); + }); + } else { + console.error('No matching user data found for id: ', id); } }; - const handleDeleteClick = (id: GridRowId) => async () => { - setLoading(true); - try { - await firebase.firestore().collection('users').doc(id.toString()).delete(); - deleteUserHTTPRequest(id.toString()); - setUserData((prev) => prev.filter((row) => row.id !== id)); - } catch (error) { - console.error('Error removing document: ', error); - } finally { - setLoading(false); - } + const handleDeleteClick = (id: GridRowId) => () => { + firebase + .firestore() + .collection('users') + .doc(id.toString()) + .delete() + .then(() => { + deleteUserHTTPRequest(id.toString()); + setUserData(userData.filter((row) => row.id !== id)); + }) + .catch((error) => { + console.error('Error removing document: ', error); + }); }; - const handleCancelClick = (id: GridRowId) => async () => { - setLoading(true); - try { - const editedRow = userData.find((row) => row.id === id); - if (editedRow?.isNew) { - await firebase.firestore().collection('users').doc(id.toString()).delete(); - setUserData((prev) => prev.filter((row) => row.id !== id)); - } else { - setRowModesModel({ - ...rowModesModel, - [id]: { mode: GridRowModes.View, ignoreModifications: true }, + const handleCancelClick = (id: GridRowId) => () => { + const editedRow = userData.find((row) => row.id === id); + if (editedRow!.isNew) { + firebase + .firestore() + .collection('users') + .doc(id.toString()) + .delete() + .then(() => { + setUserData(userData.filter((row) => row.id !== id)); + }) + .catch((error) => { + console.error('Error removing document: ', error); }); - } - } catch (error) { - console.error('Error removing document: ', error); - } finally { - setLoading(false); + } else { + setRowModesModel({ + ...rowModesModel, + [id]: { mode: GridRowModes.View, ignoreModifications: true }, + }); } }; - const processRowUpdate = async (newRow: GridRowModel) => { - setLoading(true); - try { - const updatedRow = { ...(newRow as User), isNew: false }; - - if ((updatedRow as any).isNew) { - await firebase.firestore().collection('users').add(updatedRow); + const processRowUpdate = (newRow: GridRowModel) => { + const updatedRow = { ...(newRow as User), isNew: false }; + if (updatedRow) { + if (updatedRow.isNew) { + return firebase + .firestore() + .collection('users') + .add(updatedRow) + .then(() => { + setUserData( + userData.map((row) => (row.id === newRow.id ? updatedRow : row)) + ); + return updatedRow; + }) + .catch((error) => { + console.error('Error adding document: ', error); + throw error; + }); } else { - await firebase.firestore().collection('users').doc(updatedRow.id).update(updatedRow); + return firebase + .firestore() + .collection('users') + .doc(updatedRow.id) + .update(updatedRow) + .then(() => { + setUserData( + userData.map((row) => (row.id === newRow.id ? updatedRow : row)) + ); + return updatedRow; + }) + .catch((error) => { + console.error('Error updating document: ', error); + throw error; + }); } - - setUserData((prev) => - prev.map((row) => (row.id === newRow.id ? (updatedRow as User) : row)) + } else { + return Promise.reject( + new Error('No matching user data found for id: ' + newRow.id) ); - - return updatedRow; - } catch (error) { - console.error('Error processing row update: ', error); - throw error; - } finally { - setLoading(false); } }; - const columns: GridColDef[] = [ - { field: 'fullname', headerName: 'Full Name', width: 202, editable: true }, - { field: 'email', headerName: 'Email', width: 215, editable: true }, - { field: 'department', headerName: 'Department', width: 119, editable: true }, - { field: 'role', headerName: 'Role', width: 150, editable: true }, + const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { + setRowModesModel(newRowModesModel); + }; + + let columns: GridColDef[] = [ { field: 'actions', type: 'actions', headerName: 'Actions', - width: 180, + width: 280, cellClassName: 'actions', getActions: ({ id }) => { const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; @@ -240,17 +277,18 @@ export default function ApprovalGrid(props: ApprovalGridProps) { if (isInEditMode) { return [ } label="Save" - sx={{ color: 'primary.main' }} + sx={{ + color: 'primary.main', + }} onClick={handleSaveClick(id)} />, } label="Cancel" - className="textPrimary" onClick={handleCancelClick(id)} color="inherit" />, @@ -258,30 +296,42 @@ export default function ApprovalGrid(props: ApprovalGridProps) { } return [ - } - label="Edit" - className="textPrimary" - onClick={handleEditClick(id)} + , + + , } label="Approve" onClick={handleApproveClick(id)} color="success" />, } label="Deny" onClick={handleDenyClick(id)} @@ -290,94 +340,104 @@ export default function ApprovalGrid(props: ApprovalGridProps) { ]; }, }, - ]; - - // ✅ Copy CourseGrid UI styling - const StripedDataGrid = styled(DataGrid)(() => ({ - border: 'none', - borderRadius: '16px', - fontFamily: 'Inter, sans-serif', - fontSize: '0.95rem', - - '& .MuiDataGrid-columnHeaders': { - backgroundColor: '#D8C6F8', - color: '#1C003D', - fontWeight: 700, - borderBottom: 'none', + { + field: 'fullname', + headerName: 'Full Name', + width: 202, + editable: true, }, - - '& .MuiDataGrid-columnHeaderTitle': { - fontWeight: 700, + { field: 'email', headerName: 'Email', width: 215, editable: true }, + { + field: 'department', + headerName: 'Department', + width: 119, + editable: true, }, - - '& .MuiDataGrid-columnHeader:first-of-type': { - paddingLeft: '20px', + { + field: 'courses', + headerName: 'Course Code', + width: 300, + editable: true, }, - '& .MuiDataGrid-cell:first-of-type': { - paddingLeft: '25px', + { + field: 'semester', + headerName: 'Semester', + width: 130, + editable: true, }, + { field: 'role', headerName: 'Role', width: 100, editable: true }, + ]; + const ODD_OPACITY = 0.2; + const StripedDataGrid = styled(DataGrid)(({ theme }) => ({ [`& .${gridClasses.row}.even`]: { - backgroundColor: '#FFFFFF', - }, - [`& .${gridClasses.row}.odd`]: { - backgroundColor: '#EEEEEE', - }, - - '& .MuiDataGrid-row:hover': { - backgroundColor: '#EFE6FF', - }, - - '& .MuiDataGrid-cell': { - borderBottom: '1px solid #ECE4FA', - }, - - '& .MuiDataGrid-footerContainer': { - borderTop: 'none', - }, - - '& .MuiTablePagination-root': { - color: '#5D3FC4', - fontWeight: 500, + backgroundColor: '#562EBA1F', + '&:hover, &.Mui-hovered': { + backgroundColor: alpha(theme.palette.primary.main, ODD_OPACITY), + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + }, + '&.Mui-selected': { + backgroundColor: alpha( + theme.palette.primary.main, + ODD_OPACITY + theme.palette.action.selectedOpacity + ), + '&:hover, &.Mui-hovered': { + backgroundColor: alpha( + theme.palette.primary.main, + ODD_OPACITY + + theme.palette.action.selectedOpacity + + theme.palette.action.hoverOpacity + ), + // Reset on touch devices, it doesn't add specificity + '@media (hover: none)': { + backgroundColor: alpha( + theme.palette.primary.main, + ODD_OPACITY + theme.palette.action.selectedOpacity + ), + }, + }, + }, }, })); return ( - {loading ? : null} - setRowModesModel(m)} + onRowModesModelChange={handleRowModesModelChange} onRowEditStop={handleRowEditStop} processRowUpdate={processRowUpdate} - onProcessRowUpdateError={(error) => - console.error('Error processing row update: ', error) - } - slots={{ toolbar: EditToolbar }} - slotProps={{ toolbar: { setApplicationData: setUserData, setRowModesModel } }} initialState={{ pagination: { paginationModel: { pageSize: 25 } }, }} getRowClassName={(params) => params.indexRelativeToCurrentPage % 2 === 0 ? 'even' : 'odd' } + sx={{ borderRadius: '16px' }} /> ); } - diff --git a/src/component/Dashboard/Users/UserGrid.tsx b/src/component/Dashboard/Users/UserGrid.tsx index 7fe1f69..dcd31d9 100644 --- a/src/component/Dashboard/Users/UserGrid.tsx +++ b/src/component/Dashboard/Users/UserGrid.tsx @@ -1,5 +1,4 @@ 'use client'; - import * as React from 'react'; import Box from '@mui/material/Box'; import EditIcon from '@mui/icons-material/Edit'; @@ -21,6 +20,7 @@ import { GridRowId, GridRowModel, GridRowEditStopReasons, + useGridApiContext, gridClasses, } from '@mui/x-data-grid'; import { @@ -29,14 +29,13 @@ import { DialogContent, DialogContentText, DialogTitle, - LinearProgress, - Button, + TextField, } from '@mui/material'; -import { styled } from '@mui/material/styles'; - +import { deleteUserHTTPRequest } from '@/firebase/auth/auth_delete_user'; import firebase from '@/firebase/firebase_config'; import 'firebase/firestore'; -import { deleteUserHTTPRequest } from '@/firebase/auth/auth_delete_user'; +import { LinearProgress, Button } from '@mui/material'; +import { alpha, styled } from '@mui/material/styles'; import { isE2EMode } from '@/utils/featureFlags'; interface User { @@ -59,13 +58,18 @@ interface EditToolbarProps { ) => void; } -function EditToolbar(_props: EditToolbarProps) { - // match CourseGrid toolbar look (default MUI icons/colors) +function EditToolbar(props: EditToolbarProps) { + const { setUserData, setRowModesModel } = props; + + // Add state to control the dialog open status + const [open, setOpen] = React.useState(false); + return ( - - - + {/* Include your Dialog component here and pass the open state and setOpen function as props */} + + + ); } @@ -77,35 +81,35 @@ interface UserGridProps { export default function UserGrid(props: UserGridProps) { const { userRole } = props; const e2e = isE2EMode(); - - const [loading, setLoading] = React.useState(false); const [userData, setUserData] = React.useState([]); - + const [open, setOpen] = React.useState(false); const [delDia, setDelDia] = React.useState(false); - const [delId, setDelId] = React.useState(null); + const [delId, setDelId] = React.useState(); React.useEffect(() => { if (e2e) { setUserData([]); return; } - const usersRef = firebase.firestore().collection('users'); const unsubscribe = usersRef.onSnapshot((querySnapshot) => { const data = querySnapshot.docs.map( (doc) => - ({ - id: doc.id, - fullname: `${doc.data().firstname ?? ''} ${doc.data().lastname ?? ''}`, - ...doc.data(), - } as unknown as User) + ({ + id: doc.id, + fullname: doc.data().firstname + ' ' + doc.data().lastname, + ...doc.data(), + } as unknown as User) ); + setUserData(data); }); return () => unsubscribe(); }, [e2e]); - + const handleDeleteDiagClose = () => { + setDelDia(false); + }; const [rowModesModel, setRowModesModel] = React.useState( {} ); @@ -120,27 +124,32 @@ export default function UserGrid(props: UserGridProps) { }; const handleEditClick = (id: GridRowId) => () => { - setLoading(true); setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); - setLoading(false); }; - const handleSaveClick = (id: GridRowId) => async () => { - setLoading(true); - try { - const updatedRow = userData.find((row) => row.id === id); - if (!updatedRow) throw new Error(`No matching user data for id: ${id}`); - - await firebase.firestore().collection('users').doc(id.toString()).update(updatedRow); - - setRowModesModel({ - ...rowModesModel, - [id]: { mode: GridRowModes.View }, - }); - } catch (err) { - console.error('Error updating document: ', err); - } finally { - setLoading(false); + const handleSaveClick = (id: GridRowId) => () => { + console.log('Clicked Save for ID:', id); + console.log('Current userData:', userData); + + const updatedRow = userData.find((row) => row.id === id); + if (updatedRow) { + firebase + .firestore() + .collection('users') + .doc(id.toString()) + .update(updatedRow) + .then(() => { + console.log('Document successfully updated!'); + setRowModesModel({ + ...rowModesModel, + [id]: { mode: GridRowModes.View }, + }); + }) + .catch((error) => { + console.error('Error updating document: ', error); + }); + } else { + console.error('No matching user data found for id: ', id); } }; @@ -149,89 +158,111 @@ export default function UserGrid(props: UserGridProps) { setDelDia(true); }; - const handleDeleteDiagClose = () => { - setDelDia(false); - setDelId(null); - }; - - const handleDeleteClick = async (id: GridRowId) => { - setLoading(true); - try { - await firebase.firestore().collection('users').doc(id.toString()).delete(); - deleteUserHTTPRequest(id.toString()); - setUserData((prev) => prev.filter((row) => row.id !== id)); - } catch (err) { - console.error('Error removing document: ', err); - } finally { - setLoading(false); - } + const handleDeleteClick = (id: GridRowId) => { + firebase + .firestore() + .collection('users') + .doc(id.toString()) + .delete() + .then(() => { + deleteUserHTTPRequest(id.toString()); + setUserData(userData.filter((row) => row.id !== id)); + }) + .catch((error) => { + console.error('Error removing document: ', error); + }); }; - const handleSubmit = async (e: React.FormEvent) => { + const handleSubmit = (e) => { e.preventDefault(); - if (delId == null) return; - await handleDeleteClick(delId); + console.log(delId.toString()); + handleDeleteClick(delId); setDelDia(false); - setDelId(null); }; - - const handleCancelClick = (id: GridRowId) => async () => { - setLoading(true); - try { - const editedRow = userData.find((row) => row.id === id); - if (editedRow?.isNew) { - await firebase.firestore().collection('users').doc(id.toString()).delete(); - setUserData((prev) => prev.filter((row) => row.id !== id)); - } else { - setRowModesModel({ - ...rowModesModel, - [id]: { mode: GridRowModes.View, ignoreModifications: true }, + function CustomToolbar() { + const apiRef = useGridApiContext(); + + return ( + + + + ); + } + + const handleCancelClick = (id: GridRowId) => () => { + const editedRow = userData.find((row) => row.id === id); + if (editedRow!.isNew) { + firebase + .firestore() + .collection('users') + .doc(id.toString()) + .delete() + .then(() => { + setUserData(userData.filter((row) => row.id !== id)); + }) + .catch((error) => { + console.error('Error removing document: ', error); }); - } - } catch (err) { - console.error('Error canceling edit: ', err); - } finally { - setLoading(false); + } else { + setRowModesModel({ + ...rowModesModel, + [id]: { mode: GridRowModes.View, ignoreModifications: true }, + }); } }; - const processRowUpdate = async (newRow: GridRowModel) => { - setLoading(true); - try { - const updatedRow = { ...(newRow as User), isNew: false }; - - // new rows not really used in your flow, but keep parity with CourseGrid - if ((updatedRow as any).isNew) { - await firebase.firestore().collection('users').add(updatedRow); + const processRowUpdate = (newRow: GridRowModel) => { + const updatedRow = { ...(newRow as User), isNew: false }; + if (updatedRow) { + if (updatedRow.isNew) { + return firebase + .firestore() + .collection('users') + .add(updatedRow) + .then(() => { + setUserData( + userData.map((row) => (row.id === newRow.id ? updatedRow : row)) + ); + return updatedRow; + }) + .catch((error) => { + console.error('Error adding document: ', error); + throw error; + }); } else { - await firebase.firestore().collection('users').doc(updatedRow.id).update(updatedRow); + return firebase + .firestore() + .collection('users') + .doc(updatedRow.id) + .update(updatedRow) + .then(() => { + setUserData( + userData.map((row) => (row.id === newRow.id ? updatedRow : row)) + ); + return updatedRow; + }) + .catch((error) => { + console.error('Error updating document: ', error); + throw error; + }); } - - setUserData((prev) => - prev.map((row) => (row.id === newRow.id ? (updatedRow as User) : row)) + } else { + return Promise.reject( + new Error('No matching user data found for id: ' + newRow.id) ); - - return updatedRow; - } catch (err) { - console.error('Error processing row update: ', err); - throw err; - } finally { - setLoading(false); } }; + const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { + setRowModesModel(newRowModesModel); + }; + const columns: GridColDef[] = [ - { field: 'firstname', headerName: 'First Name', width: 150, editable: true }, - { field: 'lastname', headerName: 'Last Name', width: 150, editable: true }, - { field: 'email', headerName: 'Email', width: 250, editable: true }, - { field: 'department', headerName: 'Department', width: 130, editable: true }, - { field: 'role', headerName: 'Role', width: 150, editable: true }, - { field: 'id', headerName: 'User ID', width: 290, editable: true }, { field: 'actions', type: 'actions', headerName: 'Actions', - width: 130, + width: 200, cellClassName: 'actions', getActions: ({ id }) => { const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; @@ -239,14 +270,16 @@ export default function UserGrid(props: UserGridProps) { if (isInEditMode) { return [ } label="Save" - sx={{ color: 'primary.main' }} + sx={{ + color: '#562EBA', + }} onClick={handleSaveClick(id)} />, } label="Cancel" className="textPrimary" @@ -257,119 +290,100 @@ export default function UserGrid(props: UserGridProps) { } return [ - } - label="Edit" - className="textPrimary" - onClick={handleEditClick(id)} + , + + , ]; }, }, - ]; - - // ✅ Copy CourseGrid UI styling - const StripedDataGrid = styled(DataGrid)(() => ({ - border: 'none', - borderRadius: '16px', - fontFamily: 'Inter, sans-serif', - fontSize: '0.95rem', - - '& .MuiDataGrid-columnHeaders': { - backgroundColor: '#D8C6F8', - color: '#1C003D', - fontWeight: 700, - borderBottom: 'none', - }, - - '& .MuiDataGrid-columnHeaderTitle': { - fontWeight: 700, - }, - - '& .MuiDataGrid-columnHeader:first-of-type': { - paddingLeft: '20px', + { + field: 'firstname', + headerName: 'First Name', + width: 150, + editable: true, }, - '& .MuiDataGrid-cell:first-of-type': { - paddingLeft: '25px', + { field: 'lastname', headerName: 'Last Name', width: 150, editable: true }, + { field: 'email', headerName: 'Email', width: 250, editable: true }, + { + field: 'department', + headerName: 'Department', + width: 130, + editable: true, }, + { field: 'role', headerName: 'Role', width: 150, editable: true }, + { field: 'id', headerName: 'User ID', width: 290, editable: true }, + ]; + const ODD_OPACITY = 0.2; + const StripedDataGrid = styled(DataGrid)(({ theme }) => ({ [`& .${gridClasses.row}.even`]: { - backgroundColor: '#FFFFFF', - }, - [`& .${gridClasses.row}.odd`]: { - backgroundColor: '#EEEEEE', - }, - - '& .MuiDataGrid-row:hover': { - backgroundColor: '#EFE6FF', - }, - - '& .MuiDataGrid-cell': { - borderBottom: '1px solid #ECE4FA', - }, - - '& .MuiDataGrid-footerContainer': { - borderTop: 'none', - }, - - '& .MuiTablePagination-root': { - color: '#5D3FC4', - fontWeight: 500, + backgroundColor: '#562EBA1F', + '&:hover, &.Mui-hovered': { + backgroundColor: alpha(theme.palette.primary.main, ODD_OPACITY), + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + }, + '&.Mui-selected': { + backgroundColor: alpha( + theme.palette.primary.main, + ODD_OPACITY + theme.palette.action.selectedOpacity + ), + '&:hover, &.Mui-hovered': { + backgroundColor: alpha( + theme.palette.primary.main, + ODD_OPACITY + + theme.palette.action.selectedOpacity + + theme.palette.action.hoverOpacity + ), + // Reset on touch devices, it doesn't add specificity + '@media (hover: none)': { + backgroundColor: alpha( + theme.palette.primary.main, + ODD_OPACITY + theme.palette.action.selectedOpacity + ), + }, + }, + }, }, })); - return ( - <> - - {loading ? : null} - - setRowModesModel(m)} - onRowEditStop={handleRowEditStop} - processRowUpdate={processRowUpdate} - onProcessRowUpdateError={(error) => - console.error('Error processing row update: ', error) - } - slots={{ - toolbar: EditToolbar, - }} - slotProps={{ - toolbar: { setUserData, setRowModesModel }, - }} - initialState={{ - pagination: { paginationModel: { pageSize: 25 } }, - }} - getRowClassName={(params) => - params.indexRelativeToCurrentPage % 2 === 0 ? 'even' : 'odd' - } - /> - - - {/* keep your confirm-delete dialog (CourseGrid doesn't have it, but UI matches your app style) */} + @@ -392,7 +408,7 @@ export default function UserGrid(props: UserGridProps) { > Delete User -
+ handleSubmit(e)}>
- + + params.indexRelativeToCurrentPage % 2 === 0 ? 'even' : 'odd' + } + sx={{ borderRadius: '16px' }} + /> +
); } - diff --git a/src/components/TopBar/TopBar.tsx b/src/components/TopBar/TopBar.tsx index dec24ba..f44897e 100644 --- a/src/components/TopBar/TopBar.tsx +++ b/src/components/TopBar/TopBar.tsx @@ -2,17 +2,37 @@ import { useUserInfo } from '@/hooks/User/useGetUserInfo'; import { AppBar } from '@mui/material'; import Toolbar from '@mui/material/Toolbar'; import IconButton from '@mui/material/IconButton'; +import Badge from '@mui/material/Badge'; import NotificationsNoneOutlinedIcon from '@mui/icons-material/NotificationsNoneOutlined'; import NotificationsActiveOutlinedIcon from '@mui/icons-material/NotificationsActiveOutlined'; import AccountCircleTwoToneIcon from '@mui/icons-material/AccountCircleTwoTone'; import { EceLogoPng } from '@/component/EceLogoPng/EceLogoPng'; import Link from 'next/link'; import { Role, roleMapping } from '@/types/User'; -import { useAnnouncements } from '@/contexts/AnnouncementsContext'; +import { useFetchAnnouncementsForAccount } from '@/hooks/Announcements/useFetchAnnouncements'; +// type TopNavProps = { +// notification?: boolean; +// }; export default function TopNav() { const [user, role, loading, error] = useUserInfo(); - const { unread } = useAnnouncements(); + const uemail = user?.email; + const { + read, + unread, + loading: announcementLoading, + loadingMore, + hasMore, + error: fetchError, + refresh, + loadMore, + } = useFetchAnnouncementsForAccount({ + userRole: role, + userEmail: uemail, + userDepartment: 'ECE', + channel: 'inApp', + realtime: true, + }); const onNotifications = () => {}; const display = (v: unknown, fallback = 'Not listed'): string => { if (v == null) return fallback; // null/undefined diff --git a/src/contexts/AnnouncementsContext.tsx b/src/contexts/AnnouncementsContext.tsx deleted file mode 100644 index 3ba4682..0000000 --- a/src/contexts/AnnouncementsContext.tsx +++ /dev/null @@ -1,204 +0,0 @@ -'use client'; - -import React, { - createContext, - useContext, - useEffect, - useState, - useMemo, - useCallback, - ReactNode, -} from 'react'; -import firebase from '@/firebase/firebase_config'; -import 'firebase/firestore'; -import { useAuth } from '@/firebase/auth/auth_context'; -import GetUserRole from '@/firebase/util/GetUserRole'; -import GetAnnouncementTimestamp from '@/firebase/util/GetAnnouncementTimestamp'; -import { - Announcement, - Audience, - AudienceDepartment, - AudienceRole, -} from '@/types/announcement'; -import { Role } from '@/types/User'; - -type AnnouncementsContextType = { - read: Announcement[]; - unread: Announcement[]; - loading: boolean; - error: Error | null; - refresh: () => void; -}; - -const AnnouncementsContext = createContext( - null -); - -function convertRole(role: string): AudienceRole { - switch (role) { - case 'admin': - return 'admin'; - case 'faculty': - return 'faculty'; - default: - return 'student'; - } -} - -function toDate(v: any): Date | null { - if (!v) return null; - if (v instanceof Date) return v; - if (typeof v.toDate === 'function') return v.toDate(); - return null; -} - -function mapDoc(doc: firebase.firestore.QueryDocumentSnapshot): Announcement { - const d = doc.data() as any; - - return { - id: doc.id, - title: d.title ?? '', - bodyMd: d.bodyMd ?? '', - pinned: !!d.pinned, - - createdAt: toDate(d.createdAt), - updatedAt: toDate(d.updatedAt), - scheduledAt: toDate(d.scheduledAt), - expiresAt: toDate(d.expiresAt), - - senderId: d.senderId ?? '', - senderName: d.senderName ?? null, - audienceTokens: Array.isArray(d.audienceTokens) ? d.audienceTokens : [], - channels: Array.isArray(d.channels) ? d.channels : [], - audience: (d.audience ?? { type: 'all' }) as Audience, - - dispatchStatus: d.dispatchStatus ?? 'unknown', - }; -} - -type AnnouncementsProviderProps = { - children: ReactNode; -}; - -export function AnnouncementsProvider({ - children, -}: AnnouncementsProviderProps) { - const { user } = useAuth(); - const [roleData, roleLoading] = GetUserRole(user?.uid); - const [timestamp] = GetAnnouncementTimestamp(user?.uid); - - const [announcements, setAnnouncements] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [refreshTrigger, setRefreshTrigger] = useState(0); - - const userEmail = user?.email ?? ''; - const userDepartment: AudienceDepartment = 'ECE'; - const channel = 'inApp'; - - const userRole = useMemo(() => { - return convertRole(roleData ?? ''); - }, [roleData]); - - const buildQuery = useCallback(() => { - const col = firebase.firestore().collection('announcements'); - let q: firebase.firestore.Query = col; - - q = q.where(`channels.${channel}`, '==', true); - q = q.where('dispatchStatus', '==', 'completed'); - q = q.orderBy('pinned', 'desc').orderBy('createdAt', 'desc'); - - if (userRole === 'admin') { - return q; - } - - const isNonEmpty = (v: string | null | undefined): v is string => - typeof v === 'string' && v.trim().length > 0; - - const norm = (s: string) => s.trim().toLowerCase(); - - const tokens = [ - 'all', - userRole ? `role:${norm(userRole)}` : null, - userRole === 'faculty' ? `role:student` : null, - userDepartment ? `dept:${norm(userDepartment)}` : null, - userEmail ? `user:${norm(userEmail)}` : null, - ].filter(isNonEmpty); - - q = q.where('audienceTokens', 'array-contains-any', tokens); - return q; - }, [userRole, userDepartment, userEmail, channel]); - - useEffect(() => { - if (roleLoading || !user) { - return; - } - - setLoading(true); - setError(null); - - const q = buildQuery().limit(50); - - const unsub = q.onSnapshot( - (snap) => { - const docs = snap.docs; - const items = docs.map(mapDoc); - setAnnouncements(items); - setLoading(false); - }, - (err) => { - console.error('Announcements listener error:', err); - setError(err); - setLoading(false); - } - ); - - return () => { - unsub(); - }; - }, [buildQuery, roleLoading, user, refreshTrigger]); - - const refresh = useCallback(() => { - setRefreshTrigger((prev) => prev + 1); - }, []); - - const { read, unread } = useMemo(() => { - return announcements.reduce( - (acc, a) => { - const tMs = a.scheduledAt ?? a.createdAt ?? new Date(Date.now()); - const isUnread = timestamp === null ? true : tMs > timestamp; - - (isUnread ? acc.unread : acc.read).push(a); - return acc; - }, - { unread: [] as Announcement[], read: [] as Announcement[] } - ); - }, [announcements, timestamp]); - - const value = useMemo( - () => ({ - read, - unread, - loading: loading || roleLoading, - error, - refresh, - }), - [read, unread, loading, roleLoading, error, refresh] - ); - - return ( - - {children} - - ); -} - -export function useAnnouncements(): AnnouncementsContextType { - const context = useContext(AnnouncementsContext); - if (!context) { - throw new Error( - 'useAnnouncements must be used within an AnnouncementsProvider' - ); - } - return context; -} From cd59106959eb281d0dcb75d169df3f039652fbe2 Mon Sep 17 00:00:00 2001 From: ThomasOli Date: Sat, 31 Jan 2026 14:27:39 -0500 Subject: [PATCH 4/6] Revert "added supervised teaching page fuctionality" This reverts commit d6194d84aad46421c91fe9a96b793484dfb3ce3c. --- src/app/applications/applicationSections.tsx | 7 - .../applications/supervisedTeaching/page.tsx | 438 ------------------ src/hooks/useGetItems.ts | 6 - src/types/navigation.ts | 2 +- 4 files changed, 1 insertion(+), 452 deletions(-) delete mode 100644 src/app/applications/supervisedTeaching/page.tsx diff --git a/src/app/applications/applicationSections.tsx b/src/app/applications/applicationSections.tsx index 114601b..487bfce 100644 --- a/src/app/applications/applicationSections.tsx +++ b/src/app/applications/applicationSections.tsx @@ -43,15 +43,8 @@ export default function ApplicationSections({ .map(({ label, to, icon: Icon }: NavbarItem) => ( ))} - - {navItems - .filter((item) => item.type === 'supervised-teaching') - .map(({ label, to, icon: Icon }: NavbarItem) => ( - - ))} -

Research

No available applications at this time.

diff --git a/src/app/applications/supervisedTeaching/page.tsx b/src/app/applications/supervisedTeaching/page.tsx deleted file mode 100644 index ad278db..0000000 --- a/src/app/applications/supervisedTeaching/page.tsx +++ /dev/null @@ -1,438 +0,0 @@ -'use client'; -import * as React from 'react'; -import Button from '@mui/material/Button'; -import CssBaseline from '@mui/material/CssBaseline'; -import TextField from '@mui/material/TextField'; -import Grid from '@mui/material/Grid'; -import Box from '@mui/material/Box'; -import DepartmentSelect from '@/component/FormUtil/DepartmentSelect'; -import GPA_Select from '@/component/FormUtil/GPASelect'; -import Typography from '@mui/material/Typography'; -import DegreeSelect from '@/component/FormUtil/DegreeSelect'; -import SemesterStatusSelect from '@/component/FormUtil/SemesterStatusSelect'; -import AvailabilityCheckbox from '@/component/FormUtil/AvailabilityCheckbox'; -import AdditionalSemesterPrompt from '@/component/FormUtil/AddtlSemesterPrompt'; -import UpdateRole from '@/firebase/util/UpdateUserRole'; -import { useAuth } from '@/firebase/auth/auth_context'; -import { Toaster, toast } from 'react-hot-toast'; -import Snackbar from '@mui/material/Snackbar'; -import MuiAlert, { AlertProps } from '@mui/material/Alert'; -import 'firebase/firestore'; -import firebase from '@/firebase/firebase_config'; -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; -import FormControl from '@mui/material/FormControl'; -import Autocomplete from '@mui/material/Autocomplete'; -import HeaderCard from '@/components/HeaderCard/HeaderCard'; -import { - fetchClosestSemesters, - parseCoursesMinimal, -} from '@/hooks/useSemesterOptions'; -import { CourseOption } from '@/hooks/useSemesterOptions'; - -export default function SupervisedTeachingApplication() { - const router = useRouter(); - const { user } = useAuth(); - const userId = user.uid; - - const current = new Date(); - const current_date = `${ - current.getMonth() + 1 - }-${current.getDate()}-${current.getFullYear()}`; - - const [nationality, setNationality] = React.useState(null); - const [additionalPromptValue, setAdditionalPromptValue] = React.useState(''); - const handleAdditionalPromptChange = (newValue: string) => { - setAdditionalPromptValue(newValue); - }; - const [loading, setLoading] = useState(false); - - const handleSubmit = async (event: React.FormEvent) => { - setLoading(true); - event.preventDefault(); - const formData = new FormData(event.currentTarget); - - const availabilityCheckbox_seven = - formData.get('availabilityCheckbox_seven') === 'on'; - const availabilityCheckbox_fourteen = - formData.get('availabilityCheckbox_fourteen') === 'on'; - const availabilityCheckbox_twenty = - formData.get('availabilityCheckbox_twenty') === 'on'; - - const availabilityArray: string[] = []; - if (availabilityCheckbox_seven) availabilityArray.push('7'); - if (availabilityCheckbox_fourteen) availabilityArray.push('14'); - if (availabilityCheckbox_twenty) availabilityArray.push('20'); - - const semesterArray: string[] = []; - semesterArray.push(...(await fetchClosestSemesters(1))); - - const coursesArray = selectedCourses; - let coursesMap: { [key: string]: string } = {}; - for (let i = 0; i < coursesArray.length; i++) { - coursesMap[coursesArray[i]] = 'applied'; - } - - const applicationData = { - firstname: formData.get('firstName') as string, - lastname: formData.get('lastName') as string, - email: formData.get('email') as string, - ufid: formData.get('ufid') as string, - phonenumber: formData.get('phone-number') as string, - gpa: formData.get('gpa-select') as string, - department: formData.get('department-select') as string, - degree: formData.get('degrees-radio-group') as string, - semesterstatus: formData.get('semstatus-radio-group') as string, - additionalprompt: additionalPromptValue, - nationality: nationality as string, - englishproficiency: 'NA', - position: 'Supervised Teaching', - available_hours: availabilityArray as string[], - available_semesters: semesterArray as string[], - courses: coursesMap, - qualifications: formData.get('qualifications-prompt') as string, - uid: userId, - date: current_date, - status: 'Submitted', - resume_link: formData.get('resumeLink') as string, - }; - - if (!applicationData.email.includes('ufl.edu')) { - toast.error('Please enter a valid ufl email!'); - setLoading(false); - return; - } else if (applicationData.firstname === '') { - toast.error('Please enter a valid first name!'); - setLoading(false); - return; - } else if (applicationData.lastname === '') { - toast.error('Please enter a valid last name!'); - setLoading(false); - return; - } else if (applicationData.ufid == '') { - toast.error('Please enter a valid ufid!'); - setLoading(false); - return; - } else if (applicationData.phonenumber === '') { - toast.error('Please enter a valid phone number!'); - setLoading(false); - return; - } else if ( - applicationData.degree === null || - applicationData.degree === '' - ) { - toast.error('Please select a degree!'); - setLoading(false); - return; - } else if ( - applicationData.department === null || - applicationData.department === '' - ) { - toast.error('Please select a department!'); - setLoading(false); - return; - } else if ( - applicationData.semesterstatus === null || - applicationData.semesterstatus === '' - ) { - toast.error('Please select a semester status!'); - setLoading(false); - return; - } else if ( - applicationData.resume_link === null || - applicationData.resume_link === '' - ) { - toast.error('Please provide a resume link!'); - setLoading(false); - return; - } else if (applicationData.available_hours.length == 0) { - toast.error('Please enter your available hours!'); - setLoading(false); - return; - } else if (applicationData.available_semesters.length == 0) { - toast.error('Please enter your available semesters!'); - setLoading(false); - return; - } else if (coursesArray.length == 0) { - toast.error('Please enter your courses!'); - setLoading(false); - return; - } else { - const toastId = toast.loading('Processing application', { - duration: 30000, - }); - await firebase.firestore().collection('assignments').doc(userId).delete(); - - const response = await fetch( - 'https://us-central1-courseconnect-c6a7b.cloudfunctions.net/processApplicationForm', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(applicationData), - } - ); - - if (response.ok) { - toast.dismiss(toastId); - toast.success('Application submitted!'); - await UpdateRole(userId, 'student_applied'); - router.push('/'); - } else { - toast.dismiss(toastId); - toast.error('Application data failed to send to server!'); - console.log('ERROR: Application data failed to send to server'); - } - setLoading(false); - } - }; - - const [success, setSuccess] = React.useState(false); - const Alert = React.forwardRef(function Alert( - props, - ref - ) { - return ; - }); - - const handleSuccess = ( - event?: React.SyntheticEvent | Event, - reason?: string - ) => { - if (reason === 'clickaway') return; - setSuccess(false); - }; - - const [selectedCourses, setSelectedCourses] = React.useState([]); - const [names, setNames] = useState([]); - - React.useEffect(() => { - async function fetchData() { - try { - let data: string[] = []; - let visibleSems: string[] = await fetchClosestSemesters(1); - await firebase - .firestore() - .collection('semesters') - .doc(visibleSems[0]) - .collection('courses') - .get() - .then((snapshot) => - snapshot.docs.map((doc) => { - if (visibleSems.includes(doc.data().semester)) { - data.push(doc.id); - } - }) - ); - setNames(data); - } catch (err) { - console.log(err); - } - } - fetchData(); - }, []); - - const courseOptions = parseCoursesMinimal(names); - - return ( - <> - - - - - Application submitted successfully! - - - - - - - - Personal Information - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - Supervision Information - -
- - - - Please list the course(s) for which you are interested in - supervised teaching. Ensure that you select the courses with - your desired semester and instructor. - -
- - - multiple - disableCloseOnSelect - options={courseOptions.sort( - (a, b) => -b.code.localeCompare(a.code) - )} - groupBy={(o) => o.department} - getOptionLabel={(o) => o.name} - value={courseOptions.filter((o) => - selectedCourses.includes(o.value) - )} - onChange={(_, vals) => - setSelectedCourses(vals.map((v) => v.value)) - } - isOptionEqualToValue={(opt, val) => opt.value === val.value} - renderInput={(params) => ( - - )} - /> - -
- - - Please provide your most recently calculated cumulative UF - GPA. - -
- -
- - - Please upload a google drive link to your resume. - - - - - - Please describe your qualifications for supervised teaching, - including any teaching, grading, or tutoring experience. If - applicable, mention courses and instructors you have worked - with. - - - -
- - -
-
-
- - ); -} diff --git a/src/hooks/useGetItems.ts b/src/hooks/useGetItems.ts index 14113a1..243fac0 100644 --- a/src/hooks/useGetItems.ts +++ b/src/hooks/useGetItems.ts @@ -95,12 +95,6 @@ export const getApplications = (userRole: Role): NavbarItem[] => { icon: FolderOutlinedIcon, type: 'ta', }, - { - label: 'Supervised Teaching', - to: '/applications/supervisedTeaching', - icon: FolderOutlinedIcon, - type: 'supervised-teaching', - }, ]; /* ────────────────────────────────── Faculty ────────────────────────────────── */ diff --git a/src/types/navigation.ts b/src/types/navigation.ts index 4185820..4e7c13c 100644 --- a/src/types/navigation.ts +++ b/src/types/navigation.ts @@ -1,6 +1,6 @@ import { SvgIconComponent } from '@mui/icons-material'; -export type CardType = 'research' | 'ta' | 'supervised-teaching'; +export type CardType = 'research' | 'ta'; export type QueryParams = Record; export type NavbarItem = { From 13f29588735247f7b2fc2f77af40cae94fd0b7ec Mon Sep 17 00:00:00 2001 From: ThomasOli Date: Sat, 31 Jan 2026 14:29:27 -0500 Subject: [PATCH 5/6] revert --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fb3a957..66361ae 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,5 @@ courseconnect-c6a7b-firebase-adminsdk-dqqis-af57e2e045.json playwright-report test-results playwright/.auth -playwright.accounts.json \ No newline at end of file +playwright.accounts.json +nul From f31c749ff7c342acb5914c8f1191e014e0afd480 Mon Sep 17 00:00:00 2001 From: qingyuan Date: Mon, 2 Feb 2026 09:43:29 -0500 Subject: [PATCH 6/6] redone supervised teaching --- src/app/applications/applicationSections.tsx | 14 + .../applications/supervised-teaching/page.tsx | 474 ++++++++++++++++++ 2 files changed, 488 insertions(+) create mode 100644 src/app/applications/supervised-teaching/page.tsx diff --git a/src/app/applications/applicationSections.tsx b/src/app/applications/applicationSections.tsx index 487bfce..b660017 100644 --- a/src/app/applications/applicationSections.tsx +++ b/src/app/applications/applicationSections.tsx @@ -7,6 +7,7 @@ import SemesterSelect from '@/components/SemesterSelect/SemesterSelect'; import { useSemesterData } from '@/hooks/Courses/useSemesterData'; import { CoursesGrid } from '@/components/CoursesGrid/CoursesGrid'; import { type SemesterName } from '@/hooks/useSemesterOptions'; +import SchoolIcon from '@mui/icons-material/School'; export default function ApplicationSections({ role, navItems, @@ -45,6 +46,19 @@ export default function ApplicationSections({ ))}
+ +
+

Supervised Teaching

+
+ +
+
+

Research

No available applications at this time.

diff --git a/src/app/applications/supervised-teaching/page.tsx b/src/app/applications/supervised-teaching/page.tsx new file mode 100644 index 0000000..7e21059 --- /dev/null +++ b/src/app/applications/supervised-teaching/page.tsx @@ -0,0 +1,474 @@ +'use client'; + +import * as React from 'react'; +import Button from '@mui/material/Button'; +import CssBaseline from '@mui/material/CssBaseline'; +import TextField from '@mui/material/TextField'; +import Grid from '@mui/material/Grid'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { useAuth } from '@/firebase/auth/auth_context'; +import { Toaster, toast } from 'react-hot-toast'; +import Snackbar from '@mui/material/Snackbar'; +import MuiAlert, { AlertProps } from '@mui/material/Alert'; +import HeaderCard from '@/components/HeaderCard/HeaderCard'; +import firebase from '@/firebase/firebase_config'; +import { useRouter } from 'next/navigation'; +import { fetchClosestSemesters } from '@/hooks/useSemesterOptions'; +import MenuItem from '@mui/material/MenuItem'; +import InputLabel from '@mui/material/InputLabel'; +import FormControl from '@mui/material/FormControl'; +import Select, { SelectChangeEvent } from '@mui/material/Select'; + +export default function SupervisedTeachingApplication() { + const { user } = useAuth(); + const userId = user?.uid || ''; + const router = useRouter(); + + const [success, setSuccess] = React.useState(false); + const [loading, setLoading] = React.useState(false); + + // Teaching choices (dropdowns) + const [teachingFirstChoice, setTeachingFirstChoice] = React.useState(''); + const [teachingSecondChoice, setTeachingSecondChoice] = React.useState(''); + const [teachingThirdChoice, setTeachingThirdChoice] = React.useState(''); + + const Alert = React.forwardRef(function Alert( + props, + ref + ) { + return ; + }); + + const handleSuccess = ( + event?: React.SyntheticEvent | Event, + reason?: string + ) => { + if (reason === 'clickaway') return; + setSuccess(false); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setLoading(true); + + const formData = new FormData(event.currentTarget); + + const firstName = (formData.get('firstName') as string) || ''; + const lastName = (formData.get('lastName') as string) || ''; + const ufid = (formData.get('ufid') as string) || ''; + const email = (formData.get('email') as string) || ''; + const confirmEmail = (formData.get('confirmEmail') as string) || ''; + const phdAdmissionTerm = (formData.get('phdAdmissionTerm') as string) || ''; + const phdAdvisor = (formData.get('phdAdvisor') as string) || ''; + const admittedToCandidacy = + (formData.get('admittedToCandidacy') as string) || ''; + const registerTerm = (formData.get('registerTerm') as string) || ''; + const previouslyRegistered = + (formData.get('previouslyRegistered') as string) || ''; + const previousDetails = (formData.get('previousDetails') as string) || ''; + const coursesComfortable = + (formData.get('coursesComfortable') as string) || ''; + // teaching choices come from Select state + const teachingFirst = teachingFirstChoice || ''; + const teachingSecond = teachingSecondChoice || ''; + const teachingThird = teachingThirdChoice || ''; + const captcha = formData.get('captcha') === 'on'; + + // basic validations + if (!email.includes('ufl.edu')) { + toast.error('Please enter a valid GatorLink (ufl.edu) email'); + setLoading(false); + return; + } + if (email !== confirmEmail) { + toast.error('Email and Confirm Email must match'); + setLoading(false); + return; + } + if (!firstName || !lastName || !ufid) { + toast.error('Please complete all required personal fields'); + setLoading(false); + return; + } + if (!registerTerm) { + toast.error('Please select the term you want to register for EEL 6940'); + setLoading(false); + return; + } + if (!coursesComfortable) { + toast.error('Please list at least one course you could teach'); + setLoading(false); + return; + } + if (!captcha) { + toast.error('Please complete the CAPTCHA confirmation'); + setLoading(false); + return; + } + + // date + const current = new Date(); + const current_date = `${ + current.getMonth() + 1 + }-${current.getDate()}-${current.getFullYear()}`; + + const applicationData = { + application_type: 'supervised_teaching', + firstname: firstName, + lastname: lastName, + ufid, + email, + phdAdmissionTerm, + phdAdvisor, + admittedToCandidacy, + registerTerm, + previouslyRegistered, + previousDetails, + coursesComfortable, + teachingFirst, + teachingSecond, + teachingThird, + uid: userId, + date: current_date, + status: 'Submitted', + }; + + try { + const toastId = toast.loading('Submitting application...'); + const response = await fetch( + 'https://us-central1-courseconnect-c6a7b.cloudfunctions.net/processApplicationForm', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(applicationData), + } + ); + + if (response.ok) { + toast.dismiss(toastId); + toast.success('Application submitted!'); + setSuccess(true); + // optional: update role or navigate + router.push('/'); + } else { + toast.dismiss(toastId); + toast.error('Submission failed. Please try again later.'); + } + } catch (err) { + console.error(err); + toast.error('Submission failed. Please try again later.'); + } + + setLoading(false); + }; + + const [visibleSems, setVisibleSems] = React.useState([]); + React.useEffect(() => { + async function load() { + const sems = await fetchClosestSemesters(3); + setVisibleSems(sems); + } + load(); + }, []); + + return ( + + + + + + + ECE Ph.D. students may register for EEL 6940 Supervised Teaching to + fulfill professional development requirements. Deadlines: + +
    +
  • Fall Semester — August 2
  • +
  • Spring Semester — January 2
  • +
  • Summer Semester — April 30
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Admitted to candidacy? (registered for EEL 7980 hours) + +
+ + + +
+
+ + + + Select term + {visibleSems.map((s) => ( + + {s} + + ))} + + + + + + Previously registered for EEL 6940? + +
+ + + +
+
+ + + + + + + + + + + + + Teaching First Choice + + + + + + + + + Teaching Second Choice + + + + + + + + + Teaching Third Choice + + + + + + + + + + + + +
+
+
+
+ + + + Application submitted successfully! + + +
+ ); +}