mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
179 lines
6.4 KiB
TypeScript
179 lines
6.4 KiB
TypeScript
import { Separator } from '@luxfi/ui/separator';
|
|
import React, { useState, useCallback, useMemo } from 'react';
|
|
|
|
import type { EssentialDappsChainConfig } from 'types/client/marketplace';
|
|
import type { AllowanceType } from 'types/client/revoke';
|
|
|
|
import { route } from 'nextjs/routes';
|
|
|
|
import useIsMobile from 'lib/hooks/useIsMobile';
|
|
import { Badge } from '@luxfi/ui/badge';
|
|
import { Heading } from '@luxfi/ui/heading';
|
|
import { Image } from '@luxfi/ui/image';
|
|
import { Link } from 'toolkit/next/link';
|
|
import { Skeleton } from '@luxfi/ui/skeleton';
|
|
import { Tooltip } from '@luxfi/ui/tooltip';
|
|
import TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
|
|
|
|
import type useApprovalsQuery from '../hooks/useApprovalsQuery';
|
|
import type useCoinBalanceQuery from '../hooks/useCoinBalanceQuery';
|
|
import AddressEntity from './AddressEntity';
|
|
import Approvals from './Approvals';
|
|
|
|
type Props = {
|
|
searchAddress: string;
|
|
selectedChain: EssentialDappsChainConfig | undefined;
|
|
isAddressMatch: boolean;
|
|
coinBalanceQuery: ReturnType<typeof useCoinBalanceQuery>;
|
|
approvalsQuery: ReturnType<typeof useApprovalsQuery>;
|
|
};
|
|
|
|
const Content = ({
|
|
searchAddress,
|
|
selectedChain,
|
|
isAddressMatch,
|
|
coinBalanceQuery,
|
|
approvalsQuery,
|
|
}: Props) => {
|
|
const isMobile = useIsMobile();
|
|
const [ hiddenApprovals, setHiddenApprovals ] = useState<Array<AllowanceType>>([]);
|
|
|
|
const approvals = useMemo(() => {
|
|
return approvalsQuery.data?.filter((approval) => !hiddenApprovals.includes(approval));
|
|
}, [ approvalsQuery.data, hiddenApprovals ]);
|
|
|
|
const totalValueAtRiskUsd = useMemo(() => {
|
|
if (approvalsQuery.isPlaceholderData || !approvals) return 0;
|
|
|
|
const maxValues: Record<`0x${ string }`, number> = {};
|
|
|
|
approvals.forEach((item) => {
|
|
const { address, valueAtRiskUsd } = item;
|
|
|
|
if (!valueAtRiskUsd) return;
|
|
|
|
if (
|
|
maxValues[address] === undefined ||
|
|
valueAtRiskUsd > maxValues[address]
|
|
) {
|
|
maxValues[address] = valueAtRiskUsd;
|
|
}
|
|
});
|
|
|
|
const sum = Object.values(maxValues).reduce((sum, val) => sum + val, 0);
|
|
|
|
return Number(sum.toFixed(2)).toLocaleString();
|
|
}, [ approvalsQuery.isPlaceholderData, approvals ]);
|
|
|
|
const hideApproval = useCallback((approval: AllowanceType) => {
|
|
setHiddenApprovals((prev) => [ ...prev, approval ]);
|
|
}, []);
|
|
|
|
return (
|
|
<div className="flex flex-col w-full">
|
|
<div
|
|
className="flex flex-col lg:flex-row gap-2 -mt-2 pt-2 pb-6 bg-white dark:bg-black"
|
|
style={ !isMobile && approvals?.length ? { position: 'sticky', top: 0, zIndex: 1 } : undefined }
|
|
>
|
|
<div className="flex flex-col items-start flex-1 bg-black/5 dark:bg-white/5 gap-3 p-6 rounded">
|
|
<div className="flex flex-col md:flex-row gap-3 items-start md:items-center justify-start md:justify-between w-full flex-wrap">
|
|
<div className="flex gap-2 items-center">
|
|
<AddressEntity
|
|
address={{ hash: searchAddress }}
|
|
truncation="constant"
|
|
className="text-lg lg:text-xl font-medium"
|
|
icon={{ size: isMobile ? undefined : 30 }}
|
|
noLink
|
|
/>
|
|
<Tooltip content="Connect a wallet to revoke approvals" disabled={ isAddressMatch } disableOnMobile>
|
|
<Badge colorPalette={ isAddressMatch ? 'green' : 'gray' }>
|
|
{ isAddressMatch ? 'Connected' : 'Not connected' }
|
|
</Badge>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-3 items-center flex-wrap">
|
|
<Skeleton
|
|
loading={ coinBalanceQuery.isPlaceholderData }
|
|
display="flex"
|
|
className="gap-3"
|
|
>
|
|
{ (coinBalanceQuery.isPlaceholderData ||
|
|
coinBalanceQuery.data) && (
|
|
<>
|
|
<div className="flex gap-2 items-center lg:ml-[5px]">
|
|
<Image
|
|
src={ coinBalanceQuery.data.coinImage }
|
|
alt={ coinBalanceQuery.data.symbol }
|
|
boxSize={ 5 }
|
|
fallback={ <TokenLogoPlaceholder/> }
|
|
/>
|
|
<span className="text-sm font-medium">
|
|
{ coinBalanceQuery.data.balance }{ ' ' }
|
|
{ coinBalanceQuery.data.symbol }
|
|
</span>
|
|
</div>
|
|
<span className="text-sm font-medium text-[var(--color-text-secondary)]">
|
|
${ coinBalanceQuery.data.balanceUsd }
|
|
</span>
|
|
</>
|
|
) }
|
|
</Skeleton>
|
|
<Link
|
|
href={ route({ pathname: '/address/[hash]', query: { hash: searchAddress } }, { chain: selectedChain, external: true }) }
|
|
external
|
|
className="text-sm font-medium"
|
|
noIcon
|
|
>
|
|
View details
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
<div className="flex w-full lg:w-[400px] bg-black/5 dark:bg-white/5 p-6 rounded">
|
|
<div className="flex flex-col flex-1 justify-center items-center gap-2">
|
|
<span className="text-sm font-medium text-[var(--color-text-secondary)]">
|
|
Total approvals
|
|
</span>
|
|
<Skeleton
|
|
loading={ approvalsQuery.isPlaceholderData }
|
|
minW="40px"
|
|
className="text-center"
|
|
>
|
|
<Heading level="3">
|
|
{ approvals?.length || 0 }
|
|
</Heading>
|
|
</Skeleton>
|
|
</div>
|
|
<Separator
|
|
orientation="vertical"
|
|
className="mx-4 md:mx-8"
|
|
/>
|
|
<div className="flex flex-col flex-1 justify-center items-center gap-2">
|
|
<span className="text-sm font-medium text-[var(--color-text-secondary)]">
|
|
Total value at risk
|
|
</span>
|
|
<Skeleton
|
|
loading={ approvalsQuery.isPlaceholderData }
|
|
minW="40px"
|
|
className="text-center"
|
|
>
|
|
<Heading level="3">
|
|
${ totalValueAtRiskUsd }
|
|
</Heading>
|
|
</Skeleton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Approvals
|
|
selectedChain={ selectedChain }
|
|
approvals={ approvals || [] }
|
|
isLoading={ approvalsQuery.isPlaceholderData }
|
|
isAddressMatch={ isAddressMatch }
|
|
hideApproval={ hideApproval }
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Content;
|