vietsub UI
This commit is contained in:
+9
-8
@@ -1,4 +1,5 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
import PublicPreviewWrapper from "@/uhm/components/preview/PublicPreviewWrapper";
|
import PublicPreviewWrapper from "@/uhm/components/preview/PublicPreviewWrapper";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -27,10 +28,10 @@ export default function Page() {
|
|||||||
{/* Header (SSR & SEO) */}
|
{/* Header (SSR & SEO) */}
|
||||||
<header style={srOnlyStyle}>
|
<header style={srOnlyStyle}>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/">Trang chủ</a>
|
<Link href="/">Trang chủ</Link>
|
||||||
<a href="/faq">Hướng dẫn / FAQ</a>
|
<Link href="/faq">Hướng dẫn / Hỏi đáp</Link>
|
||||||
<a href="/about-us">Về chúng tôi</a>
|
<Link href="/about-us">Về chúng tôi</Link>
|
||||||
<a href="/user">Quản trị viên</a>
|
<Link href="/user">Quản trị viên</Link>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -43,9 +44,9 @@ export default function Page() {
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Tính năng chính bao gồm:
|
Tính năng chính bao gồm:
|
||||||
- Xem bản đồ lịch sử theo dòng thời gian (Timeline).
|
- Xem bản đồ lịch sử theo dòng thời gian.
|
||||||
- Trình phát diễn biến lịch sử và chiến trận (Replay).
|
- Trình phát diễn biến lịch sử và chiến trận.
|
||||||
- Tra cứu thông tin sự kiện lịch sử (Wiki & Entities).
|
- Tra cứu thông tin sự kiện lịch sử.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ export default function Page() {
|
|||||||
|
|
||||||
{/* Footer (SSR & SEO) */}
|
{/* Footer (SSR & SEO) */}
|
||||||
<footer style={srOnlyStyle}>
|
<footer style={srOnlyStyle}>
|
||||||
<p>© {new Date().getFullYear()} Ultimate History Map. All rights reserved.</p>
|
<p>© {new Date().getFullYear()} Ultimate History Map. Đã đăng ký bản quyền.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+16
-16
@@ -333,7 +333,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ fontWeight: 800, marginBottom: "6px" }}>
|
<div style={{ fontWeight: 800, marginBottom: "6px" }}>
|
||||||
Map khong khoi tao duoc
|
Không khởi tạo được bản đồ
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: "#cbd5e1", fontSize: "13px" }}>
|
<div style={{ color: "#cbd5e1", fontSize: "13px" }}>
|
||||||
{fatalInitError}
|
{fatalInitError}
|
||||||
@@ -478,8 +478,8 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
<label
|
<label
|
||||||
title={
|
title={
|
||||||
isGlobeProjection
|
isGlobeProjection
|
||||||
? "Dang o che do hinh cau (globe)"
|
? "Đang ở chế độ hình cầu"
|
||||||
: "Dang o che do trai phang (flat)"
|
: "Đang ở chế độ bản đồ phẳng"
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
@@ -495,7 +495,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isGlobeProjection}
|
checked={isGlobeProjection}
|
||||||
onChange={(e) => setIsGlobeProjection(e.target.checked)}
|
onChange={(e) => setIsGlobeProjection(e.target.checked)}
|
||||||
aria-label="Toggle globe projection"
|
aria-label="Chuyển chế độ hiển thị hình cầu"
|
||||||
style={{ display: "none" }}
|
style={{ display: "none" }}
|
||||||
/>
|
/>
|
||||||
<div className={`premium-toggle-track ${isGlobeProjection ? "active" : ""}`}>
|
<div className={`premium-toggle-track ${isGlobeProjection ? "active" : ""}`}>
|
||||||
@@ -511,7 +511,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
}}
|
}}
|
||||||
className="hidden sm:block"
|
className="hidden sm:block"
|
||||||
>
|
>
|
||||||
{isGlobeProjection ? "Globe" : "Flat"}
|
{isGlobeProjection ? "Cầu" : "Phẳng"}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -532,7 +532,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
transition: "background 150ms, color 150ms",
|
transition: "background 150ms, color 150ms",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
LOCAL
|
CỤC BỘ
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -549,7 +549,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
transition: "background 150ms, color 150ms",
|
transition: "background 150ms, color 150ms",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
GLOBAL
|
TOÀN CỤC
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -568,10 +568,10 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
color: isPreviewMode ? "#ffffff" : "#34d399",
|
color: isPreviewMode ? "#ffffff" : "#34d399",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
aria-label={isPreviewMode ? "Exit preview" : "Enter preview"}
|
aria-label={isPreviewMode ? "Thoát xem trước" : "Vào chế độ xem trước"}
|
||||||
title={isPreviewMode ? "Thoat preview" : "Xem nhu nguoi dung"}
|
title={isPreviewMode ? "Thoát xem trước" : "Xem như người dùng"}
|
||||||
>
|
>
|
||||||
{isPreviewMode ? "Editor" : "Preview"}
|
{isPreviewMode ? "Trình sửa" : "Xem trước"}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -592,8 +592,8 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
fontSize: "13px",
|
fontSize: "13px",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
aria-label="Play selected replay"
|
aria-label="Phát diễn biến đã chọn"
|
||||||
title="Play replay của geometry đang chọn"
|
title="Phát diễn biến của hình đang chọn"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -605,7 +605,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
borderLeft: "8px solid currentColor",
|
borderLeft: "8px solid currentColor",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
Play
|
Phát
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -615,7 +615,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
onClick={() => handleZoomByStep(-0.8)}
|
onClick={() => handleZoomByStep(-0.8)}
|
||||||
className="premium-zoom-btn"
|
className="premium-zoom-btn"
|
||||||
style={{ flexShrink: 0 }}
|
style={{ flexShrink: 0 }}
|
||||||
aria-label="Zoom out"
|
aria-label="Thu nhỏ bản đồ"
|
||||||
>
|
>
|
||||||
-
|
-
|
||||||
</button>
|
</button>
|
||||||
@@ -648,7 +648,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
onPointerCancel={endZoomSliderDrag}
|
onPointerCancel={endZoomSliderDrag}
|
||||||
onBlur={endZoomSliderDrag}
|
onBlur={endZoomSliderDrag}
|
||||||
onChange={(event) => handleZoomSliderChange(Number(event.target.value))}
|
onChange={(event) => handleZoomSliderChange(Number(event.target.value))}
|
||||||
aria-label="Map zoom"
|
aria-label="Mức thu phóng bản đồ"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -656,7 +656,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
|
|||||||
onClick={() => handleZoomByStep(0.8)}
|
onClick={() => handleZoomByStep(0.8)}
|
||||||
className="premium-zoom-btn"
|
className="premium-zoom-btn"
|
||||||
style={{ flexShrink: 0 }}
|
style={{ flexShrink: 0 }}
|
||||||
aria-label="Zoom in"
|
aria-label="Phóng to bản đồ"
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export default function PresentPlaceSearch({
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (controller.signal.aborted || requestSeqRef.current !== seq) return;
|
if (controller.signal.aborted || requestSeqRef.current !== seq) return;
|
||||||
setResults([]);
|
setResults([]);
|
||||||
setError(err instanceof Error ? err.message : "Không search được địa điểm.");
|
setError(err instanceof Error ? err.message : "Không tìm được địa điểm.");
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (requestSeqRef.current === seq) {
|
if (requestSeqRef.current === seq) {
|
||||||
@@ -159,7 +159,7 @@ export default function PresentPlaceSearch({
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (historicalRequestSeqRef.current !== seq) return;
|
if (historicalRequestSeqRef.current !== seq) return;
|
||||||
setHistoricalResults([]);
|
setHistoricalResults([]);
|
||||||
setHistoricalError(err instanceof Error ? err.message : "Không search được entity lịch sử.");
|
setHistoricalError(err instanceof Error ? err.message : "Không tìm được thực thể lịch sử.");
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (historicalRequestSeqRef.current === seq) {
|
if (historicalRequestSeqRef.current === seq) {
|
||||||
@@ -196,7 +196,7 @@ export default function PresentPlaceSearch({
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (wikiRequestSeqRef.current !== seq) return;
|
if (wikiRequestSeqRef.current !== seq) return;
|
||||||
setWikiResults([]);
|
setWikiResults([]);
|
||||||
setWikiError(err instanceof Error ? err.message : "Không search được wiki.");
|
setWikiError(err instanceof Error ? err.message : "Không tìm được bài viết wiki.");
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (wikiRequestSeqRef.current === seq) {
|
if (wikiRequestSeqRef.current === seq) {
|
||||||
@@ -364,11 +364,11 @@ export default function PresentPlaceSearch({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={cycleMode}
|
onClick={cycleMode}
|
||||||
title={`Switch search mode (current: ${mode})`}
|
title={`Đổi chế độ tìm kiếm (hiện tại: ${getSearchModeLabel(mode)})`}
|
||||||
aria-label={`Switch search mode (current: ${mode})`}
|
aria-label={`Đổi chế độ tìm kiếm (hiện tại: ${getSearchModeLabel(mode)})`}
|
||||||
style={modeSwitchStyle}
|
style={modeSwitchStyle}
|
||||||
>
|
>
|
||||||
{mode === "present" ? "Present" : mode === "history" ? "History" : "Wiki"}
|
{getSearchModeLabel(mode)}
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
value={activeQuery}
|
value={activeQuery}
|
||||||
@@ -408,7 +408,7 @@ export default function PresentPlaceSearch({
|
|||||||
mode === "present"
|
mode === "present"
|
||||||
? "Tìm địa điểm hiện tại"
|
? "Tìm địa điểm hiện tại"
|
||||||
: mode === "history"
|
: mode === "history"
|
||||||
? "Tìm entity lịch sử"
|
? "Tìm thực thể lịch sử"
|
||||||
: "Tìm bài viết wiki"
|
: "Tìm bài viết wiki"
|
||||||
}
|
}
|
||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
@@ -417,8 +417,8 @@ export default function PresentPlaceSearch({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={clearSearch}
|
onClick={clearSearch}
|
||||||
title="Clear"
|
title="Xóa tìm kiếm"
|
||||||
aria-label="Clear place search"
|
aria-label="Xóa ô tìm kiếm"
|
||||||
style={clearButtonStyle}
|
style={clearButtonStyle}
|
||||||
>
|
>
|
||||||
x
|
x
|
||||||
@@ -535,9 +535,9 @@ function HistoricalResults({
|
|||||||
onSelectEntity: (item: EntityGeometriesSearchItem) => void;
|
onSelectEntity: (item: EntityGeometriesSearchItem) => void;
|
||||||
onSelectGeometry: (item: EntityGeometriesSearchItem, geometry: EntityGeometrySearchGeo) => void;
|
onSelectGeometry: (item: EntityGeometriesSearchItem, geometry: EntityGeometrySearchGeo) => void;
|
||||||
}) {
|
}) {
|
||||||
if (isLoading) return <div style={statusStyle}>Đang tìm entity...</div>;
|
if (isLoading) return <div style={statusStyle}>Đang tìm thực thể...</div>;
|
||||||
if (error) return <div style={{ ...statusStyle, color: "#fecaca" }}>{error}</div>;
|
if (error) return <div style={{ ...statusStyle, color: "#fecaca" }}>{error}</div>;
|
||||||
if (!results.length && query.trim().length >= 2) return <div style={statusStyle}>Không có entity phù hợp.</div>;
|
if (!results.length && query.trim().length >= 2) return <div style={statusStyle}>Không có thực thể phù hợp.</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -563,8 +563,8 @@ function HistoricalResults({
|
|||||||
<span style={primaryResultTextStyle}>{item.name || item.entity_id}</span>
|
<span style={primaryResultTextStyle}>{item.name || item.entity_id}</span>
|
||||||
<span style={secondaryResultTextStyle}>
|
<span style={secondaryResultTextStyle}>
|
||||||
{item.geometries.length
|
{item.geometries.length
|
||||||
? `${item.geometries.length} geometry${item.geometries.length > 1 ? "s" : ""}`
|
? `${item.geometries.length} hình bản đồ`
|
||||||
: "Không có geometry"}
|
: "Không có hình bản đồ"}
|
||||||
{item.description ? ` · ${item.description}` : ""}
|
{item.description ? ` · ${item.description}` : ""}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -672,7 +672,7 @@ function formatAdminLabel(state: AdminLabelState | undefined): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatGeometryMeta(geometry: EntityGeometrySearchGeo): string {
|
function formatGeometryMeta(geometry: EntityGeometrySearchGeo): string {
|
||||||
const type = geometry.type || "geometry";
|
const type = geometry.type || "hình bản đồ";
|
||||||
const timeStart = geometry.time_start ?? null;
|
const timeStart = geometry.time_start ?? null;
|
||||||
const timeEnd = geometry.time_end ?? null;
|
const timeEnd = geometry.time_end ?? null;
|
||||||
const time =
|
const time =
|
||||||
@@ -780,3 +780,9 @@ const statusStyle = {
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
} satisfies CSSProperties;
|
} satisfies CSSProperties;
|
||||||
|
|
||||||
|
function getSearchModeLabel(mode: SearchMode): string {
|
||||||
|
if (mode === "present") return "Hiện tại";
|
||||||
|
if (mode === "history") return "Lịch sử";
|
||||||
|
return "Wiki";
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import type { BackgroundLayerId } from "@/uhm/lib/map/styles/backgroundLayers";
|
import type { BackgroundLayerId } from "@/uhm/lib/map/styles/backgroundLayers";
|
||||||
import { BACKGROUND_LAYER_OPTIONS } from "@/uhm/lib/map/styles/backgroundLayers";
|
import { BACKGROUND_LAYER_OPTIONS } from "@/uhm/lib/map/styles/backgroundLayers";
|
||||||
import { GEO_TYPE_KEYS } from "@/uhm/lib/map/geo/geoTypeMap";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
backgroundVisibility: Record<string, boolean>;
|
backgroundVisibility: Record<string, boolean>;
|
||||||
@@ -165,9 +164,6 @@ const LAYER_ICONS: Record<string, React.ReactNode> = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Class name helper for tooltips using CSS
|
|
||||||
const buttonClassName = "preview-layer-btn";
|
|
||||||
|
|
||||||
export default function ReplayPreviewLayerPanel({
|
export default function ReplayPreviewLayerPanel({
|
||||||
backgroundVisibility,
|
backgroundVisibility,
|
||||||
geometryVisibility,
|
geometryVisibility,
|
||||||
@@ -245,7 +241,7 @@ export default function ReplayPreviewLayerPanel({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Background layers */}
|
{/* Background layers */}
|
||||||
<div style={groupHeaderStyle}>Map</div>
|
<div style={groupHeaderStyle}>Bản đồ</div>
|
||||||
<div style={gridStyle}>
|
<div style={gridStyle}>
|
||||||
{BACKGROUND_LAYER_OPTIONS.map((layer) => {
|
{BACKGROUND_LAYER_OPTIONS.map((layer) => {
|
||||||
const active = Boolean(backgroundVisibility[layer.id]);
|
const active = Boolean(backgroundVisibility[layer.id]);
|
||||||
@@ -266,11 +262,11 @@ export default function ReplayPreviewLayerPanel({
|
|||||||
<div style={dividerStyle} />
|
<div style={dividerStyle} />
|
||||||
|
|
||||||
{/* Territories / Polygons */}
|
{/* Territories / Polygons */}
|
||||||
<div style={groupHeaderStyle}>Areas</div>
|
<div style={groupHeaderStyle}>Khu vực</div>
|
||||||
<div style={gridStyle}>
|
<div style={gridStyle}>
|
||||||
{polygonKeys.map((typeKey) => {
|
{polygonKeys.map((typeKey) => {
|
||||||
const active = geometryVisibility[typeKey] !== false;
|
const active = geometryVisibility[typeKey] !== false;
|
||||||
const label = typeKey.replace("_", " ").toUpperCase();
|
const label = getGeometryTypeLabel(typeKey);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={typeKey}
|
key={typeKey}
|
||||||
@@ -288,11 +284,11 @@ export default function ReplayPreviewLayerPanel({
|
|||||||
<div style={dividerStyle} />
|
<div style={dividerStyle} />
|
||||||
|
|
||||||
{/* Routes / Lines */}
|
{/* Routes / Lines */}
|
||||||
<div style={groupHeaderStyle}>Routes</div>
|
<div style={groupHeaderStyle}>Tuyến</div>
|
||||||
<div style={gridStyle}>
|
<div style={gridStyle}>
|
||||||
{lineKeys.map((typeKey) => {
|
{lineKeys.map((typeKey) => {
|
||||||
const active = geometryVisibility[typeKey] !== false;
|
const active = geometryVisibility[typeKey] !== false;
|
||||||
const label = typeKey.replace("_", " ").toUpperCase();
|
const label = getGeometryTypeLabel(typeKey);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={typeKey}
|
key={typeKey}
|
||||||
@@ -310,11 +306,11 @@ export default function ReplayPreviewLayerPanel({
|
|||||||
<div style={dividerStyle} />
|
<div style={dividerStyle} />
|
||||||
|
|
||||||
{/* Places & Events / Points */}
|
{/* Places & Events / Points */}
|
||||||
<div style={groupHeaderStyle}>Points</div>
|
<div style={groupHeaderStyle}>Điểm</div>
|
||||||
<div style={gridStyle}>
|
<div style={gridStyle}>
|
||||||
{pointKeys.map((typeKey) => {
|
{pointKeys.map((typeKey) => {
|
||||||
const active = geometryVisibility[typeKey] !== false;
|
const active = geometryVisibility[typeKey] !== false;
|
||||||
const label = typeKey.replace("_", " ").toUpperCase();
|
const label = getGeometryTypeLabel(typeKey);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={typeKey}
|
key={typeKey}
|
||||||
@@ -383,3 +379,26 @@ const dividerStyle: React.CSSProperties = {
|
|||||||
width: "80%",
|
width: "80%",
|
||||||
margin: "6px 0",
|
margin: "6px 0",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getGeometryTypeLabel(typeKey: string): string {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
country: "Quốc gia",
|
||||||
|
state: "Nhà nước / vùng",
|
||||||
|
faction: "Phe phái",
|
||||||
|
rebellion_zone: "Vùng nổi dậy",
|
||||||
|
defense_line: "Tuyến phòng thủ",
|
||||||
|
military_route: "Đường hành quân",
|
||||||
|
retreat_route: "Đường rút lui",
|
||||||
|
migration_route: "Đường di cư",
|
||||||
|
trade_route: "Tuyến thương mại",
|
||||||
|
battle: "Trận đánh",
|
||||||
|
person_event: "Nhân vật / sự kiện",
|
||||||
|
temple: "Đền miếu",
|
||||||
|
capital: "Kinh đô",
|
||||||
|
city: "Thành phố",
|
||||||
|
fortification: "Công sự",
|
||||||
|
ruin: "Di tích",
|
||||||
|
port: "Cảng",
|
||||||
|
};
|
||||||
|
return labels[typeKey] || typeKey.replaceAll("_", " ");
|
||||||
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export default function ReplayPreviewOverlay({
|
|||||||
{dialog.image_url?.trim() ? (
|
{dialog.image_url?.trim() ? (
|
||||||
<img
|
<img
|
||||||
src={dialog.image_url}
|
src={dialog.image_url}
|
||||||
alt="Historical"
|
alt="Hình ảnh lịch sử"
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
display: "block",
|
display: "block",
|
||||||
@@ -212,7 +212,7 @@ export default function ReplayPreviewOverlay({
|
|||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Preview
|
Xem trước
|
||||||
</span>
|
</span>
|
||||||
{activeStepLabel ? (
|
{activeStepLabel ? (
|
||||||
<span
|
<span
|
||||||
@@ -252,7 +252,7 @@ export default function ReplayPreviewOverlay({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 11, color: "#94a3b8" }}>
|
<div style={{ fontSize: 11, color: "#94a3b8" }}>
|
||||||
Step {activeStepNumber || 0}/{totalSteps}
|
Bước {activeStepNumber || 0}/{totalSteps}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -289,7 +289,7 @@ export default function ReplayPreviewOverlay({
|
|||||||
onClick={onExitPreview}
|
onClick={onExitPreview}
|
||||||
style={previewButtonStyle("#334155")}
|
style={previewButtonStyle("#334155")}
|
||||||
>
|
>
|
||||||
Thoát preview
|
Thoát xem trước
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export default function GeometrySelectionPanel({
|
|||||||
color: "#94a3b8",
|
color: "#94a3b8",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Geometry
|
Hình bản đồ
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -94,7 +94,7 @@ export default function GeometrySelectionPanel({
|
|||||||
color: "#f8fafc",
|
color: "#f8fafc",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Chọn entity để zoom
|
Chọn thực thể để phóng tới
|
||||||
</div>
|
</div>
|
||||||
{wikiSlug ? (
|
{wikiSlug ? (
|
||||||
<div
|
<div
|
||||||
@@ -132,7 +132,7 @@ export default function GeometrySelectionPanel({
|
|||||||
outline: "none",
|
outline: "none",
|
||||||
}}
|
}}
|
||||||
className="hover:bg-slate-700/50 hover:text-slate-100"
|
className="hover:bg-slate-700/50 hover:text-slate-100"
|
||||||
aria-label="Close geometry chooser"
|
aria-label="Đóng bảng chọn hình bản đồ"
|
||||||
>
|
>
|
||||||
x
|
x
|
||||||
</button>
|
</button>
|
||||||
@@ -234,7 +234,7 @@ export default function GeometrySelectionPanel({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ fontSize: 14, lineHeight: "20px", color: "#94a3b8" }}>
|
<div style={{ fontSize: 14, lineHeight: "20px", color: "#94a3b8" }}>
|
||||||
Wiki này chưa có entity hoặc geometry liên quan.
|
Wiki này chưa có thực thể hoặc hình bản đồ liên quan.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function MapPlaceholder({ onEnter }: MapPlaceholderProps) {
|
|||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img
|
<img
|
||||||
src="/images/map_placeholder.webp"
|
src="/images/map_placeholder.webp"
|
||||||
alt="Map Background"
|
alt="Nền bản đồ"
|
||||||
fetchPriority="high"
|
fetchPriority="high"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
decoding="sync"
|
decoding="sync"
|
||||||
@@ -119,7 +119,7 @@ export default function MapPlaceholder({ onEnter }: MapPlaceholderProps) {
|
|||||||
textShadow: "0 1px 2px rgba(0, 0, 0, 0.8)",
|
textShadow: "0 1px 2px rgba(0, 0, 0, 0.8)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
hiện dự án chỉ đang hỗ trợ người dùng máy tính, các phiên bản di động hiên không ổn định
|
Hiện dự án chỉ hỗ trợ tốt trên máy tính; phiên bản di động chưa ổn định.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
|
|||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return `Stage #${replayPreviewActiveCursor.stageId} · Step ${replayPreviewActiveCursor.stepIndex + 1}`;
|
return `Cảnh #${replayPreviewActiveCursor.stageId} · Bước ${replayPreviewActiveCursor.stepIndex + 1}`;
|
||||||
}, [replayPreviewActiveCursor.stageId, replayPreviewActiveCursor.stepIndex]);
|
}, [replayPreviewActiveCursor.stageId, replayPreviewActiveCursor.stepIndex]);
|
||||||
|
|
||||||
// Active wiki snapshot
|
// Active wiki snapshot
|
||||||
|
|||||||
@@ -300,12 +300,12 @@ export default function PreviewMapShell({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isMobileOrTablet) {
|
if (isMobileOrTablet) {
|
||||||
alert("Tính năng quản trị và chỉnh sửa chỉ hỗ trợ trên máy tính (Desktop)");
|
alert("Tính năng quản trị và chỉnh sửa chỉ hỗ trợ trên máy tính.");
|
||||||
} else {
|
} else {
|
||||||
window.location.href = "/user";
|
window.location.href = "/user";
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title={isMobileOrTablet ? "Tính năng này chỉ hoạt động trên Desktop" : "Quản trị & Chỉnh sửa (Edit)"}
|
title={isMobileOrTablet ? "Tính năng này chỉ hoạt động trên máy tính" : "Quản trị và chỉnh sửa"}
|
||||||
style={{
|
style={{
|
||||||
...menuOptionStyle,
|
...menuOptionStyle,
|
||||||
opacity: isMobileOrTablet ? 0.5 : 1,
|
opacity: isMobileOrTablet ? 0.5 : 1,
|
||||||
@@ -349,7 +349,7 @@ export default function PreviewMapShell({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { window.location.href = "/faq"; }}
|
onClick={() => { window.location.href = "/faq"; }}
|
||||||
title="Hỏi đáp & Hướng dẫn (FAQ)"
|
title="Hỏi đáp và hướng dẫn"
|
||||||
style={menuOptionStyle}
|
style={menuOptionStyle}
|
||||||
>
|
>
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
@@ -360,7 +360,7 @@ export default function PreviewMapShell({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { window.location.href = "/about-us"; }}
|
onClick={() => { window.location.href = "/about-us"; }}
|
||||||
title="Về chúng tôi (About Us)"
|
title="Về chúng tôi"
|
||||||
style={menuOptionStyle}
|
style={menuOptionStyle}
|
||||||
>
|
>
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ export default function PublicPreviewClientPage({
|
|||||||
wikiSlug: nextSlug,
|
wikiSlug: nextSlug,
|
||||||
rows: [],
|
rows: [],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: "Wiki này chưa có entity liên quan.",
|
error: "Wiki này chưa có thực thể liên quan.",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -727,7 +727,7 @@ export default function PublicPreviewClientPage({
|
|||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return `Stage #${replayPreview.activeCursor.stageId} · Step ${replayPreview.activeCursor.stepIndex + 1}`;
|
return `Cảnh #${replayPreview.activeCursor.stageId} · Bước ${replayPreview.activeCursor.stepIndex + 1}`;
|
||||||
}, [replayPreview.activeCursor.stageId, replayPreview.activeCursor.stepIndex]);
|
}, [replayPreview.activeCursor.stageId, replayPreview.activeCursor.stepIndex]);
|
||||||
|
|
||||||
const isWikiChooserOpen = rightPanelMode === "selection" && Boolean(wikiSelectionPanelAnchor);
|
const isWikiChooserOpen = rightPanelMode === "selection" && Boolean(wikiSelectionPanelAnchor);
|
||||||
@@ -1292,14 +1292,14 @@ function PublicMapZoomPanel({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={toggleProjection}
|
onClick={toggleProjection}
|
||||||
className="uhm-public-projection-toggle"
|
className="uhm-public-projection-toggle"
|
||||||
aria-label="Toggle globe projection"
|
aria-label="Chuyển chế độ hiển thị hình cầu"
|
||||||
title={isGlobeProjection ? "Dang o che do hinh cau (globe)" : "Dang o che do trai phang (flat)"}
|
title={isGlobeProjection ? "Đang ở chế độ hình cầu" : "Đang ở chế độ bản đồ phẳng"}
|
||||||
>
|
>
|
||||||
<span className={`uhm-public-projection-track ${isGlobeProjection ? "active" : ""}`}>
|
<span className={`uhm-public-projection-track ${isGlobeProjection ? "active" : ""}`}>
|
||||||
<span className="uhm-public-projection-thumb" />
|
<span className="uhm-public-projection-thumb" />
|
||||||
</span>
|
</span>
|
||||||
<span className={`uhm-public-projection-label ${isGlobeProjection ? "active" : ""}`}>
|
<span className={`uhm-public-projection-label ${isGlobeProjection ? "active" : ""}`}>
|
||||||
{isGlobeProjection ? "Globe" : "Flat"}
|
{isGlobeProjection ? "Cầu" : "Phẳng"}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{onPlayPreviewReplay ? (
|
{onPlayPreviewReplay ? (
|
||||||
@@ -1307,11 +1307,11 @@ function PublicMapZoomPanel({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onPlayPreviewReplay}
|
onClick={onPlayPreviewReplay}
|
||||||
className="uhm-public-play-btn"
|
className="uhm-public-play-btn"
|
||||||
aria-label="Play selected replay"
|
aria-label="Phát diễn biến đã chọn"
|
||||||
title="Play replay cua geometry dang chon"
|
title="Phát diễn biến của hình đang chọn"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true" className="uhm-public-play-icon" />
|
<span aria-hidden="true" className="uhm-public-play-icon" />
|
||||||
Play
|
Phát
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
{onResumePreviewReplay ? (
|
{onResumePreviewReplay ? (
|
||||||
@@ -1319,8 +1319,8 @@ function PublicMapZoomPanel({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onResumePreviewReplay}
|
onClick={onResumePreviewReplay}
|
||||||
className="uhm-public-play-btn resume"
|
className="uhm-public-play-btn resume"
|
||||||
aria-label="Resume selected replay"
|
aria-label="Tiếp tục diễn biến đã chọn"
|
||||||
title="Tiep tuc replay dang tam dung"
|
title="Tiếp tục diễn biến đang tạm dừng"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true" className="uhm-public-play-icon" />
|
<span aria-hidden="true" className="uhm-public-play-icon" />
|
||||||
Tiếp tục
|
Tiếp tục
|
||||||
@@ -1331,8 +1331,8 @@ function PublicMapZoomPanel({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onStopPreviewReplay}
|
onClick={onStopPreviewReplay}
|
||||||
className="uhm-public-play-btn stop"
|
className="uhm-public-play-btn stop"
|
||||||
aria-label="Stop selected replay"
|
aria-label="Dừng diễn biến đã chọn"
|
||||||
title="Dung replay dang phat"
|
title="Dừng diễn biến đang phát"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true" className="uhm-public-stop-icon" />
|
<span aria-hidden="true" className="uhm-public-stop-icon" />
|
||||||
Dừng
|
Dừng
|
||||||
@@ -1342,7 +1342,7 @@ function PublicMapZoomPanel({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => zoomByStep(-0.8)}
|
onClick={() => zoomByStep(-0.8)}
|
||||||
className="uhm-public-zoom-btn"
|
className="uhm-public-zoom-btn"
|
||||||
aria-label="Zoom out"
|
aria-label="Thu nhỏ bản đồ"
|
||||||
>
|
>
|
||||||
-
|
-
|
||||||
</button>
|
</button>
|
||||||
@@ -1368,13 +1368,13 @@ function PublicMapZoomPanel({
|
|||||||
isDraggingRef.current = false;
|
isDraggingRef.current = false;
|
||||||
}}
|
}}
|
||||||
onChange={(event) => handleSliderChange(Number(event.target.value))}
|
onChange={(event) => handleSliderChange(Number(event.target.value))}
|
||||||
aria-label="Map zoom"
|
aria-label="Mức thu phóng bản đồ"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => zoomByStep(0.8)}
|
onClick={() => zoomByStep(0.8)}
|
||||||
className="uhm-public-zoom-btn"
|
className="uhm-public-zoom-btn"
|
||||||
aria-label="Zoom in"
|
aria-label="Phóng to bản đồ"
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export default function WikiSelectionPanel({
|
|||||||
outline: "none",
|
outline: "none",
|
||||||
}}
|
}}
|
||||||
className="hover:bg-slate-700/50 hover:text-slate-100"
|
className="hover:bg-slate-700/50 hover:text-slate-100"
|
||||||
aria-label="Close wiki chooser"
|
aria-label="Đóng bảng chọn wiki"
|
||||||
>
|
>
|
||||||
x
|
x
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -596,7 +596,7 @@ function PublicWikiSidebar({
|
|||||||
outline: "none",
|
outline: "none",
|
||||||
}}
|
}}
|
||||||
className="hover:bg-slate-700/50 hover:text-slate-100"
|
className="hover:bg-slate-700/50 hover:text-slate-100"
|
||||||
aria-label="Close wiki sidebar"
|
aria-label="Đóng khung wiki"
|
||||||
>
|
>
|
||||||
x
|
x
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user