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 { 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 (
<html lang="en" className={inter.variable}>
<body className={`${inter.className} dark:bg-gray-900`}>
<html lang="en" className={isPublicRoot ? '' : inter.variable}>
<body className={`${isPublicRoot ? 'font-sans' : inter.className} dark:bg-gray-900`}>
<StoreProvider>
{children}
<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[];
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 && (
<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 ? (
<div
style={{
@@ -71,6 +98,7 @@ export default function ReplayPreviewOverlay({
display: "grid",
gap: 8,
width: 280,
pointerEvents: "auto",
}}
>
{toasts.map((toast) => (
@@ -200,22 +228,24 @@ export default function ReplayPreviewOverlay({
>
<div style={{ display: "grid", gap: 6, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
<span
style={{
display: "inline-flex",
alignItems: "center",
padding: "3px 8px",
borderRadius: 999,
background: "rgba(34, 197, 94, 0.2)",
color: "#86efac",
fontWeight: 900,
fontSize: 11,
letterSpacing: 0.3,
textTransform: "uppercase",
}}
>
Xem trước
</span>
{!simplified && (
<span
style={{
display: "inline-flex",
alignItems: "center",
padding: "3px 8px",
borderRadius: 999,
background: "rgba(34, 197, 94, 0.2)",
color: "#86efac",
fontWeight: 900,
fontSize: 11,
letterSpacing: 0.3,
textTransform: "uppercase",
}}
>
Xem trước
</span>
)}
{activeStepLabel ? (
<span
style={{
@@ -228,9 +258,11 @@ export default function ReplayPreviewOverlay({
{activeStepLabel}
</span>
) : null}
<span style={{ fontSize: 12, color: "#94a3b8" }}>
x{playbackSpeed.toFixed(2)}
</span>
{!simplified && (
<span style={{ fontSize: 12, color: "#94a3b8" }}>
x{playbackSpeed.toFixed(2)}
</span>
)}
</div>
{totalSteps > 0 ? (
<div style={{ display: "grid", gap: 6 }}>
@@ -253,9 +285,11 @@ export default function ReplayPreviewOverlay({
}}
/>
</div>
<div style={{ fontSize: 11, color: "#94a3b8" }}>
Bước {activeStepNumber || 0}/{totalSteps}
</div>
{!simplified && (
<div style={{ fontSize: 11, color: "#94a3b8" }}>
Bước {activeStepNumber || 0}/{totalSteps}
</div>
)}
</div>
) : null}
</div>
@@ -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"}
</button>
</div>
</div>
@@ -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}
@@ -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 });
@@ -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 {