From 103f9321095a7bcd21bfbf25f37092964fb248e9 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 3 Mar 2026 13:48:31 -0800 Subject: [PATCH] add ReplayModal, polish player css and dark mode bug, mobile-friendly --- .../[websiteId]/replays/ReplayModal.tsx | 43 +++++++++++++++++++ .../[websiteId]/replays/ReplaysDataTable.tsx | 2 +- .../[websiteId]/replays/ReplaysPage.tsx | 2 + .../[websiteId]/replays/ReplaysTable.tsx | 16 +++---- .../replays/[sessionId]/ReplayPlayback.tsx | 38 +++++++++++----- .../replays/[sessionId]/ReplayPlayer.tsx | 17 +++++--- src/app/global.css | 13 ++++++ 7 files changed, 104 insertions(+), 27 deletions(-) create mode 100644 src/app/(main)/websites/[websiteId]/replays/ReplayModal.tsx diff --git a/src/app/(main)/websites/[websiteId]/replays/ReplayModal.tsx b/src/app/(main)/websites/[websiteId]/replays/ReplayModal.tsx new file mode 100644 index 000000000..362724bf5 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/replays/ReplayModal.tsx @@ -0,0 +1,43 @@ +'use client'; +import { Column, Dialog, Modal, type ModalProps } from '@umami/react-zen'; +import { ReplayPlayback } from '@/app/(main)/websites/[websiteId]/replays/[sessionId]/ReplayPlayback'; +import { useNavigation } from '@/components/hooks'; + +export interface ReplayModalProps extends ModalProps { + websiteId: string; +} + +export function ReplayModal({ websiteId, ...props }: ReplayModalProps) { + const { + router, + query: { replay }, + updateParams, + } = useNavigation(); + + const handleOpenChange = (isOpen: boolean) => { + if (!isOpen) { + router.push(updateParams({ replay: undefined })); + } + }; + + return ( + + + + {({ close }) => ( + + + + )} + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/replays/ReplaysDataTable.tsx b/src/app/(main)/websites/[websiteId]/replays/ReplaysDataTable.tsx index 19e195d80..6d047081d 100644 --- a/src/app/(main)/websites/[websiteId]/replays/ReplaysDataTable.tsx +++ b/src/app/(main)/websites/[websiteId]/replays/ReplaysDataTable.tsx @@ -8,7 +8,7 @@ export function ReplaysDataTable({ websiteId }: { websiteId: string }) { return ( {({ data }) => { - return ; + return ; }} ); diff --git a/src/app/(main)/websites/[websiteId]/replays/ReplaysPage.tsx b/src/app/(main)/websites/[websiteId]/replays/ReplaysPage.tsx index d4127fc6d..414fae0fb 100644 --- a/src/app/(main)/websites/[websiteId]/replays/ReplaysPage.tsx +++ b/src/app/(main)/websites/[websiteId]/replays/ReplaysPage.tsx @@ -3,6 +3,7 @@ import { Column } from '@umami/react-zen'; import { SessionModal } from '@/app/(main)/websites/[websiteId]/sessions/SessionModal'; import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; import { Panel } from '@/components/common/Panel'; +import { ReplayModal } from './ReplayModal'; import { ReplaysDataTable } from './ReplaysDataTable'; export function ReplaysPage({ websiteId }: { websiteId: string }) { @@ -13,6 +14,7 @@ export function ReplaysPage({ websiteId }: { websiteId: string }) { + ); } diff --git a/src/app/(main)/websites/[websiteId]/replays/ReplaysTable.tsx b/src/app/(main)/websites/[websiteId]/replays/ReplaysTable.tsx index 5549d3ddc..2ab8f860d 100644 --- a/src/app/(main)/websites/[websiteId]/replays/ReplaysTable.tsx +++ b/src/app/(main)/websites/[websiteId]/replays/ReplaysTable.tsx @@ -13,10 +13,10 @@ function formatDuration(ms: number) { return `${minutes}:${secs.toString().padStart(2, '0')}`; } -export function ReplaysTable({ websiteId, ...props }: DataTableProps & { websiteId: string }) { +export function ReplaysTable({ ...props }: DataTableProps) { const { t, labels } = useMessages(); const { formatValue } = useFormat(); - const { updateParams } = useNavigation(); + const { router, updateParams } = useNavigation(); return ( @@ -65,13 +65,11 @@ export function ReplaysTable({ websiteId, ...props }: DataTableProps & { website {(row: any) => ( - - - + )} diff --git a/src/app/(main)/websites/[websiteId]/replays/[sessionId]/ReplayPlayback.tsx b/src/app/(main)/websites/[websiteId]/replays/[sessionId]/ReplayPlayback.tsx index 9dfcf3793..5046f144a 100644 --- a/src/app/(main)/websites/[websiteId]/replays/[sessionId]/ReplayPlayback.tsx +++ b/src/app/(main)/websites/[websiteId]/replays/[sessionId]/ReplayPlayback.tsx @@ -1,12 +1,21 @@ 'use client'; -import { Column, Row, Text } from '@umami/react-zen'; +import { Button, Column, Icon, Row, Text } from '@umami/react-zen'; +import { X } from 'lucide-react'; import { SessionInfo } from '@/app/(main)/websites/[websiteId]/sessions/SessionInfo'; import { Avatar } from '@/components/common/Avatar'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { useMessages, useReplayQuery, useWebsiteSessionQuery } from '@/components/hooks'; import { ReplayPlayer } from './ReplayPlayer'; -export function ReplayPlayback({ websiteId, sessionId }: { websiteId: string; sessionId: string }) { +export function ReplayPlayback({ + websiteId, + sessionId, + onClose, +}: { + websiteId: string; + sessionId: string; + onClose?: () => void; +}) { const { data: replay, isLoading, error } = useReplayQuery(websiteId, sessionId); const { data: session } = useWebsiteSessionQuery(websiteId, sessionId); const { t, labels } = useMessages(); @@ -22,14 +31,23 @@ export function ReplayPlayback({ websiteId, sessionId }: { websiteId: string; se {replay && ( {session && ( - - - - {t(labels.replay)} - - {replay.eventCount} {t(labels.actions).toLowerCase()} - - + + + + + {t(labels.replay)} + + {replay.eventCount} {t(labels.actions).toLowerCase()} + + + + {onClose && ( + + )} )} diff --git a/src/app/(main)/websites/[websiteId]/replays/[sessionId]/ReplayPlayer.tsx b/src/app/(main)/websites/[websiteId]/replays/[sessionId]/ReplayPlayer.tsx index bb2a99a35..dfc3a12f0 100644 --- a/src/app/(main)/websites/[websiteId]/replays/[sessionId]/ReplayPlayer.tsx +++ b/src/app/(main)/websites/[websiteId]/replays/[sessionId]/ReplayPlayer.tsx @@ -1,21 +1,24 @@ 'use client'; import { Column } from '@umami/react-zen'; import { useEffect, useRef, useState } from 'react'; +import { useMobile } from '@/components/hooks'; import 'rrweb-player/dist/style.css'; export function ReplayPlayer({ events }: { events: any[] }) { const containerRef = useRef(null); const playerRef = useRef(null); const [loaded, setLoaded] = useState(false); + const { isMobile, isPhone } = useMobile(); + + const playerWidth = isPhone ? 360 : isMobile ? 640 : 1024; + const playerHeight = isPhone ? 202 : isMobile ? 360 : 576; useEffect(() => { if (!containerRef.current || !events?.length) return; - // Dynamically import rrweb-player to avoid SSR issues import('rrweb-player').then(mod => { const RRWebPlayer = mod.default; - // Clear any previous player if (containerRef.current) { containerRef.current.innerHTML = ''; } @@ -24,8 +27,8 @@ export function ReplayPlayer({ events }: { events: any[] }) { target: containerRef.current, props: { events, - width: 1024, - height: 576, + width: playerWidth, + height: playerHeight, autoPlay: false, showController: true, speedOption: [1, 2, 4, 8], @@ -41,15 +44,15 @@ export function ReplayPlayer({ events }: { events: any[] }) { playerRef.current = null; } }; - }, [events]); + }, [events, playerWidth, playerHeight]); return (