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

Commit 4439ed2

Browse files
committed
rework (ui): search pages
- reduce duplicated code. - type things properly so it's easier to change. - prefer SSR for queries where applicable. - prefer query params over custom state transfer. - use react-hook-form correctly. - remove weird reset effects.
1 parent 4317d55 commit 4439ed2

21 files changed

Lines changed: 1087 additions & 1307 deletions

File tree

apps/ui/components/application/ApplicationEvent.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
} from "@/modules/util";
2121
import { ApplicationEventSummary } from "./ApplicationEventSummary";
2222
import Accordion from "../common/Accordion";
23-
import { getDurationNumberOptions } from "@/modules/const";
23+
import { getDurationOptions } from "@/modules/const";
2424
import ConfirmationModal, { ModalRef } from "../common/ConfirmationModal";
2525
import { MediumButton } from "@/styles/util";
2626
import { ApplicationFormValues } from "./Form";
@@ -177,6 +177,7 @@ function ApplicationEventInner({
177177
return "";
178178
};
179179

180+
const durationOptions = getDurationOptions(t);
180181
return (
181182
<>
182183
<SubHeadLine>
@@ -219,11 +220,13 @@ function ApplicationEventInner({
219220
rules={{ required: true }}
220221
name={`applicationSections.${index}.ageGroup`}
221222
render={({ field }) => (
222-
<Select<OptionType>
223+
<Select
223224
value={
224225
ageGroupOptions.find((v) => v.value === field.value) ?? null
225226
}
226-
onChange={(v: OptionType) => field.onChange(v.value)}
227+
onChange={(v: (typeof ageGroupOptions)[0]) =>
228+
field.onChange(v.value)
229+
}
227230
required
228231
label={t("application:Page1.ageGroup")}
229232
options={ageGroupOptions}
@@ -237,12 +240,14 @@ function ApplicationEventInner({
237240
rules={{ required: true }}
238241
name={`applicationSections.${index}.purpose`}
239242
render={({ field }) => (
240-
<Select<OptionType>
243+
<Select
241244
label={t("application:Page1.purpose")}
242245
value={
243246
purposeOptions.find((v) => v.value === field.value) ?? null
244247
}
245-
onChange={(v: OptionType) => field.onChange(v.value)}
248+
onChange={(v: (typeof purposeOptions)[0]) =>
249+
field.onChange(v.value)
250+
}
246251
required
247252
options={purposeOptions}
248253
invalid={errors.applicationSections?.[index]?.purpose != null}
@@ -332,6 +337,7 @@ function ApplicationEventInner({
332337
invalid={errors.applicationSections?.[index]?.end != null}
333338
errorText={getTranslatedError("end")}
334339
/>
340+
{/* TODO should use ControlledSelect */}
335341
<Controller
336342
control={control}
337343
name={`applicationSections.${index}.minDuration`}
@@ -340,22 +346,21 @@ function ApplicationEventInner({
340346
<Select
341347
id={field.name}
342348
value={
343-
getDurationNumberOptions().find(
344-
(x) => x.value === field.value
345-
) ?? null
349+
durationOptions.find((x) => x.value === field.value) ?? null
346350
}
347351
placeholder={t("common:select")}
348-
options={getDurationNumberOptions()}
352+
options={durationOptions}
349353
label={t("application:Page1.minDuration")}
350354
required
351-
onChange={(selection: OptionType): void => {
355+
onChange={(selection: (typeof durationOptions)[0]): void => {
352356
field.onChange(selection.value);
353357
}}
354358
invalid={errors.applicationSections?.[index]?.minDuration != null}
355359
error={getTranslatedError("minDuration")}
356360
/>
357361
)}
358362
/>
363+
{/* TODO should use ControlledSelect */}
359364
<Controller
360365
control={control}
361366
name={`applicationSections.${index}.maxDuration`}
@@ -364,15 +369,13 @@ function ApplicationEventInner({
364369
<Select
365370
id={field.name}
366371
value={
367-
getDurationNumberOptions().find(
368-
(x) => x.value === field.value
369-
) ?? null
372+
durationOptions.find((x) => x.value === field.value) ?? null
370373
}
371374
placeholder={t("common:select")}
372-
options={getDurationNumberOptions()}
375+
options={durationOptions}
373376
label={t("application:Page1.maxDuration")}
374377
required
375-
onChange={(selection: OptionType): void => {
378+
onChange={(selection: (typeof durationOptions)[0]): void => {
376379
field.onChange(selection.value);
377380
}}
378381
invalid={errors.applicationSections?.[index]?.maxDuration != null}

apps/ui/components/calendar/ReservationCalendarControls.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Button, IconAngleDown, IconAngleUp, IconCross } from "hds-react";
55
import { maxBy, trim } from "lodash";
66
import { fromUIDate, toUIDate } from "common/src/common/util";
77
import { Transition } from "react-transition-group";
8-
import type { OptionType } from "common/types/common";
98
import {
109
fontBold,
1110
fontMedium,
@@ -23,7 +22,7 @@ import type { SubmitHandler, UseFormReturn } from "react-hook-form";
2322
import type { TimeRange } from "@/components/reservation-unit/QuickReservation";
2423
import { PendingReservationFormType } from "@/components/reservation-unit/schema";
2524
import ControlledDateInput from "@/components/common/ControlledDateInput";
26-
import ControlledSelect from "@/components/common/ControlledSelect";
25+
import { ControlledSelect } from "@/components/common/ControlledSelect";
2726

2827
export type FocusTimeSlot = TimeRange & {
2928
isReservable: boolean;
@@ -41,7 +40,7 @@ type Props = {
4140
date?: string;
4241
time?: string;
4342
}>;
44-
durationOptions: OptionType[];
43+
durationOptions: { label: string; value: number }[];
4544
startingTimeOptions: { label: string; value: string }[];
4645
focusSlot: FocusTimeSlot;
4746
submitReservation: SubmitHandler<PendingReservationFormType>;
Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
11
import React from "react";
22
import { Select } from "hds-react";
33
import { useTranslation } from "next-i18next";
4-
import { Controller, useForm } from "react-hook-form";
5-
import { OptionType } from "common/types/common";
6-
import { getSelectedOption } from "@/modules/util";
4+
import {
5+
type Control,
6+
type FieldValues,
7+
type Path,
8+
useController,
9+
} from "react-hook-form";
710

8-
type Props = {
9-
name: string;
11+
type Props<T extends FieldValues, U> = {
12+
name: Path<T>;
13+
control: Control<T>;
1014
label: string;
11-
control: ReturnType<typeof useForm>["control"];
12-
options: OptionType[];
15+
options: Array<{ label: string; value: U }>;
1316
required?: boolean;
1417
placeholder?: string;
1518
error?: string;
1619
validate?: { [key: string]: (val: string) => boolean };
20+
style?: React.CSSProperties;
21+
className?: string;
22+
clearable?: boolean;
1723
};
18-
const ControlledSelect = ({
24+
25+
export function ControlledSelect<
26+
T extends FieldValues,
27+
U extends number | string,
28+
>({
1929
name,
2030
label,
2131
control,
@@ -24,36 +34,33 @@ const ControlledSelect = ({
2434
error,
2535
placeholder,
2636
validate,
27-
}: Props): JSX.Element => {
37+
style,
38+
className,
39+
clearable,
40+
}: Props<T, U>): JSX.Element {
2841
const { t } = useTranslation();
42+
const {
43+
field: { value, onChange },
44+
} = useController({ name, control, rules: { required, validate } });
45+
46+
const currentValue = options.find((x) => x.value === value) ?? null;
47+
2948
return (
30-
<Controller
31-
control={control}
32-
name={name}
33-
rules={{ required, validate }}
34-
render={({ field }) => {
35-
const currentValue = getSelectedOption(field.value, options) ?? {
36-
label: "",
37-
value: "",
38-
};
39-
return (
40-
<Select
41-
id={name}
42-
value={currentValue}
43-
options={options}
44-
label={label}
45-
required={required ?? false}
46-
onChange={(selection: OptionType): void => {
47-
field.onChange(selection.value);
48-
}}
49-
placeholder={placeholder ?? t("common:select")}
50-
invalid={Boolean(error)}
51-
error={error}
52-
/>
53-
);
49+
<Select
50+
id={name}
51+
style={style}
52+
className={className}
53+
clearable={clearable}
54+
value={currentValue}
55+
options={options}
56+
label={label}
57+
required={required}
58+
onChange={(selection?: (typeof options)[0]): void => {
59+
onChange(selection?.value);
5460
}}
61+
placeholder={placeholder ?? t("common:select")}
62+
invalid={Boolean(error)}
63+
error={error}
5564
/>
5665
);
57-
};
58-
59-
export default ControlledSelect;
66+
}

apps/ui/components/common/Navigation/Navigation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export function Navigation({
8888
// NOTE have to do both, otherwise the i18n and router are out of sync and
8989
// navigation uses translations for the previous language
9090
i18n.changeLanguage(language);
91-
router.replace(router.pathname, router.asPath, {
91+
router.replace({ query: router.query }, router.asPath, {
9292
locale: language,
9393
});
9494
};

apps/ui/components/reservation-unit/Head.tsx

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { IconClock, IconGroup, IconGlyphEuro } from "hds-react";
2-
import React, { useMemo } from "react";
2+
import React from "react";
33
import NextImage from "next/image";
44
import { useTranslation } from "next-i18next";
55
import styled from "styled-components";
@@ -11,8 +11,6 @@ import {
1111
ReservationKind,
1212
type ReservationUnitNode,
1313
} from "common/types/gql-types";
14-
import { omit } from "lodash";
15-
import { useLocalStorage } from "react-use";
1614
import { Container } from "common";
1715
import {
1816
formatDate,
@@ -142,21 +140,15 @@ const NonReservableNotification = ({
142140
);
143141
};
144142

145-
const Head = ({
143+
function Head({
146144
reservationUnit,
147145
// activeOpeningTimes,
148146
reservationUnitIsReservable,
149147
subventionSuffix,
150-
}: PropsType): JSX.Element => {
148+
}: PropsType): JSX.Element {
151149
const { t } = useTranslation();
152150

153-
const storageKey = "reservationUnit-search";
154-
155-
const [storedValues] = useLocalStorage(storageKey, null);
156-
157-
const searchUrlWithParams = useMemo(() => {
158-
return singleSearchUrl(omit(storedValues, "applicationRound"));
159-
}, [storedValues]);
151+
const searchUrl = singleSearchUrl();
160152

161153
const minDur = reservationUnit.minReservationDuration ?? 0;
162154
const maxDur = reservationUnit.maxReservationDuration ?? 0;
@@ -180,9 +172,9 @@ const Head = ({
180172
return (
181173
<>
182174
<BreadcrumbWrapper
183-
route={[searchUrlWithParams, "reservationUnitName"]}
175+
route={[searchUrl, "reservationUnitName"]}
184176
aliases={[
185-
{ slug: searchUrlWithParams, title: t("breadcrumb:search") },
177+
{ slug: searchUrl, title: t("breadcrumb:search") },
186178
{ slug: "reservationUnitName", title: reservationUnitName ?? "-" },
187179
]}
188180
/>
@@ -270,6 +262,6 @@ const Head = ({
270262
</TopContainer>
271263
</>
272264
);
273-
};
265+
}
274266

275267
export default Head;

apps/ui/components/reservation-unit/QuickReservation.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React, { useCallback, useMemo } from "react";
2-
import type { OptionType } from "common/types/common";
32
import { Button } from "hds-react";
43
import { useTranslation } from "next-i18next";
54
import styled from "styled-components";
@@ -14,7 +13,7 @@ import type { FocusTimeSlot } from "@/components/calendar/ReservationCalendarCon
1413
import type { SubmitHandler, UseFormReturn } from "react-hook-form";
1514
import { PendingReservationFormType } from "@/components/reservation-unit/schema";
1615
import ControlledDateInput from "@/components/common/ControlledDateInput";
17-
import ControlledSelect from "@/components/common/ControlledSelect";
16+
import { ControlledSelect } from "@/components/common/ControlledSelect";
1817
import { getSelectedOption } from "@/modules/util";
1918

2019
export type TimeRange = {
@@ -30,7 +29,7 @@ type Props = {
3029
date?: string;
3130
time?: string;
3231
}>;
33-
durationOptions: OptionType[];
32+
durationOptions: { label: string; value: number }[];
3433
startingTimeOptions: { label: string; value: string }[];
3534
focusSlot: FocusTimeSlot | null;
3635
nextAvailableTime: Date | null;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Control, FieldValues, Path, useController } from "react-hook-form";
2+
import { MultiSelectDropdown } from "../form";
3+
4+
export function ControlledMultiSelect<T extends FieldValues>({
5+
name,
6+
control,
7+
options,
8+
label,
9+
}: {
10+
name: Path<T>;
11+
control: Control<T>;
12+
options:
13+
| Array<{ value: string; label: string }>
14+
| Array<{ value: number; label: string }>;
15+
label: string;
16+
}): JSX.Element {
17+
const {
18+
field: { value, onChange },
19+
} = useController({ control, name });
20+
21+
// TODO replace with HDS ComboBox
22+
return (
23+
<MultiSelectDropdown
24+
id={`${name}Filter`}
25+
checkboxName={`${name}Filter`}
26+
name={name}
27+
onChange={(selection): void => {
28+
onChange(selection.filter((n) => n !== "").join(","));
29+
}}
30+
options={options}
31+
showSearch
32+
title={label}
33+
value={value?.split(",") ?? [""]}
34+
/>
35+
);
36+
}

0 commit comments

Comments
 (0)