@bettoredge/calcutta 0.2.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.
- package/package.json +46 -0
- package/src/components/CalcuttaAuction.tsx +453 -0
- package/src/components/CalcuttaAuctionItem.tsx +292 -0
- package/src/components/CalcuttaBidInput.tsx +214 -0
- package/src/components/CalcuttaCard.tsx +131 -0
- package/src/components/CalcuttaDetail.tsx +377 -0
- package/src/components/CalcuttaEscrow.tsx +464 -0
- package/src/components/CalcuttaItemResults.tsx +207 -0
- package/src/components/CalcuttaLeaderboard.tsx +179 -0
- package/src/components/CalcuttaPayoutPreview.tsx +194 -0
- package/src/components/CalcuttaRoundResults.tsx +250 -0
- package/src/components/CalcuttaTemplateSelector.tsx +124 -0
- package/src/components/sealed/AuctionResultsModal.tsx +165 -0
- package/src/components/sealed/EscrowBottomSheet.tsx +185 -0
- package/src/components/sealed/SealedBidAuction.tsx +541 -0
- package/src/components/sealed/SealedBidHeader.tsx +116 -0
- package/src/components/sealed/SealedBidInfoTab.tsx +247 -0
- package/src/components/sealed/SealedBidItemCard.tsx +385 -0
- package/src/components/sealed/SealedBidItemsTab.tsx +235 -0
- package/src/components/sealed/SealedBidMyBidsTab.tsx +512 -0
- package/src/components/sealed/SealedBidPlayersTab.tsx +220 -0
- package/src/components/sealed/SealedBidStatusBar.tsx +415 -0
- package/src/components/sealed/SealedBidTabBar.tsx +172 -0
- package/src/helpers/formatting.ts +56 -0
- package/src/helpers/lifecycleState.ts +71 -0
- package/src/helpers/payout.ts +39 -0
- package/src/helpers/validation.ts +64 -0
- package/src/hooks/useCalcuttaAuction.ts +164 -0
- package/src/hooks/useCalcuttaBid.ts +43 -0
- package/src/hooks/useCalcuttaCompetition.ts +63 -0
- package/src/hooks/useCalcuttaEscrow.ts +52 -0
- package/src/hooks/useCalcuttaItemImages.ts +79 -0
- package/src/hooks/useCalcuttaPlayers.ts +46 -0
- package/src/hooks/useCalcuttaResults.ts +58 -0
- package/src/hooks/useCalcuttaSocket.ts +131 -0
- package/src/hooks/useCalcuttaTemplates.ts +36 -0
- package/src/index.ts +74 -0
- package/src/types.ts +31 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { StyleSheet, FlatList, Image } from 'react-native';
|
|
3
|
+
import { View, Text, useTheme } from '@bettoredge/styles';
|
|
4
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
5
|
+
import type { CalcuttaParticipantProps, PublicPlayerProps } from '@bettoredge/types';
|
|
6
|
+
import type { CalcuttaPresence } from '../../hooks/useCalcuttaSocket';
|
|
7
|
+
import type { CalcuttaLifecycleState } from '../../helpers/lifecycleState';
|
|
8
|
+
import { formatCurrency } from '../../helpers/formatting';
|
|
9
|
+
import { showResults } from '../../helpers/lifecycleState';
|
|
10
|
+
|
|
11
|
+
interface SealedBidPlayersTabProps {
|
|
12
|
+
participants: CalcuttaParticipantProps[];
|
|
13
|
+
players: Record<string, PublicPlayerProps>;
|
|
14
|
+
player_id?: string;
|
|
15
|
+
presence: CalcuttaPresence;
|
|
16
|
+
market_type: string;
|
|
17
|
+
lifecycleState: CalcuttaLifecycleState;
|
|
18
|
+
socketConnected?: boolean;
|
|
19
|
+
listHeader?: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const SealedBidPlayersTab: React.FC<SealedBidPlayersTabProps> = ({
|
|
23
|
+
participants,
|
|
24
|
+
players,
|
|
25
|
+
player_id,
|
|
26
|
+
presence,
|
|
27
|
+
market_type,
|
|
28
|
+
lifecycleState,
|
|
29
|
+
socketConnected,
|
|
30
|
+
listHeader,
|
|
31
|
+
}) => {
|
|
32
|
+
const { theme } = useTheme();
|
|
33
|
+
const isResultsPhase = showResults(lifecycleState);
|
|
34
|
+
const showOnlineStatus = socketConnected && !isResultsPhase;
|
|
35
|
+
|
|
36
|
+
const onlineIds = useMemo(
|
|
37
|
+
() => new Set(presence.players.map(p => p.player_id)),
|
|
38
|
+
[presence.players],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const onlineCount = useMemo(
|
|
42
|
+
() => participants.filter(p => onlineIds.has(p.player_id)).length,
|
|
43
|
+
[participants, onlineIds],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Sort by rank/winnings in results phase
|
|
47
|
+
const sortedParticipants = useMemo(() => {
|
|
48
|
+
if (!isResultsPhase) return participants;
|
|
49
|
+
return [...participants].sort((a, b) => {
|
|
50
|
+
if (a.place && b.place) return a.place - b.place;
|
|
51
|
+
if (a.place) return -1;
|
|
52
|
+
if (b.place) return 1;
|
|
53
|
+
return (b.total_winnings || 0) - (a.total_winnings || 0);
|
|
54
|
+
});
|
|
55
|
+
}, [participants, isResultsPhase]);
|
|
56
|
+
|
|
57
|
+
if (participants.length === 0) {
|
|
58
|
+
return (
|
|
59
|
+
<View variant="transparent" style={styles.container}>
|
|
60
|
+
<>{listHeader}</>
|
|
61
|
+
<View variant="transparent" style={styles.emptyContainer}>
|
|
62
|
+
<Ionicons name="people-outline" size={40} color={theme.colors.text.tertiary} />
|
|
63
|
+
<Text variant="body" color="tertiary" style={{ marginTop: 12 }}>No players yet</Text>
|
|
64
|
+
</View>
|
|
65
|
+
</View>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<View variant="transparent" style={styles.container}>
|
|
71
|
+
<FlatList
|
|
72
|
+
data={sortedParticipants}
|
|
73
|
+
keyExtractor={p => p.calcutta_participant_id}
|
|
74
|
+
contentContainerStyle={styles.list}
|
|
75
|
+
ListHeaderComponent={
|
|
76
|
+
<View variant="transparent">
|
|
77
|
+
<>{listHeader}</>
|
|
78
|
+
<View variant="transparent" style={styles.headerRow}>
|
|
79
|
+
<Text variant="caption" color="secondary">
|
|
80
|
+
{showOnlineStatus
|
|
81
|
+
? `${onlineCount} of ${participants.length} player${participants.length !== 1 ? 's' : ''} online`
|
|
82
|
+
: `${participants.length} player${participants.length !== 1 ? 's' : ''}`
|
|
83
|
+
}
|
|
84
|
+
</Text>
|
|
85
|
+
</View>
|
|
86
|
+
</View>
|
|
87
|
+
}
|
|
88
|
+
renderItem={({ item: participant, index }) => {
|
|
89
|
+
const player = players[participant.player_id];
|
|
90
|
+
const isOnline = onlineIds.has(participant.player_id);
|
|
91
|
+
const isMe = participant.player_id === player_id;
|
|
92
|
+
const username = player?.username || player?.show_name || `Player ${participant.player_id.slice(0, 6)}`;
|
|
93
|
+
const profilePic = player?.profile_pic;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<View variant="transparent" style={[styles.playerRow, { borderColor: theme.colors.border.subtle }]}>
|
|
97
|
+
{/* Rank in results phase */}
|
|
98
|
+
{isResultsPhase && (
|
|
99
|
+
<View variant="transparent" style={styles.rankCol}>
|
|
100
|
+
{(participant.place ?? index + 1) <= 3 ? (
|
|
101
|
+
<Ionicons
|
|
102
|
+
name="trophy"
|
|
103
|
+
size={14}
|
|
104
|
+
color={(participant.place ?? index + 1) === 1 ? '#FFD700' : (participant.place ?? index + 1) === 2 ? '#C0C0C0' : '#CD7F32'}
|
|
105
|
+
/>
|
|
106
|
+
) : (
|
|
107
|
+
<Text variant="caption" color="tertiary">{participant.place ?? index + 1}</Text>
|
|
108
|
+
)}
|
|
109
|
+
</View>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{/* Avatar */}
|
|
113
|
+
<View variant="transparent" style={styles.avatarWrap}>
|
|
114
|
+
{profilePic ? (
|
|
115
|
+
<Image source={{ uri: profilePic }} style={styles.avatar} />
|
|
116
|
+
) : (
|
|
117
|
+
<View variant="transparent" style={[styles.avatar, { backgroundColor: theme.colors.primary.subtle }]}>
|
|
118
|
+
<Text variant="caption" bold style={{ color: theme.colors.primary.default }}>
|
|
119
|
+
{username.charAt(0).toUpperCase()}
|
|
120
|
+
</Text>
|
|
121
|
+
</View>
|
|
122
|
+
)}
|
|
123
|
+
{showOnlineStatus && isOnline && (
|
|
124
|
+
<View variant="transparent" style={[styles.onlineDot, { backgroundColor: '#10B981', borderColor: theme.colors.surface.base }]} />
|
|
125
|
+
)}
|
|
126
|
+
</View>
|
|
127
|
+
|
|
128
|
+
{/* Name */}
|
|
129
|
+
<View variant="transparent" style={{ flex: 1 }}>
|
|
130
|
+
<View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
131
|
+
<Text variant="body" bold={isMe} numberOfLines={1}>{username}</Text>
|
|
132
|
+
{isMe && (
|
|
133
|
+
<View variant="transparent" style={[styles.youBadge, { backgroundColor: theme.colors.primary.subtle }]}>
|
|
134
|
+
<Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 9 }}>YOU</Text>
|
|
135
|
+
</View>
|
|
136
|
+
)}
|
|
137
|
+
</View>
|
|
138
|
+
{isResultsPhase && (
|
|
139
|
+
<Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>
|
|
140
|
+
{participant.items_owned > 0
|
|
141
|
+
? `${participant.items_owned} item${participant.items_owned !== 1 ? 's' : ''} · ${formatCurrency(participant.total_spent, market_type)} spent`
|
|
142
|
+
: 'No items'}
|
|
143
|
+
</Text>
|
|
144
|
+
)}
|
|
145
|
+
</View>
|
|
146
|
+
|
|
147
|
+
{/* Winnings in results phase */}
|
|
148
|
+
{isResultsPhase && participant.total_winnings > 0 && (
|
|
149
|
+
<View variant="transparent" style={{ alignItems: 'flex-end' }}>
|
|
150
|
+
<Text variant="caption" color="tertiary" style={{ fontSize: 10 }}>Won</Text>
|
|
151
|
+
<Text variant="body" bold style={{ color: theme.colors.status.success }}>
|
|
152
|
+
{formatCurrency(participant.total_winnings, market_type)}
|
|
153
|
+
</Text>
|
|
154
|
+
</View>
|
|
155
|
+
)}
|
|
156
|
+
</View>
|
|
157
|
+
);
|
|
158
|
+
}}
|
|
159
|
+
/>
|
|
160
|
+
</View>
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const styles = StyleSheet.create({
|
|
165
|
+
container: {
|
|
166
|
+
flex: 1,
|
|
167
|
+
},
|
|
168
|
+
emptyContainer: {
|
|
169
|
+
flex: 1,
|
|
170
|
+
alignItems: 'center',
|
|
171
|
+
justifyContent: 'center',
|
|
172
|
+
padding: 40,
|
|
173
|
+
},
|
|
174
|
+
headerRow: {
|
|
175
|
+
paddingHorizontal: 16,
|
|
176
|
+
paddingVertical: 10,
|
|
177
|
+
},
|
|
178
|
+
list: {
|
|
179
|
+
paddingBottom: 40,
|
|
180
|
+
},
|
|
181
|
+
rankCol: {
|
|
182
|
+
width: 28,
|
|
183
|
+
alignItems: 'center',
|
|
184
|
+
marginRight: 4,
|
|
185
|
+
},
|
|
186
|
+
playerRow: {
|
|
187
|
+
flexDirection: 'row',
|
|
188
|
+
alignItems: 'center',
|
|
189
|
+
paddingHorizontal: 16,
|
|
190
|
+
paddingVertical: 10,
|
|
191
|
+
borderBottomWidth: 1,
|
|
192
|
+
},
|
|
193
|
+
avatarWrap: {
|
|
194
|
+
marginRight: 12,
|
|
195
|
+
position: 'relative',
|
|
196
|
+
},
|
|
197
|
+
avatar: {
|
|
198
|
+
width: 40,
|
|
199
|
+
height: 40,
|
|
200
|
+
borderRadius: 20,
|
|
201
|
+
alignItems: 'center',
|
|
202
|
+
justifyContent: 'center',
|
|
203
|
+
overflow: 'hidden',
|
|
204
|
+
},
|
|
205
|
+
onlineDot: {
|
|
206
|
+
position: 'absolute',
|
|
207
|
+
bottom: 0,
|
|
208
|
+
right: 0,
|
|
209
|
+
width: 12,
|
|
210
|
+
height: 12,
|
|
211
|
+
borderRadius: 6,
|
|
212
|
+
borderWidth: 2,
|
|
213
|
+
},
|
|
214
|
+
youBadge: {
|
|
215
|
+
paddingHorizontal: 6,
|
|
216
|
+
paddingVertical: 2,
|
|
217
|
+
borderRadius: 6,
|
|
218
|
+
marginLeft: 6,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { StyleSheet, TouchableOpacity } from 'react-native';
|
|
3
|
+
import { View, Text, useTheme } from '@bettoredge/styles';
|
|
4
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
5
|
+
import type { CalcuttaCompetitionProps, CalcuttaEscrowProps, CalcuttaBidProps, CalcuttaAuctionItemProps } from '@bettoredge/types';
|
|
6
|
+
import type { CalcuttaLifecycleState } from '../../helpers/lifecycleState';
|
|
7
|
+
import { formatCurrency } from '../../helpers/formatting';
|
|
8
|
+
|
|
9
|
+
interface SealedBidStatusBarProps {
|
|
10
|
+
competition: CalcuttaCompetitionProps;
|
|
11
|
+
escrow?: CalcuttaEscrowProps;
|
|
12
|
+
my_bids: CalcuttaBidProps[];
|
|
13
|
+
items: CalcuttaAuctionItemProps[];
|
|
14
|
+
totalItems: number;
|
|
15
|
+
market_type: string;
|
|
16
|
+
player_id?: string;
|
|
17
|
+
lifecycleState: CalcuttaLifecycleState;
|
|
18
|
+
auctionExpired?: boolean;
|
|
19
|
+
isParticipant?: boolean;
|
|
20
|
+
onEscrowTap: () => void;
|
|
21
|
+
onLeave?: () => void;
|
|
22
|
+
leaving?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const getTimerColor = (msLeft: number) => {
|
|
26
|
+
if (msLeft > 3600000) return '#10B981';
|
|
27
|
+
if (msLeft > 600000) return '#F59E0B';
|
|
28
|
+
return '#EF4444';
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const SealedBidStatusBar: React.FC<SealedBidStatusBarProps> = ({
|
|
32
|
+
competition,
|
|
33
|
+
escrow,
|
|
34
|
+
my_bids,
|
|
35
|
+
items,
|
|
36
|
+
totalItems,
|
|
37
|
+
market_type,
|
|
38
|
+
player_id,
|
|
39
|
+
lifecycleState,
|
|
40
|
+
auctionExpired,
|
|
41
|
+
isParticipant,
|
|
42
|
+
onEscrowTap,
|
|
43
|
+
onLeave,
|
|
44
|
+
leaving,
|
|
45
|
+
}) => {
|
|
46
|
+
const { theme } = useTheme();
|
|
47
|
+
const [timeLeft, setTimeLeft] = useState('');
|
|
48
|
+
const [msLeft, setMsLeft] = useState(Infinity);
|
|
49
|
+
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
50
|
+
|
|
51
|
+
const timerTarget = lifecycleState === 'auctioning'
|
|
52
|
+
? competition.auction_end_datetime
|
|
53
|
+
: lifecycleState === 'scheduled'
|
|
54
|
+
? competition.auction_start_datetime
|
|
55
|
+
: undefined;
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!timerTarget) {
|
|
59
|
+
setTimeLeft('');
|
|
60
|
+
setMsLeft(0);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const update = () => {
|
|
65
|
+
const end = new Date(timerTarget).getTime();
|
|
66
|
+
const diff = end - Date.now();
|
|
67
|
+
setMsLeft(diff);
|
|
68
|
+
|
|
69
|
+
if (diff <= 0) {
|
|
70
|
+
setTimeLeft(lifecycleState === 'auctioning' ? 'Closed' : 'Starting...');
|
|
71
|
+
if (timerRef.current) clearInterval(timerRef.current);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const d = Math.floor(diff / 86400000);
|
|
76
|
+
const h = Math.floor((diff % 86400000) / 3600000);
|
|
77
|
+
const m = Math.floor((diff % 3600000) / 60000);
|
|
78
|
+
const s = Math.floor((diff % 60000) / 1000);
|
|
79
|
+
|
|
80
|
+
if (d > 0) setTimeLeft(`${d}d ${h}h`);
|
|
81
|
+
else if (h > 0) setTimeLeft(`${h}h ${m}m`);
|
|
82
|
+
else setTimeLeft(`${m}:${s.toString().padStart(2, '0')}`);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
update();
|
|
86
|
+
timerRef.current = setInterval(update, 1000);
|
|
87
|
+
return () => { if (timerRef.current) clearInterval(timerRef.current); };
|
|
88
|
+
}, [timerTarget, lifecycleState]);
|
|
89
|
+
|
|
90
|
+
const escrowBalance = Number(escrow?.escrow_balance ?? 0);
|
|
91
|
+
const committed = Number(escrow?.committed_balance ?? 0);
|
|
92
|
+
const total = escrowBalance + committed;
|
|
93
|
+
// All items I purchased (active + eliminated)
|
|
94
|
+
const myPurchasedItems = items.filter(i => i.winning_player_id === player_id);
|
|
95
|
+
|
|
96
|
+
// ============================================
|
|
97
|
+
// PENDING
|
|
98
|
+
// ============================================
|
|
99
|
+
if (lifecycleState === 'pending') {
|
|
100
|
+
return (
|
|
101
|
+
<View variant="transparent" style={[styles.container, { borderColor: theme.colors.border.subtle }]}>
|
|
102
|
+
|
|
103
|
+
<View variant="transparent" style={styles.cells}>
|
|
104
|
+
<View variant="transparent" style={styles.cell}>
|
|
105
|
+
<Ionicons name="lock-closed-outline" size={14} color="#F59E0B" />
|
|
106
|
+
<Text variant="caption" bold style={{ color: '#F59E0B', marginLeft: 4 }}>
|
|
107
|
+
Coming Soon
|
|
108
|
+
</Text>
|
|
109
|
+
</View>
|
|
110
|
+
<View variant="transparent" style={styles.cell}>
|
|
111
|
+
<Text variant="caption" color="secondary">Items: {totalItems}</Text>
|
|
112
|
+
</View>
|
|
113
|
+
<View variant="transparent" style={styles.cell} />
|
|
114
|
+
</View>
|
|
115
|
+
</View>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================
|
|
120
|
+
// SCHEDULED
|
|
121
|
+
// ============================================
|
|
122
|
+
if (lifecycleState === 'scheduled') {
|
|
123
|
+
return (
|
|
124
|
+
<View variant="transparent" style={[styles.container, { borderColor: theme.colors.border.subtle }]}>
|
|
125
|
+
|
|
126
|
+
<View variant="transparent" style={styles.cells}>
|
|
127
|
+
<View variant="transparent" style={styles.cell}>
|
|
128
|
+
<Ionicons name="calendar-outline" size={14} color="#3B82F6" />
|
|
129
|
+
<Text variant="caption" bold style={{ color: '#3B82F6', marginLeft: 4 }}>
|
|
130
|
+
{timeLeft ? `Starts ${timeLeft}` : 'Scheduled'}
|
|
131
|
+
</Text>
|
|
132
|
+
</View>
|
|
133
|
+
<TouchableOpacity style={styles.cell} onPress={onEscrowTap} activeOpacity={0.7}>
|
|
134
|
+
<Ionicons name="wallet-outline" size={14} color={theme.colors.text.secondary} />
|
|
135
|
+
<Text variant="caption" bold style={{ marginLeft: 4 }}>
|
|
136
|
+
{formatCurrency(escrowBalance, market_type)}
|
|
137
|
+
</Text>
|
|
138
|
+
</TouchableOpacity>
|
|
139
|
+
<View variant="transparent" style={styles.cell}>
|
|
140
|
+
<Ionicons name="people-outline" size={14} color={theme.colors.text.secondary} />
|
|
141
|
+
<Text variant="caption" bold style={{ marginLeft: 4 }}>
|
|
142
|
+
{items.length > 0 ? `${totalItems} items` : '0'}
|
|
143
|
+
</Text>
|
|
144
|
+
</View>
|
|
145
|
+
</View>
|
|
146
|
+
</View>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ============================================
|
|
151
|
+
// AUCTIONING — big KPI-style bar
|
|
152
|
+
// ============================================
|
|
153
|
+
if (lifecycleState === 'auctioning') {
|
|
154
|
+
const activeBids = my_bids.filter(b => b.bid_status === 'active').length;
|
|
155
|
+
const timerColor = auctionExpired ? '#F59E0B' : getTimerColor(msLeft);
|
|
156
|
+
|
|
157
|
+
// Safe auction metrics (no bid amount leaks)
|
|
158
|
+
const totalBidsPlaced = items.reduce((s, i) => s + (i.bid_count ?? 0), 0);
|
|
159
|
+
const itemsWithBids = items.filter(i => (i.bid_count ?? 0) > 0).length;
|
|
160
|
+
const minPot = competition.min_bid * totalItems;
|
|
161
|
+
const myTotalBid = my_bids
|
|
162
|
+
.filter(b => b.bid_status === 'active')
|
|
163
|
+
.reduce((s, b) => s + Number(b.bid_amount), 0);
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<View variant="transparent" style={[styles.auctionContainer, { borderColor: theme.colors.border.subtle }]}>
|
|
167
|
+
|
|
168
|
+
{/* Timer row */}
|
|
169
|
+
<View variant="transparent" style={styles.timerRow}>
|
|
170
|
+
<Ionicons name={auctionExpired ? 'hourglass-outline' : 'timer-outline'} size={18} color={timerColor} />
|
|
171
|
+
<Text variant="h3" bold style={{ color: timerColor, marginLeft: 6 }}>
|
|
172
|
+
{auctionExpired ? 'Closing...' : timeLeft}
|
|
173
|
+
</Text>
|
|
174
|
+
</View>
|
|
175
|
+
|
|
176
|
+
{/* Auction Activity (safe metrics only) */}
|
|
177
|
+
<View variant="transparent" style={[styles.potTracker, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
|
|
178
|
+
<View variant="transparent" style={styles.potHeader}>
|
|
179
|
+
<Ionicons name="pulse-outline" size={16} color={theme.colors.primary.default} />
|
|
180
|
+
<Text variant="caption" bold style={{ color: theme.colors.primary.default, marginLeft: 6 }}>Auction Activity</Text>
|
|
181
|
+
</View>
|
|
182
|
+
<View variant="transparent" style={styles.potRow}>
|
|
183
|
+
<View variant="transparent" style={styles.potItem}>
|
|
184
|
+
<Text variant="h3" bold>{itemsWithBids}<Text variant="caption" color="tertiary">/{totalItems}</Text></Text>
|
|
185
|
+
<Text variant="caption" color="tertiary">Items Bid</Text>
|
|
186
|
+
</View>
|
|
187
|
+
<View variant="transparent" style={[styles.potDivider, { backgroundColor: theme.colors.border.subtle }]} />
|
|
188
|
+
<View variant="transparent" style={styles.potItem}>
|
|
189
|
+
<Text variant="h3" bold>{totalBidsPlaced}</Text>
|
|
190
|
+
<Text variant="caption" color="tertiary">Total Bids</Text>
|
|
191
|
+
</View>
|
|
192
|
+
<View variant="transparent" style={[styles.potDivider, { backgroundColor: theme.colors.border.subtle }]} />
|
|
193
|
+
<View variant="transparent" style={styles.potItem}>
|
|
194
|
+
<Text variant="h3" bold>{formatCurrency(minPot, market_type)}</Text>
|
|
195
|
+
<Text variant="caption" color="tertiary">Min Pot</Text>
|
|
196
|
+
</View>
|
|
197
|
+
</View>
|
|
198
|
+
</View>
|
|
199
|
+
|
|
200
|
+
{/* My Escrow KPIs — only for participants */}
|
|
201
|
+
{isParticipant && (
|
|
202
|
+
<View variant="transparent" style={styles.kpiRow}>
|
|
203
|
+
<TouchableOpacity
|
|
204
|
+
style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}
|
|
205
|
+
onPress={onEscrowTap}
|
|
206
|
+
activeOpacity={0.7}
|
|
207
|
+
>
|
|
208
|
+
<Ionicons name="wallet-outline" size={16} color={theme.colors.primary.default} />
|
|
209
|
+
<Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>Available</Text>
|
|
210
|
+
<Text variant="body" bold>{formatCurrency(escrowBalance, market_type)}</Text>
|
|
211
|
+
</TouchableOpacity>
|
|
212
|
+
|
|
213
|
+
<View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
|
|
214
|
+
<Ionicons name="pricetag-outline" size={16} color={theme.colors.text.secondary} />
|
|
215
|
+
<Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>My Bids</Text>
|
|
216
|
+
<Text variant="body" bold>{formatCurrency(myTotalBid, market_type)}</Text>
|
|
217
|
+
</View>
|
|
218
|
+
|
|
219
|
+
<View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
|
|
220
|
+
<Ionicons name="pricetag-outline" size={16} color={theme.colors.text.secondary} />
|
|
221
|
+
<Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>My Items</Text>
|
|
222
|
+
<Text variant="body" bold>{activeBids}<Text variant="caption" color="tertiary">/{totalItems}</Text></Text>
|
|
223
|
+
</View>
|
|
224
|
+
</View>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
{/* Transfer actions — only for participants */}
|
|
228
|
+
{isParticipant && <View variant="transparent" style={styles.transferRow}>
|
|
229
|
+
<TouchableOpacity
|
|
230
|
+
style={[styles.transferBtn, { backgroundColor: theme.colors.primary.default }]}
|
|
231
|
+
onPress={onEscrowTap}
|
|
232
|
+
activeOpacity={0.7}
|
|
233
|
+
>
|
|
234
|
+
<Ionicons name="arrow-down-outline" size={16} color="#FFFFFF" />
|
|
235
|
+
<Text variant="caption" bold style={{ color: '#FFFFFF', marginLeft: 4 }}>
|
|
236
|
+
Transfer In
|
|
237
|
+
</Text>
|
|
238
|
+
</TouchableOpacity>
|
|
239
|
+
<TouchableOpacity
|
|
240
|
+
style={[styles.transferBtn, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle, borderWidth: 1 }]}
|
|
241
|
+
onPress={onEscrowTap}
|
|
242
|
+
activeOpacity={0.7}
|
|
243
|
+
>
|
|
244
|
+
<Ionicons name="arrow-up-outline" size={16} color={theme.colors.text.secondary} />
|
|
245
|
+
<Text variant="caption" bold style={{ color: theme.colors.text.secondary, marginLeft: 4 }}>
|
|
246
|
+
Transfer Out
|
|
247
|
+
</Text>
|
|
248
|
+
</TouchableOpacity>
|
|
249
|
+
</View>}
|
|
250
|
+
|
|
251
|
+
{/* Leave button — only when: not expired, no active bids, no escrow balance */}
|
|
252
|
+
{onLeave && !auctionExpired && activeBids === 0 && escrowBalance === 0 && committed === 0 && (
|
|
253
|
+
<TouchableOpacity
|
|
254
|
+
style={styles.leaveBtn}
|
|
255
|
+
onPress={onLeave}
|
|
256
|
+
disabled={leaving}
|
|
257
|
+
activeOpacity={0.7}
|
|
258
|
+
>
|
|
259
|
+
<Ionicons name="exit-outline" size={16} color="#EF4444" />
|
|
260
|
+
<Text variant="caption" bold style={{ color: '#EF4444', marginLeft: 6 }}>
|
|
261
|
+
{leaving ? 'Leaving...' : 'Leave Auction'}
|
|
262
|
+
</Text>
|
|
263
|
+
</TouchableOpacity>
|
|
264
|
+
)}
|
|
265
|
+
</View>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ============================================
|
|
270
|
+
// TOURNAMENT
|
|
271
|
+
// ============================================
|
|
272
|
+
if (lifecycleState === 'tournament') {
|
|
273
|
+
const totalPurchased = myPurchasedItems.reduce((s, i) => s + Number(i.winning_bid), 0);
|
|
274
|
+
const stillAlive = myPurchasedItems.filter(i => i.status !== 'eliminated').length;
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<View variant="transparent" style={[styles.auctionContainer, { borderColor: theme.colors.border.subtle }]}>
|
|
278
|
+
|
|
279
|
+
<View variant="transparent" style={styles.kpiRow}>
|
|
280
|
+
<View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
|
|
281
|
+
<Ionicons name="ellipse" size={8} color="#10B981" />
|
|
282
|
+
<Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>Status</Text>
|
|
283
|
+
<Text variant="body" bold style={{ color: '#8B5CF6' }}>In Progress</Text>
|
|
284
|
+
</View>
|
|
285
|
+
|
|
286
|
+
<View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
|
|
287
|
+
<Ionicons name="cart-outline" size={16} color={theme.colors.primary.default} />
|
|
288
|
+
<Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>Purchased</Text>
|
|
289
|
+
<Text variant="body" bold>{myPurchasedItems.length} <Text variant="caption" color="tertiary">({stillAlive} alive)</Text></Text>
|
|
290
|
+
</View>
|
|
291
|
+
|
|
292
|
+
<View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
|
|
293
|
+
<Ionicons name="cash-outline" size={16} color={theme.colors.text.secondary} />
|
|
294
|
+
<Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>Spent</Text>
|
|
295
|
+
<Text variant="body" bold>{formatCurrency(totalPurchased, market_type)}</Text>
|
|
296
|
+
</View>
|
|
297
|
+
</View>
|
|
298
|
+
</View>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ============================================
|
|
303
|
+
// COMPLETED
|
|
304
|
+
// ============================================
|
|
305
|
+
const totalPurchased = myPurchasedItems.reduce((s, i) => s + Number(i.winning_bid), 0);
|
|
306
|
+
return (
|
|
307
|
+
<View variant="transparent" style={[styles.auctionContainer, { borderColor: theme.colors.border.subtle }]}>
|
|
308
|
+
|
|
309
|
+
<View variant="transparent" style={styles.kpiRow}>
|
|
310
|
+
<View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
|
|
311
|
+
<Ionicons name="checkmark-circle" size={16} color={theme.colors.status.success} />
|
|
312
|
+
<Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>Status</Text>
|
|
313
|
+
<Text variant="body" bold style={{ color: theme.colors.status.success }}>Completed</Text>
|
|
314
|
+
</View>
|
|
315
|
+
|
|
316
|
+
<View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
|
|
317
|
+
<Ionicons name="cart-outline" size={16} color={theme.colors.primary.default} />
|
|
318
|
+
<Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>Purchased</Text>
|
|
319
|
+
<Text variant="body" bold>{myPurchasedItems.length}</Text>
|
|
320
|
+
</View>
|
|
321
|
+
|
|
322
|
+
<View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
|
|
323
|
+
<Ionicons name="cash-outline" size={16} color={theme.colors.text.secondary} />
|
|
324
|
+
<Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>Spent</Text>
|
|
325
|
+
<Text variant="body" bold>{formatCurrency(totalPurchased, market_type)}</Text>
|
|
326
|
+
</View>
|
|
327
|
+
</View>
|
|
328
|
+
</View>
|
|
329
|
+
);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const styles = StyleSheet.create({
|
|
333
|
+
// Compact container for pending/scheduled
|
|
334
|
+
container: {
|
|
335
|
+
borderBottomWidth: 1,
|
|
336
|
+
paddingTop: 8,
|
|
337
|
+
paddingHorizontal: 12,
|
|
338
|
+
paddingBottom: 6,
|
|
339
|
+
},
|
|
340
|
+
cells: {
|
|
341
|
+
flexDirection: 'row',
|
|
342
|
+
justifyContent: 'space-between',
|
|
343
|
+
},
|
|
344
|
+
cell: {
|
|
345
|
+
flexDirection: 'row',
|
|
346
|
+
alignItems: 'center',
|
|
347
|
+
},
|
|
348
|
+
// Full KPI container for auctioning/tournament/completed
|
|
349
|
+
auctionContainer: {
|
|
350
|
+
borderBottomWidth: 1,
|
|
351
|
+
paddingHorizontal: 12,
|
|
352
|
+
paddingVertical: 10,
|
|
353
|
+
},
|
|
354
|
+
timerRow: {
|
|
355
|
+
flexDirection: 'row',
|
|
356
|
+
alignItems: 'center',
|
|
357
|
+
justifyContent: 'center',
|
|
358
|
+
marginBottom: 10,
|
|
359
|
+
},
|
|
360
|
+
potTracker: {
|
|
361
|
+
borderRadius: 12,
|
|
362
|
+
borderWidth: 1,
|
|
363
|
+
padding: 12,
|
|
364
|
+
marginBottom: 10,
|
|
365
|
+
},
|
|
366
|
+
potHeader: {
|
|
367
|
+
flexDirection: 'row',
|
|
368
|
+
alignItems: 'center',
|
|
369
|
+
marginBottom: 10,
|
|
370
|
+
},
|
|
371
|
+
potRow: {
|
|
372
|
+
flexDirection: 'row',
|
|
373
|
+
alignItems: 'center',
|
|
374
|
+
},
|
|
375
|
+
potItem: {
|
|
376
|
+
flex: 1,
|
|
377
|
+
alignItems: 'center',
|
|
378
|
+
},
|
|
379
|
+
potDivider: {
|
|
380
|
+
width: 1,
|
|
381
|
+
height: 32,
|
|
382
|
+
},
|
|
383
|
+
kpiRow: {
|
|
384
|
+
flexDirection: 'row',
|
|
385
|
+
gap: 8,
|
|
386
|
+
},
|
|
387
|
+
kpiCard: {
|
|
388
|
+
flex: 1,
|
|
389
|
+
borderRadius: 10,
|
|
390
|
+
borderWidth: 1,
|
|
391
|
+
paddingVertical: 10,
|
|
392
|
+
paddingHorizontal: 8,
|
|
393
|
+
alignItems: 'center',
|
|
394
|
+
},
|
|
395
|
+
transferRow: {
|
|
396
|
+
flexDirection: 'row',
|
|
397
|
+
gap: 8,
|
|
398
|
+
marginTop: 10,
|
|
399
|
+
},
|
|
400
|
+
transferBtn: {
|
|
401
|
+
flex: 1,
|
|
402
|
+
flexDirection: 'row',
|
|
403
|
+
alignItems: 'center',
|
|
404
|
+
justifyContent: 'center',
|
|
405
|
+
height: 36,
|
|
406
|
+
borderRadius: 8,
|
|
407
|
+
},
|
|
408
|
+
leaveBtn: {
|
|
409
|
+
flexDirection: 'row',
|
|
410
|
+
alignItems: 'center',
|
|
411
|
+
justifyContent: 'center',
|
|
412
|
+
marginTop: 10,
|
|
413
|
+
paddingVertical: 8,
|
|
414
|
+
},
|
|
415
|
+
});
|