@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,377 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import { StyleSheet, TouchableOpacity, ActivityIndicator, ScrollView, Image, FlatList } from 'react-native';
|
|
3
|
+
import { View, Text, useTheme } from '@bettoredge/styles';
|
|
4
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
5
|
+
import type { CalcuttaParticipantProps } from '@bettoredge/types';
|
|
6
|
+
import { useCalcuttaCompetition } from '../hooks/useCalcuttaCompetition';
|
|
7
|
+
import { useCalcuttaPlayers } from '../hooks/useCalcuttaPlayers';
|
|
8
|
+
import { formatCurrency, getStatusLabel } from '../helpers/formatting';
|
|
9
|
+
|
|
10
|
+
export interface CalcuttaDetailProps {
|
|
11
|
+
calcutta_competition_id: string;
|
|
12
|
+
player_id?: string;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
onJoin?: () => Promise<void>;
|
|
15
|
+
onLeave?: () => Promise<void>;
|
|
16
|
+
onShare?: () => void;
|
|
17
|
+
onManage?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const getStatusConfig = (status: string) => {
|
|
21
|
+
switch (status) {
|
|
22
|
+
case 'pending': return { label: 'Pending', color: '#F59E0B', bg: '#F59E0B15' };
|
|
23
|
+
case 'scheduled': return { label: 'Open to Join', color: '#3B82F6', bg: '#3B82F615' };
|
|
24
|
+
case 'auction_open': return { label: 'Auction Live', color: '#10B981', bg: '#10B98115' };
|
|
25
|
+
case 'inprogress': return { label: 'In Progress', color: '#8B5CF6', bg: '#8B5CF615' };
|
|
26
|
+
case 'closed': return { label: 'Completed', color: '#6B7280', bg: '#6B728015' };
|
|
27
|
+
default: return { label: status, color: '#6B7280', bg: '#6B728015' };
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
32
|
+
calcutta_competition_id,
|
|
33
|
+
player_id,
|
|
34
|
+
onClose,
|
|
35
|
+
onJoin,
|
|
36
|
+
onLeave,
|
|
37
|
+
onShare,
|
|
38
|
+
onManage,
|
|
39
|
+
}) => {
|
|
40
|
+
const { theme } = useTheme();
|
|
41
|
+
const {
|
|
42
|
+
loading,
|
|
43
|
+
competition,
|
|
44
|
+
rounds,
|
|
45
|
+
items,
|
|
46
|
+
participants,
|
|
47
|
+
payout_rules,
|
|
48
|
+
refresh,
|
|
49
|
+
} = useCalcuttaCompetition(calcutta_competition_id);
|
|
50
|
+
|
|
51
|
+
const participantIds = useMemo(() => participants.map(p => p.player_id), [participants]);
|
|
52
|
+
const { players: enrichedPlayers } = useCalcuttaPlayers(participantIds);
|
|
53
|
+
|
|
54
|
+
const [joining, setJoining] = useState(false);
|
|
55
|
+
const [leaving, setLeaving] = useState(false);
|
|
56
|
+
|
|
57
|
+
if (loading && !competition) {
|
|
58
|
+
return (
|
|
59
|
+
<View variant="transparent" style={styles.loadingContainer}>
|
|
60
|
+
<ActivityIndicator size="large" color={theme.colors.primary.default} />
|
|
61
|
+
</View>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!competition) {
|
|
66
|
+
return (
|
|
67
|
+
<View variant="transparent" style={styles.loadingContainer}>
|
|
68
|
+
<Text variant="body" color="secondary">Competition not found</Text>
|
|
69
|
+
</View>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const statusConfig = getStatusConfig(competition.status);
|
|
74
|
+
const isAdmin = player_id != null && player_id == competition.admin_id;
|
|
75
|
+
const hasJoined = participants.some(p => p.player_id == player_id);
|
|
76
|
+
const entryFee = Number(competition.entry_fee) || 0;
|
|
77
|
+
const isFree = entryFee === 0;
|
|
78
|
+
const isJoinable = ['scheduled', 'auction_open'].includes(competition.status);
|
|
79
|
+
const totalPot = Number(competition.total_pot) || 0;
|
|
80
|
+
const canLeave = hasJoined && ['pending', 'scheduled'].includes(competition.status);
|
|
81
|
+
const heroImage = competition.image?.url;
|
|
82
|
+
|
|
83
|
+
const sections = [
|
|
84
|
+
{ key: 'hero' },
|
|
85
|
+
{ key: 'info' },
|
|
86
|
+
{ key: 'stats' },
|
|
87
|
+
{ key: 'cta' },
|
|
88
|
+
{ key: 'payouts' },
|
|
89
|
+
{ key: 'items-header' },
|
|
90
|
+
{ key: 'items' },
|
|
91
|
+
{ key: 'participants-header' },
|
|
92
|
+
{ key: 'participants' },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const renderSection = ({ item }: { item: { key: string } }) => {
|
|
96
|
+
switch (item.key) {
|
|
97
|
+
case 'hero':
|
|
98
|
+
return heroImage ? (
|
|
99
|
+
<Image source={{ uri: heroImage }} style={styles.heroImage} resizeMode="cover" />
|
|
100
|
+
) : (
|
|
101
|
+
<View variant="transparent" style={[styles.heroPlaceholder, { backgroundColor: theme.colors.surface.elevated }]}>
|
|
102
|
+
<Ionicons name="hammer-outline" size={48} color={theme.colors.text.tertiary} />
|
|
103
|
+
</View>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
case 'info':
|
|
107
|
+
return (
|
|
108
|
+
<View variant="transparent" style={styles.infoSection}>
|
|
109
|
+
<View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 6 }}>
|
|
110
|
+
<View variant="transparent" style={[styles.statusBadge, { backgroundColor: statusConfig.bg }]}>
|
|
111
|
+
<View variant="transparent" style={[styles.statusDot, { backgroundColor: statusConfig.color }]} />
|
|
112
|
+
<Text variant="caption" bold style={{ color: statusConfig.color }}>{statusConfig.label}</Text>
|
|
113
|
+
</View>
|
|
114
|
+
<Text variant="caption" color="tertiary" style={{ marginLeft: 8 }}>
|
|
115
|
+
{competition.auction_type === 'live' ? 'Live Auction' : 'Sealed Bid'}
|
|
116
|
+
</Text>
|
|
117
|
+
</View>
|
|
118
|
+
<Text variant="h3" bold>{competition.competition_name}</Text>
|
|
119
|
+
{competition.competition_description ? (
|
|
120
|
+
<Text variant="body" color="secondary" style={{ marginTop: 4 }}>{competition.competition_description}</Text>
|
|
121
|
+
) : null}
|
|
122
|
+
{competition.competition_code ? (
|
|
123
|
+
<View variant="transparent" style={[styles.codeChip, { backgroundColor: theme.colors.primary.subtle }]}>
|
|
124
|
+
<Ionicons name="key-outline" size={12} color={theme.colors.primary.default} />
|
|
125
|
+
<Text variant="caption" bold style={{ color: theme.colors.primary.default, marginLeft: 4 }}>{competition.competition_code}</Text>
|
|
126
|
+
</View>
|
|
127
|
+
) : null}
|
|
128
|
+
</View>
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
case 'stats':
|
|
132
|
+
return (
|
|
133
|
+
<View variant="transparent" style={[styles.statsGrid, { borderColor: theme.colors.border.subtle }]}>
|
|
134
|
+
<View variant="transparent" style={styles.statBox}>
|
|
135
|
+
<Text variant="caption" color="tertiary">Entry Fee</Text>
|
|
136
|
+
<Text variant="body" bold>{isFree ? 'FREE' : formatCurrency(entryFee, competition.market_type)}</Text>
|
|
137
|
+
</View>
|
|
138
|
+
<View variant="transparent" style={[styles.statBox, { borderLeftWidth: 1, borderColor: theme.colors.border.subtle }]}>
|
|
139
|
+
<Text variant="caption" color="tertiary">Pot</Text>
|
|
140
|
+
<Text variant="body" bold>{formatCurrency(totalPot, competition.market_type)}</Text>
|
|
141
|
+
</View>
|
|
142
|
+
<View variant="transparent" style={[styles.statBox, { borderLeftWidth: 1, borderColor: theme.colors.border.subtle }]}>
|
|
143
|
+
<Text variant="caption" color="tertiary">Players</Text>
|
|
144
|
+
<Text variant="body" bold>{participants.length}{competition.max_participants > 0 ? `/${competition.max_participants}` : ''}</Text>
|
|
145
|
+
</View>
|
|
146
|
+
<View variant="transparent" style={[styles.statBox, { borderLeftWidth: 1, borderColor: theme.colors.border.subtle }]}>
|
|
147
|
+
<Text variant="caption" color="tertiary">Items</Text>
|
|
148
|
+
<Text variant="body" bold>{items.length}</Text>
|
|
149
|
+
</View>
|
|
150
|
+
</View>
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
case 'cta':
|
|
154
|
+
return (
|
|
155
|
+
<View variant="transparent" style={styles.ctaSection}>
|
|
156
|
+
{/* Admin manage button */}
|
|
157
|
+
{isAdmin && onManage && ['pending', 'scheduled'].includes(competition.status) && (
|
|
158
|
+
<TouchableOpacity onPress={onManage} style={[styles.ctaButton, { backgroundColor: theme.colors.primary.default }]}>
|
|
159
|
+
<Ionicons name="settings-outline" size={18} color="#FFF" />
|
|
160
|
+
<Text variant="body" bold style={{ color: '#FFF', marginLeft: 8 }}>Manage Competition</Text>
|
|
161
|
+
</TouchableOpacity>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{/* Join button */}
|
|
165
|
+
{isJoinable && !hasJoined && onJoin && (
|
|
166
|
+
<TouchableOpacity
|
|
167
|
+
onPress={async () => {
|
|
168
|
+
setJoining(true);
|
|
169
|
+
try {
|
|
170
|
+
await onJoin();
|
|
171
|
+
await refresh();
|
|
172
|
+
} catch {}
|
|
173
|
+
setJoining(false);
|
|
174
|
+
}}
|
|
175
|
+
disabled={joining}
|
|
176
|
+
style={[styles.ctaButton, { backgroundColor: '#10B981', opacity: joining ? 0.7 : 1 }]}
|
|
177
|
+
>
|
|
178
|
+
{joining ? (
|
|
179
|
+
<ActivityIndicator size="small" color="#FFF" />
|
|
180
|
+
) : (
|
|
181
|
+
<Text variant="body" bold style={{ color: '#FFF' }}>
|
|
182
|
+
{isFree ? 'Join - Free!' : `Join - ${formatCurrency(entryFee, competition.market_type)}`}
|
|
183
|
+
</Text>
|
|
184
|
+
)}
|
|
185
|
+
</TouchableOpacity>
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
{/* Already joined badge + leave */}
|
|
189
|
+
{hasJoined && (
|
|
190
|
+
<View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
|
|
191
|
+
<View variant="transparent" style={[styles.joinedBadge, { flex: 1, backgroundColor: '#10B98115' }]}>
|
|
192
|
+
<Ionicons name="checkmark-circle" size={16} color="#10B981" />
|
|
193
|
+
<Text variant="caption" bold style={{ color: '#10B981', marginLeft: 6 }}>You're in!</Text>
|
|
194
|
+
</View>
|
|
195
|
+
{onLeave && canLeave && (
|
|
196
|
+
<TouchableOpacity
|
|
197
|
+
onPress={async () => {
|
|
198
|
+
setLeaving(true);
|
|
199
|
+
try {
|
|
200
|
+
await onLeave();
|
|
201
|
+
await refresh();
|
|
202
|
+
} catch {}
|
|
203
|
+
setLeaving(false);
|
|
204
|
+
}}
|
|
205
|
+
disabled={leaving}
|
|
206
|
+
style={[styles.ctaButtonOutline, { borderColor: theme.colors.status.error, paddingHorizontal: 16, opacity: leaving ? 0.7 : 1 }]}
|
|
207
|
+
>
|
|
208
|
+
{leaving ? (
|
|
209
|
+
<ActivityIndicator size="small" color={theme.colors.status.error} />
|
|
210
|
+
) : (
|
|
211
|
+
<Text variant="caption" bold style={{ color: theme.colors.status.error }}>Leave</Text>
|
|
212
|
+
)}
|
|
213
|
+
</TouchableOpacity>
|
|
214
|
+
)}
|
|
215
|
+
</View>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{/* Share button */}
|
|
219
|
+
{onShare && (
|
|
220
|
+
<TouchableOpacity onPress={onShare} style={[styles.ctaButtonOutline, { borderColor: theme.colors.border.subtle }]}>
|
|
221
|
+
<Ionicons name="share-outline" size={16} color={theme.colors.text.secondary} />
|
|
222
|
+
<Text variant="caption" bold color="secondary" style={{ marginLeft: 6 }}>Share</Text>
|
|
223
|
+
</TouchableOpacity>
|
|
224
|
+
)}
|
|
225
|
+
</View>
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
case 'payouts':
|
|
229
|
+
if (payout_rules.length === 0) return null;
|
|
230
|
+
return (
|
|
231
|
+
<View variant="transparent" style={[styles.section, { borderColor: theme.colors.border.subtle }]}>
|
|
232
|
+
<Text variant="body" bold style={styles.sectionTitle}>Payout Structure</Text>
|
|
233
|
+
{payout_rules.map((rule, i) => {
|
|
234
|
+
const round = rounds.find(r => r.round_number === rule.round_number);
|
|
235
|
+
const pct = Number(rule.payout_pct);
|
|
236
|
+
const amount = totalPot > 0 ? (pct / 100) * totalPot : 0;
|
|
237
|
+
return (
|
|
238
|
+
<View key={rule.calcutta_payout_rule_id || `rule-${i}`} variant="transparent" style={[styles.payoutRow, i < payout_rules.length - 1 && { borderBottomWidth: 1, borderColor: theme.colors.border.subtle }]}>
|
|
239
|
+
<View variant="transparent" style={{ flex: 1 }}>
|
|
240
|
+
<Text variant="body">{rule.description || round?.round_name || `Round ${rule.round_number}`}</Text>
|
|
241
|
+
</View>
|
|
242
|
+
<Text variant="body" bold>{pct}%</Text>
|
|
243
|
+
{totalPot > 0 && (
|
|
244
|
+
<Text variant="caption" color="tertiary" style={{ marginLeft: 8, minWidth: 60, textAlign: 'right' }}>
|
|
245
|
+
{formatCurrency(amount, competition.market_type)}
|
|
246
|
+
</Text>
|
|
247
|
+
)}
|
|
248
|
+
</View>
|
|
249
|
+
);
|
|
250
|
+
})}
|
|
251
|
+
</View>
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
case 'items-header':
|
|
255
|
+
if (items.length === 0) return null;
|
|
256
|
+
return (
|
|
257
|
+
<View variant="transparent" style={[styles.section, { borderColor: theme.colors.border.subtle, paddingBottom: 0 }]}>
|
|
258
|
+
<Text variant="body" bold style={styles.sectionTitle}>Auction Items ({items.length})</Text>
|
|
259
|
+
</View>
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
case 'items':
|
|
263
|
+
if (items.length === 0) return null;
|
|
264
|
+
return (
|
|
265
|
+
<View variant="transparent" style={{ paddingHorizontal: 16 }}>
|
|
266
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
267
|
+
{items.map(item => (
|
|
268
|
+
<View key={item.calcutta_auction_item_id} variant="transparent" style={[styles.itemChip, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
|
|
269
|
+
<Text variant="caption" bold numberOfLines={1}>{item.item_name}</Text>
|
|
270
|
+
{item.seed != null && <Text variant="caption" color="tertiary" style={{ fontSize: 10 }}>#{item.seed}</Text>}
|
|
271
|
+
</View>
|
|
272
|
+
))}
|
|
273
|
+
</ScrollView>
|
|
274
|
+
</View>
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
case 'participants-header':
|
|
278
|
+
return (
|
|
279
|
+
<View variant="transparent" style={[styles.section, { borderColor: theme.colors.border.subtle, paddingBottom: 0 }]}>
|
|
280
|
+
<Text variant="body" bold style={styles.sectionTitle}>
|
|
281
|
+
Players ({participants.length})
|
|
282
|
+
</Text>
|
|
283
|
+
</View>
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
case 'participants':
|
|
287
|
+
if (participants.length === 0) {
|
|
288
|
+
return (
|
|
289
|
+
<View variant="transparent" style={{ padding: 20, alignItems: 'center' }}>
|
|
290
|
+
<Ionicons name="people-outline" size={32} color={theme.colors.text.tertiary} />
|
|
291
|
+
<Text variant="caption" color="tertiary" style={{ marginTop: 8 }}>No players yet. Share the code to invite friends!</Text>
|
|
292
|
+
</View>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
return (
|
|
296
|
+
<View variant="transparent" style={{ paddingHorizontal: 16, paddingBottom: 40 }}>
|
|
297
|
+
{participants.map((p, i) => {
|
|
298
|
+
const enriched = enrichedPlayers[p.player_id];
|
|
299
|
+
const username = enriched?.username || enriched?.show_name || `Player ${p.player_id.slice(0, 6)}`;
|
|
300
|
+
const profilePic = enriched?.profile_pic;
|
|
301
|
+
return (
|
|
302
|
+
<View key={p.calcutta_participant_id} variant="transparent" style={[styles.participantRow, i < participants.length - 1 && { borderBottomWidth: 1, borderColor: theme.colors.border.subtle }]}>
|
|
303
|
+
{profilePic ? (
|
|
304
|
+
<Image source={{ uri: profilePic }} style={[styles.avatarCircle, { overflow: 'hidden' }]} />
|
|
305
|
+
) : (
|
|
306
|
+
<View variant="transparent" style={[styles.avatarCircle, { backgroundColor: theme.colors.primary.subtle }]}>
|
|
307
|
+
<Text variant="caption" bold style={{ color: theme.colors.primary.default }}>
|
|
308
|
+
{username.charAt(0).toUpperCase()}
|
|
309
|
+
</Text>
|
|
310
|
+
</View>
|
|
311
|
+
)}
|
|
312
|
+
<View variant="transparent" style={{ flex: 1 }}>
|
|
313
|
+
<Text variant="body">{username}</Text>
|
|
314
|
+
<Text variant="caption" color="tertiary">
|
|
315
|
+
{p.items_owned > 0 ? `${p.items_owned} items \u00B7 ${formatCurrency(p.total_spent, competition.market_type)} spent` : 'Joined'}
|
|
316
|
+
</Text>
|
|
317
|
+
</View>
|
|
318
|
+
{p.player_id == player_id && (
|
|
319
|
+
<View variant="transparent" style={[styles.youBadge, { backgroundColor: theme.colors.primary.subtle }]}>
|
|
320
|
+
<Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 10 }}>YOU</Text>
|
|
321
|
+
</View>
|
|
322
|
+
)}
|
|
323
|
+
</View>
|
|
324
|
+
);
|
|
325
|
+
})}
|
|
326
|
+
</View>
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
default:
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
<View variant="transparent" style={styles.container}>
|
|
336
|
+
{/* Header bar */}
|
|
337
|
+
<View variant="transparent" style={[styles.headerBar, { borderColor: theme.colors.border.subtle }]}>
|
|
338
|
+
<TouchableOpacity onPress={onClose} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
|
339
|
+
<Ionicons name="arrow-back" size={24} color={theme.colors.text.primary} />
|
|
340
|
+
</TouchableOpacity>
|
|
341
|
+
<Text variant="body" bold style={{ flex: 1, marginLeft: 12 }} numberOfLines={1}>{competition.competition_name}</Text>
|
|
342
|
+
</View>
|
|
343
|
+
|
|
344
|
+
<FlatList
|
|
345
|
+
data={sections}
|
|
346
|
+
keyExtractor={(item) => item.key}
|
|
347
|
+
renderItem={renderSection}
|
|
348
|
+
showsVerticalScrollIndicator={false}
|
|
349
|
+
/>
|
|
350
|
+
</View>
|
|
351
|
+
);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const styles = StyleSheet.create({
|
|
355
|
+
container: { flex: 1 },
|
|
356
|
+
loadingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 40 },
|
|
357
|
+
headerBar: { flexDirection: 'row', alignItems: 'center', padding: 12, borderBottomWidth: 1 },
|
|
358
|
+
heroImage: { width: '100%', height: 180 },
|
|
359
|
+
heroPlaceholder: { width: '100%', height: 140, alignItems: 'center', justifyContent: 'center' },
|
|
360
|
+
infoSection: { padding: 16 },
|
|
361
|
+
statusBadge: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 12 },
|
|
362
|
+
statusDot: { width: 6, height: 6, borderRadius: 3, marginRight: 6 },
|
|
363
|
+
codeChip: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', paddingHorizontal: 10, paddingVertical: 5, borderRadius: 12, marginTop: 10 },
|
|
364
|
+
statsGrid: { flexDirection: 'row', borderTopWidth: 1, borderBottomWidth: 1 },
|
|
365
|
+
statBox: { flex: 1, alignItems: 'center', paddingVertical: 12 },
|
|
366
|
+
ctaSection: { padding: 16, gap: 10 },
|
|
367
|
+
ctaButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 14, borderRadius: 10 },
|
|
368
|
+
ctaButtonOutline: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 10, borderRadius: 10, borderWidth: 1 },
|
|
369
|
+
joinedBadge: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 10, borderRadius: 10 },
|
|
370
|
+
section: { padding: 16, borderTopWidth: 1 },
|
|
371
|
+
sectionTitle: { marginBottom: 10 },
|
|
372
|
+
payoutRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10 },
|
|
373
|
+
itemChip: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 8, borderWidth: 1, marginRight: 8, minWidth: 80, alignItems: 'center' },
|
|
374
|
+
participantRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10 },
|
|
375
|
+
avatarCircle: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', marginRight: 10 },
|
|
376
|
+
youBadge: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 8 },
|
|
377
|
+
});
|