-
-
Notifications
You must be signed in to change notification settings - Fork 11.1k
Added UTM dropdowns to Analytics Growth pages #25104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
87c70ff
6271dd6
8e65387
3480ed2
9c0cc3e
a088815
700f52e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import React from 'react'; | ||
import {TableFilterDropdownTab, TableFilterTab, TableFilterTabs} from '../table-filter-tabs/table-filter-tabs'; | ||
|
||
export type GrowthCampaignType = '' | 'UTM sources' | 'UTM mediums' | 'UTM campaigns' | 'UTM contents' | 'UTM terms'; | ||
export type GrowthTabType = 'sources' | 'campaigns'; | ||
|
||
export const GROWTH_CAMPAIGN_TYPES = [ | ||
'UTM sources', | ||
'UTM mediums', | ||
'UTM campaigns', | ||
'UTM contents', | ||
'UTM terms' | ||
] as const satisfies readonly Exclude<GrowthCampaignType, ''>[]; | ||
|
||
export const UTM_TYPE_MAP: Record<Exclude<GrowthCampaignType, ''>, string> = { | ||
'UTM sources': 'utm_source', | ||
'UTM mediums': 'utm_medium', | ||
'UTM campaigns': 'utm_campaign', | ||
'UTM contents': 'utm_content', | ||
'UTM terms': 'utm_term' | ||
}; | ||
|
||
export const getUtmType = (campaign: GrowthCampaignType): string => { | ||
return campaign ? UTM_TYPE_MAP[campaign as Exclude<GrowthCampaignType, ''>] || '' : ''; | ||
}; | ||
|
||
export const GROWTH_CAMPAIGN_OPTIONS = GROWTH_CAMPAIGN_TYPES.map(type => ({ | ||
value: type, | ||
label: type | ||
})); | ||
|
||
interface UtmGrowthTabsProps { | ||
className?: string; | ||
selectedTab: GrowthTabType; | ||
onTabChange: (tab: GrowthTabType) => void; | ||
selectedCampaign: GrowthCampaignType; | ||
onCampaignChange: (campaign: GrowthCampaignType) => void; | ||
} | ||
|
||
export const UtmGrowthTabs: React.FC<UtmGrowthTabsProps> = ({ | ||
className, | ||
selectedTab, | ||
onTabChange, | ||
selectedCampaign, | ||
onCampaignChange | ||
}) => { | ||
const handleTabChange = (tab: string) => { | ||
// Prevent switching to campaigns without selection | ||
if (tab === 'campaigns' && !selectedCampaign) { | ||
return; | ||
} | ||
onTabChange(tab as GrowthTabType); | ||
|
||
// Clear campaign when switching away | ||
if (tab !== 'campaigns') { | ||
onCampaignChange(''); | ||
} | ||
}; | ||
|
||
const handleCampaignChange = (campaign: string) => { | ||
onCampaignChange(campaign as GrowthCampaignType); | ||
onTabChange('campaigns'); | ||
}; | ||
|
||
return ( | ||
<TableFilterTabs | ||
className={className} | ||
selectedTab={selectedTab} | ||
onTabChange={handleTabChange} | ||
> | ||
<TableFilterTab value='sources'>Sources</TableFilterTab> | ||
<TableFilterDropdownTab | ||
options={GROWTH_CAMPAIGN_OPTIONS} | ||
placeholder='Campaigns' | ||
selectedOption={selectedCampaign} | ||
value='campaigns' | ||
onOptionChange={handleCampaignChange} | ||
/> | ||
</TableFilterTabs> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ import SortButton from '../components/SortButton'; | |
import StatsHeader from '../layout/StatsHeader'; | ||
import StatsLayout from '../layout/StatsLayout'; | ||
import StatsView from '../layout/StatsView'; | ||
import {Button, Card, CardContent, CardDescription, CardHeader, CardTitle, EmptyIndicator, LucideIcon, SkeletonTable, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsList, TabsTrigger, centsToDollars, formatDisplayDate, formatNumber} from '@tryghost/shade'; | ||
import {Button, Card, CardContent, CardDescription, CardHeader, CardTitle, EmptyIndicator, GROWTH_CAMPAIGN_OPTIONS, GrowthCampaignType, LucideIcon, SkeletonTable, Table, TableBody, TableCell, TableFilterDropdownTab, TableHead, TableHeader, TableRow, Tabs, TabsList, TabsTrigger, centsToDollars, formatDisplayDate, formatNumber} from '@tryghost/shade'; | ||
import {CONTENT_TYPES, ContentType, getContentTitle, getGrowthContentDescription} from '@src/utils/content-helpers'; | ||
import {getClickHandler} from '@src/utils/url-helpers'; | ||
import {getPeriodText} from '@src/utils/chart-helpers'; | ||
|
@@ -39,13 +39,17 @@ type SourcesOrder = 'free_members desc' | 'paid_members desc' | 'mrr desc' | 'so | |
type UnifiedSortOrder = TopPostsOrder | SourcesOrder; | ||
|
||
const Growth: React.FC = () => { | ||
const {range, site} = useGlobalData(); | ||
const {range, site, data: globalData} = useGlobalData(); | ||
const navigate = useNavigate(); | ||
const [sortBy, setSortBy] = useState<UnifiedSortOrder>('free_members desc'); | ||
const [selectedContentType, setSelectedContentType] = useState<ContentType>(CONTENT_TYPES.POSTS_AND_PAGES); | ||
const [selectedCampaign, setSelectedCampaign] = useState<GrowthCampaignType>(''); | ||
const [searchParams] = useSearchParams(); | ||
const {appSettings} = useAppContext(); | ||
|
||
// Check if UTM tracking is enabled | ||
const utmTrackingEnabled = globalData?.labs?.utmTracking || false; | ||
|
||
// Get the initial tab from URL search parameters | ||
const initialTab = searchParams.get('tab') || 'total-members'; | ||
|
||
|
@@ -160,14 +164,29 @@ const Growth: React.FC = () => { | |
<TableHeader> | ||
<TableRow className='[&>th]:h-auto [&>th]:pb-2 [&>th]:pt-0'> | ||
<TableHead className='min-w-[320px] pl-0'> | ||
<Tabs defaultValue={selectedContentType} variant='button-sm' onValueChange={(value: string) => { | ||
<Tabs value={selectedCampaign ? 'campaigns' : selectedContentType} variant='button-sm' onValueChange={(value: string) => { | ||
setSelectedContentType(value as ContentType); | ||
// Clear campaign selection when switching away from campaigns | ||
if (value !== 'campaigns') { | ||
setSelectedCampaign(''); | ||
} | ||
}}> | ||
Comment on lines
+167
to
173
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: setting content type to 'campaigns' causes invalid post_type in data fetch. Don’t set selectedContentType when switching to campaigns. Apply this diff: -<Tabs value={selectedCampaign ? 'campaigns' : selectedContentType} variant='button-sm' onValueChange={(value: string) => {
- setSelectedContentType(value as ContentType);
- // Clear campaign selection when switching away from campaigns
- if (value !== 'campaigns') {
- setSelectedCampaign('');
- }
-}}>
+<Tabs value={selectedCampaign ? 'campaigns' : selectedContentType} variant='button-sm' onValueChange={(value: string) => {
+ // Only update content type for content tabs
+ if (value !== 'campaigns') {
+ setSelectedContentType(value as ContentType);
+ setSelectedCampaign('');
+ }
+}}> Optionally, consider using the shared UtmGrowthTabs component for consistency with Posts UI.
🤖 Prompt for AI Agents
|
||
<TabsList> | ||
<TabsTrigger value={CONTENT_TYPES.POSTS_AND_PAGES}>Posts & pages</TabsTrigger> | ||
<TabsTrigger value={CONTENT_TYPES.POSTS}>Posts</TabsTrigger> | ||
<TabsTrigger value={CONTENT_TYPES.PAGES}>Pages</TabsTrigger> | ||
<TabsTrigger value={CONTENT_TYPES.SOURCES}>Sources</TabsTrigger> | ||
{utmTrackingEnabled && ( | ||
<TableFilterDropdownTab | ||
options={GROWTH_CAMPAIGN_OPTIONS} | ||
placeholder='Campaigns' | ||
selectedOption={selectedCampaign} | ||
value='campaigns' | ||
onOptionChange={(campaign) => { | ||
setSelectedCampaign(campaign as GrowthCampaignType); | ||
}} | ||
/> | ||
)} | ||
</TabsList> | ||
</Tabs> | ||
</TableHead> | ||
|
@@ -196,10 +215,11 @@ const Growth: React.FC = () => { | |
} | ||
</TableRow> | ||
</TableHeader> | ||
{selectedContentType === CONTENT_TYPES.SOURCES ? | ||
{(selectedContentType === CONTENT_TYPES.SOURCES || selectedCampaign) ? | ||
<GrowthSources | ||
limit={20} | ||
range={range} | ||
selectedCampaign={selectedCampaign} | ||
setSortBy={(newSortBy: SourcesOrder) => setSortBy(newSortBy)} | ||
showViewAll={true} | ||
sortBy={sortBy as SourcesOrder} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review the CardHeader placement inside table structure.
The
CardHeader
component (lines 251-254) is being passed as children toSourcesTable
and will be rendered inside a<th>
element (see line 33 where{children}
appears inTableHead
). This creates semantically incorrect HTML with complex div structures nested inside table header cells.While the
headerStyle='card'
andvariant='cardhead'
suggest this might be intentional, consider:<th>
elementsConsider refactoring to render the
CardHeader
outside theSourcesTable
, similar to how it's done whentopSources.length <= 0
(lines 209-212). The table should only contain tabular data in its header, not complex component structures.Alternative structure:
🤖 Prompt for AI Agents