add ReplayModal, polish player css and dark mode bug, mobile-friendly
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user