@@ -43,9 +42,13 @@ const AddressCoinBalanceTableItem = (props: Props) => {
) }
-
- { timeAgo }
-
+
|
diff --git a/ui/address/internals/AddressIntTxsListItem.tsx b/ui/address/internals/AddressIntTxsListItem.tsx
index 4b0861e706..2e38defecb 100644
--- a/ui/address/internals/AddressIntTxsListItem.tsx
+++ b/ui/address/internals/AddressIntTxsListItem.tsx
@@ -5,7 +5,6 @@ import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import { currencyUnits } from 'lib/units';
import AddressFromTo from 'ui/shared/address/AddressFromTo';
import Tag from 'ui/shared/chakra/Tag';
@@ -13,6 +12,7 @@ import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TxStatus from 'ui/shared/statusTag/TxStatus';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { currentAddress: string; isLoading?: boolean };
@@ -47,9 +47,13 @@ const TxInternalsListItem = ({
fontWeight={ 700 }
truncation="constant_long"
/>
-
- { dayjs(timestamp).fromNow() }
-
+
Block
diff --git a/ui/address/internals/AddressIntTxsTableItem.tsx b/ui/address/internals/AddressIntTxsTableItem.tsx
index 49f83d5100..d19730773d 100644
--- a/ui/address/internals/AddressIntTxsTableItem.tsx
+++ b/ui/address/internals/AddressIntTxsTableItem.tsx
@@ -5,12 +5,12 @@ import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import config from 'configs/app';
-import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import AddressFromTo from 'ui/shared/address/AddressFromTo';
import Tag from 'ui/shared/chakra/Tag';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxStatus from 'ui/shared/statusTag/TxStatus';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { currentAddress: string; isLoading?: boolean }
@@ -32,8 +32,6 @@ const AddressIntTxsTableItem = ({
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to ? to : createdContract;
- const timeAgo = useTimeAgoIncrement(timestamp, true);
-
return (
@@ -45,11 +43,14 @@ const AddressIntTxsTableItem = ({
noIcon
truncation="constant_long"
/>
- { timestamp && (
-
- { timeAgo }
-
- ) }
+
|
diff --git a/ui/block/BlockDetails.tsx b/ui/block/BlockDetails.tsx
index 656fe2878f..68b1f5bd6a 100644
--- a/ui/block/BlockDetails.tsx
+++ b/ui/block/BlockDetails.tsx
@@ -1,4 +1,4 @@
-import { Grid, GridItem, Text, Link, Box, Tooltip, useColorModeValue, Skeleton } from '@chakra-ui/react';
+import { Grid, GridItem, Text, Link, Box, Tooltip, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize';
import { useRouter } from 'next/router';
@@ -18,6 +18,7 @@ import { space } from 'lib/html-entities';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import getQueryParamString from 'lib/router/getQueryParamString';
import { currencyUnits } from 'lib/units';
+import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
@@ -26,14 +27,12 @@ import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
-import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/links/LinkInternal';
import PrevNext from 'ui/shared/PrevNext';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
import StatusTag from 'ui/shared/statusTag/StatusTag';
-import TextSeparator from 'ui/shared/TextSeparator';
import Utilization from 'ui/shared/Utilization/Utilization';
import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps';
import ZkSyncL2TxnBatchHashesInfo from 'ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo';
@@ -52,8 +51,6 @@ const BlockDetails = ({ query }: Props) => {
const router = useRouter();
const heightOrHash = getQueryParamString(router.query.height_or_hash);
- const separatorColor = useColorModeValue('gray.200', 'gray.700');
-
const { data, isPlaceholderData } = query;
const handleCutClick = React.useCallback(() => {
@@ -412,18 +409,13 @@ const BlockDetails = ({ query }: Props) => {
{ BigNumber(data.gas_used || 0).toFormat() }
-
- { data.gas_target_percentage && (
- <>
-
-
- >
- ) }
{
- const timeAgo = useTimeAgoIncrement(ts, isEnabled);
-
- return (
-
- { timeAgo }
-
- );
-};
-
-export default React.memo(chakra(BlockTimestamp));
diff --git a/ui/blocks/BlocksListItem.tsx b/ui/blocks/BlocksListItem.tsx
index ea12fd5a1f..0d6d8ce58f 100644
--- a/ui/blocks/BlocksListItem.tsx
+++ b/ui/blocks/BlocksListItem.tsx
@@ -1,4 +1,4 @@
-import { Flex, Skeleton, Text, Box, useColorModeValue } from '@chakra-ui/react';
+import { Flex, Skeleton, Text, Box } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize';
import React from 'react';
@@ -12,14 +12,13 @@ import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import { WEI } from 'lib/consts';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import { currencyUnits } from 'lib/units';
-import BlockTimestamp from 'ui/blocks/BlockTimestamp';
+import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
-import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
-import TextSeparator from 'ui/shared/TextSeparator';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
import Utilization from 'ui/shared/Utilization/Utilization';
interface Props {
@@ -35,8 +34,6 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const burntFees = BigNumber(data.burnt_fees || 0);
const txFees = BigNumber(data.tx_fees || 0);
- const separatorColor = useColorModeValue('gray.200', 'gray.700');
-
return (
@@ -49,7 +46,14 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
fontWeight={ 600 }
/>
-
+
Size
@@ -85,13 +89,12 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
{ BigNumber(data.gas_used || 0).toFormat() }
-
- { data.gas_target_percentage && (
- <>
-
-
- >
- ) }
+
{ !isRollup && !config.UI.views.block.hiddenFields?.total_reward && (
diff --git a/ui/blocks/BlocksTableItem.tsx b/ui/blocks/BlocksTableItem.tsx
index 1e230e4788..8ded36820f 100644
--- a/ui/blocks/BlocksTableItem.tsx
+++ b/ui/blocks/BlocksTableItem.tsx
@@ -10,13 +10,12 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import { WEI } from 'lib/consts';
-import BlockTimestamp from 'ui/blocks/BlockTimestamp';
+import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
-import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/links/LinkInternal';
-import TextSeparator from 'ui/shared/TextSeparator';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
import Utilization from 'ui/shared/Utilization/Utilization';
interface Props {
@@ -32,7 +31,6 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const burntFees = BigNumber(data.burnt_fees || 0);
const txFees = BigNumber(data.tx_fees || 0);
- const separatorColor = useColorModeValue('gray.200', 'gray.700');
const burntFeesIconColor = useColorModeValue('gray.500', 'inherit');
return (
@@ -58,7 +56,14 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
/>
-
+
|
@@ -89,21 +94,12 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
{ BigNumber(data.gas_used || 0).toFormat() }
-
-
-
-
-
- { data.gas_target_percentage && (
- <>
-
-
- >
- ) }
+
|
{ !isRollup && !config.UI.views.block.hiddenFields?.total_reward && (
diff --git a/ui/deposits/optimisticL2/OptimisticDepositsListItem.tsx b/ui/deposits/optimisticL2/OptimisticDepositsListItem.tsx
index fe8abd7a0a..94cdd9e2f9 100644
--- a/ui/deposits/optimisticL2/OptimisticDepositsListItem.tsx
+++ b/ui/deposits/optimisticL2/OptimisticDepositsListItem.tsx
@@ -5,20 +5,18 @@ import React from 'react';
import type { OptimisticL2DepositsItem } from 'types/api/optimisticL2';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import AddressEntityL1 from 'ui/shared/entities/address/AddressEntityL1';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
type Props = { item: OptimisticL2DepositsItem; isLoading?: boolean };
const OptimisticDepositsListItem = ({ item, isLoading }: Props) => {
- const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
-
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
}
@@ -50,7 +48,11 @@ const OptimisticDepositsListItem = ({ item, isLoading }: Props) => {
Age
- { timeAgo }
+
L1 txn hash
diff --git a/ui/deposits/optimisticL2/OptimisticDepositsTableItem.tsx b/ui/deposits/optimisticL2/OptimisticDepositsTableItem.tsx
index af231b159f..2284839a18 100644
--- a/ui/deposits/optimisticL2/OptimisticDepositsTableItem.tsx
+++ b/ui/deposits/optimisticL2/OptimisticDepositsTableItem.tsx
@@ -5,18 +5,17 @@ import React from 'react';
import type { OptimisticL2DepositsItem } from 'types/api/optimisticL2';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import AddressEntityL1 from 'ui/shared/entities/address/AddressEntityL1';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
type Props = { item: OptimisticL2DepositsItem; isLoading?: boolean };
const OptimisticDepositsTableItem = ({ item, isLoading }: Props) => {
- const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
@@ -45,7 +44,12 @@ const OptimisticDepositsTableItem = ({ item, isLoading }: Props) => {
/>
|
- { timeAgo }
+
|
{
- const timeAgo = dayjs(item.timestamp).fromNow();
-
if (!(feature.isEnabled && feature.type === 'shibarium')) {
return null;
}
@@ -70,7 +67,11 @@ const DepositsListItem = ({ item, isLoading }: Props) => {
Age
- { timeAgo }
+
diff --git a/ui/deposits/shibarium/DepositsTableItem.tsx b/ui/deposits/shibarium/DepositsTableItem.tsx
index 7c38f52dd5..da23f68ecc 100644
--- a/ui/deposits/shibarium/DepositsTableItem.tsx
+++ b/ui/deposits/shibarium/DepositsTableItem.tsx
@@ -1,21 +1,20 @@
-import { Td, Tr, Skeleton } from '@chakra-ui/react';
+import { Td, Tr } from '@chakra-ui/react';
import React from 'react';
import type { ShibariumDepositsItem } from 'types/api/shibarium';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import AddressStringOrParam from 'ui/shared/entities/address/AddressStringOrParam';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const feature = config.features.rollup;
type Props = { item: ShibariumDepositsItem; isLoading?: boolean };
const DepositsTableItem = ({ item, isLoading }: Props) => {
- const timeAgo = dayjs(item.timestamp).fromNow();
if (!(feature.isEnabled && feature.type === 'shibarium')) {
return null;
@@ -59,7 +58,12 @@ const DepositsTableItem = ({ item, isLoading }: Props) => {
/>
|
- { timeAgo }
+
|
);
diff --git a/ui/deposits/zkEvmL2/ZkEvmL2DepositsListItem.tsx b/ui/deposits/zkEvmL2/ZkEvmL2DepositsListItem.tsx
index 578d04a628..4ec6b525f2 100644
--- a/ui/deposits/zkEvmL2/ZkEvmL2DepositsListItem.tsx
+++ b/ui/deposits/zkEvmL2/ZkEvmL2DepositsListItem.tsx
@@ -5,11 +5,11 @@ import React from 'react';
import type { ZkEvmL2DepositsItem } from 'types/api/zkEvmL2';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
@@ -20,8 +20,6 @@ const ZkEvmL2DepositsListItem = ({ item, isLoading }: Props) => {
return null;
}
- const timeAgo = dayjs(item.timestamp).fromNow();
-
return (
@@ -56,7 +54,11 @@ const ZkEvmL2DepositsListItem = ({ item, isLoading }: Props) => {
Age
- { timeAgo }
+
L2 txn hash
diff --git a/ui/deposits/zkEvmL2/ZkEvmL2DepositsTableItem.tsx b/ui/deposits/zkEvmL2/ZkEvmL2DepositsTableItem.tsx
index 37f57d9d66..962135741b 100644
--- a/ui/deposits/zkEvmL2/ZkEvmL2DepositsTableItem.tsx
+++ b/ui/deposits/zkEvmL2/ZkEvmL2DepositsTableItem.tsx
@@ -5,10 +5,10 @@ import React from 'react';
import type { ZkEvmL2DepositsItem } from 'types/api/zkEvmL2';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
@@ -19,8 +19,6 @@ const ZkEvmL2DepositsTableItem = ({ item, isLoading }: Props) => {
return null;
}
- const timeAgo = dayjs(item.timestamp).fromNow();
-
return (
@@ -49,9 +47,11 @@ const ZkEvmL2DepositsTableItem = ({ item, isLoading }: Props) => {
/>
|
-
- { timeAgo }
-
+
|
{ item.l2_transaction_hash ? (
diff --git a/ui/disputeGames/optimisticL2/OptimisticL2DisputeGamesListItem.tsx b/ui/disputeGames/optimisticL2/OptimisticL2DisputeGamesListItem.tsx
index 3c6546de3b..27d83573c5 100644
--- a/ui/disputeGames/optimisticL2/OptimisticL2DisputeGamesListItem.tsx
+++ b/ui/disputeGames/optimisticL2/OptimisticL2DisputeGamesListItem.tsx
@@ -4,11 +4,11 @@ import React from 'react';
import type { OptimisticL2DisputeGamesItem } from 'types/api/optimisticL2';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import HashStringShorten from 'ui/shared/HashStringShorten';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
@@ -53,7 +53,11 @@ const OptimisticL2DisputeGamesListItem = ({ item, isLoading }: Props) => {
Age
- { dayjs(item.created_at).fromNow() }
+
Status
@@ -64,7 +68,11 @@ const OptimisticL2DisputeGamesListItem = ({ item, isLoading }: Props) => {
{ item.resolved_at && (
<>
Resolution age
- { dayjs(item.resolved_at).fromNow() }
+
>
) }
diff --git a/ui/disputeGames/optimisticL2/OptimisticL2DisputeGamesTableItem.tsx b/ui/disputeGames/optimisticL2/OptimisticL2DisputeGamesTableItem.tsx
index 0de2ced695..dd55542b5d 100644
--- a/ui/disputeGames/optimisticL2/OptimisticL2DisputeGamesTableItem.tsx
+++ b/ui/disputeGames/optimisticL2/OptimisticL2DisputeGamesTableItem.tsx
@@ -4,10 +4,10 @@ import React from 'react';
import type { OptimisticL2DisputeGamesItem } from 'types/api/optimisticL2';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import HashStringShorten from 'ui/shared/HashStringShorten';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const faultProofSystemFeature = config.features.faultProofSystem;
@@ -44,15 +44,22 @@ const OptimisticL2DisputeGamesTableItem = ({ item, isLoading }: Props) => {
/>
|
- { dayjs(item.created_at).fromNow() }
+
|
{ item.status }
|
-
- { item.resolved_at ? dayjs(item.resolved_at).fromNow() : 'N/A' }
-
+
|
);
diff --git a/ui/home/LatestBlocksItem.tsx b/ui/home/LatestBlocksItem.tsx
index 3f2a1fb31c..72b428d93c 100644
--- a/ui/home/LatestBlocksItem.tsx
+++ b/ui/home/LatestBlocksItem.tsx
@@ -12,9 +12,9 @@ import type { Block } from 'types/api/block';
import config from 'configs/app';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
-import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
type Props = {
block: Block;
@@ -46,10 +46,13 @@ const LatestBlocksItem = ({ block, isLoading }: Props) => {
fontWeight={ 500 }
mr="auto"
/>
- {
- const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
const isMobile = useIsMobile();
if (!feature.isEnabled || feature.type !== 'optimistic') {
@@ -66,9 +65,11 @@ const LatestDepositsItem = ({ item, isLoading }: Props) => {
<>
{ l1BlockLink }
-
- { timeAgo }
-
+
@@ -91,9 +92,14 @@ const LatestDepositsItem = ({ item, isLoading }: Props) => {
L1 txn
{ l1TxLink }
-
- { timeAgo }
-
+
L2 txn
diff --git a/ui/home/LatestTxsItem.tsx b/ui/home/LatestTxsItem.tsx
index c556b24f22..7fcc367f8f 100644
--- a/ui/home/LatestTxsItem.tsx
+++ b/ui/home/LatestTxsItem.tsx
@@ -12,11 +12,11 @@ import type { Transaction } from 'types/api/transaction';
import config from 'configs/app';
import getValueWithUnit from 'lib/getValueWithUnit';
-import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import { currencyUnits } from 'lib/units';
import AddressFromTo from 'ui/shared/address/AddressFromTo';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxStatus from 'ui/shared/statusTag/TxStatus';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
import TxFee from 'ui/shared/tx/TxFee';
import TxWatchListTags from 'ui/shared/tx/TxWatchListTags';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
@@ -29,7 +29,6 @@ type Props = {
const LatestTxsItem = ({ tx, isLoading }: Props) => {
const dataTo = tx.to ? tx.to : tx.created_contract;
- const timeAgo = useTimeAgoIncrement(tx.timestamp || '0', true);
const columnNum = config.UI.views.tx.hiddenFields?.value && config.UI.views.tx.hiddenFields?.tx_fee ? 2 : 3;
return (
@@ -65,18 +64,16 @@ const LatestTxsItem = ({ tx, isLoading }: Props) => {
hash={ tx.hash }
fontWeight="700"
/>
- { tx.timestamp && (
-
- { timeAgo }
-
- ) }
+
diff --git a/ui/home/LatestTxsItemMobile.tsx b/ui/home/LatestTxsItemMobile.tsx
index 323ccafa48..e3d88bd9d0 100644
--- a/ui/home/LatestTxsItemMobile.tsx
+++ b/ui/home/LatestTxsItemMobile.tsx
@@ -11,11 +11,11 @@ import type { Transaction } from 'types/api/transaction';
import config from 'configs/app';
import getValueWithUnit from 'lib/getValueWithUnit';
-import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import { currencyUnits } from 'lib/units';
import AddressFromTo from 'ui/shared/address/AddressFromTo';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxStatus from 'ui/shared/statusTag/TxStatus';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
import TxFee from 'ui/shared/tx/TxFee';
import TxWatchListTags from 'ui/shared/tx/TxWatchListTags';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
@@ -28,7 +28,6 @@ type Props = {
const LatestTxsItem = ({ tx, isLoading }: Props) => {
const dataTo = tx.to ? tx.to : tx.created_contract;
- const timeAgo = useTimeAgoIncrement(tx.timestamp || '0', true);
return (
{
fontWeight="700"
truncation="constant_long"
/>
- { tx.timestamp && (
-
- { timeAgo }
-
- ) }
+
{
fontWeight={ 500 }
mr="auto"
/>
- {
await mockApiResponse('stats', statsMock.noChartData);
await mockApiResponse('stats_charts_txs', dailyTxsMock.noData);
- await mockAssetResponse(statsMock.base.coin_image as string, './playwright/mocks/image_s.jpg');
+ await mockAssetResponse(statsMock.noChartData.coin_image as string, './playwright/mocks/image_s.jpg');
const component = await render();
await expect(component).toHaveScreenshot();
diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-dark-mode-mobile-1.png b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-dark-mode-mobile-1.png
index 5d23631570..d6c68c11aa 100644
Binary files a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-dark-mode-mobile-1.png and b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-dark-mode-mobile-1.png differ
diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-mobile-1.png b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-mobile-1.png
index 587d663315..afb024abc0 100644
Binary files a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-mobile-1.png and b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_default_daily-txs-chart-mobile-1.png differ
diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-dark-mode-mobile-1.png b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-dark-mode-mobile-1.png
index fba7f1d6e5..bbd552cddf 100644
Binary files a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-dark-mode-mobile-1.png and b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-dark-mode-mobile-1.png differ
diff --git a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-mobile-1.png b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-mobile-1.png
index dabebb47a0..4fd9099e3e 100644
Binary files a/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-mobile-1.png and b/ui/home/indicators/__screenshots__/ChainIndicators.pw.tsx_mobile_daily-txs-chart-mobile-1.png differ
diff --git a/ui/marketplace/AppSecurityReport.tsx b/ui/marketplace/AppSecurityReport.tsx
index badbf181a3..740a7f09ae 100644
--- a/ui/marketplace/AppSecurityReport.tsx
+++ b/ui/marketplace/AppSecurityReport.tsx
@@ -72,7 +72,7 @@ const AppSecurityReport = ({
className={ className }
/>
-
+
Smart contracts info
diff --git a/ui/marketplace/Banner/FeaturedApp.tsx b/ui/marketplace/Banner/FeaturedApp.tsx
index 02913e4806..72dd6f49cd 100644
--- a/ui/marketplace/Banner/FeaturedApp.tsx
+++ b/ui/marketplace/Banner/FeaturedApp.tsx
@@ -7,8 +7,8 @@ import type { MarketplaceAppPreview } from 'types/client/marketplace';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
-import IconSvg from 'ui/shared/IconSvg';
+import FavoriteIcon from '../FavoriteIcon';
import MarketplaceAppIntegrationIcon from '../MarketplaceAppIntegrationIcon';
import FeaturedAppMobile from './FeaturedAppMobile';
@@ -136,10 +136,7 @@ const FeaturedApp = ({
w={ 9 }
h={ 8 }
onClick={ handleFavoriteClick }
- icon={ isFavorite ?
- :
-
- }
+ icon={ }
/>
) }
diff --git a/ui/marketplace/Banner/FeaturedAppMobile.tsx b/ui/marketplace/Banner/FeaturedAppMobile.tsx
index f4fb2f31d5..5f7e8c7b4b 100644
--- a/ui/marketplace/Banner/FeaturedAppMobile.tsx
+++ b/ui/marketplace/Banner/FeaturedAppMobile.tsx
@@ -4,8 +4,7 @@ import React from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace';
-import IconSvg from 'ui/shared/IconSvg';
-
+import FavoriteIcon from '../FavoriteIcon';
import MarketplaceAppCardLink from '../MarketplaceAppCardLink';
import MarketplaceAppIntegrationIcon from '../MarketplaceAppIntegrationIcon';
@@ -144,10 +143,7 @@ const FeaturedAppMobile = ({
w={ 9 }
h={ 8 }
onClick={ onFavoriteClick }
- icon={ isFavorite ?
- :
-
- }
+ icon={ }
/>
) }
diff --git a/ui/marketplace/EmptySearchResult.tsx b/ui/marketplace/EmptySearchResult.tsx
index 7d3185019d..171a68cc78 100644
--- a/ui/marketplace/EmptySearchResult.tsx
+++ b/ui/marketplace/EmptySearchResult.tsx
@@ -21,7 +21,7 @@ const EmptySearchResult = ({ favoriteApps, selectedCategoryId }: Props) => (
(selectedCategoryId === MarketplaceCategory.FAVORITES && !favoriteApps.length) ? (
<>
You don{ apos }t have any favorite apps.
- Click on the icon on the app{ apos }s card to add it to Favorites.
+ Click on the icon on the app{ apos }s card to add it to Favorites.
>
) : (
<>
diff --git a/ui/marketplace/FavoriteIcon.tsx b/ui/marketplace/FavoriteIcon.tsx
new file mode 100644
index 0000000000..e2b589b98d
--- /dev/null
+++ b/ui/marketplace/FavoriteIcon.tsx
@@ -0,0 +1,24 @@
+import { useColorModeValue } from '@chakra-ui/react';
+import React from 'react';
+
+import IconSvg from 'ui/shared/IconSvg';
+
+type Props = {
+ isFavorite: boolean;
+ color?: string;
+}
+
+const FavoriteIcon = ({ isFavorite, color }: Props) => {
+ const heartFilledColor = useColorModeValue('blue.700', 'gray.400');
+ const defaultColor = isFavorite ? heartFilledColor : 'gray.400';
+
+ return (
+
+ );
+};
+
+export default FavoriteIcon;
diff --git a/ui/marketplace/MarketplaceAppCard.tsx b/ui/marketplace/MarketplaceAppCard.tsx
index 7db60d3ec4..834255eff6 100644
--- a/ui/marketplace/MarketplaceAppCard.tsx
+++ b/ui/marketplace/MarketplaceAppCard.tsx
@@ -1,15 +1,17 @@
-import { Box, IconButton, Image, Link, LinkBox, Skeleton, useColorModeValue, chakra, Flex } from '@chakra-ui/react';
+import { IconButton, Image, Link, LinkBox, Skeleton, useColorModeValue, chakra, Flex } from '@chakra-ui/react';
import type { MouseEvent } from 'react';
import React, { useCallback } from 'react';
-import type { MarketplaceAppWithSecurityReport, ContractListTypes } from 'types/client/marketplace';
+import type { MarketplaceAppWithSecurityReport, ContractListTypes, AppRating } from 'types/client/marketplace';
import useIsMobile from 'lib/hooks/useIsMobile';
-import IconSvg from 'ui/shared/IconSvg';
import AppSecurityReport from './AppSecurityReport';
+import FavoriteIcon from './FavoriteIcon';
import MarketplaceAppCardLink from './MarketplaceAppCardLink';
import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon';
+import Rating from './Rating/Rating';
+import type { RateFunction } from './Rating/useRatings';
interface Props extends MarketplaceAppWithSecurityReport {
onInfoClick: (id: string) => void;
@@ -19,6 +21,11 @@ interface Props extends MarketplaceAppWithSecurityReport {
onAppClick: (event: MouseEvent, id: string) => void;
className?: string;
showContractList: (id: string, type: ContractListTypes) => void;
+ userRating?: AppRating;
+ rateApp: RateFunction;
+ isRatingSending: boolean;
+ isRatingLoading: boolean;
+ canRate: boolean | undefined;
}
const MarketplaceAppCard = ({
@@ -39,6 +46,12 @@ const MarketplaceAppCard = ({
securityReport,
className,
showContractList,
+ rating,
+ userRating,
+ rateApp,
+ isRatingSending,
+ isRatingLoading,
+ canRate,
}: Props) => {
const isMobile = useIsMobile();
const categoriesLabel = categories.join(', ');
@@ -141,8 +154,7 @@ const MarketplaceAppCard = ({
{ !isLoading && (
-
More info
- :
-
- }
- />
-
+
+
+ }
+ />
+
+
) }
{ securityReport && (
diff --git a/ui/marketplace/MarketplaceAppIntegrationIcon.tsx b/ui/marketplace/MarketplaceAppIntegrationIcon.tsx
index 07d2dd14ff..378c7c0c95 100644
--- a/ui/marketplace/MarketplaceAppIntegrationIcon.tsx
+++ b/ui/marketplace/MarketplaceAppIntegrationIcon.tsx
@@ -36,7 +36,7 @@ const MarketplaceAppIntegrationIcon = ({ external, internalWallet }: Props) => {
textAlign="center"
padding={ 2 }
openDelay={ 300 }
- maxW={ 400 }
+ maxW={{ base: 'calc(100vw - 8px)', lg: '400px' }}
>
{},
+ isRatingSending: false,
+ isRatingLoading: false,
+ canRate: undefined,
};
-const testFn: Parameters[1] = async({ render, page, mockAssetResponse }) => {
+const testFn: Parameters[1] = async({ render, page, mockAssetResponse, mockEnvs }) => {
+ await mockEnvs([
+ [ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY', 'test' ],
+ [ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID', 'test' ],
+ ]);
await mockAssetResponse(appsMock[0].logo, './playwright/mocks/image_s.jpg');
await render();
+ await page.getByText('Launch app').focus();
await expect(page).toHaveScreenshot();
};
diff --git a/ui/marketplace/MarketplaceAppModal.tsx b/ui/marketplace/MarketplaceAppModal.tsx
index 03e3885f45..0eb9906480 100644
--- a/ui/marketplace/MarketplaceAppModal.tsx
+++ b/ui/marketplace/MarketplaceAppModal.tsx
@@ -4,9 +4,10 @@ import {
} from '@chakra-ui/react';
import React, { useCallback } from 'react';
-import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace';
+import type { MarketplaceAppWithSecurityReport, AppRating } from 'types/client/marketplace';
import { ContractListTypes } from 'types/client/marketplace';
+import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
import * as mixpanel from 'lib/mixpanel/index';
@@ -14,7 +15,13 @@ import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
import AppSecurityReport from './AppSecurityReport';
+import FavoriteIcon from './FavoriteIcon';
import MarketplaceAppModalLink from './MarketplaceAppModalLink';
+import Rating from './Rating/Rating';
+import type { RateFunction } from './Rating/useRatings';
+
+const feature = config.features.marketplace;
+const isRatingEnabled = feature.isEnabled && feature.rating;
type Props = {
onClose: () => void;
@@ -22,6 +29,11 @@ type Props = {
onFavoriteClick: (id: string, isFavorite: boolean, source: 'App modal') => void;
data: MarketplaceAppWithSecurityReport;
showContractList: (id: string, type: ContractListTypes, hasPreviousStep: boolean) => void;
+ userRating?: AppRating;
+ rateApp: RateFunction;
+ isRatingSending: boolean;
+ isRatingLoading: boolean;
+ canRate: boolean | undefined;
}
const MarketplaceAppModal = ({
@@ -30,9 +42,12 @@ const MarketplaceAppModal = ({
onFavoriteClick,
data,
showContractList: showContractListProp,
+ userRating,
+ rateApp,
+ isRatingSending,
+ isRatingLoading,
+ canRate,
}: Props) => {
- const starOutlineIconColor = useColorModeValue('gray.600', 'gray.300');
-
const {
id,
title,
@@ -49,6 +64,7 @@ const MarketplaceAppModal = ({
logoDarkMode,
categories,
securityReport,
+ rating,
} = data;
const socialLinks = [
@@ -119,7 +135,7 @@ const MarketplaceAppModal = ({
w={{ base: '72px', md: '144px' }}
h={{ base: '72px', md: '144px' }}
marginRight={{ base: 6, md: 8 }}
- gridRow={{ base: '1 / 3', md: '1 / 4' }}
+ gridRow={{ base: '1 / 3', md: '1 / 5' }}
>
{ title }
@@ -142,16 +158,37 @@ const MarketplaceAppModal = ({
By{ nbsp }{ author }
+ { isRatingEnabled && (
+
+
+
+ ) }
+
@@ -170,9 +207,7 @@ const MarketplaceAppModal = ({
w={ 9 }
h={ 8 }
onClick={ handleFavoriteClick }
- icon={ isFavorite ?
- :
- }
+ icon={ }
/>
diff --git a/ui/marketplace/MarketplaceAppTopBar.tsx b/ui/marketplace/MarketplaceAppTopBar.tsx
index f7e5235f2d..5cfc939208 100644
--- a/ui/marketplace/MarketplaceAppTopBar.tsx
+++ b/ui/marketplace/MarketplaceAppTopBar.tsx
@@ -18,18 +18,23 @@ import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop';
import AppSecurityReport from './AppSecurityReport';
import ContractListModal from './ContractListModal';
import MarketplaceAppInfo from './MarketplaceAppInfo';
+import Rating from './Rating/Rating';
+import useRatings from './Rating/useRatings';
type Props = {
+ appId: string;
data: MarketplaceAppOverview | undefined;
isLoading: boolean;
securityReport?: MarketplaceAppSecurityReport;
}
-const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => {
+const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props) => {
const [ contractListType, setContractListType ] = React.useState();
const appProps = useAppContext();
const isMobile = useIsMobile();
+ const { ratings, userRatings, rateApp, isRatingSending, isRatingLoading, canRate } = useRatings();
+
const goBackUrl = React.useMemo(() => {
if (appProps.referrer && appProps.referrer.includes('/apps') && !appProps.referrer.includes('/apps/')) {
return appProps.referrer;
@@ -82,6 +87,16 @@ const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => {
source="App page"
/>
) }
+
{ !isMobile && (
{ config.features.account.isEnabled && }
diff --git a/ui/marketplace/MarketplaceList.tsx b/ui/marketplace/MarketplaceList.tsx
index b64f6c9618..19cf53722b 100644
--- a/ui/marketplace/MarketplaceList.tsx
+++ b/ui/marketplace/MarketplaceList.tsx
@@ -1,13 +1,15 @@
-import { Grid } from '@chakra-ui/react';
+import { Grid, Box } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { MouseEvent } from 'react';
-import type { MarketplaceAppWithSecurityReport, ContractListTypes } from 'types/client/marketplace';
+import type { MarketplaceAppWithSecurityReport, ContractListTypes, AppRating } from 'types/client/marketplace';
+import useLazyRenderedList from 'lib/hooks/useLazyRenderedList';
import * as mixpanel from 'lib/mixpanel/index';
import EmptySearchResult from './EmptySearchResult';
import MarketplaceAppCard from './MarketplaceAppCard';
+import type { RateFunction } from './Rating/useRatings';
type Props = {
apps: Array;
@@ -18,9 +20,19 @@ type Props = {
selectedCategoryId?: string;
onAppClick: (event: MouseEvent, id: string) => void;
showContractList: (id: string, type: ContractListTypes) => void;
+ userRatings: Record;
+ rateApp: RateFunction;
+ isRatingSending: boolean;
+ isRatingLoading: boolean;
+ canRate: boolean | undefined;
}
-const MarketplaceList = ({ apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId, onAppClick, showContractList }: Props) => {
+const MarketplaceList = ({
+ apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId,
+ onAppClick, showContractList, userRatings, rateApp, isRatingSending, isRatingLoading, canRate,
+}: Props) => {
+ const { cutRef, renderedItemsNum } = useLazyRenderedList(apps, !isLoading, 16);
+
const handleInfoClick = useCallback((id: string) => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Discovery view' });
showAppInfo(id);
@@ -31,37 +43,46 @@ const MarketplaceList = ({ apps, showAppInfo, favoriteApps, onFavoriteClick, isL
}, [ onFavoriteClick ]);
return apps.length > 0 ? (
-
- { apps.map((app, index) => (
-
- )) }
-
+ <>
+
+ { apps.slice(0, renderedItemsNum).map((app, index) => (
+
+ )) }
+
+
+ >
) : (
);
diff --git a/ui/marketplace/Rating/PopoverContent.tsx b/ui/marketplace/Rating/PopoverContent.tsx
new file mode 100644
index 0000000000..d86d31179d
--- /dev/null
+++ b/ui/marketplace/Rating/PopoverContent.tsx
@@ -0,0 +1,81 @@
+import { Text, Flex, Spinner } from '@chakra-ui/react';
+import React from 'react';
+
+import type { AppRating } from 'types/client/marketplace';
+
+import type { EventTypes, EventPayload } from 'lib/mixpanel/index';
+import IconSvg from 'ui/shared/IconSvg';
+
+import Stars from './Stars';
+import type { RateFunction } from './useRatings';
+
+const ratingDescriptions = [ 'Very bad', 'Bad', 'Average', 'Good', 'Excellent' ];
+
+type Props = {
+ appId: string;
+ rating?: AppRating;
+ userRating?: AppRating;
+ rate: RateFunction;
+ isSending?: boolean;
+ source: EventPayload['Source'];
+};
+
+const PopoverContent = ({ appId, rating, userRating, rate, isSending, source }: Props) => {
+ const [ hovered, setHovered ] = React.useState(-1);
+
+ const filledIndex = React.useMemo(() => {
+ if (hovered >= 0) {
+ return hovered;
+ }
+ return userRating?.value ? userRating?.value - 1 : -1;
+ }, [ userRating, hovered ]);
+
+ const handleMouseOverFactory = React.useCallback((index: number) => () => {
+ setHovered(index);
+ }, []);
+
+ const handleMouseOut = React.useCallback(() => {
+ setHovered(-1);
+ }, []);
+
+ const handleRateFactory = React.useCallback((index: number) => () => {
+ rate(appId, rating?.recordId, userRating?.recordId, index + 1, source);
+ }, [ appId, rating, rate, userRating, source ]);
+
+ if (isSending) {
+ return (
+
+
+ Sending your feedback
+
+ );
+ }
+
+ return (
+ <>
+
+ { userRating && (
+
+ ) }
+
+ { userRating ? 'App is already rated by you' : 'How was your experience?' }
+
+
+
+
+ { (filledIndex >= 0) && (
+
+ { ratingDescriptions[filledIndex] }
+
+ ) }
+
+ >
+ );
+};
+
+export default PopoverContent;
diff --git a/ui/marketplace/Rating/Rating.tsx b/ui/marketplace/Rating/Rating.tsx
new file mode 100644
index 0000000000..21d5bcf9b4
--- /dev/null
+++ b/ui/marketplace/Rating/Rating.tsx
@@ -0,0 +1,85 @@
+import { Text, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure, Skeleton, useOutsideClick, Box } from '@chakra-ui/react';
+import React from 'react';
+
+import type { AppRating } from 'types/client/marketplace';
+
+import config from 'configs/app';
+import type { EventTypes, EventPayload } from 'lib/mixpanel/index';
+import Popover from 'ui/shared/chakra/Popover';
+
+import Content from './PopoverContent';
+import Stars from './Stars';
+import TriggerButton from './TriggerButton';
+import type { RateFunction } from './useRatings';
+
+const feature = config.features.marketplace;
+const isEnabled = feature.isEnabled && feature.rating;
+
+type Props = {
+ appId: string;
+ rating?: AppRating;
+ userRating?: AppRating;
+ rate: RateFunction;
+ isSending?: boolean;
+ isLoading?: boolean;
+ fullView?: boolean;
+ canRate: boolean | undefined;
+ source: EventPayload['Source'];
+};
+
+const Rating = ({
+ appId, rating, userRating, rate,
+ isSending, isLoading, fullView, canRate, source,
+}: Props) => {
+ const { isOpen, onToggle, onClose } = useDisclosure();
+ // have to implement this solution because popover loses focus on button click inside it (issue: https://github.com/chakra-ui/chakra-ui/issues/7359)
+ const popoverRef = React.useRef(null);
+ useOutsideClick({ ref: popoverRef, handler: onClose });
+
+ if (!isEnabled) {
+ return null;
+ }
+
+ return (
+
+ { fullView && (
+ <>
+
+ { rating?.value }
+ >
+ ) }
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Rating;
diff --git a/ui/marketplace/Rating/Stars.tsx b/ui/marketplace/Rating/Stars.tsx
new file mode 100644
index 0000000000..ca2e8d0be4
--- /dev/null
+++ b/ui/marketplace/Rating/Stars.tsx
@@ -0,0 +1,38 @@
+import { Flex, useColorModeValue } from '@chakra-ui/react';
+import React from 'react';
+import type { MouseEventHandler } from 'react';
+
+import IconSvg from 'ui/shared/IconSvg';
+
+type Props = {
+ filledIndex: number;
+ onMouseOverFactory?: (index: number) => MouseEventHandler;
+ onMouseOut?: () => void;
+ onClickFactory?: (index: number) => MouseEventHandler;
+};
+
+const Stars = ({ filledIndex, onMouseOverFactory, onMouseOut, onClickFactory }: Props) => {
+ const disabledStarColor = useColorModeValue('gray.200', 'gray.700');
+ const outlineStartColor = onMouseOverFactory ? 'gray.400' : disabledStarColor;
+ return (
+
+ { Array(5).fill(null).map((_, index) => (
+ = index ? 'star_filled' : 'star_outline' }
+ color={ filledIndex >= index ? 'yellow.400' : outlineStartColor }
+ w={ 6 } // 5 + 1 padding
+ h={ 5 }
+ pr={ 1 } // use padding intead of margin so that there are no empty spaces between stars without hover effect
+ _last={{ w: 5, pr: 0 }}
+ cursor={ onMouseOverFactory ? 'pointer' : 'default' }
+ onMouseOver={ onMouseOverFactory?.(index) }
+ onMouseOut={ onMouseOut }
+ onClick={ onClickFactory?.(index) }
+ />
+ )) }
+
+ );
+};
+
+export default Stars;
diff --git a/ui/marketplace/Rating/TriggerButton.tsx b/ui/marketplace/Rating/TriggerButton.tsx
new file mode 100644
index 0000000000..309447a1b8
--- /dev/null
+++ b/ui/marketplace/Rating/TriggerButton.tsx
@@ -0,0 +1,89 @@
+import { Button, chakra, useColorModeValue, Tooltip, useDisclosure } from '@chakra-ui/react';
+import React from 'react';
+
+import useIsMobile from 'lib/hooks/useIsMobile';
+import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
+import IconSvg from 'ui/shared/IconSvg';
+
+type Props = {
+ rating?: number;
+ fullView?: boolean;
+ isActive: boolean;
+ onClick: () => void;
+ canRate: boolean | undefined;
+};
+
+const getTooltipText = (canRate: boolean | undefined) => {
+ if (canRate === undefined) {
+ return <>Please connect your wallet to Blockscout to rate this DApp. Only wallets with 5+ transactions are eligible>;
+ }
+ if (!canRate) {
+ return <>Brand new wallets cannot leave ratings. Please connect a wallet with 5 or more transactions on this chain>;
+ }
+ return <>Ratings come from verified users. Click here to rate!>;
+};
+
+const TriggerButton = (
+ { rating, fullView, isActive, onClick, canRate }: Props,
+ ref: React.ForwardedRef,
+) => {
+ const textColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
+ const onFocusCapture = usePreventFocusAfterModalClosing();
+
+ // have to implement controlled tooltip on mobile because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
+ const { isOpen, onToggle, onClose } = useDisclosure();
+ const isMobile = useIsMobile();
+
+ const handleClick = React.useCallback(() => {
+ if (canRate) {
+ onClick();
+ } else if (isMobile) {
+ onToggle();
+ }
+ }, [ canRate, isMobile, onToggle, onClick ]);
+
+ return (
+
+
+
+ );
+};
+
+export default React.forwardRef(TriggerButton);
diff --git a/ui/marketplace/Rating/useRatings.test.tsx b/ui/marketplace/Rating/useRatings.test.tsx
new file mode 100644
index 0000000000..ee8d71695d
--- /dev/null
+++ b/ui/marketplace/Rating/useRatings.test.tsx
@@ -0,0 +1,74 @@
+import { renderHook, wrapper } from 'jest/lib';
+
+import useRatings from './useRatings';
+
+const useAccount = jest.fn();
+const useApiQuery = jest.fn();
+
+jest.mock('lib/hooks/useToast', () => jest.fn());
+jest.mock('wagmi', () => ({ useAccount: () => useAccount() }));
+jest.mock('lib/api/useApiQuery', () => () => useApiQuery());
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+it('should set canRate to true if address is defined and transactions_count is 5 or more', async() => {
+ useAccount.mockReturnValue({ address: '0x123' });
+ useApiQuery.mockReturnValue({
+ isPlaceholderData: false,
+ data: { transactions_count: 5 },
+ });
+ const { result } = renderHook(() => useRatings(), { wrapper });
+ expect(result.current.canRate).toBe(true);
+});
+
+it('should set canRate to undefined if address is undefined', async() => {
+ useAccount.mockReturnValue({ address: undefined });
+ useApiQuery.mockReturnValue({
+ isPlaceholderData: false,
+ data: { transactions_count: 5 },
+ });
+ const { result } = renderHook(() => useRatings(), { wrapper });
+ expect(result.current.canRate).toBe(undefined);
+});
+
+it('should set canRate to false if transactions_count is less than 5', async() => {
+ useAccount.mockReturnValue({ address: '0x123' });
+ useApiQuery.mockReturnValue({
+ isPlaceholderData: false,
+ data: { transactions_count: 4 },
+ });
+ const { result } = renderHook(() => useRatings(), { wrapper });
+ expect(result.current.canRate).toBe(false);
+});
+
+it('should set canRate to false if isPlaceholderData is true', async() => {
+ useAccount.mockReturnValue({ address: '0x123' });
+ useApiQuery.mockReturnValue({
+ isPlaceholderData: true,
+ data: { transactions_count: 5 },
+ });
+ const { result } = renderHook(() => useRatings(), { wrapper });
+ expect(result.current.canRate).toBe(false);
+});
+
+it('should set canRate to false if data is undefined', async() => {
+ useAccount.mockReturnValue({ address: '0x123' });
+ useApiQuery.mockReturnValue({
+ isPlaceholderData: false,
+ data: undefined,
+ });
+ const { result } = renderHook(() => useRatings());
+ expect(result.current.canRate).toBe(false);
+});
+
+it('should set canRate to false if transactions_count is undefined', async() => {
+ useAccount.mockReturnValue({ address: '0x123' });
+ useApiQuery.mockReturnValue({
+ isPlaceholderData: false,
+ data: {},
+ });
+ const { result } = renderHook(() => useRatings());
+ expect(result.current.canRate).toBe(false);
+});
diff --git a/ui/marketplace/Rating/useRatings.tsx b/ui/marketplace/Rating/useRatings.tsx
new file mode 100644
index 0000000000..7c4ff1aa9f
--- /dev/null
+++ b/ui/marketplace/Rating/useRatings.tsx
@@ -0,0 +1,193 @@
+import Airtable from 'airtable';
+import { useEffect, useState, useCallback } from 'react';
+import { useAccount } from 'wagmi';
+
+import type { AppRating } from 'types/client/marketplace';
+
+import config from 'configs/app';
+import useApiQuery from 'lib/api/useApiQuery';
+import useToast from 'lib/hooks/useToast';
+import type { EventTypes, EventPayload } from 'lib/mixpanel/index';
+import * as mixpanel from 'lib/mixpanel/index';
+import { ADDRESS_COUNTERS } from 'stubs/address';
+
+const MIN_TRANSACTION_COUNT = 5;
+
+const feature = config.features.marketplace;
+const airtable = (feature.isEnabled && feature.rating) ?
+ new Airtable({ apiKey: feature.rating.airtableApiKey }).base(feature.rating.airtableBaseId) :
+ undefined;
+
+export type RateFunction = (
+ appId: string,
+ appRecordId: string | undefined,
+ userRecordId: string | undefined,
+ rating: number,
+ source: EventPayload['Source'],
+) => void;
+
+function formatRatings(data: Airtable.Records) {
+ return data.reduce((acc: Record, record) => {
+ const fields = record.fields as { appId: string | Array; rating: number | undefined };
+ const appId = Array.isArray(fields.appId) ? fields.appId[0] : fields.appId;
+ acc[appId] = {
+ recordId: record.id,
+ value: fields.rating,
+ };
+ return acc;
+ }, {});
+}
+
+export default function useRatings() {
+ const { address } = useAccount();
+ const toast = useToast();
+
+ const addressCountersQuery = useApiQuery<'address_counters', { status: number }>('address_counters', {
+ pathParams: { hash: address },
+ queryOptions: {
+ enabled: Boolean(address),
+ placeholderData: ADDRESS_COUNTERS,
+ refetchOnMount: false,
+ },
+ });
+
+ const [ ratings, setRatings ] = useState>({});
+ const [ userRatings, setUserRatings ] = useState>({});
+ const [ isRatingLoading, setIsRatingLoading ] = useState(false);
+ const [ isUserRatingLoading, setIsUserRatingLoading ] = useState(false);
+ const [ isSending, setIsSending ] = useState(false);
+ const [ canRate, setCanRate ] = useState(undefined);
+
+ const fetchRatings = useCallback(async() => {
+ if (!airtable) {
+ return;
+ }
+ try {
+ const data = await airtable('apps_ratings').select({ fields: [ 'appId', 'rating' ] }).all();
+ const ratings = formatRatings(data);
+ setRatings(ratings);
+ } catch (error) {
+ toast({
+ status: 'error',
+ title: 'Error loading ratings',
+ description: 'Please try again later',
+ });
+ }
+ }, [ toast ]);
+
+ useEffect(() => {
+ async function fetch() {
+ setIsRatingLoading(true);
+ await fetchRatings();
+ setIsRatingLoading(false);
+ }
+ fetch();
+ }, [ fetchRatings ]);
+
+ useEffect(() => {
+ async function fetchUserRatings() {
+ setIsUserRatingLoading(true);
+ let userRatings = {} as Record;
+ if (address && airtable) {
+ try {
+ const data = await airtable('users_ratings').select({
+ filterByFormula: `address = "${ address }"`,
+ fields: [ 'appId', 'rating' ],
+ }).all();
+ userRatings = formatRatings(data);
+ } catch (error) {
+ toast({
+ status: 'error',
+ title: 'Error loading user ratings',
+ description: 'Please try again later',
+ });
+ }
+ }
+ setUserRatings(userRatings);
+ setIsUserRatingLoading(false);
+ }
+ fetchUserRatings();
+ }, [ address, toast ]);
+
+ useEffect(() => {
+ const { isPlaceholderData, data } = addressCountersQuery;
+ const canRate = address && !isPlaceholderData && Number(data?.transactions_count) >= MIN_TRANSACTION_COUNT;
+ setCanRate(canRate);
+ }, [ address, addressCountersQuery ]);
+
+ const rateApp = useCallback(async(
+ appId: string,
+ appRecordId: string | undefined,
+ userRecordId: string | undefined,
+ rating: number,
+ source: EventPayload['Source'],
+ ) => {
+ setIsSending(true);
+
+ try {
+ if (!address || !airtable) {
+ throw new Error('Address is missing');
+ }
+
+ if (!appRecordId) {
+ const records = await airtable('apps_ratings').create([ { fields: { appId } } ]);
+ appRecordId = records[0].id;
+ if (!appRecordId) {
+ throw new Error('Record ID is missing');
+ }
+ }
+
+ if (!userRecordId) {
+ const userRecords = await airtable('users_ratings').create([
+ {
+ fields: {
+ address,
+ appRecordId: [ appRecordId ],
+ rating,
+ },
+ },
+ ]);
+ userRecordId = userRecords[0].id;
+ } else {
+ await airtable('users_ratings').update(userRecordId, { rating });
+ }
+
+ setUserRatings({
+ ...userRatings,
+ [appId]: {
+ recordId: userRecordId,
+ value: rating,
+ },
+ });
+ fetchRatings();
+
+ toast({
+ status: 'success',
+ title: 'Awesome! Thank you 💜',
+ description: 'Your rating improves the service',
+ });
+ mixpanel.logEvent(
+ mixpanel.EventTypes.APP_FEEDBACK,
+ { Action: 'Rating', Source: source, AppId: appId, Score: rating },
+ );
+ } catch (error) {
+ toast({
+ status: 'error',
+ title: 'Ooops! Something went wrong',
+ description: 'Please try again later',
+ });
+ }
+
+ setIsSending(false);
+ }, [ address, userRatings, fetchRatings, toast ]);
+
+ return {
+ ratings,
+ userRatings,
+ rateApp,
+ isRatingSending: isSending,
+ isRatingLoading,
+ isUserRatingLoading,
+ canRate,
+ };
+}
diff --git a/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_dark-color-mode_base-view-dark-mode-1.png
index df220dabd8..e917e49cca 100644
Binary files a/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_dark-color-mode_base-view-dark-mode-1.png and b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ
diff --git a/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_base-view-dark-mode-1.png b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_base-view-dark-mode-1.png
index f2e12cad51..b714324fa3 100644
Binary files a/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_base-view-dark-mode-1.png and b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_base-view-dark-mode-1.png differ
diff --git a/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_mobile-base-view-1.png b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_mobile-base-view-1.png
index 925173b47b..11b310da3c 100644
Binary files a/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_mobile-base-view-1.png and b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_mobile-base-view-1.png differ
diff --git a/ui/marketplace/useMarketplace.tsx b/ui/marketplace/useMarketplace.tsx
index c7db6567ab..8e4b793aa1 100644
--- a/ui/marketplace/useMarketplace.tsx
+++ b/ui/marketplace/useMarketplace.tsx
@@ -9,6 +9,7 @@ import useDebounce from 'lib/hooks/useDebounce';
import * as mixpanel from 'lib/mixpanel/index';
import getQueryParamString from 'lib/router/getQueryParamString';
+import useRatings from './Rating/useRatings';
import useMarketplaceApps from './useMarketplaceApps';
import useMarketplaceCategories from './useMarketplaceCategories';
@@ -85,9 +86,10 @@ export default function useMarketplace() {
setSelectedCategoryId(newCategory);
}, []);
+ const { ratings, userRatings, rateApp, isRatingSending, isRatingLoading, canRate } = useRatings();
const {
isPlaceholderData, isError, error, data, displayedApps, setSorting,
- } = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps, isFavoriteAppsLoaded);
+ } = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps, isFavoriteAppsLoaded, ratings);
const {
isPlaceholderData: isCategoriesPlaceholderData, data: categories,
} = useMarketplaceCategories(data, isPlaceholderData);
@@ -151,6 +153,11 @@ export default function useMarketplace() {
contractListModalType,
hasPreviousStep,
setSorting,
+ userRatings,
+ rateApp,
+ isRatingSending,
+ isRatingLoading,
+ canRate,
}), [
selectedCategoryId,
categories,
@@ -174,5 +181,10 @@ export default function useMarketplace() {
contractListModalType,
hasPreviousStep,
setSorting,
+ userRatings,
+ rateApp,
+ isRatingSending,
+ isRatingLoading,
+ canRate,
]);
}
diff --git a/ui/marketplace/useMarketplaceApps.tsx b/ui/marketplace/useMarketplaceApps.tsx
index f3c952b539..7d6730046c 100644
--- a/ui/marketplace/useMarketplaceApps.tsx
+++ b/ui/marketplace/useMarketplaceApps.tsx
@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/react-query';
import React from 'react';
-import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace';
+import type { MarketplaceAppWithSecurityReport, AppRating } from 'types/client/marketplace';
import { MarketplaceCategory } from 'types/client/marketplace';
import config from 'configs/app';
@@ -55,6 +55,7 @@ export default function useMarketplaceApps(
selectedCategoryId: string = MarketplaceCategory.ALL,
favoriteApps: Array | undefined = undefined,
isFavoriteAppsLoaded: boolean = false, // eslint-disable-line @typescript-eslint/no-inferrable-types
+ ratings: Record | undefined = undefined,
) {
const fetch = useFetch();
const apiFetch = useApiFetch();
@@ -91,20 +92,27 @@ export default function useMarketplaceApps(
const [ sorting, setSorting ] = React.useState();
- const appsWithSecurityReports = React.useMemo(() =>
- data?.map((app) => ({ ...app, securityReport: securityReports?.[app.id] })),
- [ data, securityReports ]);
+ const appsWithSecurityReportsAndRating = React.useMemo(() =>
+ data?.map((app) => ({
+ ...app,
+ securityReport: securityReports?.[app.id],
+ rating: ratings?.[app.id],
+ })),
+ [ data, securityReports, ratings ]);
const displayedApps = React.useMemo(() => {
- return appsWithSecurityReports
+ return appsWithSecurityReportsAndRating
?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps))
.sort((a, b) => {
if (sorting === 'security_score') {
return (b.securityReport?.overallInfo.securityScore || 0) - (a.securityReport?.overallInfo.securityScore || 0);
}
+ if (sorting === 'rating') {
+ return (b.rating?.value || 0) - (a.rating?.value || 0);
+ }
return 0;
}) || [];
- }, [ selectedCategoryId, appsWithSecurityReports, filter, favoriteApps, sorting ]);
+ }, [ selectedCategoryId, appsWithSecurityReportsAndRating, filter, favoriteApps, sorting ]);
return React.useMemo(() => ({
data,
diff --git a/ui/marketplace/utils.ts b/ui/marketplace/utils.ts
index 09cb0ec28f..3f0fb9538b 100644
--- a/ui/marketplace/utils.ts
+++ b/ui/marketplace/utils.ts
@@ -4,10 +4,11 @@ import getQueryParamString from 'lib/router/getQueryParamString';
import removeQueryParam from 'lib/router/removeQueryParam';
import type { TOption } from 'ui/shared/sort/Option';
-export type SortValue = 'security_score';
+export type SortValue = 'rating' | 'security_score';
export const SORT_OPTIONS: Array> = [
{ title: 'Default', id: undefined },
+ { title: 'Rating', id: 'rating' },
{ title: 'Security score', id: 'security_score' },
];
diff --git a/ui/messages/ArbitrumL2MessagesListItem.tsx b/ui/messages/ArbitrumL2MessagesListItem.tsx
index e00543b440..422366dac0 100644
--- a/ui/messages/ArbitrumL2MessagesListItem.tsx
+++ b/ui/messages/ArbitrumL2MessagesListItem.tsx
@@ -4,13 +4,13 @@ import React from 'react';
import type { ArbitrumL2MessagesItem } from 'types/api/arbitrumL2';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import ArbitrumL2MessageStatus from 'ui/shared/statusTag/ArbitrumL2MessageStatus';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
import type { MessagesDirection } from './ArbitrumL2Messages';
@@ -23,8 +23,6 @@ const ArbitrumL2MessagesListItem = ({ item, isLoading, direction }: Props) => {
return null;
}
- const timeAgo = dayjs(item.origination_timestamp).fromNow();
-
const l1TxHash = direction === 'from-rollup' ? item.completion_transaction_hash : item.origination_transaction_hash;
const l2TxHash = direction === 'from-rollup' ? item.origination_transaction_hash : item.completion_transaction_hash;
@@ -88,7 +86,11 @@ const ArbitrumL2MessagesListItem = ({ item, isLoading, direction }: Props) => {
Age
- { timeAgo }
+
Status
diff --git a/ui/messages/ArbitrumL2MessagesTableItem.tsx b/ui/messages/ArbitrumL2MessagesTableItem.tsx
index ed46084461..fbd4f13ef9 100644
--- a/ui/messages/ArbitrumL2MessagesTableItem.tsx
+++ b/ui/messages/ArbitrumL2MessagesTableItem.tsx
@@ -4,12 +4,12 @@ import React from 'react';
import type { ArbitrumL2MessagesItem } from 'types/api/arbitrumL2';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import ArbitrumL2MessageStatus from 'ui/shared/statusTag/ArbitrumL2MessageStatus';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
import type { MessagesDirection } from './ArbitrumL2Messages';
@@ -22,8 +22,6 @@ const ArbitrumL2MessagesTableItem = ({ item, direction, isLoading }: Props) => {
return null;
}
- const timeAgo = dayjs(item.origination_timestamp).fromNow();
-
const l1TxHash = direction === 'from-rollup' ? item.completion_transaction_hash : item.origination_transaction_hash;
const l2TxHash = direction === 'from-rollup' ? item.origination_transaction_hash : item.completion_transaction_hash;
@@ -75,9 +73,11 @@ const ArbitrumL2MessagesTableItem = ({ item, direction, isLoading }: Props) => {
) }
|
-
- { timeAgo }
-
+
|
diff --git a/ui/nameDomain/history/NameDomainHistoryListItem.tsx b/ui/nameDomain/history/NameDomainHistoryListItem.tsx
index b14a3b65a3..ebc43e607e 100644
--- a/ui/nameDomain/history/NameDomainHistoryListItem.tsx
+++ b/ui/nameDomain/history/NameDomainHistoryListItem.tsx
@@ -1,4 +1,3 @@
-import { Skeleton } from '@chakra-ui/react';
import React from 'react';
import type * as bens from '@blockscout/bens-types';
@@ -6,12 +5,12 @@ import type * as bens from '@blockscout/bens-types';
import { route } from 'nextjs-routes';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import Tag from 'ui/shared/chakra/Tag';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
interface Props {
event: bens.DomainEvent;
@@ -38,9 +37,12 @@ const NameDomainHistoryListItem = ({ isLoading, domain, event }: Props) => {
Age
-
- { dayjs(event.timestamp).fromNow() }
-
+
{ event.from_address && (
diff --git a/ui/nameDomain/history/NameDomainHistoryTableItem.tsx b/ui/nameDomain/history/NameDomainHistoryTableItem.tsx
index 41e2d76f16..cd287cc98d 100644
--- a/ui/nameDomain/history/NameDomainHistoryTableItem.tsx
+++ b/ui/nameDomain/history/NameDomainHistoryTableItem.tsx
@@ -1,4 +1,4 @@
-import { Tr, Td, Skeleton } from '@chakra-ui/react';
+import { Tr, Td } from '@chakra-ui/react';
import React from 'react';
import type * as bens from '@blockscout/bens-types';
@@ -6,11 +6,11 @@ import type * as bens from '@blockscout/bens-types';
import { route } from 'nextjs-routes';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import Tag from 'ui/shared/chakra/Tag';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
interface Props {
event: bens.DomainEvent;
@@ -41,9 +41,12 @@ const NameDomainHistoryTableItem = ({ isLoading, event, domain }: Props) => {
/>
|
-
- { dayjs(event.timestamp).fromNow() }
-
+
|
{ event.from_address && }
diff --git a/ui/outputRoots/optimisticL2/OptimisticL2OutputRootsListItem.tsx b/ui/outputRoots/optimisticL2/OptimisticL2OutputRootsListItem.tsx
index 2e61672773..76140ce1f6 100644
--- a/ui/outputRoots/optimisticL2/OptimisticL2OutputRootsListItem.tsx
+++ b/ui/outputRoots/optimisticL2/OptimisticL2OutputRootsListItem.tsx
@@ -4,20 +4,18 @@ import React from 'react';
import type { OptimisticL2OutputRootsItem } from 'types/api/optimisticL2';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShorten from 'ui/shared/HashStringShorten';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
type Props = { item: OptimisticL2OutputRootsItem; isLoading?: boolean };
const OptimisticL2OutputRootsListItem = ({ item, isLoading }: Props) => {
- const timeAgo = dayjs(item.l1_timestamp).fromNow();
-
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
}
@@ -32,9 +30,11 @@ const OptimisticL2OutputRootsListItem = ({ item, isLoading }: Props) => {
Age
-
- { timeAgo }
-
+
L2 block #
diff --git a/ui/outputRoots/optimisticL2/OptimisticL2OutputRootsTableItem.tsx b/ui/outputRoots/optimisticL2/OptimisticL2OutputRootsTableItem.tsx
index 6a2e6d9fb5..25fb2d7da8 100644
--- a/ui/outputRoots/optimisticL2/OptimisticL2OutputRootsTableItem.tsx
+++ b/ui/outputRoots/optimisticL2/OptimisticL2OutputRootsTableItem.tsx
@@ -4,19 +4,17 @@ import React from 'react';
import type { OptimisticL2OutputRootsItem } from 'types/api/optimisticL2';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShorten from 'ui/shared/HashStringShorten';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
type Props = { item: OptimisticL2OutputRootsItem; isLoading?: boolean };
const OptimisticL2OutputRootsTableItem = ({ item, isLoading }: Props) => {
- const timeAgo = dayjs(item.l1_timestamp).fromNow();
-
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
}
@@ -27,7 +25,12 @@ const OptimisticL2OutputRootsTableItem = ({ item, isLoading }: Props) => {
{ item.l2_output_index }
|
- { timeAgo }
+
|
{
+test.beforeEach(async({ mockConfigResponse, mockEnvs, mockAssetResponse, page }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ],
[ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL ],
[ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL ],
+ [ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY', 'test' ],
+ [ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID', 'test' ],
]);
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL, JSON.stringify(appsMock));
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, JSON.stringify(securityReportsMock));
await Promise.all(appsMock.map(app => mockAssetResponse(app.logo, './playwright/mocks/image_s.jpg')));
+ await page.route('https://api.airtable.com/v0/test/apps_ratings?fields%5B%5D=appId&fields%5B%5D=rating', (route) => route.fulfill({
+ status: 200,
+ body: JSON.stringify(ratingsMock),
+ }));
});
test('base view +@dark-mode', async({ render }) => {
diff --git a/ui/pages/Marketplace.tsx b/ui/pages/Marketplace.tsx
index a6e9c6f98d..aeab726593 100644
--- a/ui/pages/Marketplace.tsx
+++ b/ui/pages/Marketplace.tsx
@@ -71,6 +71,11 @@ const Marketplace = () => {
contractListModalType,
hasPreviousStep,
setSorting,
+ userRatings,
+ rateApp,
+ isRatingSending,
+ isRatingLoading,
+ canRate,
} = useMarketplace();
const isMobile = useIsMobile();
@@ -92,13 +97,13 @@ const Marketplace = () => {
tabs.unshift({
id: MarketplaceCategory.FAVORITES,
- title: () => ,
- count: null,
+ title: () => ,
+ count: favoriteApps.length,
component: null,
});
return tabs;
- }, [ categories, appsTotal ]);
+ }, [ categories, appsTotal, favoriteApps.length ]);
const selectedCategoryIndex = React.useMemo(() => {
const index = categoryTabs.findIndex(c => c.id === selectedCategoryId);
@@ -224,6 +229,11 @@ const Marketplace = () => {
selectedCategoryId={ selectedCategoryId }
onAppClick={ handleAppClick }
showContractList={ showContractList }
+ userRatings={ userRatings }
+ rateApp={ rateApp }
+ isRatingSending={ isRatingSending }
+ isRatingLoading={ isRatingLoading }
+ canRate={ canRate }
/>
{ (selectedApp && isAppInfoModalOpen) && (
@@ -233,6 +243,11 @@ const Marketplace = () => {
onFavoriteClick={ onFavoriteClick }
data={ selectedApp }
showContractList={ showContractList }
+ userRating={ userRatings[selectedApp.id] }
+ rateApp={ rateApp }
+ isRatingSending={ isRatingSending }
+ isRatingLoading={ isRatingLoading }
+ canRate={ canRate }
/>
) }
diff --git a/ui/pages/MarketplaceApp.pw.tsx b/ui/pages/MarketplaceApp.pw.tsx
index 94b0811089..bebab02457 100644
--- a/ui/pages/MarketplaceApp.pw.tsx
+++ b/ui/pages/MarketplaceApp.pw.tsx
@@ -4,6 +4,8 @@ import { numberToHex } from 'viem';
import config from 'configs/app';
import { apps as appsMock } from 'mocks/apps/apps';
+import { ratings as ratingsMock } from 'mocks/apps/ratings';
+import { securityReports as securityReportsMock } from 'mocks/apps/securityReports';
import { test, expect, devices } from 'playwright/lib';
import MarketplaceApp from './MarketplaceApp';
@@ -16,18 +18,27 @@ const hooksConfig = {
};
const MARKETPLACE_CONFIG_URL = 'https://marketplace-config.json';
+const MARKETPLACE_SECURITY_REPORTS_URL = 'https://marketplace-security-reports.json';
-const testFn: Parameters[1] = async({ render, mockConfigResponse, mockAssetResponse, mockEnvs, mockRpcResponse }) => {
+const testFn: Parameters[1] = async({ render, mockConfigResponse, mockAssetResponse, mockEnvs, mockRpcResponse, page }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ],
[ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL ],
+ [ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL ],
+ [ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY', 'test' ],
+ [ 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID', 'test' ],
]);
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL, JSON.stringify(appsMock));
+ await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, JSON.stringify(securityReportsMock));
await mockAssetResponse(appsMock[0].url, './mocks/apps/app.html');
await mockRpcResponse({
Method: 'eth_chainId',
ReturnType: numberToHex(Number(config.chain.id)),
});
+ await page.route('https://api.airtable.com/v0/test/apps_ratings?fields%5B%5D=appId&fields%5B%5D=rating', (route) => route.fulfill({
+ status: 200,
+ body: JSON.stringify(ratingsMock),
+ }));
const component = await render(
diff --git a/ui/pages/MarketplaceApp.tsx b/ui/pages/MarketplaceApp.tsx
index b1e1893785..78e14ca4e3 100644
--- a/ui/pages/MarketplaceApp.tsx
+++ b/ui/pages/MarketplaceApp.tsx
@@ -151,6 +151,7 @@ const MarketplaceApp = () => {
return (
address ? [ address ] : [], [ address ]);
const { data } = useAddressMetadataInfoQuery(memoizedArray, isEnabled);
const metadata = data?.addresses[address?.toLowerCase()];
diff --git a/ui/shared/HashStringShortenDynamic.tsx b/ui/shared/HashStringShortenDynamic.tsx
index 43a7bb79f8..cb1130662d 100644
--- a/ui/shared/HashStringShortenDynamic.tsx
+++ b/ui/shared/HashStringShortenDynamic.tsx
@@ -95,7 +95,7 @@ const HashStringShortenDynamic = ({ hash, fontWeight = '400', isTooltipDisabled,
if (isTruncated) {
return (
- { content }
+ { content }
);
}
diff --git a/ui/shared/Hint.tsx b/ui/shared/Hint.tsx
index e7fa1f8590..5aac2b52e5 100644
--- a/ui/shared/Hint.tsx
+++ b/ui/shared/Hint.tsx
@@ -28,7 +28,7 @@ const Hint = ({ label, className, tooltipProps, isLoading }: Props) => {
diff --git a/ui/shared/TimeAgoWithTooltip.tsx b/ui/shared/TimeAgoWithTooltip.tsx
new file mode 100644
index 0000000000..a8e4c0d6d4
--- /dev/null
+++ b/ui/shared/TimeAgoWithTooltip.tsx
@@ -0,0 +1,32 @@
+import { Skeleton, Tooltip, chakra } from '@chakra-ui/react';
+import React from 'react';
+
+import dayjs from 'lib/date/dayjs';
+import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
+
+type Props = {
+ timestamp?: string | null;
+ fallbackText?: string;
+ isLoading?: boolean;
+ enableIncrement?: boolean;
+ className?: string;
+}
+
+const TimeAgoWithTooltip = ({ timestamp, fallbackText, isLoading, enableIncrement, className }: Props) => {
+ const timeAgo = useTimeAgoIncrement(timestamp || '', enableIncrement && !isLoading);
+ if (!timestamp && !fallbackText) {
+ return null;
+ }
+
+ const content = timestamp ?
+ { timeAgo } :
+ { fallbackText };
+
+ return (
+
+ { content }
+
+ );
+};
+
+export default chakra(TimeAgoWithTooltip);
diff --git a/ui/shared/TokenTransfer/TokenTransferListItem.tsx b/ui/shared/TokenTransfer/TokenTransferListItem.tsx
index 1c0ba064a3..03977f0916 100644
--- a/ui/shared/TokenTransfer/TokenTransferListItem.tsx
+++ b/ui/shared/TokenTransfer/TokenTransferListItem.tsx
@@ -4,7 +4,6 @@ import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import getCurrencyValue from 'lib/getCurrencyValue';
-import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import { getTokenTypeName } from 'lib/token/tokenTypes';
import AddressFromTo from 'ui/shared/address/AddressFromTo';
import Tag from 'ui/shared/chakra/Tag';
@@ -15,6 +14,8 @@ import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
+import TimeAgoWithTooltip from '../TimeAgoWithTooltip';
+
type Props = TokenTransfer & {
baseAddress?: string;
showTxInfo?: boolean;
@@ -35,7 +36,6 @@ const TokenTransferListItem = ({
enableTimeIncrement,
isLoading,
}: Props) => {
- const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement);
const { usd, valueStr } = 'value' in total && total.value !== null ? getCurrencyValue({
value: total.value,
exchangeRate: token.exchange_rate,
@@ -71,11 +71,14 @@ const TokenTransferListItem = ({
truncation="constant_long"
fontWeight="700"
/>
- { timestamp && (
-
- { timeAgo }
-
- ) }
+
) }
{
- const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement);
const { usd, valueStr } = 'value' in total && total.value !== null ? getCurrencyValue({
value: total.value,
exchangeRate: token.exchange_rate,
@@ -78,11 +78,15 @@ const TokenTransferTableItem = ({
mt="7px"
truncation="constant_long"
/>
- { timestamp && (
-
- { timeAgo }
-
- ) }
+
|
) }
diff --git a/ui/shared/TruncatedTextTooltip.tsx b/ui/shared/TruncatedTextTooltip.tsx
index e1f333d5e1..eaf7dfdd66 100644
--- a/ui/shared/TruncatedTextTooltip.tsx
+++ b/ui/shared/TruncatedTextTooltip.tsx
@@ -79,7 +79,7 @@ const TruncatedTextTooltip = ({ children, label, placement }: Props) => {
return (
diff --git a/ui/shared/block/BlockGasUsed.tsx b/ui/shared/block/BlockGasUsed.tsx
new file mode 100644
index 0000000000..0c1ee6f416
--- /dev/null
+++ b/ui/shared/block/BlockGasUsed.tsx
@@ -0,0 +1,54 @@
+import { chakra, Tooltip, Box, useColorModeValue } from '@chakra-ui/react';
+import BigNumber from 'bignumber.js';
+import React from 'react';
+
+import config from 'configs/app';
+
+import GasUsedToTargetRatio from '../GasUsedToTargetRatio';
+import TextSeparator from '../TextSeparator';
+import Utilization from '../Utilization/Utilization';
+
+const rollupFeature = config.features.rollup;
+
+interface Props {
+ className?: string;
+ gasUsed?: string;
+ gasLimit: string;
+ gasTarget?: number;
+ isLoading?: boolean;
+}
+
+const BlockGasUsed = ({ className, gasUsed, gasLimit, gasTarget, isLoading }: Props) => {
+ const hasGasUtilization =
+ gasUsed && gasUsed !== '0' &&
+ (!rollupFeature.isEnabled || rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium');
+
+ const separatorColor = useColorModeValue('gray.200', 'gray.700');
+
+ if (!hasGasUtilization) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+
+
+
+ { gasTarget && (
+ <>
+
+
+ >
+ ) }
+ >
+ );
+};
+
+export default React.memo(chakra(BlockGasUsed));
diff --git a/ui/shared/chart/ChartArea.tsx b/ui/shared/chart/ChartArea.tsx
index 479e010508..5bb03281a5 100644
--- a/ui/shared/chart/ChartArea.tsx
+++ b/ui/shared/chart/ChartArea.tsx
@@ -11,10 +11,10 @@ interface Props extends React.SVGProps {
yScale: d3.ScaleTime | d3.ScaleLinear;
color?: string;
data: Array;
- disableAnimation?: boolean;
+ noAnimation?: boolean;
}
-const ChartArea = ({ id, xScale, yScale, color, data, disableAnimation, ...props }: Props) => {
+const ChartArea = ({ id, xScale, yScale, color, data, noAnimation, ...props }: Props) => {
const ref = React.useRef(null);
const theme = useTheme();
@@ -26,7 +26,7 @@ const ChartArea = ({ id, xScale, yScale, color, data, disableAnimation, ...props
};
React.useEffect(() => {
- if (disableAnimation) {
+ if (noAnimation) {
d3.select(ref.current).attr('opacity', 1);
return;
}
@@ -34,10 +34,11 @@ const ChartArea = ({ id, xScale, yScale, color, data, disableAnimation, ...props
.duration(750)
.ease(d3.easeBackIn)
.attr('opacity', 1);
- }, [ disableAnimation ]);
+ }, [ noAnimation ]);
const d = React.useMemo(() => {
const area = d3.area()
+ .defined(({ isApproximate }) => !isApproximate)
.x(({ date }) => xScale(date))
.y1(({ value }) => yScale(value))
.y0(() => yScale(yScale.domain()[0]))
diff --git a/ui/shared/chart/ChartAxis.tsx b/ui/shared/chart/ChartAxis.tsx
index 487e3ace61..203ba65ccf 100644
--- a/ui/shared/chart/ChartAxis.tsx
+++ b/ui/shared/chart/ChartAxis.tsx
@@ -5,13 +5,13 @@ import React from 'react';
interface Props extends Omit, 'scale'> {
type: 'left' | 'bottom';
scale: d3.ScaleTime | d3.ScaleLinear;
- disableAnimation?: boolean;
+ noAnimation?: boolean;
ticks: number;
tickFormatGenerator?: (axis: d3.Axis) => (domainValue: d3.AxisDomain, index: number) => string;
anchorEl?: SVGRectElement | null;
}
-const ChartAxis = ({ type, scale, ticks, tickFormatGenerator, disableAnimation, anchorEl, ...props }: Props) => {
+const ChartAxis = ({ type, scale, ticks, tickFormatGenerator, noAnimation, anchorEl, ...props }: Props) => {
const ref = React.useRef(null);
const textColorToken = useColorModeValue('blackAlpha.600', 'whiteAlpha.500');
@@ -31,7 +31,7 @@ const ChartAxis = ({ type, scale, ticks, tickFormatGenerator, disableAnimation,
const axisGroup = d3.select(ref.current);
- if (disableAnimation) {
+ if (noAnimation) {
axisGroup.call(axis);
} else {
axisGroup.transition().duration(750).ease(d3.easeLinear).call(axis);
@@ -42,7 +42,7 @@ const ChartAxis = ({ type, scale, ticks, tickFormatGenerator, disableAnimation,
.attr('opacity', 1)
.attr('color', textColor)
.attr('font-size', '0.75rem');
- }, [ scale, ticks, tickFormatGenerator, disableAnimation, type, textColor ]);
+ }, [ scale, ticks, tickFormatGenerator, noAnimation, type, textColor ]);
React.useEffect(() => {
if (!anchorEl) {
diff --git a/ui/shared/chart/ChartGridLine.tsx b/ui/shared/chart/ChartGridLine.tsx
index 2d140f25c1..3756b9239b 100644
--- a/ui/shared/chart/ChartGridLine.tsx
+++ b/ui/shared/chart/ChartGridLine.tsx
@@ -5,12 +5,12 @@ import React from 'react';
interface Props extends Omit, 'scale'> {
type: 'vertical' | 'horizontal';
scale: d3.ScaleTime | d3.ScaleLinear;
- disableAnimation?: boolean;
+ noAnimation?: boolean;
size: number;
ticks: number;
}
-const ChartGridLine = ({ type, scale, ticks, size, disableAnimation, ...props }: Props) => {
+const ChartGridLine = ({ type, scale, ticks, size, noAnimation, ...props }: Props) => {
const ref = React.useRef(null);
const strokeColor = useToken('colors', 'divider');
@@ -24,7 +24,7 @@ const ChartGridLine = ({ type, scale, ticks, size, disableAnimation, ...props }:
const axis = axisGenerator(scale).ticks(ticks).tickSize(-size);
const gridGroup = d3.select(ref.current);
- if (disableAnimation) {
+ if (noAnimation) {
gridGroup.call(axis);
} else {
gridGroup.transition().duration(750).ease(d3.easeLinear).call(axis);
@@ -32,7 +32,7 @@ const ChartGridLine = ({ type, scale, ticks, size, disableAnimation, ...props }:
gridGroup.select('.domain').remove();
gridGroup.selectAll('text').remove();
gridGroup.selectAll('line').attr('stroke', strokeColor);
- }, [ scale, ticks, size, disableAnimation, type, strokeColor ]);
+ }, [ scale, ticks, size, noAnimation, type, strokeColor ]);
return ;
};
diff --git a/ui/shared/chart/ChartLine.tsx b/ui/shared/chart/ChartLine.tsx
index a96b7e84c5..2d34add516 100644
--- a/ui/shared/chart/ChartLine.tsx
+++ b/ui/shared/chart/ChartLine.tsx
@@ -3,56 +3,38 @@ import React from 'react';
import type { TimeChartItem } from 'ui/shared/chart/types';
+import type { AnimationType } from './utils/animations';
+import { ANIMATIONS } from './utils/animations';
+import { getIncompleteDataLineSource } from './utils/formatters';
+
interface Props extends React.SVGProps {
xScale: d3.ScaleTime | d3.ScaleLinear;
yScale: d3.ScaleTime | d3.ScaleLinear;
data: Array;
- animation: 'left' | 'fadeIn' | 'none';
+ animation: AnimationType;
}
const ChartLine = ({ xScale, yScale, data, animation, ...props }: Props) => {
- const ref = React.useRef(null);
-
- // Define different types of animation that we can use
- const animateLeft = React.useCallback(() => {
- const totalLength = ref.current?.getTotalLength() || 0;
- d3.select(ref.current)
- .attr('opacity', 1)
- .attr('stroke-dasharray', `${ totalLength },${ totalLength }`)
- .attr('stroke-dashoffset', totalLength)
- .transition()
- .duration(750)
- .ease(d3.easeLinear)
- .attr('stroke-dashoffset', 0);
- }, []);
-
- const animateFadeIn = React.useCallback(() => {
- d3.select(ref.current)
- .transition()
- .duration(750)
- .ease(d3.easeLinear)
- .attr('opacity', 1);
- }, []);
-
- const noneAnimation = React.useCallback(() => {
- d3.select(ref.current).attr('opacity', 1);
- }, []);
+ const dataPathRef = React.useRef(null);
+ const incompleteDataPathRef = React.useRef(null);
React.useEffect(() => {
- const ANIMATIONS = {
- left: animateLeft,
- fadeIn: animateFadeIn,
- none: noneAnimation,
- };
const animationFn = ANIMATIONS[animation];
- window.setTimeout(animationFn, 100);
- }, [ animateLeft, animateFadeIn, noneAnimation, animation ]);
+ const timeoutId = window.setTimeout(() => {
+ dataPathRef.current && animationFn(dataPathRef.current);
+ incompleteDataPathRef.current && animationFn(incompleteDataPathRef.current);
+ }, 100);
+
+ return () => {
+ window.clearTimeout(timeoutId);
+ };
+ }, [ animation ]);
// Recalculate line length if scale has changed
React.useEffect(() => {
if (animation === 'left') {
- const totalLength = ref.current?.getTotalLength();
- d3.select(ref.current).attr(
+ const totalLength = dataPathRef.current?.getTotalLength();
+ d3.select(dataPathRef.current).attr(
'stroke-dasharray',
`${ totalLength },${ totalLength }`,
);
@@ -65,15 +47,27 @@ const ChartLine = ({ xScale, yScale, data, animation, ...props }: Props) => {
.curve(d3.curveMonotoneX);
return (
-
+ <>
+
+ !isApproximate)) || undefined }
+ strokeWidth={ 1 }
+ strokeLinecap="round"
+ fill="none"
+ opacity={ 0 }
+ { ...props }
+ />
+ >
);
};
diff --git a/ui/shared/chart/ChartTooltip.tsx b/ui/shared/chart/ChartTooltip.tsx
index e257b5a519..71e51ab738 100644
--- a/ui/shared/chart/ChartTooltip.tsx
+++ b/ui/shared/chart/ChartTooltip.tsx
@@ -1,12 +1,16 @@
-import { useToken, useColorModeValue } from '@chakra-ui/react';
import * as d3 from 'd3';
import React from 'react';
-import type { TimeChartItem, TimeChartData } from 'ui/shared/chart/types';
+import type { TimeChartData } from 'ui/shared/chart/types';
-import computeTooltipPosition from 'ui/shared/chart/utils/computeTooltipPosition';
-import type { Pointer } from 'ui/shared/chart/utils/pointerTracker';
-import { trackPointer } from 'ui/shared/chart/utils/pointerTracker';
+import ChartTooltipBackdrop, { useRenderBackdrop } from './tooltip/ChartTooltipBackdrop';
+import ChartTooltipContent, { useRenderContent } from './tooltip/ChartTooltipContent';
+import ChartTooltipLine, { useRenderLine } from './tooltip/ChartTooltipLine';
+import ChartTooltipPoint, { useRenderPoints } from './tooltip/ChartTooltipPoint';
+import ChartTooltipRow, { useRenderRows } from './tooltip/ChartTooltipRow';
+import ChartTooltipTitle, { useRenderTitle } from './tooltip/ChartTooltipTitle';
+import { trackPointer } from './tooltip/pointerTracker';
+import type { Pointer } from './tooltip/pointerTracker';
interface Props {
width?: number;
@@ -16,151 +20,62 @@ interface Props {
xScale: d3.ScaleTime;
yScale: d3.ScaleLinear;
anchorEl: SVGRectElement | null;
+ noAnimation?: boolean;
}
-const TEXT_LINE_HEIGHT = 12;
-const PADDING = 16;
-const LINE_SPACE = 10;
-const POINT_SIZE = 16;
-const LABEL_WIDTH = 80;
-
-const ChartTooltip = ({ xScale, yScale, width, tooltipWidth = 200, height, data, anchorEl, ...props }: Props) => {
- const lineColor = useToken('colors', 'gray.400');
- const titleColor = useToken('colors', 'blue.100');
- const textColor = useToken('colors', 'white');
- const markerBgColor = useToken('colors', useColorModeValue('black', 'white'));
- const markerBorderColor = useToken('colors', useColorModeValue('white', 'black'));
- const bgColor = useToken('colors', 'blackAlpha.900');
-
- const ref = React.useRef(null);
+const ChartTooltip = ({ xScale, yScale, width, tooltipWidth = 200, height, data, anchorEl, noAnimation, ...props }: Props) => {
+ const ref = React.useRef(null);
const trackerId = React.useRef();
const isVisible = React.useRef(false);
- const drawLine = React.useCallback(
- (x: number) => {
- d3.select(ref.current)
- .select('.ChartTooltip__line')
- .attr('x1', x)
- .attr('x2', x)
- .attr('y1', 0)
- .attr('y2', height || 0);
- },
- [ ref, height ],
- );
-
- const drawContent = React.useCallback(
- (x: number, y: number) => {
- const tooltipContent = d3.select(ref.current).select('.ChartTooltip__content');
-
- tooltipContent.attr('transform', (cur, i, nodes) => {
- const node = nodes[i] as SVGGElement | null;
- const { width: nodeWidth, height: nodeHeight } = node?.getBoundingClientRect() || { width: 0, height: 0 };
- const [ translateX, translateY ] = computeTooltipPosition({
- canvasWidth: width || 0,
- canvasHeight: height || 0,
- nodeWidth,
- nodeHeight,
- pointX: x,
- pointY: y,
- offset: POINT_SIZE,
- });
- return `translate(${ translateX }, ${ translateY })`;
- });
-
- const date = xScale.invert(x);
- const dateLabel = data[0].items.find((item) => item.date.getTime() === date.getTime())?.dateLabel;
-
- tooltipContent
- .select('.ChartTooltip__contentDate')
- .text(dateLabel || d3.timeFormat('%e %b %Y')(xScale.invert(x)));
- },
- [ xScale, data, width, height ],
- );
-
- const updateDisplayedValue = React.useCallback((d: TimeChartItem, i: number) => {
- const nodes = d3.select(ref.current)
- .selectAll('.ChartTooltip__value')
- .filter((td, tIndex) => tIndex === i)
- .text(
- (data[i].valueFormatter?.(d.value) || d.value.toLocaleString(undefined, { minimumSignificantDigits: 1 })) +
- (data[i].units ? ` ${ data[i].units }` : ''),
- )
- .nodes();
-
- const widthLimit = tooltipWidth - 2 * PADDING - LABEL_WIDTH;
- const width = nodes.map((node) => node?.getBoundingClientRect?.().width);
- const maxNodeWidth = Math.max(...width);
- d3.select(ref.current)
- .select('.ChartTooltip__contentBg')
- .attr('width', tooltipWidth + Math.max(0, (maxNodeWidth - widthLimit)));
-
- }, [ data, tooltipWidth ]);
-
- const drawPoints = React.useCallback((x: number) => {
- const xDate = xScale.invert(x);
- const bisectDate = d3.bisector((d) => d.date).left;
- let baseXPos = 0;
- let baseYPos = 0;
+ const transitionDuration = !noAnimation ? 100 : null;
- d3.select(ref.current)
- .selectAll('.ChartTooltip__point')
- .attr('transform', (cur, i) => {
- const index = bisectDate(data[i].items, xDate, 1);
- const d0 = data[i].items[index - 1] as TimeChartItem | undefined;
- const d1 = data[i].items[index] as TimeChartItem | undefined;
- const d = (() => {
- if (!d0) {
- return d1;
- }
- if (!d1) {
- return d0;
- }
- return xDate.getTime() - d0.date.getTime() > d1.date.getTime() - xDate.getTime() ? d1 : d0;
- })();
-
- if (d?.date === undefined && d?.value === undefined) {
- // move point out of container
- return 'translate(-100,-100)';
- }
-
- const xPos = xScale(d.date);
- const yPos = yScale(d.value);
-
- if (i === 0) {
- baseXPos = xPos;
- baseYPos = yPos;
- }
-
- updateDisplayedValue(d, i);
-
- return `translate(${ xPos }, ${ yPos })`;
- });
-
- return [ baseXPos, baseYPos ];
- }, [ data, updateDisplayedValue, xScale, yScale ]);
+ const renderLine = useRenderLine(ref, height);
+ const renderContent = useRenderContent(ref, { chart: { width, height }, transitionDuration });
+ const renderPoints = useRenderPoints(ref, { data, xScale, yScale });
+ const renderTitle = useRenderTitle(ref);
+ const renderRows = useRenderRows(ref, { data, xScale, minWidth: tooltipWidth });
+ const renderBackdrop = useRenderBackdrop(ref, { seriesNum: data.length, transitionDuration });
const draw = React.useCallback((pointer: Pointer) => {
if (pointer.point) {
- const [ baseXPos, baseYPos ] = drawPoints(pointer.point[0]);
- drawLine(baseXPos);
- drawContent(baseXPos, baseYPos);
+ const { x, y, currentPoints } = renderPoints(pointer.point[0]);
+ const isIncompleteData = currentPoints.some(({ item }) => item.isApproximate);
+ renderLine(x);
+ renderContent(x, y);
+ renderTitle(isIncompleteData);
+ const { width } = renderRows(x, currentPoints);
+ renderBackdrop(width, isIncompleteData);
}
- }, [ drawPoints, drawLine, drawContent ]);
+ }, [ renderPoints, renderLine, renderContent, renderTitle, renderRows, renderBackdrop ]);
const showContent = React.useCallback(() => {
if (!isVisible.current) {
- d3.select(ref.current).attr('opacity', 1);
- d3.select(ref.current)
- .selectAll('.ChartTooltip__point')
- .attr('opacity', 1);
+ if (transitionDuration) {
+ d3.select(ref.current)
+ .transition()
+ .delay(transitionDuration)
+ .attr('opacity', 1);
+ } else {
+ d3.select(ref.current)
+ .attr('opacity', 1);
+ }
isVisible.current = true;
}
- }, []);
+ }, [ transitionDuration ]);
const hideContent = React.useCallback(() => {
- d3.select(ref.current).attr('opacity', 0);
+ if (transitionDuration) {
+ d3.select(ref.current)
+ .transition()
+ .delay(transitionDuration)
+ .attr('opacity', 0);
+ } else {
+ d3.select(ref.current)
+ .attr('opacity', 0);
+ }
isVisible.current = false;
- }, []);
+ }, [ transitionDuration ]);
const createPointerTracker = React.useCallback((event: PointerEvent, isSubsequentCall?: boolean) => {
let isPressed = event.pointerType === 'mouse' && event.type === 'pointerdown' && !isSubsequentCall;
@@ -224,73 +139,21 @@ const ChartTooltip = ({ xScale, yScale, width, tooltipWidth = 200, height, data,
}, [ anchorEl, createPointerTracker, draw, hideContent, showContent ]);
return (
-
-
- { data.map(({ name }) => (
-
- )) }
-
-
-
-
- Date
-
-
-
- { data.map(({ name }, index) => (
-
-
- { name }
-
-
-
- )) }
-
+
+
+ { data.map(({ name }) => ) }
+
+
+
+
+ { data.map(({ name }, index) => ) }
+
);
};
diff --git a/ui/shared/chart/ChartWidget.pw.tsx b/ui/shared/chart/ChartWidget.pw.tsx
index 409cf5bdd6..21fc07272f 100644
--- a/ui/shared/chart/ChartWidget.pw.tsx
+++ b/ui/shared/chart/ChartWidget.pw.tsx
@@ -1,5 +1,7 @@
import React from 'react';
+import type { TimeChartItem } from './types';
+
import { test, expect } from 'playwright/lib';
import type { Props } from './ChartWidget';
@@ -26,6 +28,7 @@ const props: Props = {
units: 'ETH',
isLoading: false,
isError: false,
+ noAnimation: true,
};
test('base view +@dark-mode', async({ render, page }) => {
@@ -41,6 +44,7 @@ test('base view +@dark-mode', async({ render, page }) => {
await page.mouse.move(0, 0);
await page.mouse.click(0, 0);
+ await page.mouse.move(80, 150);
await page.mouse.move(100, 150);
await expect(component).toHaveScreenshot();
@@ -109,3 +113,24 @@ test('small variations in big values', async({ render, page }) => {
});
await expect(component).toHaveScreenshot();
});
+
+test('incomplete day', async({ render, page }) => {
+ const modifiedProps = {
+ ...props,
+ items: [
+ ...props.items as Array,
+ { date: new Date('2023-02-24'), value: 25136740.887217894 / 4, isApproximate: true },
+ ],
+ };
+
+ const component = await render();
+ await page.waitForFunction(() => {
+ return document.querySelector('path[data-name="chart-Nativecoincirculatingsupply-small"]')?.getAttribute('opacity') === '1';
+ });
+ await expect(component).toHaveScreenshot();
+
+ await page.hover('.ChartOverlay', { position: { x: 120, y: 120 } });
+ await page.hover('.ChartOverlay', { position: { x: 320, y: 120 } });
+ await expect(page.getByText('Incomplete day')).toBeVisible();
+ await expect(component).toHaveScreenshot();
+});
diff --git a/ui/shared/chart/ChartWidget.tsx b/ui/shared/chart/ChartWidget.tsx
index 550f912f56..e32a1e29ae 100644
--- a/ui/shared/chart/ChartWidget.tsx
+++ b/ui/shared/chart/ChartWidget.tsx
@@ -36,11 +36,12 @@ export type Props = {
className?: string;
isError: boolean;
emptyText?: string;
+ noAnimation?: boolean;
}
const DOWNLOAD_IMAGE_SCALE = 5;
-const ChartWidget = ({ items, title, description, isLoading, className, isError, units, emptyText }: Props) => {
+const ChartWidget = ({ items, title, description, isLoading, className, isError, units, emptyText, noAnimation }: Props) => {
const ref = useRef(null);
const [ isFullscreen, setIsFullscreen ] = useState(false);
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
@@ -148,6 +149,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
isZoomResetInitial={ isZoomResetInitial }
title={ title }
units={ units }
+ noAnimation={ noAnimation }
/>
);
diff --git a/ui/shared/chart/ChartWidgetGraph.tsx b/ui/shared/chart/ChartWidgetGraph.tsx
index 7dbce5bb55..ce1ee8e4c9 100644
--- a/ui/shared/chart/ChartWidgetGraph.tsx
+++ b/ui/shared/chart/ChartWidgetGraph.tsx
@@ -23,13 +23,14 @@ interface Props {
onZoom: () => void;
isZoomResetInitial: boolean;
margin?: ChartMargin;
+ noAnimation?: boolean;
}
// temporarily turn off the data aggregation, we need a better algorithm for that
const MAX_SHOW_ITEMS = 100_000_000_000;
const DEFAULT_CHART_MARGIN = { bottom: 20, left: 10, right: 20, top: 10 };
-const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title, margin: marginProps, units }: Props) => {
+const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title, margin: marginProps, units, noAnimation }: Props) => {
const isMobile = useIsMobile();
const color = useToken('colors', 'blue.200');
const chartId = `chart-${ title.split(' ').join('') }-${ isEnlarged ? 'fullscreen' : 'small' }`;
@@ -99,7 +100,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
scale={ axes.y.scale }
ticks={ axesConfig.y.ticks }
size={ innerWidth }
- disableAnimation
+ noAnimation
/>
@@ -146,6 +148,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
xScale={ axes.x.scale }
yScale={ axes.y.scale }
data={ chartData }
+ noAnimation={ noAnimation }
/>
{
+ const bgColor = useToken('colors', 'blackAlpha.900');
+
+ return (
+
+ );
+};
+
+export default React.memo(ChartTooltipBackdrop);
+
+interface UseRenderBackdropParams {
+ seriesNum: number;
+ transitionDuration: number | null;
+}
+
+export function useRenderBackdrop(ref: React.RefObject, { seriesNum, transitionDuration }: UseRenderBackdropParams) {
+ return React.useCallback((width: number, isIncompleteData: boolean) => {
+ const height = calculateContainerHeight(seriesNum, isIncompleteData);
+
+ if (transitionDuration) {
+ d3.select(ref.current)
+ .select('.ChartTooltip__backdrop')
+ .transition()
+ .duration(transitionDuration)
+ .ease(d3.easeLinear)
+ .attr('width', width)
+ .attr('height', height);
+ } else {
+ d3.select(ref.current)
+ .select('.ChartTooltip__backdrop')
+ .attr('width', width)
+ .attr('height', height);
+ }
+ }, [ ref, seriesNum, transitionDuration ]);
+}
diff --git a/ui/shared/chart/tooltip/ChartTooltipContent.tsx b/ui/shared/chart/tooltip/ChartTooltipContent.tsx
new file mode 100644
index 0000000000..da1a62a5d6
--- /dev/null
+++ b/ui/shared/chart/tooltip/ChartTooltipContent.tsx
@@ -0,0 +1,101 @@
+import * as d3 from 'd3';
+import _clamp from 'lodash/clamp';
+import React from 'react';
+
+import { POINT_SIZE } from './utils';
+
+interface Props {
+ children: React.ReactNode;
+}
+
+const ChartTooltipContent = ({ children }: Props) => {
+ return { children };
+};
+
+export default React.memo(ChartTooltipContent);
+
+interface UseRenderContentParams {
+ chart: {
+ width?: number;
+ height?: number;
+ };
+ transitionDuration: number | null;
+}
+
+export function useRenderContent(ref: React.RefObject, { chart, transitionDuration }: UseRenderContentParams) {
+ return React.useCallback((x: number, y: number) => {
+ const tooltipContent = d3.select(ref.current).select('.ChartTooltip__content');
+
+ const transformAttributeFn: d3.ValueFn = (cur, i, nodes) => {
+ const node = nodes[i] as SVGGElement | null;
+ const { width: nodeWidth, height: nodeHeight } = node?.getBoundingClientRect() || { width: 0, height: 0 };
+ const [ translateX, translateY ] = calculatePosition({
+ canvasWidth: chart.width || 0,
+ canvasHeight: chart.height || 0,
+ nodeWidth,
+ nodeHeight,
+ pointX: x,
+ pointY: y,
+ offset: POINT_SIZE,
+ });
+ return `translate(${ translateX }, ${ translateY })`;
+ };
+
+ if (transitionDuration) {
+ tooltipContent
+ .transition()
+ .duration(transitionDuration)
+ .ease(d3.easeLinear)
+ .attr('transform', transformAttributeFn);
+ } else {
+ tooltipContent
+ .attr('transform', transformAttributeFn);
+ }
+
+ }, [ chart.height, chart.width, ref, transitionDuration ]);
+}
+
+interface CalculatePositionParams {
+ pointX: number;
+ pointY: number;
+ offset: number;
+ nodeWidth: number;
+ nodeHeight: number;
+ canvasWidth: number;
+ canvasHeight: number;
+}
+
+function calculatePosition({ pointX, pointY, canvasWidth, canvasHeight, nodeWidth, nodeHeight, offset }: CalculatePositionParams): [ number, number ] {
+ // right
+ if (pointX + offset + nodeWidth <= canvasWidth) {
+ const x = pointX + offset;
+ const y = _clamp(pointY - nodeHeight / 2, 0, canvasHeight - nodeHeight);
+ return [ x, y ];
+ }
+
+ // left
+ if (nodeWidth + offset <= pointX) {
+ const x = pointX - offset - nodeWidth;
+ const y = _clamp(pointY - nodeHeight / 2, 0, canvasHeight - nodeHeight);
+ return [ x, y ];
+ }
+
+ // top
+ if (nodeHeight + offset <= pointY) {
+ const x = _clamp(pointX - nodeWidth / 2, 0, canvasWidth - nodeWidth);
+ const y = pointY - offset - nodeHeight;
+ return [ x, y ];
+ }
+
+ // bottom
+ if (pointY + offset + nodeHeight <= canvasHeight) {
+ const x = _clamp(pointX - nodeWidth / 2, 0, canvasWidth - nodeWidth);
+ const y = pointY + offset;
+ return [ x, y ];
+ }
+
+ const x = _clamp(pointX / 2, 0, canvasWidth - nodeWidth);
+ const y = _clamp(pointY / 2, 0, canvasHeight - nodeHeight);
+
+ return [ x, y ];
+}
diff --git a/ui/shared/chart/tooltip/ChartTooltipLine.tsx b/ui/shared/chart/tooltip/ChartTooltipLine.tsx
new file mode 100644
index 0000000000..7397365945
--- /dev/null
+++ b/ui/shared/chart/tooltip/ChartTooltipLine.tsx
@@ -0,0 +1,21 @@
+import { useToken } from '@chakra-ui/react';
+import * as d3 from 'd3';
+import React from 'react';
+
+const ChartTooltipLine = () => {
+ const lineColor = useToken('colors', 'gray.400');
+ return ;
+};
+
+export default React.memo(ChartTooltipLine);
+
+export function useRenderLine(ref: React.RefObject, chartHeight: number | undefined) {
+ return React.useCallback((x: number) => {
+ d3.select(ref.current)
+ .select('.ChartTooltip__line')
+ .attr('x1', x)
+ .attr('x2', x)
+ .attr('y1', 0)
+ .attr('y2', chartHeight || 0);
+ }, [ ref, chartHeight ]);
+}
diff --git a/ui/shared/chart/tooltip/ChartTooltipPoint.tsx b/ui/shared/chart/tooltip/ChartTooltipPoint.tsx
new file mode 100644
index 0000000000..c6dd1052f4
--- /dev/null
+++ b/ui/shared/chart/tooltip/ChartTooltipPoint.tsx
@@ -0,0 +1,93 @@
+import { useColorModeValue, useToken } from '@chakra-ui/react';
+import * as d3 from 'd3';
+import React from 'react';
+
+import type { TimeChartData, TimeChartItem } from 'ui/shared/chart/types';
+
+import { POINT_SIZE } from './utils';
+
+const ChartTooltipPoint = () => {
+ const bgColor = useToken('colors', useColorModeValue('black', 'white'));
+ const borderColor = useToken('colors', useColorModeValue('white', 'black'));
+
+ return (
+
+ );
+};
+
+export default React.memo(ChartTooltipPoint);
+
+interface UseRenderPointsParams {
+ data: TimeChartData;
+ xScale: d3.ScaleTime;
+ yScale: d3.ScaleLinear;
+}
+
+export interface CurrentPoint {
+ datumIndex: number;
+ item: TimeChartItem;
+}
+
+interface RenderPointsReturnType{
+ x: number;
+ y: number;
+ currentPoints: Array;
+}
+
+export function useRenderPoints(ref: React.RefObject, params: UseRenderPointsParams) {
+ return React.useCallback((x: number): RenderPointsReturnType => {
+ const xDate = params.xScale.invert(x);
+ const bisectDate = d3.bisector((d) => d.date).left;
+ let baseXPos = 0;
+ let baseYPos = 0;
+ const currentPoints: Array = [];
+
+ d3.select(ref.current)
+ .selectAll('.ChartTooltip__point')
+ .attr('transform', (cur, elementIndex) => {
+ const datum = params.data[elementIndex];
+ const index = bisectDate(datum.items, xDate, 1);
+ const d0 = datum.items[index - 1] as TimeChartItem | undefined;
+ const d1 = datum.items[index] as TimeChartItem | undefined;
+ const d = (() => {
+ if (!d0) {
+ return d1;
+ }
+ if (!d1) {
+ return d0;
+ }
+ return xDate.getTime() - d0.date.getTime() > d1.date.getTime() - xDate.getTime() ? d1 : d0;
+ })();
+
+ if (d?.date === undefined && d?.value === undefined) {
+ // move point out of container
+ return 'translate(-100,-100)';
+ }
+
+ const xPos = params.xScale(d.date);
+ const yPos = params.yScale(d.value);
+
+ if (elementIndex === 0) {
+ baseXPos = xPos;
+ baseYPos = yPos;
+ }
+
+ currentPoints.push({ item: d, datumIndex: elementIndex });
+
+ return `translate(${ xPos }, ${ yPos })`;
+ });
+
+ return {
+ x: baseXPos,
+ y: baseYPos,
+ currentPoints,
+ };
+ }, [ ref, params ]);
+}
diff --git a/ui/shared/chart/tooltip/ChartTooltipRow.tsx b/ui/shared/chart/tooltip/ChartTooltipRow.tsx
new file mode 100644
index 0000000000..a35dcb25f1
--- /dev/null
+++ b/ui/shared/chart/tooltip/ChartTooltipRow.tsx
@@ -0,0 +1,96 @@
+import { useToken } from '@chakra-ui/react';
+import * as d3 from 'd3';
+import React from 'react';
+
+import type { TimeChartData } from '../types';
+
+import type { CurrentPoint } from './ChartTooltipPoint';
+import { calculateRowTransformValue, LABEL_WIDTH, PADDING } from './utils';
+
+type Props = {
+ lineNum: number;
+} & ({ label: string; children?: never } | { children: React.ReactNode; label?: never })
+
+const ChartTooltipRow = ({ label, lineNum, children }: Props) => {
+ const labelColor = useToken('colors', 'blue.100');
+ const textColor = useToken('colors', 'white');
+
+ return (
+
+ { children || (
+ <>
+
+ { label }
+
+
+ >
+ ) }
+
+ );
+};
+
+export default React.memo(ChartTooltipRow);
+
+interface UseRenderRowsParams {
+ data: TimeChartData;
+ xScale: d3.ScaleTime;
+ minWidth: number;
+}
+
+interface UseRenderRowsReturnType {
+ width: number;
+}
+
+export function useRenderRows(ref: React.RefObject, { data, xScale, minWidth }: UseRenderRowsParams) {
+ return React.useCallback((x: number, currentPoints: Array): UseRenderRowsReturnType => {
+
+ // update "transform" prop of all rows
+ const isIncompleteData = currentPoints.some(({ item }) => item.isApproximate);
+ d3.select(ref.current)
+ .selectAll('.ChartTooltip__row')
+ .attr('transform', (datum, index) => {
+ return calculateRowTransformValue(index - (isIncompleteData ? 0 : 1));
+ });
+
+ // update date and indicators value
+ // here we assume that the first value element contains the date
+ const valueNodes = d3.select(ref.current)
+ .selectAll('.ChartTooltip__value')
+ .text((_, index) => {
+ if (index === 0) {
+ const date = xScale.invert(x);
+ const dateValue = data[0].items.find((item) => item.date.getTime() === date.getTime())?.dateLabel;
+ const dateValueFallback = d3.timeFormat('%e %b %Y')(xScale.invert(x));
+ return dateValue || dateValueFallback;
+ }
+
+ const { datumIndex, item } = currentPoints.find(({ datumIndex }) => datumIndex === index - 1) || {};
+ if (datumIndex === undefined || !item) {
+ return null;
+ }
+
+ const value = data[datumIndex]?.valueFormatter?.(item.value) ?? item.value.toLocaleString(undefined, { minimumSignificantDigits: 1 });
+ const units = data[datumIndex]?.units ? ` ${ data[datumIndex]?.units }` : '';
+
+ return value + units;
+ })
+ .nodes();
+
+ const valueWidths = valueNodes.map((node) => node?.getBoundingClientRect?.().width);
+ const maxValueWidth = Math.max(...valueWidths);
+ const maxRowWidth = Math.max(minWidth, 2 * PADDING + LABEL_WIDTH + maxValueWidth);
+
+ return { width: maxRowWidth };
+
+ }, [ data, minWidth, ref, xScale ]);
+}
diff --git a/ui/shared/chart/tooltip/ChartTooltipTitle.tsx b/ui/shared/chart/tooltip/ChartTooltipTitle.tsx
new file mode 100644
index 0000000000..93ab2e9943
--- /dev/null
+++ b/ui/shared/chart/tooltip/ChartTooltipTitle.tsx
@@ -0,0 +1,33 @@
+import { useToken } from '@chakra-ui/react';
+import * as d3 from 'd3';
+import React from 'react';
+
+import ChartTooltipRow from './ChartTooltipRow';
+
+const ChartTooltipTitle = () => {
+ const titleColor = useToken('colors', 'yellow.300');
+
+ return (
+
+
+ Incomplete day
+
+
+ );
+};
+
+export default React.memo(ChartTooltipTitle);
+
+export function useRenderTitle(ref: React.RefObject) {
+ return React.useCallback((isVisible: boolean) => {
+ d3.select(ref.current)
+ .select('.ChartTooltip__title')
+ .attr('opacity', isVisible ? 1 : 0);
+ }, [ ref ]);
+}
diff --git a/ui/shared/chart/utils/pointerTracker.tsx b/ui/shared/chart/tooltip/pointerTracker.ts
similarity index 100%
rename from ui/shared/chart/utils/pointerTracker.tsx
rename to ui/shared/chart/tooltip/pointerTracker.ts
diff --git a/ui/shared/chart/tooltip/utils.ts b/ui/shared/chart/tooltip/utils.ts
new file mode 100644
index 0000000000..8b11b366a9
--- /dev/null
+++ b/ui/shared/chart/tooltip/utils.ts
@@ -0,0 +1,16 @@
+export const TEXT_LINE_HEIGHT = 12;
+export const PADDING = 16;
+export const LINE_SPACE = 10;
+export const POINT_SIZE = 16;
+export const LABEL_WIDTH = 80;
+
+export const calculateContainerHeight = (seriesNum: number, isIncomplete?: boolean) => {
+ const linesNum = isIncomplete ? seriesNum + 2 : seriesNum + 1;
+
+ return 2 * PADDING + linesNum * TEXT_LINE_HEIGHT + (linesNum - 1) * LINE_SPACE;
+};
+
+export const calculateRowTransformValue = (rowNum: number) => {
+ const top = Math.max(0, PADDING + rowNum * (LINE_SPACE + TEXT_LINE_HEIGHT));
+ return `translate(${ PADDING },${ top })`;
+};
diff --git a/ui/shared/chart/types.tsx b/ui/shared/chart/types.tsx
index 5810fe2fb5..c1c02b1b12 100644
--- a/ui/shared/chart/types.tsx
+++ b/ui/shared/chart/types.tsx
@@ -8,6 +8,7 @@ export interface TimeChartItem {
date: Date;
dateLabel?: string;
value: number;
+ isApproximate?: boolean;
}
export interface ChartMargin {
diff --git a/ui/shared/chart/utils/animations.ts b/ui/shared/chart/utils/animations.ts
new file mode 100644
index 0000000000..2f873f4a69
--- /dev/null
+++ b/ui/shared/chart/utils/animations.ts
@@ -0,0 +1,33 @@
+import * as d3 from 'd3';
+
+export type AnimationType = 'left' | 'fadeIn' | 'none';
+
+export const animateLeft = (path: SVGPathElement) => {
+ const totalLength = path.getTotalLength() || 0;
+ d3.select(path)
+ .attr('opacity', 1)
+ .attr('stroke-dasharray', `${ totalLength },${ totalLength }`)
+ .attr('stroke-dashoffset', totalLength)
+ .transition()
+ .duration(750)
+ .ease(d3.easeLinear)
+ .attr('stroke-dashoffset', 0);
+};
+
+export const animateFadeIn = (path: SVGPathElement) => {
+ d3.select(path)
+ .transition()
+ .duration(750)
+ .ease(d3.easeLinear)
+ .attr('opacity', 1);
+};
+
+export const noneAnimation = (path: SVGPathElement) => {
+ d3.select(path).attr('opacity', 1);
+};
+
+export const ANIMATIONS: Record void> = {
+ left: animateLeft,
+ fadeIn: animateFadeIn,
+ none: noneAnimation,
+};
diff --git a/ui/shared/chart/utils/computeTooltipPosition.ts b/ui/shared/chart/utils/computeTooltipPosition.ts
deleted file mode 100644
index 88356d34cd..0000000000
--- a/ui/shared/chart/utils/computeTooltipPosition.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import _clamp from 'lodash/clamp';
-
-interface Params {
- pointX: number;
- pointY: number;
- offset: number;
- nodeWidth: number;
- nodeHeight: number;
- canvasWidth: number;
- canvasHeight: number;
-}
-
-export default function computeTooltipPosition({ pointX, pointY, canvasWidth, canvasHeight, nodeWidth, nodeHeight, offset }: Params): [ number, number ] {
- // right
- if (pointX + offset + nodeWidth <= canvasWidth) {
- const x = pointX + offset;
- const y = _clamp(pointY - nodeHeight / 2, 0, canvasHeight - nodeHeight);
- return [ x, y ];
- }
-
- // left
- if (nodeWidth + offset <= pointX) {
- const x = pointX - offset - nodeWidth;
- const y = _clamp(pointY - nodeHeight / 2, 0, canvasHeight - nodeHeight);
- return [ x, y ];
- }
-
- // top
- if (nodeHeight + offset <= pointY) {
- const x = _clamp(pointX - nodeWidth / 2, 0, canvasWidth - nodeWidth);
- const y = pointY - offset - nodeHeight;
- return [ x, y ];
- }
-
- // bottom
- if (pointY + offset + nodeHeight <= canvasHeight) {
- const x = _clamp(pointX - nodeWidth / 2, 0, canvasWidth - nodeWidth);
- const y = pointY + offset;
- return [ x, y ];
- }
-
- const x = _clamp(pointX / 2, 0, canvasWidth - nodeWidth);
- const y = _clamp(pointY / 2, 0, canvasHeight - nodeHeight);
-
- return [ x, y ];
-}
diff --git a/ui/shared/chart/utils/formatters.ts b/ui/shared/chart/utils/formatters.ts
new file mode 100644
index 0000000000..9d0d13a7d2
--- /dev/null
+++ b/ui/shared/chart/utils/formatters.ts
@@ -0,0 +1,19 @@
+import type { TimeChartItem } from '../types';
+
+export const getIncompleteDataLineSource = (data: Array): Array => {
+ const result: Array = [];
+
+ for (let index = 0; index < data.length; index++) {
+ const current = data[index];
+ if (current.isApproximate) {
+ const prev = data[index - 1];
+ const next = data[index + 1];
+
+ prev && !prev.isApproximate && result.push(prev);
+ result.push(current);
+ next && !next.isApproximate && result.push(next);
+ }
+ }
+
+ return result;
+};
diff --git a/ui/shared/entities/address/AddressEntity.tsx b/ui/shared/entities/address/AddressEntity.tsx
index 1519571bb8..704285e0ff 100644
--- a/ui/shared/entities/address/AddressEntity.tsx
+++ b/ui/shared/entities/address/AddressEntity.tsx
@@ -108,7 +108,7 @@ const Content = chakra((props: ContentProps) => {
);
return (
-
+
{ nameText }
diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_contract-unverified-1.png b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_contract-unverified-1.png
index 5af8e38ece..a18893d2ea 100644
Binary files a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_contract-unverified-1.png and b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_contract-unverified-1.png differ
diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_hover-1.png b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_hover-1.png
index 0d77e3eb86..bf2243601a 100644
Binary files a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_hover-1.png and b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_hover-1.png differ
diff --git a/ui/shared/entities/token/__screenshots__/TokenEntity.pw.tsx_default_with-logo-long-name-and-symbol-1.png b/ui/shared/entities/token/__screenshots__/TokenEntity.pw.tsx_default_with-logo-long-name-and-symbol-1.png
index 038508436e..5534d9df03 100644
Binary files a/ui/shared/entities/token/__screenshots__/TokenEntity.pw.tsx_default_with-logo-long-name-and-symbol-1.png and b/ui/shared/entities/token/__screenshots__/TokenEntity.pw.tsx_default_with-logo-long-name-and-symbol-1.png differ
diff --git a/ui/shared/entities/token/__screenshots__/TokenEntity.pw.tsx_default_with-logo-long-name-and-symbol-2.png b/ui/shared/entities/token/__screenshots__/TokenEntity.pw.tsx_default_with-logo-long-name-and-symbol-2.png
index d311239584..9d8d7e86d8 100644
Binary files a/ui/shared/entities/token/__screenshots__/TokenEntity.pw.tsx_default_with-logo-long-name-and-symbol-2.png and b/ui/shared/entities/token/__screenshots__/TokenEntity.pw.tsx_default_with-logo-long-name-and-symbol-2.png differ
diff --git a/ui/shared/forms/FileSnippet.tsx b/ui/shared/forms/FileSnippet.tsx
index 172c045ed9..73656e46d8 100644
--- a/ui/shared/forms/FileSnippet.tsx
+++ b/ui/shared/forms/FileSnippet.tsx
@@ -76,7 +76,7 @@ const FileSnippet = ({ file, className, index, onRemove, isDisabled, error }: Pr
diff --git a/ui/snippets/navigation/vertical/NavLink.tsx b/ui/snippets/navigation/vertical/NavLink.tsx
index 95b3a3a210..4dfaf67e43 100644
--- a/ui/snippets/navigation/vertical/NavLink.tsx
+++ b/ui/snippets/navigation/vertical/NavLink.tsx
@@ -64,6 +64,7 @@ const NavLink = ({ item, isCollapsed, px, className, onClick, disableActiveState
variant="nav"
gutter={ 20 }
color={ isInternalLink && item.isActive ? colors.text.active : colors.text.hover }
+ margin={ 0 }
>
diff --git a/ui/stats/ChartWidgetContainer.tsx b/ui/stats/ChartWidgetContainer.tsx
index b9e45ebc79..5f41a5c2af 100644
--- a/ui/stats/ChartWidgetContainer.tsx
+++ b/ui/stats/ChartWidgetContainer.tsx
@@ -42,7 +42,7 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
});
const items = useMemo(() => data?.chart?.map((item) => {
- return { date: new Date(item.date), value: Number(item.value) };
+ return { date: new Date(item.date), value: Number(item.value), isApproximate: item.is_approximate };
}), [ data ]);
useEffect(() => {
diff --git a/ui/token/TokenDetails.tsx b/ui/token/TokenDetails.tsx
index 223f1f974a..e689d8e646 100644
--- a/ui/token/TokenDetails.tsx
+++ b/ui/token/TokenDetails.tsx
@@ -12,7 +12,6 @@ import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getCurrencyValue from 'lib/getCurrencyValue';
-import useFeatureValue from 'lib/growthbook/useFeatureValue';
import useIsMounted from 'lib/hooks/useIsMounted';
import { TOKEN_COUNTERS } from 'stubs/token';
import type { TokenTabs } from 'ui/pages/Token';
@@ -31,7 +30,6 @@ interface Props {
const TokenDetails = ({ tokenQuery }: Props) => {
const router = useRouter();
const isMounted = useIsMounted();
- const { value: isActionButtonExperiment } = useFeatureValue('action_button_exp', false);
const hash = router.query.hash?.toString();
@@ -40,7 +38,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
queryOptions: { enabled: Boolean(router.query.hash), placeholderData: TOKEN_COUNTERS },
});
- const appActionData = useAppActionData(hash, isActionButtonExperiment);
+ const appActionData = useAppActionData(hash);
const changeUrlAndScroll = useCallback((tab: TokenTabs) => () => {
router.push(
@@ -200,11 +198,10 @@ const TokenDetails = ({ tokenQuery }: Props) => {
isLoading={ tokenQuery.isPlaceholderData }
appActionData={ appActionData }
source="NFT collection"
- isActionButtonExperiment={ isActionButtonExperiment }
/>
) }
- { (type !== 'ERC-20' && config.UI.views.nft.marketplaces.length === 0 && appActionData && isActionButtonExperiment) && (
+ { (type !== 'ERC-20' && config.UI.views.nft.marketplaces.length === 0 && appActionData) && (
<>
{
+const TokenNftMarketplaces = ({ hash, id, isLoading, appActionData, source }: Props) => {
if (!hash || config.UI.views.nft.marketplaces.length === 0) {
return null;
}
@@ -31,7 +30,7 @@ const TokenNftMarketplaces = ({ hash, id, isLoading, appActionData, source, isAc
Marketplaces
{ config.UI.views.nft.marketplaces.map((item) => {
@@ -52,7 +51,7 @@ const TokenNftMarketplaces = ({ hash, id, isLoading, appActionData, source, isAc
);
}) }
- { (appActionData && isActionButtonExperiment) && (
+ { appActionData && (
<>
diff --git a/ui/token/TokenTransfer/TokenTransferListItem.tsx b/ui/token/TokenTransfer/TokenTransferListItem.tsx
index f4c582e039..40c9b70cd2 100644
--- a/ui/token/TokenTransfer/TokenTransferListItem.tsx
+++ b/ui/token/TokenTransfer/TokenTransferListItem.tsx
@@ -4,13 +4,13 @@ import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import getCurrencyValue from 'lib/getCurrencyValue';
-import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import AddressFromTo from 'ui/shared/address/AddressFromTo';
import Tag from 'ui/shared/chakra/Tag';
import NftEntity from 'ui/shared/entities/nft/NftEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
import TruncatedValue from 'ui/shared/TruncatedValue';
type Props = TokenTransfer & { tokenId?: string; isLoading?: boolean };
@@ -26,7 +26,6 @@ const TokenTransferListItem = ({
tokenId,
isLoading,
}: Props) => {
- const timeAgo = useTimeAgoIncrement(timestamp, true);
const { usd, valueStr } = 'value' in total && total.value !== null ? getCurrencyValue({
value: total.value,
exchangeRate: token.exchange_rate,
@@ -44,13 +43,15 @@ const TokenTransferListItem = ({
truncation="constant_long"
fontWeight="700"
/>
- { timestamp && (
-
-
- { timeAgo }
-
-
- ) }
+
{ method && { method } }
{
- const timeAgo = useTimeAgoIncrement(timestamp, true);
const { usd, valueStr } = 'value' in total && total.value !== null ? getCurrencyValue({
value: total.value,
exchangeRate: token.exchange_rate,
@@ -44,13 +43,15 @@ const TokenTransferTableItem = ({
noIcon
truncation="constant_long"
/>
- { timestamp && (
-
-
- { timeAgo }
-
-
- ) }
+
|
diff --git a/ui/tokenInstance/TokenInstanceDetails.pw.tsx b/ui/tokenInstance/TokenInstanceDetails.pw.tsx
index 17658a89d2..84ea79091e 100644
--- a/ui/tokenInstance/TokenInstanceDetails.pw.tsx
+++ b/ui/tokenInstance/TokenInstanceDetails.pw.tsx
@@ -55,8 +55,7 @@ test('base view +@dark-mode +@mobile', async({ render, page }) => {
});
test.describe('action button', () => {
- test.beforeEach(async({ mockFeatures, mockApiResponse, mockAssetResponse }) => {
- await mockFeatures([ [ 'action_button_exp', true ] ]);
+ test.beforeEach(async({ mockApiResponse, mockAssetResponse }) => {
const metadataResponse = generateAddressMetadataResponse(protocolTagWithMeta);
await mockApiResponse('address_metadata_info', metadataResponse, { queryParams: addressMetadataQueryParams });
await mockAssetResponse(protocolTagWithMeta?.meta?.appLogoURL as string, './playwright/mocks/image_s.jpg');
diff --git a/ui/tokenInstance/TokenInstanceDetails.tsx b/ui/tokenInstance/TokenInstanceDetails.tsx
index 68e774fd3e..cac82c8ace 100644
--- a/ui/tokenInstance/TokenInstanceDetails.tsx
+++ b/ui/tokenInstance/TokenInstanceDetails.tsx
@@ -4,7 +4,6 @@ import React from 'react';
import type { TokenInfo, TokenInstance } from 'types/api/token';
import config from 'configs/app';
-import useFeatureValue from 'lib/growthbook/useFeatureValue';
import useIsMounted from 'lib/hooks/useIsMounted';
import AppActionButton from 'ui/shared/AppActionButton/AppActionButton';
import useAppActionData from 'ui/shared/AppActionButton/useAppActionData';
@@ -29,8 +28,7 @@ interface Props {
}
const TokenInstanceDetails = ({ data, token, scrollRef, isLoading }: Props) => {
- const { value: isActionButtonExperiment } = useFeatureValue('action_button_exp', false);
- const appActionData = useAppActionData(token?.address, isActionButtonExperiment && !isLoading);
+ const appActionData = useAppActionData(token?.address, !isLoading);
const isMounted = useIsMounted();
const handleCounterItemClick = React.useCallback(() => {
@@ -96,10 +94,9 @@ const TokenInstanceDetails = ({ data, token, scrollRef, isLoading }: Props) => {
id={ data.id }
appActionData={ appActionData }
source="NFT item"
- isActionButtonExperiment={ isActionButtonExperiment }
/>
- { (config.UI.views.nft.marketplaces.length === 0 && appActionData && isActionButtonExperiment) && (
+ { (config.UI.views.nft.marketplaces.length === 0 && appActionData) && (
<>
{
await expect(component).toHaveScreenshot();
});
- test('with interpretation and action button +@mobile +@dark-mode', async({ render, mockApiResponse, mockAssetResponse, mockFeatures }) => {
- await mockFeatures([ [ 'action_button_exp', true ] ]);
+ test('with interpretation and action button +@mobile +@dark-mode', async({ render, mockApiResponse, mockAssetResponse }) => {
const metadataResponse = generateAddressMetadataResponse(protocolTagWithMeta);
await mockApiResponse('address_metadata_info', metadataResponse, { queryParams: addressMetadataQueryParams });
await mockAssetResponse(protocolTagWithMeta?.meta?.appLogoURL as string, './playwright/mocks/image_s.jpg');
@@ -76,9 +75,8 @@ test.describe('blockscout provider', () => {
});
test('with interpretation and view all link, and action button (external link) +@mobile', async({
- render, mockApiResponse, mockAssetResponse, mockFeatures,
+ render, mockApiResponse, mockAssetResponse,
}) => {
- await mockFeatures([ [ 'action_button_exp', true ] ]);
delete protocolTagWithMeta?.meta?.appID;
const metadataResponse = generateAddressMetadataResponse(protocolTagWithMeta);
await mockApiResponse('address_metadata_info', metadataResponse, { queryParams: addressMetadataQueryParams });
@@ -92,9 +90,8 @@ test.describe('blockscout provider', () => {
await expect(component).toHaveScreenshot();
});
- test('no interpretation, has method called', async({ render, mockApiResponse, mockFeatures }) => {
+ test('no interpretation, has method called', async({ render, mockApiResponse }) => {
// the action button should not render if there is no interpretation
- await mockFeatures([ [ 'action_button_exp', true ] ]);
const metadataResponse = generateAddressMetadataResponse(protocolTagWithMeta);
await mockApiResponse('address_metadata_info', metadataResponse, { queryParams: addressMetadataQueryParams });
@@ -103,9 +100,8 @@ test.describe('blockscout provider', () => {
await expect(component).toHaveScreenshot();
});
- test('no interpretation', async({ render, mockApiResponse, mockFeatures }) => {
+ test('no interpretation', async({ render, mockApiResponse }) => {
// the action button should not render if there is no interpretation
- await mockFeatures([ [ 'action_button_exp', true ] ]);
const metadataResponse = generateAddressMetadataResponse(protocolTagWithMeta);
await mockApiResponse('address_metadata_info', metadataResponse, { queryParams: addressMetadataQueryParams });
diff --git a/ui/tx/TxSubHeading.tsx b/ui/tx/TxSubHeading.tsx
index 278bcc137d..dcfcf76837 100644
--- a/ui/tx/TxSubHeading.tsx
+++ b/ui/tx/TxSubHeading.tsx
@@ -3,7 +3,6 @@ import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
-import useFeatureValue from 'lib/growthbook/useFeatureValue';
import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate';
import { TX_INTERPRETATION } from 'stubs/txInterpretation';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu';
@@ -29,8 +28,7 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => {
const hasInterpretationFeature = feature.isEnabled;
const isNovesInterpretation = hasInterpretationFeature && feature.provider === 'noves';
- const { value: isActionButtonExperiment } = useFeatureValue('action_button_exp', false);
- const appActionData = useAppActionData(txQuery.data?.to?.hash, isActionButtonExperiment && !txQuery.isPlaceholderData);
+ const appActionData = useAppActionData(txQuery.data?.to?.hash, !txQuery.isPlaceholderData);
const txInterpretationQuery = useApiQuery('tx_interpretation', {
pathParams: { hash },
@@ -127,7 +125,7 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => {
mt={{ base: 3, lg: 0 }}
>
{ !hasTag && }
- { (appActionData && isActionButtonExperiment && hasAnyInterpretation) && (
+ { (appActionData && hasAnyInterpretation) && (
) }
diff --git a/ui/txnBatches/arbitrumL2/ArbitrumL2TxnBatchesListItem.tsx b/ui/txnBatches/arbitrumL2/ArbitrumL2TxnBatchesListItem.tsx
index 78d8356371..73fbfc78a0 100644
--- a/ui/txnBatches/arbitrumL2/ArbitrumL2TxnBatchesListItem.tsx
+++ b/ui/txnBatches/arbitrumL2/ArbitrumL2TxnBatchesListItem.tsx
@@ -6,21 +6,19 @@ import type { ArbitrumL2TxnBatchesItem } from 'types/api/arbitrumL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import ArbitrumL2TxnBatchStatus from 'ui/shared/statusTag/ArbitrumL2TxnBatchStatus';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
type Props = { item: ArbitrumL2TxnBatchesItem; isLoading?: boolean };
const ArbitrumL2TxnBatchesListItem = ({ item, isLoading }: Props) => {
- const timeAgo = item.commitment_transaction.timestamp ? dayjs(item.commitment_transaction.timestamp).fromNow() : 'Undefined';
-
if (!rollupFeature.isEnabled || rollupFeature.type !== 'arbitrum') {
return null;
}
@@ -76,7 +74,12 @@ const ArbitrumL2TxnBatchesListItem = ({ item, isLoading }: Props) => {
Age
- { timeAgo }
+
Txn count
diff --git a/ui/txnBatches/arbitrumL2/ArbitrumL2TxnBatchesTableItem.tsx b/ui/txnBatches/arbitrumL2/ArbitrumL2TxnBatchesTableItem.tsx
index 4c59836e7b..94f480b952 100644
--- a/ui/txnBatches/arbitrumL2/ArbitrumL2TxnBatchesTableItem.tsx
+++ b/ui/txnBatches/arbitrumL2/ArbitrumL2TxnBatchesTableItem.tsx
@@ -6,20 +6,18 @@ import type { ArbitrumL2TxnBatchesItem } from 'types/api/arbitrumL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ArbitrumL2TxnBatchStatus from 'ui/shared/statusTag/ArbitrumL2TxnBatchStatus';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
type Props = { item: ArbitrumL2TxnBatchesItem; isLoading?: boolean };
const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
- const timeAgo = item.commitment_transaction.timestamp ? dayjs(item.commitment_transaction.timestamp).fromNow() : 'Undefined';
-
if (!rollupFeature.isEnabled || rollupFeature.type !== 'arbitrum') {
return null;
}
@@ -60,9 +58,12 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
/>
|
-
- { timeAgo }
-
+
|
{
- const timeAgo = dayjs(item.l1_timestamp).fromNow();
-
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
}
@@ -67,7 +65,11 @@ const OptimisticL2TxnBatchesListItem = ({ item, isLoading }: Props) => {
Age
- { timeAgo }
+
diff --git a/ui/txnBatches/optimisticL2/OptimisticL2TxnBatchesTableItem.tsx b/ui/txnBatches/optimisticL2/OptimisticL2TxnBatchesTableItem.tsx
index fff8a75b38..02d0964073 100644
--- a/ui/txnBatches/optimisticL2/OptimisticL2TxnBatchesTableItem.tsx
+++ b/ui/txnBatches/optimisticL2/OptimisticL2TxnBatchesTableItem.tsx
@@ -6,18 +6,16 @@ import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/links/LinkInternal';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
type Props = { item: OptimisticL2TxnBatchesItem; isLoading?: boolean };
const OptimisticL2TxnBatchesTableItem = ({ item, isLoading }: Props) => {
- const timeAgo = dayjs(item.l1_timestamp).fromNow();
-
if (!rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
}
@@ -60,9 +58,13 @@ const OptimisticL2TxnBatchesTableItem = ({ item, isLoading }: Props) => {
|
-
- { timeAgo }
-
+
|
);
diff --git a/ui/txnBatches/zkEvmL2/ZkEvmTxnBatchesListItem.tsx b/ui/txnBatches/zkEvmL2/ZkEvmTxnBatchesListItem.tsx
index 454c4e887c..43be4e0fd7 100644
--- a/ui/txnBatches/zkEvmL2/ZkEvmTxnBatchesListItem.tsx
+++ b/ui/txnBatches/zkEvmL2/ZkEvmTxnBatchesListItem.tsx
@@ -6,20 +6,18 @@ import type { ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
-import dayjs from 'lib/date/dayjs';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import ZkEvmL2TxnBatchStatus from 'ui/shared/statusTag/ZkEvmL2TxnBatchStatus';
+import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
const rollupFeature = config.features.rollup;
type Props = { item: ZkEvmL2TxnBatchesItem; isLoading?: boolean };
const ZkEvmTxnBatchesListItem = ({ item, isLoading }: Props) => {
- const timeAgo = item.timestamp ? dayjs(item.timestamp).fromNow() : 'Undefined';
-
if (!rollupFeature.isEnabled || rollupFeature.type !== 'zkEvm') {
return null;
}
@@ -45,7 +43,12 @@ const ZkEvmTxnBatchesListItem = ({ item, isLoading }: Props) => {