diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e587e28..b478991 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,6 +1,9 @@ { "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/functions/src/index.ts b/functions/src/index.ts index 7bbb291..b25ae7b 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -24,7 +24,7 @@ import { const admin = require('firebase-admin'); admin.initializeApp(); -const db = admin.firestore(); +export const db = admin.firestore(); const auth = admin.auth(); db.settings({ ignoreUndefinedProperties: true }); diff --git a/playwright-report/index.html b/playwright-report/index.html new file mode 100644 index 0000000..c53ecff --- /dev/null +++ b/playwright-report/index.html @@ -0,0 +1,85 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/playwright/.auth/admin.json b/playwright/.auth/admin.json new file mode 100644 index 0000000..1e3f74b --- /dev/null +++ b/playwright/.auth/admin.json @@ -0,0 +1,25 @@ +{ + "cookies": [ + { + "name": "__next_hmr_refresh_hash__", + "value": "831e14094f0632de387bc1e46cf07f9b633457081244afec", + "domain": "localhost", + "path": "/", + "expires": -1, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + } + ], + "origins": [ + { + "origin": "http://localhost:3000", + "localStorage": [ + { + "name": "theme", + "value": "light" + } + ] + } + ] +} \ No newline at end of file diff --git a/playwright/.auth/faculty.json b/playwright/.auth/faculty.json new file mode 100644 index 0000000..509aecc --- /dev/null +++ b/playwright/.auth/faculty.json @@ -0,0 +1,14 @@ +{ + "cookies": [], + "origins": [ + { + "origin": "http://localhost:3000", + "localStorage": [ + { + "name": "theme", + "value": "light" + } + ] + } + ] +} \ No newline at end of file diff --git a/playwright/.auth/student.json b/playwright/.auth/student.json new file mode 100644 index 0000000..509aecc --- /dev/null +++ b/playwright/.auth/student.json @@ -0,0 +1,14 @@ +{ + "cookies": [], + "origins": [ + { + "origin": "http://localhost:3000", + "localStorage": [ + { + "name": "theme", + "value": "light" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/app/Research/ApplicationForm/page.tsx b/src/app/Research/ApplicationForm/page.tsx new file mode 100644 index 0000000..8a86df9 --- /dev/null +++ b/src/app/Research/ApplicationForm/page.tsx @@ -0,0 +1,215 @@ +'use client'; + +import React, { useState } from 'react'; +import { getAuth } from 'firebase/auth'; +import firebase from '@/firebase/firebase_config'; +import { + Box, + Button, + TextField, + Typography, + Grid, + Snackbar, + Alert, +} from '@mui/material'; + +const ApplicationFormPage = () => { + const auth = getAuth(); + const user = auth.currentUser; + + const [formData, setFormData] = useState({ + firstname: '', + lastname: '', + email: user?.email || '', + phone: '', + department: '', + degree: '', + gpa: '', + graduationDate: '', + resume: '', + qualifications: '', + weeklyHours: '', + availableSemesters: [], + }); + + const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(''); + + const handleChange = (e: any) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const handleSubmit = async () => { + if (!user) { + setError('You must be signed in to apply.'); + return; + } + + try { + await firebase + .firestore() + .collection('research-applications') + .add({ + ...formData, + uid: user.uid, + app_status: 'Pending', + date: new Date().toLocaleDateString(), + }); + + setSubmitted(true); + setFormData({ + firstname: '', + lastname: '', + email: user.email || '', + phone: '', + department: '', + degree: '', + gpa: '', + graduationDate: '', + resume: '', + qualifications: '', + weeklyHours: '', + availableSemesters: [], + }); + } catch (err) { + setError('Failed to submit application. Try again.'); + console.error(err); + } + }; + + return ( + + + Research Application Form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setSubmitted(false)} + > + + Application submitted successfully! + + + + setError('')} + > + + {error} + + + + ); +}; + +export default ApplicationFormPage; diff --git a/src/app/Research/page.tsx b/src/app/Research/page.tsx index 7ac4378..0f779b1 100644 --- a/src/app/Research/page.tsx +++ b/src/app/Research/page.tsx @@ -1,9 +1,13 @@ 'use client'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { Toaster } from 'react-hot-toast'; -import HeaderCard from '@/component/HeaderCard/HeaderCard'; -import { JobCard } from '@/component/JobCard/JobCard'; 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'; + interface ResearchPageProps { user: { uid: string; @@ -12,86 +16,204 @@ interface ResearchPageProps { }; } -const researchJobs = [ - { - title: - 'Intelligent Natural Interaction Technology (INIT) Lab Research Assistant', - department: 'Computer and Information Sciences and Engineering', - faculty: 'Engineering', - terms: ['Fall', 'Spring', 'Summer'], - level: ['Freshman', 'Sophomore', 'Junior', 'Senior'], - occupancy: 3, - description: - 'Research focusing on advanced interaction technologies such as touch, gesture, voice, and mixed reality, in the context of human-AI interaction, education, healthcare, and serious games. Current priorities include designing intelligent chatbots for mobile health monitoring apps, digital AI assistants for novice users, and human-centered interactive machine learning interfaces.', - mentor: 'Dr. Lisa Anthony', - prereq: - 'Programming fundamentals, experimental design, data analysis preferred. Experience with children, good people skills, attention to detail, organization, and time management helpful.', - credits: '0-3 via EGN 4912', - stipend: - 'First semester: none (unless University Scholars), After trial: $15/hour up to 10 hours/week', - requirements: 'Resume, UF unofficial transcripts, faculty interview', - deadline: - 'Rolling basis (Recommended: Mar 15/July 1 for Fall, Nov 15 for Spring, Mar 15 for Summer)', - website: 'http://init.cise.ufl.edu', - contact: 'lanthony@cise.ufl.edu', - }, - { - title: 'Modeling Dialogue for Supporting Learning Research Assistant', - department: 'Computer and Information Sciences and Engineering', - faculty: 'Engineering', - terms: ['Fall', 'Spring', 'Summer'], - level: ['Freshman', 'Sophomore', 'Junior', 'Senior'], - occupancy: 2, - description: - 'Research focused on understanding and modeling dialogue for learning, building computational models of dialogue to support students through intelligent learning environments.', - mentor: 'Dr. Kristy Boyer', - prereq: - 'Java I and Java II strongly preferred. Data Structures recommended. High-achieving freshmen encouraged to apply.', - credits: '0-3 via EGN 4912', - stipend: '$10 per hour, flexible hours', - requirements: - 'Resume, UF unofficial transcripts, faculty interview, cover letter', - deadline: 'Rolling basis', - website: 'https://www.cise.ufl.edu/research/learndialogue/', - contact: 'timothy.brown@ufl.edu', - }, -]; +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 [user, role, loading, error] = useUserInfo(); + const [department, setDepartment] = React.useState(''); + const [studentLevel, setStudentLevel] = React.useState(''); + const [termsAvailable, setTermsAvailable] = React.useState(''); + 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); + } + 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); + } catch (e) { + console.error('Error adding document: ', e); + } + }, []); + + useEffect(() => { + if (user) { + getResearchListings(); + getApplications(); + } + }, [user, getResearchListings, getApplications]); + if (error) { return

Error loading role

; } - if (loading) return

Loading…

; - if (!user) return
Forbidden
; + if (loading) { + return
Loading...
; + } + + if (!user) { + return

Please sign in.

; + } + return ( <> - - -
- {role === 'faculty'} - {researchJobs.map((job, index) => ( -
- -
- ))} -
+ + {(role === 'student_applying' || role === 'student_applied') && ( + + )} + {role === 'faculty' && ( + + )} + ); }; diff --git a/src/app/Research/testdata.js b/src/app/Research/testdata.js new file mode 100644 index 0000000..2421187 --- /dev/null +++ b/src/app/Research/testdata.js @@ -0,0 +1,67 @@ +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/api/v1/research/route.ts b/src/app/api/v1/research/route.ts new file mode 100644 index 0000000..7082c3f --- /dev/null +++ b/src/app/api/v1/research/route.ts @@ -0,0 +1,51 @@ +// make a route that returns hello world + +import { NextResponse } from 'next/server'; +import { ResearchListing } from '@/app/models/ResearchModel'; +const admin = require('firebase-admin'); +admin.initializeApp(); +const db = admin.firestore(); + +export async function GET(request: Request) { + try { + const url = new URL(request.url); + const department = url.searchParams.get('department'); + + // Retrieve the student_levels parameter(s) as an array. + let studentLevels: string[] = url.searchParams.getAll('student_levels'); + if (studentLevels.length === 0) { + const param = url.searchParams.get('student_levels'); + if (param) { + studentLevels = param.split(',').map((s) => s.trim()); + } + } + + // Begin building the query by department (or any other Firestore-queryable filter). + let queryRef = db.collection('research-listings'); + if (department) { + queryRef = queryRef.where('department', '==', department); + } + if (studentLevels.length > 0) { + studentLevels.forEach((level) => { + queryRef = queryRef.whereField(level, true); + }); + } + // Execute the query. + const snapshot = await queryRef.get(); + let researchListings: ResearchListing[] = snapshot.docs.map((doc: any) => ({ + id: doc.id, + ...doc.data(), + })); + + // If studentLevels filter is provided, perform local filtering. + // We check that for each listing, at least one of the requested student levels is true. + + return NextResponse.json(researchListings); + } catch (error) { + console.error('Error fetching research listings:', error); + return NextResponse.json( + { error: 'Failed to fetch research listings' }, + { status: 500 } + ); + } +} diff --git a/src/app/models/ResearchModel.ts b/src/app/models/ResearchModel.ts new file mode 100644 index 0000000..8e734d3 --- /dev/null +++ b/src/app/models/ResearchModel.ts @@ -0,0 +1,17 @@ +export interface ResearchListing { + id: string; + project_title: string; + department: string; + faculty_mentor: string; + 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; + faculty_members?: string[]; +} diff --git a/src/app/profile/DeleteUserButton.tsx b/src/app/profile/DeleteUserButton.tsx deleted file mode 100644 index ee4c2a9..0000000 --- a/src/app/profile/DeleteUserButton.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import * as React from 'react'; -import Button from '@mui/material/Button'; -import TextField from '@mui/material/TextField'; -import PersonRemoveOutlinedIcon from '@mui/icons-material/PersonRemoveOutlined'; -import ConfirmDialog from '@/components/ConfirmDialog/ConfirmDialog'; -import { HandleDeleteUser } from '@/firebase/auth/auth_delete_prompt'; - -interface DeleteUserDialogProps { - open: boolean; - setOpen: (value: boolean) => void; -} - -const DeleteUserDialog: React.FC = ({ - open, - setOpen, -}) => { - const [email, setEmail] = React.useState(''); - const [password, setPassword] = React.useState(''); - const [loading, setLoading] = React.useState(false); - const [err, setErr] = React.useState(null); - - const handleClickOpen = () => setOpen(true); - const handleClose = () => { - if (!loading) setOpen(false); - }; - - const handleConfirm = async () => { - setErr(null); - setLoading(true); - try { - await HandleDeleteUser(email.trim(), password); - setOpen(false); // close on success - } catch (e: any) { - setErr(e?.message ?? 'Failed to delete user'); - } finally { - setLoading(false); - } - }; - - const confirmDisabled = !email.trim() || !password || loading; - - return ( -
- - - - {/* Custom content inside the dialog */} -
- setEmail(e.target.value)} - autoComplete="email" - /> - setPassword(e.target.value)} - autoComplete="current-password" - /> - {err &&

{err}

} -
-
-
- ); -}; - -export default DeleteUserDialog; diff --git a/src/app/profile/temp.tsx b/src/app/profile/temp.tsx index 978634e..5fb45ab 100644 --- a/src/app/profile/temp.tsx +++ b/src/app/profile/temp.tsx @@ -1,38 +1,202 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useAuth } from '@/firebase/auth/auth_context'; -import { Button } from '@mui/material'; +import { + Button, + Grid, + TextField, + FormControl, + InputLabel, + Select, + MenuItem, + CircularProgress, + Box, + Typography, +} from '@mui/material'; import './style.css'; import HeaderCard from '@/component/HeaderCard/HeaderCard'; import DeleteUserButton from './DeleteUserButton'; +import GetUserRole from '@/firebase/util/GetUserRole'; import { updateProfile } from 'firebase/auth'; +import { getFirestore, doc, updateDoc } from 'firebase/firestore'; +import { collection, query, where, getDocs } from 'firebase/firestore'; +import { QrCode2 } from '@mui/icons-material'; +import { set } from 'react-hook-form'; +import { placeholderCSS } from 'react-select/dist/declarations/src/components/Placeholder'; interface ProfileProps { userRole: string; } +const primaryButtonStyle: React.CSSProperties = { + borderRadius: '8px', + height: '40px', + width: '80px', + textTransform: 'none', + fontFamily: 'SF Pro Display-Bold , Helvetica', + backgroundColor: '#5736ac', + color: '#ffffff', +}; + +const secondaryButtonStyle: React.CSSProperties = { + borderRadius: '8px', + height: '40px', + width: '80px', + borderWidth: '2px', + textTransform: 'none', + fontFamily: 'SF Pro Display-Bold , Helvetica', + borderColor: '#808080', + color: '#808080', +}; + +const textFieldStyles = (isEditable: boolean) => ({ + '& .MuiOutlinedInput-root': { + '& fieldset': { + borderColor: isEditable ? 'black' : '#cecece', + }, + '&:hover fieldset': { + borderColor: isEditable ? 'black' : '#cecece', + }, + '&.Mui-focused fieldset': { + borderColor: isEditable ? undefined : '#cecece', + borderWidth: isEditable ? undefined : '1px', + }, + }, + '& .MuiInputBase-input': { + cursor: isEditable ? 'text' : 'default', + }, + '& .MuiInputLabel-outlined': { + color: isEditable ? undefined : '#888', + }, + '& .MuiInputLabel-outlined.Mui-focused': { + color: isEditable ? undefined : '#888', + }, +}); + export default function Profile(props: ProfileProps) { const { user } = useAuth(); + const [role, loading, error] = GetUserRole(user?.uid); + const uid = user?.uid as string; + const db = getFirestore(); - const nameParts = user.displayName.split(' '); + const [isLoading, setIsLoading] = useState(true); - // Extract first and last names - const firstName = nameParts[0] || ''; - const lastName = nameParts.slice(1).join(' '); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [email, setEmail] = useState(''); + const [department, setDepartment] = useState(''); + + const [gpa, setGpa] = useState(''); + const [phoneNumber, setPhoneNumber] = useState(''); + const [graduationDate, setGraduationDate] = useState(''); + const [degree, setDegree] = useState(''); const [updatedFirst, setUpdatedFirst] = useState(''); const [updatedLast, setUpdatedLast] = useState(''); + const [updatedDepartment, setUpdatedDepartment] = useState(''); + const [updatedGpa, setUpdatedGpa] = useState(''); + const [updatedPhoneNumber, setUpdatedPhoneNumber] = useState(''); + const [updatedGraduationDate, setUpdatedGraduationDate] = useState(''); + const [updatedDegree, setUpdatedDegree] = useState(''); + + const [isEditing, setIsEditing] = useState(false); const [open, setOpen] = React.useState(false); + useEffect(() => { + const fetchUserData = async () => { + setIsLoading(true); + + try { + const usersCollectionRef = collection(db, 'users'); + const q = query(usersCollectionRef, where('uid', '==', uid)); + const querySnapshot = await getDocs(q); + + if (!querySnapshot.empty) { + const docSnap = querySnapshot.docs[0]; + const data = docSnap.data(); + setFirstName(data.firstname || ''); + setLastName(data.lastname || ''); + setEmail(data.email || ''); + setGpa(data.gpa || ''); + setDepartment(data.department || ''); + setPhoneNumber(data.phonenumber || ''); + setGraduationDate(data.graduationdate || ''); + setDegree(data.degree || ''); + } else { + console.log('No such document!'); + } + } catch (error) { + console.error('Error fetching user data:', error); + } finally { + setIsLoading(false); + } + }; + + if (uid) { + fetchUserData(); + } + }, [db, uid]); + const handleSave = async (e: any) => { - e.preventDefault(); // Prevent the form from submitting in the traditional way - if (updatedFirst.trim() !== '' && updatedLast.trim() !== '') { + e.preventDefault(); + if (firstName.trim() !== '' && lastName.trim() !== '') { try { - await updateProfile(user, { - displayName: `${updatedFirst} ${updatedLast}`, - }); - window.location.reload(); - alert('Profile updated successfully'); + const usersCollectionRef = collection(db, 'users'); + const q = query(usersCollectionRef, where('uid', '==', uid)); + const querySnapshot = await getDocs(q); + + if (!querySnapshot.empty) { + const docRef = doc(db, 'users', querySnapshot.docs[0].id); + const docSnap = querySnapshot.docs[0]; + const currentData = docSnap.data(); + const updatedData: any = {}; + + // Always include these fields for all users + if (updatedFirst.trim() !== '') updatedData.firstname = updatedFirst; + if (updatedLast.trim() !== '') updatedData.lastname = updatedLast; + if (updatedDepartment.trim() !== '') + updatedData.department = updatedDepartment; + + // Only include student fields if user is not faculty + if (showStudentFields) { + // For GPA field + if (updatedGpa.trim() !== '') { + updatedData.gpa = updatedGpa; + } + + // For phone number field + if (updatedPhoneNumber.trim() !== '') { + updatedData.phonenumber = updatedPhoneNumber; + } + + // For graduation date field + if (updatedGraduationDate.trim() !== '') { + updatedData.graduationdate = updatedGraduationDate; + } + + // For degree field + if (updatedDegree.trim() !== '') { + updatedData.degree = updatedDegree; + } + } + + // Only update if there are changes + if (Object.keys(updatedData).length > 0) { + await updateDoc(docRef, updatedData); + console.log('Profile updated successfully'); + alert('Profile updated successfully'); + setIsEditing(false); + window.location.reload(); + } else { + console.log('No changes to update'); + alert('No changes detected'); + setIsEditing(false); + } + } else { + // This will only happen if the user ID doesn't exist in the collection + console.log('No document found for this user ID'); + alert('Profile not found. Please contact support.'); + } } catch (error) { console.error('Error updating profile: ', error); alert('Failed to update profile'); @@ -41,97 +205,270 @@ export default function Profile(props: ProfileProps) { alert('First name and last name cannot be empty.'); } }; + const handleCancel = () => { setUpdatedFirst(''); setUpdatedLast(''); + setUpdatedGpa(''); + setUpdatedDepartment(''); + setUpdatedPhoneNumber(''); + setUpdatedGraduationDate(''); + setUpdatedDegree(''); + setIsEditing(false); }; + if (loading || isLoading) { + return ( + + + + Loading your profile... + + + ); + } + + if (error) { + return ( + + + There was an error loading your profile. Please try again later. + + + + ); + } + + const showStudentFields = role !== 'faculty'; + return ( <> -
-
-
-
-
- {firstName[0].toUpperCase() + lastName[0].toUpperCase()} -
-
-
{user.displayName}
-
{user.email}
-
- +
+
+
+
+ {firstName[0]?.toUpperCase() + lastName[0]?.toUpperCase()}
+
{firstName + ' ' + lastName}
+
{email}
+
-
- -
-
-
-
BASIC INFO
-
- - + +
+ +
+
Personal Information
+
+ {!isEditing ? ( + + ) : ( + <> + + + + )}
-
-
-
First Name
-
Last Name
-
-
-
- setUpdatedFirst(e.target.value)} + + + + setUpdatedFirst(e.target.value)} + sx={textFieldStyles(isEditing)} /> -
- -
- + + setUpdatedLast(e.target.value)} + sx={textFieldStyles(isEditing)} /> -
-
+ + {showStudentFields && ( + + setUpdatedPhoneNumber(e.target.value)} + sx={textFieldStyles(isEditing)} + /> + + )} + + setUpdatedDepartment(e.target.value)} + sx={textFieldStyles(isEditing)} + /> + + {showStudentFields && ( + <> + + + + Degree + + + + + + + { + const value = parseFloat(e.target.value); + if (value >= 0.0 && value <= 4.0) { + setUpdatedGpa(e.target.value); + } + }} + sx={textFieldStyles(isEditing)} + /> + + + setUpdatedGraduationDate(e.target.value)} + sx={textFieldStyles(isEditing)} + /> + + + )} +
diff --git a/src/component/Dashboard/Welcome/Welcome.tsx b/src/component/Dashboard/Welcome/Welcome.tsx index cbb555b..820d5c1 100644 --- a/src/component/Dashboard/Welcome/Welcome.tsx +++ b/src/component/Dashboard/Welcome/Welcome.tsx @@ -90,6 +90,13 @@ export default function DashboardWelcome(props: DashboardProps) { image="https://c.animaapp.com/vYQBTcnO/img/profile@2x.png" /> + + +
)} @@ -221,6 +228,13 @@ export default function DashboardWelcome(props: DashboardProps) { image="https://c.animaapp.com/vYQBTcnO/img/profile@2x.png" /> + + +
)} @@ -277,6 +291,13 @@ export default function DashboardWelcome(props: DashboardProps) { text="Profile" /> + + +
)} diff --git a/src/component/TopNavBarSigned/style.css b/src/component/TopNavBarSigned/style.css index 35faec8..2e38adb 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: 279px; + width: 379px; } .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: 100px; + left: 210px; letter-spacing: 0; line-height: normal; position: absolute; @@ -23,7 +23,21 @@ font-family: "SF Pro Display-Bold", Helvetica; font-size: 16px; font-weight: 700; - left: 0; + 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; letter-spacing: 0; line-height: normal; position: absolute; @@ -36,7 +50,7 @@ all: unset; box-sizing: border-box; height: 37px; - left: 207px; + left: 307px; position: absolute; top: 0; width: 117px; diff --git a/src/components/Research/ApplicationCard.tsx b/src/components/Research/ApplicationCard.tsx new file mode 100644 index 0000000..b90b0ae --- /dev/null +++ b/src/components/Research/ApplicationCard.tsx @@ -0,0 +1,196 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Card, CardContent, Typography, Button, Box } from '@mui/material'; + +interface ApplicationCardProps { + userRole: string; + uid?: string; + project_title: string; + department: string; + faculty_mentor: { name: string; email: string }; + date_applied: string; + terms_available: string; + student_level: string; + project_description: string; + faculty_members?: string[]; + phd_student_mentor?: string; + prerequisites?: string; + credit?: string; + stipend?: string; + application_requirements?: string; + application_deadline?: string; + website?: string; + app_status: string; + onEdit?: () => void; + onShowApplications?: () => void; +} + +const ApplicationCard: React.FC = ({ + userRole, + uid, + project_title, + 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, + 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); + }; + + checkTextOverflow(); + + window.addEventListener('resize', checkTextOverflow); + return () => window.removeEventListener('resize', checkTextOverflow); + }, [expanded, project_description]); + + return ( + + + + {project_title} + + + {department} + + + + Status: + {' '} + + {app_status} + +
+ + Date Applied: + {' '} + + {date_applied} + +
+ + + Faculty Mentor: + {' '} + + {Object.entries(faculty_mentor ?? {}) + .map(([key, value]) => `${value}`) + .join(', ')} + +
+ + Faculty Email: + {' '} + + {Object.entries(faculty_mentor ?? {}) + .map(([key, value]) => `${key}`) + .join(', ')} + +
+ + Research Description + + {project_description} + + +
+ + {needsExpansion ? ( + + ) : ( +
+ )} + {isFacultyInvolved && ( + + + + + )} +
+
+ ); +}; + +export default ApplicationCard; diff --git a/src/components/Research/ApplicationTile.tsx b/src/components/Research/ApplicationTile.tsx new file mode 100644 index 0000000..1831599 --- /dev/null +++ b/src/components/Research/ApplicationTile.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { + Box, + Tabs, + Tab, + Typography, + Avatar, + IconButton, + Button, + Paper, + Stack, +} from '@mui/material'; +import ThumbUpAltOutlinedIcon from '@mui/icons-material/ThumbUpAltOutlined'; +import ThumbDownAltOutlinedIcon from '@mui/icons-material/ThumbDownAltOutlined'; +import ReviewModal from './ReviewModal'; + +interface ApplicationTileProps { + key: any; + item: any; + status: 'Pending' | 'Approved' | 'Denied'; + changeStatus: (id: string, app_status: string) => Promise; +} + +const ApplicationTile: React.FC = ({ + key, + item, + status, + changeStatus, +}) => { + return ( + + + {item?.firstname[0] || '' + item?.lastname[0] || ''} + + + {item.name} + + {item.email} + + + {status === 'Pending' && ( + + changeStatus(item.id, 'Approved')} + > + + + + changeStatus(item.id, 'Denied')} + /> + + + + )} + {status === 'Approved' && ( + + Approved + + )} + {status === 'Denied' && ( + + Denied + + )} + + ); +}; + +export default ApplicationTile; diff --git a/src/components/Research/EditResearchModal.tsx b/src/components/Research/EditResearchModal.tsx new file mode 100644 index 0000000..7e02e1b --- /dev/null +++ b/src/components/Research/EditResearchModal.tsx @@ -0,0 +1,272 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + Button, + Grid, + Typography, +} from '@mui/material'; +import firebase from '@/firebase/firebase_config'; + +interface EditResearchModalProps { + open: boolean; + onClose: () => void; + listingData: any; // The current listing's data (pre-filled) + onSubmitSuccess: () => void; // Callback to refresh the listings +} + +const EditResearchModal: React.FC = ({ + open, + onClose, + listingData, + onSubmitSuccess, +}) => { + const [formData, setFormData] = useState({ ...listingData }); + const [facultyEmail, setFacultyEmail] = useState(''); + const [facultyName, setFacultyName] = useState(''); + + useEffect(() => { + setFormData({ ...listingData }); + }, [listingData]); + + 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(''); + } + }; + + /** 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 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!'); + onSubmitSuccess(); + onClose(); + } catch (error) { + console.error('Update failed:', error); + alert('Failed to update listing.'); + } + }; + + return ( + + Edit Research Listing + + + + + + + + + + + {/* Faculty Mentor */} + + Faculty Mentors + setFacultyEmail(e.target.value)} + fullWidth + margin="dense" + /> + setFacultyName(e.target.value)} + fullWidth + margin="dense" + /> + + + {formData.faculty_mentor && + Object.entries(formData.faculty_mentor).map(([email, name]) => ( + +
+ + {name} ({email}) + + +
+
+ ))} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ ); +}; + +export default EditResearchModal; diff --git a/src/components/Research/FacultyApplicantsView.tsx b/src/components/Research/FacultyApplicantsView.tsx new file mode 100644 index 0000000..9ff1de3 --- /dev/null +++ b/src/components/Research/FacultyApplicantsView.tsx @@ -0,0 +1,161 @@ +import React, { useEffect } from 'react'; +import { + Box, + Tabs, + Tab, + Typography, + Avatar, + IconButton, + Button, + Paper, + Stack, +} from '@mui/material'; +import ApplicationTile from './ApplicationTile'; +import firebase from '@/firebase/firebase_config'; +import { + collection, + addDoc, + updateDoc, + doc, + where, + query, + documentId, + getDocs, +} from 'firebase/firestore'; + +interface FacultyApplicantsViewProps { + id: string; + researchListing: any; + onBack: () => void; +} + +const FacultyApplicantsView: React.FC = ({ + id, + researchListing, + onBack, +}) => { + const [tabIndex, setTabIndex] = React.useState(0); + const [applications, setApplications] = React.useState( + researchListing.applications + ); + + const handleChange = (_event: React.SyntheticEvent, newValue: number) => { + setTabIndex(newValue); + }; + + const changeStatus = async (id: string, app_status: string) => { + const db = firebase.firestore(); + const docRef = doc(db, 'research-listings', researchListing.docID); + const colRef = collection(docRef, 'applications'); + const docAppRef = doc(colRef, id); + await updateDoc(docAppRef, { app_status }); + for (var i = 0; i < researchListing.applications.length; i++) { + setApplications((old) => + old.map((app) => (app.id === id ? { ...app, app_status } : app)) + ); + } + }; + + return ( + + + {/* Top header area with button now aligned */} + + + {researchListing.project_title} + + + + + + {/* Tabs for Needs Review, Approved, Denied */} + + + + + + + + + + + {/* Section title */} + + Applications + + + {/* Application tiles based on tabs */} + {tabIndex === 0 && + applications + .filter((item) => item.app_status === 'Pending') + .map((item, index) => ( + + ))} + {tabIndex === 1 && + applications + .filter((item) => item.app_status === 'Approved') + .map((item, index) => ( + + ))} + {tabIndex === 2 && + applications + .filter((item) => item.app_status === 'Denied') + .map((item, index) => ( + + ))} + + + ); +}; + +export default FacultyApplicantsView; diff --git a/src/components/Research/FacultyResearchView.tsx b/src/components/Research/FacultyResearchView.tsx new file mode 100644 index 0000000..99c2f9e --- /dev/null +++ b/src/components/Research/FacultyResearchView.tsx @@ -0,0 +1,322 @@ +import React, { useState } from 'react'; +import { + Box, + Typography, + Button, + Grid, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, +} from '@mui/material'; +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'; +import EditResearchModal from './EditResearchModal'; + +interface FacultyResearchViewProps { + researchListings: any[]; + role: string; + uid: string; + getResearchListings: () => void; + postNewResearchPosition: (formData: any) => Promise; +} + +const FacultyResearchView: React.FC = ({ + researchListings, + role, + uid, + getResearchListings, + postNewResearchPosition, +}) => { + const [studentView, showStudentView] = useState(true); + const [editModalOpen, setEditModalOpen] = useState(false); + const [editingForm, setEditingForm] = useState(null); + const [selectedResearchId, setSelectedResearchId] = useState( + null + ); + + // 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); + } finally { + handleCloseDeleteModal(); + } + }; + + // Get my positions by filtering + const myPositions = researchListings.filter((item) => + item.faculty_members?.includes(uid) + ); + + // Check if there are no positions when in my positions view + const hasNoPositions = studentView && myPositions.length === 0; + + // Callback to go back to the research listings view + const handleBackToListings = () => { + setSelectedResearchId(null); + }; + + return ( + <> + {selectedResearchId ? ( + + listing.docID === selectedResearchId + )} + onBack={handleBackToListings} + /> + + ) : ( + <> + {/* Header section with buttons */} + + + + My Positions: + + + + + + + + {hasNoPositions ? ( + + + No research positions found + + + You haven't created any research positions yet. Get started + by creating your first position using the "Create New + 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)} + /> + + )) + : researchListings.map((item, index) => ( + + { + setSelectedResearchId(item.docID); + }} + onEdit={() => { + console.log('Opening edit modal'); + setEditingForm(item); + setEditModalOpen(true); + }} + onDelete={() => handleOpenDeleteModal(item.docID)} + /> + + ))} + + )} + + )} + + {/* Delete Confirmation Modal */} + + + Confirm Deletion + + + + Are you sure you want to delete this research listing? This action + cannot be undone. + + + + + + + + + {editingForm && ( + setEditModalOpen(false)} + listingData={editingForm} + onSubmitSuccess={getResearchListings} + /> + )} + + ); +}; + +export default FacultyResearchView; diff --git a/src/components/Research/LessInfoButton.tsx b/src/components/Research/LessInfoButton.tsx new file mode 100644 index 0000000..3755317 --- /dev/null +++ b/src/components/Research/LessInfoButton.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import Button from '@mui/material/Button'; +import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; + +interface LessInfoButtonProps { + onClick: () => void; +} + +const LessInfoButton: React.FC = ({ onClick }) => { + return ( + + ); +}; + +export default LessInfoButton; diff --git a/src/components/Research/Modal.tsx b/src/components/Research/Modal.tsx new file mode 100644 index 0000000..39383e5 --- /dev/null +++ b/src/components/Research/Modal.tsx @@ -0,0 +1,397 @@ +import React, { useState } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + SxProps, + Grid, + MenuItem, + Checkbox, + InputLabel, + FormControlLabel, + InputAdornment, +} from '@mui/material'; +import firebase from '@/firebase/firebase_config'; +import { collection, addDoc } from 'firebase/firestore'; +import { Theme } from '@emotion/react'; +import { v4 as uuidv4 } from 'uuid'; +import { getStorage, ref, uploadBytes, getDownloadURL } from 'firebase/storage'; + +/** Define an interface that matches your JSON keys (updated for faculty_mentor). */ +interface FormData { + id: string; + project_title: string; + department: string; + faculty_mentor: { [email: string]: string }; // Updated to use curly braces for the map + 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; +} + +/** Initialize all fields to empty strings. */ +interface ResearchModal { + onSubmitSuccess: () => void; + currentFormData: FormData; + buttonStyle?: SxProps; + buttonText: React.ReactNode; // Changed from string to ReactNode + firebaseQuery: (formData: any) => Promise; + uid: string; +} + +const ResearchModal: React.FC = ({ + 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); + }; + + /** 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 })); + }; + + /** Adds a faculty mentor to the map. */ + const handleAddFacultyMentor = () => { + if (facultyEmail && facultyName) { + setFormData((prev) => ({ + ...prev, + faculty_mentor: { + ...prev.faculty_mentor, + [facultyEmail]: facultyName, + }, + })); + setFacultyEmail(''); + setFacultyName(''); + } + }; + + /** 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 }; + }); + }; + + /** Submits the form and clears it, then closes the dialog. */ + const handleSubmit = async () => { + const finalFormData = { + ...formData, + faculty_mentor: formData.faculty_mentor, + creator_id: uid, + faculty_members: [uid], + }; + console.log('Final Form Data:', finalFormData); + firebaseQuery(finalFormData); + onSubmitSuccess(); + setFormData(currentFormData); + handleClose(); + }; + + 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 */} + + + + + {/* Terms Available */} + + + + {/* Student Level */} + + + + + {/* Prerequisites */} + + + + + {/* Credit */} + + + + + {/* Stipend */} + + $ + ), // Add dollar sign + }} + fullWidth + /> + + + {/* Website */} + + + + + {/* Application Requirements */} + + + + + {/* Application Deadline */} + + Application Deadline + + { + setFormData((prev) => ({ + ...prev, + application_deadline: e.target.checked ? 'Rolling' : '', + })); + }} + /> + } + label="Rolling" + /> + + + {/* Project Description */} + + + +
+
+ + + + +
+
+ ); +}; + +export default ResearchModal; diff --git a/src/components/Research/ModalApplicationForm.tsx b/src/components/Research/ModalApplicationForm.tsx new file mode 100644 index 0000000..a5f42ae --- /dev/null +++ b/src/components/Research/ModalApplicationForm.tsx @@ -0,0 +1,358 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + Button, + Grid, + Snackbar, + Alert, +} from '@mui/material'; +import { getAuth } from 'firebase/auth'; +import firebase from '@/firebase/firebase_config'; +import { + collection, + addDoc, + updateDoc, + doc, + where, + query, + documentId, + getDocs, +} from 'firebase/firestore'; +import { v4 as uuidv4 } from 'uuid'; + +interface ModalApplicationFormProps { + open: boolean; + onClose: () => void; + listingId: string; +} + +const ModalApplicationForm: React.FC = ({ + open, + onClose, + listingId, +}) => { + const auth = getAuth(); + const user = auth.currentUser; + + const [formData, setFormData] = useState({ + firstname: '', + lastname: '', + email: user?.email || '', + phone: '', + department: '', + degree: '', + gpa: '', + graduationDate: '', + resume: '', + qualifications: '', + weeklyHours: '', + }); + + const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(''); + + useEffect(() => { + const fetchProfileData = async () => { + if (!user) return; + console.log('🔥 Fetching profile for UID:', user.uid); + + try { + const db = firebase.firestore(); + const snapshot = await db + .collection('users') // change users to users_test for profile database + .where('uid', '==', user.uid) // change uid to userId + .get(); + + if (!snapshot.empty) { + const profileData = snapshot.docs[0].data(); + console.log('✅ Profile data:', profileData); + setFormData((prev) => ({ + ...prev, + firstname: profileData.firstname || '', + lastname: profileData.lastname || '', + phone: profileData.phonenumber || '', + department: profileData.department || '', + degree: profileData.degree || '', + gpa: profileData.gpa || '', + graduationDate: profileData.graduationdate || '', + resume: '', + qualifications: '', + weeklyHours: '', + })); + } else { + console.warn('⚠️ No matching profile found for user.uid'); + } + } catch (error) { + console.error('Error fetching profile data:', error); + } + }; + + if (open && user?.uid) { + fetchProfileData(); + } + }, [open, user]); + + const handleChange = (e: any) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const handleSubmit = async () => { + if (!user) { + setError('You must be signed in to apply.'); + return; + } + const requiredFields = [ + 'firstname', + 'lastname', + 'phone', + 'department', + 'degree', + 'gpa', + 'graduationDate', + 'resume', + 'qualifications', + 'weeklyHours', + ]; + + const missingField = requiredFields.find( + (field) => !formData[field as keyof typeof formData] + ); + if (missingField) { + setError(`Please fill out the ${missingField} field.`); + return; + } + + const urlRegex = /^https?:\/\/[\w.-]+\.[a-z]{2,}.*$/i; + if (!urlRegex.test(formData.resume)) { + setError('Please enter a valid URL for the resume.'); + return; + } + + try { + const db = firebase.firestore(); + + // First, update the user profile with any changed form data + console.log('⏳ Updating user profile if values changed...'); + const userRef = db.collection('users').where('uid', '==', user.uid); + const userSnapshot = await userRef.get(); + + if (!userSnapshot.empty) { + const userDocRef = userSnapshot.docs[0].ref; + const userData = userSnapshot.docs[0].data(); + + // Check if any profile data has been updated + const updatedProfileData = { + firstname: + formData.firstname !== userData.firstname + ? formData.firstname + : userData.firstname, + lastname: + formData.lastname !== userData.lastname + ? formData.lastname + : userData.lastname, + phonenumber: + formData.phone !== userData.phonenumber + ? formData.phone + : userData.phonenumber, + department: + formData.department !== userData.department + ? formData.department + : userData.department, + degree: + formData.degree !== userData.degree + ? formData.degree + : userData.degree, + gpa: formData.gpa !== userData.gpa ? formData.gpa : userData.gpa, + graduationdate: + formData.graduationDate !== userData.graduationdate + ? formData.graduationDate + : userData.graduationdate, + }; + + // Only update fields that have changed + const fieldsToUpdate = Object.entries(updatedProfileData) + .filter(([key, value]) => userData[key] !== value) + .reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {}); + + if (Object.keys(fieldsToUpdate).length > 0) { + await userDocRef.update(fieldsToUpdate); + console.log('User profile updated with new values'); + } else { + console.log('No profile updates needed'); + } + } + + const parentDocRef = doc(db, 'research-listings', listingId); + const noteColRef = collection(parentDocRef, 'applications'); + const finalFormData = { + uid: user.uid, + app_status: 'Pending', + date: new Date().toLocaleDateString(), + ...formData, + }; + await addDoc(noteColRef, finalFormData); + alert('Application submitted successfully!'); + setSubmitted(true); + onClose(); + } catch (err) { + if (err instanceof Error) { + console.error('🔥 Submission failed:', err.message); + alert('Submission failed: ' + err.message); + } else { + console.error('🔥 Submission failed with unknown error:', err); + alert('Submission failed. Unknown error occurred.'); + } + + setError('Submission failed. Please try again.'); + } + }; + + return ( + <> + + Apply for Research Position + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setSubmitted(false)} + > + Application submitted! + + + setError('')} + > + {error} + + + ); +}; + +export default ModalApplicationForm; diff --git a/src/components/Research/MoreInfoButton.tsx b/src/components/Research/MoreInfoButton.tsx new file mode 100644 index 0000000..97fd097 --- /dev/null +++ b/src/components/Research/MoreInfoButton.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import Button from '@mui/material/Button'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; + +interface MoreInfoButtonProps { + onClick: () => void; // Function signature for the click handler +} + +const MoreInfoButton: React.FC = ({ onClick }) => { + return ( + + ); +}; + +export default MoreInfoButton; diff --git a/src/components/Research/ProjectCard.tsx b/src/components/Research/ProjectCard.tsx new file mode 100644 index 0000000..876578b --- /dev/null +++ b/src/components/Research/ProjectCard.tsx @@ -0,0 +1,275 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { + Card, + CardContent, + Typography, + Button, + Box, + Grid, +} 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: {}; + terms_available: string; + student_level: string; + project_description: string; + faculty_members?: string[]; + phd_student_mentor?: {} | string; + prerequisites?: string; + credit?: string; + stipend?: string; + application_requirements?: string; + application_deadline?: string; + website?: string; + applications?: any[]; + onEdit?: () => void; + onShowApplications?: () => void; + onDelete?: () => void; + listingId: string; +} + +const ProjectCard: React.FC = ({ + userRole, + uid, + project_title, + department, + faculty_mentor, + terms_available, + student_level, + project_description, + faculty_members = [], + listingId, + phd_student_mentor, + prerequisites, + credit, + stipend, + application_requirements, + application_deadline, + website, + applications = [], + 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; + + if (expanded) { + setNeedsExpansion(true); + return; + } + + const isOverflowing = element.scrollHeight > element.clientHeight; + setNeedsExpansion(isOverflowing); + }; + + checkTextOverflow(); + + window.addEventListener('resize', checkTextOverflow); + return () => window.removeEventListener('resize', checkTextOverflow); + }, [expanded, project_description]); + + const handleModalOpen = async () => { + // for (const application of applications) { + // if (application?.uid === uid) { + // alert('You have already applied to this project.'); + // return; + // } + // } + + setOpenModal(true); + }; + + return ( + <> + + + + {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(' ')} + + + + + + Academic Information + + + Student Level: {student_level} + + + Terms Available: {terms_available} + + + Prerequisites: {prerequisites} + + + Credit: {credit} + + + Stipend: {stipend} + + + + + + Application Details + + + Application Requirements:{' '} + {application_requirements} + + + Application Deadline: {application_deadline} + + + Website:{' '} + {website && + !['n/a', 'na', 'none', 'no', ''].includes( + website.toLowerCase().trim() + ) ? ( + + {website} + + ) : ( + 'None provided' + )} + + + + + + Research Description + + + {project_description} + + + + + + {needsExpansion ? ( + + ) : ( +
+ )} + {userRole === 'student_applying' || userRole === 'student_applied' ? ( + + ) : isFacultyInvolved ? ( + + + + + + ) : null} +
+
+ setOpenModal(false)} + listingId={listingId} + /> + + ); +}; + +export default ProjectCard; diff --git a/src/components/Research/ReviewModal.tsx b/src/components/Research/ReviewModal.tsx new file mode 100644 index 0000000..4f07cd4 --- /dev/null +++ b/src/components/Research/ReviewModal.tsx @@ -0,0 +1,303 @@ +import React, { useState } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + Typography, + Box, + Divider, + SxProps, + Paper, + Grid, +} from '@mui/material'; +import { Theme } from '@emotion/react'; + +interface FormData { + item: any; + buttonText?: string; + buttonStyle?: SxProps; +} + +// Fields that should be hidden from the review +const HIDDEN_FIELDS = ['id', 'uid', 'docID', 'appid', '_id', 'app_id', 'key']; + +// Fields that should be displayed first and in a specific order +const PRIORITY_FIELDS = [ + 'firstname', + 'lastname', + 'email', + 'phone', + 'department', + 'degree', + 'gpa', + 'graduationDate', + 'project_title', + 'qualifications', + 'weeklyHours', + 'resume', + 'app_status', + 'date_applied', +]; + +// Fields that should be displayed with more space (multiline) +const MULTILINE_FIELDS = [ + 'qualifications', + 'project_description', + 'message', + 'additional_info', +]; + +// Human-readable field labels +const FIELD_LABELS: Record = { + firstname: 'First Name', + lastname: 'Last Name', + qualifications: 'Qualifications', + project_title: 'Project Title', + terms_available: 'Terms Available', + student_level: 'Student Level', + degree: 'Degree', + gpa: 'GPA', + date_applied: 'Date Applied', + app_status: 'Application Status', +}; + +/** ReviewModal component for viewing application details */ +const ReviewModal: React.FC = ({ + item, + buttonText = 'Review', + buttonStyle, +}) => { + const [open, setOpen] = useState(false); + + /** Opens the dialog (modal). */ + const handleOpen = () => { + setOpen(true); + }; + + /** Closes the dialog (modal). */ + const handleClose = () => { + setOpen(false); + }; + + // Get formatted field label + const getFieldLabel = (key: string): string => { + // Return predefined label if available + if (FIELD_LABELS[key]) return FIELD_LABELS[key]; + + // Format camelCase to Title Case + return key + .replace(/([A-Z])/g, ' $1') // Insert a space before all capital letters + .replace(/_/g, ' ') // Replace underscores with spaces + .replace( + /\w\S*/g, + (txt) => txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase() + ); // Capitalize first letter + }; + + // Check if field should be displayed + const shouldDisplayField = (key: string, value: any): boolean => { + if (HIDDEN_FIELDS.includes(key.toLowerCase())) return false; + if ( + key.toLowerCase().includes('id') && + typeof value === 'string' && + value.length > 20 + ) + return false; + if (value === undefined || value === null) return false; + if (typeof value === 'object' && Object.keys(value).length === 0) + return false; + return true; + }; + + // Format field value for display + const formatFieldValue = (key: string, value: any): string => { + if (value === null || value === undefined) return ''; + if (typeof value === 'object') return JSON.stringify(value); + return String(value); + }; + + // Order fields for display + const getOrderedFields = () => { + const entries = Object.entries(item).filter(([key, value]) => + shouldDisplayField(key, value) + ); + + // Extract priority fields first (in specified order) + const priorityEntries = PRIORITY_FIELDS.map((key) => + entries.find(([entryKey]) => entryKey.toLowerCase() === key.toLowerCase()) + ).filter((entry) => entry !== undefined) as [string, any][]; + + // Get remaining fields + const remainingEntries = entries.filter( + ([key]) => + !PRIORITY_FIELDS.some( + (priorityKey) => priorityKey.toLowerCase() === key.toLowerCase() + ) + ); + + return [...priorityEntries, ...remainingEntries]; + }; + + return ( +
+ {/* Button to open the modal */} + + + + + Review Application + + + + + {item.project_title && ( + + {item.project_title} + + )} + + {item.date_applied && ( + + Applied: {item.date_applied} + + )} + + + {/* Personal Info */} + + Personal Information + + + + {['firstname', 'lastname', 'email', 'phone'].map((key) => + item[key] ? ( + + + {getFieldLabel(key)} + + {formatFieldValue(key, item[key])} + + ) : null + )} + + + {/* Academic Info */} + + Academic Background + + + + {['department', 'degree', 'gpa', 'student_level'].map((key) => + item[key] ? ( + + + {getFieldLabel(key)} + + {formatFieldValue(key, item[key])} + + ) : null + )} + + + {/* Application Info */} + + Application Details + + + + {['weeklyHours', 'resume', 'app_status'].map((key) => + item[key] ? ( + + + {getFieldLabel(key)} + + {key === 'resume' ? ( + + + View Resume + + + ) : ( + {formatFieldValue(key, item[key])} + )} + + ) : null + )} + + {item.qualifications && ( + + + {getFieldLabel('qualifications')} + + + + {formatFieldValue('qualifications', item.qualifications)} + + + + )} + + + + + + + +
+ ); +}; + +export default ReviewModal; diff --git a/src/components/Research/SearchBox.tsx b/src/components/Research/SearchBox.tsx new file mode 100644 index 0000000..7ffc03a --- /dev/null +++ b/src/components/Research/SearchBox.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { Box, InputBase, SxProps, Theme } from '@mui/material'; +import MenuIcon from '@mui/icons-material/Menu'; +import SearchIcon from '@mui/icons-material/Search'; +interface SearchBoxProps { + placeholder?: string; + sx?: SxProps; + researchListingsFunc: () => Promise; +} + +const SearchBox: React.FC = ({ + placeholder = 'Hinted search text', + sx, + researchListingsFunc, +}) => { + return ( + + + + researchListingsFunc()} /> + + ); +}; + +export default SearchBox; diff --git a/src/components/Research/StudentResearchView.tsx b/src/components/Research/StudentResearchView.tsx new file mode 100644 index 0000000..87664de --- /dev/null +++ b/src/components/Research/StudentResearchView.tsx @@ -0,0 +1,297 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { + Box, + TextField, + FormControl, + InputLabel, + Select, + MenuItem, + Button, + Grid, +} from '@mui/material'; +import ProjectCard from '@/components/Research/ProjectCard'; +import ApplicationCard from '@/components/Research/ApplicationCard'; + +interface StudentResearchViewProps { + researchListings: any[]; + researchApplications: any[]; + role: string; + uid: string; + department: string; + setDepartment: (value: string) => void; + studentLevel: string; + setStudentLevel: (value: string) => void; + termsAvailable: string; + setTermsAvailable: (value: string) => void; + getResearchListings: () => void; + setResearchListings: (listings: any[]) => void; + getApplications: () => void; + setResearchApplications: (Applications: any[]) => void; +} + +const StudentResearchView: React.FC = ({ + researchListings, + researchApplications, + role, + uid, + department, + setDepartment, + setStudentLevel, + setTermsAvailable, + getResearchListings, + setResearchListings, + getApplications, +}) => { + const [myApplications, showMyApplications] = useState(true); + const [originalListings, setOriginalListings] = useState([]); + const searchInputRef = useRef(null); + + useEffect(() => { + if (researchListings.length > 0 && originalListings.length === 0) { + setOriginalListings([...researchListings]); + } + }, [researchListings, originalListings.length]); + + useEffect(() => { + getApplications(); + }, []); + + const handleSearch = (searchText: string) => { + if (!searchText && department === '') { + setResearchListings([...originalListings]); + return; + } + + const listingsToFilter = + originalListings.length > 0 + ? [...originalListings] + : [...researchListings]; + + let filteredListings = listingsToFilter; + + // Text search + if (searchText) { + filteredListings = filteredListings.filter((item) => { + const searchLower = searchText.toLowerCase(); + + const titleMatch = + item.project_title && + typeof item.project_title === 'string' && + item.project_title.toLowerCase().includes(searchLower); + + const descriptionMatch = + item.project_description && + typeof item.project_description === 'string' && + item.project_description.toLowerCase().includes(searchLower); + + const mentorMatch = + item.faculty_mentor && + typeof item.faculty_mentor === 'string' && + item.faculty_mentor.toLowerCase().includes(searchLower); + + return titleMatch || descriptionMatch || mentorMatch; + }); + console.log('Filtered Listings: ', filteredListings); + } + + // Department filter with special handling for CISE + if (department) { + filteredListings = filteredListings.filter((item) => { + const normalized = item.department?.toLowerCase().trim(); + + if ( + department === 'Computer and Information Sciences and Engineering' + ) { + return ( + normalized === 'computer and information science and engineering' || + normalized === 'computer and information sciences and engineering' + ); + } + + return normalized === department.toLowerCase(); + }); + + console.log('Filtered Listings by Department: ', filteredListings); + } + + setResearchListings(filteredListings); + }; + + const handleClearFilters = () => { + setDepartment(''); + setStudentLevel(''); + setTermsAvailable(''); + + if (searchInputRef.current) { + searchInputRef.current.value = ''; + } + + if (originalListings.length > 0) { + setResearchListings([...originalListings]); + } else { + getResearchListings(); + } + }; + + const handleDepartmentChange = (value: string) => { + setDepartment(value); + const searchText = searchInputRef.current?.value || ''; + handleSearch(searchText); + }; + + return ( + <> + + + + + + {/* Only show search controls when viewing research listings (not applications) */} + {myApplications ? ( + + { + 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 }} + /> + + + + + Department + + + + ) : null} + + {/* Content Display */} + {myApplications ? ( + + {researchListings.map((item, index) => ( + + + + ))} + + ) : ( + + {researchApplications.map((item, index) => { + return ( + + + + ); + })} + + )} + + + ); +}; + +export default StudentResearchView; diff --git a/src/oldPages/Apply/page.tsx b/src/oldPages/Apply/page.tsx index 1d83daa..ae94d35 100644 --- a/src/oldPages/Apply/page.tsx +++ b/src/oldPages/Apply/page.tsx @@ -62,8 +62,9 @@ export default function Application() { // 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()}`; // extract the nationality const [nationality, setNationality] = React.useState(null); @@ -368,14 +369,13 @@ export default function Application() { ); setNames(data); console.log(names); - } catch (err) { console.log(err); } } fetchData(); }, []); - console.log("eek"); + console.log('eek'); return ( <> diff --git a/src/types/User.ts b/src/types/User.ts index 883cfd1..b070c9f 100644 --- a/src/types/User.ts +++ b/src/types/User.ts @@ -34,14 +34,14 @@ export type Role = | 'student_applied' | 'student_accepted' | 'student_denied'; - + export const roleMapping: Record = { - Student: "Student", - admin: "Admin", - faculty: "Faculty", - unapproved: "Unapproved", - student_applying: "Student", - student_applied: "Student", - student_accepted: "Student (Accepted)", - student_denied: "Student (Denied)", -}; \ No newline at end of file + Student: 'Student', + admin: 'Admin', + faculty: 'Faculty', + unapproved: 'Unapproved', + student_applying: 'Student', + student_applied: 'Student', + student_accepted: 'Student (Accepted)', + student_denied: 'Student (Denied)', +};