diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index b478991..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(git merge:*)", - "Bash(git rm:*)", - "Bash(git commit -m \"$\\(cat <<''EOF''\nMerge main into researchpage with Research page refactoring\n\n- Refactored Research page to use PageLayout component with SideNav/TopBar\n- Updated hooks to use useUserInfo instead of getAuth/useUserRole directly\n- Removed HeaderCard from FacultyResearchView and StudentResearchView\n- Layout now handled by PageLayout component for consistency\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")" - "Bash(cd:*)" - ] - } -} diff --git a/.gitignore b/.gitignore index fb3a957..48525f6 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,11 @@ next-env.d.ts .vscode courseconnect-c6a7b-firebase-adminsdk-dqqis-af57e2e045.json playwright-report +nul test-results playwright/.auth -playwright.accounts.json \ No newline at end of file +playwright.accounts.json + +# Claude Code +.claude/ +CLAUDE.md \ No newline at end of file diff --git a/src/app/Research/page.tsx b/src/app/Research/page.tsx index 0f779b1..8d0c6c9 100644 --- a/src/app/Research/page.tsx +++ b/src/app/Research/page.tsx @@ -4,58 +4,15 @@ import { Toaster } from 'react-hot-toast'; import { useUserInfo } from '@/hooks/User/useGetUserInfo'; import { getNavItems } from '@/hooks/useGetItems'; import PageLayout from '@/components/PageLayout/PageLayout'; -import firebase from '@/firebase/firebase_config'; import StudentResearchView from '@/components/Research/StudentResearchView'; import FacultyResearchView from '@/components/Research/FacultyResearchView'; +import { ResearchListing } from '@/app/models/ResearchModel'; +import { + fetchResearchListings, + createResearchListing, +} from '@/services/researchService'; -interface ResearchPageProps { - user: { - uid: string; - fullName: string; - bio: string; - }; -} - -interface ResearchListing { - id: string; - project_title: string; - department: string; - faculty_mentor: {}; - phd_student_mentor: string | {}; - terms_available: string; - student_level: string; - prerequisites: string; - credit: string; - stipend: string; - application_requirements: string; - application_deadline: string; - website: string; - project_description: string; -} - -interface ResearchApplication { - appid: string; - app_status: string; - terms_available: string; - date_applied: string; - degree: string; - department: string; - email: string; - first_name: string; - last_name: string; - gpa: string; - graduation_date: string; - phone_number: string; - qualifications: string; - resume: string; - uid: string; - weekly_hours: string; - project_title: string; - faculty_mentor: {}; - project_description: string; -} - -const ResearchPage: React.FC = () => { +const ResearchPage: React.FC = () => { const [user, role, loading, error] = useUserInfo(); const [department, setDepartment] = React.useState(''); @@ -64,100 +21,24 @@ const ResearchPage: React.FC = () => { const [researchListings, setResearchListings] = useState( [] ); - const [researchApplications, setResearchApplications] = useState< - ResearchApplication[] - >([]); const getResearchListings = useCallback(async () => { - let collectionRef: firebase.firestore.Query = - firebase.firestore().collection('research-listings'); - if (department) { - collectionRef = collectionRef.where('department', '==', department); - } - if (studentLevel) { - collectionRef = collectionRef.where('student_level', '==', studentLevel); + try { + const listings = await fetchResearchListings({ + department, + studentLevel, + }); + setResearchListings(listings); + } catch (error) { + console.error('Error fetching research listings:', error); + setResearchListings([]); } - let snapshot = await collectionRef.get(); - let listings: ResearchListing[] = await Promise.all( - snapshot.docs.map(async (doc: any) => { - const detailsSnap = await doc.ref.collection('applications').get(); - const apps = detailsSnap.docs.map( - (d: firebase.firestore.QueryDocumentSnapshot) => ({ - id: d.id, - ...d.data(), - }) - ); - return { - docID: doc.id, - applications: apps, - ...doc.data(), - }; - }) - ); - setResearchListings(listings); }, [department, studentLevel]); - const getApplications = useCallback(async () => { - if (!user?.uid) return; - const snapshot = await firebase - .firestore() - .collectionGroup('applications') - .where('uid', '==', user.uid) - .get(); - const results = await Promise.all( - snapshot.docs.map(async (appDoc) => { - const appData = appDoc.data(); - - // navigate back up to the parent document - var listingRef = appDoc.ref.parent.parent; - let listingData: any = {}; - if (listingRef) { - const listingSnap = await listingRef.get(); - if (listingSnap.exists) { - listingData = listingSnap.data(); - } - } - - return { - appId: appDoc.id, - ...appData, - listingId: listingRef?.id ?? null, - listingData, - }; - }) - ); - - let applications: ResearchApplication[] = results.map((doc: any) => ({ - appid: doc.appId, - app_status: doc.app_status, - terms_available: doc.listingData.terms_available, - date_applied: doc.date, - degree: doc.degree, - department: doc.department, - email: doc.email, - first_name: doc.firstname, - last_name: doc.lastname, - gpa: doc.gpa, - graduation_date: doc.graduation_date, - phone_number: doc.phone_number, - qualifications: doc.qualifications, - resume: doc.resume, - uid: doc.uid, - weekly_hours: doc.weekly_hours, - faculty_mentor: doc.listingData.faculty_mentor, - project_title: doc.listingData.project_title, - project_description: doc.listingData.project_description, - })); - setResearchApplications(applications); - }, [user?.uid]); - const postNewResearchPosition = useCallback(async (formData: any) => { try { - const docRef = await firebase - .firestore() - .collection('research-listings') - .add(formData); - console.log('Document written with ID: ', docRef.id); + const docId = await createResearchListing(formData); + console.log('Document written with ID: ', docId); } catch (e) { console.error('Error adding document: ', e); } @@ -166,9 +47,8 @@ const ResearchPage: React.FC = () => { useEffect(() => { if (user) { getResearchListings(); - getApplications(); } - }, [user, getResearchListings, getApplications]); + }, [user, getResearchListings]); if (error) { return

Error loading role

; @@ -185,11 +65,13 @@ const ResearchPage: React.FC = () => { return ( <> - + {(role === 'student_applying' || role === 'student_applied') && ( = () => { setStudentLevel={setStudentLevel} getResearchListings={getResearchListings} setResearchListings={setResearchListings} - getApplications={getApplications} termsAvailable={termsAvailable} setTermsAvailable={setTermsAvailable} - setResearchApplications={setResearchApplications} /> )} {role === 'faculty' && ( diff --git a/src/app/Research/testdata.js b/src/app/Research/testdata.js deleted file mode 100644 index 2421187..0000000 --- a/src/app/Research/testdata.js +++ /dev/null @@ -1,67 +0,0 @@ -export const testData = [ - { - project_title: 'Intelligent Natural Interaction Technology (INIT) Lab', - department: 'Computer and Information Sciences and Engineering', - faculty_mentor: 'Lisa Anthony', - phd_student_mentor: 'TBD based on project and availability', - terms_available: 'Fall, Spring, Summer', - student_level: - 'Freshman, Sophomore, Junior, Senior, 2-3 students per semester', - prerequisites: - 'Projects can be customized for background and interest of the student, pending lab needs at the time. Helpful skills (encouraged but not required) include: programming fundamentals, experimental design, data analysis, experience working with children, good people skills, attention to detail, organization, time management. High-achieving freshman encouraged to apply! Students considering graduate school strongly encouraged to apply!', - credit: '0-3 credits via EGN 4912', - stipend: - '1st semester, none unless selected for University Scholars; after trial period, $15/hour up to 10 hours per week', - application_requirements: - 'Resume, UF unofficial transcripts, faculty interview; email all application requirements to Lisa Anthony', - application_deadline: - 'applications are accepted on a rolling basis, but first come first served (recommend: Mar 15 or July 1 for Fall, Nov 15 for Spring, Mar 15 for Summer)', - website: 'http://init.cise.ufl.edu', - project_description: - 'Our lab focuses on advanced interaction technologies such as touch, gesture, voice, and mixed reality, in the context of human-AI interaction, education, healthcare, and serious games. Many of our projects emphasize children and/or families as a unique user group. Our projects advance human-computer interaction (HCI) research questions of how users want to interact with these natural modalities, and computer science research questions of how to build recognition algorithms that can understand user input in these ambiguous modalities. Top priorities currently: (a) designing intelligent chatbots for mobile health monitoring apps; (b) designing digital AI assistants to help novice users complete more expert tasks; and (c) designing human-centered interactive machine learning interfaces.', - }, - { - project_title: - 'dawdawdawdawdaIntelligent Natural Interaction Technology (INIT) Lab', - department: 'Computer and Information Sciences and Engineering', - faculty_mentor: 'Lisa Anthony', - phd_student_mentor: 'TBD based on project and availability', - terms_available: 'Fall, Spring, Summer', - student_level: - 'Freshman, Sophomore, Junior, Senior, 2-3 students per semester', - prerequisites: - 'Projects can be customized for background and interest of the student, pending lab needs at the time. Helpful skills (encouraged but not required) include: programming fundamentals, experimental design, data analysis, experience working with children, good people skills, attention to detail, organization, time management. High-achieving freshman encouraged to apply! Students considering graduate school strongly encouraged to apply!', - credit: '0-3 credits via EGN 4912', - stipend: - '1st semester, none unless selected for University Scholars; after trial period, $15/hour up to 10 hours per week', - application_requirements: - 'Resume, UF unofficial transcripts, faculty interview; email all application requirements to Lisa Anthony', - application_deadline: - 'applications are accepted on a rolling basis, but first come first served (recommend: Mar 15 or July 1 for Fall, Nov 15 for Spring, Mar 15 for Summer)', - website: 'http://init.cise.ufl.edu', - project_description: - 'Our lab focuses on advanced interaction technologies such as touch, gesture, voice, and mixed reality, in the context of human-AI interaction, education, healthcare, and serious games. Many of our projects emphasize children and/or families as a unique user group. Our projects advance human-computer interaction (HCI) research questions of how users want to interact with these natural modalities, and computer science research questions of how to build recognition algorithms that can understand user input in these ambiguous modalities. Top priorities currently: (a) designing intelligent chatbots for mobile health monitoring apps; (b) designing digital AI assistants to help novice users complete more expert tasks; and (c) designing human-centered interactive machine learning interfaces.', - }, - { - project_title: - 'dawdawdawdawdaIntelligent Natural Interaction Technology (INIT) Lab', - department: 'Computer and Information Sciences and Engineering', - faculty_mentor: 'Lisa Anthony', - phd_student_mentor: 'TBD based on project and availability', - terms_available: 'Fall, Spring, Summer', - student_level: - 'Freshman, Sophomore, Junior, Senior, 2-3 students per semester', - prerequisites: - 'Projects can be customized for background and interest of the student, pending lab needs at the time. Helpful skills (encouraged but not required) include: programming fundamentals, experimental design, data analysis, experience working with children, good people skills, attention to detail, organization, time management. High-achieving freshman encouraged to apply! Students considering graduate school strongly encouraged to apply!', - credit: '0-3 credits via EGN 4912', - stipend: - '1st semester, none unless selected for University Scholars; after trial period, $15/hour up to 10 hours per week', - application_requirements: - 'Resume, UF unofficial transcripts, faculty interview; email all application requirements to Lisa Anthony', - application_deadline: - 'applications are accepted on a rolling basis, but first come first served (recommend: Mar 15 or July 1 for Fall, Nov 15 for Spring, Mar 15 for Summer)', - website: 'http://init.cise.ufl.edu', - project_description: - 'Our lab focuses on advanced interaction technologies such as touch, gesture, voice, and mixed reality, in the context of human-AI interaction, education, healthcare, and serious games. Many of our projects emphasize children and/or families as a unique user group. Our projects advance human-computer interaction (HCI) research questions of how users want to interact with these natural modalities, and computer science research questions of how to build recognition algorithms that can understand user input in these ambiguous modalities. Top priorities currently: (a) designing intelligent chatbots for mobile health monitoring apps; (b) designing digital AI assistants to help novice users complete more expert tasks; and (c) designing human-centered interactive machine learning interfaces.', - }, -]; diff --git a/src/app/admin-applications/page.tsx b/src/app/admin-applications/page.tsx index c44068e..2d45771 100644 --- a/src/app/admin-applications/page.tsx +++ b/src/app/admin-applications/page.tsx @@ -6,8 +6,7 @@ import { Toaster, toast } from 'react-hot-toast'; import GetUserRole from '@/firebase/util/GetUserRole'; import 'firebase/firestore'; -import Applications from '@/component/Dashboard/Applications/Applications'; -import HeaderCard from '@/component/HeaderCard/HeaderCard'; +import Applications from '@/components/Dashboard/Applications/Applications'; import PageLayout from '@/components/PageLayout/PageLayout'; import { CssBaseline } from '@mui/material'; import { getNavItems } from '@/hooks/useGetItems'; diff --git a/src/app/applications/applicationSections.tsx b/src/app/applications/applicationSections.tsx index 487bfce..d43d816 100644 --- a/src/app/applications/applicationSections.tsx +++ b/src/app/applications/applicationSections.tsx @@ -1,12 +1,108 @@ /* components/DashboardSections.tsx */ import { Role } from '@/types/User'; -import { useState } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { DashboardCard } from '@/components/DashboardCard/DashboardCard'; import { NavbarItem } from '@/types/navigation'; 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 ApplicationCard from '@/components/Research/ApplicationCard'; +import firebase from '@/firebase/firebase_config'; +import { useAuth } from '@/firebase/auth/auth_context'; + +function ResearchApplicationsList() { + const { user } = useAuth(); + const [applications, setApplications] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchApplications = useCallback(async () => { + if (!user?.uid) return; + setLoading(true); + try { + const snapshot = await firebase + .firestore() + .collectionGroup('applications') + .where('uid', '==', user.uid) + .get(); + + const results = await Promise.all( + snapshot.docs.map(async (appDoc) => { + const appData = appDoc.data(); + const listingRef = appDoc.ref.parent.parent; + let listingData: any = {}; + if (listingRef) { + const listingSnap = await listingRef.get(); + if (listingSnap.exists) { + listingData = listingSnap.data(); + } + } + return { + appId: appDoc.id, + ...appData, + listingData, + }; + }) + ); + + const mapped = results.map((doc: any) => ({ + appid: doc.appId, + app_status: doc.app_status, + terms_available: doc.listingData?.terms_available || 'N/A', + date_applied: doc.date || 'N/A', + department: doc.department || doc.listingData?.department || 'N/A', + faculty_mentor: doc.listingData?.faculty_mentor, + faculty_contact: doc.listingData?.faculty_contact, + project_title: doc.listingData?.project_title || 'N/A', + project_description: + doc.listingData?.project_description || 'No description provided', + degree: doc.degree || 'N/A', + })); + + setApplications(mapped); + } catch (error) { + console.error('Error fetching research applications:', error); + } finally { + setLoading(false); + } + }, [user?.uid]); + + useEffect(() => { + fetchApplications(); + }, [fetchApplications]); + + if (loading) { + return ( +

Loading research applications...

+ ); + } + + if (applications.length === 0) { + return

No research applications at this time.

; + } + + return ( +
+ {applications.map((item, index) => ( +
+ +
+ ))} +
+ ); +} + export default function ApplicationSections({ role, navItems, @@ -47,7 +143,7 @@ export default function ApplicationSections({

Research

-

No available applications at this time.

+
); diff --git a/src/app/faculty-stats/page.tsx b/src/app/faculty-stats/page.tsx index f2ad004..591415a 100644 --- a/src/app/faculty-stats/page.tsx +++ b/src/app/faculty-stats/page.tsx @@ -15,8 +15,8 @@ import GetUserRole from '@/firebase/util/GetUserRole'; import 'firebase/firestore'; import firebase from '@/firebase/firebase_config'; import { read, utils, writeFile, readFile } from 'xlsx'; -import FacultyStats from '@/component/Dashboard/Users/FacultyStats'; -import HeaderCard from '@/component/HeaderCard/HeaderCard'; +import FacultyStats from '@/components/Dashboard/Users/FacultyStats'; +import HeaderCard from '@/components/HeaderCard/HeaderCard'; export default function User() { const { user } = useAuth(); @@ -107,140 +107,141 @@ export default function User() { if (role !== 'admin') return
Forbidden
; return ( <> - - - - (event.currentTarget.value = '')} - /> - - -
-
+
+
- - setOpen(false)} - > - + setOpen(false)} > - Clear Data - -
- - - Are you sure you want to clear all existing faculty - statistics? - - - - + - - -
-
+ + + + +
- +
); } diff --git a/src/app/faculty/page.tsx b/src/app/faculty/page.tsx index 541d9c2..516c8ef 100644 --- a/src/app/faculty/page.tsx +++ b/src/app/faculty/page.tsx @@ -4,7 +4,7 @@ import { useSearchParams } from 'next/navigation'; import firebase from '@/firebase/firebase_config'; import 'firebase/firestore'; import FacultyDetails from '@/component/FacultyDetails/FacultyDetails'; -import HeaderCard from '@/component/HeaderCard/HeaderCard'; +import HeaderCard from '@/components/HeaderCard/HeaderCard'; import { FacultyStats } from '@/types/User'; // import { useFacultyStats } from '@/hooks/useFacultyStats'; import { Toaster } from 'react-hot-toast'; diff --git a/src/app/models/ResearchModel.ts b/src/app/models/ResearchModel.ts index 8e734d3..eb6dcc3 100644 --- a/src/app/models/ResearchModel.ts +++ b/src/app/models/ResearchModel.ts @@ -1,17 +1,83 @@ export interface ResearchListing { id: string; + docID?: string; project_title: string; department: string; - faculty_mentor: string; - phd_student_mentor: string; + project_description: string; + application_deadline: string; + prerequisites: string; + + // New fields (target schema) + faculty_contact?: string; + phd_student_contact?: string; + nature_of_job?: string; + hours_per_week?: string; + compensation?: string; + image_url?: string; + + // Kept fields (still used in display) terms_available: string; student_level: string; - prerequisites: string; - credit: string; - stipend: string; application_requirements: string; - application_deadline: string; website: string; - project_description: string; + + // Legacy fields (kept optional for backward compatibility) + faculty_mentor?: { [email: string]: string } | string; + phd_student_mentor?: string | { [key: string]: string }; + credit?: string; + stipend?: string; + + // System fields + creator_id?: string; faculty_members?: string[]; + applications?: Array<{ + id: string; + student_id: string; + app_status: 'Pending' | 'Approved' | 'Denied'; + [key: string]: any; + }>; +} + +/** + * Normalizes a raw Firestore document into the new ResearchListing shape, + * deriving new fields from legacy fields when they are missing. + */ +export function normalizeResearchListing(raw: any): ResearchListing { + const listing = { ...raw }; + + // faculty_contact: derive from faculty_mentor if missing + if (!listing.faculty_contact && listing.faculty_mentor) { + if (typeof listing.faculty_mentor === 'object') { + listing.faculty_contact = Object.keys(listing.faculty_mentor)[0] || ''; + } else if (typeof listing.faculty_mentor === 'string') { + listing.faculty_contact = listing.faculty_mentor; + } + } + + // phd_student_contact: derive from phd_student_mentor if missing + if (!listing.phd_student_contact && listing.phd_student_mentor) { + if (typeof listing.phd_student_mentor === 'string') { + listing.phd_student_contact = listing.phd_student_mentor; + } else if (typeof listing.phd_student_mentor === 'object') { + listing.phd_student_contact = + Object.keys(listing.phd_student_mentor)[0] || ''; + } + } + + // compensation: derive from credit + stipend if missing + if (!listing.compensation) { + const parts: string[] = []; + if (listing.credit) parts.push(`${listing.credit} credits`); + if (listing.stipend) parts.push(`$${listing.stipend}`); + listing.compensation = parts.join(', ') || ''; + } + + // defaults for new fields + listing.nature_of_job = listing.nature_of_job || ''; + listing.hours_per_week = listing.hours_per_week || ''; + listing.image_url = listing.image_url || ''; + listing.faculty_contact = listing.faculty_contact || ''; + listing.phd_student_contact = listing.phd_student_contact || ''; + + return listing as ResearchListing; } diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index b5ef74a..3ad6029 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,5 +1,5 @@ 'use client'; -import HeaderCard from '@/component/HeaderCard/HeaderCard'; +import HeaderCard from '@/components/HeaderCard/HeaderCard'; import Link from 'next/link'; import { Toaster } from 'react-hot-toast'; import { redirect } from 'next/navigation'; diff --git a/src/app/page.tsx b/src/app/page.tsx index d973948..f18f50c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,4 @@ -import { EceLogoPng } from '@/component/EceLogoPng/EceLogoPng'; +import { EceLogoPng } from '@/components/EceLogoPng/EceLogoPng'; import styles from './style.module.css'; import AuthSwitcher from './AuthSwitcher'; export const metadata = { diff --git a/src/app/profile/temp.tsx b/src/app/profile/temp.tsx index 5fb45ab..cc2bca0 100644 --- a/src/app/profile/temp.tsx +++ b/src/app/profile/temp.tsx @@ -14,7 +14,7 @@ import { Typography, } from '@mui/material'; import './style.css'; -import HeaderCard from '@/component/HeaderCard/HeaderCard'; +import HeaderCard from '@/components/HeaderCard/HeaderCard'; import DeleteUserButton from './DeleteUserButton'; import GetUserRole from '@/firebase/util/GetUserRole'; import { updateProfile } from 'firebase/auth'; diff --git a/src/app/users/page.tsx b/src/app/users/page.tsx index 2c9acf2..30d18ef 100644 --- a/src/app/users/page.tsx +++ b/src/app/users/page.tsx @@ -16,10 +16,10 @@ import { getNavItems } from '@/hooks/useGetItems'; import { useAuth } from '@/firebase/auth/auth_context'; import GetUserRole from '@/firebase/util/GetUserRole'; -import UserGrid from '@/component/Dashboard/Users/UserGrid'; -import ApprovalGrid from '@/component/Dashboard/Users/ApprovalGrid'; +import UserGrid from '@/components/Dashboard/Users/UserGrid'; +import ApprovalGrid from '@/components/Dashboard/Users/ApprovalGrid'; -interface PageProps { } +interface PageProps {} const User: FC = () => { const { user } = useAuth(); @@ -91,4 +91,3 @@ const User: FC = () => { }; export default User; - diff --git a/src/component/Dashboard/Applications/ApplicationGrid.tsx b/src/component/Dashboard/Applications/ApplicationGrid.tsx index 3c64860..8b98237 100644 --- a/src/component/Dashboard/Applications/ApplicationGrid.tsx +++ b/src/component/Dashboard/Applications/ApplicationGrid.tsx @@ -187,8 +187,9 @@ export default function ApplicationGrid(props: ApplicationGridProps) { // Get the current date in month/day/year format const current = new Date(); - const current_date = `${current.getMonth() + 1 - }-${current.getDate()}-${current.getFullYear()}`; + const current_date = `${ + current.getMonth() + 1 + }-${current.getDate()}-${current.getFullYear()}`; const assignmentObject = { date: current_date as string, @@ -341,7 +342,7 @@ export default function ApplicationGrid(props: ApplicationGridProps) { if (userRole === 'admin') { const unsubscribe = applicationsRef.onSnapshot((querySnapshot) => { const data = querySnapshot.docs - .filter(function(doc) { + .filter(function (doc) { if (doc.data().status != 'Admin_denied') { if ( doc.data().status == 'Admin_approved' && @@ -356,16 +357,16 @@ export default function ApplicationGrid(props: ApplicationGridProps) { }) .map( (doc) => - ({ - id: doc.id, - ...doc.data(), - courses: Object.entries(doc.data().courses) - .filter(([key, value]) => value == 'approved') - .map(([key, value]) => key), - allcourses: Object.entries(doc.data().courses).map( - ([key, value]) => key - ), - } as Application) + ({ + id: doc.id, + ...doc.data(), + courses: Object.entries(doc.data().courses) + .filter(([key, value]) => value == 'approved') + .map(([key, value]) => key), + allcourses: Object.entries(doc.data().courses).map( + ([key, value]) => key + ), + } as Application) ); setApplicationData(data); }); @@ -393,10 +394,10 @@ export default function ApplicationGrid(props: ApplicationGridProps) { applicationsRef.get().then((querySnapshot) => { const data = querySnapshot.docs.map( (doc) => - ({ - id: doc.id, - ...doc.data(), - } as Application) + ({ + id: doc.id, + ...doc.data(), + } as Application) ); setApplicationData(data); }); @@ -438,8 +439,9 @@ export default function ApplicationGrid(props: ApplicationGridProps) { type: 'applicationStatusDenied', data: { user: { - name: `${applicationData.firstname ?? ''} ${applicationData.lastname ?? '' - }`.trim(), + name: `${applicationData.firstname ?? ''} ${ + applicationData.lastname ?? '' + }`.trim(), email: applicationData.email, }, position: applicationData.position, @@ -492,8 +494,9 @@ export default function ApplicationGrid(props: ApplicationGridProps) { type: 'applicationStatusApproved', data: { user: { - name: `${applicationData.firstname ?? ''} ${applicationData.lastname ?? '' - }`.trim(), + name: `${applicationData.firstname ?? ''} ${ + applicationData.lastname ?? '' + }`.trim(), email: applicationData.email, }, position: assignmentData.position, @@ -966,59 +969,44 @@ export default function ApplicationGrid(props: ApplicationGridProps) { } const ODD_OPACITY = 0.2; - // ✅ 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', - }, - '& .MuiDataGrid-cell:first-of-type': { - paddingLeft: '25px', - }, - + 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 ( params.indexRelativeToCurrentPage % 2 === 0 ? 'even' : 'odd' } - sx={{ - borderRadius: '16px', maxHeight: '80vh', maxWidth: '180vh', - minHeight: '80vh', minWidth: '180vh' - }} + sx={{ borderRadius: '16px' }} /> ; - } - - // Default: return nothing - if (userRole !== 'admin') return <>; - - // Admin: tabs to switch between grids - return ( - - - - setTab(newValue)} - aria-label="applications view switch" - TabIndicatorProps={{ - style: { - height: 3, - borderRadius: 2, - }, - }} - > - - - - - - - {tab === 0 ? ( - + // if admin + if (userRole === 'admin') { + return ( + <> + +

Applications

-
- ) : ( - - +

Assignments

-
- )} -
- ); + + + ); + } + // if faculy + else if (userRole === 'faculty') { + return ( + <> + +

Applications

+ +
+ + ); + } + // default: return nothing + return <>; } - diff --git a/src/component/Dashboard/Applications/AssignmentGrid.tsx b/src/component/Dashboard/Applications/AssignmentGrid.tsx index 9a763ae..f0d5fd7 100644 --- a/src/component/Dashboard/Applications/AssignmentGrid.tsx +++ b/src/component/Dashboard/Applications/AssignmentGrid.tsx @@ -143,26 +143,26 @@ export default function AssignmentGrid(props: AssignmentGridProps) { const unsubscribe = assignmentsRef.onSnapshot((querySnapshot) => { const data = querySnapshot.docs.map( (doc) => - ({ - id: doc.id, - ...doc.data(), - firstName: - doc.data().name != undefined - ? doc.data().name.split(' ')[0] - : ' ', - lastName: - doc.data().name != undefined - ? doc.data().name.split(' ')[1] - : ' ', - year: - doc.data().semesters != undefined - ? doc.data().semesters[0].split(' ')[1] - : ' ', - fte: 15, - pname: 'DEPARTMENT TA/UPIS', - pid: '000108927', - hr: 15, - } as Assignment) + ({ + id: doc.id, + ...doc.data(), + firstName: + doc.data().name != undefined + ? doc.data().name.split(' ')[0] + : ' ', + lastName: + doc.data().name != undefined + ? doc.data().name.split(' ')[1] + : ' ', + year: + doc.data().semesters != undefined + ? doc.data().semesters[0].split(' ')[1] + : ' ', + fte: 15, + pname: 'DEPARTMENT TA/UPIS', + pid: '000108927', + hr: 15, + } as Assignment) ); setAssignmentData(data); }); @@ -762,61 +762,44 @@ export default function AssignmentGrid(props: AssignmentGridProps) { ]; const ODD_OPACITY = 0.2; - // ✅ 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', - }, - '& .MuiDataGrid-cell:first-of-type': { - paddingLeft: '25px', - }, - + 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 ( params.indexRelativeToCurrentPage % 2 === 0 ? 'even' : 'odd' - } sx={{ - borderRadius: '16px', maxHeight: '70vh', maxWidth: '170vh', - minHeight: '70vh', minWidth: '170vh' - }} - + } + sx={{ borderRadius: '16px' }} /> 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/component/Dashboard/Welcome/Welcome.tsx b/src/component/Dashboard/Welcome/Welcome.tsx index 820d5c1..cbb555b 100644 --- a/src/component/Dashboard/Welcome/Welcome.tsx +++ b/src/component/Dashboard/Welcome/Welcome.tsx @@ -90,13 +90,6 @@ export default function DashboardWelcome(props: DashboardProps) { image="https://c.animaapp.com/vYQBTcnO/img/profile@2x.png" /> - - - )} @@ -228,13 +221,6 @@ export default function DashboardWelcome(props: DashboardProps) { image="https://c.animaapp.com/vYQBTcnO/img/profile@2x.png" /> - - - )} @@ -291,13 +277,6 @@ export default function DashboardWelcome(props: DashboardProps) { text="Profile" /> - - - )} diff --git a/src/component/SignUpCard/style.css b/src/component/SignUpCard/style.css new file mode 100644 index 0000000..3538089 --- /dev/null +++ b/src/component/SignUpCard/style.css @@ -0,0 +1 @@ +fatal: path 'src/component/SignUpCard/style.css' exists on disk, but not in 'main' diff --git a/src/component/TopNavBarSigned/style.css b/src/component/TopNavBarSigned/style.css index 2e38adb..35faec8 100644 --- a/src/component/TopNavBarSigned/style.css +++ b/src/component/TopNavBarSigned/style.css @@ -1,7 +1,7 @@ .top-nav-bar-signed { height: 37px; position: fixed; - width: 379px; + width: 279px; } .top-nav-bar-signed .text-wrapper-3 { @@ -9,7 +9,7 @@ font-family: "SF Pro Display-Bold", Helvetica; font-size: 16px; font-weight: 700; - left: 210px; + left: 100px; letter-spacing: 0; line-height: normal; position: absolute; @@ -23,21 +23,7 @@ font-family: "SF Pro Display-Bold", Helvetica; font-size: 16px; font-weight: 700; - left: 130px; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 8px; - white-space: nowrap; - width: 51px; -} - -.top-nav-bar-signed .text-wrapper-2 { - color: #ffffff; - font-family: "SF Pro Display-Bold", Helvetica; - font-size: 18px; - font-weight: 700; - left: 25px; + left: 0; letter-spacing: 0; line-height: normal; position: absolute; @@ -50,7 +36,7 @@ all: unset; box-sizing: border-box; height: 37px; - left: 307px; + left: 207px; position: absolute; top: 0; width: 117px; diff --git a/src/components/Dashboard/Applications/AppStatus.tsx b/src/components/Dashboard/Applications/AppStatus.tsx new file mode 100644 index 0000000..eef2c1a --- /dev/null +++ b/src/components/Dashboard/Applications/AppStatus.tsx @@ -0,0 +1,45 @@ +import SubmitAppDialog from './ApplicationDialog'; +/* +there are multiple states represented here: + 1. student has applied (student_applied) +simply display the application status + 2. student has been denied (student_denied) +display that the student has been denied and offer a dialog with the application form to resubmit + 3. student has been accepted but not assigned (student_accepted) +display that the student has been accepted and say that they will receive an email when +they have been assigned a course +*/ + +interface AppStatusProps { + user: any; + userRole: string; +} + +export default function ShowApplicationStatus(props: AppStatusProps) { + const { userRole, user } = props; + + if (userRole === 'student_applied') { + return ( + <> +

Your application has been received.

+

It is under review.

+ + ); + } else if (userRole === 'student_denied') { + return ( + <> +

Your application has been denied.

+

You can reapply below:

+ + + ); + } else if (userRole === 'student_accepted') { + return ( + <> +

Your application has been accepted!

+

You shall be assigned a course soon.

+ + ); + } + return <>; +} diff --git a/src/components/Dashboard/Applications/AppView.tsx b/src/components/Dashboard/Applications/AppView.tsx new file mode 100644 index 0000000..af19136 --- /dev/null +++ b/src/components/Dashboard/Applications/AppView.tsx @@ -0,0 +1,228 @@ +import * as React from 'react'; +import './style.css'; +import { useCallback } from 'react'; + +import ThumbDownOffAltIcon from '@mui/icons-material/ThumbDownOffAlt'; +import ThumbUpOffAltIcon from '@mui/icons-material/ThumbUpOffAlt'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import firebase from '@/firebase/firebase_config'; +import 'firebase/firestore'; +import { GridRowId } from '@mui/x-data-grid'; + +export interface AppViewProps { + uid: string; + close: () => void; + handleDenyClick: (id: GridRowId) => void; + handleOpenAssignmentDialog: (id: GridRowId) => void; +} + +export default function AppView({ + close, + uid, + handleDenyClick, + handleOpenAssignmentDialog, +}: AppViewProps) { + const [docData, setDocData] = React.useState(null); + + // get application object from uid + const applicationsRef = firebase.firestore().collection('applications'); + const docRef = applicationsRef.doc(uid); + + const onThumbUpClick = useCallback( + (event: any) => { + event?.stopPropagation(); + handleOpenAssignmentDialog(uid); + }, + [handleOpenAssignmentDialog, uid] + ); + + const onThumbDownIconClick = useCallback( + (event: any) => { + event?.stopPropagation(); + handleDenyClick(uid); + }, + [handleDenyClick, uid] + ); + + React.useEffect(() => { + docRef + .get() + .then((doc) => { + if (doc.exists) { + setDocData(doc.data()); + } else { + console.log('No such document!'); + } + }) + .catch((error) => { + console.log('Error getting document:', error); + }); + }, [uid, docRef]); // Only re-run the effect if uid changes + return ( + + {docData && ( + <> +
+
+
+
+ {docData.firstname[0].toUpperCase() + + docData.lastname[0].toUpperCase()} +
+
+
+
+ {docData.firstname} {docData.lastname} +
+ +
+
{docData.email}
+
{docData.phonenumber}
+
+
+
+ + +
+
Review
+
+
+
+ +
+
+
Applying for:
+
{docData.position}
+
+
+
Semester(s):
+
{docData.available_semesters.join(', ')}
+
+
+
Availability:
+
+ {docData.available_hours.join(', ')} +
+
+
+
All Course(s):
+
+ {Object.entries(docData.courses) + .map(([key, value]) => key) + .join(', ')} +
+
+
+
Faculty Approved Course(s):
+
+ {Object.entries(docData.courses) + .filter(([key, value]) => value == 'accepted') + .map(([key, value]) => key) + .join(', ')} +
+
+ +
+
+
+
Department:
+
+ {docData.department} +
+
+ +
+
Degree:
+
+ {docData.degree} +
+
+ +
+
GPA:
+
+ {docData.gpa} +
+
+
+ +
+ Qualifications: +
+
+ {docData.qualifications} +
+
+ Graduate Plan: +
+
+ {docData.additionalprompt} +
+
+ Resume Link: +
+ {docData.resume_link ? ( + + {docData.resume_link} + + ) : ( +
+ No resume link provided. +
+ )} +
+
+ + )} +
+ ); +} diff --git a/src/components/Dashboard/Applications/Application.tsx b/src/components/Dashboard/Applications/Application.tsx new file mode 100644 index 0000000..bebd273 --- /dev/null +++ b/src/components/Dashboard/Applications/Application.tsx @@ -0,0 +1,456 @@ +'use client'; +import * as React 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 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 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 { toast } from 'react-hot-toast'; +import { LinearProgress } from '@mui/material'; +import Snackbar from '@mui/material/Snackbar'; +import MuiAlert, { AlertProps } from '@mui/material/Alert'; + +import { useState } from 'react'; +// note that the application needs to be able to be connected to a specific faculty member +// so that the faculty member can view the application and accept/reject it +// the user can indicate whether or not it is unspecified I suppose? +// but that would leave a little bit of a mess. +// best to parse the existing courses and then have the user select +// from a list of existing courses +// ...yeah that's probably the best way to do it + +export default function Application() { + // get the current user's uid + + const { user } = useAuth(); + const userId = user.uid; + + // get the current date in month/day/year format + const current = new Date(); + const current_date = `${ + current.getMonth() + 1 + }-${current.getDate()}-${current.getFullYear()}`; + + // extract the nationality + 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(); + // extract the form data from the current event + const formData = new FormData(event.currentTarget); + + // extract availability checkbox's values + 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'); + } + + // extract semester checkbox's values + const semesterCheckbox_fall_2023 = + formData.get('semesterCheckbox_fall_2023') === 'on'; + const semesterCheckbox_spring_2024 = + formData.get('semesterCheckbox_spring_2024') === 'on'; + + const semesterArray: string[] = []; + if (semesterCheckbox_fall_2023) { + semesterArray.push('Fall 2023'); + } + if (semesterCheckbox_spring_2024) { + semesterArray.push('Spring 2024'); + } + + // get courses as array + const coursesString = formData.get('course-prompt') as string; + + const coursesArray = coursesString + .split(',') + .map((professorEmail) => professorEmail.trim()); + + // extract the specific user data from the form data into a parsable object + 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: formData.get('proficiency-select') as string, + position: formData.get('positions-radio-group') as string, + available_hours: availabilityArray as string[], + available_semesters: semesterArray as string[], + courses: coursesArray as string[], + qualifications: formData.get('qualifications-prompt') as string, + uid: userId, + date: current_date, + status: 'Submitted', + }; + + if (!applicationData.email.includes('ufl')) { + 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.phonenumber === '') { + toast.error('Please enter a valid phone number!'); + setLoading(false); + } 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.englishproficiency === null || + applicationData.englishproficiency === '' + ) { + toast.error('Please select your english proficiency level!'); + setLoading(false); + return; + } else if ( + applicationData.nationality === null || + applicationData.nationality === '' + ) { + toast.error('Please select your nationality!'); + setLoading(false); + return; + } else if ( + applicationData.position === null || + applicationData.position === '' + ) { + toast.error('Please enter a position!'); + 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 (applicationData.courses.length == 0) { + toast.error('Please enter your courses!'); + setLoading(false); + return; + } else { + // console.log(applicationData); // FOR DEBUGGING ONLY! + + // use fetch to send the application data to the server + // this goes to a cloud function which creates a document based on + // the data from the form, identified by the user's firebase auth uid + 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) { + console.log('SUCCESS: Application data sent to server successfully'); + // now, update the role of the user to student_applied + await UpdateRole(userId, 'student_applied'); + // then, refresh the page somehow to reflect the state changing + // so the form goes away and the user can see the status of their application + location.reload(); + } else { + 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); + }; + return ( + + + + Application submitted successfully! + + + {loading ? : null} + + + + + + + TA/UPI/Grader Application + + + + Personal Information + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Demographic Information + + + + + Please select your nationality, based on country of origin.{' '} +
+ Federal laws prohibit discrimination based on a person's + national origin, race, color, religion, disability, sex, and + familial status. This question is asked to confirm the + demographic basis for the applicant's proficiency in + English. +
+ +
+ + + Please select your proficiency in English. + + + +
+ + Position Information + + + + + Please select the position for which you are interested in + applying. + + + + + + Please select the semester(s) for which you are applying. + + + + + + Please select one or more options describing the number of hours + per week you will be available. + + + + + + Please list the course(s) for which you are applying, separated + by commas. + + + + + + Please provide your most recently calculated cumulative UF GPA. + + + + + + Please describe your qualifications for the position and + course(s) for which you are applying.
+ + If you have been a TA, UPI, or grader before, please mention + the course(s) and teacher(s) for which you worked. + {' '} +

+ Write about any relevant experience, such as teaching, tutoring, + grading, or coursework.
+
+ +
+ +
+ + +
+
+
+ ); +} diff --git a/src/components/Dashboard/Applications/ApplicationDialog.tsx b/src/components/Dashboard/Applications/ApplicationDialog.tsx new file mode 100644 index 0000000..f9823f1 --- /dev/null +++ b/src/components/Dashboard/Applications/ApplicationDialog.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import Application from './Application'; +import { Toaster } from 'react-hot-toast'; + +export default function SubmitAppDialog() { + const [open, setOpen] = React.useState(false); + + const handleClickOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + return ( +
+ + + + + + + +
+ ); +} diff --git a/src/components/Dashboard/Applications/ApplicationGrid.tsx b/src/components/Dashboard/Applications/ApplicationGrid.tsx new file mode 100644 index 0000000..281cce2 --- /dev/null +++ b/src/components/Dashboard/Applications/ApplicationGrid.tsx @@ -0,0 +1,1327 @@ +// @ts-nocheck + +'use client'; +import * as React from 'react'; +import { useState, useEffect } from 'react'; +import Box from '@mui/material/Box'; + +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/DeleteOutlined'; +import SaveIcon from '@mui/icons-material/Save'; +import CancelIcon from '@mui/icons-material/Close'; +import ZoomInIcon from '@mui/icons-material/ZoomIn'; +import ThumbUpAltIcon from '@mui/icons-material/ThumbUpAlt'; +import ThumbDownAltIcon from '@mui/icons-material/ThumbDownAlt'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormControl from '@mui/material/FormControl'; +import { + GridRowModesModel, + GridRowModes, + DataGrid, + GridColDef, + GridActionsCellItem, + GridEventListener, + GridRowId, + GridRowModel, + GridRowEditStopReasons, + GridRowsProp, + GridToolbarContainer, + GridToolbarExport, + GridToolbarFilterButton, + GridToolbarColumnsButton, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import firebase from '@/firebase/firebase_config'; +import 'firebase/firestore'; +import { query, where, collection, getDocs, getDoc } from 'firebase/firestore'; +import { useAuth } from '@/firebase/auth/auth_context'; +import GetUserName from '@/firebase/util/GetUserName'; +import { alpha, styled } from '@mui/material/styles'; +import { gridClasses } from '@mui/x-data-grid'; + +import { + Dialog, + DialogContent, + DialogTitle, + DialogContentText, + Button, + TextField, + DialogActions, + LinearProgress, +} from '@mui/material'; +import { purple } from '@mui/material/colors'; + +import UnderDevelopment from '@/component/UnderDevelopment'; +import AppView from './AppView'; +import { ThumbDownOffAlt, ThumbUpOffAlt } from '@mui/icons-material'; + +interface Application { + id: string; + additionalprompt: string; + available_hours: string; + available_semesters: string; + courses: string[]; + date: string; + degree: string; + department: string; + email: string; + englishproficiency: string; + firstname: string; + gpa: string; + lastname: string; + nationality: string; + phonenumber: string; + position: string; + qualifications: string; + semesterstatus: string; + ufid: string; + isNew?: boolean; + mode?: 'edit' | 'view' | undefined; +} + +interface ApplicationGridProps { + userRole: string; +} + +function getFullName(params: GridValueGetterParams) { + return `${params?.row.firstname || ''} ${params?.row.lastname || ''}`; +} + +export default function ApplicationGrid(props: ApplicationGridProps) { + // current user + const { user } = useAuth(); + const { userRole } = props; + const userName = GetUserName(user?.uid); + const [paginationModel, setPaginationModel] = React.useState({ + page: 0, + pageSize: 25, + }); + const [hours, setHours] = React.useState(0); + + // application props + const [applicationData, setApplicationData] = React.useState( + [] + ); + const [valueRadio, setValueRadio] = React.useState(''); + + const handleChangeRadio = (event: React.ChangeEvent) => { + setValueRadio((event.target as HTMLInputElement).value); + }; + // assignment dialog pop-up view setup + const [openAssignmentDialog, setOpenAssignmentDialog] = React.useState(false); + const [openDenyDialog, setOpenDenyDialog] = React.useState(false); + + const handleOpenAssignmentDialog = async (id: GridRowId) => { + const statusRef = firebase + .firestore() + .collection('applications') + .doc(id.toString()); + + const doc = await getDoc(statusRef); + setCodes( + Object.entries(doc.data().courses) + .filter(([key, value]) => value == 'approved') + .map(([key, value]) => key) + ); + setSelectedUserGrid(id); + + setOpenAssignmentDialog(true); + }; + + const handleDenyAssignmentDialog = async (id: GridRowId) => { + const statusRef = firebase + .firestore() + .collection('applications') + .doc(id.toString()); + + const doc = await getDoc(statusRef); + + setCodes( + Object.entries(doc.data().courses) + .filter(([key, value]) => value == 'denied') // Change 'accepted' to 'denied' + .map(([key, value]) => key) + ); + + setSelectedUserGrid(id); + setOpenDenyDialog(true); + }; + + const handleCloseAssignmentDialog = () => { + setOpenAssignmentDialog(false); + }; + const handleCloseDenyDialog = () => { + setOpenDenyDialog(false); + }; + + const handleSubmitAssignment = async ( + event: React.FormEvent + ) => { + event.preventDefault(); + setLoading(true); + + try { + const student_uid = selectedUserGrid as string; + const statusRef = firebase + .firestore() + .collection('applications') + .doc(student_uid.toString()); + let doc = await getDoc(statusRef); + + const courseDetails = firebase + .firestore() + .collection('courses') + .doc(valueRadio); + const courseDoc = await getDoc(courseDetails); + + // Update student's application to approved + await firebase + .firestore() + .collection('applications') + .doc(student_uid.toString()) + .update({ + status: 'Admin_approved', + [`courses.${valueRadio}`]: 'approved', + }); + + // Get the current date in month/day/year format + const current = new Date(); + const current_date = `${ + current.getMonth() + 1 + }-${current.getDate()}-${current.getFullYear()}`; + + const assignmentObject = { + date: current_date as string, + student_uid: student_uid as string, + class_codes: valueRadio, + email: doc.data()?.email, + name: doc.data()?.firstname + ' ' + doc.data()?.lastname, + semesters: doc.data()?.available_semesters, + department: doc.data()?.department, + hours: [hours], + position: doc.data()?.position, + degree: doc.data()?.degree, + ufid: doc.data()?.ufid, + }; + + // Create the document within the "assignments" collection + const assignmentRef = firebase + .firestore() + .collection('assignments') + .doc(assignmentObject.student_uid); + + doc = await assignmentRef.get(); + let uid = assignmentObject.student_uid; + + if (doc.exists) { + let counter = 1; + let newRef = firebase + .firestore() + .collection('assignments') + .doc(`${uid}-${counter}`); + + // Loop to check for the next available document ID + while ((await newRef.get()).exists) { + counter++; + newRef = firebase + .firestore() + .collection('assignments') + .doc(`${uid}-${counter}`); + } + + // Create a new document with the updated UID and assignmentObject + await newRef.set(assignmentObject); + } else { + // Document does not exist, create the original document + await assignmentRef.set(assignmentObject); + } + + // Extract and process the professor emails + const emailArray = courseDoc + .data() + ?.professor_emails.split(';') + .map((email) => email.trim()); + + // Send emails after all documents have been fetched and updated + if (emailArray) { + for (const email of emailArray) { + try { + const response = await fetch( + 'https://us-central1-courseconnect-c6a7b.cloudfunctions.net/sendEmail', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + type: 'facultyAssignment', + data: { + userEmail: email, + position: doc.data()?.position, + classCode: courseDoc.data()?.code, + semester: courseDoc.data()?.semester, + }, + }), + } + ); + + if (response.ok) { + const data = await response.json(); + console.log('Email sent successfully:', data); + } else { + throw new Error('Failed to send email'); + } + } catch (error) { + console.error('Error sending email:', error); + } + } + } + + handleSendEmail(assignmentObject); + handleCloseAssignmentDialog(); + } catch (error) { + console.error('Error in handleSubmitAssignment:', error); + } finally { + setLoading(false); + } + }; + + // toolbar + interface EditToolbarProps { + setApplicationData: ( + newRows: (oldRows: GridRowsProp) => GridRowsProp + ) => void; + setRowModesModel: ( + newModel: (oldModel: GridRowModesModel) => GridRowModesModel + ) => void; + } + + function EditToolbar(props: EditToolbarProps) { + const { setApplicationData, setRowModesModel } = props; + + // Add state to control the dialog open status + const [open, setOpen] = React.useState(false); + + return ( + + + + + + ); + } + + // pop-up view setup + const [open, setOpen] = React.useState(false); + const [delDia, setDelDia] = React.useState(false); + const [delId, setDelId] = React.useState(); + + const [codes, setCodes] = React.useState([]); + const [selectedUserGrid, setSelectedUserGrid] = + React.useState(null); + + const handleClickOpenGrid = (id: GridRowId) => { + setSelectedUserGrid(id); + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + const handleDeleteDiagClose = () => { + setDelDia(false); + }; + + // fetching application data from firestore + const [loading, setLoading] = useState(false); + React.useEffect(() => { + const applicationsRef = firebase.firestore().collection('applications'); + + if (userRole === 'admin') { + const unsubscribe = applicationsRef.onSnapshot((querySnapshot) => { + const data = querySnapshot.docs + .filter(function (doc) { + if (doc.data().status != 'Admin_denied') { + if ( + doc.data().status == 'Admin_approved' && + Object.values(doc.data().courses).length < 2 + ) { + return false; + } + return true; + } else { + return false; + } + }) + .map( + (doc) => + ({ + id: doc.id, + ...doc.data(), + courses: Object.entries(doc.data().courses) + .filter(([key, value]) => value == 'approved') + .map(([key, value]) => key), + allcourses: Object.entries(doc.data().courses).map( + ([key, value]) => key + ), + } as Application) + ); + setApplicationData(data); + }); + // Clean up the subscription on unmount + return () => unsubscribe(); + } else if (userRole === 'faculty') { + // the faculty member can only see applications that specify the same class as they have + // get the courses that the application specifies + // find the courses that the faculty member teaches + // if there is an intersection, then the faculty member can see the application + + // find courses that the faculty member teaches + const facultyCourses = collection(firebase.firestore(), 'courses'); + const q = query( + facultyCourses, + where('professor_emails', 'array-contains', user?.email) + ); + const facultyCoursesSnapshot = getDocs(q); + + // now we have every course that the faculty member teaches + // we need the course code from each of them + // then we can compare them to the courses that the application specifies + // if there is an intersection, then the faculty member can see the application + + applicationsRef.get().then((querySnapshot) => { + const data = querySnapshot.docs.map( + (doc) => + ({ + id: doc.id, + ...doc.data(), + } as Application) + ); + setApplicationData(data); + }); + } + }, [userRole, user?.email]); + + const [rowModesModel, setRowModesModel] = React.useState( + {} + ); + + const handleRowEditStop: GridEventListener<'rowEditStop'> = ( + params, + event + ) => { + if (params.reason === GridRowEditStopReasons.rowFocusOut) { + event.defaultMuiPrevented = true; + } + }; + + const handleDenyEmail = async (id: GridRowId) => { + try { + const snapshot = await firebase + .firestore() + .collection('applications') + .doc(id.toString()) + .get(); + + if (snapshot.exists) { + const applicationData = snapshot.data() as Application; + + const response = await fetch( + 'https://us-central1-courseconnect-c6a7b.cloudfunctions.net/sendEmail', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + type: 'applicationStatusDenied', + data: { + user: { + name: `${applicationData.firstname ?? ''} ${ + applicationData.lastname ?? '' + }`.trim(), + email: applicationData.email, + }, + position: applicationData.position, + classCode: applicationData.courses, + }, + }), + } + ); + + if (response.ok) { + const data = await response.json(); + console.log('Email sent successfully:', data); + } else { + throw new Error('Failed to send email'); + } + } else { + throw new Error('Application data not found'); + } + } catch (error) { + console.error('Error sending email:', error); + } + }; + + const handleSendEmail = async (id: GridRowId) => { + try { + // Retrieve application data from Firestore + const snapshot = await firebase + .firestore() + .collection('applications') + .doc(id.student_uid.toString()) + .get(); + const snapshot2 = await firebase + .firestore() + .collection('assignments') + .doc(id.student_uid.toString()) + .get(); + + if (snapshot.exists) { + const applicationData = snapshot.data() as Application; + const assignmentData = snapshot2.data(); + // Send email using fetched application data + const response = await fetch( + 'https://us-central1-courseconnect-c6a7b.cloudfunctions.net/sendEmail', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + type: 'applicationStatusApproved', + data: { + user: { + name: `${applicationData.firstname ?? ''} ${ + applicationData.lastname ?? '' + }`.trim(), + email: applicationData.email, + }, + position: assignmentData.position, + classCode: assignmentData.class_codes, + }, + }), + } + ); + + if (response.ok) { + const data = await response.json(); + console.log('Email sent successfully:', data); + } else { + throw new Error('Failed to send email'); + } + } else { + throw new Error('Application data not found'); + } + } catch (error) { + console.error('Error sending email:', error); + } + }; + + // approve/deny click handlers + const handleDenyClick = async (id: GridRowId) => { + event.preventDefault(); + setLoading(true); + + try { + // Update the 'applications' collection in Firestore + await firebase + .firestore() + .collection('applications') + .doc(id.toString()) + .update({ status: 'Admin_denied' }); + + // Remove the denied row from the local state + setApplicationData((prevData) => { + const newData = prevData.filter((row) => row.id !== id); + return newData; // Only return the updated state without the denied row + }); + + await handleDenyEmail(id); + // Close the deny dialog + handleCloseDenyDialog(); + } catch (error) { + console.error('Error updating application document: ', error); + } finally { + setLoading(false); + } + }; + + const handleApproveClick = async (id: GridRowId) => { + setLoading(true); + try { + // Update the 'applications' collection + await firebase + .firestore() + .collection('applications') + .doc(id.toString()) + .update({ status: 'Approved' }); + + // Update the state locally to avoid reloading the entire data + setApplicationData((prevData) => + prevData.map((row) => + row.id === id ? { ...row, status: 'Approved' } : row + ) + ); + + // Send email notification or any other side effects + await handleSendEmail(id); + } catch (error) { + console.error('Error updating application document: ', error); + } finally { + setLoading(false); + } + }; + + const handleEditClick = (id: GridRowId) => () => { + setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); + }; + + const handleSaveClick = (id: GridRowId) => () => { + setLoading(true); + const updatedRow = applicationData.find((row) => row.id === id); + if (updatedRow) { + firebase + .firestore() + .collection('applications') + .doc(id.toString()) + .update(updatedRow) + .then(() => { + setRowModesModel({ + ...rowModesModel, + [id]: { mode: GridRowModes.View }, + }); + setLoading(false); + }) + .catch((error) => { + setLoading(false); + console.error('Error updating document: ', error); + }); + } else { + console.error('No matching user data found for id: ', id); + } + }; + + const handleDel = (id: GridRowId) => () => { + setDelId(id); + setDelDia(true); + }; + const handleDeleteClick = (id: GridRowId) => { + setLoading(true); + firebase + .firestore() + .collection('applications') + .doc(id.toString()) + .delete() + .then(() => { + setLoading(false); + setApplicationData(applicationData.filter((row) => row.id !== id)); + }) + .catch((error) => { + setLoading(false); + console.error('Error removing document: ', error); + }); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleDeleteClick(delId); + setDelDia(false); + }; + + const handleCancelClick = (id: GridRowId) => () => { + const editedRow = applicationData.find((row) => row.id === id); + if (editedRow!.isNew) { + firebase + .firestore() + .collection('applications') + .doc(id.toString()) + .delete() + .then(() => { + setApplicationData(applicationData.filter((row) => row.id !== id)); + }) + .catch((error) => { + console.error('Error removing document: ', error); + }); + } else { + setRowModesModel({ + ...rowModesModel, + [id]: { mode: GridRowModes.View, ignoreModifications: true }, + }); + } + }; + + const processRowUpdate = (newRow: GridRowModel, oldRow: GridRowModel) => { + setLoading(true); + const availableHoursArray = + typeof newRow.availability === 'string' && newRow.availability + ? newRow.availability.split(',').map((hour) => hour.trim()) + : oldRow.availability; + + const availableSemestersArray = + typeof newRow.semesters === 'string' && newRow.semesters + ? newRow.semesters.split(',').map((semester) => semester.trim()) + : oldRow.semesters; + + const coursesArray = + typeof newRow.courses === 'string' && newRow.courses + ? newRow.courses.split(',').map((course) => course.trim()) + : oldRow.courses; + + const updatedRow = { + ...(newRow as Application), + availability: availableHoursArray, + semesters: availableSemestersArray, + courses: coursesArray, + isNew: false, + }; + + if (updatedRow) { + if (updatedRow.isNew) { + return firebase + .firestore() + .collection('applications') + .add(updatedRow) + .then(() => { + setApplicationData( + applicationData.map((row) => + row.id === newRow.id ? updatedRow : row + ) + ); + setLoading(false); + return updatedRow; + }) + .catch((error) => { + setLoading(false); + console.error('Error adding document: ', error); + throw error; + }); + } else { + return firebase + .firestore() + .collection('applications') + .doc(updatedRow.id) + .update(updatedRow) + .then(() => { + setApplicationData( + applicationData.map((row) => + row.id === newRow.id ? updatedRow : row + ) + ); + setLoading(false); + return updatedRow; + }) + .catch((error) => { + setLoading(false); + console.error('Error updating document: ', error); + throw error; + }); + } + } else { + setLoading(false); + return Promise.reject( + new Error('No matching user data found for id: ' + newRow.id) + ); + } + }; + + const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { + setRowModesModel(newRowModesModel); + }; + + let columns: GridColDef[] = [ + { + field: 'actions', + type: 'actions', + headerName: 'Actions', + width: 370, + cellClassName: 'actions', + getActions: ({ id, row }) => { + const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; + + if (isInEditMode) { + return [ + } + label="Save" + sx={{ + color: 'primary.main', + }} + onClick={handleSaveClick(id)} + />, + } + label="Cancel" + onClick={handleCancelClick(id)} + color="inherit" + />, + ]; + } + + return [ + , + , + + , + } + label="Approve" + onClick={(event) => handleOpenAssignmentDialog(id)} + color="success" + />, + } + label="Deny" + onClick={() => handleDenyAssignmentDialog(id)} + color="error" + />, + ]; + }, + }, + { + field: 'fullname', + headerName: 'Full Name', + width: 200, + editable: false, + valueGetter: getFullName, + }, + + { field: 'email', headerName: 'Email', width: 230, editable: true }, + + { + field: 'degree', + headerName: 'Degree', + width: 100, + editable: true, + }, + + { + field: 'available_semesters', + headerName: 'Semester(s)', + width: 150, + editable: false, + }, + + { + field: 'allcourses', + headerName: 'All Course(s)', + width: 250, + editable: true, + }, + { + field: 'courses', + headerName: 'Faculty Approved Course(s)', + width: 250, + editable: true, + }, + + { field: 'position', headerName: 'Position', width: 70, editable: true }, + { field: 'date', headerName: 'Date', width: 100, editable: true }, + { + field: 'status', + headerName: 'Status', + width: 130, + editable: true, + renderCell: (params) => { + let color = '#f2a900'; + let backgroundColor = '#fffdf0'; + + if (params.value === 'Admin_approved') { + color = '#4caf50'; + backgroundColor = '#e8f5e9'; + } + + return ( + + {params.value} + + ); + }, + }, + , + ]; + + if (userRole === 'faculty') { + columns = [ + { + field: 'actions', + type: 'actions', + headerName: 'Actions', + width: 130, + cellClassName: 'actions', + getActions: ({ id }) => { + return [ + } + label="View" + onClick={(event) => handleClickOpenGrid(id)} + color="primary" + />, + } + label="Approve" + onClick={(event) => handleApproveClick(id)} + color="success" + />, + } + label="Deny" + onClick={(event) => handleDenyClick(id)} + color="error" + />, + ]; + }, + }, + { + field: 'ufid', + headerName: 'UFID', + width: 100, + editable: false, + }, + { field: 'position', headerName: 'Position', width: 70, editable: false }, + { + field: 'semesters', + headerName: 'Semester(s)', + width: 130, + editable: false, + }, + { + field: 'available_hours', + headerName: 'Hours', + width: 100, + editable: false, + }, + { + field: 'fullname', + headerName: 'Full Name', + width: 150, + editable: false, + valueGetter: getFullName, + }, + { field: 'email', headerName: 'Email', width: 200, editable: false }, + { field: 'courses', headerName: 'Courses', width: 200, editable: false }, + { + field: 'semesterstatus', + headerName: 'Academic Status', + width: 130, + editable: false, + }, + { field: 'date', headerName: 'Date', width: 80, editable: false }, + { + field: 'status', + headerName: 'App Status', + width: 100, + editable: false, + }, + ]; + } + const ODD_OPACITY = 0.2; + + // ✅ 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', + }, + '& .MuiDataGrid-cell:first-of-type': { + paddingLeft: '25px', + }, + + [`& .${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, + }, + })); + + return ( + + {loading ? : null} + + params.indexRelativeToCurrentPage % 2 === 0 ? 'even' : 'odd' + } + sx={{ + borderRadius: '16px', + maxHeight: '80vh', + maxWidth: '180vh', + minHeight: '80vh', + minWidth: '180vh', + }} + /> + + {/* Display the application data of the selected user */} + {selectedUserGrid && ( + + + + )} + + + + + Delete Applicant + +
handleSubmit(e)}> + + + Are you sure you want to delete this applicant? + + + + + + + +
+
+ + + Course Assignment +
+ + {codes != [] ? ( + <> + + Please select the course code to which the student shall be + assigned and the hours the student will work. + +
+ + + {codes.map((code) => { + return ( + } + label={code.replace(/,/g, ', ')} + /> + ); + })} + +
+ { + setHours(event.target.value); + }} + > + {' '} + +
{' '} + + ) : ( + + No faculty has accepted this student yet. + + )} +
+ + + + +
+
+ + + + Deny Applicant + +
+ + + Are you sure you want to deny this applicant? + + + + + + + +
+
+
+ ); +} diff --git a/src/components/Dashboard/Applications/Applications.tsx b/src/components/Dashboard/Applications/Applications.tsx new file mode 100644 index 0000000..658e539 --- /dev/null +++ b/src/components/Dashboard/Applications/Applications.tsx @@ -0,0 +1,85 @@ +'use client'; + +import * as React from 'react'; + +import ApplicationGrid from './ApplicationGrid'; +import AssignmentGrid from './AssignmentGrid'; + +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 Typography from '@mui/material/Typography'; + +// for admin and faculty views +interface ApplicationsProps { + userRole: string; +} + +export default function Applications(props: ApplicationsProps) { + const { userRole } = props; + + // 0 = Applications, 1 = Assignments + const [tab, setTab] = React.useState(0); + + // Faculty only has ApplicationGrid, so no tabs needed + if (userRole === 'faculty') { + return ; + } + + // Default: return nothing + if (userRole !== 'admin') return <>; + + // Admin: tabs to switch between grids + return ( + + + + setTab(newValue)} + aria-label="applications view switch" + TabIndicatorProps={{ + style: { + height: 3, + borderRadius: 2, + }, + }} + > + + + + + + + {tab === 0 ? ( + + + + ) : ( + + + + )} + + ); +} diff --git a/src/components/Dashboard/Applications/AssignView.tsx b/src/components/Dashboard/Applications/AssignView.tsx new file mode 100644 index 0000000..9120700 --- /dev/null +++ b/src/components/Dashboard/Applications/AssignView.tsx @@ -0,0 +1,773 @@ +import * as React from 'react'; + +import TextField from '@mui/material/TextField'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import firebase from '@/firebase/firebase_config'; +import 'firebase/firestore'; +import { Button, Grid, Input } from '@mui/material'; +import Paper from '@mui/material/Paper'; +import { table } from 'console'; +import { setDoc } from 'firebase/firestore'; + +export interface AppViewProps { + uid: string; +} + +export default function AppView({ uid }: AppViewProps) { + const [docData, setDocData] = React.useState(null); + const [studentName, setStudentName] = React.useState(''); + const [studentEmail, setStudentEmail] = React.useState(''); + const [studentId, setStudentId] = React.useState(''); + const [facultyId, setFacultyId] = React.useState(''); + const [startDate, setStartDate] = React.useState(''); + const [endDate, setEndDate] = React.useState(''); + const [hours, setHours] = React.useState(null); + const [title, setTitle] = React.useState(''); + const [percentage, setPercentage] = React.useState(''); + const [annualRate, setAnnualRate] = React.useState(''); + const [biweeklyRate, setBiweeklyRate] = React.useState(''); + const [targetAmt, setTargetAmt] = React.useState(''); + const [remote, setRemote] = React.useState(''); + + // get application object from uid + const applicationsRef = firebase.firestore().collection('assignments'); + const docRef = applicationsRef.doc(uid); + + function handleSave(event: any) { + setDoc(docRef, { + name: studentName, + email: studentEmail, + ufid: studentId, + supervisor_ufid: facultyId, + start_date: startDate, + end_date: endDate, + date: docData.date, + class_codes: docData.class_codes, + degree: docData.degree, + department: docData.department, + hours: Array.isArray(hours) ? hours : docData.hours, + position: docData.position, + semesters: docData.semesters, + student_uid: docData.student_uid, + title: title, + percentage: percentage, + annual_rate: annualRate, + biweekly_rate: biweeklyRate, + target_amount: targetAmt, + remote: remote, + }); + } + + React.useEffect(() => { + docRef + .get() + .then((doc) => { + if (doc.exists) { + setDocData(doc.data()); + console.log(doc.data()); + + setStudentName(doc.data().name); + setStudentEmail(doc.data().email); + setHours(doc.data().hours[0]); + if (doc.data().ufid === undefined) { + setStudentId(''); + } else { + setStudentId(doc.data().ufid); + } + + if (doc.data().supervisor_ufid === undefined) { + setFacultyId(''); + } else { + setFacultyId(doc.data().supervisor_ufid); + } + + if (doc.data().start_date === undefined) { + setStartDate(''); + } else { + setStartDate(doc.data().start_date); + } + + if (doc.data().end_date === undefined) { + setEndDate(''); + } else { + setEndDate(doc.data().end_date); + } + + if (doc.data().title === undefined) { + setTitle(''); + } else { + setTitle(doc.data().title); + } + + if (doc.data().percentage === undefined) { + setPercentage(''); + } else { + setPercentage(doc.data().percentage); + } + + if (doc.data().annual_rate === undefined) { + setAnnualRate(''); + } else { + setAnnualRate(doc.data().annual_rate); + } + + if (doc.data().biweekly_rate === undefined) { + setBiweeklyRate(''); + } else { + setBiweeklyRate(doc.data().biweekly_rate); + } + + if (doc.data().target_amount === undefined) { + setTargetAmt(''); + } else { + setTargetAmt(doc.data().target_amount); + } + + if (doc.data().remote === undefined) { + setRemote('No'); + } else { + setRemote(doc.data().remote); + } + console.log(studentName); + } else { + console.log('No such document!'); + } + }) + .catch((error) => { + console.log('Error getting document:', error); + }); + }, [uid]); // Only re-run the effect if uid changes + + return ( + + {docData && ( + + + + {[0, 1, 2, 3, 4, 5, 6, 7, 8].map((value) => { + if (value == 0) { + return ( + + + theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + borderRadius: 4, + }} + > + + Student Information: + + + + Name:{' '} + { + setStudentName(event.target.value); + }} + /> + + + + Email:{' '} + { + setStudentEmail(event.target.value); + }} + /> + + + + UFID:{' '} + { + setStudentId(event.target.value); + }} + /> + + + + ); + } else if (value == 1) { + return ( + + + theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + borderRadius: 4, + }} + > + + Supervisor Information: + + + + Supervisor Name: {docData.class_codes.split(' ')[4]} + + + + Supervisor Email: ----------- + + + + UFID:{' '} + { + setFacultyId(event.target.value); + }} + /> + + + + ); + } else if (value == 2) { + return ( + + + theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + borderRadius: 4, + }} + > + + Proxy Information: + + + + Proxy Name: Christophe Bobda + + + + Proxy Email: cbobda@ufl.edu + + + + UFID: ------ + + + + ); + } else if (value == 3) { + return ( + + + theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + borderRadius: 4, + }} + > + + Application Details: + + + + Requested Action: NEW HIRE + + + + Position Type: TA + + + + Degree Type: BS + + + + Available Hours: {docData.hours[0]} + + + + ); + } else if (value == 4) { + return ( + + + theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + borderRadius: 4, + }} + > + + Employment Duration: + + + + Semester: {docData.semesters} + + + + Starting Date:{' '} + { + setStartDate(event.target.value); + }} + /> + + + + End Date:{' '} + { + setEndDate(event.target.value); + }} + /> + + + + FTE: 15 Hours + + + + ); + } else if (value == 5) { + return ( + + + theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + borderRadius: 4, + }} + > + + Employment Details: + + + + Working Title:{' '} + { + setTitle(event.target.value); + }} + /> + + + + Duties: UPI in {docData.class_codes} + + + Remote:{' '} + { + setRemote(event.target.value); + }} + /> + + + + ); + } else if (value == 6) { + return ( + + + theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + borderRadius: 4, + }} + > + + Project Details: + + + + Project ID: 000108927 + + + + Project Name: DEPARTMENT TA/UPIS + + + + Percentage:{' '} + { + setPercentage(event.target.value); + }} + /> + + + + Hours:{' '} + { + setHours([event.target.value]); + }} + /> + + + + ); + } else if (value == 7) { + return ( + + + theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + borderRadius: 4, + }} + > + + Financial Details: + + + + Annual Rate:{' '} + { + setAnnualRate(event.target.value); + }} + /> + + + + Biweekly Rate:{' '} + { + setBiweeklyRate(event.target.value); + }} + />{' '} + + + + Hourly Rate: 15 + + + + Target Amount:{' '} + { + setTargetAmt(event.target.value); + }} + /> + + + + ); + } else { + return ( + + + + {/* */} + {/* theme.palette.mode === 'dark' ? '#1A2027' : '#fff', */} + {/* borderRadius: 4, */} + {/* }} */} + {/* /> */} + + ); + } + })} + + + + )} + + ); +} diff --git a/src/components/Dashboard/Applications/AssignViewOnly.tsx b/src/components/Dashboard/Applications/AssignViewOnly.tsx new file mode 100644 index 0000000..4566261 --- /dev/null +++ b/src/components/Dashboard/Applications/AssignViewOnly.tsx @@ -0,0 +1,595 @@ +import * as React from 'react'; + +import TextField from '@mui/material/TextField'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import firebase from '@/firebase/firebase_config'; +import 'firebase/firestore'; +import { Button, Grid, Input } from '@mui/material'; +import Paper from '@mui/material/Paper'; +import { table } from 'console'; +import { setDoc } from 'firebase/firestore'; + +export interface AppViewProps { + uid: string; +} + +export default function AppView({ uid }: AppViewProps) { + const [docData, setDocData] = React.useState(null); + const [studentName, setStudentName] = React.useState(''); + const [studentEmail, setStudentEmail] = React.useState(''); + const [studentId, setStudentId] = React.useState(''); + const [facultyId, setFacultyId] = React.useState(''); + const [startDate, setStartDate] = React.useState(''); + const [endDate, setEndDate] = React.useState(''); + const [hours, setHours] = React.useState(null); + const [title, setTitle] = React.useState(''); + const [percentage, setPercentage] = React.useState(''); + const [annualRate, setAnnualRate] = React.useState(''); + const [biweeklyRate, setBiweeklyRate] = React.useState(''); + const [targetAmt, setTargetAmt] = React.useState(''); + const [remote, setRemote] = React.useState(''); + + // get application object from uid + const applicationsRef = firebase.firestore().collection('assignments'); + const docRef = applicationsRef.doc(uid); + + function handleSave(event: any) { + setDoc(docRef, { + name: studentName, + email: studentEmail, + ufid: studentId, + supervisor_ufid: facultyId, + start_date: startDate, + end_date: endDate, + date: docData.date, + class_codes: docData.class_codes, + degree: docData.degree, + department: docData.department, + hours: Array.isArray(hours) ? hours : docData.hours, + position: docData.position, + semesters: docData.semesters, + student_uid: docData.student_uid, + title: title, + percentage: percentage, + annual_rate: annualRate, + biweekly_rate: biweeklyRate, + target_amount: targetAmt, + remote: remote, + }); + } + + React.useEffect(() => { + docRef + .get() + .then((doc) => { + if (doc.exists) { + setDocData(doc.data()); + console.log(doc.data()); + + setStudentName(doc.data().name); + setStudentEmail(doc.data().email); + setHours(doc.data().hours[0]); + if (doc.data().ufid === undefined) { + setStudentId(''); + } else { + setStudentId(doc.data().ufid); + } + + if (doc.data().supervisor_ufid === undefined) { + setFacultyId(''); + } else { + setFacultyId(doc.data().supervisor_ufid); + } + + if (doc.data().start_date === undefined) { + setStartDate(''); + } else { + setStartDate(doc.data().start_date); + } + + if (doc.data().end_date === undefined) { + setEndDate(''); + } else { + setEndDate(doc.data().end_date); + } + + if (doc.data().title === undefined) { + setTitle(''); + } else { + setTitle(doc.data().title); + } + + if (doc.data().percentage === undefined) { + setPercentage(''); + } else { + setPercentage(doc.data().percentage); + } + + if (doc.data().annual_rate === undefined) { + setAnnualRate(''); + } else { + setAnnualRate(doc.data().annual_rate); + } + + if (doc.data().biweekly_rate === undefined) { + setBiweeklyRate(''); + } else { + setBiweeklyRate(doc.data().biweekly_rate); + } + + if (doc.data().target_amount === undefined) { + setTargetAmt(''); + } else { + setTargetAmt(doc.data().target_amount); + } + + if (doc.data().remote === undefined) { + setRemote('No'); + } else { + setRemote(doc.data().remote); + } + console.log(studentName); + } else { + console.log('No such document!'); + } + }) + .catch((error) => { + console.log('Error getting document:', error); + }); + }, [uid]); // Only re-run the effect if uid changes + + return ( + + {docData && ( + + + + {[0, 1, 2, 3, 4, 5, 6, 7, 8].map((value) => { + if (value == 0) { + return ( + + + theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + borderRadius: 4, + }} + > + + Student Information: + + + + Name: {docData.name} + + + + Email: {docData.email} + + + + UFID: {studentId} + + + + ); + } else if (value == 1) { + return ( + + + theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + borderRadius: 4, + }} + > + + Supervisor Information: + + + + Supervisor Name: {docData.class_codes.split(' ')[4]} + + + + Supervisor Email: ----------- + + + + UFID: {facultyId} + + + + ); + } else if (value == 2) { + return ( + + + theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + borderRadius: 4, + }} + > + + Proxy Information: + + + + Proxy Name: Christophe Bobda + + + + Proxy Email: cbobda@ufl.edu + + + + UFID: ------ + + + + ); + } else if (value == 3) { + return ( + + + theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + borderRadius: 4, + }} + > + + Application Details: + + + + Requested Action: NEW HIRE + + + + Position Type: TA + + + + Degree Type: BS + + + + Available Hours: {docData.hours[0]} + + + + ); + } else if (value == 4) { + return ( + + + theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + borderRadius: 4, + }} + > + + Employment Duration: + + + + Semester: {docData.semesters} + + + + Starting Date: {startDate} + + + + End Date:{endDate} + + + + FTE: 15 Hours + + + + ); + } else if (value == 5) { + return ( + + + theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + borderRadius: 4, + }} + > + + Employment Details: + + + + Working Title: {title} + + + + Duties: UPI in {docData.class_codes} + + + Remote: {remote} + + + + ); + } else if (value == 6) { + return ( + + + theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + borderRadius: 4, + }} + > + + Project Details: + + + + Project ID: 000108927 + + + + Project Name: DEPARTMENT TA/UPIS + + + + Percentage: {percentage} + + + + Hours: {docData.hours[0]} + + + + ); + } else if (value == 7) { + return ( + + + theme.palette.mode === 'dark' ? '#1A2027' : '#fff', + borderRadius: 4, + }} + > + + Financial Details: + + + + Annual Rate: {annualRate} + + + + Biweekly Rate: {biweeklyRate} + + + + Hourly Rate: 15 + + + + Target Amount: {targetAmt} + + + + ); + } else { + return ( + + {/* */} + {/* theme.palette.mode === 'dark' ? '#1A2027' : '#fff', */} + {/* borderRadius: 4, */} + {/* }} */} + {/* /> */} + + ); + } + })} + + + + )} + + ); +} diff --git a/src/components/Dashboard/Applications/AssignmentGrid.tsx b/src/components/Dashboard/Applications/AssignmentGrid.tsx new file mode 100644 index 0000000..b2409e2 --- /dev/null +++ b/src/components/Dashboard/Applications/AssignmentGrid.tsx @@ -0,0 +1,1028 @@ +'use client'; +import * as React from 'react'; +import { useState } from 'react'; +import Box from '@mui/material/Box'; +import DeleteIcon from '@mui/icons-material/DeleteOutlined'; +import SaveIcon from '@mui/icons-material/Save'; +import CancelIcon from '@mui/icons-material/Close'; +import ZoomInIcon from '@mui/icons-material/ZoomIn'; +import { + GridRowModesModel, + GridRowModes, + DataGrid, + GridColDef, + GridActionsCellItem, + GridEventListener, + GridRowId, + GridRowModel, + GridRowEditStopReasons, + GridRowsProp, + GridToolbarContainer, + GridToolbarExport, + GridToolbarFilterButton, + GridToolbarColumnsButton, +} from '@mui/x-data-grid'; + +import EditIcon from '@mui/icons-material/Edit'; +import firebase from '@/firebase/firebase_config'; +import 'firebase/firestore'; +import { alpha, styled } from '@mui/material/styles'; +import { gridClasses } from '@mui/x-data-grid'; + +import { getDoc, getDocs, collection, query, where } from 'firebase/firestore'; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + LinearProgress, + TextField, +} from '@mui/material'; +import UnderDevelopment from '@/component/UnderDevelopment'; +import AssignView from './AssignView'; +import AssignViewOnly from './AssignViewOnly'; + +interface Assignment { + id: string; + approver_name: string; + approver_role: string; + approver_uid: string; + date: string; + isNew?: boolean; + mode?: 'edit' | 'view' | undefined; + firstName: string; + lastName: string; + year: string; + fte: number; + pname: string; + pid: string; + hr: number; + bwr: number; +} + +interface AssignmentGridProps { + userRole: string; +} + +export default function AssignmentGrid(props: AssignmentGridProps) { + const [loading, setLoading] = useState(false); + const { userRole } = props; + const [assignmentData, setAssignmentData] = React.useState([]); + + // toolbar + interface EditToolbarProps { + setAssignmentData: ( + newRows: (oldRows: GridRowsProp) => GridRowsProp + ) => void; + setRowModesModel: ( + newModel: (oldModel: GridRowModesModel) => GridRowModesModel + ) => void; + } + + function EditToolbar(props: EditToolbarProps) { + const { setAssignmentData, setRowModesModel } = props; + + // Add state to control the dialog open status + const [open, setOpen] = React.useState(false); + + return ( + + + + + + ); + } + + // pop-up view setup + const [open, setOpen] = React.useState(false); + const [openView, setOpenView] = React.useState(false); + const [delDia, setDelDia] = React.useState(false); + const [loading2, setLoading2] = React.useState(false); + const [delId, setDelId] = React.useState(); + const [selectedUserGrid, setSelectedUserGrid] = + React.useState(null); + const [courseEmailMap, setCourseEmailMap] = React.useState(new Map()); + + const handleClickOpenGrid = (id: GridRowId) => { + setSelectedUserGrid(id); + setOpen(true); + }; + + const handleClickViewGrid = (id: GridRowId) => { + setSelectedUserGrid(id); + setOpenView(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + const handleViewClose = () => { + setOpenView(false); + }; + const handleDeleteDiagClose = () => { + setDelDia(false); + }; + // assignment dialog pop-up view setup + const [openAssignmentDialog, setOpenAssignmentDialog] = React.useState(false); + const handleOpenAssignmentDialog = (id: GridRowId) => { + setSelectedUserGrid(id); + setOpenAssignmentDialog(true); + }; + const handleCloseAssignmentDialog = () => { + setOpenAssignmentDialog(false); + }; + + // application data from firestore + React.useEffect(() => { + const assignmentsRef = firebase.firestore().collection('assignments'); + + const unsubscribe = assignmentsRef.onSnapshot((querySnapshot) => { + const data = querySnapshot.docs.map( + (doc) => + ({ + id: doc.id, + ...doc.data(), + firstName: + doc.data().name != undefined + ? doc.data().name.split(' ')[0] + : ' ', + lastName: + doc.data().name != undefined + ? doc.data().name.split(' ')[1] + : ' ', + year: + doc.data().semesters != undefined + ? doc.data().semesters[0].split(' ')[1] + : ' ', + fte: 15, + pname: 'DEPARTMENT TA/UPIS', + pid: '000108927', + hr: 15, + } as Assignment) + ); + setAssignmentData(data); + }); + + const courseRef = firebase.firestore().collection('courses'); + const map = new Map(courseEmailMap); + courseRef.onSnapshot((querySnapshot) => { + const data = querySnapshot.docs.map((doc) => { + map.set(doc.id, doc.data().professor_emails); + }); + }); + setCourseEmailMap(map); + + // Clean up the subscription on unmount + return () => unsubscribe(); + }, []); + + const [rowModesModel, setRowModesModel] = React.useState( + {} + ); + + const handleRowEditStop: GridEventListener<'rowEditStop'> = ( + params, + event + ) => { + if (params.reason === GridRowEditStopReasons.rowFocusOut) { + event.defaultMuiPrevented = true; + } + }; + + const handleSubmitAssignment = async ( + event: React.FormEvent + ) => { + setLoading(true); + event.preventDefault(); + // extract the form data from the current event + const formData = new FormData(event.currentTarget); + + // get student's user id + const student_uid = selectedUserGrid as string; + + // get class numbers as array + const classNumberString = formData.get('class-numbers') as string; + const classNumberArray = classNumberString + .split(',') + .map((classNumber) => classNumber.trim()); + + // get the courses collection + const coursesRef = collection(firebase.firestore(), 'courses'); + const q = query(coursesRef, where('id', 'in', classNumberArray)); + const snapshot = await getDocs(q); + + // the snapshot will contain all the courses that match the class numbers in the array + // therefore, if the length of the array is greater than the length of the snapshot, + // it means that there is a class number in the array that does not exist in the database + // therefore, display an error + if (classNumberArray.length > snapshot.size) { + alert( + 'One or more of the class numbers you entered does not exist in the database. Please check your input and try again.' + ); + return; + } + + // now, get the users collection + const userRef = firebase.firestore().collection('users').doc(student_uid); + const userData = (await userRef.get()).data(); + //const userData = (await firebase.firestore().collection('users').doc(student_uid).get()).data(); + const userFullName = userData?.firstname + ' ' + userData?.lastname; + const userEmail = userData?.email; + + // for every class number, update the course with the student's information + classNumberArray.forEach(async (classNumber) => { + const courseRef = firebase + .firestore() + .collection('courses') + .doc(classNumber); + await courseRef.update({ + // the student's full name needs to be added to the course's helper_names array + helper_names: firebase.firestore.FieldValue.arrayUnion(userFullName), + // the student's email needs to be added to the course's helper_emails array + helper_emails: firebase.firestore.FieldValue.arrayUnion(userEmail), + }); + }); + + // then, update the student's role to 'student_assigned' + await userRef.update({ role: 'student_assigned' }); + + handleCloseAssignmentDialog(); + setLoading(false); + }; + + const handleEditClick = (id: GridRowId) => () => { + setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); + }; + + const handleSaveClick = (id: GridRowId) => () => { + setLoading(true); + const updatedRow = assignmentData.find((row) => row.id === id); + + if (updatedRow) { + firebase + .firestore() + .collection('assignments') + .doc(id.toString()) + .update(updatedRow) + .then(() => { + setRowModesModel({ + ...rowModesModel, + [id]: { mode: GridRowModes.View }, + }); + setLoading(false); + }) + .catch((error) => { + setLoading(false); + console.error('Error updating document: ', error); + }); + } else { + setLoading(false); + console.error('No matching user data found for id: ', id); + } + }; + + const handleDeleteClick = (id: GridRowId) => { + setLoading(true); + firebase + .firestore() + .collection('assignments') + .doc(id.toString()) + .delete() + .then(() => { + setAssignmentData(assignmentData.filter((row) => row.id !== id)); + setLoading(false); + }) + .catch((error) => { + setLoading(false); + console.error('Error removing document: ', error); + }); + }; + const handleDel = (id: GridRowId) => () => { + setDelId(id); + setDelDia(true); + }; + + const handleCancelClick = (id: GridRowId) => () => { + setLoading(true); + const editedRow = assignmentData.find((row) => row.id === id); + if (editedRow!.isNew) { + firebase + .firestore() + .collection('assignments') + .doc(id.toString()) + .delete() + .then(() => { + setAssignmentData(assignmentData.filter((row) => row.id !== id)); + setLoading(false); + }) + .catch((error) => { + setLoading(false); + console.error('Error removing document: ', error); + }); + } else { + setRowModesModel({ + ...rowModesModel, + [id]: { mode: GridRowModes.View, ignoreModifications: true }, + }); + setLoading(false); + } + }; + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + console.log(delId!.toString()); + handleDeleteClick(delId!); + setDelDia(false); + }; + + const processRowUpdate = (newRow: GridRowModel, oldRow: GridRowModel) => { + setLoading(true); + const updatedRow = { + ...(newRow as Assignment), + isNew: false, + }; + + if (updatedRow) { + if (updatedRow.isNew) { + return firebase + .firestore() + .collection('assignments') + .add(updatedRow) + .then(() => { + setAssignmentData( + assignmentData.map((row) => + row.id === newRow.id ? updatedRow : row + ) + ); + setLoading(false); + return updatedRow; + }) + .catch((error) => { + console.error('Error adding document: ', error); + setLoading(false); + throw error; + }); + } else { + return firebase + .firestore() + .collection('assignments') + .doc(updatedRow.id) + .update(updatedRow) + .then(() => { + setAssignmentData( + assignmentData.map((row) => + row.id === newRow.id ? updatedRow : row + ) + ); + setLoading(false); + return updatedRow; + }) + .catch((error) => { + setLoading(false); + console.error('Error updating document: ', error); + throw error; + }); + } + } else { + setLoading(false); + return Promise.reject( + new Error('No matching user data found for id: ' + newRow.id) + ); + } + }; + + const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { + setRowModesModel(newRowModesModel); + }; + + let columns: GridColDef[] = [ + { + field: 'actions', + type: 'actions', + headerName: 'Actions', + width: 290, + + cellClassName: 'actions', + getActions: ({ id }) => { + const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; + + if (isInEditMode) { + return [ + } + label="Save" + sx={{ + color: 'primary.main', + }} + onClick={handleSaveClick(id)} + />, + } + label="Cancel" + onClick={handleCancelClick(id)} + color="inherit" + />, + ]; + } + + return [ + , + , + + , + ]; + }, + }, + { + field: 'ufid', + headerName: 'Student UFID', + width: 190, + editable: false, + }, + + { + field: 'firstName', + headerName: 'Student First Name', + width: 190, + editable: true, + }, + { + field: 'lastName', + headerName: 'Student Last Name', + width: 190, + editable: true, + }, + { + field: 'email', + headerName: 'Student Email', + width: 210, + editable: true, + }, + { + field: 'date', + headerName: 'Timestamp', + width: 210, + editable: true, + }, + { + field: 'supervisor_ufid', + headerName: 'Supervisor UFID', + width: 190, + editable: true, + }, + + // { + // field: 'sf', + // headerName: 'Supervisor First Name', + // width: 190, + // editable: true, + + // valueGetter: (params) => + // params.row.class_codes != undefined + // ? params.row.class_codes.split(' ')[2].split(',')[1] + // : ' ', + // }, + // { + // field: 'supervisorLastName', + // headerName: 'Supervisor Last Name', + // width: 190, + // editable: true, + + // valueGetter: (params) => + // params.row.class_codes != undefined + // ? params.row.class_codes.split(' ')[2].split(',')[0] + // : ' ', + // }, + // { + // field: 'supervisorEmail', + // headerName: 'Supervisor Email', + // width: 190, + // editable: true, + // valueGetter: (params) => { + // if (params.row.class_codes != undefined) { + // return courseEmailMap.get(params.row.class_codes); + // } else { + // return ' '; + // } + // }, + // }, + { + field: 'proxyUfid', + headerName: 'Proxy UFID', + width: 190, + editable: true, + }, + + { + field: 'proxyFirstName', + headerName: 'Proxy First Name', + width: 190, + editable: true, + valueGetter: (value) => { + return 'Christophe'; + }, + }, + { + field: 'proxyLastName', + headerName: 'Proxy Last Name', + width: 190, + editable: true, + valueGetter: (value) => { + return 'Bobda'; + }, + }, + + { + field: 'proxyEmail', + headerName: 'Proxy Email', + width: 190, + editable: true, + valueGetter: (value) => { + return 'cbobda@ufl.edu'; + }, + }, + { + field: 'action', + headerName: 'Requested Action', + width: 140, + editable: true, + + valueFormatter: (params) => 'NEW HIRE', + }, + + { + field: 'position', + headerName: 'Position Type', + width: 110, + editable: true, + valueFormatter: (value) => { + return 'TA'; + }, + }, + { + field: 'degree', + headerName: 'Degree Type', + width: 110, + editable: true, + }, + + { + field: 'semesters', + headerName: 'Semester', + width: 110, + editable: true, + valueGetter: (value) => { + return value; + }, + valueFormatter: (value) => { + const val = value as Array; + try { + if (val[0].includes('Fall')) { + return 'FALL'; + } + if (val[0].includes('Spring')) { + return 'SPRING'; + } + if (val[0].includes('Summer')) { + return 'SUMMER'; + } + } catch { + return 'FALL'; + } + }, + }, + + { + field: 'year', + headerName: 'Year', + width: 110, + editable: true, + }, + { + field: 'start_date', + headerName: 'Starting Date', + width: 100, + editable: true, + }, + { + field: 'end_date', + headerName: 'End Date', + width: 110, + editable: true, + }, + + { + field: 'pid', + headerName: 'Project Id', + width: 110, + editable: true, + }, + { + field: 'pname', + headerName: 'Project Name', + width: 240, + editable: true, + + valueFormatter: (params) => 'DEPARTMENT TA / UPIS', + }, + + { + field: 'percentage', + headerName: 'Percentage', + width: 110, + editable: true, + }, + { + field: 'hours', + headerName: 'Hours', + width: 140, + editable: true, + valueFormatter: (value) => { + return Number(value[0]); + }, + }, + + { + field: 'annual_rate', + headerName: 'Annual Rate', + width: 110, + editable: true, + }, + + { + field: 'biweekly_rate', + headerName: 'Biweekly Rate', + width: 110, + editable: true, + }, + { + field: 'hr', + headerName: 'Hourly Rate', + width: 110, + editable: true, + }, + + { + field: 'target_amount', + headerName: 'Target Amount', + width: 110, + editable: true, + }, + + { + field: 'title', + headerName: 'Working Title', + width: 110, + editable: true, + }, + + { + field: 'class_codes', + headerName: 'Duties', + width: 180, + editable: true, + valueFormatter: (value) => + `UPI in ${String(value ?? '').replace(/,/g, ' ')}`, + }, + + { + field: 'fte', + headerName: 'FTE', + width: 110, + // If FTE is derived from hours, editable should probably be false + editable: false, + valueGetter: (_value, row) => { + const h = row.hours?.[0]; + if (typeof h !== 'number') return null; + return Math.floor((h / 1.029411 / 40) * 100) / 100; + }, + valueFormatter: (value) => (value == null ? '' : String(value)), + }, + { + field: 'Imported', + headerName: 'Imported', + width: 140, + editable: false, + valueFormatter: (value) => { + return 'YES'; + }, + }, + { + field: 'remote', + headerName: 'Remote', + width: 140, + editable: false, + valueFormatter: (value) => { + if (value === undefined) { + return 'No'; + } + + return value; + }, + }, + ]; + const ODD_OPACITY = 0.2; + + // ✅ 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', + }, + '& .MuiDataGrid-cell:first-of-type': { + paddingLeft: '25px', + }, + + [`& .${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, + }, + })); + + return ( + + {loading ? : null} + + params.indexRelativeToCurrentPage % 2 === 0 ? 'even' : 'odd' + } + sx={{ + borderRadius: '16px', + maxHeight: '70vh', + maxWidth: '170vh', + minHeight: '70vh', + minWidth: '170vh', + }} + /> + + + {'Edit Assignment Details'} + + + + {/* Display the application data of the selected user */} + {selectedUserGrid && ( +
+ {/* Display the user's application data in a different format */} + +
+ )} +
+
+ + + + {'View Assignment Details'} + + + + {/* Display the application data of the selected user */} + {selectedUserGrid && ( +
+ {/* Display the user's application data in a different format */} + +
+ )} +
+
+ + + + Delete Applicant + +
handleSubmit(e)}> + + + Are you sure you want to delete this applicant? + + + + + + + +
+
+ + + Course Assignment +
+ + + Please enter one or more class numbers to which the student shall + be assigned. + + + + + + + + +
+
+
+ ); +} diff --git a/src/components/Dashboard/Applications/style.css b/src/components/Dashboard/Applications/style.css new file mode 100644 index 0000000..8f117be --- /dev/null +++ b/src/components/Dashboard/Applications/style.css @@ -0,0 +1,123 @@ +.applicantCardApprovedeny1 { + position: relative; + margin: 0 28px 0 40px; + display: flex; + align-items: center; + + text-align: left; + font-size: 24px; + color: #000; + + font-family: "SF Pro Display-Regular", Helvetica; + + border-radius: 20px; + box-shadow: 0px 2px 20px 4px rgba(0, 0, 0, 0.25); + justify-content: space-between; + cursor: pointer; + } + + .ellipse5 { + height: 71px; + width: 71px; + margin: 17px 0 19px 24px; + border-radius: 50%; + background-color: rgba(158, 158, 158, 0.58); + border: 2px solid #4d4d4d; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + font-size: 35px; + } + .initials { + + font-family: 'SF Pro Display' , Helvetica; + color:#ffffff; + } + .name5 { + margin: 17px 0px 0px 25px; + white-space: nowrap; + font-size: 23px; + font-family: "SF Pro Display-Regular", Helvetica; + } + .number{ + margin: 50px 0 42px 270px; + font-size: 16px; + font-family: "SF Pro Display-Regular", Helvetica; + color: #9e9e9e; + + } + + .email1 { + position: absolute; + margin: 50px 0 42px 25px; + font-size: 16px; + font-family: "SF Pro Display-Regular", Helvetica; + color: #9e9e9e; + } + + .thumbsContainer3 { + position: absolute; + right: 20px; + align-items: center; + display: flex; + justify-content: space-around; + margin: 24px 0 26px 0; + } + + .thumbsUpIcon { + color: green; + cursor: pointer; + margin-right: 23px; + font-size: 2rem; + } + + .thumbsDownIcon { + color: red; + cursor: pointer; + margin-right: 31px; + font-size: 2rem; + } + + .review23 { + + font-size: 23px; + font-weight: 500; + font-family: "SF Pro Display-Regular", Helvetica !important; + color: #f2a900; + text-align: center; + display: inline-block; + width: 80px; + + } + .applicantStatus231 { + + border-radius: 10px; + background-color: rgba(242, 169, 0, 0.12); + border: 2px solid #f2a900; + box-sizing: border-box; + padding: 11px 35px; + cursor: pointer; + display: inline-block; + } + + .description{ + margin-bottom: 31px; + } + .label50{ + font-family: "SF Pro Display-Regular", "Helvetica"; + font-size: 16px; + font-weight: 500; + margin-bottom: 20px; + color: #000000;; + } + .availability2{ + font-family: "SF Pro Display-Regular", "Helvetica"; + font-size: 16px; + font-weight: 400; + margin-bottom: 20px; + color:#4c4c4c; + } + + /* Add any other styles as needed */ + \ No newline at end of file diff --git a/src/components/Dashboard/Users/ApprovalGrid.tsx b/src/components/Dashboard/Users/ApprovalGrid.tsx new file mode 100644 index 0000000..2d38d58 --- /dev/null +++ b/src/components/Dashboard/Users/ApprovalGrid.tsx @@ -0,0 +1,415 @@ +'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 CancelIcon from '@mui/icons-material/Close'; +import SaveIcon from '@mui/icons-material/Save'; +import { ThumbDownOffAlt, ThumbUpOffAlt } from '@mui/icons-material'; + +import { + GridRowModesModel, + GridRowsProp, + GridRowModes, + GridToolbarContainer, + GridToolbarExport, + GridToolbarFilterButton, + GridToolbarColumnsButton, + DataGrid, + GridColDef, + GridActionsCellItem, + GridEventListener, + GridRowId, + GridRowModel, + GridRowEditStopReasons, + gridClasses, +} from '@mui/x-data-grid'; + +import { LinearProgress } 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'; + +interface User { + id: string; + firstname: string; + lastname: string; + email: string; + department: string; + role: string; + isNew?: boolean; + fullname: string; + mode?: 'edit' | 'view' | undefined; +} + +interface EditToolbarProps { + setApplicationData: ( + newRows: (oldRows: GridRowsProp) => GridRowsProp + ) => void; + setRowModesModel: ( + newModel: (oldModel: GridRowModesModel) => GridRowModesModel + ) => void; +} + +function EditToolbar(_props: EditToolbarProps) { + // ✅ match CourseGrid toolbar (default styling) + return ( + + + + + + ); +} + +interface ApprovalGridProps { + userRole: string; +} + +export default function ApprovalGrid(props: ApprovalGridProps) { + const { userRole } = props; + + const [loading, setLoading] = React.useState(false); + const [userData, setUserData] = React.useState([]); + + React.useEffect(() => { + const usersRef = firebase + .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) + ); + + setUserData(data); + }); + + return () => unsubscribe(); + }, []); + + const [rowModesModel, setRowModesModel] = React.useState( + {} + ); + + const handleRowEditStop: GridEventListener<'rowEditStop'> = ( + params, + event + ) => { + if (params.reason === GridRowEditStopReasons.rowFocusOut) { + event.defaultMuiPrevented = true; + } + }; + + 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 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 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 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 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 }, + }); + } + } catch (error) { + console.error('Error removing document: ', error); + } finally { + setLoading(false); + } + }; + + 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); + } else { + await firebase + .firestore() + .collection('users') + .doc(updatedRow.id) + .update(updatedRow); + } + + setUserData((prev) => + prev.map((row) => (row.id === newRow.id ? (updatedRow as User) : row)) + ); + + 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 }, + { + field: 'actions', + type: 'actions', + headerName: 'Actions', + width: 180, + cellClassName: 'actions', + getActions: ({ id }) => { + const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; + + if (isInEditMode) { + return [ + } + label="Save" + sx={{ color: 'primary.main' }} + onClick={handleSaveClick(id)} + />, + } + label="Cancel" + className="textPrimary" + onClick={handleCancelClick(id)} + color="inherit" + />, + ]; + } + + return [ + } + label="Edit" + className="textPrimary" + onClick={handleEditClick(id)} + color="inherit" + />, + } + label="Delete" + onClick={handleDeleteClick(id)} + color="inherit" + />, + } + label="Approve" + onClick={handleApproveClick(id)} + color="success" + />, + } + label="Deny" + onClick={handleDenyClick(id)} + color="error" + />, + ]; + }, + }, + ]; + + // ✅ 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', + }, + '& .MuiDataGrid-cell:first-of-type': { + paddingLeft: '25px', + }, + + [`& .${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, + }, + })); + + return ( + + {loading ? : null} + + setRowModesModel(m)} + 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' + } + /> + + ); +} diff --git a/src/components/Dashboard/Users/FacultyStats.tsx b/src/components/Dashboard/Users/FacultyStats.tsx new file mode 100644 index 0000000..9db1a4d --- /dev/null +++ b/src/components/Dashboard/Users/FacultyStats.tsx @@ -0,0 +1,19 @@ +import Container from '@mui/material/Container'; +import StatsGrid from './StatsGrid'; +import ApprovalGrid from './ApprovalGrid'; +interface UsersProps { + userRole: string; +} + +export default function FacultyStats(props: UsersProps) { + const { userRole } = props; + return ( + <> + +
+ +
+
+ + ); +} diff --git a/src/components/Dashboard/Users/StatsGrid.tsx b/src/components/Dashboard/Users/StatsGrid.tsx new file mode 100644 index 0000000..fba367a --- /dev/null +++ b/src/components/Dashboard/Users/StatsGrid.tsx @@ -0,0 +1,404 @@ +// components/StatsGrid.tsx +'use client'; + +import * as React from 'react'; +import Box from '@mui/material/Box'; +import ZoomIn from '@mui/icons-material/ZoomIn'; +import DeleteIcon from '@mui/icons-material/DeleteOutlined'; +import { + GridRowModesModel, + GridToolbarContainer, + GridToolbarExport, + GridToolbarFilterButton, + GridToolbarColumnsButton, + DataGrid, + GridColDef, + GridActionsCellItem, + GridEventListener, + GridRowId, + GridRowModel, + GridRowEditStopReasons, + useGridApiContext, + gridClasses, +} from '@mui/x-data-grid'; +import { + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + LinearProgress, + Button, +} from '@mui/material'; +import Link from 'next/link'; +import { + useFacultyStats, + useDeleteFacultyStat, + useUpdateFacultyStat, +} from '@/hooks/useFacultyStats'; +import { User } from '@/types/User'; +import { alpha, styled } from '@mui/material/styles'; + +interface EditToolbarProps { + setRowModesModel: ( + newModel: (oldModel: GridRowModesModel) => GridRowModesModel + ) => void; +} + +function EditToolbar(props: EditToolbarProps) { + const { setRowModesModel } = props; + + return ( + + + + + + ); +} + +interface UserGridProps { + userRole: string; +} + +export default function StatsGrid(props: UserGridProps) { + const { userRole } = props; + const { data, isLoading, error } = useFacultyStats(); + const deleteMutation = useDeleteFacultyStat(); + const updateMutation = useUpdateFacultyStat(); + + const [delDia, setDelDia] = React.useState(false); + const [delId, setDelId] = React.useState(); + + const [rowModesModel, setRowModesModel] = React.useState( + {} + ); + + const handleDeleteDiagClose = () => { + setDelDia(false); + }; + + const handleRowEditStop: GridEventListener<'rowEditStop'> = ( + params, + event + ) => { + if (params.reason === GridRowEditStopReasons.rowFocusOut) { + event.defaultMuiPrevented = true; + } + }; + + const handleSaveClick = (id: GridRowId) => () => { + const updatedRow = data?.find((row) => row.id === id); + if (updatedRow) { + updateMutation.mutate(updatedRow); + setRowModesModel({ + ...rowModesModel, + [id]: { mode: GridRowModes.View }, + }); + } else { + console.error('No matching user data found for id: ', id); + } + }; + + const handleDel = (id: GridRowId) => () => { + setDelId(id.toString()); + setDelDia(true); + }; + + const handleDeleteClick = (id: string) => { + deleteMutation.mutate(id); + // deleteUserHTTPRequest(id); // Remove if redundant, as React Query handles refetching + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (delId) { + handleDeleteClick(delId); + } + setDelDia(false); + }; + + function CustomToolbar() { + const apiRef = useGridApiContext(); + + return ( + + + + ); + } + + const handleCancelClick = (id: GridRowId) => () => { + const editedRow = data?.find((row) => row.id === id); + if (editedRow && editedRow.isNew) { + deleteMutation.mutate(id); + } else { + setRowModesModel({ + ...rowModesModel, + [id]: { mode: GridRowModes.View, ignoreModifications: true }, + }); + } + }; + + const processRowUpdate = async (newRow: GridRowModel) => { + const updatedRow = { ...(newRow as User), isNew: false }; + try { + await updateMutation.mutateAsync(updatedRow); + return updatedRow; + } catch (error) { + console.error('Error updating row:', error); + throw error; + } + }; + + const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { + setRowModesModel(newRowModesModel); + }; + + const columns: GridColDef[] = [ + { + field: 'actions', + type: 'actions', + headerName: 'Actions', + width: 200, + cellClassName: 'actions', + getActions: ({ id }) => { + return [ + , + } + label="Delete" + onClick={handleDel(id)} + color="inherit" + />, + ]; + }, + }, + { + field: 'instructor', + headerName: 'Instructor', + width: 150, + editable: false, + }, + { + field: 'research_level', + headerName: 'Research Activity Level', + width: 200, + editable: false, + }, + { + field: 'teaching_load', + headerName: 'Teaching Load', + width: 200, + editable: false, + }, + // { + // field: 'teaching_load', + // headerName: 'Teaching Load', + // width: 200, + // editable: false, + // }, + // { + // field: 'acu2', + // headerName: 'Accumulated Course Credits', + // width: 220, + // editable: true, + // }, + // { field: 'cd', headerName: 'Credit Deficit', width: 150, editable: true }, + // { field: 'ce', headerName: 'Credit Excess', width: 150, editable: true }, + // { + // field: 'tot', + // headerName: 'Total Classes Taught (3yrs)', + // width: 200, + // editable: true, + // }, + // { + // field: 'acu3', + // headerName: 'Average Course Units', + // width: 170, + // editable: true, + // }, + // { field: 'lc', headerName: 'Lab Courses', width: 150, editable: true }, + ]; + + const ODD_OPACITY = 0.2; + + const StripedDataGrid = styled(DataGrid)(({ theme }) => ({ + [`& .${gridClasses.row}.even`]: { + 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 + ), + }, + }, + }, + }, + })); + + if (isLoading) { + return ; + } + + if (error) { + return
Error loading data
; + } + + return ( + + + + Delete Instructor + +
+ + + Are you sure you want to delete this instructor? + + + + + + + +
+
+ row.instructor} + rowModesModel={rowModesModel} + onRowModesModelChange={handleRowModesModelChange} + onRowEditStop={handleRowEditStop} + processRowUpdate={processRowUpdate} + initialState={{ + pagination: { paginationModel: { pageSize: 25 } }, + }} + getRowClassName={(params) => + params.indexRelativeToCurrentPage % 2 === 0 ? 'even' : 'odd' + } + sx={{ borderRadius: '16px' }} + /> +
+ ); +} diff --git a/src/components/Dashboard/Users/User.tsx b/src/components/Dashboard/Users/User.tsx new file mode 100644 index 0000000..fc64f13 --- /dev/null +++ b/src/components/Dashboard/Users/User.tsx @@ -0,0 +1,10 @@ +// individual user +// this could be a modal which pops up when a user is clicked on in the table +// same principle for application & courses in their respective tables +export default function User() { + return ( + <> +

individual user

+ + ); +} diff --git a/src/components/Dashboard/Users/UserGrid.tsx b/src/components/Dashboard/Users/UserGrid.tsx new file mode 100644 index 0000000..a009ac8 --- /dev/null +++ b/src/components/Dashboard/Users/UserGrid.tsx @@ -0,0 +1,487 @@ +'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 SaveIcon from '@mui/icons-material/Save'; +import CancelIcon from '@mui/icons-material/Close'; +import { + GridRowModesModel, + GridRowsProp, + GridRowModes, + GridToolbarContainer, + GridToolbarExport, + GridToolbarFilterButton, + GridToolbarColumnsButton, + DataGrid, + GridColDef, + GridActionsCellItem, + GridEventListener, + GridRowId, + GridRowModel, + GridRowEditStopReasons, + gridClasses, +} from '@mui/x-data-grid'; +import { + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + LinearProgress, + Button, +} from '@mui/material'; +import { styled } from '@mui/material/styles'; + +import firebase from '@/firebase/firebase_config'; +import 'firebase/firestore'; +import { deleteUserHTTPRequest } from '@/firebase/auth/auth_delete_user'; +import { isE2EMode } from '@/utils/featureFlags'; + +interface User { + id: string; + firstname: string; + lastname: string; + email: string; + password: string; + department: string; + role: string; + ufid: string; + isNew?: boolean; + mode?: 'edit' | 'view' | undefined; +} + +interface EditToolbarProps { + setUserData: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; + setRowModesModel: ( + newModel: (oldModel: GridRowModesModel) => GridRowModesModel + ) => void; +} + +function EditToolbar(_props: EditToolbarProps) { + // match CourseGrid toolbar look (default MUI icons/colors) + return ( + + + + + + ); +} + +interface UserGridProps { + userRole: string; +} + +export default function UserGrid(props: UserGridProps) { + const { userRole } = props; + const e2e = isE2EMode(); + + const [loading, setLoading] = React.useState(false); + const [userData, setUserData] = React.useState([]); + + const [delDia, setDelDia] = React.useState(false); + const [delId, setDelId] = React.useState(null); + + 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) + ); + setUserData(data); + }); + + return () => unsubscribe(); + }, [e2e]); + + const [rowModesModel, setRowModesModel] = React.useState( + {} + ); + + const handleRowEditStop: GridEventListener<'rowEditStop'> = ( + params, + event + ) => { + if (params.reason === GridRowEditStopReasons.rowFocusOut) { + event.defaultMuiPrevented = true; + } + }; + + 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 handleDel = (id: GridRowId) => () => { + setDelId(id); + 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 handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (delId == null) return; + await 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 }, + }); + } + } catch (err) { + console.error('Error canceling edit: ', err); + } finally { + setLoading(false); + } + }; + + 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); + } else { + await firebase + .firestore() + .collection('users') + .doc(updatedRow.id) + .update(updatedRow); + } + + setUserData((prev) => + prev.map((row) => (row.id === newRow.id ? (updatedRow as User) : row)) + ); + + return updatedRow; + } catch (err) { + console.error('Error processing row update: ', err); + throw err; + } finally { + setLoading(false); + } + }; + + 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, + cellClassName: 'actions', + getActions: ({ id }) => { + const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; + + if (isInEditMode) { + return [ + } + label="Save" + sx={{ color: 'primary.main' }} + onClick={handleSaveClick(id)} + />, + } + label="Cancel" + className="textPrimary" + onClick={handleCancelClick(id)} + color="inherit" + />, + ]; + } + + return [ + } + label="Edit" + className="textPrimary" + onClick={handleEditClick(id)} + color="inherit" + />, + } + label="Delete" + onClick={handleDel(id)} + color="inherit" + />, + ]; + }, + }, + ]; + + // ✅ 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', + }, + '& .MuiDataGrid-cell:first-of-type': { + paddingLeft: '25px', + }, + + [`& .${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, + }, + })); + + 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) */} + + + Delete User + +
+ + + Are you sure you want to delete this user? + + + + + + + +
+
+ + ); +} diff --git a/src/components/Dashboard/Users/Users.tsx b/src/components/Dashboard/Users/Users.tsx new file mode 100644 index 0000000..577886a --- /dev/null +++ b/src/components/Dashboard/Users/Users.tsx @@ -0,0 +1,22 @@ +import Container from '@mui/material/Container'; +import UserGrid from './UserGrid'; +import ApprovalGrid from './ApprovalGrid'; +interface UsersProps { + userRole: string; +} + +export default function Users(props: UsersProps) { + const { userRole } = props; + return ( + <> + +
+

All Users

+ +
+

Unapproved Users

+ +
+ + ); +} diff --git a/src/components/EceLogoPng/CCLogo.svg b/src/components/EceLogoPng/CCLogo.svg new file mode 100644 index 0000000..7f3cdf1 --- /dev/null +++ b/src/components/EceLogoPng/CCLogo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/EceLogoPng/EceLogoPng.jsx b/src/components/EceLogoPng/EceLogoPng.jsx new file mode 100644 index 0000000..4047eaf --- /dev/null +++ b/src/components/EceLogoPng/EceLogoPng.jsx @@ -0,0 +1,5 @@ +import React from 'react'; +import './style.css'; +export const EceLogoPng = ({ className }) => { + return
; +}; diff --git a/src/components/EceLogoPng/style.css b/src/components/EceLogoPng/style.css new file mode 100644 index 0000000..1e77cb8 --- /dev/null +++ b/src/components/EceLogoPng/style.css @@ -0,0 +1,11 @@ +.ece-logo-png { + background-image: url(CCLogo.svg); + background-position: 0% 0%; + position: relative; + background-size: 60%; + background-repeat: no-repeat; + height: 40px; + width: 300px; + position: fixed; +} + diff --git a/src/components/PageLayout/PageLayout.tsx b/src/components/PageLayout/PageLayout.tsx index 051d593..f47bed5 100644 --- a/src/components/PageLayout/PageLayout.tsx +++ b/src/components/PageLayout/PageLayout.tsx @@ -16,8 +16,8 @@ const PageLayout: FC = ({ mainTitle, navItems, children }) => {
-
-

+
+

{mainTitle}

{children} diff --git a/src/components/Research/ApplicationCard.tsx b/src/components/Research/ApplicationCard.tsx index b90b0ae..03619c6 100644 --- a/src/components/Research/ApplicationCard.tsx +++ b/src/components/Research/ApplicationCard.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState } from 'react'; import { Card, CardContent, Typography, Button, Box } from '@mui/material'; interface ApplicationCardProps { @@ -6,7 +6,7 @@ interface ApplicationCardProps { uid?: string; project_title: string; department: string; - faculty_mentor: { name: string; email: string }; + faculty_mentor?: { [email: string]: string } | string; date_applied: string; terms_available: string; student_level: string; @@ -20,6 +20,8 @@ interface ApplicationCardProps { application_deadline?: string; website?: string; app_status: string; + faculty_contact?: string; + compensation?: string; onEdit?: () => void; onShowApplications?: () => void; } @@ -31,51 +33,38 @@ const ApplicationCard: React.FC = ({ department, date_applied, faculty_mentor, - terms_available, - student_level, - project_description, faculty_members = [], app_status, - phd_student_mentor, - prerequisites, - credit, - stipend, - application_requirements, - application_deadline, - website, + project_description, + faculty_contact, onEdit, onShowApplications, }) => { const [expanded, setExpanded] = useState(false); - const [needsExpansion, setNeedsExpansion] = useState(false); - const descriptionRef = useRef(null); const isFacultyInvolved = userRole === 'faculty' && faculty_members.includes(uid || ''); - useEffect(() => { - const checkTextOverflow = () => { - const element = descriptionRef.current; - if (!element) return; - - if (expanded) { - setNeedsExpansion(true); - return; - } - - const isOverflowing = element.scrollHeight > element.clientHeight; - setNeedsExpansion(isOverflowing); - }; + // Backward-compatible faculty display + const facultyDisplay = + faculty_contact || + (typeof faculty_mentor === 'object' && faculty_mentor + ? Object.values(faculty_mentor).join(', ') + : typeof faculty_mentor === 'string' + ? faculty_mentor + : 'N/A'); - checkTextOverflow(); - - window.addEventListener('resize', checkTextOverflow); - return () => window.removeEventListener('resize', checkTextOverflow); - }, [expanded, project_description]); + const sectionHeaderSx = { + color: '#5A41D8', + fontWeight: 'bold', + fontSize: '0.95rem', + mt: 2, + mb: 0.5, + }; return ( = ({ height: '100%', }} > - - + + {project_title} - + {department} - - - Status: - {' '} - - {app_status} - -
- - Date Applied: - {' '} - - {date_applied} - -
- - - Faculty Mentor: - {' '} - - {Object.entries(faculty_mentor ?? {}) - .map(([key, value]) => `${value}`) - .join(', ')} + + + + Status:{' '} + + {app_status} + -
- - Faculty Email: - {' '} - - {Object.entries(faculty_mentor ?? {}) - .map(([key, value]) => `${key}`) - .join(', ')} + + Date Applied: {date_applied} -
- - Research Description - - {project_description} + + Faculty Mentor: {facultyDisplay} + + Research Description + + {project_description} +
- - {needsExpansion ? ( - - ) : ( -
- )} + + + + {isFacultyInvolved && ( )} diff --git a/src/components/Research/EditResearchModal.tsx b/src/components/Research/EditResearchModal.tsx index 7e02e1b..412cf0f 100644 --- a/src/components/Research/EditResearchModal.tsx +++ b/src/components/Research/EditResearchModal.tsx @@ -10,14 +10,26 @@ import { Button, Grid, Typography, + MenuItem, + IconButton, + Box, } from '@mui/material'; -import firebase from '@/firebase/firebase_config'; +import CloseIcon from '@mui/icons-material/Close'; +import { toast } from 'react-hot-toast'; +import { normalizeResearchListing } from '@/app/models/ResearchModel'; +import { updateResearchListing } from '@/services/researchService'; +import { + NATURE_OF_JOB_OPTIONS, + ResearchFormData, +} from './shared/researchModalUtils'; +import ImageUploadField from './shared/ImageUploadField'; +import { COLORS } from '@/constants/theme'; interface EditResearchModalProps { open: boolean; onClose: () => void; - listingData: any; // The current listing's data (pre-filled) - onSubmitSuccess: () => void; // Callback to refresh the listings + listingData: any; + onSubmitSuccess: () => void; } const EditResearchModal: React.FC = ({ @@ -26,243 +38,308 @@ const EditResearchModal: React.FC = ({ listingData, onSubmitSuccess, }) => { - const [formData, setFormData] = useState({ ...listingData }); - const [facultyEmail, setFacultyEmail] = useState(''); - const [facultyName, setFacultyName] = useState(''); + const [formData, setFormData] = useState( + {} as ResearchFormData + ); + const [uploading, setUploading] = useState(false); + const [imageFileName, setImageFileName] = useState(''); useEffect(() => { - setFormData({ ...listingData }); + const normalized = normalizeResearchListing(listingData); + setFormData(normalized); + if (normalized.image_url) { + setImageFileName('Current image'); + } }, [listingData]); - const handleChange = (e: React.ChangeEvent) => { + const handleChange = ( + e: React.ChangeEvent + ) => { const { name, value } = e.target; - setFormData((prev: typeof formData) => ({ ...prev, [name]: value })); - }; - - /** Adds a faculty mentor to the map. */ - const handleAddFacultyMentor = () => { - if (facultyEmail && facultyName) { - setFormData((prev) => ({ - ...prev, - faculty_mentor: { - ...prev.faculty_mentor, - [facultyEmail]: facultyName, - }, - })); - setFacultyEmail(''); - setFacultyName(''); - } + setFormData((prev) => ({ ...prev, [name]: value })); }; - /** Removes a faculty mentor from the map. */ - const handleRemoveFacultyMentor = (email: string) => { - setFormData((prev) => { - const updatedMentors = { ...prev.faculty_mentor }; - delete updatedMentors[email]; - return { ...prev, faculty_mentor: updatedMentors }; - }); + const handleImageUpload = (url: string, fileName: string) => { + setFormData((prev) => ({ ...prev, image_url: url })); + setImageFileName(fileName); }; const handleSubmit = async () => { try { - const db = firebase.firestore(); - const querySnapshot = await db - .collection('research-listings') - .where('id', '==', listingData.id) - .get(); - - if (querySnapshot.empty) { - throw new Error('No matching listing found!'); - } - - const listingRef = querySnapshot.docs[0].ref; - await listingRef.update(formData); - - alert('Research listing updated!'); + const docID = listingData.docID || listingData.id; + await updateResearchListing(docID, formData); + toast.success('Research listing updated!'); onSubmitSuccess(); onClose(); } catch (error) { console.error('Update failed:', error); - alert('Failed to update listing.'); + toast.error('Failed to update listing.'); } }; return ( - - Edit Research Listing - + + Edit Position + + + + + + - + {/* Title + Description */} + + + * Title + + + + + * Position + Description + + - + {/* Department */} + + + * Department + - {/* Faculty Mentor */} + {/* Image Upload */} - Faculty Mentors + setUploading(true)} + onUploadEnd={() => setUploading(false)} + /> + + + {/* Nature of Job, Compensation, Faculty Contact, PhD Student Contact */} + + + * Nature of Job + setFacultyEmail(e.target.value)} + name="nature_of_job" + select + value={formData.nature_of_job || ''} + onChange={handleChange} fullWidth - margin="dense" + size="small" + > + {NATURE_OF_JOB_OPTIONS.map((option) => ( + + {option} + + ))} + + + + + * Compensation + + + + + + * Faculty Contact + setFacultyName(e.target.value)} + name="faculty_contact" + placeholder="Ex. albertgator@ufl.edu" + value={formData.faculty_contact || ''} + onChange={handleChange} fullWidth - margin="dense" + size="small" /> - - - {formData.faculty_mentor && - Object.entries(formData.faculty_mentor).map(([email, name]) => ( - -
- - {name} ({email}) - - -
-
- ))} -
- - + + + PhD Student Contact + - + {/* Application Deadline, Hours per Week, Prerequisites */} + + + * Application + Deadline + - - + + + * Hours per Week + - - + + + Prerequisites + - + {/* Terms Available, Student Level, Website, Application Requirements */} + + + Terms Available + - - + + + Student Level + - - + + + Website + - - + + + Application Requirements +
- - - +
diff --git a/src/components/Research/FacultyResearchView.tsx b/src/components/Research/FacultyResearchView.tsx index 99c2f9e..ef32e6a 100644 --- a/src/components/Research/FacultyResearchView.tsx +++ b/src/components/Research/FacultyResearchView.tsx @@ -4,14 +4,19 @@ import { Typography, Button, Grid, + Card, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, + FormControl, + Select, + MenuItem, } from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined'; import ResearchModal from '@/components/Research/Modal'; -import ProjectCard from '@/components/Research/ProjectCard'; import FacultyApplicantsView from '@/components/Research/FacultyApplicantsView'; import { deleteDoc, doc } from 'firebase/firestore'; import firebase from '@/firebase/firebase_config'; @@ -32,37 +37,35 @@ const FacultyResearchView: React.FC = ({ getResearchListings, postNewResearchPosition, }) => { - const [studentView, showStudentView] = useState(true); + const [createModalOpen, setCreateModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false); const [editingForm, setEditingForm] = useState(null); const [selectedResearchId, setSelectedResearchId] = useState( null ); + const [selectedSemester, setSelectedSemester] = useState('Spring 2026'); + const [showAll, setShowAll] = useState(false); // State for delete confirmation modal const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [deleteDocID, setDeleteDocID] = useState(null); - // Open delete confirmation modal const handleOpenDeleteModal = (docID: string) => { setDeleteDocID(docID); setDeleteModalOpen(true); }; - // Close delete confirmation modal const handleCloseDeleteModal = () => { setDeleteModalOpen(false); setDeleteDocID(null); }; - // Handle delete action const handleDelete = async () => { if (!deleteDocID) return; try { const db = firebase.firestore(); const docRef = doc(db, 'research-listings', deleteDocID); await deleteDoc(docRef); - console.log(`Listing with ID ${deleteDocID} deleted successfully.`); getResearchListings(); } catch (error) { console.error('Error deleting listing:', error); @@ -76,10 +79,8 @@ const FacultyResearchView: React.FC = ({ item.faculty_members?.includes(uid) ); - // Check if there are no positions when in my positions view - const hasNoPositions = studentView && myPositions.length === 0; + const displayedPositions = showAll ? myPositions : myPositions.slice(0, 6); - // Callback to go back to the research listings view const handleBackToListings = () => { setSelectedResearchId(null); }; @@ -98,76 +99,59 @@ const FacultyResearchView: React.FC = ({
) : ( <> - {/* Header section with buttons */} + {/* Header row */} - - - My Positions: + + + Research - + + - } + variant="contained" + sx={{ backgroundColor: '#5A41D8', color: '#FFFFFF', textTransform: 'none', borderRadius: '8px', - boxShadow: '0px 0px 8px #E5F0DC', fontWeight: 500, padding: '10px 24px', '&:hover': { backgroundColor: '#4A35B8', - boxShadow: '0px 0px 12px #E5F0DC', }, }} - /> + onClick={() => setCreateModalOpen(true)} + > + Create Position + - {hasNoPositions ? ( + {/* Position cards grid */} + {myPositions.length === 0 ? ( = ({
You haven't created any research positions yet. Get started - by creating your first position using the "Create New + by creating your first position using the "Create Position" button above. ) : ( - - {studentView - ? myPositions.map((item, index) => ( - - { - setSelectedResearchId(item.docID); - }} - onEdit={() => { - console.log('Opening edit modal'); - setEditingForm(item); - setEditModalOpen(true); - }} - onDelete={() => handleOpenDeleteModal(item.docID)} + <> + + {displayedPositions.map((item, index) => ( + + setSelectedResearchId(item.docID)} + > + - - )) - : researchListings.map((item, index) => ( - - { - setSelectedResearchId(item.docID); - }} - onEdit={() => { - console.log('Opening edit modal'); - setEditingForm(item); - setEditModalOpen(true); - }} - onDelete={() => handleOpenDeleteModal(item.docID)} - /> - - ))} - + + + {item.project_title} + + + {item.department} + + + + + + +
+ + ))} + + + {/* See More link */} + {myPositions.length > 6 && !showAll && ( + + + + )} + )} )} + {/* Create Position Modal */} + setCreateModalOpen(false)} + uid={uid} + onSubmitSuccess={getResearchListings} + firebaseQuery={postNewResearchPosition} + /> + {/* Delete Confirmation Modal */} = ({ + {/* Edit Modal */} {editingForm && ( void; onSubmitSuccess: () => void; - currentFormData: FormData; - buttonStyle?: SxProps; - buttonText: React.ReactNode; // Changed from string to ReactNode firebaseQuery: (formData: any) => Promise; uid: string; } -const ResearchModal: React.FC = ({ +const ResearchModal: React.FC = ({ + open, + onClose, onSubmitSuccess, - currentFormData, - buttonStyle, - buttonText, firebaseQuery, uid, }) => { - const [open, setOpen] = useState(false); - const [formData, setFormData] = useState(currentFormData); - const [facultyEmail, setFacultyEmail] = useState(''); - const [facultyName, setFacultyName] = useState(''); - - /** Opens the dialog (modal). */ - const handleOpen = () => { - setOpen(true); - }; + const [formData, setFormData] = useState({ + ...INITIAL_FORM_DATA, + }); + const [errors, setErrors] = useState< + Partial> + >({}); + const [uploading, setUploading] = useState(false); + const [imageFileName, setImageFileName] = useState(''); - /** Closes the dialog (modal). - * Note that we do NOT reset the form data here, - * so the draft remains if the user reopens the modal. - */ - const handleClose = () => { - setOpen(false); - }; - - /** Updates the corresponding form field in state. */ const handleChange = ( event: React.ChangeEvent ) => { const { name, value } = event.target; setFormData((prev) => ({ ...prev, [name]: value })); + if (errors[name as keyof ResearchFormData]) { + setErrors((prev) => ({ ...prev, [name]: '' })); + } }; - /** Adds a faculty mentor to the map. */ - const handleAddFacultyMentor = () => { - if (facultyEmail && facultyName) { - setFormData((prev) => ({ - ...prev, - faculty_mentor: { - ...prev.faculty_mentor, - [facultyEmail]: facultyName, - }, - })); - setFacultyEmail(''); - setFacultyName(''); - } + const handleImageUpload = (url: string, fileName: string) => { + setFormData((prev) => ({ ...prev, image_url: url })); + setImageFileName(fileName); }; - /** Removes a faculty mentor from the map. */ - const handleRemoveFacultyMentor = (email: string) => { - setFormData((prev) => { - const updatedMentors = { ...prev.faculty_mentor }; - delete updatedMentors[email]; - return { ...prev, faculty_mentor: updatedMentors }; - }); + const validate = (): boolean => { + const newErrors = validateResearchForm(formData); + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleDiscard = () => { + setFormData({ ...INITIAL_FORM_DATA }); + setErrors({}); + setImageFileName(''); + onClose(); + }; + + const handleSaveAndExit = () => { + // Save draft in state (form data persists until discard) + onClose(); }; - /** Submits the form and clears it, then closes the dialog. */ const handleSubmit = async () => { + if (!validate()) return; + const finalFormData = { ...formData, - faculty_mentor: formData.faculty_mentor, creator_id: uid, faculty_members: [uid], }; - console.log('Final Form Data:', finalFormData); - firebaseQuery(finalFormData); + await firebaseQuery(finalFormData); onSubmitSuccess(); - setFormData(currentFormData); - handleClose(); + setFormData({ ...INITIAL_FORM_DATA }); + setErrors({}); + setImageFileName(''); + onClose(); }; return ( -
- {/* Button to open the modal */} - - - - {buttonText} - - - {/* Project Title */} - - - - - {/* Department */} - - - - - {/* Faculty Mentor */} - - setFacultyEmail(e.target.value)} - fullWidth - /> - setFacultyName(e.target.value)} - fullWidth - /> - - - {Object.entries(formData.faculty_mentor).map( - ([email, name]) => ( - -
- - {name} ({email}) - - -
-
- ) - )} -
-
- - {/* PhD Student Mentor */} - - - + + + Create Position + + + + - {/* Terms Available */} - - - - {/* Student Level */} - - - - - {/* Prerequisites */} - - - - - {/* Credit */} - - - + + + {/* Row 1: Title + Position Description */} + + + * Title + + + + + + * Position Description + + + - {/* Stipend */} - - $ - ), // Add dollar sign - }} - fullWidth - /> - + {/* Row 2: Department */} + + + * Department + + + - {/* Website */} - - - + {/* Image Upload */} + + setUploading(true)} + onUploadEnd={() => setUploading(false)} + /> + - {/* Application Requirements */} - - - + {/* Row 3: Nature of Job, Compensation, Faculty Contact, PhD Student Contact */} + + + * Nature of Job + + + {NATURE_OF_JOB_OPTIONS.map((option) => ( + + {option} + + ))} + + + + + * Compensation + + + + + + * Faculty Contact + + + + + + PhD Student Contact + + + - {/* Application Deadline */} - - Application Deadline - - { - setFormData((prev) => ({ - ...prev, - application_deadline: e.target.checked ? 'Rolling' : '', - })); - }} - /> - } - label="Rolling" - /> - + {/* Row 4: Application Deadline, Hours per Week, Prerequisites */} + + + * Application Deadline + + + + + + * Hours per Week + + + + + + Prerequisites + + + - {/* Project Description */} - - - + {/* Row 5: Terms Available, Student Level, Website, Application Requirements */} + + + Terms Available + + - - + + + Student Level + + + + + + Website + + + + + + Application Requirements + + + +
+
+ + + + - -
-
+ + +

); }; diff --git a/src/components/Research/ProjectCard.tsx b/src/components/Research/ProjectCard.tsx index 876578b..88db7c6 100644 --- a/src/components/Research/ProjectCard.tsx +++ b/src/components/Research/ProjectCard.tsx @@ -1,31 +1,13 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { - Card, - CardContent, - Typography, - Button, - Box, - Grid, -} from '@mui/material'; +import React, { useState } from 'react'; +import { Card, CardContent, Typography, Button, Box } from '@mui/material'; import ModalApplicationForm from './ModalApplicationForm'; -import { - collection, - addDoc, - updateDoc, - doc, - where, - query, - documentId, - getDocs, -} from 'firebase/firestore'; -import firebase from '@/firebase/firebase_config'; interface ProjectCardProps { userRole: string; uid?: string; project_title: string; department: string; - faculty_mentor: {}; + faculty_mentor?: { [email: string]: string } | string; terms_available: string; student_level: string; project_description: string; @@ -38,6 +20,14 @@ interface ProjectCardProps { application_deadline?: string; website?: string; applications?: any[]; + // New fields + faculty_contact?: string; + phd_student_contact?: string; + compensation?: string; + nature_of_job?: string; + hours_per_week?: string; + image_url?: string; + // Callbacks onEdit?: () => void; onShowApplications?: () => void; onDelete?: () => void; @@ -59,57 +49,57 @@ const ProjectCard: React.FC = ({ prerequisites, credit, stipend, - application_requirements, application_deadline, - website, applications = [], + faculty_contact, + phd_student_contact, + compensation, onEdit, onShowApplications, onDelete, }) => { const [expanded, setExpanded] = useState(false); - const [needsExpansion, setNeedsExpansion] = useState(false); - const descriptionRef = useRef(null); const isFacultyInvolved = userRole === 'faculty' && faculty_members.includes(uid || ''); const [openModal, setOpenModal] = useState(false); - useEffect(() => { - const checkTextOverflow = () => { - const element = descriptionRef.current; - if (!element) return; + // Backward-compatible display helpers + const facultyDisplay = + faculty_contact || + (typeof faculty_mentor === 'object' && faculty_mentor + ? Object.values(faculty_mentor).join(', ') + : typeof faculty_mentor === 'string' + ? faculty_mentor + : 'N/A'); - if (expanded) { - setNeedsExpansion(true); - return; - } + const phdDisplay = + phd_student_contact || + (typeof phd_student_mentor === 'string' + ? phd_student_mentor + : typeof phd_student_mentor === 'object' && phd_student_mentor + ? Object.values(phd_student_mentor as Record).join(', ') + : 'N/A'); - const isOverflowing = element.scrollHeight > element.clientHeight; - setNeedsExpansion(isOverflowing); - }; - - checkTextOverflow(); - - window.addEventListener('resize', checkTextOverflow); - return () => window.removeEventListener('resize', checkTextOverflow); - }, [expanded, project_description]); + const compensationDisplay = + compensation || [credit, stipend].filter(Boolean).join(', ') || 'N/A'; const handleModalOpen = async () => { - // for (const application of applications) { - // if (application?.uid === uid) { - // alert('You have already applied to this project.'); - // return; - // } - // } - setOpenModal(true); }; + const sectionHeaderSx = { + color: '#5A41D8', + fontWeight: 'bold', + fontSize: '0.95rem', + mt: 2, + mb: 0.5, + }; + return ( <> = ({ height: '100%', }} > - - + + {/* Always visible: Title + Department */} + {project_title} - + {department} - - - Mentor Information - - - Faculty Mentor:{' '} - {Object.values(faculty_mentor).join(', ')} - - - PhD Student Mentor:{' '} - {typeof phd_student_mentor === 'string' - ? phd_student_mentor - : Object.entries(phd_student_mentor ?? {}) - .map(([k, v]) => (k === 'info' ? v : `${v}, ${k}`)) - .join(' ')} - - + {/* Research Description (always visible) */} + Research Description + + {project_description} + - - - Academic Information - - - Student Level: {student_level} - - - Terms Available: {terms_available} - - - Prerequisites: {prerequisites} - - - Credit: {credit} - - - Stipend: {stipend} - - + {/* Application Details (always visible) */} + Application Details + + Application Deadline:{' '} + {application_deadline || 'N/A'} + - - - Application Details - - - Application Requirements:{' '} - {application_requirements} - - - Application Deadline: {application_deadline} - - - Website:{' '} - {website && - !['n/a', 'na', 'none', 'no', ''].includes( - website.toLowerCase().trim() - ) ? ( - - {website} - - ) : ( - 'None provided' - )} - - + {/* Expanded content */} + {expanded && ( + <> + {/* Mentor Information */} + Mentor Information + + Faculty Mentor: {facultyDisplay} + + + PhD Student Mentor: {phdDisplay} + - - - Research Description - - - {project_description} - - + {/* Academic Information */} + Academic Information + + Student Level: {student_level || 'N/A'} + + + Terms Available: {terms_available || 'N/A'} + + + Prerequisites: {prerequisites || 'N/A'} + + + Compensation: {compensationDisplay} + + + )} - - {needsExpansion ? ( - - ) : ( -
- )} - {userRole === 'student_applying' || userRole === 'student_applied' ? ( + {/* Action buttons */} + + + + {(userRole === 'student_applying' || + userRole === 'student_applied') && ( - ) : isFacultyInvolved ? ( + )} + + {isFacultyInvolved && ( - ) : null} + )}
void; getResearchListings: () => void; setResearchListings: (listings: any[]) => void; - getApplications: () => void; - setResearchApplications: (Applications: any[]) => void; } const StudentResearchView: React.FC = ({ researchListings, - researchApplications, role, uid, department, setDepartment, + studentLevel, setStudentLevel, + termsAvailable, setTermsAvailable, getResearchListings, setResearchListings, - getApplications, }) => { - const [myApplications, showMyApplications] = useState(true); const [originalListings, setOriginalListings] = useState([]); const searchInputRef = useRef(null); @@ -52,12 +47,8 @@ const StudentResearchView: React.FC = ({ } }, [researchListings, originalListings.length]); - useEffect(() => { - getApplications(); - }, []); - const handleSearch = (searchText: string) => { - if (!searchText && department === '') { + if (!searchText && department === '' && termsAvailable === '') { setResearchListings([...originalListings]); return; } @@ -85,16 +76,15 @@ const StudentResearchView: React.FC = ({ item.project_description.toLowerCase().includes(searchLower); const mentorMatch = - item.faculty_mentor && - typeof item.faculty_mentor === 'string' && - item.faculty_mentor.toLowerCase().includes(searchLower); + item.faculty_contact && + typeof item.faculty_contact === 'string' && + item.faculty_contact.toLowerCase().includes(searchLower); return titleMatch || descriptionMatch || mentorMatch; }); - console.log('Filtered Listings: ', filteredListings); } - // Department filter with special handling for CISE + // Department filter if (department) { filteredListings = filteredListings.filter((item) => { const normalized = item.department?.toLowerCase().trim(); @@ -110,187 +100,184 @@ const StudentResearchView: React.FC = ({ return normalized === department.toLowerCase(); }); - - console.log('Filtered Listings by Department: ', filteredListings); } - setResearchListings(filteredListings); - }; - - const handleClearFilters = () => { - setDepartment(''); - setStudentLevel(''); - setTermsAvailable(''); - - if (searchInputRef.current) { - searchInputRef.current.value = ''; + // Terms Available filter + if (termsAvailable) { + filteredListings = filteredListings.filter((item) => + item.terms_available + ?.toLowerCase() + .includes(termsAvailable.toLowerCase()) + ); } - if (originalListings.length > 0) { - setResearchListings([...originalListings]); - } else { - getResearchListings(); - } + setResearchListings(filteredListings); }; const handleDepartmentChange = (value: string) => { setDepartment(value); - const searchText = searchInputRef.current?.value || ''; - handleSearch(searchText); + setTimeout(() => { + const searchText = searchInputRef.current?.value || ''; + handleSearch(searchText); + }, 0); + }; + + const handleTermsChange = (value: string) => { + setTermsAvailable(value); + setTimeout(() => { + const searchText = searchInputRef.current?.value || ''; + handleSearch(searchText); + }, 0); }; return ( - <> - - - + + { + if (e.target.value === '') { + handleSearch(''); + } + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + const searchText = (e.target as HTMLInputElement).value; + handleSearch(searchText); + } + }} + sx={{ flex: 1 }} + /> + { + const searchText = searchInputRef.current?.value || ''; + handleSearch(searchText); + }} + /> +
- - {/* Only show search controls when viewing research listings (not applications) */} - {myApplications ? ( - + Department + + - + {/* Student Level filter */} + + Student Level + + - - Department - - - - ) : null} + {/* Terms Available filter */} + + Terms Available + + + - {/* Content Display */} - {myApplications ? ( - - {researchListings.map((item, index) => ( - - - - ))} - - ) : ( - - {researchApplications.map((item, index) => { - return ( - - - - ); - })} + {/* Research Listings */} + + {researchListings.map((item, index) => ( + + - )} -
- + ))} + + ); }; diff --git a/src/components/Research/shared/ImageUploadField.tsx b/src/components/Research/shared/ImageUploadField.tsx new file mode 100644 index 0000000..d20c386 --- /dev/null +++ b/src/components/Research/shared/ImageUploadField.tsx @@ -0,0 +1,100 @@ +/** + * Reusable image upload field component for Research modals + */ +import React, { useRef } from 'react'; +import { Box, Typography, CircularProgress } from '@mui/material'; +import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined'; +import { uploadResearchImage } from './researchModalUtils'; +import { COLORS } from '@/constants/theme'; + +interface ImageUploadFieldProps { + imageFileName: string; + uploading: boolean; + onImageUpload: (url: string, fileName: string) => void; + onUploadStart?: () => void; + onUploadEnd?: () => void; + onError?: (error: Error) => void; +} + +const ImageUploadField: React.FC = ({ + imageFileName, + uploading, + onImageUpload, + onUploadStart, + onUploadEnd, + onError, +}) => { + const fileInputRef = useRef(null); + + const handleImageUpload = async (file: File) => { + onUploadStart?.(); + try { + const downloadURL = await uploadResearchImage(file); + onImageUpload(downloadURL, file.name); + } catch (error) { + console.error('Error uploading image:', error); + onError?.(error as Error); + } finally { + onUploadEnd?.(); + } + }; + + const handleFileDrop = (e: React.DragEvent) => { + e.preventDefault(); + const file = e.dataTransfer.files[0]; + if (file && file.type.startsWith('image/')) { + handleImageUpload(file); + } + }; + + const handleFileSelect = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + handleImageUpload(file); + } + }; + + return ( + e.preventDefault()} + onClick={() => fileInputRef.current?.click()} + sx={{ + border: '2px dashed #ccc', + borderRadius: '12px', + p: 3, + textAlign: 'center', + cursor: 'pointer', + backgroundColor: '#fafafa', + '&:hover': { borderColor: COLORS.primary }, + }} + > + {uploading ? ( + + ) : imageFileName ? ( + + {imageFileName} + + ) : ( + <> + + + Drop your image here, or{' '} + + browse + + + + )} + + + ); +}; + +export default ImageUploadField; diff --git a/src/components/Research/shared/researchModalUtils.ts b/src/components/Research/shared/researchModalUtils.ts new file mode 100644 index 0000000..615d081 --- /dev/null +++ b/src/components/Research/shared/researchModalUtils.ts @@ -0,0 +1,106 @@ +/** + * Shared utilities and constants for Research modal components + */ +import { getStorage, ref, uploadBytes, getDownloadURL } from 'firebase/storage'; +import { v4 as uuidv4 } from 'uuid'; +import { NATURE_OF_JOB_OPTIONS } from '@/constants/research'; + +// Re-export constants for backward compatibility +export { NATURE_OF_JOB_OPTIONS }; + +export interface ResearchFormData { + project_title: string; + project_description: string; + department: string; + image_url: string; + nature_of_job: string; + compensation: string; + faculty_contact: string; + phd_student_contact: string; + application_deadline: string; + hours_per_week: string; + prerequisites: string; + terms_available: string; + student_level: string; + application_requirements: string; + website: string; +} + +export const INITIAL_FORM_DATA: ResearchFormData = { + project_title: '', + project_description: '', + department: '', + image_url: '', + nature_of_job: '', + compensation: '', + faculty_contact: '', + phd_student_contact: '', + application_deadline: '', + hours_per_week: '', + prerequisites: '', + terms_available: '', + student_level: '', + application_requirements: '', + website: '', +}; + +/** + * Upload an image file to Firebase Storage + * @param file - The image file to upload + * @returns The download URL of the uploaded image + */ +export async function uploadResearchImage(file: File): Promise { + const storage = getStorage(); + const storageRef = ref(storage, `research-images/${uuidv4()}_${file.name}`); + await uploadBytes(storageRef, file); + const downloadURL = await getDownloadURL(storageRef); + return downloadURL; +} + +/** + * Validate research form data + * @param formData - The form data to validate + * @returns An object with error messages for each field, or empty if valid + */ +export function validateResearchForm( + formData: ResearchFormData +): Partial> { + const errors: Partial> = {}; + + if (!formData.project_title.trim()) { + errors.project_title = 'Required'; + } + if (!formData.project_description.trim()) { + errors.project_description = 'Required'; + } + if (!formData.department.trim()) { + errors.department = 'Required'; + } + if (!formData.nature_of_job) { + errors.nature_of_job = 'Required'; + } + if (!formData.compensation.trim()) { + errors.compensation = 'Required'; + } + if (!formData.faculty_contact.trim()) { + errors.faculty_contact = 'Required'; + } + if (!formData.application_deadline.trim()) { + errors.application_deadline = 'Required'; + } + if (!formData.hours_per_week.trim()) { + errors.hours_per_week = 'Required'; + } + + return errors; +} + +/** + * Check if form data is valid + * @param formData - The form data to check + * @returns true if valid, false otherwise + */ +export function isFormValid(formData: ResearchFormData): boolean { + const errors = validateResearchForm(formData); + return Object.keys(errors).length === 0; +} diff --git a/src/components/TopBar/TopBar.tsx b/src/components/TopBar/TopBar.tsx index dec24ba..ec17bad 100644 --- a/src/components/TopBar/TopBar.tsx +++ b/src/components/TopBar/TopBar.tsx @@ -5,7 +5,7 @@ import IconButton from '@mui/material/IconButton'; 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 { EceLogoPng } from '@/components/EceLogoPng/EceLogoPng'; import Link from 'next/link'; import { Role, roleMapping } from '@/types/User'; import { useAnnouncements } from '@/contexts/AnnouncementsContext'; diff --git a/src/constants/research.ts b/src/constants/research.ts new file mode 100644 index 0000000..4211d45 --- /dev/null +++ b/src/constants/research.ts @@ -0,0 +1,131 @@ +/** + * Research-related constants for CourseConnect + */ + +// Nature of job options for research positions +export const NATURE_OF_JOB_OPTIONS = [ + 'Research Assistant', + 'Lab Assistant', + 'Teaching Assistant', + 'Field Work', + 'Data Analysis', + 'Other', +] as const; + +// Department options +export const DEPARTMENTS = [ + { + value: 'Computer and Information Sciences and Engineering', + label: 'CISE', + fullName: 'Computer and Information Sciences and Engineering', + }, + { + value: 'Electrical and Computer Engineering', + label: 'ECE', + fullName: 'Electrical and Computer Engineering', + }, + { + value: 'Engineering Education', + label: 'Education', + fullName: 'Engineering Education', + }, + { + value: 'Materials Science and Engineering', + label: 'MSE', + fullName: 'Materials Science and Engineering', + }, + { + value: 'Mechanical and Aerospace Engineering', + label: 'MAE', + fullName: 'Mechanical and Aerospace Engineering', + }, + { + value: 'Civil and Coastal Engineering', + label: 'CCE', + fullName: 'Civil and Coastal Engineering', + }, + { + value: 'Chemical Engineering', + label: 'ChemE', + fullName: 'Chemical Engineering', + }, + { + value: 'Biomedical Engineering', + label: 'BME', + fullName: 'Biomedical Engineering', + }, + { + value: 'Industrial and Systems Engineering', + label: 'ISE', + fullName: 'Industrial and Systems Engineering', + }, + { + value: 'Environmental Engineering Sciences', + label: 'EES', + fullName: 'Environmental Engineering Sciences', + }, +] as const; + +// Student level options +export const STUDENT_LEVELS = [ + 'Freshman', + 'Sophomore', + 'Junior', + 'Senior', + 'Graduate', +] as const; + +// Academic terms (update annually) +export const TERMS = [ + 'Spring 2026', + 'Summer 2026', + 'Fall 2026', + 'Spring 2027', + 'Summer 2027', + 'Fall 2027', +] as const; + +// Application status options +export const APPLICATION_STATUS = { + PENDING: 'Pending', + APPROVED: 'Approved', + DENIED: 'Denied', +} as const; + +// Helper function to get department full name +export function getDepartmentFullName(value: string): string { + const dept = DEPARTMENTS.find((d) => d.value === value); + return dept?.fullName || value; +} + +// Helper function to get department label +export function getDepartmentLabel(value: string): string { + const dept = DEPARTMENTS.find((d) => d.value === value); + return dept?.label || value; +} + +// Helper function to check if a department matches (handles naming variations) +export function isDepartmentMatch( + department: string, + searchTerm: string +): boolean { + const normalized = searchTerm.toLowerCase().trim(); + const deptNormalized = department.toLowerCase().trim(); + + // Direct match + if (deptNormalized === normalized) return true; + + // Check for common variations + if ( + department === 'Computer and Information Sciences and Engineering' || + department === 'Computer and Information Science and Engineering' + ) { + return ( + normalized === 'computer and information science and engineering' || + normalized === 'computer and information sciences and engineering' || + normalized === 'cise' + ); + } + + return false; +} diff --git a/src/constants/theme.ts b/src/constants/theme.ts new file mode 100644 index 0000000..7c90837 --- /dev/null +++ b/src/constants/theme.ts @@ -0,0 +1,144 @@ +/** + * Theme and UI constants for CourseConnect + */ + +// Brand colors +export const COLORS = { + // Primary colors + primary: '#5A41D8', + primaryDark: '#4A35B8', + primaryLight: '#6B52E9', + + // Secondary colors + secondary: '#03ccb9', + secondaryDark: '#01534B', + + // Accent colors + success: '#4caf50', + error: '#F44336', + warning: '#ff9800', + info: '#2196f3', + + // Neutral colors + white: '#FFFFFF', + black: '#000000', + gray: { + 50: '#fafafa', + 100: '#f5f5f5', + 200: '#eeeeee', + 300: '#e0e0e0', + 400: '#bdbdbd', + 500: '#9e9e9e', + 600: '#757575', + 700: '#616161', + 800: '#424242', + 900: '#212121', + }, + + // Background colors + background: { + default: '#FFFFFF', + paper: '#F5F5F5', + }, + + // Text colors + text: { + primary: '#000000', + secondary: '#757575', + disabled: '#bdbdbd', + }, +} as const; + +// Border radius values +export const BORDER_RADIUS = { + xs: '4px', + sm: '8px', + md: '12px', + lg: '16px', + xl: '20px', + round: '9999px', +} as const; + +// Spacing values (in pixels) +export const SPACING = { + xs: 4, + sm: 8, + md: 16, + lg: 24, + xl: 32, + xxl: 48, +} as const; + +// Common Material-UI sx styles +export const COMMON_SX = { + // Card styles + card: { + borderRadius: BORDER_RADIUS.xl, + boxShadow: '0px 4px 20px rgba(0, 0, 0, 0.1)', + padding: '24px', + backgroundColor: COLORS.white, + }, + + // Button styles + primaryButton: { + backgroundColor: COLORS.primary, + color: COLORS.white, + borderRadius: BORDER_RADIUS.md, + textTransform: 'none', + '&:hover': { + backgroundColor: COLORS.primaryDark, + }, + }, + + outlinedButton: { + borderColor: COLORS.primary, + color: COLORS.primary, + borderRadius: BORDER_RADIUS.md, + textTransform: 'none', + borderWidth: '2px', + '&:hover': { + borderWidth: '2px', + backgroundColor: 'rgba(90, 65, 216, 0.04)', + }, + }, + + // Section header styles + sectionHeader: { + fontSize: '1.25rem', + fontWeight: 600, + color: COLORS.text.primary, + mb: 2, + }, +} as const; + +// Grid breakpoints (Material-UI standard) +export const GRID_BREAKPOINTS = { + xs: 0, + sm: 600, + md: 900, + lg: 1200, + xl: 1536, +} as const; + +// Common grid sizes +export const GRID_SIZES = { + full: 12, + half: 6, + third: 4, + quarter: 3, +} as const; + +// Z-index layers +export const Z_INDEX = { + drawer: 1200, + modal: 1300, + snackbar: 1400, + tooltip: 1500, +} as const; + +// Animation durations (in ms) +export const ANIMATION = { + fast: 200, + normal: 300, + slow: 500, +} as const; diff --git a/src/firebase/firebase_config.ts b/src/firebase/firebase_config.ts index 7d4e84b..48c6bad 100644 --- a/src/firebase/firebase_config.ts +++ b/src/firebase/firebase_config.ts @@ -1,6 +1,7 @@ import firebase from 'firebase/compat/app'; import 'firebase/compat/auth'; import 'firebase/compat/firestore'; +import 'firebase/compat/storage'; // TODO: Add SDKs for Firebase products that you want to use // https://firebase.google.com/docs/web/setup#available-libraries diff --git a/src/hooks/useGetItems.ts b/src/hooks/useGetItems.ts index 243fac0..bc05b34 100644 --- a/src/hooks/useGetItems.ts +++ b/src/hooks/useGetItems.ts @@ -6,6 +6,7 @@ import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined import BarChartOutlinedIcon from '@mui/icons-material/BarChartOutlined'; import CampaignOutlinedIcon from '@mui/icons-material/CampaignOutlined'; import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined'; +import ScienceOutlinedIcon from '@mui/icons-material/ScienceOutlined'; import { NavbarItem } from '@/types/navigation'; import { SemesterName } from './useSemesterOptions'; import { useMemo } from 'react'; @@ -15,13 +16,14 @@ export const getNavItems = (userRole: Role): NavbarItem[] => { /* ─────────────────────────────── Student buckets ───────────────────────────── */ case 'Student': case 'student_applied': - case 'student_applying': // <- you had “student_applying” in code + case 'student_applying': // <- you had "student_applying" in code return [ { label: 'Applications', to: '/applications', icon: DescriptionOutlinedIcon, }, + { label: 'Research', to: '/Research', icon: ScienceOutlinedIcon }, { label: 'Status', to: '/status', icon: CheckBoxOutlinedIcon }, { label: 'Announcements', @@ -38,6 +40,7 @@ export const getNavItems = (userRole: Role): NavbarItem[] => { to: '/applications', icon: DescriptionOutlinedIcon, }, + { label: 'Research', to: '/Research', icon: ScienceOutlinedIcon }, { label: 'Courses', to: '/courses', icon: BookOutlinedIcon }, { label: 'Announcements', diff --git a/src/oldPages/Apply/inacessible.tsx b/src/oldPages/Apply/inacessible.tsx deleted file mode 100644 index 53a4415..0000000 --- a/src/oldPages/Apply/inacessible.tsx +++ /dev/null @@ -1,62 +0,0 @@ -'use client'; -import HeaderCard from '@/component/HeaderCard/HeaderCard'; -import SmallHeader from '@/component/SmallHeader/SmallHeader'; -import { Toaster } from 'react-hot-toast'; -import Link from 'next/link'; - -export default function Application() { - return ( - <> - - -
-
-
- Sorry, it looks like applications aren't open yet for the next - semester! -
-
- Please check back later for more information regarding open - positions. -
- -
- If you have any questions, please contact{' '} - - courseconnect.team@gmail.com - {' '} - for further assistance. -
- - Return to Home - -
-
- - ); -} diff --git a/src/oldPages/Apply/page.tsx b/src/oldPages/Apply/page.tsx deleted file mode 100644 index ae94d35..0000000 --- a/src/oldPages/Apply/page.tsx +++ /dev/null @@ -1,623 +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 Container from '@mui/material/Container'; -import DegreeSelect from '@/component/FormUtil/DegreeSelect'; -import SemesterStatusSelect from '@/component/FormUtil/SemesterStatusSelect'; -import PositionSelect from '@/component/FormUtil/PositionSelect'; -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 { FilledInput } from '@mui/material'; -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 { TopNavBarSigned } from '@/component/TopNavBarSigned/TopNavBarSigned'; -import { EceLogoPng } from '@/component/EceLogoPng/EceLogoPng'; -import Chip from '@mui/material/Chip'; -import styles from './style.module.css'; -import { useRouter } from 'next/navigation'; -import InputLabel from '@mui/material/InputLabel'; -import MenuItem from '@mui/material/MenuItem'; -import FormControl from '@mui/material/FormControl'; -import ListItemText from '@mui/material/ListItemText'; -import Select, { SelectChangeEvent } from '@mui/material/Select'; -import Checkbox from '@mui/material/Checkbox'; -import HeaderCard from '@/component/HeaderCard/HeaderCard'; - -const ITEM_HEIGHT = 48; -const ITEM_PADDING_TOP = 8; -const MenuProps = { - PaperProps: { - style: { - maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, - width: 250, - }, - }, -}; - -// note that the application needs to be able to be connected to a specific faculty member -// so that the faculty member can view the application and accept/reject it -// the user can indicate whether or not it is unspecified I suppose? -// but that would leave a little bit of a mess. -// best to parse the existing courses and then have the user select -// from a list of existing courses -// ...yeah that's probably the best way to do it -export default function Application() { - // get the current user's uid - const router = useRouter(); - const { user } = useAuth(); - const userId = user.uid; - - // get the current date in month/day/year format - const current = new Date(); - const current_date = `${ - current.getMonth() + 1 - }-${current.getDate()}-${current.getFullYear()}`; - - // extract the nationality - 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) => { - const handleSendEmail = async () => { - try { - let courseNamesWithSemester = coursesArray.map((course) => { - let parts = course.split(' :'); - let courseNameWithSemester = parts[0].trim(); - return courseNameWithSemester; // Get the first part and trim any extra spaces - }); - let resultString = courseNamesWithSemester.join(', '); - - const response = await fetch( - 'https://us-central1-courseconnect-c6a7b.cloudfunctions.net/sendEmail', - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - type: 'applicationConfirmation', - data: { - user: { - name: user.displayName, - email: applicationData.email, - }, - position: applicationData.position, - classCode: resultString, - }, - }), - } - ); - - if (response.ok) { - const data = await response.json(); - console.log('Email sent successfully:', data); - } else { - throw new Error('Failed to send email'); - } - } catch (error) { - console.error('Error sending email:', error); - } - }; - - setLoading(true); - event.preventDefault(); - // extract the form data from the current event - const formData = new FormData(event.currentTarget); - - // extract availability checkbox's values - 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'); - } - - // extract semester checkbox's values - const semesterCheckbox_fall_2023 = - formData.get('semesterCheckbox_fall_2024') === 'on'; - const semesterCheckbox_spring_2024 = - formData.get('semesterCheckbox_spring_2025') === 'on'; - - const semesterArray: string[] = []; - - let f24 = false; - let s25 = false; - for (let i = 0; i < personName.length; i++) { - if (personName[i].includes('Summer 2025')) { - f24 = true; - } - if (personName[i].includes('Fall 2025')) { - s25 = true; - } - } - - if (f24) { - semesterArray.push('Summer 2025'); - } - if (s25) { - semesterArray.push('Fall 2025'); - } - - // get courses as array - const coursesArray = personName; - - let coursesMap: { [key: string]: string } = {}; - for (let i = 0; i < coursesArray.length; i++) { - coursesMap[coursesArray[i]] = 'applied'; - } - - // extract the specific user data from the form data into a parsable object - 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: formData.get('positions-radio-group') as string, - 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')) { - 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.position === null || - applicationData.position === '' - ) { - toast.error('Please enter a position!'); - 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(); - // console.log(applicationData); // FOR DEBUGGING ONLY! - - // use fetch to send the application data to the server - // this goes to a cloud function which creates a document based on - // the data from the form, identified by the user's firebase auth uid - 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) { - await handleSendEmail(); - toast.dismiss(toastId); - toast.success('Application submitted!'); - console.log('SUCCESS: Application data sent to server successfully'); - // now, update the role of the user to student_applied - await UpdateRole(userId, 'student_applied'); - // then, refresh the page somehow to reflect the state changing - // so the form goes away and the user can see the status of their application - - 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 [personName, setPersonName] = React.useState([]); - - const handleChange = (event: SelectChangeEvent) => { - const { - target: { value }, - } = event; - setPersonName( - // On autofill we get a stringified value. - typeof value === 'string' ? value.split(',') : value - ); - }; - const [names, setNames] = useState([]); - - React.useEffect(() => { - async function fetchData() { - try { - let data: string[] = []; - let visibleSems: string[] = []; - await firebase - .firestore() - .collection('semesters') - .get() - .then((snapshot) => - snapshot.docs.map((doc) => { - if (!doc.data().hidden) { - visibleSems.push(doc.data().semester); - } - }) - ); - - await firebase - .firestore() - .collection('courses') - .get() - .then((snapshot) => - snapshot.docs.map((doc) => { - if (visibleSems.includes(doc.data().semester)) { - data.push(doc.id); - } - }) - ); - setNames(data); - console.log(names); - } catch (err) { - console.log(err); - } - } - fetchData(); - }, []); - console.log('eek'); - - return ( - <> - - - - - - Application submitted successfully! - - - - - - - - Personal Information - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - Position Information - -
- - - - Please select the position for which you are interested in - applying. - - - - - - Please select one or more options describing the number of - hours per week you will be available. - - - - - - Please list the course(s) for which you are applying. Ensure - that you select the courses with your desired semester and - instructor. - - - - - Course(s)* - - - - - - - Please provide your most recently calculated cumulative UF - GPA. - - - - - - Please upload a google drive link to your resume. - - - - - - Please describe your qualifications for the position and - course(s) for which you are applying.
- - If you have been a TA, UPI, or grader before, please mention - the course(s) and teacher(s) for which you worked. - {' '} -

- Write about any relevant experience, such as teaching, - tutoring, grading, or coursework.
-
- -
- -
- - -
-
-
- - ); -} diff --git a/src/oldPages/Apply/style.css b/src/oldPages/Apply/style.css deleted file mode 100644 index 474c309..0000000 --- a/src/oldPages/Apply/style.css +++ /dev/null @@ -1,115 +0,0 @@ -.student-landing-page { - display: flex; - flex-direction: row; - justify-content: center; - width: 100%; -} - - -.student-landing-page .overlap-wrapper { - height: 1024px; - width: 100%; -} - -.student-landing-page .overlap { - height: 1024px; - position: relative; - width: 100%; -} - -.student-landing-page .overlap-2 { - height: 545px; - left: 0; - position: absolute; - top: 0; - width: 100%; -} - -.student-landing-page .color-block-frame { - height: 350px; - left: 0; - overflow: hidden; - position: absolute; - top: 0; - width: 100%; -} - -.student-landing-page .overlap-group-2 { - height: 458px; - left: -5px; - position: relative; - width: 100%; -} - -.student-landing-page .color-block { - background-color: #001776; - height: 458px; - left: 5px; - position: absolute; - top: 0; - width: 100%; -} - -.student-landing-page .GRADIENTS { - height: 350px; - left: 5px; - position: absolute; - top: 0; - width: 100%; -} - -.student-landing-page .glass-card { - -webkit-backdrop-filter: blur(200px) brightness(100%); - backdrop-filter: blur(200px) brightness(100%); - background-blend-mode: luminosity; - background-color: #00000024; - height: 458px; - left: 0; - position: absolute; - top: 0; - width: 101%; -} - -.student-landing-page .ece-logo-png-2 { - left: 27px !important; - position: absolute !important; - top: 23px !important; -} - -.student-landing-page .full-name-and-bio-instance { - left: 38% !important; - position: absolute !important; - top: 250px !important; -} - -.student-landing-page .top-nav-bar-signed-in { - left: 80% !important; - position: absolute !important; - top: 27px !important; -} - -.student-landing-page .text-wrapper-8 { - color: #ffffff; - font-family: "SF Pro Display-Medium", Helvetica; - font-size: 50px; - font-weight: 500; - left: 45%; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 114px; - width: 133px; -} - -.student-landing-page .profile-instance { - left: 52% !important; - position: absolute !important; - top: 640px !important; -} - -.student-landing-page .apply-instance { - left: 38% !important; - position: absolute !important; - top: 640px !important; -} - diff --git a/src/oldPages/Course/[className]/page.tsx b/src/oldPages/Course/[className]/page.tsx deleted file mode 100644 index 12bf7ce..0000000 --- a/src/oldPages/Course/[className]/page.tsx +++ /dev/null @@ -1,148 +0,0 @@ -'use client'; - -import { FC, useEffect, useState } from 'react'; -import HeaderCard from '@/component/HeaderCard/HeaderCard'; -import './style.css'; -import firebase from '@/firebase/firebase_config'; -import 'firebase/firestore'; -import CourseDetails from '@/component/CourseDetails/CourseDetails'; -import { getAuth } from 'firebase/auth'; -import { useUserRole } from '@/firebase/util/GetUserRole'; -import { useSearchParams } from 'next/navigation'; -import { LinearProgress } from '@mui/material'; - -interface pageProps { - params: { semester: string; collection: string; courseCode: string }; -} - -interface TA { - name: string; - email: string; -} - -interface Schedule { - day: string; - location: string; - time: string; -} - -interface CourseDetails { - id: string; - courseName: string; - instructor: string; - email: string; - studentsEnrolled: number; - maxStudents: number; - courseCode: string; - TAs: TA[]; - department: string; - credits: number; - semester: string; - title: string; - meetingTimes: Schedule[]; -} - -const StatisticsPage: FC = ({ params }) => { - const auth = getAuth(); - const user = auth.currentUser; - const searchParams = useSearchParams(); - const courseId = searchParams.get('courseId'); - const onGoing = searchParams.get('onGoing') === 'true'; - const { - role, - loading: roleLoading, - error: roleError, - } = useUserRole(user?.uid); - - const [courseData, setCourseData] = useState(null); - - const getCourseDetails = async ( - courseId: string - ): Promise => { - try { - const db = firebase.firestore(); // Use the existing Firestore instance - const doc = onGoing - ? await db.collection('courses').doc(courseId).get() - : await db.collection('past-courses').doc(courseId).get(); - if (doc.exists) { - const data = doc.data(); - return { - id: doc.id, - courseName: data?.code || 'N/A', - instructor: data?.professor_names || 'Unknown', - email: data?.professor_emails || 'Unknown', - studentsEnrolled: data?.enrolled || 0, - maxStudents: data?.enrollment_cap || 0, - courseCode: data?.class_number || 'N/A', - TAs: data?.tas || [], - department: data?.department || 'Unknown', - credits: data?.credits || 0, - semester: data?.semester || 'N/A', - title: data?.title || 'N/A', - meetingTimes: data?.meeting_times || 'N/A', - }; - } else { - throw new Error('No matching documents found'); - } - } catch (error) { - console.error('Error getting course details:', error); - return null; - } - }; - - useEffect(() => { - if (courseId) { - const fetchData = async () => { - try { - const result = await getCourseDetails(courseId); - setCourseData(result); - } catch (error) { - console.error('Error fetching data: ', error); - } - }; - - if (role && !roleLoading) { - fetchData(); - } - } - }, [courseId, role, roleLoading]); - - if (roleError) { - return

Error loading role

; - } - - if (!user) { - return

Please sign in.

; - } - - if (roleLoading || !role || (role !== 'faculty' && role !== 'admin')) { - return ; - } - - return ( - <> - {courseData && ( - <> - - - - - )} - - ); -}; - -export default StatisticsPage; diff --git a/src/oldPages/Course/[className]/style.css b/src/oldPages/Course/[className]/style.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/oldPages/Course/page.tsx b/src/oldPages/Course/page.tsx deleted file mode 100644 index d1a24e8..0000000 --- a/src/oldPages/Course/page.tsx +++ /dev/null @@ -1,227 +0,0 @@ -'use client'; -import './style.css'; -import React, { useEffect, useState, useMemo } from 'react'; -import { Toaster } from 'react-hot-toast'; -import SmallClassCard from '@/component/SmallClassCard/SmallClassCard'; -import HeaderCard from '@/component/HeaderCard/HeaderCard'; -import firebase from '@/firebase/firebase_config'; -import 'firebase/firestore'; -import { getAuth } from 'firebase/auth'; -import { Bio } from '@/components/Bio/Bio'; -import { SemesterTimeline } from '@/component/SemesterTimeline/SemesterTimeline'; -import useFetchPastCourses from '@/hooks/old/usePastCourses'; -import { CourseType } from '@/types/User'; -import SemesterSelection from '@/component/SemesterSelection/SemesterSelection'; -import { SelectSemester } from '@/types/User'; - -export default function FacultyCourses() { - const auth = getAuth(); - const [courses, setCourses] = useState([]); - const [loading, setLoading] = useState(true); // Loading state for courses - - const db = firebase.firestore(); - const [selectedSemester, setSelectedSemester] = useState(0); - const [selectedSemesters, setSelectedSemesters] = useState( - () => { - const stored = localStorage.getItem('selectedSemesters'); - return stored ? JSON.parse(stored) : []; - } - ); - - useEffect(() => { - localStorage.setItem( - 'selectedSemesters', - JSON.stringify(selectedSemesters) - ); - }, [selectedSemesters]); - - const selectedSemesterValues = useMemo(() => { - return selectedSemesters.map((option) => option.value); - }, [selectedSemesters]); - const [groupedCourses, setGroupedCourses] = useState< - Map - >(new Map()); - const [semesterArray, setSemesterArray] = useState([]); - const user = auth.currentUser; - const uemail = user?.email; - const { pastCourses, loadingPast, error } = useFetchPastCourses( - selectedSemesterValues, - uemail - ); - - useEffect(() => { - const fetchCourses = async () => { - try { - setLoading(true); - const snapshot = await db - .collection('courses') - .where('professor_emails', 'array-contains', uemail) - .get(); - - const filteredDocs = snapshot.docs.filter( - (doc) => doc.data().code !== null && doc.data().code !== undefined - ); - - const courses = filteredDocs.map((doc) => ({ - id: doc.id, - code: doc.data().code, - courseId: doc.data().class_number, - semester: doc.data().semester, - })); - - const courseMap = new Map(); - courses.forEach((course) => { - if (!courseMap.has(course.semester)) { - courseMap.set(course.semester, []); - } - courseMap.get(course.semester)?.push(course); - }); - - setGroupedCourses(courseMap); - const semesterKeys = Array.from(courseMap.keys()); - const order = ['Fall', 'Spring', 'Summer']; - const sortedSemesterKeys = semesterKeys.sort( - (a, b) => - order.indexOf(a.split(' ')[0]) - order.indexOf(b.split(' ')[0]) - ); - setSemesterArray(sortedSemesterKeys); - } catch (error) { - console.error('Error fetching courses:', error); - } finally { - setLoading(false); - } - }; - fetchCourses(); - }, [uemail]); - - useEffect(() => { - setCourses(groupedCourses.get(semesterArray[selectedSemester]) || []); - }, [selectedSemester, groupedCourses]); - - useEffect(() => { - return () => { - localStorage.removeItem('selectedSemesters'); - }; - }, []); - - return ( - <> - - - -
-
-
My courses:
- {semesterArray.length > 0 && ( - - )} - {loading ? null : courses.length !== 0 ? ( -
- {courses.map((course, index) => ( -
- -
- ))} -
- ) : ( -
- Currently, no courses have been assigned to you yet. Please wait - until an admin assigns your courses. Once your courses are - assigned, you'll be able to access applicants for those - classes.{' '} -
- )} -
- -
-
Past Courses:
- - - {loadingPast ? ( -
Loading past courses...
- ) : pastCourses.length !== 0 ? ( -
- {pastCourses.map((course, index) => ( -
- -
- ))} -
- ) : ( -
No past courses available.
- )} -
-
- - ); -} diff --git a/src/oldPages/Course/style.css b/src/oldPages/Course/style.css deleted file mode 100644 index 007824f..0000000 --- a/src/oldPages/Course/style.css +++ /dev/null @@ -1,74 +0,0 @@ -.text-wrapper-11 { - - font-family: 'SF Pro Display-Medium', Helvetica; - color: #000; - text-align: left; - margin-top: 18px; - margin-bottom: 26px; - -} -.courses { - font-size: 36px; -} -.text-past { - margin-top: 18px; - font-size: 36px; - font-weight: 500; - font-family: 'SF Pro Display-Medium', Helvetica; - color: #000; - margin-bottom: 26px; -} - -.ta { - font-size: 40px; -} - -.page-container { - display: flex; - flex-direction: row; - padding-left: 20px; - padding-right: 20px; - margin-top: 175px; - justify-content: space-around; - width: calc(100% - 40px); - box-sizing: border-box; -} - -@media (max-width: 1200px) { - .page-container { - padding-left: 40px; /* Adjust padding */ - gap: 10%; /* Decrease gap */ - } -} - -@media (max-width: 900px) { - .page-container { - padding-left: 30px; /* Adjust padding */ - gap: 7%; /* Further decrease gap */ - } -} - -@media (max-width: 600px) { - .page-container { - padding-left: 20px; /* Adjust padding */ - gap: 5%; /* Minimal gap */ - } -} - - - -.class-cards-container { - display: flex; - margin-top: 8px; - height: 100%; -} -.class { - margin: 22px; -} -.full-name-and-bio-instance { - left: 50%; - position: absolute !important; - top: 200px !important; - transform: translateY(-50%); - transform: translateX(-50%); -} diff --git a/src/oldPages/Statuse/page.tsx b/src/oldPages/Statuse/page.tsx deleted file mode 100644 index 8a4c373..0000000 --- a/src/oldPages/Statuse/page.tsx +++ /dev/null @@ -1,178 +0,0 @@ -'use client'; -import * as React from 'react'; -import CssBaseline from '@mui/material/CssBaseline'; - -import Box from '@mui/material/Box'; -import HeaderCard from '@/component/HeaderCard/HeaderCard'; -import Container from '@mui/material/Container'; - -import { useAuth } from '@/firebase/auth/auth_context'; -import { Toaster } from 'react-hot-toast'; - -import { ApplicationStatusCard } from '@/components/ApplicationStatusCard/ApplicationStatusCard'; -import { useState } from 'react'; -import GetUserRole from '@/firebase/util/GetUserRole'; -import { ApplicationStatusCardDenied } from '@/components/ApplicationStatusCardDenied/ApplicationStatusCardDenied'; -import { ApplicationStatusCardAccepted } from '@/components/ApplicationStatusCardAccepted/ApplicationStatusCardAccepted'; -import './style.css'; -import firebase from '@/firebase/firebase_config'; -import 'firebase/firestore'; -import { getDoc } from 'firebase/firestore'; - -// note that the application needs to be able to be connected to a specific faculty member -// so that the faculty member can view the application and accept/reject it -// the user can indicate whether or not it is unspecified I suppose? -// but that would leave a little bit of a mess. -// best to parse the existing courses and then have the user select -// from a list of existing courses -// ...yeah that's probably the best way to do it - -// todo: If the user role says application denied, then make the general denied box with a -// denied message that leads to a reapplication form. -// If their user role says accepted, then add all of their assignments as accepted -// If nothing then (go throgh their application list and say pending). -// -// todo: add the apply link to all states. -export default function Status() { - // get the current user's uid - interface Application { - id: string; - additionalprompt: string; - available_hours: string; - available_semesters: string; - courses: string; - date: string; - degree: string; - department: string; - email: string; - englishproficiency: string; - firstname: string; - gpa: string; - lastname: string; - nationality: string; - phonenumber: string; - position: string; - qualifications: string; - semesterstatus: string; - ufid: string; - isNew?: boolean; - mode?: 'edit' | 'view' | undefined; - } - const db = firebase.firestore(); - const { user } = useAuth(); - const userId = user.uid; - const [role, loading, error] = GetUserRole(user?.uid); - - const [courses, setCourses] = useState(null); - const [adminDenied, setAdminDenied] = useState(false); - - const [assignment, setAssignment] = useState([]); - - React.useEffect(() => { - async function fetch() { - let counter = 0; // Start counter at 0 (for the original userId) - let statusRef2 = db.collection('assignments').doc(userId); // Initial document reference - let assignmentArray = []; // Temporary array to store all class_codes - - while (true) { - const doc = await statusRef2.get(); - - if (doc.exists) { - const classCodes = doc.data()?.class_codes; - - // Append class_codes (string) to assignmentArray - if (classCodes) { - assignmentArray.push(classCodes); // Append string class_codes - } - } else { - break; // Stop when no more documents are found - } - - // Move to the next document (userId-1, userId-2, etc.) - counter++; - statusRef2 = db.collection('assignments').doc(`${userId}-${counter}`); - } - - // Update the state with the final array after fetching all documents - - setAssignment(assignmentArray); - - const statusRef = db.collection('applications').doc(userId); - await getDoc(statusRef).then((doc) => { - if (doc.data() != null && doc.data() != undefined) { - setAdminDenied(doc.data()?.status == 'Admin_denied'); - setCourses(doc.data()?.courses); - } - }); - - return; - } - if (!courses) { - fetch(); - } - }, [courses]); - - return ( - <> - - - - - - - {courses && - adminDenied && - Object.entries(courses).map(([key, value]) => ( -
- -
- ))} - - {assignment && - !adminDenied && - Object.entries(assignment).map(([key, value]) => ( -
- -
- ))} - - {courses && - !adminDenied && - Object.entries(courses).map(([key, value]) => ( -
- {(value == 'applied' || value == 'accepted') && ( - - )} - {value == 'denied' && ( - - )} -
- ))} -
-
-
- - ); -} diff --git a/src/oldPages/Statuse/style.css b/src/oldPages/Statuse/style.css deleted file mode 100644 index 5f2ce12..0000000 --- a/src/oldPages/Statuse/style.css +++ /dev/null @@ -1,183 +0,0 @@ - - -@media (max-width: 650px) { -.application-status-card { - background-color: #ffffff; - border-radius: 20px; - box-shadow: 0px 2px 20px 4px #00000040; - height: 222px; - position: relative; - width: 400px; -} - -.application-status-card .overlap { - height: 186px; - left: 34px; - position: absolute; - top: 12px; - width: 204px; -} - -.application-status-card .inner-content { - height: 38px; - left: 0; - position: absolute; - top: 0; - width: 159px; -} - -.application-status-card .text-wrapper-6 { - color: black; - font-family: "SF Pro Display-Medium", Helvetica; - font-size: 32px; - font-weight: 500; - left: 0; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 0; - white-space: nowrap; -} - -.application-status-card .coarse-assistant-wrapper { - height: 29px; - left: 0; - position: absolute; - top: 54px; - width: 172px; -} - -.application-status-card .text-wrapper-7 { - color: #000000; - font-family: "SF Pro Display-Regular", Helvetica; - font-size: 21px; - font-weight: 400; - left: 20; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 10px; -} - -.application-status-card .overlap-2 { - color: black; - height: 100px; - left: 0px; - position: absolute; - top: 0; - width: 104px; -} - -.application-status-card .div-wrapper { - color: black; - height: 19px; - left: 100px; - position: absolute; - top: 88px; - width: 104px; -} - -.application-status-card .text-wrapper-8 { - color: black; - font-size: 16px; - font-weight: 400; - left: 0; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 0; - white-space: nowrap; -} - -.application-status-card .rectangle { - display: none; -} - -.application-status-card .inner-content-2 { - height: 29px; - left: 34px; - position: absolute; - top: 160px; - width: 197px; -} - -.application-status-card .text-wrapper-9 { - color: #000000; - font-family: "SF Pro Display-Regular", Helvetica; - font-size: 24px; - font-weight: 400; - left: 100px; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 10px; -} - -.application-status-card .application-button { - height: 43px; - left: 120px; - position: absolute; - top: 160px; - width: 226px; - margin-top: 0px; -} - -.application-status-card .overlap-group-2 { - background-color: #f2a9001f; - border: 1px solid; - border-color: #f2a900; - border-radius: 10px; - height: 43px; - position: relative; - width: 224px; -} -.application-status-card .overlap-group-34 { - background-color: #00f2301f; - border: 1px solid; - border-color: #00f230; - border-radius: 10px; - height: 43px; - position: relative; - width: 224px; -} -.application-status-card .text-wrapper-19 { - color: #00a674; - font-family: "SF Pro Display-Medium", Helvetica; - font-size: 15px; - font-weight: 500; - left: 20px; - letter-spacing: 0; - line-height: normal; - position: absolute; - text-align: center; - top: 11px; - width: 182px; -} - -.application-status-card .text-wrapper-10 { - color: #f2a900; - font-family: "SF Pro Display-Medium", Helvetica; - font-size: 15px; - font-weight: 500; - left: 20px; - letter-spacing: 0; - line-height: normal; - position: absolute; - text-align: center; - top: 11px; - width: 182px; -} - -.application-status-card .text-wrapper-9 { - color: #000000; - font-family: "SF Pro Display-Regular", Helvetica; - font-size: 10px; - font-weight: 200; - left: 40px; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 45px; - width: 300px; -} -} diff --git a/src/oldPages/about/page.tsx b/src/oldPages/about/page.tsx deleted file mode 100644 index 408666f..0000000 --- a/src/oldPages/about/page.tsx +++ /dev/null @@ -1,92 +0,0 @@ -'use client'; - -import { Toaster } from 'react-hot-toast'; -import { EceLogoPng } from '@/component/EceLogoPng/EceLogoPng'; -import { TopNavBar } from '@/component/TopNavBar/TopNavBar'; -import styles from './style.module.css'; -import { Card } from '@/component/Card/Card'; - -import { useAuth } from '@/firebase/auth/auth_context'; -import { TopNavBarSigned } from '@/component/TopNavBarSigned/TopNavBarSigned'; -export default function About() { - const { user } = useAuth(); - return ( - <> - -
-
-
- Color block frame -
-
-
Welcome to
-
Course Connect
-
-

- - Connecting Bright Minds for a Brighter Future -
-
- -
-
-

-

- Course Connect is your all-in-one solution for managing the TA, - PI, and grader processes. Whether you are a student looking for - opportunities, a faculty member seeking to streamline your - workflow, or an administrator in charge of overseeing the entire - system, Course Connect offers the features and functionality you - need to succeed. -

-
-
- - {!user && } - {user && } - -
-
-
- Line -
Features
- Line -
-
- - - - -
-
-
- - ); -} diff --git a/src/oldPages/about/style.css b/src/oldPages/about/style.css deleted file mode 100644 index 6f0226d..0000000 --- a/src/oldPages/about/style.css +++ /dev/null @@ -1,280 +0,0 @@ -.section-about-us { - height: 557px; - left: 61px; - position: absolute; - top: 770px; - width: 1310px; -} - -.about-us { - height: 36px; - left: 56px; - position: absolute; - top: 0; - width: 1208px; -} - -.img { - height: 5px; - left: 80%; - position: absolute; - top: 13px; - width: 450px; -} - -.img2 { - position: absolute; - top: 13px; - left: 20%; -} - -.student-card { - left: 80px !important; - position: absolute !important; - top: 70px !important; -} - -.faculty-card-2 { - left: 660px !important; - position: absolute !important; - top: 70px !important; -} - -.administrator-card-2 { - left: 1260px !important; - position: absolute !important; - top: 70px !important; -} - -.text-wrapper-6 { - color: #000000; - font-family: "SF Pro Display-Medium", Helvetica; - font-size: 30px; - font-weight: 500; - left: 64%; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 0; - white-space: nowrap; -} - - -.login-low-fi { - background-color: #ffffff; - display: flex; - flex-direction: row; - justify-content: center; -} - -.login-low-fi .div-2 { - background-color: #ffffff; - height: 100%; - position: absolute; - width: 100%; -} - -.login-low-fi .overlap-2 { - height: 777px; - left: 0; - position: absolute; - top: 0; - width: 100%; -} - -.login-low-fi .color-block-frame { - height: 458px; - left: 0; - position: relative; - top: 0; - width: 100%; - - height: 80%; -} - -.login-low-fi .sign-in-title { - height: 154px; - left: 20%; - position: absolute; - top: 141px; - width: 100%; -} - -.login-low-fi .connecting-bright { - color: #ffffff; - font-family: "SF Pro Display-Regular", Helvetica; - font-size: 20px; - font-weight: 400; - left: 7px; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 124px; - width: 100%; -} - -.login-low-fi .text-wrapper-8 { - color: #ffffff; - font-family: "SF Pro Display-Regular", Helvetica; - font-size: 25px; - font-weight: 400; - letter-spacing: 0; -} - -.login-low-fi .text-wrapper-9 { - font-family: "Poppins", Helvetica; - font-weight: 300; -} - -.login-low-fi .sign-in-to { - height: 120px; - left: 0; - position: absolute; - top: 0; - width: 553px; -} - -.login-low-fi .text-wrapper-10 { - color: #ffffff; - font-family: "SF Pro Display-Medium", Helvetica; - font-size: 45px; - font-weight: 500; - left: 0; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 0; - white-space: nowrap; - padding-bottom: 80px; -} - -.login-low-fi .text-wrapper-11 { - color: #ffffff; - font-family: "SF Pro Display-Bold", Helvetica; - font-size: 70px; - font-weight: 700; - left: 0; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 48px; - width: 549px; -} - -.login-low-fi .register-here-text { - height: 54px; - left: 20%; - position: absolute; - top: 401px; - width: 312px; - -} - -.login-low-fi .p { - color: #ffffff; - font-family: "SF Pro Display-Regular", Helvetica; - font-size: 20px; - font-weight: 400; - left: 0; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 0; - width: 1100px; -} - -.login-low-fi .you-can-register { - color: #ffffff; - font-family: "SF Pro Display-Regular", Helvetica; - font-size: 20px; - font-weight: 400; - left: 0; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 30px; - width: 308px; -} - -.login-low-fi .text-wrapper-12 { - font-family: "SF Pro Display-Bold", Helvetica; - font-weight: 300; - text-decoration: underline; - color: white; - -} - -.login-low-fi .line { - height: 1px; - left: 150px; - object-fit: cover; - position: absolute; - top: 402px; - width: 126px; -} - -.login-low-fi .log-in-card-instance { - left: 100px !important; - position: absolute !important; - top: 10px !important; -} - -.login-low-fi .overlap-wrapper { - all: unset; - box-sizing: border-box; - height: 37px; - left: 1265px; - position: absolute; - top: 27px; - width: 137px; -} - -.login-low-fi .overlap-3 { - background-color: #ffffff; - border-radius: 10px; - height: 37px; - position: relative; - width: 135px; -} - -.login-low-fi .text-wrapper-13 { - color: #000000; - font-family: "SF Pro Display-Bold", Helvetica; - font-size: 18px; - font-weight: 700; - left: 11px; - letter-spacing: 0; - line-height: normal; - position: absolute; - text-align: center; - top: 9px; - white-space: nowrap; - width: 114px; -} - -.login-low-fi .ece-logo-png-2 { - left: 27px !important; - position: absolute !important; - top: 23px !important; -} - -.login-low-fi .top-nav-bar-instance { - left: 82% !important; - position: absolute !important; - top: 27px !important; - width: 100% !important; -} - -.login-low-fi .design-component-instance-node { - left: 10px !important; - width: 105px !important; -} - -.login-low-fi .top-nav-bar-2 { - width: 100% !important; -} - -.login-low-fi .top-nav-bar-3 { - width: 100% !important; -} - diff --git a/src/oldPages/about/style.module.css b/src/oldPages/about/style.module.css deleted file mode 100644 index 331b2a4..0000000 --- a/src/oldPages/about/style.module.css +++ /dev/null @@ -1,392 +0,0 @@ -.sectionaboutus { - height: 557px; - margin-top: 500px; - position: sticky; - width: 1310px; -} - -.aboutus { - display: flex; /* Use flexbox to align items horizontally */ - align-items: center; /* Vertically center the content */ - justify-content: space-between; /* Space elements evenly */ - height: 36px; - width: 100%; /* Ensure the container spans the full width */ -} - -.img { - height: 5px; - width: 450px; -} - -.img2 { - height: 5px; - width: 450px; -} -.cardholder{ - display:flex; - justify-content: space-between; - margin-top:60px; - flex-direction: row; -} -.textwrapper6 { - margin-left: -250px; /* Center the text */ -} - -.textwrapper6 { - color: #000000; - font-family: 'SF Pro Display-Medium', Helvetica; - font-size: 30px; - font-weight: 500; - left: 64%; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 0; - white-space: nowrap; -} - -.loginlowfi { - background-color: #ffffff; - display: flex; - flex-direction: column; - justify-content: center; - margin-bottom: 200px; - align-items: center; -} - -.loginlowfi .div2 { - background-color: #ffffff; - height: 100%; - width: 100%; - padding: 0; - margin-left: 10px; - margin-right: 10px; - overflow-x: hidden; -} - -.loginlowfi .overlap2 { - height: 777px; - left: 0; - position: absolute; - top: 0; - width: 100%; - padding: 0; - margin: 0; -} - -.loginlowfi .colorblockframe { - height: 458px; - left: 0; - position: relative; - top: 0; - width: 100%; - - height: 60%; -} - -.loginlowfi .signintitle { - height: 154px; - position: absolute; - margin-left: 16%; - top: 130px; - width: 80%; -} - -.loginlowfi .connectingbright { - color: #ffffff; - font-family: 'SF Pro Display-Regular', Helvetica; - font-size: 20px; - font-weight: 400; - letter-spacing: 0; - line-height: normal; - width: 100%; -} - -.loginlowfi .textwrapper8 { - color: #ffffff; - font-family: 'SF Pro Display-Regular', Helvetica; - font-size: 25px; - font-weight: 400; - letter-spacing: 0; -} - -.loginlowfi .textwrapper9 { - font-family: 'Poppins', Helvetica; - font-weight: 300; -} - -.loginlowfi .signinto { - height: 120px; - width: 553px; -} - -.loginlowfi .textwrapper10 { - color: #ffffff; - font-family: 'SF Pro Display-Medium', Helvetica; - font-size: 45px; - font-weight: 500; - left: 0; - letter-spacing: 0; - line-height: normal; - top: 0; - white-space: nowrap; -} - -.loginlowfi .textwrapper11 { - color: #ffffff; - font-family: 'SF Pro Display-Bold', Helvetica; - font-size: 70px; - font-weight: 700; - left: 0; - letter-spacing: 0; - line-height: normal; -} - -.loginlowfi .p { - color: #ffffff; - font-family: 'SF Pro Display-Regular', Helvetica; - font-size: 18px; - font-weight: 400; - left: 0; - letter-spacing: 0; - line-height: normal; - width: 100%; -} - -.loginlowfi .youcanregister { - color: #ffffff; - font-family: 'SF Pro Display-Regular', Helvetica; - font-size: 20px; - font-weight: 400; - left: 0; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 30px; - width: 308px; -} - -.loginlowfi .textwrapper12 { - font-family: 'SF Pro Display-Bold', Helvetica; - font-weight: 300; - text-decoration: underline; - color: white; -} - -.loginlowfi .line { - height: 1px; - left: 150px; - object-fit: cover; - position: absolute; - top: 402px; - width: 126px; -} - -.loginlowfi .logincardinstance { - left: 100px !important; - position: absolute !important; - top: 10px !important; -} - -.loginlowfi .overlapwrapper { - all: unset; - box-sizing: border-box; - height: 37px; - left: 1265px; - position: absolute; - top: 27px; - width: 137px; -} - -.loginlowfi .overlap3 { - background-color: #ffffff; - border-radius: 10px; - height: 37px; - position: relative; - width: 135px; -} - -.loginlowfi .textwrapper13 { - color: #000000; - font-family: 'SF Pro Display-Bold', Helvetica; - font-size: 18px; - font-weight: 700; - left: 11px; - letter-spacing: 0; - line-height: normal; - position: absolute; - text-align: center; - top: 9px; - white-space: nowrap; - width: 114px; -} - -.loginlowfi .ecelogopng2 { - left: 27px !important; - position: absolute !important; - top: 23px !important; -} - -.loginlowfi .topnavbarinstance { - position: absolute; - right: 70px; - top: 27px; -} - -@media (max-width: 570px) { - .ecelogopng2 { - display: none; - } -} -.loginlowfi .designcomponentinstancenode { - left: 10px !important; - width: 105px !important; -} - -.loginlowfi .topnavbar2 { - width: 100% !important; -} - -.loginlowfi .topnavbar3 { - width: 100% !important; -} - -@media (max-width: 800px) { - .loginlowfi .signintitle { - height: 154px; - margin-left: 6%; - position: absolute; - top: 98px; - width: 80%; - } - .loginlowfi .textwrapper10 { - color: #ffffff; - font-family: 'SF Pro Display-Medium', Helvetica; - font-size: 30px; - font-weight: 500; - left: 0; - letter-spacing: 0; - line-height: normal; - top: 0; - white-space: nowrap; - } - - .loginlowfi .textwrapper11 { - color: #ffffff; - font-family: 'SF Pro Display-Bold', Helvetica; - font-size: 52px; - font-weight: 700; - left: 0; - letter-spacing: 0; - line-height: normal; - } - - .loginlowfi .p { - color: #ffffff; - font-family: 'SF Pro Display-Regular', Helvetica; - font-size: 18px; - font-weight: 400; - letter-spacing: 0; - line-height: normal; - width: 100%; /* Set width to be responsive */ - white-space: normal; /* Ensure text can wrap */ - overflow-wrap: break-word; /* Ensure long words are broken if needed */ - word-break: break-word; /* Ensure words are broken and wrapped properly */ - position: relative; - } - - .loginlowfi .textwrapper8 { - color: #ffffff; - font-family: 'SF Pro Display-Regular', Helvetica; - font-size: 20px; - font-weight: 400; - letter-spacing: 0; - } - .img { - display:none - } - - .img2 { - display:none - - } - .cardholder{ - flex-direction: column; - align-items: center; - gap:20px; - } -} -@media (max-width: 500px) { - -.loginlowfi .signintitle { - height: 154px; - margin-left: 6%; - position: absolute; - top: 98px; - width: 80%; -} -.loginlowfi .textwrapper10 { - color: #ffffff; - font-family: 'SF Pro Display-Medium', Helvetica; - font-size: 30px; - font-weight: 500; - left: 0; - letter-spacing: 0; - line-height: normal; - top: 0; - white-space: nowrap; -} - -.loginlowfi .textwrapper11 { - color: #ffffff; - font-family: 'SF Pro Display-Bold', Helvetica; - font-size: 45px; - font-weight: 700; - left: 0; - letter-spacing: 0; - line-height: normal; -} - -.loginlowfi .p { - color: #ffffff; - font-family: 'SF Pro Display-Regular', Helvetica; - font-size: 10px; - font-weight: 400; - letter-spacing: 0; - line-height: normal; - width: 100%; /* Set width to be responsive */ - white-space: normal; /* Ensure text can wrap */ - overflow-wrap: break-word; /* Ensure long words are broken if needed */ - word-break: break-word; /* Ensure words are broken and wrapped properly */ - position: relative; -} - -.loginlowfi .textwrapper8 { - color: #ffffff; -font-family: 'SF Pro Display-Regular', Helvetica; -font-size: 14px; -font-weight: 400; -letter-spacing: 0; -} -.img { - display:none -} - -.img2 { - display:none - -} -.loginlowfi .colorblockframe { - - - height: 45%; -} -.loginlowfi .signinto { - height: 80px; - width: 553px; -} -.sectionaboutus { - margin-top: 380px; -} -} - diff --git a/src/oldPages/about/uf admin 1.png b/src/oldPages/about/uf admin 1.png deleted file mode 100644 index 238a66d..0000000 Binary files a/src/oldPages/about/uf admin 1.png and /dev/null differ diff --git a/src/oldPages/about/uf faculty 1.png b/src/oldPages/about/uf faculty 1.png deleted file mode 100644 index 0d61afa..0000000 Binary files a/src/oldPages/about/uf faculty 1.png and /dev/null differ diff --git a/src/oldPages/about/ufstudents.png b/src/oldPages/about/ufstudents.png deleted file mode 100644 index 044b8f7..0000000 Binary files a/src/oldPages/about/ufstudents.png and /dev/null differ diff --git a/src/oldPages/application/[className]/page.tsx b/src/oldPages/application/[className]/page.tsx deleted file mode 100644 index eac16d6..0000000 --- a/src/oldPages/application/[className]/page.tsx +++ /dev/null @@ -1,384 +0,0 @@ -'use client'; -import { FC } from 'react'; -import React, { useEffect, useState } from 'react'; -import { Toaster } from 'react-hot-toast'; -import HeaderCard from '@/component/HeaderCard/HeaderCard'; -import './style.css'; -import ApplicantCardApprovedeny from '@/component/ApplicantCardApprovedeny/ApplicantCardApprovedeny'; -import firebase from '@/firebase/firebase_config'; -import 'firebase/firestore'; -import Spinner from '@/component/Spinner/Spinner'; -import ApplicantCardAssign from '@/component/ApplicantCardAssign/ApplicantCardAssign'; -import ApplicantCardApprove from '@/component/ApplicantCardApprove/ApplicantCardApprove'; -import ApplicantCardDeny from '@/component/ApplicantCardDeny/ApplicantCardDeny'; -import { getAuth, onAuthStateChanged } from 'firebase/auth'; -import { useUserRole } from '@/firebase/util/GetUserRole'; -import { useSearchParams } from 'next/navigation'; - -interface pageProps { - params: { className: string; semester: string }; -} -interface QueryParams { - [key: string]: string; -} -interface applicationData { - id: string; - uf_email: string; - firstname: string; - lastname: string; - number: string; - position: string; - semester: string; - availability: string; - department: string; - degree: string; - collegestatus: string; - qualifications: string; - resume: string; - plan: string; - gpa: string; -} - -// const [selectedItem, setSelectedItem] = useState('needsReview'); - -// const NeedsReviewApplicants = () => { -// const [applicants, setApplicants] = useState([]); - -// useEffect(() => { -// const fetchApplicants = async () => { -// const needsReviewApplicants = await getNeedsReviewApplicants(); - -// }; - -// fetchApplicants(); -// }, []); -// } -// const handleNavBarItemClick = (item) => { -// setSelectedItem(item); -// }; - -const CoursePage: FC = ({ params }) => { - const db = firebase.firestore(); - const auth = getAuth(); - const user = auth.currentUser; - const { - role, - loading: roleLoading, - error: roleError, - } = useUserRole(user?.uid); - - const [openApproveDialog, setOpenApproveDialog] = useState(false); - const [openDenyDialog, setOpenDenyDialog] = useState(false); - const [openReviewDialog, setOpenReviewDialog] = useState(false); - const [openRenewDialog, setOpenRenewDialog] = useState(false); - const [currentStu, setCurrentStu] = useState('null'); - const [className, setClassName] = useState('none'); - const [loading, setLoading] = useState(true); - - const [expandedStates, setExpandedStates] = useState<{ - [id: string]: boolean; - }>({}); - - const handleExpandToggle = (id: string) => { - setExpandedStates((prevExpandedStates) => ({ - ...prevExpandedStates, - [id]: !prevExpandedStates[id], - })); - }; - - const [taData, setTaData] = useState([]); - const [upiData, setupiData] = useState([]); - const [graderData, setgraderData] = useState([]); - const [selection, setSelection] = useState('Review'); - const searchParams = useSearchParams(); - const courseTitle = searchParams.get('courseTitle'); - - const toggleSelection = (select: string): void => { - setSelection(select); - setExpandedStates({}); - }; - - const getDataByPositionAndStatus = async ( - position: string, - status: string - ) => { - try { - const snapshot = await db - .collection('applications') - .where(`courses.${className}`, '>=', '') - .orderBy(`courses.${className}`) - - // .where('semesters', 'array-contains', params.semester ) - .get(); - - const snapshot2 = await db - .collection('assignments') - .where('class_codes', '==', className) - .where('position', '==', position) - .get(); - - if (selection == 'Assigned') { - return snapshot2.docs.map((doc) => ({ - id: doc.id, - uf_email: doc.data().email, - firstname: doc.data().name, - lastname: ' ', - number: '', - position: doc.data().position, - semester: params.semester, - availability: doc.data().hours, - department: doc.data().department, - degree: '', - collegestatus: '', - qualifications: '', - resume: '', - plan: '', - gpa: '', - })); - } - return snapshot.docs - .filter(function (doc) { - if (doc.data().position != position) { - return false; - } - if (doc.data().status == 'Admin_approved') { - return false; - } - if (doc.data().status == 'Admin_denied' && selection != 'Denied') { - return false; - } - if ( - doc.data().courses[className] == 'applied' && - selection == 'Review' - ) { - return true; - } else if ( - doc.data().courses[className] == 'accepted' && - selection == 'Approved' - ) { - return true; - } else if ( - doc.data().courses[className] == 'denied' && - selection == 'Denied' - ) { - return true; - } else { - return false; - } - }) - .map((doc) => ({ - id: doc.id, - uf_email: doc.data().email, - firstname: doc.data().firstname, - lastname: doc.data().lastname, - number: doc.data().phonenumber, - position: doc.data().position, - semester: params.semester, - availability: doc.data().available_hours, - department: doc.data().department, - degree: doc.data().degree, - collegestatus: doc.data().semesterstatus, - qualifications: doc.data().qualifications, - resume: doc.data().resume_link, - plan: doc.data().grad_plans, - gpa: doc.data().gpa, - })); - } catch (error) { - console.error(`Error getting ${className} applicants: `, error); - return []; - } - }; - - useEffect(() => { - const fetchData = async () => { - try { - setLoading(true); - const result = await getDataByPositionAndStatus('TA', selection); - const result2 = await getDataByPositionAndStatus('UPI', selection); - const result3 = await getDataByPositionAndStatus('Grader', selection); - setTaData(result); - setupiData(result2); - setgraderData(result3); - } catch (error) { - console.error('Error fetching data: ', error); - } finally { - setLoading(false); - } - }; - - fetchData(); - - const getQueryParams = (query: string): QueryParams => { - return query - ? (/^[?#]/.test(query) ? query.slice(1) : query) - .split('&') - .reduce((params: QueryParams, param) => { - let [key, value] = param.split('='); - params[key] = value - ? decodeURIComponent(value.replace(/\+/g, ' ')) - : ''; - return params; - }, {}) - : {}; - }; - - const params = getQueryParams(window.location.search); - const data = params.data; - - setClassName(data); - }, [selection, className]); - - const mapElement = ( - data: { - id: string; - uf_email: any; - firstname: any; - lastname: any; - number: string; - position: string; - semester: string; - availability: string; - department: string; - degree: string; - collegestatus: string; - qualifications: string; - resume: string; - plan: string; - gpa: string; - }[] - ) => { - return data.map((ta) => { - const commonProps = { - id: ta.id, - number: ta.number, - position: ta.position, - semester: ta.semester, - availability: ta.availability, - department: ta.department, - degree: ta.degree, - collegestatus: ta.collegestatus, - qualifications: ta.qualifications, - expanded: expandedStates[ta.id] || false, - onExpandToggle: () => handleExpandToggle(ta.id), - uf_email: ta.uf_email, - firstname: ta.firstname, - lastname: ta.lastname, - resume: ta.resume, - plan: ta.plan, - gpa: ta.gpa, - currentStu: currentStu, - setCurrentStu: setCurrentStu, - className: className, - }; - return ( -
- {selection === 'Review' && ( - - )} - - {selection === 'Approved' && ( - - )} - - {selection === 'Assigned' && ( - - )} - - {selection === 'Denied' && ( - - )} -
- ); - }); - }; - - if (roleError) { - return

Error loading role

; - } - - if (!user) { - return

Please sign in.

; - } - - if (role !== 'faculty' && role !== 'admin') { - return

Loading.

; - } - - return ( - <> - - -
-
- {className.substring(0, className.indexOf(' ')) + - ': ' + - courseTitle + - className.substring( - className.indexOf(' '), - className.indexOf(')') + 1 - )} -
-
-
- {/* */} -
- - {loading && } - - {taData.length != 0 && ( -
- TA -
- )} - {mapElement(taData)} - - {upiData.length != 0 && ( -
- UPI -
- )} - {mapElement(upiData)} - - {graderData.length != 0 && ( -
- Grader -
- )} - {mapElement(graderData)} - - ); -}; - -export default CoursePage; diff --git a/src/oldPages/application/[className]/style.css b/src/oldPages/application/[className]/style.css deleted file mode 100644 index f4c92c1..0000000 --- a/src/oldPages/application/[className]/style.css +++ /dev/null @@ -1,24 +0,0 @@ -@font-face { - font-family: "SF Pro Display-Bold"; - src: url("../../../../fonts/SFPRODISPLAYBOLD.OTF"); -} - -.classe { - font-size: 32px; - font-family: 'SF Pro Display-Bold', Helvetica; - color: #000; - margin-left: 40px; -} -.semester { - font-size: 28px; - font-family: 'SF Pro Display', Helvetica; - color: #000; - margin-right: 29px; -} -.TAtext { - position: relative; - font-size: 36px; - font-family: 'SF Pro Display-Bold', Helvetica; - color: #000; - text-align: left; -} diff --git a/src/oldPages/application/page.tsx b/src/oldPages/application/page.tsx deleted file mode 100644 index 433975e..0000000 --- a/src/oldPages/application/page.tsx +++ /dev/null @@ -1,150 +0,0 @@ -'use client'; -import './style.css'; -import React, { useEffect, useState } from 'react'; -import { Toaster } from 'react-hot-toast'; -import SemesterSelect from './semesterselect'; -import ClassCard from '@/component/ClassCard/ClassCard'; -import HeaderCard from '@/component/HeaderCard/HeaderCard'; -import firebase from '@/firebase/firebase_config'; -import 'firebase/firestore'; -import { getAuth, onAuthStateChanged } from 'firebase/auth'; - -export default function FacultyApplication() { - const auth = getAuth(); - const currentYear = new Date().getFullYear(); - const currentMonth = new Date().getMonth(); - const currentSemester = - currentMonth < 5 ? 'Spring' : currentMonth < 8 ? 'Summer' : 'Fall'; - const [semester, setSemester] = useState(`${currentSemester} ${currentYear}`); - const [courses, setCourses] = useState<[string, any, string][]>([]); - const db = firebase.firestore(); - - const generateSemesterNames = ( - currentSem: string, - currentYr: number - ): string[] => { - const names = [`${currentSemester} ${currentYear}`]; - let semesters = currentSem; - let years = currentYr; - - for (let i = 0; i < 2; i++) { - if (semesters === 'Spring') { - semesters = 'Summer'; - } else if (semesters === 'Summer') { - semesters = 'Fall'; - } else { - semesters = 'Spring'; - years = years + 1; - } - names.push(`${semesters} ${years}`); - } - return names; - }; - const [semesterNames, setSemesterNames] = useState([]); - - useEffect(() => { - setSemesterNames(generateSemesterNames(currentSemester, currentYear)); - }, []); - - const user = auth.currentUser; - const uemail = user?.email; - - const getCourses = async ( - semester: string - ): Promise<[string, any, string][]> => { - try { - const snapshot = await db - .collection(`courses`) - .where('semester', '==', semester) - .where('professor_emails', 'array-contains', uemail) // Check if the current user is the instructor - .get(); - - const filteredDocs = snapshot.docs.filter( - (doc) => - doc.data().code !== null && - doc.data().code !== undefined && - doc.data().title !== undefined - ); - - return filteredDocs.map((doc) => [ - doc.id, - doc.data().code, - doc.data().title, - ]); - } catch (error) { - console.error(`Error getting courses:`, error); - alert('Error getting courses:'); - return []; - } - }; - - useEffect(() => { - const fetchData = async () => { - try { - const result = await getCourses(semester); - setCourses(result); - } catch (error) { - console.error('Error fetching data: ', error); - } - }; - - fetchData(); - }, [semester]); - - const mapElement = () => { - return courses.map((val) => { - return ( -
- -
- ); - }); - }; - return ( - <> - - -
-
TA/UPI/Grader Applications
-
-
- Applications sorted by course: -
-
- -
-
-
- {courses.length !== 0 && ( -
{mapElement()}
- )} - {courses.length === 0 && ( -
- Currently, no courses have been assigned to you yet. Please wait until - an admin assigns your courses. Once your courses are assigned, - you'll be able to access applicants for those classes. -
- )} - - ); -} diff --git a/src/oldPages/application/semesterselect.tsx b/src/oldPages/application/semesterselect.tsx deleted file mode 100644 index b0dc2e0..0000000 --- a/src/oldPages/application/semesterselect.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import * as React from 'react'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import MenuItem from '@mui/material/MenuItem'; -import FormControl from '@mui/material/FormControl'; -import Select, { SelectChangeEvent } from '@mui/material/Select'; - -const ITEM_HEIGHT = 48; -const ITEM_PADDING_TOP = 8; -const MenuProps = { - PaperProps: { - style: { - maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, - width: 250, - borderColor: '#000000', // Add border color - border: '1px solid #000000', - borderRadius: '20px', - }, - }, -}; - -interface PageProps { - semester: string; - setSemester: (value: string) => void; - names: string[]; -} - -const SemesterSelect: React.FunctionComponent = ({ - semester, - setSemester, - names, -}) => { - const [personName, setPersonName] = React.useState(''); - const handleChange = (event: SelectChangeEvent) => { - setPersonName(event.target.value as string); - }; - - return ( -
- - - -
- ); -}; - -export default SemesterSelect; diff --git a/src/oldPages/application/style.css b/src/oldPages/application/style.css deleted file mode 100644 index 8609346..0000000 --- a/src/oldPages/application/style.css +++ /dev/null @@ -1,39 +0,0 @@ -.text-wrapper-11 { - position: relative; - font-size: 36px; - font-weight: 500; - font-family: 'SF Pro Display-Medium', Helvetica; - color: #000; - text-align: left; - -} - - - .courses{ - font-size: 36px; - padding-top: 34px; -} - -.ta{ - font-size: 40px; -} - -.page-container { - display: flex; - flex-direction: column; - padding-left: 27px; - padding-bottom: 90px; -} - - -.class-cards-container { - display: flex; - justify-content: center; - align-items: center; - height: 100%; - - -} -.class{ - margin:22px -} \ No newline at end of file diff --git a/src/oldPages/dashboard/page.tsx b/src/oldPages/dashboard/page.tsx deleted file mode 100644 index cec1bc4..0000000 --- a/src/oldPages/dashboard/page.tsx +++ /dev/null @@ -1,39 +0,0 @@ -'use client'; -import React from 'react'; -import { useAuth } from '@/firebase/auth/auth_context'; -import GetUserRole from '@/firebase/util/GetUserRole'; -import BottomMenu from '@/component/BottomMenu/BottomMenu'; - -// dashboard components -import DashboardWelcome from '@/component/Dashboard/Welcome/Welcome'; -import Profile from '@/component/Dashboard/Profile/Profile'; -import Users from '@/component/Dashboard/Users/Users'; -import Courses from '@/component/Dashboard/Courses/Courses'; -import Applications from '@/component/Dashboard/Applications/Applications'; -import Application from '@/component/Dashboard/Applications/Application'; -import ShowApplicationStatus from '@/component/Dashboard/Applications/AppStatus'; -import { Toaster } from 'react-hot-toast'; -import { TopNavBarSigned } from '@/component/TopNavBarSigned/TopNavBarSigned'; - -// user information reference: https://firebase.google.com/docs/auth/web/manage-users - -export default function Dashboard() { - const { user } = useAuth(); - const [role, loading, error] = GetUserRole(user?.uid); - const [activeComponent, setActiveComponent] = React.useState('welcome'); - - const handleComponentChange = (componentName: string) => { - setActiveComponent(componentName); - }; - - return ( - <> - - - - ); -} diff --git a/src/oldPages/signup/page.tsx b/src/oldPages/signup/page.tsx deleted file mode 100644 index 8a0c35c..0000000 --- a/src/oldPages/signup/page.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import toast, { Toaster } from 'react-hot-toast'; -import { EceLogoPng } from '@/component/EceLogoPng/EceLogoPng'; - -import { TopNavBar } from '@/component/TopNavBar/TopNavBar'; -import './style.css'; -import Link from 'next/link'; -import { SignUpCard } from '@/component/SignUpCard/SignUpCard'; -export default function SignUpPage() { - return ( - <> - -
-
-
- Color block frame -
-

- - Connecting Bright Minds for a Brighter Future -
-
- -
-
-

-
-
Sign in to
-
Course Connect
-
-
-
-

Already have an account?

-

- You can - - {' '} - {'Login here!'}{' '} - -

-
- - -
- - -
-
- - ); -} - -export const metadata = { - title: 'Course Connect', - description: 'Hiring management for students, faculty, and administrators.', -}; diff --git a/src/oldPages/signup/style.css b/src/oldPages/signup/style.css deleted file mode 100644 index 0101475..0000000 --- a/src/oldPages/signup/style.css +++ /dev/null @@ -1,248 +0,0 @@ -.login-low-fi { - background-color: #ffffff; - display: flex; - flex-direction: row; - justify-content: center; - overflow-x: hidden; -} - -.login-low-fi .log-in-card-instance { - height: 799px; - position: absolute; - width: 543px; - left: 55% !important; - position: absolute !important; - top: 150px !important; -} - - -.login-low-fi .div-2 { - background-color: #ffffff; - height: 100%; - position: absolute; - width: 100%; - overflow-x: hidden; -} - -.login-low-fi .overlap-2 { - height: 777px; - left: 0; - position: absolute; - top: 0; - width: 100%; -} - -.login-low-fi .color-block-frame { - height: 458px; - left: 0; - position: relative; - top: 0; - width: 100%; -} - -.login-low-fi .sign-in-title { - height: 154px; - left: 73px; - position: absolute; - top: 141px; - width: 100%; -} - -.login-low-fi .connecting-bright { - color: #ffffff; - font-family: "SF Pro Display-Regular", Helvetica; - font-size: 20px; - font-weight: 400; - left: 7px; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 124px; - width: 100%; -} - -.login-low-fi .text-wrapper-8 { - color: #ffffff; - font-family: "SF Pro Display-Regular", Helvetica; - font-size: 20px; - font-weight: 400; - letter-spacing: 0; -} - -.login-low-fi .text-wrapper-9 { - font-family: "Poppins", Helvetica; - font-weight: 300; -} - -.login-low-fi .sign-in-to { - height: 120px; - left: 0; - position: absolute; - top: 0; - width: 553px; -} - -.login-low-fi .text-wrapper-10 { - color: #ffffff; - font-family: "SF Pro Display-Medium", Helvetica; - font-size: 40px; - font-weight: 500; - left: 0; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 0; - white-space: nowrap; -} - -.login-low-fi .text-wrapper-11 { - color: #ffffff; - font-family: "SF Pro Display-Bold", Helvetica; - font-size: 60px; - font-weight: 700; - left: 0; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 48px; - width: 549px; -} - -.login-low-fi .register-here-text { - height: 54px; - left: 80px; - position: absolute; - top: 351px; - width: 312px; - -} - -.login-low-fi .p { - color: #ffffff; - font-family: "SF Pro Display-Regular", Helvetica; - font-size: 20px; - font-weight: 400; - left: 0; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 0; - width: 308px; -} - -.login-low-fi .you-can-register { - color: #ffffff; - font-family: "SF Pro Display-Regular", Helvetica; - font-size: 20px; - font-weight: 400; - left: 0; - letter-spacing: 0; - line-height: normal; - position: absolute; - top: 30px; - width: 308px; -} - -.login-low-fi .text-wrapper-12 { - font-family: "SF Pro Display-Bold", Helvetica; - font-weight: 300; - text-decoration: underline; - color: white; - -} - -.login-low-fi .line { - height: 1px; - left: 150px; - object-fit: cover; - position: absolute; - top: 402px; - width: 126px; -} - - -.login-low-fi .overlap-wrapper { - all: unset; - box-sizing: border-box; - height: 37px; - left: 1265px; - position: absolute; - top: 27px; - width: 137px; -} - -.login-low-fi .overlap-3 { - background-color: #ffffff; - border-radius: 10px; - height: 37px; - position: relative; - width: 135px; -} - -.login-low-fi .text-wrapper-13 { - color: #000000; - font-family: "SF Pro Display-Bold", Helvetica; - font-size: 18px; - font-weight: 700; - left: 11px; - letter-spacing: 0; - line-height: normal; - position: absolute; - text-align: center; - top: 9px; - white-space: nowrap; - width: 114px; -} - -.login-low-fi .ece-logo-png-2 { - left: 27px !important; - position: absolute !important; - top: 23px !important; -} - -.login-low-fi .top-nav-bar-instance { - right: 70px !important; - position: absolute !important; - top: 27px !important; -} - -.login-low-fi .design-component-instance-node { - left: 10px !important; - width: 105px !important; -} - -.login-low-fi .top-nav-bar-2 { - width: 100% !important; -} - -.login-low-fi .top-nav-bar-3 { - width: 100% !important; -} - -@media (max-width: 570px) { - .login-low-fi .log-in-card-instance { - position: relative !important; - left: 0 !important; - top: 0 !important; - margin: 20px auto; - } - .login-low-fi .connecting-bright{ - display:none; - } - .ece-logo-png-2 { - display: none; - } - .login-low-fi .sign-in-title{ - left:40px; - } - .login-low-fi .register-here-text{ - left: 40px; - } - .login-low-fi .top-nav-bar-instance{ - right: 50px; - } - .login-low-fi .sign-in-title .text-wrapper-10, - .login-low-fi .sign-in-title .text-wrapper-11{ - width: 80%; - } -} diff --git a/src/services/firebase.ts b/src/services/firebase.ts new file mode 100644 index 0000000..da019c5 --- /dev/null +++ b/src/services/firebase.ts @@ -0,0 +1,13 @@ +/** + * Firebase service singleton + * Provides centralized access to Firebase services + */ +import firebase from '@/firebase/firebase_config'; + +// Export singleton instances +export const db = firebase.firestore(); +export const storage = firebase.storage(); +export const auth = firebase.auth(); + +// Export firebase instance for backward compatibility +export default firebase; diff --git a/src/services/researchService.ts b/src/services/researchService.ts new file mode 100644 index 0000000..ea23eea --- /dev/null +++ b/src/services/researchService.ts @@ -0,0 +1,189 @@ +/** + * Research service for Firebase operations + * Centralized CRUD operations for research listings and applications + */ +import { db } from './firebase'; +import { + ResearchListing, + normalizeResearchListing, +} from '@/app/models/ResearchModel'; + +export interface ResearchFilters { + department?: string; + studentLevel?: string; + termsAvailable?: string; +} + +/** + * Fetch research listings with optional filters + * Uses optimized query pattern to avoid N+1 problem + */ +export async function fetchResearchListings( + filters: ResearchFilters = {} +): Promise { + try { + // Build query with filters + let query = db.collection('research-listings'); + + if (filters.department) { + query = query.where('department', '==', filters.department) as any; + } + + if (filters.studentLevel) { + query = query.where('student_level', '==', filters.studentLevel) as any; + } + + // Fetch listings + const snapshot = await query.get(); + const listingIds = snapshot.docs.map((doc) => doc.id); + + // Fetch all applications in a single query using collectionGroup + const applicationsMap = new Map(); + + if (listingIds.length > 0) { + const allApplicationsSnap = await db + .collectionGroup('applications') + .get(); + + // Group applications by their parent listing ID + allApplicationsSnap.docs.forEach((appDoc) => { + const parentId = appDoc.ref.parent.parent?.id; + if (parentId && listingIds.includes(parentId)) { + if (!applicationsMap.has(parentId)) { + applicationsMap.set(parentId, []); + } + applicationsMap.get(parentId)!.push({ + id: appDoc.id, + ...appDoc.data(), + }); + } + }); + } + + // Map listings with their applications + const listings: ResearchListing[] = snapshot.docs.map((doc) => { + const apps = applicationsMap.get(doc.id) || []; + return normalizeResearchListing({ + docID: doc.id, + applications: apps, + ...doc.data(), + }); + }); + + return listings; + } catch (error) { + console.error('Error fetching research listings:', error); + throw new Error('Failed to fetch research listings'); + } +} + +/** + * Create a new research listing + */ +export async function createResearchListing( + formData: Partial +): Promise { + try { + const docRef = await db.collection('research-listings').add(formData); + return docRef.id; + } catch (error) { + console.error('Error creating research listing:', error); + throw new Error('Failed to create research listing'); + } +} + +/** + * Update an existing research listing + */ +export async function updateResearchListing( + listingId: string, + updates: Partial +): Promise { + try { + await db.collection('research-listings').doc(listingId).update(updates); + } catch (error) { + console.error('Error updating research listing:', error); + throw new Error('Failed to update research listing'); + } +} + +/** + * Delete a research listing + */ +export async function deleteResearchListing(listingId: string): Promise { + try { + // Delete the listing document + await db.collection('research-listings').doc(listingId).delete(); + + // Note: Applications subcollection will need to be deleted separately + // if you want cascade delete behavior + } catch (error) { + console.error('Error deleting research listing:', error); + throw new Error('Failed to delete research listing'); + } +} + +/** + * Fetch applications for a specific listing + */ +export async function fetchApplicationsForListing( + listingId: string +): Promise { + try { + const snapshot = await db + .collection('research-listings') + .doc(listingId) + .collection('applications') + .get(); + + return snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })); + } catch (error) { + console.error('Error fetching applications:', error); + throw new Error('Failed to fetch applications'); + } +} + +/** + * Submit an application to a research listing + */ +export async function submitApplication( + listingId: string, + applicationData: any +): Promise { + try { + const docRef = await db + .collection('research-listings') + .doc(listingId) + .collection('applications') + .add(applicationData); + + return docRef.id; + } catch (error) { + console.error('Error submitting application:', error); + throw new Error('Failed to submit application'); + } +} + +/** + * Update application status + */ +export async function updateApplicationStatus( + listingId: string, + applicationId: string, + status: 'Pending' | 'Approved' | 'Denied' +): Promise { + try { + await db + .collection('research-listings') + .doc(listingId) + .collection('applications') + .doc(applicationId) + .update({ app_status: status }); + } catch (error) { + console.error('Error updating application status:', error); + throw new Error('Failed to update application status'); + } +} diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css index b784596..f10a7d0 100644 --- a/src/styles/tailwind.css +++ b/src/styles/tailwind.css @@ -2,6 +2,10 @@ /* Tailwind v4 entry */ @import "tailwindcss"; +/* Explicit content sources for class detection */ +@source "../../src/**/*.{js,ts,jsx,tsx}"; +@source "../../components/**/*.{js,ts,jsx,tsx}"; + /* ---- Design tokens (Tailwind v4) --------------------------------------- */ @theme { /* ---------- Colors ---------- */ diff --git a/src/types/research.ts b/src/types/research.ts new file mode 100644 index 0000000..10474bb --- /dev/null +++ b/src/types/research.ts @@ -0,0 +1,126 @@ +/** + * TypeScript interfaces for Research feature + */ + +/** + * Research application submitted by students + */ +export interface ResearchApplication { + id: string; + student_id: string; + student_name?: string; + student_email?: string; + app_status: 'Pending' | 'Approved' | 'Denied'; + gpa?: string; + major?: string; + year?: string; + why_interested?: string; + relevant_experience?: string; + resume_url?: string; + transcript_url?: string; + created_at?: string | Date; + updated_at?: string | Date; +} + +/** + * Filter state for research listings + */ +export interface ResearchFilters { + department?: string; + studentLevel?: string; + termsAvailable?: string; + searchText?: string; +} + +/** + * Form data for creating/editing research listings + */ +export interface ResearchFormData { + project_title: string; + project_description: string; + department: string; + image_url: string; + nature_of_job: string; + compensation: string; + faculty_contact: string; + phd_student_contact: string; + application_deadline: string; + hours_per_week: string; + prerequisites: string; + terms_available: string; + student_level: string; + application_requirements: string; + website: string; +} + +/** + * Props for StudentResearchView component + */ +export interface StudentResearchViewProps { + researchListings: import('@/app/models/ResearchModel').ResearchListing[]; + role: string; + uid: string; + department: string; + setDepartment: (dept: string) => void; + studentLevel: string; + setStudentLevel: (level: string) => void; + getResearchListings: () => void; + setResearchListings: ( + listings: import('@/app/models/ResearchModel').ResearchListing[] + ) => void; + termsAvailable: string; + setTermsAvailable: (terms: string) => void; +} + +/** + * Props for FacultyResearchView component + */ +export interface FacultyResearchViewProps { + researchListings: import('@/app/models/ResearchModel').ResearchListing[]; + role: string; + uid: string; + getResearchListings: () => void; + postNewResearchPosition: (formData: ResearchFormData) => Promise; +} + +/** + * Props for ProjectCard component + */ +export interface ProjectCardProps { + listing: import('@/app/models/ResearchModel').ResearchListing; + onEdit?: ( + listing: import('@/app/models/ResearchModel').ResearchListing + ) => void; + onShowApplications?: ( + listing: import('@/app/models/ResearchModel').ResearchListing + ) => void; + onDelete?: (listingId: string) => void; +} + +/** + * Props for ApplicationCard component + */ +export interface ApplicationCardProps { + listing: import('@/app/models/ResearchModel').ResearchListing; + uid: string; + onApplySuccess?: () => void; +} + +/** + * Props for Modal components + */ +export interface ResearchModalProps { + open: boolean; + onClose: () => void; + onSubmitSuccess: () => void; + firebaseQuery: (formData: ResearchFormData) => Promise; + uid: string; +} + +export interface EditResearchModalProps { + open: boolean; + onClose: () => void; + onSubmitSuccess: () => void; + listingData: import('@/app/models/ResearchModel').ResearchListing; + uid: string; +} diff --git a/src/utils/researchFilters.ts b/src/utils/researchFilters.ts new file mode 100644 index 0000000..3ec4a9b --- /dev/null +++ b/src/utils/researchFilters.ts @@ -0,0 +1,136 @@ +/** + * Filter utilities for Research listings + * Extracted from StudentResearchView for reusability and testing + */ +import { ResearchListing } from '@/app/models/ResearchModel'; +import { isDepartmentMatch } from '@/constants/research'; + +/** + * Filter research listings by search text + * Searches in project title, description, and faculty contact + */ +export function filterBySearchText( + listings: ResearchListing[], + searchText: string +): ResearchListing[] { + if (!searchText.trim()) return listings; + + const searchLower = searchText.toLowerCase().trim(); + + return listings.filter((listing) => { + const title = (listing.project_title || '').toLowerCase(); + const description = (listing.project_description || '').toLowerCase(); + const faculty = (listing.faculty_contact || '').toLowerCase(); + + return ( + title.includes(searchLower) || + description.includes(searchLower) || + faculty.includes(searchLower) + ); + }); +} + +/** + * Filter research listings by department + * Handles department name variations + */ +export function filterByDepartment( + listings: ResearchListing[], + department: string +): ResearchListing[] { + if (!department.trim()) return listings; + + return listings.filter((listing) => { + return isDepartmentMatch(listing.department, department); + }); +} + +/** + * Filter research listings by terms available + */ +export function filterByTerms( + listings: ResearchListing[], + terms: string +): ResearchListing[] { + if (!terms.trim()) return listings; + + const termsLower = terms.toLowerCase().trim(); + + return listings.filter((listing) => { + const listingTerms = (listing.terms_available || '').toLowerCase(); + return listingTerms.includes(termsLower); + }); +} + +/** + * Filter research listings by student level + */ +export function filterByStudentLevel( + listings: ResearchListing[], + studentLevel: string +): ResearchListing[] { + if (!studentLevel.trim()) return listings; + + const levelLower = studentLevel.toLowerCase().trim(); + + return listings.filter((listing) => { + const listingLevel = (listing.student_level || '').toLowerCase(); + return listingLevel.includes(levelLower); + }); +} + +/** + * Apply all filters to research listings + * This is the main entry point for filtering + */ +export function applyAllFilters( + listings: ResearchListing[], + filters: { + searchText?: string; + department?: string; + terms?: string; + studentLevel?: string; + } +): ResearchListing[] { + let filtered = listings; + + if (filters.searchText) { + filtered = filterBySearchText(filtered, filters.searchText); + } + + if (filters.department) { + filtered = filterByDepartment(filtered, filters.department); + } + + if (filters.terms) { + filtered = filterByTerms(filtered, filters.terms); + } + + if (filters.studentLevel) { + filtered = filterByStudentLevel(filtered, filters.studentLevel); + } + + return filtered; +} + +/** + * Sort research listings by application deadline (earliest first) + */ +export function sortByDeadline(listings: ResearchListing[]): ResearchListing[] { + return [...listings].sort((a, b) => { + const dateA = new Date(a.application_deadline).getTime(); + const dateB = new Date(b.application_deadline).getTime(); + return dateA - dateB; + }); +} + +/** + * Sort research listings by creation date (newest first) + */ +export function sortByNewest(listings: ResearchListing[]): ResearchListing[] { + return [...listings].sort((a, b) => { + // Assuming docID contains timestamp info or we have a created_at field + // For now, just maintain original order + return 0; + }); +}