diff --git a/__tests__/helpers.tsx b/__tests__/helpers.tsx index fedd8b07d..469b8ec55 100644 --- a/__tests__/helpers.tsx +++ b/__tests__/helpers.tsx @@ -6,7 +6,7 @@ import { ethers } from 'ethers'; import WrappedAccountSelect from 'modules/app/components/layout/header/AccountSelect'; import theme from 'lib/theme'; import React from 'react'; -import { accountsApi } from 'stores/accounts'; +import { accountsApi } from 'modules/app/stores/accounts'; import { createCurrency } from '@makerdao/currency'; import { AnalyticsProvider } from 'modules/app/client/analytics/AnalyticsContext'; import { CookiesProvider } from 'modules/app/client/cookies/CookiesContext'; diff --git a/__tests__/pages/delegates.spec.tsx b/__tests__/pages/delegates.spec.tsx index c2666af7e..bf1fa5947 100644 --- a/__tests__/pages/delegates.spec.tsx +++ b/__tests__/pages/delegates.spec.tsx @@ -86,7 +86,7 @@ describe('Delegates list page', () => { }); test('can delegate MKR to a delegate', async () => { - await screen.findByText('Recognized Delegates'); + await screen.findAllByText('Recognized Delegates'); // Open delegate modal const delegateButton = screen.getByText('Delegate'); diff --git a/__tests__/pages/esmodule.spec.tsx b/__tests__/pages/esmodule.spec.tsx index 5289d6aed..ac6b7f422 100644 --- a/__tests__/pages/esmodule.spec.tsx +++ b/__tests__/pages/esmodule.spec.tsx @@ -1,11 +1,11 @@ // @ts-nocheck import { renderWithTheme } from '../helpers'; -import { cleanup, fireEvent, waitFor, configure, screen } from '@testing-library/react'; +import { fireEvent, waitFor, configure, screen } from '@testing-library/react'; import waitForExpect from 'wait-for-expect'; import { TestAccountProvider } from '@makerdao/test-helpers'; import ESModule from '../../pages/esmodule'; import getMaker from '../../lib/maker'; -import { accountsApi } from '../../stores/accounts'; +import { accountsApi } from '../../modules/app/stores/accounts'; import BigNumber from 'bignumber.js'; import { ethers } from 'ethers'; diff --git a/__tests__/pages/executive.spec.tsx b/__tests__/pages/executive.spec.tsx index edabd5783..31deb38f9 100644 --- a/__tests__/pages/executive.spec.tsx +++ b/__tests__/pages/executive.spec.tsx @@ -10,7 +10,7 @@ import { } from '../helpers'; import { ExecutiveOverview } from '../../pages/executive'; import proposals from 'modules/executive/api/mocks/proposals.json'; -import { accountsApi } from 'stores/accounts'; +import { accountsApi } from 'modules/app/stores/accounts'; jest.mock('@theme-ui/match-media', () => { return { diff --git a/jest.config.js b/jest.config.js index cf750c8e2..73946bfd0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,7 +6,7 @@ module.exports = { ], coverageReporters: ['json', 'lcov', 'text-summary'], setupFilesAfterEnv: ['/__tests__/setup.ts'], - testPathIgnorePatterns: ['/node_modules/', '/.next/', '/setup', '/helpers', '/__tests__/__mocks__', '__tests__/lib/polling/poll-327.js', '__tests__/lib/polling/poll-431.js'], + testPathIgnorePatterns: ['/node_modules/', '/.next/', '/setup', '__tests__/helpers.tsx', '/__tests__/__mocks__', '__tests__/lib/polling/poll-327.js', '__tests__/lib/polling/poll-431.js'], transformIgnorePatterns: ['/node_modules/'], moduleDirectories: ['node_modules'], globals: { diff --git a/lib/theme.js b/lib/theme.js index c694184d6..e98b240b2 100644 --- a/lib/theme.js +++ b/lib/theme.js @@ -60,18 +60,21 @@ export default { tagColorFifteenBg: '#FFFBEF', tagColorSixteen: '#FF8237', tagColorSixteenBg: '#FFFBEF', + bull: '#1AAB9B', + bear: '#F77249', modes: { dark: { primary: '#1DC1AE', onPrimary: '#000', background: '#141414', onBackgroundAlt: '#D7C9EA', + onBackground: '#7E7E88', text: '#D7C9EA', textMuted: '#7E7E88', textSecondary: '#7E7E88', secondaryMuted: '#D7C9EA', surface: '#000', - onSurface: '#282F3A', + onSurface: '#7E7E88', outline: '#282F3A', primaryMuted: '#13554D', muted: '#282F3A', @@ -117,7 +120,9 @@ export default { tagColorFifteen: '#FF8237', tagColorFifteenBg: '#121212', tagColorSixteen: '#FF8237', - tagColorSixteenBg: '#121212' + tagColorSixteenBg: '#121212', + bull: '#1AAB9B', + bear: '#F77249' } } }, @@ -160,6 +165,10 @@ export default { variant: 'cards.primary', p: 3 }, + noPadding: { + variant: 'cards.primary', + p: 0 + }, tight: { variant: 'cards.primary', p: [2, 2] @@ -285,6 +294,13 @@ export default { color: 'textSecondary', letterSpacing: '0.05em' }, + smallCaps: { + ...theme.text.caps, + fontSize: 1, + fontWeight: 'body', + color: 'textSecondary', + letterSpacing: '0.05em' + }, secondary: { color: 'textSecondary', fontSize: '15px', @@ -1095,6 +1111,61 @@ export default { /> ) + }, + decrease: { + viewBox: '0 0 8 7', + path: ( + + + + + + ) + }, + increase: { + viewBox: '0 0 8 8', + path: ( + + + + + + ) + }, + minus: { + viewBox: '0 0 6 3', + path: + }, + plus: { + viewBox: '0 0 9 9', + path: ( + + ) } } }; diff --git a/lib/utils.ts b/lib/utils.ts index 19e89e0ec..09725eb36 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -5,8 +5,8 @@ import { cloneElement } from 'react'; import { jsx } from 'theme-ui'; import { css, ThemeUIStyleObject } from '@theme-ui/css'; import BigNumber from 'bignumber.js'; -import { CurrencyObject } from 'types/currency'; -import { SpellStateDiff } from 'types/spellStateDiff'; +import { CurrencyObject } from 'modules/app/types/currency'; +import { SpellStateDiff } from 'modules/app/types/spellStateDiff'; import { SupportedNetworks, ETHERSCAN_PREFIXES } from './constants'; import getMaker from './maker'; import mockPolls from 'modules/polling/api/mocks/polls.json'; diff --git a/modules/address/components/AddressDelegatedTo.tsx b/modules/address/components/AddressDelegatedTo.tsx new file mode 100644 index 000000000..08ff0598c --- /dev/null +++ b/modules/address/components/AddressDelegatedTo.tsx @@ -0,0 +1,213 @@ +import Link from 'next/link'; +import { Box, Text, Link as ThemeUILink, Flex, IconButton, Heading } from 'theme-ui'; +import { useBreakpointIndex } from '@theme-ui/match-media'; +import { Icon } from '@makerdao/dai-ui-icons'; + +import BigNumber from 'bignumber.js'; +import { getNetwork } from 'lib/maker'; +import { Address } from 'modules/address/components/Address'; +import Skeleton from 'modules/app/components/SkeletonThemed'; +import { DelegationHistory } from 'modules/delegates/types'; +import { useState } from 'react'; +import { getEtherscanLink } from 'lib/utils'; +import { formatDateWithTime } from 'lib/datetime'; +import Tooltip from 'modules/app/components/Tooltip'; + +type CollapsableRowProps = { + delegate: DelegationHistory; + network: string; + bpi: number; + totalDelegated: number; +}; + +const CollapsableRow = ({ delegate, network, bpi, totalDelegated }: CollapsableRowProps) => { + const [expanded, setExpanded] = useState(false); + + const { address, lockAmount, events } = delegate; + const sortedEvents = events.sort((prev, next) => (prev.blockTimestamp > next.blockTimestamp ? -1 : 1)); + + return ( + + + + + +
+ + + + {expanded && ( + + {sortedEvents.map(({ blockTimestamp }) => { + return ( + + {formatDateWithTime(blockTimestamp)} + + ); + })} + + )} + + + + {`${new BigNumber(lockAmount).toFormat(2)}${bpi > 0 ? ' MKR' : ''}`} + + {expanded && ( + + {sortedEvents.map(({ blockTimestamp, lockAmount }) => { + return ( + + {lockAmount.indexOf('-') === 0 ? ( + + ) : ( + + )} + + {`${new BigNumber( + lockAmount.indexOf('-') === 0 ? lockAmount.substring(1) : lockAmount + ).toFormat(2)}${bpi > 0 ? ' MKR' : ''}`} + + + ); + })} + + )} + + + + {totalDelegated ? ( + {`${new BigNumber(lockAmount).div(totalDelegated).times(100).toFormat(1)}%`} + ) : ( + + + + )} + + + + + + setExpanded(!expanded)}> + + + + + {expanded && ( + + {sortedEvents.map(({ blockTimestamp, hash }) => { + return ( + + + + + + ); + })} + + )} + + + ); +}; + +type DelegatedByAddressProps = { + delegatedTo: DelegationHistory[]; + totalDelegated: number; +}; + +const AddressDelegatedTo = ({ delegatedTo, totalDelegated }: DelegatedByAddressProps): JSX.Element => { + const bpi = useBreakpointIndex(); + const network = getNetwork(); + + return ( + + + + + + Address + + + MKR Delegated + + + + Voting Weight + + + + Expand + + + + + {delegatedTo ? ( + delegatedTo.map((delegate, i) => ( + + )) + ) : ( + + + + )} + +
+ + Loading + +
+
+ ); +}; + +export default AddressDelegatedTo; diff --git a/modules/address/components/AddressDetail.tsx b/modules/address/components/AddressDetail.tsx index 94d043a05..3be5d485e 100644 --- a/modules/address/components/AddressDetail.tsx +++ b/modules/address/components/AddressDetail.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { Box, Text, Link as ExternalLink, Flex, Divider } from 'theme-ui'; -import { useBreakpointIndex } from '@theme-ui/match-media'; import { Icon } from '@makerdao/dai-ui-icons'; import { getNetwork } from 'lib/maker'; import { getEtherscanLink } from 'lib/utils'; @@ -8,18 +7,41 @@ import AddressIcon from './AddressIcon'; import { PollVoteHistoryList } from 'modules/polling/components/PollVoteHistoryList'; import { AddressAPIStats, VoteProxyInfo } from '../types/addressApiResponse'; import Tooltip from 'modules/app/components/Tooltip'; -import { cutMiddle } from 'lib/string'; import { PollingParticipationOverview } from 'modules/polling/components/PollingParticipationOverview'; import { Address } from './Address'; +import useSWR from 'swr'; +import { fetchJson } from 'lib/fetchJson'; +import LastVoted from 'modules/polling/components/LastVoted'; +import AddressDelegatedTo from './AddressDelegatedTo'; +import { MKRDelegatedToAPIResponse } from 'pages/api/address/[address]/delegated-to'; +import SkeletonThemed from 'modules/app/components/SkeletonThemed'; +import { AddressMKRDelegatedStats } from './AddressMKRDelegatedStats'; type PropTypes = { address: string; - stats: AddressAPIStats; voteProxyInfo?: VoteProxyInfo; }; -export function AddressDetail({ address, stats, voteProxyInfo }: PropTypes): React.ReactElement { - const bpi = useBreakpointIndex(); +export function AddressDetail({ address, voteProxyInfo }: PropTypes): React.ReactElement { + const { data: statsData } = useSWR( + `/api/address/${address}/stats?network=${getNetwork()}`, + fetchJson, + { + revalidateOnFocus: false, + refreshInterval: 0, + revalidateOnMount: true + } + ); + + const { data: delegatedToData } = useSWR( + `/api/address/${address}/delegated-to?network=${getNetwork()}`, + fetchJson, + { + revalidateOnFocus: false, + refreshInterval: 0, + revalidateOnMount: true + } + ); const tooltipLabel = voteProxyInfo ? ( @@ -36,9 +58,18 @@ export function AddressDetail({ address, stats, voteProxyInfo }: PropTypes): Rea ) : null; return ( - + - + + + @@ -68,6 +99,44 @@ export function AddressDetail({ address, stats, voteProxyInfo }: PropTypes): Rea )} + + + + + + + + + + + + + + MKR Delegated per address + + {!delegatedToData && ( + + + + )} + {delegatedToData && delegatedToData.delegatedTo.length > 0 && ( + + )} + {delegatedToData && delegatedToData.delegatedTo.length === 0 && ( + + No MKR delegated + + )} @@ -83,11 +152,17 @@ export function AddressDetail({ address, stats, voteProxyInfo }: PropTypes): Rea Polling Proposals + + {!statsData && ( + + + + )} - + {statsData && } - + {statsData && } ); } diff --git a/modules/address/components/AddressMKRDelegatedStats.tsx b/modules/address/components/AddressMKRDelegatedStats.tsx new file mode 100644 index 000000000..2c3cac594 --- /dev/null +++ b/modules/address/components/AddressMKRDelegatedStats.tsx @@ -0,0 +1,39 @@ +import BigNumber from 'bignumber.js'; +import { Flex } from 'theme-ui'; +import { StatBox } from 'modules/app/components/StatBox'; +import { useMKRVotingWeight } from 'modules/mkr/hooks/useMKRVotingWeight'; + +export function AddressMKRDelegatedStats({ + totalMKRDelegated, + address +}: { + totalMKRDelegated?: number; + address: string; +}): React.ReactElement { + const { data: votingWeight } = useMKRVotingWeight(address); + + return ( + + + + + + ); +} diff --git a/modules/address/types/addressApiResponse.d.ts b/modules/address/types/addressApiResponse.d.ts index dd92fa3c8..b7b51ef57 100644 --- a/modules/address/types/addressApiResponse.d.ts +++ b/modules/address/types/addressApiResponse.d.ts @@ -18,5 +18,4 @@ export type AddressApiResponse = { voteProxyInfo?: VoteProxyInfo; delegateInfo?: Delegate; address: string; - stats: AddressAPIStats; }; diff --git a/modules/app/components/SystemStatsSidebar.tsx b/modules/app/components/SystemStatsSidebar.tsx index 596f1329a..7ad9ac767 100644 --- a/modules/app/components/SystemStatsSidebar.tsx +++ b/modules/app/components/SystemStatsSidebar.tsx @@ -8,7 +8,7 @@ import getMaker, { DAI, getNetwork } from 'lib/maker'; import { useMkrBalance } from 'modules/mkr/hooks/useMkrBalance'; import { bigNumberKFormat, formatAddress, getEtherscanLink } from 'lib/utils'; import BigNumber from 'bignumber.js'; -import { CurrencyObject } from 'types/currency'; +import { CurrencyObject } from 'modules/app/types/currency'; async function getSystemStats(): Promise< [CurrencyObject, BigNumber, CurrencyObject, CurrencyObject, CurrencyObject] diff --git a/modules/app/components/TxFinal.tsx b/modules/app/components/TxFinal.tsx index fbfb08be2..f575bc814 100644 --- a/modules/app/components/TxFinal.tsx +++ b/modules/app/components/TxFinal.tsx @@ -1,7 +1,7 @@ import { Button, Flex, Link, Text } from 'theme-ui'; import { Icon } from '@makerdao/dai-ui-icons'; import TxIndicators from 'modules/app/components/TxIndicators'; -import { TXMined } from 'types/transaction'; +import { TXMined } from 'modules/app/types/transaction'; import { getNetwork } from 'lib/maker'; import { getEtherscanLink } from 'lib/utils'; diff --git a/modules/app/components/TxInProgress.tsx b/modules/app/components/TxInProgress.tsx index 7a1bd41d6..eb48318c6 100644 --- a/modules/app/components/TxInProgress.tsx +++ b/modules/app/components/TxInProgress.tsx @@ -1,7 +1,7 @@ import { Flex, Text, Box, Link } from '@theme-ui/components'; import { Icon } from '@makerdao/dai-ui-icons'; import TxIndicators from 'modules/app/components/TxIndicators'; -import { TXMined } from 'types/transaction'; +import { TXMined } from 'modules/app/types/transaction'; import { getNetwork } from 'lib/maker'; import { getEtherscanLink } from 'lib/utils'; diff --git a/modules/app/components/VideoModal.tsx b/modules/app/components/VideoModal.tsx new file mode 100644 index 000000000..6900b13f2 --- /dev/null +++ b/modules/app/components/VideoModal.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { Box } from 'theme-ui'; +import { DialogOverlay, DialogContent } from '@reach/dialog'; +import { useBreakpointIndex } from '@theme-ui/match-media'; +import { fadeIn, slideUp } from 'lib/keyframes'; + +const VideoModal = ({ + embedId, + isOpen, + onDismiss +}: { + embedId?: string; + isOpen: boolean; + onDismiss: () => void; +}): React.ReactElement => { + const bpi = useBreakpointIndex(); + + return ( + + + +