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
Build and Release / release (push) Successful in 39s
This commit is contained in:
+8
-3
@@ -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" />
|
||||
|
||||
@@ -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,6 +228,7 @@ export default function ReplayPreviewOverlay({
|
||||
>
|
||||
<div style={{ display: "grid", gap: 6, minWidth: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
||||
{!simplified && (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
@@ -216,6 +245,7 @@ export default function ReplayPreviewOverlay({
|
||||
>
|
||||
Xem trước
|
||||
</span>
|
||||
)}
|
||||
{activeStepLabel ? (
|
||||
<span
|
||||
style={{
|
||||
@@ -228,9 +258,11 @@ export default function ReplayPreviewOverlay({
|
||||
{activeStepLabel}
|
||||
</span>
|
||||
) : null}
|
||||
{!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>
|
||||
{!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 {
|
||||
|
||||
Reference in New Issue
Block a user