diff --git a/src/apps/explorer/components/SummaryCardsWidget/SummaryCards.tsx b/src/apps/explorer/components/SummaryCardsWidget/SummaryCards.tsx new file mode 100644 index 000000000..26bc4885c --- /dev/null +++ b/src/apps/explorer/components/SummaryCardsWidget/SummaryCards.tsx @@ -0,0 +1,167 @@ +import React from 'react' +import styled, { css } from 'styled-components' +import { media } from 'theme/styles/media' +import { formatDistanceToNowStrict } from 'date-fns' + +import { Card, CardContent } from 'components/common/Card' +import { CardRow } from 'components/common/CardRow' +import { TotalSummaryResponse } from '.' +import { abbreviateString } from 'utils' +import { useMediaBreakpoint } from 'hooks/useMediaBreakPoint' + +const BatchInfoHeight = '19.6rem' +const DESKTOP_TEXT_SIZE = 1.8 // rem +const MOBILE_TEXT_SIZE = 1.65 // rem + +const WrapperCardRow = styled(CardRow)` + max-width: 70%; + + ${media.mobile} { + max-width: 100%; + } +` + +const DoubleContentSize = css` + min-height: ${BatchInfoHeight}; +` +const WrapperColumn = styled.div` + /* Equivalent to use lg={8} MUI grid system */ + flex-grow: 0; + max-width: 66.666667%; + flex-basis: 66.666667%; + padding-right: 2rem; + + > div { + margin: 1rem; + max-height: ${BatchInfoHeight}; + } + + ${media.mediumDownMd} { + flex-grow: 0; + max-width: 100%; + flex-basis: 100%; + } +` +const DoubleCardStyle = css` + ${DoubleContentSize} + + ${media.mediumDownMd} { + min-height: 16rem; + } +` +const WrappedDoubleCard = styled(Card)` + ${DoubleCardStyle} +` + +const CardTransactions = styled(Card)` + ${media.mediumDownMd} { + ${DoubleCardStyle} + } +` + +const WrapperDoubleContent = styled.div` + display: flex; + flex-direction: column; + gap: 3rem; + + ${media.mediumDownMd} { + gap: 2rem; + } +` + +interface SummaryCardsProps { + summaryData: TotalSummaryResponse | undefined + children: React.ReactNode +} + +function calcDiff(a: number, b: number): number { + return (a - b === 0 ? 0 : (100 * (a - b)) / b) || 0 +} + +function getColorBySign(n: number): string { + if (n > 0) { + return 'green' + } else if (n < 0) { + return 'red1' + } + + return 'grey' +} + +export function SummaryCards({ summaryData, children }: SummaryCardsProps): JSX.Element { + const { batchInfo, dailyTransactions, totalTokens, dailyFees, isLoading } = summaryData || {} + const isDesktop = useMediaBreakpoint(['xl', 'lg']) + const valueTextSize = isDesktop ? DESKTOP_TEXT_SIZE : MOBILE_TEXT_SIZE + const rowsByCard = isDesktop ? '2row' : '3row' + const diffTransactions = (dailyTransactions && calcDiff(dailyTransactions.now, dailyTransactions.before)) || 0 + const diffFees = (dailyFees && calcDiff(dailyFees.now, dailyFees.before)) || 0 + + return ( + + <> + {children} + + + + + + + + + + + + + + + + {/** Surplus is not yet available */} + {/* + + */} + + + ) +} diff --git a/src/apps/explorer/components/SummaryCardsWidget/index.tsx b/src/apps/explorer/components/SummaryCardsWidget/index.tsx new file mode 100644 index 000000000..a8d710a6e --- /dev/null +++ b/src/apps/explorer/components/SummaryCardsWidget/index.tsx @@ -0,0 +1,81 @@ +import React, { useEffect, useState } from 'react' +import styled from 'styled-components' +import { SummaryCards } from './SummaryCards' + +import summaryData from './summaryGraphResp.json' + +const DELAY_SECONDS = 3 // Emulating API request + +export interface BatchInfo { + lastBatchDate: Date + batchId: string +} + +interface PastAndPresentValue { + now: number + before: number +} + +interface TotalSummary { + batchInfo?: BatchInfo + dailyTransactions?: PastAndPresentValue + totalTokens?: number + dailyFees?: PastAndPresentValue +} + +type RawTotalSummary = Omit & { + batchInfo: { lastBatchDate: number; batchId: string } +} + +function buildSummary(data: RawTotalSummary): TotalSummary { + return { + ...data, + batchInfo: { + ...data.batchInfo, + lastBatchDate: new Date(data.batchInfo.lastBatchDate * 1000), + }, + } +} + +export type TotalSummaryResponse = TotalSummary & { + isLoading: boolean +} + +function useGetTotalSummary(): TotalSummaryResponse | undefined { + const [summary, setSummary] = useState() + + useEffect(() => { + setSummary((prevState) => { + return { ...prevState, isLoading: true } + }) + const timer = setTimeout(() => setSummary({ ...buildSummary(summaryData), isLoading: false }), DELAY_SECONDS * 1000) + + return (): void => clearTimeout(timer) + }, []) + + return summary +} + +const Wrapper = styled.div` + display: flex; + flex: 1; + justify-content: center; +` +const VolumeChart = styled.div` + background: #28f3282c; + border-radius: 0.4rem; + height: 19.6rem; + width: 100%; +` + +export function StatsSummaryCardsWidget(): JSX.Element { + const summary = useGetTotalSummary() + + return ( + + + + + + ) +} diff --git a/src/apps/explorer/components/SummaryCardsWidget/summaryGraphResp.json b/src/apps/explorer/components/SummaryCardsWidget/summaryGraphResp.json new file mode 100644 index 000000000..82e9bf390 --- /dev/null +++ b/src/apps/explorer/components/SummaryCardsWidget/summaryGraphResp.json @@ -0,0 +1,6 @@ +{ + "batchInfo": { "lastBatchDate": 1649881035, "batchId": "0x0003723b9eb1598e12d15c69206bf13c971be0310fa5744cf9601ed79f89e29c" }, + "dailyTransactions": {"now": 612, "before": 912}, + "totalTokens": 193, + "dailyFees": {"now": 55225.61205047748511254485049406426, "before": 40361.20651840192742698089787142249} +} \ No newline at end of file diff --git a/src/apps/explorer/components/common/ShimmerBar/index.tsx b/src/apps/explorer/components/common/ShimmerBar/index.tsx new file mode 100644 index 000000000..6461be4a9 --- /dev/null +++ b/src/apps/explorer/components/common/ShimmerBar/index.tsx @@ -0,0 +1,30 @@ +import styled, { keyframes } from 'styled-components' + +const ShimmerKeyframe = keyframes` + 0% { + background-position: 0px top; + } + 90% { + background-position: 300px top; + } + 100% { + background-position: 300px top; + } +` + +const ShimmerBar = styled.div` + width: 100%; + height: 12px; + border-radius: 2px; + color: white; + background: ${({ theme }): string => + `${theme.greyOpacity} -webkit-gradient(linear, 100% 0, 0 0, from(${theme.greyOpacity}), color-stop(0.5, ${theme.borderPrimary}), to(${theme.gradient1}))`}; + background-position: -5rem top; + background-repeat: no-repeat; + -webkit-animation-name: ${ShimmerKeyframe}; + -webkit-animation-duration: 1.3s; + -webkit-animation-iteration-count: infinite; + -webkit-background-size: 5rem 100%; +` + +export default ShimmerBar diff --git a/src/apps/explorer/pages/Home/index.tsx b/src/apps/explorer/pages/Home/index.tsx index 497c8715c..93b2de00a 100644 --- a/src/apps/explorer/pages/Home/index.tsx +++ b/src/apps/explorer/pages/Home/index.tsx @@ -3,12 +3,14 @@ import { Search } from 'apps/explorer/components/common/Search' import { Wrapper as WrapperMod } from 'apps/explorer/pages/styled' import styled from 'styled-components' import { media } from 'theme/styles/media' +import { StatsSummaryCardsWidget } from 'apps/explorer/components/SummaryCardsWidget' const Wrapper = styled(WrapperMod)` max-width: 140rem; flex-flow: column wrap; - justify-content: center; + justify-content: flex-start; display: flex; + padding-top: 10rem; > h1 { justify-content: center; @@ -23,11 +25,23 @@ const Wrapper = styled(WrapperMod)` } ` +const SummaryWrapper = styled.section` + display: flex; + padding-top: 10rem; + + ${media.mobile} { + padding-top: 4rem; + } +` + export const Home: React.FC = () => { return (

Search on CoW Protocol Explorer

+ + +
) } diff --git a/src/components/common/Card/Card.tsx b/src/components/common/Card/Card.tsx index 9c5a9dfd7..fd9bed7af 100644 --- a/src/components/common/Card/Card.tsx +++ b/src/components/common/Card/Card.tsx @@ -1,6 +1,7 @@ import React from 'react' import styled from 'styled-components' -import Grid from '@material-ui/core/Grid' +import Grid, { GridSize } from '@material-ui/core/Grid' +import { media } from 'theme/styles/media' import { COLOURS } from 'styles' import { Theme } from 'theme' @@ -9,12 +10,15 @@ const { white, fadedGreyishWhite, blackLight } = COLOURS const DefaultCard = styled.div` height: inherit; - min-width: 200px; + min-width: 15rem; min-height: 100px; background-color: #f5f5f5; border-radius: 6px; box-shadow: 0 10px 15px -3px rgb(0 0 0 / 7%), 0 4px 6px -2px rgb(0 0 0 / 5%); - margin: 10px; + margin: 1rem; + ${media.xSmallDown} { + min-width: 14rem; + } ` const CardComponent = styled(DefaultCard)` @@ -35,16 +39,25 @@ const CardContent = styled.div` font-size: 15px; padding: 16px; line-height: normal; + ${media.xSmallDown} { + padding: 0.2rem; + font-size: 1.1rem; + } ` -type CardBreakdown = boolean | 1 | 'auto' | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | undefined +enum CardSize { + xs = 12, + sm = 6, + md = 4, + lg = 3, +} export interface CardBaseProps { - children?: React.ReactElement | string - xs?: CardBreakdown - sm?: CardBreakdown - md?: CardBreakdown - lg?: CardBreakdown + children?: React.ReactNode + xs?: GridSize + sm?: GridSize + md?: GridSize + lg?: GridSize } /** @@ -52,7 +65,14 @@ export interface CardBaseProps { * * An extensible content container. */ -export const Card: React.FC = ({ children, xs = 12, sm = 6, md = 4, lg = 3, ...rest }) => { +export const Card: React.FC = ({ + children, + xs = CardSize.xs, + sm = CardSize.sm, + md = CardSize.md, + lg = CardSize.lg, + ...rest +}): JSX.Element => { return ( diff --git a/src/components/common/Card/CardContent.tsx b/src/components/common/Card/CardContent.tsx index 32197b088..cc00d8c9e 100644 --- a/src/components/common/Card/CardContent.tsx +++ b/src/components/common/Card/CardContent.tsx @@ -1,6 +1,8 @@ import React from 'react' import styled from 'styled-components' +import ShimmerBar from 'apps/explorer/components/common/ShimmerBar' + export type statusType = 'success' | 'danger' export interface CardContentProps { @@ -8,10 +10,10 @@ export interface CardContentProps { direction?: string icon1?: React.ReactElement label1: string - value1: string + value1: string | number | undefined valueSize?: number labelWidth?: number - caption1?: string + caption1?: string | number captionColor?: string hint1?: string hintColor?: string @@ -20,6 +22,7 @@ export interface CardContentProps { value2?: string caption2?: string hint2?: string + loading?: boolean } const CardBody = styled.div<{ @@ -58,7 +61,7 @@ const CardBody = styled.div<{ margin-top: ${({ direction }): string => (direction === 'row' ? '0' : '8px')}; > h3 { - font-size: ${({ valueSize }): number => valueSize || 18}px; + font-size: ${({ valueSize }): number => valueSize || 1.8}rem; margin: 0px; } > span { @@ -98,6 +101,7 @@ export const CardContent: React.FC = ({ value2, caption2, hint2, + loading, }): JSX.Element => { return ( = ({ {label1}

-

{value1}

- {caption1 && ( + {loading ? :

{value1}

} + {!loading && caption1 && ( {caption1} {hint1} @@ -123,7 +127,7 @@ export const CardContent: React.FC = ({ )}
- {label2 && ( + {!loading && label2 && (

{icon2 && {icon2}  } diff --git a/src/components/common/CardRow/CardRow.stories.tsx b/src/components/common/CardRow/CardRow.stories.tsx index 7df39f5ce..1d847c857 100644 --- a/src/components/common/CardRow/CardRow.stories.tsx +++ b/src/components/common/CardRow/CardRow.stories.tsx @@ -4,9 +4,8 @@ import { Story, Meta } from '@storybook/react/types-6-0' import { GlobalStyles, ThemeToggler } from 'storybook/decorators' -import { Card } from '../Card/index' -import { CardRow, CardRowProps } from '.' -import { CardContent } from '../Card/CardContent' +import { Card, CardContent } from 'components/common/Card' +import { CardRow, CardRowProps } from 'components/common/CardRow' import QuestionIcon from '../../../assets/img/question1.svg' @@ -50,7 +49,7 @@ const Template: Story = (args) => ( /** * CardRow component. * * Place cards side-by-side */ -export const CardRow: React.FC = ({ children }) => { - return {children} +export const CardRow: React.FC = ({ children, ...rest }) => { + return ( + + {children} + + ) } diff --git a/src/hooks/useGetMatchingScreenSize.tsx b/src/hooks/useGetMatchingScreenSize.tsx new file mode 100644 index 000000000..6ffc3b4c9 --- /dev/null +++ b/src/hooks/useGetMatchingScreenSize.tsx @@ -0,0 +1,14 @@ +import { useEffect, useState } from 'react' + +import { getMatchingScreenSize, subscribeToScreenSizeChange, Breakpoints } from 'utils/mediaQueries' + +export function useGetMatchingScreenSize(): Breakpoints { + const [resolution, setResolution] = useState(getMatchingScreenSize()) + + useEffect(() => { + const mediaQuery = subscribeToScreenSizeChange(() => setResolution(getMatchingScreenSize())) + return (): void => mediaQuery() + }, []) + + return resolution +} diff --git a/src/hooks/useMediaBreakPoint.tsx b/src/hooks/useMediaBreakPoint.tsx new file mode 100644 index 000000000..b4fb3a04b --- /dev/null +++ b/src/hooks/useMediaBreakPoint.tsx @@ -0,0 +1,8 @@ +import { useGetMatchingScreenSize } from 'hooks/useGetMatchingScreenSize' +import { Breakpoints } from 'utils/mediaQueries' + +export function useMediaBreakpoint(breakpoints: Breakpoints[]): boolean { + const resolution = useGetMatchingScreenSize() + + return breakpoints.includes(resolution) +} diff --git a/src/theme/styles/global.ts b/src/theme/styles/global.ts index bbf401955..a3b793fa1 100644 --- a/src/theme/styles/global.ts +++ b/src/theme/styles/global.ts @@ -8,13 +8,13 @@ import variables from 'components/layout/GenericLayout/variablesCss' const selection = css` /* CSS for selecting text */ *::selection { - background: #218dff; /* WebKit/Blink Browsers */ + background-color: var(--color-gradient-2); /* WebKit/Blink Browsers */ } *::-moz-selection { - background: #218dff; /* Gecko Browsers */ + background-color: var(--color-gradient-2); /* Gecko Browsers */ } *::-webkit-selection { - background: #218dff; /* Chrome Browsers */ + background-color: var(--color-gradient-2); /* Chrome Browsers */ } /* End CSS for selecting text */ ` diff --git a/src/utils/mediaQueries.ts b/src/utils/mediaQueries.ts index 3a4888efa..6fafb5273 100644 --- a/src/utils/mediaQueries.ts +++ b/src/utils/mediaQueries.ts @@ -1,6 +1,8 @@ import { Command } from 'types' -export const MEDIA_QUERY_MATCHES = [ +export type Breakpoints = 'xl' | 'lg' | 'md' | 'sm' | 'xs' + +export const MEDIA_QUERY_MATCHES: Array<{ name: Breakpoints; query: string }> = [ // must be in descending order for .find to match from largest to smallest // as sm will also match for xl and lg, for example { @@ -22,9 +24,9 @@ export const MEDIA_QUERY_MATCHES = [ // anything smaller -- xs ] -const DEFAULT_QUERY_NAME = 'xs' +const DEFAULT_QUERY_NAME: Breakpoints = 'xs' -export const getMatchingScreenSize = (): string => +export const getMatchingScreenSize = (): Breakpoints => MEDIA_QUERY_MATCHES.find(({ query }) => window.matchMedia(query).matches)?.name || DEFAULT_QUERY_NAME export const MEDIA_QUERIES = MEDIA_QUERY_MATCHES.map(({ query }) => query) @@ -33,7 +35,7 @@ export const MEDIA_QUERY_NAMES = MEDIA_QUERY_MATCHES.map(({ name }) => name).con export const subscribeToScreenSizeChange = (callback: (event: MediaQueryListEvent) => void): Command => { const mediaQueryLists = MEDIA_QUERIES.map((query) => window.matchMedia(query)) - mediaQueryLists.forEach((mql) => mql.addListener(callback)) + mediaQueryLists.forEach((mql) => mql.addEventListener('change', callback)) - return (): void => mediaQueryLists.forEach((mql) => mql.removeListener(callback)) + return (): void => mediaQueryLists.forEach((mql) => mql.removeEventListener('change', callback)) }