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