import createSchedule from '@pages/background/lib/createSchedule'; import switchSchedule from '@pages/background/lib/switchSchedule'; import { ArrowUpRight, CalendarDots, ChatText, Check, Copy, FileText, Minus, Plus, Smiley, X, } from '@phosphor-icons/react'; import { background } from '@shared/messages'; import type { Course } from '@shared/types/Course'; import type Instructor from '@shared/types/Instructor'; import type { UserSchedule } from '@shared/types/UserSchedule'; import { englishStringifyList } from '@shared/util/string'; import { Button } from '@views/components/common/Button'; import { Chip, coreMap, flagMap } from '@views/components/common/Chip'; import { usePrompt } from '@views/components/common/DialogProvider/DialogProvider'; import Divider from '@views/components/common/Divider'; import Link from '@views/components/common/Link'; import Text from '@views/components/common/Text/Text'; import { useCalendar } from '@views/contexts/CalendarContext'; import clsx from 'clsx'; import React, { useRef, useState } from 'react'; import DisplayMeetingInfo from './DisplayMeetingInfo'; const { openNewTab, addCourse, removeCourse, openCESPage } = background; /** * Capitalizes the first letter of a string and converts the rest of the letters to lowercase. * * @param str - The string to be capitalized. * @returns The capitalized string. */ const capitalizeString = (str: string) => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); interface HeadingAndActionProps { /* The course to display */ course: Course; /* The active schedule */ activeSchedule: UserSchedule; /* The function to call when the popup should be closed */ onClose: () => void; } /** * Renders the heading component for the CoursePopup component. * * @param course - The course object containing course details. * @param activeSchedule - The active schedule object. * @param onClose - The function to close the popup. * @returns The rendered component. */ export default function HeadingAndActions({ course, activeSchedule, onClose }: HeadingAndActionProps): JSX.Element { const { courseName, department, number: courseNumber, uniqueId, instructors, flags, core } = course; const courseAdded = activeSchedule.courses.some(ourCourse => ourCourse.uniqueId === uniqueId); const formattedUniqueId = uniqueId.toString().padStart(5, '0'); const isInCalendar = useCalendar(); const [isCopied, setIsCopied] = useState(false); const lastCopyTime = useRef(0); const showDialog = usePrompt(); const getInstructorFullName = (instructor: Instructor) => instructor.toString({ format: 'first_last' }); const handleCopy = async (e: React.MouseEvent) => { e.stopPropagation(); const now = Date.now(); if (now - lastCopyTime.current < 500) { return; } lastCopyTime.current = now; await navigator.clipboard.writeText(formattedUniqueId); setIsCopied(true); setTimeout(() => setIsCopied(false), 500); }; const handleOpenRateMyProf = async () => { const openTabs = instructors.map(instructor => { const instructorSearchTerm = getInstructorFullName(instructor); instructorSearchTerm.replace(' ', '+'); const url = `https://www.ratemyprofessors.com/search/professors/1255?q=${instructorSearchTerm}`; return openNewTab({ url }); }); await Promise.all(openTabs); }; const handleOpenCES = async () => { const openTabs = instructors.map(instructor => { let { firstName = '', lastName = '' } = instructor; firstName = capitalizeString(firstName); lastName = capitalizeString(lastName); return openCESPage({ instructorFirstName: firstName, instructorLastName: lastName }); }); await Promise.all(openTabs); }; const handleOpenPastSyllabi = async () => { for (const instructor of instructors) { let { firstName = '', lastName = '' } = instructor; firstName = capitalizeString(firstName); lastName = capitalizeString(lastName); const url = `https://utdirect.utexas.edu/apps/student/coursedocs/nlogon/?year=&semester=&department=${department}&course_number=${courseNumber}&course_title=&unique=&instructor_first=${firstName}&instructor_last=${lastName}&course_type=In+Residence&search=Search`; openNewTab({ url }); } // Show the course's syllabi when no instructors listed if (instructors.length === 0) { const url = `https://utdirect.utexas.edu/apps/student/coursedocs/nlogon/?year=&semester=&department=${department}&course_number=${courseNumber}&course_title=&unique=&instructor_first=&instructor_last=&course_type=In+Residence&search=Search`; openNewTab({ url }); } }; const handleAddToNewSchedule = async (close: () => void) => { const newScheduleId = await createSchedule(`${course.semester.season} ${course.semester.year}`); switchSchedule(newScheduleId); addCourse({ course, scheduleId: newScheduleId }); close(); }; const handleAddOrRemoveCourse = async () => { const uniqueSemesterCodes = [ ...new Set( activeSchedule.courses .map(course => course.semester.code) .filter((code): code is string => code !== undefined) ), ]; uniqueSemesterCodes.sort(); const codeToReadableMap: Record = {}; activeSchedule.courses.forEach(course => { const { code } = course.semester; if (code) { const readable = `${course.semester.season} ${course.semester.year}`; codeToReadableMap[code] = readable; } }); const sortedSemesters = uniqueSemesterCodes .map(code => codeToReadableMap[code]) .filter((value): value is string => value !== undefined); const activeSemesters = englishStringifyList(sortedSemesters); if (!activeSchedule) return; if (!courseAdded) { const currentSemesterCode = course.semester.code; // Show warning if this course is for a different semester than the selected schedule if ( activeSchedule.courses.length > 0 && activeSchedule.courses.every(otherCourse => otherCourse.semester.code !== currentSemesterCode) ) { const dialogButtons = (close: () => void) => ( <> ); showDialog({ title: 'This course section is from a different semester!', description: ( <> The section you're adding is for{' '} {course.semester.season} {course.semester.year} , but your current schedule contains sections in{' '} {activeSemesters}. Mixing semesters in one schedule may cause confusion. ), buttons: dialogButtons, }); } else { addCourse({ course, scheduleId: activeSchedule.id }); } } else { removeCourse({ course, scheduleId: activeSchedule.id }); } }; return (
{courseName} ({department} {courseNumber})
{instructors.length > 0 ? ( with{' '} {instructors .map(instructor => ( {getInstructorFullName(instructor)} )) .flatMap((el, i) => (i === 0 ? [el] : [', ', el]))} ) : ( (No instructor has been provided) )}
{flags.map((flag: string) => ( ))} {core.map((coreVal: string) => ( ))}
); }