mirror of
https://github.com/luxfi/explore.git
synced 2026-07-27 03:13:50 +00:00
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:
+11
-31
@@ -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"]
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user