feat: add replay button to TimelineBar and implement responsive search bar layout with 100svh height support
Build and Release / release (push) Successful in 37s
Build and Release / release (push) Successful in 37s
This commit is contained in:
+1
-1
@@ -20,7 +20,7 @@ const srOnlyStyle: React.CSSProperties = {
|
||||
|
||||
export default function Page() {
|
||||
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 */}
|
||||
<link rel="preload" as="image" href="/images/map_placeholder.webp" fetchPriority="high" />
|
||||
|
||||
|
||||
@@ -153,8 +153,8 @@ export default function PreviewMapShell({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen overflow-hidden bg-gray-950 text-gray-100">
|
||||
<div className="relative min-h-screen">
|
||||
<div className="relative overflow-hidden bg-gray-950 text-gray-100" style={{ minHeight: "100svh", height: "100svh" }}>
|
||||
<div className="relative" style={{ minHeight: "100svh", height: "100svh" }}>
|
||||
<Map
|
||||
ref={mapHandleRef}
|
||||
mode="preview"
|
||||
@@ -175,6 +175,7 @@ export default function PreviewMapShell({
|
||||
onPlayPreviewReplay={onPlayPreviewReplay}
|
||||
onLoad={onLoad}
|
||||
showViewportControls={false}
|
||||
height="100svh"
|
||||
/>
|
||||
|
||||
<TimelineBar
|
||||
@@ -188,6 +189,7 @@ export default function PreviewMapShell({
|
||||
filterEnabled={timelineFilterEnabled}
|
||||
onFilterEnabledChange={onTimelineFilterEnabledChange}
|
||||
style={timelineStyle}
|
||||
onPlayReplay={onPlayPreviewReplay}
|
||||
/>
|
||||
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
@@ -408,45 +410,7 @@ export default function PreviewMapShell({
|
||||
)}
|
||||
</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}
|
||||
|
||||
|
||||
@@ -404,6 +404,46 @@ export default function PublicPreviewClientPage({
|
||||
};
|
||||
}, [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 (
|
||||
<>
|
||||
{isBackgroundVisibilityReady && loadInteractiveMap && (
|
||||
@@ -470,19 +510,7 @@ export default function PublicPreviewClientPage({
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 18,
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
alignItems: "flex-start",
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
<div style={searchBarWrapperStyle}>
|
||||
<PresentPlaceSearch
|
||||
focusedPlace={focusedPresentPlace}
|
||||
onFocusPlace={handleFocusPresentPlace}
|
||||
@@ -493,7 +521,7 @@ export default function PublicPreviewClientPage({
|
||||
top: 0,
|
||||
left: 0,
|
||||
transform: "none",
|
||||
width: "min(392px, calc(100vw - 120px))",
|
||||
width: searchBarWidth,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ type Props = {
|
||||
filterEnabled?: boolean;
|
||||
onFilterEnabledChange?: (enabled: boolean) => void;
|
||||
style?: React.CSSProperties;
|
||||
onPlayReplay?: () => void;
|
||||
};
|
||||
|
||||
export default function TimelineBar({
|
||||
@@ -28,6 +29,7 @@ export default function TimelineBar({
|
||||
filterEnabled,
|
||||
onFilterEnabledChange,
|
||||
style,
|
||||
onPlayReplay,
|
||||
}: Props) {
|
||||
const lower = FIXED_TIMELINE_START_YEAR;
|
||||
const upper = FIXED_TIMELINE_END_YEAR;
|
||||
@@ -272,6 +274,40 @@ export default function TimelineBar({
|
||||
>
|
||||
+
|
||||
</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>
|
||||
|
||||
{/* Ruler row below: full width */}
|
||||
@@ -336,6 +372,7 @@ export default function TimelineBar({
|
||||
maxYear={upper}
|
||||
disabled={effectiveDisabled}
|
||||
/>
|
||||
|
||||
<div className={styles.numberWrapper}>
|
||||
<input
|
||||
type="number"
|
||||
@@ -385,6 +422,39 @@ export default function TimelineBar({
|
||||
</button>
|
||||
</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 ? (
|
||||
<label
|
||||
title="time_range (0-30)"
|
||||
|
||||
Reference in New Issue
Block a user