diff --git a/apps/backend/src/modules/schedule/typedefs/schedule.ts b/apps/backend/src/modules/schedule/typedefs/schedule.ts index 14d7c382f..e32f2addb 100644 --- a/apps/backend/src/modules/schedule/typedefs/schedule.ts +++ b/apps/backend/src/modules/schedule/typedefs/schedule.ts @@ -3,7 +3,7 @@ import { gql } from "graphql-tag"; const typedef = gql` type SelectedClass { class: Class! - selectedSections: [String!] + selectedSections: [Int!] } type Event { @@ -16,7 +16,7 @@ const typedef = gql` } type Schedule { - _id: ID + _id: ID! name: String! createdBy: String! year: Int! @@ -45,7 +45,7 @@ const typedef = gql` subject: String! courseNumber: String! number: String! - sections: [String!]! + sections: [Int!]! } input UpdateScheduleInput { diff --git a/apps/frontend/schema.graphql b/apps/frontend/schema.graphql index af63ffd82..4875ca3f4 100644 --- a/apps/frontend/schema.graphql +++ b/apps/frontend/schema.graphql @@ -57,6 +57,13 @@ type Query { classNumber: String! number: String! ): Section + enrollment( + year: Int! + semester: Semester! + subject: String! + courseNumber: String! + number: String! + ): Section! } input BookmarkedCourseInput { @@ -131,7 +138,7 @@ enum Semester { type SelectedClass { class: Class! - selectedSections: [String!] + selectedSections: [Int!] } type Event { @@ -144,7 +151,7 @@ type Event { } type Schedule { - _id: ID + _id: ID! name: String! createdBy: String! year: Int! @@ -168,7 +175,7 @@ input SelectedClassInput { subject: String! courseNumber: String! number: String! - sections: [String!]! + sections: [Int!]! } input UpdateScheduleInput { diff --git a/apps/frontend/src/app/Plan/Term/Catalog/Catalog.module.scss b/apps/frontend/src/app/Plan/Term/Catalog/Catalog.module.scss index e32621815..b538d42fc 100644 --- a/apps/frontend/src/app/Plan/Term/Catalog/Catalog.module.scss +++ b/apps/frontend/src/app/Plan/Term/Catalog/Catalog.module.scss @@ -1,9 +1,7 @@ .content { height: 100dvh; - position: fixed; left: 0; top: 0; - z-index: 989; display: flex; flex-direction: column; diff --git a/apps/frontend/src/app/Schedule/Editor/SideBar/Catalog/Catalog.module.scss b/apps/frontend/src/app/Schedule/Editor/SideBar/Catalog/Catalog.module.scss index 6bb641259..3fe96c55e 100644 --- a/apps/frontend/src/app/Schedule/Editor/SideBar/Catalog/Catalog.module.scss +++ b/apps/frontend/src/app/Schedule/Editor/SideBar/Catalog/Catalog.module.scss @@ -1,9 +1,7 @@ .content { height: 100dvh; - position: fixed; left: 0; top: 0; - z-index: 989; display: flex; flex-direction: column; diff --git a/apps/frontend/src/app/Schedule/Editor/SideBar/Class/Class.module.scss b/apps/frontend/src/app/Schedule/Editor/SideBar/Class/Class.module.scss index 0192ffe32..6a8600774 100644 --- a/apps/frontend/src/app/Schedule/Editor/SideBar/Class/Class.module.scss +++ b/apps/frontend/src/app/Schedule/Editor/SideBar/Class/Class.module.scss @@ -1,6 +1,27 @@ .root { display: flex; + &:global(.draggable-mirror) { + border-color: var(--blue-500); + z-index: 999; + box-shadow: 0 4px 16px rgb(0 0 0 / 10%); + } + + &:global(.draggable-source--is-dragging) { + background-color: transparent; + box-shadow: unset; + border-style: dashed; + + .body { + visibility: hidden; + } + } + + &:not(:global(.draggable-source--is-dragging)) { + box-shadow: 0 1px 2px rgb(0 0 0 / 2.5%); + background-color: var(--foreground-color); + } + .border { width: 8px; background-color: var(--purple-500); @@ -100,4 +121,4 @@ } } } -} \ No newline at end of file +} diff --git a/apps/frontend/src/app/Schedule/Editor/SideBar/Class/index.tsx b/apps/frontend/src/app/Schedule/Editor/SideBar/Class/index.tsx index 99f8d315a..00c27a737 100644 --- a/apps/frontend/src/app/Schedule/Editor/SideBar/Class/index.tsx +++ b/apps/frontend/src/app/Schedule/Editor/SideBar/Class/index.tsx @@ -49,7 +49,7 @@ export default function Class({ }, [_class]); return ( -
+
void; onClassSelect: ( subject: string, courseNumber: string, @@ -44,9 +46,39 @@ export default function SideBar({ onSectionMouseOver, onSectionMouseOut, onExpandedChange, + onSortEnd, }: SideBarProps) { + const bodyRef = useRef(null); + const [minimum, maximum] = useMemo(() => getUnits(schedule), [schedule]); + useEffect(() => { + if (!bodyRef.current) return; + + const sortable = new Sortable(bodyRef.current, { + draggable: `[data-draggable]`, + distance: 8, + mirror: { + constrainDimensions: true, + }, + plugins: [Plugins.ResizeMirror], + }); + + sortable.on("drag:stop", (event) => { + event.cancel(); + }); + + sortable.on("sortable:stop", (event) => { + const { oldIndex, newIndex } = event; + + onSortEnd(oldIndex, newIndex); + }); + + return () => { + sortable.destroy(); + }; + }, [onSortEnd]); + return (
@@ -73,7 +105,7 @@ export default function SideBar({
-
+
{schedule.classes.map((selectedClass, index) => { return ( { + // Clone the schedule for immutability + const _schedule = structuredClone(schedule); + + const [removed] = _schedule.classes.splice(previousIndex, 1); + + _schedule.classes.splice(currentIndex, 0, removed); + + // Update the schedule + updateSchedule( + schedule._id, + { + classes: _schedule.classes.map( + ({ + selectedSections, + class: { number, subject, courseNumber }, + }) => ({ + subject, + courseNumber, + number, + sections: selectedSections, + }) + ), + }, + { + optimisticResponse: { + updateSchedule: _schedule, + }, + } + ); + }, + [schedule] + ); + const handleClassSelect = useCallback( async (subject: string, courseNumber: string, number: string) => { // Clone the schedule for immutability @@ -178,6 +213,8 @@ export default function Editor() { // Move existing classes to the top rather than duplicating them if (existingClass) { + console.log("existingClass", existingClass); + const index = _schedule.classes.findIndex( (selectedClass) => selectedClass.class.subject === subject && @@ -282,7 +319,7 @@ export default function Editor() { } ); }, - [apolloClient, setExpanded] + [apolloClient, setExpanded, schedule] ); const handleExpandedChange = (index: number, expanded: boolean) => { @@ -347,6 +384,7 @@ export default function Editor() { onExpandedChange={handleExpandedChange} onSectionMouseOver={handleSectionMouseOver} onSectionMouseOut={() => setCurrentSection(null)} + onSortEnd={handleSortEnd} />
{tab === 0 ? ( diff --git a/apps/frontend/src/components/ClassDrawer/ClassDrawer.module.scss b/apps/frontend/src/components/ClassDrawer/ClassDrawer.module.scss index bd6fbf9ce..f331f8467 100644 --- a/apps/frontend/src/components/ClassDrawer/ClassDrawer.module.scss +++ b/apps/frontend/src/components/ClassDrawer/ClassDrawer.module.scss @@ -1,12 +1,10 @@ .content { height: 100dvh; - position: fixed; right: 0; top: 0; background-color: var(--background-color); width: 640px; border-left: 1px solid var(--border-color); - z-index: 989; display: flex; flex-direction: column; diff --git a/apps/frontend/src/components/CourseDrawer/CourseDrawer.module.scss b/apps/frontend/src/components/CourseDrawer/CourseDrawer.module.scss index f4189d55c..c3465476e 100644 --- a/apps/frontend/src/components/CourseDrawer/CourseDrawer.module.scss +++ b/apps/frontend/src/components/CourseDrawer/CourseDrawer.module.scss @@ -1,12 +1,10 @@ .content { height: 100dvh; - position: fixed; right: 0; top: 0; background-color: var(--background-color); width: 640px; border-left: 1px solid var(--border-color); - z-index: 989; display: flex; flex-direction: column; diff --git a/apps/frontend/src/components/Layout/Feedback/Feedback.module.scss b/apps/frontend/src/components/Layout/Feedback/Feedback.module.scss index 12c5a93f5..75e1197ec 100644 --- a/apps/frontend/src/components/Layout/Feedback/Feedback.module.scss +++ b/apps/frontend/src/components/Layout/Feedback/Feedback.module.scss @@ -14,16 +14,11 @@ height: 48px; transform: translateX(calc(100% - 48px)); pointer-events: auto; + transition: transform 150ms ease-out; &:hover, &:focus { - animation: slideOut 250ms ease-in-out forwards; + transform: translateX(0); } } } - -@keyframes slideOut { - to { - transform: translateY(0); - } -} diff --git a/apps/frontend/src/components/Layout/Pins/Pins.module.scss b/apps/frontend/src/components/Layout/Pins/Pins.module.scss deleted file mode 100644 index 66b6415b3..000000000 --- a/apps/frontend/src/components/Layout/Pins/Pins.module.scss +++ /dev/null @@ -1,26 +0,0 @@ -.overlay { - background: linear-gradient(to left, rgb(255 255 255 / 50%), transparent); - inset: 0; - position: fixed; - z-index: 988; -} - -.content { - width: 384px; - border-left: 1px solid var(--border-color); - flex-shrink: 0; - position: fixed; - height: 100dvh; - background-color: var(--background-color); - right: 0; - top: 0; - z-index: 989; - - .header { - padding: 12px; - border-bottom: 1px solid var(--border-color); - background-color: var(--foreground-color); - display: flex; - gap: 12px; - } -} diff --git a/apps/frontend/src/components/Layout/PinsDrawer/PinsDrawer.module.scss b/apps/frontend/src/components/Layout/PinsDrawer/PinsDrawer.module.scss new file mode 100644 index 000000000..98b6e8faf --- /dev/null +++ b/apps/frontend/src/components/Layout/PinsDrawer/PinsDrawer.module.scss @@ -0,0 +1,86 @@ +.overlay { + background: linear-gradient( + to left, + rgb(255 255 255 / 50%) 384px, + transparent + ); + inset: 0; + position: fixed; + z-index: 988; + + &[data-state="open"] { + animation: fadeIn 250ms ease-in; + } + + &[data-state="closed"] { + animation: fadeOut 250ms ease-out; + } +} + +.content { + width: 384px; + border-left: 1px solid var(--border-color); + flex-shrink: 0; + position: fixed; + height: 100dvh; + background-color: var(--background-color); + right: 0; + top: 0; + z-index: 989; + + &[data-state="open"] { + animation: slideIn 150ms ease-out; + } + + &[data-state="closed"] { + animation: slideOut 150ms ease-in; + } + + .header { + padding: 12px; + border-bottom: 1px solid var(--border-color); + background-color: var(--foreground-color); + display: flex; + gap: 12px; + } +} + +@keyframes slideIn { + from { + transform: translateX(100%); + } + + to { + transform: translateX(0); + } +} + +@keyframes slideOut { + from { + transform: translateX(0); + } + + to { + transform: translateX(100%); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} diff --git a/apps/frontend/src/components/Layout/Pins/index.tsx b/apps/frontend/src/components/Layout/PinsDrawer/index.tsx similarity index 96% rename from apps/frontend/src/components/Layout/Pins/index.tsx rename to apps/frontend/src/components/Layout/PinsDrawer/index.tsx index 3d69aa0c5..ecad1845e 100644 --- a/apps/frontend/src/components/Layout/Pins/index.tsx +++ b/apps/frontend/src/components/Layout/PinsDrawer/index.tsx @@ -7,7 +7,7 @@ import { Button, IconButton } from "@repo/theme"; import usePins from "@/hooks/usePins"; -import styles from "./Pins.module.scss"; +import styles from "./PinsDrawer.module.scss"; interface PinsDrawerProps { children: ReactNode; diff --git a/apps/frontend/src/components/Layout/index.tsx b/apps/frontend/src/components/Layout/index.tsx index b495f97f4..96de66978 100644 --- a/apps/frontend/src/components/Layout/index.tsx +++ b/apps/frontend/src/components/Layout/index.tsx @@ -19,7 +19,7 @@ interface LayoutProps { export default function Layout({ header = true, footer = true, - feedback, + feedback = true, }: LayoutProps) { return (
diff --git a/apps/frontend/src/components/NavigationBar/SideBar/SideBar.module.scss b/apps/frontend/src/components/NavigationBar/SideBar/SideBar.module.scss index fcc3d656d..1b841dfbd 100644 --- a/apps/frontend/src/components/NavigationBar/SideBar/SideBar.module.scss +++ b/apps/frontend/src/components/NavigationBar/SideBar/SideBar.module.scss @@ -1,11 +1,9 @@ .content { height: 100dvh; - position: fixed; left: 0; top: 0; background-color: var(--foreground-color); width: 288px; - z-index: 989; display: flex; flex-direction: column; diff --git a/apps/frontend/src/components/NavigationBar/index.tsx b/apps/frontend/src/components/NavigationBar/index.tsx index 644ce5170..5a87491b4 100644 --- a/apps/frontend/src/components/NavigationBar/index.tsx +++ b/apps/frontend/src/components/NavigationBar/index.tsx @@ -7,7 +7,7 @@ import { Button, IconButton, MenuItem } from "@repo/theme"; import { useReadUser } from "@/hooks/api"; import { signIn, signOut } from "@/lib/api"; -import PinsDrawer from "../Layout/Pins"; +import PinsDrawer from "../Layout/PinsDrawer"; import styles from "./NavigationBar.module.scss"; import SideBar from "./SideBar"; diff --git a/apps/frontend/src/hooks/api/schedules/useUpdateSchedule.ts b/apps/frontend/src/hooks/api/schedules/useUpdateSchedule.ts index 2f1de7182..f43522204 100644 --- a/apps/frontend/src/hooks/api/schedules/useUpdateSchedule.ts +++ b/apps/frontend/src/hooks/api/schedules/useUpdateSchedule.ts @@ -4,6 +4,7 @@ import { MutationHookOptions, Reference, useMutation } from "@apollo/client"; import { IScheduleInput, + READ_SCHEDULE, ScheduleIdentifier, UPDATE_SCHEDULE, UpdateScheduleResponse, @@ -16,9 +17,16 @@ export const useUpdateSchedule = () => { if (!schedule) return; + cache.writeQuery({ + query: READ_SCHEDULE, + variables: { id: schedule._id }, + data: { + schedule, + }, + }); + cache.modify({ fields: { - // TODO: Properly type schedules: (existingSchedules = [], { readField }) => existingSchedules.map((reference: Reference) => readField("_id", reference) === schedule._id diff --git a/apps/frontend/src/lib/api/schedules.ts b/apps/frontend/src/lib/api/schedules.ts index f91eba960..72378e86a 100644 --- a/apps/frontend/src/lib/api/schedules.ts +++ b/apps/frontend/src/lib/api/schedules.ts @@ -157,14 +157,85 @@ export const UPDATE_SCHEDULE = gql` _id name public - year createdBy + year semester + term { + startDate + endDate + } classes { class { subject + unitsMax + unitsMin courseNumber number + course { + title + } + primarySection { + courseNumber + classNumber + subject + number + startDate + endDate + ccn + component + enrollCount + enrollMax + waitlistCount + waitlistMax + meetings { + days + location + endTime + startTime + instructors { + familyName + givenName + } + } + exams { + date + final + location + startTime + endTime + } + } + sections { + number + courseNumber + classNumber + subject + ccn + component + enrollCount + startDate + endDate + enrollMax + waitlistCount + waitlistMax + meetings { + days + location + endTime + startTime + instructors { + familyName + givenName + } + } + exams { + date + final + location + startTime + endTime + } + } } selectedSections } diff --git a/packages/common/src/models/schedule.ts b/packages/common/src/models/schedule.ts index b233bd2da..ed06e1e65 100644 --- a/packages/common/src/models/schedule.ts +++ b/packages/common/src/models/schedule.ts @@ -48,7 +48,7 @@ export const selectedClassSchema = new Schema({ required: true, }, sections: { - type: [String], + type: [Number], trim: true, required: false, },