Skip to content

Commit 0c2472c

Browse files
authored
Merge pull request #253 from zigzagdev/feat/search-form-region-replace
Align heritage search selectors with domain region and category types
2 parents dce4680 + f8df4d6 commit 0c2472c

10 files changed

Lines changed: 196 additions & 89 deletions

File tree

client/src/app/features/search/containers/__tests__/search-heritage-container.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,9 @@ describe("TopPageContainer", () => {
169169
await waitFor(() => {
170170
expect(lastSubHeaderProps).not.toBeNull();
171171
expect(lastSubHeaderProps!.value).toEqual({
172-
region: "",
172+
region: "Africa",
173173
category: "",
174-
keyword: "",
174+
keyword: "Kyoto",
175175
yearInscribedFrom: "",
176176
yearInscribedTo: "",
177177
});

client/src/app/features/search/containers/search-heritage-form-container.tsx

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,18 @@
11
import { useCallback, useEffect, useMemo, useState } from "react";
22
import { useLocation, useNavigate } from "react-router-dom";
3-
import type { HeritageSearchParams } from "../../../../domain/types";
3+
import {
4+
STUDY_REGIONS,
5+
CATEGORIES,
6+
type Category,
7+
type HeritageSearchParams,
8+
type StudyRegion,
9+
} from "../../../../domain/types";
410
import {
511
parseHeritageSearchParams,
612
serializeHeritageSearchParams,
713
} from "../mapper/search-heritages.params.ts";
8-
import {
9-
HeritageSubHeader,
10-
type SearchValues,
11-
} from "@features/top/components/HeritageSubHeader.tsx";
12-
13-
const REGION_VALUES = [
14-
"Africa",
15-
"Asia",
16-
"Europe",
17-
"North America",
18-
"South America",
19-
"Oceania",
20-
] as const;
21-
22-
type HeritageRegion = (typeof REGION_VALUES)[number];
14+
import { HeritageSubHeader } from "@features/top/components/HeritageSubHeader.tsx";
15+
import type { SearchValues } from "@features/top/components/HeritageSearchForm.tsx";
2316

2417
const toSearchYearOrNull = (value: string): number | null => {
2518
const trimmed = value.trim();
@@ -31,17 +24,27 @@ const toSearchYearOrNull = (value: string): number | null => {
3124
return Math.floor(parsed);
3225
};
3326

34-
const toTrimmedStringOrNull = (value: string): string | null => {
35-
const trimmed = value.trim();
36-
return trimmed === "" ? null : trimmed;
27+
const isStudyRegion = (value: string): value is StudyRegion => {
28+
return (STUDY_REGIONS as readonly string[]).includes(value);
3729
};
3830

39-
const toRegionOrNull = (value: string): HeritageRegion | null => {
40-
const trimmed = value.trim();
31+
const isCategory = (value: string): value is Category => {
32+
return (CATEGORIES as readonly string[]).includes(value);
33+
};
4134

42-
if (trimmed === "") return null;
35+
const toRegionOrNull = (value: StudyRegion | ""): StudyRegion | null => {
36+
if (value === "") return null;
37+
return isStudyRegion(value) ? value : null;
38+
};
4339

44-
return REGION_VALUES.includes(trimmed as HeritageRegion) ? (trimmed as HeritageRegion) : null;
40+
const toCategoryOrNull = (value: Category | ""): Category | null => {
41+
if (value === "") return null;
42+
return isCategory(value) ? value : null;
43+
};
44+
45+
const toKeywordOrNull = (value: string): string | null => {
46+
const trimmed = value.trim();
47+
return trimmed === "" ? null : trimmed;
4548
};
4649

4750
export function SearchHeritageFormContainer() {
@@ -94,8 +97,8 @@ export function SearchHeritageFormContainer() {
9497
const nextParams: HeritageSearchParams = {
9598
...params,
9699
region: toRegionOrNull(merged.region),
97-
category: toTrimmedStringOrNull(merged.category),
98-
search_query: toTrimmedStringOrNull(merged.keyword),
100+
category: toCategoryOrNull(merged.category),
101+
search_query: toKeywordOrNull(merged.keyword),
99102
year_inscribed_from: toSearchYearOrNull(merged.yearInscribedFrom),
100103
year_inscribed_to: toSearchYearOrNull(merged.yearInscribedTo),
101104
current_page: 1,

client/src/app/features/search/containers/search-heritage-result-container.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import { useHeritageSearchQuery } from "../../search/hooks/use-search-heritage-q
1111
import SearchResultsPage from "../components/SearchResultsPage";
1212
import { toWorldHeritageListVm } from "@features/heritages/mappers/to-world-heritage-vm";
1313
import type { Pagination } from "../types";
14-
import { HeritageSubHeader, type SearchValues } from "@features/top/components/HeritageSubHeader";
14+
import { HeritageSubHeader } from "@features/top/components/HeritageSubHeader";
15+
import type { SearchValues } from "@features/top/components/HeritageSearchForm";
1516
import { DEFAULT_HERITAGE_SEARCH_PARAMS as SEARCH_PARAMS } from "../mapper/search-heritage.types";
1617
import type { ApiSearchResponse } from "@features/search/apis/search-api";
1718

client/src/app/features/search/mapper/search-heritages.params.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import type { HeritageSearchParams, IdSortOption, StudyRegion } from "../../../../domain/types.ts";
2-
import { STUDY_REGIONS } from "../../../../domain/types.ts";
1+
import type {
2+
Category,
3+
HeritageSearchParams,
4+
IdSortOption,
5+
StudyRegion,
6+
} from "../../../../domain/types.ts";
7+
import { CATEGORIES, STUDY_REGIONS } from "../../../../domain/types.ts";
38
import { DEFAULT_HERITAGE_SEARCH_PARAMS as defaultSearchParams } from "./search-heritage.types.ts";
49

510
const toNullIfEmpty = (v: string | null): string | null => {
@@ -12,12 +17,16 @@ const toIntOrNull = (v: string | null): number | null => {
1217
if (v == null) return null;
1318
const s = v.trim();
1419
if (s === "") return null;
20+
1521
const n = Number(s);
1622
if (!Number.isFinite(n)) return null;
23+
1724
return Math.floor(n);
1825
};
1926

20-
const clampMin = (n: number, min: number) => (n < min ? min : n);
27+
const clampMin = (n: number, min: number): number => {
28+
return n < min ? min : n;
29+
};
2130

2231
const isStudyRegion = (value: string): value is StudyRegion => {
2332
return STUDY_REGIONS.includes(value as StudyRegion);
@@ -26,16 +35,29 @@ const isStudyRegion = (value: string): value is StudyRegion => {
2635
const toRegionOrNull = (v: string | null): StudyRegion | null => {
2736
const s = toNullIfEmpty(v);
2837
if (s == null) return null;
38+
2939
return isStudyRegion(s) ? s : null;
3040
};
3141

42+
const isCategory = (value: string): value is Category => {
43+
return CATEGORIES.includes(value as Category);
44+
};
45+
46+
const toCategoryOrNull = (v: string | null): Category | null => {
47+
const s = toNullIfEmpty(v);
48+
if (s == null) return null;
49+
50+
return isCategory(s) ? s : null;
51+
};
52+
3253
const isIdSortOption = (value: string): value is IdSortOption => {
3354
return value === "asc" || value === "desc";
3455
};
3556

3657
const toOrderOrNull = (v: string | null): IdSortOption | null => {
3758
const s = toNullIfEmpty(v);
3859
if (s == null) return null;
60+
3961
return isIdSortOption(s) ? s : null;
4062
};
4163

@@ -44,12 +66,16 @@ export function parseHeritageSearchParams(search: string): HeritageSearchParams
4466

4567
const search_query =
4668
toNullIfEmpty(searchParams.get("search_query")) ?? defaultSearchParams.search_query;
69+
4770
const country = toNullIfEmpty(searchParams.get("country")) ?? defaultSearchParams.country;
71+
4872
const region = toRegionOrNull(searchParams.get("region")) ?? defaultSearchParams.region;
49-
const category = toNullIfEmpty(searchParams.get("category")) ?? defaultSearchParams.category;
73+
74+
const category = toCategoryOrNull(searchParams.get("category")) ?? defaultSearchParams.category;
5075

5176
const year_inscribed_from =
5277
toIntOrNull(searchParams.get("year_inscribed_from")) ?? defaultSearchParams.year_inscribed_from;
78+
5379
const year_inscribed_to =
5480
toIntOrNull(searchParams.get("year_inscribed_to")) ?? defaultSearchParams.year_inscribed_to;
5581

@@ -83,17 +109,21 @@ export function serializeHeritageSearchParams(p: HeritageSearchParams): string {
83109

84110
const setStr = (k: string, v: string | null, def: string | null) => {
85111
if (v == null) return;
112+
86113
const s = v.trim();
87114
if (s === "") return;
88115
if (def != null && s === def) return;
116+
89117
searchParams.set(k, s);
90118
};
91119

92120
const setNum = (k: string, v: number | null, def: number | null) => {
93121
if (v == null) return;
94122
if (!Number.isFinite(v)) return;
123+
95124
const i = Math.floor(v);
96125
if (def != null && i === def) return;
126+
97127
searchParams.set(k, String(i));
98128
};
99129

client/src/app/features/top/components/HeritageSearchForm.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { useState } from "react";
22
import { Button } from "@shared/uis/Button.tsx";
33
import SearchIcon from "@mui/icons-material/Search";
4-
import { STUDY_REGIONS, type StudyRegion } from "../../../../domain/types.ts";
4+
import {
5+
CATEGORIES,
6+
STUDY_REGIONS,
7+
type Category,
8+
type StudyRegion,
9+
} from "../../../../domain/types.ts";
510

611
export type SearchValues = {
712
region: StudyRegion | "";
8-
category: string;
13+
category: Category | "";
914
keyword: string;
1015
yearInscribedFrom: string;
1116
yearInscribedTo: string;
@@ -16,7 +21,7 @@ type Props = {
1621
onChange?: (next: SearchValues) => void;
1722
onSubmit?: (next: {
1823
region?: StudyRegion | "";
19-
category?: string;
24+
category?: Category | "";
2025
keyword?: string;
2126
yearInscribedFrom?: string;
2227
yearInscribedTo?: string;
@@ -33,6 +38,15 @@ const toStudyRegionOrEmpty = (value: string): StudyRegion | "" => {
3338
return isStudyRegion(value) ? value : "";
3439
};
3540

41+
const isCategory = (value: string): value is Category => {
42+
return (CATEGORIES as readonly string[]).includes(value);
43+
};
44+
45+
const toCategoryOrEmpty = (value: string): Category | "" => {
46+
if (value === "") return "";
47+
return isCategory(value) ? value : "";
48+
};
49+
3650
function Divider({ hidden }: { hidden?: boolean }) {
3751
return (
3852
<div
@@ -58,7 +72,7 @@ export function HeritageSearchForm({
5872
expandKeywordOnFocus = true,
5973
}: Props) {
6074
const regionOptions: readonly (StudyRegion | "")[] = ["", ...STUDY_REGIONS];
61-
const categoryOptions = ["", "Cultural", "Natural", "Mixed"] as const;
75+
const categoryOptions: readonly (Category | "")[] = ["", ...CATEGORIES];
6276

6377
const [internal, setInternal] = useState<SearchValues>({
6478
region: value?.region ?? "",
@@ -127,7 +141,7 @@ export function HeritageSearchForm({
127141
<FieldLabel title="Category" subtitle="Type" />
128142
<select
129143
value={searchValues.category}
130-
onChange={(e) => set({ category: e.target.value })}
144+
onChange={(e) => set({ category: toCategoryOrEmpty(e.target.value) })}
131145
className="h-10 w-full rounded-xl bg-transparent px-2 text-sm font-semibold text-zinc-900 hover:bg-zinc-50 focus:outline-none"
132146
aria-label="Category"
133147
>

client/src/app/features/top/components/HeritageSubHeader.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
1-
import { HeritageSearchForm } from "./HeritageSearchForm";
2-
import type { StudyRegion } from "../../../../domain/types.ts";
1+
import { HeritageSearchForm, type SearchValues } from "./HeritageSearchForm";
32

4-
export type SearchValues = {
5-
region: StudyRegion | "";
6-
category: string;
7-
keyword: string;
8-
yearInscribedFrom: string;
9-
yearInscribedTo: string;
10-
};
11-
12-
type Props = {
3+
export type Props = {
134
value: SearchValues;
145
onSubmit: (q: Partial<SearchValues>) => void;
156
onChange?: (v: SearchValues) => void;

client/src/app/features/top/components/heritage-detail/HeritageDetailLayout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useState, useEffect } from "react";
22
import type { WorldHeritageDetailVm } from "../../../../../domain/types.ts";
33
import type { Locale } from "../../../../../domain/criteria";
4-
import { HeritageSubHeader, type SearchValues } from "../HeritageSubHeader.tsx";
4+
import { HeritageSubHeader } from "../HeritageSubHeader.tsx";
5+
import { type SearchValues } from "@features/top/components/HeritageSearchForm.tsx";
56
import { HeritageHero } from "./HeritageHero";
67
import { HeritageOverViewSection } from "./HeritageOverviewSection";
78
import { HeritageSidebar } from "./HeritageSidebar";

client/src/app/features/top/components/heritage-detail/HeritageSubHeader.tsx

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import { useMemo, useState } from "react";
22
import { Button } from "@shared/uis/Button";
33
import SearchIcon from "@mui/icons-material/Search";
4+
import { STUDY_REGIONS, CATEGORIES } from "../../../../../domain/types";
5+
import type { Category, StudyRegion } from "../../../../../domain/types";
6+
7+
type SearchQuery = {
8+
region?: StudyRegion;
9+
category?: Category;
10+
keyword?: string;
11+
};
412

513
type Props = {
614
title: string;
7-
onSearch?: (q: { region?: string; category?: string; keyword?: string }) => void;
15+
onSearch?: (q: SearchQuery) => void;
816
onKeywordChange?: (keyword: string) => void;
917
};
1018

@@ -21,19 +29,27 @@ function FieldLabel({ title, subtitle }: { title: string; subtitle: string }) {
2129
);
2230
}
2331

24-
export function HeritageSubHeader({ title, onSearch, onKeywordChange }: Props) {
25-
const regionOptions = useMemo(() => ["", "AFR", "ARB", "APA", "EUR", "LAC"] as const, []);
26-
const categoryOptions = useMemo(() => ["", "Cultural", "Natural", "Mixed"] as const, []);
32+
const isStudyRegion = (value: string): value is StudyRegion => {
33+
return STUDY_REGIONS.includes(value as StudyRegion);
34+
};
35+
36+
const isCategory = (value: string): value is Category => {
37+
return CATEGORIES.includes(value as Category);
38+
};
39+
40+
export function HeritageSubHeader({ title, onSearch, onKeywordChange }: Props): React.JSX.Element {
41+
const regionOptions = useMemo(() => ["", ...STUDY_REGIONS] as const, []);
42+
const categoryOptions = useMemo(() => ["", ...CATEGORIES] as const, []);
2743

28-
const [region, setRegion] = useState("");
29-
const [category, setCategory] = useState("");
44+
const [region, setRegion] = useState<StudyRegion | "">("");
45+
const [category, setCategory] = useState<Category | "">("");
3046
const [keyword, setKeyword] = useState("");
3147

3248
const submit = () => {
3349
onSearch?.({
34-
region: region || undefined,
35-
category: category || undefined,
36-
keyword: keyword.trim() || undefined,
50+
region: region === "" ? undefined : region,
51+
category: category === "" ? undefined : category,
52+
keyword: keyword.trim() === "" ? undefined : keyword.trim(),
3753
});
3854
};
3955

@@ -56,13 +72,16 @@ export function HeritageSubHeader({ title, onSearch, onKeywordChange }: Props) {
5672
<FieldLabel title="Region" subtitle="Area" />
5773
<select
5874
value={region}
59-
onChange={(e) => setRegion(e.target.value)}
75+
onChange={(e) => {
76+
const value = e.target.value;
77+
setRegion(value === "" || isStudyRegion(value) ? value : "");
78+
}}
6079
className="h-10 w-full rounded-xl bg-transparent px-2 text-sm font-semibold text-zinc-900 hover:bg-zinc-50 focus:outline-none"
6180
aria-label="Region"
6281
>
63-
{regionOptions.map((v, i) => (
64-
<option key={`${v || "all"}-${i}`} value={v}>
65-
{v ? v : "All"}
82+
{regionOptions.map((value, index) => (
83+
<option key={`${value || "all"}-${index}`} value={value}>
84+
{value || "All"}
6685
</option>
6786
))}
6887
</select>
@@ -74,13 +93,16 @@ export function HeritageSubHeader({ title, onSearch, onKeywordChange }: Props) {
7493
<FieldLabel title="Category" subtitle="Type" />
7594
<select
7695
value={category}
77-
onChange={(e) => setCategory(e.target.value)}
96+
onChange={(e) => {
97+
const value = e.target.value;
98+
setCategory(value === "" || isCategory(value) ? value : "");
99+
}}
78100
className="h-10 w-full rounded-xl bg-transparent px-2 text-sm font-semibold text-zinc-900 hover:bg-zinc-50 focus:outline-none"
79101
aria-label="Category"
80102
>
81-
{categoryOptions.map((v, i) => (
82-
<option key={`${v || "all"}-${i}`} value={v}>
83-
{v ? v : "All"}
103+
{categoryOptions.map((value, index) => (
104+
<option key={`${value || "all"}-${index}`} value={value}>
105+
{value || "All"}
84106
</option>
85107
))}
86108
</select>

0 commit comments

Comments
 (0)