From 45164f73e300a6122114d8a658926246a7302985 Mon Sep 17 00:00:00 2001 From: Raghu Kapur <64493087+raghukapur9@users.noreply.github.com> Date: Fri, 27 Sep 2024 09:29:10 -0400 Subject: [PATCH] feat: update homepage (#589) Co-authored-by: Aashay Kapoor <38524076+aashaykapoor@users.noreply.github.com> Co-authored-by: aashaykapoor-bimaplan <104904081+aashaykapoor-bimaplan@users.noreply.github.com> Co-authored-by: shivam-25 Co-authored-by: ankit723 --- package.json | 5 + packages/app-explorer/package.json | 2 + packages/app-explorer/src/app/layout.tsx | 2 +- .../src/systems/Block/screens/BlockScreen.tsx | 29 +- .../systems/Core/components/Layout/Layout.tsx | 16 +- .../Home/components/BlockTableTile.tsx | 77 +++ .../Home/components/DailyTransaction.tsx | 141 ++++++ .../Home/components/DataTable/GridTable.tsx | 131 +++++ .../Home/components/DataTable/index.tsx | 34 ++ .../Home/components/GasSpentChart/index.tsx | 144 ++++++ .../Home/components/Hero/Hero.stories.tsx | 35 ++ .../src/systems/Home/components/Hero/Hero.tsx | 227 ++++++++- .../Hero/actions/get-blocks-dashboard.ts | 1 + .../Home/components/Hero/actions/get-tps.ts | 1 + .../systems/Home/components/LatestBlock.tsx | 64 +++ .../src/systems/Home/components/TPS.tsx | 151 ++++++ .../Home/components/TotalDapps/TotalDapps.tsx | 140 ++++++ .../components/TrendingCard/TrendingCard.tsx | 31 ++ .../Home/components/TrendingCardCarousel.tsx | 132 +++++ .../Home/interface/blocks.interface.ts | 6 + .../components/TxsTitle/TxsTitle.tsx | 1 - packages/ui/package.json | 1 + .../src/components/Box/RoundedContainer.tsx | 25 + packages/ui/src/components/Box/index.tsx | 3 + .../ui/src/components/Carousel/Carousel.tsx | 195 ++++++++ packages/ui/src/components/Carousel/index.tsx | 11 + packages/ui/src/components/Charts/Charts.tsx | 366 ++++++++++++++ packages/ui/src/components/Charts/index.tsx | 10 + packages/ui/src/index.ts | 4 +- packages/ui/src/theme/tailwind-preset.ts | 6 + pnpm-lock.yaml | 461 ++++++++++++++---- 31 files changed, 2321 insertions(+), 131 deletions(-) create mode 100644 packages/app-explorer/src/systems/Home/components/BlockTableTile.tsx create mode 100644 packages/app-explorer/src/systems/Home/components/DailyTransaction.tsx create mode 100644 packages/app-explorer/src/systems/Home/components/DataTable/GridTable.tsx create mode 100644 packages/app-explorer/src/systems/Home/components/DataTable/index.tsx create mode 100644 packages/app-explorer/src/systems/Home/components/GasSpentChart/index.tsx create mode 100644 packages/app-explorer/src/systems/Home/components/Hero/Hero.stories.tsx create mode 100644 packages/app-explorer/src/systems/Home/components/LatestBlock.tsx create mode 100644 packages/app-explorer/src/systems/Home/components/TPS.tsx create mode 100644 packages/app-explorer/src/systems/Home/components/TotalDapps/TotalDapps.tsx create mode 100644 packages/app-explorer/src/systems/Home/components/TrendingCard/TrendingCard.tsx create mode 100644 packages/app-explorer/src/systems/Home/components/TrendingCardCarousel.tsx create mode 100644 packages/app-explorer/src/systems/Home/interface/blocks.interface.ts create mode 100644 packages/ui/src/components/Box/RoundedContainer.tsx create mode 100644 packages/ui/src/components/Carousel/Carousel.tsx create mode 100644 packages/ui/src/components/Carousel/index.tsx create mode 100644 packages/ui/src/components/Charts/Charts.tsx create mode 100644 packages/ui/src/components/Charts/index.tsx 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/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/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 3c96f6422..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,36 +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 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/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 ( +