diff --git a/fuel-toolchain.toml b/fuel-toolchain.toml
new file mode 100644
index 000000000..d541e585d
--- /dev/null
+++ b/fuel-toolchain.toml
@@ -0,0 +1,6 @@
+[toolchain]
+channel = "latest-2023-11-30"
+
+[components]
+fuel-core = "0.24.3"
+forc = "0.54.0"
diff --git a/package.json b/package.json
index 9576bd344..d89f07a6b 100644
--- a/package.json
+++ b/package.json
@@ -136,5 +136,8 @@
"ws@<8.17.1": ">=8.17.1",
"fast-loops@<1.1.4": ">=1.1.4"
}
+ },
+ "dependencies": {
+ "recharts": "2.12.7"
}
}
diff --git a/packages/app-explorer/package.json b/packages/app-explorer/package.json
index 142cbe77f..a261e57a0 100644
--- a/packages/app-explorer/package.json
+++ b/packages/app-explorer/package.json
@@ -40,8 +40,10 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-json-view-lite": "1.4.0",
+ "react-modal": "3.16.1",
"react-use": "17.5.0",
"react-window": "1.8.10",
+ "recharts": "2.12.7",
"tai64": "1.0.0",
"tailwind-variants": "0.1.20",
"wagmi": "2.12.7",
diff --git a/packages/app-explorer/src/app/layout.tsx b/packages/app-explorer/src/app/layout.tsx
index f3323df9f..82c379600 100644
--- a/packages/app-explorer/src/app/layout.tsx
+++ b/packages/app-explorer/src/app/layout.tsx
@@ -43,7 +43,7 @@ export default function RootLayout({
-
+
{children}
diff --git a/packages/app-explorer/src/app/page.tsx b/packages/app-explorer/src/app/page.tsx
index 5fb8a51d1..8c7e0a68e 100644
--- a/packages/app-explorer/src/app/page.tsx
+++ b/packages/app-explorer/src/app/page.tsx
@@ -1,8 +1,51 @@
+// 'use client';
+// import { GridTable } from '@fuels/ui';
+// import { Suspense, useState } from 'react';
+// import { columns, data } from '~/systems/Home/components/DataTable/data';
+// import { TxListLoader } from '~/systems/Transactions/components/TxList/TxListLoader';
+// import { TxsAccountTable } from '~/systems/Transactions/components/TxsAccontTable/TxsAccountTable';
+// import { TxsTitle } from '~/systems/Transactions/components/TxsTitle/TxsTitle';
+// import { TxsTokenTable } from '~/systems/Transactions/components/TxsTokenTable/TxsTokenTable';
+// import type { TxsRouteProps } from '~/systems/Transactions/types';
+
+// export default function Home({ searchParams: { page = '1' } }: TxsRouteProps) {
+// const [activeTab, setActiveTab] = useState('Top Tokens');
+// const [currentGridPage, setCurrentGridPage] = useState(0);
+// return (
+// <>
+//
+
+// }
+// >
+// {activeTab === 'Top Tokens' ? (
+// {}}
+// data={data}
+// columns={columns}
+// pageCount={1}
+// currentPage={currentGridPage}
+// setCurrentPage={setCurrentGridPage}
+// />
+// ) : activeTab === 'Top NFTs' ? (
+//
+// ) : activeTab === 'Top Accounts' ? (
+//
+// ) : (
+// ''
+// )}
+//
+// >
+// );
+// }
+
import { Suspense } from 'react';
import { TxListLoader } from '~/systems/Transactions/components/TxList/TxListLoader';
import { TxsTitle } from '~/systems/Transactions/components/TxsTitle/TxsTitle';
import { TxsScreenSync } from '~/systems/Transactions/pages/TxsScreenSync';
import type { TxsRouteProps } from '~/systems/Transactions/types';
+// import { TxsTitle } from '~/systems/Transactions/components/TxsTitle/TxsTitle';
export default async function Home({
searchParams: { cursor, dir },
diff --git a/packages/app-explorer/src/systems/Core/components/Layout/Layout.tsx b/packages/app-explorer/src/systems/Core/components/Layout/Layout.tsx
index 0041f44ed..55b3bd8de 100644
--- a/packages/app-explorer/src/systems/Core/components/Layout/Layout.tsx
+++ b/packages/app-explorer/src/systems/Core/components/Layout/Layout.tsx
@@ -23,7 +23,7 @@ export function Layout({ children, contentClassName }: LayoutProps) {
diff --git a/packages/app-explorer/src/systems/Core/utils/sdk.ts b/packages/app-explorer/src/systems/Core/utils/sdk.ts
index 43f1a5ec5..f30730925 100644
--- a/packages/app-explorer/src/systems/Core/utils/sdk.ts
+++ b/packages/app-explorer/src/systems/Core/utils/sdk.ts
@@ -1,8 +1,9 @@
import { getSdk } from '@fuel-explorer/graphql/sdk';
import { GraphQLClient } from 'graphql-request';
-const FUEL_INDEXER_API_KEY = process.env.FUEL_INDEXER_API_KEY;
-const FUEL_INDEXER_API = process.env.FUEL_INDEXER_API;
+const FUEL_INDEXER_API_KEY = 'nZ9GZayrd8';
+const FUEL_INDEXER_API =
+ 'https://explorer-indexer-testnet.fuel.network/graphql';
const FUEL_INDEXER_MAINNET_KEY = process.env.FUEL_INDEXER_MAINNET_KEY;
if (!FUEL_INDEXER_API) {
diff --git a/packages/app-explorer/src/systems/Home/components/BlockTableTile.tsx b/packages/app-explorer/src/systems/Home/components/BlockTableTile.tsx
new file mode 100644
index 000000000..765594c7a
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/BlockTableTile.tsx
@@ -0,0 +1,83 @@
+import { HStack, RoundedContainer, VStack } from '@fuels/ui';
+import { tv } from 'tailwind-variants';
+import { fromNow } from '~/systems/Core/utils/dayjs';
+import { Block } from '../interface/blocks.interface';
+
+interface BlockTableProps {
+ block: Block;
+}
+
+export const BlockTableTile: React.FC = ({ block }) => {
+ const classes = styles();
+
+ return (
+
+
+
+
#{block.blockNo}
+
{+block.gasUsed / 10 ** 9} ETH
+
+
+ {/* Uncomment the image if needed */}
+ {/*
*/}
+
+ {block.producer}
+
+
+
+
+
+
+
+ Settled
+
+
+ {fromNow(block.timeStamp)}
+
+
+
+
+
+ );
+};
+
+const styles = tv({
+ slots: {
+ paragraphStrong: [
+ 'text-[12px]',
+ 'text-[color:var(--gray-12)]',
+ 'font-bold',
+ ' w-[110px]',
+ ],
+ paragraph: [
+ 'text-muted',
+ 'text-[12px]',
+ 'p-0',
+ 'whitespace-nowrap',
+ 'text-ellipsis',
+ ],
+ paragraphAccent: ['text-accent text-[12px] p-0'],
+ },
+});
diff --git a/packages/app-explorer/src/systems/Home/components/DailyTransaction.tsx b/packages/app-explorer/src/systems/Home/components/DailyTransaction.tsx
new file mode 100644
index 000000000..ecd01fa41
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/DailyTransaction.tsx
@@ -0,0 +1,134 @@
+import { ChartConfig, RoundedContainer } from '@fuels/ui';
+import dayjs from 'dayjs';
+import {
+ CartesianGrid,
+ Line,
+ LineChart,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from 'recharts';
+// import { Block } from '../interface/blocks.interface';
+
+// const chartData = [
+// { time: '01:00', value: 100000 },
+// { time: '09:00', value: 500000 },
+// { time: '17:00', value: 1000000 },
+// { time: '23:00', value: 1500000 },
+// ];
+
+const chartConfig = {
+ desktop: {
+ label: 'Desktop',
+ color: '#00F58C',
+ },
+} satisfies ChartConfig;
+
+interface DailyTransactionProps {
+ blocks: any;
+}
+const DailyTransaction = (blocks: DailyTransactionProps) => {
+ const chartData = blocks.blocks?.map((block: any) => ({
+ time: dayjs(Number(block.time)).format('HH:mm'),
+ value: block.value,
+ }));
+ const cumilativeTsx = blocks.blocks.reduce(
+ (sum: any, block: any) => sum + Number(block.value),
+ 0,
+ );
+ return (
+
+
+
+
+
+
+
Daily Transactions
+
+
+
+
+
+ The total number of transactions completed on Fuel Network
+ within a 24-hour period.
+
+
+
+
+
+ 24h
+
+
+
+ {cumilativeTsx.toLocaleString()}
+
+
+
+
+
+
+ [`${Number(value)}`]}
+ labelFormatter={(label) => label.toLocaleString()}
+ contentStyle={{
+ backgroundColor: 'var(--gray-1)',
+ borderColor: 'var(--gray-2)',
+ borderRadius: '8px',
+ color: 'var(--gray-1)',
+ }}
+ labelStyle={{
+ color: 'var(--gray-12)',
+ fontWeight: 'bold',
+ }}
+ itemStyle={{
+ color: '#00F58C',
+ }}
+ cursor={{ strokeWidth: 0.1, radius: 10 }}
+ />
+
+
+
+
+
+
+ );
+};
+
+export default DailyTransaction;
diff --git a/packages/app-explorer/src/systems/Home/components/DataTable/GridTable.stories.tsx b/packages/app-explorer/src/systems/Home/components/DataTable/GridTable.stories.tsx
new file mode 100644
index 000000000..264a226cb
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/DataTable/GridTable.stories.tsx
@@ -0,0 +1,60 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import GridTable, { GridTableProps } from './GridTable';
+import { columns, data } from './data';
+
+const meta: Meta = {
+ title: 'Home/GridTable',
+ component: GridTable,
+ parameters: {
+ layout: 'fullscreen',
+ },
+};
+
+export default meta;
+type Story = StoryObj>;
+
+export const Desktop: Story = {
+ args: {
+ columns,
+ data: data.slice(0, 10),
+ totalRows: data.length,
+ rowsPerPage: 10,
+ pageCount: Math.ceil(data.length / 10),
+ onPageChanged: (selectedPage: number) =>
+ console.log(`Page changed to: ${selectedPage}`),
+ },
+};
+
+export const Tablet: Story = {
+ args: {
+ columns,
+ data: data.slice(0, 10),
+ totalRows: data.length,
+ rowsPerPage: 10,
+ pageCount: Math.ceil(data.length / 10),
+ onPageChanged: (selectedPage: number) =>
+ console.log(`Page changed to: ${selectedPage}`),
+ },
+ parameters: {
+ viewport: {
+ defaultViewport: 'ipad',
+ },
+ },
+};
+
+export const Mobile: Story = {
+ args: {
+ columns,
+ data: data.slice(0, 10),
+ totalRows: data.length,
+ rowsPerPage: 10,
+ pageCount: Math.ceil(data.length / 10),
+ onPageChanged: (selectedPage: number) =>
+ console.log(`Page changed to: ${selectedPage}`),
+ },
+ parameters: {
+ viewport: {
+ defaultViewport: 'iphonex',
+ },
+ },
+};
diff --git a/packages/app-explorer/src/systems/Home/components/DataTable/GridTable.tsx b/packages/app-explorer/src/systems/Home/components/DataTable/GridTable.tsx
new file mode 100644
index 000000000..62f7ba152
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/DataTable/GridTable.tsx
@@ -0,0 +1,131 @@
+import React, { useState } from 'react';
+import DataTable, { TableProps, TableColumn } from 'react-data-table-component';
+import ReactPaginate from 'react-paginate';
+import './gridTable.css';
+
+export interface GridTableProps extends TableProps {
+ columns: TableColumn[];
+ data: T[];
+ pageCount: number;
+ onPageChanged: (selectedItem: number) => void;
+}
+
+function GridTable({
+ columns,
+ data,
+ pageCount,
+ onPageChanged,
+ ...props
+}: GridTableProps): React.JSX.Element {
+ const [_currentPage, setCurrentPage] = useState(0);
+
+ const customStyles = {
+ tableWrapper: {
+ style: {
+ borderRadius: '7px',
+ overflow: 'hidden',
+ },
+ },
+ table: {
+ style: {
+ backgroundColor: 'var(--gray-2)',
+ tableLayout: 'auto',
+ },
+ },
+ headRow: {
+ style: {
+ backgroundColor: 'var(--gray-2)',
+ color: '#9f9f9f',
+ fontWeight: '600',
+ },
+ },
+ headCells: {
+ style: {
+ backgroundColor: 'var(--gray-2)',
+ color: '#9f9f9f',
+ paddingLeft: '1rem',
+ paddingRight: '1rem',
+ paddingTop: '1.2rem',
+ paddingBottom: '1.2rem',
+ fontWeight: '600',
+ whiteSpace: 'nowrap',
+ },
+ },
+ rows: {
+ style: {
+ backgroundColor: 'var(--gray-2)',
+ fontWeight: '400',
+ },
+ },
+ cells: {
+ style: {
+ paddingLeft: '1rem',
+ paddingRight: '1rem',
+ color: 'var(--gray-table-text)',
+ paddingTop: '1.2rem',
+ paddingBottom: '1.2rem',
+ backgroundColor: 'var(--gray-2)',
+ fontWeight: '400',
+ whiteSpace: 'nowrap',
+ },
+ },
+ 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={handlePageClick}
+ containerClassName={'pagination'}
+ activeClassName={'selected'}
+ disabledClassName={'disabled'} // Handles styling for disabled state
+ pageLinkClassName={'page-link'} // Ensures consistent page link styling
+ />
+ );
+ };
+
+ const handlePageClick = (data: { selected: number }) => {
+ setCurrentPage(data.selected);
+ onPageChanged(data.selected);
+ };
+
+ return (
+
+
+
+ );
+}
+
+export default GridTable;
diff --git a/packages/app-explorer/src/systems/Home/components/DataTable/data.ts b/packages/app-explorer/src/systems/Home/components/DataTable/data.ts
new file mode 100644
index 000000000..af1edf92c
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/DataTable/data.ts
@@ -0,0 +1,110 @@
+import { TableColumn } from 'react-data-table-component';
+
+export interface RowData {
+ id: number;
+ name: string;
+ age: number;
+ email: string;
+ status: string;
+}
+
+export const data: RowData[] = [
+ {
+ id: 1,
+ name: 'John Doe',
+ age: 28,
+ email: 'john.doe@example.com',
+ status: 'Active',
+ },
+ {
+ id: 2,
+ name: 'Jane Smith',
+ age: 34,
+ email: 'jane.smith@example.com',
+ status: 'Inactive',
+ },
+ {
+ id: 3,
+ name: 'Michael Johnson',
+ age: 45,
+ email: 'michael.johnson@example.com',
+ status: 'Active',
+ },
+ {
+ id: 4,
+ name: 'Emily Davis',
+ age: 23,
+ email: 'emily.davis@example.com',
+ status: 'Pending',
+ },
+ {
+ id: 5,
+ name: 'William Brown',
+ age: 39,
+ email: 'william.brown@example.com',
+ status: 'Active',
+ },
+ {
+ id: 6,
+ name: 'Olivia Taylor',
+ age: 29,
+ email: 'olivia.taylor@example.com',
+ status: 'Inactive',
+ },
+ {
+ id: 7,
+ name: 'James Anderson',
+ age: 32,
+ email: 'james.anderson@example.com',
+ status: 'Pending',
+ },
+ {
+ id: 8,
+ name: 'Sophia Thomas',
+ age: 27,
+ email: 'sophia.thomas@example.com',
+ status: 'Active',
+ },
+ {
+ id: 9,
+ name: 'Isabella Lee',
+ age: 31,
+ email: 'isabella.lee@example.com',
+ status: 'Inactive',
+ },
+ {
+ id: 10,
+ name: 'David Martinez',
+ age: 36,
+ email: 'david.martinez@example.com',
+ status: 'Active',
+ },
+];
+
+export const columns: TableColumn[] = [
+ {
+ name: 'Rank',
+ selector: (row) => row.id,
+ sortable: true,
+ },
+ {
+ name: 'Token',
+ selector: (row) => row.name,
+ sortable: true,
+ },
+ {
+ name: 'Price',
+ selector: (row) => row.age,
+ sortable: true,
+ },
+ {
+ name: '24H Change (%)',
+ selector: (row) => row.email,
+ sortable: false,
+ },
+ {
+ name: 'Volume (24H)',
+ selector: (row) => row.status,
+ sortable: true,
+ },
+];
diff --git a/packages/app-explorer/src/systems/Home/components/DataTable/gridTable.css b/packages/app-explorer/src/systems/Home/components/DataTable/gridTable.css
new file mode 100644
index 000000000..3f6f23359
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/DataTable/gridTable.css
@@ -0,0 +1,47 @@
+.pagination {
+ display: flex;
+ justify-content: end;
+ align-items: center;
+ list-style: none;
+ padding: 0;
+ margin: 1rem 0;
+ }
+
+ .pagination li {
+ margin: 0 8px; /* Adjusts space between the pagination items */
+ }
+
+ .pagination li a {
+ padding: 8px 16px; /* Adjusts padding inside each pagination item */
+ color: #f0f0f0;
+ background-color: rgb(32, 32, 32);
+ border-radius: 7px;
+ cursor: pointer;
+ text-decoration: none;
+ }
+
+ .pagination li.selected a {
+ background-color: rgba(255, 255, 255, 0.2); /* Slightly lighter background */
+ font-weight: bold;
+ }
+
+ .pagination li a:hover {
+ background-color: rgba(255, 255, 255, 0.1);
+ }
+
+ /* Remove background color for Previous and Next buttons */
+ .pagination li.previous a,
+ .pagination li.next a {
+ background-color: transparent;
+ padding: 0;
+ }
+
+ .pagination li.disabled a {
+ color: #888; /* Disabled color for previous/next buttons */
+ cursor: not-allowed;
+ }
+
+ .pagination li.disabled a:hover {
+ background-color: transparent; /* Keeps background same on hover */
+ }
+
\ No newline at end of file
diff --git a/packages/app-explorer/src/systems/Home/components/DataTable/index.tsx b/packages/app-explorer/src/systems/Home/components/DataTable/index.tsx
new file mode 100644
index 000000000..9ab1264fb
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/DataTable/index.tsx
@@ -0,0 +1,32 @@
+import { Button, Link, RoundedContainer, VStack } from '@fuels/ui';
+
+import NextLink from 'next/link';
+import React from 'react';
+import { Block } from '../../interface/blocks.interface';
+import { BlockTableTile } from '../BlockTableTile';
+interface DataTableProps {
+ blocks: Block[];
+}
+
+export const DataTable = (props: DataTableProps) => {
+ return (
+
+
+ {Array.from({ length: props.blocks.length }, (_, index) => (
+
+
+
+ ))}
+
+
+
+
+
+ );
+};
diff --git a/packages/app-explorer/src/systems/Home/components/Epoch.tsx b/packages/app-explorer/src/systems/Home/components/Epoch.tsx
new file mode 100644
index 000000000..f861265f7
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/Epoch.tsx
@@ -0,0 +1,34 @@
+import { HStack, RoundedContainer } from '@fuels/ui';
+
+const Epoch = () => {
+ return (
+
+
+
+
+ Current EPOCH
+
+
+
+
+ 657
+
+
+ 55%
+
+
+
+
+
+
+ 0d 15h 32m 30s
+
+
+ );
+};
+
+export default Epoch;
diff --git a/packages/app-explorer/src/systems/Home/components/GasSaved/index.tsx b/packages/app-explorer/src/systems/Home/components/GasSaved/index.tsx
new file mode 100644
index 000000000..06d65139b
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/GasSaved/index.tsx
@@ -0,0 +1,108 @@
+import { ChartConfig, HStack, RoundedContainer } from '@fuels/ui';
+import {
+ CartesianGrid,
+ Line,
+ LineChart,
+ ResponsiveContainer,
+ XAxis,
+ YAxis,
+} from 'recharts';
+
+const chartData = [
+ { time: '01:00', ETH: 0.1, FUEL: 0.12 },
+ { time: '09:00', ETH: 0.5, FUEL: 0.48 },
+ { time: '17:00', ETH: 0.9, FUEL: 0.85 },
+ { time: '23:00', ETH: 1.2, FUEL: 1.15 },
+];
+
+const chartConfig = {
+ eth: {
+ label: 'ETH',
+ color: '#00E8FC', // Light blue for ETH
+ },
+ fuel: {
+ label: 'FUEL',
+ color: '#00F58C', // Light green for FUEL
+ },
+} satisfies ChartConfig;
+
+export const GasSaved = () => {
+ const numberFormatter = new Intl.NumberFormat('en-US', {
+ maximumFractionDigits: 1,
+ style: 'currency',
+ currency: 'USD',
+ });
+
+ return (
+
+
+
+
+ Gas Used
+
+
+ 24h
+
+
+
+
+ $1.14
+
+
+ Per Transaction v ETH
+
+
+
+
+
+
+ {
+ return numberFormatter.format(value);
+ }}
+ />
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/app-explorer/src/systems/Home/components/GasSpentChart/index.tsx b/packages/app-explorer/src/systems/Home/components/GasSpentChart/index.tsx
new file mode 100644
index 000000000..0110979b9
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/GasSpentChart/index.tsx
@@ -0,0 +1,161 @@
+import { ChartConfig, HStack, RoundedContainer } from '@fuels/ui';
+import dayjs from 'dayjs';
+import {
+ CartesianGrid,
+ Line,
+ LineChart,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from 'recharts';
+
+const chartConfig = {
+ // eth: {
+ // label: 'ETH',
+ // color: '#00E8FC', // Light blue for ETH
+ // },
+ fuel: {
+ label: 'FUEL',
+ color: '#00F58C', // Light green for FUEL
+ },
+} satisfies ChartConfig;
+
+interface GasSpentProps {
+ blocks: any;
+}
+export const GasSpentChart = (gasSpent: GasSpentProps) => {
+ const totalGasSpent = gasSpent.blocks
+ .map((block: any) => +block.value)
+ .reduce((acc: any, value: any) => acc + value, 0);
+ const chartData = gasSpent.blocks.map((e: any) => {
+ return {
+ time: dayjs(Number(e.time)).format('HH:mm'),
+ ETH: +e.value,
+ };
+ });
+ const minGasUsed = Math.min(...chartData.map((e: any) => e.ETH)) / 10 ** 9;
+ const maxGasUsed = Math.max(...chartData.map((e: any) => e.ETH)) / 10 ** 9;
+
+ // const numberFormatter = new Intl.NumberFormat('en-US', {
+ // maximumFractionDigits: 1,
+ // style: 'currency',
+ // currency: 'USD',
+ // });
+
+ return (
+
+
+
+
+
+
+
+ The percentage of block resources utilized by transactions.
+
+
+
+
+
+ 24h
+
+
+
+
+ {(totalGasSpent / 10 ** 9).toFixed(8)}
+
+
+ ETH
+
+
+
+
+
+
+ [`${Number(value) / 10 ** 9} ETH`]}
+ labelFormatter={(label) => label.toLocaleString()}
+ contentStyle={{
+ backgroundColor: 'var(--gray-1)',
+ borderColor: 'var(--gray-2)',
+ borderRadius: '8px',
+ color: 'var(--gray-1)',
+ }}
+ labelStyle={{
+ color: 'var(--gray-12)',
+ fontWeight: 'bold',
+ }}
+ itemStyle={{
+ color: '#00F58C',
+ }}
+ cursor={{ strokeWidth: 0.1, radius: 10 }}
+ />
+ {
+ return Number((value / 10 ** 9).toFixed(6)).toExponential();
+ }}
+ />
+
+ {/* */}
+
+
+
+
+
+
+ );
+};
diff --git a/packages/app-explorer/src/systems/Home/components/GasTracker.tsx b/packages/app-explorer/src/systems/Home/components/GasTracker.tsx
new file mode 100644
index 000000000..74e4775e2
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/GasTracker.tsx
@@ -0,0 +1,78 @@
+import { RoundedContainer } from '@fuels/ui';
+import React from 'react';
+
+export const GasTracker = () => {
+ return (
+
+
+
+
+ Gas Tracker
+
+
+
+
+
+
+ );
+};
diff --git a/packages/app-explorer/src/systems/Home/components/Hero/Hero.tsx b/packages/app-explorer/src/systems/Home/components/Hero/Hero.tsx
index 9730ad906..6c1fc8caf 100644
--- a/packages/app-explorer/src/systems/Home/components/Hero/Hero.tsx
+++ b/packages/app-explorer/src/systems/Home/components/Hero/Hero.tsx
@@ -1,40 +1,212 @@
'use client';
-import { Box, Container, Heading } from '@fuels/ui';
+import { Box, Container, Heading, Theme, VStack } from '@fuels/ui';
+
+import { LoadingBox, LoadingWrapper } from '@fuels/ui';
+import { useEffect, useState } from 'react';
import { tv } from 'tailwind-variants';
-import { SearchWidget } from '~/systems/Core/components/Search/SearchWidget';
+import projectJson from '../../../../../../app-portal/src/systems/Ecosystem/data/projects.json';
+import { DataTable } from '../../components/DataTable';
+import { Block } from '../../interface/blocks.interface';
+import DailyTransaction from '../DailyTransaction';
+// import Epoch from '../Epoch';
+import { GasSpentChart } from '../GasSpentChart';
+// import { GasTracker } from '../GasTracker';
+import { LatestBlock } from '../LatestBlock';
+import { TPS } from '../TPS';
+import TotalDapps from '../TotalDapps/TotalDapps';
+import { getBlocksDashboard } from './actions/get-blocks-dashboard';
+import { getTPS } from './actions/get-tps';
export function Hero() {
const classes = styles();
+ const [isLoading, setIsLoading] = useState(true);
+ const [tpsData, setTpsData] = useState(null);
+ const [blocksData, setBlocksData] = useState(null);
+ const [isFirstFetch, setIsFirstFetch] = useState(true);
+
+ const getTPSData = async () => {
+ try {
+ const result = await getTPS();
+ const dashboard = await getBlocksDashboard();
+
+ setTpsData(result);
+
+ setBlocksData(dashboard);
+
+ if (isFirstFetch) {
+ setIsLoading(false); // Set loading to false only after first fetch
+ setIsFirstFetch(false); // Mark that the first fetch is done
+ }
+ } catch (_e) {}
+ };
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ getTPSData();
+ }, 10000);
+
+ return () => clearInterval(interval);
+ }, []);
+
+ const blocks: Block[] =
+ blocksData?.getBlocksDashboard.nodes.map((node: any) => ({
+ blockNo: node.blockNo ?? '',
+ producer: node.producer ?? '',
+ timeStamp: node.timestamp,
+ gasUsed: node.gasUsed,
+ tps: node.tps,
+ })) || [];
+
+ const tps: any =
+ tpsData?.tps.nodes.map((node: any) => ({
+ start: node.start ?? '',
+ end: node.end ?? '',
+ totalGas: node.totalGas,
+ txCount: node.txCount,
+ })) || [];
+
+ const dailyTsxData = tps?.map((t: any) => ({
+ time: t.start ?? '',
+ value: t.txCount,
+ }));
+
+ console.log('the dailyTsx is ', dailyTsxData);
+
+ const tpsTsxData = tps?.map((t: any) => ({
+ time: t.start ?? '',
+ value: t.txCount,
+ }));
+ console.log('the tps6Tsx is ', tpsTsxData);
+
+ const totalProjects = projectJson.length;
+ const activeProjects = projectJson.filter(
+ (item) => item.isLive === true,
+ ).length;
+
+ const elementsWithImage = projectJson.filter((item) => item.image);
+
+ const top3Projects = elementsWithImage
+ .filter((element) => element.isFeatured && element.isLive)
+ .slice(0, 3);
return (
-
-
-
- Explore Fuel
-
-
-
-
-
-
+
+
+
+
+
+ Fuel Explorer
+
+ {/*
+ Trending Items
+
+
+
+
*/}
+
+
+
+ }
+ regularEl={}
+ />
+
+
+ }
+ regularEl={
+
+ }
+ />
+
+
+ }
+ regularEl={
+
+ }
+ />
+
+
+ }
+ regularEl={}
+ />
+
+
+
+ }
+ regularEl={}
+ />
+ {/* */}
+
+
+ }
+ regularEl={}
+ />
+
+
+
+
+
+
);
}
const styles = tv({
slots: {
- root: 'hero-bg overflow-clip relative w-full border-b border-border',
+ root: 'overflow-clip relative w-full border-border bg-gray-3 dark:bg-gray-1',
container: [
- 'z-20 relative py-8 pt-6 px-8 tablet:py-28 tablet:pt-24 tablet:px-10',
- 'tablet:max-laptop:max-w-[500px] [&_.rt-ContainerInner]:p-2 [&_.rt-ContainerInner]:min-h-[120px]',
- '[&_.rt-ContainerInner]:tablet:max-laptop:bg-black [&_.rt-ContainerInner]:tablet:max-laptop:bg-opacity-60 [&_.rt-ContainerInner]:tablet:max-laptop:rounded-lg [&_.rt-ContainerInner]:tablet:max-laptop:shadow-2xl',
+ 'z-20 relative py-8 pt-6 px-8 tablet:pt-18 tablet:px-10',
+ 'tablet:max-laptop:max-w-[500px] [&_.rt-ContainerInner]:p-2',
+ ' [&_.rt-ContainerInner]:tablet:max-laptop:bg-opacity-60 [&_.rt-ContainerInner]:tablet:max-laptop:rounded-lg [&_.rt-ContainerInner]:tablet:max-laptop:shadow-2xl',
],
input: 'w-full tablet:w-[400px]',
title: [
- 'text-2xl leading-snug text-white mb-4 justify-center',
+ 'text-2xl leading-snug text-heading justify-center',
'tablet:text-left tablet:text-4xl tablet:justify-start',
],
subtitle: ['text-base mb-8 justify-center'],
- searchWrapper: 'max-w-[400px]',
+ searchWrapper: [
+ 'grid gap-5',
+ 'grid-cols-1 grid-rows-auto auto-rows-min',
+ 'md:grid-cols-1 md:grid-rows-[auto,auto]',
+ 'lg:grid-cols-12 lg:grid-rows-[repeat(4,auto)]',
+ 'gap-y-5 gap-x-4',
+ 'sm:grid-cols-1',
+ ],
},
});
diff --git a/packages/graphql/logs/empty b/packages/app-explorer/src/systems/Home/components/Hero/Home/components/DailyTransaction.tsx
similarity index 100%
rename from packages/graphql/logs/empty
rename to packages/app-explorer/src/systems/Home/components/Hero/Home/components/DailyTransaction.tsx
diff --git a/packages/app-explorer/src/systems/Home/components/Hero/Home/components/Hero/Hero.stories.tsx b/packages/app-explorer/src/systems/Home/components/Hero/Home/components/Hero/Hero.stories.tsx
new file mode 100644
index 000000000..51f030b43
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/Hero/Home/components/Hero/Hero.stories.tsx
@@ -0,0 +1,36 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Hero } from './Hero';
+
+const meta: Meta = {
+ title: 'Home/Hero',
+ component: Hero,
+ parameters: {
+ layout: 'fullscreen',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Desktop: Story = {
+ args: {},
+};
+
+export const Tablet: Story = {
+ args: {},
+ parameters: {
+ viewport: {
+ defaultViewport: 'ipad',
+ },
+ },
+};
+
+export const Mobile: Story = {
+ args: {},
+ parameters: {
+ viewport: {
+ defaultViewport: 'iphonex',
+ },
+ },
+};
diff --git a/packages/app-explorer/src/systems/Home/components/Hero/actions/get-blocks-dashboard.ts b/packages/app-explorer/src/systems/Home/components/Hero/actions/get-blocks-dashboard.ts
new file mode 100644
index 000000000..ac4622aa0
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/Hero/actions/get-blocks-dashboard.ts
@@ -0,0 +1,6 @@
+import { sdk } from '~/systems/Core/utils/sdk';
+
+export const getBlocksDashboard = async () => {
+ const { data } = await sdk.getBlocksDashboard();
+ return data;
+};
diff --git a/packages/app-explorer/src/systems/Home/components/Hero/actions/get-tps.ts b/packages/app-explorer/src/systems/Home/components/Hero/actions/get-tps.ts
new file mode 100644
index 000000000..467c7a701
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/Hero/actions/get-tps.ts
@@ -0,0 +1,6 @@
+import { sdk } from '~/systems/Core/utils/sdk';
+
+export const getTPS = async () => {
+ const { data } = await sdk.tps();
+ return data;
+};
diff --git a/packages/app-explorer/src/systems/Home/components/LatestBlock.tsx b/packages/app-explorer/src/systems/Home/components/LatestBlock.tsx
new file mode 100644
index 000000000..652b99032
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/LatestBlock.tsx
@@ -0,0 +1,63 @@
+import { HStack, RoundedContainer, VStack } from '@fuels/ui';
+
+import { fromNow } from '~/systems/Core/utils/dayjs';
+import { Block } from '../interface/blocks.interface';
+
+export const LatestBlock = (block: Block) => {
+ return (
+
+
+
+
+
+
+
Latest Block
+
+
+
+
+
+ The percentage of block resources utilized by transactions.
+
+
+
+
+
+ {fromNow(block?.timeStamp)}
+
+
+
+ {block.blockNo}
+
+
+
+
+
+
+
+ {block.producer}
+
+
+
+
+ Block Reward
+
+
+ {(+block.gasUsed / 10 ** 9).toFixed(2)} ETH
+
+
+
+
+ );
+};
diff --git a/packages/app-explorer/src/systems/Home/components/TPS.tsx b/packages/app-explorer/src/systems/Home/components/TPS.tsx
new file mode 100644
index 000000000..5d93d88b2
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/TPS.tsx
@@ -0,0 +1,139 @@
+import { HStack, RoundedContainer } from '@fuels/ui';
+import dayjs from 'dayjs';
+import {
+ Bar,
+ BarChart,
+ CartesianGrid,
+ Cell,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from 'recharts';
+
+export interface TPSProps {
+ blocks: any;
+}
+
+export const TPS = (props: TPSProps) => {
+ const blocks = props.blocks;
+
+ const chartData = blocks?.map((block: any) => ({
+ time: dayjs(Number(block.time)).format('HH:mm'),
+ value: block.value / 3600,
+ }));
+
+ const averageTPS =
+ blocks.reduce((sum: any, block: any) => sum + Number(block.value), 0) /
+ blocks.length;
+
+ const highestValue = Math.max(
+ ...chartData.map((data: any) => Number(data.value)),
+ );
+
+ const getTicks = () => {
+ const ticks: string[] = [];
+ for (let i = 0; i < chartData.length; i += 6) {
+ ticks.push(chartData[i].time);
+ }
+ return ticks;
+ };
+
+ return (
+
+
+
+
+
+
+
+ Transactions Per Second processed by the Fuel network.
+
+
+
+
+
+ 24h
+
+
+
+
+ {`${Math.ceil(averageTPS / 3600)}`}
+
+ TX/s
+
+
+
+
+
+ value}
+ />
+
+ [
+ `${Number(value).toFixed(2)}`,
+ 'Avg TPS per hour',
+ ]}
+ labelFormatter={(label) => label.toLocaleString()}
+ contentStyle={{
+ backgroundColor: 'var(--gray-1)',
+ borderColor: 'var(--gray-2)',
+ borderRadius: '8px',
+ color: 'var(--gray-1)',
+ }}
+ labelStyle={{
+ color: 'var(--gray-12)',
+ fontWeight: 'bold',
+ }}
+ itemStyle={{
+ color: '#00F58C',
+ }}
+ cursor={{ strokeWidth: 0.1, radius: 10 }}
+ />
+
+ {chartData.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+
+ );
+};
diff --git a/packages/app-explorer/src/systems/Home/components/TableCell/TableTile.jsx b/packages/app-explorer/src/systems/Home/components/TableCell/TableTile.jsx
new file mode 100644
index 000000000..6224fd894
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/TableCell/TableTile.jsx
@@ -0,0 +1,55 @@
+import { Flex, HStack, RoundedContainer } from '@fuels/ui';
+import { tv } from 'tailwind-variants';
+
+export const TableTile = () => {
+ const classes = styles();
+
+ return (
+
+
+
+
#4540916
+
0.004878700 ETH
+
+
+
+
fuel1a...3zxt
+
+
+
+
+
+ Settled
+
+
1 Hour Ago
+
+
+
+ );
+};
+
+const styles = tv({
+ slots: {
+ paragraphStrong: ['text-[14px]'],
+ paragraph: ['text-muted text-[14px] p-0'],
+ paragraphAccent: ['text-accent text-[14px] p-0'],
+ },
+});
diff --git a/packages/app-explorer/src/systems/Home/components/TotalDapps/DAppTile.tsx b/packages/app-explorer/src/systems/Home/components/TotalDapps/DAppTile.tsx
new file mode 100644
index 000000000..d6a83c4fd
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/TotalDapps/DAppTile.tsx
@@ -0,0 +1,23 @@
+import { HStack } from '@fuels/ui';
+import React from 'react';
+
+export interface DAppTileProps {
+ name: string;
+ image: string;
+}
+const DAppTile = (props: DAppTileProps) => {
+ return (
+
+
+ {props.name}
+
+ );
+};
+
+export default DAppTile;
diff --git a/packages/app-explorer/src/systems/Home/components/TotalDapps/TotalDapps.tsx b/packages/app-explorer/src/systems/Home/components/TotalDapps/TotalDapps.tsx
new file mode 100644
index 000000000..f60e4eaee
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/TotalDapps/TotalDapps.tsx
@@ -0,0 +1,142 @@
+import { RoundedContainer } from '@fuels/ui';
+import Link from 'next/link';
+import React from 'react';
+
+interface ValidatorStatusProps {
+ active: number;
+ total: number;
+ featured: any;
+}
+
+const TotalDapps: React.FC = ({
+ active,
+ total,
+ featured,
+}) => {
+ const activePercentage = (active / total) * 100;
+ const buildingPercentage = ((total - active) / total) * 100;
+
+ const activeBarStyle = {
+ width: `${activePercentage}%`,
+ height: '5px',
+ borderRadius: '4px',
+ transition: 'width 0.4s ease-in-out',
+ };
+
+ const buildingBarStyle = {
+ width: `${buildingPercentage}%`,
+ height: '5px',
+ borderRadius: '4px',
+ transition: 'width 0.4s ease-in-out',
+ };
+ const _image = 'zap';
+
+ return (
+
+
+
+ Fuel Dapps
+
+
+ View All
+
+
+
+ {total}
+
+
+
+
+
+
+ Active: {active}
+
+
+ Building: {total - active}
+
+
+
+
+
+
+ Featured Dapps
+
+
+ {featured.map((feature: any) => {
+ return (
+
+
+
{feature.name}
+
+ );
+ })}
+
+
+ );
+};
+;
+export default TotalDapps;
diff --git a/packages/app-explorer/src/systems/Home/components/TrendingCard/TrendingCard.tsx b/packages/app-explorer/src/systems/Home/components/TrendingCard/TrendingCard.tsx
new file mode 100644
index 000000000..65ae217ca
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/TrendingCard/TrendingCard.tsx
@@ -0,0 +1,31 @@
+import { RoundedContainer } from '@fuels/ui';
+import { tv } from 'tailwind-variants';
+
+export interface TrendingCardProps {
+ title: string;
+ icon: string;
+}
+
+export const TrendingCard = ({ icon, title }: TrendingCardProps) => {
+ const classes = styles();
+ return (
+
+
+
+
{title}
+
+
+ );
+};
+
+const styles = tv({
+ slots: {
+ paragraphStrong: ['text-sm px-2 whitespace-nowrap'],
+ },
+});
diff --git a/packages/app-explorer/src/systems/Home/components/TrendingCardCarousel.tsx b/packages/app-explorer/src/systems/Home/components/TrendingCardCarousel.tsx
new file mode 100644
index 000000000..964461392
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/TrendingCardCarousel.tsx
@@ -0,0 +1,132 @@
+'use client';
+
+import { Button, Carousel, CarouselContent, CarouselItem } from '@fuels/ui';
+import { CarouselApi } from '@fuels/ui/src/components/Carousel/Carousel';
+import React from 'react';
+import { TrendingCard } from '../components/TrendingCard/TrendingCard';
+
+export const TRENDING_DATA = [
+ {
+ title: 'Bored Ape',
+ icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__',
+ },
+ {
+ title: 'Bored Ape',
+ icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__',
+ },
+ {
+ title: 'Bored Ape',
+ icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__',
+ },
+ {
+ title: 'Bored Ape',
+ icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__',
+ },
+ {
+ title: 'Bored Ape',
+ icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__',
+ },
+ {
+ title: 'Bored Ape',
+ icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__',
+ },
+ {
+ title: 'Bored Ape',
+ icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__',
+ },
+ {
+ title: 'Bored Ape',
+ icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__',
+ },
+ {
+ title: 'Bored Ape',
+ icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__',
+ },
+ {
+ title: 'Bored Ape',
+ icon: 'https://s3-alpha-sig.figma.com/img/5749/3ce7/292d2723de1ca424839b5b023c2aa32a?Expires=1724025600&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ErIqnWTWbaxPj5AuflxmF-maSANAvESYDz~GS7U4C8fZwxqzOaBMWZvcMPMloF5b6EeNzRktU6-YsGzlCz31S3Ch9EhGzGCCuqb2U6JNoOMKfsO7i2VTvsGT2ScFn1tTyxVYegQTRIwyhnV7feQW7KU7biO4W0Ahs-Ncvytj0wohz2dNXjrHeYN7Yap5o5aU-No2ct54EaevLGiGFgeDWO9Ysbl1o4AcCAH4Eua9~S3IBV035RSXQNYdSqQjYOsuRcpBKfbbZoZOHw5yWCNEUOqnTjFpXbFC7bGqwPCaaYbSjElFObIpqI83GNbZ0UOEQb-eI9lPJEX-2lOvJFdm3g__',
+ },
+];
+
+export const TrendingCardCarousel = () => {
+ const [api, setApi] = React.useState();
+ const [current, setCurrent] = React.useState(0);
+ const [count, setCount] = React.useState(0);
+
+ React.useEffect(() => {
+ if (!api) {
+ return;
+ }
+
+ setCount(api.scrollSnapList().length);
+ setCurrent(api.selectedScrollSnap() + 1);
+
+ api.on('select', () => {
+ setCurrent(api.selectedScrollSnap() + 1);
+ });
+ }, [api]);
+
+ return (
+
+ {/* Mext button */}
+ {current < count && (
+
+ )}
+ {/* Previous Button */}
+ {current > 1 && (
+
+ )}
+
+
+ {TRENDING_DATA.map((item, i) => (
+
+
+
+ ))}
+
+
+
+ );
+};
diff --git a/packages/app-explorer/src/systems/Home/components/ViewButton.tsx b/packages/app-explorer/src/systems/Home/components/ViewButton.tsx
new file mode 100644
index 000000000..7717b7987
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/components/ViewButton.tsx
@@ -0,0 +1,34 @@
+import { HStack, RoundedContainer } from '@fuels/ui';
+import { tv } from 'tailwind-variants';
+
+export const ViewAllButton = () => {
+ const classes = styles();
+ return (
+
+
+ View All
+
+
+
+ );
+};
+
+const styles = tv({
+ slots: {
+ paragraphStrong: ['text-[14px] p-0'],
+ },
+});
diff --git a/packages/app-explorer/src/systems/Home/interface/blocks.interface.ts b/packages/app-explorer/src/systems/Home/interface/blocks.interface.ts
new file mode 100644
index 000000000..96c58b59d
--- /dev/null
+++ b/packages/app-explorer/src/systems/Home/interface/blocks.interface.ts
@@ -0,0 +1,6 @@
+export interface Block {
+ blockNo: string;
+ producer: string;
+ timeStamp: string;
+ gasUsed: string;
+}
diff --git a/packages/app-explorer/src/systems/Statistics/getAccounts.ts b/packages/app-explorer/src/systems/Statistics/getAccounts.ts
new file mode 100644
index 000000000..6d130f58f
--- /dev/null
+++ b/packages/app-explorer/src/systems/Statistics/getAccounts.ts
@@ -0,0 +1,137 @@
+'use server';
+
+import { z } from 'zod';
+import { act } from '../../systems/Core/utils/act-server';
+import { sdk } from '../../systems/Core/utils/sdk';
+import {
+ createIntervals,
+ getUnitAndInterval,
+ processAccounts, // You would need to implement this to handle account-specific data
+} from '../utils/utils';
+
+const schema = z.object({
+ timeFilter: z.string().optional().nullable(),
+});
+
+interface AccountParams {
+ timeFilter?: string;
+}
+
+interface AccountNode {
+ __typename: 'Account';
+ timestamp: string;
+}
+
+// Common function to process account statistics
+async function fetchAccountStatistics(
+ params: AccountParams,
+ fieldName:
+ | 'newAccountStatistics'
+ | 'cumulativeAccountCreationStatistics'
+ | 'dailyActiveAccounts',
+ unit: 'minute' | 'hour' | 'day' | 'month',
+ intervalSize: number,
+ isCumulative = false,
+) {
+ const data = await (fieldName === 'dailyActiveAccounts'
+ ? sdk.dailyActiveAccounts(params)
+ : isCumulative
+ ? sdk.cumulativeAccountCreationStatistics(params)
+ : sdk.newAccountStatistics(params));
+
+ const { nodes, offset } = extractAccountData(data, fieldName);
+ if (!nodes.length) {
+ return isCumulative ? { accounts: [], offset: 0 } : { accounts: [] };
+ }
+
+ const firstTimestamp = Number(nodes[0].timestamp);
+ const lastTimestamp = Number(nodes[nodes.length - 1].timestamp);
+
+ const intervalMap = createIntervals(
+ firstTimestamp,
+ lastTimestamp,
+ unit,
+ intervalSize,
+ );
+ const accounts = processAccounts(nodes, intervalMap);
+
+ if (isCumulative) {
+ return { accounts, offset };
+ }
+
+ return accounts;
+}
+
+// Helper to extract nodes and timestamps from the response
+function extractAccountData(
+ data: any,
+ fieldName: string,
+): { nodes: AccountNode[]; offset?: number } {
+ const nodes = data.data[fieldName]?.nodes || [];
+ const offset = data.data[fieldName]?.accountOffset;
+ return { nodes, offset };
+}
+
+async function getDailyActiveAccountStats(
+ params: AccountParams,
+ unit: 'minute' | 'hour' | 'day' | 'month',
+ intervalSize: number,
+) {
+ return fetchAccountStatistics(
+ params,
+ 'dailyActiveAccounts', // Specify the new field for daily active accounts
+ unit,
+ intervalSize,
+ );
+}
+
+async function getCumulativeAccountCreationStats(
+ params: AccountParams,
+ unit: 'minute' | 'hour' | 'day' | 'month',
+ intervalSize: number,
+) {
+ return fetchAccountStatistics(
+ params,
+ 'cumulativeAccountCreationStatistics',
+ unit,
+ intervalSize,
+ true,
+ );
+}
+
+async function getNewAccountCreationStats(
+ params: AccountParams,
+ unit: 'minute' | 'hour' | 'day' | 'month',
+ intervalSize: number,
+) {
+ return fetchAccountStatistics(
+ params,
+ 'newAccountStatistics',
+ unit,
+ intervalSize,
+ );
+}
+
+export const getDailyActiveAccountStatsAction = act(
+ schema,
+ async ({ timeFilter }) => {
+ const params = { timeFilter: timeFilter } as { timeFilter?: string };
+ const { unit, intervalSize } = getUnitAndInterval(params.timeFilter || '');
+
+ return getDailyActiveAccountStats(params, unit, intervalSize);
+ },
+);
+
+export const getNewAccountStats = act(schema, async ({ timeFilter }) => {
+ const params = { timeFilter: timeFilter } as { timeFilter?: string };
+ const { unit, intervalSize } = getUnitAndInterval(params.timeFilter || '');
+
+ return getNewAccountCreationStats(params, unit, intervalSize);
+});
+
+export const getCumulativeAccountStats = act(schema, async ({ timeFilter }) => {
+ const params = { timeFilter: timeFilter } as { timeFilter?: string };
+ const { unit, intervalSize } = getUnitAndInterval(params.timeFilter || '');
+
+ return getCumulativeAccountCreationStats(params, unit, intervalSize);
+});
diff --git a/packages/app-explorer/src/systems/Statistics/getBlocks.ts b/packages/app-explorer/src/systems/Statistics/getBlocks.ts
new file mode 100644
index 000000000..1eba670f1
--- /dev/null
+++ b/packages/app-explorer/src/systems/Statistics/getBlocks.ts
@@ -0,0 +1,62 @@
+'use server';
+
+import { z } from 'zod';
+import { act } from '../../systems/Core/utils/act-server';
+import { sdk } from '../../systems/Core/utils/sdk';
+import { DateHelper } from '../utils/date';
+import { createIntervals, getUnitAndInterval } from '../utils/utils';
+
+const schema = z.object({
+ timeFilter: z.string().optional().nullable(),
+});
+
+export const getBlockStats = act(schema, async ({ timeFilter }) => {
+ const params = { timeFilter: timeFilter } as {
+ timeFilter?: string;
+ };
+ const data = await sdk.blockRewardStatistics(params);
+
+ if (!data.data.blockRewardStatistics.nodes) {
+ return {};
+ }
+ const { unit, intervalSize } = getUnitAndInterval(params.timeFilter || '');
+ const nodes = data.data.blockRewardStatistics.nodes;
+ const firstTimestamp = Number(DateHelper.tai64toDate(nodes[0].timestamp));
+ const lastTimestamp = Number(
+ DateHelper.tai64toDate(nodes[nodes.length - 1].timestamp),
+ );
+
+ const intervals = createIntervals(
+ firstTimestamp,
+ lastTimestamp,
+ unit,
+ intervalSize,
+ );
+
+ const intervalMap = intervals.map((interval) => ({
+ start: interval.start.toISOString(),
+ end: interval.end.toISOString(),
+ count: 0,
+ totalRewards: 0,
+ }));
+
+ // Process blocks and put them into the correct interval
+ nodes.forEach((block) => {
+ const blockTimestamp = Number(DateHelper.tai64toDate(block.timestamp));
+ const blockReward = Number(block.reward);
+
+ // Find the correct interval for the current block
+ for (const interval of intervalMap) {
+ const intervalStart = new Date(interval.start).getTime();
+ const intervalEnd = new Date(interval.end).getTime();
+
+ if (blockTimestamp >= intervalStart && blockTimestamp < intervalEnd) {
+ // Increment count and add the reward to totalRewards
+ interval.count += 1;
+ interval.totalRewards += blockReward;
+ break; // Block has been assigned to the correct interval, no need to check further
+ }
+ }
+ });
+ return intervalMap;
+});
diff --git a/packages/app-explorer/src/systems/Statistics/getExportData.ts b/packages/app-explorer/src/systems/Statistics/getExportData.ts
new file mode 100644
index 000000000..d64ff58f7
--- /dev/null
+++ b/packages/app-explorer/src/systems/Statistics/getExportData.ts
@@ -0,0 +1,40 @@
+'use server';
+
+import { z } from 'zod';
+import { act } from '../../systems/Core/utils/act-server';
+import { sdk } from '../../systems/Core/utils/sdk';
+
+const schema = z.object({
+ account: z.string().optional().nullable(),
+ startDate: z.string(),
+ endDate: z.string(),
+});
+
+export const getTransactionStats = act(
+ schema,
+ async ({ account, startDate, endDate }) => {
+ if (!account) {
+ throw new Error('Account is required');
+ }
+
+ // Prepare parameters for the GraphQL query
+ const params = {
+ account,
+ startDate,
+ endDate,
+ };
+
+ // Call the getTransactionsByAccountAndDate function from the sdk
+ const data = await sdk.getTransactionsByAccountAndDate(params);
+
+ // Check if any transactions are available
+ if (!data.data.transactionsByAccountAndDate) {
+ return [];
+ }
+
+ // Destructure transactions and return the raw data
+ const transactions = data.data.transactionsByAccountAndDate;
+
+ return transactions;
+ },
+);
diff --git a/packages/app-explorer/src/systems/Statistics/getTopAccounts.ts b/packages/app-explorer/src/systems/Statistics/getTopAccounts.ts
new file mode 100644
index 000000000..11393c06e
--- /dev/null
+++ b/packages/app-explorer/src/systems/Statistics/getTopAccounts.ts
@@ -0,0 +1,75 @@
+'use server';
+
+import { z } from 'zod';
+import { act } from '../../systems/Core/utils/act-server';
+import { sdk } from '../../systems/Core/utils/sdk';
+
+// Schema to validate inputs
+const schema = z.object({
+ sortBy: z.string().optional(), // Sorting criteria (balance, transaction_count, etc.)
+ sortOrder: z.string().optional(), // asc or desc
+ first: z.number().optional().nullable(), // Number of accounts to fetch, can be null
+ cursor: z.string().optional().nullable(), // Pagination cursor
+});
+
+// Common function to fetch top accounts
+async function fetchTopAccounts(
+ sortBy = 'transaction_count', // Default to transaction_count
+ sortOrder: 'asc' | 'desc' = 'desc', // Default to descending order
+ first: number | null = null, // Allow null to fetch all records if no limit is provided
+ cursor?: string | null, // Cursor for pagination (optional)
+) {
+ const queryParams: Record = {
+ sortBy,
+ sortOrder,
+ cursor,
+ };
+
+ // If `first` is provided and not null, add it to the query parameters
+ if (first !== null) {
+ queryParams.first = first;
+ }
+
+ const data = await sdk.paginatedAccounts(queryParams);
+
+ // Handle case where no data is returned
+ if (!data.data.paginatedAccounts.nodes.length) {
+ return {
+ accounts: [],
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor: null,
+ },
+ };
+ }
+
+ const { nodes, pageInfo } = data.data.paginatedAccounts;
+
+ const accounts = nodes.map((account: any) => ({
+ id: account.id,
+ account_id: account.account_id,
+ balance: account.balance,
+ transaction_count: account.transaction_count,
+ }));
+
+ return {
+ accounts,
+ pageInfo: {
+ hasNextPage: pageInfo.hasNextPage,
+ hasPreviousPage: pageInfo.hasPreviousPage,
+ startCursor: pageInfo.startCursor,
+ endCursor: pageInfo.endCursor,
+ },
+ };
+}
+
+export const getTopAccounts = act(schema, async (params) => {
+ const sortBy = params.sortBy || 'transaction_count';
+ const sortOrder = (params.sortOrder || 'desc') as 'asc' | 'desc';
+ const first = params.first === null ? null : params.first || 10;
+ const cursor = params.cursor || null;
+
+ return fetchTopAccounts(sortBy, sortOrder, first, cursor);
+});
diff --git a/packages/app-explorer/src/systems/Statistics/getTransactions.ts b/packages/app-explorer/src/systems/Statistics/getTransactions.ts
new file mode 100644
index 000000000..9b5657346
--- /dev/null
+++ b/packages/app-explorer/src/systems/Statistics/getTransactions.ts
@@ -0,0 +1,131 @@
+'use server';
+
+import { z } from 'zod';
+import { act } from '../../systems/Core/utils/act-server';
+import { sdk } from '../../systems/Core/utils/sdk';
+import { DateHelper } from '../utils/date';
+import {
+ createIntervals,
+ getUnitAndInterval,
+ processTransactions,
+} from '../utils/utils';
+
+const schema = z.object({
+ timeFilter: z.string().optional().nullable(),
+});
+
+interface TransactionParams {
+ timeFilter?: string;
+}
+
+interface TransactionNode {
+ __typename: 'TransactionFee';
+ fee: string;
+ timestamp: string;
+}
+
+// Common function to process transaction statistics
+async function fetchTransactionStatistics(
+ params: TransactionParams,
+ fieldName:
+ | 'transactionsFeeStatistics'
+ | 'cumulativeTransactionsFeeStatistics',
+ unit: 'minute' | 'hour' | 'day' | 'month',
+ intervalSize: number,
+ isCumulative = false,
+) {
+ const data = await (isCumulative
+ ? sdk.cumulativeTransactionsFeeStatistics(params)
+ : sdk.transactionsFeeStatistics(params));
+
+ const { nodes, offset } = extractTransactionData(data, fieldName);
+
+ if (!nodes.length) {
+ return isCumulative
+ ? { transactions: [], offset: 0 }
+ : { transactions: [] };
+ }
+
+ const firstTimestamp = Number(DateHelper.tai64toDate(nodes[0].timestamp));
+ const lastTimestamp = Number(
+ DateHelper.tai64toDate(nodes[nodes.length - 1].timestamp),
+ );
+
+ const intervalMap = createIntervals(
+ firstTimestamp,
+ lastTimestamp,
+ unit,
+ intervalSize,
+ );
+ const transactions = processTransactions(nodes, intervalMap);
+
+ if (isCumulative) {
+ return { transactions, offset };
+ }
+
+ return transactions;
+}
+
+// Helper to extract nodes and timestamps from the response
+function extractTransactionData(
+ data: any,
+ fieldName: string,
+): { nodes: TransactionNode[]; offset?: number } {
+ const nodes = data.data[fieldName]?.nodes || [];
+ const offset = data.data[fieldName]?.transactionOffset;
+ return { nodes, offset };
+}
+
+async function getCumulativeTransactionFeeStats(
+ params: TransactionParams,
+ unit: 'minute' | 'hour' | 'day' | 'month',
+ intervalSize: number,
+) {
+ return fetchTransactionStatistics(
+ params,
+ 'cumulativeTransactionsFeeStatistics',
+ unit,
+ intervalSize,
+ true,
+ );
+}
+
+export async function getTransactionFeeStats(
+ params: TransactionParams,
+ unit: 'minute' | 'hour' | 'day' | 'month',
+ intervalSize: number,
+) {
+ return fetchTransactionStatistics(
+ params,
+ 'transactionsFeeStatistics',
+ unit,
+ intervalSize,
+ );
+}
+
+export const getTransactionStats = act(schema, async ({ timeFilter }) => {
+ const params = { timeFilter: timeFilter } as { timeFilter?: string };
+ const { unit, intervalSize } = getUnitAndInterval(params.timeFilter || '');
+
+ return getTransactionFeeStats(params, unit, intervalSize);
+});
+
+export const getDailyTransactionFeeStats = act(
+ schema,
+ async ({ timeFilter }) => {
+ const params = { timeFilter: timeFilter } as { timeFilter?: string };
+
+ // Use 'day' as the unit and 1 as the interval size
+ return getTransactionFeeStats(params, 'day', 1);
+ },
+);
+
+export const getCumulativeTransactionStats = act(
+ schema,
+ async ({ timeFilter }) => {
+ const params = { timeFilter: timeFilter } as { timeFilter?: string };
+ const { unit, intervalSize } = getUnitAndInterval(params.timeFilter || '');
+
+ return getCumulativeTransactionFeeStats(params, unit, intervalSize);
+ },
+);
diff --git a/packages/app-explorer/src/systems/Transactions/components/ModalContainer/ModalContainer.tsx b/packages/app-explorer/src/systems/Transactions/components/ModalContainer/ModalContainer.tsx
new file mode 100644
index 000000000..b53d6acb0
--- /dev/null
+++ b/packages/app-explorer/src/systems/Transactions/components/ModalContainer/ModalContainer.tsx
@@ -0,0 +1,28 @@
+'use client';
+import React from 'react';
+import Modal from 'react-modal';
+// Bind modal to your appElement (for accessibility)
+
+const ModalContainer = ({
+ isOpen,
+ onRequestClose,
+ children,
+}: {
+ isOpen: boolean;
+ onRequestClose: () => void;
+ children: React.ReactNode;
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default ModalContainer;
diff --git a/packages/app-explorer/src/systems/Transactions/components/NFTHashItem/NFTHashItem.tsx b/packages/app-explorer/src/systems/Transactions/components/NFTHashItem/NFTHashItem.tsx
new file mode 100644
index 000000000..d2cc486b9
--- /dev/null
+++ b/packages/app-explorer/src/systems/Transactions/components/NFTHashItem/NFTHashItem.tsx
@@ -0,0 +1,22 @@
+import { Box, Copyable, HStack, VStack } from '@fuels/ui';
+
+type NFTHashItemProps = {
+ hashAddress: string;
+ width: string;
+};
+
+export default function NFTHashItem({ hashAddress, width }: NFTHashItemProps) {
+ return (
+
+
+
+
+
+ {hashAddress}
+
+
+
+
+
+ );
+}
diff --git a/packages/app-explorer/src/systems/Transactions/components/NFTProfileHolderIcon/NFTProfileHolderIcon.tsx b/packages/app-explorer/src/systems/Transactions/components/NFTProfileHolderIcon/NFTProfileHolderIcon.tsx
new file mode 100644
index 000000000..970cfd2ed
--- /dev/null
+++ b/packages/app-explorer/src/systems/Transactions/components/NFTProfileHolderIcon/NFTProfileHolderIcon.tsx
@@ -0,0 +1,14 @@
+import { HStack, Text } from '@fuels/ui';
+
+export default function NFTProfileHolderIcon() {
+ return (
+
+
+ 0x52....13d1
+
+ );
+}
diff --git a/packages/app-explorer/src/systems/Transactions/components/NFTProfileIcon/NFTProfileIcon.tsx b/packages/app-explorer/src/systems/Transactions/components/NFTProfileIcon/NFTProfileIcon.tsx
new file mode 100644
index 000000000..33ff43bc8
--- /dev/null
+++ b/packages/app-explorer/src/systems/Transactions/components/NFTProfileIcon/NFTProfileIcon.tsx
@@ -0,0 +1,14 @@
+import { HStack, Text } from '@fuels/ui';
+
+export default function NFTProfileIcon() {
+ return (
+
+
+ DeGod #5990
+
+ );
+}
diff --git a/packages/app-explorer/src/systems/Transactions/components/NFTitem/NFTitem.tsx b/packages/app-explorer/src/systems/Transactions/components/NFTitem/NFTitem.tsx
new file mode 100644
index 000000000..c071922ce
--- /dev/null
+++ b/packages/app-explorer/src/systems/Transactions/components/NFTitem/NFTitem.tsx
@@ -0,0 +1,19 @@
+import { Box, Copyable, HStack, Text, VStack } from '@fuels/ui';
+
+export default function NFTItem() {
+ return (
+
+
+
+
+ #275029958
+
+
+
+ 0.004878700 ETH
+
+ );
+}
diff --git a/packages/app-explorer/src/systems/Transactions/components/NFTsHeader/NFTsHeader.tsx b/packages/app-explorer/src/systems/Transactions/components/NFTsHeader/NFTsHeader.tsx
new file mode 100644
index 000000000..e445bfc70
--- /dev/null
+++ b/packages/app-explorer/src/systems/Transactions/components/NFTsHeader/NFTsHeader.tsx
@@ -0,0 +1,28 @@
+import { RoundedContainer } from '@fuels/ui';
+import React from 'react';
+
+interface LineGraphProps {
+ titleProp: string;
+ valuesProp: string;
+ timeProp: string;
+}
+
+export const NFTsHeader: React.FC = ({
+ titleProp,
+ valuesProp,
+ timeProp,
+}) => {
+ return (
+
+
+ {titleProp}
+
+
+ {valuesProp}
+
+
+ {timeProp}
+
+
+ );
+};
diff --git a/packages/app-explorer/src/systems/Transactions/components/TxList/TxListLoader.tsx b/packages/app-explorer/src/systems/Transactions/components/TxList/TxListLoader.tsx
index 33b5d798f..4fd5a8ea6 100644
--- a/packages/app-explorer/src/systems/Transactions/components/TxList/TxListLoader.tsx
+++ b/packages/app-explorer/src/systems/Transactions/components/TxList/TxListLoader.tsx
@@ -1,3 +1,24 @@
+// import type { GQLRecentTransactionsQuery } from '@fuel-explorer/graphql';
+// import { TxList } from './TxList';
+
+// type TxListLoaderProps = {
+// numberOfTxs?: number;
+// page: string;
+// };
+
+// export const TxListLoader = ({ numberOfTxs = 4, page }: TxListLoaderProps) => {
+// console.log(page);
+// const baseArray = Array.from(
+// { length: numberOfTxs },
+// (_, index) => index + 1,
+// );
+// const txs = baseArray.map((v) => ({
+// id: `${v}`,
+// })) as GQLRecentTransactionsQuery['transactions']['nodes'];
+
+// return ;
+// };
+
import type { GQLRecentTransactionsQuery } from '@fuel-explorer/graphql';
import { TxList } from './TxList';
diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsAccontTable/TxsAccountTable.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsAccontTable/TxsAccountTable.tsx
new file mode 100644
index 000000000..0987fa0e0
--- /dev/null
+++ b/packages/app-explorer/src/systems/Transactions/components/TxsAccontTable/TxsAccountTable.tsx
@@ -0,0 +1,145 @@
+import { GridTable } from '@fuels/ui';
+import { useState } from 'react';
+import NFTHashItem from '../NFTHashItem/NFTHashItem';
+import NFTProfileHolderIcon from '../NFTProfileHolderIcon/NFTProfileHolderIcon';
+import NFTItem from '../NFTitem/NFTitem';
+
+export interface RowData {
+ id: number;
+ name: string;
+ age: number;
+ email: string;
+ status: string;
+}
+
+export const data: RowData[] = [
+ {
+ id: 1,
+ name: 'John Doe',
+ age: 28,
+ email: 'john.doe@example.com',
+ status: 'Active',
+ },
+ {
+ id: 2,
+ name: 'Jane Smith',
+ age: 34,
+ email: 'jane.smith@example.com',
+ status: 'Inactive',
+ },
+ {
+ id: 3,
+ name: 'Michael Johnson',
+ age: 45,
+ email: 'michael.johnson@example.com',
+ status: 'Active',
+ },
+ {
+ id: 4,
+ name: 'Emily Davis',
+ age: 23,
+ email: 'emily.davis@example.com',
+ status: 'Pending',
+ },
+ {
+ id: 5,
+ name: 'William Brown',
+ age: 39,
+ email: 'william.brown@example.com',
+ status: 'Active',
+ },
+ {
+ id: 6,
+ name: 'Olivia Taylor',
+ age: 29,
+ email: 'olivia.taylor@example.com',
+ status: 'Inactive',
+ },
+ {
+ id: 7,
+ name: 'James Anderson',
+ age: 32,
+ email: 'james.anderson@example.com',
+ status: 'Pending',
+ },
+ {
+ id: 8,
+ name: 'Sophia Thomas',
+ age: 27,
+ email: 'sophia.thomas@example.com',
+ status: 'Active',
+ },
+ {
+ id: 9,
+ name: 'Isabella Lee',
+ age: 31,
+ email: 'isabella.lee@example.com',
+ status: 'Inactive',
+ },
+ {
+ id: 10,
+ name: 'David Martinez',
+ age: 36,
+ email: 'david.martinez@example.com',
+ status: 'Active',
+ },
+];
+
+export const columns = [
+ {
+ name: 'Rank',
+ cell: () => 1
,
+ sortable: true,
+ },
+ {
+ name: 'Account Adderess',
+ cell: () => (
+
+
+
+ ),
+ sortable: true,
+ },
+ {
+ name: 'Balance',
+ cell: () => (
+
+
+
+ ),
+ sortable: true,
+ },
+ {
+ name: 'Percentage',
+ cell: () => 22.00%
,
+ sortable: false,
+ },
+ {
+ name: 'Tx Count',
+ cell: () => (
+
+
+
+ ),
+ sortable: true,
+ },
+];
+
+export function TxsAccountTable() {
+ const [currentGridPage, setCurrentGridPage] = useState(0);
+ return (
+
+ {}}
+ pageCount={2}
+ currentPage={currentGridPage}
+ setCurrentPage={setCurrentGridPage}
+ />
+
+ );
+}
diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsButton/TxsButton.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsButton/TxsButton.tsx
new file mode 100644
index 000000000..4fdc34a5f
--- /dev/null
+++ b/packages/app-explorer/src/systems/Transactions/components/TxsButton/TxsButton.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+interface TxsButtonProps {
+ children: React.ReactNode; // Use children instead of buttonName
+ type?: 'button' | 'submit' | 'reset';
+}
+
+function TxsButton({ children }: TxsButtonProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default TxsButton;
diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsButtonContainer/TxsButtonContainer.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsButtonContainer/TxsButtonContainer.tsx
new file mode 100644
index 000000000..301362057
--- /dev/null
+++ b/packages/app-explorer/src/systems/Transactions/components/TxsButtonContainer/TxsButtonContainer.tsx
@@ -0,0 +1,65 @@
+import React, { useState } from 'react';
+import ModalContainer from '../ModalContainer/ModalContainer';
+import TxsButton from '../TxsButton/TxsButton';
+import TxsExportCsvModal from '../TxsExportCsvModal/TxsExportCsvModal';
+
+function TxsButtonContainer() {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ const openModal = () => setIsModalOpen(true);
+ const closeModal = () => setIsModalOpen(false);
+
+ return (
+
+
+ Download Page Data
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default TxsButtonContainer;
diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsExportCsvModal/TxsExportCsvModal.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsExportCsvModal/TxsExportCsvModal.tsx
new file mode 100644
index 000000000..bb810ed67
--- /dev/null
+++ b/packages/app-explorer/src/systems/Transactions/components/TxsExportCsvModal/TxsExportCsvModal.tsx
@@ -0,0 +1,139 @@
+'use client';
+import { Select } from '@fuels/ui';
+import React, { useState } from 'react';
+
+type TxsExportCsvModalProps = {
+ closeModal: () => void;
+};
+
+const TxsExportCsvModal: React.FC = ({
+ closeModal,
+}) => {
+ const [_transactionType, _setTransactionType] =
+ useState('Transactions');
+ const [address, setAddress] = useState('');
+ const [downloadOption, setDownloadOption] = useState<'date' | 'dataBlock'>(
+ 'date',
+ );
+ const [startDate, setStartDate] = useState('');
+ const [endDate, setEndDate] = useState('');
+
+ const handleExportData = (): void => {};
+
+ return (
+
+
+ {/*
+
+
+
*/}
+
+
+
+
+ setAddress(e.target.value)}
+ />
+
+
+
+
+
+ The earliest 5,000 records within the selected range will be exported
+
+
+ );
+};
+
+export default TxsExportCsvModal;
diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsTab/TsxTab.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsTab/TsxTab.tsx
new file mode 100644
index 000000000..cf1229b24
--- /dev/null
+++ b/packages/app-explorer/src/systems/Transactions/components/TxsTab/TsxTab.tsx
@@ -0,0 +1,30 @@
+'use client';
+import React from 'react';
+
+interface TxsTabProps {
+ tabTitle: string;
+ isActive: boolean;
+ onClick: () => void; // onClick prop
+}
+
+export function TxsTab({ tabTitle, isActive, onClick }: TxsTabProps) {
+ // Handle key events
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ // Allow activation with Enter or Space
+ onClick();
+ }
+ };
+
+ return (
+
+ {tabTitle}
+
+ );
+}
diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsTabList/TxsTabList.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsTabList/TxsTabList.tsx
new file mode 100644
index 000000000..69c223cf5
--- /dev/null
+++ b/packages/app-explorer/src/systems/Transactions/components/TxsTabList/TxsTabList.tsx
@@ -0,0 +1,34 @@
+'use client';
+import { TxsTab } from '../TxsTab/TsxTab';
+
+export type TxsTabListProps = {
+ activeTab: string;
+ setActiveTab: (tabName: string) => void;
+};
+
+export function TxsTabList({ activeTab, setActiveTab }: TxsTabListProps) {
+ // Default active tab
+
+ const tabList = [
+ { name: 'Top Tokens' },
+ { name: 'Top NFTs' },
+ { name: 'Top Accounts' },
+ ];
+
+ const handleTabClick = (tabName: string) => {
+ setActiveTab(tabName);
+ };
+
+ return (
+
+ {tabList.map((tab) => (
+ handleTabClick(tab.name)}
+ />
+ ))}
+
+ );
+}
diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsTitle/TxsTitle.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsTitle/TxsTitle.tsx
index eaefa40c2..c5e65e2ab 100644
--- a/packages/app-explorer/src/systems/Transactions/components/TxsTitle/TxsTitle.tsx
+++ b/packages/app-explorer/src/systems/Transactions/components/TxsTitle/TxsTitle.tsx
@@ -1,4 +1,31 @@
-'use client';
+// 'use client';
+// import { PageTitle } from 'app-commons';
+// import TxsButtonContainer from '../TxsButtonContainer/TxsButtonContainer';
+// import { TxsTabList } from '../TxsTabList/TxsTabList';
+// // import TxsExportCsvModal from '../TxsExportCsvModal/TxsExportCsvModal';
+
+// export type TxsTabListProps = {
+// activeTab: string;
+// setActiveTab: (tabName: string) => void;
+// };
+
+// export function TxsTitle({ activeTab='Recent Transactions', setActiveTab=()=>void }: TxsTabListProps) {
+// return (
+// <>
+// }
+// >
+// Top Tokens
+//
+//
+//
+// {/* */}
+// >
+// );
+// }
+
import { PageTitle } from 'app-commons';
export function TxsTitle() {
diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsTokenTable/TxsTokenTable.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsTokenTable/TxsTokenTable.tsx
new file mode 100644
index 000000000..eedcc8aac
--- /dev/null
+++ b/packages/app-explorer/src/systems/Transactions/components/TxsTokenTable/TxsTokenTable.tsx
@@ -0,0 +1,157 @@
+import { GridTable } from '@fuels/ui';
+import { useState } from 'react';
+import NFTHashItem from '../NFTHashItem/NFTHashItem';
+import NFTProfileHolderIcon from '../NFTProfileHolderIcon/NFTProfileHolderIcon';
+import NFTItem from '../NFTitem/NFTitem';
+
+export interface RowData {
+ id: number;
+ name: string;
+ age: number;
+ email: string;
+ status: string;
+}
+
+export const data: RowData[] = [
+ {
+ id: 1,
+ name: 'John Doe',
+ age: 28,
+ email: 'john.doe@example.com',
+ status: 'Active',
+ },
+ {
+ id: 2,
+ name: 'Jane Smith',
+ age: 34,
+ email: 'jane.smith@example.com',
+ status: 'Inactive',
+ },
+ {
+ id: 3,
+ name: 'Michael Johnson',
+ age: 45,
+ email: 'michael.johnson@example.com',
+ status: 'Active',
+ },
+ {
+ id: 4,
+ name: 'Emily Davis',
+ age: 23,
+ email: 'emily.davis@example.com',
+ status: 'Pending',
+ },
+ {
+ id: 5,
+ name: 'William Brown',
+ age: 39,
+ email: 'william.brown@example.com',
+ status: 'Active',
+ },
+ {
+ id: 6,
+ name: 'Olivia Taylor',
+ age: 29,
+ email: 'olivia.taylor@example.com',
+ status: 'Inactive',
+ },
+ {
+ id: 7,
+ name: 'James Anderson',
+ age: 32,
+ email: 'james.anderson@example.com',
+ status: 'Pending',
+ },
+ {
+ id: 8,
+ name: 'Sophia Thomas',
+ age: 27,
+ email: 'sophia.thomas@example.com',
+ status: 'Active',
+ },
+ {
+ id: 9,
+ name: 'Isabella Lee',
+ age: 31,
+ email: 'isabella.lee@example.com',
+ status: 'Inactive',
+ },
+ {
+ id: 10,
+ name: 'David Martinez',
+ age: 36,
+ email: 'david.martinez@example.com',
+ status: 'Active',
+ },
+];
+
+export const columns = [
+ {
+ name: 'Rank',
+ cell: () => 1
,
+ sortable: true,
+ },
+ {
+ name: 'NFT Collection',
+ cell: () => (
+
+
+
+ ),
+ sortable: true,
+ },
+ {
+ name: 'Mint Adderess',
+ cell: () => (
+
+
+
+ ),
+ sortable: true,
+ },
+ {
+ name: 'Floor Price',
+ cell: () => 22.00%
,
+ sortable: false,
+ },
+ {
+ name: 'Total Supply',
+ cell: () => (
+
+
+
+ ),
+ sortable: true,
+ },
+ {
+ name: 'Holders',
+ cell: () => (
+
+
+
+ ),
+ sortable: true,
+ },
+];
+
+export function TxsTokenTable() {
+ const [currentGridPage, setCurrentGridPage] = useState(0);
+ return (
+
+ {}}
+ pageCount={2}
+ currentPage={currentGridPage}
+ setCurrentPage={setCurrentGridPage}
+ />
+
+ );
+}
diff --git a/packages/app-explorer/src/systems/utils/date.ts b/packages/app-explorer/src/systems/utils/date.ts
new file mode 100644
index 000000000..9995818df
--- /dev/null
+++ b/packages/app-explorer/src/systems/utils/date.ts
@@ -0,0 +1,16 @@
+import dayjs from 'dayjs';
+import relativeTime from 'dayjs/plugin/relativeTime';
+import { TAI64 } from 'tai64';
+
+dayjs.extend(relativeTime);
+
+export class DateHelper {
+ static tai64toDate(tai64Timestamp: string) {
+ const timestamp = TAI64.fromString(tai64Timestamp, 10).toUnix();
+ return dayjs(timestamp * 1000);
+ }
+
+ static dateToTai64(date: Date) {
+ return TAI64.fromUnix(Math.floor(date.getTime() / 1000)).toString(10);
+ }
+}
diff --git a/packages/app-explorer/src/systems/utils/utils.ts b/packages/app-explorer/src/systems/utils/utils.ts
new file mode 100644
index 000000000..f8e579de3
--- /dev/null
+++ b/packages/app-explorer/src/systems/utils/utils.ts
@@ -0,0 +1,204 @@
+import { DateHelper } from './date';
+
+interface TransactionNode {
+ __typename: 'TransactionFee';
+ fee: string;
+ timestamp: string;
+}
+
+interface AccountNode {
+ timestamp: string;
+ count?: number;
+}
+
+interface Interval {
+ start: Date;
+ end: Date;
+ count: number; // To track the number of transactions
+ totalFee: number; // To track the total transaction fees
+}
+
+export function getUnitAndInterval(timeRange: string): {
+ unit: 'minute' | 'hour' | 'day' | 'month';
+ intervalSize: number;
+} {
+ switch (timeRange) {
+ case '1hr':
+ return { unit: 'minute', intervalSize: 5 };
+ case '12hr':
+ return { unit: 'hour', intervalSize: 1 };
+ case '1day':
+ return { unit: 'hour', intervalSize: 2 };
+ case '7days':
+ return { unit: 'hour', intervalSize: 12 };
+ case '14days':
+ return { unit: 'day', intervalSize: 1 };
+ case '30days':
+ return { unit: 'day', intervalSize: 3 };
+ case '90days':
+ return { unit: 'day', intervalSize: 10 };
+ default:
+ return { unit: 'month', intervalSize: 1 };
+ }
+}
+
+function roundToNearest(
+ time: number,
+ unit: 'minute' | 'hour' | 'day' | 'month',
+ roundUp = false,
+): number {
+ const date = new Date(time);
+
+ switch (unit) {
+ case 'minute': {
+ const msInMinute = 60 * 1000;
+ const msInFiveMinutes = 5 * msInMinute;
+ return roundUp
+ ? Math.ceil(time / msInFiveMinutes) * msInFiveMinutes
+ : Math.floor(time / msInFiveMinutes) * msInFiveMinutes;
+ }
+
+ case 'hour': {
+ const msInHour = 60 * 60 * 1000;
+ return roundUp
+ ? Math.ceil(time / msInHour) * msInHour
+ : Math.floor(time / msInHour) * msInHour;
+ }
+
+ case 'day':
+ if (roundUp) {
+ date.setUTCHours(0, 0, 0, 0);
+ return date.getTime() + 24 * 60 * 60 * 1000; // Add one day
+ }
+ date.setUTCHours(0, 0, 0, 0); // Set to midnight
+ return date.getTime();
+
+ case 'month':
+ if (roundUp) {
+ if (date.getUTCMonth() === 11) {
+ // If December, increment year
+ date.setUTCFullYear(date.getUTCFullYear() + 1);
+ date.setUTCMonth(0);
+ } else {
+ date.setUTCMonth(date.getUTCMonth() + 1);
+ }
+ date.setUTCDate(1); // First day of the next month
+ } else {
+ date.setUTCDate(1); // First day of the current month
+ }
+ date.setUTCHours(0, 0, 0, 0); // Set time to midnight
+ return date.getTime();
+ }
+}
+
+// General interval creation function
+export function createIntervals(
+ startTime: number,
+ endTime: number,
+ unit: 'minute' | 'hour' | 'day' | 'month',
+ intervalSize: number,
+): Array<{ start: Date; end: Date; count: number; totalFee: number }> {
+ const roundedStartTime = roundToNearest(startTime, unit);
+ const roundedEndTime = roundToNearest(endTime, unit, true);
+
+ const intervals: Array<{
+ start: Date;
+ end: Date;
+ count: number;
+ totalFee: number;
+ }> = [];
+
+ let currentTime = roundedStartTime;
+
+ if (unit === 'month') {
+ // Handle month-specific interval logic (varying days in a month)
+ const currentDate = new Date(roundedStartTime);
+ while (currentDate.getTime() < roundedEndTime) {
+ const startInterval = new Date(currentDate);
+ currentDate.setUTCMonth(currentDate.getUTCMonth() + intervalSize); // Move by `intervalSize` months
+ const endInterval = new Date(currentDate);
+ intervals.push({
+ start: startInterval,
+ end: endInterval,
+ count: 0,
+ totalFee: 0,
+ });
+ }
+ } else {
+ // Handle minute, hour, and day intervals
+ const msInUnit = {
+ minute: 60 * 1000,
+ hour: 60 * 60 * 1000,
+ day: 24 * 60 * 60 * 1000,
+ };
+
+ const intervalDuration = intervalSize * msInUnit[unit];
+
+ while (currentTime < roundedEndTime) {
+ const startInterval = new Date(currentTime);
+ const endInterval = new Date(currentTime + intervalDuration);
+ intervals.push({
+ start: startInterval,
+ end: endInterval,
+ count: 0,
+ totalFee: 0,
+ });
+ currentTime += intervalDuration;
+ }
+ }
+
+ return intervals;
+}
+
+// Helper to process transactions and map to intervals
+export function processTransactions(
+ nodes: TransactionNode[],
+ intervalMap: Interval[],
+) {
+ nodes.forEach((transaction) => {
+ const transactionTimestamp = Number(
+ DateHelper.tai64toDate(transaction.timestamp),
+ );
+ const transactionFee = Number(transaction.fee);
+
+ // Find the correct interval for the current transaction
+ for (const interval of intervalMap) {
+ const intervalStart = new Date(interval.start).getTime();
+ const intervalEnd = new Date(interval.end).getTime();
+
+ if (
+ transactionTimestamp >= intervalStart &&
+ transactionTimestamp < intervalEnd
+ ) {
+ // Increment count and add the transaction fee to totalFee
+ interval.count += 1;
+ interval.totalFee += transactionFee;
+ break; // Transaction assigned, no need to check further
+ }
+ }
+ });
+ return intervalMap;
+}
+
+export function processAccounts(nodes: AccountNode[], intervalMap: Interval[]) {
+ nodes.forEach((account) => {
+ const accountTimestamp = Number(account.timestamp);
+
+ // Find the correct interval for the current account
+ for (const interval of intervalMap) {
+ const intervalStart = new Date(interval.start).getTime();
+ const intervalEnd = new Date(interval.end).getTime();
+
+ if (accountTimestamp >= intervalStart && accountTimestamp < intervalEnd) {
+ // Increment count for the number of accounts created
+ if (account.count) {
+ interval.count += account.count;
+ } else {
+ interval.count += 1;
+ }
+ break; // Account assigned, no need to check further
+ }
+ }
+ });
+ return intervalMap;
+}
diff --git a/packages/app-portal/src/systems/Chains/eth/hooks/useAddAssetForm.ts b/packages/app-portal/src/systems/Chains/eth/hooks/useAddAssetForm.ts
index d30fe0ec3..ee789ca53 100644
--- a/packages/app-portal/src/systems/Chains/eth/hooks/useAddAssetForm.ts
+++ b/packages/app-portal/src/systems/Chains/eth/hooks/useAddAssetForm.ts
@@ -17,7 +17,7 @@ type UseAddAssetOpts = {
};
export const useAddAssetForm = (opts: UseAddAssetOpts = {}) => {
- const schema = yup.object({
+ const schema: any = yup.object({
symbol: yup.string().required('Symbol is required'),
decimals: yup
.string()
diff --git a/packages/app-portal/src/systems/Chains/eth/hooks/useSetAssetAddressForm.ts b/packages/app-portal/src/systems/Chains/eth/hooks/useSetAssetAddressForm.ts
index f91804ced..a1e0c11a7 100644
--- a/packages/app-portal/src/systems/Chains/eth/hooks/useSetAssetAddressForm.ts
+++ b/packages/app-portal/src/systems/Chains/eth/hooks/useSetAssetAddressForm.ts
@@ -15,7 +15,7 @@ type UseSetAddressOpts = {
};
export const useSetAddressForm = (opts: UseSetAddressOpts = {}) => {
- const schema = yup.object({
+ const schema: any = yup.object({
address: yup.string().required('Address is required'),
});
diff --git a/packages/graphql/.gitignore b/packages/graphql/.gitignore
index e69de29bb..941c2c569 100644
--- a/packages/graphql/.gitignore
+++ b/packages/graphql/.gitignore
@@ -0,0 +1,2 @@
+logs
+check.sh
\ No newline at end of file
diff --git a/packages/graphql/database/1.sql b/packages/graphql/database/1.sql
new file mode 100644
index 000000000..0729e1cfc
--- /dev/null
+++ b/packages/graphql/database/1.sql
@@ -0,0 +1,14 @@
+CREATE TABLE indexer.accounts (
+ _id SERIAL PRIMARY KEY,
+ account_id character varying(66) NOT NULL UNIQUE,
+ balance BIGINT NOT NULL DEFAULT 0,
+ transaction_count INTEGER NOT NULL DEFAULT 0,
+ data jsonb NOT NULL DEFAULT '{}',
+ first_transaction_timestamp timestamp without time zone NOT NULL,
+ recent_transaction_timestamp timestamp without time zone NOT NULL
+);
+CREATE UNIQUE INDEX ON indexer.accounts(_id);
+CREATE UNIQUE INDEX ON indexer.accounts(account_id);
+CREATE INDEX ON indexer.accounts(transaction_count);
+CREATE INDEX ON indexer.accounts(recent_transaction_timestamp);
+CREATE INDEX ON indexer.accounts(first_transaction_timestamp);
\ No newline at end of file
diff --git a/packages/graphql/package.json b/packages/graphql/package.json
index ff304c2bd..d06244ff1 100644
--- a/packages/graphql/package.json
+++ b/packages/graphql/package.json
@@ -14,7 +14,9 @@
},
"typesVersions": {
"*": {
- "sdk": ["./src/graphql/generated/sdk.ts"]
+ "sdk": [
+ "./src/graphql/generated/sdk.ts"
+ ]
}
},
"scripts": {
@@ -70,6 +72,7 @@
"pino": "9.2.0",
"pino-pretty": "11.2.1",
"pm2": "^5.4.1",
+ "recharts": "2.12.7",
"shallow-equal-object": "1.1.1",
"tai64": "^1.0.0",
"winston": "3.13.0",
diff --git a/packages/graphql/src/db/migrations/createBlock.ts b/packages/graphql/src/db/migrations/createBlock.ts
new file mode 100644
index 000000000..b6bf9dd58
--- /dev/null
+++ b/packages/graphql/src/db/migrations/createBlock.ts
@@ -0,0 +1,210 @@
+import { DatabaseConnection } from '../../infra/database/DatabaseConnection';
+
+async function migrate() {
+ const db = DatabaseConnection.getInstance();
+
+ // Step 1: Drop the existing indexer schema if it exists
+ await db.query(
+ `
+ DROP SCHEMA IF EXISTS indexer CASCADE;
+ `,
+ [], // Provide an empty array for query parameters
+ );
+ console.log('Existing indexer schema dropped if it was present.');
+
+ // Step 2: Create the new indexer schema
+ await db.query(
+ `
+ CREATE SCHEMA indexer;
+ `,
+ [],
+ );
+ console.log('New indexer schema created successfully.');
+
+ // Step 3: Create the blocks table
+ await db.query(
+ `
+ CREATE TABLE indexer.blocks (
+ _id INTEGER PRIMARY KEY,
+ id CHARACTER VARYING(66) NOT NULL UNIQUE,
+ timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL,
+ data JSONB NOT NULL,
+ gas_used CHARACTER VARYING(66),
+ producer CHARACTER VARYING(66)
+ );
+ `,
+ [],
+ );
+ console.log('Blocks table created successfully.');
+
+ // Step 4: Create indexes for blocks table
+ await db.query(
+ `
+ CREATE UNIQUE INDEX ON indexer.blocks(_id);
+ CREATE UNIQUE INDEX ON indexer.blocks(id);
+ CREATE INDEX ON indexer.blocks(timestamp);
+ CREATE INDEX ON indexer.blocks(id);
+ CREATE INDEX ON indexer.blocks(_id);
+ `,
+ [],
+ );
+ console.log('Indexes for blocks table created successfully.');
+
+ // Step 5: Create the transactions table
+ await db.query(
+ `
+ CREATE TABLE indexer.transactions (
+ _id CHARACTER VARYING(66) PRIMARY KEY,
+ tx_hash CHARACTER VARYING(66) NOT NULL UNIQUE,
+ timestamp TIMESTAMP WITHOUT TIME ZONE,
+ data JSONB NOT NULL,
+ block_id INTEGER NOT NULL REFERENCES indexer.blocks(_id)
+ );
+ `,
+ [],
+ );
+ console.log('Transactions table created successfully.');
+
+ // Step 6: Create indexes for transactions table
+ await db.query(
+ `
+ CREATE UNIQUE INDEX ON indexer.transactions(_id);
+ CREATE UNIQUE INDEX ON indexer.transactions(tx_hash);
+ CREATE INDEX ON indexer.transactions(timestamp);
+ CREATE INDEX ON indexer.transactions(_id);
+ CREATE INDEX ON indexer.transactions(block_id);
+ CREATE INDEX ON indexer.transactions(tx_hash);
+ `,
+ [],
+ );
+ console.log('Indexes for transactions table created successfully.');
+
+ // Step 7: Create the contracts table
+ await db.query(
+ `
+ CREATE TABLE indexer.contracts (
+ _id SERIAL PRIMARY KEY,
+ contract_hash CHARACTER VARYING(66) NOT NULL UNIQUE,
+ data JSONB NOT NULL
+ );
+ `,
+ [],
+ );
+ console.log('Contracts table created successfully.');
+
+ // Step 8: Create indexes for contracts table
+ await db.query(
+ `
+ CREATE UNIQUE INDEX ON indexer.contracts(_id);
+ CREATE UNIQUE INDEX ON indexer.contracts(contract_hash);
+ CREATE INDEX ON indexer.contracts(_id);
+ CREATE INDEX ON indexer.contracts(contract_hash);
+ `,
+ [],
+ );
+ console.log('Indexes for contracts table created successfully.');
+
+ // Step 9: Create the inputs table
+ await db.query(
+ `
+ CREATE TABLE indexer.inputs (
+ _id SERIAL PRIMARY KEY,
+ data JSONB NOT NULL,
+ transaction_id CHARACTER VARYING(66) NOT NULL REFERENCES indexer.transactions(_id)
+ );
+ `,
+ [],
+ );
+ console.log('Inputs table created successfully.');
+
+ // Step 10: Create indexes for inputs table
+ await db.query(
+ `
+ CREATE UNIQUE INDEX ON indexer.inputs(_id);
+ CREATE INDEX ON indexer.inputs(_id);
+ CREATE INDEX ON indexer.inputs(transaction_id);
+ `,
+ [],
+ );
+ console.log('Indexes for inputs table created successfully.');
+
+ // Step 11: Create the outputs table
+ await db.query(
+ `
+ CREATE TABLE indexer.outputs (
+ _id SERIAL PRIMARY KEY,
+ data JSONB NOT NULL,
+ transaction_id CHARACTER VARYING(66) NOT NULL REFERENCES indexer.transactions(_id)
+ );
+ `,
+ [],
+ );
+ console.log('Outputs table created successfully.');
+
+ // Step 12: Create indexes for outputs table
+ await db.query(
+ `
+ CREATE UNIQUE INDEX ON indexer.outputs(_id);
+ CREATE INDEX ON indexer.outputs(_id);
+ CREATE INDEX ON indexer.outputs(transaction_id);
+ `,
+ [],
+ );
+ console.log('Indexes for outputs table created successfully.');
+
+ // Step 13: Create the predicates table
+ await db.query(
+ `
+ CREATE TABLE indexer.predicates (
+ _id SERIAL PRIMARY KEY,
+ bytecode TEXT NOT NULL,
+ address CHARACTER VARYING(66) NOT NULL UNIQUE
+ );
+ `,
+ [],
+ );
+ console.log('Predicates table created successfully.');
+
+ // Step 14: Create indexes for predicates table
+ await db.query(
+ `
+ CREATE UNIQUE INDEX ON indexer.predicates(_id);
+ CREATE UNIQUE INDEX ON indexer.predicates(address);
+ CREATE INDEX ON indexer.predicates(_id);
+ CREATE INDEX ON indexer.predicates(address);
+ `,
+ [],
+ );
+ console.log('Indexes for predicates table created successfully.');
+
+ // Step 15: Create the transactions_accounts table
+ await db.query(
+ `
+ CREATE TABLE indexer.transactions_accounts (
+ _id TEXT NOT NULL,
+ block_id INTEGER NOT NULL,
+ tx_hash TEXT NOT NULL,
+ account_hash TEXT NOT NULL,
+ PRIMARY KEY (_id, block_id, tx_hash, account_hash)
+ );
+ `,
+ [],
+ );
+ console.log('Transactions_Accounts table created successfully.');
+
+ // Step 16: Create indexes for transactions_accounts table
+ await db.query(
+ `
+ CREATE INDEX ON indexer.transactions_accounts (_id);
+ CREATE INDEX ON indexer.transactions_accounts (block_id);
+ CREATE INDEX ON indexer.transactions_accounts (tx_hash);
+ CREATE INDEX ON indexer.transactions_accounts (account_hash);
+ `,
+ [],
+ );
+ console.log('Indexes for transactions_accounts table created successfully.');
+}
+
+migrate()
+ .catch(console.error)
+ .finally(() => process.exit());
diff --git a/packages/graphql/src/domain/Account/AccountEntity.ts b/packages/graphql/src/domain/Account/AccountEntity.ts
new file mode 100644
index 000000000..4c58d7136
--- /dev/null
+++ b/packages/graphql/src/domain/Account/AccountEntity.ts
@@ -0,0 +1,73 @@
+import { Hash256 } from '../../application/vo/Hash256';
+import { Entity } from '../../core/Entity';
+import { AccountBalance } from './vo/AccountBalance';
+import { AccountData } from './vo/AccountData';
+import { AccountModelID } from './vo/AccountModelID';
+
+type AccountInputProps = {
+ account_id: Hash256;
+ balance: AccountBalance;
+ data: AccountData;
+ transactionCount: number;
+};
+
+export class AccountEntity extends Entity {
+ // Adjust the constructor to not require an ID initially
+ static create(account: any) {
+ const account_id = Hash256.create(account.account_id);
+ const balance = AccountBalance.create(account.balance);
+ const data = AccountData.create(account.data);
+ const transactionCount = account.transactionCount || 0;
+
+ const props: AccountInputProps = {
+ account_id,
+ balance,
+ data,
+ transactionCount,
+ };
+
+ // If _id is not provided, set it as undefined
+ const id = account._id ? AccountModelID.create(account._id) : undefined;
+
+ return new AccountEntity(props, id);
+ }
+
+ static toDBItem(account: AccountEntity): any {
+ return {
+ account_id: account.props.account_id.value(),
+ balance: account.props.balance.value().toString(),
+ data: AccountEntity.serializeData(account.props.data.value()),
+ transaction_count: account.props.transactionCount,
+ };
+ }
+
+ static serializeData(data: any): string {
+ return JSON.stringify(data, (_, value) =>
+ typeof value === 'bigint' ? value.toString() : value,
+ );
+ }
+
+ get cursor() {
+ return this.id ? this.id.value() : null;
+ }
+
+ get id() {
+ return this._id;
+ }
+
+ get account_id() {
+ return this.props.account_id.value();
+ }
+
+ get balance() {
+ return this.props.balance.value();
+ }
+
+ get transactionCount() {
+ return this.props.transactionCount;
+ }
+
+ get data() {
+ return this.props.data.value();
+ }
+}
diff --git a/packages/graphql/src/domain/Account/vo/AccountBalance.ts b/packages/graphql/src/domain/Account/vo/AccountBalance.ts
new file mode 100644
index 000000000..be538e838
--- /dev/null
+++ b/packages/graphql/src/domain/Account/vo/AccountBalance.ts
@@ -0,0 +1,27 @@
+import { bigint as DrizzleBigint } from 'drizzle-orm/pg-core';
+import { ValueObject } from '../../../core/ValueObject';
+interface Props {
+ value: bigint;
+}
+
+export class AccountBalance extends ValueObject {
+ private constructor(props: Props) {
+ super(props);
+ }
+
+ static type() {
+ return DrizzleBigint('balance', { mode: 'bigint' }).notNull();
+ }
+
+ static create(value: bigint) {
+ return new AccountBalance({ value });
+ }
+
+ value() {
+ return this.props.value;
+ }
+
+ add(amount: bigint): AccountBalance {
+ return new AccountBalance({ value: this.value() + amount });
+ }
+}
diff --git a/packages/graphql/src/domain/Account/vo/AccountData.ts b/packages/graphql/src/domain/Account/vo/AccountData.ts
new file mode 100644
index 000000000..080d74b73
--- /dev/null
+++ b/packages/graphql/src/domain/Account/vo/AccountData.ts
@@ -0,0 +1,24 @@
+import { jsonb } from 'drizzle-orm/pg-core';
+import { ValueObject } from '../../../core/ValueObject';
+
+interface Props {
+ value: any;
+}
+
+export class AccountData extends ValueObject {
+ private constructor(props: Props) {
+ super(props);
+ }
+
+ static type() {
+ return jsonb('data').notNull();
+ }
+
+ static create(value: any) {
+ return new AccountData({ value });
+ }
+
+ value() {
+ return this.props.value;
+ }
+}
diff --git a/packages/graphql/src/domain/Account/vo/AccountModelID.ts b/packages/graphql/src/domain/Account/vo/AccountModelID.ts
new file mode 100644
index 000000000..e5d6513f4
--- /dev/null
+++ b/packages/graphql/src/domain/Account/vo/AccountModelID.ts
@@ -0,0 +1,20 @@
+import { integer } from 'drizzle-orm/pg-core';
+import { Identifier } from '../../../core/Identifier';
+
+export class AccountModelID extends Identifier {
+ private constructor(id: number) {
+ super(id);
+ }
+
+ static type() {
+ return integer('_id').primaryKey();
+ }
+
+ static create(id: number): AccountModelID {
+ if (typeof id !== 'number' || Number.isNaN(id)) {
+ throw new Error('Invalid ID: ID must be a valid number.');
+ }
+
+ return new AccountModelID(id);
+ }
+}
diff --git a/packages/graphql/src/domain/Account/vo/AccountRef.ts b/packages/graphql/src/domain/Account/vo/AccountRef.ts
new file mode 100644
index 000000000..ca4f4474e
--- /dev/null
+++ b/packages/graphql/src/domain/Account/vo/AccountRef.ts
@@ -0,0 +1,18 @@
+import { ValueObject } from '../../../core/ValueObject';
+interface Props {
+ value: number;
+}
+
+export class AccountRef extends ValueObject {
+ private constructor(props: Props) {
+ super(props);
+ }
+
+ static create(id: number) {
+ return new AccountRef({ value: id });
+ }
+
+ value() {
+ return this.props.value;
+ }
+}
diff --git a/packages/graphql/src/graphql/generated/fuelcore/queries/cumulativeTransactionsFeeStatistics.graphql b/packages/graphql/src/graphql/generated/fuelcore/queries/cumulativeTransactionsFeeStatistics.graphql
new file mode 100644
index 000000000..0e3c5bcb1
--- /dev/null
+++ b/packages/graphql/src/graphql/generated/fuelcore/queries/cumulativeTransactionsFeeStatistics.graphql
@@ -0,0 +1,9 @@
+query cumulativeTransactionsFeeStatistics($timeFilter: String){
+ cumulativeTransactionsFeeStatistics(timeFilter: $timeFilter){
+ nodes{
+ fee
+ timestamp
+ }
+ transactionOffset
+ }
+}
\ No newline at end of file
diff --git a/packages/graphql/src/graphql/generated/fuelcore/queries/getBlocksDashboard.graphql b/packages/graphql/src/graphql/generated/fuelcore/queries/getBlocksDashboard.graphql
new file mode 100644
index 000000000..e6f943f4d
--- /dev/null
+++ b/packages/graphql/src/graphql/generated/fuelcore/queries/getBlocksDashboard.graphql
@@ -0,0 +1,10 @@
+query getBlocksDashboard{
+ getBlocksDashboard{
+ nodes{
+ timestamp
+ gasUsed
+ blockNo
+ producer
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/graphql/src/graphql/generated/fuelcore/queries/index.js b/packages/graphql/src/graphql/generated/fuelcore/queries/index.js
index f99a6bf08..eae1a0017 100644
--- a/packages/graphql/src/graphql/generated/fuelcore/queries/index.js
+++ b/packages/graphql/src/graphql/generated/fuelcore/queries/index.js
@@ -1,30 +1,119 @@
const fs = require('fs');
const path = require('path');
-module.exports.balance = fs.readFileSync(path.join(__dirname, 'balance.graphql'), 'utf8');
-module.exports.balances = fs.readFileSync(path.join(__dirname, 'balances.graphql'), 'utf8');
-module.exports.block = fs.readFileSync(path.join(__dirname, 'block.graphql'), 'utf8');
-module.exports.blocks = fs.readFileSync(path.join(__dirname, 'blocks.graphql'), 'utf8');
-module.exports.chain = fs.readFileSync(path.join(__dirname, 'chain.graphql'), 'utf8');
-module.exports.coin = fs.readFileSync(path.join(__dirname, 'coin.graphql'), 'utf8');
-module.exports.coins = fs.readFileSync(path.join(__dirname, 'coins.graphql'), 'utf8');
-module.exports.coinsToSpend = fs.readFileSync(path.join(__dirname, 'coinsToSpend.graphql'), 'utf8');
-module.exports.contract = fs.readFileSync(path.join(__dirname, 'contract.graphql'), 'utf8');
-module.exports.contractBalance = fs.readFileSync(path.join(__dirname, 'contractBalance.graphql'), 'utf8');
-module.exports.contractBalances = fs.readFileSync(path.join(__dirname, 'contractBalances.graphql'), 'utf8');
-module.exports.estimateGasPrice = fs.readFileSync(path.join(__dirname, 'estimateGasPrice.graphql'), 'utf8');
-module.exports.estimatePredicates = fs.readFileSync(path.join(__dirname, 'estimatePredicates.graphql'), 'utf8');
-module.exports.health = fs.readFileSync(path.join(__dirname, 'health.graphql'), 'utf8');
-module.exports.latestGasPrice = fs.readFileSync(path.join(__dirname, 'latestGasPrice.graphql'), 'utf8');
-module.exports.memory = fs.readFileSync(path.join(__dirname, 'memory.graphql'), 'utf8');
-module.exports.message = fs.readFileSync(path.join(__dirname, 'message.graphql'), 'utf8');
-module.exports.messageProof = fs.readFileSync(path.join(__dirname, 'messageProof.graphql'), 'utf8');
-module.exports.messageStatus = fs.readFileSync(path.join(__dirname, 'messageStatus.graphql'), 'utf8');
-module.exports.messages = fs.readFileSync(path.join(__dirname, 'messages.graphql'), 'utf8');
-module.exports.nodeInfo = fs.readFileSync(path.join(__dirname, 'nodeInfo.graphql'), 'utf8');
-module.exports.register = fs.readFileSync(path.join(__dirname, 'register.graphql'), 'utf8');
-module.exports.relayedTransactionStatus = fs.readFileSync(path.join(__dirname, 'relayedTransactionStatus.graphql'), 'utf8');
-module.exports.transaction = fs.readFileSync(path.join(__dirname, 'transaction.graphql'), 'utf8');
-module.exports.transactions = fs.readFileSync(path.join(__dirname, 'transactions.graphql'), 'utf8');
-module.exports.transactionsByOwner = fs.readFileSync(path.join(__dirname, 'transactionsByOwner.graphql'), 'utf8');
-module.exports.transactionsByBlockId = fs.readFileSync(path.join(__dirname, 'transactionsByBlockId.graphql'), 'utf8');
+module.exports.balance = fs.readFileSync(
+ path.join(__dirname, 'balance.graphql'),
+ 'utf8',
+);
+module.exports.balances = fs.readFileSync(
+ path.join(__dirname, 'balances.graphql'),
+ 'utf8',
+);
+module.exports.block = fs.readFileSync(
+ path.join(__dirname, 'block.graphql'),
+ 'utf8',
+);
+module.exports.blocks = fs.readFileSync(
+ path.join(__dirname, 'blocks.graphql'),
+ 'utf8',
+);
+module.exports.chain = fs.readFileSync(
+ path.join(__dirname, 'chain.graphql'),
+ 'utf8',
+);
+module.exports.coin = fs.readFileSync(
+ path.join(__dirname, 'coin.graphql'),
+ 'utf8',
+);
+module.exports.coins = fs.readFileSync(
+ path.join(__dirname, 'coins.graphql'),
+ 'utf8',
+);
+module.exports.coinsToSpend = fs.readFileSync(
+ path.join(__dirname, 'coinsToSpend.graphql'),
+ 'utf8',
+);
+module.exports.contract = fs.readFileSync(
+ path.join(__dirname, 'contract.graphql'),
+ 'utf8',
+);
+module.exports.contractBalance = fs.readFileSync(
+ path.join(__dirname, 'contractBalance.graphql'),
+ 'utf8',
+);
+module.exports.contractBalances = fs.readFileSync(
+ path.join(__dirname, 'contractBalances.graphql'),
+ 'utf8',
+);
+module.exports.estimateGasPrice = fs.readFileSync(
+ path.join(__dirname, 'estimateGasPrice.graphql'),
+ 'utf8',
+);
+module.exports.estimatePredicates = fs.readFileSync(
+ path.join(__dirname, 'estimatePredicates.graphql'),
+ 'utf8',
+);
+module.exports.health = fs.readFileSync(
+ path.join(__dirname, 'health.graphql'),
+ 'utf8',
+);
+module.exports.latestGasPrice = fs.readFileSync(
+ path.join(__dirname, 'latestGasPrice.graphql'),
+ 'utf8',
+);
+module.exports.memory = fs.readFileSync(
+ path.join(__dirname, 'memory.graphql'),
+ 'utf8',
+);
+module.exports.message = fs.readFileSync(
+ path.join(__dirname, 'message.graphql'),
+ 'utf8',
+);
+module.exports.messageProof = fs.readFileSync(
+ path.join(__dirname, 'messageProof.graphql'),
+ 'utf8',
+);
+module.exports.messageStatus = fs.readFileSync(
+ path.join(__dirname, 'messageStatus.graphql'),
+ 'utf8',
+);
+module.exports.messages = fs.readFileSync(
+ path.join(__dirname, 'messages.graphql'),
+ 'utf8',
+);
+module.exports.nodeInfo = fs.readFileSync(
+ path.join(__dirname, 'nodeInfo.graphql'),
+ 'utf8',
+);
+module.exports.register = fs.readFileSync(
+ path.join(__dirname, 'register.graphql'),
+ 'utf8',
+);
+module.exports.relayedTransactionStatus = fs.readFileSync(
+ path.join(__dirname, 'relayedTransactionStatus.graphql'),
+ 'utf8',
+);
+module.exports.transaction = fs.readFileSync(
+ path.join(__dirname, 'transaction.graphql'),
+ 'utf8',
+);
+module.exports.transactions = fs.readFileSync(
+ path.join(__dirname, 'transactions.graphql'),
+ 'utf8',
+);
+module.exports.transactionsByOwner = fs.readFileSync(
+ path.join(__dirname, 'transactionsByOwner.graphql'),
+ 'utf8',
+);
+module.exports.transactionsByBlockId = fs.readFileSync(
+ path.join(__dirname, 'transactionsByBlockId.graphql'),
+ 'utf8',
+);
+module.exports.tps = fs.readFileSync(
+ path.join(__dirname, 'tps.graphql'),
+ 'utf8',
+);
+module.exports.getBlocksDashboard = fs.readFileSync(
+ path.join(__dirname, 'getBlocksDashboard.graphql'),
+ 'utf8',
+);
diff --git a/packages/graphql/src/graphql/generated/fuelcore/queries/tps.graphql b/packages/graphql/src/graphql/generated/fuelcore/queries/tps.graphql
new file mode 100644
index 000000000..e926f58bb
--- /dev/null
+++ b/packages/graphql/src/graphql/generated/fuelcore/queries/tps.graphql
@@ -0,0 +1,10 @@
+query tps{
+ tps{
+ nodes{
+ start
+ end
+ txCount
+ totalGas
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/graphql/src/graphql/generated/mocks.ts b/packages/graphql/src/graphql/generated/mocks.ts
index 364e55959..93b46f3b7 100644
--- a/packages/graphql/src/graphql/generated/mocks.ts
+++ b/packages/graphql/src/graphql/generated/mocks.ts
@@ -820,7 +820,6 @@ export const aQuery = (overrides?: Partial): { __typename: 'Query' } &
search: overrides && overrides.hasOwnProperty('search') ? overrides.search! : aSearchResult(),
transaction: overrides && overrides.hasOwnProperty('transaction') ? overrides.transaction! : aTransaction(),
transactions: overrides && overrides.hasOwnProperty('transactions') ? overrides.transactions! : aTransactionConnection(),
- transactionsByBlockId: overrides && overrides.hasOwnProperty('transactionsByBlockId') ? overrides.transactionsByBlockId! : aTransactionConnection(),
transactionsByOwner: overrides && overrides.hasOwnProperty('transactionsByOwner') ? overrides.transactionsByOwner! : aTransactionConnection(),
};
};
diff --git a/packages/graphql/src/graphql/generated/sdk-provider.ts b/packages/graphql/src/graphql/generated/sdk-provider.ts
index 04526dc4a..da5d18f5e 100644
--- a/packages/graphql/src/graphql/generated/sdk-provider.ts
+++ b/packages/graphql/src/graphql/generated/sdk-provider.ts
@@ -117,6 +117,19 @@ export enum GQLBlockVersion {
V1 = 'V1',
}
+export type GQLBlocksDashboard = {
+ __typename: 'BlocksDashboard';
+ blockNo: Scalars['U64']['output'];
+ gasUsed: Scalars['U64']['output'];
+ producer?: Maybe;
+ timestamp: Scalars['U64']['output'];
+};
+
+export type GQLBlocksDashboardConnection = {
+ __typename: 'BlocksDashboardConnection';
+ nodes: Array;
+};
+
/** Breakpoint, defined as a tuple of contract ID and relative PC offset inside it */
export type GQLBreakpoint = {
contract: Scalars['ContractId']['input'];
@@ -968,6 +981,7 @@ export type GQLQuery = {
estimateGasPrice: GQLEstimateGasPrice;
/** Estimate the predicate gas for the provided transaction */
estimatePredicates: GQLTransaction;
+ getBlocksDashboard: GQLBlocksDashboardConnection;
/** Returns true when the GraphQL API is serving requests. */
health: Scalars['Boolean']['output'];
latestGasPrice: GQLLatestGasPrice;
@@ -983,6 +997,7 @@ export type GQLQuery = {
register: Scalars['U64']['output'];
relayedTransactionStatus?: Maybe;
search?: Maybe;
+ tps: GQLTpsConnection;
transaction?: Maybe;
transactions: GQLTransactionConnection;
transactionsByBlockId: GQLTransactionConnection;
@@ -1319,6 +1334,19 @@ export type GQLSuccessStatus = {
transactionId: Scalars['TransactionId']['output'];
};
+export type GQLTps = {
+ __typename: 'TPS';
+ end?: Maybe;
+ start?: Maybe;
+ totalGas: Scalars['U64']['output'];
+ txCount: Scalars['U64']['output'];
+};
+
+export type GQLTpsConnection = {
+ __typename: 'TPSConnection';
+ nodes: Array;
+};
+
export type GQLTransaction = {
__typename: 'Transaction';
_id?: Maybe;
@@ -3516,6 +3544,22 @@ export type GQLNodeInfoQuery = {
};
};
+export type GQLTpsQueryVariables = Exact<{ [key: string]: never }>;
+
+export type GQLTpsQuery = {
+ __typename: 'Query';
+ tps: {
+ __typename: 'TPSConnection';
+ nodes: Array<{
+ __typename: 'TPS';
+ start?: string | null;
+ end?: string | null;
+ txCount: string;
+ totalGas: string;
+ }>;
+ };
+};
+
export const BalanceItemFragmentDoc = gql`
fragment BalanceItem on Balance {
amount
@@ -5244,6 +5288,18 @@ export const NodeInfoDocument = gql`
}
}
`;
+export const TpsDocument = gql`
+ query tps {
+ tps {
+ nodes {
+ start
+ end
+ txCount
+ totalGas
+ }
+ }
+}
+ `;
export type SdkFunctionWrapper = (
action: (requestHeaders?: Record) => Promise,
@@ -5267,6 +5323,7 @@ const ContractDocumentString = print(ContractDocument);
const ContractBalanceDocumentString = print(ContractBalanceDocument);
const ContractBalancesDocumentString = print(ContractBalancesDocument);
const NodeInfoDocumentString = print(NodeInfoDocument);
+const TpsDocumentString = print(TpsDocument);
export function getSdk(
client: GraphQLClient,
withWrapper: SdkFunctionWrapper = defaultWrapper,
@@ -5466,6 +5523,27 @@ export function getSdk(
variables,
);
},
+ tps(
+ variables?: GQLTpsQueryVariables,
+ requestHeaders?: GraphQLClientRequestHeaders,
+ ): Promise<{
+ data: GQLTpsQuery;
+ errors?: GraphQLError[];
+ extensions?: any;
+ headers: Headers;
+ status: number;
+ }> {
+ return withWrapper(
+ (wrappedRequestHeaders) =>
+ client.rawRequest(TpsDocumentString, variables, {
+ ...requestHeaders,
+ ...wrappedRequestHeaders,
+ }),
+ 'tps',
+ 'query',
+ variables,
+ );
+ },
};
}
export type Sdk = ReturnType;
diff --git a/packages/graphql/src/graphql/generated/sdk.ts b/packages/graphql/src/graphql/generated/sdk.ts
index 7d00e6408..ffad30630 100644
--- a/packages/graphql/src/graphql/generated/sdk.ts
+++ b/packages/graphql/src/graphql/generated/sdk.ts
@@ -117,6 +117,19 @@ export enum GQLBlockVersion {
V1 = 'V1',
}
+export type GQLBlocksDashboard = {
+ __typename: 'BlocksDashboard';
+ blockNo: Scalars['U64']['output'];
+ gasUsed: Scalars['U64']['output'];
+ producer?: Maybe;
+ timestamp: Scalars['U64']['output'];
+};
+
+export type GQLBlocksDashboardConnection = {
+ __typename: 'BlocksDashboardConnection';
+ nodes: Array;
+};
+
/** Breakpoint, defined as a tuple of contract ID and relative PC offset inside it */
export type GQLBreakpoint = {
contract: Scalars['ContractId']['input'];
@@ -968,6 +981,7 @@ export type GQLQuery = {
estimateGasPrice: GQLEstimateGasPrice;
/** Estimate the predicate gas for the provided transaction */
estimatePredicates: GQLTransaction;
+ getBlocksDashboard: GQLBlocksDashboardConnection;
/** Returns true when the GraphQL API is serving requests. */
health: Scalars['Boolean']['output'];
latestGasPrice: GQLLatestGasPrice;
@@ -983,6 +997,7 @@ export type GQLQuery = {
register: Scalars['U64']['output'];
relayedTransactionStatus?: Maybe;
search?: Maybe;
+ tps: GQLTpsConnection;
transaction?: Maybe;
transactions: GQLTransactionConnection;
transactionsByBlockId: GQLTransactionConnection;
@@ -1319,6 +1334,19 @@ export type GQLSuccessStatus = {
transactionId: Scalars['TransactionId']['output'];
};
+export type GQLTps = {
+ __typename: 'TPS';
+ end?: Maybe;
+ start?: Maybe;
+ totalGas: Scalars['U64']['output'];
+ txCount: Scalars['U64']['output'];
+};
+
+export type GQLTpsConnection = {
+ __typename: 'TPSConnection';
+ nodes: Array;
+};
+
export type GQLTransaction = {
__typename: 'Transaction';
_id?: Maybe;
@@ -2689,6 +2717,24 @@ export type GQLContractBalancesQuery = {
};
};
+export type GQLGetBlocksDashboardQueryVariables = Exact<{
+ [key: string]: never;
+}>;
+
+export type GQLGetBlocksDashboardQuery = {
+ __typename: 'Query';
+ getBlocksDashboard: {
+ __typename: 'BlocksDashboardConnection';
+ nodes: Array<{
+ __typename: 'BlocksDashboard';
+ timestamp: string;
+ gasUsed: string;
+ blockNo: string;
+ producer?: string | null;
+ }>;
+ };
+};
+
export type GQLPredicateQueryVariables = Exact<{
address: Scalars['String']['input'];
}>;
@@ -2782,6 +2828,22 @@ export type GQLSearchQuery = {
} | null;
};
+export type GQLTpsQueryVariables = Exact<{ [key: string]: never }>;
+
+export type GQLTpsQuery = {
+ __typename: 'Query';
+ tps: {
+ __typename: 'TPSConnection';
+ nodes: Array<{
+ __typename: 'TPS';
+ start?: string | null;
+ end?: string | null;
+ txCount: string;
+ totalGas: string;
+ }>;
+ };
+};
+
export type GQLTransactionDetailsQueryVariables = Exact<{
id: Scalars['TransactionId']['input'];
}>;
@@ -6270,6 +6332,18 @@ export const ContractBalancesDocument = gql`
}
}
${ContractBalanceConnectionNodeFragmentDoc}`;
+export const GetBlocksDashboardDocument = gql`
+ query getBlocksDashboard {
+ getBlocksDashboard {
+ nodes {
+ timestamp
+ gasUsed
+ blockNo
+ producer
+ }
+ }
+}
+ `;
export const PredicateDocument = gql`
query predicate($address: String!) {
predicate(address: $address) {
@@ -6315,6 +6389,18 @@ export const SearchDocument = gql`
}
}
`;
+export const TpsDocument = gql`
+ query tps {
+ tps {
+ nodes {
+ start
+ end
+ txCount
+ totalGas
+ }
+ }
+}
+ `;
export const TransactionDetailsDocument = gql`
query transactionDetails($id: TransactionId!) {
transaction(id: $id) {
@@ -6385,9 +6471,11 @@ const ChainDocumentString = print(ChainDocument);
const CoinsDocumentString = print(CoinsDocument);
const ContractDocumentString = print(ContractDocument);
const ContractBalancesDocumentString = print(ContractBalancesDocument);
+const GetBlocksDashboardDocumentString = print(GetBlocksDashboardDocument);
const PredicateDocumentString = print(PredicateDocument);
const RecentTransactionsDocumentString = print(RecentTransactionsDocument);
const SearchDocumentString = print(SearchDocument);
+const TpsDocumentString = print(TpsDocument);
const TransactionDetailsDocumentString = print(TransactionDetailsDocument);
const TransactionsByBlockIdDocumentString = print(
TransactionsByBlockIdDocument,
@@ -6548,6 +6636,28 @@ export function getSdk(
variables,
);
},
+ getBlocksDashboard(
+ variables?: GQLGetBlocksDashboardQueryVariables,
+ requestHeaders?: GraphQLClientRequestHeaders,
+ ): Promise<{
+ data: GQLGetBlocksDashboardQuery;
+ errors?: GraphQLError[];
+ extensions?: any;
+ headers: Headers;
+ status: number;
+ }> {
+ return withWrapper(
+ (wrappedRequestHeaders) =>
+ client.rawRequest(
+ GetBlocksDashboardDocumentString,
+ variables,
+ { ...requestHeaders, ...wrappedRequestHeaders },
+ ),
+ 'getBlocksDashboard',
+ 'query',
+ variables,
+ );
+ },
predicate(
variables: GQLPredicateQueryVariables,
requestHeaders?: GraphQLClientRequestHeaders,
@@ -6613,6 +6723,27 @@ export function getSdk(
variables,
);
},
+ tps(
+ variables?: GQLTpsQueryVariables,
+ requestHeaders?: GraphQLClientRequestHeaders,
+ ): Promise<{
+ data: GQLTpsQuery;
+ errors?: GraphQLError[];
+ extensions?: any;
+ headers: Headers;
+ status: number;
+ }> {
+ return withWrapper(
+ (wrappedRequestHeaders) =>
+ client.rawRequest(TpsDocumentString, variables, {
+ ...requestHeaders,
+ ...wrappedRequestHeaders,
+ }),
+ 'tps',
+ 'query',
+ variables,
+ );
+ },
transactionDetails(
variables: GQLTransactionDetailsQueryVariables,
requestHeaders?: GraphQLClientRequestHeaders,
diff --git a/packages/graphql/src/graphql/queries/provider/tps.graphql b/packages/graphql/src/graphql/queries/provider/tps.graphql
new file mode 100644
index 000000000..3599163b8
--- /dev/null
+++ b/packages/graphql/src/graphql/queries/provider/tps.graphql
@@ -0,0 +1,10 @@
+query tps{
+ tps{
+ nodes {
+ start
+ end
+ txCount
+ totalGas
+ }
+ }
+}
diff --git a/packages/graphql/src/graphql/queries/sdk/getBlocksDashboard.graphql b/packages/graphql/src/graphql/queries/sdk/getBlocksDashboard.graphql
new file mode 100644
index 000000000..e1ad2f02e
--- /dev/null
+++ b/packages/graphql/src/graphql/queries/sdk/getBlocksDashboard.graphql
@@ -0,0 +1,10 @@
+query getBlocksDashboard{
+ getBlocksDashboard{
+ nodes {
+ timestamp
+ gasUsed
+ blockNo
+ producer
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/graphql/src/graphql/queries/sdk/tps.graphql b/packages/graphql/src/graphql/queries/sdk/tps.graphql
new file mode 100644
index 000000000..3599163b8
--- /dev/null
+++ b/packages/graphql/src/graphql/queries/sdk/tps.graphql
@@ -0,0 +1,10 @@
+query tps{
+ tps{
+ nodes {
+ start
+ end
+ txCount
+ totalGas
+ }
+ }
+}
diff --git a/packages/graphql/src/graphql/resolvers/BlockResolver.ts b/packages/graphql/src/graphql/resolvers/BlockResolver.ts
index 0ca336c7e..e309db74b 100644
--- a/packages/graphql/src/graphql/resolvers/BlockResolver.ts
+++ b/packages/graphql/src/graphql/resolvers/BlockResolver.ts
@@ -12,6 +12,8 @@ type Source = GQLBlock;
type Params = {
blocks: GQLQueryBlocksArgs;
block: GQLQueryBlockArgs;
+ tps: null;
+ getBlocksDashboard: null;
};
export class BlockResolver {
@@ -21,6 +23,8 @@ export class BlockResolver {
Query: {
block: resolvers.block,
blocks: resolvers.blocks,
+ tps: resolvers.tps,
+ getBlocksDashboard: resolvers.getBlocksDashboard,
},
};
}
@@ -56,4 +60,18 @@ export class BlockResolver {
);
return blocks;
}
+
+ async getBlocksDashboard(_: Source, _params: Params['getBlocksDashboard']) {
+ const blockDAO = new BlockDAO();
+ const blocks = await blockDAO.getBlocksDashboard();
+
+ return blocks;
+ }
+
+ async tps(_: Source, _params: Params['tps']) {
+ const blockDAO = new BlockDAO();
+ const tps = await blockDAO.tps();
+
+ return tps;
+ }
}
diff --git a/packages/graphql/src/graphql/resolvers/getBlocksDashboard.graphql b/packages/graphql/src/graphql/resolvers/getBlocksDashboard.graphql
new file mode 100644
index 000000000..e1ad2f02e
--- /dev/null
+++ b/packages/graphql/src/graphql/resolvers/getBlocksDashboard.graphql
@@ -0,0 +1,10 @@
+query getBlocksDashboard{
+ getBlocksDashboard{
+ nodes {
+ timestamp
+ gasUsed
+ blockNo
+ producer
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/graphql/src/graphql/schemas/explorer.graphql b/packages/graphql/src/graphql/schemas/explorer.graphql
index 9f1d4bc44..efe1b21b4 100644
--- a/packages/graphql/src/graphql/schemas/explorer.graphql
+++ b/packages/graphql/src/graphql/schemas/explorer.graphql
@@ -223,4 +223,28 @@ type Query {
search(query: String!): SearchResult
predicate(address: String!): PredicateItem
+ tps: TPSConnection!
+ getBlocksDashboard: BlocksDashboardConnection!
}
+
+type TPS {
+ start: String
+ end: String
+ txCount: U64!
+ totalGas: U64!
+}
+
+type TPSConnection {
+ nodes: [TPS!]!
+}
+
+type BlocksDashboard {
+ timestamp: U64!
+ gasUsed: U64!
+ blockNo: U64!
+ producer: String
+}
+
+type BlocksDashboardConnection {
+ nodes: [BlocksDashboard!]!
+}
\ No newline at end of file
diff --git a/packages/graphql/src/graphql/schemas/fuelcore.graphql b/packages/graphql/src/graphql/schemas/fuelcore.graphql
index ab84ad760..5e880dd0d 100644
--- a/packages/graphql/src/graphql/schemas/fuelcore.graphql
+++ b/packages/graphql/src/graphql/schemas/fuelcore.graphql
@@ -781,6 +781,8 @@ type Query {
transactions(after: String, before: String, first: Int, last: Int): TransactionConnection!
transactionsByOwner(after: String, before: String, first: Int, last: Int, owner: Address!): TransactionConnection!
transactionsByBlockId(after: String, before: String, first: Int, last: Int, blockId: String!): TransactionConnection!
+ tps: TPSConnection!
+ getBlocksDashboard : BlocksDashboardConnection!
}
type Receipt {
@@ -1021,4 +1023,26 @@ type VariableOutput {
amount: U64!
assetId: AssetId!
to: Address!
+}
+
+type TPS {
+ start: String
+ end: String
+ txCount: U64!
+ totalGas: U64!
+}
+
+type TPSConnection {
+ nodes: [TPS!]!
+}
+
+type BlocksDashboard {
+ timestamp: U64!
+ gasUsed: U64!
+ blockNo: U64!
+ producer: String
+}
+
+type BlocksDashboardConnection {
+ nodes: [BlocksDashboard!]!
}
\ No newline at end of file
diff --git a/packages/graphql/src/infra/dao/Block.ts b/packages/graphql/src/infra/dao/Block.ts
index e2849909e..90c653c10 100644
--- a/packages/graphql/src/infra/dao/Block.ts
+++ b/packages/graphql/src/infra/dao/Block.ts
@@ -62,4 +62,15 @@ export default class Block {
transactions: this.transactions.map((t) => t.toGQLNode()),
};
}
+
+ toTPSNode() {
+ const _data = this.data;
+ return {
+ blockNo: String(this.id),
+ producer: this.producer,
+ timestamp: this.time.rawUnix,
+ gasUsed: `${this.totalGasUsed}`,
+ tps: String(this.transactions.length),
+ };
+ }
}
diff --git a/packages/graphql/src/infra/dao/BlockDAO.ts b/packages/graphql/src/infra/dao/BlockDAO.ts
index 33e1c517f..cf9436204 100644
--- a/packages/graphql/src/infra/dao/BlockDAO.ts
+++ b/packages/graphql/src/infra/dao/BlockDAO.ts
@@ -1,6 +1,7 @@
import { DatabaseConnection } from '../database/DatabaseConnection';
import PaginatedParams from '../paginator/PaginatedParams';
import Block from './Block';
+import { createIntervals, roundToNearest } from './utils';
export default class BlockDAO {
databaseConnection: DatabaseConnection;
@@ -121,4 +122,93 @@ export default class BlockDAO {
if (!blockData) return;
return new Block(blockData);
}
+
+ async getBlocksDashboard() {
+ const blocksData = await this.databaseConnection.query(
+ `
+ select
+ b._id AS blockno,
+ b.gas_used AS gasused,
+ b.producer,
+ b.timestamp AS timestamp
+ from
+ indexer.blocks b
+ order by
+ b._id desc
+ limit 6
+ `,
+ [],
+ );
+
+ const formattedBlocksData = blocksData.map((block) => ({
+ timestamp: new Date(Number(block.timestamp)).getTime(),
+ gasUsed: Number(block.gasused),
+ blockNo: block.blockno,
+ producer: block.producer,
+ }));
+
+ return {
+ nodes: formattedBlocksData,
+ };
+ }
+
+ async tps() {
+ const currentTime = new Date();
+ const timeMinusOneDay = new Date(
+ currentTime.getTime() - 24 * 60 * 60 * 1000,
+ );
+ const timeMinusOneDayRoundDown = new Date(
+ roundToNearest(timeMinusOneDay.getTime()),
+ );
+ const timeMinusOneDayRoundDownISO = timeMinusOneDayRoundDown.toISOString();
+
+ const blocksData = await this.databaseConnection.query(
+ `
+ SELECT
+ b.timestamp AS timestamp,
+ b.data->'header'->>'transactionsCount' AS tps,
+ b.gas_used AS gasused
+ FROM
+ indexer.blocks b
+ WHERE
+ b.timestamp >= $1
+ ORDER BY _id asc;
+ `,
+ [timeMinusOneDayRoundDownISO],
+ );
+
+ if (blocksData.length === 0) {
+ return { nodes: [] };
+ }
+
+ const lastTimestamp = new Date(
+ Number(blocksData[blocksData.length - 1].timestamp),
+ ).getTime();
+ const firstTimestamp = new Date(Number(blocksData[0].timestamp)).getTime();
+
+ const intervals = createIntervals(firstTimestamp, lastTimestamp, 'hour', 1);
+
+ // Process blocks and put them into the correct interval
+ blocksData.forEach((block) => {
+ const blockTimestamp = new Date(Number(block.timestamp)).getTime();
+ const txCount = Number(block.tps);
+ const gasUsed = Number(block.gasused);
+
+ // Find the correct interval for the current block
+ for (const interval of intervals) {
+ const intervalStart = new Date(interval.start).getTime();
+ const intervalEnd = new Date(interval.end).getTime();
+
+ if (blockTimestamp >= intervalStart && blockTimestamp < intervalEnd) {
+ interval.txCount += txCount;
+ interval.totalGas += gasUsed;
+ break;
+ }
+ }
+ });
+
+ return {
+ nodes: intervals,
+ };
+ }
}
diff --git a/packages/graphql/src/infra/dao/utils.ts b/packages/graphql/src/infra/dao/utils.ts
new file mode 100644
index 000000000..fef2cb26a
--- /dev/null
+++ b/packages/graphql/src/infra/dao/utils.ts
@@ -0,0 +1,49 @@
+export function roundToNearest(time: number, roundUp = false): number {
+ const msInHour = 60 * 60 * 1000;
+ return roundUp
+ ? Math.ceil(time / msInHour) * msInHour
+ : Math.floor(time / msInHour) * msInHour;
+}
+
+// General interval creation function
+export function createIntervals(
+ startTime: number,
+ endTime: number,
+ unit: 'minute' | 'hour' | 'day',
+ intervalSize: number,
+): Array<{ start: Date; end: Date; txCount: number; totalGas: number }> {
+ const roundedStartTime = roundToNearest(startTime);
+ const roundedEndTime = roundToNearest(endTime, true);
+
+ const intervals: Array<{
+ start: Date;
+ end: Date;
+ txCount: number;
+ totalGas: number;
+ }> = [];
+
+ let currentTime = roundedStartTime;
+
+ // Handle minute, hour, and day intervals
+ const msInUnit = {
+ minute: 60 * 1000,
+ hour: 60 * 60 * 1000,
+ day: 24 * 60 * 60 * 1000,
+ };
+
+ const intervalDuration = intervalSize * msInUnit[unit];
+
+ while (currentTime < roundedEndTime) {
+ const startInterval = new Date(currentTime);
+ const endInterval = new Date(currentTime + intervalDuration);
+ intervals.push({
+ start: startInterval,
+ end: endInterval,
+ txCount: 0,
+ totalGas: 0,
+ });
+ currentTime += intervalDuration;
+ }
+
+ return intervals;
+}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index f1419af1c..386b6740b 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -59,8 +59,8 @@
"@radix-ui/colors": "3.0.0",
"@radix-ui/react-accordion": "1.1.2",
"@radix-ui/react-aspect-ratio": "1.1.0",
- "@radix-ui/react-portal": "1.1.1",
"@radix-ui/react-dialog": "1.1.1",
+ "@radix-ui/react-portal": "1.1.1",
"@radix-ui/react-slot": "1.0.2",
"@radix-ui/react-toast": "1.2.1",
"@radix-ui/themes": "3.1.1",
@@ -69,13 +69,16 @@
"@tailwindcss/typography": "0.5.10",
"clsx": "2.1.0",
"csstype": "3.1.3",
+ "embla-carousel-react": "8.2.1",
"framer-motion": "11.0.5",
"geist": "1.2.2",
"modern-normalize": "2.0.0",
"next": "14.1.0",
"react": "18.2.0",
"react-aria": "3.32.1",
+ "react-data-table-component": "7.6.2",
"react-dom": "18.2.0",
+ "react-paginate": "8.2.0",
"react-stately": "3.29.1",
"react-use": "17.5.0",
"tailwind-variants": "0.1.20",
diff --git a/packages/ui/src/components/Box/RoundedContainer.tsx b/packages/ui/src/components/Box/RoundedContainer.tsx
new file mode 100644
index 000000000..c652a60d2
--- /dev/null
+++ b/packages/ui/src/components/Box/RoundedContainer.tsx
@@ -0,0 +1,25 @@
+import { Box as RadixBox } from '@radix-ui/themes';
+
+import { tv } from 'tailwind-variants';
+import { createComponent } from '../../utils/component';
+import type { PropsOf, WithAsProps } from '../../utils/types';
+
+export type RoundedContainerProps = WithAsProps & PropsOf;
+
+const styles = tv({
+ slots: {
+ root: 'rounded-[13px] p-4 bg-white dark:bg-card-bg',
+ },
+});
+
+export const RoundedContainer = createComponent<
+ RoundedContainerProps,
+ typeof RadixBox
+>({
+ id: 'RoundedContainer',
+ baseElement: RadixBox,
+ className: ({ className }) => styles().root({ className }),
+ render: (Comp, { children, ...props }) => {
+ return {children};
+ },
+});
diff --git a/packages/ui/src/components/Box/index.tsx b/packages/ui/src/components/Box/index.tsx
index d0dc5bdda..392656237 100644
--- a/packages/ui/src/components/Box/index.tsx
+++ b/packages/ui/src/components/Box/index.tsx
@@ -15,3 +15,6 @@ export type { VStackProps } from './VStack';
export { HStack } from './HStack';
export type { HStackProps } from './HStack';
+
+export { RoundedContainer } from './RoundedContainer';
+export type { RoundedContainerProps } from './RoundedContainer';
diff --git a/packages/ui/src/components/Carousel/Carousel.tsx b/packages/ui/src/components/Carousel/Carousel.tsx
new file mode 100644
index 000000000..f6d71d73f
--- /dev/null
+++ b/packages/ui/src/components/Carousel/Carousel.tsx
@@ -0,0 +1,195 @@
+'use client';
+
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from 'embla-carousel-react';
+import * as React from 'react';
+
+import { cx } from '../../utils/css';
+
+type CarouselApi = UseEmblaCarouselType[1];
+export type UseCarouselParameters = Parameters;
+export type CarouselOptions = UseCarouselParameters[0];
+export type CarouselPlugin = UseCarouselParameters[1];
+
+export type CarouselProps = {
+ opts?: CarouselOptions;
+ plugins?: CarouselPlugin;
+ orientation?: 'horizontal' | 'vertical';
+ setApi?: (api: CarouselApi) => void;
+};
+
+export type CarouselContextProps = {
+ carouselRef: ReturnType[0];
+ api: ReturnType[1];
+ scrollPrev: () => void;
+ scrollNext: () => void;
+ canScrollPrev: boolean;
+ canScrollNext: boolean;
+} & CarouselProps;
+
+const CarouselContext = React.createContext(null);
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext);
+
+ if (!context) {
+ throw new Error('useCarousel must be used within a ');
+ }
+
+ return context;
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = 'horizontal',
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref,
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === 'horizontal' ? 'x' : 'y',
+ },
+ plugins,
+ );
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return;
+ }
+
+ setCanScrollPrev(api.canScrollPrev());
+ setCanScrollNext(api.canScrollNext());
+ }, []);
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev();
+ }, [api]);
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext();
+ }, [api]);
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === 'ArrowLeft') {
+ event.preventDefault();
+ scrollPrev();
+ } else if (event.key === 'ArrowRight') {
+ event.preventDefault();
+ scrollNext();
+ }
+ },
+ [scrollPrev, scrollNext],
+ );
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return;
+ }
+
+ setApi(api);
+ }, [api, setApi]);
+
+ React.useEffect(() => {
+ if (!api) {
+ return;
+ }
+
+ onSelect(api);
+ api.on('reInit', onSelect);
+ api.on('select', onSelect);
+
+ return () => {
+ api?.off('select', onSelect);
+ };
+ }, [api, onSelect]);
+
+ return (
+
+
+ {children}
+
+
+ );
+ },
+);
+Carousel.displayName = 'Carousel';
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel();
+
+ return (
+
+ );
+});
+CarouselContent.displayName = 'CarouselContent';
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel();
+
+ return (
+
+ );
+});
+CarouselItem.displayName = 'CarouselItem';
+
+export { type CarouselApi, Carousel, CarouselContent, CarouselItem };
diff --git a/packages/ui/src/components/Carousel/index.tsx b/packages/ui/src/components/Carousel/index.tsx
new file mode 100644
index 000000000..7cbb397d3
--- /dev/null
+++ b/packages/ui/src/components/Carousel/index.tsx
@@ -0,0 +1,11 @@
+'use client';
+
+export { Carousel, CarouselContent, CarouselItem } from './Carousel';
+
+export type {
+ CarouselContextProps,
+ CarouselOptions,
+ CarouselPlugin,
+ CarouselProps,
+ UseCarouselParameters,
+} from './Carousel';
diff --git a/packages/ui/src/components/Charts/Charts.tsx b/packages/ui/src/components/Charts/Charts.tsx
new file mode 100644
index 000000000..53431a871
--- /dev/null
+++ b/packages/ui/src/components/Charts/Charts.tsx
@@ -0,0 +1,366 @@
+'use client';
+
+import * as React from 'react';
+import * as RechartsPrimitive from 'recharts';
+
+import { cx } from '../../utils/css';
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: '', dark: '.dark' } as const;
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode;
+ icon?: React.ComponentType;
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ );
+};
+
+type ChartContextProps = {
+ config: ChartConfig;
+};
+
+const ChartContext = React.createContext(null);
+
+function useChart() {
+ const context = React.useContext(ChartContext);
+
+ if (!context) {
+ throw new Error('useChart must be used within a ');
+ }
+
+ return context;
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<'div'> & {
+ config: ChartConfig;
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >['children'];
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId();
+ const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ );
+});
+ChartContainer.displayName = 'Chart';
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([_, config]) => config.theme || config.color,
+ );
+
+ if (!colorConfig.length) {
+ return null;
+ }
+
+ return (
+