Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new timetable page #12

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/api/timetable/createTimetableEvent.ts
Original file line number Diff line number Diff line change
@@ -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<TimetableCreateEntryRequestDto, Omit<TimetableEvent, 'column' | 'id'>>('/timetable/current', {
firstRepetitionDate: start,
repetitions: 1,
duration: end.getTime() - start.getTime(),
groups: [],
location,
})
.toPromise();
}
12 changes: 12 additions & 0 deletions src/api/timetable/getGroups.ts
Original file line number Diff line number Diff line change
@@ -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<string[]>([]);
useEffect(() => {
api.get<string[]>('/timetable/current/groups/').on(StatusCodes.OK, setGroups);
}, []);
return groups;
}
17 changes: 17 additions & 0 deletions src/api/timetable/getTimetableEvents.ts
Original file line number Diff line number Diff line change
@@ -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<TimetableEvent[]>([]);
useEffect(() => {
api
.get<
TimetableEvent[]
>(`/timetable/current/${numberOfDays}/${firstDay.getDate()}/${firstDay.getMonth() + 1}/${firstDay.getFullYear()}/`)
.on(StatusCodes.OK, setEvents);
}, [firstDay, numberOfDays]);
return events;
}
8 changes: 8 additions & 0 deletions src/api/timetable/timetable.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface TimetableCreateEntryRequestDto {
location: string;
duration: number;
firstRepetitionDate: Date;
repetitionFrequency?: number;
repetitions?: number;
groups: string[];
}
83 changes: 83 additions & 0 deletions src/app/timetable/page.tsx
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Peut être plus simple à écrire comme ça ?

Suggested change
const offset = numberOfDays === 7 ? -(firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1) : 0;
const offset = numberOfDays === 7 ? -((firstDay.getDay() + 5) % 6) : 0;

Ps : peut être commenter et préciser que le hook sert à se recaler sur le début d'une semaine ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je sais pas si c'est plus lisible tho

setFirstDay(new Date(firstDay.getTime() + offset * DAY_LENGTH)); // I verified, there is no problem with changing the clocks :)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logique que ça change pas si tu changes l'horloge (puisqu'on travaille avec des timestamps - donc relatifs au début de l'époque Unix en UTC)

Le changement de fuseau horaire pourrait poser des soucis mais pas à cet endroit là : imagine tu regardes un emploi du temps avec des heures françaises alors que t'es sur un autre fuseau horaire, qu'est ce que ça donne ?

En l'occurrence (j'ai pas vérifié, je dis ça en lisant le code), vu que firstDay est défini par rapport au fuseau horaire de l'utilisateur, les évènements des emplois du temps vont se décaler au niveau des jours et des heures en fonction du décalage horaire pour correspondre aux jours et heures du fuseau horaire du pc

Je ne sais pas si ce qui est pertinent entre le fait de s'adapter au fuseau horaire local ou de proposer un edt "fixe" :

  • d'une manière si t'es à l'étranger et que tu regardes l'edt de quelqu'un ça peut être sympa de voir directement "ah le lundi t'as SY06, IF36, LO02 et HT44" et pas "Wait y a SY06 et IF36 le dimanche soir et LO02 et HT44 tôt le matin le lundi"
  • après si tu veux voir quand tu peux appeler la personne par exemple, ajuster les horaires en fonction du fuseau horaire local est plus que pertinent

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

De mémoire j'avais eu un ptit traumatisme avec les fuseaux horaires dans cette PR >.<

Je pense que ce point ça peut être un truc pour plus tard, je pense que c'est le genre de détail sur lequel faut pas forcément s'attarder sinon on aura jamais fini (on pourra toujours faire mieux). Par contre ouais, en vrai ça peut être sympa de tenir une liste. Flemme de le faire ce soir tho, je le fais demain si j'y pense

}, [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 (
<div className={styles.timetablePage}>
<h1>Timetable</h1>
<div className={styles.settings}>
<div className={styles.range}>
<Button noStyle onClick={() => setFirstDay(new Date(firstDay.getTime() - DAY_LENGTH * numberOfDays))}>
{'<'}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

En vrai pourquoi ne pas utiliser une icône LeftChevron ? (idem symétriquement pour RightChevron)

</Button>
<p>
{format(firstDay, `d MMMM yyyy`)}{' '}
au{' '}
{format(new Date(firstDay.getTime() + (numberOfDays - 1) * DAY_LENGTH), `d MMMM yyyy`)}
</p>
<Button noStyle onClick={() => setFirstDay(new Date(firstDay.getTime() + DAY_LENGTH * numberOfDays))}>
{'>'}
</Button>
</div>
<div className={styles.rangeLength}>
<Button noStyle onClick={() => setNumberOfDays(1)}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Le style est ptet à revoir je pense 😅
D'ailleiurs puisque je suis au niveau des boutons, ce serait nice de mettre un bouton pour revenir au jour actuel

1 jour
</Button>
<Button noStyle onClick={() => setNumberOfDays(3)}>
3 jours
</Button>
<Button noStyle onClick={() => setNumberOfDays(7)}>
7 jours
</Button>
</div>
</div>
<div className={styles.timetable}>
{eventIndicesPerDay.map((eventIndices, i) => (
<div key={i} className={styles.day}>
<h2 className={styles.dayTitle}>
{format(new Date(firstDay.getTime() + i * DAY_LENGTH), `ccc d`)}
</h2>
<TimetableDay
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', []) // TODO : It's missing an interface to fill-in the info woopsyyy
}
/>
</div>
))}
</div>
</div>
);
}
64 changes: 64 additions & 0 deletions src/app/timetable/styles.module.scss
Original file line number Diff line number Diff line change
@@ -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%;
}
}
}
}
69 changes: 4 additions & 65 deletions src/components/homepageComponents/DailyTimetable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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(() => {
Expand All @@ -38,41 +37,9 @@ export default function DailyTimetable() {
.get<GetDailyTimetableResponseDto>(
`/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 (
<div className={styles.dailyTimetable}>
<h2>EDT JOURNALIER</h2>
Expand All @@ -87,35 +54,7 @@ export default function DailyTimetable() {
<Icons.RightArrow />
</Button>
</div>
<div className={styles.timetable}>
<div className={styles.hours}>
{Array(12)
.fill(0)
.map((_, i) => (
<div key={i}>
<span>{i * 2}h</span>
</div>
))}
</div>
<div className={styles.events}>
{Array(12)
.fill(0)
.map((_, i) => (
<div key={i} className={styles.timeSeparator} />
))}
{timetable.map((event) => (
<div
key={event.id}
className={styles.event}
style={{
top: `${((event.start.getTime() - selectedDate.getTime()) / DAY_LENGTH) * 100}%`,
height: `${((event.end.getTime() - event.start.getTime()) / DAY_LENGTH) * 100}%`,
left: `${(event.column! / columnsCount) * 100}%`,
width: `${100 / columnsCount}%`,
}}></div>
))}
</div>
</div>
<TimetableDay className={styles.timetable} day={selectedDate} events={timetable} />
</div>
);
}
55 changes: 55 additions & 0 deletions src/components/timetable/TimetableDay.module.scss
Original file line number Diff line number Diff line change
@@ -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%;
}
}
}
Loading