From 38d01d87da7c0e7706e50854d5ea774824587841 Mon Sep 17 00:00:00 2001 From: Juan M Date: Tue, 21 May 2024 02:42:00 -0300 Subject: [PATCH 1/2] Found with strict --- src/components/AddressAddForm.js | 8 ++ src/components/AddressErrorList.js | 2 +- src/components/AddressOwner.js | 12 ++- src/components/AddressProfile.js | 55 ++++++----- src/components/AddressesForm.js | 2 +- src/components/AddressesList.js | 2 +- src/components/ButtonAddressProfile.js | 10 +- src/components/ButtonEdit.js | 8 +- src/components/ButtonExpand.js | 6 +- src/components/ButtonExportAddressCsv.js | 4 +- src/components/CachedEventList.js | 17 +++- src/components/CollectionList.js | 4 +- src/components/CollectionSet.js | 4 +- src/components/EventInfo.js | 2 +- src/components/EventsOwners.js | 29 ++++-- src/components/EventsPageError.js | 4 +- src/components/ExternalLink.js | 2 +- src/components/Feedback.js | 9 +- src/components/InCommon.js | 62 +++++++----- src/components/LastEvents.js | 36 +++++-- src/components/Progress.js | 4 +- src/components/Stats.js | 4 +- src/index.js | 6 +- src/loaders/api.js | 40 ++++---- src/models/drop.js | 8 +- src/models/event.js | 20 ++-- src/pages/Addresses.js | 116 ++++++++++++++++++----- src/pages/Event.js | 67 +++++++++---- src/pages/Events.js | 72 ++++++++++---- src/pages/FeedbackList.js | 8 +- src/pages/Home.js | 1 + src/pages/Settings.js | 44 ++++++--- src/stores/admin.js | 6 ++ src/stores/analytics.js | 6 +- src/stores/cache.js | 10 +- src/stores/ethereum.js | 48 +++++++--- src/styles/app.css | 4 + src/utils/array.js | 3 + src/utils/date.js | 13 ++- src/utils/linkify.js | 2 +- src/utils/number.js | 9 ++ src/utils/url.js | 9 +- 42 files changed, 550 insertions(+), 228 deletions(-) diff --git a/src/components/AddressAddForm.js b/src/components/AddressAddForm.js index 6fc8d44..da5aeb8 100644 --- a/src/components/AddressAddForm.js +++ b/src/components/AddressAddForm.js @@ -1,3 +1,4 @@ +import PropTypes from 'prop-types' import { useState } from 'react' import { Plus } from 'iconoir-react' import { parseAddresses } from 'models/address' @@ -5,6 +6,9 @@ import Button from 'components/Button' import ErrorMessage from 'components/ErrorMessage' import 'styles/address-add.css' +/** + * @param {PropTypes.InferProps} props + */ function AddressAddForm({ onSubmit = /** @@ -87,4 +91,8 @@ function AddressAddForm({ ) } +AddressAddForm.propTypes = { + onSubmit: PropTypes.func.isRequired, +} + export default AddressAddForm diff --git a/src/components/AddressErrorList.js b/src/components/AddressErrorList.js index 9575bd0..77b0221 100644 --- a/src/components/AddressErrorList.js +++ b/src/components/AddressErrorList.js @@ -38,7 +38,7 @@ AddressErrorList.propTypes = { PropTypes.shape({ address: PropTypes.string.isRequired, error: PropTypes.instanceOf(Error).isRequired, - }) + }).isRequired ).isRequired ), onRetry: PropTypes.func.isRequired, diff --git a/src/components/AddressOwner.js b/src/components/AddressOwner.js index 4d6bb73..88eaad0 100644 --- a/src/components/AddressOwner.js +++ b/src/components/AddressOwner.js @@ -72,11 +72,13 @@ function AddressOwner({ AddressOwner.propTypes = { address: PropTypes.string.isRequired, - events: PropTypes.objectOf(PropTypes.shape(DropProps)), - eventIds: PropTypes.arrayOf(PropTypes.number), - ownerEventIds: PropTypes.arrayOf(PropTypes.number), - inCommonEventIds: PropTypes.arrayOf(PropTypes.number), - inCommonAddresses: PropTypes.arrayOf(PropTypes.string), + events: PropTypes.objectOf( + PropTypes.shape(DropProps).isRequired + ).isRequired, + eventIds: PropTypes.arrayOf(PropTypes.number.isRequired), + ownerEventIds: PropTypes.arrayOf(PropTypes.number.isRequired), + inCommonEventIds: PropTypes.arrayOf(PropTypes.number.isRequired), + inCommonAddresses: PropTypes.arrayOf(PropTypes.string.isRequired), linkToScan: PropTypes.bool, } diff --git a/src/components/AddressProfile.js b/src/components/AddressProfile.js index 116e855..fb18575 100644 --- a/src/components/AddressProfile.js +++ b/src/components/AddressProfile.js @@ -25,7 +25,7 @@ import 'styles/address-profile.css' */ function AddressProfile({ address, - events = {}, + events, inCommonEventIds = [], inCommonAddresses = [], }) { @@ -52,7 +52,7 @@ function AddressProfile({ */ const [error, setError] = useState(null) /** - * @type {ReturnType>> | null>} + * @type {ReturnType> | null>>} */ const [poaps, setPOAPs] = useState(null) /** @@ -60,28 +60,28 @@ function AddressProfile({ */ const [since, setSince] = useState(null) - const poapsTotal = poaps === null ? 0 : poaps.length + const poapsTotal = poaps == null ? 0 : poaps.length const poapsHasMore = poapsTotal > POAP_PROFILE_LIMIT - let poapsVisible = poaps === null ? [] : poaps.slice() + let poapsVisible = poaps == null ? [] : poaps.slice() if (poapsHasMore && !showAllPOAPs) { - poapsVisible = poaps.slice(0, POAP_PROFILE_LIMIT) + poapsVisible = poapsVisible.slice(0, POAP_PROFILE_LIMIT) } - const inCommonEventsTotal = inCommonEventIds.length + const inCommonEventsTotal = inCommonEventIds == null ? 0 : inCommonEventIds.length const inCommonEventsHasMore = inCommonEventsTotal > INCOMMON_EVENTS_LIMIT - let inCommonEventIdsVisible = inCommonEventIds.slice() + let inCommonEventIdsVisible = inCommonEventIds == null ? [] : inCommonEventIds.slice() if (inCommonEventsHasMore && !showAllInCommonEvents) { - inCommonEventIdsVisible = inCommonEventIds.slice(0, INCOMMON_EVENTS_LIMIT) + inCommonEventIdsVisible = inCommonEventIdsVisible.slice(0, INCOMMON_EVENTS_LIMIT) } - const inCommonAddressesTotal = inCommonAddresses.length + const inCommonAddressesTotal = inCommonAddresses == null ? 0 : inCommonAddresses.length const inCommonAddressesHasMore = inCommonAddressesTotal > INCOMMON_ADDRESSES_LIMIT - let inCommonAddressesVisible = inCommonAddresses.slice() + let inCommonAddressesVisible = inCommonAddresses == null ? [] : inCommonAddresses.slice() if (inCommonAddressesHasMore && !showAllInCommonAddresses) { - inCommonAddressesVisible = inCommonAddresses.slice(0, INCOMMON_ADDRESSES_LIMIT) + inCommonAddressesVisible = inCommonAddressesVisible.slice(0, INCOMMON_ADDRESSES_LIMIT) } useEffect( @@ -94,13 +94,13 @@ function AddressProfile({ ) && !error ) { - setLoading((prevLoading) => prevLoading + 1) + setLoading((prevLoading) => (prevLoading ?? 0) + 1) resolveMeta(ensNames[address], address).then( (meta) => { - setLoading((prevLoading) => prevLoading - 1) + setLoading((prevLoading) => (prevLoading ?? 0) - 1) }, (err) => { - setLoading((prevLoading) => prevLoading - 1) + setLoading((prevLoading) => (prevLoading ?? 0) - 1) setError(err) } ) @@ -111,23 +111,23 @@ function AddressProfile({ useEffect( () => { + /** + * @type {AbortController | undefined} + */ let controller - if ( - poaps === null && - !error - ) { + if (poaps == null && error == null) { controller = new AbortController() - setLoading((prevLoading) => prevLoading + 1) + setLoading((prevLoading) => (prevLoading ?? 0) + 1) scanAddress(address, controller.signal).then( (foundPOAPs) => { - setLoading((prevLoading) => prevLoading - 1) + setLoading((prevLoading) => (prevLoading ?? 0) - 1) setPOAPs(foundPOAPs) if (Array.isArray(foundPOAPs) && foundPOAPs.length > 0) { setSince(findInitialPOAPDate(foundPOAPs)) } }, (err) => { - setLoading((prevLoading) => prevLoading - 1) + setLoading((prevLoading) => (prevLoading ?? 0) - 1) setError(err) setPOAPs([]) } @@ -144,7 +144,10 @@ function AddressProfile({ const hasAvatarImage = ( address in ensNames && + ensNames[address] != null && + ensNames[address] in avatars && avatars[ensNames[address]] != null && + typeof avatars[ensNames[address]] === 'string' && avatars[ensNames[address]].startsWith('http') && !avatars[ensNames[address]].endsWith('json') ) @@ -190,7 +193,7 @@ function AddressProfile({ {address in ensNames && ( {ensNames[address]} )} - {poaps !== null && Array.isArray(poaps) && poaps.length > 0 && ( + {poaps != null && Array.isArray(poaps) && poaps.length > 0 && (

{poapsTotal} collected drops {since && ( @@ -288,9 +291,11 @@ function AddressProfile({ AddressProfile.propTypes = { address: PropTypes.string.isRequired, - events: PropTypes.objectOf(PropTypes.shape(DropProps)), - inCommonEventIds: PropTypes.arrayOf(PropTypes.number), - inCommonAddresses: PropTypes.arrayOf(PropTypes.string), + events: PropTypes.objectOf( + PropTypes.shape(DropProps).isRequired + ).isRequired, + inCommonEventIds: PropTypes.arrayOf(PropTypes.number.isRequired), + inCommonAddresses: PropTypes.arrayOf(PropTypes.string.isRequired), } export default AddressProfile diff --git a/src/components/AddressesForm.js b/src/components/AddressesForm.js index 31689d5..5f43db5 100644 --- a/src/components/AddressesForm.js +++ b/src/components/AddressesForm.js @@ -102,7 +102,7 @@ function AddressesForm({ } AddressesForm.propTypes = { - addresses: PropTypes.arrayOf(PropTypes.string).isRequired, + addresses: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, onSubmit: PropTypes.func.isRequired, onClose: PropTypes.func, } diff --git a/src/components/AddressesList.js b/src/components/AddressesList.js index 878f03b..073fdd4 100644 --- a/src/components/AddressesList.js +++ b/src/components/AddressesList.js @@ -34,7 +34,7 @@ function AddressesList({ } AddressesList.propTypes = { - addresses: PropTypes.arrayOf(PropTypes.string).isRequired, + addresses: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, } export default AddressesList diff --git a/src/components/ButtonAddressProfile.js b/src/components/ButtonAddressProfile.js index d63eb76..c9197d1 100644 --- a/src/components/ButtonAddressProfile.js +++ b/src/components/ButtonAddressProfile.js @@ -14,7 +14,7 @@ import 'styles/button-address-profile.css' */ function ButtonAddressProfile({ address, - events = {}, + events, inCommonEventIds = [], inCommonAddresses = [], }) { @@ -62,9 +62,11 @@ function ButtonAddressProfile({ ButtonAddressProfile.propTypes = { address: PropTypes.string.isRequired, - events: PropTypes.objectOf(PropTypes.shape(DropProps)), - inCommonEventIds: PropTypes.arrayOf(PropTypes.number), - inCommonAddresses: PropTypes.arrayOf(PropTypes.string), + events: PropTypes.objectOf( + PropTypes.shape(DropProps).isRequired + ).isRequired, + inCommonEventIds: PropTypes.arrayOf(PropTypes.number.isRequired), + inCommonAddresses: PropTypes.arrayOf(PropTypes.string.isRequired), } export default ButtonAddressProfile diff --git a/src/components/ButtonEdit.js b/src/components/ButtonEdit.js index 50f6cbb..b7bb730 100644 --- a/src/components/ButtonEdit.js +++ b/src/components/ButtonEdit.js @@ -7,11 +7,13 @@ import Button from 'components/Button' */ function ButtonEdit({ onEdit = () => {}, - ...props + title, }) { return ( {inCommonTotal > 0 && ( - {createButtons(activeEventIds)} + {createButtons != null && + createButtons(activeEventIds)} )} @@ -312,7 +322,8 @@ function InCommon({
- {createActiveTopButtons(activeEventId)} + {createActiveTopButtons != null && + createActiveTopButtons(activeEventId)} removeActiveEventId(activeEventId)} /> @@ -371,7 +382,8 @@ function InCommon({
@@ -384,8 +396,14 @@ function InCommon({ InCommon.propTypes = { children: PropTypes.node, - inCommon: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)).isRequired, - events: PropTypes.objectOf(PropTypes.shape(DropProps)).isRequired, + inCommon: PropTypes.objectOf( + PropTypes.arrayOf( + PropTypes.string.isRequired + ).isRequired + ).isRequired, + events: PropTypes.objectOf( + PropTypes.shape(DropProps).isRequired + ).isRequired, showCount: PropTypes.number, showActive: PropTypes.bool, createButtons: PropTypes.func, diff --git a/src/components/LastEvents.js b/src/components/LastEvents.js index 2a6c327..9ee333f 100644 --- a/src/components/LastEvents.js +++ b/src/components/LastEvents.js @@ -38,11 +38,11 @@ function LastEvents({ /** * @type {ReturnType>} */ - const [page, setPage] = useState(initialPage) + const [page, setPage] = useState(initialPage ?? DEFAULT_PAGE) /** * @type {ReturnType>} */ - const [perPage, setPerPage] = useState(initialPerPage) + const [perPage, setPerPage] = useState(initialPerPage ?? DEFAULT_PER_PAGE) /** * @type {ReturnType>} */ @@ -66,14 +66,18 @@ function LastEvents({ useEffect( () => { - setPage(initialPage) + if (initialPage != null) { + setPage(initialPage) + } }, [initialPage] ) useEffect( () => { - setPerPage(initialPerPage) + if (initialPerPage != null) { + setPerPage(initialPerPage) + } }, [initialPerPage] ) @@ -145,7 +149,9 @@ function LastEvents({ { setPerPage(10) - onPageChange(1, 10) + if (onPageChange != null) { + onPageChange(1, 10) + } }} disabled={perPage === 10} > @@ -155,7 +161,9 @@ function LastEvents({ { setPerPage(10) - onPageChange(1, 100) + if (onPageChange != null) { + onPageChange(1, 100) + } }} disabled={perPage === 100} > @@ -175,14 +183,22 @@ function LastEvents({ {pages > 1 && !loading && ( 0 && maxPages < pages ? maxPages : pages} + pages={ + maxPages != null && + maxPages > 0 && + maxPages < pages + ? maxPages + : pages + } total={total} onPage={(newPage) => { setPage(newPage) - onPageChange(newPage, perPage) + if (onPageChange != null) { + onPageChange(newPage, perPage) + } }} > - {maxPages > 0 && showMore && ( + {maxPages != null && maxPages > 0 && showMore && ( )} - {!hasDetails && formatPercentage(value)} + {!hasDetails && formatPercentage(value ?? 0)} }

{showDetails && ( diff --git a/src/components/Stats.js b/src/components/Stats.js index 6cebf0c..e67f72c 100644 --- a/src/components/Stats.js +++ b/src/components/Stats.js @@ -24,7 +24,7 @@ function Stats({ stats, highlight }) { >
{stat.text} @@ -75,7 +75,7 @@ Stats.propTypes = { href: PropTypes.string, external: PropTypes.bool, small: PropTypes.bool, - }) + }).isRequired ).isRequired ), highlight: PropTypes.string, diff --git a/src/index.js b/src/index.js index f45f934..5529d6a 100644 --- a/src/index.js +++ b/src/index.js @@ -5,5 +5,7 @@ import App from 'app/App' const rootElement = document.getElementById('root') -ReactModal.setAppElement(rootElement) -ReactDOM.createRoot(rootElement).render() +if (rootElement != null) { + ReactModal.setAppElement(rootElement) + ReactDOM.createRoot(rootElement).render() +} diff --git a/src/loaders/api.js b/src/loaders/api.js index 0e35d59..2b93543 100644 --- a/src/loaders/api.js +++ b/src/loaders/api.js @@ -249,21 +249,29 @@ export async function getInCommonEventsWithProgress( } ) } catch (err) { - if (err.response) { - if (err.response.status === 404) { - return null - } - - console.error(err) - - throw new Error( - `Drop ${eventId} in common failed to fetch ` + - `(status ${err.response.status})` - ) + const status = + err != null && + typeof err === 'object' && + 'response' in err && + err.response != null && + typeof err.response === 'object' && + 'status' in err.response && + err.response.status != null && + typeof err.response.status === 'number' + ? err.response.status + : 500 + + if (status === 404) { + return null } console.error(err) + throw new Error( + `Drop ${eventId} in common failed to fetch (status ${status})` + ) + } + if (response == null) { throw new Error(`Drop ${eventId} in common failed to fetch`) } @@ -438,7 +446,7 @@ export async function getEvents(eventIds, abortSignal) { */ const body = await response.json() - if (typeof body !== 'object') { + if (body == null || typeof body !== 'object') { throw new Error(`Malformed drops (type ${typeof body})`) } @@ -489,7 +497,7 @@ export async function getEventOwners(eventId, abortSignal, refresh = false) { */ const body = await response.json() - if (typeof body !== 'object') { + if (body == null || typeof body !== 'object') { throw new Error(`Malformed drop owners (type ${typeof body})`) } @@ -563,7 +571,7 @@ export async function getEventsOwners(eventIds, abortSignal, expiryDates) { */ const body = await response.json() - if (typeof body !== 'object') { + if (body == null || typeof body !== 'object') { throw new Error(`Malformed drops owners (type ${typeof body})`) } @@ -615,7 +623,7 @@ export async function getEventMetrics(eventId, abortSignal, refresh = false) { */ const body = await response.json() - if (typeof body !== 'object') { + if (body == null || typeof body !== 'object') { throw new Error(`Malformed drops metrics (type ${typeof body})`) } @@ -689,7 +697,7 @@ export async function getEventsMetrics(eventIds, abortSignal, expiryDates) { */ const body = await response.json() - if (typeof body !== 'object') { + if (body == null || typeof body !== 'object') { throw new Error(`Malformed drops metrics (type ${typeof body})`) } diff --git a/src/models/drop.js b/src/models/drop.js index 974f67a..a57fde3 100644 --- a/src/models/drop.js +++ b/src/models/drop.js @@ -125,10 +125,8 @@ export const DropProps = { * }} */ export function DropOwners(eventOwners) { - if (eventOwners == null) { - return null - } if ( + eventOwners == null || typeof eventOwners !== 'object' || !('owners' in eventOwners) || !Array.isArray(eventOwners.owners) || @@ -157,7 +155,7 @@ export function DropOwners(eventOwners) { * momentsUploaded: number * collectionsIncludes: number * ts: number -* }} +* } | null} */ export function DropMetrics(eventMetrics) { if (eventMetrics == null) { @@ -267,7 +265,7 @@ export function DropData( * @returns {Record>} */ export function Drops(drops, includeDescription = false) { - if (typeof drops !== 'object') { + if (drops == null || typeof drops !== 'object') { throw new Error('Invalid drops') } diff --git a/src/models/event.js b/src/models/event.js index 2f19376..4ac6f54 100644 --- a/src/models/event.js +++ b/src/models/event.js @@ -27,18 +27,20 @@ export function joinEventIds(eventIds) { /** * @param {Record} events - * @returns {Record} + * @returns {Record} */ export function parseExpiryDates(events) { return Object.fromEntries( - Object.entries(events).map( - ([eventId, event]) => ([ - eventId, - event?.expiry_date - ? parseEndOfDayDate(event.expiry_date) - : undefined, - ]) - ) + Object.entries(events) + .map( + ([eventId, event]) => ([ + eventId, + event?.expiry_date + ? parseEndOfDayDate(event.expiry_date) + : undefined, + ]) + ) + .filter(([, endOfDayDate]) => endOfDayDate != null) ) } diff --git a/src/pages/Addresses.js b/src/pages/Addresses.js index f68a11e..8d8a9c3 100644 --- a/src/pages/Addresses.js +++ b/src/pages/Addresses.js @@ -107,11 +107,22 @@ function Addresses() { setLoadingByAddress((prevLoading) => ({ ...prevLoading, [address]: true })) try { const tokens = await scanAddress(address, controller.signal) - setLoadedCount((prevLoadedCount) => prevLoadedCount + 1) + setLoadedCount((prevLoadedCount) => (prevLoadedCount ?? 0) + 1) setPowers((oldPowers) => ({ ...oldPowers, [address]: tokens.length })) for (const token of tokens) { - const eventId = token.event.id + const event = token.event + if (event == null) { + setErrorsByAddress((oldErrors) => ({ + ...oldErrors, + [address]: new Error(`Could not find POAP ${token.id}`), + })) + continue + } + const eventId = event.id setInCommon((prevInCommon) => { + if (!prevInCommon) { + return { [eventId]: [address] } + } if (eventId in prevInCommon) { if (prevInCommon[eventId].indexOf(address) === -1) { prevInCommon[eventId].push(address) @@ -121,9 +132,15 @@ function Addresses() { } return prevInCommon }) - setEvents((prevEvents) => ({ ...prevEvents, [eventId]: token.event })) + setEvents((prevEvents) => ({ + ...prevEvents, + [eventId]: event, + })) } setLoadingByAddress((oldLoading) => { + if (oldLoading == null) { + return {} + } /** * @type {Record} */ @@ -137,6 +154,9 @@ function Addresses() { }) } catch (err) { setLoadingByAddress((oldLoading) => { + if (oldLoading == null) { + return {} + } /** * @type {Record} */ @@ -148,8 +168,18 @@ function Addresses() { } return newLoading }) - if (!(err instanceof AbortedError) && !err.aborted) { - setErrorsByAddress((oldErrors) => ({ ...oldErrors, [address]: err })) + if (!(err instanceof AbortedError)) { + if (err instanceof Error) { + setErrorsByAddress((oldErrors) => ({ + ...oldErrors, + [address]: err, + })) + } else { + setErrorsByAddress((oldErrors) => ({ + ...oldErrors, + [address]: new Error('Load collector failed', { cause: err }), + })) + } } throw err } @@ -166,7 +196,7 @@ function Addresses() { setEvents({}) setPowers({}) setErrorsByAddress({}) - let promise = new Promise((r) => r()) + let promise = new Promise((r) => r(undefined)) /** * @type {Record} */ @@ -207,6 +237,9 @@ function Addresses() { (ensNameAddress) => { if (ensNameAddress) { setLoadingByIndex((oldLoading) => { + if (oldLoading == null) { + return {} + } /** * @type {Record} */ @@ -223,6 +256,9 @@ function Addresses() { return Promise.resolve(ensNameAddress) } else { setLoadingByIndex((oldLoading) => { + if (oldLoading == null) { + return {} + } /** * @type {Record} */ @@ -234,13 +270,16 @@ function Addresses() { } return newLoading }) - const err = new Error(`Could not resolve ${ensName}`) - setErrorsByIndex((oldErrors) => ({ ...oldErrors, [index]: err })) - return Promise.reject(err) + const error = new Error(`Could not resolve ${ensName}`) + setErrorsByIndex((oldErrors) => ({ ...oldErrors, [index]: error })) + return Promise.reject(error) } }, (err) => { setLoadingByIndex((oldLoading) => { + if (oldLoading == null) { + return {} + } /** * @type {Record} */ @@ -276,7 +315,7 @@ function Addresses() { .map((input) => input.address) if (missingEnsNames.length > 0) { resolveEnsNames(missingEnsNames).catch((err) => { - setErrors((oldErrors) => ([...oldErrors, err])) + setErrors((oldErrors) => ([...(oldErrors ?? []), err])) }) } } @@ -300,10 +339,21 @@ function Addresses() { ) if (missingAddresses.length > 0) { setState(STATE_ENS_RESOLVING) - let promise = new Promise((r) => r()) + let promise = new Promise((r) => r(undefined)) for (const { ens, index } of missingAddresses) { if (ens in addressByEnsName) { - setCollectors((oldCollectors) => ({ ...oldCollectors, [index]: addressByEnsName[ens] })) + const address = addressByEnsName[ens] + if (address != null) { + setCollectors((oldCollectors) => ({ + ...oldCollectors, + [index]: address, + })) + } else { + setErrorsByIndex((oldErrors) => ({ + ...oldErrors, + [index]: new Error(`Address for ${ens} not found`), + })) + } } else { promise = promise.then( () => processEnsName(ens, index), @@ -312,7 +362,7 @@ function Addresses() { } } promise.catch((err) => { - if (!(err instanceof AbortedError) && !err.aborted) { + if (!(err instanceof AbortedError)) { console.error(err) } }) @@ -327,6 +377,9 @@ function Addresses() { if (addresses === null) { return } + /** + * @type {AbortController[]} + */ let controllers = [] const resolved = Object.values(collectors) if (resolved.length === addresses.length) { @@ -358,16 +411,22 @@ function Addresses() { const searchEvents = useMemo( () => parseEventIds( - searchParams.get('events') + searchParams.get('events') ?? '' ), [searchParams] ) useEffect( () => { - const controller= new AbortController() - const requests = [] - const onHashChange = (ev, initial = false) => { + const controller = new AbortController() + /** + * @type {AbortController[]} + */ + const controllers = [] + /** + * @param {HashChangeEvent} event + */ + const onHashChange = (event, initial = false) => { setState(STATE_INIT_PARSING) setErrors([]) setErrorsByAddress({}) @@ -396,7 +455,7 @@ function Addresses() { } else if (searchEvents.length > 0) { setLoadingEventsOwners(true) if (searchParams.get('force') === 'true') { - let promise = new Promise((resolve) => resolve()) + let promise = new Promise((r) => r(undefined)) for (const searchEventId of searchEvents) { const controller = new AbortController() promise = promise.then(() => { @@ -421,11 +480,11 @@ function Addresses() { }, (err) => { setLoadingEventsOwners(false) - setErrors((oldErrors) => ([...oldErrors, err])) + setErrors((oldErrors) => ([...(oldErrors ?? []), err])) } ) }) - requests.push(controller) + controllers.push(controller) } } else { getEventsOwners(searchEvents, controller.signal).then( @@ -453,12 +512,12 @@ function Addresses() { ) ) } else { - setErrors((oldErrors) => ([...oldErrors, new Error(`Events owners could not be loaded`)])) + setErrors((oldErrors) => ([...(oldErrors ?? []), new Error(`Events owners could not be loaded`)])) } }, (err) => { setLoadingEventsOwners(false) - setErrors((oldErrors) => ([...oldErrors, err])) + setErrors((oldErrors) => ([...(oldErrors ?? []), err])) } ) } @@ -471,7 +530,7 @@ function Addresses() { window.addEventListener('hashchange', onHashChange, false) return () => { controller.abort() - for (const request of requests) { + for (const request of controllers) { request.abort() } window.removeEventListener('hashchange', onHashChange, false) @@ -571,6 +630,9 @@ function Addresses() { */ const retryResolveAddress = (index) => { setErrorsByIndex((prevErrors) => { + if (prevErrors == null) { + return {} + } /** * @type {Record} */ @@ -583,7 +645,7 @@ function Addresses() { return newErrors }) const entry = addresses[index] - if (entry && entry.ens) { + if (entry != null && entry.ens) { processEnsName(entry.ens, index).catch((err) => { if (!(err instanceof AbortedError) && !err.aborted) { console.error(err) @@ -666,7 +728,11 @@ function Addresses() { ))} - edit()} /> + edit()} + title="Manually enter the list of addresses" + /> diff --git a/src/pages/Event.js b/src/pages/Event.js index 2a04507..2557337 100644 --- a/src/pages/Event.js +++ b/src/pages/Event.js @@ -106,23 +106,42 @@ function Event() { (address, abortSignal) => scanAddress(address, abortSignal).then( (ownerTokens) => { for (const ownerToken of ownerTokens) { - const eventId = ownerToken.event.id - setInCommon((prevInCommon) => { - if (eventId in prevInCommon) { - if (prevInCommon[eventId].indexOf(address) === -1) { - prevInCommon[eventId].push(address) + const event = ownerToken.event + if (event != null) { + const eventId = event.id + setInCommon((prevInCommon) => { + if (prevInCommon == null) { + return { + [eventId]: [address], + } } - } else { - prevInCommon[eventId] = [address] - } - return prevInCommon - }) - setEvents((prevEvents) => ({ ...prevEvents, [eventId]: ownerToken.event })) + if (eventId in prevInCommon) { + if (prevInCommon[eventId].indexOf(address) === -1) { + prevInCommon[eventId].push(address) + } + } else { + prevInCommon[eventId] = [address] + } + return prevInCommon + }) + setEvents((prevEvents) => ({ ...prevEvents, [eventId]: event })) + } else { + setErrors((prevErrors) => ([ + ...(prevErrors ?? []), + { + address, + error: new Error(`Could not find POAP ${ownerToken.id}`), + }, + ])) + } } - setLoadedCount((prevLoadedCount) => prevLoadedCount + 1) + setLoadedCount((prevLoadedCount) => (prevLoadedCount ?? 0) + 1) }, (err) => { - setErrors((prevErrors) => ([...prevErrors, { address, error: err }])) + setErrors((prevErrors) => ([ + ...(prevErrors ?? []), + { address, error: err }, + ])) } ), [] @@ -143,7 +162,7 @@ function Event() { const processedOwners = [] setLoading(true) setErrors([]) - let promise = new Promise((r) => { r() }) + let promise = new Promise((r) => { r(undefined) }) for (const owner of owners) { if (processedOwners.indexOf(owner) !== -1) { continue @@ -194,9 +213,12 @@ function Event() { useEffect( () => { resolveEnsNames(owners) - let requests = [] + /** + * @type {AbortController[]} + */ + let controllers = [] if (searchParams.get('force') === 'true') { - requests = process() + controllers = process() } else { setLoading(true) setLoadedCount(0) @@ -205,13 +227,15 @@ function Event() { event.id, /*abortSignal*/undefined, /*onProgress*/({ progress, estimated, rate }) => { - setLoadedProgress({ progress, estimated, rate }) + if (progress != null && estimated != null && rate != null) { + setLoadedProgress({ progress, estimated, rate }) + } } ).then( (result) => { setLoadedProgress(null) if (!result) { - requests = process() + controllers = process() } else { setEvents(result.events) setCachedTs(result.ts) @@ -226,12 +250,12 @@ function Event() { (err) => { setLoadedProgress(null) console.error(err) - requests = process() + controllers = process() } ) } return () => { - for (const request of requests) { + for (const request of controllers) { request.abort() } } @@ -288,6 +312,9 @@ function Event() { */ const retryAddress = (address) => { setErrors((prevErrors) => { + if (prevErrors == null) { + return [] + } const newErrors = [] for (const { error, address: errorAddress } of prevErrors) { if (errorAddress !== address) { diff --git a/src/pages/Events.js b/src/pages/Events.js index 6464ebc..4dd0ffc 100644 --- a/src/pages/Events.js +++ b/src/pages/Events.js @@ -91,7 +91,7 @@ function Events() { */ const [loadingCollections, setLoadingCollections] = useState(false) /** - * @type {ReturnType; inCommon: Record; ts: number }>>>} + * @type {ReturnType; inCommon: Record; ts: number | null }>>>} */ const [eventData, setEventData] = useState({}) /** @@ -219,6 +219,9 @@ function Events() { */ const removeLoading = (eventId) => { setLoading((alsoLoading) => { + if (alsoLoading == null) { + return {} + } /** * @type {Record} */ @@ -247,6 +250,9 @@ function Events() { */ const disableProgress = (eventId) => { setProgress((alsoProgress) => { + if (alsoProgress == null) { + return {} + } /** * @type {Record} */ @@ -294,6 +300,9 @@ function Events() { */ const removeError = (eventId) => { setErrors((alsoErrors) => { + if (alsoErrors == null) { + return {} + } /** * @type {Record} */ @@ -314,9 +323,9 @@ function Events() { */ const updateEventOwnerError = (eventId, address, err) => { setEventOwnerErrors((oldEventOwnerErrors) => ({ - ...oldEventOwnerErrors, + ...(oldEventOwnerErrors ?? {}), [eventId]: { - ...(oldEventOwnerErrors[eventId] ?? {}), + ...((oldEventOwnerErrors ?? {})[eventId] ?? {}), [address]: err, }, })) @@ -328,6 +337,9 @@ function Events() { */ const removeEventOwnerError = (eventId, address) => { setEventOwnerErrors((oldEventOwnerErrors) => { + if (oldEventOwnerErrors == null) { + return {} + } if (eventId in oldEventOwnerErrors && address in oldEventOwnerErrors[eventId]) { if (Object.keys(oldEventOwnerErrors[eventId]).length === 1) { delete oldEventOwnerErrors[eventId] @@ -344,6 +356,9 @@ function Events() { */ const removeEventOwnerErrors = (eventId) => { setEventOwnerErrors((alsoErrors) => { + if (alsoErrors == null) { + return {} + } /** * @type {Record>} */ @@ -364,6 +379,19 @@ function Events() { */ const updateEventOwnerData = (eventId, address, event) => { setEventData((prevEventData) => { + if (prevEventData == null) { + return { + [eventId]: { + events: { + [event.id]: event, + }, + inCommon: { + [event.id]: [address], + }, + ts: null, + }, + } + } if (eventId in prevEventData) { if (event.id in prevEventData[eventId].inCommon) { if (!prevEventData[eventId].inCommon[event.id].includes(address)) { @@ -451,10 +479,10 @@ function Events() { ts = Math.trunc(Date.now() / 1000) } setEventData((prevEventData) => ({ - ...prevEventData, + ...(prevEventData ?? {}), [eventId]: { - events: prevEventData[eventId].events, - inCommon: prevEventData[eventId].inCommon, + events: (prevEventData ?? {})[eventId].events, + inCommon: (prevEventData ?? {})[eventId].inCommon, ts, }, })) @@ -466,7 +494,7 @@ function Events() { const incrLoadedCount = (eventId) => { setLoadedCount((prevLoadedCount) => ({ ...prevLoadedCount, - [eventId]: (prevLoadedCount[eventId] ?? 0) + 1, + [eventId]: ((prevLoadedCount ?? {})[eventId] ?? 0) + 1, })) } @@ -485,7 +513,10 @@ function Events() { * @param {number} eventId */ const addStaleEvent = (eventId) => { - setStaleEvents((oldStaleEvents) => uniq([...oldStaleEvents, eventId])) + setStaleEvents((oldStaleEvents) => uniq([ + ...(oldStaleEvents ?? []), + eventId, + ])) } const loadCahedOwnersAndMetrics = useCallback( @@ -511,7 +542,9 @@ function Events() { removeLoading(eventId) if (eventAndOwners != null) { updateEventOwners(eventId, eventAndOwners.owners) - updateEventMetrics(eventId, eventAndOwners.metrics) + if (eventAndOwners.metrics) { + updateEventMetrics(eventId, eventAndOwners.metrics) + } } else { const error = new Error('Could not fetch drop and collectors') updateError(eventId, error) @@ -577,7 +610,9 @@ function Events() { ) } if (eventMetricsResult.status === 'fulfilled') { - updateEventMetrics(eventId, eventMetricsResult.value) + if (eventMetricsResult.value) { + updateEventMetrics(eventId, eventMetricsResult.value) + } } else { console.error(eventMetricsResult.reason) } @@ -656,7 +691,7 @@ function Events() { const processedOwners = [] setLoadingScans(eventId) enableProgress(eventId) - let promise = new Promise((r) => { r() }) + let promise = new Promise((r) => { r(undefined) }) for (const owner of owners[eventId]) { if (processedOwners.indexOf(owner) !== -1) { continue @@ -766,7 +801,7 @@ function Events() { setStatus(STATUS_LOADING_OWNERS) } } else if (status === STATUS_LOADING_OWNERS) { - let promise = new Promise((r) => { r() }) + let promise = new Promise((r) => { r(undefined) }) for (const eventId of eventIds) { const controller = new AbortController() const process = () => force @@ -804,7 +839,7 @@ function Events() { */ const controllers = [] if (status === STATUS_LOADING_SCANS) { - let promise = new Promise((r) => { r() }) + let promise = new Promise((r) => { r(undefined) }) for (const eventId of eventIds) { const controller = new AbortController() /** @@ -823,7 +858,12 @@ function Events() { eventId, controller.signal, /*onProgress*/({ progress, estimated, rate }) => { - setLoadedProgress((alsoProgress) => ({ ...alsoProgress, [eventId]: { progress, estimated, rate } })) + if (progress != null && estimated != null && rate != null) { + setLoadedProgress((alsoProgress) => ({ + ...alsoProgress, + [eventId]: { progress, estimated, rate }, + })) + } } ).then( (result) => { @@ -1001,7 +1041,7 @@ function Events() { * @param {number} eventId */ const delEvent = (eventId) => { - const eventIds = parseEventIds(rawEventIds).filter( + const eventIds = parseEventIds(String(rawEventIds)).filter( (paramEventId) => String(paramEventId) !== String(eventId) ) if (eventIds.length === 1) { @@ -1228,7 +1268,7 @@ function Events() {

) )} - {event.id in eventData && eventData[event.id].ts && ( + {event.id in eventData && eventData[event.id].ts != null && (

Cached {!(event.id in loading) && event.id in eventData && event.id in eventData[event.id].inCommon && eventData[event.id].inCommon[event.id].length !== owners[event.id].length && ( diff --git a/src/pages/FeedbackList.js b/src/pages/FeedbackList.js index 0ba5c60..9ed76f3 100644 --- a/src/pages/FeedbackList.js +++ b/src/pages/FeedbackList.js @@ -56,7 +56,7 @@ function FeedbackList({ qty = 10 }) { useEffect( () => { - if (!authenticated) { + if (!authenticated || passphrase == null) { return } setLoading(true) @@ -100,7 +100,13 @@ function FeedbackList({ qty = 10 }) { * @param {number} id */ const handleDelFeedback = (id) => { + if (!authenticated || passphrase == null) { + return + } setFeedback((oldFeedback) => { + if (oldFeedback == null) { + return [] + } const newFeedback = [] for (let i = 0; i < oldFeedback.length; i++) { if (oldFeedback[i].id !== id) { diff --git a/src/pages/Home.js b/src/pages/Home.js index 9411451..e8d4c56 100644 --- a/src/pages/Home.js +++ b/src/pages/Home.js @@ -30,6 +30,7 @@ function Home() {

{settings && settings.showLastEvents && ( { - set('showLastEvents', !!event.target.checked) + /** + * @param {boolean} checked + */ + const handleShowLastEvents = (checked) => { + set('showLastEvents', checked) } - const handleAutoScrollCollectors = (event) => { - set('autoScrollCollectors', !!event.target.checked) + /** + * @param {boolean} checked + */ + const handleAutoScrollCollectors = (checked) => { + set('autoScrollCollectors', checked) } - const handleOpenProfiles = (event) => { - set('openProfiles', !!event.target.checked) + /** + * @param {boolean} checked + */ + const handleOpenProfiles = (checked) => { + set('openProfiles', checked) } - const handleShowCollections = (event) => { - set('showCollections', !!event.target.checked) + /** + * @param {boolean} checked + */ + const handleShowCollections = (checked) => { + set('showCollections', checked) } return ( @@ -43,7 +55,9 @@ function Settings() { { + handleShowLastEvents(event.target.checked) + }} />
@@ -53,7 +67,9 @@ function Settings() { { + handleAutoScrollCollectors(event.target.checked) + }} /> @@ -63,7 +79,9 @@ function Settings() { { + handleOpenProfiles(event.target.checked) + }} /> @@ -73,7 +91,9 @@ function Settings() { { + handleShowCollections(event.target.checked) + }} /> diff --git a/src/stores/admin.js b/src/stores/admin.js index daa4f1a..35ebb93 100644 --- a/src/stores/admin.js +++ b/src/stores/admin.js @@ -4,8 +4,14 @@ import { auth } from 'loaders/api' export const AdminContext = createContext({ authenticated: false, + /** + * @type {string | null} + */ passphrase: null, loading: false, + /** + * @type {Error | null} + */ error: null, authenticate: /** diff --git a/src/stores/analytics.js b/src/stores/analytics.js index 056a66e..eff4a18 100644 --- a/src/stores/analytics.js +++ b/src/stores/analytics.js @@ -2,9 +2,11 @@ import PropTypes from 'prop-types' import { createInstance, MatomoProvider } from '@datapunt/matomo-tracker-react' const matomoHost = process.env.REACT_APP_MATOMO_HOST -const matomoSiteId = parseInt(process.env.REACT_APP_MATOMO_SITE_ID) +const matomoSiteId = process.env.REACT_APP_MATOMO_SITE_ID + ? parseInt(process.env.REACT_APP_MATOMO_SITE_ID) + : undefined -const matomo = matomoHost +const matomo = matomoHost && matomoSiteId ? createInstance({ siteId: matomoSiteId, urlBase: `https://${matomoHost}`, diff --git a/src/stores/cache.js b/src/stores/cache.js index 3fb5107..d15be10 100644 --- a/src/stores/cache.js +++ b/src/stores/cache.js @@ -16,9 +16,9 @@ export const SettingsContext = createContext({ */ export function SettingsProvider({ children }) { /** - * @type {ReturnType>} + * @type {ReturnType>} */ - const [settings, setSettings] = useState(null) + const [settings, setSettings] = useState(DEFAULT_SETTINGS) useEffect( () => { @@ -35,6 +35,12 @@ export function SettingsProvider({ children }) { */ const set = (key, value) => { setSettings((oldSettings) => { + if (!oldSettings) { + return { ...DEFAULT_SETTINGS, [key]: value } + } + /** + * @type {typeof DEFAULT_SETTINGS} + */ const newSettings = { ...oldSettings, [key]: value } saveSettings(newSettings) return newSettings diff --git a/src/stores/ethereum.js b/src/stores/ethereum.js index f501e8a..381a2e8 100644 --- a/src/stores/ethereum.js +++ b/src/stores/ethereum.js @@ -9,7 +9,7 @@ import { export const ResolverEnsContext = createContext({ /** - * @type {Record} + * @type {Record} */ addresses: {}, /** @@ -17,7 +17,7 @@ export const ResolverEnsContext = createContext({ */ resolveAddress: async (ensName) => null, /** - * @type {Record} + * @type {Record} */ avatars: {}, /** @@ -45,13 +45,16 @@ export const ReverseEnsContext = createContext({ isNotFound: (address) => false, }) +/** + * @param {PropTypes.InferProps} props + */ function ResolverEnsProvider({ children }) { /** - * @type {ReturnType>>} + * @type {ReturnType>>} */ const [addressByEnsName, setAddressByEnsName] = useState({}) /** - * @type {ReturnType>>} + * @type {ReturnType>>} */ const [avatarByEnsName, setAvatarByEnsName] = useState({}) @@ -72,7 +75,7 @@ function ResolverEnsProvider({ children }) { })) } else { setAddressByEnsName((oldAddressByEnsName) => ({ - ...oldAddressByEnsName, + ...(oldAddressByEnsName ?? {}), [ensName]: null, })) } @@ -102,7 +105,7 @@ function ResolverEnsProvider({ children }) { })) } else { setAvatarByEnsName((oldAvatarByEnsName) => ({ - ...oldAvatarByEnsName, + ...(oldAvatarByEnsName ?? {}), [ensName]: null, })) } @@ -141,6 +144,13 @@ function ResolverEnsProvider({ children }) { ) } +ResolverEnsProvider.propTypes = { + children: PropTypes.node.isRequired, +} + +/** + * @param {PropTypes.InferProps} props + */ function ReverseEnsProvider({ children, limitEnsNames = ENS_RESOLVE_BATCH_SIZE, @@ -157,6 +167,12 @@ function ReverseEnsProvider({ */ const [notFoundAddresses, setNotFoundAddresses] = useState([]) + // Number of ENS names to resolve at a time. + const batchSize = useMemo( + () => limitEnsNames ?? ENS_RESOLVE_BATCH_SIZE, + [limitEnsNames] + ) + const resolveNames = useCallback( /** * @param {string[]} names @@ -164,11 +180,11 @@ function ReverseEnsProvider({ * @returns {Promise} */ (names, addresses) => { - let promise = new Promise((r) => r()) - for (let i = 0; i < names.length; i += limitEnsNames) { + let promise = new Promise((r) => r(undefined)) + for (let i = 0; i < names.length; i += batchSize) { promise = promise.then( () => Promise.allSettled( - names.slice(i, i + limitEnsNames).map( + names.slice(i, i + batchSize).map( (name) => resolveMeta(name, addresses ? addresses[i] : undefined) ) ) @@ -176,7 +192,7 @@ function ReverseEnsProvider({ } return promise.then(() => {}) }, - [limitEnsNames, resolveMeta] + [batchSize, resolveMeta] ) const resolveEnsNames = useCallback( @@ -210,6 +226,11 @@ function ReverseEnsProvider({ const [resolvedAddresses, resolvedEnsNames] = Object .entries(resolved) .reduce( + /** + * @param {[string[], string[]]} param0 + * @param {[string, string]} param1 + * @returns {[string[], string[]]} + */ ([resolvedAddresses, resolvedEnsNames], [address, ensName]) => [ [...resolvedAddresses, address], [...resolvedEnsNames, ensName] @@ -223,7 +244,7 @@ function ReverseEnsProvider({ setNotFoundAddresses( (oldNotFoundByAddress) => ([ ...new Set([ - ...oldNotFoundByAddress, + ...(oldNotFoundByAddress ?? []), ...newAddresses.filter((address) => !(address in ensNames)), ]) ]) @@ -281,6 +302,11 @@ function ReverseEnsProvider({ ) } +ReverseEnsProvider.propTypes = { + children: PropTypes.node.isRequired, + limitEnsNames: PropTypes.number, +} + /** * @param {PropTypes.InferProps} props */ diff --git a/src/styles/app.css b/src/styles/app.css index 91d37aa..9929273 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -55,6 +55,10 @@ input[type="search"]::-webkit-search-results-decoration { display: none; } +.icon.warning::before { + content: "⚠"; +} + ::-webkit-scrollbar { -webkit-appearance: none; width: .5rem; diff --git a/src/utils/array.js b/src/utils/array.js index 82afde0..412c01b 100644 --- a/src/utils/array.js +++ b/src/utils/array.js @@ -5,6 +5,9 @@ * @param {...T[]} array */ export function intersection(array) { + if (array == null) { + return [] + } const result = [] const argsLength = arguments.length for (var i = 0, length = array.length; i < length; i++) { diff --git a/src/utils/date.js b/src/utils/date.js index 26e3ef0..99f0741 100644 --- a/src/utils/date.js +++ b/src/utils/date.js @@ -5,6 +5,9 @@ import localizedFormat from 'dayjs/plugin/localizedFormat' dayjs.extend(relativeTime) dayjs.extend(localizedFormat) +/** + * @param {Date | number | string} date + */ export function formatDate(date) { return dayjs(date).format('ll') } @@ -31,10 +34,16 @@ export function secondsInTheFuture(secs) { return dayjs(ts).fromNow() } -export function formatMonthYear(d) { - return dayjs(d).format('MMM \'YY') +/** + * @param {Date | number | string} date + */ +export function formatMonthYear(date) { + return dayjs(date).format('MMM \'YY') } +/** + * @param {Date | number | string} date + */ export function parseEndOfDayDate(date) { return dayjs(date).endOf('day').toDate() } diff --git a/src/utils/linkify.js b/src/utils/linkify.js index 489b435..fba7f90 100644 --- a/src/utils/linkify.js +++ b/src/utils/linkify.js @@ -7,7 +7,7 @@ const regexp = * Splits a text by its URLs and wrap them in Anchor component. * * @param {string} text - * @param {React.FC} Anchor + * @param {React.FC<{ href: string; children: React.ReactNode }>} Anchor * @returns {React.ReactNode[]} */ export default function linkify(text, Anchor) { diff --git a/src/utils/number.js b/src/utils/number.js index 272b75d..ca3426a 100644 --- a/src/utils/number.js +++ b/src/utils/number.js @@ -1,13 +1,22 @@ import numbro from 'numbro' +/** + * @param {string | number} n + */ export function formatStat(n) { return numbro(n).format({ thousandSeparated: false }) } +/** + * @param {string | number} n + */ export function formatPercentage(n) { return numbro(n).format({ mantissa: 0, output: 'percent' }) } +/** + * @param {string | number} n + */ export function formatByte(n) { return numbro(n).format({ output: 'byte', base: 'binary', mantissa: 0 }) } diff --git a/src/utils/url.js b/src/utils/url.js index 592e987..fafe9f6 100644 --- a/src/utils/url.js +++ b/src/utils/url.js @@ -8,9 +8,12 @@ */ export function getSearchParamNumber(searchParams, key, defaultValue) { if (searchParams.has(key)) { - const n = parseInt(searchParams.get(key)) - if (!isNaN(n)) { - return n + const value = searchParams.get(key) + if (value != null) { + const n = parseInt(value) + if (!isNaN(n)) { + return n + } } } return defaultValue From 7438c742e78cb4b5d7a30c915a3c77df37907ae1 Mon Sep 17 00:00:00 2001 From: Juan M Date: Tue, 21 May 2024 02:43:00 -0300 Subject: [PATCH 2/2] Version 1.13.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e13f2b9..3f19b89 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@poap-xyz/poap-family", - "version": "1.13.9", + "version": "1.13.10", "author": { "name": "POAP", "url": "https://poap.xyz"