From 490cc0475ac520c39a00e06cc07aacc42f5fe5c5 Mon Sep 17 00:00:00 2001 From: Teddy Roncin Date: Sun, 31 Mar 2024 18:36:07 +0200 Subject: [PATCH 1/3] feat: new timetable page --- src/app/timetable/page.tsx | 99 +++++++++++++++++++ src/app/timetable/styles.module.scss | 64 ++++++++++++ .../homepageComponents/DailyTimetable.tsx | 66 +------------ .../timetable/TimetableDay.module.scss | 55 +++++++++++ src/components/timetable/TimetableDay.tsx | 81 +++++++++++++++ src/utils/utils.ts | 4 + 6 files changed, 306 insertions(+), 63 deletions(-) create mode 100644 src/app/timetable/page.tsx create mode 100644 src/app/timetable/styles.module.scss create mode 100644 src/components/timetable/TimetableDay.module.scss create mode 100644 src/components/timetable/TimetableDay.tsx create mode 100644 src/utils/utils.ts diff --git a/src/app/timetable/page.tsx b/src/app/timetable/page.tsx new file mode 100644 index 0000000..b61cb4c --- /dev/null +++ b/src/app/timetable/page.tsx @@ -0,0 +1,99 @@ +'use client'; +import styles from './styles.module.scss'; +import { useEffect, useState } from 'react'; +import { GetDailyTimetableResponseDto, TimetableEvent } from '@/api/users/getDailyTimetable'; +import { useAPI } from '@/api/api'; +import { DAY_LENGTH, roundToStartOfDay } from '@/utils/utils'; +import { TimetableDay } from '@/components/timetable/TimetableDay'; +import { format } from 'date-fns'; +import * as locale from 'date-fns/locale'; +import Button from '@/components/UI/Button'; + +export default function TimetablePage() { + const [firstDay, setFirstDay] = useState(roundToStartOfDay(new Date())); + const [numberOfDays, setNumberOfDays] = useState(7); + const [events, setEvents] = useState([] as TimetableEvent[]); + 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]); + + /** + * Called when the selected date is changed. + * Fetches the timetable of the user for the selected date, and update the state. + */ + useEffect(() => { + if (firstDay.getTime() === 0) return; + api + .get( + `/timetable/current/daily/${firstDay.getDate()}/${firstDay.getMonth() + 1}/${firstDay.getFullYear()}`, + ) + .on('success', setEvents); + }, [firstDay, numberOfDays]); + + return ( +
+

Timetable

+
+
+ +

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

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

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

+ events[i])} + day={new Date(firstDay.getTime() + i * DAY_LENGTH)} + className={styles.dayTimetable} + /> +
+ ))} +
+
+ ); +} 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..2d669e6 100644 --- a/src/components/homepageComponents/DailyTimetable.tsx +++ b/src/components/homepageComponents/DailyTimetable.tsx @@ -7,6 +7,7 @@ import { format } from 'date-fns'; import * as locale from 'date-fns/locale'; import Icons from '@/icons'; import Button from '@/components/UI/Button'; +import { TimetableDay } from '@/components/timetable/TimetableDay'; const DAY_LENGTH = 24 * 3_600_000; @@ -17,7 +18,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 +38,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 +55,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..4fb0935 --- /dev/null +++ b/src/components/timetable/TimetableDay.tsx @@ -0,0 +1,81 @@ +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 = '', +}: { + events: TimetableEvent[]; + day: Date; + className?: string; +}) { + 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 +
+ ))} +
+
+ {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..e6b9c79 --- /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; \ No newline at end of file From ec2be2ca9c575b385b6ffbe41291f2eca3561453 Mon Sep 17 00:00:00 2001 From: Teddy Roncin Date: Tue, 2 Apr 2024 00:54:35 +0200 Subject: [PATCH 2/3] feat: starting implementing timetable events creation --- src/api/timetable/createTimetableEvent.ts | 15 +++++++++++++++ src/api/timetable/getGroups.ts | 11 +++++++++++ src/api/timetable/getTimetableEvents.ts | 17 +++++++++++++++++ src/api/timetable/timetable.interface.ts | 8 ++++++++ src/app/timetable/page.tsx | 21 ++++++--------------- src/components/timetable/TimetableDay.tsx | 11 +++++++++-- 6 files changed, 66 insertions(+), 17 deletions(-) create mode 100644 src/api/timetable/createTimetableEvent.ts create mode 100644 src/api/timetable/getGroups.ts create mode 100644 src/api/timetable/getTimetableEvents.ts create mode 100644 src/api/timetable/timetable.interface.ts 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..3231f43 --- /dev/null +++ b/src/api/timetable/getGroups.ts @@ -0,0 +1,11 @@ +import { useAPI } from '@/api/api'; +import { useState } from 'react'; + +export function useGroups() { + const api = useAPI(); + const [groups, setGroups] = useState([]); + /*useEffect(() => { + api.get('/timetable/groups/').on(StatusCodes.OK, setGroups); + }, []);*/ + return groups; +} \ No newline at end of file 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 index b61cb4c..df472d7 100644 --- a/src/app/timetable/page.tsx +++ b/src/app/timetable/page.tsx @@ -1,18 +1,19 @@ 'use client'; import styles from './styles.module.scss'; import { useEffect, useState } from 'react'; -import { GetDailyTimetableResponseDto, TimetableEvent } from '@/api/users/getDailyTimetable'; import { useAPI } from '@/api/api'; import { DAY_LENGTH, roundToStartOfDay } from '@/utils/utils'; import { TimetableDay } from '@/components/timetable/TimetableDay'; import { format } from 'date-fns'; import * as locale from 'date-fns/locale'; 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, setEvents] = useState([] as TimetableEvent[]); + const events = useTimetableEvents(firstDay, numberOfDays); const [eventIndicesPerDay, setEventIndicesPerDay] = useState([] as number[][]); const api = useAPI(); useEffect(() => { @@ -32,19 +33,6 @@ export default function TimetablePage() { setEventIndicesPerDay(newEventsPerDay); }, [events, firstDay, numberOfDays]); - /** - * Called when the selected date is changed. - * Fetches the timetable of the user for the selected date, and update the state. - */ - useEffect(() => { - if (firstDay.getTime() === 0) return; - api - .get( - `/timetable/current/daily/${firstDay.getDate()}/${firstDay.getMonth() + 1}/${firstDay.getFullYear()}`, - ) - .on('success', setEvents); - }, [firstDay, numberOfDays]); - return (

Timetable

@@ -90,6 +78,9 @@ export default function TimetablePage() { events={eventIndices.map((i) => 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') + } />
))} diff --git a/src/components/timetable/TimetableDay.tsx b/src/components/timetable/TimetableDay.tsx index 4fb0935..550446e 100644 --- a/src/components/timetable/TimetableDay.tsx +++ b/src/components/timetable/TimetableDay.tsx @@ -7,10 +7,12 @@ export function TimetableDay({ events, day, className = '', + onClickOnEmptySlot = () => {}, }: { events: TimetableEvent[]; day: Date; className?: string; + onClickOnEmptySlot?: (time: Date) => void; }) { const [columnsCount, setColumnsCount] = useState(0); @@ -58,7 +60,11 @@ export function TimetableDay({
))} -
+
{ + onClickOnEmptySlot(day); + }}> {Array(12) .fill(0) .map((_, i) => ( @@ -73,7 +79,8 @@ export function TimetableDay({ height: `${((event.end.getTime() - event.start.getTime()) / DAY_LENGTH) * 100}%`, left: `${(event.column! / columnsCount) * 100}%`, width: `${100 / columnsCount}%`, - }}>
+ }} + /> ))}
From d88ed3db9cf447921e67476931c6898d4c835b03 Mon Sep 17 00:00:00 2001 From: Teddy Roncin Date: Fri, 3 May 2024 02:29:57 +0200 Subject: [PATCH 3/3] fix: started review --- src/api/timetable/getGroups.ts | 11 ++++++----- src/app/timetable/page.tsx | 17 +++++------------ .../homepageComponents/DailyTimetable.tsx | 3 +-- src/utils/utils.ts | 2 +- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/api/timetable/getGroups.ts b/src/api/timetable/getGroups.ts index 3231f43..74d670f 100644 --- a/src/api/timetable/getGroups.ts +++ b/src/api/timetable/getGroups.ts @@ -1,11 +1,12 @@ import { useAPI } from '@/api/api'; -import { useState } from 'react'; +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/groups/').on(StatusCodes.OK, setGroups); - }, []);*/ + useEffect(() => { + api.get('/timetable/current/groups/').on(StatusCodes.OK, setGroups); + }, []); return groups; -} \ No newline at end of file +} diff --git a/src/app/timetable/page.tsx b/src/app/timetable/page.tsx index df472d7..05a1b81 100644 --- a/src/app/timetable/page.tsx +++ b/src/app/timetable/page.tsx @@ -5,7 +5,6 @@ import { useAPI } from '@/api/api'; import { DAY_LENGTH, roundToStartOfDay } from '@/utils/utils'; import { TimetableDay } from '@/components/timetable/TimetableDay'; import { format } from 'date-fns'; -import * as locale from 'date-fns/locale'; import Button from '@/components/UI/Button'; import { useTimetableEvents } from '@/api/timetable/getTimetableEvents'; import { createTimetableEvent } from '@/api/timetable/createTimetableEvent'; @@ -42,13 +41,9 @@ export default function TimetablePage() { {'<'}

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