import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { StyleSheet, TouchableOpacity, ActivityIndicator, ScrollView, Image, FlatList, TextInput, Platform, useWindowDimensions, RefreshControl } from 'react-native'; import { View, Text, useTheme } from '@bettoredge/styles'; import { Ionicons } from '@expo/vector-icons'; import type { CalcuttaParticipantProps } from '@bettoredge/types'; import { useCalcuttaCompetition } from '../hooks/useCalcuttaCompetition'; import { useCalcuttaPlayers } from '../hooks/useCalcuttaPlayers'; import { useCalcuttaItemImages } from '../hooks/useCalcuttaItemImages'; import { formatCurrency, getStatusLabel, resolveItemImageUrl } from '../helpers/formatting'; import { deriveLifecycleState, canManageEscrow } from '../helpers/lifecycleState'; import { useCalcuttaEscrow } from '../hooks/useCalcuttaEscrow'; import { startSweepstakesCompetition, startCalcuttaAuction } from '@bettoredge/api'; import { CalcuttaActionCard } from './CalcuttaActionCard'; import { CalcuttaCountdown } from './CalcuttaCountdown'; import { AuctionInfoChips } from './AuctionInfoChips'; import { EscrowWidget } from './EscrowWidget'; import { AuctionCountdownOverlay } from './AuctionCountdownOverlay'; import { CalcuttaWalkthrough } from './CalcuttaWalkthrough'; import { useCalcuttaSocket } from '../hooks/useCalcuttaSocket'; export interface CalcuttaDetailProps { calcutta_competition_id: string; player_id?: string; access_token?: string; device_id?: string; player_username?: string; player_profile_pic?: string; onClose?: () => void; onJoin?: () => Promise; onLeave?: () => Promise; onShare?: () => void; onManage?: () => void; player_balance?: number; onDepositFunds?: (amount: number) => void; initialShowWalkthrough?: boolean; onWalkthroughDismiss?: () => void; onRefresh?: () => void | Promise; onAuctionStarted?: () => void; } const getStatusConfig = (status: string) => { switch (status) { case 'pending': return { label: 'Pending', color: '#F59E0B', bg: '#F59E0B15' }; case 'scheduled': return { label: 'Open to Join', color: '#3B82F6', bg: '#3B82F615' }; case 'auction_open': return { label: 'Auction Live', color: '#10B981', bg: '#10B98115' }; case 'inprogress': return { label: 'In Progress', color: '#8B5CF6', bg: '#8B5CF615' }; case 'closed': return { label: 'Completed', color: '#6B7280', bg: '#6B728015' }; default: return { label: status, color: '#6B7280', bg: '#6B728015' }; } }; export const CalcuttaDetail: React.FC = ({ calcutta_competition_id, player_id, onClose, onJoin, onLeave, onShare, onManage, player_balance, onDepositFunds, initialShowWalkthrough, onWalkthroughDismiss, access_token, device_id, player_username, player_profile_pic, onRefresh, onAuctionStarted, }) => { const { theme } = useTheme(); const { width: windowWidth } = useWindowDimensions(); const [containerWidth, setContainerWidth] = useState(0); const measuredWidth = containerWidth || windowWidth; const isDesktop = Platform.OS === 'web' && measuredWidth >= 700; const [refreshing, setRefreshing] = useState(false); // Walkthrough state const [showWalkthrough, setShowWalkthrough] = useState(false); const walkthroughTriggered = useRef(false); useEffect(() => { if (initialShowWalkthrough && !walkthroughTriggered.current) { walkthroughTriggered.current = true; setShowWalkthrough(true); } }, [initialShowWalkthrough]); // Countdown overlay state const [showCountdown, setShowCountdown] = useState(false); const [countdownItem, setCountdownItem] = useState(undefined); // Socket for presence + auction start detection const handleItemActive = useCallback((data: any) => { // Auction just started — show countdown for ALL connected clients setCountdownItem(data.item); setShowCountdown(true); }, []); const { connected: socketConnected, presence, socketState } = useCalcuttaSocket( calcutta_competition_id, access_token, device_id, { username: player_username, profile_pic: player_profile_pic }, { onItemActive: handleItemActive }, ); const onlinePlayers = presence.players; const onlinePlayerIds = useMemo(() => new Set(onlinePlayers.map(p => p.player_id)), [onlinePlayers]); const { loading, competition, rounds, items, participants, payout_rules, refresh, } = useCalcuttaCompetition(calcutta_competition_id); const participantIds = useMemo(() => participants.map(p => p.player_id), [participants]); const { players: enrichedPlayers } = useCalcuttaPlayers(participantIds); const { escrow, fetchEscrow } = useCalcuttaEscrow(calcutta_competition_id); const [escrowExpanded, setEscrowExpanded] = useState(false); const { images: itemImages } = useCalcuttaItemImages(items); const [joining, setJoining] = useState(false); const [leaving, setLeaving] = useState(false); const [startingComp, setStartingComp] = useState(false); const [itemSearch, setItemSearch] = useState(''); const [itemsPage, setItemsPage] = useState(1); const ITEMS_PER_PAGE = 10; const [mobileListTab, setMobileListTab] = useState<'items' | 'players'>('items'); const handleRefresh = async () => { setRefreshing(true); try { await Promise.all([refresh(), fetchEscrow(), onRefresh?.()]); } catch {} finally { setRefreshing(false); } }; const filteredItems = useMemo(() => { if (!itemSearch.trim()) return items; const q = itemSearch.toLowerCase().trim(); return items.filter(item => { if (item.item_name.toLowerCase().includes(q)) return true; if (item.seed != null && `#${item.seed}`.includes(q)) return true; if (item.winning_player_id) { const owner = enrichedPlayers[item.winning_player_id]; const ownerName = (owner?.username || owner?.show_name || '').toLowerCase(); if (ownerName.includes(q)) return true; } return false; }); }, [items, itemSearch, enrichedPlayers]); // Reset page when search changes useEffect(() => { setItemsPage(1); }, [itemSearch]); const totalItemPages = Math.max(1, Math.ceil(filteredItems.length / ITEMS_PER_PAGE)); const paginatedItems = useMemo(() => { const start = (itemsPage - 1) * ITEMS_PER_PAGE; return filteredItems.slice(start, start + ITEMS_PER_PAGE); }, [filteredItems, itemsPage]); if (loading && !competition) { return ( ); } if (!competition) { return ( Competition not found ); } const statusConfig = getStatusConfig(competition.status); const isAdmin = player_id != null && player_id == competition.admin_id; const hasJoined = participants.some(p => p.player_id == player_id); const entryFee = Number(competition.entry_fee) || 0; const isFree = entryFee === 0; const isSweepstakes = competition.auction_type === 'sweepstakes'; const isJoinable = isSweepstakes ? competition.status === 'scheduled' : ['scheduled', 'auction_open'].includes(competition.status); const totalPot = Number(competition.total_pot) || 0; const canLeave = hasJoined && !isSweepstakes && ['pending', 'scheduled'].includes(competition.status); const heroImage = competition.image?.url; const lifecycleState = deriveLifecycleState(competition.status, competition.auction_status, competition.auction_type); const showEscrow = canManageEscrow(lifecycleState, competition.auction_type) && hasJoined; // Sweepstakes: find the player's assigned item const myParticipant = participants.find(p => p.player_id == player_id); const myAssignedItem = isSweepstakes && hasJoined ? items.find(i => i.winning_player_id == player_id) : undefined; const unassignedItemCount = isSweepstakes ? items.filter(i => !i.winning_player_id && i.status === 'pending').length : 0; const sections = [ { key: 'hero' }, { key: 'info' }, { key: 'action-card' }, ...(showEscrow ? [{ key: 'escrow' }] : []), { key: 'stats' }, { key: 'payouts' }, { key: 'items-players-tabbed' }, ...(canLeave ? [{ key: 'leave' }] : []), ]; const renderItemsList = () => { if (items.length === 0) { return ( No items added yet. ); } return ( { if (Platform.OS === 'web' && e?.target?.scrollIntoView) { setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300); } }} /> {itemSearch.trim() !== '' && ( {filteredItems.length} of {items.length} items )} {paginatedItems.map(item => { const imgUrl = resolveItemImageUrl(item.item_image) || itemImages[item.item_id]?.url; const owner = item.winning_player_id ? enrichedPlayers[item.winning_player_id] : undefined; const ownerName = owner?.username || owner?.show_name; const isMe = item.winning_player_id == player_id; return ( {imgUrl ? ( ) : ( )} {item.item_name} {isSweepstakes && ownerName ? ( {isMe ? 'You' : ownerName} ) : isSweepstakes ? ( Available ) : null} {item.seed != null && #{item.seed}} {isMe && YOU} ); })} {totalItemPages > 1 && ( setItemsPage(p => p - 1)} disabled={itemsPage <= 1} style={{ opacity: itemsPage <= 1 ? 0.3 : 1, padding: 4 }} > {itemsPage} of {totalItemPages} setItemsPage(p => p + 1)} disabled={itemsPage >= totalItemPages} style={{ opacity: itemsPage >= totalItemPages ? 0.3 : 1, padding: 4 }} > )} ); }; const renderParticipantsList = () => { if (participants.length === 0) { return ( No players yet. Share the code to invite friends! ); } return ( {participants.map((p, i) => { const enriched = enrichedPlayers[p.player_id]; const username = enriched?.username || enriched?.show_name || `Player ${p.player_id.slice(0, 6)}`; const profilePic = enriched?.profile_pic; const isOnline = onlinePlayerIds.has(p.player_id); return ( {profilePic ? ( ) : ( {username.charAt(0).toUpperCase()} )} {isOnline && ( )} {username} {p.items_owned > 0 ? `${p.items_owned} items \u00B7 ${formatCurrency(p.total_spent, competition.market_type)} spent` : 'Joined'} {p.player_id == player_id && ( YOU )} ); })} ); }; const renderSection = ({ item }: { item: { key: string } }) => { switch (item.key) { case 'hero': return heroImage ? ( ) : ( ); case 'info': return ( {statusConfig.label} {isSweepstakes ? 'Sweepstakes' : competition.auction_type === 'live' ? 'Live Auction' : 'Sealed Bid'} setShowWalkthrough(true)} style={{ marginRight: 8, padding: 4 }} activeOpacity={0.7} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} > {isAdmin && onManage && ( Manage )} {competition.competition_name} {competition.competition_description ? ( {competition.competition_description} ) : null} {competition.competition_code ? ( {competition.competition_code} ) : null} {competition.scheduled_datetime && ( )} {competition.auction_type === 'sealed_bid' && competition.auction_end_datetime && ( )} {onShare && ( Share )} ); case 'stats': return ( Entry Fee {isFree ? 'FREE' : formatCurrency(entryFee, competition.market_type)} {isSweepstakes ? ( Available {unassignedItemCount}/{items.length} ) : ( Pot {formatCurrency(totalPot, competition.market_type)} )} Players {participants.length}{competition.max_participants > 0 ? `/${competition.max_participants}` : ''} {isSweepstakes ? 'Prizes' : 'Items'} {isSweepstakes ? rounds.filter(r => r.prize_description).length : items.length} ); case 'action-card': return ( { setJoining(true); try { await onJoin(); await refresh(); fetchEscrow(); } catch {} setJoining(false); } : undefined} onDepositEscrow={undefined} onManage={onManage} onStartAuction={isAdmin && competition.status === 'scheduled' ? async () => { setStartingComp(true); try { if (isSweepstakes) { await startSweepstakesCompetition(calcutta_competition_id); await refresh(); } else { await startCalcuttaAuction(calcutta_competition_id); // Socket will broadcast CALCUTTA_ITEM_ACTIVE which triggers countdown // refresh happens after countdown completes } } catch {} setStartingComp(false); } : undefined} joining={joining} /> {/* Sweepstakes: show assigned item below action card */} {isSweepstakes && myAssignedItem && ( {resolveItemImageUrl(myAssignedItem.item_image) ? ( ) : ( )} Your Item {myAssignedItem.item_name} {myAssignedItem.seed != null && Seed #{myAssignedItem.seed}} )} ); case 'escrow': return ( { refresh(); fetchEscrow(); }} initialExpanded={Number(escrow?.escrow_balance ?? 0) <= 0} /> ); case 'leave': return ( { if (!onLeave) return; setLeaving(true); try { await onLeave(); await refresh(); } catch {} setLeaving(false); }} disabled={leaving} style={{ opacity: leaving ? 0.5 : 1 }} > {leaving ? ( ) : ( Leave Competition )} ); case 'payouts': // Sweepstakes: show prize list per round instead of payout percentages if (isSweepstakes) { const prizeRounds = rounds.filter(r => r.prize_description); if (prizeRounds.length === 0) return null; return ( Prizes {prizeRounds.map((round, i) => ( {round.prize_image?.url ? ( ) : null} {round.round_name} {round.prize_description} ))} ); } if (payout_rules.length === 0) return null; return ( Payout Structure {payout_rules.map((rule, i) => { const round = rounds.find(r => r.round_number === rule.round_number); const pct = Number(rule.payout_pct); const amount = totalPot > 0 ? (pct / 100) * totalPot : 0; return ( {rule.description || round?.round_name || `Round ${rule.round_number}`} {pct}% {totalPot > 0 && ( {formatCurrency(amount, competition.market_type)} )} ); })} ); case 'items': return renderItemsList(); case 'participants': return renderParticipantsList(); case 'items-players-tabbed': return ( {/* Tab toggle */} setMobileListTab('items')} > {isSweepstakes ? 'Teams' : 'Items'} ({items.length}) setMobileListTab('players')} > Players ({participants.length}) {onlinePlayers.length > 0 && ( {onlinePlayers.length} )} {mobileListTab === 'items' ? renderItemsList() : renderParticipantsList()} ); default: return null; } }; return ( setContainerWidth(e.nativeEvent.layout.width)}> {isDesktop ? ( }> {/* Row 1: Hero + Info/Action left, Items right */} {renderSection({ item: { key: 'hero' } })} {renderSection({ item: { key: 'info' } })} {renderSection({ item: { key: 'action-card' } })} {showEscrow && renderSection({ item: { key: 'escrow' } })} {isSweepstakes ? 'Teams' : 'Auction Items'} ({items.length}) {renderSection({ item: { key: 'items' } })} {/* Row 2: Stats + Payouts + Participants */} {renderSection({ item: { key: 'stats' } })} Payouts {renderSection({ item: { key: 'payouts' } })} Players ({participants.length}) {renderSection({ item: { key: 'participants' } })} {canLeave && renderSection({ item: { key: 'leave' } })} ) : ( item.key} renderItem={renderSection} extraData={`${escrowExpanded}-${onlinePlayers.length}`} showsVerticalScrollIndicator={false} style={{ backgroundColor: theme.colors.surface.base }} contentContainerStyle={{ paddingBottom: Platform.OS === 'web' ? 300 : 40 }} keyboardShouldPersistTaps="handled" refreshControl={} /> )} {/* Auction starting countdown overlay */} { setShowCountdown(false); refresh(); onAuctionStarted?.(); }} /> {/* Walkthrough */} {competition && ( { setShowWalkthrough(false); onWalkthroughDismiss?.(); }} auction_type={competition.auction_type as any} /> )} ); }; const styles = StyleSheet.create({ container: { flex: 1 }, loadingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 40 }, headerBar: { flexDirection: 'row', alignItems: 'center', padding: 12, borderBottomWidth: 1 }, heroImage: { width: '100%', height: 180 }, heroPlaceholder: { width: '100%', height: 140, alignItems: 'center', justifyContent: 'center' }, infoSection: { padding: 16 }, statusBadge: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 12 }, statusDot: { width: 6, height: 6, borderRadius: 3, marginRight: 6 }, codeChip: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', paddingHorizontal: 10, paddingVertical: 5, borderRadius: 12, marginTop: 10 }, statsGrid: { flexDirection: 'row', borderTopWidth: 1, borderBottomWidth: 1 }, statBox: { flex: 1, alignItems: 'center', paddingVertical: 12 }, ctaSection: { padding: 16, gap: 10 }, ctaButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 14, borderRadius: 10 }, ctaButtonOutline: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 10, borderRadius: 10, borderWidth: 1 }, joinedBadge: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 10, borderRadius: 10 }, section: { padding: 16, borderTopWidth: 1 }, sectionTitle: { marginBottom: 10 }, payoutRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10 }, participantRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10 }, avatarCircle: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', marginRight: 10 }, youBadge: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 8 }, assignedItemCard: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 10, borderWidth: 1 }, assignedItemImage: { width: 44, height: 44, borderRadius: 8 }, listTabBar: { flexDirection: 'row', borderBottomWidth: 1, marginBottom: 8 }, listTab: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 10 }, desktopPanel: { borderRadius: 12, borderWidth: 1, padding: 14 }, desktopPanelTitle: { textTransform: 'uppercase', letterSpacing: 1, fontSize: 10, lineHeight: 13, marginBottom: 8 }, });