Skip to content

Commit

Permalink
FET-987: new avatar uploader
Browse files Browse the repository at this point in the history
  • Loading branch information
Stanislav Lysak committed Aug 2, 2024
1 parent feeabdc commit 46525bc
Show file tree
Hide file tree
Showing 9 changed files with 350 additions and 174 deletions.
5 changes: 4 additions & 1 deletion public/locales/en/transactionFlow.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
"profileEditor": {
"tabs": {
"avatar": {
"change": "Change avatar",
"label": "Avatar",
"dropdown": {
"selectNFT": "Select NFT",
"uploadImage": "Upload Image"
"uploadImage": "Upload Image",
"enterManually": "Enter manually"
},
"nft": {
"title": "Select an NFT",
Expand Down
111 changes: 72 additions & 39 deletions src/components/@molecules/ProfileEditor/Avatar/AvatarButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import { ComponentProps, Dispatch, SetStateAction, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import styled, { css } from 'styled-components'

import { Avatar, Dropdown } from '@ensdomains/thorin'
import { Avatar, Button, Dropdown } from '@ensdomains/thorin'
import { DropdownItem } from '@ensdomains/thorin/dist/types/components/molecules/Dropdown/Dropdown'

import CameraIcon from '@app/assets/Camera.svg'
import { LegacyDropdown } from '@app/components/@molecules/LegacyDropdown/LegacyDropdown'

const Container = styled.button<{ $error?: boolean; $validated?: boolean; $dirty?: boolean }>(
const AvatarWrapper = styled.button<{ $error?: boolean; $validated?: boolean; $dirty?: boolean }>(
({ theme, $validated, $dirty, $error }) => css`
position: relative;
width: 90px;
height: 90px;
width: 120px;
height: 120px;
border-radius: 50%;
background-color: ${theme.colors.backgroundPrimary};
cursor: pointer;
Expand Down Expand Up @@ -66,8 +66,8 @@ const IconMask = styled.div(
position: absolute;
top: 0;
left: 0;
width: 90px;
height: 90px;
width: 120px;
height: 120px;
border-radius: 50%;
display: flex;
align-items: center;
Expand All @@ -83,7 +83,25 @@ const IconMask = styled.div(
`,
)

export type AvatarClickType = 'upload' | 'nft'
const ActionContainer = styled.div(
({ theme }) => css`
display: flex;
flex-direction: column;
gap: ${theme.space[2]};
max-width: 200px;
`,
)

const Container = styled.div(
({ theme }) => css`
width: 100%;
display: flex;
align-items: center;
gap: ${theme.space[4]};
`,
)

export type AvatarClickType = 'upload' | 'nft' | 'manual'

type PickedDropdownProps = Pick<ComponentProps<typeof Dropdown>, 'isOpen' | 'setIsOpen'>

Expand All @@ -100,8 +118,8 @@ type Props = {

const AvatarButton = ({
validated,
dirty,
error,
dirty,
src,
onSelectOption,
onAvatarChange,
Expand Down Expand Up @@ -129,41 +147,56 @@ const AvatarButton = ({
: ({} as { isOpen: never; setIsOpen: never })

return (
<LegacyDropdown
items={
[
{
label: t('input.profileEditor.tabs.avatar.dropdown.selectNFT'),
color: 'black',
onClick: handleSelectOption('nft'),
},
{
label: t('input.profileEditor.tabs.avatar.dropdown.uploadImage'),
color: 'black',
onClick: handleSelectOption('upload'),
},
...(validated
? [
{
label: t('action.remove', { ns: 'common' }),
color: 'red',
onClick: handleSelectOption('remove'),
},
]
: []),
] as DropdownItem[]
}
keepMenuOnTop
shortThrow
{...dropdownProps}
>
<Container $validated={validated && dirty} $error={error} $dirty={dirty} type="button">
<Container>
<AvatarWrapper $validated={validated && dirty} $error={error} $dirty={dirty} type="button">
<Avatar label="profile-button-avatar" src={src} noBorder />
{!validated && !error && (
<IconMask>
<CameraIcon />
</IconMask>
)}
</AvatarWrapper>
<LegacyDropdown
items={
[
{
label: t('input.profileEditor.tabs.avatar.dropdown.selectNFT'),
color: 'black',
onClick: handleSelectOption('nft'),
},
{
label: t('input.profileEditor.tabs.avatar.dropdown.uploadImage'),
color: 'black',
onClick: handleSelectOption('upload'),
},
{
label: t('input.profileEditor.tabs.avatar.dropdown.enterManually'),
color: 'black',
onClick: handleSelectOption('manual'),
},
...(validated
? [
{
label: t('action.remove', { ns: 'common' }),
color: 'red',
onClick: handleSelectOption('remove'),
},
]
: []),
] as DropdownItem[]
}
keepMenuOnTop
shortThrow
{...dropdownProps}
>
<ActionContainer>
<Button disabled colorStyle="accentSecondary">
{src}
</Button>
<Button colorStyle="accentSecondary">
{t('input.profileEditor.tabs.avatar.change')}
</Button>
</ActionContainer>
<input
type="file"
style={{ display: 'none' }}
Expand All @@ -176,8 +209,8 @@ const AvatarButton = ({
}
}}
/>
</Container>
</LegacyDropdown>
</LegacyDropdown>
</Container>
)
}

Expand Down
94 changes: 94 additions & 0 deletions src/components/@molecules/ProfileEditor/Avatar/AvatarManual.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/* eslint-disable no-multi-assign */

import { useState } from 'react'
import { useTranslation } from 'react-i18next'

import { Button, Dialog, Helper, Input } from '@ensdomains/thorin'

import { useUploadAvatar } from './useUploadAvatar'

type AvatarManualProps = {
name: string
handleCancel: () => void
handleSubmit: (type: 'manual', uri: string, display?: string) => void
}

function isValidHttpUrl(value: string) {
let url

try {
url = new URL(value)
} catch (_) {
return false
}

return url.protocol === 'http:' || url.protocol === 'https:'
}

export function AvatarManual({ name, handleCancel, handleSubmit }: AvatarManualProps) {
const { t } = useTranslation('transactionFlow')

const [value, setValue] = useState<string>('')

const { signAndUpload, isPending, error } = useUploadAvatar()

const handleUpload = async () => {
try {
const dataURL = await fetch(value)
.then((res) => res.blob())
.then((blob) => {
return new Promise<string>((res) => {
const reader = new FileReader()

reader.onload = (e) => {
if (e.target) res(e.target.result as string)
}

reader.readAsDataURL(blob)
})
})

const endpoint = await signAndUpload({ dataURL, name })

if (endpoint) {
handleSubmit('manual', endpoint, value)
}
} catch (e) {
console.error(e)
}
}

return (
<>
<Dialog.Heading title={t('input.profileEditor.tabs.avatar.dropdown.enterManually')} />
<Dialog.Content>
<Input
label={t('input.profileEditor.tabs.avatar.label')}
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</Dialog.Content>
{error && (
<Helper data-testid="avatar-upload-error" type="error">
{error.message}
</Helper>
)}
<Dialog.Footer
leading={
<Button colorStyle="accentSecondary" onClick={() => handleCancel()}>
{t('action.back', { ns: 'common' })}
</Button>
}
trailing={
<Button
disabled={isPending || !isValidHttpUrl(value)}
colorStyle={error ? 'redSecondary' : undefined}
onClick={handleUpload}
>
{error ? t('action.tryAgain', { ns: 'common' }) : t('action.confirm', { ns: 'common' })}
</Button>
}
/>
</>
)
}
Loading

0 comments on commit 46525bc

Please sign in to comment.