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 './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" />
|
||||||
|
|||||||
@@ -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,22 +228,24 @@ 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" }}>
|
||||||
<span
|
{!simplified && (
|
||||||
style={{
|
<span
|
||||||
display: "inline-flex",
|
style={{
|
||||||
alignItems: "center",
|
display: "inline-flex",
|
||||||
padding: "3px 8px",
|
alignItems: "center",
|
||||||
borderRadius: 999,
|
padding: "3px 8px",
|
||||||
background: "rgba(34, 197, 94, 0.2)",
|
borderRadius: 999,
|
||||||
color: "#86efac",
|
background: "rgba(34, 197, 94, 0.2)",
|
||||||
fontWeight: 900,
|
color: "#86efac",
|
||||||
fontSize: 11,
|
fontWeight: 900,
|
||||||
letterSpacing: 0.3,
|
fontSize: 11,
|
||||||
textTransform: "uppercase",
|
letterSpacing: 0.3,
|
||||||
}}
|
textTransform: "uppercase",
|
||||||
>
|
}}
|
||||||
Xem trước
|
>
|
||||||
</span>
|
Xem trước
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{activeStepLabel ? (
|
{activeStepLabel ? (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
@@ -228,9 +258,11 @@ export default function ReplayPreviewOverlay({
|
|||||||
{activeStepLabel}
|
{activeStepLabel}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<span style={{ fontSize: 12, color: "#94a3b8" }}>
|
{!simplified && (
|
||||||
x{playbackSpeed.toFixed(2)}
|
<span style={{ fontSize: 12, color: "#94a3b8" }}>
|
||||||
</span>
|
x{playbackSpeed.toFixed(2)}
|
||||||
|
</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>
|
||||||
<div style={{ fontSize: 11, color: "#94a3b8" }}>
|
{!simplified && (
|
||||||
Bước {activeStepNumber || 0}/{totalSteps}
|
<div style={{ fontSize: 11, color: "#94a3b8" }}>
|
||||||
</div>
|
Bước {activeStepNumber || 0}/{totalSteps}
|
||||||
|
</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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user