diff --git a/.env.example b/.env.example index 51955f24f..381931eb7 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,15 @@ -REACT_APP_CHAIN_ID=4 -REACT_APP_NETWORK_URL="PUT_YOUR_INFURA_URL" -REACT_APP_GOOGLE_ANALYTICS_ID="PUT_YOUR_GOOGLE_ANALYTICS_ID" -REACT_APP_GIT_COMMIT_HASH="PUT_YOUR_LAST_COMMIT_HASH" -REACT_APP_FORTMATIC_KEY="PUT_YOUR_FORTMATIC_KEY" -REACT_APP_PORTIS_ID="PUT_YOUR_PORTIS_ID" +PUBLIC_URL="." REACT_APP_ADDITIONAL_SERVICES_API_URL_RINKEBY="PUT_YOUR_API_URL" REACT_APP_ADDITIONAL_SERVICES_API_URL_PROD_RINKEBY="PUT_YOUR_API_URL" REACT_APP_ADDITIONAL_SERVICES_API_URL_MAINNET="PUT_YOUR_API_URL" REACT_APP_ADDITIONAL_SERVICES_API_URL_PROD_MAINNET="PUT_YOUR_API_URL" REACT_APP_ADDITIONAL_SERVICES_API_URL_XDAI="PUT_YOUR_API_URL" -REACT_APP_ADDITIONAL_SERVICES_API_URL_PROD_XDAI="PUT_YOUR_API_URL" \ No newline at end of file +REACT_APP_ADDITIONAL_SERVICES_API_URL_PROD_XDAI="PUT_YOUR_API_URL" +REACT_APP_CHAIN_ID=4 +REACT_APP_NETWORK_URL_RINKEBY="PUT_YOUR_INFURA_URL" +REACT_APP_NETWORK_URL_MAINNET="PUT_YOUR_INFURA_URL" +REACT_APP_NETWORK_URL_XDAI="PUT_YOUR_INFURA_URL" +REACT_APP_FORTMATIC_KEY="PUT_YOUR_FORTMATIC_KEY" +REACT_APP_PORTIS_ID="PUT_YOUR_PORTIS_ID" +REACT_APP_GOOGLE_ANALYTICS_ID="PUT_YOUR_GOOGLE_ANALYTICS_ID" +REACT_APP_GIT_COMMIT_HASH="PUT_YOUR_LAST_COMMIT_HASH" diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..191ae4cc9 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +*.d.ts \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index cd6c67489..4ee445e27 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,31 +1,103 @@ module.exports = { - parser: "@typescript-eslint/parser", + parser: '@typescript-eslint/parser', parserOptions: { - ecmaVersion: 2020, - sourceType: "module", ecmaFeatures: { - // Allows for the parsing of JSX jsx: true, }, + ecmaVersion: 2020, + sourceType: 'module', }, - ignorePatterns: ["node_modules/**/*"], + env: { + browser: true, + es6: true, + }, + ignorePatterns: ['node_modules/**/*'], settings: { react: { - version: "detect", + pragma: 'React', + version: 'detect', }, + 'import/extensions': ['.js', '.jsx', '.ts', '.tsx'], }, + plugins: [ + 'react', + 'react-hooks', + '@typescript-eslint', + 'import', + 'sort-destructure-keys', + 'prettier', + ], extends: [ - "plugin:react/recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react-hooks/recommended", - "prettier/@typescript-eslint", - "plugin:prettier/recommended", + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier/@typescript-eslint', + 'prettier/react', + 'plugin:prettier/recommended', + 'plugin:import/errors', + 'plugin:import/warnings', + 'plugin:import/typescript', ], rules: { - "@typescript-eslint/explicit-function-return-type": "off", - "prettier/prettier": "error", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/ban-types": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", + '@typescript-eslint/explicit-function-return-type': 'off', + 'prettier/prettier': 'error', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-types': 'off', + 'react/prop-types': 'off', + 'no-console': [ + 'warn', + { + allow: ['warn', 'error'], + }, + ], + 'sort-imports': [ + 'error', + { + ignoreDeclarationSort: true, + }, + ], + semi: ['error', 'never'], + 'no-warning-comments': 0, + 'import/extensions': 0, + 'import/no-unresolved': 0, + 'import/no-extraneous-dependencies': [ + 'error', + { optionalDependencies: false, peerDependencies: false }, + ], + 'import/order': [ + 'error', + { + alphabetize: { order: 'asc' }, + groups: [ + ['builtin', 'external'], + ['internal', 'parent', 'sibling', 'index'], + ], + 'newlines-between': 'always', + pathGroups: [ + { group: 'builtin', pattern: 'react', position: 'before' }, + { + group: 'external', + pattern: + '{uniswap-xdai-sdk,rebass/styled-components,polished,react-feather,react-router,@walletconnect/web3-provider,@testing-library/dom,@testing-library/user-event,@testing-library/react,styled-components,ethers/utils,react-dom,react-router-dom,ethers,ethers/providers,web3-utils}', + position: 'before', + }, + ], + pathGroupsExcludedImportTypes: ['builtin'], + }, + ], + 'import/prefer-default-export': 0, + 'sort-destructure-keys/sort-destructure-keys': 2, + 'react/jsx-label-has-associated-control': 0, + 'react/jsx-sort-props': 2, + 'react/jsx-filename-extension': [ + 1, + { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + ], + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', + '@typescript-eslint/explicit-module-boundary-types': 'off', }, -}; +} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 37d0de1ec..52e21f6e2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,7 +9,6 @@ jobs: runs-on: ubuntu-latest env: REPO_NAME_SLUG: idoux - steps: - name: Checkout code uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index 9c87e44a6..576492d45 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ package-lock.json cypress/videos cypress/screenshots cypress/fixtures/example.json +.project \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..cd4efd8e5 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +*.d.ts diff --git a/.prettierrc.js b/.prettierrc.js index 9fe772025..654c2932f 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,4 +1,7 @@ module.exports = { + printWidth: 100, + singleQuote: true, + semi: false, bracketSpacing: true, trailingComma: "all", overrides: [ diff --git a/package.json b/package.json index 459da4dbd..3e3d9322a 100644 --- a/package.json +++ b/package.json @@ -3,25 +3,39 @@ "description": "Gnosis Auction", "homepage": ".", "private": true, + "engines": { + "node": ">=4.4.7 <=14.15.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "cross-env REACT_APP_GIT_COMMIT_HASH=$(git show -s --format=%H) react-scripts build", + "ipfs-build": "cross-env PUBLIC_URL=\".\" react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject", + "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --fix", + "lint:fix": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --fix", + "prettier:fix": "prettier --write \"./src/**/*.{js,jsx,ts,tsx}\"", + "cy:run": "cypress run", + "serve:build": "serve -s build -l 3000", + "integration-test": "yarn build && start-server-and-test 'yarn run serve:build' http://localhost:3000 cy:run" + }, + "eslintConfig": { + "extends": "react-app" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "license": "GPL-3.0-or-later", "devDependencies": { - "uniswap-xdai-sdk": "^3.0.2-a3", - "@amcharts/amcharts4": "^4.10.13", - "@ethersproject/address": "^5.0.0-beta.134", - "@ethersproject/bignumber": "^5.0.0-beta.138", - "@ethersproject/constants": "^5.0.0-beta.133", - "@ethersproject/contracts": "^5.0.0-beta.151", - "@ethersproject/experimental": "^5.0.0-beta.141", - "@ethersproject/providers": "5.0.0-beta.162", - "@ethersproject/strings": "^5.0.0-beta.136", - "@ethersproject/units": "^5.0.0-beta.132", - "@ethersproject/wallet": "^5.0.0-beta.141", - "@fortawesome/fontawesome-svg-core": "^1.2.32", - "@fortawesome/free-solid-svg-icons": "^5.15.1", - "@fortawesome/react-fontawesome": "^0.1.14", - "@popperjs/core": "^2.4.0", - "@reach/dialog": "^0.10.3", - "@reach/portal": "^0.10.3", - "@reduxjs/toolkit": "^1.3.5", "@types/jest": "^25.2.1", "@types/lodash.flatmap": "^4.5.6", "@types/node": "^13.13.5", @@ -32,41 +46,71 @@ "@types/react-router-dom": "^5.0.0", "@types/react-window": "^1.8.2", "@types/rebass": "^4.0.5", - "@types/styled-components": "^4.2.0", + "@types/styled-components": "^5.1.7", "@types/testing-library__cypress": "^5.0.5", "@typescript-eslint/eslint-plugin": "^4.8.2", "@typescript-eslint/parser": "^4.8.2", + "@web3-react/types": "^6.0.7", + "eslint": "^7.14.0", + "eslint-config-prettier": "^6.15.0", + "eslint-plugin-prettier": "^3.1.4", + "eslint-plugin-react": "^7.19.0", + "eslint-plugin-react-hooks": "^4.0.0", + "eslint-plugin-sort-destructure-keys": "^1.3.5", + "husky": "^4.3.6", + "lint-staged": "^10.5.3", + "prettier": "^2.2.1" + }, + "dependencies": { + "@amcharts/amcharts4": "^4.10.13", + "@babel/helper-define-map": "^7.12.13", + "@babel/helper-regex": "^7.10.5", + "@ethersproject/address": "^5.0.0-beta.134", + "@ethersproject/bignumber": "^5.0.0-beta.138", + "@ethersproject/constants": "^5.0.0-beta.133", + "@ethersproject/contracts": "^5.0.0-beta.151", + "@ethersproject/experimental": "^5.0.0-beta.191", + "@ethersproject/providers": "5.0.23", + "@ethersproject/strings": "^5.0.0-beta.136", + "@ethersproject/units": "^5.0.0-beta.191", + "@ethersproject/wallet": "^5.0.0-beta.191", + "@fortawesome/fontawesome-svg-core": "^1.2.32", + "@fortawesome/free-solid-svg-icons": "^5.15.1", + "@fortawesome/react-fontawesome": "^0.1.14", + "@popperjs/core": "^2.4.0", + "@reach/dialog": "^0.10.3", + "@reach/portal": "^0.10.3", + "@reduxjs/toolkit": "^1.3.5", + "@types/react-router-hash-link": "^1.2.1", + "@uniswap/sdk": "^2.0.5", + "@uniswap/token-lists": "^1.0.0-beta.19", "@uniswap/v2-core": "1.0.0", "@uniswap/v2-periphery": "1.0.0-beta.0", + "@web3-react/abstract-connector": "^6.0.7", "@web3-react/core": "^6.1.9", "@web3-react/fortmatic-connector": "^6.1.6", "@web3-react/injected-connector": "^6.0.7", "@web3-react/portis-connector": "^6.1.9", "@web3-react/walletconnect-connector": "^6.1.9", "@web3-react/walletlink-connector": "^6.1.9", + "ajv": "^6.12.3", "copy-to-clipboard": "^3.2.0", "cross-env": "^7.0.2", "cypress": "^4.5.0", - "eslint": "^7.14.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.19.0", - "eslint-plugin-react-hooks": "^4.0.0", - "husky": "^4.3.6", "i18next": "^15.0.9", "i18next-browser-languagedetector": "^3.0.1", "i18next-xhr-backend": "^2.0.1", + "inter-ui": "^3.15.0", "isexe": "^2.0.0", "jazzicon": "^1.5.0", "levenary": "^1.1.1", - "lint-staged": "^10.5.3", "lodash.flatmap": "^4.5.0", "modali": "^1.2.0", "polished": "^3.3.2", - "prettier": "^2.2.1", "qrcode.react": "^0.9.3", "qs": "^6.9.4", "react": "^16.13.1", + "react-copy-to-clipboard": "^5.0.3", "react-device-detect": "^1.6.2", "react-dom": "^16.13.1", "react-feather": "^2.0.8", @@ -75,51 +119,25 @@ "react-popper": "^2.2.3", "react-redux": "^7.2.0", "react-router-dom": "^5.0.0", + "react-router-hash-link": "^2.4.0", "react-scripts": "^4.0.1", "react-spring": "^8.0.27", + "react-table": "^7.6.3", + "react-tooltip": "^4.2.14", "react-use-gesture": "^6.0.14", "react-window": "^1.8.5", "rebass": "^4.0.7", + "redux": "^4.0.5", "redux-localstorage-simple": "^2.2.0", + "sanitize.css": "^12.0.1", "serve": "^11.3.0", "start-server-and-test": "^1.11.0", - "styled-components": "^4.2.0", + "styled-components": "^5.2.1", + "tiny-invariant": "^1.1.0", "typescript": "^3.8.3", + "uniswap-xdai-sdk": "^3.0.2-a3", "use-media": "^1.4.0" }, - "scripts": { - "start": "react-scripts start", - "build": "cross-env REACT_APP_GIT_COMMIT_HASH=$(git show -s --format=%H) react-scripts build", - "ipfs-build": "cross-env PUBLIC_URL=\".\" react-scripts build", - "test": "react-scripts test --env=jsdom", - "eject": "react-scripts eject", - "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix", - "lint:fix": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix", - "prettier:fix": "prettier --write './src/**/*.{js,jsx,ts,tsx}'", - "cy:run": "cypress run", - "serve:build": "serve -s build -l 3000", - "integration-test": "yarn build && start-server-and-test 'yarn run serve:build' http://localhost:3000 cy:run" - }, - "eslintConfig": { - "extends": "react-app" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "license": "GPL-3.0-or-later", - "dependencies": { - "inter-ui": "^3.15.0", - "react-table": "^7.6.3" - }, "lint-staged": { "src/**/*.{js,jsx,ts,tsx}": [ "yarn run lint:fix", diff --git a/public/browserconfig.xml b/public/browserconfig.xml new file mode 100644 index 000000000..70cb989d3 --- /dev/null +++ b/public/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/public/favicon/android-chrome-192x192.png b/public/favicon/android-chrome-192x192.png new file mode 100644 index 000000000..ae6f53cd5 Binary files /dev/null and b/public/favicon/android-chrome-192x192.png differ diff --git a/public/favicon/android-chrome-512x512.png b/public/favicon/android-chrome-512x512.png new file mode 100644 index 000000000..e04e91b4c Binary files /dev/null and b/public/favicon/android-chrome-512x512.png differ diff --git a/public/favicon/apple-touch-icon.png b/public/favicon/apple-touch-icon.png new file mode 100644 index 000000000..485e6121a Binary files /dev/null and b/public/favicon/apple-touch-icon.png differ diff --git a/public/favicon/favicon-16x16.png b/public/favicon/favicon-16x16.png new file mode 100644 index 000000000..83f5bbc45 Binary files /dev/null and b/public/favicon/favicon-16x16.png differ diff --git a/public/favicon/favicon-32x32.png b/public/favicon/favicon-32x32.png new file mode 100644 index 000000000..87f5c313b Binary files /dev/null and b/public/favicon/favicon-32x32.png differ diff --git a/public/favicon/favicon.ico b/public/favicon/favicon.ico new file mode 100644 index 000000000..aecaee0e3 Binary files /dev/null and b/public/favicon/favicon.ico differ diff --git a/public/favicon/mstile-150x150.png b/public/favicon/mstile-150x150.png new file mode 100644 index 000000000..d185d77f1 Binary files /dev/null and b/public/favicon/mstile-150x150.png differ diff --git a/public/favicon/safari-pinned-tab.svg b/public/favicon/safari-pinned-tab.svg new file mode 100644 index 000000000..f52c776c0 --- /dev/null +++ b/public/favicon/safari-pinned-tab.svg @@ -0,0 +1,29 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + diff --git a/public/index.html b/public/index.html index c8d5e86a5..e24d41b8e 100644 --- a/public/index.html +++ b/public/index.html @@ -1,39 +1,92 @@ + - - - - - - - + + + + + + + + GnosisAuction + +
- + diff --git a/public/manifest.json b/public/manifest.json index 7389c7a66..e9f3b25f3 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -3,20 +3,18 @@ "name": "GnosisAuction", "icons": [ { - "src": "./images/192x192_App_Icon.png", + "src": "./favicon/android-chrome-192x192.png", "sizes": "192x192", - "type": "image/png", - "purpose": "any maskable" + "type": "image/png" }, { - "src": "./images/512x512_App_Icon.png", + "src": "./favicon/android-chrome-512x512.png", "sizes": "512x512", - "type": "image/png", - "purpose": "any maskable" + "type": "image/png" } ], - "orientation": "portrait", + "theme_color": "#ffffff", + "background_color": "#ffffff", "display": "standalone", - "theme_color": "#ff007a", - "background_color": "#fff" + "orientation": "portrait" } diff --git a/src/api/AdditionalServicesApi.ts b/src/api/AdditionalServicesApi.ts index 2fb6324d5..b6228dbd1 100644 --- a/src/api/AdditionalServicesApi.ts +++ b/src/api/AdditionalServicesApi.ts @@ -1,47 +1,52 @@ -import { BigNumber } from "@ethersproject/bignumber"; -import { decodeOrder, encodeOrder, Order } from "../hooks/Order"; -import { AuctionInfo } from "../hooks/useAllAuctionInfos"; +import { BigNumber } from '@ethersproject/bignumber' + +import { Order, decodeOrder, encodeOrder } from '../hooks/Order' +import { AuctionInfo } from '../hooks/useAllAuctionInfos' +import { AuctionInfoDetail } from '../hooks/useAuctionDetails' + export interface AdditionalServicesApi { - getOrderBookUrl(params: OrderBookParams): string; - getOrderBookData(params: OrderBookParams): Promise; - getPreviousOrderUrl(params: PreviousOrderParams): string; - getPreviousOrder(params: PreviousOrderParams): Promise; - getCurrentUserOrdersUrl(params: UserOrderParams): string; - getCurrentUserOrders(params: UserOrderParams): Promise; - getAllUserOrdersUrl(params: UserOrderParams): string; - getAllUserOrders(params: UserOrderParams): Promise; - getMostInterestingAuctionDetailsUrl(params: InterestingAuctionParams): string; - getMostInterestingAuctionDetails( - params: InterestingAuctionParams, - ): Promise; - getAllAuctionDetailsUrl(networkId: number): string; - getAllAuctionDetails(): Promise; - getClearingPriceOrderAndVolumeUrl(params: OrderBookParams): string; - getClearingPriceOrderAndVolume( - params: OrderBookParams, - ): Promise; + getOrderBookUrl(params: OrderBookParams): string + getOrderBookData(params: OrderBookParams): Promise + getPreviousOrderUrl(params: PreviousOrderParams): string + getPreviousOrder(params: PreviousOrderParams): Promise + getCurrentUserOrdersUrl(params: UserOrderParams): string + getCurrentUserOrders(params: UserOrderParams): Promise + getAllUserOrdersUrl(params: UserOrderParams): string + getAllUserOrders(params: UserOrderParams): Promise + getMostInterestingAuctionDetailsUrl(params: InterestingAuctionParams): string + getMostInterestingAuctionDetails(): Promise + getAllAuctionDetailsUrl(networkId: number): string + getAllAuctionDetails(): Promise + getClearingPriceOrderAndVolumeUrl(params: OrderBookParams): string + getClearingPriceOrderAndVolume(params: OrderBookParams): Promise + getAuctionDetails(params: AuctionDetailParams): Promise } interface OrderBookParams { - networkId: number; - auctionId: number; + networkId: number + auctionId: number } interface InterestingAuctionParams { - networkId: number; - numberOfAuctions: number; + networkId: number + numberOfAuctions: number } interface PreviousOrderParams { - networkId: number; - auctionId: number; - order: Order; + networkId: number + auctionId: number + order: Order } interface UserOrderParams { - networkId: number; - auctionId: number; - user: string; + networkId: number + auctionId: number + user: string +} + +interface AuctionDetailParams { + networkId: number + auctionId: number } /** @@ -49,226 +54,259 @@ interface UserOrderParams { * Both price and volume are numbers (floats) */ export interface PricePoint { - price: number; - volume: number; + price: number + volume: number } /** * DATA returned from api as JSON */ export interface OrderBookData { - asks: PricePoint[]; - bids: PricePoint[]; + asks: PricePoint[] + bids: PricePoint[] } export interface ClearingPriceAndVolumeData { - clearingOrder: Order; - volume: BigNumber; + clearingOrder: Order + volume: BigNumber } export interface AdditionalServicesEndpoint { - networkId: number; - url_production: string; - url_develop?: string; + networkId: number + url_production: string + url_develop?: string } -function getAdditionalServiceUrl(baseUlr: string): string { - return `${baseUlr}${baseUlr.endsWith("/") ? "" : "/"}api/v1/`; +function getAdditionalServiceUrl(baseUrl: string): string { + return `${baseUrl}${baseUrl.endsWith('/') ? '' : '/'}api/v1/` } -export type AdditionalServicesApiParams = AdditionalServicesEndpoint[]; +export type AdditionalServicesApiParams = AdditionalServicesEndpoint[] export class AdditionalServicesApiImpl implements AdditionalServicesApi { - private urlsByNetwork: { [networkId: number]: string } = {}; + private urlsByNetwork: { [networkId: number]: string } = {} public constructor(params: AdditionalServicesApiParams) { params.forEach((endpoint) => { if (endpoint.url_develop || endpoint.url_production) { this.urlsByNetwork[endpoint.networkId] = getAdditionalServiceUrl( - process.env.PRICE_ESTIMATOR_URL === "production" + process.env.PRICE_ESTIMATOR_URL === 'production' ? endpoint.url_production : endpoint.url_develop || endpoint.url_production, // fallback on required url_production - ); + ) } - }); + }) } public getOrderBookUrl(params: OrderBookParams): string { - const { networkId, auctionId } = params; + const { auctionId, networkId } = params - const baseUrl = this._getBaseUrl(networkId); + const baseUrl = this._getBaseUrl(networkId) - const url = `${baseUrl}get_order_book_display_data/${auctionId}`; - return url; + const url = `${baseUrl}get_order_book_display_data/${auctionId}` + return url } public getClearingPriceOrderAndVolumeUrl(params: OrderBookParams): string { - const { networkId, auctionId } = params; + const { auctionId, networkId } = params - const baseUrl = this._getBaseUrl(networkId); + const baseUrl = this._getBaseUrl(networkId) - const url = `${baseUrl}get_clearing_order_and_volume/${auctionId}`; - return url; + const url = `${baseUrl}get_clearing_order_and_volume/${auctionId}` + return url } public getPreviousOrderUrl(params: PreviousOrderParams): string { - const { networkId, auctionId, order } = params; + const { auctionId, networkId, order } = params - const baseUrl = this._getBaseUrl(networkId); + const baseUrl = this._getBaseUrl(networkId) - const url = `${baseUrl}get_previous_order/${auctionId}/${encodeOrder( - order, - )}`; - return url; + const url = `${baseUrl}get_previous_order/${auctionId}/${encodeOrder(order)}` + return url } public getAllUserOrdersUrl(params: UserOrderParams): string { - const { networkId, auctionId, user } = params; + const { auctionId, networkId, user } = params - const baseUrl = this._getBaseUrl(networkId); + const baseUrl = this._getBaseUrl(networkId) - const url = `${baseUrl}get_user_orders/${auctionId}/${user}`; - return url; + const url = `${baseUrl}get_user_orders/${auctionId}/${user}` + return url } - public getMostInterestingAuctionDetailsUrl( - params: InterestingAuctionParams, - ): string { - const { networkId, numberOfAuctions } = params; + public getMostInterestingAuctionDetailsUrl(params: InterestingAuctionParams): string { + const { networkId, numberOfAuctions } = params - const baseUrl = this._getBaseUrl(networkId); + const baseUrl = this._getBaseUrl(networkId) - const url = `${baseUrl}get_details_of_most_interesting_auctions/${numberOfAuctions}`; - return url; + const url = `${baseUrl}get_details_of_most_interesting_auctions/${numberOfAuctions}` + return url } + public getAllAuctionDetailsUrl(networkId: number): string { - const baseUrl = this._getBaseUrl(networkId); + const baseUrl = this._getBaseUrl(networkId) + + const url = `${baseUrl}get_all_auction_with_details/` + return url + } + + public getAuctionDetailsUrl(params: AuctionDetailParams): string { + const { auctionId, networkId } = params + const baseUrl = this._getBaseUrl(networkId) - const url = `${baseUrl}get_all_auction_with_details/`; - return url; + return `${baseUrl}get_auction_with_details/${auctionId}` } public getCurrentUserOrdersUrl(params: UserOrderParams): string { - const { networkId, auctionId, user } = params; + const { auctionId, networkId, user } = params - const baseUrl = this._getBaseUrl(networkId); + const baseUrl = this._getBaseUrl(networkId) - const url = `${baseUrl}get_user_orders_without_canceled_or_claimed/${auctionId}/${user}`; - return url; + const url = `${baseUrl}get_user_orders_without_canceled_or_claimed/${auctionId}/${user}` + return url } - public async getAllAuctionDetails(): Promise { + public async getAllAuctionDetails(): Promise> { try { - const promises: Promise[] = []; + const promises: Promise[] = [] for (const networkId in this.urlsByNetwork) { - const url = await this.getAllAuctionDetailsUrl(Number(networkId)); + const url = await this.getAllAuctionDetailsUrl(Number(networkId)) - promises.push(fetch(url)); + promises.push(fetch(url)) } - const results = await Promise.all(promises); - const allAuctions = []; + const results = await Promise.all(promises) + const allAuctions = [] for (const res of results) { if (!res.ok) { // backend returns {"message":"invalid url query"} // for bad requests - throw await res.json(); + throw await res.json() } - allAuctions.push(await res.json()); + allAuctions.push(await res.json()) } - return allAuctions.flat(); + return allAuctions.flat() } catch (error) { - console.error(error); + console.error(error) - throw new Error(`Failed to query all auctions: ${error.message}`); + throw new Error(`Failed to query all auctions: ${error.message}`) } } - public async getMostInterestingAuctionDetails( - params: InterestingAuctionParams, - ): Promise { + + public async getAuctionDetails(params: AuctionDetailParams): Promise { try { - const url = await this.getMostInterestingAuctionDetailsUrl(params); + const url = await this.getAuctionDetailsUrl(params) - const res = await fetch(url); + const res = await fetch(url) if (!res.ok) { // backend returns {"message":"invalid url query"} // for bad requests - throw await res.json(); + throw await res.json() } - return await res.json(); + return res.json() } catch (error) { - console.error(error); + console.error(error) - const { networkId } = params; + const { auctionId } = params throw new Error( - `Failed to query interesting auctions for network ${networkId}: ${error.message}`, - ); + `Failed to query auction details for auction id ${auctionId} : ${error.message}`, + ) + } + } + + public async getMostInterestingAuctionDetails(): Promise> { + try { + const promises: Promise[] = [] + for (const networkId in this.urlsByNetwork) { + const url = await this.getMostInterestingAuctionDetailsUrl({ + networkId: Number(networkId), + numberOfAuctions: 3, + }) + + promises.push(fetch(url)) + } + const results = await Promise.all(promises) + const allInterestingAuctions = [] + for (const res of results) { + if (!res.ok) { + // backend returns {"message":"invalid url query"} + // for bad requests + throw await res.json() + } + allInterestingAuctions.push(await res.json()) + } + + const allInterestingAuctionsOrdered = allInterestingAuctions.sort( + (auctionA, auctionB) => auctionB.interestScore - auctionA.interestScore, + ) + return allInterestingAuctionsOrdered.flat() + } catch (error) { + console.error(error) + throw new Error(`Failed to query interesting auctions: ${error.message}`) } } public async getPreviousOrder(params: PreviousOrderParams): Promise { try { - const url = await this.getPreviousOrderUrl(params); + const url = await this.getPreviousOrderUrl(params) - const res = await fetch(url); + const res = await fetch(url) if (!res.ok) { // backend returns {"message":"invalid url query"} // for bad requests - throw await res.json(); + throw await res.json() } - return await res.json(); + return await res.json() } catch (error) { - console.error(error); + console.error(error) - const { auctionId, order } = params; + const { auctionId, order } = params throw new Error( `Failed to query previous order for auction id ${auctionId} and order ${encodeOrder( order, )}: ${error.message}`, - ); + ) } } public async getAllUserOrders(params: UserOrderParams): Promise { try { - const url = await this.getAllUserOrdersUrl(params); + const url = await this.getAllUserOrdersUrl(params) - const res = await fetch(url); + const res = await fetch(url) if (!res.ok) { // backend returns {"message":"invalid url query"} // for bad requests - throw await res.json(); + throw await res.json() } - return await res.json(); + return await res.json() } catch (error) { - console.error(error); + console.error(error) - const { auctionId, user } = params; + const { auctionId, user } = params throw new Error( `Failed to query previous order for auction id ${auctionId} and order ${user}: ${error.message}`, - ); + ) } } - public async getCurrentUserOrders( - params: UserOrderParams, - ): Promise { + public async getCurrentUserOrders(params: UserOrderParams): Promise { try { - const url = await this.getCurrentUserOrdersUrl(params); + const url = await this.getCurrentUserOrdersUrl(params) - const res = await fetch(url); + const res = await fetch(url) if (!res.ok) { // backend returns {"message":"invalid url query"} // for bad requests - throw await res.json(); + throw await res.json() } - return await res.json(); + return await res.json() } catch (error) { - console.error(error); + console.error(error) - const { auctionId, user } = params; + const { auctionId, user } = params throw new Error( `Failed to query previous order for auction id ${auctionId} and order ${user}: ${error.message}`, - ); + ) } } @@ -276,86 +314,80 @@ export class AdditionalServicesApiImpl implements AdditionalServicesApi { params: OrderBookParams, ): Promise { try { - const url = await this.getClearingPriceOrderAndVolumeUrl(params); + const url = await this.getClearingPriceOrderAndVolumeUrl(params) - const res = await fetch(url); + const res = await fetch(url) if (!res.ok) { // backend returns {"message":"invalid url query"} // for bad requests - throw await res.json(); + throw await res.json() } - const result = await res.json(); + const result = await res.json() return { clearingOrder: decodeOrder(result[0]), volume: BigNumber.from(result[1]), - }; - return await res.json(); + } } catch (error) { - console.error(error); + console.error(error) - const { auctionId } = params; + const { auctionId } = params throw new Error( `Failed to query clearing price order for auction id ${auctionId} : ${error.message}`, - ); + ) } } - public async getOrderBookData( - params: OrderBookParams, - ): Promise { + public async getOrderBookData(params: OrderBookParams): Promise { try { - const url = await this.getOrderBookUrl(params); + const url = await this.getOrderBookUrl(params) - const res = await fetch(url); + const res = await fetch(url) if (!res.ok) { // backend returns {"message":"invalid url query"} // for bad requests - throw await res.json(); + throw await res.json() } - return await res.json(); + return await res.json() } catch (error) { - console.error(error); + console.error(error) - const { auctionId } = params; + const { auctionId } = params throw new Error( `Failed to query orderbook data for auction id ${auctionId} : ${error.message}`, - ); + ) } } - private async query( - networkId: number, - queryString: string, - ): Promise { - const baseUrl = this._getBaseUrl(networkId); + private async query(networkId: number, queryString: string): Promise> { + const baseUrl = this._getBaseUrl(networkId) - const url = baseUrl + queryString; + const url = baseUrl + queryString - const response = await fetch(url); + const response = await fetch(url) if (!response.ok) { - throw new Error(`Request failed: [${response.status}] ${response.body}`); + throw new Error(`Request failed: [${response.status}] ${response.body}`) } - const body = await response.text(); + const body = await response.text() if (!body) { - return null; + return null } - return JSON.parse(body); + return JSON.parse(body) } private _getBaseUrl(networkId: number): string { - const baseUrl = this.urlsByNetwork[networkId]; - if (typeof baseUrl === "undefined") { + const baseUrl = this.urlsByNetwork[networkId] + if (typeof baseUrl === 'undefined') { throw new Error( `REACT_APP_ADDITIONAL_SERVICES_API_URL must be a defined environment variable for network ${networkId}`, - ); + ) } - return baseUrl; + return baseUrl } } diff --git a/src/api/TokenLogosServiceApi.ts b/src/api/TokenLogosServiceApi.ts new file mode 100644 index 000000000..e326d1647 --- /dev/null +++ b/src/api/TokenLogosServiceApi.ts @@ -0,0 +1,61 @@ +import { TokenInfo, TokenList } from '@uniswap/token-lists' +import schema from '@uniswap/token-lists/src/tokenlist.schema.json' +import Ajv from 'ajv' + +const TOKEN_LIST_RESOURCES = [ + 'https://tokens.coingecko.com/uniswap/all.json', + 'https://raw.githubusercontent.com/gnosis/gp-swap-ui/develop/src/custom/tokens/rinkeby-token-list.json', + 'https://tokens.honeyswap.org', +] +const tokenListValidator = new Ajv({ allErrors: true }).compile(schema) + +export interface TokenLogosServiceApiInterface { + getTokensByUrl(url: string): Promise + getAllTokens(): Promise +} + +export class TokenLogosServiceApi implements TokenLogosServiceApiInterface { + public async getTokensByUrl(url: string): Promise { + try { + const response = await fetch(url) + + if (!response.ok) { + throw new Error('Invalid token list response.') + } + + const data = await response.json() + + if (!tokenListValidator(data)) { + console.error(tokenListValidator.errors) + + throw new Error('Token list failed validation') + } + + return data as TokenList + } catch (error) { + console.error(error) + + throw new Error(`Failed to fetch token list from URL ${url}`) + } + } + + public async getAllTokens(): Promise { + const tokens: TokenInfo[] = [] + + try { + const [coingeckoTokenList, gnosisTokenList, honeyswapTokenList] = await Promise.all( + TOKEN_LIST_RESOURCES.map((url) => this.getTokensByUrl(url)), + ) + + tokens.push(...coingeckoTokenList.tokens) + tokens.push(...gnosisTokenList.tokens) + tokens.push(...honeyswapTokenList.tokens) + } catch (error) { + console.error(error) + + throw new Error('Failed to get all tokens') + } + + return tokens + } +} diff --git a/src/api/index.tsx b/src/api/index.tsx index ab42555a7..0ad9bedd7 100644 --- a/src/api/index.tsx +++ b/src/api/index.tsx @@ -1,46 +1,42 @@ +import { + API_URL_DEVELOP_MAINNET, + API_URL_DEVELOP_RINKEBY, + API_URL_DEVELOP_XDAI, + API_URL_PRODUCTION_MAINNET, + API_URL_PRODUCTION_RINKEBY, + API_URL_PRODUCTION_XDAI, +} from '../constants/config' import { AdditionalServicesApi, - AdditionalServicesEndpoint, AdditionalServicesApiImpl, -} from "./AdditionalServicesApi"; + AdditionalServicesEndpoint, +} from './AdditionalServicesApi' +import { TokenLogosServiceApi, TokenLogosServiceApiInterface } from './TokenLogosServiceApi' function createAdditionalServiceApi(): AdditionalServicesApi { - const url_develop_rinkeby = - process.env.REACT_APP_ADDITIONAL_SERVICES_API_URL_RINKEBY; - const url_production_rinkeby = - process.env.REACT_APP_ADDITIONAL_SERVICES_API_URL_PROD_RINKEBY; - - const url_develop_mainnet = - process.env.REACT_APP_ADDITIONAL_SERVICES_API_URL_MAINNET; - const url_production_mainnet = - process.env.REACT_APP_ADDITIONAL_SERVICES_API_URL_PROD_MAINNET; - - const url_develop_xdai = - process.env.REACT_APP_ADDITIONAL_SERVICES_API_URL_XDAI; - const url_production_xdai = - process.env.REACT_APP_ADDITIONAL_SERVICES_API_URL_PROD_XDAI; const config: AdditionalServicesEndpoint[] = [ { networkId: 4, - url_production: url_production_rinkeby, - url_develop: url_develop_rinkeby, + url_production: API_URL_PRODUCTION_RINKEBY, + url_develop: API_URL_DEVELOP_RINKEBY, }, { networkId: 100, - url_production: url_production_xdai, - url_develop: url_develop_xdai, + url_production: API_URL_PRODUCTION_XDAI, + url_develop: API_URL_DEVELOP_XDAI, }, { networkId: 1, - url_production: url_production_mainnet, - url_develop: url_develop_mainnet, + url_production: API_URL_PRODUCTION_MAINNET, + url_develop: API_URL_DEVELOP_MAINNET, }, - ]; - const dexPriceEstimatorApi = new AdditionalServicesApiImpl(config); + ] + const dexPriceEstimatorApi = new AdditionalServicesApiImpl(config) - window["dexPriceEstimatorApi"] = dexPriceEstimatorApi; - return dexPriceEstimatorApi; + window['dexPriceEstimatorApi'] = dexPriceEstimatorApi + return dexPriceEstimatorApi } // Build APIs -export const additionalServiceApi: AdditionalServicesApi = createAdditionalServiceApi(); +export const additionalServiceApi: AdditionalServicesApi = createAdditionalServiceApi() +export const tokenLogosServiceApi: TokenLogosServiceApiInterface = new TokenLogosServiceApi() diff --git a/src/components/AccountDetails/Copy.tsx b/src/components/AccountDetails/Copy.tsx index 1532c53e9..2a7782e8d 100644 --- a/src/components/AccountDetails/Copy.tsx +++ b/src/components/AccountDetails/Copy.tsx @@ -1,9 +1,9 @@ -import React from "react"; -import styled from "styled-components"; -import useCopyClipboard from "../../hooks/useCopyClipboard"; +import React from 'react' +import { CheckCircle, Copy } from 'react-feather' +import styled from 'styled-components' -import { LinkStyledButton } from "../../theme"; -import { CheckCircle, Copy } from "react-feather"; +import useCopyClipboard from '../../hooks/useCopyClipboard' +import { LinkStyledButton } from '../../theme' const CopyIcon = styled(LinkStyledButton)` color: ${({ theme }) => theme.text4}; @@ -18,32 +18,29 @@ const CopyIcon = styled(LinkStyledButton)` text-decoration: none; color: ${({ theme }) => theme.text3}; } -`; +` const TransactionStatusText = styled.span` margin-left: 0.25rem; ${({ theme }) => theme.flexRowNoWrap}; align-items: center; -`; +` -export default function CopyHelper(props: { - toCopy: string; - children?: React.ReactNode; -}) { - const [isCopied, setCopied] = useCopyClipboard(); +export default function CopyHelper(props: { toCopy: string; children?: React.ReactNode }) { + const [isCopied, setCopied] = useCopyClipboard() return ( setCopied(props.toCopy)}> {props.children} {isCopied ? ( - + Copied ) : ( - + )} - ); + ) } diff --git a/src/components/AccountDetails/Transaction.tsx b/src/components/AccountDetails/Transaction.tsx index 9e6207226..408aa8939 100644 --- a/src/components/AccountDetails/Transaction.tsx +++ b/src/components/AccountDetails/Transaction.tsx @@ -1,82 +1,53 @@ -import React from "react"; -import styled from "styled-components"; -import { Check, Triangle } from "react-feather"; +import React from 'react' +import { Check, Triangle } from 'react-feather' +import styled from 'styled-components' -import { useActiveWeb3React } from "../../hooks"; -import { getEtherscanLink } from "../../utils"; -import { ExternalLink, Spinner } from "../../theme"; -import Circle from "../../assets/images/circle.svg"; - -import { transparentize } from "polished"; -import { useAllTransactions } from "../../state/transactions/hooks"; +import Circle from '../../assets/images/circle.svg' +import { useActiveWeb3React } from '../../hooks' +import { useAllTransactions } from '../../state/transactions/hooks' +import { ExternalLink, Spinner } from '../../theme' +import { getEtherscanLink } from '../../utils' const TransactionWrapper = styled.div` margin-top: 0.75rem; -`; +` const TransactionStatusText = styled.div` margin-right: 0.5rem; -`; +` -const TransactionState = styled(ExternalLink)<{ - pending: boolean; - success?: boolean; -}>` +const TransactionState = styled(ExternalLink)` + border-color: #fff; + border-radius: 0.5rem; + border: 1px solid; + color: #fff; display: flex; + font-size: 0.75rem; + font-weight: 500; justify-content: space-between; - text-decoration: none !important; - - border-radius: 0.5rem; padding: 0.5rem 0.75rem; - font-weight: 500; - font-size: 0.75rem; - border: 1px solid; - - color: ${({ pending, success, theme }) => - pending ? theme.primary1 : success ? theme.green1 : theme.red1}; - - border-color: ${({ pending, success, theme }) => - pending - ? transparentize(0.75, theme.primary1) - : success - ? transparentize(0.75, theme.green1) - : transparentize(0.75, theme.red1)}; - - :hover { - border-color: ${({ pending, success, theme }) => - pending - ? transparentize(0, theme.primary1) - : success - ? transparentize(0, theme.green1) - : transparentize(0, theme.red1)}; - } -`; + text-decoration: none !important; +` const IconWrapper = styled.div` flex-shrink: 0; -`; +` export default function Transaction({ hash }: { hash: string }) { - const { chainId } = useActiveWeb3React(); - const allTransactions = useAllTransactions(); + const { chainId } = useActiveWeb3React() + const allTransactions = useAllTransactions() - const summary = allTransactions?.[hash]?.summary; - const pending = !allTransactions?.[hash]?.receipt; + const summary = allTransactions?.[hash]?.summary + const pending = !allTransactions?.[hash]?.receipt const success = !pending && (allTransactions[hash].receipt.status === 1 || - typeof allTransactions[hash].receipt.status === "undefined"); + typeof allTransactions[hash].receipt.status === 'undefined') return ( - - - {summary ? summary : hash} - + + {summary ? summary : hash} {pending ? ( @@ -88,5 +59,5 @@ export default function Transaction({ hash }: { hash: string }) { - ); + ) } diff --git a/src/components/AccountDetails/index.tsx b/src/components/AccountDetails/index.tsx index 8e8bc7e82..e0a99373d 100644 --- a/src/components/AccountDetails/index.tsx +++ b/src/components/AccountDetails/index.tsx @@ -1,44 +1,36 @@ -import React, { useCallback, useContext } from "react"; -import { useDispatch } from "react-redux"; -import styled, { ThemeContext } from "styled-components"; -import { useActiveWeb3React } from "../../hooks"; -import { isMobile } from "react-device-detect"; -import { AppDispatch } from "../../state"; -import { clearAllTransactions } from "../../state/transactions/actions"; -import { AutoRow } from "../Row"; -import Copy from "./Copy"; -import Transaction from "./Transaction"; - -import { SUPPORTED_WALLETS } from "../../constants"; -import { ReactComponent as Close } from "../../assets/images/x.svg"; -import { getEtherscanLink } from "../../utils"; -import { - injected, - walletconnect, - walletlink, - fortmatic, - portis, -} from "../../connectors"; -import CoinbaseWalletIcon from "../../assets/images/coinbaseWalletIcon.svg"; -import WalletConnectIcon from "../../assets/images/walletConnectIcon.svg"; -import FortmaticIcon from "../../assets/images/fortmaticIcon.png"; -import PortisIcon from "../../assets/images/portisIcon.png"; -import Identicon from "../Identicon"; - -import { ButtonEmpty } from "../Button"; - -import { ExternalLink, LinkStyledButton, TYPE } from "../../theme"; +import React, { useCallback, useContext } from 'react' +import styled, { ThemeContext } from 'styled-components' + +import { isMobile } from 'react-device-detect' +import { useDispatch } from 'react-redux' + +import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg' +import FortmaticIcon from '../../assets/images/fortmaticIcon.png' +import PortisIcon from '../../assets/images/portisIcon.png' +import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg' +import { ReactComponent as Close } from '../../assets/images/x.svg' +import { fortmatic, injected, portis, walletconnect, walletlink } from '../../connectors' +import { SUPPORTED_WALLETS } from '../../constants' +import { useActiveWeb3React } from '../../hooks' +import { AppDispatch } from '../../state' +import { clearAllTransactions } from '../../state/transactions/actions' +import { ExternalLink, LinkStyledButton, TYPE } from '../../theme' +import { getEtherscanLink } from '../../utils' +import { ButtonEmpty } from '../Button' +import Identicon from '../Identicon' +import { AutoRow } from '../Row' +import Copy from './Copy' +import Transaction from './Transaction' const HeaderRow = styled.div` ${({ theme }) => theme.flexRowNoWrap}; padding: 1rem 1rem; font-weight: 500; - color: ${(props) => - props.color === "blue" ? ({ theme }) => theme.primary1 : "inherit"}; + color: ${(props) => (props.color === 'blue' ? ({ theme }) => theme.primary1 : 'inherit')}; ${({ theme }) => theme.mediaWidth.upToMedium` padding: 1rem; `}; -`; +` const UpperSection = styled.div` position: relative; @@ -58,13 +50,13 @@ const UpperSection = styled.div` margin-top: 0; font-weight: 500; } -`; +` const InfoCard = styled.div` padding: 1rem; background-color: ${({ theme }) => theme.bg2}; border-radius: 20px; -`; +` const AccountGroupingRow = styled.div` ${({ theme }) => theme.flexRowNoWrap}; @@ -81,13 +73,13 @@ const AccountGroupingRow = styled.div` &:first-of-type { margin-bottom: 8px; } -`; +` const AccountSection = styled.div` background-color: ${({ theme }) => theme.bg1}; padding: 0rem 1rem; ${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1rem 1rem;`}; -`; +` const YourAccount = styled.div` h5 { @@ -99,7 +91,7 @@ const YourAccount = styled.div` margin: 0; font-weight: 500; } -`; +` const GreenCircle = styled.div` ${({ theme }) => theme.flexRowNoWrap} @@ -114,14 +106,14 @@ const GreenCircle = styled.div` background-color: ${({ theme }) => theme.green1}; border-radius: 50%; } -`; +` const CircleWrapper = styled.div` color: ${({ theme }) => theme.green1}; display: flex; justify-content: center; align-items: center; -`; +` const LowerSection = styled.div` ${({ theme }) => theme.flexColumnNoWrap} @@ -137,7 +129,7 @@ const LowerSection = styled.div` font-weight: 400; color: ${({ theme }) => theme.text3}; } -`; +` const AccountControl = styled.div<{ hasENS: boolean; isENS: boolean }>` ${({ theme }) => theme.flexRowNoWrap}; @@ -145,8 +137,7 @@ const AccountControl = styled.div<{ hasENS: boolean; isENS: boolean }>` min-width: 0; font-weight: ${({ hasENS, isENS }) => (hasENS ? (isENS ? 500 : 400) : 500)}; - font-size: ${({ hasENS, isENS }) => - hasENS ? (isENS ? "1rem" : "0.8rem") : "1rem"}; + font-size: ${({ hasENS, isENS }) => (hasENS ? (isENS ? '1rem' : '0.8rem') : '1rem')}; a:hover { text-decoration: underline; @@ -159,19 +150,19 @@ const AccountControl = styled.div<{ hasENS: boolean; isENS: boolean }>` text-overflow: ellipsis; white-space: nowrap; } -`; +` const ConnectButtonRow = styled.div` ${({ theme }) => theme.flexRowNoWrap} align-items: center; justify-content: center; margin: 10px 0; -`; +` const AddressLink = styled(ExternalLink)<{ hasENS: boolean; isENS: boolean }>` color: ${({ hasENS, isENS, theme }) => hasENS ? (isENS ? theme.primary1 : theme.text3) : theme.primary1}; -`; +` const CloseIcon = styled.div` position: absolute; @@ -181,18 +172,18 @@ const CloseIcon = styled.div` cursor: pointer; opacity: 0.6; } -`; +` const CloseColor = styled(Close)` path { stroke: ${({ theme }) => theme.text4}; } -`; +` const WalletName = styled.div` padding-left: 0.5rem; width: initial; -`; +` const IconWrapper = styled.div<{ size?: number }>` ${({ theme }) => theme.flexColumnNoWrap}; @@ -200,17 +191,17 @@ const IconWrapper = styled.div<{ size?: number }>` justify-content: center; & > img, span { - height: ${({ size }) => (size ? size + "px" : "32px")}; - width: ${({ size }) => (size ? size + "px" : "32px")}; + height: ${({ size }) => (size ? size + 'px' : '32px')}; + width: ${({ size }) => (size ? size + 'px' : '32px')}; } ${({ theme }) => theme.mediaWidth.upToMedium` align-items: flex-end; `}; -`; +` const TransactionListWrapper = styled.div` ${({ theme }) => theme.flexColumnNoWrap}; -`; +` const WalletAction = styled.div` color: ${({ theme }) => theme.text4}; @@ -220,52 +211,52 @@ const WalletAction = styled.div` cursor: pointer; text-decoration: underline; } -`; +` const MainWalletAction = styled(WalletAction)` color: ${({ theme }) => theme.primary1}; -`; +` function renderTransactions(transactions) { return ( {transactions.map((hash, i) => { - return ; + return })} - ); + ) } interface AccountDetailsProps { - toggleWalletModal: () => void; - pendingTransactions: any[]; - confirmedTransactions: any[]; - ENSName?: string; - openOptions: () => void; + toggleWalletModal: () => void + pendingTransactions: any[] + confirmedTransactions: any[] + ENSName?: string + openOptions: () => void } export default function AccountDetails({ - toggleWalletModal, - pendingTransactions, - confirmedTransactions, ENSName, + confirmedTransactions, openOptions, + pendingTransactions, + toggleWalletModal, }: AccountDetailsProps) { - const { chainId, account, connector } = useActiveWeb3React(); - const theme = useContext(ThemeContext); - const dispatch = useDispatch(); + const { account, chainId, connector } = useActiveWeb3React() + const theme = useContext(ThemeContext) + const dispatch = useDispatch() function formatConnectorName() { - const { ethereum } = window; - const isMetaMask = !!(ethereum && ethereum.isMetaMask); + const { ethereum } = window + const isMetaMask = !!(ethereum && ethereum.isMetaMask) const name = Object.keys(SUPPORTED_WALLETS) .filter( (k) => SUPPORTED_WALLETS[k].connector === connector && - (connector !== injected || isMetaMask === (k === "METAMASK")), + (connector !== injected || isMetaMask === (k === 'METAMASK')), ) - .map((k) => SUPPORTED_WALLETS[k].name)[0]; - return {name}; + .map((k) => SUPPORTED_WALLETS[k].name)[0] + return {name} } function getStatusIcon() { @@ -274,50 +265,50 @@ export default function AccountDetails({ {formatConnectorName()} - ); + ) } else if (connector === walletconnect) { return ( - {""} {formatConnectorName()} + {''} {formatConnectorName()} - ); + ) } else if (connector === walletlink) { return ( - {""} {formatConnectorName()} + {''} {formatConnectorName()} - ); + ) } else if (connector === fortmatic) { return ( - {""} {formatConnectorName()} + {''} {formatConnectorName()} - ); + ) } else if (connector === portis) { return ( <> - {""} {formatConnectorName()} + {''} {formatConnectorName()} { - portis.portis.showPortis(); + portis.portis.showPortis() }} > Show Portis - ); + ) } } const clearAllTransactionsCallback = useCallback( (event: React.MouseEvent) => { - event.preventDefault(); - dispatch(clearAllTransactions({ chainId })); + event.preventDefault() + dispatch(clearAllTransactions({ chainId })) }, [dispatch, chainId], - ); + ) return ( <> @@ -335,7 +326,7 @@ export default function AccountDetails({ {connector !== injected && connector !== walletlink && ( { - (connector as any).close(); + ;(connector as any).close() }} > Disconnect @@ -369,8 +360,8 @@ export default function AccountDetails({ View on Etherscan ↗ @@ -381,8 +372,8 @@ export default function AccountDetails({ View on Etherscan ↗ @@ -396,12 +387,12 @@ export default function AccountDetails({ {!(isMobile && (window.web3 || window.ethereum)) && ( { - openOptions(); + openOptions() }} + padding={'12px'} + style={{ fontWeight: 400 }} + width={'260px'} > Connect to a different wallet @@ -411,22 +402,18 @@ export default function AccountDetails({ {!!pendingTransactions.length || !!confirmedTransactions.length ? ( - + Recent Transactions - - (clear all) - + (clear all) {renderTransactions(pendingTransactions)} {renderTransactions(confirmedTransactions)} ) : ( - - Your transactions will appear here... - + Your transactions will appear here... )} - ); + ) } diff --git a/src/components/AddressInputPanel/index.tsx b/src/components/AddressInputPanel/index.tsx index 54ba7d07d..7f005601f 100644 --- a/src/components/AddressInputPanel/index.tsx +++ b/src/components/AddressInputPanel/index.tsx @@ -1,13 +1,12 @@ -import React, { useState, useEffect, useContext } from "react"; -import styled, { ThemeContext } from "styled-components"; -import useDebounce from "../../hooks/useDebounce"; +import React, { useContext, useEffect, useState } from 'react' +import styled, { ThemeContext } from 'styled-components' -import { isAddress } from "../../utils"; -import { useActiveWeb3React } from "../../hooks"; -import { ExternalLink, TYPE } from "../../theme"; -import { AutoColumn } from "../Column"; -import { RowBetween } from "../Row"; -import { getEtherscanLink } from "../../utils"; +import { useActiveWeb3React } from '../../hooks' +import useDebounce from '../../hooks/useDebounce' +import { ExternalLink, TYPE } from '../../theme' +import { getEtherscanLink, isAddress } from '../../utils' +import { AutoColumn } from '../Column' +import { RowBetween } from '../Row' const InputPanel = styled.div` ${({ theme }) => theme.flexColumnNoWrap} @@ -16,7 +15,7 @@ const InputPanel = styled.div` background-color: ${({ theme }) => theme.bg1}; z-index: 1; width: 100%; -`; +` const ContainerRow = styled.div<{ error: boolean }>` display: flex; @@ -25,12 +24,12 @@ const ContainerRow = styled.div<{ error: boolean }>` border-radius: 1.25rem; border: 1px solid ${({ error, theme }) => (error ? theme.red1 : theme.bg2)}; background-color: ${({ theme }) => theme.bg1}; -`; +` const InputContainer = styled.div` flex: 1; padding: 1rem; -`; +` const Input = styled.input<{ error?: boolean }>` font-size: 1.25rem; @@ -62,133 +61,129 @@ const Input = styled.input<{ error?: boolean }>` ::placeholder { color: ${({ theme }) => theme.text4}; } -`; +` export default function AddressInputPanel({ - initialInput = "", + initialInput = '', onChange, onError, }: { - initialInput?: string; - onChange: (val: { address: string; name?: string }) => void; - onError: (error: boolean, input: string) => void; + initialInput?: string + onChange: (val: { address: string; name?: string }) => void + onError: (error: boolean, input: string) => void }) { - const { chainId, library } = useActiveWeb3React(); - const theme = useContext(ThemeContext); + const { chainId, library } = useActiveWeb3React() + const theme = useContext(ThemeContext) - const [input, setInput] = useState(initialInput ? initialInput : ""); - const debouncedInput = useDebounce(input, 200); + const [input, setInput] = useState(initialInput ? initialInput : '') + const debouncedInput = useDebounce(input, 200) const [data, setData] = useState<{ address: string; name: string }>({ address: undefined, name: undefined, - }); - const [error, setError] = useState(false); + }) + const [error, setError] = useState(false) // keep data and errors in sync useEffect(() => { - onChange({ address: data.address, name: data.name }); - }, [onChange, data.address, data.name]); + onChange({ address: data.address, name: data.name }) + }, [onChange, data.address, data.name]) useEffect(() => { - onError(error, input); - }, [onError, error, input]); + onError(error, input) + }, [onError, error, input]) // run parser on debounced input useEffect(() => { - let stale = false; + let stale = false // if the input is an address, try to look up its name if (isAddress(debouncedInput)) { library .lookupAddress(debouncedInput) .then((name) => { - if (stale) return; + if (stale) return // if an ENS name exists, set it as the destination if (name) { - setInput(name); + setInput(name) } else { - setData({ address: debouncedInput, name: "" }); - setError(null); + setData({ address: debouncedInput, name: '' }) + setError(null) } }) .catch(() => { - if (stale) return; - setData({ address: debouncedInput, name: "" }); - setError(null); - }); + if (stale) return + setData({ address: debouncedInput, name: '' }) + setError(null) + }) } // otherwise try to look up the address of the input, treated as an ENS name else { - if (debouncedInput !== "") { + if (debouncedInput !== '') { library .resolveName(debouncedInput) .then((address) => { - if (stale) return; + if (stale) return // if the debounced input name resolves to an address if (address) { - setData({ address: address, name: debouncedInput }); - setError(null); + setData({ address: address, name: debouncedInput }) + setError(null) } else { - setError(true); + setError(true) } }) .catch(() => { - if (stale) return; - setError(true); - }); - } else if (debouncedInput === "") { - setError(true); + if (stale) return + setError(true) + }) + } else if (debouncedInput === '') { + setError(true) } } return () => { - stale = true; - }; - }, [debouncedInput, library]); + stale = true + } + }, [debouncedInput, library]) function onInput(event) { - setData({ address: undefined, name: undefined }); - setError(false); - const input = event.target.value; - const checksummedInput = isAddress(input.replace(/\s/g, "")); // delete whitespace - setInput(checksummedInput || input); + setData({ address: undefined, name: undefined }) + setError(false) + const input = event.target.value + const checksummedInput = isAddress(input.replace(/\s/g, '')) // delete whitespace + setInput(checksummedInput || input) } return ( - + - + Recipient {data.address && ( (View on Etherscan) )} - ); + ) } diff --git a/src/components/AllAuctionsTable/index.tsx b/src/components/AllAuctionsTable/index.tsx index 980bd7fdb..e3a92f265 100644 --- a/src/components/AllAuctionsTable/index.tsx +++ b/src/components/AllAuctionsTable/index.tsx @@ -1,6 +1,7 @@ -import React, { useMemo } from "react"; -import { useTable, useGlobalFilter } from "react-table"; -import styled from "styled-components"; +import React, { useMemo } from 'react' +import styled from 'styled-components' + +import { useGlobalFilter, useTable } from 'react-table' const Styles = styled.div` padding: 1rem; @@ -22,91 +23,88 @@ const Styles = styled.div` border-bottom: 1px solid black; } } -`; +` // value and onChange function const GlobalFilter = ({ globalFilter, setGlobalFilter }: any) => { return ( { - setGlobalFilter(e.target.value || undefined); // Set undefined to remove the filter entirely + setGlobalFilter(e.target.value || undefined) // Set undefined to remove the filter entirely }} placeholder={`Search All ...`} + value={globalFilter || ''} /> - ); -}; + ) +} export default function DatatablePage(allAuctions: any[]) { const columns = useMemo( () => [ { - Header: "Pair", - accessor: "symbol", + Header: 'Pair', + accessor: 'symbol', minWidth: 300, }, { - Header: "#AuctionId", - accessor: "auctionId", + Header: '#AuctionId', + accessor: 'auctionId', minWidth: 50, }, { - Header: "Network", - accessor: "chainId", + Header: 'Network', + accessor: 'chainId', minWidth: 50, }, { - Header: "Selling", - accessor: "selling", + Header: 'Selling', + accessor: 'selling', minWidth: 50, }, { - Header: "Buying", - accessor: "buying", + Header: 'Buying', + accessor: 'buying', minWidth: 50, }, { - Header: "Status", - accessor: "status", + Header: 'Status', + accessor: 'status', minWidth: 100, }, { - Header: "End date", - accessor: "date", + Header: 'End date', + accessor: 'date', minWidth: 50, }, { - Header: "Link", - accessor: "link", + Header: 'Link', + accessor: 'link', minWidth: 50, }, ], [], - ); - const data = useMemo(() => Object.values(allAuctions), [allAuctions]); + ) + const data = useMemo(() => Object.values(allAuctions), [allAuctions]) const { - getTableProps, getTableBodyProps, + getTableProps, headerGroups, - rows, prepareRow, - state, + rows, setGlobalFilter, + state, } = useTable( { columns, data, }, useGlobalFilter, - ); + ) return ( <> - + @@ -115,7 +113,7 @@ export default function DatatablePage(allAuctions: any[]) { {headerGroup.headers.map((column, j) => ( ))} @@ -123,22 +121,22 @@ export default function DatatablePage(allAuctions: any[]) { {rows.map((row, i) => { - prepareRow(row); + prepareRow(row) return ( {row.cells.map((cell, j) => { return ( - ); + ) })} - ); + ) })}
- {column.render("Header")} + {column.render('Header')}
- {cell.render("Cell")} + {cell.render('Cell')}
- ); + ) } diff --git a/src/components/AuctionDetails/index.tsx b/src/components/AuctionDetails/index.tsx deleted file mode 100644 index f522e3f51..000000000 --- a/src/components/AuctionDetails/index.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import React, { useMemo } from "react"; -import styled from "styled-components"; -import { ExternalLink } from "../../theme"; -import { - useDerivedAuctionInfo, - AuctionState, - useDerivedAuctionState, - orderToPrice, - orderToSellOrder, -} from "../../state/orderPlacement/hooks"; - -import { OrderBookBtn } from "../OrderbookBtn"; -import { getEtherscanLink, getTokenDisplay } from "../../utils"; -import { useActiveWeb3React } from "../../hooks"; -import { useClearingPriceInfo } from "../../hooks/useCurrentClearingOrderAndVolumeCallback"; - -const Wrapper = styled.div` - position: relative; - width: calc(60% - 8px); - background: none; - box-shadow: none; - border-radius: 20px; - padding: 0px; - flex: 0 1 auto; - box-sizing: border-box; - display: flex; - flex-flow: column wrap; - align-items: center; - - ${({ theme }) => theme.mediaWidth.upToMedium` - width: 100%; - `}; -`; - -const Details = styled.div` - color: ${({ theme }) => theme.text1}; - font-size: 13px; - font-weight: normal; - align-items: center; - margin-right: auto; - margin-left: auto; - width: 100%; - display: flex; - flex-flow: column wrap; - padding: 16px; - margin: 16px 0 0; - border-radius: 20px; - border: 1px solid ${({ theme }) => theme.bg2}; -`; - -const Row = styled.span` - flex-flow: row-wrap; - width: 100%; - justify-content: space-between; - align-items: flex; - margin: 0 0 4px 0; - font-weight: normal; - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-areas: "label value"; - - > i { - color: ${({ theme }) => theme.text3}; - font-style: normal; - text-align: left; - } - - > a { - text-align: right; - } - - > p { - margin: 0; - padding: 0; - text-align: right; - white-space: normal; - } -`; - -export default function AuctionDetails() { - const { chainId } = useActiveWeb3React(); - const { - auctioningToken, - biddingToken, - clearingPrice, - initialAuctionOrder, - initialPrice, - } = useDerivedAuctionInfo(); - const { auctionState } = useDerivedAuctionState(); - - const auctionTokenAddress = useMemo( - () => getEtherscanLink(chainId, auctioningToken?.address, "address"), - [chainId, auctioningToken], - ); - - const biddingTokenAddress = useMemo( - () => getEtherscanLink(chainId, biddingToken?.address, "address"), - [chainId, biddingToken], - ); - - const clearingPriceInfo = useClearingPriceInfo(); - - const biddingTokenDisplay = useMemo(() => getTokenDisplay(biddingToken), [ - biddingToken, - ]); - - const auctioningTokenDisplay = useMemo( - () => getTokenDisplay(auctioningToken), - [auctioningToken], - ); - - const clearingPriceDisplay = useMemo(() => { - const clearingPriceInfoAsSellOrder = - clearingPriceInfo && - orderToSellOrder( - clearingPriceInfo.clearingOrder, - biddingToken, - auctioningToken, - ); - let clearingPriceNumber = orderToPrice( - clearingPriceInfoAsSellOrder, - )?.toSignificant(4); - - if (clearingPrice) { - clearingPriceNumber = clearingPrice && clearingPrice.toSignificant(4); - } - - return !!clearingPriceNumber - ? `${clearingPriceNumber} ${getTokenDisplay( - biddingToken, - )} per ${getTokenDisplay(auctioningToken)}` - : "-"; - }, [auctioningToken, biddingToken, clearingPrice, clearingPriceInfo]); - - const titlePrice = useMemo( - () => - auctionState == AuctionState.ORDER_PLACING || - auctionState == AuctionState.ORDER_PLACING_AND_CANCELING - ? "Current price" - : auctionState == AuctionState.PRICE_SUBMISSION - ? "Clearing price" - : "Closing price", - [auctionState], - ); - - return ( - - -
- - - {titlePrice} - -

{clearingPriceDisplay}

-
- - Bidding with - - {biddingTokenDisplay} ↗ - - - - - Total auctioned -

- {initialAuctionOrder?.sellAmount.toSignificant(2)}{" "} - - {auctioningTokenDisplay} ↗ - -

-
- - - Min. sell price -

- {initialPrice ? `${initialPrice?.toSignificant(2)} ` : " - "} - {biddingTokenDisplay} per {auctioningTokenDisplay} -

-
-
-
- ); -} diff --git a/src/components/AuctionHeader/index.tsx b/src/components/AuctionHeader/index.tsx deleted file mode 100644 index 2a418c095..000000000 --- a/src/components/AuctionHeader/index.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React from "react"; -import { - AuctionState, - SellOrder, - useDerivedAuctionInfo, - useDerivedAuctionState, -} from "../../state/orderPlacement/hooks"; -import styled from "styled-components"; -import CountdownTimer from "../CountDown"; -import { Token } from "uniswap-xdai-sdk"; -import { getTokenDisplay } from "../../utils"; - -const Wrapper = styled.div` - display: flex; - width: 100%; - align-content: center; - text-align: center; - flex-flow: row nowrap; - justify-content: space-between; - margin: 0; - background: ${({ theme }) => theme.bg2}; - border-radius: 20px; - padding: 16px; - box-sizing: border-box; - margin: 0 0 16px; - - ${({ theme }) => theme.mediaWidth.upToMedium` - flex-flow: column wrap; - `}; - - > h3 { - flex: 1 1 auto; - display: flex; - text-align: center; - align-items: center; - margin: 0 auto; - font-weight: normal; - } - - > h4 { - flex: 1 1 auto; - display: flex; - text-align: center; - align-items: center; - margin: 0 auto; - font-size: 18px; - font-weight: normal; - - ${({ theme }) => theme.mediaWidth.upToMedium` - margin: 0; - text-align: center; - justify-content: center; - `}; - } - - > h5 { - width: 100%; - margin: auto; - display: flex; - align-items: center; - justify-content: center; - font-size: 16px; - min-height: 150px; - } - - > h4 > b { - margin: 0 5px; - } -`; - -const renderAuctionStatus = ({ - auctionState, - auctioningToken, - initialAuctionOrder, -}: { - auctionState: AuctionState; - auctioningToken: Token | null; - initialAuctionOrder: SellOrder | null; -}) => { - switch (auctionState) { - case AuctionState.ORDER_PLACING: - case AuctionState.ORDER_PLACING_AND_CANCELING: - return ( -

- Selling - - {initialAuctionOrder?.sellAmount.toSignificant(2)}{" "} - {getTokenDisplay(auctioningToken)} - -

- ); - - case AuctionState.PRICE_SUBMISSION: - return

🗓 Auction closed. Pending on-chain price-calculation.

; - - default: - return

🏁 Auction is settled

; - } -}; - -export function AuctionHeaderForScheduledAuction() { - const { - auctioningToken, - initialAuctionOrder, - auctionEndDate, - } = useDerivedAuctionInfo(); - const { auctionState } = useDerivedAuctionState(); - - return ( - <> - {renderAuctionStatus({ - auctioningToken, - auctionState, - initialAuctionOrder, - })} - - - ); -} - -export default function AuctionHeader() { - const { auctionState } = useDerivedAuctionState(); - return ( - - {auctionState == undefined ? ( -
⌛ Loading
- ) : auctionState == AuctionState.NOT_YET_STARTED ? ( -
⌛ Auction not yet started
- ) : ( - - )} -
- ); -} diff --git a/src/components/AuctionInfoCard/index.tsx b/src/components/AuctionInfoCard/index.tsx deleted file mode 100644 index 00732505a..000000000 --- a/src/components/AuctionInfoCard/index.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import { AuctionInfo } from "../../hooks/useAllAuctionInfos"; -import DoubleLogo from "../DoubleLogo"; -import { ButtonLight } from "../Button"; -import { useHistory } from "react-router-dom"; -import CountdownTimer from "../CountDown"; - -const HeaderWrapper = styled.div` - display: flex; - width: 100%; - align-content: center; - text-align: center; - flex-flow: row nowrap; - justify-content: space-between; - margin: 0; - background: ${({ theme }) => theme.bg2}; - border-radius: 20px; - padding: 16px; - box-sizing: border-box; - margin: 0 0 16px; - - ${({ theme }) => theme.mediaWidth.upToMedium` - flex-flow: column wrap; - `}; - - > h3 { - flex: 1 1 auto; - display: flex; - text-align: center; - align-items: center; - margin: 0 auto; - font-weight: normal; - } - - > h4 { - flex: 1 1 auto; - display: flex; - text-align: center; - align-items: center; - margin: 0 auto; - font-size: 18px; - font-weight: normal; - - ${({ theme }) => theme.mediaWidth.upToMedium` - margin: 0; - text-align: center; - justify-content: center; - `}; - } - - > h5 { - width: 100%; - margin: auto; - display: flex; - align-items: center; - justify-content: center; - font-size: 16px; - min-height: 150px; - } - - > h4 > b { - margin: 0 5px; - } -`; - -const Wrapper = styled.div` - position: relative; - width: calc(50% - 8px); - background: none; - box-shadow: none; - border-radius: 20px; - padding: 0px; - flex: 0 1 auto; - box-sizing: border-box; - display: flex; - flex-flow: column wrap; - - ${({ theme }) => theme.mediaWidth.upToMedium` - width: 100%; - `}; -`; - -const ViewBtn = styled(ButtonLight)` - background: none; - height: 100%; - width: 100%; - color: ${({ theme }) => theme.text3}; - - &:hover { - background: none; - } - - > svg { - margin: 0 0 0 5px; - } -`; - -const Details = styled.div` - color: ${({ theme }) => theme.text1}; - background: ${({ theme }) => theme.bg1}; - font-size: 13px; - width: 100%; - height: 100%; - font-weight: normal; - display: flex; - flex-flow: column wrap; - padding: 16px; - border-radius: 20px; - border: 1px solid ${({ theme }) => theme.bg2}; -`; - -const Row = styled.span` - flex-flow: row-wrap; - width: 100%; - justify-content: space-between; - align-items: flex; - margin: 0 0 4px 0; - font-weight: normal; - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-areas: "label value"; - - > i { - color: ${({ theme }) => theme.text3}; - font-style: normal; - text-align: left; - } - - > a { - text-align: right; - } - - > p { - margin: 0; - padding: 0; - text-align: right; - white-space: normal; - } -`; - -export default function AuctionInfoCard(auctionInfo: AuctionInfo) { - const history = useHistory(); - - function handleClick() { - history.push( - `/auction?auctionId=${auctionInfo.auctionId}&chainId=${Number( - auctionInfo.chainId, - )}`, - ); - } - - return ( - - -
- -

- Selling {auctionInfo.order.volume + ` `} - {auctionInfo.symbolAuctioningToken} -

- -
- - Ends in - - - - - Min. price -

- {auctionInfo.order.price} {` ` + auctionInfo.symbolBiddingToken}{" "} - per {auctionInfo.symbolAuctioningToken} -

-
- - Id

{auctionInfo.auctionId}

-
-
-
-
- ); -} diff --git a/src/components/BoxTitle/index.tsx b/src/components/BoxTitle/index.tsx index 077027aa0..71e6652ce 100644 --- a/src/components/BoxTitle/index.tsx +++ b/src/components/BoxTitle/index.tsx @@ -1,4 +1,4 @@ -import styled from "styled-components"; +import styled from 'styled-components' export const BoxTitle = styled.div` text-align: center; @@ -10,4 +10,4 @@ export const BoxTitle = styled.div` text-decoration: none; color: ${({ theme }) => theme.text1}; font-size: 20px; -`; +` diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 74e776a33..34deed026 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -1,18 +1,18 @@ -import React from "react"; -import styled from "styled-components"; -import { darken, lighten } from "polished"; +import { darken, lighten } from 'polished' +import React from 'react' +import { ChevronDown } from 'react-feather' +import { ButtonProps, Button as RebassButton } from 'rebass/styled-components' +import styled from 'styled-components' -import { RowBetween } from "../Row"; -import { ChevronDown } from "react-feather"; -import { Button as RebassButton, ButtonProps } from "rebass/styled-components"; +import { RowBetween } from '../Row' const Base = styled(RebassButton)<{ - padding?: string; - width?: string; - borderRadius?: string; + padding?: string + width?: string + borderRadius?: string }>` - padding: ${({ padding }) => (padding ? padding : "18px")}; - width: ${({ width }) => (width ? width : "100%")}; + padding: ${({ padding }) => (padding ? padding : '18px')}; + width: ${({ width }) => (width ? width : '100%')}; font-weight: 500; text-align: center; border-radius: 20px; @@ -32,14 +32,14 @@ const Base = styled(RebassButton)<{ > * { user-select: none; } -`; +` const BaseSmall = styled(RebassButton)<{ - padding?: string; - width?: string; - borderRadius?: string; + padding?: string + width?: string + borderRadius?: string }>` - padding: ${({ padding }) => (padding ? padding : "5px")}; - width: ${({ width }) => (width ? width : "100%")}; + padding: ${({ padding }) => (padding ? padding : '5px')}; + width: ${({ width }) => (width ? width : '100%')}; font-weight: 500; text-align: center; border-radius: 5px; @@ -59,7 +59,7 @@ const BaseSmall = styled(RebassButton)<{ > * { user-select: none; } -`; +` export const ButtonPrimary = styled(Base)` background-image: ${({ theme }) => @@ -87,7 +87,7 @@ export const ButtonPrimary = styled(Base)` outline: none; opacity: 1; } -`; +` export const ButtonCancelPrimary = styled(BaseSmall)` background-image: ${({ theme }) => @@ -112,7 +112,7 @@ export const ButtonCancelPrimary = styled(BaseSmall)` box-shadow: none; border: 1px solid ${({ theme }) => theme.bg2}; } -`; +` export const ButtonLight = styled(Base)` background-color: ${({ theme }) => theme.primary5}; @@ -120,22 +120,17 @@ export const ButtonLight = styled(Base)` font-size: 16px; font-weight: 500; &:focus { - box-shadow: 0 0 0 1pt - ${({ theme, disabled }) => !disabled && darken(0.03, theme.primary5)}; - background-color: ${({ theme, disabled }) => - !disabled && darken(0.03, theme.primary5)}; + box-shadow: 0 0 0 1pt ${({ disabled, theme }) => !disabled && darken(0.03, theme.primary5)}; + background-color: ${({ disabled, theme }) => !disabled && darken(0.03, theme.primary5)}; } &:hover { - background-color: ${({ theme, disabled }) => - !disabled && darken(0.03, theme.primary5)}; + background-color: ${({ disabled, theme }) => !disabled && darken(0.03, theme.primary5)}; } &:active { - box-shadow: 0 0 0 1pt - ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)}; - background-color: ${({ theme, disabled }) => - !disabled && darken(0.05, theme.primary5)}; + box-shadow: 0 0 0 1pt ${({ disabled, theme }) => !disabled && darken(0.05, theme.primary5)}; + background-color: ${({ disabled, theme }) => !disabled && darken(0.05, theme.primary5)}; } -`; +` export const ButtonGray = styled(Base)` background-color: ${({ theme }) => theme.bg3}; @@ -143,29 +138,24 @@ export const ButtonGray = styled(Base)` font-size: 16px; font-weight: 500; &:focus { - box-shadow: 0 0 0 1pt - ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg2)}; - background-color: ${({ theme, disabled }) => - !disabled && darken(0.05, theme.bg2)}; + box-shadow: 0 0 0 1pt ${({ disabled, theme }) => !disabled && darken(0.05, theme.bg2)}; + background-color: ${({ disabled, theme }) => !disabled && darken(0.05, theme.bg2)}; } &:hover { - background-color: ${({ theme, disabled }) => - !disabled && darken(0.05, theme.bg2)}; + background-color: ${({ disabled, theme }) => !disabled && darken(0.05, theme.bg2)}; } &:active { - box-shadow: 0 0 0 1pt - ${({ theme, disabled }) => !disabled && darken(0.1, theme.bg2)}; - background-color: ${({ theme, disabled }) => - !disabled && darken(0.1, theme.bg2)}; + box-shadow: 0 0 0 1pt ${({ disabled, theme }) => !disabled && darken(0.1, theme.bg2)}; + background-color: ${({ disabled, theme }) => !disabled && darken(0.1, theme.bg2)}; } -`; +` export const ButtonSecondary = styled(Base)` background-color: ${({ theme }) => theme.primary5}; color: ${({ theme }) => theme.primaryText1}; font-size: 16px; border-radius: 8px; - padding: ${({ padding }) => (padding ? padding : "10px")}; + padding: ${({ padding }) => (padding ? padding : '10px')}; &:focus { box-shadow: 0 0 0 1pt ${({ theme }) => theme.primary4}; @@ -183,7 +173,7 @@ export const ButtonSecondary = styled(Base)` opacity: 50%; cursor: auto; } -`; +` export const ButtonPink = styled(Base)` background-color: ${({ theme }) => theme.primary1}; @@ -205,7 +195,7 @@ export const ButtonPink = styled(Base)` opacity: 50%; cursor: auto; } -`; +` export const ButtonOutlined = styled(Base)` border: 1px solid ${({ theme }) => theme.bg2}; @@ -225,7 +215,7 @@ export const ButtonOutlined = styled(Base)` opacity: 50%; cursor: auto; } -`; +` export const ButtonEmpty = styled(Base)` background-color: transparent; @@ -247,7 +237,7 @@ export const ButtonEmpty = styled(Base)` opacity: 50%; cursor: auto; } -`; +` export const ButtonWhite = styled(Base)` border: 1px solid #edeef2; @@ -256,19 +246,19 @@ export const ButtonWhite = styled(Base)` &:focus { // eslint-disable-next-line @typescript-eslint/no-unused-vars - box-shadow: 0 0 0 1pt ${darken(0.05, "#edeef2")}; + box-shadow: 0 0 0 1pt ${darken(0.05, '#edeef2')}; } &:hover { - box-shadow: 0 0 0 1pt ${darken(0.1, "#edeef2")}; + box-shadow: 0 0 0 1pt ${darken(0.1, '#edeef2')}; } &:active { - box-shadow: 0 0 0 1pt ${darken(0.1, "#edeef2")}; + box-shadow: 0 0 0 1pt ${darken(0.1, '#edeef2')}; } &:disabled { opacity: 50%; cursor: auto; } -`; +` const ButtonConfirmedStyle = styled(Base)` background-color: ${({ theme }) => lighten(0.5, theme.green1)}; @@ -279,7 +269,7 @@ const ButtonConfirmedStyle = styled(Base)` opacity: 50%; cursor: auto; } -`; +` const ButtonErrorStyle = styled(Base)` background-color: ${({ theme }) => theme.red1}; @@ -300,7 +290,7 @@ const ButtonErrorStyle = styled(Base)` opacity: 50%; cursor: auto; } -`; +` const ButtonCancelErrorStyle = styled(BaseSmall)` background-color: ${({ theme }) => theme.red1}; border: 0.5px solid ${({ theme }) => theme.red1}; @@ -320,78 +310,66 @@ const ButtonCancelErrorStyle = styled(BaseSmall)` opacity: 50%; cursor: auto; } -`; +` -export function ButtonConfirmed({ - confirmed, - ...rest -}: { confirmed?: boolean } & ButtonProps) { +export function ButtonConfirmed({ confirmed, ...rest }: { confirmed?: boolean } & ButtonProps) { if (confirmed) { - return ; + return } else { - return ; + return } } -export function ButtonError({ - error, - ...rest -}: { error?: boolean } & ButtonProps) { +export function ButtonError({ error, ...rest }: { error?: boolean } & ButtonProps) { if (error) { - return ; + return } else { - return ; + return } } -export function ButtonCancel({ - error, - ...rest -}: { error?: boolean } & ButtonProps) { +export function ButtonCancel({ error, ...rest }: { error?: boolean } & ButtonProps) { if (error) { - return ; + return } else { - return ; + return } } export function ButtonDropwdown({ - disabled = false, children, + disabled = false, ...rest }: { disabled?: boolean } & ButtonProps) { return ( -
{children}
+
{children}
- ); + ) } export function ButtonDropwdownLight({ - disabled = false, children, + disabled = false, ...rest }: { disabled?: boolean } & ButtonProps) { return ( -
{children}
+
{children}
- ); + ) } -export function ButtonRadio({ - active, - ...rest -}: { active?: boolean } & ButtonProps) { +export function ButtonRadio({ active, ...rest }: { active?: boolean } & ButtonProps) { if (!active) { - return ; + return } else { - return ; + return } } diff --git a/src/components/Card/index.tsx b/src/components/Card/index.tsx index 615403078..624316c23 100644 --- a/src/components/Card/index.tsx +++ b/src/components/Card/index.tsx @@ -1,12 +1,13 @@ -import React from "react"; -import styled from "styled-components"; -import { CardProps, Text } from "rebass"; -import { Box } from "rebass/styled-components"; +import React from 'react' +import { Box } from 'rebass/styled-components' +import styled from 'styled-components' + +import { CardProps, Text } from 'rebass' const Card = styled(Box)<{ - padding?: string; - border?: string; - borderRadius?: string; + padding?: string + border?: string + borderRadius?: string }>` width: 100%; border-radius: 16px; @@ -14,47 +15,47 @@ const Card = styled(Box)<{ padding: ${({ padding }) => padding}; border: ${({ border }) => border}; border-radius: ${({ borderRadius }) => borderRadius}; -`; -export default Card; +` +export default Card export const LightCard = styled(Card)` border: 1px solid ${({ theme }) => theme.bg2}; background-color: ${({ theme }) => theme.bg1}; -`; +` export const GreyCard = styled(Card)` background-color: ${({ theme }) => theme.advancedBG}; -`; +` export const OutlineCard = styled(Card)` border: 1px solid ${({ theme }) => theme.advancedBG}; -`; +` export const YellowCard = styled(Card)` background-color: rgba(243, 132, 30, 0.05); color: ${({ theme }) => theme.yellow2}; font-weight: 500; -`; +` export const PinkCard = styled(Card)` background-color: rgba(255, 0, 122, 0.03); color: ${({ theme }) => theme.primary1}; font-weight: 500; -`; +` const BlueCardStyled = styled(Card)` background-color: ${({ theme }) => theme.primary5}; color: ${({ theme }) => theme.primary1}; border-radius: 12px; width: fit-content; -`; +` export const BlueCard = ({ children, ...rest }: CardProps) => { return ( - + {children} - ); -}; + ) +} diff --git a/src/components/ClaimConfirmationModal/index.tsx b/src/components/ClaimConfirmationModal/index.tsx index ed85ac6d8..6f5924173 100644 --- a/src/components/ClaimConfirmationModal/index.tsx +++ b/src/components/ClaimConfirmationModal/index.tsx @@ -1,49 +1,50 @@ -import React, { useContext } from "react"; -import styled, { ThemeContext } from "styled-components"; -import Modal from "../Modal"; -import Loader from "../Loader"; -import { ExternalLink } from "../../theme"; -import { Text } from "rebass"; -import { CloseIcon } from "../../theme/components"; -import { RowBetween } from "../Row"; -import { ArrowUpCircle } from "react-feather"; -import { ButtonPrimary } from "../Button"; -import { AutoColumn, ColumnCenter } from "../Column"; +import React, { useContext } from 'react' +import { ArrowUpCircle } from 'react-feather' +import styled, { ThemeContext } from 'styled-components' -import { useActiveWeb3React } from "../../hooks"; -import { getEtherscanLink } from "../../utils"; +import { Text } from 'rebass' + +import { useActiveWeb3React } from '../../hooks' +import { ExternalLink } from '../../theme' +import { CloseIcon } from '../../theme/components' +import { getEtherscanLink } from '../../utils' +import { ButtonPrimary } from '../Button' +import { AutoColumn, ColumnCenter } from '../Column' +import Loader from '../Loader' +import { RowBetween } from '../Row' +import Modal from '../modals/Modal' const Wrapper = styled.div` width: 100%; -`; +` const Section = styled(AutoColumn)` padding: 24px; -`; +` const ConfirmedIcon = styled(ColumnCenter)` padding: 60px 0; -`; +` interface ConfirmationModalProps { - isOpen: boolean; - onDismiss: () => void; - hash: string; - pendingConfirmation: boolean; - pendingText: string; + isOpen: boolean + onDismiss: () => void + hash: string + pendingConfirmation: boolean + pendingText: string } export default function ClaimConfirmationModal({ + hash, isOpen, onDismiss, - hash, pendingConfirmation, pendingText, }: ConfirmationModalProps) { - const { chainId } = useActiveWeb3React(); - const theme = useContext(ThemeContext); + const { chainId } = useActiveWeb3React() + const theme = useContext(ThemeContext) return ( - + {
@@ -55,43 +56,27 @@ export default function ClaimConfirmationModal({ {pendingConfirmation ? ( ) : ( - + )} - - - {!pendingConfirmation - ? "Transaction Submitted" - : "Waiting For Confirmation"} + + + {!pendingConfirmation ? 'Transaction Submitted' : 'Waiting For Confirmation'} - - + + {pendingText} {!pendingConfirmation && ( <> - - + + View on Etherscan - - + + Close @@ -99,7 +84,7 @@ export default function ClaimConfirmationModal({ )} {pendingConfirmation && ( - + Confirm this transaction in your wallet )} @@ -108,5 +93,5 @@ export default function ClaimConfirmationModal({ } - ); + ) } diff --git a/src/components/Claimer/index.tsx b/src/components/Claimer/index.tsx deleted file mode 100644 index fe735ac31..000000000 --- a/src/components/Claimer/index.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import React, { useState, useMemo } from "react"; -import { Text } from "rebass"; -import { - ButtonPrimary, - ButtonError, - ButtonLight, -} from "../../components/Button"; -import { BottomGrouping, Wrapper } from "../swap/styleds"; -import ClaimConfirmationModal from "../ClaimConfirmationModal"; -import styled from "styled-components"; - -import { useActiveWeb3React } from "../../hooks"; -import { - useClaimOrderCallback, - useGetAuctionProceeds, -} from "../../hooks/useClaimOrderCallback"; -import { useWalletModalToggle } from "../../state/application/hooks"; -import { - useDerivedClaimInfo, - useDerivedAuctionInfo, - useSwapState, -} from "../../state/orderPlacement/hooks"; -import TokenLogo from "../TokenLogo"; -import { getTokenDisplay } from "../../utils"; - -export const AuctionTokenWrapper = styled.div` - width: 100%; - display: flex; - flex-flow: column wrap; - justify-content: center; - box-sizing: border-box; - padding: 0 0 16px; -`; - -export const AuctionToken = styled.div` - display: flex; - padding: 0; - margin: 0 0 10px; - box-sizing: border-box; - align-items: center; - - > img { - margin: 0 10px 0 0; - } -`; - -export default function Claimer() { - const { account } = useActiveWeb3React(); - - // toggle wallet when disconnected - const toggleWalletModal = useWalletModalToggle(); - - // swap state - const { auctionId } = useSwapState(); - const { biddingToken, auctioningToken } = useDerivedAuctionInfo(); - const { error } = useDerivedClaimInfo(auctionId); - - const isValid = !error; - // modal and loading - const [showConfirm, setShowConfirm] = useState(false); - const [pendingConfirmation, setPendingConfirmation] = useState(true); // waiting for user confirmation - - const { - claimableBiddingToken, - claimableAuctioningToken, - } = useGetAuctionProceeds(); - - // txn values - const [txHash, setTxHash] = useState(""); - - // reset modal state when closed - function resetModal() { - setPendingConfirmation(true); - } - - // the callback to execute the swap - const claimOrderCallback = useClaimOrderCallback(); - - function onClaimOrder() { - claimOrderCallback().then((hash) => { - setTxHash(hash); - setPendingConfirmation(false); - }); - } - - // text to show while loading - const pendingText = `Claiming Funds`; - - const biddingTokenDisplay = useMemo(() => getTokenDisplay(biddingToken), [ - biddingToken, - ]); - - const auctioningTokenDisplay = useMemo( - () => getTokenDisplay(auctioningToken), - [auctioningToken], - ); - return ( - <> - - - - - - {claimableBiddingToken - ? claimableBiddingToken.toSignificant(2) - : `0 ${biddingTokenDisplay}`} - - - - - - - {claimableAuctioningToken - ? claimableAuctioningToken.toSignificant(2) - : `0 ${auctioningTokenDisplay}`} - - - - - { - resetModal(); - setShowConfirm(false); - }} - pendingConfirmation={pendingConfirmation} - hash={txHash} - pendingText={pendingText} - /> - - {!account ? ( - - Connect Wallet - - ) : error ? ( - {error} - ) : ( - { - setShowConfirm(true); - onClaimOrder(); - }} - id="swap-button" - disabled={!isValid} - error={isValid} - > - - {error ?? `Claim Funds`} - - - )} - - - - ); -} diff --git a/src/components/Column/index.tsx b/src/components/Column/index.tsx index 9ea8eefa1..d5812d102 100644 --- a/src/components/Column/index.tsx +++ b/src/components/Column/index.tsx @@ -1,34 +1,24 @@ -import styled from "styled-components"; +import styled from 'styled-components' const Column = styled.div` display: flex; flex-direction: column; justify-content: flex-start; -`; +` export const ColumnCenter = styled(Column)` width: 100%; align-items: center; -`; +` export const AutoColumn = styled.div<{ - gap?: "sm" | "md" | "lg" | string; - justify?: - | "stretch" - | "center" - | "start" - | "end" - | "flex-start" - | "flex-end" - | "space-between"; + gap?: 'sm' | 'md' | 'lg' | string + justify?: 'stretch' | 'center' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'space-between' }>` display: grid; grid-auto-rows: auto; grid-row-gap: ${({ gap }) => - (gap === "sm" && "8px") || - (gap === "md" && "12px") || - (gap === "lg" && "24px") || - gap}; + (gap === 'sm' && '8px') || (gap === 'md' && '12px') || (gap === 'lg' && '24px') || gap}; justify-items: ${({ justify }) => justify && justify}; -`; +` -export default Column; +export default Column diff --git a/src/components/ConfirmationModal/index.tsx b/src/components/ConfirmationModal/index.tsx index f7692ebf4..0a99169fe 100644 --- a/src/components/ConfirmationModal/index.tsx +++ b/src/components/ConfirmationModal/index.tsx @@ -1,68 +1,69 @@ -import React, { useContext } from "react"; -import styled, { ThemeContext } from "styled-components"; -import Modal from "../Modal"; -import Loader from "../Loader"; -import { ExternalLink } from "../../theme"; -import { Text } from "rebass"; -import { CloseIcon } from "../../theme/components"; -import { RowBetween } from "../Row"; -import { ArrowUpCircle } from "react-feather"; -import { ButtonPrimary } from "../Button"; -import { AutoColumn, ColumnCenter } from "../Column"; +import React, { useContext } from 'react' +import { ArrowUpCircle } from 'react-feather' +import styled, { ThemeContext } from 'styled-components' -import { useActiveWeb3React } from "../../hooks"; -import { getEtherscanLink } from "../../utils"; +import { Text } from 'rebass' + +import { useActiveWeb3React } from '../../hooks' +import { ExternalLink } from '../../theme' +import { CloseIcon } from '../../theme/components' +import { getEtherscanLink } from '../../utils' +import { ButtonPrimary } from '../Button' +import { AutoColumn, ColumnCenter } from '../Column' +import Loader from '../Loader' +import { RowBetween } from '../Row' +import Modal from '../modals/Modal' const Wrapper = styled.div` width: 100%; -`; +` const Section = styled(AutoColumn)` padding: 24px; -`; +` const BottomSection = styled(Section)` background-color: ${({ theme }) => theme.bg2}; border-bottom-left-radius: 20px; border-bottom-right-radius: 20px; -`; +` const ConfirmedIcon = styled(ColumnCenter)` padding: 60px 0; -`; +` interface ConfirmationModalProps { - isOpen: boolean; - onDismiss: () => void; - hash: string; - topContent: () => React.ReactChild; - bottomContent: () => React.ReactChild; - attemptingTxn: boolean; - pendingConfirmation: boolean; - pendingText: string; - title?: string; + isOpen: boolean + onDismiss: () => void + hash: string + topContent: () => React.ReactChild + bottomContent: () => React.ReactChild + attemptingTxn: boolean + pendingConfirmation: boolean + pendingText: string + title?: string } export default function ConfirmationModal({ + attemptingTxn, + bottomContent, + hash, isOpen, onDismiss, - hash, - topContent, - bottomContent, - attemptingTxn, pendingConfirmation, pendingText, - title = "", + title = '', + topContent, }: ConfirmationModalProps) { - const { chainId } = useActiveWeb3React(); - const theme = useContext(ThemeContext); + const { chainId } = useActiveWeb3React() + const theme = useContext(ThemeContext) return ( - + {!attemptingTxn ? (
- + {title} @@ -82,43 +83,27 @@ export default function ConfirmationModal({ {pendingConfirmation ? ( ) : ( - + )} - - - {!pendingConfirmation - ? "Transaction Submitted" - : "Waiting For Confirmation"} + + + {!pendingConfirmation ? 'Transaction Submitted' : 'Waiting For Confirmation'} - - + + {pendingText} {!pendingConfirmation && ( <> - - + + View on Etherscan - - + + Close @@ -126,7 +111,7 @@ export default function ConfirmationModal({ )} {pendingConfirmation && ( - + Confirm this transaction in your wallet )} @@ -135,5 +120,5 @@ export default function ConfirmationModal({ )} - ); + ) } diff --git a/src/components/CountDown/index.tsx b/src/components/CountDown/index.tsx deleted file mode 100644 index 4eb303d2e..000000000 --- a/src/components/CountDown/index.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import styled from "styled-components"; -import React, { useEffect, useState } from "react"; - -const CountDownStyled = styled.div` - display: flex; - flex: 0 1 auto; - font-family: var(--font-mono); - text-align: right; - font-size: 13px; - color: ${({ theme }) => `1px solid ${theme.text2}`}; - letter-spacing: 0; - justify-content: center; - flex-flow: row wrap; - align-items: center; - background: none; - box-sizing: border-box; - position: relative; - - > p { - margin: 0 5px 0 0; - } - - > strong { - color: ${({ theme }) => `1px solid ${theme.text1}`}; - } -`; - -export function formatSeconds(seconds: number): string { - const days = Math.floor(seconds / 24 / 60 / 60) % 360; - const hours = Math.floor(seconds / 60 / 60) % 24; - const minutes = Math.floor(seconds / 60) % 60; - const remainderSeconds = Math.floor(seconds % 60); - let s = ""; - - if (days > 0) { - s += `${days}d `; - } - if (hours > 0) { - s += `${hours}h `; - } - if (minutes > 0) { - s += `${minutes}m `; - } - if (remainderSeconds > 0 && hours < 2) { - s += `${remainderSeconds}s`; - } - if (minutes === 0 && remainderSeconds === 0) { - s = "0s"; - } - - return s; -} - -const calculateTimeLeft = (auctionEndDate) => { - const diff = auctionEndDate - Date.now() / 1000; - if (diff < 0) return 0; - return diff; -}; - -export default function CountdownTimer({ - auctionEndDate, - showText, -}: { - auctionEndDate: number; - showText: boolean; -}) { - const [timeLeft, setTimeLeft] = useState(calculateTimeLeft(auctionEndDate)); - - useEffect(() => { - let mounted = true; - setTimeout(() => { - if (mounted) setTimeLeft(calculateTimeLeft(auctionEndDate)); - }, 1000); - - return () => (mounted = false); - }); - - return timeLeft && timeLeft > 0 ? ( - - {showText ?

Auction ends in

: <>} - {formatSeconds(timeLeft)} -
- ) : null; -} diff --git a/src/components/CurrencyInputPanel/index.tsx b/src/components/CurrencyInputPanel/index.tsx deleted file mode 100644 index 5bd66cebd..000000000 --- a/src/components/CurrencyInputPanel/index.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import { Pair, Token } from "uniswap-xdai-sdk"; -import React, { useContext } from "react"; -import styled, { ThemeContext } from "styled-components"; -import { darken } from "polished"; -import { useTokenBalance } from "../../state/wallet/hooks"; -import TokenLogo from "../TokenLogo"; -import DoubleLogo from "../DoubleLogo"; -import { RowBetween } from "../Row"; -import { TYPE, CursorPointer } from "../../theme"; -import { Input as NumericalInput } from "../NumericalInput"; - -import { useActiveWeb3React } from "../../hooks"; -import { useTranslation } from "react-i18next"; - -const InputRow = styled.div<{ selected: boolean }>` - ${({ theme }) => theme.flexRowNoWrap} - align-items: center; - padding: ${({ selected }) => - selected ? "0.75rem 0.5rem 0.75rem 1rem" : "0.75rem 0.75rem 0.75rem 1rem"}; -`; - -const CurrencySelect = styled.button<{ selected: boolean }>` - align-items: center; - height: 2.2rem; - font-size: 20px; - font-weight: 500; - background-color: ${({ selected, theme }) => - selected ? theme.bg1 : theme.primary1}; - color: ${({ selected, theme }) => (selected ? theme.text1 : theme.white)}; - border-radius: 12px; - box-shadow: ${({ selected }) => - selected ? "none" : "0px 6px 10px rgba(0, 0, 0, 0.075)"}; - outline: none; - cursor: pointer; - user-select: none; - border: none; - padding: 0 0.5rem; - - :focus, - :hover { - background-color: ${({ selected, theme }) => - selected ? theme.bg2 : darken(0.05, theme.primary1)}; - } -`; - -const LabelRow = styled.div` - ${({ theme }) => theme.flexRowNoWrap} - align-items: center; - color: ${({ theme }) => theme.text1}; - font-size: 0.75rem; - line-height: 1rem; - padding: 0.75rem 1rem 0 1rem; - height: 20px; - span:hover { - cursor: pointer; - color: ${({ theme }) => darken(0.2, theme.text2)}; - } -`; - -const Aligner = styled.span` - display: flex; - align-items: center; - justify-content: space-between; -`; - -const InputPanel = styled.div<{ hideInput?: boolean }>` - ${({ theme }) => theme.flexColumnNoWrap} - position: relative; - border-radius: ${({ hideInput }) => (hideInput ? "8px" : "20px")}; - background-color: ${({ theme }) => theme.bg2}; - z-index: 1; -`; - -const Container = styled.div<{ hideInput: boolean }>` - border-radius: ${({ hideInput }) => (hideInput ? "8px" : "20px")}; - border: 1px solid ${({ theme }) => theme.bg2}; - background-color: ${({ theme }) => theme.bg1}; -`; - -const StyledTokenName = styled.span<{ active?: boolean }>` - ${({ active }) => - active - ? " margin: 0 0.25rem 0 0.75rem;" - : " margin: 0 0.25rem 0 0.25rem;"} - font-size: ${({ active }) => (active ? "20px" : "16px")}; -`; - -const StyledBalanceMax = styled.button` - height: 28px; - background-color: ${({ theme }) => theme.primary5}; - border: 1px solid ${({ theme }) => theme.primary5}; - border-radius: 0.5rem; - font-size: 0.875rem; - - font-weight: 500; - cursor: pointer; - margin-right: 0.5rem; - color: ${({ theme }) => theme.primaryText1}; - :hover { - border: 1px solid ${({ theme }) => theme.primary1}; - } - :focus { - border: 1px solid ${({ theme }) => theme.primary1}; - outline: none; - } - - ${({ theme }) => theme.mediaWidth.upToExtraSmall` - margin-right: 0.5rem; - `}; -`; - -interface CurrencyInputPanelProps { - value: string; - onUserSellAmountInput: (val: string) => void; - onMax?: () => void; - showMaxButton: boolean; - label?: string; - onTokenSelection?: (tokenAddress: string) => void; - token?: Token | null; - disableTokenSelect?: boolean; - hideBalance?: boolean; - isExchange?: boolean; - pair?: Pair | null; - hideInput?: boolean; - showSendWithSwap?: boolean; - otherSelectedTokenAddress?: string | null; - id: string; -} - -export default function CurrencyInputPanel({ - value, - onUserSellAmountInput, - onMax, - showMaxButton, - label = "Input", - token = null, - disableTokenSelect = false, - hideBalance = false, - isExchange = false, - pair = null, // used for double token logo - hideInput = false, - id, -}: CurrencyInputPanelProps) { - const { t } = useTranslation(); - - const { account } = useActiveWeb3React(); - const userTokenBalance = useTokenBalance(account, token); - const theme = useContext(ThemeContext); - - return ( - - - {!hideInput && ( - - - - {label} - - {account && ( - - - {!hideBalance && !!token && userTokenBalance - ? "Balance: " + userTokenBalance?.toSignificant(6) - : " -"} - - - )} - - - )} - - {!hideInput && ( - <> - { - onUserSellAmountInput(val); - }} - /> - {account && - !!token?.address && - showMaxButton && - label !== "To" && ( - MAX - )} - - )} - - - {isExchange ? ( - - ) : token?.address ? ( - - ) : null} - {isExchange ? ( - - {pair?.token0.symbol}:{pair?.token1.symbol} - - ) : ( - - {(token && token.symbol && token.symbol.length > 20 - ? token.symbol.slice(0, 4) + - "..." + - token.symbol.slice( - token.symbol.length - 5, - token.symbol.length, - ) - : token?.symbol) || t("selectToken")} - - )} - {!disableTokenSelect} - - - - - - ); -} diff --git a/src/components/DarkModeSwitch/index.tsx b/src/components/DarkModeSwitch/index.tsx deleted file mode 100644 index ce5bcba26..000000000 --- a/src/components/DarkModeSwitch/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; -import { Sun, Moon } from "react-feather"; -import { useDarkModeManager } from "../../state/user/hooks"; - -import { ButtonSecondary } from "../Button"; - -export default function DarkModeSwitch() { - const [darkMode, toggleDarkMode] = useDarkModeManager(); - - return ( - - {darkMode ? : } - - ); -} diff --git a/src/components/DoubleLogo/index.tsx b/src/components/DoubleLogo/index.tsx deleted file mode 100644 index 7039c56e4..000000000 --- a/src/components/DoubleLogo/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import TokenLogo from "../TokenLogo"; - -const TokenWrapper = styled.div` - position: relative; - display: flex; - flex-direction: row; - align-items: center; -`; - -interface DoubleTokenLogoProps { - margin?: boolean; - size?: number; - a0: string; - a1: string; -} - -const HigherLogo = styled(TokenLogo)` - z-index: 2; -`; - -const CoveredLogo = styled(TokenLogo)` - margin-left: -5px; -`; - -export default function DoubleTokenLogo({ - a0, - a1, - size = 28, -}: DoubleTokenLogoProps) { - return ( - - - - - ); -} diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx deleted file mode 100644 index 141b67982..000000000 --- a/src/components/Footer/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; -import styled from "styled-components"; - -const FooterFrame = styled.div` - display: flex; - align-items: center; - justify-content: flex-end; - position: fixed; - right: 1rem; - bottom: 1rem; - ${({ theme }) => theme.mediaWidth.upToMedium` - display: none; - `}; -`; - -export default function Footer() { - return ; -} diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx deleted file mode 100644 index a57979161..000000000 --- a/src/components/Header/index.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import React from "react"; -import { Link as HistoryLink } from "react-router-dom"; - -import styled from "styled-components"; -import { useTokenBalanceTreatingWETHasETH } from "../../state/wallet/hooks"; - -import Row from "../Row"; -import Menu from "../Menu"; -import Web3Status from "../Web3Status"; - -import { WETH, ChainId } from "uniswap-xdai-sdk"; -import { isMobile } from "react-device-detect"; -import { YellowCard } from "../Card"; -import { useActiveWeb3React } from "../../hooks"; - -import { RowBetween } from "../Row"; - -const HeaderFrame = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - flex-direction: column; - width: 100%; - top: 0; - position: absolute; - pointer-events: none; - - ${({ theme }) => theme.mediaWidth.upToExtraSmall` - padding: 12px 0 0 0; - position: relative; - `}; - z-index: 2; -`; - -const HeaderElement = styled.div` - display: flex; - align-items: center; - - ${({ theme }) => theme.mediaWidth.upToMedium` - flex-direction: row; - justify-content: space-between; - justify-self: center; - padding: 0 10px; - position: fixed; - bottom: 0px; - left: 0px; - width: 100%; - z-index: 99; - height: 72px; - border-radius: 12px 12px 0px 0px; - background-color: ${({ theme }) => theme.bg3}; - box-sizing: border-box; - `}; -`; - -const Title = styled.div` - display: flex; - align-items: center; - pointer-events: auto; - - ${({ theme }) => theme.mediaWidth.upToMedium` - margin: 0 auto; - `}; - - :hover { - cursor: pointer; - } -`; - -const TitleText = styled(Row)` - width: fit-content; - font-size: 18px; - font-weight: 500; - white-space: nowrap; - color: ${({ theme }) => theme.text1}; - - > a { - color: inherit; - text-decoration: none; - } -`; - -const EthBalance = styled.div` - display: flex; - align-items: center; - pointer-events: auto; - padding: 0 10px; - - ${({ theme }) => theme.mediaWidth.upToMedium` - background-color: ${({ theme }) => theme.bg3}; - `}; - - ${({ theme }) => theme.mediaWidth.upToSmall` - display: none; - `}; -`; - -const AccountElement = styled.div<{ active: boolean }>` - display: flex; - flex-direction: row; - align-items: center; - background-color: ${({ theme, active }) => (!active ? theme.bg1 : theme.bg3)}; - border-radius: 12px; - white-space: nowrap; - - :focus { - border: 1px solid blue; - } - - ${({ theme }) => theme.mediaWidth.upToMedium` - margin: 0 auto 0 0; - `} -`; - -const TestnetWrapper = styled.div` - white-space: nowrap; - width: fit-content; - margin-left: 10px; - - ${({ theme }) => theme.mediaWidth.upToSmall` - display: none; - `}; -`; - -const NetworkCard = styled(YellowCard)` - width: fit-content; - margin-right: 10px; - border-radius: 12px; - padding: 8px 12px; -`; - -const MenuWrapper = styled.div` - pointer-events: auto; - display: flex; - position: relative; -`; - -export default function Header() { - const { account, chainId } = useActiveWeb3React(); - - const userEthBalance = useTokenBalanceTreatingWETHasETH( - account, - WETH[chainId], - ); - - return ( - - - - <TitleText> - <HistoryLink id="link" to="/"> - 🏁 GnosisAuction - </HistoryLink> - </TitleText> - - - - - - - {!isMobile && chainId === ChainId.ROPSTEN && ( - Ropsten - )} - {!isMobile && chainId === ChainId.RINKEBY && ( - Rinkeby - )} - {!isMobile && chainId === ChainId.GÖRLI && ( - Görli - )} - {!isMobile && chainId === ChainId.KOVAN && ( - Kovan - )} - - - {account && userEthBalance ? ( - {userEthBalance?.toSignificant(4)} ETH - ) : null} - - - - - - - - - ); -} diff --git a/src/components/Identicon/index.tsx b/src/components/Identicon/index.tsx index 59821f47d..d30b2802b 100644 --- a/src/components/Identicon/index.tsx +++ b/src/components/Identicon/index.tsx @@ -1,28 +1,28 @@ -import React, { useEffect, useRef } from "react"; +import React, { useEffect, useRef } from 'react' +import styled from 'styled-components' -import styled from "styled-components"; +import Jazzicon from 'jazzicon' -import { useActiveWeb3React } from "../../hooks"; -import Jazzicon from "jazzicon"; +import { useActiveWeb3React } from '../../hooks' const StyledIdenticon = styled.div` height: 1rem; width: 1rem; border-radius: 1.125rem; background-color: ${({ theme }) => theme.bg4}; -`; +` export default function Identicon() { - const ref = useRef(); + const ref = useRef() - const { account } = useActiveWeb3React(); + const { account } = useActiveWeb3React() useEffect(() => { if (account && ref.current) { - ref.current.innerHTML = ""; - ref.current.appendChild(Jazzicon(16, parseInt(account.slice(2, 10), 16))); + ref.current.innerHTML = '' + ref.current.appendChild(Jazzicon(16, parseInt(account.slice(2, 10), 16))) } - }, [account]); + }, [account]) - return ; + return } diff --git a/src/components/Loader/index.tsx b/src/components/Loader/index.tsx index b2874fb87..dc9afc19d 100644 --- a/src/components/Loader/index.tsx +++ b/src/components/Loader/index.tsx @@ -1,15 +1,14 @@ -import React from "react"; +import React from 'react' +import styled from 'styled-components' -import styled from "styled-components"; - -import { Spinner } from "../../theme"; -import Circle from "../../assets/images/blue-loader.svg"; +import Circle from '../../assets/images/blue-loader.svg' +import { Spinner } from '../../theme' const SpinnerWrapper = styled(Spinner)<{ size: string }>` height: ${({ size }) => size}; width: ${({ size }) => size}; -`; +` export default function Loader({ size }: { size: string }) { - return ; + return } diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index 5fdcb3b2a..559c17821 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -1,16 +1,17 @@ -import React, { useRef, useEffect } from "react"; -import { Info, Code, MessageCircle } from "react-feather"; -import styled from "styled-components"; -import { ReactComponent as MenuIcon } from "../../assets/images/menu.svg"; -import useToggle from "../../hooks/useToggle"; +import React, { useEffect, useRef } from 'react' +import { Code, Info, MessageCircle } from 'react-feather' +import styled from 'styled-components' -import { ExternalLink } from "../../theme"; +import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg' +import { GIT_COMMIT_HASH } from '../../constants/config' +import useToggle from '../../hooks/useToggle' +import { ExternalLink } from '../../theme' const StyledMenuIcon = styled(MenuIcon)` path { stroke: ${({ theme }) => theme.text1}; } -`; +` const StyledMenuButton = styled.button` width: 100%; @@ -38,7 +39,7 @@ const StyledMenuButton = styled.button` svg { margin-top: 2px; } -`; +` const StyledMenu = styled.div` margin-left: 0.5rem; @@ -48,7 +49,7 @@ const StyledMenu = styled.div` position: relative; border: none; text-align: left; -`; +` const MenuFlyout = styled.span` min-width: 8.125rem; @@ -69,7 +70,7 @@ const MenuFlyout = styled.span` bottom: 70px; top: initial; `} -`; +` const MenuItem = styled(ExternalLink)` flex: 1; @@ -83,34 +84,34 @@ const MenuItem = styled(ExternalLink)` > svg { margin-right: 8px; } -`; +` -const CODE_LINK = !!process.env.REACT_APP_GIT_COMMIT_HASH - ? `https://github.com/gnosis/ido-contracts/tree/${process.env.REACT_APP_GIT_COMMIT_HASH}` - : "https://github.com/gnosis/ido-contracts"; +const CODE_LINK = GIT_COMMIT_HASH + ? `https://github.com/gnosis/ido-contracts/tree/${GIT_COMMIT_HASH}` + : 'https://github.com/gnosis/ido-contracts' export default function Menu() { - const node = useRef(); - const [open, toggle] = useToggle(false); + const node = useRef() + const [open, toggle] = useToggle(false) useEffect(() => { const handleClickOutside = (e) => { if (node.current?.contains(e.target) ?? false) { - return; + return } - toggle(); - }; + toggle() + } if (open) { - document.addEventListener("mousedown", handleClickOutside); + document.addEventListener('mousedown', handleClickOutside) } else { - document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener('mousedown', handleClickOutside) } return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [open, toggle]); + document.removeEventListener('mousedown', handleClickOutside) + } + }, [open, toggle]) return ( @@ -120,22 +121,22 @@ export default function Menu() { {open && ( About - + Code - + Discord )} - ); + ) } diff --git a/src/components/NavigationTabs/index.tsx b/src/components/NavigationTabs/index.tsx index a900cdfa6..6563a589e 100644 --- a/src/components/NavigationTabs/index.tsx +++ b/src/components/NavigationTabs/index.tsx @@ -1,113 +1,101 @@ -import React, { useCallback } from "react"; -import styled from "styled-components"; -import { - withRouter, - Link as HistoryLink, - RouteComponentProps, -} from "react-router-dom"; -import useBodyKeyDown from "../../hooks/useBodyKeyDown"; +import React, { useCallback } from 'react' +import { ArrowLeft } from 'react-feather' +import { Link as HistoryLink, RouteComponentProps, withRouter } from 'react-router-dom' +import styled from 'styled-components' -import { CursorPointer } from "../../theme"; -import { ArrowLeft } from "react-feather"; -import { RowBetween } from "../Row"; -import QuestionHelper from "../QuestionHelper"; +import useBodyKeyDown from '../../hooks/useBodyKeyDown' +import { CursorPointer } from '../../theme' +import QuestionHelper from '../QuestionHelper' +import { RowBetween } from '../Row' const tabOrder = [ { - path: "/swap", - textKey: "Place Order", + path: '/swap', + textKey: 'Place Order', regex: /\/swap/, }, -]; +] const Tabs = styled.div` ${({ theme }) => theme.flexRowNoWrap} align-items: center; border-radius: 3rem; -`; +` const ActiveText = styled.div` font-weight: 500; font-size: 20px; -`; +` const ArrowLink = styled(ArrowLeft)` color: ${({ theme }) => theme.text1}; -`; +` -function NavigationTabs({ - location: { pathname }, - history, -}: RouteComponentProps) { +function NavigationTabs({ history, location: { pathname } }: RouteComponentProps) { const navigate = useCallback( (direction) => { - const tabIndex = tabOrder.findIndex(({ regex }) => pathname.match(regex)); - history.push( - tabOrder[(tabIndex + tabOrder.length + direction) % tabOrder.length] - .path, - ); + const tabIndex = tabOrder.findIndex(({ regex }) => pathname.match(regex)) + history.push(tabOrder[(tabIndex + tabOrder.length + direction) % tabOrder.length].path) }, [pathname, history], - ); + ) const navigateRight = useCallback(() => { - navigate(1); - }, [navigate]); + navigate(1) + }, [navigate]) const navigateLeft = useCallback(() => { - navigate(-1); - }, [navigate]); + navigate(-1) + }, [navigate]) - useBodyKeyDown("ArrowRight", navigateRight); - useBodyKeyDown("ArrowLeft", navigateLeft); + useBodyKeyDown('ArrowRight', navigateRight) + useBodyKeyDown('ArrowLeft', navigateLeft) - const adding = pathname.match("/add"); - const removing = pathname.match("/remove"); - const finding = pathname.match("/find"); - const creating = pathname.match("/create"); + const adding = pathname.match('/add') + const removing = pathname.match('/remove') + const finding = pathname.match('/find') + const creating = pathname.match('/create') return ( <> {adding || removing ? ( - - history.push("/pool")}> + + history.push('/pool')}> - {adding ? "Add" : "Remove"} Liquidity + {adding ? 'Add' : 'Remove'} Liquidity ) : finding ? ( - + Import Pool ) : creating ? ( - + Create Pool - + ) : ( - + {/* {tabOrder.map(({ path, textKey, regex }) => ( )} - ); + ) } -export default withRouter(NavigationTabs); +export default withRouter(NavigationTabs) diff --git a/src/components/NumericalInput/index.tsx b/src/components/NumericalInput/index.tsx deleted file mode 100644 index 91a18e182..000000000 --- a/src/components/NumericalInput/index.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import { escapeRegExp } from "../../utils"; - -const StyledInput = styled.input<{ - error?: boolean; - fontSize?: string; - align?: string; -}>` - color: ${({ error, theme }) => (error ? theme.red1 : theme.text1)}; - width: 0; - position: relative; - font-weight: 500; - outline: none; - border: none; - flex: 1 1 auto; - background-color: ${({ theme }) => theme.bg1}; - font-size: ${({ fontSize }) => fontSize ?? "24px"}; - text-align: ${({ align }) => align && align}; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - padding: 0px; - -webkit-appearance: textfield; - - ::-webkit-search-decoration { - -webkit-appearance: none; - } - - [type="number"] { - -moz-appearance: textfield; - } - - ::-webkit-outer-spin-button, - ::-webkit-inner-spin-button { - -webkit-appearance: none; - } - - ::placeholder { - color: ${({ theme }) => theme.text4}; - } -`; - -const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`); // match escaped "." characters via in a non-capturing group - -export const Input = React.memo(function InnerInput({ - value, - onUserSellAmountInput, - placeholder, - ...rest -}: { - value: string | number; - onUserSellAmountInput: (string) => void; - error?: boolean; - fontSize?: string; - align?: "right" | "left"; -} & Omit, "ref" | "onChange" | "as">) { - const enforcer = (nextUserInput: string) => { - if (nextUserInput === "" || inputRegex.test(escapeRegExp(nextUserInput))) { - onUserSellAmountInput(nextUserInput); - } - }; - - return ( - { - // replace commas with periods, because uniswap exclusively uses period as the decimal separator - enforcer(event.target.value.replace(/,/g, ".")); - }} - // universal input options - inputMode="decimal" - title="Token Amount" - autoComplete="off" - autoCorrect="off" - // text-specific options - type="text" - pattern="^[0-9]*[.,]?[0-9]*$" - placeholder={placeholder || "0.0"} - minLength={1} - maxLength={79} - spellCheck="false" - /> - ); -}); - -export default Input; - -// const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group diff --git a/src/components/OrderDropdown/index.tsx b/src/components/OrderDropdown/index.tsx deleted file mode 100644 index 2c4315f35..000000000 --- a/src/components/OrderDropdown/index.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useContext } from "react"; -import { ChevronDown } from "react-feather"; -import { ThemeContext } from "styled-components"; -import { RowBetween } from "../Row"; -import { AdvancedDropdown, SectionBreak } from "../swap/styleds"; - -import { ChevronUp } from "react-feather"; -import { Text } from "rebass"; -import { CursorPointer } from "../../theme"; -import { AutoColumn } from "../Column"; -import OrderTable from "../OrderTable"; -import { OrderDisplay } from "../../state/orders/reducer"; - -export interface OrderTableDetailsProps { - orders: OrderDisplay[]; -} - -export default function OrderDisplayDropdown({ - showAdvanced, - orders, - setShowAdvanced, - ...rest -}: Omit & { - showAdvanced: boolean; - setShowAdvanced: (showAdvanced: boolean) => void; -}) { - const theme = useContext(ThemeContext); - return ( - - {showAdvanced && !!orders ? ( - setShowAdvanced(false)} - orders={orders} - /> - ) : ( - - setShowAdvanced(true)} - padding="4px 4px" - id="show-advanced" - > - - {!orders || orders.length === 0 - ? "You have no orders yet" - : `Show ${orders.length} orders`} - - - - - )} - - ); -} - -export interface AdvancedOrderDetailsProps extends OrderTableDetailsProps { - onDismiss: () => void; -} - -export function AdvancedOrderDetails({ - orders, - onDismiss, -}: AdvancedOrderDetailsProps) { - const theme = useContext(ThemeContext); - - return ( - - - - - Hide {orders.length} orders - - - - - - - - - - ); -} diff --git a/src/components/OrderPlacement/index.tsx b/src/components/OrderPlacement/index.tsx deleted file mode 100644 index 4f6f0fe13..000000000 --- a/src/components/OrderPlacement/index.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import { TokenAmount, ChainId, Fraction } from "uniswap-xdai-sdk"; -import React, { useState, useEffect, useMemo } from "react"; -import { Text } from "rebass"; -import { ButtonLight, ButtonPrimary } from "../../components/Button"; -import { AutoColumn } from "../../components/Column"; -import ConfirmationModal from "../../components/ConfirmationModal"; -import WarningModal from "../../components/WarningModal"; -import CurrencyInputPanel from "../../components/CurrencyInputPanel"; -import PriceInputPanel from "../../components/PriceInputPanel"; -import { BottomGrouping, Dots, Wrapper } from "../../components/swap/styleds"; -import SwapModalFooter from "../swap/PlaceOrderModalFooter"; -import SwapModalHeader from "../../components/swap/SwapModalHeader"; -import { useActiveWeb3React } from "../../hooks"; -import { EASY_AUCTION_NETWORKS } from "../../constants"; -import { - useApproveCallback, - ApprovalState, -} from "../../hooks/useApproveCallback"; -import { usePlaceOrderCallback } from "../../hooks/usePlaceOrderCallback"; -import { useWalletModalToggle } from "../../state/application/hooks"; -import { - useDerivedAuctionInfo, - useGetOrderPlacementError, - useSwapActionHandlers, - useSwapState, -} from "../../state/orderPlacement/hooks"; -import { getTokenDisplay } from "../../utils"; -import { useOrderState } from "../../state/orders/hooks"; -import { OrderState } from "../../state/orders/reducer"; - -export default function OrderPlacement() { - const { chainId, account } = useActiveWeb3React(); - const orders: OrderState | undefined = useOrderState(); - - // toggle wallet when disconnected - const toggleWalletModal = useWalletModalToggle(); - - // swap state - const { price, sellAmount } = useSwapState(); - const { - biddingTokenBalance, - parsedBiddingAmount, - auctioningToken, - biddingToken, - initialPrice, - } = useDerivedAuctionInfo(); - const { error } = useGetOrderPlacementError(); - const { onUserSellAmountInput } = useSwapActionHandlers(); - const { onUserPriceInput } = useSwapActionHandlers(); - - const isValid = !error; - - // modal and loading - const [showConfirm, setShowConfirm] = useState(false); - const [showWarning, setShowWarning] = useState(false); - const [attemptingTxn, setAttemptingTxn] = useState(false); // clicked confirmed - const [pendingConfirmation, setPendingConfirmation] = useState(true); // waiting for user confirmation - - // txn values - const [txHash, setTxHash] = useState(""); - - const approvalTokenAmount: TokenAmount | undefined = parsedBiddingAmount; - // check whether the user has approved the EasyAuction Contract - const [approval, approveCallback] = useApproveCallback( - approvalTokenAmount, - EASY_AUCTION_NETWORKS[chainId as ChainId], - ); - const [approvalSubmitted, setApprovalSubmitted] = useState(false); - - useEffect(() => { - if (approval === ApprovalState.PENDING) { - setApprovalSubmitted(true); - } - }, [approval, approvalSubmitted]); - - const maxAmountInput: TokenAmount = !!biddingTokenBalance - ? biddingTokenBalance - : undefined; - const atMaxAmountInput: boolean = - maxAmountInput && parsedBiddingAmount - ? maxAmountInput.equalTo(parsedBiddingAmount) - : undefined; - - useEffect(() => { - if (price == "-" && initialPrice) { - onUserPriceInput( - initialPrice.multiply(new Fraction("1001", "1000")).toSignificant(4), - ); - } - }, [onUserPriceInput, price, initialPrice]); - - // reset modal state when closed - function resetModal() { - // clear input if txn submitted - if (!pendingConfirmation) { - onUserSellAmountInput(""); - } - setPendingConfirmation(true); - setAttemptingTxn(false); - } - - // the callback to execute the swap - const placeOrderCallback = usePlaceOrderCallback( - auctioningToken, - biddingToken, - ); - - function onPlaceOrder() { - setAttemptingTxn(true); - - placeOrderCallback().then((hash) => { - setTxHash(hash); - setPendingConfirmation(false); - }); - } - - // errors - const [showInverted, setShowInverted] = useState(false); - - function modalHeader() { - return ; - } - - function modalBottom() { - return ( - - ); - } - - // text to show while loading - const pendingText = `Placing order`; - - const biddingTokenDisplay = useMemo(() => getTokenDisplay(biddingToken), [ - biddingToken, - ]); - - const auctioningTokenDisplay = useMemo( - () => getTokenDisplay(auctioningToken), - [auctioningToken], - ); - - const handleShowConfirm = () => { - const sameOrder = orders.orders.find((order) => order.price === price); - - if (!sameOrder) { - setShowConfirm(true); - } else { - setShowWarning(true); - } - }; - - return ( - <> - - { - resetModal(); - setShowConfirm(false); - }} - attemptingTxn={attemptingTxn} - pendingConfirmation={pendingConfirmation} - hash={txHash} - topContent={modalHeader} - bottomContent={modalBottom} - pendingText={pendingText} - /> - { - setShowWarning(false); - }} - /> - - <> - { - maxAmountInput && - onUserSellAmountInput(maxAmountInput.toExact()); - }} - id="auction-input" - /> - - - - - - {!account ? ( - - Connect Wallet - - ) : approval === ApprovalState.NOT_APPROVED || - approval === ApprovalState.PENDING ? ( - - {approval === ApprovalState.PENDING ? ( - Approving {biddingTokenDisplay} - ) : ( - `Approve ${biddingTokenDisplay}` - )} - - ) : ( - - - {error ?? `Place Order`} - - - )} - - - - ); -} diff --git a/src/components/OrderTable/index.tsx b/src/components/OrderTable/index.tsx deleted file mode 100644 index 125793d76..000000000 --- a/src/components/OrderTable/index.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import React, { useState } from "react"; -import styled from "styled-components"; -import { Text } from "rebass"; - -import { useCancelOrderCallback } from "../../hooks/useCancelOrderCallback"; -import { - AuctionState, - useDerivedAuctionInfo, - useDerivedAuctionState, -} from "../../state/orderPlacement/hooks"; -import { ButtonCancel } from "../Button"; -import ConfirmationModal from "../ConfirmationModal"; -import CancelModalFooter from "../swap/CancelOrderModealFooter"; -import SwapModalHeader from "../swap/SwapModalHeader"; -import { useOrderActionHandlers } from "../../state/orders/hooks"; -import { OrderDisplay, OrderStatus } from "../../state/orders/reducer"; -import { useClearingPriceInfo } from "../../hooks/useCurrentClearingOrderAndVolumeCallback"; -import { ClearingPriceAndVolumeData } from "../../api/AdditionalServicesApi"; -import { decodeOrder, encodeOrder } from "../../hooks/Order"; -import { Fraction } from "uniswap-xdai-sdk"; - -const StyledRow = styled.div` - display: grid; - grid-template-columns: 1.5fr 0.6fr 1fr 1fr 1fr; - grid-template-areas: "amount price fill status action"; - font-weight: normal; - font-size: 13px; - padding: 8px 0; - transition: background-color 0.1s ease-in-out; - - &:hover { - background-color: ${({ theme }) => theme.advancedBG}; - } - - &:not(:last-child) { - border-bottom: 1px solid rgba(43, 43, 43, 0.435); - } - - > div { - display: flex; - align-items: center; - } - - > div:last-of-type { - margin: 0 0 0 auto; - } -`; - -const StyledHeader = styled(StyledRow)` - &:hover { - background: none; - } - > div { - font-weight: 700; - } -`; - -const Wrapper = styled.div` - width: 100%; - padding: 0; -`; - -function getMatchedVolume( - orderId: string, - clearingOrderInfo: ClearingPriceAndVolumeData, -): Number { - if (orderId == encodeOrder(clearingOrderInfo.clearingOrder)) { - return Number( - new Fraction( - clearingOrderInfo.volume.mul(100).toString(), - clearingOrderInfo.clearingOrder.sellAmount.toString(), - ).toSignificant(2), - ); - } else { - const order = decodeOrder(orderId); - if ( - order.sellAmount - .mul(clearingOrderInfo.clearingOrder.buyAmount) - .lt(order.buyAmount.mul(clearingOrderInfo.clearingOrder.sellAmount)) - ) { - return Number(0); - } else { - return 100; - } - } -} - -function Table(orders: OrderDisplay[]) { - const { auctionState } = useDerivedAuctionState(); - const { biddingToken } = useDerivedAuctionInfo(); - const cancelOrderCallback = useCancelOrderCallback(biddingToken); - const { onDeleteOrder } = useOrderActionHandlers(); - const clearingPriceInfo = useClearingPriceInfo(); - - // modal and loading - const [showConfirm, setShowConfirm] = useState(false); - const [attemptingTxn, setAttemptingTxn] = useState(false); // clicked confirmed - const [pendingConfirmation, setPendingConfirmation] = useState(true); // waiting for user confirmation - - // txn values - const [txHash, setTxHash] = useState(""); - const [orderId, setOrderId] = useState(""); - - // reset modal state when closed - function resetModal() { - setPendingConfirmation(true); - setAttemptingTxn(false); - } - - function onCancelOrder() { - setAttemptingTxn(true); - - cancelOrderCallback(orderId).then((hash) => { - onDeleteOrder(orderId); - setTxHash(hash); - setPendingConfirmation(false); - }); - } - - function modalHeader() { - return ; - } - - function modalBottom() { - return ( - - ); - } - const pendingText = `Canceling Order`; - let error = undefined; - if (auctionState != AuctionState.ORDER_PLACING_AND_CANCELING) { - error = "Not allowed"; - } - if (!orders || orders.length == 0) return null; - return ( - <> - { - resetModal(); - setShowConfirm(false); - }} - attemptingTxn={attemptingTxn} - pendingConfirmation={pendingConfirmation} - hash={txHash} - topContent={modalHeader} - bottomContent={modalBottom} - pendingText={pendingText} - /> - -
Amount
-
Price
-
Est. Fill
-
Status
-
Actions
-
- {Object.entries(orders).map((order) => ( - -
{order[1].sellAmount}
-
{order[1].price}
-
- {clearingPriceInfo - ? getMatchedVolume(order[1].id, clearingPriceInfo) - : "loading"} -
-
- {order[1].status == OrderStatus.PLACED ? "Placed" : "Pending"} -
-
- {order[1].status == OrderStatus.PENDING ? ( -
- ) : ( - { - if (!error) { - setOrderId(order[1].id); - setShowConfirm(true); - } - }} - id="cancel-button" - > - - {error ?? `Cancel Order`} - - - )} -
-
- ))} - - ); -} - -export default function OrderTable(orders: OrderDisplay[]) { - return ( - - - - ); -} diff --git a/src/components/OrderbookBtn.tsx b/src/components/OrderbookBtn.tsx deleted file mode 100644 index 91834ffd5..000000000 --- a/src/components/OrderbookBtn.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import React, { useMemo } from "react"; -import styled from "styled-components"; -import Modal, { useModal } from "./MesaModal"; - -// const, types, utils -import { Token } from "uniswap-xdai-sdk"; - -// components -import { DEFAULT_MODAL_OPTIONS } from "./Modal"; -import { ButtonLight } from "./Button"; -import OrderBookWidget, { processOrderbookData } from "./OrderbookWidget"; - -// hooks -import { useActiveWeb3React } from "../hooks"; -import { useSwapState } from "../state/orderPlacement/hooks"; - -// utils -import { getTokenDisplay } from "../utils"; -import OrderBookChartSmall, { OrderBookError } from "./OrderbookChartSmall"; -import { useOrderbookState } from "../state/orderbook/hooks"; - -const ViewOrderBookBtn = styled(ButtonLight)` - margin: 0 0 0 0; - background: none; - height: auto; - width: 100%; - padding: 0; - color: ${({ theme }) => theme.text3}; - - &:hover { - background: none; - } - - > svg { - margin: 0 0 0 5px; - } -`; - -const Wrapper = styled.div` - display: block; -`; - -// todo correct circular reference: -// const ModalWrapper = styled(ModalBodyWrapper)` -const ModalWrapper = styled.div` - display: flex; - text-align: center; - height: 100%; - min-width: 100%; - width: 100%; - align-items: center; - align-content: flex-start; - flex-flow: row wrap; - padding: 0; - justify-content: center; - - > span { - display: flex; - flex-flow: row wrap; - align-items: center; - margin: 1.6rem 0 1rem; - } - - > span:first-of-type::after { - content: "/"; - margin: 0 1rem; - } - - > span:first-of-type > p { - margin: 0 1rem 0 0; - } - - > span:last-of-type > p { - margin: 0 0 0 1rem; - } - - .amcharts-Sprite-group { - font-size: 1rem; - } - - .amcharts-Container .amcharts-Label { - text-transform: uppercase; - font-size: 11px; - } - - .amcharts-ZoomOutButton-group > .amcharts-RoundedRectangle-group { - fill: var(--color-text-active); - opacity: 0.6; - transition: 0.3s ease-in-out; - - &:hover { - opacity: 1; - } - } -`; - -interface OrderBookBtnProps { - baseToken: Token; - quoteToken: Token; - label?: string; - className?: string; -} - -export const OrderBookBtn: React.FC = ( - props: OrderBookBtnProps, -) => { - const { baseToken, quoteToken, className } = props; - // const theme = useContext(ThemeContext); - const { chainId } = useActiveWeb3React(); - const { auctionId } = useSwapState(); - - const biddingTokenDisplay = useMemo(() => getTokenDisplay(baseToken), [ - baseToken, - ]); - - const auctioningTokenDisplay = useMemo(() => getTokenDisplay(quoteToken), [ - quoteToken, - ]); - - const [modalHook, toggleModal] = useModal({ - ...DEFAULT_MODAL_OPTIONS, - large: true, - title: `${biddingTokenDisplay}-${auctioningTokenDisplay} Order book`, - message: ( - - - - ), - buttons: [ - <> , - modalHook.hide()} - />, - ], - }); - const { - error, - bids, - asks, - userOrderPrice, - userOrderVolume, - } = useOrderbookState(); - - if (error || !asks || asks.length == 0) - return ; - const processedOrderbook = processOrderbookData({ - data: { bids, asks }, - userOrder: { price: userOrderPrice, volume: userOrderVolume }, - baseToken, - quoteToken, - }); - return ( - - - - - - - ); -}; diff --git a/src/components/OrderbookChart.tsx b/src/components/OrderbookChart.tsx deleted file mode 100644 index bae1d0d1b..000000000 --- a/src/components/OrderbookChart.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import React, { useEffect, useRef } from "react"; -import styled from "styled-components"; - -import * as am4core from "@amcharts/amcharts4/core"; -import * as am4charts from "@amcharts/amcharts4/charts"; -import am4themesSpiritedaway from "@amcharts/amcharts4/themes/spiritedaway"; - -import { Token } from "uniswap-xdai-sdk"; - -export interface OrderBookChartProps { - /** - * Base Token for Y-axis - */ - baseToken: Token; - /** - * Quote Token for X-axis - */ - quoteToken: Token; - /** - * current network id - */ - networkId: number; - /** - * price/volume data with asks and bids - */ - data: PricePointDetails[] | null; -} - -const Wrapper = styled.div` - display: flex; - justify-content: center; - min-height: calc(60vh - 30rem); - text-align: center; - width: 100%; - height: 100%; - min-width: 100%; - padding: 16px; - box-sizing: border-box; - - .amcharts-Sprite-group { - font-size: 1rem; - } - - .amcharts-Label { - text-transform: uppercase; - font-size: 10px; - letter-spacing: 1px; - color: ${({ theme }) => theme.text4}; - margin: 10px; - } - - .amcharts-ZoomOutButton-group > .amcharts-RoundedRectangle-group { - fill: var(--color-text-active); - opacity: 0.6; - transition: 0.3s ease-in-out; - - &:hover { - opacity: 1; - } - } - - .amcharts-AxisLabel, - .amcharts-CategoryAxis .amcharts-Label-group > .amcharts-Label, - .amcharts-ValueAxis-group .amcharts-Label-group > .amcharts-Label { - fill: ${({ theme }) => theme.text3}; - } -`; - -interface OrderBookErrorProps { - error: Error; -} - -export const OrderBookError: React.FC = ({ - error, -}: OrderBookErrorProps) => ( - {error ? error.message : "loading"} -); - -export enum Offer { - Bid, - Ask, -} - -/** - * Price point data represented in the graph. Contains BigNumbers for operate with less errors and more precission - * but for representation uses number as expected by the library - */ -export interface PricePointDetails { - // Basic data - type: Offer; - volume: number; // volume for the price point - totalVolume: number; // cumulative volume - price: number; - - // Data for representation - priceNumber: number; - priceFormatted: string; - totalVolumeNumber: number; - totalVolumeFormatted: string; - askValueY: number | null; - bidValueY: number | null; - newOrderValueY: number | null; - clearingPriceValueY: number | null; -} - -export const createChart = (chartElement: HTMLElement): am4charts.XYChart => { - am4core.useTheme(am4themesSpiritedaway); - am4core.options.autoSetClassName = true; - const chart = am4core.create(chartElement, am4charts.XYChart); - chart.paddingTop = 20; - chart.marginTop = 20; - chart.paddingBottom = 0; - chart.paddingLeft = 0; - chart.paddingRight = 0; - chart.marginBottom = 0; - - // Colors - const colors = { - green: "#28a745", - red: "#dc3545", - white: "#FFFFFF", - grey: "#565A69", - orange: "#FF6347", - }; - - // Create axes - const priceAxis = chart.xAxes.push(new am4charts.ValueAxis()); - const volumeAxis = chart.yAxes.push(new am4charts.ValueAxis()); - priceAxis.renderer.labels.template.disabled = true; - volumeAxis.renderer.labels.template.disabled = true; - priceAxis.renderer.grid.template.disabled = true; - volumeAxis.renderer.tooltip.getFillFromObject = false; - priceAxis.renderer.tooltip.getFillFromObject = false; - - volumeAxis.renderer.grid.template.disabled = true; - priceAxis.renderer.minGridDistance = 10; - volumeAxis.renderer.minGridDistance = 10; - // Create series - const bidSeries = chart.series.push(new am4charts.StepLineSeries()); - bidSeries.dataFields.valueX = "priceNumber"; - bidSeries.dataFields.valueY = "bidValueY"; - bidSeries.strokeWidth = 2; - bidSeries.stroke = am4core.color(colors.green); - bidSeries.fill = bidSeries.stroke; - bidSeries.fillOpacity = 0.2; - - const askSeries = chart.series.push(new am4charts.LineSeries()); - askSeries.dataFields.valueX = "priceNumber"; - askSeries.dataFields.valueY = "askValueY"; - askSeries.strokeWidth = 2; - askSeries.stroke = am4core.color(colors.red); - askSeries.fill = askSeries.stroke; - askSeries.fillOpacity = 0.1; - - const inputSeries = chart.series.push(new am4charts.LineSeries()); - inputSeries.dataFields.valueX = "priceNumber"; - inputSeries.dataFields.valueY = "newOrderValueY"; - inputSeries.strokeWidth = 4; - inputSeries.stroke = am4core.color(colors.orange); - inputSeries.fill = inputSeries.stroke; - inputSeries.fillOpacity = 0.1; - - const priceSeries = chart.series.push(new am4charts.LineSeries()); - priceSeries.dataFields.valueX = "priceNumber"; - priceSeries.dataFields.valueY = "clearingPriceValueY"; - priceSeries.strokeWidth = 2; - priceSeries.strokeDasharray = "3,3"; - priceSeries.stroke = am4core.color(colors.white); - priceSeries.fill = inputSeries.stroke; - priceSeries.fillOpacity = 0.1; - - // Add cursor - chart.cursor = new am4charts.XYCursor(); - chart.cursor.lineX.stroke = am4core.color(colors.white); - chart.cursor.lineX.strokeWidth = 1; - chart.cursor.lineX.strokeOpacity = 0.6; - chart.cursor.lineX.strokeDasharray = "4"; - - chart.cursor.lineY.stroke = am4core.color(colors.white); - chart.cursor.lineY.strokeWidth = 1; - chart.cursor.lineY.strokeOpacity = 0.6; - chart.cursor.lineY.strokeDasharray = "4"; - - // Button configuration - chart.zoomOutButton.background.cornerRadius(5, 5, 5, 5); - chart.zoomOutButton.background.fill = am4core.color("#25283D"); - chart.zoomOutButton.icon.stroke = am4core.color(colors.white); - chart.zoomOutButton.icon.strokeWidth = 2; - - // Add default empty data array - chart.data = []; - - return chart; -}; - -export interface DrawLabelsParams { - chart: am4charts.XYChart; - baseToken: Token; - quoteToken: Token; - networkId: number; -} - -export const drawLabels = ({ - chart, - baseToken, - quoteToken, -}: DrawLabelsParams): void => { - const baseTokenLabel = baseToken.symbol; - const quoteTokenLabel = quoteToken.symbol; - const market = baseTokenLabel + "-" + quoteTokenLabel; - - const [xAxis] = chart.xAxes; - const [yAxis] = chart.yAxes; - xAxis.title.text = ` Price (${baseTokenLabel})`; - yAxis.title.text = ` Volume (${quoteTokenLabel})`; - - xAxis.tooltip.background.cornerRadius = 0; - xAxis.tooltip.background.fill = am4core.color("green"); - yAxis.tooltip.background.cornerRadius = 0; - yAxis.tooltip.background.fill = am4core.color("red"); - - xAxis.title.fill = am4core.color("white"); - yAxis.title.fill = am4core.color("white"); - - const [bidSeries, askSeries] = chart.series; - - bidSeries.tooltipText = `[bold]${market}[/]\nBid Price: [bold]{priceFormatted}[/] ${quoteTokenLabel}\nVolume: [bold]{totalVolumeFormatted}[/] ${baseTokenLabel}`; - askSeries.tooltipText = `[bold]${market}[/]\nAsk Price: [bold]{priceFormatted}[/] ${quoteTokenLabel}\nVolume: [bold]{totalVolumeFormatted}[/] ${baseTokenLabel}`; -}; - -const OrderBookChart: React.FC = ( - props: OrderBookChartProps, -) => { - const { baseToken, quoteToken, networkId, data } = props; - const mountPoint = useRef(null); - const chartRef = useRef(null); - - useEffect(() => { - if (!mountPoint.current) return; - const chart = createChart(mountPoint.current); - chartRef.current = chart; - - // dispose on mount only - return (): void => chart.dispose(); - }, []); - - useEffect(() => { - if (!chartRef.current || data === null) return; - - if (data.length === 0) { - chartRef.current.data = []; - return; - } - - // go on with the update when data is ready - drawLabels({ - chart: chartRef.current, - baseToken, - quoteToken, - networkId, - }); - - chartRef.current.data = data; - }, [baseToken, networkId, quoteToken, data]); - - return Show order book for auction; -}; - -export default OrderBookChart; diff --git a/src/components/OrderbookChartSmall.tsx b/src/components/OrderbookChartSmall.tsx deleted file mode 100644 index a33030682..000000000 --- a/src/components/OrderbookChartSmall.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import React, { useEffect, useRef } from "react"; -import styled from "styled-components"; - -import * as am4core from "@amcharts/amcharts4/core"; -import * as am4charts from "@amcharts/amcharts4/charts"; -import { - OrderBookChartProps, - DrawLabelsParams, - createChart, -} from "./OrderbookChart"; - -const drawLabels = ({ - chart, - baseToken, - quoteToken, -}: DrawLabelsParams): void => { - const baseTokenLabel = baseToken.symbol; - const quoteTokenLabel = quoteToken.symbol; - const market = baseTokenLabel + "-" + quoteTokenLabel; - - const [xAxis] = chart.xAxes; - const [yAxis] = chart.yAxes; - - xAxis.title.text = ` Price (${baseTokenLabel})`; - yAxis.title.text = ` Volume (${quoteTokenLabel})`; - - xAxis.tooltip.background.cornerRadius = 0; - xAxis.tooltip.background.fill = am4core.color("green"); - yAxis.tooltip.background.cornerRadius = 0; - yAxis.tooltip.background.fill = am4core.color("red"); - - xAxis.title.fill = am4core.color("white"); - yAxis.title.fill = am4core.color("white"); - - const [bidSeries, askSeries] = chart.series; - - bidSeries.tooltipText = `[bold]${market}[/]\nBid Price: [bold]{priceFormatted}[/] ${quoteTokenLabel}\nVolume: [bold]{totalVolumeFormatted}[/] ${baseTokenLabel}`; - askSeries.tooltipText = `[bold]${market}[/]\nAsk Price: [bold]{priceFormatted}[/] ${quoteTokenLabel}\nVolume: [bold]{totalVolumeFormatted}[/] ${baseTokenLabel}`; -}; - -const Wrapper = styled.div` - display: flex; - justify-content: center; - align-items: center; - align-content: center; - min-height: calc(70vh - 30rem); - min-width: 550px; - max-height: 260.44px; - padding: 26px; - - text-align: center; - box-sizing: border-box; - color: ${({ theme }) => theme.text2}; - position: relative; - - .amcharts-Sprite-group { - pointer-events: none; - } - - .amcharts-Label { - text-transform: uppercase; - font-size: 10px; - letter-spacing: 1px; - color: ${({ theme }) => theme.text4}; - margin: 10px; - } - - .amcharts-ZoomOutButton-group > .amcharts-RoundedRectangle-group { - fill: var(--color-text-active); - opacity: 0.6; - transition: 0.3s ease-in-out; - - &:hover { - opacity: 1; - } - } - - .amcharts-CategoryAxis .amcharts-Label-group > .amcharts-Label, - .amcharts-ValueAxis-group .amcharts-Label-group > .amcharts-Label { - fill: ${({ theme }) => theme.text3}; - } -`; - -const OrderBookChartSmall: React.FC = ( - props: OrderBookChartProps, -) => { - const { baseToken, quoteToken, networkId, data } = props; - const mountPoint = useRef(null); - const chartRef = useRef(null); - - useEffect(() => { - if (!mountPoint.current) return; - const chart = createChart(mountPoint.current); - chartRef.current = chart; - - // dispose on mount only - return (): void => chart.dispose(); - }, []); - - useEffect(() => { - if (!chartRef.current) return; - - if (data && data.length !== 0) { - chartRef.current.data = data; - } - - // go on with the update when data is ready - drawLabels({ - chart: chartRef.current, - baseToken, - quoteToken, - networkId, - }); - }, [baseToken, networkId, quoteToken, data]); - - return Show order book for this auction; -}; - -interface OrderBookErrorProps { - error: Error; -} - -export const OrderBookError: React.FC = ({ - error, -}: OrderBookErrorProps) => ( - {error ? error.message : "loading"} -); - -export default OrderBookChartSmall; diff --git a/src/components/Popover/index.tsx b/src/components/Popover/index.tsx index 977eb7656..987ec2850 100644 --- a/src/components/Popover/index.tsx +++ b/src/components/Popover/index.tsx @@ -1,10 +1,12 @@ -import { Placement } from "@popperjs/core"; -import { transparentize } from "polished"; -import React, { useState } from "react"; -import { usePopper } from "react-popper"; -import styled, { keyframes } from "styled-components"; -import useInterval from "../../hooks/useInterval"; -import Portal from "@reach/portal"; +import { transparentize } from 'polished' +import React, { useState } from 'react' +import styled, { keyframes } from 'styled-components' + +import { Placement } from '@popperjs/core' +import Portal from '@reach/portal' +import { usePopper } from 'react-popper' + +import useInterval from '../../hooks/useInterval' const fadeIn = keyframes` from { @@ -14,7 +16,7 @@ const fadeIn = keyframes` to { opacity : 1; } -`; +` const fadeOut = keyframes` from { @@ -24,12 +26,12 @@ const fadeOut = keyframes` to { opacity : 0; } -`; +` const PopoverContainer = styled.div<{ show: boolean }>` z-index: 9999; - visibility: ${(props) => (!props.show ? "hidden" : "visible")}; + visibility: ${(props) => (!props.show ? 'hidden' : 'visible')}; animation: ${(props) => (!props.show ? fadeOut : fadeIn)} 150ms linear; transition: visibility 150ms linear; @@ -38,11 +40,11 @@ const PopoverContainer = styled.div<{ show: boolean }>` box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.9, theme.shadow1)}; color: ${({ theme }) => theme.text2}; border-radius: 8px; -`; +` const ReferenceElement = styled.div` display: inline-block; -`; +` const Arrow = styled.div` width: 8px; @@ -55,7 +57,7 @@ const Arrow = styled.div` height: 8px; z-index: 9998; - content: ""; + content: ''; border: 1px solid ${({ theme }) => theme.bg3}; transform: rotate(45deg); background: ${({ theme }) => theme.bg2}; @@ -93,55 +95,42 @@ const Arrow = styled.div` border-top: none; } } -`; +` export interface PopoverProps { - content: React.ReactNode; - show: boolean; - children: React.ReactNode; - placement?: Placement; + content: React.ReactNode + show: boolean + children: React.ReactNode + placement?: Placement } -export default function Popover({ - content, - show, - children, - placement = "auto", -}: PopoverProps) { - const [referenceElement, setReferenceElement] = useState( - null, - ); - const [popperElement, setPopperElement] = useState(null); - const [arrowElement, setArrowElement] = useState(null); - const { styles, update, attributes } = usePopper( - referenceElement, - popperElement, - { - placement, - strategy: "fixed", - modifiers: [ - { name: "offset", options: { offset: [8, 8] } }, - { name: "arrow", options: { element: arrowElement } }, - ], - }, - ); - useInterval(update, show ? 100 : null); +export default function Popover({ children, content, placement = 'auto', show }: PopoverProps) { + const [referenceElement, setReferenceElement] = useState(null) + const [popperElement, setPopperElement] = useState(null) + const [arrowElement, setArrowElement] = useState(null) + const { attributes, styles, update } = usePopper(referenceElement, popperElement, { + placement, + strategy: 'fixed', + modifiers: [ + { name: 'offset', options: { offset: [8, 8] } }, + { name: 'arrow', options: { element: arrowElement } }, + ], + }) + useInterval(update, show ? 100 : null) return ( <> {children} {content} - ); + ) } diff --git a/src/components/Popups/index.tsx b/src/components/Popups/index.tsx index 792a1ff60..092e19ecb 100644 --- a/src/components/Popups/index.tsx +++ b/src/components/Popups/index.tsx @@ -1,17 +1,18 @@ -import { ChainId, Pair, Token } from "uniswap-xdai-sdk"; -import React, { useContext, useMemo } from "react"; -import styled, { ThemeContext } from "styled-components"; -import { useMediaLayout } from "use-media"; - -import { X } from "react-feather"; -import { PopupContent } from "../../state/application/actions"; -import { useActivePopups, useRemovePopup } from "../../state/application/hooks"; -import { ExternalLink } from "../../theme"; -import { AutoColumn } from "../Column"; -import DoubleTokenLogo from "../DoubleLogo"; -import Row from "../Row"; -import TxnPopup from "../TxnPopup"; -import { Text } from "rebass"; +import React, { useContext, useMemo } from 'react' +import { X } from 'react-feather' +import styled, { ThemeContext } from 'styled-components' +import { ChainId, Pair, Token } from 'uniswap-xdai-sdk' + +import { Text } from 'rebass' +import { useMediaLayout } from 'use-media' + +import { PopupContent } from '../../state/application/actions' +import { useActivePopups, useRemovePopup } from '../../state/application/hooks' +import { ExternalLink } from '../../theme' +import { AutoColumn } from '../Column' +import Row from '../Row' +import TxnPopup from '../TxnPopup' +import DoubleTokenLogo from '../common/DoubleLogo' const StyledClose = styled(X)` position: absolute; @@ -21,15 +22,15 @@ const StyledClose = styled(X)` :hover { cursor: pointer; } -`; +` const MobilePopupWrapper = styled.div<{ height: string | number }>` position: relative; max-width: 100%; height: ${({ height }) => height}; - margin: ${({ height }) => (height ? "0 auto;" : 0)}; - margin-bottom: ${({ height }) => (height ? "20px" : 0)}}; -`; + margin: ${({ height }) => (height ? '0 auto;' : 0)}; + margin-bottom: ${({ height }) => (height ? '20px' : 0)}}; +` const MobilePopupInner = styled.div` height: 99%; @@ -41,7 +42,7 @@ const MobilePopupInner = styled.div` ::-webkit-scrollbar { display: none; } -`; +` const FixedPopupColumn = styled(AutoColumn)` position: absolute; @@ -53,7 +54,7 @@ const FixedPopupColumn = styled(AutoColumn)` ${({ theme }) => theme.mediaWidth.upToSmall` display: none; `}; -`; +` const Popup = styled.div` display: inline-block; @@ -70,35 +71,44 @@ const Popup = styled.div` ${({ theme }) => theme.mediaWidth.upToSmall` min-width: 290px; `} -`; +` function PoolPopup({ token0, token1, }: { - token0: { address?: string; symbol?: string }; - token1: { address?: string; symbol?: string }; + token0: { address?: string; symbol?: string } + token1: { address?: string; symbol?: string } }) { - const pairAddress: string | null = useMemo(() => { - if (!token0 || !token1) return null; + const pairAddress: Maybe = useMemo(() => { + if (!token0 || !token1) return null // just mock it out return Pair.getAddress( new Token(ChainId.MAINNET, token0.address, 18), new Token(ChainId.MAINNET, token1.address, 18), - ); - }, [token0, token1]); + ) + }, [token0, token1]) return ( - + Pool Imported - + {token0 && token1 ? ( + + ) : ( + '-' + )} UNI {token0?.symbol} / {token1?.symbol} @@ -109,45 +119,32 @@ function PoolPopup({ ) : null} - ); + ) } -function PopupItem({ - content, - popKey, -}: { - content: PopupContent; - popKey: string; -}) { - if ("txn" in content) { +function PopupItem({ content, popKey }: { content: PopupContent; popKey: string }) { + if ('txn' in content) { const { txn: { hash, success, summary }, - } = content; - return ( - - ); - } else if ("poolAdded" in content) { + } = content + return + } else if ('poolAdded' in content) { const { poolAdded: { token0, token1 }, - } = content; + } = content - return ; + return } } export default function Popups() { - const theme = useContext(ThemeContext); + const theme = useContext(ThemeContext) // get all popups - const activePopups = useActivePopups(); - const removePopup = useRemovePopup(); + const activePopups = useActivePopups() + const removePopup = useRemovePopup() // switch view settings on mobile - const isMobile = useMediaLayout({ maxWidth: "600px" }); + const isMobile = useMediaLayout({ maxWidth: '600px' }) if (!isMobile) { return ( @@ -155,21 +152,18 @@ export default function Popups() { {activePopups.map((item) => { return ( - removePopup(item.key)} - /> + removePopup(item.key)} /> - ); + ) })} - ); + ) } //mobile else return ( - 0 ? "fit-content" : 0}> + 0 ? 'fit-content' : 0}> {activePopups // reverse so new items up front .slice(0) @@ -177,15 +171,12 @@ export default function Popups() { .map((item) => { return ( - removePopup(item.key)} - /> + removePopup(item.key)} /> - ); + ) })} - ); + ) } diff --git a/src/components/PositionCard/index.tsx b/src/components/PositionCard/index.tsx index 4723bf9de..e8f55846c 100644 --- a/src/components/PositionCard/index.tsx +++ b/src/components/PositionCard/index.tsx @@ -1,62 +1,57 @@ -import React, { useState } from "react"; -import styled from "styled-components"; -import { darken } from "polished"; -import { RouteComponentProps, withRouter } from "react-router-dom"; -import { Percent, Pair, JSBI } from "uniswap-xdai-sdk"; +import { darken } from 'polished' +import React, { useState } from 'react' +import { ChevronDown, ChevronUp } from 'react-feather' +import { RouteComponentProps, withRouter } from 'react-router-dom' +import styled from 'styled-components' +import { JSBI, Pair, Percent } from 'uniswap-xdai-sdk' -import { useActiveWeb3React } from "../../hooks"; -import { useTotalSupply } from "../../data/TotalSupply"; -import { useTokenBalance } from "../../state/wallet/hooks"; +import { Text } from 'rebass' -import Card, { GreyCard } from "../Card"; -import TokenLogo from "../TokenLogo"; -import DoubleLogo from "../DoubleLogo"; -import { Text } from "rebass"; -import { ExternalLink } from "../../theme/components"; -import { AutoColumn } from "../Column"; -import { ChevronDown, ChevronUp } from "react-feather"; -import { ButtonSecondary } from "../Button"; -import { RowBetween, RowFixed, AutoRow } from "../Row"; +import { useTotalSupply } from '../../data/TotalSupply' +import { useActiveWeb3React } from '../../hooks' +import { useTokenBalance } from '../../state/wallet/hooks' +import { ExternalLink } from '../../theme/components' +import { ButtonSecondary } from '../Button' +import Card, { GreyCard } from '../Card' +import { AutoColumn } from '../Column' +import { AutoRow, RowBetween, RowFixed } from '../Row' +import DoubleLogo from '../common/DoubleLogo' +import TokenLogo from '../common/TokenLogo' const FixedHeightRow = styled(RowBetween)` height: 24px; -`; +` const HoverCard = styled(Card)` border: 1px solid ${({ theme }) => theme.bg2}; :hover { border: 1px solid ${({ theme }) => darken(0.06, theme.bg2)}; } -`; +` interface PositionCardProps extends RouteComponentProps { - pair: Pair; - minimal?: boolean; - border?: string; + pair: Pair + minimal?: boolean + border?: string } -function PositionCard({ - pair, - history, - border, - minimal = false, -}: PositionCardProps) { - const { account } = useActiveWeb3React(); +function PositionCard({ border, history, minimal = false, pair }: PositionCardProps) { + const { account } = useActiveWeb3React() - const token0 = pair?.token0; - const token1 = pair?.token1; + const token0 = pair?.token0 + const token1 = pair?.token1 - const [showMore, setShowMore] = useState(false); + const [showMore, setShowMore] = useState(false) - const userPoolBalance = useTokenBalance(account, pair?.liquidityToken); - const totalPoolTokens = useTotalSupply(pair?.liquidityToken); + const userPoolBalance = useTokenBalance(account, pair?.liquidityToken) + const totalPoolTokens = useTotalSupply(pair?.liquidityToken) const poolTokenPercentage = !!userPoolBalance && !!totalPoolTokens && JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw) ? new Percent(userPoolBalance.raw, totalPoolTokens.raw) - : undefined; + : undefined const [token0Deposited, token1Deposited] = !!pair && @@ -65,20 +60,10 @@ function PositionCard({ // this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw) ? [ - pair.getLiquidityValue( - token0, - totalPoolTokens, - userPoolBalance, - false, - ), - pair.getLiquidityValue( - token1, - totalPoolTokens, - userPoolBalance, - false, - ), + pair.getLiquidityValue(token0, totalPoolTokens, userPoolBalance, false), + pair.getLiquidityValue(token1, totalPoolTokens, userPoolBalance, false), ] - : [undefined, undefined]; + : [undefined, undefined] if (minimal) { return ( @@ -88,26 +73,35 @@ function PositionCard({ - + Your current position setShowMore(!showMore)}> - - + {token0 && token1 ? ( + + ) : ( + '-' + )} + {token0?.symbol}/{token1?.symbol} - - {userPoolBalance ? userPoolBalance.toSignificant(4) : "-"} + + {userPoolBalance ? userPoolBalance.toSignificant(4) : '-'} @@ -118,18 +112,15 @@ function PositionCard({ {token0Deposited ? ( - {!minimal && } - + {!minimal && token0 && ( + + )} + {token0Deposited?.toSignificant(6)} ) : ( - "-" + '-' )} @@ -138,18 +129,15 @@ function PositionCard({ {token1Deposited ? ( - {!minimal && } - + {!minimal && token1 && ( + + )} + {token1Deposited?.toSignificant(6)} ) : ( - "-" + '-' )} @@ -157,31 +145,37 @@ function PositionCard({ )} - ); + ) } else return ( - setShowMore(!showMore)} - style={{ cursor: "pointer" }} - > + setShowMore(!showMore)} style={{ cursor: 'pointer' }}> - - + {token0 && token1 ? ( + + ) : ( + '-' + )} + {token0?.symbol}/{token1?.symbol} {showMore ? ( - + ) : ( - + )} @@ -195,19 +189,18 @@ function PositionCard({ {token0Deposited ? ( - + {token0Deposited?.toSignificant(6)} - {!minimal && ( + {!minimal && token0 && ( )} ) : ( - "-" + '-' )} @@ -219,19 +212,18 @@ function PositionCard({ {token1Deposited ? ( - + {token1Deposited?.toSignificant(6)} - {!minimal && ( + {!minimal && token1 && ( )} ) : ( - "-" + '-' )} {!minimal && ( @@ -240,7 +232,7 @@ function PositionCard({ Your pool tokens: - {userPoolBalance ? userPoolBalance.toSignificant(4) : "-"} + {userPoolBalance ? userPoolBalance.toSignificant(4) : '-'} )} @@ -250,38 +242,30 @@ function PositionCard({ Your pool share - {poolTokenPercentage - ? poolTokenPercentage.toFixed(2) + "%" - : "-"} + {poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'} )} - - + + View pool information ↗ { - history.push( - "/add/" + token0?.address + "-" + token1?.address, - ); + history.push('/add/' + token0?.address + '-' + token1?.address) }} + width="48%" > Add { - history.push( - "/remove/" + token0?.address + "-" + token1?.address, - ); + history.push('/remove/' + token0?.address + '-' + token1?.address) }} + width="48%" > Remove @@ -290,7 +274,7 @@ function PositionCard({ )} - ); + ) } -export default withRouter(PositionCard); +export default withRouter(PositionCard) diff --git a/src/components/PriceInputPanel/index.tsx b/src/components/PriceInputPanel/index.tsx deleted file mode 100644 index 1c8b0fc4c..000000000 --- a/src/components/PriceInputPanel/index.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { Pair, Token } from "uniswap-xdai-sdk"; -import React, { useContext } from "react"; -import styled, { ThemeContext } from "styled-components"; -import { darken } from "polished"; -import DoubleLogo from "../DoubleLogo"; -import { RowBetween } from "../Row"; -import { TYPE } from "../../theme"; -import { Input as NumericalInput } from "../NumericalInput"; - -const InputRow = styled.div<{ selected: boolean }>` - ${({ theme }) => theme.flexRowNoWrap} - align-items: center; - padding: ${({ selected }) => - selected ? "0.75rem 0.5rem 0.75rem 1rem" : "0.75rem 0.75rem 0.75rem 1rem"}; -`; - -const CurrencySelect = styled.button<{ selected: boolean }>` - align-items: center; - height: 2.2rem; - font-size: 20px; - font-weight: 500; - background-color: ${({ selected, theme }) => - selected ? theme.bg1 : theme.primary1}; - color: ${({ selected, theme }) => (selected ? theme.text1 : theme.white)}; - border-radius: 12px; - box-shadow: ${({ selected }) => - selected ? "none" : "0px 6px 10px rgba(0, 0, 0, 0.075)"}; - outline: none; - user-select: none; - border: none; - padding: 0 0.5rem; - - /* :focus, - :hover { - background-color: ${({ selected, theme }) => - selected ? theme.bg2 : darken(0.05, theme.primary1)}; - } */ -`; - -const LabelRow = styled.div` - ${({ theme }) => theme.flexRowNoWrap} - align-items: center; - color: ${({ theme }) => theme.text1}; - font-size: 0.75rem; - line-height: 1rem; - padding: 0.75rem 1rem 0 1rem; - height: 20px; - span:hover { - cursor: pointer; - color: ${({ theme }) => darken(0.2, theme.text2)}; - } -`; - -const Aligner = styled.span` - display: flex; - align-items: center; - justify-content: space-between; -`; - -const InputPanel = styled.div<{ hideInput?: boolean }>` - ${({ theme }) => theme.flexColumnNoWrap} - position: relative; - border-radius: ${({ hideInput }) => (hideInput ? "8px" : "20px")}; - background-color: ${({ theme }) => theme.bg2}; - z-index: 1; -`; - -const Container = styled.div<{ hideInput: boolean }>` - border-radius: ${({ hideInput }) => (hideInput ? "8px" : "20px")}; - border: 1px solid ${({ theme }) => theme.bg2}; - background-color: ${({ theme }) => theme.bg1}; -`; - -interface CurrencyInputPanelProps { - value: string; - onUserPriceInput: (val: string) => void; - onMax?: () => void; - showMaxButton: boolean; - label?: string; - onTokenSelection?: (tokenAddress: string) => void; - biddingToken: Token | null; - auctioningToken: Token | null; - disableTokenSelect?: boolean; - hideBalance?: boolean; - isExchange?: boolean; - pair?: Pair | null; - hideInput?: boolean; - showSendWithSwap?: boolean; - otherSelectedTokenAddress?: string | null; - id: string; -} - -export default function PriceInputPanel({ - value, - onUserPriceInput, - label = "Input", - biddingToken = null, - auctioningToken = null, - disableTokenSelect = false, - hideInput = false, - id, -}: CurrencyInputPanelProps) { - const theme = useContext(ThemeContext); - - return ( - - - {!hideInput && ( - - - - {label} - - - - )} - - {!hideInput && ( - <> - { - onUserPriceInput(val); - }} - /> - - )} - - - { - - } - {!disableTokenSelect} - - - - - - ); -} diff --git a/src/components/QuestionHelper/index.tsx b/src/components/QuestionHelper/index.tsx index 9b7369aa9..9c6476eaa 100644 --- a/src/components/QuestionHelper/index.tsx +++ b/src/components/QuestionHelper/index.tsx @@ -1,7 +1,8 @@ -import React, { useCallback, useState } from "react"; -import { HelpCircle as Question } from "react-feather"; -import styled from "styled-components"; -import Tooltip from "../Tooltip"; +import React, { useCallback, useState } from 'react' +import { HelpCircle as Question } from 'react-feather' +import styled from 'styled-components' + +import Tooltip from '../Tooltip' const QuestionWrapper = styled.div` display: flex; @@ -20,31 +21,21 @@ const QuestionWrapper = styled.div` :focus { opacity: 0.7; } -`; +` -export default function QuestionHelper({ - text, - disabled, -}: { - text: string; - disabled?: boolean; -}) { - const [show, setShow] = useState(false); +export default function QuestionHelper({ disabled, text }: { text: string; disabled?: boolean }) { + const [show, setShow] = useState(false) - const open = useCallback(() => setShow(true), [setShow]); - const close = useCallback(() => setShow(false), [setShow]); + const open = useCallback(() => setShow(true), [setShow]) + const close = useCallback(() => setShow(false), [setShow]) return ( - - + + - ); + ) } diff --git a/src/components/Row/index.tsx b/src/components/Row/index.tsx index c23a75548..181142dab 100644 --- a/src/components/Row/index.tsx +++ b/src/components/Row/index.tsx @@ -1,11 +1,11 @@ -import styled from "styled-components"; -import { Box } from "rebass/styled-components"; +import { Box } from 'rebass/styled-components' +import styled from 'styled-components' const Row = styled(Box)<{ - align?: string; - padding?: string; - border?: string; - borderRadius?: string; + align?: string + padding?: string + border?: string + borderRadius?: string }>` width: 100%; display: flex; @@ -15,21 +15,21 @@ const Row = styled(Box)<{ padding: ${({ padding }) => padding}; border: ${({ border }) => border}; border-radius: ${({ borderRadius }) => borderRadius}; -`; +` export const RowBetween = styled(Row)<{ - align?: string; - padding?: string; - border?: string; - borderRadius?: string; + align?: string + padding?: string + border?: string + borderRadius?: string }>` justify-content: space-between; -`; +` export const RowFlat = styled.div` display: flex; align-items: flex-end; -`; +` export const AutoRow = styled(Row)<{ gap?: string; justify?: string }>` flex-wrap: wrap; @@ -39,11 +39,11 @@ export const AutoRow = styled(Row)<{ gap?: string; justify?: string }>` & > * { margin: ${({ gap }) => gap} !important; } -`; +` export const RowFixed = styled(Row)<{ gap?: string; justify?: string }>` width: fit-content; margin: ${({ gap }) => gap && `-${gap}`}; -`; +` -export default Row; +export default Row diff --git a/src/components/Slider/index.tsx b/src/components/Slider/index.tsx index 68f1cb921..0ff0a538f 100644 --- a/src/components/Slider/index.tsx +++ b/src/components/Slider/index.tsx @@ -1,5 +1,5 @@ -import React, { useCallback } from "react"; -import styled from "styled-components"; +import React, { useCallback } from 'react' +import styled from 'styled-components' const StyledRangeInput = styled.input<{ value: number }>` -webkit-appearance: none; /* Hides the slider so that custom slider can be made */ @@ -27,9 +27,8 @@ const StyledRangeInput = styled.input<{ value: number }>` &:hover, &:focus { - box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), - 0px 4px 8px rgba(0, 0, 0, 0.08), 0px 16px 24px rgba(0, 0, 0, 0.06), - 0px 24px 32px rgba(0, 0, 0, 0.04); + box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), 0px 4px 8px rgba(0, 0, 0, 0.08), + 0px 16px 24px rgba(0, 0, 0, 0.06), 0px 24px 32px rgba(0, 0, 0, 0.04); } } @@ -43,9 +42,8 @@ const StyledRangeInput = styled.input<{ value: number }>` &:hover, &:focus { - box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), - 0px 4px 8px rgba(0, 0, 0, 0.08), 0px 16px 24px rgba(0, 0, 0, 0.06), - 0px 24px 32px rgba(0, 0, 0, 0.04); + box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), 0px 4px 8px rgba(0, 0, 0, 0.08), + 0px 16px 24px rgba(0, 0, 0, 0.06), 0px 24px 32px rgba(0, 0, 0, 0.04); } } @@ -58,9 +56,8 @@ const StyledRangeInput = styled.input<{ value: number }>` &:hover, &:focus { - box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), - 0px 4px 8px rgba(0, 0, 0, 0.08), 0px 16px 24px rgba(0, 0, 0, 0.06), - 0px 24px 32px rgba(0, 0, 0, 0.04); + box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), 0px 4px 8px rgba(0, 0, 0, 0.08), + 0px 16px 24px rgba(0, 0, 0, 0.06), 0px 24px 32px rgba(0, 0, 0, 0.04); } } @@ -100,36 +97,36 @@ const StyledRangeInput = styled.input<{ value: number }>` &::-ms-fill-upper { background: ${({ theme }) => theme.bg3}; } -`; +` interface InputSliderProps { - value: number; - onChange: (value: number) => void; + value: number + onChange: (value: number) => void } -export default function InputSlider({ value, onChange }: InputSliderProps) { +export default function InputSlider({ onChange, value }: InputSliderProps) { const changeCallback = useCallback( (e) => { - onChange(e.target.value); + onChange(e.target.value) }, [onChange], - ); + ) return ( - ); + ) } diff --git a/src/components/TokenLogo/index.tsx b/src/components/TokenLogo/index.tsx deleted file mode 100644 index 99a4b7475..000000000 --- a/src/components/TokenLogo/index.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import React, { useState } from "react"; -import styled from "styled-components"; -import { isAddress } from "../../utils"; -import { useActiveWeb3React } from "../../hooks"; -import { WETH } from "uniswap-xdai-sdk"; - -import EthereumLogo from "../../assets/images/ethereum-logo.png"; - -const TOKEN_ICON_API = (address) => - `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`; -const BAD_IMAGES = {}; - -const Image = styled.img<{ size: string }>` - width: ${({ size }) => size}; - height: ${({ size }) => size}; - background-color: white; - border-radius: ${({ size }) => size}; - box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075); -`; - -const Emoji = styled.span<{ size?: string }>` - display: flex; - align-items: center; - justify-content: center; - font-size: ${({ size }) => size}; - width: ${({ size }) => size}; - height: ${({ size }) => size}; - margin-bottom: -4px; -`; - -const StyledEthereumLogo = styled.img<{ size: string }>` - width: ${({ size }) => size}; - height: ${({ size }) => size}; - box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075); - border-radius: 24px; -`; - -export default function TokenLogo({ - address, - size = "24px", - ...rest -}: { - address?: string; - size?: string; - style?: React.CSSProperties; -}) { - const [error, setError] = useState(false); - const { chainId } = useActiveWeb3React(); - - // mock rinkeby DAI - if ( - chainId === 4 && - address && - address.toLowerCase() === - "0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa".toLowerCase() - ) { - address = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; - } - // mock rinkeby WBTC - if ( - chainId === 4 && - address && - address.toLowerCase() === - "0x577d296678535e4903d59a4c929b718e1d575e0a".toLowerCase() - ) { - address = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"; - } - - // mock rinkeby GNO - if ( - chainId === 4 && - address && - address.toLowerCase() === - "0xd0dab4e640d95e9e8a47545598c33e31bdb53c7c".toLowerCase() - ) { - address = "0x6810e776880C02933D47DB1b9fc05908e5386b96"; - } - - // mock rinkeby STAKE on xDAI - if ( - chainId === 100 && - address && - address.toLowerCase() === - "0xb7D311E2Eb55F2f68a9440da38e7989210b9A05e".toLowerCase() - ) { - address = "0x0Ae055097C6d159879521C384F1D2123D1f195e6"; - } - - let path = ""; - // hard code to show ETH instead of WETH in UI - if ( - address && - address.toLowerCase() === WETH[chainId].address.toLowerCase() - ) { - return ; - } else if (!error && !BAD_IMAGES[address] && isAddress(address)) { - path = TOKEN_ICON_API(address); - } else { - return ( - - - 🤔 - - - ); - } - - return ( - { - BAD_IMAGES[address] = true; - setError(true); - }} - /> - ); -} diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx index 1df511c45..3172703ee 100644 --- a/src/components/Tooltip/index.tsx +++ b/src/components/Tooltip/index.tsx @@ -1,20 +1,19 @@ -import React from "react"; -import styled from "styled-components"; -import Popover, { PopoverProps } from "../Popover"; +import React from 'react' +import styled from 'styled-components' + +import Popover, { PopoverProps } from '../Popover' const TooltipContainer = styled.div` width: 228px; padding: 0.6rem 1rem; line-height: 150%; font-weight: 400; -`; +` -interface TooltipProps extends Omit { - text: string; +interface TooltipProps extends Omit { + text: string } export default function Tooltip({ text, ...rest }: TooltipProps) { - return ( - {text}} {...rest} /> - ); + return {text}} {...rest} /> } diff --git a/src/components/TxnPopup/index.tsx b/src/components/TxnPopup/index.tsx index 01cf772ef..f47466193 100644 --- a/src/components/TxnPopup/index.tsx +++ b/src/components/TxnPopup/index.tsx @@ -1,17 +1,15 @@ -import React, { useCallback, useState } from "react"; -import { AlertCircle, CheckCircle } from "react-feather"; +import React, { useCallback, useState } from 'react' +import { AlertCircle, CheckCircle } from 'react-feather' +import styled from 'styled-components' -import styled from "styled-components"; - -import { useActiveWeb3React } from "../../hooks"; -import useInterval from "../../hooks/useInterval"; -import { useRemovePopup } from "../../state/application/hooks"; -import { TYPE } from "../../theme"; - -import { ExternalLink } from "../../theme/components"; -import { getEtherscanLink } from "../../utils"; -import { AutoColumn } from "../Column"; -import { AutoRow } from "../Row"; +import { useActiveWeb3React } from '../../hooks' +import useInterval from '../../hooks/useInterval' +import { useRemovePopup } from '../../state/application/hooks' +import { TYPE } from '../../theme' +import { ExternalLink } from '../../theme/components' +import { getEtherscanLink } from '../../utils' +import { AutoColumn } from '../Column' +import { AutoRow } from '../Row' const Fader = styled.div<{ count: number }>` position: absolute; @@ -21,68 +19,52 @@ const Fader = styled.div<{ count: number }>` height: 2px; background-color: ${({ theme }) => theme.bg3}; transition: width 100ms linear; -`; +` -const delay = 100; +const delay = 100 export default function TxnPopup({ hash, + popKey, success, summary, - popKey, }: { - hash: string; - success?: boolean; - summary?: string; - popKey?: string; + hash: string + success?: boolean + summary?: string + popKey?: string }) { - const { chainId } = useActiveWeb3React(); - const [count, setCount] = useState(1); + const { chainId } = useActiveWeb3React() + const [count, setCount] = useState(1) - const [isRunning, setIsRunning] = useState(true); - const removePopup = useRemovePopup(); + const [isRunning, setIsRunning] = useState(true) + const removePopup = useRemovePopup() - const removeThisPopup = useCallback(() => removePopup(popKey), [ - popKey, - removePopup, - ]); + const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup]) useInterval( () => { - count > 150 ? removeThisPopup() : setCount(count + 1); + count > 150 ? removeThisPopup() : setCount(count + 1) }, isRunning ? delay : null, - ); + ) return ( - setIsRunning(false)} - onMouseLeave={() => setIsRunning(true)} - > + setIsRunning(false)} onMouseLeave={() => setIsRunning(true)}> {success ? ( - + ) : ( - + )} - {summary - ? summary - : "Hash: " + hash.slice(0, 8) + "..." + hash.slice(58, 65)} + {summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)} - + View on Etherscan - ); + ) } diff --git a/src/components/WalletModal/WalletConnectData.tsx b/src/components/WalletModal/WalletConnectData.tsx deleted file mode 100644 index 3e34eb5b7..000000000 --- a/src/components/WalletModal/WalletConnectData.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import QRCode from "qrcode.react"; - -const QRCodeWrapper = styled.div` - ${({ theme }) => theme.flexColumnNoWrap}; - align-items: center; - justify-content: center; - border-radius: 12px; - margin-bottom: 20px; -`; - -interface WalletConnectDataProps { - uri?: string; - size: number; -} - -export default function WalletConnectData({ - uri = "", - size, -}: WalletConnectDataProps) { - return ( - {uri && } - ); -} diff --git a/src/components/Web3ReactManager/index.tsx b/src/components/Web3ReactManager/index.tsx index 87df6c680..459963426 100644 --- a/src/components/Web3ReactManager/index.tsx +++ b/src/components/Web3ReactManager/index.tsx @@ -1,29 +1,27 @@ -import React, { useState, useEffect } from "react"; -import { useWeb3React } from "@web3-react/core"; -import styled from "styled-components"; -import { useTranslation } from "react-i18next"; - -import { network } from "../../connectors"; -import { - useEagerConnect, - useInactiveListener, - useActiveListener, -} from "../../hooks"; -import { Spinner } from "../../theme"; -import Circle from "../../assets/images/circle.svg"; -import { NetworkContextName } from "../../constants"; -import { useSwapState } from "../../state/orderPlacement/hooks"; +import React, { useEffect, useState } from 'react' +import styled from 'styled-components' + +import { useWeb3React } from '@web3-react/core' +import { useTranslation } from 'react-i18next' + +import { tokenLogosServiceApi } from '../../api' +import Circle from '../../assets/images/circle.svg' +import { network } from '../../connectors' +import { NetworkContextName } from '../../constants' +import { useActiveListener, useEagerConnect, useInactiveListener } from '../../hooks' +import { useTokenListActionHandlers } from '../../state/tokenList/hooks' +import { Spinner } from '../../theme' const MessageWrapper = styled.div` display: flex; align-items: center; justify-content: center; height: 20rem; -`; +` const Message = styled.h2` color: ${({ theme }) => theme.secondary1}; -`; +` const SpinnerWrapper = styled(Spinner)` font-size: 4rem; @@ -33,65 +31,77 @@ const SpinnerWrapper = styled(Spinner)` color: ${({ theme }) => theme.secondary1}; } } -`; +` export default function Web3ReactManager({ children }) { - const { t } = useTranslation(); - const { active } = useWeb3React(); - const { - active: networkActive, - error: networkError, - activate: activateNetwork, - } = useWeb3React(NetworkContextName); - const { chainId } = useSwapState(); + const { t } = useTranslation() + const { active } = useWeb3React() + const { activate: activateNetwork, active: networkActive, error: networkError } = useWeb3React( + NetworkContextName, + ) + const { onLoadTokenList } = useTokenListActionHandlers() // try to eagerly connect to an injected provider, if it exists and has granted access already - const triedEager = useEagerConnect(); + const triedEager = useEagerConnect() // after eagerly trying injected, if the network connect ever isn't active or in an error state, activate itd useEffect(() => { if (triedEager && !networkActive && !networkError && !active) { - activateNetwork(network[chainId]); + activateNetwork(network) } - }, [ - triedEager, - networkActive, - networkError, - activateNetwork, - active, - chainId, - ]); + }, [triedEager, networkActive, networkError, activateNetwork, active]) // when there's no account connected, react to logins (broadly speaking) on the injected provider, if it exists - useInactiveListener(!triedEager); + useInactiveListener(!triedEager) // So we can trigger some events on accountsChanged - useActiveListener(); + useActiveListener() // handle delayed loader state - const [showLoader, setShowLoader] = useState(false); + const [showLoader, setShowLoader] = useState(false) useEffect(() => { const timeout = setTimeout(() => { - setShowLoader(true); - }, 600); + setShowLoader(true) + }, 600) return () => { - clearTimeout(timeout); - }; - }, []); + clearTimeout(timeout) + } + }, []) + + // Fetch token logos by chain ID + useEffect(() => { + const fetchTokenList = async (): Promise => { + try { + setShowLoader(true) + + const data = await tokenLogosServiceApi.getAllTokens() + + onLoadTokenList(data) + setShowLoader(false) + } catch (error) { + console.error('Error getting token list', error) + + onLoadTokenList(null) + setShowLoader(false) + } + } + + fetchTokenList() + }, [onLoadTokenList]) // on page load, do nothing until we've tried to connect to the injected connector if (!triedEager) { - return null; + return null } // if the account context isn't active, and there's an error on the network context, it's an irrecoverable error if (!active && networkError) { return ( - {t("unknownError")} + {t('unknownError')} - ); + ) } // if neither context is active, spin @@ -100,8 +110,8 @@ export default function Web3ReactManager({ children }) { - ) : null; + ) : null } - return children; + return children } diff --git a/src/components/Web3Status/index.tsx b/src/components/Web3Status/index.tsx index d8991f45e..9ecbe1c19 100644 --- a/src/components/Web3Status/index.tsx +++ b/src/components/Web3Status/index.tsx @@ -1,51 +1,44 @@ -import React, { useMemo } from "react"; -import styled, { css } from "styled-components"; -import { useTranslation } from "react-i18next"; -import { useWeb3React, UnsupportedChainIdError } from "@web3-react/core"; -import { darken, lighten } from "polished"; -import { Activity } from "react-feather"; -import useENSName from "../../hooks/useENSName"; -import { useWalletModalToggle } from "../../state/application/hooks"; -import { TransactionDetails } from "../../state/transactions/reducer"; +import { darken, lighten } from 'polished' +import React, { useMemo } from 'react' +import { Activity } from 'react-feather' +import styled, { css } from 'styled-components' -import Identicon from "../Identicon"; -import PortisIcon from "../../assets/images/portisIcon.png"; -import WalletModal from "../WalletModal"; -import { ButtonSecondary } from "../Button"; -import FortmaticIcon from "../../assets/images/fortmaticIcon.png"; -import WalletConnectIcon from "../../assets/images/walletConnectIcon.svg"; -import CoinbaseWalletIcon from "../../assets/images/coinbaseWalletIcon.svg"; +import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core' +import { useTranslation } from 'react-i18next' -import { Spinner } from "../../theme"; -import LightCircle from "../../assets/svg/lightcircle.svg"; - -import { RowBetween } from "../Row"; -import { shortenAddress } from "../../utils"; -import { useAllTransactions } from "../../state/transactions/hooks"; -import { chainNames, NetworkContextName } from "../../constants"; -import { - injected, - walletconnect, - walletlink, - fortmatic, - portis, -} from "../../connectors"; -import { useSwapState } from "../../state/orderPlacement/hooks"; -import { useActiveWeb3React } from "../../hooks"; +import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg' +import FortmaticIcon from '../../assets/images/fortmaticIcon.png' +import PortisIcon from '../../assets/images/portisIcon.png' +import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg' +import LightCircle from '../../assets/svg/lightcircle.svg' +import { fortmatic, injected, portis, walletconnect, walletlink } from '../../connectors' +import { NetworkContextName, chainNames } from '../../constants' +import { useActiveWeb3React } from '../../hooks' +import useENSName from '../../hooks/useENSName' +import { useWalletModalToggle } from '../../state/application/hooks' +import { useSwapState } from '../../state/orderPlacement/hooks' +import { useAllTransactions } from '../../state/transactions/hooks' +import { TransactionDetails } from '../../state/transactions/reducer' +import { Spinner } from '../../theme' +import { shortenAddress } from '../../utils' +import { ButtonSecondary } from '../Button' +import Identicon from '../Identicon' +import { RowBetween } from '../Row' +import WalletModal from '../modals/WalletModal' const SpinnerWrapper = styled(Spinner)` margin: 0 0.25rem 0 0.25rem; -`; +` const IconWrapper = styled.div<{ size?: number }>` ${({ theme }) => theme.flexColumnNoWrap}; align-items: center; justify-content: center; & > * { - height: ${({ size }) => (size ? size + "px" : "32px")}; - width: ${({ size }) => (size ? size + "px" : "32px")}; + height: ${({ size }) => (size ? size + 'px' : '32px')}; + width: ${({ size }) => (size ? size + 'px' : '32px')}; } -`; +` const Web3StatusGeneric = styled(ButtonSecondary)` ${({ theme }) => theme.flexRowNoWrap} @@ -58,7 +51,7 @@ const Web3StatusGeneric = styled(ButtonSecondary)` :focus { outline: none; } -`; +` const Web3StatusError = styled(Web3StatusGeneric)` background-color: ${({ theme }) => theme.red1}; border: 1px solid ${({ theme }) => theme.red1}; @@ -68,7 +61,7 @@ const Web3StatusError = styled(Web3StatusGeneric)` :focus { background-color: ${({ theme }) => darken(0.1, theme.red1)}; } -`; +` const Web3StatusConnect = styled(Web3StatusGeneric)<{ faded?: boolean }>` background-color: ${({ theme }) => theme.primary4}; @@ -95,13 +88,11 @@ const Web3StatusConnect = styled(Web3StatusGeneric)<{ faded?: boolean }>` color: ${({ theme }) => darken(0.05, theme.primaryText1)}; } `} -`; +` const Web3StatusConnected = styled(Web3StatusGeneric)<{ pending?: boolean }>` - background-color: ${({ pending, theme }) => - pending ? theme.primary1 : theme.bg2}; - border: 1px solid - ${({ pending, theme }) => (pending ? theme.primary1 : theme.bg3)}; + background-color: ${({ pending, theme }) => (pending ? theme.primary1 : theme.bg2)}; + border: 1px solid ${({ pending, theme }) => (pending ? theme.primary1 : theme.bg3)}; color: ${({ pending, theme }) => (pending ? theme.white : theme.text1)}; font-weight: 500; :hover, @@ -111,11 +102,10 @@ const Web3StatusConnected = styled(Web3StatusGeneric)<{ pending?: boolean }>` :focus { border: 1px solid - ${({ pending, theme }) => - pending ? darken(0.1, theme.primary1) : darken(0.1, theme.bg3)}; + ${({ pending, theme }) => (pending ? darken(0.1, theme.primary1) : darken(0.1, theme.bg3))}; } } -`; +` const Text = styled.p` flex: 1 1 auto; @@ -126,90 +116,86 @@ const Text = styled.p` font-size: 1rem; width: fit-content; font-weight: 500; -`; +` const NetworkIcon = styled(Activity)` margin-left: 0.25rem; margin-right: 0.5rem; width: 16px; height: 16px; -`; +` // we want the latest one to come first, so return negative if a is after b function newTranscationsFirst(a: TransactionDetails, b: TransactionDetails) { - return b.addedTime - a.addedTime; + return b.addedTime - a.addedTime } function recentTransactionsOnly(a: TransactionDetails) { - return new Date().getTime() - a.addedTime < 86_400_000; + return new Date().getTime() - a.addedTime < 86_400_000 } export function useNetworkCheck(): { errorWrongNetwork: string | undefined } { - const { chainId: injectedChainId } = useActiveWeb3React(); - const { chainId } = useSwapState(); + const { chainId: injectedChainId } = useActiveWeb3React() + const { chainId } = useSwapState() const errorWrongNetwork = injectedChainId == undefined || chainId == injectedChainId || chainId == 0 ? undefined - : `Please make sure you connect to the network: ${chainNames[chainId]} in your wallet`; + : `Please make sure you connect to the network: ${chainNames[chainId]} in your wallet` return { errorWrongNetwork, - }; + } } export default function Web3Status() { - const { t } = useTranslation(); - const { active, account, connector, error } = useWeb3React(); - const contextNetwork = useWeb3React(NetworkContextName); + const { t } = useTranslation() + const { account, active, connector, error } = useWeb3React() + const contextNetwork = useWeb3React(NetworkContextName) - const ENSName = useENSName(account); + const ENSName = useENSName(account) - const allTransactions = useAllTransactions(); + const allTransactions = useAllTransactions() const sortedRecentTransactions = useMemo(() => { - const txs = Object.values(allTransactions); - return txs.filter(recentTransactionsOnly).sort(newTranscationsFirst); - }, [allTransactions]); + const txs = Object.values(allTransactions) + return txs.filter(recentTransactionsOnly).sort(newTranscationsFirst) + }, [allTransactions]) - const pending = sortedRecentTransactions - .filter((tx) => !tx.receipt) - .map((tx) => tx.hash); - const confirmed = sortedRecentTransactions - .filter((tx) => tx.receipt) - .map((tx) => tx.hash); + const pending = sortedRecentTransactions.filter((tx) => !tx.receipt).map((tx) => tx.hash) + const confirmed = sortedRecentTransactions.filter((tx) => tx.receipt).map((tx) => tx.hash) - const hasPendingTransactions = !!pending.length; + const hasPendingTransactions = !!pending.length - const toggleWalletModal = useWalletModalToggle(); + const toggleWalletModal = useWalletModalToggle() - const { errorWrongNetwork } = useNetworkCheck(); + const { errorWrongNetwork } = useNetworkCheck() // handle the logo we want to show with the account function getStatusIcon() { if (connector === injected) { - return ; + return } else if (connector === walletconnect) { return ( - {""} + {''} - ); + ) } else if (connector === walletlink) { return ( - {""} + {''} - ); + ) } else if (connector === fortmatic) { return ( - {""} + {''} - ); + ) } else if (connector === portis) { return ( - {""} + {''} - ); + ) } } @@ -223,51 +209,48 @@ export default function Web3Status() { > {hasPendingTransactions ? ( - {pending?.length} Pending{" "} - + {pending?.length} Pending{' '} + ) : ( {ENSName || shortenAddress(account)} )} {!hasPendingTransactions && getStatusIcon()} - ); + ) } else if (error || errorWrongNetwork) { return ( {error instanceof UnsupportedChainIdError || errorWrongNetwork - ? "Wrong Network" - : "Error"} + ? 'Wrong Network' + : 'Error'} - ); + ) } else { return ( - - {t("Connect to a wallet")} + + {t('Connect to a wallet')} - ); + ) } } if (!contextNetwork.active && !active) { - return null; + return null } return ( <> {getWeb3Status()} - ); + ) } diff --git a/src/components/auction/AuctionDetails/index.tsx b/src/components/auction/AuctionDetails/index.tsx new file mode 100644 index 000000000..d78a94517 --- /dev/null +++ b/src/components/auction/AuctionDetails/index.tsx @@ -0,0 +1,208 @@ +import React, { useMemo } from 'react' +import styled from 'styled-components' + +import { useActiveWeb3React } from '../../../hooks' +import { useClearingPriceInfo } from '../../../hooks/useCurrentClearingOrderAndVolumeCallback' +import { + AuctionState, + orderToPrice, + orderToSellOrder, + useDerivedAuctionInfo, + useDerivedAuctionState, +} from '../../../state/orderPlacement/hooks' +import { getEtherscanLink, getTokenDisplay } from '../../../utils' +import { normalizePrice } from '../../../utils/tools' +import { KeyValue } from '../../common/KeyValue' +import TokenLogo from '../../common/TokenLogo' +import { Tooltip } from '../../common/Tooltip' +import { ExternalLink } from '../../navigation/ExternalLink' +import { BaseCard } from '../../pureStyledComponents/BaseCard' +import { AuctionTimer } from '../AuctionTimer' + +const Wrapper = styled(BaseCard)` + align-items: center; + display: grid; + grid-template-columns: 1fr 3px 1fr 154px 1fr 3px 1fr; + margin: 0 0 50px; + max-width: 100%; + min-height: 130px; +` + +const Cell = styled(KeyValue)` + padding: 0 10px; + + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + } +` + +const Break = styled.div` + background-color: ${({ theme }) => theme.primary1}; + border-radius: 3px; + min-height: 50px; + width: 3px; +` + +const TimerWrapper = styled.div` + max-height: 130px; + position: relative; +` + +const AuctionDetails = () => { + const { chainId } = useActiveWeb3React() + const { + auctioningToken, + biddingToken, + clearingPrice, + initialAuctionOrder, + initialPrice, + } = useDerivedAuctionInfo() + const { auctionState } = useDerivedAuctionState() + + const auctionTokenAddress = useMemo( + () => getEtherscanLink(chainId, auctioningToken?.address, 'address'), + [chainId, auctioningToken], + ) + + const biddingTokenAddress = useMemo( + () => getEtherscanLink(chainId, biddingToken?.address, 'address'), + [chainId, biddingToken], + ) + + const clearingPriceInfo = useClearingPriceInfo() + const biddingTokenDisplay = useMemo(() => getTokenDisplay(biddingToken), [biddingToken]) + const auctioningTokenDisplay = useMemo(() => getTokenDisplay(auctioningToken), [auctioningToken]) + + const clearingPriceDisplay = useMemo(() => { + const clearingPriceInfoAsSellOrder = + clearingPriceInfo && + orderToSellOrder(clearingPriceInfo.clearingOrder, biddingToken, auctioningToken) + let clearingPriceNumber = orderToPrice(clearingPriceInfoAsSellOrder)?.toSignificant(4) + + if (clearingPrice && auctioningToken && biddingToken) { + clearingPriceNumber = normalizePrice( + auctioningToken, + biddingToken, + clearingPrice, + ).toSignificant(4) + } + + return clearingPriceNumber + ? `${clearingPriceNumber} ${getTokenDisplay(biddingToken)}/${getTokenDisplay( + auctioningToken, + )}` + : '-' + }, [auctioningToken, biddingToken, clearingPrice, clearingPriceInfo]) + + const titlePrice = useMemo( + () => + auctionState === AuctionState.ORDER_PLACING || + auctionState === AuctionState.ORDER_PLACING_AND_CANCELING + ? 'Current price' + : auctionState === AuctionState.PRICE_SUBMISSION + ? 'Clearing price' + : 'Closing price', + [auctionState], + ) + + const initialPriceToDisplay = useMemo(() => { + if (initialPrice && auctioningToken && biddingToken) { + return normalizePrice(auctioningToken, biddingToken, initialPrice) + } else { + return initialPrice + } + }, [initialPrice, auctioningToken, biddingToken]) + + return ( + + + {titlePrice} + + + } + itemValue={clearingPriceDisplay ? clearingPriceDisplay : '-'} + /> + + + Bidding with + + + } + itemValue={ + biddingToken ? ( + <> + + {biddingTokenDisplay} + + + ) : ( + '-' + ) + } + /> + + + + + Total auctioned + + + } + itemValue={ + auctioningToken && initialAuctionOrder ? ( + <> + + {`${initialAuctionOrder?.sellAmount.toSignificant( + 2, + )} ${auctioningTokenDisplay}`} + + + ) : ( + '-' + ) + } + /> + + + + Min Sell Price + + + } + itemValue={ + <> + {initialPriceToDisplay ? initialPriceToDisplay?.toSignificant(2) : ' - '} + {initialPriceToDisplay && auctioningTokenDisplay + ? `${biddingTokenDisplay}/${auctioningTokenDisplay}` + : '-'} + + } + /> + + ) +} + +export default AuctionDetails diff --git a/src/components/auction/AuctionNotStarted/index.tsx b/src/components/auction/AuctionNotStarted/index.tsx new file mode 100644 index 000000000..0ef5161ba --- /dev/null +++ b/src/components/auction/AuctionNotStarted/index.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +import { LockBig } from '../../icons/LockBig' +import { EmptyContentText, EmptyContentWrapper } from '../../pureStyledComponents/EmptyContent' + +export const AuctionNotStarted: React.FC = () => { + return ( + + + Auction not started yet. + + ) +} diff --git a/src/components/auction/AuctionPending/index.tsx b/src/components/auction/AuctionPending/index.tsx new file mode 100644 index 000000000..2607fdca9 --- /dev/null +++ b/src/components/auction/AuctionPending/index.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +import { LockBig } from '../../icons/LockBig' +import { EmptyContentText, EmptyContentWrapper } from '../../pureStyledComponents/EmptyContent' + +export const AuctionPending: React.FC = () => { + return ( + + + Pending on-chain price-calculation. + + ) +} diff --git a/src/components/auction/AuctionTimer/index.tsx b/src/components/auction/AuctionTimer/index.tsx new file mode 100644 index 000000000..2bb0fae43 --- /dev/null +++ b/src/components/auction/AuctionTimer/index.tsx @@ -0,0 +1,212 @@ +import React from 'react' +import styled, { keyframes } from 'styled-components' + +import { + AuctionState, + useDerivedAuctionInfo, + useDerivedAuctionState, +} from '../../../state/orderPlacement/hooks' +import { + calculateTimeLeft, + calculateTimeProgress, + getDays, + getHours, + getMinutes, + getSeconds, +} from '../../../utils/tools' + +const TIMER_SIZE = '154px' + +const Wrapper = styled.div<{ progress?: string }>` + align-items: center; + background: ${({ theme }) => theme.primary1}; + background: conic-gradient( + ${({ theme }) => theme.primary1} calc(${(props) => props.progress}), + ${({ theme }) => theme.primary3} 0% + ); + border-radius: 50%; + display: flex; + height: ${TIMER_SIZE}; + justify-content: center; + margin-top: -13px; + width: ${TIMER_SIZE}; +` + +Wrapper.defaultProps = { + progress: '0%', +} + +const Center = styled.div` + align-items: center; + background-color: ${({ theme }) => theme.mainBackground}; + border-radius: 50%; + box-shadow: 0 0 6px 0 ${({ theme }) => theme.mainBackground}; + display: flex; + flex-flow: column; + height: 126px; + justify-content: center; + width: 126px; +` + +const Days = styled.div` + font-size: 20px; + line-height: 1; + margin: 0; + text-transform: uppercase; +` + +const Time = styled.div` + color: ${({ theme }) => theme.primary1}; + flex-shrink: 1; + font-size: 18px; + font-weight: 700; + letter-spacing: -1px; + line-height: 1.2; + margin-bottom: 3px; + min-width: 0; + text-align: center; + white-space: nowrap; +` + +const Text = styled.div` + color: ${({ theme }) => theme.primary1}; + font-size: 15px; + font-weight: 700; + line-height: 1; + opacity: 0.8; + text-align: center; + text-transform: uppercase; +` + +const TextBig = styled.div` + color: ${({ theme }) => theme.primary1}; + font-size: 17px; + font-weight: 600; + line-height: 1.2; + text-align: center; + text-transform: uppercase; +` + +const Blinker = keyframes` + 0% { + opacity: 1; + } + 50% { + opacity: 1; + } + 50.01% { + opacity: 0; + } + 100% { + opacity: 0; + } +` + +const Blink = styled.span` + animation-direction: alternate; + animation-duration: 0.5s; + animation-iteration-count: infinite; + animation-name: ${Blinker}; + animation-timing-function: linear; + + &::before { + content: ':'; + } +` + +const formatSeconds = (seconds: number): React.ReactNode => { + const days = getDays(seconds) + const hours = getHours(seconds) + const minutes = getMinutes(seconds) + const remainderSeconds = getSeconds(seconds) + + return ( + <> + {days > 0 && ( + + {`${days} `} + {days === 1 ? 'Day' : 'Days'} + + )} +
+ <> + {hours >= 0 && hours < 10 && `0`} + {hours} + + <> + + {minutes >= 0 && minutes < 10 && `0`} + {minutes} + + <> + + {remainderSeconds >= 0 && remainderSeconds < 10 && `0`} + {remainderSeconds} + +
+ + ) +} + +export const AuctionTimer = () => { + const { auctionState, loading } = useDerivedAuctionState() + const { auctionEndDate, auctionStartDate } = useDerivedAuctionInfo() + const [timeLeft, setTimeLeft] = React.useState(calculateTimeLeft(auctionEndDate)) + + React.useEffect(() => { + const id = setInterval(() => { + setTimeLeft(calculateTimeLeft(auctionEndDate)) + }, 1000) + return () => { + clearInterval(id) + } + }, [auctionEndDate]) + + return ( + +
+ {(auctionState === null || loading) && Loading} + {auctionState === AuctionState.NOT_YET_STARTED && ( + + Auction +
not +
+ started +
+ )} + {auctionState === AuctionState.CLAIMING && ( + + Auction +
claiming +
+ )} + {(auctionState === AuctionState.ORDER_PLACING_AND_CANCELING || + auctionState === AuctionState.ORDER_PLACING) && ( + <> + + Ends in + + )} + {auctionState === AuctionState.PRICE_SUBMISSION && Auction Closed} + {auctionState !== AuctionState.NOT_YET_STARTED && + auctionState !== AuctionState.ORDER_PLACING_AND_CANCELING && + auctionState !== AuctionState.ORDER_PLACING && + auctionState !== AuctionState.PRICE_SUBMISSION && + auctionState !== AuctionState.CLAIMING && + auctionState !== null && Auction Settled} +
+
+ ) +} diff --git a/src/components/auction/Claimer/index.tsx b/src/components/auction/Claimer/index.tsx new file mode 100644 index 000000000..560ce5ec2 --- /dev/null +++ b/src/components/auction/Claimer/index.tsx @@ -0,0 +1,170 @@ +import React, { useMemo, useState } from 'react' +import styled from 'styled-components' + +import { useActiveWeb3React } from '../../../hooks' +import { useClaimOrderCallback, useGetAuctionProceeds } from '../../../hooks/useClaimOrderCallback' +import { useWalletModalToggle } from '../../../state/application/hooks' +import { + useDerivedAuctionInfo, + useDerivedClaimInfo, + useSwapState, +} from '../../../state/orderPlacement/hooks' +import { getTokenDisplay } from '../../../utils' +import ClaimConfirmationModal from '../../ClaimConfirmationModal' +import { Button } from '../../buttons/Button' +import TokenLogo from '../../common/TokenLogo' +import { ErrorInfo } from '../../icons/ErrorInfo' +import { BaseCard } from '../../pureStyledComponents/BaseCard' +import { ErrorRow, ErrorText, ErrorWrapper } from '../../pureStyledComponents/Error' + +const Wrapper = styled(BaseCard)` + max-width: 100%; + min-height: 380px; + min-width: 100%; +` + +const ActionButton = styled(Button)` + height: 52px; + margin-top: auto; +` + +const TokensWrapper = styled.div` + background-color: ${({ theme }) => theme.primary4}; + border-radius: 12px; + border: 1px solid ${({ theme }) => theme.textField.borderColor}; + margin-bottom: 20px; +` + +const TokenItem = styled.div` + align-items: center; + border-bottom: 1px solid ${({ theme }) => theme.textField.borderColor}; + display: flex; + justify-content: space-between; + padding: 7px 14px; + + &:last-child { + border-bottom: none; + } +` + +const Token = styled.div` + align-items: center; + display: flex; + justify-content: space-between; +` + +const Text = styled.div` + color: ${({ theme }) => theme.text1}; + font-size: 24px; + font-weight: 600; + line-height: 1.2; + margin-left: 10px; +` + +const Claimer: React.FC = () => { + const { account } = useActiveWeb3React() + const toggleWalletModal = useWalletModalToggle() + const { auctionId } = useSwapState() + const { auctioningToken, biddingToken } = useDerivedAuctionInfo() + const { error } = useDerivedClaimInfo(auctionId) + + const isValid = !error + const [showConfirm, setShowConfirm] = useState(false) + const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for user confirmation + + const { claimableAuctioningToken, claimableBiddingToken } = useGetAuctionProceeds() + const [txHash, setTxHash] = useState('') + + function resetModal() { + setPendingConfirmation(true) + } + + const claimOrderCallback = useClaimOrderCallback() + + function onClaimOrder() { + claimOrderCallback().then((hash) => { + setTxHash(hash) + setPendingConfirmation(false) + }) + } + + const pendingText = `Claiming Funds` + const biddingTokenDisplay = useMemo(() => getTokenDisplay(biddingToken), [biddingToken]) + const auctioningTokenDisplay = useMemo(() => getTokenDisplay(auctioningToken), [auctioningToken]) + + return ( + + + + + {biddingToken && biddingTokenDisplay ? ( + <> + + {biddingTokenDisplay} + + ) : ( + '-' + )} + + + {claimableBiddingToken ? `${claimableBiddingToken.toSignificant(2)} ` : `0.00`} + + + + + {auctioningToken && auctioningTokenDisplay ? ( + <> + + {auctioningTokenDisplay} + + ) : ( + '-' + )} + + + {claimableAuctioningToken ? `${claimableAuctioningToken.toSignificant(2)}` : `0.00`} + + + + {!isValid && ( + + + + {error} + + + )} + {!account ? ( + Connect Wallet + ) : ( + { + setShowConfirm(true) + onClaimOrder() + }} + > + Claim + + )} + { + resetModal() + setShowConfirm(false) + }} + pendingConfirmation={pendingConfirmation} + pendingText={pendingText} + /> + + ) +} + +export default Claimer diff --git a/src/components/auction/OrderPlacement/index.tsx b/src/components/auction/OrderPlacement/index.tsx new file mode 100644 index 000000000..8b6de3498 --- /dev/null +++ b/src/components/auction/OrderPlacement/index.tsx @@ -0,0 +1,294 @@ +import React, { useEffect, useMemo, useState } from 'react' +import styled from 'styled-components' +import { ChainId, Fraction, TokenAmount } from 'uniswap-xdai-sdk' + +import { EASY_AUCTION_NETWORKS } from '../../../constants' +import { useActiveWeb3React } from '../../../hooks' +import { ApprovalState, useApproveCallback } from '../../../hooks/useApproveCallback' +import { usePlaceOrderCallback } from '../../../hooks/usePlaceOrderCallback' +import { useWalletModalToggle } from '../../../state/application/hooks' +import { + AuctionState, + useDerivedAuctionInfo, + useDerivedAuctionState, + useGetOrderPlacementError, + useSwapActionHandlers, + useSwapState, +} from '../../../state/orderPlacement/hooks' +import { useOrderState } from '../../../state/orders/hooks' +import { OrderState } from '../../../state/orders/reducer' +import { useTokenBalance } from '../../../state/wallet/hooks' +import { getTokenDisplay } from '../../../utils' +import ConfirmationModal from '../../ConfirmationModal' +import { Button } from '../../buttons/Button' +import { ButtonType } from '../../buttons/buttonStylingTypes' +import TokenLogo from '../../common/TokenLogo' +import CurrencyInputPanel from '../../form/CurrencyInputPanel' +import PriceInputPanel from '../../form/PriceInputPanel' +import { ErrorInfo } from '../../icons/ErrorInfo' +import { ErrorLock } from '../../icons/ErrorLock' +import WarningModal from '../../modals/WarningModal' +import { BaseCard } from '../../pureStyledComponents/BaseCard' +import { ErrorRow, ErrorText, ErrorWrapper } from '../../pureStyledComponents/Error' +import SwapModalFooter from '../../swap/PlaceOrderModalFooter' +import SwapModalHeader from '../../swap/SwapModalHeader' + +const Wrapper = styled(BaseCard)` + max-width: 100%; + min-width: 100%; +` + +const ActionButton = styled(Button)` + height: 52px; + margin-top: auto; +` + +const BalanceWrapper = styled.div` + display: flex; + align-items: center; + margin-bottom: 20px; +` + +const Balance = styled.p` + color: ${({ theme }) => theme.text1}; + font-size: 18px; + font-weight: 600; + line-height: 1.2; + margin: 0 10px 0 0; + text-align: left; +` + +const Total = styled.span` + font-weight: 400; +` + +const ApprovalWrapper = styled.div` + align-items: center; + border-radius: 6px; + border: 1px solid ${({ theme }) => theme.primary1}; + display: flex; + margin-bottom: 10px; + padding: 7px 12px; +` + +const ApprovalText = styled.p` + color: ${({ theme }) => theme.primary1}; + font-size: 13px; + font-weight: normal; + line-height: 1.23; + margin: 0 25px 0 0; + text-align: left; +` + +const ApprovalButton = styled(Button)` + border-radius: 4px; + font-size: 14px; + font-weight: 600; + height: 26px; + padding: 0 14px; +` + +const OrderPlacement: React.FC = () => { + const { account, chainId } = useActiveWeb3React() + const orders: OrderState | undefined = useOrderState() + const toggleWalletModal = useWalletModalToggle() + const { price, sellAmount } = useSwapState() + const { + auctioningToken, + biddingToken, + biddingTokenBalance, + initialPrice, + parsedBiddingAmount, + } = useDerivedAuctionInfo() + const { error } = useGetOrderPlacementError() + const { onUserSellAmountInput } = useSwapActionHandlers() + const { onUserPriceInput } = useSwapActionHandlers() + const { auctionState } = useDerivedAuctionState() + + const isValid = !error + + const [showConfirm, setShowConfirm] = useState(false) + const [showWarning, setShowWarning] = useState(false) + const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirmed + const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for user confirmation + const [txHash, setTxHash] = useState('') + const approvalTokenAmount: TokenAmount | undefined = parsedBiddingAmount + + const [approval, approveCallback] = useApproveCallback( + approvalTokenAmount, + EASY_AUCTION_NETWORKS[chainId as ChainId], + ) + const [approvalSubmitted, setApprovalSubmitted] = useState(false) + + useEffect(() => { + if (approval === ApprovalState.PENDING) { + setApprovalSubmitted(true) + } + }, [approval, approvalSubmitted]) + + const maxAmountInput: TokenAmount = biddingTokenBalance ? biddingTokenBalance : undefined + + useEffect(() => { + if (price == '-' && initialPrice) { + onUserPriceInput(initialPrice.multiply(new Fraction('1001', '1000')).toSignificant(4)) + } + }, [onUserPriceInput, price, initialPrice]) + + const resetModal = () => { + if (!pendingConfirmation) { + onUserSellAmountInput('') + } + setPendingConfirmation(true) + setAttemptingTxn(false) + } + + const placeOrderCallback = usePlaceOrderCallback(auctioningToken, biddingToken) + + const onPlaceOrder = () => { + setAttemptingTxn(true) + + placeOrderCallback().then((hash) => { + setTxHash(hash) + setPendingConfirmation(false) + }) + } + + const [showInverted, setShowInverted] = useState(false) + + const modalHeader = () => { + return + } + + const modalBottom = () => { + return ( + + ) + } + + const pendingText = `Placing order` + const biddingTokenDisplay = useMemo(() => getTokenDisplay(biddingToken), [biddingToken]) + const auctioningTokenDisplay = useMemo(() => getTokenDisplay(auctioningToken), [auctioningToken]) + const userTokenBalance = useTokenBalance(account, biddingToken) + const notApproved = approval === ApprovalState.NOT_APPROVED || approval === ApprovalState.PENDING + const orderPlacingOnly = auctionState === AuctionState.ORDER_PLACING + + const handleShowConfirm = () => { + const sameOrder = orders.orders.find((order) => order.price === price) + + if (!sameOrder) { + setShowConfirm(true) + } else { + setShowWarning(true) + } + } + + return ( + + + + Your Balance:{' '} + {`${ + account + ? `${userTokenBalance?.toSignificant(6)} ${biddingToken?.symbol}` + : 'Connect your wallet' + } `} + + {account && biddingToken && biddingToken.address && ( + + )} + + { + maxAmountInput && onUserSellAmountInput(maxAmountInput.toExact()) + }} + onUserSellAmountInput={onUserSellAmountInput} + token={biddingToken} + value={sellAmount} + /> + + {(error || orderPlacingOnly) && ( + + {error && sellAmount !== '' && price !== '' && ( + + + {error} + + )} + {orderPlacingOnly && ( + + + + New orders can't be cancelled once you confirm the transaction in the next + step. + + + )} + + )} + {notApproved && ( + + + You need to unlock {biddingToken.symbol} to allow the smart contract to interact with + it. This has to be done for each new token. + + + {approval === ApprovalState.PENDING ? `Approving` : `Approve`} + + + )} + {!account ? ( + Connect Wallet + ) : ( + + Place Order + + )} + { + setShowWarning(false) + }} + title="Warning!" + /> + { + resetModal() + setShowConfirm(false) + }} + pendingConfirmation={pendingConfirmation} + pendingText={pendingText} + title="Confirm Order" + topContent={modalHeader} + /> + + ) +} + +export default OrderPlacement diff --git a/src/components/auction/Orderbook/index.tsx b/src/components/auction/Orderbook/index.tsx new file mode 100644 index 000000000..d860bf9ca --- /dev/null +++ b/src/components/auction/Orderbook/index.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import styled from 'styled-components' +import { Token } from 'uniswap-xdai-sdk' + +import { useActiveWeb3React } from '../../../hooks' +import { useOrderbookState } from '../../../state/orderbook/hooks' +import { BaseCard } from '../../pureStyledComponents/BaseCard' +import OrderBookChart, { OrderBookError } from '../OrderbookChart' +import { processOrderbookData } from '../OrderbookWidget' + +const Wrapper = styled(BaseCard)` + max-width: 100%; + min-width: 100%; +` + +interface OrderBookProps { + baseToken: Token + label?: string + quoteToken: Token +} + +export const OrderBook: React.FC = (props: OrderBookProps) => { + const { baseToken, quoteToken } = props + const { chainId } = useActiveWeb3React() + + const { asks, bids, error, userOrderPrice, userOrderVolume } = useOrderbookState() + + const processedOrderbook = processOrderbookData({ + data: { bids, asks }, + userOrder: { price: userOrderPrice, volume: userOrderVolume }, + baseToken, + quoteToken, + }) + + return ( + <> + + {error || !asks || asks.length === 0 ? ( + + ) : ( + + )} + + + ) +} diff --git a/src/components/auction/OrderbookChart/index.tsx b/src/components/auction/OrderbookChart/index.tsx new file mode 100644 index 000000000..94245a441 --- /dev/null +++ b/src/components/auction/OrderbookChart/index.tsx @@ -0,0 +1,286 @@ +import React, { useEffect, useRef } from 'react' +import styled from 'styled-components' +import { Token } from 'uniswap-xdai-sdk' + +import * as am4charts from '@amcharts/amcharts4/charts' +import * as am4core from '@amcharts/amcharts4/core' +import am4themesSpiritedaway from '@amcharts/amcharts4/themes/spiritedaway' + +import { InlineLoading } from '../../common/InlineLoading' +import { SpinnerSize } from '../../common/Spinner' + +export enum Offer { + Bid, + Ask, +} + +/** + * Price point data represented in the graph. Contains BigNumbers for operate with less errors and more precission + * but for representation uses number as expected by the library + */ +export interface PricePointDetails { + // Basic data + type: Offer + volume: number // volume for the price point + totalVolume: number // cumulative volume + price: number + + // Data for representation + priceNumber: number + priceFormatted: string + totalVolumeNumber: number + totalVolumeFormatted: string + askValueY: Maybe + bidValueY: Maybe + newOrderValueY: Maybe + clearingPriceValueY: Maybe +} + +export interface OrderBookChartProps { + baseToken: Token + quoteToken: Token + networkId: number + data: Maybe +} + +const Wrapper = styled.div` + align-content: center; + align-items: center; + box-sizing: border-box; + color: ${({ theme }) => theme.text2}; + display: flex; + height: 100%; + justify-content: center; + position: relative; + width: 100%; + + .amcharts-Sprite-group { + pointer-events: none; + } + + .amcharts-Label { + text-transform: uppercase; + font-size: 10px; + letter-spacing: 1px; + color: ${({ theme }) => theme.text4}; + margin: 10px; + } + + .amcharts-ZoomOutButton-group > .amcharts-RoundedRectangle-group { + fill: var(--color-text-active); + opacity: 0.6; + transition: 0.3s ease-in-out; + + &:hover { + opacity: 1; + } + } + + .amcharts-CategoryAxis .amcharts-Label-group > .amcharts-Label, + .amcharts-ValueAxis-group .amcharts-Label-group > .amcharts-Label { + fill: ${({ theme }) => theme.text3}; + } +` + +am4core.useTheme(am4themesSpiritedaway) + +const OrderBookChart: React.FC = (props: OrderBookChartProps) => { + const { baseToken, data, quoteToken } = props + const chartRef = useRef(null) + + useEffect(() => { + if (!baseToken || !quoteToken || !data) return + + if (chartRef.current) return + + const baseTokenLabel = baseToken.symbol + const quoteTokenLabel = quoteToken.symbol + const market = quoteTokenLabel + '-' + baseTokenLabel + + const priceTitle = ` Price (${baseTokenLabel})` + const volumeTitle = ` Volume (${quoteTokenLabel})` + + chartRef.current = am4core.create('chartdiv', am4charts.XYChart) + // Add data + chartRef.current.data = data + + chartRef.current.paddingTop = 20 + chartRef.current.marginTop = 20 + chartRef.current.paddingBottom = 0 + chartRef.current.paddingLeft = 0 + chartRef.current.paddingRight = 0 + chartRef.current.marginBottom = 0 + + // Colors + const colors = { + green: '#28a745', + red: '#dc3545', + white: '#FFFFFF', + grey: '#565A69', + orange: '#FF6347', + } + + // Create axes + const priceAxis = chartRef.current.xAxes.push(new am4charts.ValueAxis()) + const volumeAxis = chartRef.current.yAxes.push(new am4charts.ValueAxis()) + volumeAxis.renderer.grid.template.stroke = am4core.color(colors.white) + volumeAxis.renderer.grid.template.strokeWidth = 0.5 + volumeAxis.renderer.grid.template.strokeOpacity = 0.5 + volumeAxis.title.text = volumeTitle + volumeAxis.title.fill = am4core.color(colors.white) + volumeAxis.renderer.labels.template.fill = am4core.color(colors.white) + + priceAxis.renderer.grid.template.stroke = am4core.color(colors.white) + priceAxis.renderer.grid.template.strokeWidth = 0.5 + priceAxis.renderer.grid.template.strokeOpacity = 0.5 + priceAxis.title.text = priceTitle + priceAxis.title.fill = am4core.color(colors.white) + priceAxis.renderer.labels.template.fill = am4core.color(colors.white) + + const min = Math.min.apply( + 0, + data.map((order) => order.priceNumber), + ) + // Reduce the min in a 5% + priceAxis.min = min - min * 0.05 + + const max = Math.max.apply( + 0, + data.map((order) => order.priceNumber), + ) + // Reduce the max in a 5% + priceAxis.max = max + max * 0.05 + + priceAxis.strictMinMax = true + priceAxis.renderer.grid.template.disabled = true + priceAxis.renderer.labels.template.disabled = true + + const createGrid = (value) => { + const range = priceAxis.axisRanges.create() + range.value = value + range.label.text = '{value}' + } + + const factor = (priceAxis.max - priceAxis.min) / 5 + + const firstGrid = priceAxis.min + factor + const secondGrid = priceAxis.min + factor * 2 + const thirdGrid = priceAxis.min + factor * 3 + const fourGrid = priceAxis.min + factor * 4 + const fiveGrid = priceAxis.min + factor * 5 + + createGrid(firstGrid.toFixed(2)) + createGrid(secondGrid.toFixed(2)) + createGrid(thirdGrid.toFixed(2)) + createGrid(fourGrid.toFixed(2)) + createGrid(fiveGrid.toFixed(2)) + + // Create serie, green line shows the price (x axis) and size (y axis) of the bids that have been placed, both expressed in the bid token + const bidSeries = chartRef.current.series.push(new am4charts.StepLineSeries()) + bidSeries.dataFields.valueX = 'priceNumber' + bidSeries.dataFields.valueY = 'bidValueY' + bidSeries.strokeWidth = 2 + bidSeries.stroke = am4core.color(colors.green) + bidSeries.fill = bidSeries.stroke + bidSeries.fillOpacity = 0.2 + bidSeries.dummyData = { + description: + 'Shows the price (x axis) and size (y axis) of the bids that have been placed, both expressed in the bid token', + } + bidSeries.tooltipText = `[bold]${market}[/]\nBid Price: [bold]{priceFormatted}[/] ${quoteTokenLabel}\nVolume: [bold]{totalVolumeFormatted}[/] ${baseTokenLabel}` + + // Create serie, red line, shows the minimum sell price (x axis) the auctioneer is willing to accept + const askSeries = chartRef.current.series.push(new am4charts.LineSeries()) + askSeries.dataFields.valueX = 'priceNumber' + askSeries.dataFields.valueY = 'askValueY' + askSeries.strokeWidth = 2 + askSeries.stroke = am4core.color(colors.red) + askSeries.fill = askSeries.stroke + askSeries.fillOpacity = 0.1 + askSeries.dummyData = { + description: 'Shows the minimum sell price (x axis) the auctioneer is willing to accept', + } + askSeries.tooltipText = `[bold]${market}[/]\nAsk Price: [bold]{priceFormatted}[/] ${quoteTokenLabel}\nVolume: [bold]{totalVolumeFormatted}[/] ${baseTokenLabel}` + + // New order to be placed + const inputSeries = chartRef.current.series.push(new am4charts.LineSeries()) + inputSeries.dataFields.valueX = 'priceNumber' + inputSeries.dataFields.valueY = 'newOrderValueY' + inputSeries.strokeWidth = 4 + inputSeries.stroke = am4core.color(colors.orange) + inputSeries.fill = inputSeries.stroke + inputSeries.fillOpacity = 0.1 + inputSeries.dummyData = { + description: 'New orders to be placed', + } + + // Dotted white line -> shows the Current price, which is the closing price of the auction if + // no more bids are submitted or canceled and the auction ends + const priceSeries = chartRef.current.series.push(new am4charts.LineSeries()) + priceSeries.dataFields.valueX = 'priceNumber' + priceSeries.dataFields.valueY = 'clearingPriceValueY' + priceSeries.strokeWidth = 2 + priceSeries.strokeDasharray = '3,3' + priceSeries.stroke = am4core.color(colors.white) + priceSeries.fill = inputSeries.stroke + priceSeries.fillOpacity = 0.1 + priceSeries.dummyData = { + description: + 'Shows the Current price, which is the closing price of the auction if no more bids are submitted or canceled and the auction ends', + } + + // Add cursor + chartRef.current.cursor = new am4charts.XYCursor() + chartRef.current.cursor.snapToSeries = [bidSeries, askSeries] + chartRef.current.cursor.lineX.stroke = am4core.color(colors.white) + chartRef.current.cursor.lineX.strokeWidth = 1 + chartRef.current.cursor.lineX.strokeOpacity = 0.6 + chartRef.current.cursor.lineX.strokeDasharray = '4' + + chartRef.current.cursor.lineY.stroke = am4core.color(colors.white) + chartRef.current.cursor.lineY.strokeWidth = 1 + chartRef.current.cursor.lineY.strokeOpacity = 0.6 + chartRef.current.cursor.lineY.strokeDasharray = '4' + + // Button configuration + chartRef.current.zoomOutButton.background.cornerRadius(5, 5, 5, 5) + chartRef.current.zoomOutButton.background.fill = am4core.color(colors.grey) + chartRef.current.zoomOutButton.icon.stroke = am4core.color(colors.white) + chartRef.current.zoomOutButton.icon.strokeWidth = 2 + chartRef.current.zoomOutButton.tooltip.text = 'Zoom out' + + // Legend + chartRef.current.legend = new am4charts.Legend() + chartRef.current.legend.labels.template.fill = am4core.color(colors.white) + chartRef.current.legend.itemContainers.template.tooltipText = + '{dataContext.dummyData.description}' + }, [data, baseToken, quoteToken]) + + // Handle component unmounting, dispose chart + useEffect(() => { + return () => { + chartRef.current && chartRef.current.dispose() + } + }, []) + + useEffect(() => { + if (!chartRef.current || data === null) return + chartRef.current.data = data.length === 0 ? [] : data + }, [data]) + + return ( + + + + ) +} + +interface OrderBookErrorProps { + error: Error +} + +export const OrderBookError: React.FC = ({ error }: OrderBookErrorProps) => ( + {error ? error.message : } +) + +export default OrderBookChart diff --git a/src/components/OrderbookWidget.tsx b/src/components/auction/OrderbookWidget/index.tsx similarity index 65% rename from src/components/OrderbookWidget.tsx rename to src/components/auction/OrderbookWidget/index.tsx index 33582dbdc..4d140c3cb 100644 --- a/src/components/OrderbookWidget.tsx +++ b/src/components/auction/OrderbookWidget/index.tsx @@ -1,25 +1,22 @@ -import React from "react"; - -import { OrderBookData, PricePoint } from "../api/AdditionalServicesApi"; +import React from 'react' +import { Token } from 'uniswap-xdai-sdk' +import { OrderBookData, PricePoint } from '../../../api/AdditionalServicesApi' +import { useOrderbookState } from '../../../state/orderbook/hooks' import OrderBookChart, { + Offer, OrderBookChartProps, OrderBookError, PricePointDetails, - Offer, -} from "./OrderbookChart"; -import { Token } from "uniswap-xdai-sdk"; -import { useOrderbookState } from "../state/orderbook/hooks"; +} from '../OrderbookChart' -const SMALL_VOLUME_THRESHOLD = 0.001; +const SMALL_VOLUME_THRESHOLD = 0.001 -// Todo: to be removed -// eslint-disable-next-line @typescript-eslint/no-explicit-any export const logDebug = (...args: any[]): void => { - if (true) { - console.log(...args); - } -}; + // eslint-disable-next-line no-console + console.log(...args) +} + const addClearingPriceInfo = ( price: number, pricePointsDetails: PricePointDetails[], @@ -34,16 +31,14 @@ const addClearingPriceInfo = ( priceNumber: price, totalVolumeNumber: 0, priceFormatted: price.toString(), - totalVolumeFormatted: "0", + totalVolumeFormatted: '0', askValueY: null, bidValueY: null, newOrderValueY: null, clearingPriceValueY: 0, - }; - const valueYofBids = pricePointsDetails.map((y) => - Math.max(y.bidValueY, y.askValueY), - ); - const maxValueYofBid = Math.max(...valueYofBids); + } + const valueYofBids = pricePointsDetails.map((y) => Math.max(y.bidValueY, y.askValueY)) + const maxValueYofBid = Math.max(...valueYofBids) const pricePointTop: PricePointDetails = { type: null, volume: null, @@ -54,14 +49,14 @@ const addClearingPriceInfo = ( priceNumber: price, totalVolumeNumber: 0, priceFormatted: price.toString(), - totalVolumeFormatted: "0", + totalVolumeFormatted: '0', askValueY: null, bidValueY: null, newOrderValueY: null, clearingPriceValueY: maxValueYofBid, - }; - return [pricePointBottom, pricePointTop]; -}; + } + return [pricePointBottom, pricePointTop] +} /** * This method turns the raw data that the backend returns into data that can be displayed by the chart. * This involves aggregating the total volume and accounting for decimals @@ -73,56 +68,48 @@ const processData = ( lowestValue: number, type: Offer, ): PricePointDetails[] => { - const isBid = type == Offer.Bid; + const isBid = type == Offer.Bid // Filter tiny orders if (isBid) { - pricePoints = pricePoints.filter( - (pricePoint) => pricePoint.volume > SMALL_VOLUME_THRESHOLD, - ); + pricePoints = pricePoints.filter((pricePoint) => pricePoint.volume > SMALL_VOLUME_THRESHOLD) } else { pricePoints = pricePoints.filter( - (pricePoint) => - pricePoint.volume * pricePoint.price > SMALL_VOLUME_THRESHOLD, - ); + (pricePoint) => pricePoint.volume * pricePoint.price > SMALL_VOLUME_THRESHOLD, + ) } // Adding first and last element to round up the picture if (type == Offer.Bid) { - if ( - userOrder && - highestValue * 1.5 > userOrder.price && - userOrder.price > lowestValue - ) { - highestValue = - highestValue > userOrder.price ? highestValue : userOrder.price; - pricePoints = pricePoints.concat(userOrder); + if (userOrder && highestValue * 1.5 > userOrder.price && userOrder.price > lowestValue) { + highestValue = highestValue > userOrder.price ? highestValue : userOrder.price + pricePoints = pricePoints.concat(userOrder) } - pricePoints.sort((lhs, rhs) => -1 * (lhs.price - rhs.price)); + pricePoints.sort((lhs, rhs) => -1 * (lhs.price - rhs.price)) pricePoints.push({ price: (highestValue * 101) / 100, volume: 0, - }); + }) - pricePoints.sort((lhs, rhs) => -1 * (lhs.price - rhs.price)); + pricePoints.sort((lhs, rhs) => -1 * (lhs.price - rhs.price)) } else { pricePoints.push({ price: (highestValue * 101) / 100, volume: 0, - }); + }) pricePoints.push({ price: (pricePoints[0].price * 99) / 100, volume: 0, - }); - pricePoints.sort((lhs, rhs) => lhs.price - rhs.price); + }) + pricePoints.sort((lhs, rhs) => lhs.price - rhs.price) } // Convert the price points that can be represented in the graph (PricePointDetails) const { points } = pricePoints.reduce( (acc, pricePoint, index) => { - const { price, volume } = pricePoint; - const totalVolume = acc.totalVolume; + const { price, volume } = pricePoint + const totalVolume = acc.totalVolume // Amcharts draws step lines so that the x value is centered (Default). To correctly display the order book, we want // the x value to be at the left side of the step for asks and at the right side of the step for bids. @@ -136,13 +123,13 @@ const processData = ( // For asks, we can offset the "startLocation" by 0.5. However, Amcharts does not support a "startLocation" of -0.5. // For bids, we therefore offset the curve by -1 (expose the previous total volume) and use an offset of 0.5. // Otherwise our steps would be off by one. - let askValueY, bidValueY; + let askValueY, bidValueY if (isBid) { - askValueY = null; - bidValueY = totalVolume; + askValueY = null + bidValueY = totalVolume } else { - askValueY = totalVolume * price; - bidValueY = null; + askValueY = totalVolume * price + bidValueY = null } // Add the new point const pricePointDetails: PricePointDetails = { @@ -160,8 +147,8 @@ const processData = ( bidValueY, newOrderValueY: null, clearingPriceValueY: null, - }; - acc.points.push(pricePointDetails); + } + acc.points.push(pricePointDetails) if (!isBid) { // Add the new point at the beginning of order // ------------ @@ -182,8 +169,8 @@ const processData = ( bidValueY, newOrderValueY: null, clearingPriceValueY: null, - }; - acc.points.push(pricePointDetails); + } + acc.points.push(pricePointDetails) } // Next two points are only added for displaying new Order @@ -204,8 +191,8 @@ const processData = ( bidValueY: null, newOrderValueY: bidValueY + volume, clearingPriceValueY: null, - }; - acc.points.push(pricePointDetails); + } + acc.points.push(pricePointDetails) const pricePointDetails_2: PricePointDetails = { type, volume, @@ -221,8 +208,8 @@ const processData = ( bidValueY: null, newOrderValueY: bidValueY, clearingPriceValueY: null, - }; - acc.points.push(pricePointDetails_2); + } + acc.points.push(pricePointDetails_2) } if ( index > 0 && @@ -249,41 +236,41 @@ const processData = ( bidValueY: null, newOrderValueY: bidValueY, clearingPriceValueY: null, - }; - acc.points.push(pricePointDetails); + } + acc.points.push(pricePointDetails) } - return { totalVolume: totalVolume + volume, points: acc.points }; + return { totalVolume: totalVolume + volume, points: acc.points } }, { totalVolume: 0, points: [] as PricePointDetails[], }, - ); + ) - return points; -}; + return points +} function _printOrderBook( pricePoints: PricePointDetails[], - baseTokenSymbol = "", - quoteTokenSymbol = "", + baseTokenSymbol = '', + quoteTokenSymbol = '', ): void { - logDebug("Order Book: " + baseTokenSymbol + "-" + quoteTokenSymbol); + logDebug('Order Book: ' + baseTokenSymbol + '-' + quoteTokenSymbol) pricePoints.forEach((pricePoint) => { - const isBid = pricePoint.type === Offer.Bid; + const isBid = pricePoint.type === Offer.Bid logDebug( - `\t${isBid ? "Bid" : "Ask"} ${ - pricePoint.totalVolumeFormatted - } ${baseTokenSymbol} at ${pricePoint.priceFormatted} ${quoteTokenSymbol}`, - ); - }); + `\t${isBid ? 'Bid' : 'Ask'} ${pricePoint.totalVolumeFormatted} ${baseTokenSymbol} at ${ + pricePoint.priceFormatted + } ${quoteTokenSymbol}`, + ) + }) } interface ProcessRawDataParams { - data: OrderBookData; - userOrder: PricePoint; - baseToken: Pick; - quoteToken: Pick; + data: OrderBookData + userOrder: PricePoint + baseToken: Pick + quoteToken: Pick } export function findClearingPrice( sellOrders: PricePoint[], @@ -291,109 +278,87 @@ export function findClearingPrice( initialAuctionOrder: PricePoint, ): number | undefined { if (userOrder) { - if (userOrder.price > initialAuctionOrder.price && userOrder.volume > 0) { - sellOrders = sellOrders.concat(userOrder); + if (userOrder?.price > initialAuctionOrder?.price && userOrder.volume > 0) { + sellOrders = sellOrders.concat(userOrder) } } - sellOrders = Object.values(sellOrders); + sellOrders = Object.values(sellOrders) - sellOrders.sort((lhs, rhs) => -1 * (lhs.price - rhs.price)); - let totalSellVolume = 0; + sellOrders.sort((lhs, rhs) => -1 * (lhs.price - rhs.price)) + let totalSellVolume = 0 for (const order of sellOrders) { - totalSellVolume = totalSellVolume + order.volume; + totalSellVolume = totalSellVolume + order.volume if (totalSellVolume >= initialAuctionOrder.volume * order.price) { const coveredBuyAmount = - initialAuctionOrder.volume * order.price - - (totalSellVolume - order.volume); + initialAuctionOrder.volume * order.price - (totalSellVolume - order.volume) if (coveredBuyAmount < order.volume) { - return order.price; + return order.price } else { - return (totalSellVolume - order.volume) / initialAuctionOrder.volume; + return (totalSellVolume - order.volume) / initialAuctionOrder.volume } } } - if ( - totalSellVolume >= - initialAuctionOrder.volume * initialAuctionOrder.price - ) { - return totalSellVolume / initialAuctionOrder.volume; + if (totalSellVolume >= initialAuctionOrder?.volume * initialAuctionOrder?.price) { + return totalSellVolume / initialAuctionOrder.volume } else { - return initialAuctionOrder.price; + return initialAuctionOrder?.price } } export const processOrderbookData = ({ - data, - userOrder, baseToken, + data, quoteToken, + userOrder, }: ProcessRawDataParams): PricePointDetails[] => { try { - const clearingPrice = findClearingPrice(data.bids, userOrder, data.asks[0]); - const bids = processData( - data.bids, - userOrder, - data.asks[0].price, - data.asks[0].price, - Offer.Bid, - ); + const clearingPrice = findClearingPrice(data.bids, userOrder, data.asks[0]) + const value = data.asks[0]?.price ?? 0 + const bids = processData(data.bids, userOrder, value, value, Offer.Bid) + + const asks = processData(data.asks, null, bids[0].price, value, Offer.Ask) + let pricePoints = bids.concat(asks) - const asks = processData( - data.asks, - null, - bids[0].price, - data.asks[0].price, - Offer.Ask, - ); - let pricePoints = bids.concat(asks); if (clearingPrice) { - const priceInfo = addClearingPriceInfo(clearingPrice, pricePoints); - pricePoints = pricePoints.concat(priceInfo); + const priceInfo = addClearingPriceInfo(clearingPrice, pricePoints) + pricePoints = pricePoints.concat(priceInfo) } - // Sort points by price - pricePoints.sort((lhs, rhs) => lhs.price - rhs.price); - const debug = false; - if (debug) - _printOrderBook(pricePoints, baseToken.symbol, quoteToken.symbol); - return pricePoints; + // Sort points by price + pricePoints.sort((lhs, rhs) => lhs.price - rhs.price) + const debug = false + if (debug) _printOrderBook(pricePoints, baseToken.symbol, quoteToken.symbol) + return pricePoints } catch (error) { - console.error("Error processing data", error); - return []; + console.error('Error processing data', error) + return [] } -}; +} -export interface OrderBookProps extends Omit { - auctionId?: number; +export interface OrderBookProps extends Omit { + auctionId?: number } const OrderBookWidget: React.FC = (props: OrderBookProps) => { - const { baseToken, quoteToken, networkId } = props; - const { - error, - bids, - asks, - userOrderPrice, - userOrderVolume, - } = useOrderbookState(); + const { baseToken, networkId, quoteToken } = props + const { asks, bids, error, userOrderPrice, userOrderVolume } = useOrderbookState() - if (error || !asks || asks.length == 0) - return ; + if (error || !asks || asks.length == 0) return const processedOrderbook = processOrderbookData({ data: { bids, asks }, userOrder: { price: userOrderPrice, volume: userOrderVolume }, baseToken, quoteToken, - }); + }) return ( - ); -}; + ) +} -export default OrderBookWidget; +export default OrderBookWidget diff --git a/src/components/auction/OrdersTable/index.tsx b/src/components/auction/OrdersTable/index.tsx new file mode 100644 index 000000000..5089bfb8b --- /dev/null +++ b/src/components/auction/OrdersTable/index.tsx @@ -0,0 +1,207 @@ +import React, { useCallback, useState } from 'react' +import styled, { css } from 'styled-components' + +import { useCancelOrderCallback } from '../../../hooks/useCancelOrderCallback' +import { useDerivedAuctionInfo } from '../../../state/orderPlacement/hooks' +import { useOrderActionHandlers } from '../../../state/orders/hooks' +import { OrderDisplay, OrderStatus } from '../../../state/orders/reducer' +import ConfirmationModal from '../../ConfirmationModal' +import { Button } from '../../buttons/Button' +import { KeyValue } from '../../common/KeyValue' +import { Tooltip } from '../../common/Tooltip' +import { InfoIcon } from '../../icons/InfoIcon' +import { OrderPending } from '../../icons/OrderPending' +import { OrderPlaced } from '../../icons/OrderPlaced' +import { BaseCard } from '../../pureStyledComponents/BaseCard' +import { EmptyContentText, EmptyContentWrapper } from '../../pureStyledComponents/EmptyContent' +import { PageTitle } from '../../pureStyledComponents/PageTitle' +import CancelModalFooter from '../../swap/CancelOrderModealFooter' +import SwapModalHeader from '../../swap/SwapModalHeader' + +const Wrapper = styled(BaseCard)` + padding: 4px 0; +` + +const SectionTitle = styled(PageTitle)` + margin-bottom: 16px; + margin-top: 0; +` + +const Grid = styled.div` + display: grid; + grid-template-columns: 1fr 1fr 1fr 115px; +` + +const ActionButton = styled(Button)` + border-radius: 6px; + font-size: 14px; + font-weight: 600; + height: 28px; +` + +const CommonCellCSS = css` + border-bottom: 1px solid ${({ theme }) => theme.border}; + padding: 13px 15px; + + &:nth-last-child(-n + 4) { + border-bottom: none; + } +` + +const Cell = styled(KeyValue)` + ${CommonCellCSS} +` + +const ButtonWrapper = styled.div` + ${CommonCellCSS} + display: flex; + align-items: center; + justify-content: flex-end; +` + +const OrderTable: React.FC<{ orders: OrderDisplay[] }> = (props) => { + const { orders } = props + const { biddingToken, orderCancellationEndDate } = useDerivedAuctionInfo() + const cancelOrderCallback = useCancelOrderCallback(biddingToken) + const { onDeleteOrder } = useOrderActionHandlers() + + // modal and loading + const [showConfirm, setShowConfirm] = useState(false) + const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirmed + const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for user confirmation + + // txn values + const [txHash, setTxHash] = useState('') + const [orderId, setOrderId] = useState('') + + // reset modal state when closed + const resetModal = useCallback(() => { + setPendingConfirmation(true) + setAttemptingTxn(false) + }, [setPendingConfirmation, setAttemptingTxn]) + + const onCancelOrder = useCallback(() => { + setAttemptingTxn(true) + + cancelOrderCallback(orderId).then((hash) => { + onDeleteOrder(orderId) + setTxHash(hash) + setPendingConfirmation(false) + }) + }, [ + setAttemptingTxn, + setTxHash, + setPendingConfirmation, + onDeleteOrder, + orderId, + cancelOrderCallback, + ]) + + const modalHeader = () => { + return + } + + const modalBottom = () => { + return ( + + ) + } + + const pendingText = `Canceling Order` + const now = Math.trunc(Date.now() / 1000) + const isOrderCancelationAllowed = now < orderCancellationEndDate + const ordersEmpty = !orders || orders.length == 0 + + return ( + <> + Your Orders + {ordersEmpty && ( + + + You have no orders for this auction. + + )} + {!ordersEmpty && ( + + + {Object.entries(orders).map((order, index) => ( + + + Amount + + + } + itemValue={order[1].sellAmount} + /> + + Limit Price + + + } + itemValue={order[1].price} + /> + Status} + itemValue={ + order[1].status == OrderStatus.PLACED ? ( + <> + Placed + + + ) : ( + <> + Pending + + + ) + } + /> + + { + if (isOrderCancelationAllowed) { + setOrderId(order[1].id) + setShowConfirm(true) + } + }} + > + Cancel + + + + ))} + + { + resetModal() + setShowConfirm(false) + }} + pendingConfirmation={pendingConfirmation} + pendingText={pendingText} + title="Confirm Order Cancellation" + topContent={modalHeader} + /> + + )} + + ) +} + +export default OrderTable diff --git a/src/components/auctions/AuctionInfoCard/index.tsx b/src/components/auctions/AuctionInfoCard/index.tsx new file mode 100644 index 000000000..34bb8772e --- /dev/null +++ b/src/components/auctions/AuctionInfoCard/index.tsx @@ -0,0 +1,274 @@ +import React from 'react' +import styled, { keyframes } from 'styled-components' + +import { HashLink } from 'react-router-hash-link' + +import { AuctionInfo } from '../../../hooks/useAllAuctionInfos' +import { + calculateTimeLeft, + calculateTimeProgress, + getChainName, + getDays, + getHours, + getMinutes, + getSeconds, +} from '../../../utils/tools' +import DoubleLogo from '../../common/DoubleLogo' +import { NetworkIcon } from '../../icons/NetworkIcon' + +const Wrapper = styled(HashLink)` + align-items: center; + border-radius: 12px; + border: 1px solid ${({ theme }) => theme.border}; + display: flex; + flex-flow: column; + justify-content: space-between; + min-height: 290px; + padding: 12px 18px; + text-decoration: none; + transition: all 0.15s linear; + + &:hover { + box-shadow: 10px -10px 24px 0 rgba(0, 34, 73, 0.7); + } +` + +const Top = styled.span` + align-items: center; + display: flex; + justify-content: space-between; + width: 100%; +` + +const Tokens = styled.span` + color: ${({ theme }) => theme.text1}; + font-size: 24px; + font-weight: 700; + line-height: 1.2; + margin: 0; + text-transform: uppercase; +` + +const Badge = styled.span` + align-items: center; + background-color: rgba(232, 102, 61, 0.3); + border-radius: 17px; + color: ${({ theme }) => theme.primary1}; + display: flex; + height: 34px; + padding: 0 18px; +` + +const Blinker = keyframes` + 0% { + opacity: 1; + } + 50% { + opacity: 1; + } + 50.01% { + opacity: 0; + } + 100% { + opacity: 0; + } +` + +const Blink = styled.span` + animation-direction: alternate; + animation-duration: 0.5s; + animation-iteration-count: infinite; + animation-name: ${Blinker}; + animation-timing-function: linear; + + &::before { + content: ':'; + } +` + +const Details = styled.span` + align-items: center; + display: flex; + flex-direction: column; + flex-grow: 1; + flex-shrink: 0; + justify-content: center; + margin: auto 0; + padding: 10px 0; +` + +const TokenIcons = styled(DoubleLogo)`` + +const SellingText = styled.span` + color: ${({ theme }) => theme.text1}; + font-size: 18px; + font-weight: 700; + line-height: 1.2; + margin: 15px 0 0 0; + text-align: center; +` + +const PriceAndDuration = styled.span` + display: flex; + justify-content: space-between; + margin: auto 0 0 0; + width: 100%; +` + +const Cell = styled.span` + display: flex; + flex-direction: column; +` + +const Subtitle = styled.span<{ textAlign?: string }>` + color: ${({ theme }) => theme.text1}; + display: block; + font-size: 13px; + font-weight: normal; + line-height: 1.2; + margin: 0; + opacity: 0.7; + padding: 0 0 5px; + text-align: ${(props) => props.textAlign}; +` + +Subtitle.defaultProps = { + textAlign: 'left', +} + +const Text = styled.span` + color: ${({ theme }) => theme.primary1}; + font-size: 13px; + font-weight: 700; + line-height: 1.2; + margin-top: auto; +` + +const ProgressBar = styled.span` + background-color: rgba(232, 102, 61, 0.15); + border-radius: 5px; + display: block; + height: 5px; + margin-bottom: 3px; + margin-top: auto; + width: 120px; +` + +const Progress = styled.span<{ width: string }>` + background-color: ${({ theme }) => theme.primary1}; + border-radius: 5px; + display: block; + height: 100%; + max-width: 100%; + width: ${(props) => props.width}; +` + +const Network = styled.div` + align-items: center; + display: flex; + justify-content: center; + opacity: 0.7; + padding-top: 4px; +` + +const NetworkName = styled.div` + color: ${({ theme }) => theme.text1}; + font-size: 11px; + font-weight: 600; + margin-left: 5px; +` + +const formatSeconds = (seconds: number): React.ReactNode => { + const days = getDays(seconds) + const hours = getHours(seconds) + const minutes = getMinutes(seconds) + const remainderSeconds = getSeconds(seconds) + + return ( + <> + {days > 0 && <>{`${days}d `}} + <> + {hours >= 0 && hours < 10 && `0`} + {hours} + + <> + + {minutes >= 0 && minutes < 10 && `0`} + {minutes} + + <> + + {remainderSeconds >= 0 && remainderSeconds < 10 && `0`} + {remainderSeconds} + + + ) +} + +interface Props { + auctionInfo: AuctionInfo +} + +const AuctionInfoCard: React.FC = (props) => { + const { auctionInfo, ...restProps } = props + const { chainId, endTimeTimestamp, startingTimestamp } = auctionInfo + const [timeLeft, setTimeLeft] = React.useState(calculateTimeLeft(endTimeTimestamp)) + + setInterval(() => { + setTimeLeft(calculateTimeLeft(endTimeTimestamp)) + }, 1000) + + return ( + + + + {auctionInfo.symbolAuctioningToken}/{auctionInfo.symbolBiddingToken} + + {timeLeft && timeLeft > -1 ? formatSeconds(timeLeft) : '-'} + +
+ + + Selling {auctionInfo.order.volume + ` `} + {auctionInfo.symbolAuctioningToken} + + + + Selling on {getChainName(parseInt(chainId.toString()))} + +
+ + + Current price + + {auctionInfo.order.price.toFixed(2)} {` ` + auctionInfo.symbolBiddingToken} per{' '} + {auctionInfo.symbolAuctioningToken} + + + + Duration + + + + + +
+ ) +} + +export default AuctionInfoCard diff --git a/src/components/auctions/FeaturedAuctions/index.tsx b/src/components/auctions/FeaturedAuctions/index.tsx new file mode 100644 index 000000000..b1fabed21 --- /dev/null +++ b/src/components/auctions/FeaturedAuctions/index.tsx @@ -0,0 +1,59 @@ +import React from 'react' +import styled from 'styled-components' + +import { useInterestingAuctionInfo } from '../../../hooks/useInterestingAuctionDetails' +import { InlineLoading } from '../../common/InlineLoading' +import { SpinnerSize } from '../../common/Spinner' +import { InfoIcon } from '../../icons/InfoIcon' +import { EmptyContentText, EmptyContentWrapper } from '../../pureStyledComponents/EmptyContent' +import { PageTitle } from '../../pureStyledComponents/PageTitle' +import AuctionInfoCard from '../AuctionInfoCard' + +const Wrapper = styled.div` + margin-bottom: 40px; +` + +const Row = styled.div` + column-gap: 40px; + display: grid; + grid-template-columns: 1fr 1fr 1fr; +` + +const SectionTitle = styled(PageTitle)` + margin: 0 0 40px; +` + +export const FeaturedAuctions = () => { + const highlightedAuctions = useInterestingAuctionInfo() + + const auctions = React.useMemo(() => { + const items: React.ReactNodeArray = [] + + if (highlightedAuctions && highlightedAuctions.length > 0) { + const highlightedAuctionsFirstThree = highlightedAuctions.slice(0, 3) + for (const highlightedAuction of highlightedAuctionsFirstThree) { + items.push( + , + ) + } + } + + return items + }, [highlightedAuctions]) + + return ( + + Featured Auctions + {(highlightedAuctions === undefined || highlightedAuctions === null) && ( + + )} + {highlightedAuctions && highlightedAuctions.length === 0 && ( + + + No featured auctions. + + )} + {highlightedAuctions && highlightedAuctions.length > 0 && {auctions}} + + ) +} diff --git a/src/components/buttons/Button/index.tsx b/src/components/buttons/Button/index.tsx new file mode 100644 index 000000000..24d2e7fcd --- /dev/null +++ b/src/components/buttons/Button/index.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import styled from 'styled-components' + +import { ButtonCSS, ButtonProps } from '../buttonStylingTypes' + +const Wrapper = styled.button` + ${ButtonCSS} +` + +export const Button: React.FC = (props: ButtonProps) => { + const { children, ...restProps } = props + + return {children} +} diff --git a/src/components/buttons/ButtonAnchor/index.tsx b/src/components/buttons/ButtonAnchor/index.tsx new file mode 100644 index 000000000..5c6e2d69c --- /dev/null +++ b/src/components/buttons/ButtonAnchor/index.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import styled from 'styled-components' + +import { ButtonCSS, ButtonLinkProps } from '../buttonStylingTypes' + +const Wrapper = styled.a` + ${ButtonCSS} +` + +export const ButtonAnchor: React.FC = (props: ButtonLinkProps) => { + const { children, ...restProps } = props + + return {children} +} diff --git a/src/components/buttons/ButtonConnect/index.tsx b/src/components/buttons/ButtonConnect/index.tsx new file mode 100644 index 000000000..4a9e74180 --- /dev/null +++ b/src/components/buttons/ButtonConnect/index.tsx @@ -0,0 +1,59 @@ +import { darken } from 'polished' +import React, { ButtonHTMLAttributes } from 'react' +import styled from 'styled-components' + +import { ChevronRight } from '../../icons/ChevronRight' + +const Wrapper = styled.button` + align-items: center; + background: transparent; + border: none; + color: ${({ theme }) => theme.primary1}; + cursor: pointer; + display: flex; + font-size: 16px; + font-weight: 400; + height: 100%; + line-height: 1.2; + outline: none; + padding: 0; + + &[disabled] { + cursor: not-allowed; + opacity: 0.5; + } + + .fill { + fill: ${({ theme }) => theme.primary1}; + } + + &:hover { + color: ${({ theme }) => darken(0.15, theme.primary1)}; + + .fill { + fill: ${({ theme }) => darken(0.15, theme.primary1)}; + } + } +` + +const Text = styled.span` + margin-right: 10px; +` + +export const ButtonConnect: React.FC> = (props) => { + const { className, ...restProps } = props + + return ( + { + // eslint-disable-next-line no-console + console.log('connect') + }} + {...restProps} + > + Connect a Wallet + + + ) +} diff --git a/src/components/buttons/ButtonCopy/index.tsx b/src/components/buttons/ButtonCopy/index.tsx new file mode 100644 index 000000000..9fab7eebc --- /dev/null +++ b/src/components/buttons/ButtonCopy/index.tsx @@ -0,0 +1,56 @@ +import React, { ButtonHTMLAttributes } from 'react' +import styled from 'styled-components' + +import { CopyToClipboard } from 'react-copy-to-clipboard' + +import { CopyIcon } from '../../icons/CopyIcon' + +const Wrapper = styled.button` + background: none; + border: none; + cursor: pointer; + height: 14px; + margin: 0; + outline: none; + padding: 0; + width: 14px; + + svg { + .fill { + fill: ${({ theme }) => theme.text1}; + transition: fill 0.1s linear; + } + } + + &:active { + opacity: 0.7; + } + + &:hover { + .fill { + fill: ${({ theme }) => theme.primary1}; + } + } + + &[disabled], + &[disabled]:hover { + cursor: not-allowed; + opacity: 0.5; + } +` + +interface ButtonCopyProps extends ButtonHTMLAttributes { + copyValue: string +} + +export const ButtonCopy: React.FC = (props) => { + const { copyValue, ...restProps } = props + + return ( + + + + + + ) +} diff --git a/src/components/buttons/ButtonMenu/index.tsx b/src/components/buttons/ButtonMenu/index.tsx new file mode 100644 index 000000000..c93b7130e --- /dev/null +++ b/src/components/buttons/ButtonMenu/index.tsx @@ -0,0 +1,36 @@ +import React, { ButtonHTMLAttributes } from 'react' +import styled from 'styled-components' + +import { MenuIcon } from '../../icons/MenuIcon' + +const Wrapper = styled.button` + background: none; + border: none; + cursor: pointer; + height: ${(props) => props.theme.header.height}; + margin: 0 auto 0 0; + outline: none; + padding: 0; + transition: ease-out 0.15s all; + width: 40px; + + &:active { + opacity: 0.5; + } + + &[disabled], + &[disabled]:hover { + cursor: not-allowed; + opacity: 0.5; + } +` + +export const ButtonMenu: React.FC> = (props) => { + const { ...restProps } = props + + return ( + + + + ) +} diff --git a/src/components/buttons/ButtonSelect/index.tsx b/src/components/buttons/ButtonSelect/index.tsx new file mode 100644 index 000000000..85a3ca647 --- /dev/null +++ b/src/components/buttons/ButtonSelect/index.tsx @@ -0,0 +1,47 @@ +import React from 'react' +import styled from 'styled-components' + +import { ChevronDown } from '../../icons/ChevronDown' + +const Wrapper = styled.button` + align-items: center; + background-color: ${(props) => props.theme.textField.backgroundColor}; + border-color: ${(props) => props.theme.textField.borderColor}; + border-radius: ${(props) => props.theme.textField.borderRadius}; + border-style: ${(props) => props.theme.textField.borderStyle}; + border-width: ${(props) => props.theme.textField.borderWidth}; + color: ${(props) => props.theme.textField.color}; + display: flex; + font-size: ${(props) => props.theme.textField.fontSize}; + font-weight: ${(props) => props.theme.textField.fontWeight}; + height: ${(props) => props.theme.textField.height}; + justify-content: space-between; + outline: none; + padding: 0 ${(props) => props.theme.textField.paddingHorizontal}; + transition: border-color 0.15s linear; + width: 100%; + + .isOpen & { + background-color: ${(props) => props.theme.textField.backgroundColorActive}; + border-color: ${(props) => props.theme.textField.borderColorActive}; + } +` + +const Chevron = styled(ChevronDown)` + margin-left: 10px; +` + +interface Props { + content: React.ReactNode | string +} + +export const ButtonSelect: React.FC = (props) => { + const { content, ...restProps } = props + + return ( + + {content} + + + ) +} diff --git a/src/components/buttons/buttonStylingTypes.ts b/src/components/buttons/buttonStylingTypes.ts new file mode 100644 index 000000000..6d3b88efc --- /dev/null +++ b/src/components/buttons/buttonStylingTypes.ts @@ -0,0 +1,97 @@ +import { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react' +import { css } from 'styled-components' + +export enum ButtonType { + primary, + primaryInverted, + danger, +} + +export interface ButtonCommonProps { + buttonType?: ButtonType + theme?: any +} + +export interface ButtonProps extends ButtonHTMLAttributes, ButtonCommonProps {} + +export interface ButtonLinkProps + extends AnchorHTMLAttributes, + ButtonCommonProps {} + +const PrimaryCSS = css` + background-color: ${(props) => props.theme.buttonPrimary.backgroundColor}; + border-color: ${(props) => props.theme.buttonPrimary.borderColor}; + color: ${(props) => props.theme.buttonPrimary.color}; + + &:hover { + background-color: ${(props) => props.theme.buttonPrimary.backgroundColorHover}; + border-color: ${(props) => props.theme.buttonPrimary.borderColorHover}; + color: ${(props) => props.theme.buttonPrimary.colorHover}; + } + + &[disabled], + &[disabled]:hover { + background-color: ${(props) => props.theme.buttonPrimary.borderColor}; + border-color: ${(props) => props.theme.buttonPrimary.borderColor}; + color: ${(props) => props.theme.buttonPrimary.color}; + cursor: not-allowed; + opacity: 0.5; + } +` + +const PrimaryInvertedCSS = css` + background-color: ${(props) => props.theme.buttonPrimaryInverted.backgroundColor}; + border-color: ${(props) => props.theme.buttonPrimaryInverted.borderColor}; + color: ${(props) => props.theme.buttonPrimaryInverted.color}; + + &:hover { + background-color: ${(props) => props.theme.buttonPrimaryInverted.backgroundColorHover}; + border-color: ${(props) => props.theme.buttonPrimaryInverted.borderColorHover}; + color: ${(props) => props.theme.buttonPrimaryInverted.colorHover}; + } + + &[disabled], + &[disabled]:hover { + background-color: ${(props) => props.theme.buttonPrimaryInverted.backgroundColor}; + border-color: ${(props) => props.theme.buttonPrimaryInverted.borderColor}; + color: ${(props) => props.theme.buttonPrimaryInverted.color}; + cursor: not-allowed; + opacity: 0.5; + } +` + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getButtonTypeStyles = (buttonType: ButtonType = ButtonType.primary): any => { + if (buttonType === ButtonType.primary) { + return PrimaryCSS + } + + if (buttonType === ButtonType.primaryInverted) { + return PrimaryInvertedCSS + } + + return PrimaryCSS +} + +export const ButtonCSS = css` + align-items: center; + border-radius: 6px; + border-style: solid; + border-width: 1px; + cursor: pointer; + display: flex; + font-size: 20px; + font-weight: 600; + height: 35px; + justify-content: center; + line-height: 1; + outline: none; + padding: 0 25px; + text-align: center; + text-decoration: none; + transition: all 0.15s ease-out; + user-select: none; + white-space: nowrap; + + ${(props) => getButtonTypeStyles(props.buttonType)} +` diff --git a/src/components/common/CenteredCard/index.tsx b/src/components/common/CenteredCard/index.tsx new file mode 100644 index 000000000..95b41b864 --- /dev/null +++ b/src/components/common/CenteredCard/index.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import styled from 'styled-components' + +import { BaseCard } from '../../pureStyledComponents/BaseCard' + +const Wrapper = styled(BaseCard)` + margin-left: auto; + margin-right: auto; + max-width: 100%; + position: relative; + width: ${(props) => props.theme.layout.commonContainerMaxWidth}; +` + +const DropdownContainer = styled.span` + position: absolute; + right: ${(props) => props.theme.cards.paddingHorizontal}; + top: ${(props) => props.theme.cards.paddingHorizontal}; + z-index: 5; +` + +interface FormCardProps { + children: React.ReactNode + dropdown?: React.ReactNode +} + +export const CenteredCard: React.FC = (props) => { + const { children, dropdown, ...restProps } = props + + return ( + + <> + {dropdown && {dropdown}} + {children} + + + ) +} diff --git a/src/components/common/CookiesBanner/index.tsx b/src/components/common/CookiesBanner/index.tsx new file mode 100644 index 000000000..77bdb1e7b --- /dev/null +++ b/src/components/common/CookiesBanner/index.tsx @@ -0,0 +1,257 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { NavLink, useLocation } from 'react-router-dom' +import styled from 'styled-components' + +import { isMobile } from 'react-device-detect' +import ReactGA from 'react-ga' + +import { GOOGLE_ANALYTICS_ID } from '../../../constants/config' +import { Button } from '../../buttons/Button' +import { CloseIcon } from '../../icons/CloseIcon' +import { Checkbox } from '../../pureStyledComponents/Checkbox' + +const INNER_WIDTH = '840px' + +const Wrapper = styled.div` + background-color: ${({ theme }) => theme.mainBackground}; + bottom: 0; + box-shadow: 0 -20px 24px 0 #002249; + display: flex; + justify-content: center; + left: 0; + min-height: 160px; + padding: 20px; + position: fixed; + width: 100%; + z-index: 123; +` + +const Content = styled.div` + max-width: 100%; + position: relative; + width: ${(props) => props.theme.layout.maxWidth}; +` + +const Text = styled.p` + color: ${({ theme }) => theme.text1}; + font-size: 17px; + font-weight: normal; + line-height: 1.4; + margin: 0 auto 20px; + max-width: 100%; + padding: 0 20px; + position: relative; + text-align: center; + width: ${INNER_WIDTH}; + z-index: 1; +` + +const Link = styled(NavLink)` + color: ${({ theme }) => theme.text1}; + text-decoration: underline; + + &:hover { + text-decoration: none; + } +` + +const ButtonContainer = styled.div` + &.buttonContainer { + align-items: center; + display: flex; + inset: auto; + justify-content: center; + position: relative; + } +` + +const Labels = styled.div` + align-items: center; + display: flex; +` + +const Label = styled.div<{ clickable?: boolean }>` + align-items: center; + color: ${({ theme }) => theme.text1}; + display: flex; + font-size: 17px; + font-weight: normal; + line-height: 1.4; + margin: 0 25px 0 0; + + &:last-child { + margin-right: 80px; + } + + ${(props) => props.clickable && 'cursor: pointer'} +` + +Label.defaultProps = { + clickable: false, +} + +const CheckboxStyled = styled(Checkbox)` + margin: 0 10px 0 0; +` + +const ButtonAccept = styled(Button)` + &.buttonAccept { + bottom: auto; + font-size: 18px; + height: 36px; + left: auto; + max-width: 170px; + position: relative; + right: auto; + top: auto; + } +` + +const ButtonClose = styled.button` + align-items: center; + background-color: transparent; + border: none; + cursor: pointer; + display: flex; + justify-content: center; + outline: none; + padding: 0; + position: absolute; + right: 0; + top: 0; + transition: all 0.15s linear; + z-index: 5; + + &:hover { + opacity: 0.5; + } +` + +const VISIBLE_COOKIES_BANNER = 'VISIBLE_COOKIES_BANNER' +const COOKIES_FALSE = 'false' +const ACCEPT_GOOGLE_ANALYTICS = 'ACCEPT_GOOGLE_ANALYTICS' + +interface Props { + isBannerVisible: boolean + onHide: () => void +} + +export const CookiesBanner: React.FC = (props) => { + const { isBannerVisible, onHide } = props + const storage = window.localStorage + + const isCookiesBannerVisible = useCallback( + () => !(storage.getItem(VISIBLE_COOKIES_BANNER) === COOKIES_FALSE), + [storage], + ) + + const location = useLocation() + const [cookiesWarningVisible, setCookiesWarningVisible] = useState(isCookiesBannerVisible()) + + const showCookiesWarning = useCallback(() => { + setCookiesWarningVisible(true) + storage.setItem(VISIBLE_COOKIES_BANNER, '') + }, [storage]) + + const isGoogleAnalyticsAccepted = useCallback( + () => storage.getItem(ACCEPT_GOOGLE_ANALYTICS) === ACCEPT_GOOGLE_ANALYTICS, + [storage], + ) + + const hideCookiesWarning = useCallback(() => { + setCookiesWarningVisible(false) + storage.setItem(VISIBLE_COOKIES_BANNER, COOKIES_FALSE) + onHide() + if (!isGoogleAnalyticsAccepted()) { + setGoogleAnalyticsAccepted(false) + } + }, [isGoogleAnalyticsAccepted, onHide, storage]) + + const [googleAnalyticsAccepted, setGoogleAnalyticsAccepted] = useState( + isGoogleAnalyticsAccepted(), + ) + + const acceptGoogleAnalytics = useCallback(() => { + setGoogleAnalyticsAccepted(true) + storage.setItem(ACCEPT_GOOGLE_ANALYTICS, ACCEPT_GOOGLE_ANALYTICS) + }, [storage]) + + const rejectGoogleAnalytics = useCallback(() => { + setGoogleAnalyticsAccepted(false) + storage.setItem(ACCEPT_GOOGLE_ANALYTICS, '') + }, [storage]) + + const toggleAcceptGoogleAnalytics = useCallback(() => { + if (googleAnalyticsAccepted) { + rejectGoogleAnalytics() + } else { + setGoogleAnalyticsAccepted(true) + } + }, [googleAnalyticsAccepted, rejectGoogleAnalytics]) + + const acceptAll = useCallback(() => { + acceptGoogleAnalytics() + hideCookiesWarning() + }, [acceptGoogleAnalytics, hideCookiesWarning]) + + const loadGoogleAnalytics = useCallback(() => { + if (!GOOGLE_ANALYTICS_ID) { + console.warn( + 'In order to use Google Analytics you need to add a trackingID using the REACT_APP_GOOGLE_ANALYTICS_ID environment variable.', + ) + return + } + + if (typeof GOOGLE_ANALYTICS_ID === 'string') { + ReactGA.initialize(GOOGLE_ANALYTICS_ID, { gaOptions: { cookieDomain: 'auto' } }) + ReactGA.set({ + customBrowserType: !isMobile + ? 'desktop' + : 'web3' in window || 'ethereum' in window + ? 'mobileWeb3' + : 'mobileRegular', + }) + ReactGA.set({ anonymizeIp: true }) + ReactGA.set({ page: location.pathname }) + ReactGA.pageview(location.pathname) + } + }, [location]) + + useEffect(() => { + if (googleAnalyticsAccepted) { + loadGoogleAnalytics() + } + if (isBannerVisible) { + showCookiesWarning() + } + }, [googleAnalyticsAccepted, isBannerVisible, loadGoogleAnalytics, showCookiesWarning]) + + return cookiesWarningVisible ? ( + + + + We use cookies to give you the best experience and to help improve our website. Please + read our Cookie Policy for more information. By + clicking "Accept All", you agree to the storing of cookies on + your device to enhance site navigation, analyze site usage and provide customer support. + + + + + + + + Accept All + + + + + + + + ) : null +} diff --git a/src/components/common/DoubleLogo/index.tsx b/src/components/common/DoubleLogo/index.tsx new file mode 100644 index 000000000..75e02990d --- /dev/null +++ b/src/components/common/DoubleLogo/index.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import styled from 'styled-components' + +import TokenLogo from '../TokenLogo' + +const TokenWrapper = styled.div` + align-items: center; + display: flex; + flex-direction: row; + position: relative; +` + +const Logo = styled(TokenLogo)` + border: 3px solid #001429; +` + +const HigherLogo = styled(Logo)` + margin-right: calc(-${(props) => props.size} / 3); + z-index: 5; +` + +const CoveredLogo = styled(Logo)` + z-index: 1; +` + +interface DoubleTokenLogoProps { + auctioningToken: { address: string; symbol: string } + biddingToken: { address: string; symbol: string } + size?: string +} + +const DoubleLogo: React.FC = (props) => { + const { auctioningToken, biddingToken, size = '24px', ...restProps } = props + + return ( + + + + + ) +} + +export default DoubleLogo diff --git a/src/components/common/Dropdown/index.tsx b/src/components/common/Dropdown/index.tsx new file mode 100644 index 000000000..425b8876d --- /dev/null +++ b/src/components/common/Dropdown/index.tsx @@ -0,0 +1,263 @@ +import React, { DOMAttributes, createRef, useCallback, useEffect, useState } from 'react' +import styled, { css } from 'styled-components' + +import { BaseCard } from '../../pureStyledComponents/BaseCard' + +export enum DropdownPosition { + center, + left, + right, +} + +export enum DropdownDirection { + downwards, + upwards, +} + +const Wrapper = styled.div<{ isOpen: boolean; disabled: boolean }>` + outline: none; + pointer-events: ${(props) => (props.disabled ? 'none' : 'initial')}; + position: relative; + z-index: ${(props) => (props.isOpen ? '100' : '50')}; + + &[disabled] { + cursor: not-allowed; + opacity: 0.5; + } +` + +const ButtonContainer = styled.div` + background-color: transparent; + border: none; + display: block; + outline: none; + padding: 0; + user-select: none; + width: 100%; +` + +const PositionLeftCSS = css` + left: 0; +` + +const PositionRightCSS = css` + right: 0; +` + +const PositionCenterCSS = css` + left: 50%; + transform: translateX(-50%); +` + +const DirectionDownwardsCSS = css` + top: calc(100% + 10px); +` + +const DirectionUpwardsCSS = css` + bottom: calc(100% + 10px); +` + +const Items = styled(BaseCard)<{ + dropdownDirection?: DropdownDirection + dropdownPosition?: DropdownPosition + fullWidth?: boolean + isOpen: boolean +}>` + background: ${({ theme }) => theme.dropdown.background}; + border-radius: 1px solid ${({ theme }) => theme.dropdown.borderRadius}; + border: 1px solid ${({ theme }) => theme.dropdown.border}; + box-shadow: ${({ theme }) => theme.dropdown.boxShadow}; + display: ${(props) => (props.isOpen ? 'block' : 'none')}; + min-width: 160px; + position: absolute; + white-space: nowrap; + + ${(props) => props.fullWidth && 'width: 100%;'} + ${(props) => (props.dropdownPosition === DropdownPosition.left ? PositionLeftCSS : '')} + ${(props) => (props.dropdownPosition === DropdownPosition.right ? PositionRightCSS : '')} + ${(props) => (props.dropdownPosition === DropdownPosition.center ? PositionCenterCSS : '')} + ${(props) => + props.dropdownDirection === DropdownDirection.downwards ? DirectionDownwardsCSS : ''} + ${(props) => (props.dropdownDirection === DropdownDirection.upwards ? DirectionUpwardsCSS : '')} +` + +Items.defaultProps = { + dropdownDirection: DropdownDirection.downwards, + dropdownPosition: DropdownPosition.left, + fullWidth: false, + isOpen: false, +} + +export interface DropdownItemProps { + disabled?: boolean +} + +export const DropdownItemCSS = css` + align-items: center; + background-color: ${(props) => props.theme.dropdown.item.backgroundColor}; + border-bottom: 1px solid ${(props) => props.theme.dropdown.item.borderColor}; + color: ${(props) => props.theme.dropdown.item.color}; + cursor: pointer; + display: flex; + font-size: 14px; + font-weight: 400; + line-height: 1.4; + min-height: ${(props) => props.theme.dropdown.item.height}; + overflow: hidden; + padding: 10px ${(props) => props.theme.dropdown.item.paddingHorizontal}; + text-decoration: none; + user-select: none; + + &.isActive { + background-color: ${(props) => props.theme.dropdown.item.backgroundColorActive}; + color: ${(props) => props.theme.dropdown.item.colorActive}; + font-weight: 600; + } + + &:first-child { + border-top-left-radius: ${(props) => props.theme.cards.borderRadius}; + border-top-right-radius: ${(props) => props.theme.cards.borderRadius}; + } + + &:last-child { + border-bottom-left-radius: ${(props) => props.theme.cards.borderRadius}; + border-bottom-right-radius: ${(props) => props.theme.cards.borderRadius}; + border-bottom: none; + } + + &:hover { + background-color: ${(props) => props.theme.dropdown.item.backgroundColorHover}; + } + + &:disabled, + &[disabled] { + &, + &:hover { + background-color: ${(props) => props.theme.dropdown.item.backgroundColor}; + cursor: not-allowed; + font-weight: 400; + opacity: 0.5; + pointer-events: none; + } + } +` + +export const DropdownItem = styled.div` + ${DropdownItemCSS} +` + +DropdownItem.defaultProps = { + disabled: false, +} + +interface Props extends DOMAttributes { + activeItemHighlight?: boolean | undefined + className?: string + closeOnClick?: boolean + currentItem?: number | undefined + disabled?: boolean + dropdownButtonContent?: React.ReactNode | string + dropdownDirection?: DropdownDirection | undefined + dropdownPosition?: DropdownPosition | undefined + fullWidth?: boolean + items: Array + triggerClose?: boolean +} + +export const Dropdown: React.FC = (props) => { + const { + activeItemHighlight = true, + className = '', + closeOnClick = true, + currentItem = 0, + disabled = false, + dropdownButtonContent, + dropdownDirection, + dropdownPosition, + fullWidth, + items, + triggerClose, + ...restProps + } = props + const [isOpen, setIsOpen] = useState(false) + const node = createRef() + + const onButtonClick = useCallback( + (e) => { + e.stopPropagation() + if (disabled) return + setIsOpen(!isOpen) + }, + [disabled, isOpen], + ) + + useEffect(() => { + // Note: you can use triggerClose to close the dropdown when clicking on a specific element + if (triggerClose) { + setIsOpen(false) + } + + // Note: This code handles closing when clickin outside of the dropdown + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleClick = (e: any) => { + if (node && node.current && node.current.contains(e.target)) { + return + } + setIsOpen(false) + } + + document.addEventListener('mousedown', handleClick) + + return () => { + document.removeEventListener('mousedown', handleClick) + } + }, [node, triggerClose]) + + return ( + + + {dropdownButtonContent} + + + { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + items.map((item: any, index: number) => { + const isActive = activeItemHighlight && index === currentItem + const dropdownItem = React.cloneElement(item, { + className: `dropdownItem ${isActive && 'isActive'}`, + key: item.key ? item.key : index, + onClick: (e) => { + e.stopPropagation() + + if (closeOnClick) { + setIsOpen(false) + } + + if (!item.props.onClick) { + return + } + + item.props.onClick() + }, + }) + + return dropdownItem + }) + } + + + ) +} diff --git a/src/components/common/InlineLoading/index.tsx b/src/components/common/InlineLoading/index.tsx new file mode 100644 index 000000000..48e9c5d04 --- /dev/null +++ b/src/components/common/InlineLoading/index.tsx @@ -0,0 +1,59 @@ +import React, { HTMLAttributes } from 'react' +import styled, { css } from 'styled-components' + +import { Spinner, SpinnerSize } from '../Spinner' + +const FlexCSS = css` + align-items: center; + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: center; +` + +const AbsoluteCSS = css` + left: 0; + position: absolute; + top: 0; + z-index: 100; +` + +const Wrapper = styled.div<{ absolute?: boolean }>` + height: 100%; + width: 100%; + + ${(props) => (props.absolute ? AbsoluteCSS : '')} + ${(props) => (!props.absolute ? FlexCSS : '')} +` + +Wrapper.defaultProps = { + absolute: false, +} + +const Text = styled.p` + color: ${({ theme }) => theme.text1}; + font-size: 22px; + font-weight: 400; + line-height: 1.4; + margin: 0; + padding: 15px 20px; + text-align: center; + width: 100%; +` + +interface Props extends HTMLAttributes { + absolute?: boolean + message?: string + size?: SpinnerSize +} + +export const InlineLoading: React.FC = (props: Props) => { + const { message, size, ...restProps } = props + + return ( + + + {message ? {message} : null} + + ) +} diff --git a/src/components/common/KeyValue/index.tsx b/src/components/common/KeyValue/index.tsx new file mode 100644 index 000000000..73a368dcd --- /dev/null +++ b/src/components/common/KeyValue/index.tsx @@ -0,0 +1,71 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.div` + display: flex; + flex-direction: column; +` + +const Value = styled.div<{ align: string }>` + align-items: center; + color: ${({ theme }) => theme.text1}; + display: flex; + font-size: 20px; + font-weight: 700; + justify-content: ${(props) => props.align}; + line-height: 1; + margin: 0 0 2px; + + > * { + margin-right: 8px; + + &:last-child { + margin-right: 0; + } + } +` + +const Key = styled.div<{ align: string }>` + align-items: center; + color: ${({ theme }) => theme.text1}; + display: flex; + font-size: 18px; + font-weight: 400; + justify-content: ${(props) => props.align}; + line-height: 1.3; + margin: 0; + text-transform: capitalize; + + > * { + margin-right: 8px; + + &:last-child { + margin-right: 0; + } + } +` + +Key.defaultProps = { + align: 'center', +} + +Value.defaultProps = { + align: 'center', +} + +interface Props { + align?: string + itemKey: React.ReactNode + itemValue: React.ReactNode +} + +export const KeyValue: React.FC = (props) => { + const { align, itemKey, itemValue, ...restProps } = props + + return ( + + {itemValue} + {itemKey} + + ) +} diff --git a/src/components/common/Logo/index.tsx b/src/components/common/Logo/index.tsx new file mode 100644 index 000000000..43acb557b --- /dev/null +++ b/src/components/common/Logo/index.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.span` + align-items: center; + display: flex; +` + +const LogoSVG = styled.svg` + .text { + fill: #fff; + font-family: ${(props) => props.theme.fonts.fontFamily}; + font-size: 24px; + font-weight: 700; + } +` + +export const Logo: React.FC = (props) => { + return ( + + + + + Gnosis Auction + + + + + + ) +} diff --git a/src/components/common/Spinner/index.tsx b/src/components/common/Spinner/index.tsx new file mode 100644 index 000000000..b65aa89f7 --- /dev/null +++ b/src/components/common/Spinner/index.tsx @@ -0,0 +1,50 @@ +import React, { HTMLAttributes } from 'react' +import styled, { keyframes } from 'styled-components' + +import { Spinner as SpinnerSVG } from '../../icons/Spinner' + +const rotate = keyframes` + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +` + +const Wrapper = styled.div<{ size?: SpinnerSize | string | undefined }>` + animation: ${rotate} 2s linear infinite; + flex-grow: 0; + flex-shrink: 0; + height: ${(props) => props.size}; + width: ${(props) => props.size}; + + svg { + height: 100%; + width: 100%; + } +` + +export enum SpinnerSize { + small = '30px', + regular = '50px', + large = '60px', +} + +Wrapper.defaultProps = { + size: SpinnerSize.regular, +} + +interface Props extends HTMLAttributes { + size?: SpinnerSize | string | undefined +} + +export const Spinner: React.FC = (props: Props) => { + const { size, ...restProps } = props + + return ( + + + + ) +} diff --git a/src/components/common/TokenLogo/index.tsx b/src/components/common/TokenLogo/index.tsx new file mode 100644 index 000000000..5249a07ff --- /dev/null +++ b/src/components/common/TokenLogo/index.tsx @@ -0,0 +1,60 @@ +import React from 'react' +import styled from 'styled-components' + +import { useTokenListState } from '../../../state/tokenList/hooks' +import { isAddress } from '../../../utils' + +const Wrapper = styled.div<{ size?: string }>` + background-color: #606467; + border-radius: 50%; + height: ${({ size }) => size}; + overflow: hidden; + position: relative; + top: -1px; + width: ${({ size }) => size}; +` + +const Image = styled.img` + display: block; + height: 100%; + width: 100%; +` + +const Placeholder = styled.div<{ size?: string }>` + align-items: center; + color: #fff; + display: flex; + font-size: calc(${(props) => props.size} * 0.26); + font-weight: bold; + height: 100%; + justify-content: center; + letter-spacing: 0px; + line-height: 1.3; + text-align: center; + width: 100%; + white-space: nowrap; +` + +interface TokenLogoProps { + token: { address: string; symbol?: string } + size?: string +} + +const TokenLogo: React.FC = (props) => { + const { size = '24px', token, ...restProps } = props + const { address, symbol } = token + const { tokens } = useTokenListState() + const validToken = isAddress(address) && tokens && tokens.length > 0 + const tokenInfo = + validToken && tokens.find((token) => token.address.toLowerCase() === address.toLowerCase()) + const imageURL = validToken && tokenInfo && tokenInfo.logoURI ? tokenInfo.logoURI : undefined + const tokenSymbol = tokenInfo && tokenInfo.symbol ? tokenInfo.symbol : symbol + + return ( + + {imageURL ? : {tokenSymbol}} + + ) +} + +export default TokenLogo diff --git a/src/components/common/Tooltip/index.tsx b/src/components/common/Tooltip/index.tsx new file mode 100644 index 000000000..f63aa5fd5 --- /dev/null +++ b/src/components/common/Tooltip/index.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import styled from 'styled-components' + +import ReactTooltip, { TooltipProps } from 'react-tooltip' + +import { TooltipIcon } from '../../icons/TooltipIcon' + +const Wrapper = styled.span` + cursor: pointer; + position: relative; + top: -1px; + + .tooltipIcon { + .fill { + fill: ${({ theme }) => theme.text1}; + transition: fill 0.1s linear; + } + } + + &:hover { + .fill { + fill: ${({ theme }) => theme.primary1}; + } + } +` + +interface Props extends TooltipProps { + className?: string + id: string + text: string +} + +export const Tooltip: React.FC = (props) => { + const { className, delayHide = 50, delayShow = 250, id, text, ...restProps } = props + const tooltipId = `tooltip_${id}` + + return ( + + + + + ) +} diff --git a/src/components/common/UserDropdown/index.tsx b/src/components/common/UserDropdown/index.tsx new file mode 100644 index 000000000..02ebdcbba --- /dev/null +++ b/src/components/common/UserDropdown/index.tsx @@ -0,0 +1,262 @@ +import React from 'react' +import styled from 'styled-components' + +import { useWeb3React } from '@web3-react/core' + +import { useActiveWeb3React } from '../../../hooks' +import { useDarkModeManager } from '../../../state/user/hooks' +import { getChainName, truncateStringInTheMiddle } from '../../../utils/tools' +import { Button } from '../../buttons/Button' +import { ButtonType } from '../../buttons/buttonStylingTypes' +import { Dropdown, DropdownItem, DropdownPosition } from '../../common/Dropdown' +import { Switch } from '../../form/Switch' +import { ChevronDown } from '../../icons/ChevronDown' +import { ChevronRight } from '../../icons/ChevronRight' +import { TransactionsModal } from '../../modals/TransactionsModal' + +const Wrapper = styled(Dropdown)` + align-items: center; + display: flex; + height: 100%; + + .dropdownButton { + height: 100%; + } + + &.isOpen { + .chevronDown { + transform: rotateX(180deg); + } + } +` + +const DropdownButton = styled.div` + align-items: flex-start; + cursor: pointer; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + + .fill { + fill: ${({ theme }) => theme.text1}; + } + + &:hover { + .addressText { + color: ${({ theme }) => theme.text1}; + } + + .chevronDown { + .fill { + fill: ${({ theme }) => theme.text1}; + } + } + } +` + +const Address = styled.div` + align-items: center; + display: flex; + margin-top: 10px; +` + +const AddressText = styled.div` + color: ${({ theme }) => theme.text1}; + font-size: 15px; + font-weight: 400; + line-height: 1.2; + margin-right: 8px; +` + +const Connection = styled.div` + align-items: center; + display: flex; +` + +const ConnectionStatus = styled.div` + background-color: ${({ theme }) => theme.green1}; + border-radius: 8px; + flex-grow: 0; + flex-shrink: 0; + height: 8px; + margin-right: 4px; + width: 8px; +` + +const ConnectionText = styled.div` + color: ${({ theme }) => theme.green1}; + font-size: 9px; + font-weight: 600; + line-height: 1.2; + margin-bottom: -2px; + text-transform: capitalize; +` + +const Content = styled.div` + width: 245px; +` +const DropdownItemStyled = styled(DropdownItem)` + cursor: default; + padding: 0; + + &:hover { + background-color: transparent; + } +` + +const Item = styled.div<{ hasOnClick?: boolean; disabled?: boolean; hide?: boolean }>` + align-items: center; + border-bottom: 1px solid ${({ theme }) => theme.border}; + color: ${({ theme }) => theme.dropdown.item.color}; + cursor: ${(props) => (props.hasOnClick ? 'pointer' : 'default')}; + display: ${(props) => (props.hide ? 'none' : 'flex')}; + font-size: 13px; + justify-content: space-between; + line-height: 1.2; + padding: 12px; + width: 100%; + + &:hover { + background-color: ${(props) => + props.hasOnClick ? props.theme.dropdown.item.backgroundColorHover : 'transparent'}; + } + + ${(props) => props.disabled && 'pointer-events: none;'} +` + +const Title = styled.div` + padding-right: 10px; +` + +const Value = styled.div` + font-weight: 600; + text-transform: capitalize; + position: relative; +` + +const DisconnectButton = styled(Button)` + border-radius: 4px; + font-size: 14px; + height: 28px; + line-height: 1; + width: 100%; +` + +const UserDropdownButton = () => { + const { account } = useWeb3React() + const { chainId } = useActiveWeb3React() + + return ( + +
+ + {account ? truncateStringInTheMiddle(account, 8, 6) : 'Invalid address.'} + + +
+ + + {getChainName(chainId)} + +
+ ) +} + +export const UserDropdown: React.FC = (props) => { + const [transactionsModalVisible, setTransactionsModalVisible] = React.useState(false) + const [darkMode, toggleDarkMode] = useDarkModeManager() + + const { deactivate, library } = useActiveWeb3React() + + const getWalletName = React.useCallback((): string => { + const provider = library.provider + + const isMetaMask = + Object.prototype.hasOwnProperty.call(provider, 'isMetaMask') && provider.isMetaMask + const isWalletConnect = Object.prototype.hasOwnProperty.call(provider, 'wc') + + return isMetaMask ? 'MetaMask' : isWalletConnect ? 'WalletConnect' : 'Unknown' + }, [library]) + + const disconnect = React.useCallback(async () => { + deactivate() + }, [deactivate]) + + const UserDropdownContent = () => { + const items = [ + { + title: 'Wallet', + value: getWalletName(), + }, + { + title: 'Your transactions', + onClick: () => { + setTransactionsModalVisible(true) + }, + value: , + }, + { + disabled: true, + hide: true, + onClick: toggleDarkMode, + title: 'Night mode', + value: , + }, + ] + + return ( + + {items.map((item, index) => { + return ( + + {item.title} + {item.value} + + ) + })} + + { + disconnect() + }} + > + Disconnect + + + + ) + } + + const headerDropdownItems = [ + + + , + ] + + return ( + <> + } + dropdownPosition={DropdownPosition.right} + items={headerDropdownItems} + {...props} + /> + setTransactionsModalVisible(false)} + /> + + ) +} diff --git a/src/components/form/CurrencyInputPanel/index.tsx b/src/components/form/CurrencyInputPanel/index.tsx new file mode 100644 index 000000000..54a487ab9 --- /dev/null +++ b/src/components/form/CurrencyInputPanel/index.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import styled from 'styled-components' +import { Token } from 'uniswap-xdai-sdk' + +import { useActiveWeb3React } from '../../../hooks' +import TokenLogo from '../../common/TokenLogo' +import { ControlButton, FormLabel } from '../../form/FormLabel' +import { Input as NumericalInput } from '../../form/NumericalInput' +import { FormRow } from '../../pureStyledComponents/FormRow' +import { TextfieldCSS } from '../../pureStyledComponents/Textfield' + +const TextfieldWrapper = styled.div` + ${TextfieldCSS} + align-items: center; + display: flex; + justify-content: space-between; +` + +const TokenInfo = styled.div` + align-items: center; + display: flex; + flex-shrink: 0; + justify-content: space-between; + margin-left: 15px; +` + +const TokenSymbol = styled.div` + color: ${({ theme }) => theme.text1}; + font-size: 18px; + font-stretch: 400; + font-weight: 600; + margin-right: 8px; + text-align: right; +` + +interface CurrencyInputPanelProps { + onMax?: () => void + onUserSellAmountInput: (val: string) => void + token: Maybe + value: string +} + +export default function CurrencyInputPanel({ + onMax, + onUserSellAmountInput, + token = null, + value, +}: CurrencyInputPanelProps) { + const { account } = useActiveWeb3React() + + return ( + + Max} + text={'Amount'} + /> + + { + onUserSellAmountInput(val) + }} + value={value} + /> + + {token && token.symbol && {token.symbol}}{' '} + {token && token.address && ( + + )} + + + + ) +} diff --git a/src/components/form/FormLabel/index.tsx b/src/components/form/FormLabel/index.tsx new file mode 100644 index 000000000..2c34d1df2 --- /dev/null +++ b/src/components/form/FormLabel/index.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import styled, { css } from 'styled-components' + +const Wrapper = styled.div` + align-items: center; + display: flex; + justify-content: space-between; + margin: 0 0 10px; +` + +export const Label = styled.label` + color: ${({ theme }) => theme.text1}; + font-size: 18px; + font-weight: 400; + line-height: 1.2; + margin: 0; + text-align: left; +` + +const ControlCSS = css` + background: none; + border: none; + color: ${({ theme }) => theme.primary1}; + cursor: pointer; + font-size: 17px; + font-weight: 400; + line-height: 1.2; + margin: 0; + outline: none; + padding: 0; + text-align: right; + text-decoration: underline; + + &:hover { + text-decoration: none; + } +` + +export const ControlA = styled.a` + ${ControlCSS} +` + +export const ControlSpan = styled.span` + ${ControlCSS} +` + +export const ControlButton = styled.button` + ${ControlCSS} +` + +interface Props { + extraControls?: React.ReactNode + text: string +} + +export const FormLabel: React.FC = (props) => { + const { extraControls, text, ...restProps } = props + + return ( + + + {extraControls && extraControls} + + ) +} diff --git a/src/components/form/NumericalInput/index.tsx b/src/components/form/NumericalInput/index.tsx new file mode 100644 index 000000000..27bc8fb02 --- /dev/null +++ b/src/components/form/NumericalInput/index.tsx @@ -0,0 +1,67 @@ +import React from 'react' +import styled from 'styled-components' + +import { escapeRegExp } from '../../../utils' +import { TexfieldPartsCSS } from '../../pureStyledComponents/Textfield' + +const StyledInput = styled.input<{ error?: boolean }>` + background-color: ${({ theme }) => theme.textField.backgroundColor}; + border: none; + border-radius: 0; + color: ${({ theme }) => (props) => + props.error ? theme.textField.errorColor : theme.textField.color}; + font-size: ${({ theme }) => theme.textField.fontSize}; + font-weight: ${({ theme }) => theme.textField.fontWeight}; + height: 100%; + outline: none; + padding: 0; + width: 100%; + + ${TexfieldPartsCSS} +` + +const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group + +export const Input = React.memo(function InnerInput({ + onUserSellAmountInput, + placeholder, + value, + ...rest +}: { + value: string | number + onUserSellAmountInput: (string) => void + error?: boolean + fontSize?: string + align?: 'right' | 'left' +} & Omit, 'ref' | 'onChange' | 'as'>) { + const enforcer = (nextUserInput: string) => { + if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) { + onUserSellAmountInput(nextUserInput) + } + } + + return ( + { + // replace commas with periods, because uniswap exclusively uses period as the decimal separator + enforcer(event.target.value.replace(/,/g, '.')) + }} + // text-specific options + pattern="^[0-9]*[.,]?[0-9]*$" + placeholder={placeholder || '0.0'} + spellCheck="false" + title="Token Amount" + type="text" + value={value} + {...rest} + /> + ) +}) + +export default Input diff --git a/src/components/form/PriceInputPanel/index.tsx b/src/components/form/PriceInputPanel/index.tsx new file mode 100644 index 000000000..ef9544eed --- /dev/null +++ b/src/components/form/PriceInputPanel/index.tsx @@ -0,0 +1,70 @@ +import React from 'react' +import styled from 'styled-components' +import { Token } from 'uniswap-xdai-sdk' + +import DoubleLogo from '../../common/DoubleLogo' +import { FormLabel } from '../../form/FormLabel' +import { Input as NumericalInput } from '../../form/NumericalInput' +import { FormRow } from '../../pureStyledComponents/FormRow' +import { TextfieldCSS } from '../../pureStyledComponents/Textfield' + +const TextfieldWrapper = styled.div` + ${TextfieldCSS} + align-items: center; + display: flex; + justify-content: space-between; +` + +const TokenInfo = styled.div` + align-items: center; + display: flex; + flex-shrink: 0; + margin-left: 15px; +` + +interface CurrencyInputPanelProps { + auctioningToken: Maybe + biddingToken: Maybe + label: string + onUserPriceInput: (val: string) => void + value: string +} + +export default function PriceInputPanel({ + auctioningToken = null, + biddingToken = null, + label, + onUserPriceInput, + value, +}: CurrencyInputPanelProps) { + return ( + + + + { + onUserPriceInput(val) + }} + value={value} + /> + + {auctioningToken && biddingToken ? ( + + ) : ( + '-' + )} + + + + ) +} diff --git a/src/components/form/Switch/index.tsx b/src/components/form/Switch/index.tsx new file mode 100644 index 000000000..c9a156f13 --- /dev/null +++ b/src/components/form/Switch/index.tsx @@ -0,0 +1,74 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.div<{ disabled?: boolean }>` + align-items: center; + cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')}; + display: flex; + justify-content: center; + opacity: ${(props) => (props.disabled ? '0.5' : '1')}; + pointer-events: ${(props) => (props.disabled ? 'none' : 'all')}; +` + +const SwitchWrapper = styled.div<{ active: boolean; small?: boolean }>` + background-color: ${(props) => (props.active ? props.theme.primary1 : '#ccc')}; + border-radius: ${(props) => (props.small ? '15px' : '20px')}; + cursor: pointer; + height: ${(props) => (props.small ? '16px' : '20px')}; + position: relative; + transition: all 0.1s linear; + width: ${(props) => (props.small ? '26px' : '36px')}; +` + +SwitchWrapper.defaultProps = { + small: false, +} + +const Circle = styled.div<{ active: boolean; small?: boolean }>` + background-color: #fff; + border-radius: 50%; + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.16); + cursor: pointer; + height: ${(props) => (props.small ? '11px' : '16px')}; + position: absolute; + top: 2px; + transition: all 0.1s linear; + width: ${(props) => (props.small ? '11px' : '16px')}; + + ${(props) => props.small && `left: ${props.active ? '13px' : '2px'};`} + ${(props) => !props.small && `left: ${props.active ? '18px' : '2px'};`} +` + +Circle.defaultProps = { + small: false, +} + +const Label = styled.span` + color: ${({ theme }) => theme.text1}; + font-size: 15px; + font-weight: 600; + line-height: 1.2; + margin-left: 6px; + text-align: left; +` + +interface Props { + active: boolean + disabled?: boolean + label?: React.ReactNode + onClick?: () => void + small?: boolean +} + +export const Switch: React.FC = (props) => { + const { active = false, disabled, label, onClick, small, ...restProps } = props + + return ( + + + + + {label && } + + ) +} diff --git a/src/components/icons/ChevronDown.tsx b/src/components/icons/ChevronDown.tsx new file mode 100644 index 000000000..180d9e006 --- /dev/null +++ b/src/components/icons/ChevronDown.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + display: block; + max-height: 100%; + max-width: 100%; + + .fill { + fill: ${({ theme }) => theme.text1}; + } +` + +export const ChevronDown: React.FC<{ className?: string }> = (props) => ( + + + +) diff --git a/src/components/icons/ChevronRight.tsx b/src/components/icons/ChevronRight.tsx new file mode 100644 index 000000000..799870669 --- /dev/null +++ b/src/components/icons/ChevronRight.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + display: block; + max-height: 100%; + max-width: 100%; + + .fill { + fill: ${({ theme }) => theme.text1}; + } +` + +export const ChevronRight: React.FC<{ className?: string }> = (props) => ( + + + +) diff --git a/src/components/icons/ClearSearch.tsx b/src/components/icons/ClearSearch.tsx new file mode 100644 index 000000000..7f00903b5 --- /dev/null +++ b/src/components/icons/ClearSearch.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + .fill { + fill: #ccc; + } +` + +export const ClearSearch: React.FC<{ className?: string }> = (props) => ( + + + +) diff --git a/src/components/icons/CloseIcon.tsx b/src/components/icons/CloseIcon.tsx new file mode 100755 index 000000000..bc0807eb6 --- /dev/null +++ b/src/components/icons/CloseIcon.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + .fill { + fill: #ccc; + } +` + +export const CloseIcon: React.FC<{ className?: string }> = (props) => ( + + + + +) diff --git a/src/components/icons/CopyIcon.tsx b/src/components/icons/CopyIcon.tsx new file mode 100644 index 000000000..318ef4710 --- /dev/null +++ b/src/components/icons/CopyIcon.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + display: block; + max-height: 100%; + max-width: 100%; + + .fill { + fill: ${({ theme }) => theme.text1}; + } +` + +export const CopyIcon: React.FC<{ className?: string }> = (props) => ( + + + +) diff --git a/src/components/icons/ErrorIcon.tsx b/src/components/icons/ErrorIcon.tsx new file mode 100644 index 000000000..748deef9a --- /dev/null +++ b/src/components/icons/ErrorIcon.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + flex-shrink: 0; + + .fill { + fill: #ccc; + } +` + +export const ErrorIcon: React.FC = (props) => { + const { ...restProps } = props + + return ( + + + + + + + + + ) +} diff --git a/src/components/icons/ErrorInfo.tsx b/src/components/icons/ErrorInfo.tsx new file mode 100644 index 000000000..a6ac7a820 --- /dev/null +++ b/src/components/icons/ErrorInfo.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + display: block; + flex-shrink: 0; + max-height: 100%; + max-width: 100%; + + .fill { + fill: ${({ theme }) => theme.error}; + } +` + +export const ErrorInfo: React.FC<{ className?: string }> = (props) => ( + + + + +) diff --git a/src/components/icons/ErrorLock.tsx b/src/components/icons/ErrorLock.tsx new file mode 100644 index 000000000..5965b1210 --- /dev/null +++ b/src/components/icons/ErrorLock.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + display: block; + flex-shrink: 0; + max-height: 100%; + max-width: 100%; + + .fill { + fill: ${({ theme }) => theme.error}; + } +` + +export const ErrorLock: React.FC<{ className?: string }> = (props) => ( + + + + +) diff --git a/src/components/icons/ExternalLinkIcon.tsx b/src/components/icons/ExternalLinkIcon.tsx new file mode 100644 index 000000000..d39694a07 --- /dev/null +++ b/src/components/icons/ExternalLinkIcon.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + display: block; + max-height: 100%; + max-width: 100%; + + .fill { + fill: ${({ theme }) => theme.text1}; + } +` + +export const ExternalLinkIcon: React.FC<{ className?: string }> = (props) => ( + + + + +) diff --git a/src/components/icons/IconDelete.tsx b/src/components/icons/IconDelete.tsx new file mode 100644 index 000000000..152366774 --- /dev/null +++ b/src/components/icons/IconDelete.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + .fill { + fill: #ccc; + } +` + +export const IconDelete: React.FC<{ className?: string }> = (props) => ( + + + + +) diff --git a/src/components/icons/IconOk.tsx b/src/components/icons/IconOk.tsx new file mode 100644 index 000000000..588a12ca8 --- /dev/null +++ b/src/components/icons/IconOk.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + .fill { + fill: #ccc; + } +` + +export const IconOk: React.FC<{ className?: string }> = (props) => ( + + + + + +) diff --git a/src/components/icons/InfoIcon.tsx b/src/components/icons/InfoIcon.tsx new file mode 100644 index 000000000..a96d01bc6 --- /dev/null +++ b/src/components/icons/InfoIcon.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + display: block; + max-height: 100%; + max-width: 100%; + + .fill { + fill: ${({ theme }) => theme.text1}; + } +` + +export const InfoIcon: React.FC<{ className?: string }> = (props) => ( + + + + + +) diff --git a/src/components/icons/LockBig.tsx b/src/components/icons/LockBig.tsx new file mode 100644 index 000000000..0c5378db3 --- /dev/null +++ b/src/components/icons/LockBig.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + display: block; + max-height: 100%; + max-width: 100%; + + .fill { + fill: ${({ theme }) => theme.text1}; + } +` + +export const LockBig: React.FC<{ className?: string }> = (props) => ( + + + + +) diff --git a/src/components/icons/Magnifier.tsx b/src/components/icons/Magnifier.tsx new file mode 100644 index 000000000..b7deab9cb --- /dev/null +++ b/src/components/icons/Magnifier.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + .fill { + fill: #ccc; + } +` + +export const Magnifier: React.FC<{ className?: string }> = (props) => ( + + + + +) diff --git a/src/components/icons/MenuIcon.tsx b/src/components/icons/MenuIcon.tsx new file mode 100644 index 000000000..11c30de41 --- /dev/null +++ b/src/components/icons/MenuIcon.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + .fill { + fill: #ccc; + } +` + +export const MenuIcon: React.FC<{ className?: string }> = (props) => ( + + + +) diff --git a/src/components/icons/NetworkIcon.tsx b/src/components/icons/NetworkIcon.tsx new file mode 100644 index 000000000..4ac2c8748 --- /dev/null +++ b/src/components/icons/NetworkIcon.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + display: block; + max-height: 100%; + max-width: 100%; + + .fill { + fill: ${({ theme }) => theme.text1}; + } +` + +export const NetworkIcon: React.FC<{ className?: string }> = (props) => ( + + + +) diff --git a/src/components/icons/OrderPending.tsx b/src/components/icons/OrderPending.tsx new file mode 100644 index 000000000..04b7df43e --- /dev/null +++ b/src/components/icons/OrderPending.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + display: block; + max-height: 100%; + max-width: 100%; + + .fill { + fill: ${({ theme }) => theme.text1}; + } +` + +export const OrderPending: React.FC<{ className?: string }> = (props) => ( + + + + +) diff --git a/src/components/icons/OrderPlaced.tsx b/src/components/icons/OrderPlaced.tsx new file mode 100644 index 000000000..fcbcd79f0 --- /dev/null +++ b/src/components/icons/OrderPlaced.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + display: block; + max-height: 100%; + max-width: 100%; + + .fill { + fill: #008c73; + } +` + +export const OrderPlaced: React.FC<{ className?: string }> = (props) => ( + + + +) diff --git a/src/components/icons/Send.tsx b/src/components/icons/Send.tsx new file mode 100755 index 000000000..689a88089 --- /dev/null +++ b/src/components/icons/Send.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + .fill { + fill: ${({ theme }) => theme.buttonPrimary.color}; + } +` + +export const Send: React.FC<{ className?: string }> = (props) => ( + + + +) diff --git a/src/components/icons/SendIcon.tsx b/src/components/icons/SendIcon.tsx new file mode 100644 index 000000000..96e160730 --- /dev/null +++ b/src/components/icons/SendIcon.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + .fill { + fill: #fff; + } +` + +export const SendIcon: React.FC<{ className?: string }> = (props) => ( + + + +) diff --git a/src/components/icons/SettingsIcon.tsx b/src/components/icons/SettingsIcon.tsx new file mode 100644 index 000000000..03e7f8289 --- /dev/null +++ b/src/components/icons/SettingsIcon.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + .fill { + fill: #ccc; + } +` + +export const SettingsIcon: React.FC<{ className?: string }> = (props) => ( + + + +) diff --git a/src/components/icons/Spinner.tsx b/src/components/icons/Spinner.tsx new file mode 100644 index 000000000..d156760d1 --- /dev/null +++ b/src/components/icons/Spinner.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + display: block; +` + +export const Spinner: React.FC = (props) => { + return ( + + + + + ) +} diff --git a/src/components/icons/TooltipIcon.tsx b/src/components/icons/TooltipIcon.tsx new file mode 100644 index 000000000..f850b6bed --- /dev/null +++ b/src/components/icons/TooltipIcon.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + display: block; + max-height: 100%; + max-width: 100%; + + .fill { + fill: ${({ theme }) => theme.text1}; + } +` + +export const TooltipIcon: React.FC<{ className?: string }> = (props) => ( + + + + +) diff --git a/src/components/icons/WarningIcon.tsx b/src/components/icons/WarningIcon.tsx new file mode 100644 index 000000000..17e57b2fc --- /dev/null +++ b/src/components/icons/WarningIcon.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import styled from 'styled-components' + +const Wrapper = styled.svg` + .fill { + fill: #ccc; + } +` + +export const WarningIcon: React.FC = (props) => { + const { ...restProps } = props + + return ( + + + + + + + + ) +} diff --git a/src/components/layout/Footer/index.tsx b/src/components/layout/Footer/index.tsx new file mode 100644 index 000000000..aff71a36d --- /dev/null +++ b/src/components/layout/Footer/index.tsx @@ -0,0 +1,128 @@ +import React from 'react' +import { NavLink } from 'react-router-dom' +import styled, { css } from 'styled-components' + +import { SettingsIcon } from '../../icons/SettingsIcon' +import { InnerContainer } from '../../pureStyledComponents/InnerContainer' + +const Wrapper = styled.footer` + border-top: solid 1px #002249; + display: flex; + height: auto; + justify-content: center; + margin-top: auto; + overflow: visible; + padding: 25px 0; + width: 100%; +` + +const Inner = styled(InnerContainer)` + align-items: center; + flex-flow: row; + flex-grow: 1; + flex-shrink: 0; + justify-content: center; + list-style: none; + margin: 0; + padding-bottom: 0; + padding-left: ${(props) => props.theme.layout.horizontalPadding}; + padding-right: ${(props) => props.theme.layout.horizontalPadding}; + padding-top: 0; + + @media (min-width: ${(props) => props.theme.themeBreakPoints.mdPre}) { + flex-direction: row; + justify-content: center; + } +` + +const Item = styled.li` + color: ${({ theme }) => theme.text1}; + margin-right: 30px; + opacity: 0.8; + + &:hover { + opacity: 1; + } + + &:last-child { + margin-right: 0; + } +` + +const LinkCSS = css` + color: ${({ theme }) => theme.text1}; + text-decoration: none; + + &:hover { + color: ${({ theme }) => theme.primary2}; + } +` + +const ExternalLink = styled.a` + ${LinkCSS} +` + +const Link = styled(NavLink)` + ${LinkCSS} +` + +const IconWrapper = styled.span` + cursor: pointer; + display: inline-block; + height: 14px; + margin-left: 6px; + position: relative; + width: 14px; +` + +const SettingsIconStyled = styled(SettingsIcon)` + fill: ${({ theme }) => theme.text1}; + height: 11px; + width: 11px; + + &:hover { + .fill { + fill: ${({ theme }) => theme.primary2}; + } + } +` + +interface Props { + onCookiesBannerShow: () => void +} + +export const Footer: React.FC = (props) => { + const { onCookiesBannerShow, ...restProps } = props + const date = new Date() + const year = date.getFullYear() + + return ( + + + + + {`©${year} Gnosis`} + + + + Terms + + + Privacy + + + Cookies + + + + + + Licenses + + + Imprint + + + + ) +} diff --git a/src/components/layout/Header/index.tsx b/src/components/layout/Header/index.tsx new file mode 100644 index 000000000..2eeda736a --- /dev/null +++ b/src/components/layout/Header/index.tsx @@ -0,0 +1,166 @@ +import React, { useState } from 'react' +import styled from 'styled-components' + +import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core' +import { HashLink } from 'react-router-hash-link' + +import useENSName from '../../../hooks/useENSName' +import { useWalletModalToggle } from '../../../state/application/hooks' +import { useAllTransactions } from '../../../state/transactions/hooks' +import { TransactionDetails } from '../../../state/transactions/reducer' +import { useNetworkCheck } from '../../Web3Status' +import { ButtonConnect } from '../../buttons/ButtonConnect' +import { ButtonMenu } from '../../buttons/ButtonMenu' +import { Logo } from '../../common/Logo' +import { UserDropdown } from '../../common/UserDropdown' +import WalletModal from '../../modals/WalletModal' +import { Mainmenu } from '../../navigation/Mainmenu' +import { Mobilemenu } from '../../navigation/Mobilemenu' +import { InnerContainer } from '../../pureStyledComponents/InnerContainer' + +const Wrapper = styled.header` + background-color: ${({ theme }) => theme.mainBackground}; + display: flex; + flex-shrink: 0; + position: relative; + z-index: 100; +` + +const Inner = styled(InnerContainer)` + align-items: center; + flex-flow: row; + flex-grow: 1; + flex-shrink: 0; + height: ${(props) => props.theme.header.height}; + justify-content: space-between; + padding-left: ${(props) => props.theme.layout.horizontalPadding}; + padding-right: ${(props) => props.theme.layout.horizontalPadding}; +` + +const LogoLink = styled(HashLink)`` + +const ButtonMenuStyled = styled(ButtonMenu)` + display: block; + position: relative; + z-index: 5; + + @media (min-width: ${(props) => props.theme.themeBreakPoints.md}) { + display: none; + } +` + +const ButtonConnectStyled = styled(ButtonConnect)` + margin-left: auto; + position: relative; + z-index: 5; + + @media (min-width: ${(props) => props.theme.themeBreakPoints.md}) { + margin-left: 0; + } +` + +const UserDropdownStyled = styled(UserDropdown)` + margin-left: auto; + position: relative; + z-index: 5; + + @media (min-width: ${(props) => props.theme.themeBreakPoints.md}) { + margin-left: 0; + } +` + +const Menu = styled(Mainmenu)` + display: none; + + @media (min-width: ${(props) => props.theme.themeBreakPoints.md}) { + display: flex; + margin-left: auto; + } +` + +const MobilemenuStyled = styled(Mobilemenu)` + display: block; + height: calc(100vh - ${(props) => props.theme.header.height}); + left: 0; + position: fixed; + top: ${(props) => props.theme.header.height}; + width: 100%; + z-index: 12345; + + @media (min-width: ${(props) => props.theme.themeBreakPoints.md}) { + display: none; + } +` + +const Error = styled.span` + align-items: center; + color: ${({ theme }) => theme.primary1}; + display: flex; + font-size: 16px; + font-weight: 600; + height: 100%; + line-height: 1.2; + margin-left: auto; +` + +export const Header: React.FC = (props) => { + const { account, error } = useWeb3React() + const { errorWrongNetwork } = useNetworkCheck() + const isConnected = !!account + const [mobileMenuVisible, setMobileMenuVisible] = useState(false) + const wrongNetwork = error instanceof UnsupportedChainIdError || errorWrongNetwork !== undefined + + const toggleWalletModal = useWalletModalToggle() + const allTransactions = useAllTransactions() + const ENSName = useENSName(account) + + const recentTransactionsOnly = (a: TransactionDetails) => { + return new Date().getTime() - a.addedTime < 86_400_000 + } + + const newTransactionFirst = (a: TransactionDetails, b: TransactionDetails) => { + return b.addedTime - a.addedTime + } + + const sortedRecentTransactions = React.useMemo(() => { + const txs = Object.values(allTransactions) + return txs.filter(recentTransactionsOnly).sort(newTransactionFirst) + }, [allTransactions]) + + const confirmed = sortedRecentTransactions.filter((tx) => tx.receipt).map((tx) => tx.hash) + const pending = sortedRecentTransactions.filter((tx) => !tx.receipt).map((tx) => tx.hash) + + const mobileMenuToggle = () => { + setMobileMenuVisible(!mobileMenuVisible) + } + + const web3Status = isConnected ? ( + + ) : wrongNetwork ? ( + Invalid network + ) : ( + + ) + + return ( + <> + + + + {mobileMenuVisible && setMobileMenuVisible(false)} />} + + + + + {web3Status} + + + + + ) +} diff --git a/src/components/MesaModal/index.tsx b/src/components/modals/MesaModal/index.tsx similarity index 95% rename from src/components/MesaModal/index.tsx rename to src/components/modals/MesaModal/index.tsx index a1f69fa54..b9e416a17 100644 --- a/src/components/MesaModal/index.tsx +++ b/src/components/modals/MesaModal/index.tsx @@ -1,15 +1,16 @@ // file copied from gnosis/dex-react -import React from "react"; -import { createGlobalStyle } from "styled-components"; +import React from 'react' +import { createGlobalStyle } from 'styled-components' + import Modali, { ModalHook, ModalOptions, ModalProps, toggleModaliComponent, useModali, -} from "modali"; +} from 'modali' -const MODALI_OVERLAY_COLOUR = "#2f3e4e80"; +const MODALI_OVERLAY_COLOUR = '#2f3e4e80' const ModaliGlobalStyle = createGlobalStyle` /* Hack to fix Modali screen flash */ @@ -155,17 +156,17 @@ const ModaliGlobalStyle = createGlobalStyle` body.modali-open .modali-body-style { padding: 0; } -`; +` const Modal: React.FC = (props) => ( <> -); +) // To import default Modali and not change much code elsewhere -const StyledModali = { ...Modali, Modal }; +const StyledModali = { ...Modali, Modal } export { StyledModali as default, @@ -174,4 +175,4 @@ export { ModalProps, toggleModaliComponent as toggleModal, useModali as useModal, -}; +} diff --git a/src/components/Modal/index.tsx b/src/components/modals/Modal/index.tsx similarity index 76% rename from src/components/Modal/index.tsx rename to src/components/modals/Modal/index.tsx index 8686c31c1..e87b0d2e3 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/modals/Modal/index.tsx @@ -1,19 +1,19 @@ -import React from "react"; -import styled, { css } from "styled-components"; -import { animated, useTransition, useSpring } from "react-spring"; -import { Spring } from "react-spring/renderprops"; +import { transparentize } from 'polished' +import React from 'react' +import styled, { css } from 'styled-components' -import { DialogOverlay, DialogContent } from "@reach/dialog"; -import { isMobile } from "react-device-detect"; -import "@reach/dialog/styles.css"; -import { transparentize } from "polished"; -import { useGesture } from "react-use-gesture"; +import { DialogContent, DialogOverlay } from '@reach/dialog' +import { isMobile } from 'react-device-detect' +import { animated, useSpring, useTransition } from 'react-spring' +import { Spring } from 'react-spring/renderprops' +import '@reach/dialog/styles.css' +import { useGesture } from 'react-use-gesture' -const AnimatedDialogOverlay = animated(DialogOverlay); +const AnimatedDialogOverlay = animated(DialogOverlay) // eslint-disable-next-line @typescript-eslint/no-unused-vars -const StyledDialogOverlay = styled(({ mobile, ...rest }) => ( - -))<{ mobile: boolean }>` +const StyledDialogOverlay = styled(({ mobile, ...rest }) => )<{ + mobile: boolean +}>` &[data-reach-dialog-overlay] { z-index: 2; display: flex; @@ -28,7 +28,7 @@ const StyledDialogOverlay = styled(({ mobile, ...rest }) => ( `} &::after { - content: ""; + content: ''; background-color: ${({ theme }) => theme.modalBG}; opacity: 0.5; top: 0; @@ -39,12 +39,12 @@ const StyledDialogOverlay = styled(({ mobile, ...rest }) => ( z-index: -1; } } -`; +` // destructure to not pass custom props to Dialog DOM element const StyledDialogContent = styled( // eslint-disable-next-line @typescript-eslint/no-unused-vars - ({ minHeight, maxHeight, mobile, isOpen, ...rest }) => ( + ({ isOpen, maxHeight, minHeight, mobile, ...rest }) => ( ), )` @@ -52,8 +52,7 @@ const StyledDialogContent = styled( margin: 0 0 2rem 0; border: 1px solid ${({ theme }) => theme.bg1}; background-color: ${({ theme }) => theme.bg1}; - box-shadow: 0 4px 8px 0 - ${({ theme }) => transparentize(0.95, theme.shadow1)}; + box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.shadow1)}; padding: 0px; width: 50vw; @@ -75,7 +74,7 @@ const StyledDialogContent = styled( max-height: 65vh; margin: 0; `} - ${({ theme, mobile }) => theme.mediaWidth.upToSmall` + ${({ mobile, theme }) => theme.mediaWidth.upToSmall` width: 85vw; max-height: 66vh; ${ @@ -89,7 +88,7 @@ const StyledDialogContent = styled( } `} } -`; +` const HiddenCloseButton = styled.button` margin: 0; @@ -97,13 +96,13 @@ const HiddenCloseButton = styled.button` width: 0; height: 0; border: none; -`; +` export const DEFAULT_MODAL_OPTIONS = { centered: true, animated: true, closeButton: true, -}; +} export const ModalBodyWrapper = styled.div` padding: 0.5rem 1.5rem; @@ -113,50 +112,50 @@ export const ModalBodyWrapper = styled.div` color: inherit; padding: 0; } -`; +` interface ModalProps { - isOpen: boolean; - onDismiss: () => void; - minHeight?: number | false; - maxHeight?: number; - initialFocusRef?: React.RefObject; - children?: React.ReactNode; + isOpen: boolean + onDismiss: () => void + minHeight?: number | false + maxHeight?: number + initialFocusRef?: React.RefObject + children?: React.ReactNode } export default function Modal({ + children, + initialFocusRef = null, isOpen, - onDismiss, - minHeight = false, maxHeight = 50, - initialFocusRef = null, - children, + minHeight = false, + onDismiss, }: ModalProps) { const transitions = useTransition(isOpen, null, { config: { duration: 200 }, from: { opacity: 0 }, enter: { opacity: 1 }, leave: { opacity: 0 }, - }); + }) - const [{ xy }, set] = useSpring(() => ({ xy: [0, 0] })); + const [{ xy }, set] = useSpring(() => ({ xy: [0, 0] })) const bind = useGesture({ onDrag: (state) => { - let velocity = state.velocity; + let velocity = state.velocity if (velocity < 1) { - velocity = 1; + velocity = 1 } if (velocity > 8) { - velocity = 8; + velocity = 8 } set({ xy: state.down ? state.movement : [0, 0], config: { mass: 1, tension: 210, friction: 20 }, - }); + }) if (velocity > 3 && state.direction[1] > 0) { - onDismiss(); + onDismiss() } }, - }); + }) if (isMobile) { return ( @@ -165,20 +164,18 @@ export default function Modal({ ({ item, key, props }) => item && ( {(props) => ( @@ -191,12 +188,11 @@ export default function Modal({ }} >