preview mode
This commit is contained in:
@@ -0,0 +1,415 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties } from "react";
|
||||
import type {
|
||||
ReplayPreviewDialog,
|
||||
ReplayPreviewImage,
|
||||
ReplayPreviewToast,
|
||||
} from "@/uhm/lib/replay/useReplayPreview";
|
||||
|
||||
type Props = {
|
||||
isPreviewMode: boolean;
|
||||
isPlaying: boolean;
|
||||
title: string;
|
||||
descriptions: string;
|
||||
subtitle: string | null;
|
||||
dialog: ReplayPreviewDialog | null;
|
||||
image: ReplayPreviewImage | null;
|
||||
toasts: ReplayPreviewToast[];
|
||||
sidebarOpen: boolean;
|
||||
playbackSpeed: number;
|
||||
activeStepLabel: string | null;
|
||||
activeStepNumber: number | null;
|
||||
totalSteps: number;
|
||||
onPlayPreview: () => void;
|
||||
onStopPreview: () => void;
|
||||
onResetPreview: () => void;
|
||||
onExitPreview: () => void;
|
||||
};
|
||||
|
||||
export default function ReplayPreviewOverlay({
|
||||
isPreviewMode,
|
||||
isPlaying,
|
||||
title,
|
||||
descriptions,
|
||||
subtitle,
|
||||
dialog,
|
||||
image,
|
||||
toasts,
|
||||
sidebarOpen,
|
||||
playbackSpeed,
|
||||
activeStepLabel,
|
||||
activeStepNumber,
|
||||
totalSteps,
|
||||
onPlayPreview,
|
||||
onStopPreview,
|
||||
onResetPreview,
|
||||
onExitPreview,
|
||||
}: Props) {
|
||||
const hasNarrativeCard = title.trim().length > 0 || descriptions.trim().length > 0;
|
||||
const hasWikiPreview = sidebarOpen;
|
||||
const shouldRender =
|
||||
isPreviewMode ||
|
||||
isPlaying ||
|
||||
hasNarrativeCard ||
|
||||
Boolean(subtitle) ||
|
||||
Boolean(dialog) ||
|
||||
Boolean(image) ||
|
||||
Boolean(toasts.length);
|
||||
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
zIndex: 15,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
{hasNarrativeCard ? (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 72,
|
||||
left: 18,
|
||||
maxWidth: 460,
|
||||
borderRadius: 18,
|
||||
border: "1px solid rgba(148, 163, 184, 0.26)",
|
||||
background: "linear-gradient(145deg, rgba(15, 23, 42, 0.94), rgba(30, 41, 59, 0.88))",
|
||||
boxShadow: "0 14px 42px rgba(2, 6, 23, 0.42)",
|
||||
padding: "18px 20px",
|
||||
}}
|
||||
>
|
||||
{title.trim().length ? (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 26,
|
||||
lineHeight: 1.1,
|
||||
fontWeight: 900,
|
||||
color: "#f8fafc",
|
||||
overflowWrap: "anywhere",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
) : null}
|
||||
{descriptions.trim().length ? (
|
||||
<div
|
||||
style={{
|
||||
marginTop: title.trim().length ? 12 : 0,
|
||||
fontSize: 14,
|
||||
lineHeight: 1.55,
|
||||
color: "#dbeafe",
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
>
|
||||
{descriptions}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{toasts.length ? (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 72,
|
||||
right: hasWikiPreview ? 454 : 18,
|
||||
display: "grid",
|
||||
gap: 8,
|
||||
width: 280,
|
||||
}}
|
||||
>
|
||||
{toasts.map((toast) => (
|
||||
<div
|
||||
key={toast.id}
|
||||
style={{
|
||||
borderRadius: 14,
|
||||
border: "1px solid rgba(56, 189, 248, 0.28)",
|
||||
background: "rgba(8, 47, 73, 0.9)",
|
||||
color: "#e0f2fe",
|
||||
padding: "12px 14px",
|
||||
fontSize: 13,
|
||||
lineHeight: 1.4,
|
||||
boxShadow: "0 10px 26px rgba(2, 6, 23, 0.32)",
|
||||
}}
|
||||
>
|
||||
{toast.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{image ? (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 18,
|
||||
bottom: 96,
|
||||
width: 320,
|
||||
borderRadius: 18,
|
||||
overflow: "hidden",
|
||||
border: "1px solid rgba(148, 163, 184, 0.22)",
|
||||
background: "rgba(15, 23, 42, 0.9)",
|
||||
boxShadow: "0 16px 44px rgba(2, 6, 23, 0.42)",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.caption || "Historical image"}
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "block",
|
||||
maxHeight: 240,
|
||||
objectFit: "cover",
|
||||
background: "#020617",
|
||||
}}
|
||||
/>
|
||||
{image.caption?.trim() ? (
|
||||
<div
|
||||
style={{
|
||||
padding: "10px 12px",
|
||||
fontSize: 12,
|
||||
lineHeight: 1.45,
|
||||
color: "#cbd5e1",
|
||||
}}
|
||||
>
|
||||
{image.caption}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{dialog ? (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: dialog.side === "right" ? "auto" : 18,
|
||||
right: dialog.side === "right" ? 18 : "auto",
|
||||
bottom: subtitle ? 138 : 96,
|
||||
maxWidth: 420,
|
||||
display: "grid",
|
||||
gap: 10,
|
||||
gridTemplateColumns: dialog.avatar.trim().length ? "56px 1fr" : "1fr",
|
||||
alignItems: "start",
|
||||
}}
|
||||
>
|
||||
{dialog.avatar.trim().length ? (
|
||||
<img
|
||||
src={dialog.avatar}
|
||||
alt={dialog.speaker || "speaker"}
|
||||
style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: "50%",
|
||||
objectFit: "cover",
|
||||
border: "2px solid rgba(125, 211, 252, 0.55)",
|
||||
background: "#0f172a",
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 18,
|
||||
border: "1px solid rgba(148, 163, 184, 0.24)",
|
||||
background: "rgba(15, 23, 42, 0.92)",
|
||||
padding: "14px 16px",
|
||||
color: "#f8fafc",
|
||||
boxShadow: "0 14px 36px rgba(2, 6, 23, 0.38)",
|
||||
}}
|
||||
>
|
||||
{dialog.speaker?.trim() ? (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 6,
|
||||
fontSize: 11,
|
||||
color: "#7dd3fc",
|
||||
fontWeight: 900,
|
||||
letterSpacing: 0.4,
|
||||
}}
|
||||
>
|
||||
{dialog.speaker}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 15,
|
||||
lineHeight: 1.5,
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
>
|
||||
{dialog.text}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{subtitle?.trim() ? (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
bottom: 90,
|
||||
transform: "translateX(-50%)",
|
||||
maxWidth: 720,
|
||||
borderRadius: 999,
|
||||
border: "1px solid rgba(148, 163, 184, 0.24)",
|
||||
background: "rgba(2, 6, 23, 0.84)",
|
||||
color: "#f8fafc",
|
||||
padding: "10px 18px",
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.45,
|
||||
textAlign: "center",
|
||||
boxShadow: "0 12px 32px rgba(2, 6, 23, 0.28)",
|
||||
}}
|
||||
>
|
||||
{subtitle}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isPreviewMode ? (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 64,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
width: "min(520px, calc(100% - 72px))",
|
||||
borderRadius: 18,
|
||||
border: "1px solid rgba(148, 163, 184, 0.24)",
|
||||
background: "rgba(15, 23, 42, 0.9)",
|
||||
color: "#e2e8f0",
|
||||
padding: "12px 14px",
|
||||
boxShadow: "0 12px 32px rgba(2, 6, 23, 0.3)",
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "grid", gap: 6, minWidth: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
padding: "3px 8px",
|
||||
borderRadius: 999,
|
||||
background: "rgba(34, 197, 94, 0.2)",
|
||||
color: "#86efac",
|
||||
fontWeight: 900,
|
||||
fontSize: 11,
|
||||
letterSpacing: 0.3,
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</span>
|
||||
{activeStepLabel ? (
|
||||
<span
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 800,
|
||||
color: "#f8fafc",
|
||||
overflowWrap: "anywhere",
|
||||
}}
|
||||
>
|
||||
{activeStepLabel}
|
||||
</span>
|
||||
) : null}
|
||||
<span style={{ fontSize: 12, color: "#94a3b8" }}>
|
||||
x{playbackSpeed.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
{totalSteps > 0 ? (
|
||||
<div style={{ display: "grid", gap: 6 }}>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 6,
|
||||
borderRadius: 999,
|
||||
background: "rgba(51, 65, 85, 0.8)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${Math.max(0, Math.min(100, ((activeStepNumber || 0) / totalSteps) * 100))}%`,
|
||||
height: "100%",
|
||||
borderRadius: 999,
|
||||
background: "linear-gradient(90deg, #22c55e, #38bdf8)",
|
||||
transition: "width 180ms ease",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: "#94a3b8" }}>
|
||||
Step {activeStepNumber || 0}/{totalSteps}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, flex: "0 0 auto" }}>
|
||||
{isPlaying ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onStopPreview}
|
||||
style={previewButtonStyle("#7f1d1d")}
|
||||
>
|
||||
Dừng
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onResetPreview}
|
||||
style={previewButtonStyle("#1e3a8a")}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onPlayPreview}
|
||||
style={previewButtonStyle("#166534")}
|
||||
>
|
||||
Phát lại
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onExitPreview}
|
||||
style={previewButtonStyle("#334155")}
|
||||
>
|
||||
Thoát preview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function previewButtonStyle(background: string): CSSProperties {
|
||||
return {
|
||||
border: "none",
|
||||
background,
|
||||
color: "white",
|
||||
borderRadius: 10,
|
||||
padding: "8px 12px",
|
||||
cursor: "pointer",
|
||||
fontSize: 12,
|
||||
fontWeight: 800,
|
||||
boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.08)",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user