diff --git a/course-matrix/frontend/package-lock.json b/course-matrix/frontend/package-lock.json index 93dafbe3..57157bc7 100644 --- a/course-matrix/frontend/package-lock.json +++ b/course-matrix/frontend/package-lock.json @@ -26,6 +26,11 @@ "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@reduxjs/toolkit": "^2.5.1", + "@schedule-x/drag-and-drop": "^2.21.1", + "@schedule-x/event-modal": "^2.21.1", + "@schedule-x/events-service": "^2.21.0", + "@schedule-x/react": "^2.21.0", + "@schedule-x/theme-default": "^2.21.0", "ai": "^4.1.45", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -2073,6 +2078,32 @@ "node": ">=14" } }, + "node_modules/@preact/signals": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@preact/signals/-/signals-1.3.2.tgz", + "integrity": "sha512-naxcJgUJ6BTOROJ7C3QML7KvwKwCXQJYTc5L/b0eEsdYgPB6SxwoQ1vDGcS0Q7GVjAenVq/tXrybVdFShHYZWg==", + "peer": true, + "dependencies": { + "@preact/signals-core": "^1.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact": "10.x" + } + }, + "node_modules/@preact/signals-core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.8.0.tgz", + "integrity": "sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", @@ -3206,6 +3237,50 @@ "win32" ] }, + "node_modules/@schedule-x/calendar": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@schedule-x/calendar/-/calendar-2.21.0.tgz", + "integrity": "sha512-wiot2lcjIMsbmKcHawD+9kxnLw2f1VSUZt3eOiiUHEZExQnYSmKhUVuufgVEHPZCPp+PrTxOJFlV8vM2pC+dhw==", + "peer": true, + "peerDependencies": { + "@preact/signals": "^1.1.5", + "preact": "^10.19.2" + } + }, + "node_modules/@schedule-x/drag-and-drop": { + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/@schedule-x/drag-and-drop/-/drag-and-drop-2.21.1.tgz", + "integrity": "sha512-trE+8lEX0eGoKb3cQN1c3DZBmYo/srveT+yzRMGdq40N0fcCZAMllnMVsoiCg8r+75Nx/ovh6UG5ajgvgW1vZQ==" + }, + "node_modules/@schedule-x/event-modal": { + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/@schedule-x/event-modal/-/event-modal-2.21.1.tgz", + "integrity": "sha512-xCvU2g6aHMI7qpkIFce0M8fPrq0nAfnNNXVmNwsz+wbixZyu7FBVvkxCo7WuL/nL1tYBj6OnEZpfdgPr8f542Q==", + "peerDependencies": { + "@preact/signals": "^1.1.5", + "preact": "^10.19.2" + } + }, + "node_modules/@schedule-x/events-service": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@schedule-x/events-service/-/events-service-2.21.0.tgz", + "integrity": "sha512-X5sK0uq5ZU9eIBQv6z0cZC/2p6RlBckfNyFI4KUp8jUcGzJSUreYF5VCr6kHTFieVFW2iXC3p5RAXl2VBp958g==" + }, + "node_modules/@schedule-x/react": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@schedule-x/react/-/react-2.21.0.tgz", + "integrity": "sha512-G2oW5Fwzh6dO4N05Kx9Ozx/FdmxgvwiGHN++eTb7kzp6rJl2WzkhJDeVi3oiF8HnZYUYGL5hYZ8WFedevDv/HQ==", + "peerDependencies": { + "@schedule-x/calendar": "^2.18.0", + "react": "^16.7.0 || ^17 || ^18 || ^19", + "react-dom": "^16.7.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/@schedule-x/theme-default": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@schedule-x/theme-default/-/theme-default-2.21.0.tgz", + "integrity": "sha512-h0s2+Z28Lj3X9QRSpC4C3gX2FZJjzkWxNFTUXrNqTeBxIqEuCUSIvGz2kbzydkD7Av8Xurk/OUYaoKf+kNQa9w==" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -9037,6 +9112,16 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/preact": { + "version": "10.26.4", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.4.tgz", + "integrity": "sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/course-matrix/frontend/package.json b/course-matrix/frontend/package.json index 5515bfcc..00b11f8c 100644 --- a/course-matrix/frontend/package.json +++ b/course-matrix/frontend/package.json @@ -30,6 +30,11 @@ "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@reduxjs/toolkit": "^2.5.1", + "@schedule-x/drag-and-drop": "^2.21.1", + "@schedule-x/event-modal": "^2.21.1", + "@schedule-x/events-service": "^2.21.0", + "@schedule-x/react": "^2.21.0", + "@schedule-x/theme-default": "^2.21.0", "ai": "^4.1.45", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/course-matrix/frontend/public/img/default-timetable-card-image.png b/course-matrix/frontend/public/img/default-timetable-card-image.png new file mode 100644 index 00000000..f8038d2b Binary files /dev/null and b/course-matrix/frontend/public/img/default-timetable-card-image.png differ diff --git a/course-matrix/frontend/src/api/baseApiSlice.ts b/course-matrix/frontend/src/api/baseApiSlice.ts index c9d55517..c464b0b7 100644 --- a/course-matrix/frontend/src/api/baseApiSlice.ts +++ b/course-matrix/frontend/src/api/baseApiSlice.ts @@ -5,6 +5,6 @@ const baseQuery = fetchBaseQuery({ baseUrl: BASE_URL }); export const apiSlice = createApi({ baseQuery, - tagTypes: ["Auth", "Course", "Department", "Offering", "Timetable"], + tagTypes: ["Auth", "Course", "Department", "Offering", "Timetable", "Event"], endpoints: () => ({}), }); diff --git a/course-matrix/frontend/src/api/config.ts b/course-matrix/frontend/src/api/config.ts index f19b290b..2a89cac7 100644 --- a/course-matrix/frontend/src/api/config.ts +++ b/course-matrix/frontend/src/api/config.ts @@ -5,3 +5,5 @@ export const AUTH_URL = `${SERVER_URL}/auth`; export const COURSES_URL = `${SERVER_URL}/api/courses`; export const DEPARTMENT_URL = `${SERVER_URL}/api/departments`; export const OFFERINGS_URL = `${SERVER_URL}/api/offerings`; +export const TIMETABLES_URL = `${SERVER_URL}/api/timetables`; +export const EVENTS_URL = `${SERVER_URL}/api/timetables/events`; diff --git a/course-matrix/frontend/src/api/eventsApiSlice.ts b/course-matrix/frontend/src/api/eventsApiSlice.ts new file mode 100644 index 00000000..48f9b271 --- /dev/null +++ b/course-matrix/frontend/src/api/eventsApiSlice.ts @@ -0,0 +1,66 @@ +import { data } from "react-router-dom"; +import { apiSlice } from "./baseApiSlice"; +import { EVENTS_URL } from "./config"; + +// Endpoints for /api/timetables/events +export const eventsApiSlice = apiSlice.injectEndpoints({ + endpoints: (builder) => ({ + createEvent: builder.mutation({ + query: (data) => ({ + url: `${EVENTS_URL}`, + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/plain, */*", + }, + providesTags: ["Event"], + body: data, + credentials: "include", + }), + }), + getEvents: builder.query({ + query: (id) => ({ + url: `${EVENTS_URL}/${id}`, + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/plain, */*", + }, + providesTags: ["Event"], + credentials: "include", + }), + }), + updateEvent: builder.mutation({ + query: (data) => ({ + url: `${EVENTS_URL}/${data.id}`, + method: "PUT", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/plain, */*", + }, + providesTags: ["Event"], + body: data, + credentials: "include", + }), + }), + deleteEvent: builder.mutation({ + query: (id) => ({ + url: `${EVENTS_URL}/${id}`, + method: "DELETE", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/plain, */*", + }, + providesTags: ["Event"], + credentials: "include", + }), + }), + }), +}); + +export const { + useCreateEventMutation, + useGetEventsQuery, + useUpdateEventMutation, + useDeleteEventMutation, +} = eventsApiSlice; diff --git a/course-matrix/frontend/src/api/timetableApiSlice.ts b/course-matrix/frontend/src/api/timetableApiSlice.ts new file mode 100644 index 00000000..35075fd3 --- /dev/null +++ b/course-matrix/frontend/src/api/timetableApiSlice.ts @@ -0,0 +1,65 @@ +import { apiSlice } from "./baseApiSlice"; +import { TIMETABLES_URL } from "./config"; + +// Endpoints for /api/timetables +export const timetableApiSlice = apiSlice.injectEndpoints({ + endpoints: (builder) => ({ + createTimetable: builder.mutation({ + query: (data) => ({ + url: `${TIMETABLES_URL}`, + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/plain, */*", + }, + providesTags: ["Timetable"], + body: data, + credentials: "include", + }), + }), + getTimetables: builder.query({ + query: () => ({ + url: `${TIMETABLES_URL}`, + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/plain, */*", + }, + providesTags: ["Timetable"], + credentials: "include", + }), + }), + updateTimetable: builder.mutation({ + query: (data) => ({ + url: `${TIMETABLES_URL}/${data.id}`, + method: "PUT", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/plain, */*", + }, + providesTags: ["Timetable"], + body: data, + credentials: "include", + }), + }), + deleteTimetable: builder.mutation({ + query: (id) => ({ + url: `${TIMETABLES_URL}/${id}`, + method: "DELETE", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/plain, */*", + }, + providesTags: ["Timetable"], + credentials: "include", + }), + }), + }), +}); + +export const { + useGetTimetablesQuery, + useUpdateTimetableMutation, + useCreateTimetableMutation, + useDeleteTimetableMutation, +} = timetableApiSlice; diff --git a/course-matrix/frontend/src/components/ui/button.tsx b/course-matrix/frontend/src/components/ui/button.tsx index d6c7bcbc..6da08872 100644 --- a/course-matrix/frontend/src/components/ui/button.tsx +++ b/course-matrix/frontend/src/components/ui/button.tsx @@ -21,6 +21,7 @@ const buttonVariants = cva( }, size: { default: "h-10 px-4 py-2", + xs: "h-6 rounded-md px-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10", diff --git a/course-matrix/frontend/src/pages/Dashboard/Dashboard.tsx b/course-matrix/frontend/src/pages/Dashboard/Dashboard.tsx index 919a89a5..78b05562 100644 --- a/course-matrix/frontend/src/pages/Dashboard/Dashboard.tsx +++ b/course-matrix/frontend/src/pages/Dashboard/Dashboard.tsx @@ -18,6 +18,7 @@ import { Link, Navigate, Route, Routes, useLocation } from "react-router-dom"; import TimetableBuilder from "../TimetableBuilder/TimetableBuilder"; import AssistantPage from "../Assistant/AssistantPage"; import { RuntimeProvider } from "../Assistant/runtime-provider"; +import Home from "../Home/Home"; /** * Dashboard Component @@ -77,7 +78,7 @@ const Dashboard = () => { } /> } /> - Home} /> + } /> } /> } /> diff --git a/course-matrix/frontend/src/pages/Home/Home.tsx b/course-matrix/frontend/src/pages/Home/Home.tsx new file mode 100644 index 00000000..dde76332 --- /dev/null +++ b/course-matrix/frontend/src/pages/Home/Home.tsx @@ -0,0 +1,77 @@ +import { Button } from "@/components/ui/button"; +import { Pin } from "lucide-react"; +import TimetableCard from "./TimetableCard"; +import TimetableCompareButton from "./TimetableCompareButton"; +import TimetableCreateNewButton from "./TimetableCreateNewButton"; +import { useGetTimetablesQuery } from "../../api/timetableApiSlice"; + +/** + * Home component that displays the user's timetables and provides options to create or compare timetables. + * @returns {JSX.Element} The rendered component. + */ +const Home = () => { + const user_metadata = JSON.parse(localStorage.getItem("userInfo") ?? "{}"); + const name = + (user_metadata?.user?.user_metadata?.username as string) ?? + (user_metadata?.user?.email as string); + + const { data, isLoading, refetch } = useGetTimetablesQuery(); + + return ( +
+
+
+

My Timetables

+ +
+
+
+ + + +
+
+ + +
+
+
+
+ {isLoading ? ( +

Loading...

+ ) : ( + data?.map((timetable, index) => ( + + )) + )} +
+
+
+ ); +}; + +export default Home; diff --git a/course-matrix/frontend/src/pages/Home/TimetableCard.tsx b/course-matrix/frontend/src/pages/Home/TimetableCard.tsx new file mode 100644 index 00000000..85fb1c23 --- /dev/null +++ b/course-matrix/frontend/src/pages/Home/TimetableCard.tsx @@ -0,0 +1,121 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Pencil } from "lucide-react"; +import { useState } from "react"; +import TimetableCardKebabMenu from "./TimetableCardKebabMenu"; +import { useUpdateTimetableMutation } from "@/api/timetableApiSlice"; + +interface TimetableCardProps { + refetch: () => void; + timetableId: number; + title: string; + lastEditedDate: Date; + owner: string; +} + +/** + * Component for displaying a timetable card with options to edit the title and access a kebab menu. + * @param {TimetableCardProps} props - The properties for the timetable card. + * @returns {JSX.Element} The rendered component. + */ +const TimetableCard = ({ + refetch, + timetableId, + title, + lastEditedDate, + owner, +}: TimetableCardProps) => { + const [updateTimetable] = useUpdateTimetableMutation(); + + const lastEditedDateArray = lastEditedDate + .toISOString() + .split("T")[0] + .split("-"); + const lastEditedYear = lastEditedDateArray[0]; + const lastEditedMonth = lastEditedDateArray[1]; + const lastEditedDay = lastEditedDateArray[2]; + const lastEditedDateTimestamp = + lastEditedMonth + "/" + lastEditedDay + "/" + lastEditedYear; + + const [timetableCardTitle, setTimetableCardTitle] = useState(title); + const [isEditingTitle, setIsEditingTitle] = useState(false); + + const handleSave = async () => { + try { + await updateTimetable({ + id: timetableId, + timetable_title: timetableCardTitle, + }).unwrap(); + setIsEditingTitle(false); + } catch (error) { + console.error("Failed to update timetable title:", error); + } + }; + + return ( + + + Timetable default image +
+ + setTimetableCardTitle(e.target.value)} + /> + +
+ {!isEditingTitle && ( + <> + + + + )} + {isEditingTitle && ( + + )} +
+
+
+ + +
Last edited {lastEditedDateTimestamp}
+
Owned by: {owner}
+
+
+
+ ); +}; + +export default TimetableCard; diff --git a/course-matrix/frontend/src/pages/Home/TimetableCardKebabMenu.tsx b/course-matrix/frontend/src/pages/Home/TimetableCardKebabMenu.tsx new file mode 100644 index 00000000..252e5882 --- /dev/null +++ b/course-matrix/frontend/src/pages/Home/TimetableCardKebabMenu.tsx @@ -0,0 +1,98 @@ +import { Button } from "@/components/ui/button"; +import { Link } from "react-router-dom"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@/components/ui/dropdown-menu"; +import { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, + DialogClose, +} from "@/components/ui/dialog"; +import { EllipsisVertical } from "lucide-react"; +import { useDeleteTimetableMutation } from "@/api/timetableApiSlice"; + +interface TimetableCardKebabMenuProps { + refetch: () => void; + timetableId: number; +} + +/** + * Component for the kebab menu in the timetable card, providing options to edit or delete the timetable. + * @returns {JSX.Element} The rendered component. + */ +const TimetableCardKebabMenu = ({ + refetch, + timetableId, +}: TimetableCardKebabMenuProps) => { + const [deleteTimetable] = useDeleteTimetableMutation(); + + const handleDelete = async () => { + try { + await deleteTimetable(timetableId); + refetch(); + } catch (error) { + console.error("Failed to delete timetable:", error); + } + }; + + return ( + + + + + + + + Edit Timetable + + + e.preventDefault()}> + + + + + + + + Delete Timetable + + + Are you sure you want to delete your timetable? This action + cannot be undone. + + + + + + + + + + + + + + + + ); +}; + +export default TimetableCardKebabMenu; diff --git a/course-matrix/frontend/src/pages/Home/TimetableCompareButton.tsx b/course-matrix/frontend/src/pages/Home/TimetableCompareButton.tsx new file mode 100644 index 00000000..963709b3 --- /dev/null +++ b/course-matrix/frontend/src/pages/Home/TimetableCompareButton.tsx @@ -0,0 +1,47 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, + DialogClose, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +/** + * Component for the "Compare" button that opens a dialog to compare timetables. + * @returns {JSX.Element} The rendered component. + */ +const TimetableCompareDialog = () => ( + + + + + + + Compare Timetables + Compare 2 of your timetables + + + + + + + + + + + + + + + +); + +export default TimetableCompareDialog; diff --git a/course-matrix/frontend/src/pages/Home/TimetableCreateNewButton.tsx b/course-matrix/frontend/src/pages/Home/TimetableCreateNewButton.tsx new file mode 100644 index 00000000..a80952d0 --- /dev/null +++ b/course-matrix/frontend/src/pages/Home/TimetableCreateNewButton.tsx @@ -0,0 +1,18 @@ +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; +import { Link } from "react-router-dom"; + +/** + * Component for the "Create New" button that navigates to the timetable creation page. + * @returns {JSX.Element} The rendered component. + */ +const TimetableCreateNewButton = () => ( + + + +); + +export default TimetableCreateNewButton; diff --git a/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx b/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx new file mode 100644 index 00000000..b787afd0 --- /dev/null +++ b/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx @@ -0,0 +1,104 @@ +import { ScheduleXCalendar } from "@schedule-x/react"; +import { + createCalendar, + createViewDay, + createViewMonthAgenda, + createViewMonthGrid, + createViewWeek, + viewWeek, +} from "@schedule-x/calendar"; +import { createDragAndDropPlugin } from "@schedule-x/drag-and-drop"; +import { createEventModalPlugin } from "@schedule-x/event-modal"; +import "@schedule-x/theme-default/dist/index.css"; +import { Button } from "@/components/ui/button"; + +function Calendar({ courseEvents, userEvents }) { + let index = 1; + const courseEventsParsed = courseEvents.map( + (event: { + event_name: string; + event_date: string; + event_start: string; + event_end: string; + }) => ({ + id: index++, + title: event.event_name, + start: + event.event_date + + " " + + event.event_start.split(":")[0] + + ":" + + event.event_start.split(":")[1], + end: + event.event_date + + " " + + event.event_end.split(":")[0] + + ":" + + event.event_end.split(":")[1], + calendarId: "courseEvent", + }), + ); + const userEventsParsed = userEvents.map( + (event: { + event_name: string; + event_date: string; + event_start: string; + event_end: string; + }) => ({ + id: index++, + title: event.event_name, + start: + event.event_date + + " " + + event.event_start.split(":")[0] + + ":" + + event.event_start.split(":")[1], + end: + event.event_date + + " " + + event.event_end.split(":")[0] + + ":" + + event.event_end.split(":")[1], + calendarId: "userEvent", + }), + ); + + const calendar = createCalendar({ + views: [ + createViewDay(), + createViewWeek(), + createViewMonthGrid(), + createViewMonthAgenda(), + ], + defaultView: viewWeek.name, + events: [...courseEventsParsed, ...userEventsParsed], + calendars: { + courseEvent: { + colorName: "courseEvent", + lightColors: { + main: "#1c7df9", + container: "#d2e7ff", + onContainer: "#002859", + }, + darkColors: { + main: "#c0dfff", + onContainer: "#dee6ff", + container: "#426aa2", + }, + }, + }, + plugins: [createDragAndDropPlugin(), createEventModalPlugin()], + }); + + return ( +
+

+
Your Timetable
+ +

+ +
+ ); +} + +export default Calendar; diff --git a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx index 6a3aa14f..33a3ab97 100644 --- a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx +++ b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx @@ -20,10 +20,12 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Select, - SelectTrigger, - SelectValue, SelectContent, + SelectGroup, SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import CourseSearch from "@/pages/TimetableBuilder/CourseSearch"; import { mockSearchData } from "./mockSearchData"; @@ -32,8 +34,13 @@ import CreateCustomSetting from "./CreateCustomSetting"; import { formatTime } from "@/utils/format-date-time"; import { FilterForm, FilterFormSchema } from "@/models/filter-form"; import { useGetCoursesQuery } from "@/api/coursesApiSlice"; +import { useGetTimetablesQuery } from "@/api/timetableApiSlice"; +import { useGetEventsQuery } from "@/api/eventsApiSlice"; import { useDebounceValue } from "@/utils/useDebounce"; import SearchFilters from "./SearchFilters"; +import Calendar from "./Calendar"; +import { Timetable } from "@/utils/type-utils"; +import { useSearchParams } from "react-router-dom"; type FormContextType = UseFormReturn>; export const FormContext = createContext(null); @@ -85,15 +92,19 @@ const TimetableBuilder = () => { resolver: zodResolver(FilterFormSchema), }); + const [queryParams, setQueryParams] = useSearchParams(); + const isEditingTimetable = queryParams.has("edit"); + const editingTimetableId = queryParams.get("edit"); + const selectedCourses = form.watch("courses") || []; const enabledRestrictions = form.watch("restrictions") || []; const searchQuery = form.watch("search"); const debouncedSearchQuery = useDebounceValue(searchQuery, 250); - const [isEditNameOpen, setIsEditNameOpen] = useState(false); const [isCustomSettingsOpen, setIsCustomSettingsOpen] = useState(false); const [filters, setFilters] = useState(null); const [showFilters, setShowFilters] = useState(false); + const [timetableId, setTimetableId] = useState(-1); const noSearchAndFilter = () => { return !searchQuery && !filters; @@ -108,6 +119,20 @@ const TimetableBuilder = () => { ...filters, }); + const { data: eventsData, isLoading: eventsLoading } = useGetEventsQuery( + timetableId, + ) as { + data: { courseEvents: unknown[]; userEvents: unknown[] }; + isLoading: boolean; + }; + const courseEvents = eventsData?.courseEvents || []; + const userEvents = eventsData?.userEvents || []; + + const { data: timetablesData } = useGetTimetablesQuery() as { + data: Timetable[]; + }; + const timetables = timetablesData || []; + useEffect(() => { if (searchQuery) { refetch(); @@ -157,19 +182,37 @@ const TimetableBuilder = () => {
-
-

- {baseTimetableForm.name} +
+

+ {isEditingTimetable ? "Edit Timetable" : "New Timetable"}

-
- { - setIsEditNameOpen(true); + {isEditingTimetable && ( + + )}
-
+
{
-
-

- {" "} - Fill in the form to create your timetable! -

+
+
{isCustomSettingsOpen && ( = Partial> & Pick; + +export type Timetable = { + id: number; + created_at: Date; + updated_at: Date; + semester: string; + timetable_title: string; + user_id: string; +};