diff --git a/.gitignore b/.gitignore index 8692cf66..e0295839 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +/.vscode diff --git a/src/apis/blocks.ts b/src/apis/blocks.ts new file mode 100644 index 00000000..d24625b3 --- /dev/null +++ b/src/apis/blocks.ts @@ -0,0 +1,17 @@ +import { axiosInstance } from '@utils/helpers/useFetch/useFetch'; +import { BLOCK_URL } from '@utils/constants/urls'; +import { IBlock } from '@utils/types/IBlocks'; + +const getLatestBlock = async (limit = 8) => { + const { + data: { data, timestamp }, + }: { data: { data: IBlock[]; timestamp: number } } = await axiosInstance.get(BLOCK_URL, { + params: { limit }, + }); + + const blockTuple: [string, IBlock][] = data.map((block: IBlock) => [block.id, block]); + const mapDate = new Map(blockTuple); + return { data: mapDate, timestamp }; +}; + +export default { getLatestBlock }; diff --git a/src/apis/transactions.ts b/src/apis/transactions.ts new file mode 100644 index 00000000..c9c154cf --- /dev/null +++ b/src/apis/transactions.ts @@ -0,0 +1,20 @@ +import { axiosInstance } from '@utils/helpers/useFetch/useFetch'; +import { TRANSACTION_URL } from '@utils/constants/urls'; +import { ITransaction } from '@utils/types/ITransactions'; + +const getLatestTransactions = async (limit = 8) => { + const { + data: { data, timestamp }, + }: { data: { data: ITransaction[]; timestamp: number } } = await axiosInstance.get( + TRANSACTION_URL, + { + params: { offset: 0, limit, sortBy: 'timestamp', sortDirection: 'DESC' }, + }, + ); + + const blockTuple: [string, ITransaction][] = data.map((block: ITransaction) => [block.id, block]); + const mapDate = new Map(blockTuple); + return { data: mapDate, timestamp }; +}; + +export default { getLatestTransactions }; diff --git a/src/components/SearchBar/SearchBar.styles.ts b/src/components/SearchBar/SearchBar.styles.ts index 5e794706..b00f5b3c 100644 --- a/src/components/SearchBar/SearchBar.styles.ts +++ b/src/components/SearchBar/SearchBar.styles.ts @@ -19,6 +19,7 @@ export const AppBar = styled(MuiAppBar)` export const IconButton = styled(MuiIconButton)` svg { + color: ${props => props.theme.palette.text.primary}; width: 22px; height: 22px; } diff --git a/src/components/SearchBar/SwitchMode.tsx b/src/components/SearchBar/SwitchMode.tsx index af46fc89..91614973 100644 --- a/src/components/SearchBar/SwitchMode.tsx +++ b/src/components/SearchBar/SwitchMode.tsx @@ -1,34 +1,47 @@ -import { useCallback, ChangeEvent } from 'react'; -import Switch from '@material-ui/core/Switch'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; +import { useCallback, MouseEvent } from 'react'; +import Brightness4Icon from '@material-ui/icons/Brightness4'; +import Brightness7Icon from '@material-ui/icons/Brightness7'; import { useDispatch, useSelector } from 'react-redux'; import { setAppThemeAction } from '@redux/actions/appThemeAction'; import { getThemeState } from '@redux/reducers/appThemeReducer'; -import themeVariant from '@theme/variants'; +import { IconButton } from '@material-ui/core'; -const { - custom: { - blue: { solitude, licorice }, - }, -} = themeVariant; -const SwitchMode = () => { +interface IProps { + isMobile?: boolean; +} +function SwitchMode({ isMobile }: IProps) { const dispatch = useDispatch(); const isDarkMode = useSelector(getThemeState).darkMode; - const handleChangeMode = useCallback((event: ChangeEvent) => { - const { checked: value } = event.target; - dispatch(setAppThemeAction(value)); - localStorage.setItem('darkMode', value ? 'true' : 'false'); - }, []); + const handleChangeMode = useCallback( + (event: MouseEvent) => { + event.preventDefault(); + dispatch(setAppThemeAction(!isDarkMode)); + localStorage.setItem('darkMode', !isDarkMode ? 'true' : 'false'); + }, + [isDarkMode], + ); return ( -
- } - label={isDarkMode ? 'Light' : 'Dark'} - style={{ color: isDarkMode ? solitude : licorice }} - /> -
+ + {isDarkMode ? ( + + ) : ( + + )} + {isMobile && Toggle light/dark theme} + ); +} +SwitchMode.defaultProps = { + isMobile: false, }; - export default SwitchMode; diff --git a/src/components/Sidebar/Sidebar.styles.ts b/src/components/Sidebar/Sidebar.styles.ts index 6774aab6..144414c8 100644 --- a/src/components/Sidebar/Sidebar.styles.ts +++ b/src/components/Sidebar/Sidebar.styles.ts @@ -97,6 +97,7 @@ export const Category = styled(ListItem)` &:hover { background: rgba(0, 0, 0, 0.08); + border-radius: ${props => props.theme.sidebar.radius.active}; } transition: all 0.3s ease-in-out; &.${props => props.activeClassName} { diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index 685a452a..150c27bf 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -199,8 +199,8 @@ const Sidebar: React.FC = ({ location, . ))} - - + + diff --git a/src/components/Summary/Summary.tsx b/src/components/Summary/Summary.tsx index 5410814d..f644d7d7 100644 --- a/src/components/Summary/Summary.tsx +++ b/src/components/Summary/Summary.tsx @@ -1,21 +1,33 @@ import * as React from 'react'; -// import { Grid } from '@material-ui/core'; +import { darken } from '@material-ui/core'; import { Skeleton } from '@material-ui/lab'; import { makeStyles } from '@material-ui/styles'; +import { useDispatch } from 'react-redux'; +import { useLocation } from 'react-router-dom'; +// application import { TAppTheme } from '@theme/index'; import * as URLS from '@utils/constants/urls'; import { useFetch } from '@utils/helpers/useFetch/useFetch'; import { formatNumber } from '@utils/helpers/formatNumbers/formatNumbers'; import { ISummary, ISummaryStats } from '@utils/types/ISummary'; import { SocketContext } from '@context/socket'; - import themeVariant from '@theme/variants'; - +import { AppThunkDispatch } from '@redux/types'; +import { BlockThunks, TransactionThunks } from '@redux/thunk'; +import { ISocketData } from '@utils/types/ISocketData'; import * as Styles from './Summary.styles'; import { initialSummaryList, calculateDifference } from './Summary.helpers'; const useStyles = makeStyles((_theme: TAppTheme) => ({ + wrapper: { + [_theme.breakpoints.up('md')]: { + width: 'calc(100vw - 273px)', + }, + [_theme.breakpoints.up('xl')]: { + width: '100%', + }, + }, root: { display: 'flex', padding: `${_theme.spacing(4)}px ${_theme.spacing(3)}px ${_theme.spacing(6)}px`, @@ -24,10 +36,17 @@ const useStyles = makeStyles((_theme: TAppTheme) => ({ [_theme.breakpoints.down('md')]: { padding: `${_theme.spacing(2)}px`, }, + '&::-webkit-scrollbar': { + background: _theme.palette.background.default, + }, + '&::-webkit-scrollbar-thumb': { + background: darken(_theme.palette.background.paper, 0.5), + }, }, cardItem: { margin: `0 ${_theme.spacing(3)}px`, minWidth: '142px', + flex: '0 0 auto', [_theme.breakpoints.down('md')]: { margin: `0 ${_theme.spacing(2)}px`, }, @@ -45,7 +64,8 @@ const Summary: React.FC = () => { const { fetchData } = useFetch({ method: 'get', url: URLS.SUMMARY_URL }); const socket = React.useContext(SocketContext); const classes = useStyles(); - + const { pathname } = useLocation(); + const dispatch = useDispatch(); const generateSummaryData = React.useCallback((summary: ISummary) => { const { currentStats, lastDayStats } = summary; @@ -77,9 +97,26 @@ const Summary: React.FC = () => { }, [fetchData]); React.useEffect(() => { - socket.on('getUpdateBlock', () => { - updateSummaryList(); - }); + socket.on( + 'getUpdateBlock', + ({ blocks, unconfirmedTransactions = [], rawTransactions = [] }: ISocketData) => { + updateSummaryList(); + if (pathname === '/') { + if (blocks && blocks.length) { + dispatch(BlockThunks.updateBlocksNewest(blocks[0])); + } + if (blocks.length || unconfirmedTransactions.length || rawTransactions.length) { + dispatch( + TransactionThunks.updateTransactionsNewest({ + blocks, + unconfirmedTransactions, + rawTransactions, + }), + ); + } + } + }, + ); return () => { socket.off('getUpdateBlock'); }; @@ -87,72 +124,74 @@ const Summary: React.FC = () => { React.useEffect(() => updateSummaryList(), []); return ( -
- {summaryList.map(({ id, name, value, difference }) => ( - - - - {name} - - - - {value === null ? : value} - - - {difference === null ? ( - - ) : ( - 0 ? themeVariant.custom.green.dark : themeVariant.custom.red.dark - }`} - > - Since yesterday -
- - {`${difference > 0 ? '+' : ''}`} - {difference}%  - {difference > 0 ? ( - - - - ) : ( - - - - )} - -
- )} -
-
- ))} +
+
+ {summaryList.map(({ id, name, value, difference }) => ( + + + + {name} + + + + {value === null ? : value} + + + {difference === null ? ( + + ) : ( + 0 ? themeVariant.custom.green.dark : themeVariant.custom.red.dark + }`} + > + Since yesterday +
+ + {`${difference > 0 ? '+' : ''}`} + {difference}%  + {difference > 0 ? ( + + + + ) : ( + + + + )} + +
+ )} +
+
+ ))} +
); }; diff --git a/src/context/socket.tsx b/src/context/socket.tsx index 7f8b186a..5c54becd 100644 --- a/src/context/socket.tsx +++ b/src/context/socket.tsx @@ -2,6 +2,10 @@ import { createContext } from 'react'; import { io, Socket } from 'socket.io-client'; import { BASE_URL } from '@utils/constants/urls'; -export const socket = io(BASE_URL || ''); +export const socket = io(BASE_URL || '', { + path: '/socket.io', + transports: ['websocket', 'polling'], + secure: true, +}); export const SocketContext = createContext(socket); diff --git a/src/pages/Explorer/Explorer.tsx b/src/pages/Explorer/Explorer.tsx index 31f21624..8e28e09f 100644 --- a/src/pages/Explorer/Explorer.tsx +++ b/src/pages/Explorer/Explorer.tsx @@ -13,6 +13,8 @@ import ExplorerMap from './ExplorerMap/ExplorerMap'; import LatestTransactions from './LatestTransactions/LatestTransactions'; import SupernodeStatistics from './SupernodeStatistics/SupernodeStatistics'; import { transformGeoLocationConnections, groupGeoLocationConnections } from './Explorer.helpers'; +import LatestTransactionsRT from './LatestTransactionsRT'; +import LatestBlocks from './LatestBlocks'; const Explorer: React.FC = () => { const [supernodeList, setSupernodeList] = React.useState | null>(null); @@ -55,6 +57,14 @@ const Explorer: React.FC = () => { + + + + + + + + diff --git a/src/pages/Explorer/LatestBlocks.tsx b/src/pages/Explorer/LatestBlocks.tsx new file mode 100644 index 00000000..de8590fb --- /dev/null +++ b/src/pages/Explorer/LatestBlocks.tsx @@ -0,0 +1,111 @@ +// React +import { memo, useEffect } from 'react'; +// third party +import { withStyles, makeStyles } from '@material-ui/core/styles'; +import { useDispatch } from 'react-redux'; +import { Link } from 'react-router-dom'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Paper from '@material-ui/core/Paper'; +import Typography from '@material-ui/core/Typography'; +// application +import { formattedDate } from '@utils/helpers/date/date'; +import { TAppTheme } from '@theme/index'; +import { BlockThunks } from '@redux/thunk'; +import { AppThunkDispatch } from '@redux/types'; +import { useBlockLatestBlocks } from '@redux/hooks/blocksHooks'; +import Skeleton from '@material-ui/lab/Skeleton'; +import { generateBlockKeyValue } from '@pages/Explorer/LatestTransactions/LatestTransactions.helpers'; + +const StyledTableCell = withStyles((theme: TAppTheme) => ({ + head: { + backgroundColor: theme.palette.background.default, + color: theme.palette.text.primary, + fontWeight: 600, + }, + body: { + fontSize: 14, + }, +}))(TableCell); + +const StyledTableRow = withStyles((theme: TAppTheme) => ({ + root: { + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover, + }, + }, +}))(TableRow); + +const useStyles = makeStyles({ + table: { + minWidth: 700, + }, + hashCell: { + maxWidth: 250, + }, + viewAll: { + padding: 16, + }, +}); + +function LatestBlocks() { + const classes = useStyles(); + const dispatch = useDispatch(); + const latestBlocks = useBlockLatestBlocks(); + useEffect(() => { + dispatch(BlockThunks.getLatestBlocks()); + }, []); + + return ( +
+

Latest Blocks (Live)

+ + + + + Block + Hash + TXs + Size + Timestamp + + + + {latestBlocks && latestBlocks.size ? ( + Array.from(latestBlocks.values()).map(block => ( + + + {generateBlockKeyValue(block.id || '', block.height || '')} + + + {block.id} + + {block.transactionCount} + {block.size.toLocaleString('en')} + {formattedDate(block.timestamp)} + + )) + ) : ( + + + + + + )} + +
+
+
+ + {`View all >>`} + +
+
+ ); +} + +export default memo(LatestBlocks); diff --git a/src/pages/Explorer/LatestTransactions/LatestTransactions.columns.ts b/src/pages/Explorer/LatestTransactions/LatestTransactions.columns.ts index 893200e1..1aa6fc43 100644 --- a/src/pages/Explorer/LatestTransactions/LatestTransactions.columns.ts +++ b/src/pages/Explorer/LatestTransactions/LatestTransactions.columns.ts @@ -17,7 +17,7 @@ export const columns = [ width: 360, minWidth: 360, flexGrow: 1, - label: 'Hash', + label: 'TXID', dataKey: BLOCK_HASH_KEY, disableSort: false, }, diff --git a/src/pages/Explorer/LastestTransactionsRT.tsx b/src/pages/Explorer/LatestTransactionsRT.tsx similarity index 60% rename from src/pages/Explorer/LastestTransactionsRT.tsx rename to src/pages/Explorer/LatestTransactionsRT.tsx index 5ddea931..b7912e45 100644 --- a/src/pages/Explorer/LastestTransactionsRT.tsx +++ b/src/pages/Explorer/LatestTransactionsRT.tsx @@ -1,6 +1,8 @@ -import { memo, useContext, useState, useEffect } from 'react'; -// import Table, { RowsProps } from '@components/Table/Table'; +// React +import { memo, useEffect } from 'react'; +// third party import { withStyles, makeStyles } from '@material-ui/core/styles'; +import { Link } from 'react-router-dom'; import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; import TableCell from '@material-ui/core/TableCell'; @@ -9,13 +11,16 @@ import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; import Paper from '@material-ui/core/Paper'; import Typography from '@material-ui/core/Typography'; +import Skeleton from '@material-ui/lab/Skeleton'; +import { useDispatch } from 'react-redux'; +// application import { TAppTheme } from '@theme/index'; -import { SocketContext } from '@context/socket'; -import { IRawTransactions } from '@utils/types/ITransactions'; -import { ISocketData } from '@utils/types/ISocketData'; import { generateBlockKeyValue } from '@pages/Explorer/LatestTransactions/LatestTransactions.helpers'; -import { setTransactionsLive } from '@utils/helpers/statisticsLib'; -import Skeleton from '@material-ui/lab/Skeleton'; +import { TransactionThunks } from '@redux/thunk'; +import { AppThunkDispatch } from '@redux/types'; +import { useTransactionLatestTransactions } from '@redux/hooks/transactionsHooks'; +import { ITransaction } from '@utils/types/ITransactions'; +import { TRANSACTION_DETAILS } from '@utils/constants/routes'; const StyledTableCell = withStyles((theme: TAppTheme) => ({ head: { @@ -23,6 +28,7 @@ const StyledTableCell = withStyles((theme: TAppTheme) => ({ color: theme.palette.text.primary, fontWeight: 600, }, + body: { fontSize: 14, }, @@ -36,40 +42,27 @@ const StyledTableRow = withStyles((theme: TAppTheme) => ({ }, }))(TableRow); -const useStyles = makeStyles({ +const useStyles = makeStyles(theme => ({ table: { minWidth: 700, }, -}); - -type ITransactionState = IRawTransactions & { pslPrice: number; recepients: number }; + link: { + color: theme.palette.text.primary, + textDecoration: 'none', + }, +})); -function LastestTransactions() { - const socket = useContext(SocketContext); - const [txs, setTxs] = useState>(new Map()); +function LatestTransactions() { + const dispatch = useDispatch(); + const transactions = useTransactionLatestTransactions(); useEffect(() => { - socket.on( - 'getUpdateBlock', - ({ unconfirmedTransactions = [], rawTransactions = [], blocks = [] }: ISocketData) => { - if ( - (unconfirmedTransactions && unconfirmedTransactions.length) || - (rawTransactions && rawTransactions.length) - ) { - setTxs(prev => - setTransactionsLive(prev, { unconfirmedTransactions, rawTransactions, blocks }), - ); - } - }, - ); - return () => { - socket.off('getUpdateBlock'); - }; + dispatch(TransactionThunks.getLatestBlocks()); }, []); const classes = useStyles(); return (
-

Lastest Transactions (Live)

+

Latest Transactions (Live)

@@ -82,19 +75,21 @@ function LastestTransactions() { - {txs.size > 0 ? ( - Array.from(txs.values()).map((tx: ITransactionState) => ( - + {transactions.size > 0 ? ( + Array.from(transactions.values()).map((tx: ITransaction) => ( + - {generateBlockKeyValue(tx.blockhash || '', tx.height || '')} + {generateBlockKeyValue(tx.blockHash || '', tx.block.height || '')} - - {tx.txid} + + + {tx.id} + - {tx.pslPrice.toLocaleString('en')} + {(+tx.totalAmount.toFixed(2)).toLocaleString('en')} - {tx.recepients} + {tx.recipientCount} {tx.fee || '--'} )) @@ -112,4 +107,4 @@ function LastestTransactions() { ); } -export default memo(LastestTransactions); +export default memo(LatestTransactions); diff --git a/src/pages/HistoricalStatistics/Chart/styles.ts b/src/pages/HistoricalStatistics/Chart/styles.ts index 55b45ac3..cd65e215 100644 --- a/src/pages/HistoricalStatistics/Chart/styles.ts +++ b/src/pages/HistoricalStatistics/Chart/styles.ts @@ -70,6 +70,13 @@ export const eChartLineStyles = makeStyles((theme: TAppTheme) => ({ reactECharts: { width: '100%', height: 'calc(100vh - 450px) !important', + [theme.breakpoints.down('lg')]: { + height: 450, + width: 'calc(100vw - 354px)', + }, + [theme.breakpoints.down('md')]: { + width: '100%', + }, }, granularitySelect: { display: 'flex', diff --git a/src/pages/HistoricalStatistics/index.tsx b/src/pages/HistoricalStatistics/index.tsx index 1b368067..edaf32cb 100644 --- a/src/pages/HistoricalStatistics/index.tsx +++ b/src/pages/HistoricalStatistics/index.tsx @@ -7,20 +7,6 @@ const Statistics = () => { return ( Pastel Statistics - - - {statistics.map(({ id, image, title, url }) => ( diff --git a/src/pages/Statistics/BlockStatistics/BlockStatistics.styles.ts b/src/pages/Statistics/BlockStatistics/BlockStatistics.styles.ts deleted file mode 100644 index b5c7c8e9..00000000 --- a/src/pages/Statistics/BlockStatistics/BlockStatistics.styles.ts +++ /dev/null @@ -1,9 +0,0 @@ -import styled from 'styled-components/macro'; -import { Grid } from '@material-ui/core'; - -export const BlocksContainer = styled(Grid)` - flex-wrap: nowrap; - // max-height: 172px; - padding: 25px 20px; - overflow-x: scroll; -`; diff --git a/src/pages/Statistics/BlockStatistics/BlockStatistics.tsx b/src/pages/Statistics/BlockStatistics/BlockStatistics.tsx index 41ea9c5b..cca4b6f3 100644 --- a/src/pages/Statistics/BlockStatistics/BlockStatistics.tsx +++ b/src/pages/Statistics/BlockStatistics/BlockStatistics.tsx @@ -1,11 +1,14 @@ import * as React from 'react'; +// third party import { useHistory } from 'react-router-dom'; import { format, fromUnixTime } from 'date-fns'; -import { Grid } from '@material-ui/core'; +import { Grid, darken } from '@material-ui/core'; import { Skeleton } from '@material-ui/lab'; +import { makeStyles } from '@material-ui/styles'; +import { TAppTheme } from '@theme/index'; +// application import Header from '@components/Header/Header'; -// import LineChart from '@components/Charts/LineChart/LineChart'; import { EChartsLineChart } from '@pages/HistoricalStatistics/Chart/EChartsLineChart'; import { BlockUnconfirmed } from '@utils/types/ITransactions'; @@ -18,17 +21,33 @@ import { useBackgroundChart } from '@utils/hooks'; import { info } from '@utils/constants/statistics'; import { SocketContext } from '@context/socket'; -import * as Styles from './BlockStatistics.styles'; - import BlockVisualization from './BlockVisualization/BlockVisualization'; -import { - transformBlocksData, - ITransformBlocksData, - // generateBlocksChartData, -} from './BlockStatistics.helpers'; +import { transformBlocksData, ITransformBlocksData } from './BlockStatistics.helpers'; const BLOCK_ELEMENTS_COUNT = 8; +const useStyles = makeStyles((_theme: TAppTheme) => ({ + wrapper: { + margin: 0, + [_theme.breakpoints.up('md')]: { + width: 'calc(100vw - 314px)', + }, + }, + root: { + padding: '25px 20px', + overflowX: 'auto', + width: '100%', + margin: 0, + marginBottom: 16, + '&::-webkit-scrollbar': { + background: _theme.palette.background.default, + }, + '&::-webkit-scrollbar-thumb': { + background: darken(_theme.palette.background.paper, 0.5), + }, + }, +})); + interface ChartProps { labels: Array; data: Array; @@ -36,6 +55,7 @@ interface ChartProps { const StatisticsBlocks: React.FC = () => { const history = useHistory(); + const classes = useStyles(); const [blockElements, setBlockElements] = React.useState>([]); const [chartData, setChartData] = React.useState(null); const [currentBgColor, handleBgColorChange] = useBackgroundChart(); @@ -93,48 +113,52 @@ const StatisticsBlocks: React.FC = () => { return ( <>
- + {blockElements && blockElements.length ? ( - - - {blocksUnconfirmed && blocksUnconfirmed.length - ? blocksUnconfirmed.map(({ height, size, txsCount }, idx) => ( - - - - )) - : null} - -
- - {blockElements - .slice(1, 8) - .map(({ id, height, size, transactionCount, minutesAgo }) => ( - + + {blocksUnconfirmed && blocksUnconfirmed.length + ? blocksUnconfirmed.map(({ height, size, txsCount }, idx) => ( + history.push(`${ROUTES.BLOCK_DETAILS}/${id}`)} - height={height} - size={size} - transactionCount={transactionCount} - minutesAgo={minutesAgo} + height="--" + className="block-unconfirmed" + size={`${(size / 1024).toFixed(2)} kB`} + transactionCount={`${txsCount} transactions`} + minutesAgo={`In ~${(blocksUnconfirmed.length - idx) * 10} minutes`} /> - ))} - + )) + : null} + +
+ + {blockElements.slice(1, 8).map(({ id, height, size, transactionCount, minutesAgo }) => ( + + history.push(`${ROUTES.BLOCK_DETAILS}/${id}`)} + height={height} + size={size} + transactionCount={transactionCount} + minutesAgo={minutesAgo} + /> + + ))} ) : ( diff --git a/src/pages/Statistics/NetworkStatistics/NetworkStatistics.tsx b/src/pages/Statistics/NetworkStatistics/NetworkStatistics.tsx index d137c2b2..6b194e57 100644 --- a/src/pages/Statistics/NetworkStatistics/NetworkStatistics.tsx +++ b/src/pages/Statistics/NetworkStatistics/NetworkStatistics.tsx @@ -32,7 +32,7 @@ const NetworkStatistics: React.FC = () => { return ( <>
- + {chartData || !isLoading ? (
diff --git a/src/pages/Statistics/TransactionStatistics/TransactionStatistics.tsx b/src/pages/Statistics/TransactionStatistics/TransactionStatistics.tsx index 4e9d838b..190c33e0 100644 --- a/src/pages/Statistics/TransactionStatistics/TransactionStatistics.tsx +++ b/src/pages/Statistics/TransactionStatistics/TransactionStatistics.tsx @@ -2,8 +2,6 @@ import { Grid } from '@material-ui/core'; import Header from '@components/Header/Header'; -// import VolumeTransactionsChart from './VolumeTransactionsChart/VolumeTransactionsChart'; -// import IncomingTransactionsChart from './IncomingTransactionsChart/IncomingTransactionsChart'; import IncomingTransactions from './IncomingTransactions'; import VolumeTransactions from './VolumeTransactions'; @@ -11,8 +9,8 @@ const TransactionStatistics: React.FC = () => { return ( <>
- - + + diff --git a/src/pages/Statistics/index.tsx b/src/pages/Statistics/index.tsx index 10e7828b..5f30e47a 100644 --- a/src/pages/Statistics/index.tsx +++ b/src/pages/Statistics/index.tsx @@ -18,13 +18,13 @@ const useStyles = makeStyles((theme: TAppTheme) => ({ const Statistics = () => { const classes = useStyles(); return ( - <> +
- +
); }; diff --git a/src/redux/actions/actionTypes.ts b/src/redux/actions/actionTypes.ts index 967ce3fd..0aa030ce 100644 --- a/src/redux/actions/actionTypes.ts +++ b/src/redux/actions/actionTypes.ts @@ -8,3 +8,16 @@ export const SET_APP_THEME = 'SET_APP_THEME'; // Filter types export const SET_FILTER_VALUE = 'SET_FILTER_VALUE'; + +// Blocks types +export const SET_LOADING_BLOCK = 'SET_LOADING_BLOCK'; + +export const SET_LATEST_BLOCKS = 'SET_LATEST_BLOCKS'; + +// Update block realtime +export const UPDATE_BLOCKS_NEWEST = 'UPDATE_BLOCKS_NEWEST'; + +// Blocks types +export const SET_LOADING_TRANSACTION = 'SET_LOADING_TRANSACTION'; + +export const SET_LATEST_TRASACTIONS = 'SET_LATEST_TRASACTIONS'; diff --git a/src/redux/actions/blocksAction.ts b/src/redux/actions/blocksAction.ts new file mode 100644 index 00000000..b0356610 --- /dev/null +++ b/src/redux/actions/blocksAction.ts @@ -0,0 +1,47 @@ +import { + SET_LATEST_BLOCKS, + UPDATE_BLOCKS_NEWEST, + SET_LOADING_BLOCK, +} from '@redux/actions/actionTypes'; +import { IBlock, IRawBlock } from '@utils/types/IBlocks'; + +export interface SetLatestBlocks { + type: typeof SET_LATEST_BLOCKS; + payload: { latestBlocks: Map; timestamp?: number }; +} + +export interface UpdateBlocksNewest { + type: typeof UPDATE_BLOCKS_NEWEST; + payload: IRawBlock; +} + +export interface SetLoadingBlock { + type: typeof SET_LOADING_BLOCK; + payload?: boolean; +} + +export function setLatestBlocks(payload: { + latestBlocks: Map; + timestamp?: number; +}): SetLatestBlocks { + return { + type: SET_LATEST_BLOCKS, + payload, + }; +} + +export function updateBlocksNewest(payload: IRawBlock): UpdateBlocksNewest { + return { + type: UPDATE_BLOCKS_NEWEST, + payload, + }; +} + +export function setLoadingBlock(payload = false): SetLoadingBlock { + return { + type: SET_LOADING_BLOCK, + payload, + }; +} + +export type BlocksActionsTypes = SetLatestBlocks | UpdateBlocksNewest | SetLoadingBlock; diff --git a/src/redux/actions/transactionAction.ts b/src/redux/actions/transactionAction.ts new file mode 100644 index 00000000..5e81aa55 --- /dev/null +++ b/src/redux/actions/transactionAction.ts @@ -0,0 +1,28 @@ +import { SET_LOADING_TRANSACTION, SET_LATEST_TRASACTIONS } from '@redux/actions/actionTypes'; +import { ITransaction } from '@utils/types/ITransactions'; + +export interface SetLoadingTransaction { + type: typeof SET_LOADING_TRANSACTION; + payload?: boolean; +} + +export interface SetLatestTransactions { + type: typeof SET_LATEST_TRASACTIONS; + payload: Map; +} + +export function setLoadingTransaction(payload = false) { + return { + type: SET_LOADING_TRANSACTION, + payload, + }; +} + +export function setLatestTransactions(payload: Map) { + return { + type: SET_LATEST_TRASACTIONS, + payload, + }; +} + +export type TransactionActionsTypes = SetLoadingTransaction | SetLatestTransactions; diff --git a/src/redux/hooks/appHooks.ts b/src/redux/hooks/appHooks.ts new file mode 100644 index 00000000..a807a574 --- /dev/null +++ b/src/redux/hooks/appHooks.ts @@ -0,0 +1,5 @@ +import { useSelector, TypedUseSelectorHook } from 'react-redux'; + +import { RootState } from '@redux/store'; + +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/src/redux/hooks/blocksHooks.ts b/src/redux/hooks/blocksHooks.ts new file mode 100644 index 00000000..734ab840 --- /dev/null +++ b/src/redux/hooks/blocksHooks.ts @@ -0,0 +1,19 @@ +import { useAppSelector } from '@redux/hooks/appHooks'; +import { BLOCK_NAMESPACE, IBlockState } from '@redux/reducers/blockReducer'; + +// eslint-disable-next-line +export function useBlockSelector(selector: (_state: IBlockState) => T): T { + return useAppSelector(state => selector(state[BLOCK_NAMESPACE])); +} + +export function useBlockLatestBlocks() { + return useBlockSelector(state => state.latestBlocks); +} + +export function useBlockIsLoading() { + return useBlockSelector(state => state.isLoading); +} + +export function useBlockTimestamp() { + return useBlockSelector(state => state.timestamp); +} diff --git a/src/redux/hooks/transactionsHooks.ts b/src/redux/hooks/transactionsHooks.ts new file mode 100644 index 00000000..e76266f4 --- /dev/null +++ b/src/redux/hooks/transactionsHooks.ts @@ -0,0 +1,15 @@ +import { useAppSelector } from '@redux/hooks/appHooks'; +import { TRANSACTION_NAMESPACE, ITransactionState } from '@redux/reducers/transactionReducer'; +import { ITransaction } from '@utils/types/ITransactions'; + +export function useTransactionSelector(selector: (_state: ITransactionState) => T): T { + return useAppSelector(state => selector(state[TRANSACTION_NAMESPACE])); +} + +export function useTransactionLatestTransactions() { + return useTransactionSelector>(state => state.latestTransaction); +} + +export function useTransactionIsLoading() { + return useTransactionSelector(state => state.isLoading); +} diff --git a/src/redux/reducers/blockReducer.ts b/src/redux/reducers/blockReducer.ts new file mode 100644 index 00000000..a5b7d2ca --- /dev/null +++ b/src/redux/reducers/blockReducer.ts @@ -0,0 +1,30 @@ +import * as types from '@redux/actions/actionTypes'; +import { IBlock } from '@utils/types/IBlocks'; +import { BlocksActionsTypes } from '@redux/actions/blocksAction'; + +export const BLOCK_NAMESPACE = 'block'; + +export interface IBlockState { + latestBlocks: Map; + isLoading: boolean; + timestamp: number | null; +} + +const inititalState: IBlockState = { + latestBlocks: new Map(), + isLoading: true, + timestamp: null, +}; + +const reducer = (state: IBlockState = inititalState, actions: BlocksActionsTypes) => { + switch (actions.type) { + case types.SET_LOADING_BLOCK: + return { ...state, isLoading: true }; + case types.SET_LATEST_BLOCKS: + return { ...state, ...actions.payload, isLoading: false }; + default: + return state; + } +}; + +export default { [BLOCK_NAMESPACE]: reducer }; diff --git a/src/redux/reducers/index.ts b/src/redux/reducers/index.ts index 3c495ea8..91d21799 100644 --- a/src/redux/reducers/index.ts +++ b/src/redux/reducers/index.ts @@ -5,6 +5,8 @@ import infoDrawerReducer from './infoDrawerReducer'; import clusterReducer from './clusterReducer'; import appThemeReducer from './appThemeReducer'; import filterReducer from './filterReducer'; +import blockReducer from './blockReducer'; +import transactionReducer from './transactionReducer'; export const rootReducer = combineReducers({ ...responseErrorsReducer, @@ -12,6 +14,8 @@ export const rootReducer = combineReducers({ cluster: clusterReducer, ...appThemeReducer, ...filterReducer, + ...blockReducer, + ...transactionReducer, }); type RootReducerType = typeof rootReducer; diff --git a/src/redux/reducers/transactionReducer.ts b/src/redux/reducers/transactionReducer.ts new file mode 100644 index 00000000..fc0e2968 --- /dev/null +++ b/src/redux/reducers/transactionReducer.ts @@ -0,0 +1,28 @@ +import * as types from '@redux/actions/actionTypes'; +import { ITransaction } from '@utils/types/ITransactions'; +import { TransactionActionsTypes } from '@redux/actions/transactionAction'; + +export const TRANSACTION_NAMESPACE = 'transaction'; + +export interface ITransactionState { + latestTransaction: Map; + isLoading: boolean; +} + +const inititalState: ITransactionState = { + latestTransaction: new Map(), + isLoading: true, +}; + +const reducer = (state: ITransactionState = inititalState, actions: TransactionActionsTypes) => { + switch (actions.type) { + case types.SET_LOADING_TRANSACTION: + return { ...state, isLoading: true }; + case types.SET_LATEST_TRASACTIONS: + return { ...state, latestTransaction: actions.payload, isLoading: false }; + default: + return state; + } +}; + +export default { [TRANSACTION_NAMESPACE]: reducer }; diff --git a/src/redux/store/index.ts b/src/redux/store/index.ts index 205d31f0..d33a4f86 100644 --- a/src/redux/store/index.ts +++ b/src/redux/store/index.ts @@ -10,4 +10,6 @@ const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk))) export type AppDispatchType = typeof store.dispatch; +export type RootState = ReturnType; + export default store; diff --git a/src/redux/thunk/blockThunks.ts b/src/redux/thunk/blockThunks.ts new file mode 100644 index 00000000..9a58863b --- /dev/null +++ b/src/redux/thunk/blockThunks.ts @@ -0,0 +1,57 @@ +import { AppThunk } from '@redux/types'; +import { setLoadingBlock, setLatestBlocks } from '@redux/actions/blocksAction'; +import blockApis from '@apis/blocks'; +import { setResponseError } from '@redux/actions/responseErrorsActions'; +import { IBlock, IRawBlock } from '@utils/types/IBlocks'; +import { BLOCK_NAMESPACE } from '@redux/reducers/blockReducer'; + +export const getLatestBlocks: () => AppThunk> = (limit = 6) => async ( + dispatch, + // _getState, +): Promise => { + try { + dispatch(setLoadingBlock()); + + const { data, timestamp } = await blockApis.getLatestBlock(limit); + + dispatch(setLatestBlocks({ latestBlocks: data, timestamp })); + } catch (error) { + dispatch(setResponseError(true, error.message)); + } +}; + +export const updateBlocksNewest: (_block: IRawBlock) => AppThunk> = block => async ( + dispatch, + getState, +): Promise => { + const prevBlocks = getState()[BLOCK_NAMESPACE].latestBlocks; + const newBlocks = new Map(); + newBlocks.set(block.hash, { + id: block.hash, + difficulty: block.difficulty, + confirmations: block.confirmations, + height: block.height, + merkleRoot: block.merkleroot, + nextBlockHash: '', + nonce: block.nonce, + previousBlockHash: block.previousblockhash, + size: block.size, + solution: block.solution, + timestamp: block.time, + transactionCount: block.transactions.length, + transactions: block.transactions, + }); + let i = 1; + prevBlocks.forEach((value, key) => { + if (i < 6) { + newBlocks.set(key, value); + } + i += 1; + }); + dispatch(setLatestBlocks({ latestBlocks: newBlocks })); +}; + +export default { + getLatestBlocks, + updateBlocksNewest, +}; diff --git a/src/redux/thunk/index.ts b/src/redux/thunk/index.ts new file mode 100644 index 00000000..9a9bb6c7 --- /dev/null +++ b/src/redux/thunk/index.ts @@ -0,0 +1,2 @@ +export { default as BlockThunks } from './blockThunks'; +export { default as TransactionThunks } from './transactionThunks'; diff --git a/src/redux/thunk/transactionThunks.ts b/src/redux/thunk/transactionThunks.ts new file mode 100644 index 00000000..cd7aba27 --- /dev/null +++ b/src/redux/thunk/transactionThunks.ts @@ -0,0 +1,35 @@ +import { AppThunk } from '@redux/types'; +import { setLoadingTransaction, setLatestTransactions } from '@redux/actions/transactionAction'; +import transactionApis from '@apis/transactions'; +import { setResponseError } from '@redux/actions/responseErrorsActions'; +import { TRANSACTION_NAMESPACE } from '@redux/reducers/transactionReducer'; +import { ISocketData } from '@utils/types/ISocketData'; +import { setTransactionsLive } from '@utils/helpers/statisticsLib'; + +export const getLatestBlocks: () => AppThunk> = (limit = 6) => async ( + dispatch, + // _getState, +): Promise => { + try { + dispatch(setLoadingTransaction()); + + const { data } = await transactionApis.getLatestTransactions(limit); + + dispatch(setLatestTransactions(data)); + } catch (error) { + dispatch(setResponseError(true, error.message)); + } +}; + +export const updateTransactionsNewest: ( + _rawTransactions: ISocketData, +) => AppThunk> = data => async (dispatch, getState): Promise => { + const prevBlocks = getState()[TRANSACTION_NAMESPACE].latestTransaction; + const newBlocks = setTransactionsLive(prevBlocks, data); + dispatch(setLatestTransactions(newBlocks)); +}; + +export default { + getLatestBlocks, + updateTransactionsNewest, +}; diff --git a/src/redux/types.ts b/src/redux/types.ts new file mode 100644 index 00000000..9271c7ed --- /dev/null +++ b/src/redux/types.ts @@ -0,0 +1,12 @@ +import { ThunkAction, ThunkDispatch } from 'redux-thunk'; +import { Action } from 'redux'; +import { RootState } from '@redux/store'; + +export type AppThunk = ThunkAction< + ReturnType, + RootState, + unknown, + Action +>; + +export type AppThunkDispatch = ThunkDispatch>; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 92fb7149..1d06f911 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -202,7 +202,7 @@ const transactionInBlockStatisticsRoutes = { id: 'transactionInBlock', path: ROUTES.STATISTICS_TRANSACTION_IN_BLOCK, component: TransactionInBlock, - seoTitle: 'Transaction In Block', + seoTitle: 'Transactions In Block', children: null, exact: false, }; diff --git a/src/utils/constants/statistics.ts b/src/utils/constants/statistics.ts index 4096dba8..3e40ee49 100644 --- a/src/utils/constants/statistics.ts +++ b/src/utils/constants/statistics.ts @@ -72,7 +72,7 @@ export const statistics = [ }, { id: 'transactionInBlock', - title: 'Transaction In Block', + title: 'Transactions In Block', url: routes.STATISTICS_TRANSACTION_IN_BLOCK, image: '/images/statistics/transactionsinblock.jpg', }, diff --git a/src/utils/helpers/statisticsLib.ts b/src/utils/helpers/statisticsLib.ts index 747817c0..e75a7724 100644 --- a/src/utils/helpers/statisticsLib.ts +++ b/src/utils/helpers/statisticsLib.ts @@ -16,7 +16,7 @@ import { } from '@utils/types/IStatistics'; import { IBlock } from '@utils/types/IBlocks'; import { formattedDate } from '@utils/helpers/date/date'; -import { IRawTransactions, ITransactionState } from '@utils/types/ITransactions'; +import { IRawTransactions, ITransaction } from '@utils/types/ITransactions'; import { ISocketData } from '@utils/types/ISocketData'; export type PeriodTypes = @@ -289,29 +289,39 @@ export function convertYAxisLabel(value: number, maxY: number): number | string } export function setTransactionsLive( - prev: Map, + prev: Map, { rawTransactions = [], unconfirmedTransactions = [], blocks }: ISocketData, ) { - const newTxs: Map = new Map(); + const newTxs: Map = new Map(); if (rawTransactions.length > 0) { - let height: number; - if (blocks && blocks.length) { - height = blocks[0].height; - } + const block = blocks[0]; rawTransactions.forEach((item: IRawTransactions) => { let pslPrice = 0; item.vout.forEach(({ value }) => { pslPrice += value; }); + const fee = prev.get(item.txid)?.fee || item.fee || 0; newTxs.set(item.txid, { - ...item, - pslPrice: +pslPrice.toFixed(2), - recepients: item.vout.length, - height, + // ...item, + block: { + height: `${block.height || ''}`, + confirmations: block.confirmations, + }, + blockHash: block.hash, + coinbase: 0, + id: item.txid, + isNonStandard: 1, + rawData: '', + fee, + height: block.height, + totalAmount: +pslPrice.toFixed(2), + recipientCount: item.vout.length, + timestamp: item.time, + size: item.size || 0, }); }); - return newTxs; + // return newTxs; } if (unconfirmedTransactions.length) { unconfirmedTransactions.forEach((item: IRawTransactions) => { @@ -319,16 +329,35 @@ export function setTransactionsLive( item.vout.forEach(({ value }) => { pslPrice += value; }); + const fee = prev.get(item.txid)?.fee || item.fee || 0; newTxs.set(item.txid, { - ...item, - pslPrice: +pslPrice.toFixed(2), - recepients: item.vout.length, + block: { + height: '', + confirmations: 0, + }, + blockHash: item.blockhash || '', + coinbase: 0, + id: item.txid, + isNonStandard: 1, + rawData: '', + fee, + height: 0, + totalAmount: +pslPrice.toFixed(2), + recipientCount: item.vout.length, + timestamp: item.time, + size: item.size || 0, }); }); } if (prev.size > 0) { + let i = newTxs.size; prev.forEach((value, key) => { - newTxs.set(key, value); + if (i < 6) { + if (!newTxs.get(key)) { + newTxs.set(key, value); + i += 1; + } + } }); } return newTxs; diff --git a/src/utils/helpers/useFetch/useFetch.ts b/src/utils/helpers/useFetch/useFetch.ts index b201599f..dafb2318 100644 --- a/src/utils/helpers/useFetch/useFetch.ts +++ b/src/utils/helpers/useFetch/useFetch.ts @@ -16,7 +16,7 @@ interface IFetchDataOptions { params?: { [key: string]: string | number }; } -const axiosInstance = Axios.create({ +export const axiosInstance = Axios.create({ baseURL: URLS.BASE_URL, }); diff --git a/src/utils/types/IBlocks.ts b/src/utils/types/IBlocks.ts index 7e394071..8962a6ee 100644 --- a/src/utils/types/IBlocks.ts +++ b/src/utils/types/IBlocks.ts @@ -19,3 +19,22 @@ export interface IBlock { transactionCount: number; transactions: Array; } + +export interface IRawBlock { + anchor: string; + bits: string; + chainwork: string; + confirmations: number; + difficulty: string; + finalsaplingroot: string; + hash: string; + height: number; + merkleroot: string; + nonce: string; + previousblockhash: string; + size: number; + solution: string; + time: number; + transactions: Array; + tx: string[]; +} diff --git a/src/utils/types/ISocketData.tsx b/src/utils/types/ISocketData.tsx index 050d97c3..eb274517 100644 --- a/src/utils/types/ISocketData.tsx +++ b/src/utils/types/ISocketData.tsx @@ -1,8 +1,8 @@ import { IRawTransactions } from './ITransactions'; -import { IBlock } from './IBlocks'; +import { IRawBlock } from './IBlocks'; export interface ISocketData { unconfirmedTransactions?: IRawTransactions[]; rawTransactions?: IRawTransactions[]; - blocks: IBlock[]; + blocks: IRawBlock[]; } diff --git a/tsconfig.paths.json b/tsconfig.paths.json index 2e04c725..160a6e4f 100644 --- a/tsconfig.paths.json +++ b/tsconfig.paths.json @@ -11,7 +11,9 @@ "@routes/*": ["src/routes/*"], "@layouts/*": ["src/layouts/*"], "@hooks/*": ["src/hooks/*"], - "@context/*": ["src/context/*"] + "@context/*": ["src/context/*"], + "@apis/*": ["src/apis/*"], + "@services/*": ["src/services/*"] } } -} +} \ No newline at end of file