Skip to content

Commit 30386b4

Browse files
committed
fix(predict): hide child more-market cards from feed
Map Polymarket parentEventId to PredictMarket parentMarketId and filter parented markets from feed and carousel render data while preserving raw pagination behavior.
1 parent 1228eda commit 30386b4

10 files changed

Lines changed: 158 additions & 5 deletions

File tree

app/components/UI/Predict/hooks/useFeaturedCarouselData.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,26 @@ describe('useFeaturedCarouselData', () => {
103103
expect(result.current.error).toBeNull();
104104
});
105105

106+
it('filters child more-market cards', async () => {
107+
const { Wrapper } = createWrapper();
108+
const parentMarket = createMockMarket({ id: 'parent-market' });
109+
const childMarket = createMockMarket({
110+
id: 'child-market',
111+
parentMarketId: 'parent-market',
112+
});
113+
mockGetCarouselMarkets.mockResolvedValue([parentMarket, childMarket]);
114+
115+
const { result } = renderHook(() => useFeaturedCarouselData(), {
116+
wrapper: Wrapper,
117+
});
118+
119+
await waitFor(() => {
120+
expect(result.current.isLoading).toBe(false);
121+
});
122+
123+
expect(result.current.markets).toEqual([parentMarket]);
124+
});
125+
106126
it('returns error when controller throws', async () => {
107127
const { Wrapper } = createWrapper();
108128
mockGetCarouselMarkets.mockRejectedValue(new Error('Network error'));

app/components/UI/Predict/hooks/useFeaturedCarouselData.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { predictQueries } from '../queries';
77
import { selectPredictUpDownEnabledFlag } from '../selectors/featureFlags';
88
import type { PredictMarket } from '../types';
99
import { isCryptoUpDown } from '../utils/cryptoUpDown';
10+
import { filterStandaloneMarkets } from '../utils/feed';
1011
import { ensureError } from '../utils/predictErrorHandler';
1112

1213
export interface UseFeaturedCarouselDataResult {
@@ -40,7 +41,7 @@ export const useFeaturedCarouselData = (): UseFeaturedCarouselDataResult => {
4041
}, [query.error]);
4142

4243
const markets = useMemo(() => {
43-
const data = query.data ?? [];
44+
const data = filterStandaloneMarkets(query.data ?? []);
4445
if (upDownEnabled) {
4546
return data;
4647
}

app/components/UI/Predict/hooks/usePredictMarketData.test.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,63 @@ describe('usePredictMarketData', () => {
201201
);
202202
});
203203

204+
it('filters child more-market cards without disabling pagination', async () => {
205+
const rawMarkets = Array.from({ length: 20 }, (_, index) => ({
206+
...mockMarketData[0],
207+
id: `market-${index}`,
208+
slug: `market-${index}`,
209+
parentMarketId: index >= 18 ? 'parent-market' : undefined,
210+
}));
211+
mockGetMarkets.mockResolvedValue(rawMarkets);
212+
213+
const { result } = renderHook(() => usePredictMarketData({ pageSize: 20 }));
214+
215+
await waitFor(() => {
216+
expect(result.current.isFetching).toBe(false);
217+
});
218+
219+
expect(result.current.marketData).toHaveLength(18);
220+
expect(result.current.marketData.map((market) => market.id)).toEqual(
221+
rawMarkets.slice(0, 18).map((market) => market.id),
222+
);
223+
expect(result.current.hasMore).toBe(true);
224+
});
225+
226+
it('uses raw page offsets when loading more after child cards are filtered', async () => {
227+
const firstRawPage = Array.from({ length: 20 }, (_, index) => ({
228+
...mockMarketData[0],
229+
id: `first-page-market-${index}`,
230+
slug: `first-page-market-${index}`,
231+
parentMarketId: index >= 18 ? 'parent-market' : undefined,
232+
}));
233+
const secondRawPage = Array.from({ length: 5 }, (_, index) => ({
234+
...mockMarketData[0],
235+
id: `second-page-market-${index}`,
236+
slug: `second-page-market-${index}`,
237+
}));
238+
239+
mockGetMarkets
240+
.mockResolvedValueOnce(firstRawPage)
241+
.mockResolvedValueOnce(secondRawPage);
242+
243+
const { result } = renderHook(() => usePredictMarketData({ pageSize: 20 }));
244+
245+
await waitFor(() => {
246+
expect(result.current.isFetching).toBe(false);
247+
});
248+
249+
await act(async () => {
250+
await result.current.fetchMore();
251+
});
252+
253+
expect(mockGetMarkets).toHaveBeenNthCalledWith(
254+
2,
255+
expect.objectContaining({ limit: 20, offset: 20 }),
256+
);
257+
expect(result.current.marketData).toHaveLength(23);
258+
expect(result.current.hasMore).toBe(false);
259+
});
260+
204261
it('handle null market data', async () => {
205262
mockGetMarkets.mockResolvedValue(null);
206263

app/components/UI/Predict/hooks/usePredictMarketData.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Logger from '../../../../util/Logger';
1313
import { PREDICT_CONSTANTS } from '../constants/errors';
1414
import { ensureError } from '../utils/predictErrorHandler';
1515
import { PredictCategory, PredictMarket } from '../types';
16+
import { filterStandaloneMarkets } from '../utils/feed';
1617

1718
export interface UsePredictMarketDataOptions {
1819
q?: string;
@@ -147,12 +148,13 @@ export const usePredictMarketData = (
147148

148149
const hasMoreData = markets.length >= pageSize;
149150
setHasMore(hasMoreData);
151+
const visibleMarkets = filterStandaloneMarkets(markets);
150152

151153
if (isLoadMore) {
152154
setMarketData((prevData) => {
153155
// Use a Set to efficiently deduplicate by ID
154156
const existingIds = new Set(prevData.map((event) => event.id));
155-
const newEvents = markets.filter(
157+
const newEvents = visibleMarkets.filter(
156158
(event) => !existingIds.has(event.id),
157159
);
158160
const accumulated = [...prevData, ...newEvents];
@@ -162,7 +164,7 @@ export const usePredictMarketData = (
162164
currentOffsetRef.current += pageSize;
163165
} else {
164166
// Replace data for initial load or refresh
165-
setMarketData(refine ? refine(markets) : markets);
167+
setMarketData(refine ? refine(visibleMarkets) : visibleMarkets);
166168
setCurrentOffset(pageSize);
167169
currentOffsetRef.current = pageSize;
168170
}

app/components/UI/Predict/providers/polymarket/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export interface PolymarketApiEvent {
124124
period?: PredictGamePeriod;
125125
live?: boolean;
126126
ended?: boolean;
127-
parentEventId?: string | number;
127+
parentEventId?: string | number | null;
128128
}
129129

130130
export interface PolymarketApiActivity {

app/components/UI/Predict/providers/polymarket/utils.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ import {
2323
getIsApprovedForAll,
2424
getOrderBook,
2525
getRawBalance,
26+
parsePolymarketEvents,
2627
previewOrder,
2728
} from './utils';
29+
import type { PolymarketApiEvent } from './types';
2830

2931
const mockSignTypedMessage = jest.fn();
3032

@@ -597,4 +599,28 @@ describe('polymarket utils', () => {
597599
}),
598600
).resolves.toBe(false);
599601
});
602+
603+
it('preserves parent market id when parsing Polymarket events', () => {
604+
const event: PolymarketApiEvent = {
605+
id: 'child-event',
606+
slug: 'child-event',
607+
title: 'Child Event',
608+
description: 'Child event description',
609+
icon: '',
610+
closed: false,
611+
series: [],
612+
markets: [],
613+
tags: [],
614+
liquidity: 0,
615+
volume: 0,
616+
parentEventId: 'parent-market',
617+
};
618+
619+
expect(parsePolymarketEvents([event], 'trending')).toEqual([
620+
expect.objectContaining({
621+
id: 'child-event',
622+
parentMarketId: 'parent-market',
623+
}),
624+
]);
625+
});
600626
});

app/components/UI/Predict/providers/polymarket/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,9 @@ export const parsePolymarketEvents = (
11111111
volume: event.volume,
11121112
game,
11131113
...(seriesData && { series: seriesData }),
1114+
...(event.parentEventId !== undefined && {
1115+
parentMarketId: event.parentEventId,
1116+
}),
11141117
};
11151118
},
11161119
);

app/components/UI/Predict/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export type PredictMarket = {
116116
volume: number;
117117
game?: PredictMarketGame;
118118
series?: PredictSeries;
119+
parentMarketId?: string | number | null;
119120
childMarketIds?: string[];
120121
};
121122

app/components/UI/Predict/utils/feed.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { deduplicateSeriesMarkets } from './feed';
1+
import { deduplicateSeriesMarkets, filterStandaloneMarkets } from './feed';
22
import { Recurrence, type PredictMarket } from '../types';
33

44
const createMockMarket = (
@@ -122,3 +122,31 @@ describe('deduplicateSeriesMarkets', () => {
122122
expect(result).toEqual([single]);
123123
});
124124
});
125+
126+
describe('filterStandaloneMarkets', () => {
127+
it('removes markets with a parent market id', () => {
128+
const parent = createMockMarket('parent');
129+
const emptyParent = createMockMarket('empty-parent', {
130+
parentMarketId: '',
131+
});
132+
const nullParent = createMockMarket('null-parent', {
133+
parentMarketId: null,
134+
});
135+
const child = createMockMarket('child', {
136+
parentMarketId: 'parent',
137+
});
138+
const numericChild = createMockMarket('numeric-child', {
139+
parentMarketId: 123,
140+
});
141+
142+
const result = filterStandaloneMarkets([
143+
parent,
144+
emptyParent,
145+
nullParent,
146+
child,
147+
numericChild,
148+
]);
149+
150+
expect(result).toEqual([parent, emptyParent, nullParent]);
151+
});
152+
});

app/components/UI/Predict/utils/feed.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
import type { PredictMarket } from '../types';
22
import { isCryptoUpDown } from './cryptoUpDown';
33

4+
export function isStandaloneMarket(market: PredictMarket): boolean {
5+
const { parentMarketId } = market;
6+
return (
7+
parentMarketId === undefined ||
8+
parentMarketId === null ||
9+
String(parentMarketId).trim() === ''
10+
);
11+
}
12+
13+
export function filterStandaloneMarkets(
14+
markets: PredictMarket[],
15+
): PredictMarket[] {
16+
return markets.filter(isStandaloneMarket);
17+
}
18+
419
/**
520
* Keeps only the first occurrence of each Crypto Up/Down series slug.
621
* Non-Up/Down markets pass through unchanged.

0 commit comments

Comments
 (0)