1- import React , { useState } from 'react' ;
1+ import React from 'react' ;
22import SourceIcon from '../../components/SourceIcon' ;
3- import { BaseSourceData , ProcessedSourceData , extendSourcesWithPercentages , processSources , useNavigate , useParams } from '@tryghost/admin-x-framework' ;
4- import { Button , Card , CardContent , CardDescription , CardFooter , CardHeader , CardTitle , EmptyIndicator , LucideIcon , Separator , Sheet , SheetContent , SheetDescription , SheetHeader , SheetTitle , SheetTrigger , SkeletonTable , Table , TableBody , TableCell , TableHead , TableHeader , TableRow , UtmCampaignTabs , UtmCampaignType , UtmTabType , cn , formatNumber , getUtmType } from '@tryghost/shade' ;
3+ import { BaseSourceData , ProcessedSourceData , extendSourcesWithPercentages , processSources , useNavigate } from '@tryghost/admin-x-framework' ;
4+ import { Button , Card , CardContent , CardDescription , CardFooter , CardHeader , CardTitle , EmptyIndicator , LucideIcon , Sheet , SheetContent , SheetDescription , SheetHeader , SheetTitle , SheetTrigger , Table , TableBody , TableCell , TableHead , TableHeader , TableRow , cn , formatNumber } from '@tryghost/shade' ;
55import { useAppContext } from '@src/App' ;
6- import { useGlobalData } from '@src/providers/PostAnalyticsContext' ;
7- import { useUtmGrowthStats } from '@tryghost/admin-x-framework/api/stats' ;
86
97// Default source icon URL - apps can override this
108const DEFAULT_SOURCE_ICON_URL = 'https://www.google.com/s2/favicons?domain=ghost.org&sz=64' ;
119
12- // Number of top sources to show in the preview card
13- const TOP_SOURCES_PREVIEW_LIMIT = 10 ;
14-
1510interface SourcesTableProps {
1611 data : ProcessedSourceData [ ] | null ;
1712 mode : 'visits' | 'growth' ;
@@ -112,72 +107,16 @@ export const GrowthSources: React.FC<SourcesCardProps> = ({
112107} ) => {
113108 const { appSettings} = useAppContext ( ) ;
114109 const navigate = useNavigate ( ) ;
115- const { data : globalData } = useGlobalData ( ) ;
116- const { postId} = useParams ( ) ;
117- const [ selectedTab , setSelectedTab ] = useState < UtmTabType > ( 'sources' ) ;
118- const [ selectedCampaign , setSelectedCampaign ] = useState < UtmCampaignType > ( '' ) ;
119-
120- // Check if UTM tracking is enabled in labs
121- const utmTrackingEnabled = globalData ?. labs ?. utmTracking || false ;
122-
123- const shouldFetchUtmData = utmTrackingEnabled
124- && selectedTab === 'campaigns'
125- && ! ! selectedCampaign
126- && ! ! postId ;
127-
128- const utmType = getUtmType ( selectedCampaign ) ;
129- const { data : utmData , isFetching : isUtmFetching } = useUtmGrowthStats ( {
130- searchParams : {
131- utm_type : utmType ,
132- post_id : postId || ''
133- } ,
134- enabled : shouldFetchUtmData
135- } ) ;
136-
137- // Select and transform the appropriate data based on current view
138- const displayData = React . useMemo ( ( ) => {
139- const isShowingCampaigns = selectedTab === 'campaigns' && selectedCampaign ;
140-
141- if ( ! isShowingCampaigns ) {
142- return data ;
143- }
144-
145- if ( ! utmData ?. stats ) {
146- return null ;
147- }
148-
149- return utmData . stats . map ( item => ( {
150- ...item ,
151- source : item . utm_value || '(not set)'
152- } ) ) ;
153- } , [ data , utmData , selectedTab , selectedCampaign ] ) ;
154-
155110 // Process and group sources data with pre-computed icons and display values
156- const processedData = React . useMemo ( ( ) : ProcessedSourceData [ ] => {
157- // UTM campaigns: UTM values are not domains, so don't show icons or links
158- if ( selectedTab === 'campaigns' && selectedCampaign && displayData ) {
159- return displayData . map ( item => ( {
160- source : String ( item . source || '(not set)' ) ,
161- visits : 0 ,
162- isDirectTraffic : false ,
163- iconSrc : '' ,
164- displayName : String ( item . source || '(not set)' ) ,
165- linkUrl : undefined ,
166- free_members : item . free_members || 0 ,
167- paid_members : item . paid_members || 0 ,
168- mrr : item . mrr || 0
169- } ) ) ;
170- }
171-
172- // For regular sources, use the standard processing
111+ const processedData = React . useMemo ( ( ) => {
173112 return processSources ( {
174- data : displayData ,
113+ data,
175114 mode,
176115 siteUrl,
177116 siteIcon,
178117 defaultSourceIconUrl
179118 } ) ;
180- } , [ displayData , siteUrl , siteIcon , mode , defaultSourceIconUrl , selectedTab , selectedCampaign ] ) ;
119+ } , [ data , siteUrl , siteIcon , mode , defaultSourceIconUrl ] ) ;
181120
182121 // Extend processed data with percentage values for visits mode
183122 const extendedData = React . useMemo ( ( ) => {
@@ -188,29 +127,25 @@ export const GrowthSources: React.FC<SourcesCardProps> = ({
188127 } ) ;
189128 } , [ processedData , totalVisitors , mode ] ) ;
190129
191- const topSources = extendedData . slice ( 0 , TOP_SOURCES_PREVIEW_LIMIT ) ;
130+ const topSources = extendedData . slice ( 0 , 10 ) ;
192131
193- // Check if original source data has content (for tab visibility)
194- const hasAnySourceData = data && data . length > 0 ;
195-
196- // Generate title and description based on mode, tab, and campaign
197- const cardTitle = selectedTab === 'campaigns' && selectedCampaign ? selectedCampaign : title ;
132+ // Generate description based on mode and range
198133 const cardDescription = description || (
199134 mode === 'growth'
200135 ? 'Where did your growth come from?'
201136 : `How readers found your ${ range ? 'site' : 'post' } ${ range && getPeriodText ? ` ${ getPeriodText ( range ) } ` : '' } `
202137 ) ;
203138
204- const sheetTitle = selectedTab === 'campaigns' && selectedCampaign ? selectedCampaign : ( mode === 'growth' ? 'Sources' : 'Top sources' ) ;
139+ const sheetTitle = mode === 'growth' ? 'Sources' : 'Top sources' ;
205140 const sheetDescription = mode === 'growth'
206141 ? 'Where did your growth come from?'
207142 : `How readers found your ${ range ? 'site' : 'post' } ${ range && getPeriodText ? ` ${ getPeriodText ( range ) } ` : '' } ` ;
208143
209144 return (
210145 < Card className = { cn ( 'group/datalist w-full max-w-[calc(100vw-64px)] overflow-x-auto sidebar:max-w-[calc(100vw-64px-280px)]' , className ) } data-testid = 'top-sources-card' >
211- { hasAnySourceData &&
146+ { topSources . length <= 0 &&
212147 < CardHeader >
213- < CardTitle > { cardTitle } </ CardTitle >
148+ < CardTitle > { title } </ CardTitle >
214149 < CardDescription > { cardDescription } </ CardDescription >
215150 </ CardHeader >
216151 }
@@ -228,60 +163,33 @@ export const GrowthSources: React.FC<SourcesCardProps> = ({
228163 >
229164 < LucideIcon . Activity />
230165 </ EmptyIndicator >
166+ ) : topSources . length > 0 ? (
167+ < SourcesTable
168+ data = { topSources }
169+ defaultSourceIconUrl = { defaultSourceIconUrl }
170+ getPeriodText = { getPeriodText }
171+ headerStyle = 'card'
172+ mode = { mode }
173+ range = { range }
174+ >
175+ < CardHeader >
176+ < CardTitle > { title } </ CardTitle >
177+ < CardDescription > { cardDescription } </ CardDescription >
178+ </ CardHeader >
179+ </ SourcesTable >
231180 ) : (
232- < >
233- { utmTrackingEnabled && mode === 'growth' && hasAnySourceData && (
234- < >
235- < div className = 'mb-4' >
236- < UtmCampaignTabs
237- selectedCampaign = { selectedCampaign }
238- selectedTab = { selectedTab }
239- onCampaignChange = { setSelectedCampaign }
240- onTabChange = { setSelectedTab }
241- />
242- </ div >
243- < Separator />
244- </ >
245- ) }
246- { ( selectedTab === 'campaigns' && selectedCampaign && isUtmFetching ) ? (
247- < SkeletonTable className = 'mt-3' />
248- ) : ( hasAnySourceData || ( selectedTab === 'campaigns' && selectedCampaign ) ) ? (
249- < >
250- { topSources . length > 0 ? (
251- < SourcesTable
252- data = { topSources }
253- defaultSourceIconUrl = { defaultSourceIconUrl }
254- getPeriodText = { getPeriodText }
255- mode = { mode }
256- range = { range }
257- />
258- ) : (
259- < div className = 'py-20 text-center text-sm text-gray-700' data-testid = 'empty-sources-indicator' >
260- < EmptyIndicator
261- className = 'h-full'
262- description = { mode === 'growth' && `No data available for this view` }
263- title = { `No ${ selectedTab === 'campaigns' && selectedCampaign ? selectedCampaign . toLowerCase ( ) : 'sources' } data available` }
264- >
265- < LucideIcon . UserPlus strokeWidth = { 1.5 } />
266- </ EmptyIndicator >
267- </ div >
268- ) }
269- </ >
270- ) : (
271- < div className = 'py-20 text-center text-sm text-gray-700' data-testid = 'empty-sources-indicator' >
272- < EmptyIndicator
273- className = 'h-full'
274- description = { mode === 'growth' && `Once someone signs up on this post, sources will show here` }
275- title = { `No sources data available ${ getPeriodText ? getPeriodText ( range ) : '' } ` }
276- >
277- < LucideIcon . UserPlus strokeWidth = { 1.5 } />
278- </ EmptyIndicator >
279- </ div >
280- ) }
281- </ >
181+ < div className = 'py-20 text-center text-sm text-gray-700' >
182+ < EmptyIndicator
183+ className = 'h-full'
184+ description = { mode === 'growth' && `Once someone signs up on this post, sources will show here` }
185+ title = { `No sources data available ${ getPeriodText ? getPeriodText ( range ) : '' } ` }
186+ >
187+ < LucideIcon . UserPlus strokeWidth = { 1.5 } />
188+ </ EmptyIndicator >
189+ </ div >
282190 ) }
283191 </ CardContent >
284- { extendedData . length > TOP_SOURCES_PREVIEW_LIMIT &&
192+ { extendedData . length > 10 &&
285193 < CardFooter >
286194 < Sheet >
287195 < SheetTrigger asChild >
0 commit comments