Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e180153c3a | ||
|
|
e35f9e16a1 | ||
|
|
8e955e9257 | ||
|
|
9ca44652b3 | ||
|
|
3d89d72426 | ||
|
|
c2ffab3273 | ||
|
|
dbb62b613c | ||
|
|
6f2d6e31c9 | ||
|
|
944939a2e9 | ||
|
|
04164a550d | ||
|
|
16a5e15070 | ||
|
|
ee97d8d902 | ||
|
|
3d4b077b89 | ||
|
|
0f4a89d938 | ||
|
|
0d0ec12dbf | ||
|
|
afe30a2c02 | ||
|
|
2f9289a2c5 | ||
|
|
7a9d2e80d0 | ||
|
|
d4b8735c04 | ||
|
|
d31687d0bf | ||
|
|
470535dd33 | ||
|
|
27f53f1e99 | ||
|
|
e7d498c95e | ||
|
|
02b617d297 | ||
|
|
96d04e1a7d | ||
|
|
fe9d805d7c | ||
|
|
b6c136839e | ||
|
|
8c947a0e0d | ||
|
|
49c5cbbf3b | ||
|
|
efaefe2e44 | ||
|
|
ed95f1b966 | ||
|
|
7ecbc552aa | ||
|
|
dadc997398 | ||
|
|
b90d6b5ab0 | ||
|
|
db5c6f82fd | ||
|
|
f161f9617b | ||
|
|
9d3249e6bd | ||
|
|
f1c65afa98 | ||
|
|
80c1f0cdf9 | ||
|
|
ea0fe83d00 | ||
|
|
994836fba7 | ||
|
|
41aa1dcb0b | ||
|
|
3965d3fdd9 | ||
|
|
ff6fd8a6e9 | ||
|
|
0a6906b23e | ||
|
|
c38b5c0ce3 | ||
|
|
86f3b5a036 | ||
|
|
382a44f040 | ||
|
|
2d9604cd14 | ||
|
|
7930709bc3 | ||
|
|
6fe2c92cee | ||
|
|
884dee2db3 | ||
|
|
1f00c2a9c4 | ||
|
|
84070835df | ||
|
|
fb389137e7 | ||
|
|
c14b6a78ae | ||
|
|
a6c1c49f98 | ||
|
|
7848ad86bd | ||
|
|
882c15dada | ||
|
|
704ad222d9 | ||
|
|
cfee80ce3c | ||
|
|
eb95cedd72 | ||
|
|
99a7fb3383 | ||
|
|
7f4dbf9346 | ||
|
|
09b00c9974 | ||
|
|
b74fb8174d | ||
|
|
a7ec5a64b7 | ||
|
|
c619dcf65d | ||
|
|
1221d88e13 | ||
|
|
48d2ead71d | ||
|
|
ed7099bfd6 | ||
|
|
2604cdfdae | ||
|
|
94dc389812 | ||
|
|
4a8c621f46 | ||
|
|
477af8af4e | ||
|
|
a9a7d524aa | ||
|
|
1cdaff8ddf | ||
|
|
eeea3d2dcc | ||
|
|
f46b6a0697 | ||
|
|
622581ee0a | ||
|
|
eb725f51ce | ||
|
|
4d4462368b | ||
|
|
6fe5d4363d | ||
|
|
b46fa27084 | ||
|
|
ade2440613 | ||
|
|
4dc4620b60 | ||
|
|
202c2662f1 | ||
|
|
d2afd71c81 | ||
|
|
bad1ce2618 | ||
|
|
f194845b2b | ||
|
|
98d4e108e6 | ||
|
|
ab43ed1900 | ||
|
|
b147e047a5 | ||
|
|
bbb616f56c | ||
|
|
35a429ea65 | ||
|
|
bd16543c10 | ||
|
|
cbdeae276e | ||
|
|
e733113963 | ||
|
|
272b030b89 | ||
|
|
472a553d13 | ||
|
|
3a1bff146c |
1
.env
1
.env
@@ -5,3 +5,4 @@ REACT_APP_AWS_API_ACCESS_KEY="AKIAYJJWW6AQ47ODATHN"
|
||||
REACT_APP_AWS_API_ACCESS_SECRET="V9PoU0FhBP3cX760rPs9jMG/MIuDNLX6hYvVcaYO"
|
||||
REACT_APP_AWS_X_API_KEY="z9dReS5UtHu7iTrUsTuWRozLthi3AxOZlvobrIdr14"
|
||||
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
|
||||
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
|
||||
|
||||
@@ -81,6 +81,18 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"name": "@ethersproject/providers",
|
||||
"message": "Please only use Providers instantiated in constants/providers to improve traceability.",
|
||||
"allowTypeImports": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
50
cypress/e2e/wallet-dropdown.test.ts
Normal file
50
cypress/e2e/wallet-dropdown.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
describe('Wallet Dropdown', () => {
|
||||
before(() => {
|
||||
cy.visit('/', { featureFlags: [FeatureFlag.navBar, FeatureFlag.tokenSafety] })
|
||||
})
|
||||
|
||||
it('should change the theme', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-select-theme')).click()
|
||||
cy.get(getTestSelector('wallet-select-theme')).contains('Light theme').should('exist')
|
||||
})
|
||||
|
||||
it('should select a language', () => {
|
||||
cy.get(getTestSelector('wallet-select-language')).click()
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Taal')
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
|
||||
cy.get(getTestSelector('wallet-back')).click()
|
||||
})
|
||||
|
||||
it('should be able to view transactions', () => {
|
||||
cy.get(getTestSelector('wallet-transactions')).click()
|
||||
cy.get(getTestSelector('wallet-empty-transaction-text')).should('exist')
|
||||
cy.get(getTestSelector('wallet-back')).click()
|
||||
})
|
||||
|
||||
it('should change the theme when not connected', () => {
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-select-theme')).click()
|
||||
cy.get(getTestSelector('wallet-select-theme')).contains('Dark theme').should('exist')
|
||||
})
|
||||
|
||||
it('should select a language when not connected', () => {
|
||||
cy.get(getTestSelector('wallet-select-language')).click()
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Taal')
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
|
||||
cy.get(getTestSelector('wallet-back')).click()
|
||||
})
|
||||
|
||||
it('should open the wallet connect modal from the drop down when not connected', () => {
|
||||
cy.get(getTestSelector('wallet-connect-wallet')).click()
|
||||
cy.get(getTestSelector('wallet-modal')).should('exist')
|
||||
cy.get(getTestSelector('wallet-modal-close')).click()
|
||||
})
|
||||
})
|
||||
@@ -24,6 +24,7 @@ describe('Wallet', () => {
|
||||
})
|
||||
|
||||
it('shows connect buttons after disconnect', () => {
|
||||
cy.get('[data-testid=web3-status-connected]').contains(TEST_ADDRESS_NEVER_USE_SHORTENED).click()
|
||||
cy.contains('Disconnect').click()
|
||||
cy.get('[data-testid=option-grid]').should('exist')
|
||||
})
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import { injected } from './ethereum'
|
||||
import assert = require('assert')
|
||||
|
||||
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
@@ -17,6 +19,7 @@ declare global {
|
||||
}
|
||||
interface VisitOptions {
|
||||
serviceWorker?: true
|
||||
featureFlags?: Array<FeatureFlag>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +39,18 @@ Cypress.Commands.overwrite(
|
||||
options?.onBeforeLoad?.(win)
|
||||
win.localStorage.clear()
|
||||
win.localStorage.setItem('redux_localstorage_simple_user', '{"selectedWallet":"INJECTED"}')
|
||||
|
||||
if (options?.featureFlags) {
|
||||
const featureFlags = options.featureFlags.reduce(
|
||||
(flags, flag) => ({
|
||||
...flags,
|
||||
[flag]: 'enabled',
|
||||
}),
|
||||
{}
|
||||
)
|
||||
win.localStorage.setItem('featureFlags', JSON.stringify(featureFlags))
|
||||
}
|
||||
|
||||
win.ethereum = injected
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { JsonRpcProvider } from '@ethersproject/providers'
|
||||
import { Wallet } from '@ethersproject/wallet'
|
||||
|
||||
|
||||
1
cypress/utils/index.ts
Normal file
1
cypress/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const getTestSelector = (selectorId: string) => `[data-testid=${selectorId}]`
|
||||
@@ -137,7 +137,7 @@
|
||||
"@uniswap/redux-multicall": "^1.1.5",
|
||||
"@uniswap/router-sdk": "^1.3.0",
|
||||
"@uniswap/sdk-core": "^3.0.1",
|
||||
"@uniswap/smart-order-router": "^2.9.2",
|
||||
"@uniswap/smart-order-router": "^2.10.0",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.30",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
@@ -145,7 +145,7 @@
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"@uniswap/v3-sdk": "^3.9.0",
|
||||
"@uniswap/widgets": "^2.3.1",
|
||||
"@uniswap/widgets": "^2.8.1",
|
||||
"@vanilla-extract/css": "^1.7.2",
|
||||
"@vanilla-extract/css-utils": "^0.1.2",
|
||||
"@vanilla-extract/dynamic": "^2.0.2",
|
||||
|
||||
@@ -15,7 +15,8 @@ export enum EventName {
|
||||
SWAP_MAX_TOKEN_AMOUNT_SELECTED = 'Swap Max Token Amount Selected',
|
||||
SWAP_PRICE_UPDATE_ACKNOWLEDGED = 'Swap Price Update Acknowledged',
|
||||
SWAP_QUOTE_RECEIVED = 'Swap Quote Received',
|
||||
SWAP_SUBMITTED = 'Swap Submitted',
|
||||
SWAP_SIGNED = 'Swap Signed',
|
||||
SWAP_SUBMITTED_BUTTON_CLICKED = 'Swap Submit Button Clicked',
|
||||
SWAP_TOKENS_REVERSED = 'Swap Tokens Reversed',
|
||||
SWAP_TRANSACTION_COMPLETED = 'Swap Transaction Completed',
|
||||
TOKEN_IMPORTED = 'Token Imported',
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Identify, identify, init, track } from '@amplitude/analytics-browser'
|
||||
import { isProductionEnv } from 'utils/env'
|
||||
|
||||
const API_KEY = isProductionEnv() ? process.env.REACT_APP_AMPLITUDE_KEY : process.env.REACT_APP_AMPLITUDE_TEST_KEY
|
||||
|
||||
/**
|
||||
* Initializes Amplitude with API key for project.
|
||||
*
|
||||
@@ -8,14 +10,11 @@ import { isProductionEnv } from 'utils/env'
|
||||
* member of the organization on Amplitude to view details.
|
||||
*/
|
||||
export function initializeAnalytics() {
|
||||
const API_KEY = isProductionEnv() ? process.env.REACT_APP_AMPLITUDE_KEY : process.env.REACT_APP_AMPLITUDE_TEST_KEY
|
||||
|
||||
if (typeof API_KEY === 'undefined') {
|
||||
const keyName = isProductionEnv() ? 'REACT_APP_AMPLITUDE_KEY' : 'REACT_APP_AMPLITUDE_TEST_KEY'
|
||||
console.error(`${keyName} is undefined, Amplitude analytics will not run.`)
|
||||
return
|
||||
}
|
||||
|
||||
init(
|
||||
API_KEY,
|
||||
/* userId= */ undefined, // User ID should be undefined to let Amplitude default to Device ID
|
||||
@@ -23,7 +22,8 @@ export function initializeAnalytics() {
|
||||
{
|
||||
// Disable tracking of private user information by Amplitude
|
||||
trackingOptions: {
|
||||
ipAddress: false,
|
||||
// IP is being dropped before ingestion on Amplitude side, only being used to determine country.
|
||||
ipAddress: isProductionEnv() ? false : true,
|
||||
carrier: false,
|
||||
city: false,
|
||||
region: false,
|
||||
@@ -33,23 +33,16 @@ export function initializeAnalytics() {
|
||||
)
|
||||
}
|
||||
|
||||
/** Sends an approved (finalized) event to Amplitude production project. */
|
||||
/** Sends an event to Amplitude. */
|
||||
export function sendAnalyticsEvent(eventName: string, eventProperties?: Record<string, unknown>) {
|
||||
if (!isProductionEnv()) {
|
||||
console.log(`[amplitude(${eventName})]: ${JSON.stringify(eventProperties)}`)
|
||||
if (!API_KEY) {
|
||||
console.log(`[analytics(${eventName})]: ${JSON.stringify(eventProperties)}`)
|
||||
return
|
||||
}
|
||||
|
||||
track(eventName, eventProperties)
|
||||
}
|
||||
|
||||
/** Sends a draft event to Amplitude test project. */
|
||||
export function sendTestAnalyticsEvent(eventName: string, eventProperties?: Record<string, unknown>) {
|
||||
if (isProductionEnv()) return
|
||||
|
||||
track(eventName, eventProperties)
|
||||
}
|
||||
|
||||
type Value = string | number | boolean | string[] | number[]
|
||||
|
||||
/**
|
||||
BIN
src/assets/images/nft-marketplaces.png
Normal file
BIN
src/assets/images/nft-marketplaces.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
186
src/assets/svg/socks.svg
Normal file
186
src/assets/svg/socks.svg
Normal file
@@ -0,0 +1,186 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20">
|
||||
<g filter="url(#a)">
|
||||
<path fill="url(#b)" d="M17.654 4.868c0-.69-.56-1.25-1.25-1.25h-2.932c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.741 1.873c-.898.999-1.115 2.687-.058 3.748 1.013 1.017 2.918.988 3.87 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.158v-7.55Z"/>
|
||||
<path fill="url(#c)" d="M17.654 4.868c0-.69-.56-1.25-1.25-1.25h-2.932c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.741 1.873c-.898.999-1.115 2.687-.058 3.748 1.013 1.017 2.918.988 3.87 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.158v-7.55Z"/>
|
||||
<path fill="url(#d)" d="M17.654 4.868c0-.69-.56-1.25-1.25-1.25h-2.932c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.741 1.873c-.898.999-1.115 2.687-.058 3.748 1.013 1.017 2.918.988 3.87 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.158v-7.55Z"/>
|
||||
<path fill="url(#e)" d="M17.654 4.868c0-.69-.56-1.25-1.25-1.25h-2.932c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.741 1.873c-.898.999-1.115 2.687-.058 3.748 1.013 1.017 2.918.988 3.87 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.158v-7.55Z"/>
|
||||
<path fill="url(#f)" d="M17.654 4.868c0-.69-.56-1.25-1.25-1.25h-2.932c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.741 1.873c-.898.999-1.115 2.687-.058 3.748 1.013 1.017 2.918.988 3.87 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.158v-7.55Z"/>
|
||||
</g>
|
||||
<g filter="url(#g)">
|
||||
<path fill="url(#h)" d="M10.954 2.602c0-.69-.56-1.25-1.25-1.25H6.773c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.742 1.873c-.898 1-1.115 2.687-.058 3.748 1.014 1.018 2.919.988 3.871 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.157V2.602Z"/>
|
||||
<path fill="url(#i)" d="M10.954 2.602c0-.69-.56-1.25-1.25-1.25H6.773c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.742 1.873c-.898 1-1.115 2.687-.058 3.748 1.014 1.018 2.919.988 3.871 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.157V2.602Z"/>
|
||||
<path fill="url(#j)" d="M10.954 2.602c0-.69-.56-1.25-1.25-1.25H6.773c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.742 1.873c-.898 1-1.115 2.687-.058 3.748 1.014 1.018 2.919.988 3.871 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.157V2.602Z"/>
|
||||
<path fill="url(#k)" d="M10.954 2.602c0-.69-.56-1.25-1.25-1.25H6.773c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.742 1.873c-.898 1-1.115 2.687-.058 3.748 1.014 1.018 2.919.988 3.871 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.157V2.602Z"/>
|
||||
<path fill="url(#l)" d="M10.954 2.602c0-.69-.56-1.25-1.25-1.25H6.773c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.742 1.873c-.898 1-1.115 2.687-.058 3.748 1.014 1.018 2.919.988 3.871 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.157V2.602Z"/>
|
||||
</g>
|
||||
<path fill="url(#m)" d="M17.654 5.776h-5.431v.673h5.431v-.673Z"/>
|
||||
<path fill="url(#n)" d="M17.654 5.776h-5.431v.673h5.431v-.673Z"/>
|
||||
<path fill="url(#o)" d="M10.955 3.51H5.523v.674h5.432V3.51Z"/>
|
||||
<path fill="url(#p)" d="M10.955 3.51H5.523v.674h5.432V3.51Z"/>
|
||||
<path fill="url(#q)" d="M17.606 4.523c.031.11.048.225.048.345v.328h-5.431v-.328c0-.12.016-.236.048-.345h5.335Z"/>
|
||||
<path fill="url(#r)" d="M17.606 4.523c.031.11.048.225.048.345v.328h-5.431v-.328c0-.12.016-.236.048-.345h5.335Z"/>
|
||||
<path fill="url(#s)" d="M10.907 2.257c.031.11.048.225.048.345v.329H5.523v-.329c0-.12.017-.235.049-.345h5.335Z"/>
|
||||
<path fill="url(#t)" d="M10.907 2.257c.031.11.048.225.048.345v.329H5.523v-.329c0-.12.017-.235.049-.345h5.335Z"/>
|
||||
<g filter="url(#u)">
|
||||
<path fill="url(#v)" d="M17.654 12.236h-3.116a.173.173 0 0 0-.176.17c0 1.255.904 2.3 2.096 2.518l.332-.349a3.125 3.125 0 0 0 .864-2.157v-.182Z"/>
|
||||
</g>
|
||||
<g filter="url(#w)">
|
||||
<path fill="url(#x)" d="M10.955 9.97H7.839a.173.173 0 0 0-.176.17c0 1.255.903 2.3 2.096 2.518l.332-.349a3.125 3.125 0 0 0 .864-2.156V9.97Z"/>
|
||||
</g>
|
||||
<g filter="url(#y)">
|
||||
<path fill="url(#z)" d="M13.243 18.21v-.05a3.424 3.424 0 0 0-3.856-3.397c-.511.986-.496 2.268.366 3.133.892.895 2.474.98 3.49.314Z"/>
|
||||
</g>
|
||||
<g filter="url(#A)">
|
||||
<path fill="url(#B)" d="M6.544 15.944v-.05a3.424 3.424 0 0 0-3.857-3.396c-.51.985-.495 2.267.366 3.132.892.896 2.475.98 3.49.314Z"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="b" x1="17.248" x2="13.193" y1="4.4" y2="16.827" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FDF2FF"/>
|
||||
<stop offset="1" stop-color="#FFECFB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="c" x1="17.654" x2="15.615" y1="11.854" y2="11.854" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFCCF1"/>
|
||||
<stop offset="1" stop-color="#FFC5EF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="f" x1="14.99" x2="13.818" y1="16.932" y2="15.721" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".209" stop-color="#EDB0DF"/>
|
||||
<stop offset="1" stop-color="#ECAAED" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="h" x1="10.549" x2="6.493" y1="2.134" y2="14.562" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FDF2FF"/>
|
||||
<stop offset="1" stop-color="#FFECFB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="i" x1="10.954" x2="8.915" y1="9.588" y2="9.588" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFCCF1"/>
|
||||
<stop offset="1" stop-color="#FFC5EF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="l" x1="8.29" x2="7.119" y1="14.666" y2="13.456" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".209" stop-color="#EDB0DF"/>
|
||||
<stop offset="1" stop-color="#ECAAED" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="m" x1="12.773" x2="16.915" y1="6.449" y2="6.449" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E95FDB"/>
|
||||
<stop offset="1" stop-color="#FF46CB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="n" x1="12.223" x2="12.793" y1="6.449" y2="6.449" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#9F4977"/>
|
||||
<stop offset="1" stop-color="#CA5284" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="o" x1="6.074" x2="10.216" y1="4.184" y2="4.184" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E95FDB"/>
|
||||
<stop offset="1" stop-color="#FF46CB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="p" x1="5.523" x2="6.094" y1="4.184" y2="4.184" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#9F4977"/>
|
||||
<stop offset="1" stop-color="#CA5284" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="q" x1="12.773" x2="16.915" y1="5.196" y2="5.196" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E95FDB"/>
|
||||
<stop offset="1" stop-color="#FF46CB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="r" x1="12.223" x2="12.793" y1="5.196" y2="5.196" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#9F4977"/>
|
||||
<stop offset="1" stop-color="#CA5284" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="s" x1="6.074" x2="10.216" y1="2.931" y2="2.931" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E95FDB"/>
|
||||
<stop offset="1" stop-color="#FF46CB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="t" x1="5.523" x2="6.094" y1="2.931" y2="2.931" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#9F4977"/>
|
||||
<stop offset="1" stop-color="#CA5284" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="d" cx="0" cy="0" r="1" gradientTransform="matrix(2.8125 0 0 6.01563 12.02 8.026)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFD1F5"/>
|
||||
<stop offset="1" stop-color="#FECAFF" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="matrix(1.54348 -1.53472 2.30642 2.31958 11.806 16.12)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFC0FC"/>
|
||||
<stop offset="1" stop-color="#FFBCFC" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="j" cx="0" cy="0" r="1" gradientTransform="matrix(2.8125 0 0 6.01563 5.322 5.76)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFD1F5"/>
|
||||
<stop offset="1" stop-color="#FECAFF" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="k" cx="0" cy="0" r="1" gradientTransform="matrix(1.54347 -1.53473 2.30643 2.31957 5.107 13.854)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFC0FC"/>
|
||||
<stop offset="1" stop-color="#FFBCFC" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="v" cx="0" cy="0" r="1" gradientTransform="matrix(-.42911 2.50572 -2.20218 -.37713 16.437 12.236)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".147" stop-color="#FF52CF"/>
|
||||
<stop offset="1" stop-color="#FB3FFF"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="x" cx="0" cy="0" r="1" gradientTransform="matrix(-.42911 2.50572 -2.20218 -.37713 9.738 9.97)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".147" stop-color="#FF52CF"/>
|
||||
<stop offset="1" stop-color="#FB3FFF"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="z" cx="0" cy="0" r="1" gradientTransform="rotate(167.471 5.464 9.437) scale(4.54024 4.83245)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".268" stop-color="#FF4EE3"/>
|
||||
<stop offset=".92" stop-color="#D12396"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="B" cx="0" cy="0" r="1" gradientTransform="rotate(167.471 2.239 7.937) scale(4.54024 4.83245)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".268" stop-color="#FF4EE3"/>
|
||||
<stop offset=".92" stop-color="#D12396"/>
|
||||
</radialGradient>
|
||||
<filter id="a" width="8.808" height="15.23" x="9.045" y="3.418" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx=".2" dy="-.2"/>
|
||||
<feGaussianBlur stdDeviation=".3"/>
|
||||
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
|
||||
<feColorMatrix values="0 0 0 0 0.522043 0 0 0 0 0.119948 0 0 0 0 0.5875 0 0 0 1 0"/>
|
||||
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
|
||||
</filter>
|
||||
<filter id="g" width="8.808" height="15.23" x="2.346" y="1.152" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx=".2" dy="-.2"/>
|
||||
<feGaussianBlur stdDeviation=".3"/>
|
||||
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
|
||||
<feColorMatrix values="0 0 0 0 0.545098 0 0 0 0 0.219608 0 0 0 0 0.512549 0 0 0 1 0"/>
|
||||
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
|
||||
</filter>
|
||||
<filter id="u" width="3.541" height="2.688" x="14.112" y="12.236" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-.25"/>
|
||||
<feGaussianBlur stdDeviation=".25"/>
|
||||
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
|
||||
<feColorMatrix values="0 0 0 0 0.976471 0 0 0 0 0.145098 0 0 0 0 0.743686 0 0 0 1 0"/>
|
||||
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
|
||||
</filter>
|
||||
<filter id="w" width="3.541" height="2.688" x="7.413" y="9.97" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-.25"/>
|
||||
<feGaussianBlur stdDeviation=".25"/>
|
||||
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
|
||||
<feColorMatrix values="0 0 0 0 0.976471 0 0 0 0 0.145098 0 0 0 0 0.743686 0 0 0 1 0"/>
|
||||
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
|
||||
</filter>
|
||||
<filter id="y" width="4.448" height="4.112" x="9.045" y="14.536" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx=".25" dy="-.2"/>
|
||||
<feGaussianBlur stdDeviation=".25"/>
|
||||
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
|
||||
<feColorMatrix values="0 0 0 0 0.906118 0 0 0 0 0.329412 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
|
||||
</filter>
|
||||
<filter id="A" width="4.448" height="4.112" x="2.346" y="12.27" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx=".25" dy="-.2"/>
|
||||
<feGaussianBlur stdDeviation=".25"/>
|
||||
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
|
||||
<feColorMatrix values="0 0 0 0 0.906118 0 0 0 0 0.329412 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -145,7 +145,7 @@ const CloseIcon = styled.div`
|
||||
top: 14px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -1,49 +1,45 @@
|
||||
import { curveCardinalOpen, scaleLinear } from 'd3'
|
||||
import { curveCardinal, scaleLinear } from 'd3'
|
||||
import { SingleTokenData, TimePeriod, useTokenPricesFromFragment } from 'graphql/data/Token'
|
||||
import React from 'react'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
|
||||
import data from './data.json'
|
||||
import { DATA_EMPTY, getPriceBounds } from '../Tokens/TokenDetails/PriceChart'
|
||||
import LineChart from './LineChart'
|
||||
|
||||
type PricePoint = { value: number; timestamp: number }
|
||||
|
||||
function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
|
||||
const prices = pricePoints.map((x) => x.value)
|
||||
const min = Math.min(...prices)
|
||||
const max = Math.max(...prices)
|
||||
return [min, max]
|
||||
}
|
||||
|
||||
interface SparklineChartProps {
|
||||
width: number
|
||||
height: number
|
||||
tokenData: SingleTokenData
|
||||
pricePercentChange: number | undefined | null
|
||||
timePeriod: TimePeriod
|
||||
}
|
||||
|
||||
function SparklineChart({ width, height }: SparklineChartProps) {
|
||||
function SparklineChart({ width, height, tokenData, pricePercentChange, timePeriod }: SparklineChartProps) {
|
||||
const theme = useTheme()
|
||||
// for sparkline
|
||||
const pricePoints = useTokenPricesFromFragment(tokenData?.prices?.[0]) ?? []
|
||||
const hasData = pricePoints.length !== 0
|
||||
const startingPrice = hasData ? pricePoints[0] : DATA_EMPTY
|
||||
const endingPrice = hasData ? pricePoints[pricePoints.length - 1] : DATA_EMPTY
|
||||
const widthScale = scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, 124])
|
||||
const rdScale = scaleLinear().domain(getPriceBounds(pricePoints)).range([42, 0])
|
||||
|
||||
/* TODO: Implement API calls & cache to use here */
|
||||
const pricePoints = data.day
|
||||
const startingPrice = pricePoints[0]
|
||||
const endingPrice = pricePoints[pricePoints.length - 1]
|
||||
|
||||
const timeScale = scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, width])
|
||||
const rdScale = scaleLinear().domain(getPriceBounds(pricePoints)).range([height, 0])
|
||||
|
||||
const isPositive = endingPrice.value >= startingPrice.value
|
||||
/* Default curve doesn't look good for the ALL chart */
|
||||
const curveTension = timePeriod === TimePeriod.ALL ? 0.75 : 0.9
|
||||
|
||||
return (
|
||||
<LineChart
|
||||
data={pricePoints}
|
||||
getX={(p: PricePoint) => timeScale(p.timestamp)}
|
||||
getX={(p: PricePoint) => widthScale(p.timestamp)}
|
||||
getY={(p: PricePoint) => rdScale(p.value)}
|
||||
curve={curveCardinalOpen.tension(0.9)}
|
||||
marginTop={0}
|
||||
color={isPositive ? theme.accentSuccess : theme.accentFailure}
|
||||
curve={curveCardinal.tension(curveTension)}
|
||||
color={pricePercentChange && pricePercentChange < 0 ? theme.accentFailure : theme.accentSuccess}
|
||||
strokeWidth={1.5}
|
||||
width={width}
|
||||
height={height}
|
||||
></LineChart>
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Trans } from '@lingui/macro'
|
||||
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
|
||||
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
|
||||
import { ElementName, Event, EventName } from 'analytics/constants'
|
||||
import { TraceEvent } from 'analytics/TraceEvent'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Trans } from '@lingui/macro'
|
||||
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
|
||||
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
|
||||
import { ElementName, Event, EventName } from 'analytics/constants'
|
||||
import { TraceEvent } from 'analytics/TraceEvent'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
|
||||
@@ -24,11 +24,13 @@ const StyledNativeLogo = styled(StyledLogo)`
|
||||
|
||||
export default function CurrencyLogo({
|
||||
currency,
|
||||
symbol,
|
||||
size = '24px',
|
||||
style,
|
||||
...rest
|
||||
}: {
|
||||
currency?: Currency | null
|
||||
symbol?: string | null
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
}) {
|
||||
@@ -36,6 +38,7 @@ export default function CurrencyLogo({
|
||||
alt: `${currency?.symbol ?? 'token'} logo`,
|
||||
size,
|
||||
srcs: useCurrencyLogoURIs(currency),
|
||||
symbol: symbol ?? currency?.symbol,
|
||||
style,
|
||||
...rest,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
|
||||
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
|
||||
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import { TokensVariant, useTokensFlag } from 'featureFlags/flags/tokens'
|
||||
import { TokenSafetyVariant, useTokenSafetyFlag } from 'featureFlags/flags/tokenSafety'
|
||||
import { TokensNetworkFilterVariant, useTokensNetworkFilterFlag } from 'featureFlags/flags/tokensNetworkFilter'
|
||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
@@ -225,12 +226,6 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.tokens}
|
||||
label="Tokens"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={TokensNetworkFilterVariant}
|
||||
value={useTokensNetworkFilterFlag()}
|
||||
featureFlag={FeatureFlag.tokensNetworkFilter}
|
||||
label="Tokens Network Filter"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={TokenSafetyVariant}
|
||||
value={useTokenSafetyFlag()}
|
||||
@@ -238,9 +233,25 @@ export default function FeatureFlagModal() {
|
||||
label="Token Safety"
|
||||
/>
|
||||
</FeatureFlagGroup>
|
||||
<FeatureFlagGroup name="Phase 0 Follow-ups">
|
||||
<FeatureFlagOption
|
||||
variant={FavoriteTokensVariant}
|
||||
value={useFavoriteTokensFlag()}
|
||||
featureFlag={FeatureFlag.favoriteTokens}
|
||||
label="Favorite Tokens"
|
||||
/>
|
||||
</FeatureFlagGroup>
|
||||
<FeatureFlagGroup name="Phase 1">
|
||||
<FeatureFlagOption variant={NftVariant} value={useNftFlag()} featureFlag={FeatureFlag.nft} label="NFTs" />
|
||||
</FeatureFlagGroup>
|
||||
<FeatureFlagGroup name="Debug">
|
||||
<FeatureFlagOption
|
||||
variant={TraceJsonRpcVariant}
|
||||
value={useTraceJsonRpcFlag()}
|
||||
featureFlag={FeatureFlag.traceJsonRpc}
|
||||
label="Enables JSON-RPC tracing"
|
||||
/>
|
||||
</FeatureFlagGroup>
|
||||
<SaveButton onClick={() => window.location.reload()}>Reload</SaveButton>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
32
src/components/Icons/index.tsx
Normal file
32
src/components/Icons/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ChevronDown, ChevronUp } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
export const StyledChevronDown = styled(ChevronDown)<{ customColor?: string }>`
|
||||
color: ${({ theme, customColor }) => customColor ?? theme.textSecondary};
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.accentActionSoft};
|
||||
transition: ${({
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.fast} color ${timing.in}`};
|
||||
}
|
||||
`
|
||||
|
||||
export const StyledChevronUp = styled(ChevronUp)<{ customColor?: string }>`
|
||||
color: ${({ theme, customColor }) => customColor ?? theme.textSecondary};
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.accentActionSoft};
|
||||
transition: ${({
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.fast} color ${timing.in}`};
|
||||
}
|
||||
`
|
||||
@@ -1,11 +1,17 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ConnectionType } from 'connection'
|
||||
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
|
||||
import useENSAvatar from 'hooks/useENSAvatar'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
|
||||
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
||||
import sockImg from '../../assets/svg/socks.svg'
|
||||
import { useHasSocks } from '../../hooks/useSocksBalance'
|
||||
import Identicon from '../Identicon'
|
||||
|
||||
const IconWrapper = styled.div<{ size?: number }>`
|
||||
position: relative;
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -20,19 +26,55 @@ const IconWrapper = styled.div<{ size?: number }>`
|
||||
`};
|
||||
`
|
||||
|
||||
export default function StatusIcon({ connectionType, size }: { connectionType: ConnectionType; size?: number }) {
|
||||
let image
|
||||
switch (connectionType) {
|
||||
case ConnectionType.INJECTED:
|
||||
image = <Identicon />
|
||||
break
|
||||
case ConnectionType.WALLET_CONNECT:
|
||||
image = <img src={WalletConnectIcon} alt="WalletConnect" />
|
||||
break
|
||||
case ConnectionType.COINBASE_WALLET:
|
||||
image = <img src={CoinbaseWalletIcon} alt="Coinbase Wallet" />
|
||||
break
|
||||
const SockContainer = styled.div`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
`
|
||||
|
||||
const SockImg = styled.img`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
`
|
||||
|
||||
const Socks = () => {
|
||||
return (
|
||||
<SockContainer>
|
||||
<SockImg src={sockImg} />
|
||||
</SockContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const useIcon = (connectionType: ConnectionType) => {
|
||||
const { account } = useWeb3React()
|
||||
const { avatar } = useENSAvatar(account ?? undefined)
|
||||
const isNavbarEnabled = useNavBarFlag() === NavBarVariant.Enabled
|
||||
|
||||
if ((isNavbarEnabled && avatar) || connectionType === ConnectionType.INJECTED) {
|
||||
return <Identicon />
|
||||
} else if (connectionType === ConnectionType.WALLET_CONNECT) {
|
||||
return <img src={WalletConnectIcon} alt="WalletConnect" />
|
||||
} else if (connectionType === ConnectionType.COINBASE_WALLET) {
|
||||
return <img src={CoinbaseWalletIcon} alt="Coinbase Wallet" />
|
||||
}
|
||||
|
||||
return <IconWrapper size={size ?? 16}>{image}</IconWrapper>
|
||||
return undefined
|
||||
}
|
||||
|
||||
export default function StatusIcon({ connectionType, size }: { connectionType: ConnectionType; size?: number }) {
|
||||
const hasSocks = useHasSocks()
|
||||
const isNavbarEnabled = useNavBarFlag() === NavBarVariant.Enabled
|
||||
const icon = useIcon(connectionType)
|
||||
|
||||
return (
|
||||
<IconWrapper size={size ?? 16}>
|
||||
{isNavbarEnabled && hasSocks && <Socks />}
|
||||
{icon}
|
||||
</IconWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import jazzicon from '@metamask/jazzicon'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
|
||||
import useENSAvatar from 'hooks/useENSAvatar'
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
const StyledIdenticon = styled.div`
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
const StyledIdenticon = styled.div<{ isNavbarEnabled: boolean }>`
|
||||
height: ${({ isNavbarEnabled }) => (isNavbarEnabled ? '24px' : '1rem')};
|
||||
width: ${({ isNavbarEnabled }) => (isNavbarEnabled ? '24px' : '1rem')};
|
||||
border-radius: 1.125rem;
|
||||
background-color: ${({ theme }) => theme.deprecated_bg4};
|
||||
font-size: initial;
|
||||
@@ -22,8 +23,10 @@ export default function Identicon() {
|
||||
const { account } = useWeb3React()
|
||||
const { avatar } = useENSAvatar(account ?? undefined)
|
||||
const [fetchable, setFetchable] = useState(true)
|
||||
const isNavbarEnabled = useNavBarFlag() === NavBarVariant.Enabled
|
||||
const iconSize = isNavbarEnabled ? 24 : 16
|
||||
|
||||
const icon = useMemo(() => account && jazzicon(16, parseInt(account.slice(2, 10), 16)), [account])
|
||||
const icon = useMemo(() => account && jazzicon(iconSize, parseInt(account.slice(2, 10), 16)), [account, iconSize])
|
||||
const iconRef = useRef<HTMLDivElement>(null)
|
||||
useLayoutEffect(() => {
|
||||
const current = iconRef.current
|
||||
@@ -41,7 +44,7 @@ export default function Identicon() {
|
||||
}, [icon, iconRef])
|
||||
|
||||
return (
|
||||
<StyledIdenticon>
|
||||
<StyledIdenticon isNavbarEnabled={isNavbarEnabled}>
|
||||
{avatar && fetchable ? (
|
||||
<StyledAvatar alt="avatar" src={avatar} onError={() => setFetchable(false)}></StyledAvatar>
|
||||
) : (
|
||||
|
||||
@@ -19,7 +19,7 @@ const HandleAccent = styled.path`
|
||||
|
||||
stroke-width: 1.5;
|
||||
stroke: ${({ theme }) => theme.deprecated_white};
|
||||
opacity: 0.6;
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
`
|
||||
|
||||
const LabelGroup = styled.g<{ visible: boolean }>`
|
||||
|
||||
@@ -14,13 +14,15 @@ export default function ListLogo({
|
||||
style,
|
||||
size = '24px',
|
||||
alt,
|
||||
symbol,
|
||||
}: {
|
||||
logoURI: string
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
alt?: string
|
||||
symbol?: string
|
||||
}) {
|
||||
const srcs: string[] = useHttpLocations(logoURI)
|
||||
|
||||
return <StyledListLogo alt={alt} size={size} srcs={srcs} style={style} />
|
||||
return <StyledListLogo alt={alt} size={size} symbol={symbol} srcs={srcs} style={style} />
|
||||
}
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
import { useState } from 'react'
|
||||
import { Slash } from 'react-feather'
|
||||
import { ImageProps } from 'rebass'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
const BAD_SRCS: { [tokenAddress: string]: true } = {}
|
||||
|
||||
interface LogoProps extends Pick<ImageProps, 'style' | 'alt' | 'className'> {
|
||||
srcs: string[]
|
||||
symbol?: string
|
||||
size?: string
|
||||
}
|
||||
|
||||
const MissingImageLogo = styled.div<{ size?: string }>`
|
||||
--size: ${({ size }) => size};
|
||||
border-radius: 100px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
font-size: calc(var(--size) / 3);
|
||||
font-weight: 500;
|
||||
height: ${({ size }) => size ?? '24px'};
|
||||
line-height: ${({ size }) => size ?? '24px'};
|
||||
text-align: center;
|
||||
width: ${({ size }) => size ?? '24px'};
|
||||
`
|
||||
|
||||
/**
|
||||
* Renders an image by sequentially trying a list of URIs, and then eventually a fallback triangle alert
|
||||
*/
|
||||
export default function Logo({ srcs, alt, style, ...rest }: LogoProps) {
|
||||
export default function Logo({ srcs, alt, style, size, symbol, ...rest }: LogoProps) {
|
||||
const [, refresh] = useState<number>(0)
|
||||
|
||||
const theme = useTheme()
|
||||
|
||||
const src: string | undefined = srcs.find((src) => !BAD_SRCS[src])
|
||||
|
||||
if (src) {
|
||||
@@ -34,5 +46,10 @@ export default function Logo({ srcs, alt, style, ...rest }: LogoProps) {
|
||||
)
|
||||
}
|
||||
|
||||
return <Slash {...rest} style={{ ...style, color: theme.deprecated_bg4 }} />
|
||||
return (
|
||||
<MissingImageLogo size={size}>
|
||||
{/* use only first 3 characters of Symbol for design reasons */}
|
||||
{symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
|
||||
</MissingImageLogo>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@ export default function Menu() {
|
||||
</ToggleMenuItem>
|
||||
<ToggleMenuItem onClick={() => toggleDarkMode()}>
|
||||
<div>{darkMode ? <Trans>Light Theme</Trans> : <Trans>Dark Theme</Trans>}</div>
|
||||
{darkMode ? <Moon opacity={0.6} size={16} /> : <Sun opacity={0.6} size={16} />}
|
||||
{darkMode ? <Sun opacity={0.6} size={16} /> : <Moon opacity={0.6} size={16} />}
|
||||
</ToggleMenuItem>
|
||||
<MenuItem href="https://docs.uniswap.org/">
|
||||
<div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from 'react'
|
||||
import { animated, useSpring, useTransition } from 'react-spring'
|
||||
import { useGesture } from 'react-use-gesture'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import { isMobile } from '../../utils/userAgent'
|
||||
|
||||
@@ -11,7 +12,7 @@ const AnimatedDialogOverlay = animated(DialogOverlay)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ redesignFlag?: boolean }>`
|
||||
&[data-reach-dialog-overlay] {
|
||||
z-index: 2;
|
||||
z-index: ${Z_INDEX.modalBackdrop};
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
23
src/components/NavBar/ChainSelector.css.ts
Normal file
23
src/components/NavBar/ChainSelector.css.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { lightGrayOverlayOnHover } from 'nft/css/common.css'
|
||||
|
||||
import { sprinkles } from '../../nft/css/sprinkles.css'
|
||||
|
||||
export const ChainSelector = style([
|
||||
lightGrayOverlayOnHover,
|
||||
sprinkles({
|
||||
borderRadius: '8',
|
||||
height: '40',
|
||||
cursor: 'pointer',
|
||||
border: 'none',
|
||||
color: 'blackBlue',
|
||||
background: 'none',
|
||||
}),
|
||||
])
|
||||
|
||||
export const Image = style([
|
||||
sprinkles({
|
||||
width: '20',
|
||||
height: '20',
|
||||
}),
|
||||
])
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { StyledChevronDown, StyledChevronUp } from 'components/Icons'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
@@ -7,46 +8,16 @@ import useSyncChainQuery from 'hooks/useSyncChainQuery'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { CheckMarkIcon, NewChevronDownIcon, NewChevronUpIcon, TokenWarningRedIcon } from 'nft/components/icons'
|
||||
import { TokenWarningRedIcon } from 'nft/components/icons'
|
||||
import { subhead } from 'nft/css/common.css'
|
||||
import { themeVars, vars } from 'nft/css/sprinkles.css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { ReactNode, useReducer, useRef } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
|
||||
import * as styles from './ChainSwitcher.css'
|
||||
import * as styles from './ChainSelector.css'
|
||||
import ChainSelectorRow from './ChainSelectorRow'
|
||||
import { NavDropdown } from './NavDropdown'
|
||||
|
||||
const ChainRow = ({
|
||||
targetChain,
|
||||
onSelectChain,
|
||||
}: {
|
||||
targetChain: SupportedChainId
|
||||
onSelectChain: (targetChain: number) => void
|
||||
}) => {
|
||||
const { chainId } = useWeb3React()
|
||||
const active = chainId === targetChain
|
||||
const { label, logoUrl } = getChainInfo(targetChain)
|
||||
|
||||
return (
|
||||
<Column borderRadius="12">
|
||||
<Row
|
||||
as="button"
|
||||
background="none"
|
||||
className={`${styles.ChainSwitcherRow} ${subhead}`}
|
||||
onClick={() => onSelectChain(targetChain)}
|
||||
>
|
||||
<ChainDetails>
|
||||
<img src={logoUrl} alt={label} className={styles.Icon} />
|
||||
{label}
|
||||
</ChainDetails>
|
||||
{active && <CheckMarkIcon width={20} height={20} color={vars.color.blue400} />}
|
||||
</Row>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
|
||||
const ChainDetails = ({ children }: { children: ReactNode }) => <Row>{children}</Row>
|
||||
|
||||
const NETWORK_SELECTOR_CHAINS = [
|
||||
SupportedChainId.MAINNET,
|
||||
SupportedChainId.POLYGON,
|
||||
@@ -55,24 +26,36 @@ const NETWORK_SELECTOR_CHAINS = [
|
||||
SupportedChainId.CELO,
|
||||
]
|
||||
|
||||
interface ChainSwitcherProps {
|
||||
interface ChainSelectorProps {
|
||||
leftAlign?: boolean
|
||||
}
|
||||
|
||||
export const ChainSwitcher = ({ leftAlign }: ChainSwitcherProps) => {
|
||||
export const ChainSelector = ({ leftAlign }: ChainSelectorProps) => {
|
||||
const { chainId } = useWeb3React()
|
||||
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false)
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
useOnClickOutside(ref, isOpen ? toggleOpen : undefined, [modalRef])
|
||||
useOnClickOutside(ref, () => setIsOpen(false), [modalRef])
|
||||
|
||||
const info = chainId ? getChainInfo(chainId) : undefined
|
||||
|
||||
const selectChain = useSelectChain()
|
||||
useSyncChainQuery()
|
||||
|
||||
const [pendingChainId, setPendingChainId] = useState<SupportedChainId | undefined>(undefined)
|
||||
|
||||
const onSelectChain = useCallback(
|
||||
async (targetChainId: SupportedChainId) => {
|
||||
setPendingChainId(targetChainId)
|
||||
await selectChain(targetChainId)
|
||||
setPendingChainId(undefined)
|
||||
setIsOpen(false)
|
||||
},
|
||||
[selectChain, setIsOpen]
|
||||
)
|
||||
|
||||
if (!chainId) {
|
||||
return null
|
||||
}
|
||||
@@ -81,15 +64,13 @@ export const ChainSwitcher = ({ leftAlign }: ChainSwitcherProps) => {
|
||||
|
||||
const dropdown = (
|
||||
<NavDropdown top="56" left={leftAlign ? '0' : 'auto'} right={leftAlign ? 'auto' : '0'} ref={modalRef}>
|
||||
<Column marginX="8">
|
||||
<Column paddingX="8">
|
||||
{NETWORK_SELECTOR_CHAINS.map((chainId: SupportedChainId) => (
|
||||
<ChainRow
|
||||
onSelectChain={async (targetChainId: SupportedChainId) => {
|
||||
await selectChain(targetChainId)
|
||||
toggleOpen()
|
||||
}}
|
||||
<ChainSelectorRow
|
||||
onSelectChain={onSelectChain}
|
||||
targetChain={chainId}
|
||||
key={chainId}
|
||||
isPending={chainId === pendingChainId}
|
||||
/>
|
||||
))}
|
||||
</Column>
|
||||
@@ -101,9 +82,9 @@ export const ChainSwitcher = ({ leftAlign }: ChainSwitcherProps) => {
|
||||
<Row
|
||||
as="button"
|
||||
gap="8"
|
||||
className={styles.ChainSwitcher}
|
||||
className={styles.ChainSelector}
|
||||
background={isOpen ? 'accentActiveSoft' : 'none'}
|
||||
onClick={toggleOpen}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{!isSupported ? (
|
||||
<>
|
||||
@@ -120,11 +101,7 @@ export const ChainSwitcher = ({ leftAlign }: ChainSwitcherProps) => {
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{isOpen ? (
|
||||
<NewChevronUpIcon width={16} height={16} color="blackBlue" />
|
||||
) : (
|
||||
<NewChevronDownIcon width={16} height={16} color="blackBlue" />
|
||||
)}
|
||||
{isOpen ? <StyledChevronUp /> : <StyledChevronDown />}
|
||||
</Row>
|
||||
{isOpen && (isMobile ? <Portal>{dropdown}</Portal> : <>{dropdown}</>)}
|
||||
</Box>
|
||||
89
src/components/NavBar/ChainSelectorRow.tsx
Normal file
89
src/components/NavBar/ChainSelectorRow.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Loader from 'components/Loader'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { CheckMarkIcon } from 'nft/components/icons'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
const LOGO_SIZE = 20
|
||||
|
||||
const Container = styled.button`
|
||||
display: grid;
|
||||
background: none;
|
||||
grid-template-columns: min-content 1fr min-content;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
line-height: 24px;
|
||||
border: none;
|
||||
justify-content: space-between;
|
||||
padding: 10px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
width: 240px;
|
||||
transition: ${({ theme }) => theme.transition.duration.medium} ${({ theme }) => theme.transition.timing.ease}
|
||||
background-color;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.backgroundOutline};
|
||||
}
|
||||
`
|
||||
|
||||
const Label = styled.div`
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
font-size: 16px;
|
||||
`
|
||||
|
||||
const Status = styled.div`
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: ${LOGO_SIZE}px;
|
||||
`
|
||||
|
||||
const ApproveText = styled.div`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 12px;
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
`
|
||||
|
||||
const Logo = styled.img`
|
||||
height: ${LOGO_SIZE}px;
|
||||
width: ${LOGO_SIZE}px;
|
||||
margin-right: 12px;
|
||||
`
|
||||
|
||||
export default function ChainSelectorRow({
|
||||
targetChain,
|
||||
onSelectChain,
|
||||
isPending,
|
||||
}: {
|
||||
targetChain: SupportedChainId
|
||||
onSelectChain: (targetChain: number) => void
|
||||
isPending: boolean
|
||||
}) {
|
||||
const { chainId } = useWeb3React()
|
||||
const active = chainId === targetChain
|
||||
const { label, logoUrl } = getChainInfo(targetChain)
|
||||
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Container onClick={() => onSelectChain(targetChain)}>
|
||||
<Logo src={logoUrl} alt={label} />
|
||||
<Label>{label}</Label>
|
||||
{isPending && <ApproveText>Approve in wallet</ApproveText>}
|
||||
<Status>
|
||||
{active && <CheckMarkIcon width={LOGO_SIZE} height={LOGO_SIZE} color={theme.accentActive} />}
|
||||
{isPending && <Loader width={LOGO_SIZE} height={LOGO_SIZE} />}
|
||||
</Status>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { lightGrayOverlayOnHover } from 'nft/css/common.css'
|
||||
|
||||
import { breakpoints, sprinkles } from '../../nft/css/sprinkles.css'
|
||||
|
||||
export const ChainSwitcher = style([
|
||||
lightGrayOverlayOnHover,
|
||||
sprinkles({
|
||||
borderRadius: '8',
|
||||
paddingY: '8',
|
||||
paddingX: '12',
|
||||
cursor: 'pointer',
|
||||
border: 'none',
|
||||
color: 'blackBlue',
|
||||
background: 'none',
|
||||
}),
|
||||
])
|
||||
|
||||
export const ChainSwitcherRow = style([
|
||||
lightGrayOverlayOnHover,
|
||||
sprinkles({
|
||||
border: 'none',
|
||||
justifyContent: 'space-between',
|
||||
paddingX: '8',
|
||||
paddingY: '8',
|
||||
cursor: 'pointer',
|
||||
color: 'blackBlue',
|
||||
borderRadius: '12',
|
||||
width: { sm: 'full' },
|
||||
}),
|
||||
{
|
||||
lineHeight: '24px',
|
||||
'@media': {
|
||||
[`screen and (min-width: ${breakpoints.sm}px)`]: {
|
||||
width: '204px',
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
export const Image = style([
|
||||
sprinkles({
|
||||
width: '20',
|
||||
height: '20',
|
||||
}),
|
||||
])
|
||||
|
||||
export const Icon = style([
|
||||
Image,
|
||||
sprinkles({
|
||||
marginRight: '12',
|
||||
}),
|
||||
])
|
||||
@@ -41,6 +41,7 @@ export const SecondaryText = style([
|
||||
paddingY: '8',
|
||||
paddingX: '8',
|
||||
color: 'darkGray',
|
||||
width: 'full',
|
||||
}),
|
||||
{
|
||||
lineHeight: '20px',
|
||||
|
||||
@@ -126,7 +126,7 @@ export const MenuDropdown = () => {
|
||||
<>
|
||||
<Box position="relative" ref={ref}>
|
||||
<NavIcon isActive={isOpen} onClick={toggleOpen}>
|
||||
<EllipsisIcon width={28} height={28} />
|
||||
<EllipsisIcon width={20} height={20} />
|
||||
</NavIcon>
|
||||
|
||||
{isOpen && (
|
||||
@@ -134,7 +134,7 @@ export const MenuDropdown = () => {
|
||||
<Column gap="16">
|
||||
<Column paddingX="8" gap="4">
|
||||
{nftFlag === NftVariant.Enabled && (
|
||||
<PrimaryMenuRow to="/nft/sell" close={toggleOpen}>
|
||||
<PrimaryMenuRow to="/nfts/sell" close={toggleOpen}>
|
||||
<Icon>
|
||||
<ThinTagIcon width={24} height={24} />
|
||||
</Icon>
|
||||
|
||||
@@ -11,7 +11,7 @@ export const navIcon = style([
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
padding: '8',
|
||||
padding: '10',
|
||||
borderRadius: '8',
|
||||
transition: '250',
|
||||
}),
|
||||
|
||||
@@ -17,6 +17,7 @@ export const NavIcon = ({ children, isActive, onClick }: NavIconProps) => {
|
||||
background={isActive ? 'accentActiveSoft' : 'none'}
|
||||
color={isActive ? 'blackBlue' : 'darkGray'}
|
||||
onClick={onClick}
|
||||
height="40"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { buttonTextSmall, subhead, subheadSmall } from 'nft/css/common.css'
|
||||
import { breakpoints, sprinkles, vars } from '../../nft/css/sprinkles.css'
|
||||
|
||||
const DESKTOP_NAVBAR_WIDTH = 360
|
||||
const MAGNIFYING_GLASS_ICON_WIDTH = 28
|
||||
|
||||
const baseSearchStyle = style([
|
||||
sprinkles({
|
||||
@@ -27,14 +28,13 @@ export const searchBarContainer = style([
|
||||
right: '0',
|
||||
top: '0',
|
||||
zIndex: '3',
|
||||
display: 'inline-block',
|
||||
}),
|
||||
{
|
||||
'@media': {
|
||||
[`screen and (min-width: ${breakpoints.sm}px)`]: {
|
||||
top: '-24px',
|
||||
},
|
||||
[`screen and (min-width: ${breakpoints.lg}px)`]: {
|
||||
right: `-${DESKTOP_NAVBAR_WIDTH / 2}px`,
|
||||
right: `-${DESKTOP_NAVBAR_WIDTH / 2 - MAGNIFYING_GLASS_ICON_WIDTH}px`,
|
||||
top: '-5px',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -43,7 +43,6 @@ export const searchBarContainer = style([
|
||||
export const searchBar = style([
|
||||
baseSearchStyle,
|
||||
sprinkles({
|
||||
height: 'full',
|
||||
color: 'placeholder',
|
||||
paddingX: '16',
|
||||
cursor: 'pointer',
|
||||
@@ -60,7 +59,9 @@ export const searchBarInput = style([
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
}),
|
||||
{ lineHeight: '24px' },
|
||||
{
|
||||
lineHeight: '24px',
|
||||
},
|
||||
])
|
||||
|
||||
export const searchBarDropdown = style([
|
||||
@@ -69,6 +70,7 @@ export const searchBarDropdown = style([
|
||||
borderBottomLeftRadius: '12',
|
||||
borderBottomRightRadius: '12',
|
||||
background: 'lightGray',
|
||||
height: { sm: 'viewHeight', md: 'auto' },
|
||||
}),
|
||||
{
|
||||
borderTop: 'none',
|
||||
@@ -83,7 +85,6 @@ export const suggestionRow = style([
|
||||
justifyContent: 'space-between',
|
||||
paddingY: '8',
|
||||
paddingX: '16',
|
||||
transition: '250',
|
||||
}),
|
||||
{
|
||||
':hover': {
|
||||
@@ -166,3 +167,50 @@ export const notFoundContainer = style([
|
||||
paddingLeft: '16',
|
||||
}),
|
||||
])
|
||||
|
||||
const visibilityTransition = `visibility ${vars.time[125]}, opacity ${vars.time[125]}`
|
||||
const delayedTransitionProperties = `padding 0s ${vars.time[125]}, height 0s ${vars.time[125]}`
|
||||
|
||||
export const hidden = style([
|
||||
sprinkles({
|
||||
visibility: 'hidden',
|
||||
opacity: '0',
|
||||
padding: '0',
|
||||
height: '0',
|
||||
}),
|
||||
{
|
||||
transition: `${visibilityTransition}, ${delayedTransitionProperties}`,
|
||||
transitionTimingFunction: 'ease-in',
|
||||
},
|
||||
])
|
||||
export const visible = style([
|
||||
sprinkles({
|
||||
visibility: 'visible',
|
||||
opacity: '1',
|
||||
height: 'full',
|
||||
}),
|
||||
{
|
||||
transition: `${visibilityTransition}`,
|
||||
transitionTimingFunction: 'ease-out',
|
||||
},
|
||||
])
|
||||
|
||||
export const searchContentCentered = style({
|
||||
'@media': {
|
||||
[`screen and (min-width: ${breakpoints.lg}px)`]: {
|
||||
transform: `translateX(${DESKTOP_NAVBAR_WIDTH / 4}px)`,
|
||||
transition: `transform ${vars.time[125]}`,
|
||||
transitionTimingFunction: 'ease-out',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const searchContentLeftAlign = style({
|
||||
'@media': {
|
||||
[`screen and (min-width: ${breakpoints.lg}px)`]: {
|
||||
transform: 'translateX(0)',
|
||||
transition: `transform ${vars.time[125]}`,
|
||||
transitionTimingFunction: 'ease-in',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -7,15 +7,14 @@ import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { organizeSearchResults } from 'lib/utils/searchBar'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { Overlay } from 'nft/components/modals/Overlay'
|
||||
import { magicalGradientOnHover, subheadSmall } from 'nft/css/common.css'
|
||||
import { useIsMobile, useSearchHistory } from 'nft/hooks'
|
||||
import { useIsMobile, useIsTablet, useSearchHistory } from 'nft/hooks'
|
||||
import { fetchSearchCollections, fetchTrendingCollections } from 'nft/queries'
|
||||
import { fetchSearchTokens } from 'nft/queries/genie/SearchTokensFetcher'
|
||||
import { fetchTrendingTokens } from 'nft/queries/genie/TrendingTokensFetcher'
|
||||
import { FungibleToken, GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
|
||||
import { formatEthPrice } from 'nft/utils/currency'
|
||||
import { ChangeEvent, useEffect, useMemo, useReducer, useRef, useState } from 'react'
|
||||
import { ChangeEvent, ReactNode, useEffect, useMemo, useReducer, useRef, useState } from 'react'
|
||||
import { useQuery } from 'react-query'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
@@ -38,6 +37,7 @@ interface SearchBarDropdownSectionProps {
|
||||
hoveredIndex: number | undefined
|
||||
startingIndex: number
|
||||
setHoveredIndex: (index: number | undefined) => void
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
export const SearchBarDropdownSection = ({
|
||||
@@ -48,6 +48,7 @@ export const SearchBarDropdownSection = ({
|
||||
hoveredIndex,
|
||||
startingIndex,
|
||||
setHoveredIndex,
|
||||
isLoading,
|
||||
}: SearchBarDropdownSectionProps) => {
|
||||
return (
|
||||
<Column gap="12">
|
||||
@@ -56,8 +57,10 @@ export const SearchBarDropdownSection = ({
|
||||
<Box>{header}</Box>
|
||||
</Row>
|
||||
<Column gap="12">
|
||||
{suggestions?.map((suggestion, index) =>
|
||||
isCollection(suggestion) ? (
|
||||
{suggestions.map((suggestion, index) =>
|
||||
isLoading ? (
|
||||
<SkeletonRow key={index} />
|
||||
) : isCollection(suggestion) ? (
|
||||
<CollectionRow
|
||||
key={suggestion.address}
|
||||
collection={suggestion as GenieCollection}
|
||||
@@ -87,17 +90,18 @@ interface SearchBarDropdownProps {
|
||||
tokens: FungibleToken[]
|
||||
collections: GenieCollection[]
|
||||
hasInput: boolean
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }: SearchBarDropdownProps) => {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(undefined)
|
||||
const searchHistory = useSearchHistory(
|
||||
(state: { history: (FungibleToken | GenieCollection)[] }) => state.history
|
||||
).slice(0, 2)
|
||||
export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput, isLoading }: SearchBarDropdownProps) => {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0)
|
||||
const searchHistory = useSearchHistory((state: { history: (FungibleToken | GenieCollection)[] }) => state.history)
|
||||
const shortenedHistory = useMemo(() => searchHistory.slice(0, 2), [searchHistory])
|
||||
const { pathname } = useLocation()
|
||||
const isNFTPage = pathname.includes('/nfts')
|
||||
const isTokenPage = pathname.includes('/tokens')
|
||||
const phase1Flag = useNftFlag()
|
||||
const [resultsState, setResultsState] = useState<ReactNode>()
|
||||
|
||||
const tokenSearchResults =
|
||||
tokens.length > 0 ? (
|
||||
@@ -131,50 +135,56 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
|
||||
)
|
||||
) : null
|
||||
|
||||
const { data: trendingCollectionResults } = useQuery(['trendingCollections', 'eth', 'twenty_four_hours'], () =>
|
||||
fetchTrendingCollections({ volumeType: 'eth', timePeriod: 'ONE_DAY' as TimePeriod, size: 3 })
|
||||
const { data: trendingCollectionResults, isLoading: trendingCollectionsAreLoading } = useQuery(
|
||||
['trendingCollections', 'eth', 'twenty_four_hours'],
|
||||
() => fetchTrendingCollections({ volumeType: 'eth', timePeriod: 'ONE_DAY' as TimePeriod, size: 3 })
|
||||
)
|
||||
|
||||
const trendingCollections = useMemo(() => {
|
||||
return trendingCollectionResults
|
||||
?.map((collection) => {
|
||||
return {
|
||||
...collection,
|
||||
collectionAddress: collection.address,
|
||||
floorPrice: formatEthPrice(collection.floor?.toString()),
|
||||
stats: {
|
||||
total_supply: collection.totalSupply,
|
||||
one_day_change: collection.floorChange,
|
||||
},
|
||||
}
|
||||
})
|
||||
.slice(0, isNFTPage ? 3 : 2)
|
||||
}, [isNFTPage, trendingCollectionResults])
|
||||
|
||||
const showTrendingCollections: boolean = useMemo(
|
||||
() => (trendingCollections?.length ?? 0) > 0 && !isTokenPage && phase1Flag === NftVariant.Enabled,
|
||||
[trendingCollections?.length, isTokenPage, phase1Flag]
|
||||
const trendingCollections = useMemo(
|
||||
() =>
|
||||
trendingCollectionResults
|
||||
? trendingCollectionResults
|
||||
.map((collection) => ({
|
||||
...collection,
|
||||
collectionAddress: collection.address,
|
||||
floorPrice: formatEthPrice(collection.floor?.toString()),
|
||||
stats: {
|
||||
total_supply: collection.totalSupply,
|
||||
one_day_change: collection.floorChange,
|
||||
},
|
||||
}))
|
||||
.slice(0, isNFTPage ? 3 : 2)
|
||||
: [...Array<GenieCollection>(isNFTPage ? 3 : 2)],
|
||||
[isNFTPage, trendingCollectionResults]
|
||||
)
|
||||
|
||||
const { data: trendingTokenResults } = useQuery([], () => fetchTrendingTokens(4), {
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
})
|
||||
const { data: trendingTokenResults, isLoading: trendingTokensAreLoading } = useQuery(
|
||||
['trendingTokens'],
|
||||
() => fetchTrendingTokens(4),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
}
|
||||
)
|
||||
|
||||
const trendingTokensLength = phase1Flag === NftVariant.Enabled ? (isTokenPage ? 3 : 2) : 4
|
||||
|
||||
const trendingTokens = useMemo(() => {
|
||||
return trendingTokenResults?.slice(0, trendingTokensLength)
|
||||
}, [trendingTokenResults, trendingTokensLength])
|
||||
const trendingTokens = useMemo(
|
||||
() =>
|
||||
trendingTokenResults
|
||||
? trendingTokenResults.slice(0, trendingTokensLength)
|
||||
: [...Array<FungibleToken>(trendingTokensLength)],
|
||||
[trendingTokenResults, trendingTokensLength]
|
||||
)
|
||||
|
||||
const totalSuggestions = hasInput
|
||||
? tokens.length + collections.length
|
||||
: Math.min(searchHistory.length, 2) +
|
||||
: Math.min(shortenedHistory.length, 2) +
|
||||
(isNFTPage || !isTokenPage ? trendingCollections?.length ?? 0 : 0) +
|
||||
(isTokenPage || !isNFTPage ? trendingTokens?.length ?? 0 : 0)
|
||||
|
||||
// Close the modal on escape
|
||||
// Navigate search results via arrow keys
|
||||
useEffect(() => {
|
||||
const keyDownHandler = (event: KeyboardEvent) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
@@ -185,6 +195,7 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
|
||||
setHoveredIndex(hoveredIndex - 1)
|
||||
}
|
||||
} else if (event.key === 'ArrowDown') {
|
||||
event.preventDefault()
|
||||
if (hoveredIndex && hoveredIndex === totalSuggestions - 1) {
|
||||
setHoveredIndex(0)
|
||||
} else {
|
||||
@@ -200,61 +211,90 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
|
||||
}
|
||||
}, [toggleOpen, hoveredIndex, totalSuggestions])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
const currentState = () =>
|
||||
hasInput ? (
|
||||
// Empty or Up to 8 combined tokens and nfts
|
||||
<Column gap="20">
|
||||
{isNFTPage ? (
|
||||
<>
|
||||
{collectionSearchResults}
|
||||
{tokenSearchResults}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{tokenSearchResults}
|
||||
{collectionSearchResults}
|
||||
</>
|
||||
)}
|
||||
</Column>
|
||||
) : (
|
||||
// Recent Searches, Trending Tokens, Trending Collections
|
||||
<Column gap="20">
|
||||
{shortenedHistory.length > 0 && (
|
||||
<SearchBarDropdownSection
|
||||
hoveredIndex={hoveredIndex}
|
||||
startingIndex={0}
|
||||
setHoveredIndex={setHoveredIndex}
|
||||
toggleOpen={toggleOpen}
|
||||
suggestions={shortenedHistory}
|
||||
header={<Trans>Recent searches</Trans>}
|
||||
headerIcon={<ClockIcon />}
|
||||
/>
|
||||
)}
|
||||
{!isNFTPage && (
|
||||
<SearchBarDropdownSection
|
||||
hoveredIndex={hoveredIndex}
|
||||
startingIndex={shortenedHistory.length}
|
||||
setHoveredIndex={setHoveredIndex}
|
||||
toggleOpen={toggleOpen}
|
||||
suggestions={trendingTokens}
|
||||
header={<Trans>Popular tokens</Trans>}
|
||||
headerIcon={<TrendingArrow />}
|
||||
isLoading={trendingTokensAreLoading}
|
||||
/>
|
||||
)}
|
||||
{!isTokenPage && phase1Flag === NftVariant.Enabled && (
|
||||
<SearchBarDropdownSection
|
||||
hoveredIndex={hoveredIndex}
|
||||
startingIndex={shortenedHistory.length + (isNFTPage ? 0 : trendingTokens?.length ?? 0)}
|
||||
setHoveredIndex={setHoveredIndex}
|
||||
toggleOpen={toggleOpen}
|
||||
suggestions={trendingCollections as unknown as GenieCollection[]}
|
||||
header={<Trans>Popular NFT collections</Trans>}
|
||||
headerIcon={<TrendingArrow />}
|
||||
isLoading={trendingCollectionsAreLoading}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
)
|
||||
|
||||
setResultsState(currentState)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
isLoading,
|
||||
tokens,
|
||||
collections,
|
||||
trendingCollections,
|
||||
trendingCollectionsAreLoading,
|
||||
trendingTokens,
|
||||
trendingTokensAreLoading,
|
||||
hoveredIndex,
|
||||
phase1Flag,
|
||||
toggleOpen,
|
||||
shortenedHistory,
|
||||
hasInput,
|
||||
isNFTPage,
|
||||
isTokenPage,
|
||||
])
|
||||
|
||||
return (
|
||||
<Box className={styles.searchBarDropdown}>
|
||||
{hasInput ? (
|
||||
// Empty or Up to 8 combined tokens and nfts
|
||||
<Column gap="20">
|
||||
{isNFTPage ? (
|
||||
<>
|
||||
{collectionSearchResults}
|
||||
{tokenSearchResults}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{tokenSearchResults}
|
||||
{collectionSearchResults}
|
||||
</>
|
||||
)}
|
||||
</Column>
|
||||
) : (
|
||||
// Recent Searches, Trending Tokens, Trending Collections
|
||||
<Column gap="20">
|
||||
{searchHistory.length > 0 && (
|
||||
<SearchBarDropdownSection
|
||||
hoveredIndex={hoveredIndex}
|
||||
startingIndex={0}
|
||||
setHoveredIndex={setHoveredIndex}
|
||||
toggleOpen={toggleOpen}
|
||||
suggestions={searchHistory}
|
||||
header={<Trans>Recent searches</Trans>}
|
||||
headerIcon={<ClockIcon />}
|
||||
/>
|
||||
)}
|
||||
{(trendingTokens?.length ?? 0) > 0 && !isNFTPage && (
|
||||
<SearchBarDropdownSection
|
||||
hoveredIndex={hoveredIndex}
|
||||
startingIndex={searchHistory.length}
|
||||
setHoveredIndex={setHoveredIndex}
|
||||
toggleOpen={toggleOpen}
|
||||
suggestions={trendingTokens ?? []}
|
||||
header={<Trans>Popular tokens</Trans>}
|
||||
headerIcon={<TrendingArrow />}
|
||||
/>
|
||||
)}
|
||||
{showTrendingCollections && (
|
||||
<SearchBarDropdownSection
|
||||
hoveredIndex={hoveredIndex}
|
||||
startingIndex={searchHistory.length + (isNFTPage ? 0 : trendingTokens?.length ?? 0)}
|
||||
setHoveredIndex={setHoveredIndex}
|
||||
toggleOpen={toggleOpen}
|
||||
suggestions={trendingCollections as unknown as GenieCollection[]}
|
||||
header={<Trans>Popular NFT collections</Trans>}
|
||||
headerIcon={<TrendingArrow />}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
)}
|
||||
<Box opacity={isLoading ? '0.3' : '1'} transition="125">
|
||||
{resultsState}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -268,9 +308,11 @@ export const SearchBar = () => {
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const debouncedSearchValue = useDebounce(searchValue, 300)
|
||||
const searchRef = useRef<HTMLDivElement>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const { pathname } = useLocation()
|
||||
const phase1Flag = useNftFlag()
|
||||
const isMobile = useIsMobile()
|
||||
const isTablet = useIsTablet()
|
||||
|
||||
useOnClickOutside(searchRef, () => {
|
||||
isOpen && toggleOpen()
|
||||
@@ -300,6 +342,7 @@ export const SearchBar = () => {
|
||||
|
||||
const [reducedTokens, reducedCollections] = organizeSearchResults(isNFTPage, tokens ?? [], collections ?? [])
|
||||
|
||||
// close dropdown on escape
|
||||
useEffect(() => {
|
||||
const escapeKeyDownHandler = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isOpen) {
|
||||
@@ -320,64 +363,77 @@ export const SearchBar = () => {
|
||||
setSearchValue('')
|
||||
}, [pathname])
|
||||
|
||||
// auto set cursor when searchbar is opened
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const placeholderText = phase1Flag === NftVariant.Enabled ? t`Search tokens and NFT collections` : t`Search tokens`
|
||||
const isMobileOrTablet = isMobile || isTablet
|
||||
const showCenteredSearchContent = !isOpen && phase1Flag !== NftVariant.Enabled && !isMobileOrTablet
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
<Box
|
||||
position={isOpen ? { sm: 'fixed', md: 'absolute' } : 'static'}
|
||||
position={{ sm: 'fixed', md: 'absolute' }}
|
||||
width={{ sm: isOpen ? 'viewWidth' : 'auto', md: 'auto' }}
|
||||
ref={searchRef}
|
||||
className={styles.searchBarContainer}
|
||||
display={{ sm: isOpen ? 'inline-block' : 'none', xl: 'inline-block' }}
|
||||
>
|
||||
<Row
|
||||
className={clsx(`${styles.searchBar} ${!isOpen && magicalGradientOnHover}`)}
|
||||
borderRadius={isOpen ? undefined : '12'}
|
||||
className={clsx(
|
||||
` ${styles.searchBar} ${!isOpen && !isMobile && magicalGradientOnHover} ${
|
||||
isMobileOrTablet && (isOpen ? styles.visible : styles.hidden)
|
||||
}`
|
||||
)}
|
||||
borderRadius={isOpen || isMobileOrTablet ? undefined : '12'}
|
||||
borderTopRightRadius={isOpen && !isMobile ? '12' : undefined}
|
||||
borderTopLeftRadius={isOpen && !isMobile ? '12' : undefined}
|
||||
borderBottomWidth={isOpen ? '0px' : '1px'}
|
||||
display={{ sm: isOpen ? 'flex' : 'none', xl: 'flex' }}
|
||||
justifyContent={isOpen || phase1Flag === NftVariant.Enabled ? 'flex-start' : 'center'}
|
||||
onFocus={() => !isOpen && toggleOpen()}
|
||||
borderBottomWidth={isOpen || isMobileOrTablet ? '0px' : '1px'}
|
||||
onClick={() => !isOpen && toggleOpen()}
|
||||
gap="12"
|
||||
>
|
||||
<Box display={{ sm: 'none', md: 'flex' }}>
|
||||
<MagnifyingGlassIcon />
|
||||
</Box>
|
||||
<Box display={{ sm: 'flex', md: 'none' }} color="placeholder" onClick={toggleOpen}>
|
||||
<ChevronLeftIcon />
|
||||
<Box className={showCenteredSearchContent ? styles.searchContentCentered : styles.searchContentLeftAlign}>
|
||||
<Box display={{ sm: 'none', md: 'flex' }}>
|
||||
<MagnifyingGlassIcon />
|
||||
</Box>
|
||||
<Box display={{ sm: 'flex', md: 'none' }} color="placeholder" onClick={toggleOpen}>
|
||||
<ChevronLeftIcon />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
as="input"
|
||||
placeholder={placeholderText}
|
||||
width={isOpen || phase1Flag === NftVariant.Enabled ? 'full' : '120'}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
!isOpen && toggleOpen()
|
||||
setSearchValue(event.target.value)
|
||||
}}
|
||||
className={styles.searchBarInput}
|
||||
className={`${styles.searchBarInput} ${
|
||||
showCenteredSearchContent ? styles.searchContentCentered : styles.searchContentLeftAlign
|
||||
}`}
|
||||
value={searchValue}
|
||||
ref={inputRef}
|
||||
width={phase1Flag === NftVariant.Enabled || isOpen ? 'full' : '160'}
|
||||
/>
|
||||
</Row>
|
||||
<Box display={{ sm: isOpen ? 'none' : 'flex', xl: 'none' }}>
|
||||
<NavIcon onClick={toggleOpen}>
|
||||
<NavMagnifyingGlassIcon width={28} height={28} />
|
||||
</NavIcon>
|
||||
</Box>
|
||||
{isOpen &&
|
||||
(debouncedSearchValue.length > 0 && (tokensAreLoading || collectionsAreLoading) ? (
|
||||
<SkeletonRow />
|
||||
) : (
|
||||
<Box className={clsx(isOpen ? styles.visible : styles.hidden)}>
|
||||
{isOpen && (
|
||||
<SearchBarDropdown
|
||||
toggleOpen={toggleOpen}
|
||||
tokens={reducedTokens}
|
||||
collections={reducedCollections}
|
||||
hasInput={debouncedSearchValue.length > 0}
|
||||
isLoading={tokensAreLoading || (collectionsAreLoading && phase1Flag === NftVariant.Enabled)}
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{isOpen && <Overlay />}
|
||||
<NavIcon onClick={toggleOpen}>
|
||||
<NavMagnifyingGlassIcon />
|
||||
</NavIcon>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
22
src/components/NavBar/ShoppingBag.css.ts
Normal file
22
src/components/NavBar/ShoppingBag.css.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { sprinkles } from 'nft/css/sprinkles.css'
|
||||
|
||||
export const bagQuantity = style([
|
||||
sprinkles({
|
||||
position: 'absolute',
|
||||
top: '4',
|
||||
right: '4',
|
||||
backgroundColor: 'magicGradient',
|
||||
borderRadius: 'round',
|
||||
color: 'explicitWhite',
|
||||
textAlign: 'center',
|
||||
fontWeight: 'semibold',
|
||||
paddingY: '1',
|
||||
paddingX: '4',
|
||||
}),
|
||||
{
|
||||
fontSize: '8px',
|
||||
lineHeight: '12px',
|
||||
minWidth: '14px',
|
||||
},
|
||||
])
|
||||
47
src/components/NavBar/ShoppingBag.tsx
Normal file
47
src/components/NavBar/ShoppingBag.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { NavIcon } from 'components/NavBar/NavIcon'
|
||||
import * as styles from 'components/NavBar/ShoppingBag.css'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { BagIcon, HundredsOverflowIcon, TagIcon } from 'nft/components/icons'
|
||||
import { useBag, useSellAsset } from 'nft/hooks'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
export const ShoppingBag = () => {
|
||||
const itemsInBag = useBag((state) => state.itemsInBag)
|
||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
||||
const [bagQuantity, setBagQuantity] = useState(0)
|
||||
const [sellQuantity, setSellQuantity] = useState(0)
|
||||
const location = useLocation()
|
||||
|
||||
const toggleBag = useBag((s) => s.toggleBag)
|
||||
|
||||
useEffect(() => {
|
||||
setBagQuantity(itemsInBag.length)
|
||||
}, [itemsInBag])
|
||||
|
||||
useEffect(() => {
|
||||
setSellQuantity(sellAssets.length)
|
||||
}, [sellAssets])
|
||||
|
||||
const isSell = location.pathname === '/nfts/sell'
|
||||
|
||||
return (
|
||||
<NavIcon onClick={toggleBag}>
|
||||
{isSell ? (
|
||||
<>
|
||||
<TagIcon width={20} height={20} />
|
||||
{sellQuantity ? (
|
||||
<Box className={styles.bagQuantity}>{sellQuantity > 99 ? <HundredsOverflowIcon /> : sellQuantity}</Box>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BagIcon width={20} height={20} />
|
||||
{bagQuantity ? (
|
||||
<Box className={styles.bagQuantity}>{bagQuantity > 99 ? <HundredsOverflowIcon /> : bagQuantity}</Box>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</NavIcon>
|
||||
)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { putCommas } from 'nft/utils/putCommas'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
|
||||
import { TokenWarningRedIcon, VerifiedIcon } from '../../nft/components/icons'
|
||||
import { VerifiedIcon } from '../../nft/components/icons'
|
||||
import * as styles from './SearchBar.css'
|
||||
|
||||
interface CollectionRowProps {
|
||||
@@ -151,11 +151,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index
|
||||
<Column className={styles.suggestionPrimaryContainer}>
|
||||
<Row gap="4" width="full">
|
||||
<Box className={styles.primaryText}>{token.name}</Box>
|
||||
{token.onDefaultList ? (
|
||||
<VerifiedIcon className={styles.suggestionIcon} />
|
||||
) : (
|
||||
<TokenWarningRedIcon className={styles.suggestionIcon} />
|
||||
)}
|
||||
{token.onDefaultList && <VerifiedIcon className={styles.suggestionIcon} />}
|
||||
</Row>
|
||||
<Box className={styles.secondaryText}>{token.symbol}</Box>
|
||||
</Column>
|
||||
@@ -179,13 +175,21 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index
|
||||
|
||||
export const SkeletonRow = () => {
|
||||
return (
|
||||
<Box className={styles.searchBarDropdown}>
|
||||
<Row className={styles.suggestionRow}>
|
||||
<Row>
|
||||
<Box className={styles.imageHolder} />
|
||||
<Box borderRadius="round" height="16" width="160" background="loading" />
|
||||
</Row>
|
||||
<Row className={styles.suggestionRow}>
|
||||
<Row width="full">
|
||||
<Box className={styles.imageHolder} />
|
||||
<Column gap="4" width="full">
|
||||
<Row justifyContent="space-between">
|
||||
<Box borderRadius="round" height="20" background="loading" style={{ width: '180px' }} />
|
||||
<Box borderRadius="round" height="20" width="48" background="loading" />
|
||||
</Row>
|
||||
|
||||
<Row justifyContent="space-between">
|
||||
<Box borderRadius="round" height="16" width="120" background="loading" />
|
||||
<Box borderRadius="round" height="16" width="48" background="loading" />
|
||||
</Row>
|
||||
</Column>
|
||||
</Row>
|
||||
</Box>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import Navbar from './Navbar'
|
||||
|
||||
export default Navbar
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import Web3Status from 'components/Web3Status'
|
||||
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Row } from 'nft/components/Flex'
|
||||
import { UniIcon } from 'nft/components/icons'
|
||||
import { ReactNode } from 'react'
|
||||
import { NavLink, NavLinkProps, useLocation } from 'react-router-dom'
|
||||
|
||||
import { Box } from '../../nft/components/Box'
|
||||
import { Row } from '../../nft/components/Flex'
|
||||
import { UniIcon } from '../../nft/components/icons'
|
||||
import { ChainSwitcher } from './ChainSwitcher'
|
||||
import { ChainSelector } from './ChainSelector'
|
||||
import { MenuDropdown } from './MenuDropdown'
|
||||
import * as styles from './Navbar.css'
|
||||
import { SearchBar } from './SearchBar'
|
||||
import { ShoppingBag } from './ShoppingBag'
|
||||
import * as styles from './style.css'
|
||||
|
||||
interface MenuItemProps {
|
||||
href: string
|
||||
@@ -64,6 +65,9 @@ const PageTabs = () => {
|
||||
}
|
||||
|
||||
const Navbar = () => {
|
||||
const { pathname } = useLocation()
|
||||
const isNftPage = pathname.startsWith('/nfts')
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className={styles.nav}>
|
||||
@@ -73,7 +77,7 @@ const Navbar = () => {
|
||||
<UniIcon width="48" height="48" className={styles.logo} />
|
||||
</Box>
|
||||
<Box display={{ sm: 'flex', lg: 'none' }}>
|
||||
<ChainSwitcher leftAlign={true} />
|
||||
<ChainSelector leftAlign={true} />
|
||||
</Box>
|
||||
<Row gap="8" display={{ sm: 'none', lg: 'flex' }}>
|
||||
<PageTabs />
|
||||
@@ -90,8 +94,9 @@ const Navbar = () => {
|
||||
<Box display={{ sm: 'none', lg: 'flex' }}>
|
||||
<MenuDropdown />
|
||||
</Box>
|
||||
{isNftPage && <ShoppingBag />}
|
||||
<Box display={{ sm: 'none', lg: 'flex' }}>
|
||||
<ChainSwitcher />
|
||||
<ChainSelector />
|
||||
</Box>
|
||||
|
||||
<Web3Status />
|
||||
@@ -101,7 +106,9 @@ const Navbar = () => {
|
||||
</nav>
|
||||
<Box className={styles.mobileBottomBar}>
|
||||
<PageTabs />
|
||||
<MenuDropdown />
|
||||
<Box marginY="4">
|
||||
<MenuDropdown />
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
@@ -81,6 +81,8 @@ const baseMenuItem = style([
|
||||
borderRadius: '12',
|
||||
transition: '250',
|
||||
height: 'min',
|
||||
width: 'full',
|
||||
textAlign: 'center',
|
||||
}),
|
||||
{
|
||||
lineHeight: '24px',
|
||||
@@ -4,9 +4,10 @@ import useInterval from 'lib/hooks/useInterval'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { usePopper } from 'react-popper'
|
||||
import styled from 'styled-components/macro'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 9999;
|
||||
z-index: ${Z_INDEX.popover};
|
||||
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
|
||||
opacity: ${(props) => (props.show ? 1 : 0)};
|
||||
transition: visibility 150ms linear, opacity 150ms linear;
|
||||
|
||||
@@ -7,7 +7,8 @@ import { useEffect } from 'react'
|
||||
import { MessageCircle, X } from 'react-feather'
|
||||
import { useShowSurveyPopup } from 'state/user/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ExternalLink, ThemedText, Z_INDEX } from 'theme'
|
||||
import { ExternalLink, ThemedText } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import BGImage from '../../assets/images/survey-orb.svg'
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ const StopOverflowQuery = `@media screen and (min-width: ${MEDIA_WIDTHS.deprecat
|
||||
|
||||
const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean; xlPadding: boolean }>`
|
||||
position: fixed;
|
||||
top: ${({ extraPadding }) => (extraPadding ? '64px' : '56px')};
|
||||
top: ${({ extraPadding }) => (extraPadding ? '72px' : '64px')};
|
||||
right: 1rem;
|
||||
max-width: 355px !important;
|
||||
width: 100%;
|
||||
@@ -52,7 +52,7 @@ const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean; xlPadding:
|
||||
`};
|
||||
|
||||
${StopOverflowQuery} {
|
||||
top: ${({ extraPadding, xlPadding }) => (xlPadding ? '64px' : extraPadding ? '64px' : '56px')};
|
||||
top: ${({ extraPadding, xlPadding }) => (xlPadding ? '72px' : extraPadding ? '72px' : '64px')};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ const ToggleWrap = styled.div`
|
||||
`
|
||||
|
||||
const ToggleLabel = styled.div`
|
||||
opacity: 0.6;
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
margin-right: 10px;
|
||||
`
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ import { RoutingDiagramEntry } from 'components/swap/SwapRoute'
|
||||
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'
|
||||
import { Box } from 'rebass'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText, Z_INDEX } from 'theme'
|
||||
import { ThemedText } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import { ReactComponent as DotLine } from '../../assets/svg/dot_line.svg'
|
||||
import { MouseoverTooltip } from '../Tooltip'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
|
||||
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
|
||||
import { getTokenAddress } from 'components/AmplitudeAnalytics/utils'
|
||||
import { ElementName, Event, EventName } from 'analytics/constants'
|
||||
import { TraceEvent } from 'analytics/TraceEvent'
|
||||
import { getTokenAddress } from 'analytics/utils'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { AutoRow } from 'components/Row'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
|
||||
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
|
||||
import { ElementName, Event, EventName } from 'analytics/constants'
|
||||
import { TraceEvent } from 'analytics/TraceEvent'
|
||||
import { LightGreyCard } from 'components/Card'
|
||||
import QuestionHelper from 'components/QuestionHelper'
|
||||
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
|
||||
@@ -165,7 +165,7 @@ function CurrencyRow({
|
||||
selected={otherSelected}
|
||||
>
|
||||
<Column>
|
||||
<CurrencyLogo currency={currency} size={'24px'} />
|
||||
<CurrencyLogo currency={currency} size={'36px'} />
|
||||
</Column>
|
||||
<AutoColumn>
|
||||
<Row>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { Currency, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { EventName, ModalName } from 'components/AmplitudeAnalytics/constants'
|
||||
import { Trace } from 'components/AmplitudeAnalytics/Trace'
|
||||
import { EventName, ModalName } from 'analytics/constants'
|
||||
import { Trace } from 'analytics/Trace'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import useDebounce from 'hooks/useDebounce'
|
||||
|
||||
@@ -8,6 +8,7 @@ import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
|
||||
import { useUserAddedTokens } from 'state/user/hooks'
|
||||
|
||||
import useLast from '../../hooks/useLast'
|
||||
import { useWindowSize } from '../../hooks/useWindowSize'
|
||||
import Modal from '../Modal'
|
||||
import { CurrencySearch } from './CurrencySearch'
|
||||
import { ImportList } from './ImportList'
|
||||
@@ -97,11 +98,16 @@ export default memo(function CurrencySearchModal({
|
||||
[setModalView, prevView]
|
||||
)
|
||||
|
||||
const { height: windowHeight } = useWindowSize()
|
||||
// change min height if not searching
|
||||
let minHeight: number | undefined = 80
|
||||
let modalHeight: number | undefined = 80
|
||||
let content = null
|
||||
switch (modalView) {
|
||||
case CurrencyModalView.search:
|
||||
if (windowHeight) {
|
||||
// Converts pixel units to vh for Modal component
|
||||
modalHeight = Math.min(Math.round((680 / windowHeight) * 100), 80)
|
||||
}
|
||||
content = (
|
||||
<CurrencySearch
|
||||
isOpen={isOpen}
|
||||
@@ -119,7 +125,7 @@ export default memo(function CurrencySearchModal({
|
||||
)
|
||||
break
|
||||
case CurrencyModalView.tokenSafety:
|
||||
minHeight = undefined
|
||||
modalHeight = undefined
|
||||
if (tokenSafetyFlag === TokenSafetyVariant.Enabled && warningToken) {
|
||||
content = (
|
||||
<TokenSafety
|
||||
@@ -133,7 +139,7 @@ export default memo(function CurrencySearchModal({
|
||||
break
|
||||
case CurrencyModalView.importToken:
|
||||
if (importToken) {
|
||||
minHeight = undefined
|
||||
modalHeight = undefined
|
||||
if (tokenSafetyFlag === TokenSafetyVariant.Enabled) {
|
||||
showTokenSafetySpeedbump(importToken)
|
||||
}
|
||||
@@ -149,7 +155,7 @@ export default memo(function CurrencySearchModal({
|
||||
}
|
||||
break
|
||||
case CurrencyModalView.importList:
|
||||
minHeight = 40
|
||||
modalHeight = 40
|
||||
if (importList && listURL) {
|
||||
content = <ImportList list={importList} listURL={listURL} onDismiss={onDismiss} setModalView={setModalView} />
|
||||
}
|
||||
@@ -167,7 +173,7 @@ export default memo(function CurrencySearchModal({
|
||||
break
|
||||
}
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={80} minHeight={minHeight}>
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={modalHeight} minHeight={modalHeight}>
|
||||
{content}
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Plural, Trans } from '@lingui/macro'
|
||||
import { Currency, Token } from '@uniswap/sdk-core'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
|
||||
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
|
||||
import { ElementName, Event, EventName } from 'analytics/constants'
|
||||
import { TraceEvent } from 'analytics/TraceEvent'
|
||||
import { ButtonPrimary } from 'components/Button'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { RowBetween } from 'components/Row'
|
||||
|
||||
@@ -8,7 +8,7 @@ import { navigatorLocale, useActiveLocale } from '../../hooks/useActiveLocale'
|
||||
import { StyledInternalLink, ThemedText } from '../../theme'
|
||||
|
||||
const Container = styled(ThemedText.DeprecatedSmall)`
|
||||
opacity: 0.6;
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import Modal from '../Modal'
|
||||
import TokenSafety from '.'
|
||||
import TokenSafety, { TokenSafetyProps } from '.'
|
||||
|
||||
interface TokenSafetyModalProps {
|
||||
interface TokenSafetyModalProps extends TokenSafetyProps {
|
||||
isOpen: boolean
|
||||
tokenAddress: string | null
|
||||
secondTokenAddress?: string
|
||||
onContinue: () => void
|
||||
onCancel: () => void
|
||||
showCancel?: boolean
|
||||
}
|
||||
|
||||
export default function TokenSafetyModal({
|
||||
@@ -16,6 +11,7 @@ export default function TokenSafetyModal({
|
||||
secondTokenAddress,
|
||||
onContinue,
|
||||
onCancel,
|
||||
onBlocked,
|
||||
showCancel,
|
||||
}: TokenSafetyModalProps) {
|
||||
return (
|
||||
@@ -23,8 +19,9 @@ export default function TokenSafetyModal({
|
||||
<TokenSafety
|
||||
tokenAddress={tokenAddress}
|
||||
secondTokenAddress={secondTokenAddress}
|
||||
onCancel={onCancel}
|
||||
onContinue={onContinue}
|
||||
onBlocked={onBlocked}
|
||||
onCancel={onCancel}
|
||||
showCancel={showCancel}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
@@ -64,7 +64,7 @@ const StyledCloseButton = styled(StyledButton)`
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
opacity: 0.6;
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
transition: opacity 250ms ease;
|
||||
}
|
||||
`
|
||||
@@ -73,11 +73,13 @@ const Buttons = ({
|
||||
warning,
|
||||
onContinue,
|
||||
onCancel,
|
||||
onBlocked,
|
||||
showCancel,
|
||||
}: {
|
||||
warning: Warning
|
||||
onContinue: () => void
|
||||
onCancel: () => void
|
||||
onBlocked?: () => void
|
||||
showCancel?: boolean
|
||||
}) => {
|
||||
return warning.canProceed ? (
|
||||
@@ -88,7 +90,7 @@ const Buttons = ({
|
||||
{showCancel && <StyledCancelButton onClick={onCancel}>Cancel</StyledCancelButton>}
|
||||
</>
|
||||
) : (
|
||||
<StyledCloseButton onClick={onCancel}>
|
||||
<StyledCloseButton onClick={onBlocked ?? onCancel}>
|
||||
<Trans>Close</Trans>
|
||||
</StyledCloseButton>
|
||||
)
|
||||
@@ -130,10 +132,10 @@ const ExplorerLinkWrapper = styled.div`
|
||||
cursor: pointer;
|
||||
|
||||
:hover {
|
||||
opacity: 0.6;
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
}
|
||||
:active {
|
||||
opacity: 0.4;
|
||||
opacity: ${({ theme }) => theme.opacity.click};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -184,11 +186,12 @@ const StyledExternalLink = styled(ExternalLink)`
|
||||
font-weight: 600;
|
||||
`
|
||||
|
||||
interface TokenSafetyProps {
|
||||
export interface TokenSafetyProps {
|
||||
tokenAddress: string | null
|
||||
secondTokenAddress?: string
|
||||
onContinue: () => void
|
||||
onCancel: () => void
|
||||
onBlocked?: () => void
|
||||
showCancel?: boolean
|
||||
}
|
||||
|
||||
@@ -197,6 +200,7 @@ export default function TokenSafety({
|
||||
secondTokenAddress,
|
||||
onContinue,
|
||||
onCancel,
|
||||
onBlocked,
|
||||
showCancel,
|
||||
}: TokenSafetyProps) {
|
||||
const logos = []
|
||||
@@ -261,7 +265,13 @@ export default function TokenSafety({
|
||||
</InfoText>
|
||||
</ShortColumn>
|
||||
<LinkColumn>{urls}</LinkColumn>
|
||||
<Buttons warning={displayWarning} onContinue={acknowledge} onCancel={onCancel} showCancel={showCancel} />
|
||||
<Buttons
|
||||
warning={displayWarning}
|
||||
onContinue={acknowledge}
|
||||
onCancel={onCancel}
|
||||
onBlocked={onBlocked}
|
||||
showCancel={showCancel}
|
||||
/>
|
||||
</Container>
|
||||
</Wrapper>
|
||||
)
|
||||
|
||||
103
src/components/Tokens/TokenDetails/About.tsx
Normal file
103
src/components/Tokens/TokenDetails/About.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { darken } from 'polished'
|
||||
import { useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import Resource from './Resource'
|
||||
|
||||
const NoInfoAvailable = styled.span`
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
`
|
||||
const TokenDescriptionContainer = styled.div`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
max-height: fit-content;
|
||||
padding-top: 16px;
|
||||
line-height: 24px;
|
||||
white-space: pre-wrap;
|
||||
`
|
||||
|
||||
const TruncateDescriptionButton = styled.div`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
padding-top: 14px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: ${({ theme }) => darken(0.1, theme.textSecondary)};
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
const truncateDescription = (desc: string) => {
|
||||
//trim the string to the maximum length
|
||||
let tokenDescriptionTruncated = desc.slice(0, TRUNCATE_CHARACTER_COUNT)
|
||||
//re-trim if we are in the middle of a word
|
||||
tokenDescriptionTruncated = `${tokenDescriptionTruncated.slice(
|
||||
0,
|
||||
Math.min(tokenDescriptionTruncated.length, tokenDescriptionTruncated.lastIndexOf(' '))
|
||||
)}...`
|
||||
return tokenDescriptionTruncated
|
||||
}
|
||||
|
||||
const TRUNCATE_CHARACTER_COUNT = 400
|
||||
|
||||
export const AboutContainer = styled.div`
|
||||
gap: 16px;
|
||||
padding: 24px 0px;
|
||||
`
|
||||
export const AboutHeader = styled.span`
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
`
|
||||
|
||||
export const ResourcesContainer = styled.div`
|
||||
display: flex;
|
||||
padding-top: 12px;
|
||||
gap: 14px;
|
||||
`
|
||||
|
||||
type AboutSectionProps = {
|
||||
address: string
|
||||
description?: string | null | undefined
|
||||
homepageUrl?: string | null | undefined
|
||||
twitterName?: string | null | undefined
|
||||
}
|
||||
|
||||
export function AboutSection({ address, description, homepageUrl, twitterName }: AboutSectionProps) {
|
||||
const [isDescriptionTruncated, setIsDescriptionTruncated] = useState(true)
|
||||
const shouldTruncate = !!description && description.length > TRUNCATE_CHARACTER_COUNT
|
||||
|
||||
const tokenDescription = shouldTruncate && isDescriptionTruncated ? truncateDescription(description) : description
|
||||
|
||||
return (
|
||||
<AboutContainer>
|
||||
<AboutHeader>
|
||||
<Trans>About</Trans>
|
||||
</AboutHeader>
|
||||
<TokenDescriptionContainer>
|
||||
{!description && (
|
||||
<NoInfoAvailable>
|
||||
<Trans>No token information available</Trans>
|
||||
</NoInfoAvailable>
|
||||
)}
|
||||
{tokenDescription}
|
||||
{shouldTruncate && (
|
||||
<TruncateDescriptionButton onClick={() => setIsDescriptionTruncated(!isDescriptionTruncated)}>
|
||||
{isDescriptionTruncated ? <Trans>Read more</Trans> : <Trans>Hide</Trans>}
|
||||
</TruncateDescriptionButton>
|
||||
)}
|
||||
</TokenDescriptionContainer>
|
||||
<ResourcesContainer>
|
||||
<Resource name={'Etherscan'} link={`https://etherscan.io/address/${address}`} />
|
||||
<Resource name={'Protocol info'} link={`https://info.uniswap.org/#/tokens/${address}`} />
|
||||
{homepageUrl && <Resource name={'Website'} link={homepageUrl} />}
|
||||
{twitterName && <Resource name={'Twitter'} link={`https://twitter.com/${twitterName}`} />}
|
||||
</ResourcesContainer>
|
||||
</AboutContainer>
|
||||
)
|
||||
}
|
||||
36
src/components/Tokens/TokenDetails/AddressSection.tsx
Normal file
36
src/components/Tokens/TokenDetails/AddressSection.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import styled from 'styled-components/macro'
|
||||
import { CopyContractAddress } from 'theme'
|
||||
|
||||
export const ContractAddressSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
gap: 4px;
|
||||
padding: 36px 0px;
|
||||
`
|
||||
|
||||
const ContractAddress = styled.button`
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
min-height: 38px;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
export default function AddressSection({ address }: { address: string }) {
|
||||
return (
|
||||
<ContractAddressSection>
|
||||
<Trans>Contract address</Trans>
|
||||
<ContractAddress>
|
||||
<CopyContractAddress address={address} />
|
||||
</ContractAddress>
|
||||
</ContractAddressSection>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { getChainInfoOrDefault } from 'constants/chainInfo'
|
||||
import { formatToDecimal } from 'analytics/utils'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
|
||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
import { useTokenBalance } from 'lib/hooks/useCurrencyBalance'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import NetworkBalance from './NetworkBalance'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
const BalancesCard = styled.div`
|
||||
width: 100%;
|
||||
@@ -33,14 +33,9 @@ const ErrorText = styled.span`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
const NetworkBalancesSection = styled.div`
|
||||
height: fit-content;
|
||||
`
|
||||
|
||||
const TotalBalanceSection = styled.div`
|
||||
height: fit-content;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
`
|
||||
const TotalBalance = styled.div`
|
||||
display: flex;
|
||||
@@ -54,58 +49,35 @@ const TotalBalanceItem = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
export default function BalanceSummary({
|
||||
address,
|
||||
networkBalances,
|
||||
totalBalance,
|
||||
}: {
|
||||
address: string
|
||||
networkBalances: (JSX.Element | null)[] | null
|
||||
totalBalance: number
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
const tokenSymbol = useToken(address)?.symbol
|
||||
const { loading, error, data } = useNetworkTokenBalances({ address })
|
||||
export default function BalanceSummary({ address }: { address: string }) {
|
||||
const token = useToken(address)
|
||||
const { loading, error } = useNetworkTokenBalances({ address })
|
||||
|
||||
const { chainId: connectedChainId } = useWeb3React()
|
||||
const { account } = useWeb3React()
|
||||
const balance = useTokenBalance(account, token ?? undefined)
|
||||
const balanceNumber = balance ? formatToDecimal(balance, Math.min(balance.currency.decimals, 6)) : undefined
|
||||
const balanceUsd = useStablecoinValue(balance)?.toFixed(2)
|
||||
const balanceUsdNumber = balanceUsd ? parseFloat(balanceUsd) : undefined
|
||||
|
||||
const { label: connectedLabel, logoUrl: connectedLogoUrl } = getChainInfoOrDefault(connectedChainId)
|
||||
const connectedFiatValue = 1
|
||||
const multipleBalances = true // for testing purposes
|
||||
|
||||
if (loading) return null
|
||||
if (loading || (!error && !balanceNumber && !balanceUsdNumber)) return null
|
||||
return (
|
||||
<BalancesCard>
|
||||
{error ? (
|
||||
<ErrorState>
|
||||
<AlertTriangle size={24} />
|
||||
<ErrorText>
|
||||
<Trans>There was an error loading your {tokenSymbol} balance</Trans>
|
||||
<Trans>There was an error loading your {token?.symbol} balance</Trans>
|
||||
</ErrorText>
|
||||
</ErrorState>
|
||||
) : multipleBalances ? (
|
||||
<>
|
||||
<TotalBalanceSection>
|
||||
Your balance across all networks
|
||||
<TotalBalance>
|
||||
<TotalBalanceItem>{`${totalBalance} ${tokenSymbol}`}</TotalBalanceItem>
|
||||
<TotalBalanceItem>$4,210.12</TotalBalanceItem>
|
||||
</TotalBalance>
|
||||
</TotalBalanceSection>
|
||||
<NetworkBalancesSection>Your balances by network</NetworkBalancesSection>
|
||||
{data && networkBalances}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Your balance on {connectedLabel}
|
||||
<NetworkBalance
|
||||
logoUrl={connectedLogoUrl}
|
||||
balance={'1'}
|
||||
tokenSymbol={tokenSymbol ?? 'XXX'}
|
||||
fiatValue={connectedFiatValue}
|
||||
label={connectedLabel}
|
||||
networkColor={theme.textPrimary}
|
||||
/>
|
||||
<TotalBalanceSection>
|
||||
Your balance
|
||||
<TotalBalance>
|
||||
<TotalBalanceItem>{`${balanceNumber} ${token?.symbol}`}</TotalBalanceItem>
|
||||
<TotalBalanceItem>{`$${balanceUsdNumber}`}</TotalBalanceItem>
|
||||
</TotalBalance>
|
||||
</TotalBalanceSection>
|
||||
</>
|
||||
)}
|
||||
</BalancesCard>
|
||||
|
||||
18
src/components/Tokens/TokenDetails/BreadcrumbNavLink.tsx
Normal file
18
src/components/Tokens/TokenDetails/BreadcrumbNavLink.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
export const BreadcrumbNavLink = styled(Link)`
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
text-decoration: none;
|
||||
margin-bottom: 16px;
|
||||
transition-duration: ${({ theme }) => theme.transition.duration.fast};
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
}
|
||||
`
|
||||
120
src/components/Tokens/TokenDetails/ChartSection.tsx
Normal file
120
src/components/Tokens/TokenDetails/ChartSection.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { ParentSize } from '@visx/responsive'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
|
||||
import { SingleTokenData } from 'graphql/data/Token'
|
||||
import { useCurrency } from 'hooks/Tokens'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { useIsFavorited, useToggleFavorite } from '../state'
|
||||
import { ClickFavorited, FavoriteIcon } from '../TokenTable/TokenRow'
|
||||
import PriceChart from './PriceChart'
|
||||
import ShareButton from './ShareButton'
|
||||
|
||||
export const ChartHeader = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
gap: 4px;
|
||||
margin-bottom: 24px;
|
||||
`
|
||||
export const TokenInfoContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
export const ChartContainer = styled.div`
|
||||
display: flex;
|
||||
height: 436px;
|
||||
align-items: center;
|
||||
`
|
||||
export const TokenNameCell = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
align-items: center;
|
||||
`
|
||||
const TokenSymbol = styled.span`
|
||||
text-transform: uppercase;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
const TokenActions = styled.div`
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
const NetworkBadge = styled.div<{ networkColor?: string; backgroundColor?: string }>`
|
||||
border-radius: 5px;
|
||||
padding: 4px 8px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
color: ${({ theme, networkColor }) => networkColor ?? theme.textPrimary};
|
||||
background-color: ${({ theme, backgroundColor }) => backgroundColor ?? theme.backgroundSurface};
|
||||
`
|
||||
|
||||
export default function ChartSection({ token, tokenData }: { token: Token; tokenData: SingleTokenData | undefined }) {
|
||||
const { chainId: connectedChainId } = useWeb3React()
|
||||
const isFavorited = useIsFavorited(token.address)
|
||||
const toggleFavorite = useToggleFavorite(token.address)
|
||||
const chainInfo = getChainInfo(token?.chainId)
|
||||
const networkLabel = chainInfo?.label
|
||||
const networkBadgebackgroundColor = chainInfo?.backgroundColor
|
||||
const warning = checkWarning(token.address)
|
||||
|
||||
let currency = useCurrency(token.address)
|
||||
|
||||
if (connectedChainId) {
|
||||
const wrappedNativeCurrency = WRAPPED_NATIVE_CURRENCY[connectedChainId]
|
||||
const isWrappedNativeToken = wrappedNativeCurrency?.address === token?.address
|
||||
if (isWrappedNativeToken) {
|
||||
currency = nativeOnChain(connectedChainId)
|
||||
}
|
||||
}
|
||||
|
||||
const tokenName = tokenData?.name ?? token?.name
|
||||
const tokenSymbol = tokenData?.tokens?.[0]?.symbol ?? token?.symbol
|
||||
|
||||
return (
|
||||
<ChartHeader>
|
||||
<TokenInfoContainer>
|
||||
<TokenNameCell>
|
||||
<CurrencyLogo currency={currency} size={'32px'} symbol={tokenSymbol} />
|
||||
{tokenName ?? <Trans>Name not found</Trans>}
|
||||
<TokenSymbol>{tokenSymbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
|
||||
{!warning && <VerifiedIcon size="20px" />}
|
||||
{networkBadgebackgroundColor && (
|
||||
<NetworkBadge networkColor={chainInfo?.color} backgroundColor={networkBadgebackgroundColor}>
|
||||
{networkLabel}
|
||||
</NetworkBadge>
|
||||
)}
|
||||
</TokenNameCell>
|
||||
<TokenActions>
|
||||
{tokenName && tokenSymbol && (
|
||||
<ShareButton tokenName={tokenName} tokenSymbol={tokenSymbol} tokenAddress={token.address} />
|
||||
)}
|
||||
{useFavoriteTokensFlag() === FavoriteTokensVariant.Enabled && (
|
||||
<ClickFavorited onClick={toggleFavorite}>
|
||||
<FavoriteIcon isFavorited={isFavorited} />
|
||||
</ClickFavorited>
|
||||
)}
|
||||
</TokenActions>
|
||||
</TokenInfoContainer>
|
||||
<ChartContainer>
|
||||
<ParentSize>
|
||||
{({ width, height }) => (
|
||||
<PriceChart tokenAddress={token.address} width={width} height={height} priceData={tokenData?.prices?.[0]} />
|
||||
)}
|
||||
</ParentSize>
|
||||
</ChartContainer>
|
||||
</ChartHeader>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { useToken } from 'hooks/Tokens'
|
||||
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
|
||||
import { useState } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { SMALLEST_MOBILE_MEDIA_BREAKPOINT } from '../constants'
|
||||
@@ -173,9 +174,11 @@ export default function FooterBalanceSummary({
|
||||
)}
|
||||
</BalanceInfo>
|
||||
)}
|
||||
<SwapButton onClick={() => (window.location.href = 'https://app.uniswap.org/#/swap')}>
|
||||
<Trans>Swap</Trans>
|
||||
</SwapButton>
|
||||
<Link to={`/swap?outputCurrency=${address}`}>
|
||||
<SwapButton>
|
||||
<Trans>Swap</Trans>
|
||||
</SwapButton>
|
||||
</Link>
|
||||
</TotalBalancesSection>
|
||||
{showMultipleBalances && (
|
||||
<NetworkBalancesSection>
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import { Footer, LeftPanel, RightPanel, TokenDetailsLayout } from 'pages/TokenDetails'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { LoadingBubble } from '../loading'
|
||||
import { AboutContainer, AboutHeader, ResourcesContainer } from './About'
|
||||
import { ContractAddressSection } from './AddressSection'
|
||||
import { BreadcrumbNavLink } from './BreadcrumbNavLink'
|
||||
import { ChartContainer, ChartHeader, TokenInfoContainer, TokenNameCell } from './ChartSection'
|
||||
import { DeltaContainer, TokenPrice } from './PriceChart'
|
||||
import {
|
||||
AboutContainer,
|
||||
AboutHeader,
|
||||
BreadcrumbNavLink,
|
||||
ChartContainer,
|
||||
ChartHeader,
|
||||
ContractAddressSection,
|
||||
ResourcesContainer,
|
||||
Stat,
|
||||
StatPair,
|
||||
StatsSection,
|
||||
TokenInfoContainer,
|
||||
TokenNameCell,
|
||||
TopArea,
|
||||
} from './TokenDetailContainers'
|
||||
import { StatPair, StatWrapper, TokenStatsSection } from './StatsSection'
|
||||
|
||||
const LoadingChartContainer = styled(ChartContainer)`
|
||||
height: 336px;
|
||||
@@ -90,7 +81,7 @@ export function Wave() {
|
||||
/* Loading State: row component with loading bubbles */
|
||||
export default function LoadingTokenDetail() {
|
||||
return (
|
||||
<TopArea>
|
||||
<LeftPanel>
|
||||
<BreadcrumbNavLink to="/explore">
|
||||
<Space heightSize={20} />
|
||||
</BreadcrumbNavLink>
|
||||
@@ -120,30 +111,30 @@ export default function LoadingTokenDetail() {
|
||||
</LoadingChartContainer>
|
||||
<Space heightSize={32} />
|
||||
</ChartHeader>
|
||||
<StatsSection>
|
||||
<TokenStatsSection>
|
||||
<StatsLoadingContainer>
|
||||
<StatPair>
|
||||
<Stat>
|
||||
<StatWrapper>
|
||||
<HalfLoadingBubble />
|
||||
<StatLoadingBubble />
|
||||
</Stat>
|
||||
<Stat>
|
||||
</StatWrapper>
|
||||
<StatWrapper>
|
||||
<HalfLoadingBubble />
|
||||
<StatLoadingBubble />
|
||||
</Stat>
|
||||
</StatWrapper>
|
||||
</StatPair>
|
||||
<StatPair>
|
||||
<Stat>
|
||||
<StatWrapper>
|
||||
<HalfLoadingBubble />
|
||||
<StatLoadingBubble />
|
||||
</Stat>
|
||||
<Stat>
|
||||
</StatWrapper>
|
||||
<StatWrapper>
|
||||
<HalfLoadingBubble />
|
||||
<StatLoadingBubble />
|
||||
</Stat>
|
||||
</StatWrapper>
|
||||
</StatPair>
|
||||
</StatsLoadingContainer>
|
||||
</StatsSection>
|
||||
</TokenStatsSection>
|
||||
<AboutContainer>
|
||||
<AboutHeader>
|
||||
<SquareLoadingBubble />
|
||||
@@ -155,6 +146,16 @@ export default function LoadingTokenDetail() {
|
||||
<ResourcesContainer>{null}</ResourcesContainer>
|
||||
</AboutContainer>
|
||||
<ContractAddressSection>{null}</ContractAddressSection>
|
||||
</TopArea>
|
||||
</LeftPanel>
|
||||
)
|
||||
}
|
||||
|
||||
export function LoadingTokenDetails() {
|
||||
return (
|
||||
<TokenDetailsLayout>
|
||||
<LoadingTokenDetail />
|
||||
<RightPanel />
|
||||
<Footer />
|
||||
</TokenDetailsLayout>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,33 @@
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { AxisBottom, TickFormatter } from '@visx/axis'
|
||||
import { localPoint } from '@visx/event'
|
||||
import { EventType } from '@visx/event/lib/types'
|
||||
import { GlyphCircle } from '@visx/glyph'
|
||||
import { Line } from '@visx/shape'
|
||||
import { filterTimeAtom } from 'components/Tokens/state'
|
||||
import { bisect, curveCardinal, NumberValue, scaleLinear } from 'd3'
|
||||
import { useTokenPriceQuery } from 'graphql/data/TokenPriceQuery'
|
||||
import { TimePeriod } from 'graphql/data/TopTokenQuery'
|
||||
import {
|
||||
bisect,
|
||||
curveCardinal,
|
||||
NumberValue,
|
||||
scaleLinear,
|
||||
timeDay,
|
||||
timeHour,
|
||||
timeMinute,
|
||||
timeMonth,
|
||||
timeTicks,
|
||||
} from 'd3'
|
||||
import { TokenPrices$key } from 'graphql/data/__generated__/TokenPrices.graphql'
|
||||
import { useTokenPricesCached } from 'graphql/data/Token'
|
||||
import { PricePoint, TimePeriod } from 'graphql/data/Token'
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { ArrowDownRight, ArrowUpRight } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { OPACITY_HOVER } from 'theme'
|
||||
import {
|
||||
dayHourFormatter,
|
||||
hourFormatter,
|
||||
monthDayFormatter,
|
||||
monthFormatter,
|
||||
monthTickFormatter,
|
||||
monthYearDayFormatter,
|
||||
monthYearFormatter,
|
||||
weekFormatter,
|
||||
@@ -29,11 +38,9 @@ import { DISPLAYS, ORDERED_TIMES } from '../TokenTable/TimeSelector'
|
||||
|
||||
// TODO: This should be combined with the logic in TimeSelector.
|
||||
|
||||
export type PricePoint = { value: number; timestamp: number }
|
||||
|
||||
export const DATA_EMPTY = { value: 0, timestamp: 0 }
|
||||
|
||||
function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
|
||||
export function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
|
||||
const prices = pricePoints.map((x) => x.value)
|
||||
const min = Math.min(...prices)
|
||||
const max = Math.max(...prices)
|
||||
@@ -72,7 +79,6 @@ export function formatDelta(delta: number) {
|
||||
export const ChartHeader = styled.div`
|
||||
position: absolute;
|
||||
`
|
||||
|
||||
export const TokenPrice = styled.span`
|
||||
font-size: 36px;
|
||||
line-height: 44px;
|
||||
@@ -112,91 +118,127 @@ const TimeButton = styled.button<{ active: boolean }>`
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: ${({ theme, active }) => (active ? theme.textPrimary : theme.textSecondary)};
|
||||
transition-duration: ${({ theme }) => theme.transition.duration.fast};
|
||||
:hover {
|
||||
${({ active }) => !active && `opacity: ${OPACITY_HOVER};`}
|
||||
${({ active, theme }) => !active && `opacity: ${theme.opacity.hover};`}
|
||||
}
|
||||
`
|
||||
|
||||
function getTicks(startTimestamp: number, endTimestamp: number, numTicks = 5) {
|
||||
return Array.from(
|
||||
{ length: numTicks },
|
||||
(v, i) => endTimestamp - ((endTimestamp - startTimestamp) / (numTicks + 1)) * (i + 1)
|
||||
)
|
||||
}
|
||||
|
||||
function tickFormat(
|
||||
startTimestamp: number,
|
||||
endTimestamp: number,
|
||||
timePeriod: TimePeriod,
|
||||
locale: string
|
||||
): [TickFormatter<NumberValue>, (v: number) => string, number[]] {
|
||||
switch (timePeriod) {
|
||||
case TimePeriod.HOUR:
|
||||
return [hourFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp)]
|
||||
case TimePeriod.DAY:
|
||||
return [hourFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp)]
|
||||
case TimePeriod.WEEK:
|
||||
return [weekFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp, 6)]
|
||||
case TimePeriod.MONTH:
|
||||
return [monthDayFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp)]
|
||||
case TimePeriod.YEAR:
|
||||
return [monthFormatter(locale), monthYearDayFormatter(locale), getTicks(startTimestamp, endTimestamp)]
|
||||
case TimePeriod.ALL:
|
||||
return [monthYearFormatter(locale), monthYearDayFormatter(locale), getTicks(startTimestamp, endTimestamp)]
|
||||
}
|
||||
}
|
||||
|
||||
const margin = { top: 100, bottom: 48, crosshair: 72 }
|
||||
const timeOptionsHeight = 44
|
||||
const crosshairDateOverhang = 80
|
||||
|
||||
interface PriceChartProps {
|
||||
width: number
|
||||
height: number
|
||||
token: Token
|
||||
tokenAddress: string
|
||||
priceData?: TokenPrices$key | null
|
||||
}
|
||||
|
||||
export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
export function PriceChart({ width, height, tokenAddress, priceData }: PriceChartProps) {
|
||||
const [timePeriod, setTimePeriod] = useAtom(filterTimeAtom)
|
||||
const locale = useActiveLocale()
|
||||
const theme = useTheme()
|
||||
|
||||
// TODO: Add network selector input, consider using backend type instead of current front end selector type
|
||||
const pricePoints: PricePoint[] = useTokenPriceQuery(token.address, timePeriod, 'ETHEREUM').filter(
|
||||
(p): p is PricePoint => Boolean(p && p.value)
|
||||
)
|
||||
const { priceMap } = useTokenPricesCached(priceData, tokenAddress, 'ETHEREUM', timePeriod)
|
||||
const prices = priceMap.get(timePeriod)
|
||||
|
||||
const hasData = pricePoints.length !== 0
|
||||
|
||||
/* TODO: Implement API calls & cache to use here */
|
||||
const startingPrice = hasData ? pricePoints[0] : DATA_EMPTY
|
||||
const endingPrice = hasData ? pricePoints[pricePoints.length - 1] : DATA_EMPTY
|
||||
// first price point on the x-axis of the current time period's chart
|
||||
const startingPrice = prices?.[0] ?? DATA_EMPTY
|
||||
// last price point on the x-axis of the current time period's chart
|
||||
const endingPrice = prices?.[prices.length - 1] ?? DATA_EMPTY
|
||||
const [displayPrice, setDisplayPrice] = useState(startingPrice)
|
||||
|
||||
// set display price to ending price when prices have changed.
|
||||
useEffect(() => {
|
||||
if (prices) {
|
||||
setDisplayPrice(endingPrice)
|
||||
}
|
||||
}, [prices, endingPrice])
|
||||
const [crosshair, setCrosshair] = useState<number | null>(null)
|
||||
|
||||
const graphWidth = width + crosshairDateOverhang
|
||||
// TODO: remove this logic after suspense is properly added
|
||||
const graphHeight = height - timeOptionsHeight > 0 ? height - timeOptionsHeight : 0
|
||||
const graphInnerHeight = graphHeight - margin.top - margin.bottom > 0 ? graphHeight - margin.top - margin.bottom : 0
|
||||
|
||||
// Defining scales
|
||||
// x scale
|
||||
const timeScale = scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, width])
|
||||
const timeScale = useMemo(
|
||||
() => scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, width]),
|
||||
[startingPrice, endingPrice, width]
|
||||
)
|
||||
// y scale
|
||||
const rdScale = scaleLinear().domain(getPriceBounds(pricePoints)).range([graphInnerHeight, 0])
|
||||
const rdScale = useMemo(
|
||||
() =>
|
||||
scaleLinear()
|
||||
.domain(getPriceBounds(prices ?? []))
|
||||
.range([graphInnerHeight, 0]),
|
||||
[prices, graphInnerHeight]
|
||||
)
|
||||
|
||||
function tickFormat(
|
||||
startTimestamp: number,
|
||||
endTimestamp: number,
|
||||
timePeriod: TimePeriod,
|
||||
locale: string
|
||||
): [TickFormatter<NumberValue>, (v: number) => string, NumberValue[]] {
|
||||
const offsetTime = (endingPrice.timestamp.valueOf() - startingPrice.timestamp.valueOf()) / 24
|
||||
const startDateWithOffset = new Date((startingPrice.timestamp.valueOf() + offsetTime) * 1000)
|
||||
const endDateWithOffset = new Date((endingPrice.timestamp.valueOf() - offsetTime) * 1000)
|
||||
switch (timePeriod) {
|
||||
case TimePeriod.HOUR:
|
||||
return [
|
||||
hourFormatter(locale),
|
||||
dayHourFormatter(locale),
|
||||
(timeMinute.every(5) ?? timeMinute)
|
||||
.range(startDateWithOffset, endDateWithOffset, 2)
|
||||
.map((x) => x.valueOf() / 1000),
|
||||
]
|
||||
case TimePeriod.DAY:
|
||||
return [
|
||||
hourFormatter(locale),
|
||||
dayHourFormatter(locale),
|
||||
timeHour.range(startDateWithOffset, endDateWithOffset, 4).map((x) => x.valueOf() / 1000),
|
||||
]
|
||||
case TimePeriod.WEEK:
|
||||
return [
|
||||
weekFormatter(locale),
|
||||
dayHourFormatter(locale),
|
||||
timeDay.range(startDateWithOffset, endDateWithOffset, 1).map((x) => x.valueOf() / 1000),
|
||||
]
|
||||
case TimePeriod.MONTH:
|
||||
return [
|
||||
monthDayFormatter(locale),
|
||||
dayHourFormatter(locale),
|
||||
timeDay.range(startDateWithOffset, endDateWithOffset, 7).map((x) => x.valueOf() / 1000),
|
||||
]
|
||||
case TimePeriod.YEAR:
|
||||
return [
|
||||
monthTickFormatter(locale),
|
||||
monthYearDayFormatter(locale),
|
||||
timeMonth.range(startDateWithOffset, endDateWithOffset, 2).map((x) => x.valueOf() / 1000),
|
||||
]
|
||||
case TimePeriod.ALL:
|
||||
return [
|
||||
monthYearFormatter(locale),
|
||||
monthYearDayFormatter(locale),
|
||||
timeTicks(startDateWithOffset, endDateWithOffset, 6).map((x) => x.valueOf() / 1000),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const handleHover = useCallback(
|
||||
(event: Element | EventType) => {
|
||||
if (!prices) return
|
||||
|
||||
const { x } = localPoint(event) || { x: 0 }
|
||||
const x0 = timeScale.invert(x) // get timestamp from the scalexw
|
||||
const index = bisect(
|
||||
pricePoints.map((x) => x.timestamp),
|
||||
prices.map((x) => x.timestamp),
|
||||
x0,
|
||||
1
|
||||
)
|
||||
|
||||
const d0 = pricePoints[index - 1]
|
||||
const d1 = pricePoints[index]
|
||||
const d0 = prices[index - 1]
|
||||
const d1 = prices[index]
|
||||
let pricePoint = d0
|
||||
|
||||
const hasPreviousData = d1 && d1.timestamp
|
||||
@@ -207,7 +249,7 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
setCrosshair(timeScale(pricePoint.timestamp))
|
||||
setDisplayPrice(pricePoint)
|
||||
},
|
||||
[timeScale, pricePoints]
|
||||
[timeScale, prices]
|
||||
)
|
||||
|
||||
const resetDisplay = useCallback(() => {
|
||||
@@ -215,8 +257,8 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
setDisplayPrice(endingPrice)
|
||||
}, [setCrosshair, setDisplayPrice, endingPrice])
|
||||
|
||||
// TODO: connect to loading state
|
||||
if (!hasData) {
|
||||
// TODO: Display no data available error
|
||||
if (!prices) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -232,8 +274,8 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
const crosshairEdgeMax = width * 0.85
|
||||
const crosshairAtEdge = !!crosshair && crosshair > crosshairEdgeMax
|
||||
|
||||
/* Default curve doesn't look good for the ALL chart */
|
||||
const curveTension = timePeriod === TimePeriod.ALL ? 0.75 : 0.9
|
||||
/* Default curve doesn't look good for the HOUR/ALL chart */
|
||||
const curveTension = timePeriod === TimePeriod.ALL ? 0.75 : timePeriod === TimePeriod.HOUR ? 1 : 0.9
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -245,13 +287,13 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
</DeltaContainer>
|
||||
</ChartHeader>
|
||||
<LineChart
|
||||
data={pricePoints}
|
||||
data={prices}
|
||||
getX={(p: PricePoint) => timeScale(p.timestamp)}
|
||||
getY={(p: PricePoint) => rdScale(p.value)}
|
||||
marginTop={margin.top}
|
||||
curve={curveCardinal.tension(curveTension)}
|
||||
strokeWidth={2}
|
||||
width={graphWidth}
|
||||
width={width}
|
||||
height={graphHeight}
|
||||
>
|
||||
{crosshair !== null ? (
|
||||
@@ -262,6 +304,7 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
tickFormat={tickFormatter}
|
||||
tickStroke={theme.backgroundOutline}
|
||||
tickLength={4}
|
||||
hideTicks={true}
|
||||
tickTransform={'translate(0 -5)'}
|
||||
tickValues={ticks}
|
||||
top={graphHeight - 1}
|
||||
@@ -316,7 +359,13 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
<TimeOptionsWrapper>
|
||||
<TimeOptionsContainer>
|
||||
{ORDERED_TIMES.map((time) => (
|
||||
<TimeButton key={DISPLAYS[time]} active={timePeriod === time} onClick={() => setTimePeriod(time)}>
|
||||
<TimeButton
|
||||
key={DISPLAYS[time]}
|
||||
active={timePeriod === time}
|
||||
onClick={() => {
|
||||
setTimePeriod(time)
|
||||
}}
|
||||
>
|
||||
{DISPLAYS[time]}
|
||||
</TimeButton>
|
||||
))}
|
||||
|
||||
@@ -5,9 +5,10 @@ import { Twitter } from 'react-feather'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ClickableStyle, CopyHelperRefType, OPACITY_CLICK, Z_INDEX } from 'theme'
|
||||
import { ClickableStyle, CopyHelperRefType } from 'theme'
|
||||
import { colors } from 'theme/colors'
|
||||
import { opacify } from 'theme/utils'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import { ReactComponent as ShareIcon } from '../../../assets/svg/share.svg'
|
||||
import { CopyHelper } from '../../../theme'
|
||||
@@ -25,7 +26,7 @@ const Share = styled(ShareIcon)<{ open: boolean }>`
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
${ClickableStyle}
|
||||
${({ open }) => open && `opacity: ${OPACITY_CLICK} !important`};
|
||||
${({ open, theme }) => open && `opacity: ${theme.opacity.click} !important`};
|
||||
`
|
||||
|
||||
const ShareActions = styled.div`
|
||||
|
||||
67
src/components/Tokens/TokenDetails/StatsSection.tsx
Normal file
67
src/components/Tokens/TokenDetails/StatsSection.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { ReactNode } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { formatDollarAmount } from 'utils/formatDollarAmt'
|
||||
|
||||
export const StatWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 14px;
|
||||
min-width: 168px;
|
||||
flex: 1;
|
||||
gap: 4px;
|
||||
padding: 24px 0px;
|
||||
`
|
||||
export const TokenStatsSection = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
export const StatPair = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
const StatPrice = styled.span`
|
||||
font-size: 28px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
`
|
||||
const NoData = styled.div`
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
`
|
||||
|
||||
type NumericStat = number | undefined | null
|
||||
|
||||
function Stat({ value, title }: { value: NumericStat; title: ReactNode }) {
|
||||
return (
|
||||
<StatWrapper>
|
||||
{title}
|
||||
<StatPrice>{value ? formatDollarAmount(value) : '-'}</StatPrice>
|
||||
</StatWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
type StatsSectionProps = {
|
||||
marketCap?: NumericStat
|
||||
volume24H?: NumericStat
|
||||
priceLow52W?: NumericStat
|
||||
priceHigh52W?: NumericStat
|
||||
}
|
||||
export default function StatsSection({ marketCap, volume24H, priceLow52W, priceHigh52W }: StatsSectionProps) {
|
||||
if (marketCap || volume24H || priceLow52W || priceHigh52W) {
|
||||
return (
|
||||
<TokenStatsSection>
|
||||
<StatPair>
|
||||
<Stat value={marketCap} title={<Trans>Market Cap</Trans>} />
|
||||
<Stat value={volume24H} title={<Trans>24H volume</Trans>} />
|
||||
</StatPair>
|
||||
<StatPair>
|
||||
<Stat value={priceLow52W} title={<Trans>52W low</Trans>} />
|
||||
<Stat value={priceHigh52W} title={<Trans>52W high</Trans>} />
|
||||
</StatPair>
|
||||
</TokenStatsSection>
|
||||
)
|
||||
} else {
|
||||
return <NoData>No stats available</NoData>
|
||||
}
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { ParentSize } from '@visx/responsive'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import PriceChart from 'components/Tokens/TokenDetails/PriceChart'
|
||||
import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
|
||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
|
||||
import { checkWarning, WARNING_LEVEL } from 'constants/tokenSafety'
|
||||
import { chainIdToChainName, useTokenDetailQuery } from 'graphql/data/TokenDetailQuery'
|
||||
import { useCurrency, useIsUserAddedToken, useToken } from 'hooks/Tokens'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { darken } from 'polished'
|
||||
import { Suspense, useCallback } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { ArrowLeft, Heart } from 'react-feather'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ClickableStyle, CopyContractAddress } from 'theme'
|
||||
import { formatDollarAmount } from 'utils/formatDollarAmt'
|
||||
|
||||
import { favoritesAtom, filterNetworkAtom, useToggleFavorite } from '../state'
|
||||
import { ClickFavorited } from '../TokenTable/TokenRow'
|
||||
import LoadingTokenDetail from './LoadingTokenDetail'
|
||||
import Resource from './Resource'
|
||||
import ShareButton from './ShareButton'
|
||||
import {
|
||||
AboutContainer,
|
||||
AboutHeader,
|
||||
BreadcrumbNavLink,
|
||||
ChartContainer,
|
||||
ChartHeader,
|
||||
ContractAddressSection,
|
||||
ResourcesContainer,
|
||||
Stat,
|
||||
StatPair,
|
||||
StatsSection,
|
||||
TokenInfoContainer,
|
||||
TokenNameCell,
|
||||
TopArea,
|
||||
} from './TokenDetailContainers'
|
||||
|
||||
const ContractAddress = styled.button`
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
min-height: 38px;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
`
|
||||
const Contract = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 14px;
|
||||
gap: 4px;
|
||||
`
|
||||
const StatPrice = styled.span`
|
||||
font-size: 28px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
`
|
||||
const TokenActions = styled.div`
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
const TokenSymbol = styled.span`
|
||||
text-transform: uppercase;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
const NetworkBadge = styled.div<{ networkColor?: string; backgroundColor?: string }>`
|
||||
border-radius: 5px;
|
||||
padding: 4px 8px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
color: ${({ theme, networkColor }) => networkColor ?? theme.textPrimary};
|
||||
background-color: ${({ theme, backgroundColor }) => backgroundColor ?? theme.backgroundSurface};
|
||||
`
|
||||
const FavoriteIcon = styled(Heart)<{ isFavorited: boolean }>`
|
||||
${ClickableStyle}
|
||||
height: 22px;
|
||||
width: 24px;
|
||||
color: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : theme.textSecondary)};
|
||||
fill: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : 'transparent')};
|
||||
`
|
||||
const NoInfoAvailable = styled.span`
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
`
|
||||
const TokenDescriptionContainer = styled.div`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
max-height: fit-content;
|
||||
padding-top: 16px;
|
||||
line-height: 24px;
|
||||
white-space: pre-wrap;
|
||||
`
|
||||
const TruncateDescriptionButton = styled.div`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
padding-top: 14px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: ${({ theme }) => darken(0.1, theme.textSecondary)};
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
const TRUNCATE_CHARACTER_COUNT = 400
|
||||
|
||||
type TokenDetailData = {
|
||||
description: string | null | undefined
|
||||
homepageUrl: string | null | undefined
|
||||
twitterName: string | null | undefined
|
||||
}
|
||||
|
||||
const truncateDescription = (desc: string) => {
|
||||
//trim the string to the maximum length
|
||||
let tokenDescriptionTruncated = desc.slice(0, TRUNCATE_CHARACTER_COUNT)
|
||||
//re-trim if we are in the middle of a word
|
||||
tokenDescriptionTruncated = `${tokenDescriptionTruncated.slice(
|
||||
0,
|
||||
Math.min(tokenDescriptionTruncated.length, tokenDescriptionTruncated.lastIndexOf(' '))
|
||||
)}...`
|
||||
return tokenDescriptionTruncated
|
||||
}
|
||||
|
||||
export function AboutSection({ address, tokenDetailData }: { address: string; tokenDetailData: TokenDetailData }) {
|
||||
const [isDescriptionTruncated, setIsDescriptionTruncated] = useState(true)
|
||||
|
||||
const shouldTruncate =
|
||||
tokenDetailData && tokenDetailData.description
|
||||
? tokenDetailData.description.length > TRUNCATE_CHARACTER_COUNT
|
||||
: false
|
||||
|
||||
const tokenDescription =
|
||||
tokenDetailData && tokenDetailData.description && shouldTruncate && isDescriptionTruncated
|
||||
? truncateDescription(tokenDetailData.description)
|
||||
: tokenDetailData.description
|
||||
|
||||
return (
|
||||
<AboutContainer>
|
||||
<AboutHeader>
|
||||
<Trans>About</Trans>
|
||||
</AboutHeader>
|
||||
<TokenDescriptionContainer>
|
||||
{(!tokenDetailData || !tokenDetailData.description) && (
|
||||
<NoInfoAvailable>
|
||||
<Trans>No token information available</Trans>
|
||||
</NoInfoAvailable>
|
||||
)}
|
||||
{tokenDescription}
|
||||
{shouldTruncate && (
|
||||
<TruncateDescriptionButton onClick={() => setIsDescriptionTruncated(!isDescriptionTruncated)}>
|
||||
{isDescriptionTruncated ? <Trans>Read more</Trans> : <Trans>Hide</Trans>}
|
||||
</TruncateDescriptionButton>
|
||||
)}
|
||||
</TokenDescriptionContainer>
|
||||
<ResourcesContainer>
|
||||
<Resource name={'Etherscan'} link={`https://etherscan.io/address/${address}`} />
|
||||
<Resource name={'Protocol info'} link={`https://info.uniswap.org/#/tokens/${address}`} />
|
||||
{tokenDetailData?.homepageUrl && <Resource name={'Website'} link={tokenDetailData.homepageUrl} />}
|
||||
{tokenDetailData?.twitterName && (
|
||||
<Resource name={'Twitter'} link={`https://twitter.com/${tokenDetailData.twitterName}`} />
|
||||
)}
|
||||
</ResourcesContainer>
|
||||
</AboutContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default function LoadedTokenDetail({ address }: { address: string }) {
|
||||
const { chainId: connectedChainId } = useWeb3React()
|
||||
const token = useToken(address)
|
||||
let currency = useCurrency(address)
|
||||
const favoriteTokens = useAtomValue<string[]>(favoritesAtom)
|
||||
const isFavorited = favoriteTokens.includes(address)
|
||||
const toggleFavorite = useToggleFavorite(address)
|
||||
const warning = checkWarning(address)
|
||||
const navigate = useNavigate()
|
||||
const isUserAddedToken = useIsUserAddedToken(token)
|
||||
const [warningModalOpen, setWarningModalOpen] = useState(!!warning && !isUserAddedToken)
|
||||
|
||||
const handleDismissWarning = useCallback(() => {
|
||||
setWarningModalOpen(false)
|
||||
}, [setWarningModalOpen])
|
||||
const handleCancel = useCallback(() => {
|
||||
setWarningModalOpen(false)
|
||||
warning && warning.level === WARNING_LEVEL.BLOCKED && navigate(-1)
|
||||
}, [setWarningModalOpen, navigate, warning])
|
||||
const chainInfo = getChainInfo(token?.chainId)
|
||||
const networkLabel = chainInfo?.label
|
||||
const networkBadgebackgroundColor = chainInfo?.backgroundColor
|
||||
const filterNetwork = useAtomValue(filterNetworkAtom)
|
||||
const tokenDetailData = useTokenDetailQuery(address, chainIdToChainName(filterNetwork))
|
||||
const relevantTokenDetailData = (({ description, homepageUrl, twitterName }) => ({
|
||||
description,
|
||||
homepageUrl,
|
||||
twitterName,
|
||||
}))(tokenDetailData)
|
||||
|
||||
if (!token || !token.name || !token.symbol || !connectedChainId) {
|
||||
return <LoadingTokenDetail />
|
||||
}
|
||||
|
||||
const wrappedNativeCurrency = WRAPPED_NATIVE_CURRENCY[connectedChainId]
|
||||
const isWrappedNativeToken = wrappedNativeCurrency?.address === token.address
|
||||
|
||||
if (isWrappedNativeToken) {
|
||||
currency = nativeOnChain(connectedChainId)
|
||||
}
|
||||
|
||||
const tokenName = isWrappedNativeToken && currency ? currency.name : tokenDetailData.name
|
||||
const defaultTokenSymbol = tokenDetailData.tokens?.[0]?.symbol ?? token.symbol
|
||||
const tokenSymbol = isWrappedNativeToken && currency ? currency.symbol : defaultTokenSymbol
|
||||
|
||||
return (
|
||||
<Suspense fallback={<LoadingTokenDetail />}>
|
||||
<TopArea>
|
||||
<BreadcrumbNavLink to="/tokens">
|
||||
<ArrowLeft size={14} /> Tokens
|
||||
</BreadcrumbNavLink>
|
||||
<ChartHeader>
|
||||
<TokenInfoContainer>
|
||||
<TokenNameCell>
|
||||
<CurrencyLogo currency={currency} size={'32px'} />
|
||||
{tokenName ?? <Trans>Name not found</Trans>}
|
||||
<TokenSymbol>{tokenSymbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
|
||||
{!warning && <VerifiedIcon size="20px" />}
|
||||
{networkBadgebackgroundColor && (
|
||||
<NetworkBadge networkColor={chainInfo?.color} backgroundColor={networkBadgebackgroundColor}>
|
||||
{networkLabel}
|
||||
</NetworkBadge>
|
||||
)}
|
||||
</TokenNameCell>
|
||||
<TokenActions>
|
||||
{tokenName && tokenSymbol && (
|
||||
<ShareButton tokenName={tokenName} tokenSymbol={tokenSymbol} tokenAddress={address} />
|
||||
)}
|
||||
<ClickFavorited onClick={toggleFavorite}>
|
||||
<FavoriteIcon isFavorited={isFavorited} />
|
||||
</ClickFavorited>
|
||||
</TokenActions>
|
||||
</TokenInfoContainer>
|
||||
<ChartContainer>
|
||||
<ParentSize>{({ width, height }) => <PriceChart token={token} width={width} height={height} />}</ParentSize>
|
||||
</ChartContainer>
|
||||
</ChartHeader>
|
||||
<StatsSection>
|
||||
<StatPair>
|
||||
<Stat>
|
||||
<Trans>Market cap</Trans>
|
||||
<StatPrice>
|
||||
{tokenDetailData.marketCap?.value ? formatDollarAmount(tokenDetailData.marketCap?.value) : '-'}
|
||||
</StatPrice>
|
||||
</Stat>
|
||||
<Stat>
|
||||
24H volume
|
||||
<StatPrice>
|
||||
{tokenDetailData.volume24h?.value ? formatDollarAmount(tokenDetailData.volume24h?.value) : '-'}
|
||||
</StatPrice>
|
||||
</Stat>
|
||||
</StatPair>
|
||||
<StatPair>
|
||||
<Stat>
|
||||
52W low
|
||||
<StatPrice>
|
||||
{tokenDetailData.priceLow52W?.value ? formatDollarAmount(tokenDetailData.priceLow52W?.value) : '-'}
|
||||
</StatPrice>
|
||||
</Stat>
|
||||
<Stat>
|
||||
52W high
|
||||
<StatPrice>
|
||||
{tokenDetailData.priceHigh52W?.value ? formatDollarAmount(tokenDetailData.priceHigh52W?.value) : '-'}
|
||||
</StatPrice>
|
||||
</Stat>
|
||||
</StatPair>
|
||||
</StatsSection>
|
||||
<AboutSection address={address} tokenDetailData={relevantTokenDetailData} />
|
||||
<ContractAddressSection>
|
||||
<Contract>
|
||||
<Trans>Contract address</Trans>
|
||||
<ContractAddress>
|
||||
<CopyContractAddress address={address} />
|
||||
</ContractAddress>
|
||||
</Contract>
|
||||
</ContractAddressSection>
|
||||
<TokenSafetyModal
|
||||
isOpen={warningModalOpen}
|
||||
tokenAddress={address}
|
||||
onCancel={handleCancel}
|
||||
onContinue={handleDismissWarning}
|
||||
/>
|
||||
</TopArea>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
export const AboutContainer = styled.div`
|
||||
gap: 16px;
|
||||
padding: 24px 0px;
|
||||
`
|
||||
export const AboutHeader = styled.span`
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
`
|
||||
export const BreadcrumbNavLink = styled(Link)`
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
text-decoration: none;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
}
|
||||
`
|
||||
export const ChartHeader = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
gap: 4px;
|
||||
margin-bottom: 24px;
|
||||
`
|
||||
export const ContractAddressSection = styled.div`
|
||||
padding: 36px 0px;
|
||||
`
|
||||
export const ChartContainer = styled.div`
|
||||
display: flex;
|
||||
height: 436px;
|
||||
align-items: center;
|
||||
`
|
||||
export const Stat = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 14px;
|
||||
min-width: 168px;
|
||||
flex: 1;
|
||||
gap: 4px;
|
||||
padding: 24px 0px;
|
||||
`
|
||||
export const StatsSection = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
export const StatPair = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
export const TokenNameCell = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
align-items: center;
|
||||
`
|
||||
export const TokenInfoContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
export const TopArea = styled.div`
|
||||
max-width: 832px;
|
||||
overflow: hidden;
|
||||
`
|
||||
export const ResourcesContainer = styled.div`
|
||||
display: flex;
|
||||
padding-top: 12px;
|
||||
gap: 14px;
|
||||
`
|
||||
@@ -5,6 +5,7 @@ import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { SMALLEST_MOBILE_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { showFavoritesAtom } from '../state'
|
||||
import FilterOption from './FilterOption'
|
||||
|
||||
const FavoriteButtonContent = styled.div`
|
||||
display: flex;
|
||||
@@ -12,20 +13,6 @@ const FavoriteButtonContent = styled.div`
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
const StyledFavoriteButton = styled.button<{ active: boolean }>`
|
||||
padding: 0px 16px;
|
||||
border-radius: 12px;
|
||||
background-color: ${({ theme, active }) => (active ? theme.accentAction : theme.backgroundInteractive)};
|
||||
border: none;
|
||||
color: ${({ theme, active }) => (active ? theme.white : theme.textPrimary)};
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
|
||||
:hover {
|
||||
background-color: ${({ theme, active }) => !active && theme.backgroundModule};
|
||||
}
|
||||
`
|
||||
const FavoriteText = styled.span`
|
||||
@media only screen and (max-width: ${SMALLEST_MOBILE_MEDIA_BREAKPOINT}) {
|
||||
display: none;
|
||||
@@ -36,13 +23,13 @@ export default function FavoriteButton() {
|
||||
const theme = useTheme()
|
||||
const [showFavorites, setShowFavorites] = useAtom(showFavoritesAtom)
|
||||
return (
|
||||
<StyledFavoriteButton onClick={() => setShowFavorites(!showFavorites)} active={showFavorites}>
|
||||
<FilterOption onClick={() => setShowFavorites(!showFavorites)} active={showFavorites} highlight>
|
||||
<FavoriteButtonContent>
|
||||
<Heart size={17} color={showFavorites ? theme.white : theme.textPrimary} fill="transparent" />
|
||||
<Heart size={20} color={showFavorites ? theme.accentActive : theme.textPrimary} />
|
||||
<FavoriteText>
|
||||
<Trans>Favorites</Trans>
|
||||
</FavoriteText>
|
||||
</FavoriteButtonContent>
|
||||
</StyledFavoriteButton>
|
||||
</FilterOption>
|
||||
)
|
||||
}
|
||||
|
||||
26
src/components/Tokens/TokenTable/FilterOption.tsx
Normal file
26
src/components/Tokens/TokenTable/FilterOption.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
//import { ReactNode } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
const FilterOption = styled.button<{ active: boolean; highlight?: boolean }>`
|
||||
height: 100%;
|
||||
color: ${({ theme, active }) => (active ? theme.accentActive : theme.textPrimary)};
|
||||
background-color: ${({ theme, active }) => (active ? theme.accentActiveSoft : theme.backgroundInteractive)};
|
||||
margin: 0;
|
||||
padding: 6px 12px 6px 14px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 600;
|
||||
transition-duration: ${({ theme }) => theme.transition.duration.fast};
|
||||
border: none;
|
||||
outline: ${({ theme, active, highlight }) => (active && highlight ? `1px solid ${theme.accentAction}` : 'none')};
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
background-color: ${({ theme, active }) => (active ? theme.accentActiveSoft : theme.backgroundModule)};
|
||||
opacity: ${({ theme, active }) => (active ? theme.opacity.hover : 1)};
|
||||
}
|
||||
:focus {
|
||||
background-color: ${({ theme, active }) => (active ? theme.accentActiveSoft : theme.backgroundInteractive)};
|
||||
}
|
||||
`
|
||||
export default FilterOption
|
||||
@@ -10,6 +10,7 @@ import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { MEDIUM_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { filterNetworkAtom } from '../state'
|
||||
import FilterOption from './FilterOption'
|
||||
|
||||
const NETWORKS = [
|
||||
SupportedChainId.MAINNET,
|
||||
@@ -28,7 +29,6 @@ const InternalMenuItem = styled.div`
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
const InternalLinkMenuItem = styled(InternalMenuItem)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -59,36 +59,6 @@ const MenuTimeFlyout = styled.span`
|
||||
z-index: 100;
|
||||
left: 0px;
|
||||
`
|
||||
|
||||
const StyledMenuButton = styled.button<{ open: boolean }>`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: ${({ theme, open }) => (open ? theme.blue200 : theme.textPrimary)};
|
||||
border: none;
|
||||
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundInteractive)};
|
||||
margin: 0;
|
||||
padding: 6px 12px 6px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundModule)};
|
||||
}
|
||||
:focus {
|
||||
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundInteractive)};
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -96,26 +66,24 @@ const StyledMenu = styled.div`
|
||||
position: relative;
|
||||
border: none;
|
||||
text-align: left;
|
||||
width: 160px;
|
||||
|
||||
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
||||
flex: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenuContent = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
border: none;
|
||||
width: 100%;
|
||||
font-weight: 600;
|
||||
vertical-align: middle;
|
||||
`
|
||||
|
||||
const Chevron = styled.span<{ open: boolean }>`
|
||||
padding-top: 1px;
|
||||
color: ${({ open, theme }) => (open ? theme.blue200 : theme.textSecondary)};
|
||||
color: ${({ open, theme }) => (open ? theme.accentActive : theme.textSecondary)};
|
||||
`
|
||||
const NetworkLabel = styled.div`
|
||||
display: flex;
|
||||
@@ -143,16 +111,20 @@ export default function NetworkFilter() {
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<StyledMenuButton onClick={toggleMenu} aria-label={`networkFilter`} open={open}>
|
||||
<FilterOption onClick={toggleMenu} aria-label={`networkFilter`} active={open}>
|
||||
<StyledMenuContent>
|
||||
<NetworkLabel>
|
||||
<Logo src={circleLogoUrl ?? logoUrl} /> {label}
|
||||
</NetworkLabel>
|
||||
<Chevron open={open}>
|
||||
{open ? <ChevronUp size={15} viewBox="0 0 24 20" /> : <ChevronDown size={15} viewBox="0 0 24 20" />}
|
||||
{open ? (
|
||||
<ChevronUp width={20} height={15} viewBox="0 0 24 20" />
|
||||
) : (
|
||||
<ChevronDown width={20} height={15} viewBox="0 0 24 20" />
|
||||
)}
|
||||
</Chevron>
|
||||
</StyledMenuContent>
|
||||
</StyledMenuButton>
|
||||
</FilterOption>
|
||||
{open && (
|
||||
<MenuTimeFlyout>
|
||||
{NETWORKS.map((network) => (
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import searchIcon from 'assets/svg/search.svg'
|
||||
import xIcon from 'assets/svg/x.svg'
|
||||
import { useAtom } from 'jotai'
|
||||
import useDebounce from 'hooks/useDebounce'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { useEffect, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { MEDIUM_MEDIA_BREAKPOINT } from '../constants'
|
||||
@@ -25,6 +27,7 @@ const SearchInput = styled.input`
|
||||
font-size: 16px;
|
||||
padding-left: 40px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
transition-duration: ${({ theme }) => theme.transition.duration.fast};
|
||||
|
||||
:hover {
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
@@ -56,7 +59,14 @@ const SearchInput = styled.input`
|
||||
`
|
||||
|
||||
export default function SearchBar() {
|
||||
const [filterString, setFilterString] = useAtom(filterStringAtom)
|
||||
const [localFilterString, setLocalFilterString] = useState('')
|
||||
const setFilterString = useUpdateAtom(filterStringAtom)
|
||||
const debouncedLocalFilterString = useDebounce(localFilterString, 300)
|
||||
|
||||
useEffect(() => {
|
||||
setFilterString(debouncedLocalFilterString)
|
||||
}, [debouncedLocalFilterString, setFilterString])
|
||||
|
||||
return (
|
||||
<SearchBarContainer>
|
||||
<Trans
|
||||
@@ -66,8 +76,8 @@ export default function SearchBar() {
|
||||
placeholder={`${translation}`}
|
||||
id="searchBar"
|
||||
autoComplete="off"
|
||||
value={filterString}
|
||||
onChange={({ target: { value } }) => setFilterString(value)}
|
||||
value={localFilterString}
|
||||
onChange={({ target: { value } }) => setLocalFilterString(value)}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TimePeriod } from 'graphql/data/TopTokenQuery'
|
||||
import { TimePeriod } from 'graphql/data/Token'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useRef } from 'react'
|
||||
@@ -9,6 +9,7 @@ import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { MOBILE_MEDIA_BREAKPOINT, SMALL_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { filterTimeAtom } from '../state'
|
||||
import FilterOption from './FilterOption'
|
||||
|
||||
export const DISPLAYS: Record<TimePeriod, string> = {
|
||||
[TimePeriod.HOUR]: '1H',
|
||||
@@ -39,7 +40,6 @@ const InternalMenuItem = styled.div`
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
const InternalLinkMenuItem = styled(InternalMenuItem)`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -76,36 +76,6 @@ const MenuTimeFlyout = styled.span`
|
||||
left: unset;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenuButton = styled.button<{ open: boolean }>`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
color: ${({ theme, open }) => (open ? theme.blue200 : theme.textPrimary)};
|
||||
margin: 0;
|
||||
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundInteractive)};
|
||||
padding: 6px 12px 6px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 600;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundModule)};
|
||||
}
|
||||
:focus {
|
||||
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundInteractive)};
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -113,25 +83,23 @@ const StyledMenu = styled.div`
|
||||
position: relative;
|
||||
border: none;
|
||||
text-align: left;
|
||||
width: 80px;
|
||||
|
||||
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
||||
width: 72px;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenuContent = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
border: none;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
`
|
||||
|
||||
const Chevron = styled.span<{ open: boolean }>`
|
||||
padding-top: 1px;
|
||||
color: ${({ open, theme }) => (open ? theme.blue200 : theme.textSecondary)};
|
||||
color: ${({ open, theme }) => (open ? theme.accentActive : theme.textSecondary)};
|
||||
`
|
||||
|
||||
// TODO: change this to reflect data pipeline
|
||||
@@ -145,14 +113,18 @@ export default function TimeSelector() {
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<StyledMenuButton onClick={toggleMenu} aria-label={`timeSelector`} open={open}>
|
||||
<FilterOption onClick={toggleMenu} aria-label={`timeSelector`} active={open}>
|
||||
<StyledMenuContent>
|
||||
{DISPLAYS[activeTime]}
|
||||
<Chevron open={open}>
|
||||
{open ? <ChevronUp size={15} viewBox="0 0 24 20" /> : <ChevronDown size={15} viewBox="0 0 24 20" />}
|
||||
{open ? (
|
||||
<ChevronUp width={20} height={15} viewBox="0 0 24 20" />
|
||||
) : (
|
||||
<ChevronDown width={20} height={15} viewBox="0 0 24 20" />
|
||||
)}
|
||||
</Chevron>
|
||||
</StyledMenuContent>
|
||||
</StyledMenuButton>
|
||||
</FilterOption>
|
||||
{open && (
|
||||
<MenuTimeFlyout>
|
||||
{ORDERED_TIMES.map((time) => (
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { ParentSize } from '@visx/responsive'
|
||||
import { sendAnalyticsEvent } from 'components/AmplitudeAnalytics'
|
||||
import { EventName } from 'components/AmplitudeAnalytics/constants'
|
||||
import { sendAnalyticsEvent } from 'analytics'
|
||||
import { EventName } from 'analytics/constants'
|
||||
import SparklineChart from 'components/Charts/SparklineChart'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { TimePeriod, TokenData } from 'graphql/data/TopTokenQuery'
|
||||
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
|
||||
import { getDurationDetails, SingleTokenData, TimePeriod } from 'graphql/data/Token'
|
||||
import { useCurrency } from 'hooks/Tokens'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { ReactNode } from 'react'
|
||||
import { ArrowDown, ArrowUp, Heart } from 'react-feather'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled, { css, useTheme } from 'styled-components/macro'
|
||||
import { ClickableStyle } from 'theme'
|
||||
import { formatDollarAmount } from 'utils/formatDollarAmt'
|
||||
|
||||
import {
|
||||
@@ -23,12 +24,12 @@ import {
|
||||
} from '../constants'
|
||||
import { LoadingBubble } from '../loading'
|
||||
import {
|
||||
favoritesAtom,
|
||||
filterNetworkAtom,
|
||||
filterStringAtom,
|
||||
filterTimeAtom,
|
||||
sortCategoryAtom,
|
||||
sortDirectionAtom,
|
||||
useIsFavorited,
|
||||
useSetSortCategory,
|
||||
useToggleFavorite,
|
||||
} from '../state'
|
||||
@@ -41,11 +42,17 @@ const Cell = styled.div`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
const StyledTokenRow = styled.div<{ first?: boolean; last?: boolean; loading?: boolean }>`
|
||||
const StyledTokenRow = styled.div<{
|
||||
first?: boolean
|
||||
last?: boolean
|
||||
loading?: boolean
|
||||
favoriteTokensEnabled?: boolean
|
||||
}>`
|
||||
background-color: transparent;
|
||||
display: grid;
|
||||
font-size: 15px;
|
||||
grid-template-columns: 1fr 7fr 4fr 4fr 4fr 4fr 5fr 1.2fr;
|
||||
grid-template-columns: ${({ favoriteTokensEnabled }) =>
|
||||
favoriteTokensEnabled ? '1fr 7fr 4fr 4fr 4fr 4fr 5fr 1.2fr' : '1fr 7fr 4fr 4fr 4fr 4fr 5fr'};
|
||||
height: 60px;
|
||||
line-height: 24px;
|
||||
max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT};
|
||||
@@ -60,6 +67,7 @@ const StyledTokenRow = styled.div<{ first?: boolean; last?: boolean; loading?: b
|
||||
},
|
||||
}) => css`background-color ${duration.medium} ${timing.ease}`};
|
||||
width: 100%;
|
||||
transition-duration: ${({ theme }) => theme.transition.duration.fast};
|
||||
|
||||
&:hover {
|
||||
${({ loading, theme }) =>
|
||||
@@ -109,6 +117,14 @@ export const ClickFavorited = styled.span`
|
||||
}
|
||||
`
|
||||
|
||||
export const FavoriteIcon = styled(Heart)<{ isFavorited: boolean }>`
|
||||
${ClickableStyle}
|
||||
height: 22px;
|
||||
width: 24px;
|
||||
color: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : theme.textSecondary)};
|
||||
fill: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : 'transparent')};
|
||||
`
|
||||
|
||||
const ClickableContent = styled.div`
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
@@ -149,10 +165,11 @@ const StyledHeaderRow = styled(StyledTokenRow)`
|
||||
justify-content: space-between;
|
||||
}
|
||||
`
|
||||
const ListNumberCell = styled(Cell)`
|
||||
|
||||
const ListNumberCell = styled(Cell)<{ header: boolean }>`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
min-width: 32px;
|
||||
height: 48px;
|
||||
height: ${({ header }) => (header ? '48px' : '60px')};
|
||||
|
||||
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
|
||||
display: none;
|
||||
@@ -215,15 +232,12 @@ const SortArrowCell = styled(Cell)`
|
||||
`
|
||||
const HeaderCellWrapper = styled.span<{ onClick?: () => void }>`
|
||||
align-items: center;
|
||||
${ClickableStyle}
|
||||
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'unset')};
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
opacity: 60%;
|
||||
}
|
||||
`
|
||||
const SparkLineCell = styled(Cell)`
|
||||
padding: 0px 24px;
|
||||
@@ -387,20 +401,25 @@ export function TokenRow({
|
||||
volume: ReactNode
|
||||
last?: boolean
|
||||
}) {
|
||||
const favoriteTokensEnabled = useFavoriteTokensFlag() === FavoriteTokensVariant.Enabled
|
||||
const rowCells = (
|
||||
<>
|
||||
<ListNumberCell>{listNumber}</ListNumberCell>
|
||||
<ListNumberCell header={header}>{listNumber}</ListNumberCell>
|
||||
<NameCell>{tokenInfo}</NameCell>
|
||||
<PriceCell sortable={header}>{price}</PriceCell>
|
||||
<PercentChangeCell sortable={header}>{percentChange}</PercentChangeCell>
|
||||
<MarketCapCell sortable={header}>{marketCap}</MarketCapCell>
|
||||
<VolumeCell sortable={header}>{volume}</VolumeCell>
|
||||
<SparkLineCell>{sparkLine}</SparkLineCell>
|
||||
<FavoriteCell>{favorited}</FavoriteCell>
|
||||
{favoriteTokensEnabled && <FavoriteCell>{favorited}</FavoriteCell>}
|
||||
</>
|
||||
)
|
||||
if (header) return <StyledHeaderRow>{rowCells}</StyledHeaderRow>
|
||||
return <StyledTokenRow {...rest}>{rowCells}</StyledTokenRow>
|
||||
if (header) return <StyledHeaderRow favoriteTokensEnabled={favoriteTokensEnabled}>{rowCells}</StyledHeaderRow>
|
||||
return (
|
||||
<StyledTokenRow favoriteTokensEnabled={favoriteTokensEnabled} {...rest}>
|
||||
{rowCells}
|
||||
</StyledTokenRow>
|
||||
)
|
||||
}
|
||||
|
||||
/* Header Row: top header row component for table */
|
||||
@@ -445,31 +464,29 @@ export function LoadingRow() {
|
||||
|
||||
/* Loaded State: row component with token information */
|
||||
export default function LoadedRow({
|
||||
tokenAddress,
|
||||
tokenListIndex,
|
||||
tokenListLength,
|
||||
tokenData,
|
||||
timePeriod,
|
||||
}: {
|
||||
tokenAddress: string
|
||||
tokenListIndex: number
|
||||
tokenListLength: number
|
||||
tokenData: TokenData
|
||||
tokenData: SingleTokenData
|
||||
timePeriod: TimePeriod
|
||||
}) {
|
||||
const tokenAddress = tokenData?.tokens?.[0].address
|
||||
const currency = useCurrency(tokenAddress)
|
||||
const tokenName = tokenData.name
|
||||
const tokenSymbol = tokenData.symbol
|
||||
const theme = useTheme()
|
||||
const [favoriteTokens] = useAtom(favoritesAtom)
|
||||
const isFavorited = favoriteTokens.includes(tokenAddress)
|
||||
const tokenName = tokenData?.name
|
||||
const tokenSymbol = tokenData?.tokens?.[0].symbol
|
||||
const isFavorited = useIsFavorited(tokenAddress)
|
||||
const toggleFavorite = useToggleFavorite(tokenAddress)
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
const filterNetwork = useAtomValue(filterNetworkAtom)
|
||||
const L2Icon = getChainInfo(filterNetwork).circleLogoUrl
|
||||
const delta = tokenData.percentChange?.[timePeriod]?.value
|
||||
const arrow = delta ? getDeltaArrow(delta) : null
|
||||
const formattedDelta = delta ? formatDelta(delta) : null
|
||||
const tokenDetails = tokenData?.markets?.[0]
|
||||
const { volume, pricePercentChange } = getDurationDetails(tokenData, timePeriod)
|
||||
const arrow = pricePercentChange ? getDeltaArrow(pricePercentChange) : null
|
||||
const formattedDelta = pricePercentChange ? formatDelta(pricePercentChange) : null
|
||||
|
||||
const exploreTokenSelectedEventProperties = {
|
||||
chain_id: filterNetwork,
|
||||
@@ -481,7 +498,6 @@ export default function LoadedRow({
|
||||
search_token_address_input: filterString,
|
||||
}
|
||||
|
||||
const heartColor = isFavorited ? theme.accentActive : undefined
|
||||
// TODO: currency logo sizing mobile (32px) vs. desktop (24px)
|
||||
return (
|
||||
<StyledLink
|
||||
@@ -497,14 +513,14 @@ export default function LoadedRow({
|
||||
toggleFavorite()
|
||||
}}
|
||||
>
|
||||
<Heart size={18} color={heartColor} fill={heartColor} />
|
||||
<FavoriteIcon isFavorited={isFavorited} />
|
||||
</ClickFavorited>
|
||||
}
|
||||
listNumber={tokenListIndex + 1}
|
||||
tokenInfo={
|
||||
<ClickableName>
|
||||
<LogoContainer>
|
||||
<CurrencyLogo currency={currency} />
|
||||
<CurrencyLogo currency={currency} symbol={tokenSymbol} />
|
||||
<L2NetworkLogo networkUrl={L2Icon} />
|
||||
</LogoContainer>
|
||||
<TokenInfoCell>
|
||||
@@ -516,7 +532,7 @@ export default function LoadedRow({
|
||||
price={
|
||||
<ClickableContent>
|
||||
<PriceInfoCell>
|
||||
{tokenData.price?.value ? formatDollarAmount(tokenData.price?.value) : '-'}
|
||||
{tokenDetails?.price?.value ? formatDollarAmount(tokenDetails?.price?.value) : '-'}
|
||||
<PercentChangeInfoCell>
|
||||
{formattedDelta}
|
||||
{arrow}
|
||||
@@ -532,19 +548,23 @@ export default function LoadedRow({
|
||||
}
|
||||
marketCap={
|
||||
<ClickableContent>
|
||||
{tokenData.marketCap?.value ? formatDollarAmount(tokenData.marketCap?.value) : '-'}
|
||||
</ClickableContent>
|
||||
}
|
||||
volume={
|
||||
<ClickableContent>
|
||||
{tokenData.volume?.[timePeriod]?.value
|
||||
? formatDollarAmount(tokenData.volume?.[timePeriod]?.value ?? undefined)
|
||||
: '-'}
|
||||
{tokenDetails?.marketCap?.value ? formatDollarAmount(tokenDetails?.marketCap?.value) : '-'}
|
||||
</ClickableContent>
|
||||
}
|
||||
volume={<ClickableContent>{volume ? formatDollarAmount(volume ?? undefined) : '-'}</ClickableContent>}
|
||||
sparkLine={
|
||||
<SparkLine>
|
||||
<ParentSize>{({ width, height }) => <SparklineChart width={width} height={height} />}</ParentSize>
|
||||
<ParentSize>
|
||||
{({ width, height }) => (
|
||||
<SparklineChart
|
||||
width={width}
|
||||
height={height}
|
||||
tokenData={tokenData}
|
||||
pricePercentChange={pricePercentChange}
|
||||
timePeriod={timePeriod}
|
||||
/>
|
||||
)}
|
||||
</ParentSize>
|
||||
</SparkLine>
|
||||
}
|
||||
first={tokenListIndex === 0}
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
sortCategoryAtom,
|
||||
sortDirectionAtom,
|
||||
} from 'components/Tokens/state'
|
||||
import { TimePeriod, TokenData } from 'graphql/data/TopTokenQuery'
|
||||
import { TokenTopQuery$data } from 'graphql/data/__generated__/TokenTopQuery.graphql'
|
||||
import { getDurationDetails, SingleTokenData, useTopTokenQuery } from 'graphql/data/Token'
|
||||
import { TimePeriod } from 'graphql/data/Token'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { ReactNode, Suspense, useCallback, useMemo } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
@@ -47,33 +49,37 @@ const TokenRowsContainer = styled.div`
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
function useFilteredTokens(tokens: TokenData[] | undefined) {
|
||||
function useFilteredTokens(data: TokenTopQuery$data): SingleTokenData[] | undefined {
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
const favoriteTokenAddresses = useAtomValue(favoritesAtom)
|
||||
const favorites = useAtomValue(favoritesAtom)
|
||||
const showFavorites = useAtomValue(showFavoritesAtom)
|
||||
const shownTokens =
|
||||
showFavorites && tokens ? tokens.filter((token) => favoriteTokenAddresses.includes(token.address)) : tokens
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
(shownTokens ?? []).filter((token) => {
|
||||
if (!token.address) {
|
||||
return false
|
||||
}
|
||||
if (!filterString) {
|
||||
return true
|
||||
}
|
||||
const lowercaseFilterString = filterString.toLowerCase()
|
||||
const addressIncludesFilterString = token?.address?.toLowerCase().includes(lowercaseFilterString)
|
||||
const nameIncludesFilterString = token?.name?.toLowerCase().includes(lowercaseFilterString)
|
||||
const symbolIncludesFilterString = token?.symbol?.toLowerCase().includes(lowercaseFilterString)
|
||||
return nameIncludesFilterString || symbolIncludesFilterString || addressIncludesFilterString
|
||||
}),
|
||||
[shownTokens, filterString]
|
||||
data.topTokenProjects
|
||||
?.filter(
|
||||
(token) => !showFavorites || (token?.tokens?.[0].address && favorites.includes(token?.tokens?.[0].address))
|
||||
)
|
||||
.filter((token) => {
|
||||
const tokenInfo = token?.tokens?.[0]
|
||||
const address = tokenInfo?.address
|
||||
if (!address) {
|
||||
return false
|
||||
} else if (!filterString) {
|
||||
return true
|
||||
} else {
|
||||
const lowercaseFilterString = filterString.toLowerCase()
|
||||
const addressIncludesFilterString = address?.toLowerCase().includes(lowercaseFilterString)
|
||||
const nameIncludesFilterString = token?.name?.toLowerCase().includes(lowercaseFilterString)
|
||||
const symbolIncludesFilterString = tokenInfo?.symbol?.toLowerCase().includes(lowercaseFilterString)
|
||||
return nameIncludesFilterString || symbolIncludesFilterString || addressIncludesFilterString
|
||||
}
|
||||
}),
|
||||
[data.topTokenProjects, favorites, filterString, showFavorites]
|
||||
)
|
||||
}
|
||||
|
||||
function useSortedTokens(tokenData: TokenData[] | null) {
|
||||
function useSortedTokens(tokenData: SingleTokenData[] | undefined) {
|
||||
const sortCategory = useAtomValue(sortCategoryAtom)
|
||||
const sortDirection = useAtomValue(sortDirectionAtom)
|
||||
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
|
||||
@@ -103,22 +109,25 @@ function useSortedTokens(tokenData: TokenData[] | null) {
|
||||
}
|
||||
let a: number | null | undefined
|
||||
let b: number | null | undefined
|
||||
|
||||
const { volume: aVolume, pricePercentChange: aChange } = getDurationDetails(token1, timePeriod)
|
||||
const { volume: bVolume, pricePercentChange: bChange } = getDurationDetails(token2, timePeriod)
|
||||
switch (sortCategory) {
|
||||
case Category.marketCap:
|
||||
a = token1.marketCap?.value
|
||||
b = token2.marketCap?.value
|
||||
a = token1.markets?.[0]?.marketCap?.value
|
||||
b = token2.markets?.[0]?.marketCap?.value
|
||||
break
|
||||
case Category.price:
|
||||
a = token1.price?.value
|
||||
b = token2.price?.value
|
||||
a = token1.markets?.[0]?.price?.value
|
||||
b = token2.markets?.[0]?.price?.value
|
||||
break
|
||||
case Category.volume:
|
||||
a = token1.volume?.[timePeriod]?.value
|
||||
b = token2.volume?.[timePeriod]?.value
|
||||
a = aVolume
|
||||
b = bVolume
|
||||
break
|
||||
case Category.percentChange:
|
||||
a = token1.percentChange?.[timePeriod]?.value
|
||||
b = token2.percentChange?.[timePeriod]?.value
|
||||
a = aChange
|
||||
b = bChange
|
||||
break
|
||||
}
|
||||
return sortFn(a, b)
|
||||
@@ -149,14 +158,15 @@ export function LoadingTokenTable() {
|
||||
)
|
||||
}
|
||||
|
||||
export default function TokenTable({ data }: { data: TokenData[] | undefined }) {
|
||||
export default function TokenTable() {
|
||||
const showFavorites = useAtomValue<boolean>(showFavoritesAtom)
|
||||
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
|
||||
const filteredTokens = useFilteredTokens(data)
|
||||
const topTokens = useTopTokenQuery(1, timePeriod)
|
||||
const filteredTokens = useFilteredTokens(topTokens)
|
||||
const sortedFilteredTokens = useSortedTokens(filteredTokens)
|
||||
|
||||
/* loading and error state */
|
||||
if (data === null) {
|
||||
if (topTokens === null) {
|
||||
return (
|
||||
<NoTokensState
|
||||
message={
|
||||
@@ -184,8 +194,7 @@ export default function TokenTable({ data }: { data: TokenData[] | undefined })
|
||||
<TokenRowsContainer>
|
||||
{sortedFilteredTokens?.map((token, index) => (
|
||||
<LoadedRow
|
||||
key={token.address}
|
||||
tokenAddress={token.address}
|
||||
key={token?.name}
|
||||
tokenListIndex={index}
|
||||
tokenListLength={sortedFilteredTokens.length}
|
||||
tokenData={token}
|
||||
|
||||
@@ -3,12 +3,13 @@ import { Link } from 'react-router-dom'
|
||||
import { useShowTokensPromoBanner } from 'state/user/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { opacify } from 'theme/utils'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import tokensPromoDark from '../../assets/images/tokensPromoDark.png'
|
||||
import tokensPromoLight from '../../assets/images/tokensPromoLight.png'
|
||||
|
||||
const PopupContainer = styled.div<{ show: boolean }>`
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
display: ${({ show }) => (show ? 'flex' : 'none')};
|
||||
flex-direction: column;
|
||||
padding: 12px 16px 12px 20px;
|
||||
@@ -17,7 +18,7 @@ const PopupContainer = styled.div<{ show: boolean }>`
|
||||
right: 16px;
|
||||
width: 320px;
|
||||
height: 88px;
|
||||
z-index: 5;
|
||||
z-index: ${Z_INDEX.sticky};
|
||||
background-color: ${({ theme }) => (theme.darkMode ? theme.backgroundScrim : opacify(60, '#FDF0F8'))};
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
@@ -32,7 +33,7 @@ const PopupContainer = styled.div<{ show: boolean }>`
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.slow}ms opacity ${timing.in}`};
|
||||
}) => `${duration.slow} opacity ${timing.in}`};
|
||||
`
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { TimePeriod } from 'graphql/data/TopTokenQuery'
|
||||
import { TimePeriod } from 'graphql/data/Token'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
import { atomWithReset, atomWithStorage } from 'jotai/utils'
|
||||
import { useCallback } from 'react'
|
||||
import { atomWithReset, atomWithStorage, useAtomValue } from 'jotai/utils'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
import { Category, SortDirection } from './types'
|
||||
|
||||
@@ -15,17 +15,18 @@ export const sortCategoryAtom = atom<Category>(Category.marketCap)
|
||||
export const sortDirectionAtom = atom<SortDirection>(SortDirection.decreasing)
|
||||
|
||||
/* for favoriting tokens */
|
||||
export function useToggleFavorite(tokenAddress: string) {
|
||||
export function useToggleFavorite(tokenAddress: string | undefined | null) {
|
||||
const [favoriteTokens, updateFavoriteTokens] = useAtom(favoritesAtom)
|
||||
|
||||
return useCallback(() => {
|
||||
if (!tokenAddress) return
|
||||
let updatedFavoriteTokens
|
||||
if (favoriteTokens.includes(tokenAddress)) {
|
||||
if (favoriteTokens.includes(tokenAddress.toLocaleLowerCase())) {
|
||||
updatedFavoriteTokens = favoriteTokens.filter((address: string) => {
|
||||
return address !== tokenAddress
|
||||
return address !== tokenAddress.toLocaleLowerCase()
|
||||
})
|
||||
} else {
|
||||
updatedFavoriteTokens = [...favoriteTokens, tokenAddress]
|
||||
updatedFavoriteTokens = [...favoriteTokens, tokenAddress.toLocaleLowerCase()]
|
||||
}
|
||||
updateFavoriteTokens(updatedFavoriteTokens)
|
||||
}, [favoriteTokens, tokenAddress, updateFavoriteTokens])
|
||||
@@ -47,3 +48,12 @@ export function useSetSortCategory(category: Category) {
|
||||
}
|
||||
}, [category, sortCategory, setSortCategory, sortDirection, setDirectionCategory])
|
||||
}
|
||||
|
||||
export function useIsFavorited(tokenAddress: string | null | undefined) {
|
||||
const favoritedTokens = useAtomValue<string[]>(favoritesAtom)
|
||||
|
||||
return useMemo(
|
||||
() => (tokenAddress ? favoritedTokens.includes(tokenAddress.toLocaleLowerCase()) : false),
|
||||
[favoritedTokens, tokenAddress]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import AddressClaimModal from 'components/claim/AddressClaimModal'
|
||||
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
|
||||
import TokensBanner from 'components/Tokens/TokensBanner'
|
||||
import { TokensVariant, useTokensFlag } from 'featureFlags/flags/tokens'
|
||||
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
|
||||
import { lazy } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
|
||||
const Cart = lazy(() => import('nft/components/sell/modal/ListingTag'))
|
||||
|
||||
export default function TopLevelModals() {
|
||||
const addressClaimOpen = useModalIsOpen(ApplicationModal.ADDRESS_CLAIM)
|
||||
const addressClaimToggle = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
|
||||
|
||||
const blockedAccountModalOpen = useModalIsOpen(ApplicationModal.BLOCKED_ACCOUNT)
|
||||
const { account } = useWeb3React()
|
||||
const location = useLocation()
|
||||
|
||||
useAccountRiskCheck(account)
|
||||
const open = Boolean(blockedAccountModalOpen && account)
|
||||
@@ -18,6 +25,9 @@ export default function TopLevelModals() {
|
||||
<>
|
||||
<AddressClaimModal isOpen={addressClaimOpen} onDismiss={addressClaimToggle} />
|
||||
<ConnectedAccountBlocked account={account} isOpen={open} />
|
||||
{useTokensFlag() === TokensVariant.Enabled &&
|
||||
(location.pathname.includes('/pool') || location.pathname.includes('/swap')) && <TokensBanner />}
|
||||
<Cart />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -132,7 +132,12 @@ const AuthenticatedHeader = () => {
|
||||
<IconContainer>
|
||||
<IconButton onClick={copy} Icon={Copy} text={isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>} />
|
||||
<IconButton href={`${explorer}address/${account}`} Icon={ExternalLink} text={<Trans>Explore</Trans>} />
|
||||
<IconButton onClick={disconnect} Icon={Power} text={<Trans>Disconnect</Trans>} />
|
||||
<IconButton
|
||||
dataTestId="wallet-disconnect"
|
||||
onClick={disconnect}
|
||||
Icon={Power}
|
||||
text={<Trans>Disconnect</Trans>}
|
||||
/>
|
||||
</IconContainer>
|
||||
</HeaderWrapper>
|
||||
<Column>
|
||||
|
||||
@@ -27,7 +27,7 @@ const ConnectButton = styled(ButtonPrimary)`
|
||||
`
|
||||
|
||||
const Divider = styled.div`
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
`
|
||||
@@ -55,7 +55,7 @@ const ToggleMenuItem = styled.button`
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.fast}ms all ${timing.in}`};
|
||||
}) => `${duration.fast} all ${timing.in}`};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -114,11 +114,13 @@ const WalletDropdown = ({ setMenu }: { setMenu: (state: MenuState) => void }) =>
|
||||
{isAuthenticated ? (
|
||||
<AuthenticatedHeader />
|
||||
) : (
|
||||
<ConnectButton onClick={toggleWalletModal}>Connect wallet</ConnectButton>
|
||||
<ConnectButton data-testid="wallet-connect-wallet" onClick={toggleWalletModal}>
|
||||
Connect wallet
|
||||
</ConnectButton>
|
||||
)}
|
||||
<Divider />
|
||||
{isAuthenticated && (
|
||||
<ToggleMenuItem onClick={() => setMenu(MenuState.TRANSACTIONS)}>
|
||||
<ToggleMenuItem data-testid="wallet-transactions" onClick={() => setMenu(MenuState.TRANSACTIONS)}>
|
||||
<DefaultText>
|
||||
<Trans>Transactions</Trans>{' '}
|
||||
{pendingTransactions.length > 0 && (
|
||||
@@ -132,7 +134,7 @@ const WalletDropdown = ({ setMenu }: { setMenu: (state: MenuState) => void }) =>
|
||||
</IconWrap>
|
||||
</ToggleMenuItem>
|
||||
)}
|
||||
<ToggleMenuItem onClick={() => setMenu(MenuState.LANGUAGE)}>
|
||||
<ToggleMenuItem data-testid="wallet-select-language" onClick={() => setMenu(MenuState.LANGUAGE)}>
|
||||
<DefaultText>
|
||||
<Trans>Language</Trans>
|
||||
</DefaultText>
|
||||
@@ -145,7 +147,7 @@ const WalletDropdown = ({ setMenu }: { setMenu: (state: MenuState) => void }) =>
|
||||
</IconWrap>
|
||||
</FlexContainer>
|
||||
</ToggleMenuItem>
|
||||
<ToggleMenuItem onClick={toggleDarkMode}>
|
||||
<ToggleMenuItem data-testid="wallet-select-theme" onClick={toggleDarkMode}>
|
||||
<DefaultText>{darkMode ? <Trans> Light theme</Trans> : <Trans>Dark theme</Trans>}</DefaultText>
|
||||
<IconWrap>{darkMode ? <Sun size={16} /> : <Moon size={16} />}</IconWrap>
|
||||
</ToggleMenuItem>
|
||||
|
||||
@@ -28,7 +28,7 @@ const IconStyles = css`
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.fast}ms background-color ${timing.in}`};
|
||||
}) => `${duration.fast} background-color ${timing.in}`};
|
||||
|
||||
${IconHoverText} {
|
||||
opacity: 1;
|
||||
@@ -64,18 +64,19 @@ interface IconButtonProps {
|
||||
Icon: Icon
|
||||
onClick?: () => void
|
||||
href?: string
|
||||
dataTestId?: string
|
||||
}
|
||||
|
||||
const IconButton = ({ Icon, onClick, text, href }: IconButtonProps) => {
|
||||
const IconButton = ({ Icon, onClick, text, href, dataTestId }: IconButtonProps) => {
|
||||
return href ? (
|
||||
<IconBlockLink href={href} target="_blank">
|
||||
<IconBlockLink data-testId={dataTestId} href={href} target="_blank">
|
||||
<IconWrapper>
|
||||
<Icon strokeWidth={1.5} size={16} />
|
||||
<IconHoverText>{text}</IconHoverText>
|
||||
</IconWrapper>
|
||||
</IconBlockLink>
|
||||
) : (
|
||||
<IconBlockButton onClick={onClick}>
|
||||
<IconBlockButton data-testId={dataTestId} onClick={onClick}>
|
||||
<IconWrapper>
|
||||
<Icon strokeWidth={1.5} size={16} />
|
||||
<IconHoverText>{text}</IconHoverText>
|
||||
|
||||
@@ -33,7 +33,7 @@ const InternalLinkMenuItem = styled(InternalMenuItem)`
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.fast}ms background-color ${timing.in}`};
|
||||
}) => `${duration.fast} background-color ${timing.in}`};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -45,7 +45,7 @@ function LanguageMenuItem({ locale, isActive }: { locale: SupportedLocale; isAct
|
||||
|
||||
return (
|
||||
<InternalLinkMenuItem onClick={onClick} to={to}>
|
||||
<Text fontSize={16} fontWeight={400} lineHeight="24px">
|
||||
<Text data-testid="wallet-language-item" fontSize={16} fontWeight={400} lineHeight="24px">
|
||||
{LOCALE_LABEL[locale]}
|
||||
</Text>
|
||||
{isActive && <Check color={theme.accentActive} opacity={1} size={20} />}
|
||||
|
||||
@@ -42,12 +42,12 @@ const ClearAll = styled.div`
|
||||
margin-bottom: auto;
|
||||
|
||||
:hover {
|
||||
opacity: 0.6;
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
transition: ${({
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.fast}ms opacity ${timing.in}`};
|
||||
}) => `${duration.fast} opacity ${timing.in}`};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -60,7 +60,7 @@ const StyledChevron = styled(ChevronLeft)`
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.fast}ms color ${timing.in}`};
|
||||
}) => `${duration.fast} color ${timing.in}`};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -101,8 +101,8 @@ export const SlideOutMenu = ({
|
||||
<Menu>
|
||||
<BackSection>
|
||||
<BackSectionContainer>
|
||||
<StyledChevron onClick={onClose} size={24} />
|
||||
<Header>{title}</Header>
|
||||
<StyledChevron data-testid="wallet-back" onClick={onClose} size={24} />
|
||||
<Header data-testid="wallet-header">{title}</Header>
|
||||
{onClear && <ClearAll onClick={onClear}>Clear All</ClearAll>}
|
||||
</BackSectionContainer>
|
||||
</BackSection>
|
||||
|
||||
@@ -158,7 +158,7 @@ export const TransactionHistoryMenu = ({ onClose }: { onClose: () => void }) =>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<EmptyTransaction>
|
||||
<EmptyTransaction data-testid="wallet-empty-transaction-text">
|
||||
<Trans>Your transactions will appear here</Trans>
|
||||
</EmptyTransaction>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { Z_INDEX } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import { useModalIsOpen } from '../../state/application/hooks'
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
@@ -37,8 +37,8 @@ export enum MenuState {
|
||||
}
|
||||
|
||||
const WalletDropdownWrapper = styled.div`
|
||||
position: absolute;
|
||||
top: 65px;
|
||||
position: fixed;
|
||||
top: 72px;
|
||||
right: 20px;
|
||||
z-index: ${Z_INDEX.dropdown};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
|
||||
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
|
||||
import { ElementName, Event, EventName } from 'analytics/constants'
|
||||
import { TraceEvent } from 'analytics/TraceEvent'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import React from 'react'
|
||||
import { Check } from 'react-feather'
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { sendAnalyticsEvent, user } from 'components/AmplitudeAnalytics'
|
||||
import { CUSTOM_USER_PROPERTIES, EventName, WALLET_CONNECTION_RESULT } from 'components/AmplitudeAnalytics/constants'
|
||||
import { sendAnalyticsEvent, user } from 'analytics'
|
||||
import { CUSTOM_USER_PROPERTIES, EventName, WALLET_CONNECTION_RESULT } from 'analytics/constants'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { AutoRow } from 'components/Row'
|
||||
import { getConnection, getConnectionName, getIsCoinbaseWallet, getIsInjected, getIsMetaMask } from 'connection/utils'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { updateConnectionError } from 'state/connection/reducer'
|
||||
@@ -35,7 +36,7 @@ const CloseIcon = styled.div`
|
||||
top: 14px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -149,6 +150,8 @@ export default function WalletModal({
|
||||
}) {
|
||||
const dispatch = useAppDispatch()
|
||||
const { connector, account, chainId } = useWeb3React()
|
||||
const previousAccount = usePrevious(account)
|
||||
|
||||
const [connectedWallets, addWalletToConnectedWallets] = useConnectedWallets()
|
||||
|
||||
const redesignFlag = useRedesignFlag()
|
||||
@@ -174,6 +177,12 @@ export default function WalletModal({
|
||||
}
|
||||
}, [walletModalOpen, setWalletView, account])
|
||||
|
||||
useEffect(() => {
|
||||
if (account && account !== previousAccount && walletModalOpen) {
|
||||
toggleWalletModal()
|
||||
}
|
||||
}, [account, previousAccount, toggleWalletModal, walletModalOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingConnector && walletView !== WALLET_VIEWS.PENDING) {
|
||||
updateConnectionError({ connectionType: getConnection(pendingConnector).type, error: undefined })
|
||||
@@ -333,7 +342,7 @@ export default function WalletModal({
|
||||
|
||||
return (
|
||||
<UpperSection>
|
||||
<CloseIcon onClick={toggleWalletModal}>
|
||||
<CloseIcon data-testid="wallet-modal-close" onClick={toggleWalletModal}>
|
||||
<CloseColor />
|
||||
</CloseIcon>
|
||||
{headerRow}
|
||||
@@ -363,7 +372,9 @@ export default function WalletModal({
|
||||
maxHeight={90}
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
>
|
||||
<Wrapper redesignFlag={redesignFlagEnabled}>{getModalContent()}</Wrapper>
|
||||
<Wrapper data-testid="wallet-modal" redesignFlag={redesignFlagEnabled}>
|
||||
{getModalContent()}
|
||||
</Wrapper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Web3ReactHooks, Web3ReactProvider } from '@web3-react/core'
|
||||
import { SupportedChainId } from '@uniswap/widgets'
|
||||
import { useWeb3React, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { Connection } from 'connection'
|
||||
import { getConnectionName } from 'connection/utils'
|
||||
import { RPC_PROVIDERS } from 'constants/providers'
|
||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||
import useEagerlyConnect from 'hooks/useEagerlyConnect'
|
||||
import useOrderedConnections from 'hooks/useOrderedConnections'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
import { ReactNode, useEffect, useMemo } from 'react'
|
||||
|
||||
export default function Web3Provider({ children }: { children: ReactNode }) {
|
||||
useEagerlyConnect()
|
||||
@@ -15,7 +18,37 @@ export default function Web3Provider({ children }: { children: ReactNode }) {
|
||||
|
||||
return (
|
||||
<Web3ReactProvider connectors={connectors} key={key}>
|
||||
<Tracer />
|
||||
{children}
|
||||
</Web3ReactProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function Tracer() {
|
||||
const { chainId, provider } = useWeb3React()
|
||||
const networkProvider = RPC_PROVIDERS[(chainId || SupportedChainId.MAINNET) as SupportedChainId]
|
||||
const shouldTrace = useTraceJsonRpcFlag() === TraceJsonRpcVariant.Enabled
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldTrace) {
|
||||
provider?.on('debug', trace)
|
||||
if (provider !== networkProvider) {
|
||||
networkProvider?.on('debug', trace)
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
provider?.off('debug', trace)
|
||||
networkProvider?.off('debug', trace)
|
||||
}
|
||||
}, [networkProvider, provider, shouldTrace])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function trace(event: any) {
|
||||
if (event.action !== 'request') return
|
||||
const { method, id, params } = event.request
|
||||
console.groupCollapsed(method, id)
|
||||
console.debug(params)
|
||||
console.groupEnd()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
|
||||
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
|
||||
import { ElementName, Event, EventName } from 'analytics/constants'
|
||||
import { TraceEvent } from 'analytics/TraceEvent'
|
||||
import { StyledChevronDown, StyledChevronUp } from 'components/Icons'
|
||||
import WalletDropdown from 'components/WalletDropdown'
|
||||
import { getConnection } from 'connection/utils'
|
||||
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
|
||||
@@ -10,10 +11,10 @@ import { Portal } from 'nft/components/common/Portal'
|
||||
import { getIsValidSwapQuote } from 'pages/Swap'
|
||||
import { darken } from 'polished'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { AlertTriangle, ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import { useDerivedSwapInfo } from 'state/swap/hooks'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import styled, { css, useTheme } from 'styled-components/macro'
|
||||
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import { useHasSocks } from '../../hooks/useSocksBalance'
|
||||
@@ -33,12 +34,15 @@ import Loader from '../Loader'
|
||||
import { RowBetween } from '../Row'
|
||||
import WalletModal from '../WalletModal'
|
||||
|
||||
// https://stackoverflow.com/a/31617326
|
||||
const FULL_BORDER_RADIUS = 9999
|
||||
|
||||
const Web3StatusGeneric = styled(ButtonSecondary)`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
border-radius: 14px;
|
||||
border-radius: ${FULL_BORDER_RADIUS}px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
height: 36px;
|
||||
@@ -59,15 +63,15 @@ const Web3StatusError = styled(Web3StatusGeneric)`
|
||||
}
|
||||
`
|
||||
|
||||
const Web3StatusConnectNavbar = styled.button<{ faded?: boolean }>`
|
||||
dispay: flex;
|
||||
align-items: center;
|
||||
const Web3StatusConnectButton = styled.button<{ faded?: boolean }>`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.accentActionSoft};
|
||||
border-radius: 12px;
|
||||
border-radius: ${FULL_BORDER_RADIUS}px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 10px 12px;
|
||||
padding: 0 12px;
|
||||
height: 40px;
|
||||
|
||||
:hover,
|
||||
:active,
|
||||
@@ -153,6 +157,7 @@ function Sock() {
|
||||
|
||||
const VerticalDivider = styled.div`
|
||||
height: 20px;
|
||||
margin: 0px 4px;
|
||||
width: 1px;
|
||||
background-color: ${({ theme }) => theme.accentAction};
|
||||
`
|
||||
@@ -169,22 +174,7 @@ const StyledConnect = styled.div`
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.fast}ms color ${timing.in}`};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledChevron = styled.span`
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
height: 24px;
|
||||
margin-left: 4px;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.accentActionSoft};
|
||||
transition: ${({
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.fast}ms color ${timing.in}`};
|
||||
}) => `${duration.fast} color ${timing.in}`};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -196,7 +186,8 @@ function Web3StatusInner() {
|
||||
inputError: swapInputError,
|
||||
} = useDerivedSwapInfo()
|
||||
const validSwapQuote = getIsValidSwapQuote(trade, tradeState, swapInputError)
|
||||
const navbarFlag = useNavBarFlag()
|
||||
const navbarFlagEnabled = useNavBarFlag() === NavBarVariant.Enabled
|
||||
const theme = useTheme()
|
||||
const toggleWalletDropdown = useToggleWalletDropdown()
|
||||
const toggleWalletModal = useToggleWalletModal()
|
||||
const walletIsOpen = useIsOpen()
|
||||
@@ -214,7 +205,7 @@ function Web3StatusInner() {
|
||||
|
||||
const hasPendingTransactions = !!pending.length
|
||||
const hasSocks = useHasSocks()
|
||||
const toggleWallet = navbarFlag === NavBarVariant.Enabled ? toggleWalletDropdown : toggleWalletModal
|
||||
const toggleWallet = navbarFlagEnabled ? toggleWalletDropdown : toggleWalletModal
|
||||
|
||||
if (!chainId) {
|
||||
return null
|
||||
@@ -230,6 +221,7 @@ function Web3StatusInner() {
|
||||
} else if (account) {
|
||||
return (
|
||||
<Web3StatusConnected data-testid="web3-status-connected" onClick={toggleWallet} pending={hasPendingTransactions}>
|
||||
{navbarFlagEnabled && !hasPendingTransactions && <StatusIcon size={24} connectionType={connectionType} />}
|
||||
{hasPendingTransactions ? (
|
||||
<RowBetween>
|
||||
<Text>
|
||||
@@ -239,11 +231,18 @@ function Web3StatusInner() {
|
||||
</RowBetween>
|
||||
) : (
|
||||
<>
|
||||
{hasSocks ? <Sock /> : null}
|
||||
{hasSocks && !navbarFlagEnabled ? <Sock /> : null}
|
||||
<Text>{ENSName || shortenAddress(account)}</Text>
|
||||
{navbarFlagEnabled ? (
|
||||
walletIsOpen ? (
|
||||
<StyledChevronUp onClick={toggleWalletDropdown} />
|
||||
) : (
|
||||
<StyledChevronDown onClick={toggleWalletDropdown} />
|
||||
)
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{!hasPendingTransactions && <StatusIcon connectionType={connectionType} />}
|
||||
{!navbarFlagEnabled && !hasPendingTransactions && <StatusIcon connectionType={connectionType} />}
|
||||
</Web3StatusConnected>
|
||||
)
|
||||
} else {
|
||||
@@ -254,16 +253,26 @@ function Web3StatusInner() {
|
||||
properties={{ received_swap_quote: validSwapQuote }}
|
||||
element={ElementName.CONNECT_WALLET_BUTTON}
|
||||
>
|
||||
{navbarFlag === NavBarVariant.Enabled ? (
|
||||
<Web3StatusConnectNavbar faded={!account}>
|
||||
<StyledConnect onClick={toggleWalletModal}>
|
||||
{navbarFlagEnabled ? (
|
||||
<Web3StatusConnectButton faded={!account}>
|
||||
<StyledConnect data-testid="navbar-connect-wallet" onClick={toggleWalletModal}>
|
||||
<Trans>Connect</Trans>
|
||||
</StyledConnect>
|
||||
<VerticalDivider />
|
||||
<StyledChevron onClick={toggleWalletDropdown}>
|
||||
{walletIsOpen ? <ChevronUp /> : <ChevronDown />}
|
||||
</StyledChevron>
|
||||
</Web3StatusConnectNavbar>
|
||||
{walletIsOpen ? (
|
||||
<StyledChevronUp
|
||||
data-testid="navbar-wallet-dropdown"
|
||||
customColor={theme.accentAction}
|
||||
onClick={toggleWalletDropdown}
|
||||
/>
|
||||
) : (
|
||||
<StyledChevronDown
|
||||
data-testid="navbar-wallet-dropdown"
|
||||
customColor={theme.accentAction}
|
||||
onClick={toggleWalletDropdown}
|
||||
/>
|
||||
)}
|
||||
</Web3StatusConnectButton>
|
||||
) : (
|
||||
<Web3StatusConnect onClick={toggleWallet} faded={!account}>
|
||||
<Text>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Currency, SwapWidget } from '@uniswap/widgets'
|
||||
import { Currency, OnReviewSwapClick, SwapWidget } from '@uniswap/widgets'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { RPC_URLS } from 'constants/networks'
|
||||
import { RPC_PROVIDERS } from 'constants/providers'
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import { useMemo } from 'react'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
@@ -16,9 +16,10 @@ const WIDGET_ROUTER_URL = 'https://api.uniswap.org/v1/'
|
||||
|
||||
export interface WidgetProps {
|
||||
defaultToken?: Currency
|
||||
onReviewSwapClick?: OnReviewSwapClick
|
||||
}
|
||||
|
||||
export default function Widget({ defaultToken }: WidgetProps) {
|
||||
export default function Widget({ defaultToken, onReviewSwapClick }: WidgetProps) {
|
||||
const locale = useActiveLocale()
|
||||
const darkMode = useIsDarkMode()
|
||||
const theme = useMemo(() => (darkMode ? DARK_THEME : LIGHT_THEME), [darkMode])
|
||||
@@ -31,12 +32,14 @@ export default function Widget({ defaultToken }: WidgetProps) {
|
||||
return (
|
||||
<>
|
||||
<SwapWidget
|
||||
disableBranding
|
||||
hideConnectionUI
|
||||
jsonRpcUrlMap={RPC_URLS}
|
||||
jsonRpcUrlMap={RPC_PROVIDERS}
|
||||
routerUrl={WIDGET_ROUTER_URL}
|
||||
width={WIDGET_WIDTH}
|
||||
locale={locale}
|
||||
theme={theme}
|
||||
onReviewSwapClick={onReviewSwapClick}
|
||||
// defaultChainId is excluded - it is always inferred from the passed provider
|
||||
provider={provider}
|
||||
{...inputs}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Currency, Field, SwapController, SwapEventHandlers, TradeType } from '@uniswap/widgets'
|
||||
import CurrencySearchModal from 'components/SearchModal/CurrencySearchModal'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
const EMPTY_AMOUNT = ''
|
||||
|
||||
/**
|
||||
* Integrates the Widget's inputs.
|
||||
@@ -8,7 +10,7 @@ import { useCallback, useMemo, useState } from 'react'
|
||||
*/
|
||||
export function useSyncWidgetInputs(defaultToken?: Currency) {
|
||||
const [type, setType] = useState(TradeType.EXACT_INPUT)
|
||||
const [amount, setAmount] = useState<string>()
|
||||
const [amount, setAmount] = useState(EMPTY_AMOUNT)
|
||||
const onAmountChange = useCallback((field: Field, amount: string) => {
|
||||
setType(toTradeType(field))
|
||||
setAmount(amount)
|
||||
@@ -17,6 +19,14 @@ export function useSyncWidgetInputs(defaultToken?: Currency) {
|
||||
const [tokens, setTokens] = useState<{ [Field.INPUT]?: Currency; [Field.OUTPUT]?: Currency }>({
|
||||
[Field.OUTPUT]: defaultToken,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setTokens({
|
||||
[Field.OUTPUT]: defaultToken,
|
||||
})
|
||||
setAmount(EMPTY_AMOUNT)
|
||||
}, [defaultToken])
|
||||
|
||||
const onSwitchTokens = useCallback(() => {
|
||||
setType((type) => invertTradeType(type))
|
||||
setTokens((tokens) => ({
|
||||
|
||||
@@ -1,18 +1,70 @@
|
||||
import { TransactionReceipt } from '@ethersproject/abstract-provider'
|
||||
import { TransactionEventHandlers } from '@uniswap/widgets'
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
TradeType,
|
||||
Transaction,
|
||||
TransactionEventHandlers,
|
||||
TransactionInfo,
|
||||
TransactionType as WidgetTransactionType,
|
||||
} from '@uniswap/widgets'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTransactionAdder } from 'state/transactions/hooks'
|
||||
import {
|
||||
ExactInputSwapTransactionInfo,
|
||||
ExactOutputSwapTransactionInfo,
|
||||
TransactionType as AppTransactionType,
|
||||
WrapTransactionInfo,
|
||||
} from 'state/transactions/types'
|
||||
import { currencyId } from 'utils/currencyId'
|
||||
|
||||
/** Integrates the Widget's transactions, showing the widget's transactions in the app. */
|
||||
export function useSyncWidgetTransactions() {
|
||||
// TODO(jfrankfurt): Integrate widget transactions with app transaction tracking.
|
||||
const txHandlers: TransactionEventHandlers = useMemo(
|
||||
() => ({
|
||||
onTxSubmit: (hash: string, tx: unknown) => console.log('onTxSubmit'),
|
||||
onTxSuccess: (hash: string, receipt: TransactionReceipt) => console.log('onTxSuccess'),
|
||||
onTxFail: (hash: string, receipt: TransactionReceipt) => console.log('onTxFail'),
|
||||
}),
|
||||
[]
|
||||
const { chainId } = useWeb3React()
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
const onTxSubmit = useCallback(
|
||||
(_hash: string, transaction: Transaction<TransactionInfo>) => {
|
||||
const { type, response } = transaction.info
|
||||
|
||||
if (!type || !response) {
|
||||
return
|
||||
} else if (type === WidgetTransactionType.WRAP || type === WidgetTransactionType.UNWRAP) {
|
||||
const { amount } = transaction.info
|
||||
|
||||
addTransaction(response, {
|
||||
type: AppTransactionType.WRAP,
|
||||
unwrapped: type === WidgetTransactionType.UNWRAP,
|
||||
currencyAmountRaw: amount.quotient.toString(),
|
||||
chainId,
|
||||
} as WrapTransactionInfo)
|
||||
} else if (type === WidgetTransactionType.SWAP) {
|
||||
const { slippageTolerance, trade, tradeType } = transaction.info
|
||||
const baseTxInfo = {
|
||||
type: AppTransactionType.SWAP,
|
||||
tradeType,
|
||||
inputCurrencyId: currencyId(trade.inputAmount.currency),
|
||||
outputCurrencyId: currencyId(trade.outputAmount.currency),
|
||||
}
|
||||
if (tradeType === TradeType.EXACT_OUTPUT) {
|
||||
addTransaction(response, {
|
||||
...baseTxInfo,
|
||||
maximumInputCurrencyAmountRaw: trade.maximumAmountIn(slippageTolerance).quotient.toString(),
|
||||
outputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
|
||||
expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
|
||||
} as ExactOutputSwapTransactionInfo)
|
||||
} else {
|
||||
addTransaction(response, {
|
||||
...baseTxInfo,
|
||||
inputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
|
||||
expectedOutputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
|
||||
minimumOutputCurrencyAmountRaw: trade.minimumAmountOut(slippageTolerance).quotient.toString(),
|
||||
} as ExactInputSwapTransactionInfo)
|
||||
}
|
||||
}
|
||||
},
|
||||
[addTransaction, chainId]
|
||||
)
|
||||
|
||||
const txHandlers: TransactionEventHandlers = useMemo(() => ({ onTxSubmit }), [onTxSubmit])
|
||||
|
||||
return { transactions: { ...txHandlers } }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TransactionResponse } from '@ethersproject/providers'
|
||||
import type { TransactionResponse } from '@ethersproject/providers'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TransactionResponse } from '@ethersproject/providers'
|
||||
import type { TransactionResponse } from '@ethersproject/providers'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TransactionResponse } from '@ethersproject/providers'
|
||||
import type { TransactionResponse } from '@ethersproject/providers'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { ModalName } from 'components/AmplitudeAnalytics/constants'
|
||||
import { Trace } from 'components/AmplitudeAnalytics/Trace'
|
||||
import { ReactNode, useCallback, useMemo, useState } from 'react'
|
||||
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { sendAnalyticsEvent } from 'analytics'
|
||||
import { ModalName } from 'analytics/constants'
|
||||
import { EventName } from 'analytics/constants'
|
||||
import { Trace } from 'analytics/Trace'
|
||||
import { formatPercentInBasisPointsNumber, formatToDecimal, getTokenAddress } from 'analytics/utils'
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
|
||||
|
||||
import TransactionConfirmationModal, {
|
||||
@@ -14,6 +18,27 @@ import TransactionConfirmationModal, {
|
||||
import SwapModalFooter from './SwapModalFooter'
|
||||
import SwapModalHeader from './SwapModalHeader'
|
||||
|
||||
const formatAnalyticsEventProperties = ({
|
||||
trade,
|
||||
txHash,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
txHash: string
|
||||
}) => ({
|
||||
transaction_hash: txHash,
|
||||
token_in_address: getTokenAddress(trade.inputAmount.currency),
|
||||
token_out_address: getTokenAddress(trade.outputAmount.currency),
|
||||
token_in_symbol: trade.inputAmount.currency.symbol,
|
||||
token_out_symbol: trade.outputAmount.currency.symbol,
|
||||
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
|
||||
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
|
||||
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
|
||||
chain_id:
|
||||
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
|
||||
? trade.inputAmount.currency.chainId
|
||||
: undefined,
|
||||
})
|
||||
|
||||
export default function ConfirmSwapModal({
|
||||
trade,
|
||||
originalTrade,
|
||||
@@ -27,6 +52,8 @@ export default function ConfirmSwapModal({
|
||||
attemptingTxn,
|
||||
txHash,
|
||||
swapQuoteReceivedDate,
|
||||
fiatValueInput,
|
||||
fiatValueOutput,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
@@ -40,10 +67,13 @@ export default function ConfirmSwapModal({
|
||||
swapErrorMessage: ReactNode | undefined
|
||||
onDismiss: () => void
|
||||
swapQuoteReceivedDate: Date | undefined
|
||||
fiatValueInput?: CurrencyAmount<Token> | null
|
||||
fiatValueOutput?: CurrencyAmount<Token> | null
|
||||
}) {
|
||||
// shouldLogModalCloseEvent lets the child SwapModalHeader component know when modal has been closed
|
||||
// and an event triggered by modal closing should be logged.
|
||||
const [shouldLogModalCloseEvent, setShouldLogModalCloseEvent] = useState(false)
|
||||
const [lastTxnHashLogged, setLastTxnHashLogged] = useState<string | null>(null)
|
||||
const showAcceptChanges = useMemo(
|
||||
() => Boolean(trade && originalTrade && tradeMeaningfullyDiffers(trade, originalTrade)),
|
||||
[originalTrade, trade]
|
||||
@@ -78,9 +108,21 @@ export default function ConfirmSwapModal({
|
||||
disabledConfirm={showAcceptChanges}
|
||||
swapErrorMessage={swapErrorMessage}
|
||||
swapQuoteReceivedDate={swapQuoteReceivedDate}
|
||||
fiatValueInput={fiatValueInput}
|
||||
fiatValueOutput={fiatValueOutput}
|
||||
/>
|
||||
) : null
|
||||
}, [onConfirm, showAcceptChanges, swapErrorMessage, trade, allowedSlippage, txHash, swapQuoteReceivedDate])
|
||||
}, [
|
||||
onConfirm,
|
||||
showAcceptChanges,
|
||||
swapErrorMessage,
|
||||
trade,
|
||||
allowedSlippage,
|
||||
txHash,
|
||||
swapQuoteReceivedDate,
|
||||
fiatValueInput,
|
||||
fiatValueOutput,
|
||||
])
|
||||
|
||||
// text to show while loading
|
||||
const pendingText = (
|
||||
@@ -105,8 +147,15 @@ export default function ConfirmSwapModal({
|
||||
[onModalDismiss, modalBottom, modalHeader, swapErrorMessage]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!attemptingTxn && isOpen && txHash && trade && txHash !== lastTxnHashLogged) {
|
||||
sendAnalyticsEvent(EventName.SWAP_SIGNED, formatAnalyticsEventProperties({ trade, txHash }))
|
||||
setLastTxnHashLogged(txHash)
|
||||
}
|
||||
}, [attemptingTxn, isOpen, txHash, trade, lastTxnHashLogged])
|
||||
|
||||
return (
|
||||
<Trace modal={ModalName.CONFIRM_SWAP} shouldLogImpression={isOpen}>
|
||||
<Trace modal={ModalName.CONFIRM_SWAP}>
|
||||
<TransactionConfirmationModal
|
||||
isOpen={isOpen}
|
||||
onDismiss={onModalDismiss}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
|
||||
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
|
||||
import { ElementName, Event, EventName } from 'analytics/constants'
|
||||
import { TraceEvent } from 'analytics/TraceEvent'
|
||||
import AnimatedDropdown from 'components/AnimatedDropdown'
|
||||
import Card, { OutlineCard } from 'components/Card'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
|
||||
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
|
||||
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { ElementName, Event, EventName } from 'analytics/constants'
|
||||
import { TraceEvent } from 'analytics/TraceEvent'
|
||||
import {
|
||||
formatPercentInBasisPointsNumber,
|
||||
formatPercentNumber,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
getDurationFromDateMilliseconds,
|
||||
getDurationUntilTimestampSeconds,
|
||||
getTokenAddress,
|
||||
} from 'components/AmplitudeAnalytics/utils'
|
||||
} from 'analytics/utils'
|
||||
import useTransactionDeadline from 'hooks/useTransactionDeadline'
|
||||
import { ReactNode } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
@@ -31,6 +31,8 @@ interface AnalyticsEventProps {
|
||||
isAutoRouterApi: boolean
|
||||
swapQuoteReceivedDate: Date | undefined
|
||||
routes: RoutingDiagramEntry[]
|
||||
fiatValueInput?: CurrencyAmount<Token> | null
|
||||
fiatValueOutput?: CurrencyAmount<Token> | null
|
||||
}
|
||||
|
||||
const formatRoutesEventProperties = (routes: RoutingDiagramEntry[]) => {
|
||||
@@ -69,6 +71,8 @@ const formatAnalyticsEventProperties = ({
|
||||
isAutoRouterApi,
|
||||
swapQuoteReceivedDate,
|
||||
routes,
|
||||
fiatValueInput,
|
||||
fiatValueOutput,
|
||||
}: AnalyticsEventProps) => ({
|
||||
estimated_network_fee_usd: trade.gasUseEstimateUSD ? formatToDecimal(trade.gasUseEstimateUSD, 2) : undefined,
|
||||
transaction_hash: hash,
|
||||
@@ -79,6 +83,8 @@ const formatAnalyticsEventProperties = ({
|
||||
token_out_symbol: trade.outputAmount.currency.symbol,
|
||||
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
|
||||
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
|
||||
token_in_amount_usd: fiatValueInput ? parseFloat(fiatValueInput.toFixed(2)) : undefined,
|
||||
token_out_amount_usd: fiatValueOutput ? parseFloat(fiatValueOutput.toFixed(2)) : undefined,
|
||||
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
|
||||
allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
|
||||
is_auto_router_api: isAutoRouterApi,
|
||||
@@ -102,6 +108,8 @@ export default function SwapModalFooter({
|
||||
swapErrorMessage,
|
||||
disabledConfirm,
|
||||
swapQuoteReceivedDate,
|
||||
fiatValueInput,
|
||||
fiatValueOutput,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
hash: string | undefined
|
||||
@@ -110,6 +118,8 @@ export default function SwapModalFooter({
|
||||
swapErrorMessage: ReactNode | undefined
|
||||
disabledConfirm: boolean
|
||||
swapQuoteReceivedDate: Date | undefined
|
||||
fiatValueInput?: CurrencyAmount<Token> | null
|
||||
fiatValueOutput?: CurrencyAmount<Token> | null
|
||||
}) {
|
||||
const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch
|
||||
const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto'
|
||||
@@ -122,7 +132,7 @@ export default function SwapModalFooter({
|
||||
<TraceEvent
|
||||
events={[Event.onClick]}
|
||||
element={ElementName.CONFIRM_SWAP_BUTTON}
|
||||
name={EventName.SWAP_SUBMITTED}
|
||||
name={EventName.SWAP_SUBMITTED_BUTTON_CLICKED}
|
||||
properties={formatAnalyticsEventProperties({
|
||||
trade,
|
||||
hash,
|
||||
@@ -132,6 +142,8 @@ export default function SwapModalFooter({
|
||||
isAutoRouterApi: !clientSideRouter,
|
||||
swapQuoteReceivedDate,
|
||||
routes,
|
||||
fiatValueInput,
|
||||
fiatValueOutput,
|
||||
})}
|
||||
>
|
||||
<ButtonError
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user