diff --git a/package.json b/package.json index 9576bd344..ff0395e5e 100644 --- a/package.json +++ b/package.json @@ -136,5 +136,10 @@ "ws@<8.17.1": ">=8.17.1", "fast-loops@<1.1.4": ">=1.1.4" } + }, + "dependencies": { + "react-data-table-component": "7.6.2", + "react-paginate": "8.2.0", + "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/public/ecosystem/images/executoors.jpeg b/packages/app-explorer/public/ecosystem/images/executoors.jpeg new file mode 100644 index 000000000..01f8c2afe Binary files /dev/null and b/packages/app-explorer/public/ecosystem/images/executoors.jpeg differ diff --git a/packages/app-explorer/public/ecosystem/images/fuelet.jpeg b/packages/app-explorer/public/ecosystem/images/fuelet.jpeg index 5e305ad99..da70bf17e 100644 Binary files a/packages/app-explorer/public/ecosystem/images/fuelet.jpeg and b/packages/app-explorer/public/ecosystem/images/fuelet.jpeg differ 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/systems/Block/screens/BlockScreen.tsx b/packages/app-explorer/src/systems/Block/screens/BlockScreen.tsx index a428fa585..527883f74 100644 --- a/packages/app-explorer/src/systems/Block/screens/BlockScreen.tsx +++ b/packages/app-explorer/src/systems/Block/screens/BlockScreen.tsx @@ -46,14 +46,35 @@ export const BlocksScreen = () => { let newCursor: string | null = null; setDir(newDir); - if (newDir === 'before' && data.pageInfo.endCursor) { - newCursor = data.pageInfo.endCursor; - } else if (newDir === 'after' && data.pageInfo.startCursor) { - newCursor = data.pageInfo.startCursor; + if ( + newPageNumber === currentPage + 1 || + newPageNumber === currentPage - 1 + ) { + if (newDir === 'before' && data.pageInfo.endCursor) { + newCursor = data.pageInfo.endCursor; + } else if (newDir === 'after' && data.pageInfo.startCursor) { + newCursor = data.pageInfo.startCursor; + } + } else { + if (newDir === 'before' && data.pageInfo.endCursor) { + newCursor = ( + +data.pageInfo.endCursor - + (newPageNumber - currentPage) * limit + ).toString(); + } else if (newDir === 'after' && data.pageInfo.startCursor) { + newCursor = ( + +data.pageInfo.startCursor + + (currentPage - newPageNumber) * limit + ).toString(); + } } setCurrentPage(newPageNumber); setCurrentCursor(newCursor); + if (newPageNumber === 1) { + router.push('/blocks'); + return; + } router.push(`/blocks?page=${newPageNumber}&cursor=${newCursor}`); } }; 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..82e808434 100644 --- a/packages/app-explorer/src/systems/Core/components/Layout/Layout.tsx +++ b/packages/app-explorer/src/systems/Core/components/Layout/Layout.tsx @@ -1,8 +1,10 @@ 'use client'; -import { Container, VStack } from '@fuels/ui'; +import { Container, LoadingBox, VStack } from '@fuels/ui'; import { usePathname } from 'next/navigation'; -import { Hero } from '~/systems/Home/components/Hero/Hero'; +const Hero = React.lazy(() => import('~/systems/Home/components/Hero/Hero')); +import { DateTime } from 'fuels'; +import React, { Suspense } from 'react'; import { cx } from '../../utils/cx'; import { Footer } from '../Footer/Footer'; import { TopNav } from '../TopNav/TopNav'; @@ -15,15 +17,19 @@ export type LayoutProps = { export function Layout({ children, contentClassName }: LayoutProps) { const pathname = usePathname(); const isHomePage = pathname === '/'; - + console.log('Page loaded', DateTime.now); return ( - {isHomePage && } + {isHomePage && ( + }> + + + )} diff --git a/packages/app-explorer/src/systems/Core/components/Search/SearchForm.tsx b/packages/app-explorer/src/systems/Core/components/Search/SearchForm.tsx index e8accce96..f74c660d2 100644 --- a/packages/app-explorer/src/systems/Core/components/Search/SearchForm.tsx +++ b/packages/app-explorer/src/systems/Core/components/Search/SearchForm.tsx @@ -8,14 +8,9 @@ import { styles } from './styles'; type SearchFormProps = { className: string; autoFocus?: boolean; - variablePosition?: boolean; }; -export function SearchForm({ - className, - autoFocus, - variablePosition, -}: SearchFormProps) { +export function SearchForm({ className, autoFocus }: SearchFormProps) { const classes = styles(); const [results, action] = useFormState( (_: GQLSearchResult | null, formData: FormData) => { @@ -27,7 +22,6 @@ export function SearchForm({ return (
& { onSubmit?: (value: string) => void; searchResult?: Maybe; alwaysDisplayActionButtons?: boolean; - variablePosition?: boolean; }; export function SearchInput({ @@ -29,7 +28,6 @@ export function SearchInput({ autoFocus, placeholder = 'Search here...', searchResult, - variablePosition, ...props }: SearchInputProps) { const classes = styles(); @@ -92,7 +90,6 @@ export function SearchInput({
diff --git a/packages/app-explorer/src/systems/Core/components/Search/SearchInput/styles.ts b/packages/app-explorer/src/systems/Core/components/Search/SearchInput/styles.ts index 339462675..77b61b852 100644 --- a/packages/app-explorer/src/systems/Core/components/Search/SearchInput/styles.ts +++ b/packages/app-explorer/src/systems/Core/components/Search/SearchInput/styles.ts @@ -6,9 +6,9 @@ export const styles = tv({ 'transition-all duration-200 [&[data-active=false]]:ease-in [&[data-active=true]]:ease-out', 'group justify-center items-center', 'block left-0 w-full', // needed for properly execution of transitions - '[&[data-variable-position=true]]:[&[data-active=true]]:w-[calc(100vw+1px)] [&[data-variable-position=true]]:[&[data-active=true]]:left-[-64px] tablet:[&[data-variable-position=true]]:[&[data-active=true]]:w-full', - '[&[data-active=true]]:absolute tablet:[&[data-variable-position=true]]:[&[data-active=true]]:left-0 [&[data-active=true]]:right-0', - '[&[data-variable-position=true]]:[&[data-active=true]]:top-[-14px] [&[data-variable-position=true]]:tablet:[&[data-active=true]]:top-[-4px] [&[data-variable-position=true]]:desktop:[&[data-active=true]]:top-[-20px]', + '[&[data-active=true]]:w-[calc(100vw+1px)] [&[data-active=true]]:left-[-64px] tablet:[&[data-active=true]]:w-full', + '[&[data-active=true]]:absolute tablet:[&[data-active=true]]:left-0 [&[data-active=true]]:right-0', + '[&[data-active=true]]:top-[-14px] tablet:[&[data-active=true]]:top-[-4px] desktop:[&[data-active=true]]:top-[-20px]', '[&[data-active=true]]:z-50', ], inputContainer: 'w-full', diff --git a/packages/app-explorer/src/systems/Core/components/Search/SearchWidget.tsx b/packages/app-explorer/src/systems/Core/components/Search/SearchWidget.tsx index e596bcaef..144a01d88 100644 --- a/packages/app-explorer/src/systems/Core/components/Search/SearchWidget.tsx +++ b/packages/app-explorer/src/systems/Core/components/Search/SearchWidget.tsx @@ -12,24 +12,16 @@ export const SearchContext = createContext<{ type SearchWidgetProps = { autoFocus?: boolean; - variablePosition?: boolean; }; -export const SearchWidget = ({ - autoFocus, - variablePosition, -}: SearchWidgetProps) => { +export const SearchWidget = ({ autoFocus }: SearchWidgetProps) => { const classes = styles(); const dropdownRef = useRef(null); return ( - + ); diff --git a/packages/app-explorer/src/systems/Core/components/TopNav/TopNav.tsx b/packages/app-explorer/src/systems/Core/components/TopNav/TopNav.tsx index e2bbaa02e..5fda4722c 100644 --- a/packages/app-explorer/src/systems/Core/components/TopNav/TopNav.tsx +++ b/packages/app-explorer/src/systems/Core/components/TopNav/TopNav.tsx @@ -22,7 +22,6 @@ export function TopNav() { PortalRoutes.bridgeHistory, ]); const isEcosystemBridge = isRoute(pathname, [PortalRoutes.ecosystem]); - const isHomePage = pathname === '/'; const isExplorer = !isBridge && !isEcosystemBridge; useEffect(() => { @@ -39,16 +38,6 @@ export function TopNav() { ); - const externalLinks = ( - <> - Developers - Community - - Labs - - - ); - const tooling = ( <> {logo} - {externalLinks} - {!isHomePage && } + + + {tooling} {themeToggle} @@ -91,13 +81,10 @@ export function TopNav() { {logo} - {!isHomePage && } + {themeToggle} - - {externalLinks} - {tooling} - + {tooling} ); 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..597fc2560 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/BlockTableTile.tsx @@ -0,0 +1,77 @@ +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

+
+
+

+ {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..8420475ff --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/DailyTransaction.tsx @@ -0,0 +1,141 @@ +import { ChartConfig, RoundedContainer } from '@fuels/ui'; +import dayjs from 'dayjs'; +import { DateTime } from 'fuels'; +import { + CartesianGrid, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +const chartConfig = { + desktop: { + label: 'Desktop', + color: '#00F58C', + }, +} satisfies ChartConfig; + +interface DailyTransactionProps { + blocks: any; +} + +const DailyTransaction = (blocks: DailyTransactionProps) => { + const chartData = blocks.blocks?.reduce( + (acc: { [key: string]: number }, block: any) => { + const time = dayjs(Number(block.time)).format('HH:mm'); + const value = +block.value; + acc[time] = (acc[time] || 0) + value; + return acc; + }, + {}, + ); + + const chartDataArray = chartData + ? Object.entries(chartData).map(([time, value]) => ({ + time, + 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.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/index.tsx b/packages/app-explorer/src/systems/Home/components/DataTable/index.tsx new file mode 100644 index 000000000..753d5d79e --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/DataTable/index.tsx @@ -0,0 +1,34 @@ +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) => ( + + + + ))} + + + + + + ); +}; +export default DataTable; 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..0ce8f0907 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/GasSpentChart/index.tsx @@ -0,0 +1,144 @@ +import { ChartConfig, HStack, RoundedContainer } from '@fuels/ui'; +import dayjs from 'dayjs'; +import { + CartesianGrid, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +const chartConfig = { + fuel: { + label: 'FUEL', + color: '#00F58C', // Light green for FUEL + }, +} satisfies ChartConfig; + +interface GasSpentProps { + blocks: any; +} +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; + + return ( + +
+
+
+
+
+ Gas Spent + + + + + +
+
+ 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(); + }} + /> + + + + +
+
+ {/*
+
+
ETH
+
*/} + {/*
+
+
FUEL
+
*/} +
+ + ); +}; +export default GasSpentChart; diff --git a/packages/app-explorer/src/systems/Home/components/Hero/Hero.stories.tsx b/packages/app-explorer/src/systems/Home/components/Hero/Hero.stories.tsx new file mode 100644 index 000000000..4cb0c1532 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/Hero/Hero.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Hero } from '~/systems/Block/components/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/Hero.tsx b/packages/app-explorer/src/systems/Home/components/Hero/Hero.tsx index 9730ad906..a21015491 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,229 @@ -'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 React, { Suspense, 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 { Block } from '../../interface/blocks.interface'; +import { getBlocksDashboard } from './actions/get-blocks-dashboard'; +import { getTPS } from './actions/get-tps'; + +const DailyTransaction = React.lazy(() => import('../DailyTransaction')); +const GasSpentChart = React.lazy(() => import('../GasSpentChart/index')); +const LatestBlock = React.lazy(() => import('../LatestBlock')); +const TPS = React.lazy(() => import('../TPS')); +const TotalDapps = React.lazy(() => import('../TotalDapps/TotalDapps')); +const DataTable = React.lazy(() => import('../../components/DataTable')); -export function Hero() { +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, dashboard] = await Promise.all([ + getTPS(), + getBlocksDashboard(), + ]); + setIsLoading(false); + + setTpsData(result); + setBlocksData(dashboard); + + if (isFirstFetch) { + setIsLoading(false); + setIsFirstFetch(false); + } + } catch (_e) {} + }; + + useEffect(() => { + getTPSData(); + 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, + })); + + const tpsTsxData = tps?.map((t: any) => ({ + time: t.start ?? '', + value: t.txCount, + })); + + 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 + + + +
+ } + 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', + ], }, }); +export default Hero; 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 index ac4622aa0..b3bae9f68 100644 --- 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 @@ -1,3 +1,4 @@ +'use server'; import { sdk } from '~/systems/Core/utils/sdk'; export const getBlocksDashboard = async () => { 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 index 467c7a701..a97708e91 100644 --- 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 @@ -1,3 +1,4 @@ +'use server'; import { sdk } from '~/systems/Core/utils/sdk'; export const getTPS = async () => { 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..5feb06800 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/LatestBlock.tsx @@ -0,0 +1,64 @@ +import { HStack, RoundedContainer, VStack } from '@fuels/ui'; + +import { fromNow } from '~/systems/Core/utils/dayjs'; +import { Block } from '../interface/blocks.interface'; + +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 +

+
+
+ + ); +}; +export default LatestBlock; 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..86dd94b34 --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/TPS.tsx @@ -0,0 +1,151 @@ +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.reduce( + (acc: { [key: string]: number }, block: any) => { + const time = dayjs(Number(block.time)).format('HH:mm'); + const value = +block.value / 3600; + acc[time] = (acc[time] || 0) + value; + return acc; + }, + {}, + ); + + const chartDataArray = chartData + ? Object.entries(chartData).map(([time, value]) => ({ + time, + value, + })) + : []; + + const averageTPS = + blocks.reduce((sum: any, block: any) => sum + Number(block.value), 0) / 24; + + const highestValue = Math.max( + ...chartDataArray.map((data: any) => Number(data.value)), + ); + + const getTicks = () => { + const ticks: string[] = []; + for (let i = 0; i < chartDataArray.length; i += 6) { + ticks.push(chartDataArray[i].time); + } + return ticks; + }; + + return ( + +
+
+
+
+
+ TPS + + + + + +
+
+ Transactions Per Second processed by the Fuel network. +
+
+
+
+ + 24h + +
+ +

+ {`${(averageTPS / 3600).toFixed(2)}`} +

+
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 }} + /> + + {chartDataArray.map((entry, index) => ( + + ))} + + + +
+ + ); +}; +export default TPS; 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..b44ecf67f --- /dev/null +++ b/packages/app-explorer/src/systems/Home/components/TotalDapps/TotalDapps.tsx @@ -0,0 +1,140 @@ +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} +

{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/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/Transactions/components/TxsTitle/TxsTitle.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsTitle/TxsTitle.tsx index eaefa40c2..0f242c8ee 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,3 @@ -'use client'; import { PageTitle } from 'app-commons'; export function TxsTitle() { diff --git a/packages/app-portal/src/systems/Ecosystem/data/projects.json b/packages/app-portal/src/systems/Ecosystem/data/projects.json index e5179d63c..ddff0c9a7 100644 --- a/packages/app-portal/src/systems/Ecosystem/data/projects.json +++ b/packages/app-portal/src/systems/Ecosystem/data/projects.json @@ -38,20 +38,21 @@ "url": "https://www.bako.id/", "tags": ["Social"], "description": "Bako ID is a comprehensive Identity System for Rollups.", - "github": "", + "github": "https://github.com/Bako-Labs", "twitter": "https://x.com/bakoidentity", - "discord": "", + "discord": "https://discord.com/invite/E5kYKSKncj", "image": "bakoid" }, { "isLive": true, + "isFeatured": true, "name": "Bako Safe", "url": "https://www.bako.global/", "tags": ["Wallet", "Multi-sig", "Tooling"], "description": "Bako Safe is the Native Multisig of Rollup OS.", - "github": "", + "github": "https://github.com/Bako-Labs", "twitter": "https://x.com/bakosafe", - "discord": "", + "discord": "https://discord.com/invite/E5kYKSKncj", "image": "bakosafe" }, { @@ -153,17 +154,6 @@ "discord": "https://discord.gg/cookbookdev", "image": "cookbook" }, - { - "isLive": false, - "name": "Everclear", - "url": "https://www.everclear.org/", - "tags": ["Bridge"], - "description": "Everclear coordinates the global settlement of liquidity between chains, solving fragmentation for modular blockchains.", - "github": "https://github.com/everclearorg", - "twitter": "https://x.com/everclearorg", - "discord": "https://discord.gg/everclear", - "image": "everclear" - }, { "isLive": false, "name": "CryptoSage", @@ -186,6 +176,28 @@ "discord": "https://discord.gg/amcY528zbZ", "image": "envio" }, + { + "isLive": false, + "name": "Everclear", + "url": "https://www.everclear.org/", + "tags": ["Bridge"], + "description": "Everclear coordinates the global settlement of liquidity between chains, solving fragmentation for modular blockchains.", + "github": "https://github.com/everclearorg", + "twitter": "https://x.com/everclearorg", + "discord": "https://discord.gg/everclear", + "image": "everclear" + }, + { + "isLive": false, + "name": "Executoors", + "url": "https://executoors.com", + "tags": ["NFT"], + "description": "A series of Executoors in search of the most flawless execution environment", + "github": "", + "twitter": "https://x.com/executoors", + "discord": "", + "image": "executoors" + }, { "isLive": false, "isFeatured": true, diff --git a/packages/graphql/src/graphql/resolvers/PublicResolver.ts b/packages/graphql/src/graphql/resolvers/PublicResolver.ts index d6be75910..7935510ac 100644 --- a/packages/graphql/src/graphql/resolvers/PublicResolver.ts +++ b/packages/graphql/src/graphql/resolvers/PublicResolver.ts @@ -1,5 +1,6 @@ import { Provider } from 'fuels'; import { env } from '~/config'; +import VerifiedAssets from '~/infra/cache/VerifiedAssets'; import AssetDAO from '~/infra/dao/AssetDAO'; type Params = { @@ -20,10 +21,8 @@ export class PublicResolver { const assetDAO = new AssetDAO(); const provider = await Provider.create(env.get('FUEL_PROVIDER')); const chainId = provider.getChainId(); - const response = await fetch( - 'https://verified-assets.fuel.network/assets.json', - ); - const verifiedAssets = await response.json(); + const nonVerifiedAsset = await assetDAO.getByAssetId(_params.assetId); + const verifiedAssets = await VerifiedAssets.getInstance().fetch(); for (const verifiedAsset of verifiedAssets) { for (const network of verifiedAsset.networks) { if (network.type === 'fuel') { @@ -43,6 +42,7 @@ export class PublicResolver { const asset = Object.assign(verifiedAsset, { assetId: _params.assetId, contractId: network.contractId, + subId: nonVerifiedAsset?.subId, decimals: network.decimals, verified: true, }); @@ -50,9 +50,8 @@ export class PublicResolver { } } } - const asset = await assetDAO.getByAssetId(_params.assetId); - if (!asset) return; - return Object.assign(asset, { + if (!nonVerifiedAsset) return; + return Object.assign(nonVerifiedAsset, { verified: false, }); } diff --git a/packages/graphql/src/infra/cache/VerifiedAssets.ts b/packages/graphql/src/infra/cache/VerifiedAssets.ts new file mode 100644 index 000000000..c2faef9f8 --- /dev/null +++ b/packages/graphql/src/infra/cache/VerifiedAssets.ts @@ -0,0 +1,31 @@ +export default class VerifiedAssets { + static instance: VerifiedAssets; + private lastDate?: Date; + private assets: any; + + private constructor() {} + + async fetch() { + const now = new Date(); + if ( + !this.lastDate || + !this.assets || + now.getTime() - this.lastDate.getTime() > 60000 + ) { + this.lastDate = new Date(); + const response = await fetch( + 'https://verified-assets.fuel.network/assets.json', + ); + this.assets = await response.json(); + return this.assets; + } + return this.assets; + } + + static getInstance() { + if (!VerifiedAssets.instance) { + VerifiedAssets.instance = new VerifiedAssets(); + } + return VerifiedAssets.instance; + } +} diff --git a/packages/ui/package.json b/packages/ui/package.json index f48c0edd4..21567c3b3 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -71,6 +71,7 @@ "react-data-table-component": "7.6.2", "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", 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 ( +