Commit 729a74c1 authored by tom's avatar tom

refactor UserProfileMenu

parent 09efb570
...@@ -27,7 +27,10 @@ const RESTRICTED_MODULES = { ...@@ -27,7 +27,10 @@ const RESTRICTED_MODULES = {
{ name: 'playwright/TestApp', message: 'Please use render() fixture from test() function of playwright/lib module' }, { name: 'playwright/TestApp', message: 'Please use render() fixture from test() function of playwright/lib module' },
{ {
name: '@chakra-ui/react', name: '@chakra-ui/react',
importNames: [ 'Menu', 'useToast', 'useDisclosure' ], importNames: [
'Menu', 'useToast', 'useDisclosure', 'useClipboard', 'Tooltip', 'Skeleton', 'IconButton', 'Button',
'Image',
],
message: 'Please use corresponding component or hook from ui/shared/chakra component instead', message: 'Please use corresponding component or hook from ui/shared/chakra component instead',
}, },
{ {
......
...@@ -13,7 +13,6 @@ import type { ...@@ -13,7 +13,6 @@ import type {
RewardsConfigResponse, RewardsConfigResponse,
} from 'types/api/rewards'; } from 'types/api/rewards';
import { toaster } from 'toolkit/chakra/toaster';
import config from 'configs/app'; import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
...@@ -26,6 +25,7 @@ import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; ...@@ -26,6 +25,7 @@ import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import removeQueryParam from 'lib/router/removeQueryParam'; import removeQueryParam from 'lib/router/removeQueryParam';
import useAccount from 'lib/web3/useAccount'; import useAccount from 'lib/web3/useAccount';
import { toaster } from 'toolkit/chakra/toaster';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery'; import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
const feature = config.features.rewards; const feature = config.features.rewards;
......
/* eslint-disable no-restricted-imports */
import type { ButtonProps as ChakraButtonProps } from '@chakra-ui/react'; import type { ButtonProps as ChakraButtonProps } from '@chakra-ui/react';
import { import {
AbsoluteCenter, AbsoluteCenter,
......
...@@ -8,6 +8,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>( ...@@ -8,6 +8,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
function IconButton(props, ref) { function IconButton(props, ref) {
return ( return (
<Button <Button
display="inline-flex"
px="0" px="0"
py="0" py="0"
height="auto" height="auto"
......
import { Tag as ChakraTag } from '@chakra-ui/react';
import * as React from 'react';
export interface TagProps extends ChakraTag.RootProps {
startElement?: React.ReactNode;
endElement?: React.ReactNode;
onClose?: VoidFunction;
closable?: boolean;
}
export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
function Tag(props, ref) {
const {
startElement,
endElement,
onClose,
closable = Boolean(onClose),
children,
...rest
} = props;
return (
<ChakraTag.Root ref={ ref } { ...rest }>
{ startElement && (
<ChakraTag.StartElement>{ startElement }</ChakraTag.StartElement>
) }
<ChakraTag.Label>{ children }</ChakraTag.Label>
{ endElement && (
<ChakraTag.EndElement>{ endElement }</ChakraTag.EndElement>
) }
{ closable && (
<ChakraTag.EndElement>
<ChakraTag.CloseTrigger onClick={ onClose }/>
</ChakraTag.EndElement>
) }
</ChakraTag.Root>
);
},
);
/* eslint-disable no-restricted-imports */
import { Tooltip as ChakraTooltip, Portal } from '@chakra-ui/react'; import { Tooltip as ChakraTooltip, Portal } from '@chakra-ui/react';
import { useClickAway } from '@uidotdev/usehooks'; import { useClickAway } from '@uidotdev/usehooks';
import * as React from 'react'; import * as React from 'react';
......
import { useCopyToClipboard } from '@uidotdev/usehooks';
import React from 'react';
export default function useClipboard(text: string, timeout?: number) {
const timeoutRef = React.useRef<number | null>(null);
const [ hasCopied, setHasCopied ] = React.useState(false);
const [ , copyToClipboard ] = useCopyToClipboard();
const copy = React.useCallback(() => {
copyToClipboard(text);
setHasCopied(true);
timeoutRef.current = window.setTimeout(() => {
setHasCopied(false);
}, timeout);
}, [ text, copyToClipboard, timeout ]);
React.useEffect(() => {
return () => {
if (timeoutRef.current) {
window.clearTimeout(timeoutRef.current);
}
};
}, []);
return React.useMemo(() => {
return {
hasCopied,
copy,
};
}, [ hasCopied, copy ]);
}
...@@ -9,6 +9,7 @@ export const recipe = defineSlotRecipe({ ...@@ -9,6 +9,7 @@ export const recipe = defineSlotRecipe({
borderRadius: 'sm', borderRadius: 'sm',
fontWeight: '500', fontWeight: '500',
textStyle: 'sm', textStyle: 'sm',
textAlign: 'center',
boxShadow: 'size.md', boxShadow: 'size.md',
zIndex: 'tooltip', zIndex: 'tooltip',
maxW: '320px', maxW: '320px',
......
import type { ButtonProps } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import { Button, chakra, Tooltip } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import { useRewardsContext } from 'lib/contexts/rewards'; import { useRewardsContext } from 'lib/contexts/rewards';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import type { ButtonProps } from 'toolkit/chakra/button';
import { Button } from 'toolkit/chakra/button';
import { Tooltip } from 'toolkit/chakra/tooltip';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/links/LinkInternal'; import LinkInternal from 'ui/shared/links/LinkInternal';
type Props = { type Props = {
size?: ButtonProps['size']; size?: ButtonProps['size'];
variant?: ButtonProps['variant']; visual?: ButtonProps['visual'];
}; };
// TODO @tom2drum chekc this component // TODO @tom2drum check this component
const RewardsButton = ({ variant = 'header', size }: Props) => { const RewardsButton = ({ visual = 'header', size }: Props) => {
const { isInitialized, apiToken, openLoginModal, dailyRewardQuery, balancesQuery } = useRewardsContext(); const { isInitialized, apiToken, openLoginModal, dailyRewardQuery, balancesQuery } = useRewardsContext();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isLoading = !isInitialized || dailyRewardQuery.isLoading || balancesQuery.isLoading; const isLoading = !isInitialized || dailyRewardQuery.isLoading || balancesQuery.isLoading;
...@@ -27,15 +29,13 @@ const RewardsButton = ({ variant = 'header', size }: Props) => { ...@@ -27,15 +29,13 @@ const RewardsButton = ({ variant = 'header', size }: Props) => {
return ( return (
<Tooltip <Tooltip
label="Earn Merits for using Blockscout" content="Earn Merits for using Blockscout"
textAlign="center"
padding={ 2 }
openDelay={ 500 } openDelay={ 500 }
isDisabled={ isMobile || isLoading || Boolean(apiToken) } disabled={ isMobile || isLoading || Boolean(apiToken) }
width="150px" // width="150px"
> >
<Button <Button
variant={ variant } visual={ visual }
data-selected={ !isLoading && Boolean(apiToken) } data-selected={ !isLoading && Boolean(apiToken) }
flexShrink={ 0 } flexShrink={ 0 }
as={ apiToken ? LinkInternal : 'button' } as={ apiToken ? LinkInternal : 'button' }
...@@ -45,19 +45,18 @@ const RewardsButton = ({ variant = 'header', size }: Props) => { ...@@ -45,19 +45,18 @@ const RewardsButton = ({ variant = 'header', size }: Props) => {
fontSize="sm" fontSize="sm"
size={ size } size={ size }
px={ !isLoading && Boolean(apiToken) ? 2.5 : 4 } px={ !isLoading && Boolean(apiToken) ? 2.5 : 4 }
isLoading={ isLoading } loading={ isLoading }
_hover={{ _hover={{
textDecoration: 'none', textDecoration: 'none',
}} }}
> >
<IconSvg <IconSvg
name={ dailyRewardQuery.data?.available ? 'merits_with_dot_slim' : 'merits_slim' } name={ dailyRewardQuery.data?.available ? 'merits_with_dot_slim' : 'merits_slim' }
boxSize={ variant === 'hero' ? 6 : 5 } boxSize={ visual === 'hero' ? 6 : 5 }
flexShrink={ 0 } flexShrink={ 0 }
/> />
<chakra.span <chakra.span
display={{ base: 'none', md: 'inline' }} display={{ base: 'none', md: 'inline' }}
ml={ 2 }
fontWeight={ apiToken ? '700' : '600' } fontWeight={ apiToken ? '700' : '600' }
> >
{ apiToken ? (balancesQuery.data?.total || 'N/A') : 'Merits' } { apiToken ? (balancesQuery.data?.total || 'N/A') : 'Merits' }
......
import { FormControl, Input, InputGroup, InputRightElement, Skeleton, chakra } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { InputGroup } from 'toolkit/chakra/input-group';
import Input from 'theme/components/Input';
import { Skeleton } from 'toolkit/chakra/skeleton';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import FormInputPlaceholder from 'ui/shared/forms/inputs/FormInputPlaceholder'; import FormInputPlaceholder from 'ui/shared/forms/inputs/FormInputPlaceholder';
import { Field } from 'toolkit/chakra/field';
type Props = { type Props = {
label: string; label: string;
...@@ -12,8 +16,8 @@ type Props = { ...@@ -12,8 +16,8 @@ type Props = {
}; };
const RewardsReadOnlyInputWithCopy = ({ label, value, className, isLoading }: Props) => ( const RewardsReadOnlyInputWithCopy = ({ label, value, className, isLoading }: Props) => (
<FormControl variant="floating" id={ label } className={ className }> <Field floating id={ label } className={ className }>
<Skeleton isLoaded={ !isLoading }> <Skeleton loading={ isLoading }>
<InputGroup> <InputGroup>
<Input <Input
readOnly readOnly
......
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, useBoolean, useDisclosure } from '@chakra-ui/react'; import { DialogHeader } from '@chakra-ui/react';
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useRewardsContext } from 'lib/contexts/rewards'; import { useRewardsContext } from 'lib/contexts/rewards';
import useIsMobile from 'lib/hooks/useIsMobile';
import useWallet from 'lib/web3/useWallet'; import useWallet from 'lib/web3/useWallet';
import { DialogBody, DialogContent, DialogRoot } from 'toolkit/chakra/dialog';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import AuthModal from 'ui/snippets/auth/AuthModal'; import AuthModal from 'ui/snippets/auth/AuthModal';
import CongratsStepContent from './steps/CongratsStepContent'; import CongratsStepContent from './steps/CongratsStepContent';
...@@ -20,61 +21,65 @@ const MIXPANEL_CONFIG = { ...@@ -20,61 +21,65 @@ const MIXPANEL_CONFIG = {
const RewardsLoginModal = () => { const RewardsLoginModal = () => {
const { isOpen: isWalletModalOpen } = useWallet({ source: 'Merits' }); const { isOpen: isWalletModalOpen } = useWallet({ source: 'Merits' });
const isMobile = useIsMobile(); const { isLoginModalOpen, closeLoginModal, openLoginModal } = useRewardsContext();
const { isLoginModalOpen, closeLoginModal } = useRewardsContext();
const [ isLoginStep, setIsLoginStep ] = useBoolean(true); const [ isLoginStep, setIsLoginStep ] = React.useState(true);
const [ isReferral, setIsReferral ] = useBoolean(false); const [ isReferral, setIsReferral ] = React.useState(false);
const [ isAuth, setIsAuth ] = useBoolean(false); const [ isAuth, setIsAuth ] = React.useState(false);
const authModal = useDisclosure(); const authModal = useDisclosure();
useEffect(() => { useEffect(() => {
if (!isLoginModalOpen) { if (!isLoginModalOpen) {
setIsLoginStep.on(); setIsLoginStep(true);
setIsReferral.off(); setIsReferral(false);
} }
}, [ isLoginModalOpen, setIsLoginStep, setIsReferral ]); }, [ isLoginModalOpen, setIsLoginStep, setIsReferral ]);
const goNext = useCallback((isReferral: boolean) => { const goNext = useCallback((isReferral: boolean) => {
if (isReferral) { if (isReferral) {
setIsReferral.on(); setIsReferral(true);
} }
setIsLoginStep.off(); setIsLoginStep(false);
}, [ setIsLoginStep, setIsReferral ]); }, [ setIsLoginStep, setIsReferral ]);
const handleOpenChange = React.useCallback(({ open }: { open: boolean }) => {
if (open) {
openLoginModal();
} else {
closeLoginModal();
}
}, [ closeLoginModal, openLoginModal ]);
const handleAuthModalOpen = useCallback((isAuth: boolean) => { const handleAuthModalOpen = useCallback((isAuth: boolean) => {
setIsAuth[isAuth ? 'on' : 'off'](); setIsAuth(isAuth);
authModal.onOpen(); authModal.onOpen();
}, [ authModal, setIsAuth ]); }, [ authModal, setIsAuth ]);
const handleAuthModalClose = useCallback(() => { const handleAuthModalClose = useCallback(() => {
setIsAuth.off(); setIsAuth(false);
authModal.onClose(); authModal.onClose();
}, [ authModal, setIsAuth ]); }, [ authModal, setIsAuth ]);
return ( return (
<> <>
<Modal <DialogRoot
isOpen={ isLoginModalOpen && !isWalletModalOpen && !authModal.isOpen } open={ isLoginModalOpen && !isWalletModalOpen && !authModal.open }
onClose={ closeLoginModal } onOpenChange={ handleOpenChange }
size={ isMobile ? 'full' : 'sm' } size={{ base: 'full', md: isLoginStep ? 'sm' : 'md' }}
isCentered
> >
<ModalOverlay/> <DialogContent>
<ModalContent width={ isLoginStep ? '400px' : '560px' } p={ 6 }> <DialogHeader>
<ModalHeader fontWeight="500" textStyle="h3" mb={ 3 }>
{ isLoginStep ? 'Login' : 'Congratulations' } { isLoginStep ? 'Login' : 'Congratulations' }
</ModalHeader> </DialogHeader>
<ModalCloseButton top={ 6 } right={ 6 }/> <DialogBody>
<ModalBody mb={ 0 }>
{ isLoginStep ? { isLoginStep ?
<LoginStepContent goNext={ goNext } openAuthModal={ handleAuthModalOpen } closeModal={ closeLoginModal }/> : <LoginStepContent goNext={ goNext } openAuthModal={ handleAuthModalOpen } closeModal={ closeLoginModal }/> :
<CongratsStepContent isReferral={ isReferral }/> <CongratsStepContent isReferral={ isReferral }/>
} }
</ModalBody> </DialogBody>
</ModalContent> </DialogContent>
</Modal> </DialogRoot>
{ authModal.isOpen && ( { authModal.open && (
<AuthModal <AuthModal
onClose={ handleAuthModalClose } onClose={ handleAuthModalClose }
initialScreen={{ type: 'connect_wallet', isAuth }} initialScreen={{ type: 'connect_wallet', isAuth }}
......
import { Text, Box, Flex, Button, Skeleton, useColorModeValue, Tag } from '@chakra-ui/react'; import { Text, Box, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import { useRewardsContext } from 'lib/contexts/rewards'; import { useRewardsContext } from 'lib/contexts/rewards';
import { Button } from 'toolkit/chakra/button';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tag } from 'toolkit/chakra/tag';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import MeritsIcon from '../../MeritsIcon'; import MeritsIcon from '../../MeritsIcon';
...@@ -13,6 +16,7 @@ type Props = { ...@@ -13,6 +16,7 @@ type Props = {
isReferral: boolean; isReferral: boolean;
}; };
// TODO @tom2drum fix this component
const CongratsStepContent = ({ isReferral }: Props) => { const CongratsStepContent = ({ isReferral }: Props) => {
const { referralsQuery, rewardsConfigQuery } = useRewardsContext(); const { referralsQuery, rewardsConfigQuery } = useRewardsContext();
...@@ -23,14 +27,13 @@ const CongratsStepContent = ({ isReferral }: Props) => { ...@@ -23,14 +27,13 @@ const CongratsStepContent = ({ isReferral }: Props) => {
const refLink = referralsQuery.data?.link || 'N/A'; const refLink = referralsQuery.data?.link || 'N/A';
const shareText = `I joined the @blockscoutcom Merits Program and got my first ${ registrationReward || 'N/A' } #Merits! Use this link for a sign-up bonus and start earning rewards with @blockscoutcom block explorer.\n\n${ refLink }`; // eslint-disable-line max-len const shareText = `I joined the @blockscoutcom Merits Program and got my first ${ registrationReward || 'N/A' } #Merits! Use this link for a sign-up bonus and start earning rewards with @blockscoutcom block explorer.\n\n${ refLink }`; // eslint-disable-line max-len
const textColor = useColorModeValue('blue.700', 'blue.100'); const textColor = { _light: 'blue.700', _dark: 'blue.100' };
const dividerColor = useColorModeValue('whiteAlpha.800', 'whiteAlpha.100');
return ( return (
<> <>
<Flex <Flex
alignItems="center" alignItems="center"
background={ useColorModeValue('linear-gradient(254.96deg, #9CD8FF 9.09%, #D0EFFF 88.45%)', 'linear-gradient(255deg, #1B253B 9.09%, #222C3F 88.45%)') } background={{ _light: 'linear-gradient(254.96deg, #9CD8FF 9.09%, #D0EFFF 88.45%)', _dark: 'linear-gradient(255deg, #1B253B 9.09%, #222C3F 88.45%)' }}
borderRadius="md" borderRadius="md"
p={ 2 } p={ 2 }
pl={{ base: isReferral ? 4 : 8, md: 8 }} pl={{ base: isReferral ? 4 : 8, md: 8 }}
...@@ -38,14 +41,14 @@ const CongratsStepContent = ({ isReferral }: Props) => { ...@@ -38,14 +41,14 @@ const CongratsStepContent = ({ isReferral }: Props) => {
h="90px" h="90px"
> >
<MeritsIcon boxSize={{ base: isReferral ? 8 : 12, md: 12 }} mr={{ base: isReferral ? 1 : 2, md: 2 }}/> <MeritsIcon boxSize={{ base: isReferral ? 8 : 12, md: 12 }} mr={{ base: isReferral ? 1 : 2, md: 2 }}/>
<Skeleton isLoaded={ !rewardsConfigQuery.isLoading }> <Skeleton loading={ rewardsConfigQuery.isLoading }>
<Text fontSize={{ base: isReferral ? '24px' : '30px', md: '30px' }} fontWeight="700" color={ textColor }> <Text fontSize={{ base: isReferral ? '24px' : '30px', md: '30px' }} fontWeight="700" color={ textColor }>
+{ rewardsConfigQuery.data?.rewards[ isReferral ? 'registration_with_referral' : 'registration' ] || 'N/A' } +{ rewardsConfigQuery.data?.rewards[ isReferral ? 'registration_with_referral' : 'registration' ] || 'N/A' }
</Text> </Text>
</Skeleton> </Skeleton>
{ isReferral && ( { isReferral && (
<Flex alignItems="center" h="56px"> <Flex alignItems="center" h="56px">
<Box w="1px" h="full" bgColor={ dividerColor } mx={{ base: 3, md: 8 }}/> <Box w="1px" h="full" bgColor={{ _light: 'whiteAlpha.800', _dark: 'whiteAlpha.100' }} mx={{ base: 3, md: 8 }}/>
<Flex flexDirection="column" justifyContent="space-between" gap={ 2 }> <Flex flexDirection="column" justifyContent="space-between" gap={ 2 }>
{ [ { [
{ {
...@@ -59,7 +62,7 @@ const CongratsStepContent = ({ isReferral }: Props) => { ...@@ -59,7 +62,7 @@ const CongratsStepContent = ({ isReferral }: Props) => {
].map(({ title, value }) => ( ].map(({ title, value }) => (
<Flex key={ title } alignItems="center" gap={{ base: 1, md: 2 }}> <Flex key={ title } alignItems="center" gap={{ base: 1, md: 2 }}>
<MeritsIcon boxSize={{ base: 5, md: 6 }}/> <MeritsIcon boxSize={{ base: 5, md: 6 }}/>
<Skeleton isLoaded={ !rewardsConfigQuery.isLoading }> <Skeleton loading={ rewardsConfigQuery.isLoading }>
<Text fontSize="sm" fontWeight="700" color={ textColor }> <Text fontSize="sm" fontWeight="700" color={ textColor }>
+{ value } +{ value }
</Text> </Text>
...@@ -84,7 +87,7 @@ const CongratsStepContent = ({ isReferral }: Props) => { ...@@ -84,7 +87,7 @@ const CongratsStepContent = ({ isReferral }: Props) => {
</Flex> </Flex>
<Text fontSize="md" mt={ 2 }> <Text fontSize="md" mt={ 2 }>
Receive a{ ' ' } Receive a{ ' ' }
<Skeleton as="span" isLoaded={ !rewardsConfigQuery.isLoading }> <Skeleton as="span" loading={ rewardsConfigQuery.isLoading }>
{ rewardsConfigQuery.data?.rewards.referral_share ? { rewardsConfigQuery.data?.rewards.referral_share ?
`${ Number(rewardsConfigQuery.data?.rewards.referral_share) * 100 }%` : `${ Number(rewardsConfigQuery.data?.rewards.referral_share) * 100 }%` :
'N/A' 'N/A'
...@@ -92,17 +95,17 @@ const CongratsStepContent = ({ isReferral }: Props) => { ...@@ -92,17 +95,17 @@ const CongratsStepContent = ({ isReferral }: Props) => {
</Skeleton> </Skeleton>
{ ' ' }bonus on all Merits earned by your referrals { ' ' }bonus on all Merits earned by your referrals
</Text> </Text>
<RewardsReadOnlyInputWithCopy { /* <RewardsReadOnlyInputWithCopy
label="Referral link" label="Referral link"
value={ refLink } value={ refLink }
isLoading={ referralsQuery.isLoading } isLoading={ referralsQuery.isLoading }
mt={ 3 } mt={ 3 }
/> /> */ }
<Button <Button
as="a" as="a"
target="_blank" _target="_blank"
mt={ 6 } mt={ 6 }
isLoading={ referralsQuery.isLoading } loading={ referralsQuery.isLoading }
href={ `https://x.com/intent/tweet?text=${ encodeURIComponent(shareText) }` } href={ `https://x.com/intent/tweet?text=${ encodeURIComponent(shareText) }` }
> >
Share on <IconSvg name="social/twitter" boxSize={ 6 } ml={ 1 }/> Share on <IconSvg name="social/twitter" boxSize={ 6 } ml={ 1 }/>
......
import { Text, Button, useColorModeValue, Image, Box, Flex, Switch, useBoolean, Input, FormControl, Alert, Skeleton, Divider } from '@chakra-ui/react'; import { Text, Box, Flex, Separator } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React, { useCallback, useState, useEffect, useMemo } from 'react'; import React from 'react';
import { useRewardsContext } from 'lib/contexts/rewards'; import { useRewardsContext } from 'lib/contexts/rewards';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import useWallet from 'lib/web3/useWallet'; import useWallet from 'lib/web3/useWallet';
import FormInputPlaceholder from 'ui/shared/forms/inputs/FormInputPlaceholder'; import { Alert } from 'toolkit/chakra/alert';
import { Button } from 'toolkit/chakra/button';
import { Field } from 'toolkit/chakra/field';
import { Image } from 'toolkit/chakra/image';
import { Input } from 'toolkit/chakra/input';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Switch } from 'toolkit/chakra/switch';
import LinkExternal from 'ui/shared/links/LinkExternal'; import LinkExternal from 'ui/shared/links/LinkExternal';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery'; import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
...@@ -21,24 +27,24 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -21,24 +27,24 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
const router = useRouter(); const router = useRouter();
const { connect, isConnected, address } = useWallet({ source: 'Merits' }); const { connect, isConnected, address } = useWallet({ source: 'Merits' });
const savedRefCode = cookies.get(cookies.NAMES.REWARDS_REFERRAL_CODE); const savedRefCode = cookies.get(cookies.NAMES.REWARDS_REFERRAL_CODE);
const [ isRefCodeUsed, setIsRefCodeUsed ] = useBoolean(Boolean(savedRefCode)); const [ isRefCodeUsed, setIsRefCodeUsed ] = React.useState(Boolean(savedRefCode));
const [ isLoading, setIsLoading ] = useBoolean(false); const [ isLoading, setIsLoading ] = React.useState(false);
const [ refCode, setRefCode ] = useState(savedRefCode || ''); const [ refCode, setRefCode ] = React.useState(savedRefCode || '');
const [ refCodeError, setRefCodeError ] = useBoolean(false); const [ refCodeError, setRefCodeError ] = React.useState(false);
const { login, checkUserQuery } = useRewardsContext(); const { login, checkUserQuery } = useRewardsContext();
const profileQuery = useProfileQuery(); const profileQuery = useProfileQuery();
const isAddressMismatch = useMemo(() => const isAddressMismatch = React.useMemo(() =>
Boolean(address) && Boolean(address) &&
Boolean(profileQuery.data?.address_hash) && Boolean(profileQuery.data?.address_hash) &&
profileQuery.data?.address_hash !== address, profileQuery.data?.address_hash !== address,
[ address, profileQuery.data ]); [ address, profileQuery.data ]);
const isLoggedIntoAccountWithWallet = useMemo(() => const isLoggedIntoAccountWithWallet = React.useMemo(() =>
!profileQuery.isLoading && profileQuery.data?.address_hash, !profileQuery.isLoading && profileQuery.data?.address_hash,
[ profileQuery ]); [ profileQuery ]);
const isSignUp = useMemo(() => const isSignUp = React.useMemo(() =>
isConnected && !isAddressMismatch && !checkUserQuery.isFetching && !checkUserQuery.data?.exists, isConnected && !isAddressMismatch && !checkUserQuery.isFetching && !checkUserQuery.data?.exists,
[ isConnected, isAddressMismatch, checkUserQuery ]); [ isConnected, isAddressMismatch, checkUserQuery ]);
...@@ -46,13 +52,13 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -46,13 +52,13 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
setRefCode(event.target.value); setRefCode(event.target.value);
}, []); }, []);
const loginToRewardsProgram = useCallback(async() => { const loginToRewardsProgram = React.useCallback(async() => {
try { try {
setRefCodeError.off(); setRefCodeError(false);
setIsLoading.on(); setIsLoading(true);
const { isNewUser, invalidRefCodeError } = await login(isSignUp && isRefCodeUsed ? refCode : ''); const { isNewUser, invalidRefCodeError } = await login(isSignUp && isRefCodeUsed ? refCode : '');
if (invalidRefCodeError) { if (invalidRefCodeError) {
setRefCodeError.on(); setRefCodeError(true);
} else { } else {
if (isNewUser) { if (isNewUser) {
goNext(Boolean(refCode)); goNext(Boolean(refCode));
...@@ -62,18 +68,18 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -62,18 +68,18 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
} }
} }
} catch (error) {} } catch (error) {}
setIsLoading.off(); setIsLoading(false);
}, [ login, goNext, setIsLoading, router, closeModal, refCode, setRefCodeError, isRefCodeUsed, isSignUp ]); }, [ login, goNext, setIsLoading, router, closeModal, refCode, setRefCodeError, isRefCodeUsed, isSignUp ]);
useEffect(() => { React.useEffect(() => {
if (isSignUp && isRefCodeUsed && refCode.length > 0 && refCode.length !== 6) { if (isSignUp && isRefCodeUsed && refCode.length > 0 && refCode.length !== 6) {
setRefCodeError.on(); setRefCodeError(true);
} else { } else {
setRefCodeError.off(); setRefCodeError(false);
} }
}, [ refCode, isRefCodeUsed, isSignUp ]); // eslint-disable-line react-hooks/exhaustive-deps }, [ refCode, isRefCodeUsed, isSignUp ]);
const handleLogin = useCallback(async() => { const handleLogin = React.useCallback(async() => {
if (isLoggedIntoAccountWithWallet) { if (isLoggedIntoAccountWithWallet) {
loginToRewardsProgram(); loginToRewardsProgram();
} else { } else {
...@@ -81,7 +87,11 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -81,7 +87,11 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
} }
}, [ loginToRewardsProgram, openAuthModal, isLoggedIntoAccountWithWallet, profileQuery ]); }, [ loginToRewardsProgram, openAuthModal, isLoggedIntoAccountWithWallet, profileQuery ]);
const buttonText = useMemo(() => { const handleToggleChange = React.useCallback(() => {
setIsRefCodeUsed((prev) => !prev);
}, []);
const buttonText = React.useMemo(() => {
if (!isConnected) { if (!isConnected) {
return 'Connect wallet'; return 'Connect wallet';
} }
...@@ -107,32 +117,33 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -107,32 +117,33 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
</Box> </Box>
{ isSignUp && isLoggedIntoAccountWithWallet && ( { isSignUp && isLoggedIntoAccountWithWallet && (
<Box mb={ 6 }> <Box mb={ 6 }>
<Divider bgColor="border.divider" mb={ 6 }/> <Separator mb={ 6 }/>
<Flex w="full" alignItems="center" justifyContent="space-between"> <Flex w="full" alignItems="center" justifyContent="space-between">
I have a referral code I have a referral code
<Switch <Switch
colorScheme="blue" colorScheme="blue"
size="md" size="md"
isChecked={ isRefCodeUsed } checked={ isRefCodeUsed }
onChange={ setIsRefCodeUsed.toggle } onCheckedChange={ handleToggleChange }
aria-label="Referral code switch" aria-label="Referral code switch"
/> />
</Flex> </Flex>
{ isRefCodeUsed && ( { isRefCodeUsed && (
<> <Field
<FormControl variant="floating" id="referral-code" mt={ 3 }> label="Code"
floating
id="referral-code"
size="xl"
mt={ 3 }
invalid={ refCodeError }
helperText={ !refCodeError ? 'The code should be in format XXXXXX' : undefined }
errorText={ refCodeError ? 'Incorrect code or format' : undefined }
>
<Input <Input
fontWeight="500"
value={ refCode } value={ refCode }
onChange={ handleRefCodeChange } onChange={ handleRefCodeChange }
isInvalid={ refCodeError }
/> />
<FormInputPlaceholder text="Code"/> </Field>
</FormControl>
<Text fontSize="sm" variant="secondary" mt={ 1 } color={ refCodeError ? 'red.500' : undefined }>
{ refCodeError ? 'Incorrect code or format' : 'The code should be in format XXXXXX' }
</Text>
</>
) } ) }
</Box> </Box>
) } ) }
...@@ -142,19 +153,18 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -142,19 +153,18 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
</Alert> </Alert>
) } ) }
<Button <Button
variant="solid" visual="solid"
colorScheme="blue"
w="full" w="full"
whiteSpace="normal" whiteSpace="normal"
mb={ 4 } mb={ 4 }
onClick={ isConnected ? handleLogin : connect } onClick={ isConnected ? handleLogin : connect }
isLoading={ isLoading || profileQuery.isLoading || checkUserQuery.isFetching } loading={ isLoading || profileQuery.isLoading || checkUserQuery.isFetching }
loadingText={ isLoading ? 'Sign message in your wallet' : undefined } loadingText={ isLoading ? 'Sign message in your wallet' : undefined }
isDisabled={ isAddressMismatch || refCodeError } disabled={ isAddressMismatch || refCodeError }
> >
{ buttonText } { buttonText }
</Button> </Button>
<Text fontSize="sm" color={ useColorModeValue('blackAlpha.500', 'whiteAlpha.500') } textAlign="center"> <Text fontSize="sm" color={{ _light: 'blackAlpha.500', _dark: 'whiteAlpha.500' }} textAlign="center">
Already registered for Blockscout Merits on another network or chain? Connect the same wallet here. Already registered for Blockscout Merits on another network or chain? Connect the same wallet here.
</Text> </Text>
</> </>
......
import { IconButton, Tooltip, useClipboard, chakra, useDisclosure, Skeleton, useColorModeValue } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import React, { useEffect, useState } from 'react'; import React from 'react';
import { IconButton } from 'toolkit/chakra/icon-button';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip';
import useClipboard from 'toolkit/hooks/useClipboard';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import type { IconName } from 'ui/shared/IconSvg'; import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
...@@ -12,55 +17,59 @@ export interface Props { ...@@ -12,55 +17,59 @@ export interface Props {
size?: number; size?: number;
type?: 'link'; type?: 'link';
icon?: IconName; icon?: IconName;
variant?: string; // TODO @tom2drum check if we need this
visual?: string;
// TODO @tom2drum check if we need this
colorScheme?: string; colorScheme?: string;
} }
const CopyToClipboard = ({ text, className, isLoading, onClick, size = 5, type, icon, variant = 'simple', colorScheme }: Props) => { const CopyToClipboard = ({ text, className, isLoading, onClick, size = 5, type, icon, colorScheme }: Props) => {
const { hasCopied, onCopy } = useClipboard(text, 1000); const { hasCopied, copy } = useClipboard(text, 1000);
const [ copied, setCopied ] = useState(false); // const [ copiedText, copyToClipboard ] = useCopyToClipboard();
// const [ copied, setCopied ] = useState(false);
// TODO @tom2drum check if we need this
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107 // have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
const { isOpen, onOpen, onClose } = useDisclosure(); const { open, onOpen, onClose } = useDisclosure();
const iconColor = useColorModeValue('gray.400', 'gray.500'); const colorProps = colorScheme ? {} : { color: { _light: 'gray.400', _dark: 'gray.500' } };
const colorProps = colorScheme ? {} : { color: iconColor };
const iconName = icon || (type === 'link' ? 'link' : 'copy'); const iconName = icon || (type === 'link' ? 'link' : 'copy');
useEffect(() => { // useEffect(() => {
if (hasCopied) { // if (hasCopied) {
setCopied(true); // setCopied(true);
} else { // } else {
setCopied(false); // setCopied(false);
} // }
}, [ hasCopied ]); // }, [ hasCopied ]);
const handleClick = React.useCallback((event: React.MouseEvent) => { const handleClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation(); event.stopPropagation();
onCopy(); copy();
onClick?.(event); onClick?.(event);
}, [ onClick, onCopy ]); }, [ onClick, copy ]);
if (isLoading) { if (isLoading) {
return <Skeleton boxSize={ size } className={ className } borderRadius="sm" flexShrink={ 0 } ml={ 2 } display="inline-block"/>; return <Skeleton boxSize={ size } className={ className } borderRadius="sm" flexShrink={ 0 } ml={ 2 } display="inline-block"/>;
} }
return ( return (
<Tooltip label={ copied ? 'Copied' : `Copy${ type === 'link' ? ' link ' : ' ' }to clipboard` } isOpen={ isOpen || copied }> <Tooltip
content={ hasCopied ? 'Copied' : `Copy${ type === 'link' ? ' link ' : ' ' }to clipboard` }
// open={ hasCopied }
>
<IconButton <IconButton
{ ...colorProps } { ...colorProps }
aria-label="copy" aria-label="copy"
icon={ <IconSvg name={ iconName } boxSize={ size }/> }
boxSize={ size } boxSize={ size }
variant={ variant }
colorScheme={ colorScheme } colorScheme={ colorScheme }
display="inline-block"
flexShrink={ 0 }
onClick={ handleClick } onClick={ handleClick }
className={ className } className={ className }
onMouseEnter={ onOpen } // onMouseEnter={ onOpen }
onMouseLeave={ onClose } // onMouseLeave={ onClose }
ml={ 2 } ml={ 2 }
borderRadius={ 0 } borderRadius="none"
/> >
<IconSvg name={ iconName } boxSize={ size }/>
</IconButton>
</Tooltip> </Tooltip>
); );
}; };
......
import { Tooltip, chakra } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import type { As } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import shortenString from 'lib/shortenString'; import shortenString from 'lib/shortenString';
import { Tooltip } from 'toolkit/chakra/tooltip';
interface Props { interface Props {
hash: string; hash: string;
isTooltipDisabled?: boolean; isTooltipDisabled?: boolean;
type?: 'long' | 'short'; type?: 'long' | 'short';
as?: As; as?: React.ElementType;
} }
const HashStringShorten = ({ hash, isTooltipDisabled, as = 'span', type }: Props) => { const HashStringShorten = ({ hash, isTooltipDisabled, as = 'span', type }: Props) => {
...@@ -18,7 +18,7 @@ const HashStringShorten = ({ hash, isTooltipDisabled, as = 'span', type }: Props ...@@ -18,7 +18,7 @@ const HashStringShorten = ({ hash, isTooltipDisabled, as = 'span', type }: Props
} }
return ( return (
<Tooltip label={ hash } isDisabled={ isTooltipDisabled }> <Tooltip content={ hash } disabled={ isTooltipDisabled }>
<chakra.span as={ as }>{ shortenString(hash, charNumber) }</chakra.span> <chakra.span as={ as }>{ shortenString(hash, charNumber) }</chakra.span>
</Tooltip> </Tooltip>
); );
......
...@@ -8,13 +8,13 @@ ...@@ -8,13 +8,13 @@
// so i did it with js // so i did it with js
import type { As } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import { Tooltip, chakra } from '@chakra-ui/react';
import _debounce from 'lodash/debounce'; import _debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useRef } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import type { FontFace } from 'use-font-face-observer'; import type { FontFace } from 'use-font-face-observer';
import useFontFaceObserver from 'use-font-face-observer'; import useFontFaceObserver from 'use-font-face-observer';
import { Tooltip } from 'toolkit/chakra/tooltip';
import { BODY_TYPEFACE, HEADING_TYPEFACE } from 'toolkit/theme/foundations/typography'; import { BODY_TYPEFACE, HEADING_TYPEFACE } from 'toolkit/theme/foundations/typography';
const TAIL_LENGTH = 4; const TAIL_LENGTH = 4;
...@@ -25,7 +25,7 @@ interface Props { ...@@ -25,7 +25,7 @@ interface Props {
fontWeight?: string | number; fontWeight?: string | number;
isTooltipDisabled?: boolean; isTooltipDisabled?: boolean;
tailLength?: number; tailLength?: number;
as?: As; as?: React.ElementType;
} }
const HashStringShortenDynamic = ({ hash, fontWeight = '400', isTooltipDisabled, tailLength = TAIL_LENGTH, as = 'span' }: Props) => { const HashStringShortenDynamic = ({ hash, fontWeight = '400', isTooltipDisabled, tailLength = TAIL_LENGTH, as = 'span' }: Props) => {
...@@ -95,7 +95,13 @@ const HashStringShortenDynamic = ({ hash, fontWeight = '400', isTooltipDisabled, ...@@ -95,7 +95,13 @@ const HashStringShortenDynamic = ({ hash, fontWeight = '400', isTooltipDisabled,
if (isTruncated) { if (isTruncated) {
return ( return (
<Tooltip label={ hash } isDisabled={ isTooltipDisabled } maxW={{ base: 'calc(100vw - 8px)', lg: '400px' }}>{ content }</Tooltip> <Tooltip
content={ hash }
disabled={ isTooltipDisabled }
contentProps={{ maxW: { base: 'calc(100vw - 8px)', lg: '400px' } }}
>
{ content }
</Tooltip>
); );
} }
......
import type { TooltipProps } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import { chakra, IconButton, Tooltip, useDisclosure, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { IconButton } from 'toolkit/chakra/icon-button';
import { Skeleton } from 'toolkit/chakra/skeleton';
import type { TooltipProps } from 'toolkit/chakra/tooltip';
import { Tooltip } from 'toolkit/chakra/tooltip';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
interface Props { interface Props {
...@@ -12,13 +15,14 @@ interface Props { ...@@ -12,13 +15,14 @@ interface Props {
} }
const Hint = ({ label, className, tooltipProps, isLoading }: Props) => { const Hint = ({ label, className, tooltipProps, isLoading }: Props) => {
// TODO @tom2drum check and remove this
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107 // have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
const { isOpen, onOpen, onToggle, onClose } = useDisclosure(); // const { open, onOpen, onToggle, onClose } = useDisclosure();
const handleClick = React.useCallback((event: React.MouseEvent) => { // const handleClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation(); // event.stopPropagation();
onToggle(); // onToggle();
}, [ onToggle ]); // }, [ onToggle ]);
if (isLoading) { if (isLoading) {
return <Skeleton className={ className } boxSize={ 5 } borderRadius="sm"/>; return <Skeleton className={ className } boxSize={ 5 } borderRadius="sm"/>;
...@@ -26,25 +30,21 @@ const Hint = ({ label, className, tooltipProps, isLoading }: Props) => { ...@@ -26,25 +30,21 @@ const Hint = ({ label, className, tooltipProps, isLoading }: Props) => {
return ( return (
<Tooltip <Tooltip
label={ label } content={ label }
placement="top" positioning={{ placement: 'top' }}
maxW={{ base: 'calc(100vw - 8px)', lg: '320px' }}
isOpen={ isOpen }
{ ...tooltipProps } { ...tooltipProps }
> >
<IconButton <IconButton
colorScheme="none"
aria-label="hint" aria-label="hint"
icon={ <IconSvg name="info" w="100%" h="100%" color="icon_info" _hover={{ color: 'link_hovered' }}/> }
boxSize={ 5 } boxSize={ 5 }
variant="simple"
display="inline-block" display="inline-block"
flexShrink={ 0 }
className={ className } className={ className }
onMouseEnter={ onOpen } // onMouseEnter={ onOpen }
onMouseLeave={ onClose } // onMouseLeave={ onClose }
onClick={ handleClick } // onClick={ handleClick }
/> >
<IconSvg name="info" w="100%" h="100%" color="icon_info" _hover={{ color: 'link.primary.hover' }}/>
</IconButton>
</Tooltip> </Tooltip>
); );
}; };
......
import type { PlacementWithLogical } from '@chakra-ui/react'; import type { Placement } from '@floating-ui/dom';
import { Tooltip, useDisclosure } from '@chakra-ui/react';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import React from 'react'; import React from 'react';
import useFontFaceObserver from 'use-font-face-observer'; import useFontFaceObserver from 'use-font-face-observer';
import { Tooltip } from 'toolkit/chakra/tooltip';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import { BODY_TYPEFACE } from 'toolkit/theme/foundations/typography'; import { BODY_TYPEFACE } from 'toolkit/theme/foundations/typography';
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
label: string; label: string;
placement?: PlacementWithLogical; placement?: Placement;
} }
const TruncatedTextTooltip = ({ children, label, placement }: Props) => { const TruncatedTextTooltip = ({ children, label, placement }: Props) => {
const childRef = React.useRef<HTMLElement>(null); const childRef = React.useRef<HTMLElement>(null);
const [ isTruncated, setTruncated ] = React.useState(false); const [ isTruncated, setTruncated ] = React.useState(false);
const { isOpen, onToggle, onOpen, onClose } = useDisclosure(); const { open, onToggle, onOpen, onClose } = useDisclosure();
const isFontFaceLoaded = useFontFaceObserver([ const isFontFaceLoaded = useFontFaceObserver([
{ family: BODY_TYPEFACE }, { family: BODY_TYPEFACE },
...@@ -78,10 +79,10 @@ const TruncatedTextTooltip = ({ children, label, placement }: Props) => { ...@@ -78,10 +79,10 @@ const TruncatedTextTooltip = ({ children, label, placement }: Props) => {
if (isTruncated) { if (isTruncated) {
return ( return (
<Tooltip <Tooltip
label={ label } content={ label }
maxW={{ base: 'calc(100vw - 8px)', lg: '400px' }} contentProps={{ maxW: { base: 'calc(100vw - 8px)', lg: '400px' } }}
placement={ placement } positioning={{ placement }}
isOpen={ isOpen } open={ open }
> >
{ modifiedChildren } { modifiedChildren }
</Tooltip> </Tooltip>
......
import type { PlacementWithLogical } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import { Skeleton, chakra } from '@chakra-ui/react'; import type { Placement } from '@floating-ui/dom';
import React from 'react'; import React from 'react';
import { Skeleton } from 'toolkit/chakra/skeleton';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props { interface Props {
className?: string; className?: string;
isLoading?: boolean; isLoading?: boolean;
value: string; value: string;
tooltipPlacement?: PlacementWithLogical; tooltipPlacement?: Placement;
} }
const TruncatedValue = ({ className, isLoading, value, tooltipPlacement }: Props) => { const TruncatedValue = ({ className, isLoading, value, tooltipPlacement }: Props) => {
...@@ -16,7 +17,7 @@ const TruncatedValue = ({ className, isLoading, value, tooltipPlacement }: Props ...@@ -16,7 +17,7 @@ const TruncatedValue = ({ className, isLoading, value, tooltipPlacement }: Props
<TruncatedTextTooltip label={ value } placement={ tooltipPlacement }> <TruncatedTextTooltip label={ value } placement={ tooltipPlacement }>
<Skeleton <Skeleton
className={ className } className={ className }
isLoaded={ !isLoading } loading={ isLoading }
display="inline-block" display="inline-block"
whiteSpace="nowrap" whiteSpace="nowrap"
overflow="hidden" overflow="hidden"
......
import type { As } from '@chakra-ui/react'; import { Box, Flex, chakra, VStack } from '@chakra-ui/react';
import { Box, Flex, Skeleton, Tooltip, chakra, VStack } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressParam } from 'types/api/addressParams'; import type { AddressParam } from 'types/api/addressParams';
...@@ -9,6 +8,8 @@ import { route } from 'nextjs-routes'; ...@@ -9,6 +8,8 @@ import { route } from 'nextjs-routes';
import { toBech32Address } from 'lib/address/bech32'; import { toBech32Address } from 'lib/address/bech32';
import { useAddressHighlightContext } from 'lib/contexts/addressHighlight'; import { useAddressHighlightContext } from 'lib/contexts/addressHighlight';
import { useSettingsContext } from 'lib/contexts/settings'; import { useSettingsContext } from 'lib/contexts/settings';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip';
import * as EntityBase from 'ui/shared/entities/base/components'; import * as EntityBase from 'ui/shared/entities/base/components';
import { distributeEntityProps, getIconProps } from '../base/utils'; import { distributeEntityProps, getIconProps } from '../base/utils';
...@@ -62,7 +63,7 @@ const Icon = (props: IconProps) => { ...@@ -62,7 +63,7 @@ const Icon = (props: IconProps) => {
const label = (isVerified ? 'verified ' : '') + (isProxy ? 'proxy contract' : 'contract'); const label = (isVerified ? 'verified ' : '') + (isProxy ? 'proxy contract' : 'contract');
return ( return (
<Tooltip label={ label.slice(0, 1).toUpperCase() + label.slice(1) }> <Tooltip content={ label.slice(0, 1).toUpperCase() + label.slice(1) }>
<span> <span>
<EntityBase.Icon <EntityBase.Icon
{ ...props } { ...props }
...@@ -106,8 +107,8 @@ const Content = chakra((props: ContentProps) => { ...@@ -106,8 +107,8 @@ const Content = chakra((props: ContentProps) => {
); );
return ( return (
<Tooltip label={ label } maxW={{ base: 'calc(100vw - 8px)', lg: '400px' }}> <Tooltip content={ label } contentProps={{ maxW: { base: 'calc(100vw - 8px)', lg: '400px' } }}>
<Skeleton isLoaded={ !props.isLoading } overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" as="span"> <Skeleton loading={ props.isLoading } overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" as="span">
{ nameText } { nameText }
</Skeleton> </Skeleton>
</Tooltip> </Tooltip>
...@@ -172,7 +173,7 @@ const AddressEntry = (props: EntityProps) => { ...@@ -172,7 +173,7 @@ const AddressEntry = (props: EntityProps) => {
); );
}; };
export default React.memo(chakra<As, EntityProps>(AddressEntry)); export default React.memo(chakra(AddressEntry));
export { export {
Container, Container,
......
import { chakra, Flex, Skeleton, useColorModeValue } from '@chakra-ui/react'; import { chakra, Flex } from '@chakra-ui/react';
import type { As, IconProps } from '@chakra-ui/react'; import type { IconProps } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { Skeleton } from 'toolkit/chakra/skeleton';
import type { Props as CopyToClipboardProps } from 'ui/shared/CopyToClipboard'; import type { Props as CopyToClipboardProps } from 'ui/shared/CopyToClipboard';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
...@@ -21,6 +22,7 @@ export interface EntityBaseProps { ...@@ -21,6 +22,7 @@ export interface EntityBaseProps {
icon?: EntityIconProps; icon?: EntityIconProps;
isExternal?: boolean; isExternal?: boolean;
isLoading?: boolean; isLoading?: boolean;
isTooltipDisabled?: boolean;
noCopy?: boolean; noCopy?: boolean;
noIcon?: boolean; noIcon?: boolean;
noLink?: boolean; noLink?: boolean;
...@@ -62,7 +64,7 @@ const Link = chakra(({ isLoading, children, isExternal, onClick, href, noLink }: ...@@ -62,7 +64,7 @@ const Link = chakra(({ isLoading, children, isExternal, onClick, href, noLink }:
}; };
if (noLink) { if (noLink) {
return <Skeleton isLoaded={ !isLoading } { ...styles }>{ children }</Skeleton>; return <Skeleton loading={ isLoading } { ...styles }>{ children }</Skeleton>;
} }
const Component = isExternal ? LinkExternal : LinkInternal; const Component = isExternal ? LinkExternal : LinkInternal;
...@@ -87,8 +89,6 @@ interface EntityIconProps extends Pick<IconProps, 'color' | 'borderRadius' | 'ma ...@@ -87,8 +89,6 @@ interface EntityIconProps extends Pick<IconProps, 'color' | 'borderRadius' | 'ma
export interface IconBaseProps extends Pick<EntityBaseProps, 'isLoading' | 'noIcon'>, EntityIconProps {} export interface IconBaseProps extends Pick<EntityBaseProps, 'isLoading' | 'noIcon'>, EntityIconProps {}
const Icon = ({ isLoading, noIcon, size, name, color, borderRadius, marginRight, boxSize }: IconBaseProps) => { const Icon = ({ isLoading, noIcon, size, name, color, borderRadius, marginRight, boxSize }: IconBaseProps) => {
const defaultColor = useColorModeValue('gray.500', 'gray.400');
if (noIcon || !name) { if (noIcon || !name) {
return null; return null;
} }
...@@ -102,7 +102,7 @@ const Icon = ({ isLoading, noIcon, size, name, color, borderRadius, marginRight, ...@@ -102,7 +102,7 @@ const Icon = ({ isLoading, noIcon, size, name, color, borderRadius, marginRight,
borderRadius={ borderRadius ?? 'base' } borderRadius={ borderRadius ?? 'base' }
display="block" display="block"
mr={ marginRight ?? 2 } mr={ marginRight ?? 2 }
color={ color ?? defaultColor } color={ color ?? { _light: 'gray.500', _dark: 'gray.400' } }
minW={ 0 } minW={ 0 }
flexShrink={ 0 } flexShrink={ 0 }
/> />
...@@ -110,9 +110,8 @@ const Icon = ({ isLoading, noIcon, size, name, color, borderRadius, marginRight, ...@@ -110,9 +110,8 @@ const Icon = ({ isLoading, noIcon, size, name, color, borderRadius, marginRight,
}; };
export interface ContentBaseProps extends Pick<EntityBaseProps, 'className' | 'isLoading' | 'truncation' | 'tailLength'> { export interface ContentBaseProps extends Pick<EntityBaseProps, 'className' | 'isLoading' | 'truncation' | 'tailLength'> {
asProp?: As; asProp?: React.ElementType;
text: string; text: string;
isTooltipDisabled?: boolean;
} }
const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dynamic', tailLength, isTooltipDisabled }: ContentBaseProps) => { const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dynamic', tailLength, isTooltipDisabled }: ContentBaseProps) => {
...@@ -154,7 +153,7 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna ...@@ -154,7 +153,7 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna
return ( return (
<Skeleton <Skeleton
className={ className } className={ className }
isLoaded={ !isLoading } loading={ isLoading }
overflow="hidden" overflow="hidden"
whiteSpace="nowrap" whiteSpace="nowrap"
textOverflow={ truncation === 'tail' ? 'ellipsis' : undefined } textOverflow={ truncation === 'tail' ? 'ellipsis' : undefined }
......
import type { LinkProps } from '@chakra-ui/react'; import type { LinkProps } from '@chakra-ui/react';
import { Link, chakra, Box, Skeleton } from '@chakra-ui/react'; import { Link, chakra, Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { Skeleton } from 'toolkit/chakra/skeleton';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import type { Variants } from './useLinkStyles'; import type { Variants } from './useLinkStyles';
import { useLinkStyles } from './useLinkStyles'; import { useLinkStyles } from './useLinkStyles';
interface Props { interface Props {
href: string; href: string | undefined;
className?: string; className?: string;
children: React.ReactNode; children: React.ReactNode;
isLoading?: boolean; isLoading?: boolean;
......
import { Box, Center, useColorModeValue } from '@chakra-ui/react'; import { Box, Center } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon'; import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon';
...@@ -10,8 +10,6 @@ type Props = { ...@@ -10,8 +10,6 @@ type Props = {
}; };
const UserIdenticon = ({ address, isAutoConnectDisabled }: Props) => { const UserIdenticon = ({ address, isAutoConnectDisabled }: Props) => {
const borderColor = useColorModeValue('orange.100', 'orange.900');
return ( return (
<Box position="relative"> <Box position="relative">
<AddressIdenticon size={ 20 } hash={ address }/> <AddressIdenticon size={ 20 } hash={ address }/>
...@@ -24,7 +22,7 @@ const UserIdenticon = ({ address, isAutoConnectDisabled }: Props) => { ...@@ -24,7 +22,7 @@ const UserIdenticon = ({ address, isAutoConnectDisabled }: Props) => {
backgroundColor="rgba(16, 17, 18, 0.80)" backgroundColor="rgba(16, 17, 18, 0.80)"
borderRadius="full" borderRadius="full"
border="1px solid" border="1px solid"
borderColor={ borderColor } borderColor={{ _light: 'orange.100', _dark: 'orange.900' }}
> >
<IconSvg <IconSvg
name="integration/partial" name="integration/partial"
......
import { Text, Flex, useColorModeValue } from '@chakra-ui/react'; import { Text, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
const UserWalletAutoConnectAlert = () => { const UserWalletAutoConnectAlert = () => {
const bgColor = useColorModeValue('orange.100', 'orange.900');
return ( return (
<Flex <Flex
borderRadius="base" borderRadius="base"
p={ 3 } p={ 3 }
mb={ 3 } mb={ 3 }
alignItems="center" alignItems="center"
bgColor={ bgColor } bgColor={{ _light: 'orange.100', _dark: 'orange.900' }}
> >
<IconSvg <IconSvg
name="integration/partial" name="integration/partial"
......
import { Box, Button, Divider, Flex, Link, VStack, useColorModeValue } from '@chakra-ui/react'; import { Box, Separator, Flex, Link, VStack } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { NavLink } from './types'; import type { NavLink } from './types';
...@@ -9,6 +9,7 @@ import { route } from 'nextjs-routes'; ...@@ -9,6 +9,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import { useMarketplaceContext } from 'lib/contexts/marketplace'; import { useMarketplaceContext } from 'lib/contexts/marketplace';
import shortenString from 'lib/shortenString'; import shortenString from 'lib/shortenString';
import { Button } from 'toolkit/chakra/button';
import Hint from 'ui/shared/Hint'; import Hint from 'ui/shared/Hint';
import TruncatedValue from 'ui/shared/TruncatedValue'; import TruncatedValue from 'ui/shared/TruncatedValue';
import useLogout from 'ui/snippets/auth/useLogout'; import useLogout from 'ui/snippets/auth/useLogout';
...@@ -62,8 +63,6 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress } ...@@ -62,8 +63,6 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress }
const { isAutoConnectDisabled } = useMarketplaceContext(); const { isAutoConnectDisabled } = useMarketplaceContext();
const logout = useLogout(); const logout = useLogout();
const accountTextColor = useColorModeValue('gray.500', 'gray.300');
if (!data) { if (!data) {
return ( return (
<Box> <Box>
...@@ -78,15 +77,14 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress } ...@@ -78,15 +77,14 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress }
<Box> <Box>
{ isAutoConnectDisabled && <UserWalletAutoConnectAlert/> } { isAutoConnectDisabled && <UserWalletAutoConnectAlert/> }
<Box fontSize="xs" lineHeight={ 6 } fontWeight="500" px={ 1 } mb="2px">Account</Box> <Box textStyle="xs" fontWeight="500" px={ 1 } mb="1">Account</Box>
<Box <Box
fontSize="xs" textStyle="xs"
lineHeight={ 4 }
fontWeight="500" fontWeight="500"
borderColor="border.divider" borderColor="border.divider"
borderWidth="1px" borderWidth="1px"
borderRadius="base" borderRadius="base"
color={ accountTextColor } color={{ _light: 'gray.500', _dark: 'gray.300' }}
> >
{ config.features.blockchainInteraction.isEnabled && ( { config.features.blockchainInteraction.isEnabled && (
<Flex p={ 2 } borderColor="border.divider" borderBottomWidth="1px"> <Flex p={ 2 } borderColor="border.divider" borderBottomWidth="1px">
...@@ -95,11 +93,10 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress } ...@@ -95,11 +93,10 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress }
label={ `This wallet address is linked to your Blockscout account. It can be used to login ${ config.features.rewards.isEnabled ? 'and is used for Merits Program participation' : '' }` } // eslint-disable-line max-len label={ `This wallet address is linked to your Blockscout account. It can be used to login ${ config.features.rewards.isEnabled ? 'and is used for Merits Program participation' : '' }` } // eslint-disable-line max-len
boxSize={ 4 } boxSize={ 4 }
ml={ 1 } ml={ 1 }
mr="auto"
/> />
{ data?.address_hash ? { data?.address_hash ?
<Box>{ shortenString(data?.address_hash) }</Box> : <Box ml="auto">{ shortenString(data?.address_hash) }</Box> :
<Link onClick={ onAddAddress } _hover={{ color: 'link_hovered', textDecoration: 'none' }}>Add address</Link> <Link ml="auto" onClick={ onAddAddress } _hover={{ color: 'link.primary.hover' }}>Add address</Link>
} }
</Flex> </Flex>
) } ) }
...@@ -107,14 +104,14 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress } ...@@ -107,14 +104,14 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress }
<Box mr="auto">Email</Box> <Box mr="auto">Email</Box>
{ data?.email ? { data?.email ?
<TruncatedValue value={ data.email }/> : <TruncatedValue value={ data.email }/> :
<Link onClick={ onAddEmail } _hover={{ color: 'link_hovered', textDecoration: 'none' }}>Add email</Link> <Link onClick={ onAddEmail } _hover={{ color: 'link.primary.hover' }}>Add email</Link>
} }
</Flex> </Flex>
</Box> </Box>
{ config.features.blockchainInteraction.isEnabled && <UserProfileContentWallet onClose={ onClose } mt={ 3 }/> } { config.features.blockchainInteraction.isEnabled && <UserProfileContentWallet onClose={ onClose } mt={ 3 }/> }
<VStack as="ul" spacing="0" alignItems="flex-start" overflow="hidden" mt={ 4 }> <VStack as="ul" gap="0" alignItems="flex-start" overflow="hidden" mt={ 4 }>
{ navLinks.map((item) => ( { navLinks.map((item) => (
<UserProfileContentNavLink <UserProfileContentNavLink
key={ item.text } key={ item.text }
...@@ -124,7 +121,7 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress } ...@@ -124,7 +121,7 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress }
)) } )) }
</VStack> </VStack>
<Divider my={ 1 }/> <Separator my={ 1 }/>
<UserProfileContentNavLink <UserProfileContentNavLink
text="Sign out" text="Sign out"
......
...@@ -16,11 +16,11 @@ const UserProfileContentNavLink = ({ href, icon, text, onClick }: NavLink) => { ...@@ -16,11 +16,11 @@ const UserProfileContentNavLink = ({ href, icon, text, onClick }: NavLink) => {
columnGap={ 3 } columnGap={ 3 }
py="14px" py="14px"
color="inherit" color="inherit"
_hover={{ textDecoration: 'none', color: 'link_hovered' }} _hover={{ textDecoration: 'none', color: 'link.primary.hover' }}
onClick={ onClick } onClick={ onClick }
> >
<IconSvg name={ icon } boxSize={ 5 } flexShrink={ 0 }/> <IconSvg name={ icon } boxSize={ 5 } flexShrink={ 0 }/>
<Box fontSize="14px" fontWeight="500" lineHeight={ 5 }>{ text }</Box> <Box textStyle="sm" fontWeight="500">{ text }</Box>
</LinkInternal> </LinkInternal>
); );
}; };
......
import { chakra, Box, Button, Flex, IconButton, useColorModeValue, Spinner } from '@chakra-ui/react'; import { chakra, Box, Flex, Spinner } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import delay from 'lib/delay'; import delay from 'lib/delay';
import useWeb3AccountWithDomain from 'lib/web3/useAccountWithDomain'; import useWeb3AccountWithDomain from 'lib/web3/useAccountWithDomain';
import useWeb3Wallet from 'lib/web3/useWallet'; import useWeb3Wallet from 'lib/web3/useWallet';
import { Button } from 'toolkit/chakra/button';
import { IconButton } from 'toolkit/chakra/icon-button';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import Hint from 'ui/shared/Hint'; import Hint from 'ui/shared/Hint';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
...@@ -14,7 +16,6 @@ interface Props { ...@@ -14,7 +16,6 @@ interface Props {
} }
const UserProfileContentWallet = ({ onClose, className }: Props) => { const UserProfileContentWallet = ({ onClose, className }: Props) => {
const walletSnippetBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const web3Wallet = useWeb3Wallet({ source: 'Profile dropdown' }); const web3Wallet = useWeb3Wallet({ source: 'Profile dropdown' });
const web3AccountWithDomain = useWeb3AccountWithDomain(true); const web3AccountWithDomain = useWeb3AccountWithDomain(true);
...@@ -34,7 +35,15 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => { ...@@ -34,7 +35,15 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => {
const content = (() => { const content = (() => {
if (web3Wallet.isConnected && web3AccountWithDomain.address) { if (web3Wallet.isConnected && web3AccountWithDomain.address) {
return ( return (
<Flex alignItems="center" columnGap={ 2 } bgColor={ walletSnippetBgColor } px={ 2 } py="10px" borderRadius="base" justifyContent="space-between"> <Flex
alignItems="center"
columnGap={ 2 }
bgColor={{ _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' }}
px={ 2 }
py="10px"
borderRadius="base"
justifyContent="space-between"
>
<AddressEntity <AddressEntity
address={{ hash: web3AccountWithDomain.address, ens_domain_name: web3AccountWithDomain.domain }} address={{ hash: web3AccountWithDomain.address, ens_domain_name: web3AccountWithDomain.domain }}
isLoading={ web3AccountWithDomain.isLoading } isLoading={ web3AccountWithDomain.isLoading }
...@@ -47,14 +56,14 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => { ...@@ -47,14 +56,14 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => {
{ web3Wallet.isReconnecting ? <Spinner size="sm" m="2px" flexShrink={ 0 }/> : ( { web3Wallet.isReconnecting ? <Spinner size="sm" m="2px" flexShrink={ 0 }/> : (
<IconButton <IconButton
aria-label="Open wallet" aria-label="Open wallet"
icon={ <IconSvg name="gear_slim" boxSize={ 5 }/> }
variant="simple"
color="icon_info" color="icon_info"
boxSize={ 5 } boxSize={ 5 }
onClick={ handleOpenWalletClick } onClick={ handleOpenWalletClick }
isLoading={ web3Wallet.isOpen } loading={ web3Wallet.isOpen }
flexShrink={ 0 } flexShrink={ 0 }
/> >
<IconSvg name="gear_slim" boxSize={ 5 }/>
</IconButton>
) } ) }
</Flex> </Flex>
); );
...@@ -64,7 +73,7 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => { ...@@ -64,7 +73,7 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => {
<Button <Button
size="sm" size="sm"
onClick={ handleConnectWalletClick } onClick={ handleConnectWalletClick }
isLoading={ web3Wallet.isOpen } loading={ web3Wallet.isOpen }
loadingText="Connect Wallet" loadingText="Connect Wallet"
w="100%" w="100%"
> >
...@@ -75,7 +84,7 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => { ...@@ -75,7 +84,7 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => {
return ( return (
<Box className={ className }> <Box className={ className }>
<Flex px={ 1 } mb="2px" fontSize="xs" alignItems="center" lineHeight={ 6 } fontWeight="500"> <Flex px={ 1 } mb="1" textStyle="xs" alignItems="center" fontWeight="500">
<span>Connected wallet</span> <span>Connected wallet</span>
<Hint <Hint
label={ label={
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment