Skip to content

Commit c987a68

Browse files
add ENS records profile edit modal
1 parent c97b689 commit c987a68

3 files changed

Lines changed: 211 additions & 110 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
'use client'
2+
3+
import { useTheme } from 'next-themes'
4+
import type React from 'react'
5+
import { ENSRecords } from 'ethereum-identity-kit'
6+
import { sha256 } from 'viem'
7+
import { useAccount, useSignTypedData } from 'wagmi'
8+
9+
interface ENSRecordsModalProps {
10+
name?: string | null
11+
onClose: () => void
12+
}
13+
14+
const dataURLToBytes = (dataUrl: string): Uint8Array => {
15+
const base64 = dataUrl.split(',')[1]
16+
const binary = atob(base64)
17+
const bytes = new Uint8Array(binary.length)
18+
19+
for (let i = 0; i < binary.length; i++) {
20+
bytes[i] = binary.charCodeAt(i)
21+
}
22+
23+
return bytes
24+
}
25+
26+
const ENSRecordsModal: React.FC<ENSRecordsModalProps> = ({ name, onClose }) => {
27+
const { resolvedTheme } = useTheme()
28+
const { address: connectedAddress } = useAccount()
29+
const { signTypedDataAsync } = useSignTypedData()
30+
31+
if (!name) return null
32+
33+
const uploadImage = async (dataURL: string, type: 'avatar' | 'header') => {
34+
if (!connectedAddress) throw new Error('Connect a wallet to upload ENS profile images')
35+
36+
const urlHash = sha256(dataURLToBytes(dataURL))
37+
const expiry = `${Date.now() + 1000 * 60 * 60 * 24 * 7}`
38+
const sig = await signTypedDataAsync({
39+
primaryType: 'Upload',
40+
domain: { name: 'Ethereum Name Service', version: '1' },
41+
types: {
42+
Upload: [
43+
{ name: 'upload', type: 'string' },
44+
{ name: 'expiry', type: 'string' },
45+
{ name: 'name', type: 'string' },
46+
{ name: 'hash', type: 'string' },
47+
],
48+
},
49+
message: {
50+
upload: type,
51+
expiry,
52+
name,
53+
hash: urlHash,
54+
},
55+
})
56+
57+
const response = await fetch(`https://eidk.me/${encodeURIComponent(name)}${type === 'header' ? '/h' : ''}`, {
58+
method: 'PUT',
59+
headers: {
60+
'Content-Type': 'application/json',
61+
},
62+
body: JSON.stringify({
63+
expiry,
64+
dataURL,
65+
sig,
66+
unverifiedAddress: connectedAddress,
67+
}),
68+
})
69+
70+
if (!response.ok) {
71+
if (response.status === 413) throw new Error('File size is too large (max 500KB)')
72+
if (response.status === 415) throw new Error('Unsupported file type. Use JPG/JPEG.')
73+
throw new Error(`Upload failed: ${response.statusText}`)
74+
}
75+
76+
const result = await response.json()
77+
78+
return result.url || `https://euc.li/${encodeURIComponent(name)}${type === 'header' ? '/h' : ''}`
79+
}
80+
81+
return (
82+
<ENSRecords
83+
name={name}
84+
defaultTab='records'
85+
darkMode={resolvedTheme === 'dark' || resolvedTheme === 'halloween'}
86+
onClose={onClose}
87+
onImageUpload={uploadImage}
88+
/>
89+
)
90+
}
91+
92+
export default ENSRecordsModal

src/components/user-profile-card/index.tsx

Lines changed: 61 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
'use client'
22

3-
import Link from 'next/link'
43
import { useRouter } from 'next/navigation'
4+
import { useState } from 'react'
5+
import { useAccount } from 'wagmi'
56
import { useTranslation } from 'react-i18next'
67
import { ProfileCard } from 'ethereum-identity-kit'
78

89
import { cn } from '#/lib/utilities'
910
import Achievements from './components/achievements'
1011
import FollowButton from '#/components/follow-button'
12+
import ENSRecordsModal from '#/components/ens-records-modal'
1113
import ThreeDotMenu from './components/three-dot-menu'
1214
import { useProfileCard } from './hooks/use-profile-card'
13-
import EnsLogo from 'public/assets/icons/socials/ens.svg'
1415
import { useEFPProfile } from '#/contexts/efp-profile-context'
1516
import LoadingProfileCard from './components/loading-profile-card'
1617
import type { ProfileDetailsResponse, StatsResponse } from '#/types/requests'
@@ -53,75 +54,74 @@ const UserProfileCard: React.FC<UserProfileCardProps> = ({
5354
}) => {
5455
const router = useRouter()
5556
const { t } = useTranslation()
57+
const { address: connectedAddress } = useAccount()
5658
const { selectedList } = useEFPProfile()
59+
const [ensRecordsOpen, setEnsRecordsOpen] = useState(false)
5760
const { followState, profileName, isConnectedUserCard } = useProfileCard(profile)
61+
const ensRecordsName = profile?.ens?.name ?? profileName
5862

5963
return (
6064
<div className={cn('bg-neutral flex w-[364px] flex-col gap-4 rounded-sm pb-3', className)}>
6165
{isLoading ? (
6266
<LoadingProfileCard hideFollowButton={true} className='bg-neutral' />
6367
) : profile?.address ? (
64-
<ProfileCard
65-
list={profileList}
66-
onStatClick={({ stat }) => {
67-
router.push(`/${profile.address}?tab=${stat}&ssr=false`)
68-
}}
69-
showFollowerState={true}
70-
showFollowButton={!hideFollowButton}
71-
addressOrName={profile.address}
72-
onProfileClick={(addressOrName) => {
73-
router.push(`/${addressOrName}?ssr=false`)
74-
}}
75-
selectedList={selectedList}
76-
className='bg-neutral'
77-
extraOptions={{
78-
openListSettings: openListSettingsModal,
79-
prefetched: {
80-
profile: {
81-
data: profile ?? undefined,
82-
isLoading: !!isLoading,
83-
refetch: refetchProfile ?? (() => {}),
68+
<>
69+
<ProfileCard
70+
list={profileList}
71+
connectedAddress={connectedAddress}
72+
onStatClick={({ stat }) => {
73+
router.push(`/${profile.address}?tab=${stat}&ssr=false`)
74+
}}
75+
showFollowerState={true}
76+
showFollowButton={!hideFollowButton}
77+
addressOrName={profile.address}
78+
onProfileClick={(addressOrName) => {
79+
router.push(`/${addressOrName}?ssr=false`)
80+
}}
81+
selectedList={selectedList}
82+
className='bg-neutral'
83+
extraOptions={{
84+
openListSettings: openListSettingsModal,
85+
onEditProfileClick: () => setEnsRecordsOpen(true),
86+
prefetched: {
87+
profile: {
88+
data: profile ?? undefined,
89+
isLoading: !!isLoading,
90+
refetch: refetchProfile ?? (() => {}),
91+
},
92+
stats: {
93+
data: stats ?? undefined,
94+
isLoading: !!isStatsLoading,
95+
refetch: refetchStats ?? (() => {}),
96+
},
8497
},
85-
stats: {
86-
data: stats ?? undefined,
87-
isLoading: !!isStatsLoading,
88-
refetch: refetchStats ?? (() => {}),
89-
},
90-
},
91-
nameMenu: (
92-
<ThreeDotMenu
93-
address={profile.address}
94-
profileList={profileList}
95-
primaryList={Number(profile.primary_list)}
96-
profileName={profileName}
97-
showMoreOptions={!!showMoreOptions}
98-
isConnectedUserCard={isConnectedUserCard}
99-
followState={followState}
100-
openBlockModal={openBlockModal}
101-
openQrCodeModal={openQrCodeModal}
102-
openListSettingsModal={openListSettingsModal}
103-
/>
104-
),
105-
customFollowButton: (
106-
<div className='mt-16'>
107-
{isConnectedUserCard ? (
108-
<Link href={`https://app.ens.domains/${profile.ens?.name}`} target='_blank'>
109-
<button className='flex items-center gap-1 rounded-sm bg-[#0080BC] p-1.5 py-2 font-semibold text-white transition-all hover:scale-110 hover:bg-[#07A9F5]'>
110-
<EnsLogo className='h-auto w-5' />
111-
<p>Edit Profile</p>
112-
</button>
113-
</Link>
114-
) : (
98+
nameMenu: (
99+
<ThreeDotMenu
100+
address={profile.address}
101+
profileList={profileList}
102+
primaryList={Number(profile.primary_list)}
103+
profileName={profileName}
104+
showMoreOptions={!!showMoreOptions}
105+
isConnectedUserCard={isConnectedUserCard}
106+
followState={followState}
107+
openBlockModal={openBlockModal}
108+
openQrCodeModal={openQrCodeModal}
109+
openListSettingsModal={openListSettingsModal}
110+
/>
111+
),
112+
customFollowButton: isConnectedUserCard ? undefined : (
113+
<div className='mt-16'>
115114
<FollowButton address={profile.address} />
116-
)}
117-
</div>
118-
),
119-
}}
120-
style={{
121-
width: '100%',
122-
zIndex: 10,
123-
}}
124-
/>
115+
</div>
116+
),
117+
}}
118+
style={{
119+
width: '100%',
120+
zIndex: 10,
121+
}}
122+
/>
123+
{ensRecordsOpen && <ENSRecordsModal name={ensRecordsName} onClose={() => setEnsRecordsOpen(false)} />}
124+
</>
125125
) : (
126126
<div className={cn('relative z-10 flex flex-col rounded-sm', isRecommended ? 'bg-neutral' : 'glass-card')}>
127127
{isRecommended ? (

src/components/user-profile/index.tsx

Lines changed: 58 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import React from 'react'
1+
'use client'
2+
3+
import React, { useState } from 'react'
24
import { useAccount } from 'wagmi'
35
import { useRouter } from 'next/navigation'
46
import { useWindowSize } from '@uidotdev/usehooks'
57
import { FullWidthProfile } from 'ethereum-identity-kit'
68

79
import FollowButton from '../follow-button'
10+
import ENSRecordsModal from '#/components/ens-records-modal'
811
import type { StatsResponse } from '#/types/requests'
912
import useFollowingState from '#/hooks/use-following-state'
1013
import { useEFPProfile } from '#/contexts/efp-profile-context'
@@ -48,59 +51,65 @@ const UserProfile: React.FC<UserProfileCardProps> = ({
4851
const { width } = useWindowSize()
4952
const { selectedList } = useEFPProfile()
5053
const { address: userAddress } = useAccount()
54+
const [ensRecordsOpen, setEnsRecordsOpen] = useState(false)
5155

5256
const { followingState } = useFollowingState({ address: profile?.address })
57+
const ensRecordsName = profile?.ens?.name ?? (addressOrName.endsWith('.eth') ? addressOrName : undefined)
5358

5459
return (
55-
<FullWidthProfile
56-
addressOrName={addressOrName}
57-
connectedAddress={userAddress}
58-
list={profileList}
59-
selectedList={selectedList?.toString()}
60-
onProfileClick={(addressOrName: string) => {
61-
router.push(`/${addressOrName}?ssr=false`)
62-
}}
63-
onStatClick={({ addressOrName, stat }: { addressOrName: string; stat: string }) => {
64-
router.push(`/${addressOrName}?tab=${stat}`)
65-
}}
66-
className={className}
67-
style={{
68-
paddingBottom: width && width < 768 ? '20px' : '110px',
69-
}}
70-
showFollowButton={profile?.address ? true : false}
71-
showFollowerState={true}
72-
extraOptions={{
73-
role: role,
74-
prefetched: {
75-
profile: {
76-
data: profile ?? undefined,
77-
isLoading: !!isLoading,
78-
refetch: refetchProfile ?? (() => {}),
79-
},
80-
stats: {
81-
data: stats ?? undefined,
82-
isLoading: !!isStatsLoading,
83-
refetch: refetchStats ?? (() => {}),
60+
<>
61+
<FullWidthProfile
62+
addressOrName={addressOrName}
63+
connectedAddress={userAddress}
64+
list={profileList}
65+
selectedList={selectedList?.toString()}
66+
onProfileClick={(addressOrName: string) => {
67+
router.push(`/${addressOrName}?ssr=false`)
68+
}}
69+
onStatClick={({ addressOrName, stat }: { addressOrName: string; stat: string }) => {
70+
router.push(`/${addressOrName}?tab=${stat}`)
71+
}}
72+
className={className}
73+
style={{
74+
paddingBottom: width && width < 768 ? '20px' : '110px',
75+
}}
76+
showFollowButton={profile?.address ? true : false}
77+
showFollowerState={true}
78+
extraOptions={{
79+
role: role,
80+
onEditProfileClick: () => setEnsRecordsOpen(true),
81+
prefetched: {
82+
profile: {
83+
data: profile ?? undefined,
84+
isLoading: !!isLoading,
85+
refetch: refetchProfile ?? (() => {}),
86+
},
87+
stats: {
88+
data: stats ?? undefined,
89+
isLoading: !!isStatsLoading,
90+
refetch: refetchStats ?? (() => {}),
91+
},
8492
},
85-
},
86-
nameMenu: profile?.address ? (
87-
<ThreeDotMenu
88-
address={profile.address}
89-
profileList={profileList}
90-
primaryList={Number(profile?.primary_list)}
91-
profileName={profile?.ens?.name}
92-
isConnectedUserCard={isMyProfile ?? false}
93-
showMoreOptions={true}
94-
followState={followingState}
95-
openBlockModal={openBlockModal}
96-
openQrCodeModal={openQrCodeModal}
97-
openListSettingsModal={openListSettingsModal}
98-
/>
99-
) : null,
100-
openListSettings: openListSettingsModal,
101-
customFollowButton: <FollowButton address={profile?.address} />,
102-
}}
103-
/>
93+
nameMenu: profile?.address ? (
94+
<ThreeDotMenu
95+
address={profile.address}
96+
profileList={profileList}
97+
primaryList={Number(profile?.primary_list)}
98+
profileName={profile?.ens?.name}
99+
isConnectedUserCard={isMyProfile ?? false}
100+
showMoreOptions={true}
101+
followState={followingState}
102+
openBlockModal={openBlockModal}
103+
openQrCodeModal={openQrCodeModal}
104+
openListSettingsModal={openListSettingsModal}
105+
/>
106+
) : null,
107+
openListSettings: openListSettingsModal,
108+
customFollowButton: isMyProfile ? undefined : <FollowButton address={profile?.address} />,
109+
}}
110+
/>
111+
{ensRecordsOpen && <ENSRecordsModal name={ensRecordsName} onClose={() => setEnsRecordsOpen(false)} />}
112+
</>
104113
)
105114
}
106115

0 commit comments

Comments
 (0)