Compare commits

...

154 Commits

Author SHA1 Message Date
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
lynn
b82b9acc54 fix: remove stablecoin usd val fetch in logging to reduce infura spend (#4543)
remove stablecoin usd val fetch reduce infura
2022-08-30 15:15:49 -04:00
cartcrom
fac3845756 fix: missing segments of price chart (#4541)
fixed missing segments of line
2022-08-30 12:28:52 -04:00
Charles Bachmeier
9381a74f1d feat: nav update to new responsive designs (#4542)
* uppdated mobile nav

* adjust searchbar for new styles

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-30 09:08:10 -07:00
Charles Bachmeier
f6662a3208 fix: hide right and left border on mobile dropdown (#4540)
hide right and left border on mobile

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-30 07:15:50 -07:00
pp-hh-ii-ll
134af82d90 fix: updated color definitions for backgroundModule, add scrolledSurface (#4538)
Updated color definitions for BackgroundModule and tokens list in Explore page

Update Dark Mode BackgroundModule to Grey800 to match new design spec

Update Widgets theme to use BackgroundModule for Module

Update Token List in Explore page to use Surface instead of Module

Add Scrolled Surface color definition
2022-08-29 22:00:34 -04:00
Vignesh Mohankumar
75175b8e54 fix: don't track balances/values (#4539) 2022-08-29 18:46:37 -04:00
Greg Bugyis
e3d8599dc7 fix: [TokenDetails] Glyph placement on line chart (#4525)
* Modify line curve on token price chart to fix inconsistency on steep drops/increases and glyph placement

* Make curve required on LineChart

* Add curve to SparkLine chart

* Remove dependency: d3-curve-circlecornders - no longer used

* Drop d3-curve-circlecorner from react-app-env

Co-authored-by: gbugyis <greg@bugyis.com>
2022-08-29 21:40:05 +03:00
Charles Bachmeier
77ee69ad52 fix: search flash of no tokens found when typing (#4531)
* add null check to collection floor price

* don't show 0 if floor is null

* use debouncedSearchValue

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-29 10:47:42 -07:00
lynn
4b82838f80 fix: add back cancel button in token safety for swap (#4527)
* add back cancel button in token safety for swap

* also add to swap flow
2022-08-29 13:08:30 -04:00
Jack Short
a177829976 style: switching hovered and idle colors (#4526) 2022-08-29 12:49:36 -04:00
Kaylee George
b8b4f960dd fix: tokens banner link works (#4522)
* maybe fixed?

* fix

* fix link
2022-08-29 12:48:13 -04:00
Charles Bachmeier
2466414307 feat: search when no results there is too much top padding (#4530)
* add null check to collection floor price

* don't show 0 if floor is null

* remove top margin for no results

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-29 09:43:50 -07:00
Charles Bachmeier
a3a32f0d68 fix: add null check to collection floor price (#4528)
* add null check to collection floor price

* don't show 0 if floor is null

* formatEthPrice accepts udnefined

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-29 09:38:22 -07:00
Vignesh Mohankumar
ee001f86f0 refactor: remove isChainAllowed (#4494) 2022-08-29 10:21:18 -04:00
lynn
87d6975bd8 fix: stop swap layout shifts when reverse token button clicked (#4523)
fix
2022-08-26 17:22:58 -04:00
Kaylee George
4cab4e27ff fix: fix favorites button responsiveness on tokens explore (#4520)
fav button responsiveness
2022-08-26 16:47:41 -04:00
Kaylee George
f105f0995b fix: formatted delta on mobile view of token explore (#4519)
fix delta
2022-08-26 13:42:38 -07:00
Kaylee George
18e89a7353 fix: rm right padding on token details (#4518)
rm padding on mobile
2022-08-26 16:32:23 -04:00
Charles Bachmeier
445f9a67a4 fix: mobile network switcher (#4516)
* fix: mobile network switcher

* fix zindex

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-26 13:01:20 -07:00
Jack Short
4c039c900c fix: wrapped native asset resolve to their appropriate native asset o… (#4497)
* fix: wrapped native asset resolve to their appropriate native asset on respective chains

* addressed comments
2022-08-26 14:42:34 -04:00
Kaylee George
2c2e0a3419 fix: vertically center the explore table header text (#4514)
center
2022-08-26 14:41:34 -04:00
aballerr
1b43e0b28a fix: wallet mobile ui changes (#4515)
fix wallet mobile

Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-08-26 14:41:02 -04:00
Connor McEwen
a23f7782b2 fix: widget module color (#4513) 2022-08-26 14:40:33 -04:00
Zach Pomerantz
723db9d0ea feat: integrate widget settings/value (#4499)
* widgets: v2.2.0

* refactor: wrap Widget

* feat: integrate widget settings

* refactor: import from widgets

* fix: better settings integration

* fix: include types

* chore: formatting

* build: bump widgets version

* feat: integrate widget value

* feat: integrate widget token selection

* feat: clean up widget integration

* build: bump widgets version

* refactor: mv widget wrapper to components

* refactor: split widget integrations

* refactor: value -> inputs

* fix: consolidate slippage hooks

* fix: memoize currency search modal

* refactor: clarify widget code

* fix: allow loading token in widget

* fix: add TradeType helpers
2022-08-26 11:19:51 -07:00
lynn
cbf165dc40 fix: oops (#4511)
oops
2022-08-26 11:12:48 -07:00
Kaylee George
6f3579acf1 feat: add token explore promo banner (#4481)
* initial design

* progress

* add background image

* add logic

* update

* delete unneed state

* more

* redux

* redux

* more redux

* rebase main

* rm unused state

* rm unused export

* relative link
2022-08-26 11:00:50 -07:00
lynn
c9e2f86e57 feat: add i18n to token detail and token explore (#4510)
init
2022-08-26 13:51:29 -04:00
aballerr
8c0199119e fix: Fix to mobile wallet experience (#4508)
* fixing wallet on mobile and making useClickoutside more extensible


Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-08-26 13:35:38 -04:00
Kaylee George
5e6e6be888 fix: fix decimal formatting of token price on details page (#4470)
* init

* 6 decimals

* dollar dollar bill yo
2022-08-26 10:19:29 -07:00
github-actions[bot]
79fb6485b1 chore(i18n): new Crowdin translations (#4495)
chore(i18n): synchronize translations from crowdin [skip ci]

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-08-26 13:16:06 -04:00
Greg Bugyis
29baaaf2ed fix: Token Warning dismiss/redirect (#4482)
* Remove cancel option from TokenSafety modal

* Only redirect if token is blocked

* Remove unused variables

Co-authored-by: gbugyis <greg@bugyis.com>
2022-08-26 20:10:54 +03:00
Kaylee George
f824fb25c2 fix: explore table hover state colors (#4479)
* fix hover state colors

* colors

* fixed
2022-08-26 09:41:05 -07:00
Charles Bachmeier
28a6ea7e1a feat: NavBar I18N (#4507)
* wrap tabs and searchbar

* wrap chain switcher and fix right align bug

* undo translate chains

* replace t with i18n

* revert i18n

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-26 09:22:05 -07:00
lynn
65566faf17 fix: yellow button redesign, improve add liquidity page readability (#4504)
* init

* add background overlay for hover states
2022-08-26 12:02:03 -04:00
lynn
eb4f90e669 feat: add volume trade info to swap completed event (#4506)
add volume trade info to swap completed event
2022-08-26 10:56:53 -04:00
aballerr
40308158ca fix: Updating search token width and adding margin between token delta and price (#4500)
Updating search token width and adding margin between token delta and price

Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-08-26 09:46:39 -04:00
Greg Bugyis
d0d5240474 fix: update Token table headers to match latest Figma (#4505)
TokenTable: update table headers to match latest Figma

Co-authored-by: gbugyis <greg@bugyis.com>
2022-08-26 15:21:35 +03:00
Charles Bachmeier
751ce8e6d6 feat: Mobile Nav (#4501)
* working and cleaned up mobile nav

* delete old files

* fix wallet position

* update searchbar breakpoint

* update full screen search

* delete old comments

* cleanup eslint

* Update MenuDropdown.tsx

* Update SearchBar.tsx

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-26 04:39:58 -07:00
Greg Bugyis
748a5eadc0 fix: Token Details - move About section below Market Stats (#4483) 2022-08-26 10:50:55 +03:00
lynn
5659fe21ea fix: fix swap small layout shifts when clicking reverse tokens button (#4503)
fix
2022-08-25 23:57:14 -04:00
lynn
bc899b74a3 feat: make token safety speedbumps appear for tokens with warnings in token selector (#4496)
* checkpoint: token modal safety warning working, showing speedbump

* fix styling

* dont show token safety once user has already ack'd that token

* fix cancel button on token safety - always navigate back to search
2022-08-25 21:20:04 -04:00
Zach Pomerantz
516c8b05a4 feat: add group flag toggles (#4502) 2022-08-25 16:54:15 -07:00
aballerr
8502f9e303 fix: Wallet updates (#4498)
* Wallet Polishing

Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-08-25 17:50:38 -04:00
Jordan Frankfurt
bc90d416e6 feat: add background-color transitions to table color changes (#4491)
* feat: add background-color transitions to table color changes:

* pr review--use theme transition info
2022-08-25 16:34:05 -05:00
Charles Bachmeier
2fd1cd72fd fix: bug with network with no info (#4489)
* fix bug with network with no info

* remove unneeded check

* add null checks

* check info for supported case

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-25 11:32:26 -07:00
github-actions[bot]
3a2276dcd1 chore(i18n): new Crowdin translations (#4478)
* chore(i18n): synchronize translations from crowdin [skip ci]

* empty

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Vignesh Mohankumar <me@vig.xyz>
2022-08-25 14:30:25 -04:00
Vignesh Mohankumar
c432c583f6 fix: rename search to filter tokens (#4485)
* fix: rename search to filter tokens

* translate
2022-08-25 14:04:35 -04:00
Connor McEwen
9b8f5ed8f4 fix: update widget color scheme (#4475)
* fix: update widget color scheme

* Map Uniswap Design System colors to Widget color styles

Co-authored-by: pp-hh-ii-ll <111304124+pp-hh-ii-ll@users.noreply.github.com>
2022-08-25 13:50:46 -04:00
Jack Short
0713a15028 fix: adding explore buttons on loading (#4490) 2022-08-25 13:17:45 -04:00
Charles Bachmeier
62c502615f feat: show searchbar on tablet size (#4484)
show searchbar on tablet size

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-25 10:00:41 -07:00
Charles Bachmeier
fdbe4b8f5e feat: mobile network switcher (#4486)
* feat: mobile network switcher

* adjust bottom border radius on mobile

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-25 09:31:47 -07:00
lynn
33c73f4dc8 fix: network balances summary styling (#4487)
init
2022-08-25 09:15:36 -07:00
aballerr
c207a576e7 feat: Sample transitions pr (#4477)
* Updating theming to include transitions


Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-08-25 11:25:49 -04:00
Jack Short
aa426514f3 chore: updating navbar padding for token pages (#4474)
* chore: updating navbar padding for token pages

* removing margin on pools page
2022-08-25 11:20:19 -04:00
aballerr
ba9c28892e fix: Fixing token modal warning to match figma design (#4476)
* fixing token modal warning to match figma design


Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-08-25 10:06:12 -04:00
Charles Bachmeier
49c31ddfc8 feat: update sprinkles breakpoints (#4480)
update breakpoints

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-24 19:01:11 -07:00
github-actions[bot]
4424205814 chore(i18n): new Crowdin translations (#4460)
* chore(i18n): synchronize translations from crowdin [skip ci]

* empty

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Vignesh Mohankumar <me@vig.xyz>
2022-08-24 18:04:37 -04:00
lynn
874f3fb737 fix: fix copy button styling (#4473)
* fix copy button styling

* fix positionig

* preserve original css for token safety warning use case
2022-08-24 17:57:25 -04:00
Kaylee George
ac27c89a44 fix: change top tokens query return type (#4453)
* query return

* fix return

* rm default empty string

* rm ?

* undef

* undefined

* null

* fix

* handle search

* .

* rebase

* top tokens

* rm blank

* rm console log

* make 100
2022-08-24 12:38:22 -07:00
aballerr
0e530cf92e fix: added in correct wallet breakpoints (#4472)
* adding in wallet breakpoints

Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-08-24 15:16:51 -04:00
Charles Bachmeier
ed66b00b20 feat: update Chain Switcher Modal and Active State (#4471)
* Add unsupported network and active state to chain switcher

* update switcher dropdown modal

* undo

* undo new styles, adjust padding

* update padding

* use supported chain helper fn

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-24 11:51:38 -07:00
cartcrom
84fb05239b fix: data api loading states and repetitive calls (#4461)
* diagnosing issues
* fixed loading states
* addressed PR comments
* fixed missing symbol issue
* fixed merge conflcit
* fixed uppercase token symbol issue
2022-08-24 14:13:21 -04:00
Vignesh Mohankumar
7500bbc0be fix: don't translate UNI (#4467) 2022-08-24 14:00:20 -04:00
Jack Short
c43c8de6cd chore: updating breakpoints (#4465)
* chore: updating breakpoints

* deprecating old breakpoints

* using breakpoints in theme instead of media queries
2022-08-24 13:43:46 -04:00
Connor McEwen
9d40db5b21 fix: update swap CTA button color (#4464) 2022-08-24 10:42:56 -07:00
Kaylee George
a61eca36ae fix: change toggle and input placeholder colors on swap settings flyout (#4466)
change colors
2022-08-24 10:41:24 -07:00
Kaylee George
60479a442f fix: remove UNI currency logo from swap widget approval button (#4468)
take away logo
2022-08-24 10:41:11 -07:00
Kaylee George
5257188f70 fix: fix token details 'share to twitter' information (#4469)
add twitter correct link
2022-08-24 10:33:19 -07:00
lynn
7599239983 fix: Web 897 redesign general token selector from swap focusing text input has incorrect background color (#4463)
* init

* add search icon
2022-08-24 12:42:17 -04:00
lynn
1561c0d000 feat: fix visual bugs on truncate token description (#4462)
init
2022-08-24 12:28:07 -04:00
Charles Bachmeier
1f740cf8c0 feat: update search icon (#4459)
* feat: update search icon

* re add chevron on mobile

* icon uses currentColor

* icon uses currentColor

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-24 07:07:12 -07:00
aballerr
55c5f03004 feat: Adding in remaining transactions to transaction history (#4455)
* adding in remaining transactions: Wrap, Collect Fees, Approval and claim rewards

Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-08-24 09:59:11 -04:00
Charles Bachmeier
a345cff614 fix: cleanup styles for overflow modal (#4458)
* improvements to overflow modal

* add active state to overflow modal

* fix right aligned models

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-24 06:32:32 -07:00
lynn
85d8566cfa feat: make currencyInputPanel redesign changes apply only to swap (#4456)
* init: revert to pre redesign swap changes

* differentiate between swapCurrencyInputPanel and regular

* it was inverted whoops

* add padding to fiat row from cal
2022-08-24 02:00:48 -04:00
Charles Bachmeier
44d68e3ef0 fix: /pool position second icon zindex issue (#4457)
fix /pool zindex issue

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-23 21:05:58 -07:00
Greg Bugyis
04bd4900b0 feat: adds banner section (carousel only) for NFT Explore (#4383)
* NFT Explore: Banner section and carousel

* Fixes from PR feedback

* PR feedback and slight refactor of Carousel Progress indicators

* Only render current collection, and simplify fullWidth class

* Add colors to sprinkles and drop zIndex on bannerContent

* Simplify component structure

* Separate out CarouselIndicator and other cleanup

* Restore CarouselProgress component

* Position carousel progress over bg overlay

Co-authored-by: gbugyis <greg@bugyis.com>
2022-08-24 01:06:54 +03:00
github-actions[bot]
81f277b36f chore(i18n): new Crowdin translations (#4454)
* chore(i18n): synchronize translations from crowdin [skip ci]

* empty

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Vignesh Mohankumar <me@vig.xyz>
2022-08-23 16:47:51 -04:00
Connor McEwen
575660d3e8 fix: respect padding in cells with proper hover (#4451) 2022-08-23 16:41:42 -04:00
lynn
1e692491f1 fix: truncates to token details descriptions (#4450)
* inti

* respond to vm comments

* add paragraphs as per fred, respond vm comments

* respond last vm comment

* simplify

* move fn out of component

* fix messed up styling

* simplify

* simplify

* fred nits

* remove xtra token detail props

* fix sentence casing
2022-08-23 15:25:37 -04:00
Vignesh Mohankumar
b3639b3453 fix: makes SearchBar focus border-width consistent (#4452) 2022-08-23 11:44:01 -07:00
Kaylee George
53ebf37b40 feat: add top tokens query for Explore tokens table (#4448)
* progress

* map??

* idk

* maybe progress?

* more

* hook

* rm favs

* add query

* comment

* get rid of page query

* fix autoimports

* Update TokenRow.tsx
2022-08-23 11:13:35 -07:00
lynn
624ec33652 build: patch vanilla extract to speed up build time (#4447)
patch ve to build

Co-authored-by: Connor McEwen <connor.mcewen@gmail.com>
2022-08-23 12:02:27 -04:00
Charles Bachmeier
2890040118 feat: Fixed light mode styles and added hover states to navbar (#4449)
* cleanup navbar lightmode

* lightmode and hover states

* move magical gradient to common stlyes

* inherit border radius

* hover transition

* further split common gradient style

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-23 08:30:37 -07:00
aballerr
68db8b3e23 feat: wallet hover styling and moving wallet to correct navbar (#4444)
* updating wallet to have correct hover highlighting and have correct scrolling

Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-08-23 11:00:36 -04:00
Vignesh Mohankumar
9873491db1 fix: increases tokens search background contrast (#4443) 2022-08-22 15:17:40 -07:00
Vignesh Mohankumar
5d64ab0146 fix: flag NetworkFilter (#4442)
feat: flag token table network filter
2022-08-22 18:11:22 -04:00
github-actions[bot]
568267ce07 chore(i18n): new Crowdin translations (#4410)
* chore(i18n): synchronize translations from crowdin [skip ci]

* empty

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Vignesh Mohankumar <me@vig.xyz>
2022-08-22 17:29:40 -04:00
Vignesh Mohankumar
69cdefe996 fix: update Swap container padding (#4440)
* fix: update Swap container padding

* rm AlertWrapper

* empty
2022-08-22 17:28:17 -04:00
Charles Bachmeier
61758db589 fix: active state for Token Explore (#4439)
fix active state for Token Explore

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-22 14:23:34 -07:00
cartcrom
9e9d98bb31 fix: chart size responsiveness issue (#4441)
fixed responsiveness issue
2022-08-22 17:15:24 -04:00
lynn
6b2b771dc4 fix: correct mistaken usdc user property that should be weth (#4438)
fix
2022-08-22 16:33:00 -04:00
pp-hh-ii-ll
031bea0f50 fix: Update outdated colors (#4420)
* Update outdated colors and add shallowShadow

* Update colors.ts

Co-authored-by: Kaylee George <62825936+kayleegeorge@users.noreply.github.com>
2022-08-22 13:20:59 -07:00
Vignesh Mohankumar
6f2e447ec3 chore: remove fortmatic key (#4435)
chore: remove unused fortmatic key
2022-08-22 16:12:55 -04:00
Kaylee George
cf831fbcea refactor: removes flyoutDropShadow, adds shallowShadow (#4437)
flyout drop shadow
2022-08-22 12:58:58 -07:00
lynn
bcd4c1c182 feat: only track usdc, weth, native token balances (#4424)
* init

* fix unit test

* extract log token logic into custom hook
2022-08-22 15:31:28 -04:00
Kaylee George
d954026cea feat: add token details and token row table query (#4419)
* add query

* revert styles

* more style

* rename

* restructure

* fix

* more

* network

* uppercase symbol

* rm unused

* fix

* check nan

* add token row query

* small change

* nan?
2022-08-22 11:52:24 -07:00
Vignesh Mohankumar
2c2dad1415 build: enforce node/npm versions (#4434)
* build: enforce node/npm versions

* enforce yarn usage
2022-08-22 13:55:48 -04:00
Kaylee George
d42ed88845 fix: fix various selected states on Explore filters and update theme colors (#4433)
* fixed

* update more
2022-08-22 10:51:59 -07:00
Kaylee George
e12c00e980 fix: time selector updated to align with price chart (#4425)
* fix time

* displays
2022-08-22 09:37:49 -07:00
Jack Short
c25971e5d2 feat: collection asset cards (#4422)
* removing differentiating mobile and desktop collections

* feat: asset cards

* changed card to module + addressed other comments

* todo
2022-08-22 12:15:17 -04:00
Charles Bachmeier
293e56758c feat: Placeholder NFT Details and NFT Sell Pages (#4431)
* add placeholder pages for nft sell and nft details

* feature flag link in navbar dropdown

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-22 07:53:11 -07:00
tainguyen-kms
a6b17f0437 fix: Fix unresponsive image in vote page (#4416)
* Fix unresponsive image scale in vote page

* Create renderer function for Image
2022-08-19 23:19:05 -04:00
cartcrom
140d59b898 fix: updated schema (#4423)
updated schema
2022-08-19 17:40:58 -04:00
cartcrom
85742c5785 feat: data api connection (#4396)
* split relay into two environments, refactored schema grabbing, implemented api call for token price graph
2022-08-19 15:31:10 -04:00
aballerr
9b07d8ce64 feat: Wallet v1 transactions part 1 (#4398)
* Adding in tx history for 3 transactions: Swap, Add Liquidity, Remove Liquidty




Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.fios-router.home>
Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
2022-08-19 14:01:34 -04:00
Charles Bachmeier
b1b9da1b17 feat: enable NFT search results (#4413)
* enable NFT search results

* update usememo deps

* simplify flag trending token logic

* respond to comments

* lowercase bool

* rename flag

* improve truncation

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-19 09:58:26 -07:00
Vignesh Mohankumar
ffe670923e feat: remove fortmatic (#4167)
* rm fortmatic (wip for test)

* remove isChainAllowed

* handle unexpected connector

* clear on catch

* Revert "remove isChainAllowed"

This reverts commit d505cac39a446cb2c273016d03896469ba39eb60.

* fix to actually use ALLOWED_CHAIN_IDS

* fix
2022-08-19 12:06:14 -04:00
Charles Bachmeier
21649967aa refactor: rename phase1flag to nftflag (#4414)
* rename phase1flag to nftflag

* add new flag file

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-19 08:41:54 -07:00
lynn
3f40f60c1c fix: fix add liquidity select tokens alignment (#4403)
* init

* only remove this component in add liquidity screen
2022-08-18 23:30:15 -04:00
lynn
176c275a06 feat: Web 640 eng transaction states redesign (#4389)
* init

* pending and failed states

* simplify

* init

* modals

* fix drop shadow logic

* fixes for kaylee comments
2022-08-18 22:49:39 -04:00
Charles Bachmeier
ae2b4b1668 fix: fix skeleton hover state bug (#4409)
* fix skeleton hover state bug

* corretc hover color

* add transition

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-18 16:10:14 -07:00
Kaylee George
a27f8e2937 feat: add no information available state for token details (#4407)
* skeleton

* fix

* fix layout

* gap
2022-08-18 15:02:44 -07:00
Charles Bachmeier
818b1c84b0 feat: add NFTs tab to navbar (#4408)
add NFT tab to navbar

Co-authored-by: Charlie <charlie@uniswap.org>
2022-08-18 14:40:52 -07:00
Vignesh Mohankumar
75eceaa5e1 fix: Revert "feat: fix padding" (#4406)
Revert "feat: fix padding (#4400)"

This reverts commit 96c23af99c.
2022-08-18 17:36:32 -04:00
Charles Bachmeier
c6b4cc8e01 feat: add phase0 searchbar (#4377)
* feat: add phase0 searchbar

* exhaustive deps

* use router Link'

* use correct navigate for tokens

* useLocation

* add util function for organizing search results

* fix mobile navbar link

* remove exhausted depedencies

* split suggestion rows to their own file

* add new file

* use pathname instead of hash

* use imageholder classname

* fallback update

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2022-08-18 14:19:03 -07:00
Jack Short
819302b51f feat: adding collection stats to collection page (#4391)
* feat: adding collection stats

* removing debounced callback

* addressing comments

* updating marquee and updating isMobile hook

* adding bool to useIsMobile
2022-08-18 16:10:22 -04:00
Kaylee George
c53d7fcc32 fix: fix Explore search bar styling (#4405)
* fix search

* fix responsive

* fix width
2022-08-18 13:06:09 -07:00
lynn
3de2e65530 feat: fix hover color on vote page items (#4404)
init
2022-08-18 15:05:27 -04:00
Kaylee George
c5319b6bea fix: move favorites to right and fix header highlight (#4402)
* change favorite style

* more

* fix header

* fix styling

* small fix
2022-08-18 11:14:43 -07:00
github-actions[bot]
801ddc0886 chore(i18n): new Crowdin translations (#4395)
chore(i18n): synchronize translations from crowdin [skip ci]

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-08-18 11:06:14 -07:00
aballerr
dfd9196aa7 feat: Wallet p0 (#4368)
* P0 Wallet

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.fios-router.home>
2022-08-18 13:56:15 -04:00
lynn
c4362297f5 feat: Safety label speedbump: fix copy (#4401)
init
2022-08-18 13:17:22 -04:00
lynn
96c23af99c feat: fix padding (#4400)
fix padding
2022-08-18 12:13:20 -04:00
lynn
6a29dacdeb feat: fix explore and pools titles to match designs. (#4399)
init
2022-08-18 11:46:39 -04:00
lynn
9ddad80f2a feat: fix favorite tokens design (#4397)
fix favorites
2022-08-18 08:24:07 -07:00
245 changed files with 18466 additions and 6620 deletions

6
.env
View File

@@ -1,3 +1,7 @@
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_FORTMATIC_KEY="pk_live_357F77728B8EB880"
REACT_APP_AMPLITUDE_TEST_KEY="add-the-real-test-key-if-you-need-to-test-amplitude-events"
REACT_APP_AWS_API_REGION="us-east-2"
REACT_APP_AWS_API_ACCESS_KEY="AKIAYJJWW6AQ47ODATHN"
REACT_APP_AWS_API_ACCESS_SECRET="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"
REACT_APP_AWS_X_API_KEY="z9dReS5UtHu7iTrUsTuWRozLthi3AxOZlvobrIdr14"
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"

1
.gitignore vendored
View File

@@ -9,7 +9,6 @@
/src/locales/**/pseudo.po
# generated graphql types
/src/graphql/schema/
__generated__/
# dependencies

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict = true

View File

@@ -1,7 +1,6 @@
overrideExisting: true
schema: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3'
documents: 'src/**/!(*.d).{ts,tsx}'
generates:
./src/graphql/schema/schema.graphql:
./src/graphql/thegraph/schema/schema.graphql:
plugins:
- schema-ast

18
fetch-schema.js Normal file
View File

@@ -0,0 +1,18 @@
/* eslint-disable */
require('dotenv').config({ path: '.env.local' })
const { exec } = require('child_process')
const dataConfig = require('./relay.config')
const thegraphConfig = require('./relay_thegraph.config')
/* eslint-enable */
const THEGRAPH_API_URL = 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3'
exec(`get-graphql-schema ${THEGRAPH_API_URL} > ${thegraphConfig.schema}`)
const API_URL = process.env.REACT_APP_GQL_API_URL
const API_KEY = process.env.REACT_APP_GQL_API_KEY
if (API_URL && API_KEY) {
exec(`get-graphql-schema ${API_URL} --h X-API-KEY=${API_KEY} > ${dataConfig.schema}`)
} else {
console.log('REACT_APP_GQL_API_URL or REACT_APP_GQL_API_KEY is missing from env.local')
}

View File

@@ -8,8 +8,10 @@
"contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"",
"contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"./node_modules/@uniswap/**/artifacts/contracts/**/*[!dbg].json\"",
"contracts:compile": "yarn contracts:compile:abi && yarn contracts:compile:v3",
"relay": "relay-compiler",
"graphql:generate": "graphql-codegen --config codegen.yml && yarn relay",
"relay": "relay-compiler relay.config.js",
"relay-thegraph": "relay-compiler relay_thegraph.config.js",
"graphql:fetch": "node fetch-schema.js",
"graphql:generate": "yarn relay && yarn relay-thegraph",
"prei18n:extract": "node prei18n-extract.js",
"i18n:extract": "lingui extract --locale en-US",
"i18n:compile": "yarn i18n:extract && lingui compile",
@@ -21,12 +23,8 @@
"lint": "yarn eslint .",
"test": "craco test --coverage",
"cypress:open": "cypress open --browser chrome --e2e",
"cypress:run": "cypress run --browser chrome --e2e"
},
"relay": {
"src": "./src",
"language": "typescript",
"schema": "./src/graphql/schema/schema.graphql"
"cypress:run": "cypress run --browser chrome --e2e",
"postinstall": "patch-package"
},
"jest": {
"collectCoverageFrom": [
@@ -63,9 +61,6 @@
"devDependencies": {
"@craco/craco": "6.4.3",
"@ethersproject/experimental": "^5.4.0",
"@graphql-codegen/cli": "1.21.5",
"@graphql-codegen/schema-ast": "^2.5.1",
"@graphql-codegen/typescript-rtk-query": "^1.1.1",
"@lingui/cli": "^3.9.0",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1",
@@ -95,9 +90,9 @@
"@types/wcag-contrast": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^4",
"@typescript-eslint/parser": "^4",
"babel-plugin-relay": "^14.1.0",
"@vanilla-extract/babel-plugin": "^1.1.7",
"@vanilla-extract/webpack-plugin": "^2.1.11",
"babel-plugin-relay": "^14.1.0",
"cypress": "^10.3.1",
"env-cmd": "^10.1.0",
"eslint": "^7.11.0",
@@ -110,6 +105,8 @@
"eslint-plugin-unused-imports": "^2.0.0",
"jest-styled-components": "^7.0.8",
"ms.macro": "^2.0.0",
"patch-package": "^6.4.7",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.7.1",
"react-scripts": "^4.0.3",
"relay-compiler": "^14.1.0",
@@ -148,7 +145,7 @@
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@uniswap/v3-sdk": "^3.9.0",
"@uniswap/widgets": "^2.1.1",
"@uniswap/widgets": "^2.7.0",
"@vanilla-extract/css": "^1.7.2",
"@vanilla-extract/css-utils": "^0.1.2",
"@vanilla-extract/dynamic": "^2.0.2",
@@ -173,15 +170,15 @@
"ajv": "^6.12.3",
"array.prototype.flat": "^1.2.4",
"array.prototype.flatmap": "^1.2.4",
"aws4fetch": "^1.0.13",
"cids": "^1.0.0",
"clsx": "^1.1.1",
"copy-to-clipboard": "^3.2.0",
"d3": "^7.6.1",
"d3-curve-circlecorners": "^0.1.6",
"ethers": "^5.1.4",
"firebase": "^9.1.3",
"focus-visible": "^5.2.0",
"fortmatic": "^2.4.0",
"get-graphql-schema": "^2.1.2",
"graphql": "^16.5.0",
"graphql-request": "^3.4.0",
"immer": "^9.0.6",
@@ -202,6 +199,7 @@
"react-dom": "^18.2.0",
"react-feather": "^2.0.8",
"react-ga4": "^1.4.1",
"react-infinite-scroll-component": "^6.1.0",
"react-is": "^17.0.2",
"react-markdown": "^4.3.1",
"react-popper": "^2.2.3",
@@ -217,7 +215,6 @@
"rebass": "^4.0.7",
"redux": "^4.1.2",
"redux-localstorage-simple": "^2.3.1",
"relay-hooks": "^7.1.0",
"setimmediate": "^1.0.5",
"styled-components": "^5.3.5",
"tiny-invariant": "^1.2.0",
@@ -233,5 +230,10 @@
"workbox-precaching": "^6.1.0",
"workbox-routing": "^6.1.0",
"zustand": "^4.0.0-rc.1"
},
"engines": {
"npm": "please-use-yarn",
"node": "14",
"yarn": ">=1.22"
}
}

View File

@@ -0,0 +1,22 @@
diff --git a/node_modules/@vanilla-extract/css/dist/vanilla-extract-css.cjs.dev.js b/node_modules/@vanilla-extract/css/dist/vanilla-extract-css.cjs.dev.js
index 6e40061..10283a2 100644
--- a/node_modules/@vanilla-extract/css/dist/vanilla-extract-css.cjs.dev.js
+++ b/node_modules/@vanilla-extract/css/dist/vanilla-extract-css.cjs.dev.js
@@ -177,11 +177,13 @@ function generateIdentifier(debugId) {
var fileScopeHash = hash__default["default"](packageName ? "".concat(packageName).concat(filePath) : filePath);
var identifier = "".concat(fileScopeHash).concat(refCount);
- if (adapter_dist_vanillaExtractCssAdapter.getIdentOption() === 'debug') {
- var devPrefix = getDevPrefix(debugId);
+ if (process.env.VANILLA_EXTRACT_DEV_PREFIX) {
+ if (adapter_dist_vanillaExtractCssAdapter.getIdentOption() === 'debug') {
+ var devPrefix = getDevPrefix(debugId);
- if (devPrefix) {
- identifier = "".concat(devPrefix, "__").concat(identifier);
+ if (devPrefix) {
+ identifier = "".concat(devPrefix, "__").concat(identifier);
+ }
}
}

View File

@@ -1,5 +1,5 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const exec = require('child_process').exec
const { exec } = require('child_process')
const isWindows = process.platform === 'win32' || /^(msys|cygwin)$/.test(process.env.OSTYPE)
if (isWindows) {

View File

@@ -16,7 +16,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#ff007a" />
<meta name="fortmatic-site-verification" content="j93LgcVZk79qcgyo" />
<!--
manifest.json provides metadata used when the app is installed as a PWA.

6
relay.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
src: './src',
language: 'typescript',
schema: './src/graphql/data/schema.graphql',
exclude: ['**/node_modules/**', '**/__mocks__/**', '**/__generated__/**', '**/thegraph/**'],
}

9
relay_thegraph.config.js Normal file
View File

@@ -0,0 +1,9 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const defaultConfig = require('./relay.config')
module.exports = {
src: defaultConfig.src,
language: defaultConfig.language,
schema: './src/graphql/thegraph/schema.graphql',
exclude: ['**/node_modules/**', '**/__mocks__/**', '**/__generated__/**', '**/data/**'],
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

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

@@ -0,0 +1,3 @@
<svg width="10" height="12" viewBox="0 0 10 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.75 11C4.75 11 5.22377 11.1391 5.80923 10.5828L7.64433 8.65908C8.35405 7.91508 8.74998 6.92678 8.74989 5.89856C8.7498 4.72716 8.74971 3.31706 8.74991 2.50009C8.74996 2.22391 8.77618 2 8.5 2H8.25M6.74898 5.75L6.74979 2L6.74991 1.50009C6.74996 1.22391 6.52609 1 6.24991 1H4.25167C3.97553 1 3.75167 1.22386 3.75167 1.5V4.75039C3.75167 5.29859 3.52665 5.82276 3.12922 6.20034L1.6891 7.56856C1.10364 8.12478 1.10363 9.0266 1.68909 9.58283C2.12197 9.99409 2.75372 10.1013 3.29025 9.90438C3.47937 9.83497 3.65665 9.72779 3.80923 9.58283L5.80923 7.6827M6.74898 5.75L6.7487 6.36119C6.74861 6.63517 6.63611 6.89711 6.43748 7.08582L5.80923 7.6827M6.74898 5.75H6.4384C5.67845 5.75 5.19623 6.56419 5.56146 7.23061L5.80923 7.6827" stroke="white" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 871 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#99A1BD" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>

Before

Width:  |  Height:  |  Size: 294 B

After

Width:  |  Height:  |  Size: 300 B

View File

@@ -24,7 +24,7 @@ const HeaderRow = styled.div`
padding: 1rem 1rem;
font-weight: 500;
color: ${(props) => (props.color === 'blue' ? ({ theme }) => theme.deprecated_primary1 : 'inherit')};
${({ theme }) => theme.mediaWidth.upToMedium`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
padding: 1rem;
`};
`
@@ -74,7 +74,7 @@ const AccountGroupingRow = styled.div`
const AccountSection = styled.div`
padding: 0rem 1rem;
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1.5rem 1rem;`};
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`padding: 0rem 1rem 1.5rem 1rem;`};
`
const YourAccount = styled.div`

View File

@@ -0,0 +1,88 @@
import { useWeb3React } from '@web3-react/core'
import { UNI_ADDRESS } from 'constants/addresses'
import { TransactionInfo, TransactionType } from 'state/transactions/types'
import styled, { css } from 'styled-components/macro'
import { nativeOnChain } from '../../constants/tokens'
import { useCurrency } from '../../hooks/Tokens'
import CurrencyLogo from '../CurrencyLogo'
const CurrencyWrap = styled.div`
position: relative;
width: 36px;
height: 36px;
`
const CurrencyWrapStyles = css`
position: absolute;
height: 24px;
`
const CurrencyLogoWrap = styled.span<{ isCentered: boolean }>`
${CurrencyWrapStyles};
left: ${({ isCentered }) => (isCentered ? '50%' : '0')};
top: ${({ isCentered }) => (isCentered ? '50%' : '0')};
transform: ${({ isCentered }) => isCentered && 'translate(-50%, -50%)'};
`
const CurrencyLogoWrapTwo = styled.span`
${CurrencyWrapStyles};
bottom: 0px;
right: 0px;
`
interface CurrencyPair {
currencyId0: string | undefined
currencyId1: string | undefined
}
const getCurrency = ({ info, chainId }: { info: TransactionInfo; chainId: number | undefined }): CurrencyPair => {
switch (info.type) {
case TransactionType.ADD_LIQUIDITY_V3_POOL:
case TransactionType.REMOVE_LIQUIDITY_V3:
case TransactionType.CREATE_V3_POOL:
const { baseCurrencyId, quoteCurrencyId } = info
return { currencyId0: baseCurrencyId, currencyId1: quoteCurrencyId }
case TransactionType.SWAP:
const { inputCurrencyId, outputCurrencyId } = info
return { currencyId0: inputCurrencyId, currencyId1: outputCurrencyId }
case TransactionType.WRAP:
const { unwrapped } = info
const native = info.chainId ? nativeOnChain(info.chainId) : undefined
const base = 'ETH'
const wrappedCurrency = native?.wrapped.address ?? 'WETH'
return { currencyId0: unwrapped ? wrappedCurrency : base, currencyId1: unwrapped ? base : wrappedCurrency }
case TransactionType.COLLECT_FEES:
const { currencyId0, currencyId1 } = info
return { currencyId0, currencyId1 }
case TransactionType.APPROVAL:
return { currencyId0: info.tokenAddress, currencyId1: undefined }
case TransactionType.CLAIM:
const uniAddress = chainId ? UNI_ADDRESS[chainId] : undefined
return { currencyId0: uniAddress, currencyId1: undefined }
default:
return { currencyId0: undefined, currencyId1: undefined }
}
}
const LogoView = ({ info }: { info: TransactionInfo }) => {
const { chainId } = useWeb3React()
const { currencyId0, currencyId1 } = getCurrency({ info, chainId })
const currency0 = useCurrency(currencyId0)
const currency1 = useCurrency(currencyId1)
const isCentered = !(currency0 && currency1)
return (
<CurrencyWrap>
<CurrencyLogoWrap isCentered={isCentered}>
<CurrencyLogo size="24px" currency={currency0} />
</CurrencyLogoWrap>
{!isCentered && (
<CurrencyLogoWrapTwo>
<CurrencyLogo size="24px" currency={currency1} />
</CurrencyLogoWrapTwo>
)}
</CurrencyWrap>
)
}
export default LogoView

View File

@@ -0,0 +1,337 @@
import { Trans } from '@lingui/macro'
import { Fraction, TradeType } from '@uniswap/sdk-core'
import JSBI from 'jsbi'
import {
AddLiquidityV3PoolTransactionInfo,
ApproveTransactionInfo,
ClaimTransactionInfo,
CollectFeesTransactionInfo,
ExactInputSwapTransactionInfo,
ExactOutputSwapTransactionInfo,
RemoveLiquidityV3TransactionInfo,
TransactionInfo,
TransactionType,
WrapTransactionInfo,
} from 'state/transactions/types'
import styled from 'styled-components/macro'
import { nativeOnChain } from '../../constants/tokens'
import { useCurrency, useToken } from '../../hooks/Tokens'
import useENSName from '../../hooks/useENSName'
import { shortenAddress } from '../../utils'
import { TransactionState } from './index'
const HighlightText = styled.span`
color: ${({ theme }) => theme.textPrimary};
font-weight: 600;
`
const BodyWrap = styled.div`
line-height: 20px;
`
interface ActionProps {
pending: JSX.Element
success: JSX.Element
failed: JSX.Element
transactionState: TransactionState
}
const Action = ({ pending, success, failed, transactionState }: ActionProps) => {
switch (transactionState) {
case TransactionState.Failed:
return failed
case TransactionState.Success:
return success
default:
return pending
}
}
const formatAmount = (amountRaw: string, decimals: number, sigFigs: number): string =>
new Fraction(amountRaw, JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(decimals))).toSignificant(sigFigs)
const FailedText = ({ transactionState }: { transactionState: TransactionState }) =>
transactionState === TransactionState.Failed ? <Trans>failed</Trans> : <span />
const FormattedCurrencyAmount = ({
rawAmount,
currencyId,
sigFigs = 2,
}: {
rawAmount: string
currencyId: string
sigFigs: number
}) => {
const currency = useCurrency(currencyId)
return currency ? (
<HighlightText>
{formatAmount(rawAmount, currency.decimals, sigFigs)} {currency.symbol}
</HighlightText>
) : null
}
const getRawAmounts = (
info: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo
): { rawAmountFrom: string; rawAmountTo: string } => {
return info.tradeType === TradeType.EXACT_INPUT
? { rawAmountFrom: info.inputCurrencyAmountRaw, rawAmountTo: info.expectedOutputCurrencyAmountRaw }
: { rawAmountFrom: info.expectedInputCurrencyAmountRaw, rawAmountTo: info.outputCurrencyAmountRaw }
}
const SwapSummary = ({
info,
transactionState,
}: {
info: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo
transactionState: TransactionState
}) => {
const actionProps = {
transactionState,
pending: <Trans>Swapping</Trans>,
success: <Trans>Swapped</Trans>,
failed: <Trans>Swap</Trans>,
}
const { rawAmountFrom, rawAmountTo } = getRawAmounts(info)
return (
<BodyWrap>
<Action {...actionProps} />{' '}
<FormattedCurrencyAmount rawAmount={rawAmountFrom} currencyId={info.inputCurrencyId} sigFigs={2} />{' '}
<Trans>for </Trans>{' '}
<FormattedCurrencyAmount rawAmount={rawAmountTo} currencyId={info.outputCurrencyId} sigFigs={2} />{' '}
<FailedText transactionState={transactionState} />
</BodyWrap>
)
}
const AddLiquidityV3PoolSummary = ({
info,
transactionState,
}: {
info: AddLiquidityV3PoolTransactionInfo
transactionState: TransactionState
}) => {
const { createPool, quoteCurrencyId, baseCurrencyId } = info
const actionProps = {
transactionState,
pending: <Trans>Adding</Trans>,
success: <Trans>Added</Trans>,
failed: <Trans>Add</Trans>,
}
return (
<BodyWrap>
{createPool ? (
<CreateV3PoolSummary info={info} transactionState={transactionState} />
) : (
<>
<Action {...actionProps} />{' '}
<FormattedCurrencyAmount rawAmount={info.expectedAmountBaseRaw} currencyId={baseCurrencyId} sigFigs={2} />{' '}
<Trans>and</Trans>{' '}
<FormattedCurrencyAmount rawAmount={info.expectedAmountQuoteRaw} currencyId={quoteCurrencyId} sigFigs={2} />
</>
)}{' '}
<FailedText transactionState={transactionState} />
</BodyWrap>
)
}
const RemoveLiquidityV3Summary = ({
info: { baseCurrencyId, quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw },
transactionState,
}: {
info: RemoveLiquidityV3TransactionInfo
transactionState: TransactionState
}) => {
const actionProps = {
transactionState,
pending: <Trans>Removing</Trans>,
success: <Trans>Removed</Trans>,
failed: <Trans>Remove</Trans>,
}
return (
<BodyWrap>
<Action {...actionProps} />{' '}
<FormattedCurrencyAmount rawAmount={expectedAmountBaseRaw} currencyId={baseCurrencyId} sigFigs={2} />{' '}
<Trans>and</Trans>{' '}
<FormattedCurrencyAmount rawAmount={expectedAmountQuoteRaw} currencyId={quoteCurrencyId} sigFigs={2} />{' '}
<FailedText transactionState={transactionState} />
</BodyWrap>
)
}
const CreateV3PoolSummary = ({
info: { baseCurrencyId, quoteCurrencyId },
transactionState,
}: {
info: AddLiquidityV3PoolTransactionInfo
transactionState: TransactionState
}) => {
const baseCurrency = useCurrency(baseCurrencyId)
const quoteCurrency = useCurrency(quoteCurrencyId)
const actionProps = {
transactionState,
pending: <Trans>Creating</Trans>,
success: <Trans>Created</Trans>,
failed: <Trans>Create</Trans>,
}
return (
<BodyWrap>
<Action {...actionProps} />{' '}
<HighlightText>
{baseCurrency?.symbol}/{quoteCurrency?.symbol}{' '}
</HighlightText>
<Trans>Pool</Trans> <FailedText transactionState={transactionState} />
</BodyWrap>
)
}
const CollectFeesSummary = ({
info,
transactionState,
}: {
info: CollectFeesTransactionInfo
transactionState: TransactionState
}) => {
const { currencyId0, expectedCurrencyOwed0 = '0', expectedCurrencyOwed1 = '0', currencyId1 } = info
const actionProps = {
transactionState,
pending: <Trans>Collecting</Trans>,
success: <Trans>Collected</Trans>,
failed: <Trans>Collect</Trans>,
}
return (
<BodyWrap>
<Action {...actionProps} />{' '}
<FormattedCurrencyAmount rawAmount={expectedCurrencyOwed0} currencyId={currencyId0} sigFigs={2} />{' '}
<Trans>and</Trans>{' '}
<FormattedCurrencyAmount rawAmount={expectedCurrencyOwed1} currencyId={currencyId1} sigFigs={2} />{' '}
<Trans>fees</Trans> <FailedText transactionState={transactionState} />
</BodyWrap>
)
}
const ApprovalSummary = ({
info,
transactionState,
}: {
info: ApproveTransactionInfo
transactionState: TransactionState
}) => {
const token = useToken(info.tokenAddress)
const actionProps = {
transactionState,
pending: <Trans>Approving</Trans>,
success: <Trans>Approved</Trans>,
failed: <Trans>Approve</Trans>,
}
return (
<BodyWrap>
<Action {...actionProps} /> <HighlightText>{token?.symbol}</HighlightText>{' '}
<FailedText transactionState={transactionState} />
</BodyWrap>
)
}
const ClaimSummary = ({
info: { recipient, uniAmountRaw },
transactionState,
}: {
info: ClaimTransactionInfo
transactionState: TransactionState
}) => {
const { ENSName } = useENSName()
const actionProps = {
transactionState,
pending: <Trans>Claiming</Trans>,
success: <Trans>Claimed</Trans>,
failed: <Trans>Claim</Trans>,
}
return (
<BodyWrap>
{uniAmountRaw && (
<>
<Action {...actionProps} />{' '}
<HighlightText>
{formatAmount(uniAmountRaw, 18, 4)}
UNI{' '}
</HighlightText>{' '}
<Trans>for</Trans> <HighlightText>{ENSName ?? shortenAddress(recipient)}</HighlightText>
</>
)}{' '}
<FailedText transactionState={transactionState} />
</BodyWrap>
)
}
const WrapSummary = ({
info: { chainId, currencyAmountRaw, unwrapped },
transactionState,
}: {
info: WrapTransactionInfo
transactionState: TransactionState
}) => {
const native = chainId ? nativeOnChain(chainId) : undefined
const from = unwrapped ? native?.wrapped.symbol ?? 'WETH' : native?.symbol ?? 'ETH'
const to = unwrapped ? native?.symbol ?? 'ETH' : native?.wrapped.symbol ?? 'WETH'
const amount = formatAmount(currencyAmountRaw, 18, 6)
const actionProps = unwrapped
? {
transactionState,
pending: <Trans>Unwrapping</Trans>,
success: <Trans>Unwrapped</Trans>,
failed: <Trans>Unwrap</Trans>,
}
: {
transactionState,
pending: <Trans>Wrapping</Trans>,
success: <Trans>Wrapped</Trans>,
failed: <Trans>Wrap</Trans>,
}
return (
<BodyWrap>
<Action {...actionProps} />{' '}
<HighlightText>
{amount} {from}
</HighlightText>{' '}
<Trans>to</Trans>{' '}
<HighlightText>
{amount} {to}
</HighlightText>{' '}
<FailedText transactionState={transactionState} />
</BodyWrap>
)
}
const TransactionBody = ({ info, transactionState }: { info: TransactionInfo; transactionState: TransactionState }) => {
switch (info.type) {
case TransactionType.SWAP:
return <SwapSummary info={info} transactionState={transactionState} />
case TransactionType.ADD_LIQUIDITY_V3_POOL:
return <AddLiquidityV3PoolSummary info={info} transactionState={transactionState} />
case TransactionType.REMOVE_LIQUIDITY_V3:
return <RemoveLiquidityV3Summary info={info} transactionState={transactionState} />
case TransactionType.WRAP:
return <WrapSummary info={info} transactionState={transactionState} />
case TransactionType.COLLECT_FEES:
return <CollectFeesSummary info={info} transactionState={transactionState} />
case TransactionType.APPROVAL:
return <ApprovalSummary info={info} transactionState={transactionState} />
case TransactionType.CLAIM:
return <ClaimSummary info={info} transactionState={transactionState} />
default:
return <span />
}
}
export default TransactionBody

View File

@@ -0,0 +1,90 @@
import { useWeb3React } from '@web3-react/core'
import { getChainInfoOrDefault } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { useMemo } from 'react'
import { AlertTriangle, CheckCircle } from 'react-feather'
import styled from 'styled-components/macro'
import { colors } from 'theme/colors'
import { TransactionDetails } from '../../state/transactions/types'
import Loader from '../Loader'
import LogoView from './LogoView'
import TransactionBody from './TransactionBody'
export enum TransactionState {
Pending,
Success,
Failed,
}
const Grid = styled.a`
cursor: pointer;
display: grid;
grid-template-columns: 44px auto 24px;
width: 100%;
text-decoration: none;
border-bottom: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
padding: 12px;
&:hover {
background-color: ${({ theme }) => theme.backgroundModule};
transition: 250ms background-color ease;
}
`
const TextContainer = styled.span`
font-size: 14px;
margin-top: auto;
margin-bottom: auto;
color: ${({ theme }) => theme.textTertiary};
`
const IconStyleWrap = styled.span`
margin-top: auto;
margin-bottom: auto;
margin-left: auto;
height: 16px;
`
export const TransactionSummary = ({ transactionDetails }: { transactionDetails: TransactionDetails }) => {
const { chainId = 1 } = useWeb3React()
const tx = transactionDetails
const { explorer } = getChainInfoOrDefault(chainId ? chainId : SupportedChainId.MAINNET)
const { info, receipt, hash } = tx
const transactionState = useMemo(() => {
const pending = !receipt
const success = !pending && tx && (receipt?.status === 1 || typeof receipt?.status === 'undefined')
const transactionState = pending
? TransactionState.Pending
: success
? TransactionState.Success
: TransactionState.Failed
return transactionState
}, [receipt, tx])
const link = `${explorer}tx/${hash}`
return chainId ? (
<Grid href={link} target="_blank">
<LogoView info={info} />
<TextContainer as="span">
<TransactionBody info={info} transactionState={transactionState} />
</TextContainer>
{transactionState === TransactionState.Pending ? (
<IconStyleWrap>
<Loader />
</IconStyleWrap>
) : transactionState === TransactionState.Success ? (
<IconStyleWrap>
<CheckCircle color={colors.green200} size="16px" />
</IconStyleWrap>
) : (
<IconStyleWrap>
<AlertTriangle color={colors.gold200} size="16px" />
</IconStyleWrap>
)}
</Grid>
) : null
}

View File

@@ -37,16 +37,9 @@ export enum CUSTOM_USER_PROPERTIES {
SCREEN_RESOLUTION_HEIGHT = 'screen_resolution_height',
SCREEN_RESOLUTION_WIDTH = 'screen_resolution_width',
WALLET_ADDRESS = 'wallet_address',
WALLET_NATIVE_CURRENCY_BALANCE_USD = 'wallet_native_currency_balance_usd',
WALLET_TOKENS_ADDRESSES = 'wallet_tokens_addresses',
WALLET_TOKENS_SYMBOLS = 'wallet_tokens_symbols',
WALLET_TYPE = 'wallet_type',
}
export enum CUSTOM_USER_PROPERTY_SUFFIXES {
WALLET_TOKEN_AMOUNT_SUFFIX = '_token_amount',
}
export enum BROWSER {
FIREFOX = 'Mozilla Firefox',
SAMSUNG = 'Samsung Internet',

View File

@@ -176,23 +176,28 @@ export const ButtonOutlined = styled(BaseButton)`
}
`
export const ButtonYellow = styled(BaseButton)`
background-color: ${({ theme }) => theme.deprecated_yellow3};
color: white;
export const ButtonYellow = styled(BaseButton)<{ redesignFlag?: boolean }>`
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.accentWarningSoft : theme.deprecated_yellow3)};
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.accentWarning : 'white')};
&:focus {
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.05, theme.deprecated_yellow3)};
background-color: ${({ theme }) => darken(0.05, theme.deprecated_yellow3)};
box-shadow: ${({ theme, redesignFlag }) => !redesignFlag && `0 0 0 1pt ${theme.deprecated_yellow3}`};
background-color: ${({ theme, redesignFlag }) =>
redesignFlag ? theme.accentWarningSoft : darken(0.05, theme.deprecated_yellow3)};
}
&:hover {
background-color: ${({ theme }) => darken(0.05, theme.deprecated_yellow3)};
background: ${({ theme, redesignFlag }) => redesignFlag && theme.stateOverlayHover};
mix-blend-mode: ${({ redesignFlag }) => redesignFlag && 'normal'};
background-color: ${({ theme, redesignFlag }) => !redesignFlag && darken(0.05, theme.deprecated_yellow3)};
}
&:active {
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.1, theme.deprecated_yellow3)};
background-color: ${({ theme }) => darken(0.1, theme.deprecated_yellow3)};
box-shadow: ${({ theme, redesignFlag }) => !redesignFlag && `0 0 0 1pt ${darken(0.1, theme.deprecated_yellow3)}`};
background-color: ${({ theme, redesignFlag }) =>
redesignFlag ? theme.accentWarningSoft : darken(0.1, theme.deprecated_yellow3)};
}
&:disabled {
background-color: ${({ theme }) => theme.deprecated_yellow3};
opacity: 50%;
background-color: ${({ theme, redesignFlag }) =>
redesignFlag ? theme.accentWarningSoft : theme.deprecated_yellow3};
opacity: ${({ redesignFlag }) => (redesignFlag ? '60%' : '50%')};
cursor: auto;
}
`

View File

@@ -1,7 +1,6 @@
import { Group } from '@visx/group'
import { LinePath } from '@visx/shape'
import { CurveFactory } from 'd3'
import { radius } from 'd3-curve-circlecorners'
import React from 'react'
import { ReactNode } from 'react'
import { useTheme } from 'styled-components/macro'
@@ -12,7 +11,7 @@ interface LineChartProps<T> {
getX: (t: T) => number
getY: (t: T) => number
marginTop?: number
curve?: CurveFactory
curve: CurveFactory
color?: Color
strokeWidth: number
children?: ReactNode
@@ -37,7 +36,7 @@ function LineChart<T>({
<svg width={width} height={height}>
<Group top={marginTop}>
<LinePath
curve={curve ?? radius(0.25)}
curve={curve}
stroke={color ?? theme.accentAction}
strokeWidth={strokeWidth}
data={data}

View File

@@ -1,4 +1,4 @@
import { scaleLinear } from 'd3'
import { curveCardinalOpen, scaleLinear } from 'd3'
import React from 'react'
import { useTheme } from 'styled-components/macro'
@@ -37,6 +37,7 @@ function SparklineChart({ width, height }: SparklineChartProps) {
data={pricePoints}
getX={(p: PricePoint) => timeScale(p.timestamp)}
getY={(p: PricePoint) => rdScale(p.value)}
curve={curveCardinalOpen.tension(0.9)}
marginTop={0}
color={isPositive ? theme.accentSuccess : theme.accentFailure}
strokeWidth={1.5}

View File

@@ -0,0 +1,423 @@
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { AutoColumn } from 'components/Column'
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
import { isSupportedChain } from 'constants/chains'
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import { darken } from 'polished'
import { ReactNode, useCallback, useState } from 'react'
import { Lock } from 'react-feather'
import { useLocation } from 'react-router-dom'
import styled, { useTheme } from 'styled-components/macro'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
import { useCurrencyBalance } from '../../state/connection/hooks'
import { ThemedText } from '../../theme'
import { ButtonGray } from '../Button'
import CurrencyLogo from '../CurrencyLogo'
import DoubleCurrencyLogo from '../DoubleLogo'
import { Input as NumericalInput } from '../NumericalInput'
import { RowBetween, RowFixed } from '../Row'
import CurrencySearchModal from '../SearchModal/CurrencySearchModal'
import { FiatValue } from './FiatValue'
const InputPanel = styled.div<{ hideInput?: boolean; redesignFlag: boolean }>`
${({ theme }) => theme.flexColumnNoWrap}
position: relative;
border-radius: ${({ hideInput }) => (hideInput ? '16px' : '20px')};
background-color: ${({ theme, redesignFlag, hideInput }) =>
redesignFlag ? 'transparent' : hideInput ? 'transparent' : theme.deprecated_bg2};
z-index: 1;
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
transition: height 1s ease;
will-change: height;
`
const FixedContainer = styled.div<{ redesignFlag: boolean }>`
width: 100%;
height: 100%;
position: absolute;
border-radius: 20px;
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_bg2)};
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
`
const Container = styled.div<{ hideInput: boolean; disabled: boolean; redesignFlag: boolean }>`
min-height: ${({ redesignFlag }) => redesignFlag && '69px'};
border-radius: ${({ hideInput }) => (hideInput ? '16px' : '20px')};
border: 1px solid ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_bg0)};
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_bg1)};
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
${({ theme, hideInput, disabled, redesignFlag }) =>
!redesignFlag &&
!disabled &&
`
:focus,
:hover {
border: 1px solid ${hideInput ? ' transparent' : theme.deprecated_bg3};
}
`}
`
const CurrencySelect = styled(ButtonGray)<{
visible: boolean
selected: boolean
hideInput?: boolean
disabled?: boolean
redesignFlag: boolean
}>`
align-items: center;
background-color: ${({ selected, theme, redesignFlag }) =>
redesignFlag
? selected
? theme.backgroundSurface
: theme.accentAction
: selected
? theme.deprecated_bg2
: theme.deprecated_primary1};
opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)};
box-shadow: ${({ selected }) => (selected ? 'none' : '0px 6px 10px rgba(0, 0, 0, 0.075)')};
color: ${({ selected, theme }) => (selected ? theme.deprecated_text1 : theme.deprecated_white)};
cursor: pointer;
height: ${({ hideInput, redesignFlag }) => (redesignFlag ? 'unset' : hideInput ? '2.8rem' : '2.4rem')};
border-radius: 16px;
outline: none;
user-select: none;
border: none;
font-size: 24px;
font-weight: 400;
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
padding: ${({ selected, redesignFlag }) =>
redesignFlag ? (selected ? '4px 8px 4px 4px' : '6px 6px 6px 8px') : '0 8px'};
gap: ${({ redesignFlag }) => (redesignFlag ? '8px' : '0px')};
justify-content: space-between;
margin-left: ${({ hideInput }) => (hideInput ? '0' : '12px')};
:focus,
:hover {
background-color: ${({ selected, theme, redesignFlag }) =>
selected
? redesignFlag
? theme.backgroundSurface
: theme.deprecated_bg3
: darken(0.05, theme.deprecated_primary1)};
}
visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')};
`
const InputCurrencySelect = styled(CurrencySelect)<{ redesignFlag: boolean }>`
background-color: ${({ theme, selected, redesignFlag }) =>
redesignFlag && (selected ? theme.backgroundModule : theme.accentAction)};
:focus,
:hover {
background-color: ${({ selected, theme, redesignFlag }) =>
selected
? redesignFlag
? theme.backgroundInteractive
: theme.deprecated_bg3
: darken(0.05, theme.deprecated_primary1)};
}
`
const InputRow = styled.div<{ selected: boolean; redesignFlag: boolean }>`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
justify-content: space-between;
padding: ${({ selected, redesignFlag }) =>
redesignFlag ? '0px' : selected ? ' 1rem 1rem 0.75rem 1rem' : '1rem 1rem 1rem 1rem'};
`
const LabelRow = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
color: ${({ theme }) => theme.deprecated_text1};
font-size: 0.75rem;
line-height: 1rem;
padding: 0 1rem 1rem;
span:hover {
cursor: pointer;
color: ${({ theme }) => darken(0.2, theme.deprecated_text2)};
}
`
const FiatRow = styled(LabelRow)<{ redesignFlag: boolean }>`
justify-content: flex-end;
min-height: ${({ redesignFlag }) => redesignFlag && '32px'};
padding: ${({ redesignFlag }) => redesignFlag && '8px 0px'};
height: ${({ redesignFlag }) => !redesignFlag && '24px'};
`
const NoBalanceState = styled.div`
color: ${({ theme }) => theme.textTertiary};
font-weight: 400;
justify-content: space-between;
padding: 0px 4px 1px 4px;
`
const NoBalanceDash = styled.span`
color: ${({ theme }) => theme.textTertiary};
font-variant: small-caps;
font-feature-settings: 'pnum' on, 'lnum' on;
`
const Aligner = styled.span`
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
`
const StyledDropDown = styled(DropDown)<{ selected: boolean; redesignFlag: boolean }>`
margin: 0 0.25rem 0 0.35rem;
height: 35%;
margin-left: ${({ redesignFlag }) => redesignFlag && '8px'};
path {
stroke: ${({ selected, theme }) => (selected ? theme.deprecated_text1 : theme.deprecated_white)};
stroke-width: ${({ redesignFlag }) => (redesignFlag ? '2px' : '1.5px')};
}
`
const StyledTokenName = styled.span<{ active?: boolean; redesignFlag: boolean }>`
${({ active }) => (active ? ' margin: 0 0.25rem 0 0.25rem;' : ' margin: 0 0.25rem 0 0.25rem;')}
font-size: ${({ active }) => (active ? '18px' : '18px')};
font-weight: ${({ redesignFlag }) => (redesignFlag ? '600' : '500')};
`
const StyledBalanceMax = styled.button<{ disabled?: boolean; redesignFlag: boolean }>`
background-color: transparent;
background-color: ${({ theme, redesignFlag }) => !redesignFlag && theme.deprecated_primary5};
border: none;
text-transform: ${({ redesignFlag }) => !redesignFlag && 'uppercase'};
border-radius: ${({ redesignFlag }) => !redesignFlag && '12px'};
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.accentAction : theme.deprecated_primary1)};
cursor: pointer;
font-size: ${({ redesignFlag }) => (redesignFlag ? '14px' : '11px')};
font-weight: ${({ redesignFlag }) => (redesignFlag ? '600' : '500')};
margin-left: ${({ redesignFlag }) => (redesignFlag ? '0px' : '0.25rem')};
opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)};
padding: 4px 6px;
pointer-events: ${({ disabled }) => (!disabled ? 'initial' : 'none')};
:hover {
opacity: ${({ disabled }) => (!disabled ? 0.8 : 0.4)};
}
:focus {
outline: none;
}
`
const StyledNumericalInput = styled(NumericalInput)<{ $loading: boolean; redesignFlag: boolean }>`
${loadingOpacityMixin};
text-align: left;
font-variant: ${({ redesignFlag }) => redesignFlag && 'small-caps'};
font-feature-settings: ${({ redesignFlag }) => redesignFlag && 'pnum on, lnum on'};
`
interface SwapCurrencyInputPanelProps {
value: string
onUserInput: (value: string) => void
onMax?: () => void
showMaxButton: boolean
label?: ReactNode
onCurrencySelect?: (currency: Currency) => void
currency?: Currency | null
hideBalance?: boolean
pair?: Pair | null
hideInput?: boolean
otherCurrency?: Currency | null
fiatValue?: CurrencyAmount<Token> | null
priceImpact?: Percent
id: string
showCommonBases?: boolean
showCurrencyAmount?: boolean
disableNonToken?: boolean
renderBalance?: (amount: CurrencyAmount<Currency>) => ReactNode
locked?: boolean
loading?: boolean
}
export default function SwapCurrencyInputPanel({
value,
onUserInput,
onMax,
showMaxButton,
onCurrencySelect,
currency,
otherCurrency,
id,
showCommonBases,
showCurrencyAmount,
disableNonToken,
renderBalance,
fiatValue,
priceImpact,
hideBalance = false,
pair = null, // used for double token logo
hideInput = false,
locked = false,
loading = false,
...rest
}: SwapCurrencyInputPanelProps) {
const [modalOpen, setModalOpen] = useState(false)
const { account, chainId } = useWeb3React()
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined)
const theme = useTheme()
const { pathname } = useLocation()
const isAddLiquidityPage = pathname.includes('/add') && !pathname.includes('/add/v2')
const handleDismissSearch = useCallback(() => {
setModalOpen(false)
}, [setModalOpen])
const chainAllowed = isSupportedChain(chainId)
return (
<InputPanel id={id} hideInput={hideInput} {...rest} redesignFlag={redesignFlagEnabled}>
{locked && (
<FixedContainer redesignFlag={redesignFlagEnabled}>
<AutoColumn gap="sm" justify="center">
<Lock />
<ThemedText.DeprecatedLabel fontSize="12px" textAlign="center" padding="0 12px">
<Trans>The market price is outside your specified price range. Single-asset deposit only.</Trans>
</ThemedText.DeprecatedLabel>
</AutoColumn>
</FixedContainer>
)}
<Container hideInput={hideInput} disabled={!chainAllowed} redesignFlag={redesignFlagEnabled}>
<InputRow
style={hideInput ? { padding: '0', borderRadius: '8px' } : {}}
selected={!onCurrencySelect}
redesignFlag={redesignFlagEnabled}
>
{!hideInput && (
<StyledNumericalInput
className="token-amount-input"
value={value}
onUserInput={onUserInput}
disabled={!chainAllowed}
$loading={loading}
redesignFlag={redesignFlagEnabled}
/>
)}
<InputCurrencySelect
disabled={!chainAllowed}
visible={currency !== undefined}
selected={!!currency}
hideInput={hideInput}
redesignFlag={redesignFlagEnabled}
className="open-currency-select-button"
onClick={() => {
if (onCurrencySelect) {
setModalOpen(true)
}
}}
>
<Aligner>
<RowFixed>
{pair ? (
<span style={{ marginRight: '0.5rem' }}>
<DoubleCurrencyLogo currency0={pair.token0} currency1={pair.token1} size={24} margin={true} />
</span>
) : currency ? (
<CurrencyLogo style={{ marginRight: '2px' }} currency={currency} size={'24px'} />
) : null}
{pair ? (
<StyledTokenName className="pair-name-container" redesignFlag={redesignFlagEnabled}>
{pair?.token0.symbol}:{pair?.token1.symbol}
</StyledTokenName>
) : (
<StyledTokenName
className="token-symbol-container"
active={Boolean(currency && currency.symbol)}
redesignFlag={redesignFlagEnabled}
>
{(currency && currency.symbol && currency.symbol.length > 20
? currency.symbol.slice(0, 4) +
'...' +
currency.symbol.slice(currency.symbol.length - 5, currency.symbol.length)
: currency?.symbol) || <Trans>Select token</Trans>}
</StyledTokenName>
)}
</RowFixed>
{onCurrencySelect && <StyledDropDown selected={!!currency} redesignFlag={redesignFlagEnabled} />}
</Aligner>
</InputCurrencySelect>
</InputRow>
{redesignFlagEnabled && !currency && !isAddLiquidityPage && (
<NoBalanceState>
<FiatRow redesignFlag={redesignFlagEnabled}>
<RowBetween>
<NoBalanceDash>-</NoBalanceDash>
<NoBalanceDash>-</NoBalanceDash>
</RowBetween>
</FiatRow>
</NoBalanceState>
)}
{!hideInput && !hideBalance && currency && (
<FiatRow redesignFlag={redesignFlagEnabled}>
<RowBetween>
<LoadingOpacityContainer $loading={loading}>
<FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />
</LoadingOpacityContainer>
{account ? (
<RowFixed style={{ height: '17px' }}>
<ThemedText.DeprecatedBody
onClick={onMax}
color={theme.deprecated_text3}
fontWeight={500}
fontSize={14}
style={{ display: 'inline', cursor: 'pointer' }}
>
{!hideBalance && currency && selectedCurrencyBalance ? (
renderBalance ? (
renderBalance(selectedCurrencyBalance)
) : (
<Trans>Balance: {formatCurrencyAmount(selectedCurrencyBalance, 4)}</Trans>
)
) : null}
</ThemedText.DeprecatedBody>
{showMaxButton && selectedCurrencyBalance ? (
<TraceEvent
events={[Event.onClick]}
name={EventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
element={ElementName.MAX_TOKEN_AMOUNT_BUTTON}
>
<StyledBalanceMax onClick={onMax} redesignFlag={redesignFlagEnabled}>
<Trans>Max</Trans>
</StyledBalanceMax>
</TraceEvent>
) : null}
</RowFixed>
) : (
<span />
)}
</RowBetween>
</FiatRow>
)}
</Container>
{onCurrencySelect && (
<CurrencySearchModal
isOpen={modalOpen}
onDismiss={handleDismissSearch}
onCurrencySelect={onCurrencySelect}
selectedCurrency={currency}
otherSelectedCurrency={otherCurrency}
showCommonBases={showCommonBases}
showCurrencyAmount={showCurrencyAmount}
disableNonToken={disableNonToken}
/>
)}
</InputPanel>
)
}

View File

@@ -25,37 +25,35 @@ import { RowBetween, RowFixed } from '../Row'
import CurrencySearchModal from '../SearchModal/CurrencySearchModal'
import { FiatValue } from './FiatValue'
const InputPanel = styled.div<{ hideInput?: boolean; redesignFlag: boolean }>`
const InputPanel = styled.div<{ hideInput?: boolean }>`
${({ theme }) => theme.flexColumnNoWrap}
position: relative;
border-radius: ${({ hideInput }) => (hideInput ? '16px' : '20px')};
background-color: ${({ theme, redesignFlag, hideInput }) =>
redesignFlag ? 'transparent' : hideInput ? 'transparent' : theme.deprecated_bg2};
background-color: ${({ theme, hideInput }) => (hideInput ? 'transparent' : theme.deprecated_bg2)};
z-index: 1;
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
transition: height 1s ease;
will-change: height;
`
const FixedContainer = styled.div<{ redesignFlag: boolean }>`
const FixedContainer = styled.div`
width: 100%;
height: 100%;
position: absolute;
border-radius: 20px;
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_bg2)};
background-color: ${({ theme }) => theme.deprecated_bg2};
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
`
const Container = styled.div<{ hideInput: boolean; disabled: boolean; redesignFlag: boolean }>`
const Container = styled.div<{ hideInput: boolean; disabled: boolean }>`
border-radius: ${({ hideInput }) => (hideInput ? '16px' : '20px')};
border: 1px solid ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_bg0)};
background-color: ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_bg1)};
border: 1px solid ${({ theme }) => theme.deprecated_bg0};
background-color: ${({ theme }) => theme.deprecated_bg1};
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
${({ theme, hideInput, disabled, redesignFlag }) =>
!redesignFlag &&
${({ theme, hideInput, disabled }) =>
!disabled &&
`
:focus,
@@ -70,65 +68,38 @@ const CurrencySelect = styled(ButtonGray)<{
selected: boolean
hideInput?: boolean
disabled?: boolean
redesignFlag: boolean
}>`
align-items: center;
background-color: ${({ selected, theme, redesignFlag }) =>
redesignFlag
? selected
? theme.backgroundSurface
: theme.accentAction
: selected
? theme.deprecated_bg2
: theme.deprecated_primary1};
background-color: ${({ selected, theme }) => (selected ? theme.deprecated_bg2 : theme.deprecated_primary1)};
opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)};
box-shadow: ${({ selected }) => (selected ? 'none' : '0px 6px 10px rgba(0, 0, 0, 0.075)')};
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
color: ${({ selected, theme }) => (selected ? theme.deprecated_text1 : theme.deprecated_white)};
cursor: pointer;
height: ${({ hideInput, redesignFlag }) => (redesignFlag ? 'unset' : hideInput ? '2.8rem' : '2.4rem')};
border-radius: 16px;
outline: none;
user-select: none;
border: none;
font-size: 24px;
font-weight: 400;
font-weight: 500;
height: ${({ hideInput }) => (hideInput ? '2.8rem' : '2.4rem')};
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
padding: ${({ selected, redesignFlag }) =>
redesignFlag ? (selected ? '4px 8px 4px 4px' : '6px 6px 6px 8px') : '0 8px'};
gap: ${({ redesignFlag }) => (redesignFlag ? '8px' : '0px')};
padding: 0 8px;
justify-content: space-between;
margin-left: ${({ hideInput }) => (hideInput ? '0' : '12px')};
:focus,
:hover {
background-color: ${({ selected, theme, redesignFlag }) =>
selected
? redesignFlag
? theme.backgroundSurface
: theme.deprecated_bg3
: darken(0.05, theme.deprecated_primary1)};
background-color: ${({ selected, theme }) =>
selected ? theme.deprecated_bg3 : darken(0.05, theme.deprecated_primary1)};
}
visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')};
`
const InputCurrencySelect = styled(CurrencySelect)<{ redesignFlag: boolean }>`
background-color: ${({ theme, selected, redesignFlag }) =>
redesignFlag && (selected ? theme.backgroundModule : theme.accentAction)};
:focus,
:hover {
background-color: ${({ selected, theme, redesignFlag }) =>
selected
? redesignFlag
? theme.backgroundInteractive
: theme.deprecated_bg3
: darken(0.05, theme.deprecated_primary1)};
}
`
const InputRow = styled.div<{ selected: boolean; redesignFlag: boolean }>`
const InputRow = styled.div<{ selected: boolean }>`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
justify-content: space-between;
padding: ${({ selected, redesignFlag }) =>
redesignFlag ? '0px' : selected ? ' 1rem 1rem 0.75rem 1rem' : '1rem 1rem 1rem 1rem'};
padding: ${({ selected }) => (selected ? ' 1rem 1rem 0.75rem 1rem' : '1rem 1rem 1rem 1rem')};
`
const LabelRow = styled.div`
@@ -138,7 +109,6 @@ const LabelRow = styled.div`
font-size: 0.75rem;
line-height: 1rem;
padding: 0 1rem 1rem;
span:hover {
cursor: pointer;
color: ${({ theme }) => darken(0.2, theme.deprecated_text2)};
@@ -147,20 +117,8 @@ const LabelRow = styled.div`
const FiatRow = styled(LabelRow)<{ redesignFlag: boolean }>`
justify-content: flex-end;
padding: ${({ redesignFlag }) => redesignFlag && '8px 0px'};
height: ${({ redesignFlag }) => !redesignFlag && '24px'};
`
const NoBalanceState = styled.div`
color: ${({ theme }) => theme.textTertiary};
font-weight: 400;
justify-content: space-between;
padding: 0px 4px;
`
const NoBalanceDash = styled.span`
color: ${({ theme }) => theme.textTertiary};
font-variant: small-caps;
font-feature-settings: 'pnum' on, 'lnum' on;
padding: ${({ redesignFlag }) => redesignFlag && '0px 1rem 0.75rem'};
height: ${({ redesignFlag }) => (redesignFlag ? '32px' : '16px')};
`
const Aligner = styled.span`
@@ -170,34 +128,31 @@ const Aligner = styled.span`
width: 100%;
`
const StyledDropDown = styled(DropDown)<{ selected: boolean; redesignFlag: boolean }>`
const StyledDropDown = styled(DropDown)<{ selected: boolean }>`
margin: 0 0.25rem 0 0.35rem;
height: 35%;
margin-left: ${({ redesignFlag }) => redesignFlag && '8px'};
path {
stroke: ${({ selected, theme }) => (selected ? theme.deprecated_text1 : theme.deprecated_white)};
stroke-width: ${({ redesignFlag }) => (redesignFlag ? '2px' : '1.5px')};
stroke-width: 1.5px;
}
`
const StyledTokenName = styled.span<{ active?: boolean; redesignFlag: boolean }>`
const StyledTokenName = styled.span<{ active?: boolean }>`
${({ active }) => (active ? ' margin: 0 0.25rem 0 0.25rem;' : ' margin: 0 0.25rem 0 0.25rem;')}
font-size: ${({ active }) => (active ? '18px' : '18px')};
font-weight: ${({ redesignFlag }) => (redesignFlag ? '600' : '500')};
`
const StyledBalanceMax = styled.button<{ disabled?: boolean; redesignFlag: boolean }>`
const StyledBalanceMax = styled.button<{ disabled?: boolean }>`
background-color: transparent;
background-color: ${({ theme, redesignFlag }) => !redesignFlag && theme.deprecated_primary5};
background-color: ${({ theme }) => theme.deprecated_primary5};
border: none;
text-transform: ${({ redesignFlag }) => !redesignFlag && 'uppercase'};
border-radius: ${({ redesignFlag }) => !redesignFlag && '12px'};
color: ${({ theme, redesignFlag }) => (redesignFlag ? theme.accentAction : theme.deprecated_primary1)};
border-radius: 12px;
color: ${({ theme }) => theme.deprecated_primary1};
cursor: pointer;
font-size: ${({ redesignFlag }) => (redesignFlag ? '14px' : '11px')};
font-weight: ${({ redesignFlag }) => (redesignFlag ? '600' : '500')};
margin-left: ${({ redesignFlag }) => (redesignFlag ? '0px' : '0.25rem')};
font-size: 11px;
font-weight: 500;
margin-left: 0.25rem;
opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)};
padding: 4px 6px;
pointer-events: ${({ disabled }) => (!disabled ? 'initial' : 'none')};
@@ -211,11 +166,9 @@ const StyledBalanceMax = styled.button<{ disabled?: boolean; redesignFlag: boole
}
`
const StyledNumericalInput = styled(NumericalInput)<{ $loading: boolean; redesignFlag: boolean }>`
const StyledNumericalInput = styled(NumericalInput)<{ $loading: boolean }>`
${loadingOpacityMixin};
text-align: left;
font-variant: ${({ redesignFlag }) => redesignFlag && 'small-caps'};
font-feature-settings: ${({ redesignFlag }) => redesignFlag && 'pnum on, lnum on'};
`
interface CurrencyInputPanelProps {
@@ -265,10 +218,10 @@ export default function CurrencyInputPanel({
}: CurrencyInputPanelProps) {
const [modalOpen, setModalOpen] = useState(false)
const { account, chainId } = useWeb3React()
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined)
const theme = useTheme()
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
const handleDismissSearch = useCallback(() => {
setModalOpen(false)
@@ -277,9 +230,9 @@ export default function CurrencyInputPanel({
const chainAllowed = isSupportedChain(chainId)
return (
<InputPanel id={id} hideInput={hideInput} {...rest} redesignFlag={redesignFlagEnabled}>
<InputPanel id={id} hideInput={hideInput} {...rest}>
{locked && (
<FixedContainer redesignFlag={redesignFlagEnabled}>
<FixedContainer>
<AutoColumn gap="sm" justify="center">
<Lock />
<ThemedText.DeprecatedLabel fontSize="12px" textAlign="center" padding="0 12px">
@@ -288,12 +241,8 @@ export default function CurrencyInputPanel({
</AutoColumn>
</FixedContainer>
)}
<Container hideInput={hideInput} disabled={!chainAllowed} redesignFlag={redesignFlagEnabled}>
<InputRow
style={hideInput ? { padding: '0', borderRadius: '8px' } : {}}
selected={!onCurrencySelect}
redesignFlag={redesignFlagEnabled}
>
<Container hideInput={hideInput} disabled={!chainAllowed}>
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}} selected={!onCurrencySelect}>
{!hideInput && (
<StyledNumericalInput
className="token-amount-input"
@@ -301,16 +250,14 @@ export default function CurrencyInputPanel({
onUserInput={onUserInput}
disabled={!chainAllowed}
$loading={loading}
redesignFlag={redesignFlagEnabled}
/>
)}
<InputCurrencySelect
<CurrencySelect
disabled={!chainAllowed}
visible={currency !== undefined}
selected={!!currency}
hideInput={hideInput}
redesignFlag={redesignFlagEnabled}
className="open-currency-select-button"
onClick={() => {
if (onCurrencySelect) {
@@ -325,40 +272,26 @@ export default function CurrencyInputPanel({
<DoubleCurrencyLogo currency0={pair.token0} currency1={pair.token1} size={24} margin={true} />
</span>
) : currency ? (
<CurrencyLogo style={{ marginRight: '2px' }} currency={currency} size={'24px'} />
<CurrencyLogo style={{ marginRight: '0.5rem' }} currency={currency} size={'24px'} />
) : null}
{pair ? (
<StyledTokenName className="pair-name-container" redesignFlag={redesignFlagEnabled}>
<StyledTokenName className="pair-name-container">
{pair?.token0.symbol}:{pair?.token1.symbol}
</StyledTokenName>
) : (
<StyledTokenName
className="token-symbol-container"
active={Boolean(currency && currency.symbol)}
redesignFlag={redesignFlagEnabled}
>
<StyledTokenName className="token-symbol-container" active={Boolean(currency && currency.symbol)}>
{(currency && currency.symbol && currency.symbol.length > 20
? currency.symbol.slice(0, 4) +
'...' +
currency.symbol.slice(currency.symbol.length - 5, currency.symbol.length)
: currency?.symbol) || <Trans>Select token</Trans>}
: currency?.symbol) || <Trans>Select a token</Trans>}
</StyledTokenName>
)}
</RowFixed>
{onCurrencySelect && <StyledDropDown selected={!!currency} redesignFlag={redesignFlagEnabled} />}
{onCurrencySelect && <StyledDropDown selected={!!currency} />}
</Aligner>
</InputCurrencySelect>
</CurrencySelect>
</InputRow>
{redesignFlagEnabled && !currency && (
<NoBalanceState>
<FiatRow redesignFlag={redesignFlagEnabled}>
<RowBetween>
<NoBalanceDash>-</NoBalanceDash>
<NoBalanceDash>-</NoBalanceDash>
</RowBetween>
</FiatRow>
</NoBalanceState>
)}
{!hideInput && !hideBalance && currency && (
<FiatRow redesignFlag={redesignFlagEnabled}>
<RowBetween>
@@ -388,8 +321,8 @@ export default function CurrencyInputPanel({
name={EventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
element={ElementName.MAX_TOKEN_AMOUNT_BUTTON}
>
<StyledBalanceMax onClick={onMax} redesignFlag={redesignFlagEnabled}>
<Trans>Max</Trans>
<StyledBalanceMax onClick={onMax}>
<Trans>MAX</Trans>
</StyledBalanceMax>
</TraceEvent>
) : null}

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

@@ -18,7 +18,7 @@ interface DoubleCurrencyLogoProps {
}
const HigherLogo = styled(CurrencyLogo)`
z-index: 2;
z-index: 1;
`
const CoveredLogo = styled(CurrencyLogo)<{ sizeraw: number }>`
position: absolute;

View File

@@ -1,11 +1,12 @@
import { FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
import { Phase1Variant, usePhase1Flag } from 'featureFlags/flags/phase1'
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import { TokensVariant, useTokensFlag } from 'featureFlags/flags/tokens'
import { TokenSafetyVariant, useTokenSafetyFlag } from 'featureFlags/flags/tokenSafety'
import { useAtomValue } from 'jotai/utils'
import { ReactNode, useState } from 'react'
import { TokensNetworkFilterVariant, useTokensNetworkFilterFlag } from 'featureFlags/flags/tokensNetworkFilter'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
import { X } from 'react-feather'
import { useModalIsOpen, useToggleFeatureFlags } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
@@ -44,7 +45,14 @@ const Row = styled.div`
const CloseButton = styled.button`
cursor: pointer;
background: 'transparent';
background: transparent;
border: none;
color: ${({ theme }) => theme.textPrimary};
`
const ToggleButton = styled.button`
cursor: pointer;
background: transparent;
border: none;
color: ${({ theme }) => theme.textPrimary};
`
@@ -58,7 +66,6 @@ const Header = styled(Row)`
const FlagName = styled.span`
font-size: 16px;
line-height: 20px;
padding-left: 8px;
color: ${({ theme }) => theme.textPrimary};
`
const FlagGroupName = styled.span`
@@ -114,17 +121,53 @@ function Variant({ option }: { option: string }) {
return <option value={option}>{option}</option>
}
function FeatureFlagOption({
variants,
featureFlag,
value,
label,
}: {
variants: string[]
interface FeatureFlagProps {
variant: Record<string, string>
featureFlag: FeatureFlag
value: string
label: string
}) {
}
function FeatureFlagGroup({ name, children }: PropsWithChildren<{ name: string }>) {
// type FeatureFlagOption = { props: FeatureFlagProps }
const togglableOptions = Children.toArray(children)
.filter<ReactElement<FeatureFlagProps>>(
(child): child is ReactElement<FeatureFlagProps> =>
child instanceof Object && 'type' in child && child.type === FeatureFlagOption
)
.map(({ props }) => props)
.filter(({ variant }) => {
const values = Object.values(variant)
return values.includes(BaseVariant.Control) && values.includes(BaseVariant.Enabled)
})
const setFeatureFlags = useUpdateAtom(featureFlagSettings)
const allEnabled = togglableOptions.every(({ value }) => value === BaseVariant.Enabled)
const onToggle = useCallback(() => {
setFeatureFlags((flags) => ({
...flags,
...togglableOptions.reduce(
(flags, { featureFlag }) => ({
...flags,
[featureFlag]: allEnabled ? BaseVariant.Control : BaseVariant.Enabled,
}),
{}
),
}))
}, [allEnabled, setFeatureFlags, togglableOptions])
return (
<>
<Row key={name}>
<FlagGroupName>{name}</FlagGroupName>
<ToggleButton onClick={onToggle}>{allEnabled ? 'Disable' : 'Enable'} group</ToggleButton>
</Row>
{children}
</>
)
}
function FeatureFlagOption({ variant, featureFlag, value, label }: FeatureFlagProps) {
const updateFlag = useUpdateFlag()
const [count, setCount] = useState(0)
const featureFlags = useAtomValue(featureFlagSettings)
@@ -143,7 +186,7 @@ function FeatureFlagOption({
}}
value={featureFlags[featureFlag]}
>
{variants.map((variant) => (
{Object.values(variant).map((variant) => (
<Variant key={variant} option={variant} />
))}
</FlagVariantSelection>
@@ -163,39 +206,42 @@ export default function FeatureFlagModal() {
<X size={24} />
</CloseButton>
</Header>
<FlagGroupName>Phase 1</FlagGroupName>
<FeatureFlagOption
variants={Object.values(Phase1Variant)}
value={usePhase1Flag()}
featureFlag={FeatureFlag.phase1}
label="All Phase 1 changes (nft features)."
/>
<FlagGroupName>Phase 0</FlagGroupName>
<FeatureFlagOption
variants={Object.values(RedesignVariant)}
value={useRedesignFlag()}
featureFlag={FeatureFlag.redesign}
label="Redesign"
/>
<FeatureFlagOption
variants={Object.values(NavBarVariant)}
value={useNavBarFlag()}
featureFlag={FeatureFlag.navBar}
label="NavBar"
/>
<FeatureFlagOption
variants={Object.values(TokensVariant)}
value={useTokensFlag()}
featureFlag={FeatureFlag.tokens}
label="Tokens"
/>
<FeatureFlagOption
variants={Object.values(TokenSafetyVariant)}
value={useTokenSafetyFlag()}
featureFlag={FeatureFlag.tokenSafety}
label="Token Safety"
/>
<SaveButton onClick={() => window.location.reload()}>Save Settings</SaveButton>
<FeatureFlagGroup name="Phase 0">
<FeatureFlagOption
variant={RedesignVariant}
value={useRedesignFlag()}
featureFlag={FeatureFlag.redesign}
label="Redesign"
/>
<FeatureFlagOption
variant={NavBarVariant}
value={useNavBarFlag()}
featureFlag={FeatureFlag.navBar}
label="NavBar"
/>
<FeatureFlagOption
variant={TokensVariant}
value={useTokensFlag()}
featureFlag={FeatureFlag.tokens}
label="Tokens"
/>
<FeatureFlagOption
variant={TokensNetworkFilterVariant}
value={useTokensNetworkFilterFlag()}
featureFlag={FeatureFlag.tokensNetworkFilter}
label="Tokens Network Filter"
/>
<FeatureFlagOption
variant={TokenSafetyVariant}
value={useTokenSafetyFlag()}
featureFlag={FeatureFlag.tokenSafety}
label="Token Safety"
/>
</FeatureFlagGroup>
<FeatureFlagGroup name="Phase 1">
<FeatureFlagOption variant={NftVariant} value={useNftFlag()} featureFlag={FeatureFlag.nft} label="NFTs" />
</FeatureFlagGroup>
<SaveButton onClick={() => window.location.reload()}>Reload</SaveButton>
</Modal>
)
}

View File

@@ -15,7 +15,7 @@ const ResponsiveText = styled(ThemedText.DeprecatedLabel)`
line-height: 16px;
font-size: 14px;
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
font-size: 12px;
line-height: 12px;
`};

View File

@@ -39,7 +39,7 @@ const Wrapper = styled.div`
padding: 16px 20px;
position: absolute;
right: 16px;
@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) {
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToMedium}px) {
display: block;
}
`

View File

@@ -11,7 +11,6 @@ import { useCloseModal, useModalIsOpen, useOpenModal, useToggleModal } from 'sta
import { ApplicationModal } from 'state/application/reducer'
import styled from 'styled-components/macro'
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
import { isChainAllowed } from 'utils/switchChain'
import { isMobile } from 'utils/userAgent'
const ActiveRowLinkList = styled.div`
@@ -53,7 +52,7 @@ const FlyoutMenu = styled.div`
width: 272px;
z-index: 99;
padding-top: 10px;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
top: 40px;
}
`
@@ -112,7 +111,7 @@ const NetworkLabel = styled.div`
`
const SelectorLabel = styled(NetworkLabel)`
display: none;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
display: block;
margin-right: 8px;
}
@@ -126,7 +125,7 @@ const NetworkAlertLabel = styled(NetworkLabel)`
font-size: 1rem;
width: fit-content;
font-weight: 500;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
display: block;
}
`
@@ -153,12 +152,12 @@ const SelectorControls = styled.div<{ supportedChain: boolean }>`
}
`
const SelectorLogo = styled(Logo)`
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
margin-right: 8px;
}
`
const SelectorWrapper = styled.div`
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
position: relative;
}
`
@@ -281,7 +280,7 @@ const NETWORK_SELECTOR_CHAINS = [
]
export default function NetworkSelector() {
const { chainId, provider, connector } = useWeb3React()
const { chainId, provider } = useWeb3React()
const node = useRef<HTMLDivElement>(null)
const isOpen = useModalIsOpen(ApplicationModal.NETWORK_SELECTOR)
@@ -328,18 +327,16 @@ export default function NetworkSelector() {
<FlyoutHeader>
<Trans>Select a {!onSupportedChain ? ' supported ' : ''}network</Trans>
</FlyoutHeader>
{NETWORK_SELECTOR_CHAINS.map((chainId: SupportedChainId) =>
isChainAllowed(connector, chainId) ? (
<Row
onSelectChain={async (targetChainId: SupportedChainId) => {
await selectChain(targetChainId)
closeModal()
}}
targetChain={chainId}
key={chainId}
/>
) : null
)}
{NETWORK_SELECTOR_CHAINS.map((chainId: SupportedChainId) => (
<Row
onSelectChain={async (targetChainId: SupportedChainId) => {
await selectChain(targetChainId)
closeModal()
}}
targetChain={chainId}
key={chainId}
/>
))}
</FlyoutMenuContents>
</FlyoutMenu>
)}

View File

@@ -26,7 +26,7 @@ const StyledPolling = styled.div<{ warning: boolean }>`
color: ${({ theme, warning }) => (warning ? theme.deprecated_yellow3 : theme.deprecated_green1)};
transition: 250ms ease color;
${({ theme }) => theme.mediaWidth.upToMedium`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
display: none;
`}
`

View File

@@ -46,16 +46,16 @@ const HeaderFrame = styled.div<{ showBackground: boolean }>`
transition: background-position 0.1s, box-shadow 0.1s;
background-blend-mode: hard-light;
${({ theme }) => theme.mediaWidth.upToLarge`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToLarge`
grid-template-columns: 48px 1fr 1fr;
`};
${({ theme }) => theme.mediaWidth.upToMedium`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
padding: 1rem;
grid-template-columns: 1fr 1fr;
`};
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
padding: 1rem;
grid-template-columns: 36px 1fr;
`};
@@ -81,7 +81,7 @@ const HeaderElement = styled.div`
margin-left: 8px;
}
${({ theme }) => theme.mediaWidth.upToMedium`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
align-items: center;
`};
`
@@ -97,13 +97,13 @@ const HeaderLinks = styled(Row)`
grid-gap: 10px;
overflow: auto;
align-items: center;
${({ theme }) => theme.mediaWidth.upToLarge`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToLarge`
justify-self: start;
`};
${({ theme }) => theme.mediaWidth.upToMedium`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
justify-self: center;
`};
${({ theme }) => theme.mediaWidth.upToMedium`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
flex-direction: row;
justify-content: space-between;
justify-self: center;
@@ -157,7 +157,7 @@ const UNIWrapper = styled.span`
`
const BalanceText = styled(Text)`
${({ theme }) => theme.mediaWidth.upToExtraSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToExtraSmall`
display: none;
`};
`
@@ -168,7 +168,7 @@ const Title = styled.a`
pointer-events: auto;
justify-self: flex-start;
margin-right: 12px;
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
justify-self: center;
`};
:hover {

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}ms color ${timing.in}`};
}
`
export const StyledChevronUp = styled(ChevronUp)<{ customColor?: string }>`
color: ${({ theme, customColor }) => customColor ?? theme.textSecondary};
height: 20px;
width: 20px;
&:hover {
color: ${({ theme }) => theme.accentActionSoft};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms color ${timing.in}`};
}
`

View File

@@ -1,12 +1,18 @@
import { useWeb3React } from '@web3-react/core'
import { ConnectionType } from 'connection'
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
import useENSAvatar from 'hooks/useENSAvatar'
import styled from 'styled-components/macro'
import { colors } from 'theme/colors'
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
import sockImg from '../../assets/svg/socks.svg'
import { useHasSocks } from '../../hooks/useSocksBalance'
import Identicon from '../Identicon'
const IconWrapper = styled.div<{ size?: number }>`
position: relative;
${({ theme }) => theme.flexColumnNoWrap};
align-items: center;
justify-content: center;
@@ -16,27 +22,62 @@ const IconWrapper = styled.div<{ size?: number }>`
height: ${({ size }) => (size ? size + 'px' : '32px')};
width: ${({ size }) => (size ? size + 'px' : '32px')};
}
${({ theme }) => theme.mediaWidth.upToMedium`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
align-items: flex-end;
`};
`
export default function StatusIcon({ connectionType }: { connectionType: ConnectionType }) {
let image
switch (connectionType) {
case ConnectionType.INJECTED:
image = <Identicon />
break
case ConnectionType.WALLET_CONNECT:
image = <img src={WalletConnectIcon} alt="WalletConnect" />
break
case ConnectionType.COINBASE_WALLET:
image = <img src={CoinbaseWalletIcon} alt="Coinbase Wallet" />
break
case ConnectionType.FORTMATIC:
image = <img src={FortmaticIcon} alt="Fortmatic" />
break
const SockContainer = styled.div`
position: absolute;
background-color: ${colors.pink400};
display: flex;
justify-content: center;
border-radius: 50%;
width: 16px;
height: 16px;
bottom: -5px;
right: -5px;
`
const SockImg = styled.img`
width: 7.5px;
height: 10px;
margin-top: 3px;
`
const Socks = () => {
return (
<SockContainer>
<SockImg src={sockImg} />
</SockContainer>
)
}
const useIcon = (connectionType: ConnectionType) => {
const { account } = useWeb3React()
const { avatar } = useENSAvatar(account ?? undefined)
const isNavbarEnabled = useNavBarFlag() === NavBarVariant.Enabled
if ((isNavbarEnabled && avatar) || connectionType === ConnectionType.INJECTED) {
return <Identicon />
} else if (connectionType === ConnectionType.WALLET_CONNECT) {
return <img src={WalletConnectIcon} alt="WalletConnect" />
} else if (connectionType === ConnectionType.COINBASE_WALLET) {
return <img src={CoinbaseWalletIcon} alt="Coinbase Wallet" />
}
return <IconWrapper size={16}>{image}</IconWrapper>
return undefined
}
export default function StatusIcon({ connectionType, size }: { connectionType: ConnectionType; size?: number }) {
const hasSocks = useHasSocks()
const isNavbarEnabled = useNavBarFlag() === NavBarVariant.Enabled
const icon = useIcon(connectionType)
return (
<IconWrapper size={size ?? 16}>
{isNavbarEnabled && hasSocks && <Socks />}
{icon}
</IconWrapper>
)
}

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

@@ -48,11 +48,11 @@ const StyledInput = styled(NumericalInput)<{ usePercent?: boolean }>`
font-weight: 500;
padding: 0 10px;
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
font-size: 16px;
`};
${({ theme }) => theme.mediaWidth.upToExtraSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToExtraSmall`
font-size: 12px;
`};
`

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

@@ -110,7 +110,7 @@ const MenuFlyout = styled.span<{ flyoutAlignment?: FlyoutAlignment }>`
: css`
left: 0rem;
`};
${({ theme }) => theme.mediaWidth.upToMedium`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
bottom: unset;
right: 0;
left: unset;

View File

@@ -26,7 +26,7 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ redesignFlag?: boole
const AnimatedDialogContent = animated(DialogContent)
// destructure to not pass custom props to Dialog DOM element
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => (
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, redesignFlag, ...rest }) => (
<AnimatedDialogContent {...rest} />
)).attrs({
'aria-label': 'dialog',
@@ -37,7 +37,8 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
margin: 0 0 2rem 0;
background-color: ${({ theme }) => theme.deprecated_bg0};
border: 1px solid ${({ theme }) => theme.deprecated_bg1};
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.shadow1)};
box-shadow: ${({ theme, redesignFlag }) =>
redesignFlag ? theme.deepShadow : `0 4px 8px 0 ${transparentize(0.95, theme.shadow1)}`};
padding: 0px;
width: 50vw;
overflow-y: auto;
@@ -58,11 +59,11 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
`}
display: flex;
border-radius: 20px;
${({ theme }) => theme.mediaWidth.upToMedium`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
width: 65vw;
margin: 0;
`}
${({ theme, mobile }) => theme.mediaWidth.upToSmall`
${({ theme, mobile }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
width: 85vw;
${
mobile &&
@@ -139,6 +140,7 @@ export default function Modal({
minHeight={minHeight}
maxHeight={maxHeight}
mobile={isMobile}
redesignFlag={redesignFlag}
>
{/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
{!initialFocusRef && isMobile ? <div tabIndex={1} /> : null}

View File

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

View File

@@ -1,16 +1,18 @@
import { useWeb3React } from '@web3-react/core'
import { StyledChevronDown, StyledChevronUp } from 'components/Icons'
import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import useSelectChain from 'hooks/useSelectChain'
import useSyncChainQuery from 'hooks/useSyncChainQuery'
import { Box } from 'nft/components/Box'
import { Portal } from 'nft/components/common/Portal'
import { Column, Row } from 'nft/components/Flex'
import { NewChevronDownIcon, NewChevronUpIcon } from 'nft/components/icons'
import { CheckMarkIcon } from 'nft/components/icons'
import { CheckMarkIcon, TokenWarningRedIcon } from 'nft/components/icons'
import { subhead } from 'nft/css/common.css'
import { themeVars, vars } from 'nft/css/sprinkles.css'
import { useIsMobile } from 'nft/hooks'
import { ReactNode, useReducer, useRef } from 'react'
import { isChainAllowed } from 'utils/switchChain'
import * as styles from './ChainSwitcher.css'
import { NavDropdown } from './NavDropdown'
@@ -27,18 +29,20 @@ const ChainRow = ({
const { label, logoUrl } = getChainInfo(targetChain)
return (
<Row
as="button"
background={active ? 'lightGrayContainer' : 'none'}
className={`${styles.ChainSwitcherRow} ${subhead}`}
onClick={() => onSelectChain(targetChain)}
>
<ChainDetails>
<img src={logoUrl} alt={label} className={styles.Icon} />
{label}
</ChainDetails>
{active && <CheckMarkIcon width={20} height={20} />}
</Row>
<Column borderRadius="12">
<Row
as="button"
background="none"
className={`${styles.ChainSwitcherRow} ${subhead}`}
onClick={() => onSelectChain(targetChain)}
>
<ChainDetails>
<img src={logoUrl} alt={label} className={styles.Icon} />
{label}
</ChainDetails>
{active && <CheckMarkIcon width={20} height={20} color={vars.color.blue400} />}
</Row>
</Column>
)
}
@@ -53,56 +57,73 @@ const NETWORK_SELECTOR_CHAINS = [
]
interface ChainSwitcherProps {
isMobile?: boolean
leftAlign?: boolean
}
export const ChainSwitcher = ({ isMobile }: ChainSwitcherProps) => {
const { chainId, connector } = useWeb3React()
export const ChainSwitcher = ({ leftAlign }: ChainSwitcherProps) => {
const { chainId } = useWeb3React()
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
const isMobile = useIsMobile()
const ref = useRef<HTMLDivElement>(null)
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)
const modalRef = useRef<HTMLDivElement>(null)
useOnClickOutside(ref, isOpen ? toggleOpen : undefined, [modalRef])
const info = chainId ? getChainInfo(chainId) : undefined
const selectChain = useSelectChain()
useSyncChainQuery()
if (!chainId || !info) {
if (!chainId) {
return null
}
const isSupported = !!info
const dropdown = (
<NavDropdown top="56" left={leftAlign ? '0' : 'auto'} right={leftAlign ? 'auto' : '0'} ref={modalRef}>
<Column marginX="8">
{NETWORK_SELECTOR_CHAINS.map((chainId: SupportedChainId) => (
<ChainRow
onSelectChain={async (targetChainId: SupportedChainId) => {
await selectChain(targetChainId)
toggleOpen()
}}
targetChain={chainId}
key={chainId}
/>
))}
</Column>
</NavDropdown>
)
return (
<Box position="relative" ref={ref}>
<Row as="button" gap="8" className={styles.ChainSwitcher} onClick={toggleOpen}>
<img src={info.logoUrl} alt={info.label} className={styles.Image} />
<Box as="span" className={subhead} color="explicitWhite" style={{ lineHeight: '20px' }}>
{info.label}
</Box>
{isOpen ? (
<NewChevronUpIcon width={16} height={16} color="darkGray" />
<Row
as="button"
gap="8"
className={styles.ChainSwitcher}
background={isOpen ? 'accentActiveSoft' : 'none'}
onClick={toggleOpen}
>
{!isSupported ? (
<>
<TokenWarningRedIcon fill={themeVars.colors.darkGray} width={24} height={24} />
<Box as="span" className={subhead} display={{ sm: 'none', xxl: 'flex' }} style={{ lineHeight: '20px' }}>
Unsupported
</Box>
</>
) : (
<NewChevronDownIcon width={16} height={16} color="darkGray" />
<>
<img src={info.logoUrl} alt={info.label} className={styles.Image} />
<Box as="span" className={subhead} display={{ sm: 'none', xxl: 'flex' }} style={{ lineHeight: '20px' }}>
{info.label}
</Box>
</>
)}
{isOpen ? <StyledChevronUp /> : <StyledChevronDown />}
</Row>
{isOpen && (
<NavDropdown top={60} leftAligned={isMobile}>
<Column gap="4">
{NETWORK_SELECTOR_CHAINS.map((chainId: SupportedChainId) =>
isChainAllowed(connector, chainId) ? (
<ChainRow
onSelectChain={async (targetChainId: SupportedChainId) => {
await selectChain(targetChainId)
toggleOpen()
}}
targetChain={chainId}
key={chainId}
/>
) : null
)}
</Column>
</NavDropdown>
)}
{isOpen && (isMobile ? <Portal>{dropdown}</Portal> : <>{dropdown}</>)}
</Box>
)
}

View File

@@ -1,13 +1,27 @@
import { style } from '@vanilla-extract/css'
import { sprinkles, themeVars } from '../../nft/css/sprinkles.css'
import { sprinkles, themeVars, vars } from '../../nft/css/sprinkles.css'
export const hover = style([
sprinkles({
transition: '250',
borderRadius: '12',
}),
{
':hover': {
background: vars.color.lightGrayOverlay,
},
},
])
export const MenuRow = style([
hover,
sprinkles({
color: 'blackBlue',
paddingY: '12',
width: 'max',
marginRight: '52',
paddingY: '8',
paddingX: '8',
width: 'full',
whiteSpace: 'nowrap',
}),
{
lineHeight: '24px',
@@ -22,8 +36,10 @@ export const PrimaryText = style([
])
export const SecondaryText = style([
hover,
sprinkles({
paddingY: '8',
paddingX: '8',
color: 'darkGray',
}),
{
@@ -34,6 +50,7 @@ export const SecondaryText = style([
export const Separator = style([
sprinkles({
height: '0',
marginX: '16',
}),
{
borderTop: 'solid',
@@ -45,6 +62,6 @@ export const Separator = style([
export const IconRow = style([
sprinkles({
paddingX: '16',
paddingY: '8',
justifyContent: { sm: 'center', md: 'flex-start' },
}),
])

View File

@@ -1,5 +1,7 @@
import { Trans } from '@lingui/macro'
import FeatureFlagModal from 'components/FeatureFlagModal/FeatureFlagModal'
import { PrivacyPolicyModal } from 'components/PrivacyPolicy'
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
@@ -9,6 +11,7 @@ import {
EllipsisIcon,
GithubIconMenu,
GovernanceIcon,
ThinTagIcon,
TwitterIconMenu,
} from 'nft/components/icons'
import { body, bodySmall } from 'nft/css/common.css'
@@ -114,6 +117,7 @@ export const MenuDropdown = () => {
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
const togglePrivacyPolicy = useToggleModal(ApplicationModal.PRIVACY_POLICY)
const openFeatureFlagsModal = useToggleModal(ApplicationModal.FEATURE_FLAGS)
const nftFlag = useNftFlag()
const ref = useRef<HTMLDivElement>(null)
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)
@@ -121,50 +125,78 @@ export const MenuDropdown = () => {
return (
<>
<Box position="relative" ref={ref}>
<NavIcon onClick={toggleOpen}>
<EllipsisIcon width={28} height={28} />
<NavIcon isActive={isOpen} onClick={toggleOpen}>
<EllipsisIcon />
</NavIcon>
{isOpen && (
<NavDropdown top={60}>
<Column gap="12">
<Column paddingX="16" gap="4">
<NavDropdown top={{ sm: 'unset', lg: '56' }} bottom={{ sm: '56', lg: 'unset' }} right="0">
<Column gap="16">
<Column paddingX="8" gap="4">
{nftFlag === NftVariant.Enabled && (
<PrimaryMenuRow to="/nft/sell" close={toggleOpen}>
<Icon>
<ThinTagIcon width={24} height={24} />
</Icon>
<PrimaryMenuRow.Text>
<Trans>Sell NFTs</Trans>
</PrimaryMenuRow.Text>
</PrimaryMenuRow>
)}
<PrimaryMenuRow to="/vote" close={toggleOpen}>
<Icon>
<GovernanceIcon width={24} height={24} />
</Icon>
<PrimaryMenuRow.Text>Vote in governance</PrimaryMenuRow.Text>
<PrimaryMenuRow.Text>
<Trans>Vote in governance</Trans>
</PrimaryMenuRow.Text>
</PrimaryMenuRow>
<PrimaryMenuRow href="https://info.uniswap.org/#/">
<Icon>
<BarChartIcon width={24} height={24} />
</Icon>
<PrimaryMenuRow.Text>View token analytics </PrimaryMenuRow.Text>
<PrimaryMenuRow.Text>
<Trans>View token analytics</Trans>
</PrimaryMenuRow.Text>
</PrimaryMenuRow>
</Column>
<Separator />
<Column paddingX="16" gap="4">
<SecondaryLinkedText href="https://help.uniswap.org/en/">Help center </SecondaryLinkedText>
<SecondaryLinkedText href="https://docs.uniswap.org/">Documentation </SecondaryLinkedText>
<Box
display="flex"
flexDirection={{ sm: 'row', md: 'column' }}
flexWrap="wrap"
alignItems={{ sm: 'center', md: 'flex-start' }}
paddingX="8"
>
<SecondaryLinkedText href="https://help.uniswap.org/en/">
<Trans>Help center</Trans>
</SecondaryLinkedText>
<SecondaryLinkedText href="https://docs.uniswap.org/">
<Trans>Documentation</Trans>
</SecondaryLinkedText>
<SecondaryLinkedText
onClick={() => {
toggleOpen()
togglePrivacyPolicy()
}}
>{`Legal & Privacy`}</SecondaryLinkedText>
>
<Trans>Legal & Privacy</Trans>
</SecondaryLinkedText>
{(isDevelopmentEnv() || isStagingEnv()) && (
<SecondaryLinkedText onClick={openFeatureFlagsModal}>{`Feature Flags`}</SecondaryLinkedText>
<SecondaryLinkedText onClick={openFeatureFlagsModal}>
<Trans>Feature Flags</Trans>
</SecondaryLinkedText>
)}
</Column>
</Box>
<IconRow>
<Icon href="https://discord.com/invite/FCfyBSbCU5">
<DiscordIconMenu width={24} height={24} color={themeVars.colors.darkGray} />
<DiscordIconMenu className={styles.hover} width={24} height={24} color={themeVars.colors.darkGray} />
</Icon>
<Icon href="https://twitter.com/Uniswap">
<TwitterIconMenu width={24} height={24} color={themeVars.colors.darkGray} />
<TwitterIconMenu className={styles.hover} width={24} height={24} color={themeVars.colors.darkGray} />
</Icon>
<Icon href="https://github.com/Uniswap">
<GithubIconMenu width={24} height={24} color={themeVars.colors.darkGray} />
<GithubIconMenu className={styles.hover} width={24} height={24} color={themeVars.colors.darkGray} />
</Icon>
</IconRow>
</Column>

View File

@@ -1,121 +0,0 @@
import { style } from '@vanilla-extract/css'
import { subhead } from 'nft/css/common.css'
import { sprinkles } from '../../nft/css/sprinkles.css'
export const sidebar = style([
sprinkles({
display: 'flex',
position: 'fixed',
background: 'white',
height: 'full',
top: '0',
left: '0',
right: '0',
bottom: '0',
paddingBottom: '16',
justifyContent: 'space-between',
}),
{
zIndex: 20,
},
])
export const icon = style([
sprinkles({
width: '32',
height: '32',
}),
])
export const iconContainer = style([
sprinkles({
position: 'relative',
display: 'flex',
flexDirection: 'column',
color: 'darkGray',
background: 'none',
border: 'none',
justifyContent: 'flex-end',
textAlign: 'center',
cursor: 'pointer',
padding: '6',
}),
])
export const linkRow = style([
subhead,
sprinkles({
color: 'blackBlue',
width: 'full',
paddingLeft: '16',
paddingY: '12',
cursor: 'pointer',
}),
{
lineHeight: '24px',
textDecoration: 'none',
},
])
export const activeLinkRow = style([
linkRow,
sprinkles({
background: 'lightGrayButton',
}),
])
export const separator = style([
sprinkles({
height: '0',
borderStyle: 'solid',
borderColor: 'medGray',
borderWidth: '1px',
marginY: '8',
marginX: '16',
}),
])
export const extraLinkRow = style([
subhead,
sprinkles({
width: 'full',
color: 'blackBlue',
paddingY: '12',
paddingLeft: '16',
cursor: 'pointer',
}),
{
lineHeight: '24px',
textDecoration: 'none',
},
])
export const bottomExternalLinks = style([
sprinkles({
gap: '4',
paddingX: '4',
width: 'max',
flexWrap: 'wrap',
}),
])
export const bottomJointExternalLinksContainer = style([
sprinkles({
paddingX: '8',
paddingY: '4',
color: 'darkGray',
fontWeight: 'medium',
fontSize: '12',
}),
{
lineHeight: '20px',
},
])
export const IconRow = style([
sprinkles({
gap: '12',
width: 'max',
}),
])

View File

@@ -1,233 +0,0 @@
import FeatureFlagModal from 'components/FeatureFlagModal/FeatureFlagModal'
import { PrivacyPolicyModal } from 'components/PrivacyPolicy'
import { Box } from 'nft/components/Box'
import { Portal } from 'nft/components/common/Portal'
import { Column, Row } from 'nft/components/Flex'
import {
BarChartIconMobile,
BulletIcon,
CloseIcon,
DiscordIconMenuMobile,
GithubIconMenuMobile,
GovernanceIconMobile,
HamburgerIcon,
TwitterIconMenuMobile,
} from 'nft/components/icons'
import { themeVars } from 'nft/css/sprinkles.css'
import { ReactNode, useReducer } from 'react'
import { NavLink, NavLinkProps, useLocation } from 'react-router-dom'
import { useToggleModal, useTogglePrivacyPolicy } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import { isDevelopmentEnv, isStagingEnv } from 'utils/env'
import * as styles from './MobileSidebar.css'
import { NavIcon } from './NavIcon'
interface NavLinkRowProps {
href: string
id?: NavLinkProps['id']
isActive?: boolean
close: () => void
children: ReactNode
}
const NavLinkRow = ({ href, id, isActive, close, children }: NavLinkRowProps) => {
return (
<NavLink to={href} className={isActive ? styles.activeLinkRow : styles.linkRow} id={id} onClick={close}>
{children}
</NavLink>
)
}
const ExtraLinkRow = ({
to,
href,
close,
children,
}: {
to?: NavLinkProps['to']
href?: string
close: () => void
children: ReactNode
}) => {
return (
<>
{to ? (
<NavLink to={to} className={styles.extraLinkRow}>
<Row gap="12" onClick={close}>
{children}
</Row>
</NavLink>
) : (
<Row
as="a"
href={href}
target={'_blank'}
rel={'noopener noreferrer'}
gap="12"
onClick={close}
className={styles.extraLinkRow}
>
{children}
</Row>
)}
</>
)
}
const BottomExternalLink = ({
href,
onClick,
children,
}: {
href?: string
onClick?: () => void
children: ReactNode
}) => {
return (
<Box
as={href ? 'a' : 'div'}
href={href ?? undefined}
target={href ? '_blank' : undefined}
rel={href ? 'noopener noreferrer' : undefined}
className={`${styles.bottomJointExternalLinksContainer}`}
onClick={onClick}
cursor="pointer"
>
{children}
</Box>
)
}
const Icon = ({ href, children }: { href?: string; children: ReactNode }) => {
return (
<>
<Box
as={href ? 'a' : 'div'}
href={href ?? undefined}
target={href ? '_blank' : undefined}
rel={href ? 'noopener noreferrer' : undefined}
display="flex"
flexDirection="column"
color="blackBlue"
background="none"
border="none"
justifyContent="center"
textAlign="center"
>
{children}
</Box>
</>
)
}
const IconRow = ({ children }: { children: ReactNode }) => {
return <Row className={styles.IconRow}>{children}</Row>
}
const Seperator = () => {
return <Box className={styles.separator} />
}
export const MobileSideBar = () => {
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
const togglePrivacyPolicy = useTogglePrivacyPolicy()
const openFeatureFlagsModal = useToggleModal(ApplicationModal.FEATURE_FLAGS)
const { pathname } = useLocation()
const isPoolActive =
pathname.startsWith('/pool') ||
pathname.startsWith('/add') ||
pathname.startsWith('/remove') ||
pathname.startsWith('/increase') ||
pathname.startsWith('/find')
return (
<>
<NavIcon onClick={toggleOpen}>
<HamburgerIcon width={28} height={28} />
</NavIcon>
{isOpen && (
<Portal>
<Column className={styles.sidebar}>
<Column>
<Row justifyContent="flex-end" marginTop="14" marginBottom="20" marginRight="8">
<Box as="button" onClick={toggleOpen} className={styles.iconContainer}>
<CloseIcon className={styles.icon} />
</Box>
</Row>
<Column gap="4">
<NavLinkRow href="/swap" close={toggleOpen} isActive={pathname.startsWith('/swap')}>
Swap
</NavLinkRow>
<NavLinkRow href="/tokens" close={toggleOpen} isActive={pathname.startsWith('/tokens')}>
Tokens
</NavLinkRow>
<NavLinkRow href="/pool" id={'pool-nav-link'} isActive={isPoolActive} close={toggleOpen}>
Pool
</NavLinkRow>
</Column>
<Seperator />
<Column gap="4">
<ExtraLinkRow to="/vote" close={toggleOpen}>
<Icon>
<GovernanceIconMobile width={24} height={24} />
</Icon>
Vote in governance
</ExtraLinkRow>
<ExtraLinkRow href="https://info.uniswap.org/#/" close={toggleOpen}>
<Icon>
<BarChartIconMobile width={24} height={24} />
</Icon>
View token analytics
</ExtraLinkRow>
</Column>
</Column>
<Column>
<Row justifyContent="center" marginBottom="12" flexWrap="wrap">
<Row className={styles.bottomExternalLinks}>
<BottomExternalLink href="https://help.uniswap.org/en/" onClick={toggleOpen}>
Help center
</BottomExternalLink>
<BulletIcon />
<BottomExternalLink href="https://docs.uniswap.org/" onClick={toggleOpen}>
Documentation
</BottomExternalLink>
<BulletIcon />
<BottomExternalLink
onClick={() => {
toggleOpen()
togglePrivacyPolicy()
}}
>
{`Legal & Privacy`}
</BottomExternalLink>
</Row>
{(isDevelopmentEnv() || isStagingEnv()) && (
<>
<BulletIcon />
<BottomExternalLink onClick={openFeatureFlagsModal}>{`Feature Flags`}</BottomExternalLink>
</>
)}
</Row>
<Row justifyContent="center">
<IconRow>
<Icon href="https://discord.com/invite/FCfyBSbCU5">
<DiscordIconMenuMobile width={32} height={32} color={themeVars.colors.darkGray} />
</Icon>
<Icon href="https://twitter.com/Uniswap">
<TwitterIconMenuMobile width={32} height={32} color={themeVars.colors.darkGray} />
</Icon>
<Icon href="https://github.com/Uniswap">
<GithubIconMenuMobile width={32} height={32} color={themeVars.colors.darkGray} />
</Icon>
</IconRow>
</Row>
</Column>
</Column>
</Portal>
)}
<PrivacyPolicyModal />
<FeatureFlagModal />
</>
)
}

View File

@@ -2,18 +2,44 @@ import { style } from '@vanilla-extract/css'
import { sprinkles } from '../../nft/css/sprinkles.css'
export const NavDropdown = style([
const baseNavDropdown = style([
sprinkles({
position: 'absolute',
background: 'white95',
borderRadius: '12',
background: 'lightGray',
borderStyle: 'solid',
borderColor: 'medGray',
paddingY: '20',
borderWidth: '1px',
paddingBottom: '8',
paddingTop: '8',
zIndex: '2',
}),
{
boxShadow: '0px 4px 12px 0px #00000026',
zIndex: 10,
},
])
export const NavDropdown = style([
baseNavDropdown,
sprinkles({
position: 'absolute',
borderRadius: '12',
}),
{},
])
export const mobileNavDropdown = style([
baseNavDropdown,
sprinkles({
position: 'fixed',
borderTopRightRadius: '12',
borderTopLeftRadius: '12',
top: 'unset',
bottom: '56',
left: '0',
right: '0',
width: 'full',
}),
{
borderRightWidth: '0px',
borderLeftWidth: '0px',
},
])

View File

@@ -1,37 +1,12 @@
import { Box } from 'nft/components/Box'
import { ReactNode } from 'react'
import { Box, BoxProps } from 'nft/components/Box'
import { useIsMobile } from 'nft/hooks'
import { ForwardedRef, forwardRef } from 'react'
import * as styles from './NavDropdown.css'
interface NavDropdownProps {
top: number
right?: number
leftAligned?: boolean
horizontalPadding?: boolean
centerHorizontally?: boolean
children: ReactNode
}
export const NavDropdown = forwardRef((props: BoxProps, ref: ForwardedRef<HTMLElement>) => {
const isMobile = useIsMobile()
return <Box ref={ref} className={isMobile ? styles.mobileNavDropdown : styles.NavDropdown} {...props} />
})
export const NavDropdown = ({
top,
centerHorizontally,
leftAligned,
horizontalPadding,
children,
}: NavDropdownProps) => {
return (
<Box
paddingX={horizontalPadding ? '16' : undefined}
style={{
top: `${top}px`,
left: centerHorizontally ? '50%' : leftAligned ? '0px' : 'auto',
right: centerHorizontally || leftAligned ? 'auto' : '10px',
transform: centerHorizontally ? 'translateX(-50%)' : 'unset',
zIndex: 3,
}}
className={styles.NavDropdown}
>
{children}
</Box>
)
}
NavDropdown.displayName = 'NavDropdown'

View File

@@ -1,25 +1,24 @@
import { style } from '@vanilla-extract/css'
import { sprinkles, themeVars } from '../../nft/css/sprinkles.css'
import { sprinkles, vars } from '../../nft/css/sprinkles.css'
export const navIcon = style([
sprinkles({
position: 'relative',
display: 'flex',
flexDirection: 'column',
color: 'blackBlue',
background: 'none',
border: 'none',
justifyContent: 'center',
textAlign: 'center',
cursor: 'pointer',
padding: '8',
borderRadius: '8',
transition: '250',
}),
{
':hover': {
background: themeVars.colors.lightGrayContainer,
background: vars.color.lightGrayOverlay,
},
zIndex: 2,
zIndex: 1,
},
])

View File

@@ -5,12 +5,19 @@ import * as styles from './NavIcon.css'
interface NavIconProps {
children: ReactNode
isActive?: boolean
onClick: () => void
}
export const NavIcon = ({ children, onClick }: NavIconProps) => {
export const NavIcon = ({ children, isActive, onClick }: NavIconProps) => {
return (
<Box as="button" className={styles.navIcon} onClick={onClick}>
<Box
as="button"
className={styles.navIcon}
background={isActive ? 'accentActiveSoft' : 'none'}
color={isActive ? 'blackBlue' : 'darkGray'}
onClick={onClick}
>
{children}
</Box>
)

View File

@@ -1,7 +1,7 @@
import { style } from '@vanilla-extract/css'
import { subhead } from '../../nft/css/common.css'
import { sprinkles } from '../../nft/css/sprinkles.css'
import { sprinkles, vars } from '../../nft/css/sprinkles.css'
export const nav = style([
sprinkles({
@@ -20,7 +20,7 @@ export const nav = style([
export const logoContainer = style([
sprinkles({
display: 'flex',
marginRight: { mobile: '12', desktopXl: '20' },
marginRight: { sm: '12', xxl: '20' },
alignItems: 'center',
}),
])
@@ -34,23 +34,14 @@ export const logo = style([
export const baseContainer = style([
sprinkles({
display: 'flex',
alignItems: 'center',
}),
])
export const baseMobileContainer = style([
sprinkles({
display: 'flex',
width: 'full',
alignItems: 'center',
marginY: '2',
}),
])
export const baseSideContainer = style([
baseContainer,
sprinkles({
display: 'flex',
width: 'full',
flex: '1',
flexShrink: '2',
@@ -64,19 +55,13 @@ export const leftSideContainer = style([
}),
])
export const leftSideMobileContainer = style([
baseMobileContainer,
sprinkles({
justifyContent: 'flex-start',
}),
])
export const middleContainer = style([
baseContainer,
sprinkles({
flex: '1',
flexShrink: '1',
justifyContent: 'center',
display: { sm: 'none', xl: 'flex' },
}),
])
@@ -94,10 +79,17 @@ const baseMenuItem = style([
paddingX: '16',
marginY: '4',
borderRadius: '12',
transition: '250',
height: 'min',
width: 'full',
textAlign: 'center',
}),
{
lineHeight: '24px',
textDecoration: 'none',
':hover': {
background: vars.color.lightGrayOverlay,
},
},
])
@@ -108,30 +100,25 @@ export const menuItem = style([
}),
])
export const rightSideMobileContainer = style([
baseMobileContainer,
sprinkles({
justifyContent: 'flex-end',
}),
])
export const activeMenuItem = style([
baseMenuItem,
sprinkles({
color: 'blackBlue',
background: 'backgroundFloating',
}),
])
export const mobileWalletContainer = style([
export const mobileBottomBar = style([
sprinkles({
position: 'fixed',
display: 'flex',
display: { sm: 'flex', lg: 'none' },
bottom: '0',
right: '1/2',
marginY: '0',
marginX: 'auto',
right: '0',
left: '0',
justifyContent: 'space-between',
paddingY: '4',
paddingX: '8',
height: '56',
background: 'lightGray',
}),
{
transform: 'translate(50%,-50%)',
},
])

View File

@@ -1,16 +1,16 @@
import { Trans } from '@lingui/macro'
import Web3Status from 'components/Web3Status'
import { useWindowSize } from 'hooks/useWindowSize'
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
import { ReactNode } from 'react'
import { NavLink, NavLinkProps, useLocation } from 'react-router-dom'
import { Box } from '../../nft/components/Box'
import { Row } from '../../nft/components/Flex'
import { UniIcon, UniIconMobile } from '../../nft/components/icons'
import { breakpoints } from '../../nft/css/sprinkles.css'
import { UniIcon } from '../../nft/components/icons'
import { ChainSwitcher } from './ChainSwitcher'
import { MenuDropdown } from './MenuDropdown'
import { MobileSideBar } from './MobileSidebar'
import * as styles from './Navbar.css'
import { SearchBar } from './SearchBar'
interface MenuItemProps {
href: string
@@ -32,39 +32,9 @@ const MenuItem = ({ href, id, isActive, children }: MenuItemProps) => {
)
}
const MobileNavbar = () => {
return (
<>
<nav className={styles.nav}>
<Box display="flex" height="full" flexWrap="nowrap" alignItems="stretch">
<Box className={styles.leftSideMobileContainer}>
<Box as="a" href="#/swap" className={styles.logoContainer}>
<UniIconMobile width="44" height="44" className={styles.logo} />
</Box>
<ChainSwitcher isMobile={true} />
</Box>
<Box className={styles.rightSideMobileContainer}>
<Row gap="16">
{/* TODO add Searchbar */}
<MobileSideBar />
</Row>
</Box>
</Box>
</nav>
<Box className={styles.mobileWalletContainer}>
<Web3Status />
</Box>
</>
)
}
const Navbar = () => {
const { width: windowWidth } = useWindowSize()
const PageTabs = () => {
const { pathname } = useLocation()
if (windowWidth && windowWidth < breakpoints.desktopXl) {
return <MobileNavbar />
}
const nftFlag = useNftFlag()
const isPoolActive =
pathname.startsWith('/pool') ||
@@ -74,34 +44,68 @@ const Navbar = () => {
pathname.startsWith('/find')
return (
<nav className={styles.nav}>
<Box display="flex" height="full" flexWrap="nowrap" alignItems="stretch">
<Box className={styles.leftSideContainer}>
<Box as="a" href="#/swap" className={styles.logoContainer}>
<UniIcon width="48" height="48" className={styles.logo} />
<>
<MenuItem href="/swap" isActive={pathname.startsWith('/swap')}>
<Trans>Swap</Trans>
</MenuItem>
<MenuItem href="/tokens" isActive={pathname.startsWith('/tokens')}>
<Trans>Tokens</Trans>
</MenuItem>
{nftFlag === NftVariant.Enabled && (
<MenuItem href="/nfts" isActive={pathname.startsWith('/nfts')}>
<Trans>NFTs</Trans>
</MenuItem>
)}
<MenuItem href="/pool" id={'pool-nav-link'} isActive={isPoolActive}>
<Trans>Pool</Trans>
</MenuItem>
</>
)
}
const Navbar = () => {
return (
<>
<nav className={styles.nav}>
<Box display="flex" height="full" flexWrap="nowrap" alignItems="stretch">
<Box className={styles.leftSideContainer}>
<Box as="a" href="#/swap" className={styles.logoContainer}>
<UniIcon width="48" height="48" className={styles.logo} />
</Box>
<Box display={{ sm: 'flex', lg: 'none' }}>
<ChainSwitcher leftAlign={true} />
</Box>
<Row gap="8" display={{ sm: 'none', lg: 'flex' }}>
<PageTabs />
</Row>
</Box>
<Box className={styles.middleContainer}>
<SearchBar />
</Box>
<Box className={styles.rightSideContainer}>
<Row gap="12">
<Box display={{ sm: 'flex', xl: 'none' }}>
<SearchBar />
</Box>
<Box display={{ sm: 'none', lg: 'flex' }}>
<MenuDropdown />
</Box>
<Box display={{ sm: 'none', lg: 'flex' }}>
<ChainSwitcher />
</Box>
<Web3Status />
</Row>
</Box>
<Row gap="8">
<MenuItem href="/swap" isActive={pathname.startsWith('/swap')}>
Swap
</MenuItem>
<MenuItem href="/tokens" isActive={pathname.startsWith('/explore')}>
Tokens
</MenuItem>
<MenuItem href="/pool" id={'pool-nav-link'} isActive={isPoolActive}>
Pool
</MenuItem>
</Row>
</Box>
<Box className={styles.middleContainer}>{/* TODO add Searchbar */}</Box>
<Box className={styles.rightSideContainer}>
<Row gap="12">
<MenuDropdown />
<ChainSwitcher />
<Web3Status />
</Row>
</nav>
<Box className={styles.mobileBottomBar}>
<PageTabs />
<Box marginY="4">
<MenuDropdown />
</Box>
</Box>
</nav>
</>
)
}

View File

@@ -0,0 +1,169 @@
import { style } from '@vanilla-extract/css'
import { buttonTextSmall, subhead, subheadSmall } from 'nft/css/common.css'
import { breakpoints, sprinkles, vars } from '../../nft/css/sprinkles.css'
const DESKTOP_NAVBAR_WIDTH = 360
const baseSearchStyle = style([
sprinkles({
paddingY: '12',
width: { sm: 'viewWidth' },
borderStyle: 'solid',
borderWidth: '1px',
borderColor: 'medGray',
}),
{
'@media': {
[`screen and (min-width: ${breakpoints.sm}px)`]: {
width: `${DESKTOP_NAVBAR_WIDTH}px`,
},
},
},
])
export const searchBarContainer = style([
sprinkles({
right: '0',
top: '0',
zIndex: '3',
display: 'inline-block',
}),
{
'@media': {
[`screen and (min-width: ${breakpoints.sm}px)`]: {
top: '-24px',
},
[`screen and (min-width: ${breakpoints.lg}px)`]: {
right: `-${DESKTOP_NAVBAR_WIDTH / 2}px`,
},
},
},
])
export const searchBar = style([
baseSearchStyle,
sprinkles({
height: 'full',
color: 'placeholder',
paddingX: '16',
cursor: 'pointer',
background: 'lightGray',
}),
])
export const searchBarInput = style([
sprinkles({
padding: '0',
fontWeight: 'normal',
fontSize: '16',
color: { default: 'blackBlue', placeholder: 'placeholder' },
border: 'none',
background: 'none',
}),
{ lineHeight: '24px' },
])
export const searchBarDropdown = style([
baseSearchStyle,
sprinkles({
borderBottomLeftRadius: '12',
borderBottomRightRadius: '12',
background: 'lightGray',
}),
{
borderTop: 'none',
},
])
export const suggestionRow = style([
sprinkles({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingY: '8',
paddingX: '16',
transition: '250',
}),
{
':hover': {
cursor: 'pointer',
background: vars.color.lightGrayOverlay,
},
textDecoration: 'none',
},
])
export const suggestionImage = sprinkles({
width: '36',
height: '36',
borderRadius: 'round',
marginRight: '8',
})
export const suggestionPrimaryContainer = style([
sprinkles({
alignItems: 'flex-start',
width: 'full',
}),
])
export const suggestionSecondaryContainer = sprinkles({
textAlign: 'right',
alignItems: 'flex-end',
})
export const primaryText = style([
subhead,
sprinkles({
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
color: 'blackBlue',
}),
{
lineHeight: '24px',
},
])
export const secondaryText = style([
buttonTextSmall,
sprinkles({
color: 'darkGray',
}),
{
lineHeight: '20px',
},
])
export const imageHolder = style([
suggestionImage,
sprinkles({
background: 'loading',
flexShrink: '0',
}),
])
export const suggestionIcon = sprinkles({
display: 'flex',
flexShrink: '0',
})
export const sectionHeader = style([
subheadSmall,
sprinkles({
color: 'darkGray',
}),
{
lineHeight: '20px',
},
])
export const notFoundContainer = style([
sectionHeader,
sprinkles({
paddingY: '4',
paddingLeft: '16',
}),
])

View File

@@ -0,0 +1,393 @@
// eslint-disable-next-line no-restricted-imports
import { t, Trans } from '@lingui/macro'
import clsx from 'clsx'
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
import useDebounce from 'hooks/useDebounce'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { organizeSearchResults } from 'lib/utils/searchBar'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { Overlay } from 'nft/components/modals/Overlay'
import { magicalGradientOnHover, subheadSmall } from 'nft/css/common.css'
import { useIsMobile, 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 { useQuery } from 'react-query'
import { useLocation } from 'react-router-dom'
import {
ChevronLeftIcon,
ClockIcon,
MagnifyingGlassIcon,
NavMagnifyingGlassIcon,
TrendingArrow,
} from '../../nft/components/icons'
import { NavIcon } from './NavIcon'
import * as styles from './SearchBar.css'
import { CollectionRow, SkeletonRow, TokenRow } from './SuggestionRow'
interface SearchBarDropdownSectionProps {
toggleOpen: () => void
suggestions: (GenieCollection | FungibleToken)[]
header: JSX.Element
headerIcon?: JSX.Element
hoveredIndex: number | undefined
startingIndex: number
setHoveredIndex: (index: number | undefined) => void
}
export const SearchBarDropdownSection = ({
toggleOpen,
suggestions,
header,
headerIcon = undefined,
hoveredIndex,
startingIndex,
setHoveredIndex,
}: SearchBarDropdownSectionProps) => {
return (
<Column gap="12">
<Row paddingX="16" paddingY="4" gap="8" color="grey300" className={subheadSmall} style={{ lineHeight: '20px' }}>
{headerIcon ? headerIcon : null}
<Box>{header}</Box>
</Row>
<Column gap="12">
{suggestions?.map((suggestion, index) =>
isCollection(suggestion) ? (
<CollectionRow
key={suggestion.address}
collection={suggestion as GenieCollection}
isHovered={hoveredIndex === index + startingIndex}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
index={index + startingIndex}
/>
) : (
<TokenRow
key={suggestion.address}
token={suggestion as FungibleToken}
isHovered={hoveredIndex === index + startingIndex}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
index={index + startingIndex}
/>
)
)}
</Column>
</Column>
)
}
interface SearchBarDropdownProps {
toggleOpen: () => void
tokens: FungibleToken[]
collections: GenieCollection[]
hasInput: boolean
}
export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }: SearchBarDropdownProps) => {
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0)
const searchHistory = useSearchHistory(
(state: { history: (FungibleToken | GenieCollection)[] }) => state.history
).slice(0, 2)
const { pathname } = useLocation()
const isNFTPage = pathname.includes('/nfts')
const isTokenPage = pathname.includes('/tokens')
const phase1Flag = useNftFlag()
const tokenSearchResults =
tokens.length > 0 ? (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={isNFTPage ? collections.length : 0}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={tokens}
header={<Trans>Tokens</Trans>}
/>
) : (
<Box className={styles.notFoundContainer}>
<Trans>No tokens found.</Trans>
</Box>
)
const collectionSearchResults =
phase1Flag === NftVariant.Enabled ? (
collections.length > 0 ? (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={isNFTPage ? 0 : tokens.length}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={collections}
header={<Trans>NFT Collections</Trans>}
/>
) : (
<Box className={styles.notFoundContainer}>No NFT collections found.</Box>
)
) : null
const { data: trendingCollectionResults } = 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 { data: trendingTokenResults } = useQuery([], () => 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 totalSuggestions = hasInput
? tokens.length + collections.length
: Math.min(searchHistory.length, 2) +
(isNFTPage || !isTokenPage ? trendingCollections?.length ?? 0 : 0) +
(isTokenPage || !isNFTPage ? trendingTokens?.length ?? 0 : 0)
// Close the modal on escape
useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => {
if (event.key === 'ArrowUp') {
event.preventDefault()
if (!hoveredIndex) {
setHoveredIndex(totalSuggestions - 1)
} else {
setHoveredIndex(hoveredIndex - 1)
}
} else if (event.key === 'ArrowDown') {
event.preventDefault()
if (hoveredIndex && hoveredIndex === totalSuggestions - 1) {
setHoveredIndex(0)
} else {
setHoveredIndex((hoveredIndex ?? -1) + 1)
}
}
}
document.addEventListener('keydown', keyDownHandler)
return () => {
document.removeEventListener('keydown', keyDownHandler)
}
}, [toggleOpen, hoveredIndex, totalSuggestions])
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>
)
}
function isCollection(suggestion: GenieCollection | FungibleToken | TrendingCollection) {
return (suggestion as FungibleToken).decimals === undefined
}
export const SearchBar = () => {
const [isOpen, toggleOpen] = useReducer((state: boolean) => !state, false)
const [searchValue, setSearchValue] = useState('')
const debouncedSearchValue = useDebounce(searchValue, 300)
const searchRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
const { pathname } = useLocation()
const phase1Flag = useNftFlag()
const isMobile = useIsMobile()
useOnClickOutside(searchRef, () => {
isOpen && toggleOpen()
})
const { data: collections, isLoading: collectionsAreLoading } = useQuery(
['searchCollections', debouncedSearchValue],
() => fetchSearchCollections(debouncedSearchValue),
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
}
)
const { data: tokens, isLoading: tokensAreLoading } = useQuery(
['searchTokens', debouncedSearchValue],
() => fetchSearchTokens(debouncedSearchValue),
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
}
)
const isNFTPage = pathname.includes('/nfts')
const [reducedTokens, reducedCollections] = organizeSearchResults(isNFTPage, tokens ?? [], collections ?? [])
useEffect(() => {
const escapeKeyDownHandler = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpen) {
event.preventDefault()
toggleOpen()
}
}
document.addEventListener('keydown', escapeKeyDownHandler)
return () => {
document.removeEventListener('keydown', escapeKeyDownHandler)
}
}, [isOpen, toggleOpen, collections])
// clear searchbar when changing pages
useEffect(() => {
setSearchValue('')
}, [pathname])
// auto set cursor when searchbar is opened
useEffect(() => {
if (isOpen) {
inputRef.current?.focus()
}
}, [isOpen])
const placeholderText = phase1Flag === NftVariant.Enabled ? t`Search tokens and NFT collections` : t`Search tokens`
return (
<Box position="relative">
<Box
position={isOpen ? { sm: 'fixed', md: 'absolute' } : 'static'}
width={{ sm: isOpen ? 'viewWidth' : 'auto', md: 'auto' }}
ref={searchRef}
className={styles.searchBarContainer}
>
<Row
className={clsx(`${styles.searchBar} ${!isOpen && magicalGradientOnHover}`)}
borderRadius={isOpen ? 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()}
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>
<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}
value={searchValue}
ref={inputRef}
/>
</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 />
) : (
<SearchBarDropdown
toggleOpen={toggleOpen}
tokens={reducedTokens}
collections={reducedCollections}
hasInput={debouncedSearchValue.length > 0}
/>
))}
</Box>
{isOpen && <Overlay />}
</Box>
)
}

View File

@@ -0,0 +1,187 @@
import clsx from 'clsx'
import uriToHttp from 'lib/utils/uriToHttp'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { vars } from 'nft/css/sprinkles.css'
import { useSearchHistory } from 'nft/hooks'
import { FungibleToken, GenieCollection } from 'nft/types'
import { ethNumberStandardFormatter } from 'nft/utils/currency'
import { putCommas } from 'nft/utils/putCommas'
import { useCallback, useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { VerifiedIcon } from '../../nft/components/icons'
import * as styles from './SearchBar.css'
interface CollectionRowProps {
collection: GenieCollection
isHovered: boolean
setHoveredIndex: (index: number | undefined) => void
toggleOpen: () => void
index: number
}
export const CollectionRow = ({ collection, isHovered, setHoveredIndex, toggleOpen, index }: CollectionRowProps) => {
const [brokenImage, setBrokenImage] = useState(false)
const [loaded, setLoaded] = useState(false)
const addToSearchHistory = useSearchHistory(
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
)
const navigate = useNavigate()
const handleClick = useCallback(() => {
addToSearchHistory(collection)
toggleOpen()
}, [addToSearchHistory, collection, toggleOpen])
useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => {
if (event.key === 'Enter' && isHovered) {
event.preventDefault()
navigate(`/nfts/collection/${collection.address}`)
handleClick()
}
}
document.addEventListener('keydown', keyDownHandler)
return () => {
document.removeEventListener('keydown', keyDownHandler)
}
}, [toggleOpen, isHovered, collection, navigate, handleClick])
return (
<Link
to={`/nfts/collection/${collection.address}`}
onClick={handleClick}
onMouseEnter={() => !isHovered && setHoveredIndex(index)}
onMouseLeave={() => isHovered && setHoveredIndex(undefined)}
className={styles.suggestionRow}
style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }}
>
<Row style={{ width: '60%' }}>
{!brokenImage && collection.imageUrl ? (
<Box
as="img"
src={collection.imageUrl}
alt={collection.name}
className={clsx(loaded ? styles.suggestionImage : styles.imageHolder)}
onError={() => setBrokenImage(true)}
onLoad={() => setLoaded(true)}
/>
) : (
<Box className={styles.imageHolder} />
)}
<Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full">
<Box className={styles.primaryText}>{collection.name}</Box>
{collection.isVerified && <VerifiedIcon className={styles.suggestionIcon} />}
</Row>
<Box className={styles.secondaryText}>{putCommas(collection.stats.total_supply)} items</Box>
</Column>
</Row>
{collection.floorPrice ? (
<Column className={styles.suggestionSecondaryContainer}>
<Row gap="4">
<Box className={styles.primaryText}>{ethNumberStandardFormatter(collection.floorPrice)} ETH</Box>
</Row>
<Box className={styles.secondaryText}>Floor</Box>
</Column>
) : null}
</Link>
)
}
interface TokenRowProps {
token: FungibleToken
isHovered: boolean
setHoveredIndex: (index: number | undefined) => void
toggleOpen: () => void
index: number
}
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index }: TokenRowProps) => {
const [brokenImage, setBrokenImage] = useState(false)
const [loaded, setLoaded] = useState(false)
const addToSearchHistory = useSearchHistory(
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
)
const navigate = useNavigate()
const handleClick = useCallback(() => {
addToSearchHistory(token)
toggleOpen()
}, [addToSearchHistory, toggleOpen, token])
// Close the modal on escape
useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => {
if (event.key === 'Enter' && isHovered) {
event.preventDefault()
navigate(`/tokens/${token.address}`)
handleClick()
}
}
document.addEventListener('keydown', keyDownHandler)
return () => {
document.removeEventListener('keydown', keyDownHandler)
}
}, [toggleOpen, isHovered, token, navigate, handleClick])
return (
<Link
to={`/tokens/${token.address}`}
onClick={handleClick}
onMouseEnter={() => !isHovered && setHoveredIndex(index)}
onMouseLeave={() => isHovered && setHoveredIndex(undefined)}
className={styles.suggestionRow}
style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }}
>
<Row style={{ width: '65%' }}>
{!brokenImage && token.logoURI ? (
<Box
as="img"
src={token.logoURI.includes('ipfs://') ? uriToHttp(token.logoURI)[0] : token.logoURI}
alt={token.name}
className={clsx(loaded ? styles.suggestionImage : styles.imageHolder)}
onError={() => setBrokenImage(true)}
onLoad={() => setLoaded(true)}
/>
) : (
<Box className={styles.imageHolder} />
)}
<Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full">
<Box className={styles.primaryText}>{token.name}</Box>
{token.onDefaultList && <VerifiedIcon className={styles.suggestionIcon} />}
</Row>
<Box className={styles.secondaryText}>{token.symbol}</Box>
</Column>
</Row>
<Column className={styles.suggestionSecondaryContainer}>
{token.priceUsd && (
<Row gap="4">
<Box className={styles.primaryText}>{ethNumberStandardFormatter(token.priceUsd, true)}</Box>
</Row>
)}
{token.price24hChange && (
<Box className={styles.secondaryText} color={token.price24hChange >= 0 ? 'green400' : 'red400'}>
{token.price24hChange.toFixed(2)}%
</Box>
)}
</Column>
</Link>
)
}
export const SkeletonRow = () => {
return (
<Box className={styles.searchBarDropdown}>
<Row className={styles.suggestionRow}>
<Row>
<Box className={styles.imageHolder} />
<Box borderRadius="round" height="16" width="160" background="loading" />
</Row>
</Row>
</Box>
)
}

View File

@@ -23,7 +23,7 @@ const Tabs = styled.div`
const StyledHistoryLink = styled(HistoryLink)<{ flex: string | undefined }>`
flex: ${({ flex }) => flex ?? 'none'};
${({ theme }) => theme.mediaWidth.upToMedium`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
flex: none;
margin-right: 10px;
`};

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'
const PopoverContainer = styled.div<{ show: boolean }>`
z-index: 9999;
z-index: ${Z_INDEX.absoluteTop};
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
opacity: ${(props) => (props.show ? 1 : 0)};
transition: visibility 150ms linear, opacity 150ms linear;

View File

@@ -1,3 +1,4 @@
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
import { useCallback, useEffect } from 'react'
import { X } from 'react-feather'
import { animated } from 'react-spring'
@@ -29,7 +30,7 @@ const Popup = styled.div`
padding-right: 35px;
overflow: hidden;
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
min-width: 290px;
&:not(:last-of-type) {
margin-right: 20px;
@@ -57,6 +58,7 @@ export default function PopupItem({
popKey: string
}) {
const removePopup = useRemovePopup()
const navbarFlag = useNavBarFlag()
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
useEffect(() => {
if (removeAfterMs === null) return undefined
@@ -71,23 +73,24 @@ export default function PopupItem({
}, [removeAfterMs, removeThisPopup])
const theme = useTheme()
const faderStyle = useSpring({
from: { width: '100%' },
to: { width: '0%' },
config: { duration: removeAfterMs ?? undefined },
})
let popupContent
if ('txn' in content) {
const {
txn: { hash },
} = content
if (navbarFlag === NavBarVariant.Enabled) return null
popupContent = <TransactionPopup hash={hash} />
} else if ('failedSwitchNetwork' in content) {
popupContent = <FailedNetworkSwitchPopup chainId={content.failedSwitchNetwork} />
}
const faderStyle = useSpring({
from: { width: '100%' },
to: { width: '0%' },
config: { duration: removeAfterMs ?? undefined },
})
return (
<Popup>
<StyledClose color={theme.deprecated_text2} onClick={removeThisPopup} />

View File

@@ -21,7 +21,7 @@ const Wrapper = styled(AutoColumn)`
color: ${({ theme }) => theme.deprecated_text1};
overflow: hidden;
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
max-width: 100%;
`}
`

View File

@@ -17,7 +17,7 @@ const MobilePopupWrapper = styled.div<{ height: string | number }>`
margin-bottom: ${({ height }) => (height ? '20px' : 0)};
display: none;
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
display: block;
padding-top: 20px;
`};
@@ -35,8 +35,8 @@ const MobilePopupInner = styled.div`
}
`
const StopOverflowQuery = `@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium + 1}px) and (max-width: ${
MEDIA_WIDTHS.upToMedium + 500
const StopOverflowQuery = `@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToMedium + 1}px) and (max-width: ${
MEDIA_WIDTHS.deprecated_upToMedium + 500
}px)`
const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean; xlPadding: boolean }>`
@@ -47,7 +47,7 @@ const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean; xlPadding:
width: 100%;
z-index: 3;
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
display: none;
`};

View File

@@ -12,7 +12,7 @@ const DesktopHeader = styled.div`
font-weight: 500;
padding: 8px;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
align-items: center;
display: flex;
justify-content: space-between;
@@ -32,11 +32,11 @@ const MobileHeader = styled.div`
justify-content: space-between;
align-items: center;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
display: none;
}
@media screen and (max-width: ${MEDIA_WIDTHS.upToExtraSmall}px) {
@media screen and (max-width: ${MEDIA_WIDTHS.deprecated_upToExtraSmall}px) {
display: flex;
flex-direction: column;
align-items: start;
@@ -55,7 +55,7 @@ const ToggleLabel = styled.div`
`
const MobileTogglePosition = styled.div`
@media screen and (max-width: ${MEDIA_WIDTHS.upToExtraSmall}px) {
@media screen and (max-width: ${MEDIA_WIDTHS.deprecated_upToExtraSmall}px) {
position: absolute;
right: 20px;
}

View File

@@ -48,11 +48,11 @@ const LinkRow = styled(Link)`
background-color: ${({ theme }) => theme.deprecated_bg2};
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) {
/* flex-direction: row; */
}
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
flex-direction: column;
row-gap: 12px;
`};
@@ -61,7 +61,7 @@ const LinkRow = styled(Link)`
const BadgeText = styled.div`
font-weight: 500;
font-size: 14px;
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
font-size: 12px;
`};
`
@@ -78,7 +78,7 @@ const RangeLineItem = styled(DataLineItem)`
margin-top: 4px;
width: 100%;
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
background-color: ${({ theme }) => theme.deprecated_bg2};
border-radius: 12px;
padding: 8px 0;
@@ -88,7 +88,7 @@ const RangeLineItem = styled(DataLineItem)`
const DoubleArrow = styled.span`
margin: 0 2px;
color: ${({ theme }) => theme.deprecated_text3};
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
margin: 4px;
padding: 20px;
`};
@@ -104,7 +104,7 @@ const ExtentsText = styled.span`
color: ${({ theme }) => theme.deprecated_text3};
font-size: 14px;
margin-right: 4px;
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
display: none;
`};
`
@@ -122,7 +122,7 @@ const DataText = styled.div`
font-weight: 600;
font-size: 18px;
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
font-size: 14px;
`};
`

View File

@@ -13,7 +13,7 @@ import styled from 'styled-components/macro'
import { currencyId } from 'utils/currencyId'
const MobileWrapper = styled(AutoColumn)`
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
display: none;
`};
`

View File

@@ -128,7 +128,7 @@ function CurrencyRow({
eventProperties,
}: {
currency: Currency
onSelect: () => void
onSelect: (hasWarning: boolean) => void
isSelected: boolean
otherSelected: boolean
style: CSSProperties
@@ -159,13 +159,13 @@ function CurrencyRow({
redesignFlag={redesignFlagEnabled}
style={style}
className={`token-item-${key}`}
onKeyPress={(e) => (!isSelected && e.key === 'Enter' ? onSelect() : null)}
onClick={() => (isSelected ? null : onSelect())}
onKeyPress={(e) => (!isSelected && e.key === 'Enter' ? onSelect(!!warning) : null)}
onClick={() => (isSelected ? null : onSelect(!!warning))}
disabled={isSelected}
selected={otherSelected}
>
<Column>
<CurrencyLogo currency={currency} size={'24px'} />
<CurrencyLogo currency={currency} size={'36px'} />
</Column>
<AutoColumn>
<Row>
@@ -279,7 +279,7 @@ export default function CurrencyList({
currencies: Currency[]
otherListTokens?: WrappedTokenInfo[]
selectedCurrency?: Currency | null
onCurrencySelect: (currency: Currency) => void
onCurrencySelect: (currency: Currency, hasWarning?: boolean) => void
otherCurrency?: Currency | null
fixedListRef?: MutableRefObject<FixedSizeList | undefined>
showImportView: () => void
@@ -308,7 +308,7 @@ export default function CurrencyList({
const isSelected = Boolean(currency && selectedCurrency && selectedCurrency.equals(currency))
const otherSelected = Boolean(currency && otherCurrency && otherCurrency.equals(currency))
const handleSelect = () => currency && onCurrencySelect(currency)
const handleSelect = (hasWarning: boolean) => currency && onCurrencySelect(currency, hasWarning)
const token = currency?.wrapped

View File

@@ -51,7 +51,7 @@ interface CurrencySearchProps {
isOpen: boolean
onDismiss: () => void
selectedCurrency?: Currency | null
onCurrencySelect: (currency: Currency) => void
onCurrencySelect: (currency: Currency, hasWarning?: boolean) => void
otherSelectedCurrency?: Currency | null
showCommonBases?: boolean
showCurrencyAmount?: boolean
@@ -136,9 +136,9 @@ export function CurrencySearch({
}, [debouncedQuery, native, filteredSortedTokens])
const handleCurrencySelect = useCallback(
(currency: Currency) => {
onCurrencySelect(currency)
onDismiss()
(currency: Currency, hasWarning?: boolean) => {
onCurrencySelect(currency, hasWarning)
if (!hasWarning) onDismiss()
},
[onDismiss, onCurrencySelect]
)

View File

@@ -3,10 +3,12 @@ import { TokenList } from '@uniswap/token-lists'
import TokenSafety from 'components/TokenSafety'
import { TokenSafetyVariant, useTokenSafetyFlag } from 'featureFlags/flags/tokenSafety'
import usePrevious from 'hooks/usePrevious'
import { useCallback, useEffect, useState } from 'react'
import { memo, useCallback, useEffect, useState } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import { useUserAddedTokens } from 'state/user/hooks'
import useLast from '../../hooks/useLast'
import { useWindowSize } from '../../hooks/useWindowSize'
import Modal from '../Modal'
import { CurrencySearch } from './CurrencySearch'
import { ImportList } from './ImportList'
@@ -29,9 +31,10 @@ export enum CurrencyModalView {
manage,
importToken,
importList,
tokenSafety,
}
export default function CurrencySearchModal({
export default memo(function CurrencySearchModal({
isOpen,
onDismiss,
onCurrencySelect,
@@ -43,6 +46,7 @@ export default function CurrencySearchModal({
}: CurrencySearchModalProps) {
const [modalView, setModalView] = useState<CurrencyModalView>(CurrencyModalView.manage)
const lastOpen = useLast(isOpen)
const userAddedTokens = useUserAddedTokens()
useEffect(() => {
if (isOpen && !lastOpen) {
@@ -50,12 +54,28 @@ export default function CurrencySearchModal({
}
}, [isOpen, lastOpen])
const showTokenSafetySpeedbump = (token: Token) => {
setWarningToken(token)
setModalView(CurrencyModalView.tokenSafety)
}
const tokenSafetyFlag = useTokenSafetyFlag()
const handleCurrencySelect = useCallback(
(currency: Currency) => {
onCurrencySelect(currency)
onDismiss()
(currency: Currency, hasWarning?: boolean) => {
if (
tokenSafetyFlag === TokenSafetyVariant.Enabled &&
hasWarning &&
currency.isToken &&
!userAddedTokens.find((token) => token.equals(currency))
) {
showTokenSafetySpeedbump(currency)
} else {
onCurrencySelect(currency)
onDismiss()
}
},
[onDismiss, onCurrencySelect]
[onDismiss, onCurrencySelect, tokenSafetyFlag, userAddedTokens]
)
// for token import view
@@ -68,6 +88,9 @@ export default function CurrencySearchModal({
const [importList, setImportList] = useState<TokenList | undefined>()
const [listURL, setListUrl] = useState<string | undefined>()
// used for token safety
const [warningToken, setWarningToken] = useState<Token | undefined>()
const showImportView = useCallback(() => setModalView(CurrencyModalView.importToken), [setModalView])
const showManageView = useCallback(() => setModalView(CurrencyModalView.manage), [setModalView])
const handleBackImport = useCallback(
@@ -75,13 +98,16 @@ export default function CurrencySearchModal({
[setModalView, prevView]
)
const tokenSafetyFlag = useTokenSafetyFlag()
const { height: windowHeight } = useWindowSize()
// change min height if not searching
let minHeight: number | undefined = 80
let modalHeight: number | undefined = 80
let content = null
switch (modalView) {
case CurrencyModalView.search:
if (windowHeight) {
// Converts pixel units to vh for Modal component
modalHeight = Math.min(Math.round((680 / windowHeight) * 100), 80)
}
content = (
<CurrencySearch
isOpen={isOpen}
@@ -98,29 +124,38 @@ export default function CurrencySearchModal({
/>
)
break
case CurrencyModalView.tokenSafety:
modalHeight = undefined
if (tokenSafetyFlag === TokenSafetyVariant.Enabled && warningToken) {
content = (
<TokenSafety
tokenAddress={warningToken.address}
onContinue={() => handleCurrencySelect(warningToken)}
onCancel={() => setModalView(CurrencyModalView.search)}
showCancel={true}
/>
)
}
break
case CurrencyModalView.importToken:
if (importToken) {
minHeight = undefined
content =
tokenSafetyFlag === TokenSafetyVariant.Enabled ? (
<TokenSafety
tokenAddress={importToken.address}
onContinue={() => handleCurrencySelect(importToken)}
onCancel={handleBackImport}
/>
) : (
<ImportToken
tokens={[importToken]}
onDismiss={onDismiss}
list={importToken instanceof WrappedTokenInfo ? importToken.list : undefined}
onBack={handleBackImport}
handleCurrencySelect={handleCurrencySelect}
/>
)
modalHeight = undefined
if (tokenSafetyFlag === TokenSafetyVariant.Enabled) {
showTokenSafetySpeedbump(importToken)
}
content = (
<ImportToken
tokens={[importToken]}
onDismiss={onDismiss}
list={importToken instanceof WrappedTokenInfo ? importToken.list : undefined}
onBack={handleBackImport}
handleCurrencySelect={handleCurrencySelect}
/>
)
}
break
case CurrencyModalView.importList:
minHeight = 40
modalHeight = 40
if (importList && listURL) {
content = <ImportList list={importList} listURL={listURL} onDismiss={onDismiss} setModalView={setModalView} />
}
@@ -138,8 +173,8 @@ export default function CurrencySearchModal({
break
}
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={80} minHeight={minHeight}>
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={modalHeight} minHeight={modalHeight}>
{content}
</Modal>
)
}
})

View File

@@ -23,7 +23,7 @@ const AddressText = styled(ThemedText.DeprecatedBlue)`
font-size: 12px;
word-break: break-all;
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
font-size: 10px;
`}
`

View File

@@ -1,3 +1,4 @@
import searchIcon from 'assets/svg/search.svg'
import { LoadingRows as BaseLoadingRows } from 'components/Loader/styled'
import styled from 'styled-components/macro'
@@ -37,14 +38,18 @@ export const MenuItem = styled(RowBetween)<{ redesignFlag?: boolean }>`
`
export const SearchInput = styled.input<{ redesignFlag?: boolean }>`
background: no-repeat scroll 7px 7px;
background-image: url(${searchIcon});
background-size: 20px 20px;
background-position: 12px center;
position: relative;
display: flex;
padding: 16px;
padding-left: 40px;
height: ${({ redesignFlag }) => redesignFlag && '40px'};
align-items: center;
width: 100%;
white-space: nowrap;
background: none;
background-color: ${({ theme, redesignFlag }) => redesignFlag && theme.backgroundModule};
border: none;
outline: none;
@@ -62,8 +67,9 @@ export const SearchInput = styled.input<{ redesignFlag?: boolean }>`
}
transition: border 100ms;
:focus {
border: 1px solid ${({ theme, redesignFlag }) => (redesignFlag ? 'transparent' : theme.deprecated_primary1)};
background-color: ${({ theme, redesignFlag }) => redesignFlag && theme.accentActionSoft};
border: 1px solid
${({ theme, redesignFlag }) => (redesignFlag ? theme.accentActiveSoft : theme.deprecated_primary1)};
background-color: ${({ theme, redesignFlag }) => redesignFlag && theme.backgroundSurface};
outline: none;
}
`

View File

@@ -99,7 +99,7 @@ const MenuFlyout = styled.span<{ redesignFlag: boolean }>`
z-index: 100;
color: ${({ theme, redesignFlag }) => redesignFlag && theme.textPrimary};
${({ theme }) => theme.mediaWidth.upToMedium`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
min-width: 18.125rem;
`};

View File

@@ -6,13 +6,18 @@ import styled, { keyframes } from 'styled-components/macro'
const Wrapper = styled.button<{ isActive?: boolean; activeElement?: boolean; redesignFlag: boolean }>`
align-items: center;
background: ${({ isActive, theme, redesignFlag }) =>
redesignFlag && isActive ? theme.accentActionSoft : theme.deprecated_bg1};
border: none;
redesignFlag && isActive
? theme.accentActionSoft
: redesignFlag && !isActive
? 'transparent'
: theme.deprecated_bg1};
border: ${({ redesignFlag, theme, isActive }) =>
redesignFlag && !isActive ? `1px solid ${theme.backgroundOutline}` : 'none'};
border-radius: 20px;
cursor: pointer;
display: flex;
outline: none;
padding: 0.4rem 0.4rem;
padding: ${({ redesignFlag }) => (redesignFlag ? '4px' : '0.4rem 0.4rem')};
width: fit-content;
`

View File

@@ -8,7 +8,7 @@ import { ExternalLink } from 'theme'
import { Color } from 'theme/styled'
const Label = styled.div<{ color: Color }>`
width: 284px;
width: 100%;
padding: 12px 20px;
background-color: ${({ color }) => color + '1F'};
border-radius: 16px;

View File

@@ -1,12 +1,8 @@
import Modal from '../Modal'
import TokenSafety from '.'
import TokenSafety, { TokenSafetyProps } from '.'
interface TokenSafetyModalProps {
interface TokenSafetyModalProps extends TokenSafetyProps {
isOpen: boolean
tokenAddress: string | null
secondTokenAddress?: string
onContinue: () => void
onCancel: () => void
}
export default function TokenSafetyModal({
@@ -15,14 +11,18 @@ export default function TokenSafetyModal({
secondTokenAddress,
onContinue,
onCancel,
onBlocked,
showCancel,
}: TokenSafetyModalProps) {
return (
<Modal isOpen={isOpen} onDismiss={onCancel}>
<TokenSafety
tokenAddress={tokenAddress}
secondTokenAddress={secondTokenAddress}
onCancel={onCancel}
onContinue={onContinue}
onBlocked={onBlocked}
onCancel={onCancel}
showCancel={showCancel}
/>
</Modal>
)

View File

@@ -4,14 +4,13 @@ import { ButtonPrimary } from 'components/Button'
import { AutoColumn } from 'components/Column'
import CurrencyLogo from 'components/CurrencyLogo'
import TokenSafetyLabel from 'components/TokenSafety/TokenSafetyLabel'
import { checkWarning, getWarningCopy, TOKEN_SAFETY_ARTICLE, Warning, WARNING_LEVEL } from 'constants/tokenSafety'
import { checkWarning, getWarningCopy, TOKEN_SAFETY_ARTICLE, Warning } from 'constants/tokenSafety'
import { useToken } from 'hooks/Tokens'
import { ExternalLink as LinkIconFeather } from 'react-feather'
import { Text } from 'rebass'
import { useAddUserToken } from 'state/user/hooks'
import styled, { useTheme } from 'styled-components/macro'
import styled from 'styled-components/macro'
import { ButtonText, CopyLinkIcon, ExternalLink } from 'theme'
import { Color } from 'theme/styled'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
const Wrapper = styled.div`
@@ -46,61 +45,54 @@ const InfoText = styled(Text)`
text-align: center;
`
const StyledButton = styled(ButtonPrimary)<{ buttonColor: Color; textColor: Color }>`
color: ${({ textColor }) => textColor};
background-color: ${({ buttonColor }) => buttonColor};
const StyledButton = styled(ButtonPrimary)`
margin-top: 24px;
width: 100%;
:hover {
background-color: ${({ buttonColor, theme }) => buttonColor ?? theme.accentAction};
}
font-weight: 600;
`
const StyledCancelButton = styled(ButtonText)<{ color?: Color }>`
const StyledCancelButton = styled(ButtonText)`
margin-top: 16px;
color: ${({ color, theme }) => color ?? theme.accentAction};
color: ${({ theme }) => theme.textSecondary};
font-weight: 600;
font-size: 14px;
`
const StyledCloseButton = styled(StyledButton)`
background-color: ${({ theme }) => theme.backgroundInteractive};
color: ${({ theme }) => theme.textPrimary};
&:hover {
background-color: ${({ theme }) => theme.backgroundInteractive};
opacity: 0.6;
transition: opacity 250ms ease;
}
`
const Buttons = ({
warning,
onContinue,
onCancel,
onBlocked,
showCancel,
}: {
warning: Warning
onContinue: () => void
onCancel: () => void
onBlocked?: () => void
showCancel?: boolean
}) => {
const theme = useTheme()
let textColor, buttonColor, cancelColor
switch (warning.level) {
case WARNING_LEVEL.MEDIUM:
textColor = theme.white
buttonColor = theme.accentAction
cancelColor = theme.accentAction
break
case WARNING_LEVEL.UNKNOWN:
textColor = theme.accentFailure
buttonColor = theme.accentFailureSoft
cancelColor = theme.textPrimary
break
case WARNING_LEVEL.BLOCKED:
textColor = theme.textPrimary
buttonColor = theme.backgroundInteractive
break
}
return warning.canProceed ? (
<>
<StyledButton buttonColor={buttonColor} textColor={textColor} onClick={onContinue}>
<Trans>I Understand</Trans>
<StyledButton onClick={onContinue}>
<Trans>I understand</Trans>
</StyledButton>
<StyledCancelButton color={cancelColor} onClick={onCancel}>
Cancel
</StyledCancelButton>
{showCancel && <StyledCancelButton onClick={onCancel}>Cancel</StyledCancelButton>}
</>
) : (
<StyledButton buttonColor={buttonColor} textColor={textColor} onClick={onCancel}>
<StyledCloseButton onClick={onBlocked ?? onCancel}>
<Trans>Close</Trans>
</StyledButton>
</StyledCloseButton>
)
}
@@ -124,8 +116,8 @@ const ExplorerContainer = styled.div`
height: 32px;
margin-top: 10px;
font-size: 20px;
background-color: ${({ theme }) => theme.accentActiveSoft};
color: ${({ theme }) => theme.accentActive};
background-color: ${({ theme }) => theme.accentActionSoft};
color: ${({ theme }) => theme.accentAction};
border-radius: 8px;
padding: 2px 12px;
display: flex;
@@ -190,14 +182,27 @@ function ExplorerView({ token }: { token: Token }) {
}
}
interface TokenSafetyProps {
const StyledExternalLink = styled(ExternalLink)`
font-weight: 600;
`
export interface TokenSafetyProps {
tokenAddress: string | null
secondTokenAddress?: string
onContinue: () => void
onCancel: () => void
onBlocked?: () => void
showCancel?: boolean
}
export default function TokenSafety({ tokenAddress, secondTokenAddress, onContinue, onCancel }: TokenSafetyProps) {
export default function TokenSafety({
tokenAddress,
secondTokenAddress,
onContinue,
onCancel,
onBlocked,
showCancel,
}: TokenSafetyProps) {
const logos = []
const urls = []
@@ -254,13 +259,13 @@ export default function TokenSafety({ tokenAddress, secondTokenAddress, onContin
<ShortColumn>
<InfoText>
{description}{' '}
<ExternalLink href={TOKEN_SAFETY_ARTICLE}>
<Trans>Learn More</Trans>
</ExternalLink>
<StyledExternalLink href={TOKEN_SAFETY_ARTICLE}>
<Trans>Learn more</Trans>
</StyledExternalLink>
</InfoText>
</ShortColumn>
<LinkColumn>{urls}</LinkColumn>
<Buttons warning={displayWarning} onContinue={acknowledge} onCancel={onCancel} />
<Buttons warning={displayWarning} onContinue={acknowledge} onCancel={onCancel} showCancel={showCancel} />
</Container>
</Wrapper>
)

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

View File

@@ -1,3 +1,4 @@
import { Trans } from '@lingui/macro'
import { useToken } from 'hooks/Tokens'
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
import { useState } from 'react'
@@ -82,7 +83,7 @@ const SwapButton = styled.button`
display: flex;
align-items: center;
border: none;
color: ${({ theme }) => theme.textPrimary};
color: ${({ theme }) => theme.accentTextLightPrimary};
padding: 12px 16px;
width: 120px;
height: 44px;
@@ -152,7 +153,9 @@ export default function FooterBalanceSummary({
) : error ? (
<ErrorState>
<AlertTriangle size={17} />
<ErrorText>There was an error fetching your balance</ErrorText>
<ErrorText>
<Trans>There was an error fetching your balance</Trans>
</ErrorText>
</ErrorState>
) : (
<BalanceInfo>
@@ -165,16 +168,21 @@ export default function FooterBalanceSummary({
</BalanceTotal>
{multipleBalances && (
<ViewAll onClick={() => setShowMultipleBalances(!showMultipleBalances)}>
{showMultipleBalances ? 'Hide' : 'View'} all balances
<Trans>{showMultipleBalances ? 'Hide' : 'View'} all balances</Trans>
</ViewAll>
)}
</BalanceInfo>
)}
<SwapButton onClick={() => (window.location.href = 'https://app.uniswap.org/#/swap')}>Swap</SwapButton>
<SwapButton onClick={() => (window.location.href = 'https://app.uniswap.org/#/swap')}>
<Trans>Swap</Trans>
</SwapButton>
</TotalBalancesSection>
{showMultipleBalances && (
<NetworkBalancesSection>
<NetworkBalancesLabel>Your balances by network</NetworkBalancesLabel> {networkBalances}
<NetworkBalancesLabel>
<Trans>Your balances by network</Trans>
</NetworkBalancesLabel>
{networkBalances}
</NetworkBalancesSection>
)}
<FakeFooterNavBar>**leaving space for updated nav footer**</FakeFooterNavBar>

View File

@@ -3,8 +3,8 @@ import styled, { useTheme } from 'styled-components/macro'
import { LoadingBubble } from '../loading'
import { DeltaContainer, TokenPrice } from './PriceChart'
import {
AboutContainer,
AboutHeader,
AboutSection,
BreadcrumbNavLink,
ChartContainer,
ChartHeader,
@@ -16,7 +16,7 @@ import {
TokenInfoContainer,
TokenNameCell,
TopArea,
} from './TokenDetail'
} from './TokenDetailContainers'
const LoadingChartContainer = styled(ChartContainer)`
height: 336px;
@@ -34,15 +34,17 @@ const TitleLoadingBubble = styled(LoadingDetailBubble)`
const SquareLoadingBubble = styled(LoadingDetailBubble)`
height: 32px;
border-radius: 8px;
margin-top: 4px;
margin-bottom: 10px;
`
const PriceLoadingBubble = styled(SquareLoadingBubble)`
height: 40px;
`
const LongLoadingBubble = styled(LoadingDetailBubble)`
margin-top: 6px;
width: 100%;
`
const HalfLoadingBubble = styled(LoadingDetailBubble)`
margin-top: 6px;
width: 50%;
`
const IconLoadingBubble = styled(LoadingDetailBubble)`
@@ -76,7 +78,7 @@ const Space = styled.div<{ heightSize: number }>`
height: ${({ heightSize }) => `${heightSize}px`};
`
function Wave() {
export function Wave() {
const theme = useTheme()
return (
<svg width="416" height="160" xmlns="http://www.w3.org/2000/svg">
@@ -118,16 +120,6 @@ export default function LoadingTokenDetail() {
</LoadingChartContainer>
<Space heightSize={32} />
</ChartHeader>
<AboutSection>
<AboutHeader>
<SquareLoadingBubble />
</AboutHeader>
<LongLoadingBubble />
<LongLoadingBubble />
<HalfLoadingBubble />
<ResourcesContainer>{null}</ResourcesContainer>
</AboutSection>
<StatsSection>
<StatsLoadingContainer>
<StatPair>
@@ -152,6 +144,16 @@ export default function LoadingTokenDetail() {
</StatPair>
</StatsLoadingContainer>
</StatsSection>
<AboutContainer>
<AboutHeader>
<SquareLoadingBubble />
</AboutHeader>
<LongLoadingBubble />
<LongLoadingBubble />
<HalfLoadingBubble />
<ResourcesContainer>{null}</ResourcesContainer>
</AboutContainer>
<ContractAddressSection>{null}</ContractAddressSection>
</TopArea>
)

View File

@@ -1,12 +1,14 @@
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, curveBasis, NumberValue, scaleLinear } from 'd3'
import { bisect, curveCardinal, NumberValue, scaleLinear } from 'd3'
import { useTokenPriceQuery } from 'graphql/data/TokenPriceQuery'
import { TimePeriod } from 'graphql/data/TopTokenQuery'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { TimePeriod } from 'hooks/useExplorePageQuery'
import { useAtom } from 'jotai'
import { useCallback, useState } from 'react'
import { ArrowDownRight, ArrowUpRight } from 'react-feather'
@@ -16,26 +18,20 @@ import {
dayHourFormatter,
hourFormatter,
monthDayFormatter,
monthFormatter,
monthTickFormatter,
monthYearDayFormatter,
monthYearFormatter,
weekFormatter,
} from 'utils/formatChartTimes'
import data from '../../Charts/data.json'
import LineChart from '../../Charts/LineChart'
import { DISPLAYS, ORDERED_TIMES } from '../TokenTable/TimeSelector'
// TODO: This should be combined with the logic in TimeSelector.
const TIME_DISPLAYS: [TimePeriod, string][] = [
[TimePeriod.hour, '1H'],
[TimePeriod.day, '1D'],
[TimePeriod.week, '1W'],
[TimePeriod.month, '1M'],
[TimePeriod.year, '1Y'],
[TimePeriod.all, 'All'],
]
type PricePoint = { value: number; timestamp: number }
export type PricePoint = { value: number; timestamp: number }
export const DATA_EMPTY = { value: 0, timestamp: 0 }
function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
const prices = pricePoints.map((x) => x.value)
@@ -51,17 +47,26 @@ const StyledDownArrow = styled(ArrowDownRight)`
color: ${({ theme }) => theme.accentFailure};
`
function getDelta(start: number, current: number) {
const delta = (current / start - 1) * 100
const isPositive = Math.sign(delta) > 0
export function calculateDelta(start: number, current: number) {
return (current / start - 1) * 100
}
const formattedDelta = delta.toFixed(2) + '%'
if (isPositive) {
return ['+' + formattedDelta, <StyledUpArrow size={16} key="arrow-up" />]
export function getDeltaArrow(delta: number) {
if (Math.sign(delta) > 0) {
return <StyledUpArrow size={16} key="arrow-up" />
} else if (delta === 0) {
return [formattedDelta, null]
return null
} else {
return <StyledDownArrow size={16} key="arrow-down" />
}
return [formattedDelta, <StyledDownArrow size={16} key="arrow-down" />]
}
export function formatDelta(delta: number) {
let formattedDelta = delta.toFixed(2) + '%'
if (Math.sign(delta) > 0) {
formattedDelta = '+' + formattedDelta
}
return formattedDelta
}
export const ChartHeader = styled.div`
@@ -76,6 +81,7 @@ export const DeltaContainer = styled.div`
height: 16px;
display: flex;
align-items: center;
margin-top: 4px;
`
const ArrowCell = styled.div`
padding-left: 2px;
@@ -125,45 +131,53 @@ function tickFormat(
locale: string
): [TickFormatter<NumberValue>, (v: number) => string, number[]] {
switch (timePeriod) {
case TimePeriod.hour:
case TimePeriod.HOUR:
return [hourFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp)]
case TimePeriod.day:
case TimePeriod.DAY:
return [hourFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp)]
case TimePeriod.week:
case TimePeriod.WEEK:
return [weekFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp, 6)]
case TimePeriod.month:
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:
case TimePeriod.YEAR:
return [monthTickFormatter(locale), monthYearDayFormatter(locale), getTicks(startTimestamp, endTimestamp)]
case TimePeriod.ALL:
return [monthYearFormatter(locale), monthYearDayFormatter(locale), getTicks(startTimestamp, endTimestamp)]
}
}
const margin = { top: 86, bottom: 48, crosshair: 72 }
const margin = { top: 100, bottom: 48, crosshair: 72 }
const timeOptionsHeight = 44
const crosshairDateOverhang = 80
interface PriceChartProps {
width: number
height: number
token: Token
}
export function PriceChart({ width, height }: PriceChartProps) {
export function PriceChart({ width, height, token }: 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 hasData = pricePoints.length !== 0
/* TODO: Implement API calls & cache to use here */
const pricePoints = data[timePeriod]
const startingPrice = pricePoints[0]
const endingPrice = pricePoints[pricePoints.length - 1]
const initialState = { pricePoint: endingPrice, xCoordinate: null }
const [selected, setSelected] = useState<{ pricePoint: PricePoint; xCoordinate: number | null }>(initialState)
const startingPrice = hasData ? pricePoints[0] : DATA_EMPTY
const endingPrice = hasData ? pricePoints[pricePoints.length - 1] : DATA_EMPTY
const [displayPrice, setDisplayPrice] = useState(startingPrice)
const [crosshair, setCrosshair] = useState<number | null>(null)
const graphWidth = width + crosshairDateOverhang
const graphHeight = height - timeOptionsHeight
const graphInnerHeight = graphHeight - margin.top - margin.bottom
// 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
@@ -174,7 +188,7 @@ export function PriceChart({ width, height }: PriceChartProps) {
const handleHover = useCallback(
(event: Element | EventType) => {
const { x } = localPoint(event) || { x: 0 }
const x0 = timeScale.invert(x) // get timestamp from the scale
const x0 = timeScale.invert(x) // get timestamp from the scalexw
const index = bisect(
pricePoints.map((x) => x.timestamp),
x0,
@@ -190,27 +204,43 @@ export function PriceChart({ width, height }: PriceChartProps) {
pricePoint = x0.valueOf() - d0.timestamp.valueOf() > d1.timestamp.valueOf() - x0.valueOf() ? d1 : d0
}
setSelected({ pricePoint, xCoordinate: timeScale(pricePoint.timestamp) })
setCrosshair(timeScale(pricePoint.timestamp))
setDisplayPrice(pricePoint)
},
[timeScale, pricePoints]
)
const resetDisplay = useCallback(() => {
setCrosshair(null)
setDisplayPrice(endingPrice)
}, [setCrosshair, setDisplayPrice, endingPrice])
// TODO: connect to loading state
if (!hasData) {
return null
}
const [tickFormatter, crosshairDateFormatter, ticks] = tickFormat(
startingPrice.timestamp,
endingPrice.timestamp,
timePeriod,
locale
)
const [delta, arrow] = getDelta(startingPrice.value, selected.pricePoint.value)
const crosshairEdgeMax = width * 0.97
const crosshairAtEdge = !!selected.xCoordinate && selected.xCoordinate > crosshairEdgeMax
const delta = calculateDelta(startingPrice.value, displayPrice.value)
const formattedDelta = formatDelta(delta)
const arrow = getDeltaArrow(delta)
const crosshairEdgeMax = width * 0.85
const crosshairAtEdge = !!crosshair && crosshair > crosshairEdgeMax
/* Default curve doesn't look good for the ALL chart */
const curveTension = timePeriod === TimePeriod.ALL ? 0.75 : 0.9
return (
<>
<ChartHeader>
<TokenPrice>${selected.pricePoint.value.toFixed(2)}</TokenPrice>
<TokenPrice>${displayPrice.value < 0.000001 ? '<0.000001' : displayPrice.value.toFixed(6)}</TokenPrice>
<DeltaContainer>
{delta}
{formattedDelta}
<ArrowCell>{arrow}</ArrowCell>
</DeltaContainer>
</ChartHeader>
@@ -219,13 +249,12 @@ export function PriceChart({ width, height }: PriceChartProps) {
getX={(p: PricePoint) => timeScale(p.timestamp)}
getY={(p: PricePoint) => rdScale(p.value)}
marginTop={margin.top}
/* Default curve doesn't look good for the ALL chart */
curve={timePeriod === TimePeriod.all ? curveBasis : undefined}
curve={curveCardinal.tension(curveTension)}
strokeWidth={2}
width={graphWidth}
height={graphHeight}
>
{selected.xCoordinate !== null ? (
{crosshair !== null ? (
<g>
<AxisBottom
scale={timeScale}
@@ -244,25 +273,25 @@ export function PriceChart({ width, height }: PriceChartProps) {
})}
/>
<text
x={selected.xCoordinate + (crosshairAtEdge ? -4 : 4)}
x={crosshair + (crosshairAtEdge ? -4 : 4)}
y={margin.crosshair + 10}
textAnchor={crosshairAtEdge ? 'end' : 'start'}
fontSize={12}
fill={theme.textSecondary}
>
{crosshairDateFormatter(selected.pricePoint.timestamp)}
{crosshairDateFormatter(displayPrice.timestamp)}
</text>
<Line
from={{ x: selected.xCoordinate, y: margin.crosshair }}
to={{ x: selected.xCoordinate, y: graphHeight }}
from={{ x: crosshair, y: margin.crosshair }}
to={{ x: crosshair, y: graphHeight }}
stroke={theme.backgroundOutline}
strokeWidth={1}
pointerEvents="none"
strokeDasharray="4,4"
/>
<GlyphCircle
left={selected.xCoordinate}
top={rdScale(selected.pricePoint.value) + margin.top}
left={crosshair}
top={rdScale(displayPrice.value) + margin.top}
size={50}
fill={theme.accentActive}
stroke={theme.backgroundOutline}
@@ -281,14 +310,14 @@ export function PriceChart({ width, height }: PriceChartProps) {
onTouchStart={handleHover}
onTouchMove={handleHover}
onMouseMove={handleHover}
onMouseLeave={() => setSelected(initialState)}
onMouseLeave={resetDisplay}
/>
</LineChart>
<TimeOptionsWrapper>
<TimeOptionsContainer>
{TIME_DISPLAYS.map(([value, display]) => (
<TimeButton key={display} active={timePeriod === value} onClick={() => setTimePeriod(value)}>
{display}
{ORDERED_TIMES.map((time) => (
<TimeButton key={DISPLAYS[time]} active={timePeriod === time} onClick={() => setTimePeriod(time)}>
{DISPLAYS[time]}
</TimeButton>
))}
</TimeOptionsContainer>

View File

@@ -1,3 +1,4 @@
import { Trans } from '@lingui/macro'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useRef } from 'react'
import { Twitter } from 'react-feather'
@@ -40,7 +41,7 @@ const ShareActions = styled.div`
padding: 8px;
background-color: ${({ theme }) => theme.backgroundSurface};
border: 0.5px solid ${({ theme }) => theme.backgroundOutline};
box-shadow: ${({ theme }) => theme.flyoutDropShadow};
box-shadow: ${({ theme }) => theme.deepShadow};
border-radius: 12px;
`
const ShareAction = styled.div`
@@ -62,6 +63,7 @@ const ShareAction = styled.div`
interface TokenInfo {
tokenName: string
tokenSymbol: string
tokenAddress: string
}
export default function ShareButton(tokenInfo: TokenInfo) {
@@ -76,7 +78,7 @@ export default function ShareButton(tokenInfo: TokenInfo) {
const shareTweet = () => {
toggleShare()
window.open(
`https://twitter.com/intent/tweet?text=Check%20out%20${tokenInfo.tokenName}%20(${tokenInfo.tokenSymbol})%20https://app.uniswap.org/%23/tokens/${tokenInfo.tokenSymbol}%20via%20@uniswap`,
`https://twitter.com/intent/tweet?text=Check%20out%20${tokenInfo.tokenName}%20(${tokenInfo.tokenSymbol})%20https://app.uniswap.org/%23/tokens/${tokenInfo.tokenAddress}%20via%20@uniswap`,
'newwindow',
`left=${positionX}, top=${positionY}, width=${TWITTER_WIDTH}, height=${TWITTER_HEIGHT}`
)
@@ -97,13 +99,13 @@ export default function ShareButton(tokenInfo: TokenInfo) {
toCopy={window.location.href}
ref={copyHelperRef}
>
Copy Link
<Trans>Copy Link</Trans>
</CopyHelper>
</ShareAction>
<ShareAction onClick={shareTweet}>
<Twitter color={theme.textPrimary} size={20} strokeWidth={1.5} />
Share to Twitter
<Trans>Share to Twitter</Trans>
</ShareAction>
</ShareActions>
)}

View File

@@ -1,57 +1,44 @@
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 } from 'constants/tokenSafety'
import { useCurrency, useIsUserAddedToken, useToken } from 'hooks/Tokens'
import { chainIdToChainName, useTokenDetailQuery } from 'graphql/data/TokenDetailQuery'
import { useCurrency, useToken } from 'hooks/Tokens'
import { useAtomValue } from 'jotai/utils'
import { useCallback } from 'react'
import { darken } from 'polished'
import { Suspense } from 'react'
import { useState } from 'react'
import { ArrowLeft, Heart } from 'react-feather'
import { Link, useNavigate } from 'react-router-dom'
import { ArrowLeft } from 'react-feather'
import styled from 'styled-components/macro'
import { ClickableStyle, CopyContractAddress } from 'theme'
import { CopyContractAddress } from 'theme'
import { formatDollarAmount } from 'utils/formatDollarAmt'
import { favoritesAtom, useToggleFavorite } from '../state'
import { ClickFavorited } from '../TokenTable/TokenRow'
import { filterNetworkAtom, useIsFavorited, useToggleFavorite } from '../state'
import { ClickFavorited, FavoriteIcon } 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'
export const AboutSection = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
padding: 24px 0px;
`
export const AboutHeader = styled.span`
font-size: 28px;
line-height: 36px;
`
export const BreadcrumbNavLink = styled(Link)`
display: flex;
color: ${({ theme }) => theme.textSecondary};
font-size: 14px;
line-height: 20px;
align-items: center;
gap: 4px;
text-decoration: none;
margin-bottom: 16px;
&:hover {
color: ${({ theme }) => theme.textTertiary};
}
`
export const ChartHeader = styled.div`
width: 100%;
display: flex;
flex-direction: column;
color: ${({ theme }) => theme.textPrimary};
gap: 4px;
margin-bottom: 24px;
`
const ContractAddress = styled.button`
display: flex;
color: ${({ theme }) => theme.textPrimary};
@@ -59,12 +46,10 @@ const ContractAddress = styled.button`
align-items: center;
background: transparent;
border: none;
min-height: 38px;
padding: 0px;
cursor: pointer;
`
export const ContractAddressSection = styled.div`
padding: 24px 0px;
`
const Contract = styled.div`
display: flex;
flex-direction: column;
@@ -72,61 +57,19 @@ const Contract = styled.div`
font-size: 14px;
gap: 4px;
`
export const ChartContainer = styled.div`
display: flex;
height: 436px;
align-items: center;
`
export const Stat = styled.div`
display: flex;
flex-direction: column;
color: ${({ theme }) => theme.textSecondary};
font-size: 14px;
min-width: 168px;
flex: 1;
gap: 4px;
padding: 24px 0px;
`
const StatPrice = styled.span`
font-size: 28px;
color: ${({ theme }) => theme.textPrimary};
`
export const StatsSection = styled.div`
display: flex;
flex-wrap: wrap;
`
export const StatPair = styled.div`
display: flex;
flex: 1;
flex-wrap: wrap;
`
export const TokenNameCell = styled.div`
display: flex;
gap: 8px;
font-size: 20px;
line-height: 28px;
align-items: center;
`
const TokenActions = styled.div`
display: flex;
gap: 16px;
color: ${({ theme }) => theme.textSecondary};
`
export const TokenInfoContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`
const TokenSymbol = styled.span`
text-transform: uppercase;
color: ${({ theme }) => theme.textSecondary};
`
export const TopArea = styled.div`
max-width: 832px;
`
export const ResourcesContainer = styled.div`
display: flex;
gap: 14px;
`
const NetworkBadge = styled.div<{ networkColor?: string; backgroundColor?: string }>`
border-radius: 5px;
padding: 4px 8px;
@@ -136,119 +79,201 @@ const NetworkBadge = styled.div<{ networkColor?: string; backgroundColor?: strin
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)
const currency = useCurrency(address)
const favoriteTokens = useAtomValue<string[]>(favoritesAtom)
const isFavorited = favoriteTokens.includes(address)
let currency = useCurrency(address)
const isFavorited = useIsFavorited(address)
const toggleFavorite = useToggleFavorite(address)
const warning = checkWarning(address)
const navigate = useNavigate()
const isUserAddedToken = useIsUserAddedToken(token)
const [warningModalOpen, setWarningModalOpen] = useState(!!warning && !isUserAddedToken)
const handleDismissWarning = useCallback(() => {
setWarningModalOpen(false)
}, [setWarningModalOpen])
const chainInfo = getChainInfo(token?.chainId)
const networkLabel = chainInfo?.label
const networkBadgebackgroundColor = chainInfo?.backgroundColor
const filterNetwork = useAtomValue(filterNetworkAtom)
const tokenDetailData = useTokenDetailQuery(address, chainIdToChainName(filterNetwork))
const relevantTokenDetailData = (({ description, homepageUrl, twitterName }) => ({
description,
homepageUrl,
twitterName,
}))(tokenDetailData)
// catch token error and loading state
if (!token || !token.name || !token.symbol) {
return <div>No Token</div>
if (!token || !token.name || !token.symbol || !connectedChainId) {
return <LoadingTokenDetail />
}
const tokenName = token.name
const tokenSymbol = token.symbol
// TODO: format price, add sparkline
const aboutToken =
'Ethereum is a decentralized computing platform that uses ETH (Ether) to pay transaction fees (gas). Developers can use Ethereum to run decentralized applications (dApps) and issue new crypto assets, known as Ethereum tokens.'
const tokenMarketCap = '23.02B'
const tokenVolume = '1.6B'
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 (
<TopArea>
<BreadcrumbNavLink to="/explore">
<ArrowLeft size={14} /> Explore
</BreadcrumbNavLink>
<ChartHeader>
<TokenInfoContainer>
<TokenNameCell>
<CurrencyLogo currency={currency} size={'32px'} />
{tokenName} <TokenSymbol>{tokenSymbol}</TokenSymbol>
{!warning && <VerifiedIcon size="20px" />}
{networkBadgebackgroundColor && (
<NetworkBadge networkColor={chainInfo?.color} backgroundColor={networkBadgebackgroundColor}>
{networkLabel}
</NetworkBadge>
)}
</TokenNameCell>
<TokenActions>
<ShareButton tokenName={tokenName} tokenSymbol={tokenSymbol} />
<ClickFavorited onClick={toggleFavorite}>
<FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited>
</TokenActions>
</TokenInfoContainer>
<ChartContainer>
<ParentSize>{({ width, height }) => <PriceChart width={width} height={height} />}</ParentSize>
</ChartContainer>
</ChartHeader>
<AboutSection>
<AboutHeader>
<Trans>About</Trans>
</AboutHeader>
{aboutToken}
<ResourcesContainer>
<Resource name={'Etherscan'} link={'https://etherscan.io/'} />
<Resource name={'Protocol Info'} link={`https://info.uniswap.org/#/tokens/${address}`} />
</ResourcesContainer>
</AboutSection>
<StatsSection>
<StatPair>
<Stat>
Market cap<StatPrice>${tokenMarketCap}</StatPrice>
</Stat>
<Stat>
{/* TODO: connect to chart's selected time */}
24H volume
<StatPrice>${tokenVolume}</StatPrice>
</Stat>
</StatPair>
<StatPair>
<Stat>
52W low
<StatPrice>$1,790.01</StatPrice>
</Stat>
<Stat>
52W high
<StatPrice>$4,420.71</StatPrice>
</Stat>
</StatPair>
</StatsSection>
<ContractAddressSection>
<Contract>
Contract Address
<ContractAddress>
<CopyContractAddress address={address} />
</ContractAddress>
</Contract>
</ContractAddressSection>
<TokenSafetyModal
isOpen={warningModalOpen}
tokenAddress={address}
onCancel={() => navigate(-1)}
onContinue={handleDismissWarning}
/>
</TopArea>
<Suspense fallback={<LoadingTokenDetail />}>
<TopArea>
<BreadcrumbNavLink to="/tokens">
<ArrowLeft size={14} /> Tokens
</BreadcrumbNavLink>
<ChartHeader>
<TokenInfoContainer>
<TokenNameCell>
<CurrencyLogo currency={currency} size={'32px'} symbol={tokenSymbol} />
{tokenName ?? <Trans>Name not found</Trans>}
<TokenSymbol>{tokenSymbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
{!warning && <VerifiedIcon size="20px" />}
{networkBadgebackgroundColor && (
<NetworkBadge networkColor={chainInfo?.color} backgroundColor={networkBadgebackgroundColor}>
{networkLabel}
</NetworkBadge>
)}
</TokenNameCell>
<TokenActions>
{tokenName && tokenSymbol && (
<ShareButton tokenName={tokenName} tokenSymbol={tokenSymbol} tokenAddress={address} />
)}
<ClickFavorited onClick={toggleFavorite}>
<FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited>
</TokenActions>
</TokenInfoContainer>
<ChartContainer>
<ParentSize>{({ width, height }) => <PriceChart token={token} width={width} height={height} />}</ParentSize>
</ChartContainer>
</ChartHeader>
<StatsSection>
<StatPair>
<Stat>
<Trans>Market cap</Trans>
<StatPrice>
{tokenDetailData.marketCap?.value ? formatDollarAmount(tokenDetailData.marketCap?.value) : '-'}
</StatPrice>
</Stat>
<Stat>
24H volume
<StatPrice>
{tokenDetailData.volume24h?.value ? formatDollarAmount(tokenDetailData.volume24h?.value) : '-'}
</StatPrice>
</Stat>
</StatPair>
<StatPair>
<Stat>
52W low
<StatPrice>
{tokenDetailData.priceLow52W?.value ? formatDollarAmount(tokenDetailData.priceLow52W?.value) : '-'}
</StatPrice>
</Stat>
<Stat>
52W high
<StatPrice>
{tokenDetailData.priceHigh52W?.value ? formatDollarAmount(tokenDetailData.priceHigh52W?.value) : '-'}
</StatPrice>
</Stat>
</StatPair>
</StatsSection>
<AboutSection address={address} tokenDetailData={relevantTokenDetailData} />
<ContractAddressSection>
<Contract>
<Trans>Contract address</Trans>
<ContractAddress>
<CopyContractAddress address={address} />
</ContractAddress>
</Contract>
</ContractAddressSection>
</TopArea>
</Suspense>
)
}

View File

@@ -0,0 +1,81 @@
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

@@ -1,8 +1,9 @@
import { Trans } from '@lingui/macro'
import { useAtom } from 'jotai'
import { Heart } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { SMALL_MEDIA_BREAKPOINT } from '../constants'
import { SMALLEST_MOBILE_MEDIA_BREAKPOINT } from '../constants'
import { showFavoritesAtom } from '../state'
const FavoriteButtonContent = styled.div`
@@ -14,19 +15,20 @@ const FavoriteButtonContent = styled.div`
const StyledFavoriteButton = styled.button<{ active: boolean }>`
padding: 0px 16px;
border-radius: 12px;
background-color: ${({ theme, active }) => (active ? theme.accentAction : theme.backgroundInteractive)};
border: none;
color: ${({ theme }) => theme.textPrimary};
background-color: ${({ theme, active }) => (active ? theme.accentActiveSoft : theme.backgroundInteractive)};
border: ${({ active, theme }) => (active ? `1px solid ${theme.accentActive}` : 'none')};
color: ${({ theme, active }) => (active ? theme.accentActive : theme.textPrimary)};
font-size: 16px;
font-weight: 600;
cursor: pointer;
:hover {
background-color: ${({ theme, active }) => !active && theme.backgroundModule};
opacity: ${({ active }) => (active ? '60%' : '100%')};
}
`
const FavoriteText = styled.span`
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
@media only screen and (max-width: ${SMALLEST_MOBILE_MEDIA_BREAKPOINT}) {
display: none;
}
`
@@ -37,8 +39,10 @@ export default function FavoriteButton() {
return (
<StyledFavoriteButton onClick={() => setShowFavorites(!showFavorites)} active={showFavorites}>
<FavoriteButtonContent>
<Heart size={17} color={theme.textPrimary} fill="transparent" />
<FavoriteText>Favorites</FavoriteText>
<Heart size={17} color={showFavorites ? theme.accentActive : theme.textPrimary} />
<FavoriteText>
<Trans>Favorites</Trans>
</FavoriteText>
</FavoriteButtonContent>
</StyledFavoriteButton>
)

View File

@@ -47,8 +47,8 @@ const MenuTimeFlyout = styled.span`
max-height: 350px;
overflow: auto;
background-color: ${({ theme }) => theme.backgroundSurface};
box-shadow: ${({ theme }) => theme.flyoutDropShadow};
border: 1px solid ${({ theme }) => theme.backgroundOutline};
box-shadow: ${({ theme }) => theme.deepShadow};
border: 0.5px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 12px;
padding: 8px;
display: flex;
@@ -63,9 +63,9 @@ const MenuTimeFlyout = styled.span`
const StyledMenuButton = styled.button<{ open: boolean }>`
width: 100%;
height: 100%;
color: ${({ theme, open }) => (open ? theme.blue200 : theme.textPrimary)};
color: ${({ theme, open }) => (open ? theme.accentActive : theme.textPrimary)};
border: none;
background-color: ${({ theme, open }) => (open ? theme.accentActionSoft : theme.backgroundInteractive)};
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundInteractive)};
margin: 0;
padding: 6px 12px 6px 12px;
border-radius: 12px;
@@ -76,10 +76,13 @@ const StyledMenuButton = styled.button<{ open: boolean }>`
:hover {
cursor: pointer;
outline: none;
background-color: ${({ theme, open }) => (open ? theme.accentActionSoft : theme.backgroundModule)};
border: none;
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundModule)};
}
:focus {
background-color: ${({ theme, open }) => (open ? theme.accentActionSoft : theme.backgroundInteractive)};
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundInteractive)};
border: none;
outline: none;
}
svg {
margin-top: 2px;
@@ -112,7 +115,7 @@ const StyledMenuContent = styled.div`
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;

View File

@@ -1,7 +1,7 @@
import { Trans } from '@lingui/macro'
import searchIcon from 'assets/svg/search.svg'
import xIcon from 'assets/svg/x.svg'
import { useAtom } from 'jotai'
import { useState } from 'react'
import styled from 'styled-components/macro'
import { MEDIUM_MEDIA_BREAKPOINT } from '../constants'
@@ -12,33 +12,32 @@ const SearchBarContainer = styled.div`
display: flex;
flex: 1;
`
const SearchInput = styled.input<{ expanded: boolean }>`
const SearchInput = styled.input`
background: no-repeat scroll 7px 7px;
background-image: ${({ expanded }) => !expanded && `url(${searchIcon})`};
background-image: url(${searchIcon});
background-size: 20px 20px;
background-position: 14px center;
background-color: 'transparent';
background-position: 12px center;
background-color: ${({ theme }) => theme.backgroundModule};
border-radius: 12px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
height: 100%;
width: ${({ expanded }) => (expanded ? '100%' : '52px')};
width: min(200px, 100%);
font-size: 16px;
padding-left: 35px;
color: ${({ theme }) => theme.textPrimary};
transition: width 0.75s cubic-bezier(0, 0.795, 0, 1);
padding-left: 40px;
color: ${({ theme }) => theme.textSecondary};
:hover {
cursor: ${({ expanded }) => !expanded && 'pointer'};
background-color: ${({ theme }) => theme.backgroundModule};
background-color: ${({ theme }) => theme.backgroundSurface};
}
:focus {
outline: none;
background-color: ${({ theme }) => theme.accentActionSoft};
border: none;
background-color: ${({ theme }) => theme.backgroundSurface};
border-color: ${({ theme }) => theme.accentActionSoft};
}
::placeholder {
color: ${({ expanded, theme }) => (expanded ? theme.textTertiary : 'transparent')};
color: ${({ theme }) => theme.textTertiary};
}
::-webkit-search-cancel-button {
-webkit-appearance: none;
@@ -58,20 +57,22 @@ const SearchInput = styled.input<{ expanded: boolean }>`
export default function SearchBar() {
const [filterString, setFilterString] = useAtom(filterStringAtom)
const [isExpanded, setExpanded] = useState(false)
return (
<SearchBarContainer>
<SearchInput
expanded={isExpanded}
type="search"
placeholder="Search by name or token address"
id="searchBar"
onBlur={() => isExpanded && filterString.length === 0 && setExpanded(false)}
onFocus={() => setExpanded(true)}
autoComplete="off"
value={filterString}
onChange={({ target: { value } }) => setFilterString(value)}
/>
<Trans
render={({ translation }) => (
<SearchInput
type="search"
placeholder={`${translation}`}
id="searchBar"
autoComplete="off"
value={filterString}
onChange={({ target: { value } }) => setFilterString(value)}
/>
)}
>
Filter tokens
</Trans>
</SearchBarContainer>
)
}

View File

@@ -1,4 +1,4 @@
import { TimePeriod } from 'hooks/useExplorePageQuery'
import { TimePeriod } from 'graphql/data/TopTokenQuery'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useAtom } from 'jotai'
import { useRef } from 'react'
@@ -10,15 +10,23 @@ import styled, { useTheme } from 'styled-components/macro'
import { MOBILE_MEDIA_BREAKPOINT, SMALL_MEDIA_BREAKPOINT } from '../constants'
import { filterTimeAtom } from '../state'
export const TIME_DISPLAYS: { [key: string]: string } = {
hour: '1H',
day: '1D',
week: '1W',
month: '1M',
year: '1Y',
export const DISPLAYS: Record<TimePeriod, string> = {
[TimePeriod.HOUR]: '1H',
[TimePeriod.DAY]: '1D',
[TimePeriod.WEEK]: '1W',
[TimePeriod.MONTH]: '1M',
[TimePeriod.YEAR]: '1Y',
[TimePeriod.ALL]: 'All',
}
const TIMES = [TimePeriod.hour, TimePeriod.day, TimePeriod.week, TimePeriod.month, TimePeriod.year]
export const ORDERED_TIMES = [
TimePeriod.HOUR,
TimePeriod.DAY,
TimePeriod.WEEK,
TimePeriod.MONTH,
TimePeriod.YEAR,
TimePeriod.ALL,
]
const InternalMenuItem = styled.div`
flex: 1;
@@ -51,8 +59,8 @@ const MenuTimeFlyout = styled.span`
max-height: 300px;
overflow: auto;
background-color: ${({ theme }) => theme.backgroundSurface};
box-shadow: ${({ theme }) => theme.flyoutDropShadow};
border: 1px solid ${({ theme }) => theme.backgroundOutline};
box-shadow: ${({ theme }) => theme.deepShadow};
border: 0.5px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 12px;
padding: 8px;
display: flex;
@@ -73,9 +81,9 @@ const StyledMenuButton = styled.button<{ open: boolean }>`
width: 100%;
height: 100%;
border: none;
color: ${({ theme, open }) => (open ? theme.blue200 : theme.textPrimary)};
color: ${({ theme, open }) => (open ? theme.accentActive : theme.textPrimary)};
margin: 0;
background-color: ${({ theme, open }) => (open ? theme.accentActionSoft : theme.backgroundInteractive)};
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundInteractive)};
padding: 6px 12px 6px 12px;
border-radius: 12px;
font-size: 16px;
@@ -84,11 +92,14 @@ const StyledMenuButton = styled.button<{ open: boolean }>`
:hover {
cursor: pointer;
border: none;
outline: none;
background-color: ${({ theme, open }) => (open ? theme.accentActionSoft : theme.backgroundModule)};
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundModule)};
}
:focus {
background-color: ${({ theme, open }) => (open ? theme.accentActionSoft : theme.backgroundInteractive)};
background-color: ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundInteractive)};
border: none;
outline: none;
}
svg {
margin-top: 2px;
@@ -120,7 +131,7 @@ const StyledMenuContent = styled.div`
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
@@ -136,7 +147,7 @@ export default function TimeSelector() {
<StyledMenu ref={node}>
<StyledMenuButton onClick={toggleMenu} aria-label={`timeSelector`} open={open}>
<StyledMenuContent>
{TIME_DISPLAYS[activeTime]}
{DISPLAYS[activeTime]}
<Chevron open={open}>
{open ? <ChevronUp size={15} viewBox="0 0 24 20" /> : <ChevronDown size={15} viewBox="0 0 24 20" />}
</Chevron>
@@ -144,15 +155,15 @@ export default function TimeSelector() {
</StyledMenuButton>
{open && (
<MenuTimeFlyout>
{TIMES.map((time) => (
{ORDERED_TIMES.map((time) => (
<InternalLinkMenuItem
key={time}
key={DISPLAYS[time]}
onClick={() => {
setTime(time)
toggleMenu()
}}
>
<div>{TIME_DISPLAYS[time]}</div>
<div>{DISPLAYS[time]}</div>
{time === activeTime && <Check color={theme.accentAction} size={16} />}
</InternalLinkMenuItem>
))}

View File

@@ -5,15 +5,15 @@ import { EventName } from 'components/AmplitudeAnalytics/constants'
import SparklineChart from 'components/Charts/SparklineChart'
import CurrencyLogo from 'components/CurrencyLogo'
import { getChainInfo } from 'constants/chainInfo'
import { useCurrency, useToken } from 'hooks/Tokens'
import { TimePeriod, TokenData } from 'hooks/useExplorePageQuery'
import { useAtom } from 'jotai'
import { TimePeriod, TokenData } from 'graphql/data/TopTokenQuery'
import { useCurrency } from 'hooks/Tokens'
import { useAtomValue } from 'jotai/utils'
import { ReactNode } from 'react'
import { ArrowDown, ArrowDownRight, ArrowUp, ArrowUpRight, Heart } from 'react-feather'
import { ArrowDown, ArrowUp, Heart } from 'react-feather'
import { Link } from 'react-router-dom'
import styled, { useTheme } from 'styled-components/macro'
import { formatAmount, formatDollarAmount } from 'utils/formatDollarAmt'
import styled, { css, useTheme } from 'styled-components/macro'
import { ClickableStyle } from 'theme'
import { formatDollarAmount } from 'utils/formatDollarAmt'
import {
LARGE_MEDIA_BREAKPOINT,
@@ -23,64 +23,76 @@ import {
} from '../constants'
import { LoadingBubble } from '../loading'
import {
favoritesAtom,
filterNetworkAtom,
filterStringAtom,
filterTimeAtom,
sortCategoryAtom,
sortDirectionAtom,
useIsFavorited,
useSetSortCategory,
useToggleFavorite,
} from '../state'
import { formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart'
import { Category, SortDirection } from '../types'
import { TIME_DISPLAYS } from './TimeSelector'
import { DISPLAYS } from './TimeSelector'
const ArrowCell = styled.div`
padding-left: 2px;
display: flex;
`
const Cell = styled.div`
display: flex;
align-items: center;
justify-content: center;
`
const StyledTokenRow = styled.div`
width: 100%;
height: 60px;
const StyledTokenRow = styled.div<{ first?: boolean; last?: boolean; loading?: boolean }>`
background-color: transparent;
display: grid;
grid-template-columns: 1.2fr 1fr 7fr 4fr 4fr 4fr 4fr 5fr;
font-size: 15px;
grid-template-columns: 1fr 7fr 4fr 4fr 4fr 4fr 5fr 1.2fr;
height: 60px;
line-height: 24px;
max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT};
min-width: 390px;
padding: 0px 12px;
padding-top: ${({ first }) => (first ? '4px' : '0px')};
padding-bottom: ${({ last }) => (last ? '4px' : '0px')};
padding-left: 12px;
padding-right: 12px;
transition: ${({
theme: {
transition: { duration, timing },
},
}) => css`background-color ${duration.medium} ${timing.ease}`};
width: 100%;
&:hover {
background-color: ${({ theme }) => theme.accentActionSoft};
${({ loading, theme }) =>
!loading &&
css`
background-color: ${theme.hoverDefault};
`}
${({ last }) =>
last &&
css`
border-radius: 0px 0px 8px 8px;
`}
}
@media only screen and (max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT}) {
grid-template-columns: 1.7fr 1fr 6.5fr 4.5fr 4.5fr 4.5fr 4.5fr;
grid-template-columns: 1fr 6.5fr 4.5fr 4.5fr 4.5fr 4.5fr 1.7fr;
width: fit-content;
padding-right: 24px;
}
@media only screen and (max-width: ${LARGE_MEDIA_BREAKPOINT}) {
grid-template-columns: 1.7fr 1fr 7.5fr 4.5fr 4.5fr 4.5fr;
grid-template-columns: 1fr 7.5fr 4.5fr 4.5fr 4.5fr 1.7fr;
width: fit-content;
}
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
grid-template-columns: 1.2fr 1fr 10fr 5fr 3fr;
grid-template-columns: 1fr 10fr 5fr 5fr 1.2fr;
width: fit-content;
}
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
grid-template-columns: 4fr 2fr;
grid-template-columns: 2fr 3fr;
min-width: unset;
border-bottom: 0.5px solid ${({ theme }) => theme.backgroundModule};
padding: 0px 12px;
:last-of-type {
border-bottom: none;
@@ -93,10 +105,18 @@ export const ClickFavorited = styled.span`
cursor: pointer;
&:hover {
color: ${({ theme }) => theme.textPrimary};
opacity: 60%;
}
`
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;
@@ -127,23 +147,21 @@ const StyledHeaderRow = styled(StyledTokenRow)`
line-height: 16px;
padding: 0px 12px;
width: 100%;
justify-content: center;
&:hover {
background-color: 'transparent';
}
@media only screen and (max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT}) {
padding-right: 24px;
background-color: transparent;
}
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
justify-content: space-between;
padding: 0px 12px;
}
`
const ListNumberCell = styled(Cell)`
const ListNumberCell = styled(Cell)<{ header: boolean }>`
color: ${({ theme }) => theme.textSecondary};
min-width: 32px;
height: ${({ header }) => (header ? '48px' : '60px')};
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
display: none;
@@ -154,10 +172,11 @@ const DataCell = styled(Cell)<{ sortable: boolean }>`
min-width: 80px;
user-select: ${({ sortable }) => (sortable ? 'none' : 'unset')};
&:hover {
color: ${({ theme, sortable }) => sortable && theme.white};
background-color: ${({ theme, sortable }) => sortable && theme.accentActionSoft};
}
transition: ${({
theme: {
transition: { duration, timing },
},
}) => css`background-color ${duration.medium} ${timing.ease}`};
`
const MarketCapCell = styled(DataCell)`
padding-right: 8px;
@@ -175,6 +194,7 @@ const PriceCell = styled(DataCell)`
padding-right: 8px;
`
const PercentChangeCell = styled(DataCell)`
padding-right: 8px;
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
display: none;
}
@@ -209,6 +229,10 @@ const HeaderCellWrapper = styled.span<{ onClick?: () => void }>`
height: 100%;
justify-content: flex-end;
width: 100%;
&:hover {
opacity: 60%;
}
`
const SparkLineCell = styled(Cell)`
padding: 0px 24px;
@@ -250,6 +274,7 @@ const TokenName = styled.div`
`
const TokenSymbol = styled(Cell)`
color: ${({ theme }) => theme.textTertiary};
text-transform: uppercase;
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
font-size: 12px;
@@ -299,8 +324,8 @@ const LogoContainer = styled.div`
`
/* formatting for volume with timeframe header display */
function getHeaderDisplay(category: string, timeframe: string): string {
if (category === Category.volume) return `${TIME_DISPLAYS[timeframe]} ${category}`
function getHeaderDisplay(category: string, timeframe: TimePeriod): string {
if (category === Category.volume || category === Category.percentChange) return `${DISPLAYS[timeframe]} ${category}`
return category
}
@@ -347,9 +372,8 @@ function HeaderCell({
/* Token Row: skeleton row component */
export function TokenRow({
address,
header,
favorited,
header,
listNumber,
tokenInfo,
price,
@@ -357,39 +381,41 @@ export function TokenRow({
marketCap,
volume,
sparkLine,
...rest
}: {
address: ReactNode
header: boolean
favorited: ReactNode
first?: boolean
header: boolean
listNumber: ReactNode
tokenInfo: ReactNode
loading?: boolean
marketCap: ReactNode
price: ReactNode
percentChange: ReactNode
marketCap: ReactNode
volume: ReactNode
sparkLine: ReactNode
tokenInfo: ReactNode
volume: ReactNode
last?: boolean
}) {
const rowCells = (
<>
<FavoriteCell>{favorited}</FavoriteCell>
<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>
</>
)
if (header) return <StyledHeaderRow>{rowCells}</StyledHeaderRow>
return <StyledTokenRow>{rowCells}</StyledTokenRow>
return <StyledTokenRow {...rest}>{rowCells}</StyledTokenRow>
}
/* Header Row: top header row component for table */
export function HeaderRow() {
return (
<TokenRow
address={null}
header={true}
favorited={null}
listNumber="#"
@@ -407,10 +433,10 @@ export function HeaderRow() {
export function LoadingRow() {
return (
<TokenRow
address={null}
header={false}
favorited={null}
header={false}
listNumber={<SmallLoadingBubble />}
loading
tokenInfo={
<>
<IconLoadingBubble />
@@ -431,54 +457,37 @@ export default function LoadedRow({
tokenAddress,
tokenListIndex,
tokenListLength,
data,
tokenData,
timePeriod,
}: {
tokenAddress: string
tokenListIndex: number
tokenListLength: number
data: TokenData
tokenData: TokenData
timePeriod: TimePeriod
}) {
const token = useToken(tokenAddress)
const currency = useCurrency(tokenAddress)
const tokenName = token?.name ?? ''
const tokenSymbol = token?.symbol ?? ''
const tokenData = data[tokenAddress]
const theme = useTheme()
const [favoriteTokens] = useAtom(favoritesAtom)
const isFavorited = favoriteTokens.includes(tokenAddress)
const tokenName = tokenData.name
const tokenSymbol = tokenData.symbol
const isFavorited = useIsFavorited(tokenAddress)
const toggleFavorite = useToggleFavorite(tokenAddress)
const isPositive = Math.sign(tokenData.delta) > 0
const filterString = useAtomValue(filterStringAtom)
const filterNetwork = useAtomValue(filterNetworkAtom)
const filterTime = useAtomValue(filterTimeAtom) // filter time period for top tokens table
const L2Icon = getChainInfo(filterNetwork).circleLogoUrl
const tokenPercentChangeInfo = (
<>
{tokenData.delta}%
<ArrowCell>
{isPositive ? (
<ArrowUpRight size={16} color={theme.accentSuccess} />
) : (
<ArrowDownRight size={16} color={theme.accentFailure} />
)}
</ArrowCell>
</>
)
const delta = tokenData.percentChange?.[timePeriod]?.value
const arrow = delta ? getDeltaArrow(delta) : null
const formattedDelta = delta ? formatDelta(delta) : null
const exploreTokenSelectedEventProperties = {
chain_id: filterNetwork,
token_address: tokenAddress,
token_symbol: token?.symbol,
token_symbol: tokenSymbol,
token_list_index: tokenListIndex,
token_list_length: tokenListLength,
time_frame: filterTime,
time_frame: timePeriod,
search_token_address_input: filterString,
}
const heartColor = isFavorited ? theme.accentActive : undefined
// TODO: currency logo sizing mobile (32px) vs. desktop (24px)
return (
<StyledLink
@@ -486,7 +495,6 @@ export default function LoadedRow({
onClick={() => sendAnalyticsEvent(EventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)}
>
<TokenRow
address={tokenAddress}
header={false}
favorited={
<ClickFavorited
@@ -495,14 +503,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>
@@ -514,19 +522,39 @@ export default function LoadedRow({
price={
<ClickableContent>
<PriceInfoCell>
{formatDollarAmount(tokenData.price)}
<PercentChangeInfoCell>{tokenPercentChangeInfo}</PercentChangeInfoCell>
{tokenData.price?.value ? formatDollarAmount(tokenData.price?.value) : '-'}
<PercentChangeInfoCell>
{formattedDelta}
{arrow}
</PercentChangeInfoCell>
</PriceInfoCell>
</ClickableContent>
}
percentChange={<ClickableContent>{tokenPercentChangeInfo}</ClickableContent>}
marketCap={<ClickableContent>{formatAmount(tokenData.marketCap).toUpperCase()}</ClickableContent>}
volume={<ClickableContent>{formatAmount(tokenData.volume[timePeriod]).toUpperCase()}</ClickableContent>}
percentChange={
<ClickableContent>
{formattedDelta}
{arrow}
</ClickableContent>
}
marketCap={
<ClickableContent>
{tokenData.marketCap?.value ? formatDollarAmount(tokenData.marketCap?.value) : '-'}
</ClickableContent>
}
volume={
<ClickableContent>
{tokenData.volume?.[timePeriod]?.value
? formatDollarAmount(tokenData.volume?.[timePeriod]?.value ?? undefined)
: '-'}
</ClickableContent>
}
sparkLine={
<SparkLine>
<ParentSize>{({ width, height }) => <SparklineChart width={width} height={height} />}</ParentSize>
</SparkLine>
}
first={tokenListIndex === 0}
last={tokenListIndex === tokenListLength - 1}
/>
</StyledLink>
)

View File

@@ -1,3 +1,4 @@
import { Trans } from '@lingui/macro'
import {
favoritesAtom,
filterStringAtom,
@@ -6,10 +7,9 @@ import {
sortCategoryAtom,
sortDirectionAtom,
} from 'components/Tokens/state'
import { useAllTokens } from 'hooks/Tokens'
import { TimePeriod, TokenData } from 'hooks/useExplorePageQuery'
import { TimePeriod, TokenData } from 'graphql/data/TopTokenQuery'
import { useAtomValue } from 'jotai/utils'
import { ReactNode, useCallback, useMemo } from 'react'
import { ReactNode, Suspense, useCallback, useMemo } from 'react'
import { AlertTriangle } from 'react-feather'
import styled from 'styled-components/macro'
@@ -21,7 +21,7 @@ const GridContainer = styled.div`
display: flex;
flex-direction: column;
max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT};
background-color: ${({ theme }) => theme.backgroundModule};
background-color: ${({ theme }) => theme.backgroundSurface};
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
0px 24px 32px rgba(0, 0, 0, 0.01);
margin-left: auto;
@@ -44,38 +44,36 @@ const NoTokenDisplay = styled.div`
gap: 8px;
`
const TokenRowsContainer = styled.div`
padding: 4px 0px;
width: 100%;
`
function useFilteredTokens(addresses: string[]) {
function useFilteredTokens(tokens: TokenData[] | undefined) {
const filterString = useAtomValue(filterStringAtom)
const favoriteTokens = useAtomValue(favoritesAtom)
const favoriteTokenAddresses = useAtomValue(favoritesAtom)
const showFavorites = useAtomValue(showFavoritesAtom)
const shownTokens = showFavorites ? favoriteTokens : addresses
const allTokens = useAllTokens()
const shownTokens =
showFavorites && tokens ? tokens.filter((token) => favoriteTokenAddresses.includes(token.address)) : tokens
return useMemo(
() =>
shownTokens.filter((tokenAddress) => {
const token = allTokens[tokenAddress]
const tokenName = token?.name ?? ''
const tokenSymbol = token?.symbol ?? ''
(shownTokens ?? []).filter((token) => {
if (!token.address) {
return false
}
if (!filterString) {
return true
}
const lowercaseFilterString = filterString.toLowerCase()
const addressIncludesFilterString = tokenAddress.toLowerCase().includes(lowercaseFilterString)
const nameIncludesFilterString = tokenName.toLowerCase().includes(lowercaseFilterString)
const symbolIncludesFilterString = tokenSymbol.toLowerCase().includes(lowercaseFilterString)
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
}),
[allTokens, shownTokens, filterString]
[shownTokens, filterString]
)
}
function useSortedTokens(addresses: string[], tokenData: TokenData | null) {
function useSortedTokens(tokenData: TokenData[] | null) {
const sortCategory = useAtomValue(sortCategoryAtom)
const sortDirection = useAtomValue(sortDirectionAtom)
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
@@ -94,39 +92,38 @@ function useSortedTokens(addresses: string[], tokenData: TokenData | null) {
return useMemo(
() =>
addresses.sort((token1Address, token2Address) => {
tokenData &&
tokenData.sort((token1, token2) => {
if (!tokenData) {
return 0
}
const token1 = tokenData[token1Address] as any
const token2 = tokenData[token2Address] as any
// fix delta/percent change property
if (!token1 || !token2 || !sortDirection || !sortCategory) {
return 0
}
let a: number
let b: number
let a: number | null | undefined
let b: number | null | undefined
switch (sortCategory) {
case Category.marketCap:
a = token1.marketCap
b = token2.marketCap
break
case Category.percentChange:
a = token1.delta
b = token2.delta
a = token1.marketCap?.value
b = token2.marketCap?.value
break
case Category.price:
a = token1.price
b = token2.price
a = token1.price?.value
b = token2.price?.value
break
case Category.volume:
a = token1.volume[timePeriod]
b = token2.volume[timePeriod]
a = token1.volume?.[timePeriod]?.value
b = token2.volume?.[timePeriod]?.value
break
case Category.percentChange:
a = token1.percentChange?.[timePeriod]?.value
b = token2.percentChange?.[timePeriod]?.value
break
}
return sortFn(a, b)
}),
[addresses, tokenData, sortDirection, sortCategory, sortFn, timePeriod]
[tokenData, sortDirection, sortCategory, sortFn, timePeriod]
)
}
@@ -143,7 +140,7 @@ const LOADING_ROWS = Array.from({ length: 100 })
.fill(0)
.map((_item, index) => <LoadingRow key={index} />)
function LoadingTokenTable() {
export function LoadingTokenTable() {
return (
<GridContainer>
<HeaderRow />
@@ -152,58 +149,51 @@ function LoadingTokenTable() {
)
}
interface TokenTableProps {
data: TokenData | null
error: string | null
loading: boolean
}
export default function TokenTable({ data, error, loading }: TokenTableProps) {
export default function TokenTable({ data }: { data: TokenData[] | undefined }) {
const showFavorites = useAtomValue<boolean>(showFavoritesAtom)
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
const topTokenAddresses = data ? Object.keys(data) : []
const filteredTokens = useFilteredTokens(topTokenAddresses)
const filteredAndSortedTokens = useSortedTokens(filteredTokens, data)
const filteredTokens = useFilteredTokens(data)
const sortedFilteredTokens = useSortedTokens(filteredTokens)
/* loading and error state */
if (loading) {
return <LoadingTokenTable />
} else if (error || data === null) {
if (data === null) {
return (
<NoTokensState
message={
<>
<AlertTriangle size={16} />
An error occured loading tokens. Please try again.
<Trans>An error occured loading tokens. Please try again.</Trans>
</>
}
/>
)
}
if (showFavorites && filteredAndSortedTokens.length === 0) {
return <NoTokensState message="You have no favorited tokens" />
if (showFavorites && sortedFilteredTokens?.length === 0) {
return <NoTokensState message={<Trans>You have no favorited tokens</Trans>} />
}
if (!showFavorites && filteredAndSortedTokens.length === 0) {
return <NoTokensState message="No tokens found" />
if (!showFavorites && sortedFilteredTokens?.length === 0) {
return <NoTokensState message={<Trans>No tokens found</Trans>} />
}
return (
<GridContainer>
<HeaderRow />
<TokenRowsContainer>
{filteredAndSortedTokens.map((tokenAddress, index) => (
<LoadedRow
key={tokenAddress}
tokenAddress={tokenAddress}
tokenListIndex={index}
tokenListLength={filteredAndSortedTokens.length}
data={data}
timePeriod={timePeriod}
/>
))}
</TokenRowsContainer>
</GridContainer>
<Suspense fallback={<LoadingTokenTable />}>
<GridContainer>
<HeaderRow />
<TokenRowsContainer>
{sortedFilteredTokens?.map((token, index) => (
<LoadedRow
key={token.address}
tokenAddress={token.address}
tokenListIndex={index}
tokenListLength={sortedFilteredTokens.length}
tokenData={token}
timePeriod={timePeriod}
/>
))}
</TokenRowsContainer>
</GridContainer>
</Suspense>
)
}

View File

@@ -0,0 +1,80 @@
import { X } from 'react-feather'
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 tokensPromoDark from '../../assets/images/tokensPromoDark.png'
import tokensPromoLight from '../../assets/images/tokensPromoLight.png'
const PopupContainer = styled.div<{ show: boolean }>`
position: absolute;
display: ${({ show }) => (show ? 'flex' : 'none')};
flex-direction: column;
padding: 12px 16px 12px 20px;
gap: 8px;
bottom: 48px;
right: 16px;
width: 320px;
height: 88px;
z-index: 5;
background-color: ${({ theme }) => (theme.darkMode ? theme.backgroundScrim : opacify(60, '#FDF0F8'))};
color: ${({ theme }) => theme.textPrimary};
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 12px;
box-shadow: ${({ theme }) => theme.deepShadow};
background-image: url(${({ theme }) => (theme.darkMode ? `${tokensPromoDark}` : `${tokensPromoLight}`)});
background-size: cover;
background-blend-mode: overlay;
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.slow}ms opacity ${timing.in}`};
`
const Header = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
`
const HeaderText = styled(Link)`
font-weight: 600;
font-size: 14px;
line-height: 20px;
text-decoration: none;
color: ${({ theme }) => theme.textPrimary};
`
const Description = styled(Link)`
font-weight: 400;
font-size: 12px;
line-height: 16px;
width: 75%;
text-decoration: none;
color: ${({ theme }) => theme.textPrimary};
`
export default function TokensBanner() {
const theme = useTheme()
const [showTokensPromoBanner, setShowTokensPromoBanner] = useShowTokensPromoBanner()
const closeBanner = () => {
setShowTokensPromoBanner(false)
}
return (
<PopupContainer show={showTokensPromoBanner}>
<Header>
<HeaderText to={'/tokens'} onClick={closeBanner}>
Explore Top Tokens
</HeaderText>
<X size={20} color={theme.textSecondary} onClick={closeBanner} style={{ cursor: 'pointer' }} />
</Header>
<Description to={'/tokens'} onClick={closeBanner}>
Check out the new explore tab to discover and learn more
</Description>
</PopupContainer>
)
}

View File

@@ -1,8 +1,8 @@
import { SupportedChainId } from 'constants/chains'
import { TimePeriod } from 'hooks/useExplorePageQuery'
import { TimePeriod } from 'graphql/data/TopTokenQuery'
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'
@@ -10,7 +10,7 @@ export const favoritesAtom = atomWithStorage<string[]>('favorites', [])
export const showFavoritesAtom = atomWithStorage<boolean>('showFavorites', false)
export const filterStringAtom = atomWithReset<string>('')
export const filterNetworkAtom = atom<SupportedChainId>(SupportedChainId.MAINNET)
export const filterTimeAtom = atom<TimePeriod>(TimePeriod.day)
export const filterTimeAtom = atom<TimePeriod>(TimePeriod.DAY)
export const sortCategoryAtom = atom<Category>(Category.marketCap)
export const sortDirectionAtom = atom<SortDirection>(SortDirection.decreasing)
@@ -20,12 +20,12 @@ export function useToggleFavorite(tokenAddress: string) {
return useCallback(() => {
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 +47,9 @@ export function useSetSortCategory(category: Category) {
}
}, [category, sortCategory, setSortCategory, sortDirection, setDirectionCategory])
}
export function useIsFavorited(tokenAddress: string) {
const favoritedTokens = useAtomValue<string[]>(favoritesAtom)
return useMemo(() => favoritedTokens.includes(tokenAddress.toLocaleLowerCase()), [favoritedTokens, tokenAddress])
}

View File

@@ -1,5 +1,5 @@
export enum Category {
percentChange = '% Change',
percentChange = 'Change',
marketCap = 'Market Cap',
price = 'Price',
volume = 'Volume',

View File

@@ -4,6 +4,7 @@ import { useWeb3React } from '@web3-react/core'
import Badge from 'components/Badge'
import { getChainInfo } from 'constants/chainInfo'
import { SupportedL2ChainId } from 'constants/chains'
import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import { ReactNode, useCallback, useState } from 'react'
import { AlertCircle, AlertTriangle, ArrowUpCircle, CheckCircle } from 'react-feather'
@@ -13,7 +14,7 @@ import styled, { useTheme } from 'styled-components/macro'
import { isL2ChainId } from 'utils/chains'
import Circle from '../../assets/images/blue-loader.svg'
import { ExternalLink } from '../../theme'
import { ExternalLink, ThemedText } from '../../theme'
import { CloseIcon, CustomLightSpinner } from '../../theme'
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
import { TransactionSummary } from '../AccountDetails/TransactionSummary'
@@ -23,7 +24,9 @@ import Modal from '../Modal'
import { RowBetween, RowFixed } from '../Row'
import AnimatedConfirmation from './AnimatedConfirmation'
const Wrapper = styled.div`
const Wrapper = styled.div<{ redesignFlag?: boolean }>`
background-color: ${({ redesignFlag, theme }) => redesignFlag && theme.backgroundSurface};
outline: ${({ redesignFlag, theme }) => redesignFlag && `1px solid ${theme.backgroundOutline}`};
width: 100%;
padding: 1rem;
`
@@ -34,6 +37,7 @@ const Section = styled(AutoColumn)<{ inline?: boolean }>`
const BottomSection = styled(Section)`
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
padding-bottom: 10px;
`
const ConfirmedIcon = styled(ColumnCenter)<{ inline?: boolean }>`
@@ -55,7 +59,36 @@ function ConfirmationPendingContent({
pendingText: ReactNode
inline?: boolean // not in modal
}) {
return (
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
const theme = useTheme()
return redesignFlagEnabled ? (
<Wrapper>
<AutoColumn gap="md">
{!inline && (
<RowBetween>
<div />
<CloseIcon onClick={onDismiss} />
</RowBetween>
)}
<ConfirmedIcon inline={inline}>
<CustomLightSpinner src={Circle} alt="loader" size={inline ? '40px' : '90px'} />
</ConfirmedIcon>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={500} fontSize={20} color={theme.textPrimary} textAlign="center">
<Trans>Waiting for confirmation</Trans>
</Text>
<Text fontWeight={600} fontSize={16} color={theme.textPrimary} textAlign="center">
{pendingText}
</Text>
<Text fontWeight={400} fontSize={12} color={theme.textSecondary} textAlign="center" marginBottom="12px">
<Trans>Confirm this transaction in your wallet</Trans>
</Text>
</AutoColumn>
</AutoColumn>
</Wrapper>
) : (
<Wrapper>
<AutoColumn gap="md">
{!inline && (
@@ -102,6 +135,9 @@ function TransactionSubmittedContent({
const token = currencyToAdd?.wrapped
const logoURL = useCurrencyLogoURIs(token)[0]
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
const [success, setSuccess] = useState<boolean | undefined>()
const addToken = useCallback(() => {
@@ -117,7 +153,52 @@ function TransactionSubmittedContent({
.catch(() => setSuccess(false))
}, [connector, logoURL, token])
return (
return redesignFlagEnabled ? (
<Wrapper>
<Section inline={inline}>
{!inline && (
<RowBetween>
<div />
<CloseIcon onClick={onDismiss} />
</RowBetween>
)}
<ConfirmedIcon inline={inline}>
<ArrowUpCircle strokeWidth={1} size={inline ? '40px' : '75px'} color={theme.accentActive} />
</ConfirmedIcon>
<AutoColumn gap="12px" justify={'center'} style={{ paddingBottom: '12px' }}>
<ThemedText.MediumHeader textAlign="center">
<Trans>Transaction submitted</Trans>
</ThemedText.MediumHeader>
{currencyToAdd && connector.watchAsset && (
<ButtonLight mt="12px" padding="6px 12px" width="fit-content" onClick={addToken}>
{!success ? (
<RowFixed>
<Trans>Add {currencyToAdd.symbol}</Trans>
</RowFixed>
) : (
<RowFixed>
<Trans>Added {currencyToAdd.symbol} </Trans>
<CheckCircle size={'16px'} stroke={theme.deprecated_green1} style={{ marginLeft: '6px' }} />
</RowFixed>
)}
</ButtonLight>
)}
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }}>
<Text fontWeight={600} fontSize={20} color={theme.accentTextLightPrimary}>
{inline ? <Trans>Return</Trans> : <Trans>Close</Trans>}
</Text>
</ButtonPrimary>
{chainId && hash && (
<ExternalLink href={getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)}>
<Text fontWeight={600} fontSize={14} color={theme.accentAction}>
<Trans>View on Etherscan</Trans>
</Text>
</ExternalLink>
)}
</AutoColumn>
</Section>
</Wrapper>
) : (
<Wrapper>
<Section inline={inline}>
{!inline && (
@@ -193,8 +274,30 @@ export function ConfirmationModalContent({
}
export function TransactionErrorContent({ message, onDismiss }: { message: ReactNode; onDismiss: () => void }) {
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
const theme = useTheme()
return (
return redesignFlagEnabled ? (
<Wrapper redesignFlag={true}>
<Section>
<RowBetween>
<Text fontWeight={600} fontSize={16}>
<Trans>Error</Trans>
</Text>
<CloseIcon onClick={onDismiss} redesignFlag={true} />
</RowBetween>
<AutoColumn style={{ marginTop: 20, padding: '2rem 0' }} gap="24px" justify="center">
<AlertTriangle color={theme.accentCritical} style={{ strokeWidth: 1 }} size={90} />
<ThemedText.MediumHeader textAlign="center">{message}</ThemedText.MediumHeader>
</AutoColumn>
</Section>
<BottomSection gap="12px">
<ButtonPrimary onClick={onDismiss} redesignFlag={true}>
<Trans>Dismiss</Trans>
</ButtonPrimary>
</BottomSection>
</Wrapper>
) : (
<Wrapper>
<Section>
<RowBetween>
@@ -344,12 +447,14 @@ export default function TransactionConfirmationModal({
currencyToAdd,
}: ConfirmationModalProps) {
const { chainId } = useWeb3React()
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
if (!chainId) return null
// confirmation screen
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90} redesignFlag={redesignFlagEnabled}>
{isL2ChainId(chainId) && (hash || attemptingTxn) ? (
<L2Content chainId={chainId} hash={hash} onDismiss={onDismiss} pendingText={pendingText} />
) : attemptingTxn ? (

View File

@@ -7,7 +7,7 @@ import { RedesignVariant, useRedesignFlag } from 'featureFlags/flags/redesign'
import ms from 'ms.macro'
import { darken } from 'polished'
import { useState } from 'react'
import { useSetUserSlippageTolerance, useUserSlippageTolerance, useUserTransactionTTL } from 'state/user/hooks'
import { useUserSlippageTolerance, useUserTransactionTTL } from 'state/user/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from '../../theme'
@@ -64,6 +64,10 @@ const Input = styled.input<{ redesignFlag: boolean }>`
}
color: ${({ theme, color }) => (color === 'red' ? theme.deprecated_red1 : theme.deprecated_text1)};
text-align: right;
::placeholder {
color: ${({ theme, redesignFlag }) => redesignFlag && theme.textTertiary};
}
`
const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean; redesignFlag: boolean }>`
@@ -91,7 +95,7 @@ const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean;
const SlippageEmojiContainer = styled.span`
color: #f3841e;
${({ theme }) => theme.mediaWidth.upToSmall`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
display: none;
`}
`
@@ -108,8 +112,7 @@ export default function TransactionSettings({ placeholderSlippage }: Transaction
const redesignFlag = useRedesignFlag()
const redesignFlagEnabled = redesignFlag === RedesignVariant.Enabled
const userSlippageTolerance = useUserSlippageTolerance()
const setUserSlippageTolerance = useSetUserSlippageTolerance()
const [userSlippageTolerance, setUserSlippageTolerance] = useUserSlippageTolerance()
const [deadline, setDeadline] = useUserTransactionTTL()

View File

@@ -0,0 +1,155 @@
import { Trans } from '@lingui/macro'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { getConnection } from 'connection/utils'
import { getChainInfoOrDefault } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import useCopyClipboard from 'hooks/useCopyClipboard'
import useStablecoinPrice from 'hooks/useStablecoinPrice'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useCallback, useMemo } from 'react'
import { Copy, ExternalLink, Power } from 'react-feather'
import { Text } from 'rebass'
import { useCurrencyBalanceString } from 'state/connection/hooks'
import { useAppDispatch } from 'state/hooks'
import { updateSelectedWallet } from 'state/user/reducer'
import styled from 'styled-components/macro'
import { shortenAddress } from '../../nft/utils/address'
import { useToggleModal } from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer'
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
import { ButtonPrimary } from '../Button'
import StatusIcon from '../Identicon/StatusIcon'
import IconButton, { IconHoverText } from './IconButton'
const UNIbutton = styled(ButtonPrimary)`
background: linear-gradient(to right, #9139b0 0%, #4261d6 100%);
border-radius: 12px;
padding-top: 10px;
padding-bottom: 10px;
margin-top: 12px;
color: white;
border: none;
`
const Column = styled.div`
display: flex;
flex-direction: column;
text-align: center;
`
const IconContainer = styled.div`
display: flex;
align-items: center;
& > a,
& > button {
margin-right: 8px;
}
& > button:last-child {
margin-right: 0px;
${IconHoverText}:last-child {
left: 0px;
}
}
`
const USDText = styled.div`
font-size: 16px;
font-weight: 500;
color: ${({ theme }) => theme.textSecondary};
margin-top: 8px;
`
const FlexContainer = styled.div`
display: flex;
`
const StatusWrapper = styled.div`
display: inline-block;
margin-top: 4px;
`
const BalanceWrapper = styled.div`
padding: 16px 0;
`
const HeaderWrapper = styled.div`
margin-bottom: 12px;
display: flex;
justify-content: space-between;
`
const AuthenticatedHeaderWrapper = styled.div`
padding: 0 16px;
`
const AuthenticatedHeader = () => {
const { account, chainId, connector } = useWeb3React()
const [isCopied, setCopied] = useCopyClipboard()
const copy = useCallback(() => {
setCopied(account || '')
}, [account, setCopied])
const dispatch = useAppDispatch()
const balanceString = useCurrencyBalanceString(account ?? '')
const {
nativeCurrency: { symbol: nativeCurrencySymbol },
explorer,
} = getChainInfoOrDefault(chainId ? chainId : SupportedChainId.MAINNET)
const unclaimedAmount: CurrencyAmount<Token> | undefined = useUserUnclaimedAmount(account)
const isUnclaimed = useUserHasAvailableClaim(account)
const connectionType = getConnection(connector).type
const nativeCurrency = useNativeCurrency()
const nativeCurrencyPrice = useStablecoinPrice(nativeCurrency ?? undefined) || 0
const openClaimModal = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
const disconnect = useCallback(() => {
if (connector && connector.deactivate) {
connector.deactivate()
}
connector.resetState()
dispatch(updateSelectedWallet({ wallet: undefined }))
}, [connector, dispatch])
const amountUSD = useMemo(() => {
const price = parseFloat(nativeCurrencyPrice.toFixed(5))
const balance = parseFloat(balanceString || '0')
return price * balance
}, [balanceString, nativeCurrencyPrice])
return (
<AuthenticatedHeaderWrapper>
<HeaderWrapper>
<StatusWrapper>
<FlexContainer>
<StatusIcon connectionType={connectionType} size={24} />
<Text fontSize={16} fontWeight={600} marginTop="2.5px">
{account && shortenAddress(account, 2, 4)}
</Text>
</FlexContainer>
</StatusWrapper>
<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>} />
</IconContainer>
</HeaderWrapper>
<Column>
<BalanceWrapper>
<Text fontSize={36} fontWeight={400}>
{balanceString} {nativeCurrencySymbol}
</Text>
<USDText>${amountUSD.toFixed(2)} USD</USDText>
</BalanceWrapper>
{isUnclaimed && (
<UNIbutton onClick={openClaimModal}>
<Trans>Claim</Trans> {unclaimedAmount?.toFixed(0, { groupSeparator: ',' } ?? '-')} <Trans>reward</Trans>
</UNIbutton>
)}
</Column>
</AuthenticatedHeaderWrapper>
)
}
export default AuthenticatedHeader

View File

@@ -0,0 +1,156 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { ButtonPrimary } from 'components/Button'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { useMemo } from 'react'
import { ChevronRight, Moon, Sun } from 'react-feather'
import { useToggleWalletModal } from 'state/application/hooks'
import { useDarkModeManager } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { useAllTransactions } from '../../state/transactions/hooks'
import AuthenticatedHeader from './AuthenticatedHeader'
import { MenuState } from './index'
const ConnectButton = styled(ButtonPrimary)`
border-radius: 12px;
height: 44px;
width: 288px;
font-weight: 600;
font-size: 16px;
margin-left: auto;
margin-right: auto;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
width: 100%;
}
`
const Divider = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
margin-top: 16px;
margin-bottom: 16px;
`
const ToggleMenuItem = styled.button`
background-color: transparent;
margin: 0;
border: none;
cursor: pointer;
display: flex;
flex: 1;
border-radius: 12px;
flex-direction: row;
align-items: center;
justify-content: space-between;
font-size: 14px;
font-weight: 400;
width: 100%;
padding: 12px 8px;
color: ${({ theme }) => theme.textSecondary};
:hover {
color: ${({ theme }) => theme.textPrimary};
background-color: ${({ theme }) => theme.backgroundModule};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms all ${timing.in}`};
}
`
const FlexContainer = styled.div`
display: flex;
`
const PendingBadge = styled.span`
background-color: ${({ theme }) => theme.accentActionSoft};
color: ${({ theme }) => theme.deprecated_primary3};
font-weight: 600;
padding: 4px 8px;
border-radius: 4px;
`
const IconWrap = styled.span`
display: inline-block;
margin-top: auto;
margin-bottom: auto;
margin-left: 4px;
height: 16px;
`
const DefaultMenuWrap = styled.div`
width: 100%;
height: 100%;
padding: 0 8px;
`
const DefaultText = styled.span`
font-size: 14px;
font-weight: 400;
`
const CenterVertically = styled.div`
margin-top: auto;
margin-bottom: auto;
`
const WalletDropdown = ({ setMenu }: { setMenu: (state: MenuState) => void }) => {
const { account } = useWeb3React()
const isAuthenticated = !!account
const [darkMode, toggleDarkMode] = useDarkModeManager()
const activeLocale = useActiveLocale()
const ISO = activeLocale.split('-')[0].toUpperCase()
const allTransactions = useAllTransactions()
const toggleWalletModal = useToggleWalletModal()
const pendingTransactions = useMemo(
() => Object.values(allTransactions).filter((tx) => !tx.receipt),
[allTransactions]
)
return (
<DefaultMenuWrap>
{isAuthenticated ? (
<AuthenticatedHeader />
) : (
<ConnectButton onClick={toggleWalletModal}>Connect wallet</ConnectButton>
)}
<Divider />
{isAuthenticated && (
<ToggleMenuItem onClick={() => setMenu(MenuState.TRANSACTIONS)}>
<DefaultText>
<Trans>Transactions</Trans>{' '}
{pendingTransactions.length > 0 && (
<PendingBadge>
{pendingTransactions.length} <Trans>Pending</Trans>
</PendingBadge>
)}
</DefaultText>
<IconWrap>
<ChevronRight size={16} strokeWidth={3} />
</IconWrap>
</ToggleMenuItem>
)}
<ToggleMenuItem onClick={() => setMenu(MenuState.LANGUAGE)}>
<DefaultText>
<Trans>Language</Trans>
</DefaultText>
<FlexContainer>
<CenterVertically>
<DefaultText>{ISO}</DefaultText>
</CenterVertically>
<IconWrap>
<ChevronRight size={16} strokeWidth={3} />
</IconWrap>
</FlexContainer>
</ToggleMenuItem>
<ToggleMenuItem onClick={toggleDarkMode}>
<DefaultText>{darkMode ? <Trans> Light theme</Trans> : <Trans>Dark theme</Trans>}</DefaultText>
<IconWrap>{darkMode ? <Sun size={16} /> : <Moon size={16} />}</IconWrap>
</ToggleMenuItem>
</DefaultMenuWrap>
)
}
export default WalletDropdown

View File

@@ -0,0 +1,87 @@
import { Icon } from 'react-feather'
import styled, { css } from 'styled-components/macro'
export const IconHoverText = styled.span`
color: ${({ theme }) => theme.textPrimary};
position: absolute;
top: 28px;
border-radius: 8px;
transform: translateX(-50%);
opacity: 0;
font-size: 12px;
padding: 5px;
left: 10px;
`
const IconStyles = css`
background-color: ${({ theme }) => theme.backgroundInteractive};
border-radius: 12px;
display: inline-block;
cursor: pointer;
position: relative;
height: 32px;
width: 32px;
color: ${({ theme }) => theme.textPrimary};
:hover {
background-color: ${({ theme }) => theme.hoverState};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms background-color ${timing.in}`};
${IconHoverText} {
opacity: 1;
}
}
:active {
background-color: ${({ theme }) => theme.backgroundSurface};
transition: background-color 50ms linear;
}
`
const IconBlockLink = styled.a`
${IconStyles};
`
const IconBlockButton = styled.button`
${IconStyles};
border: none;
outline: none;
`
const IconWrapper = styled.span`
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
`
interface IconButtonProps {
text: React.ReactNode
Icon: Icon
onClick?: () => void
href?: string
}
const IconButton = ({ Icon, onClick, text, href }: IconButtonProps) => {
return href ? (
<IconBlockLink href={href} target="_blank">
<IconWrapper>
<Icon strokeWidth={1.5} size={16} />
<IconHoverText>{text}</IconHoverText>
</IconWrapper>
</IconBlockLink>
) : (
<IconBlockButton onClick={onClick}>
<IconWrapper>
<Icon strokeWidth={1.5} size={16} />
<IconHoverText>{text}</IconHoverText>
</IconWrapper>
</IconBlockButton>
)
}
export default IconButton

View File

@@ -0,0 +1,68 @@
import { Trans } from '@lingui/macro'
import { LOCALE_LABEL, SUPPORTED_LOCALES, SupportedLocale } from 'constants/locales'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { useLocationLinkProps } from 'hooks/useLocationLinkProps'
import { Check } from 'react-feather'
import { Link } from 'react-router-dom'
import { Text } from 'rebass'
import styled, { useTheme } from 'styled-components/macro'
import { SlideOutMenu } from './SlideOutMenu'
const InternalMenuItem = styled(Link)`
flex: 1;
padding: 0.5rem 0.5rem;
color: ${({ theme }) => theme.textTertiary};
:hover {
cursor: pointer;
}
`
const InternalLinkMenuItem = styled(InternalMenuItem)`
display: flex;
flex-direction: row;
align-items: center;
padding: 12px 16px;
justify-content: space-between;
text-decoration: none;
color: ${({ theme }) => theme.textPrimary};
:hover {
cursor: pointer;
background-color: ${({ theme }) => theme.backgroundModule};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms background-color ${timing.in}`};
}
`
function LanguageMenuItem({ locale, isActive }: { locale: SupportedLocale; isActive: boolean }) {
const { to, onClick } = useLocationLinkProps(locale)
const theme = useTheme()
if (!to) return null
return (
<InternalLinkMenuItem onClick={onClick} to={to}>
<Text fontSize={16} fontWeight={400} lineHeight="24px">
{LOCALE_LABEL[locale]}
</Text>
{isActive && <Check color={theme.accentActive} opacity={1} size={20} />}
</InternalLinkMenuItem>
)
}
const LanguageMenu = ({ onClose }: { onClose: () => void }) => {
const activeLocale = useActiveLocale()
return (
<SlideOutMenu title={<Trans>Language</Trans>} onClose={onClose}>
{SUPPORTED_LOCALES.map((locale) => (
<LanguageMenuItem locale={locale} isActive={activeLocale === locale} key={locale} />
))}
</SlideOutMenu>
)
}
export default LanguageMenu

View File

@@ -0,0 +1,112 @@
import { ChevronLeft } from 'react-feather'
import styled from 'styled-components/macro'
const Menu = styled.div`
width: 100%;
height: 100%;
font-size: 16px;
overflow: auto;
// Firefox scrollbar styling
scrollbar-width: thin;
scrollbar-color: ${({ theme }) => `${theme.backgroundOutline} transparent`};
// safari and chrome scrollbar styling
::-webkit-scrollbar {
background: transparent;
width: 4px;
}
::-webkit-scrollbar-track {
margin-top: 40px;
}
::-webkit-scrollbar-thumb {
background: ${({ theme }) => theme.backgroundOutline};
border-radius: 8px;
}
`
const Header = styled.span`
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
`
const ClearAll = styled.div`
display: inline-block;
cursor: pointer;
color: ${({ theme }) => theme.accentAction};
font-weight: 600;
font-size: 14px;
margin-top: auto;
margin-bottom: auto;
:hover {
opacity: 0.6;
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms opacity ${timing.in}`};
}
`
const StyledChevron = styled(ChevronLeft)`
cursor: pointer;
&:hover {
color: ${({ theme }) => theme.textPrimary};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms color ${timing.in}`};
}
`
const BackSection = styled.div`
position: absolute;
background-color: ${({ theme }) => theme.backgroundSurface};
width: 99%;
padding: 0 16px 16px 16px;
color: ${({ theme }) => theme.textSecondary};
cursor: default;
display: flex;
justify-content: space-between;
z-index: 1;
`
const BackSectionContainer = styled.div`
display: flex;
justify-content: space-between;
position: relative;
width: 100%;
`
const ChildrenContainer = styled.div`
margin-top: 40px;
`
export const SlideOutMenu = ({
children,
onClose,
title,
onClear,
}: {
onClose: () => void
title: React.ReactNode
children: React.ReactNode
onClear?: () => void
}) => (
<Menu>
<BackSection>
<BackSectionContainer>
<StyledChevron onClick={onClose} size={24} />
<Header>{title}</Header>
{onClear && <ClearAll onClick={onClear}>Clear All</ClearAll>}
</BackSectionContainer>
</BackSection>
<ChildrenContainer>{children}</ChildrenContainer>
</Menu>
)

View File

@@ -0,0 +1,167 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { getYear, isSameDay, isSameWeek, isSameYear } from 'date-fns'
import ms from 'ms.macro'
import { useCallback, useMemo } from 'react'
import { useAppDispatch } from 'state/hooks'
import styled from 'styled-components/macro'
import { useAllTransactions } from '../../state/transactions/hooks'
import { clearAllTransactions } from '../../state/transactions/reducer'
import { TransactionDetails } from '../../state/transactions/types'
import { TransactionSummary } from '../AccountDetailsV2'
import { SlideOutMenu } from './SlideOutMenu'
const THIRTY_DAYS = ms`30 days`
const Divider = styled.div`
margin-top: 16px;
border-bottom: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
`
const TransactionListWrapper = styled.div`
${({ theme }) => theme.flexColumnNoWrap};
`
interface TransactionInformation {
title: string
transactions: TransactionDetails[]
}
const TransactionTitle = styled.span`
padding-bottom: 8px;
padding-top: 20px;
padding-left: 12px;
padding-right: 12px;
font-weight: 600;
color: ${({ theme }) => theme.textTertiary};
`
const TransactionList = ({ transactionInformation }: { transactionInformation: TransactionInformation }) => {
const { title, transactions } = transactionInformation
return (
<TransactionListWrapper key={title}>
<TransactionTitle>{title}</TransactionTitle>
{transactions.map((transactionDetails) => (
<TransactionSummary key={transactionDetails.hash} transactionDetails={transactionDetails} />
))}
</TransactionListWrapper>
)
}
const getConfirmedTransactions = (confirmedTransactions: Array<TransactionDetails>) => {
const now = new Date().getTime()
const today: Array<TransactionDetails> = []
const currentWeek: Array<TransactionDetails> = []
const last30Days: Array<TransactionDetails> = []
const currentYear: Array<TransactionDetails> = []
const yearMap: { [key: string]: Array<TransactionDetails> } = {}
confirmedTransactions.forEach((transaction) => {
const { addedTime } = transaction
if (isSameDay(now, addedTime)) {
today.push(transaction)
} else if (isSameWeek(addedTime, now)) {
currentWeek.push(transaction)
} else if (now - addedTime < THIRTY_DAYS) {
last30Days.push(transaction)
} else if (isSameYear(addedTime, now)) {
currentYear.push(transaction)
} else {
const year = getYear(addedTime)
if (!yearMap[year]) {
yearMap[year] = [transaction]
} else {
yearMap[year].push(transaction)
}
}
})
const transactionGroups: Array<TransactionInformation> = [
{
title: 'Today',
transactions: today,
},
{
title: 'This week',
transactions: currentWeek,
},
{
title: 'Past 30 Days',
transactions: last30Days,
},
{
title: 'This year',
transactions: currentYear,
},
]
const sortedYears = Object.keys(yearMap)
.sort((a, b) => parseInt(b) - parseInt(a))
.map((year) => ({ title: year, transactions: yearMap[year] }))
transactionGroups.push(...sortedYears)
return transactionGroups.filter((transactionInformation) => transactionInformation.transactions.length > 0)
}
const EmptyTransaction = styled.div`
text-align: center;
margin-top: 24px;
font-weight: 400;
font-size: 14px;
padding-left: 12px;
padding-right: 12px;
color: ${({ theme }) => theme.textSecondary};
`
export const TransactionHistoryMenu = ({ onClose }: { onClose: () => void }) => {
const allTransactions = useAllTransactions()
const { chainId } = useWeb3React()
const dispatch = useAppDispatch()
const transactionGroupsInformation = []
const clearAllTransactionsCallback = useCallback(() => {
if (chainId) dispatch(clearAllTransactions({ chainId }))
}, [dispatch, chainId])
const [confirmed, pending] = useMemo(() => {
const confirmed: Array<TransactionDetails> = []
const pending: Array<TransactionDetails> = []
const sorted = Object.values(allTransactions).sort((a, b) => b.addedTime - a.addedTime)
sorted.forEach((transaction) => (transaction.receipt ? confirmed.push(transaction) : pending.push(transaction)))
return [confirmed, pending]
}, [allTransactions])
const confirmedTransactions = useMemo(() => getConfirmedTransactions(confirmed), [confirmed])
if (pending.length) transactionGroupsInformation.push({ title: `Pending (${pending.length})`, transactions: pending })
if (confirmedTransactions.length) transactionGroupsInformation.push(...confirmedTransactions)
return (
<SlideOutMenu
onClose={onClose}
onClear={transactionGroupsInformation.length > 0 ? clearAllTransactionsCallback : undefined}
title={<Trans>Transactions</Trans>}
>
<Divider />
{transactionGroupsInformation.length > 0 ? (
<>
{transactionGroupsInformation.map((transactionInformation, index) => (
<TransactionList key={transactionInformation.title} transactionInformation={transactionInformation} />
))}
</>
) : (
<EmptyTransaction>
<Trans>Your transactions will appear here</Trans>
</EmptyTransaction>
)}
</SlideOutMenu>
)
}

View File

@@ -0,0 +1,72 @@
import { useState } from 'react'
import styled from 'styled-components/macro'
import { Z_INDEX } from 'theme'
import { useModalIsOpen } from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer'
import DefaultMenu from './DefaultMenu'
import LanguageMenu from './LanguageMenu'
import { TransactionHistoryMenu } from './TransactionMenu'
const WalletWrapper = styled.div`
border-radius: 12px;
width: 320px;
max-height: 376px;
display: flex;
flex-direction: column;
font-size: 16px;
top: 60px;
right: 70px;
background-color: ${({ theme }) => theme.backgroundSurface};
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
box-shadow: ${({ theme }) => theme.deepShadow};
padding: 16px 0;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
width: 100%;
border-bottom-right-radius: 0px;
border-bottom-left-radius: 0px;
box-shadow: unset;
}
`
export enum MenuState {
DEFAULT = 'DEFAULT',
LANGUAGE = 'LANGUAGE',
TRANSACTIONS = 'TRANSACTIONS',
}
const WalletDropdownWrapper = styled.div`
position: fixed;
top: 72px;
right: 20px;
z-index: ${Z_INDEX.dropdown};
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
top: unset;
left: 0;
right: 0;
bottom: 56px;
}
`
const WalletDropdown = () => {
const [menu, setMenu] = useState<MenuState>(MenuState.DEFAULT)
const walletDropdownOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN)
return (
<>
{walletDropdownOpen && (
<WalletDropdownWrapper>
<WalletWrapper>
{menu === MenuState.TRANSACTIONS && <TransactionHistoryMenu onClose={() => setMenu(MenuState.DEFAULT)} />}
{menu === MenuState.LANGUAGE && <LanguageMenu onClose={() => setMenu(MenuState.DEFAULT)} />}
{menu === MenuState.DEFAULT && <DefaultMenu setMenu={setMenu} />}
</WalletWrapper>
</WalletDropdownWrapper>
)}
</>
)
}
export default WalletDropdown

View File

@@ -1,24 +0,0 @@
import { Connector } from '@web3-react/types'
import FORTMATIC_ICON_URL from 'assets/images/fortmaticIcon.png'
import { ConnectionType, fortmaticConnection } from 'connection'
import { getConnectionName } from 'connection/utils'
import Option from './Option'
const BASE_PROPS = {
color: '#6748FF',
icon: FORTMATIC_ICON_URL,
id: 'fortmatic',
}
export function FortmaticOption({ tryActivation }: { tryActivation: (connector: Connector) => void }) {
const isActive = fortmaticConnection.hooks.useIsActive()
return (
<Option
{...BASE_PROPS}
isActive={isActive}
onClick={() => tryActivation(fortmaticConnection.connector)}
header={getConnectionName(ConnectionType.FORTMATIC)}
/>
)
}

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