add ReplayModal, polish player css and dark mode bug, mobile-friendly

This commit is contained in:
Francis Cao
2026-03-03 13:48:31 -08:00
parent 5e68661c5d
commit 103f932109
7 changed files with 104 additions and 27 deletions
@@ -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 (
<Modal
placement="bottom"
offset="80px"
isOpen={!!replay}
onOpenChange={handleOpenChange}
isDismissable
{...props}
>
<Column height="100%" maxWidth="1320px" style={{ margin: '0 auto' }}>
<Dialog variant="sheet">
{({ close }) => (
<Column padding="6">
<ReplayPlayback websiteId={websiteId} sessionId={replay} onClose={close} />
</Column>
)}
</Dialog>
</Column>
</Modal>
);
}
@@ -8,7 +8,7 @@ export function ReplaysDataTable({ websiteId }: { websiteId: string }) {
return (
<DataGrid query={queryResult} allowPaging allowSearch>
{({ data }) => {
return <ReplaysTable data={data} websiteId={websiteId} />;
return <ReplaysTable data={data} />;
}}
</DataGrid>
);
@@ -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 }) {
<ReplaysDataTable websiteId={websiteId} />
</Panel>
<SessionModal websiteId={websiteId} />
<ReplayModal websiteId={websiteId} />
</Column>
);
}
@@ -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 (
<DataTable {...props}>
@@ -65,13 +65,11 @@ export function ReplaysTable({ websiteId, ...props }: DataTableProps & { website
</DataColumn>
<DataColumn id="play" label="" width="80px">
{(row: any) => (
<Link href={`/websites/${websiteId}/replays/${row.id}`}>
<Button variant="quiet">
<Icon>
<Play />
</Icon>
</Button>
</Link>
<Button variant="quiet" onClick={() => router.push(updateParams({ replay: row.id }))}>
<Icon>
<Play />
</Icon>
</Button>
)}
</DataColumn>
</DataTable>
@@ -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 && (
<Column gap="6">
{session && (
<Row alignItems="center" gap="4">
<Avatar seed={sessionId} size={48} />
<Column>
<Text weight="bold">{t(labels.replay)}</Text>
<Text color="muted">
{replay.eventCount} {t(labels.actions).toLowerCase()}
</Text>
</Column>
<Row justifyContent="space-between" alignItems="flex-start">
<Row alignItems="center" gap="4">
<Avatar seed={sessionId} size={48} />
<Column>
<Text weight="bold">{t(labels.replay)}</Text>
<Text color="muted">
{replay.eventCount} {t(labels.actions).toLowerCase()}
</Text>
</Column>
</Row>
{onClose && (
<Button onPress={onClose} variant="quiet">
<Icon>
<X />
</Icon>
</Button>
)}
</Row>
)}
<ReplayPlayer events={replay.events} />
@@ -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<HTMLDivElement>(null);
const playerRef = useRef<any>(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 (
<Column alignItems="center">
<div
ref={containerRef}
style={{
minWidth: 1024,
minHeight: loaded ? undefined : 576,
width: playerWidth,
minHeight: loaded ? undefined : playerHeight,
maxWidth: '100%',
overflow: 'hidden',
borderRadius: '8px',
+13
View File
@@ -55,3 +55,16 @@ select:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 1000px var(--background-color) inset !important;
transition: color 5000s ease-in-out 0s;
}
/* rrweb-player overrides */
.rr-controller__btns {
gap: 8px;
}
[data-theme="dark"] .rr-controller__btns button {
color: black;
}
[data-theme="dark"] .rr-controller__btns .label {
color: black;
}