Commit 70ddce7a authored by isstuev's avatar isstuev

Multi chain balance button

parent 1b977de2
......@@ -15,6 +15,7 @@ export { default as growthBook } from './growthBook';
export { default as marketplace } from './marketplace';
export { default as metasuites } from './metasuites';
export { default as mixpanel } from './mixpanel';
export { default as multichainButton } from './multichainButton';
export { default as nameService } from './nameService';
export { default as restApiDocs } from './restApiDocs';
export { default as rollup } from './rollup';
......
import type { Feature } from './types';
import type { MultichainProviderConfig } from 'types/client/multichainProviderConfig';
import { getEnvValue, parseEnvJson } from '../utils';
import marketplace from './marketplace';
const value = parseEnvJson<MultichainProviderConfig>(getEnvValue('NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG'));
const title = 'Multichain button';
function isValidUrl(string: string) {
try {
new URL(string);
return true;
} catch (error) {
return false;
}
}
const config: Feature<{name: string; logoUrl?: string } & ({ dappId: string } | { url: string })> = (() => {
if (value) {
const enabledOptions = {
title,
isEnabled: true as const,
name: value.name,
logoUrl: value.logo,
};
if (isValidUrl(value.url)) {
return Object.freeze({
...enabledOptions,
url: value.url,
});
} else if (marketplace.isEnabled) {
return Object.freeze({
...enabledOptions,
dappId: value.url,
});
}
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
......@@ -54,6 +54,7 @@ NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKj
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=gearbox-protocol
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG={'name': 'zerion', 'url': 'zerion', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
#meta
NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth.jpg?raw=true
......
......@@ -15,6 +15,7 @@ import type { ContractCodeIde } from '../../../types/client/contract';
import { GAS_UNITS } from '../../../types/client/gasTracker';
import type { GasUnit } from '../../../types/client/gasTracker';
import type { MarketplaceAppOverview, MarketplaceAppSecurityReportRaw, MarketplaceAppSecurityReport } from '../../../types/client/marketplace';
import type { MultichainProviderConfig } from '../../../types/client/multichainProviderConfig';
import { NAVIGATION_LINK_IDS } from '../../../types/client/navigation-items';
import type { NavItemExternal, NavigationLinkId } from '../../../types/client/navigation-items';
import { ROLLUP_TYPES } from '../../../types/client/rollup';
......@@ -620,6 +621,11 @@ const schema = yup
NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(),
NEXT_PUBLIC_METASUITES_ENABLED: yup.boolean(),
NEXT_PUBLIC_SWAP_BUTTON_URL: yup.string(),
NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG: yup.object<MultichainProviderConfig>().transform(replaceQuotes).json().shape({
name: yup.string().required(),
url: yup.string().required(),
logo: yup.string(),
}).nullable().notRequired(),
NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE: yup.string<ValidatorsChainType>().oneOf(VALIDATORS_CHAIN_TYPE),
NEXT_PUBLIC_GAS_TRACKER_ENABLED: yup.boolean(),
NEXT_PUBLIC_GAS_TRACKER_UNITS: yup.array().transform(replaceQuotes).json().of(yup.string<GasUnit>().oneOf(GAS_UNITS)),
......
......@@ -57,6 +57,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Sentry error monitoring](ENVS.md#sentry-error-monitoring)
- [OpenTelemetry](ENVS.md#opentelemetry)
- [Swap button](ENVS.md#swap-button)
- [Multichain balance button](ENVS.md#multichain-button)
- [3rd party services configuration](ENVS.md#external-services-configuration)
&nbsp;
......@@ -679,6 +680,26 @@ If the feature is enabled, a Swap button will be displayed at the top of the exp
&nbsp;
### Multichain balance button
If the feature is enabled, a Multichain balance button will be displayed on the address page, which will take you to the portfolio application in the marketplace or to an external site.
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG | `{ name: string; url: string; logo?: string }` | Multichain portfolio application config See [below](#multichain-button-configuration-properties) | - | - | `{ name: 'zerion', url: 'zerion', logo: 'https://example.com/icon.svg'` |
&nbsp;
#### Multichain button configuration properties
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| name | `string` | Multichain portfolio application name | Required | - | `zerion` |
| url | `string` | Application ID in the marketplace or website URL | Required | - | `zerion` |
| logo | `string` | Multichain portfolio application logo (.svg) url | - | - | `https://example.com/icon.svg` |
&nbsp;
## External services configuration
### Google ReCaptcha
......
......@@ -128,7 +128,7 @@ Type extends EventTypes.FILTERS ? {
'Filter name': string;
} :
Type extends EventTypes.BUTTON_CLICK ? {
'Content': 'Swap button';
'Content': 'Swap button' | 'Multichain';
'Source': string;
} :
Type extends EventTypes.PROMO_BANNER ? {
......
......@@ -75,7 +75,7 @@ export const token: Address = {
coin_balance: '1',
creation_tx_hash: '0xc38cf7377bf72d6436f63c37b01b24d032101f20ec1849286dc703c712f10c98',
creator_address_hash: '0x34A9c688512ebdB575e82C50c9803F6ba2916E72',
exchange_rate: null,
exchange_rate: '0.04311',
implementation_address: null,
has_decompiled_code: false,
has_logs: false,
......
export type MultichainProviderConfig = {
name: string;
url: string;
logo?: string;
};
......@@ -9,7 +9,7 @@ import config from 'configs/app';
import useIsInitialLoading from 'lib/hooks/useIsInitialLoading';
import useIsMobile from 'lib/hooks/useIsMobile';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
interface Props {
address: string;
......
......@@ -17,6 +17,7 @@ import TxEntity from 'ui/shared/entities/tx/TxEntity';
import AddressBalance from './details/AddressBalance';
import AddressNameInfo from './details/AddressNameInfo';
import AddressNetWorth from './details/AddressNetWorth';
import TokenSelect from './tokenSelect/TokenSelect';
import useAddressCountersQuery from './utils/useAddressCountersQuery';
import type { AddressQuery } from './utils/useAddressQuery';
......@@ -129,6 +130,17 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
{ addressQuery.data ? <TokenSelect onClick={ handleCounterItemClick }/> : <Box py="6px">0</Box> }
</DetailsInfoItem>
) }
{ (data.exchange_rate && data.has_tokens) && (
<DetailsInfoItem
title="Net worth"
hint="Total net worth in USD of all tokens for the address"
alignSelf="center"
isLoading={ addressQuery.isPlaceholderData }
>
<AddressNetWorth addressData={ addressQuery.data } isLoading={ addressQuery.isPlaceholderData }/>
</DetailsInfoItem>
)
}
<DetailsInfoItem
title="Transactions"
hint="Number of transactions related to this address"
......
......@@ -7,7 +7,7 @@ import React from 'react';
import solidityScanIcon from 'icons/brands/solidity_scan.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { SOLIDITYSCAN_REPORT } from 'stubs/contract';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore';
......
......@@ -5,7 +5,7 @@ import type { NovesResponseData } from 'types/api/noves';
import dayjs from 'lib/date/dayjs';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import NovesFromTo from 'ui/shared/Noves/NovesFromTo';
......
......@@ -5,7 +5,7 @@ import type { NovesResponseData } from 'types/api/noves';
import dayjs from 'lib/date/dayjs';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import NovesFromTo from 'ui/shared/Noves/NovesFromTo';
type Props = {
......
......@@ -7,7 +7,7 @@ import type { FormSubmitResultWalletClient } from '../types';
import { route } from 'nextjs-routes';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
interface Props {
result: FormSubmitResultWalletClient['result'];
......
......@@ -19,8 +19,8 @@ import useSocketMessage from 'lib/socket/useSocketMessage';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import Hint from 'ui/shared/Hint';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
import ContractSecurityAudits from './ContractSecurityAudits';
......
......@@ -3,7 +3,7 @@ import React from 'react';
import config from 'configs/app';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
interface Props {
className?: string;
......
......@@ -7,7 +7,7 @@ import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs';
import ContainerWithScrollY from 'ui/shared/ContainerWithScrollY';
import FormModal from 'ui/shared/FormModal';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import ContractSubmitAuditForm from './contractSubmitAuditForm/ContractSubmitAuditForm';
......
......@@ -9,7 +9,7 @@ import { route } from 'nextjs-routes';
import useApiQuery from 'lib/api/useApiQuery';
import * as stubs from 'stubs/contract';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import CodeEditor from 'ui/shared/monaco/CodeEditor';
import formatFilePath from 'ui/shared/monaco/utils/formatFilePath';
......
......@@ -67,7 +67,7 @@ const AddressBalance = ({ data, isLoading }: Props) => {
return (
<DetailsInfoItem
title="Balance"
title={ `${ currencyUnits.ether } balance` }
hint={ `Address balance in ${ currencyUnits.ether }. Doesn't include ERC20, ERC721 and ERC1155 tokens` }
flexWrap="nowrap"
alignSelf="center"
......
......@@ -8,7 +8,7 @@ import type { AddressCounters } from 'types/api/address';
import { route } from 'nextjs-routes';
import type { ResourceError } from 'lib/api/resources';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
interface Props {
prop: keyof AddressCounters;
......
import React from 'react';
import * as addressMock from 'mocks/address/address';
import * as tokensMock from 'mocks/address/tokens';
import { test, expect } from 'playwright/lib';
import TestApp from 'playwright/TestApp';
import AddressNetWorth from './AddressNetWorth';
const ADDRESS_HASH = addressMock.hash;
const ICON_URL = 'https://localhost:3000/my-icon.png';
test.beforeEach(async({ mockApiResponse }) => {
await mockApiResponse('address_tokens', tokensMock.erc20List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' } });
await mockApiResponse('address_tokens', tokensMock.erc721List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-721' } });
await mockApiResponse('address_tokens', tokensMock.erc1155List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-1155' } });
await mockApiResponse('address_tokens', tokensMock.erc404List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-404' } });
});
test('base view', async({ mount }) => {
const component = await mount(
<TestApp>
<AddressNetWorth addressData={ addressMock.token }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('with multichain button internal +@dark-mode', async({ mount, mockEnvs, mockAssetResponse }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG', `{"name": "zerion", "url": "zerion", "logo": "${ ICON_URL }"}` ],
]);
await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg');
const component = await mount(
<TestApp>
<AddressNetWorth addressData={ addressMock.token }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('with multichain button external', async({ mount, mockEnvs, mockAssetResponse }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG', `{"name": "zerion", "url": "https://duck.url", "logo": "${ ICON_URL }"}` ],
]);
await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg');
const component = await mount(
<TestApp>
<AddressNetWorth addressData={ addressMock.token }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Image, Skeleton, Text } from '@chakra-ui/react';
import _capitalize from 'lodash/capitalize';
import React from 'react';
import type { Address } from 'types/api/address';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import getCurrencyValue from 'lib/getCurrencyValue';
import * as mixpanel from 'lib/mixpanel/index';
import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator';
import { getTokensTotalInfo } from '../utils/tokenUtils';
import useFetchTokens from '../utils/useFetchTokens';
const multichainFeature = config.features.multichainButton;
type Props = {
addressData?: Address;
isLoading?: boolean;
}
const AddressNetWorth = ({ addressData, isLoading }: Props) => {
const { data, isError, isPending } = useFetchTokens({ hash: addressData?.hash });
const { usdBn: nativeUsd } = getCurrencyValue({
value: addressData?.coin_balance || '0',
accuracy: 8,
accuracyUsd: 2,
exchangeRate: addressData?.exchange_rate,
decimals: String(config.chain.currency.decimals),
});
const { usd, isOverflow } = getTokensTotalInfo(data);
const prefix = isOverflow ? '>' : '';
const totalUsd = nativeUsd.plus(usd);
const onMultichainClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.BUTTON_CLICK, { Content: 'Multichain', Source: 'address' });
}, []);
let multichainItem = null;
if (multichainFeature.isEnabled) {
const buttonContent = (
<>
{ multichainFeature.logoUrl &&
<Image src={ multichainFeature.logoUrl } alt={ multichainFeature.name } boxSize={ 5 } mr={ 2 } borderRadius="4px" overflow="hidden"/>
}
{ _capitalize(multichainFeature.name) }</>
);
const linkProps = {
variant: 'subtle' as const,
display: 'flex',
alignItems: 'center',
fontSize: 'sm',
fontWeight: 500,
onClick: onMultichainClick,
};
multichainItem = (
<>
<TextSeparator mx={ 3 } color="gray.500"/>
<Text mr={ 2 }>Multichain</Text>
{ 'url' in multichainFeature ? (
<LinkExternal
href={ multichainFeature.url }
{ ...linkProps }
>
{ buttonContent }
</LinkExternal>
) : (
<LinkInternal
href={ route({ pathname: '/apps/[id]', query: { id: multichainFeature.dappId, utm_source: 'blockscout', utm_medium: 'address-page' } }) }
{ ...linkProps }
>
{ buttonContent }
</LinkInternal>
) }
</>
);
}
return (
<Skeleton display="flex" alignItems="center" isLoaded={ !isLoading && !isPending }>
<Text>
{ isError ? 'N/A' : `${ prefix }$${ totalUsd.toFormat(2) }` }
</Text>
{ multichainItem }
</Skeleton>
);
};
export default AddressNetWorth;
......@@ -25,7 +25,7 @@ import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs';
import EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import PopoverTriggerTooltip from 'ui/shared/PopoverTriggerTooltip';
interface Props {
......
......@@ -6,7 +6,7 @@ import { route } from 'nextjs-routes';
import getCurrencyValue from 'lib/getCurrencyValue';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import TruncatedValue from 'ui/shared/TruncatedValue';
import type { TokenEnhancedData } from '../utils/tokenUtils';
......
......@@ -8,7 +8,7 @@ import { apos } from 'lib/html-entities';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import NftFallback from 'ui/shared/nft/NftFallback';
import Pagination from 'ui/shared/pagination/Pagination';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
......
......@@ -12,6 +12,7 @@ import useSocketMessage from 'lib/socket/useSocketMessage';
import { calculateUsdValue } from './tokenUtils';
interface Props {
hash?: string;
enabled?: boolean;
}
const tokenBalanceItemIdentityFactory = (match: AddressTokenBalance) => (item: AddressTokenBalance) => ((
......@@ -20,26 +21,26 @@ const tokenBalanceItemIdentityFactory = (match: AddressTokenBalance) => (item: A
match.token_instance?.id === item.token_instance?.id
));
export default function useFetchTokens({ hash }: Props) {
export default function useFetchTokens({ hash, enabled }: Props) {
const erc20query = useApiQuery('address_tokens', {
pathParams: { hash },
queryParams: { type: 'ERC-20' },
queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
queryOptions: { enabled: Boolean(hash) && enabled, refetchOnMount: false },
});
const erc721query = useApiQuery('address_tokens', {
pathParams: { hash },
queryParams: { type: 'ERC-721' },
queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
queryOptions: { enabled: Boolean(hash) && enabled, refetchOnMount: false },
});
const erc1155query = useApiQuery('address_tokens', {
pathParams: { hash },
queryParams: { type: 'ERC-1155' },
queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
queryOptions: { enabled: Boolean(hash) && enabled, refetchOnMount: false },
});
const erc404query = useApiQuery('address_tokens', {
pathParams: { hash },
queryParams: { type: 'ERC-404' },
queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
queryOptions: { enabled: Boolean(hash) && enabled, refetchOnMount: false },
});
const queryClient = useQueryClient();
......
......@@ -16,7 +16,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import AdminSupportText from 'ui/shared/texts/AdminSupportText';
import AddressVerificationFieldAddress from '../fields/AddressVerificationFieldAddress';
......
......@@ -25,7 +25,7 @@ import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import PrevNext from 'ui/shared/PrevNext';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
import TextSeparator from 'ui/shared/TextSeparator';
......
......@@ -17,7 +17,7 @@ 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/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TextSeparator from 'ui/shared/TextSeparator';
import Utilization from 'ui/shared/Utilization/Utilization';
......
......@@ -15,7 +15,7 @@ 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/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator';
import Utilization from 'ui/shared/Utilization/Utilization';
......
......@@ -7,7 +7,7 @@ import useApiQuery from 'lib/api/useApiQuery';
import { STATS_CHARTS } from 'stubs/stats';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ChartWidgetContainer from 'ui/stats/ChartWidgetContainer';
const GAS_PRICE_CHART_ID = 'averageGasPrice';
......
......@@ -16,7 +16,7 @@ import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { BLOCK } from 'stubs/block';
import { HOMEPAGE_STATS } from 'stubs/stats';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import LatestBlocksItem from './LatestBlocksItem';
......
......@@ -11,7 +11,7 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { L2_DEPOSIT_ITEM } from 'stubs/L2';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import LatestDepositsItem from './LatestDepositsItem';
......
......@@ -8,7 +8,7 @@ import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import { TX } from 'stubs/tx';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import LatestTxsItem from './LatestTxsItem';
......
......@@ -7,7 +7,7 @@ import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { TX } from 'stubs/tx';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import LatestTxsItem from './LatestTxsItem';
import LatestTxsItemMobile from './LatestTxsItemMobile';
......
......@@ -13,7 +13,7 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { ZKEVM_L2_TXN_BATCHES_ITEM } from 'stubs/zkEvmL2';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import LatestZkevmL2BatchItem from './LatestZkevmL2BatchItem';
......
......@@ -12,7 +12,7 @@ import { route } from 'nextjs-routes';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ZkEvmL2TxnBatchStatus from 'ui/shared/statusTag/ZkEvmL2TxnBatchStatus';
type Props = {
......
......@@ -5,7 +5,7 @@ import type { SolidityscanReport } from 'types/api/contract';
import config from 'configs/app';
import * as mixpanel from 'lib/mixpanel/index';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore';
......
......@@ -6,7 +6,7 @@ import config from 'configs/app';
import { apos } from 'lib/html-entities';
import EmptySearchResultDefault from 'ui/shared/EmptySearchResult';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
const feature = config.features.marketplace;
......
......@@ -10,8 +10,8 @@ import config from 'configs/app';
import { useAppContext } from 'lib/contexts/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop';
......
......@@ -12,7 +12,7 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import NftEntity from 'ui/shared/entities/nft/NftEntity';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator';
import NameDomainExpiryStatus from './NameDomainExpiryStatus';
......
......@@ -17,7 +17,7 @@ import MarketplaceListWithScores from 'ui/marketplace/MarketplaceListWithScores'
import FilterInput from 'ui/shared/filters/FilterInput';
import IconSvg from 'ui/shared/IconSvg';
import type { IconName } from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle';
import RadioButtonGroup from 'ui/shared/radioButtonGroup/RadioButtonGroup';
import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
......
......@@ -17,7 +17,7 @@ import TextAd from 'ui/shared/ad/TextAd';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
......
......@@ -23,7 +23,7 @@ import TextAd from 'ui/shared/ad/TextAd';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import Tag from 'ui/shared/chakra/Tag';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
......
......@@ -20,8 +20,8 @@ import * as TxEntity from 'ui/shared/entities/tx/TxEntity';
import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import type { SearchResultAppItem } from 'ui/shared/search/utils';
import { getItemCategory, searchItemTitles } from 'ui/shared/search/utils';
......
......@@ -20,8 +20,8 @@ import * as TxEntity from 'ui/shared/entities/tx/TxEntity';
import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import type { SearchResultAppItem } from 'ui/shared/search/utils';
import { getItemCategory, searchItemTitles } from 'ui/shared/search/utils';
interface Props {
......
......@@ -8,7 +8,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import * as mixpanel from 'lib/mixpanel/index';
import LinkExternal from '../LinkExternal';
import LinkExternal from '../links/LinkExternal';
type Props = {
data: NonNullable<AddressMetadataTagFormatted['meta']>;
......
......@@ -4,8 +4,8 @@ import React from 'react';
import type { EntityTag } from './types';
import * as mixpanel from 'lib/mixpanel/index';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import { getTagLinkParams } from './utils';
......
......@@ -5,7 +5,7 @@ import type { EntityTag } from './types';
import makePrettyLink from 'lib/makePrettyLink';
import * as mixpanel from 'lib/mixpanel/index';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
interface Props {
data: EntityTag;
......
......@@ -19,7 +19,7 @@ import type { NetworkExplorer as TNetworkExplorer } from 'types/networks';
import config from 'configs/app';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import PopoverTriggerTooltip from 'ui/shared/PopoverTriggerTooltip';
interface Props {
......
......@@ -5,7 +5,7 @@ import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import TextAd from 'ui/shared/ad/TextAd';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
type BackLinkProp = { label: string; url: string } | { label: string; onClick: () => void };
......
......@@ -8,8 +8,8 @@ import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import { getIconProps, type IconSize } from './utils';
......
......@@ -7,7 +7,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import GasInfoTooltipRow from './GasInfoTooltipRow';
import GasInfoUpdateTimer from './GasInfoUpdateTimer';
......
import type { ChakraProps, LinkProps } from '@chakra-ui/react';
import { Link, chakra, Box, Skeleton, useColorModeValue } from '@chakra-ui/react';
import type { LinkProps } from '@chakra-ui/react';
import { Link, chakra, Box, Skeleton } from '@chakra-ui/react';
import React from 'react';
import IconSvg from 'ui/shared/IconSvg';
import type { Variants } from './useLinkStyles';
import { useLinkStyles } from './useLinkStyles';
interface Props {
href: string;
className?: string;
children: React.ReactNode;
isLoading?: boolean;
variant?: 'subtle';
variant?: Variants;
iconColor?: LinkProps['color'];
onClick?: LinkProps['onClick'];
}
const LinkExternal = ({ href, children, className, isLoading, variant, iconColor, onClick }: Props) => {
const subtleLinkBg = useColorModeValue('gray.100', 'gray.700');
const styleProps: ChakraProps = (() => {
const commonProps = {
display: 'inline-block',
alignItems: 'center',
};
const commonProps = {
display: 'inline-block',
alignItems: 'center',
};
switch (variant) {
case 'subtle': {
return {
...commonProps,
px: '10px',
py: '6px',
bgColor: subtleLinkBg,
borderRadius: 'base',
};
}
default:{
return commonProps;
}
}
})();
const styleProps = useLinkStyles(commonProps, variant);
if (isLoading) {
if (variant === 'subtle') {
......
......@@ -5,18 +5,28 @@ import NextLink from 'next/link';
import type { LegacyRef } from 'react';
import React from 'react';
const LinkInternal = ({ isLoading, ...props }: LinkProps & { isLoading?: boolean }, ref: LegacyRef<HTMLAnchorElement>) => {
import type { Variants } from './useLinkStyles';
import { useLinkStyles } from './useLinkStyles';
type Props = LinkProps & {
variant?: Variants;
isLoading?: boolean;
}
const LinkInternal = ({ isLoading, variant, ...props }: Props, ref: LegacyRef<HTMLAnchorElement>) => {
const styleProps = useLinkStyles({}, variant);
if (isLoading) {
return <Flex alignItems="center" { ...props as FlexProps }>{ props.children }</Flex>;
return <Flex alignItems="center" { ...props as FlexProps } { ...styleProps }>{ props.children }</Flex>;
}
if (!props.href) {
return <Link { ...props } ref={ ref }/>;
return <Link { ...props } ref={ ref } { ...styleProps }/>;
}
return (
<NextLink href={ props.href as NextLinkProps['href'] } passHref target={ props.target } legacyBehavior>
<Link { ...props } ref={ ref }/>
<Link { ...props } ref={ ref } { ...styleProps }/>
</NextLink>
);
};
......
import type { ChakraProps } from '@chakra-ui/react';
import { useColorModeValue } from '@chakra-ui/react';
export type Variants = 'subtle'
export function useLinkStyles(commonProps: ChakraProps, variant?: Variants) {
const subtleLinkBg = useColorModeValue('gray.100', 'gray.700');
switch (variant) {
case 'subtle': {
return {
...commonProps,
px: '10px',
py: '6px',
bgColor: subtleLinkBg,
borderRadius: 'base',
};
}
default:{
return commonProps;
}
}
}
......@@ -20,7 +20,7 @@ import { route } from 'nextjs-routes';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import { getRecentSearchKeywords, saveToRecentKeywords } from 'lib/recentSearchKeywords';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import SearchBarBackdrop from './SearchBarBackdrop';
import SearchBarInput from './SearchBarInput';
......
......@@ -7,7 +7,7 @@ import { route } from 'nextjs-routes';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import NftMedia from 'ui/shared/nft/NftMedia';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
......
......@@ -6,7 +6,7 @@ import type { TokenVerifiedInfo as TTokenVerifiedInfo } from 'types/api/token';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import TokenProjectInfo from './TokenProjectInfo';
......
......@@ -7,7 +7,7 @@ import type { MetadataAttributes } from 'types/client/token';
import parseMetadata from 'lib/token/parseMetadata';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import TruncatedValue from 'ui/shared/TruncatedValue';
interface Props {
......
......@@ -5,7 +5,7 @@ import { route } from 'nextjs-routes';
import useApiQuery from 'lib/api/useApiQuery';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
interface Props {
hash: string;
......
......@@ -3,7 +3,7 @@ import React from 'react';
import type { Primitive } from 'react-hook-form';
import urlParser from 'lib/token/metadata/urlParser';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import MetadataAccordionItem from './MetadataAccordionItem';
import MetadataAccordionItemTitle from './MetadataAccordionItemTitle';
......
......@@ -7,7 +7,7 @@ import { route } from 'nextjs-routes';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import TxDetailsTokenTransfer from './TxDetailsTokenTransfer';
......
......@@ -9,7 +9,7 @@ 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/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const rollupFeature = config.features.rollup;
......
......@@ -9,7 +9,7 @@ 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/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
const rollupFeature = config.features.rollup;
......
......@@ -18,7 +18,7 @@ import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import DetailsTimestamp from 'ui/shared/DetailsTimestamp';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import PrevNext from 'ui/shared/PrevNext';
import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps';
......
......@@ -9,7 +9,7 @@ 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/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import ZkEvmL2TxnBatchStatus from 'ui/shared/statusTag/ZkEvmL2TxnBatchStatus';
......
......@@ -9,7 +9,7 @@ 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/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ZkEvmL2TxnBatchStatus from 'ui/shared/statusTag/ZkEvmL2TxnBatchStatus';
const rollupFeature = config.features.rollup;
......
......@@ -19,7 +19,7 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import DetailsTimestamp from 'ui/shared/DetailsTimestamp';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import PrevNext from 'ui/shared/PrevNext';
import TruncatedValue from 'ui/shared/TruncatedValue';
import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps';
......
......@@ -9,7 +9,7 @@ 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/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import ZkSyncL2TxnBatchStatus from 'ui/shared/statusTag/ZkSyncL2TxnBatchStatus';
......
......@@ -9,7 +9,7 @@ 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/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ZkSyncL2TxnBatchStatus from 'ui/shared/statusTag/ZkSyncL2TxnBatchStatus';
const rollupFeature = config.features.rollup;
......
......@@ -11,7 +11,7 @@ import getValueWithUnit from 'lib/getValueWithUnit';
import { currencyUnits } from 'lib/units';
import CurrencyValue from 'ui/shared/CurrencyValue';
import BlobEntity from 'ui/shared/entities/blob/BlobEntity';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator';
import TxFeeStability from 'ui/shared/tx/TxFeeStability';
import Utilization from 'ui/shared/Utilization/Utilization';
......
......@@ -8,7 +8,7 @@ import dayjs from 'lib/date/dayjs';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const rollupFeature = config.features.rollup;
......
......@@ -8,7 +8,7 @@ import dayjs from 'lib/date/dayjs';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
const rollupFeature = config.features.rollup;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment