From c987a68c21d59b8418831906c5c5d18e9f3a5275 Mon Sep 17 00:00:00 2001 From: encrypted Date: Mon, 1 Jun 2026 21:46:05 +0000 Subject: [PATCH 1/6] add ENS records profile edit modal --- src/components/ens-records-modal.tsx | 92 ++++++++++++++++ src/components/user-profile-card/index.tsx | 122 ++++++++++----------- src/components/user-profile/index.tsx | 107 +++++++++--------- 3 files changed, 211 insertions(+), 110 deletions(-) create mode 100644 src/components/ens-records-modal.tsx diff --git a/src/components/ens-records-modal.tsx b/src/components/ens-records-modal.tsx new file mode 100644 index 00000000..b7f87688 --- /dev/null +++ b/src/components/ens-records-modal.tsx @@ -0,0 +1,92 @@ +'use client' + +import { useTheme } from 'next-themes' +import type React from 'react' +import { ENSRecords } from 'ethereum-identity-kit' +import { sha256 } from 'viem' +import { useAccount, useSignTypedData } from 'wagmi' + +interface ENSRecordsModalProps { + name?: string | null + onClose: () => void +} + +const dataURLToBytes = (dataUrl: string): Uint8Array => { + const base64 = dataUrl.split(',')[1] + const binary = atob(base64) + const bytes = new Uint8Array(binary.length) + + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i) + } + + return bytes +} + +const ENSRecordsModal: React.FC = ({ name, onClose }) => { + const { resolvedTheme } = useTheme() + const { address: connectedAddress } = useAccount() + const { signTypedDataAsync } = useSignTypedData() + + if (!name) return null + + const uploadImage = async (dataURL: string, type: 'avatar' | 'header') => { + if (!connectedAddress) throw new Error('Connect a wallet to upload ENS profile images') + + const urlHash = sha256(dataURLToBytes(dataURL)) + const expiry = `${Date.now() + 1000 * 60 * 60 * 24 * 7}` + const sig = await signTypedDataAsync({ + primaryType: 'Upload', + domain: { name: 'Ethereum Name Service', version: '1' }, + types: { + Upload: [ + { name: 'upload', type: 'string' }, + { name: 'expiry', type: 'string' }, + { name: 'name', type: 'string' }, + { name: 'hash', type: 'string' }, + ], + }, + message: { + upload: type, + expiry, + name, + hash: urlHash, + }, + }) + + const response = await fetch(`https://eidk.me/${encodeURIComponent(name)}${type === 'header' ? '/h' : ''}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + expiry, + dataURL, + sig, + unverifiedAddress: connectedAddress, + }), + }) + + if (!response.ok) { + if (response.status === 413) throw new Error('File size is too large (max 500KB)') + if (response.status === 415) throw new Error('Unsupported file type. Use JPG/JPEG.') + throw new Error(`Upload failed: ${response.statusText}`) + } + + const result = await response.json() + + return result.url || `https://euc.li/${encodeURIComponent(name)}${type === 'header' ? '/h' : ''}` + } + + return ( + + ) +} + +export default ENSRecordsModal diff --git a/src/components/user-profile-card/index.tsx b/src/components/user-profile-card/index.tsx index 2b73bfd1..ac27e841 100644 --- a/src/components/user-profile-card/index.tsx +++ b/src/components/user-profile-card/index.tsx @@ -1,16 +1,17 @@ 'use client' -import Link from 'next/link' import { useRouter } from 'next/navigation' +import { useState } from 'react' +import { useAccount } from 'wagmi' import { useTranslation } from 'react-i18next' import { ProfileCard } from 'ethereum-identity-kit' import { cn } from '#/lib/utilities' import Achievements from './components/achievements' import FollowButton from '#/components/follow-button' +import ENSRecordsModal from '#/components/ens-records-modal' import ThreeDotMenu from './components/three-dot-menu' import { useProfileCard } from './hooks/use-profile-card' -import EnsLogo from 'public/assets/icons/socials/ens.svg' import { useEFPProfile } from '#/contexts/efp-profile-context' import LoadingProfileCard from './components/loading-profile-card' import type { ProfileDetailsResponse, StatsResponse } from '#/types/requests' @@ -53,75 +54,74 @@ const UserProfileCard: React.FC = ({ }) => { const router = useRouter() const { t } = useTranslation() + const { address: connectedAddress } = useAccount() const { selectedList } = useEFPProfile() + const [ensRecordsOpen, setEnsRecordsOpen] = useState(false) const { followState, profileName, isConnectedUserCard } = useProfileCard(profile) + const ensRecordsName = profile?.ens?.name ?? profileName return (
{isLoading ? ( ) : profile?.address ? ( - { - router.push(`/${profile.address}?tab=${stat}&ssr=false`) - }} - showFollowerState={true} - showFollowButton={!hideFollowButton} - addressOrName={profile.address} - onProfileClick={(addressOrName) => { - router.push(`/${addressOrName}?ssr=false`) - }} - selectedList={selectedList} - className='bg-neutral' - extraOptions={{ - openListSettings: openListSettingsModal, - prefetched: { - profile: { - data: profile ?? undefined, - isLoading: !!isLoading, - refetch: refetchProfile ?? (() => {}), + <> + { + router.push(`/${profile.address}?tab=${stat}&ssr=false`) + }} + showFollowerState={true} + showFollowButton={!hideFollowButton} + addressOrName={profile.address} + onProfileClick={(addressOrName) => { + router.push(`/${addressOrName}?ssr=false`) + }} + selectedList={selectedList} + className='bg-neutral' + extraOptions={{ + openListSettings: openListSettingsModal, + onEditProfileClick: () => setEnsRecordsOpen(true), + prefetched: { + profile: { + data: profile ?? undefined, + isLoading: !!isLoading, + refetch: refetchProfile ?? (() => {}), + }, + stats: { + data: stats ?? undefined, + isLoading: !!isStatsLoading, + refetch: refetchStats ?? (() => {}), + }, }, - stats: { - data: stats ?? undefined, - isLoading: !!isStatsLoading, - refetch: refetchStats ?? (() => {}), - }, - }, - nameMenu: ( - - ), - customFollowButton: ( -
- {isConnectedUserCard ? ( - - - - ) : ( + nameMenu: ( + + ), + customFollowButton: isConnectedUserCard ? undefined : ( +
- )} -
- ), - }} - style={{ - width: '100%', - zIndex: 10, - }} - /> +
+ ), + }} + style={{ + width: '100%', + zIndex: 10, + }} + /> + {ensRecordsOpen && setEnsRecordsOpen(false)} />} + ) : (
{isRecommended ? ( diff --git a/src/components/user-profile/index.tsx b/src/components/user-profile/index.tsx index a3c56aaf..5fd46218 100644 --- a/src/components/user-profile/index.tsx +++ b/src/components/user-profile/index.tsx @@ -1,10 +1,13 @@ -import React from 'react' +'use client' + +import React, { useState } from 'react' import { useAccount } from 'wagmi' import { useRouter } from 'next/navigation' import { useWindowSize } from '@uidotdev/usehooks' import { FullWidthProfile } from 'ethereum-identity-kit' import FollowButton from '../follow-button' +import ENSRecordsModal from '#/components/ens-records-modal' import type { StatsResponse } from '#/types/requests' import useFollowingState from '#/hooks/use-following-state' import { useEFPProfile } from '#/contexts/efp-profile-context' @@ -48,59 +51,65 @@ const UserProfile: React.FC = ({ const { width } = useWindowSize() const { selectedList } = useEFPProfile() const { address: userAddress } = useAccount() + const [ensRecordsOpen, setEnsRecordsOpen] = useState(false) const { followingState } = useFollowingState({ address: profile?.address }) + const ensRecordsName = profile?.ens?.name ?? (addressOrName.endsWith('.eth') ? addressOrName : undefined) return ( - { - router.push(`/${addressOrName}?ssr=false`) - }} - onStatClick={({ addressOrName, stat }: { addressOrName: string; stat: string }) => { - router.push(`/${addressOrName}?tab=${stat}`) - }} - className={className} - style={{ - paddingBottom: width && width < 768 ? '20px' : '110px', - }} - showFollowButton={profile?.address ? true : false} - showFollowerState={true} - extraOptions={{ - role: role, - prefetched: { - profile: { - data: profile ?? undefined, - isLoading: !!isLoading, - refetch: refetchProfile ?? (() => {}), - }, - stats: { - data: stats ?? undefined, - isLoading: !!isStatsLoading, - refetch: refetchStats ?? (() => {}), + <> + { + router.push(`/${addressOrName}?ssr=false`) + }} + onStatClick={({ addressOrName, stat }: { addressOrName: string; stat: string }) => { + router.push(`/${addressOrName}?tab=${stat}`) + }} + className={className} + style={{ + paddingBottom: width && width < 768 ? '20px' : '110px', + }} + showFollowButton={profile?.address ? true : false} + showFollowerState={true} + extraOptions={{ + role: role, + onEditProfileClick: () => setEnsRecordsOpen(true), + prefetched: { + profile: { + data: profile ?? undefined, + isLoading: !!isLoading, + refetch: refetchProfile ?? (() => {}), + }, + stats: { + data: stats ?? undefined, + isLoading: !!isStatsLoading, + refetch: refetchStats ?? (() => {}), + }, }, - }, - nameMenu: profile?.address ? ( - - ) : null, - openListSettings: openListSettingsModal, - customFollowButton: , - }} - /> + nameMenu: profile?.address ? ( + + ) : null, + openListSettings: openListSettingsModal, + customFollowButton: isMyProfile ? undefined : , + }} + /> + {ensRecordsOpen && setEnsRecordsOpen(false)} />} + ) } From 0f9c1888d4b971078bccbf0ebd91386ab10080d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=BDunec?= Date: Wed, 3 Jun 2026 02:15:08 +0200 Subject: [PATCH 2/6] make modal, ensure proper data refetching --- .gitignore | 3 + bun.lockb | Bin 645520 -> 645520 bytes package.json | 2 +- src/app/[user]/components/user-info.tsx | 2 + src/components/ens-records-modal.tsx | 90 ++++++++++++++++++++---- src/components/user-profile/index.tsx | 12 +++- 6 files changed, 94 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 08e83746..5ed5eb5a 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,6 @@ public/*.js package-lock.json yarn.lock pnpm-lock.yaml + +# local EIK test script +/scripts/eik-local.sh \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index d6a93767891e56fad7ae5ad055ce05533fcd4dd0..063d2b95f40693c292b80c9d3039f47f3f5f4d98 100755 GIT binary patch delta 179 zcmbQRS$zT!=_#BHdHI;<@tk?V%Nagg^Sbfr{2%7Bg5_`0o|sszpUHi8Nu%FZKgM4g zj6RbKERWQ%{&GDmg(DOytVdgxAkTQVwUZ;-mE8o fFq*ZqyRZQ<`*wC0j!m6F{;sne+jpJiyv7XxFr`mK delta 179 zcmbQRS$zT!=_#1LO^=M@*;n%KM@Pb{@b)RDVH&?!%uoGKIriXYl}GpCM!&6ojK4G( zy(SmNsZZVz$I)J)$q2+u+bcAgTNNFaft|eb-seYuo^;!A|A? diff --git a/package.json b/package.json index 566d60fb..9e78a9cd 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@vercel/kv": "^3.0.0", "caniuse-lite": "^1.0.30001687", "clsx": "^2.1.0", - "ethereum-identity-kit": "^0.2.74", + "ethereum-identity-kit": "^0.2.76", "i18next": "^23.11.5", "i18next-browser-languagedetector": "^8.0.0", "i18next-http-backend": "^2.5.2", diff --git a/src/app/[user]/components/user-info.tsx b/src/app/[user]/components/user-info.tsx index 77cb0bcd..13b48714 100644 --- a/src/app/[user]/components/user-info.tsx +++ b/src/app/[user]/components/user-info.tsx @@ -65,6 +65,7 @@ const UserInfo: React.FC = ({ user }) => { followersTagsFilter, followerTagsLoading, followingTagsLoading, + setFetchFreshProfile, isFetchingMoreFollowers, isFetchingMoreFollowing, setFollowersTagsFilter, @@ -195,6 +196,7 @@ const UserInfo: React.FC = ({ user }) => { }} openQrCodeModal={() => setQrCodeModalOpen(true)} openListSettingsModal={() => setListSettingsOpen(true)} + setFetchFreshProfile={setFetchFreshProfile} />
diff --git a/src/components/ens-records-modal.tsx b/src/components/ens-records-modal.tsx index b7f87688..8e55c602 100644 --- a/src/components/ens-records-modal.tsx +++ b/src/components/ens-records-modal.tsx @@ -1,14 +1,18 @@ 'use client' +import React from 'react' +import { createPortal } from 'react-dom' +import { sha256 } from 'viem' import { useTheme } from 'next-themes' -import type React from 'react' import { ENSRecords } from 'ethereum-identity-kit' -import { sha256 } from 'viem' import { useAccount, useSignTypedData } from 'wagmi' +import type { SetStateAction, Dispatch } from 'react' +import { useQueryClient } from '@tanstack/react-query' interface ENSRecordsModalProps { name?: string | null onClose: () => void + setFetchFreshProfile?: Dispatch> } const dataURLToBytes = (dataUrl: string): Uint8Array => { @@ -23,10 +27,44 @@ const dataURLToBytes = (dataUrl: string): Uint8Array => { return bytes } -const ENSRecordsModal: React.FC = ({ name, onClose }) => { +const forceRefetchImage = (rawUrl: string) => { + if (typeof window === 'undefined' || !rawUrl) return + + let target: URL + try { + target = new URL(rawUrl, window.location.href) + } catch { + return + } + + const targetKey = `${target.origin}${target.pathname}` + void fetch(target.toString(), { cache: 'reload', mode: 'no-cors' }).catch(() => {}) + + const cacheBust = Date.now().toString() + for (const img of Array.from(document.images)) { + const current = img.currentSrc || img.src + if (!current) continue + + let candidate: URL + try { + candidate = new URL(current, window.location.href) + } catch { + continue + } + + if (`${candidate.origin}${candidate.pathname}` !== targetKey) continue + + const refreshed = new URL(target.toString()) + refreshed.searchParams.set('_cb', cacheBust) + img.src = refreshed.toString() + } +} + +const ENSRecordsModal: React.FC = ({ name, onClose, setFetchFreshProfile }) => { const { resolvedTheme } = useTheme() const { address: connectedAddress } = useAccount() const { signTypedDataAsync } = useSignTypedData() + const queryClient = useQueryClient() if (!name) return null @@ -73,19 +111,47 @@ const ENSRecordsModal: React.FC = ({ name, onClose }) => { throw new Error(`Upload failed: ${response.statusText}`) } + await new Promise((resolve) => setTimeout(resolve, 1000)) // wait for image to be uploaded to euc.li + const result = await response.json() + const finalUrl = result.url || `https://euc.li/${encodeURIComponent(name)}${type === 'header' ? '/h' : ''}` - return result.url || `https://euc.li/${encodeURIComponent(name)}${type === 'header' ? '/h' : ''}` + forceRefetchImage(finalUrl) + + return finalUrl + } + + const onSuccess = () => { + queryClient.refetchQueries({ queryKey: ['profile', name, true] }) + setFetchFreshProfile?.(true) } - return ( - + const modalRoot = typeof document === 'undefined' ? null : document.getElementById('modal-root') + if (!modalRoot) return null + + // EFP owns the modal chrome (overlay + backdrop + click-outside-to-close). + // The ENSRecords component from ethereum-identity-kit renders only its own card + // content — it deliberately does not provide a fixed overlay of its own. + return createPortal( +
+
e.stopPropagation()} + className='bg-neutral mt-12 h-fit min-w-[min(32rem,100%)] overflow-hidden rounded-md' + > + +
+
, + modalRoot ) } diff --git a/src/components/user-profile/index.tsx b/src/components/user-profile/index.tsx index 5fd46218..1b9d6b18 100644 --- a/src/components/user-profile/index.tsx +++ b/src/components/user-profile/index.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useState } from 'react' +import React, { useState, type Dispatch, type SetStateAction } from 'react' import { useAccount } from 'wagmi' import { useRouter } from 'next/navigation' import { useWindowSize } from '@uidotdev/usehooks' @@ -29,6 +29,7 @@ interface UserProfileCardProps { refetchStats?: () => void openQrCodeModal?: () => void className?: string + setFetchFreshProfile?: Dispatch> } const UserProfile: React.FC = ({ @@ -46,6 +47,7 @@ const UserProfile: React.FC = ({ refetchProfile, refetchStats, className, + setFetchFreshProfile, }) => { const router = useRouter() const { width } = useWindowSize() @@ -108,7 +110,13 @@ const UserProfile: React.FC = ({ customFollowButton: isMyProfile ? undefined : , }} /> - {ensRecordsOpen && setEnsRecordsOpen(false)} />} + {ensRecordsOpen && ( + setEnsRecordsOpen(false)} + setFetchFreshProfile={setFetchFreshProfile} + /> + )} ) } From e7d4305c4dbbe0c390ae37a9635e164957d183f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=BDunec?= Date: Wed, 3 Jun 2026 02:21:30 +0200 Subject: [PATCH 3/6] format + type fixes --- src/components/ens-records-modal.tsx | 6 +++--- src/components/user-profile/index.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ens-records-modal.tsx b/src/components/ens-records-modal.tsx index 8e55c602..d77fd57d 100644 --- a/src/components/ens-records-modal.tsx +++ b/src/components/ens-records-modal.tsx @@ -12,12 +12,12 @@ import { useQueryClient } from '@tanstack/react-query' interface ENSRecordsModalProps { name?: string | null onClose: () => void - setFetchFreshProfile?: Dispatch> + setFetchFreshProfile?: Dispatch> | ((state: boolean) => void) } const dataURLToBytes = (dataUrl: string): Uint8Array => { const base64 = dataUrl.split(',')[1] - const binary = atob(base64) + const binary = atob(base64 || '') const bytes = new Uint8Array(binary.length) for (let i = 0; i < binary.length; i++) { @@ -113,7 +113,7 @@ const ENSRecordsModal: React.FC = ({ name, onClose, setFet await new Promise((resolve) => setTimeout(resolve, 1000)) // wait for image to be uploaded to euc.li - const result = await response.json() + const result = (await response.json()) as { url?: string } const finalUrl = result.url || `https://euc.li/${encodeURIComponent(name)}${type === 'header' ? '/h' : ''}` forceRefetchImage(finalUrl) diff --git a/src/components/user-profile/index.tsx b/src/components/user-profile/index.tsx index 1b9d6b18..952a4931 100644 --- a/src/components/user-profile/index.tsx +++ b/src/components/user-profile/index.tsx @@ -29,7 +29,7 @@ interface UserProfileCardProps { refetchStats?: () => void openQrCodeModal?: () => void className?: string - setFetchFreshProfile?: Dispatch> + setFetchFreshProfile?: Dispatch> | ((state: boolean) => void) } const UserProfile: React.FC = ({ From b66f64b93ae900cd5f4eb6db38432b88792ac9a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=BDunec?= Date: Wed, 3 Jun 2026 12:58:28 +0200 Subject: [PATCH 4/6] increase timeout + type fixes --- src/app/swipe/components/recommended-profiles.tsx | 2 -- src/components/ens-records-modal.tsx | 10 ++++++++-- src/components/profile-tooltip-wrapper.tsx | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/app/swipe/components/recommended-profiles.tsx b/src/app/swipe/components/recommended-profiles.tsx index 5de95507..fd4cba55 100644 --- a/src/app/swipe/components/recommended-profiles.tsx +++ b/src/app/swipe/components/recommended-profiles.tsx @@ -141,13 +141,11 @@ const RecommendedCards = () => { if (gone.has(i + 3)) return null return ( - // @ts-expect-error animated.div is a valid component - {/* @ts-expect-error animated.div is a valid component */} = ({ name, onClose, setFet } const onSuccess = () => { - queryClient.refetchQueries({ queryKey: ['profile', name, true] }) - setFetchFreshProfile?.(true) + setTimeout(() => { + // @ts-expect-error - both types support function calls with a state argument + setFetchFreshProfile?.((state) => { + if (state) queryClient.refetchQueries({ queryKey: ['profile', connectedAddress] }) + return true + }) + onClose() + }, 1500) } const modalRoot = typeof document === 'undefined' ? null : document.getElementById('modal-root') diff --git a/src/components/profile-tooltip-wrapper.tsx b/src/components/profile-tooltip-wrapper.tsx index fbc0ec50..2e6629ca 100644 --- a/src/components/profile-tooltip-wrapper.tsx +++ b/src/components/profile-tooltip-wrapper.tsx @@ -43,7 +43,7 @@ const ProfileTooltipWrapper: React.FC = ({ router.push(`/${addressOrName}?ssr=false`) }} > - {children as React.ReactElement} + {children} ) } From 0bb9555930f944d4fa23e454f64a629c71e83a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=BDunec?= Date: Wed, 3 Jun 2026 13:12:20 +0200 Subject: [PATCH 5/6] add back ts ignore for animated divs --- src/app/swipe/components/recommended-profiles.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/swipe/components/recommended-profiles.tsx b/src/app/swipe/components/recommended-profiles.tsx index fd4cba55..5de95507 100644 --- a/src/app/swipe/components/recommended-profiles.tsx +++ b/src/app/swipe/components/recommended-profiles.tsx @@ -141,11 +141,13 @@ const RecommendedCards = () => { if (gone.has(i + 3)) return null return ( + // @ts-expect-error animated.div is a valid component + {/* @ts-expect-error animated.div is a valid component */} Date: Thu, 4 Jun 2026 00:44:25 +0200 Subject: [PATCH 6/6] bump up EIK version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 604f23ad..a99ae57c 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@vercel/kv": "^3.0.0", "caniuse-lite": "^1.0.30001687", "clsx": "^2.1.0", - "ethereum-identity-kit": "^0.2.76", + "ethereum-identity-kit": "^0.2.77", "i18next": "^23.11.5", "i18next-browser-languagedetector": "^8.0.0", "i18next-http-backend": "^2.5.2",