Added base timetable

This commit is contained in:
sianida26 2024-06-25 02:08:29 +07:00
parent 3d209968ce
commit 6f3f651294
18 changed files with 433 additions and 7 deletions

View File

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@mantine/core": "^7.10.2", "@mantine/core": "^7.10.2",
"@mantine/dates": "^7.10.2",
"@mantine/form": "^7.10.2", "@mantine/form": "^7.10.2",
"@mantine/hooks": "^7.10.2", "@mantine/hooks": "^7.10.2",
"@mantine/notifications": "^7.10.2", "@mantine/notifications": "^7.10.2",
@ -19,6 +20,7 @@
"@tanstack/react-table": "^8.17.3", "@tanstack/react-table": "^8.17.3",
"backend": "workspace:*", "backend": "workspace:*",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.11",
"hono": "^4.4.6", "hono": "^4.4.6",
"mantine-form-zod-resolver": "^1.1.0", "mantine-form-zod-resolver": "^1.1.0",
"react": "^18.3.1", "react": "^18.3.1",

View File

@ -8,6 +8,7 @@ import { routeTree } from "./routeTree.gen";
import "@mantine/core/styles.css"; import "@mantine/core/styles.css";
import "@mantine/notifications/styles.css"; import "@mantine/notifications/styles.css";
import "@mantine/dates/styles.css";
import { AuthProvider } from "./contexts/AuthContext"; import { AuthProvider } from "./contexts/AuthContext";
const queryClient = new QueryClient(); const queryClient = new QueryClient();

View File

@ -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 (
<div className="basis-[14.3%] flex flex-col border-r border-t border-b border-gray-100">
{/* Column Header */}
<div className="flex flex-col h-20 p-2 relative">
{isToday && (
<div className="w-full h-1 bg-primary-500 top-0 left-0 absolute" />
)}
<p className="text-2xl font-bold">{day.date()}</p>
<p>{day.format("dddd")}</p>
</div>
{/* Hour rows */}
{[...new Array(endTime.diff(startTime, "h"))].map((_, i) => {
const currentDateTime = day.hour(startTime.hour() + i);
return (
<button
key={i}
className="border-t h-20 hover:bg-gray-100 flex pr-1.5 gap-1 relative"
>
{/* <div className="absolute top-1 right-1 text-sm text-gray-500 rounded-full size-6 bg-gray-100 flex items-center justify-center">
5
</div> */}
{events
.filter((event) => {
// return event.start.isSame(
// startTime.add(i, "h"),
// "hour"
// );
return (
currentDateTime.isSame(
event.start,
"hour"
) ||
(currentDateTime.isAfter(event.start) &&
currentDateTime.isBefore(event.end))
);
})
.map((event, i) =>
currentDateTime.isSame(event.start, "hour") ? (
<div
key={i}
className="bg-primary-100 rounded-sm text-sm text-left pl-1 text-primary-800 font-medium w-full z-10 relative"
style={{
minHeight: "min-content",
// The height is calculated from the duration of the event in minutes, converted to a percentage of an hour,
// plus an additional number of pixels equivalent to the number of hours in the event duration
height: `calc(${
(event.end.diff(
event.start,
"minute"
) *
100) /
60
}% + ${event.end.diff(event.start, "hour")}px)`,
// The top position is calculated from the start minute of the event, converted to a percentage of an hour
top: `${(event.start.minute() * 100) / 60}%`,
}}
>
{event.title}
</div>
) : (
<div className="w-full"></div>
)
)}
{/* {Math.random() < 0.1 && (
<div className="bg-purple-200/80 rounded-md w-full h-full"></div>
)} */}
</button>
);
})}
</div>
);
}

View File

@ -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 (
<div className="flex flex-col border w-12">
{/* Column Header */}
<div className="flex flex-col h-20"></div>
{/* Hour Rows */}
{hoursArray.map((h) => (
<div
key={h.format("HH:mm")}
className="border-t h-20 flex items-center justify-center font-medium text-xs text-gray-500"
>
{h.format("hA").toLowerCase()}
</div>
))}
</div>
);
}

View File

@ -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 (
<div className="w-full h-full flex flex-col gap-4">
{/* Header */}
<div className="flex justify-between items-center">
{/* Left */}
<div className="flex gap-8">
<button
className="flex items-center border border-gray-900 font-medium px-2 py-1 rounded-md"
onClick={() => setCurrentDate(dayjs())}
>
Today
</button>
<div className="flex gap-2">
<button
onClick={() =>
setCurrentDate(currentDate.subtract(1, "week"))
}
>
<TbChevronLeft />
</button>
<button
onClick={() =>
setCurrentDate(currentDate.add(1, "week"))
}
>
<TbChevronRight />
</button>
</div>
<WeekPicker
currentDate={currentDate}
onChange={(date) => setCurrentDate(dayjs(date))}
/>
</div>
</div>
{/* The Table */}
<div className="flex">
{/* Columns */}
<HourColumn startTime={startTime} endTime={endTime} />
{weekDays.map((day, i) => (
<DayColumn
key={day.format()}
day={day}
events={eventPerDay[i]}
startTime={startTime}
endTime={endTime}
/>
))}
</div>
</div>
);
}

View File

@ -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<Date | null>(null);
const [value, setValue] = useState<Date | null>(null);
return (
<Popover>
<Popover.Target>
<button>{currentDate.format("MMMM YYYY")}</button>
</Popover.Target>
<Popover.Dropdown>
<DatePicker
getDayProps={(date) => {
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())}
/>
</Popover.Dropdown>
</Popover>
);
}

View File

@ -0,0 +1,3 @@
import Timetable from "./Timetable";
export default Timetable;

View File

@ -0,0 +1,7 @@
type Event = {
title: string;
start: dayjs.Dayjs;
end: dayjs.Dayjs;
};
export default Event;

View File

@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client";
import App from "./App.tsx"; import App from "./App.tsx";
import "./index.css"; import "./index.css";
import "./styles/tailwind.css"; import "./styles/tailwind.css";
import "./styles/fonts/manrope.css";
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>

View File

@ -15,6 +15,7 @@ import { createFileRoute } from '@tanstack/react-router'
import { Route as rootRoute } from './routes/__root' import { Route as rootRoute } from './routes/__root'
import { Route as DashboardLayoutImport } from './routes/_dashboardLayout' import { Route as DashboardLayoutImport } from './routes/_dashboardLayout'
import { Route as DashboardLayoutUsersIndexImport } from './routes/_dashboardLayout/users/index' 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' import { Route as DashboardLayoutDashboardIndexImport } from './routes/_dashboardLayout/dashboard/index'
// Create Virtual Routes // Create Virtual Routes
@ -52,6 +53,12 @@ const DashboardLayoutUsersIndexRoute = DashboardLayoutUsersIndexImport.update({
import('./routes/_dashboardLayout/users/index.lazy').then((d) => d.Route), import('./routes/_dashboardLayout/users/index.lazy').then((d) => d.Route),
) )
const DashboardLayoutTimetableIndexRoute =
DashboardLayoutTimetableIndexImport.update({
path: '/timetable/',
getParentRoute: () => DashboardLayoutRoute,
} as any)
const DashboardLayoutDashboardIndexRoute = const DashboardLayoutDashboardIndexRoute =
DashboardLayoutDashboardIndexImport.update({ DashboardLayoutDashboardIndexImport.update({
path: '/dashboard/', path: '/dashboard/',
@ -97,6 +104,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardLayoutDashboardIndexImport preLoaderRoute: typeof DashboardLayoutDashboardIndexImport
parentRoute: typeof DashboardLayoutImport parentRoute: typeof DashboardLayoutImport
} }
'/_dashboardLayout/timetable/': {
id: '/_dashboardLayout/timetable/'
path: '/timetable'
fullPath: '/timetable'
preLoaderRoute: typeof DashboardLayoutTimetableIndexImport
parentRoute: typeof DashboardLayoutImport
}
'/_dashboardLayout/users/': { '/_dashboardLayout/users/': {
id: '/_dashboardLayout/users/' id: '/_dashboardLayout/users/'
path: '/users' path: '/users'
@ -113,6 +127,7 @@ export const routeTree = rootRoute.addChildren({
IndexLazyRoute, IndexLazyRoute,
DashboardLayoutRoute: DashboardLayoutRoute.addChildren({ DashboardLayoutRoute: DashboardLayoutRoute.addChildren({
DashboardLayoutDashboardIndexRoute, DashboardLayoutDashboardIndexRoute,
DashboardLayoutTimetableIndexRoute,
DashboardLayoutUsersIndexRoute, DashboardLayoutUsersIndexRoute,
}), }),
LoginIndexLazyRoute, LoginIndexLazyRoute,
@ -140,6 +155,7 @@ export const routeTree = rootRoute.addChildren({
"filePath": "_dashboardLayout.tsx", "filePath": "_dashboardLayout.tsx",
"children": [ "children": [
"/_dashboardLayout/dashboard/", "/_dashboardLayout/dashboard/",
"/_dashboardLayout/timetable/",
"/_dashboardLayout/users/" "/_dashboardLayout/users/"
] ]
}, },
@ -153,6 +169,10 @@ export const routeTree = rootRoute.addChildren({
"filePath": "_dashboardLayout/dashboard/index.tsx", "filePath": "_dashboardLayout/dashboard/index.tsx",
"parent": "/_dashboardLayout" "parent": "/_dashboardLayout"
}, },
"/_dashboardLayout/timetable/": {
"filePath": "_dashboardLayout/timetable/index.tsx",
"parent": "/_dashboardLayout"
},
"/_dashboardLayout/users/": { "/_dashboardLayout/users/": {
"filePath": "_dashboardLayout/users/index.tsx", "filePath": "_dashboardLayout/users/index.tsx",
"parent": "/_dashboardLayout" "parent": "/_dashboardLayout"

View File

@ -8,9 +8,9 @@ interface RouteContext {
export const Route = createRootRouteWithContext<RouteContext>()({ export const Route = createRootRouteWithContext<RouteContext>()({
component: () => ( component: () => (
<> <div className="font-manrope">
<Outlet /> <Outlet />
<TanStackRouterDevtools /> <TanStackRouterDevtools />
</> </div>
), ),
}); });

View File

@ -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 (
<Card>
<Timetable events={events} />
</Card>
);
}

View File

@ -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 { useMutation } from "@tanstack/react-query";
import { import {
Paper, Paper,
@ -17,7 +17,7 @@ import { zodResolver } from "mantine-form-zod-resolver";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import useAuth from "@/hooks/useAuth"; import useAuth from "@/hooks/useAuth";
export const Route = createFileRoute("/login/")({ export const Route = createLazyFileRoute("/login/")({
component: LoginPage, component: LoginPage,
}); });

View File

@ -1,9 +1,9 @@
import useAuth from "@/hooks/useAuth"; import useAuth from "@/hooks/useAuth";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { createLazyFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react"; import { useEffect } from "react";
export const Route = createFileRoute("/logout/")({ export const Route = createLazyFileRoute("/logout/")({
component: LogoutPage, component: LogoutPage,
}); });

View File

@ -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;
}

View File

@ -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;
}

View File

@ -1,8 +1,14 @@
import colors from "tailwindcss/colors";
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: { theme: {
extend: {}, extend: {
colors: {
primary: colors.blue,
},
},
}, },
plugins: [], plugins: [],
}; };

View File

@ -90,6 +90,9 @@ importers:
'@mantine/core': '@mantine/core':
specifier: ^7.10.2 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) 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': '@mantine/form':
specifier: ^7.10.2 specifier: ^7.10.2
version: 7.10.2(react@18.3.1) version: 7.10.2(react@18.3.1)
@ -114,6 +117,9 @@ importers:
clsx: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
dayjs:
specifier: ^1.11.11
version: 1.11.11
hono: hono:
specifier: ^4.4.6 specifier: ^4.4.6
version: 4.4.6 version: 4.4.6
@ -1338,6 +1344,15 @@ packages:
react: ^18.2.0 react: ^18.2.0
react-dom: ^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': '@mantine/form@7.10.2':
resolution: {integrity: sha512-OlXQ04orkwQO+AEeA4OihYtfxpaoK/LC1r2/nnUQmChG/GO1X9MoEW8oTQYKyYDIpQc8+lHhos4gl9dEF5YAWw==} resolution: {integrity: sha512-OlXQ04orkwQO+AEeA4OihYtfxpaoK/LC1r2/nnUQmChG/GO1X9MoEW8oTQYKyYDIpQc8+lHhos4gl9dEF5YAWw==}
peerDependencies: peerDependencies:
@ -2467,6 +2482,9 @@ packages:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
engines: {node: '>=0.11'} engines: {node: '>=0.11'}
dayjs@1.11.11:
resolution: {integrity: sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==}
debug@3.2.7: debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies: peerDependencies:
@ -6317,6 +6335,15 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- '@types/react' - '@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)': '@mantine/form@7.10.2(react@18.3.1)':
dependencies: dependencies:
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
@ -7683,6 +7710,8 @@ snapshots:
dependencies: dependencies:
'@babel/runtime': 7.24.7 '@babel/runtime': 7.24.7
dayjs@1.11.11: {}
debug@3.2.7: debug@3.2.7:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3