diff --git a/client/src/components/time_indicator.tsx b/client/src/components/time_indicator.tsx
new file mode 100644
index 0000000..f053a70
--- /dev/null
+++ b/client/src/components/time_indicator.tsx
@@ -0,0 +1,61 @@
+import {
+ getTimeTopPosition,
+ getVisibleTimetableRect,
+} from "@/components/timetable";
+
+export function resizeAndPositionTimeIndicator() {
+ const time_indicator = document.getElementById("time-indicator");
+ if (time_indicator == null) return;
+
+ const now_label = document.getElementById("time-indicator-label");
+ if (now_label == null) return;
+
+ const visible = getVisibleTimetableRect();
+ if (visible == undefined) return;
+
+ time_indicator.style.width = visible.width + "px";
+ time_indicator.style.left = visible.left + "px";
+
+ const now = new Date(Date.now());
+ const hour = now.getHours();
+ const mins = now.getMinutes();
+
+ const top = getTimeTopPosition(hour, mins);
+ if (top == undefined) return;
+ time_indicator.style.top = top + "px";
+
+ const now_label_rect = now_label.getBoundingClientRect();
+ if (now_label_rect == undefined) return;
+
+ const now_label_left = visible.left - now_label_rect.width;
+ const now_label_top = top - now_label_rect.height / 2;
+ now_label.style.left = now_label_left + "px";
+ now_label.style.top = now_label_top + "px";
+
+ if (top < visible.top || top > visible.bottom) {
+ time_indicator.style.display = "none";
+ now_label.style.display = "none";
+ } else {
+ time_indicator.style.display = "inline";
+ now_label.style.display = "inline";
+ }
+}
+
+function TimeIndicator() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default TimeIndicator;
diff --git a/client/src/components/time_tag.tsx b/client/src/components/time_tag.tsx
new file mode 100644
index 0000000..6e02758
--- /dev/null
+++ b/client/src/components/time_tag.tsx
@@ -0,0 +1,37 @@
+import { getDurationMinutes } from "@/components/timetable";
+
+interface TimeTagProps {
+ start_time: string;
+ end_time: string;
+ display?: string;
+}
+
+function TimeTag({ start_time, end_time, display }: TimeTagProps) {
+ const time_string =
+ start_time.substring(0, 5) + "-" + end_time.substring(0, 5);
+
+ const duration_minutes = getDurationMinutes(start_time, end_time);
+ const hours = Math.floor(duration_minutes / 60);
+ const minutes = duration_minutes % 60;
+ const duration_string = hours + "h " + minutes + "m";
+
+ let display_string = time_string; // Default to time string
+ if (display != undefined) {
+ display = display.toLowerCase();
+ if (display == "duration") display_string = duration_string;
+ else if (display == "both") display_string += " (" + duration_string + ")";
+ }
+
+ return (
+
+ );
+}
+
+export default TimeTag;
diff --git a/client/src/components/timetable.tsx b/client/src/components/timetable.tsx
new file mode 100644
index 0000000..a4c9207
--- /dev/null
+++ b/client/src/components/timetable.tsx
@@ -0,0 +1,204 @@
+import { ReactElement, useEffect } from "react";
+
+import TimeIndicator, {
+ resizeAndPositionTimeIndicator,
+} from "@/components/time_indicator";
+import {
+ TimetableDayColumn,
+ TimetableTimeColumn,
+} from "@/components/timetable_column";
+import { resizeAndPositionTimetableTasks } from "@/components/timetable_task";
+
+export function getVisibleTimetableRect() {
+ const time_header = document.getElementById("time-header");
+ if (time_header == null) return;
+ const time_header_rect = time_header.getBoundingClientRect();
+
+ const timetable_barrier = document.getElementById("timetable-barrier");
+ if (timetable_barrier == null) return;
+ const timetable_barrier_rect = timetable_barrier.getBoundingClientRect();
+
+ const rect = {
+ left: time_header_rect.right,
+ right: timetable_barrier_rect.left + timetable_barrier.clientWidth,
+ top: time_header_rect.bottom,
+ bottom: timetable_barrier_rect.top + timetable_barrier.clientHeight,
+ width: 0,
+ height: 0,
+ };
+ rect.width = rect.right - rect.left;
+ rect.height = rect.bottom - rect.top;
+
+ return rect;
+}
+
+export function getDurationMinutes(start_time: string, end_time: string) {
+ const start_hour: number = +start_time.substring(0, 2);
+ const start_minute: number = +start_time.substring(3, 5);
+ const end_hour: number = +end_time.substring(0, 2);
+ const end_minute: number = +end_time.substring(3, 5);
+ return 60 * (end_hour - start_hour) + (end_minute - start_minute);
+}
+
+export function getTimeTopPosition(hour: number, mins: number) {
+ const hour_label = document.getElementById(hour + ":00:00");
+ if (hour_label == null) return;
+
+ const hour_rect = hour_label.getBoundingClientRect();
+ if (hour_rect == undefined) return;
+
+ const offset = (mins / 60) * hour_rect.height;
+ const top = hour_rect.top + offset;
+
+ return top;
+}
+
+export function getDayLeftPosition(day: string) {
+ const day_label = document.getElementById(day);
+ if (day_label == null) return;
+ return day_label.getBoundingClientRect().left;
+}
+
+export function resizeTimetableElements() {
+ resizeAndPositionTimetableTasks();
+ resizeAndPositionTimeIndicator();
+}
+
+export function scrollToCurrentTime(align?: string) {
+ const timetable_barrier = document.getElementById("timetable-barrier");
+ if (timetable_barrier == null) return;
+
+ const visible = getVisibleTimetableRect();
+ if (visible == undefined) return;
+
+ const timetable = document.getElementById("timetable");
+ if (timetable == null) return;
+
+ const time_header = document.getElementById("time-header");
+ if (time_header == undefined) return;
+ const time_header_height = time_header.getBoundingClientRect().height;
+ if (time_header_height == undefined) return;
+
+ const scroll_height = timetable.scrollHeight - time_header_height;
+ const scroll_max = scroll_height - visible.height; // Top row is sticky
+
+ timetable_barrier.scrollTop = 0; // Ensures consistent position
+
+ const now = new Date(Date.now());
+ const now_top = getTimeTopPosition(now.getHours(), now.getMinutes());
+ if (now_top == undefined) return;
+
+ let target_top;
+ if (align === "top") target_top = visible.top;
+ else if (align === "bottom") target_top = visible.bottom;
+ else target_top = visible.top + visible.height / 2;
+
+ let scroll_amount = now_top - target_top;
+ if (scroll_amount < 0) scroll_amount = 0;
+ if (scroll_amount > scroll_max) scroll_amount = scroll_max;
+
+ timetable_barrier.scrollTop = scroll_amount;
+}
+
+export function scrollToCurrentDay(align?: string) {
+ const timetable_barrier = document.getElementById("timetable-barrier");
+ if (timetable_barrier == null) return;
+
+ const visible = getVisibleTimetableRect();
+ if (visible == undefined) return;
+
+ const timetable = document.getElementById("timetable");
+ if (timetable == null) return;
+
+ const time_header = document.getElementById("time-header");
+ if (time_header == undefined) return;
+ const time_header_width = time_header.getBoundingClientRect().width;
+ if (time_header_width == undefined) return;
+ const row_width = time_header_width; // easier to read later
+
+ const scroll_width = timetable.scrollWidth - time_header_width; // sticky
+ const scroll_max = scroll_width - visible.width;
+
+ timetable_barrier.scrollLeft = 0;
+
+ enum DateDay {
+ Monday = 1,
+ Tuesday,
+ Wednesday,
+ Thursday,
+ Friday,
+ Saturday,
+ Sunday,
+ }
+
+ const now = new Date(Date.now());
+ const today = DateDay[now.getDay()];
+ const today_left = getDayLeftPosition(today);
+ if (today_left == undefined) return;
+
+ let target_left;
+ if (align == "left") target_left = visible.left;
+ else if (align == "right") target_left = visible.right - row_width;
+ else target_left = visible.left + visible.width / 2 - row_width / 2;
+
+ let scroll_amount = today_left - target_left;
+ if (scroll_amount < 0) scroll_amount = 0;
+ if (scroll_amount > scroll_max) scroll_amount = scroll_max;
+
+ timetable_barrier.scrollLeft = scroll_amount;
+}
+
+export function scrollToCurrentTimeAndDay(
+ time_align?: string,
+ day_align?: string,
+) {
+ scrollToCurrentTime(time_align);
+ scrollToCurrentDay(day_align);
+}
+
+interface TimetableProps {
+ children: ReactElement[];
+}
+
+function Timetable({ children }: TimetableProps) {
+ useEffect(() => {
+ resizeTimetableElements();
+ scrollToCurrentTimeAndDay();
+ });
+
+ return (
+
+ );
+}
+
+export default Timetable;
diff --git a/client/src/components/timetable_column.tsx b/client/src/components/timetable_column.tsx
new file mode 100644
index 0000000..60de809
--- /dev/null
+++ b/client/src/components/timetable_column.tsx
@@ -0,0 +1,101 @@
+import React, { ReactElement } from "react";
+
+import TimetableSlot from "./timetable_slot";
+
+interface TimetableColumnProps {
+ children?: ReactElement[];
+ sticky?: boolean;
+ label: string;
+ day: string;
+}
+
+export default function TimetableColumn({
+ children,
+ day,
+ sticky,
+}: TimetableColumnProps) {
+ let class_name =
+ "timetable-column w-full min-w-48 h-full \
+ flex flex-col bg-slate-500";
+
+ if (sticky === true) {
+ class_name += " sticky left-0 z-10";
+ } else {
+ class_name += " z-0";
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function TimetableDayColumn({ label, day }: TimetableColumnProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function TimetableTimeColumn() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/timetable_slot.tsx b/client/src/components/timetable_slot.tsx
new file mode 100644
index 0000000..8e44e32
--- /dev/null
+++ b/client/src/components/timetable_slot.tsx
@@ -0,0 +1,49 @@
+interface TimetableSlotProps {
+ label?: string;
+ header?: boolean;
+ time_label?: boolean;
+ time: string;
+}
+
+function TimetableSlot({
+ label,
+ header,
+ time_label,
+ time,
+}: TimetableSlotProps) {
+ let class_name =
+ "timetable-slot w-full h-full min-h-16 \
+ flex justify-center items-center border-solid ";
+
+ if (header === true) {
+ class_name =
+ class_name +
+ " bg-slate-800 sticky top-0 z-10 \
+ border-b-2 border-b-slate-900";
+ } else {
+ class_name =
+ class_name +
+ " bg-slate-600 \
+ border-t border-b border-t-slate-400 border-b-slate-400";
+ }
+
+ if (header !== true && time_label !== true) {
+ class_name = class_name + " hover:bg-slate-500";
+ }
+
+ // There's probably a better way to do this
+ if (time_label === true)
+ return (
+
+ );
+ else
+ return (
+
+ );
+}
+
+export default TimetableSlot;
diff --git a/client/src/components/timetable_task.tsx b/client/src/components/timetable_task.tsx
new file mode 100644
index 0000000..14a26e3
--- /dev/null
+++ b/client/src/components/timetable_task.tsx
@@ -0,0 +1,300 @@
+import TimeTag from "@/components/time_tag";
+import { getVisibleTimetableRect } from "@/components/timetable";
+import TopicTag from "@/components/topic_tag";
+
+function getTimetableTaskDataProperties(task: HTMLElement) {
+ const day = task.dataset.day;
+ if (day == undefined) return;
+
+ const hour = task.dataset.start_hour;
+ if (hour == undefined) return;
+
+ const duration_minutes_data = task.dataset.duration_minutes;
+ if (duration_minutes_data == undefined) return;
+ const duration_hours = Number(duration_minutes_data) / 60;
+
+ const start_offset_data = task.dataset.start_offset;
+ if (start_offset_data == undefined) return;
+ const start_offset_hours = Number(start_offset_data) / 60;
+
+ return {
+ day: day,
+ hour: hour,
+ duration_hours: duration_hours,
+ start_offset_hours: start_offset_hours,
+ };
+}
+
+/*
+NOTE: there is a small issue with this code which causes the TimetableTask to
+expand to the size of the column + border while the next column is offscreen
+before returning to just the size of the column (no border) once the next
+column becomes visible.
+
+Pretty sure the issue is because the client rect.right includes the border
+which I don't want it to.
+*/
+export function resizeAndPositionTimetableTask(task: HTMLElement) {
+ // Get task
+ if (task == null) return;
+ // Get task data
+ const task_data = getTimetableTaskDataProperties(task);
+ if (task_data == null) return;
+ const { day, hour, duration_hours, start_offset_hours } = task_data;
+ // Get visible area
+ const visible = getVisibleTimetableRect();
+ if (visible == null) return;
+ // Get column
+ const col = document.getElementById(day);
+ if (col == null) return;
+ // Get row
+ const row = document.getElementById(hour);
+ if (row == null) return;
+ // Get dimensions of column and row
+ const col_rect = col.getBoundingClientRect();
+ const row_rect = row.getBoundingClientRect();
+
+ // Calculate dimensions
+ let left, top, width, height;
+
+ let left_width_reduction = 0;
+ if (col_rect.left > visible.left) {
+ left = col_rect.left;
+ } else {
+ left = visible.left;
+ left_width_reduction = visible.left - col_rect.left;
+ }
+
+ width =
+ col_rect.right < visible.right ? row_rect.width : visible.right - left;
+ width -= left_width_reduction;
+
+ const one_hour_height = row_rect.height;
+ const duration_height = one_hour_height * duration_hours;
+ const offset_top = row_rect.top + one_hour_height * start_offset_hours;
+
+ let top_height_reduction = 0;
+ if (offset_top > visible.top) {
+ top = offset_top;
+ } else {
+ top = visible.top;
+ top_height_reduction = visible.top - offset_top;
+ }
+
+ height =
+ top + duration_height < visible.bottom
+ ? duration_height
+ : visible.bottom - top;
+ height -= top_height_reduction;
+
+ // Set dimensions
+ task.style.left = left + "px";
+ task.style.top = top + "px";
+ task.style.width = width + "px";
+ task.style.height = height + "px";
+
+ // Hide elements not in the visible area
+ const right = left + width;
+ const bottom = top + height;
+ const is_visible = !(
+ right <= visible.left ||
+ left >= visible.right ||
+ bottom <= visible.top ||
+ top >= visible.bottom
+ );
+ if (is_visible) {
+ task.style.display = "inline";
+ } else {
+ task.style.display = "none";
+ }
+}
+
+function resizeAndPositionTimetableTaskTooltip(tooltip: HTMLElement) {
+ const task_id = tooltip.id.slice(0, -"-tooltip".length);
+ const task = document.getElementById(task_id);
+ if (task == null) return;
+
+ const task_rect = task.getBoundingClientRect();
+ if (task_rect == undefined) return;
+
+ tooltip.style.display = "flex";
+ const tooltip_rect = tooltip.getBoundingClientRect();
+ if (tooltip_rect == undefined) return;
+
+ const visible = getVisibleTimetableRect();
+ if (visible == undefined) return;
+
+ if (task_rect.left - visible.left > visible.right - task_rect.right) {
+ tooltip.style.left = task_rect.left - tooltip_rect.width + "px";
+ } else {
+ tooltip.style.left = task_rect.right + "px";
+ }
+
+ const target_pos =
+ task_rect.top + task_rect.height / 2 - tooltip_rect.height / 2;
+
+ let tooltip_top = target_pos;
+ if (target_pos < visible.top) {
+ tooltip_top = visible.top;
+ } else if (target_pos > visible.bottom) {
+ tooltip_top = visible.bottom - tooltip_rect.height;
+ }
+
+ tooltip.style.top = tooltip_top + "px";
+}
+
+export function resizeAndPositionTimetableTasks() {
+ const tasks = document.getElementsByClassName("timetable-task");
+ for (let i = 0; i < tasks.length; i++) {
+ const task = tasks[i];
+ resizeAndPositionTimetableTask(task);
+ }
+}
+
+export interface Topic {
+ id: number;
+ name: string;
+ color_hex: number;
+ user: number;
+}
+
+export interface TimetableTaskContentProps {
+ name: string;
+ start_time: string;
+ end_time: string;
+ topics?: Topic[];
+ description?: string;
+ time_display?: string;
+}
+
+function TimetableTaskContent({
+ name,
+ start_time,
+ end_time,
+ topics,
+ description,
+ time_display,
+}: TimetableTaskContentProps) {
+ return (
+
+
{name}
+
+
+ {topics?.map((topic) => (
+
+ ))}
+
+
+
+ );
+}
+
+export interface TimetableTaskProps {
+ id: string;
+ name: string;
+ topics?: Topic[];
+ description?: string;
+ completed: boolean;
+ day: string;
+ start_time: string;
+ end_time: string;
+ duration_minutes: number;
+ clash?: boolean;
+ tooltip_props: TimetableTaskContentProps[];
+}
+
+function TimetableTask({
+ id,
+ name,
+ topics,
+ description,
+ completed,
+ day,
+ start_time,
+ end_time,
+ duration_minutes,
+ clash,
+ tooltip_props,
+}: TimetableTaskProps) {
+ let additional_style = " ";
+
+ if (completed === true) {
+ additional_style += "opacity-75 ";
+ }
+
+ if (clash === true) {
+ additional_style += "bg-slate-700 hover:bg-slate-500 ";
+ } else {
+ additional_style += "bg-slate-400 hover:bg-slate-300 ";
+ }
+
+ const mouseOverHandler = () => {
+ const tooltip = document.getElementById(id + "-tooltip");
+ if (tooltip == null) return;
+ resizeAndPositionTimetableTaskTooltip(tooltip); // Also sets display
+ };
+
+ const mouseOutHandler = () => {
+ const tooltip = document.getElementById(id + "-tooltip");
+ if (tooltip == null) return;
+ tooltip.style.display = "none";
+ };
+
+ return (
+ <>
+
+
+
+
+ {tooltip_props.map((props) => (
+
+
+
+ ))}
+
+ >
+ );
+}
+
+export default TimetableTask;
diff --git a/client/src/components/topic_tag.tsx b/client/src/components/topic_tag.tsx
new file mode 100644
index 0000000..82901d4
--- /dev/null
+++ b/client/src/components/topic_tag.tsx
@@ -0,0 +1,22 @@
+interface TopicTagProps {
+ name: string;
+ color_hex: number;
+}
+
+function TopicTag({ name, color_hex }: TopicTagProps) {
+ const bg_color = "#" + color_hex.toString(16).padStart(6, "0");
+
+ return (
+
+ );
+}
+
+export default TopicTag;
diff --git a/client/src/pages/[id]/schedule.tsx b/client/src/pages/[id]/schedule.tsx
new file mode 100644
index 0000000..99704b6
--- /dev/null
+++ b/client/src/pages/[id]/schedule.tsx
@@ -0,0 +1,251 @@
+import { useEffect, useState } from "react";
+
+import Timetable, {
+ getDurationMinutes,
+ resizeTimetableElements,
+} from "@/components/timetable";
+import TimetableTask, { TimetableTaskProps } from "@/components/timetable_task";
+
+interface Time {
+ id: number;
+ day: number;
+ start_time: string;
+ end_time: string;
+ repeating: boolean;
+ task: number;
+}
+
+interface Topic {
+ id: number;
+ name: string;
+ color_hex: number;
+ user: number;
+}
+
+interface Task {
+ id: number;
+ name: string;
+ description: string;
+ completed: boolean;
+ user: number;
+ topics: Topic[];
+ times: Time[];
+}
+
+enum Day {
+ Monday = 0,
+ Tuesday,
+ Wednesday,
+ Thursday,
+ Friday,
+ Saturday,
+ Sunday,
+}
+
+function Schedule() {
+ const [timetableTaskProps, setTimetableTaskProps] = useState<
+ TimetableTaskProps[]
+ >([]);
+
+ /* This effect runs in an infinte loop if timetableTaskProps is defined as a
+ dependency */
+ useEffect(() => {
+ function addEventListeners() {
+ window.addEventListener("resize", resizeTimetableElements);
+ }
+
+ function removeEventListeners() {
+ window.removeEventListener("resize", resizeTimetableElements);
+ }
+
+ async function fetchTasks() {
+ const API_URL = "http://localhost:8000/api/planner/task/";
+ try {
+ const response = await fetch(API_URL);
+ if (!response.ok) {
+ throw new Error(`Response status: ${response.status}`);
+ }
+ const data = await response.json();
+ let timetable_task_props =
+ await formatTaskDataToTimetableTaskProps(data);
+ timetable_task_props =
+ await formatTimetableClashes(timetable_task_props);
+ setTimetableTaskProps(timetable_task_props);
+ } catch (error) {
+ console.error(error);
+ }
+ }
+
+ async function formatTaskDataToTimetableTaskProps(data: Task[]) {
+ const timetable_task_props: TimetableTaskProps[] = [];
+ for (let i = 0; i < data.length; i++) {
+ const task = data[i];
+ for (let j = 0; j < task.times.length; j++) {
+ const time = task.times[j];
+ const props = {
+ id: task.id + ":" + time.id,
+ name: task.name,
+ topics: task.topics,
+ description: task.description,
+ completed: task.completed,
+ day: Day[time.day],
+ start_time: time.start_time,
+ end_time: time.end_time,
+ duration_minutes: getDurationMinutes(
+ time.start_time,
+ time.end_time,
+ ),
+ tooltip_props: [
+ {
+ name: task.name,
+ start_time: time.start_time,
+ end_time: time.end_time,
+ topics: task.topics,
+ description: task.description,
+ },
+ ],
+ };
+ timetable_task_props.push(props);
+ }
+ }
+ return timetable_task_props;
+ }
+
+ function timeToMins(time: string) {
+ const hours = Number(time.substring(0, 2));
+ const mins = Number(time.substring(3, 5));
+ return hours * 60 + mins;
+ }
+
+ /*
+ Returns null if no clash exists, otherwise returns clash_props as a
+ TimetableTaskProps and alters the parameters to not clash anymore.
+
+ NOTE: If there is a clash between two tasks that have the same start_time
+ or the same end_time one or both of them will be given a duration of 0.
+ */
+ function checkForTimetableClash(
+ task_a: TimetableTaskProps,
+ task_b: TimetableTaskProps,
+ ): null | TimetableTaskProps {
+ if (task_a.day !== task_b.day) return null;
+ if (task_a.duration_minutes === 0 || task_b.duration_minutes === 0)
+ return null;
+
+ const start_a = timeToMins(task_a.start_time);
+ const end_a = timeToMins(task_a.end_time);
+ const start_b = timeToMins(task_b.start_time);
+ const end_b = timeToMins(task_b.end_time);
+
+ let first_task, second_task;
+ // First start is commented out to avoid a linting error (unused variable)
+ let /*first_start,*/ first_end, second_start, second_end;
+ if (start_a <= start_b) {
+ first_task = task_a;
+ /*first_start = start_a;*/ first_end = end_a;
+ second_task = task_b;
+ second_start = start_b;
+ second_end = end_b;
+ } else {
+ first_task = task_b;
+ /*first_start = start_b;*/ first_end = end_b;
+ second_task = task_a;
+ second_start = start_a;
+ second_end = end_a;
+ }
+
+ if (second_start < first_end) {
+ const clash_props = {
+ id: first_task.id + "/" + second_task.id,
+ name: "Multiple Tasks",
+ topics: [],
+ description: "Hover for more details...",
+ completed: false,
+ day: first_task.day,
+ start_time: second_task.start_time,
+ end_time: first_task.end_time,
+ duration_minutes: 0, // Set just below
+ clash: true,
+ tooltip_props: first_task.tooltip_props.concat(
+ second_task.tooltip_props,
+ ),
+ };
+ clash_props.duration_minutes = getDurationMinutes(
+ clash_props.start_time,
+ clash_props.end_time,
+ );
+
+ /* Second task is contained within first task so is replaced completely
+ by the clash, the first task in then split into 2 using a deep copy */
+ if (second_end < first_end) {
+ second_task = JSON.parse(JSON.stringify(first_task)); // Deep copy
+ second_task.id += ".2"; // Make the ID unique again
+ }
+
+ first_task.end_time = clash_props.start_time;
+ first_task.duration_minutes = getDurationMinutes(
+ first_task.start_time,
+ first_task.end_time,
+ );
+ second_task.start_time = clash_props.end_time;
+ second_task.duration_minutes = getDurationMinutes(
+ second_task.start_time,
+ second_task.end_time,
+ );
+
+ return clash_props;
+ } else {
+ return null;
+ }
+ }
+
+ async function formatTimetableClashes(
+ timetable_task_props: TimetableTaskProps[],
+ ) {
+ for (let i = 0; i < timetable_task_props.length; i++) {
+ const current_prop = timetable_task_props[i];
+
+ for (let j = i + 1; j < timetable_task_props.length; j++) {
+ const compare_prop = timetable_task_props[j];
+
+ const clash_props = checkForTimetableClash(
+ current_prop,
+ compare_prop,
+ );
+ if (clash_props === null) continue;
+
+ timetable_task_props.push(clash_props);
+ }
+ }
+
+ // Remove tasks that are contained within a clash
+ timetable_task_props = timetable_task_props.filter(
+ (props) => props.duration_minutes > 0,
+ );
+ return timetable_task_props;
+ }
+
+ addEventListeners();
+ fetchTasks();
+
+ return () => {
+ removeEventListeners();
+ };
+ }, []);
+ /* This should be dependent on timetableTaskProps however doing so causes it
+ to run in an infinite loop. */
+
+ return (
+
+
+
+ {timetableTaskProps.map((props) => (
+
+ ))}
+
+
+
+ );
+}
+
+export default Schedule;