From b31bfae21b3c182d24df8e8ee66f24dc8049b287 Mon Sep 17 00:00:00 2001 From: Matthew Rowland Date: Fri, 7 Jun 2024 12:26:14 -0700 Subject: [PATCH] feat: Schedule, Calendar --- frontend/src/app/Plan/index.tsx | 2 - .../Schedule/Calendar/Calendar.module.scss | 85 +----- .../Calendar/Week/Day/Day.module.scss | 54 ++++ .../app/Schedule/Calendar/Week/Day/index.tsx | 55 ++++ .../Schedule/Calendar/Week/Week.module.scss | 34 +++ .../src/app/Schedule/Calendar/Week/index.tsx | 43 +++ frontend/src/app/Schedule/Calendar/index.tsx | 248 +++++------------- .../src/app/Schedule/Calendar/semester.ts | 10 + .../Schedule/Schedule/Event/Event.module.scss | 50 ++++ .../src/app/Schedule/Schedule/Event/index.tsx | 62 +++++ .../Schedule/Schedule/Schedule.module.scss | 125 +++++++++ frontend/src/app/Schedule/Schedule/index.tsx | 218 +++++++++++++++ .../Schedule/SideBar/Class/Section/index.tsx | 4 +- .../src/app/Schedule/SideBar/Class/index.tsx | 8 +- frontend/src/app/Schedule/index.tsx | 19 +- frontend/src/components/Browser/index.tsx | 7 - frontend/src/lib/api.ts | 4 + frontend/src/lib/section.ts | 35 +++ 18 files changed, 784 insertions(+), 279 deletions(-) create mode 100644 frontend/src/app/Schedule/Calendar/Week/Day/Day.module.scss create mode 100644 frontend/src/app/Schedule/Calendar/Week/Day/index.tsx create mode 100644 frontend/src/app/Schedule/Calendar/Week/Week.module.scss create mode 100644 frontend/src/app/Schedule/Calendar/Week/index.tsx create mode 100644 frontend/src/app/Schedule/Calendar/semester.ts create mode 100644 frontend/src/app/Schedule/Schedule/Event/Event.module.scss create mode 100644 frontend/src/app/Schedule/Schedule/Event/index.tsx create mode 100644 frontend/src/app/Schedule/Schedule/Schedule.module.scss create mode 100644 frontend/src/app/Schedule/Schedule/index.tsx diff --git a/frontend/src/app/Plan/index.tsx b/frontend/src/app/Plan/index.tsx index dab143e44..b7a12a853 100644 --- a/frontend/src/app/Plan/index.tsx +++ b/frontend/src/app/Plan/index.tsx @@ -104,8 +104,6 @@ export default function Plan() { }; }, []); - console.log(terms); - return (
diff --git a/frontend/src/app/Schedule/Calendar/Calendar.module.scss b/frontend/src/app/Schedule/Calendar/Calendar.module.scss index 9b6401a3f..d98f2f811 100644 --- a/frontend/src/app/Schedule/Calendar/Calendar.module.scss +++ b/frontend/src/app/Schedule/Calendar/Calendar.module.scss @@ -16,19 +16,6 @@ line-height: 1; z-index: 2; - .timeZone { - height: 32px; - width: 64px; - display: flex; - align-items: center; - justify-content: flex-end; - padding: 0 12px; - border-right: 1px solid var(--border-color); - position: sticky; - left: 0; - background-color: var(--background-color); - } - .week { display: grid; grid-template-columns: repeat(7, 1fr); @@ -49,77 +36,7 @@ .view { display: flex; + flex-direction: column; z-index: 1; - - .week { - flex-grow: 1; - display: grid; - grid-template-columns: repeat(7, 1fr); - - .day { - position: relative; - - &:not(:last-child) { - border-right: 1px solid var(--border-color); - } - - .line { - position: absolute; - z-index: 1; - left: -1px; - width: calc(100% + 1px); - height: 1px; - background-color: var(--red-500); - } - - .hour { - height: 60px; - display: flex; - align-items: center; - justify-content: center; - - &:not(:first-child) { - border-top: 1px solid var(--border-color); - } - } - } - } - - .sideBar { - flex-shrink: 0; - width: 64px; - display: flex; - flex-direction: column; - position: sticky; - left: 0; - z-index: 2; - background-color: var(--background-color); - border-right: 1px solid var(--border-color); - - .time { - position: absolute; - z-index: 1; - height: 24px; - display: flex; - align-items: center; - padding: 0 12px; - font-size: 12px; - color: white; - line-height: 1; - border-radius: 4px; - right: 0; - background-color: var(--red-500); - } - - .hour { - height: 0; - margin-top: 60px; - padding: 0 12px; - font-size: 12px; - color: var(--label-color); - text-align: right; - transform: translateY(-6px); - } - } } } \ No newline at end of file diff --git a/frontend/src/app/Schedule/Calendar/Week/Day/Day.module.scss b/frontend/src/app/Schedule/Calendar/Week/Day/Day.module.scss new file mode 100644 index 000000000..5bf0825cd --- /dev/null +++ b/frontend/src/app/Schedule/Calendar/Week/Day/Day.module.scss @@ -0,0 +1,54 @@ +.root { + aspect-ratio: 1; + padding: 8px; + display: flex; + flex-direction: column; + font-size: 12px; + line-height: 1; + gap: 4px; + overflow: hidden; + + &:not(:last-child) { + border-right: 1px solid var(--border-color); + } + + .event { + display: flex; + align-items: center; + padding: 0 8px; + border-radius: 4px; + gap: 8px; + height: 24px; + white-space: nowrap; + + &.active { + opacity: 0.5; + } + + .time { + color: rgb(255 255 255 / 75%); + font-size: 8px; + } + + .course { + color: white; + font-weight: 500; + text-overflow: ellipsis; + overflow: hidden; + } + } + + .date { + display: flex; + + .month { + color: var(--heading-color); + font-weight: 500; + } + + .number { + margin-left: auto; + color: var(--label-color); + } + } +} \ No newline at end of file diff --git a/frontend/src/app/Schedule/Calendar/Week/Day/index.tsx b/frontend/src/app/Schedule/Calendar/Week/Day/index.tsx new file mode 100644 index 000000000..96815d6e9 --- /dev/null +++ b/frontend/src/app/Schedule/Calendar/Week/Day/index.tsx @@ -0,0 +1,55 @@ +import classNames from "classnames"; + +import { getColor } from "@/lib/section"; + +import { IDay } from "../../semester"; +import styles from "./Day.module.scss"; + +const parseTime = (time: string) => { + const [hours, minutes] = time.split(":").map(Number); + + let _time = `${hours % 12 || 12}`; + if (minutes > 0) _time += `:${minutes.toString().padStart(2, "0")}`; + _time += hours < 12 ? " AM" : " PM"; + + return _time; +}; + +interface DayProps extends IDay { + active: boolean; +} + +export default function Day({ date, events, active }: DayProps) { + return ( +
+ {active && ( +
+ {date.format("D") === "1" && ( +

{date.format("MMMM")}

+ )} +

{date.format("D")}

+
+ )} + {events.map((event) => ( +
+
+ {parseTime(event.meetings[0].startTime)} +
+
+ {event.course.subject} {event.course.number} +
+
+ ))} +
+ ); +} diff --git a/frontend/src/app/Schedule/Calendar/Week/Week.module.scss b/frontend/src/app/Schedule/Calendar/Week/Week.module.scss new file mode 100644 index 000000000..cbe972758 --- /dev/null +++ b/frontend/src/app/Schedule/Calendar/Week/Week.module.scss @@ -0,0 +1,34 @@ +.root { + display: flex; + flex-direction: column; + + &:not(:first-child) { + border-top: 1px solid var(--border-color); + } + + &.dead { + background-color: var(--slate-100); + } + + &.finals { + background-color: white; + } + + .header { + font-size: 12px; + line-height: 1; + height: 32px; + color: var(--paragraph-color); + padding: 0 12px; + display: flex; + gap: 12px; + align-items: center; + border-bottom: 1px solid var(--border-color); + } + + .body { + flex-grow: 1; + display: grid; + grid-template-columns: repeat(7, 1fr); + } +} \ No newline at end of file diff --git a/frontend/src/app/Schedule/Calendar/Week/index.tsx b/frontend/src/app/Schedule/Calendar/Week/index.tsx new file mode 100644 index 000000000..e0a7ec3f6 --- /dev/null +++ b/frontend/src/app/Schedule/Calendar/Week/index.tsx @@ -0,0 +1,43 @@ +import classNames from "classnames"; +import { ArrowDown } from "iconoir-react"; +import moment from "moment"; + +import { IDay } from "../semester"; +import Day from "./Day"; +import styles from "./Week.module.scss"; + +interface WeekProps { + days: IDay[]; + finals: boolean; + dead: boolean; + first: moment.Moment; + last: moment.Moment; +} + +export default function Week({ days, finals, dead, first, last }: WeekProps) { + return ( +
+ {(dead || finals) && ( +
+ + {dead ? "Reading, Review, and Recitation (RRR)" : "Finals"} week +
+ )} +
+ {days.map(({ date, events }) => ( + + ))} +
+
+ ); +} diff --git a/frontend/src/app/Schedule/Calendar/index.tsx b/frontend/src/app/Schedule/Calendar/index.tsx index 6bb762631..7d89fec3c 100644 --- a/frontend/src/app/Schedule/Calendar/index.tsx +++ b/frontend/src/app/Schedule/Calendar/index.tsx @@ -1,47 +1,12 @@ -import { MouseEvent, useMemo, useRef, useState } from "react"; +import { useMemo, useState } from "react"; -import { ISection } from "@/lib/api"; -import { getY } from "@/lib/schedule"; - -import styles from "./Calendar.module.scss"; -import Event from "./Event"; - -const getId = (section: ISection) => - `${section.course.subject} ${section.course.number} ${section.class.number} ${section.number}`; - -// You have to trust me on this math -const adjustAttachedEvents = ( - relevantSections: ISection[], - attachedSections: string[], - minutes: string[][], - positions: Record -) => { - const adjustedSections: string[] = []; - - const adjustSection = (id: string) => { - if (adjustedSections.includes(id)) return; - - adjustedSections.push(id); - - positions[id][1]++; +import moment from "moment"; - const section = relevantSections.find((section) => id === getId(section)); - if (!section) return; - - const top = getY(section.meetings[0].startTime); - const height = getY(section.meetings[0].endTime) - top; - - for (let i = top; i < top + height; i++) { - for (const id of minutes[i]) { - adjustSection(id); - } - } - }; +import { ISection } from "@/lib/api"; - for (const id of attachedSections) { - adjustSection(id); - } -}; +import styles from "./Semester.module.scss"; +import Week from "./Week"; +import { IDay } from "./semester"; interface CalendarProps { selectedSections: ISection[]; @@ -52,122 +17,68 @@ export default function Calendar({ selectedSections, currentSection, }: CalendarProps) { - const viewRef = useRef(null); - const [y, setY] = useState(null); - const sections = useMemo( () => currentSection ? [...selectedSections, currentSection] : selectedSections, [selectedSections, currentSection] ); - const days = useMemo( - () => - [...Array(7)].map((_, day) => { - const positions: Record = {}; - const minutes: string[][] = [...Array(60 * 18)].map(() => []); - - const relevantSections = sections - // Filter sections for the current day which have a time specified - .filter( - (section) => - section.meetings[0].days[day] && - section.meetings[0].startTime && - getY(section.meetings[0].startTime) > 0 - ) - // Sort sections by when they start - .sort( - (a, b) => - getY(a.meetings[0].startTime) - getY(b.meetings[0].startTime) - ); - - // Maintain an array of sections that are attached to each minute - for (const section of relevantSections) { - const top = getY(section.meetings[0].startTime); - const height = getY(section.meetings[0].endTime) - top; - - const attachedSections = minutes[top]; - - let position = 0; - - while ( - attachedSections.findIndex( - (eventId) => positions[eventId][0] === position - ) !== -1 - ) { - position++; - } - - if ( - attachedSections.length > 0 && - Math.max( - position, - ...attachedSections.map((eventId) => positions[eventId][0]) - ) === position - ) { - adjustAttachedEvents( - relevantSections, - attachedSections, - minutes, - positions - ); - } - - const id = getId(section); - - positions[id] = [ - position, - attachedSections.length === 0 - ? 1 - : positions[attachedSections[0]][1], - ]; - - for (let i = top; i < top + height; i++) { - minutes[i].push(id); - } - } - - return relevantSections.map((section) => { - const [position, columns] = positions[getId(section)]; - - return { - ...section, - position, - active: section.ccn !== currentSection?.ccn, - columns, - }; - }); - }), - [sections, currentSection] - ); - - const currentTime = useMemo(() => { - if (!viewRef.current || !y) return; - - const hour = (Math.floor(y / 60) + 6) % 12 || 12; - const minute = Math.floor(y % 60); - - return `${hour}:${minute < 10 ? `0${minute}` : minute}`; - }, [y]); - - const updateY = (event: MouseEvent) => { - if (!viewRef.current) return; + const [first] = useState(() => moment("2024-01-01")); + const [last] = useState(() => moment("2024-05-31")); + + const [start] = useState(() => { + const current = moment("2024-01-01"); + current.subtract(current.day(), "days"); + return current; + }); + + const [stop] = useState(() => { + const stop = moment("2024-05-31"); + stop.add(6 - stop.day(), "days"); + return stop; + }); + + const weeks = useMemo(() => { + const weeks: IDay[][] = []; + + const current = moment(start); + + while (current.isSameOrBefore(stop)) { + const week = []; + + for (let i = 0; i < 7; i++) { + const day = { + date: moment(current), + events: sections + .filter( + ({ startDate, endDate, meetings }) => + current.isSameOrAfter(startDate) && + current.isSameOrBefore(endDate) && + meetings[0]?.days[current.day()] + ) + .sort((a, b) => + a.meetings[0].startTime.localeCompare(b.meetings[0].startTime) + ) + .map((section) => ({ + ...section, + active: section.ccn === currentSection?.ccn, + })), + }; + + week.push(day); + + current.add(1, "days"); + } - const y = Math.max( - 15, - Math.min( - event.clientY - viewRef.current.getBoundingClientRect().top, - viewRef.current.clientHeight - 15 - ) - ); + weeks.push(week); + } - setY(y); - }; + return weeks; + }, [sections, currentSection, start, stop]); return (
-
PST
Sunday
Monday
@@ -178,40 +89,19 @@ export default function Calendar({
Saturday
-
setY(null)} - > -
- {currentTime && y && ( -
- {currentTime} -
- )} - {[...Array(17)].map((_, hour) => ( -
- {hour + 7 < 12 - ? `${hour + 7} AM` - : `${hour + 7 === 12 ? 12 : hour - 5} PM`} -
- ))} -
-
- {days.map((events, day) => ( -
- {[...Array(18)].map((_, hour) => ( -
- ))} - {events.map((event) => ( - - ))} - {y &&
} -
- ))} -
+
+ {weeks.map((days, index) => { + return ( + + ); + })}
); diff --git a/frontend/src/app/Schedule/Calendar/semester.ts b/frontend/src/app/Schedule/Calendar/semester.ts new file mode 100644 index 000000000..d156625d5 --- /dev/null +++ b/frontend/src/app/Schedule/Calendar/semester.ts @@ -0,0 +1,10 @@ +import { ISection } from "@/lib/api"; + +interface IEvent extends ISection { + active?: boolean; +} + +export interface IDay { + date: moment.Moment; + events: IEvent[]; +} diff --git a/frontend/src/app/Schedule/Schedule/Event/Event.module.scss b/frontend/src/app/Schedule/Schedule/Event/Event.module.scss new file mode 100644 index 000000000..fbb8078f9 --- /dev/null +++ b/frontend/src/app/Schedule/Schedule/Event/Event.module.scss @@ -0,0 +1,50 @@ +.trigger { + position: absolute; + z-index: 2; + padding: 8px; + border-radius: 4px; + background-color: var(--blue-500); + font-size: 12px; + opacity: 0.5; + + &.active { + opacity: 1; + } + + .heading { + font-weight: 500; + color: white; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .description { + margin-top: 4px; + color: rgb(255 255 255 / 75%); + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } +} + +.content { + height: 128px; + width: 320px; + background-color: var(--foreground-color); + border-radius: 8px; + box-shadow: 0 0 16px rgb(0 0 0 / 10%); + z-index: 989; + opacity: 0; + animation: fadeIn 100ms ease-in-out forwards; + + .arrow { + fill: var(--foreground-color); + } +} + +@keyframes fadeIn { + to { + opacity: 1; + } +} \ No newline at end of file diff --git a/frontend/src/app/Schedule/Schedule/Event/index.tsx b/frontend/src/app/Schedule/Schedule/Event/index.tsx new file mode 100644 index 000000000..423ce3663 --- /dev/null +++ b/frontend/src/app/Schedule/Schedule/Event/index.tsx @@ -0,0 +1,62 @@ +import { useMemo } from "react"; + +import * as HoverCard from "@radix-ui/react-hover-card"; +import classNames from "classnames"; + +import { ISection } from "@/lib/api"; +import { getY } from "@/lib/schedule"; +import { getColor } from "@/lib/section"; + +import styles from "./Event.module.scss"; + +interface EventProps { + columns: number; + position: number; + active: boolean; +} + +export default function Event({ + columns, + position, + meetings: [{ startTime, endTime }], + course, + component, + number, + active, +}: EventProps & ISection) { + const top = useMemo(() => getY(startTime!), [startTime]); + + const height = useMemo(() => getY(endTime!) - top + 1, [top, endTime]); + + // TODO: Hover card content + return ( + + +
+
+ {course.subject} {course.number} +
+
+ {component} {number} +
+
+
+ + +
+ +
+
+
+
+ ); +} diff --git a/frontend/src/app/Schedule/Schedule/Schedule.module.scss b/frontend/src/app/Schedule/Schedule/Schedule.module.scss new file mode 100644 index 000000000..9b6401a3f --- /dev/null +++ b/frontend/src/app/Schedule/Schedule/Schedule.module.scss @@ -0,0 +1,125 @@ +.root { + display: flex; + flex-direction: column; + background-color: var(--background-color); + min-width: 1080px; + + .header { + display: flex; + border-bottom: 1px solid var(--border-color); + position: sticky; + top: 0; + background-color: var(--background-color); + z-index: 1; + font-size: 12px; + color: var(--label-color); + line-height: 1; + z-index: 2; + + .timeZone { + height: 32px; + width: 64px; + display: flex; + align-items: center; + justify-content: flex-end; + padding: 0 12px; + border-right: 1px solid var(--border-color); + position: sticky; + left: 0; + background-color: var(--background-color); + } + + .week { + display: grid; + grid-template-columns: repeat(7, 1fr); + flex-grow: 1; + + .day { + height: 32px; + display: flex; + align-items: center; + justify-content: center; + + &:not(:last-child) { + border-right: 1px solid var(--border-color); + } + } + } + } + + .view { + display: flex; + z-index: 1; + + .week { + flex-grow: 1; + display: grid; + grid-template-columns: repeat(7, 1fr); + + .day { + position: relative; + + &:not(:last-child) { + border-right: 1px solid var(--border-color); + } + + .line { + position: absolute; + z-index: 1; + left: -1px; + width: calc(100% + 1px); + height: 1px; + background-color: var(--red-500); + } + + .hour { + height: 60px; + display: flex; + align-items: center; + justify-content: center; + + &:not(:first-child) { + border-top: 1px solid var(--border-color); + } + } + } + } + + .sideBar { + flex-shrink: 0; + width: 64px; + display: flex; + flex-direction: column; + position: sticky; + left: 0; + z-index: 2; + background-color: var(--background-color); + border-right: 1px solid var(--border-color); + + .time { + position: absolute; + z-index: 1; + height: 24px; + display: flex; + align-items: center; + padding: 0 12px; + font-size: 12px; + color: white; + line-height: 1; + border-radius: 4px; + right: 0; + background-color: var(--red-500); + } + + .hour { + height: 0; + margin-top: 60px; + padding: 0 12px; + font-size: 12px; + color: var(--label-color); + text-align: right; + transform: translateY(-6px); + } + } + } +} \ No newline at end of file diff --git a/frontend/src/app/Schedule/Schedule/index.tsx b/frontend/src/app/Schedule/Schedule/index.tsx new file mode 100644 index 000000000..04e185fda --- /dev/null +++ b/frontend/src/app/Schedule/Schedule/index.tsx @@ -0,0 +1,218 @@ +import { MouseEvent, useMemo, useRef, useState } from "react"; + +import { ISection } from "@/lib/api"; +import { getY } from "@/lib/schedule"; + +import Event from "./Event"; +import styles from "./Schedule.module.scss"; + +const getId = (section: ISection) => + `${section.course.subject} ${section.course.number} ${section.class.number} ${section.number}`; + +// You have to trust me on this math +const adjustAttachedEvents = ( + relevantSections: ISection[], + attachedSections: string[], + minutes: string[][], + positions: Record +) => { + const adjustedSections: string[] = []; + + const adjustSection = (id: string) => { + if (adjustedSections.includes(id)) return; + + adjustedSections.push(id); + + positions[id][1]++; + + const section = relevantSections.find((section) => id === getId(section)); + if (!section) return; + + const top = getY(section.meetings[0].startTime); + const height = getY(section.meetings[0].endTime) - top; + + for (let i = top; i < top + height; i++) { + for (const id of minutes[i]) { + adjustSection(id); + } + } + }; + + for (const id of attachedSections) { + adjustSection(id); + } +}; + +interface CalendarProps { + selectedSections: ISection[]; + currentSection: ISection | null; +} + +export default function Schedule({ + selectedSections, + currentSection, +}: CalendarProps) { + const viewRef = useRef(null); + const [y, setY] = useState(null); + + const sections = useMemo( + () => + currentSection ? [...selectedSections, currentSection] : selectedSections, + [selectedSections, currentSection] + ); + + const days = useMemo( + () => + [...Array(7)].map((_, day) => { + const positions: Record = {}; + const minutes: string[][] = [...Array(60 * 18)].map(() => []); + + const relevantSections = sections + // Filter sections for the current day which have a time specified + .filter( + (section) => + section.meetings[0].days[day] && + section.meetings[0].startTime && + getY(section.meetings[0].startTime) > 0 + ) + // Sort sections by when they start + .sort( + (a, b) => + getY(a.meetings[0].startTime) - getY(b.meetings[0].startTime) + ); + + // Maintain an array of sections that are attached to each minute + for (const section of relevantSections) { + const top = getY(section.meetings[0].startTime); + const height = getY(section.meetings[0].endTime) - top; + + const attachedSections = minutes[top]; + + let position = 0; + + while ( + attachedSections.findIndex( + (eventId) => positions[eventId][0] === position + ) !== -1 + ) { + position++; + } + + if ( + attachedSections.length > 0 && + Math.max( + position, + ...attachedSections.map((eventId) => positions[eventId][0]) + ) === position + ) { + adjustAttachedEvents( + relevantSections, + attachedSections, + minutes, + positions + ); + } + + const id = getId(section); + + positions[id] = [ + position, + attachedSections.length === 0 + ? 1 + : positions[attachedSections[0]][1], + ]; + + for (let i = top; i < top + height; i++) { + minutes[i].push(id); + } + } + + return relevantSections.map((section) => { + const [position, columns] = positions[getId(section)]; + + return { + ...section, + position, + active: section.ccn !== currentSection?.ccn, + columns, + }; + }); + }), + [sections, currentSection] + ); + + const currentTime = useMemo(() => { + if (!viewRef.current || !y) return; + + const hour = (Math.floor(y / 60) + 6) % 12 || 12; + const minute = Math.floor(y % 60); + + return `${hour}:${minute < 10 ? `0${minute}` : minute}`; + }, [y]); + + const updateY = (event: MouseEvent) => { + if (!viewRef.current) return; + + const y = Math.max( + 15, + Math.min( + event.clientY - viewRef.current.getBoundingClientRect().top, + viewRef.current.clientHeight - 15 + ) + ); + + setY(y); + }; + + return ( +
+
+
PST
+
+
Sunday
+
Monday
+
Tuesday
+
Wednesday
+
Thursday
+
Friday
+
Saturday
+
+
+
setY(null)} + > +
+ {currentTime && y && ( +
+ {currentTime} +
+ )} + {[...Array(17)].map((_, hour) => ( +
+ {hour + 7 < 12 + ? `${hour + 7} AM` + : `${hour + 7 === 12 ? 12 : hour - 5} PM`} +
+ ))} +
+
+ {days.map((events, day) => ( +
+ {[...Array(18)].map((_, hour) => ( +
+ ))} + {events.map((event) => ( + + ))} + {y &&
} +
+ ))} +
+
+
+ ); +} diff --git a/frontend/src/app/Schedule/SideBar/Class/Section/index.tsx b/frontend/src/app/Schedule/SideBar/Class/Section/index.tsx index b4ff91586..d371e0023 100644 --- a/frontend/src/app/Schedule/SideBar/Class/Section/index.tsx +++ b/frontend/src/app/Schedule/SideBar/Class/Section/index.tsx @@ -36,8 +36,8 @@ export default function Section({

{number}