Compare commits

...

101 Commits

Author SHA1 Message Date
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
Yadong Zhang
eb725f51ce fix: handled liquidity minPrice and maxPrice are empty case. (#4569)
thanks!
2022-09-04 18:37:42 -05:00
lynn
4d4462368b fix: add back usd volume to swap submitted from ui instead of infura hook (#4563)
* init

* typo
2022-09-02 14:56:05 -04:00
lynn
6fe5d4363d fix: remove warning icon on search (#4564)
init
2022-09-02 14:55:50 -04:00
Jordan Frankfurt
b46fa27084 feat: disable branding on swap widget (#4561)
* feat: disable branding on swap widget:

* pr feedback
2022-09-02 10:03:06 -05:00
lynn
ade2440613 fix: missing logo redesign (#4535)
* fix: logos

* different font sizings for diff token sizes from fred

* remove missing symbol

* fix

* fixes to comments

* fix

* use calc instead of fn

* add comment
2022-09-01 17:51:43 -04:00
Jordan Frankfurt
4dc4620b60 feat: integrate widget tx states (#4553)
* feat(widget): sync transaction states

* s

* waiting on type release

* slippage is all that remains

* finalize tx integration

* pr feedback

* pr feedback - else if

* update @uniswap/widgets to 2.7

* add slippage tolerance from transaction.info
2022-09-01 13:17:39 -05:00
aballerr
202c2662f1 fix: Adding socks icon to users profile icon (#4545)
* Adding icon for socks owner for p0 redesign


Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-01 14:08:49 -04:00
Charles Bachmeier
d2afd71c81 fix: change from flex to inline to fix safari bugs (#4559)
change from flex to inline to fix safari bugs

Co-authored-by: Charlie <charlie@uniswap.org>
2022-09-01 08:29:46 -07:00
aballerr
bad1ce2618 fix: Prod has chevron (#4558)
Remove chevron from prod

Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-09-01 10:20:09 -04:00
Vignesh Mohankumar
f194845b2b chore: remove theme.blue200 (#4533)
* chore: remove theme.blue200

* favorite button changes
2022-08-31 20:13:21 -04:00
Jack Short
98d4e108e6 fix: search icon bug (#4556) 2022-08-31 19:35:06 -04:00
lynn
ab43ed1900 fix: connect wallet button chevron styling fixes (#4554)
* fix

* simplify
2022-08-31 18:16:42 -04:00
Greg Bugyis
b147e047a5 fix: Bump height on LineNumberCell and pass header boolean (#4548) 2022-08-31 20:36:23 +03:00
Charles Bachmeier
bbb616f56c feat: auto set cursor on searchbar opening (#4552)
* auto set cursor on searchbar opening

* comment update

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-31 10:33:51 -07:00
Charles Bachmeier
35a429ea65 feat: highlight first result by default (#4551)
highlight first result by default

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-31 10:33:33 -07:00
Zach Pomerantz
bd16543c10 build: upgrade @uniswap/widgets to 2.5.0 (#4546)
* build: rm d3-curve-circlecorners entry

* build: upgrade @uniswap/widgets package

* fix: widget input typing
2022-08-31 10:29:50 -07:00
Charles Bachmeier
cbdeae276e feat: adjust bottom navbar padding (#4550)
adjust bottom navbar padding

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-31 09:57:42 -07:00
lynn
e733113963 fix: modify chart axis to to match crosshairs in year view (#4547)
* fix

* simplify

* refactor

* move logic to monthTickFormatter

* time to date string
2022-08-31 11:31:33 -04:00
aballerr
272b030b89 fix: Fixing border overlap and reducing button size (#4537)
* fixing styling on wallet border

Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-08-31 11:11:37 -04:00
Jack Short
472a553d13 fix: catch invalid address token details (#4529) 2022-08-30 17:25:59 -04:00
cartcrom
3a1bff146c feat: adding token safety article link (#4532)
updated article link
2022-08-30 17:24:23 -04:00
283 changed files with 10386 additions and 2462 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.3.1",
"@uniswap/widgets": "^2.8.1",
"@vanilla-extract/css": "^1.7.2",
"@vanilla-extract/css-utils": "^0.1.2",
"@vanilla-extract/dynamic": "^2.0.2",

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',

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

186
src/assets/svg/socks.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 13 KiB

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

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

@@ -24,11 +24,13 @@ const StyledNativeLogo = styled(StyledLogo)`
export default function CurrencyLogo({
currency,
symbol,
size = '24px',
style,
...rest
}: {
currency?: Currency | null
symbol?: string | null
size?: string
style?: React.CSSProperties
}) {
@@ -36,6 +38,7 @@ export default function CurrencyLogo({
alt: `${currency?.symbol ?? 'token'} logo`,
size,
srcs: useCurrencyLogoURIs(currency),
symbol: symbol ?? currency?.symbol,
style,
...rest,
}

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

@@ -0,0 +1,32 @@
import { ChevronDown, ChevronUp } from 'react-feather'
import styled from 'styled-components/macro'
export const StyledChevronDown = styled(ChevronDown)<{ customColor?: string }>`
color: ${({ theme, customColor }) => customColor ?? theme.textSecondary};
height: 20px;
width: 20px;
&:hover {
color: ${({ theme }) => theme.accentActionSoft};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.fast} color ${timing.in}`};
}
`
export const StyledChevronUp = styled(ChevronUp)<{ customColor?: string }>`
color: ${({ theme, customColor }) => customColor ?? theme.textSecondary};
height: 20px;
width: 20px;
&:hover {
color: ${({ theme }) => theme.accentActionSoft};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.fast} color ${timing.in}`};
}
`

View File

@@ -1,11 +1,17 @@
import { useWeb3React } from '@web3-react/core'
import { ConnectionType } from 'connection'
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
import useENSAvatar from 'hooks/useENSAvatar'
import styled from 'styled-components/macro'
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
import sockImg from '../../assets/svg/socks.svg'
import { useHasSocks } from '../../hooks/useSocksBalance'
import Identicon from '../Identicon'
const IconWrapper = styled.div<{ size?: number }>`
position: relative;
${({ theme }) => theme.flexColumnNoWrap};
align-items: center;
justify-content: center;
@@ -20,19 +26,55 @@ const IconWrapper = styled.div<{ size?: number }>`
`};
`
export default function StatusIcon({ connectionType, size }: { connectionType: ConnectionType; size?: number }) {
let image
switch (connectionType) {
case ConnectionType.INJECTED:
image = <Identicon />
break
case ConnectionType.WALLET_CONNECT:
image = <img src={WalletConnectIcon} alt="WalletConnect" />
break
case ConnectionType.COINBASE_WALLET:
image = <img src={CoinbaseWalletIcon} alt="Coinbase Wallet" />
break
const SockContainer = styled.div`
position: absolute;
display: flex;
justify-content: center;
border-radius: 50%;
width: 16px;
height: 16px;
bottom: -4px;
right: -4px;
`
const SockImg = styled.img`
width: 16px;
height: 16px;
`
const Socks = () => {
return (
<SockContainer>
<SockImg src={sockImg} />
</SockContainer>
)
}
const useIcon = (connectionType: ConnectionType) => {
const { account } = useWeb3React()
const { avatar } = useENSAvatar(account ?? undefined)
const isNavbarEnabled = useNavBarFlag() === NavBarVariant.Enabled
if ((isNavbarEnabled && avatar) || connectionType === ConnectionType.INJECTED) {
return <Identicon />
} else if (connectionType === ConnectionType.WALLET_CONNECT) {
return <img src={WalletConnectIcon} alt="WalletConnect" />
} else if (connectionType === ConnectionType.COINBASE_WALLET) {
return <img src={CoinbaseWalletIcon} alt="Coinbase Wallet" />
}
return <IconWrapper size={size ?? 16}>{image}</IconWrapper>
return undefined
}
export default function StatusIcon({ connectionType, size }: { connectionType: ConnectionType; size?: number }) {
const hasSocks = useHasSocks()
const isNavbarEnabled = useNavBarFlag() === NavBarVariant.Enabled
const icon = useIcon(connectionType)
return (
<IconWrapper size={size ?? 16}>
{isNavbarEnabled && hasSocks && <Socks />}
{icon}
</IconWrapper>
)
}

View File

@@ -1,12 +1,13 @@
import jazzicon from '@metamask/jazzicon'
import { useWeb3React } from '@web3-react/core'
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
import useENSAvatar from 'hooks/useENSAvatar'
import { useLayoutEffect, useMemo, useRef, useState } from 'react'
import styled from 'styled-components/macro'
const StyledIdenticon = styled.div`
height: 1rem;
width: 1rem;
const StyledIdenticon = styled.div<{ isNavbarEnabled: boolean }>`
height: ${({ isNavbarEnabled }) => (isNavbarEnabled ? '24px' : '1rem')};
width: ${({ isNavbarEnabled }) => (isNavbarEnabled ? '24px' : '1rem')};
border-radius: 1.125rem;
background-color: ${({ theme }) => theme.deprecated_bg4};
font-size: initial;
@@ -22,8 +23,10 @@ export default function Identicon() {
const { account } = useWeb3React()
const { avatar } = useENSAvatar(account ?? undefined)
const [fetchable, setFetchable] = useState(true)
const isNavbarEnabled = useNavBarFlag() === NavBarVariant.Enabled
const iconSize = isNavbarEnabled ? 24 : 16
const icon = useMemo(() => account && jazzicon(16, parseInt(account.slice(2, 10), 16)), [account])
const icon = useMemo(() => account && jazzicon(iconSize, parseInt(account.slice(2, 10), 16)), [account, iconSize])
const iconRef = useRef<HTMLDivElement>(null)
useLayoutEffect(() => {
const current = iconRef.current
@@ -41,7 +44,7 @@ export default function Identicon() {
}, [icon, iconRef])
return (
<StyledIdenticon>
<StyledIdenticon isNavbarEnabled={isNavbarEnabled}>
{avatar && fetchable ? (
<StyledAvatar alt="avatar" src={avatar} onError={() => setFetchable(false)}></StyledAvatar>
) : (

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

@@ -14,13 +14,15 @@ export default function ListLogo({
style,
size = '24px',
alt,
symbol,
}: {
logoURI: string
size?: string
style?: React.CSSProperties
alt?: string
symbol?: string
}) {
const srcs: string[] = useHttpLocations(logoURI)
return <StyledListLogo alt={alt} size={size} srcs={srcs} style={style} />
return <StyledListLogo alt={alt} size={size} symbol={symbol} srcs={srcs} style={style} />
}

View File

@@ -1,22 +1,34 @@
import { useState } from 'react'
import { Slash } from 'react-feather'
import { ImageProps } from 'rebass'
import { useTheme } from 'styled-components/macro'
import styled from 'styled-components/macro'
const BAD_SRCS: { [tokenAddress: string]: true } = {}
interface LogoProps extends Pick<ImageProps, 'style' | 'alt' | 'className'> {
srcs: string[]
symbol?: string
size?: string
}
const MissingImageLogo = styled.div<{ size?: string }>`
--size: ${({ size }) => size};
border-radius: 100px;
color: ${({ theme }) => theme.textPrimary};
background-color: ${({ theme }) => theme.backgroundInteractive};
font-size: calc(var(--size) / 3);
font-weight: 500;
height: ${({ size }) => size ?? '24px'};
line-height: ${({ size }) => size ?? '24px'};
text-align: center;
width: ${({ size }) => size ?? '24px'};
`
/**
* Renders an image by sequentially trying a list of URIs, and then eventually a fallback triangle alert
*/
export default function Logo({ srcs, alt, style, ...rest }: LogoProps) {
export default function Logo({ srcs, alt, style, size, symbol, ...rest }: LogoProps) {
const [, refresh] = useState<number>(0)
const theme = useTheme()
const src: string | undefined = srcs.find((src) => !BAD_SRCS[src])
if (src) {
@@ -34,5 +46,10 @@ export default function Logo({ srcs, alt, style, ...rest }: LogoProps) {
)
}
return <Slash {...rest} style={{ ...style, color: theme.deprecated_bg4 }} />
return (
<MissingImageLogo size={size}>
{/* use only first 3 characters of Symbol for design reasons */}
{symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
</MissingImageLogo>
)
}

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,4 +1,5 @@
import { useWeb3React } from '@web3-react/core'
import { StyledChevronDown, StyledChevronUp } from 'components/Icons'
import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
@@ -7,46 +8,16 @@ import useSyncChainQuery from 'hooks/useSyncChainQuery'
import { Box } from 'nft/components/Box'
import { Portal } from 'nft/components/common/Portal'
import { Column, Row } from 'nft/components/Flex'
import { CheckMarkIcon, NewChevronDownIcon, NewChevronUpIcon, TokenWarningRedIcon } from 'nft/components/icons'
import { TokenWarningRedIcon } from 'nft/components/icons'
import { subhead } from 'nft/css/common.css'
import { themeVars, vars } from 'nft/css/sprinkles.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { useIsMobile } from 'nft/hooks'
import { ReactNode, useReducer, useRef } from 'react'
import { useCallback, useRef, useState } from 'react'
import * as styles from './ChainSwitcher.css'
import * as styles from './ChainSelector.css'
import ChainSelectorRow from './ChainSelectorRow'
import { NavDropdown } from './NavDropdown'
const ChainRow = ({
targetChain,
onSelectChain,
}: {
targetChain: SupportedChainId
onSelectChain: (targetChain: number) => void
}) => {
const { chainId } = useWeb3React()
const active = chainId === targetChain
const { label, logoUrl } = getChainInfo(targetChain)
return (
<Column borderRadius="12">
<Row
as="button"
background="none"
className={`${styles.ChainSwitcherRow} ${subhead}`}
onClick={() => onSelectChain(targetChain)}
>
<ChainDetails>
<img src={logoUrl} alt={label} className={styles.Icon} />
{label}
</ChainDetails>
{active && <CheckMarkIcon width={20} height={20} color={vars.color.blue400} />}
</Row>
</Column>
)
}
const ChainDetails = ({ children }: { children: ReactNode }) => <Row>{children}</Row>
const NETWORK_SELECTOR_CHAINS = [
SupportedChainId.MAINNET,
SupportedChainId.POLYGON,
@@ -55,24 +26,36 @@ const NETWORK_SELECTOR_CHAINS = [
SupportedChainId.CELO,
]
interface ChainSwitcherProps {
interface ChainSelectorProps {
leftAlign?: boolean
}
export const ChainSwitcher = ({ leftAlign }: ChainSwitcherProps) => {
export const ChainSelector = ({ leftAlign }: ChainSelectorProps) => {
const { chainId } = useWeb3React()
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
const [isOpen, setIsOpen] = useState<boolean>(false)
const isMobile = useIsMobile()
const ref = useRef<HTMLDivElement>(null)
const modalRef = useRef<HTMLDivElement>(null)
useOnClickOutside(ref, isOpen ? toggleOpen : undefined, [modalRef])
useOnClickOutside(ref, () => setIsOpen(false), [modalRef])
const info = chainId ? getChainInfo(chainId) : undefined
const selectChain = useSelectChain()
useSyncChainQuery()
const [pendingChainId, setPendingChainId] = useState<SupportedChainId | undefined>(undefined)
const onSelectChain = useCallback(
async (targetChainId: SupportedChainId) => {
setPendingChainId(targetChainId)
await selectChain(targetChainId)
setPendingChainId(undefined)
setIsOpen(false)
},
[selectChain, setIsOpen]
)
if (!chainId) {
return null
}
@@ -81,15 +64,13 @@ export const ChainSwitcher = ({ leftAlign }: ChainSwitcherProps) => {
const dropdown = (
<NavDropdown top="56" left={leftAlign ? '0' : 'auto'} right={leftAlign ? 'auto' : '0'} ref={modalRef}>
<Column marginX="8">
<Column paddingX="8">
{NETWORK_SELECTOR_CHAINS.map((chainId: SupportedChainId) => (
<ChainRow
onSelectChain={async (targetChainId: SupportedChainId) => {
await selectChain(targetChainId)
toggleOpen()
}}
<ChainSelectorRow
onSelectChain={onSelectChain}
targetChain={chainId}
key={chainId}
isPending={chainId === pendingChainId}
/>
))}
</Column>
@@ -101,9 +82,9 @@ export const ChainSwitcher = ({ leftAlign }: ChainSwitcherProps) => {
<Row
as="button"
gap="8"
className={styles.ChainSwitcher}
className={styles.ChainSelector}
background={isOpen ? 'accentActiveSoft' : 'none'}
onClick={toggleOpen}
onClick={() => setIsOpen(!isOpen)}
>
{!isSupported ? (
<>
@@ -120,11 +101,7 @@ export const ChainSwitcher = ({ leftAlign }: ChainSwitcherProps) => {
</Box>
</>
)}
{isOpen ? (
<NewChevronUpIcon width={16} height={16} color="blackBlue" />
) : (
<NewChevronDownIcon width={16} height={16} color="blackBlue" />
)}
{isOpen ? <StyledChevronUp /> : <StyledChevronDown />}
</Row>
{isOpen && (isMobile ? <Portal>{dropdown}</Portal> : <>{dropdown}</>)}
</Box>

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 width={28} height={28} />
<EllipsisIcon width={20} height={20} />
</NavIcon>
{isOpen && (
@@ -134,7 +134,7 @@ export const MenuDropdown = () => {
<Column gap="16">
<Column paddingX="8" gap="4">
{nftFlag === NftVariant.Enabled && (
<PrimaryMenuRow to="/nft/sell" close={toggleOpen}>
<PrimaryMenuRow to="/nfts/sell" close={toggleOpen}>
<Icon>
<ThinTagIcon width={24} height={24} />
</Icon>

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({
@@ -27,14 +28,13 @@ export const searchBarContainer = style([
right: '0',
top: '0',
zIndex: '3',
display: 'inline-block',
}),
{
'@media': {
[`screen and (min-width: ${breakpoints.sm}px)`]: {
top: '-24px',
},
[`screen and (min-width: ${breakpoints.lg}px)`]: {
right: `-${DESKTOP_NAVBAR_WIDTH / 2}px`,
right: `-${DESKTOP_NAVBAR_WIDTH / 2 - MAGNIFYING_GLASS_ICON_WIDTH}px`,
top: '-5px',
},
},
},
@@ -43,7 +43,6 @@ export const searchBarContainer = style([
export const searchBar = style([
baseSearchStyle,
sprinkles({
height: 'full',
color: 'placeholder',
paddingX: '16',
cursor: 'pointer',
@@ -60,7 +59,9 @@ export const searchBarInput = style([
border: 'none',
background: 'none',
}),
{ lineHeight: '24px' },
{
lineHeight: '24px',
},
])
export const searchBarDropdown = style([
@@ -69,6 +70,7 @@ export const searchBarDropdown = style([
borderBottomLeftRadius: '12',
borderBottomRightRadius: '12',
background: 'lightGray',
height: { sm: 'viewHeight', md: 'auto' },
}),
{
borderTop: 'none',
@@ -83,7 +85,6 @@ export const suggestionRow = style([
justifyContent: 'space-between',
paddingY: '8',
paddingX: '16',
transition: '250',
}),
{
':hover': {
@@ -166,3 +167,50 @@ export const notFoundContainer = style([
paddingLeft: '16',
}),
])
const visibilityTransition = `visibility ${vars.time[125]}, opacity ${vars.time[125]}`
const delayedTransitionProperties = `padding 0s ${vars.time[125]}, height 0s ${vars.time[125]}`
export const hidden = style([
sprinkles({
visibility: 'hidden',
opacity: '0',
padding: '0',
height: '0',
}),
{
transition: `${visibilityTransition}, ${delayedTransitionProperties}`,
transitionTimingFunction: 'ease-in',
},
])
export const visible = style([
sprinkles({
visibility: 'visible',
opacity: '1',
height: 'full',
}),
{
transition: `${visibilityTransition}`,
transitionTimingFunction: 'ease-out',
},
])
export const searchContentCentered = style({
'@media': {
[`screen and (min-width: ${breakpoints.lg}px)`]: {
transform: `translateX(${DESKTOP_NAVBAR_WIDTH / 4}px)`,
transition: `transform ${vars.time[125]}`,
transitionTimingFunction: 'ease-out',
},
},
})
export const searchContentLeftAlign = style({
'@media': {
[`screen and (min-width: ${breakpoints.lg}px)`]: {
transform: 'translateX(0)',
transition: `transform ${vars.time[125]}`,
transitionTimingFunction: 'ease-in',
},
},
})

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) => {
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(undefined)
const searchHistory = useSearchHistory(
(state: { history: (FungibleToken | GenieCollection)[] }) => state.history
).slice(0, 2)
export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput, isLoading }: SearchBarDropdownProps) => {
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0)
const searchHistory = useSearchHistory((state: { history: (FungibleToken | GenieCollection)[] }) => state.history)
const shortenedHistory = useMemo(() => searchHistory.slice(0, 2), [searchHistory])
const { pathname } = useLocation()
const isNFTPage = pathname.includes('/nfts')
const isTokenPage = pathname.includes('/tokens')
const phase1Flag = useNftFlag()
const [resultsState, setResultsState] = useState<ReactNode>()
const tokenSearchResults =
tokens.length > 0 ? (
@@ -131,50 +135,56 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
)
) : null
const { data: trendingCollectionResults } = useQuery(['trendingCollections', 'eth', 'twenty_four_hours'], () =>
fetchTrendingCollections({ volumeType: 'eth', timePeriod: 'ONE_DAY' as TimePeriod, size: 3 })
const { data: trendingCollectionResults, isLoading: trendingCollectionsAreLoading } = useQuery(
['trendingCollections', 'eth', 'twenty_four_hours'],
() => fetchTrendingCollections({ volumeType: 'eth', timePeriod: 'ONE_DAY' as TimePeriod, size: 3 })
)
const trendingCollections = useMemo(() => {
return trendingCollectionResults
?.map((collection) => {
return {
...collection,
collectionAddress: collection.address,
floorPrice: formatEthPrice(collection.floor?.toString()),
stats: {
total_supply: collection.totalSupply,
one_day_change: collection.floorChange,
},
}
})
.slice(0, isNFTPage ? 3 : 2)
}, [isNFTPage, trendingCollectionResults])
const showTrendingCollections: boolean = useMemo(
() => (trendingCollections?.length ?? 0) > 0 && !isTokenPage && phase1Flag === NftVariant.Enabled,
[trendingCollections?.length, isTokenPage, phase1Flag]
const trendingCollections = useMemo(
() =>
trendingCollectionResults
? trendingCollectionResults
.map((collection) => ({
...collection,
collectionAddress: collection.address,
floorPrice: formatEthPrice(collection.floor?.toString()),
stats: {
total_supply: collection.totalSupply,
one_day_change: collection.floorChange,
},
}))
.slice(0, isNFTPage ? 3 : 2)
: [...Array<GenieCollection>(isNFTPage ? 3 : 2)],
[isNFTPage, trendingCollectionResults]
)
const { data: trendingTokenResults } = useQuery([], () => fetchTrendingTokens(4), {
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
})
const { data: trendingTokenResults, isLoading: trendingTokensAreLoading } = useQuery(
['trendingTokens'],
() => fetchTrendingTokens(4),
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
}
)
const trendingTokensLength = phase1Flag === NftVariant.Enabled ? (isTokenPage ? 3 : 2) : 4
const trendingTokens = useMemo(() => {
return trendingTokenResults?.slice(0, trendingTokensLength)
}, [trendingTokenResults, trendingTokensLength])
const trendingTokens = useMemo(
() =>
trendingTokenResults
? trendingTokenResults.slice(0, trendingTokensLength)
: [...Array<FungibleToken>(trendingTokensLength)],
[trendingTokenResults, trendingTokensLength]
)
const totalSuggestions = hasInput
? tokens.length + collections.length
: Math.min(searchHistory.length, 2) +
: Math.min(shortenedHistory.length, 2) +
(isNFTPage || !isTokenPage ? trendingCollections?.length ?? 0 : 0) +
(isTokenPage || !isNFTPage ? trendingTokens?.length ?? 0 : 0)
// Close the modal on escape
// Navigate search results via arrow keys
useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => {
if (event.key === 'ArrowUp') {
@@ -185,6 +195,7 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
setHoveredIndex(hoveredIndex - 1)
}
} else if (event.key === 'ArrowDown') {
event.preventDefault()
if (hoveredIndex && hoveredIndex === totalSuggestions - 1) {
setHoveredIndex(0)
} else {
@@ -200,61 +211,90 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
}
}, [toggleOpen, hoveredIndex, totalSuggestions])
useEffect(() => {
if (!isLoading) {
const currentState = () =>
hasInput ? (
// Empty or Up to 8 combined tokens and nfts
<Column gap="20">
{isNFTPage ? (
<>
{collectionSearchResults}
{tokenSearchResults}
</>
) : (
<>
{tokenSearchResults}
{collectionSearchResults}
</>
)}
</Column>
) : (
// Recent Searches, Trending Tokens, Trending Collections
<Column gap="20">
{shortenedHistory.length > 0 && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={0}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={shortenedHistory}
header={<Trans>Recent searches</Trans>}
headerIcon={<ClockIcon />}
/>
)}
{!isNFTPage && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={shortenedHistory.length}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={trendingTokens}
header={<Trans>Popular tokens</Trans>}
headerIcon={<TrendingArrow />}
isLoading={trendingTokensAreLoading}
/>
)}
{!isTokenPage && phase1Flag === NftVariant.Enabled && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={shortenedHistory.length + (isNFTPage ? 0 : trendingTokens?.length ?? 0)}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={trendingCollections as unknown as GenieCollection[]}
header={<Trans>Popular NFT collections</Trans>}
headerIcon={<TrendingArrow />}
isLoading={trendingCollectionsAreLoading}
/>
)}
</Column>
)
setResultsState(currentState)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isLoading,
tokens,
collections,
trendingCollections,
trendingCollectionsAreLoading,
trendingTokens,
trendingTokensAreLoading,
hoveredIndex,
phase1Flag,
toggleOpen,
shortenedHistory,
hasInput,
isNFTPage,
isTokenPage,
])
return (
<Box className={styles.searchBarDropdown}>
{hasInput ? (
// Empty or Up to 8 combined tokens and nfts
<Column gap="20">
{isNFTPage ? (
<>
{collectionSearchResults}
{tokenSearchResults}
</>
) : (
<>
{tokenSearchResults}
{collectionSearchResults}
</>
)}
</Column>
) : (
// Recent Searches, Trending Tokens, Trending Collections
<Column gap="20">
{searchHistory.length > 0 && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={0}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={searchHistory}
header={<Trans>Recent searches</Trans>}
headerIcon={<ClockIcon />}
/>
)}
{(trendingTokens?.length ?? 0) > 0 && !isNFTPage && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={searchHistory.length}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={trendingTokens ?? []}
header={<Trans>Popular tokens</Trans>}
headerIcon={<TrendingArrow />}
/>
)}
{showTrendingCollections && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={searchHistory.length + (isNFTPage ? 0 : trendingTokens?.length ?? 0)}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={trendingCollections as unknown as GenieCollection[]}
header={<Trans>Popular NFT collections</Trans>}
headerIcon={<TrendingArrow />}
/>
)}
</Column>
)}
<Box opacity={isLoading ? '0.3' : '1'} transition="125">
{resultsState}
</Box>
</Box>
)
}
@@ -268,9 +308,11 @@ export const SearchBar = () => {
const [searchValue, setSearchValue] = useState('')
const debouncedSearchValue = useDebounce(searchValue, 300)
const searchRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
const { pathname } = useLocation()
const phase1Flag = useNftFlag()
const isMobile = useIsMobile()
const isTablet = useIsTablet()
useOnClickOutside(searchRef, () => {
isOpen && toggleOpen()
@@ -300,6 +342,7 @@ export const SearchBar = () => {
const [reducedTokens, reducedCollections] = organizeSearchResults(isNFTPage, tokens ?? [], collections ?? [])
// close dropdown on escape
useEffect(() => {
const escapeKeyDownHandler = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpen) {
@@ -320,64 +363,77 @@ export const SearchBar = () => {
setSearchValue('')
}, [pathname])
// auto set cursor when searchbar is opened
useEffect(() => {
if (isOpen) {
inputRef.current?.focus()
}
}, [isOpen])
const placeholderText = phase1Flag === NftVariant.Enabled ? t`Search tokens and NFT collections` : t`Search tokens`
const isMobileOrTablet = isMobile || isTablet
const showCenteredSearchContent = !isOpen && phase1Flag !== NftVariant.Enabled && !isMobileOrTablet
return (
<Box position="relative">
<Box
position={isOpen ? { sm: 'fixed', md: 'absolute' } : 'static'}
position={{ sm: 'fixed', md: 'absolute' }}
width={{ sm: isOpen ? 'viewWidth' : 'auto', md: 'auto' }}
ref={searchRef}
className={styles.searchBarContainer}
display={{ sm: isOpen ? 'inline-block' : 'none', xl: 'inline-block' }}
>
<Row
className={clsx(`${styles.searchBar} ${!isOpen && magicalGradientOnHover}`)}
borderRadius={isOpen ? undefined : '12'}
className={clsx(
` ${styles.searchBar} ${!isOpen && !isMobile && magicalGradientOnHover} ${
isMobileOrTablet && (isOpen ? styles.visible : styles.hidden)
}`
)}
borderRadius={isOpen || isMobileOrTablet ? undefined : '12'}
borderTopRightRadius={isOpen && !isMobile ? '12' : undefined}
borderTopLeftRadius={isOpen && !isMobile ? '12' : undefined}
borderBottomWidth={isOpen ? '0px' : '1px'}
display={{ sm: isOpen ? 'flex' : 'none', xl: 'flex' }}
justifyContent={isOpen || phase1Flag === NftVariant.Enabled ? 'flex-start' : 'center'}
onFocus={() => !isOpen && toggleOpen()}
borderBottomWidth={isOpen || isMobileOrTablet ? '0px' : '1px'}
onClick={() => !isOpen && toggleOpen()}
gap="12"
>
<Box display={{ sm: 'none', md: 'flex' }}>
<MagnifyingGlassIcon />
</Box>
<Box display={{ sm: 'flex', md: 'none' }} color="placeholder" onClick={toggleOpen}>
<ChevronLeftIcon />
<Box className={showCenteredSearchContent ? styles.searchContentCentered : styles.searchContentLeftAlign}>
<Box display={{ sm: 'none', md: 'flex' }}>
<MagnifyingGlassIcon />
</Box>
<Box display={{ sm: 'flex', md: 'none' }} color="placeholder" onClick={toggleOpen}>
<ChevronLeftIcon />
</Box>
</Box>
<Box
as="input"
placeholder={placeholderText}
width={isOpen || phase1Flag === NftVariant.Enabled ? 'full' : '120'}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
!isOpen && toggleOpen()
setSearchValue(event.target.value)
}}
className={styles.searchBarInput}
className={`${styles.searchBarInput} ${
showCenteredSearchContent ? styles.searchContentCentered : styles.searchContentLeftAlign
}`}
value={searchValue}
ref={inputRef}
width={phase1Flag === NftVariant.Enabled || isOpen ? 'full' : '160'}
/>
</Row>
<Box display={{ sm: isOpen ? 'none' : 'flex', xl: 'none' }}>
<NavIcon onClick={toggleOpen}>
<NavMagnifyingGlassIcon width={28} height={28} />
</NavIcon>
</Box>
{isOpen &&
(debouncedSearchValue.length > 0 && (tokensAreLoading || collectionsAreLoading) ? (
<SkeletonRow />
) : (
<Box className={clsx(isOpen ? styles.visible : styles.hidden)}>
{isOpen && (
<SearchBarDropdown
toggleOpen={toggleOpen}
tokens={reducedTokens}
collections={reducedCollections}
hasInput={debouncedSearchValue.length > 0}
isLoading={tokensAreLoading || (collectionsAreLoading && phase1Flag === NftVariant.Enabled)}
/>
))}
)}
</Box>
</Box>
{isOpen && <Overlay />}
<NavIcon onClick={toggleOpen}>
<NavMagnifyingGlassIcon />
</NavIcon>
</Box>
)
}

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

@@ -10,7 +10,7 @@ import { putCommas } from 'nft/utils/putCommas'
import { useCallback, useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { TokenWarningRedIcon, VerifiedIcon } from '../../nft/components/icons'
import { VerifiedIcon } from '../../nft/components/icons'
import * as styles from './SearchBar.css'
interface CollectionRowProps {
@@ -151,11 +151,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index
<Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full">
<Box className={styles.primaryText}>{token.name}</Box>
{token.onDefaultList ? (
<VerifiedIcon className={styles.suggestionIcon} />
) : (
<TokenWarningRedIcon className={styles.suggestionIcon} />
)}
{token.onDefaultList && <VerifiedIcon className={styles.suggestionIcon} />}
</Row>
<Box className={styles.secondaryText}>{token.symbol}</Box>
</Column>
@@ -179,13 +175,21 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index
export const SkeletonRow = () => {
return (
<Box className={styles.searchBarDropdown}>
<Row className={styles.suggestionRow}>
<Row>
<Box className={styles.imageHolder} />
<Box borderRadius="round" height="16" width="160" background="loading" />
</Row>
<Row className={styles.suggestionRow}>
<Row width="full">
<Box className={styles.imageHolder} />
<Column gap="4" width="full">
<Row justifyContent="space-between">
<Box borderRadius="round" height="20" background="loading" style={{ width: '180px' }} />
<Box borderRadius="round" height="20" width="48" background="loading" />
</Row>
<Row justifyContent="space-between">
<Box borderRadius="round" height="16" width="120" background="loading" />
<Box borderRadius="round" height="16" width="48" background="loading" />
</Row>
</Column>
</Row>
</Box>
</Row>
)
}

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 />
@@ -101,7 +106,9 @@ const Navbar = () => {
</nav>
<Box className={styles.mobileBottomBar}>
<PageTabs />
<MenuDropdown />
<Box marginY="4">
<MenuDropdown />
</Box>
</Box>
</>
)

View File

@@ -81,6 +81,8 @@ const baseMenuItem = style([
borderRadius: '12',
transition: '250',
height: 'min',
width: 'full',
textAlign: 'center',
}),
{
lineHeight: '24px',

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'
@@ -165,7 +165,7 @@ function CurrencyRow({
selected={otherSelected}
>
<Column>
<CurrencyLogo currency={currency} size={'24px'} />
<CurrencyLogo currency={currency} size={'36px'} />
</Column>
<AutoColumn>
<Row>

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

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'} />
{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.accentAction : theme.backgroundInteractive)};
border: none;
color: ${({ theme, active }) => (active ? theme.white : theme.textPrimary)};
font-size: 16px;
font-weight: 600;
cursor: pointer;
:hover {
background-color: ${({ theme, active }) => !active && theme.backgroundModule};
}
`
const FavoriteText = styled.span`
@media only screen and (max-width: ${SMALLEST_MOBILE_MEDIA_BREAKPOINT}) {
display: none;
@@ -36,13 +23,13 @@ export default function FavoriteButton() {
const theme = useTheme()
const [showFavorites, setShowFavorites] = useAtom(showFavoritesAtom)
return (
<StyledFavoriteButton onClick={() => setShowFavorites(!showFavorites)} active={showFavorites}>
<FilterOption onClick={() => setShowFavorites(!showFavorites)} active={showFavorites} highlight>
<FavoriteButtonContent>
<Heart size={17} color={showFavorites ? theme.white : theme.textPrimary} fill="transparent" />
<Heart size={20} color={showFavorites ? theme.accentActive : theme.textPrimary} />
<FavoriteText>
<Trans>Favorites</Trans>
</FavoriteText>
</FavoriteButtonContent>
</StyledFavoriteButton>
</FilterOption>
)
}

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.blue200 : theme.textPrimary)};
border: none;
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundInteractive)};
margin: 0;
padding: 6px 12px 6px 12px;
border-radius: 12px;
font-size: 16px;
line-height: 24px;
font-weight: 400;
:hover {
cursor: pointer;
outline: none;
border: none;
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundModule)};
}
:focus {
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundInteractive)};
border: none;
outline: none;
}
svg {
margin-top: 2px;
}
`
const StyledMenu = styled.div`
display: flex;
justify-content: center;
@@ -96,26 +66,24 @@ const StyledMenu = styled.div`
position: relative;
border: none;
text-align: left;
width: 160px;
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
flex: 1;
}
`
const StyledMenuContent = styled.div`
display: flex;
justify-content: space-between;
gap: 8px;
align-items: center;
border: none;
width: 100%;
font-weight: 600;
vertical-align: middle;
`
const Chevron = styled.span<{ open: boolean }>`
padding-top: 1px;
color: ${({ open, theme }) => (open ? theme.blue200 : theme.textSecondary)};
color: ${({ open, theme }) => (open ? theme.accentActive : theme.textSecondary)};
`
const NetworkLabel = styled.div`
display: flex;
@@ -143,16 +111,20 @@ export default function NetworkFilter() {
return (
<StyledMenu ref={node}>
<StyledMenuButton onClick={toggleMenu} aria-label={`networkFilter`} open={open}>
<FilterOption onClick={toggleMenu} aria-label={`networkFilter`} active={open}>
<StyledMenuContent>
<NetworkLabel>
<Logo src={circleLogoUrl ?? logoUrl} /> {label}
</NetworkLabel>
<Chevron open={open}>
{open ? <ChevronUp size={15} viewBox="0 0 24 20" /> : <ChevronDown size={15} viewBox="0 0 24 20" />}
{open ? (
<ChevronUp width={20} height={15} viewBox="0 0 24 20" />
) : (
<ChevronDown width={20} height={15} viewBox="0 0 24 20" />
)}
</Chevron>
</StyledMenuContent>
</StyledMenuButton>
</FilterOption>
{open && (
<MenuTimeFlyout>
{NETWORKS.map((network) => (

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

View File

@@ -1,18 +1,19 @@
import { Trans } from '@lingui/macro'
import { ParentSize } from '@visx/responsive'
import { sendAnalyticsEvent } from 'components/AmplitudeAnalytics'
import { EventName } from 'components/AmplitudeAnalytics/constants'
import { sendAnalyticsEvent } from 'analytics'
import { EventName } from 'analytics/constants'
import SparklineChart from 'components/Charts/SparklineChart'
import CurrencyLogo from 'components/CurrencyLogo'
import { getChainInfo } from 'constants/chainInfo'
import { TimePeriod, TokenData } from 'graphql/data/TopTokenQuery'
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
import { getDurationDetails, SingleTokenData, TimePeriod } from 'graphql/data/Token'
import { useCurrency } from 'hooks/Tokens'
import { useAtom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import { ReactNode } from 'react'
import { ArrowDown, ArrowUp, Heart } from 'react-feather'
import { Link } from 'react-router-dom'
import styled, { css, useTheme } from 'styled-components/macro'
import { ClickableStyle } from 'theme'
import { formatDollarAmount } from 'utils/formatDollarAmt'
import {
@@ -23,12 +24,12 @@ import {
} from '../constants'
import { LoadingBubble } from '../loading'
import {
favoritesAtom,
filterNetworkAtom,
filterStringAtom,
filterTimeAtom,
sortCategoryAtom,
sortDirectionAtom,
useIsFavorited,
useSetSortCategory,
useToggleFavorite,
} from '../state'
@@ -41,11 +42,17 @@ const Cell = styled.div`
align-items: center;
justify-content: center;
`
const StyledTokenRow = styled.div<{ first?: boolean; last?: boolean; loading?: boolean }>`
const StyledTokenRow = styled.div<{
first?: boolean
last?: boolean
loading?: boolean
favoriteTokensEnabled?: boolean
}>`
background-color: transparent;
display: grid;
font-size: 15px;
grid-template-columns: 1fr 7fr 4fr 4fr 4fr 4fr 5fr 1.2fr;
grid-template-columns: ${({ favoriteTokensEnabled }) =>
favoriteTokensEnabled ? '1fr 7fr 4fr 4fr 4fr 4fr 5fr 1.2fr' : '1fr 7fr 4fr 4fr 4fr 4fr 5fr'};
height: 60px;
line-height: 24px;
max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT};
@@ -60,6 +67,7 @@ const StyledTokenRow = styled.div<{ first?: boolean; last?: boolean; loading?: b
},
}) => css`background-color ${duration.medium} ${timing.ease}`};
width: 100%;
transition-duration: ${({ theme }) => theme.transition.duration.fast};
&:hover {
${({ loading, theme }) =>
@@ -109,6 +117,14 @@ export const ClickFavorited = styled.span`
}
`
export const FavoriteIcon = styled(Heart)<{ isFavorited: boolean }>`
${ClickableStyle}
height: 22px;
width: 24px;
color: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : theme.textSecondary)};
fill: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : 'transparent')};
`
const ClickableContent = styled.div`
display: flex;
text-decoration: none;
@@ -149,10 +165,11 @@ const StyledHeaderRow = styled(StyledTokenRow)`
justify-content: space-between;
}
`
const ListNumberCell = styled(Cell)`
const ListNumberCell = styled(Cell)<{ header: boolean }>`
color: ${({ theme }) => theme.textSecondary};
min-width: 32px;
height: 48px;
height: ${({ header }) => (header ? '48px' : '60px')};
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
display: none;
@@ -215,15 +232,12 @@ const SortArrowCell = styled(Cell)`
`
const HeaderCellWrapper = styled.span<{ onClick?: () => void }>`
align-items: center;
${ClickableStyle}
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'unset')};
display: flex;
height: 100%;
justify-content: flex-end;
width: 100%;
&:hover {
opacity: 60%;
}
`
const SparkLineCell = styled(Cell)`
padding: 0px 24px;
@@ -387,20 +401,25 @@ export function TokenRow({
volume: ReactNode
last?: boolean
}) {
const favoriteTokensEnabled = useFavoriteTokensFlag() === FavoriteTokensVariant.Enabled
const rowCells = (
<>
<ListNumberCell>{listNumber}</ListNumberCell>
<ListNumberCell header={header}>{listNumber}</ListNumberCell>
<NameCell>{tokenInfo}</NameCell>
<PriceCell sortable={header}>{price}</PriceCell>
<PercentChangeCell sortable={header}>{percentChange}</PercentChangeCell>
<MarketCapCell sortable={header}>{marketCap}</MarketCapCell>
<VolumeCell sortable={header}>{volume}</VolumeCell>
<SparkLineCell>{sparkLine}</SparkLineCell>
<FavoriteCell>{favorited}</FavoriteCell>
{favoriteTokensEnabled && <FavoriteCell>{favorited}</FavoriteCell>}
</>
)
if (header) return <StyledHeaderRow>{rowCells}</StyledHeaderRow>
return <StyledTokenRow {...rest}>{rowCells}</StyledTokenRow>
if (header) return <StyledHeaderRow favoriteTokensEnabled={favoriteTokensEnabled}>{rowCells}</StyledHeaderRow>
return (
<StyledTokenRow favoriteTokensEnabled={favoriteTokensEnabled} {...rest}>
{rowCells}
</StyledTokenRow>
)
}
/* Header Row: top header row component for table */
@@ -445,31 +464,29 @@ export function LoadingRow() {
/* Loaded State: row component with token information */
export default function LoadedRow({
tokenAddress,
tokenListIndex,
tokenListLength,
tokenData,
timePeriod,
}: {
tokenAddress: string
tokenListIndex: number
tokenListLength: number
tokenData: TokenData
tokenData: SingleTokenData
timePeriod: TimePeriod
}) {
const tokenAddress = tokenData?.tokens?.[0].address
const currency = useCurrency(tokenAddress)
const tokenName = tokenData.name
const tokenSymbol = tokenData.symbol
const theme = useTheme()
const [favoriteTokens] = useAtom(favoritesAtom)
const isFavorited = favoriteTokens.includes(tokenAddress)
const tokenName = tokenData?.name
const tokenSymbol = tokenData?.tokens?.[0].symbol
const isFavorited = useIsFavorited(tokenAddress)
const toggleFavorite = useToggleFavorite(tokenAddress)
const filterString = useAtomValue(filterStringAtom)
const filterNetwork = useAtomValue(filterNetworkAtom)
const L2Icon = getChainInfo(filterNetwork).circleLogoUrl
const delta = tokenData.percentChange?.[timePeriod]?.value
const arrow = delta ? getDeltaArrow(delta) : null
const formattedDelta = delta ? formatDelta(delta) : null
const tokenDetails = tokenData?.markets?.[0]
const { volume, pricePercentChange } = getDurationDetails(tokenData, timePeriod)
const arrow = pricePercentChange ? getDeltaArrow(pricePercentChange) : null
const formattedDelta = pricePercentChange ? formatDelta(pricePercentChange) : null
const exploreTokenSelectedEventProperties = {
chain_id: filterNetwork,
@@ -481,7 +498,6 @@ export default function LoadedRow({
search_token_address_input: filterString,
}
const heartColor = isFavorited ? theme.accentActive : undefined
// TODO: currency logo sizing mobile (32px) vs. desktop (24px)
return (
<StyledLink
@@ -497,14 +513,14 @@ export default function LoadedRow({
toggleFavorite()
}}
>
<Heart size={18} color={heartColor} fill={heartColor} />
<FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited>
}
listNumber={tokenListIndex + 1}
tokenInfo={
<ClickableName>
<LogoContainer>
<CurrencyLogo currency={currency} />
<CurrencyLogo currency={currency} symbol={tokenSymbol} />
<L2NetworkLogo networkUrl={L2Icon} />
</LogoContainer>
<TokenInfoCell>
@@ -516,7 +532,7 @@ export default function LoadedRow({
price={
<ClickableContent>
<PriceInfoCell>
{tokenData.price?.value ? formatDollarAmount(tokenData.price?.value) : '-'}
{tokenDetails?.price?.value ? formatDollarAmount(tokenDetails?.price?.value) : '-'}
<PercentChangeInfoCell>
{formattedDelta}
{arrow}
@@ -532,19 +548,23 @@ export default function LoadedRow({
}
marketCap={
<ClickableContent>
{tokenData.marketCap?.value ? formatDollarAmount(tokenData.marketCap?.value) : '-'}
</ClickableContent>
}
volume={
<ClickableContent>
{tokenData.volume?.[timePeriod]?.value
? formatDollarAmount(tokenData.volume?.[timePeriod]?.value ?? undefined)
: '-'}
{tokenDetails?.marketCap?.value ? formatDollarAmount(tokenDetails?.marketCap?.value) : '-'}
</ClickableContent>
}
volume={<ClickableContent>{volume ? formatDollarAmount(volume ?? undefined) : '-'}</ClickableContent>}
sparkLine={
<SparkLine>
<ParentSize>{({ width, height }) => <SparklineChart width={width} height={height} />}</ParentSize>
<ParentSize>
{({ width, height }) => (
<SparklineChart
width={width}
height={height}
tokenData={tokenData}
pricePercentChange={pricePercentChange}
timePeriod={timePeriod}
/>
)}
</ParentSize>
</SparkLine>
}
first={tokenListIndex === 0}

View File

@@ -7,7 +7,9 @@ import {
sortCategoryAtom,
sortDirectionAtom,
} from 'components/Tokens/state'
import { TimePeriod, TokenData } from 'graphql/data/TopTokenQuery'
import { TokenTopQuery$data } from 'graphql/data/__generated__/TokenTopQuery.graphql'
import { getDurationDetails, SingleTokenData, useTopTokenQuery } from 'graphql/data/Token'
import { TimePeriod } from 'graphql/data/Token'
import { useAtomValue } from 'jotai/utils'
import { ReactNode, Suspense, useCallback, useMemo } from 'react'
import { AlertTriangle } from 'react-feather'
@@ -47,33 +49,37 @@ const TokenRowsContainer = styled.div`
width: 100%;
`
function useFilteredTokens(tokens: TokenData[] | undefined) {
function useFilteredTokens(data: TokenTopQuery$data): SingleTokenData[] | undefined {
const filterString = useAtomValue(filterStringAtom)
const favoriteTokenAddresses = useAtomValue(favoritesAtom)
const favorites = useAtomValue(favoritesAtom)
const showFavorites = useAtomValue(showFavoritesAtom)
const shownTokens =
showFavorites && tokens ? tokens.filter((token) => favoriteTokenAddresses.includes(token.address)) : tokens
return useMemo(
() =>
(shownTokens ?? []).filter((token) => {
if (!token.address) {
return false
}
if (!filterString) {
return true
}
const lowercaseFilterString = filterString.toLowerCase()
const addressIncludesFilterString = token?.address?.toLowerCase().includes(lowercaseFilterString)
const nameIncludesFilterString = token?.name?.toLowerCase().includes(lowercaseFilterString)
const symbolIncludesFilterString = token?.symbol?.toLowerCase().includes(lowercaseFilterString)
return nameIncludesFilterString || symbolIncludesFilterString || addressIncludesFilterString
}),
[shownTokens, filterString]
data.topTokenProjects
?.filter(
(token) => !showFavorites || (token?.tokens?.[0].address && favorites.includes(token?.tokens?.[0].address))
)
.filter((token) => {
const tokenInfo = token?.tokens?.[0]
const address = tokenInfo?.address
if (!address) {
return false
} else if (!filterString) {
return true
} else {
const lowercaseFilterString = filterString.toLowerCase()
const addressIncludesFilterString = address?.toLowerCase().includes(lowercaseFilterString)
const nameIncludesFilterString = token?.name?.toLowerCase().includes(lowercaseFilterString)
const symbolIncludesFilterString = tokenInfo?.symbol?.toLowerCase().includes(lowercaseFilterString)
return nameIncludesFilterString || symbolIncludesFilterString || addressIncludesFilterString
}
}),
[data.topTokenProjects, favorites, filterString, showFavorites]
)
}
function useSortedTokens(tokenData: TokenData[] | null) {
function useSortedTokens(tokenData: SingleTokenData[] | undefined) {
const sortCategory = useAtomValue(sortCategoryAtom)
const sortDirection = useAtomValue(sortDirectionAtom)
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
@@ -103,22 +109,25 @@ function useSortedTokens(tokenData: TokenData[] | null) {
}
let a: number | null | undefined
let b: number | null | undefined
const { volume: aVolume, pricePercentChange: aChange } = getDurationDetails(token1, timePeriod)
const { volume: bVolume, pricePercentChange: bChange } = getDurationDetails(token2, timePeriod)
switch (sortCategory) {
case Category.marketCap:
a = token1.marketCap?.value
b = token2.marketCap?.value
a = token1.markets?.[0]?.marketCap?.value
b = token2.markets?.[0]?.marketCap?.value
break
case Category.price:
a = token1.price?.value
b = token2.price?.value
a = token1.markets?.[0]?.price?.value
b = token2.markets?.[0]?.price?.value
break
case Category.volume:
a = token1.volume?.[timePeriod]?.value
b = token2.volume?.[timePeriod]?.value
a = aVolume
b = bVolume
break
case Category.percentChange:
a = token1.percentChange?.[timePeriod]?.value
b = token2.percentChange?.[timePeriod]?.value
a = aChange
b = bChange
break
}
return sortFn(a, b)
@@ -149,14 +158,15 @@ export function LoadingTokenTable() {
)
}
export default function TokenTable({ data }: { data: TokenData[] | undefined }) {
export default function TokenTable() {
const showFavorites = useAtomValue<boolean>(showFavoritesAtom)
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
const filteredTokens = useFilteredTokens(data)
const topTokens = useTopTokenQuery(1, timePeriod)
const filteredTokens = useFilteredTokens(topTokens)
const sortedFilteredTokens = useSortedTokens(filteredTokens)
/* loading and error state */
if (data === null) {
if (topTokens === null) {
return (
<NoTokensState
message={
@@ -184,8 +194,7 @@ export default function TokenTable({ data }: { data: TokenData[] | undefined })
<TokenRowsContainer>
{sortedFilteredTokens?.map((token, index) => (
<LoadedRow
key={token.address}
tokenAddress={token.address}
key={token?.name}
tokenListIndex={index}
tokenListLength={sortedFilteredTokens.length}
tokenData={token}

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,8 +1,8 @@
import { SupportedChainId } from 'constants/chains'
import { TimePeriod } from 'graphql/data/TopTokenQuery'
import { TimePeriod } from 'graphql/data/Token'
import { atom, useAtom } from 'jotai'
import { atomWithReset, atomWithStorage } from 'jotai/utils'
import { useCallback } from 'react'
import { atomWithReset, atomWithStorage, useAtomValue } from 'jotai/utils'
import { useCallback, useMemo } from 'react'
import { Category, SortDirection } from './types'
@@ -15,17 +15,18 @@ export const sortCategoryAtom = atom<Category>(Category.marketCap)
export const sortDirectionAtom = atom<SortDirection>(SortDirection.decreasing)
/* for favoriting tokens */
export function useToggleFavorite(tokenAddress: string) {
export function useToggleFavorite(tokenAddress: string | undefined | null) {
const [favoriteTokens, updateFavoriteTokens] = useAtom(favoritesAtom)
return useCallback(() => {
if (!tokenAddress) return
let updatedFavoriteTokens
if (favoriteTokens.includes(tokenAddress)) {
if (favoriteTokens.includes(tokenAddress.toLocaleLowerCase())) {
updatedFavoriteTokens = favoriteTokens.filter((address: string) => {
return address !== tokenAddress
return address !== tokenAddress.toLocaleLowerCase()
})
} else {
updatedFavoriteTokens = [...favoriteTokens, tokenAddress]
updatedFavoriteTokens = [...favoriteTokens, tokenAddress.toLocaleLowerCase()]
}
updateFavoriteTokens(updatedFavoriteTokens)
}, [favoriteTokens, tokenAddress, updateFavoriteTokens])
@@ -47,3 +48,12 @@ export function useSetSortCategory(category: Category) {
}
}, [category, sortCategory, setSortCategory, sortDirection, setDirectionCategory])
}
export function useIsFavorited(tokenAddress: string | null | undefined) {
const favoritedTokens = useAtomValue<string[]>(favoritesAtom)
return useMemo(
() => (tokenAddress ? favoritedTokens.includes(tokenAddress.toLocaleLowerCase()) : false),
[favoritedTokens, tokenAddress]
)
}

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

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

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,8 +1,9 @@
// eslint-disable-next-line no-restricted-imports
import { t, Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { StyledChevronDown, StyledChevronUp } from 'components/Icons'
import WalletDropdown from 'components/WalletDropdown'
import { getConnection } from 'connection/utils'
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
@@ -10,10 +11,10 @@ import { Portal } from 'nft/components/common/Portal'
import { getIsValidSwapQuote } from 'pages/Swap'
import { darken } from 'polished'
import { useMemo, useRef } from 'react'
import { AlertTriangle, ChevronDown, ChevronUp } from 'react-feather'
import { AlertTriangle } from 'react-feather'
import { useAppSelector } from 'state/hooks'
import { useDerivedSwapInfo } from 'state/swap/hooks'
import styled, { css } from 'styled-components/macro'
import styled, { css, useTheme } from 'styled-components/macro'
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
import { useHasSocks } from '../../hooks/useSocksBalance'
@@ -33,12 +34,15 @@ import Loader from '../Loader'
import { RowBetween } from '../Row'
import WalletModal from '../WalletModal'
// https://stackoverflow.com/a/31617326
const FULL_BORDER_RADIUS = 9999
const Web3StatusGeneric = styled(ButtonSecondary)`
${({ theme }) => theme.flexRowNoWrap}
width: 100%;
align-items: center;
padding: 0.5rem;
border-radius: 14px;
border-radius: ${FULL_BORDER_RADIUS}px;
cursor: pointer;
user-select: none;
height: 36px;
@@ -59,15 +63,15 @@ const Web3StatusError = styled(Web3StatusGeneric)`
}
`
const Web3StatusConnectNavbar = styled.button<{ faded?: boolean }>`
dispay: flex;
align-items: center;
const Web3StatusConnectButton = styled.button<{ faded?: boolean }>`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
background-color: ${({ theme }) => theme.accentActionSoft};
border-radius: 12px;
border-radius: ${FULL_BORDER_RADIUS}px;
border: none;
cursor: pointer;
padding: 10px 12px;
padding: 0 12px;
height: 40px;
:hover,
:active,
@@ -153,6 +157,7 @@ function Sock() {
const VerticalDivider = styled.div`
height: 20px;
margin: 0px 4px;
width: 1px;
background-color: ${({ theme }) => theme.accentAction};
`
@@ -169,22 +174,7 @@ const StyledConnect = styled.div`
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms color ${timing.in}`};
}
`
const StyledChevron = styled.span`
color: ${({ theme }) => theme.accentAction};
height: 24px;
margin-left: 4px;
&:hover {
color: ${({ theme }) => theme.accentActionSoft};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms color ${timing.in}`};
}) => `${duration.fast} color ${timing.in}`};
}
`
@@ -196,7 +186,8 @@ function Web3StatusInner() {
inputError: swapInputError,
} = useDerivedSwapInfo()
const validSwapQuote = getIsValidSwapQuote(trade, tradeState, swapInputError)
const navbarFlag = useNavBarFlag()
const navbarFlagEnabled = useNavBarFlag() === NavBarVariant.Enabled
const theme = useTheme()
const toggleWalletDropdown = useToggleWalletDropdown()
const toggleWalletModal = useToggleWalletModal()
const walletIsOpen = useIsOpen()
@@ -214,7 +205,7 @@ function Web3StatusInner() {
const hasPendingTransactions = !!pending.length
const hasSocks = useHasSocks()
const toggleWallet = navbarFlag === NavBarVariant.Enabled ? toggleWalletDropdown : toggleWalletModal
const toggleWallet = navbarFlagEnabled ? toggleWalletDropdown : toggleWalletModal
if (!chainId) {
return null
@@ -230,6 +221,7 @@ function Web3StatusInner() {
} else if (account) {
return (
<Web3StatusConnected data-testid="web3-status-connected" onClick={toggleWallet} pending={hasPendingTransactions}>
{navbarFlagEnabled && !hasPendingTransactions && <StatusIcon size={24} connectionType={connectionType} />}
{hasPendingTransactions ? (
<RowBetween>
<Text>
@@ -239,11 +231,18 @@ function Web3StatusInner() {
</RowBetween>
) : (
<>
{hasSocks ? <Sock /> : null}
{hasSocks && !navbarFlagEnabled ? <Sock /> : null}
<Text>{ENSName || shortenAddress(account)}</Text>
{navbarFlagEnabled ? (
walletIsOpen ? (
<StyledChevronUp onClick={toggleWalletDropdown} />
) : (
<StyledChevronDown onClick={toggleWalletDropdown} />
)
) : null}
</>
)}
{!hasPendingTransactions && <StatusIcon connectionType={connectionType} />}
{!navbarFlagEnabled && !hasPendingTransactions && <StatusIcon connectionType={connectionType} />}
</Web3StatusConnected>
)
} else {
@@ -254,16 +253,26 @@ function Web3StatusInner() {
properties={{ received_swap_quote: validSwapQuote }}
element={ElementName.CONNECT_WALLET_BUTTON}
>
{navbarFlag === NavBarVariant.Enabled ? (
<Web3StatusConnectNavbar faded={!account}>
<StyledConnect onClick={toggleWalletModal}>
{navbarFlagEnabled ? (
<Web3StatusConnectButton faded={!account}>
<StyledConnect data-testid="navbar-connect-wallet" onClick={toggleWalletModal}>
<Trans>Connect</Trans>
</StyledConnect>
<VerticalDivider />
<StyledChevron onClick={toggleWalletDropdown}>
{walletIsOpen ? <ChevronUp /> : <ChevronDown />}
</StyledChevron>
</Web3StatusConnectNavbar>
{walletIsOpen ? (
<StyledChevronUp
data-testid="navbar-wallet-dropdown"
customColor={theme.accentAction}
onClick={toggleWalletDropdown}
/>
) : (
<StyledChevronDown
data-testid="navbar-wallet-dropdown"
customColor={theme.accentAction}
onClick={toggleWalletDropdown}
/>
)}
</Web3StatusConnectButton>
) : (
<Web3StatusConnect onClick={toggleWallet} faded={!account}>
<Text>

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])
@@ -31,12 +32,14 @@ export default function Widget({ defaultToken }: WidgetProps) {
return (
<>
<SwapWidget
disableBranding
hideConnectionUI
jsonRpcUrlMap={RPC_URLS}
jsonRpcUrlMap={RPC_PROVIDERS}
routerUrl={WIDGET_ROUTER_URL}
width={WIDGET_WIDTH}
locale={locale}
theme={theme}
onReviewSwapClick={onReviewSwapClick}
// defaultChainId is excluded - it is always inferred from the passed provider
provider={provider}
{...inputs}

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<string>()
const [amount, setAmount] = useState(EMPTY_AMOUNT)
const onAmountChange = useCallback((field: Field, amount: string) => {
setType(toTradeType(field))
setAmount(amount)
@@ -17,6 +19,14 @@ export function useSyncWidgetInputs(defaultToken?: Currency) {
const [tokens, setTokens] = useState<{ [Field.INPUT]?: Currency; [Field.OUTPUT]?: Currency }>({
[Field.OUTPUT]: defaultToken,
})
useEffect(() => {
setTokens({
[Field.OUTPUT]: defaultToken,
})
setAmount(EMPTY_AMOUNT)
}, [defaultToken])
const onSwitchTokens = useCallback(() => {
setType((type) => invertTradeType(type))
setTokens((tokens) => ({

View File

@@ -1,18 +1,70 @@
import { TransactionReceipt } from '@ethersproject/abstract-provider'
import { TransactionEventHandlers } from '@uniswap/widgets'
import { useMemo } from 'react'
import {
TradeType,
Transaction,
TransactionEventHandlers,
TransactionInfo,
TransactionType as WidgetTransactionType,
} from '@uniswap/widgets'
import { useWeb3React } from '@web3-react/core'
import { useCallback, useMemo } from 'react'
import { useTransactionAdder } from 'state/transactions/hooks'
import {
ExactInputSwapTransactionInfo,
ExactOutputSwapTransactionInfo,
TransactionType as AppTransactionType,
WrapTransactionInfo,
} from 'state/transactions/types'
import { currencyId } from 'utils/currencyId'
/** Integrates the Widget's transactions, showing the widget's transactions in the app. */
export function useSyncWidgetTransactions() {
// TODO(jfrankfurt): Integrate widget transactions with app transaction tracking.
const txHandlers: TransactionEventHandlers = useMemo(
() => ({
onTxSubmit: (hash: string, tx: unknown) => console.log('onTxSubmit'),
onTxSuccess: (hash: string, receipt: TransactionReceipt) => console.log('onTxSuccess'),
onTxFail: (hash: string, receipt: TransactionReceipt) => console.log('onTxFail'),
}),
[]
const { chainId } = useWeb3React()
const addTransaction = useTransactionAdder()
const onTxSubmit = useCallback(
(_hash: string, transaction: Transaction<TransactionInfo>) => {
const { type, response } = transaction.info
if (!type || !response) {
return
} else if (type === WidgetTransactionType.WRAP || type === WidgetTransactionType.UNWRAP) {
const { amount } = transaction.info
addTransaction(response, {
type: AppTransactionType.WRAP,
unwrapped: type === WidgetTransactionType.UNWRAP,
currencyAmountRaw: amount.quotient.toString(),
chainId,
} as WrapTransactionInfo)
} else if (type === WidgetTransactionType.SWAP) {
const { slippageTolerance, trade, tradeType } = transaction.info
const baseTxInfo = {
type: AppTransactionType.SWAP,
tradeType,
inputCurrencyId: currencyId(trade.inputAmount.currency),
outputCurrencyId: currencyId(trade.outputAmount.currency),
}
if (tradeType === TradeType.EXACT_OUTPUT) {
addTransaction(response, {
...baseTxInfo,
maximumInputCurrencyAmountRaw: trade.maximumAmountIn(slippageTolerance).quotient.toString(),
outputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
} as ExactOutputSwapTransactionInfo)
} else {
addTransaction(response, {
...baseTxInfo,
inputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
expectedOutputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
minimumOutputCurrencyAmountRaw: trade.minimumAmountOut(slippageTolerance).quotient.toString(),
} as ExactInputSwapTransactionInfo)
}
}
},
[addTransaction, chainId]
)
const txHandlers: TransactionEventHandlers = useMemo(() => ({ onTxSubmit }), [onTxSubmit])
return { transactions: { ...txHandlers } }
}

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, Percent, TradeType } from '@uniswap/sdk-core'
import { ModalName } from 'components/AmplitudeAnalytics/constants'
import { Trace } from 'components/AmplitudeAnalytics/Trace'
import { ReactNode, useCallback, useMemo, useState } from 'react'
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { sendAnalyticsEvent } from 'analytics'
import { ModalName } from 'analytics/constants'
import { EventName } from 'analytics/constants'
import { Trace } from 'analytics/Trace'
import { formatPercentInBasisPointsNumber, formatToDecimal, getTokenAddress } from 'analytics/utils'
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { computeRealizedPriceImpact } from 'utils/prices'
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
import TransactionConfirmationModal, {
@@ -14,6 +18,27 @@ import TransactionConfirmationModal, {
import SwapModalFooter from './SwapModalFooter'
import SwapModalHeader from './SwapModalHeader'
const formatAnalyticsEventProperties = ({
trade,
txHash,
}: {
trade: InterfaceTrade<Currency, Currency, TradeType>
txHash: string
}) => ({
transaction_hash: txHash,
token_in_address: getTokenAddress(trade.inputAmount.currency),
token_out_address: getTokenAddress(trade.outputAmount.currency),
token_in_symbol: trade.inputAmount.currency.symbol,
token_out_symbol: trade.outputAmount.currency.symbol,
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
chain_id:
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
? trade.inputAmount.currency.chainId
: undefined,
})
export default function ConfirmSwapModal({
trade,
originalTrade,
@@ -27,6 +52,8 @@ export default function ConfirmSwapModal({
attemptingTxn,
txHash,
swapQuoteReceivedDate,
fiatValueInput,
fiatValueOutput,
}: {
isOpen: boolean
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
@@ -40,10 +67,13 @@ export default function ConfirmSwapModal({
swapErrorMessage: ReactNode | undefined
onDismiss: () => void
swapQuoteReceivedDate: Date | undefined
fiatValueInput?: CurrencyAmount<Token> | null
fiatValueOutput?: CurrencyAmount<Token> | null
}) {
// shouldLogModalCloseEvent lets the child SwapModalHeader component know when modal has been closed
// and an event triggered by modal closing should be logged.
const [shouldLogModalCloseEvent, setShouldLogModalCloseEvent] = useState(false)
const [lastTxnHashLogged, setLastTxnHashLogged] = useState<string | null>(null)
const showAcceptChanges = useMemo(
() => Boolean(trade && originalTrade && tradeMeaningfullyDiffers(trade, originalTrade)),
[originalTrade, trade]
@@ -78,9 +108,21 @@ export default function ConfirmSwapModal({
disabledConfirm={showAcceptChanges}
swapErrorMessage={swapErrorMessage}
swapQuoteReceivedDate={swapQuoteReceivedDate}
fiatValueInput={fiatValueInput}
fiatValueOutput={fiatValueOutput}
/>
) : null
}, [onConfirm, showAcceptChanges, swapErrorMessage, trade, allowedSlippage, txHash, swapQuoteReceivedDate])
}, [
onConfirm,
showAcceptChanges,
swapErrorMessage,
trade,
allowedSlippage,
txHash,
swapQuoteReceivedDate,
fiatValueInput,
fiatValueOutput,
])
// text to show while loading
const pendingText = (
@@ -105,8 +147,15 @@ export default function ConfirmSwapModal({
[onModalDismiss, modalBottom, modalHeader, swapErrorMessage]
)
useEffect(() => {
if (!attemptingTxn && isOpen && txHash && trade && txHash !== lastTxnHashLogged) {
sendAnalyticsEvent(EventName.SWAP_SIGNED, formatAnalyticsEventProperties({ trade, txHash }))
setLastTxnHashLogged(txHash)
}
}, [attemptingTxn, isOpen, txHash, trade, lastTxnHashLogged])
return (
<Trace modal={ModalName.CONFIRM_SWAP} shouldLogImpression={isOpen}>
<Trace modal={ModalName.CONFIRM_SWAP}>
<TransactionConfirmationModal
isOpen={isOpen}
onDismiss={onModalDismiss}

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, Percent, TradeType } from '@uniswap/sdk-core'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import {
formatPercentInBasisPointsNumber,
formatPercentNumber,
@@ -9,7 +9,7 @@ import {
getDurationFromDateMilliseconds,
getDurationUntilTimestampSeconds,
getTokenAddress,
} from 'components/AmplitudeAnalytics/utils'
} from 'analytics/utils'
import useTransactionDeadline from 'hooks/useTransactionDeadline'
import { ReactNode } from 'react'
import { Text } from 'rebass'
@@ -31,6 +31,8 @@ interface AnalyticsEventProps {
isAutoRouterApi: boolean
swapQuoteReceivedDate: Date | undefined
routes: RoutingDiagramEntry[]
fiatValueInput?: CurrencyAmount<Token> | null
fiatValueOutput?: CurrencyAmount<Token> | null
}
const formatRoutesEventProperties = (routes: RoutingDiagramEntry[]) => {
@@ -69,6 +71,8 @@ const formatAnalyticsEventProperties = ({
isAutoRouterApi,
swapQuoteReceivedDate,
routes,
fiatValueInput,
fiatValueOutput,
}: AnalyticsEventProps) => ({
estimated_network_fee_usd: trade.gasUseEstimateUSD ? formatToDecimal(trade.gasUseEstimateUSD, 2) : undefined,
transaction_hash: hash,
@@ -79,6 +83,8 @@ const formatAnalyticsEventProperties = ({
token_out_symbol: trade.outputAmount.currency.symbol,
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
token_in_amount_usd: fiatValueInput ? parseFloat(fiatValueInput.toFixed(2)) : undefined,
token_out_amount_usd: fiatValueOutput ? parseFloat(fiatValueOutput.toFixed(2)) : undefined,
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
is_auto_router_api: isAutoRouterApi,
@@ -102,6 +108,8 @@ export default function SwapModalFooter({
swapErrorMessage,
disabledConfirm,
swapQuoteReceivedDate,
fiatValueInput,
fiatValueOutput,
}: {
trade: InterfaceTrade<Currency, Currency, TradeType>
hash: string | undefined
@@ -110,6 +118,8 @@ export default function SwapModalFooter({
swapErrorMessage: ReactNode | undefined
disabledConfirm: boolean
swapQuoteReceivedDate: Date | undefined
fiatValueInput?: CurrencyAmount<Token> | null
fiatValueOutput?: CurrencyAmount<Token> | null
}) {
const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch
const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto'
@@ -122,7 +132,7 @@ export default function SwapModalFooter({
<TraceEvent
events={[Event.onClick]}
element={ElementName.CONFIRM_SWAP_BUTTON}
name={EventName.SWAP_SUBMITTED}
name={EventName.SWAP_SUBMITTED_BUTTON_CLICKED}
properties={formatAnalyticsEventProperties({
trade,
hash,
@@ -132,6 +142,8 @@ export default function SwapModalFooter({
isAutoRouterApi: !clientSideRouter,
swapQuoteReceivedDate,
routes,
fiatValueInput,
fiatValueOutput,
})}
>
<ButtonError

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