diff --git a/src/api/timetable/createTimetableEvent.ts b/src/api/timetable/createTimetableEvent.ts new file mode 100644 index 0000000..3104b70 --- /dev/null +++ b/src/api/timetable/createTimetableEvent.ts @@ -0,0 +1,15 @@ +import { API } from '@/api/api'; +import { TimetableEvent } from '@/api/users/getDailyTimetable'; +import { TimetableCreateEntryRequestDto } from '@/api/timetable/timetable.interface'; + +export function createTimetableEvent(api: API, start: Date, end: Date, location: string, groups: string[]) { + return api + .post>('/timetable/current', { + firstRepetitionDate: start, + repetitions: 1, + duration: end.getTime() - start.getTime(), + groups: [], + location, + }) + .toPromise(); +} diff --git a/src/api/timetable/getGroups.ts b/src/api/timetable/getGroups.ts new file mode 100644 index 0000000..74d670f --- /dev/null +++ b/src/api/timetable/getGroups.ts @@ -0,0 +1,12 @@ +import { useAPI } from '@/api/api'; +import { useEffect, useState } from 'react'; +import { StatusCodes } from 'http-status-codes'; + +export function useGroups() { + const api = useAPI(); + const [groups, setGroups] = useState([]); + useEffect(() => { + api.get('/timetable/current/groups/').on(StatusCodes.OK, setGroups); + }, []); + return groups; +} diff --git a/src/api/timetable/getTimetableEvents.ts b/src/api/timetable/getTimetableEvents.ts new file mode 100644 index 0000000..29d6010 --- /dev/null +++ b/src/api/timetable/getTimetableEvents.ts @@ -0,0 +1,17 @@ +import { useAPI } from '@/api/api'; +import { StatusCodes } from 'http-status-codes'; +import { useEffect, useState } from 'react'; +import { TimetableEvent } from '@/api/users/getDailyTimetable'; + +export function useTimetableEvents(firstDay: Date, numberOfDays: number): TimetableEvent[] { + const api = useAPI(); + const [events, setEvents] = useState([]); + useEffect(() => { + api + .get< + TimetableEvent[] + >(`/timetable/current/${numberOfDays}/${firstDay.getDate()}/${firstDay.getMonth() + 1}/${firstDay.getFullYear()}/`) + .on(StatusCodes.OK, setEvents); + }, [firstDay, numberOfDays]); + return events; +} diff --git a/src/api/timetable/timetable.interface.ts b/src/api/timetable/timetable.interface.ts new file mode 100644 index 0000000..2024d7d --- /dev/null +++ b/src/api/timetable/timetable.interface.ts @@ -0,0 +1,8 @@ +export interface TimetableCreateEntryRequestDto { + location: string; + duration: number; + firstRepetitionDate: Date; + repetitionFrequency?: number; + repetitions?: number; + groups: string[]; +} diff --git a/src/app/timetable/page.tsx b/src/app/timetable/page.tsx new file mode 100644 index 0000000..05a1b81 --- /dev/null +++ b/src/app/timetable/page.tsx @@ -0,0 +1,83 @@ +'use client'; +import styles from './styles.module.scss'; +import { useEffect, useState } from 'react'; +import { useAPI } from '@/api/api'; +import { DAY_LENGTH, roundToStartOfDay } from '@/utils/utils'; +import { TimetableDay } from '@/components/timetable/TimetableDay'; +import { format } from 'date-fns'; +import Button from '@/components/UI/Button'; +import { useTimetableEvents } from '@/api/timetable/getTimetableEvents'; +import { createTimetableEvent } from '@/api/timetable/createTimetableEvent'; + +export default function TimetablePage() { + const [firstDay, setFirstDay] = useState(roundToStartOfDay(new Date())); + const [numberOfDays, setNumberOfDays] = useState(7); + const events = useTimetableEvents(firstDay, numberOfDays); + const [eventIndicesPerDay, setEventIndicesPerDay] = useState([] as number[][]); + const api = useAPI(); + useEffect(() => { + const offset = numberOfDays === 7 ? -(firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1) : 0; + setFirstDay(new Date(firstDay.getTime() + offset * DAY_LENGTH)); // I verified, there is no problem with changing the clocks :) + }, [numberOfDays]); + useEffect(() => { + const newEventsPerDay = new Array(numberOfDays).fill(undefined).map(() => []) as number[][]; + for (let i = 0; i < events.length; i++) { + const event = events[i]; + const startDayIndex = Math.floor((roundToStartOfDay(event.start).getTime() - firstDay.getTime()) / DAY_LENGTH); + const endDayIndex = Math.floor((roundToStartOfDay(event.end).getTime() - firstDay.getTime()) / DAY_LENGTH); + for (let j = startDayIndex; j < endDayIndex; j++) { + newEventsPerDay[j].push(i); + } + } + setEventIndicesPerDay(newEventsPerDay); + }, [events, firstDay, numberOfDays]); + + return ( +
+

Timetable

+
+
+ +

+ {format(firstDay, `d MMMM yyyy`)}{' '} + au{' '} + {format(new Date(firstDay.getTime() + (numberOfDays - 1) * DAY_LENGTH), `d MMMM yyyy`)} +

+ +
+
+ + + +
+
+
+ {eventIndicesPerDay.map((eventIndices, i) => ( +
+

+ {format(new Date(firstDay.getTime() + i * DAY_LENGTH), `ccc d`)} +

+ events[i])} + day={new Date(firstDay.getTime() + i * DAY_LENGTH)} + className={styles.dayTimetable} + onClickOnEmptySlot={ + (time) => createTimetableEvent(api, time, new Date(time.getTime() + 3_600_000), 'a random place', []) // TODO : It's missing an interface to fill-in the info woopsyyy + } + /> +
+ ))} +
+
+ ); +} diff --git a/src/app/timetable/styles.module.scss b/src/app/timetable/styles.module.scss new file mode 100644 index 0000000..f53b495 --- /dev/null +++ b/src/app/timetable/styles.module.scss @@ -0,0 +1,64 @@ +.timetablePage { + height: 100%; + display: flex; + flex-direction: column; + gap: 2rem; + + .settings { + display: flex; + justify-content: space-between; + align-items: center; + margin-left: 3rem; + margin-right: 1.5rem; + + .rangeLength { + > button { + margin: 0 10px; + &:first-child { + margin-left: 0; + } + &:last-child { + margin-right: 0; + } + } + } + + .range { + display: flex; + justify-content: space-between; + align-items: center; + width: 40%; + align-self: center; + + & > p { + margin: auto 0; + font-size: 1.2rem; + } + + & > button { + font-size: 2rem; + } + } + } + + .timetable { + display: flex; + bottom: 0; + position: relative; + height: auto; + gap: 10px; + flex-grow: 1; + + .day { + flex-grow: 1; + + .dayTitle { + text-align: center; + } + + .dayTimetable { + height: 100%; + } + } + } +} \ No newline at end of file diff --git a/src/components/homepageComponents/DailyTimetable.tsx b/src/components/homepageComponents/DailyTimetable.tsx index 16f9f98..bdeefc8 100644 --- a/src/components/homepageComponents/DailyTimetable.tsx +++ b/src/components/homepageComponents/DailyTimetable.tsx @@ -7,8 +7,8 @@ import { format } from 'date-fns'; import * as locale from 'date-fns/locale'; import Icons from '@/icons'; import Button from '@/components/UI/Button'; - -const DAY_LENGTH = 24 * 3_600_000; +import { TimetableDay } from '@/components/timetable/TimetableDay'; +import { DAY_LENGTH } from '@/utils/utils'; /** * Renders a one-day timetable. @@ -17,7 +17,6 @@ const DAY_LENGTH = 24 * 3_600_000; export default function DailyTimetable() { const [timetable, setTimetable] = useState([] as TimetableEvent[]); const [selectedDate, setSelectedDate] = useState(new Date(0)); - const [columnsCount, setColumnsCount] = useState(0); const api = useAPI(); useEffect(() => { @@ -38,41 +37,9 @@ export default function DailyTimetable() { .get( `/timetable/current/daily/${selectedDate.getDate()}/${selectedDate.getMonth() + 1}/${selectedDate.getFullYear()}`, ) - .on('success', (body) => { - const columnsCount = formatTimetable(body); - setTimetable(body); - setColumnsCount(columnsCount); - }); + .on('success', setTimetable); }, [selectedDate]); - /** - * Clamps every event of the timetable in the current day, and assign a column number to each event. - * The operation is done in place. - * @param timetable The timetable to format. - * @returns The number of columns that are needed. - */ - const formatTimetable = (timetable: TimetableEvent[]): number => { - const endOfSelectedDate = new Date(selectedDate.getTime() + DAY_LENGTH); - const columnsFreeFrom: Date[] = []; - for (const event of timetable) { - if (event.start < selectedDate) { - event.start = new Date(selectedDate); - } - if (event.end > endOfSelectedDate) { - event.end = new Date(endOfSelectedDate); - } - const columnIndex = columnsFreeFrom.findIndex((column) => column < event.start); - if (columnIndex === -1) { - columnsFreeFrom.push(event.end); - event.column = columnsFreeFrom.length - 1; - } else { - columnsFreeFrom[columnIndex] = event.end; - event.column = columnIndex; - } - } - return columnsFreeFrom.length; - }; - return (

EDT JOURNALIER

@@ -87,35 +54,7 @@ export default function DailyTimetable() {
-
-
- {Array(12) - .fill(0) - .map((_, i) => ( -
- {i * 2}h -
- ))} -
-
- {Array(12) - .fill(0) - .map((_, i) => ( -
- ))} - {timetable.map((event) => ( -
- ))} -
-
+
); } diff --git a/src/components/timetable/TimetableDay.module.scss b/src/components/timetable/TimetableDay.module.scss new file mode 100644 index 0000000..9b273a2 --- /dev/null +++ b/src/components/timetable/TimetableDay.module.scss @@ -0,0 +1,55 @@ +@import '@/variables.scss'; + +.timetableDay { + flex-grow: 1; + display: flex; + gap: 10px; + + .hours { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + overflow-y: hidden; // The page would become scrollable for 1 pixel, probably due to a computing imprecision somewhere in the interpretation of the CSS. + + > div { + flex-grow: 1; + text-align: center; + display: flex; + align-items: center; + justify-content: flex-end; + } + } + + .events { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + position: relative; + flex-grow: 1; + overflow-y: hidden; // The page would become scrollable for 1 pixel, probably due to a computing imprecision somewhere in the interpretation of the CSS. + + .timeSeparator { + width: 100%; + flex-grow: 1; + position: relative; + + &:after { + content: ''; + position: absolute; + width: 100%; + height: 1px; + background-color: $ung-dark-grey; + top: calc(50% - 1px); + left: 0; + } + } + + .event { + position: absolute; + background-color: red; + width: 100%; + } + } +} \ No newline at end of file diff --git a/src/components/timetable/TimetableDay.tsx b/src/components/timetable/TimetableDay.tsx new file mode 100644 index 0000000..550446e --- /dev/null +++ b/src/components/timetable/TimetableDay.tsx @@ -0,0 +1,88 @@ +import styles from './TimetableDay.module.scss'; +import { TimetableEvent } from '@/api/users/getDailyTimetable'; +import { useEffect, useState } from 'react'; +import { DAY_LENGTH } from '@/utils/utils'; + +export function TimetableDay({ + events, + day, + className = '', + onClickOnEmptySlot = () => {}, +}: { + events: TimetableEvent[]; + day: Date; + className?: string; + onClickOnEmptySlot?: (time: Date) => void; +}) { + const [columnsCount, setColumnsCount] = useState(0); + + useEffect(() => { + setColumnsCount(formatTimetable(events)); + }, []); + + /** + * Clamps every event of the timetable in the current day, and assign a column number to each event. + * The operation is done in place. + * @param timetable The timetable to format. + * @returns The number of columns that are needed. + */ + const formatTimetable = (timetable: TimetableEvent[]): number => { + const endOfSelectedDate = new Date(day.getTime() + DAY_LENGTH); + // For each column, the array contains the next time the column will be free and able to receive an event. + const columnsFreeFrom: Date[] = []; + for (const event of timetable) { + if (event.start < day) { + event.start = new Date(day); + } + if (event.end > endOfSelectedDate) { + event.end = new Date(endOfSelectedDate); + } + const columnIndex = columnsFreeFrom.findIndex((column) => column < event.start); + if (columnIndex === -1) { + columnsFreeFrom.push(event.end); + event.column = columnsFreeFrom.length - 1; + } else { + columnsFreeFrom[columnIndex] = event.end; + event.column = columnIndex; + } + } + return columnsFreeFrom.length; + }; + + return ( +
+
+ {Array(12) + .fill(0) + .map((_, i) => ( +
+ {i * 2}h +
+ ))} +
+
{ + onClickOnEmptySlot(day); + }}> + {Array(12) + .fill(0) + .map((_, i) => ( +
+ ))} + {events.map((event) => ( +
+ ))} +
+
+ ); +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..7731bbd --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,4 @@ +export function roundToStartOfDay(date: Date) { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); +} +export const DAY_LENGTH = 24 * 3_600_000;