Skip to content

Commit 6afe28c

Browse files
peterzimon9larsons
andauthored
Added clickthrough from post analytics to members (#24624)
ref https://linear.app/ghost/issue/PROD-2353/add-links-to-filter-members-by-opens-clicks-from-post-newsletter-page - Customers were missing the ability to filter members who received, clicked or opened emails + who signed up, directly from the post analytics Newsletter & Growth tabs. --------- Co-authored-by: Steve Larson <[email protected]>
1 parent ae66b36 commit 6afe28c

File tree

3 files changed

+101
-31
lines changed

3 files changed

+101
-31
lines changed

apps/posts/src/views/PostAnalytics/Growth/Growth.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import GrowthSources from './components/GrowthSources';
2-
import KpiCard, {KpiCardContent, KpiCardLabel, KpiCardValue} from '../components/KpiCard';
2+
import KpiCard, {KpiCardContent, KpiCardLabel, KpiCardMoreButton, KpiCardValue} from '../components/KpiCard';
33
import PostAnalyticsContent from '../components/PostAnalyticsContent';
44
import PostAnalyticsHeader from '../components/PostAnalyticsHeader';
55
import React from 'react';
66
import {Card, CardContent, CardDescription, CardHeader, CardTitle, LucideIcon, Separator, Skeleton, SkeletonTable, formatNumber} from '@tryghost/shade';
77
import {useAppContext} from '@src/App';
88
import {useGlobalData} from '@src/providers/PostAnalyticsContext';
9-
import {useParams} from '@tryghost/admin-x-framework';
9+
import {useNavigate, useParams} from '@tryghost/admin-x-framework';
1010
import {usePostReferrers} from '@src/hooks/usePostReferrers';
1111

1212
export const centsToDollars = (value : number) => {
@@ -20,6 +20,7 @@ const Growth: React.FC<postAnalyticsProps> = () => {
2020
const {postId} = useParams();
2121
const {stats: postReferrers, totals, isLoading, currencySymbol} = usePostReferrers(postId || '');
2222
const {appSettings} = useAppContext();
23+
const navigate = useNavigate();
2324

2425
// Get site URL and icon from global data
2526
const siteUrl = globalData?.url as string | undefined;
@@ -72,7 +73,16 @@ const Growth: React.FC<postAnalyticsProps> = () => {
7273
<CardContent className='p-0'>
7374
<div className='flex flex-col md:grid md:grid-cols-3 md:items-stretch'>
7475
<KpiCard className='grow'>
75-
<KpiCardLabel>
76+
<KpiCardMoreButton onClick={() => {
77+
const filterParam = encodeURIComponent(`signup:'${postId}'+conversion:-'${postId}'`);
78+
navigate(`/members?filterParam=${filterParam}`, {crossApp: true});
79+
}}>
80+
View members &rarr;
81+
</KpiCardMoreButton>
82+
<KpiCardLabel onClick={() => {
83+
const filterParam = encodeURIComponent(`signup:'${postId}'+conversion:-'${postId}'`);
84+
navigate(`/members?filterParam=${filterParam}`, {crossApp: true});
85+
}}>
7686
<LucideIcon.User strokeWidth={1.5} />
7787
Free members
7888
</KpiCardLabel>
@@ -83,9 +93,18 @@ const Growth: React.FC<postAnalyticsProps> = () => {
8393
{appSettings?.paidMembersEnabled &&
8494
<>
8595
<KpiCard className='grow'>
86-
<KpiCardLabel>
96+
<KpiCardMoreButton onClick={() => {
97+
const filterParam = encodeURIComponent(`conversion:'${postId}'`);
98+
navigate(`/members?filterParam=${filterParam}`, {crossApp: true});
99+
}}>
100+
View members &rarr;
101+
</KpiCardMoreButton>
102+
<KpiCardLabel onClick={() => {
103+
const filterParam = encodeURIComponent(`conversion:'${postId}'`);
104+
navigate(`/members?filterParam=${filterParam}`, {crossApp: true});
105+
}}>
87106
<LucideIcon.WalletCards strokeWidth={1.5} />
88-
Paid members
107+
Paid members
89108
</KpiCardLabel>
90109
<KpiCardContent>
91110
<KpiCardValue>{formatNumber(totals?.paid_members || 0)}</KpiCardValue>
@@ -94,7 +113,7 @@ const Growth: React.FC<postAnalyticsProps> = () => {
94113
<KpiCard className='grow'>
95114
<KpiCardLabel>
96115
<LucideIcon.Coins strokeWidth={1.5} />
97-
MRR
116+
MRR
98117
</KpiCardLabel>
99118
<KpiCardContent>
100119
<KpiCardValue>+{currencySymbol}{centsToDollars(totals?.mrr || 0)}</KpiCardValue>

apps/posts/src/views/PostAnalytics/Newsletter/Newsletter.tsx

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// import AudienceSelect from './components/AudienceSelect';
22
import Feedback from './components/Feedback';
3-
import KpiCard, {KpiCardContent, KpiCardLabel, KpiCardValue} from '../components/KpiCard';
3+
import KpiCard, {KpiCardContent, KpiCardLabel, KpiCardMoreButton, KpiCardValue} from '../components/KpiCard';
44
import PostAnalyticsContent from '../components/PostAnalyticsContent';
55
import PostAnalyticsHeader from '../components/PostAnalyticsHeader';
66
import {BarChartLoadingIndicator, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, ChartConfig, DataList, DataListBar, DataListBody, DataListItemContent, DataListItemValue, DataListItemValueAbs, DataListItemValuePerc, DataListRow, HTable, Input, LucideIcon, Separator, SimplePagination, SimplePaginationNavigation, SimplePaginationNextButton, SimplePaginationPreviousButton, SkeletonTable, formatNumber, formatPercentage, useSimplePagination} from '@tryghost/shade';
@@ -281,41 +281,81 @@ const Newsletter: React.FC<postAnalyticsProps> = () => {
281281
:
282282
<CardContent className='p-0'>
283283
<div className={`grid ${chartHeaderClass} items-stretch border-b`}>
284-
<KpiCard className='relative grow p-3 md:p-6'>
285-
{/* <FunnelArrow /> */}
286-
<KpiCardLabel>
284+
<KpiCard className='group relative isolate grow p-3 md:px-6 md:py-5'>
285+
<KpiCardMoreButton onClick={() => {
286+
const params = new URLSearchParams({
287+
filterParam: `emails.post_id:${postId}`,
288+
postAnalytics: postId
289+
});
290+
navigate(`/members?${params.toString()}`, {crossApp: true});
291+
}}>
292+
View members &rarr;
293+
</KpiCardMoreButton>
294+
<KpiCardLabel onClick={() => {
295+
const params = new URLSearchParams({
296+
filterParam: `emails.post_id:${postId}`,
297+
postAnalytics: postId
298+
});
299+
navigate(`/members?${params.toString()}`, {crossApp: true});
300+
}}>
287301
<div className='ml-0.5 size-[9px] rounded-full bg-chart-purple !text-sm opacity-50 lg:text-base'></div>
288-
{/* <LucideIcon.Send strokeWidth={1.5} /> */}
289302
Sent
290303
</KpiCardLabel>
291304
<KpiCardContent>
292-
<KpiCardValue className='text-xl sm:text-2xl md:text-[2.6rem]'>{formatNumber(stats.sent)}</KpiCardValue>
305+
<KpiCardValue className='text-xl leading-none sm:text-2xl md:text-[2.6rem]'>{formatNumber(stats.sent)}</KpiCardValue>
293306
</KpiCardContent>
294307
</KpiCard>
295308

296309
{emailTrackOpensEnabled &&
297-
<KpiCard className='relative grow p-3 md:p-6'>
298-
{/* <FunnelArrow /> */}
299-
<KpiCardLabel>
310+
<KpiCard className='p-3 md:px-6 md:py-5'>
311+
<KpiCardMoreButton onClick={() => {
312+
const params = new URLSearchParams({
313+
filterParam: `opened_emails.post_id:${postId}`,
314+
postAnalytics: postId
315+
});
316+
navigate(`/members?${params.toString()}`, {crossApp: true});
317+
}}>
318+
View members &rarr;
319+
</KpiCardMoreButton>
320+
<KpiCardLabel onClick={() => {
321+
const params = new URLSearchParams({
322+
filterParam: `opened_emails.post_id:${postId}`,
323+
postAnalytics: postId
324+
});
325+
navigate(`/members?${params.toString()}`, {crossApp: true});
326+
}}>
300327
<div className='ml-0.5 size-[9px] rounded-full bg-chart-blue !text-sm opacity-50 lg:text-base'></div>
301-
{/* <LucideIcon.Eye strokeWidth={1.5} /> */}
302-
Opened
328+
Opened
303329
</KpiCardLabel>
304330
<KpiCardContent>
305-
<KpiCardValue className='text-xl sm:text-2xl md:text-[2.6rem]'>{formatNumber(stats.opened)}</KpiCardValue>
331+
<KpiCardValue className='text-xl leading-none sm:text-2xl md:text-[2.6rem]'>{formatNumber(stats.opened)}</KpiCardValue>
306332
</KpiCardContent>
307333
</KpiCard>
308334
}
309335

310336
{emailTrackClicksEnabled &&
311-
<KpiCard className='relative grow p-3 md:p-6'>
312-
<KpiCardLabel>
337+
<KpiCard className='group relative isolate grow p-3 md:px-6 md:py-5'>
338+
<KpiCardMoreButton onClick={() => {
339+
const params = new URLSearchParams({
340+
filterParam: `clicked_links.post_id:${postId}`,
341+
postAnalytics: postId
342+
});
343+
navigate(`/members?${params.toString()}`, {crossApp: true});
344+
}}>
345+
View members &rarr;
346+
</KpiCardMoreButton>
347+
<KpiCardLabel onClick={() => {
348+
const params = new URLSearchParams({
349+
filterParam: `clicked_links.post_id:${postId}`,
350+
postAnalytics: postId
351+
});
352+
navigate(`/members?${params.toString()}`, {crossApp: true});
353+
}}>
313354
<div className='ml-0.5 size-[9px] rounded-full bg-chart-teal !text-sm opacity-50 lg:text-base'></div>
314-
{/* <LucideIcon.MousePointer strokeWidth={1.5} /> */}
315-
Clicked
355+
Clicked
316356
</KpiCardLabel>
317357
<KpiCardContent>
318-
<KpiCardValue className='text-xl sm:text-2xl md:text-[2.6rem]'>{formatNumber(stats.clicked)}</KpiCardValue>
358+
<KpiCardValue className='text-xl leading-none sm:text-2xl md:text-[2.6rem]'>{formatNumber(stats.clicked)}</KpiCardValue>
319359
</KpiCardContent>
320360
</KpiCard>
321361
}

apps/posts/src/views/PostAnalytics/components/KpiCard.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import {cn} from '@tryghost/shade';
2+
import {Button, cn} from '@tryghost/shade';
33

44
export const KpiCardContent: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({children, className, ...props}) => {
55
return (
@@ -11,7 +11,11 @@ export const KpiCardContent: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
1111

1212
export const KpiCardLabel: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({children, className, ...props}) => {
1313
return (
14-
<div className={cn('[&_svg]:size-4 flex items-center gap-1.5 text-base h-[22px] font-medium transition-all', className)} {...props}>
14+
<div className={
15+
cn('[&_svg]:size-4 flex items-center gap-1.5 text-base h-[22px] font-medium transition-all',
16+
className,
17+
props.onClick && 'hover:cursor-pointer hover:text-black'
18+
)} {...props}>
1519
{children}
1620
</div>
1721
);
@@ -25,20 +29,27 @@ export const KpiCardValue: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ch
2529
);
2630
};
2731

28-
const KpiCard: React.FC<React.HTMLAttributes<HTMLButtonElement>> = ({children, className, ...props}) => {
32+
export const KpiCardMoreButton: React.FC<React.ComponentProps<typeof Button>> = ({children, className, ...props}) => {
2933
return (
30-
<button
34+
<Button className={cn('absolute right-4 top-4 z-50 hidden translate-x-10 text-black opacity-0 transition-all duration-200 group-hover:translate-x-0 group-hover:opacity-100 md:!visible md:!block', className)} size='sm' variant='outline' {...props}>
35+
{children}
36+
</Button>
37+
);
38+
};
39+
40+
const KpiCard: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({children, className, ...props}) => {
41+
return (
42+
<div
3143
className={
3244
cn(
33-
'group flex flex-col border-r border-border last:border-none items-start gap-2 px-6 py-5 transition-all text-muted-foreground',
34-
props.onClick ? 'hover:bg-accent/50 hover:text-foreground' : 'cursor-auto',
45+
'group relative isolate flex flex-col border-r border-border last:border-none items-start gap-2 px-6 py-5 transition-all text-muted-foreground',
46+
props.onClick ? 'hover:bg-accent/50 hover:text-foreground cursor-pointer' : 'cursor-auto',
3547
className
3648
)}
37-
type='button'
3849
{...props}
3950
>
4051
{children}
41-
</button>
52+
</div>
4253
);
4354
};
4455

0 commit comments

Comments
 (0)