From 13f2354e256fb3c09773cfe7e48605b3e21814d3 Mon Sep 17 00:00:00 2001 From: Joonatan Kuosa Date: Thu, 6 Mar 2025 08:20:11 +0200 Subject: [PATCH] fix: new application redirect non logged users - fix: include all query params when redirecting to login - refactor: new reservation login redirect to SSR only --- apps/ui/components/LoginFragment.tsx | 53 ++-- .../recurring/StartApplicationBar.tsx | 34 ++- apps/ui/hooks/useRemoveStoredReservation.ts | 14 ++ apps/ui/modules/util.ts | 14 +- apps/ui/pages/recurring/[id]/index.tsx | 65 ++++- .../ui/pages/reservation-unit/[...params].tsx | 32 +-- apps/ui/pages/reservation-unit/[id].tsx | 235 ++++++++---------- packages/common/src/browserHelpers.ts | 3 +- packages/common/src/helpers.ts | 8 + 9 files changed, 253 insertions(+), 205 deletions(-) create mode 100644 apps/ui/hooks/useRemoveStoredReservation.ts diff --git a/apps/ui/components/LoginFragment.tsx b/apps/ui/components/LoginFragment.tsx index ebd3b263bc..797d6902f0 100644 --- a/apps/ui/components/LoginFragment.tsx +++ b/apps/ui/components/LoginFragment.tsx @@ -2,53 +2,40 @@ import React from "react"; import { useTranslation } from "next-i18next"; import { signIn } from "common/src/browserHelpers"; import { useSession } from "@/hooks/auth"; -import { SubmitButton } from "@/styles/util"; +import { Button } from "hds-react"; type Props = { apiBaseUrl: string; - text?: string; - componentIfAuthenticated?: React.ReactNode; + componentIfAuthenticated: JSX.Element; isActionDisabled?: boolean; - actionCallback?: () => void; returnUrl?: string; }; -function LoginFragment({ +export function LoginFragment({ apiBaseUrl, - text, componentIfAuthenticated, isActionDisabled, - actionCallback, returnUrl, -}: Props): JSX.Element | null { +}: Props): JSX.Element { // TODO pass the isAuthenticated from SSR and remove the hook const { isAuthenticated } = useSession(); const { t } = useTranslation(); - return !isAuthenticated ? ( -
- { - if (actionCallback) { - actionCallback(); - } - if (returnUrl) { - signIn(apiBaseUrl, returnUrl); - return; - } - signIn(apiBaseUrl); - }} - aria-label={t("reservationCalendar:loginAndReserve")} - className="login-fragment__button--login" - disabled={isActionDisabled} - > - {t("reservationCalendar:loginAndReserve")} - - {text} -
- ) : ( -
{componentIfAuthenticated}
+ const handleClick = () => { + signIn(apiBaseUrl, returnUrl); + }; + + if (isAuthenticated) { + return componentIfAuthenticated; + } + + return ( + ); } - -export default LoginFragment; diff --git a/apps/ui/components/recurring/StartApplicationBar.tsx b/apps/ui/components/recurring/StartApplicationBar.tsx index 31381953a8..e591a4e6ff 100644 --- a/apps/ui/components/recurring/StartApplicationBar.tsx +++ b/apps/ui/components/recurring/StartApplicationBar.tsx @@ -22,6 +22,8 @@ import { useDisplayError } from "@/hooks/useDisplayError"; import { useReservationUnitList } from "@/hooks"; import { useSearchParams } from "next/navigation"; import { pageSideMargins } from "common/styles/layout"; +import { LoginFragment } from "../LoginFragment"; +import { getPostLoginUrl } from "@/modules/util"; const BackgroundContainer = styled.div` position: fixed; @@ -60,9 +62,11 @@ type Props = { applicationRound: { reservationUnits: NodeList; }; + apiBaseUrl: string; }; export function StartApplicationBar({ + apiBaseUrl, applicationRound, }: Props): JSX.Element | null { const { t } = useTranslation(); @@ -138,17 +142,25 @@ export function StartApplicationBar({ ? t("shoppingCart:deleteSelectionsShort") : t("shoppingCart:deleteSelections")} - } - colorVariant="light" - > - {t("shoppingCart:nextShort")} - + storeReservationForLogin()} + apiBaseUrl={apiBaseUrl} + componentIfAuthenticated={ + } + colorVariant="light" + > + {t("shoppingCart:nextShort")} + + } + /> ); diff --git a/apps/ui/hooks/useRemoveStoredReservation.ts b/apps/ui/hooks/useRemoveStoredReservation.ts new file mode 100644 index 0000000000..793ed6bf9a --- /dev/null +++ b/apps/ui/hooks/useRemoveStoredReservation.ts @@ -0,0 +1,14 @@ +import { ReservationProps } from "@/context/DataContext"; +import { useEffect } from "react"; +import { useLocalStorage } from "react-use"; + +/// Local storage usage has been removed +/// leaving this hook for now to cleanup any existing users +export function useRemoveStoredReservation() { + const [storedReservation, , removeStoredReservation] = + useLocalStorage("reservation"); + + useEffect(() => { + if (storedReservation) removeStoredReservation(); + }, [storedReservation, removeStoredReservation]); +} diff --git a/apps/ui/modules/util.ts b/apps/ui/modules/util.ts index bfdac5bc5b..e1f60dc692 100644 --- a/apps/ui/modules/util.ts +++ b/apps/ui/modules/util.ts @@ -14,6 +14,7 @@ import { timeToMinutes, type LocalizationLanguages, } from "common/src/helpers"; +import { ReadonlyURLSearchParams } from "next/navigation"; export { formatDuration } from "common/src/common/util"; export { fromAPIDate, fromUIDate }; @@ -120,14 +121,19 @@ export const getAddressAlt = (ru: { export const isTouchDevice = (): boolean => isBrowser && window?.matchMedia("(any-hover: none)").matches; -export function getPostLoginUrl() { +export function getPostLoginUrl( + params: Readonly = new ReadonlyURLSearchParams() +): string | undefined { if (!isBrowser) { return undefined; } const { origin, pathname, searchParams } = new URL(window.location.href); - const params = new URLSearchParams(searchParams); - params.set("isPostLogin", "true"); - return `${origin}${pathname}?${params.toString()}`; + const p = new URLSearchParams(searchParams); + for (const [key, value] of params) { + p.append(key, value); + } + p.set("isPostLogin", "true"); + return `${origin}${pathname}?${p.toString()}`; } // date format should always be in finnish, but the weekday and time separator should be localized diff --git a/apps/ui/pages/recurring/[id]/index.tsx b/apps/ui/pages/recurring/[id]/index.tsx index 915101f0e9..2cc70297e8 100644 --- a/apps/ui/pages/recurring/[id]/index.tsx +++ b/apps/ui/pages/recurring/[id]/index.tsx @@ -9,6 +9,12 @@ import { ApplicationRoundDocument, type ApplicationRoundQuery, type ApplicationRoundQueryVariables, + type CreateApplicationMutationVariables, + type CreateApplicationMutation, + CreateApplicationDocument, + type ApplicationCreateMutationInput, + CurrentUserDocument, + type CurrentUserQuery, } from "@gql/gql-types"; import { base64encode, @@ -28,14 +34,17 @@ import { useSearchQuery } from "@/hooks/useSearchQuery"; import { SortingComponent } from "@/components/SortingComponent"; import { useSearchParams } from "next/navigation"; import { Breadcrumb } from "@/components/common/Breadcrumb"; -import { seasonalPrefix } from "@/modules/urls"; +import { getApplicationPath, seasonalPrefix } from "@/modules/urls"; import { getApplicationRoundName } from "@/modules/applicationRound"; import { gql } from "@apollo/client"; function SeasonalSearch({ + apiBaseUrl, applicationRound, options, -}: Readonly>): JSX.Element { +}: Readonly< + Pick +>): JSX.Element { const { t, i18n } = useTranslation(); const searchValues = useSearchParams(); @@ -102,7 +111,10 @@ function SeasonalSearch({ fetchMore={(cursor) => fetchMore(cursor)} sortingComponent={} /> - + ); } @@ -111,10 +123,11 @@ type Props = Awaited>["props"]; type NarrowedProps = Exclude; export async function getServerSideProps(ctx: GetServerSidePropsContext) { - const { locale, params } = ctx; + const { locale, params, query } = ctx; const commonProps = getCommonServerSideProps(); const apolloClient = createApolloClient(commonProps.apiBaseUrl, ctx); const pk = toNumber(ignoreMaybeArray(params?.id)); + const isPostLogin = query.isPostLogin === "true"; const notFound = { notFound: true, @@ -141,7 +154,49 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { return notFound; } - const opts = await getSearchOptions(apolloClient, "seasonal", locale ?? ""); + const { data: userData } = await apolloClient.query({ + query: CurrentUserDocument, + }); + + if (isPostLogin && userData.currentUser != null) { + const input: ApplicationCreateMutationInput = { + applicationRound: applicationRound.pk ?? 0, + }; + + // don't catch errors here -> results in 500 page + // we can't display proper error message to user (no page for it) + // and we can't handle them + const mutRes = await apolloClient.mutate< + CreateApplicationMutation, + CreateApplicationMutationVariables + >({ + mutation: CreateApplicationDocument, + variables: { + input, + }, + }); + + if (mutRes.data?.createApplication?.pk) { + const { pk } = mutRes.data.createApplication; + const selected = query.selectedReservationUnits ?? []; + const forwardParams = new URLSearchParams(); + for (const s of selected) { + forwardParams.append("selectedReservationUnits", s); + } + const url = `${getApplicationPath(pk, "page1")}?${forwardParams.toString()}`; + return { + redirect: { + permanent: false, + destination: url, + }, + props: { + notFound: true, // for prop narrowing + }, + }; + } + } + + const opts = await getSearchOptions(apolloClient, "seasonal", locale ?? "fi"); return { props: { ...commonProps, diff --git a/apps/ui/pages/reservation-unit/[...params].tsx b/apps/ui/pages/reservation-unit/[...params].tsx index 536f832a1c..3b0115846f 100644 --- a/apps/ui/pages/reservation-unit/[...params].tsx +++ b/apps/ui/pages/reservation-unit/[...params].tsx @@ -1,8 +1,7 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import styled from "styled-components"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { useRouter } from "next/router"; -import { useLocalStorage } from "react-use"; import { Stepper } from "hds-react"; import { FormProvider, useForm } from "react-hook-form"; import type { GetServerSidePropsContext } from "next"; @@ -21,6 +20,7 @@ import { } from "@gql/gql-types"; import { type Inputs } from "common/src/reservation-form/types"; import { createApolloClient } from "@/modules/apolloClient"; +import { default as NextError } from "next/error"; import { getReservationPath, getReservationUnitPath, @@ -29,7 +29,6 @@ import { import { Sanitize } from "common/src/components/Sanitize"; import { isReservationUnitFreeOfCharge } from "@/modules/reservationUnit"; import { getCheckoutUrl } from "@/modules/reservation"; -import { ReservationProps } from "@/context/DataContext"; import { ReservationInfoCard } from "@/components/reservation/ReservationInfoCard"; import { Step0 } from "@/components/reservation/Step0"; import { Step1 } from "@/components/reservation/Step1"; @@ -51,6 +50,7 @@ import { Flex } from "common/styles/util"; import { Breadcrumb } from "@/components/common/Breadcrumb"; import { ReservationPageWrapper } from "@/components/reservations/styles"; import { useDisplayError } from "@/hooks/useDisplayError"; +import { useRemoveStoredReservation } from "@/hooks/useRemoveStoredReservation"; const StyledReservationInfoCard = styled(ReservationInfoCard)` grid-column: 1 / -1; @@ -86,18 +86,6 @@ const TitleSection = styled(Flex)` } `; -/// We want to get rid of the local storage -/// but there is still code that requires it to be used. -/// Other pages (ex. login + book) get confused if we don't clear it here. -const useRemoveStoredReservation = () => { - const [storedReservation, , removeStoredReservation] = - useLocalStorage("reservation"); - - useEffect(() => { - if (storedReservation) removeStoredReservation(); - }, [storedReservation, removeStoredReservation]); -}; - // NOTE back / forward buttons (browser) do NOT work properly // router.beforePopState could be used to handle them but it's super hackish // the correct solution is to create separate pages (files) for each step (then next-router does this for free) @@ -122,15 +110,6 @@ function NewReservation(props: PropsNarrowed): JSX.Element | null { const reservation = resData?.reservation ?? props.reservation; const reservationUnit = reservation?.reservationUnits?.find(() => true); - // it should be Created only here (SSR should have redirected) - if (reservation.state !== ReservationStateChoice.Created) { - // eslint-disable-next-line no-console - console.warn( - "should NOT be here when reservation state is ", - reservation.state - ); - } - useRemoveStoredReservation(); const [step, setStep] = useState(0); @@ -352,6 +331,11 @@ function NewReservation(props: PropsNarrowed): JSX.Element | null { } }; + // it should be Created only here (SSR should have redirected) + if (reservation.state !== ReservationStateChoice.Created) { + return ; + } + return ( diff --git a/apps/ui/pages/reservation-unit/[id].tsx b/apps/ui/pages/reservation-unit/[id].tsx index 25b1b046a5..c5a1ecf883 100644 --- a/apps/ui/pages/reservation-unit/[id].tsx +++ b/apps/ui/pages/reservation-unit/[id].tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useMemo, useState } from "react"; import type { GetServerSidePropsContext } from "next"; import { Trans, useTranslation } from "next-i18next"; import { useRouter } from "next/router"; @@ -14,9 +14,7 @@ import { toUIDate, } from "common/src/common/util"; import { formatters as getFormatters, H4 } from "common"; -import { useLocalStorage } from "react-use"; import { breakpoints } from "common/src/common/style"; -import { type PendingReservation } from "@/modules/types"; import { type ApplicationRoundTimeSlotNode, type ReservationCreateMutationInput, @@ -27,12 +25,21 @@ import { RelatedReservationUnitsDocument, type RelatedReservationUnitsQuery, type RelatedReservationUnitsQueryVariables, + CreateReservationDocument, + type CreateReservationMutation, + type CreateReservationMutationVariables, + CurrentUserDocument, + type CurrentUserQuery, + type TimeSlotType, } from "@gql/gql-types"; import { base64encode, filterNonNullable, + formatTimeRange, fromMondayFirstUnsafe, + ignoreMaybeArray, isPriceFree, + timeToMinutes, toNumber, } from "common/src/helpers"; import { Head } from "@/components/reservation-unit/Head"; @@ -84,7 +91,7 @@ import { PendingReservationFormSchema, type PendingReservationFormType, } from "@/components/reservation-unit/schema"; -import LoginFragment from "@/components/LoginFragment"; +import { LoginFragment } from "@/components/LoginFragment"; import { RELATED_RESERVATION_STATES } from "common/src/const"; import { useReservableTimes } from "@/hooks/useReservableTimes"; import { ReservationTimePicker } from "@/components/reservation/ReservationTimePicker"; @@ -98,18 +105,66 @@ import { Breadcrumb } from "@/components/common/Breadcrumb"; import { Flex } from "common/styles/util"; import { SubmitButton } from "@/styles/util"; import { useDisplayError } from "@/hooks/useDisplayError"; +import { useRemoveStoredReservation } from "@/hooks/useRemoveStoredReservation"; type Props = Awaited>["props"]; type PropsNarrowed = Exclude; export async function getServerSideProps(ctx: GetServerSidePropsContext) { const { params, query, locale } = ctx; - const pk = Number(params?.id); + const pk = toNumber(ignoreMaybeArray(params?.id)); const uuid = query.ru; const commonProps = getCommonServerSideProps(); const apolloClient = createApolloClient(commonProps.apiBaseUrl, ctx); - if (pk) { + const notFound = { + props: { + ...commonProps, + ...(await serverSideTranslations(locale ?? "fi")), + notFound: true, // required for type narrowing + }, + notFound: true, + }; + + const isPostLogin = query.isPostLogin === "true"; + + // recheck login status in case user cancelled the login + const { data: userData } = await apolloClient.query({ + query: CurrentUserDocument, + }); + if (pk != null && pk > 0 && isPostLogin && userData?.currentUser != null) { + const begin = ignoreMaybeArray(query.begin); + const end = ignoreMaybeArray(query.end); + + if (begin != null && end != null) { + const input: ReservationCreateMutationInput = { + begin, + end, + reservationUnit: pk, + }; + const res = await apolloClient.mutate< + CreateReservationMutation, + CreateReservationMutationVariables + >({ + mutation: CreateReservationDocument, + variables: { + input, + }, + }); + const { pk: reservationPk } = res.data?.createReservation ?? {}; + return { + redirect: { + destination: getReservationInProgressPath(pk, reservationPk), + permanent: false, + }, + props: { + notFound: true, // required for type narrowing + }, + }; + } + } + + if (pk != null && pk > 0) { const today = new Date(); const startDate = today; const endDate = addYears(today, 2); @@ -121,7 +176,6 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { ReservationUnitPageQueryVariables >({ query: ReservationUnitPageDocument, - fetchPolicy: "no-cache", variables: { id, beginDate: toApiDate(startDate) ?? "", @@ -138,24 +192,12 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { const reservationUnit = reservationUnitData?.reservationUnit ?? undefined; if (!isReservationUnitPublished(reservationUnit) && !previewPass) { - return { - props: { - ...commonProps, - notFound: true, // required for type narrowing - }, - notFound: true, - }; + return notFound; } const isDraft = reservationUnit?.isDraft; if (isDraft && !previewPass) { - return { - props: { - ...commonProps, - notFound: true, // required for type narrowing - }, - notFound: true, - }; + return notFound; } const bookingTerms = await getGenericTerms(apolloClient); @@ -179,13 +221,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { } if (!reservationUnit?.pk) { - return { - props: { - ...commonProps, - notFound: true, // required for type narrowing - }, - notFound: true, - }; + return notFound; } const reservableTimeSpans = filterNonNullable( @@ -194,9 +230,9 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { const queryParams = new URLSearchParams(query as Record); const searchDate = queryParams.get("date") ?? null; const searchTime = queryParams.get("time") ?? null; - const searchDuration = Number.isNaN(Number(queryParams.get("duration"))) - ? null - : Number(queryParams.get("duration")); + const searchDuration = toNumber( + ignoreMaybeArray(queryParams.get("duration")) + ); const blockingReservations = filterNonNullable( reservationUnitData?.affectingReservations @@ -212,7 +248,6 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { relatedReservationUnits, activeApplicationRounds, termsOfUse: { genericTerms: bookingTerms }, - isPostLogin: query?.isPostLogin === "true", searchDuration, searchDate, searchTime, @@ -220,15 +255,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { }; } - return { - props: { - ...commonProps, - ...(await serverSideTranslations(locale ?? "fi")), - notFound: true, // required for type narrowing - paramsId: pk, - }, - notFound: true, - }; + return notFound; } const StyledApplicationRoundScheduleDay = styled.p` @@ -240,19 +267,26 @@ const StyledApplicationRoundScheduleDay = styled.p` } `; +function formatTimeSlot(slot: TimeSlotType): string { + const { begin, end } = slot; + if (!begin || !end) { + return ""; + } + const beginTime = timeToMinutes(begin); + const endTime = timeToMinutes(end); + const endTimeChecked = endTime === 0 ? 24 * 60 : endTime; + return formatTimeRange(beginTime, endTimeChecked, true); +} + // Returns an element for a weekday in the application round timetable, with up to two timespans function ApplicationRoundScheduleDay( - props: Omit + props: Pick< + ApplicationRoundTimeSlotNode, + "weekday" | "reservableTimes" | "closed" + > ) { const { t } = useTranslation(); const { weekday, reservableTimes, closed } = props; - const noSeconds = (time: string) => time.split(":").slice(0, 2).join(":"); - const timeSlotString = (idx: number): string => - reservableTimes?.[idx]?.begin && reservableTimes?.[idx]?.end - ? `${noSeconds(String(reservableTimes?.[idx]?.begin))}-${noSeconds( - String(reservableTimes?.[idx]?.end) - )}` - : ""; return ( @@ -263,8 +297,9 @@ function ApplicationRoundScheduleDay( ) : ( reservableTimes && ( - {reservableTimes[0] && timeSlotString(0)} - {reservableTimes[1] && ` ${t("common:and")} ${timeSlotString(1)}`} + {reservableTimes[0] && formatTimeSlot(reservableTimes[0])} + {reservableTimes[1] && + ` ${t("common:and")} ${formatTimeSlot(reservableTimes[1])}`} ) )} @@ -276,19 +311,30 @@ function SubmitFragment( props: Readonly<{ focusSlot: FocusTimeSlot; apiBaseUrl: string; - actionCallback: () => void; reservationForm: UseFormReturn; loadingText: string; buttonText: string; }> -) { +): JSX.Element { const { isSubmitting } = props.reservationForm.formState; + const { focusSlot } = props; const { isReservable } = props.focusSlot; + const returnToUrl = useMemo(() => { + if (!focusSlot.isReservable) { + return; + } + const { start: begin, end } = focusSlot ?? {}; + + const params = new URLSearchParams(); + params.set("begin", begin.toISOString()); + params.set("end", end.toISOString()); + return getPostLoginUrl(params); + }, [focusSlot]); + return ( } - returnUrl={getPostLoginUrl()} + returnUrl={returnToUrl} /> ); } @@ -328,7 +374,6 @@ function ReservationUnit({ activeApplicationRounds, blockingReservations, termsOfUse, - isPostLogin, apiBaseUrl, searchDuration, searchDate, @@ -336,6 +381,7 @@ function ReservationUnit({ }: PropsNarrowed): JSX.Element | null { const { t } = useTranslation(); const router = useRouter(); + useRemoveStoredReservation(); const [isDialogOpen, setIsDialogOpen] = useState(false); @@ -455,7 +501,7 @@ function ReservationUnit({ return reservationUnit.canApplyFreeOfCharge && isPaid; }, [reservationUnit.canApplyFreeOfCharge, reservationUnit.pricings]); - const [addReservation] = useCreateReservationMutation(); + const [createReservationMutation] = useCreateReservationMutation(); const displayError = useDisplayError(); @@ -466,7 +512,7 @@ function ReservationUnit({ if (reservationUnit.pk == null) { throw new Error("Reservation unit pk is missing"); } - const res = await addReservation({ + const res = await createReservationMutation({ variables: { input, }, @@ -475,9 +521,7 @@ function ReservationUnit({ if (pk == null) { throw new Error("Reservation creation failed"); } - if (reservationUnit.pk != null) { - router.push(getReservationInProgressPath(reservationUnit.pk, pk)); - } + router.push(getReservationInProgressPath(reservationUnit.pk, pk)); } catch (err) { displayError(err); } @@ -491,70 +535,8 @@ function ReservationUnit({ console.warn("not reservable because: ", reason); } - const [storedReservation, setStoredReservation] = - useLocalStorage("reservation"); - - const storeReservationForLogin = useCallback(() => { - if (reservationUnit.pk == null) { - return; - } - if (!focusSlot.isReservable) { - return; - } + const shouldDisplayBottomWrapper = relatedReservationUnits?.length > 0; - const { start, end } = focusSlot ?? {}; - // NOTE the only place where we use ISO strings since they are always converted to Date objects - // another option would be to refactor storaged reservation to use Date objects - setStoredReservation({ - begin: start.toISOString(), - end: end.toISOString(), - price: undefined, - reservationUnitPk: reservationUnit.pk ?? 0, - }); - }, [focusSlot, reservationUnit.pk, setStoredReservation]); - - // If returning from login, continue on to reservation details - useEffect(() => { - if ( - !!isPostLogin && - storedReservation && - !isReservationQuotaReached && - reservationUnit?.pk - ) { - const { begin, end } = storedReservation; - const input: ReservationCreateMutationInput = { - begin, - end, - reservationUnit: reservationUnit.pk, - }; - createReservation(input); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - /* TODO why is this needed? do we need to reset the form values? - useEffect(() => { - const { begin, end } = storedReservation ?? {}; - if (begin == null || end == null) { - return; - } - - const beginDate = new Date(begin); - const endDate = new Date(end); - - // TODO why? can't we set it using the form or can we make an intermediate reset function - handleCalendarEventChange({ - start: beginDate, - end: endDate, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [storedReservation?.begin, storedReservation?.end]); - */ - - const shouldDisplayBottomWrapper = useMemo( - () => relatedReservationUnits?.length > 0, - [relatedReservationUnits?.length] - ); const termsOfUseContent = getTranslation(reservationUnit, "termsOfUse"); const paymentTermsContent = reservationUnit.paymentTerms ? getTranslation(reservationUnit.paymentTerms, "text") @@ -576,13 +558,12 @@ function ReservationUnit({ storeReservationForLogin()} reservationForm={reservationForm} loadingText={t("reservationCalendar:makeReservationLoading")} buttonText={t("reservationCalendar:makeReservation")} /> ), - [apiBaseUrl, focusSlot, reservationForm, storeReservationForLogin, t] + [apiBaseUrl, focusSlot, reservationForm, t] ); const startingTimeOptions = getPossibleTimesForDay({ diff --git a/packages/common/src/browserHelpers.ts b/packages/common/src/browserHelpers.ts index 23048c1d33..70ddf17e55 100644 --- a/packages/common/src/browserHelpers.ts +++ b/packages/common/src/browserHelpers.ts @@ -21,7 +21,8 @@ export function signIn(apiBaseUrl: string, returnUrl?: unknown) { throw new Error("signIn can only be called in the browser"); } const returnUrlParam = cleanupUrlParam(returnUrl); - const returnTo = returnUrlParam ?? window.location.href; + // encode otherwise backend will drop the query params + const returnTo = encodeURIComponent(returnUrlParam ?? window.location.href); const url = getSignInUrl(apiBaseUrl, returnTo); window.location.href = url; } diff --git a/packages/common/src/helpers.ts b/packages/common/src/helpers.ts index d8cd2de7ae..cc81fe9b78 100644 --- a/packages/common/src/helpers.ts +++ b/packages/common/src/helpers.ts @@ -211,6 +211,14 @@ export function formatMinutes( return `${hours}`; } +export function formatTimeRange( + begin: number, + end: number, + trailingMinutes = false +): string { + return `${formatMinutes(begin, trailingMinutes)}–${formatMinutes(end, trailingMinutes)}`; +} + export function formatApiTimeInterval({ beginTime, endTime,