Skip to content

Commit 4e696f2

Browse files
authored
Merge pull request #303 from zigzagdev/chore/refactor-language-ja
Refactor: `locale-aware VM`s, `dictionary-driven view`s, and `TopPage` decomposition
2 parents e01dfed + fc728f7 commit 4e696f2

16 files changed

Lines changed: 366 additions & 234 deletions

File tree

client/src/app/features/search/components/SearchResultsPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Pagination } from "@features/top/components/Pagination.tsx";
88
import { BreadcrumbList } from "@shared/components/BreadcrumbList.tsx";
99
import { SearchResultMapComponent } from "@features/search/components/SearchResultMapComponent.tsx";
1010

11-
export type SearchResultsPageProps = {
11+
type Props = {
1212
header?: ReactNode;
1313
items: WorldHeritageVm[];
1414
pagination: SearchResultsPagination | null;
@@ -30,7 +30,7 @@ export default function SearchResultsPage({
3030
errorMessage,
3131
onPageChange,
3232
onBackToAllSites,
33-
}: SearchResultsPageProps) {
33+
}: Props) {
3434
return (
3535
<main className="mx-auto max-w-7xl px-4 py-12">
3636
<div className="sticky top-0 z-20 -mx-4 border-b border-zinc-200 bg-white/95 px-4 py-3 backdrop-blur">

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

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -56,30 +56,58 @@ jest.mock("@features/top/components/HeritageSubHeader.tsx", () => ({
5656
},
5757
}));
5858

59-
type TopPageProps = {
60-
header: React.ReactNode;
61-
items: unknown[];
62-
onClickItem: (id: number) => void;
63-
onReload: () => void;
64-
currentPage: number;
65-
perPage: number;
59+
type TitleBarProps = {
6660
order: IdSortOption;
6761
onChangeOrder: (order: IdSortOption) => void;
62+
onReload?: () => void;
63+
};
64+
65+
type PaginationProps = {
66+
currentPage: number;
67+
perPage: number;
6868
lastPage: number;
6969
onChangePage: (page: number) => void;
70-
paginationDisabled: boolean;
71-
onChangePerPage: (perPage: number) => void;
72-
perPageOptions: number[];
70+
onChangePerPage?: (perPage: number) => void;
71+
perPageOptions?: readonly number[];
72+
disabled?: boolean;
7373
};
7474

75-
let lastTopPageProps: TopPageProps | null = null;
75+
let lastTitleBarProps: TitleBarProps | null = null;
76+
let lastPaginationProps: PaginationProps | null = null;
77+
78+
jest.mock("@features/top/components/TopPageTitleBar.tsx", () => ({
79+
TopPageTitleBar: (props: TitleBarProps) => {
80+
lastTitleBarProps = props;
81+
return null;
82+
},
83+
}));
84+
85+
jest.mock("@features/top/components/TopPagePagination.tsx", () => ({
86+
TopPagePagination: (props: PaginationProps) => {
87+
lastPaginationProps = props;
88+
return null;
89+
},
90+
}));
91+
92+
jest.mock("@features/top/components/HeritageList.tsx", () => ({
93+
HeritageList: () => null,
94+
}));
7695

7796
jest.mock("@features/top/components/TopPage.tsx", () => ({
7897
__esModule: true,
79-
default: (props: TopPageProps) => {
80-
lastTopPageProps = props;
81-
return <>{props.header}</>;
82-
},
98+
default: (props: {
99+
titleBar: React.ReactNode;
100+
header?: React.ReactNode;
101+
content: React.ReactNode;
102+
pagination?: React.ReactNode;
103+
}) => (
104+
<>
105+
{props.titleBar}
106+
{props.header}
107+
{props.content}
108+
{props.pagination}
109+
</>
110+
),
83111
}));
84112

85113
const parseMock = parseHeritageSearchParams as jest.MockedFunction<
@@ -126,7 +154,8 @@ describe("TopPageContainer", () => {
126154
beforeEach(() => {
127155
jest.clearAllMocks();
128156
lastSubHeaderProps = null;
129-
lastTopPageProps = null;
157+
lastTitleBarProps = null;
158+
lastPaginationProps = null;
130159

131160
currentLocation = location(
132161
"?region=Africa&search_query=Kyoto&current_page=3&per_page=30&order=asc",
@@ -183,10 +212,10 @@ describe("TopPageContainer", () => {
183212
order: "asc",
184213
});
185214

186-
expect(lastTopPageProps).not.toBeNull();
187-
expect(lastTopPageProps!.currentPage).toBe(3);
188-
expect(lastTopPageProps!.perPage).toBe(30);
189-
expect(lastTopPageProps!.order).toBe("asc");
215+
expect(lastPaginationProps).not.toBeNull();
216+
expect(lastPaginationProps!.currentPage).toBe(3);
217+
expect(lastPaginationProps!.perPage).toBe(30);
218+
expect(lastTitleBarProps!.order).toBe("asc");
190219
});
191220

192221
test("onSubmit serialises merged search params and navigates to results", async () => {
@@ -280,10 +309,10 @@ describe("TopPageContainer", () => {
280309

281310
render(<TopPageContainer />);
282311

283-
await waitFor(() => expect(lastTopPageProps).not.toBeNull());
312+
await waitFor(() => expect(lastPaginationProps).not.toBeNull());
284313

285314
act(() => {
286-
lastTopPageProps!.onChangePage(4);
315+
lastPaginationProps!.onChangePage(4);
287316
});
288317

289318
expect(navigateMock).toHaveBeenCalledWith(
@@ -306,10 +335,10 @@ describe("TopPageContainer", () => {
306335

307336
render(<TopPageContainer />);
308337

309-
await waitFor(() => expect(lastTopPageProps).not.toBeNull());
338+
await waitFor(() => expect(lastPaginationProps).not.toBeNull());
310339

311340
act(() => {
312-
lastTopPageProps!.onChangePerPage(50);
341+
lastPaginationProps!.onChangePerPage!(50);
313342
});
314343

315344
expect(navigateMock).toHaveBeenCalledWith(
@@ -332,10 +361,10 @@ describe("TopPageContainer", () => {
332361

333362
render(<TopPageContainer />);
334363

335-
await waitFor(() => expect(lastTopPageProps).not.toBeNull());
364+
await waitFor(() => expect(lastTitleBarProps).not.toBeNull());
336365

337366
act(() => {
338-
lastTopPageProps!.onChangeOrder("desc");
367+
lastTitleBarProps!.onChangeOrder("desc");
339368
});
340369

341370
expect(navigateMock).toHaveBeenCalledWith(
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { WorldHeritageVm } from "../../../../domain/types.ts";
2+
import { HeritageCard } from "../cards/HeritageCard";
3+
4+
export function HeritageList({
5+
items,
6+
onClickItem,
7+
}: {
8+
items: ReadonlyArray<WorldHeritageVm>;
9+
onClickItem?: (id: number) => void;
10+
}) {
11+
if (items.length === 0) {
12+
return (
13+
<div className="py-20 text-center">
14+
<p className="text-sm text-zinc-600">No sites found.</p>
15+
</div>
16+
);
17+
}
18+
19+
return (
20+
<ul className="grid list-none grid-cols-1 gap-6 p-0 md:grid-cols-2 lg:grid-cols-3">
21+
{items.map((it) => (
22+
<li key={it.id} className="list-none">
23+
<HeritageCard item={it} onClickItem={onClickItem} />
24+
</li>
25+
))}
26+
</ul>
27+
);
28+
}

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

Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type SearchValues,
88
type StudyRegion,
99
} from "../../../../domain/types.ts";
10+
import { useText } from "@shared/locale/ui-text.ts";
1011

1112
type Props = {
1213
value?: SearchValues;
@@ -28,24 +29,8 @@ const toCategoryOrEmpty = (value: string): Category | "" => {
2829
return isCategory(value) ? value : "";
2930
};
3031

31-
const REGION_LABELS: Record<StudyRegion | "", string> = {
32-
"": "All",
33-
Africa: "Africa",
34-
Asia: "Asia",
35-
Europe: "Europe",
36-
"North America": "North America",
37-
"South America": "South America",
38-
Oceania: "Oceania",
39-
};
40-
41-
const CATEGORY_LABELS: Record<Category | "", string> = {
42-
"": "All",
43-
Cultural: "Cultural",
44-
Natural: "Natural",
45-
Mixed: "Mixed",
46-
};
47-
4832
export function HeritageSearchForm({ value, onChange, onSubmit }: Props) {
33+
const text = useText();
4934
const regionOptions: readonly (StudyRegion | "")[] = ["", ...STUDY_REGIONS];
5035
const categoryOptions: readonly (Category | "")[] = ["", ...CATEGORIES];
5136

@@ -85,7 +70,7 @@ export function HeritageSearchForm({ value, onChange, onSubmit }: Props) {
8570
>
8671
{/* Region チップ */}
8772
<div className="px-4 pt-3 pb-2 border-b border-zinc-100">
88-
<div className="text-[11px] font-semibold text-zinc-400 mb-2">Region</div>
73+
<div className="text-[11px] font-semibold text-zinc-400 mb-2">{text.region}</div>
8974
<div className="flex flex-wrap gap-2">
9075
{regionOptions.map((opt) => {
9176
const isActive = searchValues.region === opt;
@@ -103,7 +88,7 @@ export function HeritageSearchForm({ value, onChange, onSubmit }: Props) {
10388
}
10489
`}
10590
>
106-
{REGION_LABELS[opt]}
91+
{opt === "" ? text.all : text.regionLabels[opt]}
10792
</button>
10893
);
10994
})}
@@ -112,7 +97,7 @@ export function HeritageSearchForm({ value, onChange, onSubmit }: Props) {
11297

11398
{/* Category チップ */}
11499
<div className="px-4 pt-3 pb-2 border-b border-zinc-100">
115-
<div className="text-[11px] font-semibold text-zinc-400 mb-2">Category</div>
100+
<div className="text-[11px] font-semibold text-zinc-400 mb-2">{text.category}</div>
116101
<div className="flex flex-wrap gap-2">
117102
{categoryOptions.map((opt) => {
118103
const isActive = searchValues.category === opt;
@@ -130,7 +115,7 @@ export function HeritageSearchForm({ value, onChange, onSubmit }: Props) {
130115
}
131116
`}
132117
>
133-
{CATEGORY_LABELS[opt]}
118+
{opt === "" ? text.all : text.categoryLabels[opt]}
134119
</button>
135120
);
136121
})}
@@ -142,40 +127,40 @@ export function HeritageSearchForm({ value, onChange, onSubmit }: Props) {
142127
{/* Year */}
143128
<div className="flex items-center gap-3 px-4 py-3 sm:border-r sm:border-zinc-100 sm:w-[220px] shrink-0">
144129
<div className="flex-1 min-w-0">
145-
<div className="text-[11px] font-semibold text-zinc-500">Year Inscribed</div>
130+
<div className="text-[11px] font-semibold text-zinc-500">{text.yearInscribed}</div>
146131
<div className="flex items-center gap-1">
147132
<input
148133
type="number"
149134
inputMode="numeric"
150135
value={searchValues.yearInscribedFrom}
151136
onChange={(e) => set({ yearInscribedFrom: e.target.value })}
152-
placeholder="From"
137+
placeholder={text.yearFrom}
153138
className="w-full bg-transparent text-sm font-semibold text-zinc-900 placeholder:text-zinc-400 placeholder:font-normal focus:outline-none"
154-
aria-label="Year inscribed from"
139+
aria-label={`${text.yearInscribed} ${text.yearFrom}`}
155140
/>
156141
<span className="text-zinc-300"></span>
157142
<input
158143
type="number"
159144
inputMode="numeric"
160145
value={searchValues.yearInscribedTo}
161146
onChange={(e) => set({ yearInscribedTo: e.target.value })}
162-
placeholder="To"
147+
placeholder={text.yearTo}
163148
className="w-full bg-transparent text-sm font-semibold text-zinc-900 placeholder:text-zinc-400 placeholder:font-normal focus:outline-none"
164-
aria-label="Year inscribed to"
149+
aria-label={`${text.yearInscribed} ${text.yearTo}`}
165150
/>
166151
</div>
167152
</div>
168153
</div>
169154
{/* Keyword + Submit */}
170155
<div className="flex flex-1 items-center gap-3 px-4 py-3">
171156
<div className="flex-1 min-w-0">
172-
<div className="text-[11px] font-semibold text-zinc-500">Keyword</div>
157+
<div className="text-[11px] font-semibold text-zinc-500">{text.keyword}</div>
173158
<input
174159
value={searchValues.keyword}
175160
onChange={(e) => set({ keyword: e.target.value })}
176-
placeholder="Name / Country"
161+
placeholder={text.keywordPlaceholder}
177162
className="w-full bg-transparent text-sm font-semibold text-zinc-900 placeholder:text-zinc-400 placeholder:font-normal focus:outline-none"
178-
aria-label="Keyword"
163+
aria-label={text.keyword}
179164
/>
180165
</div>
181166

@@ -187,10 +172,10 @@ export function HeritageSearchForm({ value, onChange, onSubmit }: Props) {
187172
hover:bg-rose-700 transition-colors
188173
focus:outline-none focus-visible:ring-2 focus-visible:ring-rose-400
189174
"
190-
aria-label="Search"
175+
aria-label={text.search}
191176
>
192177
<SearchIcon fontSize="small" />
193-
<span>Search</span>
178+
<span>{text.search}</span>
194179
</button>
195180
</div>
196181
</div>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { HeritageSearchForm } from "./HeritageSearchForm";
22
import { type SearchValues } from "../../../../domain/types.ts";
33

4-
export type Props = {
4+
type Props = {
55
value: SearchValues;
66
onSubmit: (q: Partial<SearchValues>) => void;
77
onChange?: (v: SearchValues) => void;

0 commit comments

Comments
 (0)