diff --git a/__tests__/components/waves/small-leaderboard/WaveSmallLeaderboardItemOutcomes.test.tsx b/__tests__/components/waves/small-leaderboard/WaveSmallLeaderboardItemOutcomes.test.tsx
index a7e08a5d5b..60f7d7eebc 100644
--- a/__tests__/components/waves/small-leaderboard/WaveSmallLeaderboardItemOutcomes.test.tsx
+++ b/__tests__/components/waves/small-leaderboard/WaveSmallLeaderboardItemOutcomes.test.tsx
@@ -1,29 +1,53 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { WaveSmallLeaderboardItemOutcomes } from '@/components/waves/small-leaderboard/WaveSmallLeaderboardItemOutcomes';
-import { ApiWaveOutcomeCredit } from '@/generated/models/ApiWaveOutcomeCredit';
-import { ApiWaveOutcomeType } from '@/generated/models/ApiWaveOutcomeType';
+const mockUseWaveRankReward = jest.fn();
+
+jest.mock('@/hooks/waves/useWaveRankReward', () => ({
+ useWaveRankReward: (args: any) => mockUseWaveRankReward(args),
+}));
describe('WaveSmallLeaderboardItemOutcomes', () => {
- const drop: any = { rank: 1 };
- const wave: any = {
- outcomes: [
- { credit: ApiWaveOutcomeCredit.Cic, distribution: [{ amount: 1 }] },
- { credit: ApiWaveOutcomeCredit.Rep, distribution: [{ amount: 2 }] },
- { type: ApiWaveOutcomeType.Manual, distribution: [{ amount: 1, description: 'Award' }] },
- ],
- };
+ const drop: any = { rank: 1, wave: { id: 'w1' } };
+
+ beforeEach(() => {
+ mockUseWaveRankReward.mockReset();
+ });
it('renders button when outcomes exist', () => {
- render();
+ mockUseWaveRankReward.mockReturnValue({
+ nicTotal: 10,
+ repTotal: 20,
+ manualOutcomes: ['Award'],
+ isLoading: false
+ });
+
+ render();
expect(screen.getByRole('button')).toBeInTheDocument();
expect(screen.getByText('Outcome:')).toBeInTheDocument();
});
- it('hides when no outcomes', () => {
- const waveEmpty = { outcomes: [] } as any;
- const { container } = render();
+ it('hides when no outcomes and not loading', () => {
+ mockUseWaveRankReward.mockReturnValue({
+ nicTotal: 0,
+ repTotal: 0,
+ manualOutcomes: [],
+ isLoading: false
+ });
+
+ const { container } = render();
expect(container.firstChild).toBeNull();
});
+
+ it('shows loading state', () => {
+ mockUseWaveRankReward.mockReturnValue({
+ nicTotal: 0,
+ repTotal: 0,
+ manualOutcomes: [],
+ isLoading: true
+ });
+ const { container } = render();
+ expect(container.querySelector('.tw-animate-pulse')).toBeInTheDocument();
+ });
});
diff --git a/components/react-query-wrapper/ReactQueryWrapper.tsx b/components/react-query-wrapper/ReactQueryWrapper.tsx
index c7c774b11f..9028dde3e5 100644
--- a/components/react-query-wrapper/ReactQueryWrapper.tsx
+++ b/components/react-query-wrapper/ReactQueryWrapper.tsx
@@ -95,6 +95,7 @@ export enum QueryKey {
WAVE_DECISIONS = "WAVE_DECISIONS",
WAVE_OUTCOMES = "WAVE_OUTCOMES",
WAVE_OUTCOME_DISTRIBUTION = "WAVE_OUTCOME_DISTRIBUTION",
+ WAVE_OUTCOME_DISTRIBUTION_PAGE = "WAVE_OUTCOME_DISTRIBUTION_PAGE",
}
interface InitProfileRatersParamsAndData {
diff --git a/components/waves/drop/SingleWaveDropInfoAuthorSection.tsx b/components/waves/drop/SingleWaveDropInfoAuthorSection.tsx
index 5d9f11674d..4321bfd862 100644
--- a/components/waves/drop/SingleWaveDropInfoAuthorSection.tsx
+++ b/components/waves/drop/SingleWaveDropInfoAuthorSection.tsx
@@ -23,7 +23,7 @@ export const SingleWaveDropInfoAuthorSection: React.FC<
{wave && drop && (
-
+
)}
);
diff --git a/components/waves/leaderboard/drops/DefaultWaveLeaderboardDrop.tsx b/components/waves/leaderboard/drops/DefaultWaveLeaderboardDrop.tsx
index 942da3a16d..fde6908bee 100644
--- a/components/waves/leaderboard/drops/DefaultWaveLeaderboardDrop.tsx
+++ b/components/waves/leaderboard/drops/DefaultWaveLeaderboardDrop.tsx
@@ -7,7 +7,6 @@ import WaveDropActionsOpen from "@/components/waves/drops/WaveDropActionsOpen";
import WaveDropActionsOptions from "@/components/waves/drops/WaveDropActionsOptions";
import WaveDropMobileMenuDelete from "@/components/waves/drops/WaveDropMobileMenuDelete";
import WaveDropMobileMenuOpen from "@/components/waves/drops/WaveDropMobileMenuOpen";
-import { ApiWave } from "@/generated/models/ObjectSerializer";
import { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import { useDropInteractionRules } from "@/hooks/drops/useDropInteractionRules";
import useIsMobileScreen from "@/hooks/isMobileScreen";
@@ -22,13 +21,12 @@ import { WaveLeaderboardDropRaters } from "./header/WaveleaderboardDropRaters";
interface DefaultWaveLeaderboardDropProps {
readonly drop: ExtendedDrop;
- readonly wave: ApiWave;
readonly onDropClick: (drop: ExtendedDrop) => void;
}
export const DefaultWaveLeaderboardDrop: React.FC<
DefaultWaveLeaderboardDropProps
-> = ({ drop, wave, onDropClick }) => {
+> = ({ drop, onDropClick }) => {
const { canShowVote, canDelete } = useDropInteractionRules(drop);
const [isVotingModalOpen, setIsVotingModalOpen] = useState(false);
const { hasTouchScreen } = useDeviceInfo();
@@ -87,7 +85,7 @@ export const DefaultWaveLeaderboardDrop: React.FC<
-
+
{canShowVote && (
= ({
return (
);
diff --git a/components/waves/leaderboard/drops/footer/WaveLeaderboardDropFooter.tsx b/components/waves/leaderboard/drops/footer/WaveLeaderboardDropFooter.tsx
index fde40edf7c..46e4f75ac8 100644
--- a/components/waves/leaderboard/drops/footer/WaveLeaderboardDropFooter.tsx
+++ b/components/waves/leaderboard/drops/footer/WaveLeaderboardDropFooter.tsx
@@ -1,15 +1,13 @@
import React from "react";
import { ExtendedDrop } from "@/helpers/waves/drop.helpers";
-import { ApiWave } from "@/generated/models/ApiWave";
import { WaveSmallLeaderboardItemOutcomes } from "@/components/waves/small-leaderboard/WaveSmallLeaderboardItemOutcomes";
interface WaveLeaderboardDropFooterProps {
readonly drop: ExtendedDrop;
- readonly wave: ApiWave;
}
export const WaveLeaderboardDropFooter: React.FC<
WaveLeaderboardDropFooterProps
-> = ({ drop, wave }) => {
- return
;
+> = ({ drop }) => {
+ return
;
};
diff --git a/components/waves/small-leaderboard/DefaultWaveSmallLeaderboardDrop.tsx b/components/waves/small-leaderboard/DefaultWaveSmallLeaderboardDrop.tsx
index d80e9e8c47..d12e9c68ef 100644
--- a/components/waves/small-leaderboard/DefaultWaveSmallLeaderboardDrop.tsx
+++ b/components/waves/small-leaderboard/DefaultWaveSmallLeaderboardDrop.tsx
@@ -2,29 +2,25 @@ import React from "react";
import { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import { WaveSmallLeaderboardTopThreeDrop } from "./WaveSmallLeaderboardTopThreeDrop";
import { WaveSmallLeaderboardDefaultDrop } from "./WaveSmallLeaderboardDefaultDrop";
-import { ApiWave } from "@/generated/models/ApiWave";
interface DefaultWaveSmallLeaderboardDropProps {
readonly drop: ExtendedDrop;
- readonly wave: ApiWave;
readonly onDropClick: (drop: ExtendedDrop) => void;
}
export const DefaultWaveSmallLeaderboardDrop: React.FC<
DefaultWaveSmallLeaderboardDropProps
-> = ({ drop, wave, onDropClick }) => {
+> = ({ drop, onDropClick }) => {
return (
onDropClick(drop)}>
{drop.rank && drop.rank <= 3 ? (
) : (
)}
diff --git a/components/waves/small-leaderboard/MemesWaveSmallLeaderboardDrop.tsx b/components/waves/small-leaderboard/MemesWaveSmallLeaderboardDrop.tsx
index b9338f4d94..5a33fa1ea5 100644
--- a/components/waves/small-leaderboard/MemesWaveSmallLeaderboardDrop.tsx
+++ b/components/waves/small-leaderboard/MemesWaveSmallLeaderboardDrop.tsx
@@ -2,27 +2,23 @@ import React from "react";
import { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import { WaveSmallLeaderboardTopThreeDrop } from "./WaveSmallLeaderboardTopThreeDrop";
import { WaveSmallLeaderboardDefaultDrop } from "./WaveSmallLeaderboardDefaultDrop";
-import { ApiWave } from "@/generated/models/ApiWave";
interface MemesWaveSmallLeaderboardDropProps {
readonly drop: ExtendedDrop;
- readonly wave: ApiWave;
readonly onDropClick: (drop: ExtendedDrop) => void;
}
-export const MemesWaveSmallLeaderboardDrop: React.FC
= ({ drop, wave, onDropClick }) => {
+export const MemesWaveSmallLeaderboardDrop: React.FC = ({ drop, onDropClick }) => {
return (
onDropClick(drop)}>
{drop.rank && drop.rank <= 3 ? (
) : (
)}
diff --git a/components/waves/small-leaderboard/WaveSmallLeaderboardDefaultDrop.tsx b/components/waves/small-leaderboard/WaveSmallLeaderboardDefaultDrop.tsx
index a582b421aa..745c4ff0c7 100644
--- a/components/waves/small-leaderboard/WaveSmallLeaderboardDefaultDrop.tsx
+++ b/components/waves/small-leaderboard/WaveSmallLeaderboardDefaultDrop.tsx
@@ -1,6 +1,5 @@
import React from "react";
import { ExtendedDrop } from "@/helpers/waves/drop.helpers";
-import { ApiWave } from "@/generated/models/ApiWave";
import Link from "next/link";
import { CICType } from "@/entities/IProfile";
import { cicToType, formatNumberWithCommas } from "@/helpers/Helpers";
@@ -14,13 +13,12 @@ import UserProfileTooltipWrapper from "@/components/utils/tooltip/UserProfileToo
interface WaveSmallLeaderboardDefaultDropProps {
readonly drop: ExtendedDrop;
- readonly wave: ApiWave;
readonly onDropClick: (drop: ExtendedDrop) => void;
}
export const WaveSmallLeaderboardDefaultDrop: React.FC<
WaveSmallLeaderboardDefaultDropProps
-> = ({ drop, wave, onDropClick }) => {
+> = ({ drop, onDropClick }) => {
const getCICColor = (cic: number): string => {
const cicType = cicToType(cic);
switch (cicType) {
@@ -129,7 +127,7 @@ export const WaveSmallLeaderboardDefaultDrop: React.FC<
/>
-
+
diff --git a/components/waves/small-leaderboard/WaveSmallLeaderboardDrop.tsx b/components/waves/small-leaderboard/WaveSmallLeaderboardDrop.tsx
index a970e9b5d5..c7a3efdcbc 100644
--- a/components/waves/small-leaderboard/WaveSmallLeaderboardDrop.tsx
+++ b/components/waves/small-leaderboard/WaveSmallLeaderboardDrop.tsx
@@ -19,7 +19,6 @@ export const WaveSmallLeaderboardDrop: React.FC<
return (
);
@@ -27,7 +26,6 @@ export const WaveSmallLeaderboardDrop: React.FC<
return (
);
diff --git a/components/waves/small-leaderboard/WaveSmallLeaderboardItemOutcomes.tsx b/components/waves/small-leaderboard/WaveSmallLeaderboardItemOutcomes.tsx
index 54faf804fc..2c712abdef 100644
--- a/components/waves/small-leaderboard/WaveSmallLeaderboardItemOutcomes.tsx
+++ b/components/waves/small-leaderboard/WaveSmallLeaderboardItemOutcomes.tsx
@@ -5,97 +5,17 @@ import { Tooltip } from "react-tooltip";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAddressCard, faStar } from "@fortawesome/free-regular-svg-icons";
import { faAward } from "@fortawesome/free-solid-svg-icons";
-
-import { ApiWave } from "@/generated/models/ApiWave";
-import { ApiWaveOutcomeCredit } from "@/generated/models/ApiWaveOutcomeCredit";
-import { ApiWaveOutcomeType } from "@/generated/models/ApiWaveOutcomeType";
import { ApiDrop } from "@/generated/models/ApiDrop";
+import { useWaveRankReward } from "@/hooks/waves/useWaveRankReward";
interface WaveSmallLeaderboardItemOutcomesProps {
readonly drop: ApiDrop;
- readonly wave: ApiWave;
readonly isMobile?: boolean;
}
-interface OutcomeSummary {
- nicTotal: number;
- repTotal: number;
- manualOutcomes: string[];
-}
-
-const calculateNIC = ({
- drop,
- wave,
-}: {
- drop: ApiDrop;
- wave: ApiWave;
-}): number => {
- const rank = drop.rank;
- if (!rank) return 0;
- const outcomes = wave.outcomes;
- const nicOutcomes = outcomes.filter(
- (outcome) => outcome.credit === ApiWaveOutcomeCredit.Cic
- );
- const nic = nicOutcomes.reduce((acc, outcome) => {
- return acc + (outcome.distribution?.[rank - 1]?.amount ?? 0);
- }, 0);
- return nic;
-};
-
-const calculateRep = ({
- drop,
- wave,
-}: {
- drop: ApiDrop;
- wave: ApiWave;
-}): number => {
- const rank = drop.rank;
- if (!rank) return 0;
- const outcomes = wave.outcomes;
- const repOutcomes = outcomes.filter(
- (outcome) => outcome.credit === ApiWaveOutcomeCredit.Rep
- );
- const rep = repOutcomes.reduce((acc, outcome) => {
- return acc + (outcome.distribution?.[rank - 1]?.amount ?? 0);
- }, 0);
- return rep;
-};
-
-const calculateManualOutcomes = ({
- drop,
- wave,
-}: {
- drop: ApiDrop;
- wave: ApiWave;
-}): string[] => {
- const rank = drop.rank;
- if (!rank) return [];
- const outcomes = wave.outcomes;
- const manualOutcomes = outcomes.filter(
- (outcome) => outcome.type === ApiWaveOutcomeType.Manual
- );
- return manualOutcomes
- .filter((outcome) => !!outcome.distribution?.[rank - 1]?.amount)
- .map((outcome) => outcome.distribution?.[rank - 1]?.description ?? "");
-};
-
-const calculateOutcomeSummary = ({
- drop,
- wave,
-}: {
- drop: ApiDrop;
- wave: ApiWave;
-}): OutcomeSummary => {
- return {
- nicTotal: calculateNIC({ drop, wave }),
- repTotal: calculateRep({ drop, wave }),
- manualOutcomes: calculateManualOutcomes({ drop, wave }),
- };
-};
-
export const WaveSmallLeaderboardItemOutcomes: React.FC<
WaveSmallLeaderboardItemOutcomesProps
-> = ({ drop, wave, isMobile = false }) => {
+> = ({ drop, isMobile = false }) => {
const [isTouch, setIsTouch] = useState(false);
const [isOpen, setIsOpen] = useState(false);
@@ -110,17 +30,24 @@ export const WaveSmallLeaderboardItemOutcomes: React.FC<
}
};
- const { nicTotal, repTotal, manualOutcomes } = calculateOutcomeSummary({
- drop,
- wave,
+ const { nicTotal, repTotal, manualOutcomes, isLoading } = useWaveRankReward({
+ waveId: drop.wave.id,
+ rank: drop.rank,
});
+
const totalOutcomes =
(nicTotal ? 1 : 0) + (repTotal ? 1 : 0) + manualOutcomes.length;
- if (totalOutcomes === 0) {
+ if (totalOutcomes === 0 && !isLoading) {
return null;
}
+ if (isLoading) {
+ return (
+
+ )
+ }
+
const tooltipContent = (
@@ -184,11 +111,9 @@ export const WaveSmallLeaderboardItemOutcomes: React.FC<
<>
-
+
diff --git a/hooks/waves/useWaveRankReward.ts b/hooks/waves/useWaveRankReward.ts
new file mode 100644
index 0000000000..d14cf63c57
--- /dev/null
+++ b/hooks/waves/useWaveRankReward.ts
@@ -0,0 +1,96 @@
+import { useQueries } from "@tanstack/react-query";
+import { useWaveOutcomesQuery } from "./useWaveOutcomesQuery";
+import { ApiWaveOutcomeCredit } from "@/generated/models/ApiWaveOutcomeCredit";
+import { ApiWaveOutcomeType } from "@/generated/models/ApiWaveOutcomeType";
+import { commonApiFetch } from "@/services/api/common-api";
+import { ApiWaveOutcomeDistributionItemsPage } from "@/generated/models/ApiWaveOutcomeDistributionItemsPage";
+import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
+
+const DISTRIBUTION_PAGE_SIZE = 100;
+
+export interface WaveRankRewards {
+ readonly nicTotal: number;
+ readonly repTotal: number;
+ readonly manualOutcomes: string[];
+ readonly isLoading: boolean;
+}
+
+export function useWaveRankReward({
+ waveId,
+ rank,
+ enabled = true,
+}: {
+ waveId: string;
+ rank: number | null;
+ enabled?: boolean;
+}): WaveRankRewards {
+ const { outcomes, isEnabled: isOutcomesEnabled } = useWaveOutcomesQuery({
+ waveId,
+ enabled: enabled && !!rank,
+ });
+
+ const targetIndex = rank ? rank - 1 : 0;
+ const page = Math.floor(targetIndex / DISTRIBUTION_PAGE_SIZE) + 1;
+
+ const distributionQueries = useQueries({
+ queries: outcomes.map((outcome) => ({
+ queryKey: [
+ QueryKey.WAVE_OUTCOME_DISTRIBUTION_PAGE,
+ waveId,
+ outcome.index,
+ page,
+ DISTRIBUTION_PAGE_SIZE,
+ ],
+ queryFn: () =>
+ commonApiFetch