-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: initial import button and modal * feat: import route placeholder and dispatch * feat: perform import using zot4plan api * fix: roadmap multiplan selector index * feat: user selects year for imported roadmap * feat: trim unused zot4plan years * feat: improve import warning styles * feat: imported planner client side checks * feat: better import modal wording * refactor: variable for warning red color * refactor: filtering undefined imported courses * refactor: throw error in import route * fix: start year for fall quarter * refactor: organize zot4plan conversion code * refactor: further organize conversion code * feat: numbering for duplicate plan names * doc: clarify month index for fall quarter cutoff * fix: prevent submitting form in modal
- Loading branch information
Showing
10 changed files
with
366 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
/** | ||
@module Zot4PlanImportRoute | ||
*/ | ||
|
||
import { z } from 'zod'; | ||
import { publicProcedure, router } from '../helpers/trpc'; | ||
import { TRPCError } from '@trpc/server'; | ||
import { SavedRoadmap, SavedPlannerData, SavedPlannerQuarterData, QuarterName } from '@peterportal/types'; | ||
|
||
type Zot4PlanYears = string[][][]; | ||
|
||
type Zot4PlanSchedule = { | ||
years: Zot4PlanYears; | ||
selectedPrograms: { | ||
value: number; | ||
label: string; | ||
is_major: boolean; | ||
}[][]; | ||
addedCourses: []; | ||
courses: []; | ||
apExam: { | ||
id: number; | ||
name: string; | ||
score: number; | ||
courses: string[]; | ||
GE: []; | ||
units: number; | ||
}[]; | ||
}; | ||
|
||
/** | ||
* Get a JSON schedule from Zot4Plan by name | ||
* Throw an error if it does not exist | ||
*/ | ||
const getFromZot4Plan = async (scheduleName: string) => { | ||
let res = {}; | ||
await fetch('https://api.zot4plan.com/api/loadSchedule/' + scheduleName, { | ||
method: 'PUT', | ||
}) | ||
.then((response) => { | ||
if (!response.ok) { | ||
throw new TRPCError({ | ||
code: 'BAD_REQUEST', | ||
message: 'Schedule name could not be obtained from Zot4Plan', | ||
}); | ||
} | ||
return response.json(); | ||
}) | ||
.then((json) => { | ||
res = json; | ||
}); | ||
return res as Zot4PlanSchedule; | ||
}; | ||
|
||
/** | ||
* Convert a Zot4Plan course name into a PeterPortal course ID | ||
*/ | ||
const convertIntoCourseID = (zot4PlanCourse: string): string => { | ||
// PeterPortal course IDs are the same as Zot4Plan course IDs except all spaces are removed | ||
return zot4PlanCourse.replace(/\s/g, ''); | ||
}; | ||
|
||
/** | ||
* Trim the empty years off the end of a saved roadmap planner | ||
* (Other than the first year) | ||
*/ | ||
const trimEmptyYears = (planner: SavedPlannerData) => { | ||
// Empty years in the middle aren't trimmed because that makes it hard to add years there | ||
while (planner.content.length > 1) { | ||
let yearHasCourses = false; | ||
for (const quarter of planner.content[planner.content.length - 1].quarters) { | ||
if (quarter.courses.length != 0) { | ||
yearHasCourses = true; | ||
} | ||
} | ||
if (!yearHasCourses) { | ||
// The year does not have courses, so trim it | ||
planner.content.pop(); | ||
} else { | ||
// The year does have courses, so we are done | ||
break; | ||
} | ||
} | ||
}; | ||
|
||
/** | ||
* Determine a student's start year based on their current year in school | ||
* (ex. a first-year's current year is "1") | ||
*/ | ||
const getStartYear = (studentYear: string): number => { | ||
let startYear = new Date().getFullYear(); | ||
startYear -= parseInt(studentYear); | ||
// First-years in Fall start this year, not the previous year | ||
// Month index 7 is August, when Fall quarter is approaching | ||
if (new Date().getMonth() >= 7) startYear += 1; | ||
return startYear; | ||
}; | ||
|
||
/** | ||
* Convert the years of a Zot4Plan schedule into the saved roadmap planner format | ||
*/ | ||
const convertIntoSavedPlanner = ( | ||
originalScheduleYears: Zot4PlanYears, | ||
scheduleName: string, | ||
startYear: number, | ||
): SavedPlannerData => { | ||
const converted: SavedPlannerData = { | ||
name: scheduleName, | ||
content: [], | ||
}; | ||
|
||
// Add courses | ||
for (let i = 0; i < originalScheduleYears.length; i++) { | ||
const year = originalScheduleYears[i]; | ||
const quartersList: SavedPlannerQuarterData[] = []; | ||
for (let j = 0; j < year.length; j++) { | ||
const quarter = year[j]; | ||
const courses: string[] = []; | ||
for (let k = 0; k < quarter.length; k++) { | ||
courses.push(convertIntoCourseID(quarter[k])); | ||
} | ||
if (j >= 3 && courses.length == 0) { | ||
// Do not include the summer quarter if it has no courses (it is irrelevant) | ||
continue; | ||
} | ||
quartersList.push({ | ||
name: ['Fall', 'Winter', 'Spring', 'Summer1', 'Summer2', 'Summer10wk'][Math.min(j, 5)] as QuarterName, | ||
courses: courses, | ||
}); | ||
} | ||
converted.content.push({ | ||
startYear: startYear + i, | ||
name: 'Year ' + (i + 1), | ||
quarters: quartersList, | ||
}); | ||
} | ||
// Trim trailing years | ||
trimEmptyYears(converted); | ||
|
||
return converted; | ||
}; | ||
|
||
/** | ||
* Convert a Zot4Plan schedule into the saved roadmap format | ||
*/ | ||
const convertIntoSavedRoadmap = ( | ||
originalSchedule: Zot4PlanSchedule, | ||
scheduleName: string, | ||
startYear: number, | ||
): SavedRoadmap => { | ||
// Convert the individual components | ||
const convertedPlanner = convertIntoSavedPlanner(originalSchedule.years, scheduleName, startYear); | ||
const res: SavedRoadmap = { | ||
planners: [convertedPlanner], | ||
transfers: [], | ||
}; | ||
return res; | ||
}; | ||
|
||
const zot4PlanImportRouter = router({ | ||
/** | ||
* Get a roadmap formatted for PeterPortal based on a Zot4Plan schedule by name | ||
* and labeled with years based on the current year and the student's year | ||
*/ | ||
getScheduleFormatted: publicProcedure | ||
.input(z.object({ scheduleName: z.string(), studentYear: z.string() })) | ||
.query(async ({ input }) => { | ||
const originalScheduleRaw = await getFromZot4Plan(input.scheduleName); | ||
const res = convertIntoSavedRoadmap(originalScheduleRaw, input.scheduleName, getStartYear(input.studentYear)); | ||
return res; | ||
}), | ||
}); | ||
|
||
export default zot4PlanImportRouter; |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
.import-schedule-btn.btn-light { | ||
background-color: #e3e5eb; | ||
} | ||
|
||
.import-schedule-icon { | ||
scale: 1.2; | ||
margin-right: 4px; | ||
} | ||
|
||
.import-schedule-warning { | ||
font-size: 14px; | ||
color: var(--warning-red); | ||
gap: 5px; | ||
|
||
.import-schedule-warning-icon { | ||
margin-bottom: 4px; | ||
margin-right: 4px; | ||
font-size: 16px; | ||
} | ||
} |
Oops, something went wrong.