Skip to content

Commit

Permalink
✨(frontend) add meeting register form
Browse files Browse the repository at this point in the history
add a form which enables the user to register the meeting. On success,
the meeting is saved to local storage. Since changes to local storage do
not cause react to rerender anything, you need to refresh the page for
the meeting to appear in the meeting list view.
  • Loading branch information
karabij committed Dec 16, 2022
1 parent 36f2ebd commit 23fc6c1
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 66 deletions.
10 changes: 8 additions & 2 deletions src/frontend/demo/src/views/meetings/list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import * as React from 'react';
import { DefaultPage } from '../../../components/DefaultPage';

export function MeetingsListView() {
const myMeetings: string | null = localStorage.getItem('meetings');
const myMeetingsList: any = myMeetings ? JSON.parse(myMeetings) : [];
const mySortedMeetingsList = myMeetingsList.sort(
(a: any, b: any) => new Date(a.startDateTime).getTime() - new Date(b.startDateTime).getTime(),
);

return (
<DefaultPage title={'test'}>
<MyMeetings meetings={[createRandomMeeting(), createRandomMeeting()]} />
<DefaultPage title={'Meetings'}>
<MyMeetings meetings={mySortedMeetingsList} />
</DefaultPage>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import React, { FunctionComponent, useState } from 'react';
import { useIntl } from 'react-intl';
import TimePicker, { TimePickerValue } from 'react-time-picker';
import SuggestionButton from './SuggestionButton';
import { mergeDateTime } from './utils';

export interface formikDateTimePickerProps {
dateName: string;
Expand Down Expand Up @@ -58,8 +59,12 @@ const FormikDateTimePicker: FunctionComponent<formikDateTimePickerProps> = ({ ..
if (timeField.value === undefined || timeField.value.length === 0) {
return;
}
const chosen = mergeDateTime(dateField.value, timeField.value);
if (chosen) {
console.log(`chosenDateTime in UTC format: ${DateTime.fromISO(chosen).toUTC().toISO()}`);
}
formikContext.setFieldTouched(props.timeName, true);
}, [timeField.value]);
}, [timeField.value, dateField.value]);

const suggestionButtons = props.localTimeSuggestions.map((value: string, index: number) => (
<SuggestionButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ const SuggestionButton: FunctionComponent<suggestionButtonProps> = ({ ...props }
primary={isChosenButton}
onClick={() => {
props.onClick(props.frenchButtonValue);
console.log(`chosenDateTime : ${chosenDateTime}`);
}}
>
<Box alignContent="center" alignSelf="center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ export const splitDateTime = (dateTimeISO: string | null): { date: string; time:
};

export const mergeDateTime = (
dateString: string | null,
timeString: string | null,
): string | null => {
dateString: string | undefined,
timeString: string | undefined,
): string | undefined => {
if (!dateString || !timeString) {
return null;
return undefined;
}
try {
const time = Duration.fromISOTime(timeString);
Expand All @@ -26,6 +26,6 @@ export const mergeDateTime = (
});
return dateTime.toISO();
} catch (e) {
return null;
return undefined;
}
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { defineMessage } from '@formatjs/intl';
import { Box, Button, ButtonExtendedProps, Card, Menu, Notification, Spinner, Text } from 'grommet';
import { MoreVertical } from 'grommet-icons';
import { Interval } from 'luxon';
import React from 'react';
import { useIntl } from 'react-intl';

import { useIsSmallSize } from '../../../hooks/useIsMobile';
import { Meeting } from '../../../types/entities/meeting';
import { Room } from '../../../types/entities/room';

export interface MeetingRowProps {
meeting: Meeting;
Expand All @@ -24,24 +24,23 @@ export default function MeetingRow({ meeting }: MeetingRowProps) {
const intl = useIntl();
const isSmallSize = useIsSmallSize();
const menuItems: ButtonExtendedProps[] = [];
const startDateTime: Date = new Date(meeting.startDateTime);
const endDateTime: Date = new Date(meeting.endDateTime);

const convertToHourMinutesFormat = (numberMinutes: number): string => {
const nbHours = Math.floor(numberMinutes / 60);
const nbMinutes = numberMinutes - 60 * nbHours;
return nbHours > 0 ? `${nbHours} h ${nbMinutes} min` : `${nbMinutes} min`;
};
const meetingDay = startDateTime.toLocaleDateString(intl.locale, {
year: '2-digit',
month: '2-digit',
day: '2-digit',
});

const zeroFormatNumber = (number: number): string => {
return number < 10 ? '0' + number.toString() : number.toString();
};
const meetingHour = startDateTime.toLocaleTimeString(intl.locale, {
timeStyle: 'short',
});

const meetingDay = `${zeroFormatNumber(meeting.startDateTime.getDay())}/${zeroFormatNumber(
meeting.startDateTime.getMonth() + 1,
)}/${meeting.startDateTime.getFullYear()}`;

const meetingHour = `${zeroFormatNumber(meeting.startDateTime.getHours())}:${zeroFormatNumber(
meeting.startDateTime.getMinutes(),
)}`;
const expectedDuration = Interval.fromDateTimes(startDateTime, endDateTime).toDuration([
'hours',
'minutes',
]);

return (
<Card background="light-2" elevation="0" pad="small" style={{ position: 'relative' }}>
Expand Down Expand Up @@ -73,7 +72,7 @@ export default function MeetingRow({ meeting }: MeetingRowProps) {
{meetingHour}
</Text>
<Text color="brand" size="small">
{convertToHourMinutesFormat(meeting.expectedDuration)}
{`${expectedDuration.hours}h ${Math.floor(expectedDuration.minutes)}min`}
</Text>
</Box>
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { ComponentMeta, ComponentStory } from '@storybook/react';
import React from 'react';
import { createRandomMeeting } from '../../../factories/meetings';
import { Meeting } from '../../../types';
import { MyMeetings } from './MyMeetings';

export default {
title: 'Meetings/MyMeetings',
component: MyMeetings,
} as ComponentMeta<typeof MyMeetings>;

const myMeetings: string | null = localStorage.getItem('meetings');
const myMeetingsList: Meeting[] = myMeetings ? JSON.parse(myMeetings) : [];
const mySortedMeetingsList = myMeetingsList.sort(
(a, b) => new Date(a.startDateTime).getTime() - new Date(b.startDateTime).getTime(),
);

const Template: ComponentStory<typeof MyMeetings> = (args) => (
<MyMeetings meetings={[createRandomMeeting(), createRandomMeeting()]} />
<MyMeetings meetings={mySortedMeetingsList} />
);

// create the template and stories
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { faker } from '@faker-js/faker';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { Form, Formik, FormikHelpers } from 'formik';
import { ErrorMessage, Form, Formik, FormikHelpers } from 'formik';
import { Box } from 'grommet';
import { DateTime, Settings } from 'luxon';
import React, { useMemo } from 'react';
Expand Down Expand Up @@ -33,6 +34,16 @@ const messages = defineMessages({
defaultMessage: 'Register meeting',
description: 'Label for the submit button to register a new meeting',
},
startTimeError: {
defaultMessage: 'Start time error. ',
description: 'Error message when starting time and date are invalid',
id: 'components.design-system.Formik.FormikDateTimePicker.startTimeError',
},
endTimeError: {
defaultMessage: 'End time error. ',
description: 'Error message when ending time and date are invalid',
id: 'components.design-system.Formik.FormikDateTimePicker.endTimeError',
},
invalidTime: {
defaultMessage:
'Starting time should be in the future and ending time should be after starting time.',
Expand Down Expand Up @@ -68,9 +79,8 @@ const RegisterMeetingForm = ({ onSuccess }: RegisterMeetingFormProps) => {
const startTimeTestOptions: Yup.TestConfig<string | undefined> = {
name: 'startDateTimeIsAfterOrNow',
test: function (startTimeValue: string | undefined) {
const nullableStartTimeValue = startTimeValue == undefined ? null : startTimeValue;
const chosenStartDateTime = this.parent
? mergeDateTime(this.parent.startDate, nullableStartTimeValue)
? mergeDateTime(this.parent.startDate, startTimeValue)
: null;
const chosenEndDateTime = this.parent
? mergeDateTime(this.parent.endDate, this.parent.endTime)
Expand All @@ -88,12 +98,11 @@ const RegisterMeetingForm = ({ onSuccess }: RegisterMeetingFormProps) => {
const endTimeTestOptions: Yup.TestConfig<string | undefined> = {
name: 'endDateTimeIsAfterOrStartDate',
test: function (endTimeValue: string | undefined) {
const nullableEndTimeValue = endTimeValue == undefined ? null : endTimeValue;
const chosenStartDateTime = this.parent
? mergeDateTime(this.parent.startDate, this.parent.startTime)
: null;
const chosenEndDateTime = this.parent
? mergeDateTime(this.parent.endDate, nullableEndTimeValue)
? mergeDateTime(this.parent.endDate, endTimeValue)
: null;
return (
chosenEndDateTime == null ||
Expand All @@ -112,19 +121,19 @@ const RegisterMeetingForm = ({ onSuccess }: RegisterMeetingFormProps) => {
startTime: Yup.string().required().test(startTimeTestOptions),
endTime: Yup.string().required().test(endTimeTestOptions),
});
const queryClient = useQueryClient();

const mutation = useMutation<Meeting, AxiosError, RegisterMeetingFormValues>(
MeetingsRepository.create,
{
onSuccess: (newMeeting) => {
queryClient.setQueryData([MagnifyQueryKeys.MEETINGS], (meetings: Meeting[] = []) => {
return [...meetings, newMeeting];
});
onSuccess(newMeeting);
},
},
);
// const queryClient = useQueryClient();

// const mutation = useMutation<Meeting, AxiosError, RegisterMeetingFormValues>(
// MeetingsRepository.create,
// {
// onSuccess: (newMeeting) => {
// queryClient.setQueryData([MagnifyQueryKeys.MEETINGS], (meetings: Meeting[] = []) => {
// return [...meetings, newMeeting];
// });
// onSuccess(newMeeting);
// },
// },
// );

const allSuggestions = getSuggestions(intl.locale);
const allFrenchSuggestions = getSuggestions('fr');
Expand All @@ -144,14 +153,43 @@ const RegisterMeetingForm = ({ onSuccess }: RegisterMeetingFormProps) => {
values: RegisterMeetingFormValues,
actions: FormikHelpers<RegisterMeetingFormValues>,
) => {
mutation.mutate(values, {
onError: (error) => {
const formErrors = error?.response?.data as Maybe<FormErrors>;
if (formErrors?.slug) {
actions.setFieldError('name', formErrors.slug.join(','));
}
},
});
// mutation.mutate(values, {
// onError: (error) => {
// const formErrors = error?.response?.data as Maybe<FormErrors>;
// if (formErrors?.slug) {
// actions.setFieldError('name', formErrors.slug.join(','));
// }
// },
// });
try {
const oldMeetings: string | null = localStorage.getItem('meetings');
const id = faker.datatype.uuid();
const startDateTime = mergeDateTime(values.startDate, values.startTime);
const endDateTime = mergeDateTime(values.endDate, values.endTime);

const newMeeting: Meeting = {
id: id,
name: values.name,
startDateTime: startDateTime
? DateTime.fromISO(startDateTime).toUTC().toISO()
: DateTime.now().toUTC().toISO(),
endDateTime: endDateTime
? DateTime.fromISO(endDateTime).toUTC().toISO()
: DateTime.now().toUTC().toISO(),
jitsi: {
room: `${id}`,
token: `${faker.datatype.number({ min: 0, max: 1000 })}`,
},
};
if (oldMeetings) {
localStorage.setItem('meetings', JSON.stringify([...JSON.parse(oldMeetings), newMeeting]));
} else {
localStorage.setItem('meetings', JSON.stringify([newMeeting]));
}
onSuccess(newMeeting);
} catch (error) {
console.log(error);
}
};

return (
Expand Down Expand Up @@ -186,15 +224,22 @@ const RegisterMeetingForm = ({ onSuccess }: RegisterMeetingFormProps) => {
</Box>
</Box>

{errors.startDate && touched.startDate ? <div>{errors.startDate}</div> : null}
{touched.endDate && errors.endDate ? <div>{errors.endDate}</div> : null}
{touched.startTime && errors.startTime ? <div>{errors.startTime}</div> : null}
{touched.endTime && errors.endTime ? <div>{errors.endTime}</div> : null}

<FormikSubmitButton
isLoading={mutation.isLoading}
// isLoading={mutation.isLoading}
label={intl.formatMessage(messages.registerMeetingSubmitLabel)}
/>
{errors.startTime && touched.startTime ? (
<Box background="#FFCCCB" margin={{ vertical: 'small' }} pad="small" round="small">
{intl.formatMessage(messages.startTimeError)}
<ErrorMessage name="startTime" />
</Box>
) : null}
{errors.endTime && touched.endTime ? (
<Box background="#FFCCCB" margin="small" pad="small" round="small">
{intl.formatMessage(messages.endTimeError)}
<ErrorMessage name="endTime" />
</Box>
) : null}
</Form>
)}
</Formik>
Expand Down
14 changes: 10 additions & 4 deletions src/frontend/magnify/src/factories/meetings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@ import { Meeting, defaultRecurrenceConfiguration } from '../../types/entities/me
export const createRandomMeeting = (isReccurent = false, room?: Room): Meeting => {
const id = faker.datatype.uuid();
const name = faker.lorem.slug();
const startDate = faker.date.between(new Date().toLocaleDateString(), '2030-01-01T00:00:00.000Z');
const duration = faker.random.numeric(2);
const startDateTime = faker.date.between(
new Date().toLocaleDateString(),
'2030-01-01T00:00:00.000Z',
);
const maxEndDateTime = new Date(startDateTime);
maxEndDateTime.setHours(maxEndDateTime.getHours() + 5);

const endDateTime = faker.date.between(startDateTime, maxEndDateTime);

return {
id: id,
name: name,
room: room,
startDateTime: startDate,
expectedDuration: Number(duration),
startDateTime: startDateTime.toISOString(),
endDateTime: endDateTime.toISOString(),
jitsi: {
room: room ? `${room.slug}-${id}` : `${id}`,
token: '456',
Expand Down
5 changes: 3 additions & 2 deletions src/frontend/magnify/src/types/entities/meeting/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ export interface Meeting {
id: string;
name: string;
room?: Room;
startDateTime: Date;
expectedDuration: number;
// start and end DateTime will be in the ISO 8601 format with UTC time zone
startDateTime: string;
endDateTime: string;
jitsi: {
room: string;
token: string;
Expand Down

0 comments on commit 23fc6c1

Please sign in to comment.