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..8ca70a6032 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,22 @@ interface ActivityFiltersProps {
readonly isMobile: boolean;
}
+export const ActivityContractItems = Object.freeze(
+ Object.values(ContractFilter).map((contract) => ({
+ key: contract,
+ label: contract,
+ value: contract,
+ }))
+);
+
+export const ActivityTypeItems = Object.freeze(
+ Object.values(TypeFilter).map((type) => ({
+ key: type,
+ label: type,
+ value: type,
+ }))
+);
+
export default function ActivityFilters({
typeFilter,
selectedContract,
@@ -20,26 +35,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..a6f7abf995 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,40 @@ export default function MemeLabPageComponent({
};
}, [nftId]);
+ const activityContent = useMemo(() => {
+ if (activity.length > 0) {
+ return (
+
+
+ {activity.map((tr) => (
+
+ ))}
+
+
+ );
+ }
+
+ if (activityLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (activity.length === 0) {
+ return (
+
+
+
+ );
+ }
+ }, [activity, activityLoading, nft]);
+
function printContent() {
if (activeTab === MEME_FOCUS.ACTIVITY) {
return printActivity();
@@ -1311,63 +1351,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,40 @@ export function MemePageActivity(
activityTypeFilter,
]);
+ const activityContent = useMemo(() => {
+ if (activity.length > 0) {
+ return (
+
+
+ {activity.map((tr) => (
+
+ ))}
+
+
+ );
+ }
+
+ if (activityLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (activity.length === 0) {
+ return (
+
+
+
+ );
+ }
+ }, [activity, activityLoading, props.nft]);
+
if (props.show && props.nft) {
return (
@@ -142,55 +187,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 (
-