feat: add replay button to TimelineBar and implement responsive search bar layout with 100svh height support
Build and Release / release (push) Successful in 37s

This commit is contained in:
taDuc
2026-06-04 19:49:25 +07:00
parent 5aee0eccb2
commit 35cd174c8b
4 changed files with 118 additions and 56 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ const srOnlyStyle: React.CSSProperties = {
export default function Page() { export default function Page() {
return ( return (
<div style={{ position: "relative", width: "100%", height: "100vh", overflow: "hidden", backgroundColor: "#0b1220" }}> <div style={{ position: "relative", width: "100%", height: "100svh", overflow: "hidden", backgroundColor: "#0b1220" }}>
{/* Preload LCP image */} {/* Preload LCP image */}
<link rel="preload" as="image" href="/images/map_placeholder.webp" fetchPriority="high" /> <link rel="preload" as="image" href="/images/map_placeholder.webp" fetchPriority="high" />
+5 -41
View File
@@ -153,8 +153,8 @@ export default function PreviewMapShell({
}; };
return ( return (
<div className="relative min-h-screen overflow-hidden bg-gray-950 text-gray-100"> <div className="relative overflow-hidden bg-gray-950 text-gray-100" style={{ minHeight: "100svh", height: "100svh" }}>
<div className="relative min-h-screen"> <div className="relative" style={{ minHeight: "100svh", height: "100svh" }}>
<Map <Map
ref={mapHandleRef} ref={mapHandleRef}
mode="preview" mode="preview"
@@ -175,6 +175,7 @@ export default function PreviewMapShell({
onPlayPreviewReplay={onPlayPreviewReplay} onPlayPreviewReplay={onPlayPreviewReplay}
onLoad={onLoad} onLoad={onLoad}
showViewportControls={false} showViewportControls={false}
height="100svh"
/> />
<TimelineBar <TimelineBar
@@ -188,6 +189,7 @@ export default function PreviewMapShell({
filterEnabled={timelineFilterEnabled} filterEnabled={timelineFilterEnabled}
onFilterEnabledChange={onTimelineFilterEnabledChange} onFilterEnabledChange={onTimelineFilterEnabledChange}
style={timelineStyle} style={timelineStyle}
onPlayReplay={onPlayPreviewReplay}
/> />
<style dangerouslySetInnerHTML={{ __html: ` <style dangerouslySetInnerHTML={{ __html: `
@@ -408,45 +410,7 @@ export default function PreviewMapShell({
)} )}
</aside> </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} {overlay}
@@ -404,6 +404,46 @@ export default function PublicPreviewClientPage({
}; };
}, [isLayerPanelVisible, displayedActiveEntity, isLargeScreen, sidebarWidth, sidebarHeight]); }, [isLayerPanelVisible, displayedActiveEntity, isLargeScreen, sidebarWidth, sidebarHeight]);
const searchBarWidth = useMemo(() => {
if (isLargeScreen) {
return "min(392px, calc(100vw - 120px))";
}
if (isLayerPanelVisible) {
return `calc(100vw - 104px)`;
} else {
return `calc(100vw - 36px)`;
}
}, [isLargeScreen, isLayerPanelVisible]);
const searchBarWrapperStyle = useMemo(() => {
if (isLargeScreen) {
return {
position: "absolute" as const,
top: 10,
left: "50%",
transform: "translateX(-50%)",
right: "auto",
zIndex: 18,
display: "flex",
gap: "10px",
alignItems: "flex-start",
pointerEvents: "auto" as const,
};
}
return {
position: "absolute" as const,
top: 10,
left: "auto",
right: 18,
transform: "none",
zIndex: 18,
display: "flex",
gap: "10px",
alignItems: "flex-start",
pointerEvents: "auto" as const,
};
}, [isLargeScreen]);
return ( return (
<> <>
{isBackgroundVisibilityReady && loadInteractiveMap && ( {isBackgroundVisibilityReady && loadInteractiveMap && (
@@ -470,19 +510,7 @@ export default function PublicPreviewClientPage({
) : null ) : null
} }
> >
<div <div style={searchBarWrapperStyle}>
style={{
position: "absolute",
top: 10,
left: "50%",
transform: "translateX(-50%)",
zIndex: 18,
display: "flex",
gap: "10px",
alignItems: "flex-start",
pointerEvents: "auto",
}}
>
<PresentPlaceSearch <PresentPlaceSearch
focusedPlace={focusedPresentPlace} focusedPlace={focusedPresentPlace}
onFocusPlace={handleFocusPresentPlace} onFocusPlace={handleFocusPresentPlace}
@@ -493,7 +521,7 @@ export default function PublicPreviewClientPage({
top: 0, top: 0,
left: 0, left: 0,
transform: "none", transform: "none",
width: "min(392px, calc(100vw - 120px))", width: searchBarWidth,
}} }}
/> />
</div> </div>
+70
View File
@@ -15,6 +15,7 @@ type Props = {
filterEnabled?: boolean; filterEnabled?: boolean;
onFilterEnabledChange?: (enabled: boolean) => void; onFilterEnabledChange?: (enabled: boolean) => void;
style?: React.CSSProperties; style?: React.CSSProperties;
onPlayReplay?: () => void;
}; };
export default function TimelineBar({ export default function TimelineBar({
@@ -28,6 +29,7 @@ export default function TimelineBar({
filterEnabled, filterEnabled,
onFilterEnabledChange, onFilterEnabledChange,
style, style,
onPlayReplay,
}: Props) { }: Props) {
const lower = FIXED_TIMELINE_START_YEAR; const lower = FIXED_TIMELINE_START_YEAR;
const upper = FIXED_TIMELINE_END_YEAR; const upper = FIXED_TIMELINE_END_YEAR;
@@ -272,6 +274,40 @@ export default function TimelineBar({
> >
+ +
</button> </button>
{onPlayReplay ? (
<>
<div style={{ width: 1, height: 16, backgroundColor: "rgba(255, 255, 255, 0.15)" }} />
<button
type="button"
onClick={onPlayReplay}
style={{
width: 32,
height: 32,
borderRadius: "50%",
backgroundColor: "#2563eb",
border: "1px solid rgba(255, 255, 255, 0.2)",
color: "white",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
transition: "all 0.2s ease",
flexShrink: 0,
}}
title="Xem diễn biến lịch sử (Replay)"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="currentColor"
>
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
</button>
</>
) : null}
</div> </div>
{/* Ruler row below: full width */} {/* Ruler row below: full width */}
@@ -336,6 +372,7 @@ export default function TimelineBar({
maxYear={upper} maxYear={upper}
disabled={effectiveDisabled} disabled={effectiveDisabled}
/> />
<div className={styles.numberWrapper}> <div className={styles.numberWrapper}>
<input <input
type="number" type="number"
@@ -385,6 +422,39 @@ export default function TimelineBar({
</button> </button>
</div> </div>
</div> </div>
{onPlayReplay ? (
<button
type="button"
onClick={onPlayReplay}
style={{
width: 32,
height: 32,
borderRadius: "50%",
backgroundColor: "#2563eb",
border: "1px solid rgba(255, 255, 255, 0.2)",
color: "white",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
transition: "all 0.2s ease",
marginLeft: 8,
marginRight: 8,
flexShrink: 0,
}}
title="Xem diễn biến lịch sử (Replay)"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="currentColor"
>
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
</button>
) : null}
{typeof timeRange === "number" && onTimeRangeChange ? ( {typeof timeRange === "number" && onTimeRangeChange ? (
<label <label
title="time_range (0-30)" title="time_range (0-30)"