Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
F
frontend
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
vicotor
frontend
Commits
c3953467
Commit
c3953467
authored
Jan 03, 2023
by
isstuev
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
token holders
parent
a8fde650
Changes
20
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
384 additions
and
64 deletions
+384
-64
resources.ts
lib/api/resources.ts
+8
-9
useQueryWithPages.ts
lib/hooks/useQueryWithPages.ts
+12
-12
tokenHolders.ts
mocks/tokens/tokenHolders.ts
+20
-0
tokenInfo.ts
types/api/tokenInfo.ts
+17
-0
AddressInternalTxs.tsx
ui/address/AddressInternalTxs.tsx
+4
-8
AddressLogs.tsx
ui/address/AddressLogs.tsx
+4
-10
AddressTokenTransfers.tsx
ui/address/AddressTokenTransfers.tsx
+2
-1
AddressTxs.tsx
ui/address/AddressTxs.tsx
+4
-8
Address.tsx
ui/pages/Address.tsx
+9
-5
Token.tsx
ui/pages/Token.tsx
+48
-4
TokenTransfer.tsx
ui/shared/TokenTransfer/TokenTransfer.tsx
+5
-7
TokenHolders.tsx
ui/token/TokenHolders/TokenHolders.tsx
+66
-0
TokenHoldersList.pw.tsx
ui/token/TokenHolders/TokenHoldersList.pw.tsx
+20
-0
TokenHoldersList.tsx
ui/token/TokenHolders/TokenHoldersList.tsx
+27
-0
TokenHoldersListItem.tsx
ui/token/TokenHolders/TokenHoldersListItem.tsx
+41
-0
TokenHoldersTable.pw.tsx
ui/token/TokenHolders/TokenHoldersTable.pw.tsx
+20
-0
TokenHoldersTable.tsx
ui/token/TokenHolders/TokenHoldersTable.tsx
+33
-0
TokenHoldersTableItem.tsx
ui/token/TokenHolders/TokenHoldersTableItem.tsx
+44
-0
TokenHoldersList.pw.tsx_default_base-view-1.png
...enshots__/TokenHoldersList.pw.tsx_default_base-view-1.png
+0
-0
TokenHoldersTable.pw.tsx_default_base-view-1.png
...nshots__/TokenHoldersTable.pw.tsx_default_base-view-1.png
+0
-0
No files found.
lib/api/resources.ts
View file @
c3953467
...
...
@@ -21,7 +21,7 @@ import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl';
import
type
{
LogsResponseTx
,
LogsResponseAddress
}
from
'
types/api/log
'
;
import
type
{
RawTracesResponse
}
from
'
types/api/rawTrace
'
;
import
type
{
Stats
,
Charts
,
HomeStats
}
from
'
types/api/stats
'
;
import
type
{
TokenCounters
,
TokenInfo
}
from
'
types/api/tokenInfo
'
;
import
type
{
TokenCounters
,
TokenInfo
,
TokenHolders
}
from
'
types/api/tokenInfo
'
;
import
type
{
TokenTransferResponse
,
TokenTransferFilters
}
from
'
types/api/tokenTransfer
'
;
import
type
{
TransactionsResponseValidated
,
TransactionsResponsePending
,
Transaction
}
from
'
types/api/transaction
'
;
import
type
{
TTxsFilters
}
from
'
types/api/txsFilters
'
;
...
...
@@ -176,13 +176,10 @@ export const RESOURCES = {
token_counters
:
{
path
:
'
/api/v2/tokens/:hash/counters
'
,
},
// TOKEN
token
:
{
path
:
'
/api/v2/tokens/:hash
'
,
},
token_counters
:
{
path
:
'
/api/v2/tokens/:hash/counters
'
,
token_holders
:
{
path
:
'
/api/v2/tokens/:hash/holders
'
,
paginationFields
:
[
'
items_count
'
as
const
,
'
value
'
as
const
],
filterFields
:
[],
},
// HOMEPAGE
...
...
@@ -240,7 +237,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'
txs_validated
'
|
'
txs_pending
'
|
'
tx_internal_txs
'
|
'
tx_logs
'
|
'
tx_token_transfers
'
|
'
address_txs
'
|
'
address_internal_txs
'
|
'
address_token_transfers
'
|
'
address_blocks_validated
'
|
'
address_coin_balance
'
|
'
address_logs
'
;
'
address_logs
'
|
'
token_holders
'
;
export
type
PaginatedResponse
<
Q
extends
PaginatedResources
>
=
ResourcePayload
<
Q
>
;
...
...
@@ -283,6 +281,7 @@ Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
Q
extends
'
address_logs
'
?
LogsResponseAddress
:
Q
extends
'
token
'
?
TokenInfo
:
Q
extends
'
token_counters
'
?
TokenCounters
:
Q
extends
'
token_holders
'
?
TokenHolders
:
Q
extends
'
config_json_rpc
'
?
JsonRpcUrlResponse
:
Q
extends
'
contract
'
?
SmartContract
:
never
;
...
...
lib/hooks/useQueryWithPages.ts
View file @
c3953467
...
...
@@ -4,7 +4,7 @@ import omit from 'lodash/omit';
import
pick
from
'
lodash/pick
'
;
import
{
useRouter
}
from
'
next/router
'
;
import
React
,
{
useCallback
}
from
'
react
'
;
import
{
animateScroll
,
scroller
}
from
'
react-scroll
'
;
import
{
animateScroll
}
from
'
react-scroll
'
;
import
type
{
PaginatedResources
,
PaginatedResponse
,
PaginationFilters
}
from
'
lib/api/resources
'
;
import
{
RESOURCES
}
from
'
lib/api/resources
'
;
...
...
@@ -16,7 +16,7 @@ interface Params<Resource extends PaginatedResources> {
options
?:
UseApiQueryParams
<
Resource
>
[
'
queryOptions
'
];
pathParams
?:
UseApiQueryParams
<
Resource
>
[
'
pathParams
'
];
filters
?:
PaginationFilters
<
Resource
>
;
scroll
?:
{
elem
:
string
;
offset
:
number
}
;
scroll
Ref
?:
React
.
RefObject
<
HTMLDivElement
>
;
}
export
default
function
useQueryWithPages
<
Resource
extends
PaginatedResources
>
({
...
...
@@ -24,7 +24,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
filters
,
options
,
pathParams
,
scroll
,
scroll
Ref
,
}:
Params
<
Resource
>
)
{
const
resource
=
RESOURCES
[
resourceName
];
const
queryClient
=
useQueryClient
();
...
...
@@ -46,8 +46,8 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const
queryParams
=
{
...
filters
,
...
pageParams
[
page
]
};
const
scrollToTop
=
useCallback
(()
=>
{
scroll
?
scroller
.
scrollTo
(
scroll
.
elem
,
{
offset
:
scroll
.
offset
}
)
:
animateScroll
.
scrollToTop
({
duration
:
0
});
},
[
scroll
]);
scroll
Ref
?.
current
?
scrollRef
.
current
.
scrollIntoView
(
true
)
:
animateScroll
.
scrollToTop
({
duration
:
0
});
},
[
scroll
Ref
]);
const
queryResult
=
useApiQuery
(
resourceName
,
{
pathParams
,
...
...
@@ -77,10 +77,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
nextPageQuery
.
page
=
String
(
page
+
1
);
setHasPagination
(
true
);
router
.
push
({
pathname
:
router
.
pathname
,
query
:
nextPageQuery
},
undefined
,
{
shallow
:
true
})
.
then
(()
=>
{
scrollToTop
();
});
scrollToTop
();
router
.
push
({
pathname
:
router
.
pathname
,
query
:
nextPageQuery
},
undefined
,
{
shallow
:
true
});
},
[
data
?.
next_page_params
,
page
,
router
,
scrollToTop
]);
const
onPrevPageClick
=
useCallback
(()
=>
{
...
...
@@ -96,9 +95,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
nextPageQuery
.
page
=
String
(
page
-
1
);
}
scrollToTop
();
router
.
push
({
pathname
:
router
.
pathname
,
query
:
nextPageQuery
},
undefined
,
{
shallow
:
true
})
.
then
(()
=>
{
scrollToTop
();
setPage
(
prev
=>
prev
-
1
);
page
===
2
&&
queryClient
.
removeQueries
({
queryKey
:
[
resourceName
]
});
});
...
...
@@ -108,9 +107,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const
resetPage
=
useCallback
(()
=>
{
queryClient
.
removeQueries
({
queryKey
:
[
resourceName
]
});
scrollToTop
();
router
.
push
({
pathname
:
router
.
pathname
,
query
:
omit
(
router
.
query
,
resource
.
paginationFields
,
'
page
'
)
},
undefined
,
{
shallow
:
true
}).
then
(()
=>
{
queryClient
.
removeQueries
({
queryKey
:
[
resourceName
]
});
scrollToTop
();
setPage
(
1
);
setPageParams
({});
canGoBackwards
.
current
=
true
;
...
...
@@ -133,6 +132,8 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
}
});
}
scrollToTop
();
router
.
push
(
{
pathname
:
router
.
pathname
,
...
...
@@ -143,7 +144,6 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
).
then
(()
=>
{
setPage
(
1
);
setPageParams
({});
scrollToTop
();
});
},
[
router
,
resource
.
paginationFields
,
resource
.
filterFields
,
scrollToTop
]);
...
...
mocks/tokens/tokenHolders.ts
0 → 100644
View file @
c3953467
import
type
{
TokenHolders
}
from
'
types/api/tokenInfo
'
;
import
{
withName
,
withoutName
}
from
'
mocks/address/address
'
;
export
const
tokenHolders
:
TokenHolders
=
{
items
:
[
{
address
:
withName
,
value
:
'
107014805905725000000
'
,
},
{
address
:
withoutName
,
value
:
'
207014805905725000000
'
,
},
],
next_page_params
:
{
value
:
'
50
'
,
items_count
:
50
,
},
};
types/api/tokenInfo.ts
View file @
c3953467
import
type
{
AddressParam
}
from
'
./addressParams
'
;
export
type
TokenType
=
'
ERC-20
'
|
'
ERC-721
'
|
'
ERC-1155
'
;
export
interface
TokenInfo
{
...
...
@@ -17,3 +19,18 @@ export interface TokenCounters {
}
export
type
TokenInfoGeneric
<
Type
extends
TokenType
>
=
Omit
<
TokenInfo
,
'
type
'
>
&
{
type
:
Type
};
export
interface
TokenHolders
{
items
:
Array
<
TokenHolder
>
;
next_page_params
:
TokenHoldersPagination
;
}
export
type
TokenHolder
=
{
address
:
AddressParam
;
value
:
string
;
}
export
type
TokenHoldersPagination
=
{
items_count
:
number
;
value
:
string
;
}
ui/address/AddressInternalTxs.tsx
View file @
c3953467
...
...
@@ -2,7 +2,6 @@ import { Text, Show, Hide } from '@chakra-ui/react';
import
castArray
from
'
lodash/castArray
'
;
import
{
useRouter
}
from
'
next/router
'
;
import
React
from
'
react
'
;
import
{
Element
}
from
'
react-scroll
'
;
import
type
{
AddressFromToFilter
}
from
'
types/api/address
'
;
import
{
AddressFromToFilterValues
}
from
'
types/api/address
'
;
...
...
@@ -21,12 +20,9 @@ import Pagination from 'ui/shared/Pagination';
import
AddressTxsFilter
from
'
./AddressTxsFilter
'
;
import
AddressIntTxsList
from
'
./internals/AddressIntTxsList
'
;
const
SCROLL_ELEM
=
'
address-internas-txs
'
;
const
SCROLL_OFFSET
=
-
100
;
const
getFilterValue
=
(
getFilterValueFromQuery
<
AddressFromToFilter
>
).
bind
(
null
,
AddressFromToFilterValues
);
const
AddressInternalTxs
=
()
=>
{
const
AddressInternalTxs
=
(
{
scrollRef
}:
{
scrollRef
?:
React
.
RefObject
<
HTMLDivElement
>
}
)
=>
{
const
router
=
useRouter
();
const
[
filterValue
,
setFilterValue
]
=
React
.
useState
<
AddressFromToFilter
>
(
getFilterValue
(
router
.
query
.
filter
));
...
...
@@ -38,7 +34,7 @@ const AddressInternalTxs = () => {
resourceName
:
'
address_internal_txs
'
,
pathParams
:
{
id
:
queryIdStr
},
filters
:
{
filter
:
filterValue
},
scroll
:
{
elem
:
SCROLL_ELEM
,
offset
:
SCROLL_OFFSET
}
,
scroll
Ref
,
});
const
handleFilterChange
=
React
.
useCallback
((
val
:
string
|
Array
<
string
>
)
=>
{
...
...
@@ -83,7 +79,7 @@ const AddressInternalTxs = () => {
}
return
(
<
Element
name=
{
SCROLL_ELEM
}
>
<>
<
ActionBar
mt=
{
-
6
}
>
<
AddressTxsFilter
defaultFilter=
{
filterValue
}
...
...
@@ -93,7 +89,7 @@ const AddressInternalTxs = () => {
{
isPaginationVisible
&&
<
Pagination
ml=
"auto"
{
...
pagination
}
/>
}
</
ActionBar
>
{
content
}
</
Element
>
</>
);
};
...
...
ui/address/AddressLogs.tsx
View file @
c3953467
import
{
Box
}
from
'
@chakra-ui/react
'
;
import
{
useRouter
}
from
'
next/router
'
;
import
React
from
'
react
'
;
import
{
Element
}
from
'
react-scroll
'
;
import
useQueryWithPages
from
'
lib/hooks/useQueryWithPages
'
;
import
ActionBar
from
'
ui/shared/ActionBar
'
;
...
...
@@ -10,19 +9,14 @@ import LogItem from 'ui/shared/logs/LogItem';
import
LogSkeleton
from
'
ui/shared/logs/LogSkeleton
'
;
import
Pagination
from
'
ui/shared/Pagination
'
;
const
SCROLL_PARAMS
=
{
elem
:
'
address-logs
'
,
offset
:
-
100
,
};
const
AddressLogs
=
()
=>
{
const
AddressLogs
=
({
scrollRef
}:
{
scrollRef
?:
React
.
RefObject
<
HTMLDivElement
>
})
=>
{
const
router
=
useRouter
();
const
addressHash
=
String
(
router
.
query
?.
id
);
const
{
data
,
isLoading
,
isError
,
pagination
,
isPaginationVisible
}
=
useQueryWithPages
({
resourceName
:
'
address_logs
'
,
pathParams
:
{
id
:
addressHash
},
scroll
:
SCROLL_PARAMS
,
scroll
Ref
,
});
if
(
isError
)
{
...
...
@@ -50,10 +44,10 @@ const AddressLogs = () => {
}
return
(
<
Element
name=
{
SCROLL_PARAMS
.
elem
}
>
<>
{
bar
}
{
data
.
items
.
map
((
item
,
index
)
=>
<
LogItem
key=
{
index
}
{
...
item
}
type=
"address"
/>)
}
</
Element
>
</>
);
};
...
...
ui/address/AddressTokenTransfers.tsx
View file @
c3953467
...
...
@@ -3,7 +3,7 @@ import React from 'react';
import
TokenTransfer
from
'
ui/shared/TokenTransfer/TokenTransfer
'
;
const
AddressTokenTransfers
=
()
=>
{
const
AddressTokenTransfers
=
(
{
scrollRef
}:
{
scrollRef
?:
React
.
RefObject
<
HTMLDivElement
>
}
)
=>
{
const
router
=
useRouter
();
const
hash
=
router
.
query
.
id
;
...
...
@@ -13,6 +13,7 @@ const AddressTokenTransfers = () => {
pathParams=
{
{
id
:
hash
?.
toString
()
}
}
baseAddress=
{
typeof
hash
===
'
string
'
?
hash
:
undefined
}
enableTimeIncrement
scrollRef=
{
scrollRef
}
/>
);
};
...
...
ui/address/AddressTxs.tsx
View file @
c3953467
import
castArray
from
'
lodash/castArray
'
;
import
{
useRouter
}
from
'
next/router
'
;
import
React
from
'
react
'
;
import
{
Element
}
from
'
react-scroll
'
;
import
type
{
AddressFromToFilter
}
from
'
types/api/address
'
;
import
{
AddressFromToFilterValues
}
from
'
types/api/address
'
;
...
...
@@ -17,10 +16,7 @@ import AddressTxsFilter from './AddressTxsFilter';
const
getFilterValue
=
(
getFilterValueFromQuery
<
AddressFromToFilter
>
).
bind
(
null
,
AddressFromToFilterValues
);
const
SCROLL_ELEM
=
'
address-txs
'
;
const
SCROLL_OFFSET
=
-
100
;
const
AddressTxs
=
()
=>
{
const
AddressTxs
=
({
scrollRef
}:
{
scrollRef
?:
React
.
RefObject
<
HTMLDivElement
>
})
=>
{
const
router
=
useRouter
();
const
isMobile
=
useIsMobile
();
...
...
@@ -31,7 +27,7 @@ const AddressTxs = () => {
resourceName
:
'
address_txs
'
,
pathParams
:
{
id
:
castArray
(
router
.
query
.
id
)[
0
]
},
filters
:
{
filter
:
filterValue
},
scroll
:
{
elem
:
SCROLL_ELEM
,
offset
:
SCROLL_OFFSET
}
,
scroll
Ref
,
});
const
handleFilterChange
=
React
.
useCallback
((
val
:
string
|
Array
<
string
>
)
=>
{
...
...
@@ -50,7 +46,7 @@ const AddressTxs = () => {
);
return
(
<
Element
name=
{
SCROLL_ELEM
}
>
<>
{
!
isMobile
&&
(
<
ActionBar
mt=
{
-
6
}
>
{
filter
}
...
...
@@ -64,7 +60,7 @@ const AddressTxs = () => {
currentAddress=
{
typeof
router
.
query
.
id
===
'
string
'
?
router
.
query
.
id
:
undefined
}
enableTimeIncrement
/>
</
Element
>
</>
);
};
...
...
ui/pages/Address.tsx
View file @
c3953467
import
{
Flex
,
Skeleton
,
Tag
}
from
'
@chakra-ui/react
'
;
import
{
Flex
,
Skeleton
,
Tag
,
Box
}
from
'
@chakra-ui/react
'
;
import
{
useRouter
}
from
'
next/router
'
;
import
React
from
'
react
'
;
...
...
@@ -33,6 +33,8 @@ const CONTRACT_TABS = [
const
AddressPageContent
=
()
=>
{
const
router
=
useRouter
();
const
tabsScrollRef
=
React
.
useRef
<
HTMLDivElement
>
(
null
);
const
addressQuery
=
useApiQuery
(
'
address
'
,
{
pathParams
:
{
id
:
router
.
query
.
id
?.
toString
()
},
queryOptions
:
{
enabled
:
Boolean
(
router
.
query
.
id
)
},
...
...
@@ -48,15 +50,15 @@ const AddressPageContent = () => {
const
tabs
:
Array
<
RoutedTab
>
=
React
.
useMemo
(()
=>
{
return
[
{
id
:
'
txs
'
,
title
:
'
Transactions
'
,
component
:
<
AddressTxs
/>
},
{
id
:
'
token_transfers
'
,
title
:
'
Token transfers
'
,
component
:
<
AddressTokenTransfers
/>
},
{
id
:
'
txs
'
,
title
:
'
Transactions
'
,
component
:
<
AddressTxs
scrollRef=
{
tabsScrollRef
}
/>
},
{
id
:
'
token_transfers
'
,
title
:
'
Token transfers
'
,
component
:
<
AddressTokenTransfers
scrollRef=
{
tabsScrollRef
}
/>
},
{
id
:
'
tokens
'
,
title
:
'
Tokens
'
,
component
:
null
},
{
id
:
'
internal_txns
'
,
title
:
'
Internal txns
'
,
component
:
<
AddressInternalTxs
/>
},
{
id
:
'
internal_txns
'
,
title
:
'
Internal txns
'
,
component
:
<
AddressInternalTxs
scrollRef=
{
tabsScrollRef
}
/>
},
{
id
:
'
coin_balance_history
'
,
title
:
'
Coin balance history
'
,
component
:
<
AddressCoinBalance
/>
},
// temporary show this tab in all address
// later api will return info about available tabs
{
id
:
'
blocks_validated
'
,
title
:
'
Blocks validated
'
,
component
:
<
AddressBlocksValidated
/>
},
isContract
?
{
id
:
'
logs
'
,
title
:
'
Logs
'
,
component
:
<
AddressLogs
/>
}
:
undefined
,
isContract
?
{
id
:
'
logs
'
,
title
:
'
Logs
'
,
component
:
<
AddressLogs
scrollRef=
{
tabsScrollRef
}
/>
}
:
undefined
,
isContract
?
{
id
:
'
contract
'
,
title
:
'
Contract
'
,
...
...
@@ -80,6 +82,8 @@ const AddressPageContent = () => {
/>
)
}
<
AddressDetails
addressQuery=
{
addressQuery
}
/>
{
/* should stay before tabs to scroll up whith pagination */
}
<
Box
ref=
{
tabsScrollRef
}
></
Box
>
{
addressQuery
.
isLoading
?
<
SkeletonTabs
/>
:
<
RoutedTabs
tabs=
{
tabs
}
tabListProps=
{
{
mt
:
8
}
}
/>
}
</
Page
>
);
...
...
ui/pages/Token.tsx
View file @
c3953467
import
{
Skeleton
}
from
'
@chakra-ui/react
'
;
import
{
Skeleton
,
Box
}
from
'
@chakra-ui/react
'
;
import
{
useRouter
}
from
'
next/router
'
;
import
React
from
'
react
'
;
import
{
Element
}
from
'
react-scroll
'
;
import
type
{
RoutedTab
}
from
'
ui/shared/RoutedTabs/types
'
;
import
useApiQuery
from
'
lib/api/useApiQuery
'
;
import
useIsMobile
from
'
lib/hooks/useIsMobile
'
;
import
useQueryWithPages
from
'
lib/hooks/useQueryWithPages
'
;
import
Page
from
'
ui/shared/Page/Page
'
;
import
PageTitle
from
'
ui/shared/Page/PageTitle
'
;
import
type
{
Props
as
PaginationProps
}
from
'
ui/shared/Pagination
'
;
import
Pagination
from
'
ui/shared/Pagination
'
;
import
RoutedTabs
from
'
ui/shared/RoutedTabs/RoutedTabs
'
;
import
TokenContractInfo
from
'
ui/token/TokenContractInfo
'
;
import
TokenDetails
from
'
ui/token/TokenDetails
'
;
import
TokenHolders
from
'
ui/token/TokenHolders/TokenHolders
'
;
export
type
TokenTabs
=
'
token_transfers
'
|
'
holders
'
const
TokenPageContent
=
()
=>
{
const
router
=
useRouter
();
const
isMobile
=
useIsMobile
();
const
scrollRef
=
React
.
useRef
<
HTMLDivElement
>
(
null
);
const
tokenQuery
=
useApiQuery
(
'
token
'
,
{
pathParams
:
{
hash
:
router
.
query
.
hash
?.
toString
()
},
queryOptions
:
{
enabled
:
Boolean
(
router
.
query
.
hash
)
},
});
// const transfersQuery = useQueryWithPages({
// resourceName: 'token_transfers',
// pathParams: { hash: router.query.hash?.toString() },
// options: {
// enabled: Boolean(router.query.hash && router.query.tab === 'holders' && tokenQuery.data),
// },
// });
const
holdersQuery
=
useQueryWithPages
({
resourceName
:
'
token_holders
'
,
pathParams
:
{
hash
:
router
.
query
.
hash
?.
toString
()
},
scrollRef
,
options
:
{
enabled
:
Boolean
(
router
.
query
.
hash
&&
router
.
query
.
tab
===
'
holders
'
&&
tokenQuery
.
data
),
},
});
const
tabs
:
Array
<
RoutedTab
>
=
[
{
id
:
'
token_transfers
'
,
title
:
'
Token transfers
'
,
component
:
null
},
{
id
:
'
holders
'
,
title
:
'
Holders
'
,
component
:
null
},
{
id
:
'
holders
'
,
title
:
'
Holders
'
,
component
:
<
TokenHolders
tokenQuery=
{
tokenQuery
}
holdersQuery=
{
holdersQuery
}
/>
},
];
let
hasPagination
;
let
pagination
;
// if (router.query.tab === 'token_transfers') {
// hasPagination = transfersQuery.isPaginationVisible;
// pagination = transfersQuery.pagination;
// }
if
(
router
.
query
.
tab
===
'
holders
'
)
{
hasPagination
=
holdersQuery
.
isPaginationVisible
;
pagination
=
holdersQuery
.
pagination
;
}
return
(
<
Page
>
{
tokenQuery
.
isLoading
?
...
...
@@ -34,7 +69,16 @@ const TokenPageContent = () => {
<
PageTitle
text=
{
`${ tokenQuery.data?.name } (${ tokenQuery.data?.symbol }) token`
}
/>
}
<
TokenContractInfo
tokenQuery=
{
tokenQuery
}
/>
<
TokenDetails
tokenQuery=
{
tokenQuery
}
/>
<
Element
name=
"token-tabs"
><
RoutedTabs
tabs=
{
tabs
}
tabListProps=
{
{
mt
:
8
}
}
/></
Element
>
{
/* should stay before tabs to scroll up whith pagination */
}
<
Box
ref=
{
scrollRef
}
></
Box
>
<
RoutedTabs
tabs=
{
tabs
}
tabListProps=
{
isMobile
?
{
mt
:
8
}
:
{
mt
:
3
,
py
:
5
,
marginBottom
:
0
}
}
rightSlot=
{
!
isMobile
&&
hasPagination
?
<
Pagination
{
...
(
pagination
as
PaginationProps
)
}
/>
:
null
}
stickyEnabled=
{
!
isMobile
}
/>
</
Page
>
);
};
...
...
ui/shared/TokenTransfer/TokenTransfer.tsx
View file @
c3953467
import
{
Hide
,
Show
,
Text
}
from
'
@chakra-ui/react
'
;
import
{
useRouter
}
from
'
next/router
'
;
import
React
from
'
react
'
;
import
{
Element
}
from
'
react-scroll
'
;
import
type
{
AddressFromToFilter
}
from
'
types/api/address
'
;
import
{
AddressFromToFilterValues
}
from
'
types/api/address
'
;
...
...
@@ -28,9 +27,6 @@ import { TOKEN_TYPE } from './helpers';
const
TOKEN_TYPES
=
TOKEN_TYPE
.
map
(
i
=>
i
.
id
);
const
SCROLL_ELEM
=
'
token-transfers
'
;
const
SCROLL_OFFSET
=
-
100
;
const
getTokenFilterValue
=
(
getFilterValuesFromQuery
<
TokenType
>
).
bind
(
null
,
TOKEN_TYPES
);
const
getAddressFilterValue
=
(
getFilterValueFromQuery
<
AddressFromToFilter
>
).
bind
(
null
,
AddressFromToFilterValues
);
...
...
@@ -43,6 +39,7 @@ interface Props<Resource extends 'tx_token_transfers' | 'address_token_transfers
txHash
?:
string
;
enableTimeIncrement
?:
boolean
;
pathParams
?:
UseApiQueryParams
<
Resource
>
[
'
pathParams
'
];
scrollRef
?:
React
.
RefObject
<
HTMLDivElement
>
;
}
type
State
=
{
...
...
@@ -58,6 +55,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr
showTxInfo
=
true
,
enableTimeIncrement
,
pathParams
,
scrollRef
,
}
: Props
<
Resource
>
) =
>
{
const
router
=
useRouter
();
const
[
filters
,
setFilters
]
=
React
.
useState
<
State
>
(
...
...
@@ -69,7 +67,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr
pathParams
,
options
:
{
enabled
:
!
isDisabled
},
filters
:
filters
as
PaginationFilters
<
Resource
>
,
scroll
:
{
elem
:
SCROLL_ELEM
,
offset
:
SCROLL_OFFSET
}
,
scroll
Ref
,
});
const
handleTypeFilterChange
=
React
.
useCallback
((
nextValue
:
Array
<
TokenType
>
)
=>
{
...
...
@@ -129,7 +127,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr
})();
return
(
<
Element
name=
{
SCROLL_ELEM
}
>
<>
{
!
isActionBarHidden
&&
(
<
ActionBar
mt=
{
-
6
}
>
<
TokenTransferFilter
...
...
@@ -144,7 +142,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr
</
ActionBar
>
)
}
{
content
}
</
Element
>
</>
);
}
;
...
...
ui/token/TokenHolders/TokenHolders.tsx
0 → 100644
View file @
c3953467
import
{
Text
}
from
'
@chakra-ui/react
'
;
import
type
{
UseQueryResult
}
from
'
@tanstack/react-query
'
;
import
React
from
'
react
'
;
import
type
{
TokenHolders
,
TokenInfo
}
from
'
types/api/tokenInfo
'
;
import
useIsMobile
from
'
lib/hooks/useIsMobile
'
;
import
ActionBar
from
'
ui/shared/ActionBar
'
;
import
DataFetchAlert
from
'
ui/shared/DataFetchAlert
'
;
import
Pagination
from
'
ui/shared/Pagination
'
;
import
type
{
Props
as
PaginationProps
}
from
'
ui/shared/Pagination
'
;
import
SkeletonList
from
'
ui/shared/skeletons/SkeletonList
'
;
import
SkeletonTable
from
'
ui/shared/skeletons/SkeletonTable
'
;
import
TokenHoldersList
from
'
./TokenHoldersList
'
;
import
TokenHoldersTable
from
'
./TokenHoldersTable
'
;
type
Props
=
{
tokenQuery
:
UseQueryResult
<
TokenInfo
>
;
holdersQuery
:
UseQueryResult
<
TokenHolders
>
&
{
pagination
:
PaginationProps
;
isPaginationVisible
:
boolean
;
};
}
const
TokenHoldersContent
=
({
holdersQuery
,
tokenQuery
}:
Props
)
=>
{
const
isMobile
=
useIsMobile
();
if
(
holdersQuery
.
isError
||
tokenQuery
.
isError
)
{
return
<
DataFetchAlert
/>;
}
const
bar
=
isMobile
&&
holdersQuery
.
isPaginationVisible
&&
(
<
ActionBar
mt=
{
-
6
}
>
<
Pagination
ml=
"auto"
{
...
holdersQuery
.
pagination
}
/>
</
ActionBar
>
);
if
(
holdersQuery
.
isLoading
||
tokenQuery
.
isLoading
)
{
return
(
<>
{
bar
}
{
isMobile
&&
<
SkeletonList
/>
}
{
!
isMobile
&&
(
<
SkeletonTable
columns=
{
[
'
100%
'
,
'
300px
'
,
'
175px
'
]
}
/>
)
}
</>
);
}
const
items
=
holdersQuery
.
data
.
items
;
if
(
!
items
?.
length
)
{
return
<
Text
as=
"span"
>
There are no holders for this token.
</
Text
>;
}
return
(
<>
{
bar
}
{
!
isMobile
&&
<
TokenHoldersTable
data=
{
items
}
token=
{
tokenQuery
.
data
}
/>
}
{
isMobile
&&
<
TokenHoldersList
data=
{
items
}
token=
{
tokenQuery
.
data
}
/>
}
</>
);
};
export
default
TokenHoldersContent
;
ui/token/TokenHolders/TokenHoldersList.pw.tsx
0 → 100644
View file @
c3953467
import
{
test
,
expect
,
devices
}
from
'
@playwright/experimental-ct-react
'
;
import
React
from
'
react
'
;
import
{
tokenHolders
}
from
'
mocks/tokens/tokenHolders
'
;
import
{
tokenInfo
}
from
'
mocks/tokens/tokenInfo
'
;
import
TestApp
from
'
playwright/TestApp
'
;
import
TokenHoldersList
from
'
./TokenHoldersList
'
;
test
.
use
({
viewport
:
devices
[
'
iPhone 13 Pro
'
].
viewport
});
test
(
'
base view
'
,
async
({
mount
})
=>
{
const
component
=
await
mount
(
<
TestApp
>
<
TokenHoldersList
data=
{
tokenHolders
.
items
}
token=
{
tokenInfo
}
/>
</
TestApp
>,
);
await
expect
(
component
).
toHaveScreenshot
();
});
ui/token/TokenHolders/TokenHoldersList.tsx
0 → 100644
View file @
c3953467
import
{
Box
}
from
'
@chakra-ui/react
'
;
import
React
from
'
react
'
;
import
type
{
TokenHolder
,
TokenInfo
}
from
'
types/api/tokenInfo
'
;
import
TokenHoldersListItem
from
'
./TokenHoldersListItem
'
;
interface
Props
{
data
:
Array
<
TokenHolder
>
;
token
:
TokenInfo
;
}
const
TokenHoldersList
=
({
data
,
token
}:
Props
)
=>
{
return
(
<
Box
>
{
data
.
map
((
item
)
=>
(
<
TokenHoldersListItem
key=
{
item
.
address
.
hash
}
token=
{
token
}
holder=
{
item
}
/>
))
}
</
Box
>
);
};
export
default
TokenHoldersList
;
ui/token/TokenHolders/TokenHoldersListItem.tsx
0 → 100644
View file @
c3953467
import
{
Flex
}
from
'
@chakra-ui/react
'
;
import
BigNumber
from
'
bignumber.js
'
;
import
React
from
'
react
'
;
import
type
{
TokenHolder
,
TokenInfo
}
from
'
types/api/tokenInfo
'
;
import
Address
from
'
ui/shared/address/Address
'
;
import
AddressIcon
from
'
ui/shared/address/AddressIcon
'
;
import
AddressLink
from
'
ui/shared/address/AddressLink
'
;
import
ListItemMobile
from
'
ui/shared/ListItemMobile
'
;
import
Utilization
from
'
ui/shared/Utilization/Utilization
'
;
interface
Props
{
holder
:
TokenHolder
;
token
:
TokenInfo
;
}
const
TokenHoldersListItem
=
({
holder
,
token
}:
Props
)
=>
{
const
quantity
=
BigNumber
(
holder
.
value
).
div
(
BigNumber
(
10
**
Number
(
token
.
decimals
))).
dp
(
6
).
toFormat
();
return
(
<
ListItemMobile
rowGap=
{
3
}
>
<
Address
display=
"inline-flex"
maxW=
"100%"
lineHeight=
"30px"
>
<
AddressIcon
address=
{
holder
.
address
}
/>
<
AddressLink
ml=
{
2
}
fontWeight=
"700"
hash=
{
holder
.
address
.
hash
}
alias=
{
holder
.
address
.
name
}
flexGrow=
{
1
}
/>
</
Address
>
<
Flex
justifyContent=
"space-between"
alignItems=
"center"
width=
"100%"
>
{
quantity
}
{
token
.
total_supply
&&
(
<
Utilization
value=
{
BigNumber
(
holder
.
value
).
div
(
BigNumber
(
token
.
total_supply
)).
dp
(
4
).
toNumber
()
}
colorScheme=
"green"
ml=
{
6
}
/>
)
}
</
Flex
>
</
ListItemMobile
>
);
};
export
default
TokenHoldersListItem
;
ui/token/TokenHolders/TokenHoldersTable.pw.tsx
0 → 100644
View file @
c3953467
import
{
Box
}
from
'
@chakra-ui/react
'
;
import
{
test
,
expect
}
from
'
@playwright/experimental-ct-react
'
;
import
React
from
'
react
'
;
import
{
tokenHolders
}
from
'
mocks/tokens/tokenHolders
'
;
import
{
tokenInfo
}
from
'
mocks/tokens/tokenInfo
'
;
import
TestApp
from
'
playwright/TestApp
'
;
import
TokenHoldersTable
from
'
./TokenHoldersTable
'
;
test
(
'
base view
'
,
async
({
mount
})
=>
{
const
component
=
await
mount
(
<
TestApp
>
<
Box
h=
"128px"
/>
<
TokenHoldersTable
data=
{
tokenHolders
.
items
}
token=
{
tokenInfo
}
/>
</
TestApp
>,
);
await
expect
(
component
).
toHaveScreenshot
();
});
ui/token/TokenHolders/TokenHoldersTable.tsx
0 → 100644
View file @
c3953467
import
{
Table
,
Tbody
,
Tr
,
Th
}
from
'
@chakra-ui/react
'
;
import
React
from
'
react
'
;
import
type
{
TokenHolder
,
TokenInfo
}
from
'
types/api/tokenInfo
'
;
import
{
default
as
Thead
}
from
'
ui/shared/TheadSticky
'
;
import
TokenHoldersTableItem
from
'
ui/token/TokenHolders/TokenHoldersTableItem
'
;
interface
Props
{
data
:
Array
<
TokenHolder
>
;
token
:
TokenInfo
;
}
const
TokenHoldersTable
=
({
data
,
token
}:
Props
)
=>
{
return
(
<
Table
variant=
"simple"
size=
"sm"
>
<
Thead
top=
{
80
}
>
<
Tr
>
<
Th
>
Holder
</
Th
>
<
Th
isNumeric
width=
"300px"
>
Quantity
</
Th
>
{
token
.
total_supply
&&
<
Th
isNumeric
width=
"175px"
>
Percentage
</
Th
>
}
</
Tr
>
</
Thead
>
<
Tbody
>
{
data
.
map
((
item
)
=>
(
<
TokenHoldersTableItem
key=
{
item
.
address
.
hash
}
holder=
{
item
}
token=
{
token
}
/>
))
}
</
Tbody
>
</
Table
>
);
};
export
default
React
.
memo
(
TokenHoldersTable
);
ui/token/TokenHolders/TokenHoldersTableItem.tsx
0 → 100644
View file @
c3953467
import
{
Tr
,
Td
}
from
'
@chakra-ui/react
'
;
import
BigNumber
from
'
bignumber.js
'
;
import
React
from
'
react
'
;
import
type
{
TokenHolder
,
TokenInfo
}
from
'
types/api/tokenInfo
'
;
import
Address
from
'
ui/shared/address/Address
'
;
import
AddressIcon
from
'
ui/shared/address/AddressIcon
'
;
import
AddressLink
from
'
ui/shared/address/AddressLink
'
;
import
Utilization
from
'
ui/shared/Utilization/Utilization
'
;
type
Props
=
{
holder
:
TokenHolder
;
token
:
TokenInfo
;
}
const
TokenTransferTableItem
=
({
holder
,
token
}:
Props
)
=>
{
const
quantity
=
BigNumber
(
holder
.
value
).
div
(
BigNumber
(
10
**
Number
(
token
.
decimals
))).
toFormat
();
return
(
<
Tr
>
<
Td
>
<
Address
display=
"inline-flex"
maxW=
"100%"
lineHeight=
"30px"
>
<
AddressIcon
address=
{
holder
.
address
}
/>
<
AddressLink
ml=
{
2
}
fontWeight=
"700"
hash=
{
holder
.
address
.
hash
}
alias=
{
holder
.
address
.
name
}
flexGrow=
{
1
}
/>
</
Address
>
</
Td
>
<
Td
isNumeric
>
{
quantity
}
</
Td
>
{
token
.
total_supply
&&
(
<
Td
isNumeric
>
<
Utilization
value=
{
BigNumber
(
holder
.
value
).
div
(
BigNumber
(
token
.
total_supply
)).
dp
(
4
).
toNumber
()
}
colorScheme=
"green"
display=
"inline-flex"
/>
</
Td
>
)
}
</
Tr
>
);
};
export
default
React
.
memo
(
TokenTransferTableItem
);
ui/token/TokenHolders/__screenshots__/TokenHoldersList.pw.tsx_default_base-view-1.png
0 → 100644
View file @
c3953467
14.1 KB
ui/token/TokenHolders/__screenshots__/TokenHoldersTable.pw.tsx_default_base-view-1.png
0 → 100644
View file @
c3953467
18.4 KB
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment