Skip to content

Commit 7ac638e

Browse files
authored
Merge pull request #260 from zigzagdev/fix/heritages-results-response-without-query
fix: set default region to null in DEFAULT_HERITAGE_SEARCH_PARAMS
2 parents 902d8bc + af0e5b4 commit 7ac638e

4 files changed

Lines changed: 85 additions & 83 deletions

File tree

client/src/app/features/search/apis/search-api.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ export type ApiSearchResponse = {
2828
};
2929
};
3030

31+
export const hasSearchConditions = (params: SearchParams): boolean => {
32+
return !!(
33+
params.keyword?.trim() ||
34+
params.region?.trim() ||
35+
params.category?.trim() ||
36+
params.yearInscribedFrom ||
37+
params.yearInscribedTo
38+
);
39+
};
40+
3141
const normalizeApiBase = (apiBase: string): string => apiBase.replace(/\/+$/, "");
3242

3343
export const createSearchApi = ({ apiBase, fetchImpl = fetch }: SearchApiDeps) => {

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

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
import { useCallback, useEffect, useMemo, useState } from "react";
1+
import { useCallback, useMemo, useState, useEffect } from "react";
22
import { useLocation, useNavigate } from "react-router-dom";
3-
import {
4-
STUDY_REGIONS,
5-
CATEGORIES,
6-
type Category,
7-
type HeritageSearchParams,
8-
type StudyRegion,
9-
} from "../../../../domain/types";
3+
import type { HeritageSearchParams } from "../../../../domain/types";
104
import {
115
parseHeritageSearchParams,
126
serializeHeritageSearchParams,
@@ -17,37 +11,25 @@ import type { SearchValues } from "@features/top/components/HeritageSearchForm.t
1711
const toSearchYearOrNull = (value: string): number | null => {
1812
const trimmed = value.trim();
1913
if (trimmed === "") return null;
20-
2114
const parsed = Number(trimmed);
2215
if (!Number.isFinite(parsed)) return null;
23-
2416
return Math.floor(parsed);
2517
};
2618

27-
const isStudyRegion = (value: string): value is StudyRegion => {
28-
return (STUDY_REGIONS as readonly string[]).includes(value);
29-
};
30-
31-
const isCategory = (value: string): value is Category => {
32-
return (CATEGORIES as readonly string[]).includes(value);
33-
};
34-
35-
const toRegionOrNull = (value: StudyRegion | ""): StudyRegion | null => {
36-
if (value === "") return null;
37-
return isStudyRegion(value) ? value : null;
38-
};
39-
40-
const toCategoryOrNull = (value: Category | ""): Category | null => {
41-
if (value === "") return null;
42-
return isCategory(value) ? value : null;
19+
/** Determine whether any valid search condition exists */
20+
const hasSearchParams = (params: HeritageSearchParams): boolean =>
21+
params.search_query !== null ||
22+
params.region !== null ||
23+
params.category !== null ||
24+
params.year_inscribed_from !== null ||
25+
params.year_inscribed_to !== null;
26+
27+
type Props = {
28+
/** Notify the parent which API should be used: list or search */
29+
onApiModeChange?: (isSearch: boolean) => void;
4330
};
4431

45-
const toKeywordOrNull = (value: string): string | null => {
46-
const trimmed = value.trim();
47-
return trimmed === "" ? null : trimmed;
48-
};
49-
50-
export function SearchHeritageFormContainer() {
32+
export function SearchHeritageFormContainer({ onApiModeChange }: Props) {
5133
const location = useLocation();
5234
const navigate = useNavigate();
5335

@@ -56,6 +38,20 @@ export function SearchHeritageFormContainer() {
5638
[location.search],
5739
);
5840

41+
// If any search parameter exists, we consider it as "search mode". Otherwise, it's "list mode".
42+
const isSearchMode = useMemo(() => hasSearchParams(params), [params]);
43+
44+
useEffect(() => {
45+
onApiModeChange?.(isSearchMode);
46+
}, [isSearchMode, onApiModeChange]);
47+
48+
// If no search condition exists on the results page, redirect to the list page.
49+
useEffect(() => {
50+
if (!isSearchMode && location.pathname === "/heritages/results") {
51+
navigate({ pathname: "/heritages", search: location.search }, { replace: true });
52+
}
53+
}, [isSearchMode, location.pathname, location.search, navigate]);
54+
5955
const valueFromUrl: SearchValues = useMemo(
6056
() => ({
6157
region: params.region ?? "",
@@ -96,25 +92,18 @@ export function SearchHeritageFormContainer() {
9692

9793
const nextParams: HeritageSearchParams = {
9894
...params,
99-
region: toRegionOrNull(merged.region),
100-
category: toCategoryOrNull(merged.category),
101-
search_query: toKeywordOrNull(merged.keyword),
95+
region: (merged.region.trim() || null) as HeritageSearchParams["region"],
96+
category: (merged.category.trim() || null) as HeritageSearchParams["category"],
97+
search_query: merged.keyword.trim() || null,
10298
year_inscribed_from: toSearchYearOrNull(merged.yearInscribedFrom),
10399
year_inscribed_to: toSearchYearOrNull(merged.yearInscribedTo),
104100
current_page: 1,
105101
};
106102

107103
const search = serializeHeritageSearchParams(nextParams);
108-
109-
navigate(
110-
{
111-
pathname: location.pathname,
112-
search,
113-
},
114-
{ replace: false },
115-
);
104+
navigate({ pathname: location.pathname, search }, { replace: false });
116105
},
117-
[draft, location.pathname, navigate, params],
106+
[navigate, location.pathname, params, draft],
118107
);
119108

120109
return <HeritageSubHeader value={draft} onChange={onChange} onSubmit={onSubmit} />;

client/src/app/features/search/mapper/search-heritage.types.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
1-
import {
2-
type HeritageSearchParams,
3-
ID_SORT_OPTIONS,
4-
STUDY_REGIONS,
5-
} from "../../../../domain/types.ts";
1+
import { type HeritageSearchParams, ID_SORT_OPTIONS } from "../../../../domain/types.ts";
62

73
export const DEFAULT_HERITAGE_SEARCH_PARAMS: HeritageSearchParams = {
84
search_query: null,
95
country: null,
10-
region: STUDY_REGIONS[0],
6+
region: null,
117
category: null,
128
year_inscribed_from: null,
139
year_inscribed_to: null,

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

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,12 @@ import { Button } from "@shared/uis/Button";
33
import SearchIcon from "@mui/icons-material/Search";
44
import { STUDY_REGIONS, CATEGORIES } from "../../../../../domain/types";
55
import type { Category, StudyRegion } from "../../../../../domain/types";
6-
7-
type SearchQuery = {
8-
region?: StudyRegion;
9-
category?: Category;
10-
keyword?: string;
11-
};
6+
import type { SearchValues } from "@features/top/components/HeritageSearchForm.tsx";
127

138
type Props = {
14-
title: string;
15-
onSearch?: (q: SearchQuery) => void;
16-
onKeywordChange?: (keyword: string) => void;
9+
value?: SearchValues;
10+
onChange?: (next: SearchValues) => void;
11+
onSubmit?: (query: Partial<SearchValues>) => void;
1712
};
1813

1914
function Divider() {
@@ -37,19 +32,33 @@ const isCategory = (value: string): value is Category => {
3732
return CATEGORIES.includes(value as Category);
3833
};
3934

40-
export function HeritageSubHeader({ title, onSearch, onKeywordChange }: Props): React.JSX.Element {
35+
export function HeritageSubHeader({ value, onChange, onSubmit }: Props): React.JSX.Element {
4136
const regionOptions = useMemo(() => ["", ...STUDY_REGIONS] as const, []);
4237
const categoryOptions = useMemo(() => ["", ...CATEGORIES] as const, []);
4338

44-
const [region, setRegion] = useState<StudyRegion | "">("");
45-
const [category, setCategory] = useState<Category | "">("");
46-
const [keyword, setKeyword] = useState("");
39+
const [internal, setInternal] = useState<SearchValues>({
40+
region: value?.region ?? "",
41+
category: value?.category ?? "",
42+
keyword: value?.keyword ?? "",
43+
yearInscribedFrom: value?.yearInscribedFrom ?? "",
44+
yearInscribedTo: value?.yearInscribedTo ?? "",
45+
});
46+
47+
const current = value ?? internal;
48+
49+
const set = (patch: Partial<SearchValues>) => {
50+
const next: SearchValues = { ...current, ...patch };
51+
if (!value) setInternal(next);
52+
onChange?.(next);
53+
};
4754

4855
const submit = () => {
49-
onSearch?.({
50-
region: region === "" ? undefined : region,
51-
category: category === "" ? undefined : category,
52-
keyword: keyword.trim() === "" ? undefined : keyword.trim(),
56+
onSubmit?.({
57+
region: current.region,
58+
category: current.category,
59+
keyword: current.keyword,
60+
yearInscribedFrom: current.yearInscribedFrom,
61+
yearInscribedTo: current.yearInscribedTo,
5362
});
5463
};
5564

@@ -61,8 +70,6 @@ export function HeritageSubHeader({ title, onSearch, onKeywordChange }: Props):
6170
<form
6271
onSubmit={(e) => {
6372
e.preventDefault();
64-
console.log(title);
65-
onKeywordChange?.(keyword);
6673
submit();
6774
}}
6875
className="rounded-2xl border border-zinc-200 bg-white px-4 py-3 shadow-sm"
@@ -71,17 +78,17 @@ export function HeritageSubHeader({ title, onSearch, onKeywordChange }: Props):
7178
<div className="flex items-center gap-3 md:w-[180px]">
7279
<FieldLabel title="Region" subtitle="Area" />
7380
<select
74-
value={region}
81+
value={current.region}
7582
onChange={(e) => {
76-
const value = e.target.value;
77-
setRegion(value === "" || isStudyRegion(value) ? value : "");
83+
const v = e.target.value;
84+
set({ region: v === "" || isStudyRegion(v) ? v : "" });
7885
}}
7986
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"
8087
aria-label="Region"
8188
>
82-
{regionOptions.map((value, index) => (
83-
<option key={`${value || "all"}-${index}`} value={value}>
84-
{value || "All"}
89+
{regionOptions.map((opt, i) => (
90+
<option key={`${opt || "all"}-${i}`} value={opt}>
91+
{opt || "All"}
8592
</option>
8693
))}
8794
</select>
@@ -92,17 +99,17 @@ export function HeritageSubHeader({ title, onSearch, onKeywordChange }: Props):
9299
<div className="flex items-center gap-3 md:w-[220px]">
93100
<FieldLabel title="Category" subtitle="Type" />
94101
<select
95-
value={category}
102+
value={current.category}
96103
onChange={(e) => {
97-
const value = e.target.value;
98-
setCategory(value === "" || isCategory(value) ? value : "");
104+
const v = e.target.value;
105+
set({ category: v === "" || isCategory(v) ? v : "" });
99106
}}
100107
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"
101108
aria-label="Category"
102109
>
103-
{categoryOptions.map((value, index) => (
104-
<option key={`${value || "all"}-${index}`} value={value}>
105-
{value || "All"}
110+
{categoryOptions.map((opt, i) => (
111+
<option key={`${opt || "all"}-${i}`} value={opt}>
112+
{opt || "All"}
106113
</option>
107114
))}
108115
</select>
@@ -113,8 +120,8 @@ export function HeritageSubHeader({ title, onSearch, onKeywordChange }: Props):
113120
<div className="flex items-center gap-3 md:flex-1">
114121
<FieldLabel title="Keyword" subtitle="Name / Country" />
115122
<input
116-
value={keyword}
117-
onChange={(e) => setKeyword(e.target.value)}
123+
value={current.keyword}
124+
onChange={(e) => set({ keyword: e.target.value })}
118125
placeholder="Search the List"
119126
className="h-10 w-full rounded-xl bg-transparent px-2 text-sm text-zinc-900 placeholder:text-zinc-400 hover:bg-zinc-50 focus:outline-none"
120127
aria-label="Keyword"

0 commit comments

Comments
 (0)