diff --git a/app/src/components/MultipleScheduleDisplay.tsx b/app/src/components/MultipleScheduleDisplay.tsx index 13472c4..7ea4d82 100644 --- a/app/src/components/MultipleScheduleDisplay.tsx +++ b/app/src/components/MultipleScheduleDisplay.tsx @@ -7,6 +7,7 @@ const NUM_PER_PAGE = 25; interface Props { schedules: Schedule[]; numPerPage?: number; + pin: (sch: Schedule) => void; } export default function MultipleScheduleDisplay(props: Props) { @@ -26,7 +27,7 @@ export default function MultipleScheduleDisplay(props: Props) { {schedulesToShow.map((schedule: Schedule, s) => (
Schedule #{s + 1} - +
))} diff --git a/app/src/components/MultipleSelectionDisplay.tsx b/app/src/components/MultipleSelectionDisplay.tsx index f0037f9..7f781d4 100644 --- a/app/src/components/MultipleSelectionDisplay.tsx +++ b/app/src/components/MultipleSelectionDisplay.tsx @@ -8,6 +8,7 @@ interface Props { newSelection: () => void; handleRemove: (sectionToRemove: Section) => void; handleDeleteSelection: (ind: number) => void; + } export default function MultipleSelectionDisplay(props: Props) { diff --git a/app/src/components/ScheduleBuilder.tsx b/app/src/components/ScheduleBuilder.tsx index c3f97d5..db33df0 100644 --- a/app/src/components/ScheduleBuilder.tsx +++ b/app/src/components/ScheduleBuilder.tsx @@ -1,269 +1,217 @@ -import { Component } from "react"; -import { Course, Section, SOC_API, SOC_Generic } from "@scripts/soc"; +//Importing React, necessary components, other class files, and any dependencies - used ChatGPT for proper syntax with the 'from ""'. +import React, { useState, useEffect } from "react"; import { - Schedule, - ScheduleGenerator, - Selection, -} from "@scripts/scheduleGenerator"; -import SectionPicker from "@components/SectionPicker"; -import MultipleSelectionDisplay from "@components/MultipleSelectionDisplay"; -import MultipleScheduleDisplay from "@components/MultipleScheduleDisplay"; + SOC_API, + SOC_Generic, + Course, + Section +} from "@scripts/soc"; +import { + Selection, + Schedule, + ScheduleGenerator, +} +from "@scripts/scheduleGenerator"; import { UF_SOC_API } from "@scripts/api"; import { API_Filters } from "@scripts/apiTypes"; import { arrayEquals, notEmpty, take } from "@scripts/utils"; import { LIMIT_VALUES, LIMITS } from "@constants/scheduleGenerator"; +import SectionPicker from "@components/SectionPicker"; +import MultipleSelectionDisplay from "@components/MultipleSelectionDisplay"; +import MultipleScheduleDisplay from "@components/MultipleScheduleDisplay"; + +//getDefaultSelections: returns an array of schedules selected +//defaultProgram: default program value (this was already specified in the original code file) const getDefaultSelections = () => [new Selection()]; const defaultProgram = "CWSP"; -interface Props {} - -interface States { - filters: API_Filters | null; - soc: SOC_Generic | null; - generator: ScheduleGenerator | null; - limit: number; - selections: Selection[]; - schedules: Schedule[]; - showAddCourse: boolean; -} - -const defaultState: States = { - filters: null, - soc: null, - generator: null, - limit: LIMIT_VALUES[0], - selections: getDefaultSelections(), - schedules: [], - showAddCourse: false, -}; - -export default class ScheduleBuilder extends Component { - constructor(props: Props) { - super(props); - this.state = defaultState; +//function to build schedules and accomplish what the ScheduleBuilder class had to achieve before +const ScheduleBuilder = () => { + //filters: API Filters fetched from the UF_SOC_API and are set to null at this time + const [filters, setFilters] = useState(null); + //generator: generation of schedules through user usage of the website + const [generator, setGenerator] = useState(null); + //soc: Schedule of Courses and is set to null to start off with (since no schedules yet built) + const [soc, setSOCState] = useState(null); + //limit: maximum limit of schedules that can be generated; set initially to LIMIT_VALUES[0] + const [limit, setLimit] = useState(LIMIT_VALUES[0]); + //schedules: initial list of schedules with no schedules at the beginning + const [schedules, setSchedules] = useState([]); + //pinnedSchedules: if a course wants to be pinned by the user, it will be added to the Schedule array + const [pinnedSchedules, setPinnedSchedules] = useState([]); + //showAddCourse: if a course needs to be added to a schedule, the AddCourse button would be implemented + const [showAddCourse, setShowAddCourse] = useState(false); + //selections: user-selected course sections + const [selections, setSelections] = useState( + getDefaultSelections() + ); + + useEffect(() => { + //fetch filters from UF_SOC_API and set the SOCState with the default program + UF_SOC_API.fetchFilters().then(async (fetchedFilters) => { + setFilters(fetchedFilters); + await setSOCState(fetchedFilters.terms[0].CODE, defaultProgram); + }); + }, []); + //allows for loading selected courses into the schedule generator + useEffect(() => { + if (generator) { + generator.loadSelections( + selections.filter((sel: Selection) => sel.length > 0) + ); + + const newSchedules: Schedule[] = take( + limit, + generator.yieldSchedules() + ); + setSchedules(newSchedules); + //output whether the schedule was changed or not changed + if (schedules === newSchedules) { + console.log("Same schedules"); + } else { + console.log("New schedules", newSchedules); + } } - - reset() { - console.log("Resetting Schedule Builder"); - this.setState({ - selections: getDefaultSelections(), - schedules: [], - }); - } - - componentDidMount() { - // TODO: implement retry upon fetch error - UF_SOC_API.fetchFilters().then(async (filters) => { - this.setState({ filters }); - await this.setSOC(filters.terms[0].CODE, defaultProgram); - }); - } - - componentDidUpdate( - _prevProps: Readonly, - prevState: Readonly, - ) { - // If limit was changed or a section was added/removed from a section, generate new schedules - if ( - this.state.limit != prevState.limit || - !arrayEquals( - this.state.selections.filter(notEmpty), - prevState.selections.filter(notEmpty), - ) - ) { - if (this.state.generator) { - // Make sure generator is not null - this.state.generator.loadSelections( - // Generate schedules from non-empty selections - this.state.selections.filter( - (sel: Selection) => sel.length > 0, - ), - ); - - const newSchedules: Schedule[] = [ - ...take( - this.state.limit, - this.state.generator.yieldSchedules(), - ), - ]; - this.setState({ schedules: newSchedules }); - console.log( - "Selections were changed, so schedules have been regenerated", - newSchedules, - ); - - // If schedules changed, log schedules - if (prevState.schedules != newSchedules) - console.log("New schedules", newSchedules); - else console.log("Same schedules"); - } - } - } - - async setSOC(termStr: string, programStr: string) { - console.log(`Setting SOC to "${termStr}" for "${programStr}"`); - await SOC_API.initialize({ termStr, programStr }).then((soc) => - this.setState({ - soc: soc, - generator: new ScheduleGenerator(soc), - }), + }, [limit, selections, generator, schedules]); + //reset the schedule building system + const reset = () => { + setSelections(getDefaultSelections()); + setSchedules([]); + console.log("Reset Schedule Builder"); + }; + //initialize soc variable + const setSOC = async (termStr: string, programStr: string) => { + console.log(`Setting SOC to "${termStr}" for "${programStr}"`); + const socInstance = await SOC_API.initialize({ termStr, programStr }); + setSOCState(socInstance); + }; + //manages dropping (removing of) a course or schedule + const handleDrop = (ind: number, uid: string) => { + if (soc) { + const item: Section | Course | null = soc.get(uid); + if (item) { + let sectionsToTryAdd: Section[]; + if (item instanceof Course) sectionsToTryAdd = item.sections; + else sectionsToTryAdd = [item]; + + const sectionsToAdd: Section[] = sectionsToTryAdd.filter( + (section) => + !selections.some((sel) => sel.includes(section)) ); - this.reset(); // Make sure to only show info from the current SOC + newSelection(ind, sectionsToAdd); + } } - - async handleDrop(ind: number, uid: string) { - if (this.state.soc) { - // Make sure SOC exists - const item: Section | Course | null = this.state.soc.get(uid); - console.log("Handling drop; will try to add", item); - - if (item) { - // Make sure a match was found (not null) - // Get the section(s) to try to add to selection - let sectionsToTryAdd: Section[]; - if (item instanceof Course) sectionsToTryAdd = item.sections; - else sectionsToTryAdd = [item]; - - // Get the sections that have not been added to a selection - const sectionsToAdd: Section[] = sectionsToTryAdd.filter( - (section) => - !this.state.selections.some( - // TODO: extract to a Selections class - (sel) => sel.includes(section), - ), - ); - this.newSelection(ind, sectionsToAdd); // Add the section that have not been added - } - } - } - - // TODO: make separate functions - newSelection(ind: number = -1, sectionsToAdd: Section[] = []) { - if (ind == -1) { - this.setState({ - selections: [...this.state.selections, new Selection()], - }); - return; - } - - const newSelections = this.state.selections.map((sel, i) => { - if (i == ind) return [...sel, ...sectionsToAdd]; + }; + + //handles adding of new courses or schedules + const newSelection = (ind: number = -1, sectionsToAdd: Section[] = []) => { + if (ind !== -1) { + const newSelections = selections.map((sel, i) => { + if (i === ind) return [...sel, ...sectionsToAdd]; return sel; }); - this.setState({ selections: newSelections }); } - - handleDeleteSelection(ind: number) { - let newSelections = this.state.selections.filter((_sel, i) => i != ind); - if (newSelections.length == 0) newSelections = getDefaultSelections(); - - this.setState({ selections: newSelections }); + else{ + setSelections([...selections, new Selection()]); + return; } - - handleRemove(sectionToRemove: Section) { - const newSelections = this.state.selections.map((sel) => - sel.filter((sec) => sec != sectionToRemove), - ); - this.setState({ selections: newSelections }); + setSelections(newSelections); + }; + //manages removal of a course selection + const handleRemove = (sectionToRemove: Section) => { + const newSelections = selections.map((sel) => + sel.filter((sec) => sec !== sectionToRemove) + ); + setSelections(newSelections); + }; + //manages deletion of a course selection + const handleDeleteSelection = (ind: number) => { + let newSelections = selections.filter((_sel, i) => i !== ind); + if (newSelections.length === 0) + newSelections = getDefaultSelections(); + setSelections(newSelections); + }; + //manages the pin status of a schedule + const togglePin = (sch: Schedule) => { + const pinned = pinnedSchedules; + if (pinned.some((s) => arrayEquals(s, sch))) { + setPinnedSchedules(pinned.filter((s) => !arrayEquals(s, sch))); + } else { + setPinnedSchedules([...pinned, sch]); } - - render() { - // Show loading screen if filters/terms haven't been fetched yet - if (this.state.filters === null) - return ( -
-

Fetching latest semester information...

-
- ); - if (this.state.soc === null) - // Make sure SOC is set + }; + //sample loading messages - used ChatGPT for assistance with this section + return ( +
+
+

+ 🐊 Swamp Scheduler 📆 +

+ +
+ + - this.setSOC(e.target.value, defaultProgram) - } - disabled={false} - > - {this.state.filters?.terms.map((t) => { - const { term, year } = SOC_Generic.decodeTermString( - t.CODE, - ); - return ( - - ); - })} - - - -
- -
- - {/* Main of Builder */} -
- {/* Picker */} -
- -
- - {/* Selected */} -
- -
- - {/* Generated Schedules */} -
- -
-
-
- ); - } -} + })} + + + + + +
+ +
+
+ +
+ +
+ +
+ +
+ +
+
+ + ); +}; +//export the function in other parts - the key purpose behind this task +export default ScheduleBuilder; diff --git a/app/src/components/ScheduleDisplay.tsx b/app/src/components/ScheduleDisplay.tsx index cd6ccc7..5b0ecd5 100644 --- a/app/src/components/ScheduleDisplay.tsx +++ b/app/src/components/ScheduleDisplay.tsx @@ -12,10 +12,9 @@ import { GrPersonalComputer } from "react-icons/gr"; interface Props { schedule: Schedule; + pin: (sch: Schedule) => void; } -interface States {} - // TODO: reconsider what to store type MeetTimeInfo = { meetTime: MeetTime; @@ -24,7 +23,7 @@ type MeetTimeInfo = { sectionIsOnline: boolean; }; -export default class ScheduleDisplay extends Component { +export default class ScheduleDisplay extends Component { // TODO: redo this (it is *disgusting*); maybe there is a library that does the work render() { const schedule = this.props.schedule, @@ -179,103 +178,105 @@ export default class ScheduleDisplay extends Component { const onlineSections: Section[] = schedule.filter((s) => s.isOnline); return ( -
-
-
- {schedule.map((sec: Section, s: number) => ( -
- ({s + 1}) Sec. {sec.number} [ - {sec.courseCode}] -
- ))} + <> +
+
+
+ {schedule.map((sec: Section, s: number) => ( +
+ ({s + 1}) Sec. {sec.number} [ + {sec.courseCode}] +
+ ))} +
-
-
-
-
- {[...Array(periodCounts.all).keys()] - .map((p) => p + 1) - .map((p) => ( -
- - {MeetTime.formatPeriod( - p, - schedule.term, - )} - -
- ))} +
+
+
+ {[...Array(periodCounts.all).keys()] + .map((p) => p + 1) + .map((p) => ( +
+ + {MeetTime.formatPeriod( + p, + schedule.term + )} + +
+ ))} - {onlineSections.length > 0 && ( -
+ {onlineSections.length > 0 && (
- ️ +
+ ️ +
-
- )} + )} +
-
-
-
- {divs} - {onlineSections.length > 0 && ( -
-
-
- {onlineSections.map( - (sec: Section, ind: number) => ( -
- {sec.displayName} - - {1 + ind} - -
- ), - )} +
+
+ {divs} + {onlineSections.length > 0 && ( +
+
+
+ {onlineSections.map( + (sec: Section, ind: number) => ( +
+ {sec.displayName} + + {1 + ind} + +
+ ) + )} +
-
- )} + )} +
-
+ + ); } } diff --git a/app/src/constants/frontend.ts b/app/src/constants/frontend.ts index 0667c10..14dcfbf 100644 --- a/app/src/constants/frontend.ts +++ b/app/src/constants/frontend.ts @@ -15,6 +15,7 @@ export function getSectionColor(sectionInd: number): string { const SearchByStringExampleMap = new Map([ ["course-code", "MAS3114"], ["course-title", "Linear Algebra"], + ["instructor", "Huang"], ]); export function getSearchByStringExample(searchByStr: string): string { diff --git a/app/src/constants/soc.ts b/app/src/constants/soc.ts index 3c3d3cf..e5227cf 100644 --- a/app/src/constants/soc.ts +++ b/app/src/constants/soc.ts @@ -64,13 +64,19 @@ export function getProgramString(program: Program): string { export enum SearchBy { COURSE_CODE = "Course Code", COURSE_TITLE = "Course Title", + INSTRUCTOR = "Instructor", } -export const SearchBys = [SearchBy.COURSE_CODE, SearchBy.COURSE_TITLE]; +export const SearchBys = [ + SearchBy.COURSE_CODE, + SearchBy.COURSE_TITLE, + SearchBy.INSTRUCTOR, +]; const SearchByStringMap = new Map([ ["course-code", SearchBy.COURSE_CODE], ["course-title", SearchBy.COURSE_TITLE], + ["instructor", SearchBy.INSTRUCTOR], ]); export function getSearchBy(searchByStr: string): SearchBy { diff --git a/app/src/scripts/soc/soc.tsx b/app/src/scripts/soc/soc.tsx index 7ed305d..30084a7 100644 --- a/app/src/scripts/soc/soc.tsx +++ b/app/src/scripts/soc/soc.tsx @@ -131,6 +131,14 @@ export abstract class SOC_Generic { return this.courses.filter((c) => c.name.toUpperCase().includes(upperPhrase), ); + } else if (searchBy === SearchBy.INSTRUCTOR) { + return this.courses.filter((c) => + c.sections.some((s) => + s.instructors.some((inst) => + inst.toUpperCase().includes(upperPhrase), + ), + ), + ); } throw new Error("Unhandled SearchBy."); }