From aac58a03fd9c2bbc1fc8d37be7f8bd5246692b64 Mon Sep 17 00:00:00 2001 From: Juan M Date: Fri, 19 May 2023 23:43:43 +0200 Subject: [PATCH 1/5] Address profile --- src/app/App.js | 122 ++++++++------- src/components/AddressCollector.js | 39 ++--- src/components/AddressOwner.js | 23 ++- src/components/AddressProfile.js | 122 +++++++++++++++ src/components/ButtonAddressProfile.js | 47 ++++++ src/components/ButtonLink.js | 4 +- src/components/InCommon.js | 5 +- src/images/POAP_Stamp.svg | 134 ++++++++++++++++ src/loaders/ethereum.js | 58 +++---- src/models/ethereum.js | 9 +- src/models/poap.js | 10 +- src/pages/Addresses.js | 4 +- src/pages/Events.js | 29 +++- src/stores/ethereum.js | 203 ++++++++++++++++++++----- src/styles/address-profile.css | 29 ++++ src/styles/button-address-profile.css | 20 +++ src/styles/owner.css | 41 +++++ 17 files changed, 716 insertions(+), 183 deletions(-) create mode 100644 src/components/AddressProfile.js create mode 100644 src/components/ButtonAddressProfile.js create mode 100644 src/images/POAP_Stamp.svg create mode 100644 src/styles/address-profile.css create mode 100644 src/styles/button-address-profile.css diff --git a/src/app/App.js b/src/app/App.js index 9963f8a..73ef13c 100644 --- a/src/app/App.js +++ b/src/app/App.js @@ -16,7 +16,7 @@ import Admin from './Admin' import Addresses from '../pages/Addresses' import { AdminProvider } from '../stores/admin' import { SettingsProvider } from '../stores/cache' -import { EnsProvider, ReverseEnsProvider } from '../stores/ethereum' +import { EnsProvider } from '../stores/ethereum' import { HTMLProvider } from '../stores/html' import '../styles/fonts.css' import '../styles/app.css' @@ -28,67 +28,65 @@ function App() { - - - , - errorElement: , - children: [ - { - index: true, - element: , - }, - { - path: '/r/event/:eventId', - loader: eventRedirect, - }, - { - path: '/event/:eventId', - loader: eventLoader, - element: , - errorElement: , - }, - { - path: '/r/events/:eventIds', - loader: eventsRedirect, - }, - { - path: '/events/:eventIds', - loader: eventsLoader, - element: , - errorElement: , - }, - { - path: '/addresses', - element: , - }, - { - path: '/last', - element: , - }, - { - path: '/settings', - element: , - }, - { - element: , - children: [ - { - path: '/feedback', - element: , - }, - ], - }, - ], - }, - ])} - fallbackElement={} - /> - - + + , + errorElement: , + children: [ + { + index: true, + element: , + }, + { + path: '/r/event/:eventId', + loader: eventRedirect, + }, + { + path: '/event/:eventId', + loader: eventLoader, + element: , + errorElement: , + }, + { + path: '/r/events/:eventIds', + loader: eventsRedirect, + }, + { + path: '/events/:eventIds', + loader: eventsLoader, + element: , + errorElement: , + }, + { + path: '/addresses', + element: , + }, + { + path: '/last', + element: , + }, + { + path: '/settings', + element: , + }, + { + element: , + children: [ + { + path: '/feedback', + element: , + }, + ], + }, + ], + }, + ])} + fallbackElement={} + /> + diff --git a/src/components/AddressCollector.js b/src/components/AddressCollector.js index 49bdb08..a42e518 100644 --- a/src/components/AddressCollector.js +++ b/src/components/AddressCollector.js @@ -1,39 +1,24 @@ import { useContext } from 'react' +import { POAP_SCAN_URL } from '../models/poap' import { ReverseEnsContext } from '../stores/ethereum' import '../styles/collector.css' -function AddressCollector({ address, ens, bigEns = false, short = false, linkToScan = true }) { +function AddressCollector({ address, ens, bigEns = false, short = false }) { const { ensNames } = useContext(ReverseEnsContext) return (
- {linkToScan - ? ( - - {address && ( - - {address.substring(0, 10)}…{address.substr(-8)} - - )} - {!short && address && ( - {address} - )} - - ) - : ( - <> - {address && ( - - {address.substring(0, 10)}…{address.substr(-8)} - - )} - {!short && address && ( - {address} - )} - - ) - } + + {address && ( + + {address.substring(0, 10)}…{address.substr(-8)} + + )} + {!short && address && ( + {address} + )} +
{(ens || (address && ensNames[address])) && (
diff --git a/src/components/AddressOwner.js b/src/components/AddressOwner.js index 2349892..1eeab2a 100644 --- a/src/components/AddressOwner.js +++ b/src/components/AddressOwner.js @@ -1,23 +1,20 @@ -import { useContext } from 'react' -import { ReverseEnsContext } from '../stores/ethereum' +import { POAP_SCAN_URL } from '../models/poap' +import POAP_Stamp from '../images/POAP_Stamp.svg' import TokenImage from './TokenImage' +import ButtonAddressProfile from './ButtonAddressProfile' import '../styles/owner.css' -function AddressOwner({ owner, ownerEvents, eventIds, linkToScan = true }) { - const { ensNames } = useContext(ReverseEnsContext) - +function AddressOwner({ owner, ownerEvents, eventIds, linkToScan = false }) { return (
- {linkToScan - ? ( - - {owner in ensNames ? {ensNames[owner]} : {owner}} - - ) - : (owner in ensNames ? {ensNames[owner]} : {owner}) - } +
+ {linkToScan && ( + + {`Scan + + )} {ownerEvents && typeof ownerEvents === 'object' && eventIds && Array.isArray(eventIds) && (
{eventIds.map( diff --git a/src/components/AddressProfile.js b/src/components/AddressProfile.js new file mode 100644 index 0000000..1622c2f --- /dev/null +++ b/src/components/AddressProfile.js @@ -0,0 +1,122 @@ +import { useContext, useEffect, useState } from 'react' +import { LazyImage } from 'react-lazy-images' +import { OpenNewWindow } from 'iconoir-react' +import { POAP_SCAN_URL } from '../models/poap' +import { ResolverEnsContext, ReverseEnsContext } from '../stores/ethereum' +import { scanAddress } from '../loaders/poap' +import ErrorMessage from './ErrorMessage' +import Loading from './Loading' +import '../styles/address-profile.css' + +function AddressProfile({ + address, + events = {}, + showInCommon = true, +}) { + const { avatars, resolveMeta } = useContext(ResolverEnsContext) + const { ensNames } = useContext(ReverseEnsContext) + const [loading, setLoading] = useState(0) + const [error, setError] = useState(null) + const [poaps, setPOAPs] = useState(null) + + useEffect( + () => { + if ( + address in ensNames && + avatars[ensNames[address]] === undefined && + !error + ) { + setLoading((prevLoading) => prevLoading++) + resolveMeta(ensNames[address]).then( + (meta) => { + setLoading((prevLoading) => prevLoading--) + }, + (err) => { + setLoading((prevLoading) => prevLoading--) + setError(err) + } + ) + } + }, + [address, avatars, ensNames, resolveMeta, error] + ) + + useEffect( + () => { + let controller + if ( + poaps === null && + !error + ) { + controller = new AbortController() + setLoading((prevLoading) => prevLoading++) + scanAddress(address, controller.signal).then( + (foundPOAPs) => { + setLoading((prevLoading) => prevLoading--) + setPOAPs(foundPOAPs) + }, + (err) => { + setLoading((prevLoading) => prevLoading--) + setError(err) + setPOAPs([]) + } + ) + } + return () => { + if (controller) { + controller.abort() + } + } + }, + [address, poaps, error] + ) + + return ( +
+ {loading > 0 && ( + + )} + {error && ( + +

{error.message}

+
+ )} + {!loading && !error && ( + <> + {address in ensNames && avatars[ensNames[address]] && avatars[ensNames[address]].startsWith('http') && ( + ( + // eslint-disable-line jsx-a11y/alt-text + )} + actual={({ imageProps }) => ( + // eslint-disable-line jsx-a11y/alt-text + )} + loading={() => ( +
+ +
+ )} + error={() => ( + +

Avatar could not be loaded

+
+ )} + /> + )} + + {address} + + + {address in ensNames && ( + {ensNames[address]} + )} + + )} +
+ ) +} + +export default AddressProfile diff --git a/src/components/ButtonAddressProfile.js b/src/components/ButtonAddressProfile.js new file mode 100644 index 0000000..3112d64 --- /dev/null +++ b/src/components/ButtonAddressProfile.js @@ -0,0 +1,47 @@ +import { useContext, useState } from 'react' +import ReactModal from 'react-modal' +import { ReverseEnsContext } from '../stores/ethereum' +import Card from './Card' +import ButtonClose from './ButtonClose' +import ButtonLink from './ButtonLink' +import AddressProfile from './AddressProfile' +import '../styles/button-address-profile.css' + +function ButtonAddressProfile({ + address, + events = {}, +}) { + const { ensNames } = useContext(ReverseEnsContext) + const [showModal, setShowModal] = useState(false) + + return ( + <> + setShowModal((show) => !show)} + > + {address in ensNames + ? {ensNames[address]} + : {address} + } + + setShowModal(false)} + shouldCloseOnEsc={true} + shouldCloseOnOverlayClick={true} + contentLabel={address in ensNames ? ensNames[address] : address} + className="button-address-profile-modal" + > +
+ + setShowModal(false)} /> + + +
+
+ + ) +} + +export default ButtonAddressProfile diff --git a/src/components/ButtonLink.js b/src/components/ButtonLink.js index ca93327..14d6195 100644 --- a/src/components/ButtonLink.js +++ b/src/components/ButtonLink.js @@ -1,8 +1,8 @@ import '../styles/button-link.css' -function ButtonLink({ onClick, children }) { +function ButtonLink({ onClick, className, children }) { return ( - ) diff --git a/src/components/InCommon.js b/src/components/InCommon.js index a762c3e..0f1527b 100644 --- a/src/components/InCommon.js +++ b/src/components/InCommon.js @@ -204,7 +204,10 @@ function InCommon({ onMouseEnter={() => onOwnerEnter(activeEventId, owner)} onMouseLeave={() => onOwnerLeave(activeEventId, owner)} > - + )} diff --git a/src/images/POAP_Stamp.svg b/src/images/POAP_Stamp.svg new file mode 100644 index 0000000..796da25 --- /dev/null +++ b/src/images/POAP_Stamp.svg @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/loaders/ethereum.js b/src/loaders/ethereum.js index 45c4059..31cab80 100644 --- a/src/loaders/ethereum.js +++ b/src/loaders/ethereum.js @@ -1,17 +1,15 @@ -import { CloudflareProvider, InfuraProvider } from '@ethersproject/providers' +import { InfuraProvider } from '@ethersproject/providers' import { Contract } from '@ethersproject/contracts' -import { INFURA_API_KEY, MAINNET_ENS_REVERSE_RECORDS } from '../models/ethereum' +import { + INFURA_API_KEY, + MAINNET_ENS_REVERSE_RECORDS, + ENS_REVERSE_RECORDS_BATCH_SIZE, +} from '../models/ethereum' -function getMainnetProvider() { - return new CloudflareProvider('mainnet') -} - -function getMainnetEnsProvider() { - return new InfuraProvider('mainnet', INFURA_API_KEY) -} - -function getMainnetEnsReverseRecordsABI() { - return [ +const ensProvider = new InfuraProvider('mainnet', INFURA_API_KEY) +const ensReverseRecordsContract = new Contract( + MAINNET_ENS_REVERSE_RECORDS, + [ { inputs: [{ internalType: 'contract ENS', name: '_ens', type: 'address' }], stateMutability: 'nonpayable', @@ -24,30 +22,30 @@ function getMainnetEnsReverseRecordsABI() { stateMutability: 'view', type: 'function', }, - ] -} - -function getMainnetEnsReverseRecordsContract() { - return new Contract( - MAINNET_ENS_REVERSE_RECORDS, - getMainnetEnsReverseRecordsABI(), - getMainnetEnsProvider() - ) -} - -const ensReverseRecordsContract = getMainnetEnsReverseRecordsContract() + ], + ensProvider +) -async function resolveEnsNames(addresses, limit = 300) { +async function resolveEnsNames( + addresses, + onProgress = (resolved) => {}, + limit = ENS_REVERSE_RECORDS_BATCH_SIZE +) { const resolvedAddresses = {} for (let i = 0; i < addresses.length; i += limit) { const chunk = addresses.slice(i, i + limit) try { const names = await ensReverseRecordsContract.getNames(chunk) + const resolved = {} for (let i = 0; i < names.length; i++) { if (names[i] !== '') { resolvedAddresses[chunk[i]] = names[i] + resolved[chunk[i]] = names[i] } } + if (Object.keys(resolved).length > 0) { + onProgress(resolved) + } } catch (err) { i = i - limit + 1 continue @@ -56,10 +54,12 @@ async function resolveEnsNames(addresses, limit = 300) { return resolvedAddresses } -const provider = getMainnetProvider() - async function resolveAddress(ensName) { - return await provider.resolveName(ensName) + return await ensProvider.resolveName(ensName) +} + +async function resolveEnsAvatar(ensName) { + return await ensProvider.getAvatar(ensName) } -export { resolveEnsNames, resolveAddress } +export { resolveEnsNames, resolveAddress, resolveEnsAvatar } diff --git a/src/models/ethereum.js b/src/models/ethereum.js index 3227a46..f7d5184 100644 --- a/src/models/ethereum.js +++ b/src/models/ethereum.js @@ -1,4 +1,11 @@ const INFURA_API_KEY = process.env.REACT_APP_INFURA_API_KEY const MAINNET_ENS_REVERSE_RECORDS = '0x3671aE578E63FdF66ad4F3E12CC0c0d71Ac7510C' +const ENS_REVERSE_RECORDS_BATCH_SIZE = 300 +const ENS_RESOLVE_BATCH_SIZE = 6 -export { INFURA_API_KEY, MAINNET_ENS_REVERSE_RECORDS } +export { + INFURA_API_KEY, + MAINNET_ENS_REVERSE_RECORDS, + ENS_REVERSE_RECORDS_BATCH_SIZE, + ENS_RESOLVE_BATCH_SIZE, +} diff --git a/src/models/poap.js b/src/models/poap.js index 633e2e0..b59daff 100644 --- a/src/models/poap.js +++ b/src/models/poap.js @@ -15,4 +15,12 @@ function POAP(poap) { } } -export { POAP_FETCH_RETRIES, POAP_API_URL, POAP_API_KEY, POAP } +const POAP_SCAN_URL = 'https://app.poap.xyz/scan' + +export { + POAP_FETCH_RETRIES, + POAP_API_URL, + POAP_API_KEY, + POAP, + POAP_SCAN_URL, +} diff --git a/src/pages/Addresses.js b/src/pages/Addresses.js index 1c854ee..b351d4c 100644 --- a/src/pages/Addresses.js +++ b/src/pages/Addresses.js @@ -4,7 +4,7 @@ import { formatStat } from '../utils/number' import { parseAddress, parseAddresses } from '../models/address' import { parseEventIds } from '../models/event' import { HTMLContext } from '../stores/html' -import { EnsContext, ReverseEnsContext } from '../stores/ethereum' +import { ResolverEnsContext, ReverseEnsContext } from '../stores/ethereum' import { fetchPOAPs, scanAddress } from '../loaders/poap' import { getEventsOwners } from '../loaders/api' import AddressesForm from '../components/AddressesForm' @@ -35,7 +35,7 @@ function Addresses() { const navigate = useNavigate() const [searchParams] = useSearchParams() const { setTitle } = useContext(HTMLContext) - const { resolveAddress, addresses: addressByEnsName } = useContext(EnsContext) + const { resolveAddress, addresses: addressByEnsName } = useContext(ResolverEnsContext) const { resolveEnsNames, setEnsName, ensNames, isNotFound } = useContext(ReverseEnsContext) const [editMode, setEditMode] = useState(false) const [state, setState] = useState(STATE_INIT_PARSING) diff --git a/src/pages/Events.js b/src/pages/Events.js index 1507e72..9936d39 100644 --- a/src/pages/Events.js +++ b/src/pages/Events.js @@ -292,9 +292,19 @@ function Events() { if (newOwners) { setOwners((oldOwners) => ({ ...oldOwners, ...newOwners })) if (Object.keys(newOwners).length === eventIds.length) { - resolveEnsNames([...new Set( - Object.values(newOwners).reduce((allOwners, eventOwners) => ([ ...allOwners, ...eventOwners ]), []), - )]) + resolveEnsNames( + [ + ...new Set( + Object.values(newOwners).reduce( + (allOwners, eventOwners) => ([ + ...allOwners, + ...eventOwners, + ]), + [] + ), + ), + ] + ) setStatus(STATUS_LOADING_SCANS) } else { setStatus(STATUS_LOADING_OWNERS) @@ -412,9 +422,16 @@ function Events() { () => { if (status === STATUS_INITIAL) { if (Object.keys(events).length === Object.keys(owners).length) { - resolveEnsNames([...new Set( - Object.values(owners).reduce((allOwners, eventOwners) => ([ ...allOwners, ...eventOwners ]), []), - )]) + resolveEnsNames( + [ + ...new Set( + Object.values(owners).reduce( + (allOwners, eventOwners) => ([ ...allOwners, ...eventOwners ]), + [] + ), + ) + ] + ) setStatus(STATUS_LOADING_SCANS) } } else if (status === STATUS_LOADING_OWNERS) { diff --git a/src/stores/ethereum.js b/src/stores/ethereum.js index 52a5aff..6c48eb1 100644 --- a/src/stores/ethereum.js +++ b/src/stores/ethereum.js @@ -1,58 +1,183 @@ -import { createContext, useState } from 'react' -import { resolveAddress, resolveEnsNames } from '../loaders/ethereum' +import { createContext, useCallback, useContext, useMemo, useState } from 'react' +import { ENS_RESOLVE_BATCH_SIZE } from '../models/ethereum' +import { + resolveAddress as ethereumResolveAddress, + resolveEnsNames as ethereumResolveEnsNames, + resolveEnsAvatar as ethereumResolveEnsAvatar +} from '../loaders/ethereum' -const ReverseEnsContext = createContext({}) +const ResolverEnsContext = createContext({ + addresses: {}, + resolveAddress: async (ensName) => {}, + avatars: {}, + resolveMeta: async (ensName) => {}, + resolve: async (ensName, full = false) => {}, +}) -function ReverseEnsProvider({ children }) { +const ReverseEnsContext = createContext({ + ensNames: {}, + resolveEnsNames: async (addresses, resolve = false) => {}, + setEnsName: (address, ensName) => {}, + isNotFound: (address) => {}, +}) + +function ResolverEnsProvider({ children }) { + const [addressByEnsName, setAddressByEnsName] = useState({}) + const [avatarByEnsName, setAvatarByEnsName] = useState({}) + const resolveAddress = useCallback( + async (ensName) => { + if (Object.keys(addressByEnsName).indexOf(ensName) !== -1) { + return addressByEnsName[ensName] + } + const address = await ethereumResolveAddress(ensName) + if (address) { + setAddressByEnsName((oldAddressByEnsName) => ({ ...oldAddressByEnsName, [ensName]: address })) + } + return address + }, + [addressByEnsName] + ) + const resolveAvatar = useCallback( + async (ensName) => { + if (Object.keys(avatarByEnsName).indexOf(ensName) !== -1) { + return avatarByEnsName[ensName] + } + const avatar = await ethereumResolveEnsAvatar(ensName) + if (avatar) { + setAvatarByEnsName((oldAvatarByEnsName) => ({ ...oldAvatarByEnsName, [ensName]: avatar })) + } + return avatar + }, + [avatarByEnsName] + ) + const resolveMeta = useCallback( + async (ensName) => { + const avatar = await resolveAvatar(ensName) + return { avatar } + }, + [resolveAvatar] + ) + const resolve = useCallback( + async (ensName, full = false) => { + const [address, meta] = await Promise.all([ + resolveAddress(ensName), + resolveMeta(ensName), + ]) + if (full) { + return { address, meta } + } else { + return address + } + }, + [resolveAddress, resolveMeta] + ) + const value = useMemo( + () => ({ + addresses: addressByEnsName, + resolveAddress, + avatars: avatarByEnsName, + resolveMeta, + resolve, + }), + [addressByEnsName, resolveAddress, avatarByEnsName, resolveMeta, resolve] + ) + return ( + + {children} + + ) +} + +function ReverseEnsProvider({ + children, + limitEnsNames = ENS_RESOLVE_BATCH_SIZE, +}) { + const { resolveMeta } = useContext(ResolverEnsContext) const [ensByAddress, setEnsByAddress] = useState({}) const [notFoundAddresses, setNotFoundAddresses] = useState([]) - async function resolve(addresses) { - const oldAddresses = Object.keys(ensByAddress) - const newAddresses = addresses.filter((address) => oldAddresses.indexOf(address) === -1) - const ensNames = await resolveEnsNames(newAddresses) - const notFoundEnsNames = newAddresses.filter((address) => !(address in ensNames)) - setEnsByAddress((oldEnsByAddress) => ({ ...oldEnsByAddress, ...ensNames })) - setNotFoundAddresses((oldNotFoundByAddress) => ([...new Set([...oldNotFoundByAddress, ...notFoundEnsNames])])) - return Object.fromEntries( - addresses - .map( - (address) => ([address, ensNames[address] ?? ensByAddress[address] ?? null]) + const resolveNames = useCallback( + (names) => { + let promise = new Promise((r) => r()) + for (let i = 0; i < names.length; i += limitEnsNames) { + promise = promise.then( + () => Promise.allSettled( + names.slice(i, i + limitEnsNames).map((name) => resolveMeta(name)) + ) ) - .filter(([_, value]) => value) - ) - } + } + return promise + }, + [limitEnsNames, resolveMeta] + ) + const resolveEnsNames = useCallback( + async (addresses, resolve = false) => { + const oldAddresses = Object.keys(ensByAddress) + const givenOldAddresses = oldAddresses.filter((address) => oldAddresses.indexOf(address) !== -1) + if (givenOldAddresses.length > 0) { + resolveNames(givenOldAddresses.map((address) => ensByAddress[address])) + } + const newAddresses = addresses.filter((address) => oldAddresses.indexOf(address) === -1) + const ensNames = await ethereumResolveEnsNames(newAddresses, (resolved) => { + setEnsByAddress((oldEnsByAddress) => ({ ...oldEnsByAddress, ...resolved })) + if (resolve) { + resolveNames(Object.values(resolved)) + } + }) + setNotFoundAddresses( + (oldNotFoundByAddress) => ([ + ...new Set([ + ...oldNotFoundByAddress, + ...newAddresses.filter((address) => !(address in ensNames)), + ]) + ]) + ) + return Object.fromEntries( + addresses + .map( + (address) => ([address, ensNames[address] ?? ensByAddress[address] ?? null]) + ) + .filter(([_, value]) => value) + ) + }, + [ensByAddress, resolveNames] + ) function set(address, ensName) { setEnsByAddress((oldEnsByAddress) => ({ ...oldEnsByAddress, [address]: ensName })) } - function isNotFound(address) { - return notFoundAddresses.indexOf(address) !== -1 - } + const isNotFound = useCallback( + (address) => { + return notFoundAddresses.indexOf(address) !== -1 + }, + [notFoundAddresses] + ) + const value = useMemo( + () => ({ + ensNames: ensByAddress, + resolveEnsNames, + setEnsName: set, + isNotFound, + }), + [ensByAddress, isNotFound, resolveEnsNames] + ) return ( - + {children} ) } -const EnsContext = createContext({}) - function EnsProvider({ children }) { - const [addressByEnsName, setAddressByEnsName] = useState({}) - async function resolve(ensName) { - if (Object.keys(addressByEnsName).indexOf(ensName) !== -1) { - return addressByEnsName[ensName] - } - const address = await resolveAddress(ensName) - if (address) { - setAddressByEnsName((oldAddressByEnsName) => ({ ...oldAddressByEnsName, [ensName]: address })) - } - return address - } return ( - - {children} - + + + {children} + + ) } -export { ReverseEnsContext, ReverseEnsProvider, EnsContext, EnsProvider } +export { + ReverseEnsContext, + ResolverEnsContext, + EnsProvider, +} diff --git a/src/styles/address-profile.css b/src/styles/address-profile.css new file mode 100644 index 0000000..ec80ffc --- /dev/null +++ b/src/styles/address-profile.css @@ -0,0 +1,29 @@ +.address-profile { + padding: 2rem; + display: flex; + flex-direction: column; + align-items: center; + gap: .5rem; +} + +.profile-avatar { + border-radius: 1rem; + border-width: 0; + width: 128px; + height: 128px; + background-color: #efefef; +} + +.profile-avatar .loading { + position: relative; + top: 48px; + left: 48px; +} + +.profile-ens { + font-size: 160%; +} + +.profile-address svg { + margin-left: .25rem; +} diff --git a/src/styles/button-address-profile.css b/src/styles/button-address-profile.css new file mode 100644 index 0000000..fee34fb --- /dev/null +++ b/src/styles/button-address-profile.css @@ -0,0 +1,20 @@ +.button-address-profile { + font-size: 100%; +} + +.button-address-profile-modal { + height: 100%; +} + +.button-address-profile-container { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + +.button-address-profile-container .button-close { + position: absolute; + top: 12px; + right: 24px; +} diff --git a/src/styles/owner.css b/src/styles/owner.css index b2aa61b..e3d2bd9 100644 --- a/src/styles/owner.css +++ b/src/styles/owner.css @@ -1,6 +1,7 @@ .owner { display: flex; gap: .5rem; + justify-content: space-between; } .owner .owner-name { @@ -13,3 +14,43 @@ width: 18px; height: 18px; } + +.owner-scan { + width: 16px; + height: 16px; + background: + linear-gradient(to right, #020202 2px, transparent 2px) 0 0, + linear-gradient(to right, #020202 2px, transparent 2px) 0 100%, + linear-gradient(to left, #020202 2px, transparent 2px) 100% 0, + linear-gradient(to left, #020202 2px, transparent 2px) 100% 100%, + linear-gradient(to bottom, #020202 2px, transparent 2px) 0 0, + linear-gradient(to bottom, #020202 2px, transparent 2px) 100% 0, + linear-gradient(to top, #020202 2px, transparent 2px) 0 100%, + linear-gradient(to top, #020202 2px, transparent 2px) 100% 100%; + background-repeat: no-repeat; + background-size: 4px 4px; + cursor: pointer; +} + +.owner-scan:hover { + background: + linear-gradient(to right, #7d73e2 2px, transparent 2px) 0 0, + linear-gradient(to right, #7d73e2 2px, transparent 2px) 0 100%, + linear-gradient(to left, #7d73e2 2px, transparent 2px) 100% 0, + linear-gradient(to left, #7d73e2 2px, transparent 2px) 100% 100%, + linear-gradient(to bottom, #7d73e2 2px, transparent 2px) 0 0, + linear-gradient(to bottom, #7d73e2 2px, transparent 2px) 100% 0, + linear-gradient(to top, #7d73e2 2px, transparent 2px) 0 100%, + linear-gradient(to top, #7d73e2 2px, transparent 2px) 100% 100%; + background-repeat: no-repeat; + background-size: 4px 4px; +} + +.owner-scan img { + position: relative; + top: 1px; + left: 1px; + margin: 0 auto; + width: 14px; + height: 14px; +} From df17d257b5e76a348c40b81aa637441a92a05bd2 Mon Sep 17 00:00:00 2001 From: Juan M Date: Thu, 25 May 2023 18:55:55 +0200 Subject: [PATCH 2/5] Address profile with poaps and in common --- src/components/AddressOwner.js | 24 ++++++--- src/components/AddressProfile.js | 72 +++++++++++++++++++++++--- src/components/ButtonAddressProfile.js | 7 ++- src/components/EventsOwners.js | 9 +++- src/components/InCommon.js | 20 ++++--- src/models/address.js | 8 ++- src/models/in-common.js | 11 ++++ src/models/poap.js | 8 +-- src/pages/Events.js | 10 +++- src/stores/ethereum.js | 4 ++ src/styles/address-profile.css | 10 ++++ 11 files changed, 152 insertions(+), 31 deletions(-) diff --git a/src/components/AddressOwner.js b/src/components/AddressOwner.js index 1eeab2a..8ab3a7b 100644 --- a/src/components/AddressOwner.js +++ b/src/components/AddressOwner.js @@ -4,22 +4,32 @@ import TokenImage from './TokenImage' import ButtonAddressProfile from './ButtonAddressProfile' import '../styles/owner.css' -function AddressOwner({ owner, ownerEvents, eventIds, linkToScan = false }) { +function AddressOwner({ + address, + events, + eventIds, + inCommonEventIds = [], + linkToScan = false, +}) { return (
- +
{linkToScan && ( - - {`Scan + + {`Scan )} - {ownerEvents && typeof ownerEvents === 'object' && eventIds && Array.isArray(eventIds) && ( + {events && typeof events === 'object' && eventIds && Array.isArray(eventIds) && (
{eventIds.map( - (eventId) => eventId in ownerEvents - ? + (eventId) => eventId in events + ? :
{' '}
)}
diff --git a/src/components/AddressProfile.js b/src/components/AddressProfile.js index 1622c2f..eea7e5d 100644 --- a/src/components/AddressProfile.js +++ b/src/components/AddressProfile.js @@ -2,8 +2,12 @@ import { useContext, useEffect, useState } from 'react' import { LazyImage } from 'react-lazy-images' import { OpenNewWindow } from 'iconoir-react' import { POAP_SCAN_URL } from '../models/poap' +import { INCOMMON_EVENTS_LIMIT } from '../models/in-common' +import { PROFILE_EVENTS_LIMIT } from '../models/address' import { ResolverEnsContext, ReverseEnsContext } from '../stores/ethereum' import { scanAddress } from '../loaders/poap' +import TokenImage from './TokenImage' +import ButtonLink from './ButtonLink' import ErrorMessage from './ErrorMessage' import Loading from './Loading' import '../styles/address-profile.css' @@ -11,14 +15,34 @@ import '../styles/address-profile.css' function AddressProfile({ address, events = {}, - showInCommon = true, + inCommonEventIds = [], }) { const { avatars, resolveMeta } = useContext(ResolverEnsContext) const { ensNames } = useContext(ReverseEnsContext) + const [showAllPOAPs, setShowAllPOAPs] = useState(false) + const [showAllInCommon, setShowAllInCommon] = useState(false) const [loading, setLoading] = useState(0) const [error, setError] = useState(null) const [poaps, setPOAPs] = useState(null) + const inCommonTotal = inCommonEventIds.length + const inCommonHasMore = inCommonTotal > INCOMMON_EVENTS_LIMIT + + let inCommonEventIdsVisible = inCommonEventIds.slice() + + if (inCommonHasMore && !showAllInCommon) { + inCommonEventIdsVisible = inCommonEventIds.slice(0, INCOMMON_EVENTS_LIMIT) + } + + const poapsTotal = poaps === null ? 0 : poaps.length + const poapsHasMore = poapsTotal > PROFILE_EVENTS_LIMIT + + let poapsVisible = poaps === null ? [] : poaps.slice() + + if (poapsHasMore && !showAllPOAPs) { + poapsVisible = poaps.slice(0, PROFILE_EVENTS_LIMIT) + } + useEffect( () => { if ( @@ -26,13 +50,13 @@ function AddressProfile({ avatars[ensNames[address]] === undefined && !error ) { - setLoading((prevLoading) => prevLoading++) + setLoading((prevLoading) => prevLoading + 1) resolveMeta(ensNames[address]).then( (meta) => { - setLoading((prevLoading) => prevLoading--) + setLoading((prevLoading) => prevLoading - 1) }, (err) => { - setLoading((prevLoading) => prevLoading--) + setLoading((prevLoading) => prevLoading - 1) setError(err) } ) @@ -49,14 +73,14 @@ function AddressProfile({ !error ) { controller = new AbortController() - setLoading((prevLoading) => prevLoading++) + setLoading((prevLoading) => prevLoading + 1) scanAddress(address, controller.signal).then( (foundPOAPs) => { - setLoading((prevLoading) => prevLoading--) + setLoading((prevLoading) => prevLoading - 1) setPOAPs(foundPOAPs) }, (err) => { - setLoading((prevLoading) => prevLoading--) + setLoading((prevLoading) => prevLoading - 1) setError(err) setPOAPs([]) } @@ -113,6 +137,40 @@ function AddressProfile({ {address in ensNames && ( {ensNames[address]} )} + {poaps !== null && Array.isArray(poaps) && poaps.length > 0 && ( +
+

{poapsTotal} collected drops

+ {poapsVisible.map((token) => ( + token.id && token.event && ( + + ) + ))} + {poapsHasMore && ( +
+ setShowAllPOAPs((prevShowAll) => !prevShowAll)}> + {showAllPOAPs ? `show ${PROFILE_EVENTS_LIMIT}` : `show all ${poapsTotal}`} + +
+ )} +
+ )} + {Array.isArray(inCommonEventIds) && inCommonEventIds.length > 0 && ( +
+

{inCommonTotal} in common drops

+ {inCommonEventIdsVisible.map((eventId) => ( + eventId in events && ( + + ) + ))} + {inCommonHasMore && ( +
+ setShowAllInCommon((prevShowAll) => !prevShowAll)}> + {showAllInCommon ? `show ${INCOMMON_EVENTS_LIMIT}` : `show all ${inCommonTotal}`} + +
+ )} +
+ )} )}
diff --git a/src/components/ButtonAddressProfile.js b/src/components/ButtonAddressProfile.js index 3112d64..c38cfa9 100644 --- a/src/components/ButtonAddressProfile.js +++ b/src/components/ButtonAddressProfile.js @@ -10,6 +10,7 @@ import '../styles/button-address-profile.css' function ButtonAddressProfile({ address, events = {}, + inCommonEventIds = [], }) { const { ensNames } = useContext(ReverseEnsContext) const [showModal, setShowModal] = useState(false) @@ -36,7 +37,11 @@ function ButtonAddressProfile({
setShowModal(false)} /> - +
diff --git a/src/components/EventsOwners.js b/src/components/EventsOwners.js index e300ca7..0a7fdf4 100644 --- a/src/components/EventsOwners.js +++ b/src/components/EventsOwners.js @@ -1,5 +1,6 @@ import { useState } from 'react' import { chunks } from '../utils/array' +import { getAddressInCommonEventIds } from '../models/in-common' import ButtonLink from './ButtonLink' import Card from './Card' import AddressOwner from './AddressOwner' @@ -27,6 +28,7 @@ function inverseOwnersSortedEntries(owners) { function EventsOwners({ children, owners = {}, + inCommon = {}, events = {}, all = false, }) { @@ -52,6 +54,7 @@ function EventsOwners({ } const ownersEntriesChunks = chunks(ownersEntries, 10) + const inCommonEntries = Object.entries(inCommon) return (
@@ -67,9 +70,11 @@ function EventsOwners({ ([address, eventIds]) => (
  • ({ ...ownerEvents, [eventId]: events[eventId] }), {})} + address={address} + events={events} eventIds={Object.keys(owners)} + inCommonEventIds={getAddressInCommonEventIds(inCommonEntries, address)} + linkToScan={false} />
  • ) diff --git a/src/components/InCommon.js b/src/components/InCommon.js index 0f1527b..f223852 100644 --- a/src/components/InCommon.js +++ b/src/components/InCommon.js @@ -2,7 +2,7 @@ import toColor from '@mapbox/to-color' import { createRef, useContext, useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { SettingsContext } from '../stores/cache' -import { filterAndSortInCommon, INCOMMON_EVENTS_LIMIT } from '../models/in-common' +import { filterAndSortInCommon, getAddressInCommonEventIds, INCOMMON_EVENTS_LIMIT } from '../models/in-common' import { intersection } from '../utils/array' import ButtonLink from './ButtonLink' import Card from './Card' @@ -32,11 +32,15 @@ function InCommon({ const [ownerHighlighted, setOwnerHighlighted] = useState(null) const [liRefs, setLiRefs] = useState({}) - let inCommonEntries = filterAndSortInCommon(Object.entries(inCommon)) + const inCommonEntries = filterAndSortInCommon( + Object.entries(inCommon) + ) + + let inCommonEventsAddresses = inCommonEntries.slice() let inCommonLimit = INCOMMON_EVENTS_LIMIT if (showCount > 0) { - inCommonLimit = inCommonEntries.reduce( + inCommonLimit = inCommonEventsAddresses.reduce( (limit, [_, addresses]) => { if (addresses.length === showCount) { return limit + 1 @@ -47,11 +51,11 @@ function InCommon({ ) } - const inCommonTotal = inCommonEntries.length + const inCommonTotal = inCommonEventsAddresses.length const hasMore = inCommonTotal > inCommonLimit if (hasMore && !showAll) { - inCommonEntries = inCommonEntries.slice(0, inCommonLimit) + inCommonEventsAddresses = inCommonEventsAddresses.slice(0, inCommonLimit) } const removeActiveEventId = (eventId) => { @@ -140,7 +144,7 @@ function InCommon({

    {showCount > 0 && `${inCommonLimit} of `}{inCommonTotal} drop{inCommonTotal === 1 ? '' : 's'} in common

    )}
    - {inCommonEntries.map( + {inCommonEventsAddresses.map( ([eventId, addresses]) => (
    onOwnerLeave(activeEventId, owner)} > diff --git a/src/models/address.js b/src/models/address.js index 048158b..94ae38b 100644 --- a/src/models/address.js +++ b/src/models/address.js @@ -17,4 +17,10 @@ function parseAddress(address) { } } -export { parseAddresses, parseAddress } +const PROFILE_EVENTS_LIMIT = 20 + +export { + parseAddresses, + parseAddress, + PROFILE_EVENTS_LIMIT, +} diff --git a/src/models/in-common.js b/src/models/in-common.js index 10081fe..5e2ae76 100644 --- a/src/models/in-common.js +++ b/src/models/in-common.js @@ -28,10 +28,21 @@ function mergeEventsInCommon(eventData, all = false) { return allInCommon } +function getAddressInCommonEventIds(inCommonEntries, address) { + const eventIds = [] + for (const [eventId, addresses] of inCommonEntries) { + if (addresses.indexOf(address) !== -1) { + eventIds.push(eventId) + } + } + return eventIds +} + const INCOMMON_EVENTS_LIMIT = 20 export { filterAndSortInCommon, mergeEventsInCommon, + getAddressInCommonEventIds, INCOMMON_EVENTS_LIMIT, } diff --git a/src/models/poap.js b/src/models/poap.js index b59daff..3bc8d55 100644 --- a/src/models/poap.js +++ b/src/models/poap.js @@ -5,13 +5,13 @@ const POAP_FETCH_RETRIES = 5 const POAP_API_URL = process.env.REACT_APP_POAP_API_URL ?? 'https://api.poap.tech' const POAP_API_KEY = process.env.REACT_APP_POAP_API_KEY -function POAP(poap) { +function POAP(token) { return { - id: poap.id, + id: token.tokenId, owner: { - id: poap.owner.id, + id: typeof token.owner === 'object' ? token.owner.id : token.owner, }, - event: !poap.event ? undefined : Event(poap.event), + event: !token.event ? undefined : Event(token.event), } } diff --git a/src/pages/Events.js b/src/pages/Events.js index 9936d39..66c567a 100644 --- a/src/pages/Events.js +++ b/src/pages/Events.js @@ -600,6 +600,11 @@ function Events() { inCommon = mergeEventsInCommon(eventData, searchParams.get('all') === 'true') } + const allEvents = Object.values(eventData).reduce( + (allEvents, data) => ({ ...allEvents, ...data.events }), + {} + ) + const refreshCache = () => { setSearchParams({ force: true }) setOwners({}) @@ -754,12 +759,13 @@ function Events() { <> ({ ...allEvents, ...data.events }), {})} + events={allEvents} createButtons={(eventIds) => ([
    +
    + +
    + +
    +
    navigate('/')}>back From c932bcd65e54226f3fd97700ac09f83e35143871 Mon Sep 17 00:00:00 2001 From: Juan M Date: Thu, 25 May 2023 19:42:05 +0200 Subject: [PATCH 5/5] Version 1.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c7aedbd..022b87d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@poap-xyz/poap-family", - "version": "1.3.5", + "version": "1.4.0", "author": { "name": "POAP", "url": "https://poap.xyz"