From 8ba6577386a8437f6eb233982ffafd941a89cf3e Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 3 May 2026 20:11:56 +0900 Subject: [PATCH 01/10] feat: thread is_endangered through search domain (types, URL, round-trip) Add is_endangered to HeritageSearchParams and SearchValues, default it to null/false, parse and serialize the URL query param, and round-trip the value through the form / result containers and the detail layout's quick-submit. The form's internal state initialiser, the detail layout's DEFAULT_SEARCH, and the search container test mocks are updated to satisfy the new field. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../search-heritage-container.test.tsx | 4 ++ .../search-heritage-form-container.tsx | 3 + .../search-heritage-result-container.tsx | 6 +- .../__tests__/search-heritages.params.test.ts | 65 +++++++++++++++++++ .../search/mapper/search-heritage.types.ts | 1 + .../search/mapper/search-heritages.params.ts | 16 +++++ .../top/components/HeritageSearchForm.tsx | 1 + .../heritage-detail/HeritageDetailLayout.tsx | 2 + client/src/domain/types.ts | 2 + 9 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 client/src/app/features/search/mapper/__tests__/search-heritages.params.test.ts diff --git a/client/src/app/features/search/containers/__tests__/search-heritage-container.test.tsx b/client/src/app/features/search/containers/__tests__/search-heritage-container.test.tsx index 62a1154..266f0c5 100644 --- a/client/src/app/features/search/containers/__tests__/search-heritage-container.test.tsx +++ b/client/src/app/features/search/containers/__tests__/search-heritage-container.test.tsx @@ -39,6 +39,7 @@ type SearchValues = { keyword: string; yearInscribedFrom: string; yearInscribedTo: string; + isEndangered: boolean; }; type HeritageSubHeaderProps = { @@ -144,6 +145,7 @@ const makeParsedParams = (overrides: Partial = {}): Herita category: null, year_inscribed_from: null, year_inscribed_to: null, + is_endangered: null, current_page: 1, per_page: 30, order: "asc", @@ -203,6 +205,7 @@ describe("TopPageContainer", () => { keyword: "Kyoto", yearInscribedFrom: "", yearInscribedTo: "", + isEndangered: false, }); }); @@ -252,6 +255,7 @@ describe("TopPageContainer", () => { category: "Cultural", year_inscribed_from: 1990, year_inscribed_to: null, + is_endangered: null, current_page: 1, per_page: 30, order: "asc", diff --git a/client/src/app/features/search/containers/search-heritage-form-container.tsx b/client/src/app/features/search/containers/search-heritage-form-container.tsx index 9ea2740..2d3521e 100644 --- a/client/src/app/features/search/containers/search-heritage-form-container.tsx +++ b/client/src/app/features/search/containers/search-heritage-form-container.tsx @@ -36,6 +36,7 @@ const toSearchValues = (params: HeritageSearchParams): SearchValues => ({ keyword: params.search_query ?? "", yearInscribedFrom: params.year_inscribed_from !== null ? String(params.year_inscribed_from) : "", yearInscribedTo: params.year_inscribed_to !== null ? String(params.year_inscribed_to) : "", + isEndangered: params.is_endangered === true, }); export function SearchHeritageFormContainer() { @@ -71,6 +72,7 @@ export function SearchHeritageFormContainer() { keyword: query.keyword ?? draft.keyword, yearInscribedFrom: query.yearInscribedFrom ?? draft.yearInscribedFrom, yearInscribedTo: query.yearInscribedTo ?? draft.yearInscribedTo, + isEndangered: query.isEndangered ?? draft.isEndangered, }; const nextParams: HeritageSearchParams = { @@ -80,6 +82,7 @@ export function SearchHeritageFormContainer() { category: toCategoryOrNull(merged.category), year_inscribed_from: toSearchYearOrNull(merged.yearInscribedFrom), year_inscribed_to: toSearchYearOrNull(merged.yearInscribedTo), + is_endangered: merged.isEndangered ? true : null, current_page: 1, per_page: params.per_page ?? DEFAULT_TOP_PER_PAGE, order: params.order ?? DEFAULT_ORDER, diff --git a/client/src/app/features/search/containers/search-heritage-result-container.tsx b/client/src/app/features/search/containers/search-heritage-result-container.tsx index f81144f..a00ada1 100644 --- a/client/src/app/features/search/containers/search-heritage-result-container.tsx +++ b/client/src/app/features/search/containers/search-heritage-result-container.tsx @@ -50,7 +50,8 @@ const hasSearchParams = (params: HeritageSearchParams): boolean => params.region !== null || params.category !== null || params.year_inscribed_from !== null || - params.year_inscribed_to !== null; + params.year_inscribed_to !== null || + params.is_endangered === true; const toDraftValues = (params: HeritageSearchParams): SearchValues => ({ region: params.region ?? "", @@ -58,6 +59,7 @@ const toDraftValues = (params: HeritageSearchParams): SearchValues => ({ keyword: params.search_query ?? "", yearInscribedFrom: params.year_inscribed_from !== null ? String(params.year_inscribed_from) : "", yearInscribedTo: params.year_inscribed_to !== null ? String(params.year_inscribed_to) : "", + isEndangered: params.is_endangered === true, }); const toSearchParams = (draft: SearchValues): HeritageSearchParams => ({ @@ -67,6 +69,7 @@ const toSearchParams = (draft: SearchValues): HeritageSearchParams => ({ category: draft.category || null, year_inscribed_from: draft.yearInscribedFrom ? Number(draft.yearInscribedFrom) : null, year_inscribed_to: draft.yearInscribedTo ? Number(draft.yearInscribedTo) : null, + is_endangered: draft.isEndangered ? true : null, current_page: 1, }); @@ -76,6 +79,7 @@ const mergeDraft = (currentDraft: SearchValues, partial: Partial): keyword: partial.keyword ?? currentDraft.keyword, yearInscribedFrom: partial.yearInscribedFrom ?? currentDraft.yearInscribedFrom, yearInscribedTo: partial.yearInscribedTo ?? currentDraft.yearInscribedTo, + isEndangered: partial.isEndangered ?? currentDraft.isEndangered, }); function useHeritageSearchDraft(params: HeritageSearchParams) { diff --git a/client/src/app/features/search/mapper/__tests__/search-heritages.params.test.ts b/client/src/app/features/search/mapper/__tests__/search-heritages.params.test.ts new file mode 100644 index 0000000..59aa2c2 --- /dev/null +++ b/client/src/app/features/search/mapper/__tests__/search-heritages.params.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from "@jest/globals"; +import { + parseHeritageSearchParams, + serializeHeritageSearchParams, +} from "../search-heritages.params.ts"; +import { DEFAULT_HERITAGE_SEARCH_PARAMS } from "../search-heritage.types.ts"; +import type { HeritageSearchParams } from "../../../../../domain/types.ts"; + +const baseParams = (overrides: Partial = {}): HeritageSearchParams => ({ + ...DEFAULT_HERITAGE_SEARCH_PARAMS, + ...overrides, +}); + +describe("parseHeritageSearchParams", () => { + it("is_endangered=true is parsed as true", () => { + const params = parseHeritageSearchParams("?is_endangered=true"); + expect(params.is_endangered).toBe(true); + }); + + it("is_endangered=false is treated as no filter (null)", () => { + const params = parseHeritageSearchParams("?is_endangered=false"); + expect(params.is_endangered).toBeNull(); + }); + + it("missing is_endangered defaults to null", () => { + const params = parseHeritageSearchParams("?region=Asia"); + expect(params.is_endangered).toBeNull(); + }); + + it("garbage is_endangered values fall back to null", () => { + const params = parseHeritageSearchParams("?is_endangered=banana"); + expect(params.is_endangered).toBeNull(); + }); +}); + +describe("serializeHeritageSearchParams", () => { + it("emits is_endangered=true when params.is_endangered is true", () => { + const queryString = serializeHeritageSearchParams(baseParams({ is_endangered: true })); + expect(queryString).toContain("is_endangered=true"); + }); + + it("omits is_endangered when null", () => { + const queryString = serializeHeritageSearchParams(baseParams({ is_endangered: null })); + expect(queryString).not.toContain("is_endangered"); + }); + + it("omits is_endangered when false", () => { + const queryString = serializeHeritageSearchParams(baseParams({ is_endangered: false })); + expect(queryString).not.toContain("is_endangered"); + }); +}); + +describe("round-trip", () => { + it("preserves is_endangered=true through serialize -> parse", () => { + const queryString = serializeHeritageSearchParams(baseParams({ is_endangered: true })); + const parsed = parseHeritageSearchParams(queryString); + expect(parsed.is_endangered).toBe(true); + }); + + it("preserves is_endangered=null through serialize -> parse", () => { + const queryString = serializeHeritageSearchParams(baseParams({ is_endangered: null })); + const parsed = parseHeritageSearchParams(queryString); + expect(parsed.is_endangered).toBeNull(); + }); +}); diff --git a/client/src/app/features/search/mapper/search-heritage.types.ts b/client/src/app/features/search/mapper/search-heritage.types.ts index bccf21e..67ed585 100644 --- a/client/src/app/features/search/mapper/search-heritage.types.ts +++ b/client/src/app/features/search/mapper/search-heritage.types.ts @@ -7,6 +7,7 @@ export const DEFAULT_HERITAGE_SEARCH_PARAMS: HeritageSearchParams = { category: null, year_inscribed_from: null, year_inscribed_to: null, + is_endangered: null, current_page: 1, per_page: 30, order: ID_SORT_OPTIONS.ASC, diff --git a/client/src/app/features/search/mapper/search-heritages.params.ts b/client/src/app/features/search/mapper/search-heritages.params.ts index 8ce00d1..10ce251 100644 --- a/client/src/app/features/search/mapper/search-heritages.params.ts +++ b/client/src/app/features/search/mapper/search-heritages.params.ts @@ -58,6 +58,13 @@ const toOrderOrNull = (value: string | null): IdSortOption | null => { return isIdSortOption(trimmed) ? trimmed : null; }; +// "true" だけ true。それ以外 (空 / 不正値 / "false") は null = フィルタなし扱い。 +const toEndangeredOrNull = (value: string | null): boolean | null => { + const trimmed = toNullIfEmpty(value); + if (trimmed == null) return null; + return trimmed === "true" ? true : null; +}; + export function parseHeritageSearchParams(search: string): HeritageSearchParams { const searchParams = new URLSearchParams(search); @@ -88,6 +95,9 @@ export function parseHeritageSearchParams(search: string): HeritageSearchParams const order = toOrderOrNull(searchParams.get("order")) ?? defaultSearchParams.order; + const is_endangered = + toEndangeredOrNull(searchParams.get("is_endangered")) ?? defaultSearchParams.is_endangered; + return { search_query, country, @@ -95,6 +105,7 @@ export function parseHeritageSearchParams(search: string): HeritageSearchParams category, year_inscribed_from, year_inscribed_to, + is_endangered, current_page, per_page, order, @@ -138,6 +149,11 @@ export function serializeHeritageSearchParams(params: HeritageSearchParams): str searchParams.set("order", params.order); } + // 危機遺産: true のときだけ URL に乗せる (false / null は省略 = no filter) + if (params.is_endangered === true) { + searchParams.set("is_endangered", "true"); + } + const queryString = searchParams.toString(); return queryString ? `?${queryString}` : ""; } diff --git a/client/src/app/features/top/components/HeritageSearchForm.tsx b/client/src/app/features/top/components/HeritageSearchForm.tsx index 47bc0dd..d5ddab5 100644 --- a/client/src/app/features/top/components/HeritageSearchForm.tsx +++ b/client/src/app/features/top/components/HeritageSearchForm.tsx @@ -40,6 +40,7 @@ export function HeritageSearchForm({ value, onChange, onSubmit }: Props) { keyword: value?.keyword ?? "", yearInscribedFrom: value?.yearInscribedFrom ?? "", yearInscribedTo: value?.yearInscribedTo ?? "", + isEndangered: value?.isEndangered ?? false, }); const searchValues = value ?? internal; diff --git a/client/src/app/features/top/components/heritage-detail/HeritageDetailLayout.tsx b/client/src/app/features/top/components/heritage-detail/HeritageDetailLayout.tsx index f9cbd06..71b0eb1 100644 --- a/client/src/app/features/top/components/heritage-detail/HeritageDetailLayout.tsx +++ b/client/src/app/features/top/components/heritage-detail/HeritageDetailLayout.tsx @@ -19,6 +19,7 @@ const DEFAULT_SEARCH: SearchValues = { keyword: "", yearInscribedFrom: "", yearInscribedTo: "", + isEndangered: false, }; const formatCriteriaInline = (criteria: string[] | undefined) => @@ -114,6 +115,7 @@ export function HeritageDetailLayout({ item }: { item: WorldHeritageDetailVm }) if (next.category) params.set("category", next.category); if (next.yearInscribedFrom) params.set("year_inscribed_from", next.yearInscribedFrom); if (next.yearInscribedTo) params.set("year_inscribed_to", next.yearInscribedTo); + if (next.isEndangered) params.set("is_endangered", "true"); navigate(`/heritages/results?${params.toString()}`); }; diff --git a/client/src/domain/types.ts b/client/src/domain/types.ts index f874aba..65ae770 100644 --- a/client/src/domain/types.ts +++ b/client/src/domain/types.ts @@ -182,6 +182,7 @@ export interface HeritageSearchParams { category: Category | null; year_inscribed_from: number | null; year_inscribed_to: number | null; + is_endangered: boolean | null; current_page: number; per_page: number; order: IdSortOption | null; @@ -198,4 +199,5 @@ export type SearchValues = { keyword: string; yearInscribedFrom: string; yearInscribedTo: string; + isEndangered: boolean; }; From 8f92ace229a8f5fb825a3a7acef1f0308a7520b4 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 3 May 2026 20:12:07 +0900 Subject: [PATCH 02/10] feat: thread is_endangered through search API and query hook Co-Authored-By: Claude Opus 4.7 (1M context) --- client/src/app/features/search/apis/search-api.ts | 2 ++ .../src/app/features/search/hooks/use-search-heritage-query.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/client/src/app/features/search/apis/search-api.ts b/client/src/app/features/search/apis/search-api.ts index 26c119a..11f66c9 100644 --- a/client/src/app/features/search/apis/search-api.ts +++ b/client/src/app/features/search/apis/search-api.ts @@ -16,6 +16,7 @@ export type SearchParams = { category?: string; yearInscribedFrom?: number; yearInscribedTo?: number; + isEndangered?: boolean; currentPage?: number; perPage?: number; }; @@ -72,6 +73,7 @@ export const createSearchApi = ({ apiBase, fetchImpl = fetch }: SearchApiDeps) = if (category) queryParams.set("category", category); if (yearInscribedFrom) queryParams.set("year_inscribed_from", yearInscribedFrom); if (yearInscribedTo) queryParams.set("year_inscribed_to", yearInscribedTo); + if (params.isEndangered === true) queryParams.set("is_endangered", "true"); if (params.currentPage != null) queryParams.set("current_page", String(params.currentPage)); if (params.perPage != null) queryParams.set("per_page", String(params.perPage)); diff --git a/client/src/app/features/search/hooks/use-search-heritage-query.ts b/client/src/app/features/search/hooks/use-search-heritage-query.ts index ed96cde..f5748d6 100644 --- a/client/src/app/features/search/hooks/use-search-heritage-query.ts +++ b/client/src/app/features/search/hooks/use-search-heritage-query.ts @@ -14,6 +14,7 @@ const toSearchParams = (params: HeritageSearchParams): SearchParams => ({ category: params.category ?? undefined, yearInscribedFrom: params.year_inscribed_from ?? undefined, yearInscribedTo: params.year_inscribed_to ?? undefined, + isEndangered: params.is_endangered === true ? true : undefined, currentPage: params.current_page, perPage: params.per_page, }); From 72abbe8d64c5c31d130b47d6e2fb0db0fbc8bbca Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 3 May 2026 20:13:36 +0900 Subject: [PATCH 03/10] feat: add Endangered checkbox to HeritageSearchForm Co-Authored-By: Claude Opus 4.7 (1M context) --- .../top/components/HeritageSearchForm.tsx | 15 +++++++++++++++ client/src/locals/en/ui.json | 1 + client/src/locals/ja/ui.json | 1 + 3 files changed, 17 insertions(+) diff --git a/client/src/app/features/top/components/HeritageSearchForm.tsx b/client/src/app/features/top/components/HeritageSearchForm.tsx index d5ddab5..78fd169 100644 --- a/client/src/app/features/top/components/HeritageSearchForm.tsx +++ b/client/src/app/features/top/components/HeritageSearchForm.tsx @@ -18,6 +18,7 @@ type Props = { keyword?: string; yearInscribedFrom?: string; yearInscribedTo?: string; + isEndangered?: boolean; }) => void; }; @@ -58,6 +59,7 @@ export function HeritageSearchForm({ value, onChange, onSubmit }: Props) { keyword: searchValues.keyword.trim() || undefined, yearInscribedFrom: searchValues.yearInscribedFrom || undefined, yearInscribedTo: searchValues.yearInscribedTo || undefined, + isEndangered: searchValues.isEndangered || undefined, }); }; @@ -123,6 +125,19 @@ export function HeritageSearchForm({ value, onChange, onSubmit }: Props) { + {/* 危機遺産フラグ */} +
+ +
+ {/* Year + Keyword + Submit */}
{/* Year */} diff --git a/client/src/locals/en/ui.json b/client/src/locals/en/ui.json index 81f8f5e..0b35c71 100644 --- a/client/src/locals/en/ui.json +++ b/client/src/locals/en/ui.json @@ -30,6 +30,7 @@ "reload": "Reload", "sortById": "Sort by ID", "all": "All", + "endangeredOnly": "Endangered only", "keyword": "Keyword", "keywordPlaceholder": "Name / Country", "yearFrom": "From", diff --git a/client/src/locals/ja/ui.json b/client/src/locals/ja/ui.json index 7ba0b60..31f8e4c 100644 --- a/client/src/locals/ja/ui.json +++ b/client/src/locals/ja/ui.json @@ -30,6 +30,7 @@ "reload": "再読み込み", "sortById": "ID で並び替え", "all": "すべて", + "endangeredOnly": "危機遺産のみ表示", "keyword": "キーワード", "keywordPlaceholder": "名称・国名", "yearFrom": "から", From fffbd7fd99a533a109303820b9be251f489e49a4 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 3 May 2026 20:14:01 +0900 Subject: [PATCH 04/10] fix: preserve lang query param in search form submit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../containers/search-heritage-form-container.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/client/src/app/features/search/containers/search-heritage-form-container.tsx b/client/src/app/features/search/containers/search-heritage-form-container.tsx index 2d3521e..27c1e48 100644 --- a/client/src/app/features/search/containers/search-heritage-form-container.tsx +++ b/client/src/app/features/search/containers/search-heritage-form-container.tsx @@ -89,11 +89,19 @@ export function SearchHeritageFormContainer() { country: null, }; - const search = serializeHeritageSearchParams(nextParams); + const baseSearch = serializeHeritageSearchParams(nextParams); + const currentLang = new URLSearchParams(location.search).get("lang"); + const finalParams = new URLSearchParams( + baseSearch.startsWith("?") ? baseSearch.slice(1) : baseSearch, + ); + if (currentLang === "ja") finalParams.set("lang", "ja"); + const finalQueryString = finalParams.toString(); + const search = finalQueryString ? `?${finalQueryString}` : ""; + navigate({ pathname: "/heritages/results", search }, { replace: false }); setDraft(merged); }, - [draft, navigate, params.per_page, params.order], + [draft, navigate, params.per_page, params.order, location.search], ); return ; From d7f1477df86bf84138d8b4268d500f3815bf231f Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 3 May 2026 20:14:55 +0900 Subject: [PATCH 05/10] fix: preserve lang query param across search result navigations Co-Authored-By: Claude Opus 4.7 (1M context) --- .../search-heritage-result-container.tsx | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/client/src/app/features/search/containers/search-heritage-result-container.tsx b/client/src/app/features/search/containers/search-heritage-result-container.tsx index a00ada1..de23a68 100644 --- a/client/src/app/features/search/containers/search-heritage-result-container.tsx +++ b/client/src/app/features/search/containers/search-heritage-result-container.tsx @@ -82,6 +82,15 @@ const mergeDraft = (currentDraft: SearchValues, partial: Partial): isEndangered: partial.isEndangered ?? currentDraft.isEndangered, }); +// 現在の URL に lang=ja があれば、遷移先 search にも持たせる (en はデフォルトなので付けない) +const preserveLang = (nextSearch: string, currentSearch: string): string => { + const currentLang = new URLSearchParams(currentSearch).get("lang"); + if (currentLang !== "ja") return nextSearch; + const params = new URLSearchParams(nextSearch.startsWith("?") ? nextSearch.slice(1) : nextSearch); + params.set("lang", "ja"); + return `?${params.toString()}`; +}; + function useHeritageSearchDraft(params: HeritageSearchParams) { const [draft, setDraft] = React.useState(() => toDraftValues(params)); @@ -125,9 +134,10 @@ export function SearchHeritageResultsContainer(): React.ReactElement { const handleClickItem = React.useCallback( (id: number) => { - navigate(`/heritages/${id}`); + const search = preserveLang("", location.search); + navigate(`/heritages/${id}${search}`); }, - [navigate], + [navigate, location.search], ); const handlePageChange = React.useCallback( @@ -137,7 +147,7 @@ export function SearchHeritageResultsContainer(): React.ReactElement { current_page: page, }; - const search = serializeHeritageSearchParams(nextParams); + const search = preserveLang(serializeHeritageSearchParams(nextParams), location.search); navigate( { @@ -147,14 +157,14 @@ export function SearchHeritageResultsContainer(): React.ReactElement { { replace: false }, ); }, - [navigate, location.pathname, params], + [navigate, location.pathname, location.search, params], ); const handleSubmit = React.useCallback( (partial: Partial) => { const nextDraft = mergeDraft(draft, partial); const nextParams = toSearchParams(nextDraft); - const search = serializeHeritageSearchParams(nextParams); + const search = preserveLang(serializeHeritageSearchParams(nextParams), location.search); navigate( { @@ -166,13 +176,14 @@ export function SearchHeritageResultsContainer(): React.ReactElement { setDraft(nextDraft); }, - [draft, navigate, setDraft], + [draft, navigate, setDraft, location.search], ); // Hooks must be called at the top level before any early returns. const handleBackToAllSites = React.useCallback(() => { - navigate("/heritages", { replace: true }); - }, [navigate]); + const search = preserveLang("", location.search); + navigate(`/heritages${search}`, { replace: true }); + }, [navigate, location.search]); const header = ( From 6929ea57fd2c99718e10c3f2dd1a35ce4022f768 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 3 May 2026 20:16:39 +0900 Subject: [PATCH 06/10] feat: add LocaleToggle and localize SearchResultsPage labels Place a LocaleToggle next to "Back to all sites" so users can switch locales from the search results page itself, and read the button text plus the empty-state message from useText. The h1 brand "Search Results" and the subtitle stay untranslated by design. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../features/search/components/SearchResultsPage.tsx | 10 +++++++--- client/src/locals/en/ui.json | 2 ++ client/src/locals/ja/ui.json | 2 ++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/client/src/app/features/search/components/SearchResultsPage.tsx b/client/src/app/features/search/components/SearchResultsPage.tsx index 911e3fc..5915e8f 100644 --- a/client/src/app/features/search/components/SearchResultsPage.tsx +++ b/client/src/app/features/search/components/SearchResultsPage.tsx @@ -7,6 +7,8 @@ import { HeritageCard } from "@features/top/cards/HeritageCard"; import { Pagination } from "@features/top/components/Pagination.tsx"; import { BreadcrumbList } from "@shared/components/BreadcrumbList.tsx"; import { SearchResultMapComponent } from "@features/search/components/SearchResultMapComponent.tsx"; +import { LocaleToggle } from "@shared/locale/LocaleToggle.tsx"; +import { useText } from "@shared/locale/ui-text.ts"; type Props = { header?: ReactNode; @@ -31,6 +33,7 @@ export default function SearchResultsPage({ onPageChange, onBackToAllSites, }: Props) { + const text = useText(); return (
@@ -69,11 +72,12 @@ export default function SearchResultsPage({ h-9 rounded-xl border border-zinc-200 bg-white px-3 text-xs font-semibold text-zinc-700 shadow-sm transition hover:bg-zinc-50 " - aria-label="Back to all sites" + aria-label={text.backToAllSites} > - Back to all sites + {text.backToAllSites} ) : null} +
@@ -87,7 +91,7 @@ export default function SearchResultsPage({ {items.length === 0 ? (
-

No sites found.

+

{text.noSitesFound}

) : (
    diff --git a/client/src/locals/en/ui.json b/client/src/locals/en/ui.json index 0b35c71..9986355 100644 --- a/client/src/locals/en/ui.json +++ b/client/src/locals/en/ui.json @@ -31,6 +31,8 @@ "sortById": "Sort by ID", "all": "All", "endangeredOnly": "Endangered only", + "backToAllSites": "Back to all sites", + "noSitesFound": "No sites found.", "keyword": "Keyword", "keywordPlaceholder": "Name / Country", "yearFrom": "From", diff --git a/client/src/locals/ja/ui.json b/client/src/locals/ja/ui.json index 31f8e4c..7490e36 100644 --- a/client/src/locals/ja/ui.json +++ b/client/src/locals/ja/ui.json @@ -31,6 +31,8 @@ "sortById": "ID で並び替え", "all": "すべて", "endangeredOnly": "危機遺産のみ表示", + "backToAllSites": "一覧に戻る", + "noSitesFound": "該当する遺産はありません。", "keyword": "キーワード", "keywordPlaceholder": "名称・国名", "yearFrom": "から", From 3dbb2f018c609d118db6901c018be201e3b24e71 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 3 May 2026 20:30:31 +0900 Subject: [PATCH 07/10] fix: add is_endangered field to remaining SearchValues / HeritageSearchParams literals Two literals in test code and the detail-side HeritageSubHeader were missed when is_endangered was added to the types, breaking CI typecheck. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../search/hooks/__tests__/use-search-heritage-query-test.ts | 1 + .../top/components/heritage-detail/HeritageSubHeader.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/client/src/app/features/search/hooks/__tests__/use-search-heritage-query-test.ts b/client/src/app/features/search/hooks/__tests__/use-search-heritage-query-test.ts index b783b1a..a0cb8c6 100644 --- a/client/src/app/features/search/hooks/__tests__/use-search-heritage-query-test.ts +++ b/client/src/app/features/search/hooks/__tests__/use-search-heritage-query-test.ts @@ -101,6 +101,7 @@ const makeParams = (overrides: Partial = {}): HeritageSear category: null, year_inscribed_from: null, year_inscribed_to: null, + is_endangered: null, current_page: 1, per_page: 30, order: null, diff --git a/client/src/app/features/top/components/heritage-detail/HeritageSubHeader.tsx b/client/src/app/features/top/components/heritage-detail/HeritageSubHeader.tsx index f855d6d..223f731 100644 --- a/client/src/app/features/top/components/heritage-detail/HeritageSubHeader.tsx +++ b/client/src/app/features/top/components/heritage-detail/HeritageSubHeader.tsx @@ -30,6 +30,7 @@ export function HeritageSubHeader({ value, onChange, onSubmit }: Props): React.J keyword: value?.keyword ?? "", yearInscribedFrom: value?.yearInscribedFrom ?? "", yearInscribedTo: value?.yearInscribedTo ?? "", + isEndangered: value?.isEndangered ?? false, }); const current = value ?? internal; From 3535b310323ba72bbdc619ef1a989e677f87db77 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 3 May 2026 22:26:21 +0900 Subject: [PATCH 08/10] feat: thread criteria multi-select through search domain (types, URL, round-trip) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add criteria: CriteriaCode[] to HeritageSearchParams and SearchValues, default it to [], and parse/serialize the URL as a comma-separated list (criteria=ii,iv) — invalid codes are dropped, duplicates collapsed, and the resulting array is sorted in CRITERIA canonical order. The form / result containers, detail layout, both HeritageSubHeaders, and the test helpers are updated to satisfy the new field. Empty array stays out of the URL. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../search-heritage-container.test.tsx | 4 ++ .../search-heritage-form-container.tsx | 3 + .../search-heritage-result-container.tsx | 6 +- .../use-search-heritage-query-test.ts | 1 + .../__tests__/search-heritages.params.test.ts | 59 +++++++++++++++++++ .../search/mapper/search-heritage.types.ts | 1 + .../search/mapper/search-heritages.params.ts | 25 +++++++- .../top/components/HeritageSearchForm.tsx | 1 + .../heritage-detail/HeritageDetailLayout.tsx | 2 + .../heritage-detail/HeritageSubHeader.tsx | 1 + client/src/domain/types.ts | 2 + 11 files changed, 103 insertions(+), 2 deletions(-) diff --git a/client/src/app/features/search/containers/__tests__/search-heritage-container.test.tsx b/client/src/app/features/search/containers/__tests__/search-heritage-container.test.tsx index 266f0c5..88eb303 100644 --- a/client/src/app/features/search/containers/__tests__/search-heritage-container.test.tsx +++ b/client/src/app/features/search/containers/__tests__/search-heritage-container.test.tsx @@ -40,6 +40,7 @@ type SearchValues = { yearInscribedFrom: string; yearInscribedTo: string; isEndangered: boolean; + criteria: string[]; }; type HeritageSubHeaderProps = { @@ -146,6 +147,7 @@ const makeParsedParams = (overrides: Partial = {}): Herita year_inscribed_from: null, year_inscribed_to: null, is_endangered: null, + criteria: [], current_page: 1, per_page: 30, order: "asc", @@ -206,6 +208,7 @@ describe("TopPageContainer", () => { yearInscribedFrom: "", yearInscribedTo: "", isEndangered: false, + criteria: [], }); }); @@ -256,6 +259,7 @@ describe("TopPageContainer", () => { year_inscribed_from: 1990, year_inscribed_to: null, is_endangered: null, + criteria: [], current_page: 1, per_page: 30, order: "asc", diff --git a/client/src/app/features/search/containers/search-heritage-form-container.tsx b/client/src/app/features/search/containers/search-heritage-form-container.tsx index 27c1e48..1d1ee6c 100644 --- a/client/src/app/features/search/containers/search-heritage-form-container.tsx +++ b/client/src/app/features/search/containers/search-heritage-form-container.tsx @@ -37,6 +37,7 @@ const toSearchValues = (params: HeritageSearchParams): SearchValues => ({ yearInscribedFrom: params.year_inscribed_from !== null ? String(params.year_inscribed_from) : "", yearInscribedTo: params.year_inscribed_to !== null ? String(params.year_inscribed_to) : "", isEndangered: params.is_endangered === true, + criteria: params.criteria, }); export function SearchHeritageFormContainer() { @@ -73,6 +74,7 @@ export function SearchHeritageFormContainer() { yearInscribedFrom: query.yearInscribedFrom ?? draft.yearInscribedFrom, yearInscribedTo: query.yearInscribedTo ?? draft.yearInscribedTo, isEndangered: query.isEndangered ?? draft.isEndangered, + criteria: query.criteria ?? draft.criteria, }; const nextParams: HeritageSearchParams = { @@ -83,6 +85,7 @@ export function SearchHeritageFormContainer() { year_inscribed_from: toSearchYearOrNull(merged.yearInscribedFrom), year_inscribed_to: toSearchYearOrNull(merged.yearInscribedTo), is_endangered: merged.isEndangered ? true : null, + criteria: merged.criteria, current_page: 1, per_page: params.per_page ?? DEFAULT_TOP_PER_PAGE, order: params.order ?? DEFAULT_ORDER, diff --git a/client/src/app/features/search/containers/search-heritage-result-container.tsx b/client/src/app/features/search/containers/search-heritage-result-container.tsx index de23a68..41a5c52 100644 --- a/client/src/app/features/search/containers/search-heritage-result-container.tsx +++ b/client/src/app/features/search/containers/search-heritage-result-container.tsx @@ -51,7 +51,8 @@ const hasSearchParams = (params: HeritageSearchParams): boolean => params.category !== null || params.year_inscribed_from !== null || params.year_inscribed_to !== null || - params.is_endangered === true; + params.is_endangered === true || + params.criteria.length > 0; const toDraftValues = (params: HeritageSearchParams): SearchValues => ({ region: params.region ?? "", @@ -60,6 +61,7 @@ const toDraftValues = (params: HeritageSearchParams): SearchValues => ({ yearInscribedFrom: params.year_inscribed_from !== null ? String(params.year_inscribed_from) : "", yearInscribedTo: params.year_inscribed_to !== null ? String(params.year_inscribed_to) : "", isEndangered: params.is_endangered === true, + criteria: params.criteria, }); const toSearchParams = (draft: SearchValues): HeritageSearchParams => ({ @@ -70,6 +72,7 @@ const toSearchParams = (draft: SearchValues): HeritageSearchParams => ({ year_inscribed_from: draft.yearInscribedFrom ? Number(draft.yearInscribedFrom) : null, year_inscribed_to: draft.yearInscribedTo ? Number(draft.yearInscribedTo) : null, is_endangered: draft.isEndangered ? true : null, + criteria: draft.criteria, current_page: 1, }); @@ -80,6 +83,7 @@ const mergeDraft = (currentDraft: SearchValues, partial: Partial): yearInscribedFrom: partial.yearInscribedFrom ?? currentDraft.yearInscribedFrom, yearInscribedTo: partial.yearInscribedTo ?? currentDraft.yearInscribedTo, isEndangered: partial.isEndangered ?? currentDraft.isEndangered, + criteria: partial.criteria ?? currentDraft.criteria, }); // 現在の URL に lang=ja があれば、遷移先 search にも持たせる (en はデフォルトなので付けない) diff --git a/client/src/app/features/search/hooks/__tests__/use-search-heritage-query-test.ts b/client/src/app/features/search/hooks/__tests__/use-search-heritage-query-test.ts index a0cb8c6..e38eb43 100644 --- a/client/src/app/features/search/hooks/__tests__/use-search-heritage-query-test.ts +++ b/client/src/app/features/search/hooks/__tests__/use-search-heritage-query-test.ts @@ -102,6 +102,7 @@ const makeParams = (overrides: Partial = {}): HeritageSear year_inscribed_from: null, year_inscribed_to: null, is_endangered: null, + criteria: [], current_page: 1, per_page: 30, order: null, diff --git a/client/src/app/features/search/mapper/__tests__/search-heritages.params.test.ts b/client/src/app/features/search/mapper/__tests__/search-heritages.params.test.ts index 59aa2c2..a86d26b 100644 --- a/client/src/app/features/search/mapper/__tests__/search-heritages.params.test.ts +++ b/client/src/app/features/search/mapper/__tests__/search-heritages.params.test.ts @@ -63,3 +63,62 @@ describe("round-trip", () => { expect(parsed.is_endangered).toBeNull(); }); }); + +describe("parseHeritageSearchParams (criteria)", () => { + it("parses comma-separated criteria values", () => { + const params = parseHeritageSearchParams("?criteria=ii,iv"); + expect(params.criteria).toStrictEqual(["ii", "iv"]); + }); + + it("missing criteria defaults to []", () => { + const params = parseHeritageSearchParams("?region=Asia"); + expect(params.criteria).toStrictEqual([]); + }); + + it("empty criteria value defaults to []", () => { + const params = parseHeritageSearchParams("?criteria="); + expect(params.criteria).toStrictEqual([]); + }); + + it("filters out invalid codes", () => { + const params = parseHeritageSearchParams("?criteria=ii,xx,iv"); + expect(params.criteria).toStrictEqual(["ii", "iv"]); + }); + + it("dedupes and sorts by CRITERIA order", () => { + const params = parseHeritageSearchParams("?criteria=v,ii,ii,i"); + expect(params.criteria).toStrictEqual(["i", "ii", "v"]); + }); +}); + +describe("serializeHeritageSearchParams (criteria)", () => { + it("emits criteria as comma-separated when non-empty", () => { + const queryString = serializeHeritageSearchParams(baseParams({ criteria: ["ii", "iv"] })); + expect(queryString).toContain("criteria=ii%2Civ"); + }); + + it("omits criteria when empty", () => { + const queryString = serializeHeritageSearchParams(baseParams({ criteria: [] })); + expect(queryString).not.toContain("criteria"); + }); +}); + +describe("round-trip (criteria)", () => { + it("preserves single value", () => { + const queryString = serializeHeritageSearchParams(baseParams({ criteria: ["iii"] })); + const parsed = parseHeritageSearchParams(queryString); + expect(parsed.criteria).toStrictEqual(["iii"]); + }); + + it("preserves multiple values in canonical order", () => { + const queryString = serializeHeritageSearchParams(baseParams({ criteria: ["v", "ii", "i"] })); + const parsed = parseHeritageSearchParams(queryString); + expect(parsed.criteria).toStrictEqual(["i", "ii", "v"]); + }); + + it("preserves empty array", () => { + const queryString = serializeHeritageSearchParams(baseParams({ criteria: [] })); + const parsed = parseHeritageSearchParams(queryString); + expect(parsed.criteria).toStrictEqual([]); + }); +}); diff --git a/client/src/app/features/search/mapper/search-heritage.types.ts b/client/src/app/features/search/mapper/search-heritage.types.ts index 67ed585..a8daff6 100644 --- a/client/src/app/features/search/mapper/search-heritage.types.ts +++ b/client/src/app/features/search/mapper/search-heritage.types.ts @@ -8,6 +8,7 @@ export const DEFAULT_HERITAGE_SEARCH_PARAMS: HeritageSearchParams = { year_inscribed_from: null, year_inscribed_to: null, is_endangered: null, + criteria: [], current_page: 1, per_page: 30, order: ID_SORT_OPTIONS.ASC, diff --git a/client/src/app/features/search/mapper/search-heritages.params.ts b/client/src/app/features/search/mapper/search-heritages.params.ts index 10ce251..0750718 100644 --- a/client/src/app/features/search/mapper/search-heritages.params.ts +++ b/client/src/app/features/search/mapper/search-heritages.params.ts @@ -1,10 +1,11 @@ import type { Category, + CriteriaCode, HeritageSearchParams, IdSortOption, StudyRegion, } from "../../../../domain/types.ts"; -import { CATEGORIES, STUDY_REGIONS } from "../../../../domain/types.ts"; +import { CATEGORIES, CRITERIA, STUDY_REGIONS } from "../../../../domain/types.ts"; import { DEFAULT_HERITAGE_SEARCH_PARAMS as defaultSearchParams } from "./search-heritage.types.ts"; const toNullIfEmpty = (value: string | null): string | null => { @@ -65,6 +66,20 @@ const toEndangeredOrNull = (value: string | null): boolean | null => { return trimmed === "true" ? true : null; }; +const isCriteriaCode = (value: string): value is CriteriaCode => + (CRITERIA as readonly string[]).includes(value); + +// comma-separated を CriteriaCode[] にデコード。重複除去 + CRITERIA 順に正規化、不正値は捨てる。 +const toCriteriaCodes = (value: string | null): CriteriaCode[] => { + if (value == null) return []; + const parts = value + .split(",") + .map((s) => s.trim()) + .filter((s) => s.length > 0); + const unique = Array.from(new Set(parts)).filter(isCriteriaCode); + return unique.sort((a, b) => CRITERIA.indexOf(a) - CRITERIA.indexOf(b)); +}; + export function parseHeritageSearchParams(search: string): HeritageSearchParams { const searchParams = new URLSearchParams(search); @@ -98,6 +113,8 @@ export function parseHeritageSearchParams(search: string): HeritageSearchParams const is_endangered = toEndangeredOrNull(searchParams.get("is_endangered")) ?? defaultSearchParams.is_endangered; + const criteria = toCriteriaCodes(searchParams.get("criteria")); + return { search_query, country, @@ -106,6 +123,7 @@ export function parseHeritageSearchParams(search: string): HeritageSearchParams year_inscribed_from, year_inscribed_to, is_endangered, + criteria, current_page, per_page, order, @@ -154,6 +172,11 @@ export function serializeHeritageSearchParams(params: HeritageSearchParams): str searchParams.set("is_endangered", "true"); } + // criteria: 非空のときだけ comma-separated で URL に乗せる + if (params.criteria.length > 0) { + searchParams.set("criteria", params.criteria.join(",")); + } + const queryString = searchParams.toString(); return queryString ? `?${queryString}` : ""; } diff --git a/client/src/app/features/top/components/HeritageSearchForm.tsx b/client/src/app/features/top/components/HeritageSearchForm.tsx index 78fd169..8c7bedb 100644 --- a/client/src/app/features/top/components/HeritageSearchForm.tsx +++ b/client/src/app/features/top/components/HeritageSearchForm.tsx @@ -42,6 +42,7 @@ export function HeritageSearchForm({ value, onChange, onSubmit }: Props) { yearInscribedFrom: value?.yearInscribedFrom ?? "", yearInscribedTo: value?.yearInscribedTo ?? "", isEndangered: value?.isEndangered ?? false, + criteria: value?.criteria ?? [], }); const searchValues = value ?? internal; diff --git a/client/src/app/features/top/components/heritage-detail/HeritageDetailLayout.tsx b/client/src/app/features/top/components/heritage-detail/HeritageDetailLayout.tsx index 71b0eb1..5ed6de8 100644 --- a/client/src/app/features/top/components/heritage-detail/HeritageDetailLayout.tsx +++ b/client/src/app/features/top/components/heritage-detail/HeritageDetailLayout.tsx @@ -20,6 +20,7 @@ const DEFAULT_SEARCH: SearchValues = { yearInscribedFrom: "", yearInscribedTo: "", isEndangered: false, + criteria: [], }; const formatCriteriaInline = (criteria: string[] | undefined) => @@ -116,6 +117,7 @@ export function HeritageDetailLayout({ item }: { item: WorldHeritageDetailVm }) if (next.yearInscribedFrom) params.set("year_inscribed_from", next.yearInscribedFrom); if (next.yearInscribedTo) params.set("year_inscribed_to", next.yearInscribedTo); if (next.isEndangered) params.set("is_endangered", "true"); + if (next.criteria.length > 0) params.set("criteria", next.criteria.join(",")); navigate(`/heritages/results?${params.toString()}`); }; diff --git a/client/src/app/features/top/components/heritage-detail/HeritageSubHeader.tsx b/client/src/app/features/top/components/heritage-detail/HeritageSubHeader.tsx index 223f731..5fea2bb 100644 --- a/client/src/app/features/top/components/heritage-detail/HeritageSubHeader.tsx +++ b/client/src/app/features/top/components/heritage-detail/HeritageSubHeader.tsx @@ -31,6 +31,7 @@ export function HeritageSubHeader({ value, onChange, onSubmit }: Props): React.J yearInscribedFrom: value?.yearInscribedFrom ?? "", yearInscribedTo: value?.yearInscribedTo ?? "", isEndangered: value?.isEndangered ?? false, + criteria: value?.criteria ?? [], }); const current = value ?? internal; diff --git a/client/src/domain/types.ts b/client/src/domain/types.ts index 65ae770..17e8179 100644 --- a/client/src/domain/types.ts +++ b/client/src/domain/types.ts @@ -183,6 +183,7 @@ export interface HeritageSearchParams { year_inscribed_from: number | null; year_inscribed_to: number | null; is_endangered: boolean | null; + criteria: CriteriaCode[]; current_page: number; per_page: number; order: IdSortOption | null; @@ -200,4 +201,5 @@ export type SearchValues = { yearInscribedFrom: string; yearInscribedTo: string; isEndangered: boolean; + criteria: CriteriaCode[]; }; From 8a080af1846aae7fa09506a3a4375ce5c005669c Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 3 May 2026 22:28:47 +0900 Subject: [PATCH 09/10] feat: thread criteria through search API and query hook Co-Authored-By: Claude Opus 4.7 (1M context) --- client/src/app/features/search/apis/search-api.ts | 5 +++++ .../app/features/search/hooks/use-search-heritage-query.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/client/src/app/features/search/apis/search-api.ts b/client/src/app/features/search/apis/search-api.ts index 11f66c9..14912aa 100644 --- a/client/src/app/features/search/apis/search-api.ts +++ b/client/src/app/features/search/apis/search-api.ts @@ -1,5 +1,6 @@ import type { ApiWorldHeritageDto, + CriteriaCode, ListResult, Pagination, StudyRegion, @@ -17,6 +18,7 @@ export type SearchParams = { yearInscribedFrom?: number; yearInscribedTo?: number; isEndangered?: boolean; + criteria?: readonly CriteriaCode[]; currentPage?: number; perPage?: number; }; @@ -74,6 +76,9 @@ export const createSearchApi = ({ apiBase, fetchImpl = fetch }: SearchApiDeps) = if (yearInscribedFrom) queryParams.set("year_inscribed_from", yearInscribedFrom); if (yearInscribedTo) queryParams.set("year_inscribed_to", yearInscribedTo); if (params.isEndangered === true) queryParams.set("is_endangered", "true"); + if (params.criteria && params.criteria.length > 0) { + queryParams.set("criteria", params.criteria.join(",")); + } if (params.currentPage != null) queryParams.set("current_page", String(params.currentPage)); if (params.perPage != null) queryParams.set("per_page", String(params.perPage)); diff --git a/client/src/app/features/search/hooks/use-search-heritage-query.ts b/client/src/app/features/search/hooks/use-search-heritage-query.ts index f5748d6..d9d1b28 100644 --- a/client/src/app/features/search/hooks/use-search-heritage-query.ts +++ b/client/src/app/features/search/hooks/use-search-heritage-query.ts @@ -15,6 +15,7 @@ const toSearchParams = (params: HeritageSearchParams): SearchParams => ({ yearInscribedFrom: params.year_inscribed_from ?? undefined, yearInscribedTo: params.year_inscribed_to ?? undefined, isEndangered: params.is_endangered === true ? true : undefined, + criteria: params.criteria.length > 0 ? params.criteria : undefined, currentPage: params.current_page, perPage: params.per_page, }); From bd2c92ca02c32b6880b940bb6c4b4a81680b4087 Mon Sep 17 00:00:00 2001 From: Application-drop-up Date: Sun, 3 May 2026 22:30:28 +0900 Subject: [PATCH 10/10] feat: add Criteria chip group to HeritageSearchForm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render the i–x criteria as multi-toggle chips between the Category row and the Endangered checkbox. Clicking a chip adds or removes the code from the selection (kept in CRITERIA canonical order); the heading reuses the existing text.criteria key. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../top/components/HeritageSearchForm.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/client/src/app/features/top/components/HeritageSearchForm.tsx b/client/src/app/features/top/components/HeritageSearchForm.tsx index 8c7bedb..1c2e454 100644 --- a/client/src/app/features/top/components/HeritageSearchForm.tsx +++ b/client/src/app/features/top/components/HeritageSearchForm.tsx @@ -2,8 +2,10 @@ import { useState } from "react"; import SearchIcon from "@mui/icons-material/Search"; import { CATEGORIES, + CRITERIA, STUDY_REGIONS, type Category, + type CriteriaCode, type SearchValues, type StudyRegion, } from "../../../../domain/types.ts"; @@ -19,6 +21,7 @@ type Props = { yearInscribedFrom?: string; yearInscribedTo?: string; isEndangered?: boolean; + criteria?: CriteriaCode[]; }) => void; }; @@ -53,6 +56,14 @@ export function HeritageSearchForm({ value, onChange, onSubmit }: Props) { onChange?.(next); }; + const toggleCriterion = (code: CriteriaCode) => { + const current = searchValues.criteria; + const next = current.includes(code) + ? current.filter((c) => c !== code) + : [...current, code].sort((a, b) => CRITERIA.indexOf(a) - CRITERIA.indexOf(b)); + set({ criteria: next }); + }; + const submit = () => { onSubmit?.({ region: searchValues.region || undefined, @@ -61,6 +72,7 @@ export function HeritageSearchForm({ value, onChange, onSubmit }: Props) { yearInscribedFrom: searchValues.yearInscribedFrom || undefined, yearInscribedTo: searchValues.yearInscribedTo || undefined, isEndangered: searchValues.isEndangered || undefined, + criteria: searchValues.criteria.length > 0 ? searchValues.criteria : undefined, }); }; @@ -126,6 +138,34 @@ export function HeritageSearchForm({ value, onChange, onSubmit }: Props) { + {/* Criteria チップ (multi-select) */} +
    +
    {text.criteria}
    +
    + {CRITERIA.map((code) => { + const isActive = searchValues.criteria.includes(code); + return ( + + ); + })} +
    +
    + {/* 危機遺産フラグ */}