From 9080e1193d0596804f9b64a9eb63e51a65edfafb Mon Sep 17 00:00:00 2001 From: taDuc Date: Mon, 15 Jun 2026 23:15:36 +0700 Subject: [PATCH] feat: add pathname proxy for layout conditional styling, introduce sidebar interaction tracking, and enhance ReplayPreviewOverlay with simplified view and dynamic pointer event blocking. --- src/app/layout.tsx | 11 ++- src/proxy.ts | 20 +++++ .../editor/ReplayPreviewOverlay.tsx | 86 +++++++++++++------ .../components/preview/PreviewMapShell.tsx | 5 +- .../preview/PublicPreviewClientPage.tsx | 20 ++++- src/uhm/components/wiki/PublicWikiSidebar.tsx | 7 +- 6 files changed, 115 insertions(+), 34 deletions(-) create mode 100644 src/proxy.ts diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a230ef3..e879a0e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Inter } from 'next/font/google'; import './globals.css'; import { Toaster } from 'sonner'; import StoreProvider from '@/store/StoreProvider'; +import { headers } from 'next/headers'; export const metadata: Metadata = { title: 'Ultimate History Map', @@ -16,14 +17,18 @@ const inter = Inter({ display: 'swap', }); -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const headersList = await headers(); + const pathname = headersList.get('x-pathname') || '/'; + const isPublicRoot = pathname === '/'; + return ( - - + + {children} diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 0000000..be05470 --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,20 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export function proxy(request: NextRequest) { + const requestHeaders = new Headers(request.headers); + requestHeaders.set("x-pathname", request.nextUrl.pathname); + + return NextResponse.next({ + request: { + headers: requestHeaders, + }, + }); +} + +export const config = { + matcher: [ + // Match all paths except static files, api, images, fonts, icons, etc. + "/((?!api|_next/static|_next/image|favicon.ico|images|font|icon).*)", + ], +}; diff --git a/src/uhm/components/editor/ReplayPreviewOverlay.tsx b/src/uhm/components/editor/ReplayPreviewOverlay.tsx index 54b20b1..e8a344b 100644 --- a/src/uhm/components/editor/ReplayPreviewOverlay.tsx +++ b/src/uhm/components/editor/ReplayPreviewOverlay.tsx @@ -11,11 +11,14 @@ type Props = { toasts: ReplayPreviewToast[]; sidebarOpen: boolean; sidebarWidth?: number; + sidebarHeight?: number; + isLargeScreen?: boolean; playbackSpeed: number; activeStepLabel: string | null; activeStepNumber: number | null; totalSteps: number; playButtonLabel?: string; + simplified?: boolean; onPlayPreview: () => void; onStopPreview: () => void; onResetPreview: () => void; @@ -29,11 +32,14 @@ export default function ReplayPreviewOverlay({ toasts, sidebarOpen, sidebarWidth = 420, + sidebarHeight = 400, + isLargeScreen = true, playbackSpeed, activeStepLabel, activeStepNumber, totalSteps, playButtonLabel = "Phát lại", + simplified = false, onPlayPreview, onStopPreview, onResetPreview, @@ -57,11 +63,32 @@ export default function ReplayPreviewOverlay({ position: "absolute", inset: 0, zIndex: 60, - // Intentional feature: block map interaction during replay preview mode to prevent conflicts/bugs - // Tính năng có chủ ý: chặn tương tác với bản đồ trong lúc chạy replay để tránh lỗi/xung đột - pointerEvents: isPreviewMode ? "auto" : "none", + pointerEvents: "none", }} > + {/* Intentional feature: block map interaction during replay preview mode to prevent conflicts/bugs */} + {/* Tính năng có chủ ý: chặn tương tác với bản đồ trong lúc chạy replay để tránh lỗi/xung đột */} + {isPreviewMode && ( +
+ )} + {toasts.length ? (
{toasts.map((toast) => ( @@ -200,22 +228,24 @@ export default function ReplayPreviewOverlay({ >
- - Xem trước - + {!simplified && ( + + Xem trước + + )} {activeStepLabel ? ( ) : null} - - x{playbackSpeed.toFixed(2)} - + {!simplified && ( + + x{playbackSpeed.toFixed(2)} + + )}
{totalSteps > 0 ? (
@@ -253,9 +285,11 @@ export default function ReplayPreviewOverlay({ }} />
-
- Bước {activeStepNumber || 0}/{totalSteps} -
+ {!simplified && ( +
+ Bước {activeStepNumber || 0}/{totalSteps} +
+ )}
) : null}
@@ -291,7 +325,7 @@ export default function ReplayPreviewOverlay({ onClick={onExitPreview} style={previewButtonStyle("#334155")} > - Thoát xem trước + {simplified ? "Thoát trình chiếu" : "Thoát xem trước"}
diff --git a/src/uhm/components/preview/PreviewMapShell.tsx b/src/uhm/components/preview/PreviewMapShell.tsx index 9daf660..bac7850 100644 --- a/src/uhm/components/preview/PreviewMapShell.tsx +++ b/src/uhm/components/preview/PreviewMapShell.tsx @@ -46,6 +46,7 @@ type Props = { onCloseWikiSidebar?: () => void; onWikiLinkRequest?: (request: { slug: string; rect: DOMRect }) => void; onWikiLinkEntitySelectionRequest?: (request: { slug: string; rect: DOMRect }) => void; + onWikiLinkInteraction?: () => void; sidebarWidth?: number; onSidebarWidthChange?: (width: number) => void; maxSidebarDragWidth?: number; @@ -95,6 +96,7 @@ export default function PreviewMapShell({ onCloseWikiSidebar, onWikiLinkRequest, onWikiLinkEntitySelectionRequest, + onWikiLinkInteraction, sidebarWidth, onSidebarWidthChange, maxSidebarDragWidth, @@ -218,7 +220,7 @@ export default function PreviewMapShell({ top: 10, bottom: (hasBottomPanel && isMobileOrTablet) ? `${(sidebarHeight || 400) + 20}px` : 20, left: 18, - zIndex: 18, + zIndex: 70, display: "flex", flexDirection: "column", gap: 12, @@ -448,6 +450,7 @@ export default function PreviewMapShell({ onClose={onCloseWikiSidebar || (() => {})} onWikiLinkRequest={onWikiLinkRequest || (() => {})} onWikiLinkEntitySelectionRequest={onWikiLinkEntitySelectionRequest} + onWikiLinkInteraction={onWikiLinkInteraction} sidebarWidth={sidebarWidth} onSidebarWidthChange={onSidebarWidthChange} maxDragWidth={maxSidebarDragWidth} diff --git a/src/uhm/components/preview/PublicPreviewClientPage.tsx b/src/uhm/components/preview/PublicPreviewClientPage.tsx index 699cd16..c9839bf 100644 --- a/src/uhm/components/preview/PublicPreviewClientPage.tsx +++ b/src/uhm/components/preview/PublicPreviewClientPage.tsx @@ -728,12 +728,19 @@ export default function PublicPreviewClientPage({ const activeStepLabel = useMemo(() => { if ( replayPreview.activeCursor.stageId == null || - replayPreview.activeCursor.stepIndex == null + replayPreview.activeCursor.stepIndex == null || + !activeReplay?.replay ) { return null; } - return `Cảnh #${replayPreview.activeCursor.stageId} · Bước ${replayPreview.activeCursor.stepIndex + 1}`; - }, [replayPreview.activeCursor.stageId, replayPreview.activeCursor.stepIndex]); + const stage = activeReplay.replay.detail?.find( + (s) => s.id === replayPreview.activeCursor.stageId + ); + if (stage && stage.title?.trim()) { + return stage.title.trim(); + } + return `Cảnh #${replayPreview.activeCursor.stageId}`; + }, [replayPreview.activeCursor.stageId, replayPreview.activeCursor.stepIndex, activeReplay]); const isWikiChooserOpen = rightPanelMode === "selection" && Boolean(wikiSelectionPanelAnchor); const isGeometryChooserOpen = rightPanelMode === "geometry-selection" && Boolean(geometrySelectionPanel); @@ -846,6 +853,7 @@ export default function PublicPreviewClientPage({ onCloseWikiSidebar={handleCloseWikiSidebar} onWikiLinkRequest={handlePanelWikiLinkRequest} onWikiLinkEntitySelectionRequest={handlePanelWikiLinkEntitySelectionRequest} + onWikiLinkInteraction={replayMode !== "idle" ? handleExitReplay : undefined} sidebarWidth={sidebarWidth} onSidebarWidthChange={setSidebarWidth} maxSidebarDragWidth={maxDragWidth} @@ -864,11 +872,14 @@ export default function PublicPreviewClientPage({ toasts={replayPreview.toasts} sidebarOpen={isSidebarOpen} sidebarWidth={sidebarWidth} + sidebarHeight={sidebarHeight} + isLargeScreen={isLargeScreen} playbackSpeed={replayPreview.playbackSpeed} activeStepLabel={activeStepLabel} activeStepNumber={replayPreview.activeStepNumber} totalSteps={replayPreview.totalSteps} playButtonLabel={replayMode === "paused" ? "Tiếp tục" : "Phát lại"} + simplified={true} onPlayPreview={handleResumePreviewReplay} onStopPreview={handleStopPreviewReplay} onResetPreview={handleResetPreviewReplay} @@ -927,6 +938,9 @@ export default function PublicPreviewClientPage({ key={entity.id} type="button" onClick={() => { + if (replayMode !== "idle") { + handleExitReplay(); + } setWikiSelectionPanelAnchor(null); setRightPanelMode("wiki"); selectEntity(entity.id, { preferredWikiSlug: linkEntityPopup.slug }); diff --git a/src/uhm/components/wiki/PublicWikiSidebar.tsx b/src/uhm/components/wiki/PublicWikiSidebar.tsx index 0e5d7c5..744ce34 100644 --- a/src/uhm/components/wiki/PublicWikiSidebar.tsx +++ b/src/uhm/components/wiki/PublicWikiSidebar.tsx @@ -21,6 +21,7 @@ type Props = { onClose: () => void; onWikiLinkRequest: (request: { slug: string; rect: DOMRect }) => void; onWikiLinkEntitySelectionRequest?: (request: { slug: string; rect: DOMRect }) => void; + onWikiLinkInteraction?: () => void; sidebarWidth?: number; onSidebarWidthChange?: (width: number) => void; maxDragWidth?: number; @@ -173,6 +174,7 @@ function PublicWikiSidebar({ onClose, onWikiLinkRequest, onWikiLinkEntitySelectionRequest, + onWikiLinkInteraction, sidebarWidth, onSidebarWidthChange, maxDragWidth, @@ -308,12 +310,13 @@ function PublicWikiSidebar({ if (!slug.length) return; event.preventDefault(); + onWikiLinkInteraction?.(); onWikiLinkRequest({ slug, rect: sourceLink.getBoundingClientRect() }); }; root.addEventListener("click", handleClick); return () => root.removeEventListener("click", handleClick); - }, [onWikiLinkRequest, renderHtml]); + }, [onWikiLinkRequest, onWikiLinkInteraction, renderHtml]); useEffect(() => { const root = contentRootRef.current; @@ -371,6 +374,7 @@ function PublicWikiSidebar({ const handleOpenStandaloneWiki = (slug: string) => { if (typeof window === "undefined") return; + onWikiLinkInteraction?.(); const url = `/wiki/${encodeURIComponent(slug)}`; const nextWindow = window.open(url, "_blank", "noopener,noreferrer"); if (nextWindow) nextWindow.opener = null; @@ -722,6 +726,7 @@ function PublicWikiSidebar({ type="button" onClick={() => { const request = { slug: wikiLinkMenu.slug, rect: wikiLinkMenu.rect }; + onWikiLinkInteraction?.(); if (onWikiLinkEntitySelectionRequest) { onWikiLinkEntitySelectionRequest(request); } else {