From 6f3f6512946364890f4a1c67690111af0e617c8f Mon Sep 17 00:00:00 2001 From: sianida26 Date: Tue, 25 Jun 2024 02:08:29 +0700 Subject: [PATCH] Added base timetable --- apps/frontend/package.json | 2 + apps/frontend/src/App.tsx | 1 + .../src/components/Timetable/DayColumn.tsx | 95 ++++++++++++++++++ .../src/components/Timetable/HourColumn.tsx | 43 ++++++++ .../src/components/Timetable/Timetable.tsx | 98 +++++++++++++++++++ .../src/components/Timetable/WeekPicker.tsx | 73 ++++++++++++++ .../src/components/Timetable/index.ts | 3 + .../src/components/Timetable/types/Event.d.ts | 7 ++ apps/frontend/src/main.tsx | 1 + apps/frontend/src/routeTree.gen.ts | 20 ++++ apps/frontend/src/routes/__root.tsx | 4 +- .../_dashboardLayout/timetable/index.tsx | 36 +++++++ apps/frontend/src/routes/login/index.lazy.tsx | 4 +- .../frontend/src/routes/logout/index.lazy.tsx | 4 +- apps/frontend/src/styles/fonts/manrope.css | 6 ++ .../src/styles/fonts/plus-jakarta-sans.css | 6 ++ apps/frontend/tailwind.config.js | 8 +- pnpm-lock.yaml | 29 ++++++ 18 files changed, 433 insertions(+), 7 deletions(-) create mode 100644 apps/frontend/src/components/Timetable/DayColumn.tsx create mode 100644 apps/frontend/src/components/Timetable/HourColumn.tsx create mode 100644 apps/frontend/src/components/Timetable/Timetable.tsx create mode 100644 apps/frontend/src/components/Timetable/WeekPicker.tsx create mode 100644 apps/frontend/src/components/Timetable/index.ts create mode 100644 apps/frontend/src/components/Timetable/types/Event.d.ts create mode 100644 apps/frontend/src/routes/_dashboardLayout/timetable/index.tsx create mode 100644 apps/frontend/src/styles/fonts/manrope.css create mode 100644 apps/frontend/src/styles/fonts/plus-jakarta-sans.css diff --git a/apps/frontend/package.json b/apps/frontend/package.json index aef672a..bfd0c9a 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -11,6 +11,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@mantine/core": "^7.10.2", + "@mantine/dates": "^7.10.2", "@mantine/form": "^7.10.2", "@mantine/hooks": "^7.10.2", "@mantine/notifications": "^7.10.2", @@ -19,6 +20,7 @@ "@tanstack/react-table": "^8.17.3", "backend": "workspace:*", "clsx": "^2.1.1", + "dayjs": "^1.11.11", "hono": "^4.4.6", "mantine-form-zod-resolver": "^1.1.0", "react": "^18.3.1", diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index f5dc7ae..a76a82b 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -8,6 +8,7 @@ import { routeTree } from "./routeTree.gen"; import "@mantine/core/styles.css"; import "@mantine/notifications/styles.css"; +import "@mantine/dates/styles.css"; import { AuthProvider } from "./contexts/AuthContext"; const queryClient = new QueryClient(); diff --git a/apps/frontend/src/components/Timetable/DayColumn.tsx b/apps/frontend/src/components/Timetable/DayColumn.tsx new file mode 100644 index 0000000..f6e2d28 --- /dev/null +++ b/apps/frontend/src/components/Timetable/DayColumn.tsx @@ -0,0 +1,95 @@ +import dayjs from "dayjs"; +import isoWeek from "dayjs/plugin/isoWeek"; +import customParseFormat from "dayjs/plugin/customParseFormat"; +import Event from "./types/Event"; + +dayjs.extend(isoWeek); +dayjs.extend(customParseFormat); + +type Props = { + day: dayjs.Dayjs; + startTime: dayjs.Dayjs; + endTime: dayjs.Dayjs; + events: Event[]; +}; + +export default function DayColumn({ day, startTime, endTime, events }: Props) { + const isToday = day.isSame(dayjs(), "day"); + + return ( +
+ {/* Column Header */} +
+ {isToday && ( +
+ )} +

{day.date()}

+

{day.format("dddd")}

+
+ + {/* Hour rows */} + {[...new Array(endTime.diff(startTime, "h"))].map((_, i) => { + const currentDateTime = day.hour(startTime.hour() + i); + + return ( + + ); + })} +
+ ); +} diff --git a/apps/frontend/src/components/Timetable/HourColumn.tsx b/apps/frontend/src/components/Timetable/HourColumn.tsx new file mode 100644 index 0000000..511f666 --- /dev/null +++ b/apps/frontend/src/components/Timetable/HourColumn.tsx @@ -0,0 +1,43 @@ +import dayjs from "dayjs"; +import isoWeek from "dayjs/plugin/isoWeek"; +import customParseFormat from "dayjs/plugin/customParseFormat"; +import { useMemo } from "react"; + +dayjs.extend(isoWeek); +dayjs.extend(customParseFormat); + +type Props = { + startTime: dayjs.Dayjs; + endTime: dayjs.Dayjs; +}; + +export default function HourColumn({ startTime, endTime }: Props) { + const hoursArray = useMemo(() => { + const arr: dayjs.Dayjs[] = []; + let currentTime = startTime; + + while (currentTime.isBefore(endTime)) { + arr.push(currentTime); + currentTime = currentTime.add(1, "hour"); + } + + return arr; + }, [startTime, endTime]); + + return ( +
+ {/* Column Header */} +
+ + {/* Hour Rows */} + {hoursArray.map((h) => ( +
+ {h.format("hA").toLowerCase()} +
+ ))} +
+ ); +} diff --git a/apps/frontend/src/components/Timetable/Timetable.tsx b/apps/frontend/src/components/Timetable/Timetable.tsx new file mode 100644 index 0000000..7e563e4 --- /dev/null +++ b/apps/frontend/src/components/Timetable/Timetable.tsx @@ -0,0 +1,98 @@ +import dayjs from "dayjs"; +import { useMemo, useState } from "react"; + +import isoWeek from "dayjs/plugin/isoWeek"; +import customParseFormat from "dayjs/plugin/customParseFormat"; +import HourColumn from "./HourColumn"; +import DayColumn from "./DayColumn"; +import { TbChevronLeft, TbChevronRight } from "react-icons/tb"; +import Event from "./types/Event"; +import WeekPicker from "./WeekPicker"; + +dayjs.extend(isoWeek); +dayjs.extend(customParseFormat); + +type Props = { + events: Event[]; +}; + +export default function Timetable({ events }: Props) { + const [currentDate, setCurrentDate] = useState(dayjs()); + + const startTime = dayjs("08:00", "HH:mm"); + const endTime = dayjs("18:00", "HH:mm"); + + const weekDays = useMemo(() => { + const startOfWeek = currentDate.startOf("isoWeek"); + + return [...new Array(7)].map((_, i) => startOfWeek.add(i, "day")); + }, [currentDate]); + + const eventPerDay = useMemo(() => { + const startOfWeek = currentDate.startOf("isoWeek"); + + return [...new Array(7)].map((_, i) => { + const currentDateIteration = startOfWeek.add(i, "day"); + + return events.filter((event) => { + return ( + event.start.isSame(currentDateIteration, "day") || + event.end.isSame(currentDateIteration, "day") + ); + }); + }); + }, [currentDate, events]); + + return ( +
+ {/* Header */} +
+ {/* Left */} +
+ + +
+ + +
+ + setCurrentDate(dayjs(date))} + /> +
+
+ {/* The Table */} +
+ {/* Columns */} + + {weekDays.map((day, i) => ( + + ))} +
+
+ ); +} diff --git a/apps/frontend/src/components/Timetable/WeekPicker.tsx b/apps/frontend/src/components/Timetable/WeekPicker.tsx new file mode 100644 index 0000000..f1bb80a --- /dev/null +++ b/apps/frontend/src/components/Timetable/WeekPicker.tsx @@ -0,0 +1,73 @@ +import { Popover } from "@mantine/core"; +import { DatePicker } from "@mantine/dates"; +import dayjs from "dayjs"; +import { useState } from "react"; + +interface Props { + currentDate: dayjs.Dayjs; + onChange: (date: Date) => void; +} + +function getDay(date: Date) { + const day = date.getDay(); + return day === 0 ? 6 : day - 1; +} + +function startOfWeek(date: Date) { + return new Date( + date.getFullYear(), + date.getMonth(), + date.getDate() - getDay(date) - 1 + ); +} + +function endOfWeek(date: Date) { + return dayjs( + new Date( + date.getFullYear(), + date.getMonth(), + date.getDate() + (6 - getDay(date)) + ) + ) + .endOf("date") + .toDate(); +} + +function isInWeekRange(date: Date, value: Date | null) { + return value + ? dayjs(date).isBefore(endOfWeek(value)) && + dayjs(date).isAfter(startOfWeek(value)) + : false; +} + +export default function WeekPicker({ currentDate, onChange }: Props) { + const [hovered, setHovered] = useState(null); + const [value, setValue] = useState(null); + + return ( + + + + + + { + const isHovered = isInWeekRange(date, hovered); + const isSelected = isInWeekRange(date, value); + const isInRange = isHovered || isSelected; + return { + onMouseEnter: () => setHovered(date), + onMouseLeave: () => setHovered(null), + inRange: isInRange, + firstInRange: isInRange && date.getDay() === 1, + lastInRange: isInRange && date.getDay() === 0, + selected: isSelected, + onClick: () => setValue(date), + }; + }} + onChange={(date) => onChange(date ?? new Date())} + /> + + + ); +} diff --git a/apps/frontend/src/components/Timetable/index.ts b/apps/frontend/src/components/Timetable/index.ts new file mode 100644 index 0000000..a4b0d6c --- /dev/null +++ b/apps/frontend/src/components/Timetable/index.ts @@ -0,0 +1,3 @@ +import Timetable from "./Timetable"; + +export default Timetable; diff --git a/apps/frontend/src/components/Timetable/types/Event.d.ts b/apps/frontend/src/components/Timetable/types/Event.d.ts new file mode 100644 index 0000000..6edb3d0 --- /dev/null +++ b/apps/frontend/src/components/Timetable/types/Event.d.ts @@ -0,0 +1,7 @@ +type Event = { + title: string; + start: dayjs.Dayjs; + end: dayjs.Dayjs; +}; + +export default Event; diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx index dca3695..6e738f1 100644 --- a/apps/frontend/src/main.tsx +++ b/apps/frontend/src/main.tsx @@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client"; import App from "./App.tsx"; import "./index.css"; import "./styles/tailwind.css"; +import "./styles/fonts/manrope.css"; ReactDOM.createRoot(document.getElementById("root")!).render( diff --git a/apps/frontend/src/routeTree.gen.ts b/apps/frontend/src/routeTree.gen.ts index 4ba8b71..8619974 100644 --- a/apps/frontend/src/routeTree.gen.ts +++ b/apps/frontend/src/routeTree.gen.ts @@ -15,6 +15,7 @@ import { createFileRoute } from '@tanstack/react-router' import { Route as rootRoute } from './routes/__root' import { Route as DashboardLayoutImport } from './routes/_dashboardLayout' import { Route as DashboardLayoutUsersIndexImport } from './routes/_dashboardLayout/users/index' +import { Route as DashboardLayoutTimetableIndexImport } from './routes/_dashboardLayout/timetable/index' import { Route as DashboardLayoutDashboardIndexImport } from './routes/_dashboardLayout/dashboard/index' // Create Virtual Routes @@ -52,6 +53,12 @@ const DashboardLayoutUsersIndexRoute = DashboardLayoutUsersIndexImport.update({ import('./routes/_dashboardLayout/users/index.lazy').then((d) => d.Route), ) +const DashboardLayoutTimetableIndexRoute = + DashboardLayoutTimetableIndexImport.update({ + path: '/timetable/', + getParentRoute: () => DashboardLayoutRoute, + } as any) + const DashboardLayoutDashboardIndexRoute = DashboardLayoutDashboardIndexImport.update({ path: '/dashboard/', @@ -97,6 +104,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DashboardLayoutDashboardIndexImport parentRoute: typeof DashboardLayoutImport } + '/_dashboardLayout/timetable/': { + id: '/_dashboardLayout/timetable/' + path: '/timetable' + fullPath: '/timetable' + preLoaderRoute: typeof DashboardLayoutTimetableIndexImport + parentRoute: typeof DashboardLayoutImport + } '/_dashboardLayout/users/': { id: '/_dashboardLayout/users/' path: '/users' @@ -113,6 +127,7 @@ export const routeTree = rootRoute.addChildren({ IndexLazyRoute, DashboardLayoutRoute: DashboardLayoutRoute.addChildren({ DashboardLayoutDashboardIndexRoute, + DashboardLayoutTimetableIndexRoute, DashboardLayoutUsersIndexRoute, }), LoginIndexLazyRoute, @@ -140,6 +155,7 @@ export const routeTree = rootRoute.addChildren({ "filePath": "_dashboardLayout.tsx", "children": [ "/_dashboardLayout/dashboard/", + "/_dashboardLayout/timetable/", "/_dashboardLayout/users/" ] }, @@ -153,6 +169,10 @@ export const routeTree = rootRoute.addChildren({ "filePath": "_dashboardLayout/dashboard/index.tsx", "parent": "/_dashboardLayout" }, + "/_dashboardLayout/timetable/": { + "filePath": "_dashboardLayout/timetable/index.tsx", + "parent": "/_dashboardLayout" + }, "/_dashboardLayout/users/": { "filePath": "_dashboardLayout/users/index.tsx", "parent": "/_dashboardLayout" diff --git a/apps/frontend/src/routes/__root.tsx b/apps/frontend/src/routes/__root.tsx index 72086a5..91b0ffe 100644 --- a/apps/frontend/src/routes/__root.tsx +++ b/apps/frontend/src/routes/__root.tsx @@ -8,9 +8,9 @@ interface RouteContext { export const Route = createRootRouteWithContext()({ component: () => ( - <> +
- +
), }); diff --git a/apps/frontend/src/routes/_dashboardLayout/timetable/index.tsx b/apps/frontend/src/routes/_dashboardLayout/timetable/index.tsx new file mode 100644 index 0000000..268d5af --- /dev/null +++ b/apps/frontend/src/routes/_dashboardLayout/timetable/index.tsx @@ -0,0 +1,36 @@ +import Timetable from "@/components/Timetable"; +import { Card } from "@mantine/core"; +import { createFileRoute } from "@tanstack/react-router"; +import dayjs from "dayjs"; + +export const Route = createFileRoute("/_dashboardLayout/timetable/")({ + component: TimetablePage, +}); + +const events = [ + { + title: "Hehe 1", + start: dayjs("24 June 2024 09:00", "DD MMMM YYYY HH:mm"), + end: dayjs("24 June 2024 11:45", "DD MMMM YYYY HH:mm"), + }, + { + title: "Hehe 2", + start: dayjs("24 June 2024 09:30", "DD MMMM YYYY HH:mm"), + end: dayjs("24 June 2024 10:00", "DD MMMM YYYY HH:mm"), + }, + { + title: "Hehe 3", + start: dayjs("24 June 2024 10:30", "DD MMMM YYYY HH:mm"), + end: dayjs("24 June 2024 11:00", "DD MMMM YYYY HH:mm"), + }, +]; + +export default function TimetablePage() { + console.log(events); + + return ( + + + + ); +} diff --git a/apps/frontend/src/routes/login/index.lazy.tsx b/apps/frontend/src/routes/login/index.lazy.tsx index ffa3130..bc9d30d 100644 --- a/apps/frontend/src/routes/login/index.lazy.tsx +++ b/apps/frontend/src/routes/login/index.lazy.tsx @@ -1,4 +1,4 @@ -import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { createLazyFileRoute, useNavigate } from "@tanstack/react-router"; import { useMutation } from "@tanstack/react-query"; import { Paper, @@ -17,7 +17,7 @@ import { zodResolver } from "mantine-form-zod-resolver"; import { useEffect, useState } from "react"; import useAuth from "@/hooks/useAuth"; -export const Route = createFileRoute("/login/")({ +export const Route = createLazyFileRoute("/login/")({ component: LoginPage, }); diff --git a/apps/frontend/src/routes/logout/index.lazy.tsx b/apps/frontend/src/routes/logout/index.lazy.tsx index f41fa0a..f42a734 100644 --- a/apps/frontend/src/routes/logout/index.lazy.tsx +++ b/apps/frontend/src/routes/logout/index.lazy.tsx @@ -1,9 +1,9 @@ import useAuth from "@/hooks/useAuth"; import { useQueryClient } from "@tanstack/react-query"; -import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { createLazyFileRoute, useNavigate } from "@tanstack/react-router"; import { useEffect } from "react"; -export const Route = createFileRoute("/logout/")({ +export const Route = createLazyFileRoute("/logout/")({ component: LogoutPage, }); diff --git a/apps/frontend/src/styles/fonts/manrope.css b/apps/frontend/src/styles/fonts/manrope.css new file mode 100644 index 0000000..cb726d3 --- /dev/null +++ b/apps/frontend/src/styles/fonts/manrope.css @@ -0,0 +1,6 @@ +@import url("https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&display=swap"); + +.font-manrope { + font-family: "Manrope", sans-serif; + font-optical-sizing: auto; +} diff --git a/apps/frontend/src/styles/fonts/plus-jakarta-sans.css b/apps/frontend/src/styles/fonts/plus-jakarta-sans.css new file mode 100644 index 0000000..389fe4b --- /dev/null +++ b/apps/frontend/src/styles/fonts/plus-jakarta-sans.css @@ -0,0 +1,6 @@ +@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap"); + +.font-plus-jakarta-sans { + font-family: "Plus Jakarta Sans", sans-serif; + font-optical-sizing: auto; +} diff --git a/apps/frontend/tailwind.config.js b/apps/frontend/tailwind.config.js index f7de5e9..11838c3 100644 --- a/apps/frontend/tailwind.config.js +++ b/apps/frontend/tailwind.config.js @@ -1,8 +1,14 @@ +import colors from "tailwindcss/colors"; + /** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { - extend: {}, + extend: { + colors: { + primary: colors.blue, + }, + }, }, plugins: [], }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 812acdc..41769ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,6 +90,9 @@ importers: '@mantine/core': specifier: ^7.10.2 version: 7.10.2(@mantine/hooks@7.10.2(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mantine/dates': + specifier: ^7.10.2 + version: 7.10.2(@mantine/core@7.10.2(@mantine/hooks@7.10.2(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.10.2(react@18.3.1))(dayjs@1.11.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/form': specifier: ^7.10.2 version: 7.10.2(react@18.3.1) @@ -114,6 +117,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + dayjs: + specifier: ^1.11.11 + version: 1.11.11 hono: specifier: ^4.4.6 version: 4.4.6 @@ -1338,6 +1344,15 @@ packages: react: ^18.2.0 react-dom: ^18.2.0 + '@mantine/dates@7.10.2': + resolution: {integrity: sha512-3YwrQ7UzwnKq07wS9/N10jkMHtTlOZI7TM9uRo4M2HPzw8d9w9IN21qAnVDkOCfATWzxiINcQEtICTdtDHhMFg==} + peerDependencies: + '@mantine/core': 7.10.2 + '@mantine/hooks': 7.10.2 + dayjs: '>=1.0.0' + react: ^18.2.0 + react-dom: ^18.2.0 + '@mantine/form@7.10.2': resolution: {integrity: sha512-OlXQ04orkwQO+AEeA4OihYtfxpaoK/LC1r2/nnUQmChG/GO1X9MoEW8oTQYKyYDIpQc8+lHhos4gl9dEF5YAWw==} peerDependencies: @@ -2467,6 +2482,9 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} + dayjs@1.11.11: + resolution: {integrity: sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -6317,6 +6335,15 @@ snapshots: transitivePeerDependencies: - '@types/react' + '@mantine/dates@7.10.2(@mantine/core@7.10.2(@mantine/hooks@7.10.2(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@7.10.2(react@18.3.1))(dayjs@1.11.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@mantine/core': 7.10.2(@mantine/hooks@7.10.2(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mantine/hooks': 7.10.2(react@18.3.1) + clsx: 2.1.1 + dayjs: 1.11.11 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@mantine/form@7.10.2(react@18.3.1)': dependencies: fast-deep-equal: 3.1.3 @@ -7683,6 +7710,8 @@ snapshots: dependencies: '@babel/runtime': 7.24.7 + dayjs@1.11.11: {} + debug@3.2.7: dependencies: ms: 2.1.3