From 1b2012c9de05a94554f0fadd237483124cd2ac92 Mon Sep 17 00:00:00 2001 From: FLAME Date: Sun, 2 Mar 2025 01:59:27 -0500 Subject: [PATCH 01/20] Timetable frontend rough draft --- course-matrix/frontend/package-lock.json | 94 ++++++++++++++++++ course-matrix/frontend/package.json | 1 + .../img/default-timetable-card-image.png | Bin 0 -> 5426 bytes .../frontend/src/components/ui/button.tsx | 1 + .../src/pages/Dashboard/Dashboard.tsx | 3 +- .../frontend/src/pages/Home/Home.tsx | 63 ++++++++++++ .../frontend/src/pages/Home/TimetableCard.tsx | 94 ++++++++++++++++++ .../src/pages/Home/TimetableCardKebabMenu.tsx | 66 ++++++++++++ .../src/pages/Home/TimetableCompareButton.tsx | 43 ++++++++ .../pages/Home/TimetableCreateNewButton.tsx | 14 +++ .../TimetableBuilder/TimetableBuilder.tsx | 9 +- 11 files changed, 385 insertions(+), 3 deletions(-) create mode 100644 course-matrix/frontend/public/img/default-timetable-card-image.png create mode 100644 course-matrix/frontend/src/pages/Home/Home.tsx create mode 100644 course-matrix/frontend/src/pages/Home/TimetableCard.tsx create mode 100644 course-matrix/frontend/src/pages/Home/TimetableCardKebabMenu.tsx create mode 100644 course-matrix/frontend/src/pages/Home/TimetableCompareButton.tsx create mode 100644 course-matrix/frontend/src/pages/Home/TimetableCreateNewButton.tsx diff --git a/course-matrix/frontend/package-lock.json b/course-matrix/frontend/package-lock.json index 8787905a..64b585d1 100644 --- a/course-matrix/frontend/package-lock.json +++ b/course-matrix/frontend/package-lock.json @@ -31,6 +31,7 @@ "clsx": "^2.1.1", "lucide-react": "^0.474.0", "react": "^18.3.1", + "react-calendar": "^5.1.0", "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", "react-redux": "^9.2.0", @@ -2861,6 +2862,14 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/@wojtekmaj/date-utils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.5.1.tgz", + "integrity": "sha512-+i7+JmNiE/3c9FKxzWFi2IjRJ+KzZl1QPu6QNrsgaa2MuBgXvUy4gA1TVzf/JMdIIloB76xSKikTWuyYAIVLww==", + "funding": { + "url": "https://github.com/wojtekmaj/date-utils?sponsor=1" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -3897,6 +3906,17 @@ "node": ">=6" } }, + "node_modules/get-user-locale": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/get-user-locale/-/get-user-locale-2.3.2.tgz", + "integrity": "sha512-O2GWvQkhnbDoWFUJfaBlDIKUEdND8ATpBXD6KXcbhxlfktyD/d8w6mkzM/IlQEqGZAMz/PW6j6Hv53BiigKLUQ==", + "dependencies": { + "mem": "^8.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/get-user-locale?sponsor=1" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -4650,6 +4670,17 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dependencies": { + "p-defer": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -4925,6 +4956,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mem": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", + "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", + "dependencies": { + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/mem?sponsor=1" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5480,6 +5526,14 @@ "node": ">=8.6" } }, + "node_modules/mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5594,6 +5648,14 @@ "node": ">= 0.8.0" } }, + "node_modules/p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5931,6 +5993,30 @@ "node": ">=0.10.0" } }, + "node_modules/react-calendar": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-5.1.0.tgz", + "integrity": "sha512-09o/rQHPZGEi658IXAJtWfra1N69D1eFnuJ3FQm9qUVzlzNnos1+GWgGiUeSs22QOpNm32aoVFOimq0p3Ug9Eg==", + "dependencies": { + "@wojtekmaj/date-utils": "^1.1.3", + "clsx": "^2.0.0", + "get-user-locale": "^2.2.1", + "warning": "^4.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-calendar?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -7100,6 +7186,14 @@ } } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/course-matrix/frontend/package.json b/course-matrix/frontend/package.json index ae3a8cae..b105d611 100644 --- a/course-matrix/frontend/package.json +++ b/course-matrix/frontend/package.json @@ -33,6 +33,7 @@ "clsx": "^2.1.1", "lucide-react": "^0.474.0", "react": "^18.3.1", + "react-calendar": "^5.1.0", "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", "react-redux": "^9.2.0", 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 0000000000000000000000000000000000000000..f8038d2bc65080c84045379fbc65a83a6a999d84 GIT binary patch literal 5426 zcmeHLX;f3!7QO*mK{13u2$LAZ0hJI$@K$8dV1-2)tdyY)5@?ImA_OE1g3t;k0Th+D z%A}y^65Ajck--WC&43hysx?f4CMXnR7=!>O1m6iH;raf&AMa1QmOrwx?%wC_{q4QK zv(G-4>f^P4^-80a5CpAucXRy`f>g;6q~fdoDUe*l9lQem5bKgz(lYD#;SR$h z+wChh8LDivIA5NST=}d)oP`{qdVizd4*SIiN#P@Bj`cT9p6op`}e=YlC&c{ z|LoR)hAfbX!XBse11v;lfU!uV(lQ6yQYOY$!*$U}n+AS(0}8XgQr7=sJkVSF8-%sV#COZJ7U?AM8Rjcj?Ig%&&4q) z4-@lpYz*rPpNMx`n{$2BzJN}Fq+96JpX4&0b!{!Rj^Fia&Lq{bxFb5Pd1Ot+&)inE zMJGDi2$A(nPLH(VP;MK1TRPxTZ>YMMo^l~!2WfRHP57R8Gf{L6t((CEQMuoyGEe7| z*-5z`B|#o#X{2z=7BoO&(Z?cO@cG&-j~Epna1K^OuK+2@gs)OzHE`@$V31MfNCas2 z?i746C=v)yhkte>IBovtGCYNul;==jO}iWEy@`J7#Pp>;I2nG|6*SUC8_<$G1w>&# ze^k2U=TSGjdyW(wUB)QP43j2Q>tb{-2bkrQ07}U~iqYiBQhn?60=&;!#=hd0Sz*nljkjhM47#W#LN zR0l_^Te%hyz3CjcHS}5o``Gwv&&l69HZHBL+=U!^QCQ17&hBZ~cTPKy9{rshi=QcZ zvOh~iyvv_?%%(1-gqF+fv8&AM-EfzjqgZB0rsw;c)f|~*)Gs(S$m68{4*#ILc4uuG zufj_I(RXK9m=;(Tpr0YRo`~`s7#@vbOJ1Zb<(Ks%NLn!dA(@aOdqpx``c+Rw?W@ zvw6Pr0a8zr_K+j>{42ozMGhH}4{B3OD)MNustUaKxpKTNqXV7fOc*E5U6EGDY`SMt zn<(nLam7@g%2RseXYw_BO&+^0IY*Iq$gQ$4o=c%?5Z!c_O#M@q)kjF?_tlCPk|(<) zUW&K|`X%#XNo1Xbz?d7Ce4!Ngy?ytZdJP>lKM0MGB-fd|SyvuhcHLYuQQ9oz%e8&;&eu3AANdHBsbtgDyj^Gfglp z%`j53n$vb!J4Te=yiKHf_`6XF!cNYTYj7?u*r9PJwZ2MaGIGTq9gjy;zn4>R8xda3 z96dg6jv6A53CbQdic?%pFY)Lc1}PxGwH+=OMxf$AagsuAGXpnbH7kDBrcBOyR?{iX zratSJ+Cc7S$jB_7Z;4CrwCoL%@(nJ0VW(!rQ6oyp_*#RA+NTvt|27<6IRRdCpzc9c zNQn$~y8r&CTUn#xd z&oHZ)v0GG#lJEqOh-@Y3%N$;G7UB7`86{TK5bg;2RGH(7-VICT2tQ0L{0xLVrKnH1 z8*#Jd`|&#Db@^U6r9-}d^O*6nHwvDR)KsOV(2LgsePFjzpLPaMfXlGUdL#1vSep=K z@~3T(S-gkGh>EZt)x&zHgxe$v0Oj5QK!1=Dv+E(g4;tIGec2!i7<~70g2Dj(H83y` z$WaElWyl9jsuupobL`05Z)?*ge?kg_o3}X4?o)P5yx3bo)&GC*mwTF2nK^IfLrlOj?a0KgQvInW z%E_2kb|It*8l{`XQLP*4^s??gUfNyq`aenCIPN~hKyfQs+#s7bL|0a*5M9Hu5nh6+ zM#{q{&$EdL^hRL)0hkH-U}!PlNCMMbgzYvPay0UCQJ?g%6_lF=tX~zzlby5OawDIb z!|Qka#VDlRm>Th*-nVpUgNDF~d_HRGIB8-%BV7ZB1i9rA}XOMtNlW0>e(J2kW!@a)@f=Cr9gz(J&(<6b^)u)DdyHY;Wo6ZcxSz+kvz z%{VbaUw0Ydj?(=yF)c|z^m2FlgT~Udg_;0oD3=VYT}pX7n&?0Y-pb|Vd#CFOK3$yV zzF=8hE8TMy%XO_hElxp%rGhT;oIUl;3DtAIIE=4N=AhPT6V+WXCzzes@KB=hXy+?h zcnE|$%133CWt(0R<`|gUoTJ~0vj37q|=93d|YyFAe%*grIU;*U5 M&&#!HZ`e=&28|h)rT_o{ literal 0 HcmV?d00001 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 9297bc5b..842a0206 100644 --- a/course-matrix/frontend/src/pages/Dashboard/Dashboard.tsx +++ b/course-matrix/frontend/src/pages/Dashboard/Dashboard.tsx @@ -17,6 +17,7 @@ import { Separator } from "@radix-ui/react-separator"; import { Link, Navigate, Route, Routes, useLocation } from "react-router-dom"; import TimetableBuilder from "../TimetableBuilder/TimetableBuilder"; import AssistantPage from "../Assistant/AssistantPage"; +import Home from "../Home/Home"; /** * Dashboard Component @@ -75,7 +76,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..74cfc3f5 --- /dev/null +++ b/course-matrix/frontend/src/pages/Home/Home.tsx @@ -0,0 +1,63 @@ +import { Button } from "@/components/ui/button"; +import { Pin } from "lucide-react"; +import TimetableCard from "./TimetableCard"; +import TimetableCompareButton from "./TimetableCompareButton"; +import TimetableCreateNewButton from "./TimetableCreateNewButton"; + +const Home = () => { + // const user_metadata = JSON.parse(localStorage.getItem("userInfo") ?? "{}"); + // const username = (user_metadata?.user?.user_metadata?.username as string) ?? "John Doe"; + const username = "Me"; + + return ( +
+
+
+

My Timetables

+ +
+
+
+ + + +
+
+ + +
+
+
+
+ {[...Array(12).keys()].map((_, index) => { + return ( + + ); + })} +
+
+
+ ); +}; + +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..3eb2acd9 --- /dev/null +++ b/course-matrix/frontend/src/pages/Home/TimetableCard.tsx @@ -0,0 +1,94 @@ +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"; + +interface TimetableCardProps { + title: string; + lastEditedDate: Date; + owner: string; +} + +const TimetableCard = ({ + title, + lastEditedDate, + owner, +}: TimetableCardProps) => { + 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); + + 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..9f5c0c74 --- /dev/null +++ b/course-matrix/frontend/src/pages/Home/TimetableCardKebabMenu.tsx @@ -0,0 +1,66 @@ +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"; + +const TimetableCardKebabMenu = () => ( + + + + + + + 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..5dfb579e --- /dev/null +++ b/course-matrix/frontend/src/pages/Home/TimetableCompareButton.tsx @@ -0,0 +1,43 @@ +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"; + +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..364ecc14 --- /dev/null +++ b/course-matrix/frontend/src/pages/Home/TimetableCreateNewButton.tsx @@ -0,0 +1,14 @@ +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; +import { Link } from "react-router-dom"; + +const TimetableCreateNewButton = () => ( + + + +); + +export default TimetableCreateNewButton; diff --git a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx index 6a3aa14f..9349cb2d 100644 --- a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx +++ b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx @@ -34,6 +34,11 @@ import { FilterForm, FilterFormSchema } from "@/models/filter-form"; import { useGetCoursesQuery } from "@/api/coursesApiSlice"; import { useDebounceValue } from "@/utils/useDebounce"; import SearchFilters from "./SearchFilters"; +import Calendar from 'react-calendar'; +import 'react-calendar/dist/Calendar.css'; + +type ValuePiece = Date | null; +type Value = ValuePiece | [ValuePiece, ValuePiece]; type FormContextType = UseFormReturn>; export const FormContext = createContext(null); @@ -94,6 +99,7 @@ const TimetableBuilder = () => { const [isCustomSettingsOpen, setIsCustomSettingsOpen] = useState(false); const [filters, setFilters] = useState(null); const [showFilters, setShowFilters] = useState(false); + const [value, onChange] = useState(new Date()); const noSearchAndFilter = () => { return !searchQuery && !filters; @@ -332,8 +338,7 @@ const TimetableBuilder = () => {

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

{isCustomSettingsOpen && ( From 39cbc23701ab6bdb731fba1bcd042a7f246baaa1 Mon Sep 17 00:00:00 2001 From: Austin-X Date: Sun, 2 Mar 2025 06:59:48 +0000 Subject: [PATCH 02/20] Auto-formatted the code using Prettier --- .../frontend/src/pages/Home/Home.tsx | 104 ++++++------ .../frontend/src/pages/Home/TimetableCard.tsx | 152 +++++++++--------- .../src/pages/Home/TimetableCardKebabMenu.tsx | 108 ++++++------- .../src/pages/Home/TimetableCompareButton.tsx | 66 ++++---- .../pages/Home/TimetableCreateNewButton.tsx | 12 +- .../TimetableBuilder/TimetableBuilder.tsx | 4 +- 6 files changed, 223 insertions(+), 223 deletions(-) diff --git a/course-matrix/frontend/src/pages/Home/Home.tsx b/course-matrix/frontend/src/pages/Home/Home.tsx index 74cfc3f5..2d0071ec 100644 --- a/course-matrix/frontend/src/pages/Home/Home.tsx +++ b/course-matrix/frontend/src/pages/Home/Home.tsx @@ -5,59 +5,59 @@ import TimetableCompareButton from "./TimetableCompareButton"; import TimetableCreateNewButton from "./TimetableCreateNewButton"; const Home = () => { - // const user_metadata = JSON.parse(localStorage.getItem("userInfo") ?? "{}"); - // const username = (user_metadata?.user?.user_metadata?.username as string) ?? "John Doe"; - const username = "Me"; + // const user_metadata = JSON.parse(localStorage.getItem("userInfo") ?? "{}"); + // const username = (user_metadata?.user?.user_metadata?.username as string) ?? "John Doe"; + const username = "Me"; - return ( -
-
-
-

My Timetables

- -
-
-
- - - -
-
- - -
-
-
-
- {[...Array(12).keys()].map((_, index) => { - return ( - - ); - })} -
-
-
- ); + return ( +
+
+
+

My Timetables

+ +
+
+
+ + + +
+
+ + +
+
+
+
+ {[...Array(12).keys()].map((_, index) => { + return ( + + ); + })} +
+
+
+ ); }; export default Home; diff --git a/course-matrix/frontend/src/pages/Home/TimetableCard.tsx b/course-matrix/frontend/src/pages/Home/TimetableCard.tsx index 3eb2acd9..4892652b 100644 --- a/course-matrix/frontend/src/pages/Home/TimetableCard.tsx +++ b/course-matrix/frontend/src/pages/Home/TimetableCard.tsx @@ -1,9 +1,9 @@ import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -12,83 +12,83 @@ import { useState } from "react"; import TimetableCardKebabMenu from "./TimetableCardKebabMenu"; interface TimetableCardProps { - title: string; - lastEditedDate: Date; - owner: string; + title: string; + lastEditedDate: Date; + owner: string; } const TimetableCard = ({ - title, - lastEditedDate, - owner, + title, + lastEditedDate, + owner, }: TimetableCardProps) => { - 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 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 [timetableCardTitle, setTimetableCardTitle] = useState(title); + const [isEditingTitle, setIsEditingTitle] = useState(false); - return ( - - - Timetable default image -
- - setTimetableCardTitle(e.target.value)} - /> - -
- {!isEditingTitle && ( - <> - - - - )} - {isEditingTitle && ( - - )} -
-
-
- - -
Last edited {lastEditedDateTimestamp}
-
Owned by: {owner}
-
-
-
- ); + 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 index 9f5c0c74..e15d88b2 100644 --- a/course-matrix/frontend/src/pages/Home/TimetableCardKebabMenu.tsx +++ b/course-matrix/frontend/src/pages/Home/TimetableCardKebabMenu.tsx @@ -1,66 +1,66 @@ import { Button } from "@/components/ui/button"; import { Link } from "react-router-dom"; import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, } from "@/components/ui/dropdown-menu"; import { - Dialog, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, - DialogClose, + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, + DialogClose, } from "@/components/ui/dialog"; import { EllipsisVertical } from "lucide-react"; const TimetableCardKebabMenu = () => ( - - - - - - - Edit Timetable - - e.preventDefault()}> - - - - - - - - Delete Timetable - - - Are you sure you want to delete your timetable? This action - cannot be undone. - - - - - - - - - - - - - - - + + + + + + + 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 index 5dfb579e..5a785d1d 100644 --- a/course-matrix/frontend/src/pages/Home/TimetableCompareButton.tsx +++ b/course-matrix/frontend/src/pages/Home/TimetableCompareButton.tsx @@ -1,43 +1,43 @@ import { Button } from "@/components/ui/button"; import { - Dialog, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, - DialogClose, + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, + DialogClose, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; const TimetableCompareDialog = () => ( - - - - - - - Compare Timetables - Compare 2 of your timetables - - - - - - - - - - - - - - - + + + + + + + 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 index 364ecc14..35b9088f 100644 --- a/course-matrix/frontend/src/pages/Home/TimetableCreateNewButton.tsx +++ b/course-matrix/frontend/src/pages/Home/TimetableCreateNewButton.tsx @@ -3,12 +3,12 @@ import { Plus } from "lucide-react"; import { Link } from "react-router-dom"; const TimetableCreateNewButton = () => ( - - - + + + ); export default TimetableCreateNewButton; diff --git a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx index 9349cb2d..6a5c9e98 100644 --- a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx +++ b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx @@ -34,8 +34,8 @@ import { FilterForm, FilterFormSchema } from "@/models/filter-form"; import { useGetCoursesQuery } from "@/api/coursesApiSlice"; import { useDebounceValue } from "@/utils/useDebounce"; import SearchFilters from "./SearchFilters"; -import Calendar from 'react-calendar'; -import 'react-calendar/dist/Calendar.css'; +import Calendar from "react-calendar"; +import "react-calendar/dist/Calendar.css"; type ValuePiece = Date | null; type Value = ValuePiece | [ValuePiece, ValuePiece]; From 3a38db577da0c0a48cf8d4408846feb67d7fd478 Mon Sep 17 00:00:00 2001 From: FLAME Date: Sun, 2 Mar 2025 02:07:43 -0500 Subject: [PATCH 03/20] Add documentation comments --- .../frontend/src/pages/Home/Home.tsx | 108 ++++++------ .../frontend/src/pages/Home/TimetableCard.tsx | 157 +++++++++--------- .../src/pages/Home/TimetableCardKebabMenu.tsx | 112 +++++++------ .../src/pages/Home/TimetableCompareButton.tsx | 70 ++++---- .../pages/Home/TimetableCreateNewButton.tsx | 16 +- 5 files changed, 242 insertions(+), 221 deletions(-) diff --git a/course-matrix/frontend/src/pages/Home/Home.tsx b/course-matrix/frontend/src/pages/Home/Home.tsx index 74cfc3f5..97626692 100644 --- a/course-matrix/frontend/src/pages/Home/Home.tsx +++ b/course-matrix/frontend/src/pages/Home/Home.tsx @@ -4,60 +4,64 @@ import TimetableCard from "./TimetableCard"; import TimetableCompareButton from "./TimetableCompareButton"; import TimetableCreateNewButton from "./TimetableCreateNewButton"; +/** + * 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 username = (user_metadata?.user?.user_metadata?.username as string) ?? "John Doe"; - const username = "Me"; + // const user_metadata = JSON.parse(localStorage.getItem("userInfo") ?? "{}"); + // const username = (user_metadata?.user?.user_metadata?.username as string) ?? "John Doe"; + const username = "Me"; - return ( -
-
-
-

My Timetables

- -
-
-
- - - -
-
- - -
-
-
-
- {[...Array(12).keys()].map((_, index) => { - return ( - - ); - })} -
-
-
- ); + return ( +
+
+
+

My Timetables

+ +
+
+
+ + + +
+
+ + +
+
+
+
+ {[...Array(12).keys()].map((_, index) => { + return ( + + ); + })} +
+
+
+ ); }; export default Home; diff --git a/course-matrix/frontend/src/pages/Home/TimetableCard.tsx b/course-matrix/frontend/src/pages/Home/TimetableCard.tsx index 3eb2acd9..5f87a22a 100644 --- a/course-matrix/frontend/src/pages/Home/TimetableCard.tsx +++ b/course-matrix/frontend/src/pages/Home/TimetableCard.tsx @@ -1,9 +1,9 @@ import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -12,83 +12,88 @@ import { useState } from "react"; import TimetableCardKebabMenu from "./TimetableCardKebabMenu"; interface TimetableCardProps { - title: string; - lastEditedDate: Date; - owner: string; + 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 = ({ - title, - lastEditedDate, - owner, + title, + lastEditedDate, + owner, }: TimetableCardProps) => { - 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 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 [timetableCardTitle, setTimetableCardTitle] = useState(title); + const [isEditingTitle, setIsEditingTitle] = useState(false); - return ( - - - Timetable default image -
- - setTimetableCardTitle(e.target.value)} - /> - -
- {!isEditingTitle && ( - <> - - - - )} - {isEditingTitle && ( - - )} -
-
-
- - -
Last edited {lastEditedDateTimestamp}
-
Owned by: {owner}
-
-
-
- ); + 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 index 9f5c0c74..2bc3afc8 100644 --- a/course-matrix/frontend/src/pages/Home/TimetableCardKebabMenu.tsx +++ b/course-matrix/frontend/src/pages/Home/TimetableCardKebabMenu.tsx @@ -1,66 +1,70 @@ import { Button } from "@/components/ui/button"; import { Link } from "react-router-dom"; import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, } from "@/components/ui/dropdown-menu"; import { - Dialog, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, - DialogClose, + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, + DialogClose, } from "@/components/ui/dialog"; import { EllipsisVertical } from "lucide-react"; +/** + * 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 = () => ( - - - - - - - Edit Timetable - - e.preventDefault()}> - - - - - - - - Delete Timetable - - - Are you sure you want to delete your timetable? This action - cannot be undone. - - - - - - - - - - - - - - - + + + + + + + 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 index 5dfb579e..7a7451d9 100644 --- a/course-matrix/frontend/src/pages/Home/TimetableCompareButton.tsx +++ b/course-matrix/frontend/src/pages/Home/TimetableCompareButton.tsx @@ -1,43 +1,47 @@ import { Button } from "@/components/ui/button"; import { - Dialog, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, - DialogClose, + 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 - - - - - - - - - - - - - - - + + + + + + + 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 index 364ecc14..6a9e0283 100644 --- a/course-matrix/frontend/src/pages/Home/TimetableCreateNewButton.tsx +++ b/course-matrix/frontend/src/pages/Home/TimetableCreateNewButton.tsx @@ -2,13 +2,17 @@ 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; From 877e797e072e2dea56b49ec9fe805f3110ae2cf6 Mon Sep 17 00:00:00 2001 From: FLAME Date: Sun, 2 Mar 2025 21:05:50 -0500 Subject: [PATCH 04/20] Use schedule-x package for the calendar --- course-matrix/frontend/package-lock.json | 163 ++++++++---------- course-matrix/frontend/package.json | 4 +- .../src/pages/TimetableBuilder/Calendar.tsx | 53 ++++++ .../TimetableBuilder/TimetableBuilder.tsx | 8 +- 4 files changed, 127 insertions(+), 101 deletions(-) create mode 100644 course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx diff --git a/course-matrix/frontend/package-lock.json b/course-matrix/frontend/package-lock.json index 64b585d1..db364ff8 100644 --- a/course-matrix/frontend/package-lock.json +++ b/course-matrix/frontend/package-lock.json @@ -26,12 +26,14 @@ "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@reduxjs/toolkit": "^2.5.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", "lucide-react": "^0.474.0", "react": "^18.3.1", - "react-calendar": "^5.1.0", "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", "react-redux": "^9.2.0", @@ -1356,6 +1358,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", @@ -2489,6 +2517,36 @@ "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/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/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2862,14 +2920,6 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, - "node_modules/@wojtekmaj/date-utils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.5.1.tgz", - "integrity": "sha512-+i7+JmNiE/3c9FKxzWFi2IjRJ+KzZl1QPu6QNrsgaa2MuBgXvUy4gA1TVzf/JMdIIloB76xSKikTWuyYAIVLww==", - "funding": { - "url": "https://github.com/wojtekmaj/date-utils?sponsor=1" - } - }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -3906,17 +3956,6 @@ "node": ">=6" } }, - "node_modules/get-user-locale": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/get-user-locale/-/get-user-locale-2.3.2.tgz", - "integrity": "sha512-O2GWvQkhnbDoWFUJfaBlDIKUEdND8ATpBXD6KXcbhxlfktyD/d8w6mkzM/IlQEqGZAMz/PW6j6Hv53BiigKLUQ==", - "dependencies": { - "mem": "^8.0.0" - }, - "funding": { - "url": "https://github.com/wojtekmaj/get-user-locale?sponsor=1" - } - }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -4670,17 +4709,6 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dependencies": { - "p-defer": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -4956,21 +4984,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/mem": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", - "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", - "dependencies": { - "map-age-cleaner": "^0.1.3", - "mimic-fn": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/mem?sponsor=1" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5526,14 +5539,6 @@ "node": ">=8.6" } }, - "node_modules/mimic-fn": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", - "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5648,14 +5653,6 @@ "node": ">= 0.8.0" } }, - "node_modules/p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", - "engines": { - "node": ">=4" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5936,6 +5933,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", @@ -5993,30 +6000,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-calendar": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-5.1.0.tgz", - "integrity": "sha512-09o/rQHPZGEi658IXAJtWfra1N69D1eFnuJ3FQm9qUVzlzNnos1+GWgGiUeSs22QOpNm32aoVFOimq0p3Ug9Eg==", - "dependencies": { - "@wojtekmaj/date-utils": "^1.1.3", - "clsx": "^2.0.0", - "get-user-locale": "^2.2.1", - "warning": "^4.0.0" - }, - "funding": { - "url": "https://github.com/wojtekmaj/react-calendar?sponsor=1" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -7186,14 +7169,6 @@ } } }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/course-matrix/frontend/package.json b/course-matrix/frontend/package.json index b105d611..aaa491a0 100644 --- a/course-matrix/frontend/package.json +++ b/course-matrix/frontend/package.json @@ -28,12 +28,14 @@ "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@reduxjs/toolkit": "^2.5.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", "lucide-react": "^0.474.0", "react": "^18.3.1", - "react-calendar": "^5.1.0", "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", "react-redux": "^9.2.0", 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..9b07a6c3 --- /dev/null +++ b/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx @@ -0,0 +1,53 @@ +import { useCalendarApp, ScheduleXCalendar } from '@schedule-x/react' +import { + createViewDay, + createViewMonthAgenda, + createViewMonthGrid, + createViewWeek, +} from '@schedule-x/calendar' +import { createEventsServicePlugin } from '@schedule-x/events-service' + +import '@schedule-x/theme-default/dist/index.css' +import { useEffect, useState } from 'react' + +function Calendar() { + const eventsService = useState(() => createEventsServicePlugin())[0] + + const calendar = useCalendarApp({ + views: [createViewDay(), createViewWeek(), createViewMonthGrid(), createViewMonthAgenda()], + events: [ + { + id: '1', + title: 'Event 1', + start: '2025-03-05', + end: '2023-03-07', + }, + { + id: '2', + title: 'Event 2', + start: '2025-02-26', + end: '2025-02-28', + }, + { + id: '3', + title: 'Event 3', + start: '2025-02-24 07:00', + end: '2025-02-24 09:00', + }, + ], + plugins: [eventsService] + }) + + useEffect(() => { + // get all events + eventsService.getAll() + }, [eventsService]) + + return ( +
+ +
+ ) +} + +export default Calendar \ No newline at end of file diff --git a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx index 6a5c9e98..d75dc48e 100644 --- a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx +++ b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx @@ -34,11 +34,7 @@ import { FilterForm, FilterFormSchema } from "@/models/filter-form"; import { useGetCoursesQuery } from "@/api/coursesApiSlice"; import { useDebounceValue } from "@/utils/useDebounce"; import SearchFilters from "./SearchFilters"; -import Calendar from "react-calendar"; -import "react-calendar/dist/Calendar.css"; - -type ValuePiece = Date | null; -type Value = ValuePiece | [ValuePiece, ValuePiece]; +import Calendar from "./Calendar"; type FormContextType = UseFormReturn>; export const FormContext = createContext(null); @@ -338,7 +334,6 @@ const TimetableBuilder = () => {

-

{isCustomSettingsOpen && ( @@ -362,6 +357,7 @@ const TimetableBuilder = () => { )} + ); }; From e3160da933ab18344614c668b380108bcafb9fe8 Mon Sep 17 00:00:00 2001 From: Austin-X Date: Mon, 3 Mar 2025 02:06:09 +0000 Subject: [PATCH 05/20] Auto-formatted the code using Prettier --- .../src/pages/TimetableBuilder/Calendar.tsx | 67 ++++++++++--------- .../TimetableBuilder/TimetableBuilder.tsx | 3 +- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx b/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx index 9b07a6c3..6eedda44 100644 --- a/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx +++ b/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx @@ -1,53 +1,58 @@ -import { useCalendarApp, ScheduleXCalendar } from '@schedule-x/react' +import { useCalendarApp, ScheduleXCalendar } from "@schedule-x/react"; import { createViewDay, createViewMonthAgenda, createViewMonthGrid, createViewWeek, -} from '@schedule-x/calendar' -import { createEventsServicePlugin } from '@schedule-x/events-service' - -import '@schedule-x/theme-default/dist/index.css' -import { useEffect, useState } from 'react' - +} from "@schedule-x/calendar"; +import { createEventsServicePlugin } from "@schedule-x/events-service"; + +import "@schedule-x/theme-default/dist/index.css"; +import { useEffect, useState } from "react"; + function Calendar() { - const eventsService = useState(() => createEventsServicePlugin())[0] - + const eventsService = useState(() => createEventsServicePlugin())[0]; + const calendar = useCalendarApp({ - views: [createViewDay(), createViewWeek(), createViewMonthGrid(), createViewMonthAgenda()], + views: [ + createViewDay(), + createViewWeek(), + createViewMonthGrid(), + createViewMonthAgenda(), + ], events: [ { - id: '1', - title: 'Event 1', - start: '2025-03-05', - end: '2023-03-07', + id: "1", + title: "Event 1", + start: "2025-03-05", + end: "2023-03-07", }, { - id: '2', - title: 'Event 2', - start: '2025-02-26', - end: '2025-02-28', + id: "2", + title: "Event 2", + start: "2025-02-26", + end: "2025-02-28", }, { - id: '3', - title: 'Event 3', - start: '2025-02-24 07:00', - end: '2025-02-24 09:00', + id: "3", + title: "Event 3", + start: "2025-02-24 07:00", + end: "2025-02-24 09:00", }, ], - plugins: [eventsService] - }) - + plugins: [eventsService], + }); + useEffect(() => { // get all events - eventsService.getAll() - }, [eventsService]) - + eventsService.getAll(); + }, [eventsService]); + return (
- ) + ); } - -export default Calendar \ No newline at end of file + +export default Calendar; diff --git a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx index d75dc48e..2c9fcd1a 100644 --- a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx +++ b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx @@ -333,8 +333,7 @@ const TimetableBuilder = () => {
-

-

+

{isCustomSettingsOpen && ( Date: Sun, 2 Mar 2025 21:53:35 -0500 Subject: [PATCH 06/20] Update course-matrix/frontend/src/pages/Home/TimetableCompareButton.tsx Co-authored-by: Kevin Lan --- .../frontend/src/pages/Home/TimetableCompareButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/course-matrix/frontend/src/pages/Home/TimetableCompareButton.tsx b/course-matrix/frontend/src/pages/Home/TimetableCompareButton.tsx index 7a7451d9..963709b3 100644 --- a/course-matrix/frontend/src/pages/Home/TimetableCompareButton.tsx +++ b/course-matrix/frontend/src/pages/Home/TimetableCompareButton.tsx @@ -19,7 +19,7 @@ import { Label } from "@/components/ui/label"; const TimetableCompareDialog = () => ( - From ecba887db7161bad1a0a6900c9caf7ff3ec3b8e7 Mon Sep 17 00:00:00 2001 From: Austin-X Date: Sun, 2 Mar 2025 21:53:40 -0500 Subject: [PATCH 07/20] Update course-matrix/frontend/src/pages/Home/TimetableCreateNewButton.tsx Co-authored-by: Kevin Lan --- .../frontend/src/pages/Home/TimetableCreateNewButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/course-matrix/frontend/src/pages/Home/TimetableCreateNewButton.tsx b/course-matrix/frontend/src/pages/Home/TimetableCreateNewButton.tsx index 6a9e0283..a80952d0 100644 --- a/course-matrix/frontend/src/pages/Home/TimetableCreateNewButton.tsx +++ b/course-matrix/frontend/src/pages/Home/TimetableCreateNewButton.tsx @@ -8,7 +8,7 @@ import { Link } from "react-router-dom"; */ const TimetableCreateNewButton = () => ( - From de5c896b215aef9543ab85e2ef01bbc9c5649757 Mon Sep 17 00:00:00 2001 From: FLAME Date: Thu, 6 Mar 2025 20:42:30 -0500 Subject: [PATCH 08/20] Finish Integration on the Home Page --- course-matrix/backend/src/index.ts | 2 +- course-matrix/frontend/package-lock.json | 16 + course-matrix/frontend/package.json | 2 + .../frontend/src/api/baseApiSlice.ts | 2 +- course-matrix/frontend/src/api/config.ts | 2 + .../frontend/src/api/eventsApiSlice.ts | 66 ++ .../frontend/src/api/timetableApiSlice.ts | 65 ++ .../frontend/src/models/timetable-form.ts | 2 +- .../frontend/src/pages/Home/Home.tsx | 117 ++-- .../frontend/src/pages/Home/TimetableCard.tsx | 169 ++--- .../src/pages/Home/TimetableCardKebabMenu.tsx | 136 ++-- .../src/pages/TimetableBuilder/Calendar.tsx | 144 +++-- .../TimetableBuilder/TimetableBuilder.tsx | 596 ++++++++++-------- .../frontend/src/utils/type-utils.ts | 9 + 14 files changed, 815 insertions(+), 513 deletions(-) create mode 100644 course-matrix/frontend/src/api/eventsApiSlice.ts create mode 100644 course-matrix/frontend/src/api/timetableApiSlice.ts diff --git a/course-matrix/backend/src/index.ts b/course-matrix/backend/src/index.ts index bcdcb594..b2bcdc2e 100644 --- a/course-matrix/backend/src/index.ts +++ b/course-matrix/backend/src/index.ts @@ -27,7 +27,7 @@ const swaggerDocs = swaggerjsdoc(swaggerOptions); app.use( cors({ origin: config.CLIENT_APP_URL, - methods: ["POST"], + methods: ["POST", "PUT", "GET", "OPTIONS", "HEAD", "DELETE"], credentials: true, allowedHeaders: ["Content-Type"], }), diff --git a/course-matrix/frontend/package-lock.json b/course-matrix/frontend/package-lock.json index db364ff8..86bf7997 100644 --- a/course-matrix/frontend/package-lock.json +++ b/course-matrix/frontend/package-lock.json @@ -26,6 +26,8 @@ "@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", @@ -2527,6 +2529,20 @@ "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", diff --git a/course-matrix/frontend/package.json b/course-matrix/frontend/package.json index aaa491a0..75e98036 100644 --- a/course-matrix/frontend/package.json +++ b/course-matrix/frontend/package.json @@ -28,6 +28,8 @@ "@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", 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..91fab262 --- /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..c611652b --- /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/models/timetable-form.ts b/course-matrix/frontend/src/models/timetable-form.ts index e4800f91..3e336719 100644 --- a/course-matrix/frontend/src/models/timetable-form.ts +++ b/course-matrix/frontend/src/models/timetable-form.ts @@ -211,7 +211,7 @@ export const TimetableFormSchema: ZodType = z ); export const baseTimetableForm: TimetableForm = { - name: "New Timetable", + name: "Edit Timetable", date: new Date(), semester: "Fall 2025", search: "", diff --git a/course-matrix/frontend/src/pages/Home/Home.tsx b/course-matrix/frontend/src/pages/Home/Home.tsx index 97626692..d31c8f75 100644 --- a/course-matrix/frontend/src/pages/Home/Home.tsx +++ b/course-matrix/frontend/src/pages/Home/Home.tsx @@ -3,65 +3,78 @@ import { Pin } from "lucide-react"; import TimetableCard from "./TimetableCard"; import TimetableCompareButton from "./TimetableCompareButton"; import TimetableCreateNewButton from "./TimetableCreateNewButton"; +import { useGetTimetablesQuery } from "../../api/timetableApiSlice"; +import { Timetable } from "../../utils/type-utils"; /** * 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 username = (user_metadata?.user?.user_metadata?.username as string) ?? "John Doe"; - const username = "Me"; + const user_metadata = JSON.parse(localStorage.getItem("userInfo") ?? "{}"); + const name = + (user_metadata?.user?.user_metadata?.username as string) ?? + (user_metadata?.user?.email as string); - return ( -
-
-
-

My Timetables

- -
-
-
- - - -
-
- - -
-
-
-
- {[...Array(12).keys()].map((_, index) => { - return ( - - ); - })} -
-
-
- ); + const { data, isLoading } = useGetTimetablesQuery() as { + data: Timetable[]; + isLoading: boolean; + }; + + 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 index 5f87a22a..a4656850 100644 --- a/course-matrix/frontend/src/pages/Home/TimetableCard.tsx +++ b/course-matrix/frontend/src/pages/Home/TimetableCard.tsx @@ -1,20 +1,22 @@ import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + 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 { - title: string; - lastEditedDate: Date; - owner: string; + timetableId: number; + title: string; + lastEditedDate: Date; + owner: string; } /** @@ -23,77 +25,92 @@ interface TimetableCardProps { * @returns {JSX.Element} The rendered component. */ const TimetableCard = ({ - title, - lastEditedDate, - owner, + timetableId, + title, + lastEditedDate, + owner, }: TimetableCardProps) => { - 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 [updateTimetable] = useUpdateTimetableMutation(); - const [timetableCardTitle, setTimetableCardTitle] = useState(title); - const [isEditingTitle, setIsEditingTitle] = useState(false); + 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; - return ( - - - Timetable default image -
- - setTimetableCardTitle(e.target.value)} - /> - -
- {!isEditingTitle && ( - <> - - - - )} - {isEditingTitle && ( - - )} -
-
-
- - -
Last edited {lastEditedDateTimestamp}
-
Owned by: {owner}
-
-
-
- ); + 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 index 2bc3afc8..40115ce7 100644 --- a/course-matrix/frontend/src/pages/Home/TimetableCardKebabMenu.tsx +++ b/course-matrix/frontend/src/pages/Home/TimetableCardKebabMenu.tsx @@ -1,70 +1,94 @@ import { Button } from "@/components/ui/button"; import { Link } from "react-router-dom"; import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, } from "@/components/ui/dropdown-menu"; import { - Dialog, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, - DialogClose, + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, + DialogClose, } from "@/components/ui/dialog"; import { EllipsisVertical } from "lucide-react"; +import { useDeleteTimetableMutation } from "@/api/timetableApiSlice"; + +interface TimetableCardKebabMenuProps { + 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 = () => ( - - - - - - - Edit Timetable - - e.preventDefault()}> - - - - - - - - Delete Timetable - - - Are you sure you want to delete your timetable? This action - cannot be undone. - - - - - - - - - - - - - - - -); +const TimetableCardKebabMenu = ({ + timetableId, +}: TimetableCardKebabMenuProps) => { + const [deleteTimetable] = useDeleteTimetableMutation(); + + const handleDelete = async () => { + try { + await deleteTimetable(timetableId); + window.location.reload(); + } 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/TimetableBuilder/Calendar.tsx b/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx index 6eedda44..0fd75dc4 100644 --- a/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx +++ b/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx @@ -1,58 +1,104 @@ -import { useCalendarApp, ScheduleXCalendar } from "@schedule-x/react"; +import { ScheduleXCalendar } from "@schedule-x/react"; import { - createViewDay, - createViewMonthAgenda, - createViewMonthGrid, - createViewWeek, + createCalendar, + createViewDay, + createViewMonthAgenda, + createViewMonthGrid, + createViewWeek, + viewWeek, } from "@schedule-x/calendar"; -import { createEventsServicePlugin } from "@schedule-x/events-service"; - +import { createDragAndDropPlugin } from "@schedule-x/drag-and-drop"; +import { createEventModalPlugin } from "@schedule-x/event-modal"; import "@schedule-x/theme-default/dist/index.css"; -import { useEffect, useState } from "react"; - -function Calendar() { - const eventsService = useState(() => createEventsServicePlugin())[0]; +import { Button } from "@/components/ui/button"; - const calendar = useCalendarApp({ - views: [ - createViewDay(), - createViewWeek(), - createViewMonthGrid(), - createViewMonthAgenda(), - ], - events: [ - { - id: "1", - title: "Event 1", - start: "2025-03-05", - end: "2023-03-07", - }, - { - id: "2", - title: "Event 2", - start: "2025-02-26", - end: "2025-02-28", - }, - { - id: "3", - title: "Event 3", - start: "2025-02-24 07:00", - end: "2025-02-24 09:00", - }, - ], - plugins: [eventsService], - }); +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", + }) + ); - useEffect(() => { - // get all events - eventsService.getAll(); - }, [eventsService]); + 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 ( -
- -
- ); + 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 2c9fcd1a..b2f7053c 100644 --- a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx +++ b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx @@ -1,17 +1,17 @@ import { Button } from "@/components/ui/button"; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { - RestrictionSchema, - SemesterEnum, - TimetableFormSchema, - baseTimetableForm, + RestrictionSchema, + SemesterEnum, + TimetableFormSchema, + baseTimetableForm, } from "@/models/timetable-form"; import { Edit, X } from "lucide-react"; import { createContext, useEffect, useState } from "react"; @@ -19,11 +19,13 @@ import { useForm, UseFormReturn } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { - Select, - SelectTrigger, - SelectValue, - SelectContent, - SelectItem, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import CourseSearch from "@/pages/TimetableBuilder/CourseSearch"; import { mockSearchData } from "./mockSearchData"; @@ -32,9 +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); @@ -77,288 +83,324 @@ const SEARCH_LIMIT = 1000; */ const TimetableBuilder = () => { - const form = useForm>({ - resolver: zodResolver(TimetableFormSchema), - defaultValues: baseTimetableForm, - }); + const form = useForm>({ + resolver: zodResolver(TimetableFormSchema), + defaultValues: baseTimetableForm, + }); - const filterForm = useForm>({ - resolver: zodResolver(FilterFormSchema), - }); + const filterForm = useForm>({ + resolver: zodResolver(FilterFormSchema), + }); - const selectedCourses = form.watch("courses") || []; - const enabledRestrictions = form.watch("restrictions") || []; - const searchQuery = form.watch("search"); - const debouncedSearchQuery = useDebounceValue(searchQuery, 250); + const [queryParams, setQueryParams] = useSearchParams(); + const isEditingTimetable = queryParams.has("edit"); - const [isEditNameOpen, setIsEditNameOpen] = useState(false); - const [isCustomSettingsOpen, setIsCustomSettingsOpen] = useState(false); - const [filters, setFilters] = useState(null); - const [showFilters, setShowFilters] = useState(false); - const [value, onChange] = useState(new Date()); + const selectedCourses = form.watch("courses") || []; + const enabledRestrictions = form.watch("restrictions") || []; + const searchQuery = form.watch("search"); + const debouncedSearchQuery = useDebounceValue(searchQuery, 250); - const noSearchAndFilter = () => { - return !searchQuery && !filters; - }; + const [isCustomSettingsOpen, setIsCustomSettingsOpen] = useState(false); + const [filters, setFilters] = useState(null); + const [showFilters, setShowFilters] = useState(false); + const [timetableId, setTimetableId] = useState(-1); - // limit search number if no search query or filters for performance purposes. - // Otherwise, limit is 10k, which effectively gets all results. - const { data, isLoading, error, refetch } = useGetCoursesQuery({ - limit: noSearchAndFilter() ? SEARCH_LIMIT : 10000, - search: debouncedSearchQuery || undefined, - semester: form.getValues("semester"), - ...filters, - }); + const noSearchAndFilter = () => { + return !searchQuery && !filters; + }; - useEffect(() => { - if (searchQuery) { - refetch(); - } - }, [debouncedSearchQuery]); + // limit search number if no search query or filters for performance purposes. + // Otherwise, limit is 10k, which effectively gets all results. + const { data, isLoading, error, refetch } = useGetCoursesQuery({ + limit: noSearchAndFilter() ? SEARCH_LIMIT : 10000, + search: debouncedSearchQuery || undefined, + semester: form.getValues("semester"), + ...filters, + }); - const createTimetable = (values: z.infer) => { - console.log(values); - // TODO Send request to /api/timetable/create - }; + const { data: eventsData, isLoading: eventsLoading } = useGetEventsQuery( + timetableId + ) as { + data: { courseEvents: unknown[]; userEvents: unknown[] }; + isLoading: boolean; + }; + const courseEvents = eventsData?.courseEvents || []; + const userEvents = eventsData?.userEvents || []; - const handleReset = () => { - form.reset(); - filterForm.reset(); - setFilters(null); - }; + const { data: timetablesData } = useGetTimetablesQuery() as { + data: Timetable[]; + }; + const timetables = timetablesData || []; + // console.log("IS EDITING? ", isEditingTimetable); + // console.log("Timetables: ", timetables); + // console.log("Timetable ID: ", timetableId); + // console.log("EVENTS: ", eventsData); + // console.log("Course Events: ", courseEvents); + // console.log("User Events: ", userEvents); - const handleRemoveCourse = (course: { - id: number; - code: string; - name: string; - }) => { - const currentList = form.getValues("courses"); - const newList = currentList.filter((item) => item.id !== course.id); - form.setValue("courses", newList); - }; + useEffect(() => { + if (searchQuery) { + refetch(); + } + }, [debouncedSearchQuery]); - const handleAddRestriction = (values: z.infer) => { - const currentList = form.getValues("restrictions"); - const newList = [...currentList, values]; - form.setValue("restrictions", newList); - }; + const createTimetable = (values: z.infer) => { + console.log(values); + // TODO Send request to /api/timetable/create + }; - const handleRemoveRestriction = (index: number) => { - const currentList = form.getValues("restrictions"); - const newList = currentList.filter((_, i) => i !== index); - form.setValue("restrictions", newList); - }; + const handleReset = () => { + form.reset(); + filterForm.reset(); + setFilters(null); + }; - const applyFilters = (values: z.infer) => { - setFilters(values); - console.log("Apply filters", values); - }; + const handleRemoveCourse = (course: { + id: number; + code: string; + name: string; + }) => { + const currentList = form.getValues("courses"); + const newList = currentList.filter((item) => item.id !== course.id); + form.setValue("courses", newList); + }; - return ( - <> -
-
-
-
-

- {baseTimetableForm.name} -

-
- { - setIsEditNameOpen(true); - }} - /> -
-
-
- - -
-
-
-
+ const handleAddRestriction = (values: z.infer) => { + const currentList = form.getValues("restrictions"); + const newList = [...currentList, values]; + form.setValue("restrictions", newList); + }; -
-
-
- - -
- ( - - Semester - - - - - - )} - /> + const handleRemoveRestriction = (index: number) => { + const currentList = form.getValues("restrictions"); + const newList = currentList.filter((_, i) => i !== index); + form.setValue("restrictions", newList); + }; - ( - - - Pick a few courses you'd like to take - - - { - field.onChange(value); - }} - data={data} // TODO: Replace with variable data - isLoading={isLoading} - showFilter={() => setShowFilters(true)} - /> - - - - )} - /> -
-
-

- Selected courses: {selectedCourses.length} -

-
- {selectedCourses.map((course, index) => ( -
-

- {course.code}: {course.name} -

- handleRemoveCourse(course)} - /> -
- ))} -
-
+ const applyFilters = (values: z.infer) => { + setFilters(values); + console.log("Apply filters", values); + }; -
-
-

Custom Settings

-

- Add additional restrictions to your timetable to - personalize it to your needs. -

-
- -
+ return ( + <> +
+
+
+
+

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

+ {isEditingTimetable && ( + + )} +
+
+ + +
+
+
+
-
-

- Enabled Restrictions: {enabledRestrictions.length} -

-
- {enabledRestrictions.map((restric, index) => ( -
- {restric.type.startsWith("Restrict") ? ( -

- {restric.type}:{" "} - {restric.startTime - ? formatTime(restric.startTime) - : ""}{" "} - {restric.type === "Restrict Between" ? " - " : ""}{" "} - {restric.endTime - ? formatTime(restric.endTime) - : ""}{" "} - {restric.days?.join(" ")} -

- ) : ( -

- {restric.type}: At least{" "} - {restric.numDays} days off -

- )} +
+
+ + + +
+ ( + + Semester + + + + + + )} + /> - handleRemoveRestriction(index)} - /> -
- ))} -
-
+ ( + + + Pick a few courses you'd like to take + + + { + field.onChange(value); + }} + data={data} // TODO: Replace with variable data + isLoading={isLoading} + showFilter={() => setShowFilters(true)} + /> + + + + )} + /> +
+
+

+ Selected courses: {selectedCourses.length} +

+
+ {selectedCourses.map((course, index) => ( +
+

+ {course.code}: {course.name} +

+ handleRemoveCourse(course)} + /> +
+ ))} +
+
- - - - -
-
-

-
- {isCustomSettingsOpen && ( - setIsCustomSettingsOpen(false)} - /> - )} +
+
+

Custom Settings

+

+ Add additional restrictions to your timetable to + personalize it to your needs. +

+
+ +
- {showFilters && ( - setShowFilters(false)} - resetHandler={() => { - setFilters(null); - setShowFilters(false); - console.log("reset filters"); - }} - filterForm={filterForm} - /> - )} -
-
- - - ); +
+

+ Enabled Restrictions: {enabledRestrictions.length} +

+
+ {enabledRestrictions.map((restric, index) => ( +
+ {restric.type.startsWith("Restrict") ? ( +

+ {restric.type}:{" "} + {restric.startTime + ? formatTime(restric.startTime) + : ""}{" "} + {restric.type === "Restrict Between" ? " - " : ""}{" "} + {restric.endTime + ? formatTime(restric.endTime) + : ""}{" "} + {restric.days?.join(" ")} +

+ ) : ( +

+ {restric.type}: At least{" "} + {restric.numDays} days off +

+ )} + + handleRemoveRestriction(index)} + /> +
+ ))} +
+
+ + + +
+ +
+
+ +
+ {isCustomSettingsOpen && ( + setIsCustomSettingsOpen(false)} + /> + )} + + {showFilters && ( + setShowFilters(false)} + resetHandler={() => { + setFilters(null); + setShowFilters(false); + console.log("reset filters"); + }} + filterForm={filterForm} + /> + )} +
+
+ + ); }; export default TimetableBuilder; diff --git a/course-matrix/frontend/src/utils/type-utils.ts b/course-matrix/frontend/src/utils/type-utils.ts index 85328ac0..37777141 100644 --- a/course-matrix/frontend/src/utils/type-utils.ts +++ b/course-matrix/frontend/src/utils/type-utils.ts @@ -2,3 +2,12 @@ export type MakeOptionalExcept = Partial> & Pick; + +export type Timetable = { + id: number; + created_at: Date; + updated_at: Date; + semester: string; + timetable_title: string; + user_id: string; +}; \ No newline at end of file From 63a914a304901cd6192b372c49be7e91d695abe0 Mon Sep 17 00:00:00 2001 From: Austin-X Date: Fri, 7 Mar 2025 01:42:48 +0000 Subject: [PATCH 09/20] Auto-formatted the code using Prettier --- .../frontend/src/api/eventsApiSlice.ts | 112 ++-- .../frontend/src/api/timetableApiSlice.ts | 112 ++-- .../frontend/src/pages/Home/Home.tsx | 124 ++-- .../frontend/src/pages/Home/TimetableCard.tsx | 180 ++--- .../src/pages/Home/TimetableCardKebabMenu.tsx | 140 ++-- .../src/pages/TimetableBuilder/Calendar.tsx | 180 ++--- .../TimetableBuilder/TimetableBuilder.tsx | 618 +++++++++--------- .../frontend/src/utils/type-utils.ts | 2 +- 8 files changed, 734 insertions(+), 734 deletions(-) diff --git a/course-matrix/frontend/src/api/eventsApiSlice.ts b/course-matrix/frontend/src/api/eventsApiSlice.ts index 91fab262..48f9b271 100644 --- a/course-matrix/frontend/src/api/eventsApiSlice.ts +++ b/course-matrix/frontend/src/api/eventsApiSlice.ts @@ -4,63 +4,63 @@ 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", - }), - }), - }), + 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, + useCreateEventMutation, + useGetEventsQuery, + useUpdateEventMutation, + useDeleteEventMutation, } = eventsApiSlice; diff --git a/course-matrix/frontend/src/api/timetableApiSlice.ts b/course-matrix/frontend/src/api/timetableApiSlice.ts index c611652b..35075fd3 100644 --- a/course-matrix/frontend/src/api/timetableApiSlice.ts +++ b/course-matrix/frontend/src/api/timetableApiSlice.ts @@ -3,63 +3,63 @@ 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", - }), - }), - }), + 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, + useGetTimetablesQuery, + useUpdateTimetableMutation, + useCreateTimetableMutation, + useDeleteTimetableMutation, } = timetableApiSlice; diff --git a/course-matrix/frontend/src/pages/Home/Home.tsx b/course-matrix/frontend/src/pages/Home/Home.tsx index d31c8f75..c1cf46a0 100644 --- a/course-matrix/frontend/src/pages/Home/Home.tsx +++ b/course-matrix/frontend/src/pages/Home/Home.tsx @@ -11,70 +11,70 @@ import { Timetable } from "../../utils/type-utils"; * @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 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 } = useGetTimetablesQuery() as { - data: Timetable[]; - isLoading: boolean; - }; + const { data, isLoading } = useGetTimetablesQuery() as { + data: Timetable[]; + isLoading: boolean; + }; - return ( -
-
-
-

My Timetables

- -
-
-
- - - -
-
- - -
-
-
-
- {isLoading ? ( -

Loading...

- ) : ( - data?.map((timetable, index) => ( - - )) - )} -
-
-
- ); + 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 index a4656850..7e54fded 100644 --- a/course-matrix/frontend/src/pages/Home/TimetableCard.tsx +++ b/course-matrix/frontend/src/pages/Home/TimetableCard.tsx @@ -1,9 +1,9 @@ import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -13,10 +13,10 @@ import TimetableCardKebabMenu from "./TimetableCardKebabMenu"; import { useUpdateTimetableMutation } from "@/api/timetableApiSlice"; interface TimetableCardProps { - timetableId: number; - title: string; - lastEditedDate: Date; - owner: string; + timetableId: number; + title: string; + lastEditedDate: Date; + owner: string; } /** @@ -25,92 +25,92 @@ interface TimetableCardProps { * @returns {JSX.Element} The rendered component. */ const TimetableCard = ({ - timetableId, - title, - lastEditedDate, - owner, + timetableId, + title, + lastEditedDate, + owner, }: TimetableCardProps) => { - const [updateTimetable] = useUpdateTimetableMutation(); + 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 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 [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); - } - }; + 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}
-
-
-
- ); + 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 index 40115ce7..fa693345 100644 --- a/course-matrix/frontend/src/pages/Home/TimetableCardKebabMenu.tsx +++ b/course-matrix/frontend/src/pages/Home/TimetableCardKebabMenu.tsx @@ -1,26 +1,26 @@ import { Button } from "@/components/ui/button"; import { Link } from "react-router-dom"; import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, } from "@/components/ui/dropdown-menu"; import { - Dialog, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, - DialogClose, + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, + DialogClose, } from "@/components/ui/dialog"; import { EllipsisVertical } from "lucide-react"; import { useDeleteTimetableMutation } from "@/api/timetableApiSlice"; interface TimetableCardKebabMenuProps { - timetableId: number; + timetableId: number; } /** @@ -28,67 +28,67 @@ interface TimetableCardKebabMenuProps { * @returns {JSX.Element} The rendered component. */ const TimetableCardKebabMenu = ({ - timetableId, + timetableId, }: TimetableCardKebabMenuProps) => { - const [deleteTimetable] = useDeleteTimetableMutation(); + const [deleteTimetable] = useDeleteTimetableMutation(); - const handleDelete = async () => { - try { - await deleteTimetable(timetableId); + const handleDelete = async () => { + try { + await deleteTimetable(timetableId); window.location.reload(); - } catch (error) { - console.error("Failed to delete timetable:", error); - } - }; + } 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. - - - - - - - - - - - - - - - - ); + 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/TimetableBuilder/Calendar.tsx b/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx index 0fd75dc4..6f6713f8 100644 --- a/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx +++ b/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx @@ -1,11 +1,11 @@ import { ScheduleXCalendar } from "@schedule-x/react"; import { - createCalendar, - createViewDay, - createViewMonthAgenda, - createViewMonthGrid, - createViewWeek, - viewWeek, + 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"; @@ -13,92 +13,92 @@ 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", - }) - ); + 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()], - }); + 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
- -

- -
- ); + 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 b2f7053c..005d31b2 100644 --- a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx +++ b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx @@ -1,17 +1,17 @@ import { Button } from "@/components/ui/button"; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { - RestrictionSchema, - SemesterEnum, - TimetableFormSchema, - baseTimetableForm, + RestrictionSchema, + SemesterEnum, + TimetableFormSchema, + baseTimetableForm, } from "@/models/timetable-form"; import { Edit, X } from "lucide-react"; import { createContext, useEffect, useState } from "react"; @@ -19,13 +19,13 @@ import { useForm, UseFormReturn } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import CourseSearch from "@/pages/TimetableBuilder/CourseSearch"; import { mockSearchData } from "./mockSearchData"; @@ -83,324 +83,324 @@ const SEARCH_LIMIT = 1000; */ const TimetableBuilder = () => { - const form = useForm>({ - resolver: zodResolver(TimetableFormSchema), - defaultValues: baseTimetableForm, - }); + const form = useForm>({ + resolver: zodResolver(TimetableFormSchema), + defaultValues: baseTimetableForm, + }); - const filterForm = useForm>({ - resolver: zodResolver(FilterFormSchema), - }); + const filterForm = useForm>({ + resolver: zodResolver(FilterFormSchema), + }); const [queryParams, setQueryParams] = useSearchParams(); const isEditingTimetable = queryParams.has("edit"); - const selectedCourses = form.watch("courses") || []; - const enabledRestrictions = form.watch("restrictions") || []; - const searchQuery = form.watch("search"); - const debouncedSearchQuery = useDebounceValue(searchQuery, 250); + const selectedCourses = form.watch("courses") || []; + const enabledRestrictions = form.watch("restrictions") || []; + const searchQuery = form.watch("search"); + const debouncedSearchQuery = useDebounceValue(searchQuery, 250); - const [isCustomSettingsOpen, setIsCustomSettingsOpen] = useState(false); - const [filters, setFilters] = useState(null); - const [showFilters, setShowFilters] = useState(false); - const [timetableId, setTimetableId] = useState(-1); + 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; - }; + const noSearchAndFilter = () => { + return !searchQuery && !filters; + }; - // limit search number if no search query or filters for performance purposes. - // Otherwise, limit is 10k, which effectively gets all results. - const { data, isLoading, error, refetch } = useGetCoursesQuery({ - limit: noSearchAndFilter() ? SEARCH_LIMIT : 10000, - search: debouncedSearchQuery || undefined, - semester: form.getValues("semester"), - ...filters, - }); + // limit search number if no search query or filters for performance purposes. + // Otherwise, limit is 10k, which effectively gets all results. + const { data, isLoading, error, refetch } = useGetCoursesQuery({ + limit: noSearchAndFilter() ? SEARCH_LIMIT : 10000, + search: debouncedSearchQuery || undefined, + semester: form.getValues("semester"), + ...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: 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 || []; + const { data: timetablesData } = useGetTimetablesQuery() as { + data: Timetable[]; + }; + const timetables = timetablesData || []; // console.log("IS EDITING? ", isEditingTimetable); - // console.log("Timetables: ", timetables); - // console.log("Timetable ID: ", timetableId); - // console.log("EVENTS: ", eventsData); - // console.log("Course Events: ", courseEvents); - // console.log("User Events: ", userEvents); + // console.log("Timetables: ", timetables); + // console.log("Timetable ID: ", timetableId); + // console.log("EVENTS: ", eventsData); + // console.log("Course Events: ", courseEvents); + // console.log("User Events: ", userEvents); - useEffect(() => { - if (searchQuery) { - refetch(); - } - }, [debouncedSearchQuery]); + useEffect(() => { + if (searchQuery) { + refetch(); + } + }, [debouncedSearchQuery]); - const createTimetable = (values: z.infer) => { - console.log(values); - // TODO Send request to /api/timetable/create - }; + const createTimetable = (values: z.infer) => { + console.log(values); + // TODO Send request to /api/timetable/create + }; - const handleReset = () => { - form.reset(); - filterForm.reset(); - setFilters(null); - }; + const handleReset = () => { + form.reset(); + filterForm.reset(); + setFilters(null); + }; - const handleRemoveCourse = (course: { - id: number; - code: string; - name: string; - }) => { - const currentList = form.getValues("courses"); - const newList = currentList.filter((item) => item.id !== course.id); - form.setValue("courses", newList); - }; + const handleRemoveCourse = (course: { + id: number; + code: string; + name: string; + }) => { + const currentList = form.getValues("courses"); + const newList = currentList.filter((item) => item.id !== course.id); + form.setValue("courses", newList); + }; - const handleAddRestriction = (values: z.infer) => { - const currentList = form.getValues("restrictions"); - const newList = [...currentList, values]; - form.setValue("restrictions", newList); - }; + const handleAddRestriction = (values: z.infer) => { + const currentList = form.getValues("restrictions"); + const newList = [...currentList, values]; + form.setValue("restrictions", newList); + }; - const handleRemoveRestriction = (index: number) => { - const currentList = form.getValues("restrictions"); - const newList = currentList.filter((_, i) => i !== index); - form.setValue("restrictions", newList); - }; + const handleRemoveRestriction = (index: number) => { + const currentList = form.getValues("restrictions"); + const newList = currentList.filter((_, i) => i !== index); + form.setValue("restrictions", newList); + }; - const applyFilters = (values: z.infer) => { - setFilters(values); - console.log("Apply filters", values); - }; + const applyFilters = (values: z.infer) => { + setFilters(values); + console.log("Apply filters", values); + }; - return ( - <> -
-
-
-
-

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

- {isEditingTimetable && ( - - )} -
-
- - -
-
-
-
+ return ( + <> +
+
+
+
+

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

+ {isEditingTimetable && ( + + )} +
+
+ + +
+
+
+
-
-
-
- - -
- ( - - Semester - - - - - - )} - /> +
+
+ + + +
+ ( + + Semester + + + + + + )} + /> - ( - - - Pick a few courses you'd like to take - - - { - field.onChange(value); - }} - data={data} // TODO: Replace with variable data - isLoading={isLoading} - showFilter={() => setShowFilters(true)} - /> - - - - )} - /> -
-
-

- Selected courses: {selectedCourses.length} -

-
- {selectedCourses.map((course, index) => ( -
-

- {course.code}: {course.name} -

- handleRemoveCourse(course)} - /> -
- ))} -
-
+ ( + + + Pick a few courses you'd like to take + + + { + field.onChange(value); + }} + data={data} // TODO: Replace with variable data + isLoading={isLoading} + showFilter={() => setShowFilters(true)} + /> + + + + )} + /> +
+
+

+ Selected courses: {selectedCourses.length} +

+
+ {selectedCourses.map((course, index) => ( +
+

+ {course.code}: {course.name} +

+ handleRemoveCourse(course)} + /> +
+ ))} +
+
-
-
-

Custom Settings

-

- Add additional restrictions to your timetable to - personalize it to your needs. -

-
- -
+
+
+

Custom Settings

+

+ Add additional restrictions to your timetable to + personalize it to your needs. +

+
+ +
-
-

- Enabled Restrictions: {enabledRestrictions.length} -

-
- {enabledRestrictions.map((restric, index) => ( -
- {restric.type.startsWith("Restrict") ? ( -

- {restric.type}:{" "} - {restric.startTime - ? formatTime(restric.startTime) - : ""}{" "} - {restric.type === "Restrict Between" ? " - " : ""}{" "} - {restric.endTime - ? formatTime(restric.endTime) - : ""}{" "} - {restric.days?.join(" ")} -

- ) : ( -

- {restric.type}: At least{" "} - {restric.numDays} days off -

- )} +
+

+ Enabled Restrictions: {enabledRestrictions.length} +

+
+ {enabledRestrictions.map((restric, index) => ( +
+ {restric.type.startsWith("Restrict") ? ( +

+ {restric.type}:{" "} + {restric.startTime + ? formatTime(restric.startTime) + : ""}{" "} + {restric.type === "Restrict Between" ? " - " : ""}{" "} + {restric.endTime + ? formatTime(restric.endTime) + : ""}{" "} + {restric.days?.join(" ")} +

+ ) : ( +

+ {restric.type}: At least{" "} + {restric.numDays} days off +

+ )} - handleRemoveRestriction(index)} - /> -
- ))} -
-
+ handleRemoveRestriction(index)} + /> +
+ ))} +
+
- - - - -
-
- -
- {isCustomSettingsOpen && ( - setIsCustomSettingsOpen(false)} - /> - )} + + + + +
+
+ +
+ {isCustomSettingsOpen && ( + setIsCustomSettingsOpen(false)} + /> + )} - {showFilters && ( - setShowFilters(false)} - resetHandler={() => { - setFilters(null); - setShowFilters(false); - console.log("reset filters"); - }} - filterForm={filterForm} - /> - )} -
-
- - ); + {showFilters && ( + setShowFilters(false)} + resetHandler={() => { + setFilters(null); + setShowFilters(false); + console.log("reset filters"); + }} + filterForm={filterForm} + /> + )} +
+
+ + ); }; export default TimetableBuilder; diff --git a/course-matrix/frontend/src/utils/type-utils.ts b/course-matrix/frontend/src/utils/type-utils.ts index 37777141..a46a8151 100644 --- a/course-matrix/frontend/src/utils/type-utils.ts +++ b/course-matrix/frontend/src/utils/type-utils.ts @@ -10,4 +10,4 @@ export type Timetable = { semester: string; timetable_title: string; user_id: string; -}; \ No newline at end of file +}; From 0720fe5c354f3ca2305a9dd7726e9dd9576bbbb4 Mon Sep 17 00:00:00 2001 From: FLAME Date: Thu, 6 Mar 2025 21:37:20 -0500 Subject: [PATCH 10/20] Fix bugs and typos --- .../frontend/src/pages/TimetableBuilder/Calendar.tsx | 2 +- .../frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx b/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx index 6f6713f8..b787afd0 100644 --- a/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx +++ b/course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx @@ -94,7 +94,7 @@ function Calendar({ courseEvents, userEvents }) {

Your Timetable
- +

diff --git a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx index 005d31b2..e81ef9ce 100644 --- a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx +++ b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx @@ -94,6 +94,7 @@ const TimetableBuilder = () => { const [queryParams, setQueryParams] = useSearchParams(); const isEditingTimetable = queryParams.has("edit"); + const editingTimetableId = queryParams.get("edit"); const selectedCourses = form.watch("courses") || []; const enabledRestrictions = form.watch("restrictions") || []; @@ -194,9 +195,7 @@ const TimetableBuilder = () => { {isEditingTimetable && ( setTimetableId(parseInt(value))} - defaultValue={editingTimetableId.toString()} + defaultValue={editingTimetableId ? editingTimetableId.toString() : ""} > From 7e423642c15cda02fc61ef6d548eb9ea7fd06b3a Mon Sep 17 00:00:00 2001 From: Austin-X Date: Fri, 7 Mar 2025 03:15:02 +0000 Subject: [PATCH 14/20] Auto-formatted the code using Prettier --- .../frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx index d11b7556..b7d6493c 100644 --- a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx +++ b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx @@ -195,7 +195,9 @@ const TimetableBuilder = () => { {isEditingTimetable && ( setTimetableId(parseInt(value))} - defaultValue={ - editingTimetableId ? editingTimetableId.toString() : "" - } - > - - - - - - {timetables.map((timetable) => ( - - {timetable.timetable_title} - - ))} - - - - )} - -
- - -
- -
- + return ( + <> +
+
+
+
+

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

+ {isEditingTimetable && ( + + )} +
+
+ + +
+
+
+
-
-
-
- - -
- ( - - Semester - - - - - - )} - /> +
+
+ + + +
+ ( + + Semester + + + + + + )} + /> - ( - - - Pick a few courses you'd like to take - - - { - field.onChange(value); - }} - data={data} // TODO: Replace with variable data - isLoading={isLoading} - showFilter={() => setShowFilters(true)} - /> - - - - )} - /> -
-
-

- Selected courses: {selectedCourses.length} -

-
- {selectedCourses.map((course, index) => ( -
-

- {course.code}: {course.name} -

- handleRemoveCourse(course)} - /> -
- ))} -
-
+ ( + + + Pick a few courses you'd like to take + + + { + field.onChange(value); + }} + data={data} // TODO: Replace with variable data + isLoading={isLoading} + showFilter={() => setShowFilters(true)} + /> + + + + )} + /> +
+
+

+ Selected courses: {selectedCourses.length} +

+
+ {selectedCourses.map((course, index) => ( +
+

+ {course.code}: {course.name} +

+ handleRemoveCourse(course)} + /> +
+ ))} +
+
-
-
-

Custom Settings

-

- Add additional restrictions to your timetable to - personalize it to your needs. -

-
- -
+
+
+

Custom Settings

+

+ Add additional restrictions to your timetable to + personalize it to your needs. +

+
+ +
-
-

- Enabled Restrictions: {enabledRestrictions.length} -

-
- {enabledRestrictions.map((restric, index) => ( -
- {restric.type.startsWith("Restrict") ? ( -

- {restric.type}:{" "} - {restric.startTime - ? formatTime(restric.startTime) - : ""}{" "} - {restric.type === "Restrict Between" ? " - " : ""}{" "} - {restric.endTime - ? formatTime(restric.endTime) - : ""}{" "} - {restric.days?.join(" ")} -

- ) : ( -

- {restric.type}: At least{" "} - {restric.numDays} days off -

- )} +
+

+ Enabled Restrictions: {enabledRestrictions.length} +

+
+ {enabledRestrictions.map((restric, index) => ( +
+ {restric.type.startsWith("Restrict") ? ( +

+ {restric.type}:{" "} + {restric.startTime + ? formatTime(restric.startTime) + : ""}{" "} + {restric.type === "Restrict Between" ? " - " : ""}{" "} + {restric.endTime + ? formatTime(restric.endTime) + : ""}{" "} + {restric.days?.join(" ")} +

+ ) : ( +

+ {restric.type}: At least{" "} + {restric.numDays} days off +

+ )} - handleRemoveRestriction(index)} - /> -
- ))} -
-
+ handleRemoveRestriction(index)} + /> +
+ ))} +
+
- - - - -
-
- -
- {isCustomSettingsOpen && ( - setIsCustomSettingsOpen(false)} - /> - )} + + + + +
+
+ +
+ {isCustomSettingsOpen && ( + setIsCustomSettingsOpen(false)} + /> + )} - {showFilters && ( - setShowFilters(false)} - resetHandler={() => { - setFilters(null); - setShowFilters(false); - console.log("reset filters"); - }} - filterForm={filterForm} - /> - )} -
-
- - ); + {showFilters && ( + setShowFilters(false)} + resetHandler={() => { + setFilters(null); + setShowFilters(false); + console.log("reset filters"); + }} + filterForm={filterForm} + /> + )} +
+ + + ); }; export default TimetableBuilder; From bf48fa4505e2d1608c8a19d981ba48a22fb8ba8b Mon Sep 17 00:00:00 2001 From: Austin-X Date: Sat, 8 Mar 2025 01:27:57 +0000 Subject: [PATCH 18/20] Auto-formatted the code using Prettier --- .../TimetableBuilder/TimetableBuilder.tsx | 620 +++++++++--------- 1 file changed, 310 insertions(+), 310 deletions(-) diff --git a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx index afd10eea..33a3ab97 100644 --- a/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx +++ b/course-matrix/frontend/src/pages/TimetableBuilder/TimetableBuilder.tsx @@ -1,17 +1,17 @@ import { Button } from "@/components/ui/button"; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { - RestrictionSchema, - SemesterEnum, - TimetableFormSchema, - baseTimetableForm, + RestrictionSchema, + SemesterEnum, + TimetableFormSchema, + baseTimetableForm, } from "@/models/timetable-form"; import { Edit, X } from "lucide-react"; import { createContext, useEffect, useState } from "react"; @@ -19,13 +19,13 @@ import { useForm, UseFormReturn } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import CourseSearch from "@/pages/TimetableBuilder/CourseSearch"; import { mockSearchData } from "./mockSearchData"; @@ -83,322 +83,322 @@ const SEARCH_LIMIT = 1000; */ const TimetableBuilder = () => { - const form = useForm>({ - resolver: zodResolver(TimetableFormSchema), - defaultValues: baseTimetableForm, - }); + const form = useForm>({ + resolver: zodResolver(TimetableFormSchema), + defaultValues: baseTimetableForm, + }); - const filterForm = useForm>({ - resolver: zodResolver(FilterFormSchema), - }); + const filterForm = useForm>({ + resolver: zodResolver(FilterFormSchema), + }); - const [queryParams, setQueryParams] = useSearchParams(); - const isEditingTimetable = queryParams.has("edit"); - const editingTimetableId = queryParams.get("edit"); + 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 selectedCourses = form.watch("courses") || []; + const enabledRestrictions = form.watch("restrictions") || []; + const searchQuery = form.watch("search"); + const debouncedSearchQuery = useDebounceValue(searchQuery, 250); - const [isCustomSettingsOpen, setIsCustomSettingsOpen] = useState(false); - const [filters, setFilters] = useState(null); - const [showFilters, setShowFilters] = useState(false); - const [timetableId, setTimetableId] = useState(-1); + 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; - }; + const noSearchAndFilter = () => { + return !searchQuery && !filters; + }; - // limit search number if no search query or filters for performance purposes. - // Otherwise, limit is 10k, which effectively gets all results. - const { data, isLoading, error, refetch } = useGetCoursesQuery({ - limit: noSearchAndFilter() ? SEARCH_LIMIT : 10000, - search: debouncedSearchQuery || undefined, - semester: form.getValues("semester"), - ...filters, - }); + // limit search number if no search query or filters for performance purposes. + // Otherwise, limit is 10k, which effectively gets all results. + const { data, isLoading, error, refetch } = useGetCoursesQuery({ + limit: noSearchAndFilter() ? SEARCH_LIMIT : 10000, + search: debouncedSearchQuery || undefined, + semester: form.getValues("semester"), + ...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: 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 || []; + const { data: timetablesData } = useGetTimetablesQuery() as { + data: Timetable[]; + }; + const timetables = timetablesData || []; - useEffect(() => { - if (searchQuery) { - refetch(); - } - }, [debouncedSearchQuery]); + useEffect(() => { + if (searchQuery) { + refetch(); + } + }, [debouncedSearchQuery]); - const createTimetable = (values: z.infer) => { - console.log(values); - // TODO Send request to /api/timetable/create - }; + const createTimetable = (values: z.infer) => { + console.log(values); + // TODO Send request to /api/timetable/create + }; - const handleReset = () => { - form.reset(); - filterForm.reset(); - setFilters(null); - }; + const handleReset = () => { + form.reset(); + filterForm.reset(); + setFilters(null); + }; - const handleRemoveCourse = (course: { - id: number; - code: string; - name: string; - }) => { - const currentList = form.getValues("courses"); - const newList = currentList.filter((item) => item.id !== course.id); - form.setValue("courses", newList); - }; + const handleRemoveCourse = (course: { + id: number; + code: string; + name: string; + }) => { + const currentList = form.getValues("courses"); + const newList = currentList.filter((item) => item.id !== course.id); + form.setValue("courses", newList); + }; - const handleAddRestriction = (values: z.infer) => { - const currentList = form.getValues("restrictions"); - const newList = [...currentList, values]; - form.setValue("restrictions", newList); - }; + const handleAddRestriction = (values: z.infer) => { + const currentList = form.getValues("restrictions"); + const newList = [...currentList, values]; + form.setValue("restrictions", newList); + }; - const handleRemoveRestriction = (index: number) => { - const currentList = form.getValues("restrictions"); - const newList = currentList.filter((_, i) => i !== index); - form.setValue("restrictions", newList); - }; + const handleRemoveRestriction = (index: number) => { + const currentList = form.getValues("restrictions"); + const newList = currentList.filter((_, i) => i !== index); + form.setValue("restrictions", newList); + }; - const applyFilters = (values: z.infer) => { - setFilters(values); - console.log("Apply filters", values); - }; + const applyFilters = (values: z.infer) => { + setFilters(values); + console.log("Apply filters", values); + }; - return ( - <> -
-
-
-
-

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

- {isEditingTimetable && ( - - )} -
-
- - -
-
-
-
+ return ( + <> +
+
+
+
+

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

+ {isEditingTimetable && ( + + )} +
+
+ + +
+
+
+
-
-
-
- - -
- ( - - Semester - - - - - - )} - /> +
+
+ + + +
+ ( + + Semester + + + + + + )} + /> - ( - - - Pick a few courses you'd like to take - - - { - field.onChange(value); - }} - data={data} // TODO: Replace with variable data - isLoading={isLoading} - showFilter={() => setShowFilters(true)} - /> - - - - )} - /> -
-
-

- Selected courses: {selectedCourses.length} -

-
- {selectedCourses.map((course, index) => ( -
-

- {course.code}: {course.name} -

- handleRemoveCourse(course)} - /> -
- ))} -
-
+ ( + + + Pick a few courses you'd like to take + + + { + field.onChange(value); + }} + data={data} // TODO: Replace with variable data + isLoading={isLoading} + showFilter={() => setShowFilters(true)} + /> + + + + )} + /> +
+
+

+ Selected courses: {selectedCourses.length} +

+
+ {selectedCourses.map((course, index) => ( +
+

+ {course.code}: {course.name} +

+ handleRemoveCourse(course)} + /> +
+ ))} +
+
-
-
-

Custom Settings

-

- Add additional restrictions to your timetable to - personalize it to your needs. -

-
- -
+
+
+

Custom Settings

+

+ Add additional restrictions to your timetable to + personalize it to your needs. +

+
+ +
-
-

- Enabled Restrictions: {enabledRestrictions.length} -

-
- {enabledRestrictions.map((restric, index) => ( -
- {restric.type.startsWith("Restrict") ? ( -

- {restric.type}:{" "} - {restric.startTime - ? formatTime(restric.startTime) - : ""}{" "} - {restric.type === "Restrict Between" ? " - " : ""}{" "} - {restric.endTime - ? formatTime(restric.endTime) - : ""}{" "} - {restric.days?.join(" ")} -

- ) : ( -

- {restric.type}: At least{" "} - {restric.numDays} days off -

- )} +
+

+ Enabled Restrictions: {enabledRestrictions.length} +

+
+ {enabledRestrictions.map((restric, index) => ( +
+ {restric.type.startsWith("Restrict") ? ( +

+ {restric.type}:{" "} + {restric.startTime + ? formatTime(restric.startTime) + : ""}{" "} + {restric.type === "Restrict Between" ? " - " : ""}{" "} + {restric.endTime + ? formatTime(restric.endTime) + : ""}{" "} + {restric.days?.join(" ")} +

+ ) : ( +

+ {restric.type}: At least{" "} + {restric.numDays} days off +

+ )} - handleRemoveRestriction(index)} - /> -
- ))} -
-
+ handleRemoveRestriction(index)} + /> +
+ ))} +
+
- - - - -
-
- -
- {isCustomSettingsOpen && ( - setIsCustomSettingsOpen(false)} - /> - )} + + + + +
+
+ +
+ {isCustomSettingsOpen && ( + setIsCustomSettingsOpen(false)} + /> + )} - {showFilters && ( - setShowFilters(false)} - resetHandler={() => { - setFilters(null); - setShowFilters(false); - console.log("reset filters"); - }} - filterForm={filterForm} - /> - )} -
-
- - ); + {showFilters && ( + setShowFilters(false)} + resetHandler={() => { + setFilters(null); + setShowFilters(false); + console.log("reset filters"); + }} + filterForm={filterForm} + /> + )} +
+
+ + ); }; export default TimetableBuilder; From 5bda338cf57e2c674afe01f1dd117beb0c996de4 Mon Sep 17 00:00:00 2001 From: FLAME Date: Fri, 7 Mar 2025 20:42:22 -0500 Subject: [PATCH 19/20] Update package-lock.json --- course-matrix/backend/package-lock.json | 220 ------------------------ 1 file changed, 220 deletions(-) diff --git a/course-matrix/backend/package-lock.json b/course-matrix/backend/package-lock.json index 3a25a10f..d79d46e1 100644 --- a/course-matrix/backend/package-lock.json +++ b/course-matrix/backend/package-lock.json @@ -2587,226 +2587,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "node_modules/jsondiffpatch": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz", - "integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==", - "dependencies": { - "@types/diff-match-patch": "^1.0.36", - "chalk": "^5.3.0", - "diff-match-patch": "^1.0.5" - }, - "bin": { - "jsondiffpatch": "bin/jsondiffpatch.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "peer": true, - "dependencies": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jsonwebtoken/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "peer": true - }, - "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "peer": true, - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "peer": true, - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/langchain": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.3.19.tgz", - "integrity": "sha512-aGhoTvTBS5ulatA67RHbJ4bcV5zcYRYdm5IH+hpX99RYSFXG24XF3ghSjhYi6sxW+SUnEQ99fJhA5kroVpKNhw==", - "dependencies": { - "@langchain/openai": ">=0.1.0 <0.5.0", - "@langchain/textsplitters": ">=0.0.0 <0.2.0", - "js-tiktoken": "^1.0.12", - "js-yaml": "^4.1.0", - "jsonpointer": "^5.0.1", - "langsmith": ">=0.2.8 <0.4.0", - "openapi-types": "^12.1.3", - "p-retry": "4", - "uuid": "^10.0.0", - "yaml": "^2.2.1", - "zod": "^3.22.4", - "zod-to-json-schema": "^3.22.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@langchain/anthropic": "*", - "@langchain/aws": "*", - "@langchain/cerebras": "*", - "@langchain/cohere": "*", - "@langchain/core": ">=0.2.21 <0.4.0", - "@langchain/deepseek": "*", - "@langchain/google-genai": "*", - "@langchain/google-vertexai": "*", - "@langchain/google-vertexai-web": "*", - "@langchain/groq": "*", - "@langchain/mistralai": "*", - "@langchain/ollama": "*", - "@langchain/xai": "*", - "axios": "*", - "cheerio": "*", - "handlebars": "^4.7.8", - "peggy": "^3.0.2", - "typeorm": "*" - }, - "peerDependenciesMeta": { - "@langchain/anthropic": { - "optional": true - }, - "@langchain/aws": { - "optional": true - }, - "@langchain/cerebras": { - "optional": true - }, - "@langchain/cohere": { - "optional": true - }, - "@langchain/deepseek": { - "optional": true - }, - "@langchain/google-genai": { - "optional": true - }, - "@langchain/google-vertexai": { - "optional": true - }, - "@langchain/google-vertexai-web": { - "optional": true - }, - "@langchain/groq": { - "optional": true - }, - "@langchain/mistralai": { - "optional": true - }, - "@langchain/ollama": { - "optional": true - }, - "@langchain/xai": { - "optional": true - }, - "axios": { - "optional": true - }, - "cheerio": { - "optional": true - }, - "handlebars": { - "optional": true - }, - "peggy": { - "optional": true - }, - "typeorm": { - "optional": true - } - } - }, - "node_modules/langchain/node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/langsmith": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.10.tgz", - "integrity": "sha512-V6SnJhxKt9AbdVfl86OrEBl8uGwO0/WE2qqDcRXxHxzdCDnj+sV7nstr5VL0a6zxZmyMaw6eBbYwB4PTYKRvvQ==", - "dependencies": { - "@types/uuid": "^10.0.0", - "chalk": "^4.1.2", - "console-table-printer": "^2.12.1", - "p-queue": "^6.6.2", - "p-retry": "4", - "semver": "^7.6.3", - "uuid": "^10.0.0" - }, - "peerDependencies": { - "openai": "*" - }, - "peerDependenciesMeta": { - "openai": { - "optional": true - } - } - }, - "node_modules/langsmith/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/langsmith/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", From 2f23bf35da391ec108203306887523911d218e00 Mon Sep 17 00:00:00 2001 From: FLAME Date: Fri, 7 Mar 2025 21:50:07 -0500 Subject: [PATCH 20/20] Update package-lock.json --- course-matrix/frontend/package-lock.json | 88 ++++++++++++------------ 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/course-matrix/frontend/package-lock.json b/course-matrix/frontend/package-lock.json index dcff55f5..57157bc7 100644 --- a/course-matrix/frontend/package-lock.json +++ b/course-matrix/frontend/package-lock.json @@ -3237,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", @@ -3438,50 +3482,6 @@ "dev": true, "peer": true }, - "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/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",