From 5b13ec8d8de1289c30f71ed6dfa831d86251dbd1 Mon Sep 17 00:00:00 2001 From: taDuc Date: Thu, 4 Jun 2026 00:28:26 +0700 Subject: [PATCH] feat: persist map viewport in localStorage, optimize public preview loading with deferred interactive map, and add geometry background management actions. --- next.config.ts | 2 +- public/images/map_placeholder.webp | Bin 0 -> 48326 bytes src/app/globals.css | 4 +- src/app/layout.tsx | 43 +- src/app/page.tsx | 537 +------- src/uhm/api/battleReplays.ts | 4 +- src/uhm/api/relations.ts | 2 +- src/uhm/components/Map.tsx | 43 +- .../editor/ReplayEffectsSidebar.tsx | 42 +- .../editor/ReplayTimelineSidebar.tsx | 146 ++- src/uhm/components/map/useMapInstance.ts | 37 +- src/uhm/components/map/useMapSync.ts | 14 +- src/uhm/components/preview/MapPlaceholder.tsx | 225 ++++ .../components/preview/PreviewMapShell.tsx | 3 + .../preview/PublicPreviewClientPage.tsx | 502 +++++++ .../preview/PublicPreviewWrapper.tsx | 46 + .../preview/hooks/usePublicPreviewData.ts | 36 +- src/uhm/doc/commit_snapshot.ts | 12 +- src/uhm/doc/editor_replay_actions.md | 8 +- src/uhm/doc/lighthouse_map_optimization.md | 147 +++ src/uhm/doc/replay_actions.md | 78 ++ src/uhm/doc/replay_undo_actions.md | 121 ++ src/uhm/lib/editor/snapshot/editorSnapshot.ts | 25 +- src/uhm/lib/replay/replayDispatcher.ts | 18 +- src/uhm/lib/replay/replayMapEffects.ts | 8 - src/uhm/lib/replay/useReplayPreview.ts | 21 +- src/uhm/types/projects.ts | 5 +- ...-019e59c7-1a1e-763a-9f58-d96200fab5be.json | 1162 +++++++++++++++++ 28 files changed, 2690 insertions(+), 601 deletions(-) create mode 100644 public/images/map_placeholder.webp create mode 100644 src/uhm/components/preview/MapPlaceholder.tsx create mode 100644 src/uhm/components/preview/PublicPreviewClientPage.tsx create mode 100644 src/uhm/components/preview/PublicPreviewWrapper.tsx create mode 100644 src/uhm/doc/lighthouse_map_optimization.md create mode 100644 src/uhm/doc/replay_actions.md create mode 100644 src/uhm/doc/replay_undo_actions.md create mode 100644 tmp/exported_data/replay-019e59c7-1a1e-763a-9f58-d96200fab5be.json diff --git a/next.config.ts b/next.config.ts index 916db6c..28c32e3 100644 --- a/next.config.ts +++ b/next.config.ts @@ -26,7 +26,7 @@ const nextConfig: NextConfig = { }, ], }, - output: 'standalone', + // output: 'standalone', webpack(config) { config.module.rules.push({ test: /\.svg$/, diff --git a/public/images/map_placeholder.webp b/public/images/map_placeholder.webp new file mode 100644 index 0000000000000000000000000000000000000000..460ceff6139bd92994eed25edac247ba011d45fd GIT binary patch literal 48326 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 literal 0 HcmV?d00001 diff --git a/src/app/globals.css b/src/app/globals.css index d75a64f..ba4377f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,13 +1,11 @@ @import 'tailwindcss'; -@import './fonts.css'; - @custom-variant dark (&:is(.dark *)); @theme { --font-*: initial; --font-outfit: Outfit, sans-serif; - --font-inter: "SF Pro Display", ui-sans-serif, system-ui, sans-serif; + --font-inter: var(--font-inter), ui-sans-serif, system-ui, sans-serif; --breakpoint-*: initial; --breakpoint-2xsm: 375px; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 347cd73..81fda39 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,43 +1,32 @@ -import localFont from 'next/font/local'; +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; import './globals.css'; import "flatpickr/dist/flatpickr.css"; import { SidebarProvider } from '@/context/SidebarContext'; import { ThemeProvider } from '@/context/ThemeContext'; import { Toaster } from 'sonner'; import StoreProvider from '@/store/StoreProvider'; + +export const metadata: Metadata = { + title: 'Ultimate History Map', + description: 'Bản đồ tương tác lịch sử thế giới qua các thời kỳ', +}; -const sfPro = localFont({ - src: [ - { - path: '../../public/font/SF-Pro-Display/SF-Pro-Display-Regular.otf', - weight: '400', - style: 'normal', - }, - { - path: '../../public/font/SF-Pro-Display/SF-Pro-Display-Medium.otf', - weight: '500', - style: 'normal', - }, - { - path: '../../public/font/SF-Pro-Display/SF-Pro-Display-Semibold.otf', - weight: '600', - style: 'normal', - }, - { - path: '../../public/font/SF-Pro-Display/SF-Pro-Display-Bold.otf', - weight: '700', - style: 'normal', - }, - ], - }) +const inter = Inter({ + subsets: ['latin', 'vietnamese'], + weight: ['400', '500', '600', '700'], + variable: '--font-inter', + display: 'swap', +}); + export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( - - + + {children} diff --git a/src/app/page.tsx b/src/app/page.tsx index 18df3f9..c3a3c15 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,477 +1,78 @@ -"use client"; +import type { Metadata } from "next"; +import PublicPreviewWrapper from "@/uhm/components/preview/PublicPreviewWrapper"; -import { useEffect, useState } from "react"; +export const metadata: Metadata = { + title: "Ultimate History Map | Bản Đồ Lịch Sử Thế Giới Tương Tác", + description: "Khám phá lịch sử thế giới qua bản đồ tương tác theo dòng thời gian. Xem lại các trận đánh diễn biến lịch sử sinh động qua hệ thống Replay.", +}; -import PreviewMapShell from "@/uhm/components/preview/PreviewMapShell"; -import ReplayPreviewOverlay from "@/uhm/components/editor/ReplayPreviewOverlay"; -import { usePublicPreviewData } from "@/uhm/components/preview/hooks/usePublicPreviewData"; -import { useReplayPreview } from "@/uhm/lib/replay/useReplayPreview"; -import type { MapHandle } from "@/uhm/components/Map"; -import { useRef, useMemo, useCallback } from "react"; -import { usePublicPreviewInteraction } from "@/uhm/components/preview/hooks/usePublicPreviewInteraction"; -import PresentPlaceSearch, { - type HistoricalGeometryFocusPayload, - type PresentPlaceSelection, -} from "@/uhm/components/editor/PresentPlaceSearch"; -import { fitMapToFeatureCollection } from "@/uhm/components/map/mapUtils"; -import type { FeatureCollection } from "@/uhm/types/geo"; -import { - type BackgroundLayerId, - type BackgroundLayerVisibility, - HIDDEN_BACKGROUND_LAYER_VISIBILITY, -} from "@/uhm/lib/map/styles/backgroundLayers"; -import { - loadBackgroundLayerVisibilityFromStorage, - persistBackgroundLayerVisibility, -} from "@/uhm/lib/editor/background/backgroundVisibilityStorage"; -import { GEO_TYPE_KEYS } from "@/uhm/lib/map/geo/geoTypeMap"; -import { clampYearToFixedRange, TIMELINE_DEBOUNCE_MS } from "@/uhm/lib/utils/timeline"; - -const CURRENT_YEAR = new Date().getUTCFullYear(); +const srOnlyStyle: React.CSSProperties = { + position: "absolute", + width: "1px", + height: "1px", + padding: "0", + margin: "-1px", + overflow: "hidden", + clip: "rect(0, 0, 0, 0)", + whiteSpace: "nowrap", + border: "0", +}; export default function Page() { - const [selectedFeatureIds, setSelectedFeatureIds] = useState<(string | number)[]>([]); - const [timelineYear, setTimelineYear] = useState(1000); - const [timelineDraftYear, setTimelineDraftYear] = useState(1000); - const [timeRange, setTimeRange] = useState(0); - const [backgroundVisibility, setBackgroundVisibility] = useState( - () => ({ ...HIDDEN_BACKGROUND_LAYER_VISIBILITY }) - ); - const [isBackgroundVisibilityReady, setIsBackgroundVisibilityReady] = useState(false); - const [geometryVisibility, setGeometryVisibility] = useState>(() => { - const init: Record = {}; - for (const key of GEO_TYPE_KEYS) init[key] = true; - return init; - }); - const [sidebarWidth, setSidebarWidth] = useState(() => { - if (typeof window !== "undefined") { - const saved = localStorage.getItem("public-wiki-sidebar-width"); - if (saved) { - const parsed = parseInt(saved, 10); - if (!isNaN(parsed) && parsed >= 320 && parsed <= 800) return parsed; - } - } - return 420; - }); - const [isLargeScreen, setIsLargeScreen] = useState(false); - - const mapHandleRef = useRef(null); - const isFirstMount = useRef(true); - const [replayMode, setReplayMode] = useState<"idle" | "playing">("idle"); - const [selectedReplayStageId, setSelectedReplayStageId] = useState(null); - const [selectedReplayStepIndex, setSelectedReplayStepIndex] = useState(null); - const [focusedPresentPlace, setFocusedPresentPlace] = useState(null); - - const [searchTimelineYear, setSearchTimelineYear] = useState(timelineYear); - useEffect(() => { - if (replayMode !== "playing") { - setSearchTimelineYear(timelineYear); - } - }, [timelineYear, replayMode]); - - const { - data, - renderDraft, - labelContextDraft, - relations, - setRelations, - isTimelineLoading, - timelineStatus, - isRelationsLoading, - relationsStatus, - replays, - } = usePublicPreviewData({ timelineYear: searchTimelineYear, timeRange }); - - const activeReplay = useMemo(() => { - if (!selectedFeatureIds.length || !replays?.length) return null; - for (const featureId of selectedFeatureIds) { - const id = String(featureId); - // 1. Direct geometry_id match (priority) - for (const replay of replays) { - if (String(replay.geometry_id || "").trim() === id) { - const firstStage = replay.detail?.find((s) => Array.isArray(s?.steps) && s.steps.length > 0); - if (firstStage) { - return { replay, stageId: firstStage.id, stepIndex: 0 }; - } - } - } - // 2. Fallback: Check inside steps parameters - for (const replay of replays) { - for (const stage of replay.detail || []) { - for (let stepIndex = 0; stepIndex < (stage.steps || []).length; stepIndex++) { - const step = stage.steps[stepIndex]; - if (step?.use_geo_function?.some((g) => g.params && Array.isArray(g.params) && g.params.some((p) => String(p) === id))) { - return { replay, stageId: stage.id, stepIndex }; - } - } - } - } - } - return null; - }, [replays, selectedFeatureIds]); - - const getMapInstance = useCallback(() => mapHandleRef.current?.getMap() || null, []); - const handleSelectReplayStep = useCallback((stageId: number | null, stepIndex: number | null) => { - setSelectedReplayStageId(stageId); - setSelectedReplayStepIndex(stepIndex); - }, []); - - const replayPreview = useReplayPreview({ - replay: activeReplay?.replay || null, - draft: renderDraft, - getMapInstance, - initialTimelineYear: timelineDraftYear, - initialTimelineFilterEnabled: false, - initialMapViewState: null, - selectedStageId: selectedReplayStageId, - selectedStepIndex: selectedReplayStepIndex, - onSelectStep: handleSelectReplayStep, - }); - - const { - activeEntity, - activeWiki, - isActiveWikiLoading, - activeWikiError, - linkEntityPopup, - linkEntityPopupRef, - getHoverPopupContent, - selectEntity, - handleWikiLinkRequest, - closeWikiSidebar, - setLinkEntityPopup, - isManualSidebarOpen, - } = usePublicPreviewInteraction({ - data, - relations, - setRelations, - selectedFeatureIds, - setSelectedFeatureIds, - replayActiveWikiId: replayPreview.activeWikiId, - replayMode, - }); - - useEffect(() => { - if (typeof window === "undefined") return; - const handleResize = () => { - setIsLargeScreen(window.innerWidth >= 1024); - }; - handleResize(); - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); - }, []); - - useEffect(() => { - const timeoutId = window.setTimeout(() => { - if (timelineDraftYear !== timelineYear) setTimelineYear(timelineDraftYear); - }, TIMELINE_DEBOUNCE_MS); - return () => window.clearTimeout(timeoutId); - }, [timelineDraftYear, timelineYear]); - - useEffect(() => { - if (typeof window !== "undefined") { - const saved = localStorage.getItem("timeline-year"); - if (saved) { - const parsed = parseInt(saved, 10); - if (!isNaN(parsed)) { - const clamped = clampYearToFixedRange(parsed); - setTimelineYear(clamped); - setTimelineDraftYear(clamped); - } - } - } - }, []); - - useEffect(() => { - if (isFirstMount.current) { - isFirstMount.current = false; - return; - } - if (typeof window !== "undefined") { - localStorage.setItem("timeline-year", String(timelineYear)); - } - }, [timelineYear]); - - useEffect(() => { - const timeoutId = window.setTimeout(() => { - setBackgroundVisibility(loadBackgroundLayerVisibilityFromStorage()); - setIsBackgroundVisibilityReady(true); - }, 0); - return () => window.clearTimeout(timeoutId); - }, []); - - const maxDragWidth = typeof window !== "undefined" - ? Math.min(800, window.innerWidth - 340) - : 800; - - const updateBackgroundVisibility = (updater: (prev: BackgroundLayerVisibility) => BackgroundLayerVisibility) => { - setBackgroundVisibility((prev) => { - const next = updater(prev); - persistBackgroundLayerVisibility(next); - return next; - }); - }; - - const handleToggleBackgroundLayer = (id: BackgroundLayerId) => { - updateBackgroundVisibility((prev) => ({ ...prev, [id]: !prev[id] })); - }; - - const handleTimelineYearChange = (nextYear: number) => { - setTimelineDraftYear(clampYearToFixedRange(Math.trunc(nextYear))); - }; - - const handleTimeRangeChange = (nextRange: number) => { - const safe = Number.isFinite(nextRange) ? Math.trunc(nextRange) : 0; - setTimeRange(Math.max(0, Math.min(30, safe))); - }; - - useEffect(() => { - if (replayMode === "playing" && !replayPreview.isPlaying) { - replayPreview.playFromSelection(); - } - }, [replayMode, replayPreview.isPlaying, replayPreview.playFromSelection]); - - const handlePlayPreviewReplay = useCallback(() => { - if (!activeReplay) return; - setReplayMode("playing"); - setSelectedReplayStageId(activeReplay.stageId); - setSelectedReplayStepIndex(activeReplay.stepIndex); - }, [activeReplay]); - - const handleExitReplay = useCallback(() => { - setReplayMode("idle"); - replayPreview.resetPreview(); - setFocusedPresentPlace(null); - }, [replayPreview]); - - const handleFocusPresentPlace = useCallback((place: PresentPlaceSelection) => { - setFocusedPresentPlace(place); - const map = mapHandleRef.current?.getMap(); - if (map) { - const currentZoom = map.getZoom(); - map.flyTo({ - center: [place.lng, place.lat], - zoom: Math.max(currentZoom, 13.5), - }); - } - }, []); - - const clearPresentPlaceFocus = useCallback(() => { - setFocusedPresentPlace(null); - }, []); - - const handleFocusHistoricalGeometry = useCallback((payload: HistoricalGeometryFocusPayload) => { - setFocusedPresentPlace(null); - - const map = mapHandleRef.current?.getMap(); - if (map && payload.geometry?.draw_geometry) { - const fc: FeatureCollection = { - type: "FeatureCollection", - features: [ - { - type: "Feature", - properties: { - id: payload.geometry.id, - }, - geometry: payload.geometry.draw_geometry, - }, - ], - }; - fitMapToFeatureCollection(map, fc, 84, { duration: 1000 }); - } - - if (payload.geometry.time_start != null) { - handleTimelineYearChange(payload.geometry.time_start); - } - - setSelectedFeatureIds([payload.geometry.id]); - - const linkedEntityIds = relations.geometryEntityIds[String(payload.geometry.id)] || []; - if (linkedEntityIds.length === 1) { - selectEntity(linkedEntityIds[0], { - sourceFeatureId: payload.geometry.id, - selectGeometry: false, - }); - } - }, [relations.geometryEntityIds, selectEntity, setSelectedFeatureIds]); - - const filteredRenderDraft = useMemo(() => { - if (replayMode !== "playing" || !replayPreview.hiddenGeometryIds?.length) { - return renderDraft; - } - const hiddenIds = new Set(replayPreview.hiddenGeometryIds); - return { - type: "FeatureCollection" as const, - features: renderDraft.features.filter( - (feature) => !hiddenIds.has(String(feature.properties.id)) - ), - }; - }, [replayMode, renderDraft, replayPreview.hiddenGeometryIds]); - - const filteredLabelContextDraft = useMemo(() => { - if (replayMode !== "playing" || !replayPreview.hiddenGeometryIds?.length) { - return labelContextDraft; - } - const hiddenIds = new Set(replayPreview.hiddenGeometryIds); - return { - type: "FeatureCollection" as const, - features: labelContextDraft.features.filter( - (feature) => !hiddenIds.has(String(feature.properties.id)) - ), - }; - }, [replayMode, labelContextDraft, replayPreview.hiddenGeometryIds]); - - const currentTimelineYear = replayMode === "playing" ? replayPreview.timelineYear : timelineDraftYear; - - const activeStepLabel = useMemo(() => { - if ( - replayPreview.activeCursor.stageId == null || - replayPreview.activeCursor.stepIndex == null - ) { - return null; - } - return `Stage #${replayPreview.activeCursor.stageId} · Step ${replayPreview.activeCursor.stepIndex + 1}`; - }, [replayPreview.activeCursor.stageId, replayPreview.activeCursor.stepIndex]); - - const isSidebarOpen = replayMode === "playing" - ? (replayPreview.sidebarOpen || isManualSidebarOpen) - : Boolean(activeEntity); - - const displayedActiveEntity = isSidebarOpen ? activeEntity : null; - const displayedActiveWiki = isSidebarOpen ? activeWiki : null; - - const computedTimelineStyle = useMemo(() => { - const rightMargin = (displayedActiveEntity && isLargeScreen) ? sidebarWidth + 32 : 18; - return { - left: "88px", - right: `${rightMargin}px`, - transition: "right 0.3s cubic-bezier(0.4, 0, 0.2, 1), left 0.3s cubic-bezier(0.4, 0, 0.2, 1)", - }; - }, [displayedActiveEntity, isLargeScreen, sidebarWidth]); - return ( - <> - {isBackgroundVisibilityReady ? ( - { - setGeometryVisibility((prev) => ({ - ...prev, - [typeKey]: prev[typeKey] === false, - })); - }} - timelineYear={currentTimelineYear} - onTimelineYearChange={handleTimelineYearChange} - timelineTimeRange={timeRange} - onTimelineTimeRangeChange={handleTimeRangeChange} - isTimelineLoading={isTimelineLoading || isRelationsLoading} - timelineStatusText={relationsStatus || timelineStatus} - timelineStyle={computedTimelineStyle} - hoverPopupEnabled - getHoverPopupContent={getHoverPopupContent} - activeEntity={displayedActiveEntity} - activeWiki={displayedActiveWiki} - isWikiLoading={isActiveWikiLoading} - wikiError={activeWikiError} - onCloseWikiSidebar={closeWikiSidebar} - onWikiLinkRequest={handleWikiLinkRequest} - sidebarWidth={sidebarWidth} - onSidebarWidthChange={setSidebarWidth} - maxSidebarDragWidth={maxDragWidth} - onPlayPreviewReplay={activeReplay && replayMode === "idle" ? handlePlayPreviewReplay : undefined} - timelineDisabled={replayMode === "playing"} - overlay={ - replayMode === "playing" ? ( - - ) : null - } - > -
- -
-
- ) : ( -
- )} +
+ {/* Preload LCP image */} + - {linkEntityPopup ? ( -
-
-
- Related Entities -
-
- /wiki/{linkEntityPopup.slug} -
-
-
-
- {linkEntityPopup.entities.map((entity) => ( - - ))} -
-
+ {/* Permanent, static LCP image that is NEVER hidden or unmounted */} + {/* eslint-disable-next-line @next/next/no-img-element */} + Map Background + + {/* Header (SSR & SEO) */} +
+ +
+ + {/* Main Content & Semantic Heading (SSR & SEO) */} +
+
+

Ultimate History Map - Bản Đồ Tương Tác Lịch Sử

+

+ Dự án Ultimate History Map cung cấp cái nhìn trực quan và sinh động về sự thay đổi biên giới, các quốc gia, sự kiện lịch sử thế giới theo từng năm. +

+

+ 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ử và chiến trận (Replay). + - Tra cứu thông tin sự kiện lịch sử (Wiki & Entities). +

- ) : null} - + + {/* Stateful Interactive Client Component */} + +
+ + {/* Footer (SSR & SEO) */} +
+

© {new Date().getFullYear()} Ultimate History Map. All rights reserved.

+
+
); } diff --git a/src/uhm/api/battleReplays.ts b/src/uhm/api/battleReplays.ts index 61e1070..a2a6cae 100644 --- a/src/uhm/api/battleReplays.ts +++ b/src/uhm/api/battleReplays.ts @@ -2,8 +2,8 @@ import { API_ENDPOINTS } from "@/uhm/api/config"; import { requestJson } from "@/uhm/api/http"; import type { BattleReplay } from "@/uhm/types/projects"; -const BATCH_SIZE = 20; -const BATCH_CONCURRENCY = 6; +const BATCH_SIZE = 100; +const BATCH_CONCURRENCY = 4; export async function fetchBattleReplaysByGeometryIds(geometryIds: string[]): Promise> { const uniqueIds = Array.from(new Set( diff --git a/src/uhm/api/relations.ts b/src/uhm/api/relations.ts index ab20d5a..3ac9728 100644 --- a/src/uhm/api/relations.ts +++ b/src/uhm/api/relations.ts @@ -4,7 +4,7 @@ import type { Entity } from "@/uhm/api/entities"; import type { Wiki } from "@/uhm/api/wikis"; const RELATION_BATCH_SIZE = 20; -const RELATION_BATCH_CONCURRENCY = 10; +const RELATION_BATCH_CONCURRENCY = 4; export type WikiContentPreview = { id: string; diff --git a/src/uhm/components/Map.tsx b/src/uhm/components/Map.tsx index 7678185..02062d2 100644 --- a/src/uhm/components/Map.tsx +++ b/src/uhm/components/Map.tsx @@ -1,8 +1,6 @@ "use client"; import { type CSSProperties, useEffect, useRef, forwardRef, useImperativeHandle, memo } from "react"; -import "maplibre-gl/dist/maplibre-gl.css"; - import { Feature, FeatureCollection, Geometry } from "@/uhm/lib/editor/state/useEditorState"; import { BackgroundLayerVisibility } from "@/uhm/lib/map/styles/backgroundLayers"; import type { EditorMode } from "@/uhm/lib/editor/session/sessionTypes"; @@ -74,6 +72,7 @@ type MapProps = { onPlayPreviewReplay?: () => void; viewMode?: "local" | "global"; onViewModeChange?: (mode: "local" | "global") => void; + onLoad?: () => void; }; const Map = memo(forwardRef(function Map({ @@ -114,6 +113,7 @@ const Map = memo(forwardRef(function Map({ onPlayPreviewReplay, viewMode = "local", onViewModeChange, + onLoad, }, ref) { // Ref giữ mode mới nhất cho MapLibre handlers được register một lần. const modeRef = useRef(mode); @@ -161,6 +161,11 @@ const Map = memo(forwardRef(function Map({ onBindGeometriesRef.current = onBindGeometries; localFeatureIdsRef.current = localFeatureIds; + useEffect(() => { + // Dynamically import MapLibre CSS to prevent it from blocking initial layout bundle CSS load. + import("maplibre-gl/dist/maplibre-gl.css"); + }, []); + // Hook sở hữu lifecycle MapLibre instance và các control camera/projection. const { mapRef, @@ -270,6 +275,12 @@ const Map = memo(forwardRef(function Map({ } }, [mode, isMapLoaded, mapRef]); + useEffect(() => { + if (isMapLoaded && onLoad) { + onLoad(); + } + }, [isMapLoaded, onLoad]); + const hasImageOverlay = Boolean(imageOverlay); useEffect(() => { const map = mapRef.current; @@ -282,8 +293,32 @@ const Map = memo(forwardRef(function Map({ }, [hasImageOverlay, isMapLoaded, mapRef]); return ( -
-
+
+ {/* Opaque map placeholder image for LCP optimization */} + Map Loading Placeholder + +
{fatalInitError ? (
- - onAppendActions( - [{ function_name: "set_timeline_filter", params: [true] }], - "Map: enable timeline filter" - ) - } - /> - - onAppendActions( - [{ function_name: "set_timeline_filter", params: [false] }], - "Map: disable timeline filter" - ) - } - />
@@ -877,6 +857,28 @@ function GeoFunctionShortcutPanel({ ) } /> + + onAppendActions( + [{ function_name: "set_as_background_geometries", params: [selectedIds] }], + `Geo: đặt ${selectedCount} geo làm background` + ) + } + /> + + onAppendActions( + [{ function_name: "remove_from_background_geometries", params: [selectedIds] }], + `Geo: loại ${selectedCount} geo khỏi background` + ) + } + />
diff --git a/src/uhm/components/editor/ReplayTimelineSidebar.tsx b/src/uhm/components/editor/ReplayTimelineSidebar.tsx index 5eee78e..5d9ffc2 100644 --- a/src/uhm/components/editor/ReplayTimelineSidebar.tsx +++ b/src/uhm/components/editor/ReplayTimelineSidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMemo, useState } from "react"; +import { useMemo, useState, useRef } from "react"; import type { BattleReplay, GeoFunctionName, @@ -91,6 +91,7 @@ export default function ReplayTimelineSidebar({ onStopPreview, onResetPreview, }: Props) { + const fileInputRef = useRef(null); const stages = useMemo(() => replay?.detail || [], [replay?.detail]); const selectedStage = stages.find((stage) => stage.id === selectedStageId) || @@ -157,6 +158,77 @@ export default function ReplayTimelineSidebar({ window.URL.revokeObjectURL(url); }; + const handleImportReplayJsonClick = () => { + fileInputRef.current?.click(); + }; + + const handleImportReplayJson = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const jsonText = e.target?.result as string; + const parsed = JSON.parse(jsonText); + + let importedReplay: BattleReplay | null = null; + if (parsed && typeof parsed === "object") { + if (parsed.current_replay && typeof parsed.current_replay === "object") { + importedReplay = parsed.current_replay as BattleReplay; + } else if (parsed.id && parsed.target_geometry_ids && Array.isArray(parsed.detail)) { + importedReplay = parsed as BattleReplay; + } + } + + if (!importedReplay || !Array.isArray(importedReplay.detail)) { + alert("Định dạng file JSON không hợp lệ cho Replay!"); + return; + } + + if (replay && importedReplay.geometry_id !== replay.geometry_id) { + const confirmImport = window.confirm( + `Geometry ID của replay nhập vào (${importedReplay.geometry_id}) khác với geometry ID hiện tại (${replay.geometry_id}). Bạn có muốn tiếp tục?` + ); + if (!confirmImport) return; + } + + onMutateReplay("Replay: import JSON", (draftReplay) => { + draftReplay.detail = importedReplay!.detail; + draftReplay.target_geometry_ids = importedReplay!.target_geometry_ids || draftReplay.target_geometry_ids; + draftReplay.id = replay?.id || draftReplay.id; + draftReplay.geometry_id = replay?.geometry_id || draftReplay.geometry_id; + }); + } catch (err) { + alert("Lỗi đọc file JSON: " + (err as Error).message); + } + }; + reader.readAsText(file); + event.target.value = ""; + }; + +function getBackgroundGeometryIdsFromReplay(replay: any): Set { + const bgIds = new Set(); + if (!replay || !Array.isArray(replay.detail)) return bgIds; + for (const stage of replay.detail) { + if (!Array.isArray(stage.steps)) continue; + for (const step of stage.steps) { + if (Array.isArray(step.use_geo_function)) { + for (const action of step.use_geo_function) { + if (action.function_name === "set_as_background_geometries") { + const ids = Array.isArray(action.params[0]) ? action.params[0] : []; + for (const id of ids) bgIds.add(String(id)); + } else if (action.function_name === "remove_from_background_geometries") { + const ids = Array.isArray(action.params[0]) ? action.params[0] : []; + for (const id of ids) bgIds.delete(String(id)); + } + } + } + } + } + return bgIds; +} + const handleCreateStage = () => { if (!replay) return; if (!validateReplayTimeFormat(createStageForm.detail_time_start) || @@ -167,6 +239,19 @@ export default function ReplayTimelineSidebar({ stages.length > 0 ? Math.max(...stages.map((stage) => stage.id)) + 1 : 0; + + const bgIds = getBackgroundGeometryIdsFromReplay(replay); + const geometriesToHide = (replay.target_geometry_ids || []).filter( + (id: string) => !bgIds.has(String(id)) + ); + const initialGeoFunctions = []; + if (geometriesToHide.length > 0) { + initialGeoFunctions.push({ + function_name: "set_geometry_visibility" as const, + params: [geometriesToHide, false], + }); + } + const nextStage: ReplayStage = { id: nextId, title: createStageForm.title.trim() || undefined, @@ -177,7 +262,7 @@ export default function ReplayTimelineSidebar({ duration: 5000, use_UI_function: [], use_map_function: [], - use_geo_function: [], + use_geo_function: initialGeoFunctions, use_narrow_function: [], }, ], @@ -413,7 +498,7 @@ export default function ReplayTimelineSidebar({ ? `Có ${pendingSaveCount} thay đổi chưa commit. Thoát replay để commit từ editor chính.` : "Replay đang đồng bộ với snapshot hiện tại."}
-
+
+ + -
+
= { const mapFunctionLabels: Record = { set_camera_view: "Camera view", - set_timeline_filter: "Lọc timeline", set_labels_visible: "Hiện nhãn map", }; @@ -1328,6 +1433,8 @@ const geoFunctionLabels: Record = { animate_dashed_border: "Border nét đứt", set_geometry_style: "Style geometry", orbit_camera_around_geometry: "Orbit quanh geo", + set_as_background_geometries: "Đặt làm background", + remove_from_background_geometries: "Loại khỏi background", }; function buildStepActionEntries(step: ReplayStep): StepActionEntry[] { @@ -1392,9 +1499,6 @@ function buildMapActionEntry( let summary = "Không có tham số."; switch (action.function_name) { - case "set_timeline_filter": - summary = `enabled=${Boolean(params[0] ?? true) ? "true" : "false"}`; - break; case "set_labels_visible": summary = `visible=${Boolean(params[0] ?? true) ? "true" : "false"}`; break; @@ -1481,6 +1585,12 @@ function buildGeoActionEntry( `keep=${summarizeGeometryIdsValue(params[0])}`, ].join(" | "); break; + case "set_as_background_geometries": + summary = `geometry=${summarizeGeometryIdsValue(params[0])}`; + break; + case "remove_from_background_geometries": + summary = `geometry=${summarizeGeometryIdsValue(params[0])}`; + break; } return { diff --git a/src/uhm/components/map/useMapInstance.ts b/src/uhm/components/map/useMapInstance.ts index 8780ba0..da73066 100644 --- a/src/uhm/components/map/useMapInstance.ts +++ b/src/uhm/components/map/useMapInstance.ts @@ -6,6 +6,7 @@ import { getBaseMapStyle } from "./useMapLayers"; import { unregisterMapFromIconUpdates } from "@/uhm/lib/map/styles/geotypeLayers"; const MAP_PROJECTION_STORAGE_KEY = "uhm:mapProjection"; +const MAP_VIEWPORT_STORAGE_KEY = "uhm:mapViewport"; export function applyMapProjection(map: maplibregl.Map, isGlobe: boolean) { map.setProjection({ type: isGlobe ? "globe" : "mercator" }); @@ -50,18 +51,48 @@ export function useMapInstance() { if (!container) return; try { + let initialCenter: [number, number] = [0, 20]; + let initialZoom = 2; + try { + const saved = window.localStorage.getItem(MAP_VIEWPORT_STORAGE_KEY); + if (saved) { + const parsed = JSON.parse(saved); + if (Number.isFinite(parsed.lng) && Number.isFinite(parsed.lat) && Number.isFinite(parsed.zoom)) { + initialCenter = [parsed.lng, parsed.lat]; + initialZoom = parsed.zoom; + } + } + } catch { + // ignore + } + const map = new maplibregl.Map({ container, attributionControl: false, minZoom: MAP_MIN_ZOOM, maxZoom: MAP_MAX_ZOOM, style: getBaseMapStyle(), - center: [0, 20], - zoom: 2, + center: initialCenter, + zoom: initialZoom, }); mapRef.current = map; + const saveViewport = () => { + const currentMap = mapRef.current; + if (!currentMap) return; + try { + const center = currentMap.getCenter(); + const zoom = currentMap.getZoom(); + window.localStorage.setItem( + MAP_VIEWPORT_STORAGE_KEY, + JSON.stringify({ lng: center.lng, lat: center.lat, zoom }) + ); + } catch { + // ignore + } + }; + let throttleTimeout: any = null; const syncZoomLevelImmediate = () => { @@ -87,6 +118,7 @@ export function useMapInstance() { syncZoomLevelImmediate(); map.on("zoom", syncZoomLevelThrottled); map.on("zoomend", syncZoomLevelImmediate); + map.on("moveend", saveViewport); setIsMapLoaded(true); }); @@ -96,6 +128,7 @@ export function useMapInstance() { } map.off("zoom", syncZoomLevelThrottled); map.off("zoomend", syncZoomLevelImmediate); + map.off("moveend", saveViewport); setIsMapLoaded(false); if (mapRef.current === map) { mapRef.current = null; diff --git a/src/uhm/components/map/useMapSync.ts b/src/uhm/components/map/useMapSync.ts index c88c04f..95b666f 100644 --- a/src/uhm/components/map/useMapSync.ts +++ b/src/uhm/components/map/useMapSync.ts @@ -161,6 +161,17 @@ export function useMapSync({ if (geolocationCenteredRef.current) return; if (fitToDraftBoundsRef.current) return; if (typeof window === "undefined") return; + + // Nếu đã có tọa độ lưu từ phiên làm việc trước, không tự động dịch chuyển nữa + try { + if (window.localStorage.getItem("uhm:mapViewport")) { + geolocationCenteredRef.current = true; + return; + } + } catch { + // ignore + } + if (!("geolocation" in navigator)) return; const map = mapRef.current; @@ -175,7 +186,8 @@ export function useMapSync({ const currentZoom = map.getZoom(); const nextZoom = Number.isFinite(currentZoom) ? Math.max(currentZoom, 5) : 5; - map.easeTo({ center: [longitude, latitude], zoom: nextZoom, duration: 900 }); + // Dùng jumpTo để teleport lập tức, loại bỏ hoạt ảnh trượt camera kéo dài + map.jumpTo({ center: [longitude, latitude], zoom: nextZoom }); }, () => { }, { enableHighAccuracy: false, timeout: 4000, maximumAge: 60_000 } diff --git a/src/uhm/components/preview/MapPlaceholder.tsx b/src/uhm/components/preview/MapPlaceholder.tsx new file mode 100644 index 0000000..1ab18b0 --- /dev/null +++ b/src/uhm/components/preview/MapPlaceholder.tsx @@ -0,0 +1,225 @@ +"use client"; + +import React from "react"; + +interface MapPlaceholderProps { + isLoaderOnly?: boolean; + onEnter?: () => void; +} + +export default function MapPlaceholder({ isLoaderOnly = true, onEnter }: MapPlaceholderProps) { + if (isLoaderOnly) { + return ( +
+ {/* Background Image */} + {/* eslint-disable-next-line @next/next/no-img-element */} + Map Loading Placeholder + + {/* Dark overlay & Spinner */} +
+
+