Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions client/src/app/features/breadcrumbs/BreadCrumbHooks.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useContext } from "react";
import BreadcrumbContext from "./BreadCrumbProvider.tsx";

// label を登録する
// register labeling
export const useSetBreadcrumbLabel = () => {
const context = useContext(BreadcrumbContext);
if (!context) throw new Error("useSetBreadcrumbLabel must be used within BreadcrumbProvider");
return context.setLabel;
};

// 全ラベルを取得する
// fetching all labels
export const useBreadcrumbLabels = () => {
const context = useContext(BreadcrumbContext);
if (!context) throw new Error("useBreadcrumbLabels must be used within BreadcrumbProvider");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ export function toWorldHeritageDetailVm(dto: ApiWorldHeritageDetailDto): WorldHe
area_hectares: dto.area_hectares,
buffer_zone_hectares: dto.buffer_zone_hectares,
short_description: dto.short_description,
short_description_jp: dto.short_description_jp,
unesco_site_url: dto.unesco_site_url,
state_party: dto.state_party,
state_party_codes: dto.state_party_codes,
state_parties_meta: dto.state_parties_meta,
thumbnail: dto.thumbnail_url,
} satisfies import("../../../../domain/types.ts").ApiWorldHeritageDto;

// Ensure the reshaped object satisfies ApiWorldHeritageDto at compile time

const base: WorldHeritageVm = toWorldHeritageVm(listDto);

const images: WorldHeritageImageVm[] = dto.images
Expand Down
2 changes: 1 addition & 1 deletion client/src/app/features/map/hooks/region-count.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { RegionCount } from "../../../../domain/types.ts";
type State = {
data: RegionCount[];
isLoading: boolean;
error: Error | null;
error: unknown;
};

export function useRegionCount() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ import "leaflet/dist/leaflet.css";
import { useEffect } from "react";
import type { LatLngBoundsExpression } from "leaflet";

type Props = {
items: WorldHeritageVm[];
};

const CATEGORY_COLOR: Record<string, string> = {
Cultural: "#f59e0b",
Natural: "#22c55e",
Expand Down Expand Up @@ -37,7 +33,7 @@ function FitBounds({ items }: { items: WorldHeritageVm[] }) {
return null;
}

export function SearchResultMapComponent({ items }: Props) {
export function SearchResultMapComponent({ items }: { items: WorldHeritageVm[] }) {
const navigate = useNavigate();

const validItems = items.filter((item) => isValidCoordinate(item.latitude, item.longitude));
Expand Down
12 changes: 4 additions & 8 deletions client/src/app/features/search/components/SearchResultsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import type { ReactNode } from "react";
import type { WorldHeritageVm } from "../../../../domain/types";
import type {
WorldHeritageVm,
Pagination as SearchResultsPagination,
} from "../../../../domain/types";
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";

type SearchResultsPagination = {
current_page: number;
per_page: number;
total: number;
last_page: number;
};

export type SearchResultsPageProps = {
header?: ReactNode;
items: WorldHeritageVm[];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import { useCallback, useMemo, useState, useEffect } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import type { HeritageSearchParams } from "../../../../domain/types";
import type {
Category,
HeritageSearchParams,
IdSortOption,
SearchValues,
StudyRegion,
} from "../../../../domain/types";
import {
parseHeritageSearchParams,
serializeHeritageSearchParams,
} from "../mapper/search-heritages.params.ts";
import { HeritageSubHeader } from "@features/top/components/HeritageSubHeader.tsx";
import type { SearchValues } from "@features/top/components/HeritageSearchForm.tsx";
} from "../mapper/search-heritages.params";
import { DEFAULT_HERITAGE_SEARCH_PARAMS as SEARCH_PARAMS } from "../mapper/search-heritage.types";
import { HeritageSubHeader } from "@features/top/components/HeritageSubHeader";

const DEFAULT_TOP_PER_PAGE = 30;
const DEFAULT_ORDER: IdSortOption = "asc";

const toStudyRegionOrNull = (value: StudyRegion | ""): StudyRegion | null =>
value === "" ? null : value;

const toCategoryOrNull = (value: Category | ""): Category | null => (value === "" ? null : value);

const toSearchYearOrNull = (value: string): number | null => {
const trimmed = value.trim();
Expand All @@ -16,71 +30,40 @@ const toSearchYearOrNull = (value: string): number | null => {
return Math.floor(parsed);
};

/** Determine whether any valid search condition exists */
const hasSearchParams = (params: HeritageSearchParams): boolean =>
params.search_query !== null ||
params.region !== null ||
params.category !== null ||
params.year_inscribed_from !== null ||
params.year_inscribed_to !== null;
const toSearchValues = (params: HeritageSearchParams): SearchValues => ({
region: params.region ?? "",
category: params.category ?? "",
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) : "",
});

type Props = {
/** Notify the parent which API should be used: list or search */
onApiModeChange?: (isSearch: boolean) => void;
};

export function SearchHeritageFormContainer({ onApiModeChange }: Props) {
export function SearchHeritageFormContainer() {
const location = useLocation();
const navigate = useNavigate();

const params: HeritageSearchParams = useMemo(
() => parseHeritageSearchParams(location.search),
[location.search],
);

// If any search parameter exists, we consider it as "search mode". Otherwise, it's "list mode".
const isSearchMode = useMemo(() => hasSearchParams(params), [params]);

useEffect(() => {
onApiModeChange?.(isSearchMode);
}, [isSearchMode, onApiModeChange]);

// If no search condition exists on the results page, redirect to the list page.
useEffect(() => {
if (!isSearchMode && location.pathname === "/heritages/results") {
navigate({ pathname: "/heritages", search: location.search }, { replace: true });
}
}, [isSearchMode, location.pathname, location.search, navigate]);

const valueFromUrl: SearchValues = useMemo(
() => ({
region: params.region ?? "",
category: params.category ?? "",
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) : "",
}),
[
params.region,
params.category,
params.search_query,
params.year_inscribed_from,
params.year_inscribed_to,
],
);
const params: HeritageSearchParams = useMemo(() => {
const parsed = parseHeritageSearchParams(location.search);
return {
...SEARCH_PARAMS,
...parsed,
current_page: parsed.current_page ?? 1,
per_page: parsed.per_page ?? DEFAULT_TOP_PER_PAGE,
order: parsed.order ?? DEFAULT_ORDER,
};
}, [location.search]);

const [draft, setDraft] = useState<SearchValues>(valueFromUrl);
const [draft, setDraft] = useState<SearchValues>(() => toSearchValues(params));

useEffect(() => {
setDraft(valueFromUrl);
}, [valueFromUrl]);
setDraft(toSearchValues(params));
}, [params]);

const onChange = useCallback((next: SearchValues) => {
const handleChange = useCallback((next: SearchValues) => {
setDraft(next);
}, []);

const onSubmit = useCallback(
const handleSubmit = useCallback(
(query: Partial<SearchValues>) => {
const merged: SearchValues = {
region: query.region ?? draft.region,
Expand All @@ -91,20 +74,24 @@ export function SearchHeritageFormContainer({ onApiModeChange }: Props) {
};

const nextParams: HeritageSearchParams = {
...params,
region: (merged.region.trim() || null) as HeritageSearchParams["region"],
category: (merged.category.trim() || null) as HeritageSearchParams["category"],
search_query: merged.keyword.trim() || null,
...SEARCH_PARAMS,
search_query: merged.keyword.trim() === "" ? null : merged.keyword.trim(),
region: toStudyRegionOrNull(merged.region),
category: toCategoryOrNull(merged.category),
year_inscribed_from: toSearchYearOrNull(merged.yearInscribedFrom),
year_inscribed_to: toSearchYearOrNull(merged.yearInscribedTo),
current_page: 1,
per_page: params.per_page ?? DEFAULT_TOP_PER_PAGE,
order: params.order ?? DEFAULT_ORDER,
country: null,
};

const search = serializeHeritageSearchParams(nextParams);
navigate({ pathname: location.pathname, search }, { replace: false });
navigate({ pathname: "/heritages/results", search }, { replace: false });
setDraft(merged);
},
[navigate, location.pathname, params, draft],
[draft, navigate, params.per_page, params.order],
);

return <HeritageSubHeader value={draft} onChange={onChange} onSubmit={onSubmit} />;
return <HeritageSubHeader value={draft} onChange={handleChange} onSubmit={handleSubmit} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import {

import { useHeritageSearchQuery } from "../../search/hooks/use-search-heritage-query";
import SearchResultsPage from "../components/SearchResultsPage";
import type { WorldHeritageVm } from "../../../../domain/types";
import type {
ApiWorldHeritageDto,
Pagination,
SearchValues,
WorldHeritageVm,
} from "../../../../domain/types";
import { toWorldHeritageListVm } from "@features/heritages/mappers/to-world-heritage-vm";
import type { Pagination } from "../types";
import { HeritageSubHeader } from "@features/top/components/HeritageSubHeader";
import type { SearchValues } from "@features/top/components/HeritageSearchForm";
import { DEFAULT_HERITAGE_SEARCH_PARAMS as SEARCH_PARAMS } from "../mapper/search-heritage.types";
import type { ApiSearchResponse } from "@features/search/apis/search-api";

const fmtRangeText = (pagination: Pagination, count: number): string => {
if (count === 0) {
Expand All @@ -33,7 +35,7 @@ const isObject = (value: unknown): value is Record<string, unknown> =>

const isValidListResult = (
value: unknown,
): value is { items: ApiSearchResponse[]; pagination: Pagination } => {
): value is { items: ApiWorldHeritageDto[]; pagination: Pagination } => {
if (!isObject(value)) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const toSearchParams = (params: HeritageSearchParams): SearchParams => ({
});

type Options = {
/** If false, the API call is skipped. Defaults to true. */
/** If false, the API call is skipped. Default is true. */
enabled?: boolean;
};

Expand Down
Loading
Loading