@bettoredge/calcutta 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bettoredge/calcutta",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Calcutta auction competition components for BettorEdge applications",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -126,6 +126,8 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
126
126
  const [leaving, setLeaving] = useState(false);
127
127
  const [startingComp, setStartingComp] = useState(false);
128
128
  const [itemSearch, setItemSearch] = useState('');
129
+ const [itemsPage, setItemsPage] = useState(1);
130
+ const ITEMS_PER_PAGE = 10;
129
131
  const [mobileListTab, setMobileListTab] = useState<'items' | 'players'>('items');
130
132
 
131
133
  const handleRefresh = async () => {
@@ -136,10 +138,11 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
136
138
  };
137
139
 
138
140
  const filteredItems = useMemo(() => {
139
- if (competition?.auction_type !== 'sweepstakes' || !itemSearch.trim()) return items;
141
+ if (!itemSearch.trim()) return items;
140
142
  const q = itemSearch.toLowerCase().trim();
141
143
  return items.filter(item => {
142
144
  if (item.item_name.toLowerCase().includes(q)) return true;
145
+ if (item.seed != null && `#${item.seed}`.includes(q)) return true;
143
146
  if (item.winning_player_id) {
144
147
  const owner = enrichedPlayers[item.winning_player_id];
145
148
  const ownerName = (owner?.username || owner?.show_name || '').toLowerCase();
@@ -147,7 +150,16 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
147
150
  }
148
151
  return false;
149
152
  });
150
- }, [items, itemSearch, enrichedPlayers, competition?.auction_type]);
153
+ }, [items, itemSearch, enrichedPlayers]);
154
+
155
+ // Reset page when search changes
156
+ useEffect(() => { setItemsPage(1); }, [itemSearch]);
157
+
158
+ const totalItemPages = Math.max(1, Math.ceil(filteredItems.length / ITEMS_PER_PAGE));
159
+ const paginatedItems = useMemo(() => {
160
+ const start = (itemsPage - 1) * ITEMS_PER_PAGE;
161
+ return filteredItems.slice(start, start + ITEMS_PER_PAGE);
162
+ }, [filteredItems, itemsPage]);
151
163
 
152
164
  if (loading && !competition) {
153
165
  return (
@@ -210,30 +222,32 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
210
222
  );
211
223
  }
212
224
 
213
- const displayItems = isSweepstakes ? filteredItems : items;
214
225
  return (
215
226
  <View variant="transparent" style={{ paddingHorizontal: 16, paddingBottom: 8 }}>
216
- {isSweepstakes && (
217
- <TextInput
218
- style={{
219
- height: 36, borderRadius: 8, borderWidth: 1,
220
- borderColor: theme.colors.border.subtle, backgroundColor: theme.colors.surface.input,
221
- color: theme.colors.text.primary, paddingHorizontal: 12, fontSize: 14, lineHeight: 19, marginBottom: 8,
222
- }}
223
- placeholder="Search by team or owner..."
224
- placeholderTextColor={theme.colors.text.tertiary}
225
- value={itemSearch}
226
- onChangeText={setItemSearch}
227
- autoCapitalize="none"
228
- autoCorrect={false}
229
- onFocus={(e: any) => {
230
- if (Platform.OS === 'web' && e?.target?.scrollIntoView) {
231
- setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300);
232
- }
233
- }}
234
- />
227
+ <TextInput
228
+ style={{
229
+ height: 36, borderRadius: 8, borderWidth: 1,
230
+ borderColor: theme.colors.border.subtle, backgroundColor: theme.colors.surface.input,
231
+ color: theme.colors.text.primary, paddingHorizontal: 12, fontSize: 14, lineHeight: 19, marginBottom: 8,
232
+ }}
233
+ placeholder={isSweepstakes ? "Search by team or owner..." : "Search items..."}
234
+ placeholderTextColor={theme.colors.text.tertiary}
235
+ value={itemSearch}
236
+ onChangeText={setItemSearch}
237
+ autoCapitalize="none"
238
+ autoCorrect={false}
239
+ onFocus={(e: any) => {
240
+ if (Platform.OS === 'web' && e?.target?.scrollIntoView) {
241
+ setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300);
242
+ }
243
+ }}
244
+ />
245
+ {itemSearch.trim() !== '' && (
246
+ <Text variant="caption" color="tertiary" style={{ marginBottom: 6 }}>
247
+ {filteredItems.length} of {items.length} items
248
+ </Text>
235
249
  )}
236
- {displayItems.map(item => {
250
+ {paginatedItems.map(item => {
237
251
  const imgUrl = resolveItemImageUrl(item.item_image) || itemImages[item.item_id]?.url;
238
252
  const owner = item.winning_player_id ? enrichedPlayers[item.winning_player_id] : undefined;
239
253
  const ownerName = owner?.username || owner?.show_name;
@@ -260,6 +274,27 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
260
274
  </View>
261
275
  );
262
276
  })}
277
+ {totalItemPages > 1 && (
278
+ <View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 10, gap: 16 }}>
279
+ <TouchableOpacity
280
+ onPress={() => setItemsPage(p => p - 1)}
281
+ disabled={itemsPage <= 1}
282
+ style={{ opacity: itemsPage <= 1 ? 0.3 : 1, padding: 4 }}
283
+ >
284
+ <Ionicons name="chevron-back" size={20} color={theme.colors.primary.default} />
285
+ </TouchableOpacity>
286
+ <Text variant="caption" color="secondary">
287
+ {itemsPage} of {totalItemPages}
288
+ </Text>
289
+ <TouchableOpacity
290
+ onPress={() => setItemsPage(p => p + 1)}
291
+ disabled={itemsPage >= totalItemPages}
292
+ style={{ opacity: itemsPage >= totalItemPages ? 0.3 : 1, padding: 4 }}
293
+ >
294
+ <Ionicons name="chevron-forward" size={20} color={theme.colors.primary.default} />
295
+ </TouchableOpacity>
296
+ </View>
297
+ )}
263
298
  </View>
264
299
  );
265
300
  };
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useMemo, useEffect } from 'react';
2
- import { StyleSheet, ScrollView, TouchableOpacity, Image, FlatList, Platform, useWindowDimensions, RefreshControl } from 'react-native';
2
+ import { StyleSheet, ScrollView, TouchableOpacity, Image, FlatList, Platform, useWindowDimensions, RefreshControl, TextInput, KeyboardAvoidingView } from 'react-native';
3
3
  import { View, Text, useTheme } from '@bettoredge/styles';
4
4
  import { Ionicons } from '@expo/vector-icons';
5
5
  import type { CalcuttaParticipantProps } from '@bettoredge/types';
@@ -54,7 +54,7 @@ export const CalcuttaResults: React.FC<CalcuttaResultsProps> = ({
54
54
  const participantIds = useMemo(() => participants.map(p => p.player_id), [participants]);
55
55
  const { players: enrichedPlayers } = useCalcuttaPlayers(participantIds);
56
56
  const { images: itemImages } = useCalcuttaItemImages(items);
57
- const { roundSummaries } = useCalcuttaResults(rounds, items, item_results, payout_rules);
57
+ const { roundSummaries, itemPerformance } = useCalcuttaResults(rounds, items, item_results, payout_rules);
58
58
 
59
59
  const { presence, socketState } = useCalcuttaSocket(
60
60
  calcutta_competition_id,
@@ -79,16 +79,10 @@ export const CalcuttaResults: React.FC<CalcuttaResultsProps> = ({
79
79
  items.filter(i => i.status === 'sold').reduce((sum, i) => sum + Number(i.winning_bid || 0), 0) || Number(competition?.total_pot) || 0,
80
80
  [items, competition?.total_pot]
81
81
  );
82
- const myTotalSpent = useMemo(() => myItems.reduce((sum, i) => sum + Number(i.winning_bid || 0), 0), [myItems]);
83
- const myTotalEarned = useMemo(() => {
84
- if (!player_id) return 0;
85
- return item_results
86
- .filter(r => {
87
- const item = items.find(i => i.calcutta_auction_item_id === r.calcutta_auction_item_id);
88
- return item?.winning_player_id == player_id;
89
- })
90
- .reduce((sum, r) => sum + Number(r.payout_earned || 0), 0);
91
- }, [item_results, items, player_id]);
82
+ const unclaimedPot = Number(competition?.unclaimed_pot) || 0;
83
+ const myParticipant = useMemo(() => participants.find(p => p.player_id == player_id), [participants, player_id]);
84
+ const myTotalSpent = Number(myParticipant?.total_spent) || 0;
85
+ const myTotalEarned = Number(myParticipant?.total_winnings) || 0;
92
86
 
93
87
  const statusLabel = competition?.status === 'closed' ? 'Completed'
94
88
  : competition?.status === 'inprogress' ? 'In Progress'
@@ -108,23 +102,84 @@ export const CalcuttaResults: React.FC<CalcuttaResultsProps> = ({
108
102
  [participants]
109
103
  );
110
104
 
105
+ // Mobile tab
106
+ type BottomTab = 'rounds' | 'leaderboard' | 'items';
107
+ const [mobileTab, setMobileTab] = useState<BottomTab>('rounds');
108
+
111
109
  // Expandable rounds
112
110
  const [expandedRound, setExpandedRound] = useState<number | null>(null);
113
111
 
114
- // Item search
112
+ // Leaderboard search, pagination
113
+ const [leaderSearch, setLeaderSearch] = useState('');
114
+ const LEADERS_PER_PAGE = 20;
115
+ const [leaderPage, setLeaderPage] = useState(0);
116
+
117
+ const filteredLeaderboard = useMemo(() => {
118
+ if (!leaderSearch.trim()) return leaderboard;
119
+ const q = leaderSearch.toLowerCase();
120
+ return leaderboard.filter(p => {
121
+ const profile = enrichedPlayers[p.player_id];
122
+ const username = (profile?.username || profile?.show_name || '').toLowerCase();
123
+ if (username.includes(q)) return true;
124
+ if (p.player_id == player_id && 'you'.includes(q)) return true;
125
+ return false;
126
+ });
127
+ }, [leaderboard, leaderSearch, enrichedPlayers, player_id]);
128
+
129
+ const pagedLeaderboard = useMemo(() => {
130
+ const start = leaderPage * LEADERS_PER_PAGE;
131
+ return filteredLeaderboard.slice(start, start + LEADERS_PER_PAGE);
132
+ }, [filteredLeaderboard, leaderPage]);
133
+
134
+ const totalLeaderPages = Math.ceil(filteredLeaderboard.length / LEADERS_PER_PAGE);
135
+
136
+ // Item search, sort, pagination
115
137
  const [itemSearch, setItemSearch] = useState('');
138
+ type ItemSort = 'name' | 'paid' | 'bid';
139
+ const [itemSort, setItemSort] = useState<ItemSort>('paid');
140
+ const ITEMS_PER_PAGE = 20;
141
+ const [itemPage, setItemPage] = useState(0);
142
+
143
+ const itemEarningsMap = useMemo(() => {
144
+ const map: Record<string, number> = {};
145
+ for (const ip of itemPerformance) {
146
+ map[ip.item.calcutta_auction_item_id] = ip.total_earned;
147
+ }
148
+ return map;
149
+ }, [itemPerformance]);
150
+
116
151
  const filteredItems = useMemo(() => {
117
- if (!itemSearch.trim()) return items;
118
- const q = itemSearch.toLowerCase();
119
- return items.filter(i => {
120
- if (i.item_name.toLowerCase().includes(q)) return true;
121
- if (i.winning_player_id) {
122
- const owner = enrichedPlayers[i.winning_player_id];
123
- if ((owner?.username || '').toLowerCase().includes(q)) return true;
152
+ let result = items;
153
+ if (itemSearch.trim()) {
154
+ const q = itemSearch.toLowerCase();
155
+ result = result.filter(i => {
156
+ if (i.item_name.toLowerCase().includes(q)) return true;
157
+ if (i.winning_player_id) {
158
+ const owner = enrichedPlayers[i.winning_player_id];
159
+ if ((owner?.username || '').toLowerCase().includes(q)) return true;
160
+ }
161
+ return false;
162
+ });
163
+ }
164
+ return [...result].sort((a, b) => {
165
+ switch (itemSort) {
166
+ case 'paid':
167
+ return (itemEarningsMap[b.calcutta_auction_item_id] || 0) - (itemEarningsMap[a.calcutta_auction_item_id] || 0);
168
+ case 'bid':
169
+ return Number(b.winning_bid || 0) - Number(a.winning_bid || 0);
170
+ case 'name':
171
+ default:
172
+ return a.item_name.localeCompare(b.item_name);
124
173
  }
125
- return false;
126
174
  });
127
- }, [items, itemSearch, enrichedPlayers]);
175
+ }, [items, itemSearch, enrichedPlayers, itemSort, itemEarningsMap]);
176
+
177
+ const pagedItems = useMemo(() => {
178
+ const start = itemPage * ITEMS_PER_PAGE;
179
+ return filteredItems.slice(start, start + ITEMS_PER_PAGE);
180
+ }, [filteredItems, itemPage]);
181
+
182
+ const totalItemPages = Math.ceil(filteredItems.length / ITEMS_PER_PAGE);
128
183
 
129
184
  if (loading && !competition) {
130
185
  return (
@@ -213,42 +268,70 @@ export const CalcuttaResults: React.FC<CalcuttaResultsProps> = ({
213
268
  );
214
269
  };
215
270
 
216
- const renderPotKPIs = () => (
217
- <View variant="transparent" style={{ flexDirection: 'row', paddingHorizontal: 16, paddingVertical: 8, gap: 10 }}>
218
- {!isSweepstakes && (
219
- <View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
220
- <Text variant="caption" color="tertiary" style={{ fontSize: 10, lineHeight: 13 }}>Pot</Text>
221
- <Text variant="h3" bold style={{ color: '#F59E0B' }}>{formatCurrency(totalPot, marketType)}</Text>
222
- </View>
223
- )}
224
- {!isSweepstakes && myTotalSpent > 0 && (
225
- <View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
226
- <Text variant="caption" color="tertiary" style={{ fontSize: 10, lineHeight: 13 }}>You Spent</Text>
227
- <Text variant="h3" bold>{formatCurrency(myTotalSpent, marketType)}</Text>
228
- </View>
229
- )}
230
- {myTotalEarned > 0 && (
231
- <View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
232
- <Text variant="caption" color="tertiary" style={{ fontSize: 10, lineHeight: 13 }}>You Earned</Text>
233
- <Text variant="h3" bold style={{ color: '#10B981' }}>{formatCurrency(myTotalEarned, marketType)}</Text>
271
+ const renderPotKPIs = () => {
272
+ const kpiStyle = isDesktop
273
+ ? [styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]
274
+ : [styles.kpiCard, { flex: 0, backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle, minWidth: '31%' as any, flexGrow: 1 }];
275
+
276
+ return (
277
+ <View variant="transparent" style={{ flexDirection: 'row', flexWrap: 'wrap', paddingHorizontal: 16, paddingVertical: 8, gap: 10 }}>
278
+ {!isSweepstakes && (
279
+ <View variant="transparent" style={kpiStyle}>
280
+ <Text variant="caption" color="tertiary" style={{ fontSize: 10, lineHeight: 13 }}>Pot</Text>
281
+ <Text variant="h3" bold style={{ color: '#F59E0B' }}>{formatCurrency(totalPot, marketType)}</Text>
282
+ </View>
283
+ )}
284
+ {!isSweepstakes && unclaimedPot > 0 && (
285
+ <View variant="transparent" style={kpiStyle}>
286
+ <Text variant="caption" color="tertiary" style={{ fontSize: 10, lineHeight: 13 }}>Unclaimed</Text>
287
+ <Text variant="h3" bold style={{ color: '#8B5CF6' }}>{formatCurrency(unclaimedPot, marketType)}</Text>
288
+ </View>
289
+ )}
290
+ {!isSweepstakes && myTotalSpent > 0 && (
291
+ <View variant="transparent" style={kpiStyle}>
292
+ <Text variant="caption" color="tertiary" style={{ fontSize: 10, lineHeight: 13 }}>You Spent</Text>
293
+ <Text variant="h3" bold>{formatCurrency(myTotalSpent, marketType)}</Text>
294
+ </View>
295
+ )}
296
+ {myTotalEarned > 0 && (
297
+ <View variant="transparent" style={kpiStyle}>
298
+ <Text variant="caption" color="tertiary" style={{ fontSize: 10, lineHeight: 13 }}>You Earned</Text>
299
+ <Text variant="h3" bold style={{ color: '#10B981' }}>{formatCurrency(myTotalEarned, marketType)}</Text>
300
+ </View>
301
+ )}
302
+ <View variant="transparent" style={kpiStyle}>
303
+ <Text variant="caption" color="tertiary" style={{ fontSize: 10, lineHeight: 13 }}>Players</Text>
304
+ <Text variant="h3" bold>{participants.length}</Text>
234
305
  </View>
235
- )}
236
- <View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
237
- <Text variant="caption" color="tertiary" style={{ fontSize: 10, lineHeight: 13 }}>Players</Text>
238
- <Text variant="h3" bold>{participants.length}</Text>
239
306
  </View>
240
- </View>
241
- );
307
+ );
308
+ };
242
309
 
243
310
  const renderRounds = () => {
244
311
  if (roundSummaries.length === 0) return null;
245
312
  const sortedRounds = [...roundSummaries].sort((a, b) => a.round.round_number - b.round.round_number);
313
+
314
+ // Compute running unclaimed balance per round
315
+ // Each closed round: pool = (pct * pot) + rolledIn, paid out = total_payout, rolled out = pool - paid
316
+ const roundUnclaimed: { rolledIn: number; pool: number; rolledOut: number }[] = [];
317
+ let runningUnclaimed = 0;
318
+ for (const rs of sortedRounds) {
319
+ const basePool = rs.payout_rule ? (rs.payout_rule.payout_pct / 100) * totalPot : 0;
320
+ const pool = basePool + runningUnclaimed;
321
+ const isClosed = rs.round.status === 'closed';
322
+ const rolledOut = isClosed ? Math.max(pool - rs.total_payout, 0) : 0;
323
+ roundUnclaimed.push({ rolledIn: runningUnclaimed, pool, rolledOut });
324
+ if (isClosed) { runningUnclaimed = rolledOut; }
325
+ }
326
+
246
327
  return (
247
328
  <View variant="transparent" style={{ paddingHorizontal: 16 }}>
248
329
  <Text variant="caption" bold color="tertiary" style={styles.sectionLabel}>Rounds</Text>
249
- {sortedRounds.map(rs => {
330
+ {sortedRounds.map((rs, idx) => {
250
331
  const isExpanded = expandedRound === rs.round.round_number;
251
332
  const isClosed = rs.round.status === 'closed';
333
+ const isFinalRound = idx === sortedRounds.length - 1;
334
+ const { rolledIn, pool: roundPool, rolledOut } = roundUnclaimed[idx];
252
335
  return (
253
336
  <TouchableOpacity
254
337
  key={rs.round.calcutta_round_id}
@@ -262,9 +345,15 @@ export const CalcuttaResults: React.FC<CalcuttaResultsProps> = ({
262
345
  {rs.payout_rule && (
263
346
  <Text variant="caption" bold style={{ color: '#F59E0B' }}>{rs.payout_rule.payout_pct}%</Text>
264
347
  )}
265
- {rs.total_payout > 0 && (
348
+ {isClosed && rs.payout_rule && !isSweepstakes && rs.total_payout > 0 && (
266
349
  <Text variant="caption" bold style={{ color: '#10B981', marginLeft: 8 }}>{formatCurrency(rs.total_payout, marketType)}</Text>
267
350
  )}
351
+ {isClosed && rs.payout_rule && !isSweepstakes && rs.total_payout < 0.01 && !isFinalRound && (
352
+ <Text variant="caption" bold style={{ color: '#8B5CF6', marginLeft: 8 }}>Unclaimed</Text>
353
+ )}
354
+ {isClosed && isFinalRound && !isSweepstakes && runningUnclaimed > 0.01 && (
355
+ <Text variant="caption" bold style={{ color: '#10B981', marginLeft: 8 }}>Redistributed</Text>
356
+ )}
268
357
  <Ionicons name={isExpanded ? 'chevron-up' : 'chevron-down'} size={14} color={theme.colors.text.tertiary} style={{ marginLeft: 8 }} />
269
358
  </View>
270
359
  {!isExpanded && isClosed && (
@@ -272,18 +361,64 @@ export const CalcuttaResults: React.FC<CalcuttaResultsProps> = ({
272
361
  {rs.advanced_items.length} advanced · {rs.eliminated_items.length} eliminated
273
362
  </Text>
274
363
  )}
364
+ {!isExpanded && !isClosed && rs.payout_rule && !isSweepstakes && (
365
+ <Text variant="caption" color="tertiary" style={{ marginTop: 4, fontSize: 11, lineHeight: 15 }}>
366
+ {formatCurrency(roundPool, marketType)} payout pool{rolledIn > 0.01 ? ` (incl. ${formatCurrency(rolledIn, marketType)} rolled in)` : ''}
367
+ </Text>
368
+ )}
275
369
  {isExpanded && (
276
370
  <View variant="transparent" style={{ marginTop: 10 }}>
371
+ {!isSweepstakes && rs.payout_rule && (
372
+ <View variant="transparent" style={{ marginBottom: 10, paddingVertical: 6, paddingHorizontal: 8, backgroundColor: theme.colors.surface.base, borderRadius: 6, gap: 4 }}>
373
+ <View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center' }}>
374
+ <Ionicons name="cash-outline" size={14} color="#F59E0B" />
375
+ <Text variant="caption" color="secondary" style={{ marginLeft: 6, flex: 1 }}>
376
+ {rs.payout_rule.payout_pct}% of pot{rolledIn > 0.01 ? ` + ${formatCurrency(rolledIn, marketType)} rolled in` : ''}
377
+ </Text>
378
+ <Text variant="caption" bold style={{ color: '#F59E0B' }}>
379
+ {formatCurrency(roundPool, marketType)}
380
+ </Text>
381
+ </View>
382
+ {isClosed && (
383
+ <>
384
+ <View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', paddingLeft: 20 }}>
385
+ <Ionicons name="checkmark-circle-outline" size={12} color="#10B981" />
386
+ <Text variant="caption" style={{ marginLeft: 6, flex: 1, color: theme.colors.text.tertiary }}>Paid to owners</Text>
387
+ <Text variant="caption" bold style={{ color: '#10B981' }}>{formatCurrency(rs.total_payout, marketType)}</Text>
388
+ </View>
389
+ {rolledOut > 0.01 && !isFinalRound && (
390
+ <View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', paddingLeft: 20 }}>
391
+ <Ionicons name="arrow-forward-outline" size={12} color="#8B5CF6" />
392
+ <Text variant="caption" style={{ marginLeft: 6, flex: 1, color: theme.colors.text.tertiary }}>Rolled to unclaimed</Text>
393
+ <Text variant="caption" bold style={{ color: '#8B5CF6' }}>{formatCurrency(rolledOut, marketType)}</Text>
394
+ </View>
395
+ )}
396
+ {isFinalRound && runningUnclaimed > 0.01 && (
397
+ <View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', paddingLeft: 20 }}>
398
+ <Ionicons name="people-outline" size={12} color="#10B981" />
399
+ <Text variant="caption" style={{ marginLeft: 6, flex: 1, color: theme.colors.text.tertiary }}>Redistributed to all players by spend</Text>
400
+ <Text variant="caption" bold style={{ color: '#10B981' }}>{formatCurrency(runningUnclaimed, marketType)}</Text>
401
+ </View>
402
+ )}
403
+ </>
404
+ )}
405
+ </View>
406
+ )}
277
407
  {rs.advanced_items.length > 0 && (
278
408
  <>
279
409
  <Text variant="caption" bold style={{ color: '#10B981', marginBottom: 4, fontSize: 10, lineHeight: 13 }}>ADVANCED ({rs.advanced_items.length})</Text>
280
410
  {rs.advanced_items.map(item => {
281
411
  const isMe = item.winning_player_id == player_id;
412
+ const result = item_results.find(r => r.calcutta_auction_item_id === item.calcutta_auction_item_id && r.round_number === rs.round.round_number);
413
+ const payout = Number(result?.payout_earned || 0);
414
+ const owner = item.winning_player_id ? enrichedPlayers[item.winning_player_id] : undefined;
415
+ const ownerName = isMe ? 'You' : owner?.username || owner?.show_name || '';
282
416
  return (
283
417
  <View key={item.calcutta_auction_item_id} variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: 3 }}>
284
418
  <Ionicons name="checkmark-circle" size={12} color="#10B981" />
285
- <Text variant="caption" style={{ marginLeft: 6, flex: 1 }}>{item.item_name}</Text>
286
- {isMe && <Text variant="caption" bold style={{ color: '#10B981', fontSize: 10, lineHeight: 13 }}>YOU</Text>}
419
+ <Text variant="caption" style={{ marginLeft: 6, flex: 1 }} numberOfLines={1}>{item.item_name}</Text>
420
+ {ownerName ? <Text variant="caption" color="tertiary" style={{ marginLeft: 4, fontSize: 10, lineHeight: 13 }}>{ownerName}</Text> : null}
421
+ {payout > 0 && <Text variant="caption" bold style={{ color: '#10B981', marginLeft: 6, fontSize: 10, lineHeight: 13 }}>{formatCurrency(payout, marketType)}</Text>}
287
422
  </View>
288
423
  );
289
424
  })}
@@ -294,16 +429,70 @@ export const CalcuttaResults: React.FC<CalcuttaResultsProps> = ({
294
429
  <Text variant="caption" bold style={{ color: theme.colors.status.error, marginTop: 8, marginBottom: 4, fontSize: 10, lineHeight: 13 }}>ELIMINATED ({rs.eliminated_items.length})</Text>
295
430
  {rs.eliminated_items.map(item => {
296
431
  const isMe = item.winning_player_id == player_id;
432
+ const result = item_results.find(r => r.calcutta_auction_item_id === item.calcutta_auction_item_id && r.round_number === rs.round.round_number);
433
+ const payout = Number(result?.payout_earned || 0);
434
+ const owner = item.winning_player_id ? enrichedPlayers[item.winning_player_id] : undefined;
435
+ const ownerName = isMe ? 'You' : owner?.username || owner?.show_name || '';
297
436
  return (
298
437
  <View key={item.calcutta_auction_item_id} variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: 3, opacity: 0.6 }}>
299
438
  <Ionicons name="close-circle" size={12} color={theme.colors.status.error} />
300
- <Text variant="caption" style={{ marginLeft: 6, flex: 1 }}>{item.item_name}</Text>
301
- {isMe && <Text variant="caption" bold style={{ color: theme.colors.status.error, fontSize: 10, lineHeight: 13 }}>YOU</Text>}
439
+ <Text variant="caption" style={{ marginLeft: 6, flex: 1 }} numberOfLines={1}>{item.item_name}</Text>
440
+ {ownerName ? <Text variant="caption" color="tertiary" style={{ marginLeft: 4, fontSize: 10, lineHeight: 13 }}>{ownerName}</Text> : null}
441
+ {payout > 0 && <Text variant="caption" bold style={{ color: '#10B981', marginLeft: 6, fontSize: 10, lineHeight: 13 }}>{formatCurrency(payout, marketType)}</Text>}
302
442
  </View>
303
443
  );
304
444
  })}
305
445
  </>
306
446
  )}
447
+ {isClosed && rs.advanced_items.length === 0 && rs.eliminated_items.length === 0 && (
448
+ <Text variant="caption" color="tertiary" style={{ fontSize: 11, lineHeight: 15, fontStyle: 'italic' }}>
449
+ No items remaining — round resolved with empty field
450
+ </Text>
451
+ )}
452
+ {isClosed && !isSweepstakes && rolledOut > 0.01 && !isFinalRound && (
453
+ <View variant="transparent" style={{ marginTop: 8, flexDirection: 'row', alignItems: 'center', backgroundColor: '#8B5CF610', paddingVertical: 6, paddingHorizontal: 8, borderRadius: 6 }}>
454
+ <Ionicons name="arrow-forward-outline" size={14} color="#8B5CF6" />
455
+ <Text variant="caption" style={{ marginLeft: 6, color: '#8B5CF6', flex: 1 }}>
456
+ {formatCurrency(rolledOut, marketType)} unclaimed — rolls forward to next round
457
+ </Text>
458
+ </View>
459
+ )}
460
+ {isClosed && !isSweepstakes && isFinalRound && runningUnclaimed > 0.01 && (() => {
461
+ const totalSpentAll = participants.reduce((s, p) => s + Number(p.total_spent), 0);
462
+ const playerShares = totalSpentAll > 0
463
+ ? participants
464
+ .filter(p => Number(p.total_spent) > 0)
465
+ .map(p => {
466
+ const pct = (Number(p.total_spent) / totalSpentAll) * 100;
467
+ const share = (pct / 100) * runningUnclaimed;
468
+ const profile = enrichedPlayers[p.player_id];
469
+ const isMe = p.player_id == player_id;
470
+ const name = isMe ? 'You' : profile?.username || profile?.show_name || 'Player';
471
+ return { name, share, pct, isMe };
472
+ })
473
+ .sort((a, b) => b.share - a.share)
474
+ : [];
475
+ return (
476
+ <View variant="transparent" style={{ marginTop: 8, backgroundColor: '#10B98110', paddingVertical: 8, paddingHorizontal: 10, borderRadius: 6 }}>
477
+ <View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 6 }}>
478
+ <Ionicons name="people-outline" size={14} color="#10B981" />
479
+ <Text variant="caption" bold style={{ marginLeft: 6, color: '#10B981' }}>
480
+ {formatCurrency(runningUnclaimed, marketType)} redistributed by spend
481
+ </Text>
482
+ </View>
483
+ {playerShares.map((ps, i) => (
484
+ <View key={i} variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: 2, paddingLeft: 20 }}>
485
+ <Text variant="caption" style={{ flex: 1, color: ps.isMe ? '#10B981' : theme.colors.text.secondary }} numberOfLines={1}>
486
+ {ps.name} <Text variant="caption" color="tertiary" style={{ fontSize: 10, lineHeight: 13 }}>({ps.pct.toFixed(1)}%)</Text>
487
+ </Text>
488
+ <Text variant="caption" bold style={{ color: '#10B981' }}>
489
+ {formatCurrency(ps.share, marketType)}
490
+ </Text>
491
+ </View>
492
+ ))}
493
+ </View>
494
+ );
495
+ })()}
307
496
  {rs.prize_description && (
308
497
  <View variant="transparent" style={{ marginTop: 8, flexDirection: 'row', alignItems: 'center' }}>
309
498
  <Ionicons name="gift" size={14} color="#8B5CF6" />
@@ -319,71 +508,213 @@ export const CalcuttaResults: React.FC<CalcuttaResultsProps> = ({
319
508
  );
320
509
  };
321
510
 
322
- const renderLeaderboard = () => (
323
- <View variant="transparent" style={{ paddingHorizontal: 16 }}>
324
- <Text variant="caption" bold color="tertiary" style={styles.sectionLabel}>Leaderboard</Text>
325
- {leaderboard.map((p, i) => {
326
- const profile = enrichedPlayers[p.player_id];
327
- const username = profile?.username || profile?.show_name || `Player`;
328
- const profilePic = profile?.profile_pic;
329
- const isMe = p.player_id == player_id;
330
- const medals = ['🥇', '🥈', '🥉'];
331
- return (
332
- <View key={p.calcutta_participant_id} variant="transparent" style={[styles.leaderRow, { borderColor: theme.colors.border.subtle }]}>
333
- <Text style={{ width: 28, textAlign: 'center', fontSize: 14, lineHeight: 19 }}>{i < 3 ? medals[i] : `${i + 1}`}</Text>
334
- {profilePic ? (
335
- <Image source={{ uri: profilePic }} style={{ width: 28, height: 28, borderRadius: 14 }} />
336
- ) : (
337
- <View variant="transparent" style={{ width: 28, height: 28, borderRadius: 14, backgroundColor: isMe ? '#10B98120' : theme.colors.primary.subtle, alignItems: 'center', justifyContent: 'center' }}>
338
- <Text variant="caption" bold style={{ fontSize: 12, lineHeight: 16, color: isMe ? '#10B981' : theme.colors.primary.default }}>{username.charAt(0).toUpperCase()}</Text>
511
+ const renderLeaderboard = () => {
512
+ const medals = ['🥇', '🥈', '🥉'];
513
+
514
+ // Map original rank from the full leaderboard
515
+ const rankMap = new Map<string, number>();
516
+ leaderboard.forEach((p, i) => rankMap.set(p.calcutta_participant_id, i));
517
+
518
+ return (
519
+ <View variant="transparent" style={{ paddingHorizontal: 16 }}>
520
+ <Text variant="caption" bold color="tertiary" style={styles.sectionLabel}>Leaderboard ({participants.length})</Text>
521
+
522
+ <TextInput
523
+ value={leaderSearch}
524
+ onChangeText={v => { setLeaderSearch(v); setLeaderPage(0); }}
525
+ placeholder="Search players..."
526
+ placeholderTextColor={theme.colors.text.tertiary}
527
+ style={{
528
+ color: theme.colors.text.primary,
529
+ backgroundColor: theme.colors.surface.elevated,
530
+ borderColor: theme.colors.border.subtle,
531
+ borderWidth: 1,
532
+ borderRadius: 8,
533
+ paddingHorizontal: 10,
534
+ paddingVertical: 8,
535
+ fontSize: 13,
536
+ marginBottom: 8,
537
+ }}
538
+ />
539
+
540
+ {pagedLeaderboard.map(p => {
541
+ const rank = rankMap.get(p.calcutta_participant_id) ?? 0;
542
+ const profile = enrichedPlayers[p.player_id];
543
+ const username = profile?.username || profile?.show_name || `Player`;
544
+ const profilePic = profile?.profile_pic;
545
+ const isMe = p.player_id == player_id;
546
+ return (
547
+ <View key={p.calcutta_participant_id} variant="transparent" style={[styles.leaderRow, { borderColor: theme.colors.border.subtle }]}>
548
+ <Text style={{ width: 28, textAlign: 'center', fontSize: 14, lineHeight: 19 }}>{rank < 3 ? medals[rank] : `${rank + 1}`}</Text>
549
+ {profilePic ? (
550
+ <Image source={{ uri: profilePic }} style={{ width: 28, height: 28, borderRadius: 14 }} />
551
+ ) : (
552
+ <View variant="transparent" style={{ width: 28, height: 28, borderRadius: 14, backgroundColor: isMe ? '#10B98120' : theme.colors.primary.subtle, alignItems: 'center', justifyContent: 'center' }}>
553
+ <Text variant="caption" bold style={{ fontSize: 12, lineHeight: 16, color: isMe ? '#10B981' : theme.colors.primary.default }}>{username.charAt(0).toUpperCase()}</Text>
554
+ </View>
555
+ )}
556
+ <View variant="transparent" style={{ flex: 1, marginLeft: 8 }}>
557
+ <Text variant="body" bold={isMe} style={isMe ? { color: '#10B981' } : undefined}>{isMe ? 'You' : username}</Text>
558
+ <Text variant="caption" color="tertiary">{p.items_owned} item{p.items_owned !== 1 ? 's' : ''} · {formatCurrency(p.total_spent, marketType)} spent{totalPot > 0 ? ` · ${((Number(p.total_spent) / totalPot) * 100).toFixed(1)}%` : ''}</Text>
339
559
  </View>
340
- )}
341
- <View variant="transparent" style={{ flex: 1, marginLeft: 8 }}>
342
- <Text variant="body" bold={isMe} style={isMe ? { color: '#10B981' } : undefined}>{isMe ? 'You' : username}</Text>
343
- <Text variant="caption" color="tertiary">{p.items_owned} item{p.items_owned !== 1 ? 's' : ''}</Text>
560
+ {Number(p.total_spent) > 0 && (
561
+ <View variant="transparent" style={{ alignItems: 'flex-end' }}>
562
+ <Text variant="caption" bold style={{ color: '#10B981' }}>{formatCurrency(p.total_winnings, marketType)}</Text>
563
+ {(() => {
564
+ const roi = ((Number(p.total_winnings) - Number(p.total_spent)) / Number(p.total_spent)) * 100;
565
+ return (
566
+ <Text variant="caption" style={{ color: roi >= 0 ? '#10B981' : theme.colors.status.error, fontSize: 10, lineHeight: 13 }}>
567
+ {roi >= 0 ? '+' : ''}{roi.toFixed(1)}%
568
+ </Text>
569
+ );
570
+ })()}
571
+ </View>
572
+ )}
344
573
  </View>
345
- {Number(p.total_winnings) > 0 && (
346
- <Text variant="caption" bold style={{ color: '#10B981' }}>{formatCurrency(p.total_winnings, marketType)}</Text>
347
- )}
574
+ );
575
+ })}
576
+
577
+ {totalLeaderPages > 1 && (
578
+ <View variant="transparent" style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginTop: 12, gap: 12 }}>
579
+ <TouchableOpacity
580
+ onPress={() => setLeaderPage(p => Math.max(0, p - 1))}
581
+ disabled={leaderPage === 0}
582
+ style={{ opacity: leaderPage === 0 ? 0.3 : 1 }}
583
+ >
584
+ <Ionicons name="chevron-back" size={20} color={theme.colors.text.secondary} />
585
+ </TouchableOpacity>
586
+ <Text variant="caption" color="secondary">
587
+ {leaderPage + 1} / {totalLeaderPages}
588
+ </Text>
589
+ <TouchableOpacity
590
+ onPress={() => setLeaderPage(p => Math.min(totalLeaderPages - 1, p + 1))}
591
+ disabled={leaderPage >= totalLeaderPages - 1}
592
+ style={{ opacity: leaderPage >= totalLeaderPages - 1 ? 0.3 : 1 }}
593
+ >
594
+ <Ionicons name="chevron-forward" size={20} color={theme.colors.text.secondary} />
595
+ </TouchableOpacity>
348
596
  </View>
349
- );
350
- })}
351
- </View>
352
- );
597
+ )}
598
+ </View>
599
+ );
600
+ };
601
+
602
+ const renderAllItems = () => {
603
+ const sortOptions: { key: ItemSort; label: string }[] = [
604
+ { key: 'paid', label: 'Most Paid' },
605
+ { key: 'bid', label: 'Highest Bid' },
606
+ { key: 'name', label: 'Name' },
607
+ ];
608
+
609
+ return (
610
+ <View variant="transparent" style={{ paddingHorizontal: 16, paddingBottom: 40 }}>
611
+ <Text variant="caption" bold color="tertiary" style={styles.sectionLabel}>All Items ({items.length})</Text>
612
+
613
+ {/* Search */}
614
+ <TextInput
615
+ value={itemSearch}
616
+ onChangeText={v => { setItemSearch(v); setItemPage(0); }}
617
+ placeholder="Search items or owners..."
618
+ placeholderTextColor={theme.colors.text.tertiary}
619
+ style={{
620
+ color: theme.colors.text.primary,
621
+ backgroundColor: theme.colors.surface.elevated,
622
+ borderColor: theme.colors.border.subtle,
623
+ borderWidth: 1,
624
+ borderRadius: 8,
625
+ paddingHorizontal: 10,
626
+ paddingVertical: 8,
627
+ fontSize: 13,
628
+ marginBottom: 8,
629
+ }}
630
+ />
631
+
632
+ {/* Sort pills */}
633
+ <View variant="transparent" style={{ flexDirection: 'row', gap: 6, marginBottom: 10 }}>
634
+ {sortOptions.map(opt => {
635
+ const active = itemSort === opt.key;
636
+ return (
637
+ <TouchableOpacity
638
+ key={opt.key}
639
+ onPress={() => { setItemSort(opt.key); setItemPage(0); }}
640
+ style={{
641
+ paddingHorizontal: 10,
642
+ paddingVertical: 4,
643
+ borderRadius: 12,
644
+ backgroundColor: active ? theme.colors.primary.default : theme.colors.surface.elevated,
645
+ borderWidth: 1,
646
+ borderColor: active ? theme.colors.primary.default : theme.colors.border.subtle,
647
+ }}
648
+ >
649
+ <Text variant="caption" bold={active} style={{ color: active ? '#FFF' : theme.colors.text.secondary, fontSize: 11, lineHeight: 15 }}>
650
+ {opt.label}
651
+ </Text>
652
+ </TouchableOpacity>
653
+ );
654
+ })}
655
+ </View>
353
656
 
354
- const renderAllItems = () => (
355
- <View variant="transparent" style={{ paddingHorizontal: 16, paddingBottom: 40 }}>
356
- <Text variant="caption" bold color="tertiary" style={styles.sectionLabel}>All Items ({items.length})</Text>
357
- {filteredItems.map(item => {
358
- const imgUrl = resolveItemImageUrl(item.item_image) || itemImages[item.item_id]?.url;
359
- const owner = item.winning_player_id ? enrichedPlayers[item.winning_player_id] : undefined;
360
- const ownerName = owner?.username || owner?.show_name;
361
- const isMe = item.winning_player_id == player_id;
362
- return (
363
- <View key={item.calcutta_auction_item_id} variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: 8, borderBottomWidth: 1, borderColor: theme.colors.border.subtle }}>
364
- {imgUrl ? (
365
- <Image source={{ uri: imgUrl }} style={{ width: 28, height: 28, borderRadius: 6 }} resizeMode="cover" />
366
- ) : (
367
- <View variant="transparent" style={{ width: 28, height: 28, borderRadius: 6, backgroundColor: theme.colors.surface.elevated, alignItems: 'center', justifyContent: 'center' }}>
368
- <Ionicons name="trophy-outline" size={14} color={theme.colors.text.tertiary} />
657
+ {/* Items */}
658
+ {pagedItems.map(item => {
659
+ const imgUrl = resolveItemImageUrl(item.item_image) || itemImages[item.item_id]?.url;
660
+ const owner = item.winning_player_id ? enrichedPlayers[item.winning_player_id] : undefined;
661
+ const ownerName = owner?.username || owner?.show_name;
662
+ const isMe = item.winning_player_id == player_id;
663
+ const earned = itemEarningsMap[item.calcutta_auction_item_id] || 0;
664
+ return (
665
+ <View key={item.calcutta_auction_item_id} variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: 8, borderBottomWidth: 1, borderColor: theme.colors.border.subtle }}>
666
+ {imgUrl ? (
667
+ <Image source={{ uri: imgUrl }} style={{ width: 28, height: 28, borderRadius: 6 }} resizeMode="cover" />
668
+ ) : (
669
+ <View variant="transparent" style={{ width: 28, height: 28, borderRadius: 6, backgroundColor: theme.colors.surface.elevated, alignItems: 'center', justifyContent: 'center' }}>
670
+ <Ionicons name="trophy-outline" size={14} color={theme.colors.text.tertiary} />
671
+ </View>
672
+ )}
673
+ <View variant="transparent" style={{ marginLeft: 8, flex: 1 }}>
674
+ <Text variant="body" numberOfLines={1}>{item.item_name}</Text>
675
+ <Text variant="caption" color="tertiary">
676
+ {ownerName ? (isMe ? 'You' : ownerName) : 'Unowned'}
677
+ {item.status === 'eliminated' ? ' · Eliminated' : item.status === 'sold' ? '' : ` · ${getStatusLabel(item.status)}`}
678
+ </Text>
679
+ </View>
680
+ <View variant="transparent" style={{ alignItems: 'flex-end' }}>
681
+ {Number(item.winning_bid) > 0 && !isSweepstakes && (
682
+ <Text variant="caption" bold>{formatCurrency(item.winning_bid, marketType)}</Text>
683
+ )}
684
+ {earned > 0 && (
685
+ <Text variant="caption" bold style={{ color: '#10B981', fontSize: 10, lineHeight: 13 }}>{formatCurrency(earned, marketType)} earned</Text>
686
+ )}
369
687
  </View>
370
- )}
371
- <View variant="transparent" style={{ marginLeft: 8, flex: 1 }}>
372
- <Text variant="body">{item.item_name}</Text>
373
- <Text variant="caption" color="tertiary">
374
- {ownerName ? (isMe ? 'You' : ownerName) : 'Unowned'}
375
- {item.status === 'eliminated' ? ' · Eliminated' : item.status === 'sold' ? '' : ` · ${getStatusLabel(item.status)}`}
376
- </Text>
688
+ {isMe && <Text variant="caption" bold style={{ color: '#10B981', marginLeft: 6, fontSize: 10, lineHeight: 13 }}>YOU</Text>}
377
689
  </View>
378
- {Number(item.winning_bid) > 0 && !isSweepstakes && (
379
- <Text variant="caption" bold>{formatCurrency(item.winning_bid, marketType)}</Text>
380
- )}
381
- {isMe && <Text variant="caption" bold style={{ color: '#10B981', marginLeft: 6, fontSize: 10, lineHeight: 13 }}>YOU</Text>}
690
+ );
691
+ })}
692
+
693
+ {/* Pagination */}
694
+ {totalItemPages > 1 && (
695
+ <View variant="transparent" style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginTop: 12, gap: 12 }}>
696
+ <TouchableOpacity
697
+ onPress={() => setItemPage(p => Math.max(0, p - 1))}
698
+ disabled={itemPage === 0}
699
+ style={{ opacity: itemPage === 0 ? 0.3 : 1 }}
700
+ >
701
+ <Ionicons name="chevron-back" size={20} color={theme.colors.text.secondary} />
702
+ </TouchableOpacity>
703
+ <Text variant="caption" color="secondary">
704
+ {itemPage + 1} / {totalItemPages}
705
+ </Text>
706
+ <TouchableOpacity
707
+ onPress={() => setItemPage(p => Math.min(totalItemPages - 1, p + 1))}
708
+ disabled={itemPage >= totalItemPages - 1}
709
+ style={{ opacity: itemPage >= totalItemPages - 1 ? 0.3 : 1 }}
710
+ >
711
+ <Ionicons name="chevron-forward" size={20} color={theme.colors.text.secondary} />
712
+ </TouchableOpacity>
382
713
  </View>
383
- );
384
- })}
385
- </View>
386
- );
714
+ )}
715
+ </View>
716
+ );
717
+ };
387
718
 
388
719
  // ═══ RENDER ═══
389
720
 
@@ -418,14 +749,38 @@ export const CalcuttaResults: React.FC<CalcuttaResultsProps> = ({
418
749
  </View>
419
750
  </ScrollView>
420
751
  ) : (
421
- <ScrollView style={{ flex: 1, backgroundColor: theme.colors.surface.base }} contentContainerStyle={{ paddingBottom: Platform.OS === 'web' ? 300 : 40, backgroundColor: theme.colors.surface.base }} keyboardShouldPersistTaps="handled" refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}>
422
- {renderHeader()}
423
- {renderMyItems()}
424
- {renderPotKPIs()}
425
- {renderRounds()}
426
- {renderLeaderboard()}
427
- {renderAllItems()}
428
- </ScrollView>
752
+ <KeyboardAvoidingView style={{ flex: 1 }} behavior={Platform.OS === 'ios' ? 'padding' : undefined} keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0}>
753
+ <ScrollView style={{ flex: 1, backgroundColor: theme.colors.surface.base }} contentContainerStyle={{ paddingBottom: 40, backgroundColor: theme.colors.surface.base }} keyboardShouldPersistTaps="handled" refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}>
754
+ {renderHeader()}
755
+ {renderMyItems()}
756
+ {renderPotKPIs()}
757
+ <View variant="transparent" style={{ flexDirection: 'row', marginHorizontal: 16, marginTop: 8, marginBottom: 4, borderRadius: 10, backgroundColor: theme.colors.surface.elevated, borderWidth: 1, borderColor: theme.colors.border.subtle, overflow: 'hidden' }}>
758
+ {([
759
+ { key: 'rounds' as BottomTab, label: 'Rounds', icon: 'layers-outline' as const },
760
+ { key: 'leaderboard' as BottomTab, label: 'Leaderboard', icon: 'podium-outline' as const },
761
+ { key: 'items' as BottomTab, label: 'Items', icon: 'list-outline' as const },
762
+ ]).map(tab => {
763
+ const active = mobileTab === tab.key;
764
+ return (
765
+ <TouchableOpacity
766
+ key={tab.key}
767
+ onPress={() => setMobileTab(tab.key)}
768
+ style={{ flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, height: 44, backgroundColor: active ? theme.colors.primary.default : 'transparent' }}
769
+ activeOpacity={0.7}
770
+ >
771
+ <Ionicons name={tab.icon} size={14} color={active ? '#FFF' : theme.colors.text.tertiary} />
772
+ <Text variant="caption" bold={active} style={{ color: active ? '#FFF' : theme.colors.text.tertiary }}>
773
+ {tab.label}
774
+ </Text>
775
+ </TouchableOpacity>
776
+ );
777
+ })}
778
+ </View>
779
+ {mobileTab === 'rounds' && renderRounds()}
780
+ {mobileTab === 'leaderboard' && renderLeaderboard()}
781
+ {mobileTab === 'items' && renderAllItems()}
782
+ </ScrollView>
783
+ </KeyboardAvoidingView>
429
784
  )}
430
785
  </View>
431
786
  );