diff --git a/package.json b/package.json index b2cc64d..f1d7809 100755 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "license": "GPL-3.0-or-later", "dependencies": { "@jediswap/token-lists": "^1.0.0-beta.1", + "@szhsin/react-accordion": "^1.2.3", "babel-plugin-styled-components": "^2.0.7", "lodash": "^4.17.21", "react-countdown": "^2.3.5", diff --git a/src/App.js b/src/App.js index d66f860..10e9674 100755 --- a/src/App.js +++ b/src/App.js @@ -3,6 +3,9 @@ import styled from 'styled-components' import { Route, Switch, BrowserRouter, Redirect } from 'react-router-dom' import { ApolloProvider } from 'react-apollo' import { isEmpty } from 'lodash' +import VolumeContestLookup from './pages/VolumeContestLookup' +import VolumeContestAccountPage from './pages/VolumeContestPage' + import { jediSwapClient } from './apollo/client' import GlobalPage from './pages/GlobalPage' import TokenPage from './pages/TokenPage' @@ -226,6 +229,29 @@ function App() { + { + if (isStarknetAddress(match.params.accountAddress.toLowerCase())) { + return ( + + + + ) + } else { + return + } + }} + /> + + + + + + + diff --git a/src/Theme/index.js b/src/Theme/index.js index 979e1df..8c52dea 100755 --- a/src/Theme/index.js +++ b/src/Theme/index.js @@ -18,6 +18,7 @@ const theme = (darkMode, color) => ({ backgroundColor: darkMode ? '#252323' : '#F7F8FA', uniswapPink: darkMode ? '#FF00E9' : 'black', + jediGray: darkMode ? '#959595' : 'black', concreteGray: darkMode ? '#292C2F' : '#FAFAFA', inputBackground: darkMode ? '#1F1F1F' : '#FAFAFA', @@ -147,8 +148,7 @@ export const ThemedBackground = styled.div` max-width: 100vw !important; height: 200vh; mix-blend-mode: color; - background: ${({ backgroundColor }) => - `radial-gradient(50% 50% at 50% 50%, ${backgroundColor} 0%, rgba(255, 255, 255, 0) 100%)`}; + background: ${({ backgroundColor }) => `radial-gradient(50% 50% at 50% 50%, ${backgroundColor} 0%, rgba(255, 255, 255, 0) 100%)`}; position: absolute; top: 0px; left: 0px; diff --git a/src/apollo/queries.js b/src/apollo/queries.js index dc49d16..583857d 100755 --- a/src/apollo/queries.js +++ b/src/apollo/queries.js @@ -198,6 +198,7 @@ export const USER_LP_CONTEST_HISTORY = gql` } } ` + export const USER_LP_CONTEST_PERCENTILE = gql` query lpContestPercentile($user: String!) { lpContestPercentile(where: { user: $user }) { @@ -366,6 +367,30 @@ export const USER_LP_CONTEST_TRANSACTIONS = gql` } ` +export const USER_VOLUME_CONTEST_TRANSACTIONS = gql` + query transactions($user: String!) { + swaps(orderBy: "timestamp", orderByDirection: "desc", where: { to: $user }) { + id + transactionHash + timestamp + pair { + token0 { + symbol + } + token1 { + symbol + } + } + amount0In + amount0Out + amount1In + amount1Out + amountUSD + to + } + } +` + export const PAIR_CHART = gql` query pairDayDatas($pairAddress: String!, $skip: Int!) { pairDayDatas(first: 1000, skip: $skip, orderBy: "date", orderByDirection: "asc", where: { pair: $pairAddress }) { @@ -653,6 +678,27 @@ export const LP_CONTEST_DATA = gql` } ` +export const USER_VOLUME_CONTEST_DATA = (account) => { + const queryString = ` + query VolumeContest { + volumeContest(where: {user: "${account}", startDate: "2023-09-04"}) { + nftLevel + totalContestScore + totalContestVolume + weeks { + endDt + id + name + score + startDt + volume + } + } + } + ` + return gql(queryString) +} + export const LP_CONTEST_NFT_RANK = gql` query lpcontestnftrank { lpContestNftRank { @@ -802,7 +848,7 @@ export const TOKEN_DATA = (tokenAddress, block) => { } export const FILTERED_TRANSACTIONS = gql` - query ($allPairs: [String!]) { + query($allPairs: [String!]) { mints(first: 20, where: { pairIn: $allPairs }, orderBy: "timestamp", orderByDirection: "desc") { transactionHash timestamp diff --git a/src/assets/banners/cup.png b/src/assets/banners/cup.png new file mode 100644 index 0000000..fbe89ca Binary files /dev/null and b/src/assets/banners/cup.png differ diff --git a/src/assets/banners/cup@x2.png b/src/assets/banners/cup@x2.png new file mode 100644 index 0000000..14197af Binary files /dev/null and b/src/assets/banners/cup@x2.png differ diff --git a/src/assets/banners/digital_wallet.png b/src/assets/banners/digital_wallet.png new file mode 100644 index 0000000..657484d Binary files /dev/null and b/src/assets/banners/digital_wallet.png differ diff --git a/src/assets/banners/digital_wallet@x2.png b/src/assets/banners/digital_wallet@x2.png new file mode 100644 index 0000000..9b34f39 Binary files /dev/null and b/src/assets/banners/digital_wallet@x2.png differ diff --git a/src/assets/banners/nft.png b/src/assets/banners/nft.png new file mode 100644 index 0000000..791bac8 Binary files /dev/null and b/src/assets/banners/nft.png differ diff --git a/src/assets/banners/nft@x2.png b/src/assets/banners/nft@x2.png new file mode 100644 index 0000000..40fe919 Binary files /dev/null and b/src/assets/banners/nft@x2.png differ diff --git a/src/components/Banner/index.js b/src/components/Banner/index.js index 87942bd..1973c81 100644 --- a/src/components/Banner/index.js +++ b/src/components/Banner/index.js @@ -1,31 +1,32 @@ import React from 'react' import styled from 'styled-components' -import Panel from "../Panel"; +import Panel from '../Panel' const PollingDot = styled.div` width: 13px; height: 13px; border-radius: 50%; background-color: ${({ theme }) => theme.green2}; -`; +` -const Title = styled.div` +export const Title = styled.div` display: flex; align-items: center; font-size: 16px; font-weight: 500; margin-bottom: 14px; -`; +` const TitleIconWrapper = styled.div` display: flex; - margin-right: .75rem; -`; + margin-right: 0.75rem; +` const Wrapper = styled(Panel)` padding: 24px; position: relative; color: #fff; + overflow: hidden; ${PollingDot} { position: absolute; @@ -36,27 +37,45 @@ const Wrapper = styled(Panel)` @media screen and (max-width: 800px) { padding: 14px; } -`; +` const Content = styled.div` font-weight: 700; font-size: 26px; -`; +` +const MainContent = styled.div` + position: relative; + z-index: 2; +` +export const DecorationWrapper = styled.div` + position: absolute; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 100%; -export function Banner({title, titleIcon = null, content, showPollingDot}) { - return ( - - - {titleIcon && (<TitleIconWrapper>{titleIcon}</TitleIconWrapper>)} - {title} - - - {content} - + & > * { + position: absolute; + bottom: 0; + right: 0; + } +` - {showPollingDot && ()} - - ) -} +export function Banner({ className, title, titleIcon = null, decoration = null, content, showPollingDot = false, style = {} }) { + return ( + + + + {titleIcon && <TitleIconWrapper>{titleIcon}</TitleIconWrapper>} + {title} + + {content} + {showPollingDot && } + + {decoration && {decoration}} + + ) +} diff --git a/src/components/ButtonStyled/index.js b/src/components/ButtonStyled/index.js index 1e06b23..47fae36 100755 --- a/src/components/ButtonStyled/index.js +++ b/src/components/ButtonStyled/index.js @@ -72,8 +72,29 @@ export const ButtonLight = styled(Base)` } :hover { - background-color: ${({ color, theme }) => - color ? transparentize(0.8, color) : transparentize(0.8, theme.primary1)}; + background-color: ${({ color, theme }) => (color ? transparentize(0.8, color) : transparentize(0.8, theme.primary1))}; + } +` + +export const ButtonGradient = styled(Base)` + background: linear-gradient(95deg, #29aafd 8%, #ff00e9 105%); + color: #fff; + transition: background-position 0.1s; + font-size: 16px; + font-weight: 750; + border: none; + white-space: nowrap; + + &:hover { + background-position: 100%; + } + + &[disabled] { + cursor: default; + background: rgba(196, 196, 196, 0.01); + box-shadow: inset 0px -63.1213px 52.3445px -49.2654px rgba(96, 68, 145, 0.3), inset 0px 75.4377px 76.9772px -36.9491px rgba(202, 172, 255, 0.3), + inset 0px 3.07909px 13.8559px rgba(154, 146, 210, 0.3), inset 0px 0.769772px 30.7909px rgba(227, 222, 255, 0.2); + overflow: hidden; } ` @@ -103,13 +124,17 @@ export const ButtonDark = styled(Base)` border-radius: 12px; white-space: nowrap; - ${(props) => !props.disabled && ` + ${(props) => + !props.disabled && + ` :hover { background-color: ${({ color, theme }) => (color ? darken(0.1, color) : darken(0.1, theme.primary1))}; } `} - - ${(props) => props.disabled && ` + + ${(props) => + props.disabled && + ` opacity: 0.5; cursor: default; `} diff --git a/src/components/FAQ/index.js b/src/components/FAQ/index.js new file mode 100755 index 0000000..51404c6 --- /dev/null +++ b/src/components/FAQ/index.js @@ -0,0 +1,140 @@ +import React from 'react' +import styled, { css } from 'styled-components' +import { Accordion as AccordionWrapper, AccordionItem as Item } from '@szhsin/react-accordion' +import { ChevronDown } from 'react-feather' + +import { TYPE } from '../../Theme' +import { AutoRow } from '../Row' + +const Wrapper = styled.div`` + +const ItemWithChevron = ({ header, ...rest }) => ( + + + + + {header} + + } + /> +) + +const Accordion = styled(AccordionWrapper)` + flex-grow: 1; +` + +const AccordionsLine = styled(AutoRow)` + ${Accordion} { + width: calc(100% / ${(props) => props.columnsAmount || 1}); + } + @media screen and (max-width: ${(props) => (props.columnsAmount === 3 ? '1200px' : props.columnsAmount === 2 ? '960px' : '0px')}) { + flex-direction: column; + + ${Accordion} { + width: 100%; + margin-bottom: 0 !important; + } + + ${Accordion} + ${Accordion} { + margin-top: 0 !important; + } + } +` + +const AccordionItem = styled(ItemWithChevron)` + --activeBackgroundColor: rgba(255, 255, 255, 0.05); + + & { + background: transparent; + border-radius: 8px; + transition: all 0.25s; + margin-bottom: 12px; + } + + &:hover { + background: var(--activeBackgroundColor); + } + + .szh-accordion__item-btn { + display: flex; + width: 100%; + padding: 12px; + background: transparent; + cursor: pointer; + border: none; + color: #fff; + font-size: 16px; + font-weight: 700; + align-items: center; + text-align: left; + } + + .szh-accordion__item-btn .chevron-wrapper { + margin-right: 10px; + display: flex; + } + + .szh-accordion__item-content { + transition: height 0.25s cubic-bezier(0, 0, 0, 1); + } + + .szh-accordion__item-panel { + padding: 1rem; + } + + svg { + margin-left: auto; + transition: transform 0.25s cubic-bezier(0, 0, 0, 1); + } + + &.szh-accordion__item--expanded { + background: var(--activeBackgroundColor); + + svg { + transform: rotate(180deg); + } + } +` + +const MAX_COLUMNS = 3 +const DEFAULT_ACCORDION_PROPS = { + transition: true, + transitionTimeout: 250, + allowMultiple: true, +} + +const FAQ = ({ items = [], columns = 1, accordionProps = {} }) => { + const columnsAmount = Math.min(Math.abs(columns), MAX_COLUMNS) || 1 + const columnSize = Math.ceil(items.length / columnsAmount) + + const mergedAccordionProps = { + ...DEFAULT_ACCORDION_PROPS, + ...accordionProps, + } + + if (!items?.length) { + return null + } + return ( + + + {Array.from({ length: columnsAmount }).map((n, i) => { + return ( + + {items.slice(i * columnSize, (i + 1) * columnSize).map(({ header, content }, i) => ( + + {content} + + ))} + + ) + })} + + + ) +} + +export default FAQ diff --git a/src/components/Panel/index.js b/src/components/Panel/index.js index af41ecb..eb32855 100755 --- a/src/components/Panel/index.js +++ b/src/components/Panel/index.js @@ -27,10 +27,8 @@ const Panel = styled(RebassBox)` flex-direction: column; justify-content: flex-start; border-radius: 8px; - box-shadow: rgb(255 255 255 / 50%) 0px 30.0211px 43.1072px -27.7118px inset, - rgb(255 255 255) 0px 5.38841px 8.46749px -3.07909px inset, - rgb(96 68 145 / 30%) 0px -63.1213px 52.3445px -49.2654px inset, - rgb(202 172 255 / 30%) 0px 75.4377px 76.9772px -36.9491px inset, + box-shadow: rgb(255 255 255 / 50%) 0px 30.0211px 43.1072px -27.7118px inset, rgb(255 255 255) 0px 5.38841px 8.46749px -3.07909px inset, + rgb(96 68 145 / 30%) 0px -63.1213px 52.3445px -49.2654px inset, rgb(202 172 255 / 30%) 0px 75.4377px 76.9772px -36.9491px inset, rgb(154 146 210 / 30%) 0px 3.07909px 13.8559px inset, rgb(227 222 255 / 20%) 0px 0.769772px 30.7909px inset; border: 1px solid ${({ theme }) => theme.bg3}; @@ -68,19 +66,11 @@ const Panel = styled(RebassBox)` ${(props) => !props.last && panelPseudo} ` -export default Panel +export const VolumeContestPanel = styled(Panel)` + box-shadow: none; + border-radius: 8px; + border: 1px solid rgba(160, 160, 160, 0.4); + background: rgba(255, 255, 255, 0.05); +` -// const Panel = styled.div` -// width: 100%; -// height: 100%; -// display: flex; -// flex-direction: column; -// justify-content: flex-start; -// border-radius: 12px; -// background-color: ${({ theme }) => theme.advancedBG}; -// padding: 1.25rem; -// box-sizing: border-box; -// box-shadow: 0 1.1px 2.8px -9px rgba(0, 0, 0, 0.008), 0 2.7px 6.7px -9px rgba(0, 0, 0, 0.012), -// 0 5px 12.6px -9px rgba(0, 0, 0, 0.015), 0 8.9px 22.6px -9px rgba(0, 0, 0, 0.018), -// 0 16.7px 42.2px -9px rgba(0, 0, 0, 0.022), 0 40px 101px -9px rgba(0, 0, 0, 0.03); -// ` +export default Panel diff --git a/src/components/SideNav/index.js b/src/components/SideNav/index.js index d1737b6..ab212cd 100755 --- a/src/components/SideNav/index.js +++ b/src/components/SideNav/index.js @@ -124,15 +124,27 @@ function SideNav({ history }) { {!below1080 && ( <AutoColumn gap="1.25rem" style={{ marginTop: '1rem' }}> - <BasicLink to="/lp-contest"> - <Option activeText={history.location.pathname.split('/')[1] === 'lp-contest' ?? undefined} style={{ opacity: 1 }}> + {/*<BasicLink to="/lp-contest">*/} + {/* <Option activeText={history.location.pathname.split('/')[1] === 'lp-contest' ?? undefined} style={{ opacity: 1 }}>*/} + {/* <img*/} + {/* src={confettiFiatGif}*/} + {/* srcSet={confettiFiatGif + ' 1x,' + confettiFiatGif_x2 + ' 2x'}*/} + {/* alt={''}*/} + {/* style={{ marginRight: '-.1rem', marginTop: '-5px', width: '35px' }}*/} + {/* />*/} + {/* <AccentText>LP Contest</AccentText>*/} + {/* </Option>*/} + {/*</BasicLink>*/} + + <BasicLink to="/volume-contest"> + <Option activeText={history.location.pathname.split('/')[1] === 'volume-contest' ?? undefined} style={{ opacity: 1 }}> <img src={confettiFiatGif} srcSet={confettiFiatGif + ' 1x,' + confettiFiatGif_x2 + ' 2x'} alt={''} style={{ marginRight: '-.1rem', marginTop: '-5px', width: '35px' }} /> - <AccentText>LP Contest</AccentText> + <AccentText>Volume Contest</AccentText> </Option> </BasicLink> diff --git a/src/components/Title/index.js b/src/components/Title/index.js index ac1dddc..d28c5d6 100755 --- a/src/components/Title/index.js +++ b/src/components/Title/index.js @@ -58,9 +58,7 @@ export default function Title() { <UniIcon id="link" onClick={() => history.push('/')}> <img width={'24px'} src={Logo} alt="logo" /> </UniIcon> - {!below1080 && ( - <img width={'110px'} style={{ marginLeft: '8px', marginTop: '-4px' }} src={Wordmark} alt="logo" /> - )} + {!below1080 && <img width={'110px'} style={{ marginLeft: '8px', marginTop: '-4px' }} src={Wordmark} alt="logo" />} </RowFixed> {below1080 && ( <RowFixed style={{ alignItems: 'flex-end' }}> @@ -70,9 +68,7 @@ export default function Title() { <BasicLink to="/tokens"> <Option activeText={ - (history.location.pathname.split('/')[1] === 'tokens' || - history.location.pathname.split('/')[1] === 'token') ?? - undefined + (history.location.pathname.split('/')[1] === 'tokens' || history.location.pathname.split('/')[1] === 'token') ?? undefined } > Tokens @@ -80,11 +76,7 @@ export default function Title() { </BasicLink> <BasicLink to="/pairs"> <Option - activeText={ - (history.location.pathname.split('/')[1] === 'pairs' || - history.location.pathname.split('/')[1] === 'pair') ?? - undefined - } + activeText={(history.location.pathname.split('/')[1] === 'pairs' || history.location.pathname.split('/')[1] === 'pair') ?? undefined} > Pairs </Option> @@ -93,21 +85,22 @@ export default function Title() { <BasicLink to="/accounts"> <Option activeText={ - (history.location.pathname.split('/')[1] === 'accounts' || - history.location.pathname.split('/')[1] === 'account') ?? - undefined + (history.location.pathname.split('/')[1] === 'accounts' || history.location.pathname.split('/')[1] === 'account') ?? undefined } > Accounts </Option> </BasicLink> - <BasicLink to="/lp-contest"> - <Option - activeText={history.location.pathname.split('/')[1] === 'lp-contest' ?? undefined} - style={{ opacity: 1 }} - > - <AccentText>LP Contest</AccentText> + {/*<BasicLink to="/lp-contest">*/} + {/* <Option activeText={history.location.pathname.split('/')[1] === 'lp-contest' ?? undefined} style={{ opacity: 1 }}>*/} + {/* <AccentText>LP Contest</AccentText>*/} + {/* </Option>*/} + {/*</BasicLink>*/} + + <BasicLink to="/volume-contest"> + <Option activeText={history.location.pathname.split('/')[1] === 'volume-contest' ?? undefined} style={{ opacity: 1 }}> + <AccentText>Volume Contest</AccentText> </Option> </BasicLink> </RowFixed> diff --git a/src/components/VolumeContestNftCategories/index.js b/src/components/VolumeContestNftCategories/index.js new file mode 100644 index 0000000..6abb490 --- /dev/null +++ b/src/components/VolumeContestNftCategories/index.js @@ -0,0 +1,150 @@ +import React from 'react' +import styled from 'styled-components' + +import nftDecorationImage from './nft_decoration.png' +import nftDecorationImage_x2 from './nft_decoration@x2.png' + +const scoreBannerThemeColors = { + theme1: '#B77C2E', + theme2: '#1B1B1B', +} + +const Wrapper = styled.div`` + +const ScoreBannerDecoration = styled.div` + position: relative; + height: 100%; + img { + position: absolute; + right: 0; + bottom: 0; + max-width: 100%; + } +` + +const ScoreBannersGridRow = styled.div` + display: grid; + width: 100%; + grid-template-columns: 1fr 1fr; + column-gap: 16px; + row-gap: 16px; + align-items: start; + justify-content: space-between; + + @media screen and (max-width: 880px) { + grid-template-columns: 1fr; + + ${ScoreBannerDecoration} { + display: none; + } + } +` + +const ScoreBannerWrapper = styled.div` + display: flex; + border-radius: 8px; + padding: 10px; + position: relative; + //width: 200px; + height: 70px; + overflow: hidden; + color: #fff; + cursor: default; + user-select: none; + + ${(props) => + props.themeColor && + ` + background: linear-gradient(97deg, ${props.themeColor} 17.34%, rgba(183, 124, 46, 0.06) 98.33%); + `} + } + + .section { + display: flex; + flex-direction: column; + width: 50%; + position: relative; + z-index: 1; + } + + .section:last-child { + text-align: right; + } + + .title { + margin-bottom: 5px; + font-weight: 700; + font-size: 10px; + white-space: nowrap; + } + + .content { + font-weight: 700; + font-size: 16px; + white-space: nowrap; + } +` + +function VolumeContestNftCategories() { + return ( + <Wrapper> + <ScoreBannersGridRow> + <ScoreBanner + themeColor={scoreBannerThemeColors.theme1} + leftTitle={'Score'} + leftContent={'8000'} + rightTitle={'NFT Unlock'} + rightContent={'T1A1'} + /> + <ScoreBannerDecoration> + <img src={nftDecorationImage} srcSet={nftDecorationImage + ' 1x,' + nftDecorationImage_x2 + ' 2x'} alt={''} /> + </ScoreBannerDecoration> + <ScoreBanner + themeColor={scoreBannerThemeColors.theme2} + leftTitle={'Score'} + leftContent={'6000-7999'} + rightTitle={'NFT Unlock'} + rightContent={'T1A2'} + /> + <ScoreBanner + themeColor={scoreBannerThemeColors.theme2} + leftTitle={'Score'} + leftContent={'4000-5999'} + rightTitle={'NFT Unlock'} + rightContent={'T1A3'} + /> + <ScoreBanner + themeColor={scoreBannerThemeColors.theme2} + leftTitle={'Score'} + leftContent={'2000-3999'} + rightTitle={'NFT Unlock'} + rightContent={'T1A4'} + /> + <ScoreBanner + themeColor={scoreBannerThemeColors.theme2} + leftTitle={'Score'} + leftContent={'500-1999'} + rightTitle={'NFT Unlock'} + rightContent={'T1A5'} + /> + </ScoreBannersGridRow> + </Wrapper> + ) +} + +function ScoreBanner({ themeColor, leftTitle = '', leftContent = '', rightTitle = '', rightContent = '' }) { + return ( + <ScoreBannerWrapper themeColor={themeColor}> + <div className={'section'}> + <div className={'title'}>{leftTitle}</div> + <div className={'content'}>{leftContent}</div> + </div> + <div className={'section'}> + <div className={'title'}>{rightTitle}</div> + <div className={'content'}>{rightContent}</div> + </div> + </ScoreBannerWrapper> + ) +} + +export default VolumeContestNftCategories diff --git a/src/components/VolumeContestNftCategories/nft_decoration.png b/src/components/VolumeContestNftCategories/nft_decoration.png new file mode 100644 index 0000000..0dd57ec Binary files /dev/null and b/src/components/VolumeContestNftCategories/nft_decoration.png differ diff --git a/src/components/VolumeContestNftCategories/nft_decoration@x2.png b/src/components/VolumeContestNftCategories/nft_decoration@x2.png new file mode 100644 index 0000000..77917e2 Binary files /dev/null and b/src/components/VolumeContestNftCategories/nft_decoration@x2.png differ diff --git a/src/components/VolumeContestNftClaim/T1A1.png b/src/components/VolumeContestNftClaim/T1A1.png new file mode 100644 index 0000000..ad58001 Binary files /dev/null and b/src/components/VolumeContestNftClaim/T1A1.png differ diff --git a/src/components/VolumeContestNftClaim/T1A1@x2.png b/src/components/VolumeContestNftClaim/T1A1@x2.png new file mode 100644 index 0000000..efc600c Binary files /dev/null and b/src/components/VolumeContestNftClaim/T1A1@x2.png differ diff --git a/src/components/VolumeContestNftClaim/index.js b/src/components/VolumeContestNftClaim/index.js new file mode 100644 index 0000000..9e34026 --- /dev/null +++ b/src/components/VolumeContestNftClaim/index.js @@ -0,0 +1,167 @@ +import React, { useRef } from 'react' +import styled from 'styled-components' +import { Swiper, SwiperSlide } from 'swiper/react' +import { Navigation } from 'swiper' +import { EffectCoverflow } from 'swiper' +import { Play } from 'react-feather' + +import 'swiper/swiper.css' +import 'swiper/modules/navigation/navigation.min.css' +import 'swiper/modules/effect-coverflow/effect-coverflow.min.css' + +import T1A1 from './T1A1.png' + +import T1A1_x2 from './T1A1@x2.png' + +// import L1PW from './L1PW.png' +// import L1P1 from './L1P1.png' +// import L1P2 from './L1P2.png' +// import L1P3 from './L1P3.png' +// import L1P4 from './L1P4.png' +// import L1P5 from './L1P5.png' +// +// import L1PW_x2 from './L1PW@x2.png' +// import L1P1_x2 from './L1P1@x2.png' +// import L1P2_x2 from './L1P2@x2.png' +// import L1P3_x2 from './L1P3@x2.png' +// import L1P4_x2 from './L1P4@x2.png' +// import L1P5_x2 from './L1P5@x2.png' + +const Wrapper = styled.div` + display: flex; + flex-direction: column; +` + +const Slider = styled.div` + position: relative; + display: flex; + align-items: center; + flex-grow: 1; + width: 100%; + + .swiper { + width: 100%; + } + + .swiper-wrapper { + align-items: center; + } + + .swiper-slide { + text-align: center; + user-select: none; + + img { + max-width: 100%; + opacity: 0; + transition: all 0.2s; + filter: saturate(0%) opacity(0.3); + } + } + + .swiper-slide-active { + img { + filter: none; + } + } + + .swiper-slide-active, + .swiper-slide-next, + .swiper-slide-prev { + img { + opacity: 1; /* Show center, left, and right slides */ + pointer-events: all; + } + } +` + +const NavigationArrow = styled.div` + color: #50d5ff; + display: inline-flex; + position: absolute; + top: 50%; + transform: translate(0, -50%); + cursor: pointer; + z-index: 1; + user-select: none; + + svg { + fill: #50d5ff; + } + + &.swiper-button-disabled { + opacity: 0.5; + } +` +const NavigationArrowPrev = styled(NavigationArrow)` + left: 0; + + svg { + transform: rotate(180deg); + } +` +const NavigationArrowNext = styled(NavigationArrow)` + right: 0; +` + +function VolumeContestNftClaim() { + const navigationPrevRef = useRef(null) + const navigationNextRef = useRef(null) + + return ( + <Wrapper> + <Slider> + <Swiper + modules={[Navigation, EffectCoverflow]} + spaceBetween={0} + effect={'coverflow'} + // speed={0} + centeredSlides={true} + slidesPerView={'auto'} + navigation={{ + prevEl: navigationPrevRef.current, + nextEl: navigationNextRef.current, + }} + coverflowEffect={{ + rotate: 0, + stretch: 310, + depth: 200, + modifier: 1, + scale: 0.9, + slideShadows: false, + }} + resistanceRatio={0.5} + style={{ paddingLeft: '20px', paddingRight: '20px' }} + onBeforeInit={(swiper) => { + swiper.params.navigation.prevEl = navigationPrevRef.current + swiper.params.navigation.nextEl = navigationNextRef.current + }} + > + <SwiperSlide> + <img src={T1A1} srcSet={T1A1 + ' 1x,' + T1A1_x2 + ' 2x'} alt={''} /> + </SwiperSlide> + <SwiperSlide> + <img src={T1A1} srcSet={T1A1 + ' 1x,' + T1A1_x2 + ' 2x'} alt={''} /> + </SwiperSlide> + <SwiperSlide> + <img src={T1A1} srcSet={T1A1 + ' 1x,' + T1A1_x2 + ' 2x'} alt={''} /> + </SwiperSlide> + <SwiperSlide> + <img src={T1A1} srcSet={T1A1 + ' 1x,' + T1A1_x2 + ' 2x'} alt={''} /> + </SwiperSlide> + <SwiperSlide> + <img src={T1A1} srcSet={T1A1 + ' 1x,' + T1A1_x2 + ' 2x'} alt={''} /> + </SwiperSlide> + <NavigationArrowPrev ref={navigationPrevRef}> + <Play size={30} /> + </NavigationArrowPrev> + <NavigationArrowNext ref={navigationNextRef}> + <Play size={30} /> + </NavigationArrowNext> + </Swiper> + </Slider> + </Wrapper> + ) +} + +export default VolumeContestNftClaim diff --git a/src/components/VolumeContestSearchWallet/index.js b/src/components/VolumeContestSearchWallet/index.js new file mode 100644 index 0000000..c95eed3 --- /dev/null +++ b/src/components/VolumeContestSearchWallet/index.js @@ -0,0 +1,157 @@ +import React, { useState, useCallback } from 'react' +import { withRouter } from 'react-router-dom' +import styled from 'styled-components' + +import { isStarknetAddress, zeroStarknetAddress } from '../../utils' + +import { AutoRow } from '../Row' +import { VolumeContestPanel } from '../Panel' +import { ButtonGradient } from '../ButtonStyled' + +import digitalWalletImage from '../../assets/banners/digital_wallet.png' +import digitalWalletImage_x2 from '../../assets/banners/digital_wallet@x2.png' + +const Decoration = styled.img` + position: absolute; + top: -45px; + right: -45px; + max-width: 100%; + width: 187px; + z-index: 0; + transform: rotate(12.116deg); +` + +const SearchPanel = styled(VolumeContestPanel)` + overflow: hidden; + color: #fff; + padding: 24px; + padding-right: 155px; + + @media screen and (max-width: 880px) { + padding-right: 24px; + + ${Decoration} { + display: none; + } + } +` + +const SearchPanelRow = styled(AutoRow)` + width: calc(100% + 32px); + + @media screen and (max-width: 610px) { + flex-wrap: wrap; + } +` + +const SearchWrapper = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-end; + width: 100%; + border-radius: 12px; +` + +const Title = styled.div` + display: flex; + align-items: center; + font-size: 18px; + font-weight: 500; + margin-bottom: 14px; +` + +const Input = styled.input` + position: relative; + display: flex; + align-items: center; + width: 100%; + height: 48px; + white-space: nowrap; + outline: none; + padding: 12px; + border-radius: 8px; + color: ${({ theme }) => theme.text1}; + background-color: #141451; + font-size: 16px; + + border: 1px solid ${({ theme }) => theme.bg3}; + + &:focus { + border: 1px solid ${({ theme }) => theme.text1}; + } + + ::placeholder { + color: ${({ theme }) => theme.text3}; + font-size: 14px; + } + + @media screen and (max-width: 640px) { + ::placeholder { + font-size: 1rem; + } + } +` + +const SearchButton = styled(ButtonGradient)` + height: 48px; + border-radius: 8px; + padding: 14px 17px; +` + +function VolumeContestSearchWalletPanel({ history }) { + const [checkAccountQuery, setCheckAccountQuery] = useState('') + const [isCheckAccountAddressValid, setIsCheckAccountAddressValid] = useState(false) + + const handleCheckAccountInputChange = useCallback( + (e) => { + const value = e.currentTarget.value + if (!value) { + setCheckAccountQuery('') + setIsCheckAccountAddressValid(false) + return + } + setCheckAccountQuery(value) + setIsCheckAccountAddressValid(isStarknetAddress(value, true)) + }, + [setCheckAccountQuery] + ) + + const handleAccountSearch = useCallback( + (e) => { + if (!(isCheckAccountAddressValid && checkAccountQuery)) { + return + } + history.push('/volume-contest/' + checkAccountQuery) + }, + [isCheckAccountAddressValid, checkAccountQuery] + ) + + return ( + <> + <SearchPanel> + <Title>Search your wallet + + + + + +
+ + View Profile + +
+
+ + + + ) +} + +export default withRouter(VolumeContestSearchWalletPanel) diff --git a/src/components/VolumeContestTxnList/index.js b/src/components/VolumeContestTxnList/index.js new file mode 100755 index 0000000..8f5c913 --- /dev/null +++ b/src/components/VolumeContestTxnList/index.js @@ -0,0 +1,374 @@ +import React, { useState, useEffect } from 'react' +import styled from 'styled-components' +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' +import { useMedia } from 'react-use' + +import { ChevronLeft } from 'react-feather' +// import { ChevronRight } from 'react-feather' + +import { formatTime, formattedNum, urls, convertDateToUnixFormat } from '../../utils' + +import LocalLoader from '../LocalLoader' +import { Box, Flex, Text } from 'rebass' +import Link from '../Link' +import { EmptyCard } from '..' +import FormattedName from '../FormattedName' +import { TYPE } from '../../Theme' +import { updateNameData } from '../../utils/data' +import { VolumeContestPanel } from '../Panel' +import { AutoColumn } from '../Column' + +dayjs.extend(utc) + +const TableWrapper = styled(VolumeContestPanel)` + padding: 24px 16px; +` + +const PageButtons = styled.div` + width: 100%; + display: flex; + justify-content: center; + align-items: center; +` + +const Arrow = styled(ChevronLeft)` + color: #50d5ff; + opacity: ${(props) => (props.faded ? 0.3 : 1)}; + user-select: none; + :hover { + cursor: pointer; + } +` + +const ArrowLeft = styled(Arrow)`` +const ArrowRight = styled(Arrow)` + transform: rotate(180deg); +` + +const List = styled(Box)` + -webkit-overflow-scrolling: touch; +` + +const DataText = styled(Flex)` + align-items: center; + text-align: center; + color: #fff; + + & > * { + font-size: 14px; + } + + @media screen and (max-width: 600px) { + font-size: 13px; + } +` + +const DashGrid = styled.div` + display: grid; + grid-gap: 1em; + grid-template-columns: 1fr 0.8fr 0.8fr 0.8fr 0.7fr; + grid-template-areas: 'tnx totalValue totalAmount1 totalAmount2 time'; + + > * { + justify-content: flex-end; + } + + ${DataText} { + padding-left: 5px; + padding-right: 5px; + } + + ${DataText}:first-child { + padding-left: 20px; + padding-right: 20px; + text-align: left; + } + + @media screen and (max-width: 780px) { + grid-template-columns: 1.8fr 1fr 1fr; + grid-template-areas: 'tnx totalValue time'; + } +` + +const DashGridItemRow = styled(DashGrid)` + border-radius: 8px; + + ${DataText} { + padding-top: 12px; + padding-bottom: 12px; + } + + &:nth-child(even) { + background: rgba(255, 255, 255, 0.05); + } +` + +const ClickableText = styled(Text)` + color: ${({ theme }) => theme.jediGray}; + user-select: none; + text-align: end; + + &:hover { + cursor: pointer; + color: ${({ theme }) => theme.text1}; + } + + @media screen and (max-width: 640px) { + font-size: 14px; + } +` + +const SORT_FIELD = { + VALUE: 'amountUSD', + AMOUNT0: 'token0Amount', + AMOUNT1: 'token1Amount', + TIMESTAMP: 'timestamp', +} + +const TXN_TYPE = { + SWAP: 'Swaps', +} + +const ITEMS_PER_PAGE = 10 + +function getTransactionType(event, symbol0, symbol1) { + const formattedS0 = symbol0?.length > 8 ? symbol0.slice(0, 7) + '...' : symbol0 + const formattedS1 = symbol1?.length > 8 ? symbol1.slice(0, 7) + '...' : symbol1 + switch (event) { + case TXN_TYPE.ADD: + return 'Add ' + formattedS0 + ' and ' + formattedS1 + case TXN_TYPE.REMOVE: + return 'Remove ' + formattedS0 + ' and ' + formattedS1 + case TXN_TYPE.SWAP: + return 'Swap ' + formattedS0 + ' for ' + formattedS1 + default: + return '' + } +} + +function VolumeContestTxnList({ transactions, account }) { + // page state + const [page, setPage] = useState(1) + const [maxPage, setMaxPage] = useState(1) + + // sorting + const [sortDirection, setSortDirection] = useState(true) + const [sortedColumn, setSortedColumn] = useState(SORT_FIELD.TIMESTAMP) + const [filteredItems, setFilteredItems] = useState() + const [txFilter, setTxFilter] = useState(TXN_TYPE.ALL) + + useEffect(() => { + setMaxPage(1) // edit this to do modular + setPage(1) + }, [transactions]) + + // parse the txns and format for UI + useEffect(() => { + if (transactions?.swaps) { + let newTxns = [] + if (transactions.swaps.length > 0) { + transactions.swaps.map((swap) => { + const netToken0 = swap.amount0In - swap.amount0Out + const netToken1 = swap.amount1In - swap.amount1Out + + let newTxn = {} + + if (netToken0 < 0) { + newTxn.token0Symbol = updateNameData(swap.pair).token0.symbol + newTxn.token1Symbol = updateNameData(swap.pair).token1.symbol + newTxn.token0Amount = Math.abs(netToken0) + newTxn.token1Amount = Math.abs(netToken1) + } else if (netToken1 < 0) { + newTxn.token0Symbol = updateNameData(swap.pair).token1.symbol + newTxn.token1Symbol = updateNameData(swap.pair).token0.symbol + newTxn.token0Amount = Math.abs(netToken1) + newTxn.token1Amount = Math.abs(netToken0) + } + + newTxn.hash = swap.transactionHash + newTxn.timestamp = dayjs.utc(swap.timestamp).local().format() + newTxn.type = TXN_TYPE.SWAP + + newTxn.amountUSD = swap.amountUSD + newTxn.account = swap.to + return newTxns.push(newTxn) + }) + } + + setFilteredItems(newTxns) + let extraPages = 1 + if (newTxns.length % ITEMS_PER_PAGE === 0) { + extraPages = 0 + } + if (newTxns.length === 0) { + setMaxPage(1) + } else { + setMaxPage(Math.floor(newTxns.length / ITEMS_PER_PAGE) + extraPages) + } + } + }, [transactions, txFilter]) + + useEffect(() => { + setPage(1) + }, [txFilter]) + + const filteredList = + filteredItems && + filteredItems + .sort((a, b) => { + let valueA = a[sortedColumn] + let valueB = b[sortedColumn] + + if (sortedColumn === SORT_FIELD.TIMESTAMP) { + valueA = convertDateToUnixFormat(a[sortedColumn]) + valueB = convertDateToUnixFormat(b[sortedColumn]) + } + + return parseFloat(valueA) > parseFloat(valueB) ? (sortDirection ? -1 : 1) * 1 : (sortDirection ? -1 : 1) * -1 + }) + .slice(ITEMS_PER_PAGE * (page - 1), page * ITEMS_PER_PAGE) + + const below780 = useMedia('(max-width: 780px)') + + const ListItem = ({ item }) => { + return ( + + + + {getTransactionType(item.type, item.token1Symbol, item.token0Symbol)} + + + + {formattedNum(item.amountUSD, true)} + + + {!below780 && ( + <> + + {formattedNum(item.token1Amount) + ' '} + + + {formattedNum(item.token0Amount) + ' '} + + + )} + + + {formatTime(convertDateToUnixFormat(item.timestamp))} + + + ) + } + + if (!filteredList) { + return ( + + + + ) + } + + if (!filteredList?.length && account) { + return ( + + No available data for {account} + + ) + } + + const transactionsList = + filteredList?.length && + filteredList.map((item, index) => { + return + }) + + return ( + <> + + + + + + + + { + setSortedColumn(SORT_FIELD.VALUE) + setSortDirection(sortedColumn !== SORT_FIELD.VALUE ? true : !sortDirection) + }} + > + Total Value {sortedColumn === SORT_FIELD.VALUE ? (!sortDirection ? '↑' : '↓') : ''} + + + + {!below780 && ( + <> + + { + setSortedColumn(SORT_FIELD.AMOUNT1) + setSortDirection(sortedColumn !== SORT_FIELD.AMOUNT1 ? true : !sortDirection) + }} + > + Total Amount {sortedColumn === SORT_FIELD.AMOUNT1 ? (sortDirection ? '↑' : '↓') : ''} + + + + { + setSortedColumn(SORT_FIELD.AMOUNT0) + setSortDirection(sortedColumn !== SORT_FIELD.AMOUNT0 ? true : !sortDirection) + }} + > + Total Amount {sortedColumn === SORT_FIELD.AMOUNT0 ? (sortDirection ? '↑' : '↓') : ''} + + + + )} + + { + setSortedColumn(SORT_FIELD.TIMESTAMP) + setSortDirection(sortedColumn !== SORT_FIELD.TIMESTAMP ? true : !sortDirection) + }} + > + Time {sortedColumn === SORT_FIELD.TIMESTAMP ? (!sortDirection ? '↑' : '↓') : ''} + + + + + {!filteredList?.length ? No recent transactions found. : {transactionsList}} + + + { + setPage(page === 1 ? page : page - 1) + }} + > + + {page + ' of ' + maxPage} + + { + setPage(page === maxPage ? page : page + 1) + }} + > + + + + + ) +} + +export default VolumeContestTxnList diff --git a/src/components/VolumeContestUserScoreTable/index.js b/src/components/VolumeContestUserScoreTable/index.js new file mode 100644 index 0000000..ab7a27b --- /dev/null +++ b/src/components/VolumeContestUserScoreTable/index.js @@ -0,0 +1,148 @@ +import React from 'react' +import dayjs from 'dayjs' +import weekday from 'dayjs/plugin/weekday' +import { Box, Flex } from 'rebass' +import styled from 'styled-components' + +import 'react-tooltip/dist/react-tooltip.css' + +import { EmptyCard } from '..' +import { formattedNum } from '../../utils' +import { TYPE } from '../../Theme' +import { VolumeContestPanel } from '../Panel' + +import { withRouter } from 'react-router-dom' +import { AutoColumn } from '../Column' +import LocalLoader from '../LocalLoader' + +dayjs.extend(weekday) + +const TableWrapper = styled(VolumeContestPanel)` + padding: 24px 16px; +` + +const List = styled(Box)` + -webkit-overflow-scrolling: touch; +` + +const DataText = styled(Flex)` + align-items: center; + text-align: center; + color: #fff; + + & > * { + font-size: 14px; + } + + @media screen and (max-width: 600px) { + font-size: 13px; + } +` + +const DashGrid = styled.div` + display: grid; + grid-gap: 1em; + grid-template-columns: 1.8fr 1fr 1fr; + grid-template-areas: 'week volume score'; + + > * { + justify-content: flex-end; + } + + ${DataText} { + padding-left: 5px; + padding-right: 5px; + } + + ${DataText}:first-child { + padding-left: 20px; + padding-right: 20px; + text-align: left; + } +` + +const DashGridItemRow = styled(DashGrid)` + border-radius: 8px; + + ${DataText} { + padding-top: 12px; + padding-bottom: 12px; + } + + &:nth-child(even) { + background: rgba(255, 255, 255, 0.05); + } +` + +function VolumeContestUserScoreTable({ weeks }) { + const sortWeeksInDescendingOrder = (a, b) => b?.id - a?.id + const areWeeksAvailable = !!weeks?.length + + const ListItem = ({ week }) => { + const hasTheDatePassed = (date) => dayjs().isAfter(dayjs(date)) + return ( + + + {`${dayjs(week?.startDt).format('MMM DD')} - ${dayjs(week?.endDt).subtract(1, 'day').format('MMM DD')}`} + + + {hasTheDatePassed(week?.startDt) ? formattedNum(week.volume, true) : '-'} + + + {hasTheDatePassed(week?.startDt) ? formattedNum(Math.floor(week.score)) : '-'} + + + ) + } + + if (!weeks) { + return ( + + + + ) + } + + if (!weeks?.length) { + return ( + + No available data + + ) + } + + const weeksList = + weeks?.length && + weeks.sort(sortWeeksInDescendingOrder).map((week, index) => { + return + }) + + return ( + <> + + + + + + Week + + + + + Volume added + + + + + Score + + + + {!areWeeksAvailable ? No data found. : {weeksList}} + + + + ) +} + +export default withRouter(VolumeContestUserScoreTable) diff --git a/src/components/index.js b/src/components/index.js index 5531542..a9b97ee 100755 --- a/src/components/index.js +++ b/src/components/index.js @@ -55,13 +55,7 @@ const Hint = ({ children, ...rest }) => ( ) const Address = ({ address, token, ...rest }) => ( - + {address} ) @@ -81,10 +75,9 @@ const EmptyCard = styled.div` display: flex; align-items: center; justify-content: center; - height: 200px; border-radius: 20px; color: ${({ theme }) => theme.text1}; - height: ${({ height }) => height && height}; + height: ${({ height }) => height ?? '200px'}; ` export const SideBar = styled.span` @@ -123,13 +116,11 @@ export const PageWrapper = styled.div` flex-direction: column; padding-top: 36px; padding-bottom: 80px; - ${ - '' /* box-shadow: rgb(255 255 255 / 50%) 0px 30.0211px 43.1072px -27.7118px inset, + ${'' /* box-shadow: rgb(255 255 255 / 50%) 0px 30.0211px 43.1072px -27.7118px inset, rgb(255 255 255) 0px 5.38841px 8.46749px -3.07909px inset, rgb(96 68 145 / 30%) 0px -63.1213px 52.3445px -49.2654px inset, rgb(202 172 255 / 30%) 0px 75.4377px 76.9772px -36.9491px inset, - rgb(154 146 210 / 30%) 0px 3.07909px 13.8559px inset, rgb(227 222 255 / 20%) 0px 0.769772px 30.7909px inset, */ - } + rgb(154 146 210 / 30%) 0px 3.07909px 13.8559px inset, rgb(227 222 255 / 20%) 0px 0.769772px 30.7909px inset, */} @media screen and (max-width: 600px) { & > * { padding: 0 12px; diff --git a/src/contexts/User.js b/src/contexts/User.js index f27d845..ab4c139 100755 --- a/src/contexts/User.js +++ b/src/contexts/User.js @@ -1,4 +1,5 @@ import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect, useState } from 'react' +import { isEmpty } from 'lodash' import { usePairData } from './PairData' import { jediSwapClient } from '../apollo/client' import { @@ -7,6 +8,7 @@ import { USER_HISTORY, PAIR_DAY_DATA_BULK, USER_LP_CONTEST_TRANSACTIONS, + USER_VOLUME_CONTEST_TRANSACTIONS, USER_LP_CONTEST_HISTORY, USER_LP_CONTEST_PERCENTILE, } from '../apollo/queries' @@ -22,6 +24,7 @@ dayjs.extend(utc) const UPDATE_TRANSACTIONS = 'UPDATE_TRANSACTIONS' const UPDATE_LP_CONTEST_TRANSACTIONS = 'UPDATE_LP_CONTEST_TRANSACTIONS' +const UPDATE_VOLUME_CONTEST_TRANSACTIONS = 'UPDATE_VOLUME_CONTEST_TRANSACTIONS' const UPDATE_POSITIONS = 'UPDATE_POSITIONS ' const UPDATE_MINING_POSITIONS = 'UPDATE_MINING_POSITIONS' const UPDATE_USER_POSITION_HISTORY = 'UPDATE_USER_POSITION_HISTORY' @@ -31,6 +34,7 @@ const UPDATE_USER_PAIR_RETURNS = 'UPDATE_USER_PAIR_RETURNS' const TRANSACTIONS_KEY = 'TRANSACTIONS_KEY' const LP_CONTEST_TRANSACTIONS_KEY = 'LP_CONTEST_TRANSACTIONS_KEY' +const VOLUME_CONTEST_TRANSACTIONS_KEY = 'VOLUME_CONTEST_TRANSACTIONS_KEY' const POSITIONS_KEY = 'POSITIONS_KEY' const MINING_POSITIONS_KEY = 'MINING_POSITIONS_KEY' const USER_SNAPSHOTS = 'USER_SNAPSHOTS' @@ -76,6 +80,16 @@ function reducer(state, { type, payload }) { }, } } + case UPDATE_VOLUME_CONTEST_TRANSACTIONS: { + const { account, transactions } = payload + return { + ...state, + [account]: { + ...state?.[account], + [VOLUME_CONTEST_TRANSACTIONS_KEY]: transactions, + }, + } + } case UPDATE_POSITIONS: { const { account, positions } = payload return { @@ -105,7 +119,6 @@ function reducer(state, { type, payload }) { [account]: { ...state?.[account], [LP_CONTEST_USER_SNAPSHOTS]: historyData }, } } - case UPDATE_USER_PAIR_RETURNS: { const { account, pairAddress, data } = payload return { @@ -161,6 +174,16 @@ export default function Provider({ children }) { }) }, []) + const updateVolumeContestTransactions = useCallback((account, transactions) => { + dispatch({ + type: UPDATE_VOLUME_CONTEST_TRANSACTIONS, + payload: { + account, + transactions, + }, + }) + }, []) + const updatePositions = useCallback((account, positions) => { dispatch({ type: UPDATE_POSITIONS, @@ -219,6 +242,7 @@ export default function Provider({ children }) { state, { updateTransactions, + updateVolumeContestTransactions, updateLpContestTransactions, updatePositions, updateMiningPositions, @@ -232,6 +256,7 @@ export default function Provider({ children }) { state, updateTransactions, updateLpContestTransactions, + updateVolumeContestTransactions, updatePositions, updateMiningPositions, updateUserSnapshots, @@ -302,6 +327,36 @@ export function useUserLpCampaignTransactions(account) { return transactions || {} } +export function useUserVolumeCampaignTransactions(account, timestampsStart, timestampsEnd) { + const [state, { updateVolumeContestTransactions }] = useUserContext() + const transactions = state?.[account]?.[VOLUME_CONTEST_TRANSACTIONS_KEY] + useEffect(() => { + async function fetchData(account) { + try { + let result = await jediSwapClient.query({ + query: USER_VOLUME_CONTEST_TRANSACTIONS, + variables: { + user: account, + timestampsStart, + timestampsEnd, + }, + fetchPolicy: 'no-cache', + }) + if (result?.data) { + updateVolumeContestTransactions(account, result?.data) + } + } catch (e) { + console.log(e) + } + } + if (!transactions && account) { + fetchData(account) + } + }, [account, transactions, updateVolumeContestTransactions]) + + return transactions || {} +} + /** * Store all the snapshots of liquidity activity for this account. * Each snapshot is a moment when an LP position was created or updated. @@ -459,12 +514,7 @@ export function useUserPositionChart(position, account) { useEffect(() => { async function fetchData() { - let fetchedData = await getHistoricalPairReturns( - startDateTimestamp, - currentPairData, - pairSnapshots, - currentETHPrice - ) + let fetchedData = await getHistoricalPairReturns(startDateTimestamp, currentPairData, pairSnapshots, currentETHPrice) updateUserPairReturns(account, pairAddress, fetchedData) } if ( @@ -619,8 +669,7 @@ export function useUserLiquidityChart(account) { return (totalUSD = totalUSD + (ownershipPerPair[dayData.pairId] - ? (parseFloat(ownershipPerPair[dayData.pairId].lpTokenBalance) / parseFloat(dayData.totalSupply)) * - parseFloat(dayData.reserveUSD) + ? (parseFloat(ownershipPerPair[dayData.pairId].lpTokenBalance) / parseFloat(dayData.totalSupply)) * parseFloat(dayData.reserveUSD) : 0)) } else { return totalUSD diff --git a/src/contexts/VolumeContestData.js b/src/contexts/VolumeContestData.js new file mode 100644 index 0000000..a100c4e --- /dev/null +++ b/src/contexts/VolumeContestData.js @@ -0,0 +1,100 @@ +import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react' + +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' +import { jediSwapClient } from '../apollo/client' +import { USER_VOLUME_CONTEST_DATA } from '../apollo/queries' +import { isEmpty } from 'lodash' + +const UPDATE_PLAYER_DATA = 'UPDATE_PLAYER_DATA' + +dayjs.extend(utc) + +const VolumeContestDataContext = createContext() + +function useVolumeContestDataContext() { + return useContext(VolumeContestDataContext) +} + +function reducer(state, { type, payload }) { + switch (type) { + case UPDATE_PLAYER_DATA: { + const { playerAddress, data } = payload + return { + ...state, + players: { + ...(state?.players ?? {}), + [playerAddress]: { + ...state?.players?.[playerAddress], + ...data, + }, + }, + } + } + + default: { + throw Error(`Unexpected action type in DataContext reducer: '${type}'.`) + } + } +} + +export default function Provider({ children }) { + const [state, dispatch] = useReducer(reducer, {}) + + const updatePlayerData = useCallback((playerAddress, data) => { + dispatch({ + type: UPDATE_PLAYER_DATA, + payload: { + playerAddress, + data, + }, + }) + }, []) + + return ( + [ + state, + { + updatePlayerData, + }, + ], + [state, updatePlayerData] + )} + > + {children} + + ) +} + +export function useVolumeContestUserData(account) { + const [state, { updatePlayerData }] = useVolumeContestDataContext() + const userData = state?.players?.[account] + + useEffect(() => { + async function fetchData(account) { + try { + let result = await jediSwapClient.query({ + query: USER_VOLUME_CONTEST_DATA(account), + fetchPolicy: 'no-cache', + }) + if (!isEmpty(result?.data?.volumeContest)) { + updatePlayerData(account, result.data.volumeContest) + } + } catch (e) { + console.log(e) + } + } + if (isEmpty(userData) && account) { + fetchData(account) + } + }, [account, userData, updatePlayerData]) + + return userData +} + +export function useAllVolumeContestData() { + const [state] = useVolumeContestDataContext() + return state || {} +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts index e89e111..bde9e1e 100755 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -3,7 +3,7 @@ import { shade } from 'polished' import Vibrant from 'node-vibrant' import { hex } from 'wcag-contrast' import copy from 'copy-to-clipboard' -import { isStarknetAddress } from '../utils' +import { convertHexToDecimal, isStarknetAddress } from '../utils' import { useWhitelistedTokens } from '../contexts/Application' export function useColor(tokenAddress, token) { @@ -99,3 +99,29 @@ export default function useInterval(callback: () => void, delay: null | number) return }, [delay]) } + +export const useUserStraknetIdDomain = (account) => { + const [domain, setDomain] = useState(false) + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + async function fetchData() { + const convertedAddress = convertHexToDecimal(account) + const response = await fetch(`https://app.starknet.id/api/indexer/addr_to_domain?addr=${convertedAddress}`) + const processedResponse = await response.json() + if (processedResponse.domain) { + setDomain(processedResponse.domain) + } + setIsLoading(false) + } + if (account) { + try { + fetchData() + } catch (e) { + setIsLoading(false) + } + } + }, [account]) + + return [isLoading, domain] +} diff --git a/src/index.js b/src/index.js index 604e8fa..74126c8 100755 --- a/src/index.js +++ b/src/index.js @@ -8,6 +8,7 @@ import TokenDataContextProvider, { Updater as TokenDataContextUpdater } from './ import GlobalDataContextProvider from './contexts/GlobalData' import PairDataContextProvider, { Updater as PairDataContextUpdater } from './contexts/PairData' import LpContestDataProvider, { Updater as LpContestDataContextUpdater } from './contexts/LpContestData' +import VolumeContestDataProvider from './contexts/VolumeContestData' import ApplicationContextProvider from './contexts/Application' import UserContextProvider from './contexts/User' import App from './App' @@ -24,11 +25,7 @@ if (typeof GOOGLE_ANALYTICS_ID === 'string') { }) ReactGA.set({ anonymizeIp: true, - customBrowserType: !isMobile - ? 'desktop' - : 'web3' in window || 'ethereum' in window - ? 'mobileWeb3' - : 'mobileRegular', + customBrowserType: !isMobile ? 'desktop' : 'web3' in window || 'ethereum' in window ? 'mobileWeb3' : 'mobileRegular', }) } else { ReactGA.initialize('test', { testMode: true, debug: true }) @@ -41,9 +38,11 @@ function ContextProviders({ children }) { - - {children} - + + + {children} + + diff --git a/src/pages/VolumeContestLookup.js b/src/pages/VolumeContestLookup.js new file mode 100755 index 0000000..bb44ec6 --- /dev/null +++ b/src/pages/VolumeContestLookup.js @@ -0,0 +1,191 @@ +import React from 'react' +import { withRouter } from 'react-router-dom' +import dayjs from 'dayjs' +import weekday from 'dayjs/plugin/weekday' +import styled from 'styled-components' +import 'feather-icons' + +import { PageWrapper, FullWrapper } from '../components' +import { VolumeContestPanel } from '../components/Panel' +import { Banner, Title as BannerTitle } from '../components/Banner' +import { AutoColumn } from '../components/Column' +import VolumeContestNftCategories from '../components/VolumeContestNftCategories' +import VolumeContestSearchWalletPanel from '../components/VolumeContestSearchWallet' +import FAQ from '../components/FAQ' +import { AutoRow } from '../components/Row' +import VolumeContestNftClaim from '../components/VolumeContestNftClaim' + +import { TYPE } from '../Theme' + +import contestFlagIcon from '../../src/assets/flag.svg' + +dayjs.extend(weekday) + +const Title = styled.div` + display: flex; + align-items: center; + font-size: 16px; + font-weight: 500; + margin-bottom: 14px; +` + +const TitleIconWrapper = styled.div` + display: flex; + margin-right: 0.75rem; + font-size: 30px; + + img { + width: 1em; + } +` + +const DashboardWrapper = styled(VolumeContestPanel)` + padding: 30px 40px; +` + +const VolumeContestBanner = styled(Banner)` + box-shadow: none; + border-radius: 8px; + border: 1px solid rgba(160, 160, 160, 0.4); + background: rgba(255, 255, 255, 0.05); + + ${BannerTitle} { + font-size: 20px; + } +` + +const InformationGridRow = styled.div` + display: grid; + width: 100%; + grid-template-columns: 1fr 3fr; + column-gap: 32px; + align-items: start; + justify-content: space-between; + + @media screen and (max-width: 1080px) { + grid-template-columns: 1fr; + row-gap: 32px; + } +` + +const SectionWrapper = styled(VolumeContestPanel)`` + +const SectionTitle = styled.div` + margin-bottom: 20px; + font-size: 24px; + font-weight: 700; + color: #fff; +` +const SectionDescription = styled.div` + font-size: 16px; + color: #fff; + margin-bottom: 50px; +` +const RewardsGridRow = styled(AutoRow)` + @media screen and (max-width: 880px) { + flex-direction: column; + + .nft-slider-title { + display: none; + } + } +` +const RewardsGridRowSection = styled.div` + width: 50%; + + @media screen and (max-width: 880px) { + width: 100%; + } +` +const NftSliderWrapper = styled.div` + max-width: 470px; + margin: 0 auto; +` + +const START_DATE = dayjs('2023-09-19T00:00:00.000Z') +const END_DATE = START_DATE.add(8, 'weeks') +const START_DATE_DAY_OF_THE_WEEK = START_DATE.weekday() +const CURRENT_WEEK_START_DATE = dayjs().weekday(START_DATE_DAY_OF_THE_WEEK) +const CURRENT_WEEK_END_DATE = CURRENT_WEEK_START_DATE.add(6, 'days') + +const HAS_CAMPAIGN_STARTED = dayjs().isAfter(dayjs.utc(START_DATE)) +const HAS_CAMPAIGN_ENDED = CURRENT_WEEK_END_DATE.isAfter(dayjs.utc(END_DATE)) + +const faqItems = [ + { + header: 'How is the score getting calculated?', + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing...', + }, + { + header: 'When and how the NFTs can be claimed?', + content: 'Quisque eget luctus mi, vehicula mollis lorem...', + }, + { + header: 'How is the score getting calculated? ', + content: 'Suspendisse massa risus, pretium id interdum in...', + }, + { + header: 'How is the score getting calculated?', + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing...', + }, + { + header: 'When and how the NFTs can be claimed?', + content: 'Quisque eget luctus mi, vehicula mollis lorem...', + }, +] + +function VolumeContestLookup() { + const getCampaignStatus = () => { + switch (true) { + case HAS_CAMPAIGN_ENDED: + return 'Campaign Ended' + case !HAS_CAMPAIGN_STARTED: + return 'Campaign Has Not Started' + default: + return `${dayjs.utc(CURRENT_WEEK_START_DATE).format('MMM DD')} - ${dayjs.utc(CURRENT_WEEK_END_DATE).format('MMM DD')}` + } + } + return ( + + + + <TitleIconWrapper> + <img src={contestFlagIcon} alt={''} /> + </TitleIconWrapper> + <TYPE.largeHeader style={{ fontWeight: 700 }}>The Trade Federation Awakens</TYPE.largeHeader> + + + + + + + + + + + + + NFT Categories + To see which NFT you are eligible for, check the score requirements. + + + +   + + + + + + + + FAQs + + + + + + + ) +} + +export default withRouter(VolumeContestLookup) diff --git a/src/pages/VolumeContestPage.js b/src/pages/VolumeContestPage.js new file mode 100755 index 0000000..f9cbf4e --- /dev/null +++ b/src/pages/VolumeContestPage.js @@ -0,0 +1,279 @@ +import React, { useMemo, useEffect, useCallback } from 'react' +import styled from 'styled-components' +import { Tooltip as ReactTooltip } from 'react-tooltip' +import dayjs from 'dayjs' +import isBetween from 'dayjs/plugin/isBetween' + +import { PageWrapper, ContentWrapper } from '../components' +import { VolumeContestPanel } from '../components/Panel' +import { BasicLink } from '../components/Link' +import { RowBetween } from '../components/Row' +import { AutoColumn } from '../components/Column' +import VolumeContestUserScoreTable from '../components/VolumeContestUserScoreTable' +import { Banner, Title as BannerTitle } from '../components/Banner' +import VolumeContestTxnList from '../components/VolumeContestTxnList' +import Link from '../components/Link' + +import { TYPE } from '../Theme' + +import { shortenStraknetAddress, urls } from '../utils' + +import eligibilityBadgeIcon from '../../src/assets/starBadge.svg' + +import nftBannerDecoration from '../../src/assets/banners/nft.png' +import nftBannerDecoration_x2 from '../../src/assets/banners/nft@x2.png' + +import cupBannerDecoration from '../../src/assets/banners/cup.png' +import cupBannerDecoration_x2 from '../../src/assets/banners/cup@x2.png' + +import { useVolumeContestUserData } from '../contexts/VolumeContestData' +import { useUserVolumeCampaignTransactions } from '../contexts/User' + +import { useUserStraknetIdDomain } from '../hooks' + +dayjs.extend(isBetween) + +const EligibilityBadge = styled.img`` + +const EligibilityBadgeWrapper = styled.a` + display: flex; + margin-left: 12px; + cursor: help; +` + +const Header = styled.div`` + +const DashboardWrapper = styled(VolumeContestPanel)` + width: 100%; +` + +const InformationGridRow = styled.div` + display: grid; + width: 100%; + grid-template-columns: 1.1fr 0.9fr; + column-gap: 32px; + align-items: start; + justify-content: space-between; + + @media screen and (max-width: 1080px) { + grid-template-columns: 1fr; + row-gap: 32px; + } +` + +const GeneralInfoRow = styled.div` + display: grid; + column-gap: 24px; + grid-template-columns: 1.3fr 2fr; + align-items: flex-start; + width: 100%; + + @media screen and (max-width: 1280px) { + grid-template-columns: 1fr; + row-gap: 32px; + } +` + +const SectionTitle = styled.div` + font-size: 18px; + font-weight: 700; + color: #fff; +` + +const VolumeContestBanner = styled(Banner)` + box-shadow: none; + border-radius: 8px; + border: 1px solid rgba(160, 160, 160, 0.4); + background: rgba(255, 255, 255, 0.05); + + ${BannerTitle} { + font-size: 20px; + } +` + +const PositionBannersGroup = styled.div` + display: flex; + + ${VolumeContestBanner}:first-child { + border-right: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + padding-right: 40px; + } + + ${VolumeContestBanner}:not(:first-child) { + border-left: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + padding-left: 40px; + } + + ${VolumeContestBanner}:not(:last-child) { + border-right: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + ${VolumeContestBanner} + ${VolumeContestBanner} { + &:after { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translate(-50%, -50%); + width: 1px; + height: 50px; + background: #fff; + } + } +` + +const nftUserScoreLookup = { + 0: 'Not Eligible', + 1: 'T1A1', + 2: 'T1A2', + 3: 'T1A3', + 4: 'T1A4', + 5: 'T1A5', +} + +function VolumeContestAccountPage({ account }) { + const [, starknetIdDomain] = useUserStraknetIdDomain(account) + const userData = useVolumeContestUserData(account) + const transactions = useUserVolumeCampaignTransactions(account) + + const userNftLevel = userData?.nftLevel + const totalContestScore = userData?.totalContestScore + + let isUserEligible = useMemo(() => { + return (typeof userNftLevel == 'number' && userNftLevel) > 0 || false + }, [userData]) + + const getEligibleNftForUserScore = useCallback(() => { + const loadingResultStub = '...' + const isUserRankAvailable = typeof userNftLevel == 'number' && userNftLevel >= 0 + if (!isUserRankAvailable) { + return loadingResultStub + } + if (!nftUserScoreLookup[userNftLevel]) { + return nftUserScoreLookup[0] + } + return nftUserScoreLookup[userNftLevel] + }, [userNftLevel]) + + const getThisWeekUserScore = useCallback(() => { + const findCurrentWeek = (week) => dayjs().isBetween(dayjs(week.startDt), dayjs(week.endDt), 'day', '[]') + const loadingResultStub = '...' + const isAllDataAvailable = Boolean(userData?.weeks?.length) + if (!isAllDataAvailable) { + return loadingResultStub + } + const currentWeek = userData.weeks.filter(findCurrentWeek).unshift() + return currentWeek?.score ?? 0 + }, [userData]) + + const getTotalUserScore = useCallback(() => { + const loadingResultStub = '...' + const normalizedData = Number(totalContestScore) + const isUserDataAvailable = !Number.isNaN(normalizedData) && typeof normalizedData == 'number' && normalizedData >= 0 + if (!isUserDataAvailable) { + return loadingResultStub + } + return Math.round(normalizedData) + }, [totalContestScore]) + + useEffect(() => { + window.scrollTo({ + behavior: 'smooth', + top: 0, + }) + }, []) + + return ( + + + + + {'The Trade Federation Awakens'}→{' '} + + {' '} + {shortenStraknetAddress(account)}{' '} + + + +
+ + +
+ {starknetIdDomain ? starknetIdDomain : shortenStraknetAddress(account)} + {isUserEligible && ( + + + + )} +
+ + View on Starkscan + +
+
+
+ + + + + + + + {''} + + } + style={{ height: 'auto' }} + /> + + + + {''} + + } + /> + + + + + Week wise score +
+ +
+
+ + Transactions + +
+ +
+
+
+
+
+
+ +
+ ) +} + +export default VolumeContestAccountPage diff --git a/yarn.lock b/yarn.lock index 1b15a99..d5bd44b 100755 --- a/yarn.lock +++ b/yarn.lock @@ -1998,6 +1998,13 @@ "@svgr/plugin-svgo" "^4.3.1" loader-utils "^1.2.3" +"@szhsin/react-accordion@^1.2.3": + version "1.2.3" + resolved "https://registry.npmjs.org/@szhsin/react-accordion/-/react-accordion-1.2.3.tgz#ee4d83c66a89748bfe2a2b23dd14bbb3d4f63fbf" + integrity sha512-KsDmaYsv4+Rnla+fSdZMIMonzuvXlDRPaFeIa7Je0dnwY4iFry5eiw27GQ6276+tYkVRgUyfS/mEcL48efSivA== + dependencies: + react-transition-state "^2.1.1" + "@types/babel__core@^7.1.0": version "7.1.15" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.15.tgz#2ccfb1ad55a02c83f8e0ad327cbc332f55eb1024" @@ -10566,6 +10573,11 @@ react-transition-group@^4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" +react-transition-state@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/react-transition-state/-/react-transition-state-2.1.1.tgz#1601a6177926b647041b7d598bf124321ab8d25b" + integrity sha512-kQx5g1FVu9knoz1T1WkapjUgFz08qQ/g1OmuWGi3/AoEFfS0kStxrPlZx81urjCXdz2d+1DqLpU6TyLW/Ro04Q== + react-use@^12.2.0: version "12.13.0" resolved "https://registry.yarnpkg.com/react-use/-/react-use-12.13.0.tgz#dfefd8145552841f1c2213c2e79966b505a264ba"