From 8f1591a9909b69944642da318e2644fe7dcc3995 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Thu, 25 Apr 2024 15:40:28 -0500 Subject: [PATCH] Feat/blocklist (#1569) * feat: nakamoto epoch 3.0 blocklist --- package.json | 2 +- pnpm-lock.yaml | 8 +- src/app/PageClient.tsx | 41 +- .../AnimatedBlockAndMicroblocksItem.tsx | 6 +- .../BlockList/BlockAndMicroblocksItem.tsx | 2 +- .../BlockList/{LayoutA => }/BlockCount.tsx | 26 +- .../context.ts => BlockListContext.ts} | 4 +- .../_components/BlockList/BlockListItem.tsx | 4 +- .../Provider.tsx => BlockListProvider.tsx} | 6 +- .../BlocksPage/BlocksPageBlockList.tsx | 68 ++ .../BlocksPage/BlocksPageBlockListGrouped.tsx | 57 ++ .../BlocksPageBlockListUngrouped.tsx | 56 ++ .../BlocksPage/BlocksPageHeaders.tsx | 146 ++++ src/app/_components/BlockList/Controls.tsx | 99 +-- .../BlockList/Grouped/BlockListGrouped.tsx | 324 ++++++++ .../BlockList/Grouped/skeleton.tsx | 235 ++++++ .../BlockList/HomePage/HomePageBlockList.tsx | 86 +++ .../HomePage/HomePageBlockListGrouped.tsx | 33 + .../HomePage/HomePageBlockListUngrouped.tsx | 33 + .../LayoutA/BlockListWithControls.tsx | 26 +- .../_components/BlockList/LayoutA/Blocks.tsx | 17 +- .../BlockList/LayoutA/NonPaginated.tsx | 16 +- .../BlockList/LayoutA/Paginated.tsx | 24 +- .../BlockList/LayoutA/UpdateBar.tsx | 73 -- .../__tests__/BlockListWithControls.test.tsx | 2 +- .../BlockListWithControls.test.tsx.snap | 106 ++- .../LayoutA/__tests__/useBlockList.test.tsx | 2 +- .../_components/BlockList/LayoutA/consts.ts | 1 - .../_components/BlockList/LayoutA/mocks.ts | 4 +- .../BlockList/LayoutA/useBlockList.ts | 41 +- .../BlockList/LayoutA/useInitialBlockList.ts | 2 +- .../LayoutA/usePaginatedBlockList.ts | 3 +- src/app/_components/BlockList/LineAndNode.tsx | 75 ++ .../_components/BlockList/ScrollableDiv.tsx | 36 + .../Sockets/use-stacks-api-socket-client.ts | 59 ++ .../Sockets/useBlockListWebSocket.ts | 39 + .../useBlockListWebSocketUIBlock.ts} | 9 +- .../BlockList/Sockets/useSubscribeBlocks.ts | 42 ++ .../Sockets/useSubscribeBlocksUIBlock.ts | 36 + .../Ungrouped/BlockListUngrouped.tsx | 350 +++++++++ .../BlockList/Ungrouped/skeleton.tsx | 132 ++++ src/app/_components/BlockList/UpdateBar.tsx | 113 +++ .../BlockList/UpdatedBlockList.tsx | 53 +- .../BlockList/__tests__/utils.test.ts | 703 ++++++++++++++++++ src/app/_components/BlockList/consts.ts | 19 + .../data/useBlocksPageBlockListGrouped.tsx | 104 +++ .../data/useBlocksPageBlockListUngrouped.ts | 106 +++ .../useBlocksPageGroupedInitialBlockList.ts | 74 ++ .../useBlocksPageUngroupedInitialBlockList.ts | 66 ++ .../BlockList/data/useHomePageBlockList.ts | 95 +++ .../data/useHomePageInitialBlockList.ts | 93 +++ src/app/_components/BlockList/data/utils.ts | 26 + src/app/_components/BlockList/index.tsx | 3 +- src/app/_components/BlockList/types.ts | 18 +- .../BlockList/useSubscribeBlocks.ts | 38 - src/app/_components/BlockList/utils.ts | 107 +++ src/app/_components/Footer.tsx | 2 +- src/app/_components/ListHeader.tsx | 23 + src/app/_components/NavBar/MobileNav.tsx | 2 +- .../__snapshots__/MobileNav.test.tsx.snap | 2 +- src/app/_components/NetworkModeToast.tsx | 2 +- src/app/_components/PageTitle.tsx | 7 - src/app/_components/PageWrapper.tsx | 2 +- .../_components/Stats/SkeletonStatSection.tsx | 2 +- src/app/_components/Stats/StatSection.tsx | 6 +- src/app/_components/Stats/Stats.tsx | 6 +- src/app/block/[hash]/PageClient.tsx | 5 +- src/app/blocks/PageClient.tsx | 67 +- src/app/blocks/page.tsx | 7 +- src/app/blocks/skeleton.tsx | 23 +- .../btcblock/[hash]/BitcoinAnchorDetails.tsx | 133 ++++ src/app/btcblock/[hash]/Header.tsx | 25 + src/app/btcblock/[hash]/NavBlock.tsx | 34 + src/app/btcblock/[hash]/PageClient.tsx | 72 ++ src/app/btcblock/[hash]/page.tsx | 12 + src/app/btcblock/[hash]/skeleton.tsx | 70 ++ src/app/layout.tsx | 9 +- src/app/sandbox/layout/RightPanel.tsx | 2 +- src/app/sandbox/transfer/PageClient.tsx | 2 +- src/app/transactions/MempoolFeeStats.tsx | 16 +- src/app/txid/[txId]/Events.tsx | 4 +- src/common/components/BtcStxBlockLinks.tsx | 8 +- src/common/components/CopyButton.tsx | 1 - src/common/components/FilterMenu.tsx | 12 +- src/common/components/KeyValueVertical.tsx | 58 +- src/common/components/ListFooter.tsx | 24 +- src/common/components/Section.tsx | 7 +- src/common/components/Timestamp.tsx | 12 +- src/common/components/TwoColumnsListItem.tsx | 4 +- src/common/components/TxIcon.tsx | 6 +- .../loaders/skeleton-transaction.tsx | 2 +- .../components/modals/unlocking-schedule.tsx | 2 +- src/common/context/GlobalContext.tsx | 24 +- src/common/hooks/useInfiniteQueryResult.ts | 10 +- src/common/queries/useBlockListInfinite.ts | 36 +- src/common/queries/useBlocksByBurnBlock.ts | 36 +- src/common/queries/useBurnBlock.ts | 44 ++ ...BurnBlocks.ts => useBurnBlocksInfinite.ts} | 20 +- src/common/utils/utils.ts | 4 +- .../txs-list/ListItem/MempoolTxListItem.tsx | 2 +- .../ListItem/MempoolTxListItemMini.tsx | 2 +- src/features/txs-list/ListItem/TxListItem.tsx | 2 +- .../ListItem/TxWithTransferListItem.tsx | 2 +- .../txsFilterAndSort/FilterButton.tsx | 21 +- .../txsFilterAndSort/TransactionMessages.tsx | 2 +- src/ui/theme/colors.ts | 2 +- src/ui/theme/componentTheme/Button.ts | 12 + src/ui/theme/componentTheme/Checkbox.ts | 4 +- src/ui/theme/componentTheme/Input.ts | 2 +- src/ui/theme/componentTheme/Menu.ts | 14 +- src/ui/theme/theme.ts | 61 +- 111 files changed, 4319 insertions(+), 601 deletions(-) rename src/app/_components/BlockList/{LayoutA => }/BlockCount.tsx (66%) rename src/app/_components/BlockList/{LayoutA/context.ts => BlockListContext.ts} (86%) rename src/app/_components/BlockList/{LayoutA/Provider.tsx => BlockListProvider.tsx} (77%) create mode 100644 src/app/_components/BlockList/BlocksPage/BlocksPageBlockList.tsx create mode 100644 src/app/_components/BlockList/BlocksPage/BlocksPageBlockListGrouped.tsx create mode 100644 src/app/_components/BlockList/BlocksPage/BlocksPageBlockListUngrouped.tsx create mode 100644 src/app/_components/BlockList/BlocksPage/BlocksPageHeaders.tsx create mode 100644 src/app/_components/BlockList/Grouped/BlockListGrouped.tsx create mode 100644 src/app/_components/BlockList/Grouped/skeleton.tsx create mode 100644 src/app/_components/BlockList/HomePage/HomePageBlockList.tsx create mode 100644 src/app/_components/BlockList/HomePage/HomePageBlockListGrouped.tsx create mode 100644 src/app/_components/BlockList/HomePage/HomePageBlockListUngrouped.tsx delete mode 100644 src/app/_components/BlockList/LayoutA/UpdateBar.tsx delete mode 100644 src/app/_components/BlockList/LayoutA/consts.ts create mode 100644 src/app/_components/BlockList/LineAndNode.tsx create mode 100644 src/app/_components/BlockList/ScrollableDiv.tsx create mode 100644 src/app/_components/BlockList/Sockets/use-stacks-api-socket-client.ts create mode 100644 src/app/_components/BlockList/Sockets/useBlockListWebSocket.ts rename src/app/_components/BlockList/{LayoutA/useBlockListWebSocket.ts => Sockets/useBlockListWebSocketUIBlock.ts} (89%) create mode 100644 src/app/_components/BlockList/Sockets/useSubscribeBlocks.ts create mode 100644 src/app/_components/BlockList/Sockets/useSubscribeBlocksUIBlock.ts create mode 100644 src/app/_components/BlockList/Ungrouped/BlockListUngrouped.tsx create mode 100644 src/app/_components/BlockList/Ungrouped/skeleton.tsx create mode 100644 src/app/_components/BlockList/UpdateBar.tsx create mode 100644 src/app/_components/BlockList/__tests__/utils.test.ts create mode 100644 src/app/_components/BlockList/consts.ts create mode 100644 src/app/_components/BlockList/data/useBlocksPageBlockListGrouped.tsx create mode 100644 src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts create mode 100644 src/app/_components/BlockList/data/useBlocksPageGroupedInitialBlockList.ts create mode 100644 src/app/_components/BlockList/data/useBlocksPageUngroupedInitialBlockList.ts create mode 100644 src/app/_components/BlockList/data/useHomePageBlockList.ts create mode 100644 src/app/_components/BlockList/data/useHomePageInitialBlockList.ts create mode 100644 src/app/_components/BlockList/data/utils.ts delete mode 100644 src/app/_components/BlockList/useSubscribeBlocks.ts create mode 100644 src/app/_components/BlockList/utils.ts create mode 100644 src/app/_components/ListHeader.tsx create mode 100644 src/app/btcblock/[hash]/BitcoinAnchorDetails.tsx create mode 100644 src/app/btcblock/[hash]/Header.tsx create mode 100644 src/app/btcblock/[hash]/NavBlock.tsx create mode 100644 src/app/btcblock/[hash]/PageClient.tsx create mode 100644 src/app/btcblock/[hash]/page.tsx create mode 100644 src/app/btcblock/[hash]/skeleton.tsx create mode 100644 src/common/queries/useBurnBlock.ts rename src/common/queries/{useBurnBlocks.ts => useBurnBlocksInfinite.ts} (75%) diff --git a/package.json b/package.json index a72e1c7e5..95a656935 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@react-stately/toggle": "3.6.1", "@reduxjs/toolkit": "1.9.7", "@stacks/auth": "6.9.0", - "@stacks/blockchain-api-client": "7.9.0-beta.1", + "@stacks/blockchain-api-client": "7.10.0", "@stacks/common": "6.8.1", "@stacks/connect": "7.4.0", "@stacks/connect-react": "22.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e83e5ae5..f27c01bd1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,8 +70,8 @@ dependencies: specifier: 6.9.0 version: 6.9.0 '@stacks/blockchain-api-client': - specifier: 7.9.0-beta.1 - version: 7.9.0-beta.1 + specifier: 7.10.0 + version: 7.10.0 '@stacks/common': specifier: 6.8.1 version: 6.8.1 @@ -4191,8 +4191,8 @@ packages: - encoding dev: false - /@stacks/blockchain-api-client@7.9.0-beta.1: - resolution: {integrity: sha512-628fBKukUR2lykkO7GVjl/dSRRgX2DnU3NsxLywsM98q+IiOaLqMeJmU3n48BkgXUrH8wxg2b54Ixfhw+nuc/A==} + /@stacks/blockchain-api-client@7.10.0: + resolution: {integrity: sha512-VQbJDJuHrj2TWDmYE2Tymqa6yMCvNkc8SXPeHgX1k1BuNKFe7HUHv1r4wA24cRpoTSpM14URJOscKLBaDyklpg==} dependencies: '@stacks/stacks-blockchain-api-types': 7.7.1 '@types/ws': 7.4.7 diff --git a/src/app/PageClient.tsx b/src/app/PageClient.tsx index 6e0a77195..25d0f6083 100644 --- a/src/app/PageClient.tsx +++ b/src/app/PageClient.tsx @@ -1,34 +1,40 @@ 'use client'; +import { NextPage } from 'next'; import dynamic from 'next/dynamic'; import { DEFAULT_BLOCKS_LIST_LIMIT, DEFAULT_LIST_LIMIT_SMALL } from '../common/constants/constants'; import { useGlobalContext } from '../common/context/useAppContext'; +import { NetworkModes } from '../common/types/network'; import { TxListTabs } from '../features/txs-list/tabs/TxListTabs'; import { Grid } from '../ui/Grid'; +import { HomePageBlockListSkeleton } from './_components/BlockList/Grouped/skeleton'; import { SkeletonBlockList } from './_components/BlockList/SkeletonBlockList'; -import { UpdatedBlocksList } from './_components/BlockList/UpdatedBlockList'; import { PageTitle } from './_components/PageTitle'; import { Stats } from './_components/Stats/Stats'; -const NonPaginatedBlockListLayoutA = dynamic( - () => - import('./_components/BlockList/LayoutA/NonPaginated').then( - mod => mod.NonPaginatedBlockListLayoutA - ), +const UpdatedBlockListDynamic = dynamic( + () => import('./_components/BlockList/UpdatedBlockList').then(mod => mod.UpdatedBlocksList), { loading: () => , ssr: false, } ); -const BlocksList = dynamic(() => import('./_components/BlockList').then(mod => mod.BlocksList), { - loading: () => , - ssr: false, -}); +const HomePageBlockListDynamic = dynamic( + () => + import('./_components/BlockList/HomePage/HomePageBlockList').then(mod => mod.HomePageBlockList), + { + loading: () => , + ssr: false, + } +); -export default function Home() { - const { activeNetwork, activeNetworkKey } = useGlobalContext(); +const Home: NextPage = () => { + const { activeNetworkKey, activeNetwork } = useGlobalContext(); + const chain = activeNetwork.mode; + const isNaka1Testnet = + chain === NetworkModes.Testnet && activeNetworkKey.indexOf('nakamoto-1') !== -1; return ( <> Stacks Explorer @@ -39,9 +45,14 @@ export default function Home() { gridTemplateColumns={['100%', '100%', '100%', 'minmax(0, 0.6fr) minmax(0, 0.4fr)']} > - - + {isNaka1Testnet ? ( + + ) : ( + + )} ); -} +}; + +export default Home; diff --git a/src/app/_components/BlockList/AnimatedBlockAndMicroblocksItem.tsx b/src/app/_components/BlockList/AnimatedBlockAndMicroblocksItem.tsx index 1e57a6037..2a2ca8aad 100644 --- a/src/app/_components/BlockList/AnimatedBlockAndMicroblocksItem.tsx +++ b/src/app/_components/BlockList/AnimatedBlockAndMicroblocksItem.tsx @@ -1,17 +1,13 @@ 'use client'; -import { ScaleFade, SlideFade } from '@chakra-ui/react'; -import { FC, memo, useEffect, useState } from 'react'; +import { FC, useEffect, useState } from 'react'; import { Box } from '../../../ui/Box'; -import { Button } from '../../../ui/Button'; import { Collapse } from '../../../ui/Collapse'; -import { useDisclosure } from '../../../ui/hooks/useDisclosure'; import { BlockAndMicroblocksItem } from './BlockAndMicroblocksItem'; import { EnhancedBlock } from './types'; export const animationDuration = 0.8; - export const AnimatedBlockAndMicroblocksItem: FC<{ block: EnhancedBlock; onAnimationExit?: () => void; diff --git a/src/app/_components/BlockList/BlockAndMicroblocksItem.tsx b/src/app/_components/BlockList/BlockAndMicroblocksItem.tsx index 11bf1a714..ea7fdcfdb 100644 --- a/src/app/_components/BlockList/BlockAndMicroblocksItem.tsx +++ b/src/app/_components/BlockList/BlockAndMicroblocksItem.tsx @@ -15,7 +15,7 @@ export const BlockAndMicroblocksItem: React.FC<{ block: Block }> = ({ block }) = return ( diff --git a/src/app/_components/BlockList/LayoutA/BlockCount.tsx b/src/app/_components/BlockList/BlockCount.tsx similarity index 66% rename from src/app/_components/BlockList/LayoutA/BlockCount.tsx rename to src/app/_components/BlockList/BlockCount.tsx index 055fb19db..856bd1838 100644 --- a/src/app/_components/BlockList/LayoutA/BlockCount.tsx +++ b/src/app/_components/BlockList/BlockCount.tsx @@ -3,21 +3,27 @@ import pluralize from 'pluralize'; import { memo } from 'react'; import { PiArrowUpRight } from 'react-icons/pi'; -import { Circle } from '../../../../common/components/Circle'; -import { ExplorerLink } from '../../../../common/components/ExplorerLinks'; -import { Flex } from '../../../../ui/Flex'; -import { Icon } from '../../../../ui/Icon'; -import { Text } from '../../../../ui/Text'; +import { Circle } from '../../../common/components/Circle'; +import { ExplorerLink } from '../../../common/components/ExplorerLinks'; +import { Flex } from '../../../ui/Flex'; +import { Icon } from '../../../ui/Icon'; +import { Text } from '../../../ui/Text'; -export const BlockCount = memo(function ({ count }: { count: number }) { +export const BlockCount = memo(function ({ + count, + btcBlockHash, +}: { + count: number; + btcBlockHash?: string; +}) { + // TODO: remove. use theme const bgColor = useColorModeValue('purple.100', 'slate.900'); const bgColorHover = useColorModeValue('purple.200', 'slate.850'); const textColor = useColorModeValue('purple.600', 'purple.400'); const iconColor = useColorModeValue('purple.600', 'purple.200'); - const circleColor = useColorModeValue('white', 'black'); return ( - - + + +{count} {pluralize('block', count)} - + diff --git a/src/app/_components/BlockList/LayoutA/context.ts b/src/app/_components/BlockList/BlockListContext.ts similarity index 86% rename from src/app/_components/BlockList/LayoutA/context.ts rename to src/app/_components/BlockList/BlockListContext.ts index 25f12fcb3..29c3f6928 100644 --- a/src/app/_components/BlockList/LayoutA/context.ts +++ b/src/app/_components/BlockList/BlockListContext.ts @@ -1,8 +1,8 @@ import { Dispatch, SetStateAction, createContext, useContext } from 'react'; interface BlockListContextType { - isUpdateListLoading: boolean; - setIsUpdateListLoading: Dispatch>; + isBlockListLoading: boolean; + setBlockListLoading: Dispatch>; groupedByBtc: boolean; setGroupedByBtc: Dispatch>; liveUpdates: boolean; diff --git a/src/app/_components/BlockList/BlockListItem.tsx b/src/app/_components/BlockList/BlockListItem.tsx index 0a19fdc8e..b5bc770eb 100644 --- a/src/app/_components/BlockList/BlockListItem.tsx +++ b/src/app/_components/BlockList/BlockListItem.tsx @@ -7,7 +7,7 @@ import { BtcStxBlockLinks } from '../../../common/components/BtcStxBlockLinks'; import { TwoColsListItem } from '../../../common/components/TwoColumnsListItem'; import { addSepBetweenStrings, toRelativeTime, truncateMiddle } from '../../../common/utils/utils'; import { Flex, FlexProps } from '../../../ui/Flex'; -import { Caption, Text } from '../../../ui/typography'; +import { Caption } from '../../../ui/typography'; export const BlockListItem: React.FC<{ block: Block } & FlexProps> = React.memo( ({ block, ...rest }) => { @@ -30,7 +30,7 @@ export const BlockListItem: React.FC<{ block: Block } & FlexProps> = React.memo( ), subtitle: ( - + {addSepBetweenStrings([ `${block?.microblocks_accepted?.length || 0} ${pluralize( 'microblock', diff --git a/src/app/_components/BlockList/LayoutA/Provider.tsx b/src/app/_components/BlockList/BlockListProvider.tsx similarity index 77% rename from src/app/_components/BlockList/LayoutA/Provider.tsx rename to src/app/_components/BlockList/BlockListProvider.tsx index 58401740a..a1d69a5d3 100644 --- a/src/app/_components/BlockList/LayoutA/Provider.tsx +++ b/src/app/_components/BlockList/BlockListProvider.tsx @@ -1,6 +1,6 @@ import { ReactNode, useState } from 'react'; -import { BlockListContext } from './context'; +import { BlockListContext } from './BlockListContext'; export function BlockListProvider({ children }: { children: ReactNode }) { const [isUpdateListLoading, setIsUpdateListLoading] = useState(false); @@ -10,8 +10,8 @@ export function BlockListProvider({ children }: { children: ReactNode }) { return ( { + const now = Date.now(); + if (now - lastClickTimeRef.current > 2000) { + lastClickTimeRef.current = now; + setLiveUpdates(!liveUpdates); + } + }, [liveUpdates, setLiveUpdates]); + + return ( +
+ + { + setGroupedByBtc(!groupedByBtc); + }, + isChecked: groupedByBtc, + }} + liveUpdates={{ + onChange: toggleLiveUpdates, + isChecked: liveUpdates, + }} + horizontal={true} + /> + + {groupedByBtc ? : } +
+ ); +} + +export function BlocksPageBlockList() { + return ( + + + + + + ); +} diff --git a/src/app/_components/BlockList/BlocksPage/BlocksPageBlockListGrouped.tsx b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockListGrouped.tsx new file mode 100644 index 000000000..0a1b81d5a --- /dev/null +++ b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockListGrouped.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { Suspense } from 'react'; + +import { ListFooter } from '../../../../common/components/ListFooter'; +import { Section } from '../../../../common/components/Section'; +import { Box } from '../../../../ui/Box'; +import { Flex } from '../../../../ui/Flex'; +import { ExplorerErrorBoundary } from '../../ErrorBoundary'; +import { useBlockListContext } from '../BlockListContext'; +import { BlockListGrouped } from '../Grouped/BlockListGrouped'; +import { BlocksPageBlockListGroupedSkeleton } from '../Grouped/skeleton'; +import { UpdateBar } from '../UpdateBar'; +import { useBlocksPageBlockListGrouped } from '../data/useBlocksPageBlockListGrouped'; + +function BlocksPageBlockListGroupedBase() { + const { liveUpdates } = useBlockListContext(); + const { blockList, updateBlockList, isFetchingNextPage, hasNextPage, fetchNextPage } = + useBlocksPageBlockListGrouped(); + + return ( + <> + {!liveUpdates && } + + + + + {!liveUpdates && ( + + )} + + + ); +} + +export function BlocksPageBlockListGrouped() { + return ( + + }> + + + + ); +} diff --git a/src/app/_components/BlockList/BlocksPage/BlocksPageBlockListUngrouped.tsx b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockListUngrouped.tsx new file mode 100644 index 000000000..810358df0 --- /dev/null +++ b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockListUngrouped.tsx @@ -0,0 +1,56 @@ +'use client'; + +import { Suspense } from 'react'; + +import { ListFooter } from '../../../../common/components/ListFooter'; +import { Section } from '../../../../common/components/Section'; +import { Box } from '../../../../ui/Box'; +import { ExplorerErrorBoundary } from '../../ErrorBoundary'; +import { useBlockListContext } from '../BlockListContext'; +import { BlockListUngrouped } from '../Ungrouped/BlockListUngrouped'; +import { BlocksPageBlockListUngroupedSkeleton } from '../Ungrouped/skeleton'; +import { UpdateBar } from '../UpdateBar'; +import { useBlocksPageBlockListUngrouped } from '../data/useBlocksPageBlockListUngrouped'; + +function BlocksPageBlockListUngroupedBase() { + const { liveUpdates } = useBlockListContext(); + + const { blockList, hasNextPage, fetchNextPage, isFetchingNextPage, updateBlockList } = + useBlocksPageBlockListUngrouped(); + + return ( + + {!liveUpdates && } + + + {!liveUpdates && ( + + )} + + + ); +} + +export function BlocksPageBlockListUngrouped() { + return ( + + }> + + + + ); +} diff --git a/src/app/_components/BlockList/BlocksPage/BlocksPageHeaders.tsx b/src/app/_components/BlockList/BlocksPage/BlocksPageHeaders.tsx new file mode 100644 index 000000000..7c42cf2ba --- /dev/null +++ b/src/app/_components/BlockList/BlocksPage/BlocksPageHeaders.tsx @@ -0,0 +1,146 @@ +'use client'; + +import { ReactNode, Suspense } from 'react'; + +import { BurnBlock } from '@stacks/blockchain-api-client'; + +import { Card } from '../../../../common/components/Card'; +import { useSuspenseInfiniteQueryResult } from '../../../../common/hooks/useInfiniteQueryResult'; +import { useSuspenseBlocksByBurnBlock } from '../../../../common/queries/useBlocksByBurnBlock'; +import { useSuspenseBurnBlocks } from '../../../../common/queries/useBurnBlocksInfinite'; +import { Flex } from '../../../../ui/Flex'; +import { Icon } from '../../../../ui/Icon'; +import { Stack } from '../../../../ui/Stack'; +import { Text } from '../../../../ui/Text'; +import { BitcoinIcon } from '../../../../ui/icons/BitcoinIcon'; +import { BlockPageHeadersSkeleton } from '../Grouped/skeleton'; + +function LastBlockCard() { + const response = useSuspenseBurnBlocks(1); + const burnBlocks = useSuspenseInfiniteQueryResult(response); + const btcBlock = burnBlocks[0]; + const stxBlocks = useSuspenseInfiniteQueryResult( + useSuspenseBlocksByBurnBlock(btcBlock.burn_block_height, 1), + 1 + ); + const lastStxBlock = stxBlocks[0]; + return ( + + + LAST BLOCK + + + + #{lastStxBlock.height} + + + + + #{btcBlock.burn_block_height} + + + + + {btcBlock.stacks_blocks.length} transactions + + + ); +} + +function AverageStacksBlockTimeCard() { + return ( + + + AVERAGE STACKS BLOCK TIME + + + 48 sec. + + + In the last 24hs. + + + ); +} + +function LastConfirmedBitcoinBlockCard() { + return ( + + + IN THE LAST CONFIRMED BITCOIN BLOCK + + + + + 214 + + + Stacks blocks + + + + + 4354 + + + Stacks transactions + + + + + ); +} + +export function BlocksPageHeaderLayout({ + lastBlockCard, + averageStacksBlockTimeCard, + lastConfirmedBitcoinBlockCard, + ...rest +}: { + lastBlockCard: ReactNode; + averageStacksBlockTimeCard: ReactNode; + lastConfirmedBitcoinBlockCard: ReactNode; +} & React.ComponentProps) { + return ( + *:not(:last-of-type)': { + borderBottom: ['1px solid var(--stacks-colors-borderPrimary)', null, null, 'none'], + borderRight: [null, null, null, '1px solid var(--stacks-colors-borderPrimary)'], + }, + '& > *:last-of-type': { + borderBottom: 'none', + borderRight: 'none', + }, + }} + {...rest} + > + {lastBlockCard} + {averageStacksBlockTimeCard} + {lastConfirmedBitcoinBlockCard} + + ); +} + +export function BlocksPageHeaders() { + return ( + }> + } + averageStacksBlockTimeCard={} + lastConfirmedBitcoinBlockCard={} + /> + + ); +} diff --git a/src/app/_components/BlockList/Controls.tsx b/src/app/_components/BlockList/Controls.tsx index ed8caa18c..f94c4dea7 100644 --- a/src/app/_components/BlockList/Controls.tsx +++ b/src/app/_components/BlockList/Controls.tsx @@ -1,60 +1,65 @@ -import React from 'react'; +import { ReactNode } from 'react'; -import { Flex } from '../../../ui/Flex'; +import { Flex, FlexProps } from '../../../ui/Flex'; import { FormControl } from '../../../ui/FormControl'; import { FormLabel } from '../../../ui/FormLabel'; -import { Stack } from '../../../ui/Stack'; +import { StackProps } from '../../../ui/Stack'; import { Switch, SwitchProps } from '../../../ui/Switch'; -interface ControlsProps { +interface ControlsProps extends StackProps { groupByBtc: SwitchProps; liveUpdates: SwitchProps; horizontal?: boolean; } -export function Controls({ groupByBtc, liveUpdates, horizontal }: ControlsProps) { +export function ControlsLayout({ + horizontal, + children, + ...rest +}: { + horizontal?: boolean; + children: ReactNode; +} & FlexProps) { + return ( + + {children} + + ); +} + +export function Controls({ groupByBtc, liveUpdates, horizontal, ...rest }: ControlsProps) { return ( - <> - - - - - - Group by Bitcoin block - - - - - - - Live updates - - - - + + + + + Group by Bitcoin block + + + + + + Live updates + + + ); } diff --git a/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx b/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx new file mode 100644 index 000000000..45c4bc932 --- /dev/null +++ b/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx @@ -0,0 +1,324 @@ +import React, { ReactNode } from 'react'; +import { PiArrowElbowLeftDown } from 'react-icons/pi'; + +import { BlockLink, ExplorerLink } from '../../../../common/components/ExplorerLinks'; +import { Timestamp } from '../../../../common/components/Timestamp'; +import { truncateMiddle } from '../../../../common/utils/utils'; +import { Box } from '../../../../ui/Box'; +import { Flex } from '../../../../ui/Flex'; +import { Grid } from '../../../../ui/Grid'; +import { HStack } from '../../../../ui/HStack'; +import { Icon } from '../../../../ui/Icon'; +import { Stack } from '../../../../ui/Stack'; +import { Text } from '../../../../ui/Text'; +import { BitcoinIcon, StxIcon } from '../../../../ui/icons'; +import { Caption } from '../../../../ui/typography'; +import { ListHeader } from '../../ListHeader'; +import { BlockCount } from '../BlockCount'; +import { useBlockListContext } from '../BlockListContext'; +import { LineAndNode } from '../LineAndNode'; +import { ScrollableBox } from '../ScrollableDiv'; +import { FADE_DURATION, getFadeAnimationStyle, mobileBorderCss } from '../consts'; +import { BlockListBtcBlock, BlockListStxBlock } from '../types'; +import { BlockListData } from '../utils'; + +const PADDING = 4; + +const GroupHeader = () => { + return ( + <> + + + Block height + + + + + Block hash + + + + + Transactions + + + + + Timestamp + + + + ); +}; + +const StxBlockRow = ({ + stxBlock, + minimized = false, + isFirst, + isLast, +}: { + stxBlock: BlockListStxBlock; + minimized?: boolean; + isFirst?: boolean; + isLast?: boolean; +}) => { + const icon = isFirst ? : undefined; + return minimized ? ( + <> + + + + + #{stxBlock.height} + + + + + ∙} gap={1} whiteSpace="nowrap" gridColumn="3 / 4"> + + + {truncateMiddle(stxBlock.hash, 3)} + + + {stxBlock.txsCount !== undefined ? ( + + {stxBlock.txsCount || 0} txn + + ) : null} + + + + ) : ( + <> + + + + + #{stxBlock.height} + + + + + + + + {stxBlock.hash} + + + + + + + {stxBlock.txsCount} + + + + + + + + ); +}; + +export function BurnBlockGroupGridLayout({ + minimized, + children, +}: { + minimized?: boolean; + children: ReactNode; +}) { + return ( + + {children} + + ); +} + +export function BurnBlockGroupGrid({ + stxBlocks, + minimized, + numStxBlocksNotDisplayed, +}: { + stxBlocks: BlockListStxBlock[]; + minimized: boolean; + numStxBlocksNotDisplayed: number; +}) { + return ( + + {minimized || stxBlocks.length === 0 ? null : } + {stxBlocks.map((stxBlock, i) => ( + + + {i < stxBlocks.length - 1 && ( + + )} + + ))} + + ); +} + +function BitcoinHeader({ + btcBlock, + minimized = false, + isFirst, +}: { + btcBlock: BlockListBtcBlock; + minimized?: boolean; + isFirst: boolean; +}) { + return ( + + + + + + #{btcBlock.height} + + + ∙} gap={1} flexWrap={'wrap'}> + + {truncateMiddle(btcBlock.hash, 6)} + + + + + ); +} + +export function Footer({ btcBlock, txSum }: { btcBlock: BlockListBtcBlock; txSum: number }) { + return ( + + ∙} gap={1} pt={4} flexWrap="wrap"> + + {btcBlock.txsCount} blocks + + + {txSum} transactions + + + Average block time: 29 sec. + + + + ); +} + +export function BurnBlockGroup({ + btcBlock, + stxBlocks, + stxBlocksLimit, + minimized = false, + isFirst, +}: { + btcBlock: BlockListBtcBlock; + stxBlocks: BlockListStxBlock[]; + stxBlocksLimit?: number; + minimized?: boolean; + isFirst: boolean; +}) { + const numStxBlocks = btcBlock.txsCount ?? stxBlocks.length; + const numStxBlocksNotDisplayed = numStxBlocks - (stxBlocksLimit || 0); + const txSum = stxBlocks.reduce((txSum, stxBlock) => { + const txsCount = stxBlock?.txsCount ?? 0; + return txSum + txsCount; + }, 0); + const stxBlocksShortList = stxBlocksLimit ? stxBlocks.slice(0, stxBlocksLimit) : stxBlocks; + return ( + + + + + + {numStxBlocksNotDisplayed > 0 ? ( + + ) : null} +