Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { WaveleaderboardSort } from '@/components/waves/leaderboard/header/WaveleaderboardSort';
import { WaveDropsLeaderboardSort } from '@/hooks/useWaveDropsLeaderboard';
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WaveleaderboardSort } from "@/components/waves/leaderboard/header/WaveleaderboardSort";
import { WaveDropsLeaderboardSort } from "@/hooks/useWaveDropsLeaderboard";

describe('WaveleaderboardSort', () => {
it('highlights active sort and triggers changes', async () => {
describe("WaveleaderboardSort", () => {
it("highlights active sort and triggers changes", async () => {
const onSortChange = jest.fn();
const user = userEvent.setup();
const queryClient = new QueryClient();

render(
<QueryClientProvider client={queryClient}>
<WaveleaderboardSort
Expand All @@ -20,15 +20,18 @@ describe('WaveleaderboardSort', () => {
</QueryClientProvider>
);

const current = screen.getByText('Current Vote');
expect(current.className).toContain('tw-bg-white/10');
const current = screen.getByText("Current Vote");
expect(current.className).toContain("tw-bg-white/10");

await user.click(screen.getByText('Projected Vote'));
await user.click(screen.getByText("Projected Vote"));
expect(onSortChange).toHaveBeenCalledWith(
WaveDropsLeaderboardSort.RATING_PREDICTION
);

await user.click(screen.getByText('Newest'));
await user.click(screen.getByText("🔥 Hot"));
expect(onSortChange).toHaveBeenCalledWith(WaveDropsLeaderboardSort.TREND);

await user.click(screen.getByText("Newest"));
expect(onSortChange).toHaveBeenCalledWith(
WaveDropsLeaderboardSort.CREATED_AT
);
Expand Down
160 changes: 93 additions & 67 deletions components/waves/leaderboard/header/WaveleaderboardSort.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
"use client"
"use client";

import React, { useCallback, useMemo } from "react";
import React, { useCallback, useEffect, useMemo } from "react";
import { debounce } from "lodash";
import { WaveDropsLeaderboardSort } from "@/hooks/useWaveDropsLeaderboard";
import { useQueryClient } from "@tanstack/react-query";
import { commonApiFetch } from "@/services/api/common-api";
import type { ApiDropsLeaderboardPage } from "@/generated/models/ApiDropsLeaderboardPage";
import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
import { WAVE_DROPS_PARAMS, getDefaultQueryRetry } from "@/components/react-query-wrapper/utils/query-utils";
import {
WAVE_DROPS_PARAMS,
getDefaultQueryRetry,
} from "@/components/react-query-wrapper/utils/query-utils";

interface WaveleaderboardSortProps {
readonly sort: WaveDropsLeaderboardSort;
readonly onSortChange: (sort: WaveDropsLeaderboardSort) => void;
readonly waveId?: string | undefined;
}

const SORT_DIRECTION_MAP: Record<WaveDropsLeaderboardSort, string | undefined> = {
const SORT_DIRECTION_MAP: Record<
WaveDropsLeaderboardSort,
"ASC" | "DESC" | undefined
> = {
[WaveDropsLeaderboardSort.RANK]: undefined,
[WaveDropsLeaderboardSort.RATING_PREDICTION]: "DESC",
[WaveDropsLeaderboardSort.TREND]: "DESC",
[WaveDropsLeaderboardSort.MY_REALTIME_VOTE]: undefined,
[WaveDropsLeaderboardSort.CREATED_AT]: "DESC",
};
Expand All @@ -28,75 +35,83 @@ export const WaveleaderboardSort: React.FC<WaveleaderboardSortProps> = ({
waveId,
}) => {
const queryClient = useQueryClient();

const prefetchSortImmediate = useCallback((targetSort: WaveDropsLeaderboardSort) => {
if (!waveId || targetSort === sort) return;

const sortDirection = SORT_DIRECTION_MAP[targetSort];
const queryKey = [
QueryKey.DROPS_LEADERBOARD,
{
waveId,
page_size: WAVE_DROPS_PARAMS.limit,
sort: targetSort,
sort_direction: sortDirection,
},
];

queryClient
.prefetchInfiniteQuery({
queryKey,
queryFn: async ({ pageParam }: { pageParam: number | null }) => {
const params: Record<string, string> = {
page_size: WAVE_DROPS_PARAMS.limit.toString(),
sort: targetSort,
};

if (sortDirection) {
params["sort_direction"] = sortDirection;
}

if (pageParam) {
params["page"] = `${pageParam}`;
}

return await commonApiFetch<ApiDropsLeaderboardPage>({
endpoint: `waves/${waveId}/leaderboard`,
params,
});

const prefetchSortImmediate = useCallback(
(targetSort: WaveDropsLeaderboardSort) => {
if (!waveId || targetSort === sort) return;

const sortDirection = SORT_DIRECTION_MAP[targetSort];
const queryKey = [
QueryKey.DROPS_LEADERBOARD,
{
waveId,
page_size: WAVE_DROPS_PARAMS.limit,
sort: targetSort,
sort_direction: sortDirection,
},
initialPageParam: null,
getNextPageParam: (lastPage: ApiDropsLeaderboardPage) => {
if (targetSort === WaveDropsLeaderboardSort.MY_REALTIME_VOTE) {
const haveZeroVotes = lastPage.drops.some(
(drop) => drop.context_profile_context?.rating === 0
);
if (haveZeroVotes) {
return null;
];

queryClient
.prefetchInfiniteQuery({
queryKey,
queryFn: async ({ pageParam }: { pageParam: number | null }) => {
const params: Record<string, string> = {
page_size: WAVE_DROPS_PARAMS.limit.toString(),
sort: targetSort,
};

if (sortDirection) {
params["sort_direction"] = sortDirection;
}
}
return lastPage.next ? lastPage.page + 1 : null;
},
pages: 1,
staleTime: 60000,
...getDefaultQueryRetry(),
})
.catch((error) => {
// Log prefetch errors for debugging while not blocking the UI
console.warn('Failed to prefetch leaderboard data:', {
waveId,
targetSort,
error: error.message || error

if (typeof pageParam === "number") {
params["page"] = `${pageParam}`;
}

return await commonApiFetch<ApiDropsLeaderboardPage>({
endpoint: `waves/${waveId}/leaderboard`,
params,
});
},
pages: 1,
initialPageParam: null,
getNextPageParam: (lastPage: ApiDropsLeaderboardPage) => {
if (targetSort === WaveDropsLeaderboardSort.MY_REALTIME_VOTE) {
const haveZeroVotes = lastPage.drops.some(
(drop) => drop.context_profile_context?.rating === 0
);
if (haveZeroVotes) {
return null;
}
}
return lastPage.next ? lastPage.page + 1 : null;
},
staleTime: 60000,
...getDefaultQueryRetry(),
})
.catch((error) => {
// Log prefetch errors for debugging while not blocking the UI
console.warn("Failed to prefetch leaderboard data:", {
waveId,
targetSort,
error: error?.message ?? error,
});
});
});
}, [queryClient, waveId, sort]);
},
[queryClient, waveId, sort]
);

// Debounce prefetch to prevent excessive network requests on rapid hover events
const prefetchSort = useMemo(
() => debounce(prefetchSortImmediate, 300),
[prefetchSortImmediate]
);


// Cancel pending debounced calls on unmount to avoid late network requests
useEffect(() => {
return () => prefetchSort.cancel();
}, [prefetchSort]);

const getButtonClassName = (buttonSort: WaveDropsLeaderboardSort) => {
const baseClass =
"tw-px-4 tw-py-1.5 tw-text-xs tw-font-medium tw-border-0 tw-rounded-md tw-transition-colors";
Expand All @@ -111,8 +126,15 @@ export const WaveleaderboardSort: React.FC<WaveleaderboardSortProps> = ({
return (
<div
id="tabsId"
className="tw-flex tw-bg-iron-950 tw-p-1 tw-rounded-lg tw-border tw-border-solid tw-border-white/10"
className="tw-flex tw-rounded-lg tw-border tw-border-solid tw-border-white/10 tw-bg-iron-950 tw-p-1"
>
<button
className={getButtonClassName(WaveDropsLeaderboardSort.TREND)}
onClick={() => onSortChange(WaveDropsLeaderboardSort.TREND)}
onMouseEnter={() => prefetchSort(WaveDropsLeaderboardSort.TREND)}
>
🔥 Hot
</button>
<button
className={getButtonClassName(WaveDropsLeaderboardSort.RANK)}
onClick={() => onSortChange(WaveDropsLeaderboardSort.RANK)}
Expand All @@ -121,9 +143,13 @@ export const WaveleaderboardSort: React.FC<WaveleaderboardSortProps> = ({
Current Vote
</button>
<button
className={getButtonClassName(WaveDropsLeaderboardSort.RATING_PREDICTION)}
className={getButtonClassName(
WaveDropsLeaderboardSort.RATING_PREDICTION
)}
onClick={() => onSortChange(WaveDropsLeaderboardSort.RATING_PREDICTION)}
onMouseEnter={() => prefetchSort(WaveDropsLeaderboardSort.RATING_PREDICTION)}
onMouseEnter={() =>
prefetchSort(WaveDropsLeaderboardSort.RATING_PREDICTION)
}
>
Projected Vote
</button>
Expand Down
Loading