diff --git a/packages/insight/src/App.tsx b/packages/insight/src/App.tsx index 8388813f2b5..df4d14fcf80 100644 --- a/packages/insight/src/App.tsx +++ b/packages/insight/src/App.tsx @@ -9,6 +9,7 @@ import {GlobalStyles} from './assets/styles/global'; import {useAppSelector} from './utilities/hooks'; import 'nprogress/nprogress.css'; import nProgress from 'nprogress'; +import { BlocksProvider } from './pages/blocks'; function App() { const theme = useAppSelector(({APP}) => APP.theme); @@ -20,15 +21,17 @@ function App() { return ( - - - - - - + + + + + + + + ); } diff --git a/packages/insight/src/Routing.tsx b/packages/insight/src/Routing.tsx index 796f2eec62d..9fddf5cfdf6 100644 --- a/packages/insight/src/Routing.tsx +++ b/packages/insight/src/Routing.tsx @@ -1,4 +1,4 @@ -import React, {lazy, Suspense} from 'react'; +import {lazy, Suspense} from 'react'; import {Navigate, Route, Routes} from 'react-router-dom'; import Home from './pages'; const Blocks = lazy(() => import('./pages/blocks')); diff --git a/packages/insight/src/assets/images/arrow-down-black.svg b/packages/insight/src/assets/images/arrow-down-black.svg new file mode 100644 index 00000000000..cd26e1e77a0 --- /dev/null +++ b/packages/insight/src/assets/images/arrow-down-black.svg @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/packages/insight/src/assets/images/arrow-down.svg b/packages/insight/src/assets/images/arrow-down.svg new file mode 100644 index 00000000000..7de9ca03b57 --- /dev/null +++ b/packages/insight/src/assets/images/arrow-down.svg @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/packages/insight/src/assets/images/arrow-forward-blue.svg b/packages/insight/src/assets/images/arrow-forward-blue.svg new file mode 100644 index 00000000000..d08d59d0f47 --- /dev/null +++ b/packages/insight/src/assets/images/arrow-forward-blue.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/insight/src/assets/images/arrow-outward.svg b/packages/insight/src/assets/images/arrow-outward.svg new file mode 100644 index 00000000000..658974e740c --- /dev/null +++ b/packages/insight/src/assets/images/arrow-outward.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/insight/src/assets/images/arrow-thin.svg b/packages/insight/src/assets/images/arrow-thin.svg new file mode 100644 index 00000000000..da9da611522 --- /dev/null +++ b/packages/insight/src/assets/images/arrow-thin.svg @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/packages/insight/src/assets/images/cube.svg b/packages/insight/src/assets/images/cube.svg new file mode 100644 index 00000000000..8f58c5695f4 --- /dev/null +++ b/packages/insight/src/assets/images/cube.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/insight/src/components/block-details.tsx b/packages/insight/src/components/block-details.tsx index 1c3036db88b..5c8d3210974 100644 --- a/packages/insight/src/components/block-details.tsx +++ b/packages/insight/src/components/block-details.tsx @@ -184,9 +184,7 @@ const BlockDetails: FC = ({currency, network, block}) => { diff --git a/packages/insight/src/components/block-list.tsx b/packages/insight/src/components/block-list.tsx new file mode 100644 index 00000000000..0e1e756c963 --- /dev/null +++ b/packages/insight/src/components/block-list.tsx @@ -0,0 +1,207 @@ +import React, {FC, useState} from 'react'; +import {getApiRoot, getConvertedValue, getDifficultyFromBits, getFormattedDate} from 'src/utilities/helper-methods'; +import {BitcoinBlockType} from 'src/utilities/models'; +import Cube from '../assets/images/cube.svg'; +import Arrow from '../assets/images/arrow-thin.svg'; +import ArrowOutward from '../assets/images/arrow-outward.svg'; +import ForwardArrow from '../assets/images/arrow-forward-blue.svg'; +import ArrowDown from '../assets/images/arrow-down.svg'; +import styled, {useTheme} from 'styled-components'; +import InfoCard from './info-card'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import {fetcher} from 'src/api/api'; +import InfiniteScrollLoadSpinner from './infinite-scroll-load-spinner'; +import Info from './info'; +import {useNavigate} from 'react-router-dom'; +import {useBlocks} from 'src/pages/blocks'; + +const BlockListTableRow = styled.tr` + text-align: center; + line-height: 45px; + + &:nth-child(odd) { + background-color: ${({theme: {dark}}) => (dark ? '#2a2a2a' : '#f6f7f9')}; + } + + &:nth-child(even) { + background-color: ${({theme: {dark}}) => (dark ? '#0f0f0f' : '#e0e4e7')}; + } + + font-size: 16px; +`; + + +const getBlocksUrl = (currency: string, network: string) => { + return `${getApiRoot(currency)}/${currency}/${network}/block?limit=200`; +}; + +const BlockList: FC<{currency: string, network: string}> = ({currency, network}) => { + const theme = useTheme(); + const { blocks, setBlocks } = useBlocks(); + if (!blocks) + return null; + const navigate = useNavigate(); + const [expandedBlocks, setExpandedBlocks] = useState([]); + const [error, setError] = useState(''); + const [hasMore, setHasMore] = useState(true); + const hasFees = blocks.every(block => block.feeData); + const columnProportion = hasFees ? '20%' : '25%'; + + const fetchMore = async (_blocksList: BitcoinBlockType[]) => { + if (!_blocksList.length || !currency || !network) return; + const since = _blocksList[_blocksList.length - 1].height; + try { + const newData: [BitcoinBlockType] = await fetcher( + `${getBlocksUrl(currency, network)}&since=${since}&paging=height&direction=-1`, + ); + if (newData?.length) { + setBlocks(_blocksList.concat(newData)); + } else { + setHasMore(false); + } + } catch (e: any) { + setError(e.message || 'Something went wrong. Please try again later.'); + } + }; + + const gotoSingleBlockDetailsView = (hash: string) => { + navigate(`/${currency}/${network}/block/${hash}`); + }; + + if (!blocks?.length) return null; + return ( + <> + {error ? : null} + fetchMore(blocks)} + hasMore={hasMore} + loader={} + dataLength={blocks.length}> + + + + + + + + {hasFees && } + + + + + { + blocks.map((block: BitcoinBlockType, index: number) => { + const feeData = block.feeData; + const expanded = expandedBlocks.includes(block.height); + + const dataRowsDB = { + 'Previous block': {label: 'Previous block', value: block.height - 1}, + 'Bits': {label: 'Bits', value: block.bits}, + 'Version': {label: 'Version', value: block.version}, + 'Block reward': {label: 'Block reward', value: `${getConvertedValue(block.reward, currency).toFixed(3)} ${currency}`}, + 'Miner fees': {label: 'Miner fees', value: `${getConvertedValue(feeData?.feeTotal, currency).toFixed(5)} ${currency}`}, + 'Next block': {label: 'Next block', value: + <> + {block.height + 1} + gotoSingleBlockDetailsView(blocks[index - 1].hash)} + alt='Next Block' + title={`Go to block ${block.height + 1}`} + /> + + }, + 'Nonce': {label: 'Nonce', value: block.nonce}, + 'Confirmations': {label: 'Confirmations', value: blocks[0].height - block.height + 1}, + 'Difficulty': {label: 'Difficulty', value: getDifficultyFromBits(block.bits).toFixed(0)}, + 'Fee data': {label: 'Fee data', value: +
+ {[{label: 'Mean', value: feeData?.mean}, {label: 'Median', value: feeData?.median}, {label: 'Mode', value: feeData?.mode}] + .map(({label, value}, key) => { + return +
+ {label} + {value?.toFixed(4)} +
+
+ }) + } +
+ } + }; + + type IDataRowsDB = keyof typeof dataRowsDB; + let columnLeftExpandedDataKeys: IDataRowsDB[]; + let columnRightExpandedDataKeys: IDataRowsDB[]; + + if (currency === 'ETH') { + columnLeftExpandedDataKeys = ['Previous block', 'Block reward']; + columnRightExpandedDataKeys = ['Next block', 'Nonce', 'Confirmations']; + } else { + columnLeftExpandedDataKeys = ['Previous block', 'Bits', 'Version', 'Block reward', 'Miner fees']; + columnRightExpandedDataKeys = ['Next block', 'Nonce', 'Confirmations', 'Difficulty', 'Fee data']; + } + const columnLeftExpandedData : Array<{label: string, value: any}> = columnLeftExpandedDataKeys.map(key => dataRowsDB[key]); + const columnRightExpandedData : Array<{label: string, value: any}> = columnRightExpandedDataKeys.map(key => dataRowsDB[key]); + + return ( + + + + + + + { feeData && } + + {expanded && <> + {/* Alternates the color so the data below this row stays the same*/} + + + + + + + } + + ); + }) + } + +
HeightTimestampTransactionsSizeFee Rate
+ expanded + ? setExpandedBlocks(expandedBlocks.filter(h => h !== block.height)) + : setExpandedBlocks([...expandedBlocks, block.height])}> + {expanded + ? arrow + : arrow + } + cube + {block.height} + + {getFormattedDate(block.time)}{block.transactionCount}{block.size}{feeData.median.toFixed(4)} +
+
+ + Summary +
+ + +
+ gotoSingleBlockDetailsView(block.hash)}> + View transactions + arrow + +
+
+
+ + ); +}; + +export default BlockList; diff --git a/packages/insight/src/components/chain-header.tsx b/packages/insight/src/components/chain-header.tsx new file mode 100644 index 00000000000..01545d7e05a --- /dev/null +++ b/packages/insight/src/components/chain-header.tsx @@ -0,0 +1,226 @@ +import {FC, useEffect, useRef, useState} from 'react'; +import {useApi} from 'src/api/api'; +import {Chart as ChartJS} from 'chart.js'; +import {colorCodes} from 'src/utilities/constants'; +import {BitcoinBlockType} from 'src/utilities/models'; +import styled, {useTheme} from 'styled-components'; +import {getName} from 'src/utilities/helper-methods'; +import Dropdown from './dropdown'; +import {useBlocks} from 'src/pages/blocks'; +import { ChangeData, FeeChangeSpan, PriceChangeSpan } from './change-span'; + +const ChartTile = styled.div` + height: 400px; + width: 50%; + background-color: ${({theme: {dark}}) => dark ? '#222' : '#fff'}; + border-radius: 10px; + padding: 1.5rem; + margin: 1rem; + display: flex; + flex-direction: column; +`; + +const ChartTileHeader = styled.span` + font-size: 27px; + font-weight: bolder; +`; + +const ChainHeader: FC<{ currency: string; network: string }> = ({ currency, network }) => { + const theme = useTheme(); + const { blocks } = useBlocks(); + const priceDetails: { + data: { + code: string, + name: string, + rate: number + } + } = useApi(`https://bitpay.com/rates/${currency}/usd`).data; + + const priceDisplay: { + data: Array<{ + prices: Array<{price: number, time: string}>, + currencyPair: string, + currencies: Array, + priceDisplay: Array, + percentChange: string, + priceDisplayPercentChange: string + }> + } = useApi( + `https://bitpay.com/currencies/prices?currencyPairs=["${currency}:USD"]`, + ).data; + + const price = network === 'mainnet' ? priceDetails?.data?.rate : 0; + + const feeChartRef = useRef(null); + const feeChartInstanceRef = useRef(null); + + const priceChartRef = useRef(null); + const priceChartInstanceRef = useRef(null); + const priceList = (priceDisplay?.data?.[0]?.priceDisplay || []); + + const feeRanges = ['128 Blocks', '32 Blocks', '16 Blocks', '8 Blocks']; + const priceRanges = ['24 Hours', '12 Hours', '6 Hours', '3 Hours']; + + const [feeSelectedRange, setFeesSelectedRange] = useState('32 Blocks'); + const [priceSelectedRange, setPriceSelectedRange] = useState('24 Hours'); + + const [priceChangeData, setPriceChangeData] = useState(); + const [feeChangeData, setFeeChangeData] = useState(); + const hasFees = blocks?.at(0)?.feeData !== undefined; + + useEffect(() => { + if (feeChartRef.current && blocks && hasFees) { + if (feeChartInstanceRef.current) { + feeChartInstanceRef.current.destroy(); + } + const numBlocks = Number(feeSelectedRange.slice(0, feeSelectedRange.indexOf(' '))); + const fees = blocks.map((block: BitcoinBlockType) => block.feeData?.median as number).reverse().slice(blocks.length - numBlocks); + const dates = blocks.map((block: BitcoinBlockType) => + new Date(block.time).toLocaleString('en-US', { + year: '2-digit', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) + ).reverse().slice(blocks.length - numBlocks); + const chartData = { + labels: dates, + datasets: [ + { + data: fees, + fill: false, + spanGaps: true, + borderColor: colorCodes[currency], + borderWidth: 1.5, + pointRadius: 3 + } + ] + }; + const options = { + scales: { + y: { + display: true, + beginAtZero: true, + ticks: { maxTicksLimit: 6 } + }, + x: { display: false } + }, + plugins: {legend: {display: false}}, + events: [], + responsive: true, + maintainAspectRatio: false, + tension: 0 + }; + feeChartInstanceRef.current = new ChartJS(feeChartRef.current, { + type: 'line', + data: chartData, + options + }); + + const feeChange = fees[fees.length - 1] - fees[0]; + const percentFeeChange = feeChange / fees[0] * 100; + + setFeeChangeData({change: feeChange, percentChange: percentFeeChange, range: numBlocks}); + } + + return () => { + feeChartInstanceRef.current?.destroy(); + }; + }, [blocks, feeSelectedRange, currency]); + + useEffect(() => { + const hours = Number(priceSelectedRange.slice(0, priceSelectedRange.indexOf(' '))) + const usedPrices = priceList.slice(priceList.length - hours); + const priceChartData = { + labels: usedPrices, + datasets: [ + { + data: usedPrices, + fill: false, + spanGaps: true, + borderColor: colorCodes[currency], + borderWidth: 1.5, + pointRadius: 3, + }, + ], + }; + + const priceOptions = { + scales: { + y: { + display: true, + beginAtZero: false, + ticks: { + maxTicksLimit: 4, + } + }, + x: {display: false} + }, + plugins: {legend: {display: false}}, + events: [], + responsive: true, + maintainAspectRatio: false, + tension: 0, + }; + if (priceChartRef.current) { + if (priceChartInstanceRef.current) { + priceChartInstanceRef.current.destroy(); + } + priceChartInstanceRef.current = new ChartJS(priceChartRef.current, { + type: 'line', + data: priceChartData, + options: priceOptions, + }); + } + + const priceChange = price - usedPrices[0]; + const percentPriceChange = priceChange / usedPrices[0] * 100; + + setPriceChangeData({change: priceChange, percentChange: percentPriceChange, range: hours}); + return () => { + priceChartInstanceRef.current?.destroy(); + }; + }, [priceList, price, priceSelectedRange, currency]); + + return ( +
+ Blocks + {currency} +
+
+ + {getName(currency)} Exchange Rate +
+ ${price.toLocaleString()} + +
+ +
+ +
+
+ { hasFees && + + {getName(currency)} Fee +
+ {blocks?.at(0)?.feeData?.median.toFixed(3)} sats/byte + +
+ +
+ +
+
+ } +
+
+
+ ); +}; + +export default ChainHeader; diff --git a/packages/insight/src/components/change-span.tsx b/packages/insight/src/components/change-span.tsx new file mode 100644 index 00000000000..d2624148f5d --- /dev/null +++ b/packages/insight/src/components/change-span.tsx @@ -0,0 +1,44 @@ +import {FC} from 'react'; + +export interface ChangeData { + change: number; + percentChange: number; + range: number; +} + +export const FeeChangeSpan: FC<{ data?: ChangeData }> = ({ data }) => { + if (!data) + return null; + const { change, percentChange, range } = data; + return ( + + + {change.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} sats/byte + ({percentChange.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%) + Last {range} Days + + ); +} + +export const PriceChangeSpan: FC<{ data?: ChangeData }> = ({ data }) => { + if (!data) + return null; + const { change, percentChange, range } = data; + + let color = 'gray'; + if (change > 0) { + color = 'green'; + } else if (change < 0) { + color = 'red'; + } + + return ( + + + ${change.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}{' '} + ({percentChange.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%) + + Last {range} Hours + + ); +} \ No newline at end of file diff --git a/packages/insight/src/components/copy-text.tsx b/packages/insight/src/components/copy-text.tsx index 156b18c8e25..2ef40f912df 100644 --- a/packages/insight/src/components/copy-text.tsx +++ b/packages/insight/src/components/copy-text.tsx @@ -1,5 +1,5 @@ import {AnimatePresence, motion} from 'framer-motion'; -import {FC, memo, useState} from 'react'; +import {CSSProperties, FC, memo, useState} from 'react'; import {CopyToClipboard} from 'react-copy-to-clipboard'; import styled from 'styled-components'; import CopySvg from '../assets/images/copy-icon.svg'; @@ -23,8 +23,9 @@ const IconImage = styled(motion.img)` interface CopyTextProps { text: string; + style?: CSSProperties } -const CopyText: FC = ({text}) => { +const CopyText: FC = ({text, style}) => { const [copied, setCopied] = useState(false); const onClickCopy = () => { @@ -60,7 +61,7 @@ const CopyText: FC = ({text}) => { }; return ( - + {copied ? ( = ({currency}) => { const {height, time, transactionCount, size} = data[0]; const imgSrc = `https://bitpay.com/img/icon/currencies/${currency}.svg`; - const gotoAllBlocks = async () => { - await navigate(`/${currency}/mainnet/blocks`); + const gotoChain = () => { + navigate(`/${currency}/mainnet/blocks`); }; return ( - + {`${currency}
diff --git a/packages/insight/src/components/data-box.tsx b/packages/insight/src/components/data-box.tsx index eec3695db14..3ff608ae97e 100644 --- a/packages/insight/src/components/data-box.tsx +++ b/packages/insight/src/components/data-box.tsx @@ -1,29 +1,41 @@ -import {Children, FC, ReactNode} from 'react'; -import {useTheme} from 'styled-components'; +import {CSSProperties, FC, ReactNode} from 'react'; +import styled from 'styled-components'; + +const DataBox: FC<{ + children: ReactNode, + label?: string, + style?: CSSProperties, + centerLabel?: boolean, + colorDark?: string, + colorLight?: string}> = ({children, label, style, centerLabel, colorDark='#5f5f5f', colorLight='#ccc'}) => { + + const DataBoxFieldset = styled.fieldset` + border: 2.5px solid ${({theme: {dark}}) => dark ? colorDark : colorLight}; + border-radius: 5px; + padding: 0.1rem 0.4rem; + word-break: break-all; + white-space: normal; + width: fit-content; + height: fit-content; + margin: 0.7rem 0.2rem; + `; -const DataBox: FC<{children: ReactNode, label: string, style?: object}> = ({children, label, style}) => { - const theme = useTheme(); - const modifiedChildren = typeof children === 'object' - ? Children.map(children as JSX.Element, (child: JSX.Element) => { - return ; - }) - : children; - return ( -
- {label} - {modifiedChildren} -
+ + { label && + + {label} + + } + {children} + ); } diff --git a/packages/insight/src/components/dropdown.tsx b/packages/insight/src/components/dropdown.tsx new file mode 100644 index 00000000000..7bc68250003 --- /dev/null +++ b/packages/insight/src/components/dropdown.tsx @@ -0,0 +1,51 @@ +import { FC, CSSProperties } from 'react'; +import ArrowDown from '../assets/images/arrow-down.svg'; +import ArrowDownBlack from '../assets/images/arrow-down-black.svg'; +import { useTheme } from 'styled-components'; + +const Dropdown: FC<{ + options: string[], + value?: string, + onChange?: (value: string) => void, + style?: CSSProperties +}> = ({options, value, onChange, style}) => { + const theme = useTheme(); + return ( +
+ + Arrow Down +
+ ); +} + +export default Dropdown; \ No newline at end of file diff --git a/packages/insight/src/components/info-card.tsx b/packages/insight/src/components/info-card.tsx new file mode 100644 index 00000000000..e99bc4f9f22 --- /dev/null +++ b/packages/insight/src/components/info-card.tsx @@ -0,0 +1,45 @@ +import { FC } from 'react'; +import CopyText from './copy-text'; +import styled from 'styled-components'; + + +type InfoCardType = { + data: Array<{label: string, value: any, copyText?: boolean}>, +}; + +const Card = styled.div` + display: flex; + flex-direction: column; + width: 100%; + background-color: ${({theme: {dark}}) => dark ? '#222' : '#fff'}; + padding: 14px; + border-radius: 8px; +`; + +const Label = styled.span` + color: ${({theme: {dark}}) => dark ? '#888' : '#474d53'}; + align-self: flex-start; + line-height: 1.6; + margin-bottom: -2; + font-size: 18px; +`; + +const InfoCard: FC = ({data}) => { + return ( + + {data.map((d, index) => { + const { label, value, copyText } = d; + return (<> + +
+ {value} + {copyText && } +
+ { index !== data.length - 1 &&
} + ); + })} +
+ ); +} + +export default InfoCard; \ No newline at end of file diff --git a/packages/insight/src/components/transaction-details.tsx b/packages/insight/src/components/transaction-details.tsx index d970727d981..2719a87d530 100644 --- a/packages/insight/src/components/transaction-details.tsx +++ b/packages/insight/src/components/transaction-details.tsx @@ -204,27 +204,21 @@ const TransactionDetails: FC = ({ - - - goToTx(item.mintTxid, undefined, item.mintIndex) - }> - {item.mintTxid} - - + + goToTx(item.mintTxid, undefined, item.mintIndex) + }> + {item.mintTxid} +
- - {item.mintIndex} - + {item.mintIndex} {item.uiConfirmations && confirmations > 0 ? ( - - {item.uiConfirmations + confirmations} - + {item.uiConfirmations + confirmations} ) : null}
diff --git a/packages/insight/src/pages/blocks.tsx b/packages/insight/src/pages/blocks.tsx index fb3f0ba21c9..d5a27ecade4 100644 --- a/packages/insight/src/pages/blocks.tsx +++ b/packages/insight/src/pages/blocks.tsx @@ -1,188 +1,79 @@ -import React, {useEffect, useState} from 'react'; -import {MainTitle} from '../assets/styles/titles'; -import {fetcher} from '../api/api'; -import InfiniteScroll from 'react-infinite-scroll-component'; - -import InfiniteScrollLoadSpinner from '../components/infinite-scroll-load-spinner'; -import Info from '../components/info'; -import SupCurrencyLogo from '../components/icons/sup-currency-logo'; - -import {getApiRoot, getFormattedDate, normalizeParams, sleep} from '../utilities/helper-methods'; -import {BlocksType} from '../utilities/models'; -import {size} from '../utilities/constants'; - -import styled from 'styled-components'; -import {motion} from 'framer-motion'; -import {routerFadeIn} from '../utilities/animations'; -import {useParams, useNavigate} from 'react-router-dom'; -import {useAppDispatch} from '../utilities/hooks'; -import {changeCurrency, changeNetwork} from '../store/app.actions'; -import {LightBlack, NeutralSlate, Slate30} from '../assets/styles/colors'; +import BlockList from 'src/components/block-list'; +import React, {createContext, useContext, useEffect, useState} from 'react'; +import ChainHeader from '../components/chain-header'; +import {useParams} from 'react-router-dom'; +import {useDispatch} from 'react-redux'; +import {changeCurrency, changeNetwork} from 'src/store/app.actions'; +import {getApiRoot, normalizeParams} from 'src/utilities/helper-methods'; +import {fetcher} from 'src/api/api'; import nProgress from 'nprogress'; +import {BitcoinBlockType} from 'src/utilities/models'; +import Info from 'src/components/info'; -const BlockListTable = styled.table` - width: 100%; - border-collapse: collapse; - overflow-x: hidden; -`; - -const BlockListTableHead = styled.tr` - text-align: center; - line-height: 45px; - background-color: ${({theme: {dark}}) => (dark ? '#090909' : NeutralSlate)}; - font-size: 16px; - - @media screen and (max-width: ${size.tablet}) { - font-size: 14px; - line-height: 30px; - } -`; - -const BlockListTableRow = styled(motion.tr)` - text-align: center; - line-height: 45px; - - &:nth-child(odd) { - background-color: ${({theme: {dark}}) => (dark ? LightBlack : Slate30)}; - } - - &:nth-child(even) { - background-color: ${({theme: {dark}}) => (dark ? '#090909' : NeutralSlate)}; - } - - transition: transform 200ms ease, box-shadow 200ms ease; - font-size: 16px; - - @media screen and (max-width: ${size.tablet}) { - font-size: 14px; - line-height: 30px; - } -`; +type BlocksContextType = { + blocks: BitcoinBlockType[] | undefined; + setBlocks: React.Dispatch>; +}; -const TdLink = styled.td` - color: ${({theme: {colors}}) => colors.link}; -`; +const BlocksContext = createContext(undefined); -const getBlocksUrl = (currency: string, network: string) => { - return `${getApiRoot(currency)}/${currency}/${network}/block?limit=200`; +export const BlocksProvider: React.FC<{children: React.ReactNode}> = ({ children }) => { + const [blocks, setBlocks] = useState(); + return ( + + {children} + + ); }; -const listAnime = { - whileHover: { - cursor: 'pointer', - scale: 1.02, - transition: { - bounce: 0, - duration: 0.05, - ease: 'linear', - }, - }, +export const useBlocks = () => { + const ctx = useContext(BlocksContext); + if (!ctx) throw new Error('useBlocks must be used within a BlocksProvider'); + return ctx; }; const Blocks: React.FC = () => { let {currency, network} = useParams<{currency: string; network: string}>(); - const dispatch = useAppDispatch(); - const navigate = useNavigate(); - const [isLoading, setIsLoading] = useState(true); - const [blocksList, setBlocksList] = useState(); + const dispatch = useDispatch(); + + const { blocks, setBlocks } = useBlocks(); const [error, setError] = useState(''); - const [hasMore, setHasMore] = useState(true); useEffect(() => { - if (!currency || !network) return; nProgress.start(); - const _normalizeParams = normalizeParams(currency, network); - currency = _normalizeParams.currency; - network = _normalizeParams.network; - - dispatch(changeCurrency(currency)); - dispatch(changeNetwork(network)); - - Promise.all([fetcher(getBlocksUrl(currency, network)), sleep(500)]) + if (!currency || !network) + return; + Promise.all([fetcher(`${getApiRoot(currency)}/${currency}/${network}/block?limit=200`)]) .then(([data]) => { - setBlocksList(data); + setBlocks(data); }) .catch((e: any) => { setError(e.message || 'Something went wrong. Please try again later.'); }) .finally(() => { - setIsLoading(false); nProgress.done(); }); - }, [currency, network]); + }, []); - const fetchMore = async (_blocksList: BlocksType[]) => { - if (!_blocksList.length || !currency || !network) return; - const since = _blocksList[_blocksList.length - 1].height; - try { - const newData: [BlocksType] = await fetcher( - `${getBlocksUrl(currency, network)}&since=${since}&paging=height&direction=-1`, - ); - if (newData?.length) { - setBlocksList(_blocksList.concat(newData)); - } else { - setHasMore(false); - } - } catch (e: any) { - setError(e.message || 'Something went wrong. Please try again later.'); - } - }; + useEffect(() => { + if (!currency || !network) return; + const _normalizeParams = normalizeParams(currency, network); + currency = _normalizeParams.currency; + network = _normalizeParams.network; + + dispatch(changeCurrency(currency)); + dispatch(changeNetwork(network)); + }, [currency, network]); - const gotoSingleBlockDetailsView = async (hash: string) => { - await navigate(`/${currency}/${network}/block/${hash}`); - }; + if (!currency || !network) return null; return ( <> - {!isLoading ? ( - <> - {error ? : null} - {blocksList?.length ? ( - - - Blocks - {currency && } - - - fetchMore(blocksList)} - hasMore={hasMore} - loader={} - dataLength={blocksList.length}> - - - - Height - Timestamp - Transactions - Size - - - - {blocksList.map((block: BlocksType, index: number) => { - const {height, hash, transactionCount, time, size} = block; - return ( - gotoSingleBlockDetailsView(hash)}> - {height} - {getFormattedDate(time)} - {transactionCount} - {size} - - ); - })} - - - - - ) : null} - - ) : null} + {error ? : null} + + { blocks && } ); -}; +} -export default Blocks; +export default Blocks; \ No newline at end of file diff --git a/packages/insight/src/pages/index.tsx b/packages/insight/src/pages/index.tsx index f24f49be01b..072433425be 100644 --- a/packages/insight/src/pages/index.tsx +++ b/packages/insight/src/pages/index.tsx @@ -1,5 +1,4 @@ import {SUPPORTED_CURRENCIES} from '../utilities/constants'; -import {SecondaryTitle} from '../assets/styles/titles'; import CurrencyTile from '../components/currency-tile'; import Masonry from 'react-masonry-css'; import {motion} from 'framer-motion'; @@ -24,8 +23,6 @@ const Home: React.FC = () => { return ( - Latest Blocks - { switch (currency.toUpperCase()) { case 'BTC': - default: return BitcoreLib; case 'BCH': return BitcoreLibCash; @@ -180,11 +179,70 @@ export const getLib = (currency: string) => { return BitcoreLibDoge; case 'LTC': return BitcoreLibLtc; + default: + return BitcoreLib; } }; +export const getName = (currency: string) => { + switch (currency.toUpperCase()) { + case 'BTC': + return 'Bitcoin'; + case 'BCH': + return 'Bitcoin Cash'; + case 'DOGE': + return 'Doge'; + case 'LTC': + return 'Litecoin'; + case 'ETH': + return 'Ethereum'; + default: + return 'Bitcoin'; + } +} + export const getDifficultyFromBits = (bits: number) => { const maxBody = Math.log(0x00ffff); const scaland = Math.log(256); return Math.exp(maxBody - Math.log(bits & 0x00ffffff) + scaland * (0x1d - ((bits & 0xff000000) >> 24))); +} + +/** + * Merges the source object into the destination object. + * For each property in the source object: + * It sets the destination object property to the source property unless + * both properties are object and the destination object property is not an array. + * + * @param object destination object + * @param source source object + */ +export const merge = (dest: TDest, src: TSrc): TDest & TSrc => { + for (const key in src) { + const destProp = dest !== undefined ? (dest as any)[key] : undefined; + const srcProp = src[key]; + let result; + if (srcProp instanceof Object && destProp instanceof Object && !Array.isArray(destProp)) { + result = merge(destProp, srcProp); + } else { + result = srcProp; + } + (dest as any)[key] = result; + } + return dest as TDest & TSrc; +} +export default merge; + +export const darkenHexColor = (hex: string, amount: number) => { + hex = hex.replace(/^#/, ''); + + let r = parseInt(hex.substring(0, 2), 16); + let g = parseInt(hex.substring(2, 4), 16); + let b = parseInt(hex.substring(4, 6), 16); + + r = Math.max(0, r - amount); + g = Math.max(0, g - amount); + b = Math.max(0, b - amount); + + const toHex = (v: number) => v.toString(16).padStart(2, '0'); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; } \ No newline at end of file diff --git a/packages/insight/src/utilities/models.ts b/packages/insight/src/utilities/models.ts index 2128c1fd3e1..cddc781fffc 100644 --- a/packages/insight/src/utilities/models.ts +++ b/packages/insight/src/utilities/models.ts @@ -109,3 +109,20 @@ export interface BlocksType { size: number; hash: string; } + +export type BitcoinBlockType = BlocksType & { + merkleRoot: string, + bits: number, + nonce: number, + reward: number, + version: number, + confirmations: number, + feeData: FeeData; +}; + +export type FeeData = { + feeTotal: number; + mean: number; + median: number; + mode: number; +}