Compare commits
185 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
b82b9acc54 | ||
|
|
fac3845756 | ||
|
|
9381a74f1d | ||
|
|
f6662a3208 | ||
|
|
134af82d90 | ||
|
|
75175b8e54 | ||
|
|
e3d8599dc7 | ||
|
|
77ee69ad52 | ||
|
|
4b82838f80 | ||
|
|
a177829976 | ||
|
|
b8b4f960dd | ||
|
|
2466414307 | ||
|
|
a3a32f0d68 | ||
|
|
ee001f86f0 | ||
|
|
87d6975bd8 | ||
|
|
4cab4e27ff | ||
|
|
f105f0995b | ||
|
|
18e89a7353 | ||
|
|
445f9a67a4 | ||
|
|
4c039c900c | ||
|
|
2c2e0a3419 | ||
|
|
1b43e0b28a | ||
|
|
a23f7782b2 | ||
|
|
723db9d0ea | ||
|
|
cbf165dc40 | ||
|
|
6f3579acf1 | ||
|
|
c9e2f86e57 | ||
|
|
8c0199119e | ||
|
|
5e6e6be888 | ||
|
|
79fb6485b1 | ||
|
|
29baaaf2ed | ||
|
|
f824fb25c2 | ||
|
|
28a6ea7e1a | ||
|
|
65566faf17 | ||
|
|
eb4f90e669 | ||
|
|
40308158ca | ||
|
|
d0d5240474 | ||
|
|
751ce8e6d6 | ||
|
|
748a5eadc0 | ||
|
|
5659fe21ea | ||
|
|
bc899b74a3 | ||
|
|
516c8b05a4 | ||
|
|
8502f9e303 | ||
|
|
bc90d416e6 | ||
|
|
2fd1cd72fd | ||
|
|
3a2276dcd1 | ||
|
|
c432c583f6 | ||
|
|
9b8f5ed8f4 | ||
|
|
0713a15028 | ||
|
|
62c502615f | ||
|
|
fdbe4b8f5e | ||
|
|
33c73f4dc8 | ||
|
|
c207a576e7 | ||
|
|
aa426514f3 | ||
|
|
ba9c28892e | ||
|
|
49c31ddfc8 | ||
|
|
4424205814 | ||
|
|
874f3fb737 | ||
|
|
ac27c89a44 | ||
|
|
0e530cf92e | ||
|
|
ed66b00b20 | ||
|
|
84fb05239b | ||
|
|
7500bbc0be | ||
|
|
c43c8de6cd | ||
|
|
9d40db5b21 | ||
|
|
a61eca36ae | ||
|
|
60479a442f | ||
|
|
5257188f70 | ||
|
|
7599239983 | ||
|
|
1561c0d000 | ||
|
|
1f740cf8c0 | ||
|
|
55c5f03004 | ||
|
|
a345cff614 | ||
|
|
85d8566cfa | ||
|
|
44d68e3ef0 | ||
|
|
04bd4900b0 | ||
|
|
81f277b36f | ||
|
|
575660d3e8 | ||
|
|
1e692491f1 | ||
|
|
b3639b3453 | ||
|
|
53ebf37b40 | ||
|
|
624ec33652 | ||
|
|
2890040118 | ||
|
|
68db8b3e23 | ||
|
|
9873491db1 | ||
|
|
5d64ab0146 | ||
|
|
568267ce07 | ||
|
|
69cdefe996 | ||
|
|
61758db589 | ||
|
|
9e9d98bb31 | ||
|
|
6b2b771dc4 | ||
|
|
031bea0f50 | ||
|
|
6f2e447ec3 | ||
|
|
cf831fbcea | ||
|
|
bcd4c1c182 | ||
|
|
d954026cea | ||
|
|
2c2dad1415 | ||
|
|
d42ed88845 | ||
|
|
e12c00e980 | ||
|
|
c25971e5d2 | ||
|
|
293e56758c | ||
|
|
a6b17f0437 | ||
|
|
140d59b898 | ||
|
|
85742c5785 | ||
|
|
9b07d8ce64 | ||
|
|
b1b9da1b17 | ||
|
|
ffe670923e | ||
|
|
21649967aa | ||
|
|
3f40f60c1c | ||
|
|
176c275a06 | ||
|
|
ae2b4b1668 | ||
|
|
a27f8e2937 | ||
|
|
818b1c84b0 | ||
|
|
75eceaa5e1 | ||
|
|
c6b4cc8e01 | ||
|
|
819302b51f | ||
|
|
c53d7fcc32 | ||
|
|
3de2e65530 | ||
|
|
c5319b6bea | ||
|
|
801ddc0886 | ||
|
|
dfd9196aa7 | ||
|
|
c4362297f5 | ||
|
|
96c23af99c | ||
|
|
6a29dacdeb | ||
|
|
9ddad80f2a |
7
.env
7
.env
@@ -1,3 +1,8 @@
|
||||
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
|
||||
REACT_APP_FORTMATIC_KEY="pk_live_357F77728B8EB880"
|
||||
REACT_APP_AMPLITUDE_TEST_KEY="add-the-real-test-key-if-you-need-to-test-amplitude-events"
|
||||
REACT_APP_AWS_API_REGION="us-east-2"
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,7 +9,6 @@
|
||||
/src/locales/**/pseudo.po
|
||||
|
||||
# generated graphql types
|
||||
/src/graphql/schema/
|
||||
__generated__/
|
||||
|
||||
# dependencies
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
overrideExisting: true
|
||||
schema: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3'
|
||||
documents: 'src/**/!(*.d).{ts,tsx}'
|
||||
generates:
|
||||
./src/graphql/schema/schema.graphql:
|
||||
./src/graphql/thegraph/schema/schema.graphql:
|
||||
plugins:
|
||||
- schema-ast
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -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}]`
|
||||
18
fetch-schema.js
Normal file
18
fetch-schema.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable */
|
||||
require('dotenv').config({ path: '.env.local' })
|
||||
const { exec } = require('child_process')
|
||||
const dataConfig = require('./relay.config')
|
||||
const thegraphConfig = require('./relay_thegraph.config')
|
||||
/* eslint-enable */
|
||||
|
||||
const THEGRAPH_API_URL = 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3'
|
||||
exec(`get-graphql-schema ${THEGRAPH_API_URL} > ${thegraphConfig.schema}`)
|
||||
|
||||
const API_URL = process.env.REACT_APP_GQL_API_URL
|
||||
const API_KEY = process.env.REACT_APP_GQL_API_KEY
|
||||
|
||||
if (API_URL && API_KEY) {
|
||||
exec(`get-graphql-schema ${API_URL} --h X-API-KEY=${API_KEY} > ${dataConfig.schema}`)
|
||||
} else {
|
||||
console.log('REACT_APP_GQL_API_URL or REACT_APP_GQL_API_KEY is missing from env.local')
|
||||
}
|
||||
36
package.json
36
package.json
@@ -8,8 +8,10 @@
|
||||
"contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"",
|
||||
"contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"./node_modules/@uniswap/**/artifacts/contracts/**/*[!dbg].json\"",
|
||||
"contracts:compile": "yarn contracts:compile:abi && yarn contracts:compile:v3",
|
||||
"relay": "relay-compiler",
|
||||
"graphql:generate": "graphql-codegen --config codegen.yml && yarn relay",
|
||||
"relay": "relay-compiler relay.config.js",
|
||||
"relay-thegraph": "relay-compiler relay_thegraph.config.js",
|
||||
"graphql:fetch": "node fetch-schema.js",
|
||||
"graphql:generate": "yarn relay && yarn relay-thegraph",
|
||||
"prei18n:extract": "node prei18n-extract.js",
|
||||
"i18n:extract": "lingui extract --locale en-US",
|
||||
"i18n:compile": "yarn i18n:extract && lingui compile",
|
||||
@@ -21,12 +23,8 @@
|
||||
"lint": "yarn eslint .",
|
||||
"test": "craco test --coverage",
|
||||
"cypress:open": "cypress open --browser chrome --e2e",
|
||||
"cypress:run": "cypress run --browser chrome --e2e"
|
||||
},
|
||||
"relay": {
|
||||
"src": "./src",
|
||||
"language": "typescript",
|
||||
"schema": "./src/graphql/schema/schema.graphql"
|
||||
"cypress:run": "cypress run --browser chrome --e2e",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"jest": {
|
||||
"collectCoverageFrom": [
|
||||
@@ -63,9 +61,6 @@
|
||||
"devDependencies": {
|
||||
"@craco/craco": "6.4.3",
|
||||
"@ethersproject/experimental": "^5.4.0",
|
||||
"@graphql-codegen/cli": "1.21.5",
|
||||
"@graphql-codegen/schema-ast": "^2.5.1",
|
||||
"@graphql-codegen/typescript-rtk-query": "^1.1.1",
|
||||
"@lingui/cli": "^3.9.0",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.1",
|
||||
@@ -95,9 +90,9 @@
|
||||
"@types/wcag-contrast": "^3.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4",
|
||||
"@typescript-eslint/parser": "^4",
|
||||
"babel-plugin-relay": "^14.1.0",
|
||||
"@vanilla-extract/babel-plugin": "^1.1.7",
|
||||
"@vanilla-extract/webpack-plugin": "^2.1.11",
|
||||
"babel-plugin-relay": "^14.1.0",
|
||||
"cypress": "^10.3.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
"eslint": "^7.11.0",
|
||||
@@ -110,6 +105,8 @@
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"jest-styled-components": "^7.0.8",
|
||||
"ms.macro": "^2.0.0",
|
||||
"patch-package": "^6.4.7",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.7.1",
|
||||
"react-scripts": "^4.0.3",
|
||||
"relay-compiler": "^14.1.0",
|
||||
@@ -140,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",
|
||||
@@ -148,7 +145,7 @@
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"@uniswap/v3-sdk": "^3.9.0",
|
||||
"@uniswap/widgets": "^2.1.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",
|
||||
@@ -173,15 +170,15 @@
|
||||
"ajv": "^6.12.3",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
"array.prototype.flatmap": "^1.2.4",
|
||||
"aws4fetch": "^1.0.13",
|
||||
"cids": "^1.0.0",
|
||||
"clsx": "^1.1.1",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"d3": "^7.6.1",
|
||||
"d3-curve-circlecorners": "^0.1.6",
|
||||
"ethers": "^5.1.4",
|
||||
"firebase": "^9.1.3",
|
||||
"focus-visible": "^5.2.0",
|
||||
"fortmatic": "^2.4.0",
|
||||
"get-graphql-schema": "^2.1.2",
|
||||
"graphql": "^16.5.0",
|
||||
"graphql-request": "^3.4.0",
|
||||
"immer": "^9.0.6",
|
||||
@@ -202,6 +199,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-feather": "^2.0.8",
|
||||
"react-ga4": "^1.4.1",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-is": "^17.0.2",
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-popper": "^2.2.3",
|
||||
@@ -217,7 +215,6 @@
|
||||
"rebass": "^4.0.7",
|
||||
"redux": "^4.1.2",
|
||||
"redux-localstorage-simple": "^2.3.1",
|
||||
"relay-hooks": "^7.1.0",
|
||||
"setimmediate": "^1.0.5",
|
||||
"styled-components": "^5.3.5",
|
||||
"tiny-invariant": "^1.2.0",
|
||||
@@ -233,5 +230,10 @@
|
||||
"workbox-precaching": "^6.1.0",
|
||||
"workbox-routing": "^6.1.0",
|
||||
"zustand": "^4.0.0-rc.1"
|
||||
},
|
||||
"engines": {
|
||||
"npm": "please-use-yarn",
|
||||
"node": "14",
|
||||
"yarn": ">=1.22"
|
||||
}
|
||||
}
|
||||
|
||||
22
patches/@vanilla-extract+css+1.7.2.patch
Normal file
22
patches/@vanilla-extract+css+1.7.2.patch
Normal file
@@ -0,0 +1,22 @@
|
||||
diff --git a/node_modules/@vanilla-extract/css/dist/vanilla-extract-css.cjs.dev.js b/node_modules/@vanilla-extract/css/dist/vanilla-extract-css.cjs.dev.js
|
||||
index 6e40061..10283a2 100644
|
||||
--- a/node_modules/@vanilla-extract/css/dist/vanilla-extract-css.cjs.dev.js
|
||||
+++ b/node_modules/@vanilla-extract/css/dist/vanilla-extract-css.cjs.dev.js
|
||||
@@ -177,11 +177,13 @@ function generateIdentifier(debugId) {
|
||||
var fileScopeHash = hash__default["default"](packageName ? "".concat(packageName).concat(filePath) : filePath);
|
||||
var identifier = "".concat(fileScopeHash).concat(refCount);
|
||||
|
||||
- if (adapter_dist_vanillaExtractCssAdapter.getIdentOption() === 'debug') {
|
||||
- var devPrefix = getDevPrefix(debugId);
|
||||
+ if (process.env.VANILLA_EXTRACT_DEV_PREFIX) {
|
||||
+ if (adapter_dist_vanillaExtractCssAdapter.getIdentOption() === 'debug') {
|
||||
+ var devPrefix = getDevPrefix(debugId);
|
||||
|
||||
- if (devPrefix) {
|
||||
- identifier = "".concat(devPrefix, "__").concat(identifier);
|
||||
+ if (devPrefix) {
|
||||
+ identifier = "".concat(devPrefix, "__").concat(identifier);
|
||||
+ }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const exec = require('child_process').exec
|
||||
const { exec } = require('child_process')
|
||||
const isWindows = process.platform === 'win32' || /^(msys|cygwin)$/.test(process.env.OSTYPE)
|
||||
|
||||
if (isWindows) {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#ff007a" />
|
||||
<meta name="fortmatic-site-verification" content="j93LgcVZk79qcgyo" />
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
|
||||
6
relay.config.js
Normal file
6
relay.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
src: './src',
|
||||
language: 'typescript',
|
||||
schema: './src/graphql/data/schema.graphql',
|
||||
exclude: ['**/node_modules/**', '**/__mocks__/**', '**/__generated__/**', '**/thegraph/**'],
|
||||
}
|
||||
9
relay_thegraph.config.js
Normal file
9
relay_thegraph.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const defaultConfig = require('./relay.config')
|
||||
|
||||
module.exports = {
|
||||
src: defaultConfig.src,
|
||||
language: defaultConfig.language,
|
||||
schema: './src/graphql/thegraph/schema.graphql',
|
||||
exclude: ['**/node_modules/**', '**/__mocks__/**', '**/__generated__/**', '**/data/**'],
|
||||
}
|
||||
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 |
BIN
src/assets/images/tokensPromoDark.png
Normal file
BIN
src/assets/images/tokensPromoDark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
src/assets/images/tokensPromoLight.png
Normal file
BIN
src/assets/images/tokensPromoLight.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
3
src/assets/svg/socks.svg
Normal file
3
src/assets/svg/socks.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="10" height="12" viewBox="0 0 10 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.75 11C4.75 11 5.22377 11.1391 5.80923 10.5828L7.64433 8.65908C8.35405 7.91508 8.74998 6.92678 8.74989 5.89856C8.7498 4.72716 8.74971 3.31706 8.74991 2.50009C8.74996 2.22391 8.77618 2 8.5 2H8.25M6.74898 5.75L6.74979 2L6.74991 1.50009C6.74996 1.22391 6.52609 1 6.24991 1H4.25167C3.97553 1 3.75167 1.22386 3.75167 1.5V4.75039C3.75167 5.29859 3.52665 5.82276 3.12922 6.20034L1.6891 7.56856C1.10364 8.12478 1.10363 9.0266 1.68909 9.58283C2.12197 9.99409 2.75372 10.1013 3.29025 9.90438C3.47937 9.83497 3.65665 9.72779 3.80923 9.58283L5.80923 7.6827M6.74898 5.75L6.7487 6.36119C6.74861 6.63517 6.63611 6.89711 6.43748 7.08582L5.80923 7.6827M6.74898 5.75H6.4384C5.67845 5.75 5.19623 6.56419 5.56146 7.23061L5.80923 7.6827" stroke="white" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 871 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#99A1BD" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 294 B After Width: | Height: | Size: 300 B |
@@ -24,7 +24,7 @@ const HeaderRow = styled.div`
|
||||
padding: 1rem 1rem;
|
||||
font-weight: 500;
|
||||
color: ${(props) => (props.color === 'blue' ? ({ theme }) => theme.deprecated_primary1 : 'inherit')};
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
padding: 1rem;
|
||||
`};
|
||||
`
|
||||
@@ -74,7 +74,7 @@ const AccountGroupingRow = styled.div`
|
||||
|
||||
const AccountSection = styled.div`
|
||||
padding: 0rem 1rem;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1.5rem 1rem;`};
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`padding: 0rem 1rem 1.5rem 1rem;`};
|
||||
`
|
||||
|
||||
const YourAccount = styled.div`
|
||||
@@ -145,7 +145,7 @@ const CloseIcon = styled.div`
|
||||
top: 14px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
88
src/components/AccountDetailsV2/LogoView.tsx
Normal file
88
src/components/AccountDetailsV2/LogoView.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { UNI_ADDRESS } from 'constants/addresses'
|
||||
import { TransactionInfo, TransactionType } from 'state/transactions/types'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
|
||||
import { nativeOnChain } from '../../constants/tokens'
|
||||
import { useCurrency } from '../../hooks/Tokens'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
|
||||
const CurrencyWrap = styled.div`
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
`
|
||||
|
||||
const CurrencyWrapStyles = css`
|
||||
position: absolute;
|
||||
height: 24px;
|
||||
`
|
||||
|
||||
const CurrencyLogoWrap = styled.span<{ isCentered: boolean }>`
|
||||
${CurrencyWrapStyles};
|
||||
left: ${({ isCentered }) => (isCentered ? '50%' : '0')};
|
||||
top: ${({ isCentered }) => (isCentered ? '50%' : '0')};
|
||||
transform: ${({ isCentered }) => isCentered && 'translate(-50%, -50%)'};
|
||||
`
|
||||
const CurrencyLogoWrapTwo = styled.span`
|
||||
${CurrencyWrapStyles};
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
`
|
||||
|
||||
interface CurrencyPair {
|
||||
currencyId0: string | undefined
|
||||
currencyId1: string | undefined
|
||||
}
|
||||
|
||||
const getCurrency = ({ info, chainId }: { info: TransactionInfo; chainId: number | undefined }): CurrencyPair => {
|
||||
switch (info.type) {
|
||||
case TransactionType.ADD_LIQUIDITY_V3_POOL:
|
||||
case TransactionType.REMOVE_LIQUIDITY_V3:
|
||||
case TransactionType.CREATE_V3_POOL:
|
||||
const { baseCurrencyId, quoteCurrencyId } = info
|
||||
return { currencyId0: baseCurrencyId, currencyId1: quoteCurrencyId }
|
||||
case TransactionType.SWAP:
|
||||
const { inputCurrencyId, outputCurrencyId } = info
|
||||
return { currencyId0: inputCurrencyId, currencyId1: outputCurrencyId }
|
||||
case TransactionType.WRAP:
|
||||
const { unwrapped } = info
|
||||
const native = info.chainId ? nativeOnChain(info.chainId) : undefined
|
||||
const base = 'ETH'
|
||||
const wrappedCurrency = native?.wrapped.address ?? 'WETH'
|
||||
return { currencyId0: unwrapped ? wrappedCurrency : base, currencyId1: unwrapped ? base : wrappedCurrency }
|
||||
case TransactionType.COLLECT_FEES:
|
||||
const { currencyId0, currencyId1 } = info
|
||||
return { currencyId0, currencyId1 }
|
||||
case TransactionType.APPROVAL:
|
||||
return { currencyId0: info.tokenAddress, currencyId1: undefined }
|
||||
case TransactionType.CLAIM:
|
||||
const uniAddress = chainId ? UNI_ADDRESS[chainId] : undefined
|
||||
return { currencyId0: uniAddress, currencyId1: undefined }
|
||||
default:
|
||||
return { currencyId0: undefined, currencyId1: undefined }
|
||||
}
|
||||
}
|
||||
|
||||
const LogoView = ({ info }: { info: TransactionInfo }) => {
|
||||
const { chainId } = useWeb3React()
|
||||
const { currencyId0, currencyId1 } = getCurrency({ info, chainId })
|
||||
const currency0 = useCurrency(currencyId0)
|
||||
const currency1 = useCurrency(currencyId1)
|
||||
const isCentered = !(currency0 && currency1)
|
||||
|
||||
return (
|
||||
<CurrencyWrap>
|
||||
<CurrencyLogoWrap isCentered={isCentered}>
|
||||
<CurrencyLogo size="24px" currency={currency0} />
|
||||
</CurrencyLogoWrap>
|
||||
{!isCentered && (
|
||||
<CurrencyLogoWrapTwo>
|
||||
<CurrencyLogo size="24px" currency={currency1} />
|
||||
</CurrencyLogoWrapTwo>
|
||||
)}
|
||||
</CurrencyWrap>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogoView
|
||||
337
src/components/AccountDetailsV2/TransactionBody.tsx
Normal file
337
src/components/AccountDetailsV2/TransactionBody.tsx
Normal file
@@ -0,0 +1,337 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Fraction, TradeType } from '@uniswap/sdk-core'
|
||||
import JSBI from 'jsbi'
|
||||
import {
|
||||
AddLiquidityV3PoolTransactionInfo,
|
||||
ApproveTransactionInfo,
|
||||
ClaimTransactionInfo,
|
||||
CollectFeesTransactionInfo,
|
||||
ExactInputSwapTransactionInfo,
|
||||
ExactOutputSwapTransactionInfo,
|
||||
RemoveLiquidityV3TransactionInfo,
|
||||
TransactionInfo,
|
||||
TransactionType,
|
||||
WrapTransactionInfo,
|
||||
} from 'state/transactions/types'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { nativeOnChain } from '../../constants/tokens'
|
||||
import { useCurrency, useToken } from '../../hooks/Tokens'
|
||||
import useENSName from '../../hooks/useENSName'
|
||||
import { shortenAddress } from '../../utils'
|
||||
import { TransactionState } from './index'
|
||||
|
||||
const HighlightText = styled.span`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
font-weight: 600;
|
||||
`
|
||||
|
||||
const BodyWrap = styled.div`
|
||||
line-height: 20px;
|
||||
`
|
||||
|
||||
interface ActionProps {
|
||||
pending: JSX.Element
|
||||
success: JSX.Element
|
||||
failed: JSX.Element
|
||||
transactionState: TransactionState
|
||||
}
|
||||
|
||||
const Action = ({ pending, success, failed, transactionState }: ActionProps) => {
|
||||
switch (transactionState) {
|
||||
case TransactionState.Failed:
|
||||
return failed
|
||||
case TransactionState.Success:
|
||||
return success
|
||||
default:
|
||||
return pending
|
||||
}
|
||||
}
|
||||
|
||||
const formatAmount = (amountRaw: string, decimals: number, sigFigs: number): string =>
|
||||
new Fraction(amountRaw, JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(decimals))).toSignificant(sigFigs)
|
||||
|
||||
const FailedText = ({ transactionState }: { transactionState: TransactionState }) =>
|
||||
transactionState === TransactionState.Failed ? <Trans>failed</Trans> : <span />
|
||||
|
||||
const FormattedCurrencyAmount = ({
|
||||
rawAmount,
|
||||
currencyId,
|
||||
sigFigs = 2,
|
||||
}: {
|
||||
rawAmount: string
|
||||
currencyId: string
|
||||
sigFigs: number
|
||||
}) => {
|
||||
const currency = useCurrency(currencyId)
|
||||
|
||||
return currency ? (
|
||||
<HighlightText>
|
||||
{formatAmount(rawAmount, currency.decimals, sigFigs)} {currency.symbol}
|
||||
</HighlightText>
|
||||
) : null
|
||||
}
|
||||
|
||||
const getRawAmounts = (
|
||||
info: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo
|
||||
): { rawAmountFrom: string; rawAmountTo: string } => {
|
||||
return info.tradeType === TradeType.EXACT_INPUT
|
||||
? { rawAmountFrom: info.inputCurrencyAmountRaw, rawAmountTo: info.expectedOutputCurrencyAmountRaw }
|
||||
: { rawAmountFrom: info.expectedInputCurrencyAmountRaw, rawAmountTo: info.outputCurrencyAmountRaw }
|
||||
}
|
||||
|
||||
const SwapSummary = ({
|
||||
info,
|
||||
transactionState,
|
||||
}: {
|
||||
info: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo
|
||||
transactionState: TransactionState
|
||||
}) => {
|
||||
const actionProps = {
|
||||
transactionState,
|
||||
pending: <Trans>Swapping</Trans>,
|
||||
success: <Trans>Swapped</Trans>,
|
||||
failed: <Trans>Swap</Trans>,
|
||||
}
|
||||
const { rawAmountFrom, rawAmountTo } = getRawAmounts(info)
|
||||
|
||||
return (
|
||||
<BodyWrap>
|
||||
<Action {...actionProps} />{' '}
|
||||
<FormattedCurrencyAmount rawAmount={rawAmountFrom} currencyId={info.inputCurrencyId} sigFigs={2} />{' '}
|
||||
<Trans>for </Trans>{' '}
|
||||
<FormattedCurrencyAmount rawAmount={rawAmountTo} currencyId={info.outputCurrencyId} sigFigs={2} />{' '}
|
||||
<FailedText transactionState={transactionState} />
|
||||
</BodyWrap>
|
||||
)
|
||||
}
|
||||
|
||||
const AddLiquidityV3PoolSummary = ({
|
||||
info,
|
||||
transactionState,
|
||||
}: {
|
||||
info: AddLiquidityV3PoolTransactionInfo
|
||||
transactionState: TransactionState
|
||||
}) => {
|
||||
const { createPool, quoteCurrencyId, baseCurrencyId } = info
|
||||
|
||||
const actionProps = {
|
||||
transactionState,
|
||||
pending: <Trans>Adding</Trans>,
|
||||
success: <Trans>Added</Trans>,
|
||||
failed: <Trans>Add</Trans>,
|
||||
}
|
||||
|
||||
return (
|
||||
<BodyWrap>
|
||||
{createPool ? (
|
||||
<CreateV3PoolSummary info={info} transactionState={transactionState} />
|
||||
) : (
|
||||
<>
|
||||
<Action {...actionProps} />{' '}
|
||||
<FormattedCurrencyAmount rawAmount={info.expectedAmountBaseRaw} currencyId={baseCurrencyId} sigFigs={2} />{' '}
|
||||
<Trans>and</Trans>{' '}
|
||||
<FormattedCurrencyAmount rawAmount={info.expectedAmountQuoteRaw} currencyId={quoteCurrencyId} sigFigs={2} />
|
||||
</>
|
||||
)}{' '}
|
||||
<FailedText transactionState={transactionState} />
|
||||
</BodyWrap>
|
||||
)
|
||||
}
|
||||
|
||||
const RemoveLiquidityV3Summary = ({
|
||||
info: { baseCurrencyId, quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw },
|
||||
transactionState,
|
||||
}: {
|
||||
info: RemoveLiquidityV3TransactionInfo
|
||||
transactionState: TransactionState
|
||||
}) => {
|
||||
const actionProps = {
|
||||
transactionState,
|
||||
pending: <Trans>Removing</Trans>,
|
||||
success: <Trans>Removed</Trans>,
|
||||
failed: <Trans>Remove</Trans>,
|
||||
}
|
||||
|
||||
return (
|
||||
<BodyWrap>
|
||||
<Action {...actionProps} />{' '}
|
||||
<FormattedCurrencyAmount rawAmount={expectedAmountBaseRaw} currencyId={baseCurrencyId} sigFigs={2} />{' '}
|
||||
<Trans>and</Trans>{' '}
|
||||
<FormattedCurrencyAmount rawAmount={expectedAmountQuoteRaw} currencyId={quoteCurrencyId} sigFigs={2} />{' '}
|
||||
<FailedText transactionState={transactionState} />
|
||||
</BodyWrap>
|
||||
)
|
||||
}
|
||||
|
||||
const CreateV3PoolSummary = ({
|
||||
info: { baseCurrencyId, quoteCurrencyId },
|
||||
transactionState,
|
||||
}: {
|
||||
info: AddLiquidityV3PoolTransactionInfo
|
||||
transactionState: TransactionState
|
||||
}) => {
|
||||
const baseCurrency = useCurrency(baseCurrencyId)
|
||||
const quoteCurrency = useCurrency(quoteCurrencyId)
|
||||
const actionProps = {
|
||||
transactionState,
|
||||
pending: <Trans>Creating</Trans>,
|
||||
success: <Trans>Created</Trans>,
|
||||
failed: <Trans>Create</Trans>,
|
||||
}
|
||||
|
||||
return (
|
||||
<BodyWrap>
|
||||
<Action {...actionProps} />{' '}
|
||||
<HighlightText>
|
||||
{baseCurrency?.symbol}/{quoteCurrency?.symbol}{' '}
|
||||
</HighlightText>
|
||||
<Trans>Pool</Trans> <FailedText transactionState={transactionState} />
|
||||
</BodyWrap>
|
||||
)
|
||||
}
|
||||
|
||||
const CollectFeesSummary = ({
|
||||
info,
|
||||
transactionState,
|
||||
}: {
|
||||
info: CollectFeesTransactionInfo
|
||||
transactionState: TransactionState
|
||||
}) => {
|
||||
const { currencyId0, expectedCurrencyOwed0 = '0', expectedCurrencyOwed1 = '0', currencyId1 } = info
|
||||
const actionProps = {
|
||||
transactionState,
|
||||
pending: <Trans>Collecting</Trans>,
|
||||
success: <Trans>Collected</Trans>,
|
||||
failed: <Trans>Collect</Trans>,
|
||||
}
|
||||
|
||||
return (
|
||||
<BodyWrap>
|
||||
<Action {...actionProps} />{' '}
|
||||
<FormattedCurrencyAmount rawAmount={expectedCurrencyOwed0} currencyId={currencyId0} sigFigs={2} />{' '}
|
||||
<Trans>and</Trans>{' '}
|
||||
<FormattedCurrencyAmount rawAmount={expectedCurrencyOwed1} currencyId={currencyId1} sigFigs={2} />{' '}
|
||||
<Trans>fees</Trans> <FailedText transactionState={transactionState} />
|
||||
</BodyWrap>
|
||||
)
|
||||
}
|
||||
|
||||
const ApprovalSummary = ({
|
||||
info,
|
||||
transactionState,
|
||||
}: {
|
||||
info: ApproveTransactionInfo
|
||||
transactionState: TransactionState
|
||||
}) => {
|
||||
const token = useToken(info.tokenAddress)
|
||||
const actionProps = {
|
||||
transactionState,
|
||||
pending: <Trans>Approving</Trans>,
|
||||
success: <Trans>Approved</Trans>,
|
||||
failed: <Trans>Approve</Trans>,
|
||||
}
|
||||
|
||||
return (
|
||||
<BodyWrap>
|
||||
<Action {...actionProps} /> <HighlightText>{token?.symbol}</HighlightText>{' '}
|
||||
<FailedText transactionState={transactionState} />
|
||||
</BodyWrap>
|
||||
)
|
||||
}
|
||||
|
||||
const ClaimSummary = ({
|
||||
info: { recipient, uniAmountRaw },
|
||||
transactionState,
|
||||
}: {
|
||||
info: ClaimTransactionInfo
|
||||
transactionState: TransactionState
|
||||
}) => {
|
||||
const { ENSName } = useENSName()
|
||||
const actionProps = {
|
||||
transactionState,
|
||||
pending: <Trans>Claiming</Trans>,
|
||||
success: <Trans>Claimed</Trans>,
|
||||
failed: <Trans>Claim</Trans>,
|
||||
}
|
||||
|
||||
return (
|
||||
<BodyWrap>
|
||||
{uniAmountRaw && (
|
||||
<>
|
||||
<Action {...actionProps} />{' '}
|
||||
<HighlightText>
|
||||
{formatAmount(uniAmountRaw, 18, 4)}
|
||||
UNI{' '}
|
||||
</HighlightText>{' '}
|
||||
<Trans>for</Trans> <HighlightText>{ENSName ?? shortenAddress(recipient)}</HighlightText>
|
||||
</>
|
||||
)}{' '}
|
||||
<FailedText transactionState={transactionState} />
|
||||
</BodyWrap>
|
||||
)
|
||||
}
|
||||
|
||||
const WrapSummary = ({
|
||||
info: { chainId, currencyAmountRaw, unwrapped },
|
||||
transactionState,
|
||||
}: {
|
||||
info: WrapTransactionInfo
|
||||
transactionState: TransactionState
|
||||
}) => {
|
||||
const native = chainId ? nativeOnChain(chainId) : undefined
|
||||
const from = unwrapped ? native?.wrapped.symbol ?? 'WETH' : native?.symbol ?? 'ETH'
|
||||
const to = unwrapped ? native?.symbol ?? 'ETH' : native?.wrapped.symbol ?? 'WETH'
|
||||
|
||||
const amount = formatAmount(currencyAmountRaw, 18, 6)
|
||||
const actionProps = unwrapped
|
||||
? {
|
||||
transactionState,
|
||||
pending: <Trans>Unwrapping</Trans>,
|
||||
success: <Trans>Unwrapped</Trans>,
|
||||
failed: <Trans>Unwrap</Trans>,
|
||||
}
|
||||
: {
|
||||
transactionState,
|
||||
pending: <Trans>Wrapping</Trans>,
|
||||
success: <Trans>Wrapped</Trans>,
|
||||
failed: <Trans>Wrap</Trans>,
|
||||
}
|
||||
|
||||
return (
|
||||
<BodyWrap>
|
||||
<Action {...actionProps} />{' '}
|
||||
<HighlightText>
|
||||
{amount} {from}
|
||||
</HighlightText>{' '}
|
||||
<Trans>to</Trans>{' '}
|
||||
<HighlightText>
|
||||
{amount} {to}
|
||||
</HighlightText>{' '}
|
||||
<FailedText transactionState={transactionState} />
|
||||
</BodyWrap>
|
||||
)
|
||||
}
|
||||
|
||||
const TransactionBody = ({ info, transactionState }: { info: TransactionInfo; transactionState: TransactionState }) => {
|
||||
switch (info.type) {
|
||||
case TransactionType.SWAP:
|
||||
return <SwapSummary info={info} transactionState={transactionState} />
|
||||
case TransactionType.ADD_LIQUIDITY_V3_POOL:
|
||||
return <AddLiquidityV3PoolSummary info={info} transactionState={transactionState} />
|
||||
case TransactionType.REMOVE_LIQUIDITY_V3:
|
||||
return <RemoveLiquidityV3Summary info={info} transactionState={transactionState} />
|
||||
case TransactionType.WRAP:
|
||||
return <WrapSummary info={info} transactionState={transactionState} />
|
||||
case TransactionType.COLLECT_FEES:
|
||||
return <CollectFeesSummary info={info} transactionState={transactionState} />
|
||||
case TransactionType.APPROVAL:
|
||||
return <ApprovalSummary info={info} transactionState={transactionState} />
|
||||
case TransactionType.CLAIM:
|
||||
return <ClaimSummary info={info} transactionState={transactionState} />
|
||||
default:
|
||||
return <span />
|
||||
}
|
||||
}
|
||||
|
||||
export default TransactionBody
|
||||
90
src/components/AccountDetailsV2/index.tsx
Normal file
90
src/components/AccountDetailsV2/index.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { getChainInfoOrDefault } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { useMemo } from 'react'
|
||||
import { AlertTriangle, CheckCircle } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { colors } from 'theme/colors'
|
||||
|
||||
import { TransactionDetails } from '../../state/transactions/types'
|
||||
import Loader from '../Loader'
|
||||
import LogoView from './LogoView'
|
||||
import TransactionBody from './TransactionBody'
|
||||
|
||||
export enum TransactionState {
|
||||
Pending,
|
||||
Success,
|
||||
Failed,
|
||||
}
|
||||
|
||||
const Grid = styled.a`
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
grid-template-columns: 44px auto 24px;
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
border-bottom: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
||||
padding: 12px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.backgroundModule};
|
||||
transition: 250ms background-color ease;
|
||||
}
|
||||
`
|
||||
|
||||
const TextContainer = styled.span`
|
||||
font-size: 14px;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
`
|
||||
|
||||
const IconStyleWrap = styled.span`
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
margin-left: auto;
|
||||
height: 16px;
|
||||
`
|
||||
|
||||
export const TransactionSummary = ({ transactionDetails }: { transactionDetails: TransactionDetails }) => {
|
||||
const { chainId = 1 } = useWeb3React()
|
||||
const tx = transactionDetails
|
||||
const { explorer } = getChainInfoOrDefault(chainId ? chainId : SupportedChainId.MAINNET)
|
||||
const { info, receipt, hash } = tx
|
||||
|
||||
const transactionState = useMemo(() => {
|
||||
const pending = !receipt
|
||||
const success = !pending && tx && (receipt?.status === 1 || typeof receipt?.status === 'undefined')
|
||||
const transactionState = pending
|
||||
? TransactionState.Pending
|
||||
: success
|
||||
? TransactionState.Success
|
||||
: TransactionState.Failed
|
||||
|
||||
return transactionState
|
||||
}, [receipt, tx])
|
||||
|
||||
const link = `${explorer}tx/${hash}`
|
||||
|
||||
return chainId ? (
|
||||
<Grid href={link} target="_blank">
|
||||
<LogoView info={info} />
|
||||
<TextContainer as="span">
|
||||
<TransactionBody info={info} transactionState={transactionState} />
|
||||
</TextContainer>
|
||||
{transactionState === TransactionState.Pending ? (
|
||||
<IconStyleWrap>
|
||||
<Loader />
|
||||
</IconStyleWrap>
|
||||
) : transactionState === TransactionState.Success ? (
|
||||
<IconStyleWrap>
|
||||
<CheckCircle color={colors.green200} size="16px" />
|
||||
</IconStyleWrap>
|
||||
) : (
|
||||
<IconStyleWrap>
|
||||
<AlertTriangle color={colors.gold200} size="16px" />
|
||||
</IconStyleWrap>
|
||||
)}
|
||||
</Grid>
|
||||
) : null
|
||||
}
|
||||
@@ -37,16 +37,9 @@ export enum CUSTOM_USER_PROPERTIES {
|
||||
SCREEN_RESOLUTION_HEIGHT = 'screen_resolution_height',
|
||||
SCREEN_RESOLUTION_WIDTH = 'screen_resolution_width',
|
||||
WALLET_ADDRESS = 'wallet_address',
|
||||
WALLET_NATIVE_CURRENCY_BALANCE_USD = 'wallet_native_currency_balance_usd',
|
||||
WALLET_TOKENS_ADDRESSES = 'wallet_tokens_addresses',
|
||||
WALLET_TOKENS_SYMBOLS = 'wallet_tokens_symbols',
|
||||
WALLET_TYPE = 'wallet_type',
|
||||
}
|
||||
|
||||
export enum CUSTOM_USER_PROPERTY_SUFFIXES {
|
||||
WALLET_TOKEN_AMOUNT_SUFFIX = '_token_amount',
|
||||
}
|
||||
|
||||
export enum BROWSER {
|
||||
FIREFOX = 'Mozilla Firefox',
|
||||
SAMSUNG = 'Samsung Internet',
|
||||
|
||||
@@ -176,23 +176,28 @@ export const ButtonOutlined = styled(BaseButton)`
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonYellow = styled(BaseButton)`
|
||||
background-color: ${({ theme }) => theme.deprecated_yellow3};
|
||||
color: white;
|
||||
export const ButtonYellow = styled(BaseButton)<{ redesignFlag?: boolean }>`
|
||||
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.accentWarningSoft : theme.deprecated_yellow3)};
|
||||
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.accentWarning : 'white')};
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.05, theme.deprecated_yellow3)};
|
||||
background-color: ${({ theme }) => darken(0.05, theme.deprecated_yellow3)};
|
||||
box-shadow: ${({ theme, redesignFlag }) => !redesignFlag && `0 0 0 1pt ${theme.deprecated_yellow3}`};
|
||||
background-color: ${({ theme, redesignFlag }) =>
|
||||
redesignFlag ? theme.accentWarningSoft : darken(0.05, theme.deprecated_yellow3)};
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => darken(0.05, theme.deprecated_yellow3)};
|
||||
background: ${({ theme, redesignFlag }) => redesignFlag && theme.stateOverlayHover};
|
||||
mix-blend-mode: ${({ redesignFlag }) => redesignFlag && 'normal'};
|
||||
background-color: ${({ theme, redesignFlag }) => !redesignFlag && darken(0.05, theme.deprecated_yellow3)};
|
||||
}
|
||||
&:active {
|
||||
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.1, theme.deprecated_yellow3)};
|
||||
background-color: ${({ theme }) => darken(0.1, theme.deprecated_yellow3)};
|
||||
box-shadow: ${({ theme, redesignFlag }) => !redesignFlag && `0 0 0 1pt ${darken(0.1, theme.deprecated_yellow3)}`};
|
||||
background-color: ${({ theme, redesignFlag }) =>
|
||||
redesignFlag ? theme.accentWarningSoft : darken(0.1, theme.deprecated_yellow3)};
|
||||
}
|
||||
&:disabled {
|
||||
background-color: ${({ theme }) => theme.deprecated_yellow3};
|
||||
opacity: 50%;
|
||||
background-color: ${({ theme, redesignFlag }) =>
|
||||
redesignFlag ? theme.accentWarningSoft : theme.deprecated_yellow3};
|
||||
opacity: ${({ redesignFlag }) => (redesignFlag ? '60%' : '50%')};
|
||||
cursor: auto;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Group } from '@visx/group'
|
||||
import { LinePath } from '@visx/shape'
|
||||
import { CurveFactory } from 'd3'
|
||||
import { radius } from 'd3-curve-circlecorners'
|
||||
import React from 'react'
|
||||
import { ReactNode } from 'react'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
@@ -12,7 +11,7 @@ interface LineChartProps<T> {
|
||||
getX: (t: T) => number
|
||||
getY: (t: T) => number
|
||||
marginTop?: number
|
||||
curve?: CurveFactory
|
||||
curve: CurveFactory
|
||||
color?: Color
|
||||
strokeWidth: number
|
||||
children?: ReactNode
|
||||
@@ -37,7 +36,7 @@ function LineChart<T>({
|
||||
<svg width={width} height={height}>
|
||||
<Group top={marginTop}>
|
||||
<LinePath
|
||||
curve={curve ?? radius(0.25)}
|
||||
curve={curve}
|
||||
stroke={color ?? theme.accentAction}
|
||||
strokeWidth={strokeWidth}
|
||||
data={data}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { scaleLinear } from 'd3'
|
||||
import { curveCardinalOpen, scaleLinear } from 'd3'
|
||||
import React from 'react'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
|
||||
@@ -37,6 +37,7 @@ function SparklineChart({ width, height }: SparklineChartProps) {
|
||||
data={pricePoints}
|
||||
getX={(p: PricePoint) => timeScale(p.timestamp)}
|
||||
getY={(p: PricePoint) => rdScale(p.value)}
|
||||
curve={curveCardinalOpen.tension(0.9)}
|
||||
marginTop={0}
|
||||
color={isPositive ? theme.accentSuccess : theme.accentFailure}
|
||||
strokeWidth={1.5}
|
||||
|
||||
423
src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx
Normal file
423
src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx
Normal file
@@ -0,0 +1,423 @@
|
||||
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 { AutoColumn } from 'components/Column'
|
||||
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
|
||||
import { darken } from 'polished'
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
import { Lock } from 'react-feather'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
|
||||
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
|
||||
import { useCurrencyBalance } from '../../state/connection/hooks'
|
||||
import { ThemedText } from '../../theme'
|
||||
import { ButtonGray } from '../Button'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import DoubleCurrencyLogo from '../DoubleLogo'
|
||||
import { Input as NumericalInput } from '../NumericalInput'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import CurrencySearchModal from '../SearchModal/CurrencySearchModal'
|
||||
import { FiatValue } from './FiatValue'
|
||||
|
||||
const InputPanel = styled.div<{ hideInput?: boolean; redesignFlag: boolean }>`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
position: relative;
|
||||
border-radius: ${({ hideInput }) => (hideInput ? '16px' : '20px')};
|
||||
background-color: ${({ theme, redesignFlag, hideInput }) =>
|
||||
redesignFlag ? 'transparent' : hideInput ? 'transparent' : theme.deprecated_bg2};
|
||||
z-index: 1;
|
||||
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
|
||||
transition: height 1s ease;
|
||||
will-change: height;
|
||||
`
|
||||
|
||||
const FixedContainer = styled.div<{ redesignFlag: boolean }>`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
border-radius: 20px;
|
||||
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_bg2)};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
`
|
||||
|
||||
const Container = styled.div<{ hideInput: boolean; disabled: boolean; redesignFlag: boolean }>`
|
||||
min-height: ${({ redesignFlag }) => redesignFlag && '69px'};
|
||||
border-radius: ${({ hideInput }) => (hideInput ? '16px' : '20px')};
|
||||
border: 1px solid ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_bg0)};
|
||||
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_bg1)};
|
||||
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
|
||||
${({ theme, hideInput, disabled, redesignFlag }) =>
|
||||
!redesignFlag &&
|
||||
!disabled &&
|
||||
`
|
||||
:focus,
|
||||
:hover {
|
||||
border: 1px solid ${hideInput ? ' transparent' : theme.deprecated_bg3};
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
const CurrencySelect = styled(ButtonGray)<{
|
||||
visible: boolean
|
||||
selected: boolean
|
||||
hideInput?: boolean
|
||||
disabled?: boolean
|
||||
redesignFlag: boolean
|
||||
}>`
|
||||
align-items: center;
|
||||
background-color: ${({ selected, theme, redesignFlag }) =>
|
||||
redesignFlag
|
||||
? selected
|
||||
? theme.backgroundSurface
|
||||
: theme.accentAction
|
||||
: selected
|
||||
? theme.deprecated_bg2
|
||||
: theme.deprecated_primary1};
|
||||
opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)};
|
||||
box-shadow: ${({ selected }) => (selected ? 'none' : '0px 6px 10px rgba(0, 0, 0, 0.075)')};
|
||||
color: ${({ selected, theme }) => (selected ? theme.deprecated_text1 : theme.deprecated_white)};
|
||||
cursor: pointer;
|
||||
height: ${({ hideInput, redesignFlag }) => (redesignFlag ? 'unset' : hideInput ? '2.8rem' : '2.4rem')};
|
||||
border-radius: 16px;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
|
||||
padding: ${({ selected, redesignFlag }) =>
|
||||
redesignFlag ? (selected ? '4px 8px 4px 4px' : '6px 6px 6px 8px') : '0 8px'};
|
||||
gap: ${({ redesignFlag }) => (redesignFlag ? '8px' : '0px')};
|
||||
justify-content: space-between;
|
||||
margin-left: ${({ hideInput }) => (hideInput ? '0' : '12px')};
|
||||
:focus,
|
||||
:hover {
|
||||
background-color: ${({ selected, theme, redesignFlag }) =>
|
||||
selected
|
||||
? redesignFlag
|
||||
? theme.backgroundSurface
|
||||
: theme.deprecated_bg3
|
||||
: darken(0.05, theme.deprecated_primary1)};
|
||||
}
|
||||
visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')};
|
||||
`
|
||||
const InputCurrencySelect = styled(CurrencySelect)<{ redesignFlag: boolean }>`
|
||||
background-color: ${({ theme, selected, redesignFlag }) =>
|
||||
redesignFlag && (selected ? theme.backgroundModule : theme.accentAction)};
|
||||
:focus,
|
||||
:hover {
|
||||
background-color: ${({ selected, theme, redesignFlag }) =>
|
||||
selected
|
||||
? redesignFlag
|
||||
? theme.backgroundInteractive
|
||||
: theme.deprecated_bg3
|
||||
: darken(0.05, theme.deprecated_primary1)};
|
||||
}
|
||||
`
|
||||
|
||||
const InputRow = styled.div<{ selected: boolean; redesignFlag: boolean }>`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: ${({ selected, redesignFlag }) =>
|
||||
redesignFlag ? '0px' : selected ? ' 1rem 1rem 0.75rem 1rem' : '1rem 1rem 1rem 1rem'};
|
||||
`
|
||||
|
||||
const LabelRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.deprecated_text1};
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
padding: 0 1rem 1rem;
|
||||
|
||||
span:hover {
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => darken(0.2, theme.deprecated_text2)};
|
||||
}
|
||||
`
|
||||
|
||||
const FiatRow = styled(LabelRow)<{ redesignFlag: boolean }>`
|
||||
justify-content: flex-end;
|
||||
min-height: ${({ redesignFlag }) => redesignFlag && '32px'};
|
||||
padding: ${({ redesignFlag }) => redesignFlag && '8px 0px'};
|
||||
height: ${({ redesignFlag }) => !redesignFlag && '24px'};
|
||||
`
|
||||
|
||||
const NoBalanceState = styled.div`
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
font-weight: 400;
|
||||
justify-content: space-between;
|
||||
padding: 0px 4px 1px 4px;
|
||||
`
|
||||
const NoBalanceDash = styled.span`
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
font-variant: small-caps;
|
||||
font-feature-settings: 'pnum' on, 'lnum' on;
|
||||
`
|
||||
|
||||
const Aligner = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const StyledDropDown = styled(DropDown)<{ selected: boolean; redesignFlag: boolean }>`
|
||||
margin: 0 0.25rem 0 0.35rem;
|
||||
height: 35%;
|
||||
margin-left: ${({ redesignFlag }) => redesignFlag && '8px'};
|
||||
|
||||
path {
|
||||
stroke: ${({ selected, theme }) => (selected ? theme.deprecated_text1 : theme.deprecated_white)};
|
||||
stroke-width: ${({ redesignFlag }) => (redesignFlag ? '2px' : '1.5px')};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledTokenName = styled.span<{ active?: boolean; redesignFlag: boolean }>`
|
||||
${({ active }) => (active ? ' margin: 0 0.25rem 0 0.25rem;' : ' margin: 0 0.25rem 0 0.25rem;')}
|
||||
font-size: ${({ active }) => (active ? '18px' : '18px')};
|
||||
font-weight: ${({ redesignFlag }) => (redesignFlag ? '600' : '500')};
|
||||
`
|
||||
|
||||
const StyledBalanceMax = styled.button<{ disabled?: boolean; redesignFlag: boolean }>`
|
||||
background-color: transparent;
|
||||
background-color: ${({ theme, redesignFlag }) => !redesignFlag && theme.deprecated_primary5};
|
||||
border: none;
|
||||
text-transform: ${({ redesignFlag }) => !redesignFlag && 'uppercase'};
|
||||
border-radius: ${({ redesignFlag }) => !redesignFlag && '12px'};
|
||||
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.accentAction : theme.deprecated_primary1)};
|
||||
cursor: pointer;
|
||||
font-size: ${({ redesignFlag }) => (redesignFlag ? '14px' : '11px')};
|
||||
font-weight: ${({ redesignFlag }) => (redesignFlag ? '600' : '500')};
|
||||
margin-left: ${({ redesignFlag }) => (redesignFlag ? '0px' : '0.25rem')};
|
||||
opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)};
|
||||
padding: 4px 6px;
|
||||
pointer-events: ${({ disabled }) => (!disabled ? 'initial' : 'none')};
|
||||
|
||||
:hover {
|
||||
opacity: ${({ disabled }) => (!disabled ? 0.8 : 0.4)};
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledNumericalInput = styled(NumericalInput)<{ $loading: boolean; redesignFlag: boolean }>`
|
||||
${loadingOpacityMixin};
|
||||
text-align: left;
|
||||
font-variant: ${({ redesignFlag }) => redesignFlag && 'small-caps'};
|
||||
font-feature-settings: ${({ redesignFlag }) => redesignFlag && 'pnum on, lnum on'};
|
||||
`
|
||||
|
||||
interface SwapCurrencyInputPanelProps {
|
||||
value: string
|
||||
onUserInput: (value: string) => void
|
||||
onMax?: () => void
|
||||
showMaxButton: boolean
|
||||
label?: ReactNode
|
||||
onCurrencySelect?: (currency: Currency) => void
|
||||
currency?: Currency | null
|
||||
hideBalance?: boolean
|
||||
pair?: Pair | null
|
||||
hideInput?: boolean
|
||||
otherCurrency?: Currency | null
|
||||
fiatValue?: CurrencyAmount<Token> | null
|
||||
priceImpact?: Percent
|
||||
id: string
|
||||
showCommonBases?: boolean
|
||||
showCurrencyAmount?: boolean
|
||||
disableNonToken?: boolean
|
||||
renderBalance?: (amount: CurrencyAmount<Currency>) => ReactNode
|
||||
locked?: boolean
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export default function SwapCurrencyInputPanel({
|
||||
value,
|
||||
onUserInput,
|
||||
onMax,
|
||||
showMaxButton,
|
||||
onCurrencySelect,
|
||||
currency,
|
||||
otherCurrency,
|
||||
id,
|
||||
showCommonBases,
|
||||
showCurrencyAmount,
|
||||
disableNonToken,
|
||||
renderBalance,
|
||||
fiatValue,
|
||||
priceImpact,
|
||||
hideBalance = false,
|
||||
pair = null, // used for double token logo
|
||||
hideInput = false,
|
||||
locked = false,
|
||||
loading = false,
|
||||
...rest
|
||||
}: SwapCurrencyInputPanelProps) {
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const { account, chainId } = useWeb3React()
|
||||
const redesignFlag = useRedesignFlag()
|
||||
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
|
||||
const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined)
|
||||
const theme = useTheme()
|
||||
const { pathname } = useLocation()
|
||||
const isAddLiquidityPage = pathname.includes('/add') && !pathname.includes('/add/v2')
|
||||
|
||||
const handleDismissSearch = useCallback(() => {
|
||||
setModalOpen(false)
|
||||
}, [setModalOpen])
|
||||
|
||||
const chainAllowed = isSupportedChain(chainId)
|
||||
|
||||
return (
|
||||
<InputPanel id={id} hideInput={hideInput} {...rest} redesignFlag={redesignFlagEnabled}>
|
||||
{locked && (
|
||||
<FixedContainer redesignFlag={redesignFlagEnabled}>
|
||||
<AutoColumn gap="sm" justify="center">
|
||||
<Lock />
|
||||
<ThemedText.DeprecatedLabel fontSize="12px" textAlign="center" padding="0 12px">
|
||||
<Trans>The market price is outside your specified price range. Single-asset deposit only.</Trans>
|
||||
</ThemedText.DeprecatedLabel>
|
||||
</AutoColumn>
|
||||
</FixedContainer>
|
||||
)}
|
||||
<Container hideInput={hideInput} disabled={!chainAllowed} redesignFlag={redesignFlagEnabled}>
|
||||
<InputRow
|
||||
style={hideInput ? { padding: '0', borderRadius: '8px' } : {}}
|
||||
selected={!onCurrencySelect}
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
>
|
||||
{!hideInput && (
|
||||
<StyledNumericalInput
|
||||
className="token-amount-input"
|
||||
value={value}
|
||||
onUserInput={onUserInput}
|
||||
disabled={!chainAllowed}
|
||||
$loading={loading}
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
/>
|
||||
)}
|
||||
|
||||
<InputCurrencySelect
|
||||
disabled={!chainAllowed}
|
||||
visible={currency !== undefined}
|
||||
selected={!!currency}
|
||||
hideInput={hideInput}
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
className="open-currency-select-button"
|
||||
onClick={() => {
|
||||
if (onCurrencySelect) {
|
||||
setModalOpen(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Aligner>
|
||||
<RowFixed>
|
||||
{pair ? (
|
||||
<span style={{ marginRight: '0.5rem' }}>
|
||||
<DoubleCurrencyLogo currency0={pair.token0} currency1={pair.token1} size={24} margin={true} />
|
||||
</span>
|
||||
) : currency ? (
|
||||
<CurrencyLogo style={{ marginRight: '2px' }} currency={currency} size={'24px'} />
|
||||
) : null}
|
||||
{pair ? (
|
||||
<StyledTokenName className="pair-name-container" redesignFlag={redesignFlagEnabled}>
|
||||
{pair?.token0.symbol}:{pair?.token1.symbol}
|
||||
</StyledTokenName>
|
||||
) : (
|
||||
<StyledTokenName
|
||||
className="token-symbol-container"
|
||||
active={Boolean(currency && currency.symbol)}
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
>
|
||||
{(currency && currency.symbol && currency.symbol.length > 20
|
||||
? currency.symbol.slice(0, 4) +
|
||||
'...' +
|
||||
currency.symbol.slice(currency.symbol.length - 5, currency.symbol.length)
|
||||
: currency?.symbol) || <Trans>Select token</Trans>}
|
||||
</StyledTokenName>
|
||||
)}
|
||||
</RowFixed>
|
||||
{onCurrencySelect && <StyledDropDown selected={!!currency} redesignFlag={redesignFlagEnabled} />}
|
||||
</Aligner>
|
||||
</InputCurrencySelect>
|
||||
</InputRow>
|
||||
{redesignFlagEnabled && !currency && !isAddLiquidityPage && (
|
||||
<NoBalanceState>
|
||||
<FiatRow redesignFlag={redesignFlagEnabled}>
|
||||
<RowBetween>
|
||||
<NoBalanceDash>-</NoBalanceDash>
|
||||
<NoBalanceDash>-</NoBalanceDash>
|
||||
</RowBetween>
|
||||
</FiatRow>
|
||||
</NoBalanceState>
|
||||
)}
|
||||
{!hideInput && !hideBalance && currency && (
|
||||
<FiatRow redesignFlag={redesignFlagEnabled}>
|
||||
<RowBetween>
|
||||
<LoadingOpacityContainer $loading={loading}>
|
||||
<FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />
|
||||
</LoadingOpacityContainer>
|
||||
{account ? (
|
||||
<RowFixed style={{ height: '17px' }}>
|
||||
<ThemedText.DeprecatedBody
|
||||
onClick={onMax}
|
||||
color={theme.deprecated_text3}
|
||||
fontWeight={500}
|
||||
fontSize={14}
|
||||
style={{ display: 'inline', cursor: 'pointer' }}
|
||||
>
|
||||
{!hideBalance && currency && selectedCurrencyBalance ? (
|
||||
renderBalance ? (
|
||||
renderBalance(selectedCurrencyBalance)
|
||||
) : (
|
||||
<Trans>Balance: {formatCurrencyAmount(selectedCurrencyBalance, 4)}</Trans>
|
||||
)
|
||||
) : null}
|
||||
</ThemedText.DeprecatedBody>
|
||||
{showMaxButton && selectedCurrencyBalance ? (
|
||||
<TraceEvent
|
||||
events={[Event.onClick]}
|
||||
name={EventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
|
||||
element={ElementName.MAX_TOKEN_AMOUNT_BUTTON}
|
||||
>
|
||||
<StyledBalanceMax onClick={onMax} redesignFlag={redesignFlagEnabled}>
|
||||
<Trans>Max</Trans>
|
||||
</StyledBalanceMax>
|
||||
</TraceEvent>
|
||||
) : null}
|
||||
</RowFixed>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
</RowBetween>
|
||||
</FiatRow>
|
||||
)}
|
||||
</Container>
|
||||
{onCurrencySelect && (
|
||||
<CurrencySearchModal
|
||||
isOpen={modalOpen}
|
||||
onDismiss={handleDismissSearch}
|
||||
onCurrencySelect={onCurrencySelect}
|
||||
selectedCurrency={currency}
|
||||
otherSelectedCurrency={otherCurrency}
|
||||
showCommonBases={showCommonBases}
|
||||
showCurrencyAmount={showCurrencyAmount}
|
||||
disableNonToken={disableNonToken}
|
||||
/>
|
||||
)}
|
||||
</InputPanel>
|
||||
)
|
||||
}
|
||||
@@ -25,37 +25,35 @@ import { RowBetween, RowFixed } from '../Row'
|
||||
import CurrencySearchModal from '../SearchModal/CurrencySearchModal'
|
||||
import { FiatValue } from './FiatValue'
|
||||
|
||||
const InputPanel = styled.div<{ hideInput?: boolean; redesignFlag: boolean }>`
|
||||
const InputPanel = styled.div<{ hideInput?: boolean }>`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
position: relative;
|
||||
border-radius: ${({ hideInput }) => (hideInput ? '16px' : '20px')};
|
||||
background-color: ${({ theme, redesignFlag, hideInput }) =>
|
||||
redesignFlag ? 'transparent' : hideInput ? 'transparent' : theme.deprecated_bg2};
|
||||
background-color: ${({ theme, hideInput }) => (hideInput ? 'transparent' : theme.deprecated_bg2)};
|
||||
z-index: 1;
|
||||
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
|
||||
transition: height 1s ease;
|
||||
will-change: height;
|
||||
`
|
||||
|
||||
const FixedContainer = styled.div<{ redesignFlag: boolean }>`
|
||||
const FixedContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
border-radius: 20px;
|
||||
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_bg2)};
|
||||
background-color: ${({ theme }) => theme.deprecated_bg2};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
`
|
||||
|
||||
const Container = styled.div<{ hideInput: boolean; disabled: boolean; redesignFlag: boolean }>`
|
||||
const Container = styled.div<{ hideInput: boolean; disabled: boolean }>`
|
||||
border-radius: ${({ hideInput }) => (hideInput ? '16px' : '20px')};
|
||||
border: 1px solid ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_bg0)};
|
||||
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_bg1)};
|
||||
border: 1px solid ${({ theme }) => theme.deprecated_bg0};
|
||||
background-color: ${({ theme }) => theme.deprecated_bg1};
|
||||
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
|
||||
${({ theme, hideInput, disabled, redesignFlag }) =>
|
||||
!redesignFlag &&
|
||||
${({ theme, hideInput, disabled }) =>
|
||||
!disabled &&
|
||||
`
|
||||
:focus,
|
||||
@@ -70,65 +68,38 @@ const CurrencySelect = styled(ButtonGray)<{
|
||||
selected: boolean
|
||||
hideInput?: boolean
|
||||
disabled?: boolean
|
||||
redesignFlag: boolean
|
||||
}>`
|
||||
align-items: center;
|
||||
background-color: ${({ selected, theme, redesignFlag }) =>
|
||||
redesignFlag
|
||||
? selected
|
||||
? theme.backgroundSurface
|
||||
: theme.accentAction
|
||||
: selected
|
||||
? theme.deprecated_bg2
|
||||
: theme.deprecated_primary1};
|
||||
background-color: ${({ selected, theme }) => (selected ? theme.deprecated_bg2 : theme.deprecated_primary1)};
|
||||
opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)};
|
||||
box-shadow: ${({ selected }) => (selected ? 'none' : '0px 6px 10px rgba(0, 0, 0, 0.075)')};
|
||||
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
|
||||
color: ${({ selected, theme }) => (selected ? theme.deprecated_text1 : theme.deprecated_white)};
|
||||
cursor: pointer;
|
||||
height: ${({ hideInput, redesignFlag }) => (redesignFlag ? 'unset' : hideInput ? '2.8rem' : '2.4rem')};
|
||||
border-radius: 16px;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
font-weight: 500;
|
||||
height: ${({ hideInput }) => (hideInput ? '2.8rem' : '2.4rem')};
|
||||
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
|
||||
padding: ${({ selected, redesignFlag }) =>
|
||||
redesignFlag ? (selected ? '4px 8px 4px 4px' : '6px 6px 6px 8px') : '0 8px'};
|
||||
gap: ${({ redesignFlag }) => (redesignFlag ? '8px' : '0px')};
|
||||
padding: 0 8px;
|
||||
justify-content: space-between;
|
||||
margin-left: ${({ hideInput }) => (hideInput ? '0' : '12px')};
|
||||
:focus,
|
||||
:hover {
|
||||
background-color: ${({ selected, theme, redesignFlag }) =>
|
||||
selected
|
||||
? redesignFlag
|
||||
? theme.backgroundSurface
|
||||
: theme.deprecated_bg3
|
||||
: darken(0.05, theme.deprecated_primary1)};
|
||||
background-color: ${({ selected, theme }) =>
|
||||
selected ? theme.deprecated_bg3 : darken(0.05, theme.deprecated_primary1)};
|
||||
}
|
||||
visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')};
|
||||
`
|
||||
const InputCurrencySelect = styled(CurrencySelect)<{ redesignFlag: boolean }>`
|
||||
background-color: ${({ theme, selected, redesignFlag }) =>
|
||||
redesignFlag && (selected ? theme.backgroundModule : theme.accentAction)};
|
||||
:focus,
|
||||
:hover {
|
||||
background-color: ${({ selected, theme, redesignFlag }) =>
|
||||
selected
|
||||
? redesignFlag
|
||||
? theme.backgroundInteractive
|
||||
: theme.deprecated_bg3
|
||||
: darken(0.05, theme.deprecated_primary1)};
|
||||
}
|
||||
`
|
||||
|
||||
const InputRow = styled.div<{ selected: boolean; redesignFlag: boolean }>`
|
||||
const InputRow = styled.div<{ selected: boolean }>`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: ${({ selected, redesignFlag }) =>
|
||||
redesignFlag ? '0px' : selected ? ' 1rem 1rem 0.75rem 1rem' : '1rem 1rem 1rem 1rem'};
|
||||
padding: ${({ selected }) => (selected ? ' 1rem 1rem 0.75rem 1rem' : '1rem 1rem 1rem 1rem')};
|
||||
`
|
||||
|
||||
const LabelRow = styled.div`
|
||||
@@ -138,7 +109,6 @@ const LabelRow = styled.div`
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
padding: 0 1rem 1rem;
|
||||
|
||||
span:hover {
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => darken(0.2, theme.deprecated_text2)};
|
||||
@@ -147,20 +117,8 @@ const LabelRow = styled.div`
|
||||
|
||||
const FiatRow = styled(LabelRow)<{ redesignFlag: boolean }>`
|
||||
justify-content: flex-end;
|
||||
padding: ${({ redesignFlag }) => redesignFlag && '8px 0px'};
|
||||
height: ${({ redesignFlag }) => !redesignFlag && '24px'};
|
||||
`
|
||||
|
||||
const NoBalanceState = styled.div`
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
font-weight: 400;
|
||||
justify-content: space-between;
|
||||
padding: 0px 4px;
|
||||
`
|
||||
const NoBalanceDash = styled.span`
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
font-variant: small-caps;
|
||||
font-feature-settings: 'pnum' on, 'lnum' on;
|
||||
padding: ${({ redesignFlag }) => redesignFlag && '0px 1rem 0.75rem'};
|
||||
height: ${({ redesignFlag }) => (redesignFlag ? '32px' : '16px')};
|
||||
`
|
||||
|
||||
const Aligner = styled.span`
|
||||
@@ -170,34 +128,31 @@ const Aligner = styled.span`
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const StyledDropDown = styled(DropDown)<{ selected: boolean; redesignFlag: boolean }>`
|
||||
const StyledDropDown = styled(DropDown)<{ selected: boolean }>`
|
||||
margin: 0 0.25rem 0 0.35rem;
|
||||
height: 35%;
|
||||
margin-left: ${({ redesignFlag }) => redesignFlag && '8px'};
|
||||
|
||||
path {
|
||||
stroke: ${({ selected, theme }) => (selected ? theme.deprecated_text1 : theme.deprecated_white)};
|
||||
stroke-width: ${({ redesignFlag }) => (redesignFlag ? '2px' : '1.5px')};
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledTokenName = styled.span<{ active?: boolean; redesignFlag: boolean }>`
|
||||
const StyledTokenName = styled.span<{ active?: boolean }>`
|
||||
${({ active }) => (active ? ' margin: 0 0.25rem 0 0.25rem;' : ' margin: 0 0.25rem 0 0.25rem;')}
|
||||
font-size: ${({ active }) => (active ? '18px' : '18px')};
|
||||
font-weight: ${({ redesignFlag }) => (redesignFlag ? '600' : '500')};
|
||||
`
|
||||
|
||||
const StyledBalanceMax = styled.button<{ disabled?: boolean; redesignFlag: boolean }>`
|
||||
const StyledBalanceMax = styled.button<{ disabled?: boolean }>`
|
||||
background-color: transparent;
|
||||
background-color: ${({ theme, redesignFlag }) => !redesignFlag && theme.deprecated_primary5};
|
||||
background-color: ${({ theme }) => theme.deprecated_primary5};
|
||||
border: none;
|
||||
text-transform: ${({ redesignFlag }) => !redesignFlag && 'uppercase'};
|
||||
border-radius: ${({ redesignFlag }) => !redesignFlag && '12px'};
|
||||
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.accentAction : theme.deprecated_primary1)};
|
||||
border-radius: 12px;
|
||||
color: ${({ theme }) => theme.deprecated_primary1};
|
||||
cursor: pointer;
|
||||
font-size: ${({ redesignFlag }) => (redesignFlag ? '14px' : '11px')};
|
||||
font-weight: ${({ redesignFlag }) => (redesignFlag ? '600' : '500')};
|
||||
margin-left: ${({ redesignFlag }) => (redesignFlag ? '0px' : '0.25rem')};
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
margin-left: 0.25rem;
|
||||
opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)};
|
||||
padding: 4px 6px;
|
||||
pointer-events: ${({ disabled }) => (!disabled ? 'initial' : 'none')};
|
||||
@@ -211,11 +166,9 @@ const StyledBalanceMax = styled.button<{ disabled?: boolean; redesignFlag: boole
|
||||
}
|
||||
`
|
||||
|
||||
const StyledNumericalInput = styled(NumericalInput)<{ $loading: boolean; redesignFlag: boolean }>`
|
||||
const StyledNumericalInput = styled(NumericalInput)<{ $loading: boolean }>`
|
||||
${loadingOpacityMixin};
|
||||
text-align: left;
|
||||
font-variant: ${({ redesignFlag }) => redesignFlag && 'small-caps'};
|
||||
font-feature-settings: ${({ redesignFlag }) => redesignFlag && 'pnum on, lnum on'};
|
||||
`
|
||||
|
||||
interface CurrencyInputPanelProps {
|
||||
@@ -265,10 +218,10 @@ export default function CurrencyInputPanel({
|
||||
}: CurrencyInputPanelProps) {
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const { account, chainId } = useWeb3React()
|
||||
const redesignFlag = useRedesignFlag()
|
||||
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
|
||||
const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined)
|
||||
const theme = useTheme()
|
||||
const redesignFlag = useRedesignFlag()
|
||||
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
|
||||
|
||||
const handleDismissSearch = useCallback(() => {
|
||||
setModalOpen(false)
|
||||
@@ -277,9 +230,9 @@ export default function CurrencyInputPanel({
|
||||
const chainAllowed = isSupportedChain(chainId)
|
||||
|
||||
return (
|
||||
<InputPanel id={id} hideInput={hideInput} {...rest} redesignFlag={redesignFlagEnabled}>
|
||||
<InputPanel id={id} hideInput={hideInput} {...rest}>
|
||||
{locked && (
|
||||
<FixedContainer redesignFlag={redesignFlagEnabled}>
|
||||
<FixedContainer>
|
||||
<AutoColumn gap="sm" justify="center">
|
||||
<Lock />
|
||||
<ThemedText.DeprecatedLabel fontSize="12px" textAlign="center" padding="0 12px">
|
||||
@@ -288,12 +241,8 @@ export default function CurrencyInputPanel({
|
||||
</AutoColumn>
|
||||
</FixedContainer>
|
||||
)}
|
||||
<Container hideInput={hideInput} disabled={!chainAllowed} redesignFlag={redesignFlagEnabled}>
|
||||
<InputRow
|
||||
style={hideInput ? { padding: '0', borderRadius: '8px' } : {}}
|
||||
selected={!onCurrencySelect}
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
>
|
||||
<Container hideInput={hideInput} disabled={!chainAllowed}>
|
||||
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}} selected={!onCurrencySelect}>
|
||||
{!hideInput && (
|
||||
<StyledNumericalInput
|
||||
className="token-amount-input"
|
||||
@@ -301,16 +250,14 @@ export default function CurrencyInputPanel({
|
||||
onUserInput={onUserInput}
|
||||
disabled={!chainAllowed}
|
||||
$loading={loading}
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
/>
|
||||
)}
|
||||
|
||||
<InputCurrencySelect
|
||||
<CurrencySelect
|
||||
disabled={!chainAllowed}
|
||||
visible={currency !== undefined}
|
||||
selected={!!currency}
|
||||
hideInput={hideInput}
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
className="open-currency-select-button"
|
||||
onClick={() => {
|
||||
if (onCurrencySelect) {
|
||||
@@ -325,40 +272,26 @@ export default function CurrencyInputPanel({
|
||||
<DoubleCurrencyLogo currency0={pair.token0} currency1={pair.token1} size={24} margin={true} />
|
||||
</span>
|
||||
) : currency ? (
|
||||
<CurrencyLogo style={{ marginRight: '2px' }} currency={currency} size={'24px'} />
|
||||
<CurrencyLogo style={{ marginRight: '0.5rem' }} currency={currency} size={'24px'} />
|
||||
) : null}
|
||||
{pair ? (
|
||||
<StyledTokenName className="pair-name-container" redesignFlag={redesignFlagEnabled}>
|
||||
<StyledTokenName className="pair-name-container">
|
||||
{pair?.token0.symbol}:{pair?.token1.symbol}
|
||||
</StyledTokenName>
|
||||
) : (
|
||||
<StyledTokenName
|
||||
className="token-symbol-container"
|
||||
active={Boolean(currency && currency.symbol)}
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
>
|
||||
<StyledTokenName className="token-symbol-container" active={Boolean(currency && currency.symbol)}>
|
||||
{(currency && currency.symbol && currency.symbol.length > 20
|
||||
? currency.symbol.slice(0, 4) +
|
||||
'...' +
|
||||
currency.symbol.slice(currency.symbol.length - 5, currency.symbol.length)
|
||||
: currency?.symbol) || <Trans>Select token</Trans>}
|
||||
: currency?.symbol) || <Trans>Select a token</Trans>}
|
||||
</StyledTokenName>
|
||||
)}
|
||||
</RowFixed>
|
||||
{onCurrencySelect && <StyledDropDown selected={!!currency} redesignFlag={redesignFlagEnabled} />}
|
||||
{onCurrencySelect && <StyledDropDown selected={!!currency} />}
|
||||
</Aligner>
|
||||
</InputCurrencySelect>
|
||||
</CurrencySelect>
|
||||
</InputRow>
|
||||
{redesignFlagEnabled && !currency && (
|
||||
<NoBalanceState>
|
||||
<FiatRow redesignFlag={redesignFlagEnabled}>
|
||||
<RowBetween>
|
||||
<NoBalanceDash>-</NoBalanceDash>
|
||||
<NoBalanceDash>-</NoBalanceDash>
|
||||
</RowBetween>
|
||||
</FiatRow>
|
||||
</NoBalanceState>
|
||||
)}
|
||||
{!hideInput && !hideBalance && currency && (
|
||||
<FiatRow redesignFlag={redesignFlagEnabled}>
|
||||
<RowBetween>
|
||||
@@ -388,8 +321,8 @@ export default function CurrencyInputPanel({
|
||||
name={EventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
|
||||
element={ElementName.MAX_TOKEN_AMOUNT_BUTTON}
|
||||
>
|
||||
<StyledBalanceMax onClick={onMax} redesignFlag={redesignFlagEnabled}>
|
||||
<Trans>Max</Trans>
|
||||
<StyledBalanceMax onClick={onMax}>
|
||||
<Trans>MAX</Trans>
|
||||
</StyledBalanceMax>
|
||||
</TraceEvent>
|
||||
) : null}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ interface DoubleCurrencyLogoProps {
|
||||
}
|
||||
|
||||
const HigherLogo = styled(CurrencyLogo)`
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
`
|
||||
const CoveredLogo = styled(CurrencyLogo)<{ sizeraw: number }>`
|
||||
position: absolute;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
|
||||
import { Phase1Variant, usePhase1Flag } from 'featureFlags/flags/phase1'
|
||||
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 { useAtomValue } from 'jotai/utils'
|
||||
import { ReactNode, useState } from 'react'
|
||||
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'
|
||||
import { useModalIsOpen, useToggleFeatureFlags } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
@@ -44,7 +46,14 @@ const Row = styled.div`
|
||||
|
||||
const CloseButton = styled.button`
|
||||
cursor: pointer;
|
||||
background: 'transparent';
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
`
|
||||
|
||||
const ToggleButton = styled.button`
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
`
|
||||
@@ -58,7 +67,6 @@ const Header = styled(Row)`
|
||||
const FlagName = styled.span`
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
padding-left: 8px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
`
|
||||
const FlagGroupName = styled.span`
|
||||
@@ -114,17 +122,53 @@ function Variant({ option }: { option: string }) {
|
||||
return <option value={option}>{option}</option>
|
||||
}
|
||||
|
||||
function FeatureFlagOption({
|
||||
variants,
|
||||
featureFlag,
|
||||
value,
|
||||
label,
|
||||
}: {
|
||||
variants: string[]
|
||||
interface FeatureFlagProps {
|
||||
variant: Record<string, string>
|
||||
featureFlag: FeatureFlag
|
||||
value: string
|
||||
label: string
|
||||
}) {
|
||||
}
|
||||
|
||||
function FeatureFlagGroup({ name, children }: PropsWithChildren<{ name: string }>) {
|
||||
// type FeatureFlagOption = { props: FeatureFlagProps }
|
||||
const togglableOptions = Children.toArray(children)
|
||||
.filter<ReactElement<FeatureFlagProps>>(
|
||||
(child): child is ReactElement<FeatureFlagProps> =>
|
||||
child instanceof Object && 'type' in child && child.type === FeatureFlagOption
|
||||
)
|
||||
.map(({ props }) => props)
|
||||
.filter(({ variant }) => {
|
||||
const values = Object.values(variant)
|
||||
return values.includes(BaseVariant.Control) && values.includes(BaseVariant.Enabled)
|
||||
})
|
||||
|
||||
const setFeatureFlags = useUpdateAtom(featureFlagSettings)
|
||||
const allEnabled = togglableOptions.every(({ value }) => value === BaseVariant.Enabled)
|
||||
const onToggle = useCallback(() => {
|
||||
setFeatureFlags((flags) => ({
|
||||
...flags,
|
||||
...togglableOptions.reduce(
|
||||
(flags, { featureFlag }) => ({
|
||||
...flags,
|
||||
[featureFlag]: allEnabled ? BaseVariant.Control : BaseVariant.Enabled,
|
||||
}),
|
||||
{}
|
||||
),
|
||||
}))
|
||||
}, [allEnabled, setFeatureFlags, togglableOptions])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row key={name}>
|
||||
<FlagGroupName>{name}</FlagGroupName>
|
||||
<ToggleButton onClick={onToggle}>{allEnabled ? 'Disable' : 'Enable'} group</ToggleButton>
|
||||
</Row>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function FeatureFlagOption({ variant, featureFlag, value, label }: FeatureFlagProps) {
|
||||
const updateFlag = useUpdateFlag()
|
||||
const [count, setCount] = useState(0)
|
||||
const featureFlags = useAtomValue(featureFlagSettings)
|
||||
@@ -143,7 +187,7 @@ function FeatureFlagOption({
|
||||
}}
|
||||
value={featureFlags[featureFlag]}
|
||||
>
|
||||
{variants.map((variant) => (
|
||||
{Object.values(variant).map((variant) => (
|
||||
<Variant key={variant} option={variant} />
|
||||
))}
|
||||
</FlagVariantSelection>
|
||||
@@ -163,39 +207,50 @@ export default function FeatureFlagModal() {
|
||||
<X size={24} />
|
||||
</CloseButton>
|
||||
</Header>
|
||||
<FlagGroupName>Phase 1</FlagGroupName>
|
||||
<FeatureFlagOption
|
||||
variants={Object.values(Phase1Variant)}
|
||||
value={usePhase1Flag()}
|
||||
featureFlag={FeatureFlag.phase1}
|
||||
label="All Phase 1 changes (nft features)."
|
||||
/>
|
||||
<FlagGroupName>Phase 0</FlagGroupName>
|
||||
<FeatureFlagOption
|
||||
variants={Object.values(RedesignVariant)}
|
||||
value={useRedesignFlag()}
|
||||
featureFlag={FeatureFlag.redesign}
|
||||
label="Redesign"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variants={Object.values(NavBarVariant)}
|
||||
value={useNavBarFlag()}
|
||||
featureFlag={FeatureFlag.navBar}
|
||||
label="NavBar"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variants={Object.values(TokensVariant)}
|
||||
value={useTokensFlag()}
|
||||
featureFlag={FeatureFlag.tokens}
|
||||
label="Tokens"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variants={Object.values(TokenSafetyVariant)}
|
||||
value={useTokenSafetyFlag()}
|
||||
featureFlag={FeatureFlag.tokenSafety}
|
||||
label="Token Safety"
|
||||
/>
|
||||
<SaveButton onClick={() => window.location.reload()}>Save Settings</SaveButton>
|
||||
<FeatureFlagGroup name="Phase 0">
|
||||
<FeatureFlagOption
|
||||
variant={RedesignVariant}
|
||||
value={useRedesignFlag()}
|
||||
featureFlag={FeatureFlag.redesign}
|
||||
label="Redesign"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={NavBarVariant}
|
||||
value={useNavBarFlag()}
|
||||
featureFlag={FeatureFlag.navBar}
|
||||
label="NavBar"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={TokensVariant}
|
||||
value={useTokensFlag()}
|
||||
featureFlag={FeatureFlag.tokens}
|
||||
label="Tokens"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={TokensNetworkFilterVariant}
|
||||
value={useTokensNetworkFilterFlag()}
|
||||
featureFlag={FeatureFlag.tokensNetworkFilter}
|
||||
label="Tokens Network Filter"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={TokenSafetyVariant}
|
||||
value={useTokenSafetyFlag()}
|
||||
featureFlag={FeatureFlag.tokenSafety}
|
||||
label="Token Safety"
|
||||
/>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const ResponsiveText = styled(ThemedText.DeprecatedLabel)`
|
||||
line-height: 16px;
|
||||
font-size: 14px;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
`};
|
||||
|
||||
@@ -39,7 +39,7 @@ const Wrapper = styled.div`
|
||||
padding: 16px 20px;
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) {
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToMedium}px) {
|
||||
display: block;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useCloseModal, useModalIsOpen, useOpenModal, useToggleModal } from 'sta
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
|
||||
import { isChainAllowed } from 'utils/switchChain'
|
||||
import { isMobile } from 'utils/userAgent'
|
||||
|
||||
const ActiveRowLinkList = styled.div`
|
||||
@@ -53,7 +52,7 @@ const FlyoutMenu = styled.div`
|
||||
width: 272px;
|
||||
z-index: 99;
|
||||
padding-top: 10px;
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
|
||||
top: 40px;
|
||||
}
|
||||
`
|
||||
@@ -112,7 +111,7 @@ const NetworkLabel = styled.div`
|
||||
`
|
||||
const SelectorLabel = styled(NetworkLabel)`
|
||||
display: none;
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
|
||||
display: block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
@@ -126,7 +125,7 @@ const NetworkAlertLabel = styled(NetworkLabel)`
|
||||
font-size: 1rem;
|
||||
width: fit-content;
|
||||
font-weight: 500;
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
|
||||
display: block;
|
||||
}
|
||||
`
|
||||
@@ -153,12 +152,12 @@ const SelectorControls = styled.div<{ supportedChain: boolean }>`
|
||||
}
|
||||
`
|
||||
const SelectorLogo = styled(Logo)`
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
`
|
||||
const SelectorWrapper = styled.div`
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
|
||||
position: relative;
|
||||
}
|
||||
`
|
||||
@@ -281,7 +280,7 @@ const NETWORK_SELECTOR_CHAINS = [
|
||||
]
|
||||
|
||||
export default function NetworkSelector() {
|
||||
const { chainId, provider, connector } = useWeb3React()
|
||||
const { chainId, provider } = useWeb3React()
|
||||
|
||||
const node = useRef<HTMLDivElement>(null)
|
||||
const isOpen = useModalIsOpen(ApplicationModal.NETWORK_SELECTOR)
|
||||
@@ -328,18 +327,16 @@ export default function NetworkSelector() {
|
||||
<FlyoutHeader>
|
||||
<Trans>Select a {!onSupportedChain ? ' supported ' : ''}network</Trans>
|
||||
</FlyoutHeader>
|
||||
{NETWORK_SELECTOR_CHAINS.map((chainId: SupportedChainId) =>
|
||||
isChainAllowed(connector, chainId) ? (
|
||||
<Row
|
||||
onSelectChain={async (targetChainId: SupportedChainId) => {
|
||||
await selectChain(targetChainId)
|
||||
closeModal()
|
||||
}}
|
||||
targetChain={chainId}
|
||||
key={chainId}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
{NETWORK_SELECTOR_CHAINS.map((chainId: SupportedChainId) => (
|
||||
<Row
|
||||
onSelectChain={async (targetChainId: SupportedChainId) => {
|
||||
await selectChain(targetChainId)
|
||||
closeModal()
|
||||
}}
|
||||
targetChain={chainId}
|
||||
key={chainId}
|
||||
/>
|
||||
))}
|
||||
</FlyoutMenuContents>
|
||||
</FlyoutMenu>
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,7 @@ const StyledPolling = styled.div<{ warning: boolean }>`
|
||||
color: ${({ theme, warning }) => (warning ? theme.deprecated_yellow3 : theme.deprecated_green1)};
|
||||
transition: 250ms ease color;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
display: none;
|
||||
`}
|
||||
`
|
||||
|
||||
@@ -46,16 +46,16 @@ const HeaderFrame = styled.div<{ showBackground: boolean }>`
|
||||
transition: background-position 0.1s, box-shadow 0.1s;
|
||||
background-blend-mode: hard-light;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToLarge`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToLarge`
|
||||
grid-template-columns: 48px 1fr 1fr;
|
||||
`};
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
padding: 1rem;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
`};
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
padding: 1rem;
|
||||
grid-template-columns: 36px 1fr;
|
||||
`};
|
||||
@@ -81,7 +81,7 @@ const HeaderElement = styled.div`
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
align-items: center;
|
||||
`};
|
||||
`
|
||||
@@ -97,13 +97,13 @@ const HeaderLinks = styled(Row)`
|
||||
grid-gap: 10px;
|
||||
overflow: auto;
|
||||
align-items: center;
|
||||
${({ theme }) => theme.mediaWidth.upToLarge`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToLarge`
|
||||
justify-self: start;
|
||||
`};
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
justify-self: center;
|
||||
`};
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
justify-self: center;
|
||||
@@ -157,7 +157,7 @@ const UNIWrapper = styled.span`
|
||||
`
|
||||
|
||||
const BalanceText = styled(Text)`
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToExtraSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
@@ -168,7 +168,7 @@ const Title = styled.a`
|
||||
pointer-events: auto;
|
||||
justify-self: flex-start;
|
||||
margin-right: 12px;
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
justify-self: center;
|
||||
`};
|
||||
:hover {
|
||||
|
||||
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,12 +1,18 @@
|
||||
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 { colors } from 'theme/colors'
|
||||
|
||||
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
|
||||
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
|
||||
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;
|
||||
@@ -16,27 +22,62 @@ const IconWrapper = styled.div<{ size?: number }>`
|
||||
height: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
width: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
}
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
align-items: flex-end;
|
||||
`};
|
||||
`
|
||||
|
||||
export default function StatusIcon({ connectionType }: { connectionType: ConnectionType }) {
|
||||
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
|
||||
case ConnectionType.FORTMATIC:
|
||||
image = <img src={FortmaticIcon} alt="Fortmatic" />
|
||||
break
|
||||
const SockContainer = styled.div`
|
||||
position: absolute;
|
||||
background-color: ${colors.pink400};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
bottom: -5px;
|
||||
right: -5px;
|
||||
`
|
||||
|
||||
const SockImg = styled.img`
|
||||
width: 7.5px;
|
||||
height: 10px;
|
||||
margin-top: 3px;
|
||||
`
|
||||
|
||||
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={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>
|
||||
) : (
|
||||
|
||||
@@ -48,11 +48,11 @@ const StyledInput = styled(NumericalInput)<{ usePercent?: boolean }>`
|
||||
font-weight: 500;
|
||||
padding: 0 10px;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
font-size: 16px;
|
||||
`};
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToExtraSmall`
|
||||
font-size: 12px;
|
||||
`};
|
||||
`
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ const MenuFlyout = styled.span<{ flyoutAlignment?: FlyoutAlignment }>`
|
||||
: css`
|
||||
left: 0rem;
|
||||
`};
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
bottom: unset;
|
||||
right: 0;
|
||||
left: unset;
|
||||
@@ -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>
|
||||
|
||||
@@ -26,7 +26,7 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ redesignFlag?: boole
|
||||
const AnimatedDialogContent = animated(DialogContent)
|
||||
// destructure to not pass custom props to Dialog DOM element
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => (
|
||||
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, redesignFlag, ...rest }) => (
|
||||
<AnimatedDialogContent {...rest} />
|
||||
)).attrs({
|
||||
'aria-label': 'dialog',
|
||||
@@ -37,7 +37,8 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
|
||||
margin: 0 0 2rem 0;
|
||||
background-color: ${({ theme }) => theme.deprecated_bg0};
|
||||
border: 1px solid ${({ theme }) => theme.deprecated_bg1};
|
||||
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.shadow1)};
|
||||
box-shadow: ${({ theme, redesignFlag }) =>
|
||||
redesignFlag ? theme.deepShadow : `0 4px 8px 0 ${transparentize(0.95, theme.shadow1)}`};
|
||||
padding: 0px;
|
||||
width: 50vw;
|
||||
overflow-y: auto;
|
||||
@@ -58,11 +59,11 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
|
||||
`}
|
||||
display: flex;
|
||||
border-radius: 20px;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
width: 65vw;
|
||||
margin: 0;
|
||||
`}
|
||||
${({ theme, mobile }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme, mobile }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
width: 85vw;
|
||||
${
|
||||
mobile &&
|
||||
@@ -139,6 +140,7 @@ export default function Modal({
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
mobile={isMobile}
|
||||
redesignFlag={redesignFlag}
|
||||
>
|
||||
{/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
|
||||
{!initialFocusRef && isMobile ? <div tabIndex={1} /> : null}
|
||||
|
||||
@@ -1,37 +1,47 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { lightGrayOverlayOnHover } from 'nft/css/common.css'
|
||||
|
||||
import { sprinkles } from '../../nft/css/sprinkles.css'
|
||||
import { breakpoints, sprinkles } from '../../nft/css/sprinkles.css'
|
||||
|
||||
export const ChainSwitcher = style([
|
||||
lightGrayOverlayOnHover,
|
||||
sprinkles({
|
||||
background: 'lightGrayContainer',
|
||||
borderRadius: '8',
|
||||
paddingY: '8',
|
||||
paddingX: '12',
|
||||
cursor: 'pointer',
|
||||
border: 'none',
|
||||
color: 'blackBlue',
|
||||
background: 'none',
|
||||
}),
|
||||
])
|
||||
|
||||
export const ChainSwitcherRow = style([
|
||||
lightGrayOverlayOnHover,
|
||||
sprinkles({
|
||||
border: 'none',
|
||||
color: 'blackBlue',
|
||||
justifyContent: 'space-between',
|
||||
paddingX: '16',
|
||||
paddingY: '12',
|
||||
paddingX: '8',
|
||||
paddingY: '8',
|
||||
cursor: 'pointer',
|
||||
color: 'blackBlue',
|
||||
borderRadius: '12',
|
||||
width: { sm: 'full' },
|
||||
}),
|
||||
{
|
||||
lineHeight: '24px',
|
||||
width: '308px',
|
||||
'@media': {
|
||||
[`screen and (min-width: ${breakpoints.sm}px)`]: {
|
||||
width: '204px',
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
export const Image = style([
|
||||
sprinkles({
|
||||
width: '28',
|
||||
height: '28',
|
||||
width: '20',
|
||||
height: '20',
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -41,9 +51,3 @@ export const Icon = style([
|
||||
marginRight: '12',
|
||||
}),
|
||||
])
|
||||
|
||||
export const Indicator = style([
|
||||
sprinkles({
|
||||
marginLeft: '8',
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
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'
|
||||
import useSelectChain from 'hooks/useSelectChain'
|
||||
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 { NewChevronDownIcon, NewChevronUpIcon } from 'nft/components/icons'
|
||||
import { CheckMarkIcon } from 'nft/components/icons'
|
||||
import { CheckMarkIcon, TokenWarningRedIcon } from 'nft/components/icons'
|
||||
import { subhead } from 'nft/css/common.css'
|
||||
import { themeVars, vars } from 'nft/css/sprinkles.css'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { ReactNode, useReducer, useRef } from 'react'
|
||||
import { isChainAllowed } from 'utils/switchChain'
|
||||
|
||||
import * as styles from './ChainSwitcher.css'
|
||||
import { NavDropdown } from './NavDropdown'
|
||||
@@ -27,18 +29,20 @@ const ChainRow = ({
|
||||
const { label, logoUrl } = getChainInfo(targetChain)
|
||||
|
||||
return (
|
||||
<Row
|
||||
as="button"
|
||||
background={active ? 'lightGrayContainer' : '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} />}
|
||||
</Row>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -53,56 +57,73 @@ const NETWORK_SELECTOR_CHAINS = [
|
||||
]
|
||||
|
||||
interface ChainSwitcherProps {
|
||||
isMobile?: boolean
|
||||
leftAlign?: boolean
|
||||
}
|
||||
|
||||
export const ChainSwitcher = ({ isMobile }: ChainSwitcherProps) => {
|
||||
const { chainId, connector } = useWeb3React()
|
||||
export const ChainSwitcher = ({ leftAlign }: ChainSwitcherProps) => {
|
||||
const { chainId } = useWeb3React()
|
||||
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
useOnClickOutside(ref, isOpen ? toggleOpen : undefined, [modalRef])
|
||||
|
||||
const info = chainId ? getChainInfo(chainId) : undefined
|
||||
|
||||
const selectChain = useSelectChain()
|
||||
useSyncChainQuery()
|
||||
|
||||
if (!chainId || !info) {
|
||||
if (!chainId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isSupported = !!info
|
||||
|
||||
const dropdown = (
|
||||
<NavDropdown top="56" left={leftAlign ? '0' : 'auto'} right={leftAlign ? 'auto' : '0'} ref={modalRef}>
|
||||
<Column marginX="8">
|
||||
{NETWORK_SELECTOR_CHAINS.map((chainId: SupportedChainId) => (
|
||||
<ChainRow
|
||||
onSelectChain={async (targetChainId: SupportedChainId) => {
|
||||
await selectChain(targetChainId)
|
||||
toggleOpen()
|
||||
}}
|
||||
targetChain={chainId}
|
||||
key={chainId}
|
||||
/>
|
||||
))}
|
||||
</Column>
|
||||
</NavDropdown>
|
||||
)
|
||||
|
||||
return (
|
||||
<Box position="relative" ref={ref}>
|
||||
<Row as="button" gap="8" className={styles.ChainSwitcher} onClick={toggleOpen}>
|
||||
<img src={info.logoUrl} alt={info.label} className={styles.Image} />
|
||||
<Box as="span" className={subhead} color="explicitWhite" style={{ lineHeight: '20px' }}>
|
||||
{info.label}
|
||||
</Box>
|
||||
{isOpen ? (
|
||||
<NewChevronUpIcon width={16} height={16} color="darkGray" />
|
||||
<Row
|
||||
as="button"
|
||||
gap="8"
|
||||
className={styles.ChainSwitcher}
|
||||
background={isOpen ? 'accentActiveSoft' : 'none'}
|
||||
onClick={toggleOpen}
|
||||
>
|
||||
{!isSupported ? (
|
||||
<>
|
||||
<TokenWarningRedIcon fill={themeVars.colors.darkGray} width={24} height={24} />
|
||||
<Box as="span" className={subhead} display={{ sm: 'none', xxl: 'flex' }} style={{ lineHeight: '20px' }}>
|
||||
Unsupported
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<NewChevronDownIcon width={16} height={16} color="darkGray" />
|
||||
<>
|
||||
<img src={info.logoUrl} alt={info.label} className={styles.Image} />
|
||||
<Box as="span" className={subhead} display={{ sm: 'none', xxl: 'flex' }} style={{ lineHeight: '20px' }}>
|
||||
{info.label}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{isOpen ? <StyledChevronUp /> : <StyledChevronDown />}
|
||||
</Row>
|
||||
{isOpen && (
|
||||
<NavDropdown top={60} leftAligned={isMobile}>
|
||||
<Column gap="4">
|
||||
{NETWORK_SELECTOR_CHAINS.map((chainId: SupportedChainId) =>
|
||||
isChainAllowed(connector, chainId) ? (
|
||||
<ChainRow
|
||||
onSelectChain={async (targetChainId: SupportedChainId) => {
|
||||
await selectChain(targetChainId)
|
||||
toggleOpen()
|
||||
}}
|
||||
targetChain={chainId}
|
||||
key={chainId}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</Column>
|
||||
</NavDropdown>
|
||||
)}
|
||||
{isOpen && (isMobile ? <Portal>{dropdown}</Portal> : <>{dropdown}</>)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
|
||||
import { sprinkles, themeVars } from '../../nft/css/sprinkles.css'
|
||||
import { sprinkles, themeVars, vars } from '../../nft/css/sprinkles.css'
|
||||
|
||||
export const hover = style([
|
||||
sprinkles({
|
||||
transition: '250',
|
||||
borderRadius: '12',
|
||||
}),
|
||||
{
|
||||
':hover': {
|
||||
background: vars.color.lightGrayOverlay,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
export const MenuRow = style([
|
||||
hover,
|
||||
sprinkles({
|
||||
color: 'blackBlue',
|
||||
paddingY: '12',
|
||||
width: 'max',
|
||||
marginRight: '52',
|
||||
paddingY: '8',
|
||||
paddingX: '8',
|
||||
width: 'full',
|
||||
whiteSpace: 'nowrap',
|
||||
}),
|
||||
{
|
||||
lineHeight: '24px',
|
||||
@@ -22,8 +36,10 @@ export const PrimaryText = style([
|
||||
])
|
||||
|
||||
export const SecondaryText = style([
|
||||
hover,
|
||||
sprinkles({
|
||||
paddingY: '8',
|
||||
paddingX: '8',
|
||||
color: 'darkGray',
|
||||
}),
|
||||
{
|
||||
@@ -34,6 +50,7 @@ export const SecondaryText = style([
|
||||
export const Separator = style([
|
||||
sprinkles({
|
||||
height: '0',
|
||||
marginX: '16',
|
||||
}),
|
||||
{
|
||||
borderTop: 'solid',
|
||||
@@ -45,6 +62,6 @@ export const Separator = style([
|
||||
export const IconRow = style([
|
||||
sprinkles({
|
||||
paddingX: '16',
|
||||
paddingY: '8',
|
||||
justifyContent: { sm: 'center', md: 'flex-start' },
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import FeatureFlagModal from 'components/FeatureFlagModal/FeatureFlagModal'
|
||||
import { PrivacyPolicyModal } from 'components/PrivacyPolicy'
|
||||
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
@@ -9,6 +11,7 @@ import {
|
||||
EllipsisIcon,
|
||||
GithubIconMenu,
|
||||
GovernanceIcon,
|
||||
ThinTagIcon,
|
||||
TwitterIconMenu,
|
||||
} from 'nft/components/icons'
|
||||
import { body, bodySmall } from 'nft/css/common.css'
|
||||
@@ -114,6 +117,7 @@ export const MenuDropdown = () => {
|
||||
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
|
||||
const togglePrivacyPolicy = useToggleModal(ApplicationModal.PRIVACY_POLICY)
|
||||
const openFeatureFlagsModal = useToggleModal(ApplicationModal.FEATURE_FLAGS)
|
||||
const nftFlag = useNftFlag()
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)
|
||||
@@ -121,50 +125,78 @@ export const MenuDropdown = () => {
|
||||
return (
|
||||
<>
|
||||
<Box position="relative" ref={ref}>
|
||||
<NavIcon onClick={toggleOpen}>
|
||||
<EllipsisIcon width={28} height={28} />
|
||||
<NavIcon isActive={isOpen} onClick={toggleOpen}>
|
||||
<EllipsisIcon width={20} height={20} />
|
||||
</NavIcon>
|
||||
|
||||
{isOpen && (
|
||||
<NavDropdown top={60}>
|
||||
<Column gap="12">
|
||||
<Column paddingX="16" gap="4">
|
||||
<NavDropdown top={{ sm: 'unset', lg: '56' }} bottom={{ sm: '56', lg: 'unset' }} right="0">
|
||||
<Column gap="16">
|
||||
<Column paddingX="8" gap="4">
|
||||
{nftFlag === NftVariant.Enabled && (
|
||||
<PrimaryMenuRow to="/nfts/sell" close={toggleOpen}>
|
||||
<Icon>
|
||||
<ThinTagIcon width={24} height={24} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>Sell NFTs</Trans>
|
||||
</PrimaryMenuRow.Text>
|
||||
</PrimaryMenuRow>
|
||||
)}
|
||||
<PrimaryMenuRow to="/vote" close={toggleOpen}>
|
||||
<Icon>
|
||||
<GovernanceIcon width={24} height={24} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>Vote in governance</PrimaryMenuRow.Text>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>Vote in governance</Trans>
|
||||
</PrimaryMenuRow.Text>
|
||||
</PrimaryMenuRow>
|
||||
<PrimaryMenuRow href="https://info.uniswap.org/#/">
|
||||
<Icon>
|
||||
<BarChartIcon width={24} height={24} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>View token analytics ↗</PrimaryMenuRow.Text>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>View token analytics</Trans>
|
||||
</PrimaryMenuRow.Text>
|
||||
</PrimaryMenuRow>
|
||||
</Column>
|
||||
<Separator />
|
||||
<Column paddingX="16" gap="4">
|
||||
<SecondaryLinkedText href="https://help.uniswap.org/en/">Help center ↗</SecondaryLinkedText>
|
||||
<SecondaryLinkedText href="https://docs.uniswap.org/">Documentation ↗</SecondaryLinkedText>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection={{ sm: 'row', md: 'column' }}
|
||||
flexWrap="wrap"
|
||||
alignItems={{ sm: 'center', md: 'flex-start' }}
|
||||
paddingX="8"
|
||||
>
|
||||
<SecondaryLinkedText href="https://help.uniswap.org/en/">
|
||||
<Trans>Help center</Trans> ↗
|
||||
</SecondaryLinkedText>
|
||||
<SecondaryLinkedText href="https://docs.uniswap.org/">
|
||||
<Trans>Documentation</Trans> ↗
|
||||
</SecondaryLinkedText>
|
||||
<SecondaryLinkedText
|
||||
onClick={() => {
|
||||
toggleOpen()
|
||||
togglePrivacyPolicy()
|
||||
}}
|
||||
>{`Legal & Privacy`}</SecondaryLinkedText>
|
||||
>
|
||||
<Trans>Legal & Privacy</Trans> ↗
|
||||
</SecondaryLinkedText>
|
||||
{(isDevelopmentEnv() || isStagingEnv()) && (
|
||||
<SecondaryLinkedText onClick={openFeatureFlagsModal}>{`Feature Flags`}</SecondaryLinkedText>
|
||||
<SecondaryLinkedText onClick={openFeatureFlagsModal}>
|
||||
<Trans>Feature Flags</Trans>
|
||||
</SecondaryLinkedText>
|
||||
)}
|
||||
</Column>
|
||||
</Box>
|
||||
<IconRow>
|
||||
<Icon href="https://discord.com/invite/FCfyBSbCU5">
|
||||
<DiscordIconMenu width={24} height={24} color={themeVars.colors.darkGray} />
|
||||
<DiscordIconMenu className={styles.hover} width={24} height={24} color={themeVars.colors.darkGray} />
|
||||
</Icon>
|
||||
<Icon href="https://twitter.com/Uniswap">
|
||||
<TwitterIconMenu width={24} height={24} color={themeVars.colors.darkGray} />
|
||||
<TwitterIconMenu className={styles.hover} width={24} height={24} color={themeVars.colors.darkGray} />
|
||||
</Icon>
|
||||
<Icon href="https://github.com/Uniswap">
|
||||
<GithubIconMenu width={24} height={24} color={themeVars.colors.darkGray} />
|
||||
<GithubIconMenu className={styles.hover} width={24} height={24} color={themeVars.colors.darkGray} />
|
||||
</Icon>
|
||||
</IconRow>
|
||||
</Column>
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { subhead } from 'nft/css/common.css'
|
||||
|
||||
import { sprinkles } from '../../nft/css/sprinkles.css'
|
||||
|
||||
export const sidebar = style([
|
||||
sprinkles({
|
||||
display: 'flex',
|
||||
position: 'fixed',
|
||||
background: 'white',
|
||||
height: 'full',
|
||||
top: '0',
|
||||
left: '0',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
paddingBottom: '16',
|
||||
justifyContent: 'space-between',
|
||||
}),
|
||||
{
|
||||
zIndex: 20,
|
||||
},
|
||||
])
|
||||
|
||||
export const icon = style([
|
||||
sprinkles({
|
||||
width: '32',
|
||||
height: '32',
|
||||
}),
|
||||
])
|
||||
|
||||
export const iconContainer = style([
|
||||
sprinkles({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
color: 'darkGray',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
justifyContent: 'flex-end',
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
padding: '6',
|
||||
}),
|
||||
])
|
||||
|
||||
export const linkRow = style([
|
||||
subhead,
|
||||
sprinkles({
|
||||
color: 'blackBlue',
|
||||
width: 'full',
|
||||
paddingLeft: '16',
|
||||
paddingY: '12',
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
{
|
||||
lineHeight: '24px',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
])
|
||||
|
||||
export const activeLinkRow = style([
|
||||
linkRow,
|
||||
sprinkles({
|
||||
background: 'lightGrayButton',
|
||||
}),
|
||||
])
|
||||
|
||||
export const separator = style([
|
||||
sprinkles({
|
||||
height: '0',
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'medGray',
|
||||
borderWidth: '1px',
|
||||
marginY: '8',
|
||||
marginX: '16',
|
||||
}),
|
||||
])
|
||||
|
||||
export const extraLinkRow = style([
|
||||
subhead,
|
||||
sprinkles({
|
||||
width: 'full',
|
||||
color: 'blackBlue',
|
||||
paddingY: '12',
|
||||
paddingLeft: '16',
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
{
|
||||
lineHeight: '24px',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
])
|
||||
|
||||
export const bottomExternalLinks = style([
|
||||
sprinkles({
|
||||
gap: '4',
|
||||
paddingX: '4',
|
||||
width: 'max',
|
||||
flexWrap: 'wrap',
|
||||
}),
|
||||
])
|
||||
|
||||
export const bottomJointExternalLinksContainer = style([
|
||||
sprinkles({
|
||||
paddingX: '8',
|
||||
paddingY: '4',
|
||||
color: 'darkGray',
|
||||
fontWeight: 'medium',
|
||||
fontSize: '12',
|
||||
}),
|
||||
{
|
||||
lineHeight: '20px',
|
||||
},
|
||||
])
|
||||
|
||||
export const IconRow = style([
|
||||
sprinkles({
|
||||
gap: '12',
|
||||
width: 'max',
|
||||
}),
|
||||
])
|
||||
@@ -1,233 +0,0 @@
|
||||
import FeatureFlagModal from 'components/FeatureFlagModal/FeatureFlagModal'
|
||||
import { PrivacyPolicyModal } from 'components/PrivacyPolicy'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import {
|
||||
BarChartIconMobile,
|
||||
BulletIcon,
|
||||
CloseIcon,
|
||||
DiscordIconMenuMobile,
|
||||
GithubIconMenuMobile,
|
||||
GovernanceIconMobile,
|
||||
HamburgerIcon,
|
||||
TwitterIconMenuMobile,
|
||||
} from 'nft/components/icons'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { ReactNode, useReducer } from 'react'
|
||||
import { NavLink, NavLinkProps, useLocation } from 'react-router-dom'
|
||||
import { useToggleModal, useTogglePrivacyPolicy } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import { isDevelopmentEnv, isStagingEnv } from 'utils/env'
|
||||
|
||||
import * as styles from './MobileSidebar.css'
|
||||
import { NavIcon } from './NavIcon'
|
||||
|
||||
interface NavLinkRowProps {
|
||||
href: string
|
||||
id?: NavLinkProps['id']
|
||||
isActive?: boolean
|
||||
close: () => void
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const NavLinkRow = ({ href, id, isActive, close, children }: NavLinkRowProps) => {
|
||||
return (
|
||||
<NavLink to={href} className={isActive ? styles.activeLinkRow : styles.linkRow} id={id} onClick={close}>
|
||||
{children}
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
|
||||
const ExtraLinkRow = ({
|
||||
to,
|
||||
href,
|
||||
close,
|
||||
children,
|
||||
}: {
|
||||
to?: NavLinkProps['to']
|
||||
href?: string
|
||||
close: () => void
|
||||
children: ReactNode
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{to ? (
|
||||
<NavLink to={to} className={styles.extraLinkRow}>
|
||||
<Row gap="12" onClick={close}>
|
||||
{children}
|
||||
</Row>
|
||||
</NavLink>
|
||||
) : (
|
||||
<Row
|
||||
as="a"
|
||||
href={href}
|
||||
target={'_blank'}
|
||||
rel={'noopener noreferrer'}
|
||||
gap="12"
|
||||
onClick={close}
|
||||
className={styles.extraLinkRow}
|
||||
>
|
||||
{children}
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const BottomExternalLink = ({
|
||||
href,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
href?: string
|
||||
onClick?: () => void
|
||||
children: ReactNode
|
||||
}) => {
|
||||
return (
|
||||
<Box
|
||||
as={href ? 'a' : 'div'}
|
||||
href={href ?? undefined}
|
||||
target={href ? '_blank' : undefined}
|
||||
rel={href ? 'noopener noreferrer' : undefined}
|
||||
className={`${styles.bottomJointExternalLinksContainer}`}
|
||||
onClick={onClick}
|
||||
cursor="pointer"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const Icon = ({ href, children }: { href?: string; children: ReactNode }) => {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
as={href ? 'a' : 'div'}
|
||||
href={href ?? undefined}
|
||||
target={href ? '_blank' : undefined}
|
||||
rel={href ? 'noopener noreferrer' : undefined}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
color="blackBlue"
|
||||
background="none"
|
||||
border="none"
|
||||
justifyContent="center"
|
||||
textAlign="center"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const IconRow = ({ children }: { children: ReactNode }) => {
|
||||
return <Row className={styles.IconRow}>{children}</Row>
|
||||
}
|
||||
|
||||
const Seperator = () => {
|
||||
return <Box className={styles.separator} />
|
||||
}
|
||||
|
||||
export const MobileSideBar = () => {
|
||||
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
|
||||
const togglePrivacyPolicy = useTogglePrivacyPolicy()
|
||||
const openFeatureFlagsModal = useToggleModal(ApplicationModal.FEATURE_FLAGS)
|
||||
const { pathname } = useLocation()
|
||||
const isPoolActive =
|
||||
pathname.startsWith('/pool') ||
|
||||
pathname.startsWith('/add') ||
|
||||
pathname.startsWith('/remove') ||
|
||||
pathname.startsWith('/increase') ||
|
||||
pathname.startsWith('/find')
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavIcon onClick={toggleOpen}>
|
||||
<HamburgerIcon width={28} height={28} />
|
||||
</NavIcon>
|
||||
{isOpen && (
|
||||
<Portal>
|
||||
<Column className={styles.sidebar}>
|
||||
<Column>
|
||||
<Row justifyContent="flex-end" marginTop="14" marginBottom="20" marginRight="8">
|
||||
<Box as="button" onClick={toggleOpen} className={styles.iconContainer}>
|
||||
<CloseIcon className={styles.icon} />
|
||||
</Box>
|
||||
</Row>
|
||||
<Column gap="4">
|
||||
<NavLinkRow href="/swap" close={toggleOpen} isActive={pathname.startsWith('/swap')}>
|
||||
Swap
|
||||
</NavLinkRow>
|
||||
<NavLinkRow href="/tokens" close={toggleOpen} isActive={pathname.startsWith('/tokens')}>
|
||||
Tokens
|
||||
</NavLinkRow>
|
||||
<NavLinkRow href="/pool" id={'pool-nav-link'} isActive={isPoolActive} close={toggleOpen}>
|
||||
Pool
|
||||
</NavLinkRow>
|
||||
</Column>
|
||||
<Seperator />
|
||||
<Column gap="4">
|
||||
<ExtraLinkRow to="/vote" close={toggleOpen}>
|
||||
<Icon>
|
||||
<GovernanceIconMobile width={24} height={24} />
|
||||
</Icon>
|
||||
Vote in governance
|
||||
</ExtraLinkRow>
|
||||
<ExtraLinkRow href="https://info.uniswap.org/#/" close={toggleOpen}>
|
||||
<Icon>
|
||||
<BarChartIconMobile width={24} height={24} />
|
||||
</Icon>
|
||||
View token analytics ↗
|
||||
</ExtraLinkRow>
|
||||
</Column>
|
||||
</Column>
|
||||
<Column>
|
||||
<Row justifyContent="center" marginBottom="12" flexWrap="wrap">
|
||||
<Row className={styles.bottomExternalLinks}>
|
||||
<BottomExternalLink href="https://help.uniswap.org/en/" onClick={toggleOpen}>
|
||||
Help center ↗
|
||||
</BottomExternalLink>
|
||||
<BulletIcon />
|
||||
<BottomExternalLink href="https://docs.uniswap.org/" onClick={toggleOpen}>
|
||||
Documentation ↗
|
||||
</BottomExternalLink>
|
||||
<BulletIcon />
|
||||
<BottomExternalLink
|
||||
onClick={() => {
|
||||
toggleOpen()
|
||||
togglePrivacyPolicy()
|
||||
}}
|
||||
>
|
||||
{`Legal & Privacy`}
|
||||
</BottomExternalLink>
|
||||
</Row>
|
||||
{(isDevelopmentEnv() || isStagingEnv()) && (
|
||||
<>
|
||||
<BulletIcon />
|
||||
<BottomExternalLink onClick={openFeatureFlagsModal}>{`Feature Flags`}</BottomExternalLink>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
<Row justifyContent="center">
|
||||
<IconRow>
|
||||
<Icon href="https://discord.com/invite/FCfyBSbCU5">
|
||||
<DiscordIconMenuMobile width={32} height={32} color={themeVars.colors.darkGray} />
|
||||
</Icon>
|
||||
<Icon href="https://twitter.com/Uniswap">
|
||||
<TwitterIconMenuMobile width={32} height={32} color={themeVars.colors.darkGray} />
|
||||
</Icon>
|
||||
<Icon href="https://github.com/Uniswap">
|
||||
<GithubIconMenuMobile width={32} height={32} color={themeVars.colors.darkGray} />
|
||||
</Icon>
|
||||
</IconRow>
|
||||
</Row>
|
||||
</Column>
|
||||
</Column>
|
||||
</Portal>
|
||||
)}
|
||||
<PrivacyPolicyModal />
|
||||
<FeatureFlagModal />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -2,18 +2,44 @@ import { style } from '@vanilla-extract/css'
|
||||
|
||||
import { sprinkles } from '../../nft/css/sprinkles.css'
|
||||
|
||||
export const NavDropdown = style([
|
||||
const baseNavDropdown = style([
|
||||
sprinkles({
|
||||
position: 'absolute',
|
||||
background: 'white95',
|
||||
borderRadius: '12',
|
||||
background: 'lightGray',
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'medGray',
|
||||
paddingY: '20',
|
||||
borderWidth: '1px',
|
||||
paddingBottom: '8',
|
||||
paddingTop: '8',
|
||||
zIndex: '2',
|
||||
}),
|
||||
{
|
||||
boxShadow: '0px 4px 12px 0px #00000026',
|
||||
zIndex: 10,
|
||||
},
|
||||
])
|
||||
|
||||
export const NavDropdown = style([
|
||||
baseNavDropdown,
|
||||
sprinkles({
|
||||
position: 'absolute',
|
||||
borderRadius: '12',
|
||||
}),
|
||||
{},
|
||||
])
|
||||
|
||||
export const mobileNavDropdown = style([
|
||||
baseNavDropdown,
|
||||
sprinkles({
|
||||
position: 'fixed',
|
||||
borderTopRightRadius: '12',
|
||||
borderTopLeftRadius: '12',
|
||||
top: 'unset',
|
||||
bottom: '56',
|
||||
left: '0',
|
||||
right: '0',
|
||||
width: 'full',
|
||||
}),
|
||||
{
|
||||
borderRightWidth: '0px',
|
||||
borderLeftWidth: '0px',
|
||||
},
|
||||
])
|
||||
|
||||
@@ -1,37 +1,12 @@
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { ReactNode } from 'react'
|
||||
import { Box, BoxProps } from 'nft/components/Box'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { ForwardedRef, forwardRef } from 'react'
|
||||
|
||||
import * as styles from './NavDropdown.css'
|
||||
|
||||
interface NavDropdownProps {
|
||||
top: number
|
||||
right?: number
|
||||
leftAligned?: boolean
|
||||
horizontalPadding?: boolean
|
||||
centerHorizontally?: boolean
|
||||
children: ReactNode
|
||||
}
|
||||
export const NavDropdown = forwardRef((props: BoxProps, ref: ForwardedRef<HTMLElement>) => {
|
||||
const isMobile = useIsMobile()
|
||||
return <Box ref={ref} className={isMobile ? styles.mobileNavDropdown : styles.NavDropdown} {...props} />
|
||||
})
|
||||
|
||||
export const NavDropdown = ({
|
||||
top,
|
||||
centerHorizontally,
|
||||
leftAligned,
|
||||
horizontalPadding,
|
||||
children,
|
||||
}: NavDropdownProps) => {
|
||||
return (
|
||||
<Box
|
||||
paddingX={horizontalPadding ? '16' : undefined}
|
||||
style={{
|
||||
top: `${top}px`,
|
||||
left: centerHorizontally ? '50%' : leftAligned ? '0px' : 'auto',
|
||||
right: centerHorizontally || leftAligned ? 'auto' : '10px',
|
||||
transform: centerHorizontally ? 'translateX(-50%)' : 'unset',
|
||||
zIndex: 3,
|
||||
}}
|
||||
className={styles.NavDropdown}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
NavDropdown.displayName = 'NavDropdown'
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
|
||||
import { sprinkles, themeVars } from '../../nft/css/sprinkles.css'
|
||||
import { sprinkles, vars } from '../../nft/css/sprinkles.css'
|
||||
|
||||
export const navIcon = style([
|
||||
sprinkles({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
color: 'blackBlue',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
padding: '8',
|
||||
padding: '10',
|
||||
borderRadius: '8',
|
||||
transition: '250',
|
||||
}),
|
||||
{
|
||||
':hover': {
|
||||
background: themeVars.colors.lightGrayContainer,
|
||||
background: vars.color.lightGrayOverlay,
|
||||
},
|
||||
zIndex: 2,
|
||||
zIndex: 1,
|
||||
},
|
||||
])
|
||||
|
||||
@@ -5,12 +5,19 @@ import * as styles from './NavIcon.css'
|
||||
|
||||
interface NavIconProps {
|
||||
children: ReactNode
|
||||
isActive?: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export const NavIcon = ({ children, onClick }: NavIconProps) => {
|
||||
export const NavIcon = ({ children, isActive, onClick }: NavIconProps) => {
|
||||
return (
|
||||
<Box as="button" className={styles.navIcon} onClick={onClick}>
|
||||
<Box
|
||||
as="button"
|
||||
className={styles.navIcon}
|
||||
background={isActive ? 'accentActiveSoft' : 'none'}
|
||||
color={isActive ? 'blackBlue' : 'darkGray'}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
|
||||
import { subhead } from '../../nft/css/common.css'
|
||||
import { sprinkles } from '../../nft/css/sprinkles.css'
|
||||
import { sprinkles, vars } from '../../nft/css/sprinkles.css'
|
||||
|
||||
export const nav = style([
|
||||
sprinkles({
|
||||
@@ -20,7 +20,7 @@ export const nav = style([
|
||||
export const logoContainer = style([
|
||||
sprinkles({
|
||||
display: 'flex',
|
||||
marginRight: { mobile: '12', desktopXl: '20' },
|
||||
marginRight: { sm: '12', xxl: '20' },
|
||||
alignItems: 'center',
|
||||
}),
|
||||
])
|
||||
@@ -34,23 +34,14 @@ export const logo = style([
|
||||
|
||||
export const baseContainer = style([
|
||||
sprinkles({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
])
|
||||
|
||||
export const baseMobileContainer = style([
|
||||
sprinkles({
|
||||
display: 'flex',
|
||||
width: 'full',
|
||||
alignItems: 'center',
|
||||
marginY: '2',
|
||||
}),
|
||||
])
|
||||
|
||||
export const baseSideContainer = style([
|
||||
baseContainer,
|
||||
sprinkles({
|
||||
display: 'flex',
|
||||
width: 'full',
|
||||
flex: '1',
|
||||
flexShrink: '2',
|
||||
@@ -64,19 +55,13 @@ export const leftSideContainer = style([
|
||||
}),
|
||||
])
|
||||
|
||||
export const leftSideMobileContainer = style([
|
||||
baseMobileContainer,
|
||||
sprinkles({
|
||||
justifyContent: 'flex-start',
|
||||
}),
|
||||
])
|
||||
|
||||
export const middleContainer = style([
|
||||
baseContainer,
|
||||
sprinkles({
|
||||
flex: '1',
|
||||
flexShrink: '1',
|
||||
justifyContent: 'center',
|
||||
display: { sm: 'none', xl: 'flex' },
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -94,10 +79,17 @@ const baseMenuItem = style([
|
||||
paddingX: '16',
|
||||
marginY: '4',
|
||||
borderRadius: '12',
|
||||
transition: '250',
|
||||
height: 'min',
|
||||
width: 'full',
|
||||
textAlign: 'center',
|
||||
}),
|
||||
{
|
||||
lineHeight: '24px',
|
||||
textDecoration: 'none',
|
||||
':hover': {
|
||||
background: vars.color.lightGrayOverlay,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
@@ -108,30 +100,25 @@ export const menuItem = style([
|
||||
}),
|
||||
])
|
||||
|
||||
export const rightSideMobileContainer = style([
|
||||
baseMobileContainer,
|
||||
sprinkles({
|
||||
justifyContent: 'flex-end',
|
||||
}),
|
||||
])
|
||||
|
||||
export const activeMenuItem = style([
|
||||
baseMenuItem,
|
||||
sprinkles({
|
||||
color: 'blackBlue',
|
||||
background: 'backgroundFloating',
|
||||
}),
|
||||
])
|
||||
|
||||
export const mobileWalletContainer = style([
|
||||
export const mobileBottomBar = style([
|
||||
sprinkles({
|
||||
position: 'fixed',
|
||||
display: 'flex',
|
||||
display: { sm: 'flex', lg: 'none' },
|
||||
bottom: '0',
|
||||
right: '1/2',
|
||||
marginY: '0',
|
||||
marginX: 'auto',
|
||||
right: '0',
|
||||
left: '0',
|
||||
justifyContent: 'space-between',
|
||||
paddingY: '4',
|
||||
paddingX: '8',
|
||||
height: '56',
|
||||
background: 'lightGray',
|
||||
}),
|
||||
{
|
||||
transform: 'translate(50%,-50%)',
|
||||
},
|
||||
])
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { ChainSwitcher } from 'components/NavBar/ChainSwitcher'
|
||||
import { MenuDropdown } from 'components/NavBar/MenuDropdown'
|
||||
import * as styles from 'components/NavBar/Navbar.css'
|
||||
import { SearchBar } from 'components/NavBar/SearchBar'
|
||||
import { ShoppingBag } from 'components/NavBar/ShoppingBag'
|
||||
import Web3Status from 'components/Web3Status'
|
||||
import { useWindowSize } from 'hooks/useWindowSize'
|
||||
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, UniIconMobile } from '../../nft/components/icons'
|
||||
import { breakpoints } from '../../nft/css/sprinkles.css'
|
||||
import { ChainSwitcher } from './ChainSwitcher'
|
||||
import { MenuDropdown } from './MenuDropdown'
|
||||
import { MobileSideBar } from './MobileSidebar'
|
||||
import * as styles from './Navbar.css'
|
||||
|
||||
interface MenuItemProps {
|
||||
href: string
|
||||
id?: NavLinkProps['id']
|
||||
@@ -32,39 +32,9 @@ const MenuItem = ({ href, id, isActive, children }: MenuItemProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
const MobileNavbar = () => {
|
||||
return (
|
||||
<>
|
||||
<nav className={styles.nav}>
|
||||
<Box display="flex" height="full" flexWrap="nowrap" alignItems="stretch">
|
||||
<Box className={styles.leftSideMobileContainer}>
|
||||
<Box as="a" href="#/swap" className={styles.logoContainer}>
|
||||
<UniIconMobile width="44" height="44" className={styles.logo} />
|
||||
</Box>
|
||||
<ChainSwitcher isMobile={true} />
|
||||
</Box>
|
||||
<Box className={styles.rightSideMobileContainer}>
|
||||
<Row gap="16">
|
||||
{/* TODO add Searchbar */}
|
||||
<MobileSideBar />
|
||||
</Row>
|
||||
</Box>
|
||||
</Box>
|
||||
</nav>
|
||||
<Box className={styles.mobileWalletContainer}>
|
||||
<Web3Status />
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Navbar = () => {
|
||||
const { width: windowWidth } = useWindowSize()
|
||||
const PageTabs = () => {
|
||||
const { pathname } = useLocation()
|
||||
|
||||
if (windowWidth && windowWidth < breakpoints.desktopXl) {
|
||||
return <MobileNavbar />
|
||||
}
|
||||
const nftFlag = useNftFlag()
|
||||
|
||||
const isPoolActive =
|
||||
pathname.startsWith('/pool') ||
|
||||
@@ -74,34 +44,72 @@ const Navbar = () => {
|
||||
pathname.startsWith('/find')
|
||||
|
||||
return (
|
||||
<nav className={styles.nav}>
|
||||
<Box display="flex" height="full" flexWrap="nowrap" alignItems="stretch">
|
||||
<Box className={styles.leftSideContainer}>
|
||||
<Box as="a" href="#/swap" className={styles.logoContainer}>
|
||||
<UniIcon width="48" height="48" className={styles.logo} />
|
||||
<>
|
||||
<MenuItem href="/swap" isActive={pathname.startsWith('/swap')}>
|
||||
<Trans>Swap</Trans>
|
||||
</MenuItem>
|
||||
<MenuItem href="/tokens" isActive={pathname.startsWith('/tokens')}>
|
||||
<Trans>Tokens</Trans>
|
||||
</MenuItem>
|
||||
{nftFlag === NftVariant.Enabled && (
|
||||
<MenuItem href="/nfts" isActive={pathname.startsWith('/nfts')}>
|
||||
<Trans>NFTs</Trans>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem href="/pool" id={'pool-nav-link'} isActive={isPoolActive}>
|
||||
<Trans>Pool</Trans>
|
||||
</MenuItem>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Navbar = () => {
|
||||
const { pathname } = useLocation()
|
||||
const isNftPage = pathname.startsWith('/nfts')
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className={styles.nav}>
|
||||
<Box display="flex" height="full" flexWrap="nowrap" alignItems="stretch">
|
||||
<Box className={styles.leftSideContainer}>
|
||||
<Box as="a" href="#/swap" className={styles.logoContainer}>
|
||||
<UniIcon width="48" height="48" className={styles.logo} />
|
||||
</Box>
|
||||
<Box display={{ sm: 'flex', lg: 'none' }}>
|
||||
<ChainSwitcher leftAlign={true} />
|
||||
</Box>
|
||||
<Row gap="8" display={{ sm: 'none', lg: 'flex' }}>
|
||||
<PageTabs />
|
||||
</Row>
|
||||
</Box>
|
||||
<Box className={styles.middleContainer}>
|
||||
<SearchBar />
|
||||
</Box>
|
||||
<Box className={styles.rightSideContainer}>
|
||||
<Row gap="12">
|
||||
<Box display={{ sm: 'flex', xl: 'none' }}>
|
||||
<SearchBar />
|
||||
</Box>
|
||||
<Box display={{ sm: 'none', lg: 'flex' }}>
|
||||
<MenuDropdown />
|
||||
</Box>
|
||||
{isNftPage && <ShoppingBag />}
|
||||
<Box display={{ sm: 'none', lg: 'flex' }}>
|
||||
<ChainSwitcher />
|
||||
</Box>
|
||||
|
||||
<Web3Status />
|
||||
</Row>
|
||||
</Box>
|
||||
<Row gap="8">
|
||||
<MenuItem href="/swap" isActive={pathname.startsWith('/swap')}>
|
||||
Swap
|
||||
</MenuItem>
|
||||
<MenuItem href="/tokens" isActive={pathname.startsWith('/explore')}>
|
||||
Tokens
|
||||
</MenuItem>
|
||||
<MenuItem href="/pool" id={'pool-nav-link'} isActive={isPoolActive}>
|
||||
Pool
|
||||
</MenuItem>
|
||||
</Row>
|
||||
</Box>
|
||||
<Box className={styles.middleContainer}>{/* TODO add Searchbar */}</Box>
|
||||
<Box className={styles.rightSideContainer}>
|
||||
<Row gap="12">
|
||||
<MenuDropdown />
|
||||
<ChainSwitcher />
|
||||
<Web3Status />
|
||||
</Row>
|
||||
</nav>
|
||||
<Box className={styles.mobileBottomBar}>
|
||||
<PageTabs />
|
||||
<Box marginY="4">
|
||||
<MenuDropdown />
|
||||
</Box>
|
||||
</Box>
|
||||
</nav>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
215
src/components/NavBar/SearchBar.css.ts
Normal file
215
src/components/NavBar/SearchBar.css.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
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({
|
||||
paddingY: '12',
|
||||
width: { sm: 'viewWidth' },
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '1px',
|
||||
borderColor: 'medGray',
|
||||
}),
|
||||
{
|
||||
'@media': {
|
||||
[`screen and (min-width: ${breakpoints.sm}px)`]: {
|
||||
width: `${DESKTOP_NAVBAR_WIDTH}px`,
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
export const searchBarContainer = style([
|
||||
sprinkles({
|
||||
right: '0',
|
||||
top: '0',
|
||||
zIndex: '3',
|
||||
display: 'inline-block',
|
||||
}),
|
||||
{
|
||||
'@media': {
|
||||
[`screen and (min-width: ${breakpoints.lg}px)`]: {
|
||||
right: `-${DESKTOP_NAVBAR_WIDTH / 2 - MAGNIFYING_GLASS_ICON_WIDTH}px`,
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
export const searchBar = style([
|
||||
baseSearchStyle,
|
||||
sprinkles({
|
||||
color: 'placeholder',
|
||||
paddingX: '16',
|
||||
cursor: 'pointer',
|
||||
background: 'lightGray',
|
||||
}),
|
||||
])
|
||||
|
||||
export const searchBarInput = style([
|
||||
sprinkles({
|
||||
padding: '0',
|
||||
fontWeight: 'normal',
|
||||
fontSize: '16',
|
||||
color: { default: 'blackBlue', placeholder: 'placeholder' },
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
}),
|
||||
{
|
||||
lineHeight: '24px',
|
||||
},
|
||||
])
|
||||
|
||||
export const searchBarDropdown = style([
|
||||
baseSearchStyle,
|
||||
sprinkles({
|
||||
borderBottomLeftRadius: '12',
|
||||
borderBottomRightRadius: '12',
|
||||
background: 'lightGray',
|
||||
height: { sm: 'viewHeight', md: 'auto' },
|
||||
}),
|
||||
{
|
||||
borderTop: 'none',
|
||||
},
|
||||
])
|
||||
|
||||
export const suggestionRow = style([
|
||||
sprinkles({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingY: '8',
|
||||
paddingX: '16',
|
||||
}),
|
||||
{
|
||||
':hover': {
|
||||
cursor: 'pointer',
|
||||
background: vars.color.lightGrayOverlay,
|
||||
},
|
||||
textDecoration: 'none',
|
||||
},
|
||||
])
|
||||
|
||||
export const suggestionImage = sprinkles({
|
||||
width: '36',
|
||||
height: '36',
|
||||
borderRadius: 'round',
|
||||
marginRight: '8',
|
||||
})
|
||||
|
||||
export const suggestionPrimaryContainer = style([
|
||||
sprinkles({
|
||||
alignItems: 'flex-start',
|
||||
width: 'full',
|
||||
}),
|
||||
])
|
||||
|
||||
export const suggestionSecondaryContainer = sprinkles({
|
||||
textAlign: 'right',
|
||||
alignItems: 'flex-end',
|
||||
})
|
||||
|
||||
export const primaryText = style([
|
||||
subhead,
|
||||
sprinkles({
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
color: 'blackBlue',
|
||||
}),
|
||||
{
|
||||
lineHeight: '24px',
|
||||
},
|
||||
])
|
||||
|
||||
export const secondaryText = style([
|
||||
buttonTextSmall,
|
||||
sprinkles({
|
||||
color: 'darkGray',
|
||||
}),
|
||||
{
|
||||
lineHeight: '20px',
|
||||
},
|
||||
])
|
||||
|
||||
export const imageHolder = style([
|
||||
suggestionImage,
|
||||
sprinkles({
|
||||
background: 'loading',
|
||||
flexShrink: '0',
|
||||
}),
|
||||
])
|
||||
|
||||
export const suggestionIcon = sprinkles({
|
||||
display: 'flex',
|
||||
flexShrink: '0',
|
||||
})
|
||||
|
||||
export const sectionHeader = style([
|
||||
subheadSmall,
|
||||
sprinkles({
|
||||
color: 'darkGray',
|
||||
}),
|
||||
{
|
||||
lineHeight: '20px',
|
||||
},
|
||||
])
|
||||
|
||||
export const notFoundContainer = style([
|
||||
sectionHeader,
|
||||
sprinkles({
|
||||
paddingY: '4',
|
||||
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',
|
||||
},
|
||||
},
|
||||
})
|
||||
442
src/components/NavBar/SearchBar.tsx
Normal file
442
src/components/NavBar/SearchBar.tsx
Normal file
@@ -0,0 +1,442 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import clsx from 'clsx'
|
||||
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
|
||||
import useDebounce from 'hooks/useDebounce'
|
||||
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, 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, ReactNode, useEffect, useMemo, useReducer, useRef, useState } from 'react'
|
||||
import { useQuery } from 'react-query'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ClockIcon,
|
||||
MagnifyingGlassIcon,
|
||||
NavMagnifyingGlassIcon,
|
||||
TrendingArrow,
|
||||
} from '../../nft/components/icons'
|
||||
import { NavIcon } from './NavIcon'
|
||||
import * as styles from './SearchBar.css'
|
||||
import { CollectionRow, SkeletonRow, TokenRow } from './SuggestionRow'
|
||||
|
||||
interface SearchBarDropdownSectionProps {
|
||||
toggleOpen: () => void
|
||||
suggestions: (GenieCollection | FungibleToken)[]
|
||||
header: JSX.Element
|
||||
headerIcon?: JSX.Element
|
||||
hoveredIndex: number | undefined
|
||||
startingIndex: number
|
||||
setHoveredIndex: (index: number | undefined) => void
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
export const SearchBarDropdownSection = ({
|
||||
toggleOpen,
|
||||
suggestions,
|
||||
header,
|
||||
headerIcon = undefined,
|
||||
hoveredIndex,
|
||||
startingIndex,
|
||||
setHoveredIndex,
|
||||
isLoading,
|
||||
}: SearchBarDropdownSectionProps) => {
|
||||
return (
|
||||
<Column gap="12">
|
||||
<Row paddingX="16" paddingY="4" gap="8" color="grey300" className={subheadSmall} style={{ lineHeight: '20px' }}>
|
||||
{headerIcon ? headerIcon : null}
|
||||
<Box>{header}</Box>
|
||||
</Row>
|
||||
<Column gap="12">
|
||||
{suggestions.map((suggestion, index) =>
|
||||
isLoading ? (
|
||||
<SkeletonRow key={index} />
|
||||
) : isCollection(suggestion) ? (
|
||||
<CollectionRow
|
||||
key={suggestion.address}
|
||||
collection={suggestion as GenieCollection}
|
||||
isHovered={hoveredIndex === index + startingIndex}
|
||||
setHoveredIndex={setHoveredIndex}
|
||||
toggleOpen={toggleOpen}
|
||||
index={index + startingIndex}
|
||||
/>
|
||||
) : (
|
||||
<TokenRow
|
||||
key={suggestion.address}
|
||||
token={suggestion as FungibleToken}
|
||||
isHovered={hoveredIndex === index + startingIndex}
|
||||
setHoveredIndex={setHoveredIndex}
|
||||
toggleOpen={toggleOpen}
|
||||
index={index + startingIndex}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Column>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
|
||||
interface SearchBarDropdownProps {
|
||||
toggleOpen: () => void
|
||||
tokens: FungibleToken[]
|
||||
collections: GenieCollection[]
|
||||
hasInput: boolean
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
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
|
||||
).slice(0, 2)
|
||||
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 ? (
|
||||
<SearchBarDropdownSection
|
||||
hoveredIndex={hoveredIndex}
|
||||
startingIndex={isNFTPage ? collections.length : 0}
|
||||
setHoveredIndex={setHoveredIndex}
|
||||
toggleOpen={toggleOpen}
|
||||
suggestions={tokens}
|
||||
header={<Trans>Tokens</Trans>}
|
||||
/>
|
||||
) : (
|
||||
<Box className={styles.notFoundContainer}>
|
||||
<Trans>No tokens found.</Trans>
|
||||
</Box>
|
||||
)
|
||||
|
||||
const collectionSearchResults =
|
||||
phase1Flag === NftVariant.Enabled ? (
|
||||
collections.length > 0 ? (
|
||||
<SearchBarDropdownSection
|
||||
hoveredIndex={hoveredIndex}
|
||||
startingIndex={isNFTPage ? 0 : tokens.length}
|
||||
setHoveredIndex={setHoveredIndex}
|
||||
toggleOpen={toggleOpen}
|
||||
suggestions={collections}
|
||||
header={<Trans>NFT Collections</Trans>}
|
||||
/>
|
||||
) : (
|
||||
<Box className={styles.notFoundContainer}>No NFT collections found.</Box>
|
||||
)
|
||||
) : null
|
||||
|
||||
const { data: trendingCollectionResults, isLoading: trendingCollectionsAreLoading } = useQuery(
|
||||
['trendingCollections', 'eth', 'twenty_four_hours'],
|
||||
() => fetchTrendingCollections({ volumeType: 'eth', timePeriod: 'ONE_DAY' as TimePeriod, size: 3 })
|
||||
)
|
||||
|
||||
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, isLoading: trendingTokensAreLoading } = useQuery(
|
||||
['trendingTokens'],
|
||||
() => fetchTrendingTokens(4),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
}
|
||||
)
|
||||
|
||||
const trendingTokensLength = phase1Flag === NftVariant.Enabled ? (isTokenPage ? 3 : 2) : 4
|
||||
|
||||
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) +
|
||||
(isNFTPage || !isTokenPage ? trendingCollections?.length ?? 0 : 0) +
|
||||
(isTokenPage || !isNFTPage ? trendingTokens?.length ?? 0 : 0)
|
||||
|
||||
// Navigate search results via arrow keys
|
||||
useEffect(() => {
|
||||
const keyDownHandler = (event: KeyboardEvent) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
if (!hoveredIndex) {
|
||||
setHoveredIndex(totalSuggestions - 1)
|
||||
} else {
|
||||
setHoveredIndex(hoveredIndex - 1)
|
||||
}
|
||||
} else if (event.key === 'ArrowDown') {
|
||||
event.preventDefault()
|
||||
if (hoveredIndex && hoveredIndex === totalSuggestions - 1) {
|
||||
setHoveredIndex(0)
|
||||
} else {
|
||||
setHoveredIndex((hoveredIndex ?? -1) + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', keyDownHandler)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keyDownHandler)
|
||||
}
|
||||
}, [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">
|
||||
{searchHistory.length > 0 && (
|
||||
<SearchBarDropdownSection
|
||||
hoveredIndex={hoveredIndex}
|
||||
startingIndex={0}
|
||||
setHoveredIndex={setHoveredIndex}
|
||||
toggleOpen={toggleOpen}
|
||||
suggestions={searchHistory}
|
||||
header={<Trans>Recent searches</Trans>}
|
||||
headerIcon={<ClockIcon />}
|
||||
/>
|
||||
)}
|
||||
{!isNFTPage && (
|
||||
<SearchBarDropdownSection
|
||||
hoveredIndex={hoveredIndex}
|
||||
startingIndex={searchHistory.length}
|
||||
setHoveredIndex={setHoveredIndex}
|
||||
toggleOpen={toggleOpen}
|
||||
suggestions={trendingTokens}
|
||||
header={<Trans>Popular tokens</Trans>}
|
||||
headerIcon={<TrendingArrow />}
|
||||
isLoading={trendingTokensAreLoading}
|
||||
/>
|
||||
)}
|
||||
{!isTokenPage && phase1Flag === NftVariant.Enabled && (
|
||||
<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 />}
|
||||
isLoading={trendingCollectionsAreLoading}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
)
|
||||
|
||||
setResultsState(currentState)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
isLoading,
|
||||
tokens,
|
||||
collections,
|
||||
trendingCollections,
|
||||
trendingCollectionsAreLoading,
|
||||
trendingTokens,
|
||||
trendingTokensAreLoading,
|
||||
hoveredIndex,
|
||||
phase1Flag,
|
||||
toggleOpen,
|
||||
searchHistory,
|
||||
hasInput,
|
||||
isNFTPage,
|
||||
isTokenPage,
|
||||
])
|
||||
|
||||
return (
|
||||
<Box className={styles.searchBarDropdown}>
|
||||
<Box opacity={isLoading ? '0.3' : '1'} transition="125">
|
||||
{resultsState}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function isCollection(suggestion: GenieCollection | FungibleToken | TrendingCollection) {
|
||||
return (suggestion as FungibleToken).decimals === undefined
|
||||
}
|
||||
|
||||
export const SearchBar = () => {
|
||||
const [isOpen, toggleOpen] = useReducer((state: boolean) => !state, false)
|
||||
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()
|
||||
})
|
||||
|
||||
const { data: collections, isLoading: collectionsAreLoading } = useQuery(
|
||||
['searchCollections', debouncedSearchValue],
|
||||
() => fetchSearchCollections(debouncedSearchValue),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
}
|
||||
)
|
||||
|
||||
const { data: tokens, isLoading: tokensAreLoading } = useQuery(
|
||||
['searchTokens', debouncedSearchValue],
|
||||
() => fetchSearchTokens(debouncedSearchValue),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
}
|
||||
)
|
||||
|
||||
const isNFTPage = pathname.includes('/nfts')
|
||||
|
||||
const [reducedTokens, reducedCollections] = organizeSearchResults(isNFTPage, tokens ?? [], collections ?? [])
|
||||
|
||||
// close dropdown on escape
|
||||
useEffect(() => {
|
||||
const escapeKeyDownHandler = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isOpen) {
|
||||
event.preventDefault()
|
||||
toggleOpen()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', escapeKeyDownHandler)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', escapeKeyDownHandler)
|
||||
}
|
||||
}, [isOpen, toggleOpen, collections])
|
||||
|
||||
// clear searchbar when changing pages
|
||||
useEffect(() => {
|
||||
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={{ 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 && !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 || isMobileOrTablet ? '0px' : '1px'}
|
||||
onClick={() => !isOpen && toggleOpen()}
|
||||
gap="12"
|
||||
>
|
||||
<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}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
!isOpen && toggleOpen()
|
||||
setSearchValue(event.target.value)
|
||||
}}
|
||||
className={`${styles.searchBarInput} ${
|
||||
showCenteredSearchContent ? styles.searchContentCentered : styles.searchContentLeftAlign
|
||||
}`}
|
||||
value={searchValue}
|
||||
ref={inputRef}
|
||||
width={phase1Flag === NftVariant.Enabled || isOpen ? 'full' : '160'}
|
||||
/>
|
||||
</Row>
|
||||
<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>
|
||||
<NavIcon onClick={toggleOpen}>
|
||||
<NavMagnifyingGlassIcon width={28} height={28} />
|
||||
</NavIcon>
|
||||
{isOpen && <Overlay />}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
195
src/components/NavBar/SuggestionRow.tsx
Normal file
195
src/components/NavBar/SuggestionRow.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import clsx from 'clsx'
|
||||
import uriToHttp from 'lib/utils/uriToHttp'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { vars } from 'nft/css/sprinkles.css'
|
||||
import { useSearchHistory } from 'nft/hooks'
|
||||
import { FungibleToken, GenieCollection } from 'nft/types'
|
||||
import { ethNumberStandardFormatter } from 'nft/utils/currency'
|
||||
import { putCommas } from 'nft/utils/putCommas'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
|
||||
import { VerifiedIcon } from '../../nft/components/icons'
|
||||
import * as styles from './SearchBar.css'
|
||||
|
||||
interface CollectionRowProps {
|
||||
collection: GenieCollection
|
||||
isHovered: boolean
|
||||
setHoveredIndex: (index: number | undefined) => void
|
||||
toggleOpen: () => void
|
||||
index: number
|
||||
}
|
||||
|
||||
export const CollectionRow = ({ collection, isHovered, setHoveredIndex, toggleOpen, index }: CollectionRowProps) => {
|
||||
const [brokenImage, setBrokenImage] = useState(false)
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const addToSearchHistory = useSearchHistory(
|
||||
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
|
||||
)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
addToSearchHistory(collection)
|
||||
toggleOpen()
|
||||
}, [addToSearchHistory, collection, toggleOpen])
|
||||
|
||||
useEffect(() => {
|
||||
const keyDownHandler = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter' && isHovered) {
|
||||
event.preventDefault()
|
||||
navigate(`/nfts/collection/${collection.address}`)
|
||||
handleClick()
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', keyDownHandler)
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keyDownHandler)
|
||||
}
|
||||
}, [toggleOpen, isHovered, collection, navigate, handleClick])
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/nfts/collection/${collection.address}`}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={() => !isHovered && setHoveredIndex(index)}
|
||||
onMouseLeave={() => isHovered && setHoveredIndex(undefined)}
|
||||
className={styles.suggestionRow}
|
||||
style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }}
|
||||
>
|
||||
<Row style={{ width: '60%' }}>
|
||||
{!brokenImage && collection.imageUrl ? (
|
||||
<Box
|
||||
as="img"
|
||||
src={collection.imageUrl}
|
||||
alt={collection.name}
|
||||
className={clsx(loaded ? styles.suggestionImage : styles.imageHolder)}
|
||||
onError={() => setBrokenImage(true)}
|
||||
onLoad={() => setLoaded(true)}
|
||||
/>
|
||||
) : (
|
||||
<Box className={styles.imageHolder} />
|
||||
)}
|
||||
<Column className={styles.suggestionPrimaryContainer}>
|
||||
<Row gap="4" width="full">
|
||||
<Box className={styles.primaryText}>{collection.name}</Box>
|
||||
{collection.isVerified && <VerifiedIcon className={styles.suggestionIcon} />}
|
||||
</Row>
|
||||
<Box className={styles.secondaryText}>{putCommas(collection.stats.total_supply)} items</Box>
|
||||
</Column>
|
||||
</Row>
|
||||
{collection.floorPrice ? (
|
||||
<Column className={styles.suggestionSecondaryContainer}>
|
||||
<Row gap="4">
|
||||
<Box className={styles.primaryText}>{ethNumberStandardFormatter(collection.floorPrice)} ETH</Box>
|
||||
</Row>
|
||||
<Box className={styles.secondaryText}>Floor</Box>
|
||||
</Column>
|
||||
) : null}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
interface TokenRowProps {
|
||||
token: FungibleToken
|
||||
isHovered: boolean
|
||||
setHoveredIndex: (index: number | undefined) => void
|
||||
toggleOpen: () => void
|
||||
index: number
|
||||
}
|
||||
|
||||
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index }: TokenRowProps) => {
|
||||
const [brokenImage, setBrokenImage] = useState(false)
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const addToSearchHistory = useSearchHistory(
|
||||
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
|
||||
)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
addToSearchHistory(token)
|
||||
toggleOpen()
|
||||
}, [addToSearchHistory, toggleOpen, token])
|
||||
|
||||
// Close the modal on escape
|
||||
useEffect(() => {
|
||||
const keyDownHandler = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter' && isHovered) {
|
||||
event.preventDefault()
|
||||
navigate(`/tokens/${token.address}`)
|
||||
handleClick()
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', keyDownHandler)
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keyDownHandler)
|
||||
}
|
||||
}, [toggleOpen, isHovered, token, navigate, handleClick])
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/tokens/${token.address}`}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={() => !isHovered && setHoveredIndex(index)}
|
||||
onMouseLeave={() => isHovered && setHoveredIndex(undefined)}
|
||||
className={styles.suggestionRow}
|
||||
style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }}
|
||||
>
|
||||
<Row style={{ width: '65%' }}>
|
||||
{!brokenImage && token.logoURI ? (
|
||||
<Box
|
||||
as="img"
|
||||
src={token.logoURI.includes('ipfs://') ? uriToHttp(token.logoURI)[0] : token.logoURI}
|
||||
alt={token.name}
|
||||
className={clsx(loaded ? styles.suggestionImage : styles.imageHolder)}
|
||||
onError={() => setBrokenImage(true)}
|
||||
onLoad={() => setLoaded(true)}
|
||||
/>
|
||||
) : (
|
||||
<Box className={styles.imageHolder} />
|
||||
)}
|
||||
<Column className={styles.suggestionPrimaryContainer}>
|
||||
<Row gap="4" width="full">
|
||||
<Box className={styles.primaryText}>{token.name}</Box>
|
||||
{token.onDefaultList && <VerifiedIcon className={styles.suggestionIcon} />}
|
||||
</Row>
|
||||
<Box className={styles.secondaryText}>{token.symbol}</Box>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Column className={styles.suggestionSecondaryContainer}>
|
||||
{token.priceUsd && (
|
||||
<Row gap="4">
|
||||
<Box className={styles.primaryText}>{ethNumberStandardFormatter(token.priceUsd, true)}</Box>
|
||||
</Row>
|
||||
)}
|
||||
{token.price24hChange && (
|
||||
<Box className={styles.secondaryText} color={token.price24hChange >= 0 ? 'green400' : 'red400'}>
|
||||
{token.price24hChange.toFixed(2)}%
|
||||
</Box>
|
||||
)}
|
||||
</Column>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export const SkeletonRow = () => {
|
||||
return (
|
||||
<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>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
@@ -23,7 +23,7 @@ const Tabs = styled.div`
|
||||
const StyledHistoryLink = styled(HistoryLink)<{ flex: string | undefined }>`
|
||||
flex: ${({ flex }) => flex ?? 'none'};
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
flex: none;
|
||||
margin-right: 10px;
|
||||
`};
|
||||
|
||||
@@ -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'
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 9999;
|
||||
z-index: ${Z_INDEX.absoluteTop};
|
||||
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
|
||||
opacity: ${(props) => (props.show ? 1 : 0)};
|
||||
transition: visibility 150ms linear, opacity 150ms linear;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import { animated } from 'react-spring'
|
||||
@@ -29,7 +30,7 @@ const Popup = styled.div`
|
||||
padding-right: 35px;
|
||||
overflow: hidden;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
min-width: 290px;
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 20px;
|
||||
@@ -57,6 +58,7 @@ export default function PopupItem({
|
||||
popKey: string
|
||||
}) {
|
||||
const removePopup = useRemovePopup()
|
||||
const navbarFlag = useNavBarFlag()
|
||||
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
|
||||
useEffect(() => {
|
||||
if (removeAfterMs === null) return undefined
|
||||
@@ -71,23 +73,24 @@ export default function PopupItem({
|
||||
}, [removeAfterMs, removeThisPopup])
|
||||
|
||||
const theme = useTheme()
|
||||
const faderStyle = useSpring({
|
||||
from: { width: '100%' },
|
||||
to: { width: '0%' },
|
||||
config: { duration: removeAfterMs ?? undefined },
|
||||
})
|
||||
|
||||
let popupContent
|
||||
if ('txn' in content) {
|
||||
const {
|
||||
txn: { hash },
|
||||
} = content
|
||||
if (navbarFlag === NavBarVariant.Enabled) return null
|
||||
|
||||
popupContent = <TransactionPopup hash={hash} />
|
||||
} else if ('failedSwitchNetwork' in content) {
|
||||
popupContent = <FailedNetworkSwitchPopup chainId={content.failedSwitchNetwork} />
|
||||
}
|
||||
|
||||
const faderStyle = useSpring({
|
||||
from: { width: '100%' },
|
||||
to: { width: '0%' },
|
||||
config: { duration: removeAfterMs ?? undefined },
|
||||
})
|
||||
|
||||
return (
|
||||
<Popup>
|
||||
<StyledClose color={theme.deprecated_text2} onClick={removeThisPopup} />
|
||||
|
||||
@@ -21,7 +21,7 @@ const Wrapper = styled(AutoColumn)`
|
||||
color: ${({ theme }) => theme.deprecated_text1};
|
||||
overflow: hidden;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
max-width: 100%;
|
||||
`}
|
||||
`
|
||||
|
||||
@@ -17,7 +17,7 @@ const MobilePopupWrapper = styled.div<{ height: string | number }>`
|
||||
margin-bottom: ${({ height }) => (height ? '20px' : 0)};
|
||||
|
||||
display: none;
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
display: block;
|
||||
padding-top: 20px;
|
||||
`};
|
||||
@@ -35,8 +35,8 @@ const MobilePopupInner = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const StopOverflowQuery = `@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium + 1}px) and (max-width: ${
|
||||
MEDIA_WIDTHS.upToMedium + 500
|
||||
const StopOverflowQuery = `@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToMedium + 1}px) and (max-width: ${
|
||||
MEDIA_WIDTHS.deprecated_upToMedium + 500
|
||||
}px)`
|
||||
|
||||
const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean; xlPadding: boolean }>`
|
||||
@@ -47,7 +47,7 @@ const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean; xlPadding:
|
||||
width: 100%;
|
||||
z-index: 3;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
display: none;
|
||||
`};
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ const DesktopHeader = styled.div`
|
||||
font-weight: 500;
|
||||
padding: 8px;
|
||||
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -32,11 +32,11 @@ const MobileHeader = styled.div`
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: ${MEDIA_WIDTHS.upToExtraSmall}px) {
|
||||
@media screen and (max-width: ${MEDIA_WIDTHS.deprecated_upToExtraSmall}px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
@@ -50,12 +50,12 @@ const ToggleWrap = styled.div`
|
||||
`
|
||||
|
||||
const ToggleLabel = styled.div`
|
||||
opacity: 0.6;
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
margin-right: 10px;
|
||||
`
|
||||
|
||||
const MobileTogglePosition = styled.div`
|
||||
@media screen and (max-width: ${MEDIA_WIDTHS.upToExtraSmall}px) {
|
||||
@media screen and (max-width: ${MEDIA_WIDTHS.deprecated_upToExtraSmall}px) {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
@@ -48,11 +48,11 @@ const LinkRow = styled(Link)`
|
||||
background-color: ${({ theme }) => theme.deprecated_bg2};
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
|
||||
/* flex-direction: row; */
|
||||
}
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
flex-direction: column;
|
||||
row-gap: 12px;
|
||||
`};
|
||||
@@ -61,7 +61,7 @@ const LinkRow = styled(Link)`
|
||||
const BadgeText = styled.div`
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
font-size: 12px;
|
||||
`};
|
||||
`
|
||||
@@ -78,7 +78,7 @@ const RangeLineItem = styled(DataLineItem)`
|
||||
margin-top: 4px;
|
||||
width: 100%;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
background-color: ${({ theme }) => theme.deprecated_bg2};
|
||||
border-radius: 12px;
|
||||
padding: 8px 0;
|
||||
@@ -88,7 +88,7 @@ const RangeLineItem = styled(DataLineItem)`
|
||||
const DoubleArrow = styled.span`
|
||||
margin: 0 2px;
|
||||
color: ${({ theme }) => theme.deprecated_text3};
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
margin: 4px;
|
||||
padding: 20px;
|
||||
`};
|
||||
@@ -104,7 +104,7 @@ const ExtentsText = styled.span`
|
||||
color: ${({ theme }) => theme.deprecated_text3};
|
||||
font-size: 14px;
|
||||
margin-right: 4px;
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
@@ -122,7 +122,7 @@ const DataText = styled.div`
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
font-size: 14px;
|
||||
`};
|
||||
`
|
||||
|
||||
@@ -13,7 +13,7 @@ import styled from 'styled-components/macro'
|
||||
import { currencyId } from 'utils/currencyId'
|
||||
|
||||
const MobileWrapper = styled(AutoColumn)`
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
@@ -128,7 +128,7 @@ function CurrencyRow({
|
||||
eventProperties,
|
||||
}: {
|
||||
currency: Currency
|
||||
onSelect: () => void
|
||||
onSelect: (hasWarning: boolean) => void
|
||||
isSelected: boolean
|
||||
otherSelected: boolean
|
||||
style: CSSProperties
|
||||
@@ -159,13 +159,13 @@ function CurrencyRow({
|
||||
redesignFlag={redesignFlagEnabled}
|
||||
style={style}
|
||||
className={`token-item-${key}`}
|
||||
onKeyPress={(e) => (!isSelected && e.key === 'Enter' ? onSelect() : null)}
|
||||
onClick={() => (isSelected ? null : onSelect())}
|
||||
onKeyPress={(e) => (!isSelected && e.key === 'Enter' ? onSelect(!!warning) : null)}
|
||||
onClick={() => (isSelected ? null : onSelect(!!warning))}
|
||||
disabled={isSelected}
|
||||
selected={otherSelected}
|
||||
>
|
||||
<Column>
|
||||
<CurrencyLogo currency={currency} size={'24px'} />
|
||||
<CurrencyLogo currency={currency} size={'36px'} />
|
||||
</Column>
|
||||
<AutoColumn>
|
||||
<Row>
|
||||
@@ -279,7 +279,7 @@ export default function CurrencyList({
|
||||
currencies: Currency[]
|
||||
otherListTokens?: WrappedTokenInfo[]
|
||||
selectedCurrency?: Currency | null
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
onCurrencySelect: (currency: Currency, hasWarning?: boolean) => void
|
||||
otherCurrency?: Currency | null
|
||||
fixedListRef?: MutableRefObject<FixedSizeList | undefined>
|
||||
showImportView: () => void
|
||||
@@ -308,7 +308,7 @@ export default function CurrencyList({
|
||||
|
||||
const isSelected = Boolean(currency && selectedCurrency && selectedCurrency.equals(currency))
|
||||
const otherSelected = Boolean(currency && otherCurrency && otherCurrency.equals(currency))
|
||||
const handleSelect = () => currency && onCurrencySelect(currency)
|
||||
const handleSelect = (hasWarning: boolean) => currency && onCurrencySelect(currency, hasWarning)
|
||||
|
||||
const token = currency?.wrapped
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ interface CurrencySearchProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
selectedCurrency?: Currency | null
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
onCurrencySelect: (currency: Currency, hasWarning?: boolean) => void
|
||||
otherSelectedCurrency?: Currency | null
|
||||
showCommonBases?: boolean
|
||||
showCurrencyAmount?: boolean
|
||||
@@ -136,9 +136,9 @@ export function CurrencySearch({
|
||||
}, [debouncedQuery, native, filteredSortedTokens])
|
||||
|
||||
const handleCurrencySelect = useCallback(
|
||||
(currency: Currency) => {
|
||||
onCurrencySelect(currency)
|
||||
onDismiss()
|
||||
(currency: Currency, hasWarning?: boolean) => {
|
||||
onCurrencySelect(currency, hasWarning)
|
||||
if (!hasWarning) onDismiss()
|
||||
},
|
||||
[onDismiss, onCurrencySelect]
|
||||
)
|
||||
|
||||
@@ -3,10 +3,12 @@ import { TokenList } from '@uniswap/token-lists'
|
||||
import TokenSafety from 'components/TokenSafety'
|
||||
import { TokenSafetyVariant, useTokenSafetyFlag } from 'featureFlags/flags/tokenSafety'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
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'
|
||||
@@ -29,9 +31,10 @@ export enum CurrencyModalView {
|
||||
manage,
|
||||
importToken,
|
||||
importList,
|
||||
tokenSafety,
|
||||
}
|
||||
|
||||
export default function CurrencySearchModal({
|
||||
export default memo(function CurrencySearchModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
onCurrencySelect,
|
||||
@@ -43,6 +46,7 @@ export default function CurrencySearchModal({
|
||||
}: CurrencySearchModalProps) {
|
||||
const [modalView, setModalView] = useState<CurrencyModalView>(CurrencyModalView.manage)
|
||||
const lastOpen = useLast(isOpen)
|
||||
const userAddedTokens = useUserAddedTokens()
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && !lastOpen) {
|
||||
@@ -50,12 +54,28 @@ export default function CurrencySearchModal({
|
||||
}
|
||||
}, [isOpen, lastOpen])
|
||||
|
||||
const showTokenSafetySpeedbump = (token: Token) => {
|
||||
setWarningToken(token)
|
||||
setModalView(CurrencyModalView.tokenSafety)
|
||||
}
|
||||
|
||||
const tokenSafetyFlag = useTokenSafetyFlag()
|
||||
|
||||
const handleCurrencySelect = useCallback(
|
||||
(currency: Currency) => {
|
||||
onCurrencySelect(currency)
|
||||
onDismiss()
|
||||
(currency: Currency, hasWarning?: boolean) => {
|
||||
if (
|
||||
tokenSafetyFlag === TokenSafetyVariant.Enabled &&
|
||||
hasWarning &&
|
||||
currency.isToken &&
|
||||
!userAddedTokens.find((token) => token.equals(currency))
|
||||
) {
|
||||
showTokenSafetySpeedbump(currency)
|
||||
} else {
|
||||
onCurrencySelect(currency)
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
[onDismiss, onCurrencySelect]
|
||||
[onDismiss, onCurrencySelect, tokenSafetyFlag, userAddedTokens]
|
||||
)
|
||||
|
||||
// for token import view
|
||||
@@ -68,6 +88,9 @@ export default function CurrencySearchModal({
|
||||
const [importList, setImportList] = useState<TokenList | undefined>()
|
||||
const [listURL, setListUrl] = useState<string | undefined>()
|
||||
|
||||
// used for token safety
|
||||
const [warningToken, setWarningToken] = useState<Token | undefined>()
|
||||
|
||||
const showImportView = useCallback(() => setModalView(CurrencyModalView.importToken), [setModalView])
|
||||
const showManageView = useCallback(() => setModalView(CurrencyModalView.manage), [setModalView])
|
||||
const handleBackImport = useCallback(
|
||||
@@ -75,13 +98,16 @@ export default function CurrencySearchModal({
|
||||
[setModalView, prevView]
|
||||
)
|
||||
|
||||
const tokenSafetyFlag = useTokenSafetyFlag()
|
||||
|
||||
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}
|
||||
@@ -98,29 +124,38 @@ export default function CurrencySearchModal({
|
||||
/>
|
||||
)
|
||||
break
|
||||
case CurrencyModalView.tokenSafety:
|
||||
modalHeight = undefined
|
||||
if (tokenSafetyFlag === TokenSafetyVariant.Enabled && warningToken) {
|
||||
content = (
|
||||
<TokenSafety
|
||||
tokenAddress={warningToken.address}
|
||||
onContinue={() => handleCurrencySelect(warningToken)}
|
||||
onCancel={() => setModalView(CurrencyModalView.search)}
|
||||
showCancel={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
break
|
||||
case CurrencyModalView.importToken:
|
||||
if (importToken) {
|
||||
minHeight = undefined
|
||||
content =
|
||||
tokenSafetyFlag === TokenSafetyVariant.Enabled ? (
|
||||
<TokenSafety
|
||||
tokenAddress={importToken.address}
|
||||
onContinue={() => handleCurrencySelect(importToken)}
|
||||
onCancel={handleBackImport}
|
||||
/>
|
||||
) : (
|
||||
<ImportToken
|
||||
tokens={[importToken]}
|
||||
onDismiss={onDismiss}
|
||||
list={importToken instanceof WrappedTokenInfo ? importToken.list : undefined}
|
||||
onBack={handleBackImport}
|
||||
handleCurrencySelect={handleCurrencySelect}
|
||||
/>
|
||||
)
|
||||
modalHeight = undefined
|
||||
if (tokenSafetyFlag === TokenSafetyVariant.Enabled) {
|
||||
showTokenSafetySpeedbump(importToken)
|
||||
}
|
||||
content = (
|
||||
<ImportToken
|
||||
tokens={[importToken]}
|
||||
onDismiss={onDismiss}
|
||||
list={importToken instanceof WrappedTokenInfo ? importToken.list : undefined}
|
||||
onBack={handleBackImport}
|
||||
handleCurrencySelect={handleCurrencySelect}
|
||||
/>
|
||||
)
|
||||
}
|
||||
break
|
||||
case CurrencyModalView.importList:
|
||||
minHeight = 40
|
||||
modalHeight = 40
|
||||
if (importList && listURL) {
|
||||
content = <ImportList list={importList} listURL={listURL} onDismiss={onDismiss} setModalView={setModalView} />
|
||||
}
|
||||
@@ -138,8 +173,8 @@ export default function CurrencySearchModal({
|
||||
break
|
||||
}
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={80} minHeight={minHeight}>
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={modalHeight} minHeight={modalHeight}>
|
||||
{content}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ const AddressText = styled(ThemedText.DeprecatedBlue)`
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
font-size: 10px;
|
||||
`}
|
||||
`
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import searchIcon from 'assets/svg/search.svg'
|
||||
import { LoadingRows as BaseLoadingRows } from 'components/Loader/styled'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
@@ -37,14 +38,18 @@ export const MenuItem = styled(RowBetween)<{ redesignFlag?: boolean }>`
|
||||
`
|
||||
|
||||
export const SearchInput = styled.input<{ redesignFlag?: boolean }>`
|
||||
background: no-repeat scroll 7px 7px;
|
||||
background-image: url(${searchIcon});
|
||||
background-size: 20px 20px;
|
||||
background-position: 12px center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
padding-left: 40px;
|
||||
height: ${({ redesignFlag }) => redesignFlag && '40px'};
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
background: none;
|
||||
background-color: ${({ theme, redesignFlag }) => redesignFlag && theme.backgroundModule};
|
||||
border: none;
|
||||
outline: none;
|
||||
@@ -62,8 +67,9 @@ export const SearchInput = styled.input<{ redesignFlag?: boolean }>`
|
||||
}
|
||||
transition: border 100ms;
|
||||
:focus {
|
||||
border: 1px solid ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_primary1)};
|
||||
background-color: ${({ theme, redesignFlag }) => redesignFlag && theme.accentActionSoft};
|
||||
border: 1px solid
|
||||
${({ theme, redesignFlag }) => (redesignFlag ? theme.accentActiveSoft : theme.deprecated_primary1)};
|
||||
background-color: ${({ theme, redesignFlag }) => redesignFlag && theme.backgroundSurface};
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -99,7 +99,7 @@ const MenuFlyout = styled.span<{ redesignFlag: boolean }>`
|
||||
z-index: 100;
|
||||
color: ${({ theme, redesignFlag }) => redesignFlag && theme.textPrimary};
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
min-width: 18.125rem;
|
||||
`};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -6,13 +6,18 @@ import styled, { keyframes } from 'styled-components/macro'
|
||||
const Wrapper = styled.button<{ isActive?: boolean; activeElement?: boolean; redesignFlag: boolean }>`
|
||||
align-items: center;
|
||||
background: ${({ isActive, theme, redesignFlag }) =>
|
||||
redesignFlag && isActive ? theme.accentActionSoft : theme.deprecated_bg1};
|
||||
border: none;
|
||||
redesignFlag && isActive
|
||||
? theme.accentActionSoft
|
||||
: redesignFlag && !isActive
|
||||
? 'transparent'
|
||||
: theme.deprecated_bg1};
|
||||
border: ${({ redesignFlag, theme, isActive }) =>
|
||||
redesignFlag && !isActive ? `1px solid ${theme.backgroundOutline}` : 'none'};
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
outline: none;
|
||||
padding: 0.4rem 0.4rem;
|
||||
padding: ${({ redesignFlag }) => (redesignFlag ? '4px' : '0.4rem 0.4rem')};
|
||||
width: fit-content;
|
||||
`
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ExternalLink } from 'theme'
|
||||
import { Color } from 'theme/styled'
|
||||
|
||||
const Label = styled.div<{ color: Color }>`
|
||||
width: 284px;
|
||||
width: 100%;
|
||||
padding: 12px 20px;
|
||||
background-color: ${({ color }) => color + '1F'};
|
||||
border-radius: 16px;
|
||||
|
||||
@@ -1,12 +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
|
||||
}
|
||||
|
||||
export default function TokenSafetyModal({
|
||||
@@ -15,14 +11,18 @@ export default function TokenSafetyModal({
|
||||
secondTokenAddress,
|
||||
onContinue,
|
||||
onCancel,
|
||||
onBlocked,
|
||||
showCancel,
|
||||
}: TokenSafetyModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onCancel}>
|
||||
<TokenSafety
|
||||
tokenAddress={tokenAddress}
|
||||
secondTokenAddress={secondTokenAddress}
|
||||
onCancel={onCancel}
|
||||
onContinue={onContinue}
|
||||
onBlocked={onBlocked}
|
||||
onCancel={onCancel}
|
||||
showCancel={showCancel}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -4,14 +4,13 @@ import { ButtonPrimary } from 'components/Button'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import TokenSafetyLabel from 'components/TokenSafety/TokenSafetyLabel'
|
||||
import { checkWarning, getWarningCopy, TOKEN_SAFETY_ARTICLE, Warning, WARNING_LEVEL } from 'constants/tokenSafety'
|
||||
import { checkWarning, getWarningCopy, TOKEN_SAFETY_ARTICLE, Warning } from 'constants/tokenSafety'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import { ExternalLink as LinkIconFeather } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import { useAddUserToken } from 'state/user/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ButtonText, CopyLinkIcon, ExternalLink } from 'theme'
|
||||
import { Color } from 'theme/styled'
|
||||
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
@@ -46,61 +45,54 @@ const InfoText = styled(Text)`
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const StyledButton = styled(ButtonPrimary)<{ buttonColor: Color; textColor: Color }>`
|
||||
color: ${({ textColor }) => textColor};
|
||||
background-color: ${({ buttonColor }) => buttonColor};
|
||||
const StyledButton = styled(ButtonPrimary)`
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
:hover {
|
||||
background-color: ${({ buttonColor, theme }) => buttonColor ?? theme.accentAction};
|
||||
}
|
||||
font-weight: 600;
|
||||
`
|
||||
|
||||
const StyledCancelButton = styled(ButtonText)<{ color?: Color }>`
|
||||
const StyledCancelButton = styled(ButtonText)`
|
||||
margin-top: 16px;
|
||||
color: ${({ color, theme }) => color ?? theme.accentAction};
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
`
|
||||
|
||||
const StyledCloseButton = styled(StyledButton)`
|
||||
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
transition: opacity 250ms ease;
|
||||
}
|
||||
`
|
||||
|
||||
const Buttons = ({
|
||||
warning,
|
||||
onContinue,
|
||||
onCancel,
|
||||
onBlocked,
|
||||
showCancel,
|
||||
}: {
|
||||
warning: Warning
|
||||
onContinue: () => void
|
||||
onCancel: () => void
|
||||
onBlocked?: () => void
|
||||
showCancel?: boolean
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
let textColor, buttonColor, cancelColor
|
||||
switch (warning.level) {
|
||||
case WARNING_LEVEL.MEDIUM:
|
||||
textColor = theme.white
|
||||
buttonColor = theme.accentAction
|
||||
cancelColor = theme.accentAction
|
||||
break
|
||||
case WARNING_LEVEL.UNKNOWN:
|
||||
textColor = theme.accentFailure
|
||||
buttonColor = theme.accentFailureSoft
|
||||
cancelColor = theme.textPrimary
|
||||
break
|
||||
case WARNING_LEVEL.BLOCKED:
|
||||
textColor = theme.textPrimary
|
||||
buttonColor = theme.backgroundInteractive
|
||||
break
|
||||
}
|
||||
return warning.canProceed ? (
|
||||
<>
|
||||
<StyledButton buttonColor={buttonColor} textColor={textColor} onClick={onContinue}>
|
||||
<Trans>I Understand</Trans>
|
||||
<StyledButton onClick={onContinue}>
|
||||
<Trans>I understand</Trans>
|
||||
</StyledButton>
|
||||
<StyledCancelButton color={cancelColor} onClick={onCancel}>
|
||||
Cancel
|
||||
</StyledCancelButton>
|
||||
{showCancel && <StyledCancelButton onClick={onCancel}>Cancel</StyledCancelButton>}
|
||||
</>
|
||||
) : (
|
||||
<StyledButton buttonColor={buttonColor} textColor={textColor} onClick={onCancel}>
|
||||
<StyledCloseButton onClick={onBlocked ?? onCancel}>
|
||||
<Trans>Close</Trans>
|
||||
</StyledButton>
|
||||
</StyledCloseButton>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -124,8 +116,8 @@ const ExplorerContainer = styled.div`
|
||||
height: 32px;
|
||||
margin-top: 10px;
|
||||
font-size: 20px;
|
||||
background-color: ${({ theme }) => theme.accentActiveSoft};
|
||||
color: ${({ theme }) => theme.accentActive};
|
||||
background-color: ${({ theme }) => theme.accentActionSoft};
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
border-radius: 8px;
|
||||
padding: 2px 12px;
|
||||
display: flex;
|
||||
@@ -140,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};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -190,14 +182,27 @@ function ExplorerView({ token }: { token: Token }) {
|
||||
}
|
||||
}
|
||||
|
||||
interface TokenSafetyProps {
|
||||
const StyledExternalLink = styled(ExternalLink)`
|
||||
font-weight: 600;
|
||||
`
|
||||
|
||||
export interface TokenSafetyProps {
|
||||
tokenAddress: string | null
|
||||
secondTokenAddress?: string
|
||||
onContinue: () => void
|
||||
onCancel: () => void
|
||||
onBlocked?: () => void
|
||||
showCancel?: boolean
|
||||
}
|
||||
|
||||
export default function TokenSafety({ tokenAddress, secondTokenAddress, onContinue, onCancel }: TokenSafetyProps) {
|
||||
export default function TokenSafety({
|
||||
tokenAddress,
|
||||
secondTokenAddress,
|
||||
onContinue,
|
||||
onCancel,
|
||||
onBlocked,
|
||||
showCancel,
|
||||
}: TokenSafetyProps) {
|
||||
const logos = []
|
||||
const urls = []
|
||||
|
||||
@@ -254,13 +259,19 @@ export default function TokenSafety({ tokenAddress, secondTokenAddress, onContin
|
||||
<ShortColumn>
|
||||
<InfoText>
|
||||
{description}{' '}
|
||||
<ExternalLink href={TOKEN_SAFETY_ARTICLE}>
|
||||
<Trans>Learn More</Trans>
|
||||
</ExternalLink>
|
||||
<StyledExternalLink href={TOKEN_SAFETY_ARTICLE}>
|
||||
<Trans>Learn more</Trans>
|
||||
</StyledExternalLink>
|
||||
</InfoText>
|
||||
</ShortColumn>
|
||||
<LinkColumn>{urls}</LinkColumn>
|
||||
<Buttons warning={displayWarning} onContinue={acknowledge} onCancel={onCancel} />
|
||||
<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 'components/AmplitudeAnalytics/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%;
|
||||
@@ -15,7 +15,9 @@ const BalancesCard = styled.div`
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
padding: 20px;
|
||||
box-shadow: ${({ theme }) => theme.shallowShadow};
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
||||
border-radius: 16px;
|
||||
`
|
||||
const ErrorState = styled.div`
|
||||
@@ -31,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;
|
||||
@@ -52,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};
|
||||
}
|
||||
`
|
||||
117
src/components/Tokens/TokenDetails/ChartSection.tsx
Normal file
117
src/components/Tokens/TokenDetails/ChartSection.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
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 { 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} />
|
||||
)}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
|
||||
import { useState } from 'react'
|
||||
@@ -82,7 +83,7 @@ const SwapButton = styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
color: ${({ theme }) => theme.accentTextLightPrimary};
|
||||
padding: 12px 16px;
|
||||
width: 120px;
|
||||
height: 44px;
|
||||
@@ -152,7 +153,9 @@ export default function FooterBalanceSummary({
|
||||
) : error ? (
|
||||
<ErrorState>
|
||||
<AlertTriangle size={17} />
|
||||
<ErrorText>There was an error fetching your balance</ErrorText>
|
||||
<ErrorText>
|
||||
<Trans>There was an error fetching your balance</Trans>
|
||||
</ErrorText>
|
||||
</ErrorState>
|
||||
) : (
|
||||
<BalanceInfo>
|
||||
@@ -165,16 +168,21 @@ export default function FooterBalanceSummary({
|
||||
</BalanceTotal>
|
||||
{multipleBalances && (
|
||||
<ViewAll onClick={() => setShowMultipleBalances(!showMultipleBalances)}>
|
||||
{showMultipleBalances ? 'Hide' : 'View'} all balances
|
||||
<Trans>{showMultipleBalances ? 'Hide' : 'View'} all balances</Trans>
|
||||
</ViewAll>
|
||||
)}
|
||||
</BalanceInfo>
|
||||
)}
|
||||
<SwapButton onClick={() => (window.location.href = 'https://app.uniswap.org/#/swap')}>Swap</SwapButton>
|
||||
<SwapButton onClick={() => (window.location.href = 'https://app.uniswap.org/#/swap')}>
|
||||
<Trans>Swap</Trans>
|
||||
</SwapButton>
|
||||
</TotalBalancesSection>
|
||||
{showMultipleBalances && (
|
||||
<NetworkBalancesSection>
|
||||
<NetworkBalancesLabel>Your balances by network</NetworkBalancesLabel> {networkBalances}
|
||||
<NetworkBalancesLabel>
|
||||
<Trans>Your balances by network</Trans>
|
||||
</NetworkBalancesLabel>
|
||||
{networkBalances}
|
||||
</NetworkBalancesSection>
|
||||
)}
|
||||
<FakeFooterNavBar>**leaving space for updated nav footer**</FakeFooterNavBar>
|
||||
|
||||
@@ -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 {
|
||||
AboutHeader,
|
||||
AboutSection,
|
||||
BreadcrumbNavLink,
|
||||
ChartContainer,
|
||||
ChartHeader,
|
||||
ContractAddressSection,
|
||||
ResourcesContainer,
|
||||
Stat,
|
||||
StatPair,
|
||||
StatsSection,
|
||||
TokenInfoContainer,
|
||||
TokenNameCell,
|
||||
TopArea,
|
||||
} from './TokenDetail'
|
||||
import { StatPair, StatWrapper, TokenStatsSection } from './StatsSection'
|
||||
|
||||
const LoadingChartContainer = styled(ChartContainer)`
|
||||
height: 336px;
|
||||
@@ -34,15 +25,17 @@ const TitleLoadingBubble = styled(LoadingDetailBubble)`
|
||||
const SquareLoadingBubble = styled(LoadingDetailBubble)`
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
const PriceLoadingBubble = styled(SquareLoadingBubble)`
|
||||
height: 40px;
|
||||
`
|
||||
const LongLoadingBubble = styled(LoadingDetailBubble)`
|
||||
margin-top: 6px;
|
||||
width: 100%;
|
||||
`
|
||||
const HalfLoadingBubble = styled(LoadingDetailBubble)`
|
||||
margin-top: 6px;
|
||||
width: 50%;
|
||||
`
|
||||
const IconLoadingBubble = styled(LoadingDetailBubble)`
|
||||
@@ -76,7 +69,7 @@ const Space = styled.div<{ heightSize: number }>`
|
||||
height: ${({ heightSize }) => `${heightSize}px`};
|
||||
`
|
||||
|
||||
function Wave() {
|
||||
export function Wave() {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<svg width="416" height="160" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -88,7 +81,7 @@ function Wave() {
|
||||
/* Loading State: row component with loading bubbles */
|
||||
export default function LoadingTokenDetail() {
|
||||
return (
|
||||
<TopArea>
|
||||
<LeftPanel>
|
||||
<BreadcrumbNavLink to="/explore">
|
||||
<Space heightSize={20} />
|
||||
</BreadcrumbNavLink>
|
||||
@@ -118,7 +111,31 @@ export default function LoadingTokenDetail() {
|
||||
</LoadingChartContainer>
|
||||
<Space heightSize={32} />
|
||||
</ChartHeader>
|
||||
<AboutSection>
|
||||
<TokenStatsSection>
|
||||
<StatsLoadingContainer>
|
||||
<StatPair>
|
||||
<StatWrapper>
|
||||
<HalfLoadingBubble />
|
||||
<StatLoadingBubble />
|
||||
</StatWrapper>
|
||||
<StatWrapper>
|
||||
<HalfLoadingBubble />
|
||||
<StatLoadingBubble />
|
||||
</StatWrapper>
|
||||
</StatPair>
|
||||
<StatPair>
|
||||
<StatWrapper>
|
||||
<HalfLoadingBubble />
|
||||
<StatLoadingBubble />
|
||||
</StatWrapper>
|
||||
<StatWrapper>
|
||||
<HalfLoadingBubble />
|
||||
<StatLoadingBubble />
|
||||
</StatWrapper>
|
||||
</StatPair>
|
||||
</StatsLoadingContainer>
|
||||
</TokenStatsSection>
|
||||
<AboutContainer>
|
||||
<AboutHeader>
|
||||
<SquareLoadingBubble />
|
||||
</AboutHeader>
|
||||
@@ -127,32 +144,18 @@ export default function LoadingTokenDetail() {
|
||||
<HalfLoadingBubble />
|
||||
|
||||
<ResourcesContainer>{null}</ResourcesContainer>
|
||||
</AboutSection>
|
||||
<StatsSection>
|
||||
<StatsLoadingContainer>
|
||||
<StatPair>
|
||||
<Stat>
|
||||
<HalfLoadingBubble />
|
||||
<StatLoadingBubble />
|
||||
</Stat>
|
||||
<Stat>
|
||||
<HalfLoadingBubble />
|
||||
<StatLoadingBubble />
|
||||
</Stat>
|
||||
</StatPair>
|
||||
<StatPair>
|
||||
<Stat>
|
||||
<HalfLoadingBubble />
|
||||
<StatLoadingBubble />
|
||||
</Stat>
|
||||
<Stat>
|
||||
<HalfLoadingBubble />
|
||||
<StatLoadingBubble />
|
||||
</Stat>
|
||||
</StatPair>
|
||||
</StatsLoadingContainer>
|
||||
</StatsSection>
|
||||
</AboutContainer>
|
||||
<ContractAddressSection>{null}</ContractAddressSection>
|
||||
</TopArea>
|
||||
</LeftPanel>
|
||||
)
|
||||
}
|
||||
|
||||
export function LoadingTokenDetails() {
|
||||
return (
|
||||
<TokenDetailsLayout>
|
||||
<LoadingTokenDetail />
|
||||
<RightPanel />
|
||||
<Footer />
|
||||
</TokenDetailsLayout>
|
||||
)
|
||||
}
|
||||
@@ -4,38 +4,31 @@ 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, curveBasis, NumberValue, scaleLinear } from 'd3'
|
||||
import { bisect, curveCardinal, NumberValue, scaleLinear, timeDay, timeHour, timeMinute, timeMonth } 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 { TimePeriod } from 'hooks/useExplorePageQuery'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, 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,
|
||||
} from 'utils/formatChartTimes'
|
||||
|
||||
import data from '../../Charts/data.json'
|
||||
import LineChart from '../../Charts/LineChart'
|
||||
import { DISPLAYS, ORDERED_TIMES } from '../TokenTable/TimeSelector'
|
||||
|
||||
// TODO: This should be combined with the logic in TimeSelector.
|
||||
const TIME_DISPLAYS: [TimePeriod, string][] = [
|
||||
[TimePeriod.hour, '1H'],
|
||||
[TimePeriod.day, '1D'],
|
||||
[TimePeriod.week, '1W'],
|
||||
[TimePeriod.month, '1M'],
|
||||
[TimePeriod.year, '1Y'],
|
||||
[TimePeriod.all, 'All'],
|
||||
]
|
||||
|
||||
type PricePoint = { value: number; timestamp: number }
|
||||
export const DATA_EMPTY = { value: 0, timestamp: 0 }
|
||||
|
||||
function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
|
||||
const prices = pricePoints.map((x) => x.value)
|
||||
@@ -51,23 +44,31 @@ const StyledDownArrow = styled(ArrowDownRight)`
|
||||
color: ${({ theme }) => theme.accentFailure};
|
||||
`
|
||||
|
||||
function getDelta(start: number, current: number) {
|
||||
const delta = (current / start - 1) * 100
|
||||
const isPositive = Math.sign(delta) > 0
|
||||
export function calculateDelta(start: number, current: number) {
|
||||
return (current / start - 1) * 100
|
||||
}
|
||||
|
||||
const formattedDelta = delta.toFixed(2) + '%'
|
||||
if (isPositive) {
|
||||
return ['+' + formattedDelta, <StyledUpArrow size={16} key="arrow-up" />]
|
||||
export function getDeltaArrow(delta: number) {
|
||||
if (Math.sign(delta) > 0) {
|
||||
return <StyledUpArrow size={16} key="arrow-up" />
|
||||
} else if (delta === 0) {
|
||||
return [formattedDelta, null]
|
||||
return null
|
||||
} else {
|
||||
return <StyledDownArrow size={16} key="arrow-down" />
|
||||
}
|
||||
return [formattedDelta, <StyledDownArrow size={16} key="arrow-down" />]
|
||||
}
|
||||
|
||||
export function formatDelta(delta: number) {
|
||||
let formattedDelta = delta.toFixed(2) + '%'
|
||||
if (Math.sign(delta) > 0) {
|
||||
formattedDelta = '+' + formattedDelta
|
||||
}
|
||||
return formattedDelta
|
||||
}
|
||||
|
||||
export const ChartHeader = styled.div`
|
||||
position: absolute;
|
||||
`
|
||||
|
||||
export const TokenPrice = styled.span`
|
||||
font-size: 36px;
|
||||
line-height: 44px;
|
||||
@@ -76,6 +77,7 @@ export const DeltaContainer = styled.div`
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
`
|
||||
const ArrowCell = styled.div`
|
||||
padding-left: 2px;
|
||||
@@ -106,83 +108,117 @@ 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: 86, bottom: 48, crosshair: 72 }
|
||||
const margin = { top: 100, bottom: 48, crosshair: 72 }
|
||||
const timeOptionsHeight = 44
|
||||
const crosshairDateOverhang = 80
|
||||
|
||||
interface PriceChartProps {
|
||||
width: number
|
||||
height: number
|
||||
tokenAddress: string
|
||||
priceData?: TokenPrices$key | null
|
||||
}
|
||||
|
||||
export function PriceChart({ width, height }: PriceChartProps) {
|
||||
export function PriceChart({ width, height, tokenAddress, priceData }: PriceChartProps) {
|
||||
const [timePeriod, setTimePeriod] = useAtom(filterTimeAtom)
|
||||
const locale = useActiveLocale()
|
||||
const theme = useTheme()
|
||||
|
||||
/* TODO: Implement API calls & cache to use here */
|
||||
const pricePoints = data[timePeriod]
|
||||
const startingPrice = pricePoints[0]
|
||||
const endingPrice = pricePoints[pricePoints.length - 1]
|
||||
const initialState = { pricePoint: endingPrice, xCoordinate: null }
|
||||
const [selected, setSelected] = useState<{ pricePoint: PricePoint; xCoordinate: number | null }>(initialState)
|
||||
const { priceMap } = useTokenPricesCached(priceData, tokenAddress, 'ETHEREUM', timePeriod)
|
||||
const prices = priceMap.get(timePeriod)
|
||||
|
||||
const startingPrice = prices?.[0] ?? DATA_EMPTY
|
||||
const endingPrice = prices?.[prices.length - 1] ?? DATA_EMPTY
|
||||
const [displayPrice, setDisplayPrice] = useState(startingPrice)
|
||||
const [crosshair, setCrosshair] = useState<number | null>(null)
|
||||
|
||||
const graphWidth = width + crosshairDateOverhang
|
||||
const graphHeight = height - timeOptionsHeight
|
||||
const graphInnerHeight = graphHeight - margin.top - margin.bottom
|
||||
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]).nice(),
|
||||
[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 startDate = new Date(startingPrice.timestamp.valueOf() * 1000)
|
||||
const endDate = new Date(endingPrice.timestamp.valueOf() * 1000)
|
||||
switch (timePeriod) {
|
||||
case TimePeriod.HOUR:
|
||||
return [
|
||||
hourFormatter(locale),
|
||||
dayHourFormatter(locale),
|
||||
timeMinute.range(startDate, endDate, 10).map((x) => x.valueOf() / 1000),
|
||||
]
|
||||
case TimePeriod.DAY:
|
||||
return [
|
||||
hourFormatter(locale),
|
||||
dayHourFormatter(locale),
|
||||
timeHour.range(startDate, endDate, 4).map((x) => x.valueOf() / 1000),
|
||||
]
|
||||
case TimePeriod.WEEK:
|
||||
return [
|
||||
weekFormatter(locale),
|
||||
dayHourFormatter(locale),
|
||||
timeDay.range(startDate, endDate, 1).map((x) => x.valueOf() / 1000),
|
||||
]
|
||||
case TimePeriod.MONTH:
|
||||
return [
|
||||
monthDayFormatter(locale),
|
||||
dayHourFormatter(locale),
|
||||
timeDay.range(startDate, endDate, 7).map((x) => x.valueOf() / 1000),
|
||||
]
|
||||
case TimePeriod.YEAR:
|
||||
return [
|
||||
monthTickFormatter(locale),
|
||||
monthYearDayFormatter(locale),
|
||||
timeMonth.range(startDate, endDate, 2).map((x) => x.valueOf() / 1000),
|
||||
]
|
||||
case TimePeriod.ALL:
|
||||
return [
|
||||
monthYearFormatter(locale),
|
||||
monthYearDayFormatter(locale),
|
||||
timeMonth.range(startDate, endDate, 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 scale
|
||||
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
|
||||
@@ -190,42 +226,57 @@ export function PriceChart({ width, height }: PriceChartProps) {
|
||||
pricePoint = x0.valueOf() - d0.timestamp.valueOf() > d1.timestamp.valueOf() - x0.valueOf() ? d1 : d0
|
||||
}
|
||||
|
||||
setSelected({ pricePoint, xCoordinate: timeScale(pricePoint.timestamp) })
|
||||
setCrosshair(timeScale(pricePoint.timestamp))
|
||||
setDisplayPrice(pricePoint)
|
||||
},
|
||||
[timeScale, pricePoints]
|
||||
[timeScale, prices]
|
||||
)
|
||||
|
||||
const resetDisplay = useCallback(() => {
|
||||
setCrosshair(null)
|
||||
setDisplayPrice(endingPrice)
|
||||
}, [setCrosshair, setDisplayPrice, endingPrice])
|
||||
|
||||
// TODO: Display no data available error
|
||||
if (!prices) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [tickFormatter, crosshairDateFormatter, ticks] = tickFormat(
|
||||
startingPrice.timestamp,
|
||||
endingPrice.timestamp,
|
||||
timePeriod,
|
||||
locale
|
||||
)
|
||||
const [delta, arrow] = getDelta(startingPrice.value, selected.pricePoint.value)
|
||||
const crosshairEdgeMax = width * 0.97
|
||||
const crosshairAtEdge = !!selected.xCoordinate && selected.xCoordinate > crosshairEdgeMax
|
||||
const delta = calculateDelta(startingPrice.value, displayPrice.value)
|
||||
const formattedDelta = formatDelta(delta)
|
||||
const arrow = getDeltaArrow(delta)
|
||||
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
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChartHeader>
|
||||
<TokenPrice>${selected.pricePoint.value.toFixed(2)}</TokenPrice>
|
||||
<TokenPrice>${displayPrice.value < 0.000001 ? '<0.000001' : displayPrice.value.toFixed(6)}</TokenPrice>
|
||||
<DeltaContainer>
|
||||
{delta}
|
||||
{formattedDelta}
|
||||
<ArrowCell>{arrow}</ArrowCell>
|
||||
</DeltaContainer>
|
||||
</ChartHeader>
|
||||
<LineChart
|
||||
data={pricePoints}
|
||||
data={prices}
|
||||
getX={(p: PricePoint) => timeScale(p.timestamp)}
|
||||
getY={(p: PricePoint) => rdScale(p.value)}
|
||||
marginTop={margin.top}
|
||||
/* Default curve doesn't look good for the ALL chart */
|
||||
curve={timePeriod === TimePeriod.all ? curveBasis : undefined}
|
||||
curve={curveCardinal.tension(curveTension)}
|
||||
strokeWidth={2}
|
||||
width={graphWidth}
|
||||
height={graphHeight}
|
||||
>
|
||||
{selected.xCoordinate !== null ? (
|
||||
{crosshair !== null ? (
|
||||
<g>
|
||||
<AxisBottom
|
||||
scale={timeScale}
|
||||
@@ -244,25 +295,25 @@ export function PriceChart({ width, height }: PriceChartProps) {
|
||||
})}
|
||||
/>
|
||||
<text
|
||||
x={selected.xCoordinate + (crosshairAtEdge ? -4 : 4)}
|
||||
x={crosshair + (crosshairAtEdge ? -4 : 4)}
|
||||
y={margin.crosshair + 10}
|
||||
textAnchor={crosshairAtEdge ? 'end' : 'start'}
|
||||
fontSize={12}
|
||||
fill={theme.textSecondary}
|
||||
>
|
||||
{crosshairDateFormatter(selected.pricePoint.timestamp)}
|
||||
{crosshairDateFormatter(displayPrice.timestamp)}
|
||||
</text>
|
||||
<Line
|
||||
from={{ x: selected.xCoordinate, y: margin.crosshair }}
|
||||
to={{ x: selected.xCoordinate, y: graphHeight }}
|
||||
from={{ x: crosshair, y: margin.crosshair }}
|
||||
to={{ x: crosshair, y: graphHeight }}
|
||||
stroke={theme.backgroundOutline}
|
||||
strokeWidth={1}
|
||||
pointerEvents="none"
|
||||
strokeDasharray="4,4"
|
||||
/>
|
||||
<GlyphCircle
|
||||
left={selected.xCoordinate}
|
||||
top={rdScale(selected.pricePoint.value) + margin.top}
|
||||
left={crosshair}
|
||||
top={rdScale(displayPrice.value) + margin.top}
|
||||
size={50}
|
||||
fill={theme.accentActive}
|
||||
stroke={theme.backgroundOutline}
|
||||
@@ -281,14 +332,20 @@ export function PriceChart({ width, height }: PriceChartProps) {
|
||||
onTouchStart={handleHover}
|
||||
onTouchMove={handleHover}
|
||||
onMouseMove={handleHover}
|
||||
onMouseLeave={() => setSelected(initialState)}
|
||||
onMouseLeave={resetDisplay}
|
||||
/>
|
||||
</LineChart>
|
||||
<TimeOptionsWrapper>
|
||||
<TimeOptionsContainer>
|
||||
{TIME_DISPLAYS.map(([value, display]) => (
|
||||
<TimeButton key={display} active={timePeriod === value} onClick={() => setTimePeriod(value)}>
|
||||
{display}
|
||||
{ORDERED_TIMES.map((time) => (
|
||||
<TimeButton
|
||||
key={DISPLAYS[time]}
|
||||
active={timePeriod === time}
|
||||
onClick={() => {
|
||||
setTimePeriod(time)
|
||||
}}
|
||||
>
|
||||
{DISPLAYS[time]}
|
||||
</TimeButton>
|
||||
))}
|
||||
</TimeOptionsContainer>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { useRef } from 'react'
|
||||
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, Z_INDEX } from 'theme'
|
||||
import { colors } from 'theme/colors'
|
||||
import { opacify } from 'theme/utils'
|
||||
|
||||
@@ -24,7 +25,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`
|
||||
@@ -40,7 +41,7 @@ const ShareActions = styled.div`
|
||||
padding: 8px;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border: 0.5px solid ${({ theme }) => theme.backgroundOutline};
|
||||
box-shadow: ${({ theme }) => theme.flyoutDropShadow};
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
border-radius: 12px;
|
||||
`
|
||||
const ShareAction = styled.div`
|
||||
@@ -62,6 +63,7 @@ const ShareAction = styled.div`
|
||||
interface TokenInfo {
|
||||
tokenName: string
|
||||
tokenSymbol: string
|
||||
tokenAddress: string
|
||||
}
|
||||
|
||||
export default function ShareButton(tokenInfo: TokenInfo) {
|
||||
@@ -76,7 +78,7 @@ export default function ShareButton(tokenInfo: TokenInfo) {
|
||||
const shareTweet = () => {
|
||||
toggleShare()
|
||||
window.open(
|
||||
`https://twitter.com/intent/tweet?text=Check%20out%20${tokenInfo.tokenName}%20(${tokenInfo.tokenSymbol})%20https://app.uniswap.org/%23/tokens/${tokenInfo.tokenSymbol}%20via%20@uniswap`,
|
||||
`https://twitter.com/intent/tweet?text=Check%20out%20${tokenInfo.tokenName}%20(${tokenInfo.tokenSymbol})%20https://app.uniswap.org/%23/tokens/${tokenInfo.tokenAddress}%20via%20@uniswap`,
|
||||
'newwindow',
|
||||
`left=${positionX}, top=${positionY}, width=${TWITTER_WIDTH}, height=${TWITTER_HEIGHT}`
|
||||
)
|
||||
@@ -97,13 +99,13 @@ export default function ShareButton(tokenInfo: TokenInfo) {
|
||||
toCopy={window.location.href}
|
||||
ref={copyHelperRef}
|
||||
>
|
||||
Copy Link
|
||||
<Trans>Copy Link</Trans>
|
||||
</CopyHelper>
|
||||
</ShareAction>
|
||||
|
||||
<ShareAction onClick={shareTweet}>
|
||||
<Twitter color={theme.textPrimary} size={20} strokeWidth={1.5} />
|
||||
Share to Twitter
|
||||
<Trans>Share to Twitter</Trans>
|
||||
</ShareAction>
|
||||
</ShareActions>
|
||||
)}
|
||||
|
||||
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,254 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { ParentSize } from '@visx/responsive'
|
||||
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 { checkWarning } from 'constants/tokenSafety'
|
||||
import { useCurrency, useIsUserAddedToken, useToken } from 'hooks/Tokens'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { useCallback } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { ArrowLeft, Heart } from 'react-feather'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ClickableStyle, CopyContractAddress } from 'theme'
|
||||
|
||||
import { favoritesAtom, useToggleFavorite } from '../state'
|
||||
import { ClickFavorited } from '../TokenTable/TokenRow'
|
||||
import Resource from './Resource'
|
||||
import ShareButton from './ShareButton'
|
||||
|
||||
export const AboutSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
`
|
||||
const ContractAddress = styled.button`
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
`
|
||||
export const ContractAddressSection = styled.div`
|
||||
padding: 24px 0px;
|
||||
`
|
||||
const Contract = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 14px;
|
||||
gap: 4px;
|
||||
`
|
||||
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;
|
||||
`
|
||||
const StatPrice = styled.span`
|
||||
font-size: 28px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
`
|
||||
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;
|
||||
`
|
||||
const TokenActions = styled.div`
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
export const TokenInfoContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
const TokenSymbol = styled.span`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
export const TopArea = styled.div`
|
||||
max-width: 832px;
|
||||
`
|
||||
export const ResourcesContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
`
|
||||
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')};
|
||||
`
|
||||
|
||||
export default function LoadedTokenDetail({ address }: { address: string }) {
|
||||
const token = useToken(address)
|
||||
const 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 chainInfo = getChainInfo(token?.chainId)
|
||||
const networkLabel = chainInfo?.label
|
||||
const networkBadgebackgroundColor = chainInfo?.backgroundColor
|
||||
|
||||
// catch token error and loading state
|
||||
if (!token || !token.name || !token.symbol) {
|
||||
return <div>No Token</div>
|
||||
}
|
||||
const tokenName = token.name
|
||||
const tokenSymbol = token.symbol
|
||||
|
||||
// TODO: format price, add sparkline
|
||||
const aboutToken =
|
||||
'Ethereum is a decentralized computing platform that uses ETH (Ether) to pay transaction fees (gas). Developers can use Ethereum to run decentralized applications (dApps) and issue new crypto assets, known as Ethereum tokens.'
|
||||
const tokenMarketCap = '23.02B'
|
||||
const tokenVolume = '1.6B'
|
||||
|
||||
return (
|
||||
<TopArea>
|
||||
<BreadcrumbNavLink to="/explore">
|
||||
<ArrowLeft size={14} /> Explore
|
||||
</BreadcrumbNavLink>
|
||||
<ChartHeader>
|
||||
<TokenInfoContainer>
|
||||
<TokenNameCell>
|
||||
<CurrencyLogo currency={currency} size={'32px'} />
|
||||
{tokenName} <TokenSymbol>{tokenSymbol}</TokenSymbol>
|
||||
{!warning && <VerifiedIcon size="20px" />}
|
||||
{networkBadgebackgroundColor && (
|
||||
<NetworkBadge networkColor={chainInfo?.color} backgroundColor={networkBadgebackgroundColor}>
|
||||
{networkLabel}
|
||||
</NetworkBadge>
|
||||
)}
|
||||
</TokenNameCell>
|
||||
<TokenActions>
|
||||
<ShareButton tokenName={tokenName} tokenSymbol={tokenSymbol} />
|
||||
<ClickFavorited onClick={toggleFavorite}>
|
||||
<FavoriteIcon isFavorited={isFavorited} />
|
||||
</ClickFavorited>
|
||||
</TokenActions>
|
||||
</TokenInfoContainer>
|
||||
<ChartContainer>
|
||||
<ParentSize>{({ width, height }) => <PriceChart width={width} height={height} />}</ParentSize>
|
||||
</ChartContainer>
|
||||
</ChartHeader>
|
||||
<AboutSection>
|
||||
<AboutHeader>
|
||||
<Trans>About</Trans>
|
||||
</AboutHeader>
|
||||
{aboutToken}
|
||||
<ResourcesContainer>
|
||||
<Resource name={'Etherscan'} link={'https://etherscan.io/'} />
|
||||
<Resource name={'Protocol Info'} link={`https://info.uniswap.org/#/tokens/${address}`} />
|
||||
</ResourcesContainer>
|
||||
</AboutSection>
|
||||
<StatsSection>
|
||||
<StatPair>
|
||||
<Stat>
|
||||
Market cap<StatPrice>${tokenMarketCap}</StatPrice>
|
||||
</Stat>
|
||||
<Stat>
|
||||
{/* TODO: connect to chart's selected time */}
|
||||
24H volume
|
||||
<StatPrice>${tokenVolume}</StatPrice>
|
||||
</Stat>
|
||||
</StatPair>
|
||||
<StatPair>
|
||||
<Stat>
|
||||
52W low
|
||||
<StatPrice>$1,790.01</StatPrice>
|
||||
</Stat>
|
||||
<Stat>
|
||||
52W high
|
||||
<StatPrice>$4,420.71</StatPrice>
|
||||
</Stat>
|
||||
</StatPair>
|
||||
</StatsSection>
|
||||
<ContractAddressSection>
|
||||
<Contract>
|
||||
Contract Address
|
||||
<ContractAddress>
|
||||
<CopyContractAddress address={address} />
|
||||
</ContractAddress>
|
||||
</Contract>
|
||||
</ContractAddressSection>
|
||||
<TokenSafetyModal
|
||||
isOpen={warningModalOpen}
|
||||
tokenAddress={address}
|
||||
onCancel={() => navigate(-1)}
|
||||
onContinue={handleDismissWarning}
|
||||
/>
|
||||
</TopArea>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useAtom } from 'jotai'
|
||||
import { Heart } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { SMALL_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { SMALLEST_MOBILE_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { showFavoritesAtom } from '../state'
|
||||
import FilterOption from './FilterOption'
|
||||
|
||||
const FavoriteButtonContent = styled.div`
|
||||
display: flex;
|
||||
@@ -11,22 +13,8 @@ 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 }) => 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: ${SMALL_MEDIA_BREAKPOINT}) {
|
||||
@media only screen and (max-width: ${SMALLEST_MOBILE_MEDIA_BREAKPOINT}) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
@@ -35,11 +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={theme.textPrimary} fill="transparent" />
|
||||
<FavoriteText>Favorites</FavoriteText>
|
||||
<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;
|
||||
@@ -47,8 +47,8 @@ const MenuTimeFlyout = styled.span`
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
box-shadow: ${({ theme }) => theme.flyoutDropShadow};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
border: 0.5px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
@@ -59,33 +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.accentActionSoft : 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;
|
||||
background-color: ${({ theme, open }) => (open ? theme.accentActionSoft : theme.backgroundModule)};
|
||||
}
|
||||
:focus {
|
||||
background-color: ${({ theme, open }) => (open ? theme.accentActionSoft : theme.backgroundInteractive)};
|
||||
}
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -93,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;
|
||||
@@ -140,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 { useState } from 'react'
|
||||
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'
|
||||
@@ -12,33 +14,33 @@ const SearchBarContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
`
|
||||
const SearchInput = styled.input<{ expanded: boolean }>`
|
||||
const SearchInput = styled.input`
|
||||
background: no-repeat scroll 7px 7px;
|
||||
background-image: ${({ expanded }) => !expanded && `url(${searchIcon})`};
|
||||
background-image: url(${searchIcon});
|
||||
background-size: 20px 20px;
|
||||
background-position: 14px center;
|
||||
background-color: 'transparent';
|
||||
background-position: 12px center;
|
||||
background-color: ${({ theme }) => theme.backgroundModule};
|
||||
border-radius: 12px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
height: 100%;
|
||||
width: ${({ expanded }) => (expanded ? '100%' : '52px')};
|
||||
width: min(200px, 100%);
|
||||
font-size: 16px;
|
||||
padding-left: 35px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
transition: width 0.75s cubic-bezier(0, 0.795, 0, 1);
|
||||
padding-left: 40px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
transition-duration: ${({ theme }) => theme.transition.duration.fast};
|
||||
|
||||
:hover {
|
||||
cursor: ${({ expanded }) => !expanded && 'pointer'};
|
||||
background-color: ${({ theme }) => theme.backgroundModule};
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
background-color: ${({ theme }) => theme.accentActionSoft};
|
||||
border: none;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border-color: ${({ theme }) => theme.accentActionSoft};
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: ${({ expanded, theme }) => (expanded ? theme.textTertiary : 'transparent')};
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
}
|
||||
::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
@@ -57,21 +59,30 @@ const SearchInput = styled.input<{ expanded: boolean }>`
|
||||
`
|
||||
|
||||
export default function SearchBar() {
|
||||
const [filterString, setFilterString] = useAtom(filterStringAtom)
|
||||
const [isExpanded, setExpanded] = useState(false)
|
||||
const [localFilterString, setLocalFilterString] = useState('')
|
||||
const setFilterString = useUpdateAtom(filterStringAtom)
|
||||
const debouncedLocalFilterString = useDebounce(localFilterString, 300)
|
||||
|
||||
useEffect(() => {
|
||||
setFilterString(debouncedLocalFilterString)
|
||||
}, [debouncedLocalFilterString, setFilterString])
|
||||
|
||||
return (
|
||||
<SearchBarContainer>
|
||||
<SearchInput
|
||||
expanded={isExpanded}
|
||||
type="search"
|
||||
placeholder="Search by name or token address"
|
||||
id="searchBar"
|
||||
onBlur={() => isExpanded && filterString.length === 0 && setExpanded(false)}
|
||||
onFocus={() => setExpanded(true)}
|
||||
autoComplete="off"
|
||||
value={filterString}
|
||||
onChange={({ target: { value } }) => setFilterString(value)}
|
||||
/>
|
||||
<Trans
|
||||
render={({ translation }) => (
|
||||
<SearchInput
|
||||
type="search"
|
||||
placeholder={`${translation}`}
|
||||
id="searchBar"
|
||||
autoComplete="off"
|
||||
value={localFilterString}
|
||||
onChange={({ target: { value } }) => setLocalFilterString(value)}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
Filter tokens
|
||||
</Trans>
|
||||
</SearchBarContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TimePeriod } from 'hooks/useExplorePageQuery'
|
||||
import { TimePeriod } from 'graphql/data/Token'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useRef } from 'react'
|
||||
@@ -9,16 +9,25 @@ 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 TIME_DISPLAYS: { [key: string]: string } = {
|
||||
hour: '1H',
|
||||
day: '1D',
|
||||
week: '1W',
|
||||
month: '1M',
|
||||
year: '1Y',
|
||||
export const DISPLAYS: Record<TimePeriod, string> = {
|
||||
[TimePeriod.HOUR]: '1H',
|
||||
[TimePeriod.DAY]: '1D',
|
||||
[TimePeriod.WEEK]: '1W',
|
||||
[TimePeriod.MONTH]: '1M',
|
||||
[TimePeriod.YEAR]: '1Y',
|
||||
[TimePeriod.ALL]: 'All',
|
||||
}
|
||||
|
||||
const TIMES = [TimePeriod.hour, TimePeriod.day, TimePeriod.week, TimePeriod.month, TimePeriod.year]
|
||||
export const ORDERED_TIMES = [
|
||||
TimePeriod.HOUR,
|
||||
TimePeriod.DAY,
|
||||
TimePeriod.WEEK,
|
||||
TimePeriod.MONTH,
|
||||
TimePeriod.YEAR,
|
||||
TimePeriod.ALL,
|
||||
]
|
||||
|
||||
const InternalMenuItem = styled.div`
|
||||
flex: 1;
|
||||
@@ -31,7 +40,6 @@ const InternalMenuItem = styled.div`
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
const InternalLinkMenuItem = styled(InternalMenuItem)`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -51,8 +59,8 @@ const MenuTimeFlyout = styled.span`
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
box-shadow: ${({ theme }) => theme.flyoutDropShadow};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
border: 0.5px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
@@ -68,33 +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.accentActionSoft : theme.backgroundInteractive)};
|
||||
padding: 6px 12px 6px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 600;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background-color: ${({ theme, open }) => (open ? theme.accentActionSoft : theme.backgroundModule)};
|
||||
}
|
||||
:focus {
|
||||
background-color: ${({ theme, open }) => (open ? theme.accentActionSoft : theme.backgroundInteractive)};
|
||||
}
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -102,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
|
||||
@@ -134,25 +113,29 @@ 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>
|
||||
{TIME_DISPLAYS[activeTime]}
|
||||
{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>
|
||||
{TIMES.map((time) => (
|
||||
{ORDERED_TIMES.map((time) => (
|
||||
<InternalLinkMenuItem
|
||||
key={time}
|
||||
key={DISPLAYS[time]}
|
||||
onClick={() => {
|
||||
setTime(time)
|
||||
toggleMenu()
|
||||
}}
|
||||
>
|
||||
<div>{TIME_DISPLAYS[time]}</div>
|
||||
<div>{DISPLAYS[time]}</div>
|
||||
{time === activeTime && <Check color={theme.accentAction} size={16} />}
|
||||
</InternalLinkMenuItem>
|
||||
))}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user