Skip to content

Commit

Permalink
Merge pull request #65 from vechain/feat/ens-avatar
Browse files Browse the repository at this point in the history
Feat/ens avatar
  • Loading branch information
Agilulfo1820 authored Feb 3, 2025
2 parents 786009a + 98e809a commit f2463b1
Show file tree
Hide file tree
Showing 36 changed files with 855 additions and 139 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,22 @@ import {
useEmbeddedWalletSettingsModal,
useExploreEcosystemModal,
useNotificationsModal,
useAccountCustomizationModal,
useFAQModal,
} from '@vechain/vechain-kit';
import { FeatureCard } from './FeatureCard';
import { GithubCard } from './GithubCard';
import { LanguageCard } from './LanguageCard';
import { ThemeCard } from './ThemeCard';
import { CgProfile } from 'react-icons/cg';

export function FeaturesToTry() {
const { account } = useWallet();

// Use the modal hooks
const { open: openChooseNameModal } = useChooseNameModal();
const { open: openAccountCustomizationModal } =
useAccountCustomizationModal();
const { open: openSendTokenModal } = useSendTokenModal();
const { open: openEmbeddedWalletSettingsModal } =
useEmbeddedWalletSettingsModal();
Expand All @@ -45,6 +49,15 @@ export function FeaturesToTry() {
link: '#',
content: openChooseNameModal,
},
{
title: 'Set Profile Image',
description:
'Customize your account with a profile image to enhance your identity across VeChain applications.',
icon: CgProfile,
highlight: !account?.domain,
link: '#',
content: openAccountCustomizationModal,
},
{
title: 'Transfer Assets',
description:
Expand Down Expand Up @@ -89,16 +102,16 @@ export function FeaturesToTry() {
return (
<VStack spacing={6} align="stretch">
<Text fontSize="xl" fontWeight="bold">
Features (click to try!)
Features
</Text>

<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
{features.map((feature) => (
<FeatureCard key={feature.title} {...feature} />
))}
<LanguageCard />
<GithubCard />
<ThemeCard />
<GithubCard />
</SimpleGrid>
</VStack>
);
Expand Down
10 changes: 9 additions & 1 deletion examples/homepage/src/app/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,15 @@ const Logo = () => {
transition: 'opacity 0.2s ease-in-out',
}}
>
<VechainLogoHorizontal maxW={200} isDark={colorMode === 'dark'} />
<VStack spacing={0}>
<Text fontSize={'md'} fontWeight={'bold'}>
Powered by
</Text>
<VechainLogoHorizontal
maxW={200}
isDark={colorMode === 'dark'}
/>
</VStack>
</HStack>
);
};
1 change: 1 addition & 0 deletions packages/vechain-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@vechain/vebetterdao-contracts": "^4.1.0",
"@wagmi/core": "^2.13.4",
"bignumber.js": "^9.1.2",
"browser-image-compression": "^2.0.2",
"buffer": "^6.0.3",
"crypto-browserify": "^3.12.0",
"dotenv": "^16.4.7",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { NotificationsContent } from './Contents/Notifications/NotificationConte
import { ExploreEcosystemContent } from './Contents/Ecosystem/ExploreEcosystemContent';
import { AppOverviewContent } from './Contents/Ecosystem/AppOverviewContent';
import { DisconnectConfirmContent } from './Contents/Account/DisconnectConfirmContent';
import { AccountCustomizationContent } from './Contents/Account/AccountCustomizationContent';

type Props = {
isOpen: boolean;
Expand Down Expand Up @@ -144,6 +145,12 @@ export const AccountModal = ({
setCurrentContent={setCurrentContent}
/>
);
case 'account-customization':
return (
<AccountCustomizationContent
setCurrentContent={setCurrentContent}
/>
);
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const AccountSelector = ({
rounded="full"
/>
<Text fontSize={size} fontWeight="500">
{humanDomain(wallet?.domain ?? '', 11, 4) ||
{humanDomain(wallet?.domain ?? '', 6, 4) ||
humanAddress(wallet?.address ?? '', 6, 4)}
</Text>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@ import {
Image,
Tag,
useColorMode,
ButtonProps,
} from '@chakra-ui/react';
import { ElementType } from 'react';
import { useTranslation } from 'react-i18next';
import { IconType } from 'react-icons';

interface ActionButtonProps {
type ActionButtonProps = {
title: string;
description: string;
onClick: () => void;
leftIcon?: ElementType;
rightIcon?: ElementType;
leftIcon?: IconType;
rightIcon?: IconType;
leftImage?: string;
backgroundColor?: string;
border?: string;
Expand All @@ -26,7 +27,10 @@ interface ActionButtonProps {
showComingSoon?: boolean;
isDisabled?: boolean;
stacked?: boolean;
}
isLoading?: boolean;
loadingText?: string;
style?: ButtonProps;
};

export const ActionButton = ({
leftIcon,
Expand All @@ -41,6 +45,9 @@ export const ActionButton = ({
_hover,
isDisabled = false,
stacked = false,
isLoading,
loadingText,
style,
}: ActionButtonProps) => {
const { t } = useTranslation();
const { colorMode } = useColorMode();
Expand All @@ -57,13 +64,23 @@ export const ActionButton = ({
onClick={onClick}
display={hide ? 'none' : 'flex'}
isDisabled={showComingSoon || isDisabled}
isLoading={isLoading}
loadingText={loadingText}
bgColor={baseBackgroundColor}
_hover={_hover}
{...style}
>
<HStack w={'full'} justify={'space-between'}>
<Box minW={'40px'}>
{leftImage ? (
<Image src={leftImage} alt="left-image" />
<Image
src={leftImage}
w={'35px'}
h={'35px'}
borderRadius={'full'}
alt="left-image"
alignSelf={'end'}
/>
) : (
<Icon as={leftIcon} fontSize={'25px'} />
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import {
ModalBody,
ModalCloseButton,
ModalHeader,
VStack,
Text,
Button,
} from '@chakra-ui/react';
import { ModalBackButton, StickyHeaderContainer } from '@/components/common';
import { AccountModalContentTypes } from '../../Types';
import { useTranslation } from 'react-i18next';
import { useVeChainKitConfig } from '@/providers';
import { getAvatarQueryKey, useWallet } from '@/hooks';
import { MdOutlineNavigateNext } from 'react-icons/md';
import { ActionButton } from '../../Components';
import { useSingleImageUpload } from '@/hooks/api/ipfs';
import { useUpdateAvatarRecord } from '@/hooks';
import { useQueryClient } from '@tanstack/react-query';
import { useRef, useState } from 'react';
import { uploadBlobToIPFS } from '@/utils/ipfs';
import { FaRegAddressCard } from 'react-icons/fa';

type Props = {
setCurrentContent: React.Dispatch<
React.SetStateAction<AccountModalContentTypes>
>;
};

export const AccountCustomizationContent = ({ setCurrentContent }: Props) => {
const { t } = useTranslation();
const { network, darkMode: isDark } = useVeChainKitConfig();
const { account } = useWallet();
const fileInputRef = useRef<HTMLInputElement>(null);
const [isUploading, setIsUploading] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [showFullText, setShowFullText] = useState(false);
const queryClient = useQueryClient();
const { onUpload } = useSingleImageUpload({
compressImage: true,
});

const { updateAvatar } = useUpdateAvatarRecord({
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: getAvatarQueryKey(account?.domain ?? ''),
});

await queryClient.refetchQueries({
queryKey: getAvatarQueryKey(account?.domain ?? ''),
});

setIsProcessing(false);
},
onError: () => {
setIsProcessing(false);
},
});

const handleImageUpload = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const file = event.target.files?.[0];
if (!file) return;

try {
setIsUploading(true);
setIsProcessing(true);
const uploadedImage = await onUpload(file);
if (!uploadedImage) throw new Error('Failed to compress image');

const ipfsHash = await uploadBlobToIPFS(
uploadedImage.file,
file.name,
network.type,
);
await updateAvatar(account?.domain ?? '', 'ipfs://' + ipfsHash);
} catch (error) {
console.error('Error uploading image:', error);
setIsProcessing(false);
} finally {
setIsUploading(false);
}
};

return (
<>
<StickyHeaderContainer>
<ModalHeader
fontSize={'md'}
fontWeight={'500'}
textAlign={'center'}
color={isDark ? '#dfdfdd' : '#4d4d4d'}
>
{t('Customize Account')}
</ModalHeader>
<ModalBackButton
onClick={() => setCurrentContent('settings')}
/>
<ModalCloseButton />
</StickyHeaderContainer>

<ModalBody>
<VStack spacing={3} align="center">
<VStack
spacing={3}
w={'full'}
justifyContent={'flex-start'}
alignItems={'flex-start'}
>
<Text fontSize={'sm'} opacity={0.5}>
{t(
'Customize your account with a unique .vet domain name and profile image to enhance your identity across VeChain applications.',
)}
</Text>

{showFullText && (
<>
<Text fontSize={'sm'} opacity={0.5}>
{t(
'Your customizations are linked to your .vet domain name, making them portable across different applications.',
)}
</Text>
<Text fontSize={'sm'} opacity={0.5}>
{t(
'To get started with customization, first secure your .vet domain name. Once you have a domain, you can add a profile image that will be visible wherever you use your account.',
)}
</Text>
<Text fontSize={'sm'} opacity={0.5}>
{t(
'Changing your domain name will update also your profile image.',
)}
</Text>
</>
)}

<Button
mt={0}
variant="link"
size="sm"
onClick={() => setShowFullText(!showFullText)}
color="blue.500"
textAlign="left"
>
{t(showFullText ? 'Show Less' : 'Read More')}
</Button>
</VStack>

<ActionButton
style={{
mt: 3,
}}
title={
account?.domain
? account?.domain
: t('Choose account name')
}
description={t(
'Choose a unique .vet domain name for your account.',
)}
onClick={() => {
if (account?.domain) {
setCurrentContent({
type: 'choose-name-search',
props: {
name: '',
setCurrentContent,
},
});
} else {
setCurrentContent('choose-name');
}
}}
leftIcon={FaRegAddressCard}
rightIcon={MdOutlineNavigateNext}
/>

<ActionButton
title={t('Update profile image')}
description={t(
!account?.domain
? 'You can change your profile image only after you setup your domain name.'
: 'Customize your account by adding a profile image, which will be displayed on the apps you use.',
)}
onClick={() => fileInputRef.current?.click()}
leftImage={account?.image}
isLoading={isUploading || isProcessing}
isDisabled={!account?.domain}
loadingText={
isUploading
? t('Uploading image')
: t('Setting image')
}
/>

<input
type="file"
ref={fileInputRef}
hidden
accept="image/*"
onChange={async (event) =>
await handleImageUpload(event)
}
/>
</VStack>
</ModalBody>
</>
);
};
Loading

0 comments on commit f2463b1

Please sign in to comment.