diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/session-replays/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/session-replays/page-client.tsx index cdbfe0007..edfde9e6e 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/session-replays/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/session-replays/page-client.tsx @@ -39,6 +39,16 @@ import { type StreamInfo, } from "./session-replay-machine"; +const replaysPanelShellClass = + "flex-1 min-h-[520px] overflow-hidden rounded-xl bg-white/90 ring-1 ring-black/[0.06] dark:bg-background/60 dark:ring-white/[0.06]"; +const replaysListChromeClass = + "shrink-0 space-y-2 border-b border-black/[0.06] px-3 py-2.5 dark:border-border/30"; +const replaysDetailChromeClass = + "flex h-10 shrink-0 items-center justify-between gap-3 border-b border-black/[0.06] px-3 py-2 dark:border-border/30"; +const replaysViewerSurfaceClass = "bg-zinc-100 dark:bg-background"; +const replaysTransportBarClass = + "flex items-center gap-3 border-t border-black/[0.06] bg-white/95 px-3 dark:border-border/30 dark:bg-background/80"; + const PAGE_SIZE = 50; const INITIAL_CHUNK_BATCH = 20; const BACKGROUND_CHUNK_BATCH = 50; @@ -289,7 +299,7 @@ function Timeline({ const hoveredMarker = hoveredMarkerIndex !== null ? markers?.[hoveredMarkerIndex] ?? null : null; return ( -
+
+
+ + +
+ + + + + setActiveFilterDialog(open ? "lastActive" : null)}> + + + Last Active Filter + +
+ {([["24h", "Last 24 hours"], ["7d", "Last 7 days"], ["30d", "Last 30 days"]] as const).map(([value, label]) => ( + + ))} +
+
-
- - + + - setActiveFilterDialog(open ? "duration" : null)}> - - - Duration Filter - -
{ + setActiveFilterDialog(open ? "clicks" : null)}> + + + Click Count Filter + + { e.preventDefault(); applyDraftFilters(); - }}> -
-
- - setActiveFilterDialog(open ? "lastActive" : null)}> - - - Last Active Filter - -
- {([["24h", "Last 24 hours"], ["7d", "Last 7 days"], ["30d", "Last 30 days"]] as const).map(([value, label]) => ( - - ))} -
-
- -
-
-
- - setActiveFilterDialog(open ? "clicks" : null)}> - - - Click Count Filter - -
{ - e.preventDefault(); - applyDraftFilters(); - }}> -
- setDraftFilters((prev) => ({ ...prev, clickCountMin: e.target.value }))} - placeholder="Minimum click count" - /> -
-
- - -
-
-
-
- - {listError && ( -
- {listError} -
- )} - -
- {loadingInitial ? ( -
- {Array.from({ length: 8 }).map((_, i) => ( -
- -
- ))} +
+ + +
+ + +
+ + {listError && ( +
+ {listError}
- ) : recordings.length === 0 ? ( -
- - {activeFilterCount > 0 ? "No replays match these filters." : "No replays yet."} - -
- ) : ( -
- {recordings.map((r) => { - const isSelected = r.id === selectedRecordingId; - const durationMs = r.lastEventAt.getTime() - r.startedAt.getTime(); - const duration = formatDurationMs(durationMs); - return ( -
- + + {duration} + +
+
+ + {(clickCountsByReplayId.get(r.id) ?? 0) > 0 && ( + + + {clickCountsByReplayId.get(r.id)} + + )} +
+ +
+ ); + })} + + {loadingMore && ( +
+ +
- ); - })} - - {loadingMore && ( -
- - -
- )} - - )} + )} + + )} + - - + - - - )} + + + )} - -
- {(standaloneReplayError || ms.downloadError || ms.playerError) && ( -
- {standaloneReplayError && {standaloneReplayError}} - {ms.downloadError && {ms.downloadError}} - {ms.playerError && {ms.playerError}} -
- )} - -
- {selectedRecording ? ( - - {getRecordingTitle(selectedRecording)} - - ) : isStandaloneReplayPage && selectedRecordingId ? ( - - Replay {selectedRecordingId} - - ) : ( - + +
+ {(standaloneReplayError || ms.downloadError || ms.playerError) && ( +
+ {standaloneReplayError && {standaloneReplayError}} + {ms.downloadError && {ms.downloadError}} + {ms.playerError && {ms.playerError}} +
)} -
- {selectedRecordingId && ( - - )} - actRef.current({ type: "UPDATE_SETTINGS", updates })} - /> + })} + > + {replayShareLinkCopied ? ( + + ) : ( + + )} + + )} + actRef.current({ type: "UPDATE_SETTINGS", updates })} + /> +
-
- {selectedRecordingId ? ( -
+ {selectedRecordingId ? (
-
- {fullStreams.length === 0 && ( -
-
- {isDownloading ? ( - <> - +
+
+ {fullStreams.length === 0 && ( +
+
+ {isDownloading ? ( + <> + + + Loading replay... + + + ) : ( - Loading replay... + No replay data loaded yet. - - ) : ( - - No replay data loaded yet. - - )} + )} +
-
- )} + )} - {fullStreams.map((s) => { - const isActive = s.tabKey === ms.activeTabKey; - const miniIndex = miniIndexByKey.get(s.tabKey); - const isMiniVisible = miniIndex !== undefined && miniIndex >= 0 && miniIndex < EXTRA_TABS_TO_SHOW; - if (!isActive && !isMiniVisible) return null; + {fullStreams.map((s) => { + const isActive = s.tabKey === ms.activeTabKey; + const miniIndex = miniIndexByKey.get(s.tabKey); + const isMiniVisible = miniIndex !== undefined && miniIndex >= 0 && miniIndex < EXTRA_TABS_TO_SHOW; + if (!isActive && !isMiniVisible) return null; - const title = getTabLabel(s.tabKey); + const title = getTabLabel(s.tabKey); - return ( - - ); - })} -
+ )} - {activeStream && activeHasEvents && ( - + {activeHasEvents && ms.settings.skipInactivity && isSkipping && ( +
+
+ + Skipping inactivity +
+
+ )} + + {activeHasEvents && isBuffering && ( +
{ + e.preventDefault(); + togglePlayPause(); + }} + > +
+
+ Buffering... +
+
+ )} + + {activeHasEvents && playerIsPlaying && !isBuffering && ( +
{ + e.stopPropagation(); + togglePlayPause(); + }} + /> + )} + + {!activeHasEvents && ( +
+
+ {isDownloading ? ( + <> + + + Loading replay... + + + ) : ( + + No events found. + + )} +
+
+ )} + + )} + + ); + })} +
+ + {activeStream && activeHasEvents && ( + + )} +
+
+ ) : ( +
+ {loadingInitial ? ( +
+ + + Loading replay... + +
+ ) : ( +
+ + + No session replays yet + + + Session replays let you watch how users interact with your app. For info on enabling replays,{" "} + + look here + + . + +
)}
-
- ) : ( -
- {loadingInitial ? ( -
- - - Loading replay... - -
- ) : ( -
- - - No session replays yet - - - Session replays let you watch how users interact with your app. For info on enabling replays,{" "} - - look here - - . - -
- )} -
- )} -
- - + )} +
+ + +
);