@bettoredge/calcutta 0.4.1 → 0.4.2

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.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Calcutta auction competition components for BettorEdge applications",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -15,6 +15,7 @@ export interface CalcuttaActionCardProps {
15
15
  activeBidCount?: number;
16
16
  itemsWon?: number;
17
17
  isAdmin?: boolean;
18
+ participantCount?: number;
18
19
  onJoin?: () => void;
19
20
  onDepositEscrow?: () => void;
20
21
  onManage?: () => void;
@@ -30,8 +31,12 @@ interface CardConfig {
30
31
  ctaLabel?: string;
31
32
  ctaColor?: string;
32
33
  ctaAction?: () => void;
34
+ secondaryCtaLabel?: string;
35
+ secondaryCtaColor?: string;
36
+ secondaryCtaAction?: () => void;
33
37
  showCountdown?: 'start' | 'end';
34
38
  loading?: boolean;
39
+ secondaryLoading?: boolean;
35
40
  }
36
41
 
37
42
  export const CalcuttaActionCard: React.FC<CalcuttaActionCardProps> = ({
@@ -42,6 +47,7 @@ export const CalcuttaActionCard: React.FC<CalcuttaActionCardProps> = ({
42
47
  activeBidCount = 0,
43
48
  itemsWon = 0,
44
49
  isAdmin,
50
+ participantCount = 0,
45
51
  onJoin,
46
52
  onDepositEscrow,
47
53
  onManage,
@@ -56,8 +62,7 @@ export const CalcuttaActionCard: React.FC<CalcuttaActionCardProps> = ({
56
62
  const getConfig = (): CardConfig => {
57
63
  // Admin: can start auction
58
64
  if (isAdmin && lifecycleState === 'scheduled' && onStartAuction) {
59
- const participantCount = competition.participants?.length ?? 0;
60
- return {
65
+ const config: CardConfig = {
61
66
  icon: 'play-circle-outline',
62
67
  title: 'Ready to start?',
63
68
  description: `${participantCount} player${participantCount !== 1 ? 's' : ''} have joined. Start the ${isSweepstakes ? 'competition' : 'auction'} when ready.`,
@@ -67,6 +72,14 @@ export const CalcuttaActionCard: React.FC<CalcuttaActionCardProps> = ({
67
72
  ctaAction: onStartAuction,
68
73
  showCountdown: 'start',
69
74
  };
75
+ // Admin hasn't joined yet — show join as secondary action
76
+ if (!hasJoined && onJoin) {
77
+ config.secondaryCtaLabel = isFree ? 'Join Free' : `Join — ${formatCurrency(entryFee, competition.market_type)}`;
78
+ config.secondaryCtaColor = '#10B981';
79
+ config.secondaryCtaAction = onJoin;
80
+ config.secondaryLoading = joining;
81
+ }
82
+ return config;
70
83
  }
71
84
 
72
85
  // Not joined — prompt to join
@@ -228,6 +241,22 @@ export const CalcuttaActionCard: React.FC<CalcuttaActionCardProps> = ({
228
241
  </View>
229
242
  )}
230
243
 
244
+ {/* Secondary CTA Button */}
245
+ {config.secondaryCtaLabel && config.secondaryCtaAction && (
246
+ <TouchableOpacity
247
+ style={[styles.ctaButton, { backgroundColor: config.secondaryCtaColor || '#10B981' }]}
248
+ onPress={config.secondaryCtaAction}
249
+ activeOpacity={0.7}
250
+ disabled={config.secondaryLoading}
251
+ >
252
+ {config.secondaryLoading ? (
253
+ <ActivityIndicator size="small" color="#FFFFFF" />
254
+ ) : (
255
+ <Text variant="body" bold style={{ color: '#FFFFFF' }}>{config.secondaryCtaLabel}</Text>
256
+ )}
257
+ </TouchableOpacity>
258
+ )}
259
+
231
260
  {/* CTA Button */}
232
261
  {config.ctaLabel && config.ctaAction && (
233
262
  <TouchableOpacity
@@ -1078,13 +1078,6 @@ export const CalcuttaAuction: React.FC<CalcuttaAuctionProps> = (props) => {
1078
1078
  );
1079
1079
  }
1080
1080
 
1081
- // Post-auction: show results view for all types
1082
- const isPostAuction = ['auction_closed', 'inprogress', 'closed'].includes(competition.status);
1083
-
1084
- if (competition.auction_type === 'sweepstakes' || isPostAuction) {
1085
- return <SweepstakesWaiting {...props} competition={competition} />;
1086
- }
1087
-
1088
1081
  if (competition.auction_type === 'sealed_bid') {
1089
1082
  return <SealedBidAuction {...props} />;
1090
1083
  }
@@ -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 } 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,8 @@ export interface CalcuttaDetailProps {
31
32
  onManage?: () => void;
32
33
  player_balance?: number;
33
34
  onDepositFunds?: (amount: number) => void;
35
+ initialShowWalkthrough?: boolean;
36
+ onWalkthroughDismiss?: () => void;
34
37
  }
35
38
 
36
39
  const getStatusConfig = (status: string) => {
@@ -54,12 +57,28 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
54
57
  onManage,
55
58
  player_balance,
56
59
  onDepositFunds,
60
+ initialShowWalkthrough,
61
+ onWalkthroughDismiss,
57
62
  access_token,
58
63
  device_id,
59
64
  player_username,
60
65
  player_profile_pic,
61
66
  }) => {
62
67
  const { theme } = useTheme();
68
+ const { width: windowWidth } = useWindowDimensions();
69
+ const [containerWidth, setContainerWidth] = useState(0);
70
+ const measuredWidth = containerWidth || windowWidth;
71
+ const isDesktop = Platform.OS === 'web' && measuredWidth >= 700;
72
+
73
+ // Walkthrough state
74
+ const [showWalkthrough, setShowWalkthrough] = useState(false);
75
+ const walkthroughTriggered = useRef(false);
76
+ useEffect(() => {
77
+ if (initialShowWalkthrough && !walkthroughTriggered.current) {
78
+ walkthroughTriggered.current = true;
79
+ setShowWalkthrough(true);
80
+ }
81
+ }, [initialShowWalkthrough]);
63
82
 
64
83
  // Countdown overlay state
65
84
  const [showCountdown, setShowCountdown] = useState(false);
@@ -102,6 +121,7 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
102
121
  const [leaving, setLeaving] = useState(false);
103
122
  const [startingComp, setStartingComp] = useState(false);
104
123
  const [itemSearch, setItemSearch] = useState('');
124
+ const [mobileListTab, setMobileListTab] = useState<'items' | 'players'>('items');
105
125
 
106
126
  const filteredItems = useMemo(() => {
107
127
  if (competition?.auction_type !== 'sweepstakes' || !itemSearch.trim()) return items;
@@ -164,13 +184,124 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
164
184
  ...(showEscrow ? [{ key: 'escrow' }] : []),
165
185
  { key: 'stats' },
166
186
  { key: 'payouts' },
167
- { key: 'items-header' },
168
- { key: 'items' },
169
- { key: 'participants-header' },
170
- { key: 'participants' },
187
+ { key: 'items-players-tabbed' },
171
188
  ...(canLeave ? [{ key: 'leave' }] : []),
172
189
  ];
173
190
 
191
+ const renderItemsList = () => {
192
+ if (items.length === 0) {
193
+ return (
194
+ <View variant="transparent" style={{ padding: 20, alignItems: 'center' }}>
195
+ <Ionicons name="list-outline" size={32} color={theme.colors.text.tertiary} />
196
+ <Text variant="caption" color="tertiary" style={{ marginTop: 8 }}>No items added yet.</Text>
197
+ </View>
198
+ );
199
+ }
200
+
201
+ const displayItems = isSweepstakes ? filteredItems : items;
202
+ return (
203
+ <View variant="transparent" style={{ paddingHorizontal: 16, paddingBottom: 8 }}>
204
+ {isSweepstakes && (
205
+ <TextInput
206
+ style={{
207
+ height: 36, borderRadius: 8, borderWidth: 1,
208
+ borderColor: theme.colors.border.subtle, backgroundColor: theme.colors.surface.input,
209
+ color: theme.colors.text.primary, paddingHorizontal: 12, fontSize: 14, lineHeight: 19, marginBottom: 8,
210
+ }}
211
+ placeholder="Search by team or owner..."
212
+ placeholderTextColor={theme.colors.text.tertiary}
213
+ value={itemSearch}
214
+ onChangeText={setItemSearch}
215
+ autoCapitalize="none"
216
+ autoCorrect={false}
217
+ onFocus={(e: any) => {
218
+ if (Platform.OS === 'web' && e?.target?.scrollIntoView) {
219
+ setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300);
220
+ }
221
+ }}
222
+ />
223
+ )}
224
+ {displayItems.map(item => {
225
+ const imgUrl = resolveItemImageUrl(item.item_image) || itemImages[item.item_id]?.url;
226
+ const owner = item.winning_player_id ? enrichedPlayers[item.winning_player_id] : undefined;
227
+ const ownerName = owner?.username || owner?.show_name;
228
+ const isMe = item.winning_player_id == player_id;
229
+ return (
230
+ <View key={item.calcutta_auction_item_id} variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: 8, borderBottomWidth: 1, borderColor: theme.colors.border.subtle }}>
231
+ {imgUrl ? (
232
+ <Image source={{ uri: imgUrl }} style={{ width: 36, height: 36, borderRadius: 8 }} resizeMode="cover" />
233
+ ) : (
234
+ <View variant="transparent" style={{ width: 36, height: 36, borderRadius: 8, backgroundColor: theme.colors.surface.elevated, alignItems: 'center', justifyContent: 'center' }}>
235
+ <Ionicons name="trophy-outline" size={16} color={theme.colors.text.tertiary} />
236
+ </View>
237
+ )}
238
+ <View variant="transparent" style={{ marginLeft: 10, flex: 1 }}>
239
+ <Text variant="body">{item.item_name}</Text>
240
+ {isSweepstakes && ownerName ? (
241
+ <Text variant="caption" color="tertiary">{isMe ? 'You' : ownerName}</Text>
242
+ ) : isSweepstakes ? (
243
+ <Text variant="caption" color="tertiary">Available</Text>
244
+ ) : null}
245
+ </View>
246
+ {item.seed != null && <Text variant="caption" color="tertiary">#{item.seed}</Text>}
247
+ {isMe && <Text variant="caption" bold style={{ color: theme.colors.primary.default, marginLeft: 8 }}>YOU</Text>}
248
+ </View>
249
+ );
250
+ })}
251
+ </View>
252
+ );
253
+ };
254
+
255
+ const renderParticipantsList = () => {
256
+ if (participants.length === 0) {
257
+ return (
258
+ <View variant="transparent" style={{ padding: 20, alignItems: 'center' }}>
259
+ <Ionicons name="people-outline" size={32} color={theme.colors.text.tertiary} />
260
+ <Text variant="caption" color="tertiary" style={{ marginTop: 8 }}>No players yet. Share the code to invite friends!</Text>
261
+ </View>
262
+ );
263
+ }
264
+ return (
265
+ <View variant="transparent" style={{ paddingHorizontal: 16, paddingBottom: 8 }}>
266
+ {participants.map((p, i) => {
267
+ const enriched = enrichedPlayers[p.player_id];
268
+ const username = enriched?.username || enriched?.show_name || `Player ${p.player_id.slice(0, 6)}`;
269
+ const profilePic = enriched?.profile_pic;
270
+ const isOnline = onlinePlayerIds.has(p.player_id);
271
+ return (
272
+ <View key={p.calcutta_participant_id} variant="transparent" style={[styles.participantRow, i < participants.length - 1 && { borderBottomWidth: 1, borderColor: theme.colors.border.subtle }]}>
273
+ <View variant="transparent" style={{ position: 'relative' }}>
274
+ {profilePic ? (
275
+ <Image source={{ uri: profilePic }} style={[styles.avatarCircle, { overflow: 'hidden' }]} />
276
+ ) : (
277
+ <View variant="transparent" style={[styles.avatarCircle, { backgroundColor: theme.colors.primary.subtle }]}>
278
+ <Text variant="caption" bold style={{ color: theme.colors.primary.default }}>
279
+ {username.charAt(0).toUpperCase()}
280
+ </Text>
281
+ </View>
282
+ )}
283
+ {isOnline && (
284
+ <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 }} />
285
+ )}
286
+ </View>
287
+ <View variant="transparent" style={{ flex: 1 }}>
288
+ <Text variant="body">{username}</Text>
289
+ <Text variant="caption" color="tertiary">
290
+ {p.items_owned > 0 ? `${p.items_owned} items \u00B7 ${formatCurrency(p.total_spent, competition.market_type)} spent` : 'Joined'}
291
+ </Text>
292
+ </View>
293
+ {p.player_id == player_id && (
294
+ <View variant="transparent" style={[styles.youBadge, { backgroundColor: theme.colors.primary.subtle }]}>
295
+ <Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 10, lineHeight: 13 }}>YOU</Text>
296
+ </View>
297
+ )}
298
+ </View>
299
+ );
300
+ })}
301
+ </View>
302
+ );
303
+ };
304
+
174
305
  const renderSection = ({ item }: { item: { key: string } }) => {
175
306
  switch (item.key) {
176
307
  case 'hero':
@@ -194,6 +325,14 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
194
325
  {isSweepstakes ? 'Sweepstakes' : competition.auction_type === 'live' ? 'Live Auction' : 'Sealed Bid'}
195
326
  </Text>
196
327
  <View variant="transparent" style={{ flex: 1 }} />
328
+ <TouchableOpacity
329
+ onPress={() => setShowWalkthrough(true)}
330
+ style={{ marginRight: 8, padding: 4 }}
331
+ activeOpacity={0.7}
332
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
333
+ >
334
+ <Ionicons name="help-circle-outline" size={20} color={theme.colors.text.tertiary} />
335
+ </TouchableOpacity>
197
336
  {isAdmin && onManage && (
198
337
  <TouchableOpacity
199
338
  onPress={onManage}
@@ -275,6 +414,7 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
275
414
  hasJoined={hasJoined}
276
415
  escrowBalance={Number(escrow?.escrow_balance ?? 0)}
277
416
  isAdmin={isAdmin}
417
+ participantCount={participants.length}
278
418
  onJoin={onJoin ? async () => {
279
419
  setJoining(true);
280
420
  try { await onJoin(); await refresh(); fetchEscrow(); } catch {}
@@ -406,148 +546,43 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
406
546
  </View>
407
547
  );
408
548
 
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
549
  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
- );
550
+ return renderItemsList();
504
551
 
505
552
  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
- }
553
+ return renderParticipantsList();
554
+
555
+ case 'items-players-tabbed':
514
556
  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>
557
+ <View variant="transparent" style={[styles.section, { borderColor: theme.colors.border.subtle }]}>
558
+ {/* Tab toggle */}
559
+ <View variant="transparent" style={[styles.listTabBar, { borderColor: theme.colors.border.subtle }]}>
560
+ <TouchableOpacity
561
+ style={[styles.listTab, mobileListTab === 'items' && { borderBottomWidth: 2, borderBottomColor: theme.colors.primary.default }]}
562
+ onPress={() => setMobileListTab('items')}
563
+ >
564
+ <Ionicons name="list-outline" size={16} color={mobileListTab === 'items' ? theme.colors.primary.default : theme.colors.text.tertiary} style={{ marginRight: 6 }} />
565
+ <Text variant="body" bold style={{ color: mobileListTab === 'items' ? theme.colors.primary.default : theme.colors.text.tertiary }}>
566
+ {isSweepstakes ? 'Teams' : 'Items'} ({items.length})
567
+ </Text>
568
+ </TouchableOpacity>
569
+ <TouchableOpacity
570
+ style={[styles.listTab, mobileListTab === 'players' && { borderBottomWidth: 2, borderBottomColor: theme.colors.primary.default }]}
571
+ onPress={() => setMobileListTab('players')}
572
+ >
573
+ <Ionicons name="people-outline" size={16} color={mobileListTab === 'players' ? theme.colors.primary.default : theme.colors.text.tertiary} style={{ marginRight: 6 }} />
574
+ <Text variant="body" bold style={{ color: mobileListTab === 'players' ? theme.colors.primary.default : theme.colors.text.tertiary }}>
575
+ Players ({participants.length})
576
+ </Text>
577
+ {onlinePlayers.length > 0 && (
578
+ <View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', marginLeft: 8, backgroundColor: '#10B98115', paddingHorizontal: 6, paddingVertical: 2, borderRadius: 8 }}>
579
+ <View variant="transparent" style={{ width: 5, height: 5, borderRadius: 2.5, backgroundColor: '#10B981', marginRight: 3 }} />
580
+ <Text variant="caption" bold style={{ color: '#10B981', fontSize: 10, lineHeight: 13 }}>{onlinePlayers.length}</Text>
542
581
  </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
- })}
582
+ )}
583
+ </TouchableOpacity>
584
+ </View>
585
+ {mobileListTab === 'items' ? renderItemsList() : renderParticipantsList()}
551
586
  </View>
552
587
  );
553
588
 
@@ -556,32 +591,64 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
556
591
  }
557
592
  };
558
593
 
559
- const socketColor = socketState === 'authenticated' ? '#10B981'
560
- : socketState === 'connected' ? '#F59E0B'
561
- : socketState === 'connecting' ? '#3B82F6'
562
- : '#EF4444';
563
-
564
594
  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>
595
+ <View variant="transparent" style={[styles.container, { backgroundColor: theme.colors.surface.base }]} onLayout={e => setContainerWidth(e.nativeEvent.layout.width)}>
596
+ {isDesktop ? (
597
+ <ScrollView style={{ flex: 1, backgroundColor: theme.colors.surface.base }} contentContainerStyle={{ padding: 12, paddingBottom: 40, backgroundColor: theme.colors.surface.base }} keyboardShouldPersistTaps="handled">
598
+ {/* Row 1: Hero + Info/Action left, Items right */}
599
+ <View variant="transparent" style={{ flexDirection: 'row', gap: 12 }}>
600
+ <View variant="transparent" style={{ flex: 3 }}>
601
+ {renderSection({ item: { key: 'hero' } })}
602
+ {renderSection({ item: { key: 'info' } })}
603
+ {renderSection({ item: { key: 'action-card' } })}
604
+ {showEscrow && renderSection({ item: { key: 'escrow' } })}
605
+ </View>
606
+ <View variant="transparent" style={[styles.desktopPanel, { flex: 2, backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
607
+ <Text variant="caption" bold color="tertiary" style={styles.desktopPanelTitle}>
608
+ {isSweepstakes ? 'Teams' : 'Auction Items'} ({items.length})
609
+ </Text>
610
+ <ScrollView nestedScrollEnabled showsVerticalScrollIndicator={false}>
611
+ {renderSection({ item: { key: 'items' } })}
612
+ </ScrollView>
613
+ </View>
614
+ </View>
574
615
 
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
- />
616
+ {/* Row 2: Stats + Payouts + Participants */}
617
+ <View variant="transparent" style={{ flexDirection: 'row', gap: 12, marginTop: 12 }}>
618
+ <View variant="transparent" style={{ flex: 1 }}>
619
+ {renderSection({ item: { key: 'stats' } })}
620
+ </View>
621
+ </View>
622
+
623
+ <View variant="transparent" style={{ flexDirection: 'row', gap: 12, marginTop: 12 }}>
624
+ <View variant="transparent" style={[styles.desktopPanel, { flex: 1, backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
625
+ <Text variant="caption" bold color="tertiary" style={styles.desktopPanelTitle}>Payouts</Text>
626
+ {renderSection({ item: { key: 'payouts' } })}
627
+ </View>
628
+ <View variant="transparent" style={[styles.desktopPanel, { flex: 1, backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
629
+ <Text variant="caption" bold color="tertiary" style={styles.desktopPanelTitle}>
630
+ Players ({participants.length})
631
+ </Text>
632
+ <ScrollView nestedScrollEnabled showsVerticalScrollIndicator={false} style={{ maxHeight: 300 }}>
633
+ {renderSection({ item: { key: 'participants' } })}
634
+ </ScrollView>
635
+ </View>
636
+ </View>
637
+
638
+ {canLeave && renderSection({ item: { key: 'leave' } })}
639
+ </ScrollView>
640
+ ) : (
641
+ <FlatList
642
+ data={sections}
643
+ keyExtractor={(item) => item.key}
644
+ renderItem={renderSection}
645
+ extraData={`${escrowExpanded}-${onlinePlayers.length}`}
646
+ showsVerticalScrollIndicator={false}
647
+ style={{ backgroundColor: theme.colors.surface.base }}
648
+ contentContainerStyle={{ paddingBottom: Platform.OS === 'web' ? 300 : 40 }}
649
+ keyboardShouldPersistTaps="handled"
650
+ />
651
+ )}
585
652
 
586
653
  {/* Auction starting countdown overlay */}
587
654
  <AuctionCountdownOverlay
@@ -594,6 +661,15 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
594
661
  refresh();
595
662
  }}
596
663
  />
664
+
665
+ {/* Walkthrough */}
666
+ {competition && (
667
+ <CalcuttaWalkthrough
668
+ visible={showWalkthrough}
669
+ onClose={() => { setShowWalkthrough(false); onWalkthroughDismiss?.(); }}
670
+ auction_type={competition.auction_type as any}
671
+ />
672
+ )}
597
673
  </View>
598
674
  );
599
675
  };
@@ -617,10 +693,13 @@ const styles = StyleSheet.create({
617
693
  section: { padding: 16, borderTopWidth: 1 },
618
694
  sectionTitle: { marginBottom: 10 },
619
695
  payoutRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10 },
620
- itemChip: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 8, borderWidth: 1, marginRight: 8, minWidth: 80, alignItems: 'center' },
621
696
  participantRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10 },
622
697
  avatarCircle: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', marginRight: 10 },
623
698
  youBadge: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 8 },
624
699
  assignedItemCard: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 10, borderWidth: 1 },
625
700
  assignedItemImage: { width: 44, height: 44, borderRadius: 8 },
701
+ listTabBar: { flexDirection: 'row', borderBottomWidth: 1, marginBottom: 8 },
702
+ listTab: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 10 },
703
+ desktopPanel: { borderRadius: 12, borderWidth: 1, padding: 14 },
704
+ desktopPanelTitle: { textTransform: 'uppercase', letterSpacing: 1, fontSize: 10, lineHeight: 13, marginBottom: 8 },
626
705
  });