From 80620c10ed04e1bea4bdd1ed6a5d495c6eb953fa Mon Sep 17 00:00:00 2001 From: Simo Date: Wed, 1 Oct 2025 17:11:46 +0300 Subject: [PATCH 001/256] wip Signed-off-by: Simo --- .../ProfileActivityLogItemWrapper.test.tsx | 4 +- .../user/layout/UserPageTab.test.tsx | 7 +- .../user/layout/UserPageTabs.test.tsx | 17 +- .../user/utils/CommonProfileLink.test.tsx | 6 +- app/[user]/collected/page.tsx | 10 +- app/[user]/followers/page.tsx | 10 +- app/[user]/groups/page.tsx | 10 +- app/[user]/identity/page.tsx | 10 +- app/[user]/page.tsx | 10 +- app/[user]/proxy/page.tsx | 10 +- app/[user]/rep/page.tsx | 10 +- app/[user]/stats/page.tsx | 10 +- app/[user]/subscriptions/page.tsx | 10 +- app/[user]/waves/page.tsx | 10 +- .../header-search/HeaderSearchModal.tsx | 4 +- .../header-search/HeaderSearchModalItem.tsx | 4 +- .../list/items/ProfileActivityLogProxy.tsx | 4 +- .../items/ProfileActivityLogProxyAction.tsx | 4 +- .../ProfileActivityLogProxyActionChange.tsx | 4 +- .../ProfileActivityLogProxyActionState.tsx | 4 +- .../list/items/ProfileActivityLogRate.tsx | 6 +- .../utils/ProfileActivityLogItemWrapper.tsx | 6 +- components/user/layout/UserPageTab.tsx | 31 ++-- components/user/layout/UserPageTabs.tsx | 150 +++++++----------- components/user/layout/userTabs.config.ts | 113 +++++++++++++ ...atsActivityWalletTableRowSecondAddress.tsx | 4 +- components/user/utils/CommonProfileLink.tsx | 2 +- helpers/Helpers.ts | 9 +- 28 files changed, 309 insertions(+), 170 deletions(-) create mode 100644 components/user/layout/userTabs.config.ts diff --git a/__tests__/components/profile-activity/list/items/utils/ProfileActivityLogItemWrapper.test.tsx b/__tests__/components/profile-activity/list/items/utils/ProfileActivityLogItemWrapper.test.tsx index fdd602e1ff..59a99a6e73 100644 --- a/__tests__/components/profile-activity/list/items/utils/ProfileActivityLogItemWrapper.test.tsx +++ b/__tests__/components/profile-activity/list/items/utils/ProfileActivityLogItemWrapper.test.tsx @@ -1,5 +1,5 @@ import ProfileActivityLogItemWrapper from "@/components/profile-activity/list/items/utils/ProfileActivityLogItemWrapper"; -import { UserPageTabType } from "@/components/user/layout/UserPageTabs"; +import { USER_PAGE_TAB_IDS } from "@/components/user/layout/userTabs.config"; import { ProfileActivityLogType, RateMatter } from "@/enums"; import { render } from "@testing-library/react"; @@ -36,7 +36,7 @@ describe("ProfileActivityLogItemWrapper", () => { expect((CommonProfileLink as jest.Mock).mock.calls[0][0]).toMatchObject({ handleOrWallet: "alice", isCurrentUser: true, - tabTarget: UserPageTabType.REP, + tabTarget: USER_PAGE_TAB_IDS.REP, }); }); diff --git a/__tests__/components/user/layout/UserPageTab.test.tsx b/__tests__/components/user/layout/UserPageTab.test.tsx index 8d9d98a9c6..8a42816a96 100644 --- a/__tests__/components/user/layout/UserPageTab.test.tsx +++ b/__tests__/components/user/layout/UserPageTab.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import UserPageTab from '../../../../components/user/layout/UserPageTab'; -import { UserPageTabType } from '../../../../components/user/layout/UserPageTabs'; +import { USER_PAGE_TAB_IDS, USER_PAGE_TAB_MAP } from '../../../../components/user/layout/userTabs.config'; import { useParams, useSearchParams } from 'next/navigation'; jest.mock('next/navigation', () => ({ @@ -21,11 +21,12 @@ describe('UserPageTab', () => { it('renders active and inactive states', () => { (useParams as jest.Mock).mockReturnValue({ user: 'bob' }); (useSearchParams as jest.Mock).mockReturnValue({ get: (k: string) => (k === 'address' ? '0x1' : null) }); - const { rerender } = render(); + const repTab = USER_PAGE_TAB_MAP[USER_PAGE_TAB_IDS.REP]; + const { rerender } = render(); const link = screen.getByTestId('link'); expect(link).toHaveAttribute('href', '/bob/rep?address=0x1'); expect(link).toHaveClass('tw-pointer-events-none'); - rerender(); + rerender(); expect(link).not.toHaveClass('tw-pointer-events-none'); }); }); diff --git a/__tests__/components/user/layout/UserPageTabs.test.tsx b/__tests__/components/user/layout/UserPageTabs.test.tsx index 3cf79321f0..1053d920b2 100644 --- a/__tests__/components/user/layout/UserPageTabs.test.tsx +++ b/__tests__/components/user/layout/UserPageTabs.test.tsx @@ -1,7 +1,6 @@ import { AuthContext } from "@/components/auth/Auth"; -import UserPageTabs, { - UserPageTabType, -} from "@/components/user/layout/UserPageTabs"; +import UserPageTabs from "@/components/user/layout/UserPageTabs"; +import { USER_PAGE_TAB_IDS } from "@/components/user/layout/userTabs.config"; import { render, screen } from "@testing-library/react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; @@ -17,7 +16,7 @@ jest.mock("@/hooks/useCapacitor", () => ({ })); jest.mock("@/components/user/layout/UserPageTab", () => ({ __esModule: true, - default: (p: any) =>
{p.tab}
, + default: (p: any) =>
{p.tab.id}
, })); jest.mock("@/components/cookies/CookieConsentContext", () => ({ useCookieConsent: jest.fn(), @@ -53,15 +52,15 @@ describe("UserPageTabs", () => { it("filters tabs based on context and platform", () => { renderTabs(false, false); const tabs = screen.getAllByTestId("tab").map((t) => t.textContent); - expect(tabs).not.toContain(UserPageTabType.BRAIN); - expect(tabs).not.toContain(UserPageTabType.WAVES); - expect(tabs).toContain(UserPageTabType.SUBSCRIPTIONS); + expect(tabs).not.toContain(USER_PAGE_TAB_IDS.BRAIN); + expect(tabs).not.toContain(USER_PAGE_TAB_IDS.WAVES); + expect(tabs).toContain(USER_PAGE_TAB_IDS.SUBSCRIPTIONS); }); it("hides subscriptions tab on iOS and shows waves when enabled", () => { renderTabs(true, true, "CA"); // Non-US country to trigger subscription hiding const tabs = screen.getAllByTestId("tab").map((t) => t.textContent); - expect(tabs).not.toContain(UserPageTabType.SUBSCRIPTIONS); - expect(tabs).toContain(UserPageTabType.WAVES); + expect(tabs).not.toContain(USER_PAGE_TAB_IDS.SUBSCRIPTIONS); + expect(tabs).toContain(USER_PAGE_TAB_IDS.WAVES); }); }); diff --git a/__tests__/components/user/utils/CommonProfileLink.test.tsx b/__tests__/components/user/utils/CommonProfileLink.test.tsx index dad451c256..f835ea672d 100644 --- a/__tests__/components/user/utils/CommonProfileLink.test.tsx +++ b/__tests__/components/user/utils/CommonProfileLink.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import CommonProfileLink from '../../../../components/user/utils/CommonProfileLink'; -import { UserPageTabType } from '../../../../components/user/layout/UserPageTabs'; +import { USER_PAGE_TAB_IDS } from '../../../../components/user/layout/userTabs.config'; jest.mock('next/link', () => ({ __esModule: true, default: ({ href, children, ...rest }: any) => {children} })); jest.mock('../../../../helpers/Helpers', () => ({ getProfileTargetRoute: jest.fn(() => '/target') })); @@ -11,13 +11,13 @@ const { getProfileTargetRoute } = require('../../../../helpers/Helpers'); describe('CommonProfileLink', () => { it('disables link for current user', () => { - render(); + render(); const link = screen.getByRole('link'); expect(link).toHaveClass('tw-pointer-events-none'); }); it('computes target route', () => { - render(); + render(); expect(getProfileTargetRoute).toHaveBeenCalled(); expect(screen.getByRole('link')).toHaveAttribute('href', '/target'); }); diff --git a/app/[user]/collected/page.tsx b/app/[user]/collected/page.tsx index a30bffd4fa..f215936157 100644 --- a/app/[user]/collected/page.tsx +++ b/app/[user]/collected/page.tsx @@ -1,9 +1,15 @@ import { createUserTabPage } from "@/app/[user]/_lib/userTabPageFactory"; import UserPageCollected from "@/components/user/collected/UserPageCollected"; +import { + USER_PAGE_TAB_IDS, + USER_PAGE_TAB_MAP, +} from "@/components/user/layout/userTabs.config"; + +const TAB_CONFIG = USER_PAGE_TAB_MAP[USER_PAGE_TAB_IDS.COLLECTED]; const { Page, generateMetadata } = createUserTabPage({ - subroute: "collected", - metaLabel: "Collected", + subroute: TAB_CONFIG.route, + metaLabel: TAB_CONFIG.metaLabel, Tab: UserPageCollected, }); diff --git a/app/[user]/followers/page.tsx b/app/[user]/followers/page.tsx index 66a3686d22..f590abcd7e 100644 --- a/app/[user]/followers/page.tsx +++ b/app/[user]/followers/page.tsx @@ -1,9 +1,15 @@ import { createUserTabPage } from "@/app/[user]/_lib/userTabPageFactory"; import UserPageFollowers from "@/components/user/followers/UserPageFollowers"; +import { + USER_PAGE_TAB_IDS, + USER_PAGE_TAB_MAP, +} from "@/components/user/layout/userTabs.config"; + +const TAB_CONFIG = USER_PAGE_TAB_MAP[USER_PAGE_TAB_IDS.FOLLOWERS]; const { Page, generateMetadata } = createUserTabPage({ - subroute: "followers", - metaLabel: "Followers", + subroute: TAB_CONFIG.route, + metaLabel: TAB_CONFIG.metaLabel, Tab: UserPageFollowers, }); diff --git a/app/[user]/groups/page.tsx b/app/[user]/groups/page.tsx index 05b4d454b0..885986dc2c 100644 --- a/app/[user]/groups/page.tsx +++ b/app/[user]/groups/page.tsx @@ -1,9 +1,15 @@ import { createUserTabPage } from "@/app/[user]/_lib/userTabPageFactory"; import UserPageGroups from "@/components/user/groups/UserPageGroups"; +import { + USER_PAGE_TAB_IDS, + USER_PAGE_TAB_MAP, +} from "@/components/user/layout/userTabs.config"; + +const TAB_CONFIG = USER_PAGE_TAB_MAP[USER_PAGE_TAB_IDS.GROUPS]; const { Page, generateMetadata } = createUserTabPage({ - subroute: "groups", - metaLabel: "Groups", + subroute: TAB_CONFIG.route, + metaLabel: TAB_CONFIG.metaLabel, Tab: UserPageGroups, }); diff --git a/app/[user]/identity/page.tsx b/app/[user]/identity/page.tsx index 3bbe64256c..565aef38cc 100644 --- a/app/[user]/identity/page.tsx +++ b/app/[user]/identity/page.tsx @@ -6,6 +6,10 @@ import { RateMatter } from "@/enums"; import type { ApiIdentity } from "@/generated/models/ApiIdentity"; import { getProfileLogTypes } from "@/helpers/profile-logs.helpers"; import { getInitialRatersParams } from "@/helpers/server.helpers"; +import { + USER_PAGE_TAB_IDS, + USER_PAGE_TAB_MAP, +} from "@/components/user/layout/userTabs.config"; const MATTER_TYPE = RateMatter.NIC; @@ -56,9 +60,11 @@ function IdentityTab({ profile }: { readonly profile: ApiIdentity }) { ); } +const TAB_CONFIG = USER_PAGE_TAB_MAP[USER_PAGE_TAB_IDS.IDENTITY]; + const { Page, generateMetadata } = createUserTabPage({ - subroute: "identity", - metaLabel: "Identity", + subroute: TAB_CONFIG.route, + metaLabel: TAB_CONFIG.metaLabel, Tab: IdentityTab, }); diff --git a/app/[user]/page.tsx b/app/[user]/page.tsx index 030c7f4f6f..69903d7cb1 100644 --- a/app/[user]/page.tsx +++ b/app/[user]/page.tsx @@ -1,9 +1,15 @@ import { createUserTabPage } from "@/app/[user]/_lib/userTabPageFactory"; import UserPageBrainWrapper from "@/components/user/brain/UserPageBrainWrapper"; +import { + USER_PAGE_TAB_IDS, + USER_PAGE_TAB_MAP, +} from "@/components/user/layout/userTabs.config"; + +const TAB_CONFIG = USER_PAGE_TAB_MAP[USER_PAGE_TAB_IDS.BRAIN]; const { Page, generateMetadata } = createUserTabPage({ - subroute: "", - metaLabel: "Brain", + subroute: TAB_CONFIG.route, + metaLabel: TAB_CONFIG.metaLabel, Tab: ({ profile }) => (
diff --git a/app/[user]/proxy/page.tsx b/app/[user]/proxy/page.tsx index 5273e045c6..e1064492b2 100644 --- a/app/[user]/proxy/page.tsx +++ b/app/[user]/proxy/page.tsx @@ -1,14 +1,20 @@ import { createUserTabPage } from "@/app/[user]/_lib/userTabPageFactory"; import UserPageProxy from "@/components/user/proxy/UserPageProxy"; import type { ApiIdentity } from "@/generated/models/ApiIdentity"; +import { + USER_PAGE_TAB_IDS, + USER_PAGE_TAB_MAP, +} from "@/components/user/layout/userTabs.config"; function ProxyTab({ profile }: { readonly profile: ApiIdentity }) { return ; } +const TAB_CONFIG = USER_PAGE_TAB_MAP[USER_PAGE_TAB_IDS.PROXY]; + const { Page, generateMetadata } = createUserTabPage({ - subroute: "proxy", - metaLabel: "Proxy", + subroute: TAB_CONFIG.route, + metaLabel: TAB_CONFIG.metaLabel, Tab: ProxyTab, }); diff --git a/app/[user]/rep/page.tsx b/app/[user]/rep/page.tsx index 1c1187e4bf..d89389772a 100644 --- a/app/[user]/rep/page.tsx +++ b/app/[user]/rep/page.tsx @@ -7,6 +7,10 @@ import { RateMatter } from "@/enums"; import type { ApiIdentity } from "@/generated/models/ApiIdentity"; import { getProfileLogTypes } from "@/helpers/profile-logs.helpers"; import { getInitialRatersParams } from "@/helpers/server.helpers"; +import { + USER_PAGE_TAB_IDS, + USER_PAGE_TAB_MAP, +} from "@/components/user/layout/userTabs.config"; export interface UserPageRepPropsRepRates { readonly ratings: ApiProfileRepRatesState; @@ -61,9 +65,11 @@ function RepTab({ profile }: { readonly profile: ApiIdentity }) { ); } +const TAB_CONFIG = USER_PAGE_TAB_MAP[USER_PAGE_TAB_IDS.REP]; + const { Page, generateMetadata } = createUserTabPage({ - subroute: "rep", - metaLabel: "Rep", + subroute: TAB_CONFIG.route, + metaLabel: TAB_CONFIG.metaLabel, Tab: RepTab, }); diff --git a/app/[user]/stats/page.tsx b/app/[user]/stats/page.tsx index ffb34a522c..adf50a04b7 100644 --- a/app/[user]/stats/page.tsx +++ b/app/[user]/stats/page.tsx @@ -1,14 +1,20 @@ import { createUserTabPage } from "@/app/[user]/_lib/userTabPageFactory"; import UserPageStats from "@/components/user/stats/UserPageStats"; import type { ApiIdentity } from "@/generated/models/ApiIdentity"; +import { + USER_PAGE_TAB_IDS, + USER_PAGE_TAB_MAP, +} from "@/components/user/layout/userTabs.config"; function StatsTab({ profile }: { readonly profile: ApiIdentity }) { return ; } +const TAB_CONFIG = USER_PAGE_TAB_MAP[USER_PAGE_TAB_IDS.STATS]; + const { Page, generateMetadata } = createUserTabPage({ - subroute: "stats", - metaLabel: "Stats", + subroute: TAB_CONFIG.route, + metaLabel: TAB_CONFIG.metaLabel, Tab: StatsTab, }); diff --git a/app/[user]/subscriptions/page.tsx b/app/[user]/subscriptions/page.tsx index 8d96acf95d..4567a07256 100644 --- a/app/[user]/subscriptions/page.tsx +++ b/app/[user]/subscriptions/page.tsx @@ -1,14 +1,20 @@ import { createUserTabPage } from "@/app/[user]/_lib/userTabPageFactory"; import UserPageSubscriptions from "@/components/user/subscriptions/UserPageSubscriptions"; import type { ApiIdentity } from "@/generated/models/ApiIdentity"; +import { + USER_PAGE_TAB_IDS, + USER_PAGE_TAB_MAP, +} from "@/components/user/layout/userTabs.config"; function SubscriptionsTab({ profile }: { readonly profile: ApiIdentity }) { return ; } +const TAB_CONFIG = USER_PAGE_TAB_MAP[USER_PAGE_TAB_IDS.SUBSCRIPTIONS]; + const { Page, generateMetadata } = createUserTabPage({ - subroute: "subscriptions", - metaLabel: "Subscriptions", + subroute: TAB_CONFIG.route, + metaLabel: TAB_CONFIG.metaLabel, Tab: SubscriptionsTab, }); diff --git a/app/[user]/waves/page.tsx b/app/[user]/waves/page.tsx index b1154cf34f..97abf43677 100644 --- a/app/[user]/waves/page.tsx +++ b/app/[user]/waves/page.tsx @@ -1,14 +1,20 @@ import { createUserTabPage } from "@/app/[user]/_lib/userTabPageFactory"; import UserPageWaves from "@/components/user/waves/UserPageWaves"; import type { ApiIdentity } from "@/generated/models/ApiIdentity"; +import { + USER_PAGE_TAB_IDS, + USER_PAGE_TAB_MAP, +} from "@/components/user/layout/userTabs.config"; function WavesTab({ profile }: { readonly profile: ApiIdentity }) { return ; } +const TAB_CONFIG = USER_PAGE_TAB_MAP[USER_PAGE_TAB_IDS.WAVES]; + const { Page, generateMetadata } = createUserTabPage({ - subroute: "waves", - metaLabel: "Waves", + subroute: TAB_CONFIG.route, + metaLabel: TAB_CONFIG.metaLabel, Tab: WavesTab, }); diff --git a/components/header/header-search/HeaderSearchModal.tsx b/components/header/header-search/HeaderSearchModal.tsx index 681fcd8f49..b06e470447 100644 --- a/components/header/header-search/HeaderSearchModal.tsx +++ b/components/header/header-search/HeaderSearchModal.tsx @@ -13,7 +13,7 @@ import HeaderSearchModalItem, { import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { getRandomObjectId } from "../../../helpers/AllowlistToolHelpers"; import { getProfileTargetRoute } from "../../../helpers/Helpers"; -import { UserPageTabType } from "../../user/layout/UserPageTabs"; +import { USER_PAGE_TAB_IDS } from "../../user/layout/userTabs.config"; import { createPortal } from "react-dom"; import { QueryKey } from "../../react-query-wrapper/ReactQueryWrapper"; import type { ApiWave } from "../../../generated/models/ApiWave"; @@ -128,7 +128,7 @@ export default function HeaderSearchModal({ const path = getProfileTargetRoute({ handleOrWallet, pathname: pathname ?? "", - defaultPath: UserPageTabType.IDENTITY, + defaultPath: USER_PAGE_TAB_IDS.IDENTITY, }); router.push(path); onClose(); diff --git a/components/header/header-search/HeaderSearchModalItem.tsx b/components/header/header-search/HeaderSearchModalItem.tsx index c72aa76a35..6abdf1c4e0 100644 --- a/components/header/header-search/HeaderSearchModalItem.tsx +++ b/components/header/header-search/HeaderSearchModalItem.tsx @@ -11,7 +11,7 @@ import { useEffect, useRef } from "react"; import HeaderSearchModalItemHighlight from "./HeaderSearchModalItemHighlight"; import UserCICAndLevel from "../../user/utils/UserCICAndLevel"; import { usePathname, useSearchParams } from "next/navigation"; -import { UserPageTabType } from "../../user/layout/UserPageTabs"; +import { USER_PAGE_TAB_IDS } from "../../user/layout/userTabs.config"; import Link from "next/link"; import { GRADIENT_CONTRACT, @@ -122,7 +122,7 @@ export default function HeaderSearchModalItem({ return getProfileTargetRoute({ handleOrWallet: profile.handle ?? profile.wallet.toLowerCase(), pathname: pathname ?? "", - defaultPath: UserPageTabType.IDENTITY, + defaultPath: USER_PAGE_TAB_IDS.IDENTITY, }); } else if (isNft()) { const nft = getNft(); diff --git a/components/profile-activity/list/items/ProfileActivityLogProxy.tsx b/components/profile-activity/list/items/ProfileActivityLogProxy.tsx index 23e7571899..54a8e791b5 100644 --- a/components/profile-activity/list/items/ProfileActivityLogProxy.tsx +++ b/components/profile-activity/list/items/ProfileActivityLogProxy.tsx @@ -4,7 +4,7 @@ import { useSearchParams } from "next/navigation"; import { ProfileActivityLogProxyCreated } from "@/entities/IProfile"; import CommonProfileLink from "@/components/user/utils/CommonProfileLink"; import ProfileActivityLogItemAction from "./utils/ProfileActivityLogItemAction"; -import { UserPageTabType } from "@/components/user/layout/UserPageTabs"; +import { USER_PAGE_TAB_IDS } from "@/components/user/layout/userTabs.config"; export default function ProfileActivityLogProxy({ log, @@ -17,7 +17,7 @@ export default function ProfileActivityLogProxy({ (searchParams?.get("user") as string)?.toLowerCase() === handleOrWallet.toLowerCase(); - const tabTarget = UserPageTabType.PROXY; + const tabTarget = USER_PAGE_TAB_IDS.PROXY; return ( <> diff --git a/components/profile-activity/list/items/ProfileActivityLogProxyAction.tsx b/components/profile-activity/list/items/ProfileActivityLogProxyAction.tsx index ee909bb052..73c07bca0f 100644 --- a/components/profile-activity/list/items/ProfileActivityLogProxyAction.tsx +++ b/components/profile-activity/list/items/ProfileActivityLogProxyAction.tsx @@ -5,7 +5,7 @@ import { ProfileActivityLogProxyActionCreated } from "@/entities/IProfile"; import { PROFILE_PROXY_ACTION_LABELS } from "@/entities/IProxy"; import CommonProfileLink from "@/components/user/utils/CommonProfileLink"; import ProfileActivityLogItemAction from "./utils/ProfileActivityLogItemAction"; -import { UserPageTabType } from "@/components/user/layout/UserPageTabs"; +import { USER_PAGE_TAB_IDS } from "@/components/user/layout/userTabs.config"; export default function ProfileActivityLogProxyAction({ log, @@ -19,7 +19,7 @@ export default function ProfileActivityLogProxyAction({ (searchParams?.get("user") as string)?.toLowerCase() === handleOrWallet.toLowerCase(); - const tabTarget = UserPageTabType.PROXY; + const tabTarget = USER_PAGE_TAB_IDS.PROXY; return ( <> diff --git a/components/profile-activity/list/items/ProfileActivityLogProxyActionChange.tsx b/components/profile-activity/list/items/ProfileActivityLogProxyActionChange.tsx index bc53408ea7..cd5511f485 100644 --- a/components/profile-activity/list/items/ProfileActivityLogProxyActionChange.tsx +++ b/components/profile-activity/list/items/ProfileActivityLogProxyActionChange.tsx @@ -3,7 +3,7 @@ import { useSearchParams } from "next/navigation"; import { ProfileActivityLogProxyActionChanged } from "@/entities/IProfile"; import ProfileActivityLogItemAction from "./utils/ProfileActivityLogItemAction"; -import { UserPageTabType } from "@/components/user/layout/UserPageTabs"; +import { USER_PAGE_TAB_IDS } from "@/components/user/layout/userTabs.config"; import CommonProfileLink from "@/components/user/utils/CommonProfileLink"; import { PROFILE_PROXY_ACTION_LABELS } from "@/entities/IProxy"; import { formatNumberWithCommas } from "@/helpers/Helpers"; @@ -19,7 +19,7 @@ export default function ProfileActivityLogProxyActionChange({ (searchParams?.get("user") as string)?.toLowerCase() === handleOrWallet.toLowerCase(); - const tabTarget = UserPageTabType.PROXY; + const tabTarget = USER_PAGE_TAB_IDS.PROXY; const getChangedParamName = () => { if (log.contents.end_time !== undefined) { diff --git a/components/profile-activity/list/items/ProfileActivityLogProxyActionState.tsx b/components/profile-activity/list/items/ProfileActivityLogProxyActionState.tsx index 094cef2659..e3f46ef953 100644 --- a/components/profile-activity/list/items/ProfileActivityLogProxyActionState.tsx +++ b/components/profile-activity/list/items/ProfileActivityLogProxyActionState.tsx @@ -5,7 +5,7 @@ import { ProfileActivityLogProxyActionStateChanged } from "@/entities/IProfile"; import { PROFILE_PROXY_ACTION_LABELS } from "@/entities/IProxy"; import { AcceptActionRequestActionEnum } from "@/generated/models/AcceptActionRequest"; import ProfileActivityLogItemAction from "./utils/ProfileActivityLogItemAction"; -import { UserPageTabType } from "@/components/user/layout/UserPageTabs"; +import { USER_PAGE_TAB_IDS } from "@/components/user/layout/userTabs.config"; import CommonProfileLink from "@/components/user/utils/CommonProfileLink"; const ACTION: Record = { @@ -27,7 +27,7 @@ export default function ProfileActivityLogProxyActionState({ (searchParams?.get("user") as string)?.toLowerCase() === handleOrWallet.toLowerCase(); - const tabTarget = UserPageTabType.PROXY; + const tabTarget = USER_PAGE_TAB_IDS.PROXY; return ( <> diff --git a/components/profile-activity/list/items/ProfileActivityLogRate.tsx b/components/profile-activity/list/items/ProfileActivityLogRate.tsx index 650ca0c59d..a243eb2584 100644 --- a/components/profile-activity/list/items/ProfileActivityLogRate.tsx +++ b/components/profile-activity/list/items/ProfileActivityLogRate.tsx @@ -1,7 +1,7 @@ "use client"; import { SystemAdjustmentPill } from "@/components/common/SystemAdjustmentPill"; -import { UserPageTabType } from "@/components/user/layout/UserPageTabs"; +import { USER_PAGE_TAB_IDS } from "@/components/user/layout/userTabs.config"; import CommonProfileLink from "@/components/user/utils/CommonProfileLink"; import { ProfileActivityLogRatingEdit, @@ -81,8 +81,8 @@ export default function ProfileActivityLogRate({ const tabTarget = log.contents.rating_matter === RateMatter.REP - ? UserPageTabType.REP - : UserPageTabType.IDENTITY; + ? USER_PAGE_TAB_IDS.REP + : USER_PAGE_TAB_IDS.IDENTITY; const getProxyHandle = (): string | null => { if (!log.proxy_handle) return null; diff --git a/components/profile-activity/list/items/utils/ProfileActivityLogItemWrapper.tsx b/components/profile-activity/list/items/utils/ProfileActivityLogItemWrapper.tsx index 9082417880..76b9ddde9f 100644 --- a/components/profile-activity/list/items/utils/ProfileActivityLogItemWrapper.tsx +++ b/components/profile-activity/list/items/utils/ProfileActivityLogItemWrapper.tsx @@ -1,4 +1,4 @@ -import { UserPageTabType } from "@/components/user/layout/UserPageTabs"; +import { USER_PAGE_TAB_IDS } from "@/components/user/layout/userTabs.config"; import CommonProfileLink from "@/components/user/utils/CommonProfileLink"; import { ProfileActivityLog } from "@/entities/IProfile"; import { ProfileActivityLogType, RateMatter } from "@/enums"; @@ -30,8 +30,8 @@ export default function ProfileActivityLogItemWrapper({ const tabTarget = log.type === ProfileActivityLogType.RATING_EDIT && log.contents.rating_matter === RateMatter.REP - ? UserPageTabType.REP - : UserPageTabType.IDENTITY; + ? USER_PAGE_TAB_IDS.REP + : USER_PAGE_TAB_IDS.IDENTITY; return ( diff --git a/components/user/layout/UserPageTab.tsx b/components/user/layout/UserPageTab.tsx index ace0f8ad73..d1a9e71a81 100644 --- a/components/user/layout/UserPageTab.tsx +++ b/components/user/layout/UserPageTab.tsx @@ -1,29 +1,34 @@ "use client"; -import { useEffect, useRef, useState } from "react"; -import { USER_PAGE_TAB_META, UserPageTabType } from "./UserPageTabs"; import Link from "next/link"; import { useParams, useSearchParams } from "next/navigation"; +import { useEffect, useRef, useState } from "react"; +import type { + UserPageTabConfig, + UserPageTabKey, +} from "./userTabs.config"; + +interface UserPageTabProps { + readonly tab: UserPageTabConfig; + readonly parentRef: React.RefObject; + readonly activeTabId: UserPageTabKey; +} export default function UserPageTab({ tab, parentRef, - activeTab, -}: { - readonly tab: UserPageTabType; - readonly parentRef: React.RefObject; - readonly activeTab: UserPageTabType; -}) { + activeTabId, +}: UserPageTabProps) { const params = useParams(); const searchParams = useSearchParams(); const handleOrWallet = params?.user?.toString(); - const path = `/${handleOrWallet}/${USER_PAGE_TAB_META[tab].route}`; + const path = `/${handleOrWallet}/${tab.route}`; - const [isActive, setIsActive] = useState(tab === activeTab); + const [isActive, setIsActive] = useState(tab.id === activeTabId); useEffect(() => { - setIsActive(tab === activeTab); - }, [activeTab]); + setIsActive(tab.id === activeTabId); + }, [activeTabId, tab.id]); const activeClasses = "tw-border-primary-400 tw-border-solid tw-border-x-0 tw-border-t-0 tw-text-iron-100 tw-whitespace-nowrap tw-border-b-2 tw-font-semibold tw-py-4 tw-px-1"; @@ -72,7 +77,7 @@ export default function UserPageTab({ className={`${ isActive ? "tw-pointer-events-none" : "" } tw-no-underline tw-leading-4 tw-p-0 tw-text-base tw-font-semibold`}> -
{USER_PAGE_TAB_META[tab].title}
+
{tab.title}
); } diff --git a/components/user/layout/UserPageTabs.tsx b/components/user/layout/UserPageTabs.tsx index 0bcb9f3d11..7bbf768976 100644 --- a/components/user/layout/UserPageTabs.tsx +++ b/components/user/layout/UserPageTabs.tsx @@ -4,116 +4,75 @@ import { AuthContext } from "@/components/auth/Auth"; import { useCookieConsent } from "@/components/cookies/CookieConsentContext"; import useCapacitor from "@/hooks/useCapacitor"; import { usePathname } from "next/navigation"; -import { useContext, useEffect, useRef, useState } from "react"; +import { useContext, useEffect, useMemo, useRef, useState } from "react"; import UserPageTab from "./UserPageTab"; +import { + DEFAULT_USER_PAGE_TAB, + USER_PAGE_TABS, + type UserPageTabConfig, + type UserPageTabKey, + type UserPageVisibilityContext, + getUserPageTabByRoute, +} from "./userTabs.config"; -export enum UserPageTabType { - BRAIN = "BRAIN", - REP = "REP", - IDENTITY = "IDENTITY", - COLLECTED = "COLLECTED", - STATS = "STATS", - SUBSCRIPTIONS = "SUBSCRIPTIONS", - PROXY = "PROXY", - GROUPS = "GROUPS", - WAVES = "WAVES", - FOLLOWERS = "FOLLOWERS", -} +const DEFAULT_TAB = DEFAULT_USER_PAGE_TAB; -export const USER_PAGE_TAB_META: Record< - UserPageTabType, - { tab: UserPageTabType; title: string; route: string } -> = { - [UserPageTabType.BRAIN]: { - tab: UserPageTabType.BRAIN, - title: "Brain", - route: "", - }, - [UserPageTabType.REP]: { - tab: UserPageTabType.REP, - title: "Rep", - route: "rep", - }, +const getVisibilityContext = ({ + showWaves, + capacitorIsIos, + country, +}: { + readonly showWaves: boolean; + readonly capacitorIsIos: boolean; + readonly country: string | null; +}): UserPageVisibilityContext => ({ + showWaves, + hideSubscriptions: capacitorIsIos && country !== "US", +}); - [UserPageTabType.IDENTITY]: { - tab: UserPageTabType.IDENTITY, - title: "Identity", - route: "identity", - }, - [UserPageTabType.COLLECTED]: { - tab: UserPageTabType.COLLECTED, - title: "Collected", - route: "collected", - }, - [UserPageTabType.STATS]: { - tab: UserPageTabType.STATS, - title: "Stats", - route: "stats", - }, - [UserPageTabType.SUBSCRIPTIONS]: { - tab: UserPageTabType.SUBSCRIPTIONS, - title: "Subscriptions", - route: "subscriptions", - }, - [UserPageTabType.PROXY]: { - tab: UserPageTabType.PROXY, - title: "Proxy", - route: "proxy", - }, - [UserPageTabType.GROUPS]: { - tab: UserPageTabType.GROUPS, - title: "Groups", - route: "groups", - }, - [UserPageTabType.WAVES]: { - tab: UserPageTabType.WAVES, - title: "Waves", - route: "waves", - }, - [UserPageTabType.FOLLOWERS]: { - tab: UserPageTabType.FOLLOWERS, - title: "Followers", - route: "followers", - }, +const resolveTabFromPath = (pathname: string): UserPageTabKey => { + const segments = pathname.split("/").filter(Boolean); + const routeSegment = segments[1] ?? ""; + const match = getUserPageTabByRoute(routeSegment); + return (match?.id ?? DEFAULT_TAB) as UserPageTabKey; }; +const filterVisibleTabs = ( + tabs: readonly UserPageTabConfig[], + context: UserPageVisibilityContext +) => + tabs.filter((tab) => (tab.isVisible ? tab.isVisible(context) : true)); + export default function UserPageTabs() { const pathname = usePathname() ?? ""; const capacitor = useCapacitor(); const { country } = useCookieConsent(); const { showWaves } = useContext(AuthContext); - const pathnameToTab = (pathname: string): UserPageTabType => { - const segments = pathname.split("/").filter(Boolean); - const name = segments[1] ?? ""; - const tab = Object.values(UserPageTabType).find( - (tab) => - USER_PAGE_TAB_META[tab].route.toLowerCase() === name?.toLowerCase() - ); - return tab ?? UserPageTabType.COLLECTED; - }; - const [tab, setTab] = useState(pathnameToTab(pathname)); + const visibilityContext = useMemo( + () => + getVisibilityContext({ + showWaves, + capacitorIsIos: capacitor.isIos, + country: country ?? null, + }), + [capacitor.isIos, country, showWaves] + ); + + const [activeTab, setActiveTab] = useState( + resolveTabFromPath(pathname) + ); useEffect(() => { - setTab(pathnameToTab(pathname)); + setActiveTab(resolveTabFromPath(pathname)); }, [pathname]); const wrapperRef = useRef(null); - const getTabsToShow = () => { - let allTabs = Object.values(UserPageTabType); - if (capacitor.isIos && country !== "US") { - allTabs = allTabs.filter((tab) => tab !== UserPageTabType.SUBSCRIPTIONS); - } - if (showWaves) return allTabs; - return allTabs.filter( - (tab) => ![UserPageTabType.BRAIN, UserPageTabType.WAVES].includes(tab) - ); - }; - const [tabsToShow, setTabsToShow] = useState( - getTabsToShow() + const visibleTabs = useMemo( + () => filterVisibleTabs(USER_PAGE_TABS, visibilityContext), + [visibilityContext] ); - useEffect(() => setTabsToShow(getTabsToShow()), [showWaves]); return (
@@ -121,13 +80,14 @@ export default function UserPageTabs() { className="tw-flex tw-gap-x-3 lg:tw-gap-x-4 tw-overflow-x-auto horizontal-menu-hide-scrollbar" aria-label="Tabs">
- {tabsToShow.map((tabType) => ( + {visibleTabs.map((tabConfig) => ( ))} diff --git a/components/user/layout/userTabs.config.ts b/components/user/layout/userTabs.config.ts new file mode 100644 index 0000000000..ff04753309 --- /dev/null +++ b/components/user/layout/userTabs.config.ts @@ -0,0 +1,113 @@ +export type UserPageVisibilityContext = { + readonly showWaves: boolean; + readonly hideSubscriptions: boolean; +}; + +export type UserPageTabConfig = { + readonly id: string; + readonly title: string; + readonly route: string; + readonly metaLabel: string; + readonly isVisible?: (context: UserPageVisibilityContext) => boolean; +}; + +const TABS = [ + { + id: "brain", + title: "Brain", + route: "", + metaLabel: "Brain", + isVisible: ({ showWaves }: UserPageVisibilityContext) => showWaves, + }, + { + id: "rep", + title: "Rep", + route: "rep", + metaLabel: "Rep", + }, + { + id: "identity", + title: "Identity", + route: "identity", + metaLabel: "Identity", + }, + { + id: "collected", + title: "Collected", + route: "collected", + metaLabel: "Collected", + }, + { + id: "stats", + title: "Stats", + route: "stats", + metaLabel: "Stats", + }, + { + id: "subscriptions", + title: "Subscriptions", + route: "subscriptions", + metaLabel: "Subscriptions", + isVisible: ({ hideSubscriptions }: UserPageVisibilityContext) => + !hideSubscriptions, + }, + { + id: "proxy", + title: "Proxy", + route: "proxy", + metaLabel: "Proxy", + }, + { + id: "groups", + title: "Groups", + route: "groups", + metaLabel: "Groups", + }, + { + id: "waves", + title: "Waves", + route: "waves", + metaLabel: "Waves", + isVisible: ({ showWaves }: UserPageVisibilityContext) => showWaves, + }, + { + id: "followers", + title: "Followers", + route: "followers", + metaLabel: "Followers", + }, +] as const satisfies readonly UserPageTabConfig[]; + +export type UserPageTabKey = (typeof TABS)[number]["id"]; +export type UserPageTabType = UserPageTabKey; + +export const USER_PAGE_TABS = TABS; + +export const USER_PAGE_TAB_MAP: Record = + USER_PAGE_TABS.reduce( + (acc, tab) => { + acc[tab.id] = tab; + return acc; + }, + {} as Record + ); + +export const USER_PAGE_TAB_IDS = USER_PAGE_TABS.reduce( + (acc, tab) => { + acc[tab.id.toUpperCase() as Uppercase] = tab.id; + return acc; + }, + {} as { [K in Uppercase]: UserPageTabKey } +); + +export const DEFAULT_USER_PAGE_TAB: UserPageTabKey = "collected"; + +export function getUserPageTabByRoute(route: string) { + return USER_PAGE_TABS.find( + (tab) => tab.route.toLowerCase() === route.toLowerCase() + ); +} + +export function getUserPageTabById(id: UserPageTabKey) { + return USER_PAGE_TAB_MAP[id]; +} diff --git a/components/user/stats/activity/wallet/table/row/UserPageStatsActivityWalletTableRowSecondAddress.tsx b/components/user/stats/activity/wallet/table/row/UserPageStatsActivityWalletTableRowSecondAddress.tsx index 1f2e0ceb5f..8568ecd3af 100644 --- a/components/user/stats/activity/wallet/table/row/UserPageStatsActivityWalletTableRowSecondAddress.tsx +++ b/components/user/stats/activity/wallet/table/row/UserPageStatsActivityWalletTableRowSecondAddress.tsx @@ -9,7 +9,7 @@ import { } from "../../../../../../../helpers/Helpers"; import { TransactionType } from "./UserPageStatsActivityWalletTableRow"; import { usePathname } from "next/navigation"; -import { UserPageTabType } from "../../../../../layout/UserPageTabs"; +import { USER_PAGE_TAB_IDS } from "../../../../../layout/userTabs.config"; export default function UserPageStatsActivityWalletTableRowSecondAddress({ transaction, @@ -75,7 +75,7 @@ export default function UserPageStatsActivityWalletTableRowSecondAddress({ const path = getProfileTargetRoute({ handleOrWallet: address, pathname: pathname ?? "", - defaultPath: UserPageTabType.STATS, + defaultPath: USER_PAGE_TAB_IDS.STATS, }); return ( diff --git a/components/user/utils/CommonProfileLink.tsx b/components/user/utils/CommonProfileLink.tsx index d1cf026696..29270f73ba 100644 --- a/components/user/utils/CommonProfileLink.tsx +++ b/components/user/utils/CommonProfileLink.tsx @@ -1,9 +1,9 @@ "use client"; import Link from "next/link"; -import { UserPageTabType } from "../layout/UserPageTabs"; import { getProfileTargetRoute } from "../../../helpers/Helpers"; import { usePathname } from "next/navigation"; +import { type UserPageTabType } from "../layout/userTabs.config"; export default function CommonProfileLink({ handleOrWallet, diff --git a/helpers/Helpers.ts b/helpers/Helpers.ts index 843429f8c8..16f12deb67 100644 --- a/helpers/Helpers.ts +++ b/helpers/Helpers.ts @@ -3,9 +3,9 @@ import { NEXTGEN_CORE, } from "@/components/nextGen/nextgen_contracts"; import { - USER_PAGE_TAB_META, - UserPageTabType, -} from "@/components/user/layout/UserPageTabs"; + type UserPageTabType, + getUserPageTabById, +} from "@/components/user/layout/userTabs.config"; import { publicEnv } from "@/config/env"; import { GRADIENT_CONTRACT, @@ -632,7 +632,8 @@ export const getProfileTargetRoute = ({ if (pathname.includes("[user]")) { return pathname.replace("[user]", handleOrWallet); } - return `/${handleOrWallet}/${USER_PAGE_TAB_META[defaultPath].route}`; + const tab = getUserPageTabById(defaultPath); + return `/${handleOrWallet}/${tab?.route ?? ""}`; }; export function isNullAddress(address: string) { From 94a05162d1c846cc59988d66f064acaae0438802 Mon Sep 17 00:00:00 2001 From: Simo Date: Mon, 6 Oct 2025 11:05:00 +0200 Subject: [PATCH 002/256] wip Signed-off-by: Simo --- app/[user]/xtdh/page.tsx | 17 +++++++++++++++++ components/user/layout/userTabs.config.ts | 6 ++++++ components/user/xtdh/UserPageXtdh.tsx | 9 +++++++++ 3 files changed, 32 insertions(+) create mode 100644 app/[user]/xtdh/page.tsx create mode 100644 components/user/xtdh/UserPageXtdh.tsx diff --git a/app/[user]/xtdh/page.tsx b/app/[user]/xtdh/page.tsx new file mode 100644 index 0000000000..8688bafe99 --- /dev/null +++ b/app/[user]/xtdh/page.tsx @@ -0,0 +1,17 @@ +import { createUserTabPage } from "@/app/[user]/_lib/userTabPageFactory"; +import { + USER_PAGE_TAB_IDS, + USER_PAGE_TAB_MAP, +} from "@/components/user/layout/userTabs.config"; +import UserPageXtdh from "@/components/user/xtdh/UserPageXtdh"; + +const TAB_CONFIG = USER_PAGE_TAB_MAP[USER_PAGE_TAB_IDS.XTDH]; + +const { Page, generateMetadata } = createUserTabPage({ + subroute: TAB_CONFIG.route, + metaLabel: TAB_CONFIG.metaLabel, + Tab: UserPageXtdh, +}); + +export default Page; +export { generateMetadata }; diff --git a/components/user/layout/userTabs.config.ts b/components/user/layout/userTabs.config.ts index ff04753309..95fc5c8cef 100644 --- a/components/user/layout/userTabs.config.ts +++ b/components/user/layout/userTabs.config.ts @@ -37,6 +37,12 @@ const TABS = [ route: "collected", metaLabel: "Collected", }, + { + id: "xtdh", + title: "xTDH", + route: "xtdh", + metaLabel: "xTDH", + }, { id: "stats", title: "Stats", diff --git a/components/user/xtdh/UserPageXtdh.tsx b/components/user/xtdh/UserPageXtdh.tsx new file mode 100644 index 0000000000..33689285f6 --- /dev/null +++ b/components/user/xtdh/UserPageXtdh.tsx @@ -0,0 +1,9 @@ +import type { ApiIdentity } from "@/generated/models/ApiIdentity"; + +export default function UserPageXtdh({ + profile: _profile, +}: { + readonly profile: ApiIdentity; +}) { + return null; +} From 78fd3702ddab6a40ff9baf055dbc70913d079f0e Mon Sep 17 00:00:00 2001 From: Simo Date: Mon, 6 Oct 2025 11:29:39 +0200 Subject: [PATCH 003/256] wip Signed-off-by: Simo --- components/user/layout/UserPageTabs.tsx | 2 +- components/user/layout/userTabs.config.ts | 2 -- components/user/utils/CommonProfileLink.tsx | 4 ++-- helpers/Helpers.ts | 4 ++-- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/components/user/layout/UserPageTabs.tsx b/components/user/layout/UserPageTabs.tsx index 7bbf768976..f479a2dc2f 100644 --- a/components/user/layout/UserPageTabs.tsx +++ b/components/user/layout/UserPageTabs.tsx @@ -34,7 +34,7 @@ const resolveTabFromPath = (pathname: string): UserPageTabKey => { const segments = pathname.split("/").filter(Boolean); const routeSegment = segments[1] ?? ""; const match = getUserPageTabByRoute(routeSegment); - return (match?.id ?? DEFAULT_TAB) as UserPageTabKey; + return match?.id ?? DEFAULT_TAB; }; const filterVisibleTabs = ( diff --git a/components/user/layout/userTabs.config.ts b/components/user/layout/userTabs.config.ts index 95fc5c8cef..89db65a5b0 100644 --- a/components/user/layout/userTabs.config.ts +++ b/components/user/layout/userTabs.config.ts @@ -85,8 +85,6 @@ const TABS = [ ] as const satisfies readonly UserPageTabConfig[]; export type UserPageTabKey = (typeof TABS)[number]["id"]; -export type UserPageTabType = UserPageTabKey; - export const USER_PAGE_TABS = TABS; export const USER_PAGE_TAB_MAP: Record = diff --git a/components/user/utils/CommonProfileLink.tsx b/components/user/utils/CommonProfileLink.tsx index 0659af8259..673804389d 100644 --- a/components/user/utils/CommonProfileLink.tsx +++ b/components/user/utils/CommonProfileLink.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { getProfileTargetRoute } from "@/helpers/Helpers"; import { usePathname } from "next/navigation"; -import { type UserPageTabType } from "@/components/user/layout/userTabs.config"; +import { type UserPageTabKey } from "@/components/user/layout/userTabs.config"; export default function CommonProfileLink({ handleOrWallet, @@ -12,7 +12,7 @@ export default function CommonProfileLink({ }: { readonly handleOrWallet: string; readonly isCurrentUser: boolean; - readonly tabTarget: UserPageTabType; + readonly tabTarget: UserPageTabKey; }) { const pathname = usePathname(); const url = getProfileTargetRoute({ diff --git a/helpers/Helpers.ts b/helpers/Helpers.ts index be504887f1..7a83ae6d51 100644 --- a/helpers/Helpers.ts +++ b/helpers/Helpers.ts @@ -3,7 +3,7 @@ import { NEXTGEN_CORE, } from "@/components/nextGen/nextgen_contracts"; import { - type UserPageTabType, + type UserPageTabKey, getUserPageTabById, } from "@/components/user/layout/userTabs.config"; import { publicEnv } from "@/config/env"; @@ -624,7 +624,7 @@ export const getProfileTargetRoute = ({ }: { readonly handleOrWallet: string; readonly pathname: string; - readonly defaultPath: UserPageTabType; + readonly defaultPath: UserPageTabKey; }): string => { if (!handleOrWallet.length) { return "/404"; From 33957727048233164841ae40ebfab80e689e75f2 Mon Sep 17 00:00:00 2001 From: simo6529 Date: Mon, 6 Oct 2025 12:43:09 +0300 Subject: [PATCH 004/256] Full NFT Picker UI (#1498) * reusable nft picker Signed-off-by: Simo --------- Signed-off-by: Simo Signed-off-by: OpenAI Assistant <123456+openai-assistant@users.noreply.github.com> --- __tests__/AGENTS.md | 152 +++- .../nft-picker/NftPicker.utils.test.ts | 163 ++++ __tests__/services/alchemy-api.test.ts | 142 ++- app/demo/nft-picker/page.tsx | 12 + app/demo/nft-picker/picker-client.tsx | 30 + components/gas-royalties/Gas.tsx | 2 +- components/gas-royalties/GasRoyalties.tsx | 2 +- components/gas-royalties/Royalties.tsx | 2 +- .../NextGenCollectionHeader.tsx | 2 +- .../collectionParts/mint/NextGenMint.tsx | 2 +- .../mint/NextGenMintBurnWidget.tsx | 2 +- .../mint/NextGenMintWidget.tsx | 2 +- .../nft-picker/AllTokensSelectedCard.tsx | 44 + components/nft-picker/NftContractHeader.tsx | 102 +++ components/nft-picker/NftEditRanges.tsx | 223 +++++ components/nft-picker/NftPicker.tsx | 836 ++++++++++++++++++ components/nft-picker/NftPicker.types.ts | 105 +++ components/nft-picker/NftPicker.utils.ts | 494 +++++++++++ components/nft-picker/NftSuggestList.tsx | 191 ++++ components/nft-picker/NftTokenList.tsx | 216 +++++ components/nft-picker/useAlchemyClient.ts | 277 ++++++ .../react-query-wrapper/ReactQueryWrapper.tsx | 3 + contexts/SeizeSettingsContext.tsx | 2 +- docs/nft-picker-quick-checklist.md | 67 ++ next.config.mjs | 3 + package-lock.json | 98 +- package.json | 1 + services/6529api.ts | 47 +- services/alchemy-api.ts | 531 ++++++++++- spec.md | 589 ++++++++++++ 30 files changed, 4170 insertions(+), 172 deletions(-) create mode 100644 __tests__/components/nft-picker/NftPicker.utils.test.ts create mode 100644 app/demo/nft-picker/page.tsx create mode 100644 app/demo/nft-picker/picker-client.tsx create mode 100644 components/nft-picker/AllTokensSelectedCard.tsx create mode 100644 components/nft-picker/NftContractHeader.tsx create mode 100644 components/nft-picker/NftEditRanges.tsx create mode 100644 components/nft-picker/NftPicker.tsx create mode 100644 components/nft-picker/NftPicker.types.ts create mode 100644 components/nft-picker/NftPicker.utils.ts create mode 100644 components/nft-picker/NftSuggestList.tsx create mode 100644 components/nft-picker/NftTokenList.tsx create mode 100644 components/nft-picker/useAlchemyClient.ts create mode 100644 docs/nft-picker-quick-checklist.md create mode 100644 spec.md diff --git a/__tests__/AGENTS.md b/__tests__/AGENTS.md index 448c2e134b..70db432c6d 100644 --- a/__tests__/AGENTS.md +++ b/__tests__/AGENTS.md @@ -1,66 +1,134 @@ -# Test Guidelines for Codex +# Codex Testing Guidelines -## Purpose and Structure +## Purpose & Structure -The `__tests__` directory contains Jest test suites for this Next.js project. Tests mirror the source folders such as `components`, `contexts`, `hooks` and `utils` to keep structure familiar. Integration tests for API routes live under `app/api`. Fixtures and helpers used across tests reside in their respective subfolders. +* All tests live in `__tests__`, mirroring source folders (`components`, `contexts`, `hooks`, `utils`). +* API integration tests: `app/api`. +* Shared fixtures & helpers: relevant subfolders. +* Jest automatically picks up mocks from `__mocks__`. -## Testing Frameworks and Tools +## Tools -- **Jest** with the `ts-jest` preset for TypeScript support. -- **@testing-library/react** and **@testing-library/user-event** for React component tests. -- Additional mocks live in `__mocks__` and are automatically picked up by Jest. +* **Jest** + `ts-jest` (TypeScript). +* **@testing-library/react** + **user-event** (React tests). +* Coverage reports in `coverage/`. -## Test Writing Guidelines +## Writing Tests -- Prioritise meaningful coverage that validates business requirements over achieving raw coverage numbers. -- Use the **Arrange – Act – Assert** pattern and keep assertions focused on behaviour, not implementation details. -- Give each test a clear, descriptive name that conveys the scenario and expected outcome. -- Tests should remain independent, deterministic and fast. +* Focus on business value, not raw coverage. +* Follow **Arrange – Act – Assert**. +* One behaviour per test; clear, descriptive names. +* Keep tests independent, deterministic, and fast. +* Use realistic data. -## Test Categorisation and Prioritisation +### Test Types -Consider the following categories when writing tests: +* **Happy Path** – expected workflows. +* **Errors** – invalid input, unexpected scenarios. +* **Edge Cases** – boundaries, rare conditions. +* **Integration** – components & API interactions. +* **Performance/Security** – when relevant. -- **Happy Path Tests** – standard workflows with valid inputs. -- **Error Handling Tests** – behaviour with invalid inputs or unexpected scenarios. -- **Edge Case Tests** – boundary conditions and uncommon situations. -- **Integration Tests** – interactions between components or with API routes. -- **Performance & Security Tests** – only when relevant. +Prioritise high-risk areas first when time-boxed. -When time‑boxed, focus on high‑risk areas first, then fill coverage gaps and polish. +## Time-Boxed Cycle (20 min) -## Time‑Boxed Testing Approach +1. **5–7 min**: core flows. +2. **5–7 min**: edge cases & branches. +3. **5–6 min**: clean up & refine. -A suggested 20‑minute cycle: +## Quality Checklist -1. **Initial Phase (5‑7 min)** – target high‑impact paths and core functionality. -2. **Middle Phase (5‑7 min)** – add tests for edge cases or missing branches. -3. **Final Phase (5‑6 min)** – clean up, improve readability and verify results. +* [ ] Clear, descriptive names +* [ ] Arrange – Act – Assert used +* [ ] Independent & fast +* [ ] One behaviour per test +* [ ] Production-like data -## Test Quality Checklist +## Running Tests -- [ ] Clear, descriptive test names -- [ ] Proper Arrange – Act – Assert structure -- [ ] Independent and deterministic -- [ ] Execute quickly and focus on one behaviour -- [ ] Use realistic, production‑like data +```bash +npm run test:cov:changed # changed files only +npm run test # full suite +npm run lint +npm run type-check +``` -## Execution Instructions +--- -Run tests with: +# Coding Standards -```bash -npm run test:cov:changed +### Complexity + +* Functions ≤ 15 cognitive complexity. +* Extract deep ternaries (>3 levels). +* Break down complex logic. + +### Modern Patterns + +**Iteration** + +```ts +// ❌ Avoid +items.forEach(item => processItem(item)); + +// ✅ Prefer +for (const item of items) { + processItem(item); +} ``` -This command runs Jest only on files changed since `main`. Use `npm run test` if -you need to execute the entire suite. +* Allows `break/continue`. +* Works with async/await. -This command also checks coverage for modified files. Linting and type‑checking should pass as well: +**Array Access** -```bash -npm run lint -npm run type-check +```ts +// ✅ Prefer +const last = array.at(-1); +const secondLast = array.at(-2); ``` -Coverage reports are generated in the `coverage` directory. A summary is printed in the terminal after tests complete. +**Strings** + +```ts +// ✅ Prefer +str.replaceAll('old', 'new'); +``` + +**Globals** + +```ts +// ✅ Prefer +globalThis.fetch(url); +``` + +**Imports** + +* One import per module. +* Order: external → internal → types. +* No duplicates. + +**Accessibility** + +* Use semantic HTML (`