feat: add pathname proxy for layout conditional styling, introduce sidebar interaction tracking, and enhance ReplayPreviewOverlay with simplified view and dynamic pointer event blocking.
Build and Release / release (push) Successful in 39s

This commit is contained in:
taDuc
2026-06-15 23:15:36 +07:00
parent 3b90549202
commit 9080e1193d
6 changed files with 115 additions and 34 deletions
+8 -3
View File
@@ -3,6 +3,7 @@ import { Inter } from 'next/font/google';
import './globals.css'; import './globals.css';
import { Toaster } from 'sonner'; import { Toaster } from 'sonner';
import StoreProvider from '@/store/StoreProvider'; import StoreProvider from '@/store/StoreProvider';
import { headers } from 'next/headers';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Ultimate History Map', title: 'Ultimate History Map',
@@ -16,14 +17,18 @@ const inter = Inter({
display: 'swap', display: 'swap',
}); });
export default function RootLayout({ export default async function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const headersList = await headers();
const pathname = headersList.get('x-pathname') || '/';
const isPublicRoot = pathname === '/';
return ( return (
<html lang="en" className={inter.variable}> <html lang="en" className={isPublicRoot ? '' : inter.variable}>
<body className={`${inter.className} dark:bg-gray-900`}> <body className={`${isPublicRoot ? 'font-sans' : inter.className} dark:bg-gray-900`}>
<StoreProvider> <StoreProvider>
{children} {children}
<Toaster closeButton richColors position="top-right" /> <Toaster closeButton richColors position="top-right" />
+20
View File
@@ -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).*)",
],
};
@@ -11,11 +11,14 @@ type Props = {
toasts: ReplayPreviewToast[]; toasts: ReplayPreviewToast[];
sidebarOpen: boolean; sidebarOpen: boolean;
sidebarWidth?: number; sidebarWidth?: number;
sidebarHeight?: number;
isLargeScreen?: boolean;
playbackSpeed: number; playbackSpeed: number;
activeStepLabel: string | null; activeStepLabel: string | null;
activeStepNumber: number | null; activeStepNumber: number | null;
totalSteps: number; totalSteps: number;
playButtonLabel?: string; playButtonLabel?: string;
simplified?: boolean;
onPlayPreview: () => void; onPlayPreview: () => void;
onStopPreview: () => void; onStopPreview: () => void;
onResetPreview: () => void; onResetPreview: () => void;
@@ -29,11 +32,14 @@ export default function ReplayPreviewOverlay({
toasts, toasts,
sidebarOpen, sidebarOpen,
sidebarWidth = 420, sidebarWidth = 420,
sidebarHeight = 400,
isLargeScreen = true,
playbackSpeed, playbackSpeed,
activeStepLabel, activeStepLabel,
activeStepNumber, activeStepNumber,
totalSteps, totalSteps,
playButtonLabel = "Phát lại", playButtonLabel = "Phát lại",
simplified = false,
onPlayPreview, onPlayPreview,
onStopPreview, onStopPreview,
onResetPreview, onResetPreview,
@@ -57,11 +63,32 @@ export default function ReplayPreviewOverlay({
position: "absolute", position: "absolute",
inset: 0, inset: 0,
zIndex: 60, zIndex: 60,
// Intentional feature: block map interaction during replay preview mode to prevent conflicts/bugs pointerEvents: "none",
// 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",
}} }}
> >
{/* 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 && (
<div
style={{
position: "absolute",
left: 0,
top: 0,
...(isLargeScreen
? {
bottom: 0,
right: sidebarOpen ? sidebarWidth + 16 : 0,
}
: {
right: 0,
bottom: sidebarOpen ? sidebarHeight + 16 : 0,
}),
pointerEvents: "auto",
background: "transparent",
}}
/>
)}
{toasts.length ? ( {toasts.length ? (
<div <div
style={{ style={{
@@ -71,6 +98,7 @@ export default function ReplayPreviewOverlay({
display: "grid", display: "grid",
gap: 8, gap: 8,
width: 280, width: 280,
pointerEvents: "auto",
}} }}
> >
{toasts.map((toast) => ( {toasts.map((toast) => (
@@ -200,6 +228,7 @@ export default function ReplayPreviewOverlay({
> >
<div style={{ display: "grid", gap: 6, minWidth: 0 }}> <div style={{ display: "grid", gap: 6, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}> <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
{!simplified && (
<span <span
style={{ style={{
display: "inline-flex", display: "inline-flex",
@@ -216,6 +245,7 @@ export default function ReplayPreviewOverlay({
> >
Xem trước Xem trước
</span> </span>
)}
{activeStepLabel ? ( {activeStepLabel ? (
<span <span
style={{ style={{
@@ -228,9 +258,11 @@ export default function ReplayPreviewOverlay({
{activeStepLabel} {activeStepLabel}
</span> </span>
) : null} ) : null}
{!simplified && (
<span style={{ fontSize: 12, color: "#94a3b8" }}> <span style={{ fontSize: 12, color: "#94a3b8" }}>
x{playbackSpeed.toFixed(2)} x{playbackSpeed.toFixed(2)}
</span> </span>
)}
</div> </div>
{totalSteps > 0 ? ( {totalSteps > 0 ? (
<div style={{ display: "grid", gap: 6 }}> <div style={{ display: "grid", gap: 6 }}>
@@ -253,9 +285,11 @@ export default function ReplayPreviewOverlay({
}} }}
/> />
</div> </div>
{!simplified && (
<div style={{ fontSize: 11, color: "#94a3b8" }}> <div style={{ fontSize: 11, color: "#94a3b8" }}>
Bước {activeStepNumber || 0}/{totalSteps} Bước {activeStepNumber || 0}/{totalSteps}
</div> </div>
)}
</div> </div>
) : null} ) : null}
</div> </div>
@@ -291,7 +325,7 @@ export default function ReplayPreviewOverlay({
onClick={onExitPreview} onClick={onExitPreview}
style={previewButtonStyle("#334155")} style={previewButtonStyle("#334155")}
> >
Thoát xem trước {simplified ? "Thoát trình chiếu" : "Thoát xem trước"}
</button> </button>
</div> </div>
</div> </div>
@@ -46,6 +46,7 @@ type Props = {
onCloseWikiSidebar?: () => void; onCloseWikiSidebar?: () => void;
onWikiLinkRequest?: (request: { slug: string; rect: DOMRect }) => void; onWikiLinkRequest?: (request: { slug: string; rect: DOMRect }) => void;
onWikiLinkEntitySelectionRequest?: (request: { slug: string; rect: DOMRect }) => void; onWikiLinkEntitySelectionRequest?: (request: { slug: string; rect: DOMRect }) => void;
onWikiLinkInteraction?: () => void;
sidebarWidth?: number; sidebarWidth?: number;
onSidebarWidthChange?: (width: number) => void; onSidebarWidthChange?: (width: number) => void;
maxSidebarDragWidth?: number; maxSidebarDragWidth?: number;
@@ -95,6 +96,7 @@ export default function PreviewMapShell({
onCloseWikiSidebar, onCloseWikiSidebar,
onWikiLinkRequest, onWikiLinkRequest,
onWikiLinkEntitySelectionRequest, onWikiLinkEntitySelectionRequest,
onWikiLinkInteraction,
sidebarWidth, sidebarWidth,
onSidebarWidthChange, onSidebarWidthChange,
maxSidebarDragWidth, maxSidebarDragWidth,
@@ -218,7 +220,7 @@ export default function PreviewMapShell({
top: 10, top: 10,
bottom: (hasBottomPanel && isMobileOrTablet) ? `${(sidebarHeight || 400) + 20}px` : 20, bottom: (hasBottomPanel && isMobileOrTablet) ? `${(sidebarHeight || 400) + 20}px` : 20,
left: 18, left: 18,
zIndex: 18, zIndex: 70,
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: 12, gap: 12,
@@ -448,6 +450,7 @@ export default function PreviewMapShell({
onClose={onCloseWikiSidebar || (() => {})} onClose={onCloseWikiSidebar || (() => {})}
onWikiLinkRequest={onWikiLinkRequest || (() => {})} onWikiLinkRequest={onWikiLinkRequest || (() => {})}
onWikiLinkEntitySelectionRequest={onWikiLinkEntitySelectionRequest} onWikiLinkEntitySelectionRequest={onWikiLinkEntitySelectionRequest}
onWikiLinkInteraction={onWikiLinkInteraction}
sidebarWidth={sidebarWidth} sidebarWidth={sidebarWidth}
onSidebarWidthChange={onSidebarWidthChange} onSidebarWidthChange={onSidebarWidthChange}
maxDragWidth={maxSidebarDragWidth} maxDragWidth={maxSidebarDragWidth}
@@ -728,12 +728,19 @@ export default function PublicPreviewClientPage({
const activeStepLabel = useMemo(() => { const activeStepLabel = useMemo(() => {
if ( if (
replayPreview.activeCursor.stageId == null || replayPreview.activeCursor.stageId == null ||
replayPreview.activeCursor.stepIndex == null replayPreview.activeCursor.stepIndex == null ||
!activeReplay?.replay
) { ) {
return null; return null;
} }
return `Cảnh #${replayPreview.activeCursor.stageId} · Bước ${replayPreview.activeCursor.stepIndex + 1}`; const stage = activeReplay.replay.detail?.find(
}, [replayPreview.activeCursor.stageId, replayPreview.activeCursor.stepIndex]); (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 isWikiChooserOpen = rightPanelMode === "selection" && Boolean(wikiSelectionPanelAnchor);
const isGeometryChooserOpen = rightPanelMode === "geometry-selection" && Boolean(geometrySelectionPanel); const isGeometryChooserOpen = rightPanelMode === "geometry-selection" && Boolean(geometrySelectionPanel);
@@ -846,6 +853,7 @@ export default function PublicPreviewClientPage({
onCloseWikiSidebar={handleCloseWikiSidebar} onCloseWikiSidebar={handleCloseWikiSidebar}
onWikiLinkRequest={handlePanelWikiLinkRequest} onWikiLinkRequest={handlePanelWikiLinkRequest}
onWikiLinkEntitySelectionRequest={handlePanelWikiLinkEntitySelectionRequest} onWikiLinkEntitySelectionRequest={handlePanelWikiLinkEntitySelectionRequest}
onWikiLinkInteraction={replayMode !== "idle" ? handleExitReplay : undefined}
sidebarWidth={sidebarWidth} sidebarWidth={sidebarWidth}
onSidebarWidthChange={setSidebarWidth} onSidebarWidthChange={setSidebarWidth}
maxSidebarDragWidth={maxDragWidth} maxSidebarDragWidth={maxDragWidth}
@@ -864,11 +872,14 @@ export default function PublicPreviewClientPage({
toasts={replayPreview.toasts} toasts={replayPreview.toasts}
sidebarOpen={isSidebarOpen} sidebarOpen={isSidebarOpen}
sidebarWidth={sidebarWidth} sidebarWidth={sidebarWidth}
sidebarHeight={sidebarHeight}
isLargeScreen={isLargeScreen}
playbackSpeed={replayPreview.playbackSpeed} playbackSpeed={replayPreview.playbackSpeed}
activeStepLabel={activeStepLabel} activeStepLabel={activeStepLabel}
activeStepNumber={replayPreview.activeStepNumber} activeStepNumber={replayPreview.activeStepNumber}
totalSteps={replayPreview.totalSteps} totalSteps={replayPreview.totalSteps}
playButtonLabel={replayMode === "paused" ? "Tiếp tục" : "Phát lại"} playButtonLabel={replayMode === "paused" ? "Tiếp tục" : "Phát lại"}
simplified={true}
onPlayPreview={handleResumePreviewReplay} onPlayPreview={handleResumePreviewReplay}
onStopPreview={handleStopPreviewReplay} onStopPreview={handleStopPreviewReplay}
onResetPreview={handleResetPreviewReplay} onResetPreview={handleResetPreviewReplay}
@@ -927,6 +938,9 @@ export default function PublicPreviewClientPage({
key={entity.id} key={entity.id}
type="button" type="button"
onClick={() => { onClick={() => {
if (replayMode !== "idle") {
handleExitReplay();
}
setWikiSelectionPanelAnchor(null); setWikiSelectionPanelAnchor(null);
setRightPanelMode("wiki"); setRightPanelMode("wiki");
selectEntity(entity.id, { preferredWikiSlug: linkEntityPopup.slug }); selectEntity(entity.id, { preferredWikiSlug: linkEntityPopup.slug });
@@ -21,6 +21,7 @@ type Props = {
onClose: () => void; onClose: () => void;
onWikiLinkRequest: (request: { slug: string; rect: DOMRect }) => void; onWikiLinkRequest: (request: { slug: string; rect: DOMRect }) => void;
onWikiLinkEntitySelectionRequest?: (request: { slug: string; rect: DOMRect }) => void; onWikiLinkEntitySelectionRequest?: (request: { slug: string; rect: DOMRect }) => void;
onWikiLinkInteraction?: () => void;
sidebarWidth?: number; sidebarWidth?: number;
onSidebarWidthChange?: (width: number) => void; onSidebarWidthChange?: (width: number) => void;
maxDragWidth?: number; maxDragWidth?: number;
@@ -173,6 +174,7 @@ function PublicWikiSidebar({
onClose, onClose,
onWikiLinkRequest, onWikiLinkRequest,
onWikiLinkEntitySelectionRequest, onWikiLinkEntitySelectionRequest,
onWikiLinkInteraction,
sidebarWidth, sidebarWidth,
onSidebarWidthChange, onSidebarWidthChange,
maxDragWidth, maxDragWidth,
@@ -308,12 +310,13 @@ function PublicWikiSidebar({
if (!slug.length) return; if (!slug.length) return;
event.preventDefault(); event.preventDefault();
onWikiLinkInteraction?.();
onWikiLinkRequest({ slug, rect: sourceLink.getBoundingClientRect() }); onWikiLinkRequest({ slug, rect: sourceLink.getBoundingClientRect() });
}; };
root.addEventListener("click", handleClick); root.addEventListener("click", handleClick);
return () => root.removeEventListener("click", handleClick); return () => root.removeEventListener("click", handleClick);
}, [onWikiLinkRequest, renderHtml]); }, [onWikiLinkRequest, onWikiLinkInteraction, renderHtml]);
useEffect(() => { useEffect(() => {
const root = contentRootRef.current; const root = contentRootRef.current;
@@ -371,6 +374,7 @@ function PublicWikiSidebar({
const handleOpenStandaloneWiki = (slug: string) => { const handleOpenStandaloneWiki = (slug: string) => {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
onWikiLinkInteraction?.();
const url = `/wiki/${encodeURIComponent(slug)}`; const url = `/wiki/${encodeURIComponent(slug)}`;
const nextWindow = window.open(url, "_blank", "noopener,noreferrer"); const nextWindow = window.open(url, "_blank", "noopener,noreferrer");
if (nextWindow) nextWindow.opener = null; if (nextWindow) nextWindow.opener = null;
@@ -722,6 +726,7 @@ function PublicWikiSidebar({
type="button" type="button"
onClick={() => { onClick={() => {
const request = { slug: wikiLinkMenu.slug, rect: wikiLinkMenu.rect }; const request = { slug: wikiLinkMenu.slug, rect: wikiLinkMenu.rect };
onWikiLinkInteraction?.();
if (onWikiLinkEntitySelectionRequest) { if (onWikiLinkEntitySelectionRequest) {
onWikiLinkEntitySelectionRequest(request); onWikiLinkEntitySelectionRequest(request);
} else { } else {