1
+ import moment from 'moment' ;
2
+ import { MemberStatusItem , useMemberCountHistory } from '@tryghost/admin-x-framework/api/stats' ;
3
+ import { formatNumber } from '@tryghost/shade' ;
4
+ import { useMemo } from 'react' ;
5
+
6
+ // Type for direction values
7
+ export type DiffDirection = 'up' | 'down' | 'same' ;
8
+
9
+ // Helper function to convert range to date parameters
10
+ export const getRangeDates = ( rangeInDays : number ) => {
11
+ // Always use UTC to stay aligned with the backend’s date arithmetic
12
+ const endDate = moment . utc ( ) . format ( 'YYYY-MM-DD' ) ;
13
+ let dateFrom ;
14
+
15
+ if ( rangeInDays === 1 ) {
16
+ // Today
17
+ dateFrom = endDate ;
18
+ } else if ( rangeInDays === 1000 ) {
19
+ // All time - use a far past date
20
+ dateFrom = '2010-01-01' ;
21
+ } else {
22
+ // Specific range
23
+ // Guard against invalid ranges
24
+ const safeRange = Math . max ( 1 , rangeInDays ) ;
25
+ dateFrom = moment . utc ( ) . subtract ( safeRange - 1 , 'days' ) . format ( 'YYYY-MM-DD' ) ;
26
+ }
27
+
28
+ return { dateFrom, endDate} ;
29
+ } ;
30
+
31
+ // Calculate totals from member data
32
+ const calculateTotals = ( memberData : MemberStatusItem [ ] ) => {
33
+ if ( ! memberData . length ) {
34
+ return {
35
+ totalMembers : 0 ,
36
+ freeMembers : 0 ,
37
+ paidMembers : 0 ,
38
+ percentChanges : {
39
+ total : '0%' ,
40
+ free : '0%' ,
41
+ paid : '0%'
42
+ } ,
43
+ directions : {
44
+ total : 'same' as DiffDirection ,
45
+ free : 'same' as DiffDirection ,
46
+ paid : 'same' as DiffDirection
47
+ }
48
+ } ;
49
+ }
50
+
51
+ // Get latest values
52
+ const latest = memberData [ memberData . length - 1 ] ;
53
+
54
+ // Calculate total members
55
+ const totalMembers = latest . free + latest . paid + latest . comped ;
56
+
57
+ // Calculate percentage changes if we have enough data
58
+ const percentChanges = {
59
+ total : '0%' ,
60
+ free : '0%' ,
61
+ paid : '0%'
62
+ } ;
63
+
64
+ const directions = {
65
+ total : 'same' as DiffDirection ,
66
+ free : 'same' as DiffDirection ,
67
+ paid : 'same' as DiffDirection
68
+ } ;
69
+
70
+ if ( memberData . length > 1 ) {
71
+ // Get first day in range
72
+ const first = memberData [ 0 ] ;
73
+ const firstTotal = first . free + first . paid + first . comped ;
74
+
75
+ if ( firstTotal > 0 ) {
76
+ const totalChange = ( ( totalMembers - firstTotal ) / firstTotal ) * 100 ;
77
+ percentChanges . total = `${ Math . abs ( totalChange ) . toFixed ( 1 ) } %` ;
78
+ directions . total = totalChange > 0 ? 'up' : totalChange < 0 ? 'down' : 'same' ;
79
+ }
80
+
81
+ if ( first . free > 0 ) {
82
+ const freeChange = ( ( latest . free - first . free ) / first . free ) * 100 ;
83
+ percentChanges . free = `${ Math . abs ( freeChange ) . toFixed ( 1 ) } %` ;
84
+ directions . free = freeChange > 0 ? 'up' : freeChange < 0 ? 'down' : 'same' ;
85
+ }
86
+
87
+ if ( first . paid > 0 ) {
88
+ const paidChange = ( ( latest . paid - first . paid ) / first . paid ) * 100 ;
89
+ percentChanges . paid = `${ Math . abs ( paidChange ) . toFixed ( 1 ) } %` ;
90
+ directions . paid = paidChange > 0 ? 'up' : paidChange < 0 ? 'down' : 'same' ;
91
+ }
92
+ }
93
+
94
+ return {
95
+ totalMembers,
96
+ freeMembers : latest . free ,
97
+ paidMembers : latest . paid ,
98
+ percentChanges,
99
+ directions
100
+ } ;
101
+ } ;
102
+
103
+ // Format chart data
104
+ const formatChartData = ( memberData : MemberStatusItem [ ] ) => {
105
+ return memberData . map ( item => ( {
106
+ date : item . date ,
107
+ value : item . free + item . paid + item . comped ,
108
+ free : item . free ,
109
+ paid : item . paid ,
110
+ comped : item . comped ,
111
+ formattedValue : formatNumber ( item . free + item . paid + item . comped ) ,
112
+ label : 'Total members'
113
+ } ) ) ;
114
+ } ;
115
+
116
+ export const useGrowthStats = ( range : number ) => {
117
+ // Calculate date range
118
+ const { dateFrom, endDate} = useMemo ( ( ) => getRangeDates ( range ) , [ range ] ) ;
119
+
120
+ // Fetch member count history from API
121
+ const { data : memberCountResponse , isLoading} = useMemberCountHistory ( {
122
+ searchParams : {
123
+ date_from : dateFrom
124
+ }
125
+ } ) ;
126
+
127
+ // Process member data with stable reference
128
+ const memberData = useMemo ( ( ) => {
129
+ // Check the structure of the response and extract data
130
+ if ( memberCountResponse ?. stats ) {
131
+ return memberCountResponse . stats ;
132
+ } else if ( Array . isArray ( memberCountResponse ) ) {
133
+ // If response is directly an array
134
+ return memberCountResponse ;
135
+ }
136
+ return [ ] ;
137
+ } , [ memberCountResponse ] ) ;
138
+
139
+ // Calculate totals
140
+ const totalsData = useMemo ( ( ) => calculateTotals ( memberData ) , [ memberData ] ) ;
141
+
142
+ // Format chart data
143
+ const chartData = useMemo ( ( ) => formatChartData ( memberData ) , [ memberData ] ) ;
144
+
145
+ return {
146
+ isLoading,
147
+ memberData,
148
+ dateFrom,
149
+ endDate,
150
+ totals : totalsData ,
151
+ chartData
152
+ } ;
153
+ } ;
0 commit comments