Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef8fba1d49 | ||
|
|
be64c03d06 | ||
|
|
45682ca59e | ||
|
|
397b9d423e | ||
|
|
d9113fb6d4 | ||
|
|
5dc0df2132 | ||
|
|
7f2cc9a3e6 | ||
|
|
d3c4ca6e09 | ||
|
|
28538214d2 | ||
|
|
4f48f3372c | ||
|
|
9e070107a2 | ||
|
|
2a92b2992f | ||
|
|
e180153c3a | ||
|
|
e35f9e16a1 | ||
|
|
8e955e9257 | ||
|
|
9ca44652b3 | ||
|
|
3d89d72426 | ||
|
|
c2ffab3273 | ||
|
|
dbb62b613c | ||
|
|
6f2d6e31c9 | ||
|
|
944939a2e9 | ||
|
|
04164a550d | ||
|
|
16a5e15070 | ||
|
|
ee97d8d902 | ||
|
|
3d4b077b89 | ||
|
|
0f4a89d938 | ||
|
|
0d0ec12dbf | ||
|
|
afe30a2c02 | ||
|
|
2f9289a2c5 | ||
|
|
7a9d2e80d0 | ||
|
|
d4b8735c04 | ||
|
|
d31687d0bf | ||
|
|
470535dd33 | ||
|
|
27f53f1e99 | ||
|
|
e7d498c95e | ||
|
|
02b617d297 | ||
|
|
96d04e1a7d | ||
|
|
fe9d805d7c | ||
|
|
b6c136839e | ||
|
|
8c947a0e0d | ||
|
|
49c5cbbf3b | ||
|
|
efaefe2e44 | ||
|
|
ed95f1b966 | ||
|
|
7ecbc552aa | ||
|
|
dadc997398 | ||
|
|
b90d6b5ab0 | ||
|
|
db5c6f82fd | ||
|
|
f161f9617b | ||
|
|
9d3249e6bd | ||
|
|
f1c65afa98 | ||
|
|
80c1f0cdf9 | ||
|
|
ea0fe83d00 | ||
|
|
994836fba7 | ||
|
|
41aa1dcb0b | ||
|
|
3965d3fdd9 | ||
|
|
ff6fd8a6e9 | ||
|
|
0a6906b23e | ||
|
|
c38b5c0ce3 | ||
|
|
86f3b5a036 | ||
|
|
382a44f040 | ||
|
|
2d9604cd14 | ||
|
|
7930709bc3 |
@@ -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
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
|
||||
/**
|
||||
BIN
src/assets/images/nft-marketplaces.png
Normal file
BIN
src/assets/images/nft-marketplaces.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
@@ -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 |
@@ -145,7 +145,7 @@ const CloseIcon = styled.div`
|
||||
top: 14px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -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>
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`};
|
||||
}
|
||||
`
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 }>`
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
23
src/components/NavBar/ChainSelector.css.ts
Normal file
23
src/components/NavBar/ChainSelector.css.ts
Normal 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',
|
||||
}),
|
||||
])
|
||||
@@ -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>
|
||||
89
src/components/NavBar/ChainSelectorRow.tsx
Normal file
89
src/components/NavBar/ChainSelectorRow.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
}),
|
||||
])
|
||||
@@ -41,6 +41,7 @@ export const SecondaryText = style([
|
||||
paddingY: '8',
|
||||
paddingX: '8',
|
||||
color: 'darkGray',
|
||||
width: 'full',
|
||||
}),
|
||||
{
|
||||
lineHeight: '20px',
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -11,7 +11,7 @@ export const navIcon = style([
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
padding: '8',
|
||||
padding: '10',
|
||||
borderRadius: '8',
|
||||
transition: '250',
|
||||
}),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
22
src/components/NavBar/ShoppingBag.css.ts
Normal file
22
src/components/NavBar/ShoppingBag.css.ts
Normal 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',
|
||||
},
|
||||
])
|
||||
47
src/components/NavBar/ShoppingBag.tsx
Normal file
47
src/components/NavBar/ShoppingBag.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import Navbar from './Navbar'
|
||||
|
||||
export default Navbar
|
||||
@@ -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 />
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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')};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ const ToggleWrap = styled.div`
|
||||
`
|
||||
|
||||
const ToggleLabel = styled.div`
|
||||
opacity: 0.6;
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
margin-right: 10px;
|
||||
`
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
103
src/components/Tokens/TokenDetails/About.tsx
Normal file
103
src/components/Tokens/TokenDetails/About.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
36
src/components/Tokens/TokenDetails/AddressSection.tsx
Normal file
36
src/components/Tokens/TokenDetails/AddressSection.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
18
src/components/Tokens/TokenDetails/BreadcrumbNavLink.tsx
Normal file
18
src/components/Tokens/TokenDetails/BreadcrumbNavLink.tsx
Normal 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};
|
||||
}
|
||||
`
|
||||
120
src/components/Tokens/TokenDetails/ChartSection.tsx
Normal file
120
src/components/Tokens/TokenDetails/ChartSection.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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`
|
||||
|
||||
68
src/components/Tokens/TokenDetails/StatsSection.tsx
Normal file
68
src/components/Tokens/TokenDetails/StatsSection.tsx
Normal 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>
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
`
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
26
src/components/Tokens/TokenTable/FilterOption.tsx
Normal file
26
src/components/Tokens/TokenTable/FilterOption.tsx
Normal 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
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
export enum Category {
|
||||
percentChange = 'Change',
|
||||
marketCap = 'Market Cap',
|
||||
price = 'Price',
|
||||
volume = 'Volume',
|
||||
}
|
||||
export enum SortDirection {
|
||||
increasing = 'Increasing',
|
||||
decreasing = 'Decreasing',
|
||||
}
|
||||
@@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ const ToggleMenuItem = styled.button`
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.fast}ms all ${timing.in}`};
|
||||
}) => `${duration.fast} all ${timing.in}`};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -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}`};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
7
src/featureFlags/flags/favoriteTokens.ts
Normal file
7
src/featureFlags/flags/favoriteTokens.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function useFavoriteTokensFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.favoriteTokens)
|
||||
}
|
||||
|
||||
export { BaseVariant as FavoriteTokensVariant }
|
||||
@@ -1,9 +1,9 @@
|
||||
export enum FeatureFlag {
|
||||
favoriteTokens = 'favoriteTokens',
|
||||
navBar = 'navBar',
|
||||
nft = 'nfts',
|
||||
redesign = 'redesign',
|
||||
tokens = 'tokens',
|
||||
tokensNetworkFilter = 'tokensNetworkFilter',
|
||||
tokenSafety = 'tokenSafety',
|
||||
traceJsonRpc = 'traceJsonRpc',
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function useTokensNetworkFilterFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.tokensNetworkFilter)
|
||||
}
|
||||
|
||||
export { BaseVariant as TokensNetworkFilterVariant }
|
||||
@@ -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:
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
223
src/graphql/data/TopTokens.ts
Normal file
223
src/graphql/data/TopTokens.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -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
49
src/graphql/data/util.ts
Normal 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'
|
||||
}
|
||||
@@ -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"""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
62
src/nft/components/bag/Bag.css.ts
Normal file
62
src/nft/components/bag/Bag.css.ts
Normal 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
Reference in New Issue
Block a user