Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
8672610
perf: create api-cache utility with TTL-based get/set
zigzagdev May 21, 2026
0a5b53d
perf: apply SWR cache to useTopPage — serve stale data instantly, rev…
zigzagdev May 21, 2026
fc910d5
perf: apply SWR cache to useHeritageSearchQuery keyed by serialized s…
zigzagdev May 21, 2026
1fad9ee
perf(card): add isPriority prop — eager loading and fetchpriority for…
zigzagdev May 21, 2026
84bd1c8
perf(list): pass isPriority to first 3 cards in HeritageList
zigzagdev May 21, 2026
eec7294
perf(search): pass isPriority to first 3 cards in SearchResultsPage
zigzagdev May 21, 2026
9c4b92b
perf: create HeritageCardSkeleton component matching thumbnail card d…
zigzagdev May 21, 2026
5b44d66
perf(list): add isLoading prop to HeritageList — show skeleton grid w…
zigzagdev May 21, 2026
726ca6d
perf(search): add isLoading prop to SearchResultsPage — show skeleton…
zigzagdev May 21, 2026
28d0f13
perf(container): remove Loading… early return from TopPageContainer —…
zigzagdev May 21, 2026
a705378
perf(container): pass isLoading to SearchResultsPage in SearchHeritag…
zigzagdev May 21, 2026
065286f
perf(top): lazy-load Map component with React.lazy and Suspense pulse…
zigzagdev May 21, 2026
85a27b0
perf(detail): lazy-load DetailHeritageMap with React.lazy and Suspens…
zigzagdev May 21, 2026
f56d85c
fix(card): remove isPriority/fetchPriority/eager loading — caused 403…
zigzagdev May 22, 2026
0b5d45d
fix(card): revert image dimension attrs added for #332
zigzagdev May 22, 2026
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
15 changes: 14 additions & 1 deletion client/src/app/features/search/components/SearchResultsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { Pagination } from "@features/top/components/Pagination.tsx";
import { BreadcrumbList } from "@shared/components/BreadcrumbList.tsx";
import { LocaleToggle } from "@shared/locale/LocaleToggle.tsx";
import { useText } from "@shared/locale/ui-text.ts";
import { HeritageCardSkeleton } from "@shared/uis/HeritageCardSkeleton.tsx";

const SKELETON_COUNT = 6;

type Props = {
header?: ReactNode;
Expand All @@ -20,6 +23,7 @@ type Props = {
errorMessage?: string;
onPageChange?: (page: number) => void;
onBackToAllSites?: () => void;
isLoading?: boolean;
};

export default function SearchResultsPage({
Expand All @@ -31,6 +35,7 @@ export default function SearchResultsPage({
errorMessage,
onPageChange,
onBackToAllSites,
isLoading = false,
}: Props) {
const text = useText();
return (
Expand Down Expand Up @@ -86,7 +91,15 @@ export default function SearchResultsPage({
<div className="pt-8">
<BreadcrumbList />

{items.length === 0 ? (
{isLoading ? (
<ul className="grid list-none grid-cols-1 gap-6 p-0 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: SKELETON_COUNT }).map((_, i) => (
<li key={i} className="list-none">
<HeritageCardSkeleton />
</li>
))}
</ul>
) : items.length === 0 ? (
<div className="py-20 text-center">
<p className="text-sm text-zinc-600">{text.noSitesFound}</p>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export function SearchHeritageResultsContainer(): React.ReactElement {
};

if (isLoading) {
return <SearchResultsPage {...baseProps} pagination={null} rangeText="Loading…" />;
return <SearchResultsPage {...baseProps} pagination={null} rangeText="" isLoading />;
}

if (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type { HeritageSearchParams } from "../../../../domain/types.ts";
import { fetchSearchHeritagesResult } from "../apis";
import type { SearchParams } from "../apis/search-api";
import type { ApiWorldHeritageDto, ListResult } from "../../../../domain/types";
import { getCached, setCached } from "@shared/cache/api-cache.ts";

const CACHE_MAX_AGE_MS = 60_000;

const isAbortError = (e: unknown): boolean => {
return e instanceof DOMException && e.name === "AbortError";
Expand Down Expand Up @@ -44,12 +47,20 @@ export function useHeritageSearchQuery(
}

const abortController = new AbortController();
const cacheKey = `search:${JSON.stringify(request)}`;
const cached = getCached<ListResult<ApiWorldHeritageDto>>(cacheKey, CACHE_MAX_AGE_MS);

setLoading(true);
if (cached) {
setData(cached);
setLoading(false);
} else {
setLoading(true);
}
setError(null);

fetchSearchHeritagesResult(request, { signal: abortController.signal })
.then((res) => {
setCached(cacheKey, res);
setData(res);
})
.catch((e: unknown) => {
Expand Down
17 changes: 17 additions & 0 deletions client/src/app/features/top/components/HeritageList.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import type { WorldHeritageVm } from "../../../../domain/types.ts";
import { HeritageCard } from "../cards/HeritageCard";
import { HeritageCardSkeleton } from "@shared/uis/HeritageCardSkeleton.tsx";

const SKELETON_COUNT = 6;

export function HeritageList({
items,
onClickItem,
isLoading = false,
}: {
items: ReadonlyArray<WorldHeritageVm>;
onClickItem?: (id: number) => void;
isLoading?: boolean;
}) {
if (isLoading) {
return (
<ul className="grid list-none grid-cols-1 gap-6 p-0 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: SKELETON_COUNT }).map((_, i) => (
<li key={i} className="list-none">
<HeritageCardSkeleton />
</li>
))}
</ul>
);
}

if (items.length === 0) {
return (
<div className="py-20 text-center">
Expand Down
9 changes: 6 additions & 3 deletions client/src/app/features/top/components/TopPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReactNode } from "react";
import { Map } from "./Map.tsx";
import { lazy, Suspense, type ReactNode } from "react";

const Map = lazy(() => import("./Map.tsx").then((m) => ({ default: m.Map })));

export default function TopPage({
hero,
Expand All @@ -23,7 +24,9 @@ export default function TopPage({
<div>{header}</div>

<div className="mt-4">
<Map />
<Suspense fallback={<div className="h-[400px] animate-pulse rounded-xl bg-zinc-100" />}>
<Map />
</Suspense>
</div>

<div className="pt-8">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { useState, useEffect } from "react";
import { useState, useEffect, lazy, Suspense } from "react";
import { useNavigate } from "react-router-dom";
import type { WorldHeritageDetailVm, SearchValues } from "../../../../../domain/types.ts";
import { HeritageSubHeader } from "../HeritageSubHeader.tsx";
import { HeritageHero } from "./HeritageHero";
import { HeritageOverViewSection } from "./HeritageOverviewSection";
import { HeritageSidebar } from "./HeritageSidebar";
import { HeritageGallery } from "./HeritageGallery";
import { DetailHeritageMap } from "@features/top/components/heritage-detail/DetailHeritageMap.tsx";
const DetailHeritageMap = lazy(() =>
import("@features/top/components/heritage-detail/DetailHeritageMap.tsx").then((m) => ({
default: m.DetailHeritageMap,
})),
);
import { textType } from "@shared/styles/typography";
import { useSetBreadcrumbLabel } from "@features/breadcrumbs/BreadCrumbHooks.ts";
import { BreadcrumbList } from "@shared/components/BreadcrumbList.tsx";
Expand Down Expand Up @@ -181,7 +185,9 @@ export function HeritageDetailLayout({ item }: { item: WorldHeritageDetailVm })

{/* Map: mobile only (PC shows in sidebar) */}
<div className="mx-auto w-full max-w-6xl px-4 mt-6 lg:hidden" id="geo-map">
<DetailHeritageMap latitude={item.latitude} longitude={item.longitude} />
<Suspense fallback={<div className="h-64 animate-pulse rounded-xl bg-zinc-100" />}>
<DetailHeritageMap latitude={item.latitude} longitude={item.longitude} />
</Suspense>
</div>

<main className="mx-auto w-full max-w-6xl px-4 pb-16 pt-8">
Expand Down
13 changes: 1 addition & 12 deletions client/src/app/features/top/containers/top-page-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,6 @@ export default function TopPageContainer(): React.ReactElement {

const header = <SearchHeritageFormContainer />;

if (isLoading) {
return (
<>
{header}
<main className="p-6">
<div>Loading…</div>
</main>
</>
);
}

if (isError) {
return (
<>
Expand All @@ -121,7 +110,7 @@ export default function TopPageContainer(): React.ReactElement {
<TopPageTitleBar order={order} onChangeOrder={handleChangeOrder} onReload={reload} />
}
header={header}
content={<HeritageList items={items} onClickItem={handleClickItem} />}
content={<HeritageList items={items} onClickItem={handleClickItem} isLoading={isLoading} />}
pagination={
<TopPagePagination
currentPage={currentPage}
Expand Down
38 changes: 22 additions & 16 deletions client/src/app/features/top/hooks/use-top-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import type {
} from "../../../../domain/types";
import { fetchTopPage } from "@features/top/apis";
import { useLocale } from "@shared/locale/LocaleHooks.ts";
import { getCached, setCached } from "@shared/cache/api-cache.ts";

const CACHE_MAX_AGE_MS = 60_000;

type State = {
data: WorldHeritageVm[];
Expand Down Expand Up @@ -67,11 +70,22 @@ export function useTopPage(args: { currentPage: number; perPage: number; order?:
const abortController = new AbortController();
abortRef.current = abortController;

setState((prev) => ({
...prev,
loading: true,
error: null,
}));
const cacheKey = `top:${targetPage}:${targetPerPage}:${targetOrder}:${locale}`;
const cached = getCached<{ vmList: WorldHeritageVm[]; pagination: Pagination }>(
cacheKey,
CACHE_MAX_AGE_MS,
);

if (cached) {
setState({
data: cached.vmList,
pagination: cached.pagination,
loading: false,
error: null,
});
} else {
setState((prev) => ({ ...prev, loading: true, error: null }));
}

fetchTopPage({
currentPage: targetPage,
Expand All @@ -84,13 +98,9 @@ export function useTopPage(args: { currentPage: number; perPage: number; order?:
if (abortController.signal.aborted) return;

const vmList = toWorldHeritageListVm(res.items, locale);
setCached(cacheKey, { vmList, pagination: res.pagination });

setState({
data: vmList,
pagination: res.pagination,
loading: false,
error: null,
});
setState({ data: vmList, pagination: res.pagination, loading: false, error: null });
})
.catch((e: unknown) => {
if (!mountedRef.current) return;
Expand All @@ -104,11 +114,7 @@ export function useTopPage(args: { currentPage: number; perPage: number; order?:
return;
}

setState((prev) => ({
...prev,
loading: false,
error: e,
}));
setState((prev) => ({ ...prev, loading: false, error: e }));
});
},
[locale],
Expand Down
13 changes: 13 additions & 0 deletions client/src/shared/cache/api-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type Entry<T> = { data: T; at: number };

const store = new Map<string, Entry<unknown>>();

export function getCached<T>(key: string, maxAgeMs: number): T | null {
const entry = store.get(key) as Entry<T> | undefined;
if (!entry || Date.now() - entry.at > maxAgeMs) return null;
return entry.data;
}

export function setCached<T>(key: string, data: T): void {
store.set(key, { data, at: Date.now() });
}
5 changes: 5 additions & 0 deletions client/src/shared/uis/HeritageCardSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function HeritageCardSkeleton() {
return (
<div className="h-64 animate-pulse overflow-hidden rounded-2xl bg-zinc-200 sm:h-72 lg:h-80" />
);
}
Loading