diff --git a/components/community-downloads/CommunityDownloadsSubscriptions.tsx b/components/community-downloads/CommunityDownloadsSubscriptions.tsx index 4cc479f6a9..e44f3f7635 100644 --- a/components/community-downloads/CommunityDownloadsSubscriptions.tsx +++ b/components/community-downloads/CommunityDownloadsSubscriptions.tsx @@ -28,17 +28,21 @@ export default function CommunityDownloadsSubscriptions() { const [totalResults, setTotalResults] = useState(0); const [page, setPage] = useState(1); - function fetchResults(mypage: number) { + useEffect(() => { + let isMounted = true; + commonApiFetch<{ count: number; data: SubscriptionDownload[] }>({ - endpoint: `subscriptions/uploads?contract=${MEMES_CONTRACT}&page_size=${PAGE_SIZE}&page=${mypage}`, + endpoint: `subscriptions/uploads?contract=${MEMES_CONTRACT}&page_size=${PAGE_SIZE}&page=${page}`, }).then((response) => { + if (!isMounted) return; + setTotalResults(response.count); setDownloads(response.data || []); }); - } - useEffect(() => { - fetchResults(page); + return () => { + isMounted = false; + }; }, [page]); return ( diff --git a/components/eula/EULAConsentContext.tsx b/components/eula/EULAConsentContext.tsx index 2fe8817c54..2ac961efbd 100644 --- a/components/eula/EULAConsentContext.tsx +++ b/components/eula/EULAConsentContext.tsx @@ -6,6 +6,7 @@ import React, { useState, useEffect, useMemo, + useCallback, ReactNode, } from "react"; import { commonApiFetch, commonApiPost } from "@/services/api/common-api"; @@ -42,11 +43,12 @@ export const EULAConsentProvider: React.FC = ({ const [showEULAConsent, setShowEULAConsent] = useState(false); const capacitor = useCapacitor(); + const { isIos, platform } = capacitor; - const getEULAConsent = async () => { + const getEULAConsent = useCallback(async () => { try { const eulaConsent = Cookies.get(CONSENT_EULA_COOKIE) === "true"; - if (!eulaConsent && capacitor.isIos) { + if (!eulaConsent && isIos) { const deviceId = await Device.getId(); const response = await commonApiFetch<{ accepted_at: number }>({ endpoint: `policies/eula-consent/${deviceId.identifier}`, @@ -67,16 +69,16 @@ export const EULAConsentProvider: React.FC = ({ } catch (error) { console.error("Failed to fetch EULA consent status", error); } - }; + }, [isIos]); - const consent = async () => { + const consent = useCallback(async () => { try { const deviceId = await Device.getId(); await commonApiPost({ endpoint: `policies/eula-consent`, body: { device_id: deviceId.identifier, - platform: capacitor.platform, + platform, }, }); Cookies.set(CONSENT_EULA_COOKIE, "true", { expires: 365 }); @@ -88,13 +90,13 @@ export const EULAConsentProvider: React.FC = ({ message: "Something went wrong...", }); } - }; + }, [getEULAConsent, platform, setToast]); const value = useMemo(() => ({ consent }), [consent]); useEffect(() => { getEULAConsent(); - }, []); + }, [getEULAConsent]); return ( diff --git a/components/gas-royalties/Gas.tsx b/components/gas-royalties/Gas.tsx index c5efdc2f2f..d612a3788f 100644 --- a/components/gas-royalties/Gas.tsx +++ b/components/gas-royalties/Gas.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { Container, Row, Col, Table } from "react-bootstrap"; import styles from "./GasRoyalties.module.scss"; import { Gas } from "@/entities/IGas"; @@ -60,11 +60,9 @@ export default function GasComponent() { toBlock, } = useSharedState(); - function getUrlWithParams() { - return getUrl("gas"); - } + const getUrlWithParams = useCallback(() => getUrl("gas"), [getUrl]); - function fetchGas() { + const fetchGas = useCallback(() => { setFetching(true); fetchUrl(getUrlWithParams()).then((res: Gas[]) => { res.forEach((r) => { @@ -74,7 +72,7 @@ export default function GasComponent() { setSumGas(res.map((g) => g.gas).reduce((a, b) => a + b, 0)); setFetching(false); }); - } + }, [getUrlWithParams]); useEffect(() => { if (collectionFocus) { @@ -89,6 +87,7 @@ export default function GasComponent() { selectedArtist, isPrimary, isCustomBlocks, + fetchGas, ]); useEffect(() => { @@ -96,7 +95,7 @@ export default function GasComponent() { setGas([]); fetchGas(); } - }, [collectionFocus]); + }, [collectionFocus, fetchGas]); if (!collectionFocus) { return <>; diff --git a/components/gas-royalties/Royalties.tsx b/components/gas-royalties/Royalties.tsx index a243dd091f..cab89287d0 100644 --- a/components/gas-royalties/Royalties.tsx +++ b/components/gas-royalties/Royalties.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useEffectEvent } from "react"; import { Container, Row, Col, Table } from "react-bootstrap"; import styles from "./GasRoyalties.module.scss"; import { Royalty } from "@/entities/IRoyalty"; @@ -71,7 +71,7 @@ export default function RoyaltiesComponent() { return getUrl("royalties"); } - function fetchRoyalties() { + const fetchRoyalties = useEffectEvent(() => { setFetching(true); fetchUrl(getUrlWithParams()).then((res: Royalty[]) => { res.forEach((r) => { @@ -88,7 +88,7 @@ export default function RoyaltiesComponent() { ); setFetching(false); }); - } + }); useEffect(() => { if (collectionFocus) { diff --git a/components/groups/page/Groups.tsx b/components/groups/page/Groups.tsx index ff4eed8283..24e3c8ebf8 100644 --- a/components/groups/page/Groups.tsx +++ b/components/groups/page/Groups.tsx @@ -1,6 +1,12 @@ "use client"; -import { useContext, useEffect, useState, type JSX } from "react"; +import { + useCallback, + useContext, + useEffect, + useState, + type JSX, +} from "react"; import GroupCreate from "./create/GroupCreate"; import { AuthContext } from "@/components/auth/Auth"; import GroupsPageListWrapper from "./GroupsPageListWrapper"; @@ -28,28 +34,38 @@ export default function Groups() { const [viewMode, setViewMode] = useState(GroupsViewMode.VIEW); - const onViewModeChange = async (mode: GroupsViewMode): Promise => { - if (mode === GroupsViewMode.CREATE) { - const { success } = await requestAuth(); - if (!success) return; - } else if (pathname) { - router.replace(pathname); - } + const onViewModeChange = useCallback( + async (mode: GroupsViewMode): Promise => { + if (mode === GroupsViewMode.CREATE) { + const { success } = await requestAuth(); + if (!success) return; + } else if (pathname) { + router.replace(pathname); + } + + setViewMode(mode); + }, + [pathname, requestAuth, router], + ); - setViewMode(mode); - }; + const hasConnectedProfile = Boolean(connectedProfile?.handle); + const isProxyActive = Boolean(activeProfileProxy); + const shouldAutoEnableCreate = Boolean( + edit && hasConnectedProfile && !isProxyActive, + ); + const shouldForceViewMode = !hasConnectedProfile || isProxyActive; useEffect(() => { - if (edit && !!connectedProfile?.handle && !activeProfileProxy) { - onViewModeChange(GroupsViewMode.CREATE); + if (shouldAutoEnableCreate) { + void onViewModeChange(GroupsViewMode.CREATE); } - }, [edit]); + }, [shouldAutoEnableCreate, onViewModeChange]); useEffect(() => { - if (!connectedProfile?.handle || activeProfileProxy) { - onViewModeChange(GroupsViewMode.VIEW); + if (shouldForceViewMode) { + void onViewModeChange(GroupsViewMode.VIEW); } - }, [connectedProfile, activeProfileProxy]); + }, [shouldForceViewMode, onViewModeChange]); const components: Record = { [GroupsViewMode.VIEW]: ( diff --git a/components/groups/page/list/card/GroupCard.tsx b/components/groups/page/list/card/GroupCard.tsx index 77c681b757..8cc7828eeb 100644 --- a/components/groups/page/list/card/GroupCard.tsx +++ b/components/groups/page/list/card/GroupCard.tsx @@ -5,7 +5,7 @@ import { ApiGroupFull } from "@/generated/models/ApiGroupFull"; import { ApiRateMatter } from "@/generated/models/ApiRateMatter"; import { getRandomColorWithSeed } from "@/helpers/Helpers"; import { useRouter } from "next/navigation"; -import { useContext, useEffect, useState, type JSX } from "react"; +import { useContext, useEffect, useEffectEvent, useState, type JSX } from "react"; import GroupCardView from "./GroupCardView"; import GroupCardVoteAll from "./vote-all/GroupCardVoteAll"; @@ -54,6 +54,12 @@ export default function GroupCard({ setState(state); }; + const resetGroupState = useEffectEvent(() => { + if (!setActiveGroupIdVoteAll || !group) return; + setActiveGroupIdVoteAll(null); + setState(GroupCardState.IDLE); + }); + const getIsActiveGroupVoteAll = () => { return activeGroupIdVoteAll === group?.id; }; @@ -70,7 +76,7 @@ export default function GroupCard({ useEffect(() => { if (!connectedProfile?.handle) { - onGroupStateChange(GroupCardState.IDLE); + resetGroupState(); } }, [connectedProfile?.handle]); diff --git a/components/groups/page/list/card/GroupCardActionWrapper.tsx b/components/groups/page/list/card/GroupCardActionWrapper.tsx index 06d083d8f8..9f2031eb96 100644 --- a/components/groups/page/list/card/GroupCardActionWrapper.tsx +++ b/components/groups/page/list/card/GroupCardActionWrapper.tsx @@ -1,6 +1,5 @@ "use client"; -import { useEffect, useState } from "react"; import GroupCardActionFooter from "./utils/GroupCardActionFooter"; import { ApiRateMatter } from "@/generated/models/ApiRateMatter"; @@ -40,11 +39,7 @@ export default function GroupCardActionWrapper({ return `${(doneMembersCount / membersCount) * 100}%`; }; - const [progress, setProgress] = useState(getProgress()); - - useEffect(() => { - setProgress(getProgress()); - }, [membersCount, doneMembersCount]); + const progress = getProgress(); return (
diff --git a/components/groups/page/list/card/utils/GroupCardActionStats.tsx b/components/groups/page/list/card/utils/GroupCardActionStats.tsx index 33942d5330..3af7f2cccb 100644 --- a/components/groups/page/list/card/utils/GroupCardActionStats.tsx +++ b/components/groups/page/list/card/utils/GroupCardActionStats.tsx @@ -32,8 +32,6 @@ export default function GroupCardActionStats({ null ); - const [creditPerMember, setCreditPerMember] = useState(0); - useEffect(() => { if (!connectedProfile?.handle) { setRater(null); @@ -96,20 +94,14 @@ export default function GroupCardActionStats({ } }; - useEffect(() => { - const credit = getCreditLeft(); - if ( - typeof credit === "number" && - typeof membersCount === "number" && - credit > 0 && - membersCount > 0 - ) { - const creditPerMember = credit / membersCount; - setCreditPerMember(creditPerMember); - } else { - setCreditPerMember(0); - } - }, [creditLeft, membersCount]); + const credit = getCreditLeft(); + const creditPerMember = + typeof credit === "number" && + typeof membersCount === "number" && + credit > 0 && + membersCount > 0 + ? credit / membersCount + : 0; const count = typeof membersCount === "number" diff --git a/components/groups/page/list/card/vote-all/GroupCardVoteAll.tsx b/components/groups/page/list/card/vote-all/GroupCardVoteAll.tsx index db3d850741..9417708016 100644 --- a/components/groups/page/list/card/vote-all/GroupCardVoteAll.tsx +++ b/components/groups/page/list/card/vote-all/GroupCardVoteAll.tsx @@ -119,16 +119,12 @@ export default function GroupCardVoteAll({ return false; }; - const [disabled, setDisabled] = useState(getIsDisabled()); + const disabled = getIsDisabled(); useEffect( () => setLoading(isFetching || doingRates), [isFetching, doingRates] ); - useEffect( - () => setDisabled(getIsDisabled()), - [amountToAdd, membersCount, loading, category] - ); const bulkRateMutation = useMutation({ mutationFn: async (body: ApiBulkRateRequest) => diff --git a/components/groups/select/item/GroupItem.tsx b/components/groups/select/item/GroupItem.tsx index e789ac038e..9895b1fe2d 100644 --- a/components/groups/select/item/GroupItem.tsx +++ b/components/groups/select/item/GroupItem.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { ImageScale, getScaledImageUri, @@ -19,14 +19,7 @@ export default function GroupItem({ readonly activeGroupId: string | null; readonly onActiveGroupId?: (groupId: string | null) => void; }) { - const getIsActive = (): boolean => - !!activeGroupId && activeGroupId === group.id; - - const [isActive, setIsActive] = useState(getIsActive()); - - useEffect(() => { - setIsActive(getIsActive()); - }, [activeGroupId]); + const isActive = !!activeGroupId && activeGroupId === group.id; const deActivate = () => { if (!isActive || !onActiveGroupId) return; diff --git a/components/header/header-search/HeaderSearchModalItem.tsx b/components/header/header-search/HeaderSearchModalItem.tsx index 9bc69a0364..60f32c6720 100644 --- a/components/header/header-search/HeaderSearchModalItem.tsx +++ b/components/header/header-search/HeaderSearchModalItem.tsx @@ -20,7 +20,12 @@ import useDeviceInfo from "@/hooks/useDeviceInfo"; import { DocumentTextIcon } from "@heroicons/react/24/outline"; import Link from "next/link"; import { usePathname, useSearchParams } from "next/navigation"; -import { useEffect, useRef, type ComponentType } from "react"; +import { + useEffect, + useEffectEvent, + useRef, + type ComponentType, +} from "react"; import { useHoverDirty } from "react-use"; import HeaderSearchModalItemMedia from "./HeaderSearchModalItemMedia"; import HeaderSearchModalPfp from "./HeaderSearchModalPfp"; @@ -140,9 +145,13 @@ export default function HeaderSearchModalItem({ } }; + const emitHover = useEffectEvent((state: boolean) => { + onHover(state); + }); + useEffect(() => { if (supportsHover) { - onHover(isHovering); + emitHover(isHovering); } }, [isHovering, supportsHover]); diff --git a/components/header/share/HeaderShare.tsx b/components/header/share/HeaderShare.tsx index 7ccd0c031a..2c433a1992 100644 --- a/components/header/share/HeaderShare.tsx +++ b/components/header/share/HeaderShare.tsx @@ -7,7 +7,7 @@ import { ShareIcon } from "@heroicons/react/24/outline"; import yaml from "js-yaml"; import Image from "next/image"; import { usePathname, useSearchParams } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { Button, Modal } from "react-bootstrap"; import { Tooltip } from "react-tooltip"; import useIsMobileDevice from "@/hooks/isMobileDevice"; @@ -46,6 +46,54 @@ const squareStyle = { justifyContent: "center", }; +interface OSInfo { + name: "windows" | "mac" | "linux"; + url: string; + displayName: string; + downloadPath: string; + image: string; + enabled: boolean; + version?: string; +} + +interface FileData { + url: string; + sha512: string; + size: number; +} + +interface LatestYml { + version: string; + files: FileData[]; +} + +const osConfigs: OSInfo[] = [ + { + name: "windows", + url: "https://6529bucket.s3.eu-west-1.amazonaws.com/6529-core-app/win/latest.yml", + displayName: "Windows", + downloadPath: "6529-core-app/win/links", + image: "/windows.png", + enabled: true, + }, + { + name: "mac", + url: "https://6529bucket.s3.eu-west-1.amazonaws.com/6529-core-app/mac/latest-mac.yml", + displayName: "macOS", + downloadPath: "6529-core-app/mac/links", + image: "/macos.png", + enabled: true, + }, + { + name: "linux", + url: "https://6529bucket.s3.eu-west-1.amazonaws.com/6529-core-app/linux/latest-linux.yml", + displayName: "Linux", + downloadPath: "6529-core-app/linux/links", + image: "/linux.png", + enabled: true, + }, +]; + export default function HeaderShare({ isCollapsed = false, }: { @@ -130,75 +178,75 @@ function HeaderQRModal({ const [urlCopied, setUrlCopied] = useState(false); - function generateSources( - refreshToken: string | null, - walletAddress: string | null, - role: string | null - ) { - let routerPath = pathname ?? ""; - if (routerPath.endsWith("/")) { - routerPath = routerPath.slice(0, -1); - } + const searchParamsString = searchParams?.toString() ?? ""; - const searchParamsString = searchParams?.toString() ?? ""; - if (searchParamsString) { - routerPath += `?${searchParamsString}`; - } + const generateSources = useCallback( + (refreshToken: string | null, walletAddress: string | null, role: string | null) => { + let routerPath = pathname ?? ""; + if (routerPath.endsWith("/")) { + routerPath = routerPath.slice(0, -1); + } - const appScheme = publicEnv.MOBILE_APP_SCHEME ?? "mobile6529"; - const coreScheme = publicEnv.CORE_SCHEME ?? "core6529"; + if (searchParamsString) { + routerPath += `?${searchParamsString}`; + } - const browserUrl = `${window.location.origin}${routerPath}`; - const appUrl = `${appScheme}://${DeepLinkScope.NAVIGATE}${routerPath}`; - const coreUrl = `${coreScheme}://${DeepLinkScope.NAVIGATE}${routerPath}`; + const appScheme = publicEnv.MOBILE_APP_SCHEME ?? "mobile6529"; + const coreScheme = publicEnv.CORE_SCHEME ?? "core6529"; - setNavigateBrowserUrl(browserUrl); - setNavigateAppUrl(appUrl); - setNavigateCoreUrl(coreUrl); + const browserUrl = `${window.location.origin}${routerPath}`; + const appUrl = `${appScheme}://${DeepLinkScope.NAVIGATE}${routerPath}`; + const coreUrl = `${coreScheme}://${DeepLinkScope.NAVIGATE}${routerPath}`; - let shareConnectionAppUrl = ""; - let shareConnectionCoreUrl = ""; + setNavigateBrowserUrl(browserUrl); + setNavigateAppUrl(appUrl); + setNavigateCoreUrl(coreUrl); - if (refreshToken && walletAddress) { - shareConnectionAppUrl = `${appScheme}://${DeepLinkScope.SHARE_CONNECTION}?token=${refreshToken}&address=${walletAddress}`; - shareConnectionCoreUrl = `${coreScheme}://${DeepLinkScope.NAVIGATE}/accept-connection-sharing?token=${refreshToken}&address=${walletAddress}`; + let shareConnectionAppUrl = ""; + let shareConnectionCoreUrl = ""; - if (role) { - shareConnectionAppUrl += `&role=${role}`; - shareConnectionCoreUrl += `&role=${role}`; - } - setShareConnectionAppUrl(shareConnectionAppUrl); - setShareConnectionCoreUrl(shareConnectionCoreUrl); - } else { - setShareConnectionSrc(""); - } + if (refreshToken && walletAddress) { + shareConnectionAppUrl = `${appScheme}://${DeepLinkScope.SHARE_CONNECTION}?token=${refreshToken}&address=${walletAddress}`; + shareConnectionCoreUrl = `${coreScheme}://${DeepLinkScope.NAVIGATE}/accept-connection-sharing?token=${refreshToken}&address=${walletAddress}`; - QRCode.toDataURL(browserUrl, { width: 500, margin: 0 }).then( - (dataUrl: string) => { - setNavigateBrowserSrc(dataUrl); + if (role) { + shareConnectionAppUrl += `&role=${role}`; + shareConnectionCoreUrl += `&role=${role}`; + } + setShareConnectionAppUrl(shareConnectionAppUrl); + setShareConnectionCoreUrl(shareConnectionCoreUrl); + } else { + setShareConnectionSrc(""); } - ); - QRCode.toDataURL(appUrl, { width: 500, margin: 0 }).then( - (dataUrl: string) => { - setNavigateAppSrc(dataUrl); - } - ); + QRCode.toDataURL(browserUrl, { width: 500, margin: 0 }).then( + (dataUrl: string) => { + setNavigateBrowserSrc(dataUrl); + } + ); - if (shareConnectionAppUrl) { - QRCode.toDataURL(shareConnectionAppUrl, { width: 500, margin: 0 }).then( + QRCode.toDataURL(appUrl, { width: 500, margin: 0 }).then( (dataUrl: string) => { - setShareConnectionSrc(dataUrl); + setNavigateAppSrc(dataUrl); } ); - } - } + + if (shareConnectionAppUrl) { + QRCode.toDataURL(shareConnectionAppUrl, { width: 500, margin: 0 }).then( + (dataUrl: string) => { + setShareConnectionSrc(dataUrl); + } + ); + } + }, + [pathname, searchParamsString] + ); useEffect(() => { if (show) { generateSources(getRefreshToken(), getWalletAddress(), getWalletRole()); } - }, [show]); + }, [show, generateSources]); useEffect(() => { setActiveTab(isAuthenticated ? Mode.SHARE : Mode.NAVIGATE); @@ -457,54 +505,6 @@ function ModalMenu({ } function CoreAppsDownload() { - interface OSInfo { - name: "windows" | "mac" | "linux"; - url: string; - displayName: string; - downloadPath: string; - image: string; - enabled: boolean; - version?: string; - } - - interface FileData { - url: string; - sha512: string; - size: number; - } - - interface LatestYml { - version: string; - files: FileData[]; - } - - const osConfigs: OSInfo[] = [ - { - name: "windows", - url: "https://6529bucket.s3.eu-west-1.amazonaws.com/6529-core-app/win/latest.yml", - displayName: "Windows", - downloadPath: "6529-core-app/win/links", - image: "/windows.png", - enabled: true, - }, - { - name: "mac", - url: "https://6529bucket.s3.eu-west-1.amazonaws.com/6529-core-app/mac/latest-mac.yml", - displayName: "macOS", - downloadPath: "6529-core-app/mac/links", - image: "/macos.png", - enabled: true, - }, - { - name: "linux", - url: "https://6529bucket.s3.eu-west-1.amazonaws.com/6529-core-app/linux/latest-linux.yml", - displayName: "Linux", - downloadPath: "6529-core-app/linux/links", - image: "/linux.png", - enabled: true, - }, - ]; - const [versions, setVersions] = useState([]); useEffect(() => { diff --git a/components/ipfs/IPFSContext.tsx b/components/ipfs/IPFSContext.tsx index 25a7a5f0eb..2090e20ec4 100644 --- a/components/ipfs/IPFSContext.tsx +++ b/components/ipfs/IPFSContext.tsx @@ -59,7 +59,7 @@ export const IpfsProvider: React.FC<{ children: React.ReactNode }> = ({ .catch((error) => { console.error("Error initializing IPFS service", error); }); - }, []); + }, [ipfsService]); const value = useMemo(() => ({ ipfsService }), [ipfsService]); diff --git a/components/leaderboard/NFTLeaderboard.tsx b/components/leaderboard/NFTLeaderboard.tsx index 478f4d36b5..f5eef54f4a 100644 --- a/components/leaderboard/NFTLeaderboard.tsx +++ b/components/leaderboard/NFTLeaderboard.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useEffectEvent } from "react"; import { Container, Row, Col, Table } from "react-bootstrap"; import styles from "./Leaderboard.module.scss"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -109,7 +109,7 @@ export default function NFTLeaderboard(props: Readonly) { const [showSearchModal, setShowSearchModal] = useState(false); const [searchWallets, setSearchWallets] = useState([]); - async function fetchResults() { + const fetchResults = useEffectEvent(async () => { setFetchingLeaderboard(true); let walletFilter = ""; if (searchWallets && searchWallets.length > 0) { @@ -134,7 +134,7 @@ export default function NFTLeaderboard(props: Readonly) { setLeaderboard(data); setScrollPosition(); setFetchingLeaderboard(false); - } + }); useEffect(() => { if (page === 1) { @@ -142,11 +142,11 @@ export default function NFTLeaderboard(props: Readonly) { } else { setPage(1); } - }, [sort, searchWallets]); + }, [sort, searchWallets, fetchResults]); useEffect(() => { fetchResults(); - }, [page]); + }, [page, fetchResults]); return ( diff --git a/components/lfg-slideshow/LFGSlideshow.tsx b/components/lfg-slideshow/LFGSlideshow.tsx index 36a2cea3fa..51eba1d5b5 100644 --- a/components/lfg-slideshow/LFGSlideshow.tsx +++ b/components/lfg-slideshow/LFGSlideshow.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useEffect, useEffectEvent, useRef, useState } from "react"; import styles from "./LFGSlideshow.module.scss"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faExpand, faXmarkCircle } from "@fortawesome/free-solid-svg-icons"; @@ -19,10 +19,10 @@ const LFGSlideshow: React.FC<{ setIsOpen: (isOpen: boolean) => void; }> = ({ isOpen, contract, setIsOpen }) => { const [currentIndex, setCurrentIndex] = useState(0); - const [bodyOverflow, setBodyOverflow] = useState(); const [isMuted, setIsMuted] = useState(false); const [media, setMedia] = useState([]); + const bodyOverflowRef = useRef(""); let slideTimer: NodeJS.Timeout | null = null; @@ -60,6 +60,10 @@ const LFGSlideshow: React.FC<{ } }; + const closeSlideshow = useEffectEvent(() => { + setIsOpen(false); + }); + const nextSlide = () => { if (slideTimer) { clearTimeout(slideTimer); @@ -70,7 +74,7 @@ const LFGSlideshow: React.FC<{ useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { - setIsOpen(false); + closeSlideshow(); } }; @@ -78,18 +82,19 @@ const LFGSlideshow: React.FC<{ return () => { window.removeEventListener("keydown", handleKeyDown); }; - }, []); + }, [closeSlideshow]); useEffect(() => { - if (isOpen) { - setBodyOverflow(document.body.style.overflow); - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = bodyOverflow ?? "unset"; + if (!isOpen) { + return; } + const { style } = document.body; + bodyOverflowRef.current = style.overflow; + style.overflow = "hidden"; + return () => { - document.body.style.overflow = bodyOverflow ?? "unset"; + style.overflow = bodyOverflowRef.current; }; }, [isOpen]); @@ -126,6 +131,10 @@ const LFGSlideshow: React.FC<{ videoElement.addEventListener("volumechange", handleVolumeChange); return () => { videoElement.removeEventListener("ended", handleEnded); + videoElement.removeEventListener( + "volumechange", + handleVolumeChange + ); }; } } else { diff --git a/components/mapping-tools/ConsolidationMappingTool.tsx b/components/mapping-tools/ConsolidationMappingTool.tsx index fb8b05b6ab..a608bec063 100644 --- a/components/mapping-tools/ConsolidationMappingTool.tsx +++ b/components/mapping-tools/ConsolidationMappingTool.tsx @@ -99,7 +99,10 @@ export default function ConsolidationMappingTool() { } useEffect(() => { - async function fetchConsolidations(url: string) { + if (!processing || !file) { + return; + } + async function fetchConsolidations(url: string, selectedFile: File) { fetchAllPages(url).then((consolidations) => { setConsolidations(consolidations); const reader = new FileReader(); @@ -133,14 +136,13 @@ export default function ConsolidationMappingTool() { parser.end(); }; - reader.readAsText(file); + reader.readAsText(selectedFile); }); } - if (processing) { - const initialUrl = `${publicEnv.API_ENDPOINT}/api/consolidations`; - fetchConsolidations(initialUrl); - } - }, [processing]); + + const initialUrl = `${publicEnv.API_ENDPOINT}/api/consolidations`; + fetchConsolidations(initialUrl, file); + }, [file, processing]); useEffect(() => { const out: ConsolidationData[] = []; diff --git a/components/memelab/MemeLab.tsx b/components/memelab/MemeLab.tsx index 55248bbe08..7cd269cb48 100644 --- a/components/memelab/MemeLab.tsx +++ b/components/memelab/MemeLab.tsx @@ -454,7 +454,7 @@ export default function MemeLabComponent() { ); setSortDir(initialSortDir); setSort(initialSort); - }, []); + }, [searchParams]); const [sortDir, setSortDir] = useState(); const [sort, setSort] = useState(MemeLabSort.AGE); diff --git a/components/memelab/MemeLabCollection.tsx b/components/memelab/MemeLabCollection.tsx index 256986bf9b..19af06e4a6 100644 --- a/components/memelab/MemeLabCollection.tsx +++ b/components/memelab/MemeLabCollection.tsx @@ -49,20 +49,22 @@ export default function LabCollection({ const [nftsLoaded, setNftsLoaded] = useState(false); const [nftBalances, setNftBalances] = useState([]); - const [sortDir, setSortDir] = useState(); - const [sort, setSort] = useState(MemeLabSort.AGE); - - const [volumeType, setVolumeType] = useState(VolumeType.HOURS_24); - - useEffect(() => { - const { initialSortDir, initialSort } = getInitialRouterValues( + const [sortDir, setSortDir] = useState(() => { + const { initialSortDir } = getInitialRouterValues( searchParams?.get("sortDir") ?? null, searchParams?.get("sort") ?? null ); - setSortDir(initialSortDir); - setSort(initialSort); - setVolumeType(VolumeType.HOURS_24); - }, []); + return initialSortDir; + }); + const [sort, setSort] = useState(() => { + const { initialSort } = getInitialRouterValues( + searchParams?.get("sortDir") ?? null, + searchParams?.get("sort") ?? null + ); + return initialSort; + }); + + const [volumeType, setVolumeType] = useState(VolumeType.HOURS_24); useEffect(() => { if (collectionName) { diff --git a/components/memelab/MemeLabPage.tsx b/components/memelab/MemeLabPage.tsx index 97c8add4db..6dc41b7915 100644 --- a/components/memelab/MemeLabPage.tsx +++ b/components/memelab/MemeLabPage.tsx @@ -72,6 +72,7 @@ export default function MemeLabPageComponent({ const router = useRouter(); const searchParams = useSearchParams(); const capacitor = useCapacitor(); + const focusParam = searchParams?.get("focus"); const { connectedProfile } = useAuth(); const { country } = useCookieConsent(); @@ -106,30 +107,27 @@ export default function MemeLabPageComponent({ useEffect(() => { setTitle(getMemeTabTitle(`Meme Lab`, nftId, nft, activeTab)); - }, [nft, nftId, activeTab]); + }, [nft, nftId, activeTab, setTitle]); useEffect(() => { setIsFullScreenSupported(fullScreenSupported()); - let initialFocus = MEME_FOCUS.LIVE; - - const routerFocus = searchParams?.get("focus"); - if (routerFocus) { - const resolvedRouterFocus = Object.values(MEME_FOCUS).find( - (sd) => sd === routerFocus - ); - if (resolvedRouterFocus) { - initialFocus = resolvedRouterFocus; - } - } - setActiveTab(initialFocus); }, []); + useEffect(() => { + const resolvedRouterFocus = focusParam + ? Object.values(MEME_FOCUS).find((sd) => sd === focusParam) + : undefined; + const nextFocus = resolvedRouterFocus ?? MEME_FOCUS.LIVE; + + setActiveTab((current) => (current === nextFocus ? current : nextFocus)); + }, [focusParam]); + useEffect(() => { if (activeTab) { let query: any = { focus: activeTab }; router.replace(`?${new URLSearchParams(query).toString()}`); } - }, [activeTab]); + }, [activeTab, router]); useEffect(() => { if (nftId) { diff --git a/components/monitoring/AwsRumProvider.tsx b/components/monitoring/AwsRumProvider.tsx index ff85182dd2..4ee3c451d5 100644 --- a/components/monitoring/AwsRumProvider.tsx +++ b/components/monitoring/AwsRumProvider.tsx @@ -12,8 +12,6 @@ export default function AwsRumProvider({ children, }: Readonly) { useEffect(() => { - // Only initialize AWS RUM on the client side - if (typeof window === "undefined") return; // Skip initialization in development mode to avoid noise if (publicEnv.NODE_ENV === "development") { diff --git a/components/nextGen/admin/NextGenAdminInitializeBurn.tsx b/components/nextGen/admin/NextGenAdminInitializeBurn.tsx index f0643aa0e7..c60e957dfa 100644 --- a/components/nextGen/admin/NextGenAdminInitializeBurn.tsx +++ b/components/nextGen/admin/NextGenAdminInitializeBurn.tsx @@ -84,12 +84,19 @@ export default function NextGenAdminInitializeBurn(props: Readonly) { setStatus(burnRead.data as any); }, [burnRead.data]); + const signMessageErrorMessage = signMessage.error?.message; + useEffect(() => { - if (signMessage.isError) { - setLoading(false); - setUploadError(`Error: ${signMessage.error?.message.split(".")[0]}`); + if (!signMessage.isError) { + return; } - }, [signMessage.isError]); + + const conciseError = + signMessageErrorMessage?.split(".")[0] ?? "Unknown error"; + + setLoading(false); + setUploadError(`Error: ${conciseError}`); + }, [signMessage.isError, signMessageErrorMessage]); useEffect(() => { if (signMessage.isSuccess && signMessage.data) { diff --git a/components/nextGen/admin/NextGenAdminSetPhases.tsx b/components/nextGen/admin/NextGenAdminSetPhases.tsx index 49e91a6fda..acb699db86 100644 --- a/components/nextGen/admin/NextGenAdminSetPhases.tsx +++ b/components/nextGen/admin/NextGenAdminSetPhases.tsx @@ -99,7 +99,7 @@ export default function NextGenAdminSetPhases(props: Readonly) { ); setSelectedAllowlist(selected); } - }, [onChainMerkleRoot, collectionID]); + }, [onChainMerkleRoot, collectionID, availableAllowlistCollections]); function clear() { setSelectedAllowlist(undefined); diff --git a/components/nextGen/admin/NextGenAdminUploadAL.tsx b/components/nextGen/admin/NextGenAdminUploadAL.tsx index cc0352afb6..f8d3d06ef9 100644 --- a/components/nextGen/admin/NextGenAdminUploadAL.tsx +++ b/components/nextGen/admin/NextGenAdminUploadAL.tsx @@ -36,6 +36,7 @@ enum Type { export default function NextGenAdminUploadAL(props: Readonly) { const account = useSeizeConnectContext(); const signMessage = useSignMessage(); + const signMessageErrorMessage = signMessage.error?.message; const uuid = useRef(uuidv4()).current; const globalAdmin = useGlobalAdmin(account.address as string); @@ -90,11 +91,16 @@ export default function NextGenAdminUploadAL(props: Readonly) { } useEffect(() => { - if (signMessage.isError) { - setUploading(false); - setUploadError(`Error: ${signMessage.error?.message.split(".")[0]}`); + if (!signMessage.isError) { + return; } - }, [signMessage.isError]); + + const conciseError = + signMessageErrorMessage?.split(".")[0] ?? "Unknown error"; + + setUploading(false); + setUploadError(`Error: ${conciseError}`); + }, [signMessage.isError, signMessageErrorMessage]); useEffect(() => { if (signMessage.isSuccess && signMessage.data) { diff --git a/components/nextGen/collections/NextGenCollections.tsx b/components/nextGen/collections/NextGenCollections.tsx index 05cfb2a504..0432c24736 100644 --- a/components/nextGen/collections/NextGenCollections.tsx +++ b/components/nextGen/collections/NextGenCollections.tsx @@ -5,7 +5,7 @@ import { publicEnv } from "@/config/env"; import { DBResponse } from "@/entities/IDBResponse"; import { NextGenCollection } from "@/entities/INextgen"; import { fetchUrl } from "@/services/6529api"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useEffectEvent } from "react"; import { Col, Container, Dropdown, Row } from "react-bootstrap"; import styles from "./NextGen.module.scss"; import NextGenCollectionPreview from "./NextGenCollectionPreview"; @@ -29,7 +29,7 @@ export default function NextGenCollections() { const [totalResults, setTotalResults] = useState(0); const [page, setPage] = useState(1); - function fetchResults(mypage: number) { + const fetchResults = useEffectEvent((mypage: number) => { setCollectionsLoaded(false); let statusFilter = ""; if (selectedStatus !== StatusFilter.ALL) { @@ -42,7 +42,7 @@ export default function NextGenCollections() { setCollections(response.data); setCollectionsLoaded(true); }); - } + }); useEffect(() => { if (page === 1) { diff --git a/components/nextGen/collections/NextGenTokenList.tsx b/components/nextGen/collections/NextGenTokenList.tsx index f174f39827..1320c1ddea 100644 --- a/components/nextGen/collections/NextGenTokenList.tsx +++ b/components/nextGen/collections/NextGenTokenList.tsx @@ -7,7 +7,7 @@ import { NextGenToken, TraitValuePair, } from "@/entities/INextgen"; -import { useEffect, useState } from "react"; +import { useEffect, useEffectEvent, useState } from "react"; import Pagination from "@/components/pagination/Pagination"; import { commonApiFetch } from "@/services/api/common-api"; import DotLoader from "@/components/dotLoader/DotLoader"; @@ -32,42 +32,52 @@ interface Props { show_pagination?: boolean; } -export default function NextGenTokenList(props: Readonly) { - const pageSize = props.limit ?? 48; +export default function NextGenTokenList({ + collection, + limit, + sort, + sort_direction: sortDirection, + selected_traits: selectedTraits, + show_normalised: showNormalised, + show_trait_count: showTraitCount, + listed_type: listedType, + setTotalResults: notifyTotalResults, + show_pagination: showPagination, +}: Readonly) { + const pageSize = limit ?? 48; const [tokens, setTokens] = useState([]); const [tokensLoaded, setTokensLoaded] = useState(false); const [totalResults, setTotalResults] = useState(0); const [page, setPage] = useState(1); const [rarityType, setRarityType] = useState(); - - function fetchResults(mypage: number) { + const fetchResults = useEffectEvent((mypage: number) => { setTokensLoaded(false); - let endpoint = `nextgen/collections/${props.collection.id}/tokens?page_size=${pageSize}&page=${mypage}`; - if (props.selected_traits) { - const traitsQ = props.selected_traits + let endpoint = `nextgen/collections/${collection.id}/tokens?page_size=${pageSize}&page=${mypage}`; + if (selectedTraits) { + const traitsQ = selectedTraits .map((t) => `${t.trait}:${t.value}`) .join(","); endpoint += `&traits=${encodeURIComponent(traitsQ)}`; } - if (props.show_normalised) { + if (showNormalised) { endpoint += `&show_normalised=true`; } - if (props.show_trait_count) { + if (showTraitCount) { endpoint += `&show_trait_count=true`; } - if (props.listed_type === NextGenTokenListedType.LISTED) { + if (listedType === NextGenTokenListedType.LISTED) { endpoint += `&listed=true`; - } else if (props.listed_type === NextGenTokenListedType.NOT_LISTED) { + } else if (listedType === NextGenTokenListedType.NOT_LISTED) { endpoint += `&listed=false`; } - if (props.sort) { - endpoint += `&sort=${props.sort.replaceAll(" ", "_").toLowerCase()}`; + if (sort) { + endpoint += `&sort=${sort.replaceAll(" ", "_").toLowerCase()}`; } else { endpoint += `&sort=random`; } - if (props.sort_direction) { - endpoint += `&sort_direction=${props.sort_direction.toLowerCase()}`; + if (sortDirection) { + endpoint += `&sort_direction=${sortDirection.toLowerCase()}`; } commonApiFetch<{ count: number; @@ -78,13 +88,13 @@ export default function NextGenTokenList(props: Readonly) { endpoint: endpoint, }).then((response) => { setTotalResults(response.count); - if (props.setTotalResults) { - props.setTotalResults(response.count); + if (notifyTotalResults) { + notifyTotalResults(response.count); } setTokens(response.data); setTokensLoaded(true); }); - } + }); useEffect(() => { if (page === 1) { @@ -93,12 +103,12 @@ export default function NextGenTokenList(props: Readonly) { setPage(1); } }, [ - props.selected_traits, - props.sort, - props.sort_direction, - props.show_normalised, - props.show_trait_count, - props.listed_type, + selectedTraits, + sort, + sortDirection, + showNormalised, + showTraitCount, + listedType, ]); useEffect(() => { @@ -121,33 +131,32 @@ export default function NextGenTokenList(props: Readonly) { } useEffect(() => { - const { sort, show_normalised, show_trait_count } = props; let newRarityType = undefined; switch (sort) { case NextGenListFilters.RARITY_SCORE: newRarityType = getRarityType( NextGenTokenRarityType.RARITY_SCORE, - show_normalised, - show_trait_count + showNormalised, + showTraitCount ); break; case NextGenListFilters.STATISTICAL_SCORE: newRarityType = getRarityType( NextGenTokenRarityType.STATISTICAL_SCORE, - show_normalised, - show_trait_count + showNormalised, + showTraitCount ); break; case NextGenListFilters.SINGLE_TRAIT_RARITY: newRarityType = getRarityType( NextGenTokenRarityType.SINGLE_TRAIT_RARITY_SCORE, - show_normalised, - show_trait_count + showNormalised, + showTraitCount ); break; } setRarityType(newRarityType); - }, [props.sort, props.show_normalised, props.show_trait_count]); + }, [sort, showNormalised, showTraitCount]); return ( @@ -166,13 +175,11 @@ export default function NextGenTokenList(props: Readonly) { token={t} rarity_type={rarityType} show_listing={ - props.sort === NextGenListFilters.LISTED_PRICE || - props.listed_type === NextGenTokenListedType.LISTED - } - show_max_sale={ - props.sort === NextGenListFilters.HIGHEST_SALE + sort === NextGenListFilters.LISTED_PRICE || + listedType === NextGenTokenListedType.LISTED } - show_last_sale={props.sort === NextGenListFilters.LAST_SALE} + show_max_sale={sort === NextGenListFilters.HIGHEST_SALE} + show_last_sale={sort === NextGenListFilters.LAST_SALE} show_owner_info={true} /> @@ -189,7 +196,7 @@ export default function NextGenTokenList(props: Readonly) { } })()} - {totalResults > pageSize && tokensLoaded && props.show_pagination && ( + {totalResults > pageSize && tokensLoaded && showPagination && ( ) { ].includes(s); } - function setTraitsQuery(q: string) { - if (q) { - const traitValues = q.split(","); - const selectedTraits: TraitValuePair[] = []; - traitValues.forEach((tv) => { - const [t, v] = tv.split(":"); - if ( - traits.some( - (tr) => - areEqualAddresses(tr.trait, t) && - tr.values.some((vl) => areEqualAddresses(vl, v)) - ) - ) { - selectedTraits.push({ - trait: t, - value: v, - }); - } - }); - setSelectedTraitValues(selectedTraits); - } else { - setSelectedTraitValues([]); - } - } + const setTraitsQuery = useCallback( + (q: string) => { + if (q) { + const traitValues = q.split(","); + const selectedTraits: TraitValuePair[] = []; + traitValues.forEach((tv) => { + const [t, v] = tv.split(":"); + if ( + traits.some( + (tr) => + areEqualAddresses(tr.trait, t) && + tr.values.some((vl) => areEqualAddresses(vl, v)) + ) + ) { + selectedTraits.push({ + trait: t, + value: v, + }); + } + }); + setSelectedTraitValues(selectedTraits); + } else { + setSelectedTraitValues([]); + } + }, + [traits] + ); useEffect(() => { if (traitsLoaded && !routerLoaded) { @@ -156,7 +159,7 @@ export default function NextGenCollectionArt(props: Readonly) { } setRouterLoaded(true); } - }, [searchParams, traitsLoaded, routerLoaded]); + }, [searchParams, traitsLoaded, routerLoaded, setTraitsQuery]); useEffect(() => { commonApiFetch({ @@ -210,9 +213,7 @@ export default function NextGenCollectionArt(props: Readonly) { ]); useEffect(() => { - if (totalResultsSet) { - setTotalResultsSet(false); - } + setTotalResultsSet(false); }, [selectedTraitValues]); function getDefaultActiveKeys() { diff --git a/components/nextGen/collections/collectionParts/NextGenCollectionArtist.tsx b/components/nextGen/collections/collectionParts/NextGenCollectionArtist.tsx index 1e7369b3b7..f40b88886b 100644 --- a/components/nextGen/collections/collectionParts/NextGenCollectionArtist.tsx +++ b/components/nextGen/collections/collectionParts/NextGenCollectionArtist.tsx @@ -19,21 +19,31 @@ interface Props { export default function NextGenCollectionArtist(props: Readonly) { const [bio, setBio] = useState(""); + const artistAddress = props.collection.artist_address; + const { profile } = useIdentity({ - handleOrWallet: props.collection.artist_address, + handleOrWallet: artistAddress, initialProfile: null, }); useEffect(() => { + let isMounted = true; commonApiFetch({ - endpoint: `profiles/${props.collection.artist_address}/cic/statements`, + endpoint: `profiles/${artistAddress}/cic/statements`, }).then((statements) => { + if (!isMounted) { + return; + } const bioStatement = statements.find( (s) => s.statement_type === STATEMENT_TYPE.BIO ); setBio(bioStatement?.statement_value ?? ""); }); - }, []); + + return () => { + isMounted = false; + }; + }, [artistAddress]); return ( diff --git a/components/nextGen/collections/collectionParts/NextGenCollectionHeader.tsx b/components/nextGen/collections/collectionParts/NextGenCollectionHeader.tsx index 12b1036cbf..c1ad8deec3 100644 --- a/components/nextGen/collections/collectionParts/NextGenCollectionHeader.tsx +++ b/components/nextGen/collections/collectionParts/NextGenCollectionHeader.tsx @@ -329,41 +329,49 @@ export function NextGenMintCounts( setShouldRefetchMintCounts?(shouldRefetchMintCounts: boolean): void; }> ) { + const { + collection, + setAvailable: notifyAvailable, + shouldRefetchMintCounts = false, + setShouldRefetchMintCounts, + } = props; + const { id: collectionId, total_supply: totalSupply } = collection; const [enableRefresh, setEnableRefresh] = useState(true); const [available, setAvailable] = useState(0); const collectionMintCount = useCollectionMintCount( - props.collection.id, + collectionId, enableRefresh ); + const { data: mintCountData, isFetching, refetch } = collectionMintCount; const [mintCount, setMintCount] = useState(0); const [isLoading, setIsLoading] = useState(true); useEffect(() => { - if (props.shouldRefetchMintCounts) { - collectionMintCount.refetch().then(() => { - if (props.setShouldRefetchMintCounts) { - props.setShouldRefetchMintCounts(false); + if (shouldRefetchMintCounts) { + refetch().then(() => { + if (setShouldRefetchMintCounts) { + setShouldRefetchMintCounts(false); } }); } - }, [props.shouldRefetchMintCounts]); + }, [shouldRefetchMintCounts, refetch, setShouldRefetchMintCounts]); useEffect(() => { - setIsLoading(collectionMintCount.isFetching); - }, [collectionMintCount.isFetching]); + setIsLoading(isFetching); + }, [isFetching]); useEffect(() => { - const mintC = parseInt(collectionMintCount.data as any); + const mintC = parseInt(mintCountData as any); setMintCount(mintC); - const avail = props.collection.total_supply - mintC; + const avail = totalSupply - mintC; setAvailable(avail); setEnableRefresh(avail > 0); - if (props.setAvailable) { - props.setAvailable(avail); + if (notifyAvailable) { + notifyAvailable(avail); } - }, [collectionMintCount.data]); + }, [mintCountData, totalSupply, notifyAvailable]); return ( diff --git a/components/nextGen/collections/collectionParts/NextGenCollectionProvenance.tsx b/components/nextGen/collections/collectionParts/NextGenCollectionProvenance.tsx index 4232589c58..9811063108 100644 --- a/components/nextGen/collections/collectionParts/NextGenCollectionProvenance.tsx +++ b/components/nextGen/collections/collectionParts/NextGenCollectionProvenance.tsx @@ -34,32 +34,39 @@ interface Props { const PAGE_SIZE = 20; export default function NextGenCollectionProvenance(props: Readonly) { + const { collection } = props; const scrollTarget = useRef(null); const [logs, setLogs] = useState([]); const [logsLoaded, setLogsLoaded] = useState(false); const [totalResults, setTotalResults] = useState(0); const [page, setPage] = useState(1); + const collectionId = collection.id; - function fetchResults(mypage: number) { + useEffect(() => { + let cancelled = false; setLogsLoaded(false); + commonApiFetch<{ count: number; page: number; next: any; data: NextGenLog[]; }>({ - endpoint: `nextgen/collections/${props.collection.id}/logs?page_size=${PAGE_SIZE}&page=${mypage}`, + endpoint: `nextgen/collections/${collectionId}/logs?page_size=${PAGE_SIZE}&page=${page}`, }).then((response) => { + if (cancelled) { + return; + } setTotalResults(response.count); setLogs(response.data); setLogsLoaded(true); }); - } - useEffect(() => { - fetchResults(page); - }, [page]); + return () => { + cancelled = true; + }; + }, [collectionId, page]); return ( @@ -67,7 +74,7 @@ export default function NextGenCollectionProvenance(props: Readonly) { {logs.map((log, index) => ( ) { + const { collection, token_id } = props; const scrollTarget = useRef(null); const logsScrollTarget = useRef(null); @@ -31,49 +32,62 @@ export default function NextGenTokenProvenance(props: Readonly) { const [logsTotalResults, setLogsTotalResults] = useState(0); const [logsPage, setLogsPage] = useState(1); - function fetchResults(mypage: number) { + const fetchResults = useEffectEvent(async (requestedPage: number) => { setTransactionsLoaded(false); - commonApiFetch<{ + const requestedTokenId = token_id; + + const response = await commonApiFetch<{ count: number; page: number; next: any; data: Transaction[]; }>({ - endpoint: `nextgen/tokens/${props.token_id}/transactions?page_size=${PAGE_SIZE}&page=${mypage}`, - }).then((response) => { - setTotalResults(response.count); - setTransactions(response.data); - setTransactionsLoaded(true); + endpoint: `nextgen/tokens/${requestedTokenId}/transactions?page_size=${PAGE_SIZE}&page=${requestedPage}`, }); - } + + if (requestedTokenId !== token_id || requestedPage !== page) { + return; + } + + setTotalResults(response.count); + setTransactions(response.data); + setTransactionsLoaded(true); + }); useEffect(() => { fetchResults(page); - }, [page]); + }, [fetchResults, page, token_id]); - function fetchLogsResults(mypage: number) { + const fetchLogsResults = useEffectEvent(async (requestedPage: number) => { setLogsLoaded(false); - commonApiFetch<{ + const requestedCollectionId = collection.id; + const requestedTokenId = token_id; + + const response = await commonApiFetch<{ count: number; page: number; next: any; data: NextGenLog[]; }>({ - endpoint: `nextgen/collections/${props.collection.id}/logs/${props.token_id}?page_size=${PAGE_SIZE}&page=${mypage}`, - }).then((response) => { - setLogsTotalResults(response.count); - setLogs(response.data); - setLogsLoaded(true); + endpoint: `nextgen/collections/${requestedCollectionId}/logs/${requestedTokenId}?page_size=${PAGE_SIZE}&page=${requestedPage}`, }); - } - useEffect(() => { - fetchResults(page); - }, [page]); + if ( + requestedCollectionId !== collection.id || + requestedTokenId !== token_id || + requestedPage !== logsPage + ) { + return; + } + + setLogsTotalResults(response.count); + setLogs(response.data); + setLogsLoaded(true); + }); useEffect(() => { fetchLogsResults(logsPage); - }, [logsPage]); + }, [collection.id, fetchLogsResults, logsPage, token_id]); return ( <> @@ -126,7 +140,7 @@ export default function NextGenTokenProvenance(props: Readonly) { {logs.map((log, index) => ( = ({ const router = useRouter(); useEffect(() => { - initializeNotifications(connectedProfile ?? undefined); - }, [connectedProfile]); - - const initializeNotifications = async (profile?: ApiIdentity) => { - try { - if (isCapacitor) { - console.log("Initializing push notifications"); - await initializePushNotifications(profile); + void initializeNotifications(connectedProfile ?? undefined); + }, [connectedProfile, isCapacitor]); + + const initializeNotifications = useEffectEvent( + async (profile?: ApiIdentity) => { + try { + if (isCapacitor) { + console.log("Initializing push notifications"); + await initializePushNotifications(profile); + } + } catch (error) { + console.error("Error initializing notifications", error); } - } catch (error) { - console.error("Error initializing notifications", error); } - }; + ); const initializePushNotifications = async (profile?: ApiIdentity) => { await PushNotifications.removeAllListeners(); diff --git a/components/prenodes/PrenodesStatus.tsx b/components/prenodes/PrenodesStatus.tsx index df5716a764..51cb8c7d53 100644 --- a/components/prenodes/PrenodesStatus.tsx +++ b/components/prenodes/PrenodesStatus.tsx @@ -1,7 +1,7 @@ "use client"; import styles from "./Prenodes.module.scss"; -import { useEffect, useState } from "react"; +import { useEffect, useEffectEvent, useState } from "react"; import { Col, Container, Row, Table } from "react-bootstrap"; import Pagination from "../pagination/Pagination"; import { Time } from "@/helpers/time"; @@ -42,19 +42,19 @@ export default function PrenodesStatus() { const [prenodes, setPrenodes] = useState([]); const [totalResults, setTotalResults] = useState(0); - function fetchResults() { - const url = `https://api.6529.io/oracle/prenodes?page=${page}&page_size=${PAGE_SIZE}`; - fetch(url).then((response) => { - response.json().then((response: { data: Prenode[]; count: number }) => { - setPrenodes(response.data); - setTotalResults(response.count); - }); - }); - } + const fetchResults = useEffectEvent(async (pageToLoad: number) => { + const response = await fetch( + `https://api.6529.io/oracle/prenodes?page=${pageToLoad}&page_size=${PAGE_SIZE}` + ); + const { data, count }: { data: Prenode[]; count: number } = + await response.json(); + setPrenodes(data); + setTotalResults(count); + }); useEffect(() => { - fetchResults(); - }, [page]); + fetchResults(page); + }, [fetchResults, page]); function printLocation(prenode: Prenode) { let location = ""; diff --git a/components/profile-activity/ProfileActivityLogs.tsx b/components/profile-activity/ProfileActivityLogs.tsx index 5fe5ab0aa1..295a0e4b91 100644 --- a/components/profile-activity/ProfileActivityLogs.tsx +++ b/components/profile-activity/ProfileActivityLogs.tsx @@ -142,12 +142,15 @@ export default function ProfileActivityLogs({ } ); + const isEmptyFirstPage = + logs?.page === 1 && (logs?.data?.length ?? 0) === 0; + useEffect(() => { - if (isLoading) return; - if (logs?.page === 1 && !logs.data.length) { - setCurrentPage(1); + if (isLoading || !isEmptyFirstPage) { + return; } - }, [logs?.page, isLoading]); + setCurrentPage(1); + }, [isEmptyFirstPage, isLoading]); return (
diff --git a/components/profile-activity/filter/ProfileActivityLogsFilterListItem.tsx b/components/profile-activity/filter/ProfileActivityLogsFilterListItem.tsx index 4d446d1857..e3dc0d3f52 100644 --- a/components/profile-activity/filter/ProfileActivityLogsFilterListItem.tsx +++ b/components/profile-activity/filter/ProfileActivityLogsFilterListItem.tsx @@ -2,7 +2,6 @@ import { PROFILE_ACTIVITY_TYPE_TO_TEXT } from "@/entities/IProfile"; import { ProfileActivityLogType } from "@/enums"; -import { useEffect, useState } from "react"; import ProfileActivityLogsIcon from "../icons/ProfileActivityLogsIcon"; export default function ProfileActivityLogsFilterListItem({ @@ -16,13 +15,7 @@ export default function ProfileActivityLogsFilterListItem({ readonly setSelected: (selected: ProfileActivityLogType) => void; readonly user: string | null; }) { - const [isSelected, setIsSelected] = useState( - selectedItems.includes(itemType) - ); - - useEffect(() => { - setIsSelected(selectedItems.includes(itemType)); - }, [selectedItems]); + const isSelected = selectedItems.includes(itemType); return (
  • diff --git a/components/rememes/RememeAddPage.tsx b/components/rememes/RememeAddPage.tsx index a86115245b..940b787697 100644 --- a/components/rememes/RememeAddPage.tsx +++ b/components/rememes/RememeAddPage.tsx @@ -108,9 +108,10 @@ export default function RememeAddPage() { useEffect(() => { if (signMessage.isError) { - setSignErrors([`Error: ${signMessage.error?.message.split(".")[0]}`]); + const messageStart = signMessage.error?.message?.split(".")[0]; + setSignErrors([`Error: ${messageStart}`]); } - }, [signMessage.isError]); + }, [signMessage.error?.message, signMessage.isError]); useEffect(() => { fetchUrl(`${publicEnv.API_ENDPOINT}/api/memes_lite`).then( diff --git a/components/rememes/Rememes.tsx b/components/rememes/Rememes.tsx index 4760a0536c..540a749cbe 100644 --- a/components/rememes/Rememes.tsx +++ b/components/rememes/Rememes.tsx @@ -21,7 +21,7 @@ import { faPlusCircle, faRefresh } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import Image from "next/image"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useEffect, useEffectEvent, useState } from "react"; import { Button, Col, Container, Dropdown, Row } from "react-bootstrap"; import { Tooltip } from "react-tooltip"; import styles from "./Rememes.module.scss"; @@ -68,6 +68,20 @@ export default function Rememes() { RememeSort.RANDOM ); + const syncSelectedMemeToQuery = useEffectEvent( + (nextSelectedMeme: number) => { + const currentId = searchParams?.get("meme_id") + ? parseInt(searchParams.get("meme_id")!) + : 0; + if (!currentId || currentId !== nextSelectedMeme) { + const querySuffix = nextSelectedMeme + ? `?meme_id=${nextSelectedMeme}` + : ""; + router.push(`${pathname}${querySuffix}`); + } + } + ); + useEffect(() => { fetchUrl(`${publicEnv.API_ENDPOINT}/api/memes_lite`) .then((response: DBResponse) => { @@ -81,7 +95,7 @@ export default function Rememes() { }); }, []); - function fetchResults(mypage: number) { + const fetchResults = useEffectEvent((mypage: number) => { setRememesLoaded(false); let memeFilter = ""; if (selectedMeme) { @@ -107,18 +121,10 @@ export default function Rememes() { .finally(() => { setRememesLoaded(true); }); - } + }); useEffect(() => { - const currentId = searchParams?.get("meme_id") - ? parseInt(searchParams.get("meme_id")!) - : 0; - if (!currentId || currentId != selectedMeme) { - const newPath = `${pathname}${ - selectedMeme ? `?meme_id=${selectedMeme}` : "" - }`; - router.push(newPath); - } + syncSelectedMemeToQuery(selectedMeme); }, [selectedMeme]); useEffect(() => { diff --git a/components/subscriptions-report/SubscriptionsReport.tsx b/components/subscriptions-report/SubscriptionsReport.tsx index ccd00d812c..dff95c74b2 100644 --- a/components/subscriptions-report/SubscriptionsReport.tsx +++ b/components/subscriptions-report/SubscriptionsReport.tsx @@ -38,6 +38,7 @@ export default function SubscriptionsReportComponent() { const { connectedProfile } = useAuth(); const pastDropsTarget = useRef(null); const upcomingToggleRef = useRef(null); + const hasLoadedInitialRedeemedPage = useRef(false); const [upcomingLoading, setUpcomingLoading] = useState(true); const [upcomingCounts, setUpcomingCounts] = useState( @@ -115,6 +116,7 @@ export default function SubscriptionsReportComponent() { setTotalRedeemed(redeemed.count); setUpcomingCounts(upcoming); } finally { + hasLoadedInitialRedeemedPage.current = true; setRedeemedLoading(false); setUpcomingLoading(false); } @@ -123,18 +125,26 @@ export default function SubscriptionsReportComponent() { }, []); useEffect(() => { - if (redeemedLoading) return; + if (!hasLoadedInitialRedeemedPage.current) return; + let isCancelled = false; const fetchData = async () => { setRedeemedLoading(true); try { const redeemed = await fetchRedeemedCounts(redeemedPage); - setRedeemedCounts(redeemed.data); - setTotalRedeemed(redeemed.count); + if (!isCancelled) { + setRedeemedCounts(redeemed.data); + setTotalRedeemed(redeemed.count); + } } finally { - setRedeemedLoading(false); + if (!isCancelled) { + setRedeemedLoading(false); + } } }; fetchData(); + return () => { + isCancelled = true; + }; }, [redeemedPage]); function renderEmptyState(loading: boolean, type: string) { diff --git a/components/the-memes/MemePage.tsx b/components/the-memes/MemePage.tsx index 06285d17ae..7089fdff75 100644 --- a/components/the-memes/MemePage.tsx +++ b/components/the-memes/MemePage.tsx @@ -5,7 +5,7 @@ import styles from "./TheMemes.module.scss"; import { MEMES_CONTRACT } from "@/constants"; import { DBResponse } from "@/entities/IDBResponse"; import dynamic from "next/dynamic"; -import { useContext, useEffect, useMemo, useState } from "react"; +import { useContext, useEffect, useEffectEvent, useMemo, useState } from "react"; import { Col, Container, Row } from "react-bootstrap"; import { AuthContext } from "@/components/auth/Auth"; @@ -59,6 +59,7 @@ export default function MemePage({ nftId }: { readonly nftId: string }) { const pathname = usePathname(); const { setTitle } = useTitle(); const { connectedProfile } = useContext(AuthContext); + const consolidationKey = connectedProfile?.consolidation_key; const [connectedWallets, setConnectedWallets] = useState([]); const focusParam = searchParams?.get("focus"); @@ -191,19 +192,27 @@ export default function MemePage({ nftId }: { readonly nftId: string }) { }; }, [nftId]); - function updateNftBalances(data: Transaction[]) { + const updateNftBalances = useEffectEvent((data: Transaction[]) => { let countIn = 0; let countOut = 0; - data.map((d: Transaction) => { - if (connectedWallets.some((w) => areEqualAddresses(w, d.from_address))) { - countOut += d.token_count; + data.forEach((transaction) => { + if ( + connectedWallets.some((wallet) => + areEqualAddresses(wallet, transaction.from_address) + ) + ) { + countOut += transaction.token_count; } - if (connectedWallets.some((w) => areEqualAddresses(w, d.to_address))) { - countIn += d.token_count; + if ( + connectedWallets.some((wallet) => + areEqualAddresses(wallet, transaction.to_address) + ) + ) { + countIn += transaction.token_count; } }); setNftBalance(countIn - countOut); - } + }); useEffect(() => { if (connectedWallets.length && nftId) { @@ -228,14 +237,14 @@ export default function MemePage({ nftId }: { readonly nftId: string }) { useEffect(() => { if (connectedWallets.length > 0 && nftId) { commonApiFetch({ - endpoint: `tdh/consolidation/${connectedProfile?.consolidation_key}`, + endpoint: `tdh/consolidation/${consolidationKey}`, }).then((response) => { setMyOwner(response); setMyTDH(response.memes.find((m) => m.id === parseInt(nftId))); setMyRank(response.memes_ranks.find((m) => m.id === parseInt(nftId))); }); } - }, [nftId, connectedWallets]); + }, [nftId, connectedWallets, consolidationKey]); function printContent() { return ( diff --git a/components/the-memes/MemePageLive.tsx b/components/the-memes/MemePageLive.tsx index 8b2a393f27..e6ffdb2b0b 100644 --- a/components/the-memes/MemePageLive.tsx +++ b/components/the-memes/MemePageLive.tsx @@ -23,7 +23,7 @@ import { faFire, faRefresh } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import Image from "next/image"; import Link from "next/link"; -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { Col, Container, Dropdown, Row, Table } from "react-bootstrap"; import { Tooltip } from "react-tooltip"; import ArtistProfileHandle from "./ArtistProfileHandle"; @@ -294,26 +294,26 @@ export function MemePageLiveSubMenu(props: { } }, [props.nft]); - useEffect(() => { - if (props.nft) { - fetchRememes(props.nft.id); - } - }, [props.nft, rememesPage, selectedRememeSorting]); - - function fetchRememes(meme_id: number) { + const fetchRememes = useCallback((memeId: number) => { let sort = ""; if (selectedRememeSorting === RememeSort.CREATED_ASC) { sort = "&sort=created_at&sort_direction=desc"; } fetchUrl( - `${publicEnv.API_ENDPOINT}/api/rememes?meme_id=${meme_id}&page_size=${REMEMES_PAGE_SIZE}&page=${rememesPage}${sort}` + `${publicEnv.API_ENDPOINT}/api/rememes?meme_id=${memeId}&page_size=${REMEMES_PAGE_SIZE}&page=${rememesPage}${sort}` ).then((response: DBResponse) => { setRememesTotalResults(response.count); setRememes(response.data); setShowRememesSort(response.count > REMEMES_PAGE_SIZE); setRememesLoaded(true); }); - } + }, [rememesPage, selectedRememeSorting]); + + useEffect(() => { + if (props.nft) { + fetchRememes(props.nft.id); + } + }, [props.nft, fetchRememes]); if (props.show) { return ( diff --git a/components/user/collected/UserPageCollected.tsx b/components/user/collected/UserPageCollected.tsx index 4a127872a2..7669036c8a 100644 --- a/components/user/collected/UserPageCollected.tsx +++ b/components/user/collected/UserPageCollected.tsx @@ -436,16 +436,9 @@ export default function UserPageCollected({ setTotalPages(pagesCount); }, [data?.count, data?.page, isFetching]); - const getShowDataRow = (): boolean => - filters.collection - ? COLLECTED_COLLECTIONS_META[filters.collection].showCardDataRow - : true; - - const [showDataRow, setShowDataRow] = useState(getShowDataRow()); - - useEffect(() => { - setShowDataRow(getShowDataRow()); - }, [filters.collection]); + const showDataRow = filters.collection + ? COLLECTED_COLLECTIONS_META[filters.collection].showCardDataRow + : true; const scrollContainer = useRef(null); diff --git a/components/user/collected/cards/UserPageCollectedCardsNoCards.tsx b/components/user/collected/cards/UserPageCollectedCardsNoCards.tsx index bb850f01df..55c62a83a7 100644 --- a/components/user/collected/cards/UserPageCollectedCardsNoCards.tsx +++ b/components/user/collected/cards/UserPageCollectedCardsNoCards.tsx @@ -1,6 +1,5 @@ "use client"; -import { useEffect, useState } from "react"; import { CollectedCollectionType, CollectionSeized, @@ -9,57 +8,53 @@ import { MEMES_SEASON } from "@/enums"; import { assertUnreachable } from "@/helpers/AllowlistToolHelpers"; import { ProfileCollectedFilters } from "../UserPageCollected"; +const getMsg = (filters: ProfileCollectedFilters): string => { + if (filters.seized !== CollectionSeized.NOT_SEIZED) { + return "No cards to display"; + } + switch (filters.collection) { + case null: + return "Congratulations, full setter!"; + case CollectedCollectionType.MEMES: + switch (filters.szn) { + case null: + return "Congratulations, The Memes full setter!"; + case MEMES_SEASON.SZN1: + case MEMES_SEASON.SZN2: + case MEMES_SEASON.SZN3: + case MEMES_SEASON.SZN4: + case MEMES_SEASON.SZN5: + case MEMES_SEASON.SZN6: + case MEMES_SEASON.SZN7: + case MEMES_SEASON.SZN8: + case MEMES_SEASON.SZN9: + case MEMES_SEASON.SZN10: + case MEMES_SEASON.SZN11: + case MEMES_SEASON.SZN12: + case MEMES_SEASON.SZN13: + return `Congratulations, ${filters.szn} full setter!`; + default: + assertUnreachable(filters.szn); + return ""; + } + case CollectedCollectionType.GRADIENTS: + return "Congratulations, Gradient full setter!"; + case CollectedCollectionType.MEMELAB: + return "Congratulations, Meme Lab full setter!"; + case CollectedCollectionType.NEXTGEN: + return "Congratulations, Next Gen full setter!"; + default: + assertUnreachable(filters.collection); + return ""; + } +}; + export default function UserPageCollectedCardsNoCards({ filters, }: { readonly filters: ProfileCollectedFilters; }) { - const getMsg = (): string => { - if (filters.seized !== CollectionSeized.NOT_SEIZED) { - return "No cards to display"; - } - switch (filters.collection) { - case null: - return "Congratulations, full setter!"; - case CollectedCollectionType.MEMES: - switch (filters.szn) { - case null: - return "Congratulations, The Memes full setter!"; - case MEMES_SEASON.SZN1: - case MEMES_SEASON.SZN2: - case MEMES_SEASON.SZN3: - case MEMES_SEASON.SZN4: - case MEMES_SEASON.SZN5: - case MEMES_SEASON.SZN6: - case MEMES_SEASON.SZN7: - case MEMES_SEASON.SZN8: - case MEMES_SEASON.SZN9: - case MEMES_SEASON.SZN10: - case MEMES_SEASON.SZN11: - case MEMES_SEASON.SZN12: - case MEMES_SEASON.SZN13: - return `Congratulations, ${filters.szn} full setter!`; - default: - assertUnreachable(filters.szn); - return ""; - } - case CollectedCollectionType.GRADIENTS: - return "Congratulations, Gradient full setter!"; - case CollectedCollectionType.MEMELAB: - return "Congratulations, Meme Lab full setter!"; - case CollectedCollectionType.NEXTGEN: - return "Congratulations, Next Gen full setter!"; - default: - assertUnreachable(filters.collection); - return ""; - } - }; - - const [msg, setMsg] = useState(getMsg()); - - useEffect(() => { - setMsg(getMsg()); - }, [filters]); + const msg = getMsg(filters); return (
    {msg}
    diff --git a/components/user/collected/filters/UserPageCollectedFiltersSortBy.tsx b/components/user/collected/filters/UserPageCollectedFiltersSortBy.tsx index 7175c99c09..84a05ff9ac 100644 --- a/components/user/collected/filters/UserPageCollectedFiltersSortBy.tsx +++ b/components/user/collected/filters/UserPageCollectedFiltersSortBy.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useMemo } from "react"; import { CollectedCollectionType, CollectionSort, @@ -28,29 +28,20 @@ export default function UserPageCollectedFiltersSortBy({ [CollectionSort.RANK]: "Rank", }; - const getItems = () => { - const items: CommonSelectItem[] = Object.values( - CollectionSort - ).map((sort) => ({ + const items = useMemo[]>(() => { + const selectItems = Object.values(CollectionSort).map((sort) => ({ label: labels[sort], value: sort, key: sort, })); - return items.filter((item) => + return selectItems.filter((item) => collection ? COLLECTED_COLLECTIONS_META[collection].filters.sort.includes( item.value ) : true ); - }; - const [items, setItems] = useState[]>( - getItems() - ); - - useEffect(() => { - setItems(getItems()); }, [collection]); return ( diff --git a/components/user/identity/header/cic-rate/UserPageIdentityHeaderCICRate.tsx b/components/user/identity/header/cic-rate/UserPageIdentityHeaderCICRate.tsx index 19c9677dee..154c921745 100644 --- a/components/user/identity/header/cic-rate/UserPageIdentityHeaderCICRate.tsx +++ b/components/user/identity/header/cic-rate/UserPageIdentityHeaderCICRate.tsx @@ -1,6 +1,6 @@ "use client"; -import { FormEvent, useContext, useEffect, useState } from "react"; +import { FormEvent, useContext, useEffect, useMemo, useState } from "react"; import { ApiProfileRaterCicState } from "@/entities/IProfile"; import { getStringAsNumberOrZero } from "@/helpers/Helpers"; import { AuthContext } from "@/components/auth/Auth"; @@ -86,7 +86,7 @@ export default function UserPageIdentityHeaderCICRate({ }, }); - const getProxyAvailableCredit = (): number | null => { + const proxyAvailableCredit = useMemo(() => { const repProxy = activeProfileProxy?.actions.find( (a) => a.action_type === ApiProfileProxyActionType.AllocateCic ); @@ -97,62 +97,29 @@ export default function UserPageIdentityHeaderCICRate({ (repProxy.credit_amount ?? 0) - (repProxy.credit_spent ?? 0), 0 ); - }; - - const [proxyAvailableCredit, setProxyAvailableCredit] = useState< - number | null - >(getProxyAvailableCredit()); + }, [activeProfileProxy]); - useEffect( - () => setProxyAvailableCredit(getProxyAvailableCredit()), - [activeProfileProxy] - ); - - const getMinValue = (): number => { + const minMaxValues = useMemo(() => { const currentCic = currentCICState?.cic_rating_by_rater ?? 0; const heroAvailableCic = currentCICState?.cic_ratings_left_to_give_by_rater ?? 0; const minHeroCic = 0 - (Math.abs(currentCic) + heroAvailableCic); - if (typeof proxyAvailableCredit !== "number") { - return minHeroCic; - } - const minProxyRep = currentCic - proxyAvailableCredit; - - return Math.abs(minHeroCic) < Math.abs(minProxyRep) - ? minHeroCic - : minProxyRep; - }; - - const getMaxValue = (): number => { - const currentCic = currentCICState?.cic_rating_by_rater ?? 0; - const heroAvailableCic = - currentCICState?.cic_ratings_left_to_give_by_rater ?? 0; const maxHeroCic = Math.abs(currentCic) + heroAvailableCic; + if (typeof proxyAvailableCredit !== "number") { - return maxHeroCic; + return { min: minHeroCic, max: maxHeroCic } as const; } - const maxProxyRep = currentCic + proxyAvailableCredit; - return Math.min(maxHeroCic, maxProxyRep); - }; - - const getMinMaxValues = (): { - readonly min: number; - readonly max: number; - } => ({ - min: getMinValue(), - max: getMaxValue(), - }); - - const [minMaxValues, setMinMaxValues] = useState<{ - readonly min: number; - readonly max: number; - }>(getMinMaxValues()); + const minProxyRep = currentCic - proxyAvailableCredit; + const maxProxyRep = currentCic + proxyAvailableCredit; - useEffect( - () => setMinMaxValues(getMinMaxValues()), - [currentCICState, proxyAvailableCredit] - ); + return { + min: Math.abs(minHeroCic) < Math.abs(minProxyRep) + ? minHeroCic + : minProxyRep, + max: Math.min(maxHeroCic, maxProxyRep), + } as const; + }, [currentCICState, proxyAvailableCredit]); const [originalRating, setOriginalRating] = useState( currentCICState?.cic_rating_by_rater ?? 0 @@ -184,7 +151,7 @@ export default function UserPageIdentityHeaderCICRate({ }; const adjustStrValueToMinMax = (): void => { - const { min, max } = getMinMaxValues(); + const { min, max } = minMaxValues; const valueAsNumber = getStringAsNumberOrZero(adjustedRatingStr); if (valueAsNumber > max) { setAdjustedRatingStr(`${max}`); @@ -196,7 +163,7 @@ export default function UserPageIdentityHeaderCICRate({ } }; - const getIsValidValue = (): boolean => { + const isValidValue = useMemo(() => { const { min, max } = minMaxValues; const valueAsNumber = getStringAsNumberOrZero(adjustedRatingStr); if (valueAsNumber > max) { @@ -207,47 +174,19 @@ export default function UserPageIdentityHeaderCICRate({ return false; } return true; - }; - - const [isValidValue, setIsValidValue] = useState(getIsValidValue()); + }, [adjustedRatingStr, minMaxValues]); - useEffect(() => setIsValidValue(getIsValidValue()), [adjustedRatingStr]); - - const [newRating, setNewRating] = useState( - getStringAsNumberOrZero(adjustedRatingStr) + const newRating = useMemo( + () => getStringAsNumberOrZero(adjustedRatingStr), + [adjustedRatingStr] ); - useEffect(() => { - setNewRating(getStringAsNumberOrZero(adjustedRatingStr)); - }, [adjustedRatingStr]); - - const [haveChanged, setHaveChanged] = useState( - newRating !== originalRating + const haveChanged = useMemo( + () => newRating !== originalRating, + [newRating, originalRating] ); - useEffect(() => { - setHaveChanged(newRating !== originalRating); - }, [newRating, originalRating]); - - const getIsSaveDisabled = (): boolean => { - if (!haveChanged) { - return true; - } - - if (!isValidValue) { - return true; - } - - return false; - }; - - const [isSaveDisabled, setIsSaveDisabled] = useState( - getIsSaveDisabled() - ); - - useEffect(() => { - setIsSaveDisabled(getIsSaveDisabled()); - }, [haveChanged, isValidValue]); + const isSaveDisabled = !haveChanged || !isValidValue; const onSave = async () => { const { success } = await requestAuth(); diff --git a/components/user/identity/header/cic-rate/UserPageIdentityHeaderCICRateStats.tsx b/components/user/identity/header/cic-rate/UserPageIdentityHeaderCICRateStats.tsx index 1cff6d2ca3..a7bc832309 100644 --- a/components/user/identity/header/cic-rate/UserPageIdentityHeaderCICRateStats.tsx +++ b/components/user/identity/header/cic-rate/UserPageIdentityHeaderCICRateStats.tsx @@ -1,6 +1,6 @@ "use client"; -import { useContext, useEffect, useState } from "react"; +import { useContext } from "react"; import { AuthContext } from "@/components/auth/Auth"; import Link from "next/link"; import { formatNumberWithCommas } from "@/helpers/Helpers"; @@ -31,14 +31,7 @@ export default function UserPageIdentityHeaderCICRateStats({ } return Math.max(0, (proxy.credit_amount ?? 0) - (proxy.credit_spent ?? 0)); }; - const [proxyAvailableCredit, setProxyAvailableCredit] = useState< - number | null - >(getProxyAvailableCredit()); - - useEffect( - () => setProxyAvailableCredit(getProxyAvailableCredit()), - [activeProfileProxy] - ); + const proxyAvailableCredit = getProxyAvailableCredit(); const getAvailableCredit = (): number => { if (!activeProfileProxy) { @@ -49,11 +42,7 @@ export default function UserPageIdentityHeaderCICRateStats({ : proxyAvailableCredit ?? 0; }; - const [availableCredit, setAvailableCredit] = useState(getAvailableCredit()); - useEffect( - () => setAvailableCredit(getAvailableCredit()), - [heroAvailableCredit, proxyAvailableCredit] - ); + const availableCredit = getAvailableCredit(); return (
    isMyProfile && !activeProfileProxy; - const [canEdit, setCanEdit] = useState(getCanEdit()); - useEffect(() => setCanEdit(getCanEdit()), [isMyProfile, activeProfileProxy]); + const canEdit = isMyProfile && !activeProfileProxy; const getPrimaryAddress = (p: ApiIdentity) => { if (p.primary_wallet) { @@ -49,13 +47,7 @@ export default function UserPageIdentityStatementsConsolidatedAddresses({ return highestTdhWallet?.wallet.toLowerCase() ?? null; }; - const [primaryAddress, setPrimaryAddress] = useState( - getPrimaryAddress(profile) - ); - - useEffect(() => { - setPrimaryAddress(getPrimaryAddress(profile)); - }, [profile]); + const primaryAddress = useMemo(() => getPrimaryAddress(profile), [profile]); const sortByPrimary = (wallets: ApiWallet[]) => { const sorted = [...wallets]; @@ -74,14 +66,11 @@ export default function UserPageIdentityStatementsConsolidatedAddresses({ return sorted; }; - const [sortedByPrimary, setSortedByPrimary] = useState( - sortByPrimary(profile.wallets ?? []) + const sortedByPrimary = useMemo( + () => sortByPrimary(profile.wallets ?? []), + [profile.wallets, primaryAddress] ); - useEffect(() => { - setSortedByPrimary(sortByPrimary(profile.wallets ?? [])); - }, [profile, primaryAddress]); - const walletConsolidations = useQueries({ queries: (profile.wallets ?? []).map((wallet) => ({ queryKey: [ diff --git a/components/user/identity/statements/header/UserPageIdentityAddStatementsHeader.tsx b/components/user/identity/statements/header/UserPageIdentityAddStatementsHeader.tsx index 3ae25c8798..37b80dcd9f 100644 --- a/components/user/identity/statements/header/UserPageIdentityAddStatementsHeader.tsx +++ b/components/user/identity/statements/header/UserPageIdentityAddStatementsHeader.tsx @@ -16,10 +16,7 @@ export default function UserPageIdentityAddStatementsHeader({ const { activeProfileProxy } = useContext(AuthContext); const [isMyProfile, setIsMyProfile] = useState(false); - const getCanEdit = (): boolean => - isMyProfile && !activeProfileProxy && !!profile?.handle; - const [canEdit, setCanEdit] = useState(getCanEdit()); - useEffect(() => setCanEdit(getCanEdit()), [isMyProfile, activeProfileProxy]); + const canEdit = isMyProfile && !activeProfileProxy && !!profile?.handle; useEffect(() => { if (!account.address) { diff --git a/components/user/identity/statements/utils/UserPageIdentityAddStatementsTypeButton.tsx b/components/user/identity/statements/utils/UserPageIdentityAddStatementsTypeButton.tsx index aca5c09a22..382f0c025e 100644 --- a/components/user/identity/statements/utils/UserPageIdentityAddStatementsTypeButton.tsx +++ b/components/user/identity/statements/utils/UserPageIdentityAddStatementsTypeButton.tsx @@ -1,6 +1,5 @@ "use client"; -import { useEffect, useState } from "react"; import { STATEMENT_META, STATEMENT_TYPE } from "@/helpers/Types"; import SocialStatementIcon from "@/components/user/utils/icons/SocialStatementIcon"; @@ -17,24 +16,15 @@ export default function UserPageIdentityAddStatementsTypeButton({ readonly isLast: boolean; readonly onClick: () => void; }) { - const getActivityClass = () => - isActive ? "tw-bg-iron-800" : "tw-bg-transparent"; - - const getPositionClass = () => { - if (isFirst) { - return "tw-rounded-l-md"; - } else if (isLast) { - return "tw-rounded-r-md"; - } - return ""; - }; - - const getDynamicClasses = () => `${getActivityClass()} ${getPositionClass()}`; - const [dynamicClasses, setDynamicClasses] = useState(getDynamicClasses()); - - useEffect(() => { - setDynamicClasses(getDynamicClasses()); - }, [isActive, isFirst, isLast]); + const activityClass = isActive ? "tw-bg-iron-800" : "tw-bg-transparent"; + const positionClass = isFirst + ? "tw-rounded-l-md" + : isLast + ? "tw-rounded-r-md" + : ""; + const dynamicClasses = [activityClass, positionClass] + .filter(Boolean) + .join(" "); return ( + + + + + +
    +
    + + +
    +
    + + + + + +
    +
    No entries loaded yet.
    +
    + + + + + + + + + + + + + + +
    FileActions
    MessageRuleSeverity
    +
    +
    + +
    +

    Add manual entry

    + + + + + + + +
    + +
    + +
    + + + + + + + + +