From 4df39daba72dbcd0d15bd5c40a8c0ec21cfb8b3f Mon Sep 17 00:00:00 2001 From: tigerding Date: Sun, 28 Apr 2024 03:31:27 +0000 Subject: [PATCH 01/11] refractor: side scroller for course evaluations --- static/js/redux/ui/SideScroller.tsx | 51 ++++++++++++++++++++++ static/js/redux/ui/evaluation_list.jsx | 2 +- static/js/redux/ui/side_scroller.jsx | 59 -------------------------- 3 files changed, 52 insertions(+), 60 deletions(-) create mode 100644 static/js/redux/ui/SideScroller.tsx delete mode 100644 static/js/redux/ui/side_scroller.jsx diff --git a/static/js/redux/ui/SideScroller.tsx b/static/js/redux/ui/SideScroller.tsx new file mode 100644 index 000000000..8b7cd5a93 --- /dev/null +++ b/static/js/redux/ui/SideScroller.tsx @@ -0,0 +1,51 @@ +/* +Copyright (C) 2017 Semester.ly Technologies, LLC + +Semester.ly is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Semester.ly is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +import React, { useState } from "react"; + +interface SideScrollerProps { + content: JSX.Element[], + navItems: JSX.Element[]; +} + +function SideScroller(props: SideScrollerProps) { + const [activeSlide, setActiveSlide] = useState(0); + + let navItems = null; + if (props.navItems) { + const navs = []; + + for (let i = 0; i < props.navItems.length; i++) { + const cls = activeSlide === i ? " nav-item-active" : ""; + navs.push( + setActiveSlide(i)} + > + {props.navItems[i]} + + ); + } + navItems =
{navs}
; + } + return ( +
+ {navItems} + {props.content[activeSlide]} +
+ ); +} + +export default SideScroller; \ No newline at end of file diff --git a/static/js/redux/ui/evaluation_list.jsx b/static/js/redux/ui/evaluation_list.jsx index 51036bdb1..bb9ac8327 100644 --- a/static/js/redux/ui/evaluation_list.jsx +++ b/static/js/redux/ui/evaluation_list.jsx @@ -15,7 +15,7 @@ GNU General Public License for more details. import PropTypes from "prop-types"; import React from "react"; import Evaluation from "./evaluation"; -import SideScroller from "./side_scroller"; +import SideScroller from "./SideScroller"; import { SEMESTER_RANKS } from "../constants/constants"; import * as SemesterlyPropTypes from "../constants/semesterlyPropTypes"; diff --git a/static/js/redux/ui/side_scroller.jsx b/static/js/redux/ui/side_scroller.jsx deleted file mode 100644 index 8f381f9cb..000000000 --- a/static/js/redux/ui/side_scroller.jsx +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright (C) 2017 Semester.ly Technologies, LLC - -Semester.ly is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Semester.ly is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. -*/ - -import PropTypes from "prop-types"; -import React from "react"; - -class SideScroller extends React.Component { - constructor(props) { - super(props); - this.state = { - activeSlide: 0, - }; - } - - render() { - let navItems = null; - if (this.props.navItems) { - const navs = []; - - for (let i = 0; i < this.props.navItems.length; i++) { - const cls = this.state.activeSlide === i ? " nav-item-active" : ""; - navs.push( - this.setState({ activeSlide: i })} - > - {this.props.navItems[i]} - - ); - } - navItems =
{navs}
; - } - return ( -
- {navItems} - {this.props.content[this.state.activeSlide]} -
- ); - } -} - -SideScroller.propTypes = { - content: PropTypes.arrayOf(PropTypes.element).isRequired, - navItems: PropTypes.arrayOf(PropTypes.element).isRequired, -}; - -export default SideScroller; From 537c4b965b4912b10fcae540098016e9b4c71f06 Mon Sep 17 00:00:00 2001 From: tigerding Date: Sun, 28 Apr 2024 03:35:05 +0000 Subject: [PATCH 02/11] refractor: constants --- static/js/redux/constants/{constants.jsx => constants.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename static/js/redux/constants/{constants.jsx => constants.ts} (100%) diff --git a/static/js/redux/constants/constants.jsx b/static/js/redux/constants/constants.ts similarity index 100% rename from static/js/redux/constants/constants.jsx rename to static/js/redux/constants/constants.ts From cbac6b55b38267f9f986124dd955ddd635162e24 Mon Sep 17 00:00:00 2001 From: tigerding Date: Sun, 28 Apr 2024 03:35:27 +0000 Subject: [PATCH 03/11] refractor: endpoints --- static/js/redux/constants/{endpoints.jsx => endpoints.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename static/js/redux/constants/{endpoints.jsx => endpoints.ts} (100%) diff --git a/static/js/redux/constants/endpoints.jsx b/static/js/redux/constants/endpoints.ts similarity index 100% rename from static/js/redux/constants/endpoints.jsx rename to static/js/redux/constants/endpoints.ts From b3cd7082211216577854fd12b8a4ccd0b1907b83 Mon Sep 17 00:00:00 2001 From: tigerding Date: Sun, 28 Apr 2024 03:35:45 +0000 Subject: [PATCH 04/11] refractor: majors --- static/js/redux/constants/{majors.jsx => majors.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename static/js/redux/constants/{majors.jsx => majors.ts} (100%) diff --git a/static/js/redux/constants/majors.jsx b/static/js/redux/constants/majors.ts similarity index 100% rename from static/js/redux/constants/majors.jsx rename to static/js/redux/constants/majors.ts From c0b94edad925a71ec231a19dedaf5fde488d721c Mon Sep 17 00:00:00 2001 From: tigerding Date: Sun, 28 Apr 2024 03:36:40 +0000 Subject: [PATCH 05/11] refractor: reactions --- static/js/redux/constants/{reactions.jsx => reactions.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename static/js/redux/constants/{reactions.jsx => reactions.ts} (100%) diff --git a/static/js/redux/constants/reactions.jsx b/static/js/redux/constants/reactions.ts similarity index 100% rename from static/js/redux/constants/reactions.jsx rename to static/js/redux/constants/reactions.ts From c946c7f03e719ce2babd99af7f3f9ab5a37af75c Mon Sep 17 00:00:00 2001 From: tigerding Date: Sun, 28 Apr 2024 03:39:45 +0000 Subject: [PATCH 06/11] refractor: school --- static/js/redux/constants/{schools.jsx => schools.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename static/js/redux/constants/{schools.jsx => schools.ts} (98%) diff --git a/static/js/redux/constants/schools.jsx b/static/js/redux/constants/schools.ts similarity index 98% rename from static/js/redux/constants/schools.jsx rename to static/js/redux/constants/schools.ts index 88f639360..0ac6297e2 100644 --- a/static/js/redux/constants/schools.jsx +++ b/static/js/redux/constants/schools.ts @@ -25,7 +25,7 @@ export const VALID_SCHOOLS = [ "salisbury", ]; -export const getSchoolSpecificInfo = (school) => { +export const getSchoolSpecificInfo = (school: typeof VALID_SCHOOLS[number]) => { switch (school) { case "uoft": return { From 047a7d20a68c50a9dde2b177da9039cc9192acf7 Mon Sep 17 00:00:00 2001 From: tigerding Date: Sun, 5 May 2024 19:35:29 +0000 Subject: [PATCH 07/11] refractor: add types in endpoints --- static/js/redux/constants/endpoints.ts | 37 ++++++++++++++------------ 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/static/js/redux/constants/endpoints.ts b/static/js/redux/constants/endpoints.ts index 5f10d77ea..f370d53c0 100644 --- a/static/js/redux/constants/endpoints.ts +++ b/static/js/redux/constants/endpoints.ts @@ -12,59 +12,62 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. */ +import { User } from "../ui/modals/PeerModalComponents/Types"; +import { Course, Semester, Slot, Timetable } from "./commonTypes"; + /* server endpoints */ export const getLogiCalEndpoint = () => "/user/log_ical/"; -export const getCourseInfoEndpoint = (courseId, semester) => +export const getCourseInfoEndpoint = (courseId: Course['id'], semester: string) => `/courses/${semester}/id/${courseId}/`; -export const getCourseSearchEndpoint = (query, semester, page = 1, limit = 6) => +export const getCourseSearchEndpoint = (query: string, semester: string, page = 1, limit = 6) => `/search/${semester}/${query}/?page=${page}&limit=${limit}`; export const getTimetablesEndpoint = () => "/timetables/"; -export const getLoadSavedTimetablesEndpoint = (semester) => +export const getLoadSavedTimetablesEndpoint = (semester: Semester) => `/user/timetables/${semester.name}/${semester.year}/`; export const getSaveTimetableEndpoint = () => "/user/timetables/"; export const getPersonalEventEndpoint = () => "/user/events/"; -export const getDeleteTimetableEndpoint = (semester, name) => +export const getDeleteTimetableEndpoint = (semester: Semester, name: string) => `/user/timetables/${semester.name}/${semester.year}/${name}/`; -export const getTimetablePreferencesEndpoint = (id) => +export const getTimetablePreferencesEndpoint = (id: Timetable['id']) => `/user/timetables/${id}/preferences/`; export const getSaveSettingsEndpoint = () => "/user/settings/"; -export const getClassmatesEndpoint = (semester, courses) => +export const getClassmatesEndpoint = (semester: Semester, courses: Array) => `/user/classmates/${semester.name}/${semester.year}?${$.param({ course_ids: courses, })}`; -export const getClassmatesInCourseEndpoint = (school, semester, courseId) => +export const getClassmatesInCourseEndpoint = (school: string, semester: string, courseId: Course['id']) => `/course_classmates/${school}/${semester}/id/${courseId}/`; -export const getMostClassmatesCountEndpoint = (semester, courses) => +export const getMostClassmatesCountEndpoint = (semester: Semester, courses: Array) => `/user/classmates/${semester.name}/${semester.year}?${$.param({ course_ids: courses, count: true, })}`; -export const getFriendsEndpoint = (semester) => +export const getFriendsEndpoint = (semester: Semester) => `/user/classmates/${semester.name}/${semester.year}/`; // Friends endpoints export const getFetchFriendsEndpointEndpoint = () => `/friends/`; -export const getRemoveFriendEndpoint = (userId) => `/friends/remove/${userId}`; -export const getSearchFriendsEndpoint = (query) => `/friends/search/${query}`; -export const getSendFriendRequestEndpoint = (userId) => +export const getRemoveFriendEndpoint = (userId: User['userId']) => `/friends/remove/${userId}`; +export const getSearchFriendsEndpoint = (query: string) => `/friends/search/${query}`; +export const getSendFriendRequestEndpoint = (userId: User['userId']) => `/friends/send_request/${userId}`; export const getFriendRequestsSentEndpoint = () => `/friends/requests_sent`; export const getFriendRequestsReceivedEndpoint = () => `friends/requests_received`; -export const getAcceptFriendRequestEndpoint = (friendRequestId) => +export const getAcceptFriendRequestEndpoint = (friendRequestId: number) => `/friends/accept_request/${friendRequestId}`; -export const getRejectFriendRequestEndpoint = (friendRequestId) => +export const getRejectFriendRequestEndpoint = (friendRequestId: number) => `/friends/reject_request/${friendRequestId}`; -export const getSchoolInfoEndpoint = (school) => `/school/${school}/`; +export const getSchoolInfoEndpoint = (school: string) => `/school/${school}/`; export const getReactToCourseEndpoint = () => "/user/reactions/"; export const getRequestShareTimetableLinkEndpoint = () => "/timetables/links/"; export const acceptTOSEndpoint = () => "/tos/accept/"; -export function getCourseShareLinkFromModal(code, semester) { +export function getCourseShareLinkFromModal(code: Course['code'], semester: Semester) { return `/course/${encodeURIComponent(code)}/${semester.name}/${semester.year}`; } // TODO: ${window.location.href.split('/')[2]} insert above ^ -export function getCourseShareLink(code, semester) { +export function getCourseShareLink(code: Course['code'], semester: Semester) { return `/course/${encodeURIComponent(code)}/${semester.name}/${semester.year}`; } From 96f46d2bfd3924574a5fb7c5d8b577baf66f1264 Mon Sep 17 00:00:00 2001 From: tigerding Date: Sun, 5 May 2024 19:47:50 +0000 Subject: [PATCH 08/11] lint: fix formatting --- static/js/redux/constants/endpoints.ts | 38 ++++++++++++++++++-------- static/js/redux/ui/SideScroller.tsx | 10 ++----- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/static/js/redux/constants/endpoints.ts b/static/js/redux/constants/endpoints.ts index f370d53c0..3f990fb87 100644 --- a/static/js/redux/constants/endpoints.ts +++ b/static/js/redux/constants/endpoints.ts @@ -17,10 +17,14 @@ import { Course, Semester, Slot, Timetable } from "./commonTypes"; /* server endpoints */ export const getLogiCalEndpoint = () => "/user/log_ical/"; -export const getCourseInfoEndpoint = (courseId: Course['id'], semester: string) => +export const getCourseInfoEndpoint = (courseId: Course["id"], semester: string) => `/courses/${semester}/id/${courseId}/`; -export const getCourseSearchEndpoint = (query: string, semester: string, page = 1, limit = 6) => - `/search/${semester}/${query}/?page=${page}&limit=${limit}`; +export const getCourseSearchEndpoint = ( + query: string, + semester: string, + page = 1, + limit = 6 +) => `/search/${semester}/${query}/?page=${page}&limit=${limit}`; export const getTimetablesEndpoint = () => "/timetables/"; export const getLoadSavedTimetablesEndpoint = (semester: Semester) => `/user/timetables/${semester.name}/${semester.year}/`; @@ -28,16 +32,25 @@ export const getSaveTimetableEndpoint = () => "/user/timetables/"; export const getPersonalEventEndpoint = () => "/user/events/"; export const getDeleteTimetableEndpoint = (semester: Semester, name: string) => `/user/timetables/${semester.name}/${semester.year}/${name}/`; -export const getTimetablePreferencesEndpoint = (id: Timetable['id']) => +export const getTimetablePreferencesEndpoint = (id: Timetable["id"]) => `/user/timetables/${id}/preferences/`; export const getSaveSettingsEndpoint = () => "/user/settings/"; -export const getClassmatesEndpoint = (semester: Semester, courses: Array) => +export const getClassmatesEndpoint = ( + semester: Semester, + courses: Array +) => `/user/classmates/${semester.name}/${semester.year}?${$.param({ course_ids: courses, })}`; -export const getClassmatesInCourseEndpoint = (school: string, semester: string, courseId: Course['id']) => - `/course_classmates/${school}/${semester}/id/${courseId}/`; -export const getMostClassmatesCountEndpoint = (semester: Semester, courses: Array) => +export const getClassmatesInCourseEndpoint = ( + school: string, + semester: string, + courseId: Course["id"] +) => `/course_classmates/${school}/${semester}/id/${courseId}/`; +export const getMostClassmatesCountEndpoint = ( + semester: Semester, + courses: Array +) => `/user/classmates/${semester.name}/${semester.year}?${$.param({ course_ids: courses, count: true, @@ -47,9 +60,10 @@ export const getFriendsEndpoint = (semester: Semester) => // Friends endpoints export const getFetchFriendsEndpointEndpoint = () => `/friends/`; -export const getRemoveFriendEndpoint = (userId: User['userId']) => `/friends/remove/${userId}`; +export const getRemoveFriendEndpoint = (userId: User["userId"]) => + `/friends/remove/${userId}`; export const getSearchFriendsEndpoint = (query: string) => `/friends/search/${query}`; -export const getSendFriendRequestEndpoint = (userId: User['userId']) => +export const getSendFriendRequestEndpoint = (userId: User["userId"]) => `/friends/send_request/${userId}`; export const getFriendRequestsSentEndpoint = () => `/friends/requests_sent`; export const getFriendRequestsReceivedEndpoint = () => `friends/requests_received`; @@ -62,12 +76,12 @@ export const getSchoolInfoEndpoint = (school: string) => `/school/${school}/`; export const getReactToCourseEndpoint = () => "/user/reactions/"; export const getRequestShareTimetableLinkEndpoint = () => "/timetables/links/"; export const acceptTOSEndpoint = () => "/tos/accept/"; -export function getCourseShareLinkFromModal(code: Course['code'], semester: Semester) { +export function getCourseShareLinkFromModal(code: Course["code"], semester: Semester) { return `/course/${encodeURIComponent(code)}/${semester.name}/${semester.year}`; } // TODO: ${window.location.href.split('/')[2]} insert above ^ -export function getCourseShareLink(code: Course['code'], semester: Semester) { +export function getCourseShareLink(code: Course["code"], semester: Semester) { return `/course/${encodeURIComponent(code)}/${semester.name}/${semester.year}`; } diff --git a/static/js/redux/ui/SideScroller.tsx b/static/js/redux/ui/SideScroller.tsx index 8b7cd5a93..8dfe1953b 100644 --- a/static/js/redux/ui/SideScroller.tsx +++ b/static/js/redux/ui/SideScroller.tsx @@ -15,7 +15,7 @@ GNU General Public License for more details. import React, { useState } from "react"; interface SideScrollerProps { - content: JSX.Element[], + content: JSX.Element[]; navItems: JSX.Element[]; } @@ -29,11 +29,7 @@ function SideScroller(props: SideScrollerProps) { for (let i = 0; i < props.navItems.length; i++) { const cls = activeSlide === i ? " nav-item-active" : ""; navs.push( - setActiveSlide(i)} - > + setActiveSlide(i)}> {props.navItems[i]} ); @@ -48,4 +44,4 @@ function SideScroller(props: SideScrollerProps) { ); } -export default SideScroller; \ No newline at end of file +export default SideScroller; From 2b795df40905cac8513492c9d50ae9ff4a72ec58 Mon Sep 17 00:00:00 2001 From: tigerding Date: Sun, 13 Oct 2024 18:26:41 -0400 Subject: [PATCH 09/11] refactor: change school type --- static/js/redux/constants/schools.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/redux/constants/schools.ts b/static/js/redux/constants/schools.ts index 0ac6297e2..ac0375282 100644 --- a/static/js/redux/constants/schools.ts +++ b/static/js/redux/constants/schools.ts @@ -25,7 +25,7 @@ export const VALID_SCHOOLS = [ "salisbury", ]; -export const getSchoolSpecificInfo = (school: typeof VALID_SCHOOLS[number]) => { +export const getSchoolSpecificInfo = (school: string) => { switch (school) { case "uoft": return { From a4fb57b10ca29fd76ff58a3a44ac176addb29cfd Mon Sep 17 00:00:00 2001 From: tigerding Date: Sun, 13 Oct 2024 18:44:31 -0400 Subject: [PATCH 10/11] refactor: adapt to endpoints compatibility --- static/js/redux/constants/endpoints.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/static/js/redux/constants/endpoints.ts b/static/js/redux/constants/endpoints.ts index 3f990fb87..1710cb54e 100644 --- a/static/js/redux/constants/endpoints.ts +++ b/static/js/redux/constants/endpoints.ts @@ -12,7 +12,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. */ -import { User } from "../ui/modals/PeerModalComponents/Types"; +import { FriendRequest, User } from "../ui/modals/PeerModalComponents/Types"; import { Course, Semester, Slot, Timetable } from "./commonTypes"; /* server endpoints */ @@ -67,21 +67,21 @@ export const getSendFriendRequestEndpoint = (userId: User["userId"]) => `/friends/send_request/${userId}`; export const getFriendRequestsSentEndpoint = () => `/friends/requests_sent`; export const getFriendRequestsReceivedEndpoint = () => `friends/requests_received`; -export const getAcceptFriendRequestEndpoint = (friendRequestId: number) => +export const getAcceptFriendRequestEndpoint = (friendRequestId: FriendRequest['friendRequestId']) => `/friends/accept_request/${friendRequestId}`; -export const getRejectFriendRequestEndpoint = (friendRequestId: number) => +export const getRejectFriendRequestEndpoint = (friendRequestId: FriendRequest['friendRequestId'] | string) => `/friends/reject_request/${friendRequestId}`; export const getSchoolInfoEndpoint = (school: string) => `/school/${school}/`; export const getReactToCourseEndpoint = () => "/user/reactions/"; export const getRequestShareTimetableLinkEndpoint = () => "/timetables/links/"; export const acceptTOSEndpoint = () => "/tos/accept/"; -export function getCourseShareLinkFromModal(code: Course["code"], semester: Semester) { +export function getCourseShareLinkFromModal(code: Course['code'] | number, semester: Semester) { return `/course/${encodeURIComponent(code)}/${semester.name}/${semester.year}`; } // TODO: ${window.location.href.split('/')[2]} insert above ^ -export function getCourseShareLink(code: Course["code"], semester: Semester) { +export function getCourseShareLink(code: Course['code'] | number, semester: Semester) { return `/course/${encodeURIComponent(code)}/${semester.name}/${semester.year}`; } From 161a362aaa0164d97ecb41b17f52fc938afd9fe5 Mon Sep 17 00:00:00 2001 From: tigerding Date: Sun, 13 Oct 2024 19:30:30 -0400 Subject: [PATCH 11/11] fix lint --- static/js/redux/constants/endpoints.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/static/js/redux/constants/endpoints.ts b/static/js/redux/constants/endpoints.ts index 1710cb54e..6ac4b228d 100644 --- a/static/js/redux/constants/endpoints.ts +++ b/static/js/redux/constants/endpoints.ts @@ -67,21 +67,26 @@ export const getSendFriendRequestEndpoint = (userId: User["userId"]) => `/friends/send_request/${userId}`; export const getFriendRequestsSentEndpoint = () => `/friends/requests_sent`; export const getFriendRequestsReceivedEndpoint = () => `friends/requests_received`; -export const getAcceptFriendRequestEndpoint = (friendRequestId: FriendRequest['friendRequestId']) => - `/friends/accept_request/${friendRequestId}`; -export const getRejectFriendRequestEndpoint = (friendRequestId: FriendRequest['friendRequestId'] | string) => - `/friends/reject_request/${friendRequestId}`; +export const getAcceptFriendRequestEndpoint = ( + friendRequestId: FriendRequest["friendRequestId"] +) => `/friends/accept_request/${friendRequestId}`; +export const getRejectFriendRequestEndpoint = ( + friendRequestId: FriendRequest["friendRequestId"] | string +) => `/friends/reject_request/${friendRequestId}`; export const getSchoolInfoEndpoint = (school: string) => `/school/${school}/`; export const getReactToCourseEndpoint = () => "/user/reactions/"; export const getRequestShareTimetableLinkEndpoint = () => "/timetables/links/"; export const acceptTOSEndpoint = () => "/tos/accept/"; -export function getCourseShareLinkFromModal(code: Course['code'] | number, semester: Semester) { +export function getCourseShareLinkFromModal( + code: Course["code"] | number, + semester: Semester +) { return `/course/${encodeURIComponent(code)}/${semester.name}/${semester.year}`; } // TODO: ${window.location.href.split('/')[2]} insert above ^ -export function getCourseShareLink(code: Course['code'] | number, semester: Semester) { +export function getCourseShareLink(code: Course["code"] | number, semester: Semester) { return `/course/${encodeURIComponent(code)}/${semester.name}/${semester.year}`; }