diff --git a/src/app/_components/BlockList/BlocksPage/BlocksPageBlockList.tsx b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockList.tsx index 62e9196b5..509ecfada 100644 --- a/src/app/_components/BlockList/BlocksPage/BlocksPageBlockList.tsx +++ b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockList.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useCallback, useRef } from 'react'; +import { ReactNode, useCallback, useRef } from 'react'; import { Section } from '../../../../common/components/Section'; import { Stack } from '../../../../ui/Stack'; @@ -11,41 +11,66 @@ import { Controls } from '../Controls'; import { BlocksPageBlockListGrouped } from './BlocksPageBlockListGrouped'; import { BlocksPageBlockListUngrouped } from './BlocksPageBlockListUngrouped'; +export function BlocksPageBlockListLayout({ children }: { children: ReactNode }) { + return {children}; +} + +export function BlocksPageControlsLayout({ + liveUpdates, + children, +}: { + liveUpdates?: boolean; + children: ReactNode; +}) { + return ( + + {children} + + ); +} + function BlocksPageBlockListBase() { const { groupedByBtc, setGroupedByBtc, liveUpdates, setLiveUpdates } = useBlockListContext(); const lastClickTimeRef = useRef(0); - const toggleLiveUpdates = useCallback(() => { - const now = Date.now(); - if (now - lastClickTimeRef.current > 2000) { - lastClickTimeRef.current = now; - setLiveUpdates(!liveUpdates); - } - }, [liveUpdates, setLiveUpdates]); + const toggleLiveUpdates = useCallback( + (immediately?: boolean) => { + const now = Date.now(); + if (immediately || now - lastClickTimeRef.current > 2000) { + lastClickTimeRef.current = now; + setLiveUpdates(!liveUpdates); + } + }, + [liveUpdates, setLiveUpdates] + ); return ( - - + + { setGroupedByBtc(!groupedByBtc); + if (liveUpdates) { + toggleLiveUpdates(true); + } }, isChecked: groupedByBtc, }} liveUpdates={{ - onChange: toggleLiveUpdates, + onChange: () => toggleLiveUpdates(), isChecked: liveUpdates, }} horizontal={true} /> - + {groupedByBtc ? : } - + ); } diff --git a/src/app/_components/BlockList/BlocksPage/BlocksPageBlockListGrouped.tsx b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockListGrouped.tsx index 1856a83db..a71cc7ffa 100644 --- a/src/app/_components/BlockList/BlocksPage/BlocksPageBlockListGrouped.tsx +++ b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockListGrouped.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Suspense } from 'react'; +import { Suspense, useMemo } from 'react'; import { ListFooter } from '../../../../common/components/ListFooter'; import { Section } from '../../../../common/components/Section'; @@ -17,22 +17,26 @@ function BlocksPageBlockListGroupedBase() { const { liveUpdates } = useBlockListContext(); const { blockList, updateBlockList, isFetchingNextPage, hasNextPage, fetchNextPage } = useBlocksPageBlockListGrouped(); + const latestBlock = useMemo(() => blockList?.[0], [blockList]); return ( <> - {!liveUpdates && } + {!liveUpdates && } - + - {!liveUpdates && ( - - )} + > ); diff --git a/src/app/_components/BlockList/BlocksPage/BlocksPageBlockListUngrouped.tsx b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockListUngrouped.tsx index f9a7a1d72..62e385044 100644 --- a/src/app/_components/BlockList/BlocksPage/BlocksPageBlockListUngrouped.tsx +++ b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockListUngrouped.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Suspense } from 'react'; +import { Suspense, useMemo } from 'react'; import { ListFooter } from '../../../../common/components/ListFooter'; import { Section } from '../../../../common/components/Section'; @@ -17,20 +17,19 @@ function BlocksPageBlockListUngroupedBase() { const { blockList, hasNextPage, fetchNextPage, isFetchingNextPage, updateBlockList } = useBlocksPageBlockListUngrouped(); + const latestBlock = useMemo(() => blockList?.[0], [blockList]); return ( - {!liveUpdates && } - + {!liveUpdates && } + - {!liveUpdates && ( - - )} + ); diff --git a/src/app/_components/BlockList/Controls.tsx b/src/app/_components/BlockList/Controls.tsx index f94c4dea7..229129d25 100644 --- a/src/app/_components/BlockList/Controls.tsx +++ b/src/app/_components/BlockList/Controls.tsx @@ -21,7 +21,7 @@ export function ControlsLayout({ children: ReactNode; } & FlexProps) { return ( - + {children} ); @@ -35,6 +35,7 @@ export function Controls({ groupByBtc, liveUpdates, horizontal, ...rest }: Contr - #{stxBlock.height} + #{stxBlock.height} isLast: {isLast?.toString()} - ∙} gap={1} whiteSpace="nowrap" gridColumn="3 / 4"> + ∙} + gap={1} + whiteSpace="nowrap" + gridColumn="3 / 4" + justifyContent="flex-end" + > {truncateMiddle(stxBlock.hash, 3)} @@ -176,7 +182,7 @@ export function BurnBlockGroupGrid({ stxBlock={stxBlock} minimized={minimized} isFirst={i === 0} - isLast={i === stxBlocks.length - 1 && numStxBlocksNotDisplayed === 0} + isLast={i === stxBlocks.length - 1 && numStxBlocksNotDisplayed <= 0} /> {i < stxBlocks.length - 1 && ( @@ -206,35 +212,52 @@ function BitcoinHeader({ px={PADDING} borderBottom={minimized ? '1px solid var(--stacks-colors-borderPrimary)' : 'none'} flexWrap={'wrap'} + // height={5} > - + {isFirst ? ( - - {btcBlock.height} - + + + Next Bitcoin block + + ) : ( - - #{btcBlock.height} - + + + #{btcBlock.height} + + )} - ∙} gap={1} flexWrap={'wrap'}> + {isFirst ? ( - - {truncateMiddle(btcBlock.hash, 6)} - + + + + Unconfirmed + + ) : ( - + ∙} gap={1} flexWrap={'wrap'}> + + {truncateMiddle(btcBlock.hash, 6)} + + + )} - - + ); } @@ -266,12 +289,14 @@ export function BurnBlockGroup({ isFirst, stxBlocksLimit, minimized = false, + onlyShowStxBlocksForFirstBtcBlock, }: { btcBlock: BlockListBtcBlock; stxBlocks: BlockListStxBlock[]; isFirst: boolean; stxBlocksLimit?: number; minimized?: boolean; + onlyShowStxBlocksForFirstBtcBlock?: boolean; }) { const unaccountedStxBlocks = btcBlock.blockCount ? stxBlocks.length - btcBlock.blockCount : 0; const unaccountedTxs = useMemo( @@ -293,7 +318,14 @@ export function BurnBlockGroup({ ? btcBlock.blockCount + unaccountedStxBlocks : btcBlock.blockCount : undefined; - const numStxBlocksNotDisplayed = blocksCount ? blocksCount - (stxBlocksLimit || 0) : 0; + const numStxBlocksNotDisplayed = + onlyShowStxBlocksForFirstBtcBlock && !isFirst + ? blocksCount + ? blocksCount + : 0 + : blocksCount + ? blocksCount - (stxBlocksLimit || 0) + : 0; const displayedStxBlocks = useMemo( () => (stxBlocksLimit ? stxBlocks.slice(0, stxBlocksLimit) : stxBlocks), [stxBlocks, stxBlocksLimit] @@ -301,13 +333,15 @@ export function BurnBlockGroup({ return ( - - - + {onlyShowStxBlocksForFirstBtcBlock && !isFirst ? null : ( + + + + )} {numStxBlocksNotDisplayed > 0 ? ( @@ -349,6 +385,7 @@ export function BlockListGrouped({ minimized={minimized} stxBlocksLimit={stxBlocksLimit} isFirst={i === 0} + onlyShowStxBlocksForFirstBtcBlock={onlyShowStxBlocksForFirstBtcBlock} /> ))} diff --git a/src/app/_components/BlockList/Grouped/skeleton.tsx b/src/app/_components/BlockList/Grouped/skeleton.tsx index c5efa05ae..f20e955c8 100644 --- a/src/app/_components/BlockList/Grouped/skeleton.tsx +++ b/src/app/_components/BlockList/Grouped/skeleton.tsx @@ -1,7 +1,6 @@ import { useColorModeValue } from '@chakra-ui/react'; import { Circle } from '../../../../common/components/Circle'; -import { Section } from '../../../../common/components/Section'; import { Box } from '../../../../ui/Box'; import { Button } from '../../../../ui/Button'; import { Flex } from '../../../../ui/Flex'; @@ -9,8 +8,13 @@ import { SkeletonItem } from '../../../../ui/SkeletonItem'; import { SkeletonText } from '../../../../ui/SkeletonText'; import { Stack } from '../../../../ui/Stack'; import { Text } from '../../../../ui/Text'; +import { + BlocksPageBlockListLayout, + BlocksPageControlsLayout, +} from '../BlocksPage/BlocksPageBlockList'; import { BlocksPageHeaderLayout } from '../BlocksPage/BlocksPageHeaders'; import { ControlsLayout } from '../Controls'; +import { HomePageBlockListLayout, HomePageControlsLayout } from '../HomePage/HomePageBlockList'; import { BlockListRowSkeleton } from '../Ungrouped/skeleton'; import { UpdateBarLayout } from '../UpdateBar'; import { BurnBlockGroupGridLayout } from './BlockListGrouped'; @@ -194,12 +198,26 @@ export function BlockPageHeadersSkeleton() { ); } -function ControlsSkeleton({ horizontal }: { horizontal?: boolean }) { +function HomePageControlsSkeleton({ horizontal }: { horizontal?: boolean }) { return ( - - - - + + + + + + + + ); +} + +function BlocksPageControlsSkeleton({ horizontal }: { horizontal?: boolean }) { + return ( + + + + + + ); } @@ -216,20 +234,20 @@ export function UpdateBarSkeleton() { export function BlocksPageBlockListSkeleton() { return ( - - + + - + ); } export function HomePageBlockListSkeleton() { return ( - - + + - + ); } diff --git a/src/app/_components/BlockList/HomePage/HomePageBlockList.tsx b/src/app/_components/BlockList/HomePage/HomePageBlockList.tsx index ee702b209..cb3848a43 100644 --- a/src/app/_components/BlockList/HomePage/HomePageBlockList.tsx +++ b/src/app/_components/BlockList/HomePage/HomePageBlockList.tsx @@ -1,7 +1,7 @@ 'use client'; import dynamic from 'next/dynamic'; -import { useCallback, useRef } from 'react'; +import { ReactNode, useCallback, useRef } from 'react'; import { Section } from '../../../../common/components/Section'; import { Stack } from '../../../../ui/Stack'; @@ -29,40 +29,67 @@ const HomePageBlockListUngroupedDynamic = dynamic( } ); +export function HomePageBlockListLayout({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + +export function HomePageControlsLayout({ + liveUpdates, + children, +}: { + liveUpdates?: boolean; + children: ReactNode; +}) { + return ( + + {children} + + ); +} + function HomePageBlockListBase() { const { groupedByBtc, setGroupedByBtc, liveUpdates, setLiveUpdates } = useBlockListContext(); const lastClickTimeRef = useRef(0); - const toggleLiveUpdates = useCallback(() => { - const now = Date.now(); - if (now - lastClickTimeRef.current > 2000) { - lastClickTimeRef.current = now; - setLiveUpdates(!liveUpdates); - } - }, [liveUpdates, setLiveUpdates]); + const toggleLiveUpdates = useCallback( + (immediately?: boolean) => { + const now = Date.now(); + if (immediately || now - lastClickTimeRef.current > 2000) { + lastClickTimeRef.current = now; + setLiveUpdates(!liveUpdates); + } + }, + [liveUpdates, setLiveUpdates] + ); return ( - - + + Recent Blocks { setGroupedByBtc(!groupedByBtc); + if (liveUpdates) { + toggleLiveUpdates(true); + } }, isChecked: groupedByBtc, }} liveUpdates={{ - onChange: toggleLiveUpdates, + onChange: () => toggleLiveUpdates(), isChecked: liveUpdates, }} padding={0} - gap={3} border="none" /> - + {groupedByBtc ? : } - + ); } diff --git a/src/app/_components/BlockList/HomePage/HomePageBlockListGrouped.tsx b/src/app/_components/BlockList/HomePage/HomePageBlockListGrouped.tsx index 138d822bd..e7b3c96af 100644 --- a/src/app/_components/BlockList/HomePage/HomePageBlockListGrouped.tsx +++ b/src/app/_components/BlockList/HomePage/HomePageBlockListGrouped.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Suspense } from 'react'; +import { Suspense, useMemo } from 'react'; import { ListFooter } from '../../../../common/components/ListFooter'; import { Flex } from '../../../../ui/Flex'; @@ -13,12 +13,13 @@ import { useHomePageBlockList } from '../data/useHomePageBlockList'; function HomePageBlockListGroupedBase() { const { liveUpdates } = useBlockListContext(); const { blockList, updateBlockList } = useHomePageBlockList(); + const latestBlock = useMemo(() => blockList?.[0], [blockList]); return ( <> - {!liveUpdates && } + {!liveUpdates && } - {!liveUpdates && } + > ); diff --git a/src/app/_components/BlockList/HomePage/HomePageBlockListUngrouped.tsx b/src/app/_components/BlockList/HomePage/HomePageBlockListUngrouped.tsx index ee76f3855..0530623cd 100644 --- a/src/app/_components/BlockList/HomePage/HomePageBlockListUngrouped.tsx +++ b/src/app/_components/BlockList/HomePage/HomePageBlockListUngrouped.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Suspense } from 'react'; +import { Suspense, useMemo } from 'react'; import { ListFooter } from '../../../../common/components/ListFooter'; import { Flex } from '../../../../ui/Flex'; @@ -13,12 +13,13 @@ import { useHomePageBlockList } from '../data/useHomePageBlockList'; function HomePageBlockListUngroupedBase() { const { liveUpdates } = useBlockListContext(); const { blockList, updateBlockList } = useHomePageBlockList(); + const latestBlock = useMemo(() => blockList?.[0], [blockList]); return ( <> - {!liveUpdates && } + {!liveUpdates && } - {!liveUpdates && } + > ); diff --git a/src/app/_components/BlockList/LayoutA/__tests__/__snapshots__/BlockListWithControls.test.tsx.snap b/src/app/_components/BlockList/LayoutA/__tests__/__snapshots__/BlockListWithControls.test.tsx.snap index 2883b0df4..b9b437d71 100644 --- a/src/app/_components/BlockList/LayoutA/__tests__/__snapshots__/BlockListWithControls.test.tsx.snap +++ b/src/app/_components/BlockList/LayoutA/__tests__/__snapshots__/BlockListWithControls.test.tsx.snap @@ -21,7 +21,7 @@ exports[`BlockListWithControls renders correctly 1`] = ` class="css-1gdm77d" > - + - - + class="css-4xvjmy" + > + + + + Update + + {isFirst ? ( - {height} + Next Bitcoin block ) : ( @@ -71,23 +71,28 @@ export function BtcBlockRowContent({ timestamp, height, hash, isFirst }: BtcBloc )} - ∙ >} fontSize={'xs'}> + {isFirst ? ( - - {truncateMiddle(hash, 6)} - + + + + Unconfirmed + + ) : ( - - {truncateMiddle(hash, 6)} - + ∙ >} fontSize={'xs'}> + + {truncateMiddle(hash, 6)} + + {timestamp && } + )} - {timestamp && } - + > ); } @@ -178,6 +183,7 @@ function StxBlockRow({ whiteSpace="nowrap" color="textSubdued" gridColumn="3 / 4" + justifyContent="flex-end" > @@ -271,7 +277,7 @@ function StxBlocksGrid({ txsCount={stxBlock.txsCount} minimized={minimized} isFirst={i === 0} - isLast={i === stxBlocks.length - 1 && numStxBlocksNotDisplayed === 0} + isLast={i === stxBlocks.length - 1 && numStxBlocksNotDisplayed <= 0} /> {i < stxBlocks.length - 1 && ( diff --git a/src/app/_components/BlockList/UpdateBar.tsx b/src/app/_components/BlockList/UpdateBar.tsx index ffe7f5be2..329562daf 100644 --- a/src/app/_components/BlockList/UpdateBar.tsx +++ b/src/app/_components/BlockList/UpdateBar.tsx @@ -2,6 +2,7 @@ import { useColorModeValue } from '@chakra-ui/react'; import { ArrowCounterClockwise } from '@phosphor-icons/react'; import { ReactNode, Suspense, useCallback, useRef } from 'react'; +import RelativeTimeDisplay from '../../../common/components/RelativeTimeDisplay'; import { Button } from '../../../ui/Button'; import { Flex, FlexProps } from '../../../ui/Flex'; import { Icon } from '../../../ui/Icon'; @@ -9,7 +10,7 @@ import { Text } from '../../../ui/Text'; import { useBlockListContext } from './BlockListContext'; import { UpdateBarSkeleton } from './Grouped/skeleton'; import { getFadeAnimationStyle } from './consts'; -import { useSuspenseAverageBlockTimes } from './data/useAverageBlockTimes'; +import { BlockListData } from './utils'; export function UpdateBarLayout({ children, ...rest }: { children: ReactNode }) { const bgColor = useColorModeValue('purple.100', 'slate.900'); // TODO: not in theme. remove @@ -32,22 +33,22 @@ export function UpdateBarLayout({ children, ...rest }: { children: ReactNode }) interface UpdateBarProps { onClick: () => void; latestBlocksCount?: number; + latestBlock?: BlockListData; } export function UpdateBarBase({ onClick, latestBlocksCount, + latestBlock, ...rest }: { onClick: () => void; latestBlocksCount?: number; + latestBlock?: BlockListData; } & FlexProps) { const textColor = useColorModeValue('slate.800', 'slate.400'); // TODO: not in theme. remove const lastClickTimeRef = useRef(0); const { isBlockListLoading } = useBlockListContext(); - const { - data: { last_24h }, - } = useSuspenseAverageBlockTimes(); const update = useCallback(() => { const now = Date.now(); @@ -57,6 +58,16 @@ export function UpdateBarBase({ } }, [onClick]); + const latestStxBlock = latestBlock?.stxBlocks[0]; + + const text = latestBlocksCount ? ( + <>{latestBlocksCount}> + ) : latestStxBlock ? ( + <> + Last update + > + ) : null; + return ( - {latestBlocksCount ? latestBlocksCount : `Avg. block time: ${last_24h}s`} + {text} Update @@ -87,10 +97,20 @@ export function UpdateBarBase({ ); } -export function UpdateBar({ onClick, latestBlocksCount, ...rest }: UpdateBarProps & FlexProps) { +export function UpdateBar({ + onClick, + latestBlocksCount, + latestBlock, + ...rest +}: UpdateBarProps & FlexProps) { return ( }> - + ); } diff --git a/src/app/_components/BlockList/utils.ts b/src/app/_components/BlockList/utils.ts index 22c4e941f..f33b0fb49 100644 --- a/src/app/_components/BlockList/utils.ts +++ b/src/app/_components/BlockList/utils.ts @@ -8,7 +8,6 @@ export type BtcBlockMap = Record; export type BlockListData = { stxBlocks: BlockListStxBlock[]; btcBlock: BlockListBtcBlock }; export function createBlockListStxBlock(stxBlock: Block | NakamotoBlock): BlockListStxBlock { - console.log('using the actual createBlockListStxBlock function'); return { type: 'stx_block', height: stxBlock.height, @@ -23,7 +22,6 @@ export function createBlockListStxBlock(stxBlock: Block | NakamotoBlock): BlockL }; } export function createBlockListBtcBlock(btcBlock: BurnBlock): BlockListBtcBlock { - console.log('using the actual createBlockListBtcBlock function'); return { type: 'btc_block', height: btcBlock.burn_block_height, @@ -38,7 +36,6 @@ export function createBlockListBtcBlock(btcBlock: BurnBlock): BlockListBtcBlock export function createBlockListBtcBlockFromStxBlock( stxBlock: Block | NakamotoBlock ): BlockListBtcBlock { - console.log('using the actual createBlockListBtcBlockFromStxBlock function'); return { type: 'btc_block', height: stxBlock.burn_block_height, diff --git a/src/app/skeleton.tsx b/src/app/skeleton.tsx index d0afa3733..396b9f563 100644 --- a/src/app/skeleton.tsx +++ b/src/app/skeleton.tsx @@ -3,7 +3,7 @@ import { Section } from '../common/components/Section'; import { SkeletonTxsList } from '../features/txs-list/SkeletonTxsList'; import { Grid } from '../ui/Grid'; -import { SkeletonBlockList } from './_components/BlockList/SkeletonBlockList'; +import { HomePageBlockListSkeleton } from './_components/BlockList/Grouped/skeleton'; import { PageTitle } from './_components/PageTitle'; import { SkeletonStatSection } from './_components/Stats/SkeletonStatSection'; import { Wrapper } from './_components/Stats/Wrapper'; @@ -26,7 +26,7 @@ export default function HomePageSkeleton() { - + > ); diff --git a/src/common/components/RelativeTimeDisplay.tsx b/src/common/components/RelativeTimeDisplay.tsx new file mode 100644 index 000000000..c2b127792 --- /dev/null +++ b/src/common/components/RelativeTimeDisplay.tsx @@ -0,0 +1,23 @@ +import dayjs from 'dayjs'; +import { useCallback, useEffect, useState } from 'react'; + +import { ONE_MINUTE } from '../queries/query-stale-time'; + +export const RelativeTimeDisplay = ({ timestampInMs }: { timestampInMs: number }) => { + const [time, setTime] = useState(''); + + const updateRelativeTime = useCallback(() => { + setTime(dayjs().to(dayjs(timestampInMs * 1000))); + }, [timestampInMs, setTime]); + + useEffect(() => { + updateRelativeTime(); // Update immediately on mount + const interval = setInterval(updateRelativeTime, ONE_MINUTE); // Update every minute + + return () => clearInterval(interval); // Cleanup the interval on component unmount + }, [timestampInMs, updateRelativeTime]); // Dependencies array: the effect depends on the timestamp + + return <>{time}>; +}; + +export default RelativeTimeDisplay; diff --git a/src/common/components/Timestamp.tsx b/src/common/components/Timestamp.tsx index 323536e70..b52c0c9ac 100644 --- a/src/common/components/Timestamp.tsx +++ b/src/common/components/Timestamp.tsx @@ -2,7 +2,7 @@ import { Flex, FlexProps } from '../../ui/Flex'; import { Tooltip } from '../../ui/Tooltip'; -import { toRelativeTime } from '../utils/utils'; +import RelativeTimeDisplay from './RelativeTimeDisplay'; import { Value } from './Value'; interface TimestampProps { @@ -17,7 +17,9 @@ export function Timestamp({ ts, ...rest }: TimestampProps & FlexProps) { return ( - {toRelativeTime(ts * 1000)} + + + ); diff --git a/src/common/utils/time-utils.ts b/src/common/utils/time-utils.ts new file mode 100644 index 000000000..bb7d40624 --- /dev/null +++ b/src/common/utils/time-utils.ts @@ -0,0 +1,5 @@ +export function getReadableTimestamp(ts: number): string { + return ts + ? `${new Date(ts * 1000).toLocaleTimeString()} ${new Date(ts * 1000).toLocaleDateString()}` + : ''; +}