diff --git a/__tests__/components/brain/BrainMobile.test.tsx b/__tests__/components/brain/BrainMobile.test.tsx index 86627206db..2148e78014 100644 --- a/__tests__/components/brain/BrainMobile.test.tsx +++ b/__tests__/components/brain/BrainMobile.test.tsx @@ -1,98 +1,148 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import BrainMobile from '@/components/brain/BrainMobile'; +import { render, screen, waitFor } from "@testing-library/react"; +import BrainMobile from "@/components/brain/BrainMobile"; -jest.mock('next/image', () => ({ __esModule: true, default: (props:any) => })); +jest.mock("next/image", () => ({ + __esModule: true, + default: (props: any) => , +})); let mockSearchParams = new URLSearchParams(); -let mockPathname = '/'; +let mockPathname = "/"; const mockPush = jest.fn(); -jest.mock('next/navigation', () => ({ +jest.mock("next/navigation", () => ({ useRouter: () => ({ push: mockPush }), useSearchParams: () => mockSearchParams, usePathname: () => mockPathname, })); let isApp = true; -jest.mock('@/hooks/useDeviceInfo', () => ({ __esModule: true, default: () => ({ isApp }) })); +jest.mock("@/hooks/useDeviceInfo", () => ({ + __esModule: true, + default: () => ({ isApp }), +})); let dropData: any = null; let waveData: any = null; -jest.mock('@tanstack/react-query', () => ({ +jest.mock("@tanstack/react-query", () => ({ keepPreviousData: {}, useQuery: jest.fn(() => ({ data: dropData })), })); -jest.mock('@/hooks/useWaveData', () => ({ - useWaveData: () => ({ data: waveData }) +jest.mock("@/hooks/useWaveData", () => ({ + useWaveData: () => ({ data: waveData }), })); -jest.mock('@/hooks/useWave', () => ({ - useWave: () => ({ isMemesWave: false, isRankWave: true }) +jest.mock("@/hooks/useWave", () => ({ + useWave: () => ({ + isMemesWave: false, + isCurationWave: false, + isRankWave: true, + }), })); -jest.mock('@/hooks/useWaveTimers', () => ({ - useWaveTimers: () => ({ voting: { isCompleted: false }, decisions: { firstDecisionDone: true } }) +jest.mock("@/hooks/useWaveTimers", () => ({ + useWaveTimers: () => ({ + voting: { isCompleted: false }, + decisions: { firstDecisionDone: true }, + }), })); -jest.mock('@/components/brain/BrainDesktopDrop', () => ({ __esModule: true, default: (props:any) =>
drop
})); +jest.mock("@/components/brain/BrainDesktopDrop", () => ({ + __esModule: true, + default: (props: any) => ( +
+ drop +
+ ), +})); -jest.mock('@/components/brain/mobile/BrainMobileTabs', () => ({ __esModule: true, default: () =>
})); +jest.mock("@/components/brain/mobile/BrainMobileTabs", () => ({ + __esModule: true, + default: () =>
, +})); -jest.mock('@/components/brain/mobile/BrainMobileAbout', () => ({ __esModule: true, default: () =>
})); +jest.mock("@/components/brain/mobile/BrainMobileAbout", () => ({ + __esModule: true, + default: () =>
, +})); -jest.mock('@/components/brain/mobile/BrainMobileWaves', () => ({ __esModule: true, default: () =>
})); +jest.mock("@/components/brain/mobile/BrainMobileWaves", () => ({ + __esModule: true, + default: () =>
, +})); -jest.mock('@/components/brain/mobile/BrainMobileMessages', () => ({ __esModule: true, default: () =>
})); +jest.mock("@/components/brain/mobile/BrainMobileMessages", () => ({ + __esModule: true, + default: () =>
, +})); -jest.mock('@/components/brain/notifications', () => ({ __esModule: true, default: () =>
})); +jest.mock("@/components/brain/notifications", () => ({ + __esModule: true, + default: () =>
, +})); -jest.mock('@/components/brain/my-stream/MyStreamWaveLeaderboard', () => ({ __esModule: true, default: () =>
})); +jest.mock("@/components/brain/my-stream/MyStreamWaveLeaderboard", () => ({ + __esModule: true, + default: () =>
, +})); -jest.mock('@/components/brain/my-stream/MyStreamWaveOutcome', () => ({ __esModule: true, default: () =>
})); +jest.mock("@/components/brain/my-stream/MyStreamWaveOutcome", () => ({ + __esModule: true, + default: () =>
, +})); -jest.mock('@/components/waves/winners/WaveWinners', () => ({ __esModule: true, WaveWinners: () =>
})); +jest.mock("@/components/waves/winners/WaveWinners", () => ({ + __esModule: true, + WaveWinners: () =>
, +})); -jest.mock('@/components/brain/my-stream/votes/MyStreamWaveMyVotes', () => ({ __esModule: true, default: () =>
})); +jest.mock("@/components/brain/my-stream/votes/MyStreamWaveMyVotes", () => ({ + __esModule: true, + default: () =>
, +})); -jest.mock('@/components/brain/my-stream/MyStreamWaveFAQ', () => ({ __esModule: true, default: () =>
})); +jest.mock("@/components/brain/my-stream/MyStreamWaveFAQ", () => ({ + __esModule: true, + default: () =>
, +})); // Tests -describe('BrainMobile', () => { +describe("BrainMobile", () => { beforeEach(() => { mockSearchParams = new URLSearchParams(); - mockPathname = '/'; + mockPathname = "/"; mockPush.mockClear(); dropData = null; waveData = null; isApp = true; }); - it('renders BrainDesktopDrop when drop is open', () => { - mockSearchParams.set('drop', 'd1'); - dropData = { id: 'd1' }; + it("renders BrainDesktopDrop when drop is open", () => { + mockSearchParams.set("drop", "d1"); + dropData = { id: "d1" }; render(child); - expect(screen.getByTestId('drop')).toBeInTheDocument(); + expect(screen.getByTestId("drop")).toBeInTheDocument(); }); - it('shows notifications view when path matches', async () => { - mockPathname = '/notifications'; + it("shows notifications view when path matches", async () => { + mockPathname = "/notifications"; render(child); await waitFor(() => { - expect(screen.getByTestId('notifications')).toBeInTheDocument(); + expect(screen.getByTestId("notifications")).toBeInTheDocument(); }); }); - it('shows tabs only when wave active or not in app', async () => { + it("shows tabs only when wave active or not in app", async () => { isApp = true; render(child); - expect(screen.queryByTestId('tabs')).toBeNull(); + expect(screen.queryByTestId("tabs")).toBeNull(); - mockSearchParams.set('wave', '1'); + mockSearchParams.set("wave", "1"); const { rerender } = render(child); - await waitFor(() => expect(screen.getByTestId('tabs')).toBeInTheDocument()); + await waitFor(() => expect(screen.getByTestId("tabs")).toBeInTheDocument()); rerender(
); }); }); diff --git a/__tests__/components/brain/mobile/BrainMobileTabs.test.tsx b/__tests__/components/brain/mobile/BrainMobileTabs.test.tsx index 68140fa0d2..4746715f31 100644 --- a/__tests__/components/brain/mobile/BrainMobileTabs.test.tsx +++ b/__tests__/components/brain/mobile/BrainMobileTabs.test.tsx @@ -18,12 +18,12 @@ enum BrainView { const push = jest.fn(); -jest.mock("next/navigation", () => ({ - useRouter: () => ({ push }), +jest.mock("next/navigation", () => ({ + useRouter: () => ({ push }), useSearchParams: () => ({ - get: jest.fn().mockReturnValue(null) + get: jest.fn().mockReturnValue(null), }), - usePathname: () => "/brain" + usePathname: () => "/brain", })); jest.mock("react-use", () => ({ @@ -72,6 +72,7 @@ describe("BrainMobileTabs", () => { jest.clearAllMocks(); (useWave as jest.Mock).mockReturnValue({ isMemesWave: false, + isCurationWave: false, isRankWave: false, }); (useUnreadIndicator as jest.Mock).mockReturnValue({ hasUnread: false }); @@ -83,6 +84,7 @@ describe("BrainMobileTabs", () => { it("renders back button and navigates to My Stream", async () => { (useWave as jest.Mock).mockReturnValue({ isMemesWave: false, + isCurationWave: false, isRankWave: false, }); render( @@ -106,6 +108,7 @@ describe("BrainMobileTabs", () => { it("shows unread indicators and handles message/notification clicks", async () => { (useWave as jest.Mock).mockReturnValue({ isMemesWave: false, + isCurationWave: false, isRankWave: false, }); (useUnreadIndicator as jest.Mock).mockReturnValue({ hasUnread: true }); @@ -141,6 +144,7 @@ describe("BrainMobileTabs", () => { it("renders leaderboard and extra tabs for memes rank wave", () => { (useWave as jest.Mock).mockReturnValue({ isMemesWave: true, + isCurationWave: false, isRankWave: true, }); @@ -168,4 +172,27 @@ describe("BrainMobileTabs", () => { expect(screen.getByText("Outcome")).toBeInTheDocument(); expect(screen.getByText("FAQ")).toBeInTheDocument(); }); + + it("renders My Votes for curation rank wave", () => { + (useWave as jest.Mock).mockReturnValue({ + isMemesWave: false, + isCurationWave: true, + isRankWave: true, + }); + + render( + + ); + + expect(screen.getByText("My Votes")).toBeInTheDocument(); + expect(screen.queryByText("FAQ")).toBeNull(); + }); }); diff --git a/__tests__/components/brain/my-stream/MyStreamWaveDesktopTabs.test.tsx b/__tests__/components/brain/my-stream/MyStreamWaveDesktopTabs.test.tsx index 203b5f7849..26d75070a1 100644 --- a/__tests__/components/brain/my-stream/MyStreamWaveDesktopTabs.test.tsx +++ b/__tests__/components/brain/my-stream/MyStreamWaveDesktopTabs.test.tsx @@ -120,6 +120,38 @@ describe("MyStreamWaveDesktopTabs", () => { expect(screen.queryByText("My Votes")).toBeNull(); }); + it("shows My Votes for curation waves", () => { + mockWaveInfo = { + isChatWave: false, + isMemesWave: false, + isCurationWave: true, + isRankWave: false, + }; + mockAvailableTabs = [ + MyStreamWaveTab.CHAT, + MyStreamWaveTab.MY_VOTES, + MyStreamWaveTab.LEADERBOARD, + ]; + renderComponent(MyStreamWaveTab.MY_VOTES); + + expect(screen.getByText("My Votes")).toBeInTheDocument(); + expect(setActiveTab).not.toHaveBeenCalled(); + }); + + it("keeps FAQ hidden outside memes waves", () => { + mockWaveInfo = { + isChatWave: false, + isMemesWave: false, + isCurationWave: true, + isRankWave: false, + }; + mockAvailableTabs = [MyStreamWaveTab.CHAT, MyStreamWaveTab.FAQ]; + renderComponent(MyStreamWaveTab.FAQ); + + expect(screen.queryByText("FAQ")).toBeNull(); + expect(setActiveTab).toHaveBeenCalledWith(MyStreamWaveTab.CHAT); + }); + it("does not render countdown; parent header handles it", () => { const spy = jest.spyOn(Time, "currentMillis").mockReturnValue(0); mockWaveInfo = { @@ -147,7 +179,6 @@ describe("MyStreamWaveDesktopTabs", () => { expect(updateAvailableTabs).toHaveBeenCalledWith( expect.objectContaining({ - waveId: expect.anything(), isChatWave: false, isMemesWave: false, isCurationWave: true, diff --git a/__tests__/components/brain/my-stream/votes/MyStreamWaveMyVote.test.tsx b/__tests__/components/brain/my-stream/votes/MyStreamWaveMyVote.test.tsx index 2ab6e1a40f..b9170b5733 100644 --- a/__tests__/components/brain/my-stream/votes/MyStreamWaveMyVote.test.tsx +++ b/__tests__/components/brain/my-stream/votes/MyStreamWaveMyVote.test.tsx @@ -1,9 +1,15 @@ import MyStreamWaveMyVote from "@/components/brain/my-stream/votes/MyStreamWaveMyVote"; -import { fireEvent, render } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; + +const mockMediaDisplay = jest.fn(); +const mockIsCurationWave = jest.fn(() => false); jest.mock("@/components/drops/view/item/content/media/MediaDisplay", () => ({ __esModule: true, - default: () =>
, + default: (props: any) => { + mockMediaDisplay(props); + return
; + }, })); jest.mock("@/components/brain/my-stream/votes/MyStreamWaveMyVoteVotes", () => ({ @@ -30,16 +36,32 @@ jest.mock("@/components/waves/drop/SingleWaveDropPosition", () => ({ default: ({ rank }: any) =>
{rank}
, })); +jest.mock("@/contexts/SeizeSettingsContext", () => ({ + useSeizeSettings: () => ({ + isCurationWave: mockIsCurationWave, + }), +})); + describe("MyStreamWaveMyVote", () => { const drop: any = { id: "d1", title: "Drop Title", parts: [{ media: [{ url: "a", mime_type: "image/jpeg" }] }], + metadata: [], + wave: { id: "w1" }, + nft_links: [], + top_raters: [], author: { handle: "alice", cic: 1, level: 2 }, rating: 0, raters_count: 3, }; + beforeEach(() => { + mockMediaDisplay.mockClear(); + mockIsCurationWave.mockReset(); + mockIsCurationWave.mockReturnValue(false); + }); + it("triggers onDropClick when no text selected", () => { const onDropClick = jest.fn(); (globalThis.getSelection as any) = () => ({ toString: () => "" }); @@ -75,4 +97,96 @@ describe("MyStreamWaveMyVote", () => { fireEvent.click(checkbox!); expect(onToggleCheck).toHaveBeenCalledWith("d1"); }); + + it("uses curation nft preview media when drop has no attached media", () => { + mockIsCurationWave.mockReturnValue(true); + const curationDrop = { + ...drop, + wave: { id: "W1" }, + parts: [{ media: [] }], + nft_links: [ + { + url_in_text: "https://opensea.io/assets/ethereum/0xabc/1", + data: { + media_uri: "https://cdn.example.com/fallback.png", + media_preview: { + status: "READY", + card_url: "https://cdn.example.com/card.webp", + small_url: "https://cdn.example.com/small.jpg", + thumb_url: "https://cdn.example.com/thumb.jpg", + mime_type: "image/webp", + }, + }, + }, + ], + }; + + render(); + + expect(screen.getByTestId("media")).toBeInTheDocument(); + expect(mockIsCurationWave).toHaveBeenCalledWith("w1"); + const props = mockMediaDisplay.mock.calls.at(-1)?.[0]; + expect(props?.media_url).toBe("https://cdn.example.com/card.webp"); + expect(props?.media_mime_type).toBe("image/webp"); + }); + + it("falls back to media_uri when preview status is not ready", () => { + mockIsCurationWave.mockReturnValue(true); + const curationDrop = { + ...drop, + parts: [{ media: [] }], + nft_links: [ + { + url_in_text: "https://opensea.io/assets/ethereum/0xabc/1", + data: { + media_uri: "https://cdn.example.com/fallback.png", + media_preview: { + status: "PROCESSING", + card_url: "https://cdn.example.com/card.webp", + small_url: "https://cdn.example.com/small.jpg", + thumb_url: "https://cdn.example.com/thumb.jpg", + mime_type: "image/webp", + }, + }, + }, + ], + }; + + render(); + + expect(screen.getByTestId("media")).toBeInTheDocument(); + const props = mockMediaDisplay.mock.calls.at(-1)?.[0]; + expect(props?.media_url).toBe("https://cdn.example.com/fallback.png"); + expect(props?.media_mime_type).toBe("image/png"); + }); + + it("does not use nft preview media outside curation waves", () => { + mockIsCurationWave.mockReturnValue(false); + const nonCurationDrop = { + ...drop, + parts: [{ media: [] }], + nft_links: [ + { + url_in_text: "https://opensea.io/assets/ethereum/0xabc/1", + data: { + media_uri: "https://cdn.example.com/fallback.png", + media_preview: { + status: "READY", + card_url: "https://cdn.example.com/card.webp", + small_url: "https://cdn.example.com/small.jpg", + thumb_url: "https://cdn.example.com/thumb.jpg", + mime_type: "image/webp", + }, + }, + }, + ], + }; + + render( + + ); + + expect(screen.queryByTestId("media")).not.toBeInTheDocument(); + expect(mockMediaDisplay).not.toHaveBeenCalled(); + }); }); diff --git a/components/brain/BrainMobile.tsx b/components/brain/BrainMobile.tsx index 3a4d195bc5..d0e8207877 100644 --- a/components/brain/BrainMobile.tsx +++ b/components/brain/BrainMobile.tsx @@ -107,7 +107,7 @@ const BrainMobile: React.FC = ({ children }) => { }, }); - const { isMemesWave } = useWave(wave); + const { isMemesWave, isCurationWave } = useWave(wave); const { voting: { isCompleted }, @@ -208,7 +208,7 @@ const BrainMobile: React.FC = ({ children }) => { const shouldResetToDefault = (activeView === BrainView.LEADERBOARD && isCompleted) || (activeView === BrainView.WINNERS && !firstDecisionDone) || - (activeView === BrainView.MY_VOTES && !isMemesWave) || + (activeView === BrainView.MY_VOTES && !isMemesWave && !isCurationWave) || (activeView === BrainView.FAQ && !isMemesWave); if (shouldResetToDefault) { @@ -232,6 +232,7 @@ const BrainMobile: React.FC = ({ children }) => { firstDecisionDone, activeView, isMemesWave, + isCurationWave, waveId, pathname, ]); diff --git a/components/brain/mobile/BrainMobileTabs.tsx b/components/brain/mobile/BrainMobileTabs.tsx index 5c293a5ed7..8e717e94ff 100644 --- a/components/brain/mobile/BrainMobileTabs.tsx +++ b/components/brain/mobile/BrainMobileTabs.tsx @@ -51,7 +51,7 @@ const BrainMobileTabs: React.FC = ({ [registerRef] ); - const { isMemesWave, isRankWave } = useWave(wave); + const { isMemesWave, isCurationWave, isRankWave } = useWave(wave); // Get unread indicator for messages const { hasUnread: hasUnreadMessages } = useUnreadIndicator({ @@ -158,8 +158,9 @@ const BrainMobileTabs: React.FC = ({ return (
-
+ className="tw-overflow-x-auto tw-px-2 tw-py-2 sm:tw-px-4 md:tw-px-6" + > +
{waveActive && showStreamBack && !isApp && ( <> {/* Divider */} -
+
)} {!waveActive && showWavesTab && ( @@ -185,7 +187,8 @@ const BrainMobileTabs: React.FC = ({ tabRefs.current[BrainView.WAVES] = el; }} onClick={() => onViewChange(BrainView.WAVES)} - className={wavesButtonClasses}> + className={wavesButtonClasses} + > Waves )} @@ -195,11 +198,12 @@ const BrainMobileTabs: React.FC = ({ tabRefs.current[BrainView.MESSAGES] = el; }} onClick={() => onViewChange(BrainView.MESSAGES)} - className={messagesButtonClasses}> + className={messagesButtonClasses} + > Messages {hasUnreadMessages && ( -
+
)}
@@ -209,7 +213,8 @@ const BrainMobileTabs: React.FC = ({ tabRefs.current[BrainView.DEFAULT] = el; }} onClick={onChatClick} - className={chatButtonClasses}> + className={chatButtonClasses} + > {waveActive ? "Chat" : "My Stream"} @@ -220,7 +225,8 @@ const BrainMobileTabs: React.FC = ({ tabRefs.current[BrainView.ABOUT] = el; }} onClick={() => onViewChange(BrainView.ABOUT)} - className={aboutButtonClasses}> + className={aboutButtonClasses} + > About )} @@ -234,14 +240,15 @@ const BrainMobileTabs: React.FC = ({ tabRefs.current[view] = el; }} /> - {isMemesWave && ( + {(isMemesWave || isCurationWave) && ( <> @@ -251,7 +258,8 @@ const BrainMobileTabs: React.FC = ({ tabRefs.current[BrainView.OUTCOME] = el; }} onClick={() => onViewChange(BrainView.OUTCOME)} - className={outcomeButtonClasses}> + className={outcomeButtonClasses} + > Outcome {isMemesWave && ( @@ -260,17 +268,19 @@ const BrainMobileTabs: React.FC = ({ tabRefs.current[BrainView.FAQ] = el; }} onClick={() => onViewChange(BrainView.FAQ)} - className={`tw-border-none tw-no-underline tw-flex tw-justify-center tw-items-center tw-px-2 tw-py-1.5 tw-gap-1 tw-flex-1 tw-rounded-md ${ + className={`tw-flex tw-flex-1 tw-items-center tw-justify-center tw-gap-1 tw-rounded-md tw-border-none tw-px-2 tw-py-1.5 tw-no-underline ${ activeView === BrainView.FAQ ? "tw-bg-iron-800" : "tw-bg-iron-950" - }`}> + }`} + > + }`} + > FAQ @@ -283,11 +293,12 @@ const BrainMobileTabs: React.FC = ({ tabRefs.current[BrainView.NOTIFICATIONS] = el; }} onClick={onNotificationsClick} - className={notificationsButtonClasses}> + className={notificationsButtonClasses} + > Notifications {haveUnreadNotifications && ( -
+
)}
diff --git a/components/brain/my-stream/MyStreamWaveDesktopTabs.tsx b/components/brain/my-stream/MyStreamWaveDesktopTabs.tsx index f545ce19c1..1d7546bbdb 100644 --- a/components/brain/my-stream/MyStreamWaveDesktopTabs.tsx +++ b/components/brain/my-stream/MyStreamWaveDesktopTabs.tsx @@ -28,6 +28,15 @@ const getContentTabPanelId = (tab: MyStreamWaveTab): string => const AUTO_EXPAND_LIMIT = 5; +const TAB_LABELS: Record = { + [MyStreamWaveTab.CHAT]: "Chat", + [MyStreamWaveTab.LEADERBOARD]: "Leaderboard", + [MyStreamWaveTab.WINNERS]: "Winners", + [MyStreamWaveTab.OUTCOME]: "Outcome", + [MyStreamWaveTab.MY_VOTES]: "My Votes", + [MyStreamWaveTab.FAQ]: "FAQ", +}; + const MyStreamWaveDesktopTabs: React.FC = ({ activeTab, wave, @@ -156,41 +165,35 @@ const MyStreamWaveDesktopTabs: React.FC = ({ } }, [wave?.wave?.type, setActiveContentTab]); - // Map enum values to label names - const tabLabels: Record = { - [MyStreamWaveTab.CHAT]: "Chat", - [MyStreamWaveTab.LEADERBOARD]: "Leaderboard", - [MyStreamWaveTab.WINNERS]: "Winners", - [MyStreamWaveTab.OUTCOME]: "Outcome", - [MyStreamWaveTab.MY_VOTES]: "My Votes", - [MyStreamWaveTab.FAQ]: "FAQ", - }; - const options: TabOption[] = React.useMemo( () => availableTabs - .filter( - (tab) => - isMemesWave || - ![MyStreamWaveTab.MY_VOTES, MyStreamWaveTab.FAQ].includes(tab) - ) + .filter((tab) => { + if (tab === MyStreamWaveTab.MY_VOTES) { + return isMemesWave || isCurationWave; + } + if (tab === MyStreamWaveTab.FAQ) { + return isMemesWave; + } + return true; + }) .map((tab) => ({ key: tab, - label: tabLabels[tab], + label: TAB_LABELS[tab], panelId: getContentTabPanelId(tab), })), - [availableTabs, isMemesWave] + [availableTabs, isMemesWave, isCurationWave] ); useEffect(() => { - if ( - !isMemesWave && - [MyStreamWaveTab.MY_VOTES, MyStreamWaveTab.FAQ].includes(activeTab) && - options.length > 0 - ) { + const isMyVotesHidden = + activeTab === MyStreamWaveTab.MY_VOTES && !isMemesWave && !isCurationWave; + const isFaqHidden = activeTab === MyStreamWaveTab.FAQ && !isMemesWave; + + if ((isMyVotesHidden || isFaqHidden) && options.length > 0) { setActiveTab(options[0]?.key!); } - }, [isMemesWave, activeTab, options, setActiveTab]); + }, [isMemesWave, isCurationWave, activeTab, options, setActiveTab]); // For simple waves, don't render any tabs if (isChatWave) { diff --git a/components/brain/my-stream/votes/MyStreamWaveMyVote.tsx b/components/brain/my-stream/votes/MyStreamWaveMyVote.tsx index f4fe308b15..a3a2a5d3b9 100644 --- a/components/brain/my-stream/votes/MyStreamWaveMyVote.tsx +++ b/components/brain/my-stream/votes/MyStreamWaveMyVote.tsx @@ -1,5 +1,7 @@ import MediaTypeBadge from "@/components/drops/media/MediaTypeBadge"; import MediaDisplay from "@/components/drops/view/item/content/media/MediaDisplay"; +import { useSeizeSettings } from "@/contexts/SeizeSettingsContext"; +import { ApiNftLinkMediaPreviewStatusEnum } from "@/generated/models/ApiNftLinkMediaPreview"; import UserCICAndLevel, { UserCICAndLevelSize, } from "@/components/user/utils/UserCICAndLevel"; @@ -23,6 +25,109 @@ interface MyStreamWaveMyVoteProps { readonly isResetting?: boolean | undefined; } +type ResolvedPreviewMedia = { + readonly url: string; + readonly mimeType: string; +}; + +const DEFAULT_MIME_TYPE = "image/jpeg"; + +const MIME_BY_EXTENSION: Record = { + avif: "image/avif", + gif: "image/gif", + jpeg: DEFAULT_MIME_TYPE, + jpg: DEFAULT_MIME_TYPE, + m4v: "video/x-m4v", + mov: "video/quicktime", + mp3: "audio/mpeg", + mp4: "video/mp4", + ogg: "audio/ogg", + ogv: "video/ogg", + flac: "audio/flac", + png: "image/png", + svg: "image/svg+xml", + wav: "audio/wav", + webm: "video/webm", + webp: "image/webp", + glb: "model/gltf-binary", + gltf: "model/gltf+json", + usdz: "model/vnd.usdz", +}; + +const toNonEmptyString = (value: unknown): string | null => { + if (typeof value !== "string") { + return null; + } + + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +}; + +const inferMimeTypeFromUrl = (url: string): string | undefined => { + try { + const parsed = new URL(url); + const extensionMatch = parsed.pathname + .toLowerCase() + .match(/\.([a-z0-9]+)$/); + if (!extensionMatch?.[1]) { + return undefined; + } + + return MIME_BY_EXTENSION[extensionMatch[1]]; + } catch { + const path = url.split("?")[0]?.toLowerCase() ?? ""; + const extensionMatch = path.match(/\.([a-z0-9]+)$/); + if (!extensionMatch?.[1]) { + return undefined; + } + + return MIME_BY_EXTENSION[extensionMatch[1]]; + } +}; + +const resolveCurationPreviewMedia = ( + nftLinks: ExtendedDrop["nft_links"] +): ResolvedPreviewMedia | null => { + if (typeof nftLinks?.length !== "number" || nftLinks.length === 0) { + return null; + } + + for (const nftLink of nftLinks) { + const preview = nftLink.data?.media_preview; + const previewMimeType = toNonEmptyString(preview?.mime_type); + const previewUrl = + preview?.status === ApiNftLinkMediaPreviewStatusEnum.Ready + ? (toNonEmptyString(preview.card_url) ?? + toNonEmptyString(preview.small_url) ?? + toNonEmptyString(preview.thumb_url)) + : null; + if (previewUrl) { + return { + url: previewUrl, + mimeType: + previewMimeType ?? + inferMimeTypeFromUrl(previewUrl) ?? + DEFAULT_MIME_TYPE, + }; + } + + const fallbackUrl = toNonEmptyString(nftLink.data?.media_uri); + if (!fallbackUrl) { + continue; + } + + return { + url: fallbackUrl, + mimeType: + inferMimeTypeFromUrl(fallbackUrl) ?? + previewMimeType ?? + DEFAULT_MIME_TYPE, + }; + } + + return null; +}; + const MyStreamWaveMyVote: React.FC = ({ drop, onDropClick, @@ -30,11 +135,23 @@ const MyStreamWaveMyVote: React.FC = ({ onToggleCheck, isResetting = false, }) => { + const { isCurationWave } = useSeizeSettings(); const artWork = drop.parts.at(0)?.media.at(0); const previewImageUrl = useMemo( () => getDropPreviewImageUrl(drop.metadata), [drop.metadata] ); + const curationPreviewMedia = useMemo( + () => + !artWork && isCurationWave(drop.wave.id.toLowerCase()) + ? resolveCurationPreviewMedia(drop.nft_links) + : null, + [artWork, drop.nft_links, drop.wave.id, isCurationWave] + ); + const resolvedMediaUrl = artWork?.url ?? curationPreviewMedia?.url ?? null; + const resolvedMediaMimeType = + artWork?.mime_type ?? curationPreviewMedia?.mimeType ?? DEFAULT_MIME_TYPE; + const badgeMimeType = artWork?.mime_type ?? curationPreviewMedia?.mimeType; const handleClick = () => { if (window.getSelection()?.toString()) { @@ -93,10 +210,10 @@ const MyStreamWaveMyVote: React.FC = ({
- {artWork && ( + {resolvedMediaUrl && ( = ({
@@ -118,7 +235,9 @@ const MyStreamWaveMyVote: React.FC = ({ {drop.title}
- {drop.rank && } + {typeof drop.rank === "number" && ( + + )}
@@ -136,11 +255,14 @@ const MyStreamWaveMyVote: React.FC = ({ user={drop.author.handle ?? drop.author.id} > { e.stopPropagation(); e.preventDefault(); - window.open(`/${drop.author.handle}`, "_blank"); + window.open( + `/${drop.author.handle ?? drop.author.primary_address}`, + "_blank" + ); }} className="tw-truncate tw-text-md tw-font-medium tw-text-iron-200 tw-no-underline tw-transition-colors tw-duration-200 desktop-hover:hover:tw-text-opacity-80 desktop-hover:hover:tw-underline" > @@ -159,12 +281,14 @@ const MyStreamWaveMyVote: React.FC = ({
- {drop.top_raters?.slice(0, 3).map((voter) => ( - + {drop.top_raters.slice(0, 3).map((voter) => ( + e.stopPropagation()} - data-tooltip-id={`my-vote-voter-${drop.id}-${voter.profile.handle}`} + data-tooltip-id={`my-vote-voter-${drop.id}-${voter.profile.handle ?? voter.profile.primary_address}`} > {voter.profile.pfp ? ( = ({ )} = ({ pointerEvents: "none", }} > - {voter.profile.handle} -{" "} - {formatNumberWithCommas(voter.rating)} + {voter.profile.handle ?? voter.profile.primary_address}{" "} + - {formatNumberWithCommas(voter.rating)} ))} diff --git a/components/profile-activity/ProfileName.tsx b/components/profile-activity/ProfileName.tsx deleted file mode 100644 index 08175ce44b..0000000000 --- a/components/profile-activity/ProfileName.tsx +++ /dev/null @@ -1,46 +0,0 @@ -"use client"; - -import type { ApiIdentity } from "@/generated/models/ApiIdentity"; -import { assertUnreachable } from "@/helpers/AllowlistToolHelpers"; -import { createPossessionStr } from "@/helpers/Helpers"; -import { useIdentity } from "@/hooks/useIdentity"; -import { useParams } from "next/navigation"; -import { useEffect, useState } from "react"; -export enum ProfileNameType { - POSSESSION = "POSSESSION", - DEFAULT = "DEFAULT", -} - -export default function ProfileName({ - type, -}: { - readonly type: ProfileNameType; -}) { - const params = useParams(); - const handleOrWallet = params?.["user"]?.toString().toLowerCase(); - - const { profile } = useIdentity({ - handleOrWallet: handleOrWallet, - initialProfile: null, - }); - - const createName = (profile: ApiIdentity | null): string => { - switch (type) { - case ProfileNameType.POSSESSION: - return createPossessionStr(profile?.handle ?? null); - case ProfileNameType.DEFAULT: - return profile?.handle ?? ""; - default: - assertUnreachable(type); - return ""; - } - }; - - const [name, setName] = useState(createName(profile ?? null)); - - useEffect(() => { - setName(createName(profile ?? null)); - }, [profile]); - - return <>{name}; -} diff --git a/components/react-query-wrapper/ReactQueryWrapper.tsx b/components/react-query-wrapper/ReactQueryWrapper.tsx index 233d7e4d38..4086e75832 100644 --- a/components/react-query-wrapper/ReactQueryWrapper.tsx +++ b/components/react-query-wrapper/ReactQueryWrapper.tsx @@ -16,7 +16,7 @@ import { convertActivityLogParams } from "@/helpers/profile-logs.helpers"; import { Time } from "@/helpers/time"; import type { CountlessPage, Page } from "@/helpers/Types"; import { useQueryKeyListener } from "@/hooks/useQueryKeyListener"; -import { RateMatter } from "@/types/enums"; +import { type ProfileRatersParamsOrderBy, RateMatter } from "@/types/enums"; import { type InfiniteData, @@ -26,7 +26,6 @@ import { import Cookies from "js-cookie"; import { createContext, useMemo } from "react"; import type { ActivityLogParams } from "../profile-activity/ProfileActivityLogs"; -import type { ProfileRatersParams } from "../user/utils/raters-table/wrapper/ProfileRatersTableWrapper"; import { addDropToDrops } from "./utils/addDropsToDrops"; import { increaseWavesOverviewDropsCount } from "./utils/increaseWavesOverviewDropsCount"; import { @@ -34,6 +33,7 @@ import { WAVE_FOLLOWING_WAVES_PARAMS, } from "./utils/query-utils"; import { toggleWaveFollowing } from "./utils/toggleWaveFollowing"; +import type { SortDirection } from "@/entities/ISort"; export enum QueryKey { PROFILE = "PROFILE", @@ -108,6 +108,16 @@ export enum QueryKey { MARKETPLACE_PREVIEW = "MARKETPLACE_PREVIEW", } +interface ProfileRatersParams { + readonly page: number; + readonly pageSize: number; + readonly given: boolean; + readonly order: SortDirection; + readonly orderBy: ProfileRatersParamsOrderBy; + readonly handleOrWallet: string; + readonly matter: RateMatter; +} + interface InitProfileRatersParamsAndData { readonly data: Page; readonly params: ProfileRatersParams; @@ -787,7 +797,10 @@ const createReactQueryContextValue = ( ( oldData: { pages: Page[]; pageParams: number[] } | undefined ): { pages: Page[]; pageParams: number[] } => { - if (!oldData?.pages.length) { + if ( + typeof oldData?.pages.length !== "number" || + oldData.pages.length === 0 + ) { return { pageParams: [1], pages: [ diff --git a/components/user/rep/new-rep/UserPageRepNewRepSearchHeader.tsx b/components/user/rep/new-rep/UserPageRepNewRepSearchHeader.tsx deleted file mode 100644 index b7528570b9..0000000000 --- a/components/user/rep/new-rep/UserPageRepNewRepSearchHeader.tsx +++ /dev/null @@ -1,138 +0,0 @@ -"use client"; - -import { useContext, useEffect, useState } from "react"; -import type { ApiProfileRepRatesState } from "@/entities/IProfile"; -import { formatNumberWithCommas } from "@/helpers/Helpers"; -import { AuthContext } from "@/components/auth/Auth"; -import { ApiProfileProxyActionType } from "@/generated/models/ApiProfileProxyActionType"; -import { useQuery } from "@tanstack/react-query"; -import { commonApiFetch } from "@/services/api/common-api"; -import Link from "next/link"; -import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper"; -import type { ApiIdentity } from "@/generated/models/ApiIdentity"; -export default function UserPageRepNewRepSearchHeader({ - repRates, - profile, -}: { - readonly repRates: ApiProfileRepRatesState | null; - readonly profile: ApiIdentity; -}) { - const { activeProfileProxy } = useContext(AuthContext); - - const { data: proxyGrantorRepRates } = useQuery({ - queryKey: [ - QueryKey.PROFILE_REP_RATINGS, - { - rater: activeProfileProxy?.created_by.handle, - handleOrWallet: profile?.handle, - }, - ], - queryFn: async () => - await commonApiFetch({ - endpoint: `profiles/${profile?.query}/rep/ratings/received`, - params: activeProfileProxy?.created_by.handle - ? { rater: activeProfileProxy.created_by.handle } - : {}, - }), - enabled: !!activeProfileProxy?.created_by.handle, - }); - - const getActiveRepRates = (): { - available: number; - rated: number; - proxyCreditLeft: number | null; - } => { - const repProxy = activeProfileProxy?.actions.find( - (a) => a.action_type === ApiProfileProxyActionType.AllocateRep - ); - if (!repProxy) { - return { - available: repRates?.rep_rates_left_for_rater ?? 0, - rated: repRates?.total_rep_rating_by_rater ?? 0, - proxyCreditLeft: null, - }; - } - if (!proxyGrantorRepRates) { - return { - available: 0, - rated: 0, - proxyCreditLeft: null, - }; - } - const proxyGrantorAvailableRep = - proxyGrantorRepRates.rep_rates_left_for_rater ?? 0; - const proxyGrantorTotalRep = - proxyGrantorRepRates.total_rep_rating_by_rater ?? 0; - return { - available: proxyGrantorAvailableRep, - rated: proxyGrantorTotalRep, - proxyCreditLeft: Math.max( - 0, - (repProxy.credit_amount ?? 0) - (repProxy.credit_spent ?? 0) - ), - }; - }; - - const [activeRepRates, setActiveRepRates] = useState<{ - available: number; - rated: number; - proxyCreditLeft: number | null; - }>(getActiveRepRates()); - - useEffect( - () => setActiveRepRates(getActiveRepRates()), - [activeProfileProxy, proxyGrantorRepRates, repRates] - ); - - const getAvailableCredit = (): number => { - if (!activeProfileProxy) { - return activeRepRates.available; - } - return Math.abs(activeRepRates.available) < - Math.abs(activeRepRates.proxyCreditLeft ?? 0) - ? activeRepRates.available - : activeRepRates.proxyCreditLeft ?? 0; - }; - - const [availableCredit, setAvailableCredit] = useState(getAvailableCredit()); - useEffect( - () => setAvailableCredit(getAvailableCredit()), - [activeRepRates, activeRepRates.proxyCreditLeft] - ); - return ( -
- {!!activeProfileProxy && ( - - You are acting as proxy for: - - - {activeProfileProxy.created_by.handle} - - - - )} -
- {!activeProfileProxy && ( - <> - - Your available Rep: - - {formatNumberWithCommas(availableCredit)} - - - - - )} - - - Your Rep assigned to{" "} - {profile.query ?? profile.handle ?? profile.display}: - - - {repRates ? formatNumberWithCommas(activeRepRates.rated) : ""} - - -
-
- ); -} diff --git a/components/user/utils/UserTableHeaderWrapper.tsx b/components/user/utils/UserTableHeaderWrapper.tsx deleted file mode 100644 index 6cb2b1d44a..0000000000 --- a/components/user/utils/UserTableHeaderWrapper.tsx +++ /dev/null @@ -1,13 +0,0 @@ -export default function UserTableHeaderWrapper({ - children, -}: { - readonly children: React.ReactNode; -}) { - return ( -
-

- {children} -

-
- ); -} diff --git a/components/user/utils/raters-table/ProfileRatersTable.tsx b/components/user/utils/raters-table/ProfileRatersTable.tsx deleted file mode 100644 index 17adedc30a..0000000000 --- a/components/user/utils/raters-table/ProfileRatersTable.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import CommonTablePagination from "@/components/utils/table/paginator/CommonTablePagination"; -import type { RatingWithProfileInfoAndLevel } from "@/entities/IProfile"; -import type { SortDirection } from "@/entities/ISort"; -import type { - ProfileRatersParamsOrderBy, - ProfileRatersTableType, -} from "@/types/enums"; -import ProfileRatersTableBody from "./ProfileRatersTableBody"; -import ProfileRatersTableHeader from "./ProfileRatersTableHeader"; - -export default function ProfileRatersTable({ - ratings, - type, - order, - orderBy, - loading, - totalPages, - currentPage, - setCurrentPage, - onSortTypeClick, - noRatingsMessage, -}: { - readonly ratings: RatingWithProfileInfoAndLevel[]; - readonly type: ProfileRatersTableType; - readonly order: SortDirection; - readonly orderBy: ProfileRatersParamsOrderBy; - readonly loading: boolean; - readonly totalPages: number; - readonly currentPage: number; - readonly setCurrentPage: (page: number) => void; - readonly onSortTypeClick: (type: ProfileRatersParamsOrderBy) => void; - readonly noRatingsMessage: string; -}) { - return ( - <> - {ratings.length ? ( -
-
- - - -
-
- - {totalPages > 1 && ( - - )} -
- ) : ( -
- - {noRatingsMessage} - -
- )} - - ); -} diff --git a/components/user/utils/raters-table/ProfileRatersTableBody.tsx b/components/user/utils/raters-table/ProfileRatersTableBody.tsx deleted file mode 100644 index 55169d2fde..0000000000 --- a/components/user/utils/raters-table/ProfileRatersTableBody.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { RatingWithProfileInfoAndLevel } from "@/entities/IProfile"; -import { getRandomObjectId } from "@/helpers/AllowlistToolHelpers"; -import ProfileRatersTableItem from "./ProfileRatersTableItem"; - -export default function ProfileRatersTableBody({ - ratings, -}: { - readonly ratings: RatingWithProfileInfoAndLevel[]; -}) { - return ( - - {ratings.map((rating) => ( - - ))} - - ); -} diff --git a/components/user/utils/raters-table/ProfileRatersTableHeader.tsx b/components/user/utils/raters-table/ProfileRatersTableHeader.tsx deleted file mode 100644 index e92b17e2c5..0000000000 --- a/components/user/utils/raters-table/ProfileRatersTableHeader.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import type { SortDirection } from "@/entities/ISort"; -import { assertUnreachable } from "@/helpers/AllowlistToolHelpers"; -import { - ProfileRatersParamsOrderBy, - ProfileRatersTableType, -} from "@/types/enums"; -import ProfileRatersTableHeaderSortableCell from "./ProfileRatersTableHeaderSortableCell"; - -export default function ProfileRatersTableHeader({ - type, - sortDirection, - sortOrderBy, - isLoading, - onSortTypeClick, -}: { - readonly type: ProfileRatersTableType; - readonly sortDirection: SortDirection; - readonly sortOrderBy: ProfileRatersParamsOrderBy; - readonly isLoading: boolean; - readonly onSortTypeClick: (newSortType: ProfileRatersParamsOrderBy) => void; -}) { - const getTotalTitle = (): string => { - switch (type) { - case ProfileRatersTableType.CIC_RECEIVED: - case ProfileRatersTableType.CIC_GIVEN: - return "Total NIC"; - case ProfileRatersTableType.REP_RECEIVED: - case ProfileRatersTableType.REP_GIVEN: - return "Total Rep"; - default: - assertUnreachable(type); - return ""; - } - }; - - return ( - - - - Name - - - - - - ); -} diff --git a/components/user/utils/raters-table/ProfileRatersTableHeaderSortableCell.tsx b/components/user/utils/raters-table/ProfileRatersTableHeaderSortableCell.tsx deleted file mode 100644 index cdbf1d4d37..0000000000 --- a/components/user/utils/raters-table/ProfileRatersTableHeaderSortableCell.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client"; - -import CircleLoader, { - CircleLoaderSize, -} from "@/components/distribution-plan-tool/common/CircleLoader"; -import { SortDirection } from "@/entities/ISort"; -import type { ProfileRatersParamsOrderBy } from "@/types/enums"; -import { useEffect, useState } from "react"; -import CommonTableSortIcon from "../icons/CommonTableSortIcon"; - -export default function ProfileRatersTableHeaderSortableCell({ - title, - sortType, - sortDirection, - sortOrderBy, - isLoading, - onSortTypeClick, -}: { - readonly title: string; - readonly sortType: ProfileRatersParamsOrderBy; - readonly sortDirection: SortDirection; - readonly sortOrderBy: ProfileRatersParamsOrderBy; - readonly isLoading: boolean; - readonly onSortTypeClick: (newSortType: ProfileRatersParamsOrderBy) => void; -}) { - const [isActive, setIsActive] = useState(sortType === sortOrderBy); - useEffect(() => { - setIsActive(sortType === sortOrderBy); - }, [sortType, sortOrderBy]); - - return ( - onSortTypeClick(sortType)} - className="tw-group tw-cursor-pointer tw-whitespace-nowrap tw-px-4 tw-py-3.5 tw-text-right tw-text-[11px] tw-font-bold tw-text-iron-500 tw-uppercase tw-tracking-widest sm:tw-px-6 lg:tw-pl-4" - > - - {title} - - {isLoading && isActive ? ( - - - - ) : ( - - - - )} - - ); -} diff --git a/components/user/utils/raters-table/ProfileRatersTableItem.tsx b/components/user/utils/raters-table/ProfileRatersTableItem.tsx deleted file mode 100644 index ec899a82a8..0000000000 --- a/components/user/utils/raters-table/ProfileRatersTableItem.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client"; - -import type { RatingWithProfileInfoAndLevel } from "@/entities/IProfile"; -import { formatNumberWithCommas, getTimeAgo } from "@/helpers/Helpers"; -import Link from "next/link"; -import UserCICAndLevel from "../UserCICAndLevel"; - -export default function ProfileRatersTableItem({ - rating, -}: { - readonly rating: RatingWithProfileInfoAndLevel; -}) { - const getRatingStr = (rating: number) => { - return rating > 0 - ? `+${formatNumberWithCommas(rating)}` - : `${formatNumberWithCommas(rating)}`; - }; - const ratingStr = getRatingStr(rating.rating); - const isPositiveRating = rating.rating > 0; - const ratingColor = isPositiveRating ? "tw-text-green" : "tw-text-red"; - const timeAgo = getTimeAgo(new Date(rating.last_modified).getTime()); - - const getProfileRoute = (): string => `/${rating.handle}/identity`; - - const profileRoute = getProfileRoute(); - - return ( - - -
- -
- - - {rating.handle} - - -
-
- - - - {ratingStr} - - - - - {timeAgo} - - - - ); -} diff --git a/components/user/utils/raters-table/wrapper/ProfileRatersTableWrapper.tsx b/components/user/utils/raters-table/wrapper/ProfileRatersTableWrapper.tsx deleted file mode 100644 index 7c5b61aed2..0000000000 --- a/components/user/utils/raters-table/wrapper/ProfileRatersTableWrapper.tsx +++ /dev/null @@ -1,165 +0,0 @@ -"use client"; - -import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper"; -import CommonSkeletonLoader from "@/components/utils/animation/CommonSkeletonLoader"; -import type { RatingWithProfileInfoAndLevel } from "@/entities/IProfile"; - -import { SortDirection } from "@/entities/ISort"; -import { assertUnreachable } from "@/helpers/AllowlistToolHelpers"; -import type { Page } from "@/helpers/Types"; -import { commonApiFetch } from "@/services/api/common-api"; -import type { ProfileRatersParamsOrderBy } from "@/types/enums"; -import { ProfileRatersTableType, RateMatter } from "@/types/enums"; -import { keepPreviousData, useQuery } from "@tanstack/react-query"; -import { useParams } from "next/navigation"; -import { useEffect, useState } from "react"; -import ProfileRatersTable from "../ProfileRatersTable"; -import ProfileRatersTableWrapperHeader from "./ProfileRatersTableWrapperHeader"; - -export interface ProfileRatersParams { - readonly page: number; - readonly pageSize: number; - readonly given: boolean; - readonly order: SortDirection; - readonly orderBy: ProfileRatersParamsOrderBy; - readonly handleOrWallet: string; - readonly matter: RateMatter; -} - -export default function ProfileRatersTableWrapper({ - initialParams, -}: { - readonly initialParams: ProfileRatersParams; -}) { - const params = useParams(); - const handleOrWallet = (params?.["user"] as string)?.toLowerCase(); - const pageSize = initialParams.pageSize; - const given = initialParams.given; - const matter = initialParams.matter; - - const [currentPage, setCurrentPage] = useState(initialParams.page); - const [order, setOrder] = useState(initialParams.order); - const [orderBy, setOrderBy] = useState( - initialParams.orderBy - ); - - const getType = (): ProfileRatersTableType => { - switch (matter) { - case RateMatter.NIC: - return given - ? ProfileRatersTableType.CIC_GIVEN - : ProfileRatersTableType.CIC_RECEIVED; - case RateMatter.REP: - case RateMatter.DROP_REP: - return given - ? ProfileRatersTableType.REP_GIVEN - : ProfileRatersTableType.REP_RECEIVED; - default: - assertUnreachable(matter); - return ProfileRatersTableType.CIC_RECEIVED; - } - }; - - const type = getType(); - - const { - isLoading, - isFetching, - data: ratings, - } = useQuery>({ - queryKey: [ - QueryKey.PROFILE_RATERS, - { - handleOrWallet, - matter, - page: `${currentPage}`, - pageSize: `${pageSize}`, - order, - orderBy, - given, - }, - ], - queryFn: async () => - await commonApiFetch>({ - endpoint: `profiles/${handleOrWallet}/${ - matter === RateMatter.NIC ? "cic" : matter.toLowerCase() - }/ratings/by-rater`, - params: { - page: `${currentPage}`, - page_size: `${pageSize}`, - order: order.toLowerCase(), - order_by: orderBy.toLowerCase(), - given: given ? "true" : "false", - }, - }), - enabled: !!handleOrWallet, - placeholderData: keepPreviousData, - }); - - const [totalPages, setTotalPages] = useState(1); - - useEffect(() => { - if (isLoading) return; - if (!ratings?.count) { - setCurrentPage(1); - setTotalPages(1); - return; - } - setTotalPages(Math.ceil(ratings.count / pageSize)); - }, [ratings?.count, ratings?.page, isLoading]); - - const getNoRatingsMessage = (): string => { - switch (type) { - case ProfileRatersTableType.CIC_RECEIVED: - return "No NIC received ratings"; - case ProfileRatersTableType.CIC_GIVEN: - return "No NIC given ratings"; - case ProfileRatersTableType.REP_RECEIVED: - return "No Rep received ratings"; - case ProfileRatersTableType.REP_GIVEN: - return "No Rep given ratings"; - default: - assertUnreachable(type); - return ""; - } - }; - - const onSortTypeClick = (newSortType: ProfileRatersParamsOrderBy) => { - if (newSortType === orderBy) { - setOrder((prev) => - prev === SortDirection.ASC ? SortDirection.DESC : SortDirection.ASC - ); - } else { - setOrderBy(newSortType); - setOrder(SortDirection.DESC); - } - }; - - return ( -
-
- -
-
- {isLoading ? ( -
- -
- ) : ( - - )} -
-
- ); -} diff --git a/components/user/utils/raters-table/wrapper/ProfileRatersTableWrapperHeader.tsx b/components/user/utils/raters-table/wrapper/ProfileRatersTableWrapperHeader.tsx deleted file mode 100644 index 926a205b6e..0000000000 --- a/components/user/utils/raters-table/wrapper/ProfileRatersTableWrapperHeader.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import ProfileName, { - ProfileNameType, -} from "@/components/profile-activity/ProfileName"; -import UserTableHeaderWrapper from "@/components/user/utils/UserTableHeaderWrapper"; -import { ProfileRatersTableType } from "@/types/enums"; - -export default function ProfileRatersTableWrapperHeader({ - type, -}: { - readonly type: ProfileRatersTableType; -}) { - return ( - - {type === ProfileRatersTableType.CIC_RECEIVED && ( -
- Who's NIC-Rating -
- )} - {type === ProfileRatersTableType.CIC_GIVEN && ( -
- Who's NIC-Rating -
- )} - {type === ProfileRatersTableType.REP_RECEIVED && ( -
- Who's Repping -
- )} - {type === ProfileRatersTableType.REP_GIVEN && ( -
- Who's Repping -
- )} -
- ); -} diff --git a/components/utils/CommonFilterTargetSelect.tsx b/components/utils/CommonFilterTargetSelect.tsx deleted file mode 100644 index a496a210fe..0000000000 --- a/components/utils/CommonFilterTargetSelect.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import { useId } from "react"; - -import { ProfileActivityFilterTargetType } from "@/types/enums"; - -const TARGETS = [ - { id: ProfileActivityFilterTargetType.ALL, name: "All" }, - { id: ProfileActivityFilterTargetType.OUTGOING, name: "Outgoing" }, - { id: ProfileActivityFilterTargetType.INCOMING, name: "Incoming" }, -]; - -export default function CommonFilterTargetSelect({ - selected, - onChange, -}: { - readonly selected: ProfileActivityFilterTargetType; - readonly onChange: (filter: ProfileActivityFilterTargetType) => void; -}) { - const baseId = useId().replaceAll(":", ""); - const groupName = `filter-target-${baseId}`; - - return ( -
- - Filter target - -
- {TARGETS.map((target) => { - const inputId = `${groupName}-${target.id}`; - - return ( -
- onChange(target.id)} - className="tw-form-radio tw-h-4 tw-w-4 tw-cursor-pointer tw-border tw-border-solid tw-border-iron-600 tw-bg-iron-700 tw-text-primary-400 tw-ring-offset-iron-800 focus:tw-ring-2 focus:tw-ring-primary-400" - /> - -
- ); - })} -
-
- ); -} diff --git a/components/utils/table/CommonTableWrapper.tsx b/components/utils/table/CommonTableWrapper.tsx deleted file mode 100644 index 40c393c4c7..0000000000 --- a/components/utils/table/CommonTableWrapper.tsx +++ /dev/null @@ -1,13 +0,0 @@ -export default function CommonTableWrapper({ - children, -}: { - readonly children: React.ReactNode; -}) { - return ( -
-
- {children}
-
-
- ); -} diff --git a/helpers/server.helpers.ts b/helpers/server.helpers.ts index 14f63d53e8..ea45fe3bfc 100644 --- a/helpers/server.helpers.ts +++ b/helpers/server.helpers.ts @@ -1,11 +1,7 @@ import type { ActivityLogParamsConverted } from "@/components/profile-activity/ProfileActivityLogs"; -import type { ProfileRatersParams } from "@/components/user/utils/raters-table/wrapper/ProfileRatersTableWrapper"; import type { ProfileActivityLog } from "@/entities/IProfile"; -import { SortDirection } from "@/entities/ISort"; import type { ApiIdentity } from "@/generated/models/ApiIdentity"; import { commonApiFetch } from "@/services/api/common-api"; -import type { RateMatter } from "@/types/enums"; -import { ProfileRatersParamsOrderBy } from "@/types/enums"; import type { Page } from "./Types"; export const getUserProfile = async ({ @@ -121,21 +117,3 @@ export const getUserProfileActivityLogs = async ({ }; } }; - -export const getInitialRatersParams = ({ - handleOrWallet, - given, - matter, -}: { - handleOrWallet: string; - given: boolean; - matter: RateMatter; -}): ProfileRatersParams => ({ - page: 1, - pageSize: 7, - given, - matter, - order: SortDirection.DESC, - orderBy: ProfileRatersParamsOrderBy.RATING, - handleOrWallet, -}); diff --git a/types/enums.ts b/types/enums.ts index 278f0c0098..acd51c0d19 100644 --- a/types/enums.ts +++ b/types/enums.ts @@ -156,13 +156,6 @@ export enum RateMatter { DROP_REP = "DROP_REP", } -export enum ProfileRatersTableType { - CIC_RECEIVED = "CIC_RECEIVED", - CIC_GIVEN = "CIC_GIVEN", - REP_RECEIVED = "REP_RECEIVED", - REP_GIVEN = "REP_GIVEN", -} - export enum ProfileRatersParamsOrderBy { RATING = "RATING", LAST_MODIFIED = "LAST_MODIFIED",