Skip to content

Commit

Permalink
feat: display modal if registration time is expiring soon
Browse files Browse the repository at this point in the history
  • Loading branch information
jorilindell committed Apr 26, 2024
1 parent bb0e6f9 commit 723de00
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 13 deletions.
15 changes: 14 additions & 1 deletion public/locales/en/reservation.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
{
"hour": "{{count}} hour",
"hour_other": "{{count}} hours",
"minute": "{{count}} minute",
"minute_other": "{{count}} minutes",
"reservationTimeExpiredModal": {
"buttonTryAgain": "Try Again",
"text": "Unfortunately, the Reservation time has expired.",
"title": "Time’s Up."
}
},
"reservationTimeExpiringModal": {
"buttonClose": "Close",
"text": "The reservation is about to expire in {{count}} seconds",
"text_other": "The reservation is about to expire in {{count}} seconds",
"title": "The reservation period is ending"
},
"second": "{{count}} second",
"second_other": "{{count}} seconds",
"timeLeft": "Time left to register"
}
1 change: 0 additions & 1 deletion public/locales/en/signup.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@
"titleBasicInfo": "Basic information of the registrant",
"titleSignups": "Registered persons"
},
"timeLeft": "Time left to register",
"titleBasicInfo": "Basic information of the registrant",
"titleRegistration": "Registration",
"warnings": {
Expand Down
15 changes: 14 additions & 1 deletion public/locales/fi/reservation.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
{
"hour": "{{count}} tunti",
"hour_other": "{{count}} tuntia",
"minute": "{{count}} minuutti",
"minute_other": "{{count}} minuuttia",
"reservationTimeExpiredModal": {
"buttonTryAgain": "Yritä uudelleen",
"text": "Valitettavasti varausaika on umpeutunut.",
"title": "Varausaika on täynnä."
}
},
"reservationTimeExpiringModal": {
"buttonClose": "Sulje",
"text": "Varauksen aika on päättymässä {{count}} sekunnin päästä",
"text_other": "Varauksen aika on päättymässä {{count}} sekunnin päästä",
"title": "Varauksen aika on päättymässä"
},
"second": "{{count}} sekunti",
"second_other": "{{count}} sekuntia",
"timeLeft": "Aikaa jäljellä ilmoittautumisen tekoon"
}
1 change: 0 additions & 1 deletion public/locales/fi/signup.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@
"titleBasicInfo": "Ilmoittautujan perustiedot",
"titleSignups": "Ilmoittautuneet"
},
"timeLeft": "Aikaa jäljellä ilmoittautumisen tekoon",
"titleRegistration": "Ilmoittautuminen",
"warnings": {
"allSeatsReserved": "Tapahtuman kaikki paikat ovat tällä hetkellä varatut. Kokeile myöhemmin uudelleen.",
Expand Down
15 changes: 14 additions & 1 deletion public/locales/sv/reservation.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
{
"hour": "{{count}} timme",
"hour_other": "{{count}} timmar",
"minute": "{{count}} minut",
"minute_other": "{{count}} minuter",
"reservationTimeExpiredModal": {
"buttonTryAgain": "Försök igen",
"text": "Tyvärr har bokningstiden gått ut.",
"title": "Tiden är ute."
}
},
"reservationTimeExpiringModal": {
"buttonClose": "Stäng",
"text": "Reservationen är på väg att löpa ut om {{count}} sekund",
"text_other": "Reservationen är på väg att löpa ut om {{count}} sekunder",
"title": "Reservationsperioden går ut"
},
"second": "{{count}} sekund",
"second_other": "{{count}} sekunder",
"timeLeft": "Tid kvar att registrera sig"
}
1 change: 0 additions & 1 deletion public/locales/sv/signup.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@
"titleBasicInfo": "Grundläggande information om registranten",
"titleSignups": "Registrerade personer"
},
"timeLeft": "Tid kvar att registrera sig",
"titleRegistration": "Registrering",
"warnings": {
"allSeatsReserved": "Alla platser i evenemanget är för närvarande bokade. Vänligen försök igen senare.",
Expand Down
1 change: 1 addition & 0 deletions src/domain/signup/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export enum SIGNUP_MODALS {
DELETE_SIGNUP_FROM_FORM = 'deleteSignupFromForm',
PERSONS_ADDED_TO_WAITLIST = 'personsAddedToWaitList',
RESERVATION_TIME_EXPIRED = 'reservationTimeExpired',
RESERVATION_TIME_EXPIRING = 'reservationTimeExpiring',
}

export enum SIGNUP_QUERY_PARAMS {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Dialog, IconInfoCircle } from 'hds-react';
import { useTranslation } from 'next-i18next';
import React from 'react';

import Button from '../../../../common/components/button/Button';
import styles from '../../../../common/components/dialog/dialog.module.scss';

export interface ReservationTimeExpiringModalProps {
isOpen: boolean;
onClose: () => void;
timeLeft: number | null;
}

const ReservationTimeExpiringModal: React.FC<
ReservationTimeExpiringModalProps
> = ({ isOpen, onClose, timeLeft }) => {
const { t } = useTranslation(['reservation']);

const handleClose = (event?: React.MouseEvent | React.KeyboardEvent) => {
event?.preventDefault();
event?.stopPropagation();

onClose();
};

const id = 'reservation-time-expiring-modal';
const titleId = `${id}-title`;
const descriptionId = `${id}-description`;

return (
<Dialog
id={id}
aria-labelledby={titleId}
aria-describedby={descriptionId}
className={styles.dialog}
isOpen={isOpen}
variant="primary"
>
<Dialog.Header
id={titleId}
iconLeft={<IconInfoCircle aria-hidden={true} />}
title={t('reservation:reservationTimeExpiringModal.title')}
/>
<Dialog.Content>
<p id={descriptionId}>
{t('reservation:reservationTimeExpiringModal.text', {
count: timeLeft as number,
})}
</p>
</Dialog.Content>
<Dialog.ActionButtons>
<Button onClick={handleClose} type="button" variant="primary">
{t('reservation:reservationTimeExpiringModal.buttonClose')}
</Button>
</Dialog.ActionButtons>
</Dialog>
);
};

export default ReservationTimeExpiringModal;
56 changes: 49 additions & 7 deletions src/domain/signupGroup/reservationTimer/ReservationTimer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import React, { useCallback, useMemo, useRef, useState } from 'react';

import { useAccessibilityNotificationContext } from '../../../common/components/accessibilityNotificationContext/hooks/useAccessibilityNotificationContext';
import { ROUTES } from '../../app/routes/constants';
import { Registration } from '../../registration/types';
import {
Expand All @@ -18,14 +19,23 @@ import useSeatsReservationActions from '../../signup/hooks/useSeatsReservationAc
import { useSignupServerErrorsContext } from '../../signup/signupServerErrorsContext/hooks/useSignupServerErrorsContext';
import { clearCreateSignupGroupFormData } from '../../signupGroup/utils';
import ReservationTimeExpiredModal from '../modals/reservationTimeExpiredModal/ReservationTimeExpiredModal';
import ReservationTimeExpiringModal from '../modals/reservationTimeExpiringModal/ReservationTimeExpiringModal';
import { useSignupGroupFormContext } from '../signupGroupFormContext/hooks/useSignupGroupFormContext';
import { SignupFormFields } from '../types';

const getTimeStr = (timeLeft: number) => {
const EXPIRING_THRESHOLD = 60;

const getTimeParts = (timeLeft: number) => {
const hours = Math.floor(timeLeft / 3600);
const minutes = Math.floor(timeLeft / 60) % 60;
const seconds = timeLeft % 60;

return { hours, minutes, seconds };
};

const getTimeStr = (timeLeft: number) => {
const { hours, minutes, seconds } = getTimeParts(timeLeft);

return [
hours,
...[minutes, seconds].map((n) => n.toString().padStart(2, '0')),
Expand Down Expand Up @@ -53,8 +63,10 @@ const ReservationTimer: React.FC<ReservationTimerProps> = ({
setSignups,
signups,
}) => {
const isExpiringModalAlreadyDisplayed = useRef(false);
const router = useRouter();
const { t } = useTranslation('signup');
const { t } = useTranslation('reservation');
const { setAccessibilityText } = useAccessibilityNotificationContext();

const creatingReservationStarted = useRef(false);
const timerEnabled = useRef(false);
Expand All @@ -67,7 +79,7 @@ const ReservationTimer: React.FC<ReservationTimerProps> = ({
setSignups,
signups,
});
const { openModal, setOpenModal } = useSignupGroupFormContext();
const { closeModal, openModal, setOpenModal } = useSignupGroupFormContext();
const { setServerErrorItems, showServerErrors } =
useSignupServerErrorsContext();

Expand Down Expand Up @@ -98,6 +110,19 @@ const ReservationTimer: React.FC<ReservationTimerProps> = ({
}
};

const setTimeLeftAndNotify = (newTimeLeft: number) => {
setTimeLeft(newTimeLeft);
const { hours, minutes, seconds } = getTimeParts(newTimeLeft);
setAccessibilityText(
[
`${t('reservation:timeLeft')}:`,
t('reservation:hour', { count: hours }),
t('reservation:minute', { count: minutes }),
t('reservation:second', { count: seconds }),
].join(' ')
);
};

React.useEffect(() => {
const data = getSeatsReservationData(registrationId);

Expand All @@ -119,7 +144,7 @@ const ReservationTimer: React.FC<ReservationTimerProps> = ({
onSuccess: (seatsReservation) => {
enableTimer();
if (seatsReservation) {
setTimeLeft(getRegistrationTimeLeft(seatsReservation));
setTimeLeftAndNotify(getRegistrationTimeLeft(seatsReservation));
}
},
});
Expand All @@ -128,7 +153,7 @@ const ReservationTimer: React.FC<ReservationTimerProps> = ({
}
} else if (data) {
enableTimer();
setTimeLeft(getRegistrationTimeLeft(data));
setTimeLeftAndNotify(getRegistrationTimeLeft(data));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Expand All @@ -138,7 +163,8 @@ const ReservationTimer: React.FC<ReservationTimerProps> = ({
/* istanbul ignore else */
if (timerEnabled.current) {
const data = getSeatsReservationData(registrationId);
setTimeLeft(getRegistrationTimeLeft(data));
const newTimeLeft = getRegistrationTimeLeft(data);
setTimeLeft(newTimeLeft);

/* istanbul ignore else */
if (!callbacksDisabled) {
Expand All @@ -153,6 +179,17 @@ const ReservationTimer: React.FC<ReservationTimerProps> = ({
if (session) {
setOpenModal(SIGNUP_MODALS.RESERVATION_TIME_EXPIRED);
}
} else if (
!isExpiringModalAlreadyDisplayed.current &&
newTimeLeft <= EXPIRING_THRESHOLD
) {
const session = await getSession();

// Show modal only if user is authenticated
if (session) {
setOpenModal(SIGNUP_MODALS.RESERVATION_TIME_EXPIRING);
isExpiringModalAlreadyDisplayed.current = true;
}
}
}
}
Expand All @@ -173,13 +210,18 @@ const ReservationTimer: React.FC<ReservationTimerProps> = ({

return (
<>
<ReservationTimeExpiringModal
isOpen={openModal === SIGNUP_MODALS.RESERVATION_TIME_EXPIRING}
onClose={closeModal}
timeLeft={timeLeft}
/>
<ReservationTimeExpiredModal
isOpen={openModal === SIGNUP_MODALS.RESERVATION_TIME_EXPIRED}
onTryAgain={handleTryAgain}
/>

<div>
{t('timeLeft')}{' '}
{t('reservation:timeLeft')}{' '}
<strong>{timeLeft !== null && getTimeStr(timeLeft)}</strong>
</div>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,28 @@ test('should show modal if reserved seats are in waiting list', async () => {
await waitFor(() => expect(modal).not.toBeInTheDocument());
});

test('should display reservation expiring modal if there is less than 60 seconds left', async () => {
const user = userEvent.setup();

setSignupGroupFormSessionStorageValues({
registrationId: registration.id,
seatsReservation: getMockedSeatsReservationData(59),
});

setQueryMocks(mockedUserResponse);
renderComponent();

const modal = await screen.findByRole(
'dialog',
{ name: 'Varauksen aika on päättymässä' },
{ timeout: 5000 }
);
const closeButton = within(modal).getByRole('button', { name: 'Sulje' });
await user.click(closeButton);

await waitFor(() => expect(modal).not.toBeInTheDocument());
});

test('should route to create signup group page if reservation is expired', async () => {
const user = userEvent.setup();

Expand Down

0 comments on commit 723de00

Please sign in to comment.