feat: update public preview UI with adjustable sidebar height, layer panel toggle, and synchronized data loading states(mobile UI)
Build and Release / release (push) Successful in 37s

This commit is contained in:
taDuc
2026-06-04 18:39:46 +07:00
parent 794ad2913f
commit 9a5dfdb2ed
14 changed files with 801 additions and 249 deletions
+184 -39
View File
@@ -1,6 +1,9 @@
"use client";
import Image from "next/image";
import { type CSSProperties, type ReactNode, useState } from "react";
import { type CSSProperties, type ReactNode, useState, useEffect } from "react";
import { apiGetCurrentUser } from "@/service/auth";
import ChatbotWidget from "@/components/ui/chat/ChatbotWidget";
import Map, { type MapFeaturePayload } from "@/uhm/components/Map";
import ReplayPreviewLayerPanel from "@/uhm/components/editor/ReplayPreviewLayerPanel";
import PublicWikiSidebar from "@/uhm/components/wiki/PublicWikiSidebar";
@@ -51,6 +54,10 @@ type Props = {
onLoad?: () => void;
instantLoad?: boolean;
onToggleInstantLoad?: (val: boolean) => void;
isLayerPanelVisible?: boolean;
onLayerPanelVisibleChange?: (visible: boolean) => void;
sidebarHeight?: number;
onSidebarHeightChange?: (height: number) => void;
};
export default function PreviewMapShell({
@@ -92,8 +99,42 @@ export default function PreviewMapShell({
onLoad,
instantLoad = true,
onToggleInstantLoad,
isLayerPanelVisible: propsLayerPanelVisible,
onLayerPanelVisibleChange,
sidebarHeight,
onSidebarHeightChange,
}: Props) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
const [localLayerPanelVisible, setLocalLayerPanelVisible] = useState(true);
const isLayerPanelVisible = propsLayerPanelVisible ?? localLayerPanelVisible;
const setIsLayerPanelVisible = onLayerPanelVisibleChange ?? setLocalLayerPanelVisible;
const [isMobileOrTablet, setIsMobileOrTablet] = useState(false);
useEffect(() => {
const checkDevice = () => setIsMobileOrTablet(window.innerWidth < 1024);
checkDevice();
window.addEventListener("resize", checkDevice);
return () => window.removeEventListener("resize", checkDevice);
}, []);
useEffect(() => {
const fetchUserAvatar = async () => {
try {
const userData = await apiGetCurrentUser();
console.log("PreviewMapShell: Fetched userData:", userData);
if (userData?.data?.profile?.avatar_url) {
console.log("PreviewMapShell: Setting avatarUrl to:", userData.data.profile.avatar_url);
setAvatarUrl(userData.data.profile.avatar_url);
} else {
console.log("PreviewMapShell: No avatar_url in profile:", userData?.data?.profile);
}
} catch (err) {
console.error("PreviewMapShell: Failed to fetch user avatar:", err);
}
};
fetchUserAvatar();
}, []);
const menuOptionStyle: CSSProperties = {
width: 46,
@@ -133,6 +174,7 @@ export default function PreviewMapShell({
getHoverPopupContent={getHoverPopupContent}
onPlayPreviewReplay={onPlayPreviewReplay}
onLoad={onLoad}
showViewportControls={false}
/>
<TimelineBar
@@ -165,7 +207,7 @@ export default function PreviewMapShell({
style={{
position: "absolute",
top: 10,
bottom: 20,
bottom: (activeEntity && isMobileOrTablet) ? `${(sidebarHeight || 400) + 20}px` : 20,
left: 18,
zIndex: 18,
display: "flex",
@@ -173,6 +215,7 @@ export default function PreviewMapShell({
gap: 12,
width: 58,
pointerEvents: "none",
transition: "bottom 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
}}
>
<div
@@ -193,9 +236,8 @@ export default function PreviewMapShell({
width: 46,
height: 46,
backgroundColor: "#1e293b",
color: "#f8fafc",
border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: 12,
border: "1px solid rgba(255, 255, 255, 0.15)",
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
@@ -204,21 +246,21 @@ export default function PreviewMapShell({
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
backdropFilter: "blur(8px)",
flexShrink: 0,
overflow: "hidden",
padding: 0,
}}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
</svg>
<Image
src={avatarUrl || "/images/no-images.jpg"}
alt="Cài đặt"
width={46}
height={46}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
</button>
{isMenuOpen && (
@@ -233,9 +275,19 @@ export default function PreviewMapShell({
>
<button
type="button"
onClick={() => { window.location.href = "/user"; }}
title="Quản trị & Chỉnh sửa (Edit)"
style={menuOptionStyle}
onClick={() => {
if (isMobileOrTablet) {
alert("Tính năng quản trị và chỉnh sửa chỉ hỗ trợ trên máy tính (Desktop)");
} else {
window.location.href = "/user";
}
}}
title={isMobileOrTablet ? "Tính năng này chỉ hoạt động trên Desktop" : "Quản trị & Chỉnh sửa (Edit)"}
style={{
...menuOptionStyle,
opacity: isMobileOrTablet ? 0.5 : 1,
cursor: isMobileOrTablet ? "not-allowed" : "pointer",
}}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
@@ -258,6 +310,19 @@ export default function PreviewMapShell({
</svg>
</button>
<button
type="button"
onClick={() => {
window.dispatchEvent(new CustomEvent("toggle-chatbot"));
}}
title="Trợ lý AI Lịch sử (Chatbot)"
style={menuOptionStyle}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
</button>
<button
type="button"
onClick={() => { window.location.href = "/faq"; }}
@@ -281,33 +346,110 @@ export default function PreviewMapShell({
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
</button>
{!isLayerPanelVisible && (
<button
type="button"
onClick={() => setIsLayerPanelVisible(true)}
title="Hiện bảng lớp bản đồ"
style={{
...menuOptionStyle,
color: "#10b981",
border: "1px solid rgba(16, 185, 129, 0.3)",
boxShadow: "0 2px 8px rgba(16, 185, 129, 0.15)",
}}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
</button>
)}
</div>
)}
</div>
<div
style={{
flexGrow: 1,
flexShrink: 1,
minHeight: 0,
display: "flex",
flexDirection: "column",
pointerEvents: "auto",
}}
>
<ReplayPreviewLayerPanel
backgroundVisibility={backgroundVisibility}
geometryVisibility={geometryVisibility}
onToggleBackground={onToggleBackground}
onToggleGeometry={onToggleGeometry}
/>
</div>
{isLayerPanelVisible && (
<div
style={{
flexGrow: 1,
flexShrink: 1,
minHeight: 0,
display: "flex",
flexDirection: "column",
pointerEvents: "auto",
}}
>
<ReplayPreviewLayerPanel
backgroundVisibility={backgroundVisibility}
geometryVisibility={geometryVisibility}
onToggleBackground={onToggleBackground}
onToggleGeometry={onToggleGeometry}
onHide={() => setIsLayerPanelVisible(false)}
/>
</div>
)}
</aside>
{onPlayPreviewReplay ? (
<button
type="button"
onClick={onPlayPreviewReplay}
style={{
position: "absolute",
top: 10,
right: 18,
width: 46,
height: 46,
backgroundColor: "#1e293b",
border: "1px solid rgba(56, 189, 248, 0.3)",
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
color: "#38bdf8",
transition: "all 0.2s ease",
boxShadow: "0 4px 12px rgba(56, 189, 248, 0.15)",
backdropFilter: "blur(8px)",
zIndex: 22,
}}
title="Play replay của geometry đang chọn"
aria-label="Play selected replay"
>
<span
aria-hidden="true"
style={{
width: 0,
height: 0,
borderTop: "6px solid transparent",
borderBottom: "6px solid transparent",
borderLeft: "9px solid currentColor",
marginLeft: "3px",
}}
/>
</button>
) : null}
{overlay}
{activeEntity ? (
<aside className="absolute bottom-4 right-4 top-4 z-20 max-w-[calc(100vw-2rem)]">
<aside
className={isMobileOrTablet ? "" : "absolute bottom-4 right-4 top-4 left-auto z-20 max-w-[calc(100vw-2rem)]"}
style={isMobileOrTablet ? {
position: "absolute",
bottom: 0,
left: 0,
right: 0,
top: "auto",
height: `${sidebarHeight || 400}px`,
maxHeight: "90vh",
width: "100%",
maxWidth: "100%",
zIndex: 20,
// Do not transition height during drag resizing for butter smoothness
} : undefined}
>
<PublicWikiSidebar
entity={activeEntity}
wiki={activeWiki}
@@ -319,10 +461,13 @@ export default function PreviewMapShell({
onSidebarWidthChange={onSidebarWidthChange}
maxDragWidth={maxSidebarDragWidth}
compactHeader
sidebarHeight={sidebarHeight}
onSidebarHeightChange={onSidebarHeightChange}
/>
</aside>
) : null}
<ChatbotWidget hideFloatingButton />
{children}
</div>
</div>