├── .editorconfig ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── todo.md ├── .gitignore ├── LICENSE.md ├── README.md ├── components ├── about │ └── index.js ├── common │ ├── Identicon.js │ ├── ProfileBadge.js │ ├── footer.js │ ├── header.js │ ├── hooks │ │ └── useHover.js │ ├── layouts │ │ ├── base.js │ │ ├── dashboard.js │ │ └── documentation.js │ ├── page.js │ ├── side-menu-footer.js │ ├── sidemenu.js │ ├── utilities │ │ └── popovers │ │ │ └── network-popover.js │ └── withSlideIn.js ├── council-member-profile │ ├── CouncilMemberInfoHeader.js │ ├── CouncilMemberKeyStats.js │ ├── EditCouncilMemberProfileModal.js │ ├── council-viz │ │ ├── Circleandline.js │ │ ├── CouncilViz.js │ │ ├── Network.js │ │ └── WhiteCircles.js │ └── index.js ├── council-members │ ├── MembersTable.js │ └── index.js ├── home │ ├── FAQs.js │ ├── SocialProofStats.js │ ├── SupportedNetworks.js │ ├── index.js │ ├── landing-page-calculator │ │ ├── earnings-output.js │ │ └── index.js │ └── testimonials │ │ └── index.js ├── new-payment │ ├── ConfirmAmountChange.js │ ├── ConfirmSelection.js │ ├── RiskTag.js │ ├── ValidatorsList.js │ └── index.js ├── nominators │ ├── NominatorsTable.js │ ├── Top3Section.js │ └── index.js ├── overview │ ├── AllNominations.js │ ├── AmountConfirmation.js │ ├── AmountInput.js │ ├── ChainErrorPage.js │ ├── ChillAlert.js │ ├── EarningsOutput.js │ ├── EditControllerModal.js │ ├── EditValidators.js │ ├── ExpectedReturns.js │ ├── FundsUpdate.js │ ├── NominationsTable.js │ ├── OverviewCards.js │ ├── PastEarningsTimeRange.js │ ├── RewardDestinationModal.js │ ├── SuccessfullyBonded.js │ └── index.js ├── payment │ ├── Confirmation.js │ ├── RewardDestination.js │ ├── TermsOfService.js │ ├── Transaction.js │ └── index.js ├── policies │ ├── disclaimer-component.js │ ├── privacy-component.js │ └── terms-component.js ├── reward-calculator │ ├── AmountInput.js │ ├── CompoundRewardSlider.js │ ├── ExpectedReturnsCard.js │ ├── RiskSelect.js │ ├── RiskTag.js │ ├── TimePeriodInput.js │ ├── ValidatorsList.js │ └── index.js ├── settings │ └── index.js ├── validator-profile │ ├── EditValidatorProfileModal.js │ ├── LimitedTextArea.js │ ├── LinkedValidatorsGroup.js │ ├── ProfileTabs.js │ ├── TeamMembers.js │ ├── TransparencyScoreModal.js │ ├── ValidatorInfoHeader.js │ ├── ValidatorKeyStats.js │ ├── ValidatorReturnsCalculator.js │ ├── index.js │ └── validator-viz │ │ ├── Circleandline.js │ │ ├── Network.js │ │ ├── ValidatorViz.js │ │ └── WhiteCircles.js ├── validators │ ├── EditAmountModal.js │ ├── FilterPanel.js │ ├── ValidatorsResult.js │ ├── ValidatorsTable.js │ └── index.js └── wallet-connect │ ├── CreateWallet.js │ ├── ImportAccount.js │ ├── RecoverAuthInfo.js │ ├── RejectedPage.js │ ├── SelectAccount.js │ ├── WalletDisclaimer.js │ └── index.js ├── jsconfig.json ├── lib ├── analytics.js ├── axios.js ├── calculate-reward.js ├── convert-arr-to-object.js ├── convert-currency.js ├── format-currency.js ├── getClaimableRewards.js ├── getErasHistoric.js ├── getRewards.js ├── getTransactionFee.js ├── getUpdateFundsTransactionFee.js ├── polkadot-api.js ├── polkadot-extension.js ├── polkadot │ ├── chill.js │ ├── edit-controller.js │ ├── nominate.js │ ├── update-funds.js │ └── update-payee.js ├── routes.js ├── stake.js └── store.js ├── next.config.js ├── package.json ├── pages ├── 404.js ├── _app.js ├── _document.js ├── _error.js ├── about.js ├── council-member-profile │ └── [id].js ├── council-members.js ├── disclaimer.js ├── index.js ├── nominators.js ├── overview.js ├── payment.js ├── privacy.js ├── reward-calculator.js ├── settings.js ├── terms.js ├── validator-profile │ └── [id].js └── validators.js ├── postcss.config.js ├── public ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── images │ ├── 404.svg │ ├── badges │ │ ├── 1.svg │ │ ├── 2.svg │ │ ├── 3.svg │ │ ├── 4.svg │ │ └── gray-badge.svg │ ├── baroque.png │ ├── chris-hutchinson.png │ ├── dave-ramico.jpg │ ├── discord-logo.svg │ ├── dollar-sign.jpg │ ├── dollar-sign.svg │ ├── empty-state.svg │ ├── enea-arllai.jpg │ ├── kusama-logo-light.png │ ├── kusama-logo.png │ ├── landing-bg.svg │ ├── polkadot-logo.png │ ├── polkadot-successfully-bonded.png │ ├── polkadot-wallet-connect-info.png │ ├── polkadot-wallet-connect-success.png │ ├── polkadot-wallet-connect.png │ ├── polkadot_alert.png │ ├── riot-logo.svg │ ├── ruben-russel.png │ ├── team │ │ ├── prastut-kumar.png │ │ ├── sahil-nanda.png │ │ └── saumya-karan.png │ ├── telegram-logo.svg │ ├── unicorn-sweat │ │ ├── unicorn-sweat.png │ │ ├── unicorn-sweat.svg │ │ ├── unicorn-sweat@0.1x.png │ │ ├── unicorn-sweat@0.25x.png │ │ ├── unicorn-sweat@0.5x.png │ │ └── unicorn-sweat@0.75x.png │ ├── web3foundation_grants_badge_black.png │ └── yieldscan-logo.svg └── zeit.svg ├── styles └── index.scss ├── tailwind.config.js ├── yarn.lock └── yieldscan.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | trim_trailing_whitespace = true 2 | indent_size = 2 3 | indent_style = tab 4 | insert_final_newline = true 5 | quote_type = single 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE REQUEST]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/todo.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: TODO 3 | about: Add a TODO 4 | title: "[TODO] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # TODO 11 | 12 | ## 🎯   What is the problem that needs to be solved? 13 | 14 | Try to be as specific as possible. Think about falsifiable outcomes. 15 | 16 | **Refer:** [SMART Goal Examples](https://www.thebalancesmb.com/smart-goal-examples-2951827) 17 | 18 | ### 🗒   Summary 19 | A brief overview of the problem to be solved. Include all relevant data points. 20 | 21 | ### ✅   Outcomes 22 | **Examples:** 23 | 24 | - [ ] Create the a functional UI for returns calculator which takes `stake_amount` as input and returns `annual_percentage_return` as output 25 | - [ ] Add a functional risk level input to the calculator 26 | - [ ] Add a switch input to compound returns 27 | 28 | ## ⏳   ETA 29 | **Example:** By `26th June, 2020 - 6 PM IST` 30 | 31 | If you don't specify a timezone, by default IST shall be assumed. 32 | 33 | Refer: [ETA culture document](https://docs.google.com/document/d/1ZGa6diesOmx-jSBjm0uj8j4oN75gYqux85qA7DJOm68/edit) 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env* 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /components/common/Identicon.js: -------------------------------------------------------------------------------- 1 | import { noop } from "lodash"; 2 | import { useToast } from "@chakra-ui/core"; 3 | import BaseIdenticon from "@polkadot/react-identicon"; 4 | 5 | const Identicon = ({ 6 | address, 7 | size = "40", 8 | onClick = noop, 9 | showToast = true, 10 | }) => { 11 | const toast = useToast(); 12 | const onCopy = () => { 13 | if (showToast) { 14 | toast({ 15 | duration: 2000, 16 | status: "success", 17 | position: "top-right", 18 | description: "Address copied", 19 | }); 20 | } 21 | 22 | onClick(address); 23 | }; 24 | 25 | return ( 26 |
ev.stopPropagation()} className="-mb-1"> 27 | 33 |
34 | ); 35 | }; 36 | 37 | export default Identicon; 38 | -------------------------------------------------------------------------------- /components/common/ProfileBadge.js: -------------------------------------------------------------------------------- 1 | import { 2 | Popover, 3 | PopoverTrigger, 4 | PopoverContent, 5 | PopoverHeader, 6 | PopoverBody, 7 | PopoverArrow, 8 | PopoverCloseButton, 9 | } from "@chakra-ui/core"; 10 | 11 | const BADGE_STATIC_URL = "/images/badges/"; 12 | 13 | const BadgeInstance = (image, text) => ({ image, text }); 14 | 15 | const getBadge = (score) => { 16 | if (score > 0 && score < 200) { 17 | return BadgeInstance("1.svg", "BASIC TRANSPARENCY"); 18 | } else if (score >= 200 && score < 330) { 19 | return BadgeInstance("2.svg", "EXCELLENT TRANSPARENCY"); 20 | } else if (score >= 330) { 21 | return BadgeInstance("3.svg", "SUPREME TRANSPARENCY"); 22 | } else { 23 | return null; 24 | } 25 | }; 26 | 27 | const ProfileBadge = ({ score }) => { 28 | const badge = getBadge(score); 29 | 30 | return badge ? ( 31 |
32 | 33 | 34 | profile-badge 39 | 40 | 50 | 51 | 52 | {badge.text} 53 | 54 | 55 | 56 |
57 | ) : ( 58 | "" 59 | ); 60 | }; 61 | 62 | export default ProfileBadge; 63 | -------------------------------------------------------------------------------- /components/common/footer.js: -------------------------------------------------------------------------------- 1 | import { useAccounts, useHeaderLoading } from "@lib/store"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | import { FaDiscord, FaTelegram } from "react-icons/fa"; 5 | import { IoIosMail } from "react-icons/io"; 6 | 7 | 8 | const Footer = () => { 9 | const { accountInfoLoading } = useAccounts(); 10 | const { headerLoading } = useHeaderLoading(); 11 | return ( 12 | !accountInfoLoading && 13 | !headerLoading && ( 14 | 68 | ) 69 | ); 70 | }; 71 | 72 | export default Footer; 73 | -------------------------------------------------------------------------------- /components/common/hooks/useHover.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | 3 | const useHover = () => { 4 | const [hovered, setHovered] = useState(false); 5 | 6 | const ref = useRef(null); 7 | 8 | const handleMouseOver = () => setHovered(true); 9 | const handleMouseOut = () => setHovered(false); 10 | 11 | useEffect( 12 | () => { 13 | const node = ref.current; 14 | if (node) { 15 | node.addEventListener('mouseover', handleMouseOver); 16 | node.addEventListener('mouseout', handleMouseOut); 17 | 18 | return () => { 19 | node.removeEventListener('mouseover', handleMouseOver); 20 | node.removeEventListener('mouseout', handleMouseOut); 21 | }; 22 | } 23 | }, 24 | [ref.current] // recall only if ref changes 25 | ); 26 | 27 | return [ref, hovered]; 28 | } 29 | 30 | export default useHover; -------------------------------------------------------------------------------- /components/common/layouts/documentation.js: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import Footer from "../footer"; 3 | 4 | const Header = dynamic( 5 | () => import("@components/common/header").then((mod) => mod.default), 6 | { ssr: false } 7 | ); 8 | 9 | const withDocumentationLayout = (children) => { 10 | return () => ( 11 |
12 |
13 |
14 |
{children()}
15 |
16 |
18 | ); 19 | }; 20 | 21 | export default withDocumentationLayout; 22 | -------------------------------------------------------------------------------- /components/common/page.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import React from "react"; 3 | import { isMobile, isTablet } from "react-device-detect"; 4 | import { trackEvent, Events } from "@lib/analytics"; 5 | import { useEffect } from "react"; 6 | 7 | window.setImmediate = (cb) => cb(); 8 | 9 | const Page = ({ title, children, layoutProvider }) => { 10 | const layoutedChild = layoutProvider ? layoutProvider(children) : children; 11 | 12 | if (isMobile || isTablet) { 13 | return ( 14 | 15 | 16 | {title} - YieldScan 17 | 18 |
22 | 23 | YieldScan 24 | 25 |

26 | We don't support this device yet. To use YieldScan please visit us 27 | on your desktop / laptop. 28 |

29 |
30 |
31 | ); 32 | } 33 | 34 | useEffect(() => { 35 | trackEvent(Events.PAGE_VIEW, { path: window.location.pathname }); 36 | }, []); 37 | 38 | return ( 39 | 40 | 41 | {title} - YieldScan 42 | 43 |
{layoutedChild()}
44 |
45 | ); 46 | }; 47 | 48 | export default Page; 49 | -------------------------------------------------------------------------------- /components/common/side-menu-footer.js: -------------------------------------------------------------------------------- 1 | import { FaDiscord, FaTelegram } from "react-icons/fa"; 2 | import { IoIosMail } from "react-icons/io"; 3 | import { MenuOption } from "@components/common/sidemenu"; 4 | import { useRouter } from "next/router"; 5 | import Routes from "@lib/routes"; 6 | 7 | const SideMenuFooter = () => { 8 | const router = useRouter(); 9 | 10 | return ( 11 |
12 | {/* */} 17 | 22 |
23 | 28 | 29 | 30 | 35 | 36 | 37 | 42 | 43 | 44 |
45 |
46 | ); 47 | }; 48 | 49 | export default SideMenuFooter; 50 | -------------------------------------------------------------------------------- /components/common/sidemenu.js: -------------------------------------------------------------------------------- 1 | import { 2 | Home, 3 | UserCheck, 4 | Users, 5 | Sliders, 6 | Globe, 7 | ChevronRight, 8 | } from "react-feather"; 9 | import { useRouter } from "next/router"; 10 | import Link from "next/link"; 11 | import Routes from "@lib/routes"; 12 | import { Badge, Collapse } from "@chakra-ui/core"; 13 | import { useState } from "react"; 14 | 15 | const MenuOption = ({ 16 | label, 17 | Icon, 18 | selected = false, 19 | href, 20 | isExternal = false, 21 | }) => { 22 | return ( 23 | 24 | 40 | {Icon && } 41 | {label} 42 | 43 | 44 | ); 45 | }; 46 | 47 | const SideMenu = () => { 48 | const router = useRouter(); 49 | const [showPortfolioTabs, setShowPortfolioTabs] = useState(true); 50 | const [showExploreTabs, setShowExploreTabs] = useState(true); 51 | 52 | return ( 53 |
54 | 66 | 67 | 73 | 79 | 80 | 104 | 105 | 111 | 117 | 123 | 124 |
125 | ); 126 | }; 127 | 128 | export default SideMenu; 129 | 130 | export { MenuOption }; 131 | -------------------------------------------------------------------------------- /components/common/withSlideIn.js: -------------------------------------------------------------------------------- 1 | import { SlideIn } from '@chakra-ui/core'; 2 | 3 | const withSlideIn = (Component) => (props) => ( 4 | 5 | {(styles) => } 6 | 7 | ); 8 | 9 | export default withSlideIn; 10 | -------------------------------------------------------------------------------- /components/council-member-profile/CouncilMemberKeyStats.js: -------------------------------------------------------------------------------- 1 | import convertCurrency from "@lib/convert-currency"; 2 | import { useEffect, useState } from "react"; 3 | import formatCurrency from "@lib/format-currency"; 4 | 5 | const CouncilMemberKeyStats = ({ 6 | voters = 0, 7 | backingAmount = 0, 8 | totalBalance = 0, 9 | networkInfo, 10 | }) => { 11 | const [backingSubCurrency, setBackingSubCurrency] = useState(); 12 | const [totalBalanceSubCurrency, setTotalBalanceSubCurrency] = useState(); 13 | useEffect(() => { 14 | if (backingAmount) { 15 | convertCurrency(backingAmount, networkInfo.denom).then((value) => 16 | setBackingSubCurrency(value) 17 | ); 18 | } 19 | }, [backingAmount]); 20 | useEffect(() => { 21 | if (totalBalance) { 22 | convertCurrency(totalBalance, networkInfo.denom).then((value) => 23 | setTotalBalanceSubCurrency(value) 24 | ); 25 | } 26 | }, [totalBalance]); 27 | return ( 28 |
29 |
30 | KEY STATISTICS 31 |
32 |
33 | No. of voters 34 |

{voters}

35 |
36 |
37 | Amount of Backing 38 |

39 | {formatCurrency.methods.formatAmount( 40 | Math.trunc((backingAmount || 0) * 10 ** networkInfo.decimalPlaces), 41 | networkInfo 42 | )} 43 |

44 | {backingSubCurrency && ( 45 | 46 | $ 47 | {formatCurrency.methods.formatNumber(backingSubCurrency.toFixed(2))} 48 | 49 | )} 50 |
51 |
52 | Total Account Balance 53 |

54 | {formatCurrency.methods.formatAmount( 55 | Math.trunc((totalBalance || 0) * 10 ** networkInfo.decimalPlaces), 56 | networkInfo 57 | )} 58 |

59 | {totalBalanceSubCurrency && ( 60 | 61 | $ 62 | {formatCurrency.methods.formatNumber( 63 | totalBalanceSubCurrency.toFixed(2) 64 | )} 65 | 66 | )} 67 |
68 |
69 | ); 70 | }; 71 | 72 | export default CouncilMemberKeyStats; 73 | -------------------------------------------------------------------------------- /components/council-member-profile/council-viz/Circleandline.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Circle, Line, Text, Rect } from "react-konva"; 3 | 4 | class Circleandline extends React.Component { 5 | constructor() { 6 | super(); 7 | this.state = { 8 | showNominatorAddress: false, 9 | }; 10 | } 11 | handleOnMouseOver = (e) => { 12 | e.target.setAttrs({ 13 | scaleX: 1.3, 14 | scaleY: 1.3, 15 | }); 16 | document.body.style.cursor = "pointer"; 17 | this.setState({ showNominatorAddress: true }); 18 | }; 19 | handleOnMouseOut = (e) => { 20 | e.target.setAttrs({ 21 | scaleX: 1, 22 | scaleY: 1, 23 | }); 24 | document.body.style.cursor = "default"; 25 | this.setState({ showNominatorAddress: false }); 26 | }; 27 | 28 | render() { 29 | const cardWidth = 159; 30 | const cardHeight = 69; 31 | 32 | let backer = this.props.name !== null ? this.props.name : this.props.backer; 33 | 34 | if (backer.length > 11) { 35 | backer = backer.slice(0, 5) + "..." + backer.slice(-5); 36 | } 37 | let stake = 38 | "Stake: " + 39 | this.props.stake.toString().slice(0, 7) + 40 | ` ${this.props.networkInfo.denom}`; 41 | 42 | let x1 = this.props.x + 15; 43 | let y1 = this.props.y - 8; 44 | 45 | if (x1 > this.props.width - cardWidth) { 46 | y1 = y1 + 20; 47 | x1 = x1 - cardWidth / 2; 48 | } 49 | 50 | return ( 51 | 52 | 57 | 66 | 67 | {this.state.showNominatorAddress && ( 68 | 80 | )} 81 | 82 | {this.state.showNominatorAddress && ( 83 | 91 | )} 92 | {this.state.showNominatorAddress && ( 93 | 94 | )} 95 | 96 | ); 97 | } 98 | } 99 | export default Circleandline; 100 | -------------------------------------------------------------------------------- /components/council-member-profile/council-viz/CouncilViz.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Stage, Layer, Circle, Line, Rect, Text } from "react-konva"; 3 | import WhiteCircles from "./WhiteCircles"; 4 | import Network from "./Network"; 5 | 6 | class CouncilViz extends React.Component { 7 | constructor() { 8 | super(); 9 | this.container = React.createRef(); 10 | this.state = { 11 | stageWidth: undefined, 12 | stageHeight: undefined, 13 | }; 14 | } 15 | 16 | componentDidMount() { 17 | this.setState({ 18 | stageWidth: this.container.offsetWidth, 19 | stageHeight: this.container.offsetHeight, 20 | }); 21 | } 22 | 23 | render() { 24 | const width = 25 | this.state.stageWidth !== undefined 26 | ? this.state.stageWidth 27 | : window.innerWidth; 28 | const height = 29 | this.state.stageHeight !== undefined 30 | ? this.state.stageHeight 31 | : window.innerHeight; 32 | let valText = ""; 33 | if (this.props.memberInfo !== undefined) { 34 | valText = 35 | this.props.memberInfo.socialInfo.name !== null 36 | ? this.props.memberInfo.socialInfo.name 37 | : this.props.memberInfo.keyStats.stashId; 38 | if (valText.length > 11) { 39 | valText = valText.slice(0, 5) + "..." + valText.slice(-5); 40 | } 41 | } 42 | const NetworkName = this.props.networkName; 43 | let radius = 400; 44 | 45 | const validatorRectangleWidth = 110; 46 | const validatorRectangleHeight = 30; 47 | 48 | let opacity = 0.3; 49 | 50 | return this.props.memberInfo === undefined ? ( 51 | 52 |
53 |
54 | ) : ( 55 | <> 56 |
{ 59 | this.container = node; 60 | }} 61 | > 62 | {/*
68 | ← 69 |
*/} 70 | 71 | 72 | 73 | 81 | {/* Here n is number of white circles to draw 82 | r is radius of the imaginary circle on which we have to draw white circles 83 | x,y is center of imaginary circle 84 | */} 85 | 86 | 95 | {/* Adding 6 to stating and ending y point and 24 to length of line 96 | because the upper left corner of rectangle is at width/2,height/2 97 | so mid point of rectangle becomes width/2+12,height/2+6 98 | */} 99 | 106 | {/* Arc used to create the semicircle on the right, 107 | Rotation is used to rotate the arc drawn by 90 degrees in clockwise direction 108 | */} 109 | 119 | 120 | 130 | 141 | 147 | 148 | 149 |
150 | 151 | ); 152 | } 153 | } 154 | 155 | export default CouncilViz; 156 | -------------------------------------------------------------------------------- /components/council-member-profile/council-viz/Network.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Text } from "react-konva"; 3 | 4 | class Network extends React.Component { 5 | render() { 6 | let angle = 180 * 0.0174533 - (1 / 5) * Math.PI; 7 | const maxAngle = (1 / 5) * 2 * Math.PI; 8 | const arr = []; 9 | const reversedName = this.props.networkInfo.name.split("").reverse(); 10 | 11 | const radius = 220; 12 | 13 | reversedName.forEach((element, index) => { 14 | angle += maxAngle / (Number(reversedName.length) + 1); 15 | arr.push( 16 | 26 | ); 27 | }); 28 | 29 | return arr; 30 | } 31 | } 32 | 33 | export default Network; 34 | -------------------------------------------------------------------------------- /components/council-member-profile/council-viz/WhiteCircles.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Circleandline from "./Circleandline"; 3 | 4 | class WhiteCircles extends React.Component { 5 | render() { 6 | let angle = (2 / 3) * Math.PI; 7 | let maxAngle = (2 / 3) * Math.PI; 8 | const arr = []; 9 | 10 | // Uncomment in case we want to extend angle for nominators display 11 | // if (this.props.memberInfo.backersInfo.length > 5) { 12 | // angle = Math.PI / 4; 13 | // maxAngle = (3 / 4) * 2 * Math.PI; 14 | // } 15 | 16 | this.props.memberInfo.backersInfo.forEach((element, index) => { 17 | angle += 18 | maxAngle / (Number(this.props.memberInfo.backersInfo.length) + 1); 19 | const radius = Math.floor( 20 | Math.random() * (this.props.maxRadius - 150) + 150 21 | ); 22 | arr.push( 23 | 37 | ); 38 | }); 39 | 40 | return arr; 41 | } 42 | } 43 | 44 | export default WhiteCircles; 45 | -------------------------------------------------------------------------------- /components/council-members/MembersTable.js: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import Routes from "@lib/routes"; 3 | import Identicon from "@components/common/Identicon"; 4 | import { ExternalLink } from "react-feather"; 5 | import formatCurrency from "@lib/format-currency"; 6 | 7 | const MemberCard = ({ 8 | name, 9 | accountId, 10 | backing, 11 | totalBalance, 12 | numberOfBackers, 13 | onProfile, 14 | networkInfo, 15 | }) => { 16 | return ( 17 |
18 |
19 |
20 | 21 |
22 |
23 | 24 | {name || accountId.slice(0, 18) + "..." || "-"} 25 | 26 |
27 | View Profile 28 | 29 |
30 |
31 |
32 |
33 |
34 | 35 | No. of voters 36 | 37 |

{numberOfBackers}

38 |
39 |
40 | Backing 41 |

42 | {formatCurrency.methods.formatAmount( 43 | Math.trunc((backing || 0) * 10 ** networkInfo.decimalPlaces), 44 | networkInfo 45 | )} 46 |

47 |
48 |
49 | Balance 50 |

51 | {formatCurrency.methods.formatAmount( 52 | Math.trunc((totalBalance || 0) * 10 ** networkInfo.decimalPlaces), 53 | networkInfo 54 | )} 55 |

56 |
57 |
58 |
59 | ); 60 | }; 61 | 62 | const MembersTable = ({ members, networkInfo }) => { 63 | const router = useRouter(); 64 | 65 | return ( 66 |
67 |
68 | {members.map((member, index) => ( 69 | 77 | window.open( 78 | `${Routes.COUNCIL_MEMBER_PROFILE}/${member.accountId}`, 79 | "_blank" 80 | ) 81 | } 82 | networkInfo={networkInfo} 83 | /> 84 | ))} 85 |
86 | 91 |
92 | ); 93 | }; 94 | 95 | export default MembersTable; 96 | -------------------------------------------------------------------------------- /components/council-members/index.js: -------------------------------------------------------------------------------- 1 | import axios from "@lib/axios"; 2 | import { Spinner } from "@chakra-ui/core"; 3 | import { useState, useEffect } from "react"; 4 | import { AlertTriangle } from "react-feather"; 5 | import { useCouncil, useSelectedNetwork } from "@lib/store"; 6 | import MembersTable from "./MembersTable"; 7 | import { getNetworkInfo } from "yieldscan.config"; 8 | 9 | const Tabs = { 10 | COUNCIL_MEMBERS: "council-members", 11 | RUNNER_UPS: "runner-ups", 12 | }; 13 | 14 | const CouncilMembers = () => { 15 | const [error, setError] = useState(false); 16 | const { selectedNetwork } = useSelectedNetwork(); 17 | const networkInfo = getNetworkInfo(selectedNetwork); 18 | const { 19 | councilMembers, 20 | setCouncilMembers, 21 | councilLoading, 22 | setCouncilLoading, 23 | } = useCouncil(); 24 | const [selectedTab, setSelectedTab] = useState(Tabs.COUNCIL_MEMBERS); 25 | 26 | // useEffect(() => { 27 | // if (!councilMembers) { 28 | // setError(false); 29 | // axios 30 | // .get(`/${networkInfo.coinGeckoDenom}/council/members`) 31 | // .then(({ data }) => { 32 | // setCouncilMembers(data); 33 | // }) 34 | // .catch(() => { 35 | // setError(true); 36 | // }) 37 | // .finally(() => { 38 | // setLoading(false); 39 | // }); 40 | // } else { 41 | // setLoading(true); 42 | // } 43 | // }, []); 44 | 45 | useEffect(() => { 46 | if (!councilMembers) { 47 | // setCouncilLoading(true); 48 | setError(false); 49 | axios 50 | .get(`/${networkInfo.coinGeckoDenom}/council/members`) 51 | .then(({ data }) => { 52 | setCouncilMembers(data); 53 | }) 54 | .catch(() => { 55 | setError(true); 56 | }) 57 | .finally(() => { 58 | setCouncilLoading(false); 59 | }); 60 | } 61 | }, [councilMembers, networkInfo]); 62 | 63 | if (error) { 64 | return ( 65 |
66 |
67 | 68 | 69 | Sorry, no data for your account! :( 70 | 71 | 72 | Try changing the stash account. 73 | 74 |
75 |
76 | ); 77 | } 78 | 79 | if (councilLoading) { 80 | return ( 81 |
82 |
83 | 84 | 85 | Fetching council members... 86 | 87 |
88 |
89 | ); 90 | } 91 | 92 | return ( 93 |
94 |
95 |

96 | {selectedTab === Tabs.COUNCIL_MEMBERS 97 | ? "Council Members" 98 | : "Runner ups"} 99 |

100 |
101 | setSelectedTab(Tabs.COUNCIL_MEMBERS)} 108 | > 109 | Council Members 110 | 111 | setSelectedTab(Tabs.RUNNER_UPS)} 118 | > 119 | Runner Ups 120 | 121 |
122 |
123 | 131 |
132 | ); 133 | }; 134 | 135 | export default CouncilMembers; 136 | -------------------------------------------------------------------------------- /components/home/FAQs.js: -------------------------------------------------------------------------------- 1 | import { 2 | Accordion, 3 | AccordionItem, 4 | AccordionHeader, 5 | Heading, 6 | AccordionIcon, 7 | AccordionPanel, 8 | Link, 9 | Box, 10 | } from "@chakra-ui/core"; 11 | 12 | const FAQs = () => { 13 | return ( 14 | 15 |

16 | Frequently Asked Questions 17 |

18 |

19 | Can’t find what you were looking for?{" "} 20 | 26 | Contact us 27 | 28 |

29 | 30 | {data.map((data, index) => ( 31 | 37 | 41 | 42 | 49 | {data.question} 50 | 51 | 52 | 53 | 54 | 55 | {data.answer}{" "} 56 | {data.link !== undefined ? ( 57 | 62 | {data.link.content} 63 | 64 | ) : ( 65 | "" 66 | )} 67 | 68 | 69 | ))} 70 | 71 |
72 | ); 73 | }; 74 | 75 | export default FAQs; 76 | 77 | const data = [ 78 | { 79 | id: 1, 80 | question: "What is YieldScan?", 81 | answer: 82 | "YieldScan is a portfolio management platform for NPoS (nominated proof-of-stake) networks like Kusama and Polkadot. We aim to simplify portfolio management to make yield optimization easier and more accessible, for technical and non-technical users alike.", 83 | }, 84 | { 85 | id: 2, 86 | question: "What is staking?", 87 | answer: `Staking is the act of bonding tokens by putting them up as "collateral" for a chance to produce a valid block (and thus obtain a block reward). Validators and nominators stake their tokens in order to secure the network and are rewarded for the same.`, 88 | }, 89 | // { 90 | // id: 3, 91 | // question: "What are the risks involved with staking?", 92 | // answer: 93 | // "Polkadot uses NPoS (Nominated Proof-of-Stake) as its mechanism for selecting the validator set. It is designed with the roles of validators and nominators, to maximize chain security.", 94 | // link: { 95 | // url: 96 | // "https://wiki.polkadot.network/docs/en/learn-staking#how-does-staking-work-in-polkadot", 97 | // content: "Learn more about staking", 98 | // isExternal: true, 99 | // }, 100 | // }, 101 | // { 102 | // id: 4, 103 | // question: "How can I obtain tokens to stake?", 104 | // answer: 105 | // "Nominators secure the relay chain by selecting good validators and staking DOTs.", 106 | // link: { 107 | // url: "https://wiki.polkadot.network/docs/en/maintain-nominator", 108 | // content: "Learn more about nominators", 109 | // isExternal: true, 110 | // }, 111 | // }, 112 | { 113 | id: 5, 114 | question: "What are your fees?", 115 | answer: 116 | "As of right now, YieldScan is 100% free to use. If this changes in the future, then the fees would be displayed on the confirmation page before sending your transaction.", 117 | }, 118 | { 119 | id: 6, 120 | question: "Who keeps the custody of my funds?", 121 | answer: 122 | "You. YieldScan is 100% non-custodial. We do not hold any of your assets, you are in control of your funds at all times.", 123 | }, 124 | ]; 125 | -------------------------------------------------------------------------------- /components/home/SocialProofStats.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Spinner } from "@chakra-ui/core"; 3 | import axios from "@lib/axios"; 4 | import convertCurrency from "@lib/convert-currency"; 5 | import formatCurrency from "@lib/format-currency"; 6 | import millify from "millify"; 7 | import Link from "next/link"; 8 | 9 | const StatCard = ({ stat, description, subText }) => { 10 | return ( 11 |
12 |

{stat}

13 |

{description}

14 | {subText} 15 |
16 | ); 17 | }; 18 | 19 | const SocialProofStats = ({ networkName, networkDenom, networkUrl }) => { 20 | const [error, setError] = React.useState(false); 21 | const [loading, setLoading] = React.useState(true); 22 | const [nominatorsData, setNominatorsData] = React.useState([]); 23 | const [ 24 | totalAmountStakedSubCurrency, 25 | setTotalAmountStakedSubCurrency, 26 | ] = React.useState(); 27 | const [ 28 | totalRewardsSubCurrency, 29 | setTotalRewardsSubCurrency, 30 | ] = React.useState(); 31 | 32 | React.useEffect(() => { 33 | setLoading(true); 34 | setError(false); 35 | axios 36 | .get(`/${networkUrl}/actors/nominator/stats`) 37 | .then(({ data }) => { 38 | setNominatorsData(data); 39 | }) 40 | .catch(() => { 41 | setError(true); 42 | }) 43 | .finally(() => { 44 | setLoading(false); 45 | }); 46 | }, [networkUrl]); 47 | 48 | React.useEffect(() => { 49 | if (nominatorsData.stats) { 50 | convertCurrency( 51 | nominatorsData.stats.totalAmountStaked, 52 | networkDenom 53 | ).then((value) => setTotalAmountStakedSubCurrency(value)); 54 | convertCurrency( 55 | nominatorsData.stats.totalRewards, 56 | networkDenom 57 | ).then((value) => setTotalRewardsSubCurrency(value)); 58 | } 59 | }, [nominatorsData, networkDenom]); 60 | 61 | if (error) { 62 | return ( 63 |
64 |
🧐
65 |

66 | Sorry, something went wrong while fetching! We'll surely look into 67 | this. 68 |

69 |
70 | ); 71 | } 72 | return ( 73 |
74 | 78 | ) : ( 79 | `$ ${millify(totalAmountStakedSubCurrency)}+` 80 | ) 81 | } 82 | description={`Invested in staking on ${networkName}`} 83 | subText={ 84 |
85 |
86 |
87 |
88 |

Live

89 |
90 | } 91 | /> 92 | 96 | ) : ( 97 | formatCurrency.methods.formatNumber( 98 | Math.floor(nominatorsData.stats.nominatorsCount / 100) * 100 99 | ) + "+" 100 | ) 101 | } 102 | description="Active nominators" 103 | subText={ 104 | 105 | View nominators 106 | 107 | } 108 | /> 109 | 113 | ) : ( 114 | `$ ${millify(totalRewardsSubCurrency)}+` 115 | ) 116 | } 117 | description="Earned as staking rewards" 118 | subText={

In the past 24 hrs

} 119 | /> 120 |
121 | ); 122 | }; 123 | 124 | export default SocialProofStats; 125 | -------------------------------------------------------------------------------- /components/home/SupportedNetworks.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from "react"; 2 | 3 | const supportedNetworks = ["kusama", "polkadot"]; 4 | 5 | const SupportedNetworks = () => { 6 | return ( 7 | 8 |

9 | Networks you can start investing in now 10 |

11 |
12 | {supportedNetworks.map((network) => ( 13 |
14 | {`${network}-logo`} 19 |

{network}

20 |
21 | ))} 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default SupportedNetworks; 28 | -------------------------------------------------------------------------------- /components/home/testimonials/index.js: -------------------------------------------------------------------------------- 1 | import { Avatar, Box } from "@chakra-ui/core"; 2 | import axios from "@lib/axios"; 3 | import { times } from "lodash"; 4 | import { useEffect, useState } from "react"; 5 | import Marquee, { Motion, randomIntFromInterval } from "react-marquee-slider"; 6 | 7 | const testimonialData = [ 8 | { 9 | name: "Chris Hutchinson", 10 | avatar: "/images/chris-hutchinson.png", 11 | designation: "Head of Global Community, Web3 Foundation", 12 | comment: 13 | "Yieldscan has a very intuitive approach making it easy to select which validators to nominate with. Making staking simple for new users is a big deal, this tool does just that.", 14 | }, 15 | { 16 | name: "Ruben Russel", 17 | avatar: "/images/ruben-russel.png", 18 | designation: 19 | "Co-founder, Caribbean Blockchain Network & former PolkaDAO community manager", 20 | comment: 21 | "Yieldscan is a powerful Polkadot & Kusama analytical tool, assisting its user to make valuable calculated decisions when staking your KSM or DOT tokens. It is easy, useful and fun!", 22 | }, 23 | { 24 | name: "Baroque", 25 | avatar: "/images/baroque.png", 26 | designation: "Polkadot & Kusama community member", 27 | comment: 28 | "The yield calculator to optimise risk to reward on staking is superb. Ultimately yieldscan has value and provides great utility for stakers and nominators.", 29 | }, 30 | { 31 | name: "Enea Arllai", 32 | avatar: "/images/enea-arllai.jpg", 33 | designation: "Polkadot & Kusama community member", 34 | comment: 35 | "As a novice in the Polkadot and Kusama ecosystems, it can be an extremely daunting introduction to the new world of parachains and relay chains, epoch periods, choosing validators, working the substrate portal, learning new terminology etc. Yieldscan brings state-of-the-art analytics to take the pressure off some of the Staking confusion and makes the general user’s experience one to remember!", 36 | }, 37 | { 38 | name: "Dave Ramico", 39 | avatar: "/images/dave-ramico.jpg", 40 | designation: "PolkaDAO member", 41 | comment: 42 | "Yieldscan allows me to keep on top of my validator selection quickly and efficiently, leaving me more time to make Kusama pottery!", 43 | }, 44 | ]; 45 | 46 | const Testimonials = () => { 47 | const [users, setUsers] = useState([]); 48 | const [isPaused, setIsPaused] = useState(false); 49 | // useEffect(() => { 50 | // axios 51 | // .get("https://dummyapi.io/data/api/user?limit=10", { 52 | // headers: { "app-id": "5f97d648839b3f7fcc501aea" }, 53 | // }) 54 | // .then(({ data }) => setUsers(data)); 55 | // }); 56 | 57 | return ( 58 | 59 |

60 | See what our users are saying 61 |

62 | 63 | {testimonialData.map(({ name, avatar, designation, comment }) => ( 64 | setIsPaused(true)} 75 | onPointerLeave={() => setIsPaused(false)} 76 | > 77 | 78 |

{name}

79 |

80 | {designation} 81 | {/* 82 | {designation ? designation : "YieldScan User"} 83 | */} 84 |

85 |

{comment}

86 |
87 | ))} 88 |
89 |
90 | ); 91 | }; 92 | 93 | export default Testimonials; 94 | -------------------------------------------------------------------------------- /components/new-payment/ConfirmAmountChange.js: -------------------------------------------------------------------------------- 1 | import ValidatorsList from "./ValidatorsList"; 2 | import { ArrowRight } from "react-feather"; 3 | import formatCurrency from "@lib/format-currency"; 4 | import { Spinner, Alert, AlertIcon } from "@chakra-ui/core"; 5 | const ConfirmAmountChange = ({ 6 | stakingAmount, 7 | validators, 8 | selectedValidators, 9 | setSelectedValidators, 10 | onAdvancedSelection, 11 | bondedAmount, 12 | stakingLoading, 13 | stakingEvent, 14 | onConfirm, 15 | transactionHash, 16 | handlePopoverClose, 17 | networkInfo, 18 | }) => ( 19 |
20 | {!stakingLoading && ( 21 | <> 22 | 23 | 24 | Your nominations have been successfully updated. 25 | 30 | Track this transaction on Subscan 31 | 32 | 33 |

Change Bonding Amount

34 |
35 |
36 | Currently Bonded 37 |

{bondedAmount.currency}

38 | 39 | ${bondedAmount.subCurrency} 40 | 41 |
42 | 43 |
44 | Final Bonded Amount 45 |

{stakingAmount.currency}

46 | 47 | ${stakingAmount.subCurrency} 48 | 49 |
50 |
51 | 52 | You used a staking budget different than your currently bonded amount 53 | on the calculator. Would you like to update the amount to match? 54 | 55 | 61 | 67 | 68 | )} 69 | {stakingLoading && ( 70 |
71 | {/*

Status:

*/} 72 |
73 | {stakingEvent} 74 | 75 |
76 |
77 | )} 78 |
79 | ); 80 | 81 | export default ConfirmAmountChange; 82 | -------------------------------------------------------------------------------- /components/new-payment/ConfirmSelection.js: -------------------------------------------------------------------------------- 1 | import { get } from "lodash"; 2 | import ValidatorsList from "./ValidatorsList"; 3 | import formatCurrency from "@lib/format-currency"; 4 | import { result } from "lodash"; 5 | import { Link, Spinner } from "@chakra-ui/core"; 6 | const ConfirmSelection = ({ 7 | stakingAmount, 8 | validators, 9 | selectedValidators, 10 | setSelectedValidators, 11 | onAdvancedSelection, 12 | bondedAmount, 13 | stakingLoading, 14 | stakingEvent, 15 | handleSelectionConfirmation, 16 | result, 17 | networkInfo, 18 | }) => ( 19 |
20 | {!stakingLoading && ( 21 | <> 22 |

Confirm Selection

23 | 24 | You are about to stake your {networkInfo.denom} on the following 25 | validators. Please make sure you understand the risks before 26 | proceeding. Read the{" "} 27 | 28 | Terms of Service 29 | 30 | . 31 | 32 |
33 | Estimated Returns 34 |
35 | {formatCurrency.methods.formatAmount( 36 | Math.trunc( 37 | result.returns.currency * 10 ** networkInfo.decimalPlaces 38 | ), 39 | networkInfo 40 | )} 41 |
42 |
43 | 52 | 58 | 59 | )} 60 | {stakingLoading && ( 61 |
62 | {/*

Status:

*/} 63 |
64 | {stakingEvent} 65 | 66 |
67 |
68 | )} 69 |
70 | ); 71 | 72 | export default ConfirmSelection; 73 | -------------------------------------------------------------------------------- /components/new-payment/RiskTag.js: -------------------------------------------------------------------------------- 1 | const getRiskCategory = riskAmount => { 2 | if (Number.isNaN(riskAmount)) throw new Error('Risk amount should be a number'); 3 | if (riskAmount < 0.33) return 'Low'; 4 | if (riskAmount >= 0.33 && riskAmount <= 0.66) return 'Medium'; 5 | if (riskAmount > 0.66) return 'High'; 6 | }; 7 | 8 | const getStyleByRisk = (risk) => { 9 | switch (risk) { 10 | case 'Low': return 'text-teal-500 bg-teal-100'; 11 | case 'Medium': return 'text-orange-500 bg-orange-100'; 12 | case 'High': return 'text-red-500 bg-red-100'; 13 | default: return ''; 14 | } 15 | }; 16 | 17 | const RiskTag = ({ risk, classes }) => { 18 | const riskCategory = getRiskCategory(risk); 19 | return ( 20 | 25 | {risk} 26 | 27 | ); 28 | }; 29 | 30 | export default RiskTag; -------------------------------------------------------------------------------- /components/nominators/NominatorsTable.js: -------------------------------------------------------------------------------- 1 | import Identicon from "@components/common/Identicon"; 2 | import { ExternalLink } from "react-feather"; 3 | import formatCurrency from "@lib/format-currency"; 4 | 5 | const MemberCard = ({ 6 | name, 7 | stashId, 8 | nominations, 9 | dailyEarnings, 10 | totalAmountStaked, 11 | onProfile, 12 | networkInfo, 13 | }) => { 14 | return ( 15 |
16 |
17 |
18 | 19 |
20 |
21 | 22 | {stashId.slice(0, 6) + "..." + stashId.slice(-6)} 23 | 24 |
25 | View on Polkascan 26 | 27 |
28 |
29 |
30 |
31 |
32 | 33 | Daily Earnings 34 | 35 |

36 | {formatCurrency.methods.formatAmount( 37 | Math.trunc( 38 | (dailyEarnings || 0) * 10 ** networkInfo.decimalPlaces 39 | ), 40 | networkInfo 41 | )} 42 |

43 |
44 |
45 | 46 | Total Amount Staked 47 | 48 |

49 | {formatCurrency.methods.formatAmount( 50 | Math.trunc( 51 | (totalAmountStaked || 0) * 10 ** networkInfo.decimalPlaces 52 | ), 53 | networkInfo 54 | )} 55 |

56 |
57 |
58 | 59 | Nominations 60 | 61 |

{nominations}

62 |
63 |
64 |
65 | ); 66 | }; 67 | 68 | const NominatorsTable = ({ nominators = [], networkInfo }) => { 69 | return ( 70 |
71 |
72 | {nominators.map((member, index) => ( 73 | 81 | window.open( 82 | `https://polkascan.io/${networkInfo.coinGeckoDenom}/account/${member.nomId}`, 83 | "_blank" 84 | ) 85 | } 86 | networkInfo={networkInfo} 87 | /> 88 | ))} 89 |
90 |
91 | ); 92 | }; 93 | 94 | export default NominatorsTable; 95 | -------------------------------------------------------------------------------- /components/nominators/Top3Section.js: -------------------------------------------------------------------------------- 1 | import Identicon from "@components/common/Identicon"; 2 | import { ExternalLink } from "react-feather"; 3 | import formatCurrency from "@lib/format-currency"; 4 | 5 | const TopNominatorCard = ({ 6 | name, 7 | stashId, 8 | nominations, 9 | totalStake, 10 | dailyEarnings, 11 | onProfile, 12 | networkInfo, 13 | }) => ( 14 |
15 | 16 |
20 | 21 | {name || stashId.slice(0, 6) + "..." + stashId.slice(-6) || "-"} 22 | 23 |
27 | View on Polkascan 28 | 29 |
30 |
31 | 32 | Daily Earnings 33 | 34 |
35 | {formatCurrency.methods.formatAmount( 36 | Math.trunc((dailyEarnings || 0) * 10 ** networkInfo.decimalPlaces), 37 | networkInfo 38 | )} 39 |
40 | 41 | Total Amount Staked 42 | 43 |
44 | {formatCurrency.methods.formatAmount( 45 | Math.trunc((totalStake || 0) * 10 ** networkInfo.decimalPlaces), 46 | networkInfo 47 | )} 48 |
49 | 50 | Nominations 51 | 52 |
{nominations}
53 |
54 | ); 55 | 56 | const Top3Section = ({ nominators = [], networkInfo }) => { 57 | return ( 58 |
59 | {nominators.map((nominator) => ( 60 |
61 | 68 | window.open( 69 | `https://polkascan.io/kusama/account/${nominator.nomId}`, 70 | "_blank" 71 | ) 72 | } 73 | networkInfo={networkInfo} 74 | /> 75 |
76 | ))} 77 |
78 | ); 79 | }; 80 | 81 | export default Top3Section; 82 | -------------------------------------------------------------------------------- /components/overview/AllNominations.js: -------------------------------------------------------------------------------- 1 | import { noop } from "lodash"; 2 | import { ExternalLink } from "react-feather"; 3 | import Identicon from "@components/common/Identicon"; 4 | import Routes from "@lib/routes"; 5 | import formatCurrency from "@lib/format-currency"; 6 | import RiskTag from "@components/reward-calculator/RiskTag"; 7 | 8 | const ValidatorCard = ({ 9 | name, 10 | stashId, 11 | riskScore, 12 | commission, 13 | totalStake, 14 | networkInfo, 15 | nominators, 16 | onProfile = noop, 17 | }) => { 18 | const displayName = name 19 | ? name.length > 13 20 | ? name.slice(0, 5) + "..." + name.slice(-5) 21 | : name 22 | : stashId.slice(0, 5) + "..." + stashId.slice(-5); 23 | return ( 24 |
25 |
26 | 27 |
28 | {displayName} 29 |
30 | View Profile 31 | 32 |
33 |
34 |
35 | {/* */} 36 |
37 | {/*
38 | 39 | Nominators 40 | 41 |

{nominators}

42 |
*/} 43 |
44 | 45 | Risk Score 46 | 47 |
48 | 49 |
50 |
51 |
52 | 53 | Commission 54 | 55 |

{commission}%

56 |
57 |
58 | 59 | Total Stake 60 | 61 |

62 | {formatCurrency.methods.formatAmount( 63 | Math.trunc(totalStake * Math.pow(10, networkInfo.decimalPlaces)), 64 | networkInfo 65 | )} 66 |

67 |
68 |
69 | {false && ( 70 | 82 | )} 83 |
84 | ); 85 | }; 86 | 87 | const AllNominations = ({ nominations = [], networkInfo }) => { 88 | const onProfile = (validator) => 89 | window.open(`${Routes.VALIDATOR_PROFILE}/${validator.stashId}`, "_blank"); 90 | 91 | return ( 92 |
93 | {nominations.map((nomination) => ( 94 | onProfile(nomination)} 104 | /> 105 | ))} 106 | {!nominations.length && ( 107 |
108 | 109 | No Nominations! 110 | 111 |
112 | )} 113 |
114 | ); 115 | }; 116 | 117 | export default AllNominations; 118 | -------------------------------------------------------------------------------- /components/overview/AmountInput.js: -------------------------------------------------------------------------------- 1 | import { InputGroup, Input, InputRightElement } from "@chakra-ui/core"; 2 | import formatCurrency from "@lib/format-currency"; 3 | import { useAccounts } from "@lib/store"; 4 | import { get } from "lodash"; 5 | import { useState, useEffect } from "react"; 6 | 7 | const AmountInputDefault = ({ bonded, type, value, onChange, networkInfo }) => { 8 | const { freeAmount, stashAccount } = useAccounts(); 9 | const [inputValue, setInputValue] = useState(value.currency); 10 | const maxAmount = 11 | type === "bond" 12 | ? get(freeAmount, "currency") - networkInfo.minAmount < 0 13 | ? 0 14 | : get(freeAmount, "currency") - networkInfo.minAmount 15 | : bonded; 16 | 17 | // useEffect(() => { 18 | // if (bonded) { 19 | // onChange(bonded); 20 | // setInputValue(Number(Math.max(bonded, 0))); 21 | // } 22 | // }, [bonded]); 23 | 24 | const handleChange = (value) => { 25 | onChange(Number(value)); 26 | setInputValue(value); 27 | }; 28 | 29 | return ( 30 | 31 | { 43 | const { value } = e.target; 44 | handleChange(value); 45 | }} 46 | border="none" 47 | fontSize="lg" 48 | color="gray.600" 49 | /> 50 |
54 | ${formatCurrency.methods.formatNumber(value.subCurrency.toFixed(2))} 55 |
56 | 60 | {inputValue !== maxAmount && ( 61 | 69 | )} 70 | 71 | {networkInfo.denom} 72 | 73 | 74 | } 75 | h="full" 76 | rounded="full" 77 | fontSize="xl" 78 | w="fit-content" 79 | px={4} 80 | /> 81 |
82 | ); 83 | }; 84 | 85 | // TODO: handle `subCurrency` properly 86 | const AmountInputAlreadyBonded = ({ value, bonded, total, onChange }) => ( 87 |
88 |
89 |
90 | Currently Bonded 91 |

{bonded.currency} KSM

92 | 95 |
96 |
97 | Bond Additional Funds 98 |
99 | { 105 | const { value } = e.target; 106 | onChange(value === "" ? 0 : Number(value)); 107 | }} 108 | /> 109 | 112 |
113 | 116 |
117 |
118 |
119 | Total Staking Amount 120 |

{total.currency} KSM

121 | 124 |
125 | 133 |
134 | ); 135 | 136 | const AmountInput = ({ value, bonded, type, networkInfo, onChange }) => { 137 | return ( 138 | 145 | ); 146 | }; 147 | 148 | export default AmountInput; 149 | -------------------------------------------------------------------------------- /components/overview/ChainErrorPage.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/node"; 2 | const ChainErrorPage = ({ onConfirm, errMessage }) => { 3 | return ( 4 |
5 | 6 |

7 | Oops. There was an error processing this staking request 8 |

9 | 10 | If you think this is an error on our part, please share this with the 11 | help center and we will do our best to help. We typically respond within 12 | 2-3 days. 13 | 14 | {/* 19 | Track this transaction on PolkaScan 20 | */} 21 | 27 | 37 |
38 | ); 39 | }; 40 | 41 | export default ChainErrorPage; 42 | -------------------------------------------------------------------------------- /components/overview/ChillAlert.js: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from "react"; 2 | import withSlideIn from "@components/common/withSlideIn"; 3 | import { 4 | AlertDialog, 5 | AlertDialogBody, 6 | AlertDialogFooter, 7 | AlertDialogHeader, 8 | AlertDialogOverlay, 9 | AlertDialogContent, 10 | AlertDialogCloseButton, 11 | Button, 12 | useToast, 13 | } from "@chakra-ui/core"; 14 | import chill from "@lib/polkadot/chill"; 15 | import { useAccounts, usePolkadotApi } from "@lib/store"; 16 | 17 | const ChillAlert = withSlideIn(({ styles, close }) => { 18 | const toast = useToast(); 19 | const cancelRef = useRef(); 20 | const { stashAccount } = useAccounts(); 21 | const { apiInstance } = usePolkadotApi(); 22 | const [chilling, setChilling] = useState(false); 23 | const [closeOnOverlayClick, setCloseOnOverlayClick] = useState(true); 24 | 25 | const onConfirm = () => { 26 | setChilling(true); 27 | setCloseOnOverlayClick(false); 28 | chill(stashAccount.address, apiInstance, { 29 | onEvent: ({ message }) => { 30 | toast({ 31 | title: "Info", 32 | description: message, 33 | status: "info", 34 | duration: 3000, 35 | position: "top-right", 36 | isClosable: true, 37 | }); 38 | }, 39 | onFinish: (failed, message) => { 40 | toast({ 41 | title: failed ? "Failure" : "Success", 42 | description: message, 43 | status: failed ? "error" : "success", 44 | duration: 3000, 45 | position: "top-right", 46 | isClosable: true, 47 | }); 48 | setChilling(false); 49 | setCloseOnOverlayClick(true); 50 | close(); 51 | }, 52 | }).catch((error) => { 53 | setChilling(false); 54 | setCloseOnOverlayClick(true); 55 | toast({ 56 | title: "Error", 57 | description: error.message, 58 | status: "error", 59 | duration: 3000, 60 | position: "top-right", 61 | isClosable: true, 62 | }); 63 | }); 64 | }; 65 | 66 | return ( 67 | 74 | 75 | 76 | Stop All Nominations? 77 | {closeOnOverlayClick && 96 | 97 | ); 98 | }); 99 | 100 | export default ChillAlert; 101 | -------------------------------------------------------------------------------- /components/overview/NominationsTable.js: -------------------------------------------------------------------------------- 1 | import { upperCase, noop } from "lodash"; 2 | import { Star, ExternalLink } from "react-feather"; 3 | import RiskTag from "@components/reward-calculator/RiskTag"; 4 | import Identicon from "@components/common/Identicon"; 5 | import Routes from "@lib/routes"; 6 | import formatCurrency from "@lib/format-currency"; 7 | 8 | const StatusTag = ({ status }) => { 9 | const getColor = () => { 10 | if (status === "active") return "teal-500"; 11 | if (status === "inactive") return "red-500"; 12 | if (status === "waiting") return "gray-500"; 13 | }; 14 | 15 | const color = getColor(); 16 | 17 | return ( 18 |
21 | {upperCase(status)} 22 |
23 | ); 24 | }; 25 | 26 | const ValidatorCard = ({ 27 | name, 28 | stashId, 29 | riskScore, 30 | stakedAmount, 31 | commission, 32 | estimatedReward, 33 | onProfile = noop, 34 | networkInfo, 35 | }) => { 36 | const displayName = name 37 | ? name.length > 13 38 | ? name.slice(0, 5) + "..." + name.slice(-5) 39 | : name 40 | : stashId.slice(0, 5) + "..." + stashId.slice(-5); 41 | return ( 42 |
43 |
44 | 45 |
46 | {displayName} 47 |
48 | View Profile 49 | 50 |
51 |
52 |
53 | {/* */} 54 |
55 |
56 | 57 | Risk Score 58 | 59 |
60 | 61 |
62 |
63 |
64 | 65 | Commission 66 | 67 |

{commission} %

68 |
69 |
70 | Stake 71 |

72 | {formatCurrency.methods.formatAmount( 73 | Math.trunc(stakedAmount * 10 ** networkInfo.decimalPlaces), 74 | networkInfo 75 | )} 76 |

77 |
78 |
79 | 80 | Estimated Reward * 81 | 82 |

83 | {formatCurrency.methods.formatAmount( 84 | Math.trunc(estimatedReward * 10 ** networkInfo.decimalPlaces), 85 | networkInfo 86 | )} 87 |

88 |
89 |
90 | {false && ( 91 | 103 | )} 104 |
105 | ); 106 | }; 107 | 108 | const NominationsTable = ({ validators, networkInfo }) => { 109 | return ( 110 |
111 |
112 | {validators 113 | .filter((validator) => validator.isElected) 114 | .map((validator) => ( 115 | 124 | window.open( 125 | `${Routes.VALIDATOR_PROFILE}/${validator.stashId}`, 126 | "_blank" 127 | ) 128 | } 129 | networkInfo={networkInfo} 130 | /> 131 | ))} 132 |
133 | * Estimated Returns are calculated per era 134 |
135 |
136 | 141 |
142 | ); 143 | }; 144 | 145 | export default NominationsTable; 146 | -------------------------------------------------------------------------------- /components/overview/OverviewCards.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { noop, get, isNil } from "lodash"; 3 | import formatCurrency from "@lib/format-currency"; 4 | import convertCurrency from "@lib/convert-currency"; 5 | import { useAccounts, usePolkadotApi, useNetworkElection } from "@lib/store"; 6 | import calculateReward from "@lib/calculate-reward"; 7 | import { HelpPopover } from "@components/reward-calculator"; 8 | 9 | const OverviewCards = ({ 10 | stats, 11 | bondedAmount, 12 | activeStake, 13 | address, 14 | validators, 15 | unlockingBalances = [], 16 | openRewardDestinationModal = noop, 17 | bondFunds = noop, 18 | unbondFunds = noop, 19 | networkInfo, 20 | }) => { 21 | const totalUnlockingBalance = formatCurrency.methods.formatAmount( 22 | Math.trunc( 23 | unlockingBalances.reduce( 24 | (total, balanceInfo) => total + balanceInfo.value, 25 | 0 26 | ) 27 | ), 28 | networkInfo 29 | ); 30 | 31 | const { apiInstance } = usePolkadotApi(); 32 | const { stashAccount } = useAccounts(); 33 | const { isInElection } = useNetworkElection(); 34 | const [compounding, setCompounding] = React.useState(false); 35 | 36 | const isActivelyStaking = isNil(validators) 37 | ? false 38 | : validators.filter((validator) => validator.isElected).length !== 0 39 | ? true 40 | : false; 41 | 42 | const [totalAmountStakedFiat, setTotalAmountStakedFiat] = React.useState(); 43 | const [earningsFiat, setEarningsFiat] = React.useState(); 44 | const [estimatedRewardsFiat, setEstimatedRewardsFiat] = React.useState(); 45 | const [expectedAPR, setExpectedAPR] = React.useState(0); 46 | 47 | React.useEffect(() => { 48 | if (!isNil(apiInstance)) { 49 | apiInstance.query.staking.payee(stashAccount.address).then((payee) => { 50 | if (payee.isStaked) setCompounding(true); 51 | else { 52 | setCompounding(false); 53 | } 54 | }); 55 | } 56 | }, [stashAccount, apiInstance]); 57 | React.useEffect(() => { 58 | if (stats) { 59 | convertCurrency( 60 | stats.totalAmountStaked, 61 | networkInfo.denom 62 | ).then((value) => setTotalAmountStakedFiat(value)); 63 | } 64 | 65 | if (stats) { 66 | convertCurrency(stats.estimatedRewards, networkInfo.denom).then((value) => 67 | setEstimatedRewardsFiat(value) 68 | ); 69 | } 70 | 71 | if (validators) { 72 | calculateReward( 73 | validators.filter((validator) => validator.isElected), 74 | stats.totalAmountStaked, 75 | 12, 76 | "months", 77 | compounding, 78 | networkInfo 79 | ).then(({ yieldPercentage }) => setExpectedAPR(yieldPercentage)); 80 | } 81 | 82 | if (stats) { 83 | convertCurrency(stats.earnings, networkInfo.denom).then((value) => 84 | setEarningsFiat(value) 85 | ); 86 | } 87 | }, [stats, compounding]); 88 | 89 | return ( 90 |
91 |
92 |
93 |

Your investment

94 |
95 |
96 |

101 | {formatCurrency.methods.formatAmount( 102 | Math.trunc( 103 | Number( 104 | get(bondedAmount, "currency", 0) * 105 | 10 ** networkInfo.decimalPlaces 106 | ) 107 | ), 108 | networkInfo 109 | )} 110 |

111 | {!isActivelyStaking && ( 112 | 115 | Currently you are not earning any rewards on your 116 | investment. 117 |

118 | } 119 | /> 120 | )} 121 |
122 | {bondedAmount && ( 123 |

124 | $ 125 | {formatCurrency.methods.formatNumber( 126 | get(bondedAmount, "subCurrency").toFixed(2) 127 | )} 128 |

129 | )} 130 |
131 |
132 | 141 | 142 | 151 |
152 |
153 |
154 |
155 | ); 156 | }; 157 | 158 | export default OverviewCards; 159 | -------------------------------------------------------------------------------- /components/overview/PastEarningsTimeRange.js: -------------------------------------------------------------------------------- 1 | import { Select } from "@chakra-ui/core"; 2 | 3 | const PastEarningsTimeRange = ({ unit, onUnitChange }) => { 4 | return ( 5 | 26 | ); 27 | }; 28 | 29 | export default PastEarningsTimeRange; 30 | -------------------------------------------------------------------------------- /components/overview/SuccessfullyBonded.js: -------------------------------------------------------------------------------- 1 | import { ExternalLink } from "react-feather"; 2 | const SuccessfullyBonded = ({ transactionHash, onConfirm }) => { 3 | return ( 4 |
5 | 6 |

7 | Your staking request is successfully sent to the network 8 |

9 | 10 | Your transaction is successfully sent to the network. You can safely 11 | close this page now. You can view the status of this transaction using 12 | the link below: 13 | 14 | 19 | Track this transaction on Subscan 20 | 21 | 27 |
28 | ); 29 | }; 30 | 31 | export default SuccessfullyBonded; 32 | -------------------------------------------------------------------------------- /components/payment/RewardDestination.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { get } from "lodash"; 3 | import { CheckCircle, Circle } from "react-feather"; 4 | import Identicon from "@components/common/Identicon"; 5 | import formatCurrency from "@lib/format-currency"; 6 | import { Icon } from "@chakra-ui/core"; 7 | import { HelpPopover } from "@components/reward-calculator"; 8 | import { Events, setUserProperties, trackEvent } from "@lib/analytics"; 9 | 10 | const RewardDestination = ({ 11 | stashAccount, 12 | transactionState, 13 | setTransactionState, 14 | onConfirm, 15 | networkInfo, 16 | }) => { 17 | const [compounding] = useState(get(transactionState, "compounding", true)); 18 | const stakingAmount = get(transactionState, "stakingAmount", 0); 19 | const [destination, setDestination] = useState("Stash"); 20 | 21 | const accounts = ["Stash", "Controller"]; 22 | // if (!compounding) accounts.push("Controller"); 23 | 24 | useEffect(() => { 25 | if (!compounding) { 26 | setTransactionState({ 27 | rewardDestination: destination === "Stash" ? 1 : 2, 28 | }); 29 | setUserProperties({ 30 | rewardDestination: `${destination}, not compounding`, 31 | }); 32 | } else { 33 | setTransactionState({ rewardDestination: 0 }); 34 | setUserProperties({ 35 | rewardDestination: `${destination}, compounding`, 36 | }); 37 | } 38 | }, [destination]); 39 | 40 | return ( 41 |
42 |
43 |

Reward Destination

44 | 47 | If you don't want to lock your reward for compounding and are 48 | using distinct stash and controller accounts for staking, then you 49 | can use this option to select the account where you would like the 50 | rewards to be credited. 51 |

52 | } 53 | /> 54 |
55 | 59 |
60 | {accounts.map((accountType) => ( 61 |
{ 72 | if (!compounding) { 73 | setDestination(accountType); 74 | trackEvent(Events.ADV_PREFS_EDIT, { rewardDestination: accountType }); 75 | } 76 | }} 77 | > 78 | {destination === accountType ? ( 79 | 80 | ) : ( 81 | 82 | )} 83 |
84 | {accountType} Account 85 |
86 |
87 | ))} 88 |
89 |
90 | ); 91 | }; 92 | 93 | export default RewardDestination; 94 | -------------------------------------------------------------------------------- /components/payment/TermsOfService.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { 3 | Modal, 4 | ModalBody, 5 | ModalOverlay, 6 | ModalContent, 7 | ModalCloseButton, 8 | ModalHeader, 9 | ModalFooter, 10 | Text, 11 | Popover, 12 | PopoverTrigger, 13 | PopoverContent, 14 | PopoverArrow, 15 | PopoverBody, 16 | } from "@chakra-ui/core"; 17 | import TermsComponent from "@components/policies/terms-component"; 18 | 19 | const TermsAndServicePopover = ({ 20 | tcPopoverOpen, 21 | setTCPopoverOpen, 22 | setHasAgreed, 23 | onConfirm, 24 | }) => { 25 | const [clickDisabled, setClickDisabled] = useState(true); 26 | const [showPopover, setShowPopover] = useState(false); 27 | 28 | const handlePopoverClose = () => { 29 | setTCPopoverOpen(false); 30 | }; 31 | 32 | const handleScroll = (e) => { 33 | const bottom = 34 | e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight; 35 | if (bottom) { 36 | setClickDisabled(false); 37 | } 38 | }; 39 | 40 | return ( 41 | 48 | 49 | 50 | 51 | Terms of Service 52 | 53 | Please ensure you understand the risks before proceeding 54 | 55 | 56 | 65 | handleScroll(e)}> 66 |
67 | 68 |
69 |
70 | 71 | 72 | 73 |
setShowPopover(false)} 75 | onPointerEnter={() => setShowPopover(true)} 76 | > 77 | 90 |
91 |
92 | 101 | 102 | 103 | Please read the terms of service before proceeding 104 | 105 | 106 |
107 |
108 |
109 |
110 | ); 111 | }; 112 | 113 | export default TermsAndServicePopover; 114 | -------------------------------------------------------------------------------- /components/reward-calculator/CompoundRewardSlider.js: -------------------------------------------------------------------------------- 1 | import { Switch } from "@chakra-ui/core"; 2 | 3 | const CompoundRewardSlider = ({ 4 | checked, 5 | setChecked, 6 | trackRewardCalculatedEvent, 7 | }) => { 8 | return ( 9 |
10 | { 15 | setChecked(e.target.checked); 16 | trackRewardCalculatedEvent({ compounding: e.target.checked }); 17 | }} 18 | /> 19 | 25 | {checked ? "Yes" : "No"} 26 | 27 |
28 | ); 29 | }; 30 | 31 | export default CompoundRewardSlider; 32 | -------------------------------------------------------------------------------- /components/reward-calculator/RiskSelect.js: -------------------------------------------------------------------------------- 1 | const RiskSelect = ({ selected, setSelected, trackRewardCalculatedEvent }) => { 2 | const options = ["Low", "Medium", "High"]; 3 | return ( 4 |
5 | {options.map((option) => ( 6 | { 25 | setSelected(option); 26 | trackRewardCalculatedEvent({ riskPreference: option }); 27 | }} 28 | > 29 | {option} 30 | 31 | ))} 32 |
33 | ); 34 | }; 35 | 36 | export default RiskSelect; 37 | -------------------------------------------------------------------------------- /components/reward-calculator/RiskTag.js: -------------------------------------------------------------------------------- 1 | const getRiskCategory = riskAmount => { 2 | if (Number.isNaN(riskAmount)) throw new Error('Risk amount should be a number'); 3 | if (riskAmount < 0.33) return 'Low'; 4 | if (riskAmount >= 0.33 && riskAmount <= 0.66) return 'Medium'; 5 | if (riskAmount > 0.66) return 'High'; 6 | }; 7 | 8 | const getStyleByRisk = (risk) => { 9 | switch (risk) { 10 | case 'Low': return 'text-teal-500 bg-teal-100'; 11 | case 'Medium': return 'text-orange-500 bg-orange-100'; 12 | case 'High': return 'text-red-500 bg-red-100'; 13 | default: return ''; 14 | } 15 | }; 16 | 17 | const RiskTag = ({ risk, classes }) => { 18 | const riskCategory = getRiskCategory(risk); 19 | return ( 20 | 25 | {risk} 26 | 27 | ); 28 | }; 29 | 30 | export default RiskTag; -------------------------------------------------------------------------------- /components/reward-calculator/TimePeriodInput.js: -------------------------------------------------------------------------------- 1 | import { Select, Input, InputRightElement, InputGroup } from "@chakra-ui/core"; 2 | 3 | const TimePeriodInput = ({ 4 | value, 5 | unit, 6 | onChange, 7 | onUnitChange, 8 | trackRewardCalculatedEvent, 9 | }) => { 10 | return ( 11 |
12 | 13 | { 29 | onUnitChange(ev.target.value); 30 | trackRewardCalculatedEvent({ 31 | timePeriod: `${value === 0 ? null : value} ${ 32 | ev.target.value 33 | }`, 34 | }); 35 | }} 36 | > 37 | 38 | 39 | 40 | 41 | } 42 | width="fit-content" 43 | rounded="full" 44 | py={4} 45 | px={2} 46 | fontSize="sm" 47 | /> 48 | { 57 | ev.target.value < 1 58 | ? onChange("") 59 | : onChange(Number(ev.target.value)); 60 | trackRewardCalculatedEvent({ 61 | timePeriod: `${Number(ev.target.value)} ${unit}`, 62 | }); 63 | }} 64 | border="none" 65 | fontSize="lg" 66 | /> 67 | 68 | {/*
69 | onChange(Number(ev.target.value))} 74 | className="w-24 text-2xl p-0 outline-none" 75 | /> 76 |
77 |
78 | 94 |
*/} 95 |
96 | ); 97 | }; 98 | 99 | export default TimePeriodInput; 100 | -------------------------------------------------------------------------------- /components/settings/index.js: -------------------------------------------------------------------------------- 1 | const { Divider } = require("@chakra-ui/core"); 2 | import Link from "next/link"; 3 | import { ChevronRight } from "react-feather"; 4 | import { FaDiscord, FaTelegram } from "react-icons/fa"; 5 | 6 | const Settings = () => { 7 | return ( 8 |
9 |

Settings

10 |
11 |

Preferences

12 | 13 |
14 | 20 |
21 |
22 |
23 |

Join Our Community

24 | 25 | 41 |
42 | 72 |
73 | ); 74 | }; 75 | 76 | export default Settings; 77 | -------------------------------------------------------------------------------- /components/validator-profile/LimitedTextArea.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | export default function LimitedTextarea({ 3 | rows, 4 | cols, 5 | value, 6 | limit, 7 | className, 8 | placeholder, 9 | setVision 10 | }) { 11 | const [content, setContent] = React.useState(value); 12 | 13 | const setFormattedContent = (text) => { 14 | text.length > limit ? setContent(text.slice(0, limit)) : setContent(text); 15 | }; 16 | 17 | React.useEffect(() => setVision(content), [content]); 18 | 19 | React.useEffect(() => { 20 | setFormattedContent(content); 21 | }, []); 22 | 23 | return ( 24 |
25 |