@bettoredge/calcutta 0.4.1 → 0.5.0

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.
@@ -1,5 +1,5 @@
1
- import React, { useState, useMemo, useCallback, useEffect } from 'react';
2
- import { StyleSheet, TouchableOpacity, ActivityIndicator, ScrollView, Image, FlatList, TextInput, Platform } from 'react-native';
1
+ import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react';
2
+ import { StyleSheet, TouchableOpacity, ActivityIndicator, ScrollView, Image, FlatList, TextInput, Platform, useWindowDimensions, RefreshControl } 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';
@@ -15,6 +15,7 @@ import { CalcuttaCountdown } from './CalcuttaCountdown';
15
15
  import { AuctionInfoChips } from './AuctionInfoChips';
16
16
  import { EscrowWidget } from './EscrowWidget';
17
17
  import { AuctionCountdownOverlay } from './AuctionCountdownOverlay';
18
+ import { CalcuttaWalkthrough } from './CalcuttaWalkthrough';
18
19
  import { useCalcuttaSocket } from '../hooks/useCalcuttaSocket';
19
20
 
20
21
  export interface CalcuttaDetailProps {
@@ -31,6 +32,10 @@ export interface CalcuttaDetailProps {
31
32
  onManage?: () => void;
32
33
  player_balance?: number;
33
34
  onDepositFunds?: (amount: number) => void;
35
+ initialShowWalkthrough?: boolean;
36
+ onWalkthroughDismiss?: () => void;
37
+ onRefresh?: () => void | Promise<void>;
38
+ onAuctionStarted?: () => void;
34
39
  }
35
40
 
36
41
  const getStatusConfig = (status: string) => {
@@ -54,12 +59,31 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
54
59
  onManage,
55
60
  player_balance,
56
61
  onDepositFunds,
62
+ initialShowWalkthrough,
63
+ onWalkthroughDismiss,
57
64
  access_token,
58
65
  device_id,
59
66
  player_username,
60
67
  player_profile_pic,
68
+ onRefresh,
69
+ onAuctionStarted,
61
70
  }) => {
62
71
  const { theme } = useTheme();
72
+ const { width: windowWidth } = useWindowDimensions();
73
+ const [containerWidth, setContainerWidth] = useState(0);
74
+ const measuredWidth = containerWidth || windowWidth;
75
+ const isDesktop = Platform.OS === 'web' && measuredWidth >= 700;
76
+ const [refreshing, setRefreshing] = useState(false);
77
+
78
+ // Walkthrough state
79
+ const [showWalkthrough, setShowWalkthrough] = useState(false);
80
+ const walkthroughTriggered = useRef(false);
81
+ useEffect(() => {
82
+ if (initialShowWalkthrough && !walkthroughTriggered.current) {
83
+ walkthroughTriggered.current = true;
84
+ setShowWalkthrough(true);
85
+ }
86
+ }, [initialShowWalkthrough]);
63
87
 
64
88
  // Countdown overlay state
65
89
  const [showCountdown, setShowCountdown] = useState(false);
@@ -102,6 +126,14 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
102
126
  const [leaving, setLeaving] = useState(false);
103
127
  const [startingComp, setStartingComp] = useState(false);
104
128
  const [itemSearch, setItemSearch] = useState('');
129
+ const [mobileListTab, setMobileListTab] = useState<'items' | 'players'>('items');
130
+
131
+ const handleRefresh = async () => {
132
+ setRefreshing(true);
133
+ try {
134
+ await Promise.all([refresh(), fetchEscrow(), onRefresh?.()]);
135
+ } catch {} finally { setRefreshing(false); }
136
+ };
105
137
 
106
138
  const filteredItems = useMemo(() => {
107
139
  if (competition?.auction_type !== 'sweepstakes' || !itemSearch.trim()) return items;
@@ -164,13 +196,124 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
164
196
  ...(showEscrow ? [{ key: 'escrow' }] : []),
165
197
  { key: 'stats' },
166
198
  { key: 'payouts' },
167
- { key: 'items-header' },
168
- { key: 'items' },
169
- { key: 'participants-header' },
170
- { key: 'participants' },
199
+ { key: 'items-players-tabbed' },
171
200
  ...(canLeave ? [{ key: 'leave' }] : []),
172
201
  ];
173
202
 
203
+ const renderItemsList = () => {
204
+ if (items.length === 0) {
205
+ return (
206
+ <View variant="transparent" style={{ padding: 20, alignItems: 'center' }}>
207
+ <Ionicons name="list-outline" size={32} color={theme.colors.text.tertiary} />
208
+ <Text variant="caption" color="tertiary" style={{ marginTop: 8 }}>No items added yet.</Text>
209
+ </View>
210
+ );
211
+ }
212
+
213
+ const displayItems = isSweepstakes ? filteredItems : items;
214
+ return (
215
+ <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
+ />
235
+ )}
236
+ {displayItems.map(item => {
237
+ const imgUrl = resolveItemImageUrl(item.item_image) || itemImages[item.item_id]?.url;
238
+ const owner = item.winning_player_id ? enrichedPlayers[item.winning_player_id] : undefined;
239
+ const ownerName = owner?.username || owner?.show_name;
240
+ const isMe = item.winning_player_id == player_id;
241
+ return (
242
+ <View key={item.calcutta_auction_item_id} variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: 8, borderBottomWidth: 1, borderColor: theme.colors.border.subtle }}>
243
+ {imgUrl ? (
244
+ <Image source={{ uri: imgUrl }} style={{ width: 36, height: 36, borderRadius: 8 }} resizeMode="cover" />
245
+ ) : (
246
+ <View variant="transparent" style={{ width: 36, height: 36, borderRadius: 8, backgroundColor: theme.colors.surface.elevated, alignItems: 'center', justifyContent: 'center' }}>
247
+ <Ionicons name="trophy-outline" size={16} color={theme.colors.text.tertiary} />
248
+ </View>
249
+ )}
250
+ <View variant="transparent" style={{ marginLeft: 10, flex: 1 }}>
251
+ <Text variant="body">{item.item_name}</Text>
252
+ {isSweepstakes && ownerName ? (
253
+ <Text variant="caption" color="tertiary">{isMe ? 'You' : ownerName}</Text>
254
+ ) : isSweepstakes ? (
255
+ <Text variant="caption" color="tertiary">Available</Text>
256
+ ) : null}
257
+ </View>
258
+ {item.seed != null && <Text variant="caption" color="tertiary">#{item.seed}</Text>}
259
+ {isMe && <Text variant="caption" bold style={{ color: theme.colors.primary.default, marginLeft: 8 }}>YOU</Text>}
260
+ </View>
261
+ );
262
+ })}
263
+ </View>
264
+ );
265
+ };
266
+
267
+ const renderParticipantsList = () => {
268
+ if (participants.length === 0) {
269
+ return (
270
+ <View variant="transparent" style={{ padding: 20, alignItems: 'center' }}>
271
+ <Ionicons name="people-outline" size={32} color={theme.colors.text.tertiary} />
272
+ <Text variant="caption" color="tertiary" style={{ marginTop: 8 }}>No players yet. Share the code to invite friends!</Text>
273
+ </View>
274
+ );
275
+ }
276
+ return (
277
+ <View variant="transparent" style={{ paddingHorizontal: 16, paddingBottom: 8 }}>
278
+ {participants.map((p, i) => {
279
+ const enriched = enrichedPlayers[p.player_id];
280
+ const username = enriched?.username || enriched?.show_name || `Player ${p.player_id.slice(0, 6)}`;
281
+ const profilePic = enriched?.profile_pic;
282
+ const isOnline = onlinePlayerIds.has(p.player_id);
283
+ return (
284
+ <View key={p.calcutta_participant_id} variant="transparent" style={[styles.participantRow, i < participants.length - 1 && { borderBottomWidth: 1, borderColor: theme.colors.border.subtle }]}>
285
+ <View variant="transparent" style={{ position: 'relative' }}>
286
+ {profilePic ? (
287
+ <Image source={{ uri: profilePic }} style={[styles.avatarCircle, { overflow: 'hidden' }]} />
288
+ ) : (
289
+ <View variant="transparent" style={[styles.avatarCircle, { backgroundColor: theme.colors.primary.subtle }]}>
290
+ <Text variant="caption" bold style={{ color: theme.colors.primary.default }}>
291
+ {username.charAt(0).toUpperCase()}
292
+ </Text>
293
+ </View>
294
+ )}
295
+ {isOnline && (
296
+ <View variant="transparent" style={{ position: 'absolute', bottom: 0, right: 0, width: 10, height: 10, borderRadius: 5, backgroundColor: '#10B981', borderWidth: 2, borderColor: theme.colors.surface.base }} />
297
+ )}
298
+ </View>
299
+ <View variant="transparent" style={{ flex: 1 }}>
300
+ <Text variant="body">{username}</Text>
301
+ <Text variant="caption" color="tertiary">
302
+ {p.items_owned > 0 ? `${p.items_owned} items \u00B7 ${formatCurrency(p.total_spent, competition.market_type)} spent` : 'Joined'}
303
+ </Text>
304
+ </View>
305
+ {p.player_id == player_id && (
306
+ <View variant="transparent" style={[styles.youBadge, { backgroundColor: theme.colors.primary.subtle }]}>
307
+ <Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 10, lineHeight: 13 }}>YOU</Text>
308
+ </View>
309
+ )}
310
+ </View>
311
+ );
312
+ })}
313
+ </View>
314
+ );
315
+ };
316
+
174
317
  const renderSection = ({ item }: { item: { key: string } }) => {
175
318
  switch (item.key) {
176
319
  case 'hero':
@@ -194,6 +337,14 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
194
337
  {isSweepstakes ? 'Sweepstakes' : competition.auction_type === 'live' ? 'Live Auction' : 'Sealed Bid'}
195
338
  </Text>
196
339
  <View variant="transparent" style={{ flex: 1 }} />
340
+ <TouchableOpacity
341
+ onPress={() => setShowWalkthrough(true)}
342
+ style={{ marginRight: 8, padding: 4 }}
343
+ activeOpacity={0.7}
344
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
345
+ >
346
+ <Ionicons name="help-circle-outline" size={20} color={theme.colors.text.tertiary} />
347
+ </TouchableOpacity>
197
348
  {isAdmin && onManage && (
198
349
  <TouchableOpacity
199
350
  onPress={onManage}
@@ -275,6 +426,7 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
275
426
  hasJoined={hasJoined}
276
427
  escrowBalance={Number(escrow?.escrow_balance ?? 0)}
277
428
  isAdmin={isAdmin}
429
+ participantCount={participants.length}
278
430
  onJoin={onJoin ? async () => {
279
431
  setJoining(true);
280
432
  try { await onJoin(); await refresh(); fetchEscrow(); } catch {}
@@ -406,148 +558,43 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
406
558
  </View>
407
559
  );
408
560
 
409
- case 'items-header':
410
- if (items.length === 0) return null;
411
- return (
412
- <View variant="transparent" style={[styles.section, { borderColor: theme.colors.border.subtle, paddingBottom: 0 }]}>
413
- <Text variant="body" bold style={styles.sectionTitle}>{isSweepstakes ? 'Teams' : 'Auction Items'} ({items.length})</Text>
414
- </View>
415
- );
416
-
417
561
  case 'items':
418
- if (items.length === 0) return null;
419
- if (isSweepstakes) {
420
- return (
421
- <View variant="transparent" style={{ paddingHorizontal: 16, paddingBottom: 8 }}>
422
- <TextInput
423
- style={{
424
- height: 36,
425
- borderRadius: 8,
426
- borderWidth: 1,
427
- borderColor: theme.colors.border.subtle,
428
- backgroundColor: theme.colors.surface.input,
429
- color: theme.colors.text.primary,
430
- paddingHorizontal: 12,
431
- fontSize: 14,
432
- lineHeight: 19,
433
- marginBottom: 8,
434
- }}
435
- placeholder="Search by team or owner..."
436
- placeholderTextColor={theme.colors.text.tertiary}
437
- value={itemSearch}
438
- onChangeText={setItemSearch}
439
- autoCapitalize="none"
440
- autoCorrect={false}
441
- onFocus={(e: any) => {
442
- if (Platform.OS === 'web' && e?.target?.scrollIntoView) {
443
- setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300);
444
- }
445
- }}
446
- />
447
- {filteredItems.map(item => {
448
- const imgUrl = resolveItemImageUrl(item.item_image) || itemImages[item.item_id]?.url;
449
- const owner = item.winning_player_id ? enrichedPlayers[item.winning_player_id] : undefined;
450
- const ownerName = owner?.username || owner?.show_name;
451
- const isMe = item.winning_player_id == player_id;
452
- return (
453
- <View key={item.calcutta_auction_item_id} variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: 8, borderBottomWidth: 1, borderColor: theme.colors.border.subtle }}>
454
- {imgUrl ? (
455
- <Image source={{ uri: imgUrl }} style={{ width: 28, height: 28, borderRadius: 6 }} resizeMode="cover" />
456
- ) : (
457
- <View variant="transparent" style={{ width: 28, height: 28, borderRadius: 6, backgroundColor: theme.colors.surface.elevated, alignItems: 'center', justifyContent: 'center' }}>
458
- <Ionicons name="trophy-outline" size={14} color={theme.colors.text.tertiary} />
459
- </View>
460
- )}
461
- <View variant="transparent" style={{ marginLeft: 8, flex: 1 }}>
462
- <Text variant="body">{item.item_name}</Text>
463
- {ownerName ? (
464
- <Text variant="caption" color="tertiary">{isMe ? 'You' : ownerName}</Text>
465
- ) : (
466
- <Text variant="caption" color="tertiary">Available</Text>
467
- )}
468
- </View>
469
- {item.seed != null && <Text variant="caption" color="tertiary">#{item.seed}</Text>}
470
- {isMe && <Text variant="caption" bold style={{ color: theme.colors.primary.default, marginLeft: 8 }}>YOU</Text>}
471
- </View>
472
- );
473
- })}
474
- </View>
475
- );
476
- }
477
- return (
478
- <View variant="transparent" style={{ paddingHorizontal: 16 }}>
479
- <ScrollView horizontal showsHorizontalScrollIndicator={false}>
480
- {items.map(item => (
481
- <View key={item.calcutta_auction_item_id} variant="transparent" style={[styles.itemChip, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
482
- <Text variant="caption" bold numberOfLines={1}>{item.item_name}</Text>
483
- {item.seed != null && <Text variant="caption" color="tertiary" style={{ fontSize: 10, lineHeight: 13 }}>#{item.seed}</Text>}
484
- </View>
485
- ))}
486
- </ScrollView>
487
- </View>
488
- );
489
-
490
- case 'participants-header':
491
- return (
492
- <View variant="transparent" style={[styles.section, { borderColor: theme.colors.border.subtle, paddingBottom: 0 }]}>
493
- <View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 10 }}>
494
- <Text variant="body" bold>Players ({participants.length})</Text>
495
- {onlinePlayers.length > 0 && (
496
- <View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', marginLeft: 10, backgroundColor: '#10B98115', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 10 }}>
497
- <View variant="transparent" style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: '#10B981', marginRight: 4 }} />
498
- <Text variant="caption" bold style={{ color: '#10B981' }}>{onlinePlayers.length} online</Text>
499
- </View>
500
- )}
501
- </View>
502
- </View>
503
- );
562
+ return renderItemsList();
504
563
 
505
564
  case 'participants':
506
- if (participants.length === 0) {
507
- return (
508
- <View variant="transparent" style={{ padding: 20, alignItems: 'center' }}>
509
- <Ionicons name="people-outline" size={32} color={theme.colors.text.tertiary} />
510
- <Text variant="caption" color="tertiary" style={{ marginTop: 8 }}>No players yet. Share the code to invite friends!</Text>
511
- </View>
512
- );
513
- }
565
+ return renderParticipantsList();
566
+
567
+ case 'items-players-tabbed':
514
568
  return (
515
- <View variant="transparent" style={{ paddingHorizontal: 16, paddingBottom: 40 }}>
516
- {participants.map((p, i) => {
517
- const enriched = enrichedPlayers[p.player_id];
518
- const username = enriched?.username || enriched?.show_name || `Player ${p.player_id.slice(0, 6)}`;
519
- const profilePic = enriched?.profile_pic;
520
- const isOnline = onlinePlayerIds.has(p.player_id);
521
- return (
522
- <View key={p.calcutta_participant_id} variant="transparent" style={[styles.participantRow, i < participants.length - 1 && { borderBottomWidth: 1, borderColor: theme.colors.border.subtle }]}>
523
- <View variant="transparent" style={{ position: 'relative' }}>
524
- {profilePic ? (
525
- <Image source={{ uri: profilePic }} style={[styles.avatarCircle, { overflow: 'hidden' }]} />
526
- ) : (
527
- <View variant="transparent" style={[styles.avatarCircle, { backgroundColor: theme.colors.primary.subtle }]}>
528
- <Text variant="caption" bold style={{ color: theme.colors.primary.default }}>
529
- {username.charAt(0).toUpperCase()}
530
- </Text>
531
- </View>
532
- )}
533
- {isOnline && (
534
- <View variant="transparent" style={{ position: 'absolute', bottom: 0, right: 0, width: 10, height: 10, borderRadius: 5, backgroundColor: '#10B981', borderWidth: 2, borderColor: theme.colors.surface.base }} />
535
- )}
536
- </View>
537
- <View variant="transparent" style={{ flex: 1 }}>
538
- <Text variant="body">{username}</Text>
539
- <Text variant="caption" color="tertiary">
540
- {p.items_owned > 0 ? `${p.items_owned} items \u00B7 ${formatCurrency(p.total_spent, competition.market_type)} spent` : 'Joined'}
541
- </Text>
569
+ <View variant="transparent" style={[styles.section, { borderColor: theme.colors.border.subtle }]}>
570
+ {/* Tab toggle */}
571
+ <View variant="transparent" style={[styles.listTabBar, { borderColor: theme.colors.border.subtle }]}>
572
+ <TouchableOpacity
573
+ style={[styles.listTab, mobileListTab === 'items' && { borderBottomWidth: 2, borderBottomColor: theme.colors.primary.default }]}
574
+ onPress={() => setMobileListTab('items')}
575
+ >
576
+ <Ionicons name="list-outline" size={16} color={mobileListTab === 'items' ? theme.colors.primary.default : theme.colors.text.tertiary} style={{ marginRight: 6 }} />
577
+ <Text variant="body" bold style={{ color: mobileListTab === 'items' ? theme.colors.primary.default : theme.colors.text.tertiary }}>
578
+ {isSweepstakes ? 'Teams' : 'Items'} ({items.length})
579
+ </Text>
580
+ </TouchableOpacity>
581
+ <TouchableOpacity
582
+ style={[styles.listTab, mobileListTab === 'players' && { borderBottomWidth: 2, borderBottomColor: theme.colors.primary.default }]}
583
+ onPress={() => setMobileListTab('players')}
584
+ >
585
+ <Ionicons name="people-outline" size={16} color={mobileListTab === 'players' ? theme.colors.primary.default : theme.colors.text.tertiary} style={{ marginRight: 6 }} />
586
+ <Text variant="body" bold style={{ color: mobileListTab === 'players' ? theme.colors.primary.default : theme.colors.text.tertiary }}>
587
+ Players ({participants.length})
588
+ </Text>
589
+ {onlinePlayers.length > 0 && (
590
+ <View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', marginLeft: 8, backgroundColor: '#10B98115', paddingHorizontal: 6, paddingVertical: 2, borderRadius: 8 }}>
591
+ <View variant="transparent" style={{ width: 5, height: 5, borderRadius: 2.5, backgroundColor: '#10B981', marginRight: 3 }} />
592
+ <Text variant="caption" bold style={{ color: '#10B981', fontSize: 10, lineHeight: 13 }}>{onlinePlayers.length}</Text>
542
593
  </View>
543
- {p.player_id == player_id && (
544
- <View variant="transparent" style={[styles.youBadge, { backgroundColor: theme.colors.primary.subtle }]}>
545
- <Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 10, lineHeight: 13 }}>YOU</Text>
546
- </View>
547
- )}
548
- </View>
549
- );
550
- })}
594
+ )}
595
+ </TouchableOpacity>
596
+ </View>
597
+ {mobileListTab === 'items' ? renderItemsList() : renderParticipantsList()}
551
598
  </View>
552
599
  );
553
600
 
@@ -556,32 +603,65 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
556
603
  }
557
604
  };
558
605
 
559
- const socketColor = socketState === 'authenticated' ? '#10B981'
560
- : socketState === 'connected' ? '#F59E0B'
561
- : socketState === 'connecting' ? '#3B82F6'
562
- : '#EF4444';
563
-
564
606
  return (
565
- <View variant="base" style={styles.container}>
566
- {/* Socket debug bar */}
567
- <View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 6, backgroundColor: socketColor + '15', borderBottomWidth: 1, borderColor: socketColor + '30' }}>
568
- <View variant="transparent" style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: socketColor, marginRight: 6 }} />
569
- <Text variant="caption" style={{ color: socketColor }}>
570
- Socket: {socketState} {onlinePlayers.length > 0 ? `· ${onlinePlayers.length} in room` : ''}
571
- </Text>
572
- {!access_token && <Text variant="caption" style={{ color: '#EF4444', marginLeft: 8 }}>No auth token</Text>}
573
- </View>
607
+ <View variant="transparent" style={[styles.container, { backgroundColor: theme.colors.surface.base }]} onLayout={e => setContainerWidth(e.nativeEvent.layout.width)}>
608
+ {isDesktop ? (
609
+ <ScrollView style={{ flex: 1, backgroundColor: theme.colors.surface.base }} contentContainerStyle={{ padding: 12, paddingBottom: 40, backgroundColor: theme.colors.surface.base }} keyboardShouldPersistTaps="handled" refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}>
610
+ {/* Row 1: Hero + Info/Action left, Items right */}
611
+ <View variant="transparent" style={{ flexDirection: 'row', gap: 12 }}>
612
+ <View variant="transparent" style={{ flex: 3 }}>
613
+ {renderSection({ item: { key: 'hero' } })}
614
+ {renderSection({ item: { key: 'info' } })}
615
+ {renderSection({ item: { key: 'action-card' } })}
616
+ {showEscrow && renderSection({ item: { key: 'escrow' } })}
617
+ </View>
618
+ <View variant="transparent" style={[styles.desktopPanel, { flex: 2, backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
619
+ <Text variant="caption" bold color="tertiary" style={styles.desktopPanelTitle}>
620
+ {isSweepstakes ? 'Teams' : 'Auction Items'} ({items.length})
621
+ </Text>
622
+ <ScrollView nestedScrollEnabled showsVerticalScrollIndicator={false}>
623
+ {renderSection({ item: { key: 'items' } })}
624
+ </ScrollView>
625
+ </View>
626
+ </View>
574
627
 
575
- <FlatList
576
- data={sections}
577
- keyExtractor={(item) => item.key}
578
- renderItem={renderSection}
579
- extraData={`${escrowExpanded}-${onlinePlayers.length}`}
580
- showsVerticalScrollIndicator={false}
581
- style={{ backgroundColor: theme.colors.surface.base }}
582
- contentContainerStyle={{ paddingBottom: Platform.OS === 'web' ? 300 : 40 }}
583
- keyboardShouldPersistTaps="handled"
584
- />
628
+ {/* Row 2: Stats + Payouts + Participants */}
629
+ <View variant="transparent" style={{ flexDirection: 'row', gap: 12, marginTop: 12 }}>
630
+ <View variant="transparent" style={{ flex: 1 }}>
631
+ {renderSection({ item: { key: 'stats' } })}
632
+ </View>
633
+ </View>
634
+
635
+ <View variant="transparent" style={{ flexDirection: 'row', gap: 12, marginTop: 12 }}>
636
+ <View variant="transparent" style={[styles.desktopPanel, { flex: 1, backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
637
+ <Text variant="caption" bold color="tertiary" style={styles.desktopPanelTitle}>Payouts</Text>
638
+ {renderSection({ item: { key: 'payouts' } })}
639
+ </View>
640
+ <View variant="transparent" style={[styles.desktopPanel, { flex: 1, backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
641
+ <Text variant="caption" bold color="tertiary" style={styles.desktopPanelTitle}>
642
+ Players ({participants.length})
643
+ </Text>
644
+ <ScrollView nestedScrollEnabled showsVerticalScrollIndicator={false} style={{ maxHeight: 300 }}>
645
+ {renderSection({ item: { key: 'participants' } })}
646
+ </ScrollView>
647
+ </View>
648
+ </View>
649
+
650
+ {canLeave && renderSection({ item: { key: 'leave' } })}
651
+ </ScrollView>
652
+ ) : (
653
+ <FlatList
654
+ data={sections}
655
+ keyExtractor={(item) => item.key}
656
+ renderItem={renderSection}
657
+ extraData={`${escrowExpanded}-${onlinePlayers.length}`}
658
+ showsVerticalScrollIndicator={false}
659
+ style={{ backgroundColor: theme.colors.surface.base }}
660
+ contentContainerStyle={{ paddingBottom: Platform.OS === 'web' ? 300 : 40 }}
661
+ keyboardShouldPersistTaps="handled"
662
+ refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}
663
+ />
664
+ )}
585
665
 
586
666
  {/* Auction starting countdown overlay */}
587
667
  <AuctionCountdownOverlay
@@ -592,8 +672,18 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
592
672
  onComplete={() => {
593
673
  setShowCountdown(false);
594
674
  refresh();
675
+ onAuctionStarted?.();
595
676
  }}
596
677
  />
678
+
679
+ {/* Walkthrough */}
680
+ {competition && (
681
+ <CalcuttaWalkthrough
682
+ visible={showWalkthrough}
683
+ onClose={() => { setShowWalkthrough(false); onWalkthroughDismiss?.(); }}
684
+ auction_type={competition.auction_type as any}
685
+ />
686
+ )}
597
687
  </View>
598
688
  );
599
689
  };
@@ -617,10 +707,13 @@ const styles = StyleSheet.create({
617
707
  section: { padding: 16, borderTopWidth: 1 },
618
708
  sectionTitle: { marginBottom: 10 },
619
709
  payoutRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10 },
620
- itemChip: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 8, borderWidth: 1, marginRight: 8, minWidth: 80, alignItems: 'center' },
621
710
  participantRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10 },
622
711
  avatarCircle: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', marginRight: 10 },
623
712
  youBadge: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 8 },
624
713
  assignedItemCard: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 10, borderWidth: 1 },
625
714
  assignedItemImage: { width: 44, height: 44, borderRadius: 8 },
715
+ listTabBar: { flexDirection: 'row', borderBottomWidth: 1, marginBottom: 8 },
716
+ listTab: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 10 },
717
+ desktopPanel: { borderRadius: 12, borderWidth: 1, padding: 14 },
718
+ desktopPanelTitle: { textTransform: 'uppercase', letterSpacing: 1, fontSize: 10, lineHeight: 13, marginBottom: 8 },
626
719
  });
@@ -226,7 +226,7 @@ export const CalcuttaEscrow: React.FC<CalcuttaEscrowComponentProps> = ({
226
226
  activeOpacity={0.7}
227
227
  >
228
228
  <Ionicons
229
- name="arrow-down-outline"
229
+ name="arrow-up-outline"
230
230
  size={18}
231
231
  color={action === 'transfer_in' ? '#FFFFFF' : theme.colors.text.secondary}
232
232
  />
@@ -250,7 +250,7 @@ export const CalcuttaEscrow: React.FC<CalcuttaEscrowComponentProps> = ({
250
250
  activeOpacity={0.7}
251
251
  >
252
252
  <Ionicons
253
- name="arrow-up-outline"
253
+ name="arrow-down-outline"
254
254
  size={18}
255
255
  color={action === 'transfer_out' ? '#FFFFFF' : theme.colors.text.secondary}
256
256
  />
@@ -307,7 +307,7 @@ export const CalcuttaEscrow: React.FC<CalcuttaEscrowComponentProps> = ({
307
307
  ? theme.colors.status.error
308
308
  : (loading || isInvalidAmount)
309
309
  ? theme.colors.surface.elevated
310
- : theme.colors.primary.default,
310
+ : theme.colors.status.success,
311
311
  }]}
312
312
  onPress={handleAction}
313
313
  disabled={loading || (isInvalidAmount && !(insufficientBalance && onDepositFunds))}
@@ -318,7 +318,7 @@ export const CalcuttaEscrow: React.FC<CalcuttaEscrowComponentProps> = ({
318
318
  ) : (
319
319
  <>
320
320
  <Ionicons
321
- name={action === 'transfer_in' ? 'arrow-down-circle' : 'arrow-up-circle'}
321
+ name={action === 'transfer_in' ? 'arrow-up-circle' : 'arrow-down-circle'}
322
322
  size={20}
323
323
  color={isInvalidAmount ? theme.colors.text.tertiary : '#FFFFFF'}
324
324
  />