feat: unify settings and wallet into single user menu

Move settings from standalone TopBar gear icon into the user profile
dropdown. The menu always renders (for settings access) and includes
wallet connect/disconnect when blockchain interaction is enabled.

- Remove Settings component from TopBar
- Integrate SettingsColorTheme, SettingsIdentIcon, SettingsAddressFormat,
  SettingsScamTokens, SettingsLocalTime into UserWalletMenuContent
- UserProfileDesktop always renders (no null fallback)
- UserWalletButton shows profile icon + "Menu" when no wallet connected
- Mobile header also always shows user menu
- Optimize Dockerfile.lux dependency layer
This commit is contained in:
Hanzo Dev
2026-02-25 12:28:17 -08:00
parent 8f5e9f1a8f
commit b741213e94
8 changed files with 151 additions and 136 deletions
+11 -31
View File
@@ -6,35 +6,21 @@ RUN corepack enable && corepack prepare pnpm@10.11.0 --activate
WORKDIR /app
# Copy package files
# Install dependencies (cacheable layer)
# Copy files needed by postinstall (chakra typegen) before install
COPY package.json pnpm-lock.yaml .npmrc ./
COPY stubs ./stubs
COPY tsconfig.json ./
COPY next.config.js ./
# Copy necessary directories
COPY types ./types
COPY lib ./lib
COPY configs ./configs
COPY toolkit ./toolkit
COPY ui ./ui
COPY pages ./pages
COPY nextjs ./nextjs
COPY theme ./theme
COPY icons ./icons
COPY mocks ./mocks
COPY playwright ./playwright
COPY deploy ./deploy
COPY public ./public
COPY stubs ./stubs
# Copy environment file
COPY .env.production .env.local
# Install dependencies
COPY configs/app ./configs/app
COPY toolkit/theme ./toolkit/theme
COPY toolkit/utils ./toolkit/utils
COPY toolkit/components/forms/validators/url.ts ./toolkit/components/forms/validators/url.ts
RUN pnpm install --frozen-lockfile
# Build the application
# Copy remaining source and build
COPY . .
ENV NODE_OPTIONS="--max-old-space-size=8192"
RUN pnpm build
@@ -48,19 +34,13 @@ WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy built application
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
# Set permissions
RUN chown -R nextjs:nodejs /app
COPY --from=builder /app/package.json ./package.json
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV NODE_ENV=production
CMD ["node", "server.js"]
+3 -4
View File
@@ -34,12 +34,11 @@ const HeaderMobile = ({ hideSearchButton, onGoToSearchResults }: Props) => {
case 'dynamic':
return <UserProfileDynamic/>;
default:
return null;
break;
}
}
if (config.features.blockchainInteraction.isEnabled) {
return <UserWalletMobile/>;
}
// Always render wallet/settings menu
return <UserWalletMobile/>;
})();
return (
+6 -13
View File
@@ -1,4 +1,4 @@
import { Flex, Separator, Box, HStack } from '@chakra-ui/react';
import { Flex, Box, HStack } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
@@ -6,7 +6,6 @@ import { CONTENT_MAX_WIDTH } from 'ui/shared/layout/utils';
import DeFiDropdown from './DeFiDropdown';
import NetworkMenu from './NetworkMenu';
import Settings from './settings/Settings';
const TopBar = () => {
const hasDeFiDropdown = Boolean(config.features.deFiDropdown.isEnabled);
@@ -25,17 +24,11 @@ const TopBar = () => {
<HStack gap={ 0 } fontSize="xs">
{ Boolean(config.UI.featuredNetworks.items) && <NetworkMenu/> }
</HStack>
<HStack
alignItems="center"
separator={ <Separator mx={{ base: 2, lg: 3 }} height={ 4 }/> }
>
{ hasDeFiDropdown && (
<HStack>
<DeFiDropdown/>
</HStack>
) }
<Settings/>
</HStack>
{ hasDeFiDropdown && (
<HStack alignItems="center">
<DeFiDropdown/>
</HStack>
) }
</Flex>
</Box>
);
+3 -5
View File
@@ -21,13 +21,11 @@ const UserProfileDesktop = ({ buttonSize, buttonVariant = 'header' }: Props) =>
case 'dynamic':
return <UserProfileDynamic buttonSize={ buttonSize } buttonVariant={ buttonVariant }/>;
default:
return null;
break;
}
}
if (config.features.blockchainInteraction.isEnabled) {
return <UserWalletDesktop buttonSize={ buttonSize } buttonVariant={ buttonVariant }/>;
}
return null;
// Always render the wallet/settings menu — it handles both wallet and settings
return <UserWalletDesktop buttonSize={ buttonSize } buttonVariant={ buttonVariant }/>;
};
export default UserProfileDesktop;
+12 -7
View File
@@ -6,6 +6,7 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import shortenString from 'lib/shortenString';
import { Button } from 'toolkit/chakra/button';
import { Tooltip } from 'toolkit/chakra/tooltip';
import IconSvg from 'ui/shared/IconSvg';
import UserIdenticon from '../UserIdenticon';
@@ -23,23 +24,27 @@ const UserWalletButton = ({ size, variant, isPending, isAutoConnectDisabled, add
const isMobile = useIsMobile();
const content = (() => {
if (!address) {
return 'Connect';
if (address) {
const text = domain || shortenString(address);
return (
<HStack gap={ 2 }>
<UserIdenticon address={ address } isAutoConnectDisabled={ isAutoConnectDisabled }/>
<Box display={{ base: 'none', md: 'block' }}>{ text }</Box>
</HStack>
);
}
const text = domain || shortenString(address);
return (
<HStack gap={ 2 }>
<UserIdenticon address={ address } isAutoConnectDisabled={ isAutoConnectDisabled }/>
<Box display={{ base: 'none', md: 'block' }}>{ text }</Box>
<IconSvg name="profile" boxSize={ 5 }/>
<Box display={{ base: 'none', md: 'block' }}>Menu</Box>
</HStack>
);
})();
return (
<Tooltip
content="Connect your wallet to Blockscout for full-featured access"
content="Settings & wallet"
disabled={ isMobile || Boolean(address) }
openDelay={ 500 }
disableOnMobile
+25 -26
View File
@@ -1,6 +1,7 @@
import { type ButtonProps } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import { useMarketplaceContext } from 'lib/contexts/marketplace';
import useWeb3AccountWithDomain from 'lib/web3/useAccountWithDomain';
import useWeb3Wallet from 'lib/web3/useWallet';
@@ -15,16 +16,19 @@ interface Props {
buttonVariant?: ButtonProps['variant'];
}
const isWalletEnabled = config.features.blockchainInteraction.isEnabled;
const UserWalletDesktop = ({ buttonSize, buttonVariant = 'header' }: Props) => {
const walletMenu = useDisclosure();
const web3Wallet = useWeb3Wallet({ source: 'Header' });
const web3AccountWithDomain = useWeb3AccountWithDomain(web3Wallet.isConnected);
const web3AccountWithDomain = useWeb3AccountWithDomain(isWalletEnabled && web3Wallet.isConnected);
const { isAutoConnectDisabled } = useMarketplaceContext();
const isPending =
const isPending = isWalletEnabled && (
(web3Wallet.isConnected && web3AccountWithDomain.isLoading) ||
(!web3Wallet.isConnected && web3Wallet.isOpen);
(!web3Wallet.isConnected && web3Wallet.isOpen)
);
const handleOpenWalletClick = React.useCallback(() => {
web3Wallet.openModal();
@@ -37,44 +41,39 @@ const UserWalletDesktop = ({ buttonSize, buttonVariant = 'header' }: Props) => {
}, [ web3Wallet, walletMenu ]);
const handleOpenChange = React.useCallback(({ open }: { open: boolean }) => {
if (!web3Wallet.isConnected) {
web3Wallet.openModal();
return;
}
if (open) {
walletMenu.onOpen();
} else {
walletMenu.onClose();
}
}, [ walletMenu, web3Wallet ]);
}, [ walletMenu ]);
return (
<PopoverRoot positioning={{ placement: 'bottom-end' }} lazyMount open={ walletMenu.open } onOpenChange={ handleOpenChange }>
<PopoverRoot positioning={{ placement: 'bottom-end' }} lazyMount={ false } open={ walletMenu.open } onOpenChange={ handleOpenChange }>
<PopoverTrigger>
<UserWalletButton
size={ buttonSize }
variant={ buttonVariant }
address={ web3AccountWithDomain.address }
domain={ web3AccountWithDomain.domain }
address={ isWalletEnabled ? web3AccountWithDomain.address : undefined }
domain={ isWalletEnabled ? web3AccountWithDomain.domain : undefined }
isPending={ isPending }
isAutoConnectDisabled={ isAutoConnectDisabled }
/>
</PopoverTrigger>
{ web3AccountWithDomain.address && walletMenu.open && (
<PopoverContent w="235px">
<PopoverBody>
<UserWalletMenuContent
address={ web3AccountWithDomain.address }
domain={ web3AccountWithDomain.domain }
isAutoConnectDisabled={ isAutoConnectDisabled }
isReconnecting={ web3Wallet.isReconnecting }
onOpenWallet={ handleOpenWalletClick }
onDisconnect={ handleDisconnectClick }
/>
</PopoverBody>
</PopoverContent>
) }
<PopoverContent w="280px">
<PopoverBody>
<UserWalletMenuContent
address={ isWalletEnabled ? web3AccountWithDomain.address : undefined }
domain={ isWalletEnabled ? web3AccountWithDomain.domain : undefined }
isAutoConnectDisabled={ isAutoConnectDisabled }
isReconnecting={ isWalletEnabled ? web3Wallet.isReconnecting : false }
isWalletEnabled={ isWalletEnabled }
onOpenWallet={ handleOpenWalletClick }
onDisconnect={ handleDisconnectClick }
onCloseMenu={ walletMenu.onClose }
/>
</PopoverBody>
</PopoverContent>
</PopoverRoot>
);
};
@@ -1,4 +1,4 @@
import { Box, Flex, Spinner, Text } from '@chakra-ui/react';
import { Box, Flex, Separator, Spinner, Text, VStack } from '@chakra-ui/react';
import React from 'react';
import delay from 'lib/delay';
@@ -6,53 +6,95 @@ import { Button } from 'toolkit/chakra/button';
import { IconButton } from 'toolkit/chakra/icon-button';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import IconSvg from 'ui/shared/IconSvg';
import SettingsAddressFormat from 'ui/snippets/topBar/settings/SettingsAddressFormat';
import SettingsColorTheme from 'ui/snippets/topBar/settings/SettingsColorTheme';
import SettingsIdentIcon from 'ui/snippets/topBar/settings/SettingsIdentIcon';
import SettingsLocalTime from 'ui/snippets/topBar/settings/SettingsLocalTime';
import SettingsScamTokens from 'ui/snippets/topBar/settings/SettingsScamTokens';
import UserWalletAutoConnectAlert from '../UserWalletAutoConnectAlert';
interface Props {
address: string;
address?: string;
domain?: string;
isAutoConnectDisabled?: boolean;
isReconnecting?: boolean;
isWalletEnabled?: boolean;
onDisconnect: () => void;
onOpenWallet: () => void;
onCloseMenu: () => void;
}
const UserWalletMenuContent = ({ isAutoConnectDisabled, address, domain, isReconnecting, onDisconnect, onOpenWallet }: Props) => {
const UserWalletMenuContent = ({
isAutoConnectDisabled,
address,
domain,
isReconnecting,
isWalletEnabled,
onDisconnect,
onOpenWallet,
onCloseMenu,
}: Props) => {
const handleOpenWalletClick = React.useCallback(async() => {
await delay(100);
onOpenWallet();
}, [ onOpenWallet ]);
const handleConnectClick = React.useCallback(() => {
onCloseMenu();
onOpenWallet();
}, [ onCloseMenu, onOpenWallet ]);
return (
<Box>
{ isAutoConnectDisabled && <UserWalletAutoConnectAlert/> }
<Text fontSize="sm" fontWeight={ 600 } mb={ 1 }>My wallet</Text>
<Text fontSize="sm" mb={ 5 } fontWeight={ 400 } color="text.secondary">
Your wallet is used to interact with apps and contracts in the explorer.
</Text>
<Flex alignItems="center" columnGap={ 2 } justifyContent="space-between">
<AddressEntity
address={{ hash: address, ens_domain_name: domain }}
truncation="dynamic"
fontSize="sm"
fontWeight={ 700 }
/>
{ isReconnecting ? <Spinner size="sm" m="2px" flexShrink={ 0 }/> : (
<IconButton
aria-label="Open wallet"
variant="icon_secondary"
size="2xs"
onClick={ handleOpenWalletClick }
>
<IconSvg name="gear"/>
</IconButton>
) }
</Flex>
<Button size="sm" width="full" variant="outline" onClick={ onDisconnect } mt={ 6 }>
Disconnect
</Button>
{ /* Wallet section */ }
{ isWalletEnabled && address && (
<>
{ isAutoConnectDisabled && <UserWalletAutoConnectAlert/> }
<Text fontSize="sm" fontWeight={ 600 } mb={ 1 }>My wallet</Text>
<Flex alignItems="center" columnGap={ 2 } justifyContent="space-between">
<AddressEntity
address={{ hash: address, ens_domain_name: domain }}
truncation="dynamic"
fontSize="sm"
fontWeight={ 700 }
/>
{ isReconnecting ? <Spinner size="sm" m="2px" flexShrink={ 0 }/> : (
<IconButton
aria-label="Open wallet"
variant="icon_secondary"
size="2xs"
onClick={ handleOpenWalletClick }
>
<IconSvg name="gear"/>
</IconButton>
) }
</Flex>
<Button size="sm" width="full" variant="outline" onClick={ onDisconnect } mt={ 3 }>
Disconnect
</Button>
<Separator my={ 3 }/>
</>
) }
{ isWalletEnabled && !address && (
<>
<Button size="sm" width="full" variant="outline" onClick={ handleConnectClick }>
Connect wallet
</Button>
<Separator my={ 3 }/>
</>
) }
{ /* Settings section */ }
<SettingsColorTheme onSelect={ onCloseMenu }/>
<Separator my={ 3 }/>
<SettingsIdentIcon/>
<SettingsAddressFormat/>
<Separator my={ 3 }/>
<VStack gap={ 1 }>
<SettingsScamTokens/>
<SettingsLocalTime/>
</VStack>
</Box>
);
};
+20 -21
View File
@@ -1,5 +1,6 @@
import React from 'react';
import config from 'configs/app';
import { useMarketplaceContext } from 'lib/contexts/marketplace';
import useWeb3AccountWithDomain from 'lib/web3/useAccountWithDomain';
import useWeb3Wallet from 'lib/web3/useWallet';
@@ -9,16 +10,19 @@ import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import UserWalletButton from './UserWalletButton';
import UserWalletMenuContent from './UserWalletMenuContent';
const isWalletEnabled = config.features.blockchainInteraction.isEnabled;
const UserWalletMobile = () => {
const walletMenu = useDisclosure();
const web3Wallet = useWeb3Wallet({ source: 'Header' });
const web3AccountWithDomain = useWeb3AccountWithDomain(web3Wallet.isConnected);
const web3AccountWithDomain = useWeb3AccountWithDomain(isWalletEnabled && web3Wallet.isConnected);
const { isAutoConnectDisabled } = useMarketplaceContext();
const isPending =
const isPending = isWalletEnabled && (
(web3Wallet.isConnected && web3AccountWithDomain.isLoading) ||
(!web3Wallet.isConnected && web3Wallet.isOpen);
(!web3Wallet.isConnected && web3Wallet.isOpen)
);
const handleOpenWalletClick = React.useCallback(() => {
web3Wallet.openModal();
@@ -31,17 +35,12 @@ const UserWalletMobile = () => {
}, [ web3Wallet, walletMenu ]);
const handleOpenChange = React.useCallback(({ open }: { open: boolean }) => {
if (!web3Wallet.isConnected) {
web3Wallet.openModal();
return;
}
if (open) {
walletMenu.onOpen();
} else {
walletMenu.onClose();
}
}, [ walletMenu, web3Wallet ]);
}, [ walletMenu ]);
return (
<DrawerRoot
@@ -51,23 +50,23 @@ const UserWalletMobile = () => {
<DrawerTrigger>
<UserWalletButton
variant="header"
address={ web3AccountWithDomain.address }
domain={ web3AccountWithDomain.domain }
address={ isWalletEnabled ? web3AccountWithDomain.address : undefined }
domain={ isWalletEnabled ? web3AccountWithDomain.domain : undefined }
isPending={ isPending }
/>
</DrawerTrigger>
<DrawerContent maxWidth="300px">
<DrawerBody p={ 6 }>
{ web3AccountWithDomain.address && walletMenu.open && (
<UserWalletMenuContent
address={ web3AccountWithDomain.address }
domain={ web3AccountWithDomain.domain }
isAutoConnectDisabled={ isAutoConnectDisabled }
isReconnecting={ web3Wallet.isReconnecting }
onOpenWallet={ handleOpenWalletClick }
onDisconnect={ handleDisconnectClick }
/>
) }
<UserWalletMenuContent
address={ isWalletEnabled ? web3AccountWithDomain.address : undefined }
domain={ isWalletEnabled ? web3AccountWithDomain.domain : undefined }
isAutoConnectDisabled={ isAutoConnectDisabled }
isReconnecting={ isWalletEnabled ? web3Wallet.isReconnecting : false }
isWalletEnabled={ isWalletEnabled }
onOpenWallet={ handleOpenWalletClick }
onDisconnect={ handleDisconnectClick }
onCloseMenu={ walletMenu.onClose }
/>
</DrawerBody>
</DrawerContent>
</DrawerRoot>