diff --git a/components/Layout/Layout.tsx b/components/Layout/Layout.tsx index da6fbb6..704f9c2 100644 --- a/components/Layout/Layout.tsx +++ b/components/Layout/Layout.tsx @@ -1,7 +1,7 @@ import { SnackbarProvider } from 'notistack' import React, { useMemo } from 'react' -import { Box, GlobalStyles, NoSsr, darken, useMediaQuery, useTheme } from '@mui/material' +import { Box, GlobalStyles, NoSsr, lighten, useMediaQuery, useTheme } from '@mui/material' import { ConsentBlock } from 'components/consent/ConsentBlock' @@ -98,7 +98,7 @@ const Layout = ({ children, hasSearchBar = true, pageTitle }: LayoutProps) => { height: '8px', }, '*::-webkit-scrollbar-thumb': { - backgroundColor: darken(`${theme.palette.border?.level2}55`, 0.3), + backgroundColor: lighten(`${theme.palette.border?.level3}55`, 0.4), borderRadius: '8px', }, '*::-webkit-scrollbar-track': { diff --git a/components/Layout/components/Sidebar/data.tsx b/components/Layout/components/Sidebar/data.tsx index 69fa0dc..026ac45 100644 --- a/components/Layout/components/Sidebar/data.tsx +++ b/components/Layout/components/Sidebar/data.tsx @@ -26,6 +26,7 @@ export enum PAGES { HOME = 'Home', EXPLORE = 'Explore', LEADERBOARD = 'Leaderboard', + CONTRACTS_LEADERBOARD = 'Contracts Leaderboard', RECENT_ACTIVITY = 'Recent Activity', DASHBOARD = 'Dashboard', MEMPOOL = 'Mempool', @@ -40,6 +41,7 @@ export enum PAGES { TERMS_OF_SERVICE = 'Terms Of Service', CHANGELOG = 'Changelog', RESOURCES = 'Resources', + TOKENS = 'Tokens', } /** diff --git a/components/Layout/components/TopBar/Buttons/WalletButton.tsx b/components/Layout/components/TopBar/Buttons/WalletButton.tsx index 423f474..b32c1c6 100644 --- a/components/Layout/components/TopBar/Buttons/WalletButton.tsx +++ b/components/Layout/components/TopBar/Buttons/WalletButton.tsx @@ -17,14 +17,12 @@ interface WalletButtonProps { } /** - * Buttons component. + * WalletButton component. * - * This component provides the interface for the top bar buttons. + * This component renders a button in the top bar for wallet interactions. * - * @param menuItems - The menu items. - * @param setMenuItems - The function to set the menu items. - * - * @returns The JSX element of the Buttons component. + * @param {WalletButtonProps} props - The props for the component. + * @returns {JSX.Element} The rendered component. */ const WalletButton: React.FC = ({ level = 'second' }) => { const { t } = useTranslation() @@ -32,19 +30,17 @@ const WalletButton: React.FC = ({ level = 'second' }) => { const { isConnected, provider, gatherData, openWallet, setOpenWallet, switchChain } = useWalletStore(s => s) const { filAddr, ethAddr, network: walletNetwork } = useWalletStore(s => s.walletInfo) const { network } = useAppSettingsStore(state => ({ network: state.network })) - const [open, setOpen] = useState(false) + const [open, setOpen] = useState(false) - const address = useMemo(() => { - return filAddr ?? ethAddr - }, [filAddr, ethAddr]) + const address = useMemo(() => filAddr ?? ethAddr, [filAddr, ethAddr]) const handleClick = useCallback(() => { setOpen(prev => !prev) - }, [setOpen]) + }, []) const handleClickAway = useCallback(() => { setOpen(false) - }, [setOpen]) + }, []) useEffect(() => { if (isConnected && walletNetwork) { @@ -53,20 +49,20 @@ const WalletButton: React.FC = ({ level = 'second' }) => { subscribeNatsSync(network, 'mempool') } else { switchChain(network) - handleClick() + setOpen(true) } } }, [walletNetwork, gatherData, isConnected, network, switchChain, handleClick]) useEffect(() => { if (openWallet) { - handleClick() + setOpen(true) setOpenWallet(false) } - }, [handleClick, openWallet, setOpenWallet]) + }, [openWallet, setOpenWallet]) - return ( - + const renderWallet = useCallback(() => { + return ( = ({ level = 'second' }) => { - - ) + ) + }, [address, handleClick, isConnected, level, network, open, provider, t, theme]) + + if (open) { + return {renderWallet()} + } + + return renderWallet() } export default WalletButton diff --git a/components/Layout/components/TopBar/Buttons/components/NotificationsPopup.tsx b/components/Layout/components/TopBar/Buttons/components/NotificationsPopup.tsx index df37883..9e70818 100644 --- a/components/Layout/components/TopBar/Buttons/components/NotificationsPopup.tsx +++ b/components/Layout/components/TopBar/Buttons/components/NotificationsPopup.tsx @@ -91,7 +91,7 @@ const NotificationsPopup = () => { }} > {notifications.map(notification => ( - + ))} diff --git a/components/Layout/components/TopBar/MenuItems.tsx b/components/Layout/components/TopBar/MenuItems.tsx index 08499c8..a3b36b0 100644 --- a/components/Layout/components/TopBar/MenuItems.tsx +++ b/components/Layout/components/TopBar/MenuItems.tsx @@ -8,6 +8,7 @@ import { ChevronDown, ChevronUp, Cube, + Currency, Document, ExecutableProgram, GasStation, @@ -26,25 +27,87 @@ import { FIL2ETHIcon } from 'components/common/Icons' const MenuItems: React.FC = () => { const theme = useTheme() const { t } = useTranslation() - const [currentHoveredMenu, setCurrentHoveredMenu] = React.useState(null) + const [currentOpenedMenu, setCurrentOpenedMenu] = React.useState(null) /** - * Handles hovering over a menu item by updating the current hovered menu number. + * Handles opening a menu item by updating the current hovered menu number. * @param menuNumber The number of the menu item being hovered over. */ - const handleHoverMenu = useCallback( - (menuNumber: number) => { - setCurrentHoveredMenu(menuNumber) + const handleOpenMenu = useCallback( + (menuId: string) => { + if (currentOpenedMenu?.includes(menuId)) { + return + } + setCurrentOpenedMenu(prev => { + if (prev === null) { + return [menuId] + } + const isSubMenu = menuId.includes('.') + if (isSubMenu) { + return prev.includes(menuId) ? prev : [...prev, menuId] + } + // Clear other primary menus if this is a primary menu + const filteredPrev = prev.filter(id => id.includes('.')) + return filteredPrev.includes(menuId) ? filteredPrev : [menuId, ...filteredPrev] + }) }, - [setCurrentHoveredMenu] + [currentOpenedMenu] ) /** - * Handles mouse leave event on the menu by resetting the current hovered menu to null. + * Handles closing a menu item by updating the current hovered menu number. */ - const handleMouseLeaveMenu = useCallback(() => { - setCurrentHoveredMenu(null) - }, [setCurrentHoveredMenu]) + const handleCloseMenu = useCallback( + (menuId: string) => { + setCurrentOpenedMenu(prev => { + if (prev === null) { + return null + } + return prev.includes(menuId) ? prev.filter(item => item !== menuId && !item.startsWith(menuId + '.')) : prev + }) + }, + [setCurrentOpenedMenu] + ) + + /** + * Handles toggling a menu item by updating the current hovered menu number. + */ + const handleToggleMenu = useCallback( + (menuId: string) => { + setCurrentOpenedMenu(prev => { + if (prev === null) { + return null + } + return prev.includes(menuId) ? prev.filter(item => item !== menuId) : [...prev, menuId] + }) + }, + [setCurrentOpenedMenu] + ) + + /** + * Handles closing all menu items by clearing the current hovered menu state. + */ + const handleCloseAllMenus = useCallback(() => { + setCurrentOpenedMenu(null) + }, [setCurrentOpenedMenu]) + + /** + * Handles mouse hover over a menu item by updating the current hovered menu state. + */ + const handleMouseOver = useCallback( + (menuId: string) => { + if (currentOpenedMenu?.includes(menuId)) { + return + } + setCurrentOpenedMenu(prev => { + if (prev === null) { + return [menuId] + } + return prev.includes(menuId) ? prev.filter(item => item !== menuId) : [...prev, menuId] + }) + }, + [currentOpenedMenu] + ) /** * Component representing a menu option with hover functionality. @@ -52,34 +115,70 @@ const MenuItems: React.FC = () => { * @param name The name of the menu option. * @param children The content of the menu option. */ - const MenuOption = ({ itemNumber, name, children }: { itemNumber: number; name: string; children: ReactNode }) => ( + const MenuOption = ({ + menuId, + name, + icon, + children, + nested = false, + }: { + menuId: string + name: string + icon?: ReactNode + children: ReactNode + nested?: boolean + }) => ( { + if (!nested) { + handleCloseMenu(menuId) + } else { + event.preventDefault() + } + }} > handleHoverMenu(itemNumber)} - onMouseOut={handleMouseLeaveMenu} + onMouseOver={() => (!nested ? handleMouseOver(menuId) : null)} + onMouseLeave={() => (!nested ? handleCloseMenu(menuId) : null)} > { flexDirection: 'column', gap: '0.5rem', backgroundColor: theme.palette.background.level0, - border: '1px solid', + border: nested ? 'none' : '1px solid', borderColor: theme.palette.border?.level0, - borderRadius: '8px', - padding: '0.5rem', + borderRadius: '10px', + padding: nested ? '0 0 0 2rem' : '0.5rem', + '& *': { + borderRadius: '6px', + }, }} > {children} @@ -100,7 +202,7 @@ const MenuItems: React.FC = () => { ) return ( - + handleCloseAllMenus()}> { justifyContent: 'space-between', }} > - - + + handleCloseMenu('1')} component="a" href="/recent_activity?tab=tipsets"> {t('Tipsets')} - + handleCloseMenu('1')} component="a" href="/recent_activity?tab=transactions"> {t('Transactions')} - - {t('Contracts')} + handleCloseMenu('1')} component="a" href="/recent_activity?tab=contracts"> + {t('Contract Invokes')} - + handleCloseMenu('1')} component="a" href="/tokens"> + {t('Tokens')} + + handleCloseMenu('1')} component="a" href="/mempool"> {t('Mempool')} - - - {t('Rich List')} - - + + } + nested + > + handleCloseMenu('2')} component="a" href="/leaderboard?tab=rich-list"> + {t('Top Accounts by Balance')} + + handleCloseMenu('2')} component="a" href="/leaderboard?tab=top-accounts"> + {t('Top Accounts by Gas Used')} + + handleCloseMenu('2')} component="a" href="/leaderboard?tab=top-accounts-by-value-exchanged"> + {t('Top Accounts by Value Exchanged')} + + + + } + nested + > + handleCloseMenu('2')} component="a" href="/contracts_leaderboard?tab=richest-contracts"> + {t('Top Contracts by Balance')} + + handleCloseMenu('2')} component="a" href="/contracts_leaderboard?tab=top-contracts"> + {t('Top Contracts by Unique Users')} + + handleCloseMenu('2')} component="a" href="/contracts_leaderboard?tab=top-contracts-by-invokes"> + {t('Top Contracts by Invokes')} + + handleCloseMenu('2')} component="a" href="/contracts_leaderboard?tab=top-contracts-by-value-exchanged"> + {t('Top Contracts by Value Exchanged')} + + + + handleCloseMenu('2')} component="a" href="/dashboard#gas-used-stats"> {t('Gas Stats')} - + handleCloseMenu('2')} component="a" href="/dashboard#contract-stats"> {t('Contract Stats')} - + handleCloseMenu('2')} component="a" href="/mempool?tab=stats"> {t('Mempool Stats')} - + handleCloseMenu('3')} sx={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }} component="a" href="/address_converter" > {t('Address Converter')} - + handleCloseMenu('3')} component="a" href="/rpc"> {t('RPC')} - + handleCloseMenu('3')} component="a" href="/faucet"> {t('Faucet')} - + handleCloseMenu('3')} component="a" href="/estimate_gas"> {t('Gas Estimator')} - + handleCloseMenu('3')} component="a" href="/interact"> {' '} {t('Contract Interaction')} - + handleCloseMenu('3')} component="a" href="/contract_verifier"> {' '} {t('Contract Verification')} - + handleCloseMenu('3')} component="a" href="/resources"> {t('Resources')} diff --git a/components/Layout/components/TopBar/NetworkSelectorBox/NetworkSelector.tsx b/components/Layout/components/TopBar/NetworkSelectorBox/NetworkSelector.tsx index a7b6642..4972274 100644 --- a/components/Layout/components/TopBar/NetworkSelectorBox/NetworkSelector.tsx +++ b/components/Layout/components/TopBar/NetworkSelectorBox/NetworkSelector.tsx @@ -85,7 +85,7 @@ const NetworkSelector: React.FC = ({ buttonSize = 'medium' setNetwork(network) switchChain(network) - if (router.route.indexOf('/search/') > -1) { + if (router.route.indexOf('/fil/') > -1) { router.push('/') } handleCloseMenu() diff --git a/components/Layout/components/TopBar/Stats/Price.tsx b/components/Layout/components/TopBar/Stats/Price.tsx index 6e8c2fb..59248d6 100644 --- a/components/Layout/components/TopBar/Stats/Price.tsx +++ b/components/Layout/components/TopBar/Stats/Price.tsx @@ -32,10 +32,9 @@ const Price: React.FC = () => { {t('FIL price')} - + { {t('Latest Tipset')} - + - + { + const theme = useTheme() + + return ( + + + + ) +} + +export default BetaLabel diff --git a/components/common/Charts/LineChart.tsx b/components/common/Charts/LineChart.tsx index 190ded4..5ff5105 100644 --- a/components/common/Charts/LineChart.tsx +++ b/components/common/Charts/LineChart.tsx @@ -30,6 +30,8 @@ interface LineChartProps { cumulative?: boolean onChartClick?: (params: any) => void hideDataZoom?: boolean + basicChart?: boolean + tooltipHour?: boolean } /** @@ -40,7 +42,7 @@ interface LineChartProps { * * @returns - The JSX element of the LineChart component. */ -const LineChart = ({ data, onChartClick, color, cumulative, hideDataZoom = false }: LineChartProps) => { +const LineChart = ({ data, onChartClick, color, cumulative, hideDataZoom = false, basicChart, tooltipHour }: LineChartProps) => { /** * @type {Theme} theme - The theme of the component. */ @@ -115,7 +117,7 @@ const LineChart = ({ data, onChartClick, color, cumulative, hideDataZoom = false const chartOptions = { grid: { - top: '15%', + top: basicChart ? '80%' : '15%', right: upLg ? '8%' : '5%', left: upLg ? '12%' : '11%', }, @@ -129,12 +131,14 @@ const LineChart = ({ data, onChartClick, color, cumulative, hideDataZoom = false zIndex: 200, formatter: (param: string) => (data.x.formatter ? data.x.formatter(param, 'MMM dd') : '{value}'), }, + show: !basicChart, }, yAxis: { zIndex: 200, type: 'value', name: data.y.unit ? data.y.unit : '', nameLocation: 'end', + show: !basicChart, nameTextStyle: { align: 'right', fontWeight: 600, @@ -143,6 +147,7 @@ const LineChart = ({ data, onChartClick, color, cumulative, hideDataZoom = false lineStyle: { color: theme.palette.background.level2, }, + show: !basicChart, }, axisLabel: { formatter: (value: number) => { @@ -165,7 +170,7 @@ const LineChart = ({ data, onChartClick, color, cumulative, hideDataZoom = false }, }, }, - tooltip: getTooltip(data, theme), + tooltip: getTooltip(data, theme, tooltipHour), dataZoom: getDataZoom(hideDataZoom, theme), series, darkMode: true, diff --git a/components/common/CodeView.test.tsx b/components/common/CodeView.test.tsx index 5491cf9..bac02f5 100644 --- a/components/common/CodeView.test.tsx +++ b/components/common/CodeView.test.tsx @@ -1,4 +1,4 @@ -import { act } from 'react-dom/test-utils' +import { act } from 'react' import { renderWithProviders } from '@/helpers/jest-react' import '@testing-library/jest-dom' @@ -14,7 +14,7 @@ describe('CodeView', () => { // Test Case: Check if CodeView component renders without crashing it('renders without crashing', async () => { // Render CodeView Component within Provider and ThemeProvider context - await renderWithProviders() + await renderWithProviders() // Check button with testId='viewTransactionButton' is in the document expect(screen.getByTestId('viewTransactionButton')).toBeInTheDocument() diff --git a/components/common/CodeView.tsx b/components/common/CodeView.tsx index 91ff499..0c261ab 100644 --- a/components/common/CodeView.tsx +++ b/components/common/CodeView.tsx @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import { fetchTransactionDetails } from '@/api-client/beryx' +import { fetchEventDetails, fetchTransactionDetails } from '@/api-client/beryx' import { useSearchStore } from '@/store/data/search' import { getContentType } from '@/utils/download' import { Close, Script } from '@carbon/icons-react' @@ -11,6 +11,8 @@ import CodeBlock from '../widgets/CodeBlock' import Panel from '../widgets/Panel' import { NoRows } from '../widgets/Table' +export type CodeViewTypes = 'transaction' | 'event' + /** * Props for CodeView component */ @@ -18,6 +20,7 @@ interface CodeViewProps { content: { search_id: string } + type: CodeViewTypes } /** @@ -26,7 +29,7 @@ interface CodeViewProps { * @param content - the content of the code to display * @returns the CodeView component */ -const CodeView = ({ content }: CodeViewProps) => { +const CodeView = ({ content, type }: CodeViewProps) => { const theme = useTheme() const { t } = useTranslation() const [openCodeModal, setOpenCodeModal] = useState(false) @@ -37,7 +40,7 @@ const CodeView = ({ content }: CodeViewProps) => { const network = useSearchStore(s => s.searchInputNetwork) /** - * Opens the modal, fetches transaction details if a search id exists, sets error state if an error occurs + * Opens the modal, fetches transaction or event details if a search id exists, sets error state if an error occurs */ const handleOpenModal = useCallback(async () => { setError(false) @@ -48,7 +51,10 @@ const CodeView = ({ content }: CodeViewProps) => { } setLoading(true) try { - const res = await fetchTransactionDetails(content.search_id, network) + const res = + type === 'transaction' + ? await fetchTransactionDetails(content.search_id, network) + : await fetchEventDetails(content.search_id, network) if (res !== 'error') { setTxDetails(res) } @@ -56,7 +62,7 @@ const CodeView = ({ content }: CodeViewProps) => { setError(true) } setLoading(false) - }, [content, network, setError, setTxDetails, setOpenCodeModal, setLoading]) + }, [content, network, type, setError, setTxDetails, setOpenCodeModal, setLoading]) /** * Closes the modal @@ -87,8 +93,8 @@ const CodeView = ({ content }: CodeViewProps) => { return ( <> - - + +