Skip to content

Commit

Permalink
feat: modularize token-selector component, better default logo icons,…
Browse files Browse the repository at this point in the history
… abstract token metadata
  • Loading branch information
kemuru committed May 24, 2024
1 parent 1ff8771 commit 09b3596
Show file tree
Hide file tree
Showing 11 changed files with 240 additions and 198 deletions.
Binary file added web/src/assets/svgs/icons/eth-token-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions web/src/components/TransactionCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import TransactionInfo from "../TransactionInfo";
import StatusBanner from "./StatusBanner";
import { useNavigateAndScrollTop } from "hooks/useNavigateAndScrollTop";
import { useNativeTokenSymbol } from "hooks/useNativeTokenSymbol";
import { useERC20TokenSymbol } from "hooks/useERC20TokenSymbol";
import useFetchIpfsJson from "hooks/useFetchIpfsJson";
import { useTokenMetadata } from "hooks/useTokenMetadata";
import { TransactionDetailsFragment } from "src/graphql/graphql";

const StyledCard = styled(Card)`
Expand Down Expand Up @@ -70,7 +70,8 @@ const TransactionCard: React.FC<ITransactionCard> = ({
const transactionInfo = useFetchIpfsJson(transactionUri);
const { isList } = useIsList();
const nativeTokenSymbol = useNativeTokenSymbol();
const { erc20TokenSymbol } = useERC20TokenSymbol(token);
const { tokenMetadata } = useTokenMetadata(token);
const erc20TokenSymbol = tokenMetadata?.symbol;
const title = transactionInfo?.title;
const navigateAndScrollTop = useNavigateAndScrollTop();

Expand Down
46 changes: 46 additions & 0 deletions web/src/hooks/useFilteredTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useEffect, useState } from "react";
import { useTokenMetadata } from "./useTokenMetadata";
import EthTokenIcon from "svgs/icons/eth-token-icon.png";

export const useFilteredTokens = (searchQuery: string, tokens, setTokens, sendingToken) => {
const { tokenMetadata } = useTokenMetadata(
searchQuery.startsWith("0x") && searchQuery.length === 42 ? searchQuery : null
);
const [filteredTokens, setFilteredTokens] = useState([]);

useEffect(() => {
const handleSearch = async () => {
let filtered = [];

if (!searchQuery) {
filtered = tokens;
if (sendingToken) {
filtered = [sendingToken, ...tokens.filter((token) => token.address !== sendingToken.address)];
}
} else if (tokenMetadata) {
const resultToken = {
symbol: tokenMetadata.symbol,
address: searchQuery.toLowerCase(),
logo: tokenMetadata.logo || EthTokenIcon,
};

const updatedTokens = [...tokens, resultToken];
const uniqueTokens = Array.from(new Set(updatedTokens.map((a) => a.address))).map((address) => {
return updatedTokens.find((a) => a.address === address);
});

filtered = [resultToken];
setTokens(uniqueTokens);
localStorage.setItem("tokens", JSON.stringify(uniqueTokens));
} else {
filtered = tokens.filter((token) => token.symbol.toLowerCase().includes(searchQuery.toLowerCase()));
}

setFilteredTokens(filtered);
};

handleSearch();
}, [searchQuery, tokens, setTokens, sendingToken, tokenMetadata]);

return { filteredTokens };
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,25 @@ import { Alchemy } from "alchemy-sdk";
import { useNetwork } from "wagmi";
import { alchemyConfig } from "utils/alchemyConfig";

export const useERC20TokenSymbol = (tokenAddress: string) => {
export const useTokenMetadata = (tokenAddress: string) => {
const { chain } = useNetwork();
const [erc20TokenSymbol, setErc20TokenSymbol] = useState<string | null>(null);
const [tokenMetadata, setTokenMetadata] = useState(null);

useEffect(() => {
const fetchTokenSymbol = async () => {
const fetchTokenMetadata = async () => {
if (!tokenAddress || tokenAddress === "native") return;
const alchemy = new Alchemy(alchemyConfig(chain?.id));
try {
const metadata = await alchemy.core.getTokenMetadata(tokenAddress);
setErc20TokenSymbol(metadata.symbol || null);
setTokenMetadata(metadata);
} catch (error) {
console.error("Error fetching token symbol:", error);
setErc20TokenSymbol(null);
console.error("Error fetching token metadata:", error);
setTokenMetadata(null);
}
};

fetchTokenSymbol();
fetchTokenMetadata();
}, [tokenAddress, chain?.id]);

return { erc20TokenSymbol };
return { tokenMetadata };
};
5 changes: 3 additions & 2 deletions web/src/pages/MyTransactions/TransactionDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { useEscrowParametersQuery } from "queries/useEscrowParametersQuery";
import { useTransactionDetailsQuery } from "queries/useTransactionsQuery";
import { useArbitrationCost } from "queries/useArbitrationCostFromKlerosCore";
import { useNativeTokenSymbol } from "hooks/useNativeTokenSymbol";
import { useERC20TokenSymbol } from "hooks/useERC20TokenSymbol";
import useFetchIpfsJson from "hooks/useFetchIpfsJson";
import { useTokenMetadata } from "hooks/useTokenMetadata";

const Container = styled.div``;

Expand All @@ -33,7 +33,8 @@ const TransactionDetails: React.FC = () => {
const { data: escrowParameters } = useEscrowParametersQuery();
const { arbitrationCost } = useArbitrationCost(escrowParameters?.escrowParameters?.arbitratorExtraData);
const nativeTokenSymbol = useNativeTokenSymbol();
const { erc20TokenSymbol } = useERC20TokenSymbol(transactionDetails?.escrow?.token);
const { tokenMetadata } = useTokenMetadata(transactionDetails?.escrow?.token);
const erc20TokenSymbol = tokenMetadata?.symbol;
const { setTransactionDetails } = useTransactionDetailsContext();

const {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from "react";
import styled from "styled-components";
import Skeleton from "react-loading-skeleton";

const Container = styled.div`
border: 1px solid ${({ theme }) => theme.stroke};
border-radius: 3px;
width: 186px;
height: 45px;
position: relative;
padding: 9.5px 14px;
cursor: pointer;
background: ${({ theme }) => theme.whiteBackground};
color: ${({ theme }) => theme.primaryText};
display: flex;
align-items: center;
justify-content: space-between;
`;

const DropdownArrow = styled.span`
border: solid ${({ theme }) => theme.stroke};
border-width: 0 1px 1px 0;
display: inline-block;
padding: 3px;
transform: rotate(45deg);
margin-left: 8px;
`;

const TokenLogo = styled.img`
width: 24px;
height: 24px;
`;

const DropdownContent = styled.div`
display: flex;
align-items: center;
gap: 8px;
`;

const LogoSkeleton = styled(Skeleton)`
width: 24px;
height: 24px;
border-radius: 50%;
margin-bottom: 2px;
`;

const SymbolSkeleton = styled(Skeleton)`
width: 40px;
height: 16px;
`;

export const DropdownButton = ({ loading, sendingToken, onClick }) => (
<Container onClick={onClick}>
<DropdownContent>
{loading ? (
<LogoSkeleton />
) : (
sendingToken && <TokenLogo src={sendingToken.logo} alt={`${sendingToken.symbol} logo`} />
)}
{loading ? <SymbolSkeleton /> : sendingToken?.symbol}
</DropdownContent>
<DropdownArrow />
</Container>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from "react";
import styled from "styled-components";

const Container = styled.div<{ selected: boolean }>`
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
cursor: pointer;
background: ${({ theme, selected }) => (selected ? theme.mediumBlue : "transparent")};
border-left: ${({ theme, selected }) => (selected ? `3px solid ${theme.primaryBlue}` : "none")};
padding-left: ${({ selected }) => (selected ? "13px" : "16px")};
&:hover {
background: ${({ theme }) => theme.lightBlue};
}
`;

const TokenLogo = styled.img`
width: 24px;
height: 24px;
`;

const TokenLabel = styled.span`
color: ${({ theme }) => theme.primaryText};
`;

const TokenItem = ({ token, selected, onSelect }) => (
<Container selected={selected} onClick={() => onSelect(token)}>
<TokenLogo src={token.logo} alt={`${token.symbol} logo`} />
<TokenLabel>{token.symbol}</TokenLabel>
</Container>
);

export default TokenItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useRef, useState } from "react";
import styled from "styled-components";
import { useClickAway } from "react-use";
import { Searchbar } from "@kleros/ui-components-library";
import { useNewTransactionContext } from "context/NewTransactionContext";
import { Overlay } from "components/Overlay";
import TokenItem from "./TokenItem";
import { StyledModal } from "pages/MyTransactions/Modal/StyledModal";
import { useFilteredTokens } from "hooks/useFilteredTokens";

const ReStyledModal = styled(StyledModal)`
display: flex;
width: 500px;
`;

const StyledSearchbar = styled(Searchbar)`
width: 100%;
input {
font-size: 16px;
}
`;

const ItemsContainer = styled.div`
display: flex;
width: 100%;
flex-direction: column;
margin-top: 24px;
`;

const StyledP = styled.p`
display: flex;
align-self: flex-start;
font-weight: 600;
margin: 0;
margin-bottom: 28px;
`;

export const TokenListModal = ({ setIsOpen, tokens, setTokens, handleSelectToken }) => {
const [searchQuery, setSearchQuery] = useState("");
const { sendingToken } = useNewTransactionContext();
const { filteredTokens } = useFilteredTokens(searchQuery, tokens, setTokens, sendingToken);
const containerRef = useRef(null);
useClickAway(containerRef, () => setIsOpen(false));

return (
<>
<Overlay />
<ReStyledModal ref={containerRef}>
<StyledP>Select a token</StyledP>
<StyledSearchbar
placeholder="Search by name or paste address"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<ItemsContainer>
{filteredTokens.map((token) => (
<TokenItem
key={token.address}
selected={sendingToken?.address === token.address}
onSelect={handleSelectToken}
{...{ token }}
/>
))}
</ItemsContainer>
</ReStyledModal>
</>
);
};
Loading

0 comments on commit 09b3596

Please sign in to comment.