vietsub UI

This commit is contained in:
taDuc
2026-06-08 19:30:08 +07:00
parent 44a2a437df
commit a4c4084f9b
12 changed files with 107 additions and 81 deletions
+9 -8
View File
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import Link from "next/link";
import PublicPreviewWrapper from "@/uhm/components/preview/PublicPreviewWrapper";
export const metadata: Metadata = {
@@ -27,10 +28,10 @@ export default function Page() {
{/* Header (SSR & SEO) */}
<header style={srOnlyStyle}>
<nav>
<a href="/">Trang chủ</a>
<a href="/faq">Hướng dẫn / FAQ</a>
<a href="/about-us">Về chúng tôi</a>
<a href="/user">Quản trị viên</a>
<Link href="/">Trang chủ</Link>
<Link href="/faq">Hướng dẫn / Hỏi đáp</Link>
<Link href="/about-us">Về chúng tôi</Link>
<Link href="/user">Quản trị viên</Link>
</nav>
</header>
@@ -43,9 +44,9 @@ export default function Page() {
</p>
<p>
Tính năng chính bao gồm:
- Xem bản đ lịch sử theo dòng thời gian (Timeline).
- Trình phát diễn biến lịch sử chiến trận (Replay).
- Tra cứu thông tin sự kiện lịch sử (Wiki & Entities).
- Xem bản đ lịch sử theo dòng thời gian.
- Trình phát diễn biến lịch sử chiến trận.
- Tra cứu thông tin sự kiện lịch sử.
</p>
</div>
@@ -55,7 +56,7 @@ export default function Page() {
{/* Footer (SSR & SEO) */}
<footer style={srOnlyStyle}>
<p>&copy; {new Date().getFullYear()} Ultimate History Map. All rights reserved.</p>
<p>&copy; {new Date().getFullYear()} Ultimate History Map. Đã đăng bản quyền.</p>
</footer>
</div>
);
+16 -16
View File
@@ -333,7 +333,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
}}
>
<div style={{ fontWeight: 800, marginBottom: "6px" }}>
Map khong khoi tao duoc
Không khởi tạo đưc bản đ
</div>
<div style={{ color: "#cbd5e1", fontSize: "13px" }}>
{fatalInitError}
@@ -478,8 +478,8 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
<label
title={
isGlobeProjection
? "Dang o che do hinh cau (globe)"
: "Dang o che do trai phang (flat)"
? "Đang chế độ hình cầu"
: "Đang chế độ bản đồ phng"
}
style={{
display: "inline-flex",
@@ -495,7 +495,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
type="checkbox"
checked={isGlobeProjection}
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" }}
/>
<div className={`premium-toggle-track ${isGlobeProjection ? "active" : ""}`}>
@@ -511,7 +511,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
}}
className="hidden sm:block"
>
{isGlobeProjection ? "Globe" : "Flat"}
{isGlobeProjection ? "Cầu" : "Phẳng"}
</span>
</label>
@@ -532,7 +532,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
transition: "background 150ms, color 150ms",
}}
>
LOCAL
CỤC BỘ
</button>
<button
type="button"
@@ -549,7 +549,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
transition: "background 150ms, color 150ms",
}}
>
GLOBAL
TOÀN CỤC
</button>
</div>
) : null}
@@ -568,10 +568,10 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
color: isPreviewMode ? "#ffffff" : "#34d399",
flexShrink: 0,
}}
aria-label={isPreviewMode ? "Exit preview" : "Enter preview"}
title={isPreviewMode ? "Thoat preview" : "Xem nhu nguoi dung"}
aria-label={isPreviewMode ? "Thoát xem trước" : "Vào chế độ xem trước"}
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>
) : null}
@@ -592,8 +592,8 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
fontSize: "13px",
flexShrink: 0,
}}
aria-label="Play selected replay"
title="Play replay của geometry đang chọn"
aria-label="Phát diễn biến đã chọn"
title="Phát diễn biến của hình đang chọn"
>
<span
aria-hidden="true"
@@ -605,7 +605,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
borderLeft: "8px solid currentColor",
}}
/>
Play
Phát
</button>
) : null}
@@ -615,7 +615,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
onClick={() => handleZoomByStep(-0.8)}
className="premium-zoom-btn"
style={{ flexShrink: 0 }}
aria-label="Zoom out"
aria-label="Thu nhỏ bản đồ"
>
-
</button>
@@ -648,7 +648,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
onPointerCancel={endZoomSliderDrag}
onBlur={endZoomSliderDrag}
onChange={(event) => handleZoomSliderChange(Number(event.target.value))}
aria-label="Map zoom"
aria-label="Mức thu phóng bản đồ"
/>
<button
@@ -656,7 +656,7 @@ const Map = memo(forwardRef<MapHandle, MapProps>(function Map({
onClick={() => handleZoomByStep(0.8)}
className="premium-zoom-btn"
style={{ flexShrink: 0 }}
aria-label="Zoom in"
aria-label="Phóng to bản đồ"
>
+
</button>
@@ -117,7 +117,7 @@ export default function PresentPlaceSearch({
.catch((err) => {
if (controller.signal.aborted || requestSeqRef.current !== seq) return;
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(() => {
if (requestSeqRef.current === seq) {
@@ -159,7 +159,7 @@ export default function PresentPlaceSearch({
.catch((err) => {
if (historicalRequestSeqRef.current !== seq) return;
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(() => {
if (historicalRequestSeqRef.current === seq) {
@@ -196,7 +196,7 @@ export default function PresentPlaceSearch({
.catch((err) => {
if (wikiRequestSeqRef.current !== seq) return;
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(() => {
if (wikiRequestSeqRef.current === seq) {
@@ -364,11 +364,11 @@ export default function PresentPlaceSearch({
<button
type="button"
onClick={cycleMode}
title={`Switch search mode (current: ${mode})`}
aria-label={`Switch search mode (current: ${mode})`}
title={`Đổi chế độ tìm kiếm (hiện tại: ${getSearchModeLabel(mode)})`}
aria-label={`Đổi chế độ tìm kiếm (hiện tại: ${getSearchModeLabel(mode)})`}
style={modeSwitchStyle}
>
{mode === "present" ? "Present" : mode === "history" ? "History" : "Wiki"}
{getSearchModeLabel(mode)}
</button>
<input
value={activeQuery}
@@ -408,7 +408,7 @@ export default function PresentPlaceSearch({
mode === "present"
? "Tìm địa điểm hiện tại"
: mode === "history"
? "Tìm entity lịch sử"
? "Tìm thực thể lịch sử"
: "Tìm bài viết wiki"
}
style={inputStyle}
@@ -417,8 +417,8 @@ export default function PresentPlaceSearch({
<button
type="button"
onClick={clearSearch}
title="Clear"
aria-label="Clear place search"
title="Xóa tìm kiếm"
aria-label="Xóa ô tìm kiếm"
style={clearButtonStyle}
>
x
@@ -535,9 +535,9 @@ function HistoricalResults({
onSelectEntity: (item: EntityGeometriesSearchItem) => 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 (!results.length && query.trim().length >= 2) return <div style={statusStyle}>Không entity phù hợp.</div>;
if (!results.length && query.trim().length >= 2) return <div style={statusStyle}>Không thực thể phù hợp.</div>;
return (
<>
@@ -563,8 +563,8 @@ function HistoricalResults({
<span style={primaryResultTextStyle}>{item.name || item.entity_id}</span>
<span style={secondaryResultTextStyle}>
{item.geometries.length
? `${item.geometries.length} geometry${item.geometries.length > 1 ? "s" : ""}`
: "Không có geometry"}
? `${item.geometries.length} hình bản đồ`
: "Không có hình bản đồ"}
{item.description ? ` · ${item.description}` : ""}
</span>
</button>
@@ -672,7 +672,7 @@ function formatAdminLabel(state: AdminLabelState | undefined): 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 timeEnd = geometry.time_end ?? null;
const time =
@@ -780,3 +780,9 @@ const statusStyle = {
fontSize: 12,
fontWeight: 700,
} 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 { BACKGROUND_LAYER_OPTIONS } from "@/uhm/lib/map/styles/backgroundLayers";
import { GEO_TYPE_KEYS } from "@/uhm/lib/map/geo/geoTypeMap";
type Props = {
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({
backgroundVisibility,
geometryVisibility,
@@ -245,7 +241,7 @@ export default function ReplayPreviewLayerPanel({
}}
>
{/* Background layers */}
<div style={groupHeaderStyle}>Map</div>
<div style={groupHeaderStyle}>Bản đ</div>
<div style={gridStyle}>
{BACKGROUND_LAYER_OPTIONS.map((layer) => {
const active = Boolean(backgroundVisibility[layer.id]);
@@ -266,11 +262,11 @@ export default function ReplayPreviewLayerPanel({
<div style={dividerStyle} />
{/* Territories / Polygons */}
<div style={groupHeaderStyle}>Areas</div>
<div style={groupHeaderStyle}>Khu vực</div>
<div style={gridStyle}>
{polygonKeys.map((typeKey) => {
const active = geometryVisibility[typeKey] !== false;
const label = typeKey.replace("_", " ").toUpperCase();
const label = getGeometryTypeLabel(typeKey);
return (
<button
key={typeKey}
@@ -288,11 +284,11 @@ export default function ReplayPreviewLayerPanel({
<div style={dividerStyle} />
{/* Routes / Lines */}
<div style={groupHeaderStyle}>Routes</div>
<div style={groupHeaderStyle}>Tuyến</div>
<div style={gridStyle}>
{lineKeys.map((typeKey) => {
const active = geometryVisibility[typeKey] !== false;
const label = typeKey.replace("_", " ").toUpperCase();
const label = getGeometryTypeLabel(typeKey);
return (
<button
key={typeKey}
@@ -310,11 +306,11 @@ export default function ReplayPreviewLayerPanel({
<div style={dividerStyle} />
{/* Places & Events / Points */}
<div style={groupHeaderStyle}>Points</div>
<div style={groupHeaderStyle}>Điểm</div>
<div style={gridStyle}>
{pointKeys.map((typeKey) => {
const active = geometryVisibility[typeKey] !== false;
const label = typeKey.replace("_", " ").toUpperCase();
const label = getGeometryTypeLabel(typeKey);
return (
<button
key={typeKey}
@@ -383,3 +379,26 @@ const dividerStyle: React.CSSProperties = {
width: "80%",
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() ? (
<img
src={dialog.image_url}
alt="Historical"
alt="Hình ảnh lịch sử"
style={{
width: "100%",
display: "block",
@@ -212,7 +212,7 @@ export default function ReplayPreviewOverlay({
textTransform: "uppercase",
}}
>
Preview
Xem trước
</span>
{activeStepLabel ? (
<span
@@ -252,7 +252,7 @@ export default function ReplayPreviewOverlay({
/>
</div>
<div style={{ fontSize: 11, color: "#94a3b8" }}>
Step {activeStepNumber || 0}/{totalSteps}
Bước {activeStepNumber || 0}/{totalSteps}
</div>
</div>
) : null}
@@ -289,7 +289,7 @@ export default function ReplayPreviewOverlay({
onClick={onExitPreview}
style={previewButtonStyle("#334155")}
>
Thoát preview
Thoát xem trước
</button>
</div>
</div>
@@ -83,7 +83,7 @@ export default function GeometrySelectionPanel({
color: "#94a3b8",
}}
>
Geometry
Hình bản đ
</div>
<div
style={{
@@ -94,7 +94,7 @@ export default function GeometrySelectionPanel({
color: "#f8fafc",
}}
>
Chọn entity đ zoom
Chọn thực thể đ phóng tới
</div>
{wikiSlug ? (
<div
@@ -132,7 +132,7 @@ export default function GeometrySelectionPanel({
outline: "none",
}}
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
</button>
@@ -234,7 +234,7 @@ export default function GeometrySelectionPanel({
</div>
) : (
<div style={{ fontSize: 14, lineHeight: "20px", color: "#94a3b8" }}>
Wiki này chưa entity hoặc geometry liên quan.
Wiki này chưa thực thể hoặc hình bản đ liên quan.
</div>
)}
</div>
@@ -28,7 +28,7 @@ export default function MapPlaceholder({ onEnter }: MapPlaceholderProps) {
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/images/map_placeholder.webp"
alt="Map Background"
alt="Nền bản đồ"
fetchPriority="high"
loading="eager"
decoding="sync"
@@ -119,7 +119,7 @@ export default function MapPlaceholder({ onEnter }: MapPlaceholderProps) {
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>
</div>
+1 -1
View File
@@ -186,7 +186,7 @@ const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
) {
return null;
}
return `Stage #${replayPreviewActiveCursor.stageId} · Step ${replayPreviewActiveCursor.stepIndex + 1}`;
return `Cảnh #${replayPreviewActiveCursor.stageId} · Bước ${replayPreviewActiveCursor.stepIndex + 1}`;
}, [replayPreviewActiveCursor.stageId, replayPreviewActiveCursor.stepIndex]);
// Active wiki snapshot
@@ -300,12 +300,12 @@ export default function PreviewMapShell({
type="button"
onClick={() => {
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 {
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={{
...menuOptionStyle,
opacity: isMobileOrTablet ? 0.5 : 1,
@@ -349,7 +349,7 @@ export default function PreviewMapShell({
<button
type="button"
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}
>
<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
type="button"
onClick={() => { window.location.href = "/about-us"; }}
title="Về chúng tôi (About Us)"
title="Về chúng tôi"
style={menuOptionStyle}
>
<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,
rows: [],
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;
}
@@ -727,7 +727,7 @@ export default function PublicPreviewClientPage({
) {
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]);
const isWikiChooserOpen = rightPanelMode === "selection" && Boolean(wikiSelectionPanelAnchor);
@@ -1292,14 +1292,14 @@ function PublicMapZoomPanel({
type="button"
onClick={toggleProjection}
className="uhm-public-projection-toggle"
aria-label="Toggle globe projection"
title={isGlobeProjection ? "Dang o che do hinh cau (globe)" : "Dang o che do trai phang (flat)"}
aria-label="Chuyển chế độ hiển thị hình cầu"
title={isGlobeProjection ? "Đang chế độ hình cầu" : "Đang chế độ bản đồ phng"}
>
<span className={`uhm-public-projection-track ${isGlobeProjection ? "active" : ""}`}>
<span className="uhm-public-projection-thumb" />
</span>
<span className={`uhm-public-projection-label ${isGlobeProjection ? "active" : ""}`}>
{isGlobeProjection ? "Globe" : "Flat"}
{isGlobeProjection ? "Cầu" : "Phẳng"}
</span>
</button>
{onPlayPreviewReplay ? (
@@ -1307,11 +1307,11 @@ function PublicMapZoomPanel({
type="button"
onClick={onPlayPreviewReplay}
className="uhm-public-play-btn"
aria-label="Play selected replay"
title="Play replay cua geometry dang chon"
aria-label="Phát diễn biến đã chọn"
title="Phát diễn biến ca hình đang chn"
>
<span aria-hidden="true" className="uhm-public-play-icon" />
Play
Phát
</button>
) : null}
{onResumePreviewReplay ? (
@@ -1319,8 +1319,8 @@ function PublicMapZoomPanel({
type="button"
onClick={onResumePreviewReplay}
className="uhm-public-play-btn resume"
aria-label="Resume selected replay"
title="Tiep tuc replay dang tam dung"
aria-label="Tiếp tục diễn biến đã chọn"
title="Tiếp tc diễn biến đang tm dng"
>
<span aria-hidden="true" className="uhm-public-play-icon" />
Tiếp tục
@@ -1331,8 +1331,8 @@ function PublicMapZoomPanel({
type="button"
onClick={onStopPreviewReplay}
className="uhm-public-play-btn stop"
aria-label="Stop selected replay"
title="Dung replay dang phat"
aria-label="Dừng diễn biến đã chọn"
title="Dng diễn biến đang phát"
>
<span aria-hidden="true" className="uhm-public-stop-icon" />
Dừng
@@ -1342,7 +1342,7 @@ function PublicMapZoomPanel({
type="button"
onClick={() => zoomByStep(-0.8)}
className="uhm-public-zoom-btn"
aria-label="Zoom out"
aria-label="Thu nhỏ bản đồ"
>
-
</button>
@@ -1368,13 +1368,13 @@ function PublicMapZoomPanel({
isDraggingRef.current = false;
}}
onChange={(event) => handleSliderChange(Number(event.target.value))}
aria-label="Map zoom"
aria-label="Mức thu phóng bản đồ"
/>
<button
type="button"
onClick={() => zoomByStep(0.8)}
className="uhm-public-zoom-btn"
aria-label="Zoom in"
aria-label="Phóng to bản đồ"
>
+
</button>
@@ -104,7 +104,7 @@ export default function WikiSelectionPanel({
outline: "none",
}}
className="hover:bg-slate-700/50 hover:text-slate-100"
aria-label="Close wiki chooser"
aria-label="Đóng bảng chọn wiki"
>
x
</button>
@@ -596,7 +596,7 @@ function PublicWikiSidebar({
outline: "none",
}}
className="hover:bg-slate-700/50 hover:text-slate-100"
aria-label="Close wiki sidebar"
aria-label="Đóng khung wiki"
>
x
</button>