diff --git a/packages/app-explorer/src/app/blocks/layout.tsx b/packages/app-explorer/src/app/blocks/layout.tsx
new file mode 100644
index 00000000..1291bdbf
--- /dev/null
+++ b/packages/app-explorer/src/app/blocks/layout.tsx
@@ -0,0 +1,17 @@
+import { OverlayDialog, Providers } from 'app-portal';
+import type { Metadata } from 'next';
+
+export const metadata: Metadata = {
+ title: 'All Blocks',
+};
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+ );
+}
+
+export const dynamic = 'force-static';
diff --git a/packages/app-explorer/src/app/blocks/page.tsx b/packages/app-explorer/src/app/blocks/page.tsx
new file mode 100644
index 00000000..90838da6
--- /dev/null
+++ b/packages/app-explorer/src/app/blocks/page.tsx
@@ -0,0 +1,23 @@
+'use client';
+import { Box, Flex } from '@fuels/ui';
+import { tv } from 'tailwind-variants';
+import { BlocksScreen } from '~/systems/Block/screens/BlockScreen';
+
+const Blocks = () => {
+ const classes = styles();
+ return (
+
+
+
+
+
+ );
+};
+const styles = tv({
+ slots: {
+ content: 'w-full max-w-[100%]',
+ },
+});
+export default Blocks;
+
+export const dynamic = 'force-static';
diff --git a/packages/app-explorer/src/systems/Block/components/BlockEfficiencyItem.tsx b/packages/app-explorer/src/systems/Block/components/BlockEfficiencyItem.tsx
new file mode 100644
index 00000000..48a82374
--- /dev/null
+++ b/packages/app-explorer/src/systems/Block/components/BlockEfficiencyItem.tsx
@@ -0,0 +1,49 @@
+import { Box, HStack, Text, VStack } from '@fuels/ui';
+
+type BlockEfficiencyItemProps = {
+ current: number;
+ total: number;
+};
+
+export default function BlockEfficiencyItem({
+ current,
+ total,
+}: BlockEfficiencyItemProps) {
+ // Convert current and total to millions
+ const currentInMillions = current / 1_000_000;
+ const totalInMillions = total / 1_000_000;
+
+ // Calculate progress percentage
+ const progress = (current / total) * 100;
+
+ return (
+
+
+
+ {/* Format current and total as M (millions) */}
+
+ {currentInMillions % 1 === 0
+ ? currentInMillions.toFixed(0)
+ : currentInMillions.toFixed(1)}
+ M /
+ {totalInMillions % 1 === 0
+ ? totalInMillions.toFixed(0)
+ : totalInMillions.toFixed(1)}
+ M
+
+
+ ({progress.toFixed(2)}%)
+
+
+
+
+
+ );
+}
diff --git a/packages/app-explorer/src/systems/Block/components/BlockHashItem.tsx b/packages/app-explorer/src/systems/Block/components/BlockHashItem.tsx
new file mode 100644
index 00000000..d36eeffd
--- /dev/null
+++ b/packages/app-explorer/src/systems/Block/components/BlockHashItem.tsx
@@ -0,0 +1,28 @@
+import { Box, Copyable, HStack, VStack } from '@fuels/ui';
+
+type BlockHashItemProps = {
+ hashAddress: string;
+ width: string;
+};
+
+export default function BlockHashItem({
+ hashAddress,
+ width,
+}: BlockHashItemProps) {
+ return (
+
+
+
+
+
+ {hashAddress}
+
+
+
+
+
+ );
+}
diff --git a/packages/app-explorer/src/systems/Block/components/BlockItem.tsx b/packages/app-explorer/src/systems/Block/components/BlockItem.tsx
new file mode 100644
index 00000000..f566d2ac
--- /dev/null
+++ b/packages/app-explorer/src/systems/Block/components/BlockItem.tsx
@@ -0,0 +1,26 @@
+import { Box, Copyable, HStack, Text, VStack } from '@fuels/ui';
+
+export interface BlockItemProps {
+ blockId: string;
+ ethValue: string;
+}
+
+export default function BlockItem({ blockId, ethValue }: BlockItemProps) {
+ return (
+
+
+
+
+ #{blockId}
+
+
+
+
+ {ethValue} ETH
+
+
+ );
+}
diff --git a/packages/app-explorer/src/systems/Block/components/BlockTimeItem.tsx b/packages/app-explorer/src/systems/Block/components/BlockTimeItem.tsx
new file mode 100644
index 00000000..74107776
--- /dev/null
+++ b/packages/app-explorer/src/systems/Block/components/BlockTimeItem.tsx
@@ -0,0 +1,28 @@
+import { Text, VStack } from '@fuels/ui';
+
+type BlockTimeItemProps = {
+ time: Date;
+ timeAgo: string;
+};
+
+export default function BlockTimeItem({ time, timeAgo }: BlockTimeItemProps) {
+ const timeDate = new Date(time);
+
+ const formattedTime = timeDate.toLocaleString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+ hour12: true,
+ });
+
+ return (
+
+ {timeAgo}
+
+ {formattedTime}
+
+
+ );
+}
diff --git a/packages/app-explorer/src/systems/Block/components/BlockValidatorItem.tsx b/packages/app-explorer/src/systems/Block/components/BlockValidatorItem.tsx
new file mode 100644
index 00000000..29f61663
--- /dev/null
+++ b/packages/app-explorer/src/systems/Block/components/BlockValidatorItem.tsx
@@ -0,0 +1,16 @@
+import { HStack } from '@fuels/ui';
+import BlockHashItem from './BlockHashItem';
+
+type BlockValidatorItemProps = {
+ hashAddress: string;
+};
+
+export default function BlockValidatorItem({
+ hashAddress,
+}: BlockValidatorItemProps) {
+ return (
+
+
+
+ );
+}
diff --git a/packages/app-explorer/src/systems/Block/components/BlocksTable.tsx b/packages/app-explorer/src/systems/Block/components/BlocksTable.tsx
new file mode 100644
index 00000000..a146eb71
--- /dev/null
+++ b/packages/app-explorer/src/systems/Block/components/BlocksTable.tsx
@@ -0,0 +1,132 @@
+import { GQLBlocksQuery } from '@fuel-explorer/graphql';
+import { GridTable } from '@fuels/ui';
+import { Link } from '@fuels/ui';
+import NextLink from 'next/link';
+import BlockEfficiencyItem from './BlockEfficiencyItem';
+import BlockHashItem from './BlockHashItem';
+import BlockItem from './BlockItem';
+import BlockTimeItem from './BlockTimeItem';
+import BlockValidatorItem from './BlockValidatorItem';
+
+const columns = [
+ {
+ name: 'Block',
+ cell: (row: any) => {
+ const totalGasUsed = (
+ parseFloat(row.node.totalGasUsed) /
+ 10 ** 9
+ ).toString();
+ return (
+
+ );
+ },
+ sortable: false,
+ },
+ {
+ name: 'Blockhash',
+ cell: (row: any) => (
+
+ ),
+ sortable: false,
+ },
+ {
+ name: 'Transactions',
+ cell: (row: any) => (
+
+ {row.node.header.transactionsCount}
+
+ ),
+ sortable: false,
+ },
+ {
+ name: 'Rewards',
+ cell: (row: any) => {
+ const mintTransaction = row.node.transactions.find(
+ (trans: any) => trans.mintAmount != null,
+ );
+ return (
+
+ {mintTransaction ? mintTransaction.mintAmount : 'No mint amount'}{' '}
+
+ );
+ },
+ sortable: false,
+ },
+ {
+ name: 'Validator',
+ cell: (row: any) => (
+
+
+
+ ),
+ sortable: false,
+ },
+ {
+ name: 'Efficiency',
+ cell: (row: any) => (
+
+
+
+ ),
+ sortable: false,
+ },
+ {
+ name: 'Time',
+ cell: (row: any) => {
+ const unixTimestamp = row.node.time.rawUnix;
+ const date = new Date(unixTimestamp * 1000);
+
+ return ;
+ },
+ sortable: false,
+ },
+ {
+ name: '',
+ cell: (row: any) => (
+
+ View
+
+ ),
+ sortable: false,
+ },
+];
+
+type BlocksTableProps = {
+ blocks: GQLBlocksQuery['blocks'];
+ onPageChanged: (pageNumber: number) => void;
+ pageCount: number;
+ currentPage: number;
+ setCurrentPage: (currentPage: number) => void;
+};
+
+function BlocksTable({
+ blocks,
+ onPageChanged,
+ pageCount,
+ currentPage,
+ setCurrentPage,
+}: BlocksTableProps) {
+ const handlePageChanged = (pageNumber: number) => {
+ onPageChanged(pageNumber);
+ };
+
+ return (
+
+
+
+ );
+}
+
+export default BlocksTable;
diff --git a/packages/app-explorer/src/systems/Block/components/Hero.tsx b/packages/app-explorer/src/systems/Block/components/Hero.tsx
new file mode 100644
index 00000000..0ed3fa4b
--- /dev/null
+++ b/packages/app-explorer/src/systems/Block/components/Hero.tsx
@@ -0,0 +1,21 @@
+import { HStack, Heading, Theme, VStack } from '@fuels/ui';
+import { IconChevronRight } from '@tabler/icons-react';
+
+export function Hero() {
+ return (
+
+
+
+ Blocks
+
+
+
+ Home
+
+
+ View All Blocks
+
+
+
+ );
+}
diff --git a/packages/app-explorer/src/systems/Block/screens/BlockScreen.tsx b/packages/app-explorer/src/systems/Block/screens/BlockScreen.tsx
new file mode 100644
index 00000000..b0a4a81d
--- /dev/null
+++ b/packages/app-explorer/src/systems/Block/screens/BlockScreen.tsx
@@ -0,0 +1,130 @@
+import { VStack } from '@fuels/ui';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { useEffect, useState } from 'react';
+
+import { GQLBlocksQuery } from '@fuel-explorer/graphql';
+import { getBlocks } from '../actions/get-blocks';
+import BlocksTable from '../components/BlocksTable';
+import { Hero } from '../components/Hero';
+
+export const BlocksScreen = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const [data, setData] = useState(
+ undefined,
+ );
+ const [dir, setDir] = useState<'after' | 'before'>('after');
+ const [totalPages, setTotalPages] = useState(1);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [currentCursor, setCurrentCursor] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const limit = 10;
+
+ const calculateTotalPages = () => {
+ if (data?.pageInfo.endCursor) {
+ const endCursor = Number(data.pageInfo.endCursor);
+ return Math.ceil(endCursor / limit);
+ }
+ return 1;
+ };
+
+ useEffect(() => {
+ if (data) {
+ const totalPageCount = calculateTotalPages();
+ setTotalPages(totalPageCount);
+ }
+ }, [data]);
+
+ const fetchBlockData = async (
+ cursor: string | null = null,
+ dir: 'after' | 'before' = 'after',
+ ) => {
+ setLoading(true);
+ try {
+ const result = await getBlocks({ cursor, dir });
+ const blockData = result.blocks;
+ setData(blockData);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handlePageChanged = (newPageNumber: number) => {
+ if (data) {
+ const newDir = newPageNumber > currentPage ? 'before' : 'after';
+ let newCursor: string | null = null;
+ setDir(newDir);
+
+ if (
+ newPageNumber === currentPage + 1 ||
+ newPageNumber === currentPage - 1
+ ) {
+ if (newDir === 'before' && data.pageInfo.endCursor) {
+ newCursor = data.pageInfo.endCursor;
+ } else if (newDir === 'after' && data.pageInfo.startCursor) {
+ newCursor = data.pageInfo.startCursor;
+ }
+ } else {
+ if (newDir === 'before' && data.pageInfo.endCursor) {
+ newCursor = (
+ +data.pageInfo.endCursor -
+ (newPageNumber - currentPage) * limit
+ ).toString();
+ } else if (newDir === 'after' && data.pageInfo.startCursor) {
+ newCursor = (
+ +data.pageInfo.startCursor +
+ (currentPage - newPageNumber) * limit
+ ).toString();
+ }
+ }
+
+ setCurrentPage(newPageNumber);
+ setCurrentCursor(newCursor);
+ if (newPageNumber === 1) {
+ router.push('/blocks');
+ return;
+ }
+ router.push(`/blocks?page=${newPageNumber}&cursor=${newCursor}`);
+ }
+ };
+
+ useEffect(() => {
+ const page = parseInt(searchParams.get('page') || '1');
+ const cursor = searchParams.get('cursor') || null;
+
+ setCurrentPage(page);
+ setCurrentCursor(cursor);
+ setDir(page > currentPage ? 'after' : 'before');
+ }, [searchParams]);
+
+ useEffect(() => {
+ fetchBlockData(currentCursor, dir);
+ }, [currentCursor, dir]);
+
+ useEffect(() => {
+ if (data) {
+ const totalPageCount = calculateTotalPages();
+ setTotalPages(totalPageCount);
+ }
+ }, [data]);
+
+ return (
+
+
+ {loading ? (
+ Loading blocks...
+ ) : (
+ data && (
+
+ )
+ )}
+
+ );
+};
diff --git a/packages/ui/package.json b/packages/ui/package.json
index f1419af1..f48c0edd 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -67,6 +67,8 @@
"@react-aria/focus": "3.16.2",
"@tabler/icons-react": "2.47.0",
"@tailwindcss/typography": "0.5.10",
+ "react-paginate": "8.2.0",
+ "react-data-table-component": "7.6.2",
"clsx": "2.1.0",
"csstype": "3.1.3",
"framer-motion": "11.0.5",
diff --git a/packages/ui/src/components/GridTable/GridTable.tsx b/packages/ui/src/components/GridTable/GridTable.tsx
new file mode 100644
index 00000000..4e103133
--- /dev/null
+++ b/packages/ui/src/components/GridTable/GridTable.tsx
@@ -0,0 +1,180 @@
+'use client';
+import React from 'react';
+import DataTable, { TableProps, TableColumn } from 'react-data-table-component';
+import ReactPaginate from 'react-paginate';
+
+export interface GridTableProps extends TableProps {
+ columns: TableColumn[];
+ data: T[];
+ pageCount: number;
+ onPageChanged: (selectedItem: number) => void;
+ currentPage: number;
+ setCurrentPage: (currentPage: number) => void;
+}
+export type GridTableColumn = TableColumn;
+
+export const GridTable = ({
+ columns,
+ data,
+ pageCount,
+ onPageChanged,
+ setCurrentPage,
+ currentPage,
+ ...props
+}: GridTableProps): React.JSX.Element => {
+ const customStyles = {
+ tableWrapper: {
+ style: {
+ borderRadius: '7px',
+ },
+ },
+ table: {
+ style: {
+ backgroundColor: 'transparent',
+ },
+ },
+ headRow: {
+ style: {
+ backgroundColor: 'transparent',
+ color: '#9f9f9f',
+ fontWeight: '600',
+ },
+ },
+ headCells: {
+ style: {
+ backgroundColor: 'transparent',
+ color: '#9f9f9f',
+ fontWeight: '600',
+ },
+ },
+ rows: {
+ style: {
+ cursor: 'pointer',
+ backgroundColor: 'transparent',
+ fontWeight: '400',
+ '&:hover': {
+ backgroundColor: 'var(--gray-2)', // Change background color on hover
+ },
+ },
+ },
+ cells: {
+ style: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingLeft: '0.5rem',
+ paddingRight: '0.5rem',
+ color: 'var(--gray-table-text)',
+ paddingTop: '0.9rem',
+ paddingBottom: '0.9rem',
+ backgroundColor: 'transparent',
+ fontWeight: '400',
+ },
+ },
+ pagination: {
+ style: {
+ backgroundColor: 'var(--gray-2)',
+ color: '#f0f0f0',
+ },
+ pageButtonsStyle: {
+ padding: '8px 16px',
+ margin: '0 4px',
+ color: '#f0f0f0',
+ borderRadius: '4px',
+ backgroundColor: 'var(--gray-2)',
+ '&.selected': {
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
+ fontWeight: 'bold',
+ },
+ '&:hover': {
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ },
+ },
+ },
+ };
+
+ const Pagination: React.FC = () => {
+ return (
+ ← Previous} // Left Arrow
+ nextLabel={Next →} // Right Arrow
+ breakLabel={'...'}
+ pageCount={pageCount}
+ marginPagesDisplayed={2}
+ pageRangeDisplayed={5}
+ onPageChange={(page) => handlePagination(page)}
+ containerClassName={'pagination'}
+ activeClassName={'selected'}
+ disabledClassName={'disabled'}
+ pageLinkClassName={'page-link'}
+ forcePage={currentPage !== 0 ? currentPage - 1 : 0}
+ />
+ );
+ };
+
+ const handlePagination = (page: any) => {
+ setCurrentPage(page.selected + 1);
+ onPageChanged(page.selected + 1);
+ };
+
+ return (
+
+
+
+
+ );
+};
diff --git a/packages/ui/src/components/GridTable/index.tsx b/packages/ui/src/components/GridTable/index.tsx
new file mode 100644
index 00000000..42edf4b3
--- /dev/null
+++ b/packages/ui/src/components/GridTable/index.tsx
@@ -0,0 +1 @@
+export { GridTable } from './GridTable';
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 85be5c02..836e5a47 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -68,4 +68,5 @@ export * from './components/ToggleGroup';
export * from './components/Tooltip';
export * from './components/Portal';
export * from './utils/component';
+export * from './components/GridTable';
export * from './utils/radixUiThemesTailwindPlugin';
diff --git a/packages/ui/src/theme/theme.css b/packages/ui/src/theme/theme.css
index bfd4d716..63941341 100644
--- a/packages/ui/src/theme/theme.css
+++ b/packages/ui/src/theme/theme.css
@@ -832,8 +832,8 @@
--gray-a10: #ffffff72;
--gray-a11: #ffffffaf;
--gray-a12: #ffffffed;
-
--gray-contrast: #FFFFFF;
+ --gray-table-text: #FFFFFF;
--gray-surface: rgba(0, 0, 0, 0.05);
--gray-indicator: var(--gray-9);
}
@@ -871,6 +871,7 @@
--gray-a12: #000000df;
--gray-contrast: #FFFFFF;
+ --gray-table-text: #000000;
--gray-surface: #ffffffcc;
--gray-indicator: var(--gray-9);
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 75a7ae6e..b69ad50d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -869,9 +869,15 @@ importers:
react-aria:
specifier: 3.32.1
version: 3.32.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ react-data-table-component:
+ specifier: 7.6.2
+ version: 7.6.2(react@18.2.0)(styled-components@5.3.11(@babel/core@7.23.9)(react-dom@18.2.0(react@18.2.0))(react-is@18.2.0)(react@18.2.0))
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
+ react-paginate:
+ specifier: 8.2.0
+ version: 8.2.0(react@18.2.0)
react-stately:
specifier: 3.29.1
version: 3.29.1(react@18.2.0)
@@ -13028,6 +13034,12 @@ packages:
react: '>=16.8.0'
react-dom: '>=16.8.0'
+ react-data-table-component@7.6.2:
+ resolution: {integrity: sha512-nHe7040fmtrJyQr/ieGrTfV0jBflYGK4sLokC6/AFOv3ThjmA9WzKz8Z8/2wMxzRqLU+Rn0CVFg+8+frKLepWQ==}
+ peerDependencies:
+ react: '>= 16.8.3'
+ styled-components: '>= 5.0.0'
+
react-docgen-typescript@2.2.2:
resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==}
peerDependencies:
@@ -13078,6 +13090,11 @@ packages:
react: '*'
react-native: '*'
+ react-paginate@8.2.0:
+ resolution: {integrity: sha512-sJCz1PW+9PNIjUSn919nlcRVuleN2YPoFBOvL+6TPgrH/3lwphqiSOgdrLafLdyLDxsgK+oSgviqacF4hxsDIw==}
+ peerDependencies:
+ react: ^16 || ^17 || ^18
+
react-refresh@0.14.0:
resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==}
engines: {node: '>=0.10.0'}
@@ -32308,6 +32325,12 @@ snapshots:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
+ react-data-table-component@7.6.2(react@18.2.0)(styled-components@5.3.11(@babel/core@7.23.9)(react-dom@18.2.0(react@18.2.0))(react-is@18.2.0)(react@18.2.0)):
+ dependencies:
+ deepmerge: 4.3.1
+ react: 18.2.0
+ styled-components: 5.3.11(@babel/core@7.23.9)(react-dom@18.2.0(react@18.2.0))(react-is@18.2.0)(react@18.2.0)
+
react-docgen-typescript@2.2.2(typescript@5.4.5):
dependencies:
typescript: 5.4.5
@@ -32363,6 +32386,11 @@ snapshots:
invariant: 2.2.4
react: 18.2.0
+ react-paginate@8.2.0(react@18.2.0):
+ dependencies:
+ prop-types: 15.8.1
+ react: 18.2.0
+
react-refresh@0.14.0: {}
react-remove-scroll-bar@2.3.4(@types/react@18.2.54)(react@18.2.0):