Skip to content

Commit b966dbd

Browse files
committed
feat: add activity calendar section to account page
1 parent 2c75bfe commit b966dbd

File tree

12 files changed

+272
-9
lines changed

12 files changed

+272
-9
lines changed

src/core/accounts/account.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const Account = new Record({
88
account_is_loading_blocks_change_summary: true,
99
account_is_loading_blocks_send_summary: true,
1010
account_is_loading_balance_history: false,
11+
account_is_loading_blocks_per_day: false,
1112

1213
account: null,
1314
alias: null,
@@ -25,6 +26,7 @@ export const Account = new Record({
2526
delegators: new List(),
2627
open: new Map(),
2728
balance_history: new List(),
29+
blocks_per_day: new List(),
2830

2931
blocks_summary: new Map(),
3032

src/core/accounts/actions.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ export const accountsActions = {
3232
GET_ACCOUNT_BALANCE_HISTORY_FULFILLED:
3333
'GET_ACCOUNT_BALANCE_HISTORY_FULFILLED',
3434

35+
GET_ACCOUNT_BLOCKS_PER_DAY: 'GET_ACCOUNT_BLOCKS_PER_DAY',
36+
GET_ACCOUNT_BLOCKS_PER_DAY_FAILED: 'GET_ACCOUNT_BLOCKS_PER_DAY_FAILED',
37+
GET_ACCOUNT_BLOCKS_PER_DAY_PENDING: 'GET_ACCOUNT_BLOCKS_PER_DAY_PENDING',
38+
GET_ACCOUNT_BLOCKS_PER_DAY_FULFILLED: 'GET_ACCOUNT_BLOCKS_PER_DAY_FULFILLED',
39+
3540
filter: ({ field, value, label } = {}) => ({
3641
type: accountsActions.FILTER_REPRESENTATIVES,
3742
payload: {
@@ -209,6 +214,36 @@ export const accountsActions = {
209214
params,
210215
data
211216
}
217+
}),
218+
219+
get_account_blocks_per_day: (account) => ({
220+
type: accountsActions.GET_ACCOUNT_BLOCKS_PER_DAY,
221+
payload: {
222+
account
223+
}
224+
}),
225+
226+
getAccountBlocksPerDayFailed: (params, error) => ({
227+
type: accountsActions.GET_ACCOUNT_BLOCKS_PER_DAY_FAILED,
228+
payload: {
229+
params,
230+
error
231+
}
232+
}),
233+
234+
getAccountBlocksPerDayPending: (params) => ({
235+
type: accountsActions.GET_ACCOUNT_BLOCKS_PER_DAY_PENDING,
236+
payload: {
237+
params
238+
}
239+
}),
240+
241+
getAccountBlocksPerDayFulfilled: (params, data) => ({
242+
type: accountsActions.GET_ACCOUNT_BLOCKS_PER_DAY_FULFILLED,
243+
payload: {
244+
params,
245+
data
246+
}
212247
})
213248
}
214249

@@ -247,3 +282,9 @@ export const accountStatsRequestActions = {
247282
pending: accountsActions.getAccountStatsPending,
248283
fulfilled: accountsActions.getAccountStatsFulfilled
249284
}
285+
286+
export const accountBlocksPerDayRequestActions = {
287+
failed: accountsActions.getAccountBlocksPerDayFailed,
288+
pending: accountsActions.getAccountBlocksPerDayPending,
289+
fulfilled: accountsActions.getAccountBlocksPerDayFulfilled
290+
}

src/core/accounts/reducer.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ export function accountsReducer(state = initialState, { payload, type }) {
5151

5252
case accountsActions.GET_ACCOUNT_FULFILLED:
5353
return state.withMutations((state) => {
54-
const existing_account = state
55-
.getIn(['items', payload.params], new Account())
56-
.toJS()
54+
const existing_account = state.getIn(
55+
['items', payload.params],
56+
new Account()
57+
)
5758
const updated_account = createAccount({
5859
...existing_account,
5960
...payload.data,
@@ -139,6 +140,28 @@ export function accountsReducer(state = initialState, { payload, type }) {
139140
new Map(payload.data)
140141
)
141142

143+
case accountsActions.GET_ACCOUNT_BLOCKS_PER_DAY_FULFILLED:
144+
return state.withMutations((state) => {
145+
state.setIn(
146+
['items', payload.params.account, 'blocks_per_day'],
147+
List(payload.data)
148+
)
149+
state.setIn(
150+
[
151+
'items',
152+
payload.params.account,
153+
'account_is_loading_blocks_per_day'
154+
],
155+
false
156+
)
157+
})
158+
159+
case accountsActions.GET_ACCOUNT_BLOCKS_PER_DAY_PENDING:
160+
return state.setIn(
161+
['items', payload.params.account, 'account_is_loading_blocks_per_day'],
162+
true
163+
)
164+
142165
default:
143166
return state
144167
}

src/core/accounts/sagas.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
getAccountBlocksSummary,
88
get_account_balance_history,
99
get_request_history,
10-
get_account_stats
10+
get_account_stats,
11+
get_account_blocks_per_day
1112
} from '@core/api'
1213
import { accountsActions } from './actions'
1314

@@ -42,6 +43,15 @@ export function* loadAccountStats({ payload }) {
4243
yield call(get_account_stats, { account })
4344
}
4445

46+
export function* loadAccountBlocksPerDay({ payload }) {
47+
const { account } = payload
48+
const request_history = yield select(get_request_history)
49+
if (request_history.has(`GET_ACCOUNT_BLOCKS_PER_DAY_${account}`)) {
50+
return
51+
}
52+
yield call(get_account_blocks_per_day, { account })
53+
}
54+
4555
//= ====================================
4656
// WATCHERS
4757
// -------------------------------------
@@ -65,6 +75,13 @@ export function* watchGetAccountStats() {
6575
yield takeLatest(accountsActions.GET_ACCOUNT_STATS, loadAccountStats)
6676
}
6777

78+
export function* watchGetAccountBlocksPerDay() {
79+
yield takeLatest(
80+
accountsActions.GET_ACCOUNT_BLOCKS_PER_DAY,
81+
loadAccountBlocksPerDay
82+
)
83+
}
84+
6885
//= ====================================
6986
// ROOT
7087
// -------------------------------------
@@ -73,5 +90,6 @@ export const accountSagas = [
7390
fork(watchGetRepresentatives),
7491
fork(watchGetAccount),
7592
fork(watchGetAccountBalanceHistory),
76-
fork(watchGetAccountStats)
93+
fork(watchGetAccountStats),
94+
fork(watchGetAccountBlocksPerDay)
7795
]

src/core/api/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export {
2121
get_blocks_unconfirmed_summary,
2222
get_account_balance_history,
2323
get_price_history,
24-
get_account_stats
24+
get_account_stats,
25+
get_account_blocks_per_day
2526
} from './sagas'
2627

2728
export { api_reducer } from './reducer'

src/core/api/reducer.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,21 @@ export function api_reducer(state = initialState, { payload, type }) {
6969
`GET_ACCOUNT_STATS_${payload.params.account}`
7070
])
7171

72+
case accountsActions.GET_ACCOUNT_BLOCKS_PER_DAY_PENDING:
73+
return state.setIn(
74+
[
75+
'request_history',
76+
`GET_ACCOUNT_BLOCKS_PER_DAY_${payload.params.account}`
77+
],
78+
true
79+
)
80+
81+
case accountsActions.GET_ACCOUNT_BLOCKS_PER_DAY_FAILED:
82+
return state.deleteIn([
83+
'request_history',
84+
`GET_ACCOUNT_BLOCKS_PER_DAY_${payload.params.account}`
85+
])
86+
7287
default:
7388
return state
7489
}

src/core/api/sagas.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import {
2424
accountOpenRequestActions,
2525
accountBlocksSummaryRequestActions,
2626
accountBalanceHistoryRequestActions,
27-
accountStatsRequestActions
27+
accountStatsRequestActions,
28+
accountBlocksPerDayRequestActions
2829
} from '@core/accounts/actions'
2930
import { blockRequestActions } from '@core/blocks/actions'
3031
import { dailyRequestActions } from '@core/ledger/actions'
@@ -164,3 +165,9 @@ export const get_account_stats = fetch.bind(
164165
api.get_account_stats,
165166
accountStatsRequestActions
166167
)
168+
169+
export const get_account_blocks_per_day = fetch.bind(
170+
null,
171+
api.get_account_blocks_per_day,
172+
accountBlocksPerDayRequestActions
173+
)

src/core/api/service.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ export const api = {
107107
get_account_stats({ account }) {
108108
const url = `${API_URL}/nanodb/accounts/${account}/stats`
109109
return { url }
110+
},
111+
get_account_blocks_per_day({ account }) {
112+
const url = `${API_URL}/nanodb/accounts/${account}/blocks_per_day`
113+
return { url }
110114
}
111115
}
112116

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import React, { useEffect, useMemo } from 'react'
2+
import PropTypes from 'prop-types'
3+
import ImmutablePropTypes from 'react-immutable-proptypes'
4+
import { List } from 'immutable'
5+
import ReactEChartsCore from 'echarts-for-react/lib/core'
6+
import * as echarts from 'echarts/core'
7+
import { HeatmapChart } from 'echarts/charts'
8+
import {
9+
TooltipComponent,
10+
CalendarComponent,
11+
VisualMapComponent
12+
} from 'echarts/components'
13+
import { CanvasRenderer } from 'echarts/renderers'
14+
15+
echarts.use([
16+
HeatmapChart,
17+
TooltipComponent,
18+
CalendarComponent,
19+
CanvasRenderer,
20+
VisualMapComponent
21+
])
22+
23+
export default function AccountActivityCalendar({
24+
account,
25+
get_account_blocks_per_day
26+
}) {
27+
const account_address = account.get('account')
28+
29+
useEffect(() => {
30+
if (account_address) {
31+
get_account_blocks_per_day(account_address)
32+
}
33+
}, [account_address, get_account_blocks_per_day])
34+
35+
const blocks_per_day = account.get('blocks_per_day', new List())
36+
const is_loading = account.get('account_is_loading_blocks_per_day')
37+
38+
const heatmap_data_by_year = useMemo(() => {
39+
const data_by_year = {}
40+
blocks_per_day
41+
.filter((item) => item.day != null)
42+
.forEach((item) => {
43+
const year = item.day.split('-')[0]
44+
if (!data_by_year[year]) {
45+
data_by_year[year] = []
46+
}
47+
data_by_year[year].push([item.day, Number(item.block_count)])
48+
})
49+
return data_by_year
50+
}, [blocks_per_day])
51+
52+
const max_block_count = useMemo(() => {
53+
let max = 0
54+
Object.values(heatmap_data_by_year).forEach((yearData) => {
55+
yearData.forEach((item) => {
56+
if (item[1] > max) max = item[1]
57+
})
58+
})
59+
return max
60+
}, [heatmap_data_by_year])
61+
62+
const options_by_year = useMemo(() => {
63+
const years_descending = Object.keys(heatmap_data_by_year).sort(
64+
(a, b) => b - a
65+
)
66+
return years_descending.map((year, index) => {
67+
const data = heatmap_data_by_year[year]
68+
return {
69+
tooltip: {
70+
position: 'top',
71+
formatter: function (params) {
72+
const formatted_date = new Date(params.value[0]).toLocaleDateString(
73+
'en-US',
74+
{
75+
year: 'numeric',
76+
month: 'long',
77+
day: 'numeric'
78+
}
79+
)
80+
const color_span = `<span style="display:inline-block;margin-left:4px;border-radius:10px;width:10px;height:10px;background-color:${params.color};"></span>`
81+
return `Date: ${formatted_date}<br/>Blocks: ${params.value[1]}${color_span}`
82+
}
83+
},
84+
visualMap: {
85+
show: index === 0,
86+
min: 0,
87+
max: max_block_count,
88+
calculable: true,
89+
orient: 'horizontal',
90+
left: 'center',
91+
top: 0
92+
},
93+
calendar: {
94+
range: year,
95+
cellSize: ['auto', 13],
96+
top: index === 0 ? '80' : '20', // Adjusted top margin for the first chart to make room for the visualMap
97+
bottom: '20'
98+
},
99+
series: [
100+
{
101+
type: 'heatmap',
102+
coordinateSystem: 'calendar',
103+
data
104+
}
105+
]
106+
}
107+
})
108+
}, [heatmap_data_by_year, max_block_count])
109+
110+
return (
111+
<>
112+
{options_by_year.map((option, index) => (
113+
<ReactEChartsCore
114+
key={index}
115+
echarts={echarts}
116+
option={option}
117+
style={{
118+
height: index === 0 ? '200px' : '140px',
119+
width: '100%',
120+
marginTop: '10px',
121+
marginBottom: '10px'
122+
}}
123+
showLoading={is_loading}
124+
loadingOption={{
125+
maskColor: 'rgba(255, 255, 255, 0)',
126+
text: '',
127+
spinnerRadius: 24,
128+
lineWidth: 2
129+
}}
130+
/>
131+
))}
132+
</>
133+
)
134+
}
135+
136+
AccountActivityCalendar.propTypes = {
137+
account: ImmutablePropTypes.map.isRequired,
138+
get_account_blocks_per_day: PropTypes.func.isRequired
139+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { connect } from 'react-redux'
2+
3+
import { accountsActions } from '@core/accounts/actions'
4+
5+
import AccountActivityCalendar from './account-activity-calendar'
6+
7+
const map_dispatch_to_props = {
8+
get_account_blocks_per_day: accountsActions.get_account_blocks_per_day
9+
}
10+
11+
export default connect(null, map_dispatch_to_props)(AccountActivityCalendar)

0 commit comments

Comments
 (0)