diff --git a/.example.env b/.example.env index dcdc01db..b331e11a 100644 --- a/.example.env +++ b/.example.env @@ -2,4 +2,8 @@ REACT_APP_NETWORK_ID=421614 REACT_APP_NETWORK_URL=https://arbitrum-sepolia.blockpi.network/v1/rpc/public REACT_APP_WALLET_CONNECT_PROJECT_ID= REACT_APP_FALLBACK_SUBGRAPH_URL=https://${GRAPH_NODE_PLAYGROUND_BASE_URL}/subgraphs/name/NAME_OF_YOUR_SUBGRAPH -REACT_APP_GEOFENCE_ENABLED=false \ No newline at end of file +REACT_APP_GEOFENCE_ENABLED=false +REACT_APP_OPENSEA_KEY= +REACT_APP_RPC_URL_ETHEREUM= +## Only needed for mainnet +REACT_APP_FUUL_API_KEY= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5276e407..51d72e61 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,7 @@ yarn-error.log* src/artifacts # Yarn -install-state.gz \ No newline at end of file +install-state.gz + +# Sentry Config File +.sentryclirc diff --git a/Earthfile b/Earthfile index 49ef530b..f13e55e7 100644 --- a/Earthfile +++ b/Earthfile @@ -28,4 +28,4 @@ build-app: FROM +deps ARG ENVIRONMENT='local' ARG VERSION='latest' - RUN yarn build + RUN yarn craco build diff --git a/README.md b/README.md index e3757835..2cdeba89 100644 --- a/README.md +++ b/README.md @@ -25,26 +25,22 @@ Frontend application for Open Dollar # Deployments -http://app.opendollar.com/ Production `main` branch +https://app.opendollar.com/ Production `main` branch -http://app.dev.opendollar.com/ Testnet `dev` branch - -## Deployment Diagram of open-dollar-app on Vercel - -vercel deployment diagram of open-dollar-app +https://app.dev.opendollar.com/ Testnet `dev` branch # ⚡️ Run the app locally For security and resiliency we publish the app as a self-contained Docker image 1. Install [Docker](https://docs.docker.com/desktop/) -2. Get the latest [Release](https://github.com/open-dollar/od-app/releases), eg. `1.5.9` +2. Get the latest [Release](https://github.com/open-dollar/od-app/releases), eg. `1.7.0` 3. Run the start command, replacing `` with the release ```bash docker run -p 3000:3000 ghcr.io/open-dollar/od-app: # For example: -docker run -p 3000:3000 ghcr.io/open-dollar/od-app:1.5.9 +docker run -p 3000:3000 ghcr.io/open-dollar/od-app:1.7.0 ``` The application will be available on http://localhost:3000 @@ -98,6 +94,7 @@ there's a fallback subgraph to query. ```bash yarn test:e2e + ``` ### Jest test diff --git a/craco.config.js b/craco.config.js index d5e954bb..8765ba1d 100644 --- a/craco.config.js +++ b/craco.config.js @@ -1,4 +1,5 @@ const cracoAlias = require('craco-alias') +const { sentryWebpackPlugin } = require('@sentry/webpack-plugin') module.exports = { content: ['./src/**/*.{html,js}'], @@ -31,5 +32,15 @@ module.exports = { }, }, }, + plugins: [ + sentryWebpackPlugin({ + org: process.env.SENTRY_ORG || 'open-dollar', + project: process.env.SENTRY_PROJECT || 'open-dollar', + authToken: process.env.SENTRY_AUTH_TOKEN, + sourcemaps: { + ignore: ['node_modules'], + } + }), + ], }, } diff --git a/package.json b/package.json index 84647771..a9f60c68 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { - "name": "@usekeyp/od-app", - "version": "1.6.1-rc.1", + "name": "od-app", + "version": "1.7.2", "private": true, "scripts": { "start": "craco start", - "build": "craco build", + "build": "craco build && yarn sentry:sourcemaps", "test": "craco test", "eject": "craco eject", "prettier": "prettier --write \"{,**/*.{ts,tsx,json,js,md}}\"", @@ -14,15 +14,12 @@ "lint:fix": "yarn run lint --fix", "lint:check": "yarn run lint --max-warnings=0", "format": "yarn run prettier:fix && yarn run lint:fix", - "format:check": "yarn run prettier && yarn run lint" + "format:check": "yarn run prettier && yarn run lint", + "sentry:sourcemaps": "sentry-cli sourcemaps inject --org open-dollar --project open-dollar ./build && sentry-cli sourcemaps upload --org open-dollar --project open-dollar ./build" }, "engines": { "node": ">=20.0.0" }, - "peerDependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, "dependencies": { "@apollo/client": "^3.7.17", "@coinbase/wallet-sdk": "^3.7.1", @@ -30,10 +27,14 @@ "@ethersproject/address": "^5.0.10", "@ethersproject/experimental": "5.4.0", "@ethersproject/providers": "5.4.5", - "@opendollar/sdk": "^1.6.1-rc.1", - "@opendollar/svg-generator": "1.0.3", + "@opendollar/sdk": "1.7.6", + "@opendollar/svg-generator": "1.7.4", "@react-spring/web": "^9.7.3", - "@types/jest": "^24.0.0", + "@sentry/cli": "^2.31.0", + "@sentry/integrations": "^7.112.2", + "@sentry/react": "^7.112.2", + "@sentry/webpack-plugin": "^2.16.1", + "@tanstack/react-table": "^8.17.3", "@types/node": "^12.0.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", @@ -47,55 +48,49 @@ "@web3-react/url": "^8.2.3", "@web3-react/walletconnect-v2": "^8.5.1", "@web3-react/walletlink-connector": "6.2.3", - "async-retry": "^1.3.1", - "autoprefixer": "^9", "axios": "^1.6.7", - "buffer": "^6.0.3", "classnames": "^2.2.6", - "comma-number": "^2.1.0", "dayjs": "^1.9.4", "easy-peasy": "^5.1.0", "ethers": "5.4.7", - "gh-pages": "^4.0.0", "graphql": "^16.8.1", - "i18next": "^19.7.0", + "i18next": "^23.11.5", "jazzicon": "^1.5.0", - "jdenticon": "^3.0.1", - "jsbi": "^3.1.4", - "jsonp": "^0.2.1", "numeral": "^2.0.6", - "postcss": "^8.4.31", - "query-string": "^6.13.5", - "react": "^17.0.1", + "react": "^18.3.1", "react-confetti": "^6.0.1", "react-cookie-consent": "^5.2.0", "react-copy-to-clipboard": "^5.0.2", "react-custom-scrollbars": "^4.2.1", "react-device-detect": "^1.13.1", - "react-dom": "^17.0.1", + "react-dom": "^18.3.1", "react-feather": "^2.0.9", - "react-helmet-async": "^1.0.7", + "react-helmet-async": "^2.0.5", "react-i18next": "^11.7.2", - "react-lottie-player": "^1.4.1", + "react-loading-skeleton": "^3.4.0", "react-number-format": "^5.2.2", "react-paginate": "^6.5.0", - "react-router-dom": "^5.3.0", + "react-router-dom": "^6.24.1", "react-scripts": "5.0.1", "react-toastify": "^6.0.9", "react-tooltip": "^5.21.1", "react-transition-group": "^4.4.1", + "siwe": "^2.3.2", "styled-components": "^5.2.0", - "tailwindcss": "^3.3.3", - "typescript": "^4.4.3", - "util": "^0.12.5" + "terser-webpack-plugin": "^5.3.10", + "tiny-invariant": "^1.3.3", + "typescript": "^4.4.3" }, "devDependencies": { + "@babel/preset-typescript": "^7.24.1", + "@testing-library/dom": "^9.3.4", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", "@testing-library/user-event": "^14.5.2", "@types/async-retry": "^1.4.2", "@types/classnames": "^2.2.11", "@types/cypress": "^1.1.3", + "@types/jest": "^29.5.12", "@types/jsonp": "^0.2.0", "@types/numeral": "^0.0.28", "@types/react-copy-to-clipboard": "^4.3.0", @@ -110,7 +105,8 @@ "cypress-wait-until": "^1.7.1", "husky": "^5.0.9", "lint-staged": "^10.5.4", - "prettier": "^2.2.1" + "prettier": "^2.2.1", + "serve": "^14.2.3" }, "eslintConfig": { "extends": "react-app", diff --git a/public/index.html b/public/index.html index 91669097..a8f5971f 100644 --- a/public/index.html +++ b/public/index.html @@ -2,34 +2,66 @@ + content=" + default-src 'self' blob: https://opendollar.wowto.ai https://verify.walletconnect.com https://verify.walletconnect.org; + style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; + script-src 'self' blob: https://kb.wowto.ai https://app.wowto.ai http://cdn.matomo.cloud/usekeyp.matomo.cloud/matomo.js https://cdn.matomo.cloud/usekeyp.matomo.cloud/matomo.js https://cdn.matomo.cloud/matomo.js https://usekeyp.matomo.cloud/matomo.js; + media-src 'self'; + img-src 'self' data: blob: https://explorer-api.walletconnect.com https://usekeyp.matomo.cloud https://app.opendollar.com; + connect-src 'self' blob: https://virtual.arbitrum.rpc.tenderly.co https://eth-pokt.nodies.app http://localhost:3000 https://*.quiknode.pro https://api.opensea.io https://api.fuul.xyz https://api.camelot.exchange https://opt-mainnet.g.alchemy.com https://arb-mainnet.g.alchemy.com https://mainnet.optimism.io/ https://eth.llamarpc.com https://base.llamarpc.com https://polygon-bor-rpc.publicnode.com https://eth-pokt.nodies.app https://polygon-pokt.nodies.app https://op-pokt.nodies.app https://arb-pokt.nodies.app https://holy-damp-firefly.arbitrum-mainnet.quiknode.pro https://api.studio.thegraph.com https://od-subgraph-node-image.onrender.com https://usekeyp.matomo.cloud https://o1016103.ingest.us.sentry.io/api/4507153379295232/envelope/ https://o1016103.ingest.us.sentry.io/api/4507153379295232/security/ https://arbitrum-sepolia.infura.io https://arbitrum-sepolia.blockpi.network/v1/rpc/public https://arbitrum.blockpi.network/v1/rpc/public https://optimism.blockpi.network wss://relay.walletconnect.com/ https://verify.walletconnect.org wss://www.walletlink.org/rpc https://explorer-api.walletconnect.com https://chain-proxy.wallet.coinbase.com https://rpc.walletconnect.com https://bot.opendollar.com https://bot.dev.opendollar.com https://subgraph.reflexer.finance/subgraphs/name/reflexer-labs/rai https://api.country.is/ ; + object-src 'self' blob:; + form-action 'self'; + font-src 'self' data: https://fonts.gstatic.com; + "> + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + OD | App - - + + + diff --git a/public/manifest.json b/public/manifest.json index 1b220378..0d682b38 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -23,7 +23,7 @@ "theme_color": "#000000", "background_color": "#ffffff", "iconPath": "od-logo.png", - "description": "Volatility dampened synthetic instruments", + "description": "Open Dollar is a stablecoin protocol built on Arbitrum designed to help you yield and leverage your assets with safety and predictability.", "providedBy": { "name": "Open Dollar", "url": "https://app.opendollar.com/" diff --git a/public/og.png b/public/og.png new file mode 100644 index 00000000..24508872 Binary files /dev/null and b/public/og.png differ diff --git a/public/squares1x.webp b/public/squares1x.webp new file mode 100644 index 00000000..f5f83e59 Binary files /dev/null and b/public/squares1x.webp differ diff --git a/public/tracking.js b/public/tracking.js index 987c4fa4..015ca894 100644 --- a/public/tracking.js +++ b/public/tracking.js @@ -1,8 +1,8 @@ -var idSite = 6; -var matomoTrackingApiUrl = 'https://usekeyp.matomo.cloud/matomo.php'; +var idSite = 6 +var matomoTrackingApiUrl = 'https://usekeyp.matomo.cloud/matomo.php' -var _paq = window._paq = window._paq || []; -_paq.push(['setTrackerUrl', matomoTrackingApiUrl]); -_paq.push(['setSiteId', idSite]); -_paq.push(['trackPageView']); -_paq.push(['enableLinkTracking']); +var _paq = (window._paq = window._paq || []) +_paq.push(['setTrackerUrl', matomoTrackingApiUrl]) +_paq.push(['setSiteId', idSite]) +_paq.push(['trackPageView']) +_paq.push(['enableLinkTracking']) diff --git a/public/wavy-blue.webp b/public/wavy-blue.webp new file mode 100644 index 00000000..894a6b3d Binary files /dev/null and b/public/wavy-blue.webp differ diff --git a/src/App.tsx b/src/App.tsx index 65fd6fdb..a077dd92 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ +import React, { useEffect, lazy, Suspense } from 'react' import i18next from 'i18next' -import { Suspense } from 'react' import { I18nextProvider } from 'react-i18next' -import { Redirect, Route, Switch } from 'react-router-dom' +import { Route, Routes, useLocation } from 'react-router-dom' import { ThemeProvider } from 'styled-components' import ErrorBoundary from './ErrorBoundary' import GlobalStyle from './GlobalStyle' @@ -10,89 +10,111 @@ import Safes from './containers/Vaults' import VaultDetails from './containers/Vaults/VaultDetails' import DepositFunds from './containers/Deposit/DepositFunds' import Shared from './containers/Shared' -import { useStoreState } from './store' -import { Theme } from './utils/interfaces' -import { darkTheme } from './utils/themes/dark' +import Bridge from './containers/Bridge' +import { lightTheme } from './utils/themes/light' import { StatsProvider } from './hooks/useStats' - import { ApolloProvider } from '@apollo/client' import { client } from './utils/graph' - -import GoogleTagManager from './components/Analytics/GoogleTagManager' import CreateVault from './containers/Vaults/CreateVault' import Auctions from './containers/Auctions' - -// Toast css import Analytics from './containers/Analytics' -import { ToastContainer } from 'react-toastify' import PageNotFound from '~/containers/PageNotFound' +import Maintenance from '~/containers/Maintenance' +import MaintenanceRedirect from '~/containers/MaintenanceRedirect' +import GeoBlockContainer from './containers/GeoBlockContainer' +import * as Sentry from '@sentry/react' +import Earn from './containers/Earn' +import Bolts from './containers/Bolts' +import EarnDetails from './containers/Earn/EarnDetails' +import Marketplace from './containers/Marketplace' +import ScreenLoader from '~/components/Modals/ScreenLoader' +import Explore from '~/containers/Explore' -declare module 'styled-components' { - export interface DefaultTheme extends Theme {} -} +import 'react-loading-skeleton/dist/skeleton.css' + +const ToastContainer = lazy(() => import('react-toastify').then((module) => ({ default: module.ToastContainer }))) -console.log( - `%c🧙 Join the Open Dollar Team! ⚔️`, - 'color:blue;font-family:sans-serif;font-size:4rem;-webkit-text-stroke: 1px black;font-weight:bold' -) -console.log( - `%cInquire about your next adventure in our Discord`, - 'font-family:sans-serif;font-size:1rem;font-weight:bold' -) +Sentry.init({ + dsn: process.env.REACT_APP_SENTRY_DSN, + integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()], + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions + // Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled + tracePropagationTargets: ['localhost'], + // Session Replay + replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. + replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. + environment: process.env.NODE_ENV, +}) const App = () => { - const { settingsModel: settingsState } = useStoreState((state) => state) + const location = useLocation() - const { bodyOverflow } = settingsState + useEffect(() => { + const params = new URLSearchParams(location.search) + const referrer = params.get('referrer') + const af = params.get('af') + + if (referrer || af) { + localStorage.setItem('referralProgram', 'true') + } + }, [location.search]) return ( - - + + - - - - - - + }> + + + + + - <> - - - - - - + + + } path="/404" /> + } path={'/'} /> + } path={'/maintenance'} /> + } path={'/earn'} /> + } path={'/bolts'} /> + } path={'/stats'} /> + } path={'/explore'} /> } + path={'/geoblock'} /> + } path={'/auctions'} /> + } path={'/marketplace'} /> } + path={'/vaults/create'} /> - - - + } path={'/bridge'} /> } + path={'/vaults/:id/deposit'} /> - - - + } path={'/vaults/:id/withdraw'} /> + } path={'/vaults/:id'} /> + } path={'/earn/:id'} /> + } path={'/vaults'} /> + } path={'/:address'} /> + } path={'/deposit/:token/deposit'} /> + } /> + + - - - - + + + + diff --git a/src/ErrorBoundary.tsx b/src/ErrorBoundary.tsx index 90a68dab..01d36a23 100644 --- a/src/ErrorBoundary.tsx +++ b/src/ErrorBoundary.tsx @@ -1,6 +1,6 @@ import React from 'react' import styled from 'styled-components' -import splashImage from '~/assets/404.png' +import splashImage from '~/assets/404.webp' import Brand from '~/components/Brand' interface State { @@ -63,6 +63,7 @@ const Container = styled.div` background-size: contain; background-position: center; background-repeat: no-repeat; + color: #fff; height: 100vh; width: 100%; position: relative; @@ -77,7 +78,7 @@ const CenterBox = styled.div` padding: 20px; border-radius: 10px; text-align: center; - backdrop-filter: blur(10px); + backdrop-filter: blur(100px); border: 1px solid rgba(255, 255, 255, 0); box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); ` diff --git a/src/GlobalStyle.tsx b/src/GlobalStyle.tsx index bc7fbf72..37d6de6b 100644 --- a/src/GlobalStyle.tsx +++ b/src/GlobalStyle.tsx @@ -1,9 +1,5 @@ import { createGlobalStyle, css } from 'styled-components' -interface Props { - bodyOverflow?: boolean -} - const GlobalStyle = createGlobalStyle` body::-webkit-scrollbar { @@ -26,12 +22,11 @@ const GlobalStyle = createGlobalStyle` } body { - color: ${(props) => props.theme.colors.primary}; - background-color:${(props) => props.theme.colors.background}; - background-size: contain; - background-position: center 100px; + background-image: url('/squares1x.webp'), url('/wavy-blue.webp'); + background-color: #E2F1FF; + background-size: contain, 100%; + background-position: bottom left, top right; background-repeat: no-repeat; - overflow: ${(props: Props) => (props.bodyOverflow ? 'hidden' : 'visible')}; .web3modal-modal-lightbox { z-index: 999; @@ -64,38 +59,19 @@ const GlobalStyle = createGlobalStyle` } } -.place-left { - &:after{ - border-left-color:${(props) => props.theme.colors.foreground} !important - } - } - .place-top { - &:after{ - border-top-color:${(props) => props.theme.colors.foreground} !important - } - } - .place-bottom { - &:after{ - border-bottom-color:${(props) => props.theme.colors.foreground} !important - } - } - .place-right { - &:after{ - border-right-color:${(props) => props.theme.colors.foreground} !important - } + .Toastify__toast-container { + padding: 0; + margin: 0; + height: 60px; } .__react_component_tooltip { max-width: 250px; padding-top: 20px; padding-bottom: 20px; border-radius: 5px; - color:${(props) => props.theme.colors.primary}; opacity: 1 !important; - background: ${(props) => props.theme.colors.foreground}; - border: ${(props) => props.theme.colors.border} !important; - box-shadow: 0 0 6px rgba(0, 0, 0, 0.16); - + box-shadow: 0 0 6px rgba(0, 0, 0, 0.16); } } ` @@ -134,9 +110,9 @@ export const BtnStyle = css<{ color?: 'blueish' | 'greenish' | 'yellowish' | 'colorPrimary' | 'colorSecondary' border?: boolean }>` - pointer-events: ${({ theme, disabled }) => (disabled ? 'none' : 'inherit')}; + pointer-events: ${({ disabled }) => (disabled ? 'none' : 'inherit')}; outline: none; - cursor: ${({ theme, disabled }) => (disabled ? 'not-allowed' : 'pointer')}; + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; min-width: 134px; border: ${({ theme, border }) => (border ? `1px solid ${theme.colors.blueish}` : 'none')}; box-shadow: none; diff --git a/src/assets/404.png b/src/assets/404.png deleted file mode 100644 index a0cb38b4..00000000 Binary files a/src/assets/404.png and /dev/null differ diff --git a/src/assets/404.webp b/src/assets/404.webp new file mode 100644 index 00000000..874a4b20 Binary files /dev/null and b/src/assets/404.webp differ diff --git a/src/assets/arb-griff.svg b/src/assets/arb-griff.svg new file mode 100644 index 00000000..2331e565 --- /dev/null +++ b/src/assets/arb-griff.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/arb.svg b/src/assets/arb.svg index 6c25725f..b9421160 100644 --- a/src/assets/arb.svg +++ b/src/assets/arb.svg @@ -1,37 +1,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + diff --git a/src/assets/base.svg b/src/assets/base.svg new file mode 100644 index 00000000..8321a356 --- /dev/null +++ b/src/assets/base.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/blueish-bg.png b/src/assets/blueish-bg.png deleted file mode 100644 index 84e6e04d..00000000 Binary files a/src/assets/blueish-bg.png and /dev/null differ diff --git a/src/assets/boxes.svg b/src/assets/boxes.svg deleted file mode 100644 index 4240a7a1..00000000 --- a/src/assets/boxes.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/camelot.svg b/src/assets/camelot.svg new file mode 100644 index 00000000..b7c6d9d2 --- /dev/null +++ b/src/assets/camelot.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/caret.png b/src/assets/caret.png deleted file mode 100644 index 542556a0..00000000 Binary files a/src/assets/caret.png and /dev/null differ diff --git a/src/assets/caret.webp b/src/assets/caret.webp new file mode 100644 index 00000000..7628dfe9 Binary files /dev/null and b/src/assets/caret.webp differ diff --git a/src/assets/cbETH.svg b/src/assets/cbETH.svg index 4f092d02..40c06cbe 100644 --- a/src/assets/cbETH.svg +++ b/src/assets/cbETH.svg @@ -1,9 +1,11 @@ - - - - - - - + + + + + + + \ No newline at end of file diff --git a/src/assets/close-icon.svg b/src/assets/close-icon.svg deleted file mode 100644 index 55d7f34c..00000000 --- a/src/assets/close-icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/closed-vault.webp b/src/assets/closed-vault.webp new file mode 100644 index 00000000..2e8775dc Binary files /dev/null and b/src/assets/closed-vault.webp differ diff --git a/src/assets/collateral.svg b/src/assets/collateral.svg deleted file mode 100644 index 8ff514d8..00000000 --- a/src/assets/collateral.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/connectors/coinbaseWalletIcon.svg b/src/assets/connectors/coinbaseWalletIcon.svg index 04e9f49b..e5c0515b 100644 --- a/src/assets/connectors/coinbaseWalletIcon.svg +++ b/src/assets/connectors/coinbaseWalletIcon.svg @@ -1,4 +1,4 @@ - + diff --git a/src/assets/connectors/metamask-fox.svg b/src/assets/connectors/metamask-fox.svg index a6cffef0..fd84faa0 100644 --- a/src/assets/connectors/metamask-fox.svg +++ b/src/assets/connectors/metamask-fox.svg @@ -1 +1,48 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/connectors/metamask.png b/src/assets/connectors/metamask.png deleted file mode 100644 index 85714ea2..00000000 Binary files a/src/assets/connectors/metamask.png and /dev/null differ diff --git a/src/assets/connectors/metamask.webp b/src/assets/connectors/metamask.webp new file mode 100644 index 00000000..edbabc9b Binary files /dev/null and b/src/assets/connectors/metamask.webp differ diff --git a/src/assets/connectors/walletConnectIcon.svg b/src/assets/connectors/walletConnectIcon.svg index f78a7b85..8d00d298 100644 --- a/src/assets/connectors/walletConnectIcon.svg +++ b/src/assets/connectors/walletConnectIcon.svg @@ -1,9 +1,10 @@ - - + + + - - + + - + diff --git a/src/assets/curve.svg b/src/assets/curve.svg deleted file mode 100644 index e6bd7b08..00000000 --- a/src/assets/curve.svg +++ /dev/null @@ -1,4581 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/assets/debt.svg b/src/assets/debt.svg deleted file mode 100644 index b2147592..00000000 --- a/src/assets/debt.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/discord.svg b/src/assets/discord.svg new file mode 100644 index 00000000..6a995f5c --- /dev/null +++ b/src/assets/discord.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/error.svg b/src/assets/error.svg deleted file mode 100644 index 7e976979..00000000 --- a/src/assets/error.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/eth-icon.svg b/src/assets/eth-icon.svg deleted file mode 100644 index 114d3649..00000000 --- a/src/assets/eth-icon.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/src/assets/eth-img.svg b/src/assets/eth-img.svg deleted file mode 100644 index 24fd4c71..00000000 --- a/src/assets/eth-img.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/eth.svg b/src/assets/eth.svg new file mode 100644 index 00000000..5173b26d --- /dev/null +++ b/src/assets/eth.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/ethereum.svg b/src/assets/ethereum.svg new file mode 100644 index 00000000..fe050eb0 --- /dev/null +++ b/src/assets/ethereum.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/flx-logo.svg b/src/assets/flx-logo.svg deleted file mode 100644 index 94d824b9..00000000 --- a/src/assets/flx-logo.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/assets/flx_uni_eth.svg b/src/assets/flx_uni_eth.svg deleted file mode 100644 index 661d7f44..00000000 --- a/src/assets/flx_uni_eth.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/flx_uni_hai.svg b/src/assets/flx_uni_hai.svg deleted file mode 100644 index 7c9ef7d0..00000000 --- a/src/assets/flx_uni_hai.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/footer-bg-art.svg b/src/assets/footer-bg-art.svg deleted file mode 100644 index d949e933..00000000 --- a/src/assets/footer-bg-art.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/galxe.svg b/src/assets/galxe.svg new file mode 100644 index 00000000..1da30bbf --- /dev/null +++ b/src/assets/galxe.svg @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/src/assets/gm-eth.svg b/src/assets/gm-eth.svg new file mode 100644 index 00000000..7456a832 --- /dev/null +++ b/src/assets/gm-eth.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/assets/greenish-bg.png b/src/assets/greenish-bg.png deleted file mode 100644 index 4856daac..00000000 Binary files a/src/assets/greenish-bg.png and /dev/null differ diff --git a/src/assets/grt.svg b/src/assets/grt.svg new file mode 100644 index 00000000..aa45d612 --- /dev/null +++ b/src/assets/grt.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/incentive.svg b/src/assets/incentive.svg deleted file mode 100644 index 10545a31..00000000 --- a/src/assets/incentive.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/leaderboard-leaders-badge.svg b/src/assets/leaderboard-leaders-badge.svg new file mode 100644 index 00000000..51d72be3 --- /dev/null +++ b/src/assets/leaderboard-leaders-badge.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/leaderboard-pillars.svg b/src/assets/leaderboard-pillars.svg new file mode 100644 index 00000000..e153a387 --- /dev/null +++ b/src/assets/leaderboard-pillars.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/logo192.png b/src/assets/logo192.png deleted file mode 100644 index 0ea90e24..00000000 Binary files a/src/assets/logo192.png and /dev/null differ diff --git a/src/assets/logo4x.png b/src/assets/logo4x.png deleted file mode 100644 index 00529b27..00000000 Binary files a/src/assets/logo4x.png and /dev/null differ diff --git a/src/assets/logo4x.webp b/src/assets/logo4x.webp new file mode 100644 index 00000000..b5f27f5a Binary files /dev/null and b/src/assets/logo4x.webp differ diff --git a/src/assets/nfts-icon.svg b/src/assets/nfts-icon.svg deleted file mode 100644 index cf1482bc..00000000 --- a/src/assets/nfts-icon.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/assets/od-colorloop.png b/src/assets/od-colorloop.png deleted file mode 100644 index 44cbe0f3..00000000 Binary files a/src/assets/od-colorloop.png and /dev/null differ diff --git a/src/assets/od-colorloop.webp b/src/assets/od-colorloop.webp new file mode 100644 index 00000000..a51b69bd Binary files /dev/null and b/src/assets/od-colorloop.webp differ diff --git a/src/assets/od-full-logo-dark.svg b/src/assets/od-full-logo-dark.svg new file mode 100644 index 00000000..da5e6e32 --- /dev/null +++ b/src/assets/od-full-logo-dark.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/od-full-logo-light.svg b/src/assets/od-full-logo-light.svg new file mode 100644 index 00000000..6456e7fa --- /dev/null +++ b/src/assets/od-full-logo-light.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/od-full-logo.png b/src/assets/od-full-logo.png deleted file mode 100644 index ebf35c0d..00000000 Binary files a/src/assets/od-full-logo.png and /dev/null differ diff --git a/src/assets/od-full-logo.svg b/src/assets/od-full-logo.svg deleted file mode 100644 index cfb1febe..00000000 --- a/src/assets/od-full-logo.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/src/assets/od-land.png b/src/assets/od-land.png deleted file mode 100644 index 865a9ccf..00000000 Binary files a/src/assets/od-land.png and /dev/null differ diff --git a/src/assets/od-logo-grey.svg b/src/assets/od-logo-grey.svg new file mode 100644 index 00000000..b803c897 --- /dev/null +++ b/src/assets/od-logo-grey.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/od-logo.png b/src/assets/od-logo.png deleted file mode 100644 index 06bc43be..00000000 Binary files a/src/assets/od-logo.png and /dev/null differ diff --git a/src/assets/od-logo192w.png b/src/assets/od-logo192w.png deleted file mode 100644 index 46cb2c5d..00000000 Binary files a/src/assets/od-logo192w.png and /dev/null differ diff --git a/src/assets/od-logo256w.png b/src/assets/od-logo256w.png deleted file mode 100644 index 58291c37..00000000 Binary files a/src/assets/od-logo256w.png and /dev/null differ diff --git a/src/assets/od-token.svg b/src/assets/od-token.svg new file mode 100644 index 00000000..bd1c4be4 --- /dev/null +++ b/src/assets/od-token.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/od-vault.png b/src/assets/od-vault.png deleted file mode 100644 index f1cf56da..00000000 Binary files a/src/assets/od-vault.png and /dev/null differ diff --git a/src/assets/od-wallet-icon.svg b/src/assets/od-wallet-icon.svg deleted file mode 100644 index a1712d00..00000000 --- a/src/assets/od-wallet-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/odg-token.svg b/src/assets/odg-token.svg new file mode 100644 index 00000000..7b2092bb --- /dev/null +++ b/src/assets/odg-token.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/odg.svg b/src/assets/odg.svg deleted file mode 100644 index 10e5f4aa..00000000 --- a/src/assets/odg.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/src/assets/opened-vault.webp b/src/assets/opened-vault.webp new file mode 100644 index 00000000..4393716b Binary files /dev/null and b/src/assets/opened-vault.webp differ diff --git a/src/assets/optimism.svg b/src/assets/optimism.svg new file mode 100644 index 00000000..a4aba1ad --- /dev/null +++ b/src/assets/optimism.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/parachute-icon.svg b/src/assets/parachute-icon.svg new file mode 100644 index 00000000..328d84f2 --- /dev/null +++ b/src/assets/parachute-icon.svg @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/src/assets/pendle-pt-reth.svg b/src/assets/pendle-pt-reth.svg new file mode 100644 index 00000000..9650fc86 --- /dev/null +++ b/src/assets/pendle-pt-reth.svg @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/pendle-pt-wsteth.svg b/src/assets/pendle-pt-wsteth.svg new file mode 100644 index 00000000..8470ba60 --- /dev/null +++ b/src/assets/pendle-pt-wsteth.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/plus.svg b/src/assets/plus.svg deleted file mode 100644 index 22ce4a08..00000000 --- a/src/assets/plus.svg +++ /dev/null @@ -1,5 +0,0 @@ - - -plus - - diff --git a/src/assets/polygon.svg b/src/assets/polygon.svg new file mode 100644 index 00000000..5ee82c82 --- /dev/null +++ b/src/assets/polygon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/pufeth.svg b/src/assets/pufeth.svg new file mode 100644 index 00000000..52a38eb7 --- /dev/null +++ b/src/assets/pufeth.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/quests-img.png b/src/assets/quests-img.png new file mode 100644 index 00000000..351efce4 Binary files /dev/null and b/src/assets/quests-img.png differ diff --git a/src/assets/rETH.svg b/src/assets/rETH.svg index ec5a2654..a0320281 100644 --- a/src/assets/rETH.svg +++ b/src/assets/rETH.svg @@ -1,67 +1,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + diff --git a/src/assets/siren.svg b/src/assets/siren.svg deleted file mode 100644 index 15f8ee90..00000000 --- a/src/assets/siren.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/splash/discord.svg b/src/assets/splash/discord.svg deleted file mode 100644 index 52e28f0a..00000000 --- a/src/assets/splash/discord.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/splash/eth.svg b/src/assets/splash/eth.svg deleted file mode 100644 index ff191c04..00000000 --- a/src/assets/splash/eth.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/assets/splash/kite.png b/src/assets/splash/kite.png deleted file mode 100644 index 0d5de859..00000000 Binary files a/src/assets/splash/kite.png and /dev/null differ diff --git a/src/assets/splash/liquid-eth.svg b/src/assets/splash/liquid-eth.svg deleted file mode 100644 index 198a833d..00000000 --- a/src/assets/splash/liquid-eth.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/assets/splash/optimism.svg b/src/assets/splash/optimism.svg deleted file mode 100644 index 0088d10d..00000000 --- a/src/assets/splash/optimism.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/splash/partly-cloudy.png b/src/assets/splash/partly-cloudy.png deleted file mode 100644 index 80d82374..00000000 Binary files a/src/assets/splash/partly-cloudy.png and /dev/null differ diff --git a/src/assets/splash/partly-cloudy.svg b/src/assets/splash/partly-cloudy.svg deleted file mode 100644 index 6ca1003d..00000000 --- a/src/assets/splash/partly-cloudy.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/splash/twitter.svg b/src/assets/splash/twitter.svg deleted file mode 100644 index 79352e85..00000000 --- a/src/assets/splash/twitter.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/assets/stFLX.svg b/src/assets/stFLX.svg deleted file mode 100644 index f61b3f85..00000000 --- a/src/assets/stFLX.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/staking.png b/src/assets/staking.png deleted file mode 100644 index b44d7dd8..00000000 Binary files a/src/assets/staking.png and /dev/null differ diff --git a/src/assets/staking.svg b/src/assets/staking.svg deleted file mode 100644 index 73a0af66..00000000 --- a/src/assets/staking.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/stats-img-lock.webp b/src/assets/stats-img-lock.webp new file mode 100644 index 00000000..cf1eed0f Binary files /dev/null and b/src/assets/stats-img-lock.webp differ diff --git a/src/assets/stats-img-vault.webp b/src/assets/stats-img-vault.webp new file mode 100644 index 00000000..0d237a64 Binary files /dev/null and b/src/assets/stats-img-vault.webp differ diff --git a/src/assets/stn-img.png b/src/assets/stn-img.png deleted file mode 100644 index db71e259..00000000 Binary files a/src/assets/stn-img.png and /dev/null differ diff --git a/src/assets/surplus.svg b/src/assets/surplus.svg deleted file mode 100644 index 729b44a9..00000000 --- a/src/assets/surplus.svg +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/src/assets/ttm-img.png b/src/assets/ttm-img.png deleted file mode 100644 index ecb2dbf9..00000000 Binary files a/src/assets/ttm-img.png and /dev/null differ diff --git a/src/assets/turtle-club.png b/src/assets/turtle-club.png new file mode 100644 index 00000000..1cd6d111 Binary files /dev/null and b/src/assets/turtle-club.png differ diff --git a/src/assets/tx-failed-icon.svg b/src/assets/tx-failed-icon.svg new file mode 100644 index 00000000..ded15406 --- /dev/null +++ b/src/assets/tx-failed-icon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/assets/tx-submitted-icon.svg b/src/assets/tx-submitted-icon.svg new file mode 100644 index 00000000..4e24e410 --- /dev/null +++ b/src/assets/tx-submitted-icon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/tx-waiting-for-confirmation-icon.svg b/src/assets/tx-waiting-for-confirmation-icon.svg new file mode 100644 index 00000000..b833824b --- /dev/null +++ b/src/assets/tx-waiting-for-confirmation-icon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/unknown-token.svg b/src/assets/unknown-token.svg new file mode 100644 index 00000000..8d4ea107 --- /dev/null +++ b/src/assets/unknown-token.svg @@ -0,0 +1,4 @@ + + + - + \ No newline at end of file diff --git a/src/assets/vault-facilitator.webp b/src/assets/vault-facilitator.webp new file mode 100644 index 00000000..ddefa862 Binary files /dev/null and b/src/assets/vault-facilitator.webp differ diff --git a/src/assets/wallet-icon.svg b/src/assets/wallet-icon.svg new file mode 100644 index 00000000..268b24b2 --- /dev/null +++ b/src/assets/wallet-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/wallet.webp b/src/assets/wallet.webp new file mode 100644 index 00000000..c470a101 Binary files /dev/null and b/src/assets/wallet.webp differ diff --git a/src/assets/wbtc-img.svg b/src/assets/wbtc-img.svg deleted file mode 100644 index 39ed65ec..00000000 --- a/src/assets/wbtc-img.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/wsteth.svg b/src/assets/wsteth.svg new file mode 100644 index 00000000..ecc43f54 --- /dev/null +++ b/src/assets/wsteth.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/zealy.svg b/src/assets/zealy.svg new file mode 100644 index 00000000..4f832625 --- /dev/null +++ b/src/assets/zealy.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/chains.ts b/src/chains.ts index fbae6ff9..51a60f47 100644 --- a/src/chains.ts +++ b/src/chains.ts @@ -16,12 +16,23 @@ import type { AddEthereumChainParameter } from '@web3-react/types' +const RPC_URL = process.env.REACT_APP_NETWORK_URL as string +const NETWORK_ID = process.env.REACT_APP_NETWORK_ID as string + const ETH: AddEthereumChainParameter['nativeCurrency'] = { name: 'Ether', symbol: 'ETH', decimals: 18, } +export const RPC_URL_ETHEREUM = process.env.REACT_APP_RPC_URL_ETHEREUM + ? process.env.REACT_APP_RPC_URL_ETHEREUM + : 'https://eth.llamarpc.com' +export const RPC_URL_ARBITRUM = RPC_URL || 'https://arbitrum.blockpi.network/v1/rpc/public' +export const RPC_URL_OPTIMISM = 'https://op-pokt.nodies.app' +export const RPC_URL_POLYGON = 'https://polygon-bor-rpc.publicnode.com' +export const RPC_URL_BASE = 'https://base.llamarpc.com' + interface BasicChainInformation { urls: string[] name: string @@ -54,16 +65,15 @@ export function getAddChainParameters(chainId: number): AddEthereumChainParamete } type ChainConfig = { [chainId: number]: BasicChainInformation | ExtendedChainInformation } - export const MAINNET_CHAINS: ChainConfig = { 42161: { - urls: ['https://arb1.arbitrum.io/rpc'], + urls: [RPC_URL, RPC_URL_ARBITRUM], name: 'Arbitrum One', nativeCurrency: ETH, blockExplorerUrls: ['https://arbiscan.io'], }, 10: { - urls: ['https://mainnet.optimism.io'], + urls: [RPC_URL_OPTIMISM], name: 'Optimism Mainnet', nativeCurrency: ETH, blockExplorerUrls: ['https://optimistic.etherscan.io'], @@ -72,20 +82,14 @@ export const MAINNET_CHAINS: ChainConfig = { export const TESTNET_CHAINS: ChainConfig = { 421614: { - urls: ['https://arbitrum-sepolia.blockpi.network/v1/rpc/public'], + urls: [RPC_URL, 'https://arbitrum-sepolia.blockpi.network/v1/rpc/public'], name: 'Arbitrum Sepolia', nativeCurrency: ETH, blockExplorerUrls: ['https://sepolia.arbiscan.io/'], }, - 420: { - urls: ['https://goerli.optimism.io'], - name: 'Optimism Goerli', - nativeCurrency: ETH, - blockExplorerUrls: ['https://goerli-optimism.etherscan.io/'], - }, } -const supportedChainId = parseInt(process.env.REACT_APP_NETWORK_ID || '', 10) +const supportedChainId = parseInt(NETWORK_ID || '', 10) if (isNaN(supportedChainId)) { throw new Error('REACT_APP_NETWORK_ID must be a valid number') diff --git a/src/components/AccountCardsWeb3ReactV2.tsx b/src/components/AccountCardsWeb3ReactV2.tsx index d1bd88ae..97e2987e 100644 --- a/src/components/AccountCardsWeb3ReactV2.tsx +++ b/src/components/AccountCardsWeb3ReactV2.tsx @@ -1,34 +1,114 @@ -// Copyright (C) 2023 Uniswap -// https://github.com/Uniswap/web3-react - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - +import React, { useState, useEffect, useRef } from 'react' import MetaMaskCard from '~/components/connectorCards/MetaMaskCard' import CoinbaseWalletCard from '~/components/connectorCards/CoinbaseWalletCard' import WalletConnectV2Card from '~/components/connectorCards/WalletConnectV2Card' -import React from 'react' import GnosisSafeCard from '~/components/connectorCards/GnosisCard' +import styled from 'styled-components' +import { useActiveWeb3React } from '~/hooks' +import { MetaMask } from '@web3-react/metamask' +import { Tooltip as ReactTooltip } from 'react-tooltip' +import { Info } from 'react-feather' export default function AccountCardsWeb3ReactV2() { + const [error, setError] = useState(undefined) + const { chainId } = useActiveWeb3React() + const { connector } = useActiveWeb3React() + return ( <> -
- - - - +
+ + {error && connector instanceof MetaMask ? ( + + ) : null} + {error && !(connector instanceof MetaMask) ? ( + + ) : null} + {String(chainId) !== process.env.REACT_APP_NETWORK_ID && chainId !== undefined ? ( + + ) : null} + + + + +
) } + +const ErrorMessage = ({ error }: { error: string }) => { + const [isOverflowing, setIsOverflowing] = useState(false) + const errorTextRef = useRef(null) + + useEffect(() => { + if (errorTextRef.current) { + setIsOverflowing(errorTextRef.current.scrollWidth > errorTextRef.current.clientWidth) + } + }, [error]) + + return ( + + {error} + {isOverflowing && ( + <> + + + + + + + + )} + + ) +} + +const TooltipText = styled.div` + font-family: 'Open Sans', sans-serif; + font-weight: normal; +` + +const ErrorContainer = styled.div` + display: flex; + text-align: start; + flex-direction: column; + justify-content: end; + align-items: start; + width: 100%; + min-height: 21px; +` + +const ErrorTextContainer = styled.div` + display: flex; + align-items: center; + max-width: 100%; + overflow: hidden; +` + +const ErrorText = styled.div` + font-family: 'Open Sans', sans-serif; + font-weight: 400; + font-size: ${(props) => props.theme.font.xxSmall}; + color: #ddf08b; + justify-content: start; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +` + +const InfoIcon = styled.div` + cursor: pointer; + margin-left: 5px; + display: flex; + align-items: center; + + svg { + color: #ddf08b; + } +` diff --git a/src/components/AddressLink.tsx b/src/components/AddressLink.tsx index 41257fcf..2cecf05a 100644 --- a/src/components/AddressLink.tsx +++ b/src/components/AddressLink.tsx @@ -1,6 +1,7 @@ import styled from 'styled-components' import { ExternalLinkArrow } from '~/GlobalStyle' -import { getEtherscanLink, returnWalletAddress } from '~/utils' +import { getEtherscanLink } from '~/utils' +import { useAddress } from '~/hooks/useAddress' export const Link = styled.a` ${ExternalLinkArrow} @@ -14,7 +15,7 @@ interface AddressLinkProps { export const AddressLink = ({ chainId, address }: AddressLinkProps) => { return ( - {returnWalletAddress(address)} + {useAddress(address, 0, true)} ) } diff --git a/src/components/Analytics/GoogleTagManager.tsx b/src/components/Analytics/GoogleTagManager.tsx index cd748f93..1bc47162 100644 --- a/src/components/Analytics/GoogleTagManager.tsx +++ b/src/components/Analytics/GoogleTagManager.tsx @@ -1,8 +1,7 @@ import React from 'react' import { Helmet } from 'react-helmet-async' -import { RouteComponentProps } from 'react-router-dom' -const GoogleTagManager = ({ location: { pathname } }: RouteComponentProps) => { +const GoogleTagManager = ({ location: { pathname } }: { location: { pathname: string } }) => { return ( {process.env.REACT_APP_GOOGLE_ANALYTICS_ID ? ( diff --git a/src/components/ApproveToken.tsx b/src/components/ApproveToken.tsx index beff3ccb..a4d902e7 100644 --- a/src/components/ApproveToken.tsx +++ b/src/components/ApproveToken.tsx @@ -111,9 +111,10 @@ const ApproveToken = ({ bids, amount, handleBackBtn, handleSuccess, methodName, passedCheckForAllowance(allowance.toString(), amountBN.toString(), true) } - } catch (e: any) { + } catch (e) { popupsActions.setBlockBackdrop(false) - if (e?.code === 4001) { + const error = e as { code?: number; message: string } + if (error.code === 4001) { setTextPayload({ title: 'Transaction Rejected.', text: '', @@ -122,7 +123,7 @@ const ApproveToken = ({ bids, amount, handleBackBtn, handleSuccess, methodName, return } setTextPayload({ - title: e.message.includes('proxy') ? 'No Open Dollar Account' : 'Transaction Failed.', + title: error.message.includes('proxy') ? 'No Open Dollar Account' : 'Transaction Failed.', text: '', status: 'error', }) @@ -176,7 +177,7 @@ const ImgContainer = styled.div` height: 40px; stroke: #4ac6b2; path { - stroke-width: 1 !important; + strokewidth: 1 !important; } &.stateless { stroke: orange; @@ -186,7 +187,7 @@ const ImgContainer = styled.div` } &.error { stroke: rgb(255, 104, 113); - stroke-width: 2; + strokewidth: 2; width: 60px !important; height: 60px !important; margin-bottom: 20px; diff --git a/src/components/AuctionBlock.tsx b/src/components/AuctionBlock.tsx index 249950db..77aecc6f 100644 --- a/src/components/AuctionBlock.tsx +++ b/src/components/AuctionBlock.tsx @@ -343,13 +343,13 @@ const InfoCol = styled.div` const InfoLabel = styled.div` color: ${(props) => props.theme.colors.secondary}; - font-size: ${(props) => props.theme.font.extraSmall}; + font-size: ${(props) => props.theme.font.xSmall}; ` const InfoValue = styled.div` margin-top: 3px; color: ${(props) => props.theme.colors.primary}; font-weight: normal; - font-size: ${(props) => props.theme.font.extraSmall}; + font-size: ${(props) => props.theme.font.xSmall}; ` const Content = styled.div` @@ -454,7 +454,6 @@ const ListItemLabel = styled.div` display:block; margin-bottom:5px; font-weight:normal; - color: ${(props) => props.theme.colors.customSecondary}; `} ` @@ -478,7 +477,6 @@ const List = styled.div` ${({ theme }) => theme.mediaWidth.upToSmall` flex-wrap:wrap; - border:1px solid ${(props) => props.theme.colors.border}; margin-bottom:10px; &:last-child { margin-bottom:0; @@ -490,7 +488,7 @@ const List = styled.div` const ListItem = styled.div` flex: 0 0 16.6%; color: ${(props) => props.theme.colors.customSecondary}; - font-size: ${(props) => props.theme.font.extraSmall}; + font-size: ${(props) => props.theme.font.xSmall}; padding: 15px 10px; &:first-child { padding-left: 25px; @@ -504,7 +502,7 @@ const ListItem = styled.div` flex: 0 0 50%; min-width:50%; - font-size: ${(props) => props.theme.font.extraSmall}; + font-size: 16px; font-weight:900; `} ` diff --git a/src/components/AuctionsFAQ.tsx b/src/components/AuctionsFAQ.tsx index 18ab0c80..95838de4 100644 --- a/src/components/AuctionsFAQ.tsx +++ b/src/components/AuctionsFAQ.tsx @@ -2,6 +2,10 @@ import { useState } from 'react' import styled from 'styled-components' import { useTranslation } from 'react-i18next' import { AuctionEventType } from '~/types' +import mine from '../assets/mine.svg' +import bid from '../assets/bid.svg' +import claim from '../assets/claim.svg' +import sellOd from '../assets/sell-od.svg' interface Props { type: AuctionEventType @@ -28,51 +32,51 @@ const AuctionsFAQ = ({ type }: Props) => { { title: t('debt_auction_minting_flx_header'), desc: t('debt_auction_minting_flx_desc'), - image: require('../assets/mine.svg').default, + image: mine, }, { title: t('debt_auction_how_to_bid'), desc: t('debt_auction_how_to_bid_desc'), - image: require('../assets/bid.svg').default, + image: bid, }, { title: t('debt_auction_claim_tokens'), desc: t('debt_auction_claim_tokens_desc'), - image: require('../assets/claim.svg').default, + image: claim, }, ], surplus: [ { title: t('surplus_auction_minting_flx_header'), desc: t('surplus_auction_minting_flx_desc'), - image: require('../assets/sell-od.svg').default, + image: sellOd, }, { title: t('surplus_auction_how_to_bid'), desc: t('surplus_auction_how_to_bid_desc'), - image: require('../assets/bid.svg').default, + image: bid, }, { title: t('surplus_auction_claim_tokens'), desc: t('surplus_auction_claim_tokens_desc'), - image: require('../assets/claim.svg').default, + image: claim, }, ], collateral: [ { title: t('collateral_auction_minting_flx_header'), desc: t('collateral_auction_minting_flx_desc'), - image: require('../assets/sell-od.svg').default, + image: sellOd, }, { title: t('collateral_auction_increasing_discount_header'), desc: t('collateral_auction_increasing_discount_desc'), - image: require('../assets/bid.svg').default, + image: bid, }, { title: t('collateral_auction_settlement_header'), desc: t('collateral_auction_settlement_desc'), - image: require('../assets/claim.svg').default, + image: claim, }, ], } @@ -108,22 +112,24 @@ const HeroSection = styled.div` ` const Header = styled.div` font-size: ${(props) => props.theme.font.large}; - font-weight: 900; + font-weight: 700; + font-family: ${(props) => props.theme.family.headers}; + color: white; display: flex; align-items: center; justify-content: center; - margin-bottom: 40px; + margin-bottom: 30px; cursor: pointer; button { margin-left: 10px; - font-size: ${(props) => props.theme.font.extraSmall}; + font-size: ${(props) => props.theme.font.xSmall}; min-width: auto !important; border-radius: 25px; padding: 2px 10px; background: linear-gradient(225deg, #4ce096 0%, #78d8ff 100%); } - ${({ theme }) => theme.mediaWidth.upToExtraSmall` + ${({ theme }) => theme.mediaWidth.upToxSmall` flex-direction:column; margin-bottom:25px; button { @@ -138,13 +144,14 @@ const Content = styled.div` ` const SectionHeading = styled.div` font-size: ${(props) => props.theme.font.default}; - font-weight: bold; + font-weight: 700; + font-family: ${(props) => props.theme.family.headers}; + color: ${(props) => props.theme.colors.accent}; ` const SectionContent = styled.div` margin-top: 10px; - font-size: ${(props) => props.theme.font.small}; - line-height: 23px; - color: ${(props) => props.theme.colors.secondary}; + font-size: ${(props) => props.theme.font.default}; + color: ${(props) => props.theme.colors.accent}; text-align: left; ` @@ -157,7 +164,7 @@ const Col = styled.div` const InnerCol = styled.div` background: ${(props) => props.theme.colors.background}; - border-radius: 20px; + border-radius: 3px; padding: 20px; text-align: center; ` diff --git a/src/components/AuctionsOperations/AuctionsPayment.tsx b/src/components/AuctionsOperations/AuctionsPayment.tsx index e9a749a6..32d7bfe3 100644 --- a/src/components/AuctionsOperations/AuctionsPayment.tsx +++ b/src/components/AuctionsOperations/AuctionsPayment.tsx @@ -9,7 +9,6 @@ import { useStoreActions, useStoreState } from '~/store' import { COIN_TICKER, formatNumber, sanitizeDecimals, toFixedString } from '~/utils' import DecimalInput from '~/components/DecimalInput' import Button from '~/components/Button' -import Results from './Results' const AuctionsPayment = () => { const { t } = useTranslation() @@ -242,14 +241,28 @@ const AuctionsPayment = () => { : BigNumber.from('0') // Collateral Error when you dont have enough balance - if (buyAmountBN.gt(totalRaiBalance) || valueBN.gt(odBalanceBN)) { + if (buyAmountBN.gt(totalRaiBalance)) { setError(`Insufficient ${COIN_TICKER} balance.`) return false } + if (valueBN.gt(odBalanceBN)) { + setError(`Insufficient OD balance.`) + return false + } + + if (valueBN.lt(ethers.utils.parseUnits('101', 18))) { + setError(`The minimum bid amount is 100 OD.`) + return false + } + // Collateral Error when there is not enough collateral left to buy if (collateralAmountBN.gt(remainingCollateral)) { - setError(`Insufficient ${tokenSymbol} to buy.`) + setError( + `Insufficient ${tokenSymbol} to buy. There is only ${Number( + ethers.utils.formatEther(remainingCollateral.toString()) + )} ${tokenSymbol} left.` + ) return false } } @@ -402,11 +415,10 @@ const AuctionsPayment = () => { label={`Claimable ${isClaim ? returnClaimValues().symbol : sellSymbol}`} /> )} - {error && {error}} - + {error}
) @@ -425,12 +437,17 @@ const MarginFixer = styled.div` const Footer = styled.div` display: flex; justify-content: space-between; - padding: 20px 0 0 0; + gap: 25px; + button { + width: 300px; + } ` const Error = styled.p` - color: ${(props) => props.theme.colors.dangerColor}; - font-size: ${(props) => props.theme.font.extraSmall}; + color: ${(props) => props.theme.colors.error}; + font-size: ${(props) => props.theme.font.xSmall}; + font-weight: 600; width: 100%; margin: 16px 0; + height: 20px; ` diff --git a/src/components/AuctionsOperations/AuctionsTransactions.tsx b/src/components/AuctionsOperations/AuctionsTransactions.tsx index 1286fabf..6070df60 100644 --- a/src/components/AuctionsOperations/AuctionsTransactions.tsx +++ b/src/components/AuctionsOperations/AuctionsTransactions.tsx @@ -2,8 +2,7 @@ import { useTranslation } from 'react-i18next' import styled from 'styled-components' import { useActiveWeb3React, handleTransactionError } from '~/hooks' -import TransactionOverview from '~/components/TransactionOverview' -import { returnConnectorName, COIN_TICKER } from '~/utils' +import { COIN_TICKER } from '~/utils' import { useStoreActions, useStoreState } from '~/store' import { AuctionEventType } from '~/types' import Button from '~/components/Button' @@ -13,7 +12,7 @@ import useGeb from '~/hooks/useGeb' const AuctionsTransactions = () => { const { t } = useTranslation() - const { connector, account, provider } = useActiveWeb3React() + const { account, provider } = useActiveWeb3React() const geb = useGeb() const { auctionModel: auctionsActions, popupsModel: popupsActions } = useStoreActions((state) => state) @@ -101,6 +100,7 @@ const AuctionsTransactions = () => { haiAmount: amount, collateral: tokenSymbol, collateralAmount: collateralAmount, + geb, }) } else if (isSettle) { await auctionsActions.auctionClaim({ @@ -108,6 +108,7 @@ const AuctionsTransactions = () => { auctionId, title: handleWaitingTitle, auctionType, + geb, }) } else if (isClaim) { await auctionsActions.auctionClaimInternalBalance({ @@ -125,6 +126,7 @@ const AuctionsTransactions = () => { title: handleWaitingTitle, auctionType, bid: amount, + geb, }) } } @@ -138,24 +140,14 @@ const AuctionsTransactions = () => { return ( - <> - - - - - -
-
- + + + + +
+
) } @@ -172,4 +164,8 @@ const Footer = styled.div` display: flex; justify-content: space-between; padding: 20px; + gap: 25px; + button { + width: 300px; + } ` diff --git a/src/components/AuctionsOperations/Results.tsx b/src/components/AuctionsOperations/Results.tsx index 69404c2b..b36556e8 100644 --- a/src/components/AuctionsOperations/Results.tsx +++ b/src/components/AuctionsOperations/Results.tsx @@ -141,14 +141,12 @@ const Item = styled.div` const Label = styled.div` font-size: ${(props) => props.theme.font.small}; - color: ${(props) => props.theme.colors.secondary}; letter-spacing: -0.09px; line-height: 21px; ` const Value = styled.div` font-size: ${(props) => props.theme.font.small}; - color: ${(props) => props.theme.colors.primary}; letter-spacing: -0.09px; line-height: 21px; font-weight: 600; diff --git a/src/components/AuctionsOperations/index.tsx b/src/components/AuctionsOperations/index.tsx index 7dc493db..aaeb1936 100644 --- a/src/components/AuctionsOperations/index.tsx +++ b/src/components/AuctionsOperations/index.tsx @@ -91,9 +91,12 @@ const AuctionsOperations = () => { export default AuctionsOperations const ModalContent = styled.div` - background: ${(props) => props.theme.colors.background}; - border-radius: ${(props) => props.theme.global.borderRadius}; - border: 1px solid ${(props) => props.theme.colors.border}; + border-radius: 8px; + margin-bottom: 15px; + background: white; + border: 3px solid #1a74ec; + box-shadow: 6px 6px 0px 0px #1a74ec, 5px 5px 0px 0px #1a74ec, 4px 4px 0px 0px #1a74ec, 3px 3px 0px 0px #1a74ec, + 2px 2px 0px 0px #1a74ec, 1px 1px 0px 0px #1a74ec; ` const Header = styled.div` diff --git a/src/components/BidLine.tsx b/src/components/BidLine.tsx index 15cb5b7d..056282f1 100644 --- a/src/components/BidLine.tsx +++ b/src/components/BidLine.tsx @@ -1,9 +1,9 @@ import dayjs from 'dayjs' import styled from 'styled-components' -import { ExternalLinkArrow } from '~/GlobalStyle' import { useActiveWeb3React } from '~/hooks' -import { ChainId, formatNumber, getEtherscanLink, returnWalletAddress } from '~/utils' +import { ChainId, formatNumber, getEtherscanLink } from '~/utils' +import { useAddress } from '~/hooks/useAddress' type Props = { eventType: string @@ -18,6 +18,7 @@ type Props = { const BidLine = ({ eventType, bidder, date, bid, buyAmount, buySymbol, sellSymbol, createdAtTransaction }: Props) => { const { chainId } = useActiveWeb3React() + const address = useAddress(bidder) const returnWad = (amount: string) => { if (!amount) return '0' @@ -30,7 +31,7 @@ const BidLine = ({ eventType, bidder, date, bid, buyAmount, buySymbol, sellSymbo {bidder && ( - {returnWalletAddress(bidder)} + {address} )} @@ -49,7 +50,7 @@ const BidLine = ({ eventType, bidder, date, bid, buyAmount, buySymbol, sellSymbo TX - {returnWalletAddress(createdAtTransaction)} + {useAddress(createdAtTransaction)} @@ -59,7 +60,9 @@ const BidLine = ({ eventType, bidder, date, bid, buyAmount, buySymbol, sellSymbo export default BidLine const Link = styled.a` - ${ExternalLinkArrow} + color: ${(props) => props.theme.colors.accent}; + line-height: ${(props) => props.theme.font.xSmall}; + font-weight: 500; ` const ListItemLabel = styled.div` @@ -68,14 +71,12 @@ const ListItemLabel = styled.div` display:block; margin-bottom:5px; font-weight:normal; - color: ${(props) => props.theme.colors.customSecondary}; `} ` const ListItem = styled.div` flex: 0 0 16.6%; - color: ${(props) => props.theme.colors.customSecondary}; - font-size: ${(props) => props.theme.font.extraSmall}; + font-size: ${(props) => props.theme.font.xSmall}; padding: 15px 10px; &:first-child { padding-left: 25px; @@ -89,7 +90,7 @@ const ListItem = styled.div` flex: 0 0 50%; min-width:50%; - font-size: ${(props) => props.theme.font.extraSmall}; + font-size: 16px; font-weight:900; `} ` diff --git a/src/components/BlockBodyContainer.tsx b/src/components/BlockBodyContainer.tsx index f716dbc5..b1d75ac2 100644 --- a/src/components/BlockBodyContainer.tsx +++ b/src/components/BlockBodyContainer.tsx @@ -1,13 +1,15 @@ import styled from 'styled-components' - -const BlockBodyContainer = () => { - return +interface Props { + header?: boolean +} +const BlockBodyContainer: React.FC = ({ header }) => { + return } export default BlockBodyContainer -const Container = styled.div` - position: fixed; +const Container = styled.div<{ header: boolean }>` + position: ${(props) => (props.header ? 'absolute' : 'fixed')}; top: 0; left: 0; height: 100%; diff --git a/src/components/Bolts/AddressCell.tsx b/src/components/Bolts/AddressCell.tsx new file mode 100644 index 00000000..b6c4c28d --- /dev/null +++ b/src/components/Bolts/AddressCell.tsx @@ -0,0 +1,68 @@ +import React, { useMemo } from 'react' +import { useAddress } from '~/hooks/useAddress' +import { returnWalletAddress } from '~/utils' +import Skeleton from 'react-loading-skeleton' +import { LeaderboardUser } from '~/model/boltsModel' +import styled from 'styled-components' + +interface AddressCellProps { + address: string + userBoltsDataAddress: string + data: LeaderboardUser[] +} + +const AddressCell: React.FC = ({ address, userBoltsDataAddress, data }) => { + const userInTop10 = useMemo(() => data.find((user) => user.rank <= 10 && user.address === address), [data, address]) + // Skip ENS check for users not in the top 10 + const resolvedAddress = useAddress(address, 0, !userInTop10) + + return ( +
+ {userBoltsDataAddress === address && ( + + YOU + + )} + {typeof resolvedAddress === 'string' && resolvedAddress.startsWith('0x') + ? returnWalletAddress(address, 2) + : resolvedAddress || } +
+ ) +} + +export default React.memo(AddressCell, (prevProps, nextProps) => { + return ( + prevProps.address === nextProps.address && + prevProps.userBoltsDataAddress === nextProps.userBoltsDataAddress && + prevProps.data === nextProps.data + ) +}) + +const Address = styled.span` + font-family: 'Open Sans', sans-serif; + font-weight: 700; + font-size: ${(props) => props.theme.font.xSmall}; + line-height: 21.79px; + letter-spacing: 0.05em; +` + +const Badge = styled.span` + background-color: #e2f1ff; + color: #1a74ec; + padding: 2px 8px; + border-radius: 4px; + margin-right: 8px; + font-family: 'Open Sans', sans-serif; + font-weight: 700; + font-size: 12px; +` diff --git a/src/components/Brand.tsx b/src/components/Brand.tsx index 5351c353..685cb31c 100644 --- a/src/components/Brand.tsx +++ b/src/components/Brand.tsx @@ -1,16 +1,19 @@ import styled from 'styled-components' -import Logo from '../assets/od-full-logo.svg' +import { useStoreState } from '~/store' +import DarkFullLogo from '~/assets/od-full-logo-dark.svg' +import LightFullLogo from '~/assets/od-full-logo-light.svg' +import { Link } from 'react-router-dom' -interface Props { - height?: number -} +const Brand = () => { + const isLightTheme = useStoreState((state) => state.settingsModel.isLightTheme) + + const LogoComponent = isLightTheme ? LightFullLogo : DarkFullLogo -const Brand = ({ height }: Props) => { return ( - - OD - + + OD + ) } diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 14dce1a5..2ee8f1fd 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -2,9 +2,9 @@ import React, { ReactNode } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' import classNames from 'classnames' - -import Arrow from './Icons/Arrow' import Loader from './Loader' +import Arrow from './Icons/Arrow' +import darkArrow from '../assets/dark-arrow.svg' interface Props extends React.HTMLAttributes { text?: string @@ -20,6 +20,7 @@ interface Props extends React.HTMLAttributes { isBordered?: boolean unstyled?: boolean arrowPlacement?: string + maxSize?: string children?: ReactNode } @@ -38,6 +39,7 @@ const Button = ({ unstyled, arrowPlacement = 'left', children, + maxSize, ...rest }: Props) => { const { t } = useTranslation() @@ -47,7 +49,6 @@ const Button = ({ secondary, dimmedNormal, }) - const returnType = () => { if (dimmed) { return ( @@ -60,13 +61,9 @@ const Button = ({ if (dimmedWithArrow) { return ( - {arrowPlacement === 'left' ? ( - {''} - ) : null} + {arrowPlacement === 'left' ? {''} : null} {text && t(text)} - {arrowPlacement === 'right' ? ( - {''} - ) : null} + {arrowPlacement === 'right' ? {''} : null} ) } else if (withArrow) { @@ -90,17 +87,23 @@ const Button = ({ isLoading={isLoading} onClick={onClick} > - {text && t(text)} + {isLoading && } + {!isLoading && text && t(text)} {children || null} - {isLoading && } ) } else { return ( - + {text && t(text)} {children || null} - {isLoading && } ) } @@ -125,41 +128,47 @@ const UnstyledContainer = styled.button<{ isLoading?: boolean }>` } &:disabled { - background: ${(props) => (props.isLoading ? props.theme.colors.placeholder : props.theme.colors.secondary)}; + background: ${(props) => (props.isLoading ? props.theme.colors.secondary : props.theme.colors.secondary)}; cursor: not-allowed; } ` -const Container = styled.button<{ isLoading?: boolean }>` +const Container = styled.button<{ isLoading?: boolean; maxSize?: string }>` outline: none; cursor: pointer; + width: 100%; min-width: 134px; border: none; box-shadow: none; - padding: 8px 30px; - line-height: 24px; - font-size: ${(props) => props.theme.font.small}; + padding: 10px 30px 10px 30px; + line-height: 20px; + font-size: 18px; + font-family: 'Open Sans', sans-serif; font-weight: 600; + display: flex; + align-items: center; + justify-content: center; color: ${(props) => props.theme.colors.neutral}; - background: ${(props) => props.theme.colors.blueish}; - border-radius: 50px; + background: ${(props) => props.theme.colors.gradientBg}; + border-radius: 3px; transition: all 0.3s ease; &.dimmedNormal { background: ${(props) => props.theme.colors.secondary}; } &.primary { - background: ${(props) => props.theme.colors.colorPrimary}; + background: ${(props) => props.theme.colors.gradientBg}; } &.secondary { - background: ${(props) => props.theme.colors.colorSecondary}; + background: ${(props) => props.theme.colors.secondary}; } &:hover { opacity: 0.8; } &:disabled { - background: ${(props) => (props.isLoading ? props.theme.colors.placeholder : props.theme.colors.secondary)}; + background: ${(props) => (props.isLoading ? props.theme.colors.placeholder : 'rgb(71, 86, 98, 0.4)')}; cursor: not-allowed; + color: #475662; } ` diff --git a/src/components/CheckBox.tsx b/src/components/CheckBox.tsx index 76cba1b3..f14ff846 100644 --- a/src/components/CheckBox.tsx +++ b/src/components/CheckBox.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { Check } from 'react-feather' import styled from 'styled-components' interface Props { @@ -13,11 +14,7 @@ const CheckBox = ({ checked, onChange }: Props) => { -
- - - -
+
{checked && }
) @@ -30,14 +27,6 @@ const CheckboxContainer = styled.label` cursor: pointer; ` -const Icon = styled.svg` - fill: none; - stroke: ${(props) => props.theme.colors.blueish}; - stroke-width: 2px; - visibility: hidden; - display: block; -` - const HiddenCheckbox = styled.input.attrs({ type: 'checkbox' })` border: 0; clip: rect(0 0 0 0); @@ -52,22 +41,16 @@ const HiddenCheckbox = styled.input.attrs({ type: 'checkbox' })` ` const StyledCheckbox = styled.div` - display: inline-block; - width: 20px; - height: 20px; - border-radius: 2.5px; transition: all 150ms; padding: 1px; + div { - border-radius: 2.5px; - border: 1px solid ${(props) => props.theme.colors.blueish}; - } - &.checked { - div { - border: 1px solid ${(props) => props.theme.colors.blueish}; - } - svg { - visibility: visible; - } + border-radius: 4px; + background: ${(props) => props.theme.colors.primary}; + width: 25px; + height: 25px; + display: flex; + justify-content: center; + align-items: center; } ` diff --git a/src/components/ConnectWalletStep.tsx b/src/components/ConnectWalletStep.tsx new file mode 100644 index 00000000..b5dc3e5d --- /dev/null +++ b/src/components/ConnectWalletStep.tsx @@ -0,0 +1,83 @@ +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' +import { useStoreActions } from '~/store' +import Button from './Button' +import closedVault from '../assets/closed-vault.webp' + +const ConnectWalletStep = () => { + const { popupsModel: popupsActions } = useStoreActions((state) => state) + const handleConnectWallet = () => popupsActions.setIsConnectorsWalletOpen(true) + + const { t } = useTranslation() + return ( + + + + + + {t('getting_started')} + {t('getting_started_text')} + {!!pendingTransactions.length || !!confirmedTransactions.length ? ( <> - {t('recent_transactions')} + {t('transaction_msg')} + + + + + {t('add_token_to_wallet')} + + + handleAddOD()} + className="group" + style={{ marginRight: 10 }} + > + + + OD + + + handleAddODG()} + className="group" + > + + + ODG + + + + + )} - - ADD TOKEN TO WALLET - - handleAddOD()} className="group"> - - X - - -
OD
-
-
- handleAddODG()} className="group"> - - X - - -
ODG
-
-
-
- - )} - - - - - - - popupsActions.setShowSideMenu(true)}> - - - - - - - -
+ + + popupsActions.setShowSideMenu(true)}> + + + + + + + + +
+ ) } export default Navbar -const SmallerIdenticonWrapper = styled.div<{ size?: number }>` +const Flex = styled.div` + align-items: center; display: flex; - & > img, - span, - svg { - height: ${({ size }) => (size ? size + 'px' : '12px')}; - width: ${({ size }) => (size ? size + 'px' : '12px')}; - } - - div { - height: ${({ size }) => (size ? size + 'px' : '12px')} !important; - width: ${({ size }) => (size ? size + 'px' : '12px')} !important; - svg { - rect { - height: ${({ size }) => (size ? size + 'px' : '12px')} !important; - width: ${({ size }) => (size ? size + 'px' : '12px')} !important; - } - } - } -` -const FixedContainer = styled.div` - position: absolute; - top: 84px; - right: 73px; - z-index: -1; + justify-content: center; ` const IdenticonWrapper = styled.div<{ size?: number }>` @@ -384,54 +393,69 @@ const IdenticonWrapper = styled.div<{ size?: number }>` } ` -const CamelotText = styled.div` - font-size: xx-small; - font-weight: 400; - color: #ffaf1d; -` - const PopupColumn = styled.div` text-align: end; ` const PopupWrapperTokenLink = styled.a` display: flex; - gap: 8px; + gap: 7px; font-size: ${(props) => props.theme.font.small}; font-weight: 600; color: ${(props) => props.theme.colors.neutral}; cursor: pointer; -` - -const PopupColumnWrapper = styled.div` - display: flex; - flex-direction: column; - gap: 8px; -` + align-items: center; + transition: all 0.3s ease; + border-radius: 4px; -const TokenTextWrapper = styled.div` - font-size: ${(props) => props.theme.font.extraSmall}; - text-align: left; - font-weight: 600; - color: #0079ad; - margin-bottom: 8px; + &:hover { + background-color: #f2f2f2; + } ` const screenWidth = '1073px' const Container = styled.div` display: flex; - height: 68px; + height: 77px; align-items: center; - justify-content: space-between; - padding: 40px 40px 0 40px; + justify-content: center; position: relative; z-index: 5; + width: 100%; + background: ${(props) => props.theme.colors.neutral}; + box-shadow: 0 8px linear-gradient(360deg, #d8e1ff -10.39%, #e2e8fb 0%); @media (max-width: ${screenWidth}) { padding: 0 20px; top: 0 !important; } + + &:after { + content: ''; + display: block; + position: absolute; + top: 100%; + left: 0; + width: 100%; + height: 8px; + background: linear-gradient(360deg, #d8e1ff -10.39%, #e2e8fb 0%); + z-index: 1; + } +` + +// Needed for a linear gradient shadow that doesn't bleed into the content +const ContainerShadowWrapper = styled(Flex)` + background: linear-gradient(360deg, #d8e1ff -10.39%, #e2e8fb 0%); + padding: 0 0 8px 0; +` + +const ContentWrapper = styled(Flex)` + height: 100%; + justify-content: space-between; + width: 100%; + max-width: 1360px; + padding: 0 15px; ` const MenuBtn = styled.div` @@ -442,13 +466,6 @@ const MenuBtn = styled.div` justify-content: center; display: none; cursor: pointer; - &:hover { - div { - div { - background: ${(props) => props.theme.colors.gradient}; - } - } - } @media (max-width: ${screenWidth}) { display: flex; @@ -489,13 +506,16 @@ const RightSide = styled.div` ` const HideMobile = styled.div` - height: -webkit-fill-available; + display: flex; + align-items: center; + justify-content: center; + height: 100%; @media (max-width: ${screenWidth}) { display: none; } ` -const Left = styled.div<{ isBigWidth?: boolean }>` +const Left = styled.div` display: flex; align-items: center; @@ -504,12 +524,6 @@ const Left = styled.div<{ isBigWidth?: boolean }>` } ` -const Flex = styled.div` - align-items: center; - display: flex; - justify-content: center; -` - const InnerBtn = styled(Flex)` div { display: block !important; @@ -520,43 +534,38 @@ const InnerBtn = styled(Flex)` } ` -const InnerBtnSmallerAddress = styled(Flex)` - div { - display: block !important; - margin-right: 2.5px; - svg { - top: 0 !important; - } - } -` - const OdButton = styled.button` - outline: none; - cursor: pointer; - border: none; - box-shadow: none; - padding: 8px 12px 8px 12px; - line-height: 24px; - font-size: ${(props) => props.theme.font.small}; - font-weight: 600; - color: ${(props) => props.theme.colors.neutral}; - background: ${(props) => props.theme.colors.colorPrimary}; - border-radius: 50px; - transition: all 0.3s ease; - margin-right: 15px; + position: relative; display: flex; - flex-direction: row; align-items: center; - justify-content: center; + justify-content: space-between; + cursor: pointer; + color: ${(props) => props.theme.colors.accent}; + width: 15vw; + max-width: 210px; + padding: 10px 18px 8px; + box-shadow: 0 4px ${(props) => props.theme.colors.primary}; + font-size: ${(props) => props.theme.font.xxSmall}; + font-weight: 700; + border-width: 1px; + border-color: ${(props) => props.theme.colors.primary}; + border-radius: 50px; + transition: all 0.15s ease; + box-sizing: border-box; + min-width: max-content; + width: auto; + height: 44px; &:hover { - opacity: 0.8; + transform: translateY(-0.5px); + box-shadow: 0 4.5px ${(props) => props.theme.colors.primary}; + background: ${(props) => props.theme.colors.neutral}33; } ` const RightPriceWrapper = styled.div` - position: relative; margin-right: auto; + position: relative; @media (max-width: ${screenWidth}) { display: none; @@ -564,7 +573,6 @@ const RightPriceWrapper = styled.div` ` const Price = styled.div` - position: relative; margin-right: auto; margin-left: 32px; @@ -573,69 +581,72 @@ const Price = styled.div` } ` -const TestTokenTextWrapper = styled.div` - font-size: ${(props) => props.theme.font.extraSmall}; - text-align: left; - font-weight: 600; - color: #0079ad; - word-wrap: break-word; - max-width: 100%; -` - -const TestTokenPopup = styled.div` - position: absolute; - max-width: 150px; - padding: 8px; - background: ${(props) => props.theme.colors.colorPrimary}; - border-radius: 8px; - top: 80px; -` - -const LiquidityInfoPopup = styled.div` +const InfoPopup = styled.div` position: absolute; - min-width: 190px; - padding: 8px; - background: ${(props) => props.theme.colors.colorPrimary}; - border-radius: 8px; - top: 45px; -` - -const PriceInfoPopup = styled.div` - position: absolute; - min-width: 160px; - padding: 8px; - background: ${(props) => props.theme.colors.colorPrimary}; - border-radius: 8px; - top: 45px; + background-color: white; + border-radius: 4px; + top: 85px; + width: 15vw; + max-width: 210px; + + &.wallet { + top: 68px; + right: 0; + } ` const PopupWrapperLink = styled.a` - display: flex; gap: 8px; font-size: ${(props) => props.theme.font.small}; font-weight: 600; color: ${(props) => props.theme.colors.neutral}; ` -const IconWrapper = styled.div` - display: flex; - align-items: center; -` - -const PoupColumn = styled.div` - text-align: end; -` - const ArrowWrapper = styled.div` margin-left: 8px; ` -const ClaimButton = styled(OdButton)`` +const ClaimButton = styled(OdButton)` + padding-left: 30px; + padding-right: 30px; +` const DollarValue = styled(OdButton)` display: flex; align-items: center; justify-content: space-between; - width: auto; white-space: nowrap; ` + +const TotalValue = styled(OdButton)`` + +const InfoPopUpHorizontalSeparator = styled.div` + width: 100%; + height: 1px; + background: ${(props) => props.theme.colors.accent}33; +` + +const InfoPopupContentWrapper = styled.div` + padding: 16px; +` + +const InfoPopUpText = styled.div` + font-size: ${(props) => props.theme.font.xSmall}; + line-height: ${(props) => props.theme.font.small}; + color: ${(props) => props.theme.colors.accent}; + font-weight: 500; + font-family: 'Barlow', sans-serif; +` +const InfoPopUpSubText = styled.div` + font-size: 13px; + line-height: ${(props) => props.theme.font.xSmall}; + font-weight: 500; + + a { + color: ${(props) => props.theme.colors.accent}; + } +` + +const OdBalanceWrapper = styled.span` + margin-right: 7px; +` diff --git a/src/components/NotificationPopup.tsx b/src/components/NotificationPopup.tsx index 0105e6ff..ac101e36 100644 --- a/src/components/NotificationPopup.tsx +++ b/src/components/NotificationPopup.tsx @@ -4,12 +4,12 @@ import styled from 'styled-components' import BellIcon from './Icons/BellIcon' const NotificationPopup = () => { - const wrapperRef = useRef(null) + const wrapperRef = useRef(null) const [isOpen, setIsOpen] = useState(false) - const handleClickOutside = (e: any) => { - const wrapper: any = wrapperRef.current - if (!wrapper.contains(e.target)) { + const handleClickOutside = (e: MouseEvent) => { + const wrapper = wrapperRef.current + if (wrapper && !wrapper.contains(e.target as Node)) { setTimeout(() => { setIsOpen(false) }, 10) @@ -21,7 +21,7 @@ const NotificationPopup = () => { return () => { document.removeEventListener('mousedown', handleClickOutside) } - }) + }, []) return ( @@ -71,7 +71,7 @@ const Menu = styled.div` top: 65px; left: -30px; width: 340px; - ${({ theme }) => theme.mediaWidth.upToExtraSmall` + ${({ theme }) => theme.mediaWidth.upToxSmall` width:290px; left:-143px; `} @@ -138,7 +138,7 @@ const Label = styled.div` const Date = styled.div` letter-spacing: 0.01px; color: ${(props) => props.theme.colors.secondary}; - font-size: ${(props) => props.theme.font.extraSmall}; + font-size: ${(props) => props.theme.font.xSmall}; ` const Value = styled.div` @@ -150,7 +150,7 @@ const Value = styled.div` ` const ExternalLink = styled.a` - font-size: ${(props) => props.theme.font.extraSmall}; + font-size: ${(props) => props.theme.font.xSmall}; background: ${(props) => props.theme.colors.gradient}; background-clip: text; -webkit-background-clip: text; diff --git a/src/components/SideMenu.tsx b/src/components/SideMenu.tsx index 5fbbda96..01d62ef7 100644 --- a/src/components/SideMenu.tsx +++ b/src/components/SideMenu.tsx @@ -3,7 +3,7 @@ import { CSSTransition } from 'react-transition-group' import { useWeb3React } from '@web3-react/core' import styled from 'styled-components' -import { amountToFiat, returnWalletAddress, getTokenLogo, formatDataNumber } from '~/utils' +import { amountToFiat, formatDataNumber, ETH_NETWORK } from '~/utils' import { useStoreActions, useStoreState } from '~/store' import ConnectedWalletIcon from './ConnectedWalletIcon' import NavLinks from './NavLinks' @@ -11,10 +11,18 @@ import Button from './Button' import ArrowDown from '~/components/Icons/ArrowDown' import Camelot from '~/components/Icons/Camelot' -import { fetchPoolData } from '@opendollar/sdk' -import { fetchAnalyticsData } from '@opendollar/sdk/lib/virtual/virtualAnalyticsData' import useGeb from '~/hooks/useGeb' import { BigNumber, ethers } from 'ethers' +import { X } from 'react-feather' +import useAnalyticsData from '~/hooks/useAnalyticsData' +import usePoolData from '~/hooks/usePoolData' +import TokenIcon from './TokenIcon' +import WalletIcon from '~/assets/wallet-icon.svg' +import DollarValueInner from './DollarValueInner' +import parachuteIcon from '../assets/parachute-icon.svg' +import { useAddress } from '~/hooks/useAddress' +import Skeleton from 'react-loading-skeleton' +import { GnosisSafe } from '@web3-react/gnosis-safe' const SideMenu = () => { const nodeRef = React.useRef(null) @@ -26,7 +34,8 @@ const SideMenu = () => { totalLiquidity: '', }) const popupRef = useRef(null) - const { isActive, account, chainId } = useWeb3React() + const priceRef = useRef(null) + const { isActive, account, chainId, connector } = useWeb3React() const dollarRef = useRef(null) const geb = useGeb() const odRef = useRef(null) @@ -38,8 +47,18 @@ const SideMenu = () => { connectWalletModel: connectWalletState, popupsModel: popupsState, } = useStoreState((state) => state) + const poolData = usePoolData() + const analyticsData = useAnalyticsData() + let address = useAddress(account) - const handleWalletConnect = () => popupsActions.setIsConnectorsWalletOpen(true) + const handleWalletConnect = () => { + if (isActive && account) { + popupsActions.setIsConnectedWalletModalOpen(true) + } + if (!(connector instanceof GnosisSafe)) { + return popupsActions.setIsConnectorsWalletOpen(true) + } + } const handleDollarClick = () => { setPopupVisibility(!isPopupVisible) @@ -66,8 +85,8 @@ const SideMenu = () => { } } - const handleClickOutsideOdWallet = (event: MouseEvent) => { - if (odRef.current && !odRef.current.contains(event.target as Node)) { + const handleClickOutsidePrice = (event: MouseEvent) => { + if (priceRef.current && !priceRef.current.contains(event.target as Node)) { setTokenPopupVisibility(false) } } @@ -93,7 +112,6 @@ const SideMenu = () => { await ethereum.request({ method: 'wallet_watchAsset', params: { - // @ts-ignore type: 'ERC20', options: { address: connectWalletModel.tokensData.OD.address, @@ -114,7 +132,6 @@ const SideMenu = () => { await ethereum.request({ method: 'wallet_watchAsset', params: { - // @ts-ignore type: 'ERC20', options: { address: connectWalletModel.tokensData.ODG.address, @@ -127,50 +144,50 @@ const SideMenu = () => { console.log('Error adding ODG to the wallet:', error) } } - useEffect(() => { - async function fetchData() { - if (geb) { - try { - const [poolData, analyticsData] = await Promise.all([fetchPoolData(geb), fetchAnalyticsData(geb)]) - - const formattedLiquidity = formatDataNumber( - ethers.utils - .parseEther(BigNumber.from(Math.floor(Number(poolData?.totalLiquidityUSD))).toString()) - .toString(), - 18, - 0, - true - ).toString() - - setState((prevState) => ({ - ...prevState, - odPrice: formatDataNumber(analyticsData.marketPrice, 18, 3, true, undefined, 2), - totalLiquidity: formattedLiquidity, - })) - } catch (error) { - console.error('Error fetching data:', error) - } - } + if (chainId !== 421614 && chainId !== 42161 && chainId !== 10) return + if (poolData && analyticsData) { + const formattedLiquidity = formatDataNumber( + ethers.utils + .parseEther( + BigNumber.from( + Math.floor(Number(poolData?.totalLiquidityUSD ? poolData.totalLiquidityUSD : '0')) + ).toString() + ) + .toString(), + 18, + 0, + true + ).toString() + + setState({ + odPrice: formatDataNumber( + analyticsData?.marketPrice ? analyticsData.marketPrice : '0', + 18, + 3, + true, + undefined, + 2 + ), + totalLiquidity: formattedLiquidity, + }) } - fetchData() document.addEventListener('mousedown', handleClickOutsideOdRef) document.addEventListener('mousedown', handleClickOutsideTestToken) - document.addEventListener('mousedown', handleClickOutsideOdWallet) + document.addEventListener('mousedown', handleClickOutsidePrice) return () => { // Cleanup the event listener on component unmount document.removeEventListener('mousedown', handleClickOutsideOdRef) document.removeEventListener('mousedown', handleClickOutsideTestToken) - document.removeEventListener('mousedown', handleClickOutsideOdWallet) + document.removeEventListener('mousedown', handleClickOutsidePrice) } - }, [geb]) + }, [geb, chainId, analyticsData, poolData]) useEffect(() => { setIsOpen(popupsState.showSideMenu) }, [popupsState.showSideMenu]) - return isOpen ? ( { popupsActions.setShowSideMenu(false)} /> popupsActions.setShowSideMenu(false)}> - X + - - {isActive && account ? ( + + {isActive && account && ( + { popupsActions.setIsConnectedWalletModalOpen(true) @@ -203,56 +216,38 @@ const SideMenu = () => { > -
{returnWalletAddress(account)}
+
{address || }
{`$ ${renderBalance()}`}
- ) : ( - -
@@ -44,13 +44,13 @@ const Container = styled.div` display: flex; align-items: center; padding: 10px 15px; - background-color: ${(props) => props.theme.colors.blueish}; + background-color: #1a74ec; svg { margin-right: 15px; } a { ${ExternalLinkArrow} - font-size: ${(props) => props.theme.font.extraSmall}; + font-size: ${(props) => props.theme.font.xSmall}; } ` diff --git a/src/components/TokenIcon.tsx b/src/components/TokenIcon.tsx new file mode 100644 index 00000000..322dc3f5 --- /dev/null +++ b/src/components/TokenIcon.tsx @@ -0,0 +1,7 @@ +import { getTokenLogo } from '~/utils' + +const TokenIcon = ({ token, width = '40px', height = '40px' }: { token: string; width?: string; height?: string }) => { + return {token} +} + +export default TokenIcon diff --git a/src/components/TokenInput.tsx b/src/components/TokenInput.tsx index 7385ac5a..8f9d829d 100644 --- a/src/components/TokenInput.tsx +++ b/src/components/TokenInput.tsx @@ -5,7 +5,7 @@ import styled from 'styled-components' import { NumericFormat, NumberFormatValues } from 'react-number-format' interface Props { - label: string + label: React.ReactNode rightLabel?: string token: { icon: string; name: string } | undefined iconSize?: string @@ -15,7 +15,7 @@ interface Props { disableMax?: boolean handleMaxClick?: () => void disabled?: boolean - maxText?: 'max' | 'min' + maxText?: 'MAX' | 'MIN' data_test_id?: string decimals?: number } @@ -31,7 +31,7 @@ const TokenInput = ({ disableMax, handleMaxClick, disabled, - maxText = 'max', + maxText = 'MAX', data_test_id, decimals = 4, }: Props) => { @@ -89,19 +89,17 @@ const TokenInput = ({ maxLength={length} minLength={1} disabled={disabled} - customInput={CustomInput} // You use your styled component as the actual input + customInput={CustomInput} data-test-id={data_test_id} isAllowed={validateInput} /> - - - - {rightLabel ? : null} + {disableMax || disabled ? null : {t(maxText)}} + + + {rightLabel ? : null} + ) } @@ -111,24 +109,25 @@ export default TokenInput const Container = styled.div`` const Label = styled.div` - line-height: 21px; - color: ${(props) => props.theme.colors.secondary}; - font-size: ${(props) => props.theme.font.small}; + line-height: 20px; + color: #1c293a; + font-size: 13px; + font-family: 'Open Sans', sans-serif; letter-spacing: -0.09px; text-transform: capitalize; display: flex; align-items: center; + padding-top: 4px; @media (max-width: 767px) { - font-size: ${(props) => props.theme.font.extraSmall}; + font-size: ${(props) => props.theme.font.xSmall}; } ` const Content = styled.div` - background: ${(props) => props.theme.colors.placeholder}; - border: 1px solid ${(props) => props.theme.colors.border}; - border-radius: 10px; + background: white; + border: 2px solid ${(props) => props.theme.colors.border}; + border-radius: 4px; transition: all 0.3s ease; - padding: 10px 20px; &.disabled { cursor: not-allowed; } @@ -140,19 +139,20 @@ export const Icon = styled.img` ` const CustomInput = styled.input` - font-size: ${(props) => props.theme.font.large}; - font-family: 'Montserrat', sans-serif; + font-size: 18px; + font-family: 'Open Sans', sans-serif; + font-weight: 700; transition: all 0.3s ease; width: 100%; border: none; - border-radius: 0; + border-radius: 4px; height: 36px; display: flex; align-items: center; padding: 0 0 0 5px; text-align: right; background: ${(props) => props.theme.colors.placeholder}; - color: ${(props) => props.theme.colors.primary}; + color: #1c293a; line-height: 24px; outline: none; @@ -164,14 +164,15 @@ const CustomInput = styled.input` const MaxBtn = styled.div` cursor: pointer; transition: all 0.3s ease; - background: transparent; - padding: 0px; - font-weight: 600; + background: #e2f1ff; + padding: 0 6px 0 6px; + font-weight: 700; + font-family: 'Open Sans', sans-serif; color: ${(props) => props.theme.colors.blueish}; - font-size: ${(props) => props.theme.font.extraSmall}; - border-radius: 0; - text-transform: capitalize; - margin-left: 3px; + border-radius: 4px; + text-transform: uppercase; + margin-left: 8px; + margin-right: 8px; ` const Flex = styled.div` @@ -186,7 +187,12 @@ const Flex = styled.div` const TokenBox = styled.div` display: flex; + background: #e2f1ff; align-items: center; - font-size: ${(props) => props.theme.font.medium}; - flex: 0 0 40%; + padding: 10px 10px; + font-size: 14px; + color: #1c293a; + font-family: 'Open Sans', sans-serif; + font-weight: 700; + flex: 0 0 30%; ` diff --git a/src/components/Transaction.tsx b/src/components/Transaction.tsx index d22b48d3..760fc17a 100644 --- a/src/components/Transaction.tsx +++ b/src/components/Transaction.tsx @@ -43,7 +43,9 @@ const Container = styled.div` const Text = styled.div` display: flex; + color: white; align-items: center; + font-size: ${(props) => props.theme.font.default} !important; ${ExternalLinkArrow} svg { width: 14px; diff --git a/src/components/TransactionOverview.tsx b/src/components/TransactionOverview.tsx index efd875e1..21029494 100644 --- a/src/components/TransactionOverview.tsx +++ b/src/components/TransactionOverview.tsx @@ -10,7 +10,7 @@ const TransactionOverview = ({ title, description, isChecked }: Props) => { return ( <> - + {isChecked ? ( <> @@ -41,20 +41,22 @@ const IconsHolder = styled.div` ` const Title = styled.div` - line-height: 24px; - font-weight: 600; - text-align: center; - color: ${(props) => props.theme.colors.primary}; - font-size: 14px; + line-height: 38.4px; + font-weight: 700; + font-family: 'Barlow', sans-serif; + text-align: left; + color: white; + font-size: 28px; letter-spacing: -0.18px; margin-top: 20px; ` const Description = styled.div` - line-height: 21px; + line-height: 27px; letter-spacing: -0.09px; - font-size: ${(props) => props.theme.font.extraSmall}; - color: ${(props) => props.theme.colors.secondary}; - text-align: center; + font-family: 'Open Sans', sans-serif; + font-size: 18px; + color: white; + text-align: left; margin-top: 4px; margin-bottom: 20px; ` diff --git a/src/components/VaultBlock.tsx b/src/components/VaultBlock.tsx index d6f2aab9..dc37c457 100644 --- a/src/components/VaultBlock.tsx +++ b/src/components/VaultBlock.tsx @@ -3,61 +3,74 @@ import styled from 'styled-components' import { returnState, COIN_TICKER, getTokenLogo, formatWithCommas } from '~/utils' -const VaultBlock = ({ ...props }) => { +interface VaultBlockProps { + id: string + riskState: number + collateralName: string + collateral: string + totalDebt: string + collateralRatio: string + liquidationPrice: string + className?: string + internalCollateralBalance: string +} + +const VaultBlock = ({ + id, + riskState, + collateralName, + collateral, + totalDebt, + collateralRatio, + liquidationPrice, + className, + internalCollateralBalance, +}: VaultBlockProps) => { + const stateClass = returnState(riskState) ? returnState(riskState).toLowerCase() : 'dimmed' + return ( - - - + + + - {props.collateralName} + {collateralName} - {`Vault #${props.id}`} + + Vault #{id} + - - {formatWithCommas(props.collateral)} + + {formatWithCommas(collateral)} - {formatWithCommas(props.totalDebt)} + {formatWithCommas(totalDebt)} - {`${formatWithCommas(props.collateralRatio)}%`} + {`${formatWithCommas(collateralRatio)}%`} - ${formatWithCommas(props.liquidationPrice)} + ${formatWithCommas(liquidationPrice)} - + - {' '} -
{returnState(props.riskState) ? returnState(props.riskState) : 'Closed'}
+
{returnState(riskState) ? returnState(riskState) : 'Closed'}
+ {Number(internalCollateralBalance) > 0 && ( + + + {formatWithCommas(internalCollateralBalance)} + + )}
@@ -75,19 +88,26 @@ const Container = styled.div` ` const BlockContainer = styled.div` - padding: 20px; - border-radius: 15px; - margin-bottom: 15px; - background: ${(props) => props.theme.colors.colorPrimary}; + border-radius: 4px; + margin-bottom: 29px; + background: white; + box-shadow: 0px 4px 6px 0px #0d4b9d33; position: relative; + display: flex; + flex-direction: column; &.empty { - background: #1e3b58; + background: white; } ` const BlockHeader = styled.div` display: flex; justify-content: space-between; + border-bottom: 1px solid #1c293a33; + padding-left: 34px; + padding-top: 22px; + padding-bottom: 11px; + padding-right: 34px; ` const Wrapper = styled.div` @@ -95,8 +115,10 @@ const Wrapper = styled.div` justify-content: flex-end; align-items: center; color: #dadada; - font-size: 14px; - font-weight: 400; + font-size: ${(props) => props.theme.font.default}; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 2px; ` const SafeInfo = styled.div` @@ -113,48 +135,33 @@ const SafeInfo = styled.div` ` const SafeData = styled.div` - margin-left: 16px; + margin-left: 20px; ${({ theme }) => theme.mediaWidth.upToSmall` margin-left: 10px; `} ` const SafeTitle = styled.div` - font-size: ${(props) => props.theme.font.small}; - color: ${(props) => props.theme.colors.primary}; - letter-spacing: -0.33px; - line-height: 22px; - font-weight: 600; -` + font-size: ${(props) => props.theme.font.large}; + font-family: ${(props) => props.theme.family.headers}; + color: ${(props) => props.theme.colors.accent}; + font-weight: 700; -const Circle = styled.div` - width: 11px; - height: 11px; - border-radius: 50%; - background: ${(props) => props.theme.colors.successColor}; - margin-right: 5px; - cursor: pointer; - &.dimmed { - background: ${(props) => props.theme.colors.secondary}; - } - &.elevated { - background: ${(props) => props.theme.colors.yellowish}; - } - &.high { - background: ${(props) => props.theme.colors.dangerColor}; - } - &.liquidation { - background: ${(props) => props.theme.colors.dangerColor}; + span { + font-weight: 500; + color: ${(props) => props.theme.colors.primary}; } ` const Block = styled.div` display: flex; - position: absolute; - right: 7px; - top: 13px; + justify-content: space-between; + + padding-left: 34px; + padding-top: 19px; + padding-bottom: 22px; + padding-right: 34px; @media (max-width: 767px) { - position: static; display: block; margin-top: 10px; &:last-child { @@ -165,7 +172,6 @@ const Block = styled.div` const Item = styled.div` margin: 0 12px; - text-align: end; @media (max-width: 767px) { display: flex; width: auto; @@ -176,24 +182,37 @@ const Item = styled.div` margin-bottom: 0; } } + + &.low div:last-child { + color: #459d00; + } + + &.elevated div:last-child { + color: #ffaf1d; + } + + &.high div:last-child { + color: #e75966; + } + + &.liquidation div:last-child { + color: #e75966; + } ` const Label = styled.div` - font-size: 13px; - color: ${(props) => props.theme.colors.secondary}; - letter-spacing: -0.09px; - line-height: 21px; + font-size: ${(props) => props.theme.font.default}; + color: ${(props) => props.theme.colors.tertiary}; + font-weight: 400; @media (max-width: 767px) { font-size: ${(props) => props.theme.font.small}; } ` const Value = styled.div` - font-size: 13px; - color: ${(props) => props.theme.colors.primary}; - letter-spacing: -0.09px; - line-height: 21px; - font-weight: 600; + font-size: ${(props) => props.theme.font.default}; + color: ${(props) => props.theme.colors.accent}; + font-weight: 700; @media (max-width: 767px) { font-size: ${(props) => props.theme.font.small}; } diff --git a/src/components/VaultManager.tsx b/src/components/VaultManager.tsx index 655f8581..2c2fd551 100644 --- a/src/components/VaultManager.tsx +++ b/src/components/VaultManager.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { isAddress } from '@ethersproject/address' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import styled from 'styled-components' import { useStoreActions, useStoreState } from '~/store' @@ -18,7 +18,7 @@ const VaultManager = () => { const [error, setError] = useState('') const [value, setValue] = useState('') - const history = useHistory() + const navigate = useNavigate() const { popupsModel: popupsActions } = useStoreActions((state) => state) const { tokensData } = useStoreState((state) => state.connectWalletModel) @@ -46,7 +46,11 @@ const VaultManager = () => { return } popupsActions.setIsWaitingModalOpen(true) - history.push(`/${value}`) + if (window?.location) { + window.location.assign(`/${value}`) + } else { + navigate(`/${value}`) + } handleCancel() await timeout(3000) popupsActions.setIsWaitingModalOpen(false) @@ -68,8 +72,11 @@ const VaultManager = () => { {error && {error}}
-
) @@ -77,6 +84,18 @@ const VaultManager = () => { export default VaultManager +const ManageVaultButton = styled.div` + display: flex; + gap: 8px; + color: white; + font-size: ${(props) => props.theme.font.small}; + font-weight: 600; +` + +const WhiteButton = styled(Button)` + color: white; +` + const Body = styled.div` padding: 20px; ` @@ -88,8 +107,8 @@ const Footer = styled.div` ` const Error = styled.p` - color: ${(props) => props.theme.colors.dangerColor}; - font-size: ${(props) => props.theme.font.extraSmall}; + color: white; + font-size: ${(props) => props.theme.font.xSmall}; width: 100%; margin: 16px 0; ` @@ -110,7 +129,7 @@ const CustomInput = styled.input` const Label = styled.div` line-height: 21px; - color: ${(props) => props.theme.colors.secondary}; + color: white; font-size: ${(props) => props.theme.font.small}; letter-spacing: -0.09px; margin-bottom: 4px; diff --git a/src/components/VaultStats.tsx b/src/components/VaultStats.tsx index d88f1db1..755216ea 100644 --- a/src/components/VaultStats.tsx +++ b/src/components/VaultStats.tsx @@ -1,32 +1,47 @@ -import { useEffect, useMemo, useState } from 'react' +import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import { Info } from 'react-feather' +import { ExternalLink, Info } from 'react-feather' import Numeral from 'numeral' import { useTokenBalanceInUSD, useSafeInfo } from '~/hooks' import { formatNumber, formatWithCommas, + getEtherscanLink, getRatePercentage, ratioChecker, returnState, - returnTotalDebt + returnTotalDebt, } from '~/utils' import { useStoreState } from '~/store' import { Tooltip as ReactTooltip } from 'react-tooltip' //@ts-ignore import { generateSvg } from '@opendollar/svg-generator' - -const VaultStats = ({ isModifying, isDeposit }: { isModifying: boolean; isDeposit: boolean; isOwner: boolean }) => { +import { useWeb3React } from '@web3-react/core' +import { useAddress } from '~/hooks/useAddress' + +import useGeb from '~/hooks/useGeb' +import Skeleton from 'react-loading-skeleton' + +const VaultStats = ({ + isModifying, + isDeposit, + isOwner, +}: { + isModifying: boolean + isDeposit: boolean + isOwner: boolean +}) => { const { t } = useTranslation() + const { chainId } = useWeb3React() const { - totalDebt: newDebt, - totalCollateral: newCollateral, collateralRatio: newCollateralRatio, parsedAmounts, liquidationPrice: newLiquidationPrice, + account, } = useSafeInfo(isModifying ? (isDeposit ? 'deposit_borrow' : 'repay_withdraw') : 'info') + const geb = useGeb() const { safeModel: safeState } = useStoreState((state) => state) const { singleSafe, liquidationData } = safeState @@ -35,7 +50,11 @@ const VaultStats = ({ isModifying, isDeposit }: { isModifying: boolean; isDeposi const collateralLiquidationData = liquidationData!.collateralLiquidationData[singleSafe?.collateralName as string] - const totalDebtCalc = returnTotalDebt(singleSafe?.debt as string, collateralLiquidationData.accumulatedRate, true) as string + const totalDebtCalc = returnTotalDebt( + singleSafe?.debt as string, + collateralLiquidationData.accumulatedRate, + true + ) as string const totalDebt = formatWithCommas(totalDebtCalc, 3) @@ -46,26 +65,21 @@ const VaultStats = ({ isModifying, isDeposit }: { isModifying: boolean; isDeposi safeState.liquidationData!.collateralLiquidationData[collateralName].currentPrice.value ) const collateralInUSD = formatNumber((Number(collateralUnitPriceUSD) * Number(collateral)).toString()) - const collateralRatio = - Number(safeState.liquidationData!.collateralLiquidationData[collateralName].safetyCRatio) * 100 const liquidationPenalty = getRatePercentage( safeState.liquidationData!.collateralLiquidationData[collateralName].liquidationPenalty, 10 ) const ODPrice = singleSafe ? formatNumber(singleSafe.currentRedemptionPrice, 3) : '0' - const [svg, setSvg] = useState('') const statsForSVG = useMemo( () => ({ vaultID: singleSafe?.id, stabilityFee: - Math.floor( - Number( - getRatePercentage( - singleSafe?.totalAnnualizedStabilityFee ? singleSafe?.totalAnnualizedStabilityFee : '0', - 2 - ) + Number( + getRatePercentage( + singleSafe?.totalAnnualizedStabilityFee ? singleSafe?.totalAnnualizedStabilityFee : '0', + 4 ) ).toString() + '%', debtAmount: formatWithCommas(totalDebt) + ' OD', @@ -77,9 +91,8 @@ const VaultStats = ({ isModifying, isDeposit }: { isModifying: boolean; isDeposi [singleSafe, totalDebt, collateral, collateralName, safeState.liquidationData] ) - useEffect(() => { - setSvg(generateSvg(statsForSVG)) - }, [singleSafe, totalDebt, collateral, collateralName, safeState.liquidationData, statsForSVG]) + const svg = useMemo(() => generateSvg(statsForSVG), [statsForSVG]) + let address = useAddress(singleSafe?.ownerAddress || account!) const returnRedRate = () => { const currentRedemptionRate = singleSafe ? getRatePercentage(singleSafe.currentRedemptionRate, 10) : '0' @@ -101,6 +114,21 @@ const VaultStats = ({ isModifying, isDeposit }: { isModifying: boolean; isDeposi return false }, [isModifying, parsedAmounts.leftInput, parsedAmounts.rightInput]) + const handleClaimClick = async () => { + if (!account) return + try { + const proxy = await geb.getProxyAction(account) + await proxy.collectTokenCollateral( + geb.contracts.safeManager.address, + geb.contracts.tokenCollateralJoin[collateralName].address, + Number(singleSafe?.id), + Number(singleSafe?.internalCollateralBalance) + ) + } catch (e) { + console.debug(e, 'Error in claiming internal balance') + } + } + return ( <> @@ -112,8 +140,6 @@ const VaultStats = ({ isModifying, isDeposit }: { isModifying: boolean; isDeposi style={{ maxWidth: '100%', height: 'auto', - border: '1px solid #00374E', - borderRadius: '0px', }} dangerouslySetInnerHTML={{ __html: svg }} > @@ -124,97 +150,49 @@ const VaultStats = ({ isModifying, isDeposit }: { isModifying: boolean; isDeposi - - - - - - Debt Owed - {modified ? ( -
- After:{' '} - - {formatWithCommas(newDebt, 2)} - -
- ) : null} -
- - {formatWithCommas(totalDebt)} OD + + + + Debt Owed + + + + + {formatWithCommas(totalDebt)} OD ${formatWithCommas(totalDebtInUSD)} - -
- - - - - - Collateral Deposited - {modified ? ( -
- After:{' '} - - {formatWithCommas(newCollateral)} - -
- ) : ( - <> - )} -
- - {formatWithCommas(collateral)} {singleSafe?.collateralName} - ${formatWithCommas(collateralInUSD, 2, 2)} - -
- - - - - - Collateral Ratio (min {collateralRatio}%) -
- + + + + Collateral Deposited + - Safety:{' '} - {Number( - safeState.liquidationData!.collateralLiquidationData[collateralName] - .safetyCRatio - ) * 100} - % - {' '} -   - + + + + {formatWithCommas(collateral)} {singleSafe?.collateralName} + + ${formatWithCommas(collateralInUSD, 2, 2)} + + + + + Collateral Ratio + - Minimum:{' '} - {Number( - safeState.liquidationData!.collateralLiquidationData[collateralName] - .liquidationCRatio - ) * 100} - % - -
+ + + + + {singleSafe ? formatWithCommas(singleSafe?.collateralRatio) : '-'}% + {modified ? ( -
+ After:{' '} {formatWithCommas(newCollateralRatio)}% -
+ ) : ( <> )} -
- {singleSafe ? formatWithCommas(singleSafe?.collateralRatio) : '-'}% -
+
+ + Safety:{' '} + {Math.round( + Number( + safeState.liquidationData!.collateralLiquidationData[collateralName] + .safetyCRatio + ) * 100 + )} + % + {' '} +   + + Minimum:{' '} + {Math.round( + Number( + safeState.liquidationData!.collateralLiquidationData[collateralName] + .liquidationCRatio + ) * 100 + )} + % + +
+ + + + + NFV Owner + + + + + + {address ? ( + + {address} + + ) : ( + + )} + + + + {singleSafe?.collateralName} Price (Delayed) - {singleSafe?.collateralName} Price (Delayed) ${formatWithCommas(collateralUnitPriceUSD, 2, 2)} - + OD Redemption Price - OD Redemption Price ${ODPrice} - - - Liquidation Price {modified ? ( @@ -278,38 +316,58 @@ const VaultStats = ({ isModifying, isDeposit }: { isModifying: boolean; isDeposi ) : null} + + + ${singleSafe ? formatWithCommas(singleSafe.liquidationPrice, 2, 2) : '-'} + Liquidation Penalty - Liquidation Penalty {`${liquidationPenalty}%`} + Stability Fee - Stability Fee {`${ singleSafe?.totalAnnualizedStabilityFee - ? getRatePercentage(singleSafe?.totalAnnualizedStabilityFee, 2) + ? getRatePercentage(singleSafe?.totalAnnualizedStabilityFee, 3) : 0 }%`} + Annual Redemption Rate - Annual Redemption Rate {`${returnRedRate()}%`} + {Number(singleSafe?.internalCollateralBalance) > 0 ? ( + + Internal Balance + + + + CLAIM + + {formatWithCommas(singleSafe?.internalCollateralBalance || '0', 2, 2)} + + + ) : ( + <> + )}
@@ -321,12 +379,71 @@ const VaultStats = ({ isModifying, isDeposit }: { isModifying: boolean; isDeposi export default VaultStats +const StatsGrid = styled.div` + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(2, 1fr); + width: 100%; + white-space: nowrap; + + .sideNote { + font-size: 12px; + font-weight: 700; + color: ${(props) => props.theme.colors.primary}; + span { + &.green { + color: ${(props) => props.theme.colors.blueish}; + } + &.yellow { + color: ${(props) => props.theme.colors.yellowish}; + } + } + } +` + +const StatHeader = styled.div` + display: flex; + align-items: start; + justify-content: start; + width: 100%; + white-space: nowrap; +` + +const StatSection = styled.div` + display: flex; + flex-direction: column; + min-height: 98px; + align-items: start; + justify-content: start; + text-align: start; +` + +const StatTitle = styled.div` + font-size: 16px; + color: #475662; + margin-right: 10px; +` + +const StatValue = styled.div` + font-size: 18px; + font-weight: 700; + margin-top: 5px; +` + +const AccountLink = styled.a` + display: flex; + gap: 5px; + color: ${(props) => props.theme.colors.primary}; +` + const SVGContainer = styled.div` display: flex; align-items: center; justify-content: center; width: 100%; - height: 100%; + height: 420px; + max-width: 420px; + border-radius: 4px; position: relative; overflow: auto; scrollbar-width: none; @@ -338,8 +455,11 @@ const SVGContainer = styled.div` const Flex = styled.div` display: flex; + justify-content: space-between; @media (max-width: 767px) { flex-direction: column; + justify-items: center; + align-items: center; } ` @@ -352,7 +472,7 @@ const InnerLeft = styled.div` const Inner = styled.div` background: ${(props) => props.theme.colors.colorPrimary}; padding: 20px; - border-radius: 20px; + border-radius: 4px; height: 100%; display: flex; flex-direction: column; @@ -363,21 +483,27 @@ const Inner = styled.div` ` const Left = styled.div` - flex: 0 0 55%; - padding-right: 10px; + display: flex; + justify-content: space-between; margin-top: 20px; @media (max-width: 767px) { flex: 0 0 100%; padding-right: 0; } ` + const Right = styled.div` + background: white; + border-radius: 4px; flex: 0 0 45%; - padding-left: 10px; margin-top: 20px; + display: flex; + justify-content: center; + align-items: center; + max-width: 420px; @media (max-width: 767px) { flex: 0 0 100%; - padding-left: 0; + padding: 10px; } ` @@ -388,8 +514,8 @@ const Main = styled.div` ` const DollarValue = styled.div` - font-size: 13px; - color: ${(props) => props.theme.colors.blueish}; + font-size: 12px; + color: #475662; ` const Side = styled.div` @@ -397,8 +523,7 @@ const Side = styled.div` &:last-child { margin-bottom: 0; } - border-bottom: 1px solid #00587e; - padding-bottom: 4px; + border-bottom: 1px solid rgba(26, 116, 236, 0.3); @media (max-width: 767px) { padding-top: 4px; } @@ -408,10 +533,14 @@ const Side = styled.div` ` const SideTitle = styled.div` - color: ${(props) => props.theme.colors.secondary}; + color: #475662; + font-family: 'Open Sans', sans-serif; font-size: 16px; + margin-right: 4px; .sideNote { font-size: 12px; + font-weight: 700; + color: ${(props) => props.theme.colors.primary}; span { &.green { color: ${(props) => props.theme.colors.blueish}; @@ -422,20 +551,31 @@ const SideTitle = styled.div` } } ` + const SideValue = styled.div` margin-left: auto; text-align: right; - color: ${(props) => props.theme.colors.customSecondary}; + font-family: 'Open Sans', sans-serif; + font-weight: 700; + color: #475662; font-size: 16px; ` const InfoIcon = styled.div` cursor: pointer; svg { - fill: ${(props) => props.theme.colors.secondary}; - color: ${(props) => props.theme.colors.foreground}; - position: relative; - top: 4px; - margin-right: 5px; + border: none; + color: #475662; + margin-top: 4px; } ` + +const ClaimLink = styled.span` + cursor: pointer; + font-family: 'Open Sans', sans-serif; + font-size: ${(props) => props.theme.font.xxSmall}; + text-decoration: underline; + color: ${(props) => props.theme.colors.blueish}; + margin-left: 5px; + padding-top: 1px; +` diff --git a/src/components/WalletModal/Option.tsx b/src/components/WalletModal/Option.tsx index 97334426..8d4af901 100644 --- a/src/components/WalletModal/Option.tsx +++ b/src/components/WalletModal/Option.tsx @@ -40,13 +40,7 @@ export default function Option({ id: string }) { const content = ( - + {active ? ( @@ -89,7 +83,7 @@ const InfoCard = styled.button<{ active?: boolean }>` } ` -const OptionCard = styled(InfoCard as any)` +const OptionCard = styled(InfoCard)` display: flex; flex-direction: row; align-items: center; diff --git a/src/components/WalletModal/index.tsx b/src/components/WalletModal/index.tsx index f1893b97..72c33e71 100644 --- a/src/components/WalletModal/index.tsx +++ b/src/components/WalletModal/index.tsx @@ -31,52 +31,68 @@ const WALLET_VIEWS = { PENDING: 'pending', } +type ChainParams = { + [key: string]: { + chainId: string + chainName: string + nativeCurrency: { + name: string + symbol: string + decimals: number + } + rpcUrls: string[] + blockExplorerUrls: string[] + } +} + export async function checkAndSwitchMetamaskNetwork() { // @ts-ignore if (window.ethereum && window.ethereum.isMetaMask && typeof window.ethereum.request === 'function') { // @ts-ignore - const chainId = await window.ethereum.request({ method: 'net_version' }) - // Check if chain ID is same as REACT_APP_NETWORK_ID and prompt user to switch networks if not - if (chainId !== process.env.REACT_APP_NETWORK_ID && process.env.REACT_APP_NETWORK_ID === '42161') { + const currentChainId = await window.ethereum.request({ method: 'net_version' }) + const targetChainId = process.env.REACT_APP_NETWORK_ID as keyof ChainParams + + if (currentChainId === targetChainId) { + return + } + + const chainParams: ChainParams = { + '42161': { + chainId: '0xA4B1', + chainName: 'Arbitrum One', + nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 }, + rpcUrls: ['https://arbitrum-one.publicnode.com'], + blockExplorerUrls: ['https://arbiscan.io/'], + }, + '10': { + chainId: '0xA', + chainName: 'OP Mainnet', + nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 }, + rpcUrls: ['https://optimism-rpc.publicnode.com'], + blockExplorerUrls: ['https://optimistic.etherscan.io/'], + }, + '421614': { + chainId: '0x66EEE', + chainName: 'Arbitrum Sepolia', + nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 }, + rpcUrls: ['https://arbitrum-sepolia.blockpi.network/v1/rpc/public'], + blockExplorerUrls: ['https://sepolia.arbiscan.io/'], + }, + } + + const params = chainParams[targetChainId] + if (params) { try { // @ts-ignore - await window.ethereum.request({ - method: 'wallet_addEthereumChain', - params: [ - { - chainId: `0xA4B1`, - chainName: 'Arbitrum One', - nativeCurrency: { - name: 'ETH', - symbol: 'ETH', - decimals: 18, - }, - rpcUrls: ['https://arbitrum-one.publicnode.com'], - blockExplorerUrls: ['https://arbiscan.io/'], - }, - ], - }) - } catch (error) { - console.error('Failed to switch network', error) - } - } else { - try { + const currentChainId = await window.ethereum.request({ method: 'net_version' }) + const targetChainId = process.env.REACT_APP_NETWORK_ID as keyof ChainParams + if (currentChainId === targetChainId) { + return + } // @ts-ignore await window.ethereum.request({ method: 'wallet_addEthereumChain', - params: [ - { - chainId: `0x66EEE`, - chainName: 'Arbitrum Sepolia', - nativeCurrency: { - name: 'ETH', - symbol: 'ETH', - decimals: 18, - }, - rpcUrls: ['https://arbitrum-sepolia.blockpi.network/v1/rpc/public'], - blockExplorerUrls: ['https://sepolia.arbiscan.io/'], - }, - ], + params: [params], }) } catch (error) { console.error('Failed to switch network', error) @@ -114,6 +130,7 @@ export default function WalletModal() { // close modal when a connection is successful const activePrevious = usePrevious(isActive) const connectorPrevious = usePrevious(connector) + useEffect(() => { if ( isConnectorsWalletOpen && @@ -123,30 +140,52 @@ export default function WalletModal() { } }, [setWalletView, isActive, connector, isConnectorsWalletOpen, activePrevious, connectorPrevious]) + function getHeaderContent() { + if (process.env.REACT_APP_NETWORK_ID === '42161') { + return ( +
+ {t('not_supported')}{' '} + + Arbitrum One + +
+ ) + } else if (process.env.REACT_APP_NETWORK_ID === '10') { + return ( +
+ {t('not_supported')}{' '} + + OP Mainnet + +
+ ) + } else if (process.env.REACT_APP_NETWORK_ID === '420') { + return ( +
+ {t('not_supported')}{' '} + + OP Goerli + +
+ ) + } else { + return ( +
+ {t('not_supported')}{' '} + + Arbitrum Sepolia + +
+ ) + } + } function getModalContent() { return ( - × {String(chainId) !== process.env.REACT_APP_NETWORK_ID && chainId !== undefined ? ( <> {'Wrong Network'} - - {process.env.REACT_APP_NETWORK_ID === '42161' ? ( -
- {t('not_supported')}{' '} - - Arbitrum One - -
- ) : ( -
- {t('not_supported')}{' '} - - Arbitrum Sepolia - -
- )} -
+ {getHeaderContent()} ) : ( @@ -157,7 +196,6 @@ export default function WalletModal() {
) } - return ( props.theme.colors.neutral}; - &:hover { - cursor: pointer; - opacity: 0.6; - } -` - const Wrapper = styled.div` - margin: 0; - padding: 0; + background: linear-gradient(to bottom, #1a74ec, #6396ff); + border-radius: 2.43px; + padding: 1rem; width: 100%; - background: ${(props) => props.theme.colors.background}; - border-radius: 20px; + color: white; + font-family: 'Barlow', sans-serif; ` const HeaderRow = styled.div` - padding: 1rem 1rem; + text-align: center; font-weight: 800; + color: white; + font-family: 'Open Sans', sans-serif; ` const ContentWrapper = styled.div` @@ -226,8 +254,14 @@ const UpperSection = styled.div` const HoverText = styled.div` color: ${(props) => props.theme.colors.neutral}; position: relative; + font-size: 19.04px; + line-height: 25.93px; + font-weight: 700; top: 10px; :hover { cursor: pointer; } + padding-bottom: 24.89px; + margin-bottom: 24.89px; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); ` diff --git a/src/components/Web3ReactManager/index.tsx b/src/components/Web3ReactManager/index.tsx index be2c3b82..c8cf2117 100644 --- a/src/components/Web3ReactManager/index.tsx +++ b/src/components/Web3ReactManager/index.tsx @@ -19,9 +19,12 @@ import { useWeb3React } from '@web3-react/core' import { useEagerConnect, useInactiveListener } from '../../hooks' import { network } from '~/connectors/network' +import { IS_IN_IFRAME } from '~/utils' +import { GnosisSafe } from '@web3-react/gnosis-safe' +import { gnosisSafe } from '~/connectors/gnosisSafe' export default function Web3ReactManager({ children }: { children: JSX.Element }) { - const { isActive } = useWeb3React() + const { isActive, connector } = useWeb3React() // try to eagerly connect to an injected provider, if it exists and has granted access already const triedEager = useEagerConnect() @@ -42,12 +45,16 @@ export default function Web3ReactManager({ children }: { children: JSX.Element } } }, []) - // If the RPC connection isn't active, and we've tried connecting eagerly already, connect to RPC + // If the RPC connection isn't active, and we've tried connecting eagerly already, and we're not in Gnosis Safe context, connect to RPC useEffect(() => { - if (!isActive && triedEager) { + if (!isActive && triedEager && !(connector instanceof GnosisSafe) && !IS_IN_IFRAME) { void network.activate().catch(() => { console.debug('Failed to connect to network') }) + } else if (!isActive && triedEager && IS_IN_IFRAME) { + void gnosisSafe.activate().catch(() => { + console.debug('Failed to connect to gnosis safe') + }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) diff --git a/src/components/connectorCards/Card.tsx b/src/components/connectorCards/Card.tsx index 5007fea5..735bc540 100644 --- a/src/components/connectorCards/Card.tsx +++ b/src/components/connectorCards/Card.tsx @@ -21,14 +21,16 @@ import { WalletConnect as WalletConnectV2 } from '@web3-react/walletconnect-v2' import { CoinbaseWallet } from '@web3-react/coinbase-wallet' import { Network } from '@web3-react/network' import { Web3ReactHooks } from '@web3-react/core' -import { Chain } from '~/components/connectorCards/Chain' import { Status } from '~/components/connectorCards/Status' import styled from 'styled-components' import { useCallback, useEffect, useState } from 'react' import { getAddChainParameters } from '~/chains' import { GnosisSafe } from '@web3-react/gnosis-safe' +import { useStoreActions } from '~/store' interface Props { + userInitiatedConnection: boolean + onUserInitiatedConnection: () => void connector: MetaMask | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe activeChainId: ReturnType chainIds?: ReturnType[] @@ -49,34 +51,23 @@ function getName(connector: Connector) { return 'Unknown' } -const InfoCard = styled.button<{ active?: boolean }>` - background-color: ${(props) => props.theme.colors.background}; - padding: 1rem; - outline: none; - border: 1px solid ${(props) => props.theme.colors.border}; - border-radius: 12px; - width: 100% !important; - &:focus { - box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15); - background: ${(props) => props.theme.colors.placeholder}; - } -` - -const OptionCard = styled(InfoCard as any)` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - margin-top: 2rem; - padding: 1rem; -` - -export function Card({ connector, activeChainId, isActivating, isActive, error, setError }: Props) { +export function Card({ + userInitiatedConnection, + onUserInitiatedConnection, + connector, + activeChainId, + isActivating, + isActive, + error, + setError, +}: Props) { const [desiredChainId, setDesiredChainId] = useState(parseInt(process.env.REACT_APP_NETWORK_ID || '-1', 10)) + const { popupsModel: popupsActions } = useStoreActions((state) => state) const switchChain = useCallback( async (desiredChainId: number) => { setDesiredChainId(desiredChainId) + onUserInitiatedConnection() try { if ( @@ -90,20 +81,26 @@ export function Card({ connector, activeChainId, isActivating, isActive, error, } if (desiredChainId === -1) { + // Disconnect from any existing connections await connector.activate() + popupsActions.setIsConnectorsWalletOpen(false) } else if (connector instanceof WalletConnectV2 || connector instanceof Network) { await connector.activate(desiredChainId) + popupsActions.setIsConnectorsWalletOpen(false) } else { await connector.activate(getAddChainParameters(desiredChainId)) + popupsActions.setIsConnectorsWalletOpen(false) } setError(undefined) } catch (error) { - // @ts-ignore - setError(error) + if (error instanceof Error) { + setError(error) + } } }, - [connector, activeChainId, setError] + // eslint-disable-next-line react-hooks/exhaustive-deps + [connector, activeChainId, setError, onUserInitiatedConnection] ) /** @@ -118,19 +115,6 @@ export function Card({ connector, activeChainId, isActivating, isActive, error, return ( switchChain(desiredChainId)}> -
- {getName(connector)} -
- -
- -
+ + {getName(connector)} +
+ +
+
) } + +const InfoCard = styled.button<{ active?: boolean }>` + padding: 1.25rem; + width: 100% !important; +` + +const OptionCard = styled(InfoCard)` + display: flex; + flex-direction: row; + border: 2px solid white; + border-radius: 4px; + min-height: 107px; + align-items: center; +` + +const NetworkCard = styled.div` + display: flex; + flex-direction: column; + text-align: left; + margin-left: 19.4px; +` + +const NetworkHeader = styled.h3` + font-family: 'Barlow', sans-serif; + font-weight: 700; + color: white; + font-size: ${(props) => props.theme.font.large}; + line-height: 38.4px; +` diff --git a/src/components/connectorCards/Chain.tsx b/src/components/connectorCards/Chain.tsx index 052d60ef..235e60f3 100644 --- a/src/components/connectorCards/Chain.tsx +++ b/src/components/connectorCards/Chain.tsx @@ -16,6 +16,7 @@ import type { Web3ReactHooks } from '@web3-react/core' import { CHAINS } from '~/chains' +import styled from 'styled-components' export function Chain({ chainId }: { chainId: ReturnType }) { if (chainId === undefined) return null @@ -24,18 +25,26 @@ export function Chain({ chainId }: { chainId: ReturnType + Chain:{' '} {name} ({chainId}) - + ) } return ( -
+ Chain Id: {chainId} -
+ ) } + +const ChainHeader = styled.div` + font-weight: 400; + font-family: 'Open Sans', serif; + font-size: 16px; + line-height: 24px; + color: #e2f1ff; +` diff --git a/src/components/connectorCards/CoinbaseWalletCard.tsx b/src/components/connectorCards/CoinbaseWalletCard.tsx index b9b45984..b55c4767 100644 --- a/src/components/connectorCards/CoinbaseWalletCard.tsx +++ b/src/components/connectorCards/CoinbaseWalletCard.tsx @@ -21,7 +21,13 @@ import { Card } from './Card' const { useChainId, useAccounts, useIsActivating, useIsActive, useProvider } = hooks -export default function CoinbaseWalletCard() { +interface CoinbaseCardProps { + error: Error | undefined + setError: (error: Error | undefined) => void +} + +export default function CoinbaseWalletCard({ error, setError }: CoinbaseCardProps) { + const [userInitiatedConnection, setUserInitiatedConnection] = useState(false) const chainId = useChainId() const accounts = useAccounts() const isActivating = useIsActivating() @@ -30,21 +36,23 @@ export default function CoinbaseWalletCard() { const provider = useProvider() - const [error, setError] = useState(undefined) - - // attempt to connect eagerly on mount useEffect(() => { void coinbaseWallet.connectEagerly().catch(() => {}) }, []) + const handleUserInitiatedConnection = () => { + setUserInitiatedConnection(true) + } + return ( void +} + +export default function GnosisSafeCard({ error, setError }: GnosisSafeCardProps) { + const [userInitiatedConnection, setUserInitiatedConnection] = useState(false) const chainId = useChainId() const accounts = useAccounts() const isActivating = useIsActivating() @@ -29,29 +35,29 @@ export default function GnosisSafeCard() { const isActive = useIsActive() const provider = useProvider() - const ENSNames = useENSNames(provider) - const [error, setError] = useState(undefined) - - // attempt to connect eagerly on mount useEffect(() => { void gnosisSafe.connectEagerly().catch(() => { console.debug('Failed to connect eagerly to gnosis safe') }) }, []) + const handleUserInitiatedConnection = () => { + setUserInitiatedConnection(true) + } + return ( ) } diff --git a/src/components/connectorCards/MetaMaskCard.tsx b/src/components/connectorCards/MetaMaskCard.tsx index 81f38bec..d75cbfcb 100644 --- a/src/components/connectorCards/MetaMaskCard.tsx +++ b/src/components/connectorCards/MetaMaskCard.tsx @@ -22,7 +22,13 @@ import { useStoreActions } from '~/store' const { useChainId, useAccounts, useIsActivating, useIsActive, useProvider } = hooks -export default function MetaMaskCard() { +interface MetaMaskCardProps { + error: Error | undefined + setError: (error: Error | undefined) => void +} + +export default function MetaMaskCard({ error, setError }: MetaMaskCardProps) { + const [userInitiatedConnection, setUserInitiatedConnection] = useState(false) const chainId = useChainId() const accounts = useAccounts() const isActivating = useIsActivating() @@ -33,25 +39,27 @@ export default function MetaMaskCard() { const { popupsModel: popupsActions } = useStoreActions((state) => state) - const [error, setError] = useState(undefined) - useEffect(() => { - void metaMask.connectEagerly().catch(() => {}) + metaMask.connectEagerly().catch(() => {}) if (provider?.provider.isMetaMask && accounts) { popupsActions.setIsConnectorsWalletOpen(false) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [accounts]) + }, []) + + const handleUserInitiatedConnection = () => { + setUserInitiatedConnection(true) + } return ( . import type { Web3ReactHooks } from '@web3-react/core' +import styled from 'styled-components' +import { useStoreActions, useStoreState } from '~/store' +import { useEffect } from 'react' export function Status({ + userInitiatedConnection, isActivating, isActive, error, }: { + userInitiatedConnection: boolean isActivating: ReturnType isActive: ReturnType error?: Error }) { + const { popupsModel: popupsActions } = useStoreActions((state) => state) + const { popupsModel: popupsState } = useStoreState((state) => state) + + // Close the wallet modal if the user is connected and not changing wallets + useEffect(() => { + if (isActive) { + if (!popupsState.isChangeWalletActive) { + popupsActions.setIsConnectorsWalletOpen(false) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isActive]) + return ( -
- {error ? ( - <> - 🔴 {error.name ?? 'Error'} - {error.message ? `: ${error.message}` : null} - - ) : isActivating ? ( - <>🟡 Connecting - ) : isActive ? ( - <>🟢 Connected + + {isActive ? ( + <>Connected + ) : error ? ( + <> + ) : isActivating && userInitiatedConnection ? ( + <>Awaiting Connection... ) : ( - <>⚪️ Disconnected + <> )} -
+ ) } + +const StatusText = styled.div` + font-weight: 400; + font-family: 'Open Sans', serif; + font-size: 16px; + line-height: 24px; + color: #e2f1ff; +` diff --git a/src/components/connectorCards/WalletConnectV2Card.tsx b/src/components/connectorCards/WalletConnectV2Card.tsx index 017909c8..a39f7bb4 100644 --- a/src/components/connectorCards/WalletConnectV2Card.tsx +++ b/src/components/connectorCards/WalletConnectV2Card.tsx @@ -16,25 +16,36 @@ import { URI_AVAILABLE } from '@web3-react/walletconnect-v2' import { useEffect, useState } from 'react' - import { MAINNET_CHAINS } from '../../chains' import { hooks, walletConnectV2 } from '../../connectors/walletConnectV2' import { Card } from './Card' +import { useStoreActions } from '~/store' const CHAIN_IDS = Object.keys(MAINNET_CHAINS).map(Number) - const { useChainId, useAccounts, useIsActivating, useIsActive, useProvider } = hooks -export default function WalletConnectV2Card() { +interface WalletConnectV2CardProps { + error: Error | undefined + setError: (error: Error | undefined) => void +} + +export default function WalletConnectV2Card({ error, setError }: WalletConnectV2CardProps) { + const [userInitiatedConnection, setUserInitiatedConnection] = useState(false) const chainId = useChainId() const accounts = useAccounts() const isActivating = useIsActivating() - const isActive = useIsActive() - const provider = useProvider() + const { popupsModel: popupsActions } = useStoreActions((state) => state) - const [error, setError] = useState(undefined) + // attempt to connect eagerly on mount + useEffect(() => { + walletConnectV2 + .connectEagerly() + .then(() => userInitiatedConnection && popupsActions.setIsConnectorsWalletOpen(false)) + .catch(() => {}) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) // log URI when available useEffect(() => { @@ -43,20 +54,20 @@ export default function WalletConnectV2Card() { }) }, []) - // attempt to connect eagerly on mount - useEffect(() => { - walletConnectV2.connectEagerly().catch(() => {}) - }, []) + const handleUserInitiatedConnection = () => { + setUserInitiatedConnection(true) + } return ( { + test('decimal is inserted when started with two zeros', () => { + const input = '00' + const output = processInputValue(input) + expect(output).toBe('0.0') + }) + + test('should return period when given period bc it fails regex test', () => { + const input = '.01' + const output = processInputValue(input) + expect(output).toBe('.01') + }) + + test('should handle long string inputs without hanging or crashing', () => { + const longInput = '0.'.padEnd(10000, '0') + const start = performance.now() + const output = processInputValue(longInput) + const end = performance.now() + expect(output).toBe(longInput) + expect(end - start).toBeLessThan(1000) + }) + + test('should handle long string inputs without hanging or crashing ex 2', () => { + const longInput = 'asdfasslajdlSFSJ;DSLJDSASFd'.padEnd(10000, '0') + const start = performance.now() + const output = processInputValue(longInput) + const end = performance.now() + expect(output).toBe(longInput) + expect(end - start).toBeLessThan(1000) + }) + + test('should not allow random characters to pass regex test', () => { + const input = 'asdasknelfawleargraaragjklrgnargl' + const output = processInputValue(input) + expect(output).toBe(input) + }) +}) diff --git a/src/containers/Analytics/ContractsTable.tsx b/src/containers/Analytics/ContractsTable.tsx index 3b14333a..e4f8f11b 100644 --- a/src/containers/Analytics/ContractsTable.tsx +++ b/src/containers/Analytics/ContractsTable.tsx @@ -4,8 +4,6 @@ import { Tooltip as ReactTooltip } from 'react-tooltip' import { Container, Content, Head, Heads, HeadsContainer, List, ListItemLabel, SectionContent } from './DataTable' import { AddressLink } from '~/components/AddressLink' -import CopyIconBlue from '~/components/Icons/CopyIconBlue' -import { useState } from 'react' interface ContractsTableProps { title: string @@ -54,28 +52,11 @@ const reorderRows = (rows: string[][]) => { } export const ContractsTable = ({ title, colums, rows }: ContractsTableProps) => { - const [tooltips, setTooltips] = useState<{ [key: string]: string }>({}) const { chainId } = useWeb3React() const reorderedColumns = reorderColumns(colums) const reorderedRows = reorderRows(rows) - const handleCopyAddress = (address: string) => { - navigator.clipboard.writeText(address || '') - - setTooltips((prevTooltips) => ({ - ...prevTooltips, - [address]: 'Copied', - })) - - setTimeout(() => { - setTooltips((prevTooltips) => ({ - ...prevTooltips, - [address]: 'Copy', - })) - }, 2000) - } - return ( @@ -89,32 +70,31 @@ export const ContractsTable = ({ title, colums, rows }: ContractsTableProps) => {reorderedRows?.map((item, index) => ( - - {item?.map((value, valueIndex) => ( - - - - {reorderedColumns[valueIndex]} - - {valueIndex === 0 && <>{value}} - {valueIndex === 1 && {value}} - {valueIndex === 2 && ( - - - handleCopyAddress(value)} - > - - - - )} - - - ))} - - +
+ {item[0] !== 'proxyFactory' && ( + + {item?.map((value, valueIndex) => ( + + + + {reorderedColumns[valueIndex]} + + {valueIndex === 0 && <>{value}} + {valueIndex === 1 && {value}} + {valueIndex === 2 && ( + + + + )} + + + ))} + + + )} +
))}
@@ -159,6 +139,7 @@ const SList = styled(List)` @media (min-width: 783px) { div:nth-child(2) div { width: 100%; + font-weight: 500; } div:nth-child(2) { @@ -194,23 +175,20 @@ const SListItem = styled.div` text-align: start; text-overflow: ellipsis; font-size: 16px; - font-weight: 400; + font-weight: 700; border-radius: 4px; & a { - color: white; + color: ${(props) => props.theme.colors.primary}; font-size: 16px; - font-weight: 400; + font-weight: 700; } width: 174px; - color: ${(props) => props.theme.colors.customSecondary}; + color: ${(props) => props.theme.colors.tertiary}; padding: 15px 10px; &:first-child { padding: 15px 25px; } - &:nth-child(1) { - background-color: #002b40; - } ${({ theme }) => theme.mediaWidth.upToSmall` &:first-child { padding: 15px 20px; @@ -221,11 +199,3 @@ const SListItem = styled.div` min-width:50%; `} ` - -const WrapperIcon = styled.div` - display: flex; - justify-content: center; - margin-left: 16px; - width: 20px; - height: 20px; -` diff --git a/src/containers/Analytics/DataCard.tsx b/src/containers/Analytics/DataCard.tsx index f50ef2c0..eba60e06 100644 --- a/src/containers/Analytics/DataCard.tsx +++ b/src/containers/Analytics/DataCard.tsx @@ -1,62 +1,68 @@ +import { ReactNode, memo } from 'react' import { Info } from 'react-feather' import styled from 'styled-components' - -import { getTokenLogo } from '~/utils' +import Loader from '~/components/Loader' const images: { [key: string]: string } = { - ETH: require('../../assets/eth-icon.svg').default, - OD: getTokenLogo('OD'), - NFTS: require('../../assets/nfts-icon.svg').default, + lock: require('../../assets/stats-img-lock.webp'), + vault: require('../../assets/stats-img-vault.webp'), } - export interface DataCardProps { - image?: string title: string value: string description?: string - children?: React.ReactNode + bg?: 'light' | 'dark' + image?: string + children?: ReactNode } -const DataCard = ({ title, image, value, description, children }: DataCardProps) => { +const DataCard = memo(({ title, bg, image, value, description, children }: DataCardProps) => { return ( - - {description && ( - - - - )} - - {image && {image}} - - {title} - {value} - {children} + + <> + {description && ( + + + + )} + {image && ( + + {image} + + )} + {title} + {value ? value : } + {children} + ) -} +}) export default DataCard -const Block = styled.div` +const Block = styled.div<{ bg?: 'light' | 'dark'; children: ReactNode }>` position: relative; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 48px 0px; + border: ${(props) => (props.bg === 'light' ? '3px solid #1A74EC' : '3px solid #ffffff')}; + box-shadow: ${(props) => + props.bg === 'light' + ? `6px 6px 0px 0px #1A74EC, 5px 5px 0px 0px #1A74EC, 4px 4px 0px 0px #1A74EC, 3px 3px 0px 0px #1A74EC, 2px 2px 0px 0px #1A74EC, 1px 1px 0px 0px #1A74EC` + : `6px 6px 0px 0px #ffffff, 5px 5px 0px 0px #ffffff, 4px 4px 0px 0px #ffffff, 3px 3px 0px 0px #ffffff, + 2px 2px 0px 0px #ffffff, 1px 1px 0px 0px #ffffff`}; width: 100%; height: 100%; - border-radius: 20px; - background: ${(props) => props.theme.colors.colorPrimary}; + border-radius: 8px; + color: ${(props) => (props.bg === 'light' ? props.theme.colors.primary : 'white')}; + background: ${(props) => (props.bg === 'light' ? 'white' : props.theme.colors.gradientBg)}; ${({ theme }) => theme.mediaWidth.upToSmall` max-width: 100%; `} - - & img { - margin-bottom: 32px; - } ` export const InfoIcon = styled.div` @@ -72,18 +78,26 @@ export const InfoIcon = styled.div` } ` -const DataTitle = styled.div` - font-size: ${(props) => props.theme.font.extraSmall}; +const ImgContainer = styled.div` + height: 270px; + margin-bottom: 32px; +` + +const DataTitle = styled.div<{ bg?: 'light' | 'dark' }>` + font-size: ${(props) => props.theme.font.small}; text-transform: uppercase; font-weight: 600; margin-bottom: 8px; text-align: center; - color: ${(props) => props.theme.colors.customSecondary}; + color: ${(props) => (props.bg === 'light' ? props.theme.colors.accent : 'white')}; ` const DataValue = styled.div` - font-size: 24px; + font-size: 48px; + min-height: 72px; font-weight: 700; text-align: center; - line-height: 28.8px; + display: flex; + justify-content: center; + align-items: center; ` diff --git a/src/containers/Analytics/DataTable.tsx b/src/containers/Analytics/DataTable.tsx index 7507c8de..a45e97e5 100644 --- a/src/containers/Analytics/DataTable.tsx +++ b/src/containers/Analytics/DataTable.tsx @@ -1,3 +1,5 @@ +import { ReactNode } from 'react' +import { Info } from 'react-feather' import styled from 'styled-components' import { ExternalLinkArrow } from '~/GlobalStyle' @@ -9,6 +11,8 @@ export interface TableProps { interface HeadsContainerProps { index?: number + key?: string + children: ReactNode } interface ListItemProps { @@ -23,8 +27,16 @@ export const DataTable = ({ title, colums, rows }: TableProps) => { {colums?.map(({ name, description }, index) => ( - + {name} + {description && ( + + + + )} ))} @@ -32,14 +44,16 @@ export const DataTable = ({ title, colums, rows }: TableProps) => { {rows?.map((item, index) => ( - {item?.map((value, valueIndex) => ( - - - {colums[valueIndex].name} - {value} - - - ))} + {item?.map((value, valueIndex) => { + return ( + + + {colums[valueIndex]?.name} + {value} + + + ) + })} ))} @@ -73,19 +87,8 @@ export const Header = styled.div` ` export const InfoIcon = styled.div` - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - position: absolute; - right: 0; - top: -2px; - - svg { - fill: ${(props) => props.theme.colors.foreground}; - color: ${(props) => props.theme.colors.secondary}; - position: relative; - } + z-index: 0 !important; + margin-left: 10px; ` export const Content = styled.div` @@ -162,8 +165,8 @@ export const Heads = styled.div` z-index: 10; } - & div:first-child { - background-color: #001828; + & div { + background-color: ${(props) => props.theme.colors.background}; } ${({ theme }) => theme.mediaWidth.upToSmall` @@ -185,15 +188,15 @@ export const ListContainer = styled.div` export const Head = styled.p` /* flex: 0 0 16.6%; */ + display: flex; + align-items: center; font-size: 12px; width: 174px; - font-weight: 600; + font-weight: 400; + text-transform: uppercase; - color: #4a4d53; - padding-left: 10px; - &:first-child { - padding: 0 25px; - } + color: ${(props) => props.theme.colors.tertiary}; + padding-left: 18px; ` export const ListItemLabel = styled.span` @@ -203,18 +206,17 @@ export const ListItemLabel = styled.span` margin-bottom:5px; font-weight:normal; width: max-content; - color: ${(props) => props.theme.colors.customSecondary}; `} ` export const List = styled.div` display: flex; - border-radius: 4px; + border-radius: 0px; width: max-content; align-items: start; justify-content: space-between; - background: #002b40; + background: white; margin-bottom: 24px; & div:nth-child(1) div { @@ -225,7 +227,6 @@ export const List = styled.div` ${({ theme }) => theme.mediaWidth.upToSmall` flex-wrap:wrap; width: unset; - border:1px solid ${(props) => props.theme.colors.border}; margin-bottom:10px; &:last-child { margin-bottom:0; @@ -235,20 +236,26 @@ export const List = styled.div` ` export const ListItem = styled.div` - /* flex: 0 0 16.6%; */ width: 174px; - color: ${(props) => props.theme.colors.customSecondary}; - font-size: ${(props) => props.theme.font.extraSmall}; + color: ${(props) => props.theme.colors.tertiary}; + font-size: ${(props) => props.theme.font.xSmall}; + font-weight: 700; padding: 15px 10px; &:first-child { - padding: 15px 25px; + padding: 15px 19px; } &:nth-child(1) { - background-color: #002b40; + background-color: white; } - text-align: ${(props) => (props.index !== undefined && props.index <= 2 ? 'start' : 'end')}; + text-align: start; + + a { + color: ${(props) => props.theme.colors.primary}; + font-weight: 700; + font-size: ${(props) => props.theme.font.xSmall}; + } ${({ theme }) => theme.mediaWidth.upToSmall` &:first-child { @@ -259,7 +266,7 @@ export const ListItem = styled.div` flex: 0 0 50%; min-width:50%; - font-size: ${(props) => props.theme.font.extraSmall}; + font-size: 16px; font-weight:900; `} ` diff --git a/src/containers/Analytics/index.tsx b/src/containers/Analytics/index.tsx index c9b9839c..b0f95a7c 100644 --- a/src/containers/Analytics/index.tsx +++ b/src/containers/Analytics/index.tsx @@ -7,20 +7,21 @@ import DataCard, { DataCardProps } from './DataCard' import { DataTable, TableProps } from './DataTable' import { ContractsTable } from './ContractsTable' import { AddressLink } from '~/components/AddressLink' -import { fetchAnalyticsData } from '@opendollar/sdk/lib/virtual/virtualAnalyticsData' import { contractsDescriptions } from '~/utils/contractsDescription' import { formatDataNumber, - multiplyRates, multiplyWad, transformToWadPercentage, transformToAnnualRate, transformToEightHourlyRate, } from '~/utils' import useGeb from '~/hooks/useGeb' -import { fetchPoolData } from '@opendollar/sdk' import { BigNumber, ethers } from 'ethers' +import useAnalyticsData from '~/hooks/useAnalyticsData' +import usePoolData from '~/hooks/usePoolData' +import MetaTags from '~/components/MetaTags' +import metaInfo from '~/utils/metaInfo' interface AnalyticsStateProps { erc20Supply: string @@ -43,6 +44,8 @@ interface AnalyticsStateProps { const Analytics = () => { const geb = useGeb() const { chainId } = useWeb3React() + const analyticsData = useAnalyticsData() + const poolData = usePoolData() const [state, setState] = useState({ erc20Supply: '', globalDebt: '', @@ -66,6 +69,7 @@ const Analytics = () => { globalDebt, globalDebtCeiling, globalDebtUtilization, + surplusInTreasury, marketPrice, redemptionPrice, totalLiquidity, @@ -83,6 +87,8 @@ const Analytics = () => { colums: [ { name: 'Collateral' }, { name: 'ERC-20', description: 'Address of the ERC20 collateral token.' }, + { name: 'Collateral Join', description: 'Address of the collateral join contract.' }, + { name: 'Auction House', description: 'Address of the collateral auction house.' }, { name: 'Oracle', description: 'Delayed oracle address for the collateral.' }, { name: 'Delayed Price', @@ -95,11 +101,6 @@ const Analytics = () => { 'Next system price of the collateral, this value is already quoted, and will impact the system on the next price update.', }, { name: 'Stability Fee', description: 'Annual interest rate paid by Safe owners on their debt.' }, - { - name: 'Borrow Rate', - description: - 'Total annual interest paid by Safe owners on their debt, includes "Stability Fee" and "Annual Redemption Rate".', - }, { name: 'Total Debt', description: 'Total amount of OD minted per collateral.' }, { name: 'Debt Utilization', @@ -148,26 +149,27 @@ const Analytics = () => { }, [geb]) const totalCollateralLocked = { - image: 'ETH', + image: 'lock', title: 'Total Collateral Locked', value: totalCollateralSum, - description: 'Mock dada for Total Collateral Locked', + description: 'Total Collateral Locked', } const vaultNFTs = { - image: 'NFTS', + image: 'vault', title: 'Vault NFTs', value: totalVaults, description: 'Vault NFTs', } - const annualStabilityFee = { - title: 'Annual Stability fee', - value: '2.0%', - description: 'Mock dada for Annual Stability fee', - } + const circulation = { title: 'circulation', value: erc20Supply, description: 'Circulating supply of OD stablecoin' } - const circulation = { title: 'circulation', value: erc20Supply, description: 'Circulation' } + const surplusInTreasuryData: DataCardProps = { + title: 'Surplus in Treasury', + value: surplusInTreasury, + description: + "Total OD accrued by the system's stability fees. It's stored in the Stability Fee Treasury accountance", + } const liquidityUniswap = { title: 'OD/ETH Liquidity in Camelot', @@ -245,232 +247,197 @@ const Analytics = () => { vaultNFTs, ] - const systemRatesData = [annualStabilityFee, annualRedemptionRate, eightHourlyRedemptionRate] - const systemInfoData: DataCardProps[] = [ circulation, // feesPendingAuction, // totalFeesEarned, globalDebtUtilizationData, globalDebtData, + surplusInTreasuryData, ] const pricesData: DataCardProps[] = [ marketPriceData, // check for market price OD not OD redemptionPriceData, liquidityUniswap, + annualRedemptionRate, + eightHourlyRedemptionRate, // marketPriceODG, ] + //@to-do: Do not use GEB as a param in useEffect, it causes a lot of re-renders useEffect(() => { - async function fetchData() { - if (geb) { - try { - const [poolData, analyticsData] = await Promise.all([fetchPoolData(geb), fetchAnalyticsData(geb)]) - let totalLockedValue = BigNumber.from('0') - const formattedLiquidity = formatDataNumber( - ethers.utils - .parseEther(BigNumber.from(Math.floor(Number(poolData?.totalLiquidityUSD))).toString()) - .toString(), - 18, - 0, - true - ).toString() - - const colRows = Object.fromEntries( - Object.entries(analyticsData?.tokenAnalyticsData).map(([key, value]) => { - const lockedAmountInUsd = multiplyWad( - value?.lockedAmount?.toString(), - value?.currentPrice?.toString() - ) - totalLockedValue = totalLockedValue.add(lockedAmountInUsd) - return [ - key, - [ - key, - , - , - formatDataNumber(value?.currentPrice?.toString() || '0', 18, 2, true), - formatDataNumber(value?.nextPrice?.toString() || '0', 18, 2, true), - transformToAnnualRate(value?.stabilityFee?.toString() || '0', 27), - transformToAnnualRate( - multiplyRates( - value?.stabilityFee?.toString(), - analyticsData.redemptionRate?.toString() - ) || '0', - 27 - ), - formatDataNumber(value?.debtAmount?.toString() || '0', 18, 2, true, true), - transformToWadPercentage( - value?.debtAmount?.toString(), - value?.debtCeiling?.toString() - ), - formatDataNumber(value?.lockedAmount?.toString() || '0', 18, 2, false, true) + - ' ' + - key, - formatDataNumber( - multiplyWad(value?.lockedAmount?.toString(), value?.currentPrice?.toString()) || - '0', - 18, - 2, - true, - true - ), - transformToWadPercentage( - multiplyWad( - value?.debtAmount?.toString(), - analyticsData?.redemptionPrice?.toString() - ), - multiplyWad(value?.lockedAmount?.toString(), value?.currentPrice?.toString()) - ), - ], - ] - }) + if (geb && analyticsData?.tokenAnalyticsData && poolData?.totalLiquidityUSD) { + let totalLockedValue = BigNumber.from('0') + const formattedLiquidity = formatDataNumber( + ethers.utils + .parseEther(BigNumber.from(Math.floor(Number(poolData?.totalLiquidityUSD))).toString()) + .toString(), + 18, + 0, + true + ).toString() + const colRows = Object.fromEntries( + Object.entries(analyticsData?.tokenAnalyticsData).map(([key, value]) => { + const lockedAmountInUsd = multiplyWad( + value?.lockedAmount?.toString(), + value?.currentPrice?.toString() ) + totalLockedValue = totalLockedValue.add(lockedAmountInUsd) + + return [ + key, + [ + key, + , + , + , + , + formatDataNumber(value?.currentPrice?.toString() || '0', 18, 2, true), + formatDataNumber(value?.nextPrice?.toString() || '0', 18, 2, true), + transformToAnnualRate(value?.stabilityFee?.toString(), 27), + formatDataNumber(value?.debtAmount?.toString() || '0', 18, 2, true, true), + transformToWadPercentage(value?.debtAmount?.toString(), value?.debtCeiling?.toString()), + formatDataNumber(value?.lockedAmount?.toString() || '0', 18, 2, false, true) + ' ' + key, + formatDataNumber( + multiplyWad(value?.lockedAmount?.toString(), value?.currentPrice?.toString()) || '0', + 18, + 2, + true, + true + ), + transformToWadPercentage( + multiplyWad(value?.debtAmount?.toString(), analyticsData?.redemptionPrice?.toString()), + multiplyWad(value?.lockedAmount?.toString(), value?.currentPrice?.toString()) + ), + ], + ] + }) + ) - setState((prevState) => ({ - ...prevState, - erc20Supply: formatDataNumber(analyticsData.erc20Supply, 18, 0, true), - globalDebt: formatDataNumber(analyticsData.globalDebt, 18, 0, true), - globalDebtCeiling: formatDataNumber(analyticsData.globalDebtCeiling, 18, 0, true), - globalDebtUtilization: transformToWadPercentage( - analyticsData.globalDebt, - analyticsData.globalDebtCeiling - ), - surplusInTreasury: formatDataNumber(analyticsData.surplusInTreasury, 18, 0, true), - marketPrice: formatDataNumber(analyticsData.marketPrice, 18, 3, true, undefined, 4), - redemptionPrice: formatDataNumber(analyticsData.redemptionPrice, 18, 3, true, undefined, 4), - totalLiquidity: formattedLiquidity, - annualRate: transformToAnnualRate(analyticsData.redemptionRate, 27, 3), - eightRate: transformToEightHourlyRate(analyticsData.redemptionRate, 27, 3), - pRate: transformToAnnualRate(analyticsData.redemptionRatePTerm, 27), - iRate: transformToAnnualRate(analyticsData.redemptionRateITerm, 27), - colRows: Object.values(colRows), - totalVaults: analyticsData.totalVaults, - totalCollateralSum: formatDataNumber(totalLockedValue.toString(), 18, 2, true, true), - })) - } catch (error) { - console.error('Error fetching data:', error) - } - } + setState((prevState) => ({ + ...prevState, + erc20Supply: formatDataNumber(analyticsData.erc20Supply, 18, 2, true), + globalDebt: formatDataNumber(analyticsData.globalDebt, 18, 2, true), + globalDebtCeiling: formatDataNumber(analyticsData.globalDebtCeiling, 18, 0, true), + globalDebtUtilization: transformToWadPercentage( + analyticsData.globalDebt, + analyticsData.globalDebtCeiling + ), + surplusInTreasury: `${formatDataNumber(analyticsData.surplusInTreasury, 18, 0)} OD`, + marketPrice: formatDataNumber(analyticsData.marketPrice, 18, 3, true, undefined, 4), + redemptionPrice: formatDataNumber(analyticsData.redemptionPrice, 18, 3, true, undefined, 4), + totalLiquidity: formattedLiquidity, + annualRate: transformToAnnualRate(analyticsData.redemptionRate, 27, 3), + eightRate: transformToEightHourlyRate(analyticsData.redemptionRate, 27, 3), + pRate: transformToAnnualRate(analyticsData.redemptionRatePTerm, 27), + iRate: transformToAnnualRate(analyticsData.redemptionRateITerm, 27), + colRows: Object.values(colRows), + totalVaults: analyticsData.totalVaults, + totalCollateralSum: formatDataNumber(totalLockedValue.toString(), 18, 2, true, true), + })) } - - fetchData() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [geb]) + }, [geb, chainId, analyticsData, poolData]) return ( - -
- Stats - - {analiticsData && - analiticsData?.map((val, index) => ( + <> + + +
+ Stats + + {analiticsData && + analiticsData?.map((val, index) => ( + + ))} + + + {systemInfoData && ( + + System Info + + {systemInfoData.slice(0, 2).map((val, index) => { + return ( + + ) + })} + + + {systemInfoData.slice(2).map((val, index) => { + return ( + + ) + })} + + + )} + + Prices + + {pricesData.map((val, index) => ( ))} - - - {systemRatesData && ( - - System Rates - - - - - - - - - )} - {systemInfoData && ( - - System Info - - {systemInfoData.slice(0, 2).map((val, index) => { - return ( - - ) - })} - - - {systemInfoData.slice(2).map((val, index) => { - return ( - - ) - })} - - - )} - - Prices - - {pricesData.map((val, index) => ( - + + - ))} - - - - -
-
- Collaterals - - - +
+
+ Collaterals + + + + +
+ +
+ Contracts + {/* Contracts Table */} + - -
- -
- Contracts - {/* Contracts Table */} - -
-
+
+
+ ) } @@ -483,6 +450,7 @@ const TooltipWrapper = styled.div` ` const Container = styled.div` + font-family: 'Open Sans', sans-serif; max-width: 1380px; margin: 80px auto; padding: 0 15px; @@ -500,10 +468,6 @@ const AnaliticsTop = styled.div` gap: 24px; margin-bottom: 64px; - & div { - height: 231px; - } - ${({ theme }) => theme.mediaWidth.upToSmall` flex-wrap: wrap; `} @@ -528,19 +492,21 @@ const AnaliticsBottom = styled.div` justify-content: space-between; gap: 24px; - ${({ theme }) => theme.mediaWidth.upToSmall` + > div { + height: 241px; + flex: 1; + padding-left: 5px; + padding-right: 5px; + min-width: 250px; + } + + @media (max-width: 1250px) { flex-wrap: wrap; - `} + } ` -const LeftColumn = styled.div`` - const RightColumn = styled.div`` -const LeftTopRow = styled.div` - margin-bottom: 24px; -` - const FlexMultipleRow = styled.div` display: flex; gap: 24px; @@ -558,13 +524,15 @@ const FlexMultipleRow = styled.div` const Title = styled.h2` font-size: 34px; font-weight: 700; + font-family: 'Barlow', sans-serif; margin-bottom: 40px; + color: ${(props) => props.theme.colors.accent}; ` const SubTitle = styled.h3` font-size: 34px; font-weight: 700; - color: #0079ad; + color: ${(props) => props.theme.colors.accent}; margin-bottom: 16px; ` diff --git a/src/containers/Auctions/AuctionsList.tsx b/src/containers/Auctions/AuctionsList.tsx index d64cce74..3d784852 100644 --- a/src/containers/Auctions/AuctionsList.tsx +++ b/src/containers/Auctions/AuctionsList.tsx @@ -42,6 +42,7 @@ const AuctionsList = ({ type, selectedItem, setSelectedItem }: Props) => { const { proxyAddress, tokensData } = connectWalletState // auctions list + const collaterals = tokensData && Object.values(tokensData).filter((token) => token.isCollateral) const auctions = useAuctions(type, selectedItem) // handle clicking to claim @@ -70,7 +71,6 @@ const AuctionsList = ({ type, selectedItem, setSelectedItem }: Props) => { }) } - const collaterals = tokensData && Object.values(tokensData).filter((token) => token.isCollateral) const collateralsDropdown = collaterals?.map((collateral) => { return { name: collateral.symbol, icon: getTokenLogo(collateral.symbol) } }) @@ -130,8 +130,10 @@ const Container = styled.div` ` const Title = styled.div` - font-size: ${(props) => props.theme.font.default}; - font-weight: bold; + font-size: ${(props) => props.theme.font.large}; + font-weight: 700; + font-family: ${(props) => props.theme.family.headers}; + color: ${(props) => props.theme.colors.accent}; text-transform: capitalize !important; ` @@ -143,14 +145,14 @@ const Box = styled.div` const InfoBox = styled.div` display: flex; align-items: center; - justify-content: space-between; + justify-content: center; margin-bottom: 10px; button { min-width: 100px; padding: 4px 12px; margin-left: 30px; } - margin-bottom: 20px; + margin-bottom: 8px; span { margin-right: 20px; font-size: 12px; @@ -158,12 +160,11 @@ const InfoBox = styled.div` ` const NoData = styled.div` - border-radius: 15px; margin-bottom: 15px; - background: ${(props) => props.theme.colors.background}; - padding: 2rem 20px; text-align: center; - font-size: ${(props) => props.theme.font.small}; + font-size: ${(props) => props.theme.font.default}; + font-weight: 400; + color: ${(props) => props.theme.colors.accent}; ` const DropdownContainer = styled.div` margin-bottom: 20px; diff --git a/src/containers/Auctions/CollateralAuctions/CollateralAuctionBlock.tsx b/src/containers/Auctions/CollateralAuctions/CollateralAuctionBlock.tsx index 975b02ec..d18847e1 100644 --- a/src/containers/Auctions/CollateralAuctions/CollateralAuctionBlock.tsx +++ b/src/containers/Auctions/CollateralAuctions/CollateralAuctionBlock.tsx @@ -9,14 +9,12 @@ import { useStoreActions, useStoreState } from '~/store' import { ICollateralAuction } from '~/types' import { COIN_TICKER, floatsTypes, formatDataNumber, formatNumber, parseWad } from '~/utils' import Button from '~/components/Button' -import useGeb from '~/hooks/useGeb' -import { fetchAnalyticsData } from '@opendollar/sdk/lib/virtual/virtualAnalyticsData' +import useAnalyticsData from '~/hooks/useAnalyticsData' import { utils as gebUtils } from '@opendollar/sdk' type Props = ICollateralAuction & { isCollapsed: boolean } const CollateralAuctionBlock = (auction: Props) => { - const geb = useGeb() const { auctionId, isClaimed, remainingToRaiseE18, remainingCollateral, tokenSymbol, biddersList, isCollapsed } = auction @@ -29,27 +27,17 @@ const CollateralAuctionBlock = (auction: Props) => { } = useStoreState((state) => state) const [collapse, setCollapse] = useState(isCollapsed) + const analyticsData = useAnalyticsData() + const [marketPriceOD, setMarketPriceOD] = useState(BigNumber.from('1')) const odBalance = gebUtils.decimalShift(BigNumber.from(auction.amountToRaise), floatsTypes.WAD - floatsTypes.RAD) useEffect(() => { - const fetchODMarketPrice = async () => { - let analytics - if (geb) { - try { - analytics = await fetchAnalyticsData(geb) - } catch (e) { - console.error(e) - } - if (analytics) { - setMarketPriceOD(BigNumber.from(analytics.marketPrice)) - } - } + if (analyticsData && analyticsData.marketPrice) { + setMarketPriceOD(BigNumber.from(analyticsData.marketPrice.toString())) } - - fetchODMarketPrice() - }, [geb]) + }, [analyticsData]) const buySymbol = COIN_TICKER @@ -102,8 +90,8 @@ const CollateralAuctionBlock = (auction: Props) => { return ( {' '} - - -
- {error ? : null} - - Auctions - + + +
+ + ) +} + +const FlexMultipleRow = styled.div` + display: flex; + gap: 24px; + margin-bottom: 24px; + + ${({ theme }) => theme.mediaWidth.upToSmall` + display: block; + + & div { + margin-bottom: 24px; + } + `} +` + +const Container = styled.div` + margin: 80px auto; + max-width: 1362px; + + @media (max-width: 767px) { + margin: 50px auto; + padding: 0 10px; + } + color: ${(props) => props.theme.colors.accent}; +` + +// const MessageBox = styled.div` +// max-width: 800px; +// margin-left: auto; +// margin-right: auto; +// border-radius: 4px; +// background: ${(props) => props.theme.colors.gradientBg}; +// color: white; +// padding-left: 28px; +// display: flex; +// align-items: center; + +// & h3 { +// font-size: 32px; +// font-weight: 700; +// font-family: ${(props) => props.theme.family.headers}; +// margin-bottom: 10px; +// line-height: 36px; +// } + +// a { +// text-decoration: underline; +// color: white; +// } + +// @media (max-width: 767px) { +// display: flex; +// flex-direction: column; +// align-items: center; +// text-align: center; +// padding-left: 0; +// padding-bottom: 36px; +// padding-left: 25px; +// padding-right: 25px; +// border-radius: 0; +// } +// ` + +// const Text = styled.div` +// max-width: 400px; +// ` + +const Title = styled.h2` + font-size: 34px; + font-weight: 700; + font-family: ${(props) => props.theme.family.headers}; + color: ${(props) => props.theme.colors.accent}; + @media (max-width: 767px) { + text-align: center; + } +` + +const SubHeader = styled.h3` + text-transform: uppercase; + font-family: ${(props) => props.theme.family.headers}; + font-size: 22px; + font-weight: 700; + color: ${(props) => props.theme.colors.tertiary}; + margin-bottom: 20px; + @media (max-width: 767px) { + font-size: 18px; + text-align: center; + } +` + +const SectionHeader = styled.h2` + font-size: 34px; + font-weight: 700; + color: ${(props) => props.theme.colors.accent}; + margin-bottom: 20px; +` + +const Section = styled.div` + padding: 0 15px; + margin-bottom: 60px; + @media (max-width: 767px) { + padding: 0 10px; + } +` + +const BtnWrapper = styled.div` + width: max-content; + margin-right: auto; + margin-left: auto; + button { + text-transform: uppercase; + font-weight: 700; + font-size: 18px; + padding: 17px 30px; + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + } +` + +// const Link = styled.a`` + +export default Bolts diff --git a/src/containers/Bolts/leaderboard.css b/src/containers/Bolts/leaderboard.css new file mode 100644 index 00000000..1d680473 --- /dev/null +++ b/src/containers/Bolts/leaderboard.css @@ -0,0 +1,44 @@ +html { + font-family: 'Open Sans', sans-serif; + font-size: 18px; +} + +@media (max-width: 768px) { + table { + display: block; + width: 100%; + overflow-x: auto; + } + + thead { + display: none; + } + + tbody { + display: flex; + flex-direction: column; + } + + tr { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + border-bottom: none; + margin-bottom: 0; + } + + td { + display: inline-flex; + align-items: center; + justify-content: center; + border: none; + } + + .address { + letter-spacing: 0.1em; + line-height: 21.79px; + font-family: 'Open Sans', sans-serif; + color: #eeeeee; + } +} diff --git a/src/containers/Bolts/quests.tsx b/src/containers/Bolts/quests.tsx new file mode 100644 index 00000000..8e3d20a6 --- /dev/null +++ b/src/containers/Bolts/quests.tsx @@ -0,0 +1,378 @@ +import Button from '~/components/Button' +import { ExternalLink } from 'react-feather' +import styled from 'styled-components' +import zealyLogo from '~/assets/zealy.svg' +import galxeLogo from '~/assets/galxe.svg' +import camelotLogo from '~/assets/camelot.svg' +import odLogo from '~/assets/od-full-logo-light.svg' +import { useNavigate } from 'react-router-dom' +import turtleClubLogo from '~/assets/turtle-club.png' +import TokenIcon from '~/components/TokenIcon' + +const StyledAnchor = styled.a` + padding: 5px 5px; + color: ${(props) => props.theme.colors.primary}; +` + +const LogoText = styled.p` + letter-spacing: 2px; + display: flex; + align-items: center; +` + +const TurtleClubLogo = () => ( + + Turtle Club Turtle Club + +) + +const CamelotLogo = () => ( + + Camelot + +) + +const ZealyLogo = () => ( + + Zealy + ZEALY + +) + +const GalxeLogo = () => ( + + Galxe + +) + +const OpenDollarLogo = () => ( + + Open Dollar + +) + +const LinkIcon = styled(ExternalLink)` + margin-left: 10px; + width: 20px; + height: 20px; +` + +const TitleContainer = styled.div` + display: inline; + align-items: center; + + img { + display: inline; + vertical-align: sub; + } +` + +const QuestTitle = styled.div` + display: inline; + align-items: center; + + font-size: 26px; + font-family: ${(props) => props.theme.family.headers}; + color: ${(props) => props.theme.colors.accent}; + font-weight: 700; + + margin-right: 10px; + + span { + font-weight: 500; + color: ${(props) => props.theme.colors.primary}; + } +` + +const TokensGroup = styled.span` + display: inline; + flex-wrap: nowrap; + align-items: center; + white-space: nowrap; + img:not(:first-child) { + margin-left: -10px; + } + flex-shrink: 0; +` + +const InternalLinkButton = ({ url }: { url: string }) => { + const navigate = useNavigate() + const onClick = () => navigate(url) + + return ( + + ) +} + +const onClick = (url: string) => { + window.open(url, '_blank') +} + +export type BoltsEarnedData = { + [key: string]: string +} + +export type MultipliersData = { + [key: string]: string +} + +export const MULTIPLIERS = (multipliersData: MultipliersData) => [ + { + title: 'Turtle Club', + button: ( + + ), + text: `Become a Turtle Club member.`, + items: [ + { + title: 'Source', + status: , + }, + { title: 'Multiplier', status: '+10%' }, + { title: 'Status', status: multipliersData['TURTLE_CLUB'] || '-' }, + ], + }, + { + title: 'Genesis NFV User', + button: ( + + ), + text: `Hold the Genesis NFV.`, + items: [ + { + title: 'Source', + status: , + }, + { title: 'Multiplier', status: '+10%' }, + { title: 'Status', status: multipliersData['GENESIS_NFV'] || '-' }, + ], + }, + { + title: 'Genesis NFT Holder', + button: ( + + ), + text: ( +
+ Hold the + + Genesis NFT + + {'.'} +
+ ), + items: [ + { title: 'Source', status: 'NFTs2Me' }, + { title: 'Multiplier', status: '+7%' }, + { title: 'Status', status: multipliersData['GENESIS_NFT'] || '-' }, + ], + }, + { + title: 'ODOG NFT Holder', + button: ( + + ), + text: ( +
+ Hold the + + ODOG NFT + + {'.'} +
+ ), + + items: [ + { title: 'Source', status: 'Guild.xyz' }, + { title: 'Multiplier', status: '+3%' }, + { title: 'Status', status: multipliersData['OG_NFT'] || '-' }, + ], + }, + { + title: 'Community Goal: 20K ETH TVL', + button: '', + text: `Existing Bolts receive a 30% bonus when TVL reaches 20,000 ETH.`, + items: [ + { + title: 'Source', + status: , + }, + { title: 'Multiplier', status: '+30% one-time bonus' }, + { title: 'Status', status: multipliersData['ETH_TVL_20K'] || '-' }, + ], + }, +] + +export const QUESTS = (boltsEarnedData: BoltsEarnedData) => [ + { + title: ( + + Deposit Collateral + + + + + + + ), + button: , + text: 'Deposit collateral to earn Bolts daily. Genesis NFV users receive a 10% bonus.', + items: [ + { + title: 'Source', + status: , + }, + { title: 'Bolts', status: '500 per ETH' }, + { title: 'Earned', status: boltsEarnedData['COLLATERAL_DEPOSIT'] || '-' }, + ], + }, + { + title: ( + + Borrow OD + + + + + ), + button: , + text: 'Borrow OD from your NFV to earn Bolts daily. Genesis NFV users receive a 10% bonus.', + items: [ + { + title: 'Source', + status: , + }, + { title: 'Bolts', status: '1,000 per ETH' }, + { title: 'Earned', status: boltsEarnedData['DEBT_BORROW'] || '-' }, + ], + }, + { + title: ( + + Provide Liquidity ODG-ETH + + + + + + ), + button: ( + + ), + text: 'Provide liquidity to the ODG/ETH pair on Camelot to earn Bolts daily. NOTE: When depositing, you must select "Auto" mode and use Gamma to qualify.', + items: [ + { title: 'Source', status: }, + { title: 'Bolts', status: '2,000 per ETH' }, + { title: 'Earned', status: boltsEarnedData['ODG_ETH_LP'] || '-' }, + ], + }, + { + title: ( + + Provide Liquidity OD-ETH + + + + + + ), + button: ( + + ), + text: 'Provide liquidity to the OD/ETH pair on Camelot to earn Bolts daily. NOTE: When depositing, you must select "Auto" mode and use Gamma to qualify.', + items: [ + { title: 'Source', status: }, + { title: 'Bolts', status: '3,000 per ETH' }, + { title: 'Earned', status: boltsEarnedData['OD_ETH_LP'] || '-' }, + ], + }, + { + title: 'Galxe', + button: ( + <> + + + ), + text: 'Complete quests on Galxe.', + items: [ + { + title: 'Source', + status: ( + <> + + + ), + }, + { title: 'Bolts', status: '1 per Point' }, + { title: 'Earned', status: boltsEarnedData['GALXE'] || '-' }, + ], + }, + { + title: 'Zealy', + button: ( + <> + + + ), + text: 'Complete quests on Zealy.', + items: [ + { + title: 'Source', + status: ( + <> + + + ), + }, + { title: 'Bolts', status: '1 per Point' }, + { title: 'Earned', status: boltsEarnedData['ZEALY'] || '-' }, + ], + }, +] diff --git a/src/containers/Bridge/BridgeFundsForm.tsx b/src/containers/Bridge/BridgeFundsForm.tsx new file mode 100644 index 00000000..9e809152 --- /dev/null +++ b/src/containers/Bridge/BridgeFundsForm.tsx @@ -0,0 +1,337 @@ +import { useState, useEffect, useMemo } from 'react' +import styled from 'styled-components' +import { getChainId, getUserBalance, bridgeTokens, getTokenLogo, formatWithCommas } from '~/utils' +import { useStoreActions, useStoreState } from '~/store' +import Button from '~/components/Button' +import { ExternalLink, Info } from 'react-feather' +import { useWeb3React } from '@web3-react/core' +import { Tooltip as ReactTooltip } from 'react-tooltip' +import Loader from '~/components/Loader' +import OPTIMISM from '~/assets/optimism.svg' +import ETHEREUM from '~/assets/ethereum.svg' +import BASE from '~/assets/base.svg' +import POLYGON from '~/assets/polygon.svg' + +const chainMapping = { + Ethereum: 'Ethereum', + Optimism: 'Optimism', + Polygon: 'Polygon', + Base: 'Base', +} + +type SelectedChain = 'Ethereum' | 'Optimism' | 'Polygon' | 'Base' + +const BridgeFundsForm = () => { + const [clickedItem, setClickedItem] = useState('') + const [selectedToken, setSelectedToken] = useState('') + const [selectedChain, setSelectedChain] = useState('Ethereum') + const [balances, setBalances] = useState>({}) + const [loading, setLoading] = useState(true) + + const { + connectWalletModel: { tokensData }, + bridgeModel: { reason, toTokenSymbol }, + } = useStoreState((state) => state) + const { account } = useWeb3React() + + const { bridge } = useStoreActions((state) => state.bridgeModel) + + const fixedTokens = bridgeTokens[getChainId(selectedChain)].tokens + + const collaterals = useMemo(() => { + return tokensData ? Object.values(tokensData).filter((token) => token.isCollateral) : [] + }, [tokensData]) + + useEffect(() => { + if (collaterals.length > 0 && selectedToken === '') setSelectedToken(toTokenSymbol) + }, [collaterals, toTokenSymbol, selectedToken]) + + useEffect(() => { + if (!account) return + const fetchAllBalances = async () => { + setLoading(true) + const balancePromises = Object.keys(chainMapping).map(async (network) => { + const chainId = chainMapping[network as SelectedChain] + const { tokens, publicRPC } = bridgeTokens[getChainId(chainId)] + const fetchedBalances = await getUserBalance(tokens, account!, publicRPC) + return { network, balances: fetchedBalances } + }) + + const results = await Promise.all(balancePromises) + const newBalances = results.reduce((acc: Record, result) => { + if (result.balances) { + acc[result.network] = result.balances + } + + return acc + }, {}) + setBalances(newBalances) + setLoading(false) + } + fetchAllBalances() + }, [account]) + + const getBalance = (token: string) => { + const tokenBalances = balances[selectedChain] || [] + token = token.toLocaleLowerCase() + const balance = tokenBalances.find((b) => { + return b.name.toLowerCase() === token + }) + return balance ? formatWithCommas(balance.balance, 4) : '-' + } + + const getNetworkLogo = (network: string) => { + switch (network) { + case 'Optimism': + return + case 'Ethereum': + return + case 'Polygon': + return + case 'Base': + return + default: + return '' + } + } + return ( + + + +
+ Bridge + Select an asset to move to Arbitrum +
+ {reason ?? ''} + Assets on the Network + + {Object.keys(chainMapping).map((network) => ( + { + setSelectedChain(network as SelectedChain) + }} + selectedChain={selectedChain} + id={network} + > + {getNetworkLogo(network)} + {network} + + ))} + + + + {fixedTokens.map((token: any) => { + return ( + { + if (token.comingSoon) return + setSelectedToken(token.name) + setClickedItem(token) + }} + style={{ + backgroundColor: selectedToken === token.name ? '#1A74EC' : 'transparent', + color: + selectedToken === token.name && token.address !== '' + ? 'white' + : '#1A74EC', + }} + key={`bridge-${token.name}`} + token={selectedToken} + > + + + {token.name} + {['ETH', 'WETH'].includes(token.name) && ( + + )} + {token.name === 'pufETH' ? coming soon : null} + + + {account ? ( + !loading ? ( + {getBalance(token.name)} + ) : ( + + + + ) + ) : null} + + ) + })} + +
+ + +
+
+
+ ) +} + +export default BridgeFundsForm + +const Container = styled.div` + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + min-height: 80vh; + width: 100%; + padding: 0 10px; +` + +const Content = styled.div` + position: relative; + width: 100%; +` + +const DropDownContainer = styled.div` + box-shadow: 0px 4px 6px 0px #0d4b9d33; + + padding: 22px; + border-radius: 4px; + background: white; +` + +const Text = styled.p` + font-size: ${(props) => props.theme.font.default}; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 5px; +` + +const Header = styled.div` + margin-bottom: 20px; +` + +const Title = styled.h2` + font-family: ${(props) => props.theme.family.headers}; + font-size: 34px; + color: ${(props) => props.theme.colors.accent}; + font-weight: 700; +` +const SubTitle = styled.p` + font-size: ${(props) => props.theme.font.default}; + color: ${(props) => props.theme.colors.tertiary}; +` + +const Description = styled.div` + color: ${(props) => props.theme.colors.accent}; + font-size: ${(props) => props.theme.font.medium}; + font-weight: 700; + margin-bottom: 10px; +` + +const Table = styled.div` + border: 3px solid ${(props) => props.theme.colors.primary}; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +` + +const List = styled.div`` + +const Item = styled.div<{ token?: string }>` + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + padding: 10px 15px; + + span { + font-size: ${(props) => props.theme.font.xSmall}; + color: white; + background-color: ${(props) => props.theme.colors.primary}; + padding: 2px 10px; + border-radius: 4px; + } + + &.disabled { + user-select: none; + opacity: 0.5; + cursor: not-allowed; + background-color: #f5f5f5 !important; + } + + &:not(:last-child) { + border-bottom: 1px solid #1c293a33; + } +` + +const ButtonsRow = styled.div` + display: flex; + align-items: center; + column-gap: 10px; + + @media (max-width: 767px) { + flex-wrap: wrap; + column-gap: 0; + div { + flex: 1; + } + } +` + +const NetworkButton = styled.div<{ selectedChain: string; id: string }>` + background-color: ${(props) => + props.selectedChain === props.id ? props.theme.colors.primary : props.theme.colors.background}; + color: ${(props) => (props.selectedChain === props.id ? 'white' : props.theme.colors.accent)}; + border-bottom: none; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + padding: 10px 20px; + cursor: pointer; + display: flex; + gap: 5px; + justify-content: flex-start; + width: 150px; +` + +const LoaderContainer = styled.div` + span { + display: none; + } +` diff --git a/src/containers/Bridge/LiFiWidget.tsx b/src/containers/Bridge/LiFiWidget.tsx new file mode 100644 index 00000000..df2a539d --- /dev/null +++ b/src/containers/Bridge/LiFiWidget.tsx @@ -0,0 +1,65 @@ +// import { LiFiWidget, WidgetConfig } from '@lifi/widget' + +// const widgetConfig: WidgetConfig = { +// integrator: 'opendollar', +// variant: 'wide', +// subvariant: 'default', +// appearance: 'light', +// theme: { +// palette: { +// primary: { +// main: '#1a74ec', +// }, +// secondary: { +// main: '#6496ff', +// }, +// // common: { +// // white: '#ffffff', +// // }, +// background: { +// paper: '#ffffff', +// }, +// text: { +// primary: '#1c293a', +// secondary: '#56636e', +// }, +// // warning: { +// // main: '#fcbf3c', +// // }, +// // error: { +// // main: '#eb7680', +// // }, +// // info: { +// // main: '#1b74ec', +// // }, +// // success: { +// // main: '#459d00', +// // }, +// grey: { +// 200: '#ccd8e5', +// 300: '#ccd8e5', +// }, +// }, +// typography: { +// fontFamily: 'Open Sans, sans-serif', +// }, +// container: { +// boxShadow: '0px 8px 32px rgba(0, 0, 0, 0.08)', +// borderRadius: '16px', +// }, +// shape: { +// borderRadiusSecondary: 3, +// borderRadius: 3, +// }, +// }, +// walletConfig: { +// async onConnect() {}, +// }, +// } + +const Widget = () => { + return <> + // return +} + +export default Widget diff --git a/src/containers/Bridge/index.tsx b/src/containers/Bridge/index.tsx new file mode 100644 index 00000000..5af27665 --- /dev/null +++ b/src/containers/Bridge/index.tsx @@ -0,0 +1,24 @@ +import styled from 'styled-components' +import BridgeFundsForm from './BridgeFundsForm' +import MetaTags from '~/components/MetaTags' +import metaInfo from '~/utils/metaInfo' + +const Bridge = () => { + return ( + <> + + + + + + ) +} + +export default Bridge + +const Container = styled.div` + max-width: 800px; + min-width: 300px; + margin-left: auto; + margin-right: auto; +` diff --git a/src/containers/Deposit/DepositFunds.tsx b/src/containers/Deposit/DepositFunds.tsx index ae264ecb..ee8e01b4 100644 --- a/src/containers/Deposit/DepositFunds.tsx +++ b/src/containers/Deposit/DepositFunds.tsx @@ -2,9 +2,8 @@ import { useState, useMemo, useEffect } from 'react' import { ethers } from 'ethers' import { ArrowLeft, Info, AlertCircle } from 'react-feather' import styled from 'styled-components' -import { useHistory } from 'react-router-dom' +import { useNavigate, useParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { useTheme } from 'styled-components' import { useTokenApproval, useProxyAddress, useActiveWeb3React } from '~/hooks' import { getTokenLogo, formatWithCommas, formatNumber } from '~/utils' import { useStoreState, useStoreActions } from '~/store' @@ -13,19 +12,17 @@ import { ApprovalState } from '~/hooks' import TokenInput from '~/components/TokenInput' import Button from '~/components/Button' -const DepositFunds = ({ ...props }) => { +const DepositFunds = () => { const { t } = useTranslation() - const { colors } = useTheme() - const history = useHistory() + const navigate = useNavigate() + const { token } = useParams<{ token: string }>() + const tokenSymbol = token?.toUpperCase() const proxyAddress = useProxyAddress() const { account, isActive } = useActiveWeb3React() const [depositAmount, setDepositAmount] = useState('') - const tokenPath = props.match.params.token as string - const tokenSymbol = tokenPath.toUpperCase() - const { safeModel: { liquidationData }, connectWalletModel: { tokensData, tokensFetchedData, isWrongNetwork }, @@ -33,11 +30,11 @@ const DepositFunds = ({ ...props }) => { const { popupsModel: popupsActions } = useStoreActions((state) => state) - const tokenData = tokensData?.[tokenSymbol] - const tokenFetchedData = tokensFetchedData?.[tokenSymbol] + const tokenData = tokensData?.[tokenSymbol || ''] + const tokenFetchedData = tokensFetchedData?.[tokenSymbol || ''] const depositAssetUSDValue = formatNumber( - liquidationData?.collateralLiquidationData?.[tokenSymbol]?.currentPrice?.value || '0' + liquidationData?.collateralLiquidationData?.[tokenSymbol || '']?.currentPrice?.value || '0' ) const depositAssetBalance = useMemo( @@ -69,10 +66,10 @@ const DepositFunds = ({ ...props }) => { approvalState !== ApprovalState.APPROVED || Number(depositAmount) === 0 || isWrongNetwork useEffect(() => { - if (!isEmptyObject(tokensData) && !tokensData?.[tokenSymbol]?.isCollateral) { - history.push('/404') + if (!isEmptyObject(tokensData) && !tokensData?.[tokenSymbol || '']?.isCollateral) { + navigate('/404') } - }, [history, tokenSymbol, tokensData]) + }, [tokenSymbol, tokensData, navigate]) // TODO: Implement onDeposit function once contracts are ready const onDeposit = () => console.log('Deposit') @@ -84,7 +81,7 @@ const DepositFunds = ({ ...props }) => { - history.goBack()} cursor="pointer" /> + navigate(-1)} cursor="pointer" /> Deposit funds @@ -117,20 +114,14 @@ const DepositFunds = ({ ...props }) => { {t('tokens_will_be_unlocked')} - + {shortStringDate(1677653610000)} - + {t('deposit_funds_warning')} diff --git a/src/containers/Earn/EarnBlock.tsx b/src/containers/Earn/EarnBlock.tsx new file mode 100644 index 00000000..83f92c1d --- /dev/null +++ b/src/containers/Earn/EarnBlock.tsx @@ -0,0 +1,224 @@ +import styled from 'styled-components' +import { Tooltip as ReactTooltip } from 'react-tooltip' + +const Link = styled.a`` + +const EarnBlock = ({ + status, + url, + apy, + tvl, + title, + rewardToken1Symbol, + rewardToken2Symbol, +}: { + status: string + url: string + apy: string + tvl: string + title: string | JSX.Element + rewardToken1Symbol: string + rewardToken2Symbol: string +}) => { + return ( + + + + + {title} + + + + + + + + {status} + + + + + {tvl} + + + + {apy} + + + + {`${rewardToken1Symbol}, ${rewardToken2Symbol}`} + + + + + + ) +} + +export default EarnBlock + +const BlockContainer = styled.div` + border-radius: 4px; + margin-bottom: 29px; + background: white; + box-shadow: 0px 4px 6px 0px #0d4b9d33; + position: relative; + display: flex; + flex-direction: column; + &.empty { + background: white; + } +` + +const BlockHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #1c293a33; + padding-left: 34px; + padding-top: 22px; + padding-bottom: 11px; + padding-right: 34px; + + ${({ theme }) => theme.mediaWidth.upToSmall` + display: flex; + flex-direction: column; + align-items: flex-end; + `} +` + +const PoolInfo = styled.div` + display: flex; + align-items: center; + svg { + border-radius: ${(props) => props.theme.global.borderRadius}; + border: 1px solid ${(props) => props.theme.colors.border}; + ${({ theme }) => theme.mediaWidth.upToSmall` + width: 25px; + height: 25px; + `} + } + + ${({ theme }) => theme.mediaWidth.upToSmall` + display: flex; + flex-direction: column; + `} +` + +const PoolData = styled.div` + display: flex; + font-size: ${(props) => props.theme.font.large}; + font-family: ${(props) => props.theme.family.headers}; + color: ${(props) => props.theme.colors.accent}; + font-weight: 700; + + img { + margin-left: 22px; + } + span { + font-weight: 500; + color: ${(props) => props.theme.colors.primary}; + } + ${({ theme }) => theme.mediaWidth.upToSmall` + margin-left: 10px; + `} +` + +const Block = styled.div` + display: flex; + justify-content: space-between; + + padding-left: 34px; + padding-top: 19px; + padding-bottom: 22px; + padding-right: 34px; + + ${({ theme }) => theme.mediaWidth.upToSmall` + display: block; + margin-top: 10px; + &:last-child { + border-bottom: 0; + } + `} +` + +const Item = styled.div` + min-width: 150px; + display: flex; + flex-direction: column; + justify-content: space-between; + + @media (max-width: 767px) { + display: flex; + flex-direction: row; + width: auto; + align-items: center; + margin: 0 0 3px 0; + &:last-child { + margin-bottom: 0; + } + } + + &.low div:last-child { + color: #459d00; + } + + &.elevated div:last-child { + color: #ffaf1d; + } + + &.high div:last-child { + color: #e75966; + } + + &.liquidation div:last-child { + color: #e75966; + } +` + +const Label = styled.div` + font-size: ${(props) => props.theme.font.default}; + color: ${(props) => props.theme.colors.tertiary}; + font-weight: 400; + display: flex; + gap: 10px; + align-items: center; + @media (max-width: 767px) { + font-size: ${(props) => props.theme.font.small}; + } +` + +const Value = styled.div` + font-size: ${(props) => props.theme.font.default}; + color: ${(props) => props.theme.colors.accent}; + font-weight: 700; + @media (max-width: 767px) { + font-size: ${(props) => props.theme.font.small}; + } + + display: flex; + align-items: center; + + &.status { + color: #459d00; + font-weight: 400; + border: 1px solid #459d00; + border-radius: 50px; + width: fit-content; + padding: 4px 15px; + } +` + +const Dot = styled.div` + width: 6px; + height: 6px; + background-color: #459d00; + border-radius: 100%; + margin-right: 5px; +` diff --git a/src/containers/Earn/EarnDetails.tsx b/src/containers/Earn/EarnDetails.tsx new file mode 100644 index 00000000..51683aa8 --- /dev/null +++ b/src/containers/Earn/EarnDetails.tsx @@ -0,0 +1,345 @@ +import { useStoreState } from 'easy-peasy' +import { useCallback, useEffect } from 'react' +import { ChevronLeft } from 'react-feather' +import { useNavigate, useLocation } from 'react-router-dom' +import styled from 'styled-components' +import { useActiveWeb3React } from '~/hooks' +import useGeb from '~/hooks/useGeb' +import { useStoreActions } from 'easy-peasy' + +import { formatDataNumber, formatWithCommas, getTokenLogo, toPercentage } from '~/utils' +import Loader from '~/components/Loader' +import Camelot from '~/components/Icons/Camelot' +import { POOLS } from '~/utils' + +const EarnDetails = () => { + const navigate = useNavigate() + const location = useLocation() + + const geb = useGeb() + const { account } = useActiveWeb3React() + // @to-do for some reason the new model is not being tracked in store type, but it is available as a function + // @ts-ignore + const { nitroPoolsModel: nitroPoolsState } = useStoreState((state) => state) + // @ts-ignore + const { nitroPoolsModel: nitroPoolsActions } = useStoreActions((state) => state) + const { nitroPools } = nitroPoolsState + const address = location.pathname.split('/').pop() + useEffect(() => { + if (!geb) return + async function fetchPools() { + const pool = POOLS.find((pool: { nitroPoolAddress: string }) => pool.nitroPoolAddress === address) + + try { + await nitroPoolsActions.fetchNitroPool({ + userAddress: account ?? undefined, + camelotPoolAddress: pool.camelotPoolAddress, + nitroPoolAddress: pool.nitroPoolAddress, + geb, + }) + } catch (e) { + throw new Error(`Error fetching nitropools data ${e}`) + } + } + fetchPools() + }, [account, geb, nitroPoolsActions, address]) + const startTime = nitroPools[0]?.nitroData.startTime + const endTime = nitroPools[0]?.nitroData.endTime + + const handleBack = useCallback(() => { + navigate('/earn') + }, [navigate]) + + const getDates = () => { + const start = new Date(Number(startTime) * 1000) + const end = new Date(Number(endTime) * 1000) + const now = new Date() + return { start, end, now } + } + + const getTimePeriod = () => { + const { start, end, now } = getDates() + return now > start && now < end ? 'Active' : 'Inactive' + } + + const getDuration = () => { + const { start, end } = getDates() + + const yearsDifference = end.getFullYear() - start.getFullYear() + const monthsDifference = end.getMonth() - start.getMonth() + let totalMonths = yearsDifference * 12 + monthsDifference + + const startCopy = new Date(start) + startCopy.setMonth(startCopy.getMonth() + totalMonths) + + let remainingDays = (end.getTime() - startCopy.getTime()) / (1000 * 60 * 60 * 24) + + if (remainingDays < 0) { + totalMonths -= 1 + startCopy.setMonth(startCopy.getMonth() - 1) + remainingDays = (end.getTime() - startCopy.getTime()) / (1000 * 60 * 60 * 24) + } + + return `${totalMonths} months ${Math.floor(remainingDays)} days` + } + + const getEndDuration = (): string => { + const { end, now } = getDates() + + const diffMs = end.getTime() - now.getTime() + + let diffSec = Math.floor(diffMs / 1000) + const diffMin = Math.floor(diffSec / 60) + const diffHrs = Math.floor(diffMin / 60) + const diffDays = Math.floor(diffHrs / 24) + + const remainingMin = diffMin % 60 + const remainingHrs = diffHrs % 24 + + return `${diffDays}D ${remainingHrs}h ${remainingMin}min` + } + const totalApy = +nitroPools[0]?.nitroData.apy.toFixed(2) + +nitroPools[0]?.spNftData.apy.toFixed(2) + return ( + <> + {!!nitroPools.length && nitroPools[0]?.nitroData ? ( + + + BACK + + + + <img src={getTokenLogo(nitroPools[0].collateral0TokenSymbol)} alt={''} width={'50px'} /> + <img src={getTokenLogo(nitroPools[0].collateral1TokenSymbol)} alt={''} width={'50px'} /> + <PoolTitle>{`${nitroPools[0].collateral0TokenSymbol} - ${nitroPools[0].collateral1TokenSymbol}`}</PoolTitle> + + + + VIEW ON CAMELOT + + + + + + + + ${formatWithCommas(nitroPools[0].nitroData.tvlUSD || 0, 0)} + + + + {`${formatWithCommas(totalApy, 0)}%`} + + + + + {`${formatDataNumber( + nitroPools[0].nitroData.rewardsToken1RemainingAmount, + 18, + 0 + )} ${nitroPools[0].rewardToken1Symbol}, ${formatDataNumber( + nitroPools[0].nitroData.rewardsToken2RemainingAmount, + 18, + 0 + )} ${nitroPools[0].rewardToken2Symbol}`} + + + + + + + + + {getTimePeriod()} + + + + {getDuration()} + + + + {getEndDuration()} + + + + +
+ My deposit + + + + + + {`$${formatWithCommas(nitroPools[0].userDollarValue, 2)}`} + + + + {toPercentage(nitroPools[0].nitroData.userPoolPercentage, 1)} + + + + VIEW ON CAMELOT + + + +
+
+ ) : ( + + + + )} + + ) +} + +export default EarnDetails + +const ExternalLink = styled.a` + display: flex; + align-items: center; + text-transform: uppercase; + letter-spacing: 2px; + font-weight: 700; + font-size: 14px; + color: ${(props) => props.theme.colors.primary}; +` + +const Container = styled.div` + max-width: 880px; + margin: 50px auto; + padding: 0 15px; + display: flex; + flex-direction: column; + gap: 50px; + @media (max-width: 767px) { + margin: 50px auto; + } +` + +const LoaderContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 100vh; +` + +const PoolTitle = styled.div` + font-size: 34px; + font-family: ${(props) => props.theme.family.headers}; + color: ${(props) => props.theme.colors.accent}; + font-weight: 700; + + margin-left: 5px; + + span { + font-weight: 500; + color: ${(props) => props.theme.colors.primary}; + } +` + +const BackBtn = styled.div` + font-size: 16px; + display: flex; + align-items: center; + font-weight: 700; + font-family: 'Open Sans', sans-serif; + color: #1c293a; + cursor: pointer; + max-width: fit-content; + svg { + margin-right: 5px; + } + + transition: opacity 0.3s ease; + &:hover { + opacity: 0.8; + } +` + +const PoolHeader = styled.div` + display: flex; + justify-content: space-between; + + @media (max-width: 767px) { + flex-direction: column; + align-items: center; + + gap: 30px; + } +` + +const Title = styled.h2` + display: flex; +` + +const Body = styled.div` + display: flex; + justify-content: space-between; + gap: 20px; + + @media (max-width: 767px) { + flex-direction: column; + } +` + +const Wrapper = styled.div` + background-color: #ffffff; + padding: 20px; + border-radius: 4px; + flex: 1; + margin-bottom: 15px; + + .footer { + width: 80%; + } +` + +const Footer = styled.div`` + +const Label = styled.div` + font-size: 16px; + color: ${(props) => props.theme.colors.accent}; +` + +const Value = styled.div` + font-size: 18px; + color: ${(props) => props.theme.colors.primary}; + font-weight: 700; +` + +const ColWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 20px; +` + +const Item = styled.div`` + +const FooterWrapper = styled.div` + display: flex; + justify-content: space-between; + gap: 80px; + + @media (max-width: 767px) { + flex-direction: column; + gap: 30px; + } +` + +const FooterHeader = styled.div` + margin-bottom: 15px; + font-size: 32px; + font-weight: 700; + font-family: ${(props) => props.theme.family.headers}; + color: ${(props) => props.theme.colors.accent}; + + display: flex; + justify-content: space-between; + flex-wrap: wrap; + + @media (max-width: 767px) { + flex-direction: column; + align-items: center; + gap: 10px; + } +` diff --git a/src/containers/Earn/PoolBlock.tsx b/src/containers/Earn/PoolBlock.tsx new file mode 100644 index 00000000..76668b32 --- /dev/null +++ b/src/containers/Earn/PoolBlock.tsx @@ -0,0 +1,242 @@ +import { Link } from 'react-router-dom' +import styled from 'styled-components' +import { formatWithCommas, getTokenLogo } from '~/utils' +import { Tooltip as ReactTooltip } from 'react-tooltip' + +interface PoolData { + nitroData: { + startTime: number + endTime: number + apy: number + tvlUSD: number + } + spNftData: { + apy: number + } + collateral0TokenSymbol: string + collateral1TokenSymbol: string + rewardToken1Symbol: string + rewardToken2Symbol: string +} + +const PoolBlock = ({ nitroPoolAddress, nitroPoolData }: { nitroPoolAddress: string; nitroPoolData: PoolData }) => { + console.log('DATA: ', nitroPoolData) + const { collateral0TokenSymbol, collateral1TokenSymbol, rewardToken1Symbol, rewardToken2Symbol } = nitroPoolData + + const getTimePeriod = () => { + const start = new Date(Number(nitroPoolData.nitroData.startTime) * 1000) + const end = new Date(Number(nitroPoolData.nitroData.endTime) * 1000) + const now = new Date() + return now > start && now < end ? 'Active' : 'Inactive' + } + const totalApy = +nitroPoolData.nitroData.apy.toFixed(2) + +nitroPoolData.spNftData.apy.toFixed(2) + return ( + + + + + + {`${collateral0TokenSymbol} - ${collateral1TokenSymbol}`} + {''} + {''} + + + + + + + + + {getTimePeriod()} + + + + + ${formatWithCommas(nitroPoolData.nitroData.tvlUSD?.toFixed(2) || 0)} + + + + {`${formatWithCommas(totalApy)}%`} + + + + {`${rewardToken1Symbol}, ${rewardToken2Symbol}`} + + + + + + ) +} + +export default PoolBlock + +const BlockContainer = styled.div` + border-radius: 4px; + background: white; + box-shadow: 0px 4px 6px 0px #0d4b9d33; + position: relative; + display: flex; + flex-direction: column; + &.empty { + background: white; + } + &:not(:last-child) { + margin-bottom: 29px; + } +` + +const BlockHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #1c293a33; + padding-left: 34px; + padding-top: 22px; + padding-bottom: 11px; + padding-right: 34px; + + ${({ theme }) => theme.mediaWidth.upToSmall` + display: flex; + flex-direction: column; + align-items: flex-end; + `} +` + +const PoolInfo = styled.div` + display: flex; + align-items: center; + svg { + border-radius: ${(props) => props.theme.global.borderRadius}; + border: 1px solid ${(props) => props.theme.colors.border}; + ${({ theme }) => theme.mediaWidth.upToSmall` + width: 25px; + height: 25px; + `} + } + + ${({ theme }) => theme.mediaWidth.upToSmall` + display: flex; + flex-direction: column; + `} +` + +const PoolData = styled.div` + display: flex; + ${({ theme }) => theme.mediaWidth.upToSmall` + margin-left: 10px; + `} +` + +const PoolTitle = styled.div` + font-size: ${(props) => props.theme.font.large}; + font-family: ${(props) => props.theme.family.headers}; + color: ${(props) => props.theme.colors.accent}; + font-weight: 700; + + margin-right: 22px; + + span { + font-weight: 500; + color: ${(props) => props.theme.colors.primary}; + } +` + +const Block = styled.div` + display: flex; + justify-content: space-between; + + padding-left: 34px; + padding-top: 19px; + padding-bottom: 22px; + padding-right: 34px; + + ${({ theme }) => theme.mediaWidth.upToSmall` + display: block; + margin-top: 10px; + &:last-child { + border-bottom: 0; + } + `} +` + +const Item = styled.div` + min-width: 150px; + display: flex; + flex-direction: column; + justify-content: space-between; + + @media (max-width: 767px) { + display: flex; + flex-direction: row; + width: auto; + align-items: center; + margin: 0 0 3px 0; + &:last-child { + margin-bottom: 0; + } + } + + &.low div:last-child { + color: #459d00; + } + + &.elevated div:last-child { + color: #ffaf1d; + } + + &.high div:last-child { + color: #e75966; + } + + &.liquidation div:last-child { + color: #e75966; + } +` + +const Label = styled.div` + font-size: ${(props) => props.theme.font.default}; + color: ${(props) => props.theme.colors.tertiary}; + font-weight: 400; + display: flex; + gap: 10px; + align-items: center; + @media (max-width: 767px) { + font-size: ${(props) => props.theme.font.small}; + } +` + +const Value = styled.div` + font-size: ${(props) => props.theme.font.default}; + color: ${(props) => props.theme.colors.accent}; + font-weight: 700; + @media (max-width: 767px) { + font-size: ${(props) => props.theme.font.small}; + } + + display: flex; + align-items: center; + + &.status { + color: #459d00; + font-weight: 400; + border: 1px solid #459d00; + border-radius: 50px; + width: fit-content; + padding: 4px 15px; + } +` + +const Dot = styled.div` + width: 6px; + height: 6px; + background-color: #459d00; + border-radius: 100%; + margin-right: 5px; +` diff --git a/src/containers/Earn/index.tsx b/src/containers/Earn/index.tsx new file mode 100644 index 00000000..d65116e7 --- /dev/null +++ b/src/containers/Earn/index.tsx @@ -0,0 +1,209 @@ +import styled from 'styled-components' +import { useStoreActions, useStoreState } from '~/store' +import { useActiveWeb3React } from '~/hooks' +import { useEffect, useMemo, useState } from 'react' +import useGeb from '~/hooks/useGeb' +import Button from '~/components/Button' +import { ExternalLink } from 'react-feather' +import { POOLS } from '~/utils' +import PoolBlock from './PoolBlock' +import { JSX } from 'react/jsx-runtime' +import { PoolData } from '@opendollar/sdk' +import Skeleton from 'react-loading-skeleton' +import EarnBlock from './EarnBlock' +import { getTokenLogo } from '~/utils' +import MetaTags from '~/components/MetaTags' +import metaInfo from '~/utils/metaInfo' + +interface Cache { + [key: string]: PoolData +} + +const Earn = () => { + const geb = useGeb() + const { account } = useActiveWeb3React() + const { nitroPoolsModel: nitroPoolsState } = useStoreState((state) => state) + const { nitroPoolsModel: nitroPoolsActions } = useStoreActions((state) => state) + const { nitroPools } = nitroPoolsState + + const [cachedPools, setCachedPools] = useState({}) + + const fetchPools = useMemo(() => { + if (!geb) return () => {} + return async () => { + try { + const poolResults = await Promise.all( + POOLS.map(async (pool: { camelotPoolAddress: string; nitroPoolAddress: string }) => { + const cacheKey = `${account}-${pool.camelotPoolAddress}-${pool.nitroPoolAddress}` + if (cachedPools[cacheKey]) { + return cachedPools[cacheKey] + } + const poolData = await nitroPoolsActions.fetchNitroPool({ + userAddress: account ?? undefined, + camelotPoolAddress: pool.camelotPoolAddress, + nitroPoolAddress: pool.nitroPoolAddress, + geb, + }) + setCachedPools((prev) => ({ + ...prev, + [cacheKey]: poolData, + })) + return poolData + }) + ) + return poolResults + } catch (e) { + console.error(`Error fetching Nitro Pools data: ${e}`) + return [] + } + } + }, [account, geb, nitroPoolsActions, cachedPools]) + + useEffect(() => { + fetchPools() + }, [fetchPools]) + + const handleClick = () => { + window.open('https://discord.opendollar.com/', '_blank') + } + + return ( + <> + + + Earn + incentivized pools and strategies + +

Earn additional yield by staking your LP position in Camelot Nitro.

+

+ When creating a OD-ETH position, use the "Auto" mode provided by Gamma.

See full + instructions{' '} + + here + + . +

+
+ + Strategies + + Credit Guild {''} + + } + /> + {nitroPools.length > 0 ? ( + POOLS?.map( + ( + pool: JSX.IntrinsicAttributes & { nitroPoolAddress: string; nitroPoolData: PoolData }, + i: number + ) => ( + + ) + ) + ) : ( + + )} + + + + + +
+ + ) +} + +const Container = styled.div` + max-width: 1362px; + margin: 80px auto; + padding: 0 15px; + @media (max-width: 767px) { + margin: 50px auto; + } + color: ${(props) => props.theme.colors.accent}; +` + +const Title = styled.h2` + font-size: 34px; + font-weight: 700; + font-family: ${(props) => props.theme.family.headers}; + color: ${(props) => props.theme.colors.accent}; +` + +const SubHeader = styled.h3` + text-transform: uppercase; + font-family: ${(props) => props.theme.family.headers}; + font-size: 22px; + font-weight: 700; + color: ${(props) => props.theme.colors.tertiary}; + margin-bottom: 20px; +` + +const Text = styled.div` + background-color: rgba(202, 234, 255, 0.3); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0); + border-radius: 3px; + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); + padding: 20px; + font-size: ${(props) => props.theme.font.default}; + border-radius: 3px; + margin-bottom: 30px; + + p { + margin-bottom: 10px; + } + + a { + text-decoration: underline; + color: ${(props) => props.theme.colors.tertiary}; + } +` + +const PoolsHeader = styled.h2` + font-size: 34px; + font-weight: 700; + color: ${(props) => props.theme.colors.accent}; + margin-bottom: 20px; +` + +const Pools = styled.div` + margin-bottom: 30px; +` + +const BtnWrapper = styled.div` + width: max-content; + margin-right: auto; + margin-left: auto; + button { + text-transform: uppercase; + font-weight: 700; + font-size: 18px; + padding: 17px 30px; + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + } +` + +const Link = styled.a`` + +export default Earn diff --git a/src/containers/Explore/ExploreTable.tsx b/src/containers/Explore/ExploreTable.tsx new file mode 100644 index 00000000..90b61e11 --- /dev/null +++ b/src/containers/Explore/ExploreTable.tsx @@ -0,0 +1,348 @@ +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, + getSortedRowModel, + getFilteredRowModel, + SortingState, + ColumnDef, +} from '@tanstack/react-table' +import './index.css' +import styled from 'styled-components' +import Button from '~/components/Button' +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' + +type Vault = { + id: string + collateral: string + image?: string | any + collateralAmount: string + debtAmount: string + riskStatus: string + actions?: any +} + +const riskStatusMapping: { [key: string]: number } = { + NO: 0, + LOW: 1, + ELEVATED: 2, + HIGH: 3, + LIQUIDATION: 4, +} + +const parseDebtAmount = (value: string): number => { + return parseFloat(value.replace(/,/g, '').replace(' OD', '')) +} + +const columnHelper = createColumnHelper() + +const ExploreTable = ({ data }: { data: Vault[] }) => { + const [sorting, setSorting] = useState([]) + const [globalFilter, setGlobalFilter] = useState('') + const navigate = useNavigate() + + const columns: ColumnDef[] = [ + columnHelper.accessor('id', { + header: () => 'ID', + cell: (info) => info.getValue(), + sortingFn: 'alphanumeric', + enableSorting: true, + }), + columnHelper.accessor('image', { + header: () => '', + cell: (info) => { + const image = info.row.original.image + const vaultID = info.row.original.id + return image ? ( + + + + ) : null + }, + enableSorting: false, + }), + columnHelper.accessor('collateralAmount', { + header: () => 'Collateral Amount', + cell: (info) => info.getValue().toLocaleString(), + sortingFn: (rowA, rowB) => { + const a = rowA.getValue('collateralAmount') + const b = rowB.getValue('collateralAmount') + return a - b + }, + filterFn: (row, columnId, filterValue) => { + const value = row.getValue(columnId) + return value.toString().includes(filterValue) + }, + }), + columnHelper.accessor('collateral', { + header: () => 'Collateral', + cell: (info) => info.getValue(), + sortingFn: 'alphanumeric', + enableSorting: true, + }), + columnHelper.accessor('debtAmount', { + header: () => 'Debt Amount', + cell: (info) => info.getValue(), + sortingFn: (rowA, rowB) => { + const a = parseDebtAmount(rowA.getValue('debtAmount')) + const b = parseDebtAmount(rowB.getValue('debtAmount')) + return a - b + }, + filterFn: (row, columnId, filterValue) => { + const value = parseDebtAmount(row.getValue(columnId)) + return value.toString().includes(filterValue) + }, + }), + columnHelper.accessor('riskStatus', { + header: () => 'Risk Status', + cell: (info) => info.getValue().toLocaleString(), + sortingFn: (rowA, rowB) => { + const a = riskStatusMapping[rowA.getValue('riskStatus')] || 1 + const b = riskStatusMapping[rowB.getValue('riskStatus')] || 1 + return a - b + }, + filterFn: (row, columnId, filterValue) => { + const value = row.getValue(columnId) + return value.includes(filterValue) + }, + }), + columnHelper.accessor('actions', { + header: '', + cell: (info) => { + return ( + + + + ) + }, + enableSorting: false, + }), + ] + + const table = useReactTable({ + data: data, + columns, + state: { + sorting, + globalFilter, + }, + onSortingChange: setSorting, + onGlobalFilterChange: setGlobalFilter, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + }) + + return ( + + setGlobalFilter(String(e.target.value))} + placeholder="Search all columns..." + style={{ marginBottom: '10px', padding: '8px', width: '100%', fontFamily: 'Barlow' }} + /> + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => { + const header = cell.column.columnDef.header + let headerText = '' + + if (typeof header === 'function') { + // @ts-ignore + const renderedHeader = header(cell.getContext()) + if (typeof renderedHeader === 'string') { + headerText = renderedHeader + } else if ( + typeof renderedHeader === 'object' && + renderedHeader.props && + renderedHeader.props.children + ) { + headerText = renderedHeader.props.children + } + } else { + headerText = header ? header.toString() : '' + } + + return ( + + ) + })} + + ))} + +
+ {header.isPlaceholder ? null : ( + + {flexRender(header.column.columnDef.header, header.getContext())} + {header.column.getCanSort() ? ( + header.column.getIsSorted() ? ( + header.column.getIsSorted() === 'asc' ? ( + + ) : ( + + ) + ) : ( +  ⇅ + ) + ) : null} + + )} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ ) +} + +export default ExploreTable + +const ArrowUpAndDownIcon = styled.span` + font-size: 15px; + padding-bottom: 4px; +` + +const StyledArrow = styled.div` + padding-left: 4px; +` + +const SortableHeader = styled.div` + display: flex; + align-items: center; + justify-content: start; + cursor: pointer; + font-family: 'Open Sans', sans-serif; + font-size: ${(props) => props.theme.font.xSmall}; +` + +const TableContainer = styled.div` + overflow-x: auto; + table { + width: 100%; + border-collapse: collapse; + min-width: 600px; + padding: 20px; + background-color: rgba(255, 255, 255, 0); + backdrop-filter: blur(10px); + } + th, + td { + padding: 8px 0px; + text-align: left; + } + + th { + background-color: #fff; + border-top: 2px solid #000; + border-bottom: 2px solid #000; + } + + tr { + margin-bottom: 20px; + } + + tr:not(:last-child) td { + border-bottom: 1px solid #ddd; + } + + @media (max-width: 768px) { + table { + min-width: 100%; + display: block; + overflow-x: auto; + } + + thead { + display: none; + } + + tbody, + td { + display: block; + width: 100%; + box-sizing: border-box; + } + + tr { + display: flex; + flex-direction: column; + margin-bottom: 20px; + border-bottom: 4px solid #ddd; + } + + td { + display: flex; + justify-content: space-between; + padding: 10px; + } + + td::before { + display: flex; + content: attr(data-label); + left: 10px; + white-space: nowrap; + font-weight: bold; + text-align: left; + } + } +` + +const SVGContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 70px; + height: 70px; + position: relative; + + @media (max-width: 768px) { + width: 140px; + height: 140px; + justify-content: center; + margin-left: auto; + margin-right: auto; + } +` + +const SvgWrapper = styled.div` + transform: scale(0.33); + border-radius: 10px; + + @media (max-width: 768px) { + transform: scale(0.7); + } +` + +const ButtonFloat = styled.div` + position: relative; + top: 0; + right: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + gap: 10px; + border-radius: 5px; + z-index: 2; + button { + margin: 5px; + padding: 5px; + } +` diff --git a/src/containers/Explore/index.css b/src/containers/Explore/index.css new file mode 100644 index 00000000..3978e6d9 --- /dev/null +++ b/src/containers/Explore/index.css @@ -0,0 +1,38 @@ +html { + font-family: 'Open Sans', sans-serif; + font-size: 18px; +} + +table { + width: 100%; + background-color: #fff; + border-radius: 4px; + +} + +th { + padding: 20px; + text-align: left; + font-family: 'Barlow', sans-serif; + text-transform: uppercase; + border-bottom: 1px solid #b6c7d3cc; + white-space: nowrap; + + &:first-child { + /* width: 150px; */ + } + + &:not(:first-child) { + /* text-align: right; */ + } +} + +td { + padding: 20px; + text-transform: capitalize; + text-decoration: none; + + &:not(:first-child) { + /* text-align: right; */ + } +} diff --git a/src/containers/Explore/index.tsx b/src/containers/Explore/index.tsx new file mode 100644 index 00000000..5c7450e9 --- /dev/null +++ b/src/containers/Explore/index.tsx @@ -0,0 +1,143 @@ +import styled from 'styled-components' +import { useState, useEffect } from 'react' +import ExploreTable from './ExploreTable' +import { ethers } from 'ethers' +import { parseRay, formatDataNumber, calculateRiskStatusText, ratioChecker, parseFormattedNumber } from '~/utils' +import { AllVaults, useVaultSubgraph } from '~/hooks/useVaultSubgraph' +import useAnalyticsData from '~/hooks/useAnalyticsData' +import useGeb from '~/hooks/useGeb' +import { generateSVGRing } from '~/utils/generateSVGRing' + +const Explore: React.FC = () => { + const [isLoading, setIsLoading] = useState(true) + const [tableData, setTableData] = useState([]) + const allVaults: AllVaults = useVaultSubgraph() + const geb = useGeb() + const analyticsData = useAnalyticsData() + + const parseNumber = (value: string): number => { + return parseFloat(value.replace(/,/g, '')) + } + + const getSafeData = () => { + setIsLoading(true) + const tableRows = [] + + if (!allVaults?.vaults || !analyticsData?.tokenAnalyticsData) return + + for (const vault of allVaults.vaults) { + try { + const estimatedValue = `${( + +ethers.utils.formatUnits(vault.collateral) * + +ethers.utils.formatUnits(analyticsData.tokenAnalyticsData[vault.collateralType].currentPrice) + ).toFixed(2)}` + + const formattedDebt = parseFormattedNumber(formatDataNumber(vault.debt)) + let cratio = 0 + if (formattedDebt !== 0) { + cratio = (+estimatedValue / formattedDebt) * 100 + } + + let correctCollateralizationRatio + if (Number(cratio) > 0) { + correctCollateralizationRatio = Number(cratio) + } else if (Number(cratio) === 0 && parseFormattedNumber(formatDataNumber(vault.collateral)) > 0) { + correctCollateralizationRatio = '∞' + } else if (Number(cratio) === 0 && parseFormattedNumber(formatDataNumber(vault.collateral)) === 0) { + correctCollateralizationRatio = 0 + } else { + correctCollateralizationRatio = 0 + } + + const svgData = { + collateralizationRatio: correctCollateralizationRatio, + liqRatio: Number( + parseRay(analyticsData.tokenAnalyticsData[vault.collateralType].liquidationCRatio) + ), + safetyRatio: Number(parseRay(analyticsData.tokenAnalyticsData[vault.collateralType].safetyCRatio)), + } + + const riskStatus = calculateRiskStatusText( + ratioChecker( + Number(cratio), + Number(parseRay(analyticsData.tokenAnalyticsData[vault.collateralType].liquidationCRatio)), + Number(parseRay(analyticsData.tokenAnalyticsData[vault.collateralType].safetyCRatio)) + ) + ) + + let svg = null + try { + svg = generateSVGRing(svgData, 210, 420, `svg-${vault.id}`) + } catch (e) { + console.error(e) + } + + tableRows.push({ + id: vault.id, + collateral: vault.collateralType, + image: svg ? svg : null, + collateralAmount: parseNumber(formatDataNumber(vault.collateral)), + debtAmount: formatDataNumber(vault.debt) + ' OD', + riskStatus: riskStatus, + }) + } catch (e) { + console.error(e) + } + } + // @ts-ignore + setTableData(tableRows) + setIsLoading(false) + } + + useEffect(() => { + getSafeData() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [allVaults, analyticsData, geb]) + + return ( + +
+ Explore Vaults +
+ {tableData.length !== 0 && } + {tableData.length === 0 && isLoading &&

Loading...

} + {tableData.length === 0 && !isLoading &&

No vaults available

} +
+ ) +} + +export default Explore + +const Container = styled.div` + max-width: 1362px; + margin: 80px auto; + padding: 0 15px; + @media (max-width: 767px) { + margin: 50px auto; + display: flex; + flex-direction: column; + } + color: ${(props) => props.theme.colors.accent}; +` + +const Title = styled.h2` + font-size: 34px; + font-weight: 700; + font-family: ${(props) => props.theme.family.headers}; + + color: ${(props) => props.theme.colors.accent}; + @media (max-width: 767px) { + font-size: 32px; + } +` + +const Header = styled.div` + display: flex; + justify-content: space-between; + @media (max-width: 767px) { + flex-direction: column; + justify-content: left; + } + + margin-bottom: 40px; +` diff --git a/src/containers/GeoBlockContainer.tsx b/src/containers/GeoBlockContainer.tsx new file mode 100644 index 00000000..dbdf5826 --- /dev/null +++ b/src/containers/GeoBlockContainer.tsx @@ -0,0 +1,172 @@ +import React, { useState } from 'react' +import { Copy, ExternalLink } from 'react-feather' +import styled from 'styled-components' + +const GeoBlockContainer = () => { + const [letter, setLetter] = useState( + `Comments on H.R.4766 - Clarity for Payment Stablecoins Act of 2023\n\nDecentralized stablecoins which are fully backed by high-quality crypto assets and fully overcollateralized should have a place in any stablecoin legislation. Organizations like Open Dollar, Liquity, and others are issuing stablecoins with protections for consumers which are even safer than centrally backed stablecoins – as demonstrated by USDC depegging when Silicon Valley Bank went defunct. Please support the innovation of American companies and legalize dollar-pegged stablecoins which are overcollateralized by more diverse assets. Thank you.` + ) + const [copied, setCopied] = useState(false) + + const copyToClipboard = () => { + navigator.clipboard.writeText(letter) + setCopied(true) + setTimeout(() => { + setCopied(false) + }, 2000) + } + + return ( + + + Location Unavailable + + It seems like you are trying to access this interface from a prohibited country or jurisdiction. As + a web interface, we must adhere to certain rules and regulations. Please connect from a different + location. + + + The Open Dollar protocol is decentralized, fully on-chain, and open-source. No one person has access + to your funds or keys, only you do. You can view the code and run the app locally using our{' '} + + GitHub + + . + + + + You can be a force for good and support the development of secure and decentralized stablecoins + + . Consider submitting a public comment to your Congressional representatives about the Payment + Stablecoins Act currently under debate. + +