Skip to content

Commit aa9b6c0

Browse files
committed
heatmaps UI polish, finish feature gate + messaging, overlay bug fixes
1 parent cd20b3a commit aa9b6c0

4 files changed

Lines changed: 134 additions & 42 deletions

File tree

src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.module.css

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,6 @@
160160
pointer-events: none;
161161
}
162162

163-
.heatOverlay {
164-
overflow: visible;
165-
}
166-
167163
.canvasLoading {
168164
position: absolute;
169165
inset: 0;

src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { Column, Grid, Heading, Loading, Row, Switch, Text } from '@umami/react-zen';
33
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
44
import { LoadingPanel } from '@/components/common/LoadingPanel';
5-
import { useMessages, useResultQuery } from '@/components/hooks';
5+
import { useResultQuery } from '@/components/hooks';
66
import { useReplayQuery } from '@/components/hooks/queries/useReplayQuery';
77
import { formatLongNumber } from '@/lib/format';
88
import type { HeatmapMode, HeatmapPoint, HeatmapResult, HeatmapSnapshot } from '@/queries/sql';
@@ -115,6 +115,14 @@ export function Heatmap({ websiteId, urlPath, onUrlPathChange, mode, search }: H
115115
onUrlPathChange(filteredPages[0].urlPath);
116116
}, [filteredPages, isLoading, onUrlPathChange, urlPath]);
117117

118+
if (!isLoading && pages.length === 0) {
119+
return (
120+
<LoadingPanel data={pagesData} isLoading={isLoading} error={error} minHeight="900px">
121+
<EmptyState message="No data available." />
122+
</LoadingPanel>
123+
);
124+
}
125+
118126
return (
119127
<LoadingPanel data={pagesData} isLoading={isLoading} error={error} minHeight="900px">
120128
<Grid columns="320px 12px 1fr" minHeight="900px" className={styles.layoutGrid}>
@@ -167,15 +175,11 @@ function PageList({
167175
mode: HeatmapMode;
168176
hasSearch: boolean;
169177
}) {
170-
const { t, messages } = useMessages();
171-
172178
return (
173179
<Column className={styles.pageList} gap="1">
174180
<Heading size="lg">Pages</Heading>
175181
<Column className={styles.pageListItems} gap="2">
176-
{pages.length === 0 && (
177-
<Text color="muted">{hasSearch ? 'No matching pages' : t(messages.noDataAvailable)}</Text>
178-
)}
182+
{pages.length === 0 && hasSearch && <Text color="muted">No matching pages</Text>}
179183
{pages.map(p => (
180184
<button
181185
key={p.urlPath}
@@ -295,29 +299,29 @@ function HeatmapView({
295299
onReady={handleSnapshotReady}
296300
/>
297301
)}
302+
{showOverlay && (
303+
<div className={styles.overlay}>
304+
{visible.map((p, i) => {
305+
const intensity = Math.min(1, p.count / maxCount);
306+
const size = 24 + intensity * 36;
307+
return (
308+
<div
309+
key={`${p.x}-${p.y}-${i}`}
310+
className={styles.dot}
311+
style={{
312+
left: p.x * scale - size / 2,
313+
top: p.y * scale - size / 2,
314+
width: size,
315+
height: size,
316+
opacity: 0.25 + intensity * 0.55,
317+
}}
318+
title={`${p.count} click${p.count === 1 ? '' : 's'}`}
319+
/>
320+
);
321+
})}
322+
</div>
323+
)}
298324
</div>
299-
{showOverlay && (
300-
<div className={`${styles.overlay} ${styles.heatOverlay}`}>
301-
{visible.map((p, i) => {
302-
const intensity = Math.min(1, p.count / maxCount);
303-
const size = 24 + intensity * 36;
304-
return (
305-
<div
306-
key={`${p.x}-${p.y}-${i}`}
307-
className={styles.dot}
308-
style={{
309-
left: p.x * scale - size / 2,
310-
top: p.y * scale - size / 2,
311-
width: size,
312-
height: size,
313-
opacity: 0.25 + intensity * 0.55,
314-
}}
315-
title={`${p.count} click${p.count === 1 ? '' : 's'}`}
316-
/>
317-
);
318-
})}
319-
</div>
320-
)}
321325
</div>
322326
</div>
323327
{snapshot && (
@@ -560,12 +564,8 @@ function ReplaySnapshot({
560564
void finalize();
561565
});
562566

563-
replayer.on('resize', (dimension: { width?: number; height?: number }) => {
564-
resizeReplayFrame(
565-
replayer,
566-
dimension.width && Number.isFinite(dimension.width) ? dimension.width : width,
567-
dimension.height && Number.isFinite(dimension.height) ? dimension.height : height,
568-
);
567+
replayer.on('resize', () => {
568+
resizeReplayFrame(replayer, width, height);
569569
});
570570

571571
setTimeout(() => {
@@ -661,6 +661,12 @@ function resizeReplayFrame(replayer: ReplayInstance, width: number, height: numb
661661
if (wrapper) {
662662
wrapper.style.width = `${width}px`;
663663
wrapper.style.height = `${height}px`;
664+
wrapper.style.minWidth = `${width}px`;
665+
wrapper.style.minHeight = `${height}px`;
666+
wrapper.style.maxWidth = `${width}px`;
667+
wrapper.style.maxHeight = `${height}px`;
668+
wrapper.style.margin = '0';
669+
wrapper.style.padding = '0';
664670
wrapper.style.overflow = 'hidden';
665671
}
666672

@@ -669,6 +675,11 @@ function resizeReplayFrame(replayer: ReplayInstance, width: number, height: numb
669675
iframe.setAttribute('height', String(height));
670676
iframe.style.width = `${width}px`;
671677
iframe.style.height = `${height}px`;
678+
iframe.style.minWidth = `${width}px`;
679+
iframe.style.minHeight = `${height}px`;
680+
iframe.style.maxWidth = `${width}px`;
681+
iframe.style.maxHeight = `${height}px`;
682+
iframe.style.margin = '0';
672683
iframe.style.display = 'block';
673684
}
674685

@@ -678,7 +689,7 @@ function resizeReplayFrame(replayer: ReplayInstance, width: number, height: numb
678689
function EmptyState({ message }: { message?: string } = {}) {
679690
return (
680691
<Column alignItems="center" justifyContent="center" minHeight="360px" gap>
681-
<Heading size="lg">{message ? 'No data' : 'Select a page'}</Heading>
692+
{!message && <Heading size="lg">Select a page</Heading>}
682693
<Text color="muted">{message ?? 'Choose a page from the list to view its heatmap.'}</Text>
683694
</Column>
684695
);

src/app/(main)/websites/[websiteId]/(reports)/heatmaps/HeatmapsPage.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
'use client';
2-
import { Column, Row, SearchField } from '@umami/react-zen';
2+
import { Button, Column, Row, SearchField } from '@umami/react-zen';
33
import { useState } from 'react';
44
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
5+
import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder';
56
import { Panel } from '@/components/common/Panel';
6-
import { useMobile } from '@/components/hooks';
7+
import { useMessages, useMobile, useSubscription, useWebsite } from '@/components/hooks';
8+
import { Flame } from '@/components/icons';
79
import { FilterButtons } from '@/components/input/FilterButtons';
810
import type { HeatmapMode } from '@/queries/sql';
911
import { Heatmap } from './Heatmap';
@@ -17,12 +19,36 @@ export function HeatmapsPage({ websiteId }: { websiteId: string }) {
1719
const [mode, setMode] = useState<HeatmapMode>('click');
1820
const [search, setSearch] = useState('');
1921
const { isPhone } = useMobile();
22+
const website = useWebsite();
23+
const { t, labels, messages } = useMessages();
24+
const { hasFeature, cloudMode } = useSubscription(website?.teamId);
2025

2126
const buttons = [
2227
{ id: 'click', label: 'Clicks' },
2328
{ id: 'scroll', label: 'Scroll' },
2429
];
2530

31+
if (cloudMode && !hasFeature('replays')) {
32+
return (
33+
<Column gap="3">
34+
<Panel>
35+
<EmptyPlaceholder
36+
icon={<Flame />}
37+
title={t(messages.upgradeRequired, { plan: 'Business' })}
38+
description="View click and scroll heatmaps for your pages."
39+
>
40+
<Button
41+
variant="primary"
42+
onPress={() => window.open(`${process.env.cloudUrl}/settings/billing`, '_blank')}
43+
>
44+
{t(labels.upgrade)}
45+
</Button>
46+
</EmptyPlaceholder>
47+
</Panel>
48+
</Column>
49+
);
50+
}
51+
2652
return (
2753
<Column gap>
2854
<WebsiteControls websiteId={websiteId} />

0 commit comments

Comments
 (0)