Compare commits

...

83 Commits

Author SHA1 Message Date
Zach Pomerantz
4f48f3372c feat: weth check (#4685)
* feat: throw on invalid deposits

* feat: add new bug link

* chore: note the usage of error state
2022-09-21 11:39:35 -05:00
Jordan Frankfurt
9e070107a2 feat: new gql schema (#4654)
* new gql schema

* refactor: switch explore over to new queries (#4657)

* initial commit
* improved performance, added filtering
* addressed pr comments
* fixed typescript issue

* drop leftover comment

* clean up loaded row props

Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>
2022-09-21 09:44:14 -05:00
vignesh mohankumar
2a92b2992f fix: remove hover state for chevrons in nav (#4681)
* fix

* fix
2022-09-21 10:28:22 -04:00
aballerr
e180153c3a chore: merge marketplace and traits (#4645)
* porting of filters

Co-authored-by: Jack Short <john.short.tj@gmail.com>
2022-09-21 07:22:05 -04:00
Greg Bugyis
e35f9e16a1 feat: NFT Explore Activity Feed (#4635)
* NFT Explore: Add Activity Feed to Banner section

* Renamed separate style file

* Fix positioning to not squish details section

* Add back activeRow state

* Hide Activity on smaller screens

* Fix for uneven widths between collections

* Addressing PR feedback

Co-authored-by: gbugyis <greg@bugyis.com>
2022-09-21 02:08:25 +03:00
vignesh mohankumar
8e955e9257 fix: fully round corner on account dropdown (#4682)
* fix: fully rounder corner on account dropdown

* comment
2022-09-20 18:38:32 -04:00
lynn
9ca44652b3 fix: make token detail chart full width and x axis fixes (#4669)
* full chart width

* add offsets to x axis start and end times so they dont get cut off

* hide ticks

* reduce offsets

* undo
2022-09-20 17:20:43 -04:00
vignesh mohankumar
3d89d72426 chore: feature flags token favorites (#4680)
* fix: fixes TokenTable header ordering

* flag favorite tokens

* imports

* remove fav col

* hide on token details
2022-09-20 16:36:44 -04:00
vignesh mohankumar
c2ffab3273 fix: center SearchBar vertically (#4677)
* fix: center SearchBar vertically

* oops
2022-09-20 16:36:23 -04:00
vignesh mohankumar
dbb62b613c chore: remove tokensNetworkFilter feature flag (#4679) 2022-09-20 15:57:22 -04:00
vignesh mohankumar
6f2d6e31c9 feat: add "approve in wallet" prompt on chain selector (#4673)
* logic

* working

* grid

* some file renaming

* Move to ChainSelectorRow.tsx

* remove flex

* more renames

* more styles

* fixes

* local imports

* more styling changes

* style

* fix mobile

* toggle open on open

* setIsOpen
2022-09-20 15:34:34 -04:00
vignesh mohankumar
944939a2e9 fix: makes sure wallet error is visible (#4676) 2022-09-20 15:32:11 -04:00
vignesh mohankumar
04164a550d fix: close WalletModal on connection (#4670)
* fix: close WalletModal on connection

* fix test
2022-09-20 14:08:09 -04:00
vignesh mohankumar
16a5e15070 fix: center Web3StatusConnectButton (#4671) 2022-09-20 14:02:59 -04:00
Charles Bachmeier
ee97d8d902 feat: add listing modal (#4663)
* add listing modal

* add new files

* remove useeffect

* re-add useeffect and includes array

* position relative

* add listing datatype

* use pluralize

* readable const

* clsx

* parseFloat 0 default

* don't use any

* cant use months for ms

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2022-09-20 10:45:22 -07:00
a6Ce6Bs
3d4b077b89 fix: networkProvider error (#4674) 2022-09-20 11:34:40 -05:00
pp-hh-ii-ll
0f4a89d938 fix: update Unisocks status icon and positioning (#4582)
* fix: update Unisocks status icon and positioning

Updates design of Unisocks icon and adjusts positioning to suit new design

* Update StatusIcon.tsx
2022-09-19 15:17:41 -04:00
vignesh mohankumar
0d0ec12dbf fix: normalize NavBar button heights (#4656)
* fix: normalize NavBar button heights

* chainswitcher and web3status are not using this component

* update nav icon
2022-09-19 14:50:30 -04:00
vignesh mohankumar
afe30a2c02 fix: reset Widget amount on URL change (#4668)
* fix: reset Widget amount on URL change

* empty
2022-09-19 14:50:16 -04:00
vignesh mohankumar
2f9289a2c5 fix: updates Widget defaultToken on URL change (#4666)
* fix: updates defaultToken on URL change

* rm usePrevious
2022-09-19 13:52:36 -04:00
vignesh mohankumar
7a9d2e80d0 refactor: move components/AmplitudeAnalytics to analytics (#4655)
* move components/AmplitudeAnalytics to analytics/amplitude

* move to analytics

* fix imports
2022-09-19 12:54:54 -04:00
vignesh mohankumar
d4b8735c04 fix: remove sendTestAnalyticsEvent (#4653)
* fix: remove sendTestAnalyticsEvent

* comments
2022-09-19 12:44:29 -04:00
vignesh mohankumar
d31687d0bf style: remove broken flex property (#4650) 2022-09-19 11:53:42 -04:00
vignesh mohankumar
470535dd33 fix: pass outputCurrency not inputCurrency (#4649) 2022-09-19 11:53:31 -04:00
lynn
27f53f1e99 fix: load correct initial display price and % delta w/o need for scrubbing on token detail chart (#4651)
* remove console logs

* fix in response to comments
2022-09-19 11:30:09 -04:00
aballerr
e7d498c95e chore: Merging details part 3 (#4637)
* Merging details part 3



Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-19 09:32:11 -04:00
vignesh mohankumar
02b617d297 fix: move token banner logic into Modals (#4638)
* move banner logic into Modals

* fixed

* z-index

* fix

* fix
2022-09-16 16:25:25 -04:00
vignesh mohankumar
96d04e1a7d fix: remove search overlay (#4648) 2022-09-16 16:24:13 -04:00
vignesh mohankumar
fe9d805d7c fix: link to swap with inputCurrency from mobile token detail (#4647) 2022-09-16 16:00:58 -04:00
vignesh mohankumar
b6c136839e fix: show copied text where address was clicked (#4646)
* pos x

* working

* tooltip width
2022-09-16 15:14:55 -04:00
Charles Bachmeier
8c947a0e0d feat: Add Select Page Modal (#4643)
* add select nfts shopping bag modal

* Add shopping bag to top lovel modal wrapper

* addressing comments

* rename to listingTag

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2022-09-16 11:31:25 -07:00
Charles Bachmeier
49c5cbbf3b feat: Add sell page filters sidebar (#4630)
* working sell filters

* split filters into own file

* include new file

* fix eslint warnings

* update filter button param and fix rerender bug

* de morgon's law

* usecallback

* move max_padding

* extend htmlinputelement for checkbox

* styles cleanup

* simplify checkbox sprinkles

* add null check to collectionfilteritem

* remove x axis scrollbar on collections

* update fitlerbutton logic

* scrollbar width

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2022-09-16 10:55:26 -07:00
Greg Bugyis
efaefe2e44 fix: Token Details chart: adjust curveTension on 1H chart timeframe (#4632)
Adjust curveTension on 1H chart timeframe
2022-09-16 20:19:20 +03:00
lynn
ed95f1b966 fix: add IP to test project for amplitude (#4644)
* init

* fix lint

* add comment

* lint fix

Co-authored-by: Lynn Yu <lynn.yu@ContangoITs-MacBook-Pro.local>
2022-09-16 13:13:30 -04:00
Jack Short
7ecbc552aa feat: cards resize to uniform height (#4639)
* feat: cards resize to uniform height if differenting heights in collection

* setting uniform height if card is video
2022-09-16 10:45:05 -04:00
Jack Short
dadc997398 chore: updating sprinkles z indices (#4641)
chore: updating sprinkles zindices
2022-09-15 19:10:43 -04:00
lynn
b90d6b5ab0 feat: amplitude swap fixes (#4627)
* init

* add back txn completed event
2022-09-15 17:53:45 -04:00
Jack Short
db5c6f82fd fix: search rerender (#4640) 2022-09-15 17:28:19 -04:00
lynn
f161f9617b feat: Web 924 explore use real data for the sparkline price charts (#4634)
* init

* test

* working w new hook

* simsplify

* fixes to comments

* extra comment remove
2022-09-15 17:27:33 -04:00
Connor McEwen
9d3249e6bd fix: rendering issues with modal/header (#4636)
* fix overlay issue with modal

* fix z index
2022-09-15 15:32:06 -04:00
Jack Short
f1c65afa98 feat: audio video nft collection cards (#4628)
* feat: video asset cards

* feat: audio cards

* square aspect ratio for videos

* adding playsinline
2022-09-15 15:12:23 -04:00
Jack Short
80c1f0cdf9 feat: nft filter bar (#4617)
* initial filter window

* filters

* filter button

* adding all filters to filter check

* sorting exports

* reviewing old css

* change to const

* responding to comments

* removing isMobile

* fixing radio input

* refactoring radio

* refactoring radio

* reusing the same class

* removing unused props

* removing useless clsx

* removing scrollToTop
2022-09-15 13:01:04 -04:00
aballerr
ea0fe83d00 chore: Merging details part 2 (#4594)
* Merging details part 2


Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-15 12:28:04 -04:00
Greg Bugyis
994836fba7 style: Extend hover on secondary dropdown menu links (#4633)
* Full width hover on secondary menu links

* Remove commented line
2022-09-15 19:27:03 +03:00
Greg Bugyis
41aa1dcb0b feat: NFT Explore Value prop row (#4603)
* NFT Explore: value prop row

* Changes from PR feedback

* Tweak opacity values

* Style cleanup

* Adjust bg image on smaller screen sizes

* Remove unnecessary media query

Co-authored-by: gbugyis <greg@bugyis.com>
2022-09-15 11:48:15 +03:00
Zach Pomerantz
3965d3fdd9 feat: upgrade widget (#4629)
* feat: upgrade widget and use static connectors

* build: upgrade lockfile
2022-09-14 16:29:18 -07:00
cartcrom
ff6fd8a6e9 style: added transition ease to explore (#4616)
* initial commit
* updated transition css
* added opacity constants to theme, switched to using transition duration constants
2022-09-14 17:03:19 -04:00
taycaldwell
0a6906b23e fix: Swaps icons for light theme and dark theme (#4571)
Fix icons for light/dark mode
2022-09-14 14:17:36 -04:00
Charles Bachmeier
c38b5c0ce3 feat: migrate select page assets (#4618)
* add main nft sell page

* remove background

* more precise naming

* Add wallet assets for select page

* update styles while without filter bar

* remove unnecessary useeffect

* deprecate old stlye

* move to common props

* add round helper fn

* use react router link

Co-authored-by: Charlie <charlie@uniswap.org>
Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2022-09-14 11:07:58 -07:00
Zach Pomerantz
86f3b5a036 feat: caching provider (#4615)
* feat: caching providers

* feat: clear cache per block
2022-09-14 10:05:29 -07:00
Jack Short
382a44f040 feat: adding bag icon to navbar (#4619)
* feat: adding bag icon to navbar

* updating sell icon
2022-09-14 12:40:42 -04:00
Greg Bugyis
2d9604cd14 fix: Tick collisions on token detail price chart (#4623) 2022-09-14 17:19:38 +03:00
cartcrom
7930709bc3 refactor: unnesting token details into sectioned components (#4621)
finished refactoring
2022-09-14 10:19:18 -04:00
Zach Pomerantz
6fe2c92cee fix: reduce price fetching to every 2m (#4622)
* fix: reduce price fetching to every 2m

* fix: grammar
2022-09-13 15:16:01 -07:00
Jack Short
884dee2db3 fix: pointing nft links to the right url hash (#4620) 2022-09-13 17:03:08 -04:00
Zach Pomerantz
1f00c2a9c4 fix: fetch the usd price from parsed amount (#4612) 2022-09-13 10:31:30 -07:00
cartcrom
84070835df feat: token data cache (#4534)
* initial cache construction
* switched to relay cache, updated hooks
* fixed comments
2022-09-13 13:09:12 -04:00
Zach Pomerantz
fb389137e7 feat: adds ethers provider tracing (#4614) 2022-09-13 09:07:47 -07:00
Charles Bachmeier
c14b6a78ae feat: add main nft sell page (#4609)
* add main nft sell page

* remove background

* more precise naming

Co-authored-by: Charlie <charlie@uniswap.org>
Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2022-09-13 09:07:06 -07:00
Zach Pomerantz
a6c1c49f98 fix: update feature flags on update (#4613) 2022-09-13 09:03:59 -07:00
Charles Bachmeier
7848ad86bd fix: adjust search input width (#4610)
adjust search input width

Co-authored-by: Charlie <charlie@uniswap.org>
2022-09-13 08:39:57 -07:00
Charles Bachmeier
882c15dada fix: bug where changing tabs can open the searchbar (#4607)
* remove onFocus trigger

* typo on comment

Co-authored-by: Charlie <charlie@uniswap.org>
2022-09-12 11:39:07 -07:00
Charles Bachmeier
704ad222d9 feat: add animation to search skeleton and typing state (#4598)
* fade when loading

* fade results between searches

* trending search loading skeleton

* adjust skeleton and cleanup

* remove unused style change

* eslint

* add trendingTokens to query and remove unnecessary returns

* move array map compatibility higher in the call hierarchy

* add feature flag to isLoading param

Co-authored-by: Charlie <charlie@uniswap.org>
2022-09-12 10:47:53 -07:00
Charles Bachmeier
cfee80ce3c feat: Add animations for desktop, tablet, and mobile searchbar (#4584)
* animations working pre cleanup

* all sprinkles

* fix bug with phase1 search content

* new isTablet hook

* add conditional vars

* typeof window

* remove unneeded clsx usage

Co-authored-by: Charlie <charlie@uniswap.org>
2022-09-12 10:09:59 -07:00
github-actions[bot]
eb95cedd72 chore(i18n): new Crowdin translations (#4512)
chore(i18n): synchronize translations from crowdin [skip ci]

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-09-12 11:21:30 -04:00
vignesh mohankumar
99a7fb3383 fix: remove refetchOnFocus for routing-api (#4601) 2022-09-09 18:35:12 -04:00
vignesh mohankumar
7f4dbf9346 chore: update smart-order-router (#4600)
* bump SOR

* dedupe
2022-09-09 18:31:41 -04:00
Zach Pomerantz
09b00c9974 fix: use static rpc urls (#4599) 2022-09-09 14:58:13 -07:00
Zach Pomerantz
b74fb8174d feat: optimize AlphaRouter usage (#4596)
* feat: add RouterPreference.PRICE

* docs: add reference to quote params

* fix: cache routers between usages

* fix: tune price inquiries

* fix: note price queries tuning

* fix: clean up params - nix mixed

* fix: defer PRICE tuning
2022-09-09 13:07:53 -07:00
Jordan Frankfurt
a7ec5a64b7 feat(explore): add a simple search debounce (#4595)
add a simple search debounce
2022-09-09 14:09:08 -05:00
Greg Bugyis
c619dcf65d fix: Token chart - inconsistent x-axis time intervals (#4579)
* Use timeScale for x-axis

* Use d3 time intervals for ticks

* Drop slice for now, will handle differently

* scaleTime turned out to be unnecessary

* Use .nice() to help with tick spacing at start/end

Co-authored-by: gbugyis <greg@bugyis.com>
2022-09-09 22:08:57 +03:00
aballerr
1221d88e13 chore: Cypress utility function for selecting feature flags and walletdrop down cypress tests (#4536)
* Adding feature flag utility to cypress and adding wallet cypress tests


Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-09 14:54:54 -04:00
Charles Bachmeier
48d2ead71d feat: update universal token search and trending tokens endpoint (#4593)
update universal token search and trending tokens endpoint

Co-authored-by: Charlie <charlie@uniswap.org>
2022-09-09 10:28:54 -07:00
aballerr
ed7099bfd6 chore: Merging details page (#4585)
* Initial merge of details page


Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-09 13:23:41 -04:00
vignesh mohankumar
2604cdfdae fix: only initialize using chain query (#4567) 2022-09-08 12:16:29 -04:00
cartcrom
94dc389812 feat: widget speedbumps on swap review (#4544)
* initial commit
* finished feature
* addressed PR comments
2022-09-07 16:12:35 -04:00
Greg Bugyis
4a8c621f46 feat: add maxHeight to CurrencySearchModal (#4557)
* Add maxHeight to CurrencySearchModal (search only)

* Combine min and maxHeight into single modalHeight value

* Use clearer variable name to distinguish window height value

Co-authored-by: gbugyis <greg@bugyis.com>
2022-09-07 23:08:29 +03:00
aballerr
477af8af4e fix: Making sure all icons are 24px (#4580)
Making all icons size 24px on web status

Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-07 15:47:24 -04:00
aballerr
a9a7d524aa fix: fixing token colors and token select persistence (#4575)
* fixing token colors and token select persistence

Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-07 13:35:00 -04:00
aballerr
1cdaff8ddf fix: fixing match design (#4577)
* fixing select token favorite icon to match design



Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-07 12:44:19 -04:00
aballerr
eeea3d2dcc fix: fixed wallet scrolling issue (#4574)
* fixed scrolling issue for wallet


Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-07 11:44:33 -04:00
lynn
f46b6a0697 fix: ensure nav bar goes above all other components when scrolling (#4576)
* fix nav bar below other components issue

* respond to comments
2022-09-06 17:18:54 -04:00
lynn
622581ee0a feat: show real values for current network balance (#4565)
* init

* remove card when balance is zero

* remove commented code

* remove commented
2022-09-06 11:50:02 -04:00
280 changed files with 10338 additions and 2559 deletions

1
.env
View File

@@ -5,3 +5,4 @@ REACT_APP_AWS_API_ACCESS_KEY="AKIAYJJWW6AQ47ODATHN"
REACT_APP_AWS_API_ACCESS_SECRET="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"
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"

View File

@@ -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
}
]
}
]
}
}

View 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()
})
})

View File

@@ -24,6 +24,7 @@ describe('Wallet', () => {
})
it('shows connect buttons after disconnect', () => {
cy.get('[data-testid=web3-status-connected]').contains(TEST_ADDRESS_NEVER_USE_SHORTENED).click()
cy.contains('Disconnect').click()
cy.get('[data-testid=option-grid]').should('exist')
})

View File

@@ -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
},
})

View File

@@ -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
View File

@@ -0,0 +1 @@
export const getTestSelector = (selectorId: string) => `[data-testid=${selectorId}]`

View File

@@ -137,7 +137,7 @@
"@uniswap/redux-multicall": "^1.1.5",
"@uniswap/router-sdk": "^1.3.0",
"@uniswap/sdk-core": "^3.0.1",
"@uniswap/smart-order-router": "^2.9.2",
"@uniswap/smart-order-router": "^2.10.0",
"@uniswap/token-lists": "^1.0.0-beta.30",
"@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
@@ -145,7 +145,7 @@
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@uniswap/v3-sdk": "^3.9.0",
"@uniswap/widgets": "^2.7.0",
"@uniswap/widgets": "^2.8.1",
"@vanilla-extract/css": "^1.7.2",
"@vanilla-extract/css-utils": "^0.1.2",
"@vanilla-extract/dynamic": "^2.0.2",

View File

@@ -15,7 +15,8 @@ export enum EventName {
SWAP_MAX_TOKEN_AMOUNT_SELECTED = 'Swap Max Token Amount Selected',
SWAP_PRICE_UPDATE_ACKNOWLEDGED = 'Swap Price Update Acknowledged',
SWAP_QUOTE_RECEIVED = 'Swap Quote Received',
SWAP_SUBMITTED = 'Swap Submitted',
SWAP_SIGNED = 'Swap Signed',
SWAP_SUBMITTED_BUTTON_CLICKED = 'Swap Submit Button Clicked',
SWAP_TOKENS_REVERSED = 'Swap Tokens Reversed',
SWAP_TRANSACTION_COMPLETED = 'Swap Transaction Completed',
TOKEN_IMPORTED = 'Token Imported',
@@ -24,6 +25,7 @@ export enum EventName {
WALLET_CONNECT_TXN_COMPLETED = 'Wallet Connect Transaction Completed',
WALLET_SELECTED = 'Wallet Selected',
WEB_VITALS = 'Web Vitals',
WRAP_TOKEN_TXN_INVALIDATED = 'Wrap Token Transaction Invalidated',
WRAP_TOKEN_TXN_SUBMITTED = 'Wrap Token Transaction Submitted',
// alphabetize additional event names.
}

View File

@@ -1,6 +1,8 @@
import { Identify, identify, init, track } from '@amplitude/analytics-browser'
import { isProductionEnv } from 'utils/env'
const API_KEY = isProductionEnv() ? process.env.REACT_APP_AMPLITUDE_KEY : process.env.REACT_APP_AMPLITUDE_TEST_KEY
/**
* Initializes Amplitude with API key for project.
*
@@ -8,14 +10,11 @@ import { isProductionEnv } from 'utils/env'
* member of the organization on Amplitude to view details.
*/
export function initializeAnalytics() {
const API_KEY = isProductionEnv() ? process.env.REACT_APP_AMPLITUDE_KEY : process.env.REACT_APP_AMPLITUDE_TEST_KEY
if (typeof API_KEY === 'undefined') {
const keyName = isProductionEnv() ? 'REACT_APP_AMPLITUDE_KEY' : 'REACT_APP_AMPLITUDE_TEST_KEY'
console.error(`${keyName} is undefined, Amplitude analytics will not run.`)
return
}
init(
API_KEY,
/* userId= */ undefined, // User ID should be undefined to let Amplitude default to Device ID
@@ -23,7 +22,8 @@ export function initializeAnalytics() {
{
// Disable tracking of private user information by Amplitude
trackingOptions: {
ipAddress: false,
// IP is being dropped before ingestion on Amplitude side, only being used to determine country.
ipAddress: isProductionEnv() ? false : true,
carrier: false,
city: false,
region: false,
@@ -33,23 +33,16 @@ export function initializeAnalytics() {
)
}
/** Sends an approved (finalized) event to Amplitude production project. */
/** Sends an event to Amplitude. */
export function sendAnalyticsEvent(eventName: string, eventProperties?: Record<string, unknown>) {
if (!isProductionEnv()) {
console.log(`[amplitude(${eventName})]: ${JSON.stringify(eventProperties)}`)
if (!API_KEY) {
console.log(`[analytics(${eventName})]: ${JSON.stringify(eventProperties)}`)
return
}
track(eventName, eventProperties)
}
/** Sends a draft event to Amplitude test project. */
export function sendTestAnalyticsEvent(eventName: string, eventProperties?: Record<string, unknown>) {
if (isProductionEnv()) return
track(eventName, eventProperties)
}
type Value = string | number | boolean | string[] | number[]
/**

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -1,3 +1,186 @@
<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 xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20">
<g filter="url(#a)">
<path fill="url(#b)" d="M17.654 4.868c0-.69-.56-1.25-1.25-1.25h-2.932c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.741 1.873c-.898.999-1.115 2.687-.058 3.748 1.013 1.017 2.918.988 3.87 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.158v-7.55Z"/>
<path fill="url(#c)" d="M17.654 4.868c0-.69-.56-1.25-1.25-1.25h-2.932c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.741 1.873c-.898.999-1.115 2.687-.058 3.748 1.013 1.017 2.918.988 3.87 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.158v-7.55Z"/>
<path fill="url(#d)" d="M17.654 4.868c0-.69-.56-1.25-1.25-1.25h-2.932c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.741 1.873c-.898.999-1.115 2.687-.058 3.748 1.013 1.017 2.918.988 3.87 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.158v-7.55Z"/>
<path fill="url(#e)" d="M17.654 4.868c0-.69-.56-1.25-1.25-1.25h-2.932c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.741 1.873c-.898.999-1.115 2.687-.058 3.748 1.013 1.017 2.918.988 3.87 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.158v-7.55Z"/>
<path fill="url(#f)" d="M17.654 4.868c0-.69-.56-1.25-1.25-1.25h-2.932c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.741 1.873c-.898.999-1.115 2.687-.058 3.748 1.013 1.017 2.918.988 3.87 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.158v-7.55Z"/>
</g>
<g filter="url(#g)">
<path fill="url(#h)" d="M10.954 2.602c0-.69-.56-1.25-1.25-1.25H6.773c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.742 1.873c-.898 1-1.115 2.687-.058 3.748 1.014 1.018 2.919.988 3.871 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.157V2.602Z"/>
<path fill="url(#i)" d="M10.954 2.602c0-.69-.56-1.25-1.25-1.25H6.773c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.742 1.873c-.898 1-1.115 2.687-.058 3.748 1.014 1.018 2.919.988 3.871 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.157V2.602Z"/>
<path fill="url(#j)" d="M10.954 2.602c0-.69-.56-1.25-1.25-1.25H6.773c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.742 1.873c-.898 1-1.115 2.687-.058 3.748 1.014 1.018 2.919.988 3.871 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.157V2.602Z"/>
<path fill="url(#k)" d="M10.954 2.602c0-.69-.56-1.25-1.25-1.25H6.773c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.742 1.873c-.898 1-1.115 2.687-.058 3.748 1.014 1.018 2.919.988 3.871 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.157V2.602Z"/>
<path fill="url(#l)" d="M10.954 2.602c0-.69-.56-1.25-1.25-1.25H6.773c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.742 1.873c-.898 1-1.115 2.687-.058 3.748 1.014 1.018 2.919.988 3.871 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.157V2.602Z"/>
</g>
<path fill="url(#m)" d="M17.654 5.776h-5.431v.673h5.431v-.673Z"/>
<path fill="url(#n)" d="M17.654 5.776h-5.431v.673h5.431v-.673Z"/>
<path fill="url(#o)" d="M10.955 3.51H5.523v.674h5.432V3.51Z"/>
<path fill="url(#p)" d="M10.955 3.51H5.523v.674h5.432V3.51Z"/>
<path fill="url(#q)" d="M17.606 4.523c.031.11.048.225.048.345v.328h-5.431v-.328c0-.12.016-.236.048-.345h5.335Z"/>
<path fill="url(#r)" d="M17.606 4.523c.031.11.048.225.048.345v.328h-5.431v-.328c0-.12.016-.236.048-.345h5.335Z"/>
<path fill="url(#s)" d="M10.907 2.257c.031.11.048.225.048.345v.329H5.523v-.329c0-.12.017-.235.049-.345h5.335Z"/>
<path fill="url(#t)" d="M10.907 2.257c.031.11.048.225.048.345v.329H5.523v-.329c0-.12.017-.235.049-.345h5.335Z"/>
<g filter="url(#u)">
<path fill="url(#v)" d="M17.654 12.236h-3.116a.173.173 0 0 0-.176.17c0 1.255.904 2.3 2.096 2.518l.332-.349a3.125 3.125 0 0 0 .864-2.157v-.182Z"/>
</g>
<g filter="url(#w)">
<path fill="url(#x)" d="M10.955 9.97H7.839a.173.173 0 0 0-.176.17c0 1.255.903 2.3 2.096 2.518l.332-.349a3.125 3.125 0 0 0 .864-2.156V9.97Z"/>
</g>
<g filter="url(#y)">
<path fill="url(#z)" d="M13.243 18.21v-.05a3.424 3.424 0 0 0-3.856-3.397c-.511.986-.496 2.268.366 3.133.892.895 2.474.98 3.49.314Z"/>
</g>
<g filter="url(#A)">
<path fill="url(#B)" d="M6.544 15.944v-.05a3.424 3.424 0 0 0-3.857-3.396c-.51.985-.495 2.267.366 3.132.892.896 2.475.98 3.49.314Z"/>
</g>
<defs>
<linearGradient id="b" x1="17.248" x2="13.193" y1="4.4" y2="16.827" gradientUnits="userSpaceOnUse">
<stop stop-color="#FDF2FF"/>
<stop offset="1" stop-color="#FFECFB"/>
</linearGradient>
<linearGradient id="c" x1="17.654" x2="15.615" y1="11.854" y2="11.854" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFCCF1"/>
<stop offset="1" stop-color="#FFC5EF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="f" x1="14.99" x2="13.818" y1="16.932" y2="15.721" gradientUnits="userSpaceOnUse">
<stop offset=".209" stop-color="#EDB0DF"/>
<stop offset="1" stop-color="#ECAAED" stop-opacity="0"/>
</linearGradient>
<linearGradient id="h" x1="10.549" x2="6.493" y1="2.134" y2="14.562" gradientUnits="userSpaceOnUse">
<stop stop-color="#FDF2FF"/>
<stop offset="1" stop-color="#FFECFB"/>
</linearGradient>
<linearGradient id="i" x1="10.954" x2="8.915" y1="9.588" y2="9.588" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFCCF1"/>
<stop offset="1" stop-color="#FFC5EF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="l" x1="8.29" x2="7.119" y1="14.666" y2="13.456" gradientUnits="userSpaceOnUse">
<stop offset=".209" stop-color="#EDB0DF"/>
<stop offset="1" stop-color="#ECAAED" stop-opacity="0"/>
</linearGradient>
<linearGradient id="m" x1="12.773" x2="16.915" y1="6.449" y2="6.449" gradientUnits="userSpaceOnUse">
<stop stop-color="#E95FDB"/>
<stop offset="1" stop-color="#FF46CB"/>
</linearGradient>
<linearGradient id="n" x1="12.223" x2="12.793" y1="6.449" y2="6.449" gradientUnits="userSpaceOnUse">
<stop stop-color="#9F4977"/>
<stop offset="1" stop-color="#CA5284" stop-opacity="0"/>
</linearGradient>
<linearGradient id="o" x1="6.074" x2="10.216" y1="4.184" y2="4.184" gradientUnits="userSpaceOnUse">
<stop stop-color="#E95FDB"/>
<stop offset="1" stop-color="#FF46CB"/>
</linearGradient>
<linearGradient id="p" x1="5.523" x2="6.094" y1="4.184" y2="4.184" gradientUnits="userSpaceOnUse">
<stop stop-color="#9F4977"/>
<stop offset="1" stop-color="#CA5284" stop-opacity="0"/>
</linearGradient>
<linearGradient id="q" x1="12.773" x2="16.915" y1="5.196" y2="5.196" gradientUnits="userSpaceOnUse">
<stop stop-color="#E95FDB"/>
<stop offset="1" stop-color="#FF46CB"/>
</linearGradient>
<linearGradient id="r" x1="12.223" x2="12.793" y1="5.196" y2="5.196" gradientUnits="userSpaceOnUse">
<stop stop-color="#9F4977"/>
<stop offset="1" stop-color="#CA5284" stop-opacity="0"/>
</linearGradient>
<linearGradient id="s" x1="6.074" x2="10.216" y1="2.931" y2="2.931" gradientUnits="userSpaceOnUse">
<stop stop-color="#E95FDB"/>
<stop offset="1" stop-color="#FF46CB"/>
</linearGradient>
<linearGradient id="t" x1="5.523" x2="6.094" y1="2.931" y2="2.931" gradientUnits="userSpaceOnUse">
<stop stop-color="#9F4977"/>
<stop offset="1" stop-color="#CA5284" stop-opacity="0"/>
</linearGradient>
<radialGradient id="d" cx="0" cy="0" r="1" gradientTransform="matrix(2.8125 0 0 6.01563 12.02 8.026)" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFD1F5"/>
<stop offset="1" stop-color="#FECAFF" stop-opacity="0"/>
</radialGradient>
<radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="matrix(1.54348 -1.53472 2.30642 2.31958 11.806 16.12)" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFC0FC"/>
<stop offset="1" stop-color="#FFBCFC" stop-opacity="0"/>
</radialGradient>
<radialGradient id="j" cx="0" cy="0" r="1" gradientTransform="matrix(2.8125 0 0 6.01563 5.322 5.76)" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFD1F5"/>
<stop offset="1" stop-color="#FECAFF" stop-opacity="0"/>
</radialGradient>
<radialGradient id="k" cx="0" cy="0" r="1" gradientTransform="matrix(1.54347 -1.53473 2.30643 2.31957 5.107 13.854)" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFC0FC"/>
<stop offset="1" stop-color="#FFBCFC" stop-opacity="0"/>
</radialGradient>
<radialGradient id="v" cx="0" cy="0" r="1" gradientTransform="matrix(-.42911 2.50572 -2.20218 -.37713 16.437 12.236)" gradientUnits="userSpaceOnUse">
<stop offset=".147" stop-color="#FF52CF"/>
<stop offset="1" stop-color="#FB3FFF"/>
</radialGradient>
<radialGradient id="x" cx="0" cy="0" r="1" gradientTransform="matrix(-.42911 2.50572 -2.20218 -.37713 9.738 9.97)" gradientUnits="userSpaceOnUse">
<stop offset=".147" stop-color="#FF52CF"/>
<stop offset="1" stop-color="#FB3FFF"/>
</radialGradient>
<radialGradient id="z" cx="0" cy="0" r="1" gradientTransform="rotate(167.471 5.464 9.437) scale(4.54024 4.83245)" gradientUnits="userSpaceOnUse">
<stop offset=".268" stop-color="#FF4EE3"/>
<stop offset=".92" stop-color="#D12396"/>
</radialGradient>
<radialGradient id="B" cx="0" cy="0" r="1" gradientTransform="rotate(167.471 2.239 7.937) scale(4.54024 4.83245)" gradientUnits="userSpaceOnUse">
<stop offset=".268" stop-color="#FF4EE3"/>
<stop offset=".92" stop-color="#D12396"/>
</radialGradient>
<filter id="a" width="8.808" height="15.23" x="9.045" y="3.418" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dx=".2" dy="-.2"/>
<feGaussianBlur stdDeviation=".3"/>
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
<feColorMatrix values="0 0 0 0 0.522043 0 0 0 0 0.119948 0 0 0 0 0.5875 0 0 0 1 0"/>
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
</filter>
<filter id="g" width="8.808" height="15.23" x="2.346" y="1.152" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dx=".2" dy="-.2"/>
<feGaussianBlur stdDeviation=".3"/>
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
<feColorMatrix values="0 0 0 0 0.545098 0 0 0 0 0.219608 0 0 0 0 0.512549 0 0 0 1 0"/>
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
</filter>
<filter id="u" width="3.541" height="2.688" x="14.112" y="12.236" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dx="-.25"/>
<feGaussianBlur stdDeviation=".25"/>
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
<feColorMatrix values="0 0 0 0 0.976471 0 0 0 0 0.145098 0 0 0 0 0.743686 0 0 0 1 0"/>
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
</filter>
<filter id="w" width="3.541" height="2.688" x="7.413" y="9.97" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dx="-.25"/>
<feGaussianBlur stdDeviation=".25"/>
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
<feColorMatrix values="0 0 0 0 0.976471 0 0 0 0 0.145098 0 0 0 0 0.743686 0 0 0 1 0"/>
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
</filter>
<filter id="y" width="4.448" height="4.112" x="9.045" y="14.536" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dx=".25" dy="-.2"/>
<feGaussianBlur stdDeviation=".25"/>
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
<feColorMatrix values="0 0 0 0 0.906118 0 0 0 0 0.329412 0 0 0 0 1 0 0 0 1 0"/>
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
</filter>
<filter id="A" width="4.448" height="4.112" x="2.346" y="12.27" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dx=".25" dy="-.2"/>
<feGaussianBlur stdDeviation=".25"/>
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
<feColorMatrix values="0 0 0 0 0.906118 0 0 0 0 0.329412 0 0 0 0 1 0 0 0 1 0"/>
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 871 B

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -145,7 +145,7 @@ const CloseIcon = styled.div`
top: 14px;
&:hover {
cursor: pointer;
opacity: 0.6;
opacity: ${({ theme }) => theme.opacity.hover};
}
`

View File

@@ -1,49 +1,47 @@
import { curveCardinalOpen, scaleLinear } from 'd3'
import { curveCardinal, scaleLinear } from 'd3'
import { filterPrices } from 'graphql/data/Token'
import { TopToken } from 'graphql/data/TopTokens'
import { TimePeriod } from 'graphql/data/util'
import React from 'react'
import { useTheme } from 'styled-components/macro'
import data from './data.json'
import { DATA_EMPTY, getPriceBounds } from '../Tokens/TokenDetails/PriceChart'
import LineChart from './LineChart'
type PricePoint = { value: number; timestamp: number }
function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
const prices = pricePoints.map((x) => x.value)
const min = Math.min(...prices)
const max = Math.max(...prices)
return [min, max]
}
interface SparklineChartProps {
width: number
height: number
tokenData: TopToken
pricePercentChange: number | undefined | null
timePeriod: TimePeriod
}
function SparklineChart({ width, height }: SparklineChartProps) {
function SparklineChart({ width, height, tokenData, pricePercentChange, timePeriod }: SparklineChartProps) {
const theme = useTheme()
// for sparkline
const pricePoints = filterPrices(tokenData?.market?.priceHistory) ?? []
const hasData = pricePoints.length !== 0
const startingPrice = hasData ? pricePoints[0] : DATA_EMPTY
const endingPrice = hasData ? pricePoints[pricePoints.length - 1] : DATA_EMPTY
const widthScale = scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, 124])
const rdScale = scaleLinear().domain(getPriceBounds(pricePoints)).range([42, 0])
/* TODO: Implement API calls & cache to use here */
const pricePoints = data.day
const startingPrice = pricePoints[0]
const endingPrice = pricePoints[pricePoints.length - 1]
const timeScale = scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, width])
const rdScale = scaleLinear().domain(getPriceBounds(pricePoints)).range([height, 0])
const isPositive = endingPrice.value >= startingPrice.value
/* Default curve doesn't look good for the ALL chart */
const curveTension = timePeriod === TimePeriod.ALL ? 0.75 : 0.9
return (
<LineChart
data={pricePoints}
getX={(p: PricePoint) => timeScale(p.timestamp)}
getX={(p: PricePoint) => widthScale(p.timestamp)}
getY={(p: PricePoint) => rdScale(p.value)}
curve={curveCardinalOpen.tension(0.9)}
marginTop={0}
color={isPositive ? theme.accentSuccess : theme.accentFailure}
curve={curveCardinal.tension(curveTension)}
color={pricePercentChange && pricePercentChange < 0 ? theme.accentFailure : theme.accentSuccess}
strokeWidth={1.5}
width={width}
height={height}
></LineChart>
/>
)
}

View File

@@ -2,8 +2,8 @@ import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { AutoColumn } from 'components/Column'
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
import { isSupportedChain } from 'constants/chains'

View File

@@ -2,8 +2,8 @@ import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { AutoColumn } from 'components/Column'
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
import { isSupportedChain } from 'constants/chains'

View File

@@ -1,10 +1,11 @@
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import { TokensVariant, useTokensFlag } from 'featureFlags/flags/tokens'
import { TokenSafetyVariant, useTokenSafetyFlag } from 'featureFlags/flags/tokenSafety'
import { TokensNetworkFilterVariant, useTokensNetworkFilterFlag } from 'featureFlags/flags/tokensNetworkFilter'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
import { X } from 'react-feather'
@@ -225,12 +226,6 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.tokens}
label="Tokens"
/>
<FeatureFlagOption
variant={TokensNetworkFilterVariant}
value={useTokensNetworkFilterFlag()}
featureFlag={FeatureFlag.tokensNetworkFilter}
label="Tokens Network Filter"
/>
<FeatureFlagOption
variant={TokenSafetyVariant}
value={useTokenSafetyFlag()}
@@ -238,9 +233,25 @@ export default function FeatureFlagModal() {
label="Token Safety"
/>
</FeatureFlagGroup>
<FeatureFlagGroup name="Phase 0 Follow-ups">
<FeatureFlagOption
variant={FavoriteTokensVariant}
value={useFavoriteTokensFlag()}
featureFlag={FeatureFlag.favoriteTokens}
label="Favorite Tokens"
/>
</FeatureFlagGroup>
<FeatureFlagGroup name="Phase 1">
<FeatureFlagOption variant={NftVariant} value={useNftFlag()} featureFlag={FeatureFlag.nft} label="NFTs" />
</FeatureFlagGroup>
<FeatureFlagGroup name="Debug">
<FeatureFlagOption
variant={TraceJsonRpcVariant}
value={useTraceJsonRpcFlag()}
featureFlag={FeatureFlag.traceJsonRpc}
label="Enables JSON-RPC tracing"
/>
</FeatureFlagGroup>
<SaveButton onClick={() => window.location.reload()}>Reload</SaveButton>
</Modal>
)

View File

@@ -1,32 +0,0 @@
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}ms 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}ms color ${timing.in}`};
}
`

View File

@@ -3,7 +3,6 @@ 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 WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
@@ -29,20 +28,18 @@ const IconWrapper = styled.div<{ size?: number }>`
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;
bottom: -4px;
right: -4px;
`
const SockImg = styled.img`
width: 7.5px;
height: 10px;
margin-top: 3px;
width: 16px;
height: 16px;
`
const Socks = () => {

View File

@@ -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 }>`

View File

@@ -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>

View File

@@ -4,6 +4,7 @@ import React from 'react'
import { animated, useSpring, useTransition } from 'react-spring'
import { useGesture } from 'react-use-gesture'
import styled, { css } from 'styled-components/macro'
import { Z_INDEX } from 'theme/zIndex'
import { isMobile } from '../../utils/userAgent'
@@ -11,7 +12,7 @@ const AnimatedDialogOverlay = animated(DialogOverlay)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ redesignFlag?: boolean }>`
&[data-reach-dialog-overlay] {
z-index: 2;
z-index: ${Z_INDEX.modalBackdrop};
background-color: transparent;
overflow: hidden;

View File

@@ -0,0 +1,23 @@
import { style } from '@vanilla-extract/css'
import { lightGrayOverlayOnHover } from 'nft/css/common.css'
import { sprinkles } from '../../nft/css/sprinkles.css'
export const ChainSelector = style([
lightGrayOverlayOnHover,
sprinkles({
borderRadius: '8',
height: '40',
cursor: 'pointer',
border: 'none',
color: 'blackBlue',
background: 'none',
}),
])
export const Image = style([
sprinkles({
width: '20',
height: '20',
}),
])

View File

@@ -1,5 +1,4 @@
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'
@@ -8,46 +7,18 @@ import useSyncChainQuery from 'hooks/useSyncChainQuery'
import { Box } from 'nft/components/Box'
import { Portal } from 'nft/components/common/Portal'
import { Column, Row } from 'nft/components/Flex'
import { CheckMarkIcon, TokenWarningRedIcon } from 'nft/components/icons'
import { TokenWarningRedIcon } from 'nft/components/icons'
import { subhead } from 'nft/css/common.css'
import { themeVars, vars } from 'nft/css/sprinkles.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { useIsMobile } from 'nft/hooks'
import { ReactNode, useReducer, useRef } from 'react'
import { useCallback, useRef, useState } from 'react'
import { ChevronDown, ChevronUp } from 'react-feather'
import { useTheme } from 'styled-components/macro'
import * as styles from './ChainSwitcher.css'
import * as styles from './ChainSelector.css'
import ChainSelectorRow from './ChainSelectorRow'
import { NavDropdown } from './NavDropdown'
const ChainRow = ({
targetChain,
onSelectChain,
}: {
targetChain: SupportedChainId
onSelectChain: (targetChain: number) => void
}) => {
const { chainId } = useWeb3React()
const active = chainId === targetChain
const { label, logoUrl } = getChainInfo(targetChain)
return (
<Column borderRadius="12">
<Row
as="button"
background="none"
className={`${styles.ChainSwitcherRow} ${subhead}`}
onClick={() => onSelectChain(targetChain)}
>
<ChainDetails>
<img src={logoUrl} alt={label} className={styles.Icon} />
{label}
</ChainDetails>
{active && <CheckMarkIcon width={20} height={20} color={vars.color.blue400} />}
</Row>
</Column>
)
}
const ChainDetails = ({ children }: { children: ReactNode }) => <Row>{children}</Row>
const NETWORK_SELECTOR_CHAINS = [
SupportedChainId.MAINNET,
SupportedChainId.POLYGON,
@@ -56,24 +27,38 @@ const NETWORK_SELECTOR_CHAINS = [
SupportedChainId.CELO,
]
interface ChainSwitcherProps {
interface ChainSelectorProps {
leftAlign?: boolean
}
export const ChainSwitcher = ({ leftAlign }: ChainSwitcherProps) => {
export const ChainSelector = ({ leftAlign }: ChainSelectorProps) => {
const { chainId } = useWeb3React()
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
const [isOpen, setIsOpen] = useState<boolean>(false)
const isMobile = useIsMobile()
const theme = useTheme()
const ref = useRef<HTMLDivElement>(null)
const modalRef = useRef<HTMLDivElement>(null)
useOnClickOutside(ref, isOpen ? toggleOpen : undefined, [modalRef])
useOnClickOutside(ref, () => setIsOpen(false), [modalRef])
const info = chainId ? getChainInfo(chainId) : undefined
const selectChain = useSelectChain()
useSyncChainQuery()
const [pendingChainId, setPendingChainId] = useState<SupportedChainId | undefined>(undefined)
const onSelectChain = useCallback(
async (targetChainId: SupportedChainId) => {
setPendingChainId(targetChainId)
await selectChain(targetChainId)
setPendingChainId(undefined)
setIsOpen(false)
},
[selectChain, setIsOpen]
)
if (!chainId) {
return null
}
@@ -82,29 +67,33 @@ export const ChainSwitcher = ({ leftAlign }: ChainSwitcherProps) => {
const dropdown = (
<NavDropdown top="56" left={leftAlign ? '0' : 'auto'} right={leftAlign ? 'auto' : '0'} ref={modalRef}>
<Column marginX="8">
<Column paddingX="8">
{NETWORK_SELECTOR_CHAINS.map((chainId: SupportedChainId) => (
<ChainRow
onSelectChain={async (targetChainId: SupportedChainId) => {
await selectChain(targetChainId)
toggleOpen()
}}
<ChainSelectorRow
onSelectChain={onSelectChain}
targetChain={chainId}
key={chainId}
isPending={chainId === pendingChainId}
/>
))}
</Column>
</NavDropdown>
)
const chevronProps = {
height: 20,
width: 20,
color: theme.textSecondary,
}
return (
<Box position="relative" ref={ref}>
<Row
as="button"
gap="8"
className={styles.ChainSwitcher}
className={styles.ChainSelector}
background={isOpen ? 'accentActiveSoft' : 'none'}
onClick={toggleOpen}
onClick={() => setIsOpen(!isOpen)}
>
{!isSupported ? (
<>
@@ -121,7 +110,7 @@ export const ChainSwitcher = ({ leftAlign }: ChainSwitcherProps) => {
</Box>
</>
)}
{isOpen ? <StyledChevronUp /> : <StyledChevronDown />}
{isOpen ? <ChevronUp {...chevronProps} /> : <ChevronDown {...chevronProps} />}
</Row>
{isOpen && (isMobile ? <Portal>{dropdown}</Portal> : <>{dropdown}</>)}
</Box>

View File

@@ -0,0 +1,89 @@
import { useWeb3React } from '@web3-react/core'
import Loader from 'components/Loader'
import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { CheckMarkIcon } from 'nft/components/icons'
import styled, { useTheme } from 'styled-components/macro'
const LOGO_SIZE = 20
const Container = styled.button`
display: grid;
background: none;
grid-template-columns: min-content 1fr min-content;
align-items: center;
text-align: left;
line-height: 24px;
border: none;
justify-content: space-between;
padding: 10px 8px;
cursor: pointer;
border-radius: 12px;
color: ${({ theme }) => theme.textPrimary};
width: 240px;
transition: ${({ theme }) => theme.transition.duration.medium} ${({ theme }) => theme.transition.timing.ease}
background-color;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
width: 100%;
}
&:hover {
background-color: ${({ theme }) => theme.backgroundOutline};
}
`
const Label = styled.div`
grid-column: 2;
grid-row: 1;
font-size: 16px;
`
const Status = styled.div`
grid-column: 3;
grid-row: 1;
display: flex;
align-items: center;
width: ${LOGO_SIZE}px;
`
const ApproveText = styled.div`
color: ${({ theme }) => theme.textSecondary};
font-size: 12px;
grid-column: 2;
grid-row: 2;
`
const Logo = styled.img`
height: ${LOGO_SIZE}px;
width: ${LOGO_SIZE}px;
margin-right: 12px;
`
export default function ChainSelectorRow({
targetChain,
onSelectChain,
isPending,
}: {
targetChain: SupportedChainId
onSelectChain: (targetChain: number) => void
isPending: boolean
}) {
const { chainId } = useWeb3React()
const active = chainId === targetChain
const { label, logoUrl } = getChainInfo(targetChain)
const theme = useTheme()
return (
<Container onClick={() => onSelectChain(targetChain)}>
<Logo src={logoUrl} alt={label} />
<Label>{label}</Label>
{isPending && <ApproveText>Approve in wallet</ApproveText>}
<Status>
{active && <CheckMarkIcon width={LOGO_SIZE} height={LOGO_SIZE} color={theme.accentActive} />}
{isPending && <Loader width={LOGO_SIZE} height={LOGO_SIZE} />}
</Status>
</Container>
)
}

View File

@@ -1,53 +0,0 @@
import { style } from '@vanilla-extract/css'
import { lightGrayOverlayOnHover } from 'nft/css/common.css'
import { breakpoints, sprinkles } from '../../nft/css/sprinkles.css'
export const ChainSwitcher = style([
lightGrayOverlayOnHover,
sprinkles({
borderRadius: '8',
paddingY: '8',
paddingX: '12',
cursor: 'pointer',
border: 'none',
color: 'blackBlue',
background: 'none',
}),
])
export const ChainSwitcherRow = style([
lightGrayOverlayOnHover,
sprinkles({
border: 'none',
justifyContent: 'space-between',
paddingX: '8',
paddingY: '8',
cursor: 'pointer',
color: 'blackBlue',
borderRadius: '12',
width: { sm: 'full' },
}),
{
lineHeight: '24px',
'@media': {
[`screen and (min-width: ${breakpoints.sm}px)`]: {
width: '204px',
},
},
},
])
export const Image = style([
sprinkles({
width: '20',
height: '20',
}),
])
export const Icon = style([
Image,
sprinkles({
marginRight: '12',
}),
])

View File

@@ -41,6 +41,7 @@ export const SecondaryText = style([
paddingY: '8',
paddingX: '8',
color: 'darkGray',
width: 'full',
}),
{
lineHeight: '20px',

View File

@@ -126,7 +126,7 @@ export const MenuDropdown = () => {
<>
<Box position="relative" ref={ref}>
<NavIcon isActive={isOpen} onClick={toggleOpen}>
<EllipsisIcon />
<EllipsisIcon width={20} height={20} />
</NavIcon>
{isOpen && (
@@ -134,7 +134,7 @@ export const MenuDropdown = () => {
<Column gap="16">
<Column paddingX="8" gap="4">
{nftFlag === NftVariant.Enabled && (
<PrimaryMenuRow to="/nft/sell" close={toggleOpen}>
<PrimaryMenuRow to="/nfts/sell" close={toggleOpen}>
<Icon>
<ThinTagIcon width={24} height={24} />
</Icon>

View File

@@ -11,7 +11,7 @@ export const navIcon = style([
justifyContent: 'center',
textAlign: 'center',
cursor: 'pointer',
padding: '8',
padding: '10',
borderRadius: '8',
transition: '250',
}),

View File

@@ -17,6 +17,7 @@ export const NavIcon = ({ children, isActive, onClick }: NavIconProps) => {
background={isActive ? 'accentActiveSoft' : 'none'}
color={isActive ? 'blackBlue' : 'darkGray'}
onClick={onClick}
height="40"
>
{children}
</Box>

View File

@@ -4,6 +4,7 @@ import { buttonTextSmall, subhead, subheadSmall } from 'nft/css/common.css'
import { breakpoints, sprinkles, vars } from '../../nft/css/sprinkles.css'
const DESKTOP_NAVBAR_WIDTH = 360
const MAGNIFYING_GLASS_ICON_WIDTH = 28
const baseSearchStyle = style([
sprinkles({
@@ -31,11 +32,9 @@ export const searchBarContainer = style([
}),
{
'@media': {
[`screen and (min-width: ${breakpoints.sm}px)`]: {
top: '-24px',
},
[`screen and (min-width: ${breakpoints.lg}px)`]: {
right: `-${DESKTOP_NAVBAR_WIDTH / 2}px`,
right: `-${DESKTOP_NAVBAR_WIDTH / 2 - MAGNIFYING_GLASS_ICON_WIDTH}px`,
top: '-5px',
},
},
},
@@ -44,7 +43,6 @@ export const searchBarContainer = style([
export const searchBar = style([
baseSearchStyle,
sprinkles({
height: 'full',
color: 'placeholder',
paddingX: '16',
cursor: 'pointer',
@@ -61,7 +59,9 @@ export const searchBarInput = style([
border: 'none',
background: 'none',
}),
{ lineHeight: '24px' },
{
lineHeight: '24px',
},
])
export const searchBarDropdown = style([
@@ -70,6 +70,7 @@ export const searchBarDropdown = style([
borderBottomLeftRadius: '12',
borderBottomRightRadius: '12',
background: 'lightGray',
height: { sm: 'viewHeight', md: 'auto' },
}),
{
borderTop: 'none',
@@ -84,7 +85,6 @@ export const suggestionRow = style([
justifyContent: 'space-between',
paddingY: '8',
paddingX: '16',
transition: '250',
}),
{
':hover': {
@@ -167,3 +167,50 @@ export const notFoundContainer = style([
paddingLeft: '16',
}),
])
const visibilityTransition = `visibility ${vars.time[125]}, opacity ${vars.time[125]}`
const delayedTransitionProperties = `padding 0s ${vars.time[125]}, height 0s ${vars.time[125]}`
export const hidden = style([
sprinkles({
visibility: 'hidden',
opacity: '0',
padding: '0',
height: '0',
}),
{
transition: `${visibilityTransition}, ${delayedTransitionProperties}`,
transitionTimingFunction: 'ease-in',
},
])
export const visible = style([
sprinkles({
visibility: 'visible',
opacity: '1',
height: 'full',
}),
{
transition: `${visibilityTransition}`,
transitionTimingFunction: 'ease-out',
},
])
export const searchContentCentered = style({
'@media': {
[`screen and (min-width: ${breakpoints.lg}px)`]: {
transform: `translateX(${DESKTOP_NAVBAR_WIDTH / 4}px)`,
transition: `transform ${vars.time[125]}`,
transitionTimingFunction: 'ease-out',
},
},
})
export const searchContentLeftAlign = style({
'@media': {
[`screen and (min-width: ${breakpoints.lg}px)`]: {
transform: 'translateX(0)',
transition: `transform ${vars.time[125]}`,
transitionTimingFunction: 'ease-in',
},
},
})

View File

@@ -7,15 +7,14 @@ import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { organizeSearchResults } from 'lib/utils/searchBar'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { Overlay } from 'nft/components/modals/Overlay'
import { magicalGradientOnHover, subheadSmall } from 'nft/css/common.css'
import { useIsMobile, useSearchHistory } from 'nft/hooks'
import { useIsMobile, useIsTablet, useSearchHistory } from 'nft/hooks'
import { fetchSearchCollections, fetchTrendingCollections } from 'nft/queries'
import { fetchSearchTokens } from 'nft/queries/genie/SearchTokensFetcher'
import { fetchTrendingTokens } from 'nft/queries/genie/TrendingTokensFetcher'
import { FungibleToken, GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
import { formatEthPrice } from 'nft/utils/currency'
import { ChangeEvent, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { ChangeEvent, ReactNode, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { useQuery } from 'react-query'
import { useLocation } from 'react-router-dom'
@@ -38,6 +37,7 @@ interface SearchBarDropdownSectionProps {
hoveredIndex: number | undefined
startingIndex: number
setHoveredIndex: (index: number | undefined) => void
isLoading?: boolean
}
export const SearchBarDropdownSection = ({
@@ -48,6 +48,7 @@ export const SearchBarDropdownSection = ({
hoveredIndex,
startingIndex,
setHoveredIndex,
isLoading,
}: SearchBarDropdownSectionProps) => {
return (
<Column gap="12">
@@ -56,8 +57,10 @@ export const SearchBarDropdownSection = ({
<Box>{header}</Box>
</Row>
<Column gap="12">
{suggestions?.map((suggestion, index) =>
isCollection(suggestion) ? (
{suggestions.map((suggestion, index) =>
isLoading ? (
<SkeletonRow key={index} />
) : isCollection(suggestion) ? (
<CollectionRow
key={suggestion.address}
collection={suggestion as GenieCollection}
@@ -87,17 +90,18 @@ interface SearchBarDropdownProps {
tokens: FungibleToken[]
collections: GenieCollection[]
hasInput: boolean
isLoading: boolean
}
export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }: SearchBarDropdownProps) => {
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 searchHistory = useSearchHistory((state: { history: (FungibleToken | GenieCollection)[] }) => state.history)
const shortenedHistory = useMemo(() => searchHistory.slice(0, 2), [searchHistory])
const { pathname } = useLocation()
const isNFTPage = pathname.includes('/nfts')
const isTokenPage = pathname.includes('/tokens')
const phase1Flag = useNftFlag()
const [resultsState, setResultsState] = useState<ReactNode>()
const tokenSearchResults =
tokens.length > 0 ? (
@@ -131,50 +135,56 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
)
) : null
const { data: trendingCollectionResults } = useQuery(['trendingCollections', 'eth', 'twenty_four_hours'], () =>
fetchTrendingCollections({ volumeType: 'eth', timePeriod: 'ONE_DAY' as TimePeriod, size: 3 })
const { data: trendingCollectionResults, isLoading: trendingCollectionsAreLoading } = useQuery(
['trendingCollections', 'eth', 'twenty_four_hours'],
() => fetchTrendingCollections({ volumeType: 'eth', timePeriod: 'ONE_DAY' as TimePeriod, size: 3 })
)
const trendingCollections = useMemo(() => {
return trendingCollectionResults
?.map((collection) => {
return {
...collection,
collectionAddress: collection.address,
floorPrice: formatEthPrice(collection.floor?.toString()),
stats: {
total_supply: collection.totalSupply,
one_day_change: collection.floorChange,
},
}
})
.slice(0, isNFTPage ? 3 : 2)
}, [isNFTPage, trendingCollectionResults])
const showTrendingCollections: boolean = useMemo(
() => (trendingCollections?.length ?? 0) > 0 && !isTokenPage && phase1Flag === NftVariant.Enabled,
[trendingCollections?.length, isTokenPage, phase1Flag]
const trendingCollections = useMemo(
() =>
trendingCollectionResults
? trendingCollectionResults
.map((collection) => ({
...collection,
collectionAddress: collection.address,
floorPrice: formatEthPrice(collection.floor?.toString()),
stats: {
total_supply: collection.totalSupply,
one_day_change: collection.floorChange,
},
}))
.slice(0, isNFTPage ? 3 : 2)
: [...Array<GenieCollection>(isNFTPage ? 3 : 2)],
[isNFTPage, trendingCollectionResults]
)
const { data: trendingTokenResults } = useQuery([], () => fetchTrendingTokens(4), {
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
})
const { data: trendingTokenResults, isLoading: trendingTokensAreLoading } = useQuery(
['trendingTokens'],
() => fetchTrendingTokens(4),
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
}
)
const trendingTokensLength = phase1Flag === NftVariant.Enabled ? (isTokenPage ? 3 : 2) : 4
const trendingTokens = useMemo(() => {
return trendingTokenResults?.slice(0, trendingTokensLength)
}, [trendingTokenResults, trendingTokensLength])
const trendingTokens = useMemo(
() =>
trendingTokenResults
? trendingTokenResults.slice(0, trendingTokensLength)
: [...Array<FungibleToken>(trendingTokensLength)],
[trendingTokenResults, trendingTokensLength]
)
const totalSuggestions = hasInput
? tokens.length + collections.length
: Math.min(searchHistory.length, 2) +
: Math.min(shortenedHistory.length, 2) +
(isNFTPage || !isTokenPage ? trendingCollections?.length ?? 0 : 0) +
(isTokenPage || !isNFTPage ? trendingTokens?.length ?? 0 : 0)
// Close the modal on escape
// Navigate search results via arrow keys
useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => {
if (event.key === 'ArrowUp') {
@@ -201,61 +211,90 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
}
}, [toggleOpen, hoveredIndex, totalSuggestions])
useEffect(() => {
if (!isLoading) {
const currentState = () =>
hasInput ? (
// Empty or Up to 8 combined tokens and nfts
<Column gap="20">
{isNFTPage ? (
<>
{collectionSearchResults}
{tokenSearchResults}
</>
) : (
<>
{tokenSearchResults}
{collectionSearchResults}
</>
)}
</Column>
) : (
// Recent Searches, Trending Tokens, Trending Collections
<Column gap="20">
{shortenedHistory.length > 0 && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={0}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={shortenedHistory}
header={<Trans>Recent searches</Trans>}
headerIcon={<ClockIcon />}
/>
)}
{!isNFTPage && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={shortenedHistory.length}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={trendingTokens}
header={<Trans>Popular tokens</Trans>}
headerIcon={<TrendingArrow />}
isLoading={trendingTokensAreLoading}
/>
)}
{!isTokenPage && phase1Flag === NftVariant.Enabled && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={shortenedHistory.length + (isNFTPage ? 0 : trendingTokens?.length ?? 0)}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={trendingCollections as unknown as GenieCollection[]}
header={<Trans>Popular NFT collections</Trans>}
headerIcon={<TrendingArrow />}
isLoading={trendingCollectionsAreLoading}
/>
)}
</Column>
)
setResultsState(currentState)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isLoading,
tokens,
collections,
trendingCollections,
trendingCollectionsAreLoading,
trendingTokens,
trendingTokensAreLoading,
hoveredIndex,
phase1Flag,
toggleOpen,
shortenedHistory,
hasInput,
isNFTPage,
isTokenPage,
])
return (
<Box className={styles.searchBarDropdown}>
{hasInput ? (
// Empty or Up to 8 combined tokens and nfts
<Column gap="20">
{isNFTPage ? (
<>
{collectionSearchResults}
{tokenSearchResults}
</>
) : (
<>
{tokenSearchResults}
{collectionSearchResults}
</>
)}
</Column>
) : (
// Recent Searches, Trending Tokens, Trending Collections
<Column gap="20">
{searchHistory.length > 0 && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={0}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={searchHistory}
header={<Trans>Recent searches</Trans>}
headerIcon={<ClockIcon />}
/>
)}
{(trendingTokens?.length ?? 0) > 0 && !isNFTPage && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={searchHistory.length}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={trendingTokens ?? []}
header={<Trans>Popular tokens</Trans>}
headerIcon={<TrendingArrow />}
/>
)}
{showTrendingCollections && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={searchHistory.length + (isNFTPage ? 0 : trendingTokens?.length ?? 0)}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={trendingCollections as unknown as GenieCollection[]}
header={<Trans>Popular NFT collections</Trans>}
headerIcon={<TrendingArrow />}
/>
)}
</Column>
)}
<Box opacity={isLoading ? '0.3' : '1'} transition="125">
{resultsState}
</Box>
</Box>
)
}
@@ -273,6 +312,7 @@ export const SearchBar = () => {
const { pathname } = useLocation()
const phase1Flag = useNftFlag()
const isMobile = useIsMobile()
const isTablet = useIsTablet()
useOnClickOutside(searchRef, () => {
isOpen && toggleOpen()
@@ -302,6 +342,7 @@ export const SearchBar = () => {
const [reducedTokens, reducedCollections] = organizeSearchResults(isNFTPage, tokens ?? [], collections ?? [])
// close dropdown on escape
useEffect(() => {
const escapeKeyDownHandler = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpen) {
@@ -330,64 +371,69 @@ export const SearchBar = () => {
}, [isOpen])
const placeholderText = phase1Flag === NftVariant.Enabled ? t`Search tokens and NFT collections` : t`Search tokens`
const isMobileOrTablet = isMobile || isTablet
const showCenteredSearchContent = !isOpen && phase1Flag !== NftVariant.Enabled && !isMobileOrTablet
return (
<Box position="relative">
<Box
position={isOpen ? { sm: 'fixed', md: 'absolute' } : 'static'}
position={{ sm: 'fixed', md: 'absolute' }}
width={{ sm: isOpen ? 'viewWidth' : 'auto', md: 'auto' }}
ref={searchRef}
className={styles.searchBarContainer}
display={{ sm: isOpen ? 'inline-block' : 'none', xl: 'inline-block' }}
>
<Row
className={clsx(`${styles.searchBar} ${!isOpen && magicalGradientOnHover}`)}
borderRadius={isOpen ? undefined : '12'}
className={clsx(
` ${styles.searchBar} ${!isOpen && !isMobile && magicalGradientOnHover} ${
isMobileOrTablet && (isOpen ? styles.visible : styles.hidden)
}`
)}
borderRadius={isOpen || isMobileOrTablet ? undefined : '12'}
borderTopRightRadius={isOpen && !isMobile ? '12' : undefined}
borderTopLeftRadius={isOpen && !isMobile ? '12' : undefined}
borderBottomWidth={isOpen ? '0px' : '1px'}
display={{ sm: isOpen ? 'flex' : 'none', xl: 'flex' }}
justifyContent={isOpen || phase1Flag === NftVariant.Enabled ? 'flex-start' : 'center'}
onFocus={() => !isOpen && toggleOpen()}
borderBottomWidth={isOpen || isMobileOrTablet ? '0px' : '1px'}
onClick={() => !isOpen && toggleOpen()}
gap="12"
>
<Box display={{ sm: 'none', md: 'flex' }}>
<MagnifyingGlassIcon />
</Box>
<Box display={{ sm: 'flex', md: 'none' }} color="placeholder" onClick={toggleOpen}>
<ChevronLeftIcon />
<Box className={showCenteredSearchContent ? styles.searchContentCentered : styles.searchContentLeftAlign}>
<Box display={{ sm: 'none', md: 'flex' }}>
<MagnifyingGlassIcon />
</Box>
<Box display={{ sm: 'flex', md: 'none' }} color="placeholder" onClick={toggleOpen}>
<ChevronLeftIcon />
</Box>
</Box>
<Box
as="input"
placeholder={placeholderText}
width={isOpen || phase1Flag === NftVariant.Enabled ? 'full' : '120'}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
!isOpen && toggleOpen()
setSearchValue(event.target.value)
}}
className={styles.searchBarInput}
className={`${styles.searchBarInput} ${
showCenteredSearchContent ? styles.searchContentCentered : styles.searchContentLeftAlign
}`}
value={searchValue}
ref={inputRef}
width={phase1Flag === NftVariant.Enabled || isOpen ? 'full' : '160'}
/>
</Row>
<Box display={{ sm: isOpen ? 'none' : 'flex', xl: 'none' }}>
<NavIcon onClick={toggleOpen}>
<NavMagnifyingGlassIcon width={28} height={28} />
</NavIcon>
</Box>
{isOpen &&
(debouncedSearchValue.length > 0 && (tokensAreLoading || collectionsAreLoading) ? (
<SkeletonRow />
) : (
<Box className={clsx(isOpen ? styles.visible : styles.hidden)}>
{isOpen && (
<SearchBarDropdown
toggleOpen={toggleOpen}
tokens={reducedTokens}
collections={reducedCollections}
hasInput={debouncedSearchValue.length > 0}
isLoading={tokensAreLoading || (collectionsAreLoading && phase1Flag === NftVariant.Enabled)}
/>
))}
)}
</Box>
</Box>
{isOpen && <Overlay />}
<NavIcon onClick={toggleOpen}>
<NavMagnifyingGlassIcon />
</NavIcon>
</Box>
)
}

View 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',
},
])

View 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>
)
}

View File

@@ -175,13 +175,21 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index
export const SkeletonRow = () => {
return (
<Box className={styles.searchBarDropdown}>
<Row className={styles.suggestionRow}>
<Row>
<Box className={styles.imageHolder} />
<Box borderRadius="round" height="16" width="160" background="loading" />
</Row>
<Row className={styles.suggestionRow}>
<Row width="full">
<Box className={styles.imageHolder} />
<Column gap="4" width="full">
<Row justifyContent="space-between">
<Box borderRadius="round" height="20" background="loading" style={{ width: '180px' }} />
<Box borderRadius="round" height="20" width="48" background="loading" />
</Row>
<Row justifyContent="space-between">
<Box borderRadius="round" height="16" width="120" background="loading" />
<Box borderRadius="round" height="16" width="48" background="loading" />
</Row>
</Column>
</Row>
</Box>
</Row>
)
}

View File

@@ -1,3 +0,0 @@
import Navbar from './Navbar'
export default Navbar

View File

@@ -1,16 +1,17 @@
import { Trans } from '@lingui/macro'
import Web3Status from 'components/Web3Status'
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
import { Box } from 'nft/components/Box'
import { Row } from 'nft/components/Flex'
import { UniIcon } from 'nft/components/icons'
import { ReactNode } from 'react'
import { NavLink, NavLinkProps, useLocation } from 'react-router-dom'
import { Box } from '../../nft/components/Box'
import { Row } from '../../nft/components/Flex'
import { UniIcon } from '../../nft/components/icons'
import { ChainSwitcher } from './ChainSwitcher'
import { ChainSelector } from './ChainSelector'
import { MenuDropdown } from './MenuDropdown'
import * as styles from './Navbar.css'
import { SearchBar } from './SearchBar'
import { ShoppingBag } from './ShoppingBag'
import * as styles from './style.css'
interface MenuItemProps {
href: string
@@ -64,6 +65,9 @@ const PageTabs = () => {
}
const Navbar = () => {
const { pathname } = useLocation()
const isNftPage = pathname.startsWith('/nfts')
return (
<>
<nav className={styles.nav}>
@@ -73,7 +77,7 @@ const Navbar = () => {
<UniIcon width="48" height="48" className={styles.logo} />
</Box>
<Box display={{ sm: 'flex', lg: 'none' }}>
<ChainSwitcher leftAlign={true} />
<ChainSelector leftAlign={true} />
</Box>
<Row gap="8" display={{ sm: 'none', lg: 'flex' }}>
<PageTabs />
@@ -90,8 +94,9 @@ const Navbar = () => {
<Box display={{ sm: 'none', lg: 'flex' }}>
<MenuDropdown />
</Box>
{isNftPage && <ShoppingBag />}
<Box display={{ sm: 'none', lg: 'flex' }}>
<ChainSwitcher />
<ChainSelector />
</Box>
<Web3Status />

View File

@@ -4,9 +4,10 @@ import useInterval from 'lib/hooks/useInterval'
import React, { useCallback, useMemo, useState } from 'react'
import { usePopper } from 'react-popper'
import styled from 'styled-components/macro'
import { Z_INDEX } from 'theme/zIndex'
const PopoverContainer = styled.div<{ show: boolean }>`
z-index: 9999;
z-index: ${Z_INDEX.popover};
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
opacity: ${(props) => (props.show ? 1 : 0)};
transition: visibility 150ms linear, opacity 150ms linear;

View File

@@ -7,7 +7,8 @@ import { useEffect } from 'react'
import { MessageCircle, X } from 'react-feather'
import { useShowSurveyPopup } from 'state/user/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { ExternalLink, ThemedText, Z_INDEX } from 'theme'
import { ExternalLink, ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import BGImage from '../../assets/images/survey-orb.svg'

View File

@@ -41,7 +41,7 @@ const StopOverflowQuery = `@media screen and (min-width: ${MEDIA_WIDTHS.deprecat
const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean; xlPadding: boolean }>`
position: fixed;
top: ${({ extraPadding }) => (extraPadding ? '64px' : '56px')};
top: ${({ extraPadding }) => (extraPadding ? '72px' : '64px')};
right: 1rem;
max-width: 355px !important;
width: 100%;
@@ -52,7 +52,7 @@ const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean; xlPadding:
`};
${StopOverflowQuery} {
top: ${({ extraPadding, xlPadding }) => (xlPadding ? '64px' : extraPadding ? '64px' : '56px')};
top: ${({ extraPadding, xlPadding }) => (xlPadding ? '72px' : extraPadding ? '72px' : '64px')};
}
`

View File

@@ -50,7 +50,7 @@ const ToggleWrap = styled.div`
`
const ToggleLabel = styled.div`
opacity: 0.6;
opacity: ${({ theme }) => theme.opacity.hover};
margin-right: 10px;
`

View File

@@ -10,7 +10,8 @@ import { RoutingDiagramEntry } from 'components/swap/SwapRoute'
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'
import { Box } from 'rebass'
import styled from 'styled-components/macro'
import { ThemedText, Z_INDEX } from 'theme'
import { ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import { ReactComponent as DotLine } from '../../assets/svg/dot_line.svg'
import { MouseoverTooltip } from '../Tooltip'

View File

@@ -1,7 +1,7 @@
import { Currency } from '@uniswap/sdk-core'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { getTokenAddress } from 'components/AmplitudeAnalytics/utils'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { getTokenAddress } from 'analytics/utils'
import { AutoColumn } from 'components/Column'
import CurrencyLogo from 'components/CurrencyLogo'
import { AutoRow } from 'components/Row'

View File

@@ -1,8 +1,8 @@
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { LightGreyCard } from 'components/Card'
import QuestionHelper from 'components/QuestionHelper'
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'

View File

@@ -2,8 +2,8 @@
import { t, Trans } from '@lingui/macro'
import { Currency, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { EventName, ModalName } from 'components/AmplitudeAnalytics/constants'
import { Trace } from 'components/AmplitudeAnalytics/Trace'
import { EventName, ModalName } from 'analytics/constants'
import { Trace } from 'analytics/Trace'
import { sendEvent } from 'components/analytics'
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import useDebounce from 'hooks/useDebounce'

View File

@@ -8,6 +8,7 @@ import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import { useUserAddedTokens } from 'state/user/hooks'
import useLast from '../../hooks/useLast'
import { useWindowSize } from '../../hooks/useWindowSize'
import Modal from '../Modal'
import { CurrencySearch } from './CurrencySearch'
import { ImportList } from './ImportList'
@@ -97,11 +98,16 @@ export default memo(function CurrencySearchModal({
[setModalView, prevView]
)
const { height: windowHeight } = useWindowSize()
// change min height if not searching
let minHeight: number | undefined = 80
let modalHeight: number | undefined = 80
let content = null
switch (modalView) {
case CurrencyModalView.search:
if (windowHeight) {
// Converts pixel units to vh for Modal component
modalHeight = Math.min(Math.round((680 / windowHeight) * 100), 80)
}
content = (
<CurrencySearch
isOpen={isOpen}
@@ -119,7 +125,7 @@ export default memo(function CurrencySearchModal({
)
break
case CurrencyModalView.tokenSafety:
minHeight = undefined
modalHeight = undefined
if (tokenSafetyFlag === TokenSafetyVariant.Enabled && warningToken) {
content = (
<TokenSafety
@@ -133,7 +139,7 @@ export default memo(function CurrencySearchModal({
break
case CurrencyModalView.importToken:
if (importToken) {
minHeight = undefined
modalHeight = undefined
if (tokenSafetyFlag === TokenSafetyVariant.Enabled) {
showTokenSafetySpeedbump(importToken)
}
@@ -149,7 +155,7 @@ export default memo(function CurrencySearchModal({
}
break
case CurrencyModalView.importList:
minHeight = 40
modalHeight = 40
if (importList && listURL) {
content = <ImportList list={importList} listURL={listURL} onDismiss={onDismiss} setModalView={setModalView} />
}
@@ -167,7 +173,7 @@ export default memo(function CurrencySearchModal({
break
}
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={80} minHeight={minHeight}>
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={modalHeight} minHeight={modalHeight}>
{content}
</Modal>
)

View File

@@ -1,8 +1,8 @@
import { Plural, Trans } from '@lingui/macro'
import { Currency, Token } from '@uniswap/sdk-core'
import { TokenList } from '@uniswap/token-lists'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { ButtonPrimary } from 'components/Button'
import { AutoColumn } from 'components/Column'
import { RowBetween } from 'components/Row'

View File

@@ -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;
}

View File

@@ -1,13 +1,8 @@
import Modal from '../Modal'
import TokenSafety from '.'
import TokenSafety, { TokenSafetyProps } from '.'
interface TokenSafetyModalProps {
interface TokenSafetyModalProps extends TokenSafetyProps {
isOpen: boolean
tokenAddress: string | null
secondTokenAddress?: string
onContinue: () => void
onCancel: () => void
showCancel?: boolean
}
export default function TokenSafetyModal({
@@ -16,6 +11,7 @@ export default function TokenSafetyModal({
secondTokenAddress,
onContinue,
onCancel,
onBlocked,
showCancel,
}: TokenSafetyModalProps) {
return (
@@ -23,8 +19,9 @@ export default function TokenSafetyModal({
<TokenSafety
tokenAddress={tokenAddress}
secondTokenAddress={secondTokenAddress}
onCancel={onCancel}
onContinue={onContinue}
onBlocked={onBlocked}
onCancel={onCancel}
showCancel={showCancel}
/>
</Modal>

View File

@@ -64,7 +64,7 @@ const StyledCloseButton = styled(StyledButton)`
&:hover {
background-color: ${({ theme }) => theme.backgroundInteractive};
opacity: 0.6;
opacity: ${({ theme }) => theme.opacity.hover};
transition: opacity 250ms ease;
}
`
@@ -73,11 +73,13 @@ const Buttons = ({
warning,
onContinue,
onCancel,
onBlocked,
showCancel,
}: {
warning: Warning
onContinue: () => void
onCancel: () => void
onBlocked?: () => void
showCancel?: boolean
}) => {
return warning.canProceed ? (
@@ -88,7 +90,7 @@ const Buttons = ({
{showCancel && <StyledCancelButton onClick={onCancel}>Cancel</StyledCancelButton>}
</>
) : (
<StyledCloseButton onClick={onCancel}>
<StyledCloseButton onClick={onBlocked ?? onCancel}>
<Trans>Close</Trans>
</StyledCloseButton>
)
@@ -130,10 +132,10 @@ const ExplorerLinkWrapper = styled.div`
cursor: pointer;
:hover {
opacity: 0.6;
opacity: ${({ theme }) => theme.opacity.hover};
}
:active {
opacity: 0.4;
opacity: ${({ theme }) => theme.opacity.click};
}
`
@@ -184,11 +186,12 @@ const StyledExternalLink = styled(ExternalLink)`
font-weight: 600;
`
interface TokenSafetyProps {
export interface TokenSafetyProps {
tokenAddress: string | null
secondTokenAddress?: string
onContinue: () => void
onCancel: () => void
onBlocked?: () => void
showCancel?: boolean
}
@@ -197,6 +200,7 @@ export default function TokenSafety({
secondTokenAddress,
onContinue,
onCancel,
onBlocked,
showCancel,
}: TokenSafetyProps) {
const logos = []
@@ -261,7 +265,13 @@ export default function TokenSafety({
</InfoText>
</ShortColumn>
<LinkColumn>{urls}</LinkColumn>
<Buttons warning={displayWarning} onContinue={acknowledge} onCancel={onCancel} showCancel={showCancel} />
<Buttons
warning={displayWarning}
onContinue={acknowledge}
onCancel={onCancel}
onBlocked={onBlocked}
showCancel={showCancel}
/>
</Container>
</Wrapper>
)

View 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>
)
}

View 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>
)
}

View File

@@ -1,12 +1,12 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { getChainInfoOrDefault } from 'constants/chainInfo'
import { formatToDecimal } from 'analytics/utils'
import { useToken } from 'hooks/Tokens'
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import { useTokenBalance } from 'lib/hooks/useCurrencyBalance'
import { AlertTriangle } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import NetworkBalance from './NetworkBalance'
import styled from 'styled-components/macro'
const BalancesCard = styled.div`
width: 100%;
@@ -33,14 +33,9 @@ const ErrorText = styled.span`
display: flex;
flex-wrap: wrap;
`
const NetworkBalancesSection = styled.div`
height: fit-content;
`
const TotalBalanceSection = styled.div`
height: fit-content;
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
margin-bottom: 20px;
padding-bottom: 20px;
`
const TotalBalance = styled.div`
display: flex;
@@ -54,58 +49,35 @@ const TotalBalanceItem = styled.div`
display: flex;
`
export default function BalanceSummary({
address,
networkBalances,
totalBalance,
}: {
address: string
networkBalances: (JSX.Element | null)[] | null
totalBalance: number
}) {
const theme = useTheme()
const tokenSymbol = useToken(address)?.symbol
const { loading, error, data } = useNetworkTokenBalances({ address })
export default function BalanceSummary({ address }: { address: string }) {
const token = useToken(address)
const { loading, error } = useNetworkTokenBalances({ address })
const { chainId: connectedChainId } = useWeb3React()
const { account } = useWeb3React()
const balance = useTokenBalance(account, token ?? undefined)
const balanceNumber = balance ? formatToDecimal(balance, Math.min(balance.currency.decimals, 6)) : undefined
const balanceUsd = useStablecoinValue(balance)?.toFixed(2)
const balanceUsdNumber = balanceUsd ? parseFloat(balanceUsd) : undefined
const { label: connectedLabel, logoUrl: connectedLogoUrl } = getChainInfoOrDefault(connectedChainId)
const connectedFiatValue = 1
const multipleBalances = true // for testing purposes
if (loading) return null
if (loading || (!error && !balanceNumber && !balanceUsdNumber)) return null
return (
<BalancesCard>
{error ? (
<ErrorState>
<AlertTriangle size={24} />
<ErrorText>
<Trans>There was an error loading your {tokenSymbol} balance</Trans>
<Trans>There was an error loading your {token?.symbol} balance</Trans>
</ErrorText>
</ErrorState>
) : multipleBalances ? (
<>
<TotalBalanceSection>
Your balance across all networks
<TotalBalance>
<TotalBalanceItem>{`${totalBalance} ${tokenSymbol}`}</TotalBalanceItem>
<TotalBalanceItem>$4,210.12</TotalBalanceItem>
</TotalBalance>
</TotalBalanceSection>
<NetworkBalancesSection>Your balances by network</NetworkBalancesSection>
{data && networkBalances}
</>
) : (
<>
Your balance on {connectedLabel}
<NetworkBalance
logoUrl={connectedLogoUrl}
balance={'1'}
tokenSymbol={tokenSymbol ?? 'XXX'}
fiatValue={connectedFiatValue}
label={connectedLabel}
networkColor={theme.textPrimary}
/>
<TotalBalanceSection>
Your balance
<TotalBalance>
<TotalBalanceItem>{`${balanceNumber} ${token?.symbol}`}</TotalBalanceItem>
<TotalBalanceItem>{`$${balanceUsdNumber}`}</TotalBalanceItem>
</TotalBalance>
</TotalBalanceSection>
</>
)}
</BalancesCard>

View 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};
}
`

View File

@@ -0,0 +1,120 @@
import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core'
import { ParentSize } from '@visx/responsive'
import { useWeb3React } from '@web3-react/core'
import CurrencyLogo from 'components/CurrencyLogo'
import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
import { getChainInfo } from 'constants/chainInfo'
import { nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { checkWarning } from 'constants/tokenSafety'
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
import { SingleTokenData } from 'graphql/data/Token'
import { useCurrency } from 'hooks/Tokens'
import styled from 'styled-components/macro'
import { useIsFavorited, useToggleFavorite } from '../state'
import { ClickFavorited, FavoriteIcon } from '../TokenTable/TokenRow'
import PriceChart from './PriceChart'
import ShareButton from './ShareButton'
export const ChartHeader = styled.div`
width: 100%;
display: flex;
flex-direction: column;
color: ${({ theme }) => theme.textPrimary};
gap: 4px;
margin-bottom: 24px;
`
export const TokenInfoContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`
export const ChartContainer = styled.div`
display: flex;
height: 436px;
align-items: center;
`
export const TokenNameCell = styled.div`
display: flex;
gap: 8px;
font-size: 20px;
line-height: 28px;
align-items: center;
`
const TokenSymbol = styled.span`
text-transform: uppercase;
color: ${({ theme }) => theme.textSecondary};
`
const TokenActions = styled.div`
display: flex;
gap: 16px;
color: ${({ theme }) => theme.textSecondary};
`
const NetworkBadge = styled.div<{ networkColor?: string; backgroundColor?: string }>`
border-radius: 5px;
padding: 4px 8px;
font-weight: 600;
font-size: 12px;
line-height: 12px;
color: ${({ theme, networkColor }) => networkColor ?? theme.textPrimary};
background-color: ${({ theme, backgroundColor }) => backgroundColor ?? theme.backgroundSurface};
`
export default function ChartSection({ token, tokenData }: { token: Token; tokenData: SingleTokenData | undefined }) {
const { chainId: connectedChainId } = useWeb3React()
const isFavorited = useIsFavorited(token.address)
const toggleFavorite = useToggleFavorite(token.address)
const chainInfo = getChainInfo(token?.chainId)
const networkLabel = chainInfo?.label
const networkBadgebackgroundColor = chainInfo?.backgroundColor
const warning = checkWarning(token.address)
let currency = useCurrency(token.address)
if (connectedChainId) {
const wrappedNativeCurrency = WRAPPED_NATIVE_CURRENCY[connectedChainId]
const isWrappedNativeToken = wrappedNativeCurrency?.address === token?.address
if (isWrappedNativeToken) {
currency = nativeOnChain(connectedChainId)
}
}
const tokenName = tokenData?.name ?? token?.name
const tokenSymbol = tokenData?.tokens?.[0]?.symbol ?? token?.symbol
return (
<ChartHeader>
<TokenInfoContainer>
<TokenNameCell>
<CurrencyLogo currency={currency} size={'32px'} symbol={tokenSymbol} />
{tokenName ?? <Trans>Name not found</Trans>}
<TokenSymbol>{tokenSymbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
{!warning && <VerifiedIcon size="20px" />}
{networkBadgebackgroundColor && (
<NetworkBadge networkColor={chainInfo?.color} backgroundColor={networkBadgebackgroundColor}>
{networkLabel}
</NetworkBadge>
)}
</TokenNameCell>
<TokenActions>
{tokenName && tokenSymbol && (
<ShareButton tokenName={tokenName} tokenSymbol={tokenSymbol} tokenAddress={token.address} />
)}
{useFavoriteTokensFlag() === FavoriteTokensVariant.Enabled && (
<ClickFavorited onClick={toggleFavorite}>
<FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited>
)}
</TokenActions>
</TokenInfoContainer>
<ChartContainer>
<ParentSize>
{({ width, height }) => (
<PriceChart tokenAddress={token.address} width={width} height={height} priceDataFragmentRef={null} />
)}
</ParentSize>
</ChartContainer>
</ChartHeader>
)
}

View File

@@ -3,6 +3,7 @@ import { useToken } from 'hooks/Tokens'
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
import { useState } from 'react'
import { AlertTriangle } from 'react-feather'
import { Link } from 'react-router-dom'
import styled from 'styled-components/macro'
import { SMALLEST_MOBILE_MEDIA_BREAKPOINT } from '../constants'
@@ -173,9 +174,11 @@ export default function FooterBalanceSummary({
)}
</BalanceInfo>
)}
<SwapButton onClick={() => (window.location.href = 'https://app.uniswap.org/#/swap')}>
<Trans>Swap</Trans>
</SwapButton>
<Link to={`/swap?outputCurrency=${address}`}>
<SwapButton>
<Trans>Swap</Trans>
</SwapButton>
</Link>
</TotalBalancesSection>
{showMultipleBalances && (
<NetworkBalancesSection>

View File

@@ -1,22 +1,13 @@
import { Footer, LeftPanel, RightPanel, TokenDetailsLayout } from 'pages/TokenDetails'
import styled, { useTheme } from 'styled-components/macro'
import { LoadingBubble } from '../loading'
import { AboutContainer, AboutHeader, ResourcesContainer } from './About'
import { ContractAddressSection } from './AddressSection'
import { BreadcrumbNavLink } from './BreadcrumbNavLink'
import { ChartContainer, ChartHeader, TokenInfoContainer, TokenNameCell } from './ChartSection'
import { DeltaContainer, TokenPrice } from './PriceChart'
import {
AboutContainer,
AboutHeader,
BreadcrumbNavLink,
ChartContainer,
ChartHeader,
ContractAddressSection,
ResourcesContainer,
Stat,
StatPair,
StatsSection,
TokenInfoContainer,
TokenNameCell,
TopArea,
} from './TokenDetailContainers'
import { StatPair, StatWrapper, TokenStatsSection } from './StatsSection'
const LoadingChartContainer = styled(ChartContainer)`
height: 336px;
@@ -90,7 +81,7 @@ export function Wave() {
/* Loading State: row component with loading bubbles */
export default function LoadingTokenDetail() {
return (
<TopArea>
<LeftPanel>
<BreadcrumbNavLink to="/explore">
<Space heightSize={20} />
</BreadcrumbNavLink>
@@ -120,30 +111,30 @@ export default function LoadingTokenDetail() {
</LoadingChartContainer>
<Space heightSize={32} />
</ChartHeader>
<StatsSection>
<TokenStatsSection>
<StatsLoadingContainer>
<StatPair>
<Stat>
<StatWrapper>
<HalfLoadingBubble />
<StatLoadingBubble />
</Stat>
<Stat>
</StatWrapper>
<StatWrapper>
<HalfLoadingBubble />
<StatLoadingBubble />
</Stat>
</StatWrapper>
</StatPair>
<StatPair>
<Stat>
<StatWrapper>
<HalfLoadingBubble />
<StatLoadingBubble />
</Stat>
<Stat>
</StatWrapper>
<StatWrapper>
<HalfLoadingBubble />
<StatLoadingBubble />
</Stat>
</StatWrapper>
</StatPair>
</StatsLoadingContainer>
</StatsSection>
</TokenStatsSection>
<AboutContainer>
<AboutHeader>
<SquareLoadingBubble />
@@ -155,6 +146,16 @@ export default function LoadingTokenDetail() {
<ResourcesContainer>{null}</ResourcesContainer>
</AboutContainer>
<ContractAddressSection>{null}</ContractAddressSection>
</TopArea>
</LeftPanel>
)
}
export function LoadingTokenDetails() {
return (
<TokenDetailsLayout>
<LoadingTokenDetail />
<RightPanel />
<Footer />
</TokenDetailsLayout>
)
}

View File

@@ -1,19 +1,29 @@
import { Token } from '@uniswap/sdk-core'
import { AxisBottom, TickFormatter } from '@visx/axis'
import { localPoint } from '@visx/event'
import { EventType } from '@visx/event/lib/types'
import { GlyphCircle } from '@visx/glyph'
import { Line } from '@visx/shape'
import { filterTimeAtom } from 'components/Tokens/state'
import { bisect, curveCardinal, NumberValue, scaleLinear } from 'd3'
import { useTokenPriceQuery } from 'graphql/data/TokenPriceQuery'
import { TimePeriod } from 'graphql/data/TopTokenQuery'
import {
bisect,
curveCardinal,
NumberValue,
scaleLinear,
timeDay,
timeHour,
timeMinute,
timeMonth,
timeTicks,
} from 'd3'
import { TokenPrices$key } from 'graphql/data/__generated__/TokenPrices.graphql'
import { useTokenPricesCached } from 'graphql/data/Token'
import { PricePoint } from 'graphql/data/Token'
import { TimePeriod } from 'graphql/data/util'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { useAtom } from 'jotai'
import { useCallback, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { ArrowDownRight, ArrowUpRight } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { OPACITY_HOVER } from 'theme'
import {
dayHourFormatter,
hourFormatter,
@@ -29,11 +39,9 @@ import { DISPLAYS, ORDERED_TIMES } from '../TokenTable/TimeSelector'
// TODO: This should be combined with the logic in TimeSelector.
export type PricePoint = { value: number; timestamp: number }
export const DATA_EMPTY = { value: 0, timestamp: 0 }
function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
export function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
const prices = pricePoints.map((x) => x.value)
const min = Math.min(...prices)
const max = Math.max(...prices)
@@ -72,7 +80,6 @@ export function formatDelta(delta: number) {
export const ChartHeader = styled.div`
position: absolute;
`
export const TokenPrice = styled.span`
font-size: 36px;
line-height: 44px;
@@ -112,91 +119,127 @@ const TimeButton = styled.button<{ active: boolean }>`
border: none;
cursor: pointer;
color: ${({ theme, active }) => (active ? theme.textPrimary : theme.textSecondary)};
transition-duration: ${({ theme }) => theme.transition.duration.fast};
:hover {
${({ active }) => !active && `opacity: ${OPACITY_HOVER};`}
${({ active, theme }) => !active && `opacity: ${theme.opacity.hover};`}
}
`
function getTicks(startTimestamp: number, endTimestamp: number, numTicks = 5) {
return Array.from(
{ length: numTicks },
(v, i) => endTimestamp - ((endTimestamp - startTimestamp) / (numTicks + 1)) * (i + 1)
)
}
function tickFormat(
startTimestamp: number,
endTimestamp: number,
timePeriod: TimePeriod,
locale: string
): [TickFormatter<NumberValue>, (v: number) => string, number[]] {
switch (timePeriod) {
case TimePeriod.HOUR:
return [hourFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp)]
case TimePeriod.DAY:
return [hourFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp)]
case TimePeriod.WEEK:
return [weekFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp, 6)]
case TimePeriod.MONTH:
return [monthDayFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp)]
case TimePeriod.YEAR:
return [monthTickFormatter(locale), monthYearDayFormatter(locale), getTicks(startTimestamp, endTimestamp)]
case TimePeriod.ALL:
return [monthYearFormatter(locale), monthYearDayFormatter(locale), getTicks(startTimestamp, endTimestamp)]
}
}
const margin = { top: 100, bottom: 48, crosshair: 72 }
const timeOptionsHeight = 44
const crosshairDateOverhang = 80
interface PriceChartProps {
width: number
height: number
token: Token
tokenAddress: string
priceDataFragmentRef?: TokenPrices$key | null
}
export function PriceChart({ width, height, token }: PriceChartProps) {
export function PriceChart({ width, height, tokenAddress, priceDataFragmentRef }: PriceChartProps) {
const [timePeriod, setTimePeriod] = useAtom(filterTimeAtom)
const locale = useActiveLocale()
const theme = useTheme()
// TODO: Add network selector input, consider using backend type instead of current front end selector type
const pricePoints: PricePoint[] = useTokenPriceQuery(token.address, timePeriod, 'ETHEREUM').filter(
(p): p is PricePoint => Boolean(p && p.value)
)
const { priceMap } = useTokenPricesCached(priceDataFragmentRef, tokenAddress, 'ETHEREUM', timePeriod)
const prices = priceMap.get(timePeriod)
const hasData = pricePoints.length !== 0
/* TODO: Implement API calls & cache to use here */
const startingPrice = hasData ? pricePoints[0] : DATA_EMPTY
const endingPrice = hasData ? pricePoints[pricePoints.length - 1] : DATA_EMPTY
// first price point on the x-axis of the current time period's chart
const startingPrice = prices?.[0] ?? DATA_EMPTY
// last price point on the x-axis of the current time period's chart
const endingPrice = prices?.[prices.length - 1] ?? DATA_EMPTY
const [displayPrice, setDisplayPrice] = useState(startingPrice)
// set display price to ending price when prices have changed.
useEffect(() => {
if (prices) {
setDisplayPrice(endingPrice)
}
}, [prices, endingPrice])
const [crosshair, setCrosshair] = useState<number | null>(null)
const graphWidth = width + crosshairDateOverhang
// TODO: remove this logic after suspense is properly added
const graphHeight = height - timeOptionsHeight > 0 ? height - timeOptionsHeight : 0
const graphInnerHeight = graphHeight - margin.top - margin.bottom > 0 ? graphHeight - margin.top - margin.bottom : 0
// Defining scales
// x scale
const timeScale = scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, width])
const timeScale = useMemo(
() => scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, width]),
[startingPrice, endingPrice, width]
)
// y scale
const rdScale = scaleLinear().domain(getPriceBounds(pricePoints)).range([graphInnerHeight, 0])
const rdScale = useMemo(
() =>
scaleLinear()
.domain(getPriceBounds(prices ?? []))
.range([graphInnerHeight, 0]),
[prices, graphInnerHeight]
)
function tickFormat(
startTimestamp: number,
endTimestamp: number,
timePeriod: TimePeriod,
locale: string
): [TickFormatter<NumberValue>, (v: number) => string, NumberValue[]] {
const offsetTime = (endingPrice.timestamp.valueOf() - startingPrice.timestamp.valueOf()) / 24
const startDateWithOffset = new Date((startingPrice.timestamp.valueOf() + offsetTime) * 1000)
const endDateWithOffset = new Date((endingPrice.timestamp.valueOf() - offsetTime) * 1000)
switch (timePeriod) {
case TimePeriod.HOUR:
return [
hourFormatter(locale),
dayHourFormatter(locale),
(timeMinute.every(5) ?? timeMinute)
.range(startDateWithOffset, endDateWithOffset, 2)
.map((x) => x.valueOf() / 1000),
]
case TimePeriod.DAY:
return [
hourFormatter(locale),
dayHourFormatter(locale),
timeHour.range(startDateWithOffset, endDateWithOffset, 4).map((x) => x.valueOf() / 1000),
]
case TimePeriod.WEEK:
return [
weekFormatter(locale),
dayHourFormatter(locale),
timeDay.range(startDateWithOffset, endDateWithOffset, 1).map((x) => x.valueOf() / 1000),
]
case TimePeriod.MONTH:
return [
monthDayFormatter(locale),
dayHourFormatter(locale),
timeDay.range(startDateWithOffset, endDateWithOffset, 7).map((x) => x.valueOf() / 1000),
]
case TimePeriod.YEAR:
return [
monthTickFormatter(locale),
monthYearDayFormatter(locale),
timeMonth.range(startDateWithOffset, endDateWithOffset, 2).map((x) => x.valueOf() / 1000),
]
case TimePeriod.ALL:
return [
monthYearFormatter(locale),
monthYearDayFormatter(locale),
timeTicks(startDateWithOffset, endDateWithOffset, 6).map((x) => x.valueOf() / 1000),
]
}
}
const handleHover = useCallback(
(event: Element | EventType) => {
if (!prices) return
const { x } = localPoint(event) || { x: 0 }
const x0 = timeScale.invert(x) // get timestamp from the scalexw
const index = bisect(
pricePoints.map((x) => x.timestamp),
prices.map((x) => x.timestamp),
x0,
1
)
const d0 = pricePoints[index - 1]
const d1 = pricePoints[index]
const d0 = prices[index - 1]
const d1 = prices[index]
let pricePoint = d0
const hasPreviousData = d1 && d1.timestamp
@@ -207,7 +250,7 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
setCrosshair(timeScale(pricePoint.timestamp))
setDisplayPrice(pricePoint)
},
[timeScale, pricePoints]
[timeScale, prices]
)
const resetDisplay = useCallback(() => {
@@ -215,8 +258,8 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
setDisplayPrice(endingPrice)
}, [setCrosshair, setDisplayPrice, endingPrice])
// TODO: connect to loading state
if (!hasData) {
// TODO: Display no data available error
if (!prices) {
return null
}
@@ -232,8 +275,8 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
const crosshairEdgeMax = width * 0.85
const crosshairAtEdge = !!crosshair && crosshair > crosshairEdgeMax
/* Default curve doesn't look good for the ALL chart */
const curveTension = timePeriod === TimePeriod.ALL ? 0.75 : 0.9
/* Default curve doesn't look good for the HOUR/ALL chart */
const curveTension = timePeriod === TimePeriod.ALL ? 0.75 : timePeriod === TimePeriod.HOUR ? 1 : 0.9
return (
<>
@@ -245,13 +288,13 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
</DeltaContainer>
</ChartHeader>
<LineChart
data={pricePoints}
data={prices}
getX={(p: PricePoint) => timeScale(p.timestamp)}
getY={(p: PricePoint) => rdScale(p.value)}
marginTop={margin.top}
curve={curveCardinal.tension(curveTension)}
strokeWidth={2}
width={graphWidth}
width={width}
height={graphHeight}
>
{crosshair !== null ? (
@@ -262,6 +305,7 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
tickFormat={tickFormatter}
tickStroke={theme.backgroundOutline}
tickLength={4}
hideTicks={true}
tickTransform={'translate(0 -5)'}
tickValues={ticks}
top={graphHeight - 1}
@@ -316,7 +360,13 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
<TimeOptionsWrapper>
<TimeOptionsContainer>
{ORDERED_TIMES.map((time) => (
<TimeButton key={DISPLAYS[time]} active={timePeriod === time} onClick={() => setTimePeriod(time)}>
<TimeButton
key={DISPLAYS[time]}
active={timePeriod === time}
onClick={() => {
setTimePeriod(time)
}}
>
{DISPLAYS[time]}
</TimeButton>
))}

View File

@@ -5,9 +5,10 @@ import { Twitter } from 'react-feather'
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import styled, { useTheme } from 'styled-components/macro'
import { ClickableStyle, CopyHelperRefType, OPACITY_CLICK, Z_INDEX } from 'theme'
import { ClickableStyle, CopyHelperRefType } from 'theme'
import { colors } from 'theme/colors'
import { opacify } from 'theme/utils'
import { Z_INDEX } from 'theme/zIndex'
import { ReactComponent as ShareIcon } from '../../../assets/svg/share.svg'
import { CopyHelper } from '../../../theme'
@@ -25,7 +26,7 @@ const Share = styled(ShareIcon)<{ open: boolean }>`
height: 24px;
width: 24px;
${ClickableStyle}
${({ open }) => open && `opacity: ${OPACITY_CLICK} !important`};
${({ open, theme }) => open && `opacity: ${theme.opacity.click} !important`};
`
const ShareActions = styled.div`

View 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>
}
}

View File

@@ -1,305 +0,0 @@
import { Trans } from '@lingui/macro'
import { ParentSize } from '@visx/responsive'
import { useWeb3React } from '@web3-react/core'
import CurrencyLogo from 'components/CurrencyLogo'
import PriceChart from 'components/Tokens/TokenDetails/PriceChart'
import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import { getChainInfo } from 'constants/chainInfo'
import { nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { checkWarning, WARNING_LEVEL } from 'constants/tokenSafety'
import { chainIdToChainName, useTokenDetailQuery } from 'graphql/data/TokenDetailQuery'
import { useCurrency, useIsUserAddedToken, useToken } from 'hooks/Tokens'
import { useAtomValue } from 'jotai/utils'
import { darken } from 'polished'
import { Suspense, useCallback } from 'react'
import { useState } from 'react'
import { ArrowLeft, Heart } from 'react-feather'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro'
import { ClickableStyle, CopyContractAddress } from 'theme'
import { formatDollarAmount } from 'utils/formatDollarAmt'
import { favoritesAtom, filterNetworkAtom, useToggleFavorite } from '../state'
import { ClickFavorited } from '../TokenTable/TokenRow'
import LoadingTokenDetail from './LoadingTokenDetail'
import Resource from './Resource'
import ShareButton from './ShareButton'
import {
AboutContainer,
AboutHeader,
BreadcrumbNavLink,
ChartContainer,
ChartHeader,
ContractAddressSection,
ResourcesContainer,
Stat,
StatPair,
StatsSection,
TokenInfoContainer,
TokenNameCell,
TopArea,
} from './TokenDetailContainers'
const ContractAddress = styled.button`
display: flex;
color: ${({ theme }) => theme.textPrimary};
gap: 10px;
align-items: center;
background: transparent;
border: none;
min-height: 38px;
padding: 0px;
cursor: pointer;
`
const Contract = styled.div`
display: flex;
flex-direction: column;
color: ${({ theme }) => theme.textSecondary};
font-size: 14px;
gap: 4px;
`
const StatPrice = styled.span`
font-size: 28px;
color: ${({ theme }) => theme.textPrimary};
`
const TokenActions = styled.div`
display: flex;
gap: 16px;
color: ${({ theme }) => theme.textSecondary};
`
const TokenSymbol = styled.span`
text-transform: uppercase;
color: ${({ theme }) => theme.textSecondary};
`
const NetworkBadge = styled.div<{ networkColor?: string; backgroundColor?: string }>`
border-radius: 5px;
padding: 4px 8px;
font-weight: 600;
font-size: 12px;
line-height: 12px;
color: ${({ theme, networkColor }) => networkColor ?? theme.textPrimary};
background-color: ${({ theme, backgroundColor }) => backgroundColor ?? theme.backgroundSurface};
`
const FavoriteIcon = styled(Heart)<{ isFavorited: boolean }>`
${ClickableStyle}
height: 22px;
width: 24px;
color: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : theme.textSecondary)};
fill: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : 'transparent')};
`
const NoInfoAvailable = styled.span`
color: ${({ theme }) => theme.textTertiary};
font-weight: 400;
font-size: 16px;
`
const TokenDescriptionContainer = styled.div`
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
max-height: fit-content;
padding-top: 16px;
line-height: 24px;
white-space: pre-wrap;
`
const TruncateDescriptionButton = styled.div`
color: ${({ theme }) => theme.textSecondary};
font-weight: 400;
font-size: 14px;
padding-top: 14px;
&:hover,
&:focus {
color: ${({ theme }) => darken(0.1, theme.textSecondary)};
cursor: pointer;
}
`
const TRUNCATE_CHARACTER_COUNT = 400
type TokenDetailData = {
description: string | null | undefined
homepageUrl: string | null | undefined
twitterName: string | null | undefined
}
const truncateDescription = (desc: string) => {
//trim the string to the maximum length
let tokenDescriptionTruncated = desc.slice(0, TRUNCATE_CHARACTER_COUNT)
//re-trim if we are in the middle of a word
tokenDescriptionTruncated = `${tokenDescriptionTruncated.slice(
0,
Math.min(tokenDescriptionTruncated.length, tokenDescriptionTruncated.lastIndexOf(' '))
)}...`
return tokenDescriptionTruncated
}
export function AboutSection({ address, tokenDetailData }: { address: string; tokenDetailData: TokenDetailData }) {
const [isDescriptionTruncated, setIsDescriptionTruncated] = useState(true)
const shouldTruncate =
tokenDetailData && tokenDetailData.description
? tokenDetailData.description.length > TRUNCATE_CHARACTER_COUNT
: false
const tokenDescription =
tokenDetailData && tokenDetailData.description && shouldTruncate && isDescriptionTruncated
? truncateDescription(tokenDetailData.description)
: tokenDetailData.description
return (
<AboutContainer>
<AboutHeader>
<Trans>About</Trans>
</AboutHeader>
<TokenDescriptionContainer>
{(!tokenDetailData || !tokenDetailData.description) && (
<NoInfoAvailable>
<Trans>No token information available</Trans>
</NoInfoAvailable>
)}
{tokenDescription}
{shouldTruncate && (
<TruncateDescriptionButton onClick={() => setIsDescriptionTruncated(!isDescriptionTruncated)}>
{isDescriptionTruncated ? <Trans>Read more</Trans> : <Trans>Hide</Trans>}
</TruncateDescriptionButton>
)}
</TokenDescriptionContainer>
<ResourcesContainer>
<Resource name={'Etherscan'} link={`https://etherscan.io/address/${address}`} />
<Resource name={'Protocol info'} link={`https://info.uniswap.org/#/tokens/${address}`} />
{tokenDetailData?.homepageUrl && <Resource name={'Website'} link={tokenDetailData.homepageUrl} />}
{tokenDetailData?.twitterName && (
<Resource name={'Twitter'} link={`https://twitter.com/${tokenDetailData.twitterName}`} />
)}
</ResourcesContainer>
</AboutContainer>
)
}
export default function LoadedTokenDetail({ address }: { address: string }) {
const { chainId: connectedChainId } = useWeb3React()
const token = useToken(address)
let currency = useCurrency(address)
const favoriteTokens = useAtomValue<string[]>(favoritesAtom)
const isFavorited = favoriteTokens.includes(address)
const toggleFavorite = useToggleFavorite(address)
const warning = checkWarning(address)
const navigate = useNavigate()
const isUserAddedToken = useIsUserAddedToken(token)
const [warningModalOpen, setWarningModalOpen] = useState(!!warning && !isUserAddedToken)
const handleDismissWarning = useCallback(() => {
setWarningModalOpen(false)
}, [setWarningModalOpen])
const handleCancel = useCallback(() => {
setWarningModalOpen(false)
warning && warning.level === WARNING_LEVEL.BLOCKED && navigate(-1)
}, [setWarningModalOpen, navigate, warning])
const chainInfo = getChainInfo(token?.chainId)
const networkLabel = chainInfo?.label
const networkBadgebackgroundColor = chainInfo?.backgroundColor
const filterNetwork = useAtomValue(filterNetworkAtom)
const tokenDetailData = useTokenDetailQuery(address, chainIdToChainName(filterNetwork))
const relevantTokenDetailData = (({ description, homepageUrl, twitterName }) => ({
description,
homepageUrl,
twitterName,
}))(tokenDetailData)
if (!token || !token.name || !token.symbol || !connectedChainId) {
return <LoadingTokenDetail />
}
const wrappedNativeCurrency = WRAPPED_NATIVE_CURRENCY[connectedChainId]
const isWrappedNativeToken = wrappedNativeCurrency?.address === token.address
if (isWrappedNativeToken) {
currency = nativeOnChain(connectedChainId)
}
const tokenName = isWrappedNativeToken && currency ? currency.name : tokenDetailData.name
const defaultTokenSymbol = tokenDetailData.tokens?.[0]?.symbol ?? token.symbol
const tokenSymbol = isWrappedNativeToken && currency ? currency.symbol : defaultTokenSymbol
return (
<Suspense fallback={<LoadingTokenDetail />}>
<TopArea>
<BreadcrumbNavLink to="/tokens">
<ArrowLeft size={14} /> Tokens
</BreadcrumbNavLink>
<ChartHeader>
<TokenInfoContainer>
<TokenNameCell>
<CurrencyLogo currency={currency} size={'32px'} 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={address} />
)}
<ClickFavorited onClick={toggleFavorite}>
<FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited>
</TokenActions>
</TokenInfoContainer>
<ChartContainer>
<ParentSize>{({ width, height }) => <PriceChart token={token} width={width} height={height} />}</ParentSize>
</ChartContainer>
</ChartHeader>
<StatsSection>
<StatPair>
<Stat>
<Trans>Market cap</Trans>
<StatPrice>
{tokenDetailData.marketCap?.value ? formatDollarAmount(tokenDetailData.marketCap?.value) : '-'}
</StatPrice>
</Stat>
<Stat>
24H volume
<StatPrice>
{tokenDetailData.volume24h?.value ? formatDollarAmount(tokenDetailData.volume24h?.value) : '-'}
</StatPrice>
</Stat>
</StatPair>
<StatPair>
<Stat>
52W low
<StatPrice>
{tokenDetailData.priceLow52W?.value ? formatDollarAmount(tokenDetailData.priceLow52W?.value) : '-'}
</StatPrice>
</Stat>
<Stat>
52W high
<StatPrice>
{tokenDetailData.priceHigh52W?.value ? formatDollarAmount(tokenDetailData.priceHigh52W?.value) : '-'}
</StatPrice>
</Stat>
</StatPair>
</StatsSection>
<AboutSection address={address} tokenDetailData={relevantTokenDetailData} />
<ContractAddressSection>
<Contract>
<Trans>Contract address</Trans>
<ContractAddress>
<CopyContractAddress address={address} />
</ContractAddress>
</Contract>
</ContractAddressSection>
<TokenSafetyModal
isOpen={warningModalOpen}
tokenAddress={address}
onCancel={handleCancel}
onContinue={handleDismissWarning}
/>
</TopArea>
</Suspense>
)
}

View File

@@ -1,81 +0,0 @@
import { Link } from 'react-router-dom'
import styled from 'styled-components/macro'
export const AboutContainer = styled.div`
gap: 16px;
padding: 24px 0px;
`
export const AboutHeader = styled.span`
font-size: 28px;
line-height: 36px;
`
export const BreadcrumbNavLink = styled(Link)`
display: flex;
color: ${({ theme }) => theme.textSecondary};
font-size: 14px;
line-height: 20px;
align-items: center;
gap: 4px;
text-decoration: none;
margin-bottom: 16px;
&:hover {
color: ${({ theme }) => theme.textTertiary};
}
`
export const ChartHeader = styled.div`
width: 100%;
display: flex;
flex-direction: column;
color: ${({ theme }) => theme.textPrimary};
gap: 4px;
margin-bottom: 24px;
`
export const ContractAddressSection = styled.div`
padding: 36px 0px;
`
export const ChartContainer = styled.div`
display: flex;
height: 436px;
align-items: center;
`
export const Stat = styled.div`
display: flex;
flex-direction: column;
color: ${({ theme }) => theme.textSecondary};
font-size: 14px;
min-width: 168px;
flex: 1;
gap: 4px;
padding: 24px 0px;
`
export const StatsSection = styled.div`
display: flex;
flex-wrap: wrap;
`
export const StatPair = styled.div`
display: flex;
flex: 1;
flex-wrap: wrap;
`
export const TokenNameCell = styled.div`
display: flex;
gap: 8px;
font-size: 20px;
line-height: 28px;
align-items: center;
`
export const TokenInfoContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`
export const TopArea = styled.div`
max-width: 832px;
overflow: hidden;
`
export const ResourcesContainer = styled.div`
display: flex;
padding-top: 12px;
gap: 14px;
`

View File

@@ -5,6 +5,7 @@ import styled, { useTheme } from 'styled-components/macro'
import { SMALLEST_MOBILE_MEDIA_BREAKPOINT } from '../constants'
import { showFavoritesAtom } from '../state'
import FilterOption from './FilterOption'
const FavoriteButtonContent = styled.div`
display: flex;
@@ -12,20 +13,6 @@ const FavoriteButtonContent = styled.div`
align-items: center;
gap: 8px;
`
const StyledFavoriteButton = styled.button<{ active: boolean }>`
padding: 0px 16px;
border-radius: 12px;
background-color: ${({ theme, active }) => (active ? theme.accentActiveSoft : theme.backgroundInteractive)};
border: none;
color: ${({ theme, active }) => (active ? theme.accentActive : theme.textPrimary)};
font-size: 16px;
font-weight: 600;
cursor: pointer;
:hover {
background-color: ${({ theme, active }) => !active && theme.backgroundModule};
}
`
const FavoriteText = styled.span`
@media only screen and (max-width: ${SMALLEST_MOBILE_MEDIA_BREAKPOINT}) {
display: none;
@@ -36,17 +23,13 @@ export default function FavoriteButton() {
const theme = useTheme()
const [showFavorites, setShowFavorites] = useAtom(showFavoritesAtom)
return (
<StyledFavoriteButton onClick={() => setShowFavorites(!showFavorites)} active={showFavorites}>
<FilterOption onClick={() => setShowFavorites(!showFavorites)} active={showFavorites} highlight>
<FavoriteButtonContent>
<Heart
size={17}
color={showFavorites ? theme.accentActive : theme.textPrimary}
fill={showFavorites ? theme.accentActive : 'transparent'}
/>
<Heart size={20} color={showFavorites ? theme.accentActive : theme.textPrimary} />
<FavoriteText>
<Trans>Favorites</Trans>
</FavoriteText>
</FavoriteButtonContent>
</StyledFavoriteButton>
</FilterOption>
)
}

View 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

View File

@@ -10,6 +10,7 @@ import styled, { useTheme } from 'styled-components/macro'
import { MEDIUM_MEDIA_BREAKPOINT } from '../constants'
import { filterNetworkAtom } from '../state'
import FilterOption from './FilterOption'
const NETWORKS = [
SupportedChainId.MAINNET,
@@ -28,7 +29,6 @@ const InternalMenuItem = styled.div`
text-decoration: none;
}
`
const InternalLinkMenuItem = styled(InternalMenuItem)`
display: flex;
align-items: center;
@@ -59,36 +59,6 @@ const MenuTimeFlyout = styled.span`
z-index: 100;
left: 0px;
`
const StyledMenuButton = styled.button<{ open: boolean }>`
width: 100%;
height: 100%;
color: ${({ theme, open }) => (open ? theme.accentActive : theme.textPrimary)};
border: none;
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundInteractive)};
margin: 0;
padding: 6px 12px 6px 12px;
border-radius: 12px;
font-size: 16px;
line-height: 24px;
font-weight: 400;
:hover {
cursor: pointer;
outline: none;
border: none;
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundModule)};
}
:focus {
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundInteractive)};
border: none;
outline: none;
}
svg {
margin-top: 2px;
}
`
const StyledMenu = styled.div`
display: flex;
justify-content: center;
@@ -96,23 +66,21 @@ 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.accentActive : theme.textSecondary)};
@@ -143,16 +111,20 @@ export default function NetworkFilter() {
return (
<StyledMenu ref={node}>
<StyledMenuButton onClick={toggleMenu} aria-label={`networkFilter`} open={open}>
<FilterOption onClick={toggleMenu} aria-label={`networkFilter`} active={open}>
<StyledMenuContent>
<NetworkLabel>
<Logo src={circleLogoUrl ?? logoUrl} /> {label}
</NetworkLabel>
<Chevron open={open}>
{open ? <ChevronUp size={15} viewBox="0 0 24 20" /> : <ChevronDown size={15} viewBox="0 0 24 20" />}
{open ? (
<ChevronUp width={20} height={15} viewBox="0 0 24 20" />
) : (
<ChevronDown width={20} height={15} viewBox="0 0 24 20" />
)}
</Chevron>
</StyledMenuContent>
</StyledMenuButton>
</FilterOption>
{open && (
<MenuTimeFlyout>
{NETWORKS.map((network) => (

View File

@@ -1,7 +1,9 @@
import { Trans } from '@lingui/macro'
import searchIcon from 'assets/svg/search.svg'
import xIcon from 'assets/svg/x.svg'
import { useAtom } from 'jotai'
import useDebounce from 'hooks/useDebounce'
import { useUpdateAtom } from 'jotai/utils'
import { useEffect, useState } from 'react'
import styled from 'styled-components/macro'
import { MEDIUM_MEDIA_BREAKPOINT } from '../constants'
@@ -25,6 +27,7 @@ const SearchInput = styled.input`
font-size: 16px;
padding-left: 40px;
color: ${({ theme }) => theme.textSecondary};
transition-duration: ${({ theme }) => theme.transition.duration.fast};
:hover {
background-color: ${({ theme }) => theme.backgroundSurface};
@@ -56,7 +59,14 @@ const SearchInput = styled.input`
`
export default function SearchBar() {
const [filterString, setFilterString] = useAtom(filterStringAtom)
const [localFilterString, setLocalFilterString] = useState('')
const setFilterString = useUpdateAtom(filterStringAtom)
const debouncedLocalFilterString = useDebounce(localFilterString, 300)
useEffect(() => {
setFilterString(debouncedLocalFilterString)
}, [debouncedLocalFilterString, setFilterString])
return (
<SearchBarContainer>
<Trans
@@ -66,8 +76,8 @@ export default function SearchBar() {
placeholder={`${translation}`}
id="searchBar"
autoComplete="off"
value={filterString}
onChange={({ target: { value } }) => setFilterString(value)}
value={localFilterString}
onChange={({ target: { value } }) => setLocalFilterString(value)}
/>
)}
>

View File

@@ -1,4 +1,4 @@
import { TimePeriod } from 'graphql/data/TopTokenQuery'
import { TimePeriod } from 'graphql/data/util'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useAtom } from 'jotai'
import { useRef } from 'react'
@@ -9,6 +9,7 @@ import styled, { useTheme } from 'styled-components/macro'
import { MOBILE_MEDIA_BREAKPOINT, SMALL_MEDIA_BREAKPOINT } from '../constants'
import { filterTimeAtom } from '../state'
import FilterOption from './FilterOption'
export const DISPLAYS: Record<TimePeriod, string> = {
[TimePeriod.HOUR]: '1H',
@@ -39,7 +40,6 @@ const InternalMenuItem = styled.div`
text-decoration: none;
}
`
const InternalLinkMenuItem = styled(InternalMenuItem)`
display: flex;
flex-direction: row;
@@ -76,36 +76,6 @@ const MenuTimeFlyout = styled.span`
left: unset;
}
`
const StyledMenuButton = styled.button<{ open: boolean }>`
width: 100%;
height: 100%;
border: none;
color: ${({ theme, open }) => (open ? theme.accentActive : theme.textPrimary)};
margin: 0;
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundInteractive)};
padding: 6px 12px 6px 12px;
border-radius: 12px;
font-size: 16px;
line-height: 24px;
font-weight: 600;
:hover {
cursor: pointer;
border: none;
outline: none;
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundModule)};
}
:focus {
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundInteractive)};
border: none;
outline: none;
}
svg {
margin-top: 2px;
}
`
const StyledMenu = styled.div`
display: flex;
justify-content: center;
@@ -113,22 +83,20 @@ 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.accentActive : theme.textSecondary)};
@@ -145,14 +113,18 @@ export default function TimeSelector() {
return (
<StyledMenu ref={node}>
<StyledMenuButton onClick={toggleMenu} aria-label={`timeSelector`} open={open}>
<FilterOption onClick={toggleMenu} aria-label={`timeSelector`} active={open}>
<StyledMenuContent>
{DISPLAYS[activeTime]}
<Chevron open={open}>
{open ? <ChevronUp size={15} viewBox="0 0 24 20" /> : <ChevronDown size={15} viewBox="0 0 24 20" />}
{open ? (
<ChevronUp width={20} height={15} viewBox="0 0 24 20" />
) : (
<ChevronDown width={20} height={15} viewBox="0 0 24 20" />
)}
</Chevron>
</StyledMenuContent>
</StyledMenuButton>
</FilterOption>
{open && (
<MenuTimeFlyout>
{ORDERED_TIMES.map((time) => (

View File

@@ -1,18 +1,20 @@
import { Trans } from '@lingui/macro'
import { ParentSize } from '@visx/responsive'
import { sendAnalyticsEvent } from 'components/AmplitudeAnalytics'
import { EventName } from 'components/AmplitudeAnalytics/constants'
import { sendAnalyticsEvent } from 'analytics'
import { EventName } from 'analytics/constants'
import SparklineChart from 'components/Charts/SparklineChart'
import CurrencyLogo from 'components/CurrencyLogo'
import { getChainInfo } from 'constants/chainInfo'
import { TimePeriod, TokenData } from 'graphql/data/TopTokenQuery'
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
import { TokenSortMethod, TopToken } from 'graphql/data/TopTokens'
import { TimePeriod } from 'graphql/data/util'
import { useCurrency } from 'hooks/Tokens'
import { useAtom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import { ReactNode } from 'react'
import { ArrowDown, ArrowUp, Heart } from 'react-feather'
import { Link } from 'react-router-dom'
import styled, { css, useTheme } from 'styled-components/macro'
import { ClickableStyle } from 'theme'
import { formatDollarAmount } from 'utils/formatDollarAmt'
import {
@@ -23,17 +25,16 @@ import {
} from '../constants'
import { LoadingBubble } from '../loading'
import {
favoritesAtom,
filterNetworkAtom,
filterStringAtom,
filterTimeAtom,
sortCategoryAtom,
sortDirectionAtom,
useSetSortCategory,
sortAscendingAtom,
sortMethodAtom,
useIsFavorited,
useSetSortMethod,
useToggleFavorite,
} from '../state'
import { formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart'
import { Category, SortDirection } from '../types'
import { DISPLAYS } from './TimeSelector'
const Cell = styled.div`
@@ -41,11 +42,17 @@ const Cell = styled.div`
align-items: center;
justify-content: center;
`
const StyledTokenRow = styled.div<{ first?: boolean; last?: boolean; loading?: boolean }>`
const StyledTokenRow = styled.div<{
first?: boolean
last?: boolean
loading?: boolean
favoriteTokensEnabled?: boolean
}>`
background-color: transparent;
display: grid;
font-size: 15px;
grid-template-columns: 1fr 7fr 4fr 4fr 4fr 4fr 5fr 1.2fr;
grid-template-columns: ${({ favoriteTokensEnabled }) =>
favoriteTokensEnabled ? '1fr 7fr 4fr 4fr 4fr 4fr 5fr 1.2fr' : '1fr 7fr 4fr 4fr 4fr 4fr 5fr'};
height: 60px;
line-height: 24px;
max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT};
@@ -60,6 +67,7 @@ const StyledTokenRow = styled.div<{ first?: boolean; last?: boolean; loading?: b
},
}) => css`background-color ${duration.medium} ${timing.ease}`};
width: 100%;
transition-duration: ${({ theme }) => theme.transition.duration.fast};
&:hover {
${({ loading, theme }) =>
@@ -109,6 +117,14 @@ export const ClickFavorited = styled.span`
}
`
export const FavoriteIcon = styled(Heart)<{ isFavorited: boolean }>`
${ClickableStyle}
height: 22px;
width: 24px;
color: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : theme.textSecondary)};
fill: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : 'transparent')};
`
const ClickableContent = styled.div`
display: flex;
text-decoration: none;
@@ -216,15 +232,12 @@ const SortArrowCell = styled(Cell)`
`
const HeaderCellWrapper = styled.span<{ onClick?: () => void }>`
align-items: center;
${ClickableStyle}
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'unset')};
display: flex;
height: 100%;
justify-content: flex-end;
width: 100%;
&:hover {
opacity: 60%;
}
`
const SparkLineCell = styled(Cell)`
padding: 0px 24px;
@@ -316,9 +329,10 @@ const LogoContainer = styled.div`
`
/* formatting for volume with timeframe header display */
function getHeaderDisplay(category: string, timeframe: TimePeriod): string {
if (category === Category.volume || category === Category.percentChange) return `${DISPLAYS[timeframe]} ${category}`
return category
function getHeaderDisplay(method: string, timeframe: TimePeriod): string {
if (method === TokenSortMethod.VOLUME || method === TokenSortMethod.PERCENT_CHANGE)
return `${DISPLAYS[timeframe]} ${method}`
return method
}
/* Get singular header cell for header row */
@@ -326,20 +340,20 @@ function HeaderCell({
category,
sortable,
}: {
category: Category // TODO: change this to make it work for trans
category: TokenSortMethod // TODO: change this to make it work for trans
sortable: boolean
}) {
const theme = useTheme()
const sortDirection = useAtomValue<SortDirection>(sortDirectionAtom)
const handleSortCategory = useSetSortCategory(category)
const sortCategory = useAtomValue<Category>(sortCategoryAtom)
const timeframe = useAtomValue<TimePeriod>(filterTimeAtom)
const sortAscending = useAtomValue(sortAscendingAtom)
const handleSortCategory = useSetSortMethod(category)
const sortMethod = useAtomValue(sortMethodAtom)
const timeframe = useAtomValue(filterTimeAtom)
if (sortCategory === category) {
if (sortMethod === category) {
return (
<HeaderCellWrapper onClick={handleSortCategory}>
<SortArrowCell>
{sortDirection === SortDirection.increasing ? (
{sortAscending ? (
<ArrowUp size={14} color={theme.accentActive} />
) : (
<ArrowDown size={14} color={theme.accentActive} />
@@ -388,6 +402,7 @@ export function TokenRow({
volume: ReactNode
last?: boolean
}) {
const favoriteTokensEnabled = useFavoriteTokensFlag() === FavoriteTokensVariant.Enabled
const rowCells = (
<>
<ListNumberCell header={header}>{listNumber}</ListNumberCell>
@@ -397,11 +412,15 @@ export function TokenRow({
<MarketCapCell sortable={header}>{marketCap}</MarketCapCell>
<VolumeCell sortable={header}>{volume}</VolumeCell>
<SparkLineCell>{sparkLine}</SparkLineCell>
<FavoriteCell>{favorited}</FavoriteCell>
{favoriteTokensEnabled && <FavoriteCell>{favorited}</FavoriteCell>}
</>
)
if (header) return <StyledHeaderRow>{rowCells}</StyledHeaderRow>
return <StyledTokenRow {...rest}>{rowCells}</StyledTokenRow>
if (header) return <StyledHeaderRow favoriteTokensEnabled={favoriteTokensEnabled}>{rowCells}</StyledHeaderRow>
return (
<StyledTokenRow favoriteTokensEnabled={favoriteTokensEnabled} {...rest}>
{rowCells}
</StyledTokenRow>
)
}
/* Header Row: top header row component for table */
@@ -412,10 +431,10 @@ export function HeaderRow() {
favorited={null}
listNumber="#"
tokenInfo={<Trans>Token Name</Trans>}
price={<HeaderCell category={Category.price} sortable />}
percentChange={<HeaderCell category={Category.percentChange} sortable />}
marketCap={<HeaderCell category={Category.marketCap} sortable />}
volume={<HeaderCell category={Category.volume} sortable />}
price={<HeaderCell category={TokenSortMethod.PRICE} sortable />}
percentChange={<HeaderCell category={TokenSortMethod.PERCENT_CHANGE} sortable />}
marketCap={<HeaderCell category={TokenSortMethod.TOTAL_VALUE_LOCKED} sortable />}
volume={<HeaderCell category={TokenSortMethod.VOLUME} sortable />}
sparkLine={null}
/>
)
@@ -444,33 +463,28 @@ export function LoadingRow() {
)
}
/* Loaded State: row component with token information */
export default function LoadedRow({
tokenAddress,
tokenListIndex,
tokenListLength,
tokenData,
timePeriod,
}: {
tokenAddress: string
interface LoadedRowProps {
tokenListIndex: number
tokenListLength: number
tokenData: TokenData
timePeriod: TimePeriod
}) {
token: TopToken
}
/* Loaded State: row component with token information */
export default function LoadedRow({ tokenListIndex, tokenListLength, token }: LoadedRowProps) {
const tokenAddress = token?.address
const currency = useCurrency(tokenAddress)
const tokenName = tokenData.name
const tokenSymbol = tokenData.symbol
const theme = useTheme()
const [favoriteTokens] = useAtom(favoritesAtom)
const isFavorited = favoriteTokens.includes(tokenAddress)
const tokenName = token?.name
const tokenSymbol = token?.symbol
const isFavorited = useIsFavorited(tokenAddress)
const toggleFavorite = useToggleFavorite(tokenAddress)
const filterString = useAtomValue(filterStringAtom)
const filterNetwork = useAtomValue(filterNetworkAtom)
const L2Icon = getChainInfo(filterNetwork).circleLogoUrl
const delta = tokenData.percentChange?.[timePeriod]?.value
const timePeriod = useAtomValue(filterTimeAtom)
const delta = token?.market?.pricePercentChange?.value
const arrow = delta ? getDeltaArrow(delta) : null
const formattedDelta = delta ? formatDelta(delta) : null
const sortAscending = useAtomValue(sortAscendingAtom)
const exploreTokenSelectedEventProperties = {
chain_id: filterNetwork,
@@ -482,7 +496,6 @@ export default function LoadedRow({
search_token_address_input: filterString,
}
const heartColor = isFavorited ? theme.accentActive : undefined
// TODO: currency logo sizing mobile (32px) vs. desktop (24px)
return (
<StyledLink
@@ -498,10 +511,10 @@ export default function LoadedRow({
toggleFavorite()
}}
>
<Heart size={18} color={heartColor} fill={heartColor} />
<FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited>
}
listNumber={tokenListIndex + 1}
listNumber={sortAscending ? 100 - tokenListIndex : tokenListIndex + 1}
tokenInfo={
<ClickableName>
<LogoContainer>
@@ -517,7 +530,7 @@ export default function LoadedRow({
price={
<ClickableContent>
<PriceInfoCell>
{tokenData.price?.value ? formatDollarAmount(tokenData.price?.value) : '-'}
{token?.market?.price?.value ? formatDollarAmount(token.market.price.value) : '-'}
<PercentChangeInfoCell>
{formattedDelta}
{arrow}
@@ -527,25 +540,33 @@ export default function LoadedRow({
}
percentChange={
<ClickableContent>
{formattedDelta}
{formattedDelta ?? '-'}
{arrow}
</ClickableContent>
}
marketCap={
<ClickableContent>
{tokenData.marketCap?.value ? formatDollarAmount(tokenData.marketCap?.value) : '-'}
{token?.market?.totalValueLocked?.value ? formatDollarAmount(token.market.totalValueLocked.value) : '-'}
</ClickableContent>
}
volume={
<ClickableContent>
{tokenData.volume?.[timePeriod]?.value
? formatDollarAmount(tokenData.volume?.[timePeriod]?.value ?? undefined)
: '-'}
{token?.market?.volume?.value ? formatDollarAmount(token.market.volume.value) : '-'}
</ClickableContent>
}
sparkLine={
<SparkLine>
<ParentSize>{({ width, height }) => <SparklineChart width={width} height={height} />}</ParentSize>
<ParentSize>
{({ width, height }) => (
<SparklineChart
width={width}
height={height}
tokenData={token}
pricePercentChange={token?.market?.pricePercentChange?.value}
timePeriod={timePeriod}
/>
)}
</ParentSize>
</SparkLine>
}
first={tokenListIndex === 0}

View File

@@ -1,20 +1,12 @@
import { Trans } from '@lingui/macro'
import {
favoritesAtom,
filterStringAtom,
filterTimeAtom,
showFavoritesAtom,
sortCategoryAtom,
sortDirectionAtom,
} from 'components/Tokens/state'
import { TimePeriod, TokenData } from 'graphql/data/TopTokenQuery'
import { showFavoritesAtom } from 'components/Tokens/state'
import { usePrefetchTopTokens, useTopTokens } from 'graphql/data/TopTokens'
import { useAtomValue } from 'jotai/utils'
import { ReactNode, Suspense, useCallback, useMemo } from 'react'
import { ReactNode } from 'react'
import { AlertTriangle } from 'react-feather'
import styled from 'styled-components/macro'
import { MAX_WIDTH_MEDIA_BREAKPOINT } from '../constants'
import { Category, SortDirection } from '../types'
import LoadedRow, { HeaderRow, LoadingRow } from './TokenRow'
const GridContainer = styled.div`
@@ -47,86 +39,6 @@ const TokenRowsContainer = styled.div`
width: 100%;
`
function useFilteredTokens(tokens: TokenData[] | undefined) {
const filterString = useAtomValue(filterStringAtom)
const favoriteTokenAddresses = useAtomValue(favoritesAtom)
const showFavorites = useAtomValue(showFavoritesAtom)
const shownTokens =
showFavorites && tokens ? tokens.filter((token) => favoriteTokenAddresses.includes(token.address)) : tokens
return useMemo(
() =>
(shownTokens ?? []).filter((token) => {
if (!token.address) {
return false
}
if (!filterString) {
return true
}
const lowercaseFilterString = filterString.toLowerCase()
const addressIncludesFilterString = token?.address?.toLowerCase().includes(lowercaseFilterString)
const nameIncludesFilterString = token?.name?.toLowerCase().includes(lowercaseFilterString)
const symbolIncludesFilterString = token?.symbol?.toLowerCase().includes(lowercaseFilterString)
return nameIncludesFilterString || symbolIncludesFilterString || addressIncludesFilterString
}),
[shownTokens, filterString]
)
}
function useSortedTokens(tokenData: TokenData[] | null) {
const sortCategory = useAtomValue(sortCategoryAtom)
const sortDirection = useAtomValue(sortDirectionAtom)
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
const sortFn = useCallback(
(a: any, b: any) => {
if (a > b) {
return sortDirection === SortDirection.decreasing ? -1 : 1
} else if (a < b) {
return sortDirection === SortDirection.decreasing ? 1 : -1
}
return 0
},
[sortDirection]
)
return useMemo(
() =>
tokenData &&
tokenData.sort((token1, token2) => {
if (!tokenData) {
return 0
}
// fix delta/percent change property
if (!token1 || !token2 || !sortDirection || !sortCategory) {
return 0
}
let a: number | null | undefined
let b: number | null | undefined
switch (sortCategory) {
case Category.marketCap:
a = token1.marketCap?.value
b = token2.marketCap?.value
break
case Category.price:
a = token1.price?.value
b = token2.price?.value
break
case Category.volume:
a = token1.volume?.[timePeriod]?.value
b = token2.volume?.[timePeriod]?.value
break
case Category.percentChange:
a = token1.percentChange?.[timePeriod]?.value
b = token2.percentChange?.[timePeriod]?.value
break
}
return sortFn(a, b)
}),
[tokenData, sortDirection, sortCategory, sortFn, timePeriod]
)
}
function NoTokensState({ message }: { message: ReactNode }) {
return (
<GridContainer>
@@ -149,51 +61,48 @@ export function LoadingTokenTable() {
)
}
export default function TokenTable({ data }: { data: TokenData[] | undefined }) {
export default function TokenTable() {
const showFavorites = useAtomValue<boolean>(showFavoritesAtom)
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
const filteredTokens = useFilteredTokens(data)
const sortedFilteredTokens = useSortedTokens(filteredTokens)
// TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
const prefetchedTokens = usePrefetchTopTokens()
const { loading, tokens, loadMoreTokens } = useTopTokens(prefetchedTokens)
/* loading and error state */
if (data === null) {
return (
<NoTokensState
message={
<>
<AlertTriangle size={16} />
<Trans>An error occured loading tokens. Please try again.</Trans>
</>
}
/>
)
if (loading) {
return <LoadingTokenTable />
} else {
if (!tokens) {
return (
<NoTokensState
message={
<>
<AlertTriangle size={16} />
<Trans>An error occured loading tokens. Please try again.</Trans>
</>
}
/>
)
} else if (tokens?.length === 0) {
return showFavorites ? (
<NoTokensState message={<Trans>You have no favorited tokens</Trans>} />
) : (
<NoTokensState message={<Trans>No tokens found</Trans>} />
)
} else {
return (
<>
<GridContainer>
<HeaderRow />
<TokenRowsContainer>
{tokens?.map((token, index) => (
<LoadedRow key={token?.name} tokenListIndex={index} tokenListLength={tokens.length} token={token} />
))}
</TokenRowsContainer>
</GridContainer>
<button onClick={loadMoreTokens}>load more</button>
</>
)
}
}
if (showFavorites && sortedFilteredTokens?.length === 0) {
return <NoTokensState message={<Trans>You have no favorited tokens</Trans>} />
}
if (!showFavorites && sortedFilteredTokens?.length === 0) {
return <NoTokensState message={<Trans>No tokens found</Trans>} />
}
return (
<Suspense fallback={<LoadingTokenTable />}>
<GridContainer>
<HeaderRow />
<TokenRowsContainer>
{sortedFilteredTokens?.map((token, index) => (
<LoadedRow
key={token.address}
tokenAddress={token.address}
tokenListIndex={index}
tokenListLength={sortedFilteredTokens.length}
tokenData={token}
timePeriod={timePeriod}
/>
))}
</TokenRowsContainer>
</GridContainer>
</Suspense>
)
}

View File

@@ -3,12 +3,13 @@ import { Link } from 'react-router-dom'
import { useShowTokensPromoBanner } from 'state/user/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { opacify } from 'theme/utils'
import { Z_INDEX } from 'theme/zIndex'
import tokensPromoDark from '../../assets/images/tokensPromoDark.png'
import tokensPromoLight from '../../assets/images/tokensPromoLight.png'
const PopupContainer = styled.div<{ show: boolean }>`
position: absolute;
position: fixed;
display: ${({ show }) => (show ? 'flex' : 'none')};
flex-direction: column;
padding: 12px 16px 12px 20px;
@@ -17,7 +18,7 @@ const PopupContainer = styled.div<{ show: boolean }>`
right: 16px;
width: 320px;
height: 88px;
z-index: 5;
z-index: ${Z_INDEX.sticky};
background-color: ${({ theme }) => (theme.darkMode ? theme.backgroundScrim : opacify(60, '#FDF0F8'))};
color: ${({ theme }) => theme.textPrimary};
border: 1px solid ${({ theme }) => theme.backgroundOutline};
@@ -32,7 +33,7 @@ const PopupContainer = styled.div<{ show: boolean }>`
theme: {
transition: { duration, timing },
},
}) => `${duration.slow}ms opacity ${timing.in}`};
}) => `${duration.slow} opacity ${timing.in}`};
`
const Header = styled.div`
display: flex;

View File

@@ -1,49 +1,56 @@
import { SupportedChainId } from 'constants/chains'
import { TimePeriod } from 'graphql/data/TopTokenQuery'
import { TokenSortMethod } from 'graphql/data/TopTokens'
import { TimePeriod } from 'graphql/data/util'
import { atom, useAtom } from 'jotai'
import { atomWithReset, atomWithStorage } from 'jotai/utils'
import { useCallback } from 'react'
import { Category, SortDirection } from './types'
import { atomWithReset, atomWithStorage, useAtomValue } from 'jotai/utils'
import { useCallback, useMemo } from 'react'
export const favoritesAtom = atomWithStorage<string[]>('favorites', [])
export const showFavoritesAtom = atomWithStorage<boolean>('showFavorites', false)
export const filterStringAtom = atomWithReset<string>('')
export const filterNetworkAtom = atom<SupportedChainId>(SupportedChainId.MAINNET)
export const filterTimeAtom = atom<TimePeriod>(TimePeriod.DAY)
export const sortCategoryAtom = atom<Category>(Category.marketCap)
export const sortDirectionAtom = atom<SortDirection>(SortDirection.decreasing)
export const sortMethodAtom = atom<TokenSortMethod>(TokenSortMethod.TOTAL_VALUE_LOCKED)
export const sortAscendingAtom = atom<boolean>(false)
/* for favoriting tokens */
export function useToggleFavorite(tokenAddress: string) {
export function useToggleFavorite(tokenAddress: string | undefined | null) {
const [favoriteTokens, updateFavoriteTokens] = useAtom(favoritesAtom)
return useCallback(() => {
if (!tokenAddress) return
let updatedFavoriteTokens
if (favoriteTokens.includes(tokenAddress)) {
if (favoriteTokens.includes(tokenAddress.toLocaleLowerCase())) {
updatedFavoriteTokens = favoriteTokens.filter((address: string) => {
return address !== tokenAddress
return address !== tokenAddress.toLocaleLowerCase()
})
} else {
updatedFavoriteTokens = [...favoriteTokens, tokenAddress]
updatedFavoriteTokens = [...favoriteTokens, tokenAddress.toLocaleLowerCase()]
}
updateFavoriteTokens(updatedFavoriteTokens)
}, [favoriteTokens, tokenAddress, updateFavoriteTokens])
}
/* keep track of sort category for token table */
export function useSetSortCategory(category: Category) {
const [sortCategory, setSortCategory] = useAtom(sortCategoryAtom)
const [sortDirection, setDirectionCategory] = useAtom(sortDirectionAtom)
export function useSetSortMethod(newSortMethod: TokenSortMethod) {
const [sortMethod, setSortMethod] = useAtom(sortMethodAtom)
const [sortAscending, setSortAscending] = useAtom(sortAscendingAtom)
return useCallback(() => {
if (category === sortCategory) {
const oppositeDirection =
sortDirection === SortDirection.increasing ? SortDirection.decreasing : SortDirection.increasing
setDirectionCategory(oppositeDirection)
if (sortMethod === newSortMethod) {
setSortAscending(!sortAscending)
} else {
setSortCategory(category)
setDirectionCategory(SortDirection.decreasing)
setSortMethod(newSortMethod)
setSortAscending(false)
}
}, [category, sortCategory, setSortCategory, sortDirection, setDirectionCategory])
}, [sortMethod, setSortMethod, setSortAscending, sortAscending, newSortMethod])
}
export function useIsFavorited(tokenAddress: string | null | undefined) {
const favoritedTokens = useAtomValue<string[]>(favoritesAtom)
return useMemo(
() => (tokenAddress ? favoritedTokens.includes(tokenAddress.toLocaleLowerCase()) : false),
[favoritedTokens, tokenAddress]
)
}

View File

@@ -1,10 +0,0 @@
export enum Category {
percentChange = 'Change',
marketCap = 'Market Cap',
price = 'Price',
volume = 'Volume',
}
export enum SortDirection {
increasing = 'Increasing',
decreasing = 'Decreasing',
}

View File

@@ -1,16 +1,23 @@
import { useWeb3React } from '@web3-react/core'
import AddressClaimModal from 'components/claim/AddressClaimModal'
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
import TokensBanner from 'components/Tokens/TokensBanner'
import { TokensVariant, useTokensFlag } from 'featureFlags/flags/tokens'
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
import { lazy } from 'react'
import { useLocation } from 'react-router-dom'
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
const Cart = lazy(() => import('nft/components/sell/modal/ListingTag'))
export default function TopLevelModals() {
const addressClaimOpen = useModalIsOpen(ApplicationModal.ADDRESS_CLAIM)
const addressClaimToggle = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
const blockedAccountModalOpen = useModalIsOpen(ApplicationModal.BLOCKED_ACCOUNT)
const { account } = useWeb3React()
const location = useLocation()
useAccountRiskCheck(account)
const open = Boolean(blockedAccountModalOpen && account)
@@ -18,6 +25,9 @@ export default function TopLevelModals() {
<>
<AddressClaimModal isOpen={addressClaimOpen} onDismiss={addressClaimToggle} />
<ConnectedAccountBlocked account={account} isOpen={open} />
{useTokensFlag() === TokensVariant.Enabled &&
(location.pathname.includes('/pool') || location.pathname.includes('/swap')) && <TokensBanner />}
<Cart />
</>
)
}

View File

@@ -132,7 +132,12 @@ const AuthenticatedHeader = () => {
<IconContainer>
<IconButton onClick={copy} Icon={Copy} text={isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>} />
<IconButton href={`${explorer}address/${account}`} Icon={ExternalLink} text={<Trans>Explore</Trans>} />
<IconButton onClick={disconnect} Icon={Power} text={<Trans>Disconnect</Trans>} />
<IconButton
dataTestId="wallet-disconnect"
onClick={disconnect}
Icon={Power}
text={<Trans>Disconnect</Trans>}
/>
</IconContainer>
</HeaderWrapper>
<Column>

View File

@@ -55,7 +55,7 @@ const ToggleMenuItem = styled.button`
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms all ${timing.in}`};
}) => `${duration.fast} all ${timing.in}`};
}
`
@@ -114,11 +114,13 @@ const WalletDropdown = ({ setMenu }: { setMenu: (state: MenuState) => void }) =>
{isAuthenticated ? (
<AuthenticatedHeader />
) : (
<ConnectButton onClick={toggleWalletModal}>Connect wallet</ConnectButton>
<ConnectButton data-testid="wallet-connect-wallet" onClick={toggleWalletModal}>
Connect wallet
</ConnectButton>
)}
<Divider />
{isAuthenticated && (
<ToggleMenuItem onClick={() => setMenu(MenuState.TRANSACTIONS)}>
<ToggleMenuItem data-testid="wallet-transactions" onClick={() => setMenu(MenuState.TRANSACTIONS)}>
<DefaultText>
<Trans>Transactions</Trans>{' '}
{pendingTransactions.length > 0 && (
@@ -132,7 +134,7 @@ const WalletDropdown = ({ setMenu }: { setMenu: (state: MenuState) => void }) =>
</IconWrap>
</ToggleMenuItem>
)}
<ToggleMenuItem onClick={() => setMenu(MenuState.LANGUAGE)}>
<ToggleMenuItem data-testid="wallet-select-language" onClick={() => setMenu(MenuState.LANGUAGE)}>
<DefaultText>
<Trans>Language</Trans>
</DefaultText>
@@ -145,7 +147,7 @@ const WalletDropdown = ({ setMenu }: { setMenu: (state: MenuState) => void }) =>
</IconWrap>
</FlexContainer>
</ToggleMenuItem>
<ToggleMenuItem onClick={toggleDarkMode}>
<ToggleMenuItem data-testid="wallet-select-theme" onClick={toggleDarkMode}>
<DefaultText>{darkMode ? <Trans> Light theme</Trans> : <Trans>Dark theme</Trans>}</DefaultText>
<IconWrap>{darkMode ? <Sun size={16} /> : <Moon size={16} />}</IconWrap>
</ToggleMenuItem>

View File

@@ -28,7 +28,7 @@ const IconStyles = css`
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms background-color ${timing.in}`};
}) => `${duration.fast} background-color ${timing.in}`};
${IconHoverText} {
opacity: 1;
@@ -64,18 +64,19 @@ interface IconButtonProps {
Icon: Icon
onClick?: () => void
href?: string
dataTestId?: string
}
const IconButton = ({ Icon, onClick, text, href }: IconButtonProps) => {
const IconButton = ({ Icon, onClick, text, href, dataTestId }: IconButtonProps) => {
return href ? (
<IconBlockLink href={href} target="_blank">
<IconBlockLink data-testId={dataTestId} href={href} target="_blank">
<IconWrapper>
<Icon strokeWidth={1.5} size={16} />
<IconHoverText>{text}</IconHoverText>
</IconWrapper>
</IconBlockLink>
) : (
<IconBlockButton onClick={onClick}>
<IconBlockButton data-testId={dataTestId} onClick={onClick}>
<IconWrapper>
<Icon strokeWidth={1.5} size={16} />
<IconHoverText>{text}</IconHoverText>

View File

@@ -33,7 +33,7 @@ const InternalLinkMenuItem = styled(InternalMenuItem)`
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms background-color ${timing.in}`};
}) => `${duration.fast} background-color ${timing.in}`};
}
`
@@ -45,7 +45,7 @@ function LanguageMenuItem({ locale, isActive }: { locale: SupportedLocale; isAct
return (
<InternalLinkMenuItem onClick={onClick} to={to}>
<Text fontSize={16} fontWeight={400} lineHeight="24px">
<Text data-testid="wallet-language-item" fontSize={16} fontWeight={400} lineHeight="24px">
{LOCALE_LABEL[locale]}
</Text>
{isActive && <Check color={theme.accentActive} opacity={1} size={20} />}

View File

@@ -42,12 +42,12 @@ const ClearAll = styled.div`
margin-bottom: auto;
:hover {
opacity: 0.6;
opacity: ${({ theme }) => theme.opacity.hover};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms opacity ${timing.in}`};
}) => `${duration.fast} opacity ${timing.in}`};
}
`
@@ -60,7 +60,7 @@ const StyledChevron = styled(ChevronLeft)`
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms color ${timing.in}`};
}) => `${duration.fast} color ${timing.in}`};
}
`
@@ -101,8 +101,8 @@ export const SlideOutMenu = ({
<Menu>
<BackSection>
<BackSectionContainer>
<StyledChevron onClick={onClose} size={24} />
<Header>{title}</Header>
<StyledChevron data-testid="wallet-back" onClick={onClose} size={24} />
<Header data-testid="wallet-header">{title}</Header>
{onClear && <ClearAll onClick={onClear}>Clear All</ClearAll>}
</BackSectionContainer>
</BackSection>

View File

@@ -158,7 +158,7 @@ export const TransactionHistoryMenu = ({ onClose }: { onClose: () => void }) =>
))}
</>
) : (
<EmptyTransaction>
<EmptyTransaction data-testid="wallet-empty-transaction-text">
<Trans>Your transactions will appear here</Trans>
</EmptyTransaction>
)}

View File

@@ -1,6 +1,6 @@
import { useState } from 'react'
import styled from 'styled-components/macro'
import { Z_INDEX } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import { useModalIsOpen } from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer'
@@ -37,8 +37,8 @@ export enum MenuState {
}
const WalletDropdownWrapper = styled.div`
position: absolute;
top: 65px;
position: fixed;
top: 72px;
right: 20px;
z-index: ${Z_INDEX.dropdown};

View File

@@ -1,5 +1,5 @@
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import React from 'react'
import { Check } from 'react-feather'

View File

@@ -1,13 +1,14 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { Connector } from '@web3-react/types'
import { sendAnalyticsEvent, user } from 'components/AmplitudeAnalytics'
import { CUSTOM_USER_PROPERTIES, EventName, WALLET_CONNECTION_RESULT } from 'components/AmplitudeAnalytics/constants'
import { sendAnalyticsEvent, user } from 'analytics'
import { CUSTOM_USER_PROPERTIES, EventName, WALLET_CONNECTION_RESULT } from 'analytics/constants'
import { sendEvent } from 'components/analytics'
import { AutoColumn } from 'components/Column'
import { AutoRow } from 'components/Row'
import { getConnection, getConnectionName, getIsCoinbaseWallet, getIsInjected, getIsMetaMask } from 'connection/utils'
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import usePrevious from 'hooks/usePrevious'
import { useCallback, useEffect, useState } from 'react'
import { ArrowLeft } from 'react-feather'
import { updateConnectionError } from 'state/connection/reducer'
@@ -35,7 +36,7 @@ const CloseIcon = styled.div`
top: 14px;
&:hover {
cursor: pointer;
opacity: 0.6;
opacity: ${({ theme }) => theme.opacity.hover};
}
`
@@ -149,6 +150,8 @@ export default function WalletModal({
}) {
const dispatch = useAppDispatch()
const { connector, account, chainId } = useWeb3React()
const previousAccount = usePrevious(account)
const [connectedWallets, addWalletToConnectedWallets] = useConnectedWallets()
const redesignFlag = useRedesignFlag()
@@ -174,6 +177,12 @@ export default function WalletModal({
}
}, [walletModalOpen, setWalletView, account])
useEffect(() => {
if (account && account !== previousAccount && walletModalOpen) {
toggleWalletModal()
}
}, [account, previousAccount, toggleWalletModal, walletModalOpen])
useEffect(() => {
if (pendingConnector && walletView !== WALLET_VIEWS.PENDING) {
updateConnectionError({ connectionType: getConnection(pendingConnector).type, error: undefined })
@@ -333,7 +342,7 @@ export default function WalletModal({
return (
<UpperSection>
<CloseIcon onClick={toggleWalletModal}>
<CloseIcon data-testid="wallet-modal-close" onClick={toggleWalletModal}>
<CloseColor />
</CloseIcon>
{headerRow}
@@ -363,7 +372,9 @@ export default function WalletModal({
maxHeight={90}
redesignFlag={redesignFlagEnabled}
>
<Wrapper redesignFlag={redesignFlagEnabled}>{getModalContent()}</Wrapper>
<Wrapper data-testid="wallet-modal" redesignFlag={redesignFlagEnabled}>
{getModalContent()}
</Wrapper>
</Modal>
)
}

View File

@@ -1,10 +1,13 @@
import { Web3ReactHooks, Web3ReactProvider } from '@web3-react/core'
import { SupportedChainId } from '@uniswap/widgets'
import { useWeb3React, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core'
import { Connector } from '@web3-react/types'
import { Connection } from 'connection'
import { getConnectionName } from 'connection/utils'
import { RPC_PROVIDERS } from 'constants/providers'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import useEagerlyConnect from 'hooks/useEagerlyConnect'
import useOrderedConnections from 'hooks/useOrderedConnections'
import { ReactNode, useMemo } from 'react'
import { ReactNode, useEffect, useMemo } from 'react'
export default function Web3Provider({ children }: { children: ReactNode }) {
useEagerlyConnect()
@@ -15,7 +18,37 @@ export default function Web3Provider({ children }: { children: ReactNode }) {
return (
<Web3ReactProvider connectors={connectors} key={key}>
<Tracer />
{children}
</Web3ReactProvider>
)
}
function Tracer() {
const { chainId, provider } = useWeb3React()
const networkProvider = RPC_PROVIDERS[(chainId || SupportedChainId.MAINNET) as SupportedChainId]
const shouldTrace = useTraceJsonRpcFlag() === TraceJsonRpcVariant.Enabled
useEffect(() => {
if (shouldTrace) {
provider?.on('debug', trace)
if (provider !== networkProvider) {
networkProvider?.on('debug', trace)
}
}
return () => {
provider?.off('debug', trace)
networkProvider?.off('debug', trace)
}
}, [networkProvider, provider, shouldTrace])
return null
}
function trace(event: any) {
if (event.action !== 'request') return
const { method, id, params } = event.request
console.groupCollapsed(method, id)
console.debug(params)
console.groupEnd()
}

View File

@@ -1,9 +1,8 @@
// eslint-disable-next-line no-restricted-imports
import { t, Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { StyledChevronDown, StyledChevronUp } from 'components/Icons'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import WalletDropdown from 'components/WalletDropdown'
import { getConnection } from 'connection/utils'
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
@@ -11,7 +10,7 @@ import { Portal } from 'nft/components/common/Portal'
import { getIsValidSwapQuote } from 'pages/Swap'
import { darken } from 'polished'
import { useMemo, useRef } from 'react'
import { AlertTriangle } from 'react-feather'
import { AlertTriangle, ChevronDown, ChevronUp } from 'react-feather'
import { useAppSelector } from 'state/hooks'
import { useDerivedSwapInfo } from 'state/swap/hooks'
import styled, { css, useTheme } from 'styled-components/macro'
@@ -34,12 +33,15 @@ import Loader from '../Loader'
import { RowBetween } from '../Row'
import WalletModal from '../WalletModal'
// https://stackoverflow.com/a/31617326
const FULL_BORDER_RADIUS = 9999
const Web3StatusGeneric = styled(ButtonSecondary)`
${({ theme }) => theme.flexRowNoWrap}
width: 100%;
align-items: center;
padding: 0.5rem;
border-radius: 14px;
border-radius: ${FULL_BORDER_RADIUS}px;
cursor: pointer;
user-select: none;
height: 36px;
@@ -60,15 +62,15 @@ const Web3StatusError = styled(Web3StatusGeneric)`
}
`
const Web3StatusConnectNavbar = styled.button<{ faded?: boolean }>`
dispay: flex;
align-items: center;
const Web3StatusConnectButton = styled.button<{ faded?: boolean }>`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
background-color: ${({ theme }) => theme.accentActionSoft};
border-radius: 12px;
border-radius: ${FULL_BORDER_RADIUS}px;
border: none;
cursor: pointer;
padding: 8px 12px;
padding: 0 12px;
height: 40px;
:hover,
:active,
@@ -171,10 +173,15 @@ const StyledConnect = styled.div`
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms color ${timing.in}`};
}) => `${duration.fast} color ${timing.in}`};
}
`
const CHEVRON_PROPS = {
height: 20,
width: 20,
}
function Web3StatusInner() {
const { account, connector, chainId, ENSName } = useWeb3React()
const connectionType = getConnection(connector).type
@@ -216,9 +223,13 @@ function Web3StatusInner() {
</Web3StatusError>
)
} else if (account) {
const chevronProps = {
...CHEVRON_PROPS,
color: theme.textSecondary,
}
return (
<Web3StatusConnected data-testid="web3-status-connected" onClick={toggleWallet} pending={hasPendingTransactions}>
{navbarFlagEnabled && !hasPendingTransactions && <StatusIcon connectionType={connectionType} />}
{navbarFlagEnabled && !hasPendingTransactions && <StatusIcon size={24} connectionType={connectionType} />}
{hasPendingTransactions ? (
<RowBetween>
<Text>
@@ -232,9 +243,9 @@ function Web3StatusInner() {
<Text>{ENSName || shortenAddress(account)}</Text>
{navbarFlagEnabled ? (
walletIsOpen ? (
<StyledChevronUp onClick={toggleWalletDropdown} />
<ChevronUp {...chevronProps} />
) : (
<StyledChevronDown onClick={toggleWalletDropdown} />
<ChevronDown {...chevronProps} />
)
) : null}
</>
@@ -243,6 +254,12 @@ function Web3StatusInner() {
</Web3StatusConnected>
)
} else {
const chevronProps = {
...CHEVRON_PROPS,
color: theme.accentAction,
'data-testid': 'navbar-wallet-dropdown',
onClick: toggleWalletDropdown,
}
return (
<TraceEvent
events={[Event.onClick]}
@@ -251,17 +268,13 @@ function Web3StatusInner() {
element={ElementName.CONNECT_WALLET_BUTTON}
>
{navbarFlagEnabled ? (
<Web3StatusConnectNavbar faded={!account}>
<StyledConnect onClick={toggleWalletModal}>
<Web3StatusConnectButton faded={!account}>
<StyledConnect data-testid="navbar-connect-wallet" onClick={toggleWalletModal}>
<Trans>Connect</Trans>
</StyledConnect>
<VerticalDivider />
{walletIsOpen ? (
<StyledChevronUp customColor={theme.accentAction} onClick={toggleWalletDropdown} />
) : (
<StyledChevronDown customColor={theme.accentAction} onClick={toggleWalletDropdown} />
)}
</Web3StatusConnectNavbar>
{walletIsOpen ? <ChevronUp {...chevronProps} /> : <ChevronDown {...chevronProps} />}
</Web3StatusConnectButton>
) : (
<Web3StatusConnect onClick={toggleWallet} faded={!account}>
<Text>

View File

@@ -1,6 +1,6 @@
import { Currency, SwapWidget } from '@uniswap/widgets'
import { Currency, OnReviewSwapClick, SwapWidget } from '@uniswap/widgets'
import { useWeb3React } from '@web3-react/core'
import { RPC_URLS } from 'constants/networks'
import { RPC_PROVIDERS } from 'constants/providers'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { useMemo } from 'react'
import { useIsDarkMode } from 'state/user/hooks'
@@ -16,9 +16,10 @@ const WIDGET_ROUTER_URL = 'https://api.uniswap.org/v1/'
export interface WidgetProps {
defaultToken?: Currency
onReviewSwapClick?: OnReviewSwapClick
}
export default function Widget({ defaultToken }: WidgetProps) {
export default function Widget({ defaultToken, onReviewSwapClick }: WidgetProps) {
const locale = useActiveLocale()
const darkMode = useIsDarkMode()
const theme = useMemo(() => (darkMode ? DARK_THEME : LIGHT_THEME), [darkMode])
@@ -33,11 +34,12 @@ export default function Widget({ defaultToken }: WidgetProps) {
<SwapWidget
disableBranding
hideConnectionUI
jsonRpcUrlMap={RPC_URLS}
jsonRpcUrlMap={RPC_PROVIDERS}
routerUrl={WIDGET_ROUTER_URL}
width={WIDGET_WIDTH}
locale={locale}
theme={theme}
onReviewSwapClick={onReviewSwapClick}
// defaultChainId is excluded - it is always inferred from the passed provider
provider={provider}
{...inputs}

View File

@@ -1,6 +1,8 @@
import { Currency, Field, SwapController, SwapEventHandlers, TradeType } from '@uniswap/widgets'
import CurrencySearchModal from 'components/SearchModal/CurrencySearchModal'
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
const EMPTY_AMOUNT = ''
/**
* Integrates the Widget's inputs.
@@ -8,7 +10,7 @@ import { useCallback, useMemo, useState } from 'react'
*/
export function useSyncWidgetInputs(defaultToken?: Currency) {
const [type, setType] = useState(TradeType.EXACT_INPUT)
const [amount, setAmount] = useState('')
const [amount, setAmount] = useState(EMPTY_AMOUNT)
const onAmountChange = useCallback((field: Field, amount: string) => {
setType(toTradeType(field))
setAmount(amount)
@@ -17,6 +19,14 @@ export function useSyncWidgetInputs(defaultToken?: Currency) {
const [tokens, setTokens] = useState<{ [Field.INPUT]?: Currency; [Field.OUTPUT]?: Currency }>({
[Field.OUTPUT]: defaultToken,
})
useEffect(() => {
setTokens({
[Field.OUTPUT]: defaultToken,
})
setAmount(EMPTY_AMOUNT)
}, [defaultToken])
const onSwitchTokens = useCallback(() => {
setType((type) => invertTradeType(type))
setTokens((tokens) => ({

View File

@@ -1,4 +1,4 @@
import { TransactionResponse } from '@ethersproject/providers'
import type { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
import { useWeb3React } from '@web3-react/core'

View File

@@ -1,4 +1,4 @@
import { TransactionResponse } from '@ethersproject/providers'
import type { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'

View File

@@ -1,4 +1,4 @@
import { TransactionResponse } from '@ethersproject/providers'
import type { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
import { useWeb3React } from '@web3-react/core'

View File

@@ -1,10 +1,14 @@
import { Trans } from '@lingui/macro'
import { Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { ModalName } from 'components/AmplitudeAnalytics/constants'
import { Trace } from 'components/AmplitudeAnalytics/Trace'
import { ReactNode, useCallback, useMemo, useState } from 'react'
import { sendAnalyticsEvent } from 'analytics'
import { ModalName } from 'analytics/constants'
import { EventName } from 'analytics/constants'
import { Trace } from 'analytics/Trace'
import { formatPercentInBasisPointsNumber, formatToDecimal, getTokenAddress } from 'analytics/utils'
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { computeRealizedPriceImpact } from 'utils/prices'
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
import TransactionConfirmationModal, {
@@ -14,6 +18,27 @@ import TransactionConfirmationModal, {
import SwapModalFooter from './SwapModalFooter'
import SwapModalHeader from './SwapModalHeader'
const formatAnalyticsEventProperties = ({
trade,
txHash,
}: {
trade: InterfaceTrade<Currency, Currency, TradeType>
txHash: string
}) => ({
transaction_hash: txHash,
token_in_address: getTokenAddress(trade.inputAmount.currency),
token_out_address: getTokenAddress(trade.outputAmount.currency),
token_in_symbol: trade.inputAmount.currency.symbol,
token_out_symbol: trade.outputAmount.currency.symbol,
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
chain_id:
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
? trade.inputAmount.currency.chainId
: undefined,
})
export default function ConfirmSwapModal({
trade,
originalTrade,
@@ -48,6 +73,7 @@ export default function ConfirmSwapModal({
// shouldLogModalCloseEvent lets the child SwapModalHeader component know when modal has been closed
// and an event triggered by modal closing should be logged.
const [shouldLogModalCloseEvent, setShouldLogModalCloseEvent] = useState(false)
const [lastTxnHashLogged, setLastTxnHashLogged] = useState<string | null>(null)
const showAcceptChanges = useMemo(
() => Boolean(trade && originalTrade && tradeMeaningfullyDiffers(trade, originalTrade)),
[originalTrade, trade]
@@ -121,8 +147,15 @@ export default function ConfirmSwapModal({
[onModalDismiss, modalBottom, modalHeader, swapErrorMessage]
)
useEffect(() => {
if (!attemptingTxn && isOpen && txHash && trade && txHash !== lastTxnHashLogged) {
sendAnalyticsEvent(EventName.SWAP_SIGNED, formatAnalyticsEventProperties({ trade, txHash }))
setLastTxnHashLogged(txHash)
}
}, [attemptingTxn, isOpen, txHash, trade, lastTxnHashLogged])
return (
<Trace modal={ModalName.CONFIRM_SWAP} shouldLogImpression={isOpen}>
<Trace modal={ModalName.CONFIRM_SWAP}>
<TransactionConfirmationModal
isOpen={isOpen}
onDismiss={onModalDismiss}

View File

@@ -1,8 +1,8 @@
import { Trans } from '@lingui/macro'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import AnimatedDropdown from 'components/AnimatedDropdown'
import Card, { OutlineCard } from 'components/Card'
import { AutoColumn } from 'components/Column'

View File

@@ -1,7 +1,7 @@
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import {
formatPercentInBasisPointsNumber,
formatPercentNumber,
@@ -9,7 +9,7 @@ import {
getDurationFromDateMilliseconds,
getDurationUntilTimestampSeconds,
getTokenAddress,
} from 'components/AmplitudeAnalytics/utils'
} from 'analytics/utils'
import useTransactionDeadline from 'hooks/useTransactionDeadline'
import { ReactNode } from 'react'
import { Text } from 'rebass'
@@ -132,7 +132,7 @@ export default function SwapModalFooter({
<TraceEvent
events={[Event.onClick]}
element={ElementName.CONFIRM_SWAP_BUTTON}
name={EventName.SWAP_SUBMITTED}
name={EventName.SWAP_SUBMITTED_BUTTON_CLICKED}
properties={formatAnalyticsEventProperties({
trade,
hash,

View File

@@ -1,9 +1,9 @@
import { Trans } from '@lingui/macro'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Price } from '@uniswap/sdk-core'
import { sendAnalyticsEvent } from 'components/AmplitudeAnalytics'
import { EventName, SWAP_PRICE_UPDATE_USER_RESPONSE } from 'components/AmplitudeAnalytics/constants'
import { formatPercentInBasisPointsNumber } from 'components/AmplitudeAnalytics/utils'
import { sendAnalyticsEvent } from 'analytics'
import { EventName, SWAP_PRICE_UPDATE_USER_RESPONSE } from 'analytics/constants'
import { formatPercentInBasisPointsNumber } from 'analytics/utils'
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import { useEffect, useState } from 'react'
import { AlertTriangle, ArrowDown } from 'react-feather'

View File

@@ -4,8 +4,8 @@ import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { FeeAmount } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import AnimatedDropdown from 'components/AnimatedDropdown'
import { AutoColumn } from 'components/Column'
import { LoadingRows } from 'components/Loader/styled'

View File

@@ -9,7 +9,8 @@ import Modal from 'components/Modal'
import { AutoRow, RowBetween } from 'components/Row'
import { useState } from 'react'
import styled from 'styled-components/macro'
import { CloseIcon, ExternalLink, ThemedText, Z_INDEX } from 'theme'
import { CloseIcon, ExternalLink, ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import { useUnsupportedTokens } from '../../hooks/Tokens'
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'

View File

@@ -4,7 +4,7 @@ import { ReactNode } from 'react'
import { AlertTriangle } from 'react-feather'
import { Text } from 'rebass'
import styled, { css } from 'styled-components/macro'
import { Z_INDEX } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import { AutoColumn } from '../Column'

Some files were not shown because too many files have changed in this diff Show More