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 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ử chiến trận (Replay). - 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ử (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>&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> </footer>
</div> </div>
); );
+16 -16
View File
@@ -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 đồ phng"
} }
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 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 ( 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 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>
)} )}
</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>
+1 -1
View File
@@ -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 đồ phng"}
> >
<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 ca hình đang chn"
> >
<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 tc diễn biến đang tm dng"
> >
<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="Dng 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>