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))
}