Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.

Commit 878bbbc

Browse files
committed
refactor: cancel reservation page
- reuse styling from edit page - remove double testids - fix: check if the reservation can be cancelled
1 parent fcbf0a8 commit 878bbbc

4 files changed

Lines changed: 188 additions & 248 deletions

File tree

Lines changed: 122 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useMemo, useState } from "react";
1+
import React, { useEffect, useState } from "react";
22
import styled from "styled-components";
33
import router from "next/router";
44
import { Controller, useForm } from "react-hook-form";
@@ -7,22 +7,29 @@ import { useTranslation } from "next-i18next";
77
import { OptionType } from "common/types/common";
88
import { breakpoints } from "common/src/common/style";
99
import NotificationBox from "common/src/common/NotificationBox";
10-
import { fontMedium, H2 } from "common/src/common/typography";
10+
import { fontMedium } from "common/src/common/typography";
1111
import {
1212
useCancelReservationMutation,
1313
type ReservationQuery,
1414
type ReservationCancelReasonsQuery,
1515
} from "@gql/gql-types";
16-
import { Container as CommonContainer } from "common";
1716
import { IconButton, ShowAllContainer } from "common/src/components";
1817
import Sanitize from "../common/Sanitize";
19-
import { JustForDesktop, JustForMobile } from "@/modules/style/layout";
2018
import { getSelectedOption, getTranslation } from "@/modules/util";
21-
import { CenterSpinner } from "../common/common";
2219
import { BlackButton, MediumButton, Toast } from "@/styles/util";
2320
import { ReservationInfoCard } from "./ReservationInfoCard";
2421
import { Paragraph } from "./styles";
2522
import { signOut } from "@/hooks/auth";
23+
import {
24+
BylineSection,
25+
Heading,
26+
HeadingSection,
27+
ReservationPageWrapper,
28+
} from "../reservations/styles";
29+
import {
30+
convertLanguageCode,
31+
getTranslationSafe,
32+
} from "common/src/common/util";
2633

2734
type CancelReasonsQ = NonNullable<
2835
ReservationCancelReasonsQuery["reservationCancelReasons"]
@@ -35,29 +42,9 @@ type NodeT = ReservationQuery["reservation"];
3542
type Props = {
3643
apiBaseUrl: string;
3744
reasons: CancelReasonsNode[];
38-
reservation: NodeT;
45+
reservation: NonNullable<NodeT>;
3946
};
4047

41-
const Spinner = styled(CenterSpinner)`
42-
margin: var(--spacing-layout-xl) auto;
43-
`;
44-
45-
const Wrapper = styled.div`
46-
background-color: var(--color-white);
47-
`;
48-
49-
const Container = styled(CommonContainer)`
50-
@media (min-width: ${breakpoints.m}) {
51-
margin-bottom: var(--spacing-layout-l);
52-
}
53-
`;
54-
55-
const Title = styled(H2).attrs({ as: "h1" })``;
56-
57-
const Heading = styled.div`
58-
font-size: var(--fontsize-body-l);
59-
`;
60-
6148
const TermsContainer = styled(ShowAllContainer)`
6249
.ShowAllContainer__ToggleButton {
6350
color: var(--color-bus);
@@ -80,32 +67,13 @@ const Actions = styled.div`
8067
}
8168
`;
8269

83-
const Content = styled.div`
84-
font-size: var(--fontsize-body-l);
85-
`;
86-
87-
const ContentContainer = styled.div`
88-
margin-bottom: var(--spacing-xl);
89-
white-space: pre-line;
90-
91-
div[role="heading"] {
92-
font-size: var(--fontsize-heading-s);
93-
}
94-
`;
95-
96-
const Columns = styled.div`
97-
grid-template-columns: 1fr;
98-
display: grid;
99-
align-items: flex-start;
100-
gap: var(--spacing-l);
101-
102-
@media (min-width: ${breakpoints.m}) {
103-
& > div:nth-of-type(1) {
104-
order: 2;
105-
}
70+
const ContentSection = styled.div`
71+
grid-column: 1 / -1;
72+
grid-row: 3 / -1;
10673
107-
margin-top: var(--spacing-xl);
108-
grid-template-columns: 1fr 378px;
74+
@media (min-width: ${breakpoints.l}) {
75+
grid-column: 1 / span 4;
76+
grid-row: 2 / -1;
10977
}
11078
`;
11179

@@ -129,18 +97,13 @@ const ReturnLinkContainer = styled.div`
12997
display: flex;
13098
flex-direction: column;
13199
align-items: flex-start;
100+
margin-top: var(--spacing-3-xl);
132101
`;
133102

134-
function ReturnLinkList({
135-
apiBaseUrl,
136-
style,
137-
}: {
138-
apiBaseUrl: string;
139-
style?: React.CSSProperties;
140-
}): JSX.Element {
103+
function ReturnLinkList({ apiBaseUrl }: { apiBaseUrl: string }): JSX.Element {
141104
const { t } = useTranslation();
142105
return (
143-
<ReturnLinkContainer style={style}>
106+
<ReturnLinkContainer>
144107
<IconButton
145108
href="/reservations"
146109
label={t("reservations:gotoReservations")}
@@ -161,16 +124,17 @@ function ReturnLinkList({
161124
}
162125

163126
// TODO there is also pages/reservation/cancel.tsx (what is that?)
164-
function ReservationCancellation(props: Props): JSX.Element {
165-
const { t } = useTranslation();
127+
export function ReservationCancellation(props: Props): JSX.Element {
128+
const { t, i18n } = useTranslation();
166129
const { apiBaseUrl } = props;
167130

168131
const [errorMsg, setErrorMsg] = useState<string | null>(null);
169132
const [formState, setFormState] = useState<"unsent" | "sent">("unsent");
170133

134+
const lang = convertLanguageCode(i18n.language);
171135
const reasons = props.reasons.map((node) => ({
172-
label: getTranslation(node, "reason"),
173-
value: node?.pk != null ? node?.pk : "",
136+
label: getTranslationSafe(node, "reason", lang),
137+
value: node?.pk ?? 0,
174138
}));
175139

176140
const [cancelReservation, { loading }] = useCancelReservationMutation();
@@ -185,22 +149,14 @@ function ReservationCancellation(props: Props): JSX.Element {
185149

186150
const { reservation } = props;
187151

188-
const bylineContent = useMemo(() => {
189-
return (
190-
reservation && (
191-
<ReservationInfoCard reservation={reservation} type="confirmed" />
192-
)
193-
);
194-
}, [reservation]);
195-
196-
if (!reservation) {
197-
return <Spinner />;
198-
}
199-
200152
const reservationUnit = reservation?.reservationUnit?.[0] ?? null;
201153
const instructions = reservationUnit
202-
? getTranslation(reservationUnit, "reservationCancelledInstructions")
203-
: undefined;
154+
? getTranslationSafe(
155+
reservationUnit,
156+
"reservationCancelledInstructions",
157+
lang
158+
)
159+
: null;
204160

205161
const onSubmit = (formData: { reason?: number; description?: string }) => {
206162
if (!reservation.pk || !formData.reason) {
@@ -225,115 +181,97 @@ function ReservationCancellation(props: Props): JSX.Element {
225181
}
226182
};
227183

184+
const title =
185+
formState === "unsent"
186+
? t("reservations:cancelReservation")
187+
: t("reservations:reservationCancelledTitle");
188+
const ingress =
189+
formState === "unsent"
190+
? t("reservations:cancelReservationBody")
191+
: t("reservations:reservationCancelledBody");
192+
193+
const cancellationTerms =
194+
reservationUnit?.cancellationTerms != null
195+
? getTranslation(reservationUnit?.cancellationTerms, "text")
196+
: null;
197+
228198
return (
229-
<Wrapper>
230-
<Container>
231-
<Columns>
232-
<div>
233-
<JustForDesktop>{bylineContent}</JustForDesktop>
234-
</div>
235-
<div>
236-
<Heading>
237-
{formState === "unsent" ? (
238-
<>
239-
<Title>{t("reservations:cancelReservation")}</Title>
240-
<JustForMobile>{bylineContent}</JustForMobile>
241-
<p>{t("reservations:cancelReservationBody")}</p>
242-
</>
243-
) : (
244-
<>
245-
<Title>{t("reservations:reservationCancelledTitle")}</Title>
246-
<JustForMobile>{bylineContent}</JustForMobile>
247-
<p>{t("reservations:reservationCancelledBody")}</p>
248-
</>
249-
)}
250-
</Heading>
251-
<Content>
252-
<ContentContainer>
253-
{formState === "unsent" ? (
254-
<>
255-
<p>{t("reservations:cancelInfoBody")}</p>
256-
<TermsContainer
257-
showAllLabel={t("reservations:showCancellationTerms")}
258-
showLessLabel={t("reservations:hideCancellationTerms")}
259-
maximumNumber={0}
260-
>
261-
{reservationUnit?.cancellationTerms != null && (
262-
<NotificationBox
263-
heading={t("reservationUnit:cancellationTerms")}
264-
body={
265-
<Sanitize
266-
html={getTranslation(
267-
reservationUnit?.cancellationTerms,
268-
"text"
269-
)}
270-
/>
271-
}
272-
/>
273-
)}
274-
</TermsContainer>
275-
<Form onSubmit={handleSubmit(onSubmit)}>
276-
<Controller
277-
name="reason"
278-
control={control}
279-
render={() => (
280-
<StyledSelect
281-
id="reservation__button--cancel-reason"
282-
label={t("reservations:cancelReason")}
283-
onChange={(val: OptionType) => {
284-
setValue("reason", val.value);
285-
}}
286-
options={[...reasons]}
287-
placeholder={t("common:select")}
288-
value={getSelectedOption(
289-
getValues("reason"),
290-
reasons
291-
)}
292-
required
293-
/>
294-
)}
295-
/>
296-
<Actions>
297-
<BlackButton
298-
variant="secondary"
299-
iconLeft={<IconCross aria-hidden />}
300-
onClick={() => router.back()}
301-
data-testid="reservation-cancel__button--back"
302-
>
303-
{t("reservations:cancelReservationCancellation")}
304-
</BlackButton>
305-
<MediumButton
306-
variant="primary"
307-
type="submit"
308-
disabled={!watch("reason")}
309-
data-testid="reservation-cancel__button--cancel"
310-
isLoading={loading}
311-
>
312-
{t("reservations:cancelReservation")}
313-
</MediumButton>
314-
</Actions>
315-
</Form>
316-
</>
317-
) : (
318-
<>
319-
{formState === "sent" && instructions && (
320-
<Paragraph style={{ margin: "var(--spacing-xl) 0" }}>
321-
{instructions}
322-
</Paragraph>
323-
)}
324-
<ReturnLinkList
325-
apiBaseUrl={apiBaseUrl}
326-
style={{
327-
marginTop: "var(--spacing-3-xl)",
199+
<>
200+
<ReservationPageWrapper>
201+
<HeadingSection>
202+
<Heading>{title}</Heading>
203+
<p>{ingress}</p>
204+
</HeadingSection>
205+
<BylineSection>
206+
<ReservationInfoCard reservation={reservation} type="confirmed" />
207+
</BylineSection>
208+
<ContentSection>
209+
{formState === "unsent" ? (
210+
<>
211+
<p>{t("reservations:cancelInfoBody")}</p>
212+
<TermsContainer
213+
showAllLabel={t("reservations:showCancellationTerms")}
214+
showLessLabel={t("reservations:hideCancellationTerms")}
215+
maximumNumber={0}
216+
>
217+
{cancellationTerms != null && (
218+
<NotificationBox
219+
heading={t("reservationUnit:cancellationTerms")}
220+
body={<Sanitize html={cancellationTerms} />}
221+
/>
222+
)}
223+
</TermsContainer>
224+
<Form onSubmit={handleSubmit(onSubmit)}>
225+
<Controller
226+
name="reason"
227+
control={control}
228+
render={() => (
229+
<StyledSelect
230+
id="reservation__button--cancel-reason"
231+
label={t("reservations:cancelReason")}
232+
onChange={(val: OptionType) => {
233+
setValue("reason", val.value);
328234
}}
235+
options={[...reasons]}
236+
placeholder={t("common:select")}
237+
value={getSelectedOption(getValues("reason"), reasons)}
238+
required
329239
/>
330-
</>
331-
)}
332-
</ContentContainer>
333-
</Content>
334-
</div>
335-
</Columns>
336-
</Container>
240+
)}
241+
/>
242+
<Actions>
243+
<BlackButton
244+
variant="secondary"
245+
iconLeft={<IconCross aria-hidden />}
246+
onClick={() => router.back()}
247+
data-testid="reservation-cancel__button--back"
248+
>
249+
{t("reservations:cancelReservationCancellation")}
250+
</BlackButton>
251+
<MediumButton
252+
variant="primary"
253+
type="submit"
254+
disabled={!watch("reason")}
255+
data-testid="reservation-cancel__button--cancel"
256+
isLoading={loading}
257+
>
258+
{t("reservations:cancelReservation")}
259+
</MediumButton>
260+
</Actions>
261+
</Form>
262+
</>
263+
) : (
264+
<>
265+
{formState === "sent" && instructions && (
266+
<Paragraph style={{ margin: "var(--spacing-xl) 0" }}>
267+
{instructions}
268+
</Paragraph>
269+
)}
270+
<ReturnLinkList apiBaseUrl={apiBaseUrl} />
271+
</>
272+
)}
273+
</ContentSection>
274+
</ReservationPageWrapper>
337275
{errorMsg && (
338276
<Toast
339277
type="error"
@@ -348,8 +286,6 @@ function ReservationCancellation(props: Props): JSX.Element {
348286
{errorMsg}
349287
</Toast>
350288
)}
351-
</Wrapper>
289+
</>
352290
);
353291
}
354-
355-
export default ReservationCancellation;

0 commit comments

Comments
 (0)