diff --git a/.gitignore b/.gitignore index 92e0fa0..dacb84f 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,4 @@ next-env.d.ts **/public/workbox-*.js **/public/workbox-*.js.map -src/app/fetch/ \ No newline at end of file +.env \ No newline at end of file diff --git a/next.config.js b/next.config.js index 02aff5a..3357211 100644 --- a/next.config.js +++ b/next.config.js @@ -2,6 +2,7 @@ const withPWA = require("next-pwa")({ dest: "public", register: true, skipWaiting: true, + disable: process.env.NODE_ENV === "development", }); const nextConfig = withPWA({ diff --git a/package.json b/package.json index bd965b6..6ea763a 100644 --- a/package.json +++ b/package.json @@ -17,12 +17,12 @@ "@mantine/dates": "^7.2.2", "@mantine/dropzone": "^7.2.2", "@mantine/ds": "^7.2.2", - "@mantine/form": "^7.2.2", + "@mantine/form": "^7.5.0", "@mantine/hooks": "^7.2.2", "@mantine/modals": "^7.2.2", "@mantine/notifications": "^7.2.2", "@mantine/nprogress": "^7.2.2", - "@mantine/spotlight": "^7.2.2", + "@mantine/spotlight": "^7.5.0", "@mantine/tiptap": "^7.2.2", "@tabler/icons-react": "^2.41.0", "@tiptap/extension-link": "^2.1.12", diff --git a/src/app/page.tsx b/src/app/page.tsx index 9fd2689..17cf51b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,8 @@ "use client"; -import { AppShell, Flex, Text } from "@mantine/core"; -import { useDisclosure } from "@mantine/hooks"; +import { AppShell, Button, Flex, em } from "@mantine/core"; +import { useDisclosure, useMediaQuery, useToggle } from "@mantine/hooks"; +import { IconCalendar, IconList } from "@tabler/icons-react"; +import { useEffect, useState } from "react"; import { Header } from "../components/Header"; import { Navbar } from "../components/Navbar"; import { Timetable } from "../components/Timetable"; @@ -9,26 +11,98 @@ import { Course } from "../type/Types"; export default function Page() { const [opened, { toggle }] = useDisclosure(false); + const isMobile = useMediaQuery(`(max-width: ${em(750)})`); + + const [weekdays, toggleSaturday] = useToggle([ + ["M", "TU", "W", "TH", "F"], + ["M", "TU", "W", "TH", "F", "SA"], + ]); + + const terms = [ + { label: "2024S", ay: "2024", season: "Spring", value: "2024S" }, + { label: "2024A", ay: "2024", season: "Autumn", value: "2024A" }, + { label: "2024W", ay: "2024", season: "Winter", value: "2024W" }, + ]; + const [selectedTermValue, setselectedTermValue] = useState(terms[0].value); + + const selectedTerm = terms.find((term) => term.value === selectedTermValue); + + const [displayMode, toggleDisplayMode] = useToggle(["list", "timetable"]); + useEffect(() => { + if (!isMobile) { + toggleDisplayMode("timetable"); + } + }, [isMobile]); // Get the list of courses from the local storage const [courses, setCourses] = useLocalStorage("courses", [ { - regno: 99999, + regno: 99997, season: "Spring", - ay: 2022, + ay: 2024, no: "CS101", lang: "E", - e: "Example Course", + e: "Example Spring Course", j: "科目例", schedule: ["3/M", "3/W", "3/F"], instructor: "John Doe", modified: new Date(2022, 5 - 1, 5, 6, 35, 20, 333), unit: 3, isEnrolled: true, - color: "#ff0000", + color: "orange 2", + }, + { + regno: 99998, + season: "Autumn", + ay: 2024, + no: "CS101", + lang: "E", + e: "Example Autumn Course", + j: "科目例", + schedule: ["3/M", "3/W", "3/F"], + instructor: "John Doe", + modified: new Date(2022, 5 - 1, 5, 6, 35, 20, 333), + unit: 3, + isEnrolled: true, + color: "pink 2", + }, + { + regno: 99999, + season: "Winter", + ay: 2024, + no: "CS101", + lang: "E", + e: "Example Winter Course", + j: "科目例", + schedule: ["3/M", "3/W", "3/F"], + instructor: "John Doe", + modified: new Date(2022, 5 - 1, 5, 6, 35, 20, 333), + unit: 3, + isEnrolled: true, + color: "green 2", }, ]); + const timetable: { [key: string]: Course[] } = {}; + const coursesInSelectedTerm = courses.filter( + (course) => + course.season === selectedTerm?.season && + course.ay.toString() === selectedTerm?.ay + ); + + const enrolledCourses = coursesInSelectedTerm.filter( + (course) => course.isEnrolled + ); + coursesInSelectedTerm.forEach((course) => { + course.schedule?.forEach((entry) => { + const [time, day] = entry.split("/"); + if (!timetable[`${time}/${day}`]) { + timetable[`${time}/${day}`] = []; + } + timetable[`${time}/${day}`].push(course); + }); + }); + // Toggle the isEnrolled property of a certain course // Usage: toggleIsEnrolled(regno) const toggleIsEnrolled = (regno: number) => { @@ -56,51 +130,82 @@ export default function Page() { }; return ( - <> - - -
- - + + +
{ + toggleSaturday(); + }} + terms={terms} + selectedTermValue={selectedTermValue} + setselectedTermValue={setselectedTermValue} + /> + + + + + + + {displayMode === "timetable" ? ( + + ) : ( - - - {/* */} - {/* */} - - {/* */} - {/* - - */} - {/* */} - + + + {/* */} + + + + {/* */} + ); } diff --git a/src/components/CourseCard/CourseCard.module.css b/src/components/CourseCard/CourseCard.module.css deleted file mode 100644 index 1f3239d..0000000 --- a/src/components/CourseCard/CourseCard.module.css +++ /dev/null @@ -1,19 +0,0 @@ -.button { - display: flex; - width: 100%; - border: rem(1px) solid - light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-8)); - border-radius: var(--mantine-radius-sm); - padding: var(--mantine-spacing-lg); - background-color: light-dark( - var(--mantine-color-white), - var(--mantine-color-dark-8) - ); - - @mixin hover { - background-color: light-dark( - var(--mantine-color-gray-0), - var(--mantine-color-dark-9) - ); - } -} diff --git a/src/components/CourseCard/index.tsx b/src/components/CourseCard/index.tsx index b8a61fe..fa5b788 100644 --- a/src/components/CourseCard/index.tsx +++ b/src/components/CourseCard/index.tsx @@ -3,69 +3,72 @@ import { Course } from "@/src/type/Types"; import { ActionIcon, Card, - Checkbox, - Flex, + Divider, Grid, + Stack, Text, UnstyledButton, } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; -import { IconTrash } from "@tabler/icons-react"; -import classes from "./CourseCard.module.css"; +import { IconEye, IconEyeOff, IconTrash } from "@tabler/icons-react"; export default function CourseCard(props: { course: Course; + open: () => void; toggleIsEnrolled: (regno: number) => void; deleteCourse: (regno: number) => void; }) { - const [modalOpened, { open, close }] = useDisclosure(false); + const [modalConfirmOpened, { open, close }] = useDisclosure(false); return ( - - + + { - props.toggleIsEnrolled(props.course.regno); + props.open(); }} + key={props.course.regno} + w="100%" + p="md" > - - { - props.toggleIsEnrolled(props.course.regno); - }} - variant="default" - mr="xl" - /> -
- - {props.course.no} ・ {props.course.unit} - - - {props.course.e} ({props.course.lang}) - - - {props.course.schedule?.map((s, i) => - i === props.course.schedule!.length - 1 ? s : s + ", " - )} - -
-
+ + + {props.course.no} ・ {props.course.unit} + + + {props.course.e} ({props.course.lang}) + + + {props.course.schedule?.map((s, i) => + i === props.course.schedule!.length - 1 ? s : s + ", " + )} + +
+ - - + + { + props.toggleIsEnrolled(props.course.regno); + }} + color="gray" + > + {props.course.isEnrolled ? : } + + + - +
diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index 8234dbe..5b5a599 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -1,16 +1,24 @@ -import { ActionIcon, Burger, Container, Group, Text } from "@mantine/core"; +import { + ActionIcon, + Container, + Group, + NativeSelect, + Text, +} from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import { IconBrandGithub, IconSettings } from "@tabler/icons-react"; -import React from "react"; - import ModalSetting from "../ModalSetting"; export function Header(props: { - opened: boolean; - toggle: React.MouseEventHandler; + weekdays: string[]; + toggleSaturday: () => void; + terms: { value: string; ay: string; season: string; label: string }[]; + selectedTermValue: string; + setselectedTermValue: (value: string) => void; }) { - const [modalOpened, { open, close }] = useDisclosure(false); + const [modalSettingOpened, { open, close }] = useDisclosure(false); + return (
- ICU Catalogue + + ) => + props.setselectedTermValue(event.currentTarget.value) + } + data={props.terms} + /> + - + { + props.toggleSaturday(); + }} + />
diff --git a/src/components/ModalConfirm/index.tsx b/src/components/ModalConfirm/index.tsx index 9c6a093..dc418ac 100644 --- a/src/components/ModalConfirm/index.tsx +++ b/src/components/ModalConfirm/index.tsx @@ -4,12 +4,12 @@ import { Button, Group, Modal } from "@mantine/core"; export default function ModalConfirm(props: { course: Course; deleteCourse: (regno: number) => void; - modalOpened: boolean; + modalConfirmOpened: boolean; close: () => void; }) { return ( void; +}) { + const seasonToNumber = (season: string) => { + switch (season) { + case "Spring": + return 1; + case "Autumn": + return 2; + case "Winter": + return 3; + default: + return 0; + } + }; + + const CourseInfo: React.FC<{ course: Course }> = (props: { + course: Course; + }) => { + return ( + + + {props.course?.no} ・ {props.course?.unit} + + + {props.course?.schedule?.map((s, i) => + i === props.course?.schedule!.length - 1 ? s : s + ", " + )} + + + + + + ); + }; + + return ( + + + + + + {props.courses?.length === 1 ? ( + + {props.courses[0].e} ({props.courses[0].lang}) + + ) : ( + + {props.courses.length} Courses Conflicted + + )} + + + + {props.courses?.length === 1 ? ( + + ) : ( + + {props.courses?.map((course) => ( + + + + {course.e} ({course.lang}) + + + + + + + ))} + + )} + + + + + + + ); +} diff --git a/src/components/ModalSetting/index.tsx b/src/components/ModalSetting/index.tsx index faacc92..68786a4 100644 --- a/src/components/ModalSetting/index.tsx +++ b/src/components/ModalSetting/index.tsx @@ -1,9 +1,8 @@ import { Button, + Checkbox, Group, - Input, Modal, - NativeSelect, Text, useComputedColorScheme, useMantineColorScheme, @@ -13,8 +12,10 @@ import { IconMoon, IconSun } from "@tabler/icons-react"; import classes from "./ModalSetting.module.css"; export default function ModalSetting(props: { - modalOpened: boolean; + modalSettingOpened: boolean; close: () => void; + weekdays: string[]; + toggleSaturday: () => void; }) { const { setColorScheme } = useMantineColorScheme(); @@ -23,7 +24,7 @@ export default function ModalSetting(props: { }); return ( - Term - Saturday + { + props.toggleSaturday(); + }} /> - ELA / JLP AS - + */} void; deleteCourse: (regno: number) => void; }) { + const [modalDetailOpened, { open, close }] = useDisclosure(false); + const [modalDetailFocusedCourse, setModalDetailFocusedCourse] = useState< + Course[] + >([]); + // Show the courses in the selected tab, and if there are no courses, show "No Results" const results = props.courses // Sort the courses by their no property - .sort(function (a, b) { + ?.sort(function (a, b) { if (a.no > b.no) { return 1; } else { return -1; } }) - .map((course) => ( - + ?.map((course) => ( +
+ { + setModalDetailFocusedCourse([course]); + open(); + }} + /> +
)); return ( - + ); } diff --git a/src/components/SpotlightSearch/index.tsx b/src/components/SpotlightSearch/index.tsx new file mode 100644 index 0000000..5e240eb --- /dev/null +++ b/src/components/SpotlightSearch/index.tsx @@ -0,0 +1,104 @@ +"use client"; +import { DevServerCourse } from "@/src/type/Types"; +import { + ActionIcon, + Card, + Divider, + Grid, + Stack, + Text, + UnstyledButton, +} from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { Spotlight } from "@mantine/spotlight"; +import { + IconExternalLink, + IconPlaylistAdd, + IconSearch, +} from "@tabler/icons-react"; +import { useEffect, useState } from "react"; + +export default function page() { + const form = useForm({ + initialValues: { + query: "", + }, + }); + const [term, setTerm] = useState("*"); + + const [courses, setCourses] = useState([]); + + const [query, setQuery] = useState(""); + const searchCourse = (query: string, term: string) => { + // Should be rewritten to use the API + const url = `https://devserver.icu/api/v3/search?q=${query}&term=${term}`; + return fetch(url) + .then((response) => response.json()) + .then((data) => { + console.log(data); + setCourses(data); + return data; + }); + }; + + useEffect(() => { + const timeoutId = setTimeout(() => { + searchCourse(query, term); + }, 500); + + return () => { + clearTimeout(timeoutId); + }; + }, [query, term]); + + const results = courses.map((course) => ( + + + + + + {course.title_e} + + {course.summary_e} + + + + + + + + + + {/* TODO - Change Academic Year and Term */} + + + + + + + + + )); + + return ( + + } + /> + + {results.length > 0 ? ( + results + ) : ( + Nothing found... + )} + + + ); +} diff --git a/src/components/Timetable/Timetable.module.css b/src/components/Timetable/Timetable.module.css deleted file mode 100644 index 3d3310a..0000000 --- a/src/components/Timetable/Timetable.module.css +++ /dev/null @@ -1,25 +0,0 @@ -.card { - background-color: light-dark( - var(--mantine-color-gray-0), - var(--mantine-color-dark-6) - ); -} - -.item { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - border-radius: var(--mantine-radius-md); - height: rem(90px); - background-color: light-dark( - var(--mantine-color-white), - var(--mantine-color-dark-7) - ); - - @mixin hover { - box-shadow: var(--mantine-shadows-md); - transform: scale(1.05); - } -} diff --git a/src/components/Timetable/TimetableCard.tsx b/src/components/Timetable/TimetableCard.tsx deleted file mode 100644 index badc075..0000000 --- a/src/components/Timetable/TimetableCard.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Card } from "@mantine/core"; -import React from "react"; -import classes from "./Timetable.module.css"; - -interface TimetableCardProps { - children: React.ReactNode; - style?: React.CSSProperties; -} - -export default function TimetableCard({ children }: TimetableCardProps) { - return ( - - {children} - - ); -} diff --git a/src/components/Timetable/index.tsx b/src/components/Timetable/index.tsx index 543215d..00dd4fa 100644 --- a/src/components/Timetable/index.tsx +++ b/src/components/Timetable/index.tsx @@ -1,17 +1,20 @@ "use client"; import { Course } from "@/src/type/Types"; -import { Card, SimpleGrid, Stack, Text } from "@mantine/core"; -import classes from "./Timetable.module.css"; +import { Card, Flex, Grid, Stack, Text, UnstyledButton } from "@mantine/core"; +import { useDisclosure } from "@mantine/hooks"; +import { useState } from "react"; +import ModalDetail from "../ModalDetail"; -export function Timetable(props: { courses: Course[] }) { - const weekDays: string[] = ["M", "TU", "W", "TH", "F", "SA"]; - const weekDayItems = weekDays.map((day) => ( - - - {day} - - - )); +export function Timetable(props: { + timetable: { [key: string]: Course[] }; + enrolledCourses: Course[]; + weekdays: string[]; + toggleIsEnrolled: (regno: number) => void; +}) { + const [modalDetailOpened, { open, close }] = useDisclosure(false); + const [modalDetailFocusedCourse, setModalDetailFocusedCourse] = useState< + Course[] + >([]); type ScheduleItem = [string, number, string]; const scheduleItems: ScheduleItem[] = [ @@ -24,78 +27,104 @@ export function Timetable(props: { courses: Course[] }) { ["17:50", 7, "19:00"], ]; - const schedule: JSX.Element[] = scheduleItems.map((item) => ( - <> - - - {item[0]} - - - {item[1]} - - - {item[2]} - - - - )); - - const timetable: { [key: string]: Course[] } = {}; - - const enrolledCourses = props.courses.filter((course) => course.isEnrolled); - enrolledCourses.forEach((course) => { - course.schedule?.forEach((entry) => { - const [time, day] = entry.split("/"); - if (!timetable[`${time}/${day}`]) { - timetable[`${time}/${day}`] = []; - } - timetable[`${time}/${day}`].push(course); - }); - }); - return ( - - - - - {enrolledCourses.reduce((sum, course) => sum + course.unit, 0)}{" "} - Units - - - - {/* Card for weekdays */} - {weekDayItems} - - {/* Show time (1st Period ~ 7th Period) */} - {schedule} + + + + + + + + {props.enrolledCourses.reduce( + (sum, course) => sum + course.unit, + 0 + )} + + + units + + + + + + {props.weekdays.map((day) => { + return ( + + + + + {day} + + + + + ); + })} + - {/* Show timetable for all weekdays */} - {weekDays.map((day) => { + {Array(7) + .fill(0) + .map((_, i) => { return ( - // Set timetable column for each day (M,TU,W,TH,F) - - {Array(7) - .fill(0) - .map((_, i) => { - return ( - - {timetable[`${i + 1}/${day}`]?.map((course) => { - return ( - - {course.e} - - ); - })} + + + + + + {scheduleItems[i][0]} + + {scheduleItems[i][1]} + + {scheduleItems[i][2]} + + + + + {props.weekdays.map((day) => { + return ( + + + { + setModalDetailFocusedCourse( + props.timetable[`${scheduleItems[i][1]}/${day}`] + ); + open(); + }} + h="100%" + disabled={ + !props.timetable[`${scheduleItems[i][1]}/${day}`] + } + > + + {props.timetable[ + `${scheduleItems[i][1]}/${day}` + ]?.map((course) => ( + + {course.e} + + ))} + + - ); - })} - + + ); + })} +
); })} - -
+ { + close(); + }} + /> + ); } diff --git a/src/type/Types.ts b/src/type/Types.ts index f8295a0..9046a77 100644 --- a/src/type/Types.ts +++ b/src/type/Types.ts @@ -13,3 +13,14 @@ export interface Course { isEnrolled: boolean; color: string; } + +export interface DevServerCourse { + cno: string; + term: string; + title_j: string; + title_e: string; + regno: number; + lang: string; + summary_e: string; + summary_j: string; +} diff --git a/yarn.lock b/yarn.lock index ac7c898..a874c74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2135,15 +2135,15 @@ __metadata: languageName: node linkType: hard -"@mantine/form@npm:^7.2.2": - version: 7.2.2 - resolution: "@mantine/form@npm:7.2.2" +"@mantine/form@npm:^7.5.0": + version: 7.5.0 + resolution: "@mantine/form@npm:7.5.0" dependencies: fast-deep-equal: "npm:^3.1.3" - klona: "npm:^2.0.5" + klona: "npm:^2.0.6" peerDependencies: react: ^18.2.0 - checksum: 2fca2378c995c240894a7cbc01f7d23cc494575f9d77142656ea4d838a4e159557fe3a3ed0f862e7f4cd277c2addfff19679a3313b3b7ff347b380b0c33704c6 + checksum: 946c300d5cecfe35ad15e0e48f6b8b1c3c442571624e21e40c86f550cc2814229312692e304d57b7be43a4c745270efb22a72ad6df9947eb59a94651677537b5 languageName: node linkType: hard @@ -2197,17 +2197,17 @@ __metadata: languageName: node linkType: hard -"@mantine/spotlight@npm:^7.2.2": - version: 7.2.2 - resolution: "@mantine/spotlight@npm:7.2.2" +"@mantine/spotlight@npm:^7.5.0": + version: 7.5.0 + resolution: "@mantine/spotlight@npm:7.5.0" dependencies: - "@mantine/store": "npm:7.2.2" + "@mantine/store": "npm:7.5.0" peerDependencies: - "@mantine/core": 7.2.2 - "@mantine/hooks": 7.2.2 + "@mantine/core": 7.5.0 + "@mantine/hooks": 7.5.0 react: ^18.2.0 react-dom: ^18.2.0 - checksum: 830c444c1f9d8f74c2eeffa86bbafdf92c7090ea100f9595fd3fdff765f094ad9cf466030bf17371cc70196d840d57736709da22629e1d705172a8915f704439 + checksum: afd8765eca5a55afe99c23bea86d98e4b8c86f641a8e8e7566ce21df0329a1574df304542394a0bd1f0948936074b0e551bc8f33711c5ff912e016234b40b4c4 languageName: node linkType: hard @@ -2220,6 +2220,15 @@ __metadata: languageName: node linkType: hard +"@mantine/store@npm:7.5.0": + version: 7.5.0 + resolution: "@mantine/store@npm:7.5.0" + peerDependencies: + react: ^18.2.0 + checksum: 30812a8d3f0f80ffcc49d3196387b2dce25369c034eff364a80421921f8396bf3a4c7042a8be019d2bd394d82140bf51b9670a6a13e23f8a23f1535fa8b2237f + languageName: node + linkType: hard + "@mantine/tiptap@npm:^7.2.2": version: 7.2.2 resolution: "@mantine/tiptap@npm:7.2.2" @@ -9730,12 +9739,12 @@ __metadata: "@mantine/dates": "npm:^7.2.2" "@mantine/dropzone": "npm:^7.2.2" "@mantine/ds": "npm:^7.2.2" - "@mantine/form": "npm:^7.2.2" + "@mantine/form": "npm:^7.5.0" "@mantine/hooks": "npm:^7.2.2" "@mantine/modals": "npm:^7.2.2" "@mantine/notifications": "npm:^7.2.2" "@mantine/nprogress": "npm:^7.2.2" - "@mantine/spotlight": "npm:^7.2.2" + "@mantine/spotlight": "npm:^7.5.0" "@mantine/tiptap": "npm:^7.2.2" "@storybook/addon-essentials": "npm:^7.5.3" "@storybook/addon-interactions": "npm:^7.5.3" @@ -10837,7 +10846,7 @@ __metadata: languageName: node linkType: hard -"klona@npm:^2.0.4, klona@npm:^2.0.5": +"klona@npm:^2.0.4, klona@npm:^2.0.6": version: 2.0.6 resolution: "klona@npm:2.0.6" checksum: 94eed2c6c2ce99f409df9186a96340558897b3e62a85afdc1ee39103954d2ebe1c1c4e9fe2b0952771771fa96d70055ede8b27962a7021406374fdb695fd4d01