diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json index 7e8a11f42..fac53be6f 100644 --- a/public/locales/en/profile.json +++ b/public/locales/en/profile.json @@ -382,6 +382,7 @@ "empty": "No subnames have been added", "noResults": "No results", "noMoreResults": "No more results", + "setProfile": "Set Profile", "addSubname": { "title": "Subnames let you create additional names from your existing name.", "learn": "Learn about subnames", diff --git a/src/hooks/useProfileEditorForm.tsx b/src/hooks/useProfileEditorForm.tsx index 69e77dc6a..18c72b518 100644 --- a/src/hooks/useProfileEditorForm.tsx +++ b/src/hooks/useProfileEditorForm.tsx @@ -172,7 +172,7 @@ export const useProfileEditorForm = (existingRecords: ProfileRecord[]) => { SUPPORTED_AVUP_ENDPOINTS.some((endpoint) => avatar?.startsWith(endpoint)) ) if (avatarIsChanged) { - setValue('avatar', avatar, { shouldDirty: true, shouldTouch: true }) + setValue('avatar', avatar || '', { shouldDirty: true, shouldTouch: true }) } } @@ -217,6 +217,7 @@ export const useProfileEditorForm = (existingRecords: ProfileRecord[]) => { const getAvatar = () => getValues('avatar') return { + isDirty: formState.isDirty, records, register, trigger, diff --git a/src/transaction-flow/input/CreateSubname-flow.tsx b/src/transaction-flow/input/CreateSubname-flow.tsx index a025c8aed..6ba308c98 100644 --- a/src/transaction-flow/input/CreateSubname-flow.tsx +++ b/src/transaction-flow/input/CreateSubname-flow.tsx @@ -1,11 +1,24 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' +import { match } from 'ts-pattern' import { validateName } from '@ensdomains/ensjs/utils' -import { Button, Dialog, Input } from '@ensdomains/thorin' +import { Button, Dialog, Input, mq, PlusSVG } from '@ensdomains/thorin' +import { ConfirmationDialogView } from '@app/components/@molecules/ConfirmationDialogView/ConfirmationDialogView' +import { AvatarClickType } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarButton' +import { AvatarViewManager } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarViewManager' +import { AddProfileRecordView } from '@app/components/pages/profile/[name]/registration/steps/Profile/AddProfileRecordView' +import { CustomProfileRecordInput } from '@app/components/pages/profile/[name]/registration/steps/Profile/CustomProfileRecordInput' +import { ProfileRecordInput } from '@app/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordInput' +import { ProfileRecordTextarea } from '@app/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordTextarea' +import { profileEditorFormToProfileRecords } from '@app/components/pages/profile/[name]/registration/steps/Profile/profileRecordUtils' +import { WrappedAvatarButton } from '@app/components/pages/profile/[name]/registration/steps/Profile/WrappedAvatarButton' +import { ProfileRecord } from '@app/constants/profileRecordOptions' +import { useContractAddress } from '@app/hooks/chain/useContractAddress' import useDebouncedCallback from '@app/hooks/useDebouncedCallback' +import { useProfileEditorForm } from '@app/hooks/useProfileEditorForm' import { useValidateSubnameLabel } from '../../hooks/useValidateSubnameLabel' import { createTransactionItem } from '../transaction' @@ -16,10 +29,29 @@ type Data = { isWrapped: boolean } +type ModalOption = AvatarClickType | 'editor' | 'profile-editor' | 'add-record' | 'clear-eth' + export type Props = { data: Data } & TransactionDialogPassthrough +const ButtonContainer = styled.div( + ({ theme }) => css` + display: flex; + justify-content: center; + padding-bottom: ${theme.space['4']}; + `, +) + +const ButtonWrapper = styled.div(({ theme }) => [ + css` + width: ${theme.space.full}; + `, + mq.xs.min(css` + width: max-content; + `), +]) + const ParentLabel = styled.div( ({ theme }) => css` overflow: hidden; @@ -29,33 +61,106 @@ const ParentLabel = styled.div( `, ) -const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Props) => { - const { t } = useTranslation('profile') - +const useSubnameLabel = (data: Data) => { const [label, setLabel] = useState('') const [_label, _setLabel] = useState('') - const debouncedSetLabel = useDebouncedCallback(setLabel, 500) - const { valid, error, expiryLabel, isLoading: isUseValidateSubnameLabelLoading, - } = useValidateSubnameLabel({ name: parent, label, isWrapped }) + } = useValidateSubnameLabel({ + name: data.parent, + label, + isWrapped: data.isWrapped, + }) + + const debouncedSetLabel = useDebouncedCallback(setLabel, 500) + + const handleChange = (e: React.ChangeEvent) => { + try { + const normalised = validateName(e.target.value) + _setLabel(normalised) + debouncedSetLabel(normalised) + } catch { + _setLabel(e.target.value) + debouncedSetLabel(e.target.value) + } + } const isLabelsInsync = label === _label const isLoading = isUseValidateSubnameLabelLoading || !isLabelsInsync + return { + valid, + error, + expiryLabel, + isLoading, + label: _label, + debouncedLabel: label, + setLabel: handleChange, + } +} + +const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Props) => { + const { t } = useTranslation('profile') + const { t: registerT } = useTranslation('register') + + const [view, setView] = useState('editor') + + const { valid, error, expiryLabel, isLoading, debouncedLabel, label, setLabel } = useSubnameLabel( + { + parent, + isWrapped, + }, + ) + + const name = `${debouncedLabel}.${parent}` + + const defaultResolverAddress = useContractAddress({ contract: 'ensPublicResolver' }) + + const { + isDirty, + records, + register, + trigger, + control, + addRecords, + getValues, + removeRecordAtIndex, + removeRecordByGroupAndKey: removeRecordByTypeAndKey, + setAvatar, + labelForRecord, + secondaryLabelForRecord, + placeholderForRecord, + validatorForRecord, + errorForRecordAtIndex, + isDirtyForRecordAtIndex, + } = useProfileEditorForm([ + { + key: 'eth', + value: '', + type: 'text', + group: 'address', + }, + ]) + const handleSubmit = () => { dispatch({ name: 'setTransactions', payload: [ createTransactionItem('createSubname', { contract: isWrapped ? 'nameWrapper' : 'registry', - label, + label: debouncedLabel, parent, }), + createTransactionItem('updateProfileRecords', { + name, + records: profileEditorFormToProfileRecords(getValues()), + resolverAddress: defaultResolverAddress, + clearRecords: false, + }), ], }) dispatch({ @@ -64,48 +169,184 @@ const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Pro }) } + const [avatarFile, setAvatarFile] = useState() + const [avatarSrc, setAvatarSrc] = useState() + + const handleDeleteRecord = (record: ProfileRecord, index: number) => { + if (record.key === 'eth') return setView('clear-eth') + removeRecordAtIndex(index) + process.nextTick(() => trigger()) + } + return ( <> - - - .{parent}} - value={_label} - onChange={(e) => { - try { - const normalised = validateName(e.target.value) - _setLabel(normalised) - debouncedSetLabel(normalised) - } catch { - _setLabel(e.target.value) - debouncedSetLabel(e.target.value) - } - }} - error={ - error - ? t(`details.tabs.subnames.addSubname.dialog.error.${error}`, { date: expiryLabel }) - : undefined - } - /> - - - {t('action.cancel', { ns: 'common' })} - - } - trailing={ - - } - /> + {match(view) + .with('editor', () => ( + <> + + + .{parent}} + value={label} + onChange={setLabel} + error={ + error + ? t(`details.tabs.subnames.addSubname.dialog.error.${error}`, { + date: expiryLabel, + }) + : undefined + } + /> + + + {t('action.cancel', { ns: 'common' })} + + } + trailing={ + + } + /> + + )) + .with('profile-editor', () => ( + <> + + + setAvatar(avatar)} + onAvatarFileChange={(file) => setAvatarFile(file)} + onAvatarSrcChange={(src) => setAvatarSrc(src)} + /> + {records.map((field, index) => + match(field) + .with({ group: 'custom' }, () => ( + handleDeleteRecord(field, index)} + /> + )) + .with({ key: 'description' }, () => ( + handleDeleteRecord(field, index)} + {...register(`records.${index}.value`, { + validate: validatorForRecord(field), + })} + /> + )) + .otherwise(() => ( + handleDeleteRecord(field, index)} + {...register(`records.${index}.value`, { + validate: validatorForRecord(field), + })} + /> + )), + )} + + + + + + + setView('editor')}> + {t('action.cancel', { ns: 'common' })} + + } + trailing={ + + } + /> + + )) + .with('add-record', () => ( + { + addRecords(newRecords) + setView('profile-editor') + }} + onClose={() => setView('profile-editor')} + /> + )) + .with('upload', 'nft', (type) => ( + setView('profile-editor')} + type={type} + handleSubmit={(_, uri, display) => { + setAvatar(uri) + setAvatarSrc(display) + setView('profile-editor') + trigger() + }} + /> + )) + .with('clear-eth', () => ( + { + removeRecordByTypeAndKey('address', 'eth') + setView('profile-editor') + }} + onDecline={() => setView('profile-editor')} + /> + )) + .exhaustive()} ) }