Compare commits

...

62 Commits

Author SHA1 Message Date
vignesh mohankumar
ef8fba1d49 feat: add git commit hash on build (#4701)
* feat: add git commit hash on build

* process.env.

* works

* log

* import

* log full
2022-09-22 15:29:29 -04:00
Zach Pomerantz
be64c03d06 fix: track origin (#4700)
fix: add origin
2022-09-22 12:02:57 -07:00
Zach Pomerantz
45682ca59e feat: track Brave and UA (#4699)
* fix: track Brave from UA

* feat: track UA
2022-09-22 11:42:33 -07:00
Jordan Frankfurt
397b9d423e feat(explore): rewrite top tokens hook (#4697)
rewrite top tokens hook
2022-09-22 13:06:31 -05:00
Jack Short
d9113fb6d4 feat: adding suspicious and pooled asset icons to cards + rarity (#4686)
* adding pooled assets and suspicious assets icons

* adding rarity

* better way to get states
2022-09-22 12:44:43 -04:00
Jack Short
5dc0df2132 feat: initial bag port (#4665)
* initial bag port

* adding add to bag

* adding remove

* addressing comments

* reenable bag on disconnect when reviewing price changes
2022-09-21 16:44:46 -04:00
Jordan Frankfurt
7f2cc9a3e6 feat: add tvl to token details page (#4692)
* add tvl to token details page

* remove mcap, add explanation of market entities
2022-09-21 15:35:17 -05:00
Charles Bachmeier
d3c4ca6e09 feat: add Sell listing page (#4664)
* add listing modal

* add new files

* Add listing page

* remove useeffect

* re-add useeffect and includes array

* position relative

* add listing datatype

* use pluralize

* readable const

* clsx

* parseFloat 0 default

* don't use any

* cant use months for ms

* remove unused input style

* border sprinkles

* clsx

* duration enum

* remove unused index

* pluralize

* clsx

* pluralize

* type refactoring

* move format to utils

* remove uneeded check

* border sprinkles

* change input based on ref

* remove console.log

* correct warning check

* better clsx

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2022-09-21 13:30:28 -07:00
lynn
28538214d2 feat: new lazy load that scrolls whole window and not inside fixed size container (#4684)
* init

* messy but working omfg

* dont set initial to 500 set to just 1 for testing purposes

* it looks pretty now and works well

* sorting filtering and suspense loading are working

* fix comments

* handle token rows lacking addresS

* start working with new data schema

* new gql schema

* initial commit

* improved performance, added filtering

* lint

* removed comments and accidental settings.json changes

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

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

* merges

* fix

* fix oopsies

* fix accidental changes

* its working

* drop leftover comment

* clean up loaded row props

* respond to comments

* respond to jordan comments

* init

* remove unnecessary pkgs

* undo yarn lock changes

* loading rows fix

* change loading rows to 3 as per fred instruction

* remove anys

Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: cartcrom <cartergcromer@gmail.com>
Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>
2022-09-21 14:03:54 -04:00
Zach Pomerantz
4f48f3372c feat: weth check (#4685)
* feat: throw on invalid deposits

* feat: add new bug link

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

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

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

* drop leftover comment

* clean up loaded row props

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

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

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

* Renamed separate style file

* Fix positioning to not squish details section

* Add back activeRow state

* Hide Activity on smaller screens

* Fix for uneven widths between collections

* Addressing PR feedback

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

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

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

* hide ticks

* reduce offsets

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

* flag favorite tokens

* imports

* remove fav col

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

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

* working

* grid

* some file renaming

* Move to ChainSelectorRow.tsx

* remove flex

* more renames

* more styles

* fixes

* local imports

* more styling changes

* style

* fix mobile

* toggle open on open

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

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

* add new files

* remove useeffect

* re-add useeffect and includes array

* position relative

* add listing datatype

* use pluralize

* readable const

* clsx

* parseFloat 0 default

* don't use any

* cant use months for ms

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

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

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

* chainswitcher and web3status are not using this component

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

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

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

* move to analytics

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

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

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



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

* fixed

* z-index

* fix

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

* working

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

* Add shopping bag to top lovel modal wrapper

* addressing comments

* rename to listingTag

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

* split filters into own file

* include new file

* fix eslint warnings

* update filter button param and fix rerender bug

* de morgon's law

* usecallback

* move max_padding

* extend htmlinputelement for checkbox

* styles cleanup

* simplify checkbox sprinkles

* add null check to collectionfilteritem

* remove x axis scrollbar on collections

* update fitlerbutton logic

* scrollbar width

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

* fix lint

* add comment

* lint fix

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

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

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

* test

* working w new hook

* simsplify

* fixes to comments

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

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

* feat: audio cards

* square aspect ratio for videos

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

* filters

* filter button

* adding all filters to filter check

* sorting exports

* reviewing old css

* change to const

* responding to comments

* removing isMobile

* fixing radio input

* refactoring radio

* refactoring radio

* reusing the same class

* removing unused props

* removing useless clsx

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


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

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

* Changes from PR feedback

* Tweak opacity values

* Style cleanup

* Adjust bg image on smaller screen sizes

* Remove unnecessary media query

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

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

* remove background

* more precise naming

* Add wallet assets for select page

* update styles while without filter bar

* remove unnecessary useeffect

* deprecate old stlye

* move to common props

* add round helper fn

* use react router link

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

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

* updating sell icon
2022-09-14 12:40:42 -04:00
Greg Bugyis
2d9604cd14 fix: Tick collisions on token detail price chart (#4623) 2022-09-14 17:19:38 +03:00
cartcrom
7930709bc3 refactor: unnesting token details into sectioned components (#4621)
finished refactoring
2022-09-14 10:19:18 -04:00
198 changed files with 9675 additions and 1863 deletions

View File

@@ -1,13 +1,21 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { DefinePlugin } = require('webpack')
const commitHash = require('child_process').execSync('git rev-parse HEAD')
module.exports = {
babel: {
plugins: ['@vanilla-extract/babel-plugin'],
},
webpack: {
plugins: [new VanillaExtractPlugin()],
plugins: [
new VanillaExtractPlugin(),
new DefinePlugin({
'process.env.REACT_APP_GIT_COMMIT_HASH': JSON.stringify(commitHash.toString()),
}),
],
configure: (webpackConfig) => {
const instanceOfMiniCssExtractPlugin = webpackConfig.plugins.find(
(plugin) => plugin instanceof MiniCssExtractPlugin

View File

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

View File

@@ -145,7 +145,7 @@
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@uniswap/v3-sdk": "^3.9.0",
"@uniswap/widgets": "^2.7.0",
"@uniswap/widgets": "^2.8.1",
"@vanilla-extract/css": "^1.7.2",
"@vanilla-extract/css-utils": "^0.1.2",
"@vanilla-extract/dynamic": "^2.0.2",

View File

@@ -44,7 +44,14 @@ export const Trace = memo(
useEffect(() => {
if (shouldLogImpression) {
sendAnalyticsEvent(name ?? EventName.PAGE_VIEWED, { ...combinedProps, ...properties })
const origin = window.location.origin
const commitHash = process.env.REACT_APP_GIT_COMMIT_HASH
sendAnalyticsEvent(name ?? EventName.PAGE_VIEWED, {
...combinedProps,
...properties,
origin,
git_commit_hash: commitHash,
})
}
// Impressions should only be logged on mount.
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -15,7 +15,8 @@ export enum EventName {
SWAP_MAX_TOKEN_AMOUNT_SELECTED = 'Swap Max Token Amount Selected',
SWAP_PRICE_UPDATE_ACKNOWLEDGED = 'Swap Price Update Acknowledged',
SWAP_QUOTE_RECEIVED = 'Swap Quote Received',
SWAP_SUBMITTED = 'Swap Submitted',
SWAP_SIGNED = 'Swap Signed',
SWAP_SUBMITTED_BUTTON_CLICKED = 'Swap Submit Button Clicked',
SWAP_TOKENS_REVERSED = 'Swap Tokens Reversed',
SWAP_TRANSACTION_COMPLETED = 'Swap Transaction Completed',
TOKEN_IMPORTED = 'Token Imported',
@@ -24,6 +25,7 @@ export enum EventName {
WALLET_CONNECT_TXN_COMPLETED = 'Wallet Connect Transaction Completed',
WALLET_SELECTED = 'Wallet Selected',
WEB_VITALS = 'Web Vitals',
WRAP_TOKEN_TXN_INVALIDATED = 'Wrap Token Transaction Invalidated',
WRAP_TOKEN_TXN_SUBMITTED = 'Wrap Token Transaction Submitted',
// alphabetize additional event names.
}
@@ -31,6 +33,7 @@ export enum EventName {
export enum CUSTOM_USER_PROPERTIES {
ALL_WALLET_ADDRESSES_CONNECTED = 'all_wallet_addresses_connected',
ALL_WALLET_CHAIN_IDS = 'all_wallet_chain_ids',
USER_AGENT = 'user_agent',
BROWSER = 'browser',
DARK_MODE = 'is_dark_mode',
EXPERT_MODE = 'is_expert_mode',
@@ -49,6 +52,7 @@ export enum BROWSER {
EDGE_CHROMIUM = 'Microsoft Edge (Chromium)',
CHROME = 'Google Chrome or Chromium',
SAFARI = 'Apple Safari',
BRAVE = 'Brave',
UNKNOWN = 'unknown',
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -1,3 +1,186 @@
<svg width="10" height="12" viewBox="0 0 10 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.75 11C4.75 11 5.22377 11.1391 5.80923 10.5828L7.64433 8.65908C8.35405 7.91508 8.74998 6.92678 8.74989 5.89856C8.7498 4.72716 8.74971 3.31706 8.74991 2.50009C8.74996 2.22391 8.77618 2 8.5 2H8.25M6.74898 5.75L6.74979 2L6.74991 1.50009C6.74996 1.22391 6.52609 1 6.24991 1H4.25167C3.97553 1 3.75167 1.22386 3.75167 1.5V4.75039C3.75167 5.29859 3.52665 5.82276 3.12922 6.20034L1.6891 7.56856C1.10364 8.12478 1.10363 9.0266 1.68909 9.58283C2.12197 9.99409 2.75372 10.1013 3.29025 9.90438C3.47937 9.83497 3.65665 9.72779 3.80923 9.58283L5.80923 7.6827M6.74898 5.75L6.7487 6.36119C6.74861 6.63517 6.63611 6.89711 6.43748 7.08582L5.80923 7.6827M6.74898 5.75H6.4384C5.67845 5.75 5.19623 6.56419 5.56146 7.23061L5.80923 7.6827" stroke="white" stroke-linecap="round"/>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20">
<g filter="url(#a)">
<path fill="url(#b)" d="M17.654 4.868c0-.69-.56-1.25-1.25-1.25h-2.932c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.741 1.873c-.898.999-1.115 2.687-.058 3.748 1.013 1.017 2.918.988 3.87 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.158v-7.55Z"/>
<path fill="url(#c)" d="M17.654 4.868c0-.69-.56-1.25-1.25-1.25h-2.932c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.741 1.873c-.898.999-1.115 2.687-.058 3.748 1.013 1.017 2.918.988 3.87 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.158v-7.55Z"/>
<path fill="url(#d)" d="M17.654 4.868c0-.69-.56-1.25-1.25-1.25h-2.932c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.741 1.873c-.898.999-1.115 2.687-.058 3.748 1.013 1.017 2.918.988 3.87 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.158v-7.55Z"/>
<path fill="url(#e)" d="M17.654 4.868c0-.69-.56-1.25-1.25-1.25h-2.932c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.741 1.873c-.898.999-1.115 2.687-.058 3.748 1.013 1.017 2.918.988 3.87 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.158v-7.55Z"/>
<path fill="url(#f)" d="M17.654 4.868c0-.69-.56-1.25-1.25-1.25h-2.932c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.741 1.873c-.898.999-1.115 2.687-.058 3.748 1.013 1.017 2.918.988 3.87 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.158v-7.55Z"/>
</g>
<g filter="url(#g)">
<path fill="url(#h)" d="M10.954 2.602c0-.69-.56-1.25-1.25-1.25H6.773c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.742 1.873c-.898 1-1.115 2.687-.058 3.748 1.014 1.018 2.919.988 3.871 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.157V2.602Z"/>
<path fill="url(#i)" d="M10.954 2.602c0-.69-.56-1.25-1.25-1.25H6.773c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.742 1.873c-.898 1-1.115 2.687-.058 3.748 1.014 1.018 2.919.988 3.871 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.157V2.602Z"/>
<path fill="url(#j)" d="M10.954 2.602c0-.69-.56-1.25-1.25-1.25H6.773c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.742 1.873c-.898 1-1.115 2.687-.058 3.748 1.014 1.018 2.919.988 3.871 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.157V2.602Z"/>
<path fill="url(#k)" d="M10.954 2.602c0-.69-.56-1.25-1.25-1.25H6.773c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.742 1.873c-.898 1-1.115 2.687-.058 3.748 1.014 1.018 2.919.988 3.871 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.157V2.602Z"/>
<path fill="url(#l)" d="M10.954 2.602c0-.69-.56-1.25-1.25-1.25H6.773c-.69 0-1.25.56-1.25 1.25v5.704a2.5 2.5 0 0 1-.67 1.703l-1.742 1.873c-.898 1-1.115 2.687-.058 3.748 1.014 1.018 2.919.988 3.871 0l3.167-3.32a3.125 3.125 0 0 0 .864-2.157V2.602Z"/>
</g>
<path fill="url(#m)" d="M17.654 5.776h-5.431v.673h5.431v-.673Z"/>
<path fill="url(#n)" d="M17.654 5.776h-5.431v.673h5.431v-.673Z"/>
<path fill="url(#o)" d="M10.955 3.51H5.523v.674h5.432V3.51Z"/>
<path fill="url(#p)" d="M10.955 3.51H5.523v.674h5.432V3.51Z"/>
<path fill="url(#q)" d="M17.606 4.523c.031.11.048.225.048.345v.328h-5.431v-.328c0-.12.016-.236.048-.345h5.335Z"/>
<path fill="url(#r)" d="M17.606 4.523c.031.11.048.225.048.345v.328h-5.431v-.328c0-.12.016-.236.048-.345h5.335Z"/>
<path fill="url(#s)" d="M10.907 2.257c.031.11.048.225.048.345v.329H5.523v-.329c0-.12.017-.235.049-.345h5.335Z"/>
<path fill="url(#t)" d="M10.907 2.257c.031.11.048.225.048.345v.329H5.523v-.329c0-.12.017-.235.049-.345h5.335Z"/>
<g filter="url(#u)">
<path fill="url(#v)" d="M17.654 12.236h-3.116a.173.173 0 0 0-.176.17c0 1.255.904 2.3 2.096 2.518l.332-.349a3.125 3.125 0 0 0 .864-2.157v-.182Z"/>
</g>
<g filter="url(#w)">
<path fill="url(#x)" d="M10.955 9.97H7.839a.173.173 0 0 0-.176.17c0 1.255.903 2.3 2.096 2.518l.332-.349a3.125 3.125 0 0 0 .864-2.156V9.97Z"/>
</g>
<g filter="url(#y)">
<path fill="url(#z)" d="M13.243 18.21v-.05a3.424 3.424 0 0 0-3.856-3.397c-.511.986-.496 2.268.366 3.133.892.895 2.474.98 3.49.314Z"/>
</g>
<g filter="url(#A)">
<path fill="url(#B)" d="M6.544 15.944v-.05a3.424 3.424 0 0 0-3.857-3.396c-.51.985-.495 2.267.366 3.132.892.896 2.475.98 3.49.314Z"/>
</g>
<defs>
<linearGradient id="b" x1="17.248" x2="13.193" y1="4.4" y2="16.827" gradientUnits="userSpaceOnUse">
<stop stop-color="#FDF2FF"/>
<stop offset="1" stop-color="#FFECFB"/>
</linearGradient>
<linearGradient id="c" x1="17.654" x2="15.615" y1="11.854" y2="11.854" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFCCF1"/>
<stop offset="1" stop-color="#FFC5EF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="f" x1="14.99" x2="13.818" y1="16.932" y2="15.721" gradientUnits="userSpaceOnUse">
<stop offset=".209" stop-color="#EDB0DF"/>
<stop offset="1" stop-color="#ECAAED" stop-opacity="0"/>
</linearGradient>
<linearGradient id="h" x1="10.549" x2="6.493" y1="2.134" y2="14.562" gradientUnits="userSpaceOnUse">
<stop stop-color="#FDF2FF"/>
<stop offset="1" stop-color="#FFECFB"/>
</linearGradient>
<linearGradient id="i" x1="10.954" x2="8.915" y1="9.588" y2="9.588" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFCCF1"/>
<stop offset="1" stop-color="#FFC5EF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="l" x1="8.29" x2="7.119" y1="14.666" y2="13.456" gradientUnits="userSpaceOnUse">
<stop offset=".209" stop-color="#EDB0DF"/>
<stop offset="1" stop-color="#ECAAED" stop-opacity="0"/>
</linearGradient>
<linearGradient id="m" x1="12.773" x2="16.915" y1="6.449" y2="6.449" gradientUnits="userSpaceOnUse">
<stop stop-color="#E95FDB"/>
<stop offset="1" stop-color="#FF46CB"/>
</linearGradient>
<linearGradient id="n" x1="12.223" x2="12.793" y1="6.449" y2="6.449" gradientUnits="userSpaceOnUse">
<stop stop-color="#9F4977"/>
<stop offset="1" stop-color="#CA5284" stop-opacity="0"/>
</linearGradient>
<linearGradient id="o" x1="6.074" x2="10.216" y1="4.184" y2="4.184" gradientUnits="userSpaceOnUse">
<stop stop-color="#E95FDB"/>
<stop offset="1" stop-color="#FF46CB"/>
</linearGradient>
<linearGradient id="p" x1="5.523" x2="6.094" y1="4.184" y2="4.184" gradientUnits="userSpaceOnUse">
<stop stop-color="#9F4977"/>
<stop offset="1" stop-color="#CA5284" stop-opacity="0"/>
</linearGradient>
<linearGradient id="q" x1="12.773" x2="16.915" y1="5.196" y2="5.196" gradientUnits="userSpaceOnUse">
<stop stop-color="#E95FDB"/>
<stop offset="1" stop-color="#FF46CB"/>
</linearGradient>
<linearGradient id="r" x1="12.223" x2="12.793" y1="5.196" y2="5.196" gradientUnits="userSpaceOnUse">
<stop stop-color="#9F4977"/>
<stop offset="1" stop-color="#CA5284" stop-opacity="0"/>
</linearGradient>
<linearGradient id="s" x1="6.074" x2="10.216" y1="2.931" y2="2.931" gradientUnits="userSpaceOnUse">
<stop stop-color="#E95FDB"/>
<stop offset="1" stop-color="#FF46CB"/>
</linearGradient>
<linearGradient id="t" x1="5.523" x2="6.094" y1="2.931" y2="2.931" gradientUnits="userSpaceOnUse">
<stop stop-color="#9F4977"/>
<stop offset="1" stop-color="#CA5284" stop-opacity="0"/>
</linearGradient>
<radialGradient id="d" cx="0" cy="0" r="1" gradientTransform="matrix(2.8125 0 0 6.01563 12.02 8.026)" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFD1F5"/>
<stop offset="1" stop-color="#FECAFF" stop-opacity="0"/>
</radialGradient>
<radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="matrix(1.54348 -1.53472 2.30642 2.31958 11.806 16.12)" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFC0FC"/>
<stop offset="1" stop-color="#FFBCFC" stop-opacity="0"/>
</radialGradient>
<radialGradient id="j" cx="0" cy="0" r="1" gradientTransform="matrix(2.8125 0 0 6.01563 5.322 5.76)" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFD1F5"/>
<stop offset="1" stop-color="#FECAFF" stop-opacity="0"/>
</radialGradient>
<radialGradient id="k" cx="0" cy="0" r="1" gradientTransform="matrix(1.54347 -1.53473 2.30643 2.31957 5.107 13.854)" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFC0FC"/>
<stop offset="1" stop-color="#FFBCFC" stop-opacity="0"/>
</radialGradient>
<radialGradient id="v" cx="0" cy="0" r="1" gradientTransform="matrix(-.42911 2.50572 -2.20218 -.37713 16.437 12.236)" gradientUnits="userSpaceOnUse">
<stop offset=".147" stop-color="#FF52CF"/>
<stop offset="1" stop-color="#FB3FFF"/>
</radialGradient>
<radialGradient id="x" cx="0" cy="0" r="1" gradientTransform="matrix(-.42911 2.50572 -2.20218 -.37713 9.738 9.97)" gradientUnits="userSpaceOnUse">
<stop offset=".147" stop-color="#FF52CF"/>
<stop offset="1" stop-color="#FB3FFF"/>
</radialGradient>
<radialGradient id="z" cx="0" cy="0" r="1" gradientTransform="rotate(167.471 5.464 9.437) scale(4.54024 4.83245)" gradientUnits="userSpaceOnUse">
<stop offset=".268" stop-color="#FF4EE3"/>
<stop offset=".92" stop-color="#D12396"/>
</radialGradient>
<radialGradient id="B" cx="0" cy="0" r="1" gradientTransform="rotate(167.471 2.239 7.937) scale(4.54024 4.83245)" gradientUnits="userSpaceOnUse">
<stop offset=".268" stop-color="#FF4EE3"/>
<stop offset=".92" stop-color="#D12396"/>
</radialGradient>
<filter id="a" width="8.808" height="15.23" x="9.045" y="3.418" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dx=".2" dy="-.2"/>
<feGaussianBlur stdDeviation=".3"/>
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
<feColorMatrix values="0 0 0 0 0.522043 0 0 0 0 0.119948 0 0 0 0 0.5875 0 0 0 1 0"/>
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
</filter>
<filter id="g" width="8.808" height="15.23" x="2.346" y="1.152" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dx=".2" dy="-.2"/>
<feGaussianBlur stdDeviation=".3"/>
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
<feColorMatrix values="0 0 0 0 0.545098 0 0 0 0 0.219608 0 0 0 0 0.512549 0 0 0 1 0"/>
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
</filter>
<filter id="u" width="3.541" height="2.688" x="14.112" y="12.236" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dx="-.25"/>
<feGaussianBlur stdDeviation=".25"/>
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
<feColorMatrix values="0 0 0 0 0.976471 0 0 0 0 0.145098 0 0 0 0 0.743686 0 0 0 1 0"/>
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
</filter>
<filter id="w" width="3.541" height="2.688" x="7.413" y="9.97" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dx="-.25"/>
<feGaussianBlur stdDeviation=".25"/>
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
<feColorMatrix values="0 0 0 0 0.976471 0 0 0 0 0.145098 0 0 0 0 0.743686 0 0 0 1 0"/>
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
</filter>
<filter id="y" width="4.448" height="4.112" x="9.045" y="14.536" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dx=".25" dy="-.2"/>
<feGaussianBlur stdDeviation=".25"/>
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
<feColorMatrix values="0 0 0 0 0.906118 0 0 0 0 0.329412 0 0 0 0 1 0 0 0 1 0"/>
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
</filter>
<filter id="A" width="4.448" height="4.112" x="2.346" y="12.27" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dx=".25" dy="-.2"/>
<feGaussianBlur stdDeviation=".25"/>
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/>
<feColorMatrix values="0 0 0 0 0.906118 0 0 0 0 0.329412 0 0 0 0 1 0 0 0 1 0"/>
<feBlend in2="shape" result="effect1_innerShadow_6126_86420"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 871 B

After

Width:  |  Height:  |  Size: 13 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ import { ConnectionType } from 'connection'
import { NavBarVariant, useNavBarFlag } from 'featureFlags/flags/navBar'
import useENSAvatar from 'hooks/useENSAvatar'
import styled from 'styled-components/macro'
import { colors } from 'theme/colors'
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
@@ -29,20 +28,18 @@ const IconWrapper = styled.div<{ size?: number }>`
const SockContainer = styled.div`
position: absolute;
background-color: ${colors.pink400};
display: flex;
justify-content: center;
border-radius: 50%;
width: 16px;
height: 16px;
bottom: -5px;
right: -5px;
bottom: -4px;
right: -4px;
`
const SockImg = styled.img`
width: 7.5px;
height: 10px;
margin-top: 3px;
width: 16px;
height: 16px;
`
const Socks = () => {

View File

@@ -19,7 +19,7 @@ const HandleAccent = styled.path`
stroke-width: 1.5;
stroke: ${({ theme }) => theme.deprecated_white};
opacity: 0.6;
opacity: ${({ theme }) => theme.opacity.hover};
`
const LabelGroup = styled.g<{ visible: boolean }>`

View File

@@ -278,7 +278,7 @@ export default function Menu() {
</ToggleMenuItem>
<ToggleMenuItem onClick={() => toggleDarkMode()}>
<div>{darkMode ? <Trans>Light Theme</Trans> : <Trans>Dark Theme</Trans>}</div>
{darkMode ? <Moon opacity={0.6} size={16} /> : <Sun opacity={0.6} size={16} />}
{darkMode ? <Sun opacity={0.6} size={16} /> : <Moon opacity={0.6} size={16} />}
</ToggleMenuItem>
<MenuItem href="https://docs.uniswap.org/">
<div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -126,7 +126,7 @@ export const MenuDropdown = () => {
<>
<Box position="relative" ref={ref}>
<NavIcon isActive={isOpen} onClick={toggleOpen}>
<EllipsisIcon />
<EllipsisIcon width={20} height={20} />
</NavIcon>
{isOpen && (

View File

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

View File

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

View File

@@ -34,6 +34,7 @@ export const searchBarContainer = style([
'@media': {
[`screen and (min-width: ${breakpoints.lg}px)`]: {
right: `-${DESKTOP_NAVBAR_WIDTH / 2 - MAGNIFYING_GLASS_ICON_WIDTH}px`,
top: '-5px',
},
},
},

View File

@@ -7,7 +7,6 @@ import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { organizeSearchResults } from 'lib/utils/searchBar'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { Overlay } from 'nft/components/modals/Overlay'
import { magicalGradientOnHover, subheadSmall } from 'nft/css/common.css'
import { useIsMobile, useIsTablet, useSearchHistory } from 'nft/hooks'
import { fetchSearchCollections, fetchTrendingCollections } from 'nft/queries'
@@ -96,9 +95,8 @@ interface SearchBarDropdownProps {
export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput, isLoading }: SearchBarDropdownProps) => {
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0)
const searchHistory = useSearchHistory(
(state: { history: (FungibleToken | GenieCollection)[] }) => state.history
).slice(0, 2)
const searchHistory = useSearchHistory((state: { history: (FungibleToken | GenieCollection)[] }) => state.history)
const shortenedHistory = useMemo(() => searchHistory.slice(0, 2), [searchHistory])
const { pathname } = useLocation()
const isNFTPage = pathname.includes('/nfts')
const isTokenPage = pathname.includes('/tokens')
@@ -182,7 +180,7 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput, i
const totalSuggestions = hasInput
? tokens.length + collections.length
: Math.min(searchHistory.length, 2) +
: Math.min(shortenedHistory.length, 2) +
(isNFTPage || !isTokenPage ? trendingCollections?.length ?? 0 : 0) +
(isTokenPage || !isNFTPage ? trendingTokens?.length ?? 0 : 0)
@@ -234,13 +232,13 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput, i
) : (
// Recent Searches, Trending Tokens, Trending Collections
<Column gap="20">
{searchHistory.length > 0 && (
{shortenedHistory.length > 0 && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={0}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={searchHistory}
suggestions={shortenedHistory}
header={<Trans>Recent searches</Trans>}
headerIcon={<ClockIcon />}
/>
@@ -248,7 +246,7 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput, i
{!isNFTPage && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={searchHistory.length}
startingIndex={shortenedHistory.length}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={trendingTokens}
@@ -260,7 +258,7 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput, i
{!isTokenPage && phase1Flag === NftVariant.Enabled && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={searchHistory.length + (isNFTPage ? 0 : trendingTokens?.length ?? 0)}
startingIndex={shortenedHistory.length + (isNFTPage ? 0 : trendingTokens?.length ?? 0)}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={trendingCollections as unknown as GenieCollection[]}
@@ -286,7 +284,7 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput, i
hoveredIndex,
phase1Flag,
toggleOpen,
searchHistory,
shortenedHistory,
hasInput,
isNFTPage,
isTokenPage,
@@ -434,9 +432,8 @@ export const SearchBar = () => {
</Box>
</Box>
<NavIcon onClick={toggleOpen}>
<NavMagnifyingGlassIcon width={28} height={28} />
<NavMagnifyingGlassIcon />
</NavIcon>
{isOpen && <Overlay />}
</Box>
)
}

View File

@@ -0,0 +1,22 @@
import { style } from '@vanilla-extract/css'
import { sprinkles } from 'nft/css/sprinkles.css'
export const bagQuantity = style([
sprinkles({
position: 'absolute',
top: '4',
right: '4',
backgroundColor: 'magicGradient',
borderRadius: 'round',
color: 'explicitWhite',
textAlign: 'center',
fontWeight: 'semibold',
paddingY: '1',
paddingX: '4',
}),
{
fontSize: '8px',
lineHeight: '12px',
minWidth: '14px',
},
])

View File

@@ -0,0 +1,47 @@
import { NavIcon } from 'components/NavBar/NavIcon'
import * as styles from 'components/NavBar/ShoppingBag.css'
import { Box } from 'nft/components/Box'
import { BagIcon, HundredsOverflowIcon, TagIcon } from 'nft/components/icons'
import { useBag, useSellAsset } from 'nft/hooks'
import { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom'
export const ShoppingBag = () => {
const itemsInBag = useBag((state) => state.itemsInBag)
const sellAssets = useSellAsset((state) => state.sellAssets)
const [bagQuantity, setBagQuantity] = useState(0)
const [sellQuantity, setSellQuantity] = useState(0)
const location = useLocation()
const toggleBag = useBag((s) => s.toggleBag)
useEffect(() => {
setBagQuantity(itemsInBag.length)
}, [itemsInBag])
useEffect(() => {
setSellQuantity(sellAssets.length)
}, [sellAssets])
const isSell = location.pathname === '/nfts/sell'
return (
<NavIcon onClick={toggleBag}>
{isSell ? (
<>
<TagIcon width={20} height={20} />
{sellQuantity ? (
<Box className={styles.bagQuantity}>{sellQuantity > 99 ? <HundredsOverflowIcon /> : sellQuantity}</Box>
) : null}
</>
) : (
<>
<BagIcon width={20} height={20} />
{bagQuantity ? (
<Box className={styles.bagQuantity}>{bagQuantity > 99 ? <HundredsOverflowIcon /> : bagQuantity}</Box>
) : null}
</>
)}
</NavIcon>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import { navigatorLocale, useActiveLocale } from '../../hooks/useActiveLocale'
import { StyledInternalLink, ThemedText } from '../../theme'
const Container = styled(ThemedText.DeprecatedSmall)`
opacity: 0.6;
opacity: ${({ theme }) => theme.opacity.hover};
:hover {
opacity: 1;
}

View File

@@ -64,7 +64,7 @@ const StyledCloseButton = styled(StyledButton)`
&:hover {
background-color: ${({ theme }) => theme.backgroundInteractive};
opacity: 0.6;
opacity: ${({ theme }) => theme.opacity.hover};
transition: opacity 250ms ease;
}
`
@@ -132,10 +132,10 @@ const ExplorerLinkWrapper = styled.div`
cursor: pointer;
:hover {
opacity: 0.6;
opacity: ${({ theme }) => theme.opacity.hover};
}
:active {
opacity: 0.4;
opacity: ${({ theme }) => theme.opacity.click};
}
`
@@ -265,7 +265,13 @@ export default function TokenSafety({
</InfoText>
</ShortColumn>
<LinkColumn>{urls}</LinkColumn>
<Buttons warning={displayWarning} onContinue={acknowledge} onCancel={onCancel} showCancel={showCancel} />
<Buttons
warning={displayWarning}
onContinue={acknowledge}
onCancel={onCancel}
onBlocked={onBlocked}
showCancel={showCancel}
/>
</Container>
</Wrapper>
)

View File

@@ -0,0 +1,103 @@
import { Trans } from '@lingui/macro'
import { darken } from 'polished'
import { useState } from 'react'
import styled from 'styled-components/macro'
import Resource from './Resource'
const NoInfoAvailable = styled.span`
color: ${({ theme }) => theme.textTertiary};
font-weight: 400;
font-size: 16px;
`
const TokenDescriptionContainer = styled.div`
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
max-height: fit-content;
padding-top: 16px;
line-height: 24px;
white-space: pre-wrap;
`
const TruncateDescriptionButton = styled.div`
color: ${({ theme }) => theme.textSecondary};
font-weight: 400;
font-size: 14px;
padding-top: 14px;
&:hover,
&:focus {
color: ${({ theme }) => darken(0.1, theme.textSecondary)};
cursor: pointer;
}
`
const truncateDescription = (desc: string) => {
//trim the string to the maximum length
let tokenDescriptionTruncated = desc.slice(0, TRUNCATE_CHARACTER_COUNT)
//re-trim if we are in the middle of a word
tokenDescriptionTruncated = `${tokenDescriptionTruncated.slice(
0,
Math.min(tokenDescriptionTruncated.length, tokenDescriptionTruncated.lastIndexOf(' '))
)}...`
return tokenDescriptionTruncated
}
const TRUNCATE_CHARACTER_COUNT = 400
export const AboutContainer = styled.div`
gap: 16px;
padding: 24px 0px;
`
export const AboutHeader = styled.span`
font-size: 28px;
line-height: 36px;
`
export const ResourcesContainer = styled.div`
display: flex;
padding-top: 12px;
gap: 14px;
`
type AboutSectionProps = {
address: string
description?: string | null | undefined
homepageUrl?: string | null | undefined
twitterName?: string | null | undefined
}
export function AboutSection({ address, description, homepageUrl, twitterName }: AboutSectionProps) {
const [isDescriptionTruncated, setIsDescriptionTruncated] = useState(true)
const shouldTruncate = !!description && description.length > TRUNCATE_CHARACTER_COUNT
const tokenDescription = shouldTruncate && isDescriptionTruncated ? truncateDescription(description) : description
return (
<AboutContainer>
<AboutHeader>
<Trans>About</Trans>
</AboutHeader>
<TokenDescriptionContainer>
{!description && (
<NoInfoAvailable>
<Trans>No token information available</Trans>
</NoInfoAvailable>
)}
{tokenDescription}
{shouldTruncate && (
<TruncateDescriptionButton onClick={() => setIsDescriptionTruncated(!isDescriptionTruncated)}>
{isDescriptionTruncated ? <Trans>Read more</Trans> : <Trans>Hide</Trans>}
</TruncateDescriptionButton>
)}
</TokenDescriptionContainer>
<ResourcesContainer>
<Resource name={'Etherscan'} link={`https://etherscan.io/address/${address}`} />
<Resource name={'Protocol info'} link={`https://info.uniswap.org/#/tokens/${address}`} />
{homepageUrl && <Resource name={'Website'} link={homepageUrl} />}
{twitterName && <Resource name={'Twitter'} link={`https://twitter.com/${twitterName}`} />}
</ResourcesContainer>
</AboutContainer>
)
}

View File

@@ -0,0 +1,36 @@
import { Trans } from '@lingui/macro'
import styled from 'styled-components/macro'
import { CopyContractAddress } from 'theme'
export const ContractAddressSection = styled.div`
display: flex;
flex-direction: column;
color: ${({ theme }) => theme.textSecondary};
font-weight: 600;
font-size: 14px;
gap: 4px;
padding: 36px 0px;
`
const ContractAddress = styled.button`
display: flex;
color: ${({ theme }) => theme.textPrimary};
gap: 10px;
align-items: center;
background: transparent;
border: none;
min-height: 38px;
padding: 0px;
cursor: pointer;
`
export default function AddressSection({ address }: { address: string }) {
return (
<ContractAddressSection>
<Trans>Contract address</Trans>
<ContractAddress>
<CopyContractAddress address={address} />
</ContractAddress>
</ContractAddressSection>
)
}

View File

@@ -1,6 +1,6 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { formatToDecimal } from 'components/AmplitudeAnalytics/utils'
import { formatToDecimal } from 'analytics/utils'
import { useToken } from 'hooks/Tokens'
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'

View File

@@ -0,0 +1,18 @@
import { Link } from 'react-router-dom'
import styled from 'styled-components/macro'
export const BreadcrumbNavLink = styled(Link)`
display: flex;
color: ${({ theme }) => theme.textSecondary};
font-size: 14px;
line-height: 20px;
align-items: center;
gap: 4px;
text-decoration: none;
margin-bottom: 16px;
transition-duration: ${({ theme }) => theme.transition.duration.fast};
&:hover {
color: ${({ theme }) => theme.textTertiary};
}
`

View File

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

View File

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

View File

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

View File

@@ -4,16 +4,26 @@ import { EventType } from '@visx/event/lib/types'
import { GlyphCircle } from '@visx/glyph'
import { Line } from '@visx/shape'
import { filterTimeAtom } from 'components/Tokens/state'
import { bisect, curveCardinal, NumberValue, scaleLinear, timeDay, timeHour, timeMinute, timeMonth } from 'd3'
import {
bisect,
curveCardinal,
NumberValue,
scaleLinear,
timeDay,
timeHour,
timeMinute,
timeMonth,
timeTicks,
} from 'd3'
import { TokenPrices$key } from 'graphql/data/__generated__/TokenPrices.graphql'
import { useTokenPricesCached } from 'graphql/data/Token'
import { PricePoint, TimePeriod } from 'graphql/data/Token'
import { PricePoint } from 'graphql/data/Token'
import { TimePeriod } from 'graphql/data/util'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { useAtom } from 'jotai'
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { ArrowDownRight, ArrowUpRight } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { OPACITY_HOVER } from 'theme'
import {
dayHourFormatter,
hourFormatter,
@@ -31,7 +41,7 @@ import { DISPLAYS, ORDERED_TIMES } from '../TokenTable/TimeSelector'
export const DATA_EMPTY = { value: 0, timestamp: 0 }
function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
export function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
const prices = pricePoints.map((x) => x.value)
const min = Math.min(...prices)
const max = Math.max(...prices)
@@ -109,43 +119,51 @@ const TimeButton = styled.button<{ active: boolean }>`
border: none;
cursor: pointer;
color: ${({ theme, active }) => (active ? theme.textPrimary : theme.textSecondary)};
transition-duration: ${({ theme }) => theme.transition.duration.fast};
:hover {
${({ active }) => !active && `opacity: ${OPACITY_HOVER};`}
${({ active, theme }) => !active && `opacity: ${theme.opacity.hover};`}
}
`
const margin = { top: 100, bottom: 48, crosshair: 72 }
const timeOptionsHeight = 44
const crosshairDateOverhang = 80
interface PriceChartProps {
width: number
height: number
tokenAddress: string
priceData?: TokenPrices$key | null
priceDataFragmentRef?: TokenPrices$key | null
}
export function PriceChart({ width, height, tokenAddress, priceData }: PriceChartProps) {
export function PriceChart({ width, height, tokenAddress, priceDataFragmentRef }: PriceChartProps) {
const [timePeriod, setTimePeriod] = useAtom(filterTimeAtom)
const locale = useActiveLocale()
const theme = useTheme()
const { priceMap } = useTokenPricesCached(priceData, tokenAddress, 'ETHEREUM', timePeriod)
const { priceMap } = useTokenPricesCached(priceDataFragmentRef, tokenAddress, 'ETHEREUM', timePeriod)
const prices = priceMap.get(timePeriod)
// first price point on the x-axis of the current time period's chart
const startingPrice = prices?.[0] ?? DATA_EMPTY
// last price point on the x-axis of the current time period's chart
const endingPrice = prices?.[prices.length - 1] ?? DATA_EMPTY
const [displayPrice, setDisplayPrice] = useState(startingPrice)
// set display price to ending price when prices have changed.
useEffect(() => {
if (prices) {
setDisplayPrice(endingPrice)
}
}, [prices, endingPrice])
const [crosshair, setCrosshair] = useState<number | null>(null)
const graphWidth = width + crosshairDateOverhang
const graphHeight = height - timeOptionsHeight > 0 ? height - timeOptionsHeight : 0
const graphInnerHeight = graphHeight - margin.top - margin.bottom > 0 ? graphHeight - margin.top - margin.bottom : 0
// Defining scales
// x scale
const timeScale = useMemo(
() => scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, width]).nice(),
() => scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, width]),
[startingPrice, endingPrice, width]
)
// y scale
@@ -163,44 +181,47 @@ export function PriceChart({ width, height, tokenAddress, priceData }: PriceChar
timePeriod: TimePeriod,
locale: string
): [TickFormatter<NumberValue>, (v: number) => string, NumberValue[]] {
const startDate = new Date(startingPrice.timestamp.valueOf() * 1000)
const endDate = new Date(endingPrice.timestamp.valueOf() * 1000)
const offsetTime = (endingPrice.timestamp.valueOf() - startingPrice.timestamp.valueOf()) / 24
const startDateWithOffset = new Date((startingPrice.timestamp.valueOf() + offsetTime) * 1000)
const endDateWithOffset = new Date((endingPrice.timestamp.valueOf() - offsetTime) * 1000)
switch (timePeriod) {
case TimePeriod.HOUR:
return [
hourFormatter(locale),
dayHourFormatter(locale),
timeMinute.range(startDate, endDate, 10).map((x) => x.valueOf() / 1000),
(timeMinute.every(5) ?? timeMinute)
.range(startDateWithOffset, endDateWithOffset, 2)
.map((x) => x.valueOf() / 1000),
]
case TimePeriod.DAY:
return [
hourFormatter(locale),
dayHourFormatter(locale),
timeHour.range(startDate, endDate, 4).map((x) => x.valueOf() / 1000),
timeHour.range(startDateWithOffset, endDateWithOffset, 4).map((x) => x.valueOf() / 1000),
]
case TimePeriod.WEEK:
return [
weekFormatter(locale),
dayHourFormatter(locale),
timeDay.range(startDate, endDate, 1).map((x) => x.valueOf() / 1000),
timeDay.range(startDateWithOffset, endDateWithOffset, 1).map((x) => x.valueOf() / 1000),
]
case TimePeriod.MONTH:
return [
monthDayFormatter(locale),
dayHourFormatter(locale),
timeDay.range(startDate, endDate, 7).map((x) => x.valueOf() / 1000),
timeDay.range(startDateWithOffset, endDateWithOffset, 7).map((x) => x.valueOf() / 1000),
]
case TimePeriod.YEAR:
return [
monthTickFormatter(locale),
monthYearDayFormatter(locale),
timeMonth.range(startDate, endDate, 2).map((x) => x.valueOf() / 1000),
timeMonth.range(startDateWithOffset, endDateWithOffset, 2).map((x) => x.valueOf() / 1000),
]
case TimePeriod.ALL:
return [
monthYearFormatter(locale),
monthYearDayFormatter(locale),
timeMonth.range(startDate, endDate, 3).map((x) => x.valueOf() / 1000),
timeTicks(startDateWithOffset, endDateWithOffset, 6).map((x) => x.valueOf() / 1000),
]
}
}
@@ -254,8 +275,8 @@ export function PriceChart({ width, height, tokenAddress, priceData }: PriceChar
const crosshairEdgeMax = width * 0.85
const crosshairAtEdge = !!crosshair && crosshair > crosshairEdgeMax
/* Default curve doesn't look good for the ALL chart */
const curveTension = timePeriod === TimePeriod.ALL ? 0.75 : 0.9
/* Default curve doesn't look good for the HOUR/ALL chart */
const curveTension = timePeriod === TimePeriod.ALL ? 0.75 : timePeriod === TimePeriod.HOUR ? 1 : 0.9
return (
<>
@@ -273,7 +294,7 @@ export function PriceChart({ width, height, tokenAddress, priceData }: PriceChar
marginTop={margin.top}
curve={curveCardinal.tension(curveTension)}
strokeWidth={2}
width={graphWidth}
width={width}
height={graphHeight}
>
{crosshair !== null ? (
@@ -284,6 +305,7 @@ export function PriceChart({ width, height, tokenAddress, priceData }: PriceChar
tickFormat={tickFormatter}
tickStroke={theme.backgroundOutline}
tickLength={4}
hideTicks={true}
tickTransform={'translate(0 -5)'}
tickValues={ticks}
top={graphHeight - 1}

View File

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

View File

@@ -0,0 +1,68 @@
import { Trans } from '@lingui/macro'
import { ReactNode } from 'react'
import styled from 'styled-components/macro'
import { formatDollarAmount } from 'utils/formatDollarAmt'
export const StatWrapper = styled.div`
display: flex;
flex-direction: column;
color: ${({ theme }) => theme.textSecondary};
font-size: 14px;
min-width: 168px;
flex: 1;
gap: 4px;
padding: 24px 0px;
`
export const TokenStatsSection = styled.div`
display: flex;
flex-wrap: wrap;
`
export const StatPair = styled.div`
display: flex;
flex: 1;
flex-wrap: wrap;
`
const StatPrice = styled.span`
font-size: 28px;
color: ${({ theme }) => theme.textPrimary};
`
const NoData = styled.div`
color: ${({ theme }) => theme.textTertiary};
`
type NumericStat = number | undefined | null
function Stat({ value, title }: { value: NumericStat; title: ReactNode }) {
return (
<StatWrapper>
{title}
<StatPrice>{value ? formatDollarAmount(value) : '-'}</StatPrice>
</StatWrapper>
)
}
type StatsSectionProps = {
priceLow52W?: NumericStat
priceHigh52W?: NumericStat
TVL?: NumericStat
volume24H?: NumericStat
}
export default function StatsSection(props: StatsSectionProps) {
const { priceLow52W, priceHigh52W, TVL, volume24H } = props
if (TVL || volume24H || priceLow52W || priceHigh52W) {
return (
<TokenStatsSection>
<StatPair>
<Stat value={TVL} title={<Trans>Total Value Locked</Trans>} />
<Stat value={volume24H} title={<Trans>24H volume</Trans>} />
</StatPair>
<StatPair>
<Stat value={priceLow52W} title={<Trans>52W low</Trans>} />
<Stat value={priceHigh52W} title={<Trans>52W high</Trans>} />
</StatPair>
</TokenStatsSection>
)
} else {
return <NoData>No stats available</NoData>
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,26 @@
//import { ReactNode } from 'react'
import styled from 'styled-components/macro'
const FilterOption = styled.button<{ active: boolean; highlight?: boolean }>`
height: 100%;
color: ${({ theme, active }) => (active ? theme.accentActive : theme.textPrimary)};
background-color: ${({ theme, active }) => (active ? theme.accentActiveSoft : theme.backgroundInteractive)};
margin: 0;
padding: 6px 12px 6px 14px;
border-radius: 12px;
font-size: 16px;
line-height: 24px;
font-weight: 600;
transition-duration: ${({ theme }) => theme.transition.duration.fast};
border: none;
outline: ${({ theme, active, highlight }) => (active && highlight ? `1px solid ${theme.accentAction}` : 'none')};
:hover {
cursor: pointer;
background-color: ${({ theme, active }) => (active ? theme.accentActiveSoft : theme.backgroundModule)};
opacity: ${({ theme, active }) => (active ? theme.opacity.hover : 1)};
}
:focus {
background-color: ${({ theme, active }) => (active ? theme.accentActiveSoft : theme.backgroundInteractive)};
}
`
export default FilterOption

View File

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

View File

@@ -27,6 +27,7 @@ const SearchInput = styled.input`
font-size: 16px;
padding-left: 40px;
color: ${({ theme }) => theme.textSecondary};
transition-duration: ${({ theme }) => theme.transition.duration.fast};
:hover {
background-color: ${({ theme }) => theme.backgroundSurface};

View File

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

View File

@@ -1,15 +1,17 @@
import { Trans } from '@lingui/macro'
import { ParentSize } from '@visx/responsive'
import { sendAnalyticsEvent } from 'components/AmplitudeAnalytics'
import { EventName } from 'components/AmplitudeAnalytics/constants'
import { sendAnalyticsEvent } from 'analytics'
import { EventName } from 'analytics/constants'
import SparklineChart from 'components/Charts/SparklineChart'
import CurrencyLogo from 'components/CurrencyLogo'
import { getChainInfo } from 'constants/chainInfo'
import { getDurationDetails, SingleTokenData } from 'graphql/data/Token'
import { TimePeriod } from 'graphql/data/Token'
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
import { TokenSortMethod, TopToken } from 'graphql/data/TopTokens'
import { TimePeriod } from 'graphql/data/util'
import { useCurrency } from 'hooks/Tokens'
import { useAtomValue } from 'jotai/utils'
import { ReactNode } from 'react'
import { ForwardedRef, forwardRef } from 'react'
import { CSSProperties, HTMLProps, ReactHTMLElement, ReactNode } from 'react'
import { ArrowDown, ArrowUp, Heart } from 'react-feather'
import { Link } from 'react-router-dom'
import styled, { css, useTheme } from 'styled-components/macro'
@@ -27,26 +29,33 @@ import {
filterNetworkAtom,
filterStringAtom,
filterTimeAtom,
sortCategoryAtom,
sortDirectionAtom,
sortAscendingAtom,
sortMethodAtom,
useIsFavorited,
useSetSortCategory,
useSetSortMethod,
useToggleFavorite,
} from '../state'
import { formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart'
import { Category, SortDirection } from '../types'
import { DISPLAYS } from './TimeSelector'
export const MAX_TOKENS_TO_LOAD = 100
const Cell = styled.div`
display: flex;
align-items: center;
justify-content: center;
`
const StyledTokenRow = styled.div<{ first?: boolean; last?: boolean; loading?: boolean }>`
const StyledTokenRow = styled.div<{
first?: boolean
last?: boolean
loading?: boolean
favoriteTokensEnabled?: boolean
}>`
background-color: transparent;
display: grid;
font-size: 15px;
grid-template-columns: 1fr 7fr 4fr 4fr 4fr 4fr 5fr 1.2fr;
grid-template-columns: ${({ favoriteTokensEnabled }) =>
favoriteTokensEnabled ? '1fr 7fr 4fr 4fr 4fr 4fr 5fr 1.2fr' : '1fr 7fr 4fr 4fr 4fr 4fr 5fr'};
height: 60px;
line-height: 24px;
max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT};
@@ -61,6 +70,7 @@ const StyledTokenRow = styled.div<{ first?: boolean; last?: boolean; loading?: b
},
}) => css`background-color ${duration.medium} ${timing.ease}`};
width: 100%;
transition-duration: ${({ theme }) => theme.transition.duration.fast};
&:hover {
${({ loading, theme }) =>
@@ -225,15 +235,12 @@ const SortArrowCell = styled(Cell)`
`
const HeaderCellWrapper = styled.span<{ onClick?: () => void }>`
align-items: center;
${ClickableStyle}
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'unset')};
display: flex;
height: 100%;
justify-content: flex-end;
width: 100%;
&:hover {
opacity: 60%;
}
`
const SparkLineCell = styled(Cell)`
padding: 0px 24px;
@@ -325,9 +332,10 @@ const LogoContainer = styled.div`
`
/* formatting for volume with timeframe header display */
function getHeaderDisplay(category: string, timeframe: TimePeriod): string {
if (category === Category.volume || category === Category.percentChange) return `${DISPLAYS[timeframe]} ${category}`
return category
function getHeaderDisplay(method: string, timeframe: TimePeriod): string {
if (method === TokenSortMethod.VOLUME || method === TokenSortMethod.PERCENT_CHANGE)
return `${DISPLAYS[timeframe]} ${method}`
return method
}
/* Get singular header cell for header row */
@@ -335,20 +343,20 @@ function HeaderCell({
category,
sortable,
}: {
category: Category // TODO: change this to make it work for trans
category: TokenSortMethod // TODO: change this to make it work for trans
sortable: boolean
}) {
const theme = useTheme()
const sortDirection = useAtomValue<SortDirection>(sortDirectionAtom)
const handleSortCategory = useSetSortCategory(category)
const sortCategory = useAtomValue<Category>(sortCategoryAtom)
const timeframe = useAtomValue<TimePeriod>(filterTimeAtom)
const sortAscending = useAtomValue(sortAscendingAtom)
const handleSortCategory = useSetSortMethod(category)
const sortMethod = useAtomValue(sortMethodAtom)
const timeframe = useAtomValue(filterTimeAtom)
if (sortCategory === category) {
if (sortMethod === category) {
return (
<HeaderCellWrapper onClick={handleSortCategory}>
<SortArrowCell>
{sortDirection === SortDirection.increasing ? (
{sortAscending ? (
<ArrowUp size={14} color={theme.accentActive} />
) : (
<ArrowDown size={14} color={theme.accentActive} />
@@ -396,7 +404,9 @@ export function TokenRow({
tokenInfo: ReactNode
volume: ReactNode
last?: boolean
style?: CSSProperties
}) {
const favoriteTokensEnabled = useFavoriteTokensFlag() === FavoriteTokensVariant.Enabled
const rowCells = (
<>
<ListNumberCell header={header}>{listNumber}</ListNumberCell>
@@ -406,11 +416,15 @@ export function TokenRow({
<MarketCapCell sortable={header}>{marketCap}</MarketCapCell>
<VolumeCell sortable={header}>{volume}</VolumeCell>
<SparkLineCell>{sparkLine}</SparkLineCell>
<FavoriteCell>{favorited}</FavoriteCell>
{favoriteTokensEnabled && <FavoriteCell>{favorited}</FavoriteCell>}
</>
)
if (header) return <StyledHeaderRow>{rowCells}</StyledHeaderRow>
return <StyledTokenRow {...rest}>{rowCells}</StyledTokenRow>
if (header) return <StyledHeaderRow favoriteTokensEnabled={favoriteTokensEnabled}>{rowCells}</StyledHeaderRow>
return (
<StyledTokenRow favoriteTokensEnabled={favoriteTokensEnabled} {...rest}>
{rowCells}
</StyledTokenRow>
)
}
/* Header Row: top header row component for table */
@@ -421,10 +435,10 @@ export function HeaderRow() {
favorited={null}
listNumber="#"
tokenInfo={<Trans>Token Name</Trans>}
price={<HeaderCell category={Category.price} sortable />}
percentChange={<HeaderCell category={Category.percentChange} sortable />}
marketCap={<HeaderCell category={Category.marketCap} sortable />}
volume={<HeaderCell category={Category.volume} sortable />}
price={<HeaderCell category={TokenSortMethod.PRICE} sortable />}
percentChange={<HeaderCell category={TokenSortMethod.PERCENT_CHANGE} sortable />}
marketCap={<HeaderCell category={TokenSortMethod.TOTAL_VALUE_LOCKED} sortable />}
volume={<HeaderCell category={TokenSortMethod.VOLUME} sortable />}
sparkLine={null}
/>
)
@@ -453,31 +467,29 @@ export function LoadingRow() {
)
}
/* Loaded State: row component with token information */
export default function LoadedRow({
tokenListIndex,
tokenListLength,
tokenData,
timePeriod,
}: {
interface LoadedRowProps extends HTMLProps<ReactHTMLElement<HTMLElement>> {
tokenListIndex: number
tokenListLength: number
tokenData: SingleTokenData
timePeriod: TimePeriod
}) {
const tokenAddress = tokenData?.tokens?.[0].address
token: TopToken
}
/* Loaded State: row component with token information */
export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HTMLDivElement>) => {
const { tokenListIndex, tokenListLength, token } = props
const tokenAddress = token?.address
const currency = useCurrency(tokenAddress)
const tokenName = tokenData?.name
const tokenSymbol = tokenData?.tokens?.[0].symbol
const tokenName = token?.name
const tokenSymbol = token?.symbol
const isFavorited = useIsFavorited(tokenAddress)
const toggleFavorite = useToggleFavorite(tokenAddress)
const filterString = useAtomValue(filterStringAtom)
const filterNetwork = useAtomValue(filterNetworkAtom)
const L2Icon = getChainInfo(filterNetwork).circleLogoUrl
const tokenDetails = tokenData?.markets?.[0]
const { volume, pricePercentChange } = getDurationDetails(tokenData, timePeriod)
const arrow = pricePercentChange ? getDeltaArrow(pricePercentChange) : null
const formattedDelta = pricePercentChange ? formatDelta(pricePercentChange) : null
const timePeriod = useAtomValue(filterTimeAtom)
const delta = token?.market?.pricePercentChange?.value
const arrow = delta ? getDeltaArrow(delta) : null
const formattedDelta = delta ? formatDelta(delta) : null
const sortAscending = useAtomValue(sortAscendingAtom)
const exploreTokenSelectedEventProperties = {
chain_id: filterNetwork,
@@ -491,66 +503,84 @@ export default function LoadedRow({
// TODO: currency logo sizing mobile (32px) vs. desktop (24px)
return (
<StyledLink
to={`/tokens/${tokenAddress}`}
onClick={() => sendAnalyticsEvent(EventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)}
>
<TokenRow
header={false}
favorited={
<ClickFavorited
onClick={(e) => {
e.preventDefault()
toggleFavorite()
}}
>
<FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited>
}
listNumber={tokenListIndex + 1}
tokenInfo={
<ClickableName>
<LogoContainer>
<CurrencyLogo currency={currency} symbol={tokenSymbol} />
<L2NetworkLogo networkUrl={L2Icon} />
</LogoContainer>
<TokenInfoCell>
<TokenName>{tokenName}</TokenName>
<TokenSymbol>{tokenSymbol}</TokenSymbol>
</TokenInfoCell>
</ClickableName>
}
price={
<ClickableContent>
<PriceInfoCell>
{tokenDetails?.price?.value ? formatDollarAmount(tokenDetails?.price?.value) : '-'}
<PercentChangeInfoCell>
{formattedDelta}
{arrow}
</PercentChangeInfoCell>
</PriceInfoCell>
</ClickableContent>
}
percentChange={
<ClickableContent>
{formattedDelta}
{arrow}
</ClickableContent>
}
marketCap={
<ClickableContent>
{tokenDetails?.marketCap?.value ? formatDollarAmount(tokenDetails?.marketCap?.value) : '-'}
</ClickableContent>
}
volume={<ClickableContent>{volume ? formatDollarAmount(volume ?? undefined) : '-'}</ClickableContent>}
sparkLine={
<SparkLine>
<ParentSize>{({ width, height }) => <SparklineChart width={width} height={height} />}</ParentSize>
</SparkLine>
}
first={tokenListIndex === 0}
last={tokenListIndex === tokenListLength - 1}
/>
</StyledLink>
<div ref={ref}>
<StyledLink
to={`/tokens/${tokenAddress}`}
onClick={() => sendAnalyticsEvent(EventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)}
>
<TokenRow
header={false}
favorited={
<ClickFavorited
onClick={(e) => {
e.preventDefault()
toggleFavorite()
}}
>
<FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited>
}
listNumber={sortAscending ? MAX_TOKENS_TO_LOAD - tokenListIndex : tokenListIndex + 1}
tokenInfo={
<ClickableName>
<LogoContainer>
<CurrencyLogo currency={currency} symbol={tokenSymbol} />
<L2NetworkLogo networkUrl={L2Icon} />
</LogoContainer>
<TokenInfoCell>
<TokenName>{tokenName}</TokenName>
<TokenSymbol>{tokenSymbol}</TokenSymbol>
</TokenInfoCell>
</ClickableName>
}
price={
<ClickableContent>
<PriceInfoCell>
{token?.market?.price?.value ? formatDollarAmount(token.market.price.value) : '-'}
<PercentChangeInfoCell>
{formattedDelta}
{arrow}
</PercentChangeInfoCell>
</PriceInfoCell>
</ClickableContent>
}
percentChange={
<ClickableContent>
{formattedDelta ?? '-'}
{arrow}
</ClickableContent>
}
marketCap={
<ClickableContent>
{token?.market?.totalValueLocked?.value ? formatDollarAmount(token.market.totalValueLocked.value) : '-'}
</ClickableContent>
}
volume={
<ClickableContent>
{token?.market?.volume?.value ? formatDollarAmount(token.market.volume.value) : '-'}
</ClickableContent>
}
sparkLine={
<SparkLine>
<ParentSize>
{({ width, height }) => (
<SparklineChart
width={width}
height={height}
tokenData={token}
pricePercentChange={token?.market?.pricePercentChange?.value}
timePeriod={timePeriod}
/>
)}
</ParentSize>
</SparkLine>
}
first={tokenListIndex === 0}
last={tokenListIndex === tokenListLength - 1}
/>
</StyledLink>
</div>
)
}
})
LoadedRow.displayName = 'LoadedRow'

View File

@@ -1,23 +1,16 @@
import { Trans } from '@lingui/macro'
import {
favoritesAtom,
filterStringAtom,
filterTimeAtom,
showFavoritesAtom,
sortCategoryAtom,
sortDirectionAtom,
} from 'components/Tokens/state'
import { TokenTopQuery$data } from 'graphql/data/__generated__/TokenTopQuery.graphql'
import { getDurationDetails, SingleTokenData, useTopTokenQuery } from 'graphql/data/Token'
import { TimePeriod } from 'graphql/data/Token'
import { showFavoritesAtom } from 'components/Tokens/state'
import { usePrefetchTopTokens, useTopTokens } from 'graphql/data/TopTokens'
import { useAtomValue } from 'jotai/utils'
import { ReactNode, Suspense, useCallback, useMemo } from 'react'
import { ReactNode, useCallback, useRef } 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'
import { HeaderRow, LoadedRow, LoadingRow, MAX_TOKENS_TO_LOAD } from './TokenRow'
const LOADING_ROWS_COUNT = 3
const ROWS_PER_PAGE_FETCH = 20
const GridContainer = styled.div`
display: flex;
@@ -33,6 +26,12 @@ const GridContainer = styled.div`
align-items: center;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
`
const TokenDataContainer = styled.div`
height: 100%;
width: 100%;
`
const NoTokenDisplay = styled.div`
display: flex;
justify-content: center;
@@ -45,96 +44,6 @@ const NoTokenDisplay = styled.div`
padding: 0px 28px;
gap: 8px;
`
const TokenRowsContainer = styled.div`
width: 100%;
`
function useFilteredTokens(data: TokenTopQuery$data): SingleTokenData[] | undefined {
const filterString = useAtomValue(filterStringAtom)
const favorites = useAtomValue(favoritesAtom)
const showFavorites = useAtomValue(showFavoritesAtom)
return useMemo(
() =>
data.topTokenProjects
?.filter(
(token) => !showFavorites || (token?.tokens?.[0].address && favorites.includes(token?.tokens?.[0].address))
)
.filter((token) => {
const tokenInfo = token?.tokens?.[0]
const address = tokenInfo?.address
if (!address) {
return false
} else if (!filterString) {
return true
} else {
const lowercaseFilterString = filterString.toLowerCase()
const addressIncludesFilterString = address?.toLowerCase().includes(lowercaseFilterString)
const nameIncludesFilterString = token?.name?.toLowerCase().includes(lowercaseFilterString)
const symbolIncludesFilterString = tokenInfo?.symbol?.toLowerCase().includes(lowercaseFilterString)
return nameIncludesFilterString || symbolIncludesFilterString || addressIncludesFilterString
}
}),
[data.topTokenProjects, favorites, filterString, showFavorites]
)
}
function useSortedTokens(tokenData: SingleTokenData[] | undefined) {
const sortCategory = useAtomValue(sortCategoryAtom)
const sortDirection = useAtomValue(sortDirectionAtom)
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
const sortFn = useCallback(
(a: any, b: any) => {
if (a > b) {
return sortDirection === SortDirection.decreasing ? -1 : 1
} else if (a < b) {
return sortDirection === SortDirection.decreasing ? 1 : -1
}
return 0
},
[sortDirection]
)
return useMemo(
() =>
tokenData &&
tokenData.sort((token1, token2) => {
if (!tokenData) {
return 0
}
// fix delta/percent change property
if (!token1 || !token2 || !sortDirection || !sortCategory) {
return 0
}
let a: number | null | undefined
let b: number | null | undefined
const { volume: aVolume, pricePercentChange: aChange } = getDurationDetails(token1, timePeriod)
const { volume: bVolume, pricePercentChange: bChange } = getDurationDetails(token2, timePeriod)
switch (sortCategory) {
case Category.marketCap:
a = token1.markets?.[0]?.marketCap?.value
b = token2.markets?.[0]?.marketCap?.value
break
case Category.price:
a = token1.markets?.[0]?.price?.value
b = token2.markets?.[0]?.price?.value
break
case Category.volume:
a = aVolume
b = bVolume
break
case Category.percentChange:
a = aChange
b = bChange
break
}
return sortFn(a, b)
}),
[tokenData, sortDirection, sortCategory, sortFn, timePeriod]
)
}
function NoTokensState({ message }: { message: ReactNode }) {
return (
@@ -145,64 +54,82 @@ function NoTokensState({ message }: { message: ReactNode }) {
)
}
const LOADING_ROWS = Array.from({ length: 100 })
.fill(0)
.map((_item, index) => <LoadingRow key={index} />)
const LoadingMoreRows = Array(LOADING_ROWS_COUNT).fill(<LoadingRow />)
const InitialLoadingRows = Array(ROWS_PER_PAGE_FETCH).fill(<LoadingRow />)
export function LoadingTokenTable() {
return (
<GridContainer>
<HeaderRow />
<TokenRowsContainer>{LOADING_ROWS}</TokenRowsContainer>
<TokenDataContainer>{InitialLoadingRows}</TokenDataContainer>
</GridContainer>
)
}
export default function TokenTable() {
const showFavorites = useAtomValue<boolean>(showFavoritesAtom)
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
const topTokens = useTopTokenQuery(1, timePeriod)
const filteredTokens = useFilteredTokens(topTokens)
const sortedFilteredTokens = useSortedTokens(filteredTokens)
// TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
const prefetchedTokens = usePrefetchTopTokens()
const { loading, tokens, loadMoreTokens } = useTopTokens(prefetchedTokens)
const hasMore = !tokens || tokens.length < MAX_TOKENS_TO_LOAD
const observer = useRef<IntersectionObserver>()
const lastTokenRef = useCallback(
(node: HTMLDivElement) => {
if (loading) return
if (observer.current) observer.current.disconnect()
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
loadMoreTokens()
}
})
if (node) observer.current.observe(node)
},
[loading, hasMore, loadMoreTokens]
)
/* loading and error state */
if (topTokens === null) {
return (
<NoTokensState
message={
<>
<AlertTriangle size={16} />
<Trans>An error occured loading tokens. Please try again.</Trans>
</>
}
/>
)
if (loading && (!tokens || tokens?.length === 0)) {
return <LoadingTokenTable />
} else {
if (!tokens) {
return (
<NoTokensState
message={
<>
<AlertTriangle size={16} />
<Trans>An error occured loading tokens. Please try again.</Trans>
</>
}
/>
)
} else if (tokens?.length === 0) {
return showFavorites ? (
<NoTokensState message={<Trans>You have no favorited tokens</Trans>} />
) : (
<NoTokensState message={<Trans>No tokens found</Trans>} />
)
} else {
return (
<>
<GridContainer>
<HeaderRow />
<TokenDataContainer>
{tokens.map((token, index) => (
<LoadedRow
key={token?.name}
tokenListIndex={index}
tokenListLength={tokens?.length ?? 0}
token={token}
ref={tokens.length === index + 1 ? lastTokenRef : undefined}
/>
))}
{loading && LoadingMoreRows}
</TokenDataContainer>
</GridContainer>
</>
)
}
}
if (showFavorites && sortedFilteredTokens?.length === 0) {
return <NoTokensState message={<Trans>You have no favorited tokens</Trans>} />
}
if (!showFavorites && sortedFilteredTokens?.length === 0) {
return <NoTokensState message={<Trans>No tokens found</Trans>} />
}
return (
<Suspense fallback={<LoadingTokenTable />}>
<GridContainer>
<HeaderRow />
<TokenRowsContainer>
{sortedFilteredTokens?.map((token, index) => (
<LoadedRow
key={token?.name}
tokenListIndex={index}
tokenListLength={sortedFilteredTokens.length}
tokenData={token}
timePeriod={timePeriod}
/>
))}
</TokenRowsContainer>
</GridContainer>
</Suspense>
)
}

View File

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

View File

@@ -1,18 +1,17 @@
import { SupportedChainId } from 'constants/chains'
import { TimePeriod } from 'graphql/data/Token'
import { TokenSortMethod } from 'graphql/data/TopTokens'
import { TimePeriod } from 'graphql/data/util'
import { atom, useAtom } from 'jotai'
import { atomWithReset, atomWithStorage, useAtomValue } from 'jotai/utils'
import { useCallback, useMemo } 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)
export const sortMethodAtom = atom<TokenSortMethod>(TokenSortMethod.TOTAL_VALUE_LOCKED)
export const sortAscendingAtom = atom<boolean>(false)
/* for favoriting tokens */
export function useToggleFavorite(tokenAddress: string | undefined | null) {
@@ -33,20 +32,18 @@ export function useToggleFavorite(tokenAddress: string | undefined | null) {
}
/* keep track of sort category for token table */
export function useSetSortCategory(category: Category) {
const [sortCategory, setSortCategory] = useAtom(sortCategoryAtom)
const [sortDirection, setDirectionCategory] = useAtom(sortDirectionAtom)
export function useSetSortMethod(newSortMethod: TokenSortMethod) {
const [sortMethod, setSortMethod] = useAtom(sortMethodAtom)
const [sortAscending, setSortAscending] = useAtom(sortAscendingAtom)
return useCallback(() => {
if (category === sortCategory) {
const oppositeDirection =
sortDirection === SortDirection.increasing ? SortDirection.decreasing : SortDirection.increasing
setDirectionCategory(oppositeDirection)
if (sortMethod === newSortMethod) {
setSortAscending(!sortAscending)
} else {
setSortCategory(category)
setDirectionCategory(SortDirection.decreasing)
setSortMethod(newSortMethod)
setSortAscending(false)
}
}, [category, sortCategory, setSortCategory, sortDirection, setDirectionCategory])
}, [sortMethod, setSortMethod, setSortAscending, sortAscending, newSortMethod])
}
export function useIsFavorited(tokenAddress: string | null | undefined) {

View File

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

View File

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

View File

@@ -55,7 +55,7 @@ const ToggleMenuItem = styled.button`
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms all ${timing.in}`};
}) => `${duration.fast} all ${timing.in}`};
}
`

View File

@@ -28,7 +28,7 @@ const IconStyles = css`
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms background-color ${timing.in}`};
}) => `${duration.fast} background-color ${timing.in}`};
${IconHoverText} {
opacity: 1;

View File

@@ -33,7 +33,7 @@ const InternalLinkMenuItem = styled(InternalMenuItem)`
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms background-color ${timing.in}`};
}) => `${duration.fast} background-color ${timing.in}`};
}
`

View File

@@ -42,12 +42,12 @@ const ClearAll = styled.div`
margin-bottom: auto;
:hover {
opacity: 0.6;
opacity: ${({ theme }) => theme.opacity.hover};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms opacity ${timing.in}`};
}) => `${duration.fast} opacity ${timing.in}`};
}
`
@@ -60,7 +60,7 @@ const StyledChevron = styled(ChevronLeft)`
theme: {
transition: { duration, timing },
},
}) => `${duration.fast}ms color ${timing.in}`};
}) => `${duration.fast} color ${timing.in}`};
}
`

View File

@@ -1,6 +1,6 @@
import { useState } from 'react'
import styled from 'styled-components/macro'
import { Z_INDEX } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import { useModalIsOpen } from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer'

View File

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

View File

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

View File

@@ -33,12 +33,12 @@ function Tracer() {
if (shouldTrace) {
provider?.on('debug', trace)
if (provider !== networkProvider) {
networkProvider.on('debug', trace)
networkProvider?.on('debug', trace)
}
}
return () => {
provider?.off('debug', trace)
networkProvider.off('debug', trace)
networkProvider?.off('debug', trace)
}
}, [networkProvider, provider, shouldTrace])

View File

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

View File

@@ -1,6 +1,6 @@
import { Currency, OnReviewSwapClick, SwapWidget } from '@uniswap/widgets'
import { useWeb3React } from '@web3-react/core'
import { RPC_URLS } from 'constants/networks'
import { RPC_PROVIDERS } from 'constants/providers'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { useMemo } from 'react'
import { useIsDarkMode } from 'state/user/hooks'
@@ -34,7 +34,7 @@ export default function Widget({ defaultToken, onReviewSwapClick }: WidgetProps)
<SwapWidget
disableBranding
hideConnectionUI
jsonRpcUrlMap={RPC_URLS}
jsonRpcUrlMap={RPC_PROVIDERS}
routerUrl={WIDGET_ROUTER_URL}
width={WIDGET_WIDTH}
locale={locale}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,49 @@
import { deepCopy } from '@ethersproject/properties'
// This is the only file which should instantiate new Providers.
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { StaticJsonRpcProvider } from '@ethersproject/providers'
import { isPlain } from '@reduxjs/toolkit'
import { SupportedChainId } from './chains'
import { RPC_URLS } from './networks'
class AppJsonRpcProvider extends StaticJsonRpcProvider {
private _blockCache = new Map<string, Promise<any>>()
get blockCache() {
// If the blockCache has not yet been initialized this block, do so by
// setting a listener to clear it on the next block.
if (!this._blockCache.size) {
this.once('block', () => this._blockCache.clear())
}
return this._blockCache
}
constructor(urls: string[]) {
super(urls[0])
}
send(method: string, params: Array<any>): Promise<any> {
// Only cache eth_call's.
if (method !== 'eth_call') return super.send(method, params)
// Only cache if params are serializable.
if (!isPlain(params)) return super.send(method, params)
const key = `call:${JSON.stringify(params)}`
const cached = this.blockCache.get(key)
if (cached) {
this.emit('debug', {
action: 'request',
request: deepCopy({ method, params, id: 'cache' }),
provider: this,
})
return cached
}
const result = super.send(method, params)
this.blockCache.set(key, result)
return result
}
}
/**

View File

@@ -0,0 +1,7 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useFavoriteTokensFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.favoriteTokens)
}
export { BaseVariant as FavoriteTokensVariant }

View File

@@ -1,9 +1,9 @@
export enum FeatureFlag {
favoriteTokens = 'favoriteTokens',
navBar = 'navBar',
nft = 'nfts',
redesign = 'redesign',
tokens = 'tokens',
tokensNetworkFilter = 'tokensNetworkFilter',
tokenSafety = 'tokenSafety',
traceJsonRpc = 'traceJsonRpc',
}

View File

@@ -1,7 +0,0 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useTokensNetworkFilterFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.tokensNetworkFilter)
}
export { BaseVariant as TokensNetworkFilterVariant }

View File

@@ -31,7 +31,9 @@ const fetchQuery = async function wrappedFetchQuery(params: RequestParameters, v
// and reusing cached data if its available/fresh.
const gcReleaseBufferSize = 10
const store = new Store(new RecordSource(), { gcReleaseBufferSize })
const queryCacheExpirationTime = ms`1m`
const store = new Store(new RecordSource(), { gcReleaseBufferSize, queryCacheExpirationTime })
const network = Network.create(fetchQuery)
// Export a singleton instance of Relay Environment configured with our network function:

View File

@@ -1,110 +1,20 @@
import graphql from 'babel-plugin-relay/macro'
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { fetchQuery, useFragment, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
import { TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql'
import { Chain, TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql'
import { TokenPrices$data, TokenPrices$key } from './__generated__/TokenPrices.graphql'
import { Chain, HistoryDuration, TokenQuery, TokenQuery$data } from './__generated__/TokenQuery.graphql'
import { TokenTopQuery, TokenTopQuery$data } from './__generated__/TokenTopQuery.graphql'
export enum TimePeriod {
HOUR,
DAY,
WEEK,
MONTH,
YEAR,
ALL,
}
function toHistoryDuration(timePeriod: TimePeriod): HistoryDuration {
switch (timePeriod) {
case TimePeriod.HOUR:
return 'HOUR'
case TimePeriod.DAY:
return 'DAY'
case TimePeriod.WEEK:
return 'WEEK'
case TimePeriod.MONTH:
return 'MONTH'
case TimePeriod.YEAR:
return 'YEAR'
case TimePeriod.ALL:
return 'MAX'
}
}
import { TokenQuery, TokenQuery$data } from './__generated__/TokenQuery.graphql'
import { TimePeriod, toHistoryDuration } from './util'
export type PricePoint = { value: number; timestamp: number }
const topTokensQuery = graphql`
query TokenTopQuery($page: Int!, $duration: HistoryDuration!) {
topTokenProjects(orderBy: MARKET_CAP, pageSize: 20, currency: USD, page: $page) {
description
homepageUrl
twitterName
name
tokens {
chain
address
symbol
}
prices: markets(currencies: [USD]) {
...TokenPrices
}
markets(currencies: [USD]) {
price {
value
currency
}
marketCap {
value
currency
}
fullyDilutedMarketCap {
value
currency
}
volume1D: volume(duration: DAY) {
value
currency
}
volume1W: volume(duration: WEEK) {
value
currency
}
volume1M: volume(duration: MONTH) {
value
currency
}
volume1Y: volume(duration: YEAR) {
value
currency
}
pricePercentChange24h {
currency
value
}
pricePercentChange1W: pricePercentChange(duration: WEEK) {
currency
value
}
pricePercentChange1M: pricePercentChange(duration: MONTH) {
currency
value
}
pricePercentChange1Y: pricePercentChange(duration: YEAR) {
currency
value
}
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
value
currency
}
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
value
currency
}
}
}
export const projectMetaDataFragment = graphql`
fragment Token_TokenProject_Metadata on TokenProject {
description
homepageUrl
twitterName
name
}
`
const tokenPricesFragment = graphql`
@@ -115,23 +25,14 @@ const tokenPricesFragment = graphql`
}
}
`
type CachedTopToken = NonNullable<NonNullable<TokenTopQuery$data>['topTokenProjects']>[number]
let cachedTopTokens: Record<string, CachedTopToken> = {}
export function useTopTokenQuery(page: number, timePeriod: TimePeriod) {
const topTokens = useLazyLoadQuery<TokenTopQuery>(topTokensQuery, { page, duration: toHistoryDuration(timePeriod) })
cachedTopTokens =
topTokens.topTokenProjects?.reduce((acc, current) => {
const address = current?.tokens?.[0].address
if (address) acc[address] = current
return acc
}, {} as Record<string, CachedTopToken>) ?? {}
console.log(cachedTopTokens)
return topTokens
}
/*
The difference between Token and TokenProject:
Token: an on-chain entity referring to a contract (e.g. uni token on ethereum 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984)
TokenProject: an off-chain, aggregated entity that consists of a token and its bridged tokens (e.g. uni token on all chains)
TokenMarket and TokenProjectMarket then are market data entities for the above.
TokenMarket is per-chain market data for contracts pulled from the graph.
TokenProjectMarket is aggregated market data (aggregated over multiple dexes and centralized exchanges) that we get from coingecko.
*/
const tokenQuery = graphql`
query TokenQuery($contract: ContractInput!, $duration: HistoryDuration!, $skip: Boolean = false) {
tokenProjects(contracts: [$contract]) @skip(if: $skip) {
@@ -143,6 +44,12 @@ const tokenQuery = graphql`
chain
address
symbol
market {
totalValueLocked {
value
currency
}
}
}
prices: markets(currencies: [USD]) {
...TokenPrices
@@ -206,14 +113,13 @@ const tokenQuery = graphql`
`
export function useTokenQuery(address: string, chain: Chain, timePeriod: TimePeriod) {
const cachedTopToken = cachedTopTokens[address]
const data = useLazyLoadQuery<TokenQuery>(tokenQuery, {
contract: { address, chain },
duration: toHistoryDuration(timePeriod),
skip: !!cachedTopToken,
skip: false,
})
return !cachedTopToken ? data : { tokenProjects: [{ ...cachedTopToken }] }
return data
}
const tokenPriceQuery = graphql`
@@ -257,27 +163,35 @@ const tokenPriceQuery = graphql`
}
`
function filterPrices(prices: TokenPrices$data['priceHistory'] | undefined) {
export function filterPrices(prices: TokenPrices$data['priceHistory'] | undefined) {
return prices?.filter((p): p is PricePoint => Boolean(p && p.value))
}
export function useTokenPricesFromFragment(key: TokenPrices$key | null | undefined) {
const fetchedTokenPrices = useFragment(tokenPricesFragment, key ?? null)?.priceHistory
return filterPrices(fetchedTokenPrices)
}
export function useTokenPricesCached(
key: TokenPrices$key | null | undefined,
priceDataFragmentRef: TokenPrices$key | null | undefined,
address: string,
chain: Chain,
timePeriod: TimePeriod
) {
// Attempt to use token prices already provided by TokenDetails / TopToken queries
const environment = useRelayEnvironment()
const fetchedTokenPrices = useFragment(tokenPricesFragment, key ?? null)?.priceHistory
const fetchedTokenPrices = useFragment(tokenPricesFragment, priceDataFragmentRef ?? null)?.priceHistory
const [priceMap, setPriceMap] = useState(
new Map<TimePeriod, PricePoint[] | undefined>([[timePeriod, filterPrices(fetchedTokenPrices)]])
const [priceMap, setPriceMap] = useState<Map<TimePeriod, PricePoint[] | undefined>>(
new Map([[timePeriod, filterPrices(fetchedTokenPrices)]])
)
function updatePrices(key: TimePeriod, data?: PricePoint[]) {
setPriceMap(new Map(priceMap.set(key, data)))
}
const updatePrices = useCallback(
(key: TimePeriod, data?: PricePoint[]) => {
setPriceMap(new Map(priceMap.set(key, data)))
},
[priceMap]
)
// Fetch the other timePeriods after first render
useEffect(() => {
@@ -310,38 +224,3 @@ export function useTokenPricesCached(
}
export type SingleTokenData = NonNullable<TokenQuery$data['tokenProjects']>[number]
export function getDurationDetails(data: SingleTokenData, timePeriod: TimePeriod) {
let volume = null
let pricePercentChange = null
const markets = data?.markets?.[0]
if (markets) {
switch (timePeriod) {
case TimePeriod.HOUR:
pricePercentChange = null
break
case TimePeriod.DAY:
volume = markets.volume1D?.value
pricePercentChange = markets.pricePercentChange24h?.value
break
case TimePeriod.WEEK:
volume = markets.volume1W?.value
pricePercentChange = markets.pricePercentChange1W?.value
break
case TimePeriod.MONTH:
volume = markets.volume1M?.value
pricePercentChange = markets.pricePercentChange1M?.value
break
case TimePeriod.YEAR:
volume = markets.volume1Y?.value
pricePercentChange = markets.pricePercentChange1Y?.value
break
case TimePeriod.ALL:
volume = null
pricePercentChange = null
break
}
}
return { volume, pricePercentChange }
}

View File

@@ -0,0 +1,223 @@
import graphql from 'babel-plugin-relay/macro'
import {
favoritesAtom,
filterStringAtom,
filterTimeAtom,
showFavoritesAtom,
sortAscendingAtom,
sortMethodAtom,
} from 'components/Tokens/state'
import { useAtomValue } from 'jotai/utils'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { fetchQuery, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
import { ContractInput, TopTokens_TokensQuery } from './__generated__/TopTokens_TokensQuery.graphql'
import type { TopTokens100Query } from './__generated__/TopTokens100Query.graphql'
import { toHistoryDuration, useCurrentChainName } from './util'
export function usePrefetchTopTokens() {
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
const chain = useCurrentChainName()
const args = useMemo(() => ({ chain, duration }), [chain, duration])
return useLazyLoadQuery<TopTokens100Query>(topTokens100Query, args)
}
const topTokens100Query = graphql`
query TopTokens100Query($duration: HistoryDuration!, $chain: Chain!) {
topTokens(pageSize: 100, page: 1, chain: $chain) {
id
name
chain
address
symbol
market(currency: USD) {
totalValueLocked {
value
currency
}
price {
value
currency
}
pricePercentChange(duration: $duration) {
currency
value
}
volume(duration: $duration) {
value
currency
}
}
}
}
`
export enum TokenSortMethod {
PRICE = 'Price',
PERCENT_CHANGE = 'Change',
TOTAL_VALUE_LOCKED = 'TVL',
VOLUME = 'Volume',
}
export type PrefetchedTopToken = NonNullable<TopTokens100Query['response']['topTokens']>[number]
function useSortedTokens(tokens: TopTokens100Query['response']['topTokens']) {
const sortMethod = useAtomValue(sortMethodAtom)
const sortAscending = useAtomValue(sortAscendingAtom)
return useMemo(() => {
if (!tokens) return []
let tokenArray = Array.from(tokens)
switch (sortMethod) {
case TokenSortMethod.PRICE:
tokenArray = tokenArray.sort((a, b) => (b?.market?.price?.value ?? 0) - (a?.market?.price?.value ?? 0))
break
case TokenSortMethod.PERCENT_CHANGE:
tokenArray = tokenArray.sort(
(a, b) => (b?.market?.pricePercentChange?.value ?? 0) - (a?.market?.pricePercentChange?.value ?? 0)
)
break
case TokenSortMethod.TOTAL_VALUE_LOCKED:
tokenArray = tokenArray.sort(
(a, b) => (b?.market?.totalValueLocked?.value ?? 0) - (a?.market?.totalValueLocked?.value ?? 0)
)
break
case TokenSortMethod.VOLUME:
tokenArray = tokenArray.sort((a, b) => (b?.market?.volume?.value ?? 0) - (a?.market?.volume?.value ?? 0))
break
}
return sortAscending ? tokenArray.reverse() : tokenArray
}, [tokens, sortMethod, sortAscending])
}
function useFilteredTokens(tokens: PrefetchedTopToken[]) {
const filterString = useAtomValue(filterStringAtom)
const favorites = useAtomValue(favoritesAtom)
const showFavorites = useAtomValue(showFavoritesAtom)
const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString])
return useMemo(() => {
if (!tokens) {
return []
}
let returnTokens = tokens
if (showFavorites) {
returnTokens = returnTokens?.filter((token) => token?.address && favorites.includes(token.address))
}
if (lowercaseFilterString) {
returnTokens = returnTokens?.filter((token) => {
const addressIncludesFilterString = token?.address?.toLowerCase().includes(lowercaseFilterString)
const nameIncludesFilterString = token?.name?.toLowerCase().includes(lowercaseFilterString)
const symbolIncludesFilterString = token?.symbol?.toLowerCase().includes(lowercaseFilterString)
return nameIncludesFilterString || symbolIncludesFilterString || addressIncludesFilterString
})
}
return returnTokens
}, [tokens, showFavorites, lowercaseFilterString, favorites])
}
const PAGE_SIZE = 20
function toContractInput(token: PrefetchedTopToken) {
return {
address: token?.address ?? '',
chain: token?.chain ?? 'ETHEREUM',
}
}
export type TopToken = NonNullable<TopTokens_TokensQuery['response']['tokens']>[number]
interface UseTopTokensReturnValue {
loading: boolean
tokens: TopToken[]
loadMoreTokens: () => void
}
export function useTopTokens(prefetchedData: TopTokens100Query['response']): UseTopTokensReturnValue {
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
const environment = useRelayEnvironment()
const [tokens, setTokens] = useState<TopToken[]>([])
const [page, setPage] = useState(0)
const [loading, setLoading] = useState(true)
const appendTokens = useCallback(
(newTokens: TopToken[]) => {
setTokens(
Object.values(
tokens
.concat(newTokens)
.reduce((acc, token) => (token?.address ? { ...acc, [token.address]: token } : acc), {})
)
)
},
[tokens]
)
const loadMoreTokens = useCallback(() => setPage(page + 1), [page])
// TopTokens should ideally be fetched with usePaginationFragment. The backend does not current support graphql cursors;
// in the meantime, fetchQuery is used, as other relay hooks do not allow the refreshing and lazy loading we need
const prefetchedSelectedTokens = useFilteredTokens(useSortedTokens(prefetchedData.topTokens))
const contracts: ContractInput[] = useMemo(
() => prefetchedSelectedTokens.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE).map(toContractInput),
[page, prefetchedSelectedTokens]
)
useEffect(() => {
const subscription = fetchQuery<TopTokens_TokensQuery>(
environment,
tokensQuery,
{ contracts, duration },
{ fetchPolicy: 'store-or-network' }
).subscribe({
start() {
setLoading(true)
},
complete() {
setLoading(false)
},
next(data) {
appendTokens(data.tokens as TopToken[])
},
})
return subscription.unsubscribe
}, [appendTokens, contracts, duration, environment])
return { loading, tokens: useFilteredTokens(useSortedTokens(tokens)) as TopToken[], loadMoreTokens }
}
export const tokensQuery = graphql`
query TopTokens_TokensQuery($contracts: [ContractInput!]!, $duration: HistoryDuration!) {
tokens(contracts: $contracts) {
id
name
chain
address
symbol
market(currency: USD) {
totalValueLocked {
value
currency
}
priceHistory(duration: $duration) {
timestamp
value
}
price {
value
currency
}
volume(duration: $duration) {
value
currency
}
pricePercentChange(duration: $duration) {
currency
value
}
}
}
}
`

View File

@@ -1,6 +1,35 @@
"""This directive allows results to be deferred during execution"""
directive @defer on FIELD
"""
Tells the service this field/object has access authorized by sigv4 signing.
"""
directive @aws_iam on OBJECT | FIELD_DEFINITION
"""
Tells the service this field/object has access authorized by an API key.
"""
directive @aws_api_key on OBJECT | FIELD_DEFINITION
"""
Tells the service this field/object has access authorized by a Lambda Authorizer.
"""
directive @aws_lambda on OBJECT | FIELD_DEFINITION
"""Directs the schema to enforce authorization on a field"""
directive @aws_auth(
"""List of cognito user pool groups which have access on this field"""
cognito_groups: [String]
) on FIELD_DEFINITION
"""Tells the service which mutation triggers this subscription."""
directive @aws_subscribe(
"""
List of mutations which will trigger this subscription when they are called.
"""
mutations: [String]
) on FIELD_DEFINITION
"""
Tells the service which subscriptions will be published to when this mutation is
called. This directive is deprecated use @aws_susbscribe directive instead.
@@ -12,34 +41,6 @@ directive @aws_publish(
subscriptions: [String]
) on FIELD_DEFINITION
"""
Tells the service this field/object has access authorized by sigv4 signing.
"""
directive @aws_iam on OBJECT | FIELD_DEFINITION
"""
Tells the service this field/object has access authorized by a Lambda Authorizer.
"""
directive @aws_lambda on OBJECT | FIELD_DEFINITION
"""
Tells the service this field/object has access authorized by an API key.
"""
directive @aws_api_key on OBJECT | FIELD_DEFINITION
"""
Tells the service this field/object has access authorized by an OIDC token.
"""
directive @aws_oidc on OBJECT | FIELD_DEFINITION
"""Tells the service which mutation triggers this subscription."""
directive @aws_subscribe(
"""
List of mutations which will trigger this subscription when they are called.
"""
mutations: [String]
) on FIELD_DEFINITION
"""
Tells the service this field/object has access authorized by a Cognito User Pools token.
"""
@@ -48,11 +49,10 @@ directive @aws_cognito_user_pools(
cognito_groups: [String]
) on OBJECT | FIELD_DEFINITION
"""Directs the schema to enforce authorization on a field"""
directive @aws_auth(
"""List of cognito user pool groups which have access on this field"""
cognito_groups: [String]
) on FIELD_DEFINITION
"""
Tells the service this field/object has access authorized by an OIDC token.
"""
directive @aws_oidc on OBJECT | FIELD_DEFINITION
enum ActivityType {
APPROVE
@@ -100,6 +100,7 @@ enum Chain {
ETHEREUM_GOERLI
OPTIMISM
POLYGON
CELO
}
input ContractInput {
@@ -112,6 +113,12 @@ enum Currency {
ETH
}
type Dimensions {
id: ID!
height: Float
width: Float
}
enum HighLow {
HIGH
LOW
@@ -136,6 +143,12 @@ interface IContract {
address: String
}
type Image {
id: ID!
url: String
dimensions: Dimensions
}
enum MarketSortableField {
MARKET_CAP
VOLUME
@@ -168,6 +181,9 @@ type NftAsset {
thumbnailUrl: String
animationUrl: String
smallImageUrl: String
image: Image
thumbnail: Image
smallImage: Image
name: String
nftContract: NftContract
@@ -204,10 +220,12 @@ type NftCollection {
assets(page: Int, pageSize: Int, orderBy: NftAssetSortableField): [NftAsset]
"""
bannerImageUrl: String
bannerImage: Image
description: String
discordUrl: String
homepageUrl: String
imageUrl: String
image: Image
instagramName: String
markets(currencies: [Currency!]!): [NftCollectionMarket]
name: String
@@ -285,12 +303,14 @@ type Portfolio {
type Query {
tokens(contracts: [ContractInput!]!): [Token]
tokenProjects(contracts: [ContractInput!]!): [TokenProject]
topTokenProjects(orderBy: MarketSortableField, page: Int, pageSize: Int, currency: Currency): [TokenProject]
topTokenProjects(orderBy: MarketSortableField!, page: Int!, pageSize: Int!, currency: Currency): [TokenProject]
searchTokens(searchQuery: String!): [Token]
searchTokenProjects(searchQuery: String!): [TokenProject]
assetActivities(address: String!, page: Int, pageSize: Int): [AssetActivity]
portfolio(ownerAddress: String!): Portfolio
portfolios(ownerAddresses: [String!]!): [Portfolio]
nftCollectionsById(collectionIds: [String]): [NftCollection]
topTokens(chain: Chain, page: Int!, pageSize: Int!): [Token]
}
type TimestampedAmount implements IAmount {
@@ -308,6 +328,8 @@ type Token implements IContract {
decimals: Int
name: String
symbol: String
project: TokenProject
market(currency: Currency): TokenMarket
}
type TokenApproval {
@@ -322,6 +344,8 @@ type TokenApproval {
type TokenBalance {
id: ID!
blockNumber: Int
blockTimestamp: Int
quantity: Float
denominatedValue: Amount
ownerAddress: String!
@@ -329,6 +353,16 @@ type TokenBalance {
tokenProjectMarket: TokenProjectMarket
}
type TokenMarket {
id: ID!
token: Token!
price: Amount
totalValueLocked: Amount
volume(duration: HistoryDuration!): Amount
pricePercentChange(duration: HistoryDuration!): Amount
priceHistory(duration: HistoryDuration!): [TimestampedAmount]
}
type TokenProject {
id: ID!
name: String

49
src/graphql/data/util.ts Normal file
View File

@@ -0,0 +1,49 @@
import { useWeb3React } from '@web3-react/core'
import { SupportedChainId } from 'constants/chains'
import { Chain, HistoryDuration } from './__generated__/TokenQuery.graphql'
export enum TimePeriod {
HOUR,
DAY,
WEEK,
MONTH,
YEAR,
ALL,
}
export function toHistoryDuration(timePeriod: TimePeriod): HistoryDuration {
switch (timePeriod) {
case TimePeriod.HOUR:
return 'HOUR'
case TimePeriod.DAY:
return 'DAY'
case TimePeriod.WEEK:
return 'WEEK'
case TimePeriod.MONTH:
return 'MONTH'
case TimePeriod.YEAR:
return 'YEAR'
case TimePeriod.ALL:
return 'MAX'
}
}
export const CHAIN_IDS_TO_BACKEND_NAME: { [key: number]: Chain } = {
[SupportedChainId.MAINNET]: 'ETHEREUM',
[SupportedChainId.GOERLI]: 'ETHEREUM_GOERLI',
[SupportedChainId.POLYGON]: 'POLYGON',
[SupportedChainId.POLYGON_MUMBAI]: 'POLYGON',
[SupportedChainId.CELO]: 'CELO',
[SupportedChainId.CELO_ALFAJORES]: 'CELO',
[SupportedChainId.ARBITRUM_ONE]: 'ARBITRUM',
[SupportedChainId.ARBITRUM_RINKEBY]: 'ARBITRUM',
[SupportedChainId.OPTIMISM]: 'OPTIMISM',
[SupportedChainId.OPTIMISTIC_KOVAN]: 'OPTIMISM',
}
export function useCurrentChainName() {
const { chainId } = useWeb3React()
return chainId && CHAIN_IDS_TO_BACKEND_NAME[chainId] ? CHAIN_IDS_TO_BACKEND_NAME[chainId] : 'ETHEREUM'
}

View File

@@ -18,6 +18,9 @@ type _Block_ {
"""The block number"""
number: Int!
"""Integer representation of the timestamp stored in blocks for the chain"""
timestamp: Int
}
"""The type for the top-level _meta field"""

View File

@@ -1,12 +1,12 @@
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { sendAnalyticsEvent } from 'components/AmplitudeAnalytics'
import { EventName } from 'components/AmplitudeAnalytics/constants'
import { formatToDecimal, getTokenAddress } from 'components/AmplitudeAnalytics/utils'
import { sendAnalyticsEvent } from 'analytics'
import { EventName } from 'analytics/constants'
import { formatToDecimal, getTokenAddress } from 'analytics/utils'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useMemo } from 'react'
import { useMemo, useState } from 'react'
import { WRAPPED_NATIVE_CURRENCY } from '../constants/tokens'
import { useCurrencyBalance } from '../state/connection/hooks'
@@ -70,6 +70,11 @@ export default function useWrapCallback(
)
const addTransaction = useTransactionAdder()
// This allows an async error to propagate within the React lifecycle.
// Without rethrowing it here, it would not show up in the UI - only the dev console.
const [error, setError] = useState<Error>()
if (error) throw error
return useMemo(() => {
if (!wethContract || !chainId || !inputCurrency || !outputCurrency) return NOT_APPLICABLE
const weth = WRAPPED_NATIVE_CURRENCY[chainId]
@@ -94,6 +99,22 @@ export default function useWrapCallback(
sufficientBalance && inputAmount
? async () => {
try {
const network = await wethContract.provider.getNetwork()
if (
network.chainId !== chainId ||
wethContract.address !== WRAPPED_NATIVE_CURRENCY[network.chainId]?.address
) {
sendAnalyticsEvent(EventName.WRAP_TOKEN_TXN_INVALIDATED, {
...eventProperties,
contract_address: wethContract.address,
contract_chain_id: network.chainId,
type: WrapType.WRAP,
})
const error = new Error(`Invalid WETH contract
Please file a bug detailing how this happened - https://github.com/Uniswap/interface/issues/new?labels=bug&template=bug-report.md&title=Invalid%20WETH%20contract`)
setError(error)
throw error
}
const txReceipt = await wethContract.deposit({ value: `0x${inputAmount.quotient.toString(16)}` })
addTransaction(txReceipt, {
type: TransactionType.WRAP,

View File

@@ -2,9 +2,9 @@ import { MaxUint256 } from '@ethersproject/constants'
import type { TransactionResponse } from '@ethersproject/providers'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { sendAnalyticsEvent } from 'components/AmplitudeAnalytics'
import { EventName } from 'components/AmplitudeAnalytics/constants'
import { getTokenAddress } from 'components/AmplitudeAnalytics/utils'
import { sendAnalyticsEvent } from 'analytics'
import { EventName } from 'analytics/constants'
import { getTokenAddress } from 'analytics/utils'
import { useTokenContract } from 'hooks/useContract'
import { useTokenAllowance } from 'hooks/useTokenAllowance'
import { useCallback, useMemo } from 'react'

View File

@@ -0,0 +1,62 @@
import { style } from '@vanilla-extract/css'
import { subhead } from 'nft/css/common.css'
import { breakpoints, sprinkles, vars } from 'nft/css/sprinkles.css'
export const bagContainer = style([
sprinkles({
position: 'fixed',
top: { sm: '0', md: '72' },
width: 'full',
height: 'full',
right: '0',
background: 'lightGray',
color: 'blackBlue',
paddingTop: '20',
paddingBottom: '24',
zIndex: { sm: 'offcanvas', md: '3' },
}),
{
'@media': {
[`(min-width: ${breakpoints.md}px)`]: {
width: '316px',
height: 'calc(100vh - 72px)',
},
},
},
])
export const assetsContainer = style([
sprinkles({
paddingX: '32',
maxHeight: 'full',
overflowY: 'scroll',
}),
{
'::-webkit-scrollbar': { display: 'none' },
scrollbarWidth: 'none',
},
])
export const header = style([
subhead,
sprinkles({
color: 'blackBlue',
justifyContent: 'space-between',
}),
{
lineHeight: '24px',
},
])
export const clearAll = style([
sprinkles({
color: 'placeholder',
cursor: 'pointer',
fontWeight: 'semibold',
}),
{
':hover': {
color: vars.color.blue400,
},
},
])

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