From da28a6ae11807b0b2334d1506fc04836190d8000 Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Mon, 15 Dec 2025 13:11:14 +0200 Subject: [PATCH 1/3] Activity Filters - use common dropdown Signed-off-by: prxt6529 --- .../latest-activity/ActivityFilters.test.tsx | 18 +-- .../components/memelab/MemeLabPage.test.tsx | 25 +++- .../the-memes/MemePageActivity.test.tsx | 12 +- .../latest-activity/ActivityFilters.tsx | 41 +++--- components/memelab/MemeLabPage.tsx | 134 +++++++++--------- components/the-memes/MemePageActivity.tsx | 113 ++++++++------- .../utils/select/dropdown/CommonDropdown.tsx | 2 +- 7 files changed, 185 insertions(+), 160 deletions(-) diff --git a/__tests__/components/latest-activity/ActivityFilters.test.tsx b/__tests__/components/latest-activity/ActivityFilters.test.tsx index 1d05bf91d2..c1c2ae2452 100644 --- a/__tests__/components/latest-activity/ActivityFilters.test.tsx +++ b/__tests__/components/latest-activity/ActivityFilters.test.tsx @@ -44,7 +44,7 @@ describe("ActivityFilters", () => { screen.getByRole("button", { name: /Collection:/i }) ).toBeInTheDocument(); expect( - screen.getByRole("button", { name: /Filter:/i }) + screen.getByRole("button", { name: /Transaction Type:/i }) ).toBeInTheDocument(); }); @@ -56,7 +56,7 @@ describe("ActivityFilters", () => { }) ).toBeInTheDocument(); expect( - screen.getByRole("button", { name: `Filter: ${TypeFilter.ALL}` }) + screen.getByRole("button", { name: `Transaction Type: ${TypeFilter.ALL}` }) ).toBeInTheDocument(); }); @@ -73,7 +73,7 @@ describe("ActivityFilters", () => { }) ).toBeInTheDocument(); expect( - screen.getByRole("button", { name: `Filter: ${TypeFilter.SALES}` }) + screen.getByRole("button", { name: `Transaction Type: ${TypeFilter.SALES}` }) ).toBeInTheDocument(); }); }); @@ -118,7 +118,7 @@ describe("ActivityFilters", () => { const props = { ...mockProps, typeFilter }; const { unmount } = render(); expect( - screen.getByRole("button", { name: `Filter: ${typeFilter}` }) + screen.getByRole("button", { name: `Transaction Type: ${typeFilter}` }) ).toBeInTheDocument(); unmount(); } @@ -157,7 +157,7 @@ describe("ActivityFilters", () => { render(); const typeButton = screen.getByRole("button", { - name: `Filter: ${TypeFilter.ALL}`, + name: `Transaction Type: ${TypeFilter.ALL}`, }); await user.click(typeButton); @@ -199,7 +199,7 @@ describe("ActivityFilters", () => { ); const typeButton = screen.getByRole("button", { - name: `Filter: ${TypeFilter.ALL}`, + name: `Transaction Type: ${TypeFilter.ALL}`, }); await user.click(typeButton); @@ -232,7 +232,7 @@ describe("ActivityFilters", () => { render(); const typeButton = screen.getByRole("button", { - name: `Filter: ${TypeFilter.ALL}`, + name: `Transaction Type: ${TypeFilter.ALL}`, }); await user.click(typeButton); @@ -252,7 +252,7 @@ describe("ActivityFilters", () => { expect(col).toBeInTheDocument(); const buttons = within(col as HTMLElement).getAllByRole("button", { - name: /Collection:|Filter:/i, + name: /Collection:|Transaction Type:/i, }); expect(buttons).toHaveLength(2); }); @@ -273,7 +273,7 @@ describe("ActivityFilters", () => { render(); const buttons = screen.getAllByRole("button", { - name: /Collection:|Filter:/i, + name: /Collection:|Transaction Type:/i, }); for (const button of buttons) { expect(button).toHaveAttribute("aria-haspopup", "true"); diff --git a/__tests__/components/memelab/MemeLabPage.test.tsx b/__tests__/components/memelab/MemeLabPage.test.tsx index 8388b4144d..a8b97b373a 100644 --- a/__tests__/components/memelab/MemeLabPage.test.tsx +++ b/__tests__/components/memelab/MemeLabPage.test.tsx @@ -149,6 +149,10 @@ require("@/components/cookies/CookieConsentContext").useCookieConsent = mockUseCookieConsent; require("@/hooks/useCapacitor").default = mockUseCapacitor; +const expectAbortSignalOptions = expect.objectContaining({ + signal: expect.objectContaining({ aborted: false }), +}); + beforeEach(() => { jest.clearAllMocks(); @@ -335,7 +339,8 @@ describe("MemeLabPageComponent", () => { await waitFor(() => { expect(mockFetchUrl).toHaveBeenCalledWith( - "https://api.test.6529.io/api/lab_extended_data?id=1" + "https://api.test.6529.io/api/lab_extended_data?id=1", + expect.objectContaining({ signal: expect.anything() }) ); }); }); @@ -349,7 +354,8 @@ describe("MemeLabPageComponent", () => { await waitFor(() => { expect(mockFetchUrl).toHaveBeenCalledWith( - "https://api.test.6529.io/api/nfts_memelab?id=1" + "https://api.test.6529.io/api/nfts_memelab?id=1", + expect.objectContaining({ signal: expect.anything() }) ); }); }); @@ -363,7 +369,8 @@ describe("MemeLabPageComponent", () => { await waitFor(() => { expect(mockFetchUrl).toHaveBeenCalledWith( - expect.stringContaining("transactions_memelab?wallet=0xabc&id=1") + expect.stringContaining("transactions_memelab?wallet=0xabc&id=1"), + expectAbortSignalOptions ); }); }); @@ -398,7 +405,8 @@ describe("MemeLabPageComponent", () => { await waitFor( () => { expect(mockFetchUrl).toHaveBeenCalledWith( - expect.stringContaining("transactions_memelab?wallet=0xabc") + expect.stringContaining("transactions_memelab?wallet=0xabc"), + expectAbortSignalOptions ); }, { timeout: 5000 } @@ -416,7 +424,8 @@ describe("MemeLabPageComponent", () => { expect(mockFetchUrl).toHaveBeenCalledWith( expect.stringMatching( /transactions_memelab.*id=1.*page_size=25.*page=1/ - ) + ), + expectAbortSignalOptions ); }); }); @@ -432,7 +441,8 @@ describe("MemeLabPageComponent", () => { expect(mockFetchAllPages).toHaveBeenCalledWith( expect.stringContaining( "nft_history/0x4db52a61dc491e15a2f78f5ac001c14ffe3568cb/1" - ) + ), + expectAbortSignalOptions ); }); }); @@ -535,7 +545,8 @@ describe("MemeLabPageComponent", () => { await waitFor(() => { expect(mockFetchUrl).toHaveBeenCalledWith( - expect.stringContaining("transactions_memelab?wallet=0xabc&id=1") + expect.stringContaining("transactions_memelab?wallet=0xabc&id=1"), + expectAbortSignalOptions ); }); }); diff --git a/__tests__/components/the-memes/MemePageActivity.test.tsx b/__tests__/components/the-memes/MemePageActivity.test.tsx index 02ad75c036..9fce7803ff 100644 --- a/__tests__/components/the-memes/MemePageActivity.test.tsx +++ b/__tests__/components/the-memes/MemePageActivity.test.tsx @@ -124,9 +124,9 @@ describe("MemePageActivity", () => { await waitFor(() => expect(fetchUrlMock).toHaveBeenCalledTimes(1)); - await userEvent.click(screen.getByRole("button", { name: /Filter/ })); + await userEvent.click(screen.getByRole("button", { name: /Transaction Type/ })); await userEvent.click( - screen.getByRole("button", { name: TypeFilter.SALES }) + screen.getByRole("menuitem", { name: TypeFilter.SALES }) ); await waitFor(() => { @@ -168,8 +168,8 @@ describe("MemePageActivity", () => { ]; for (const { filter, param } of filterTypes) { - await userEvent.click(screen.getByRole("button", { name: /Filter/ })); - await userEvent.click(screen.getByRole("button", { name: filter })); + await userEvent.click(screen.getByRole("button", { name: /Transaction Type/ })); + await userEvent.click(screen.getByRole("menuitem", { name: filter })); await waitFor(() => { expect(fetchUrlMock).toHaveBeenLastCalledWith( @@ -201,9 +201,9 @@ describe("MemePageActivity", () => { }); // Change filter - should reset to page 1 - await userEvent.click(screen.getByRole("button", { name: /Filter/ })); + await userEvent.click(screen.getByRole("button", { name: /Transaction Type/ })); await userEvent.click( - screen.getByRole("button", { name: TypeFilter.SALES }) + screen.getByRole("menuitem", { name: TypeFilter.SALES }) ); await waitFor(() => { diff --git a/components/latest-activity/ActivityFilters.tsx b/components/latest-activity/ActivityFilters.tsx index db24e2e2e8..fc62cbb29e 100644 --- a/components/latest-activity/ActivityFilters.tsx +++ b/components/latest-activity/ActivityFilters.tsx @@ -2,7 +2,6 @@ import CommonDropdown from "@/components/utils/select/dropdown/CommonDropdown"; import { ContractFilter, TypeFilter } from "@/hooks/useActivityData"; -import { useMemo } from "react"; import { Col } from "react-bootstrap"; interface ActivityFiltersProps { @@ -13,6 +12,20 @@ interface ActivityFiltersProps { readonly isMobile: boolean; } +export const ActivityContractItems = Object.values(ContractFilter).map( + (contract) => ({ + key: contract, + label: contract, + value: contract, + }) +); + +export const ActivityTypeItems = Object.values(TypeFilter).map((type) => ({ + key: type, + label: type, + value: type, +})); + export default function ActivityFilters({ typeFilter, selectedContract, @@ -20,26 +33,6 @@ export default function ActivityFilters({ onContractFilterChange, isMobile, }: ActivityFiltersProps) { - const contractItems = useMemo( - () => - Object.values(ContractFilter).map((contract) => ({ - key: contract, - label: contract, - value: contract, - })), - [] - ); - - const typeItems = useMemo( - () => - Object.values(TypeFilter).map((type) => ({ - key: type, - label: type, - value: type, - })), - [] - ); - return ( diff --git a/components/memelab/MemeLabPage.tsx b/components/memelab/MemeLabPage.tsx index de4759e332..93e86190e6 100644 --- a/components/memelab/MemeLabPage.tsx +++ b/components/memelab/MemeLabPage.tsx @@ -4,7 +4,11 @@ import styles from "./MemeLab.module.scss"; import { useAuth } from "@/components/auth/Auth"; import { useCookieConsent } from "@/components/cookies/CookieConsentContext"; +import CircleLoader, { + CircleLoaderSize, +} from "@/components/distribution-plan-tool/common/CircleLoader"; import Download from "@/components/download/Download"; +import { ActivityTypeItems } from "@/components/latest-activity/ActivityFilters"; import LatestActivityRow from "@/components/latest-activity/LatestActivityRow"; import MemeLabLeaderboard from "@/components/leaderboard/MemeLabLeaderboard"; import NFTAttributes from "@/components/nft-attributes/NFTAttributes"; @@ -24,6 +28,7 @@ import { TabButton, } from "@/components/the-memes/MemeShared"; import Timeline from "@/components/timeline/Timeline"; +import CommonDropdown from "@/components/utils/select/dropdown/CommonDropdown"; import { publicEnv } from "@/config/env"; import { MEMELAB_CONTRACT, MEMES_CONTRACT, NULL_ADDRESS } from "@/constants"; import { useTitle } from "@/contexts/TitleContext"; @@ -52,15 +57,8 @@ import { faExpandAlt, faFire } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; -import { Fragment, useEffect, useState } from "react"; -import { - Carousel, - Col, - Container, - Dropdown, - Row, - Table, -} from "react-bootstrap"; +import { Fragment, useEffect, useMemo, useState } from "react"; +import { Carousel, Col, Container, Row, Table } from "react-bootstrap"; const ACTIVITY_PAGE_SIZE = 25; @@ -114,6 +112,7 @@ export default function MemeLabPageComponent({ const [activityTypeFilter, setActivityTypeFilter] = useState( TypeFilter.ALL ); + const [activityLoading, setActivityLoading] = useState(false); useEffect(() => { setTitle(getMemeTabTitle(`Meme Lab`, nftId, nft, activeTab)); @@ -345,6 +344,7 @@ export default function MemeLabPageComponent({ return; } + setActivityLoading(true); let url = `${publicEnv.API_ENDPOINT}/api/transactions_memelab?id=${nftId}&page_size=${ACTIVITY_PAGE_SIZE}&page=${activityPage}`; switch (activityTypeFilter) { case TypeFilter.SALES: @@ -383,6 +383,9 @@ export default function MemeLabPageComponent({ console.error(`Failed to fetch Meme Lab activity for ${nftId}`, error); setActivityTotalResults(0); setActivity([]); + }) + .finally(() => { + setActivityLoading(false); }); return () => { @@ -408,7 +411,10 @@ export default function MemeLabPageComponent({ if (cancelled || isAbortError(error)) { return; } - console.error(`Failed to fetch NFT history for Meme Lab ${nftId}`, error); + console.error( + `Failed to fetch NFT history for Meme Lab ${nftId}`, + error + ); setNftHistory([]); } } @@ -426,6 +432,37 @@ export default function MemeLabPageComponent({ }; }, [nftId]); + const activityContent = useMemo(() => { + if (activityLoading) { + return ( +
+ +
+ ); + } + if (activity.length === 0) { + return ( +
+ +
+ ); + } + + return ( + + + {activity.map((tr) => ( + + ))} + +
+ ); + }, [activity, activityLoading, nft]); + function printContent() { if (activeTab === MEME_FOCUS.ACTIVITY) { return printActivity(); @@ -1311,63 +1348,30 @@ export default function MemeLabPageComponent({ )} - - -

Card Activity

- - - - Filter: {activityTypeFilter} - - {Object.values(TypeFilter).map((filter) => ( - { - setActivityPage(1); - setActivityTypeFilter(filter); - }}> - {filter} - - ))} - - + + +
+

+ Card Activity +

+
+ { + setActivityPage(1); + setActivityTypeFilter(filter); + }} + /> +
+
- {activity.length > 0 ? ( - - - - - {activity.map((tr) => ( - - ))} - -
- -
- ) : ( - - - - - - )} - {activity.length > 0 && ( + + {activityContent} + + {activity.length > 0 && !activityLoading && ( ( TypeFilter.ALL ); + const [activityLoading, setActivityLoading] = useState(false); useEffect(() => { if (!props.show || !props.nft?.id) { @@ -37,6 +44,7 @@ export function MemePageActivity( let cancelled = false; if (props.nft?.id) { + setActivityLoading(true); let url = `${publicEnv.API_ENDPOINT}/api/transactions?contract=${MEMES_CONTRACT}&id=${props.nft.id}&page_size=${props.pageSize}&page=${activityPage}`; switch (activityTypeFilter) { case TypeFilter.SALES: @@ -65,6 +73,9 @@ export function MemePageActivity( if (cancelled) return; setActivityTotalResults(0); setActivity([]); + }) + .finally(() => { + setActivityLoading(false); }); } return () => { @@ -78,6 +89,37 @@ export function MemePageActivity( activityTypeFilter, ]); + const activityContent = useMemo(() => { + if (activityLoading) { + return ( +
+ +
+ ); + } + if (activity.length === 0) { + return ( +
+ +
+ ); + } + + return ( + + + {activity.map((tr) => ( + + ))} + +
+ ); + }, [activity, activityLoading, props.nft]); + if (props.show && props.nft) { return ( @@ -142,55 +184,30 @@ export function MemePageActivity(
)} - - -

Card Activity

- - - - Filter: {activityTypeFilter} - - {Object.values(TypeFilter).map((filter) => ( - { - setActivityPage(1); - setActivityTypeFilter(filter); - }}> - {filter} - - ))} - - + + +
+

+ Card Activity +

+
+ { + setActivityPage(1); + setActivityTypeFilter(filter); + }} + /> +
+
- - - - {activity.map((tr) => ( - - ))} - -
- + {activityContent}
- {activity.length > 0 && ( + {activity.length > 0 && !activityLoading && ( ; } -} \ No newline at end of file +} diff --git a/components/utils/select/dropdown/CommonDropdown.tsx b/components/utils/select/dropdown/CommonDropdown.tsx index e180bbc986..6fe8bc1f9e 100644 --- a/components/utils/select/dropdown/CommonDropdown.tsx +++ b/components/utils/select/dropdown/CommonDropdown.tsx @@ -106,7 +106,7 @@ export default function CommonDropdown( const [isMobile, setIsMobile] = useState(false); return ( -
+