Skip to content

Commit 240cf38

Browse files
authored
Merge pull request #8 from codersforcauses/issue-3-Create_Timetable_Page
Add Schedule Page Containing Timetable (Issue #3)
2 parents 9bb2e90 + d763149 commit 240cf38

File tree

8 files changed

+1025
-0
lines changed

8 files changed

+1025
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {
2+
getTimeTopPosition,
3+
getVisibleTimetableRect,
4+
} from "@/components/timetable";
5+
6+
export function resizeAndPositionTimeIndicator() {
7+
const time_indicator = document.getElementById("time-indicator");
8+
if (time_indicator == null) return;
9+
10+
const now_label = document.getElementById("time-indicator-label");
11+
if (now_label == null) return;
12+
13+
const visible = getVisibleTimetableRect();
14+
if (visible == undefined) return;
15+
16+
time_indicator.style.width = visible.width + "px";
17+
time_indicator.style.left = visible.left + "px";
18+
19+
const now = new Date(Date.now());
20+
const hour = now.getHours();
21+
const mins = now.getMinutes();
22+
23+
const top = getTimeTopPosition(hour, mins);
24+
if (top == undefined) return;
25+
time_indicator.style.top = top + "px";
26+
27+
const now_label_rect = now_label.getBoundingClientRect();
28+
if (now_label_rect == undefined) return;
29+
30+
const now_label_left = visible.left - now_label_rect.width;
31+
const now_label_top = top - now_label_rect.height / 2;
32+
now_label.style.left = now_label_left + "px";
33+
now_label.style.top = now_label_top + "px";
34+
35+
if (top < visible.top || top > visible.bottom) {
36+
time_indicator.style.display = "none";
37+
now_label.style.display = "none";
38+
} else {
39+
time_indicator.style.display = "inline";
40+
now_label.style.display = "inline";
41+
}
42+
}
43+
44+
function TimeIndicator() {
45+
return (
46+
<>
47+
<div
48+
id="time-indicator-label"
49+
className="absolute z-[1000] h-fit w-fit rounded-full bg-slate-400 p-1 pl-2 pr-2"
50+
>
51+
<p className="text-slate-100">Now</p>
52+
</div>
53+
<div
54+
id="time-indicator"
55+
className="absolute z-[100] min-h-[2px] bg-slate-200 opacity-75"
56+
></div>
57+
</>
58+
);
59+
}
60+
61+
export default TimeIndicator;

client/src/components/time_tag.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { getDurationMinutes } from "@/components/timetable";
2+
3+
interface TimeTagProps {
4+
start_time: string;
5+
end_time: string;
6+
display?: string;
7+
}
8+
9+
function TimeTag({ start_time, end_time, display }: TimeTagProps) {
10+
const time_string =
11+
start_time.substring(0, 5) + "-" + end_time.substring(0, 5);
12+
13+
const duration_minutes = getDurationMinutes(start_time, end_time);
14+
const hours = Math.floor(duration_minutes / 60);
15+
const minutes = duration_minutes % 60;
16+
const duration_string = hours + "h " + minutes + "m";
17+
18+
let display_string = time_string; // Default to time string
19+
if (display != undefined) {
20+
display = display.toLowerCase();
21+
if (display == "duration") display_string = duration_string;
22+
else if (display == "both") display_string += " (" + duration_string + ")";
23+
}
24+
25+
return (
26+
<div
27+
className={
28+
"flex h-fit w-fit flex-row items-center justify-center rounded-full text-lg font-medium"
29+
}
30+
>
31+
<div className="placeholder-clock aspect-1/1 h-5 w-5 rounded-[50] bg-slate-200"></div>
32+
<p className="ml-1">{display_string}</p>
33+
</div>
34+
);
35+
}
36+
37+
export default TimeTag;
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { ReactElement, useEffect } from "react";
2+
3+
import TimeIndicator, {
4+
resizeAndPositionTimeIndicator,
5+
} from "@/components/time_indicator";
6+
import {
7+
TimetableDayColumn,
8+
TimetableTimeColumn,
9+
} from "@/components/timetable_column";
10+
import { resizeAndPositionTimetableTasks } from "@/components/timetable_task";
11+
12+
export function getVisibleTimetableRect() {
13+
const time_header = document.getElementById("time-header");
14+
if (time_header == null) return;
15+
const time_header_rect = time_header.getBoundingClientRect();
16+
17+
const timetable_barrier = document.getElementById("timetable-barrier");
18+
if (timetable_barrier == null) return;
19+
const timetable_barrier_rect = timetable_barrier.getBoundingClientRect();
20+
21+
const rect = {
22+
left: time_header_rect.right,
23+
right: timetable_barrier_rect.left + timetable_barrier.clientWidth,
24+
top: time_header_rect.bottom,
25+
bottom: timetable_barrier_rect.top + timetable_barrier.clientHeight,
26+
width: 0,
27+
height: 0,
28+
};
29+
rect.width = rect.right - rect.left;
30+
rect.height = rect.bottom - rect.top;
31+
32+
return rect;
33+
}
34+
35+
export function getDurationMinutes(start_time: string, end_time: string) {
36+
const start_hour: number = +start_time.substring(0, 2);
37+
const start_minute: number = +start_time.substring(3, 5);
38+
const end_hour: number = +end_time.substring(0, 2);
39+
const end_minute: number = +end_time.substring(3, 5);
40+
return 60 * (end_hour - start_hour) + (end_minute - start_minute);
41+
}
42+
43+
export function getTimeTopPosition(hour: number, mins: number) {
44+
const hour_label = document.getElementById(hour + ":00:00");
45+
if (hour_label == null) return;
46+
47+
const hour_rect = hour_label.getBoundingClientRect();
48+
if (hour_rect == undefined) return;
49+
50+
const offset = (mins / 60) * hour_rect.height;
51+
const top = hour_rect.top + offset;
52+
53+
return top;
54+
}
55+
56+
export function getDayLeftPosition(day: string) {
57+
const day_label = document.getElementById(day);
58+
if (day_label == null) return;
59+
return day_label.getBoundingClientRect().left;
60+
}
61+
62+
export function resizeTimetableElements() {
63+
resizeAndPositionTimetableTasks();
64+
resizeAndPositionTimeIndicator();
65+
}
66+
67+
export function scrollToCurrentTime(align?: string) {
68+
const timetable_barrier = document.getElementById("timetable-barrier");
69+
if (timetable_barrier == null) return;
70+
71+
const visible = getVisibleTimetableRect();
72+
if (visible == undefined) return;
73+
74+
const timetable = document.getElementById("timetable");
75+
if (timetable == null) return;
76+
77+
const time_header = document.getElementById("time-header");
78+
if (time_header == undefined) return;
79+
const time_header_height = time_header.getBoundingClientRect().height;
80+
if (time_header_height == undefined) return;
81+
82+
const scroll_height = timetable.scrollHeight - time_header_height;
83+
const scroll_max = scroll_height - visible.height; // Top row is sticky
84+
85+
timetable_barrier.scrollTop = 0; // Ensures consistent position
86+
87+
const now = new Date(Date.now());
88+
const now_top = getTimeTopPosition(now.getHours(), now.getMinutes());
89+
if (now_top == undefined) return;
90+
91+
let target_top;
92+
if (align === "top") target_top = visible.top;
93+
else if (align === "bottom") target_top = visible.bottom;
94+
else target_top = visible.top + visible.height / 2;
95+
96+
let scroll_amount = now_top - target_top;
97+
if (scroll_amount < 0) scroll_amount = 0;
98+
if (scroll_amount > scroll_max) scroll_amount = scroll_max;
99+
100+
timetable_barrier.scrollTop = scroll_amount;
101+
}
102+
103+
export function scrollToCurrentDay(align?: string) {
104+
const timetable_barrier = document.getElementById("timetable-barrier");
105+
if (timetable_barrier == null) return;
106+
107+
const visible = getVisibleTimetableRect();
108+
if (visible == undefined) return;
109+
110+
const timetable = document.getElementById("timetable");
111+
if (timetable == null) return;
112+
113+
const time_header = document.getElementById("time-header");
114+
if (time_header == undefined) return;
115+
const time_header_width = time_header.getBoundingClientRect().width;
116+
if (time_header_width == undefined) return;
117+
const row_width = time_header_width; // easier to read later
118+
119+
const scroll_width = timetable.scrollWidth - time_header_width; // sticky
120+
const scroll_max = scroll_width - visible.width;
121+
122+
timetable_barrier.scrollLeft = 0;
123+
124+
enum DateDay {
125+
Monday = 1,
126+
Tuesday,
127+
Wednesday,
128+
Thursday,
129+
Friday,
130+
Saturday,
131+
Sunday,
132+
}
133+
134+
const now = new Date(Date.now());
135+
const today = DateDay[now.getDay()];
136+
const today_left = getDayLeftPosition(today);
137+
if (today_left == undefined) return;
138+
139+
let target_left;
140+
if (align == "left") target_left = visible.left;
141+
else if (align == "right") target_left = visible.right - row_width;
142+
else target_left = visible.left + visible.width / 2 - row_width / 2;
143+
144+
let scroll_amount = today_left - target_left;
145+
if (scroll_amount < 0) scroll_amount = 0;
146+
if (scroll_amount > scroll_max) scroll_amount = scroll_max;
147+
148+
timetable_barrier.scrollLeft = scroll_amount;
149+
}
150+
151+
export function scrollToCurrentTimeAndDay(
152+
time_align?: string,
153+
day_align?: string,
154+
) {
155+
scrollToCurrentTime(time_align);
156+
scrollToCurrentDay(day_align);
157+
}
158+
159+
interface TimetableProps {
160+
children: ReactElement[];
161+
}
162+
163+
function Timetable({ children }: TimetableProps) {
164+
useEffect(() => {
165+
resizeTimetableElements();
166+
scrollToCurrentTimeAndDay();
167+
});
168+
169+
return (
170+
<div
171+
id="timetable-border"
172+
className="h-full w-full rounded-lg bg-slate-900 p-3"
173+
>
174+
<div
175+
id="timetable-barrier"
176+
className="h-full w-full overflow-auto"
177+
onScroll={resizeTimetableElements}
178+
>
179+
<div
180+
id="timetable"
181+
className="timetable flex h-full w-full flex-row gap-1"
182+
>
183+
<TimetableTimeColumn />
184+
<TimetableDayColumn day="Monday" label="Monday" />
185+
<TimetableDayColumn day="Tuesday" label="Tuesday" />
186+
<TimetableDayColumn day="Wednesday" label="Wednesday" />
187+
<TimetableDayColumn day="Thursday" label="Thursday" />
188+
<TimetableDayColumn day="Friday" label="Friday" />
189+
<TimetableDayColumn day="Saturday" label="Saturday" />
190+
<TimetableDayColumn day="Sunday" label="Sunday" />
191+
<TimeIndicator />
192+
<div
193+
id="timetable-tasks"
194+
className="timetable-tasks absolute left-0 top-0"
195+
>
196+
{children}
197+
</div>
198+
</div>
199+
</div>
200+
</div>
201+
);
202+
}
203+
204+
export default Timetable;

0 commit comments

Comments
 (0)