diff --git a/.gitignore b/.gitignore index 45c1abc..232b908 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +/.vscode/ # dependencies /node_modules diff --git a/api/apiLinks.ts b/api/apiLinks.ts new file mode 100644 index 0000000..8207fba --- /dev/null +++ b/api/apiLinks.ts @@ -0,0 +1,2 @@ +const baseURL = 'http://localhost:5000/' + diff --git a/api/blank.tsx b/api/blank.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/api/mocks/subject.ts b/api/mocks/subject.ts new file mode 100644 index 0000000..04f710c --- /dev/null +++ b/api/mocks/subject.ts @@ -0,0 +1,148 @@ +import { SubjectClass } from "@/types/subject"; + +export const mockSubjectClasses: SubjectClass[] = [ + { + id: 'INT3209 1', + lessonStart: 1, + lessonEnd: 3, + group: 'CL', + name: 'Khai phá dữ liệu', + place: '309-GĐ2', + credits: 3, + teacherName: 'Hà Quang Thuỵ', + weekDay: 2, + numberOfStudents: 100, + highlightColor: '#EFEF98', + }, + { + id: 'INT3401 4', + lessonStart: 7, + lessonEnd: 9, + group: 'CL', + name: 'Trí tuệ nhân tạo', + place: '206-GĐ3', + credits: 3, + teacherName: 'Nguyễn Thanh Thuỷ', + weekDay: 2, + numberOfStudents: 100, + highlightColor: '#F5A56D', + description: 'Thầy dạy hay vcl' + }, + { + id: 'INT3306 3', + lessonStart: 10, + lessonEnd: 11, + group: 'CL', + name: 'Web', + place: '206-GĐ3', + credits: 3, + teacherName: 'Hoàng Xuân Tùng', + weekDay: 2, + numberOfStudents: 50, + highlightColor: '#CC66FF', + }, + { + id: 'PES1075 4', + lessonStart: 3, + lessonEnd: 4, + group: 'CL', + name: 'Bóng chuyền hơi', + place: 'SVĐ ĐHNN', + credits: 2, + teacherName: 'Giảng viên GDTC', + weekDay: 3, + numberOfStudents: 50, + highlightColor: '#F7CECE', + }, + { + id: 'INT3403 1', + lessonStart: 11, + lessonEnd: 12, + group: '1', + name: 'Đồ hoạ máy tính', + place: 'PM501-E5', + credits: 3, + teacherName: 'Ma Thị Châu', + weekDay: 3, + numberOfStudents: 50, + highlightColor: '#8BEFB8', + }, + { + id: 'PHI1002 2', + lessonStart: 3, + lessonEnd: 4, + group: 'CL', + name: 'Chủ nghĩa xã hội khoa học', + place: '308-GĐ2', + credits: 2, + teacherName: 'Nguyễn Thị Lan', + weekDay: 4, + numberOfStudents: 50, + highlightColor: '#9BC2E6', + }, + { + id: 'INT3403 1', + lessonStart: 7, + lessonEnd: 8, + group: 'CL', + name: 'Đồ hoạ máy tính', + place: '301-GĐ2', + credits: 3, + teacherName: 'Ma Thị Châu', + weekDay: 4, + numberOfStudents: 50, + highlightColor: '#8BEFB8', + }, + { + id: 'INT3306 3', + lessonStart: 9, + lessonEnd: 10, + group: '1', + name: 'Web', + place: 'PM208-G2', + credits: 3, + teacherName: '??', + weekDay: 4, + numberOfStudents: 50, + highlightColor: '#CC66FF', + }, + { + id: 'MAT1101 4', + lessonStart: 7, + lessonEnd: 9, + group: 'CL', + name: 'Xác suất thống kê', + place: '105-GĐ3', + credits: 3, + teacherName: 'Lê Sỹ Vinh', + weekDay: 6, + numberOfStudents: 50, + highlightColor: '#BAAB7E', + }, + { + id: 'INT3111 1', + lessonStart: 1, + lessonEnd: 3, + group: 'CL', + name: 'Quản lý dự án phần mềm', + place: '106-GĐ3', + credits: 3, + teacherName: 'Trần Hoàng Việt', + weekDay: 7, + numberOfStudents: 50, + highlightColor: '#F2EBE6', + }, + { + id: 'INT3514 2', + lessonStart: 5, + lessonEnd: 6, + group: 'CL', + name: 'Pháp luật và đạo đức trong CNTT', + place: '105-GĐ3', + credits: 2, + teacherName: 'Trần Văn Luân', + weekDay: 7, + numberOfStudents: 50, + highlightColor: '#B7C8D4', + } +] diff --git a/api/schedule.ts b/api/schedule.ts new file mode 100644 index 0000000..96d3455 --- /dev/null +++ b/api/schedule.ts @@ -0,0 +1,22 @@ +import Schedule from "@/components/common/Schedule/Schedule"; +import { ScheduleInfo } from "@/types/schedule"; +import { mockSubjectClasses } from "./mocks/subject"; +import { THEME } from "@/styles/theme"; + +export function getScheduleInfo(shouldDispatch: boolean = true): ScheduleInfo { + let scheduleInfo: ScheduleInfo = { + subjectClassData: mockSubjectClasses, + scheduleStyle: { + hasBorder: false, + roundedBorder: true, + lessonColumnColor: "#FEF2CB", + timeColumnColor: "#FFD965", + headerRowColor: THEME.PRIMARY_COLOR, + dividerRowColor: "#262626", + hasDivider: true, + displayColumnSettings: false, + } + } + + return scheduleInfo; +} diff --git a/api/subject.ts b/api/subject.ts new file mode 100644 index 0000000..ff5f0bd --- /dev/null +++ b/api/subject.ts @@ -0,0 +1,7 @@ +import { SubjectClass } from "@/types/subject"; +import { mockSubjectClasses } from "./mocks/subject" + +export function getSubjectClasses(): SubjectClass[] { + return mockSubjectClasses; +} + diff --git a/app/(dashboard)/schedule/(subject)/[subjectIdAndGroup]/page.tsx b/app/(dashboard)/schedule/(subject)/[subjectIdAndGroup]/page.tsx new file mode 100644 index 0000000..cb2be10 --- /dev/null +++ b/app/(dashboard)/schedule/(subject)/[subjectIdAndGroup]/page.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +export default function page() { + return ( +
page
+ ) +} diff --git a/app/(dashboard)/schedule/exam/page.tsx b/app/(dashboard)/schedule/exam/page.tsx new file mode 100644 index 0000000..cb2be10 --- /dev/null +++ b/app/(dashboard)/schedule/exam/page.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +export default function page() { + return ( +
page
+ ) +} diff --git a/app/(dashboard)/schedule/page.tsx b/app/(dashboard)/schedule/page.tsx new file mode 100644 index 0000000..cb2be10 --- /dev/null +++ b/app/(dashboard)/schedule/page.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +export default function page() { + return ( +
page
+ ) +} diff --git a/app/globals.css b/app/globals.css index fd81e88..4201efa 100644 --- a/app/globals.css +++ b/app/globals.css @@ -16,7 +16,12 @@ } } -body { +main { + margin: 50px + /* height: auto; */ +} + +/* body { color: rgb(var(--foreground-rgb)); background: linear-gradient( to bottom, @@ -24,4 +29,4 @@ body { rgb(var(--background-end-rgb)) ) rgb(var(--background-start-rgb)); -} +} */ diff --git a/app/layout.tsx b/app/layout.tsx index 2516b1d..9933dcb 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,12 +1,11 @@ -'use client'; import Header from '@/components/layouts/Header' import './globals.css' import type { Metadata } from 'next' import Footer from '@/components/layouts/Footer' import { ReduxProvider } from '@/redux/provider'; import NavBar from '@/components/layouts/NavBar'; -import { THEME } from '@/styles/theme'; import { MAIN_FONT } from '@/styles/fonts'; +import { ConfigProvider } from 'antd'; export const metadata: Metadata = { @@ -23,14 +22,22 @@ export default function RootLayout({ -
- -
-
- {children} -
+ +
+ +
+
+ {children} +
+
-
+ diff --git a/app/page.tsx b/app/page.tsx index 7d79ebb..a43678c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,121 +1,20 @@ -import Image from 'next/image' -import { Inter } from 'next/font/google' -const inter = Inter({ subsets: ['latin'] }) +'use client'; +import NotSignedInHomePage from '@/components/home/NotSignedInHomePage/NotSignedInHomePage'; +import SignedInHomePage from '@/components/home/SignedInHomePage'; +import { authSelector } from '@/redux/auth/authSelector'; +import React from 'react' +import { useSelector } from 'react-redux'; -export default function Home() { - return ( -
-
-

- Get started by editing  - app/page.tsx -

- -
- -
- Next.js Logo -
- -
- -

- Docs{' '} - - -> - -

-

- Find in-depth information about Next.js features and API. -

-
+const HomePage: React.FC = () => { + const authState = useSelector(authSelector); + if (authState.signedIn) + return ( + + ) - -

- Learn{' '} - - -> - -

-

- Learn about Next.js in an interactive course with quizzes! -

-
- - -

- Templates{' '} - - -> - -

-

- Explore the Next.js 13 playground. -

-
- - -

- Deploy{' '} - - -> - -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-
-
-
- ) -} + return ( + + ); +}; -// export default function Home() { -// return ( -//
-// ) -// } +export default HomePage; diff --git a/components/common/Box.tsx b/components/common/Box.tsx new file mode 100644 index 0000000..511f8c8 --- /dev/null +++ b/components/common/Box.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { twMerge } from 'tailwind-merge' + +export default function Box({ + children, + className +}: { + children?: React.ReactNode + className?: string +}) { + return ( +
+ {children} +
+ ) +} diff --git a/components/common/Checkbox.tsx b/components/common/Checkbox.tsx new file mode 100644 index 0000000..b4c9f9b --- /dev/null +++ b/components/common/Checkbox.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { Space } from "antd"; +import Checkbox, { CheckboxChangeEvent } from "antd/es/checkbox"; +import { useState } from "react"; + +interface MyCheckboxProps { + onClick: () => void + children: React.ReactNode, + checked: boolean +} + +export default function MyCheckbox({ + onClick, + children, + checked = false +}: MyCheckboxProps) { + + return ( +
+ + + +
+ ); +} diff --git a/components/common/Icons/NavIcons.tsx b/components/common/Icons/NavIcons.tsx new file mode 100644 index 0000000..9fff72d --- /dev/null +++ b/components/common/Icons/NavIcons.tsx @@ -0,0 +1,25 @@ +import { THEME } from "@/styles/theme"; +import { AiFillSchedule, AiOutlineMenu } from "react-icons/ai"; +import { BsPersonRolodex } from "react-icons/bs"; +import { ImStatsDots } from "react-icons/im"; +import { IoHome } from "react-icons/io5"; +import { MdTopic } from "react-icons/md"; + +const navIconSize = 22; +export interface NavsIconsProp { + 'expand-nav': React.JSX.Element; + 'home': React.JSX.Element; + 'schedule': React.JSX.Element; + 'my-subjects': React.JSX.Element; + 'all-subjects': React.JSX.Element; + 'stats': React.JSX.Element; +} +const navIconColor = THEME.PRIMARY_ICON_COLOR; +export const NavsIcons: NavsIconsProp = { + 'expand-nav': , + 'home': , + 'schedule': , + 'my-subjects': , + 'all-subjects': , + 'stats': +}; diff --git a/components/common/Icons/SubjectPropIcons.tsx b/components/common/Icons/SubjectPropIcons.tsx new file mode 100644 index 0000000..19c3200 --- /dev/null +++ b/components/common/Icons/SubjectPropIcons.tsx @@ -0,0 +1,26 @@ +import { THEME } from "@/styles/theme"; +import { SubjectClass } from "@/types/subject"; +import { BiSolidGroup, BiSolidTimeFive } from "react-icons/bi"; +import { HiUserGroup } from "react-icons/hi"; +import { IoMdColorPalette } from "react-icons/io"; +import { MdPlace, MdTopic } from "react-icons/md"; +import { PiChalkboardTeacherFill } from "react-icons/pi"; +import { LuTextQuote } from "react-icons/lu"; + + + +export const subjectPropsIconSize = '1em'; +export const subjectPropsIconColor = THEME.PRIMARY_ICON_COLOR; +type SubjectPropIcon = Record | 'time', React.JSX.Element>; + +export const subjectPropIcons: SubjectPropIcon = { + id: , + group: , + teacherName: , + place: , + // name: undefined, + numberOfStudents: , + time: , + highlightColor: , + description: +}; diff --git a/components/common/Icons/SuccessIcon.tsx b/components/common/Icons/SuccessIcon.tsx new file mode 100644 index 0000000..bbf4d2a --- /dev/null +++ b/components/common/Icons/SuccessIcon.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { IconBaseProps } from 'react-icons'; +import { IoMdDoneAll } from "react-icons/io"; + + +export default function SuccessIcon(props?: IconBaseProps) { + return ( + + ) +} diff --git a/components/common/MyButton/Copy.tsx b/components/common/MyButton/Copy.tsx new file mode 100644 index 0000000..6475d64 --- /dev/null +++ b/components/common/MyButton/Copy.tsx @@ -0,0 +1,21 @@ +import { THEME } from "@/styles/theme" +import { Button } from "antd" +import { BiCopy } from "react-icons/bi" +import { twMerge } from "tailwind-merge" +import MyButtonWrapper from "./MyButtonWrapper" + +export default function CopyButton({ + size = '1em', + color = THEME.ROYAL_GRAY_COLOR, + className +} : { + size?: number | string + color?: string + className?: string +}) { + return ( + + + + ) +} diff --git a/components/common/MyButton/DangerButton.tsx b/components/common/MyButton/DangerButton.tsx new file mode 100644 index 0000000..7fb483b --- /dev/null +++ b/components/common/MyButton/DangerButton.tsx @@ -0,0 +1,19 @@ +import { Button } from 'antd' +import { BaseButtonProps } from 'antd/es/button/button' +import React from 'react' + +export default function DangerButton({ + children, + onClick, + disable = false +} : { + children: React.ReactNode + onClick?: () => void + disable?: boolean +}) { + return ( + + ) +} diff --git a/components/common/MyButton/Download.tsx b/components/common/MyButton/Download.tsx new file mode 100644 index 0000000..2ebdfa5 --- /dev/null +++ b/components/common/MyButton/Download.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { THEME } from "@/styles/theme"; +import { Button } from "antd"; +import { BiDownload } from "react-icons/bi"; +import { FaDownload } from "react-icons/fa6"; + +export default function Download() { + return ( + + ); +} diff --git a/components/common/MyButton/MyButtonWrapper.tsx b/components/common/MyButton/MyButtonWrapper.tsx new file mode 100644 index 0000000..1c6dacf --- /dev/null +++ b/components/common/MyButton/MyButtonWrapper.tsx @@ -0,0 +1,17 @@ +import { Button } from 'antd' +import React from 'react' +import { twMerge } from 'tailwind-merge' + +export default function MyButtonWrapper({ + className, + children +}: { + children?: React.ReactNode + className?: string +}) { + return ( + + ) +} diff --git a/components/common/MyButton/SaveButton.tsx b/components/common/MyButton/SaveButton.tsx new file mode 100644 index 0000000..659b09d --- /dev/null +++ b/components/common/MyButton/SaveButton.tsx @@ -0,0 +1,28 @@ +import { Button } from "antd"; +import React from "react"; +import { twMerge } from "tailwind-merge"; +export function SaveButton({ + editing, + onClick, + children, + disable = false +}: { + editing: boolean + onClick: () => void + disable?: boolean + children: React.ReactNode +}) { + return ( + + ); +} diff --git a/components/common/MyCountDown.tsx b/components/common/MyCountDown.tsx new file mode 100644 index 0000000..d598a84 --- /dev/null +++ b/components/common/MyCountDown.tsx @@ -0,0 +1,138 @@ +import { THEME } from '@/styles/theme'; +import React from 'react' +import { CountdownCircleTimer } from 'react-countdown-circle-timer' + +const toSeconds = { + days: 86400, + hours: 3600, + minutes: 60, + seconds: 1, +} + +type TimeUnit = keyof typeof toSeconds; + +const unitToText: Record = { + days: 'Ngày', + hours: 'Giờ', + minutes: 'Phút', + seconds: 'Giây' +} + +function convertSeconds(unit: TimeUnit, seconds: number): number { + switch (unit) { + case 'days': + return Math.floor(seconds / toSeconds.days); + case 'hours': + return Math.floor((seconds % toSeconds.days) / toSeconds.hours); + case 'minutes': + return Math.floor((seconds % toSeconds.hours) / toSeconds.minutes); + case 'seconds': + return Math.floor(seconds); + } +} + +export default function MyCountDown({ + duration, + onComplete +}: { + duration: number, + onComplete?: () => void +}) { + return ( +
+
+ + + + + + + + +
+
+ duration-totalElapsedTime > toSeconds.days} + /> + duration-totalElapsedTime > toSeconds.hours} + /> + duration-totalElapsedTime > toSeconds.minutes} + /> + { + if (duration-totalElapsedTime > 0) + return true; + onComplete?.(); + return false; + }} + /> +
+
+ ) +} + +function Countdown({ + duration, + remainingTime, + unit, + hasColon = false, + shouldRepeat +}: { + duration: number + remainingTime: number + unit: TimeUnit + hasColon?: boolean + shouldRepeat: (totalElapsedTime: number) => boolean +}) { + return ( +
+
+ { + return { + shouldRepeat: shouldRepeat(totalElapsedTime) + } + }} + > + {({ remainingTime, elapsedTime, color }) => { + return
{convertSeconds(unit, duration - elapsedTime)}
; + }} +
+
+ { + hasColon ? +
:
+ : +
+ } +
+ { + unitToText[unit] + } +
+
+ ) +} + diff --git a/components/common/Schedule/Schedule.tsx b/components/common/Schedule/Schedule.tsx new file mode 100644 index 0000000..05dba1e --- /dev/null +++ b/components/common/Schedule/Schedule.tsx @@ -0,0 +1,407 @@ +import { SaveButton } from '../MyButton/SaveButton'; +import React, { useState } from 'react'; +import { Button, ColorPicker, ConfigProvider, Space, Table } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import ScheduleCell from "./ScheduleCell"; +import type { MenuProps } from 'antd'; +import { Dropdown } from 'antd'; +import { AiTwotoneSetting } from "react-icons/ai"; +import MyCheckbox from "../Checkbox"; +// @ts-ignore +import { LightenDarkenColor } from 'lighten-darken-color'; +import { useDispatch, useSelector } from "react-redux"; +import { scheduleDataSelector, scheduleSelector } from "@/redux/schedule/scheduleSelector"; +import { scheduleActions } from '@/redux/schedule/scheduleSlice'; +import ScheduleSetting from './ScheduleSetting'; +import Download from '../MyButton/Download'; +import { THEME } from '@/styles/theme'; +import DangerButton from '../MyButton/DangerButton'; + +const numberOfLessons = 12; + +interface ScheduleProps { + onlyViewMode?: boolean + scale?: number +} + +interface TableData { + lesson: number +} +type ColumnKey = 'lesson' | 'time' | 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday' +const weekdays: ({ name: string, asNumber: number, key: ColumnKey })[] = [ + { + name: 'Thứ hai', + asNumber: 2, + key: 'monday' + }, + { + name: 'Thứ ba', + asNumber: 3, + key: 'tuesday' + }, + { + name: 'Thứ tư', + asNumber: 4, + key: 'wednesday' + }, + { + name: 'Thứ năm', + asNumber: 5, + key: 'thursday' + }, + { + name: 'Thứ sáu', + asNumber: 6, + key: 'friday' + }, + { + name: 'Thứ bảy', + asNumber: 7, + key: 'saturday' + }, + { + name: 'Chủ nhật', + asNumber: 8, + key: 'sunday' + } +] + +export default function Schedule({ + onlyViewMode = false, + scale = 1 +}: ScheduleProps) { + let columns: ColumnsType = []; + let data: TableData[] = []; + const { scheduleStyle, subjectClassData } = useSelector(scheduleDataSelector); + const { editing } = useSelector(scheduleSelector) + const [hidenColumns, setHidenColumns] = useState<(React.Key | undefined)[]>([]); + const cellStyle: Record<'normal' | 'header' | 'lesson' | 'time' | 'divider' | 'subject', React.CSSProperties> = { + 'normal': {}, + 'divider': { + backgroundColor: scheduleStyle.dividerRowColor + }, + 'header': {}, + 'lesson': {}, + 'time': {}, + 'subject': {} + } + cellStyle['normal'] = { + borderColor: scheduleStyle.hasBorder ? THEME.TABLE_BORDER_COLOR : '', + textAlign: 'left', + padding: '0px', + height: 30 + } + cellStyle['header'] = { + ...cellStyle['normal'], + backgroundColor: scheduleStyle.headerRowColor, + width: 2000, + textAlign: 'center' + } + cellStyle['lesson'] = { + ...cellStyle['normal'], + backgroundColor: scheduleStyle.lessonColumnColor, + textAlign: 'center' + } + cellStyle['time'] = { + ...cellStyle['normal'], + backgroundColor: scheduleStyle.timeColumnColor, + textAlign: 'center' + } + cellStyle['subject'] = { + ...cellStyle['normal'], + // borderRadius: scheduleStyle.roundedBorder ? 6 : 0, + verticalAlign: 'top' + } + const dispatch = useDispatch(); + const [hoverSubject, setHoverSubject] = useState(-1); + + const handleHideColumn = (colKey: React.Key) => { + setHidenColumns([...hidenColumns, colKey]); + } + + initData(); + + const MainComponent = ( +
+ {!onlyViewMode && +
+
+ dispatch(scheduleActions.updateScheduleStyle({ + displayColumnSettings: !scheduleStyle.displayColumnSettings + }))} + > +
Hiện cài đặt cột
+
+
+ { + dispatch(scheduleActions.discardChanges()) + dispatch(scheduleActions.updateScheduleState({ + editing: false + })) + }} + disable={!editing} + > + Huỷ thay đổi + + { + dispatch(scheduleActions.saveChanges()) + dispatch(scheduleActions.updateScheduleState({ + editing: false + })) + }} + disable={!editing} + > + Lưu + + + + setHidenColumns([])} /> +
+ } + !hidenColumns.includes(val.key))} + dataSource={data} + pagination={false} + bordered={scheduleStyle.hasBorder} + /> + + ); + + function isDivider(data: TableData) { + if (scheduleStyle.hasDivider) + return data.lesson === -1; + return false; + } + + function initData() { + columns.push({ + title: + dispatch(scheduleActions.updateScheduleStyle({ + lessonColumnColor: color + }))} + > + Tiết + , + key: 'lesson', + onCell: (data) => ({ + style: isDivider(data) ? cellStyle['divider'] : cellStyle['lesson'], + }), + render: (_, data) => ({ + children: isDivider(data) ? '' : data.lesson + }) + }); + + columns.push({ + title: + dispatch(scheduleActions.updateScheduleStyle({ + timeColumnColor: color + }))} + > + Thời gian + , + key: 'time', + onCell: (data: TableData) => ({ + style: isDivider(data) ? cellStyle['divider'] : cellStyle['time'], + }), + render: (_, data) => ({ + children: isDivider(data) ? '' : `${data.lesson as number + 6}h` + }) + }) + + for (let day of weekdays) { + columns.push({ + title: + + {day.name} + , + key: day.key, + render: (_, data: TableData) => { + for (let i = 0; i < subjectClassData.length; ++i) { + let subject = subjectClassData[i]; + if (day.asNumber === subject.weekDay) { + if (subject.lessonStart === data.lesson) { + return { + children: +
+
+ setHoverSubject(i)} + onMouseLeave={() => setHoverSubject(-1)} + onColorChange={(_, color) => { + dispatch(scheduleActions.updateScheduleSubjects({ + index: i, + newProps: { + highlightColor: color + } + })) + }} + className='flex-1' + /> +
+
+ } + } + } + } + }, + onCell: (data) => { + for (let i = 0; i < subjectClassData.length; ++i) { + let subject = subjectClassData[i]; + if (day.asNumber === subject.weekDay) { + if (subject.lessonStart === data.lesson) + return { + rowSpan: subject.lessonEnd - subject.lessonStart + 1, + style: { + ...cellStyle['subject'], + cursor: 'pointer' + } + } + if (subject.lessonStart < data.lesson && data.lesson <= subject.lessonEnd) + return { + rowSpan: 0, + style: cellStyle['normal'] + } + } + } + return { + style: isDivider(data) ? cellStyle['divider'] : cellStyle['normal'] + } + } + }) + } + + for (let i = 0; i < columns.length; ++i) + columns[i].onHeaderCell = (colIndex) => ({ + style: { + ...cellStyle['header'], + ...(i === 0 ? { + width: 50, + minWidth: 50 + } : (i === 1 ? { + width: 100, + minWidth: 100 + } : {})) + } + }) + + for (let i = 1; i <= numberOfLessons; ++i) { + data.push({ + lesson: i + }) + if (scheduleStyle.hasDivider && i === 6) + data.push({ + lesson: -1 + }) + } + } + return MainComponent; +} + +const settingIconColor = 'black'; + +function TableHeader({ + children, + colorPicker, + color, + colKey, + displaySetting, + onHideColumn, + onColorPick, +}: { + children: React.ReactNode + colorPicker?: boolean + color?: string + colKey: ColumnKey + displaySetting: boolean + onHideColumn: (colKey: React.Key) => void + onColorPick?: (pickedColor: string) => void +}) { + const items: MenuProps['items'] = [ + { + key: 'hide_column', + label: 'Ẩn cột', + onClick: () => onHideColumn(colKey) + }, + ]; + const [openDropDown, setOpenDropDown] = useState(false); + const [openColorPicker, setOpenColorPicker] = useState(false); + + if (colorPicker) + items.push({ + key: 'color_picker', + label: + setOpenColorPicker(true)} + onMouseLeave={() => setOpenColorPicker(false)} + > + Chọn màu + onColorPick?.(pickedColor)} + trigger='hover' + open={openColorPicker} + arrow={false} + /> + , + onClick: () => setOpenDropDown(true) + }) + + const [hoverSetting, setHoverSetting] = useState(false); + + return ( +
+
{children}
+ {displaySetting && + + setOpenDropDown(flag)} + > +
setHoverSetting(true)} + onMouseLeave={() => setHoverSetting(false)} + > + +
+
+
+ } +
+ ) +} diff --git a/components/common/Schedule/ScheduleCell.tsx b/components/common/Schedule/ScheduleCell.tsx new file mode 100644 index 0000000..99ca4c7 --- /dev/null +++ b/components/common/Schedule/ScheduleCell.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { SubjectClass } from '@/types/subject' +import { Popover, Typography } from 'antd' +import React, { useState } from 'react' +import SubjectClassInfo, { SubjectClassInfoProps } from '../SubjectClassInfo' +import { twMerge } from 'tailwind-merge'; +import { lightenDarkenColor } from '@/utils/lightenDarkenColor'; + +const {Text, Title} = Typography; + +interface ScheduleCellProps { + subjectClass: SubjectClass + pattern?: (keyof SubjectClass)[][] + onMouseEnter?: () => void + onMouseLeave?: () => void + onColorChange: SubjectClassInfoProps['onColorChange'] + className?: string +} + +export default function ScheduleCellContent({ + subjectClass, + className, + onMouseEnter, + onMouseLeave, + onColorChange +}: ScheduleCellProps) { + const [hover, setHover] = useState(false); + return ( + + } + placement='top' + overlayInnerStyle={{ + padding: 0 + }} + trigger={'hover'} + > +
{onMouseEnter?.(); setHover(true);}} + onMouseLeave={() => {onMouseLeave?.(); setHover(false)}} + className={twMerge('flex flex-col rounded-md p-2', className)} + style={{ + backgroundColor: + hover ? lightenDarkenColor(subjectClass.highlightColor, -20) : subjectClass.highlightColor + }} + > + {subjectClass.name} + + {`${subjectClass.place}, Nhóm ${subjectClass.group}`} + +
+
+ ) +} diff --git a/components/common/Schedule/ScheduleSetting.tsx b/components/common/Schedule/ScheduleSetting.tsx new file mode 100644 index 0000000..2de3777 --- /dev/null +++ b/components/common/Schedule/ScheduleSetting.tsx @@ -0,0 +1,214 @@ +'use client'; + +import { DownOutlined } from "@ant-design/icons"; +import { Button, ColorPicker, ConfigProvider, Dropdown, MenuProps, Space } from "antd"; +import MyCheckbox from "../Checkbox"; +import { MenuDividerType, MenuItemGroupType, MenuItemType } from "antd/es/menu/hooks/useItems"; +import { useDispatch, useSelector } from "react-redux"; +import { scheduleActions } from "@/redux/schedule/scheduleSlice"; +import { scheduleDataSelector } from "@/redux/schedule/scheduleSelector"; +import { useState } from "react"; + +const tableSettingTask: Record<'show-hiden-columns' | 'change-header-color' | 'styling-table' | 'delete-table', React.ReactNode> = { + // 'column-setting': 'Cài đặt cột', + 'show-hiden-columns': 'Hiện cột ẩn', + 'change-header-color': 'Đổi màu Header', + 'styling-table': 'Tuỳ chỉnh', + 'delete-table': 'Xoá bảng' +} + +type TableSettingKey = keyof typeof tableSettingTask; + +let stylingTableTask = { + 'has-divider':
, + 'cell-round':
, + 'has-border':
+} +type StylingTableKey = keyof typeof stylingTableTask; + + +export default function ScheduleSetting({ + onHideColumn +}: { + onHideColumn: () => void +}) { + + const dispatch = useDispatch(); + const { scheduleStyle } = useSelector(scheduleDataSelector); + const [open, setOpen] = useState(false); + + tableSettingTask["change-header-color"] = ( +
+ Đổi màu Header + dispatch(scheduleActions.updateScheduleStyle({ + headerRowColor: color + }))} + trigger="hover" + /> +
+ ) + + stylingTableTask = { + 'has-divider': + + dispatch(scheduleActions.updateScheduleStyle({ + hasDivider: !scheduleStyle.hasDivider + }))} + > + Ngăn cách sáng chiều + + { + scheduleStyle.hasDivider && + { + dispatch(scheduleActions.updateScheduleStyle({ + dividerRowColor: dividerRowColor + })) + }} + trigger="hover" + > + + } + + , + 'cell-round': + dispatch(scheduleActions.updateScheduleStyle({ + roundedBorder: !scheduleStyle.roundedBorder + }))} + > + Bo góc môn học + , + 'has-border': + + dispatch(scheduleActions.updateScheduleStyle({ + hasBorder: !scheduleStyle.hasBorder + }))} + > + Hiển thị viền + + } + + const handleTableSetting: MenuProps['onClick'] = (e) => { + switch (e.key as TableSettingKey | StylingTableKey) { + case 'show-hiden-columns': + onHideColumn(); + setOpen(true); + break; + case 'change-header-color': + setOpen(true); + break; + case 'has-border': + setOpen(true); + break; + case 'cell-round': + setOpen(true); + break; + case 'has-divider': + setOpen(true); + break; + } + }; + + const tableSettings: MenuProps = { + items: Object.keys(tableSettingTask).map((key) => getTableSettingItem(key as TableSettingKey)), + onClick: handleTableSetting, + }; + tableSettings.items?.splice(tableSettings.items.length - 1, 0, { + type: 'divider' + } as MenuDividerType) + + return ( + + setOpen(flag)} + > + + + + ); +} + +function getTableSettingItem(tableSettingKey: TableSettingKey) { + switch (tableSettingKey) { + case 'show-hiden-columns': + return showHidenColumnsItem(); + case 'change-header-color': + return changeHeaderColorItem(); + case 'delete-table': + return deleteTableItem(); + case 'styling-table': + return stylingTableItem(); + } +} + +function showHidenColumnsItem(): MenuItemType { + let key: TableSettingKey = 'show-hiden-columns' + return { + label: tableSettingTask[key], + key + } +} + +function changeHeaderColorItem(): MenuItemType { + let key: TableSettingKey = 'change-header-color'; + return { + label: tableSettingTask[key], + key + } +} +function deleteTableItem(): MenuItemType { + let key: TableSettingKey = 'delete-table'; + return { + label: tableSettingTask[key], + key, + danger: true + } +} + +function stylingTableItem(): MenuItemGroupType { + + let key: TableSettingKey = 'styling-table'; + let children: MenuItemType[] = []; + for (const stylingTableKey in stylingTableTask) { + children.push({ + label: stylingTableTask[stylingTableKey as StylingTableKey], + key: stylingTableKey, + // onClick: (e) => { e.domEvent.preventDefault(); e.domEvent.stopPropagation() }, + // className: "!hover:bg-transparent", + style: { + backgroundColor: 'transparent' + } + }) + } + + return { + label: tableSettingTask[key], + key, + children, + type: 'group' + } +} diff --git a/components/common/SubjectClassInfo.tsx b/components/common/SubjectClassInfo.tsx new file mode 100644 index 0000000..1d199b5 --- /dev/null +++ b/components/common/SubjectClassInfo.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { SubjectClass } from "@/types/subject"; +import { ColorPicker, ColorPickerProps, Divider, Space, Typography } from 'antd'; +import CopyButton from "./MyButton/Copy"; +import { BiSolidTimeFive } from "react-icons/bi"; +import { subjectPropIcons, subjectPropsIconColor, subjectPropsIconSize } from "./Icons/SubjectPropIcons"; +import { twMerge } from "tailwind-merge"; +import { THEME } from "@/styles/theme"; +import SuccessIcon from "./Icons/SuccessIcon"; +import MyButtonWrapper from "./MyButton/MyButtonWrapper"; + +const { Title, Text, Paragraph } = Typography; + +export interface SubjectClassInfoProps { + subjectClass: SubjectClass + editable?: boolean + onColorChange?: ColorPickerProps['onChange'] +} + +export default function SubjectClassInfo({ + subjectClass, + onColorChange, + editable = false +}: SubjectClassInfoProps) { + + return ( + + , <MyButtonWrapper key={1}><SuccessIcon/></MyButtonWrapper>], + text: subjectClass.name, + tooltips: ['Sao chép', subjectClass.name] + }} + className={`!text-xl !flex w-full p-4 rounded-t-md bg-blue-500`} + style={{ backgroundColor: subjectClass.highlightColor }} + > + <span className="flex-1 mr-5">{subjectClass.name}</span> + + {/*
+ +
*/} + + , ], + text: subjectClass.id, + tooltips: ['Sao chép', subjectClass.id] + }} + className="!flex flex-1 items-center" + style={{ + fontSize: 'inherit' + }} + > + {subjectClass.id} + + } + /> + + + , ], + text: subjectClass.place, + tooltips: ['Sao chép', subjectClass.place] + }} + className="!flex flex-1 items-center" + style={{ + fontSize: 'inherit' + }} + > + {subjectClass.place} + + } + /> + + + + } + /> + + + + {subjectPropIcons.description} + {subjectClass.description || 'Không có mô tả'} + +
+ ); +} + +function InfoBlock({ + icon, + title, + content, + className, +}: { + icon?: React.ReactNode + title?: React.ReactNode + content?: React.ReactNode + className?: string +}) { + return ( +
+ {icon} + {title}: + {content} +
+ ) +} diff --git a/components/home/NotSignedInHomePage/NotSignedInHomePage.tsx b/components/home/NotSignedInHomePage/NotSignedInHomePage.tsx new file mode 100644 index 0000000..9ca6eb1 --- /dev/null +++ b/components/home/NotSignedInHomePage/NotSignedInHomePage.tsx @@ -0,0 +1,77 @@ +'use client'; + +import Image from "next/image"; +import HomeImage from '../../../public/images/hero-illo@3x.png'; +import { Typography } from "antd"; +import { NavsIcons } from "@/components/common/Icons/NavIcons"; +import Schedule from "@/components/common/Schedule/Schedule"; +const { Text, Title } = Typography; + +const headerText = ""; + +export default function NotSignedInHomePage() { + return ( +
+ + + + + +
+ ); +} + +function AboutScheduleAndMySubjects() { + return ( +
+
+
+ + <div className="whitespace-nowrap">Tạo thời khoá biểu</div> + <div className="whitespace-nowrap">& Quản lý môn học</div> + + Bắt đầu quá trình học tập của bạn +
+
+
{NavsIcons['schedule']}
+
+ Thời khoá biểu + Tạo và tuỳ chỉnh thời khoá biểu nhanh chóng +
+
+ +
+
{NavsIcons['my-subjects']}
+
+ Môn học + Quản lý các học phần theo ngành học của bạn +
+
+
+
+ + + {/* */} +
+
+ ); +} + +function AboutAllSubjects({ }) { + return (
+ Khám phá các học phần + + + + Nơi bạn có thể đánh giá và tìm tài liệu của các học phần + +
); +} +function AboutStats({ }) { + return (
+ Dữ liệu thống kê + + Xem dữ liệu được thống kê theo Ngành, Khoá và bản thân + +
); +} diff --git a/components/home/SignedInHomePage.tsx b/components/home/SignedInHomePage.tsx new file mode 100644 index 0000000..f91e689 --- /dev/null +++ b/components/home/SignedInHomePage.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { getSubjectClasses } from "@/api/subject"; +import Schedule from "../common/Schedule/Schedule"; +import { CountdownCircleTimer } from "react-countdown-circle-timer"; +import { THEME } from "@/styles/theme"; +import MyCountDown from "../common/MyCountDown"; +import { Button, Popover, Space } from "antd"; +import { HiInformationCircle, HiSpeakerphone } from "react-icons/hi"; +import { useMemo, useState } from "react"; +import { lessonToHour, nowToNextSubjectClass } from "@/utils/subjectClass"; +import { useSelector } from "react-redux"; +import { scheduleDataSelector } from "@/redux/schedule/scheduleSelector"; + +export default function SignedInHomePage() { + return ( +
+ + +
+ ); +} + +function NextSubjectInfo() { + const { subjectClassData } = useSelector(scheduleDataSelector); + const [{ + time: timeToNextSubject, + subjectClass: nextSubjectClass + }, setNextSubjectInfo] = useState(() => nowToNextSubjectClass(subjectClassData)); + + console.log(timeToNextSubject, nextSubjectClass.name) + + const [countdownKey, setCountdownKey] = useState(0); + + return ( + { + setNextSubjectInfo(nowToNextSubjectClass(subjectClassData)); + setCountdownKey(countdownKey + 1); + }} + /> + } + placement="right" + > + + +

+ Bạn sẽ có tiết học + {` ${nextSubjectClass?.name} `} + vào lúc + + {` ${lessonToHour(nextSubjectClass.lessonStart)}h-${lessonToHour(nextSubjectClass.lessonEnd) + 1}h + Thứ ${nextSubjectClass.weekDay}`} + +

+
+
+ ); +} diff --git a/components/layouts/Header.tsx b/components/layouts/Header.tsx index 0132384..f098359 100644 --- a/components/layouts/Header.tsx +++ b/components/layouts/Header.tsx @@ -108,7 +108,7 @@ export default function Header() { >
- +
diff --git a/components/layouts/NavBar.tsx b/components/layouts/NavBar.tsx index 9df425d..63cd29b 100644 --- a/components/layouts/NavBar.tsx +++ b/components/layouts/NavBar.tsx @@ -3,62 +3,86 @@ import { THEME } from '@/styles/theme' import Link from 'next/link' import React, { useState } from 'react' -import { AiOutlineMenu, AiOutlineSchedule, AiFillSchedule } from 'react-icons/ai' -import { MdOutlineTopic, MdTopic } from 'react-icons/md' -import { BsFiletypeDoc, BsPerson, BsPersonRolodex } from 'react-icons/bs' -import { TfiStatsUp } from 'react-icons/tfi' -import { ImStatsDots } from 'react-icons/im' -import MySubjects from '../../public/images/my-subjects.svg' -import Image from 'next/image' +import { Menu } from 'antd'; +import type { MenuProps } from 'antd/es/menu'; +import { NavsIcons, NavsIconsProp } from "../common/Icons/NavIcons"; +import { useAutoAnimate } from '@formkit/auto-animate/react'; + +type MenuItem = Required['items'][number]; + +function getItem( + label: React.ReactNode, + key?: React.Key | null, + icon?: React.ReactNode, + children?: MenuItem[], +): MenuItem { + return { + key, + icon, + children, + label, + } as MenuItem; +} const NavsContent: string[] = [ + "Trang chủ", "Thời khoá biểu", "Môn học của tôi", "Học phần", "Thống kê", ]; -const iconSize = 22; - -// const NavsIcons = [ -// , -// // , -// // , -// , -// , -// -// ] - -const NavsIcons = [ - , - // , - // , - , - , - -] - const NavsRoutes = [ + '/', '/schedule', - '/mysubject', - '/allsubject', + '/mysubjects', + '/allsubjects', '/stats' ] +const iconsKeys: (keyof NavsIconsProp)[] = [ + 'home', + 'schedule', + 'my-subjects', + 'all-subjects', + 'stats' +] + +const statsContent = ['Tín chỉ', 'Điểm số', 'Cá nhân'] + + export default function NavBar() { const [expand, setExpand] = useState(true); + const [clickToExpand, setClickToExpand] = useState(!expand); + const [r] = useAutoAnimate(); - const handleCollapse = () => { + const NavsItems: MenuItem[] = NavsContent.map((navContent, i) => { + return getItem( + +
+ {expand &&
{navContent}
} +
+ + , + i, + NavsIcons[iconsKeys[i]], + navContent === 'Thống kê' ? + statsContent.map((statContent) => getItem(
{statContent}
, statContent)) + : undefined + ) + }) + + const handleClickToExpand = () => { setExpand(!expand); + setClickToExpand(!clickToExpand) }; return (
-
- - {/* */} +
+ { expand && @@ -66,21 +90,12 @@ export default function NavBar() { }
-
- { - NavsContent.map((navContent: string, i: number) => { - return ( -
- -
- {NavsIcons[i]} - {expand &&
{navContent}
} -
- -
- ) - }) - } +
{ if (!expand) setTimeout(() => setExpand(true), 500) }} + onMouseLeave={() => { setExpand(!clickToExpand) }} + > +
) diff --git a/package-lock.json b/package-lock.json index 7916496..33d09c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@ant-design/icons": "^5.2.6", + "@formkit/auto-animate": "^0.8.0", "@reduxjs/toolkit": "^1.9.7", "@types/node": "20.6.2", "@types/react": "18.2.22", @@ -17,14 +18,22 @@ "autoprefixer": "10.4.15", "eslint": "8.49.0", "eslint-config-next": "13.4.19", - "next": "13.4.19", + "invert-color": "^2.0.0", + "lighten-darken-color": "^1.0.0", + "lodash": "^4.17.21", + "next": "^13.5.5", "postcss": "8.4.30", "react": "18.2.0", + "react-countdown-circle-timer": "^3.2.1", "react-dom": "18.2.0", "react-icons": "^4.11.0", "react-redux": "^8.1.3", + "tailwind-merge": "^1.14.0", "tailwindcss": "3.3.3", "typescript": "5.2.2" + }, + "devDependencies": { + "@types/lodash": "^4.14.200" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -192,6 +201,11 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@formkit/auto-animate": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.0.tgz", + "integrity": "sha512-G8f7489ka0mWyi+1IEZT+xgIwcpWtRMmE2x+IrVoQ+KM1cP6VDj/TbujZjwxdb0P8w8b16/qBfViRmydbYHwMw==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -266,9 +280,9 @@ } }, "node_modules/@next/env": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.19.tgz", - "integrity": "sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ==" + "version": "13.5.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.5.tgz", + "integrity": "sha512-agvIhYWp+ilbScg81s/sLueZo8CNEYLjNOqhISxheLmD/AQI4/VxV7bV76i/KzxH4iHy/va0YS9z0AOwGnw4Fg==" }, "node_modules/@next/eslint-plugin-next": { "version": "13.4.19", @@ -279,9 +293,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz", - "integrity": "sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ==", + "version": "13.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.5.tgz", + "integrity": "sha512-FvTdcJdTA7H1FGY8dKPPbf/O0oDC041/znHZwXA7liiGUhgw5hOQ+9z8tWvuz0M5a/SDjY/IRPBAb5FIFogYww==", "cpu": [ "arm64" ], @@ -294,9 +308,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz", - "integrity": "sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw==", + "version": "13.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.5.tgz", + "integrity": "sha512-mTqNIecaojmyia7appVO2QggBe1Z2fdzxgn6jb3x9qlAk8yY2sy4MAcsj71kC9RlenCqDmr9vtC/ESFf110TPA==", "cpu": [ "x64" ], @@ -309,9 +323,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz", - "integrity": "sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg==", + "version": "13.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.5.tgz", + "integrity": "sha512-U9e+kNkfvwh/T8yo+xcslvNXgyMzPPX1IbwCwnHHFmX5ckb1Uc3XZSInNjFQEQR5xhJpB5sFdal+IiBIiLYkZA==", "cpu": [ "arm64" ], @@ -324,9 +338,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz", - "integrity": "sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA==", + "version": "13.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.5.tgz", + "integrity": "sha512-h7b58eIoNCSmKVC5fr167U0HWZ/yGLbkKD9wIller0nGdyl5zfTji0SsPKJvrG8jvKPFt2xOkVBmXlFOtuKynw==", "cpu": [ "arm64" ], @@ -339,9 +353,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz", - "integrity": "sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g==", + "version": "13.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.5.tgz", + "integrity": "sha512-6U4y21T1J6FfcpM9uqzBJicxycpB5gJKLyQ3g6KOfBzT8H1sMwfHTRrvHKB09GIn1BCRy5YJHrA1G26DzqR46w==", "cpu": [ "x64" ], @@ -354,9 +368,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz", - "integrity": "sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q==", + "version": "13.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.5.tgz", + "integrity": "sha512-OuqWSAQHJQM2EsapPFTSU/FLQ0wKm7UeRNatiR/jLeCe1V02aB9xmzuWYo2Neaxxag4rss3S8fj+lvMLzwDaFA==", "cpu": [ "x64" ], @@ -369,9 +383,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz", - "integrity": "sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw==", + "version": "13.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.5.tgz", + "integrity": "sha512-+yLrOZIIZDY4uGn9bLOc0wTgs+M8RuOUFSUK3BhmcLav9e+tcAj0jyBHD4aXv2qWhppUeuYMsyBo1I58/eE6Dg==", "cpu": [ "arm64" ], @@ -384,9 +398,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz", - "integrity": "sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA==", + "version": "13.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.5.tgz", + "integrity": "sha512-SyMxXyJtf9ScMH0Dh87THJMXNFvfkRAk841xyW9SeOX3KxM1buXX3hN7vof4kMGk0Yg996OGsX+7C9ueS8ugsw==", "cpu": [ "ia32" ], @@ -399,9 +413,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz", - "integrity": "sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw==", + "version": "13.5.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.5.tgz", + "integrity": "sha512-n5KVf2Ok0BbLwofAaHiiKf+BQCj1M8WmTujiER4/qzYAVngnsNSjqEWvJ03raeN9eURqxDO+yL5VRoDrR33H9A==", "cpu": [ "x64" ], @@ -586,9 +600,9 @@ "integrity": "sha512-cEjvTPU32OM9lUFegJagO0mRnIn+rbqrG89vV8/xLnLFX0DoR0r1oy5IlTga71Q7uT3Qus7qm7wgeiMT/+Irlg==" }, "node_modules/@swc/helpers": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz", - "integrity": "sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", + "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", "dependencies": { "tslib": "^2.4.0" } @@ -607,6 +621,12 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/lodash": { + "version": "4.14.200", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.200.tgz", + "integrity": "sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q==", + "dev": true + }, "node_modules/@types/node": { "version": "20.6.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", @@ -2467,6 +2487,11 @@ "node": ">= 0.4" } }, + "node_modules/invert-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-color/-/invert-color-2.0.0.tgz", + "integrity": "sha512-9s6IATlhOAr0/0MPUpLdMpk81ixIu8IqwPwORssXBauFT/4ff/iyEOcojd0UYuPwkDbJvL1+blIZGhqVIaAm5Q==" + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -2894,6 +2919,11 @@ "node": ">= 0.8.0" } }, + "node_modules/lighten-darken-color": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lighten-darken-color/-/lighten-darken-color-1.0.0.tgz", + "integrity": "sha512-rKToRUNvcIJGuT+Zd/ljNb83wwJPc4V4HNxYuqIdizQHt3avilV6H1rq2feaxruJBpAIbE1ZJ8wX7BKjTylIsA==" + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -2921,6 +2951,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3025,35 +3060,34 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "node_modules/next": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/next/-/next-13.4.19.tgz", - "integrity": "sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw==", + "version": "13.5.5", + "resolved": "https://registry.npmjs.org/next/-/next-13.5.5.tgz", + "integrity": "sha512-LddFJjpfrtrMMw8Q9VLhIURuSidiCNcMQjRqcPtrKd+Fx07MsG7hYndJb/f2d3I+mTbTotsTJfCnn0eZ/YPk8w==", "dependencies": { - "@next/env": "13.4.19", - "@swc/helpers": "0.5.1", + "@next/env": "13.5.5", + "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", - "postcss": "8.4.14", + "postcss": "8.4.31", "styled-jsx": "5.1.1", - "watchpack": "2.4.0", - "zod": "3.21.4" + "watchpack": "2.4.0" }, "bin": { "next": "dist/bin/next" }, "engines": { - "node": ">=16.8.0" + "node": ">=16.14.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "13.4.19", - "@next/swc-darwin-x64": "13.4.19", - "@next/swc-linux-arm64-gnu": "13.4.19", - "@next/swc-linux-arm64-musl": "13.4.19", - "@next/swc-linux-x64-gnu": "13.4.19", - "@next/swc-linux-x64-musl": "13.4.19", - "@next/swc-win32-arm64-msvc": "13.4.19", - "@next/swc-win32-ia32-msvc": "13.4.19", - "@next/swc-win32-x64-msvc": "13.4.19" + "@next/swc-darwin-arm64": "13.5.5", + "@next/swc-darwin-x64": "13.5.5", + "@next/swc-linux-arm64-gnu": "13.5.5", + "@next/swc-linux-arm64-musl": "13.5.5", + "@next/swc-linux-x64-gnu": "13.5.5", + "@next/swc-linux-x64-musl": "13.5.5", + "@next/swc-win32-arm64-msvc": "13.5.5", + "@next/swc-win32-ia32-msvc": "13.5.5", + "@next/swc-win32-x64-msvc": "13.5.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -3071,9 +3105,9 @@ } }, "node_modules/next/node_modules/postcss": { - "version": "8.4.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", - "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -3082,10 +3116,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -4126,6 +4164,14 @@ "node": ">=0.10.0" } }, + "node_modules/react-countdown-circle-timer": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/react-countdown-circle-timer/-/react-countdown-circle-timer-3.2.1.tgz", + "integrity": "sha512-yBAy/9ILXOiFbLBM+3jS72TW5LeRcH8wkRC9NNqMpUkCXkGjSnaeRbJMsR9lsYF0oVXjSDbJaRbCuVMT+9HnKA==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -4670,6 +4716,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz", + "integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", @@ -5079,14 +5134,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zod": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", - "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } } } diff --git a/package.json b/package.json index fb9d9fa..9509ea5 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@ant-design/icons": "^5.2.6", + "@formkit/auto-animate": "^0.8.0", "@reduxjs/toolkit": "^1.9.7", "@types/node": "20.6.2", "@types/react": "18.2.22", @@ -18,13 +19,21 @@ "autoprefixer": "10.4.15", "eslint": "8.49.0", "eslint-config-next": "13.4.19", - "next": "13.4.19", + "invert-color": "^2.0.0", + "lighten-darken-color": "^1.0.0", + "lodash": "^4.17.21", + "next": "^13.5.5", "postcss": "8.4.30", "react": "18.2.0", + "react-countdown-circle-timer": "^3.2.1", "react-dom": "18.2.0", "react-icons": "^4.11.0", "react-redux": "^8.1.3", + "tailwind-merge": "^1.14.0", "tailwindcss": "3.3.3", "typescript": "5.2.2" + }, + "devDependencies": { + "@types/lodash": "^4.14.200" } } diff --git a/public/images/hero-illo@3x.png b/public/images/hero-illo@3x.png new file mode 100644 index 0000000..e303cff Binary files /dev/null and b/public/images/hero-illo@3x.png differ diff --git a/public/images/homepage-removebg.png b/public/images/homepage-removebg.png new file mode 100644 index 0000000..378345c Binary files /dev/null and b/public/images/homepage-removebg.png differ diff --git a/public/images/homepage.png b/public/images/homepage.png new file mode 100644 index 0000000..bca1640 Binary files /dev/null and b/public/images/homepage.png differ diff --git a/redux/schedule/actions/discardChanges.ts b/redux/schedule/actions/discardChanges.ts new file mode 100644 index 0000000..ff63594 --- /dev/null +++ b/redux/schedule/actions/discardChanges.ts @@ -0,0 +1,7 @@ +import { ScheduleState } from "../scheduleSlice"; +import { cloneDeep } from "lodash"; + +export function discardChanges(state: ScheduleState) { + state.tempData = state.realData; + // state.editing = false; +} diff --git a/redux/schedule/actions/saveChanges.ts b/redux/schedule/actions/saveChanges.ts new file mode 100644 index 0000000..7562cf1 --- /dev/null +++ b/redux/schedule/actions/saveChanges.ts @@ -0,0 +1,6 @@ +import { ScheduleState } from "../scheduleSlice"; + +export function saveChanges(state: ScheduleState) { + state.realData = state.tempData; + state.editing = true; +} diff --git a/redux/schedule/actions/updateScheduleState.ts b/redux/schedule/actions/updateScheduleState.ts new file mode 100644 index 0000000..efc9ec0 --- /dev/null +++ b/redux/schedule/actions/updateScheduleState.ts @@ -0,0 +1,10 @@ +import { ScheduleState } from '@/redux/schedule/scheduleSlice'; +import { ScheduleStyle } from '@/types/schedule'; +import { PayloadAction } from '@reduxjs/toolkit'; + +export function updateScheduleState(state: ScheduleState, action: PayloadAction>) { + return { + ...state, + ...action.payload + } +} diff --git a/redux/schedule/actions/updateScheduleStyle.ts b/redux/schedule/actions/updateScheduleStyle.ts new file mode 100644 index 0000000..ad4f838 --- /dev/null +++ b/redux/schedule/actions/updateScheduleStyle.ts @@ -0,0 +1,11 @@ +import { ScheduleStyle } from "@/types/schedule"; +import { PayloadAction } from "@reduxjs/toolkit"; +import { ScheduleState } from "../scheduleSlice"; + +export function updateScheduleStyle(state: ScheduleState, action: PayloadAction>) { + state.tempData.scheduleStyle = { + ...state.tempData.scheduleStyle, + ...action.payload + } + state.editing = true; +} diff --git a/redux/schedule/actions/updateScheduleSubjects.ts b/redux/schedule/actions/updateScheduleSubjects.ts new file mode 100644 index 0000000..fb18aa1 --- /dev/null +++ b/redux/schedule/actions/updateScheduleSubjects.ts @@ -0,0 +1,29 @@ +import { PayloadAction } from "@reduxjs/toolkit" +import { ScheduleState } from "../scheduleSlice" +import { SubjectClass } from "@/types/subject" +import { isUndefined } from "@/utils/isUndefined" + +export const updateScheduleSubjects = (state: ScheduleState, { + payload: { + index, + newProps + } +}: PayloadAction<{ + index: number + newProps: Partial +}>) => { + state.tempData.subjectClassData[index] = { + ...state.tempData.subjectClassData[index], + ...newProps + } + let subjectClassData = state.tempData.subjectClassData; + + if (!isUndefined(newProps.highlightColor)) { + subjectClassData.forEach((subjectClass, i) => { + if (i !== index && subjectClass.id === subjectClassData[index].id) { + subjectClassData[i].highlightColor = newProps.highlightColor as string + } + }) + } + state.editing = true; +} diff --git a/redux/schedule/scheduleSelector.ts b/redux/schedule/scheduleSelector.ts new file mode 100644 index 0000000..6f82d23 --- /dev/null +++ b/redux/schedule/scheduleSelector.ts @@ -0,0 +1,7 @@ +import { ScheduleInfo } from "@/types/schedule"; +import { RootState } from "../store"; +import { ScheduleState } from "./scheduleSlice"; + +export const scheduleDataSelector = (state: RootState): ScheduleInfo => state.schedule.tempData; + +export const scheduleSelector = (state: RootState): ScheduleState => state.schedule diff --git a/redux/schedule/scheduleSlice.ts b/redux/schedule/scheduleSlice.ts new file mode 100644 index 0000000..e29468e --- /dev/null +++ b/redux/schedule/scheduleSlice.ts @@ -0,0 +1,38 @@ +import { updateScheduleSubjects } from './actions/updateScheduleSubjects'; +import { createSlice } from "@reduxjs/toolkit"; +import { ScheduleInfo } from "@/types/schedule"; +import { getScheduleInfo } from "@/api/schedule"; +import { updateScheduleStyle } from "./actions/updateScheduleStyle"; +import { cloneDeep } from 'lodash'; +import { discardChanges } from './actions/discardChanges'; +import { saveChanges } from './actions/saveChanges'; +import { updateScheduleState } from './actions/updateScheduleState'; + +export interface ScheduleState { + fetched: boolean + tempData: ScheduleInfo + realData: ScheduleInfo + editing: boolean +} + +const initTempData: ScheduleInfo = getScheduleInfo(false); +const initRealData: ScheduleInfo = cloneDeep(initTempData); + +const initialState: ScheduleState = { + fetched: true, + tempData: initTempData, + realData: initRealData, + editing: false +} + +export const {reducer: scheduleReducer, actions: scheduleActions} = createSlice({ + name: 'schedule', + initialState, + reducers: { + updateScheduleState, + updateScheduleStyle, + updateScheduleSubjects, + discardChanges, + saveChanges + } +}) diff --git a/redux/store.ts b/redux/store.ts index b66f3a8..b9cd6e8 100644 --- a/redux/store.ts +++ b/redux/store.ts @@ -1,9 +1,11 @@ import { configureStore } from "@reduxjs/toolkit"; import { authReducer } from "./auth/authSlice"; +import { scheduleReducer } from "./schedule/scheduleSlice"; export const store = configureStore({ reducer: { - auth: authReducer + auth: authReducer, + schedule: scheduleReducer } }); diff --git a/styles/theme.ts b/styles/theme.ts index f49473f..c1511de 100644 --- a/styles/theme.ts +++ b/styles/theme.ts @@ -8,5 +8,8 @@ export const THEME = { LIGHT_PRIMARY_COLOR: '#c6e0f6', SECONDARY_COLOR: '#FFFFFF', ROYAL_GRAY_COLOR: '#7D7C7C', + TABLE_BORDER_COLOR: 'rgba(75, 85, 99)', + PRIMARY_ICON_COLOR: '#7D7C7C', + DANGER_COLOR: '#FF4D4F' // MAIN_FONT: inter, } diff --git a/tailwind.config.ts b/tailwind.config.ts index 060e3c5..361274f 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -2,6 +2,11 @@ import type { Config } from 'tailwindcss' import { THEME } from './styles/theme' const config: Config = { + // mode: 'jit', + // purge: [ + // './public/**/*.html', + // './**/*.{js,jsx,ts,tsx,vue}', + // ], content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', @@ -22,6 +27,11 @@ const config: Config = { "light-primary": THEME.LIGHT_PRIMARY_COLOR, secondary: THEME.SECONDARY_COLOR, "royal-gray": THEME.ROYAL_GRAY_COLOR, + 'table-border': THEME.TABLE_BORDER_COLOR, + 'danger': THEME.DANGER_COLOR + }, + fontSize: { + 'fs-inherit': 'inherit' } }, }, diff --git a/types/schedule.ts b/types/schedule.ts new file mode 100644 index 0000000..279b41c --- /dev/null +++ b/types/schedule.ts @@ -0,0 +1,17 @@ +import { SubjectClass } from "./subject" + +export interface ScheduleStyle { + hasBorder: boolean + roundedBorder: boolean + lessonColumnColor: string + timeColumnColor: string + headerRowColor: string + dividerRowColor: string + hasDivider: boolean + displayColumnSettings: boolean +} + +export interface ScheduleInfo { + subjectClassData: SubjectClass[] + scheduleStyle: ScheduleStyle +} diff --git a/types/subject.ts b/types/subject.ts new file mode 100644 index 0000000..c16ac50 --- /dev/null +++ b/types/subject.ts @@ -0,0 +1,15 @@ + +export interface SubjectClass { + id: string; /// + lessonStart: number; + lessonEnd: number; + group: string; + name: string; + place: string; + credits: number; + teacherName: string; + weekDay: number; + numberOfStudents: number; + highlightColor: string; + description?: string +} diff --git a/utils/isUndefined.ts b/utils/isUndefined.ts new file mode 100644 index 0000000..342cbcc --- /dev/null +++ b/utils/isUndefined.ts @@ -0,0 +1,3 @@ +export function isUndefined(value: any) { + return typeof value === 'undefined'; +} diff --git a/utils/lightenDarkenColor.ts b/utils/lightenDarkenColor.ts new file mode 100644 index 0000000..a29edec --- /dev/null +++ b/utils/lightenDarkenColor.ts @@ -0,0 +1,6 @@ +// @ts-ignore +import {LightenDarkenColor} from 'lighten-darken-color'; + +export function lightenDarkenColor(color: string, amount: number): string { + return LightenDarkenColor(color, amount) +} diff --git a/utils/subjectClass.ts b/utils/subjectClass.ts new file mode 100644 index 0000000..50019fb --- /dev/null +++ b/utils/subjectClass.ts @@ -0,0 +1,63 @@ +import { SubjectClass } from "@/types/subject"; +import { time } from "console"; +import internal from "stream"; + +/** + * thời gian còn lại tính từ hiện tại cho tới thời gian học của lớp đã cho tính bằng giây + * @param subjectClass thông tin về lớp + * @returns thời gian còn lại tính từ hiện tại cho tới thời gian học của lớp đó tính bằng giây + */ + +export function nowToSubjectClass(subjectClass: SubjectClass): number { + const currentDate = new Date(); + let currentWeekday = currentDate.getDay(); + currentWeekday += (currentWeekday === 0 ? 8 : 1); + + const targetDate = new Date(currentDate); + targetDate.setDate(currentDate.getDate() + subjectClass.weekDay - currentWeekday); + targetDate.setHours(lessonToHour(subjectClass.lessonStart), 0, 0, 0); + + if (targetDate < currentDate) + targetDate.setDate(targetDate.getDate() + 7) + + return (targetDate.getTime() - currentDate.getTime()) / 1000; +}; + +/** + * + * @param subjectClasses mảng các môn học + * @returns { + * time: Thời gian đến môn học gần nhất, + * subjectClass: môn học gần nhất, + * index: chỉ số môn học trong mảng + * } hoặc time = 0 và các trường khác undefined nếu mảng rỗng + */ + +export function nowToNextSubjectClass(subjectClasses: SubjectClass[]): { + time: number + subjectClass: SubjectClass + index: number +} { + if (subjectClasses.length === 0) + throw new Error('Hàm nowToNextSubjectClass có subjectClasses rỗng') + let mi: number = 0; + let minTime = nowToSubjectClass(subjectClasses[0]); + + for (let i = 1; i < subjectClasses.length; ++i) { + let current = nowToSubjectClass(subjectClasses[i]); + if (minTime > current) { + minTime = current; + mi = i; + } + } + + return { + time: minTime, + subjectClass: subjectClasses[mi], + index: mi + } +} + +export function lessonToHour(lesson: number): number { + return lesson + 6; +}