Compare commits

...

9 Commits

Author SHA1 Message Date
Moody Salem
63af1a160d Fix lint errors 2020-05-22 09:43:11 -04:00
Moody Salem
85d52b3480 perf(search modal): refactor before more dramatic changes 2020-05-22 09:36:09 -04:00
Moody Salem
219de1f471 fix(release): remove console.log statement 2020-05-22 08:24:16 -04:00
Moody Salem
f110fa7732 chore(release): update DNS on release 2020-05-22 08:08:11 -04:00
Ian Lapham
513a1b0c4b add default text on create flow (#825) 2020-05-21 19:16:42 -04:00
Moody Salem
96c9eede18 chore(release): automatically publish daily releases 2020-05-21 15:55:12 -04:00
Moody Salem
f4a97501e5 chore(readme): update the README.md 2020-05-21 15:54:28 -04:00
Moody Salem
2ff5ce62db chore(package.json): every dependency is a dev dependency, clean up generated release description 2020-05-21 15:47:52 -04:00
Moody Salem
79176dfe79 chore(release): Change the release schedule to daily 2020-05-21 15:38:30 -04:00
9 changed files with 289 additions and 310 deletions

View File

@@ -1,12 +1,8 @@
name: Daily Release
name: Release
# every morning
#on:
# schedule:
# - cron: '0 12 * * *'
on:
push:
branches:
- v2
schedule:
- cron: '0 12 * * *'
jobs:
create-release:
name: Create Release
@@ -48,6 +44,10 @@ jobs:
pinata-api-key: ${{ secrets.PINATA_API_KEY }}
pinata-secret-api-key: ${{ secrets.PINATA_API_SECRET_KEY }}
- name: Update DNS with new IPFS hash
id: update_dns
run: npx vercel --token ${{ secrets.VERCEL_TOKEN }} --scope uniswap dns add uniswap.org _dnslink.app TXT "dnslink=/ipfs/${{ steps.upload.outputs.hash }}"
- name: Create GitHub Release
id: create_release
uses: actions/create-release@v1.1.0
@@ -56,7 +56,6 @@ jobs:
with:
tag_name: ${{ steps.bump_version.outputs.new_tag }}
release_name: Release ${{ steps.bump_version.outputs.new_tag }}
draft: true
body: |
Release built from commit [`${{ github.sha }}`](https://github.com/Uniswap/uniswap-frontend/tree/${{ github.sha }})
@@ -68,6 +67,4 @@ jobs:
- https://ipfs.io/ipfs/${{ steps.upload.outputs.hash }}/
- https://dweb.link/ipfs/${{ steps.upload.outputs.hash }}/
Changes since the last release below.
${{ steps.bump_version.outputs.changelog }}

View File

@@ -3,6 +3,7 @@
[![Netlify Status](https://api.netlify.com/api/v1/badges/fa110555-b3c7-4eeb-b840-88a835009c62/deploy-status)](https://app.netlify.com/sites/uniswap/deploys)
[![Tests](https://github.com/Uniswap/uniswap-frontend/workflows/Tests/badge.svg?branch=v2)](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ATests)
[![Styled With Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io/)
[![Release](https://github.com/Uniswap/uniswap-frontend/workflows/Release/badge.svg?branch=v2)](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ARelease)
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
@@ -13,19 +14,15 @@ An open source interface for Uniswap -- a protocol for decentralized exchange of
- Email: [contact@uniswap.org](mailto:contact@uniswap.org)
- Discord: [Uniswap](https://discord.gg/Y7TF6QA)
- Whitepaper: [Link](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
## Run Uniswap Locally
1. Download and unzip the `build.zip` file from the latest release in the [Releases tab](https://github.com/Uniswap/uniswap-frontend/releases/latest).
2. Serve the `build/` folder locally, and access the application via a browser.
For more information on running a local server see
[https://developer.mozilla.org/en-US/docs/Learn/Common_questions/set_up_a_local_testing_server](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/set_up_a_local_testing_server).
## Accessing the frontend
This simple approach has one downside: refreshing the page will give a `404` because of how React handles client-side routing.
To fix this issue, consider running `serve -s` courtesy of the [serve](https://github.com/zeit/serve) package.
## Develop Uniswap Locally
The front end is deployed to IPFS as well as to [uniswap.exchange](https://uniswap.exchange).
To access the front end via IPFS, use a link from the
[latest release](https://github.com/Uniswap/uniswap-frontend/releases/latest).
## Development
### Install Dependencies
@@ -50,12 +47,7 @@ change `REACT_APP_NETWORK_ID` to `"{yourNetworkId}"`, and change `REACT_APP_NETW
Note that the front end only works properly on testnets where both
[Uniswap V2](https://uniswap.org/docs/v2/smart-contracts/factory/) and
[eth-scan](https://github.com/MyCryptoHQ/eth-scan) are deployed.
The frontend is not expected to work with local testnets.
### Deployment
As a single page application, all routes that do not match an asset must be redirect to `/index.html`.
See [create-react-app documentation.](https://create-react-app.dev/docs/deployment#notes-on-client-side-routing).
The frontend will not work on other networks.
## Contributions

View File

@@ -3,20 +3,34 @@
"description": "Uniswap Interface",
"homepage": "https://uniswap.exchange",
"private": true,
"dependencies": {
"devDependencies": {
"@ethersproject/address": "^5.0.0-beta.134",
"@ethersproject/bignumber": "^5.0.0-beta.138",
"@ethersproject/constants": "^5.0.0-beta.133",
"@ethersproject/contracts": "^5.0.0-beta.151",
"@ethersproject/experimental": "^5.0.0-beta.141",
"@ethersproject/providers": "5.0.0-beta.162",
"@ethersproject/strings": "^5.0.0-beta.136",
"@ethersproject/units": "^5.0.0-beta.132",
"@ethersproject/wallet": "^5.0.0-beta.141",
"@material-ui/core": "^4.9.5",
"@mycrypto/eth-scan": "^2.1.0",
"@popperjs/core": "^2.4.0",
"@reach/dialog": "^0.2.8",
"@reach/tooltip": "^0.2.0",
"@reduxjs/toolkit": "^1.3.5",
"@types/jest": "^25.2.1",
"@types/node": "^13.13.5",
"@types/qs": "^6.9.2",
"@types/react": "^16.9.34",
"@types/react-dom": "^16.9.7",
"@types/react-redux": "^7.1.8",
"@types/react-router-dom": "^5.0.0",
"@types/rebass": "^4.0.5",
"@types/styled-components": "^4.2.0",
"@types/testing-library__cypress": "^5.0.5",
"@typescript-eslint/eslint-plugin": "^2.31.0",
"@typescript-eslint/parser": "^2.31.0",
"@uniswap/sdk": "^2.0.5",
"@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "1.0.0-beta.0",
@@ -29,12 +43,19 @@
"@web3-react/walletlink-connector": "^6.0.9",
"copy-to-clipboard": "^3.2.0",
"cross-env": "^7.0.2",
"cypress": "^4.5.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^4.0.0",
"history": "^4.9.0",
"i18next": "^15.0.9",
"i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^2.0.1",
"jazzicon": "^1.5.0",
"polished": "^3.3.2",
"prettier": "^1.17.0",
"qrcode.react": "^0.9.3",
"qs": "^6.9.4",
"react": "^16.13.1",
@@ -51,35 +72,12 @@
"react-use-gesture": "^6.0.14",
"rebass": "^4.0.7",
"redux-localstorage-simple": "^2.2.0",
"styled-components": "^4.2.0",
"swr": "0.1.18",
"use-media": "^1.4.0"
},
"devDependencies": {
"@ethersproject/experimental": "^5.0.0-beta.141",
"@ethersproject/wallet": "^5.0.0-beta.141",
"@types/jest": "^25.2.1",
"@types/node": "^13.13.5",
"@types/qs": "^6.9.2",
"@types/react": "^16.9.34",
"@types/react-dom": "^16.9.7",
"@types/react-redux": "^7.1.8",
"@types/react-router-dom": "^5.0.0",
"@types/rebass": "^4.0.5",
"@types/styled-components": "^4.2.0",
"@types/testing-library__cypress": "^5.0.5",
"@typescript-eslint/eslint-plugin": "^2.31.0",
"@typescript-eslint/parser": "^2.31.0",
"cypress": "^4.5.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^4.0.0",
"prettier": "^1.17.0",
"serve": "^11.3.0",
"start-server-and-test": "^1.11.0",
"typescript": "^3.8.3"
"styled-components": "^4.2.0",
"swr": "0.1.18",
"typescript": "^3.8.3",
"use-media": "^1.4.0"
},
"scripts": {
"start": "react-scripts start",
@@ -109,4 +107,4 @@
]
},
"license": "GPL-3.0-or-later"
}
}

View File

@@ -125,7 +125,6 @@ interface CurrencyInputPanelProps {
onMax?: () => void
showMaxButton: boolean
label?: string
urlAddedTokens?: Token[]
onTokenSelection?: (tokenAddress: string) => void
token?: Token | null
disableTokenSelect?: boolean
@@ -145,7 +144,6 @@ export default function CurrencyInputPanel({
onMax,
showMaxButton,
label = 'Input',
urlAddedTokens = [], // used
onTokenSelection = null,
token = null,
disableTokenSelect = false,
@@ -246,7 +244,6 @@ export default function CurrencyInputPanel({
setModalOpen(false)
}}
filterType="tokens"
urlAddedTokens={urlAddedTokens}
onTokenSelect={onTokenSelection}
showSendWithSwap={showSendWithSwap}
hiddenToken={token?.address}

View File

@@ -0,0 +1,24 @@
import React from 'react'
import { Text } from 'rebass'
import { FilterWrapper } from './styleds'
export function TokenSortButton({
title,
toggleSortOrder,
invertSearchOrder
}: {
title: string
toggleSortOrder: () => void
invertSearchOrder: boolean
}) {
return (
<FilterWrapper onClick={toggleSortOrder}>
<Text fontSize={14} fontWeight={500}>
{title}
</Text>
<Text fontSize={14} fontWeight={500}>
{!invertSearchOrder ? '↓' : '↑'}
</Text>
</FilterWrapper>
)
}

View File

@@ -1,170 +1,66 @@
import React, { useState, useRef, useMemo, useEffect, useContext } from 'react'
import '@reach/tooltip/styles.css'
import styled, { ThemeContext } from 'styled-components'
import { JSBI, Token, WETH } from '@uniswap/sdk'
import { ChainId, JSBI, Token, WETH } from '@uniswap/sdk'
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
import { isMobile } from 'react-device-detect'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { COMMON_BASES } from '../../constants'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { Link as StyledLink } from '../../theme/components'
import Card from '../../components/Card'
import Modal from '../Modal'
import Circle from '../../assets/images/circle.svg'
import TokenLogo from '../TokenLogo'
import DoubleTokenLogo from '../DoubleLogo'
import Column, { AutoColumn } from '../Column'
import { Text } from 'rebass'
import { CursorPointer } from '../../theme'
import { ArrowLeft } from 'react-feather'
import { CloseIcon } from '../../theme/components'
import { ButtonPrimary, ButtonSecondary } from '../../components/Button'
import { Spinner, TYPE } from '../../theme'
import { RowBetween, RowFixed, AutoRow } from '../Row'
import { isAddress, escapeRegExp } from '../../utils'
import { useActiveWeb3React } from '../../hooks'
import {
useAllDummyPairs,
useFetchTokenByAddress,
useAddUserToken,
useRemoveUserAddedToken,
useUserAddedTokens
} from '../../state/user/hooks'
import { useTranslation } from 'react-i18next'
import { useToken, useAllTokens } from '../../hooks/Tokens'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import Circle from '../../assets/images/circle.svg'
import Card from '../../components/Card'
import { COMMON_BASES } from '../../constants'
import { ALL_TOKENS } from '../../constants/tokens'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens, useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useAllDummyPairs, useRemoveUserAddedToken } from '../../state/user/hooks'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { CursorPointer, TYPE } from '../../theme'
import { CloseIcon, Link as StyledLink } from '../../theme/components'
import { escapeRegExp, isAddress } from '../../utils'
import { ButtonPrimary, ButtonSecondary } from '../Button'
import Column, { AutoColumn } from '../Column'
import DoubleTokenLogo from '../DoubleLogo'
import Modal from '../Modal'
import QuestionHelper from '../Question'
import { AutoRow, RowBetween, RowFixed } from '../Row'
import TokenLogo from '../TokenLogo'
import { useTokenComparator } from './sorting'
import {
BaseWrapper,
FadedSpan,
GreySpan,
Input,
ItemList,
MenuItem,
PaddedColumn,
SpinnerWrapper,
TokenModalInfo
} from './styleds'
import { TokenSortButton } from './TokenSortButton'
const TokenModalInfo = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
padding: 1rem 1rem;
margin: 0.25rem 0.5rem;
justify-content: center;
user-select: none;
min-height: 200px;
`
const ItemList = styled.div`
flex-grow: 1;
height: 254px;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
`
const FadedSpan = styled(RowFixed)`
color: ${({ theme }) => theme.primary1};
font-size: 14px;
`
const GreySpan = styled.span`
color: ${({ theme }) => theme.text3};
font-weight: 400;
`
const SpinnerWrapper = styled(Spinner)`
margin: 0 0.25rem 0 0.25rem;
color: ${({ theme }) => theme.text4};
opacity: 0.6;
`
const Input = styled.input`
position: relative;
display: flex;
padding: 16px;
align-items: center;
width: 100%;
white-space: nowrap;
background: none;
border: none;
outline: none;
border-radius: 20px;
color: ${({ theme }) => theme.text1};
border-style: solid;
border: 1px solid ${({ theme }) => theme.bg3};
-webkit-appearance: none;
font-size: 18px;
::placeholder {
color: ${({ theme }) => theme.text3};
}
`
const FilterWrapper = styled(RowFixed)`
padding: 8px;
background-color: ${({ selected, theme }) => selected && theme.bg2};
color: ${({ selected, theme }) => (selected ? theme.text1 : theme.text2)};
border-radius: 8px;
user-select: none;
& > * {
user-select: none;
}
:hover {
cursor: pointer;
}
`
const PaddedColumn = styled(AutoColumn)`
padding: 20px;
padding-bottom: 12px;
`
const PaddedItem = styled(RowBetween)`
padding: 4px 20px;
height: 56px;
`
const MenuItem = styled(PaddedItem)`
cursor: ${({ disabled }) => !disabled && 'pointer'};
pointer-events: ${({ disabled }) => disabled && 'none'};
:hover {
background-color: ${({ theme, disabled }) => !disabled && theme.bg2};
}
opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)};
`
const BaseWrapper = styled(AutoRow)<{ disable?: boolean }>`
border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
padding: 0 6px;
border-radius: 10px;
width: 120px;
:hover {
cursor: ${({ disable }) => !disable && 'pointer'};
background-color: ${({ theme, disable }) => !disable && theme.bg2};
}
background-color: ${({ theme, disable }) => disable && theme.bg3};
opacity: ${({ disable }) => disable && '0.4'};
`
// filters on results
const FILTERS = {
VOLUME: 'VOLUME',
LIQUIDITY: 'LIQUIDITY',
BALANCES: 'BALANCES'
}
interface SearchModalProps extends RouteComponentProps<{}> {
interface SearchModalProps extends RouteComponentProps {
isOpen?: boolean
onDismiss?: () => void
filterType?: 'tokens'
hiddenToken?: string
showSendWithSwap?: boolean
onTokenSelect?: (address: string) => void
urlAddedTokens?: Token[]
otherSelectedTokenAddress?: string
otherSelectedText?: string
showCommonBases?: boolean
}
function isDefaultToken(tokenAddress: string, chainId?: number): boolean {
const address = isAddress(tokenAddress)
return Boolean(chainId && address && ALL_TOKENS[chainId as ChainId]?.[tokenAddress])
}
function SearchModal({
history,
isOpen,
onDismiss,
onTokenSelect,
urlAddedTokens,
filterType,
hiddenToken,
showSendWithSwap,
@@ -183,17 +79,10 @@ function SearchModal({
const [searchQuery, setSearchQuery] = useState('')
const [invertSearchOrder, setInvertSearchOrder] = useState(false)
const userAddedTokens = useUserAddedTokens()
const fetchTokenByAddress = useFetchTokenByAddress()
const addToken = useAddUserToken()
const removeTokenByAddress = useRemoveUserAddedToken()
// if the current input is an address, and we don't have the token in context, try to fetch it
const token = useToken(searchQuery)
const [temporaryToken, setTemporaryToken] = useState<Token | null>()
// filters for ordering
const [activeFilter, setActiveFilter] = useState(FILTERS.BALANCES)
const searchQueryToken = useTokenByAddressAndAutomaticallyAdd(searchQuery)
// toggle specific token import view
const [showTokenImport, setShowTokenImport] = useState(false)
@@ -201,22 +90,6 @@ function SearchModal({
// used to help scanning on results, put token found from input on left
const [identifiedToken, setIdentifiedToken] = useState<Token>()
useEffect(() => {
const address = isAddress(searchQuery)
if (address && !token) {
let stale = false
fetchTokenByAddress(address).then(token => {
if (!stale) {
setTemporaryToken(token)
}
})
return () => {
stale = true
setTemporaryToken(null)
}
}
}, [searchQuery, token, fetchTokenByAddress])
// reset view on close
useEffect(() => {
if (!isOpen) {
@@ -224,45 +97,24 @@ function SearchModal({
}
}, [isOpen])
const tokenList = useMemo(() => {
return Object.keys(allTokens)
.sort((tokenAddressA, tokenAddressB): number => {
// -1 = a is first
// 1 = b is first
const tokenComparator = useTokenComparator(invertSearchOrder)
// sort ETH first
const a = allTokens[tokenAddressA]
const b = allTokens[tokenAddressB]
if (a.equals(WETH[chainId])) return -1
if (b.equals(WETH[chainId])) return 1
// sort by balances
const balanceA = allBalances[account]?.[tokenAddressA]
const balanceB = allBalances[account]?.[tokenAddressB]
if (balanceA?.greaterThan('0') && !balanceB?.greaterThan('0')) return !invertSearchOrder ? -1 : 1
if (!balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) return !invertSearchOrder ? 1 : -1
if (balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) {
return balanceA.greaterThan(balanceB) ? (!invertSearchOrder ? -1 : 1) : !invertSearchOrder ? 1 : -1
}
// sort by symbol
return a.symbol.toLowerCase() < b.symbol.toLowerCase() ? -1 : 1
})
.map(tokenAddress => {
const token = allTokens[tokenAddress]
const sortedTokenList = useMemo(() => {
return Object.values(allTokens)
.sort(tokenComparator)
.map(token => {
return {
name: token.name,
symbol: token.symbol,
address: isAddress(tokenAddress) as string,
balance: allBalances?.[account]?.[tokenAddress]
address: token.address,
balance: allBalances[account]?.[token.address]
}
})
}, [allTokens, chainId, allBalances, account, invertSearchOrder])
}, [allTokens, tokenComparator, allBalances, account])
const filteredTokenList = useMemo(() => {
return tokenList.filter(tokenEntry => {
const urlAdded = urlAddedTokens?.some(token => token.address === tokenEntry.address)
const customAdded = userAddedTokens?.some(token => token.address === tokenEntry.address) && !urlAdded
return sortedTokenList.filter(tokenEntry => {
const customAdded = !isDefaultToken(tokenEntry.address, chainId)
// if token import page dont show preset list, else show all
const include = !showTokenImport || (showTokenImport && customAdded && searchQuery !== '')
@@ -285,7 +137,7 @@ function SearchModal({
})
return regexMatches.some(m => m)
})
}, [tokenList, urlAddedTokens, userAddedTokens, showTokenImport, searchQuery])
}, [sortedTokenList, chainId, showTokenImport, searchQuery])
function _onTokenSelect(address) {
setSearchQuery('')
@@ -313,18 +165,14 @@ function SearchModal({
// try to find an exact match by address
if (searchQueryIsAddress) {
const identifiedTokenByAddress = Object.values(allTokens).filter(token => {
if (searchQueryIsAddress && token.address === isAddress(searchQuery)) {
return true
}
return false
return searchQueryIsAddress && token.address === isAddress(searchQuery)
})
if (identifiedTokenByAddress.length > 0) setIdentifiedToken(identifiedTokenByAddress[0])
}
// try to find an exact match by symbol
else {
const identifiedTokenBySymbol = Object.values(allTokens).filter(token => {
if (token.symbol.slice(0, searchQuery.length).toLowerCase() === searchQuery.toLowerCase()) return true
return false
return token.symbol.slice(0, searchQuery.length).toLowerCase() === searchQuery.toLowerCase()
})
if (identifiedTokenBySymbol.length > 0) setIdentifiedToken(identifiedTokenBySymbol[0])
}
@@ -423,25 +271,22 @@ function SearchModal({
function renderTokenList() {
if (filteredTokenList.length === 0) {
if (isAddress(searchQuery)) {
if (temporaryToken === undefined) {
return <TokenModalInfo>Searching for Token...</TokenModalInfo>
} else if (temporaryToken === null) {
return <TokenModalInfo>Address is not a valid ERC-20 token.</TokenModalInfo>
if (!searchQueryToken) {
return <TokenModalInfo>Searching...</TokenModalInfo>
} else {
// a user found a token by search that isn't yet added to localstorage
return (
<MenuItem
key={temporaryToken.address}
className={`temporary-token-${temporaryToken}`}
key={searchQueryToken.address}
className={`temporary-token-${searchQueryToken.address}`}
onClick={() => {
addToken(temporaryToken)
_onTokenSelect(temporaryToken.address)
_onTokenSelect(searchQueryToken.address)
}}
>
<RowFixed>
<TokenLogo address={temporaryToken.address} size={'24px'} style={{ marginRight: '14px' }} />
<TokenLogo address={searchQueryToken.address} size={'24px'} style={{ marginRight: '14px' }} />
<Column>
<Text fontWeight={500}>{temporaryToken.symbol}</Text>
<Text fontWeight={500}>{searchQueryToken.symbol}</Text>
<FadedSpan>(Found by search)</FadedSpan>
</Column>
</RowFixed>
@@ -453,8 +298,7 @@ function SearchModal({
}
} else {
return filteredTokenList.map(({ address, symbol, balance }) => {
const urlAdded = urlAddedTokens?.some(token => token.address === address)
const customAdded = userAddedTokens?.some(token => token.address === address) && !urlAdded
const customAdded = !isDefaultToken(address, chainId)
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
@@ -475,10 +319,7 @@ function SearchModal({
{otherSelectedTokenAddress === address && <GreySpan> ({otherSelectedText})</GreySpan>}
</Text>
<FadedSpan>
<TYPE.main fontWeight={500}>
{urlAdded && 'Added by URL'}
{customAdded && 'Added by user'}
</TYPE.main>
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
{customAdded && (
<div
onClick={event => {
@@ -522,27 +363,6 @@ function SearchModal({
}
}
const Filter = ({ title, filter, filterType }: { title: string; filter: string; filterType: string }) => {
return (
<FilterWrapper
onClick={() => {
setActiveFilter(filter)
setInvertSearchOrder(invertSearchOrder => !invertSearchOrder)
}}
selected={filter === activeFilter}
>
<Text fontSize={14} fontWeight={500}>
{title}
</Text>
{filter === activeFilter && filterType === 'tokens' && (
<Text fontSize={14} fontWeight={500}>
{!invertSearchOrder ? '↓' : '↑'}
</Text>
)}
</FilterWrapper>
)
}
return (
<Modal
isOpen={isOpen}
@@ -578,7 +398,7 @@ function SearchModal({
<PaddedColumn gap="20px">
<RowBetween>
<Text fontWeight={500} fontSize={16}>
{filterType === 'tokens' ? 'Select A Token' : 'Select A Pool'}
{filterType === 'tokens' ? 'Select a token' : 'Select a pool'}
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
@@ -621,11 +441,13 @@ function SearchModal({
<Text fontSize={14} fontWeight={500}>
{filterType === 'tokens' ? 'Token Name' : 'Pool Name'}
</Text>
<Filter
title={filterType === 'tokens' ? 'Your Balances' : ' '}
filter={FILTERS.BALANCES}
filterType={filterType}
/>
{filterType === 'tokens' && (
<TokenSortButton
invertSearchOrder={invertSearchOrder}
toggleSortOrder={() => setInvertSearchOrder(iso => !iso)}
title={filterType === 'tokens' ? 'Your Balances' : ' '}
/>
)}
</RowBetween>
</PaddedColumn>
)}

View File

@@ -0,0 +1,41 @@
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
import { useMemo } from 'react'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
function getTokenComparator(
weth: Token | undefined,
balances: { [tokenAddress: string]: TokenAmount },
invertSearchOrder: boolean
): (tokenA: Token, tokenB: Token) => number {
return function sortTokens(tokenA: Token, tokenB: Token): number {
// -1 = a is first
// 1 = b is first
// sort ETH first
if (weth) {
if (tokenA.equals(weth)) return -1
if (tokenB.equals(weth)) return 1
}
// sort by balances
const balanceA = balances[tokenA.address]
const balanceB = balances[tokenB.address]
if (balanceA?.greaterThan('0') && !balanceB?.greaterThan('0')) return !invertSearchOrder ? -1 : 1
if (!balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) return !invertSearchOrder ? 1 : -1
if (balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) {
return balanceA.greaterThan(balanceB) ? (!invertSearchOrder ? -1 : 1) : !invertSearchOrder ? 1 : -1
}
// sort by symbol
return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1
}
}
export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number {
const { account, chainId } = useActiveWeb3React()
const weth = WETH[chainId]
const balances = useAllTokenBalancesTreatingWETHasETH()
return useMemo(() => getTokenComparator(weth, balances[account] ?? {}, inverted), [account, balances, inverted, weth])
}

View File

@@ -0,0 +1,108 @@
import styled from 'styled-components'
import { Spinner } from '../../theme'
import { AutoColumn } from '../Column'
import { AutoRow, RowBetween, RowFixed } from '../Row'
export const TokenModalInfo = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
padding: 1rem 1rem;
margin: 0.25rem 0.5rem;
justify-content: center;
user-select: none;
min-height: 200px;
`
export const ItemList = styled.div`
flex-grow: 1;
height: 254px;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
`
export const FadedSpan = styled(RowFixed)`
color: ${({ theme }) => theme.primary1};
font-size: 14px;
`
export const GreySpan = styled.span`
color: ${({ theme }) => theme.text3};
font-weight: 400;
`
export const SpinnerWrapper = styled(Spinner)`
margin: 0 0.25rem 0 0.25rem;
color: ${({ theme }) => theme.text4};
opacity: 0.6;
`
export const Input = styled.input`
position: relative;
display: flex;
padding: 16px;
align-items: center;
width: 100%;
white-space: nowrap;
background: none;
border: none;
outline: none;
border-radius: 20px;
color: ${({ theme }) => theme.text1};
border-style: solid;
border: 1px solid ${({ theme }) => theme.bg3};
-webkit-appearance: none;
font-size: 18px;
::placeholder {
color: ${({ theme }) => theme.text3};
}
`
export const FilterWrapper = styled(RowFixed)`
padding: 8px;
background-color: ${({ selected, theme }) => selected && theme.bg2};
color: ${({ selected, theme }) => (selected ? theme.text1 : theme.text2)};
border-radius: 8px;
user-select: none;
& > * {
user-select: none;
}
:hover {
cursor: pointer;
}
`
export const PaddedColumn = styled(AutoColumn)`
padding: 20px;
padding-bottom: 12px;
`
const PaddedItem = styled(RowBetween)`
padding: 4px 20px;
height: 56px;
`
export const MenuItem = styled(PaddedItem)`
cursor: ${({ disabled }) => !disabled && 'pointer'};
pointer-events: ${({ disabled }) => disabled && 'none'};
:hover {
background-color: ${({ theme, disabled }) => !disabled && theme.bg2};
}
opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)};
`
export const BaseWrapper = styled(AutoRow)<{ disable?: boolean }>`
border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
padding: 0 6px;
border-radius: 10px;
width: 120px;
:hover {
cursor: ${({ disable }) => !disable && 'pointer'};
background-color: ${({ theme, disable }) => !disable && theme.bg2};
}
background-color: ${({ theme, disable }) => disable && theme.bg3};
opacity: ${({ disable }) => disable && '0.4'};
`

View File

@@ -80,7 +80,7 @@ export default function CreatePool({ history, location }: RouteComponentProps) {
{token0?.symbol}{' '}
</Text>
<TYPE.darkGray fontWeight={500} fontSize={16} marginLeft={'8px'}>
{token0?.address === 'ETH' && '(default)'}
{token0?.address === WETH[chainId]?.address && '(default)'}
</TYPE.darkGray>
</Row>
</ButtonDropwdownLight>