Skip to content

Commit 8106f3d

Browse files
committed
Feat: Compare
1 parent 13cc5f8 commit 8106f3d

File tree

7 files changed

+272
-47
lines changed

7 files changed

+272
-47
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Leaf, Snowflake, Sun } from "lucide-react";
2+
3+
interface SemesterIconProps {
4+
semester: string;
5+
size?: number
6+
}
7+
8+
export const SemesterIcon = ({semester, size}: SemesterIconProps) => {
9+
return <>
10+
{semester === "Summer 2025" ?
11+
<Sun className="text-yellow-500" size={size}/> :
12+
semester === "Fall 2025" ?
13+
<Leaf className="text-orange-500" size={size}/> :
14+
<Snowflake className="text-blue-500" size={size}/>
15+
}
16+
</>
17+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { useGetTimetableQuery } from "@/api/timetableApiSlice";
2+
import { Button } from "@/components/ui/button";
3+
import { Timetable, TimetableEvents } from "@/utils/type-utils";
4+
import { useEffect, useState } from "react";
5+
import { Link, useSearchParams } from "react-router-dom";
6+
import Calendar from "../TimetableBuilder/Calendar";
7+
import { useGetEventsQuery } from "@/api/eventsApiSlice";
8+
import { Spinner } from "@/components/ui/spinner";
9+
10+
export const CompareTimetables = () => {
11+
12+
const [queryParams] = useSearchParams();
13+
const validParams = queryParams.has("id1") && queryParams.has("id2");
14+
15+
if (!validParams) {
16+
return (
17+
<div className="w-full text-red-500 text-center mt-10">You have not selected two timetables to compare. Please try again.</div>
18+
)
19+
}
20+
const timetableId1 = parseInt(queryParams.get("id1") || "0");
21+
const timetableId2 = parseInt(queryParams.get("id2") || "0");
22+
23+
const { data: data1} = useGetTimetableQuery(timetableId1) as { data: Timetable[]};
24+
const { data: data2} = useGetTimetableQuery(timetableId2) as { data: Timetable[]};
25+
26+
const { data: timetableEventsData1 } = useGetEventsQuery(timetableId1) as {
27+
data: TimetableEvents;
28+
};
29+
const { data: timetableEventsData2 } = useGetEventsQuery(timetableId2) as {
30+
data: TimetableEvents;
31+
};
32+
33+
const [timetable1, setTimetable1] = useState<Timetable | null>(null);
34+
const [timetable2, setTimetable2] = useState<Timetable | null>(null);
35+
const [offeringIds1, setOfferingIds1] = useState<number[]>([]);
36+
const [offeringIds2, setOfferingIds2] = useState<number[]>([]);
37+
38+
39+
useEffect(() => {
40+
if (data1) {
41+
setTimetable1(data1[0])
42+
}
43+
}, [data1])
44+
useEffect(()=> {
45+
if (data2) {
46+
setTimetable2(data2[0])
47+
}
48+
}, [data2])
49+
50+
// get unique offeringIds for calendar
51+
useEffect(() => {
52+
if (timetableEventsData1) {
53+
const uniqueOfferingIds = new Set<number>();
54+
for (const event of timetableEventsData1.courseEvents) {
55+
if (!uniqueOfferingIds.has(event.offering_id))
56+
uniqueOfferingIds.add(event.offering_id)
57+
}
58+
setOfferingIds1(Array.from(uniqueOfferingIds))
59+
}
60+
61+
}, [timetableEventsData1])
62+
63+
useEffect(() => {
64+
if (timetableEventsData2) {
65+
const uniqueOfferingIds = new Set<number>();
66+
for (const event of timetableEventsData2.courseEvents) {
67+
if (!uniqueOfferingIds.has(event.offering_id))
68+
uniqueOfferingIds.add(event.offering_id)
69+
}
70+
setOfferingIds2(Array.from(uniqueOfferingIds))
71+
}
72+
}, [timetableEventsData2])
73+
74+
return <>
75+
<div className="w-full">
76+
<div className="mb-4 p-8">
77+
<div className="mb-2 flex flex-row justify-between">
78+
<div>
79+
<h1 className="text-2xl font-medium tracking-tight mb-4">
80+
Comparing Timetables
81+
</h1>
82+
</div>
83+
<div className="flex gap-2 ">
84+
<Link to="/dashboard/home">
85+
<Button size="sm" variant="outline" onClick={() => {}}>
86+
Back to Home
87+
</Button>
88+
</Link>
89+
</div>
90+
</div>
91+
<hr className="mb-4"/>
92+
<div className="flex gap-4">
93+
<div className="w-1/2">
94+
{offeringIds1.length === 0 ? <Spinner/> : (
95+
<Calendar
96+
setShowLoadingPage={() => {}}
97+
isChoosingSectionsManually={false}
98+
semester={timetable1?.semester ?? "Fall 2025"}
99+
selectedCourses={[]}
100+
newOfferingIds={offeringIds1}
101+
restrictions={[]}
102+
header={timetable1?.timetable_title}
103+
/>
104+
)}
105+
106+
</div>
107+
<div className="w-1/2">
108+
{offeringIds2.length === 0 ? <Spinner/> : (
109+
<Calendar
110+
setShowLoadingPage={() => {}}
111+
isChoosingSectionsManually={false}
112+
semester={timetable2?.semester ?? "Fall 2025"}
113+
selectedCourses={[]}
114+
newOfferingIds={offeringIds2}
115+
restrictions={[]}
116+
header={timetable2?.timetable_title}
117+
/>
118+
)}
119+
</div>
120+
</div>
121+
</div>
122+
123+
</div>
124+
</>
125+
}

course-matrix/frontend/src/pages/Dashboard/Dashboard.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import TimetableBuilder from "../TimetableBuilder/TimetableBuilder";
1919
import AssistantPage from "../Assistant/AssistantPage";
2020
import { RuntimeProvider } from "../Assistant/runtime-provider";
2121
import Home from "../Home/Home";
22+
import { CompareTimetables } from "../Compare/CompareTimetables";
2223

2324
/**
2425
* Dashboard Component
@@ -61,9 +62,9 @@ const Dashboard = () => {
6162
</Link>
6263
) : location.pathname === "/dashboard/assistant" ? (
6364
<Link to="/dashboard/assistant">AI Assistant</Link>
64-
) : (
65-
<></>
66-
)}
65+
) : location.pathname.startsWith("/dashboard/compare") ? (
66+
<>Compare Timeatables</>
67+
) : <></>}
6768
</BreadcrumbItem>
6869
{/* <BreadcrumbSeparator className="hidden md:block" /> */}
6970
{/* <BreadcrumbItem>
@@ -81,6 +82,7 @@ const Dashboard = () => {
8182
<Route path="/home" element={<Home />} />
8283
<Route path="/timetable" element={<TimetableBuilder />} />
8384
<Route path="/assistant" element={<AssistantPage />} />
85+
<Route path="/compare" element={<CompareTimetables/>} />
8486
</Routes>
8587
</div>
8688
</SidebarInset>

course-matrix/frontend/src/pages/Home/Home.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
11
import { Button } from "@/components/ui/button";
22
import { Pin } from "lucide-react";
33
import TimetableCard from "./TimetableCard";
4-
import TimetableCompareButton from "./TimetableCompareButton";
54
import TimetableCreateNewButton from "./TimetableCreateNewButton";
65
import { useGetTimetablesQuery } from "../../api/timetableApiSlice";
7-
8-
export interface Timetable {
9-
id: number;
10-
created_at: string;
11-
updated_at: string;
12-
user_id: string;
13-
semester: string;
14-
timetable_title: string;
15-
favorite: boolean;
16-
}
6+
import { Timetable } from "@/utils/type-utils";
7+
import { TimetableCompareButton } from "./TimetableCompareButton";
178

189
/**
1910
* Home component that displays the user's timetables and provides options to create or compare timetables.
@@ -61,8 +52,8 @@ const Home = () => {
6152
Shared
6253
</Button>
6354
</div>
64-
<div className="flex gap-8">
65-
<TimetableCompareButton />
55+
<div className="flex gap-2">
56+
<TimetableCompareButton timetables={data} />
6657
<TimetableCreateNewButton />
6758
</div>
6859
</div>
Lines changed: 115 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { SemesterIcon } from "@/components/semester-icon";
12
import { Button } from "@/components/ui/button";
23
import {
34
Dialog,
@@ -9,39 +10,125 @@ import {
910
DialogDescription,
1011
DialogClose,
1112
} from "@/components/ui/dialog";
13+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
1214
import { Input } from "@/components/ui/input";
1315
import { Label } from "@/components/ui/label";
16+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
17+
import { Timetable } from "@/utils/type-utils";
18+
import { zodResolver } from "@hookform/resolvers/zod";
19+
import { GitCompareArrows } from "lucide-react";
20+
import { useState } from "react";
21+
import { useForm } from "react-hook-form";
22+
import { useNavigate } from "react-router-dom";
23+
import { z } from "zod";
1424

25+
const CompareFormSchema = z.object({
26+
timetable1: z.number().positive(),
27+
timetable2: z.number().positive(),
28+
})
29+
30+
interface TimetableCompareDialogProps {
31+
timetables: Timetable[];
32+
}
1533
/**
1634
* Component for the "Compare" button that opens a dialog to compare timetables.
1735
* @returns {JSX.Element} The rendered component.
1836
*/
19-
const TimetableCompareDialog = () => (
20-
<Dialog>
21-
<DialogTrigger asChild>
22-
<Button size="sm" className="px-5">
23-
Compare
24-
</Button>
25-
</DialogTrigger>
26-
<DialogContent className="gap-5">
27-
<DialogHeader>
28-
<DialogTitle>Compare Timetables</DialogTitle>
29-
<DialogDescription>Compare 2 of your timetables</DialogDescription>
30-
</DialogHeader>
31-
<Label htmlFor="timetable1">First Timetable Name</Label>
32-
<Input id="timetable1" placeholder="Placeholder name" disabled />
33-
<Label htmlFor="timetable2">Second Timetable Name</Label>
34-
<Input id="timetable2" placeholder="Placeholder name" disabled />
35-
<DialogFooter>
36-
<DialogClose asChild>
37-
<Button variant="secondary">Cancel</Button>
38-
</DialogClose>
39-
<DialogClose asChild>
40-
<Button>Compare</Button>
41-
</DialogClose>
42-
</DialogFooter>
43-
</DialogContent>
44-
</Dialog>
45-
);
37+
export const TimetableCompareButton = ({ timetables }: TimetableCompareDialogProps) => {
38+
const [open, setOpen] = useState(false);
39+
const navigate = useNavigate();
40+
41+
const compareForm = useForm<z.infer<typeof CompareFormSchema>>({
42+
resolver: zodResolver(CompareFormSchema),
43+
});
44+
45+
const onSubmit = (values: z.infer<typeof CompareFormSchema>) => {
46+
console.log("Comapare Form submitted:", values);
47+
setOpen(false);
48+
navigate(`/dashboard/compare?id1=${values.timetable1}&id2=${values.timetable2}`)
49+
}
50+
51+
return (
52+
<Dialog open={open} onOpenChange={setOpen}>
53+
<DialogTrigger asChild>
54+
<Button variant="secondary" size="sm" className="px-5">
55+
Compare
56+
<GitCompareArrows/>
57+
</Button>
58+
</DialogTrigger>
59+
<DialogContent className="gap-5">
60+
<DialogHeader>
61+
<DialogTitle>Compare Timetables</DialogTitle>
62+
<DialogDescription>View timetables side by side. </DialogDescription>
63+
</DialogHeader>
64+
65+
<Form {...compareForm}>
66+
<form onSubmit={compareForm.handleSubmit(onSubmit)} className="space-y-6">
67+
<FormField
68+
control={compareForm.control}
69+
name="timetable1"
70+
render={({ field }) => (
71+
<FormItem>
72+
<FormLabel>Timetable 1</FormLabel>
73+
<Select onValueChange={(value) => field.onChange(Number(value))}>
74+
<FormControl>
75+
<SelectTrigger>
76+
<SelectValue placeholder="Select a timetable" />
77+
</SelectTrigger>
78+
</FormControl>
79+
<SelectContent>
80+
{timetables.map(timetable => (
81+
<SelectItem key={timetable.id} value={timetable.id.toString()}>
82+
<div className="flex items-center gap-2">
83+
<SemesterIcon semester={timetable.semester} size={18}/>
84+
<span>{timetable.timetable_title}</span>
85+
</div>
86+
</SelectItem>
87+
))}
88+
</SelectContent>
89+
</Select>
90+
<FormMessage />
91+
</FormItem>
92+
)}
93+
/>
94+
95+
<FormField
96+
control={compareForm.control}
97+
name="timetable2"
98+
render={({ field }) => (
99+
<FormItem>
100+
<FormLabel>Timetable 2</FormLabel>
101+
<Select onValueChange={(value) => field.onChange(Number(value))}>
102+
<FormControl>
103+
<SelectTrigger>
104+
<SelectValue placeholder="Select a timetable" />
105+
</SelectTrigger>
106+
</FormControl>
107+
<SelectContent>
108+
{timetables.map(timetable => (
109+
<SelectItem key={timetable.id} value={timetable.id.toString()}>
110+
<div className="flex items-center gap-2">
111+
<SemesterIcon semester={timetable.semester} size={18}/>
112+
<span>{timetable.timetable_title}</span>
113+
</div>
114+
</SelectItem>
115+
))}
116+
</SelectContent>
117+
</Select>
118+
<FormMessage />
119+
</FormItem>
120+
)}
121+
/>
46122

47-
export default TimetableCompareDialog;
123+
<DialogFooter>
124+
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
125+
Cancel
126+
</Button>
127+
<Button type="submit">Submit</Button>
128+
</DialogFooter>
129+
</form>
130+
</Form>
131+
</DialogContent>
132+
</Dialog>
133+
);
134+
};

course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ interface CalendarProps {
6666
selectedCourses: TimetableForm["courses"];
6767
newOfferingIds: number[];
6868
restrictions: TimetableForm["restrictions"];
69+
header?: string;
6970
}
7071

7172
function parseEvent(id: number, event: Event, calendarId: string) {
@@ -96,6 +97,7 @@ const Calendar = React.memo<CalendarProps>(
9697
newOfferingIds,
9798
restrictions,
9899
isChoosingSectionsManually,
100+
header = "Your Timetable",
99101
}) => {
100102
const form = useForm<z.infer<typeof TimetableFormSchema>>();
101103

@@ -335,7 +337,7 @@ const Calendar = React.memo<CalendarProps>(
335337
return (
336338
<div>
337339
<h1 className="text-2xl flex flex-row justify-between font-medium tracking-tight mb-8">
338-
<div>Your Timetable </div>
340+
<div>{header}</div>
339341
{!isEditingTimetable ? (
340342
<Dialog>
341343
{isChoosingSectionsManually &&

0 commit comments

Comments
 (0)