diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 4a7266a4..00000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - ignorePatterns: ['dist', '.eslintrc.cjs'], - parser: '@typescript-eslint/parser', -} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..b0135f9a --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,83 @@ +{ + "ignorePatterns": [ + "*.test.ts" + ], + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended" + ], + "settings": { + "react": { + "version": "detect" + } + }, + "overrides": [], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "react", + "@typescript-eslint" + ], + "rules": { + "indent": [ + "warn", + 4, + { + "SwitchCase": 1 + } + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single", + { + "allowTemplateLiterals": true + } + ], + "semi": [ + "error", + "never" + ], + "eol-last": [ + "error", + "always" + ], + "comma-dangle": [ + "error", + { + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "never" + } + ], + "@typescript-eslint/no-explicit-any": "off", + "no-extra-semi": "off", + "react/react-in-jsx-scope": "off", + "react/display-name": "off", + "react/prop-types": "off", + "react/jsx-filename-extension": [ + 1, + { + "extensions": [ + ".js", + ".jsx", + ".ts", + ".tsx" + ] + } + ] + } +} \ No newline at end of file diff --git a/index.html b/index.html index ffd486ce..64f46a77 100644 --- a/index.html +++ b/index.html @@ -5,10 +5,20 @@ - + - HAI | App + + + + + + + + + + + HAI • COMING SOON
diff --git a/package.json b/package.json index a74ecdef..ce497f71 100644 --- a/package.json +++ b/package.json @@ -22,35 +22,30 @@ "react-dom": "17.0.1" }, "dependencies": { + "@apollo/client": "^3.8.8", "@ethersproject/experimental": "5.4.0", "@ethersproject/providers": "5.4.5", "@hai-on-op/sdk": "1.2.2-1f60d23d.0", + "@nivo/core": "0.84.0", + "@nivo/line": "0.84.0", + "@nivo/pie": "0.84.0", "@rainbow-me/rainbowkit": "1.0.9", "axios": "0.27.2", - "classnames": "2.2.6", "dayjs": "1.9.4", "easy-peasy": "3.3.1", "ethers": "5.4.7", - "gh-pages": "4.0.0", + "graphql": "^16.8.1", "i18next": "19.7.0", "numeral": "2.0.6", - "postcss": "8.4.28", "react": "17.0.1", - "react-confetti": "6.0.1", - "react-custom-scrollbars": "4.2.1", - "react-device-detect": "1.13.1", "react-dom": "17.0.1", "react-feather": "2.0.9", "react-helmet-async": "1.0.7", "react-i18next": "11.7.2", - "react-lottie-player": "1.4.1", "react-paginate": "6.5.0", "react-router-dom": "5.3.0", "react-toastify": "6.0.9", - "react-tooltip": "4.2.21", - "react-transition-group": "4.4.1", "styled-components": "5.2.0", - "tailwindcss": "3.3.3", "viem": "1.19.15", "wagmi": "1.4.12" }, @@ -62,6 +57,7 @@ "@types/node": "12.0.0", "@types/numeral": "0.0.28", "@types/react-custom-scrollbars": "4.0.7", + "@types/react-dom": "17.0.1", "@types/react-paginate": "6.2.1", "@types/react-router-dom": "5.3.0", "@types/react-transition-group": "4.4.0", @@ -74,6 +70,7 @@ "craco-alias": "3.0.1", "eslint": "8.45.0", "eslint-config-prettier": "9.0.0", + "eslint-config-react-app": "7.0.1", "eslint-plugin-import": "2.28.1", "eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-prettier": "5.0.0", diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index 33ad091d..00000000 --- a/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/public/assets/tie-dye-reduced.mov b/public/assets/tie-dye-reduced.mov new file mode 100644 index 00000000..e0285b13 Binary files /dev/null and b/public/assets/tie-dye-reduced.mov differ diff --git a/public/assets/tie-dye.MOV b/public/assets/tie-dye.MOV new file mode 100644 index 00000000..0d800c94 Binary files /dev/null and b/public/assets/tie-dye.MOV differ diff --git a/public/audio/get-hai-together.wav b/public/audio/get-hai-together.wav new file mode 100644 index 00000000..fd3bdcdc Binary files /dev/null and b/public/audio/get-hai-together.wav differ diff --git a/public/audio/hai-as-fuck.wav b/public/audio/hai-as-fuck.wav new file mode 100644 index 00000000..d58ed10d Binary files /dev/null and b/public/audio/hai-as-fuck.wav differ diff --git a/public/icon.png b/public/icon.png index dcc1ea44..c358ae32 100644 Binary files a/public/icon.png and b/public/icon.png differ diff --git a/public/icons/android-chrome-192x192.png b/public/icons/android-chrome-192x192.png index c7aee48a..6b718ec8 100644 Binary files a/public/icons/android-chrome-192x192.png and b/public/icons/android-chrome-192x192.png differ diff --git a/public/icons/android-chrome-512x512.png b/public/icons/android-chrome-512x512.png index fcaa4178..c358ae32 100644 Binary files a/public/icons/android-chrome-512x512.png and b/public/icons/android-chrome-512x512.png differ diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png index 92c56cdb..47041cab 100644 Binary files a/public/icons/apple-touch-icon.png and b/public/icons/apple-touch-icon.png differ diff --git a/public/icons/favicon-16x16.png b/public/icons/favicon-16x16.png index 261f47f5..2e10f549 100644 Binary files a/public/icons/favicon-16x16.png and b/public/icons/favicon-16x16.png differ diff --git a/public/icons/favicon-32x32.png b/public/icons/favicon-32x32.png index fa040528..7b5abc48 100644 Binary files a/public/icons/favicon-32x32.png and b/public/icons/favicon-32x32.png differ diff --git a/public/icons/favicon.ico b/public/icons/favicon.ico index b862a8ed..a426cb2d 100644 Binary files a/public/icons/favicon.ico and b/public/icons/favicon.ico differ diff --git a/public/icons/logo192.png b/public/icons/logo192.png index ef80305f..17722a6f 100644 Binary files a/public/icons/logo192.png and b/public/icons/logo192.png differ diff --git a/public/icons/logo512.png b/public/icons/logo512.png index dcc1ea44..c358ae32 100644 Binary files a/public/icons/logo512.png and b/public/icons/logo512.png differ diff --git a/public/icons/mstile-150x150.png b/public/icons/mstile-150x150.png index 3f59b037..422810a9 100644 Binary files a/public/icons/mstile-150x150.png and b/public/icons/mstile-150x150.png differ diff --git a/public/logo192.png b/public/logo192.png index ef80305f..f6fc0df3 100644 Binary files a/public/logo192.png and b/public/logo192.png differ diff --git a/public/logo512.png b/public/logo512.png index dcc1ea44..c358ae32 100644 Binary files a/public/logo512.png and b/public/logo512.png differ diff --git a/src/App.tsx b/src/App.tsx index ce96cabd..f92d9ae0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,60 +1,63 @@ -import i18next from 'i18next' import { Suspense } from 'react' -import { I18nextProvider } from 'react-i18next' import { Redirect, Route, Switch } from 'react-router-dom' -import { ThemeProvider } from 'styled-components' -import ErrorBoundary from './ErrorBoundary' -import GlobalStyle from './GlobalStyle' -import Safes from './containers/Safes' -import SafeDetails from './containers/Safes/SafeDetails' -import Shared from './containers/Shared' -import { useStoreState } from './store' -import { Theme } from './utils/interfaces' -import { darkTheme } from './utils/themes/dark' +import i18next from 'i18next' +import { I18nextProvider } from 'react-i18next' +import { ApolloProvider } from '@apollo/client' -import Splash from './containers/Splash' -import Privacy from './containers/Privacy' -import CreateSafe from './containers/Safes/CreateSafe' -import Auctions from './containers/Auctions' -import Analytics from './containers/Analytics' +import type { Theme } from '~/types' +import { client } from '~/utils' +// import { AnalyticsProvider } from '~/providers/AnalyticsProvider' + +import { GlobalStyle } from '~/styles' +import { ErrorBoundary } from '~/ErrorBoundary' +import { Shared } from '~/containers/Shared' +import { Splash } from '~/containers/Splash' +// import { Privacy } from '~/containers/Privacy' +// import { Auctions } from '~/containers/Auctions' +// import { Analytics } from '~/containers/Analytics' +// import { Earn } from '~/containers/Earn' +// import { Vaults } from '~/containers/Vaults' +// import { Contracts } from '~/containers/Contracts' +// import { Learn } from './containers/Learn' +// import { VaultExplorer } from './containers/Vaults/Explore' declare module 'styled-components' { export interface DefaultTheme extends Theme {} } const App = () => { - const { settingsModel: settingsState } = useStoreState((state) => state) - - const { bodyOverflow } = settingsState - return ( - - - + + + + {/* */} <> - + {/* - - - - - - + + + + + + + + */} - - + {/* */} + + ) } diff --git a/src/ErrorBoundary.tsx b/src/ErrorBoundary.tsx index 52707fd0..b6996981 100644 --- a/src/ErrorBoundary.tsx +++ b/src/ErrorBoundary.tsx @@ -1,5 +1,6 @@ import React from 'react' import styled from 'styled-components' + import error from './assets/error.svg' interface State { @@ -11,8 +12,11 @@ interface Props { children?: any } -class ErrorBoundary extends React.Component { - state: State = { error: null, errorInfo: null } +export class ErrorBoundary extends React.Component { + state: State = { + error: null, + errorInfo: null, + } componentDidCatch(error: any, errorInfo: any) { this.setState({ @@ -42,8 +46,6 @@ class ErrorBoundary extends React.Component { } } -export default ErrorBoundary - const Container = styled.div` width: 100%; height: 100%; diff --git a/src/GlobalStyle.tsx b/src/GlobalStyle.tsx deleted file mode 100644 index cb2a4a3d..00000000 --- a/src/GlobalStyle.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { createGlobalStyle, css } from 'styled-components' -import boxes from './assets/boxes.svg' - -interface Props { - bodyOverflow?: boolean -} - -const GlobalStyle = createGlobalStyle` - - body::-webkit-scrollbar { - width: 10px; - background: transparent; - } - - body::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.1); - border-radius: 4px; - } - - body::-webkit-scrollbar-thumb:hover { - background: rgba(0, 0, 0, 0.2); - box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.2); - } - - body::-webkit-scrollbar-thumb:active { - background-color: rgba(0, 0, 0, 0.2); - } - - body { - color: ${(props) => props.theme.colors.primary}; - background-color:${(props) => props.theme.colors.background}; - background-image: url(${boxes}); - background-size: contain; - background-position: center 100px; - background-repeat: no-repeat; - overflow: ${(props: Props) => (props.bodyOverflow ? 'hidden' : 'visible')}; - - .web3modal-modal-lightbox { - z-index: 999; - - .web3modal-modal-card { - display:block; - max-width:400px; - .web3modal-provider-container { - display:flex; - flex-direction:row; - justify-content:space-between; - align-items:center; - padding:16px; - - .web3modal-provider-name{ - font-size: 16px; - width:auto; - } - - .web3modal-provider-icon{ - order:2; - width:30px; - height:30px; - - } - .web3modal-provider-description { - display:none; - } - } - - } -} -.place-left { - &:after{ - border-left-color:${(props) => props.theme.colors.foreground} !important - } - } - - .place-top { - &:after{ - border-top-color:${(props) => props.theme.colors.foreground} !important - } - } - .place-bottom { - &:after{ - border-bottom-color:${(props) => props.theme.colors.foreground} !important - } - } - .place-right { - &:after{ - border-right-color:${(props) => props.theme.colors.foreground} !important - } - } -.__react_component_tooltip { - max-width: 250px; - padding-top: 20px; - padding-bottom: 20px; - border-radius: 5px; - color:${(props) => props.theme.colors.primary}; - opacity: 1 !important; - background: ${(props) => props.theme.colors.foreground}; - border: ${(props) => props.theme.colors.border} !important; - box-shadow: 0 0 6px rgba(0, 0, 0, 0.16); - - } - } -` - -export const ExternalLinkArrow = css` - border: 0; - cursor: pointer; - box-shadow: none; - outline: none; - padding: 0; - margin: 0; - color: ${(props) => props.theme.colors.blueish}; - font-size: ${(props) => props.theme.font.small}; - font-weight: 600; - line-height: 24px; - letter-spacing: -0.18px; - &:disabled { - cursor: not-allowed; - opacity: 0.5; - &:hover { - opacity: 0.5; - } - } - transition: all 0.3s ease; - &:hover { - opacity: 0.8; - } - img { - position: relative; - top: 1px; - } -` - -export const BtnStyle = css<{ - disabled?: boolean - color?: 'blueish' | 'greenish' | 'yellowish' | 'colorPrimary' | 'colorSecondary' -}>` - pointer-events: ${({ theme, disabled }) => (disabled ? 'none' : 'inherit')}; - outline: none; - cursor: ${({ theme, disabled }) => (disabled ? 'not-allowed' : 'pointer')}; - min-width: 134px; - border: none; - box-shadow: none; - line-height: 24px; - font-size: ${(props) => props.theme.font.small}; - font-weight: 600; - padding: 8px 30px; - color: ${(props) => props.theme.colors.neutral}; - background: ${({ theme, disabled, color }) => - disabled ? theme.colors.dimmedBackground : theme.colors[color ?? 'blueish']}; - border-radius: ${(props) => props.theme.global.borderRadius}; - transition: all 0.3s ease; - display: flex; - align-items: center; - border-radius: 50px; - justify-content: space-between; -` - -export default GlobalStyle diff --git a/src/assets/LogoIcon.png b/src/assets/LogoIcon.png index bdc176ce..27c83ed5 100644 Binary files a/src/assets/LogoIcon.png and b/src/assets/LogoIcon.png differ diff --git a/src/assets/border-image.png b/src/assets/border-image.png new file mode 100644 index 00000000..ce3b88b8 Binary files /dev/null and b/src/assets/border-image.png differ diff --git a/src/assets/hai-logo.png b/src/assets/hai-logo.png index dcc1ea44..02203b8c 100644 Binary files a/src/assets/hai-logo.png and b/src/assets/hai-logo.png differ diff --git a/src/assets/hai-logo.svg b/src/assets/hai-logo.svg index 8a526e92..8817aba9 100644 --- a/src/assets/hai-logo.svg +++ b/src/assets/hai-logo.svg @@ -1,22 +1,20 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/kite-img.svg b/src/assets/kite-img.svg new file mode 100644 index 00000000..90f927bc --- /dev/null +++ b/src/assets/kite-img.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 00000000..16d13abb Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/assets/logo192.png b/src/assets/logo192.png index 0ea90e24..27c83ed5 100644 Binary files a/src/assets/logo192.png and b/src/assets/logo192.png differ diff --git a/src/assets/popout.svg b/src/assets/popout.svg new file mode 100644 index 00000000..d3f4ccba --- /dev/null +++ b/src/assets/popout.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/splash/cloud-1.png b/src/assets/splash/cloud-1.png new file mode 100644 index 00000000..3daf310a Binary files /dev/null and b/src/assets/splash/cloud-1.png differ diff --git a/src/assets/splash/cloud-2.png b/src/assets/splash/cloud-2.png new file mode 100644 index 00000000..6b01085f Binary files /dev/null and b/src/assets/splash/cloud-2.png differ diff --git a/src/assets/splash/elf-1.png b/src/assets/splash/elf-1.png new file mode 100644 index 00000000..f249fd2c Binary files /dev/null and b/src/assets/splash/elf-1.png differ diff --git a/src/assets/splash/elf-2.png b/src/assets/splash/elf-2.png new file mode 100644 index 00000000..d3067489 Binary files /dev/null and b/src/assets/splash/elf-2.png differ diff --git a/src/assets/splash/elf-3.png b/src/assets/splash/elf-3.png new file mode 100644 index 00000000..dc5c187c Binary files /dev/null and b/src/assets/splash/elf-3.png differ diff --git a/src/assets/splash/elf-4.png b/src/assets/splash/elf-4.png new file mode 100644 index 00000000..0d919cc7 Binary files /dev/null and b/src/assets/splash/elf-4.png differ diff --git a/src/assets/splash/elf-5.png b/src/assets/splash/elf-5.png new file mode 100644 index 00000000..febc4c0c Binary files /dev/null and b/src/assets/splash/elf-5.png differ diff --git a/src/assets/splash/elf-6.png b/src/assets/splash/elf-6.png new file mode 100644 index 00000000..2e4b8772 Binary files /dev/null and b/src/assets/splash/elf-6.png differ diff --git a/src/assets/splash/elf-kite.png b/src/assets/splash/elf-kite.png new file mode 100644 index 00000000..04e6eacc Binary files /dev/null and b/src/assets/splash/elf-kite.png differ diff --git a/src/assets/splash/splash.jpg b/src/assets/splash/splash.jpg new file mode 100644 index 00000000..bca00185 Binary files /dev/null and b/src/assets/splash/splash.jpg differ diff --git a/src/assets/velodrome-img.svg b/src/assets/velodrome-img.svg new file mode 100644 index 00000000..2bb21acf --- /dev/null +++ b/src/assets/velodrome-img.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/wsteth-img.png b/src/assets/wsteth-img.png new file mode 100644 index 00000000..4544c6be Binary files /dev/null and b/src/assets/wsteth-img.png differ diff --git a/src/components/AddressLink.tsx b/src/components/AddressLink.tsx index 41257fcf..ba96e000 100644 --- a/src/components/AddressLink.tsx +++ b/src/components/AddressLink.tsx @@ -1,20 +1,21 @@ -import styled from 'styled-components' -import { ExternalLinkArrow } from '~/GlobalStyle' -import { getEtherscanLink, returnWalletAddress } from '~/utils' +import { useNetwork } from 'wagmi' -export const Link = styled.a` - ${ExternalLinkArrow} -` +import { NETWORK_ID, getEtherscanLink, returnWalletAddress } from '~/utils' -interface AddressLinkProps { - chainId: number +import { ExternalLink, ExternalLinkProps } from './ExternalLink' + +type AddressLinkProps = Partial & { + chainId?: number address: string + type?: 'address' | 'transaction' } -export const AddressLink = ({ chainId, address }: AddressLinkProps) => { +export const AddressLink = ({ chainId, address, type = 'address', children, ...props }: AddressLinkProps) => { + const { chain } = useNetwork() + return ( - - {returnWalletAddress(address)} - + + {children || returnWalletAddress(address)} + ) } diff --git a/src/components/AlertLabel.tsx b/src/components/AlertLabel.tsx deleted file mode 100644 index 76e00936..00000000 --- a/src/components/AlertLabel.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import styled from 'styled-components' - -interface Props { - type: string - text: string - padding?: string - isBlock?: boolean -} -const AlertLabel = ({ text, type, padding, isBlock = true }: Props) => { - return ( - - - {isBlock ? : null} - {text} - - - ) -} - -export default AlertLabel -const Flex = styled.div` - display: flex; - align-items: center; - justify-content: center; -` - -const Circle = styled.div` - width: 10px; - height: 10px; - border-radius: 50%; - margin-right: 5px; -` -const Container = styled.div<{ isBlock?: boolean }>` - padding: 8px; - height: fit-content; - text-align: center; - font-size: ${(props) => props.theme.font.small}; - border-radius: ${(props) => props.theme.global.borderRadius}; - line-height: 21px; - letter-spacing: -0.09px; - &.alert { - border: 1px solid ${(props) => props.theme.colors.alertBorder}; - border-width: ${({ theme, isBlock }) => (isBlock ? '0' : '1px')}; - background: ${({ theme, isBlock }) => (isBlock ? 'transparent' : theme.colors.alertBackground)}; - color: ${({ theme, isBlock }) => (isBlock ? theme.colors.customSecondary : theme.colors.alertColor)}; - ${Circle} { - background: ${(props) => props.theme.colors.alertColor}; - } - } - &.success { - border: 1px solid ${(props) => props.theme.colors.successBorder}; - border-width: ${({ theme, isBlock }) => (isBlock ? '0' : '1px')}; - background: ${({ theme, isBlock }) => (isBlock ? 'transparent' : theme.colors.successBackground)}; - color: ${({ theme, isBlock }) => (isBlock ? theme.colors.customSecondary : theme.colors.successColor)}; - ${Circle} { - background: ${(props) => props.theme.colors.successColor}; - } - } - &.danger { - border: 1px solid ${(props) => props.theme.colors.dangerColor}; - border-width: ${({ theme, isBlock }) => (isBlock ? '0' : '1px')}; - background: ${({ theme, isBlock }) => (isBlock ? 'transparent' : theme.colors.dangerBackground)}; - color: ${({ theme, isBlock }) => (isBlock ? theme.colors.customSecondary : theme.colors.dangerColor)}; - ${Circle} { - background: ${(props) => props.theme.colors.dangerColor}; - } - } - &.warning { - border: 1px solid ${(props) => props.theme.colors.warningBorder}; - border-width: ${({ theme, isBlock }) => (isBlock ? '0' : '1px')}; - background: ${({ theme, isBlock }) => (isBlock ? 'transparent' : theme.colors.warningBackground)}; - color: ${({ theme, isBlock }) => (isBlock ? theme.colors.customSecondary : theme.colors.warningColor)}; - ${Circle} { - background: ${(props) => props.theme.colors.warningColor}; - } - } - - &.dimmed { - border: 1px solid #959595; - background: ${({ theme, isBlock }) => (isBlock ? 'transparent' : theme.colors.secondary)}; - color: #fff; - ${Circle} { - } - } - - &.gradient { - border: 1px solid ${({ theme, isBlock }) => (isBlock ? 'transparent' : theme.colors.inputBorderColor)}; - background: ${({ theme, isBlock }) => (isBlock ? 'transparent' : theme.colors.gradient)}; - color: #fff; - ${Circle} { - background: ${(props) => props.theme.colors.inputBorderColor}; - } - } - - &.greenish { - border: 1px solid ${(props) => props.theme.colors.inputBorderColor}; - background: #6dbab5; - color: #fff; - ${Circle} { - background: #6dbab5; - } - } - - &.floated { - position: fixed; - width: 100%; - left: 0; - right: 0; - z-index: 996; - } - border-width: ${({ theme, isBlock }) => (isBlock ? '0' : '1px')}; -` diff --git a/src/components/ApproveToken.tsx b/src/components/ApproveToken.tsx deleted file mode 100644 index 07647065..00000000 --- a/src/components/ApproveToken.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { useState } from 'react' -import { AlertTriangle, ArrowUpCircle, CheckCircle } from 'react-feather' -import { utils as gebUtils } from '@hai-on-op/sdk' -import { utils as ethersUtils } from 'ethers' -import styled from 'styled-components' -import { useAccount } from 'wagmi' - -import { useTokenContract, useTransactionAdder, useGeb } from '~/hooks' -import { useStoreActions, useStoreState } from '~/store' -import { AuctionEventType, IAuctionBidder } from '~/types' -import { timeout } from '~/utils' -import Button from './Button' -import Loader from './Loader' - -export type ApproveMethod = 'systemCoin' | 'protocolToken' - -interface Props { - handleBackBtn: () => void - handleSuccess: () => void - bids: IAuctionBidder[] - amount: string - allowance: string - methodName: ApproveMethod - coinName: string - auctionType: AuctionEventType -} - -const ApproveToken = ({ bids, amount, handleBackBtn, handleSuccess, methodName, coinName, auctionType }: Props) => { - const TEXT_PAYLOAD_DEFAULT_STATE = { - title: `${coinName} Allowance`, - text: `Allow your account to manage your ${coinName}`, - status: '', - } - - const geb = useGeb() - const tokenContract = useTokenContract(geb?.contracts[methodName].address) - const [textPayload, setTextPayload] = useState(TEXT_PAYLOAD_DEFAULT_STATE) - const { address: account } = useAccount() - - const addTransaction = useTransactionAdder() - const { connectWalletModel: connectWalletState, popupsModel: popupsState } = useStoreState((state) => state) - const { popupsModel: popupsActions } = useStoreActions((state) => state) - - const { proxyAddress } = connectWalletState - - const returnStatusIcon = (status: string) => { - switch (status) { - case 'success': - return - case 'error': - return - case 'loading': - return - default: - return - } - } - - const passedCheckForAllowance = async (allowance: string, amount: string, isPaid: boolean) => { - if (!isPaid) return - const allowanceBN = allowance ? ethersUtils.parseEther(allowance) : ethersUtils.parseEther('0') - const amountBN = ethersUtils.parseEther(amount) - if (allowanceBN.gte(amountBN)) { - setTextPayload({ - title: `${coinName} Unlocked`, - text: `${coinName} unlocked successfully, proceeding to review transaction...`, - status: 'success', - }) - await timeout(2000) - handleSuccess() - popupsActions.setBlockBackdrop(false) - } else { - setTextPayload(TEXT_PAYLOAD_DEFAULT_STATE) - popupsActions.setBlockBackdrop(false) - } - } - - const unlockRAI = async () => { - try { - if (!account || !tokenContract) return false - if (!proxyAddress) { - throw new Error('No proxy address, disconnect your wallet and reconnect it again') - } - popupsActions.setBlockBackdrop(true) - setTextPayload({ - title: 'Waiting for confirmation', - text: 'Confirm this transaction in your wallet', - status: 'loading', - }) - - let approveAmount: string - if (auctionType === 'SURPLUS' || auctionType === 'COLLATERAL') { - approveAmount = amount - } else { - approveAmount = bids[0].buyAmount - } - - const amountBN = ethersUtils.parseEther(approveAmount) - const txResponse = await tokenContract.approve(proxyAddress, amountBN) - await txResponse.wait() - const allowance = await tokenContract.allowance(account, proxyAddress) - - if (txResponse && allowance.toString()) { - setTextPayload({ - title: `Unlocking ${coinName}`, - text: `Confirming transaction and unlocking ${coinName}`, - status: 'loading', - }) - addTransaction(txResponse, `Unlocking ${coinName}`) - - passedCheckForAllowance(allowance.toString(), amountBN.toString(), true) - } - } catch (e: any) { - popupsActions.setBlockBackdrop(false) - if (e?.code === 4001) { - setTextPayload({ - title: 'Transaction Rejected.', - text: '', - status: 'error', - }) - return - } - setTextPayload({ - title: e.message.includes('proxy') ? 'No Proxy Contract' : 'Transaction Failed.', - text: '', - status: 'error', - }) - console.error(`Transaction failed`, e) - console.log('Required String', gebUtils.getRequireString(e)) - } - } - - return ( - - - {popupsState.blockBackdrop ? null : ( - - - - )} - {returnStatusIcon(textPayload.status)} - {textPayload.title} - - {textPayload.text ? {textPayload.text} : null} - - {!textPayload.status || textPayload.status === 'error' ? ( - - - - ) : null} - - - ) -} - -export default ApproveToken - -const Container = styled.div` - max-width: 400px; - background: ${(props) => props.theme.colors.foreground}; - border-radius: 25px; - margin: 0 auto; -` - -const InnerContainer = styled.div` - background: ${(props) => props.theme.colors.foreground}; - text-align: center; - border-radius: 20px; - padding: 20px 20px 35px 20px; -` - -const ImgContainer = styled.div` - svg { - margin: 25px auto; - height: 40px; - stroke: #4ac6b2; - path { - stroke-width: 1 !important; - } - &.stateless { - stroke: orange; - } - &.success { - stroke: #4ac6b2; - } - &.error { - stroke: rgb(255, 104, 113); - stroke-width: 2; - width: 60px !important; - height: 60px !important; - margin-bottom: 20px; - } - } -` - -const Title = styled.div` - font-size: ${(props) => props.theme.font.medium}; - color: ${(props) => props.theme.colors.primary}; - font-weight: 600; - &.error { - color: rgb(255, 104, 113); - font-weight: normal; - } -` - -const Text = styled.div` - font-size: ${(props) => props.theme.font.small}; - color: ${(props) => props.theme.colors.primary}; - margin: 10px 0; -` - -const BtnContainer = styled.div` - padding: 20px; - margin: 20px -20px -38px; - background-color: ${(props) => props.theme.colors.background}; - border-radius: 0 0 20px 20px; - text-align: center; - svg { - stroke: white; - margin: 0; - } -` - -const BackContainer = styled.div`` diff --git a/src/components/AuctionBlock.tsx b/src/components/AuctionBlock.tsx deleted file mode 100644 index c2e847be..00000000 --- a/src/components/AuctionBlock.tsx +++ /dev/null @@ -1,529 +0,0 @@ -import { useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' -import _ from '~/utils/lodash' -import dayjs from 'dayjs' -import { useConnectModal } from '@rainbow-me/rainbowkit' -import { useAccount, useNetwork } from 'wagmi' - -import { COIN_TICKER, formatNumber, getEtherscanLink, returnWalletAddress, ChainId, parseRad } from '~/utils' -import { useStoreActions, useStoreState } from '~/store' -import { IAuction, IAuctionBidder } from '~/types' -import { ExternalLinkArrow } from '~/GlobalStyle' -import AlertLabel from './AlertLabel' -import Button from './Button' -import debtImage from '~/assets/debt.svg' -import collateralImage from '~/assets/collateral.svg' -import surplusImage from '~/assets/surplus.svg' - -type Props = IAuction & { isCollapsed: boolean } - -const AuctionBlock = (auction: Props) => { - const { chain } = useNetwork() - const { address: account } = useAccount() - const { t } = useTranslation() - const { openConnectModal } = useConnectModal() - const handleConnectWallet = () => openConnectModal && openConnectModal() - - const { popupsModel: popupsActions, auctionModel: auctionsActions } = useStoreActions((state) => state) - - const { connectWalletModel: connectWalletState, auctionModel: auctionsState } = useStoreState((state) => state) - - const isCollapsed = _.get(auction, 'isCollapsed', false) - - const [collapse, setCollapse] = useState(isCollapsed) - - const id = _.get(auction, 'auctionId', '') - const eventType = _.get(auction, 'englishAuctionType', 'debt') - const buyToken = _.get(auction, 'buyToken', 'COIN') - const sellToken = _.get(auction, 'sellToken', 'PROTOCOL_TOKEN') - const buyAmount = _.get(auction, 'buyAmount', '0') - const amountToRaise = _.get(auction, 'amountToRaise', '0') - - const sellInititalAmount = _.get(auction, 'sellInitialAmount', '0') - const buySymbol = buyToken === 'COIN' ? COIN_TICKER : 'KITE' - const sellAmount = _.get(auction, 'sellAmount', '0') - - const tokenSymbol = _.get(auction, 'tokenSymbol', 'TEST') - const sellSymbol = eventType === 'COLLATERAL' ? tokenSymbol : sellToken === 'COIN' ? COIN_TICKER : 'KITE' - - const auctionDeadline = _.get(auction, 'auctionDeadline', '') - const isClaimed = _.get(auction, 'isClaimed', false) - const endsOn = auctionDeadline ? dayjs.unix(Number(auctionDeadline)).format('MMM D, h:mm A') : '' - const isOngoingAuction = auctionDeadline ? Number(auctionDeadline) * 1000 > Date.now() : false - const bidders = _.get(auction, 'englishAuctionBids', []) - const biddersList = _.get(auction, 'biddersList', []) - const winner = _.get(auction, 'winner', '') - - const sellAmountParsed = formatNumber(eventType === 'DEBT' ? sellAmount : sellInititalAmount, 4) - - const buyAmountParsed = formatNumber(eventType === 'COLLATERAL' ? parseRad(amountToRaise) : buyAmount, 4) - - // kickstart bid as in first bid when auction started - const kickBidder = { - bidder: _.get(auction, 'startedBy', ''), - buyAmount: _.get(auction, 'buyInitialAmount', ''), - createdAt: _.get(auction, 'createdAt', ''), - sellAmount: _.get(auction, 'sellInitialAmount', ''), - createdAtTransaction: _.get(auction, 'createdAtTransaction', ''), - } - - const userProxy = _.get(connectWalletState, 'proxyAddress', '') - - const returnWad = (amount: string, i: number) => { - if (!amount) return '0' - return formatNumber(amount, 2) - } - - const returnEventType = (bidder: IAuctionBidder, i: number) => { - if (!isOngoingAuction && bidder.sellAmount === sellAmount && isClaimed && i === 0) { - return 'Settle' - } - if (bidder.bidder === kickBidder.bidder) { - return 'Start' - } - return 'Bid' - } - - const handleClick = (type: string) => { - if (!account) { - handleConnectWallet() - return - } - - if (!userProxy) { - popupsActions.setIsProxyModalOpen(true) - popupsActions.setReturnProxyFunction((storeActions: any) => { - storeActions.popupsModel.setAuctionOperationPayload({ - isOpen: true, - type, - auctionType: eventType, - }) - storeActions.auctionsModel.setSelectedAuction(auction) - }) - return - } - - popupsActions.setAuctionOperationPayload({ - isOpen: true, - type, - auctionType: eventType, - }) - auctionsActions.setSelectedAuction(auction) - } - - const returnBtn = () => { - if ( - !isOngoingAuction && - !isClaimed && - userProxy && - winner && - userProxy.toString() === winner.toString() && - bidders.length - ) { - return ( - - - ) - } - - if ( - isOngoingAuction || - !bidders.length || - (userProxy && - winner && - userProxy.toLowerCase() === winner.toLowerCase() && - !isClaimed && - eventType !== 'COLLATERAL') - ) { - return ( - - - ) - } - return null - } - - const returnLabel = () => { - if (isOngoingAuction) { - return { - text: 'Auction is Ongoing', - label: 'warning', - } - } else if (isClaimed && winner) { - return { - text: 'Auction Completed', - label: 'success', - } - } else if (!isClaimed && !winner) { - return { - text: 'Auction to Restart', - label: 'dimmed', - } - } else { - return { - text: 'Auction to Settle', - label: 'greenish', - } - } - } - - const returnImage = useMemo(() => { - if (eventType.toLocaleLowerCase() === 'collateral') { - return collateralImage - } else if (eventType.toLocaleLowerCase() === 'debt') { - return debtImage - } else { - return surplusImage - } - }, []) - - return ( - -
setCollapse(!collapse)}> - -
auction - {`Auction #${id}`} - - - - - - - {sellSymbol} OFFERED - {`${sellAmountParsed} ${sellSymbol}`} - - - - {buySymbol} BID - {`${buyAmountParsed} ${buySymbol}`} - - - - {isOngoingAuction ? 'ENDS ON' : 'ENDED ON'} - {endsOn} - - - - - - - - - - {collapse ? null : ( - - - - - Event Type - Bidder - Timestamp - Sell Amount - Buy Amount - TX - - - {biddersList.map((bidder: IAuctionBidder, i: number) => { - return ( - - - Event Type - {returnEventType(bidder, i)} - - - Bidder - {bidder.bidder && ( - - {returnWalletAddress(bidder.bidder)} - - )} - - - Timestamp - {dayjs.unix(Number(bidder.createdAt)).format('MMM D, h:mm A')} - - - Sell Amount - {formatNumber(bidder.sellAmount, 2)} {sellSymbol} - - - Buy Amount - {returnWad(bidder.buyAmount, i)} {buySymbol} - - - TX - - {returnWalletAddress(bidder.createdAtTransaction)} - - - - ) - })} - - - {returnBtn()} - - - )} - - ) -} - -export default AuctionBlock - -const Container = styled.div` - border-radius: 15px; - margin-bottom: 15px; - background: #05284c; -` -const Header = styled.div` - font-size: ${(props) => props.theme.font.small}; - font-weight: 600; - padding: 20px; - display: flex; - align-items: center; - justify-content: space-between; - cursor: pointer; - ${({ theme }) => theme.mediaWidth.upToSmall` - flex-direction: column; - align-items:flex-start; - `} -` - -const Info = styled.div` - display: flex; - align-items: center; - ${({ theme }) => theme.mediaWidth.upToSmall` - flex-direction:column; - - `} -` - -const InfoCol = styled.div` - font-size: ${(props) => props.theme.font.small}; - min-width: 100px; - - ${({ theme }) => theme.mediaWidth.upToSmall` - flex: 0 0 100%; - min-width:100%; - display:flex; - align-items:center; - justify-content:space-between; - margin-left:0; - margin-top:5px; - - `} -` - -const InfoLabel = styled.div` - color: ${(props) => props.theme.colors.secondary}; - font-size: ${(props) => props.theme.font.extraSmall}; -` -const InfoValue = styled.div` - margin-top: 3px; - color: ${(props) => props.theme.colors.primary}; - font-weight: normal; - font-size: ${(props) => props.theme.font.extraSmall}; -` - -const Content = styled.div` - padding: 20px 20px 20px 20px; - border-top: 1px solid ${(props) => props.theme.colors.border}; - background: #031f3a; - border-radius: 0 0 15px 15px; -` - -const SectionContent = styled.div` - font-size: ${(props) => props.theme.font.default}; -` - -const Link = styled.a` - ${ExternalLinkArrow} -` - -const BtnContainer = styled.div` - text-align: right; - padding-top: 15px; - margin-bottom: -5px; - margin-top: 10px; - border-top: 1px solid ${(props) => props.theme.colors.border}; -` - -const LeftAucInfo = styled.div<{ type?: string }>` - display: flex; - align-items: center; - img { - margin-right: 10px; - width: 25px; - } -` - -const RightAucInfo = styled.div` - display: flex; - align-items: center; - ${({ theme }) => theme.mediaWidth.upToSmall` - flex: 0 0 100%; - min-width:100%; - flex-direction:column; - `} -` - -const AlertContainer = styled.div` - width: 200px; - text-align: right; - > div { - display: inline-block; - margin-left: auto; - padding-right: 10px !important; - } - div { - font-size: 13px; - ${({ theme }) => theme.mediaWidth.upToSmall` - margin-left:0; - - `} - } - ${({ theme }) => theme.mediaWidth.upToSmall` - - margin-top:10px; - margin-bottom:10px; - `} -` - -const InfoContainer = styled.div` - ${({ theme }) => theme.mediaWidth.upToSmall` - order:2; - min-width:100%; - `} -` - -const Bidders = styled.div` - /* margin-top: 20px; */ -` - -const Heads = styled.div` - display: flex; - align-items: center; - margin-bottom: 10px; - ${({ theme }) => theme.mediaWidth.upToSmall` - display:none; - `} -` - -const Head = styled.div` - flex: 0 0 16.6%; - font-size: 12px; - font-weight: bold; - text-transform: uppercase; - color: ${(props) => props.theme.colors.secondary}; - padding-left: 10px; - &:first-child { - padding-left: 25px; - } -` - -const ListItemLabel = styled.div` - display: none; - ${({ theme }) => theme.mediaWidth.upToSmall` - display:block; - margin-bottom:5px; - font-weight:normal; - color: ${(props) => props.theme.colors.customSecondary}; - `} -` - -const List = styled.div` - display: flex; - align-items: center; - border-radius: 10px; - &:nth-child(even) { - background: #12385e; - } - &.winner { - background: ${(props) => props.theme.colors.greenish}; - a, - div { - color: ${(props) => props.theme.colors.neutral} !important; - } - ${ListItemLabel} { - color: ${(props) => props.theme.colors.background} !important; - } - } - - ${({ theme }) => theme.mediaWidth.upToSmall` - flex-wrap:wrap; - border:1px solid ${(props) => props.theme.colors.border}; - margin-bottom:10px; - &:last-child { - margin-bottom:0; - } - - `} -` - -const ListItem = styled.div` - flex: 0 0 16.6%; - color: ${(props) => props.theme.colors.customSecondary}; - font-size: ${(props) => props.theme.font.extraSmall}; - padding: 15px 10px; - &:first-child { - padding-left: 25px; - } - - ${({ theme }) => theme.mediaWidth.upToSmall` - &:first-child { - padding: 15px 20px; - } - padding: 15px 20px; - - flex: 0 0 50%; - min-width:50%; - font-size: ${(props) => props.theme.font.extraSmall}; - font-weight:900; - `} -` diff --git a/src/components/AuctionsFAQ.tsx b/src/components/AuctionsFAQ.tsx deleted file mode 100644 index 227b3f5d..00000000 --- a/src/components/AuctionsFAQ.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { useState } from 'react' -import styled from 'styled-components' -import { useTranslation } from 'react-i18next' -import { AuctionEventType } from '~/types' -import mine from '../assets/mine.svg' -import bid from '../assets/bid.svg' -import claim from '../assets/claim.svg' -import sellHai from '../assets/sell-hai.svg' - -interface Props { - type: AuctionEventType -} - -interface FAQ { - title: string - desc: string - image: string -} -interface FAQS { - debt: Array - surplus: Array - collateral: Array -} - -const AuctionsFAQ = ({ type }: Props) => { - const { t } = useTranslation() - - const [collapseIndex, setCollapseIndex] = useState(0) - - const faqs: FAQS = { - debt: [ - { - title: t('debt_auction_minting_flx_header'), - desc: t('debt_auction_minting_flx_desc'), - image: mine, - }, - { - title: t('debt_auction_how_to_bid'), - desc: t('debt_auction_how_to_bid_desc'), - image: bid, - }, - { - title: t('debt_auction_claim_tokens'), - desc: t('debt_auction_claim_tokens_desc'), - image: claim, - }, - ], - surplus: [ - { - title: t('surplus_auction_minting_flx_header'), - desc: t('surplus_auction_minting_flx_desc'), - image: sellHai, - }, - { - title: t('surplus_auction_how_to_bid'), - desc: t('surplus_auction_how_to_bid_desc'), - image: bid, - }, - { - title: t('surplus_auction_claim_tokens'), - desc: t('surplus_auction_claim_tokens_desc'), - image: claim, - }, - ], - collateral: [ - { - title: t('collateral_auction_minting_flx_header'), - desc: t('collateral_auction_minting_flx_desc'), - image: sellHai, - }, - { - title: t('collateral_auction_increasing_discount_header'), - desc: t('collateral_auction_increasing_discount_desc'), - image: bid, - }, - { - title: t('collateral_auction_settlement_header'), - desc: t('collateral_auction_settlement_desc'), - image: claim, - }, - ], - } - - return ( - -
- How do {type === 'COLLATERAL' ? '' : 'HAI'} {type.toLowerCase()} auctions work? -
- - {faqs[type.toLowerCase() as 'debt' | 'surplus' | 'collateral'].map((faq: FAQ, index) => ( - - - setCollapseIndex(index)}> - {faq.title} - {faq.title} - - {collapseIndex === index ? {faq.desc} : null} - - - ))} - -
- ) -} - -export default AuctionsFAQ - -const HeroSection = styled.div` - margin-bottom: 20px; - margin-top: 30px; - overflow: hidden; -` -const Header = styled.div` - font-size: ${(props) => props.theme.font.large}; - font-weight: 900; - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 40px; - cursor: pointer; - button { - margin-left: 10px; - font-size: ${(props) => props.theme.font.extraSmall}; - min-width: auto !important; - border-radius: 25px; - padding: 2px 10px; - background: linear-gradient(225deg, #4ce096 0%, #78d8ff 100%); - } - - ${({ theme }) => theme.mediaWidth.upToExtraSmall` - flex-direction:column; - margin-bottom:25px; - button { - margin-top:10px; - } - `} -` -const Content = styled.div` - ${({ theme }) => theme.mediaWidth.upToSmall` - flex-direction:column; - `} -` -const SectionHeading = styled.div` - font-size: ${(props) => props.theme.font.default}; - font-weight: bold; -` -const SectionContent = styled.div` - margin-top: 10px; - font-size: ${(props) => props.theme.font.small}; - line-height: 23px; - color: ${(props) => props.theme.colors.secondary}; - text-align: left; -` - -const Col = styled.div` - margin-bottom: 10px; - &:last-child { - margin-bottom: 0; - } -` - -const InnerCol = styled.div` - background: ${(props) => props.theme.colors.background}; - border-radius: 20px; - padding: 20px; - text-align: center; -` - -const HeaderSection = styled.div` - display: flex; - align-items: center; - cursor: pointer; - img { - width: 20px; - margin-right: 10px; - } -` diff --git a/src/components/AuctionsOperations/AuctionsPayment.tsx b/src/components/AuctionsOperations/AuctionsPayment.tsx deleted file mode 100644 index 5e02cdf9..00000000 --- a/src/components/AuctionsOperations/AuctionsPayment.tsx +++ /dev/null @@ -1,440 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' -import { utils as gebUtils } from '@hai-on-op/sdk' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' -import { BigNumber, ethers, constants } from 'ethers' -import _ from '~/utils/lodash' - -import { useStoreActions, useStoreState } from '~/store' -import { COIN_TICKER, formatNumber, sanitizeDecimals, toFixedString } from '~/utils' -import DecimalInput from '~/components/DecimalInput' -import Button from '~/components/Button' -import Results from './Results' - -const AuctionsPayment = () => { - const { t } = useTranslation() - const [value, setValue] = useState('') - const [collateralValue, setCollateralValue] = useState('') - const [error, setError] = useState('') - const { - auctionModel: auctionsState, - popupsModel: popupsState, - connectWalletModel: connectWalletState, - } = useStoreState((state) => state) - const { auctionModel: auctionsActions, popupsModel: popupsActions } = useStoreActions((state) => state) - - const { - selectedAuction: surplusOrDebtAuction, - selectedCollateralAuction, - amount, - coinBalances, - internalBalance, - protInternalBalance, - } = auctionsState - - const selectedAuction = surplusOrDebtAuction ? surplusOrDebtAuction : selectedCollateralAuction - - const sectionType = popupsState.auctionOperationPayload.auctionType - const isSettle = popupsState.auctionOperationPayload.type.includes('settle') - const isBid = popupsState.auctionOperationPayload.type.includes('bid') - const isClaim = popupsState.auctionOperationPayload.type.includes('claim') - const isBuyCollateral = popupsState.auctionOperationPayload.type.includes('buy') - - const tokenSymbol = _.get(selectedAuction, 'tokenSymbol', undefined) - const auctionType = _.get(selectedAuction, 'englishAuctionType', 'DEBT') - const buyInitialAmount = _.get(selectedAuction, 'buyInitialAmount', '0') - const sellInitialAmount = _.get(selectedAuction, 'sellInitialAmount', '0') - const bids = _.get(selectedAuction, 'englishAuctionBids', '[]') - const biddersList = _.get(selectedAuction, 'biddersList', '[]') - const remainingCollateral = _.get(selectedAuction, 'remainingCollateral', '0') - const remainingToRaise = _.get(selectedAuction, 'remainingToRaiseE18', '0') - - const sellAmount = _.get(selectedAuction, 'sellAmount', '0') - const buyAmount = _.get(selectedAuction, 'buyAmount', '0') - - const buyToken = _.get(selectedAuction, 'buyToken', 'COIN') - const sellToken = _.get(selectedAuction, 'sellToken', 'PROTOCOL_TOKEN') - const auctionId = _.get(selectedAuction, 'auctionId', 1) - - // const bidIncrease: string = _.get(selectedAuction, 'englishAuctionConfiguration.bidIncrease', '1') - const bidIncreaseBN = _.get( - auctionsState, - 'auctionsData.surplusAuctionHouseParams.bidIncrease', - '1010000000000000000' - ) - const bidIncrease: string = ethers.utils.formatEther(bidIncreaseBN) - // const debt_amountSoldIncrease: string = _.get( - // selectedAuction, - // 'englishAuctionConfiguration.DEBT_amountSoldIncrease', - // '1' - // ) - const bidDecreseBN = _.get(auctionsState, 'auctionsData.debtAuctionHouseParams.bidDecrease', '1050000000000000000') - const debt_amountSoldIncrease: string = ethers.utils.formatEther(bidDecreseBN) - - const auctionDeadline = _.get(selectedAuction, 'auctionDeadline', '') - const isOngoingAuction = auctionDeadline ? Number(auctionDeadline) * 1000 > Date.now() : false - - const haiBalance = _.get(coinBalances, 'hai', '0') - const kiteBalance = _.get(coinBalances, 'kite', '0') - const haiAllowance = _.get(connectWalletState, 'coinAllowance', '0') - const kiteAllowance = _.get(connectWalletState, 'protAllowance', '0') - - const buySymbol = buyToken === 'COIN' ? COIN_TICKER : 'KITE' - const sellSymbol = sellToken === 'COIN' ? COIN_TICKER : 'KITE' - - const collateralPrice = useMemo(() => { - if (auctionsState.collateralData && selectedCollateralAuction) { - const data = auctionsState.collateralData.filter((item) => item._auctionId.toString() === auctionId) - const price = data[0]?._boughtCollateral.mul(constants.WeiPerEther).div(data[0]._adjustedBid) - - // we divide by 1e18 because we multiplied by 1e18 in the line above - // this was required to handle decimal prices (<0) - return price - } - return BigNumber.from('0') - }, [auctionId, auctionsState.collateralData]) - - const collateralPriceFormatted = ethers.utils.formatUnits(collateralPrice || constants.WeiPerEther, 18) - - const handleAmountChange = (val: string) => { - setError('') - setValue(val) - auctionsActions.setAmount(val) - const valBN = BigNumber.from(ethers.utils.parseEther(val || '0')) - const colValueBN = valBN.mul(collateralPrice).div(constants.WeiPerEther) - const colValueBNDecimalsRemoved = gebUtils.decimalShift(gebUtils.decimalShift(colValueBN, -8), 8) - setCollateralValue(ethers.utils.formatEther(colValueBNDecimalsRemoved.toString())) - auctionsActions.setCollateralAmount(ethers.utils.formatEther(colValueBNDecimalsRemoved.toString())) - } - - const handleCollateralAmountChange = useCallback( - (amount: string) => { - setError('') - setCollateralValue(amount) - auctionsActions.setCollateralAmount(amount) - - const value = (Number(amount) / Number(collateralPriceFormatted)).toString() || '' - setValue(sanitizeDecimals(value, 18)) - auctionsActions.setAmount(sanitizeDecimals(value, 18)) - }, - [auctionsActions, collateralPriceFormatted] - ) - - const maxBid = (): string => { - const buyAmountBN = buyAmount ? BigNumber.from(toFixedString(buyAmount, 'WAD')) : BigNumber.from('0') - const bidIncreaseBN = BigNumber.from(toFixedString(bidIncrease, 'WAD')) - if (auctionType === 'DEBT') { - const sellAmountBN = sellAmount ? BigNumber.from(toFixedString(sellAmount, 'WAD')) : BigNumber.from('0') - if (bids.length === 0) { - if (isOngoingAuction) { - // We need to bid N% less than the current best bid - return gebUtils - .wadToFixed(sellAmountBN.mul(100).div(bidDecreseBN).mul(gebUtils.WAD).div(100)) - .toString() - } else { - // TODO: check those calcs - // Auction restart (no bids and passed the dealine) - // When doing restart we're allowed to accept more FLX, DEBT_amountSoldIncrease=1.2 - const numerator = sellAmountBN.mul(BigNumber.from(toFixedString(debt_amountSoldIncrease, 'WAD'))) - return gebUtils.wadToFixed(numerator.div(bidIncreaseBN).mul(gebUtils.WAD)).toString() - } - } else { - // We need to bid N% less than the current best bid - return gebUtils - .wadToFixed(sellAmountBN.mul(100).div(bidDecreseBN).mul(gebUtils.WAD).div(100)) - .toString() - } - } - const amountToBuy = - biddersList.length > 0 && buyAmountBN.isZero() - ? BigNumber.from(toFixedString(biddersList[0].buyAmount, 'WAD')) - : buyAmountBN - - const max = gebUtils.wadToFixed(amountToBuy.mul(bidIncreaseBN).div(gebUtils.WAD)).toString() - - return max - } - - const maxAmount = (function () { - if (auctionType === 'COLLATERAL') { - const haiToBidPlusOne = BigNumber.from(remainingToRaise).add(1) - const haiToBid = ethers.utils.formatUnits(haiToBidPlusOne.toString(), 18) - const haiBalanceNumber = Number(haiBalance) - return haiBalanceNumber < Number(haiToBid) ? haiBalance : haiToBid.toString() - } else { - return maxBid() - } - })() - - const maxCollateral = BigNumber.from(ethers.utils.parseEther(maxAmount)) - .mul(collateralPrice) - .div(constants.WeiPerEther) - const maxCollateralParsed = ethers.utils.formatEther(maxCollateral) - - const passedChecks = () => { - const maxBidAmountBN = BigNumber.from(toFixedString(maxBid(), 'WAD')) - - const valueBN = value ? BigNumber.from(toFixedString(value, 'WAD')) : BigNumber.from('0') - - const raiBalanceBN = haiBalance ? BigNumber.from(toFixedString(haiBalance, 'WAD')) : BigNumber.from('0') - const flxBalanceBN = kiteBalance ? BigNumber.from(toFixedString(kiteBalance, 'WAD')) : BigNumber.from('0') - const internalBalanceBN = - internalBalance && Number(internalBalance) > 0.00001 - ? BigNumber.from(toFixedString(internalBalance, 'WAD')) - : BigNumber.from('0') - const flxInternalBalance = - protInternalBalance && Number(protInternalBalance) > 0.00001 - ? BigNumber.from(toFixedString(protInternalBalance, 'WAD')) - : BigNumber.from('0') - - const totalRaiBalance = raiBalanceBN.add(internalBalanceBN) - const totalFlxBalance = flxBalanceBN.add(flxInternalBalance) - - const buyAmountBN = buyAmount ? BigNumber.from(toFixedString(buyAmount, 'WAD')) : BigNumber.from('0') - - if (valueBN.lt(BigNumber.from('0'))) { - setError(`You cannot bid a negative number`) - return false - } - if (valueBN.isZero()) { - setError(`You cannot submit nothing`) - return false - } - - if (auctionType === 'SURPLUS') { - if (buyAmountBN.gt(totalFlxBalance) || valueBN.gt(flxBalanceBN)) { - setError(`Insufficient KITE balance.`) - return false - } - - if (bids.length > 0 && valueBN.lt(maxBidAmountBN)) { - setError( - `You need to bid ${((Number(bidIncrease) - 1) * 100).toFixed(0)}% more KITE vs the highest bid` - ) - return false - } - } - - if (auctionType === 'DEBT') { - if (buyAmountBN.gt(totalRaiBalance) || valueBN.gt(raiBalanceBN)) { - setError(`Insufficient ${COIN_TICKER} balance.`) - return false - } - - if (!bids.length && valueBN.gt(maxBidAmountBN)) { - setError(`You can only bid a maximum of ${maxBid()} ${sellSymbol}`) - return false - } - - if (bids.length > 0 && valueBN.gt(maxBidAmountBN)) { - setError( - `You need to bid ${((Number(debt_amountSoldIncrease) - 1) * 100).toFixed( - 0 - )}% less KITE vs the lowest bid` - ) - return false - } - } - - if (auctionType === 'COLLATERAL') { - const haiBalanceBN = ethers.utils.parseUnits(haiBalance) - const valueBN = value ? ethers.utils.parseUnits(value, 18) : BigNumber.from('0') - const collateralAmountBN = collateralValue - ? ethers.utils.parseUnits(collateralValue, 18) - : BigNumber.from('0') - - // Collateral Error when you dont have enough balance - if (buyAmountBN.gt(totalRaiBalance) || valueBN.gt(haiBalanceBN)) { - setError(`Insufficient ${COIN_TICKER} balance.`) - return false - } - - // Collateral Error when there is not enough collateral left to buy - if (collateralAmountBN.gt(remainingCollateral)) { - setError(`Insufficient ${tokenSymbol} to buy.`) - return false - } - } - - return true - } - - const hasAllowance = () => { - let tempValue = value - const haiAllowanceBN = haiAllowance ? BigNumber.from(toFixedString(haiAllowance, 'WAD')) : BigNumber.from('0') - const kiteAllowanceBN = kiteAllowance - ? BigNumber.from(toFixedString(kiteAllowance, 'WAD')) - : BigNumber.from('0') - - if (auctionType === 'COLLATERAL') { - const haiAmountBN = amount ? BigNumber.from(toFixedString(amount, 'WAD')) : BigNumber.from('0') - return haiAllowanceBN.gte(haiAmountBN) - } - - if (auctionType === 'DEBT') { - tempValue = buyAmount - } - const valueBN = tempValue ? BigNumber.from(toFixedString(tempValue, 'WAD')) : BigNumber.from('0') - if (auctionType === 'SURPLUS') { - return kiteAllowanceBN.gte(valueBN) - } - return haiAllowanceBN.gte(valueBN) - } - - const handleSubmit = () => { - if (isBuyCollateral) { - if (passedChecks()) { - if (hasAllowance()) { - auctionsActions.setOperation(2) - } else { - auctionsActions.setOperation(1) - } - return - } - return - } - - if (isBid) { - if (passedChecks()) { - if (hasAllowance()) { - auctionsActions.setOperation(2) - } else { - auctionsActions.setOperation(1) - } - } - return - } - - if (sectionType === 'DEBT') { - auctionsActions.setOperation(2) - } else { - auctionsActions.setAmount(protInternalBalance) - auctionsActions.setOperation(2) - } - } - - const returnClaimValues = () => { - const amount = - Number(protInternalBalance) > Number(internalBalance) - ? Number(protInternalBalance) - : Number(internalBalance) - const symbol = Number(protInternalBalance) > Number(internalBalance) ? 'KITE' : 'HAI' - return { amount, symbol } - } - - const handleCancel = () => { - popupsActions.setAuctionOperationPayload({ - isOpen: false, - type: '', - auctionType: '', - }) - auctionsActions.setOperation(0) - auctionsActions.setSelectedAuction(null) - auctionsActions.setAmount('') - } - - useEffect(() => { - setValue(amount) - }, [amount]) - - const upperInput = (function () { - switch (auctionType) { - case 'DEBT': - return { value: buyInitialAmount, label: `${buySymbol} to Bid` } - case 'SURPLUS': - return { value: sellInitialAmount, label: `${sellSymbol} to Receive` } - case 'COLLATERAL': - return { - value: collateralValue, - label: `${tokenSymbol} to Receive (Max: ${formatNumber(maxCollateralParsed, 4)} ${tokenSymbol})`, - } - default: - return { value: '', label: '' } - } - })() - - const lowerInput = (function () { - switch (auctionType) { - case 'DEBT': - return { value: value, label: `${sellSymbol} to Receive` } - case 'SURPLUS': - return { value: value, label: `${buySymbol} to Bid` } - case 'COLLATERAL': - return { - value: value, - label: `${buySymbol} to Bid (Max: ${formatNumber(maxAmount, 4)} ${buySymbol})`, - } - default: - return { value: '', label: '' } - } - })() - - return ( - - {!isSettle && !isClaim ? ( - <> - - - handleAmountChange(maxAmount)} - /> - - ) : ( - {}} - value={ - isClaim - ? Number(returnClaimValues().amount) < 0.0001 - ? '< 0.0001' - : returnClaimValues().amount - : sellAmount - } - label={`Claimable ${isClaim ? returnClaimValues().symbol : sellSymbol}`} - /> - )} - {error && {error}} - -
-
-
- ) -} - -export default AuctionsPayment - -const Container = styled.div` - padding: 20px; -` - -const MarginFixer = styled.div` - margin-top: 20px; -` - -const Footer = styled.div` - display: flex; - justify-content: space-between; - padding: 20px 0 0 0; -` - -const Error = styled.p` - color: ${(props) => props.theme.colors.dangerColor}; - font-size: ${(props) => props.theme.font.extraSmall}; - width: 100%; - margin: 16px 0; -` diff --git a/src/components/AuctionsOperations/AuctionsTransactions.tsx b/src/components/AuctionsOperations/AuctionsTransactions.tsx deleted file mode 100644 index 29418e4b..00000000 --- a/src/components/AuctionsOperations/AuctionsTransactions.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' -import { useAccount } from 'wagmi' - -import { handleTransactionError, useGeb, useEthersSigner } from '~/hooks' -import TransactionOverview from '~/components/TransactionOverview' -import { COIN_TICKER } from '~/utils' -import { useStoreActions, useStoreState } from '~/store' -import { AuctionEventType } from '~/types' -import Button from '~/components/Button' -import Results from './Results' -import _ from '~/utils/lodash' - -const AuctionsTransactions = () => { - const { t } = useTranslation() - const { address: account } = useAccount() - const signer = useEthersSigner() - const geb = useGeb() - - const { auctionModel: auctionsActions, popupsModel: popupsActions } = useStoreActions((state) => state) - - const { - auctionModel: auctionsState, - popupsModel: popupsState, - connectWalletModel: { proxyAddress }, - } = useStoreState((state) => state) - - const { - amount, - selectedAuction: surplusOrDebtAuction, - protInternalBalance, - internalBalance, - selectedCollateralAuction, - collateralAmount, - } = auctionsState - - const selectedAuction = surplusOrDebtAuction ? surplusOrDebtAuction : selectedCollateralAuction - - const auctionId = _.get(selectedAuction, 'auctionId', '') - const auctionType = _.get(selectedAuction, 'englishAuctionType', 'DEBT') - const tokenSymbol = _.get(selectedAuction, 'tokenSymbol', 'WETH') - const isClaim = popupsState.auctionOperationPayload.type.includes('claim') - const isSettle = popupsState.auctionOperationPayload.type.includes('settle') - const isBuy = popupsState.auctionOperationPayload.type.includes('buy') - const sectionType = popupsState.auctionOperationPayload.auctionType - const handleBack = () => auctionsActions.setOperation(0) - - const reset = async () => { - // after execute any action we refetch auctions and auctionsData again to update the bidding list - if (geb && proxyAddress) { - auctionsActions.fetchAuctionsData({ geb, proxyAddress }) - auctionsActions.fetchAuctions({ geb, type: sectionType as AuctionEventType, tokenSymbol }) - } - - auctionsActions.setAmount('') - auctionsActions.setOperation(0) - auctionsActions.setSelectedAuction(null) - auctionsActions.setIsSubmitting(false) - - // after execute a transaction, fetch auctions again to update the bidding list - auctionsActions.fetchAuctions({ geb, type: sectionType as AuctionEventType }) - } - - const handleWaitingTitle = (function () { - switch (auctionType) { - case 'DEBT': - return isSettle ? 'Claiming KITE' : isClaim ? 'Claiming Tokens' : `Bid ${COIN_TICKER} and Receive KITE` - - case 'SURPLUS': - return isSettle ? 'Claiming HAI' : isClaim ? 'Claiming Tokens' : `Bid KITE and Receive ${COIN_TICKER}` - - case 'COLLATERAL': - return 'Buying Collateral' - - default: - return '' - } - })() - - const handleConfirm = async () => { - try { - if (account && signer) { - popupsActions.setAuctionOperationPayload({ - isOpen: false, - type: '', - auctionType: '', - }) - popupsActions.setIsWaitingModalOpen(true) - popupsActions.setWaitingPayload({ - title: 'Waiting For Confirmation', - text: handleWaitingTitle, - hint: 'Confirm this transaction in your wallet', - status: 'loading', - }) - - if (isBuy) { - await auctionsActions.auctionBuy({ - signer, - auctionId, - title: handleWaitingTitle, - haiAmount: amount, - collateral: tokenSymbol, - collateralAmount: collateralAmount, - }) - } else if (isSettle) { - await auctionsActions.auctionClaim({ - signer, - auctionId, - title: handleWaitingTitle, - auctionType, - }) - } else if (isClaim) { - await auctionsActions.auctionClaimInternalBalance({ - signer, - auctionId, - title: handleWaitingTitle, - auctionType, - bid: Number(internalBalance) > 0 ? internalBalance : protInternalBalance, - token: Number(internalBalance) > 0 ? 'COIN' : 'PROTOCOL_TOKEN', - }) - } else { - await auctionsActions.auctionBid({ - signer, - auctionId, - title: handleWaitingTitle, - auctionType, - bid: amount, - }) - } - } - reset() - } catch (e) { - reset() - handleTransactionError(e) - } finally { - } - } - - return ( - - <> - - - - - -
-
- -
- ) -} - -export default AuctionsTransactions - -const Container = styled.div`` - -const Body = styled.div` - padding: 20px; -` - -const Footer = styled.div` - display: flex; - justify-content: space-between; - padding: 20px; -` diff --git a/src/components/AuctionsOperations/Results.tsx b/src/components/AuctionsOperations/Results.tsx deleted file mode 100644 index 50d8b7dd..00000000 --- a/src/components/AuctionsOperations/Results.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { useMemo } from 'react' -import styled from 'styled-components' - -import { useStoreState } from '~/store' -import { formatNumber, COIN_TICKER } from '~/utils' -import _ from '~/utils/lodash' - -const Results = () => { - const { auctionModel: auctionsState, popupsModel: popupsState } = useStoreState((state) => state) - - const { - selectedAuction: surplusOrDebtAuction, - selectedCollateralAuction, - amount, - internalBalance, - protInternalBalance, - collateralAmount, - } = auctionsState - const selectedAuction = surplusOrDebtAuction ? surplusOrDebtAuction : selectedCollateralAuction - - const buyInititalAmount = _.get(selectedAuction, 'buyInitialAmount', '0') - const sellInitialAmount = _.get(selectedAuction, 'sellInitialAmount', '0') - const auctionType = _.get(selectedAuction, 'englishAuctionType', 'DEBT') - const auctionId = _.get(selectedAuction, 'auctionId', '') - const buyToken = _.get(selectedAuction, 'buyToken', 'COIN') - const sellToken = _.get(selectedAuction, 'sellToken', 'PROTOCOL_TOKEN') - const sellAmount = _.get(selectedAuction, 'sellAmount', '0') - const tokenSymbol = _.get(selectedAuction, 'tokenSymbol', undefined) - - const buySymbol = buyToken === 'PROTOCOL_TOKEN_LP' ? 'KITE/ETH LP' : buyToken === 'COIN' ? COIN_TICKER : 'KITE' - const sellSymbol = sellToken === 'PROTOCOL_TOKEN_LP' ? 'KITE/ETH LP' : sellToken === 'COIN' ? COIN_TICKER : 'KITE' - - const isClaim = popupsState.auctionOperationPayload.type.includes('claim') - const isSettle = popupsState.auctionOperationPayload.type.includes('settle') - - const leftOverBalance = useMemo(() => { - const balance = Number(protInternalBalance) > Number(internalBalance) ? protInternalBalance : internalBalance - return Number(balance) < 0.0001 ? '< 0.0001' : formatNumber(balance, 2) - }, [internalBalance, protInternalBalance]) - - const resultSection = (function () { - switch (auctionType) { - case 'DEBT': - return { - firstLabel: `${buySymbol} to Bid`, - firstValue: buyInititalAmount, - secondLabel: `${sellSymbol} to Receive`, - secondValue: amount, - } - case 'SURPLUS': - return { - firstLabel: `${sellSymbol} to Receive`, - firstValue: sellInitialAmount, - secondLabel: `${buySymbol} to Bid`, - secondValue: amount, - } - case 'COLLATERAL': - return { - firstLabel: `${tokenSymbol} to Receive`, - firstValue: collateralAmount, - secondLabel: `${buySymbol} to Bid`, - secondValue: amount, - } - default: - return { firstLabel: '', firstValue: '', secondLabel: '', secondValue: '' } - } - })() - - return ( - - - {isClaim ? ( - - - {`${leftOverBalance}`} - - ) : ( - <> - - - {`${auctionId}`} - - {isSettle ? ( - - - {`${formatNumber(sellAmount, 2)}`} - - ) : ( - <> - - - {`${formatNumber( - resultSection.firstValue, - auctionType === 'COLLATERAL' ? 4 : 2 - )}`} - - - - {`${formatNumber(amount, 2)}`} - - - )} - - )} - - - ) -} - -export default Results - -const Result = styled.div` - margin-top: 20px; - border-radius: ${(props) => props.theme.global.borderRadius}; - border: 1px solid ${(props) => props.theme.colors.border}; - background: ${(props) => props.theme.colors.foreground}; -` - -const Block = styled.div` - border-bottom: 1px solid; - padding: 16px 20px; - border-bottom: 1px solid ${(props) => props.theme.colors.border}; - &:last-child { - border-bottom: 0; - } -` - -const Item = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 8px; - &:last-child { - margin-bottom: 0; - } -` - -const Label = styled.div` - font-size: ${(props) => props.theme.font.small}; - color: ${(props) => props.theme.colors.secondary}; - letter-spacing: -0.09px; - line-height: 21px; -` - -const Value = styled.div` - font-size: ${(props) => props.theme.font.small}; - color: ${(props) => props.theme.colors.primary}; - letter-spacing: -0.09px; - line-height: 21px; - font-weight: 600; -` diff --git a/src/components/AuctionsOperations/index.tsx b/src/components/AuctionsOperations/index.tsx deleted file mode 100644 index af1251ee..00000000 --- a/src/components/AuctionsOperations/index.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import React from 'react' -import { CSSTransition, SwitchTransition } from 'react-transition-group' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' - -import ApproveToken from '~/components/ApproveToken' -import { useStoreActions, useStoreState } from '~/store' -import AuctionsTransactions from './AuctionsTransactions' -import AuctionsPayment from './AuctionsPayment' -import { COIN_TICKER } from '~/utils' -import _ from '~/utils/lodash' - -const AuctionsOperations = () => { - const { t } = useTranslation() - const nodeRef = React.useRef(null) - const { auctionModel: auctionsActions } = useStoreActions((state) => state) - const { - auctionModel: auctionsState, - popupsModel: popupsState, - connectWalletModel: connectWalletState, - } = useStoreState((state) => state) - - const { selectedAuction: surplusOrDebtAuction, selectedCollateralAuction } = auctionsState - const selectedAuction = surplusOrDebtAuction ? surplusOrDebtAuction : selectedCollateralAuction - - const raiCoinAllowance = _.get(connectWalletState, 'coinAllowance', '0') - const flxAllowance = _.get(connectWalletState, 'protAllowance', '0') - const auctionType = _.get(selectedAuction, 'englishAuctionType', 'DEBT') - const bids = _.get(selectedAuction, 'englishAuctionBids', '[]') - const amount = _.get(auctionsState, 'amount', '0') - - const returnBody = () => { - switch (auctionsState.operation) { - case 0: - return - case 2: - return - default: - break - } - } - - return ( - - - - {auctionsState.operation === 1 ? ( - auctionsActions.setOperation(0)} - handleSuccess={() => auctionsActions.setOperation(2)} - amount={amount} - bids={bids} - allowance={ - auctionType === 'DEBT' || auctionType === 'COLLATERAL' ? raiCoinAllowance : flxAllowance - } - coinName={ - auctionType === 'DEBT' || auctionType === 'COLLATERAL' - ? (COIN_TICKER as string) - : 'KITE' - } - methodName={ - auctionType === 'DEBT' || auctionType === 'COLLATERAL' ? 'systemCoin' : 'protocolToken' - } - auctionType={auctionType} - /> - ) : ( - -
- {t(popupsState.auctionOperationPayload.type, { - hai: COIN_TICKER, - })} -
- {returnBody()} -
- )} -
-
-
- ) -} - -export default AuctionsOperations - -const ModalContent = styled.div` - background: ${(props) => props.theme.colors.background}; - border-radius: ${(props) => props.theme.global.borderRadius}; - border: 1px solid ${(props) => props.theme.colors.border}; -` - -const Header = styled.div` - padding: 20px; - font-size: ${(props) => props.theme.font.large}; - font-weight: 600; - color: ${(props) => props.theme.colors.primary}; - border-bottom: 1px solid ${(props) => props.theme.colors.border}; - letter-spacing: -0.47px; - span { - text-transform: capitalize; - } -` - -const Fade = styled.div` - &.fade-enter { - opacity: 0; - transform: translateX(50px); - } - &.fade-enter-active { - opacity: 1; - transform: translateX(0); - } - &.fade-exit { - opacity: 1; - transform: translateX(0); - } - &.fade-exit-active { - opacity: 0; - transform: translateX(-50px); - } - &.fade-enter-active, - &.fade-exit-active { - transition: - opacity 300ms, - transform 300ms; - } -` diff --git a/src/components/BidLine.tsx b/src/components/BidLine.tsx deleted file mode 100644 index 88763988..00000000 --- a/src/components/BidLine.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import dayjs from 'dayjs' -import styled from 'styled-components' - -import { ExternalLinkArrow } from '~/GlobalStyle' -import { ChainId, formatNumber, getEtherscanLink, returnWalletAddress } from '~/utils' -import { useNetwork } from 'wagmi' -type Props = { - eventType: string - bidder: string - date: string - bid: string - buyAmount: string - buySymbol: string - sellSymbol: string - createdAtTransaction: string -} - -const BidLine = ({ eventType, bidder, date, bid, buyAmount, buySymbol, sellSymbol, createdAtTransaction }: Props) => { - const { chain } = useNetwork() - - const chainId = chain?.id - - const returnWad = (amount: string) => { - if (!amount) return '0' - return formatNumber(amount, 2) - } - - return ( - <> - {eventType} - - {bidder && ( - - {returnWalletAddress(bidder)} - - )} - - - Timestamp - {dayjs.unix(Number(date)).format('MMM D, h:mm A')} - - - Sell Amount - {formatNumber(bid)} {sellSymbol} - - - Buy Amount - {returnWad(buyAmount)} {buySymbol} - - - TX - - {returnWalletAddress(createdAtTransaction)} - - - - ) -} - -export default BidLine - -const Link = styled.a` - ${ExternalLinkArrow} -` - -const ListItemLabel = styled.div` - display: none; - ${({ theme }) => theme.mediaWidth.upToSmall` - display:block; - margin-bottom:5px; - font-weight:normal; - color: ${(props) => props.theme.colors.customSecondary}; - `} -` - -const ListItem = styled.div` - flex: 0 0 16.6%; - color: ${(props) => props.theme.colors.customSecondary}; - font-size: ${(props) => props.theme.font.extraSmall}; - padding: 15px 10px; - &:first-child { - padding-left: 25px; - } - - ${({ theme }) => theme.mediaWidth.upToSmall` - &:first-child { - padding: 15px 20px; - } - padding: 15px 20px; - - flex: 0 0 50%; - min-width:50%; - font-size: ${(props) => props.theme.font.extraSmall}; - font-weight:900; - `} -` diff --git a/src/components/BlockBodyContainer.tsx b/src/components/BlockBodyContainer.tsx deleted file mode 100644 index f716dbc5..00000000 --- a/src/components/BlockBodyContainer.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import styled from 'styled-components' - -const BlockBodyContainer = () => { - return -} - -export default BlockBodyContainer - -const Container = styled.div` - position: fixed; - top: 0; - left: 0; - height: 100%; - width: 100%; - z-index: 1000; - background-color: rgba(35, 37, 39, 0.75); - -webkit-tap-highlight-color: transparent; -` diff --git a/src/components/BlockedAddress.tsx b/src/components/BlockedAddress.tsx index c291c044..b38d5235 100644 --- a/src/components/BlockedAddress.tsx +++ b/src/components/BlockedAddress.tsx @@ -1,6 +1,6 @@ import styled from 'styled-components' -const BlockedAddress = () => { +export function BlockedAddress() { return ( Sorry, you cannot use the app! @@ -8,10 +8,8 @@ const BlockedAddress = () => { ) } -export default BlockedAddress - const Box = styled.div` - background: ${(props) => props.theme.colors.colorSecondary}; + background: ${({ theme }) => theme.colors.colorSecondary}; padding: 30px; border-radius: 10px; text-align: center; diff --git a/src/components/Brand.tsx b/src/components/Brand.tsx deleted file mode 100644 index 204e1bf3..00000000 --- a/src/components/Brand.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import styled from 'styled-components' -import Logo from '../assets/hai-logo.png' - -interface Props { - height?: number -} - -const Brand = ({ height }: Props) => { - return ( - - - HAI - - - ) -} - -export default Brand - -const Container = styled.div` - a { - color: inherit; - text-decoration: none; - img { - width: 75px; - &.small { - width: 50px; - height: 50px; - } - ${({ theme }) => theme.mediaWidth.upToSmall` - width: 50px; - height: 50px; - } - `} - } - } -` diff --git a/src/components/BrandElements/Cloud.tsx b/src/components/BrandElements/Cloud.tsx new file mode 100644 index 00000000..b6da0e4c --- /dev/null +++ b/src/components/BrandElements/Cloud.tsx @@ -0,0 +1,34 @@ +import styled from 'styled-components' + +import cloud1 from '~/assets/splash/cloud-1.png' +import cloud2 from '~/assets/splash/cloud-2.png' + +const clouds = [ + { + src: cloud1, + width: 446, + height: 550, + }, + { + src: cloud2, + width: 571, + height: 528, + }, +] + +type CloudProps = { + variant: number + width?: string + style?: object +} + +export function Cloud({ variant, width, ...props }: CloudProps) { + return +} + +export const CloudImage = styled.img<{ $width?: string }>` + position: absolute; + width: ${({ $width = 'auto' }) => $width}; + height: auto; + pointer-events: none; +` diff --git a/src/components/BrandElements/Elf.tsx b/src/components/BrandElements/Elf.tsx new file mode 100644 index 00000000..5f659550 --- /dev/null +++ b/src/components/BrandElements/Elf.tsx @@ -0,0 +1,79 @@ +import { useState } from 'react' +import styled, { css, keyframes } from 'styled-components' + +import elf1 from '~/assets/splash/elf-1.png' +import elf2 from '~/assets/splash/elf-2.png' +import elf3 from '~/assets/splash/elf-3.png' +import elf4 from '~/assets/splash/elf-4.png' +import elf5 from '~/assets/splash/elf-5.png' +import elf6 from '~/assets/splash/elf-6.png' + +const elves = [ + { + src: elf1, + width: 446, + height: 550, + }, + { + src: elf2, + width: 420, + height: 476, + }, + { + src: elf3, + width: 400, + height: 580, + }, + { + src: elf4, + width: 459, + height: 459, + }, + { + src: elf5, + width: 545, + height: 545, + }, + { + src: elf6, + width: 400, + height: 503, + }, +] + +type ElfProps = { + variant: number + width?: string + style?: object + animated?: boolean +} + +export function Elf({ variant, width, animated, ...props }: ElfProps) { + const [animDuration] = useState(6 + 6 * Math.random()) + return ( + + ) +} + +const hueAnim = keyframes` + 0% { filter: hue-rotate(0deg); } + 100% { filter: hue-rotate(360deg); } +` + +export const ElfImage = styled.img<{ $width?: string; $animated?: boolean; $animDuration: number }>` + position: absolute; + width: ${({ $width = 'auto' }) => $width}; + height: auto; + pointer-events: none; + ${({ $animated, $animDuration }) => + $animated && + css` + animation: ${hueAnim} ${$animDuration.toFixed(2)}s linear infinite; + `} +` diff --git a/src/components/BrandElements/FloatingElements.tsx b/src/components/BrandElements/FloatingElements.tsx new file mode 100644 index 00000000..d5da022a --- /dev/null +++ b/src/components/BrandElements/FloatingElements.tsx @@ -0,0 +1,59 @@ +import type { SplashImage, TokenKey } from '~/types' + +import { Cloud } from './Cloud' +import { Elf } from './Elf' +import { HaiCoin } from './HaiCoin' + +export type FloatingElementsProps = { + elves?: SplashImage[] + clouds?: SplashImage[] + coins?: (Omit & { + index: number | TokenKey + thickness?: number + })[] +} +export function FloatingElements({ elves, clouds, coins }: FloatingElementsProps) { + return ( + <> + {(elves || []).map(({ index, width, style, rotation = 0, flip, zIndex = 0 }, i) => ( + + ))} + {(clouds || []).map(({ index, width, style, rotation = 0, flip, zIndex = 0 }, i) => ( + + ))} + {(coins || []).map(({ index, width, style, rotation = 0, zIndex = 0, thickness }, i) => ( + + ))} + + ) +} diff --git a/src/components/BrandElements/HaiCoin.tsx b/src/components/BrandElements/HaiCoin.tsx new file mode 100644 index 00000000..7e37465d --- /dev/null +++ b/src/components/BrandElements/HaiCoin.tsx @@ -0,0 +1,124 @@ +import { HTMLProps, useState } from 'react' + +import type { TokenKey } from '~/types' +import { TOKEN_LOGOS } from '~/utils' + +import styled, { css, keyframes } from 'styled-components' +import { CenteredFlex } from '~/styles' + +type HaiCoinProps = Omit, 'ref' | 'as'> & { + variant?: TokenKey + width?: string + animated?: boolean + thickness?: number + rotateOnAxis?: number +} + +export function HaiCoin({ variant = 'HAI', width, animated, thickness, rotateOnAxis, ...props }: HaiCoinProps) { + const [animDuration] = useState(1.5 + 0.75 * Math.random()) + + return ( + + + + + + + + + + ) +} + +const rotate = keyframes` + 0% { transform: rotateY(-45deg); } + 100% { transform: rotateY(45deg); } +` + +export const HaiCoinImage = styled(CenteredFlex)<{ $width?: string }>` + position: absolute; + width: ${({ $width = 'auto' }) => $width}; + height: ${({ $width = 'auto' }) => $width}; + pointer-events: none; +` +const Face = styled(CenteredFlex)<{ $thickness?: number }>` + position: absolute; + width: 100%; + height: 100%; + border-radius: 50%; + background-color: ${({ theme }) => theme.colors.greenish}; + transform: translateZ(${({ $thickness = 12 }) => $thickness}px); + z-index: 3; + + & svg, + & img { + width: 80%; + height: 80%; + } +` +const BackFace = styled(Face)` + background-color: #b2e3ad; + transform: translateZ(-${({ $thickness = 12 }) => $thickness}px); + z-index: 1; +` +const InsidePiece = styled.div<{ $thickness?: number }>` + position: absolute; + width: ${({ $thickness = 12 }) => 2 * $thickness}px; + height: 100%; + transform: rotateY(90deg); + z-index: 2; +` +const Inner = styled(CenteredFlex)<{ + $variant?: HaiCoinProps['variant'] + $animated?: boolean + $animDur: number + $rotateOnAxis?: number +}>` + width: 100%; + height: 100%; + perspective: 1000px; + transform-style: preserve-3d; + ${({ $animated, $animDur, $rotateOnAxis }) => + $rotateOnAxis + ? css` + transform: rotateY(${$rotateOnAxis}deg); + ` + : $animated && + css` + animation: ${rotate} ${$animDur}s ease-in-out infinite alternate; + `} + + ${({ theme, $variant = 'HAI' }) => { + let frontColor = theme.colors.greenish + let backColor = '#B2E3AD' + switch ($variant) { + case 'KITE': { + frontColor = '#EECABC' + backColor = '#D6B5A8' + break + } + case 'OP': { + frontColor = '#FF0000' + backColor = '#DD0000' + break + } + case 'HAI': + default: { + frontColor = theme.colors.greenish + backColor = '#B2E3AD' + break + } + } + return css` + & ${Face} { + background-color: ${frontColor}; + } + & ${BackFace} { + background-color: ${backColor}; + } + & ${InsidePiece} { + background-color: ${backColor}; + } + ` + }} +` diff --git a/src/components/BrandedDropdown.tsx b/src/components/BrandedDropdown.tsx new file mode 100644 index 00000000..9aed4f90 --- /dev/null +++ b/src/components/BrandedDropdown.tsx @@ -0,0 +1,76 @@ +import { type HTMLProps, useState } from 'react' + +import type { ReactChildren } from '~/types' +import { useOutsideClick } from '~/hooks' + +import styled, { css } from 'styled-components' +import { CenteredFlex, Flex, HaiButton, type HaiButtonProps, Popout } from '~/styles' +import { Caret } from './Icons/Caret' + +type ButtonProps = Omit, 'ref' | 'as' | 'type' | 'label' | 'children'> +type BrandedDropdownProps = ButtonProps & + HaiButtonProps & { + width?: string + label: ReactChildren + children: ReactChildren + } +export function BrandedDropdown({ width, label, children, ...props }: BrandedDropdownProps) { + const [container, setContainer] = useState(null) + const [expanded, setExpanded] = useState(false) + + useOutsideClick(container, () => setExpanded(false)) + + return ( + setExpanded((e) => !e)}> + {label} + + + + + + ) +} + +const Container = styled(HaiButton)` + position: relative; + height: 48px; + z-index: 1; +` + +const IconContainer = styled(CenteredFlex)<{ $rotate?: boolean }>` + transition: all 0.5s ease; + transform: ${({ $rotate }) => ($rotate ? 'rotate(-180deg)' : 'rotate(0deg)')}; + margin-left: 12px; +` + +const Dropdown = styled(Popout)` + width: ${({ $width = 'fit-content' }) => $width}; + padding: 24px; + margin-right: -16px; + gap: 12px; + z-index: 2; +` + +export const DropdownOption = styled(Flex).attrs((props) => ({ + $width: '100%', + $align: 'center', + $gap: 12, + ...props, +}))<{ $active?: boolean }>` + min-width: 160px; + padding: 8px 16px; + border-radius: 999px; + border: 2px solid rgba(0, 0, 0, 0.1); + cursor: pointer; + + ${({ $active }) => + !!$active && + css` + background-color: rgba(0, 0, 0, 0.1); + `} + &:hover { + background-color: rgba(0, 0, 0, 0.1); + } +` diff --git a/src/components/BrandedSelect.tsx b/src/components/BrandedSelect.tsx new file mode 100644 index 00000000..9a2aee97 --- /dev/null +++ b/src/components/BrandedSelect.tsx @@ -0,0 +1,201 @@ +import { useState, type ChangeEvent, useEffect } from 'react' + +import { useOutsideClick } from '~/hooks' + +import styled from 'styled-components' +import { CenteredFlex, Popout, type TextProps, Title, Flex, Text } from '~/styles' +import { ExternalLink } from './ExternalLink' +import { CaretWithOutline } from './Icons/CaretWithOutline' + +type BrandedSelectOption = { + label: string + value: string + icon?: JSX.Element | string | (JSX.Element | string)[] + description?: string + href?: string +} + +type BrandedSelectProps = TextProps & { + width?: string + options: BrandedSelectOption[] + value: string + onChange: (value: string) => void +} +export function BrandedSelect({ width, options, value, onChange, ...props }: BrandedSelectProps) { + const [container, setContainer] = useState(null) + const [persistent, setPersistent] = useState(false) + // const [active, setActive] = useState(false) + + useOutsideClick(container, () => setPersistent(false)) + + return ( + setActive(true)} + // onPointerLeave={() => setActive(false)} + onClick={() => setPersistent((p) => !p)} + > + + {options.find(({ value: v }) => v === value)?.label.toUpperCase()} + + + + + + + + ) +} + +const Container = styled(CenteredFlex)` + position: relative; + min-height: 80px; + padding: 0 12px; + border-bottom: 2px solid rgba(0, 0, 0, 0.1); + cursor: pointer; +` +const HiddenText = styled(Title)` + visibility: hidden; + white-space: pre-wrap; +` + +const Select = styled(Title)` + position: absolute; + left: 12px; + appearance: none; + -webkit-appearance: none; + width: 100%; + background-color: transparent; + outline: none; + border: none; + pointer-events: none; + white-space: pre-wrap; +` +const IconContainer = styled(CenteredFlex)<{ $active?: boolean }>` + margin-top: 8px; + transition: all 0.5s ease; + transform: rotate(${({ $active }) => ($active ? -180 : 0)}deg); + & > svg { + pointer-events: none; + width: 32px; + height: auto; + fill: ${({ theme }) => theme.colors.yellowish}; + stroke: black; + stroke-width: 1px; + } +` + +const Dropdown = styled(Popout)` + width: 400px; + max-width: calc(100vw - 72px); + padding: 24px; + margin-right: -14px; + gap: 12px; + cursor: default; +` +const DropdownOption = styled(Flex).attrs((props) => ({ + $width: '100%', + $justify: 'flex-start', + $align: 'center', + $gap: 12, + ...props, +}))<{ $active?: boolean }>` + padding: 12px; + border-radius: 12px; + border: 1px solid rgba(0, 0, 0, 0.1); + font-size: 0.8em; + cursor: pointer; + + background-color: ${({ $active = false }) => ($active ? 'rgba(0,0,0,0.05)' : 'transparent')}; + + &:hover { + background-color: rgba(0, 0, 0, 0.05); + } +` +const DropdownIconContainer = styled(CenteredFlex)` + width: 52px; + height: 52px; + border-radius: 50%; + flex-shrink: 0; + border: ${({ theme }) => theme.border.medium}; + background-color: ${({ theme }) => theme.colors.greenish}; + overflow: hidden; + + & > img { + width: 52px; + height: 52px; + } +` + +function DropdownIcon({ icon }: { icon: BrandedSelectOption['icon'] }) { + const [currentIcon, setCurrentIcon] = useState(Array.isArray(icon) ? icon[0] : icon) + + useEffect(() => { + if (!Array.isArray(icon)) return + + let index = 0 + const int = setInterval(() => { + index++ + setCurrentIcon(icon[index % icon.length]) + }, 3000) + + return () => clearInterval(int) + }, [icon]) + + return ( + + {typeof currentIcon === 'string' ? : currentIcon} + + ) +} diff --git a/src/components/BrandedTitle.tsx b/src/components/BrandedTitle.tsx new file mode 100644 index 00000000..d9ee50fe --- /dev/null +++ b/src/components/BrandedTitle.tsx @@ -0,0 +1,19 @@ +import { type TextProps, Title, Text } from '~/styles' + +const colors = ['pinkish', 'greenish', 'blueish', 'orangeish'] + +type BrandedTitleProps = TextProps & { + textContent: string + colorOffset?: number +} +export function BrandedTitle({ textContent, colorOffset = 0, ...props }: BrandedTitleProps) { + return ( + + {textContent.split(' ').map((str, i) => ( + <Text key={i} as="span" $color={colors[(i + colorOffset) % colors.length]}> + {str + ' '} + </Text> + ))} + + ) +} diff --git a/src/components/Button.tsx b/src/components/Button.tsx deleted file mode 100644 index 835b4968..00000000 --- a/src/components/Button.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import React, { ReactNode } from 'react' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' -import classNames from 'classnames' -import darkArrow from '../assets/dark-arrow.svg' -import Arrow from './Icons/Arrow' -import Loader from './Loader' - -interface Props extends React.HTMLAttributes { - text?: string - onClick?: (event?: React.MouseEvent) => void - primary?: boolean - secondary?: boolean - dimmed?: boolean - dimmedNormal?: boolean - withArrow?: boolean - disabled?: boolean - isLoading?: boolean - dimmedWithArrow?: boolean - isBordered?: boolean - arrowPlacement?: string - children?: ReactNode -} - -const Button = ({ - text, - onClick, - dimmed, - dimmedNormal, - primary, - secondary, - withArrow, - disabled, - isLoading, - dimmedWithArrow, - isBordered, - arrowPlacement = 'left', - children, - ...rest -}: Props) => { - const { t } = useTranslation() - - const classes = classNames({ - primary, - secondary, - dimmedNormal, - }) - - const returnType = () => { - if (dimmed) { - return ( - - {text && t(text)} - - ) - } - - if (dimmedWithArrow) { - return ( - - {arrowPlacement === 'left' ? {''} : null} - {text && t(text)} - {arrowPlacement === 'right' ? {''} : null} - - ) - } else if (withArrow) { - return ( - - {text && t(text)} - - ) - } else if (isBordered) { - return ( - - {text && t(text)} - - ) - } else { - return ( - - {text && t(text)} - {children || null} - {isLoading && } - - ) - } - } - return returnType() -} - -export default React.memo(Button) - -const Container = styled.button<{ isLoading?: boolean }>` - outline: none; - cursor: pointer; - min-width: 134px; - border: none; - box-shadow: none; - padding: 8px 30px; - line-height: 24px; - font-size: ${(props) => props.theme.font.small}; - font-weight: 600; - color: ${(props) => props.theme.colors.neutral}; - background: ${(props) => props.theme.colors.blueish}; - border-radius: 50px; - transition: all 0.3s ease; - &.dimmedNormal { - background: ${(props) => props.theme.colors.secondary}; - } - &.primary { - background: ${(props) => props.theme.colors.colorPrimary}; - } - &.secondary { - background: ${(props) => props.theme.colors.colorSecondary}; - } - &:hover { - opacity: 0.8; - } - - &:disabled { - background: ${(props) => (props.isLoading ? props.theme.colors.placeholder : props.theme.colors.secondary)}; - cursor: not-allowed; - } -` - -const DimmedBtn = styled.button` - cursor: pointer; - border: none; - box-shadow: none; - outline: none; - background: transparent; - border-radius: 0; - color: ${(props) => props.theme.colors.secondary}; - font-size: ${(props) => props.theme.font.small}; - font-weight: 600; - line-height: 24px; - padding: 0; - margin: 0; - display: flex; - align-items: center; - img { - margin-right: 3px; - &.rotate { - transform: rotate(180deg); - margin-right: 0; - margin-left: 3px; - } - } - transition: all 0.3s ease; - &:hover { - opacity: 0.8; - } - &:disabled { - cursor: not-allowed; - } -` - -const ArrowBtn = styled.button` - span { - background: ${(props) => props.theme.colors.gradient}; - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - color: ${(props) => props.theme.colors.inputBorderColor}; - } - background: transparent; - border: 0; - cursor: pointer; - box-shadow: none; - outline: none; - padding: 0; - margin: 0; - font-size: ${(props) => props.theme.font.small}; - font-weight: 600; - line-height: 24px; - letter-spacing: -0.18px; - transition: all 0.3s ease; - - &:disabled { - cursor: not-allowed; - opacity: 0.5; - &:hover { - opacity: 0.5; - } - } - &:hover { - opacity: 0.8; - } -` - -const BorderedBtn = styled.button` - background: ${(props) => props.theme.colors.gradient}; - padding: 2px; - border-radius: 25px; - box-shadow: none; - outline: none; - border: 0; - cursor: pointer; - &:disabled { - cursor: not-allowed; - } -` - -const Inner = styled.div` - background: ${(props) => props.theme.colors.background}; - color: ${(props) => props.theme.colors.inputBorderColor}; - border-radius: 25px; - padding: 4px 6px; - transition: all 0.3s ease; - &:hover { - opacity: 0.8; - } -` diff --git a/src/components/Charts/ChartTooltip.tsx b/src/components/Charts/ChartTooltip.tsx new file mode 100644 index 00000000..e76dcbb0 --- /dev/null +++ b/src/components/Charts/ChartTooltip.tsx @@ -0,0 +1,54 @@ +import { type HTMLProps, forwardRef } from 'react' + +import styled from 'styled-components' +import { CenteredFlex, Popout, Text } from '~/styles' + +type ChartTooltipProps = Omit, 'children' | 'ref' | 'as' | 'label'> & { + heading: string | number + subHeading: string | number + label?: string | number + color: string + size?: number + active?: boolean +} +export const ChartTooltip = forwardRef( + ({ heading, subHeading, label, color, size = 0, active = false, ...props }, ref) => ( + + + + ) +) + +const PopoutContainer = styled(CenteredFlex)<{ $size: number }>` + width: ${({ $size }) => $size}px; + height: ${({ $size }) => $size}px; + position: absolute; + overflow: visible; + + z-index: 2; +` +const GraphPopout = styled(Popout).attrs((props) => ({ + $width: 'auto', + $anchor: 'bottom', + $margin: '20px', + $gap: 4, + $shrink: 0, + ...props, +}))` + min-width: fit-content; + padding: 12px 24px; + & > ${Text} { + white-space: nowrap; + &:nth-child(2) { + filter: brightness(75%); + } + } +` diff --git a/src/components/Charts/Legend.tsx b/src/components/Charts/Legend.tsx new file mode 100644 index 00000000..e761576f --- /dev/null +++ b/src/components/Charts/Legend.tsx @@ -0,0 +1,45 @@ +import { type CSSProperties } from 'react' + +import styled from 'styled-components' +import { Flex, type FlexProps, Text } from '~/styles' + +type LegendProps = FlexProps & { + data: { + id: string + label?: string + color: string + }[] + style?: CSSProperties +} +export const Legend = ({ data, ...props }: LegendProps) => ( + + {data.map(({ id, label, color }) => ( + + {label || id} + + ))} + +) + +const Container = styled(Flex)` + position: absolute; + top: 24px; + left: 24px; + pointer-events: none; +` + +const Entry = styled(Text)` + width: 100%; + height: 24px; + padding: 0 16px; + border-radius: 999px; + background-color: white; + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.1); + line-height: 24px; + font-size: 0.7rem; + font-weight: 700; + + & > ${Text} { + filter: brightness(75%); + } +` diff --git a/src/components/Charts/Line/BorderedLine.tsx b/src/components/Charts/Line/BorderedLine.tsx new file mode 100644 index 00000000..eaf2517f --- /dev/null +++ b/src/components/Charts/Line/BorderedLine.tsx @@ -0,0 +1,35 @@ +import { Fragment } from 'react' +import { type CustomLayer } from '@nivo/line' + +export const BorderedLine: CustomLayer = ({ series, lineGenerator, xScale, yScale }) => { + return series.map(({ id, data, color }) => { + const line = + lineGenerator( + data.map((d) => ({ + x: (xScale as any)(d.data.x), + y: (yScale as any)(d.data.y), + })) + ) ?? undefined + + return ( + + + + + ) + }) +} diff --git a/src/components/Charts/Line/PointWithPopout.tsx b/src/components/Charts/Line/PointWithPopout.tsx new file mode 100644 index 00000000..8cb882de --- /dev/null +++ b/src/components/Charts/Line/PointWithPopout.tsx @@ -0,0 +1,71 @@ +import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' +import { type Point } from '@nivo/line' + +import styled from 'styled-components' +import { ChartTooltip } from '../ChartTooltip' + +type PointProps = Point & { + formatX?: (value: string | number) => string + formatY?: (value: string | number) => string +} +export const PointWithPopout = ({ serieId, x, y, data, serieColor, formatX, formatY }: PointProps) => { + const [hovered, setHovered] = useState(false) + const [circle, setCircle] = useState(null) + const [container, setContainer] = useState(null) + + useEffect(() => { + if (!circle || !container) return + + const onResize = () => { + const { left, top, width, height } = circle.getBoundingClientRect() + Object.assign(container.style, { + top: `${top + window.scrollY}px`, + left: `${left}px`, + width: `${width}px`, + height: `${height}px`, + }) + } + onResize() + window.addEventListener('resize', onResize) + + return () => window.removeEventListener('resize', onResize) + }, [circle, container, x, y]) + + return ( + <> + + {createPortal( + setHovered(true)} + onPointerLeave={() => setHovered(false)} + active={hovered} + heading={formatY ? formatY(data.yFormatted) : data.yFormatted} + subHeading={serieId} + label={formatX ? formatX(data.xFormatted) : data.xFormatted} + color={serieColor} + size={10} + />, + document.body + )} + + ) +} + +const Circle = styled.circle<{ $hovered?: boolean }>` + opacity: ${({ $hovered }) => ($hovered ? 1 : 0)}; + &:hover { + opacity: 1; + } +` diff --git a/src/components/Charts/Line/index.tsx b/src/components/Charts/Line/index.tsx new file mode 100644 index 00000000..a27bc4d8 --- /dev/null +++ b/src/components/Charts/Line/index.tsx @@ -0,0 +1,107 @@ +import dayjs from 'dayjs' +import { type LineProps, ResponsiveLine } from '@nivo/line' + +import { Timeframe } from '~/utils' + +import { PointWithPopout } from './PointWithPopout' +import { BorderedLine } from './BorderedLine' + +const formatMap: Record = { + [Timeframe.ONE_DAY]: { + format: `M/D HH:mm`, + tickValues: 4, + }, + [Timeframe.ONE_WEEK]: { + format: `M/D`, + tickValues: 7, + }, + [Timeframe.ONE_MONTH]: { + format: `M/D`, + tickValues: 4, + }, + [Timeframe.ONE_YEAR]: { + format: `'YY/M/D`, + tickValues: 4, + }, +} + +type LineChartProps = LineProps & { + timeframe: Timeframe +} +export function LineChart({ data, timeframe, axisBottom, xScale, axisRight, yScale, ...props }: LineChartProps) { + const { format, tickValues } = formatMap[timeframe] + + return ( + d.color} + xScale={{ + type: 'time', + ...xScale, + }} + axisTop={null} + axisBottom={{ + tickSize: 0, + tickValues, + format: (value) => { + const time = new Date(value).getTime() / 1000 + return dayjs.unix(time).format(format) + }, + ...axisBottom, + }} + yScale={{ + type: 'linear', + ...yScale, + }} + axisLeft={{ + tickSize: 0, + tickValues: 0, + tickPadding: -50, + }} + axisRight={{ + tickSize: 0, + tickValues: 5, + tickPadding: -50, + ...axisRight, + }} + margin={{ + top: 32, + left: 0, + right: 0, + bottom: 32, + }} + lineWidth={10} + enablePoints={false} + enableGridX={false} + enableGridY={false} + // enableSlices="x" + // enableCrosshair + // crosshairType="bottom-right" + {...props} + layers={[ + // 'markers', + // 'areas', + 'axes', + // 'lines', + BorderedLine, + // 'slices', + // 'crosshair', + // 'points', + ({ points }) => { + return points.map((point) => ( + { + const time = new Date(value).getTime() / 1000 + return dayjs.unix(time).format(format) + }} + formatY={axisRight?.format as any} + /> + )) + }, + 'legends', + ]} + /> + ) +} diff --git a/src/components/Charts/Line/useDummyData.tsx b/src/components/Charts/Line/useDummyData.tsx new file mode 100644 index 00000000..1f3b7acf --- /dev/null +++ b/src/components/Charts/Line/useDummyData.tsx @@ -0,0 +1,56 @@ +import { useMemo } from 'react' + +import { ONE_DAY_MS, ONE_HOUR_MS, Timeframe } from '~/utils' + +// DEV only +type Options = { + timeframe?: Timeframe + min?: number + max?: number + enabled?: boolean +} +export function useDummyData(baseData: any[], options: Options = {}) { + const data = useMemo(() => { + const { timeframe = Timeframe.ONE_WEEK, min = 0, max = 1, enabled = true } = options + + if (!enabled) return [] + + const now = Date.now() + switch (timeframe) { + case Timeframe.ONE_DAY: + return baseData.map((d) => ({ + ...d, + data: Array.from({ length: 12 }, (_, i) => ({ + x: new Date(now - (12 - i) * 2 * ONE_HOUR_MS), + y: min + (max - min) * Math.random(), + })), + })) + case Timeframe.ONE_WEEK: + return baseData.map((d) => ({ + ...d, + data: Array.from({ length: 7 }, (_, i) => ({ + x: new Date(now - (7 - i) * ONE_DAY_MS), + y: min + (max - min) * Math.random(), + })), + })) + case Timeframe.ONE_MONTH: + return baseData.map((d) => ({ + ...d, + data: Array.from({ length: 4 }, (_, i) => ({ + x: new Date(now - (4 - i) * 7 * ONE_DAY_MS), + y: min + (max - min) * Math.random(), + })), + })) + case Timeframe.ONE_YEAR: + return baseData.map((d) => ({ + ...d, + data: Array.from({ length: 12 }, (_, i) => ({ + x: new Date(now - (12 - i) * 30 * ONE_DAY_MS), + y: min + (max - min) * Math.random(), + })), + })) + } + }, [baseData, options]) + + return data +} diff --git a/src/components/Charts/Pie/Label.tsx b/src/components/Charts/Pie/Label.tsx new file mode 100644 index 00000000..b74d66e1 --- /dev/null +++ b/src/components/Charts/Pie/Label.tsx @@ -0,0 +1,24 @@ +import { animated } from '@react-spring/web' + +type LabelProps = { + datum: { + value: number + } + style: any + total: number +} +export const Label = ({ datum, style, total }: LabelProps) => ( + + + {parseFloat(((100 * datum.value) / total).toFixed(1))}% + + +) diff --git a/src/components/Charts/Pie/index.tsx b/src/components/Charts/Pie/index.tsx new file mode 100644 index 00000000..6b54d766 --- /dev/null +++ b/src/components/Charts/Pie/index.tsx @@ -0,0 +1,47 @@ +import { ResponsivePie, type PieSvgProps } from '@nivo/pie' + +import { Label } from './Label' +import { ChartTooltip } from '../ChartTooltip' + +export type PieChartDatum = { + id: string + label?: string + value: number + color: string +} +type PieChartProps = Omit, 'width' | 'height'> +export function PieChart({ data, valueFormat, ...props }: PieChartProps) { + return ( + d.color)} + borderColor="black" + borderWidth={2} + margin={{ + top: 12, + left: 24, + right: 24, + bottom: 12, + }} + innerRadius={0.5} + arcLabelsComponent={(labelProps) => ( +