diff --git a/explorer/client/package.json b/explorer/client/package.json index 6a2212f5a7cb4..dac7c7e4452c9 100644 --- a/explorer/client/package.json +++ b/explorer/client/package.json @@ -30,15 +30,16 @@ }, "dependencies": { "@mysten/sui.js": "file:../../sdk/typescript", + "@tanstack/react-table": "^8.1.4", + "bn.js": "^5.2.0", "classnames": "^2.3.1", "react": "^17.0.2", "react-dom": "^17.0.2", "react-ga4": "^1.4.1", + "react-json-view": "^1.21.3", "react-router-dom": "^6.2.1", "vanilla-cookieconsent": "^2.8.0", - "web-vitals": "^2.1.4", - "react-json-view": "^1.21.3", - "bn.js": "^5.2.0" + "web-vitals": "^2.1.4" }, "scripts": { "start": "react-scripts start", diff --git a/explorer/client/src/assets/SVGIcons/failed.svg b/explorer/client/src/assets/SVGIcons/failed.svg new file mode 100644 index 0000000000000..2cf419b195063 --- /dev/null +++ b/explorer/client/src/assets/SVGIcons/failed.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/client/src/assets/SVGIcons/forward-arrow-dark.svg b/explorer/client/src/assets/SVGIcons/forward-arrow-dark.svg new file mode 100644 index 0000000000000..0984a54e4a39f --- /dev/null +++ b/explorer/client/src/assets/SVGIcons/forward-arrow-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/client/src/assets/SVGIcons/forward-arrow.svg b/explorer/client/src/assets/SVGIcons/forward-arrow.svg new file mode 100644 index 0000000000000..c2d74d6356e82 --- /dev/null +++ b/explorer/client/src/assets/SVGIcons/forward-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/client/src/assets/SVGIcons/success.svg b/explorer/client/src/assets/SVGIcons/success.svg new file mode 100644 index 0000000000000..4a13ab78f314b --- /dev/null +++ b/explorer/client/src/assets/SVGIcons/success.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer/client/src/components/longtext/Longtext.tsx b/explorer/client/src/components/longtext/Longtext.tsx index 1e538f34330f3..349d0b6d7e287 100644 --- a/explorer/client/src/components/longtext/Longtext.tsx +++ b/explorer/client/src/components/longtext/Longtext.tsx @@ -4,6 +4,7 @@ import { useCallback, useState, useContext } from 'react'; import { Link, useNavigate } from 'react-router-dom'; +import { ReactComponent as ContentForwardArrowDark } from '../../assets/SVGIcons/forward-arrow-dark.svg'; import { ReactComponent as ContentCopyIcon } from '../../assets/content_copy_black_18dp.svg'; import { NetworkContext } from '../../context'; import { navigateWithUnknown } from '../../utils/searchUtil'; @@ -17,6 +18,7 @@ function Longtext({ isLink = true, alttext = '', isCopyButton = true, + showIconButton = false, }: { text: string; category: @@ -28,6 +30,7 @@ function Longtext({ isLink?: boolean; alttext?: string; isCopyButton?: boolean; + showIconButton?: boolean; }) { const [isCopyIcon, setCopyIcon] = useState(true); @@ -42,6 +45,7 @@ function Longtext({ }, [setCopyIcon, text]); let icon; + let iconButton = <>; if (isCopyButton) { if (pleaseWait) { @@ -59,6 +63,10 @@ function Longtext({ icon = <>; } + if (showIconButton) { + iconButton = ; + } + const navigateUnknown = useCallback(() => { setPleaseWait(true); navigateWithUnknown(text, navigate, network).then(() => @@ -97,7 +105,7 @@ function Longtext({ className={styles.longtext} to={`/${category}/${encodeURIComponent(text)}`} > - {alttext ? alttext : text} + {alttext ? alttext : text} {iconButton} ); } diff --git a/explorer/client/src/components/network-stats/SuiNetworkStats.module.css b/explorer/client/src/components/network-stats/SuiNetworkStats.module.css new file mode 100644 index 0000000000000..73d2d206dbfae --- /dev/null +++ b/explorer/client/src/components/network-stats/SuiNetworkStats.module.css @@ -0,0 +1,11 @@ +.networkstats { + @apply bg-[#F7F8F8] p-5 font-[500] text-[#767A81] text-[13px] leading-4 uppercase grid md:grid-cols-2 grid-cols-1 gap-10; +} + +.statsitem { + @apply flex justify-between items-center leading-6 text-left; +} + +.stats { + @apply text-black text-[16px]; +} diff --git a/explorer/client/src/components/network-stats/SuiNetworkStats.tsx b/explorer/client/src/components/network-stats/SuiNetworkStats.tsx new file mode 100644 index 0000000000000..2030001f13cc1 --- /dev/null +++ b/explorer/client/src/components/network-stats/SuiNetworkStats.tsx @@ -0,0 +1,77 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { IS_STATIC_ENV } from '../../utils/envUtil'; + +import styles from './SuiNetworkStats.module.css'; + +//TODO add the backend service to get all Network stats data +function SuiNetworkCard({ count }: { count: number | string }) { + const totalStatsData = [ + { + title: 'TOTAL Objects', + value: '372.5M', + }, + { + title: 'TOTAL MODULES', + value: '153,510', + }, + { + title: 'TOTAL BYTES STORED', + value: '2.591B', + }, + { + title: 'TOTAL TRANSACTIONS', + value: count, + }, + ]; + + const currentStatsData = [ + { + title: 'CURRENT SUI PRICE', + value: '$26.45', + }, + { + title: 'Current Epoch', + value: '142,215', + }, + { + title: 'CURRENT VALIDATORS', + value: '15,482', + }, + { + title: 'CURRENT TPS', + value: '2,125', + }, + ]; + + return ( +
+
+ {totalStatsData.map((item, idx) => ( +
+ {item.title} + {item.value} +
+ ))} +
+
+ {currentStatsData.map((item, idx) => ( +
+ {item.title} + {item.value} +
+ ))} +
+
+ ); +} + +function SuiNetworkCardStatic() { + return ; +} + +const SuiNetworkStats = ({ count }: { count: number }) => + IS_STATIC_ENV ? : ; + +export default SuiNetworkStats; diff --git a/explorer/client/src/components/table/TableCard.module.css b/explorer/client/src/components/table/TableCard.module.css new file mode 100644 index 0000000000000..31e52a038da58 --- /dev/null +++ b/explorer/client/src/components/table/TableCard.module.css @@ -0,0 +1,33 @@ +.content { + @apply container overflow-x-auto text-left text-[#4E555D]; +} + +.table { + @apply w-full text-sm text-left text-[#4E555D] overflow-x-auto min-w-max; + + border-bottom: 1px solid #f0f1f2; +} + +.table th { + @apply text-left font-normal uppercase text-[#767A81] text-xs; +} + +.table td { + @apply pr-5 text-[#4E555D]; +} + +.table tbody tr { + @apply space-y-10 mt-10; +} + +.addresses { + @apply flex flex-wrap; +} + +.addresses svg { + @apply mr-1; +} + +.tablespacing { + @apply pb-1 pt-1; +} diff --git a/explorer/client/src/components/table/TableCard.tsx b/explorer/client/src/components/table/TableCard.tsx new file mode 100644 index 0000000000000..2ab1eccc31e1b --- /dev/null +++ b/explorer/client/src/components/table/TableCard.tsx @@ -0,0 +1,183 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +import { + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { useMemo } from 'react'; + +import { ReactComponent as ContentFailedStatus } from '../../assets/SVGIcons/failed.svg'; +import { ReactComponent as ContentForwardArrow } from '../../assets/SVGIcons/forward-arrow.svg'; +import { ReactComponent as ContentSuccessStatus } from '../../assets/SVGIcons/success.svg'; +import Longtext from '../../components/longtext/Longtext'; + +import type { ExecutionStatusType, TransactionKindName } from '@mysten/sui.js'; + +import styles from './TableCard.module.css'; + +type Category = + | 'objects' + | 'transactions' + | 'addresses' + | 'ethAddress' + | 'unknown'; + +type Link = { + url: string; + name?: string; + copy?: boolean; + category?: string; + isLink?: boolean; +}; + +type TableColumn = { + headerLabel: string; + accessorKey: string; +}; +// TODO: update Link to use Tuple type +// type Links = [Link, Link?]; +type Links = Link[]; + +type TxStatus = { + txTypeName: TransactionKindName | undefined; + status: ExecutionStatusType; +}; + +// support multiple types with special handling for 'addresses'/links and status +// TODO: Not sure to allow HTML elements in the table +type TxType = { + [key: string]: + | string + | number + | boolean + | Links + | React.ReactElement + | TxStatus; +}; + +function TxAddresses({ content }: { content: Link[] }) { + return ( +
+ {content.map((itm, idx) => ( +
+ + {idx !== content.length - 1 && } +
+ ))} +
+ ); +} + +function TxStatusType({ content }: { content: TxStatus }) { + return ( + <> + {content.status === 'success' ? ( + + ) : ( + + )}{' '} + {content.txTypeName} + + ); +} + +function columnsContent(columns: TableColumn[]) { + return columns.map((column) => ({ + accessorKey: column.accessorKey, + id: column.accessorKey, + header: column.headerLabel, + // cell renderer for each column from react-table + cell: (info: any) => { + const content = info.getValue(); + + // handle multiple links in one cell + if (Array.isArray(content)) { + return ; + } + // Special case for txTypes and status + if ( + typeof content === 'object' && + content !== null && + content.txTypeName + ) { + return ; + } + // handle most common types including + return content; + }, + })); +} + +function TableCard({ + tabledata, +}: { + tabledata: { + data: TxType[]; + columns: TableColumn[]; + }; +}) { + const data = useMemo(() => tabledata.data, [tabledata.data]); + // Use Columns to create a table + const columns = useMemo( + () => columnsContent(tabledata.columns), + [tabledata.columns] + ); + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
+
+ ); +} + +export default TableCard; diff --git a/explorer/client/src/components/tabs/TabFooter.tsx b/explorer/client/src/components/tabs/TabFooter.tsx new file mode 100644 index 0000000000000..3092ed939bd2b --- /dev/null +++ b/explorer/client/src/components/tabs/TabFooter.tsx @@ -0,0 +1,39 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { numberSuffix } from '../../utils/numberUtil'; + +import styles from './Tabs.module.css'; + +//TODO: update this component to use account for multipe formats +// Update this footer now accept React.ReactElement as a child +function TabFooter({ + stats, + children, +}: { + children?: React.ReactElement; + stats?: { + count: number | string; + stats_text: string; + }; +}) { + return ( +
+ {children ? ( + [...(Array.isArray(children) ? children : [children])] + ) : ( + <> + )} + {stats && ( +

+ {typeof stats.count === 'number' + ? numberSuffix(stats.count) + : stats.count}{' '} + {stats.stats_text} +

+ )} +
+ ); +} + +export default TabFooter; diff --git a/explorer/client/src/components/tabs/Tabs.module.css b/explorer/client/src/components/tabs/Tabs.module.css new file mode 100644 index 0000000000000..56055c76f317d --- /dev/null +++ b/explorer/client/src/components/tabs/Tabs.module.css @@ -0,0 +1,30 @@ +.tabs { + @apply w-full; +} + +.tablist { + @apply m-0 p-0 md:block mb-3 w-full transition-all duration-200 ease-in-out flex flex-row md:flex-row overflow-x-auto items-center; + + border-bottom: 1px solid #f0f1f2; +} + +.tab { + @apply list-none capitalize font-normal text-sm text-gray-500 font-sans mr-5 inline-block w-auto cursor-pointer pt-1 pb-1 whitespace-nowrap; +} + +.tab-content { + @apply w-full mt-20; +} + +.selected { + border-bottom: 1px solid #767a81; + @apply text-black; +} + +.tabsfooter { + @apply flex justify-between text-xs text-gray-500 capitalize items-center p-1; +} + +.tabsfooter a { + @apply text-gray-700 font-semibold; +} diff --git a/explorer/client/src/components/tabs/Tabs.tsx b/explorer/client/src/components/tabs/Tabs.tsx new file mode 100644 index 0000000000000..69477cfa897a8 --- /dev/null +++ b/explorer/client/src/components/tabs/Tabs.tsx @@ -0,0 +1,51 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +import cl from 'classnames'; +import { useCallback, useState } from 'react'; + +import styles from './Tabs.module.css'; + +type Props = { + children: JSX.Element[] | JSX.Element; + selected?: number; +}; + +function Tabs({ children, selected }: Props) { + const [activeTab, setActivetab] = useState(selected || 0); + const selectActiveTab = useCallback((e: React.MouseEvent) => { + if (e.currentTarget.dataset.activetab) + setActivetab(parseInt(e.currentTarget.dataset.activetab)); + }, []); + return ( +
+
    + {[...(Array.isArray(children) ? children : [children])].map( + (elem, index) => { + return ( +
  • + {elem.props.title} +
  • + ); + } + )} +
+
+ { + [...(Array.isArray(children) ? children : [children])][ + activeTab + ] + } +
+
+ ); +} + +export default Tabs; diff --git a/explorer/client/src/components/top-groups/TopGroups.module.css b/explorer/client/src/components/top-groups/TopGroups.module.css new file mode 100644 index 0000000000000..5cce5fbd93186 --- /dev/null +++ b/explorer/client/src/components/top-groups/TopGroups.module.css @@ -0,0 +1,15 @@ +.validators { + @apply mb-5; +} + +.recenttxfooter { + @apply flex justify-between text-xs text-gray-500 capitalize items-center p-1; +} + +.recenttxfooter a { + @apply text-gray-700 font-semibold; +} + +.stakepercent { + @apply text-[#9C9FA4] text-xs; +} diff --git a/explorer/client/src/components/top-groups/TopGroups.tsx b/explorer/client/src/components/top-groups/TopGroups.tsx new file mode 100644 index 0000000000000..52d61c05ca91c --- /dev/null +++ b/explorer/client/src/components/top-groups/TopGroups.tsx @@ -0,0 +1,140 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import Longtext from '../../components/longtext/Longtext'; +import TableCard from '../../components/table/TableCard'; +import TabFooter from '../../components/tabs/TabFooter'; +import Tabs from '../../components/tabs/Tabs'; +import { numberSuffix } from '../../utils/numberUtil'; + +import styles from './TopGroups.module.css'; + +// TODO: Specify the type of the context +// Specify the type of the context +function TopGroupsCard() { + // mock validators data + const validatorsData = [ + { + name: 'BoredApe', + volume: '3,299', + floorprice: '1,672', + transaction: numberSuffix(17_220_000), + position: 1, + }, + { + name: 'BoredApe', + volume: '3,299', + floorprice: '1,672', + transaction: numberSuffix(17_220_000), + position: 1, + }, + { + name: 'BoredApe', + volume: '3,299', + floorprice: '1,672', + transaction: numberSuffix(17_220_000), + position: 1, + }, + { + name: 'BoredApe', + volume: '3,299', + floorprice: '1,672', + transaction: numberSuffix(17_220_000), + position: 1, + }, + { + name: 'BoredApe', + volume: '3,299', + floorprice: '1,672', + transaction: numberSuffix(17_220_000), + position: 1, + }, + { + name: 'BoredApe', + volume: '3,299', + floorprice: '1,672', + transaction: numberSuffix(17_220_000), + position: 1, + }, + { + name: 'BoredApe', + volume: '3,299', + floorprice: '1,672', + transaction: numberSuffix(17_220_000), + position: 1, + }, + { + name: 'BoredApe', + volume: '3,299', + floorprice: '1,672', + transaction: numberSuffix(17_220_000), + position: 1, + }, + { + name: 'BoredApe', + volume: '3,299', + floorprice: '1,672', + transaction: numberSuffix(17_220_000), + position: 1, + }, + { + name: 'BoredApe', + volume: '3,299', + floorprice: '1,672', + transaction: numberSuffix(17_220_000), + position: 1, + }, + ]; + const mockValidatorsData = { + data: validatorsData, + columns: [ + { + headerLabel: '#', + accessorKey: 'position', + }, + { + headerLabel: 'NAME', + accessorKey: 'name', + }, + { + headerLabel: 'FLOOR PRICE', + accessorKey: 'floorprice', + }, + { + headerLabel: 'TRANSACTIONS', + accessorKey: 'transaction', + }, + ], + }; + const defaultActiveTab = 1; + const tabsFooter = { + stats: { + count: 326, + stats_text: 'Collections', + }, + }; + // Mork data + return ( +
+ +
+
+ + + + +
+
Top Address Component
+
+
+ ); +} + +export default TopGroupsCard; diff --git a/explorer/client/src/components/top-validators-card/TopValidatorsCard.module.css b/explorer/client/src/components/top-validators-card/TopValidatorsCard.module.css new file mode 100644 index 0000000000000..87e97f9a45e84 --- /dev/null +++ b/explorer/client/src/components/top-validators-card/TopValidatorsCard.module.css @@ -0,0 +1,7 @@ +.validators { + @apply mb-5; +} + +.stakepercent { + @apply text-[#9C9FA4] text-xs; +} diff --git a/explorer/client/src/components/top-validators-card/TopValidatorsCard.tsx b/explorer/client/src/components/top-validators-card/TopValidatorsCard.tsx new file mode 100644 index 0000000000000..f57e920ea3a14 --- /dev/null +++ b/explorer/client/src/components/top-validators-card/TopValidatorsCard.tsx @@ -0,0 +1,154 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import Longtext from '../../components/longtext/Longtext'; +import TableCard from '../../components/table/TableCard'; +import TabFooter from '../../components/tabs/TabFooter'; +import Tabs from '../../components/tabs/Tabs'; +import { numberSuffix } from '../../utils/numberUtil'; + +import styles from './TopValidatorsCard.module.css'; + +// TODO: Specify the type of the context +// Specify the type of the context +function TopValidatorsCard() { + // mock validators data + const validatorsData = [ + { + validatorName: 'Jump Crypto', + suiStake: 9_220_000, + suiStakePercent: '5.2%', + eporchReward: '38,026', + position: 1, + }, + { + validatorName: 'Blockdaemon', + suiStake: 8_220_000, + suiStakePercent: '4.2%', + eporchReward: '34,100', + position: 2, + }, + { + validatorName: 'Kraken', + suiStake: 4_650_000, + suiStakePercent: '2.69%', + eporchReward: '19,220', + position: 3, + }, + { + validatorName: 'Coinbase', + suiStake: 4_550_000, + suiStakePercent: '2.63%', + eporchReward: '18,806', + position: 4, + }, + { + validatorName: 'a16z', + suiStake: 2_860_000, + suiStakePercent: '1.58%', + eporchReward: '11,821', + position: 5, + }, + { + validatorName: 'Figment', + suiStake: 2_840_000, + suiStakePercent: '1.63%', + eporchReward: '11,736', + position: 6, + }, + { + validatorName: '0x813e...d21f07', + suiStake: 2_730_000, + suiStakePercent: '1.58%', + eporchReward: '11,736', + position: 7, + }, + { + validatorName: '0x813e...d21f07', + suiStake: 2_730_000, + suiStakePercent: '1.58%', + eporchReward: '11,736', + position: 8, + }, + { + validatorName: '0x813e...d21f07', + suiStake: 2_730_000, + suiStakePercent: '1.58%', + eporchReward: '11,736', + position: 9, + }, + { + validatorName: '0x813e...d21f07', + suiStake: 2_730_000, + suiStakePercent: '1.58%', + eporchReward: '11,736', + position: 10, + }, + ]; + // map the above data to match the table combine stake and stake percent + const mockValidatorsData = { + data: validatorsData.map((validator) => ({ + validatorName: validator.validatorName, + stake: ( +
+ {' '} + {numberSuffix(validator.suiStake)}{' '} + + {' '} + {validator.suiStakePercent} + +
+ ), + eporchReward: validator.eporchReward, + position: validator.position, + })), + columns: [ + { + headerLabel: '#', + accessorKey: 'position', + }, + { + headerLabel: 'Validator', + accessorKey: 'validatorName', + }, + { + headerLabel: 'STAKE', + accessorKey: 'stake', + }, + { + headerLabel: 'LAST EPOCH REWARD', + accessorKey: 'eporchReward', + }, + ], + }; + + const tabsFooter = { + stats: { + count: 15482, + stats_text: 'total transactions', + }, + }; + + return ( +
+ +
+ + + + +
+
+
+
+ ); +} + +export default TopValidatorsCard; diff --git a/explorer/client/src/components/transaction-card/RecentTxCard.module.css b/explorer/client/src/components/transaction-card/RecentTxCard.module.css index cfb0e4bf5ef49..7dea99b7e5022 100644 --- a/explorer/client/src/components/transaction-card/RecentTxCard.module.css +++ b/explorer/client/src/components/transaction-card/RecentTxCard.module.css @@ -18,8 +18,8 @@ div.txcardgrid > div:first-child { @apply w-full md:w-1/4 lg:w-1/3; } -.txlatestesults { - @apply w-11/12 md:w-10/12 max-w-[1200px] mx-auto mb-[2rem] mt-10 m-auto; +.txlatestresults { + @apply mx-auto mb-[2rem] m-auto; } div.txadd { @@ -69,3 +69,11 @@ div.txadd { .success { @apply text-green-400; } + +.txlatestresults .tab { + @apply text-lg !important; +} + +.moretxbtn { + @apply text-left bg-transparent cursor-pointer border-0 pl-0 pr-0 text-[#636870] hover:text-gray-400 transition-colors duration-200 ease-in-out font-normal text-sm; +} diff --git a/explorer/client/src/components/transaction-card/RecentTxCard.tsx b/explorer/client/src/components/transaction-card/RecentTxCard.tsx index 0c1bf7795387d..cdbc8fcb6679f 100644 --- a/explorer/client/src/components/transaction-card/RecentTxCard.tsx +++ b/explorer/client/src/components/transaction-card/RecentTxCard.tsx @@ -1,11 +1,14 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import cl from 'classnames'; -import { useEffect, useState, useContext } from 'react'; +// import cl from 'classnames'; +import { useEffect, useState, useContext, useCallback } from 'react'; import { useSearchParams } from 'react-router-dom'; -import Longtext from '../../components/longtext/Longtext'; +import { ReactComponent as ContentForwardArrowDark } from '../../assets/SVGIcons/forward-arrow-dark.svg'; +import TableCard from '../../components/table/TableCard'; +import TabFooter from '../../components/tabs/TabFooter'; +import Tabs from '../../components/tabs/Tabs'; import { NetworkContext } from '../../context'; import theme from '../../styles/theme.module.css'; import { @@ -14,11 +17,11 @@ import { getDataOnTxDigests, } from '../../utils/api/DefaultRpcClient'; import { IS_STATIC_ENV } from '../../utils/envUtil'; +import { numberSuffix } from '../../utils/numberUtil'; import { getAllMockTransaction } from '../../utils/static/searchUtil'; import { truncate } from '../../utils/stringUtils'; import { timeAgo } from '../../utils/timeUtils'; import ErrorResult from '../error-result/ErrorResult'; -import Pagination from '../pagination/Pagination'; import type { GetTxnDigestsResponse, @@ -28,11 +31,17 @@ import type { import styles from './RecentTxCard.module.css'; -const TRUNCATE_LENGTH = 25; +const TRUNCATE_LENGTH = 10; +const NUMBER_OF_TX_PER_PAGE = 15; -const initState: { loadState: string; latestTx: TxnData[] } = { +const initState: { + loadState: string; + latestTx: TxnData[]; + totalTxcount?: number; +} = { loadState: 'pending', latestTx: [], + totalTxcount: 0, }; type TxnData = { @@ -97,106 +106,122 @@ async function getRecentTransactions( function LatestTxView({ results, }: { - results: { loadState: string; latestTx: TxnData[] }; + results: { loadState: string; latestTx: TxnData[]; totalTxcount?: number }; }) { - const [network] = useContext(NetworkContext); + // This is temporary, pagination component already does this + const totalCount = results.totalTxcount || 1; + const [searchParams, setSearchParams] = useSearchParams(); + const pageParam = parseInt(searchParams.get('p') || '1', 10); + const [showNextPage, setShowNextPage] = useState(true); + + const changePage = useCallback(() => { + const nextpage = pageParam + (showNextPage ? 1 : 0); + setSearchParams({ p: nextpage.toString() }); + setShowNextPage( + Math.ceil(NUMBER_OF_TX_PER_PAGE * nextpage) < totalCount + ); + }, [pageParam, totalCount, showNextPage, setSearchParams]); + + //TODO update initial state and match the latestTx table data + const defaultActiveTab = 0; + const recentTx = { + data: results.latestTx.map((txn) => ({ + date: `${timeAgo(txn.timestamp_ms, undefined, true)} `, + transactionId: [ + { + url: txn.txId, + name: truncate(txn.txId, TRUNCATE_LENGTH), + category: 'transactions', + isLink: true, + copy: false, + }, + ], + addresses: [ + { + url: txn.From, + name: truncate(txn.From, TRUNCATE_LENGTH), + category: 'addresses', + isLink: true, + copy: false, + }, + ...(txn.To + ? [ + { + url: txn.To, + name: truncate(txn.To, TRUNCATE_LENGTH), + category: 'addresses', + isLink: true, + copy: false, + }, + ] + : []), + ], + txTypes: { + txTypeName: txn.kind, + status: txn.status, + }, + + gas: numberSuffix(txn.txGas), + })), + columns: [ + { + headerLabel: 'Date', + accessorKey: 'date', + }, + { + headerLabel: 'Type', + accessorKey: 'txTypes', + }, + { + headerLabel: 'Transactions ID', + accessorKey: 'transactionId', + }, + { + headerLabel: 'Addresses', + accessorKey: 'addresses', + }, + { + headerLabel: 'Gas', + accessorKey: 'gas', + }, + ], + }; + const tabsFooter = { + stats: { + count: totalCount || 0, + stats_text: 'total transactions', + }, + }; return ( -
-
-

Latest Transactions on {network}

-
-
-
-
-
TxId
- {results.latestTx[0].timestamp_ms && ( -
Time
- )} -
TxType
-
Status
-
Gas
-
Addresses
-
- {results.latestTx.map((tx, index) => ( -
-
- -
- {tx.timestamp_ms && ( -
{`${timeAgo( - tx.timestamp_ms - )} ago`}
- )} -
{tx.kind}
-
+ +
+ + + {showNextPage ? ( +
-
{tx.txGas}
-
-
- From: - -
- {tx.To && ( -
- To : - -
- )} -
-
- ))} + More Transactions + + ) : ( + <> + )} +
-
+
); } -function LatestTxCardStatic({ count }: { count: number }) { +function LatestTxCardStatic() { const latestTx = getAllMockTransaction().map((tx) => ({ ...tx, status: tx.status as ExecutionStatusType, kind: tx.kind as TransactionKindName, })); - const [searchParams] = useSearchParams(); - const pagedNum: number = parseInt(searchParams.get('p') || '1', 10); const results = { loadState: 'loaded', @@ -205,7 +230,6 @@ function LatestTxCardStatic({ count }: { count: number }) { return ( <> - ); } @@ -215,7 +239,8 @@ function LatestTxCardAPI({ count }: { count: number }) { const [results, setResults] = useState(initState); const [network] = useContext(NetworkContext); const [searchParams] = useSearchParams(); - const [txNumPerPage] = useState(15); + const [txNumPerPage] = useState(NUMBER_OF_TX_PER_PAGE); + useEffect(() => { let isMounted = true; const pagedNum: number = parseInt(searchParams.get('p') || '1', 10); @@ -227,6 +252,7 @@ function LatestTxCardAPI({ count }: { count: number }) { setResults({ loadState: 'loaded', latestTx: resp, + totalTxcount: count, }); }) .catch((err) => { @@ -270,16 +296,11 @@ function LatestTxCardAPI({ count }: { count: number }) { return ( <> - ); } const LatestTxCard = ({ count }: { count: number }) => - IS_STATIC_ENV ? ( - - ) : ( - - ); + IS_STATIC_ENV ? : ; export default LatestTxCard; diff --git a/explorer/client/src/components/transaction-count/TxCountCard.module.css b/explorer/client/src/components/transaction-count/TxCountCard.module.css deleted file mode 100644 index b8082e6a9d1f2..0000000000000 --- a/explorer/client/src/components/transaction-count/TxCountCard.module.css +++ /dev/null @@ -1,7 +0,0 @@ -div.txcount { - @apply font-normal leading-3 font-mono text-center w-11/12 md:w-10/12 mx-auto mb-28 mt-10; -} - -div.txcount > div { - margin-top: 1rem; -} diff --git a/explorer/client/src/components/transaction-count/TxCountCard.tsx b/explorer/client/src/components/transaction-count/TxCountCard.tsx deleted file mode 100644 index 5cde3ac692f06..0000000000000 --- a/explorer/client/src/components/transaction-count/TxCountCard.tsx +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2022, Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { IS_STATIC_ENV } from '../../utils/envUtil'; - -import styles from './TxCountCard.module.css'; - -function TxCountCard({ count }: { count: number | string }) { - return ( -
- Total Transactions -
{count}
-
- ); -} - -function TxCountCardStatic() { - return ; -} - -const LatestTxCard = ({ count }: { count: number }) => - IS_STATIC_ENV ? : ; - -export default LatestTxCard; diff --git a/explorer/client/src/pages/home/Home.module.css b/explorer/client/src/pages/home/Home.module.css index 0350a047bf037..2c60eb7a6d10a 100644 --- a/explorer/client/src/pages/home/Home.module.css +++ b/explorer/client/src/pages/home/Home.module.css @@ -1,3 +1,7 @@ div.home { - @apply min-h-screen; + @apply bg-white min-h-screen; +} + +div.container { + @apply min-h-screen mx-auto grid md:grid-cols-2 grid-cols-1 max-w-[1440px] pl-5 pr-5 pt-10 gap-10; } diff --git a/explorer/client/src/pages/home/Home.tsx b/explorer/client/src/pages/home/Home.tsx index 2436ffd2ea700..a5752d3f250db 100644 --- a/explorer/client/src/pages/home/Home.tsx +++ b/explorer/client/src/pages/home/Home.tsx @@ -1,11 +1,13 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 - +import cl from 'classnames'; import { useEffect, useState, useContext } from 'react'; import ErrorResult from '../../components/error-result/ErrorResult'; +// import SuiNetworkStats from '../../components/network-stats/SuiNetworkStats'; +// import TopGroupsCard from '../../components/top-groups/TopGroups'; +// import TopValidatorsCard from '../../components/top-validators-card/TopValidatorsCard'; import LastestTxCard from '../../components/transaction-card/RecentTxCard'; -import TxCountCard from '../../components/transaction-count/TxCountCard'; import { NetworkContext } from '../../context'; import { DefaultRpcClient as rpc, @@ -27,7 +29,6 @@ function HomeStatic() { return (
-
); } @@ -74,9 +75,15 @@ function HomeAPI() { ); } return ( -
- - +
+
+ +
+
); } diff --git a/explorer/client/src/utils/numberUtil.ts b/explorer/client/src/utils/numberUtil.ts new file mode 100644 index 0000000000000..1eaea69a238aa --- /dev/null +++ b/explorer/client/src/utils/numberUtil.ts @@ -0,0 +1,13 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Number Suffix +export const numberSuffix = (num: number): string => { + if (num >= 1000000) { + return (num / 1000000).toFixed(1) + 'M'; + } + if (num >= 1000) { + return (num / 1000).toFixed(1) + 'K'; + } + return num.toString(); +}; diff --git a/explorer/client/src/utils/timeUtils.ts b/explorer/client/src/utils/timeUtils.ts index f15be70dd33d7..ca673d05d6e3a 100644 --- a/explorer/client/src/utils/timeUtils.ts +++ b/explorer/client/src/utils/timeUtils.ts @@ -34,38 +34,70 @@ export const convertNumberToDate = (epochMilliSecs: number | null): string => { )}:${stdToN(date.getUTCSeconds(), 2)} UTC`; }; +// TODO - this need a bit of modification to account for multiple display formate types export const timeAgo = ( epochMilliSecs: number | null | undefined, - timeNow?: number + timeNow?: number, + shortenTimeLabel?: boolean ): string => { if (!epochMilliSecs) return ''; //In static mode the time is fixed at 1 Jan 2025 01:13:10 UTC for testing purposes timeNow = timeNow ? timeNow : IS_STATIC_ENV ? 1735693990000 : Date.now(); + const timeLabel = { + year: { + full: 'year', + short: 'y', + }, + month: { + full: 'month', + short: 'm', + }, + day: { + full: 'day', + short: 'd', + }, + hour: { + full: 'hour', + short: 'h', + }, + min: { + full: 'min', + short: 'm', + }, + sec: { + full: 'sec', + short: 's', + }, + }; + const dateKeyType = shortenTimeLabel ? 'short' : 'full'; + let timeUnit: [string, number][]; let timeCol = timeNow - epochMilliSecs; if (timeCol >= 1000 * 60 * 60 * 24) { timeUnit = [ - ['day', 1000 * 60 * 60 * 24], - ['hour', 1000 * 60 * 60], + [timeLabel.day[dateKeyType], 1000 * 60 * 60 * 24], + [timeLabel.hour[dateKeyType], 1000 * 60 * 60], ]; } else if (timeCol >= 1000 * 60 * 60) { timeUnit = [ - ['hour', 1000 * 60 * 60], - ['min', 1000 * 60], + [timeLabel.hour[dateKeyType], 1000 * 60 * 60], + [timeLabel.min[dateKeyType], 1000 * 60], ]; } else { timeUnit = [ - ['min', 1000 * 60], - ['sec', 1000], + [timeLabel.min[dateKeyType], 1000 * 60], + [timeLabel.sec[dateKeyType], 1000], ]; } const convertAmount = (amount: number, label: string) => { - if (amount > 1) return `${amount} ${label}s`; - if (amount === 1) return `${amount} ${label}`; + const spacing = shortenTimeLabel ? '' : ' '; + if (amount > 1) + return `${amount}${spacing}${label}${!shortenTimeLabel ? 's' : ''}`; + if (amount === 1) return `${amount}${spacing}${label}`; return ''; };