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
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,25 @@ import { HeritageHero } from "./HeritageHero";
import { HeritageOverviewSection } from "./HeritageOverviewSection.tsx";
import { HeritageSidebar } from "./HeritageSidebar.tsx";
import { HeritageGallery } from "./HeritageGallery";
import type { Locale } from "../../../../../domain/criteria.ts";

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript imports should omit .tsx/.ts extensions in non-test runtime code files. The codebase convention is to use imports without extensions for relative paths. Remove the .tsx and .ts extensions from these import statements.

Suggested change
import type { Locale } from "../../../../../domain/criteria.ts";
import type { Locale } from "../../../../../domain/criteria";

Copilot uses AI. Check for mistakes.

type Props = {
item: WorldHeritageDetailVm;
locale: Locale;
};

export function HeritageDetailLayout({ item }: Props) {
export function HeritageDetailLayout({ item, locale }: Props) {
return (
<div className="heritage-detail">
<HeritageHero item={item} />
<HeritageHero item={item} locale={locale} />

<main className="heritage-detail__body">
<div className="heritage-detail__main">
<HeritageOverviewSection item={item} />
<HeritageOverviewSection item={item} locale={locale} />
<HeritageGallery images={item.images} />
</div>

<HeritageSidebar item={item} />
<HeritageSidebar item={item} locale={locale} />
</main>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import type { WorldHeritageDetailVm, WorldHeritageImageVm } from "../../types";
import { HeroImage } from "./HeroImage";
import "./heritage-detail.css";
import { CriteriaTags } from "@shared/uis/CriteriaTags.tsx";
import type { Locale } from "../../../../../domain/criteria.ts";

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript imports should omit .tsx/.ts extensions in non-test runtime code files. The codebase convention is to use imports without extensions for relative paths. Remove the .tsx and .ts extensions from these import statements.

Suggested change
import type { Locale } from "../../../../../domain/criteria.ts";
import type { Locale } from "../../../../../domain/criteria";

Copilot uses AI. Check for mistakes.

type Props = {
item: WorldHeritageDetailVm;
locale: Locale;
};

export function HeritageHero({ item }: Props) {
export function HeritageHero({ item, locale }: Props) {
const primaryImage: WorldHeritageImageVm | undefined =
item.images.find((img) => img.isPrimary) ?? item.images[0];

Expand All @@ -20,7 +23,7 @@ export function HeritageHero({ item }: Props) {
<p className="heritage-detail__subtitle">{item.subtitle}</p>

<div className="heritage-detail__meta-chips">
<span className="chip chip--category">{item.category}</span>
<CriteriaTags criteria={item.criteria} locale={locale} />
{item.stateParty && <span className="chip chip--country">{item.stateParty}</span>}
{item.yearInscribed && (
<span className="chip chip--year">Inscribed: {item.yearInscribed}</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,64 @@
import type { WorldHeritageDetailVm } from "../../types";
import "./heritage-detail.css";
import { HeritageMetadataList } from "./HeritageMetadataList";
import { CriteriaTags } from "@shared/uis/CriteriaTags.tsx";
import type { Locale } from "../../../../../domain/criteria.ts";

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript imports should omit .tsx/.ts extensions in non-test runtime code files. The codebase convention is to use imports without extensions for relative paths. Remove the .tsx and .ts extensions from these import statements.

Suggested change
import type { Locale } from "../../../../../domain/criteria.ts";
import type { Locale } from "../../../../../domain/criteria";

Copilot uses AI. Check for mistakes.

type Props = {
item: WorldHeritageDetailVm;
locale: Locale;
};

export function HeritageOverviewSection({ item }: Props) {
export function HeritageOverviewSection({ item, locale }: Props) {
return (
<section className="heritage-detail__section" aria-labelledby="heritage-overview-heading">
<h2 id="heritage-overview-heading" className="heritage-detail__section-title">
Overview
</h2>

<p className="heritage-detail__description">{item.shortDescription}</p>
{item.shortDescription && (
<p className="heritage-detail__description">{item.shortDescription}</p>
)}

<section className="heritage-detail__subsection" aria-labelledby="heritage-criteria-heading">
<h3 id="heritage-criteria-heading" className="heritage-detail__subsection-title">
Criteria
</h3>

<div className="heritage-detail__section">
<h3 className="heritage-detail__subsection-title">Criteria</h3>
<p className="heritage-detail__text">{item.criteriaText || "—"}</p>
</div>
<div className="heritage-detail__chips">
{item.criteria?.length ? (
<CriteriaTags criteria={item.criteria} locale={locale} />
) : (
<span className="heritage-detail__muted">—</span>
)}
</div>
Comment on lines +28 to +34

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CSS classes 'heritage-detail__chips' and 'heritage-detail__muted' are used but not defined in any CSS file. These classes need to be added to the heritage-detail.css file or the elements should use existing classes. Without these styles, the criteria tags and the fallback dash may not be displayed correctly.

Copilot uses AI. Check for mistakes.
</section>

<section className="heritage-detail__subsection" aria-labelledby="heritage-area-heading">
<h3 id="heritage-area-heading" className="heritage-detail__subsection-title">
Area &amp; Buffer zone
</h3>

<div className="heritage-detail__section">
<h3 className="heritage-detail__subsection-title">Area &amp; Buffer zone</h3>
<HeritageMetadataList
items={[
{ label: "Area", value: item.areaText },
{ label: "Buffer zone", value: item.bufferText },
{ label: "Area", value: item.areaText ?? "—" },
{ label: "Buffer zone", value: item.bufferText ?? "—" },
]}
/>
</div>
</section>

{item.isEndangered && (
<div className="heritage-detail__section heritage-detail__section--danger">
<h3 className="heritage-detail__subsection-title">Danger status</h3>
<section
className="heritage-detail__subsection heritage-detail__subsection--danger"
aria-labelledby="heritage-danger-heading"
>
<h3 id="heritage-danger-heading" className="heritage-detail__subsection-title">
Danger status
</h3>
<p className="heritage-detail__danger-text">
This property is inscribed on the List of World Heritage in Danger.
</p>
</div>
</section>
Comment on lines +23 to +61

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CSS classes 'heritage-detail__subsection' and 'heritage-detail__subsection--danger' are used but not defined in the CSS file. These classes should be added to heritage-detail.css to properly style these sections, or the existing 'heritage-detail__section' and 'heritage-detail__section--danger' classes should be used instead.

Copilot uses AI. Check for mistakes.
)}
</section>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,38 @@
import type { WorldHeritageDetailVm } from "../../types";
import "./heritage-detail.css";
import { HeritageMetadataList } from "./HeritageMetadataList";
import type { Locale } from "../../../../../domain/criteria";

type Props = {
item: WorldHeritageDetailVm;
locale: Locale;
};

const formatCriteriaInline = (criteria: string[] | undefined) =>
criteria?.length ? criteria.map((c) => `(${c})`).join("") : "—";

export function HeritageSidebar({ item }: Props) {

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The locale prop is defined in the Props type but not destructured in the function signature. The function signature should be: export function HeritageSidebar({ item, locale }: Props) to match the Props type. Currently the locale parameter is accepted but never accessible in the function body.

Suggested change
export function HeritageSidebar({ item }: Props) {
export function HeritageSidebar({ item, locale }: Props) {

Copilot uses AI. Check for mistakes.
const items = [
{ label: "Country", value: item.country },
{ label: "Region", value: item.region },
{ label: "Year of inscription", value: item.yearInscribed },
{ label: "Criteria", value: item.criteriaText || "—" },
{ label: "Area", value: item.areaText },
{ label: "Buffer zone", value: item.bufferText },
const facts = [
{ label: "Country", value: item.country ?? "—" },
{ label: "Date of inscription", value: item.yearInscribed ?? "—" },
{ label: "Criteria", value: formatCriteriaInline(item.criteria) },
{ label: "Property", value: item.areaText ?? "—" },
{ label: "Buffer zone", value: item.bufferText ?? "—" },
{
label: "Coordinates",
value:
item.latitude != null && item.longitude != null
? `${item.latitude.toFixed(4)}, ${item.longitude.toFixed(4)}`
? `${item.latitude.toFixed(4)} ${item.longitude.toFixed(4)}`
: "—",
hidden: item.latitude == null && item.longitude == null,
},
] as const;

return (
<aside className="heritage-detail__sidebar" aria-label="Summary">
<div className="heritage-detail__sidebar-card">
<h2 className="heritage-detail__sidebar-title">Info</h2>

<HeritageMetadataList items={items} />
<aside className="heritage-detail__sidebar" aria-label="Facts">
<div className="heritage-detail__facts-card">
<h2 className="heritage-detail__facts-title">Facts</h2>
<HeritageMetadataList items={facts} />

{item.unescoSiteUrl && (
<a
Expand All @@ -42,6 +45,9 @@ export function HeritageSidebar({ item }: Props) {
</a>
)}
</div>
<div className="heritage-detail__map-card" aria-label="Map">

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The aria-label attribute on a div element has no semantic meaning without an appropriate ARIA role. Consider changing this div to a section element or adding role="region" to make the aria-label effective for screen readers.

Suggested change
<div className="heritage-detail__map-card" aria-label="Map">
<div className="heritage-detail__map-card" aria-label="Map" role="region">

Copilot uses AI. Check for mistakes.
<div className="heritage-detail__map-placeholder">Map</div>
</div>
</aside>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,50 @@
color: #b91c1c;
margin: 0.25rem 0 0;
}

.heritage-detail__layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
gap: 24px;
align-items: start;
}

.heritage-detail__sidebar {
position: sticky;
top: 16px;
display: grid;
gap: 16px;
}

.heritage-detail__facts-card,
.heritage-detail__map-card {
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 14px;
background: #fff;
padding: 16px;
}

.heritage-detail__facts-title {
margin: 0 0 12px 0;
font-size: 18px;
font-weight: 800;
}

.heritage-detail__map-placeholder {
height: 220px;
border-radius: 12px;
background: rgba(0, 0, 0, 0.06);
display: grid;
place-items: center;
font-weight: 700;
opacity: 0.8;
}

@media (max-width: 900px) {
.heritage-detail__layout {
grid-template-columns: 1fr;
}
.heritage-detail__sidebar {
position: static;
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { useParams } from "react-router-dom";
import { useMemo } from "react";
import { useParams, useSearchParams } from "react-router-dom";
import { useWorldHeritageDetail } from "../hooks/use-world-heritage-detail";
import { HeritageDetailLayout } from "../components/heritage-detail/HeritageDetailLayout.tsx";
import { HeritageDetailLayout } from "../components/heritage-detail/HeritageDetailLayout";
import { LOCALES, type Locale } from "../../../../domain/criteria";

function resolveLocale(raw: string | null): Locale {
if (!raw) return "en";
return (LOCALES as readonly string[]).includes(raw) ? (raw as Locale) : "en";
}

export function WorldHeritageDetailContainer() {
const params = useParams<{ id: string }>();
const id = params.id ?? null;
const { item, isLoading, isError, error, reload } = useWorldHeritageDetail(id);
const [searchParams] = useSearchParams();
const locale = useMemo(() => resolveLocale(searchParams.get("lang")), [searchParams]);

if (!id) {
return <p>World Heritage id is required.</p>;
}
const { item, isLoading, isError, error, reload } = useWorldHeritageDetail(id);

if (isLoading) {
return <p>Loading…</p>;
}
if (!id) return <p>World Heritage id is required.</p>;
if (isLoading) return <p>Loading…</p>;

if (isError || !item) {
return (
Expand All @@ -27,5 +32,5 @@ export function WorldHeritageDetailContainer() {
);
}

return <HeritageDetailLayout item={item} />;
return <HeritageDetailLayout item={item} locale={locale} />;
}
55 changes: 55 additions & 0 deletions client/src/domain/criteria.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import en from "../locals/en/world-heritage-criteria.json";
import ja from "../locals/ja/world-heritage-criteria.json";

export type CriteriaText = {
title: string;
description: string;
};

export const CRITERIA_CODES = [
"i",
"ii",
"iii",
"iv",
"v",
"vi",
"vii",
"viii",
"ix",
"x",
] as const;

export type CriteriaCode = (typeof CRITERIA_CODES)[number];
export type CriteriaDict = Record<CriteriaCode, CriteriaText>;

export const LOCALES = ["en", "ja"] as const;
export type Locale = (typeof LOCALES)[number];

export const UNESCO_CRITERIA_SOURCE_URL = "https://whc.unesco.org/en/criteria/";
const CRITERIA_BY_LOCALE: Record<Locale, CriteriaDict> = {
en: en as CriteriaDict,
ja: ja as CriteriaDict,
};

export const getCriteriaDict = (locale: Locale): CriteriaDict => CRITERIA_BY_LOCALE[locale];

export const getCriteria = (code: CriteriaCode, locale: Locale): CriteriaText => {
const entry = CRITERIA_BY_LOCALE[locale]?.[code];
if (entry) return entry;

const fallback = CRITERIA_BY_LOCALE.en?.[code];
if (fallback) return fallback;

return { title: code, description: "" };
};

export const getCriteriaSafe = (code: CriteriaCode, locale: string): CriteriaText => {
const safeLocale: Locale = (LOCALES as readonly string[]).includes(locale)
? (locale as Locale)
: "en";
return CRITERIA_BY_LOCALE[safeLocale][code];

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getCriteriaSafe function doesn't include the same fallback logic as the getCriteria function. If the specified locale doesn't contain the requested code, this will return undefined instead of falling back to English or a default value. Consider adding the same fallback chain as getCriteria (lines 36-43) to ensure consistent behavior.

Suggested change
return CRITERIA_BY_LOCALE[safeLocale][code];
const entry = CRITERIA_BY_LOCALE[safeLocale]?.[code];
if (entry) return entry;
const fallback = CRITERIA_BY_LOCALE.en?.[code];
if (fallback) return fallback;
return { title: code, description: "" };

Copilot uses AI. Check for mistakes.
};

export const isCriteriaCode = (v: string): v is CriteriaCode => {
return (CRITERIA_CODES as readonly string[]).includes(v);
};
42 changes: 42 additions & 0 deletions client/src/locals/en/world-heritage-criteria.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"i": {
"title": "Masterpiece",
"description": "Represents a masterpiece of human creative genius."
},
"ii": {
"title": "Interchange of values",
"description": "Shows an important interchange of human values over time or within a cultural area."
},
"iii": {
"title": "Unique testimony",
"description": "Bears a unique or exceptional testimony to a cultural tradition or civilization."
},
"iv": {
"title": "Architectural ensemble",
"description": "An outstanding example of a type of building, architectural or technological ensemble, or landscape illustrating a significant stage in human history."
},
"v": {
"title": "Traditional human settlement",
"description": "An outstanding example of traditional human settlement or land-use representative of a culture."
},
"vi": {
"title": "Associative significance",
"description": "Directly associated with events, living traditions, ideas, beliefs, or artistic works of outstanding universal significance."
},
"vii": {
"title": "Natural beauty",
"description": "Contains superlative natural phenomena or areas of exceptional natural beauty."
},
"viii": {
"title": "Earth’s history",
"description": "Outstanding examples representing major stages of Earth’s history, including geological processes."
},
"ix": {
"title": "Ecological processes",
"description": "Outstanding examples representing significant ongoing ecological and biological processes."
},
"x": {
"title": "Biodiversity",
"description": "Contains important natural habitats for in-situ conservation of biological diversity."
}
}
Loading