feat: explore UI (#4262)
* feat(explore): add /explore route (#3935) add route * Explore use top tokens (#3954) feat(explore): add a top tokens hook with mock data * feat(explore): use token price (#3958) feat(explore): add useTokenPrice hook and dumby data * fix(explore): mock data fetching hook return type (#3959) * chore(deps): bump token-lists (#3929) (#3961) Co-authored-by: matteenm <105068213+matteenm@users.noreply.github.com> * feat: Kg/add time selector dropdown UI (#3956) * feat: add time selector dropdown UI * update time selector style * feat(explore): use token relevant resources (#3963) chore(deps): bump token-lists (#3929) (#3961) Co-authored-by: matteenm <105068213+matteenm@users.noreply.github.com> Co-authored-by: matteenm <105068213+matteenm@users.noreply.github.com> * chore: merge main into explore (#3970) * chore(deps): bump token-lists (#3929) * feat: empty to deploy 628417f696f40cb54ef5bbba2374573e75a59915 (#3962) feat: empty to deploy * feat: fix metamask mobile browser connection (#3964) * fix metamask * forceActivate * remove forceActivate * unused change * feat(risk): cache risk check with ttl (#3965) Co-authored-by: matteenm <105068213+matteenm@users.noreply.github.com> Co-authored-by: Vignesh Mohankumar <vignesh@vigneshmohankumar.com> * feat: add initial token table (#3957) * add token table UI * update token table with intial data pipeline * feat: Load token table with initial dummy data TODO: get token information (token name and symbol) * add token table UI and token row components * update table with token logo * update table with correct arrow * update table border * runs prettier (#3971) prettier * add header to tokenRow, format dollar util, add responsiveness * update table styling * update table styling and components setup * add back side padding * create header cell component * update table styling * fix padding * update css styling * Alphabetize styles Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com> * fix: add mobile responsiveness break point (#3988) update width mobile breakpoint * fix: hide header when mobile (#3989) hide header on token table when mobile view * feat: stack token name and symbol (mobile view) (#3996) * stack token name and symbol * style: clean up CSS * feat: add token table loading state (#3984) * add token table loading state * make token row components reusable * change typing and CSS styling * remove key props * feat: token table mobile view (#4003) * fix conflicts * style: CSS cleaning 2 * clean divs * add media breakpoint constants * feat: add favorites button frontend component (#4007) * add favorites button frontend component * fix height and width CSS * fix: small arrow sizing detail (#4012) fix small arrow sizing detail * feat: filter favorite tokens (#4010) * filter favorite tokens * fix atom * make showFavorite an atom * implement atom and clean CSS * change naming schema * feat: explore search bar UI (#4018) * search bar CSS * style css fix * change from atom to useState * fix: fix slow favoriting bug (#4033) * fix favoriting bug * fix code styling * minor change * feat: search responsiveness (#4034) * search responsiveness * hide placeholder * css fix * shared file * feat: token link page with token address URL param (#4039) * token detail draft * initial route path and some info * reduce PR * fixes * token null fix * feat: token detail page header UI (#4041) * token detail draft * initial route path and some info * reduce PR * token header * remove flex * font sizes * fix CSS * feat: add timeframe options UI (#4042) * add timeframe options * map times * list times * feat: explore & token detail linking (#4048) * link routing * fix focus * Update index.tsx * feat: token detail page metadata UI (#4047) * skeleton * padding change * fix link styling * add resource component * feat: remove swap button (#4055) * remove swap button & responsiveness * center sparkline * remove margin * fix: token details color fix (#4056) fix hover * feat: network balances component (#4059) * fix hover * initial network balance * fix network * checking 0 balance * add unsupported chain check * add network selector * multiple netwrk logic * change polygon logo * fix * naming * feat: add more and incorporate dummy data (#4066) * for demo * link protocol info * colors in shared file * feat: loading state for token detail (#4068) * animate chart mwaha * get rid of comments * add timeout * add fake widget * style * move loading into own file * fix: patch bad imports * feat: header hover states and favorite active state styling (#4079) * hover states * favorite * type boolean * fixes * fix eslint * fix prettier * fix import * feat(explore-table-filter): add basic text filtering to explore page (#4105) * feat(explore-table-filter): add basic text filtering to explore page token table * pr feedback * chore: merge in latest changes from main (#4108) * refactor: remove hideRouteDiagram prop (#3763) * fix: Revert "refactor: remove coinbase wallet resetState" (#4081) Revert "refactor: remove coinbase wallet resetState (#4024)" This reverts commit e36722ccb4cd282aa932ff7c7e6082190f3ed131. * feat: add support for Celo (#3915) * feat: Support for Celo * fix: wrong condition * combine celo and alfajores lists * use celo erc20 representation * fix: refactor infura.ts to networks.ts & add celo to rpc urls * feature: add celo contract addresses fix: remove celo from supported gas estimate chains until feature is available * refactor: useUSDCPrice to useStablecoinPrice fix: add celo to supported gas estimate chains * fix: use unique factory address for getting pool address * fix: darkmode background graident * fix: removing a comment left behind * fix: remove bad import * fix: remove dead link until the Celo is live on info.uniswap.org * fix: add asset to common bases & minor refactoring * fix: celo info links point to root info.uniswap.org * fix: change celo token bridge to portal * fix: update redux-multicall to latest version * refactor: for code readability * fix: celo banner colors & remove unused alternative logo * fix: change celo token list to hosted version * fix: update celo banner colors * fix: move celo to the bottom of the network selector list * fix: dedup dependencies @uniswap/router-sdk @uniswap/v3-sdk * fix: refactoring + move Celo above L2s * fix: update celo contract addresses * fix: update celo subgraph * fix: update v3-sdk and smart-order-router versions * fix: move Celo to the bottom of the network selector list * fix: downgrade smart-order-router and add casting fix * fix: downgrade smart-order-router and add casting fix * fix: resolve Pool dependency * fix: bridge chain id types * fix: explorer link test * fix: use quoter v2 ABI in useClientSideV3Trade fro Celo * fix: update connection "infura_rpc" to networks * fix: revert yarn.lock and force install * fix: dedup router and v3 sdk * refactor: mv quoter v2 to client side v3 trade * build: dedup lockfile * feature: add portal ether to common bases * fix: add comment for chains that use QuoterV2 * fix: use token as native asset * fix: supply correct factory address to getPoolAddress call & refactor nativeOnChain method * feature: adjust celo tokens presetned * fix: update celo explorer to celoscan * fix: celo token casting * fix: celo celo explorer it * fix: celo chain info should be consistent with block explorer used. Co-authored-by: Zach Pomerantz <zzmp@uniswap.org> * fix: revert "fix button jump on currency panel" (#4083) fix padding * fix: unsupported chain displays message instead of crash (#4054) * made initial changes for pools page displaying w/ unsupported chains * condensed styling * added chain validation to CTACards and wrote tests for both CTAcards and Pools page * linted changes * switched from snapshot to text matching tests * switched test to use check for text instead of testid * fix: add crossplatform `prei18n-extract` script (#3728) * fix: 🐛 add crossplatform `prei18n-extract` script * fix: 🚨 add newline * Revert "fix: 🐛 add crossplatform `prei18n-extract` script" This reverts commit 201bd2308a3caf648368b3945d5b73d8cb46c816. * build: 📦 add `shx` as dev dep, use it in `prei18n:extract` script * fix: 🐛 use platform-specific commands for prei18n-extract * chore(i18n): new Crowdin translations (#4084) chore(i18n): synchronize translations from crowdin [skip ci] Co-authored-by: Crowdin Bot <support+bot@crowdin.com> * feat: implement trace framework for analytics (#4060) * init commit * add amplitude ts sdk to package.json * add more comments and documentation * respond to vm comments * respond to cmcewen comments * fix: remove unused constants * init commit * adapt to web * add optional event properties to trace * correct telemetry to analytics * change telemetry to analytics in doc * fix: respond to cmcewen comments + initialize analytics in app.tsx + add missing return statement * respond to zzmp comments * fixes * eliminate unnecessary state * respond to part of zzmp comments * respond to zzmp comments round 2 * fixes * respond to zzmp comments * refactor: wallet specific Option components (#4065) * refactor: wallet specific Option components * fix * fix * fix coinbase wallet logic * injected logic * remove wallet.ts * install metamask * move all into InjectedOption * fix mobile metamask * wip * more mocking * more test fixes * refactor * more special casing * isMetaMask * simplify components * fix imports * fix coinbase wallet * test fix * fix connectors changing * Revert "fix connectors changing" This reverts commit 2acfe645ca506048e599d515674a54b27d12144f. * more to typescript logic instead of jsx * chore(i18n): new Crowdin translations (#4090) * build: upgrade @typescript-eslint (#4095) build: update @typescript-eslint * build: update caniuse-lite (#4093) * test: enforce deps deduplication (#4097) * build: use fewer babel versions * build: dedup * test: test deps dedups * fix: test.yml * fix: typo * test: failing * fix: dedup * fix: dedup * test: comment dedup tests * chore: whitespace * feat: implement token selector events (#4067) * init commit * add amplitude ts sdk to package.json * add more comments and documentation * respond to vm comments * respond to cmcewen comments * fix: remove unused constants * init commit * adapt to web * add optional event properties to trace * correct telemetry to analytics * change telemetry to analytics in doc * fix: respond to cmcewen comments + initialize analytics in app.tsx + add missing return statement * init commit * respond to zzmp comments * add token selected event * fixes * eliminate unnecessary state * respond to part of zzmp comments * respond to zzmp comments round 2 * fixes * respond to zzmp comments * add imported token event and other fixes * also log onKeyPress for suggested tokens * respond to cmcewen comments * chore: updates web3-react, adds key for changing connector order (#4085) * fix connectors changing * update package * add connection name * rename file * de-dupe * cb wallet fix * fix * yarn change * log the key * re-order connections * memoize the key * some updates * rm console * prevent memory leak Co-authored-by: Noah Zinsmeister <noahwz@gmail.com> * feat: implement-page-viewed-event-for-all-main-pages-of-app (#4089) * init commit: initial constants for pages, implement vote page viewed * implement swap * implement pool * remove charts * simplify shouldLogImpression * chore: upgrade to react 18 (#3992) * chore: upgrade to react 18 * fix: update tests * fix: fix lint issues and remove unnecessary react hooks testing library * fix: add types for stricter typescript checks * fix: fix additional typescript check issues * fix: revert to prev commmit * rebase * rebase * fix: fix lint issues and remove unnecessary react hooks testing library * fix: add types for stricter typescript checks * fix: fix additional typescript check issues * rebase * fix: rebase * fix * eslint fix * fix: package.json changes * fix: package.json changes * fix yarn lock * fix version package.json * fix: downgrade react-router-dom to original * fix: undo modification of .github/workflows/release.yaml * fix: revert cypress testing version update * rebase * rebase * fix: fix lint issues and remove unnecessary react hooks testing library * fix: add types for stricter typescript checks * fix: fix additional typescript check issues * rebase * chore: upgrade to react 18 * fix: update tests * fix: fix lint issues and remove unnecessary react hooks testing library * fix: add types for stricter typescript checks * fix: fix additional typescript check issues * fix * eslint fix * fix: package.json changes * fix: package.json changes * fix yarn lock * fix version package.json * fix: downgrade react-router-dom to original * fix: undo modification of .github/workflows/release.yaml * fix: revert cypress testing version update * fix * fix: error boundary change * yarn.lock change * fix: cypress tests finally passing due to zzmp redux multicall fix HOORAY * undo service worker changes * build: dedup lockfile * yarn.lock + lint * update snapshot tests * checkpoint * yarn.lock * fix: fix type errors during build * fixes * fix yarn.lock * dedup yarn * fix: import react components explicitly instead of all of react * dedup * yarn.lock * yarn.lock * dedup * yarn * dedup * dedupe use-sync-external-store * fix build issues * dedup use-sync-external-store Co-authored-by: Zach Pomerantz <zzmp@uniswap.org> * chore(web3-react): fix connectEagerly for MetaMask mobile (#4101) * chore(web3-react): fix connectEagerly for MetaMask mobile * fix * build: pause deploy (#4102) * fix: update styled-components in package.json to latest to remove react invalid hook call warnings (#4103) * fix warning vig found by updating styled-components * revert unnecessary yarn.lock changes * reduce unnecessary changes * dedup * manual fix and dedup of yarn.lock * manually dedup @emotion/is-prop-valid * update snapshot tests * build: upgrade prettier to v2.7.1 (#4109) * style: prettier based on v2.2 * 2.7.1 instead? * npx * ^ * add celo chain text colors Co-authored-by: Anas Yousef <anas.y0807@gmail.com> Co-authored-by: Vignesh Mohankumar <vignesh@vigneshmohankumar.com> Co-authored-by: Jesse <31524583+Jesse-Sawa@users.noreply.github.com> Co-authored-by: Zach Pomerantz <zzmp@uniswap.org> Co-authored-by: Kaylee George <62825936+kayleegeorge@users.noreply.github.com> Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com> Co-authored-by: Bruno Crosier <bruno.crosier@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Crowdin Bot <support+bot@crowdin.com> Co-authored-by: lynn <41491154+lynnshaoyu@users.noreply.github.com> Co-authored-by: Noah Zinsmeister <noahwz@gmail.com> Co-authored-by: Charles Bachmeier <charlie@genie.xyz> * feat: favorite token on tokenDetail page (#4091) * favorite token on tokenDetail page * make favoriting reusable * export * fix hook call * fixes * fix * fix function * remove files * remove random * fix * fix spaces * fix color * Update settings.json * Update .gitignore * feat: add hook for multi-network token balances (#4104) * add hook for multi-network token balances * add predictable order to network balances * patch some lint issues and code cleanup * chore: add craco and vanilla extract libraries (#4100) * chore: upgrade to react 18 * fix: update tests * fix: fix lint issues and remove unnecessary react hooks testing library * fix: add types for stricter typescript checks * fix: fix additional typescript check issues * fix: revert to prev commmit * rebase * rebase * fix: fix lint issues and remove unnecessary react hooks testing library * fix: add types for stricter typescript checks * fix: fix additional typescript check issues * rebase * fix: rebase * fix * eslint fix * fix: package.json changes * fix: package.json changes * fix yarn lock * fix version package.json * fix: downgrade react-router-dom to original * fix: undo modification of .github/workflows/release.yaml * fix: revert cypress testing version update * rebase * rebase * fix: fix lint issues and remove unnecessary react hooks testing library * fix: add types for stricter typescript checks * fix: fix additional typescript check issues * rebase * chore: upgrade to react 18 * fix: update tests * fix: fix lint issues and remove unnecessary react hooks testing library * fix: add types for stricter typescript checks * fix: fix additional typescript check issues * fix * eslint fix * fix: package.json changes * fix: package.json changes * fix yarn lock * fix version package.json * fix: downgrade react-router-dom to original * fix: undo modification of .github/workflows/release.yaml * fix: revert cypress testing version update * fix * fix: error boundary change * yarn.lock change * fix: cypress tests finally passing due to zzmp redux multicall fix HOORAY * undo service worker changes * build: dedup lockfile * yarn.lock + lint * update snapshot tests * checkpoint * yarn.lock * fix: fix type errors during build * fixes * fix yarn.lock * dedup yarn * fix: import react components explicitly instead of all of react * chore: add craco and vanilla extract libraries * add craco config file * Add VE common styles, sprinkles, and themes * Actually add VE common styles, sprinkles, and themes Co-authored-by: Lynn Yu <lynn.yu@uniswap.org> Co-authored-by: lynn <41491154+lynnshaoyu@users.noreply.github.com> Co-authored-by: Zach Pomerantz <zzmp@uniswap.org> Co-authored-by: Charles Bachmeier <charlie@genie.xyz> * feat: Kg/explore expanding search bar (#4099) * expanding search * fix focus * making search * kms * ngmi * done * icons * color fix * add animation * fix start state * responsive * mouse * expanded Co-authored-by: Vignesh Mohankumar <me@vig.xyz> * fix: expand state (#4126) expand state * chore: merge main into explore (#4131) * refactor: remove hideRouteDiagram prop (#3763) * fix: Revert "refactor: remove coinbase wallet resetState" (#4081) Revert "refactor: remove coinbase wallet resetState (#4024)" This reverts commit e36722ccb4cd282aa932ff7c7e6082190f3ed131. * feat: add support for Celo (#3915) * feat: Support for Celo * fix: wrong condition * combine celo and alfajores lists * use celo erc20 representation * fix: refactor infura.ts to networks.ts & add celo to rpc urls * feature: add celo contract addresses fix: remove celo from supported gas estimate chains until feature is available * refactor: useUSDCPrice to useStablecoinPrice fix: add celo to supported gas estimate chains * fix: use unique factory address for getting pool address * fix: darkmode background graident * fix: removing a comment left behind * fix: remove bad import * fix: remove dead link until the Celo is live on info.uniswap.org * fix: add asset to common bases & minor refactoring * fix: celo info links point to root info.uniswap.org * fix: change celo token bridge to portal * fix: update redux-multicall to latest version * refactor: for code readability * fix: celo banner colors & remove unused alternative logo * fix: change celo token list to hosted version * fix: update celo banner colors * fix: move celo to the bottom of the network selector list * fix: dedup dependencies @uniswap/router-sdk @uniswap/v3-sdk * fix: refactoring + move Celo above L2s * fix: update celo contract addresses * fix: update celo subgraph * fix: update v3-sdk and smart-order-router versions * fix: move Celo to the bottom of the network selector list * fix: downgrade smart-order-router and add casting fix * fix: downgrade smart-order-router and add casting fix * fix: resolve Pool dependency * fix: bridge chain id types * fix: explorer link test * fix: use quoter v2 ABI in useClientSideV3Trade fro Celo * fix: update connection "infura_rpc" to networks * fix: revert yarn.lock and force install * fix: dedup router and v3 sdk * refactor: mv quoter v2 to client side v3 trade * build: dedup lockfile * feature: add portal ether to common bases * fix: add comment for chains that use QuoterV2 * fix: use token as native asset * fix: supply correct factory address to getPoolAddress call & refactor nativeOnChain method * feature: adjust celo tokens presetned * fix: update celo explorer to celoscan * fix: celo token casting * fix: celo celo explorer it * fix: celo chain info should be consistent with block explorer used. Co-authored-by: Zach Pomerantz <zzmp@uniswap.org> * fix: revert "fix button jump on currency panel" (#4083) fix padding * fix: unsupported chain displays message instead of crash (#4054) * made initial changes for pools page displaying w/ unsupported chains * condensed styling * added chain validation to CTACards and wrote tests for both CTAcards and Pools page * linted changes * switched from snapshot to text matching tests * switched test to use check for text instead of testid * fix: add crossplatform `prei18n-extract` script (#3728) * fix: 🐛 add crossplatform `prei18n-extract` script * fix: 🚨 add newline * Revert "fix: 🐛 add crossplatform `prei18n-extract` script" This reverts commit 201bd2308a3caf648368b3945d5b73d8cb46c816. * build: 📦 add `shx` as dev dep, use it in `prei18n:extract` script * fix: 🐛 use platform-specific commands for prei18n-extract * chore(i18n): new Crowdin translations (#4084) chore(i18n): synchronize translations from crowdin [skip ci] Co-authored-by: Crowdin Bot <support+bot@crowdin.com> * feat: implement trace framework for analytics (#4060) * init commit * add amplitude ts sdk to package.json * add more comments and documentation * respond to vm comments * respond to cmcewen comments * fix: remove unused constants * init commit * adapt to web * add optional event properties to trace * correct telemetry to analytics * change telemetry to analytics in doc * fix: respond to cmcewen comments + initialize analytics in app.tsx + add missing return statement * respond to zzmp comments * fixes * eliminate unnecessary state * respond to part of zzmp comments * respond to zzmp comments round 2 * fixes * respond to zzmp comments * refactor: wallet specific Option components (#4065) * refactor: wallet specific Option components * fix * fix * fix coinbase wallet logic * injected logic * remove wallet.ts * install metamask * move all into InjectedOption * fix mobile metamask * wip * more mocking * more test fixes * refactor * more special casing * isMetaMask * simplify components * fix imports * fix coinbase wallet * test fix * fix connectors changing * Revert "fix connectors changing" This reverts commit 2acfe645ca506048e599d515674a54b27d12144f. * more to typescript logic instead of jsx * chore(i18n): new Crowdin translations (#4090) * build: upgrade @typescript-eslint (#4095) build: update @typescript-eslint * build: update caniuse-lite (#4093) * test: enforce deps deduplication (#4097) * build: use fewer babel versions * build: dedup * test: test deps dedups * fix: test.yml * fix: typo * test: failing * fix: dedup * fix: dedup * test: comment dedup tests * chore: whitespace * feat: implement token selector events (#4067) * init commit * add amplitude ts sdk to package.json * add more comments and documentation * respond to vm comments * respond to cmcewen comments * fix: remove unused constants * init commit * adapt to web * add optional event properties to trace * correct telemetry to analytics * change telemetry to analytics in doc * fix: respond to cmcewen comments + initialize analytics in app.tsx + add missing return statement * init commit * respond to zzmp comments * add token selected event * fixes * eliminate unnecessary state * respond to part of zzmp comments * respond to zzmp comments round 2 * fixes * respond to zzmp comments * add imported token event and other fixes * also log onKeyPress for suggested tokens * respond to cmcewen comments * chore: updates web3-react, adds key for changing connector order (#4085) * fix connectors changing * update package * add connection name * rename file * de-dupe * cb wallet fix * fix * yarn change * log the key * re-order connections * memoize the key * some updates * rm console * prevent memory leak Co-authored-by: Noah Zinsmeister <noahwz@gmail.com> * feat: implement-page-viewed-event-for-all-main-pages-of-app (#4089) * init commit: initial constants for pages, implement vote page viewed * implement swap * implement pool * remove charts * simplify shouldLogImpression * chore: upgrade to react 18 (#3992) * chore: upgrade to react 18 * fix: update tests * fix: fix lint issues and remove unnecessary react hooks testing library * fix: add types for stricter typescript checks * fix: fix additional typescript check issues * fix: revert to prev commmit * rebase * rebase * fix: fix lint issues and remove unnecessary react hooks testing library * fix: add types for stricter typescript checks * fix: fix additional typescript check issues * rebase * fix: rebase * fix * eslint fix * fix: package.json changes * fix: package.json changes * fix yarn lock * fix version package.json * fix: downgrade react-router-dom to original * fix: undo modification of .github/workflows/release.yaml * fix: revert cypress testing version update * rebase * rebase * fix: fix lint issues and remove unnecessary react hooks testing library * fix: add types for stricter typescript checks * fix: fix additional typescript check issues * rebase * chore: upgrade to react 18 * fix: update tests * fix: fix lint issues and remove unnecessary react hooks testing library * fix: add types for stricter typescript checks * fix: fix additional typescript check issues * fix * eslint fix * fix: package.json changes * fix: package.json changes * fix yarn lock * fix version package.json * fix: downgrade react-router-dom to original * fix: undo modification of .github/workflows/release.yaml * fix: revert cypress testing version update * fix * fix: error boundary change * yarn.lock change * fix: cypress tests finally passing due to zzmp redux multicall fix HOORAY * undo service worker changes * build: dedup lockfile * yarn.lock + lint * update snapshot tests * checkpoint * yarn.lock * fix: fix type errors during build * fixes * fix yarn.lock * dedup yarn * fix: import react components explicitly instead of all of react * dedup * yarn.lock * yarn.lock * dedup * yarn * dedup * dedupe use-sync-external-store * fix build issues * dedup use-sync-external-store Co-authored-by: Zach Pomerantz <zzmp@uniswap.org> * chore(web3-react): fix connectEagerly for MetaMask mobile (#4101) * chore(web3-react): fix connectEagerly for MetaMask mobile * fix * build: pause deploy (#4102) * fix: update styled-components in package.json to latest to remove react invalid hook call warnings (#4103) * fix warning vig found by updating styled-components * revert unnecessary yarn.lock changes * reduce unnecessary changes * dedup * manual fix and dedup of yarn.lock * manually dedup @emotion/is-prop-valid * update snapshot tests * build: upgrade prettier to v2.7.1 (#4109) * style: prettier based on v2.2 * 2.7.1 instead? * npx * ^ * refactor: adding safe getter for ChainInfo (#4110) * replaced CHAIN_INFO access with a function call * updated CTACard tests to work with getChainInfo * updated typechecking, removed console.log * build: Revert "build: pause deploy" (#4107) * Revert "build: pause deploy (#4102)" This reverts commit 3a1ea3df85a60fd32f47b67ce933a6edd239384f. * prettier * refactor: remaining changes from the large celo merge (#4088) * refactor: useUSDCValue -> useStablecoinValue * refactor: use the isCelo() helper * refactor: remove unneeded white space * chore: upgrades react-router-dom, fixes dev-mode linking (#4115) * fix: stale route * fix: add e2e test * fix: update e2e test * fix: fixes Popover arrow positioning (#4119) fix: fix arrow position * build: don't fail cypress on unhandled exception (#4122) * fix: catch vibrant failure (#4123) fix: catch CORS error * feat: enable 1bp optimism fee tier (#4124) enable new optimism fee tier * balance summary fix * rm isChainAllowed Co-authored-by: Anas Yousef <anas.y0807@gmail.com> Co-authored-by: Jesse <31524583+Jesse-Sawa@users.noreply.github.com> Co-authored-by: Zach Pomerantz <zzmp@uniswap.org> Co-authored-by: Kaylee George <62825936+kayleegeorge@users.noreply.github.com> Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com> Co-authored-by: Bruno Crosier <bruno.crosier@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Crowdin Bot <support+bot@crowdin.com> Co-authored-by: lynn <41491154+lynnshaoyu@users.noreply.github.com> Co-authored-by: Noah Zinsmeister <noahwz@gmail.com> Co-authored-by: Sam Chen <chenxsan@gmail.com> * feat: share popout (#4112) * share popout * tweet * remove yarn update * update unnecessary adds * naming * success state * tweet * new window * new twitter window position and fix network display * fix css and add promise * comments * quick fixes * feat: Kg/explore network selector filter UI (#4129) * initial network * search bar fix * fix menu items * fix * renaming and remove comment * Update package.json * Update yarn.lock * Update yarn.lock * Update yarn.lock * update chain info * fix props * moving to another PR * fix: search responsiveness and expand state (#4142) * fix search expand * search moves to newline * chore: merge main into explore (#4153) * merge main * deprecated * deprecate colors * chore: merge main into explore (#4164) * merge main * re-work App.tsx * feat: add new colors to explore page (#4139) * update color theme file * update explore colors * fix * rename * hover state colors * table highlight * update colors! * small changes * Update settings.json * feat: different table states (#4166) * error state * center * filter at table level (error) * Update settings.json * filter at table * add hook * fix hook, add no tokens state * favorite in hook * add favorites part 2 * fix import * revert toptokens data type * componenet * feat: explore state management (#4171) * initial * add jotai * refile * save file * change style * reset filter string query * Update settings.json * location * feat: token details mobile responsiveness (#4172) * initial * gap * flexy * Update settings.json * flex box gains * fix: fix mobile padding on table and show header (#4168) * initial * fix padding * fix alignment * fix padding * Update settings.json * feat: change token price sparkline colors (#4173) * fix: make all token row content clickable (#4183) * initial * link content to token details * Update settings.json * header name token name label * cursor fix * Update settings.json * feat: token details small mobile views (320px min) (#4185) * initial * make min width 320 * Update settings.json * no mobile use * fix: hover color for TokenTable header cell state (#4184) * initial * change header cell hover state * add * Update settings.json * padding 4 to 8 * change header cell hover state * add * padding 4 to 8 * Update settings.json * reusability * Update settings.json * Update index.tsx * fix: Explore color changes (#4195) * initial * initial color changes * Update settings.json * color changes * fix: make token details loading state responsive (#4203) * initial * skeleton width * fix jumps * Update settings.json * rm wrapper * fix spaces * rm random height * fix props * fix: update colors on token details page (#4201) * initial * update token detail colors * Update settings.json * feat: explore table sort (#4202) * fix some sort style nits * style fixes * style fixes * sort functionality * refactor(explore): sortfn input from vig (#4209) sortfn input from vig * ts nits (#4210) * fix: add shimmer animation to Explore loading bubbles (#4211) * initial * add shimmer animation to loading bubbles * update shimmer * export * animation load * shared loaded * Update settings.json * feat: add error state for network balance summary (#4215) error state * feat: network badge on token details page (#4212) * initial network badge * update colors * fix color schema * update chain usage * change loading color * rm css * update naming * rename colors * feat: TokenDetailsPagequery (#4179) * general query for token details page * fix conditional useEffect * feat: amplitude analytics in explore, and make entire token row clickable (#4149) * initial * page log * token select * explore token page amplitude * add storage * comment * Update settings.json * rebased new * Update settings.json * fixes * fix amy * rebase with state management * rebase * Update TokenTable.tsx * Delete TokenTable.tsx * make row clickable and send event * rip out unnecessary leftover event * remove listNumber prop and derive from tokenListIndex directly Co-authored-by: Lynn Yu <lynn.yu@uniswap.org> * fix: rm underline for token details (#4255) rm underline * feat: initial price charts (#4254) * Created initial price chart using static data * addressed PR comments * applied theme, removed unused visx dependencies * chore: merge main into explore (#4260) * refactor: remove hideRouteDiagram prop (#3763) * fix: Revert "refactor: remove coinbase wallet resetState" (#4081) Revert "refactor: remove coinbase wallet resetState (#4024)" This reverts commit e36722ccb4cd282aa932ff7c7e6082190f3ed131. * feat: add support for Celo (#3915) * feat: Support for Celo * fix: wrong condition * combine celo and alfajores lists * use celo erc20 representation * fix: refactor infura.ts to networks.ts & add celo to rpc urls * feature: add celo contract addresses fix: remove celo from supported gas estimate chains until feature is available * refactor: useUSDCPrice to useStablecoinPrice fix: add celo to supported gas estimate chains * fix: use unique factory address for getting pool address * fix: darkmode background graident * fix: removing a comment left behind * fix: remove bad import * fix: remove dead link until the Celo is live on info.uniswap.org * fix: add asset to common bases & minor refactoring * fix: celo info links point to root info.uniswap.org * fix: change celo token bridge to portal * fix: update redux-multicall to latest version * refactor: for code readability * fix: celo banner colors & remove unused alternative logo * fix: change celo token list to hosted version * fix: update celo banner colors * fix: move celo to the bottom of the network selector list * fix: dedup dependencies @uniswap/router-sdk @uniswap/v3-sdk * fix: refactoring + move Celo above L2s * fix: update celo contract addresses * fix: update celo subgraph * fix: update v3-sdk and smart-order-router versions * fix: move Celo to the bottom of the network selector list * fix: downgrade smart-order-router and add casting fix * fix: downgrade smart-order-router and add casting fix * fix: resolve Pool dependency * fix: bridge chain id types * fix: explorer link test * fix: use quoter v2 ABI in useClientSideV3Trade fro Celo * fix: update connection "infura_rpc" to networks * fix: revert yarn.lock and force install * fix: dedup router and v3 sdk * refactor: mv quoter v2 to client side v3 trade * build: dedup lockfile * feature: add portal ether to common bases * fix: add comment for chains that use QuoterV2 * fix: use token as native asset * fix: supply correct factory address to getPoolAddress call & refactor nativeOnChain method * feature: adjust celo tokens presetned * fix: update celo explorer to celoscan * fix: celo token casting * fix: celo celo explorer it * fix: celo chain info should be consistent with block explorer used. Co-authored-by: Zach Pomerantz <zzmp@uniswap.org> * fix: revert "fix button jump on currency panel" (#4083) fix padding * fix: unsupported chain displays message instead of crash (#4054) * made initial changes for pools page displaying w/ unsupported chains * condensed styling * added chain validation to CTACards and wrote tests for both CTAcards and Pools page * linted changes * switched from snapshot to text matching tests * switched test to use check for text instead of testid * fix: add crossplatform `prei18n-extract` script (#3728) * fix: 🐛 add crossplatform `prei18n-extract` script * fix: 🚨 add newline * Revert "fix: 🐛 add crossplatform `prei18n-extract` script" This reverts commit 201bd2308a3caf648368b3945d5b73d8cb46c816. * build: 📦 add `shx` as dev dep, use it in `prei18n:extract` script * fix: 🐛 use platform-specific commands for prei18n-extract * chore(i18n): new Crowdin translations (#4084) chore(i18n): synchronize translations from crowdin [skip ci] Co-authored-by: Crowdin Bot <support+bot@crowdin.com> * feat: implement trace framework for analytics (#4060) * init commit * add amplitude ts sdk to package.json * add more comments and documentation * respond to vm comments * respond to cmcewen comments * fix: remove unused constants * init commit * adapt to web * add optional event properties to trace * correct telemetry to analytics * change telemetry to analytics in doc * fix: respond to cmcewen comments + initialize analytics in app.tsx + add missing return statement * respond to zzmp comments * fixes * eliminate unnecessary state * respond to part of zzmp comments * respond to zzmp comments round 2 * fixes * respond to zzmp comments * refactor: wallet specific Option components (#4065) * refactor: wallet specific Option components * fix * fix * fix coinbase wallet logic * injected logic * remove wallet.ts * install metamask * move all into InjectedOption * fix mobile metamask * wip * more mocking * more test fixes * refactor * more special casing * isMetaMask * simplify components * fix imports * fix coinbase wallet * test fix * fix connectors changing * Revert "fix connectors changing" This reverts commit 2acfe645ca506048e599d515674a54b27d12144f. * more to typescript logic instead of jsx * chore(i18n): new Crowdin translations (#4090) * build: upgrade @typescript-eslint (#4095) build: update @typescript-eslint * build: update caniuse-lite (#4093) * test: enforce deps deduplication (#4097) * build: use fewer babel versions * build: dedup * test: test deps dedups * fix: test.yml * fix: typo * test: failing * fix: dedup * fix: dedup * test: comment dedup tests * chore: whitespace * feat: implement token selector events (#4067) * init commit * add amplitude ts sdk to package.json * add more comments and documentation * respond to vm comments * respond to cmcewen comments * fix: remove unused constants * init commit * adapt to web * add optional event properties to trace * correct telemetry to analytics * change telemetry to analytics in doc * fix: respond to cmcewen comments + initialize analytics in app.tsx + add missing return statement * init commit * respond to zzmp comments * add token selected event * fixes * eliminate unnecessary state * respond to part of zzmp comments * respond to zzmp comments round 2 * fixes * respond to zzmp comments * add imported token event and other fixes * also log onKeyPress for suggested tokens * respond to cmcewen comments * chore: updates web3-react, adds key for changing connector order (#4085) * fix connectors changing * update package * add connection name * rename file * de-dupe * cb wallet fix * fix * yarn change * log the key * re-order connections * memoize the key * some updates * rm console * prevent memory leak Co-authored-by: Noah Zinsmeister <noahwz@gmail.com> * feat: implement-page-viewed-event-for-all-main-pages-of-app (#4089) * init commit: initial constants for pages, implement vote page viewed * implement swap * implement pool * remove charts * simplify shouldLogImpression * chore: upgrade to react 18 (#3992) * chore: upgrade to react 18 * fix: update tests * fix: fix lint issues and remove unnecessary react hooks testing library * fix: add types for stricter typescript checks * fix: fix additional typescript check issues * fix: revert to prev commmit * rebase * rebase * fix: fix lint issues and remove unnecessary react hooks testing library * fix: add types for stricter typescript checks * fix: fix additional typescript check issues * rebase * fix: rebase * fix * eslint fix * fix: package.json changes * fix: package.json changes * fix yarn lock * fix version package.json * fix: downgrade react-router-dom to original * fix: undo modification of .github/workflows/release.yaml * fix: revert cypress testing version update * rebase * rebase * fix: fix lint issues and remove unnecessary react hooks testing library * fix: add types for stricter typescript checks * fix: fix additional typescript check issues * rebase * chore: upgrade to react 18 * fix: update tests * fix: fix lint issues and remove unnecessary react hooks testing library * fix: add types for stricter typescript checks * fix: fix additional typescript check issues * fix * eslint fix * fix: package.json changes * fix: package.json changes * fix yarn lock * fix version package.json * fix: downgrade react-router-dom to original * fix: undo modification of .github/workflows/release.yaml * fix: revert cypress testing version update * fix * fix: error boundary change * yarn.lock change * fix: cypress tests finally passing due to zzmp redux multicall fix HOORAY * undo service worker changes * build: dedup lockfile * yarn.lock + lint * update snapshot tests * checkpoint * yarn.lock * fix: fix type errors during build * fixes * fix yarn.lock * dedup yarn * fix: import react components explicitly instead of all of react * dedup * yarn.lock * yarn.lock * dedup * yarn * dedup * dedupe use-sync-external-store * fix build issues * dedup use-sync-external-store Co-authored-by: Zach Pomerantz <zzmp@uniswap.org> * chore(web3-react): fix connectEagerly for MetaMask mobile (#4101) * chore(web3-react): fix connectEagerly for MetaMask mobile * fix * build: pause deploy (#4102) * fix: update styled-components in package.json to latest to remove react invalid hook call warnings (#4103) * fix warning vig found by updating styled-components * revert unnecessary yarn.lock changes * reduce unnecessary changes * dedup * manual fix and dedup of yarn.lock * manually dedup @emotion/is-prop-valid * update snapshot tests * build: upgrade prettier to v2.7.1 (#4109) * style: prettier based on v2.2 * 2.7.1 instead? * npx * ^ * refactor: adding safe getter for ChainInfo (#4110) * replaced CHAIN_INFO access with a function call * updated CTACard tests to work with getChainInfo * updated typechecking, removed console.log * build: Revert "build: pause deploy" (#4107) * Revert "build: pause deploy (#4102)" This reverts commit 3a1ea3df85a60fd32f47b67ce933a6edd239384f. * prettier * refactor: remaining changes from the large celo merge (#4088) * refactor: useUSDCValue -> useStablecoinValue * refactor: use the isCelo() helper * refactor: remove unneeded white space * chore: upgrades react-router-dom, fixes dev-mode linking (#4115) * fix: stale route * fix: add e2e test * fix: update e2e test * fix: fixes Popover arrow positioning (#4119) fix: fix arrow position * build: don't fail cypress on unhandled exception (#4122) * fix: catch vibrant failure (#4123) fix: catch CORS error * feat: enable 1bp optimism fee tier (#4124) enable new optimism fee tier * chore: move prettier, jest-styled-components to devDependencies (#4128) * change package * yarn.lock * feat: implement connect wallet category events (#4111) * init commit * wallet connected event init commit * add received_swap_quote event property * add page context, connect wallet event log * add received_swap_quote property * fix typo * respond to cmcewen comments * respond to vm comments * move trace to app.tsx from header * respond to vm comments * build: change project name to @uniswap/interface (#4125) * fix: increase celo blocksPerFetch to 5 to improve interface performance (#4130) * init commit * revert yarn.lock changes * update test snapshots * build: lock jest-styled-components@7.0.7 (#4132) * fix: don't toggle desktop NetworkSelector on click (#4134) fix: don't NetworkSelector onClick on desktop * chore: access router data with hooks (#4121) * chore: access router data with hooks * chore: clean RouteComponentProps * chore: use children instead of render * add import * test: fix swap test flake (#4135) * remove all the funky logic * clear stuff * uncomment some tests * remove expert mode tests * skip these tests again, smh * fix: sync chain query parameter (#4019) * replaceURLChain * reorder stuff * don't use usePrevious for previousChainId * remove the replace param call in promise * variable names * comment * confirm isActive * wrong place for isActive * change ret type * add comments * check if not previous chain id * fix: unused onClickOutside reference (#4140) * refactor: clean floating Route (#4144) * fix: increase Polygon gas limit (#3882) * Update graph link * Add Gas over ride temp for Polygon * removal of personal tweaks * Update index.tsx * reset to original file * missing EOL * Update useClientSideV3Trade.ts * remove space * fix: add celo gas override (#4147) fix: add celo gas override to circumvent 'out of gas' error from multicall * build: add global jest-styled-components config (#4148) * add test.config.ts * don't need per file * comment * ts -> js * rm test.config.js? * update snapshots * update jest-styled-components * style: Adds "deprecated_" prefix to all non-theme colors (#4146) * Add deprecated_ label to white and black * Add deprecated_ label to text1 through text5 * Add deprecated_ label for bg0 through bg6 * Add deprecated_ prefix to remaining colors * Add deprecated_ prefix to direct style references * Add deprecated_ prefix to all remaining colors * Update link color * Fix 'deprecated_white' -> theme.deprecated_white * Update snapshots * style: updating ui on unsupported network (#4138) * initial changes * disabled all swap ui buttons when on unsupported chain * implementing Cal's requests to change sizing and copy on pools * updated snapshots * reverted changed snapshots * updated unsupported network test * fixing deprecated colors missing * build: only test for highest yarn-deduplicate strategy (#4154) * build: only test for highest yarn-deduplicate strategy * remove exclusions * fix: fix swap details expanded not working on local build (#4156) fix swap details expanded not working * refactor: remove unused SwapPoolTabs (#4159) * chore: clean useless code * clean unused code * chore(i18n): new Crowdin translations (#4155) chore(i18n): synchronize translations from crowdin [skip ci] Co-authored-by: Crowdin Bot <support+bot@crowdin.com> * feat: implement other swap events part 1 (#4151) * init commit * fix prettier errors * check node env in vercel * add shouldLogImpression to TraceEvent * chore: upgrade cypress (#4161) * chore: upgrade cypress * 10.3.1 * feat: add updated theme colors (#4141) * add colors * Update settings.json * Update settings.json * remove comments * rename * feat: Web 214 implement the main submit swap event (#4061) * init commit * add amplitude ts sdk to package.json * add more comments and documentation * respond to vm comments * respond to cmcewen comments * fix: remove unused constants * init commit * adapt to web * add optional event properties to trace * correct telemetry to analytics * init commit * change telemetry to analytics in doc * init commit * fix: respond to cmcewen comments + initialize analytics in app.tsx + add missing return statement * add element name constant * init commit * correct price_impact calculation * resolve vm comments * fixes in response to comments * respond to vm * use ALL significant digits for token amounts * create helper function getPriceImpactPercentageNumber * 4 decimal points for percentages * change percentage to basis points units * feat: implement swap quote received event (#4165) * init commit * add amplitude ts sdk to package.json * add more comments and documentation * respond to vm comments * respond to cmcewen comments * fix: remove unused constants * init commit * adapt to web * add optional event properties to trace * correct telemetry to analytics * init commit * change telemetry to analytics in doc * init commit * fix: respond to cmcewen comments + initialize analytics in app.tsx + add missing return statement * add element name constant * init commit * correct price_impact calculation * resolve vm comments * fixes in response to comments * respond to vm * use ALL significant digits for token amounts * init commit * logged all properties * create helper function getPriceImpactPercentageNumber * 4 decimal points for percentages * price percentage fn * only log event on FIRST price fetch * respond to cmcewen comments * fix: scroll to top only when pathname changes (#4180) * fix: Update V2 Pool Document link (#4188) Update V2 Pool Document link Current link line 163 point to old documents, and gives error page changed to https://docs.uniswap.org/protocol/V2/concepts/core-concepts/pools current version of pool documents for V2 * fix: updated external docs link for Propose (#4186) FIxed Propose External Docs Link * chore: upgrade react-router-dom to v6 (#4143) * chore: upgrade react-router-dom to v6 * migrate Redirect to Navigate * use Routes instead of Switch * migrate useHistory to useNavigate * use To type * use element * work around activeClassName * fix typing for useParams * deduplicate * fix Navigate * add e2e tests * visit /swap directly Co-authored-by: Vignesh Mohankumar <me@vig.xyz> * style: Add Deprecated prefix to ThemedText components (#4192) * Add Deprecated prefix to ThemedText components * Fix lint errors * fix: update critical red HEX (#4191) change red * feat: Web 262 user model custom properties first PR (#4190) * init commit * abstract amplitude stuff away to separate function * feat: remaining swap events (#4169) * init commit * add amplitude ts sdk to package.json * add more comments and documentation * respond to vm comments * respond to cmcewen comments * fix: remove unused constants * init commit * adapt to web * add optional event properties to trace * correct telemetry to analytics * init commit * change telemetry to analytics in doc * init commit * fix: respond to cmcewen comments + initialize analytics in app.tsx + add missing return statement * add element name constant * init commit * correct price_impact calculation * resolve vm comments * fixes in response to comments * respond to vm * use ALL significant digits for token amounts * init commit * logged all properties * create helper function getPriceImpactPercentageNumber * 4 decimal points for percentages * price percentage fn * only log event on FIRST price fetch * init commit * add swap transaction completed event * respond to cmcewen comments * add two events * remove console.logs * move transaction completed logging to reducer * simplify and remove unnecessary logic and constants * respond to cmcewen comments * respond to cmcewen comments * respond to vm comment * feat: add time / duration based event properties to swap events (#4193) * init commit * remove absolute value in date calc * all the events are now logged properly plus changed native token address to NATIVE * add documentation line * remove unnecessary prop * respond to vm comments * merge and rename util method * respond to vm comments again * feat: fetch stablecoin price with SOR, PI warning (#4217) * feat: fetch stablecoins price with SOR, PI warning * calculate realized price impact * remove unrelated changes * dupe import * pr feedback * use the same calculation function for PI * use proper var * feat: update unsupported token list (#4219) * feat: new swap events and properties in taxonomy (#4204) * init commit * remove absolute value in date calc * all the events are now logged properly plus changed native token address to NATIVE * add documentation line * remove unnecessary prop * init * add approve token event * fix build * add route event properties * fix build * respond to vm comments * respond to vm comments * remove routes properties * feat(risk): tune down cache (#4208) * tune down cache from 7 days to 12 hours * minimal cache time * fix: hide text cursor on network selector hover (#4249) Dont' show text cursor when hovering over network dropdown text * feat: initial FeatureFlagProvider (#4248) * initial * add to index * show more logic * split up * nvm combine * combine more * loading state for the app * no conditional * rm var * comment * move comment * add control specifically * feat: amplitude logs is_reconnect (#4214) * modified redux state to track wallet connections to properly log reconnects * linted and removed console.log * fixes for lynn's comments + documenting * fix: update SOR to refundETH on high price impact ETH trades (#4251) fix lock * feat: theme color updates under feature flag (#4252) * toggle * fixed position * im bad at spelling * rm button * fix * add feature flag * naming * rm blue5 * uppercase * rm file * attempting to resolve some theme/unused var issues Co-authored-by: Anas Yousef <anas.y0807@gmail.com> Co-authored-by: Vignesh Mohankumar <vignesh@vigneshmohankumar.com> Co-authored-by: Jesse <31524583+Jesse-Sawa@users.noreply.github.com> Co-authored-by: Zach Pomerantz <zzmp@uniswap.org> Co-authored-by: Kaylee George <62825936+kayleegeorge@users.noreply.github.com> Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com> Co-authored-by: Bruno Crosier <bruno.crosier@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Crowdin Bot <support+bot@crowdin.com> Co-authored-by: lynn <41491154+lynnshaoyu@users.noreply.github.com> Co-authored-by: Noah Zinsmeister <noahwz@gmail.com> Co-authored-by: Sam Chen <chenxsan@gmail.com> Co-authored-by: Rachel-Eichenberger <60412342+Rachel-Eichenberger@users.noreply.github.com> Co-authored-by: Daniel James <danielcolinjames@gmail.com> Co-authored-by: Akshit Choudhary <akshitchoudhary007@gmail.com> Co-authored-by: Vignesh Mohankumar <me@vig.xyz> Co-authored-by: Connor McEwen <connor.mcewen@gmail.com> Co-authored-by: matteenm <105068213+matteenm@users.noreply.github.com> Co-authored-by: David Walsh <davidwalsh83@gmail.com> Co-authored-by: Emily Williams <emag3m@gmail.com> * patch yarn.lock * chore: merge main into explore (#4264) merge main * fix: use absolute path for TokenRow Link (#4266) * feat: feature flag for explore (#4265) * deduplicate yarn.lock * build: default enabled flag on local (#4267) default flag on local * chore: Revert "chore: add craco and vanilla extract libraries (#4100)" (#4269) Revert "chore: add craco and vanilla extract libraries (#4100)" This reverts commit fa284d85f1c3cc9f9d276d2e0207ebc1fc5de656. * feat: Token safety labels/speedbumps (#4200) * setup warning modal * modal pops up on direct link to token details * updated styles based on fred's review, fixed error where token safety was innacurate on first site visit * test: updating snapshot changed by token safety (#4272) updated snapshot changed by token safety merge * refactor: moved token detail price into chart (#4274) * moved token price and delta into chart, expanded hoverability for crosshair * fix: update theme color files on explore (#4277) fix theme * fix: theme add hover state and flyout colors (#4279) * add flyout * fix hover * feat(token-details): lazy load some heavy stuff (#4282) * chore: merge main into explore (#4281) merge main into explore * feat: token balances across networks -- footer view for token details page (#4194) * initial * initial footer * network balances * alphabetize * add smallest media breakpoint * Update colors.ts * rm console log * add loading and error state * fix multiple vs single * updates * updates * fix * Update settings.json * import fix * test: update snapshots based on color change (#4287) * lint errors * build: declare d3-curve-circlecorners types (#4288) * fix: merging explore to main nits (#4289) nits Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com> Co-authored-by: matteenm <105068213+matteenm@users.noreply.github.com> Co-authored-by: Vignesh Mohankumar <vignesh@vigneshmohankumar.com> Co-authored-by: Charles Bachmeier <charles@bachmeier.io> Co-authored-by: Anas Yousef <anas.y0807@gmail.com> Co-authored-by: Jesse <31524583+Jesse-Sawa@users.noreply.github.com> Co-authored-by: Zach Pomerantz <zzmp@uniswap.org> Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com> Co-authored-by: Bruno Crosier <bruno.crosier@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Crowdin Bot <support+bot@crowdin.com> Co-authored-by: lynn <41491154+lynnshaoyu@users.noreply.github.com> Co-authored-by: Noah Zinsmeister <noahwz@gmail.com> Co-authored-by: Charles Bachmeier <charlie@genie.xyz> Co-authored-by: Lynn Yu <lynn.yu@uniswap.org> Co-authored-by: Vignesh Mohankumar <me@vig.xyz> Co-authored-by: Sam Chen <chenxsan@gmail.com> Co-authored-by: Rachel-Eichenberger <60412342+Rachel-Eichenberger@users.noreply.github.com> Co-authored-by: Daniel James <danielcolinjames@gmail.com> Co-authored-by: Akshit Choudhary <akshitchoudhary007@gmail.com> Co-authored-by: Connor McEwen <connor.mcewen@gmail.com> Co-authored-by: David Walsh <davidwalsh83@gmail.com> Co-authored-by: Emily Williams <emag3m@gmail.com>
This commit is contained in:
parent
8ce599790f
commit
25ea7f9caf
10
package.json
10
package.json
@ -135,6 +135,12 @@
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"@uniswap/v3-sdk": "^3.9.0",
|
||||
"@visx/axis": "^2.12.2",
|
||||
"@visx/event": "^2.6.0",
|
||||
"@visx/glyph": "^2.10.0",
|
||||
"@visx/group": "^2.10.0",
|
||||
"@visx/responsive": "^2.10.0",
|
||||
"@visx/shape": "^2.11.1",
|
||||
"@walletconnect/ethereum-provider": "1.7.1",
|
||||
"@web3-react/coinbase-wallet": "^8.0.34-beta.0",
|
||||
"@web3-react/core": "^8.0.35-beta.0",
|
||||
@ -151,7 +157,8 @@
|
||||
"array.prototype.flatmap": "^1.2.4",
|
||||
"cids": "^1.0.0",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"d3": "^7.0.0",
|
||||
"d3": "^7.6.1",
|
||||
"d3-curve-circlecorners": "^0.1.6",
|
||||
"ethers": "^5.1.4",
|
||||
"firebase": "^9.1.3",
|
||||
"fortmatic": "^2.4.0",
|
||||
@ -165,6 +172,7 @@
|
||||
"multicodec": "^3.0.1",
|
||||
"multihashes": "^4.0.2",
|
||||
"node-vibrant": "^3.2.1-alpha.1",
|
||||
"numbro": "^2.3.6",
|
||||
"polished": "^3.3.2",
|
||||
"polyfill-object.fromentries": "^1.0.1",
|
||||
"popper-max-size-modifier": "^0.2.0",
|
||||
|
1
src/assets/svg/search.svg
Normal file
1
src/assets/svg/search.svg
Normal file
@ -0,0 +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-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
After Width: | Height: | Size: 303 B |
3
src/assets/svg/tooltip_triangle.svg
Normal file
3
src/assets/svg/tooltip_triangle.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="7" viewBox="0 0 12 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.4 1.13333C5.2 0.0666671 6.8 0.0666668 7.6 1.13333L12 7H0L4.4 1.13333Z" fill="#0D0E0E"/>
|
||||
</svg>
|
After Width: | Height: | Size: 201 B |
4
src/assets/svg/verified.svg
Normal file
4
src/assets/svg/verified.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.24453 18.0887C3.24331 19.0467 3.47372 19.7558 3.93576 20.2158C4.39658 20.6771 5.09574 20.904 6.03326 20.8967H8.11975C8.20693 20.8934 8.29386 20.9079 8.37521 20.9395C8.45656 20.9711 8.53062 21.019 8.5928 21.0802L10.0779 22.5484C10.7527 23.2226 11.4139 23.5578 12.0617 23.5541C12.7096 23.5504 13.3709 23.2152 14.0456 22.5484L15.5124 21.0802C15.5767 21.0182 15.6529 20.97 15.7365 20.9385C15.82 20.9069 15.9091 20.8927 15.9982 20.8967H18.0719C19.0192 20.8979 19.7251 20.6673 20.1896 20.2048C20.6541 19.7423 20.8864 19.0333 20.8864 18.0777V16.0021C20.8816 15.8222 20.9474 15.6476 21.0697 15.5157L22.5365 14.0475C23.2198 13.3758 23.559 12.7145 23.5541 12.0636C23.5492 11.4127 23.21 10.7508 22.5365 10.0779L21.0697 8.6097C20.9471 8.47802 20.8812 8.30329 20.8864 8.12336V6.04769C20.8851 5.09092 20.6547 4.3819 20.1951 3.92064C19.7355 3.45939 19.0278 3.22875 18.0719 3.22875H15.9982C15.9091 3.23242 15.8201 3.21807 15.7366 3.18653C15.6532 3.155 15.5769 3.10694 15.5124 3.04523L14.0456 1.57703C13.3709 0.902883 12.7096 0.567648 12.0617 0.571319C11.4139 0.574989 10.7527 0.910224 10.0779 1.57703L8.5928 3.04523C8.53043 3.10622 8.45638 3.15393 8.37508 3.18547C8.29377 3.21701 8.20689 3.23173 8.11975 3.22875H6.03326C5.08718 3.22998 4.38373 3.45877 3.92291 3.91513C3.4621 4.3715 3.23168 5.08235 3.23168 6.04769V8.12887C3.23683 8.3088 3.17096 8.48352 3.04833 8.6152L1.58154 10.0834C0.908042 10.7551 0.571289 11.417 0.571289 12.0691C0.571289 12.7213 0.912332 13.3844 1.59439 14.0585L3.06118 15.5267C3.18346 15.6586 3.24928 15.8332 3.24453 16.0131V18.0887Z" fill="#4C82FB"/>
|
||||
<path d="M11.996 15.9909C11.7795 16.3208 11.4599 16.5064 11.0887 16.5064C10.7072 16.5064 10.4083 16.3517 10.1299 15.9909L7.69677 13.0216C7.5215 12.8051 7.42871 12.5783 7.42871 12.3309C7.42871 11.8154 7.82049 11.4133 8.32567 11.4133C8.63497 11.4133 8.8824 11.5267 9.12984 11.8463L11.0475 14.2897L15.1199 7.75329C15.3364 7.40275 15.6147 7.23779 15.924 7.23779C16.4086 7.23779 16.8622 7.57802 16.8622 8.0832C16.8622 8.32033 16.7385 8.56777 16.6045 8.78427L11.996 15.9909Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
1
src/assets/svg/x.svg
Normal file
1
src/assets/svg/x.svg
Normal file
@ -0,0 +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>
|
After Width: | Height: | Size: 294 B |
@ -8,6 +8,7 @@ export enum EventName {
|
||||
APP_LOADED = 'Application Loaded',
|
||||
APPROVE_TOKEN_TXN_SUBMITTED = 'Approve Token Transaction Submitted',
|
||||
CONNECT_WALLET_BUTTON_CLICKED = 'Connect Wallet Button Clicked',
|
||||
EXPLORE_TOKEN_ROW_CLICKED = 'Explore Token Row Clicked',
|
||||
PAGE_VIEWED = 'Page Viewed',
|
||||
SWAP_AUTOROUTER_VISUALIZATION_EXPANDED = 'Swap Autorouter Visualization Expanded',
|
||||
SWAP_DETAILS_EXPANDED = 'Swap Details Expanded',
|
||||
|
177
src/components/Charts/PriceChart.tsx
Normal file
177
src/components/Charts/PriceChart.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import { localPoint } from '@visx/event'
|
||||
import { EventType } from '@visx/event/lib/types'
|
||||
import { GlyphCircle } from '@visx/glyph'
|
||||
import { Group } from '@visx/group'
|
||||
import { Line, LinePath } from '@visx/shape'
|
||||
import { bisect, scaleLinear } from 'd3'
|
||||
import { radius } from 'd3-curve-circlecorners'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { ArrowDownRight, ArrowUpRight } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import data from './data.json'
|
||||
|
||||
type PricePoint = { value: number; timestamp: number }
|
||||
|
||||
function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
|
||||
const prices = pricePoints.map((x) => x.value)
|
||||
const min = Math.min(...prices)
|
||||
const max = Math.max(...prices)
|
||||
return [min, max]
|
||||
}
|
||||
|
||||
const StyledUpArrow = styled(ArrowUpRight)`
|
||||
color: ${({ theme }) => theme.accentSuccess};
|
||||
`
|
||||
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
|
||||
|
||||
const formattedDelta = delta.toFixed(2) + '%'
|
||||
if (isPositive) {
|
||||
return ['+' + formattedDelta, <StyledUpArrow size={16} key="arrow-up" />]
|
||||
} else if (delta === 0) {
|
||||
return [formattedDelta, null]
|
||||
}
|
||||
return [formattedDelta, <StyledDownArrow size={16} key="arrow-down" />]
|
||||
}
|
||||
|
||||
export const ChartWrapper = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
export const ChartHeader = styled.div`
|
||||
position: absolute;
|
||||
`
|
||||
|
||||
export const TokenPrice = styled.span`
|
||||
font-size: 36px;
|
||||
line-height: 44px;
|
||||
`
|
||||
export const DeltaContainer = styled.div`
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
const ArrowCell = styled.div`
|
||||
padding-left: 2px;
|
||||
display: flex;
|
||||
`
|
||||
|
||||
interface PriceChartProps {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export function PriceChart({ width, height }: PriceChartProps) {
|
||||
const margin = { top: 80, bottom: 20, crosshair: 72 }
|
||||
// defining inner measurements
|
||||
const innerHeight = height - margin.top - margin.bottom
|
||||
const theme = useTheme()
|
||||
|
||||
const pricePoints = data.priceHistory
|
||||
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)
|
||||
|
||||
// Defining scales
|
||||
// x scale
|
||||
const timeScale = scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, width])
|
||||
|
||||
// y scale
|
||||
const rdScale = scaleLinear().domain(getPriceBounds(pricePoints)).range([innerHeight, 0])
|
||||
|
||||
const handleHover = useCallback(
|
||||
(event: Element | EventType) => {
|
||||
const { x } = localPoint(event) || { x: 0 }
|
||||
const x0 = timeScale.invert(x) // get timestamp from the scale
|
||||
const index = bisect(
|
||||
data.priceHistory.map((x) => x.timestamp),
|
||||
x0,
|
||||
1
|
||||
)
|
||||
|
||||
const d0 = data.priceHistory[index - 1]
|
||||
const d1 = data.priceHistory[index]
|
||||
let pricePoint = d0
|
||||
|
||||
const hasPreviousData = d1 && d1.timestamp
|
||||
if (hasPreviousData) {
|
||||
pricePoint = x0.valueOf() - d0.timestamp.valueOf() > d1.timestamp.valueOf() - x0.valueOf() ? d1 : d0
|
||||
}
|
||||
|
||||
setSelected({ pricePoint, xCoordinate: x })
|
||||
},
|
||||
[timeScale]
|
||||
)
|
||||
|
||||
const [delta, arrow] = getDelta(startingPrice.value, selected.pricePoint.value)
|
||||
|
||||
return (
|
||||
<ChartWrapper>
|
||||
<ChartHeader>
|
||||
<TokenPrice>${selected.pricePoint.value.toFixed(2)}</TokenPrice>
|
||||
<DeltaContainer>
|
||||
{delta}
|
||||
<ArrowCell>{arrow}</ArrowCell>
|
||||
</DeltaContainer>
|
||||
</ChartHeader>
|
||||
<svg width={width} height={height}>
|
||||
{selected.xCoordinate && (
|
||||
<g>
|
||||
<Line
|
||||
from={{ x: selected.xCoordinate, y: margin.crosshair }}
|
||||
to={{ x: selected.xCoordinate, y: height }}
|
||||
stroke={'#99A1BD3D'}
|
||||
strokeWidth={1}
|
||||
pointerEvents="none"
|
||||
strokeDasharray="4,4"
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
<Group top={margin.top}>
|
||||
<LinePath
|
||||
curve={radius(1)}
|
||||
stroke={theme.accentActive}
|
||||
strokeWidth={2}
|
||||
data={data.priceHistory}
|
||||
x={(d: PricePoint) => timeScale(d.timestamp) ?? 0}
|
||||
y={(d: PricePoint) => rdScale(d.value) ?? 0}
|
||||
/>
|
||||
{selected.xCoordinate && (
|
||||
<g>
|
||||
<GlyphCircle
|
||||
left={selected.xCoordinate}
|
||||
top={rdScale(selected.pricePoint.value)}
|
||||
size={50}
|
||||
fill={theme.accentActive}
|
||||
stroke={theme.backgroundOutline}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
</Group>
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={width}
|
||||
height={height}
|
||||
fill={'transparent'}
|
||||
onTouchStart={handleHover}
|
||||
onTouchMove={handleHover}
|
||||
onMouseMove={handleHover}
|
||||
onMouseLeave={() => setSelected(initialState)}
|
||||
/>
|
||||
</svg>
|
||||
</ChartWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default PriceChart
|
680
src/components/Charts/data.json
Normal file
680
src/components/Charts/data.json
Normal file
@ -0,0 +1,680 @@
|
||||
{
|
||||
"priceHistory": [
|
||||
{
|
||||
"timestamp": 1658782873,
|
||||
"value": 1525.9201755187335
|
||||
},
|
||||
{
|
||||
"timestamp": 1658786491,
|
||||
"value": 1519.8295371691217
|
||||
},
|
||||
{
|
||||
"timestamp": 1658790066,
|
||||
"value": 1483.5590930942979
|
||||
},
|
||||
{
|
||||
"timestamp": 1658793633,
|
||||
"value": 1450.4822852139405
|
||||
},
|
||||
{
|
||||
"timestamp": 1658797330,
|
||||
"value": 1420.792145662317
|
||||
},
|
||||
{
|
||||
"timestamp": 1658800857,
|
||||
"value": 1423.5078080124129
|
||||
},
|
||||
{
|
||||
"timestamp": 1658804474,
|
||||
"value": 1422.6763937046176
|
||||
},
|
||||
{
|
||||
"timestamp": 1658808024,
|
||||
"value": 1434.3617035058262
|
||||
},
|
||||
{
|
||||
"timestamp": 1658811615,
|
||||
"value": 1431.2771658285428
|
||||
},
|
||||
{
|
||||
"timestamp": 1658815299,
|
||||
"value": 1428.2600217406002
|
||||
},
|
||||
{
|
||||
"timestamp": 1658818900,
|
||||
"value": 1418.5922298226747
|
||||
},
|
||||
{
|
||||
"timestamp": 1658822493,
|
||||
"value": 1413.8693295479209
|
||||
},
|
||||
{
|
||||
"timestamp": 1658826054,
|
||||
"value": 1416.2538720991788
|
||||
},
|
||||
{
|
||||
"timestamp": 1658829639,
|
||||
"value": 1423.7205417844457
|
||||
},
|
||||
{
|
||||
"timestamp": 1658833239,
|
||||
"value": 1413.3042696484272
|
||||
},
|
||||
{
|
||||
"timestamp": 1658836851,
|
||||
"value": 1413.1643118077432
|
||||
},
|
||||
{
|
||||
"timestamp": 1658840434,
|
||||
"value": 1393.0468880056253
|
||||
},
|
||||
{
|
||||
"timestamp": 1658844078,
|
||||
"value": 1389.1088471860737
|
||||
},
|
||||
{
|
||||
"timestamp": 1658847682,
|
||||
"value": 1390.3399193455411
|
||||
},
|
||||
{
|
||||
"timestamp": 1658851301,
|
||||
"value": 1372.3903885322657
|
||||
},
|
||||
{
|
||||
"timestamp": 1658854898,
|
||||
"value": 1378.752061518675
|
||||
},
|
||||
{
|
||||
"timestamp": 1658858467,
|
||||
"value": 1383.4425675355733
|
||||
},
|
||||
{
|
||||
"timestamp": 1658862125,
|
||||
"value": 1374.810072223161
|
||||
},
|
||||
{
|
||||
"timestamp": 1658865709,
|
||||
"value": 1367.9330311429242
|
||||
},
|
||||
{
|
||||
"timestamp": 1658869311,
|
||||
"value": 1379.6979185860337
|
||||
},
|
||||
{
|
||||
"timestamp": 1658872957,
|
||||
"value": 1379.1902350117707
|
||||
},
|
||||
{
|
||||
"timestamp": 1658876454,
|
||||
"value": 1407.5842076237182
|
||||
},
|
||||
{
|
||||
"timestamp": 1658880105,
|
||||
"value": 1445.7282048859022
|
||||
},
|
||||
{
|
||||
"timestamp": 1658883717,
|
||||
"value": 1439.5606799925856
|
||||
},
|
||||
{
|
||||
"timestamp": 1658887328,
|
||||
"value": 1435.8974758296513
|
||||
},
|
||||
{
|
||||
"timestamp": 1658890916,
|
||||
"value": 1438.1636783089357
|
||||
},
|
||||
{
|
||||
"timestamp": 1658894455,
|
||||
"value": 1425.3821586063548
|
||||
},
|
||||
{
|
||||
"timestamp": 1658898044,
|
||||
"value": 1437.5009616628586
|
||||
},
|
||||
{
|
||||
"timestamp": 1658901712,
|
||||
"value": 1453.9488959474504
|
||||
},
|
||||
{
|
||||
"timestamp": 1658905242,
|
||||
"value": 1451.345745271787
|
||||
},
|
||||
{
|
||||
"timestamp": 1658908825,
|
||||
"value": 1463.04463442291
|
||||
},
|
||||
{
|
||||
"timestamp": 1658912493,
|
||||
"value": 1466.9815210024062
|
||||
},
|
||||
{
|
||||
"timestamp": 1658916065,
|
||||
"value": 1460.8901718141108
|
||||
},
|
||||
{
|
||||
"timestamp": 1658919670,
|
||||
"value": 1461.3577381747023
|
||||
},
|
||||
{
|
||||
"timestamp": 1658923242,
|
||||
"value": 1466.4111347109824
|
||||
},
|
||||
{
|
||||
"timestamp": 1658926957,
|
||||
"value": 1480.310794051447
|
||||
},
|
||||
{
|
||||
"timestamp": 1658930459,
|
||||
"value": 1491.8949421945376
|
||||
},
|
||||
{
|
||||
"timestamp": 1658934083,
|
||||
"value": 1492.01598554772
|
||||
},
|
||||
{
|
||||
"timestamp": 1658937666,
|
||||
"value": 1489.4233830835992
|
||||
},
|
||||
{
|
||||
"timestamp": 1658941251,
|
||||
"value": 1500.0252631106548
|
||||
},
|
||||
{
|
||||
"timestamp": 1658944907,
|
||||
"value": 1502.2792018134394
|
||||
},
|
||||
{
|
||||
"timestamp": 1658948537,
|
||||
"value": 1574.81503876036
|
||||
},
|
||||
{
|
||||
"timestamp": 1658952060,
|
||||
"value": 1588.4019278854032
|
||||
},
|
||||
{
|
||||
"timestamp": 1658955719,
|
||||
"value": 1603.0725259676824
|
||||
},
|
||||
{
|
||||
"timestamp": 1658959326,
|
||||
"value": 1609.7670645389308
|
||||
},
|
||||
{
|
||||
"timestamp": 1658962806,
|
||||
"value": 1626.0144460099953
|
||||
},
|
||||
{
|
||||
"timestamp": 1658966430,
|
||||
"value": 1636.9491814677851
|
||||
},
|
||||
{
|
||||
"timestamp": 1658970066,
|
||||
"value": 1617.2751296328988
|
||||
},
|
||||
{
|
||||
"timestamp": 1658973675,
|
||||
"value": 1614.9644510511373
|
||||
},
|
||||
{
|
||||
"timestamp": 1658977302,
|
||||
"value": 1667.687165929466
|
||||
},
|
||||
{
|
||||
"timestamp": 1658980874,
|
||||
"value": 1648.3956682162782
|
||||
},
|
||||
{
|
||||
"timestamp": 1658984493,
|
||||
"value": 1637.5197791538285
|
||||
},
|
||||
{
|
||||
"timestamp": 1658988065,
|
||||
"value": 1647.04528851572
|
||||
},
|
||||
{
|
||||
"timestamp": 1658991686,
|
||||
"value": 1639.555191525111
|
||||
},
|
||||
{
|
||||
"timestamp": 1658995241,
|
||||
"value": 1618.5590870637554
|
||||
},
|
||||
{
|
||||
"timestamp": 1658998876,
|
||||
"value": 1626.733798561607
|
||||
},
|
||||
{
|
||||
"timestamp": 1659002491,
|
||||
"value": 1617.723872882873
|
||||
},
|
||||
{
|
||||
"timestamp": 1659006017,
|
||||
"value": 1624.2509761249412
|
||||
},
|
||||
{
|
||||
"timestamp": 1659009713,
|
||||
"value": 1637.9171561898722
|
||||
},
|
||||
{
|
||||
"timestamp": 1659013289,
|
||||
"value": 1634.3081519264022
|
||||
},
|
||||
{
|
||||
"timestamp": 1659016876,
|
||||
"value": 1619.654599458442
|
||||
},
|
||||
{
|
||||
"timestamp": 1659020478,
|
||||
"value": 1645.6351548789594
|
||||
},
|
||||
{
|
||||
"timestamp": 1659024085,
|
||||
"value": 1728.9127994491312
|
||||
},
|
||||
{
|
||||
"timestamp": 1659027649,
|
||||
"value": 1727.2419770211789
|
||||
},
|
||||
{
|
||||
"timestamp": 1659031296,
|
||||
"value": 1747.723790854222
|
||||
},
|
||||
{
|
||||
"timestamp": 1659034886,
|
||||
"value": 1724.4682037331775
|
||||
},
|
||||
{
|
||||
"timestamp": 1659038416,
|
||||
"value": 1724.3985295332625
|
||||
},
|
||||
{
|
||||
"timestamp": 1659042059,
|
||||
"value": 1759.4960202713903
|
||||
},
|
||||
{
|
||||
"timestamp": 1659045667,
|
||||
"value": 1739.5188454302136
|
||||
},
|
||||
{
|
||||
"timestamp": 1659049362,
|
||||
"value": 1745.7650110588763
|
||||
},
|
||||
{
|
||||
"timestamp": 1659052895,
|
||||
"value": 1725.577697180815
|
||||
},
|
||||
{
|
||||
"timestamp": 1659056497,
|
||||
"value": 1725.582484862776
|
||||
},
|
||||
{
|
||||
"timestamp": 1659060077,
|
||||
"value": 1710.9037186176313
|
||||
},
|
||||
{
|
||||
"timestamp": 1659063685,
|
||||
"value": 1711.8391109264646
|
||||
},
|
||||
{
|
||||
"timestamp": 1659067265,
|
||||
"value": 1726.6068635745162
|
||||
},
|
||||
{
|
||||
"timestamp": 1659070897,
|
||||
"value": 1740.335556931605
|
||||
},
|
||||
{
|
||||
"timestamp": 1659074455,
|
||||
"value": 1732.500123876767
|
||||
},
|
||||
{
|
||||
"timestamp": 1659078055,
|
||||
"value": 1724.3521214949503
|
||||
},
|
||||
{
|
||||
"timestamp": 1659081669,
|
||||
"value": 1719.5827492915225
|
||||
},
|
||||
{
|
||||
"timestamp": 1659085305,
|
||||
"value": 1720.2981607194665
|
||||
},
|
||||
{
|
||||
"timestamp": 1659088865,
|
||||
"value": 1724.0430661128873
|
||||
},
|
||||
{
|
||||
"timestamp": 1659092531,
|
||||
"value": 1726.6573807560105
|
||||
},
|
||||
{
|
||||
"timestamp": 1659096109,
|
||||
"value": 1683.9249162304673
|
||||
},
|
||||
{
|
||||
"timestamp": 1659099693,
|
||||
"value": 1669.1474476097915
|
||||
},
|
||||
{
|
||||
"timestamp": 1659103225,
|
||||
"value": 1700.953986672358
|
||||
},
|
||||
{
|
||||
"timestamp": 1659106827,
|
||||
"value": 1733.436973728088
|
||||
},
|
||||
{
|
||||
"timestamp": 1659110456,
|
||||
"value": 1723.9903646371772
|
||||
},
|
||||
{
|
||||
"timestamp": 1659114043,
|
||||
"value": 1688.1297030770547
|
||||
},
|
||||
{
|
||||
"timestamp": 1659117661,
|
||||
"value": 1696.7445002961126
|
||||
},
|
||||
{
|
||||
"timestamp": 1659121335,
|
||||
"value": 1725.4984460435912
|
||||
},
|
||||
{
|
||||
"timestamp": 1659124804,
|
||||
"value": 1722.8827814456286
|
||||
},
|
||||
{
|
||||
"timestamp": 1659128500,
|
||||
"value": 1729.347919095565
|
||||
},
|
||||
{
|
||||
"timestamp": 1659132077,
|
||||
"value": 1713.7962501224094
|
||||
},
|
||||
{
|
||||
"timestamp": 1659135724,
|
||||
"value": 1740.6711356450423
|
||||
},
|
||||
{
|
||||
"timestamp": 1659139323,
|
||||
"value": 1732.43794687956
|
||||
},
|
||||
{
|
||||
"timestamp": 1659142812,
|
||||
"value": 1731.3289938389023
|
||||
},
|
||||
{
|
||||
"timestamp": 1659146422,
|
||||
"value": 1702.2753395122602
|
||||
},
|
||||
{
|
||||
"timestamp": 1659150077,
|
||||
"value": 1714.1140812298127
|
||||
},
|
||||
{
|
||||
"timestamp": 1659153731,
|
||||
"value": 1708.1639368133017
|
||||
},
|
||||
{
|
||||
"timestamp": 1659157303,
|
||||
"value": 1704.1503028631896
|
||||
},
|
||||
{
|
||||
"timestamp": 1659160873,
|
||||
"value": 1705.7727071686102
|
||||
},
|
||||
{
|
||||
"timestamp": 1659164460,
|
||||
"value": 1714.0119624118752
|
||||
},
|
||||
{
|
||||
"timestamp": 1659168054,
|
||||
"value": 1716.362808637293
|
||||
},
|
||||
{
|
||||
"timestamp": 1659171671,
|
||||
"value": 1713.3896984979494
|
||||
},
|
||||
{
|
||||
"timestamp": 1659175324,
|
||||
"value": 1705.1150624871284
|
||||
},
|
||||
{
|
||||
"timestamp": 1659178928,
|
||||
"value": 1689.0055673338316
|
||||
},
|
||||
{
|
||||
"timestamp": 1659182453,
|
||||
"value": 1709.1029480994125
|
||||
},
|
||||
{
|
||||
"timestamp": 1659186029,
|
||||
"value": 1712.7188068035068
|
||||
},
|
||||
{
|
||||
"timestamp": 1659189698,
|
||||
"value": 1730.1945219069557
|
||||
},
|
||||
{
|
||||
"timestamp": 1659193336,
|
||||
"value": 1720.921650298128
|
||||
},
|
||||
{
|
||||
"timestamp": 1659196895,
|
||||
"value": 1738.4957818565333
|
||||
},
|
||||
{
|
||||
"timestamp": 1659200563,
|
||||
"value": 1734.0532077448488
|
||||
},
|
||||
{
|
||||
"timestamp": 1659204128,
|
||||
"value": 1726.2511965293515
|
||||
},
|
||||
{
|
||||
"timestamp": 1659207696,
|
||||
"value": 1727.1774604904306
|
||||
},
|
||||
{
|
||||
"timestamp": 1659211292,
|
||||
"value": 1708.2040569339322
|
||||
},
|
||||
{
|
||||
"timestamp": 1659214869,
|
||||
"value": 1701.2385000283969
|
||||
},
|
||||
{
|
||||
"timestamp": 1659218420,
|
||||
"value": 1703.2036428777258
|
||||
},
|
||||
{
|
||||
"timestamp": 1659222021,
|
||||
"value": 1700.2895560492104
|
||||
},
|
||||
{
|
||||
"timestamp": 1659225620,
|
||||
"value": 1697.2163951060672
|
||||
},
|
||||
{
|
||||
"timestamp": 1659229243,
|
||||
"value": 1704.3775021114698
|
||||
},
|
||||
{
|
||||
"timestamp": 1659232842,
|
||||
"value": 1700.4204754876303
|
||||
},
|
||||
{
|
||||
"timestamp": 1659236437,
|
||||
"value": 1696.5914252859611
|
||||
},
|
||||
{
|
||||
"timestamp": 1659240035,
|
||||
"value": 1694.4481830011448
|
||||
},
|
||||
{
|
||||
"timestamp": 1659243620,
|
||||
"value": 1703.3224446414056
|
||||
},
|
||||
{
|
||||
"timestamp": 1659247307,
|
||||
"value": 1697.8424772021297
|
||||
},
|
||||
{
|
||||
"timestamp": 1659250903,
|
||||
"value": 1700.8224587969446
|
||||
},
|
||||
{
|
||||
"timestamp": 1659254466,
|
||||
"value": 1699.7873428170249
|
||||
},
|
||||
{
|
||||
"timestamp": 1659258143,
|
||||
"value": 1693.6685599914267
|
||||
},
|
||||
{
|
||||
"timestamp": 1659261688,
|
||||
"value": 1699.2427714792027
|
||||
},
|
||||
{
|
||||
"timestamp": 1659265245,
|
||||
"value": 1703.6152066934699
|
||||
},
|
||||
{
|
||||
"timestamp": 1659268916,
|
||||
"value": 1713.3214096347672
|
||||
},
|
||||
{
|
||||
"timestamp": 1659272415,
|
||||
"value": 1718.6008570600566
|
||||
},
|
||||
{
|
||||
"timestamp": 1659276010,
|
||||
"value": 1710.908943989972
|
||||
},
|
||||
{
|
||||
"timestamp": 1659279659,
|
||||
"value": 1706.9337136493657
|
||||
},
|
||||
{
|
||||
"timestamp": 1659283294,
|
||||
"value": 1712.3564006340769
|
||||
},
|
||||
{
|
||||
"timestamp": 1659286891,
|
||||
"value": 1709.6740674282
|
||||
},
|
||||
{
|
||||
"timestamp": 1659290469,
|
||||
"value": 1707.3229025436356
|
||||
},
|
||||
{
|
||||
"timestamp": 1659294046,
|
||||
"value": 1699.3406158125497
|
||||
},
|
||||
{
|
||||
"timestamp": 1659297738,
|
||||
"value": 1719.4076152711061
|
||||
},
|
||||
{
|
||||
"timestamp": 1659301320,
|
||||
"value": 1720.9427440142197
|
||||
},
|
||||
{
|
||||
"timestamp": 1659304851,
|
||||
"value": 1705.1891389481118
|
||||
},
|
||||
{
|
||||
"timestamp": 1659308495,
|
||||
"value": 1674.7321260090307
|
||||
},
|
||||
{
|
||||
"timestamp": 1659312067,
|
||||
"value": 1682.011373614746
|
||||
},
|
||||
{
|
||||
"timestamp": 1659315639,
|
||||
"value": 1692.4709015972671
|
||||
},
|
||||
{
|
||||
"timestamp": 1659319336,
|
||||
"value": 1694.5662110255037
|
||||
},
|
||||
{
|
||||
"timestamp": 1659322868,
|
||||
"value": 1696.6981903223634
|
||||
},
|
||||
{
|
||||
"timestamp": 1659326448,
|
||||
"value": 1690.5297521871978
|
||||
},
|
||||
{
|
||||
"timestamp": 1659330161,
|
||||
"value": 1695.878025817704
|
||||
},
|
||||
{
|
||||
"timestamp": 1659333699,
|
||||
"value": 1689.9406830105013
|
||||
},
|
||||
{
|
||||
"timestamp": 1659337283,
|
||||
"value": 1685.7000111795764
|
||||
},
|
||||
{
|
||||
"timestamp": 1659340802,
|
||||
"value": 1692.3951031976128
|
||||
},
|
||||
{
|
||||
"timestamp": 1659344497,
|
||||
"value": 1684.9023217083284
|
||||
},
|
||||
{
|
||||
"timestamp": 1659348121,
|
||||
"value": 1689.4504425336531
|
||||
},
|
||||
{
|
||||
"timestamp": 1659351735,
|
||||
"value": 1658.7053302279778
|
||||
},
|
||||
{
|
||||
"timestamp": 1659355290,
|
||||
"value": 1669.3473691430552
|
||||
},
|
||||
{
|
||||
"timestamp": 1659358832,
|
||||
"value": 1661.1873135571395
|
||||
},
|
||||
{
|
||||
"timestamp": 1659362418,
|
||||
"value": 1657.2148949274574
|
||||
},
|
||||
{
|
||||
"timestamp": 1659366157,
|
||||
"value": 1686.3973094195583
|
||||
},
|
||||
{
|
||||
"timestamp": 1659369717,
|
||||
"value": 1673.691878986328
|
||||
},
|
||||
{
|
||||
"timestamp": 1659373225,
|
||||
"value": 1656.5687536861142
|
||||
},
|
||||
{
|
||||
"timestamp": 1659376826,
|
||||
"value": 1629.019709947513
|
||||
},
|
||||
{
|
||||
"timestamp": 1659380552,
|
||||
"value": 1622.5174376243767
|
||||
},
|
||||
{
|
||||
"timestamp": 1659384162,
|
||||
"value": 1623.3989005910057
|
||||
},
|
||||
{
|
||||
"timestamp": 1659387469,
|
||||
"value": 1620.1890596719502
|
||||
}
|
||||
]
|
||||
}
|
112
src/components/Explore/TokenDetails/BalanceSummary.tsx
Normal file
112
src/components/Explore/TokenDetails/BalanceSummary.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { getChainInfoOrDefault } from 'constants/chainInfo'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import NetworkBalance from './NetworkBalance'
|
||||
|
||||
const BalancesCard = styled.div`
|
||||
width: 284px;
|
||||
height: fit-content;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
padding: 20px;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border-radius: 12px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
`
|
||||
const ErrorState = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
`
|
||||
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;
|
||||
justify-content: space-between;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
margin-top: 12px;
|
||||
align-items: center;
|
||||
`
|
||||
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 })
|
||||
|
||||
const { chainId: connectedChainId } = useWeb3React()
|
||||
|
||||
const { label: connectedLabel, logoUrl: connectedLogoUrl } = getChainInfoOrDefault(connectedChainId)
|
||||
const connectedFiatValue = 1
|
||||
const multipleBalances = true // for testing purposes
|
||||
|
||||
if (loading) return null
|
||||
return (
|
||||
<BalancesCard>
|
||||
{error ? (
|
||||
<ErrorState>
|
||||
<AlertTriangle size={24} />
|
||||
<ErrorText>
|
||||
<Trans>There was an error loading your {tokenSymbol} 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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</BalancesCard>
|
||||
)
|
||||
}
|
183
src/components/Explore/TokenDetails/FooterBalanceSummary.tsx
Normal file
183
src/components/Explore/TokenDetails/FooterBalanceSummary.tsx
Normal file
@ -0,0 +1,183 @@
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
|
||||
import { useState } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { SMALLEST_MOBILE_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { LoadingBubble } from '../loading'
|
||||
|
||||
const PLACEHOLDER_NAV_FOOTER_HEIGHT = '56px'
|
||||
const BalanceFooter = styled.div`
|
||||
height: fit-content;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border-radius: 20px 20px 0px 0px;
|
||||
padding: 12px 16px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
width: 100%;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: ${PLACEHOLDER_NAV_FOOTER_HEIGHT};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
`
|
||||
const BalanceValue = styled.div`
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`
|
||||
const BalanceTotal = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
`
|
||||
const BalanceInfo = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
`
|
||||
const FakeFooterNavBar = styled.div`
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
background-color: ${({ theme }) => theme.backgroundBackdrop};
|
||||
height: ${PLACEHOLDER_NAV_FOOTER_HEIGHT};
|
||||
width: 100%;
|
||||
align-items: flex-end;
|
||||
padding: 20px 8px;
|
||||
font-size: 10px;
|
||||
`
|
||||
const FiatValue = styled.span`
|
||||
display: flex;
|
||||
align-self: flex-end;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
|
||||
@media only screen and (max-width: ${SMALLEST_MOBILE_MEDIA_BREAKPOINT}) {
|
||||
line-height: 16px;
|
||||
}
|
||||
`
|
||||
const NetworkBalancesSection = styled.div`
|
||||
height: fit-content;
|
||||
border-top: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 0px 8px 0px;
|
||||
margin-top: 16px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
`
|
||||
const NetworkBalancesLabel = styled.span`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
const SwapButton = styled.button`
|
||||
background-color: ${({ theme }) => theme.accentAction};
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
padding: 12px 16px;
|
||||
width: 120px;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
justify-content: center;
|
||||
`
|
||||
const TotalBalancesSection = styled.div`
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
const ViewAll = styled.span`
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
`
|
||||
const ErrorState = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-right: 8px;
|
||||
`
|
||||
const LoadingState = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
`
|
||||
const TopBalanceLoadBubble = styled(LoadingBubble)`
|
||||
height: 12px;
|
||||
width: 172px;
|
||||
`
|
||||
const BottomBalanceLoadBubble = styled(LoadingBubble)`
|
||||
height: 16px;
|
||||
width: 188px;
|
||||
`
|
||||
const ErrorText = styled.span`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
|
||||
export default function FooterBalanceSummary({
|
||||
address,
|
||||
networkBalances,
|
||||
totalBalance,
|
||||
}: {
|
||||
address: string
|
||||
networkBalances: (JSX.Element | null)[] | null
|
||||
totalBalance: number
|
||||
}) {
|
||||
const tokenSymbol = useToken(address)?.symbol
|
||||
const [showMultipleBalances, setShowMultipleBalances] = useState(false)
|
||||
const multipleBalances = false // for testing purposes
|
||||
const networkNameIfOneBalance = 'Ethereum' // for testing purposes
|
||||
const { loading, error } = useNetworkTokenBalances({ address })
|
||||
return (
|
||||
<BalanceFooter>
|
||||
<TotalBalancesSection>
|
||||
{loading ? (
|
||||
<LoadingState>
|
||||
<TopBalanceLoadBubble></TopBalanceLoadBubble>
|
||||
<BottomBalanceLoadBubble></BottomBalanceLoadBubble>
|
||||
</LoadingState>
|
||||
) : error ? (
|
||||
<ErrorState>
|
||||
<AlertTriangle size={17} />
|
||||
<ErrorText>There was an error fetching your balance</ErrorText>
|
||||
</ErrorState>
|
||||
) : (
|
||||
<BalanceInfo>
|
||||
{multipleBalances ? 'Balance on all networks' : `Your balance on ${networkNameIfOneBalance}`}
|
||||
<BalanceTotal>
|
||||
<BalanceValue>
|
||||
{totalBalance} {tokenSymbol}
|
||||
</BalanceValue>
|
||||
<FiatValue>($107, 610.04)</FiatValue>
|
||||
</BalanceTotal>
|
||||
{multipleBalances && (
|
||||
<ViewAll onClick={() => setShowMultipleBalances(!showMultipleBalances)}>
|
||||
{showMultipleBalances ? 'Hide' : 'View'} all balances
|
||||
</ViewAll>
|
||||
)}
|
||||
</BalanceInfo>
|
||||
)}
|
||||
<SwapButton onClick={() => (window.location.href = 'https://app.uniswap.org/#/swap')}>Swap</SwapButton>
|
||||
</TotalBalancesSection>
|
||||
{showMultipleBalances && (
|
||||
<NetworkBalancesSection>
|
||||
<NetworkBalancesLabel>Your balances by network</NetworkBalancesLabel> {networkBalances}
|
||||
</NetworkBalancesSection>
|
||||
)}
|
||||
<FakeFooterNavBar>**leaving space for updated nav footer**</FakeFooterNavBar>
|
||||
</BalanceFooter>
|
||||
)
|
||||
}
|
157
src/components/Explore/TokenDetails/LoadingTokenDetail.tsx
Normal file
157
src/components/Explore/TokenDetails/LoadingTokenDetail.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { ChartWrapper, DeltaContainer, TokenPrice } from '../../Charts/PriceChart'
|
||||
import { LoadingBubble } from '../loading'
|
||||
import {
|
||||
AboutHeader,
|
||||
AboutSection,
|
||||
BreadcrumbNavLink,
|
||||
ChartContainer,
|
||||
ChartHeader,
|
||||
ContractAddressSection,
|
||||
ResourcesContainer,
|
||||
Stat,
|
||||
StatPair,
|
||||
StatsSection,
|
||||
TimeOptionsContainer,
|
||||
TokenInfoContainer,
|
||||
TokenNameCell,
|
||||
TopArea,
|
||||
} from './TokenDetail'
|
||||
|
||||
/* Loading state bubbles */
|
||||
const LoadingDetailBubble = styled(LoadingBubble)`
|
||||
height: 16px;
|
||||
width: 180px;
|
||||
`
|
||||
const TitleLoadingBubble = styled(LoadingDetailBubble)`
|
||||
width: 140px;
|
||||
`
|
||||
const SquareLoadingBubble = styled(LoadingDetailBubble)`
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
margin-top: 4px;
|
||||
`
|
||||
const PriceLoadingBubble = styled(SquareLoadingBubble)`
|
||||
height: 40px;
|
||||
`
|
||||
const LongLoadingBubble = styled(LoadingDetailBubble)`
|
||||
width: 100%;
|
||||
`
|
||||
const HalfLoadingBubble = styled(LoadingDetailBubble)`
|
||||
width: 50%;
|
||||
`
|
||||
const IconLoadingBubble = styled(LoadingDetailBubble)`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
`
|
||||
const StatLoadingBubble = styled(SquareLoadingBubble)`
|
||||
width: 116px;
|
||||
`
|
||||
const StatsLoadingContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
const ChartAnimation = styled.div`
|
||||
display: flex;
|
||||
animation: wave 8s cubic-bezier(0.36, 0.45, 0.63, 0.53) infinite;
|
||||
|
||||
@keyframes wave {
|
||||
0% {
|
||||
margin-left: 0;
|
||||
}
|
||||
100% {
|
||||
margin-left: -800px;
|
||||
}
|
||||
}
|
||||
`
|
||||
const Space = styled.div<{ heightSize: number }>`
|
||||
height: ${({ heightSize }) => `${heightSize}px`};
|
||||
`
|
||||
/* Loading State: row component with loading bubbles */
|
||||
export default function LoadingTokenDetail() {
|
||||
return (
|
||||
<TopArea>
|
||||
<BreadcrumbNavLink to="/explore">
|
||||
<Space heightSize={20} />
|
||||
</BreadcrumbNavLink>
|
||||
<ChartHeader>
|
||||
<TokenInfoContainer>
|
||||
<TokenNameCell>
|
||||
<IconLoadingBubble />
|
||||
<TitleLoadingBubble />
|
||||
</TokenNameCell>
|
||||
</TokenInfoContainer>
|
||||
<ChartContainer>
|
||||
<ChartWrapper>
|
||||
<ChartHeader>
|
||||
<TokenPrice>
|
||||
<PriceLoadingBubble />
|
||||
</TokenPrice>
|
||||
<DeltaContainer>
|
||||
<Space heightSize={20} />
|
||||
</DeltaContainer>
|
||||
</ChartHeader>
|
||||
<ChartAnimation>
|
||||
<svg width="416" height="160" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M 0 80 Q 104 10, 208 80 T 416 80" stroke="#2e3138" fill="transparent" strokeWidth="2" />
|
||||
</svg>
|
||||
<svg width="416" height="160" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M 0 80 Q 104 10, 208 80 T 416 80" stroke="#2e3138" fill="transparent" strokeWidth="2" />
|
||||
</svg>
|
||||
<svg width="416" height="160" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M 0 80 Q 104 10, 208 80 T 416 80" stroke="#2e3138" fill="transparent" strokeWidth="2" />
|
||||
</svg>
|
||||
<svg width="416" height="160" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M 0 80 Q 104 10, 208 80 T 416 80" stroke="#2e3138" fill="transparent" strokeWidth="2" />
|
||||
</svg>
|
||||
<svg width="416" height="160" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M 0 80 Q 104 10, 208 80 T 416 80" stroke="#2e3138" fill="transparent" strokeWidth="2" />
|
||||
</svg>
|
||||
</ChartAnimation>
|
||||
</ChartWrapper>
|
||||
</ChartContainer>
|
||||
<TimeOptionsContainer>
|
||||
<Space heightSize={32} />
|
||||
</TimeOptionsContainer>
|
||||
</ChartHeader>
|
||||
<AboutSection>
|
||||
<AboutHeader>
|
||||
<SquareLoadingBubble />
|
||||
</AboutHeader>
|
||||
<LongLoadingBubble />
|
||||
<LongLoadingBubble />
|
||||
<HalfLoadingBubble />
|
||||
|
||||
<ResourcesContainer>{null}</ResourcesContainer>
|
||||
</AboutSection>
|
||||
<StatsSection>
|
||||
<StatsLoadingContainer>
|
||||
<StatPair>
|
||||
<Stat>
|
||||
<HalfLoadingBubble />
|
||||
<StatLoadingBubble />
|
||||
</Stat>
|
||||
<Stat>
|
||||
<HalfLoadingBubble />
|
||||
<StatLoadingBubble />
|
||||
</Stat>
|
||||
</StatPair>
|
||||
<StatPair>
|
||||
<Stat>
|
||||
<HalfLoadingBubble />
|
||||
<StatLoadingBubble />
|
||||
</Stat>
|
||||
<Stat>
|
||||
<HalfLoadingBubble />
|
||||
<StatLoadingBubble />
|
||||
</Stat>
|
||||
</StatPair>
|
||||
</StatsLoadingContainer>
|
||||
</StatsSection>
|
||||
<ContractAddressSection>{null}</ContractAddressSection>
|
||||
</TopArea>
|
||||
)
|
||||
}
|
65
src/components/Explore/TokenDetails/NetworkBalance.tsx
Normal file
65
src/components/Explore/TokenDetails/NetworkBalance.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
const Balance = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
`
|
||||
const BalanceItem = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
const BalanceRow = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`
|
||||
const Logo = styled.img`
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
margin-right: 8px;
|
||||
`
|
||||
const Network = styled.span<{ color: string }>`
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
color: ${({ color }) => color};
|
||||
`
|
||||
const NetworkBalanceContainer = styled.div`
|
||||
display: flex;
|
||||
padding-top: 16px;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export default function NetworkBalance({
|
||||
logoUrl,
|
||||
balance,
|
||||
tokenSymbol,
|
||||
fiatValue,
|
||||
label,
|
||||
networkColor,
|
||||
}: {
|
||||
logoUrl: string
|
||||
balance: string
|
||||
tokenSymbol: string
|
||||
fiatValue: string | number
|
||||
label: string
|
||||
networkColor: string | undefined
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<NetworkBalanceContainer>
|
||||
<Logo src={logoUrl} />
|
||||
<Balance>
|
||||
<BalanceRow>
|
||||
<BalanceItem>
|
||||
{balance} {tokenSymbol}
|
||||
</BalanceItem>
|
||||
<BalanceItem>${fiatValue}</BalanceItem>
|
||||
</BalanceRow>
|
||||
<Network color={networkColor ?? theme.textPrimary}>{label}</Network>
|
||||
</Balance>
|
||||
</NetworkBalanceContainer>
|
||||
)
|
||||
}
|
27
src/components/Explore/TokenDetails/Resource.tsx
Normal file
27
src/components/Explore/TokenDetails/Resource.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { darken } from 'polished'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink } from 'theme'
|
||||
|
||||
const ResourceLink = styled(ExternalLink)`
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
gap: 4px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: ${({ theme }) => darken(0.1, theme.accentAction)};
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
export default function Resource({ name, link }: { name: string; link: string }) {
|
||||
return (
|
||||
<ResourceLink href={link}>
|
||||
{name}
|
||||
<sup>↗</sup>
|
||||
</ResourceLink>
|
||||
)
|
||||
}
|
139
src/components/Explore/TokenDetails/ShareButton.tsx
Normal file
139
src/components/Explore/TokenDetails/ShareButton.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { darken } from 'polished'
|
||||
import { useRef, useState } from 'react'
|
||||
import { Check, Link, Share, Twitter } from 'react-feather'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
const TWITTER_WIDTH = 560
|
||||
const TWITTER_HEIGHT = 480
|
||||
|
||||
const ShareButtonDisplay = styled.div`
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => darken(0.1, theme.textSecondary)};
|
||||
}
|
||||
`
|
||||
const ShareActions = styled.div`
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
right: 0px;
|
||||
padding: 8px 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
overflow: auto;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
box-shadow: ${({ theme }) => theme.flyoutDropShadow};
|
||||
border-radius: 12px;
|
||||
`
|
||||
const ShareAction = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
gap: 8px;
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.backgroundContainer};
|
||||
}
|
||||
`
|
||||
|
||||
const LinkCopied = styled.div<{ show: boolean }>`
|
||||
display: ${({ show }) => (show ? 'flex' : 'none')};
|
||||
width: 328px;
|
||||
height: 72px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
background-color: ${({ theme }) => theme.backgroundBackdrop};
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 24px 16px;
|
||||
position: absolute;
|
||||
right: 32px;
|
||||
bottom: 32px;
|
||||
font-size: 14px;
|
||||
gap: 8px;
|
||||
border: 1px solid rgba(153, 161, 189, 0.08);
|
||||
box-shadow: ${({ theme }) => theme.flyoutDropShadow};
|
||||
border-radius: 20px;
|
||||
animation: floatIn 0s ease-in 3s forwards;
|
||||
|
||||
@keyframes floatIn {
|
||||
to {
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
interface TokenInfo {
|
||||
tokenName: string
|
||||
tokenSymbol: string
|
||||
}
|
||||
|
||||
export default function ShareButton(tokenInfo: TokenInfo) {
|
||||
const theme = useTheme()
|
||||
const node = useRef<HTMLDivElement | null>(null)
|
||||
const open = useModalIsOpen(ApplicationModal.SHARE)
|
||||
const toggleShare = useToggleModal(ApplicationModal.SHARE)
|
||||
useOnClickOutside(node, open ? toggleShare : undefined)
|
||||
const [showCopied, setShowCopied] = useState(false)
|
||||
const positionX = (window.screen.width - TWITTER_WIDTH) / 2
|
||||
const positionY = (window.screen.height - TWITTER_HEIGHT) / 2
|
||||
|
||||
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`,
|
||||
'newwindow',
|
||||
`left=${positionX}, top=${positionY}, width=${TWITTER_WIDTH}, height=${TWITTER_HEIGHT}`
|
||||
)
|
||||
}
|
||||
const copyLink = () => {
|
||||
navigator.clipboard.writeText(window.location.href).then(
|
||||
function handleClipboardWriteSuccess() {
|
||||
setShowCopied(true)
|
||||
toggleShare()
|
||||
setTimeout(() => setShowCopied(false), 3000)
|
||||
},
|
||||
function error() {
|
||||
console.error('Clipboard copy failed.')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShareButtonDisplay ref={node}>
|
||||
<Share size={18} onClick={toggleShare} aria-label={`ShareOptions`} />
|
||||
{open && (
|
||||
<ShareActions>
|
||||
<ShareAction onClick={copyLink}>
|
||||
<Link color={theme.textSecondary} size={18} />
|
||||
Copy link
|
||||
</ShareAction>
|
||||
|
||||
<ShareAction onClick={shareTweet}>
|
||||
<Twitter color={theme.textSecondary} size={18} />
|
||||
Share to Twitter
|
||||
</ShareAction>
|
||||
</ShareActions>
|
||||
)}
|
||||
</ShareButtonDisplay>
|
||||
<LinkCopied show={showCopied}>
|
||||
<Check color={theme.accentSuccess} />
|
||||
Link Copied
|
||||
</LinkCopied>
|
||||
</>
|
||||
)
|
||||
}
|
310
src/components/Explore/TokenDetails/TokenDetail.tsx
Normal file
310
src/components/Explore/TokenDetails/TokenDetail.tsx
Normal file
@ -0,0 +1,310 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { ParentSize } from '@visx/responsive'
|
||||
import PriceChart from 'components/Charts/PriceChart'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
|
||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
import { useCurrency, useIsUserAddedToken, useToken } from 'hooks/Tokens'
|
||||
import { TimePeriod } from 'hooks/useTopTokens'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { darken } from 'polished'
|
||||
import { useCallback } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { ArrowLeft, Copy, Heart } from 'react-feather'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { MOBILE_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { favoritesAtom, useToggleFavorite } from '../state'
|
||||
import { ClickFavorited } from '../TokenTable/TokenRow'
|
||||
import Resource from './Resource'
|
||||
import ShareButton from './ShareButton'
|
||||
|
||||
const TIME_DISPLAYS: Record<TimePeriod, string> = {
|
||||
[TimePeriod.hour]: '1H',
|
||||
[TimePeriod.day]: '1D',
|
||||
[TimePeriod.week]: '1W',
|
||||
[TimePeriod.month]: '1M',
|
||||
[TimePeriod.year]: '1Y',
|
||||
}
|
||||
const TIME_PERIODS = [TimePeriod.hour, TimePeriod.day, TimePeriod.week, TimePeriod.month, TimePeriod.year]
|
||||
|
||||
export const AboutSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 24px 0px;
|
||||
`
|
||||
export const AboutHeader = styled.span`
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
`
|
||||
export const BreadcrumbNavLink = styled(Link)`
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
text-decoration: none;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
}
|
||||
`
|
||||
export const ChartHeader = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
gap: 4px;
|
||||
margin-bottom: 24px;
|
||||
`
|
||||
const ContractAddress = styled.button`
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => darken(0.1, theme.textPrimary)};
|
||||
}
|
||||
`
|
||||
export const ContractAddressSection = styled.div`
|
||||
padding: 24px 0px;
|
||||
`
|
||||
const Contract = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 14px;
|
||||
gap: 4px;
|
||||
`
|
||||
export const ChartContainer = styled.div`
|
||||
display: flex;
|
||||
height: 404px;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
`
|
||||
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;
|
||||
`
|
||||
const TimeButton = styled.button<{ active: boolean }>`
|
||||
background-color: ${({ theme, active }) => (active ? theme.accentActive : 'transparent')};
|
||||
font-size: 14px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
`
|
||||
export const TimeOptionsContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
`
|
||||
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: 24px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
export const TokenInfoContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
const TokenSymbol = styled.span`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
export const TopArea = styled.div`
|
||||
max-width: 832px;
|
||||
overflow: hidden;
|
||||
`
|
||||
export const ResourcesContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
`
|
||||
const FullAddress = styled.span`
|
||||
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
const TruncatedAddress = styled.span`
|
||||
display: none;
|
||||
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
||||
display: flex;
|
||||
}
|
||||
`
|
||||
const NetworkBadge = styled.div<{ networkColor?: string; backgroundColor?: string }>`
|
||||
border-radius: 5px;
|
||||
padding: 4px 8px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
color: ${({ theme, networkColor }) => networkColor ?? theme.textPrimary};
|
||||
background-color: ${({ theme, backgroundColor }) => backgroundColor ?? theme.backgroundSurface};
|
||||
`
|
||||
|
||||
export default function LoadedTokenDetail({ address }: { address: string }) {
|
||||
const theme = useTheme()
|
||||
const token = useToken(address)
|
||||
const currency = useCurrency(address)
|
||||
const favoriteTokens = useAtomValue<string[]>(favoritesAtom)
|
||||
const [activeTimePeriod, setTimePeriod] = useState(TimePeriod.hour)
|
||||
const isFavorited = favoriteTokens.includes(address)
|
||||
const toggleFavorite = useToggleFavorite(address)
|
||||
const warning = checkWarning(address)
|
||||
const navigate = useNavigate()
|
||||
const isUserAddedToken = useIsUserAddedToken(token)
|
||||
const [warningModalOpen, setWarningModalOpen] = useState(!!warning && !isUserAddedToken)
|
||||
|
||||
const handleDismissWarning = useCallback(() => {
|
||||
setWarningModalOpen(false)
|
||||
}, [setWarningModalOpen])
|
||||
const chainInfo = getChainInfo(token?.chainId)
|
||||
const networkLabel = chainInfo?.label
|
||||
const networkBadgebackgroundColor = chainInfo?.backgroundColor
|
||||
|
||||
// catch token error and loading state
|
||||
if (!token || !token.name || !token.symbol) {
|
||||
return <div>No Token</div>
|
||||
}
|
||||
const tokenName = token.name
|
||||
const tokenSymbol = token.symbol
|
||||
|
||||
// TODO: format price, add sparkline
|
||||
const aboutToken =
|
||||
'Ethereum is a decentralized computing platform that uses ETH (Ether) to pay transaction fees (gas). Developers can use Ethereum to run decentralized applications (dApps) and issue new crypto assets, known as Ethereum tokens.'
|
||||
const tokenMarketCap = '23.02B'
|
||||
const tokenVolume = '1.6B'
|
||||
const truncatedTokenAddress = `${address.slice(0, 4)}...${address.slice(-3)}`
|
||||
|
||||
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="24px" />}
|
||||
{networkBadgebackgroundColor && (
|
||||
<NetworkBadge networkColor={chainInfo?.color} backgroundColor={networkBadgebackgroundColor}>
|
||||
{networkLabel}
|
||||
</NetworkBadge>
|
||||
)}
|
||||
</TokenNameCell>
|
||||
<TokenActions>
|
||||
<ShareButton tokenName={tokenName} tokenSymbol={tokenSymbol} />
|
||||
<ClickFavorited onClick={toggleFavorite}>
|
||||
<Heart
|
||||
size={15}
|
||||
color={isFavorited ? theme.accentAction : theme.textSecondary}
|
||||
fill={isFavorited ? theme.accentAction : theme.none}
|
||||
/>
|
||||
</ClickFavorited>
|
||||
</TokenActions>
|
||||
</TokenInfoContainer>
|
||||
<ChartContainer>
|
||||
<ParentSize>{({ width, height }) => <PriceChart width={width} height={height} />}</ParentSize>
|
||||
</ChartContainer>
|
||||
<TimeOptionsContainer>
|
||||
{TIME_PERIODS.map((timePeriod) => (
|
||||
<TimeButton
|
||||
key={timePeriod}
|
||||
active={activeTimePeriod === timePeriod}
|
||||
onClick={() => setTimePeriod(timePeriod)}
|
||||
>
|
||||
{TIME_DISPLAYS[timePeriod]}
|
||||
</TimeButton>
|
||||
))}
|
||||
</TimeOptionsContainer>
|
||||
</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>
|
||||
{TIME_DISPLAYS[activeTimePeriod]} 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 onClick={() => navigator.clipboard.writeText(address)}>
|
||||
<FullAddress>{address}</FullAddress>
|
||||
<TruncatedAddress>{truncatedTokenAddress}</TruncatedAddress>
|
||||
<Copy size={13} color={theme.textSecondary} />
|
||||
</ContractAddress>
|
||||
</Contract>
|
||||
</ContractAddressSection>
|
||||
<TokenSafetyModal
|
||||
isOpen={warningModalOpen}
|
||||
tokenAddress={address}
|
||||
onCancel={() => navigate(-1)}
|
||||
onContinue={handleDismissWarning}
|
||||
/>
|
||||
</TopArea>
|
||||
)
|
||||
}
|
46
src/components/Explore/TokenTable/FavoriteButton.tsx
Normal file
46
src/components/Explore/TokenTable/FavoriteButton.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import { useAtom } from 'jotai'
|
||||
import { Heart } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { SMALL_MOBILE_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { showFavoritesAtom } from '../state'
|
||||
|
||||
const FavoriteButtonContent = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
const StyledFavoriteButton = styled.button<{ active: boolean }>`
|
||||
padding: 0px 16px;
|
||||
border-radius: 12px;
|
||||
background-color: ${({ theme, active }) => (active ? theme.accentAction : theme.none)};
|
||||
border: 1px solid ${({ theme, active }) => (active ? theme.accentActive : theme.backgroundOutline)};
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
|
||||
:hover {
|
||||
background-color: ${({ theme, active }) => !active && theme.backgroundContainer};
|
||||
}
|
||||
`
|
||||
const FavoriteText = styled.span`
|
||||
@media only screen and (max-width: ${SMALL_MOBILE_MEDIA_BREAKPOINT}) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default function FavoriteButton() {
|
||||
const theme = useTheme()
|
||||
const [showFavorites, setShowFavorites] = useAtom(showFavoritesAtom)
|
||||
return (
|
||||
<StyledFavoriteButton onClick={() => setShowFavorites(!showFavorites)} active={showFavorites}>
|
||||
<FavoriteButtonContent>
|
||||
<Heart size={17} color={theme.textPrimary} fill={theme.none} />
|
||||
<FavoriteText>Favorites</FavoriteText>
|
||||
</FavoriteButtonContent>
|
||||
</StyledFavoriteButton>
|
||||
)
|
||||
}
|
183
src/components/Explore/TokenTable/NetworkFilter.tsx
Normal file
183
src/components/Explore/TokenTable/NetworkFilter.tsx
Normal file
@ -0,0 +1,183 @@
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useRef } from 'react'
|
||||
import { Check, ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { MEDIUM_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { filterNetworkAtom } from '../state'
|
||||
|
||||
const NETWORKS = [
|
||||
SupportedChainId.MAINNET,
|
||||
SupportedChainId.ARBITRUM_ONE,
|
||||
SupportedChainId.POLYGON,
|
||||
SupportedChainId.OPTIMISM,
|
||||
]
|
||||
|
||||
const InternalMenuItem = styled.div`
|
||||
flex: 1;
|
||||
padding: 12px 8px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
const InternalLinkMenuItem = styled(InternalMenuItem)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
|
||||
:hover {
|
||||
background-color: ${({ theme }) => theme.hoverState};
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
const MenuTimeFlyout = styled.span`
|
||||
min-width: 200px;
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
box-shadow: ${({ theme }) => theme.flyoutDropShadow};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
z-index: 100;
|
||||
left: 0px;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
bottom: unset;
|
||||
right: 0;
|
||||
left: unset;
|
||||
`};
|
||||
`
|
||||
|
||||
const StyledMenuButton = styled.button<{ open: boolean }>`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: ${({ theme, open }) => (open ? theme.blue200 : theme.textPrimary)};
|
||||
background-color: transparent;
|
||||
background-color: ${({ theme, open }) => (open ? theme.accentActionSoft : theme.none)};
|
||||
border: 1px solid ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundOutline)};
|
||||
margin: 0;
|
||||
padding: 6px 12px 6px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background-color: ${({ theme, open }) => !open && theme.backgroundContainer};
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
text-align: left;
|
||||
width: 160px;
|
||||
|
||||
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
||||
flex: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenuContent = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: none;
|
||||
width: 100%;
|
||||
font-weight: 600;
|
||||
vertical-align: middle;
|
||||
`
|
||||
|
||||
const Chevron = styled.span<{ open: boolean }>`
|
||||
padding-top: 1px;
|
||||
color: ${({ open, theme }) => (open ? theme.blue200 : theme.textSecondary)};
|
||||
`
|
||||
const NetworkLabel = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
`
|
||||
const Logo = styled.img`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
`
|
||||
const CheckContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: flex-end;
|
||||
`
|
||||
|
||||
// TODO: change this to reflect data pipeline
|
||||
export default function NetworkFilter() {
|
||||
const theme = useTheme()
|
||||
const node = useRef<HTMLDivElement | null>(null)
|
||||
const open = useModalIsOpen(ApplicationModal.NETWORK_FILTER)
|
||||
const toggleMenu = useToggleModal(ApplicationModal.NETWORK_FILTER)
|
||||
useOnClickOutside(node, open ? toggleMenu : undefined)
|
||||
const [activeNetwork, setNetwork] = useAtom(filterNetworkAtom)
|
||||
const { label, logoUrl } = getChainInfo(activeNetwork)
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<StyledMenuButton onClick={toggleMenu} aria-label={`networkFilter`} open={open}>
|
||||
<StyledMenuContent>
|
||||
<NetworkLabel>
|
||||
<Logo src={logoUrl} /> {label}
|
||||
</NetworkLabel>
|
||||
<Chevron open={open}>
|
||||
{open ? <ChevronUp size={15} viewBox="0 0 24 20" /> : <ChevronDown size={15} viewBox="0 0 24 20" />}
|
||||
</Chevron>
|
||||
</StyledMenuContent>
|
||||
</StyledMenuButton>
|
||||
{open && (
|
||||
<MenuTimeFlyout>
|
||||
{NETWORKS.map((network) => (
|
||||
<InternalLinkMenuItem
|
||||
key={network}
|
||||
onClick={() => {
|
||||
setNetwork(network)
|
||||
toggleMenu()
|
||||
}}
|
||||
>
|
||||
<NetworkLabel>
|
||||
<Logo src={getChainInfo(network).logoUrl} /> {getChainInfo(network).label}
|
||||
</NetworkLabel>
|
||||
{network === activeNetwork && (
|
||||
<CheckContainer>
|
||||
<Check size={16} color={theme.accentAction} />
|
||||
</CheckContainer>
|
||||
)}
|
||||
</InternalLinkMenuItem>
|
||||
))}
|
||||
</MenuTimeFlyout>
|
||||
)}
|
||||
</StyledMenu>
|
||||
)
|
||||
}
|
77
src/components/Explore/TokenTable/SearchBar.tsx
Normal file
77
src/components/Explore/TokenTable/SearchBar.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
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'
|
||||
import { filterStringAtom } from '../state'
|
||||
const ICON_SIZE = '20px'
|
||||
|
||||
const SearchBarContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
`
|
||||
const SearchInput = styled.input<{ expanded: boolean }>`
|
||||
background: no-repeat scroll 7px 7px;
|
||||
background-image: ${({ expanded }) => !expanded && `url(${searchIcon})`};
|
||||
background-size: 20px 20px;
|
||||
background-position: 11px center;
|
||||
background-color: ${({ theme }) => theme.none};
|
||||
border-radius: 12px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
height: 100%;
|
||||
width: ${({ expanded }) => (expanded ? '100%' : '44px')};
|
||||
font-size: 16px;
|
||||
padding-left: 18px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
transition: width 0.75s cubic-bezier(0, 0.795, 0, 1);
|
||||
|
||||
:hover {
|
||||
cursor: ${({ expanded }) => !expanded && 'pointer'};
|
||||
background-color: ${({ theme }) => theme.backgroundContainer};
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
background-color: ${({ theme }) => theme.accentActionSoft};
|
||||
border: none;
|
||||
}
|
||||
::placeholder {
|
||||
color: ${({ expanded, theme }) => expanded && theme.textTertiary};
|
||||
}
|
||||
::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: ${ICON_SIZE};
|
||||
width: ${ICON_SIZE};
|
||||
background-image: url(${xIcon});
|
||||
margin-right: 10px;
|
||||
background-size: ${ICON_SIZE} ${ICON_SIZE};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
||||
width: 100%;
|
||||
}
|
||||
`
|
||||
|
||||
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)}
|
||||
/>
|
||||
</SearchBarContainer>
|
||||
)
|
||||
}
|
163
src/components/Explore/TokenTable/TimeSelector.tsx
Normal file
163
src/components/Explore/TokenTable/TimeSelector.tsx
Normal file
@ -0,0 +1,163 @@
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { TimePeriod } from 'hooks/useTopTokens'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useRef } from 'react'
|
||||
import { Check, ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { MOBILE_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',
|
||||
}
|
||||
|
||||
const TIMES = Object.values(TimePeriod)
|
||||
|
||||
const InternalMenuItem = styled.div`
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
border-radius: 12px;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
const InternalLinkMenuItem = styled(InternalMenuItem)`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 12px 8px;
|
||||
justify-content: space-between;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
:hover {
|
||||
background-color: ${({ theme }) => theme.hoverState};
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
const MenuTimeFlyout = styled.span`
|
||||
min-width: 200px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
box-shadow: ${({ theme }) => theme.flyoutDropShadow};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
z-index: 100;
|
||||
left: 0px;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
bottom: unset;
|
||||
right: 0;
|
||||
left: unset;
|
||||
`};
|
||||
`
|
||||
|
||||
const StyledMenuButton = styled.button<{ open: boolean }>`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: ${({ theme, open }) => (open ? theme.blue200 : theme.textPrimary)};
|
||||
margin: 0;
|
||||
background-color: ${({ theme, open }) => (open ? theme.accentActionSoft : theme.none)};
|
||||
border: 1px solid ${({ theme, open }) => (open ? theme.accentActiveSoft : theme.backgroundOutline)};
|
||||
padding: 6px 12px 6px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 600;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background-color: ${({ theme, open }) => !open && theme.backgroundContainer};
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
text-align: left;
|
||||
width: 80px;
|
||||
|
||||
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
||||
width: 72px;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenuContent = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: none;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
`
|
||||
|
||||
const Chevron = styled.span<{ open: boolean }>`
|
||||
padding-top: 1px;
|
||||
color: ${({ open, theme }) => (open ? theme.blue200 : theme.textSecondary)};
|
||||
`
|
||||
|
||||
// TODO: change this to reflect data pipeline
|
||||
export default function TimeSelector() {
|
||||
const theme = useTheme()
|
||||
const node = useRef<HTMLDivElement | null>(null)
|
||||
const open = useModalIsOpen(ApplicationModal.TIME_SELECTOR)
|
||||
const toggleMenu = useToggleModal(ApplicationModal.TIME_SELECTOR)
|
||||
useOnClickOutside(node, open ? toggleMenu : undefined)
|
||||
const [activeTime, setTime] = useAtom(filterTimeAtom)
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<StyledMenuButton onClick={toggleMenu} aria-label={`timeSelector`} open={open}>
|
||||
<StyledMenuContent>
|
||||
{TIME_DISPLAYS[activeTime]}
|
||||
<Chevron open={open}>
|
||||
{open ? <ChevronUp size={15} viewBox="0 0 24 20" /> : <ChevronDown size={15} viewBox="0 0 24 20" />}
|
||||
</Chevron>
|
||||
</StyledMenuContent>
|
||||
</StyledMenuButton>
|
||||
{open && (
|
||||
<MenuTimeFlyout>
|
||||
{TIMES.map((time) => (
|
||||
<InternalLinkMenuItem
|
||||
key={time}
|
||||
onClick={() => {
|
||||
setTime(time)
|
||||
toggleMenu()
|
||||
}}
|
||||
>
|
||||
<div>{TIME_DISPLAYS[time]}</div>
|
||||
{time === activeTime && <Check color={theme.accentAction} size={16} />}
|
||||
</InternalLinkMenuItem>
|
||||
))}
|
||||
</MenuTimeFlyout>
|
||||
)}
|
||||
</StyledMenu>
|
||||
)
|
||||
}
|
514
src/components/Explore/TokenTable/TokenRow.tsx
Normal file
514
src/components/Explore/TokenTable/TokenRow.tsx
Normal file
@ -0,0 +1,514 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent } from 'components/AmplitudeAnalytics'
|
||||
import { EventName } from 'components/AmplitudeAnalytics/constants'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { useCurrency, useToken } from 'hooks/Tokens'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import { TimePeriod, TokenData } from 'hooks/useTopTokens'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { ReactNode } from 'react'
|
||||
import { ArrowDown, ArrowDownRight, ArrowUp, ArrowUpRight, Heart } from 'react-feather'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { formatAmount, formatDollarAmount } from 'utils/formatDollarAmt'
|
||||
|
||||
import {
|
||||
LARGE_MEDIA_BREAKPOINT,
|
||||
MAX_WIDTH_MEDIA_BREAKPOINT,
|
||||
MEDIUM_MEDIA_BREAKPOINT,
|
||||
MOBILE_MEDIA_BREAKPOINT,
|
||||
SMALL_MEDIA_BREAKPOINT,
|
||||
} from '../constants'
|
||||
import { LoadingBubble } from '../loading'
|
||||
import {
|
||||
favoritesAtom,
|
||||
filterNetworkAtom,
|
||||
filterStringAtom,
|
||||
filterTimeAtom,
|
||||
sortCategoryAtom,
|
||||
sortDirectionAtom,
|
||||
useSetSortCategory,
|
||||
useToggleFavorite,
|
||||
} from '../state'
|
||||
import { Category, SortDirection } from '../types'
|
||||
import { TIME_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;
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr 7fr 4fr 4fr 4fr 4fr 5fr;
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
|
||||
max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT};
|
||||
min-width: 390px;
|
||||
padding: 0px 12px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.accentActionSoft};
|
||||
}
|
||||
|
||||
@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;
|
||||
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;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
||||
grid-template-columns: 1.2fr 1fr 8fr 5fr 5fr;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
|
||||
grid-template-columns: 1fr 7fr 4fr 4fr 0.5px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
min-width: unset;
|
||||
border-bottom: 0.5px solid ${({ theme }) => theme.backgroundContainer};
|
||||
padding: 0px 12px;
|
||||
|
||||
:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
export const ClickFavorited = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
}
|
||||
`
|
||||
|
||||
const ClickableContent = styled.div`
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
`
|
||||
const ClickableName = styled(ClickableContent)`
|
||||
gap: 8px;
|
||||
`
|
||||
const FavoriteCell = styled(Cell)`
|
||||
min-width: 40px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
fill: none;
|
||||
|
||||
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
const StyledHeaderRow = styled(StyledTokenRow)`
|
||||
border-bottom: 1px solid;
|
||||
border-color: ${({ theme }) => theme.backgroundOutline};
|
||||
border-radius: 8px 8px 0px 0px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 12px;
|
||||
height: 48px;
|
||||
line-height: 16px;
|
||||
padding: 0px 12px;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT}) {
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
||||
justify-content: space-between;
|
||||
padding: 0px 12px;
|
||||
}
|
||||
`
|
||||
const ListNumberCell = styled(Cell)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
min-width: 32px;
|
||||
|
||||
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
const DataCell = styled(Cell)<{ sortable: boolean }>`
|
||||
justify-content: flex-end;
|
||||
min-width: 80px;
|
||||
user-select: ${({ sortable }) => (sortable ? 'none' : 'unset')};
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme, sortable }) => sortable && theme.white};
|
||||
background-color: ${({ theme, sortable }) => sortable && theme.accentActionSoft};
|
||||
}
|
||||
`
|
||||
const MarketCapCell = styled(DataCell)`
|
||||
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
const NameCell = styled(Cell)`
|
||||
justify-content: flex-start;
|
||||
padding-left: 8px;
|
||||
min-width: 200px;
|
||||
gap: 8px;
|
||||
|
||||
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
||||
padding-right: 8px;
|
||||
}
|
||||
`
|
||||
const PriceCell = styled(DataCell)``
|
||||
const PercentChangeCell = styled(DataCell)`
|
||||
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
const PercentChangeInfoCell = styled(Cell)`
|
||||
display: none;
|
||||
|
||||
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
`
|
||||
const PriceInfoCell = styled(Cell)`
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
|
||||
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
`
|
||||
const SortArrowCell = styled(Cell)`
|
||||
padding-right: 2px;
|
||||
`
|
||||
const HeaderCellWrapper = styled.span<{ onClick?: () => void }>`
|
||||
align-items: center;
|
||||
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'unset')};
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: flex-end;
|
||||
padding-right: 8px;
|
||||
width: 100%;
|
||||
`
|
||||
const SparkLineCell = styled(Cell)`
|
||||
padding: 0px 24px;
|
||||
min-width: 120px;
|
||||
|
||||
@media only screen and (max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT}) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
const SparkLineImg = styled(Cell)<{ isPositive: boolean }>`
|
||||
max-width: 124px;
|
||||
max-height: 28px;
|
||||
flex-direction: column;
|
||||
transform: scale(1.2);
|
||||
|
||||
polyline {
|
||||
stroke: ${({ theme, isPositive }) => (isPositive ? theme.accentSuccess : theme.accentFailure)};
|
||||
}
|
||||
`
|
||||
const StyledLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
`
|
||||
const TokenInfoCell = styled(Cell)`
|
||||
gap: 8px;
|
||||
line-height: 24px;
|
||||
font-size: 16px;
|
||||
|
||||
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 0px;
|
||||
width: max-content;
|
||||
font-weight: 500;
|
||||
}
|
||||
`
|
||||
const TokenName = styled.div`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 160px;
|
||||
white-space: nowrap;
|
||||
`
|
||||
const TokenSymbol = styled(Cell)`
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
|
||||
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
||||
font-size: 12px;
|
||||
height: 16px;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
`
|
||||
const VolumeCell = styled(DataCell)`
|
||||
@media only screen and (max-width: ${LARGE_MEDIA_BREAKPOINT}) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
const SmallLoadingBubble = styled(LoadingBubble)`
|
||||
width: 25%;
|
||||
`
|
||||
const MediumLoadingBubble = styled(LoadingBubble)`
|
||||
width: 65%;
|
||||
`
|
||||
const LongLoadingBubble = styled(LoadingBubble)`
|
||||
width: 90%;
|
||||
`
|
||||
const IconLoadingBubble = styled(LoadingBubble)`
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
`
|
||||
const SparkLineLoadingBubble = styled(LongLoadingBubble)`
|
||||
height: 4px;
|
||||
`
|
||||
|
||||
/* formatting for volume with timeframe header display */
|
||||
function getHeaderDisplay(category: string, timeframe: string): string {
|
||||
if (category === Category.volume) return `${TIME_DISPLAYS[timeframe]} ${category}`
|
||||
return category
|
||||
}
|
||||
|
||||
/* Get singular header cell for header row */
|
||||
function HeaderCell({
|
||||
category,
|
||||
sortable,
|
||||
}: {
|
||||
category: Category // TODO: change this to make it work for trans
|
||||
sortable: boolean
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
const sortDirection = useAtomValue<SortDirection>(sortDirectionAtom)
|
||||
const handleSortCategory = useSetSortCategory(category)
|
||||
const sortCategory = useAtomValue<Category>(sortCategoryAtom)
|
||||
const timeframe = useAtomValue<TimePeriod>(filterTimeAtom)
|
||||
|
||||
if (sortCategory === category) {
|
||||
return (
|
||||
<HeaderCellWrapper onClick={handleSortCategory}>
|
||||
<SortArrowCell>
|
||||
{sortDirection === SortDirection.increasing ? (
|
||||
<ArrowUp size={14} color={theme.accentActive} />
|
||||
) : (
|
||||
<ArrowDown size={14} color={theme.accentActive} />
|
||||
)}
|
||||
</SortArrowCell>
|
||||
{getHeaderDisplay(category, timeframe)}
|
||||
</HeaderCellWrapper>
|
||||
)
|
||||
}
|
||||
if (sortable) {
|
||||
return (
|
||||
<HeaderCellWrapper onClick={handleSortCategory}>
|
||||
<SortArrowCell>
|
||||
<ArrowUp size={14} visibility="hidden" />
|
||||
</SortArrowCell>
|
||||
{getHeaderDisplay(category, timeframe)}
|
||||
</HeaderCellWrapper>
|
||||
)
|
||||
}
|
||||
return <HeaderCellWrapper>{getHeaderDisplay(category, timeframe)}</HeaderCellWrapper>
|
||||
}
|
||||
|
||||
/* Token Row: skeleton row component */
|
||||
export function TokenRow({
|
||||
address,
|
||||
header,
|
||||
favorited,
|
||||
listNumber,
|
||||
tokenInfo,
|
||||
price,
|
||||
percentChange,
|
||||
marketCap,
|
||||
volume,
|
||||
sparkLine,
|
||||
}: {
|
||||
address: ReactNode
|
||||
header: boolean
|
||||
favorited: ReactNode
|
||||
listNumber: ReactNode
|
||||
tokenInfo: ReactNode
|
||||
price: ReactNode
|
||||
percentChange: ReactNode
|
||||
marketCap: ReactNode
|
||||
volume: ReactNode
|
||||
sparkLine: ReactNode
|
||||
}) {
|
||||
const rowCells = (
|
||||
<>
|
||||
<FavoriteCell>{favorited}</FavoriteCell>
|
||||
<ListNumberCell>{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>
|
||||
</>
|
||||
)
|
||||
if (header) return <StyledHeaderRow>{rowCells}</StyledHeaderRow>
|
||||
return <StyledTokenRow>{rowCells}</StyledTokenRow>
|
||||
}
|
||||
|
||||
/* Header Row: top header row component for table */
|
||||
export function HeaderRow() {
|
||||
return (
|
||||
<TokenRow
|
||||
address={null}
|
||||
header={true}
|
||||
favorited={null}
|
||||
listNumber={null}
|
||||
tokenInfo={<Trans>Token Name</Trans>}
|
||||
price={<HeaderCell category={Category.price} sortable />}
|
||||
percentChange={<HeaderCell category={Category.percentChange} sortable />}
|
||||
marketCap={<HeaderCell category={Category.marketCap} sortable />}
|
||||
volume={<HeaderCell category={Category.volume} sortable />}
|
||||
sparkLine={null}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/* Loading State: row component with loading bubbles */
|
||||
export function LoadingRow() {
|
||||
return (
|
||||
<TokenRow
|
||||
address={null}
|
||||
header={false}
|
||||
favorited={null}
|
||||
listNumber={<SmallLoadingBubble />}
|
||||
tokenInfo={
|
||||
<>
|
||||
<IconLoadingBubble />
|
||||
<MediumLoadingBubble />
|
||||
</>
|
||||
}
|
||||
price={<MediumLoadingBubble />}
|
||||
percentChange={<LoadingBubble />}
|
||||
marketCap={<LoadingBubble />}
|
||||
volume={<LoadingBubble />}
|
||||
sparkLine={<SparkLineLoadingBubble />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/* Loaded State: row component with token information */
|
||||
export default function LoadedRow({
|
||||
tokenAddress,
|
||||
tokenListIndex,
|
||||
tokenListLength,
|
||||
data,
|
||||
timePeriod,
|
||||
}: {
|
||||
tokenAddress: string
|
||||
tokenListIndex: number
|
||||
tokenListLength: number
|
||||
data: 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 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 tokenPercentChangeInfo = (
|
||||
<>
|
||||
{tokenData.delta}%
|
||||
<ArrowCell>
|
||||
{isPositive ? (
|
||||
<ArrowUpRight size={16} color={theme.accentSuccess} />
|
||||
) : (
|
||||
<ArrowDownRight size={16} color={theme.accentFailure} />
|
||||
)}
|
||||
</ArrowCell>
|
||||
</>
|
||||
)
|
||||
|
||||
const exploreTokenSelectedEventProperties = {
|
||||
chain_id: filterNetwork,
|
||||
token_address: tokenAddress,
|
||||
token_symbol: token?.symbol,
|
||||
token_list_index: tokenListIndex,
|
||||
token_list_length: tokenListLength,
|
||||
time_frame: filterTime,
|
||||
search_token_address_input: filterString,
|
||||
}
|
||||
|
||||
const heartColor = isFavorited ? theme.accentActive : undefined
|
||||
// TODO: currency logo sizing mobile (32px) vs. desktop (24px)
|
||||
return (
|
||||
<StyledLink
|
||||
to={`/tokens/${tokenAddress}`}
|
||||
onClick={() => sendAnalyticsEvent(EventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)}
|
||||
>
|
||||
<TokenRow
|
||||
address={tokenAddress}
|
||||
header={false}
|
||||
favorited={
|
||||
<ClickFavorited
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
toggleFavorite()
|
||||
}}
|
||||
>
|
||||
<Heart size={15} color={heartColor} fill={heartColor} />
|
||||
</ClickFavorited>
|
||||
}
|
||||
listNumber={tokenListIndex + 1}
|
||||
tokenInfo={
|
||||
<ClickableName>
|
||||
<CurrencyLogo currency={currency} />
|
||||
<TokenInfoCell>
|
||||
<TokenName>{tokenName}</TokenName>
|
||||
<TokenSymbol>{tokenSymbol}</TokenSymbol>
|
||||
</TokenInfoCell>
|
||||
</ClickableName>
|
||||
}
|
||||
price={
|
||||
<ClickableContent>
|
||||
<PriceInfoCell>
|
||||
{formatDollarAmount(tokenData.price)}
|
||||
<PercentChangeInfoCell>{tokenPercentChangeInfo}</PercentChangeInfoCell>
|
||||
</PriceInfoCell>
|
||||
</ClickableContent>
|
||||
}
|
||||
percentChange={<ClickableContent>{tokenPercentChangeInfo}</ClickableContent>}
|
||||
marketCap={<ClickableContent>{formatAmount(tokenData.marketCap).toUpperCase()}</ClickableContent>}
|
||||
volume={<ClickableContent>{formatAmount(tokenData.volume[timePeriod]).toUpperCase()}</ClickableContent>}
|
||||
sparkLine={<SparkLineImg dangerouslySetInnerHTML={{ __html: tokenData.sparkline }} isPositive={isPositive} />}
|
||||
/>
|
||||
</StyledLink>
|
||||
)
|
||||
}
|
205
src/components/Explore/TokenTable/TokenTable.tsx
Normal file
205
src/components/Explore/TokenTable/TokenTable.tsx
Normal file
@ -0,0 +1,205 @@
|
||||
import {
|
||||
favoritesAtom,
|
||||
filterStringAtom,
|
||||
filterTimeAtom,
|
||||
showFavoritesAtom,
|
||||
sortCategoryAtom,
|
||||
sortDirectionAtom,
|
||||
} from 'components/Explore/state'
|
||||
import { useAllTokens } from 'hooks/Tokens'
|
||||
import useTopTokens, { TimePeriod, TokenData } from 'hooks/useTopTokens'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { ReactNode, useCallback, useMemo } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { MAX_WIDTH_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { Category, SortDirection } from '../types'
|
||||
import LoadedRow, { HeaderRow, LoadingRow } from './TokenRow'
|
||||
|
||||
const GridContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT};
|
||||
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;
|
||||
margin-right: auto;
|
||||
border-radius: 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
`
|
||||
const NoTokenDisplay = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
align-items: center;
|
||||
padding: 0px 28px;
|
||||
gap: 8px;
|
||||
`
|
||||
const TokenRowsContainer = styled.div`
|
||||
padding: 4px 0px;
|
||||
`
|
||||
|
||||
function useFilteredTokens(addresses: string[]) {
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
const favoriteTokens = useAtomValue(favoritesAtom)
|
||||
const showFavorites = useAtomValue(showFavoritesAtom)
|
||||
const shownTokens = showFavorites ? favoriteTokens : addresses
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
shownTokens.filter((tokenAddress) => {
|
||||
const token = allTokens[tokenAddress]
|
||||
const tokenName = token?.name ?? ''
|
||||
const tokenSymbol = token?.symbol ?? ''
|
||||
|
||||
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)
|
||||
return nameIncludesFilterString || symbolIncludesFilterString || addressIncludesFilterString
|
||||
}),
|
||||
[allTokens, shownTokens, filterString]
|
||||
)
|
||||
}
|
||||
|
||||
function useSortedTokens(addresses: string[], tokenData: TokenData | null) {
|
||||
const sortCategory = useAtomValue(sortCategoryAtom)
|
||||
const sortDirection = useAtomValue(sortDirectionAtom)
|
||||
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
|
||||
|
||||
const sortFn = useCallback(
|
||||
(a: any, b: any) => {
|
||||
if (a > b) {
|
||||
return sortDirection === SortDirection.decreasing ? -1 : 1
|
||||
} else if (a < b) {
|
||||
return sortDirection === SortDirection.decreasing ? 1 : -1
|
||||
}
|
||||
return 0
|
||||
},
|
||||
[sortDirection]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
addresses.sort((token1Address, token2Address) => {
|
||||
if (!tokenData) {
|
||||
return 0
|
||||
}
|
||||
const token1 = tokenData[token1Address] as any
|
||||
const token2 = tokenData[token2Address] as any
|
||||
|
||||
if (!token1 || !token2 || !sortDirection || !sortCategory) {
|
||||
return 0
|
||||
}
|
||||
let a: number
|
||||
let b: number
|
||||
switch (sortCategory) {
|
||||
case Category.marketCap:
|
||||
a = token1.marketCap
|
||||
b = token2.marketCap
|
||||
break
|
||||
case Category.percentChange:
|
||||
a = token1.delta
|
||||
b = token2.delta
|
||||
break
|
||||
case Category.price:
|
||||
a = token1.price
|
||||
b = token2.price
|
||||
break
|
||||
case Category.volume:
|
||||
a = token1.volume[timePeriod]
|
||||
b = token2.volume[timePeriod]
|
||||
break
|
||||
}
|
||||
return sortFn(a, b)
|
||||
}),
|
||||
[addresses, tokenData, sortDirection, sortCategory, sortFn, timePeriod]
|
||||
)
|
||||
}
|
||||
|
||||
function NoTokensState({ message }: { message: ReactNode }) {
|
||||
return (
|
||||
<GridContainer>
|
||||
<HeaderRow />
|
||||
<NoTokenDisplay>{message}</NoTokenDisplay>
|
||||
</GridContainer>
|
||||
)
|
||||
}
|
||||
|
||||
function LoadingTokenTable() {
|
||||
return (
|
||||
<GridContainer>
|
||||
<HeaderRow />
|
||||
<TokenRowsContainer>
|
||||
{Array(10)
|
||||
.fill(0)
|
||||
.map((_item, index) => (
|
||||
<LoadingRow key={index} />
|
||||
))}
|
||||
</TokenRowsContainer>
|
||||
</GridContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default function TokenTable() {
|
||||
const { data, error, loading } = useTopTokens()
|
||||
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)
|
||||
|
||||
/* loading and error state */
|
||||
if (loading) {
|
||||
return <LoadingTokenTable />
|
||||
} else if (error || data === null) {
|
||||
return (
|
||||
<NoTokensState
|
||||
message={
|
||||
<>
|
||||
<AlertTriangle size={16} />
|
||||
An error occured loading tokens. Please try again.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (showFavorites && filteredAndSortedTokens.length === 0) {
|
||||
return <NoTokensState message="You have no favorited tokens" />
|
||||
}
|
||||
|
||||
if (!showFavorites && filteredAndSortedTokens.length === 0) {
|
||||
return <NoTokensState message="No tokens found" />
|
||||
}
|
||||
|
||||
return (
|
||||
<GridContainer>
|
||||
<HeaderRow />
|
||||
<TokenRowsContainer>
|
||||
{filteredAndSortedTokens.map((tokenAddress, index) => (
|
||||
<LoadedRow
|
||||
key={tokenAddress}
|
||||
tokenAddress={tokenAddress}
|
||||
tokenListIndex={index}
|
||||
tokenListLength={filteredAndSortedTokens.length}
|
||||
data={data}
|
||||
timePeriod={timePeriod}
|
||||
/>
|
||||
))}
|
||||
</TokenRowsContainer>
|
||||
</GridContainer>
|
||||
)
|
||||
}
|
7
src/components/Explore/constants.ts
Normal file
7
src/components/Explore/constants.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const MAX_WIDTH_MEDIA_BREAKPOINT = '960px'
|
||||
export const LARGE_MEDIA_BREAKPOINT = '840px'
|
||||
export const MEDIUM_MEDIA_BREAKPOINT = '720px'
|
||||
export const SMALL_MEDIA_BREAKPOINT = '540px'
|
||||
export const MOBILE_MEDIA_BREAKPOINT = '420px'
|
||||
export const SMALL_MOBILE_MEDIA_BREAKPOINT = '390px'
|
||||
export const SMALLEST_MOBILE_MEDIA_BREAKPOINT = '320px'
|
19
src/components/Explore/loading.tsx
Normal file
19
src/components/Explore/loading.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { loadingAnimation } from 'components/Loader/styled'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
/* Loading state bubbles (animation style from: src/components/Loader/styled.tsx) */
|
||||
export const LoadingBubble = styled.div`
|
||||
border-radius: 12px;
|
||||
height: 24px;
|
||||
width: 50%;
|
||||
animation: ${loadingAnimation} 1.5s infinite;
|
||||
animation-fill-mode: both;
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
${({ theme }) => theme.backgroundAction} 25%,
|
||||
${({ theme }) => theme.backgroundOutline} 50%,
|
||||
${({ theme }) => theme.backgroundAction} 75%
|
||||
);
|
||||
will-change: background-position;
|
||||
background-size: 400%;
|
||||
`
|
49
src/components/Explore/state.ts
Normal file
49
src/components/Explore/state.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { TimePeriod } from 'hooks/useTopTokens'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
import { atomWithReset, atomWithStorage } from 'jotai/utils'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { Category, SortDirection } from './types'
|
||||
|
||||
export const favoritesAtom = atomWithStorage<string[]>('favorites', [])
|
||||
export const showFavoritesAtom = atomWithStorage<boolean>('showFavorites', false)
|
||||
export const filterStringAtom = atomWithReset<string>('')
|
||||
export const filterNetworkAtom = atom<SupportedChainId>(SupportedChainId.MAINNET)
|
||||
export const filterTimeAtom = atom<TimePeriod>(TimePeriod.day)
|
||||
export const sortCategoryAtom = atom<Category>(Category.marketCap)
|
||||
export const sortDirectionAtom = atom<SortDirection>(SortDirection.decreasing)
|
||||
|
||||
/* for favoriting tokens */
|
||||
export function useToggleFavorite(tokenAddress: string) {
|
||||
const [favoriteTokens, updateFavoriteTokens] = useAtom(favoritesAtom)
|
||||
|
||||
return useCallback(() => {
|
||||
let updatedFavoriteTokens
|
||||
if (favoriteTokens.includes(tokenAddress)) {
|
||||
updatedFavoriteTokens = favoriteTokens.filter((address: string) => {
|
||||
return address !== tokenAddress
|
||||
})
|
||||
} else {
|
||||
updatedFavoriteTokens = [...favoriteTokens, tokenAddress]
|
||||
}
|
||||
updateFavoriteTokens(updatedFavoriteTokens)
|
||||
}, [favoriteTokens, tokenAddress, updateFavoriteTokens])
|
||||
}
|
||||
|
||||
/* keep track of sort category for token table */
|
||||
export function useSetSortCategory(category: Category) {
|
||||
const [sortCategory, setSortCategory] = useAtom(sortCategoryAtom)
|
||||
const [sortDirection, setDirectionCategory] = useAtom(sortDirectionAtom)
|
||||
|
||||
return useCallback(() => {
|
||||
if (category === sortCategory) {
|
||||
const oppositeDirection =
|
||||
sortDirection === SortDirection.increasing ? SortDirection.decreasing : SortDirection.increasing
|
||||
setDirectionCategory(oppositeDirection)
|
||||
} else {
|
||||
setSortCategory(category)
|
||||
setDirectionCategory(SortDirection.decreasing)
|
||||
}
|
||||
}, [category, sortCategory, setSortCategory, sortDirection, setDirectionCategory])
|
||||
}
|
10
src/components/Explore/types.ts
Normal file
10
src/components/Explore/types.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export enum Category {
|
||||
percentChange = '% Change',
|
||||
marketCap = 'Market Cap',
|
||||
price = 'Price',
|
||||
volume = 'Volume',
|
||||
}
|
||||
export enum SortDirection {
|
||||
increasing = 'Increasing',
|
||||
decreasing = 'Decreasing',
|
||||
}
|
@ -3,6 +3,7 @@ import useScrollPosition from '@react-hook/window-scroll'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { getChainInfoOrDefault } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { Phase0Variant, usePhase0Flag } from 'featureFlags/flags/phase0'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import { darken } from 'polished'
|
||||
import { NavLink, useLocation } from 'react-router-dom'
|
||||
@ -244,6 +245,8 @@ const StyledExternalLink = styled(ExternalLink)`
|
||||
`
|
||||
|
||||
export default function Header() {
|
||||
const phase0Flag = usePhase0Flag()
|
||||
|
||||
const { account, chainId } = useWeb3React()
|
||||
|
||||
const userEthBalance = useNativeCurrencyBalances(account ? [account] : [])?.[account ?? '']
|
||||
@ -289,6 +292,11 @@ export default function Header() {
|
||||
<StyledNavLink id={`swap-nav-link`} to={'/swap'}>
|
||||
<Trans>Swap</Trans>
|
||||
</StyledNavLink>
|
||||
{phase0Flag === Phase0Variant.Enabled && (
|
||||
<StyledNavLink id={`explore-nav-link`} to={'/explore'}>
|
||||
<Trans>Explore</Trans>
|
||||
</StyledNavLink>
|
||||
)}
|
||||
<StyledNavLink
|
||||
data-cy="pool-nav-link"
|
||||
id={`pool-nav-link`}
|
||||
|
@ -46,7 +46,7 @@ const SHOULD_SHOW_ALERT = {
|
||||
[SupportedChainId.CELO_ALFAJORES]: true,
|
||||
}
|
||||
|
||||
type NetworkAlertChains = keyof typeof SHOULD_SHOW_ALERT
|
||||
export type NetworkAlertChains = keyof typeof SHOULD_SHOW_ALERT
|
||||
|
||||
const BG_COLORS_BY_DARK_MODE_AND_CHAIN_ID: {
|
||||
[darkMode in 'dark' | 'light']: { [chainId in NetworkAlertChains]: string }
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
exports[`renders currency rows correctly when currencies list is non-empty 1`] = `
|
||||
<DocumentFragment>
|
||||
.c5 {
|
||||
.c8 {
|
||||
color: #6E727D;
|
||||
}
|
||||
|
||||
@ -12,6 +12,21 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
width: 0.9rem;
|
||||
height: 0.9rem;
|
||||
margin-left: 4px;
|
||||
color: #FD4040;
|
||||
display: -webkit-inline-box;
|
||||
display: -webkit-inline-flex;
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
@ -64,6 +79,25 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
||||
background-color: #EDEEF2;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
max-width: 90%;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
<div
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
>
|
||||
@ -80,15 +114,51 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="css-8mokm4"
|
||||
title="Dai Stablecoin"
|
||||
class="c5"
|
||||
>
|
||||
DAI
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="Dai Stablecoin"
|
||||
>
|
||||
Dai Stablecoin
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
color="#FD4040"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 css-1j6a53a"
|
||||
class="c8 css-1j6a53a"
|
||||
>
|
||||
Dai Stablecoin
|
||||
DAI
|
||||
</div>
|
||||
</div>
|
||||
<span />
|
||||
@ -103,15 +173,51 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="css-8mokm4"
|
||||
title="USD//C"
|
||||
class="c5"
|
||||
>
|
||||
USDC
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="USD//C"
|
||||
>
|
||||
USD//C
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
color="#FD4040"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 css-1j6a53a"
|
||||
class="c8 css-1j6a53a"
|
||||
>
|
||||
USD//C
|
||||
USDC
|
||||
</div>
|
||||
</div>
|
||||
<span />
|
||||
@ -126,15 +232,51 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="css-8mokm4"
|
||||
title="Wrapped BTC"
|
||||
class="c5"
|
||||
>
|
||||
WBTC
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="Wrapped BTC"
|
||||
>
|
||||
Wrapped BTC
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
color="#FD4040"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 css-1j6a53a"
|
||||
class="c8 css-1j6a53a"
|
||||
>
|
||||
Wrapped BTC
|
||||
WBTC
|
||||
</div>
|
||||
</div>
|
||||
<span />
|
||||
|
@ -52,6 +52,8 @@ it('renders loading rows when isLoading is true', () => {
|
||||
showImportView={noOp}
|
||||
setImportToken={noOp}
|
||||
isLoading={true}
|
||||
searchQuery={''}
|
||||
isAddressSearch={''}
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
@ -68,6 +70,8 @@ it('renders currency rows correctly when currencies list is non-empty', () => {
|
||||
showImportView={noOp}
|
||||
setImportToken={noOp}
|
||||
isLoading={false}
|
||||
searchQuery={''}
|
||||
isAddressSearch={''}
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
|
@ -5,6 +5,8 @@ import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/con
|
||||
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
|
||||
import { LightGreyCard } from 'components/Card'
|
||||
import QuestionHelper from 'components/QuestionHelper'
|
||||
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import { CSSProperties, MutableRefObject, useCallback, useMemo } from 'react'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
@ -37,6 +39,14 @@ const StyledBalanceText = styled(Text)`
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
const CurrencyName = styled(Text)`
|
||||
max-width: 90%;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
const Tag = styled.div`
|
||||
background-color: ${({ theme }) => theme.deprecated_bg3};
|
||||
color: ${({ theme }) => theme.deprecated_text2};
|
||||
@ -58,7 +68,6 @@ const FixedContentRow = styled.div`
|
||||
grid-gap: 16px;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
function Balance({ balance }: { balance: CurrencyAmount<Currency> }) {
|
||||
return <StyledBalanceText title={balance.toExact()}>{balance.toSignificant(4)}</StyledBalanceText>
|
||||
}
|
||||
@ -72,6 +81,11 @@ const TokenListLogoWrapper = styled.img`
|
||||
height: 20px;
|
||||
`
|
||||
|
||||
const NameContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
function TokenTags({ currency }: { currency: Currency }) {
|
||||
if (!(currency instanceof WrappedTokenInfo)) {
|
||||
return <span />
|
||||
@ -124,6 +138,7 @@ function CurrencyRow({
|
||||
const isOnSelectedList = isTokenOnList(selectedTokenList, currency.isToken ? currency : undefined)
|
||||
const customAdded = useIsUserAddedToken(currency)
|
||||
const balance = useCurrencyBalance(account ?? undefined, currency)
|
||||
const warning = currency.isNative ? null : checkWarning(currency.address)
|
||||
|
||||
// only show add or remove buttons if not on selected list
|
||||
return (
|
||||
@ -144,14 +159,15 @@ function CurrencyRow({
|
||||
>
|
||||
<CurrencyLogo currency={currency} size={'24px'} />
|
||||
<Column>
|
||||
<Text title={currency.name} fontWeight={500}>
|
||||
{currency.symbol}
|
||||
</Text>
|
||||
<NameContainer>
|
||||
<CurrencyName title={currency.name}>{currency.name}</CurrencyName>
|
||||
<TokenSafetyIcon warning={warning} />
|
||||
</NameContainer>
|
||||
<ThemedText.DeprecatedDarkGray ml="0px" fontSize={'12px'} fontWeight={300}>
|
||||
{!currency.isNative && !isOnSelectedList && customAdded ? (
|
||||
<Trans>{currency.name} • Added by user</Trans>
|
||||
<Trans>{currency.symbol} • Added by user</Trans>
|
||||
) : (
|
||||
currency.name
|
||||
currency.symbol
|
||||
)}
|
||||
</ThemedText.DeprecatedDarkGray>
|
||||
</Column>
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { Currency, Token } from '@uniswap/sdk-core'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import TokenSafety from 'components/TokenSafety'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import useLast from '../../hooks/useLast'
|
||||
import { WrappedTokenInfo } from '../../state/lists/wrappedTokenInfo'
|
||||
import Modal from '../Modal'
|
||||
import { CurrencySearch } from './CurrencySearch'
|
||||
import { ImportList } from './ImportList'
|
||||
import { ImportToken } from './ImportToken'
|
||||
import Manage from './Manage'
|
||||
|
||||
interface CurrencySearchModalProps {
|
||||
@ -74,7 +73,7 @@ export default function CurrencySearchModal({
|
||||
)
|
||||
|
||||
// change min height if not searching
|
||||
const minHeight = modalView === CurrencyModalView.importToken || modalView === CurrencyModalView.importList ? 40 : 80
|
||||
let minHeight: number | undefined = 80
|
||||
let content = null
|
||||
switch (modalView) {
|
||||
case CurrencyModalView.search:
|
||||
@ -96,18 +95,18 @@ export default function CurrencySearchModal({
|
||||
break
|
||||
case CurrencyModalView.importToken:
|
||||
if (importToken) {
|
||||
minHeight = undefined
|
||||
content = (
|
||||
<ImportToken
|
||||
tokens={[importToken]}
|
||||
onDismiss={onDismiss}
|
||||
list={importToken instanceof WrappedTokenInfo ? importToken.list : undefined}
|
||||
onBack={handleBackImport}
|
||||
handleCurrencySelect={handleCurrencySelect}
|
||||
<TokenSafety
|
||||
tokenAddress={importToken.address}
|
||||
onContinue={() => handleCurrencySelect(importToken)}
|
||||
onCancel={handleBackImport}
|
||||
/>
|
||||
)
|
||||
}
|
||||
break
|
||||
case CurrencyModalView.importList:
|
||||
minHeight = 40
|
||||
if (importList && listURL) {
|
||||
content = <ImportList list={importList} listURL={listURL} onDismiss={onDismiss} setModalView={setModalView} />
|
||||
}
|
||||
|
38
src/components/TokenSafety/TokenSafetyIcon.tsx
Normal file
38
src/components/TokenSafety/TokenSafetyIcon.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { ReactComponent as Verified } from 'assets/svg/verified.svg'
|
||||
import { Warning, WARNING_LEVEL } from 'constants/tokenSafety'
|
||||
import { useTokenWarningColor } from 'hooks/useTokenWarningColor'
|
||||
import { AlertOctagon, AlertTriangle } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { Color } from 'theme/styled'
|
||||
|
||||
const Container = styled.div<{ color: Color }>`
|
||||
width: 0.9rem;
|
||||
height: 0.9rem;
|
||||
margin-left: 4px;
|
||||
color: ${({ color }) => color};
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const VerifiedContainer = styled.div`
|
||||
margin-left: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
export const VerifiedIcon = styled(Verified)<{ size?: string }>`
|
||||
width: ${({ size }) => size ?? '1em'};
|
||||
height: ${({ size }) => size ?? '1em'};
|
||||
`
|
||||
|
||||
export default function TokenSafetyIcon({ warning }: { warning: Warning | null }) {
|
||||
const color = useTokenWarningColor(warning ? warning.level : WARNING_LEVEL.UNKNOWN)
|
||||
if (!warning) {
|
||||
return (
|
||||
<VerifiedContainer>
|
||||
<VerifiedIcon />
|
||||
</VerifiedContainer>
|
||||
)
|
||||
}
|
||||
return <Container color={color}>{warning.canProceed ? <AlertTriangle /> : <AlertOctagon />}</Container>
|
||||
}
|
37
src/components/TokenSafety/TokenSafetyLabel.tsx
Normal file
37
src/components/TokenSafety/TokenSafetyLabel.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { WARNING_LEVEL } from 'constants/tokenSafety'
|
||||
import { useTokenWarningColor } from 'hooks/useTokenWarningColor'
|
||||
import { ReactNode } from 'react'
|
||||
import { AlertOctagon, AlertTriangle } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components/macro'
|
||||
import { Color } from 'theme/styled'
|
||||
|
||||
const Label = styled.div<{ color: Color }>`
|
||||
padding: 4px 4px;
|
||||
font-size: 12px;
|
||||
background-color: ${({ color }) => color + '1F'};
|
||||
border-radius: 8px;
|
||||
color: ${({ color }) => color};
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Title = styled(Text)`
|
||||
margin-right: 5px;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
type TokenWarningLabelProps = {
|
||||
level: WARNING_LEVEL
|
||||
canProceed: boolean
|
||||
children: ReactNode
|
||||
}
|
||||
export default function TokenSafetyLabel({ level, canProceed, children }: TokenWarningLabelProps) {
|
||||
return (
|
||||
<Label color={useTokenWarningColor(level)}>
|
||||
<Title marginRight="5px">{children}</Title>
|
||||
{canProceed ? <AlertTriangle strokeWidth={2.5} size="14px" /> : <AlertOctagon strokeWidth={2.5} size="14px" />}
|
||||
</Label>
|
||||
)
|
||||
}
|
65
src/components/TokenSafety/TokenSafetyMessage.tsx
Normal file
65
src/components/TokenSafety/TokenSafetyMessage.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { getWarningCopy, TOKEN_SAFETY_ARTICLE, Warning } from 'constants/tokenSafety'
|
||||
import { useTokenWarningColor } from 'hooks/useTokenWarningColor'
|
||||
import { AlertOctagon, AlertTriangle } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink } from 'theme'
|
||||
import { Color } from 'theme/styled'
|
||||
|
||||
const Label = styled.div<{ color: Color }>`
|
||||
width: 284px;
|
||||
padding: 12px 20px;
|
||||
background-color: ${({ color }) => color + '1F'};
|
||||
border-radius: 16px;
|
||||
color: ${({ color }) => color};
|
||||
`
|
||||
|
||||
const TitleRow = styled.div`
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
display: inline-flex;
|
||||
`
|
||||
|
||||
const Title = styled(Text)`
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
margin-left: 7px;
|
||||
`
|
||||
|
||||
const DetailsRow = styled.div`
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
|
||||
type TokenWarningMessageProps = {
|
||||
warning: Warning
|
||||
tokenAddress: string
|
||||
}
|
||||
|
||||
export default function TokenWarningMessage({ warning, tokenAddress }: TokenWarningMessageProps) {
|
||||
const color = useTokenWarningColor(warning.level)
|
||||
const { heading, description } = getWarningCopy(warning)
|
||||
|
||||
return (
|
||||
<Label color={color}>
|
||||
<TitleRow>
|
||||
{warning.canProceed ? <AlertTriangle size={'16px'} /> : <AlertOctagon size={'16px'} />}
|
||||
<Title marginLeft="7px">{warning.message}</Title>
|
||||
</TitleRow>
|
||||
|
||||
<DetailsRow>
|
||||
{heading && [heading, '. ']}
|
||||
{description}
|
||||
{tokenAddress && (
|
||||
<ExternalLink href={TOKEN_SAFETY_ARTICLE}>
|
||||
{' '}
|
||||
<Trans>Learn more</Trans>
|
||||
</ExternalLink>
|
||||
)}
|
||||
</DetailsRow>
|
||||
</Label>
|
||||
)
|
||||
}
|
29
src/components/TokenSafety/TokenSafetyModal.tsx
Normal file
29
src/components/TokenSafety/TokenSafetyModal.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import Modal from '../Modal'
|
||||
import TokenSafety from '.'
|
||||
|
||||
interface TokenSafetyModalProps {
|
||||
isOpen: boolean
|
||||
tokenAddress: string | null
|
||||
secondTokenAddress?: string
|
||||
onContinue: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export default function TokenSafetyModal({
|
||||
isOpen,
|
||||
tokenAddress,
|
||||
secondTokenAddress,
|
||||
onContinue,
|
||||
onCancel,
|
||||
}: TokenSafetyModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onCancel}>
|
||||
<TokenSafety
|
||||
tokenAddress={tokenAddress}
|
||||
secondTokenAddress={secondTokenAddress}
|
||||
onCancel={onCancel}
|
||||
onContinue={onContinue}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
268
src/components/TokenSafety/index.tsx
Normal file
268
src/components/TokenSafety/index.tsx
Normal file
@ -0,0 +1,268 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
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 { 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 { ButtonText, CopyLinkIcon, ExternalLink } from 'theme'
|
||||
import { Color } from 'theme/styled'
|
||||
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
padding: 32px 50px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const LogoContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
const ShortColumn = styled(AutoColumn)`
|
||||
margin-top: 10px;
|
||||
`
|
||||
|
||||
const InfoText = styled(Text)`
|
||||
padding: 0 12px 0 12px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const StyledButton = styled(ButtonPrimary)<{ buttonColor: Color; textColor: Color }>`
|
||||
color: ${({ textColor }) => textColor};
|
||||
background-color: ${({ buttonColor }) => buttonColor};
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
:hover {
|
||||
background-color: ${({ buttonColor, theme }) => buttonColor ?? theme.accentAction};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledCancelButton = styled(ButtonText)<{ color?: Color }>`
|
||||
margin-top: 16px;
|
||||
color: ${({ color, theme }) => color ?? theme.accentAction};
|
||||
`
|
||||
|
||||
const Buttons = ({
|
||||
warning,
|
||||
onContinue,
|
||||
onCancel,
|
||||
}: {
|
||||
warning: Warning
|
||||
onContinue: () => void
|
||||
onCancel: () => void
|
||||
}) => {
|
||||
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.backgroundAction
|
||||
break
|
||||
}
|
||||
return warning.canProceed ? (
|
||||
<>
|
||||
<StyledButton buttonColor={buttonColor} textColor={textColor} onClick={onContinue}>
|
||||
<Trans>I Understand</Trans>
|
||||
</StyledButton>
|
||||
<StyledCancelButton color={cancelColor} onClick={onCancel}>
|
||||
Cancel
|
||||
</StyledCancelButton>
|
||||
</>
|
||||
) : (
|
||||
<StyledButton buttonColor={buttonColor} textColor={textColor} onClick={onCancel}>
|
||||
<Trans>Close</Trans>
|
||||
</StyledButton>
|
||||
)
|
||||
}
|
||||
|
||||
const SafetyLabel = ({ warning }: { warning: Warning }) => {
|
||||
return (
|
||||
<TokenSafetyLabel level={warning.level} canProceed={warning.canProceed}>
|
||||
{warning.message}
|
||||
</TokenSafetyLabel>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Replace color with stylesheet color
|
||||
const LinkColumn = styled(AutoColumn)`
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const ExplorerContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
margin-top: 10px;
|
||||
font-size: 20px;
|
||||
background-color: ${({ theme }) => theme.accentActiveSoft};
|
||||
color: ${({ theme }) => theme.accentActive};
|
||||
border-radius: 8px;
|
||||
padding: 2px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const ExplorerLinkWrapper = styled.div`
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
:active {
|
||||
opacity: 0.4;
|
||||
}
|
||||
`
|
||||
|
||||
const ExplorerLink = styled.div`
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`
|
||||
const ExplorerLinkIcon = styled(LinkIconFeather)`
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
margin-left: 8px;
|
||||
`
|
||||
|
||||
const LinkIconWrapper = styled.div`
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
`
|
||||
|
||||
export function ExternalLinkIcon() {
|
||||
return (
|
||||
<LinkIconWrapper>
|
||||
<ExplorerLinkIcon />
|
||||
</LinkIconWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function ExplorerView({ token }: { token: Token }) {
|
||||
if (token) {
|
||||
const explorerLink = getExplorerLink(token?.chainId, token?.address, ExplorerDataType.TOKEN)
|
||||
return (
|
||||
<ExplorerContainer>
|
||||
<ExplorerLinkWrapper onClick={() => window.open(explorerLink, '_blank')}>
|
||||
<ExplorerLink>{explorerLink}</ExplorerLink>
|
||||
<ExternalLinkIcon />
|
||||
</ExplorerLinkWrapper>
|
||||
<CopyLinkIcon toCopy={explorerLink} />
|
||||
</ExplorerContainer>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
interface TokenSafetyProps {
|
||||
tokenAddress: string | null
|
||||
secondTokenAddress?: string
|
||||
onContinue: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export default function TokenSafety({ tokenAddress, secondTokenAddress, onContinue, onCancel }: TokenSafetyProps) {
|
||||
const logos = []
|
||||
const urls = []
|
||||
|
||||
const token1Warning = tokenAddress ? checkWarning(tokenAddress) : null
|
||||
const token1 = useToken(tokenAddress)
|
||||
const token2Warning = secondTokenAddress ? checkWarning(secondTokenAddress) : null
|
||||
const token2 = useToken(secondTokenAddress)
|
||||
|
||||
const token1Unsupported = !token1Warning?.canProceed
|
||||
const token2Unsupported = !token2Warning?.canProceed
|
||||
|
||||
// Logic for only showing the 'unsupported' warning if one is supported and other isn't
|
||||
if (token1 && token1Warning && (token1Unsupported || !(token2Warning && token2Unsupported))) {
|
||||
logos.push(<CurrencyLogo currency={token1} size="48px" />)
|
||||
urls.push(<ExplorerView token={token1} />)
|
||||
}
|
||||
if (token2 && token2Warning && (token2Unsupported || !(token1Warning && token1Unsupported))) {
|
||||
logos.push(<CurrencyLogo currency={token2} size="48px" />)
|
||||
urls.push(<ExplorerView token={token2} />)
|
||||
}
|
||||
|
||||
const plural = logos.length > 1
|
||||
// Show higher level warning if two are present
|
||||
let displayWarning = token1Warning
|
||||
if (!token1Warning || (token2Warning && token2Unsupported && !token1Unsupported)) {
|
||||
displayWarning = token2Warning
|
||||
}
|
||||
|
||||
// If a warning is acknowledged, import these tokens
|
||||
const addToken = useAddUserToken()
|
||||
const acknowledge = () => {
|
||||
if (token1) {
|
||||
addToken(token1)
|
||||
}
|
||||
if (token2) {
|
||||
addToken(token2)
|
||||
}
|
||||
onContinue()
|
||||
}
|
||||
|
||||
const { heading, description } = getWarningCopy(displayWarning, plural)
|
||||
|
||||
return (
|
||||
displayWarning && (
|
||||
<Wrapper>
|
||||
<Container>
|
||||
<AutoColumn>
|
||||
<LogoContainer>{logos}</LogoContainer>
|
||||
</AutoColumn>
|
||||
<ShortColumn>
|
||||
<SafetyLabel warning={displayWarning} />
|
||||
</ShortColumn>
|
||||
<ShortColumn>{heading && <InfoText fontSize="20px">{heading}</InfoText>}</ShortColumn>
|
||||
<ShortColumn>
|
||||
<InfoText>
|
||||
{description}{' '}
|
||||
<ExternalLink href={TOKEN_SAFETY_ARTICLE}>
|
||||
<Trans>Learn More</Trans>
|
||||
</ExternalLink>
|
||||
</InfoText>
|
||||
</ShortColumn>
|
||||
<LinkColumn>{urls}</LinkColumn>
|
||||
<Buttons warning={displayWarning} onContinue={acknowledge} onCancel={onCancel} />
|
||||
</Container>
|
||||
</Wrapper>
|
||||
)
|
||||
)
|
||||
}
|
54
src/constants/TokenSafetyLookupTable.ts
Normal file
54
src/constants/TokenSafetyLookupTable.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import store from '../state'
|
||||
import { UNI_EXTENDED_LIST, UNI_LIST } from './lists'
|
||||
import brokenTokenList from './tokenLists/broken.tokenlist.json'
|
||||
import unsupportedTokenList from './tokenLists/unsupported.tokenlist.json'
|
||||
|
||||
export enum TOKEN_LIST_TYPES {
|
||||
UNI_DEFAULT = 1,
|
||||
UNI_EXTENDED,
|
||||
UNKNOWN,
|
||||
BLOCKED,
|
||||
BROKEN,
|
||||
}
|
||||
|
||||
class TokenSafetyLookupTable {
|
||||
dict: { [key: string]: TOKEN_LIST_TYPES } | null = null
|
||||
|
||||
createMap() {
|
||||
const dict: { [key: string]: TOKEN_LIST_TYPES } = {}
|
||||
let uniDefaultTokens = store.getState().lists.byUrl[UNI_LIST].current?.tokens
|
||||
let uniExtendedTokens = store.getState().lists.byUrl[UNI_EXTENDED_LIST].current?.tokens
|
||||
const brokenTokens = brokenTokenList.tokens
|
||||
const unsupportTokens = unsupportedTokenList.tokens
|
||||
|
||||
if (!uniDefaultTokens) {
|
||||
uniDefaultTokens = []
|
||||
}
|
||||
if (!uniExtendedTokens) {
|
||||
uniExtendedTokens = []
|
||||
}
|
||||
brokenTokens.forEach((token) => {
|
||||
dict[token.address.toLowerCase()] = TOKEN_LIST_TYPES.BROKEN
|
||||
})
|
||||
unsupportTokens.forEach((token) => {
|
||||
dict[token.address.toLowerCase()] = TOKEN_LIST_TYPES.BLOCKED
|
||||
})
|
||||
uniExtendedTokens.forEach((token) => {
|
||||
dict[token.address.toLowerCase()] = TOKEN_LIST_TYPES.UNI_EXTENDED
|
||||
})
|
||||
uniDefaultTokens.forEach((token) => {
|
||||
dict[token.address.toLowerCase()] = TOKEN_LIST_TYPES.UNI_DEFAULT
|
||||
})
|
||||
|
||||
return dict
|
||||
}
|
||||
|
||||
checkToken(address: string) {
|
||||
if (!this.dict) {
|
||||
this.dict = this.createMap()
|
||||
}
|
||||
return this.dict[address] ?? TOKEN_LIST_TYPES.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
export default new TokenSafetyLookupTable()
|
@ -4,6 +4,7 @@ import celoLogo from 'assets/svg/celo_logo.svg'
|
||||
import optimismLogoUrl from 'assets/svg/optimistic_ethereum.svg'
|
||||
import polygonMaticLogo from 'assets/svg/polygon-matic-logo.svg'
|
||||
import ms from 'ms.macro'
|
||||
import { colorsDark } from 'theme/colors'
|
||||
|
||||
import { SupportedChainId, SupportedL1ChainId, SupportedL2ChainId } from './chains'
|
||||
import { ARBITRUM_LIST, CELO_LIST, OPTIMISM_LIST } from './lists'
|
||||
@ -28,6 +29,8 @@ interface BaseChainInfo {
|
||||
symbol: string // e.g. 'gorETH',
|
||||
decimals: number // e.g. 18,
|
||||
}
|
||||
readonly color?: string
|
||||
readonly backgroundColor?: string
|
||||
}
|
||||
|
||||
export interface L1ChainInfo extends BaseChainInfo {
|
||||
@ -55,6 +58,7 @@ const CHAIN_INFO: ChainInfoMap = {
|
||||
label: 'Ethereum',
|
||||
logoUrl: ethereumLogoUrl,
|
||||
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
||||
color: colorsDark.chain_1,
|
||||
},
|
||||
[SupportedChainId.RINKEBY]: {
|
||||
networkType: NetworkType.L1,
|
||||
@ -64,6 +68,7 @@ const CHAIN_INFO: ChainInfoMap = {
|
||||
label: 'Rinkeby',
|
||||
logoUrl: ethereumLogoUrl,
|
||||
nativeCurrency: { name: 'Rinkeby Ether', symbol: 'rETH', decimals: 18 },
|
||||
color: colorsDark.chain_4,
|
||||
},
|
||||
[SupportedChainId.ROPSTEN]: {
|
||||
networkType: NetworkType.L1,
|
||||
@ -73,6 +78,7 @@ const CHAIN_INFO: ChainInfoMap = {
|
||||
label: 'Ropsten',
|
||||
logoUrl: ethereumLogoUrl,
|
||||
nativeCurrency: { name: 'Ropsten Ether', symbol: 'ropETH', decimals: 18 },
|
||||
color: colorsDark.chain_3,
|
||||
},
|
||||
[SupportedChainId.KOVAN]: {
|
||||
networkType: NetworkType.L1,
|
||||
@ -82,6 +88,7 @@ const CHAIN_INFO: ChainInfoMap = {
|
||||
label: 'Kovan',
|
||||
logoUrl: ethereumLogoUrl,
|
||||
nativeCurrency: { name: 'Kovan Ether', symbol: 'kovETH', decimals: 18 },
|
||||
color: colorsDark.chain_69,
|
||||
},
|
||||
[SupportedChainId.GOERLI]: {
|
||||
networkType: NetworkType.L1,
|
||||
@ -91,6 +98,7 @@ const CHAIN_INFO: ChainInfoMap = {
|
||||
label: 'Görli',
|
||||
logoUrl: ethereumLogoUrl,
|
||||
nativeCurrency: { name: 'Görli Ether', symbol: 'görETH', decimals: 18 },
|
||||
color: colorsDark.chain_5,
|
||||
},
|
||||
[SupportedChainId.OPTIMISM]: {
|
||||
networkType: NetworkType.L2,
|
||||
@ -105,6 +113,8 @@ const CHAIN_INFO: ChainInfoMap = {
|
||||
statusPage: 'https://optimism.io/status',
|
||||
helpCenterUrl: 'https://help.uniswap.org/en/collections/3137778-uniswap-on-optimistic-ethereum-oξ',
|
||||
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
||||
color: colorsDark.chain_10,
|
||||
backgroundColor: colorsDark.chain_10_background,
|
||||
},
|
||||
[SupportedChainId.OPTIMISTIC_KOVAN]: {
|
||||
networkType: NetworkType.L2,
|
||||
@ -119,6 +129,7 @@ const CHAIN_INFO: ChainInfoMap = {
|
||||
statusPage: 'https://optimism.io/status',
|
||||
helpCenterUrl: 'https://help.uniswap.org/en/collections/3137778-uniswap-on-optimistic-ethereum-oξ',
|
||||
nativeCurrency: { name: 'Optimistic Kovan Ether', symbol: 'kovOpETH', decimals: 18 },
|
||||
color: colorsDark.chain_69,
|
||||
},
|
||||
[SupportedChainId.ARBITRUM_ONE]: {
|
||||
networkType: NetworkType.L2,
|
||||
@ -132,6 +143,8 @@ const CHAIN_INFO: ChainInfoMap = {
|
||||
defaultListUrl: ARBITRUM_LIST,
|
||||
helpCenterUrl: 'https://help.uniswap.org/en/collections/3137787-uniswap-on-arbitrum',
|
||||
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
||||
color: colorsDark.chain_42,
|
||||
backgroundColor: colorsDark.chain_42161_background,
|
||||
},
|
||||
[SupportedChainId.ARBITRUM_RINKEBY]: {
|
||||
networkType: NetworkType.L2,
|
||||
@ -145,6 +158,7 @@ const CHAIN_INFO: ChainInfoMap = {
|
||||
defaultListUrl: ARBITRUM_LIST,
|
||||
helpCenterUrl: 'https://help.uniswap.org/en/collections/3137787-uniswap-on-arbitrum',
|
||||
nativeCurrency: { name: 'Rinkeby Arbitrum Ether', symbol: 'rinkArbETH', decimals: 18 },
|
||||
color: colorsDark.chain_421611,
|
||||
},
|
||||
[SupportedChainId.POLYGON]: {
|
||||
networkType: NetworkType.L1,
|
||||
@ -156,6 +170,8 @@ const CHAIN_INFO: ChainInfoMap = {
|
||||
label: 'Polygon',
|
||||
logoUrl: polygonMaticLogo,
|
||||
nativeCurrency: { name: 'Polygon Matic', symbol: 'MATIC', decimals: 18 },
|
||||
color: colorsDark.chain_137,
|
||||
backgroundColor: colorsDark.chain_137_background,
|
||||
},
|
||||
[SupportedChainId.POLYGON_MUMBAI]: {
|
||||
networkType: NetworkType.L1,
|
||||
|
@ -65,6 +65,18 @@ export const UNSUPPORTED_V2POOL_CHAIN_IDS = [
|
||||
SupportedChainId.ARBITRUM_ONE,
|
||||
]
|
||||
|
||||
export const TESTNET_CHAIN_IDS = [
|
||||
SupportedChainId.ROPSTEN,
|
||||
SupportedChainId.RINKEBY,
|
||||
SupportedChainId.GOERLI,
|
||||
SupportedChainId.KOVAN,
|
||||
SupportedChainId.POLYGON_MUMBAI,
|
||||
SupportedChainId.ARBITRUM_RINKEBY,
|
||||
SupportedChainId.OPTIMISTIC_KOVAN,
|
||||
] as const
|
||||
|
||||
export type SupportedTestnetChainId = typeof TESTNET_CHAIN_IDS[number]
|
||||
|
||||
/**
|
||||
* All the chain IDs that are running the Ethereum protocol.
|
||||
*/
|
||||
|
@ -1,4 +1,5 @@
|
||||
const UNI_LIST = 'https://tokens.uniswap.org'
|
||||
export const UNI_LIST = 'https://tokens.uniswap.org'
|
||||
export const UNI_EXTENDED_LIST = 'https://gateway.pinata.cloud/ipfs/QmaQvV3pWKKaWJcHvSBuvQMrpckV3KKtGJ6p3HZjakwFtX'
|
||||
const AAVE_LIST = 'tokenlist.aave.eth'
|
||||
const BA_LIST = 'https://raw.githubusercontent.com/The-Blockchain-Association/sec-notice-list/master/ba-sec-list.json'
|
||||
const CMC_ALL_LIST = 'https://api.coinmarketcap.com/data-api/v3/uniswap/all.json'
|
||||
@ -20,6 +21,7 @@ export const UNSUPPORTED_LIST_URLS: string[] = [BA_LIST]
|
||||
// lower index == higher priority for token import
|
||||
const DEFAULT_LIST_OF_LISTS_TO_DISPLAY: string[] = [
|
||||
UNI_LIST,
|
||||
UNI_EXTENDED_LIST,
|
||||
COMPOUND_LIST,
|
||||
AAVE_LIST,
|
||||
CMC_ALL_LIST,
|
||||
|
73
src/constants/tokenSafety.tsx
Normal file
73
src/constants/tokenSafety.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { Plural, Trans } from '@lingui/macro'
|
||||
|
||||
import WarningCache, { TOKEN_LIST_TYPES } from './TokenSafetyLookupTable'
|
||||
|
||||
// TODO: Replace this with Steph's article when it is available.
|
||||
export const TOKEN_SAFETY_ARTICLE = 'https://help.uniswap.org/en/'
|
||||
|
||||
export enum WARNING_LEVEL {
|
||||
MEDIUM,
|
||||
UNKNOWN,
|
||||
BLOCKED,
|
||||
}
|
||||
|
||||
export function getWarningCopy(warning: Warning | null, plural = false) {
|
||||
let heading = null,
|
||||
description = null
|
||||
if (warning) {
|
||||
if (warning.canProceed) {
|
||||
heading = <Plural value={plural ? 2 : 1} _1="This token isn't verified" other="These tokens aren't verified" />
|
||||
description = <Trans>{'Please do your own research before trading.'}</Trans>
|
||||
} else {
|
||||
description = (
|
||||
<Plural
|
||||
value={plural ? 2 : 1}
|
||||
_1="You can't trade this token using the Uniswap App."
|
||||
other="You can't trade these tokens using the Uniswap App."
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
return { heading, description }
|
||||
}
|
||||
|
||||
export type Warning = {
|
||||
level: WARNING_LEVEL
|
||||
message: JSX.Element
|
||||
/* canProceed determines whether triangle/octagon alert icon is used, and
|
||||
whether this token is supported/able to be traded */
|
||||
canProceed: boolean
|
||||
}
|
||||
|
||||
const MediumWarning: Warning = {
|
||||
level: WARNING_LEVEL.MEDIUM,
|
||||
message: <Trans>Caution</Trans>,
|
||||
canProceed: true,
|
||||
}
|
||||
|
||||
const StrongWarning: Warning = {
|
||||
level: WARNING_LEVEL.UNKNOWN,
|
||||
message: <Trans>Warning</Trans>,
|
||||
canProceed: true,
|
||||
}
|
||||
|
||||
const BlockedWarning: Warning = {
|
||||
level: WARNING_LEVEL.BLOCKED,
|
||||
message: <Trans>Not Available</Trans>,
|
||||
canProceed: false,
|
||||
}
|
||||
|
||||
export function checkWarning(tokenAddress: string) {
|
||||
switch (WarningCache.checkToken(tokenAddress.toLowerCase())) {
|
||||
case TOKEN_LIST_TYPES.UNI_DEFAULT:
|
||||
return null
|
||||
case TOKEN_LIST_TYPES.UNI_EXTENDED:
|
||||
return MediumWarning
|
||||
case TOKEN_LIST_TYPES.UNKNOWN:
|
||||
return StrongWarning
|
||||
case TOKEN_LIST_TYPES.BLOCKED:
|
||||
return BlockedWarning
|
||||
case TOKEN_LIST_TYPES.BROKEN:
|
||||
return BlockedWarning
|
||||
}
|
||||
}
|
@ -19,11 +19,12 @@ export function useFeatureFlagsContext(): FeatureFlagsContextType {
|
||||
export function FeatureFlagsProvider({ children }: { children: ReactNode }) {
|
||||
// TODO(vm): `isLoaded` to `true` so `App.tsx` will render. Later, this will be dependent on
|
||||
// flags loading from Amplitude, with a timeout.
|
||||
const variant = process.env.NODE_ENV === 'development' ? 'enabled' : 'control'
|
||||
const value = {
|
||||
isLoaded: true,
|
||||
flags: {
|
||||
phase0: 'control',
|
||||
phase1: 'control',
|
||||
phase0: variant,
|
||||
phase1: variant,
|
||||
},
|
||||
}
|
||||
return <FeatureFlagContext.Provider value={value}>{children}</FeatureFlagContext.Provider>
|
||||
|
69
src/hooks/useNetworkTokenBalances.ts
Normal file
69
src/hooks/useNetworkTokenBalances.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { gql } from 'graphql-request'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
type NetworkTokenBalancesMap = Partial<Record<SupportedChainId, CurrencyAmount<Token>>>
|
||||
|
||||
interface useNetworkTokenBalancesResult {
|
||||
data: NetworkTokenBalancesMap | null
|
||||
error: null | string
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
interface useNetworkTokenBalancesArgs {
|
||||
address: string
|
||||
}
|
||||
|
||||
export function useNetworkTokenBalances({ address }: useNetworkTokenBalancesArgs): useNetworkTokenBalancesResult {
|
||||
const [data, setData] = useState<NetworkTokenBalancesMap | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const query = gql``
|
||||
|
||||
useEffect(() => {
|
||||
const FAKE_TOKEN_NETWORK_BALANCES = {
|
||||
[SupportedChainId.ARBITRUM_ONE]: CurrencyAmount.fromRawAmount(
|
||||
new Token(SupportedChainId.ARBITRUM_ONE, address, 18),
|
||||
10e18
|
||||
),
|
||||
[SupportedChainId.MAINNET]: CurrencyAmount.fromRawAmount(new Token(SupportedChainId.MAINNET, address, 18), 1e18),
|
||||
[SupportedChainId.RINKEBY]: CurrencyAmount.fromRawAmount(new Token(SupportedChainId.RINKEBY, address, 9), 10e18),
|
||||
}
|
||||
|
||||
const fetchNetworkTokenBalances = async (address: string): Promise<NetworkTokenBalancesMap | void> => {
|
||||
const waitRandom = (min: number, max: number): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, min + Math.round(Math.random() * Math.max(0, max - min))))
|
||||
try {
|
||||
console.log('useNetworkTokenBalances.fetchNetworkTokenBalances', query)
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
console.log('useNetworkTokenBalances.fetchNetworkTokenBalances', address)
|
||||
await waitRandom(250, 2000)
|
||||
if (Math.random() < 0.05) {
|
||||
throw new Error('fake error')
|
||||
}
|
||||
return FAKE_TOKEN_NETWORK_BALANCES
|
||||
} catch (e) {
|
||||
setError('something went wrong')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
fetchNetworkTokenBalances(address)
|
||||
.then((data) => {
|
||||
if (data) setData(data)
|
||||
})
|
||||
.catch((e) => setError(e))
|
||||
.finally(() => setLoading(false))
|
||||
}, [address, query])
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
loading,
|
||||
}
|
||||
}
|
128
src/hooks/useTokenDetailPageQuery.tsx
Normal file
128
src/hooks/useTokenDetailPageQuery.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { isAddress } from 'ethers/lib/utils'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// mock data relies on this wip reference:
|
||||
// https://www.notion.so/uniswaplabs/GraphQL-Schema-eebbd70635ae4acc851e2542cb5de575
|
||||
|
||||
enum Currency {
|
||||
USD,
|
||||
}
|
||||
|
||||
enum TimePeriod {
|
||||
hour = 'hour',
|
||||
day = 'day',
|
||||
week = 'week',
|
||||
month = 'month',
|
||||
year = 'year',
|
||||
max = 'max',
|
||||
}
|
||||
|
||||
interface HistoricalPrice {
|
||||
id: string
|
||||
currency: Currency
|
||||
priceInCurrency: number
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
type TokenDetailPageQueryResult = {
|
||||
priceHistory: Partial<Record<SupportedChainId, HistoricalPrice[]>>
|
||||
links: {
|
||||
name: string
|
||||
url: string
|
||||
displayable_name: string
|
||||
}[]
|
||||
marketCap: number
|
||||
volume: {
|
||||
[TimePeriod.day]: number
|
||||
}
|
||||
}
|
||||
|
||||
interface UseTokenDetailPageQueryResult {
|
||||
data: TokenDetailPageQueryResult | null
|
||||
error: string | null
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const FAKE_TOKEN_DETAIL_PAGE_QUERY_RESULT: TokenDetailPageQueryResult = {
|
||||
priceHistory: {
|
||||
[SupportedChainId.MAINNET]: [
|
||||
{
|
||||
id: 'string',
|
||||
currency: Currency.USD,
|
||||
priceInCurrency: 1000,
|
||||
timestamp: 'Sat Jul 23 2022 08:35:30 GMT-0000',
|
||||
},
|
||||
{
|
||||
id: 'string',
|
||||
currency: Currency.USD,
|
||||
priceInCurrency: 1100,
|
||||
timestamp: 'Sat Jul 23 2022 09:35:30 GMT-0000',
|
||||
},
|
||||
{
|
||||
id: 'string',
|
||||
currency: Currency.USD,
|
||||
priceInCurrency: 900,
|
||||
timestamp: 'Sat Jul 23 2022 10:35:30 GMT-0000',
|
||||
},
|
||||
],
|
||||
},
|
||||
links: [
|
||||
{
|
||||
name: 'github',
|
||||
url: 'https://github.com/JFrankfurt',
|
||||
displayable_name: 'Github',
|
||||
},
|
||||
{
|
||||
name: 'twitter',
|
||||
url: 'https://twitter.com/JordanFrankfurt',
|
||||
displayable_name: 'Twitter',
|
||||
},
|
||||
],
|
||||
marketCap: 1_000_000_000,
|
||||
volume: {
|
||||
[TimePeriod.day]: 1_000_000,
|
||||
},
|
||||
}
|
||||
|
||||
const useTokenDetailPageQuery = (tokenAddress: string | undefined): UseTokenDetailPageQueryResult => {
|
||||
const [data, setData] = useState<TokenDetailPageQueryResult | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const fetchTokenDetails = async (addresses: string): Promise<TokenDetailPageQueryResult | void> => {
|
||||
const waitRandom = (min: number, max: number): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, min + Math.round(Math.random() * Math.max(0, max - min))))
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
await waitRandom(250, 2000)
|
||||
if (Math.random() < 0.05) {
|
||||
throw new Error('fake error')
|
||||
}
|
||||
console.log('fetchTokenDetails', addresses)
|
||||
return FAKE_TOKEN_DETAIL_PAGE_QUERY_RESULT
|
||||
} catch (e) {
|
||||
setError('something went wrong')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (tokenAddress && isAddress(tokenAddress)) {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
fetchTokenDetails(tokenAddress)
|
||||
.then((data) => {
|
||||
if (data) setData(data)
|
||||
})
|
||||
.catch((e) => setError(e))
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
}, [tokenAddress])
|
||||
|
||||
return { data, error, loading }
|
||||
}
|
||||
|
||||
export default useTokenDetailPageQuery
|
85
src/hooks/useTokenPrice.ts
Normal file
85
src/hooks/useTokenPrice.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
enum TimePeriod {
|
||||
hour = 'hour',
|
||||
day = 'day',
|
||||
week = 'week',
|
||||
month = 'month',
|
||||
year = 'year',
|
||||
}
|
||||
|
||||
type Dictionary<K extends keyof any, T> = Partial<Record<K, T>>
|
||||
|
||||
type TokenData = {
|
||||
[address: string]: {
|
||||
price: number
|
||||
delta: Dictionary<TimePeriod, number>
|
||||
}
|
||||
}
|
||||
|
||||
interface UseTokenPriceResult {
|
||||
data: TokenData | null
|
||||
error: string | null
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const FAKE_TOKEN_PRICE_RESULT = {
|
||||
'0x03ab458634910aad20ef5f1c8ee96f1d6ac54919': {
|
||||
price: 3.05,
|
||||
delta: {
|
||||
[TimePeriod.hour]: 25_000,
|
||||
[TimePeriod.day]: 619_000,
|
||||
[TimePeriod.week]: 16_800_000,
|
||||
[TimePeriod.month]: 58_920_000,
|
||||
},
|
||||
},
|
||||
'0x0cec1a9154ff802e7934fc916ed7ca50bde6844e': {
|
||||
price: 0.66543,
|
||||
delta: {
|
||||
[TimePeriod.hour]: 5_000,
|
||||
[TimePeriod.day]: 100_000,
|
||||
[TimePeriod.week]: 800_000,
|
||||
[TimePeriod.month]: 4_920_000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const useTokenPrice = (tokenAddresses: Set<string>): UseTokenPriceResult => {
|
||||
const [data, setData] = useState<TokenData | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const fetchTokenPrices = async (addresses: Set<string>): Promise<TokenData | void> => {
|
||||
const waitRandom = (min: number, max: number): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, min + Math.round(Math.random() * Math.max(0, max - min))))
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
await waitRandom(250, 2000)
|
||||
if (Math.random() < 0.05) {
|
||||
throw new Error('fake error')
|
||||
}
|
||||
console.log('fetchTokenPrices', addresses)
|
||||
return FAKE_TOKEN_PRICE_RESULT
|
||||
} catch (e) {
|
||||
setError('something went wrong')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
fetchTokenPrices(tokenAddresses)
|
||||
.then((data) => {
|
||||
if (data) setData(data)
|
||||
})
|
||||
.catch((e) => setError(e))
|
||||
.finally(() => setLoading(false))
|
||||
}, [tokenAddresses])
|
||||
|
||||
return { data, error, loading }
|
||||
}
|
||||
|
||||
export default useTokenPrice
|
84
src/hooks/useTokenRelevantResources.ts
Normal file
84
src/hooks/useTokenRelevantResources.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface RelevantResource {
|
||||
name: string
|
||||
url: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
interface RelevantResourcesMap {
|
||||
[address: string]: RelevantResource[]
|
||||
}
|
||||
|
||||
interface useTokenRelevantResourcesResult {
|
||||
data: RelevantResourcesMap | null
|
||||
error: string | null
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const FAKE_TOKEN_RELEVANT_RESOURCES = {
|
||||
'0x03ab458634910aad20ef5f1c8ee96f1d6ac54919': [
|
||||
{
|
||||
name: 'github',
|
||||
url: 'https://github.com/reflexer-labs/',
|
||||
displayName: 'Github',
|
||||
},
|
||||
{
|
||||
name: 'website',
|
||||
url: 'https://reflexer.finance/',
|
||||
displayName: 'reflexer.finance',
|
||||
},
|
||||
],
|
||||
'0x0cec1a9154ff802e7934fc916ed7ca50bde6844e': [
|
||||
{
|
||||
name: 'github',
|
||||
url: 'https://github.com/pooltogether/',
|
||||
displayName: 'Github',
|
||||
},
|
||||
{
|
||||
name: 'website',
|
||||
url: 'https://pooltogether.com/',
|
||||
displayName: 'pooltogether.com',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const useTokenRelevantResources = (addresses: Set<string>): useTokenRelevantResourcesResult => {
|
||||
const [data, setData] = useState<RelevantResourcesMap | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const fetchRelevantResources = async (addresses: Set<string>): Promise<RelevantResourcesMap | void> => {
|
||||
const waitRandom = (min: number, max: number): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, min + Math.round(Math.random() * Math.max(0, max - min))))
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
console.log('useTokenRelevantResources.fetchRelevantResources', addresses)
|
||||
await waitRandom(250, 2000)
|
||||
if (Math.random() < 0.05) {
|
||||
throw new Error('fake error')
|
||||
}
|
||||
return FAKE_TOKEN_RELEVANT_RESOURCES
|
||||
} catch (e) {
|
||||
setError('something went wrong')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
fetchRelevantResources(addresses)
|
||||
.then((data) => {
|
||||
if (data) setData(data)
|
||||
})
|
||||
.catch((e) => setError(e))
|
||||
.finally(() => setLoading(false))
|
||||
}, [addresses])
|
||||
|
||||
return { data, error, loading }
|
||||
}
|
||||
|
||||
export default useTokenRelevantResources
|
21
src/hooks/useTokenWarningColor.ts
Normal file
21
src/hooks/useTokenWarningColor.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { WARNING_LEVEL } from 'constants/tokenSafety'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
|
||||
export const useTokenWarningColor = (level: WARNING_LEVEL) => {
|
||||
const [color, setColor] = useState('')
|
||||
const theme = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
switch (level) {
|
||||
case WARNING_LEVEL.MEDIUM:
|
||||
return setColor(theme.accentWarning)
|
||||
case WARNING_LEVEL.UNKNOWN:
|
||||
return setColor(theme.accentFailure)
|
||||
case WARNING_LEVEL.BLOCKED:
|
||||
return setColor(theme.textSecondary)
|
||||
}
|
||||
}, [level, theme])
|
||||
|
||||
return color
|
||||
}
|
322
src/hooks/useTopTokens.ts
Normal file
322
src/hooks/useTopTokens.ts
Normal file
File diff suppressed because one or more lines are too long
@ -8,6 +8,7 @@ import { useMemo } from 'react'
|
||||
import { getTxOptimizedSwapRouter, SwapRouterVersion } from 'utils/getTxOptimizedSwapRouter'
|
||||
|
||||
import { ApprovalState, useApproval, useApprovalStateForSpender } from '../useApproval'
|
||||
|
||||
export { ApprovalState } from '../useApproval'
|
||||
|
||||
/** Returns approval state for all known swap routers */
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { initializeAnalytics } from 'components/AmplitudeAnalytics'
|
||||
import { sendAnalyticsEvent, user } from 'components/AmplitudeAnalytics'
|
||||
import { initializeAnalytics, sendAnalyticsEvent, user } from 'components/AmplitudeAnalytics'
|
||||
import { CUSTOM_USER_PROPERTIES, EventName, PageName } from 'components/AmplitudeAnalytics/constants'
|
||||
import { Trace } from 'components/AmplitudeAnalytics/Trace'
|
||||
import Loader from 'components/Loader'
|
||||
import TopLevelModals from 'components/TopLevelModals'
|
||||
import { useFeatureFlagsIsLoaded } from 'featureFlags'
|
||||
import { Phase0Variant, usePhase0Flag } from 'featureFlags/flags/phase0'
|
||||
import ApeModeQueryParamReader from 'hooks/useApeModeQueryParamReader'
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { lazy, Suspense, useEffect } from 'react'
|
||||
import { Navigate, Route, Routes, useLocation } from 'react-router-dom'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { SpinnerSVG } from 'theme/components'
|
||||
import { getBrowser } from 'utils/browser'
|
||||
|
||||
import { useAnalyticsReporter } from '../components/analytics'
|
||||
@ -25,6 +25,7 @@ import { RedirectDuplicateTokenIds } from './AddLiquidity/redirects'
|
||||
import { RedirectDuplicateTokenIdsV2 } from './AddLiquidityV2/redirects'
|
||||
import Earn from './Earn'
|
||||
import Manage from './Earn/Manage'
|
||||
import Explore from './Explore'
|
||||
import MigrateV2 from './MigrateV2'
|
||||
import MigrateV2Pair from './MigrateV2/MigrateV2Pair'
|
||||
import Pool from './Pool'
|
||||
@ -36,7 +37,7 @@ import RemoveLiquidityV3 from './RemoveLiquidity/V3'
|
||||
import Swap from './Swap'
|
||||
import { OpenClaimAddressModalAndRedirectToSwap, RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
|
||||
|
||||
// lazy load vote related pages
|
||||
const TokenDetails = lazy(() => import('./TokenDetails'))
|
||||
const Vote = lazy(() => import('./Vote'))
|
||||
|
||||
const AppWrapper = styled.div`
|
||||
@ -80,13 +81,30 @@ function getCurrentPageFromLocation(locationPathname: string): PageName | undefi
|
||||
return PageName.VOTE_PAGE
|
||||
case '/pool':
|
||||
return PageName.POOL_PAGE
|
||||
case '/explore':
|
||||
return PageName.EXPLORE_PAGE
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
// this is the same svg defined in assets/images/blue-loader.svg
|
||||
// it is defined here because the remote asset may not have had time to load when this file is executing
|
||||
const LazyLoadSpinner = () => (
|
||||
<SpinnerSVG width="94" height="94" viewBox="0 0 94 94" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M92 47C92 22.1472 71.8528 2 47 2C22.1472 2 2 22.1472 2 47C2 71.8528 22.1472 92 47 92"
|
||||
stroke="#2172E5"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</SpinnerSVG>
|
||||
)
|
||||
|
||||
export default function App() {
|
||||
const isLoaded = useFeatureFlagsIsLoaded()
|
||||
const phase0Flag = usePhase0Flag()
|
||||
|
||||
const { pathname } = useLocation()
|
||||
const currentPage = getCurrentPageFromLocation(pathname)
|
||||
@ -132,7 +150,27 @@ export default function App() {
|
||||
<Suspense fallback={<Loader />}>
|
||||
{isLoaded ? (
|
||||
<Routes>
|
||||
<Route path="vote/*" element={<Vote />} />
|
||||
{phase0Flag === Phase0Variant.Enabled && (
|
||||
<>
|
||||
<Route path="/explore" element={<Explore />} />
|
||||
<Route
|
||||
path="/tokens/:tokenAddress"
|
||||
element={
|
||||
<Suspense fallback={<LazyLoadSpinner />}>
|
||||
<TokenDetails />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Route
|
||||
path="vote/*"
|
||||
element={
|
||||
<Suspense fallback={<LazyLoadSpinner />}>
|
||||
<Vote />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route path="create-proposal" element={<Navigate to="/vote/create-proposal" replace />} />
|
||||
<Route path="claim" element={<OpenClaimAddressModalAndRedirectToSwap />} />
|
||||
<Route path="uni" element={<Earn />} />
|
||||
|
@ -11,7 +11,7 @@ import { AutoColumn } from 'components/Column'
|
||||
import JSBI from 'jsbi'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { Wrapper } from 'pages/Pool/styleds'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import {
|
||||
CreateProposalData,
|
||||
ProposalState,
|
||||
|
90
src/pages/Explore/index.tsx
Normal file
90
src/pages/Explore/index.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import { PageName } from 'components/AmplitudeAnalytics/constants'
|
||||
import { Trace } from 'components/AmplitudeAnalytics/Trace'
|
||||
import { MAX_WIDTH_MEDIA_BREAKPOINT, MEDIUM_MEDIA_BREAKPOINT } from 'components/Explore/constants'
|
||||
import { filterStringAtom } from 'components/Explore/state'
|
||||
import FavoriteButton from 'components/Explore/TokenTable/FavoriteButton'
|
||||
import NetworkFilter from 'components/Explore/TokenTable/NetworkFilter'
|
||||
import SearchBar from 'components/Explore/TokenTable/SearchBar'
|
||||
import TimeSelector from 'components/Explore/TokenTable/TimeSelector'
|
||||
import TokenTable from 'components/Explore/TokenTable/TokenTable'
|
||||
import { useResetAtom } from 'jotai/utils'
|
||||
import { useEffect } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
const ExploreContainer = styled.div`
|
||||
width: 100%;
|
||||
min-width: 320px;
|
||||
padding: 0px 12px;
|
||||
`
|
||||
const TokenTableContainer = styled.div`
|
||||
padding: 16px 0px;
|
||||
`
|
||||
const TitleContainer = styled.div`
|
||||
font-size: 32px;
|
||||
margin-bottom: 16px;
|
||||
max-width: 960px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: flex;
|
||||
`
|
||||
const FiltersContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
height: 44px;
|
||||
|
||||
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
||||
order: 2;
|
||||
}
|
||||
`
|
||||
const SearchContainer = styled(FiltersContainer)`
|
||||
width: 100%;
|
||||
margin-left: 8px;
|
||||
|
||||
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
||||
margin: 0px;
|
||||
order: 1;
|
||||
}
|
||||
`
|
||||
const FiltersWrapper = styled.div`
|
||||
display: flex;
|
||||
max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT};
|
||||
margin: 0 auto;
|
||||
|
||||
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
const Explore = () => {
|
||||
const resetFilterString = useResetAtom(filterStringAtom)
|
||||
const location = useLocation()
|
||||
useEffect(() => {
|
||||
resetFilterString()
|
||||
}, [location, resetFilterString])
|
||||
|
||||
return (
|
||||
<Trace page={PageName.EXPLORE_PAGE} shouldLogImpression>
|
||||
<ExploreContainer>
|
||||
<TitleContainer>Explore Tokens</TitleContainer>
|
||||
<FiltersWrapper>
|
||||
<FiltersContainer>
|
||||
<NetworkFilter />
|
||||
<FavoriteButton />
|
||||
<TimeSelector />
|
||||
</FiltersContainer>
|
||||
<SearchContainer>
|
||||
<SearchBar />
|
||||
</SearchContainer>
|
||||
</FiltersWrapper>
|
||||
|
||||
<TokenTableContainer>
|
||||
<TokenTable />
|
||||
</TokenTableContainer>
|
||||
</ExploreContainer>
|
||||
</Trace>
|
||||
)
|
||||
}
|
||||
|
||||
export default Explore
|
@ -19,6 +19,7 @@ import { NetworkAlert } from 'components/NetworkAlert/NetworkAlert'
|
||||
import PriceImpactWarning from 'components/swap/PriceImpactWarning'
|
||||
import SwapDetailsDropdown from 'components/swap/SwapDetailsDropdown'
|
||||
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
|
||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { useSwapCallback } from 'hooks/useSwapCallback'
|
||||
@ -47,7 +48,6 @@ import ConfirmSwapModal from '../../components/swap/ConfirmSwapModal'
|
||||
import { ArrowWrapper, SwapCallbackError, Wrapper } from '../../components/swap/styleds'
|
||||
import SwapHeader from '../../components/swap/SwapHeader'
|
||||
import { SwitchLocaleLink } from '../../components/SwitchLocaleLink'
|
||||
import TokenWarningModal from '../../components/TokenWarningModal'
|
||||
import { TOKEN_SHORTHANDS } from '../../constants/tokens'
|
||||
import { useAllTokens, useCurrency } from '../../hooks/Tokens'
|
||||
import { ApprovalState, useApprovalOptimizedTrade, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback'
|
||||
@ -136,9 +136,6 @@ export default function Swap() {
|
||||
() => [loadedInputCurrency, loadedOutputCurrency]?.filter((c): c is Token => c?.isToken ?? false) ?? [],
|
||||
[loadedInputCurrency, loadedOutputCurrency]
|
||||
)
|
||||
const handleConfirmTokenWarning = useCallback(() => {
|
||||
setDismissTokenWarning(true)
|
||||
}, [])
|
||||
|
||||
// dismiss warning if all imported tokens are in active lists
|
||||
const defaultTokens = useAllTokens()
|
||||
@ -161,6 +158,10 @@ export default function Swap() {
|
||||
[chainId, defaultTokens, urlLoadedTokens]
|
||||
)
|
||||
|
||||
const handleConfirmTokenWarning = useCallback(() => {
|
||||
setDismissTokenWarning(true)
|
||||
}, [])
|
||||
|
||||
const theme = useContext(ThemeContext as Context<DefaultTheme>)
|
||||
|
||||
// toggle wallet when disconnected
|
||||
@ -490,11 +491,12 @@ export default function Swap() {
|
||||
return (
|
||||
<Trace page={PageName.SWAP_PAGE} shouldLogImpression>
|
||||
<>
|
||||
<TokenWarningModal
|
||||
<TokenSafetyModal
|
||||
isOpen={importTokensNotInDefault.length > 0 && !dismissTokenWarning}
|
||||
tokens={importTokensNotInDefault}
|
||||
onConfirm={handleConfirmTokenWarning}
|
||||
onDismiss={handleDismissTokenWarning}
|
||||
tokenAddress={importTokensNotInDefault[0]?.address}
|
||||
secondTokenAddress={importTokensNotInDefault[1]?.address}
|
||||
onContinue={handleConfirmTokenWarning}
|
||||
onCancel={handleDismissTokenWarning}
|
||||
/>
|
||||
<AppBody>
|
||||
<SwapHeader allowedSlippage={allowedSlippage} />
|
||||
|
144
src/pages/TokenDetails/index.tsx
Normal file
144
src/pages/TokenDetails/index.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import {
|
||||
LARGE_MEDIA_BREAKPOINT,
|
||||
MAX_WIDTH_MEDIA_BREAKPOINT,
|
||||
MOBILE_MEDIA_BREAKPOINT,
|
||||
SMALL_MEDIA_BREAKPOINT,
|
||||
} from 'components/Explore/constants'
|
||||
import BalanceSummary from 'components/Explore/TokenDetails/BalanceSummary'
|
||||
import FooterBalanceSummary from 'components/Explore/TokenDetails/FooterBalanceSummary'
|
||||
import LoadingTokenDetail from 'components/Explore/TokenDetails/LoadingTokenDetail'
|
||||
import NetworkBalance from 'components/Explore/TokenDetails/NetworkBalance'
|
||||
import TokenDetail from 'components/Explore/TokenDetails/TokenDetail'
|
||||
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { L1_CHAIN_IDS, L2_CHAIN_IDS, SupportedChainId, TESTNET_CHAIN_IDS } from 'constants/chains'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
|
||||
import useTokenDetailPageQuery from 'hooks/useTokenDetailPageQuery'
|
||||
import { useMemo } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
const Footer = styled.div`
|
||||
display: none;
|
||||
@media only screen and (max-width: ${LARGE_MEDIA_BREAKPOINT}) {
|
||||
display: flex;
|
||||
}
|
||||
`
|
||||
const TokenDetailsLayout = styled.div`
|
||||
display: flex;
|
||||
gap: 80px;
|
||||
padding: 0px 20px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
|
||||
@media only screen and (max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT}) {
|
||||
gap: 40px;
|
||||
}
|
||||
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
|
||||
padding: 0px 16px;
|
||||
}
|
||||
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
||||
padding: 0px 8px;
|
||||
}
|
||||
`
|
||||
const RightPanel = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
@media only screen and (max-width: ${LARGE_MEDIA_BREAKPOINT}) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
const Widget = styled.div`
|
||||
height: 348px;
|
||||
width: 284px;
|
||||
background-color: ${({ theme }) => theme.backgroundContainer};
|
||||
border-radius: 12px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
`
|
||||
function NetworkBalances(tokenAddress: string) {
|
||||
return useNetworkTokenBalances({ address: tokenAddress })
|
||||
}
|
||||
|
||||
export default function TokenDetails() {
|
||||
const { tokenAddress } = useParams<{ tokenAddress?: string }>()
|
||||
const { loading } = useTokenDetailPageQuery(tokenAddress)
|
||||
const tokenSymbol = useToken(tokenAddress)?.symbol
|
||||
|
||||
let tokenDetail
|
||||
if (!tokenAddress) {
|
||||
// TODO: handle no address / invalid address cases
|
||||
tokenDetail = 'invalid token'
|
||||
} else if (loading) {
|
||||
tokenDetail = <LoadingTokenDetail />
|
||||
} else {
|
||||
tokenDetail = <TokenDetail address={tokenAddress} />
|
||||
}
|
||||
|
||||
const tokenWarning = tokenAddress ? checkWarning(tokenAddress) : null
|
||||
/* network balance handling */
|
||||
const { data: networkData } = tokenAddress ? NetworkBalances(tokenAddress) : { data: null }
|
||||
const { chainId: connectedChainId } = useWeb3React()
|
||||
const totalBalance = 4.3 // dummy data
|
||||
|
||||
const chainsToList = useMemo(() => {
|
||||
let chainIds = [...L1_CHAIN_IDS, ...L2_CHAIN_IDS]
|
||||
const userConnectedToATestNetwork = connectedChainId && TESTNET_CHAIN_IDS.includes(connectedChainId)
|
||||
if (!userConnectedToATestNetwork) {
|
||||
chainIds = chainIds.filter((id) => !(TESTNET_CHAIN_IDS as unknown as SupportedChainId[]).includes(id))
|
||||
}
|
||||
return chainIds
|
||||
}, [connectedChainId])
|
||||
|
||||
const balancesByNetwork = networkData
|
||||
? chainsToList.map((chainId) => {
|
||||
const amount = networkData[chainId]
|
||||
const fiatValue = amount // for testing purposes
|
||||
if (!fiatValue || !tokenSymbol) return null
|
||||
const chainInfo = getChainInfo(chainId)
|
||||
const networkColor = chainInfo.color
|
||||
if (!chainInfo) return null
|
||||
return (
|
||||
<NetworkBalance
|
||||
key={chainId}
|
||||
logoUrl={chainInfo.logoUrl}
|
||||
balance={'1'}
|
||||
tokenSymbol={tokenSymbol}
|
||||
fiatValue={fiatValue.toSignificant(2)}
|
||||
label={chainInfo.label}
|
||||
networkColor={networkColor}
|
||||
/>
|
||||
)
|
||||
})
|
||||
: null
|
||||
|
||||
return (
|
||||
<TokenDetailsLayout>
|
||||
{tokenDetail}
|
||||
{tokenAddress && (
|
||||
<>
|
||||
<RightPanel>
|
||||
<Widget />
|
||||
{tokenWarning && <TokenSafetyMessage tokenAddress={tokenAddress} warning={tokenWarning} />}
|
||||
{!loading && (
|
||||
<BalanceSummary address={tokenAddress} totalBalance={totalBalance} networkBalances={balancesByNetwork} />
|
||||
)}
|
||||
</RightPanel>
|
||||
<Footer>
|
||||
{!loading && (
|
||||
<FooterBalanceSummary
|
||||
address={tokenAddress}
|
||||
totalBalance={totalBalance}
|
||||
networkBalances={balancesByNetwork}
|
||||
/>
|
||||
)}
|
||||
</Footer>
|
||||
</>
|
||||
)}
|
||||
</TokenDetailsLayout>
|
||||
)
|
||||
}
|
4
src/react-app-env.d.ts
vendored
4
src/react-app-env.d.ts
vendored
@ -27,3 +27,7 @@ declare module 'multihashes' {
|
||||
declare function decode(buff: Uint8Array): { code: number; name: string; length: number; digest: Uint8Array }
|
||||
declare function toB58String(hash: Uint8Array): string
|
||||
}
|
||||
|
||||
declare module 'd3-curve-circlecorners' {
|
||||
declare function radius(r: number): d3.CurveFactory
|
||||
}
|
||||
|
@ -66,6 +66,10 @@ export function useTogglePrivacyPolicy(): () => void {
|
||||
return useToggleModal(ApplicationModal.PRIVACY_POLICY)
|
||||
}
|
||||
|
||||
export function useToggleTimeSelector(): () => void {
|
||||
return useToggleModal(ApplicationModal.TIME_SELECTOR)
|
||||
}
|
||||
|
||||
// returns a function that allows adding a popup
|
||||
export function useAddPopup(): (content: PopupContent, key?: string, removeAfterMs?: number) => void {
|
||||
const dispatch = useAppDispatch()
|
||||
|
@ -28,6 +28,9 @@ export enum ApplicationModal {
|
||||
WALLET,
|
||||
QUEUE,
|
||||
EXECUTE,
|
||||
TIME_SELECTOR,
|
||||
SHARE,
|
||||
NETWORK_FILTER,
|
||||
}
|
||||
|
||||
type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }>
|
||||
|
@ -74,6 +74,7 @@ export interface GlobalPalette {
|
||||
blue900: Color
|
||||
blueVibrant: Color
|
||||
magentaVibrant: Color
|
||||
purple900: Color
|
||||
networkEthereum: Color
|
||||
networkOptimism: Color
|
||||
networkOptimismSoft: Color
|
||||
@ -111,7 +112,7 @@ export const colors: GlobalPalette = {
|
||||
red100: '#FFD9CE',
|
||||
red200: '#FDA799',
|
||||
red300: '#FF776D',
|
||||
red400: '#FA2B39',
|
||||
red400: '#FD4040',
|
||||
red500: '#C52533',
|
||||
red600: '#891E20',
|
||||
red700: '#530F10',
|
||||
@ -156,6 +157,7 @@ export const colors: GlobalPalette = {
|
||||
blueVibrant: '#587BFF',
|
||||
// TODO: add magenta 50-900
|
||||
magentaVibrant: '#FC72FF',
|
||||
purple900: '#1C0337',
|
||||
// TODO: add all other vibrant variations
|
||||
networkEthereum: '#627EEA',
|
||||
networkOptimism: '#FF0420',
|
||||
@ -215,6 +217,11 @@ export interface Palette {
|
||||
chain_42161: Color
|
||||
chain_421611: Color
|
||||
chain_80001: Color
|
||||
chain_137_background: Color
|
||||
chain_10_background: Color
|
||||
chain_42161_background: Color
|
||||
|
||||
flyoutDropShadow: Color
|
||||
}
|
||||
|
||||
export const colorsLight: Palette = {
|
||||
@ -267,6 +274,11 @@ export const colorsLight: Palette = {
|
||||
chain_42161: colors.networkEthereum,
|
||||
chain_421611: colors.networkEthereum,
|
||||
chain_80001: colors.networkPolygon,
|
||||
chain_137_background: colors.purple900,
|
||||
chain_10_background: colors.red900,
|
||||
chain_42161_background: colors.blue900,
|
||||
|
||||
flyoutDropShadow: colors.black,
|
||||
}
|
||||
|
||||
export const colorsDark: Palette = {
|
||||
@ -318,4 +330,9 @@ export const colorsDark: Palette = {
|
||||
chain_42161: colors.networkEthereum,
|
||||
chain_421611: colors.networkEthereum,
|
||||
chain_80001: colors.networkPolygon,
|
||||
chain_137_background: colors.purple900,
|
||||
chain_10_background: colors.red900,
|
||||
chain_42161_background: colors.blue900,
|
||||
|
||||
flyoutDropShadow: colors.black,
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { outboundLink } from 'components/analytics'
|
||||
import React, { HTMLProps } from 'react'
|
||||
import { ArrowLeft, ExternalLink as LinkIconFeather, Trash, X } from 'react-feather'
|
||||
import useCopyClipboard from 'hooks/useCopyClipboard'
|
||||
import React, { HTMLProps, useCallback } from 'react'
|
||||
import { ArrowLeft, Copy, ExternalLink as LinkIconFeather, Trash, X } from 'react-feather'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled, { keyframes } from 'styled-components/macro'
|
||||
import styled, { css, keyframes } from 'styled-components/macro'
|
||||
|
||||
import { ReactComponent as TooltipTriangle } from '../assets/svg/tooltip_triangle.svg'
|
||||
import { anonymizeLink } from '../utils/anonymizeLink'
|
||||
|
||||
export const ButtonText = styled.button`
|
||||
@ -38,7 +40,7 @@ export const IconWrapper = styled.div<{ stroke?: string; size?: string; marginRi
|
||||
margin-right: ${({ marginRight }) => marginRight ?? 0};
|
||||
margin-left: ${({ marginLeft }) => marginLeft ?? 0};
|
||||
& > * {
|
||||
stroke: ${({ theme, stroke }) => stroke ?? theme.deprecated_blue1};
|
||||
stroke: ${({ theme, stroke }) => stroke ?? theme.accentActive};
|
||||
}
|
||||
`
|
||||
|
||||
@ -66,80 +68,59 @@ export const LinkStyledButton = styled.button<{ disabled?: boolean }>`
|
||||
}
|
||||
`
|
||||
|
||||
// An internal link from the react-router-dom library that is correctly styled
|
||||
export const StyledInternalLink = styled(Link)`
|
||||
export const LinkStyle = css`
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
stroke: ${({ theme }) => theme.accentAction};
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => theme.deprecated_primary1};
|
||||
font-weight: 500;
|
||||
|
||||
:hover {
|
||||
text-decoration: underline;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
:active {
|
||||
text-decoration: none;
|
||||
opacity: 0.4;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledLink = styled.a`
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => theme.deprecated_primary1};
|
||||
font-weight: 500;
|
||||
|
||||
:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
// An internal link from the react-router-dom library that is correctly styled
|
||||
export const StyledInternalLink = styled(Link)`
|
||||
${LinkStyle}
|
||||
`
|
||||
|
||||
const LinkIconWrapper = styled.a`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const CopyIconWrapper = styled.div`
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
`
|
||||
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
const IconStyle = css`
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
margin-left: 10px;
|
||||
`
|
||||
|
||||
const LinkIcon = styled(LinkIconFeather)`
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
margin-left: 10px;
|
||||
stroke: ${({ theme }) => theme.deprecated_blue1};
|
||||
${IconStyle}
|
||||
${LinkStyle}
|
||||
`
|
||||
|
||||
const CopyIcon = styled(Copy)`
|
||||
${IconStyle}
|
||||
${LinkStyle}
|
||||
stroke: ${({ theme }) => theme.accentActive};
|
||||
`
|
||||
|
||||
export const TrashIcon = styled(Trash)`
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
margin-left: 10px;
|
||||
${IconStyle}
|
||||
stroke: ${({ theme }) => theme.deprecated_text3};
|
||||
|
||||
cursor: pointer;
|
||||
@ -187,6 +168,9 @@ function handleClickExternalLink(event: React.MouseEvent<HTMLAnchorElement>) {
|
||||
}
|
||||
}
|
||||
|
||||
const StyledLink = styled.a`
|
||||
${LinkStyle}
|
||||
`
|
||||
/**
|
||||
* Outbound link that handles firing google analytics events
|
||||
*/
|
||||
@ -212,6 +196,50 @@ export function ExternalLinkIcon({
|
||||
)
|
||||
}
|
||||
|
||||
const ToolTipWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
transform: translate(5px, 32px);
|
||||
z-index: 9999;
|
||||
`
|
||||
|
||||
const CopiedTooltip = styled.div`
|
||||
background-color: ${({ theme }) => theme.black};
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
width: 60px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 8px;
|
||||
|
||||
color: ${({ theme }) => theme.white};
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
function ToolTip() {
|
||||
return (
|
||||
<ToolTipWrapper>
|
||||
<TooltipTriangle />
|
||||
<CopiedTooltip>Copied!</CopiedTooltip>
|
||||
</ToolTipWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export function CopyLinkIcon({ toCopy }: { toCopy: string }) {
|
||||
const [isCopied, setCopied] = useCopyClipboard()
|
||||
const copy = useCallback(() => {
|
||||
setCopied(toCopy)
|
||||
}, [toCopy, setCopied])
|
||||
return (
|
||||
<CopyIconWrapper onClick={copy}>
|
||||
<CopyIcon />
|
||||
{isCopied && <ToolTip />}
|
||||
</CopyIconWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const rotate = keyframes`
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
@ -220,12 +248,18 @@ const rotate = keyframes`
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
`
|
||||
const SpinnerCss = css`
|
||||
animation: 2s ${rotate} linear infinite;
|
||||
`
|
||||
|
||||
const Spinner = styled.img`
|
||||
animation: 2s ${rotate} linear infinite;
|
||||
${SpinnerCss}
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
`
|
||||
export const SpinnerSVG = styled.svg`
|
||||
${SpinnerCss}
|
||||
`
|
||||
|
||||
const BackArrowLink = styled(StyledInternalLink)`
|
||||
color: ${({ theme }) => theme.deprecated_text1};
|
||||
|
@ -107,6 +107,9 @@ function uniswapThemeColors(darkMode: boolean): ThemeColors {
|
||||
chain_80001: colorsDark.chain_80001,
|
||||
|
||||
blue200: ColorsPalette.blue200,
|
||||
flyoutDropShadow:
|
||||
'0px 24px 32px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 0px 1px rgba(0, 0, 0, 0.12)',
|
||||
hoverState: opacify(24, ColorsPalette.blue200),
|
||||
}
|
||||
}
|
||||
|
||||
|
2
src/theme/styled.d.ts
vendored
2
src/theme/styled.d.ts
vendored
@ -52,6 +52,8 @@ export interface ThemeColors {
|
||||
chain_80001: Color
|
||||
|
||||
blue200: Color
|
||||
flyoutDropShadow: Color
|
||||
hoverState: Color
|
||||
}
|
||||
|
||||
export interface Colors {
|
||||
|
37
src/utils/formatDollarAmt.tsx
Normal file
37
src/utils/formatDollarAmt.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
/* Copied from Uniswap/v-3: https://github.com/Uniswap/v3-info/blob/master/src/utils/numbers.ts */
|
||||
import numbro from 'numbro'
|
||||
|
||||
// using a currency library here in case we want to add more in future
|
||||
export const formatDollarAmount = (num: number | undefined, digits = 2, round = true) => {
|
||||
if (num === 0) return '0'
|
||||
if (!num) return '-'
|
||||
if (num < 0.001 && digits <= 3) {
|
||||
return '<0.001'
|
||||
}
|
||||
|
||||
return numbro(num).formatCurrency({
|
||||
average: round,
|
||||
mantissa: num > 1000 ? 2 : digits,
|
||||
abbreviations: {
|
||||
million: 'M',
|
||||
billion: 'B',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// using a currency library here in case we want to add more in future
|
||||
export const formatAmount = (num: number | undefined, digits = 2) => {
|
||||
if (num === 0) return '0'
|
||||
if (!num) return '-'
|
||||
if (num < 0.001) {
|
||||
return '<0.001'
|
||||
}
|
||||
return numbro(num).format({
|
||||
average: true,
|
||||
mantissa: num > 1000 ? 2 : digits,
|
||||
abbreviations: {
|
||||
million: 'M',
|
||||
billion: 'B',
|
||||
},
|
||||
})
|
||||
}
|
318
yarn.lock
318
yarn.lock
@ -3736,6 +3736,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-2.0.3.tgz#3009b792b754da964d893b4269d1fe7757f21370"
|
||||
integrity sha512-koIqSNQLPRQPXt7c55hgRF6Lr9Ps72r1+Biv55jdYR+SHJ463MsB2lp4ktzttFNmrQw/9yWthf/OmSUj5dNXKw==
|
||||
|
||||
"@types/d3-color@^1":
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.4.2.tgz#944f281d04a0f06e134ea96adbb68303515b2784"
|
||||
integrity sha512-fYtiVLBYy7VQX+Kx7wU/uOIkGQn8aAEY8oWMoyja3N4dLd8Yf6XgSIR/4yWvMuveNOH5VShnqCgRqqh/UNanBA==
|
||||
|
||||
"@types/d3-color@^2":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-2.0.3.tgz#8bc4589073c80e33d126345542f588056511fe82"
|
||||
@ -3805,6 +3810,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-2.0.2.tgz#afd09d509c36e8cd4907333556f8b591f23589e9"
|
||||
integrity sha512-6PlBRwbjUPPt0ZFq/HTUyOAdOF3p73EUYots74lHMUyAVtdFSOS/hAeNXtEIM9i7qRDntuIblXxHGUMb9MuNRA==
|
||||
|
||||
"@types/d3-interpolate@^1.3.1":
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-1.4.2.tgz#88902a205f682773a517612299a44699285eed7b"
|
||||
integrity sha512-ylycts6llFf8yAEs1tXzx2loxxzDZHseuhPokrqKprTQSTcD3JbJI1omZP1rphsELZO3Q+of3ff0ZS7+O6yVzg==
|
||||
dependencies:
|
||||
"@types/d3-color" "^1"
|
||||
|
||||
"@types/d3-interpolate@^2":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-2.0.2.tgz#78eddf7278b19e48e8652603045528d46897aba0"
|
||||
@ -3812,6 +3824,11 @@
|
||||
dependencies:
|
||||
"@types/d3-color" "^2"
|
||||
|
||||
"@types/d3-path@^1", "@types/d3-path@^1.0.8":
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.9.tgz#73526b150d14cd96e701597cbf346cfd1fd4a58c"
|
||||
integrity sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ==
|
||||
|
||||
"@types/d3-path@^2":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-2.0.1.tgz#ca03dfa8b94d8add97ad0cd97e96e2006b4763cb"
|
||||
@ -3837,7 +3854,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-2.0.1.tgz#495cbbae7273e0d0ff564cdc19aa6d2b9928da83"
|
||||
integrity sha512-3EuZlbPu+pvclZcb1DhlymTWT2W+lYsRKBjvkH2ojDbCWDYavifqu1vYX9WGzlPgCgcS4Alhk1+zapXbGEGylQ==
|
||||
|
||||
"@types/d3-scale@^3":
|
||||
"@types/d3-scale@^3", "@types/d3-scale@^3.3.0":
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-3.3.2.tgz#18c94e90f4f1c6b1ee14a70f14bfca2bd1c61d06"
|
||||
integrity sha512-gGqr7x1ost9px3FvIfUMi5XA/F/yAf4UkUDtdQhpH92XCT0Oa7zkkRzY61gPVJq+DxpHn/btouw5ohWkbBsCzQ==
|
||||
@ -3849,6 +3866,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-2.0.1.tgz#bc2816c96faff285d204dda72b79734d4f37d583"
|
||||
integrity sha512-3mhtPnGE+c71rl/T5HMy+ykg7migAZ4T6gzU0HxpgBFKcasBrSnwRbYV1/UZR6o5fkpySxhWxAhd7yhjj8jL7g==
|
||||
|
||||
"@types/d3-shape@^1.3.1":
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.8.tgz#c3c15ec7436b4ce24e38de517586850f1fea8e89"
|
||||
integrity sha512-gqfnMz6Fd5H6GOLYixOZP/xlrMtJms9BaS+6oWxTKHNqPGZ93BkWWupQSCYm6YHqx6h9wjRupuJb90bun6ZaYg==
|
||||
dependencies:
|
||||
"@types/d3-path" "^1"
|
||||
|
||||
"@types/d3-shape@^2":
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-2.1.3.tgz#35d397b9e687abaa0de82343b250b9897b8cacf3"
|
||||
@ -3861,7 +3885,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-3.0.1.tgz#1680fb6c41ab3a85db261ede296626668592246a"
|
||||
integrity sha512-5GIimz5IqaRsdnxs4YlyTZPwAMfALu/wA4jqSiuqgdbCxUZ2WjrnwANqOtoBJQgeaUTdYNfALJO0Yb0YrDqduA==
|
||||
|
||||
"@types/d3-time@^2":
|
||||
"@types/d3-time@^2", "@types/d3-time@^2.0.0":
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-2.1.1.tgz#743fdc821c81f86537cbfece07093ac39b4bc342"
|
||||
integrity sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg==
|
||||
@ -4072,10 +4096,10 @@
|
||||
"@types/lingui__core" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/lodash@^4.14.168":
|
||||
version "4.14.177"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.177.tgz#f70c0d19c30fab101cad46b52be60363c43c4578"
|
||||
integrity sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==
|
||||
"@types/lodash@^4.14.168", "@types/lodash@^4.14.172":
|
||||
version "4.14.182"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2"
|
||||
integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==
|
||||
|
||||
"@types/long@^4.0.1":
|
||||
version "4.0.1"
|
||||
@ -4810,6 +4834,115 @@
|
||||
dependencies:
|
||||
"@vibrant/types" "^3.2.1-alpha.1"
|
||||
|
||||
"@visx/axis@^2.12.2":
|
||||
version "2.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@visx/axis/-/axis-2.12.2.tgz#0aa50ae35d0cd6d8a11c59ad0d874cfeea9e3b89"
|
||||
integrity sha512-nE+DGNwRzXOmp6ZwMQ1yUhbF7uR2wd3j6Xja/kVgGA7wSbqUeCZzqKZvhRsCqyay6PtHVlRRAhHP31Ob39+jtw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
"@visx/group" "2.10.0"
|
||||
"@visx/point" "2.6.0"
|
||||
"@visx/scale" "2.2.2"
|
||||
"@visx/shape" "2.12.2"
|
||||
"@visx/text" "2.12.2"
|
||||
classnames "^2.3.1"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
"@visx/curve@2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@visx/curve/-/curve-2.1.0.tgz#f614bfe3db66df7db7382db7a75ced1506b94602"
|
||||
integrity sha512-9b6JOnx91gmOQiSPhUOxdsvcnW88fgqfTPKoVgQxidMsD/I3wksixtwo8TR/vtEz2aHzzsEEhlv1qK7Y3yaSDw==
|
||||
dependencies:
|
||||
"@types/d3-shape" "^1.3.1"
|
||||
d3-shape "^1.0.6"
|
||||
|
||||
"@visx/event@^2.6.0":
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@visx/event/-/event-2.6.0.tgz#0718eb1efabd5305cf659a153779c94ba4038996"
|
||||
integrity sha512-WGp91g82s727g3NAnENF1ppC3ZAlvWg+Y+GG0WFg34NmmOZbvPI/PTOqTqZE3x6B8EUn8NJiMxRjxIMbi+IvRw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
"@visx/point" "2.6.0"
|
||||
|
||||
"@visx/glyph@^2.10.0":
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@visx/glyph/-/glyph-2.10.0.tgz#9f8929b8391b9862023af00d64446188208b232b"
|
||||
integrity sha512-qjbnfSgV920+V4ctXeDJWwzBorZLz97cDA4b/GmJ2tk2h0AVMrAejF2LNLgPQpzsVb7BIuVXAJXgp0dop2gr6w==
|
||||
dependencies:
|
||||
"@types/d3-shape" "^1.3.1"
|
||||
"@types/react" "*"
|
||||
"@visx/group" "2.10.0"
|
||||
classnames "^2.3.1"
|
||||
d3-shape "^1.2.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
"@visx/group@2.10.0", "@visx/group@^2.10.0":
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@visx/group/-/group-2.10.0.tgz#95839851832545621eb0d091866a61dafe552ae1"
|
||||
integrity sha512-DNJDX71f65Et1+UgQvYlZbE66owYUAfcxTkC96Db6TnxV221VKI3T5l23UWbnMzwFBP9dR3PWUjjqhhF12N5pA==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
classnames "^2.3.1"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
"@visx/point@2.6.0":
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@visx/point/-/point-2.6.0.tgz#c4316ca409b5b829c5455f07118d8c14a92cc633"
|
||||
integrity sha512-amBi7yMz4S2VSchlPdliznN41TuES64506ySI22DeKQ+mc1s1+BudlpnY90sM1EIw4xnqbKmrghTTGfy6SVqvQ==
|
||||
|
||||
"@visx/responsive@^2.10.0":
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@visx/responsive/-/responsive-2.10.0.tgz#3e5c5853c7b2b33481e99a64678063cef717de0b"
|
||||
integrity sha512-NssDPpuUYp7hqVISuYkKZ5zk6ob0++RdTIaUjRcUdyFEbvzb9+zIb8QToOkvI90L2EC/MY4Jx0NpDbEe79GpAw==
|
||||
dependencies:
|
||||
"@juggle/resize-observer" "^3.3.1"
|
||||
"@types/lodash" "^4.14.172"
|
||||
"@types/react" "*"
|
||||
lodash "^4.17.21"
|
||||
prop-types "^15.6.1"
|
||||
|
||||
"@visx/scale@2.2.2":
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@visx/scale/-/scale-2.2.2.tgz#b8eafabdcf92bb45ab196058fe184772ad80fd25"
|
||||
integrity sha512-3aDySGUTpe6VykDQmF+g2nz5paFu9iSPTcCOEgkcru0/v5tmGzUdvivy8CkYbr87HN73V/Jc53lGm+kJUQcLBw==
|
||||
dependencies:
|
||||
"@types/d3-interpolate" "^1.3.1"
|
||||
"@types/d3-scale" "^3.3.0"
|
||||
"@types/d3-time" "^2.0.0"
|
||||
d3-interpolate "^1.4.0"
|
||||
d3-scale "^3.3.0"
|
||||
d3-time "^2.1.1"
|
||||
|
||||
"@visx/shape@2.12.2", "@visx/shape@^2.11.1":
|
||||
version "2.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@visx/shape/-/shape-2.12.2.tgz#81ed88bf823aa84a4f5f32a9c9daf8371a606897"
|
||||
integrity sha512-4gN0fyHWYXiJ+Ck8VAazXX0i8TOnLJvOc5jZBnaJDVxgnSIfCjJn0+Nsy96l9Dy/bCMTh4DBYUBv9k+YICBUOA==
|
||||
dependencies:
|
||||
"@types/d3-path" "^1.0.8"
|
||||
"@types/d3-shape" "^1.3.1"
|
||||
"@types/lodash" "^4.14.172"
|
||||
"@types/react" "*"
|
||||
"@visx/curve" "2.1.0"
|
||||
"@visx/group" "2.10.0"
|
||||
"@visx/scale" "2.2.2"
|
||||
classnames "^2.3.1"
|
||||
d3-path "^1.0.5"
|
||||
d3-shape "^1.2.0"
|
||||
lodash "^4.17.21"
|
||||
prop-types "^15.5.10"
|
||||
|
||||
"@visx/text@2.12.2":
|
||||
version "2.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@visx/text/-/text-2.12.2.tgz#f4cd32424b1866d8a7f26bdc7cc8396727da06f0"
|
||||
integrity sha512-Sv9YEolggfv2Nf6+l28ESG3VXVR1+s4u/Cz17QpgOxygcbOM8LfLtriWtBsBMKdMbYKeUpoUro0clx55TUwzew==
|
||||
dependencies:
|
||||
"@types/lodash" "^4.14.172"
|
||||
"@types/react" "*"
|
||||
classnames "^2.3.1"
|
||||
lodash "^4.17.21"
|
||||
prop-types "^15.7.2"
|
||||
reduce-css-calc "^1.3.0"
|
||||
|
||||
"@walletconnect/browser-utils@^1.7.1":
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/browser-utils/-/browser-utils-1.7.1.tgz#2a28846cd4d73166debbbf7d470e78ba25616f5e"
|
||||
@ -6083,6 +6216,11 @@ bail@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz"
|
||||
integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==
|
||||
|
||||
balanced-match@^0.4.2:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
|
||||
integrity sha512-STw03mQKnGUYtoNjmowo4F2cRmIIxYEGiMsjjwla/u5P1lxadj/05WkNaFjNiKTgJkj8KiXbgAiRTmcQRwQNtg==
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
@ -6159,6 +6297,11 @@ big.js@^5.2.2:
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
|
||||
|
||||
bignumber.js@^8.1.1:
|
||||
version "8.1.1"
|
||||
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-8.1.1.tgz#4b072ae5aea9c20f6730e4e5d529df1271c4d885"
|
||||
integrity sha512-QD46ppGintwPGuL1KqmwhR0O+N2cZUg8JG/VzwI2e28sM9TqHjQB10lI4QAaMHVbLzwVLLAwEglpKPViWX+5NQ==
|
||||
|
||||
binary-extensions@^1.0.0:
|
||||
version "1.13.1"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
|
||||
@ -6215,7 +6358,7 @@ bn.js@4.11.8:
|
||||
resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz"
|
||||
integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
|
||||
|
||||
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.0, bn.js@^4.11.1, bn.js@^4.11.8, bn.js@^4.11.9:
|
||||
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.0, bn.js@^4.11.8, bn.js@^4.11.9:
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
|
||||
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
|
||||
@ -6963,6 +7106,11 @@ class-utils@^0.3.5:
|
||||
isobject "^3.0.0"
|
||||
static-extend "^0.1.1"
|
||||
|
||||
classnames@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
||||
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
||||
|
||||
clean-css@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
|
||||
@ -7907,10 +8055,17 @@ cypress@*, cypress@^10.3.1:
|
||||
untildify "^4.0.0"
|
||||
yauzl "^2.10.0"
|
||||
|
||||
"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.0.1.tgz#ca45c263f5bb780ab5a34a6e1d3d5883fe4a8d14"
|
||||
integrity sha512-l3Bh5o8RSoC3SBm5ix6ogaFW+J6rOUm42yOtZ2sQPCEvCqUMepeX7zgrlLLGIemxgOyo9s2CsWEidnLv5PwwRw==
|
||||
d3-array@2, d3-array@^2.3.0:
|
||||
version "2.12.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81"
|
||||
integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==
|
||||
dependencies:
|
||||
internmap "^1.0.0"
|
||||
|
||||
"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.0.tgz#15bf96cd9b7333e02eb8de8053d78962eafcff14"
|
||||
integrity sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==
|
||||
dependencies:
|
||||
internmap "1 - 2"
|
||||
|
||||
@ -7937,17 +8092,32 @@ d3-chord@3:
|
||||
dependencies:
|
||||
d3-path "1 - 3"
|
||||
|
||||
d3-color@1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a"
|
||||
integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==
|
||||
|
||||
"d3-color@1 - 2":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e"
|
||||
integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==
|
||||
|
||||
"d3-color@1 - 3", d3-color@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.0.1.tgz#03316e595955d1fcd39d9f3610ad41bb90194d0a"
|
||||
integrity sha512-6/SlHkDOBLyQSJ1j1Ghs82OIUXpKWlR0hCsw0XrLSQhuUPuCSmLQ1QPH98vpnQxMUQM2/gfAkUEWsupVpd9JGw==
|
||||
|
||||
d3-contour@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-3.0.1.tgz#2c64255d43059599cd0dba8fe4cc3d51ccdd9bbd"
|
||||
integrity sha512-0Oc4D0KyhwhM7ZL0RMnfGycLN7hxHB8CMmwZ3+H26PWAG0ozNuYG5hXSDNgmP1SgJkQMrlG6cP20HoaSbvcJTQ==
|
||||
d3-contour@4:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.0.tgz#5a1337c6da0d528479acdb5db54bc81a0ff2ec6b"
|
||||
integrity sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==
|
||||
dependencies:
|
||||
d3-array "2 - 3"
|
||||
d3-array "^3.2.0"
|
||||
|
||||
d3-curve-circlecorners@^0.1.6:
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/d3-curve-circlecorners/-/d3-curve-circlecorners-0.1.6.tgz#da786c38b4a50024c6ae07e203a98c16e9f85202"
|
||||
integrity sha512-FPOhAYPuEMcIuj3GZ/lp+GzJ82EpcuMjnwBXJZjCRk1NNuQail6DgCz+VjEhdMeef/VBoh1k8A8fn6XJWUlEjw==
|
||||
|
||||
d3-delaunay@6:
|
||||
version "6.0.2"
|
||||
@ -7999,6 +8169,11 @@ d3-force@3:
|
||||
d3-quadtree "1 - 3"
|
||||
d3-timer "1 - 3"
|
||||
|
||||
"d3-format@1 - 2":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767"
|
||||
integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==
|
||||
|
||||
"d3-format@1 - 3", d3-format@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.0.1.tgz#e41b81b2ab79277141ec1404aa5d05001da64084"
|
||||
@ -8023,6 +8198,25 @@ d3-hierarchy@3:
|
||||
dependencies:
|
||||
d3-color "1 - 3"
|
||||
|
||||
"d3-interpolate@1.2.0 - 2":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163"
|
||||
integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==
|
||||
dependencies:
|
||||
d3-color "1 - 2"
|
||||
|
||||
d3-interpolate@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987"
|
||||
integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==
|
||||
dependencies:
|
||||
d3-color "1"
|
||||
|
||||
d3-path@1, d3-path@^1.0.5:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf"
|
||||
integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
|
||||
|
||||
"d3-path@1 - 3", d3-path@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e"
|
||||
@ -8062,6 +8256,17 @@ d3-scale@4:
|
||||
d3-time "2.1.1 - 3"
|
||||
d3-time-format "2 - 4"
|
||||
|
||||
d3-scale@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.3.0.tgz#28c600b29f47e5b9cd2df9749c206727966203f3"
|
||||
integrity sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==
|
||||
dependencies:
|
||||
d3-array "^2.3.0"
|
||||
d3-format "1 - 2"
|
||||
d3-interpolate "1.2.0 - 2"
|
||||
d3-time "^2.1.1"
|
||||
d3-time-format "2 - 3"
|
||||
|
||||
"d3-selection@2 - 3", d3-selection@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
|
||||
@ -8074,6 +8279,20 @@ d3-shape@3:
|
||||
dependencies:
|
||||
d3-path "1 - 3"
|
||||
|
||||
d3-shape@^1.0.6, d3-shape@^1.2.0:
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
|
||||
integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==
|
||||
dependencies:
|
||||
d3-path "1"
|
||||
|
||||
"d3-time-format@2 - 3":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6"
|
||||
integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==
|
||||
dependencies:
|
||||
d3-time "1 - 2"
|
||||
|
||||
"d3-time-format@2 - 4", d3-time-format@4:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.0.0.tgz#930ded86a9de761702344760d8a25753467f28b7"
|
||||
@ -8081,6 +8300,13 @@ d3-shape@3:
|
||||
dependencies:
|
||||
d3-time "1 - 3"
|
||||
|
||||
"d3-time@1 - 2", d3-time@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682"
|
||||
integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==
|
||||
dependencies:
|
||||
d3-array "2"
|
||||
|
||||
"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.0.0.tgz#65972cb98ae2d4954ef5c932e8704061335d4975"
|
||||
@ -8115,17 +8341,17 @@ d3-zoom@3:
|
||||
d3-selection "2 - 3"
|
||||
d3-transition "2 - 3"
|
||||
|
||||
d3@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3/-/d3-7.0.0.tgz#fe6036b38ba2026ff34223e208fd294db1b997da"
|
||||
integrity sha512-t+jEKGO2jQiSBLJYYq6RFc500tsCeXBB4x41oQaSnZD3Som95nQrlw9XJGrFTMUOQOkwSMauWy9+8Tz1qm9UZw==
|
||||
d3@^7.6.1:
|
||||
version "7.6.1"
|
||||
resolved "https://registry.yarnpkg.com/d3/-/d3-7.6.1.tgz#b21af9563485ed472802f8c611cc43be6c37c40c"
|
||||
integrity sha512-txMTdIHFbcpLx+8a0IFhZsbp+PfBBPt8yfbmukZTQFroKuFqIwqswF0qE5JXWefylaAVpSXFoKm3yP+jpNLFLw==
|
||||
dependencies:
|
||||
d3-array "3"
|
||||
d3-axis "3"
|
||||
d3-brush "3"
|
||||
d3-chord "3"
|
||||
d3-color "3"
|
||||
d3-contour "3"
|
||||
d3-contour "4"
|
||||
d3-delaunay "6"
|
||||
d3-dispatch "3"
|
||||
d3-drag "3"
|
||||
@ -11137,6 +11363,11 @@ internal-slot@^1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.1.tgz#33d0fa016185397549fb1a14ea3dbe5a2949d1cd"
|
||||
integrity sha512-Ujwccrj9FkGqjbY3iVoxD1VV+KdZZeENx0rphrtzmRXbFvkFO88L80BL/zeSIguX/7T+y8k04xqtgWgS5vxwxw==
|
||||
|
||||
internmap@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95"
|
||||
integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==
|
||||
|
||||
invariant@^2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz"
|
||||
@ -13125,6 +13356,11 @@ markdown-escapes@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz"
|
||||
integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==
|
||||
|
||||
math-expression-evaluator@^1.2.14:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.4.0.tgz#3d66031117fbb7b9715ea6c9c68c2cd2eebd37e2"
|
||||
integrity sha512-4vRUvPyxdO8cWULGTh9dZWL2tZK6LDBvj+OGHBER7poH9Qdt7kXEoj20wiz4lQUbUXQZFjPbe5mVDo9nutizCw==
|
||||
|
||||
mcl-wasm@^0.7.1:
|
||||
version "0.7.9"
|
||||
resolved "https://registry.yarnpkg.com/mcl-wasm/-/mcl-wasm-0.7.9.tgz#c1588ce90042a8700c3b60e40efb339fc07ab87f"
|
||||
@ -13891,6 +14127,13 @@ number-is-nan@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
|
||||
integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
|
||||
|
||||
numbro@^2.3.6:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/numbro/-/numbro-2.3.6.tgz#4bd622ebe59ccbc49dad365c5b9eed200781fa21"
|
||||
integrity sha512-pxpoTT3hVxQGaOA2RTzXR/muonQNd1K1HPJbWo7QOmxPwiPmoFCFfsG9XXgW3uqjyzezJ0P9IvCPDXUtJexjwg==
|
||||
dependencies:
|
||||
bignumber.js "^8.1.1"
|
||||
|
||||
nwsapi@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
|
||||
@ -13989,11 +14232,6 @@ object.values@^1.1.0, object.values@^1.1.3, object.values@^1.1.4:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.18.2"
|
||||
|
||||
obliterator@^1.6.1:
|
||||
version "1.6.1"
|
||||
resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-1.6.1.tgz#dea03e8ab821f6c4d96a299e17aef6a3af994ef3"
|
||||
integrity sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==
|
||||
|
||||
obliterator@^2.0.0:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.4.tgz#fa650e019b2d075d745e44f1effeb13a2adbe816"
|
||||
@ -15411,14 +15649,14 @@ prompts@2.4.0, prompts@^2.0.1:
|
||||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.5"
|
||||
|
||||
prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
version "15.7.2"
|
||||
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz"
|
||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
||||
prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
dependencies:
|
||||
loose-envify "^1.4.0"
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.8.1"
|
||||
react-is "^16.13.1"
|
||||
|
||||
protobufjs@^6.10.0:
|
||||
version "6.11.2"
|
||||
@ -15758,7 +15996,7 @@ react-ga4@^1.4.1:
|
||||
resolved "https://registry.yarnpkg.com/react-ga4/-/react-ga4-1.4.1.tgz#6ee2a2db115ed235b2f2092bc746b4eeeca9e206"
|
||||
integrity sha512-ioBMEIxd4ePw4YtaloTUgqhQGqz5ebDdC4slEpLgy2sLx1LuZBC9iYCwDymTXzcntw6K1dHX183ulP32nNdG7w==
|
||||
|
||||
react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6:
|
||||
react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.6:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
@ -16057,6 +16295,22 @@ redent@^3.0.0:
|
||||
indent-string "^4.0.0"
|
||||
strip-indent "^3.0.0"
|
||||
|
||||
reduce-css-calc@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716"
|
||||
integrity sha512-0dVfwYVOlf/LBA2ec4OwQ6p3X9mYxn/wOl2xTcLwjnPYrkgEfPx3VI4eGCH3rQLlPISG5v9I9bkZosKsNRTRKA==
|
||||
dependencies:
|
||||
balanced-match "^0.4.2"
|
||||
math-expression-evaluator "^1.2.14"
|
||||
reduce-function-call "^1.0.1"
|
||||
|
||||
reduce-function-call@^1.0.1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.3.tgz#60350f7fb252c0a67eb10fd4694d16909971300f"
|
||||
integrity sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
redux-localstorage-simple@^2.3.1:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.npmjs.org/redux-localstorage-simple/-/redux-localstorage-simple-2.4.0.tgz"
|
||||
|
Loading…
Reference in New Issue
Block a user