@bettoredge/calcutta 0.3.0 → 0.4.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.
@@ -0,0 +1,154 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { StyleSheet, Animated, TouchableOpacity } from 'react-native';
3
+ import { View, Text, useTheme } from '@bettoredge/styles';
4
+ import { Ionicons } from '@expo/vector-icons';
5
+
6
+ export interface AuctionPausedOverlayProps {
7
+ visible: boolean;
8
+ isAdmin?: boolean;
9
+ onResume?: () => void;
10
+ resuming?: boolean;
11
+ }
12
+
13
+ export const AuctionPausedOverlay: React.FC<AuctionPausedOverlayProps> = ({
14
+ visible,
15
+ isAdmin,
16
+ onResume,
17
+ resuming,
18
+ }) => {
19
+ const { theme } = useTheme();
20
+ const opacityAnim = useRef(new Animated.Value(0)).current;
21
+ const pulseAnim = useRef(new Animated.Value(1)).current;
22
+
23
+ useEffect(() => {
24
+ if (visible) {
25
+ Animated.timing(opacityAnim, { toValue: 1, duration: 300, useNativeDriver: true }).start();
26
+ const loop = Animated.loop(
27
+ Animated.sequence([
28
+ Animated.timing(pulseAnim, { toValue: 0.5, duration: 1200, useNativeDriver: true }),
29
+ Animated.timing(pulseAnim, { toValue: 1, duration: 1200, useNativeDriver: true }),
30
+ ])
31
+ );
32
+ loop.start();
33
+ return () => loop.stop();
34
+ } else {
35
+ Animated.timing(opacityAnim, { toValue: 0, duration: 200, useNativeDriver: true }).start();
36
+ }
37
+ }, [visible]);
38
+
39
+ if (!visible) return null;
40
+
41
+ return (
42
+ <Animated.View style={[styles.overlay, { opacity: opacityAnim }]}>
43
+ <View variant="transparent" style={styles.backdrop} />
44
+ <View variant="transparent" style={styles.content}>
45
+ <Animated.View style={{ opacity: pulseAnim }}>
46
+ <View variant="transparent" style={styles.iconCircle}>
47
+ <Ionicons name="pause" size={48} color="#FFFFFF" />
48
+ </View>
49
+ </Animated.View>
50
+
51
+ <Text style={styles.title}>Auction Paused</Text>
52
+ <Text style={styles.subtitle}>
53
+ {isAdmin
54
+ ? 'The auction is paused. Resume when ready.'
55
+ : 'The host has paused the auction.\nBidding will resume shortly.'}
56
+ </Text>
57
+
58
+ {isAdmin && onResume && (
59
+ <TouchableOpacity
60
+ style={[styles.resumeButton, resuming && { opacity: 0.6 }]}
61
+ onPress={onResume}
62
+ disabled={resuming}
63
+ activeOpacity={0.7}
64
+ >
65
+ <Ionicons name="play" size={20} color="#FFFFFF" />
66
+ <Text style={styles.resumeText}>
67
+ {resuming ? 'Resuming...' : 'Resume Auction'}
68
+ </Text>
69
+ </TouchableOpacity>
70
+ )}
71
+
72
+ {!isAdmin && (
73
+ <View variant="transparent" style={styles.waitingRow}>
74
+ <Animated.View style={{ opacity: pulseAnim }}>
75
+ <View variant="transparent" style={styles.waitingDot} />
76
+ </Animated.View>
77
+ <Text style={styles.waitingText}>Waiting for host...</Text>
78
+ </View>
79
+ )}
80
+ </View>
81
+ </Animated.View>
82
+ );
83
+ };
84
+
85
+ const styles = StyleSheet.create({
86
+ overlay: {
87
+ ...StyleSheet.absoluteFillObject,
88
+ zIndex: 999,
89
+ alignItems: 'center',
90
+ justifyContent: 'center',
91
+ },
92
+ backdrop: {
93
+ ...StyleSheet.absoluteFillObject,
94
+ backgroundColor: 'rgba(0,0,0,0.80)',
95
+ },
96
+ content: {
97
+ alignItems: 'center',
98
+ padding: 40,
99
+ },
100
+ iconCircle: {
101
+ width: 100,
102
+ height: 100,
103
+ borderRadius: 50,
104
+ backgroundColor: '#EF4444',
105
+ alignItems: 'center',
106
+ justifyContent: 'center',
107
+ marginBottom: 24,
108
+ },
109
+ title: {
110
+ color: '#FFFFFF',
111
+ fontSize: 28,
112
+ lineHeight: 36,
113
+ fontWeight: '800',
114
+ marginBottom: 8,
115
+ },
116
+ subtitle: {
117
+ color: 'rgba(255,255,255,0.7)',
118
+ fontSize: 16,
119
+ textAlign: 'center',
120
+ lineHeight: 22,
121
+ marginBottom: 32,
122
+ },
123
+ resumeButton: {
124
+ flexDirection: 'row',
125
+ alignItems: 'center',
126
+ backgroundColor: '#10B981',
127
+ paddingHorizontal: 28,
128
+ paddingVertical: 14,
129
+ borderRadius: 12,
130
+ },
131
+ resumeText: {
132
+ color: '#FFFFFF',
133
+ fontSize: 17,
134
+ lineHeight: 24,
135
+ fontWeight: '700',
136
+ marginLeft: 10,
137
+ },
138
+ waitingRow: {
139
+ flexDirection: 'row',
140
+ alignItems: 'center',
141
+ },
142
+ waitingDot: {
143
+ width: 8,
144
+ height: 8,
145
+ borderRadius: 4,
146
+ backgroundColor: '#F59E0B',
147
+ marginRight: 8,
148
+ },
149
+ waitingText: {
150
+ color: 'rgba(255,255,255,0.5)',
151
+ fontSize: 14,
152
+ lineHeight: 19,
153
+ },
154
+ });
@@ -0,0 +1,291 @@
1
+ import React from 'react';
2
+ import { StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
3
+ import { View, Text, useTheme } from '@bettoredge/styles';
4
+ import { Ionicons } from '@expo/vector-icons';
5
+ import type { CalcuttaCompetitionProps } from '@bettoredge/types';
6
+ import type { CalcuttaLifecycleState } from '../helpers/lifecycleState';
7
+ import { formatCurrency } from '../helpers/formatting';
8
+ import { CalcuttaCountdown } from './CalcuttaCountdown';
9
+
10
+ export interface CalcuttaActionCardProps {
11
+ competition: CalcuttaCompetitionProps;
12
+ lifecycleState: CalcuttaLifecycleState;
13
+ hasJoined: boolean;
14
+ escrowBalance?: number;
15
+ activeBidCount?: number;
16
+ itemsWon?: number;
17
+ isAdmin?: boolean;
18
+ onJoin?: () => void;
19
+ onDepositEscrow?: () => void;
20
+ onManage?: () => void;
21
+ onStartAuction?: () => void;
22
+ joining?: boolean;
23
+ }
24
+
25
+ interface CardConfig {
26
+ icon: string;
27
+ title: string;
28
+ description: string;
29
+ accentColor: string;
30
+ ctaLabel?: string;
31
+ ctaColor?: string;
32
+ ctaAction?: () => void;
33
+ showCountdown?: 'start' | 'end';
34
+ loading?: boolean;
35
+ }
36
+
37
+ export const CalcuttaActionCard: React.FC<CalcuttaActionCardProps> = ({
38
+ competition,
39
+ lifecycleState,
40
+ hasJoined,
41
+ escrowBalance = 0,
42
+ activeBidCount = 0,
43
+ itemsWon = 0,
44
+ isAdmin,
45
+ onJoin,
46
+ onDepositEscrow,
47
+ onManage,
48
+ onStartAuction,
49
+ joining,
50
+ }) => {
51
+ const { theme } = useTheme();
52
+ const isSweepstakes = competition.auction_type === 'sweepstakes';
53
+ const entryFee = Number(competition.entry_fee) || 0;
54
+ const isFree = entryFee === 0;
55
+
56
+ const getConfig = (): CardConfig => {
57
+ // Admin: can start auction
58
+ if (isAdmin && lifecycleState === 'scheduled' && onStartAuction) {
59
+ const participantCount = competition.participants?.length ?? 0;
60
+ return {
61
+ icon: 'play-circle-outline',
62
+ title: 'Ready to start?',
63
+ description: `${participantCount} player${participantCount !== 1 ? 's' : ''} have joined. Start the ${isSweepstakes ? 'competition' : 'auction'} when ready.`,
64
+ accentColor: theme.colors.primary.default,
65
+ ctaLabel: isSweepstakes ? 'Start Competition' : 'Start Auction',
66
+ ctaColor: theme.colors.primary.default,
67
+ ctaAction: onStartAuction,
68
+ showCountdown: 'start',
69
+ };
70
+ }
71
+
72
+ // Not joined — prompt to join
73
+ if (!hasJoined && (lifecycleState === 'scheduled' || (lifecycleState === 'auctioning' && !isSweepstakes))) {
74
+ return {
75
+ icon: 'enter-outline',
76
+ title: isSweepstakes ? 'Join for a random team!' : 'Join this auction',
77
+ description: isFree
78
+ ? 'Free to enter'
79
+ : `Entry fee: ${formatCurrency(entryFee, competition.market_type)}`,
80
+ accentColor: '#10B981',
81
+ ctaLabel: isFree ? 'Join Free' : `Join — ${formatCurrency(entryFee, competition.market_type)}`,
82
+ ctaColor: '#10B981',
83
+ ctaAction: onJoin,
84
+ showCountdown: 'start',
85
+ loading: joining,
86
+ };
87
+ }
88
+
89
+ // Joined + scheduled
90
+ if (hasJoined && lifecycleState === 'scheduled') {
91
+ // Sweepstakes — just waiting
92
+ if (isSweepstakes) {
93
+ return {
94
+ icon: 'checkmark-circle',
95
+ title: "You're in!",
96
+ description: 'Your team will be assigned when the host starts the competition.',
97
+ accentColor: '#10B981',
98
+ showCountdown: 'start',
99
+ };
100
+ }
101
+ // No escrow — prompt to fund
102
+ if (escrowBalance <= 0) {
103
+ return {
104
+ icon: 'wallet-outline',
105
+ title: 'Fund your escrow',
106
+ description: 'You need funds in your escrow to place bids when the auction opens.',
107
+ accentColor: '#D97706',
108
+ ctaLabel: 'Add Funds',
109
+ ctaColor: '#D97706',
110
+ ctaAction: onDepositEscrow,
111
+ showCountdown: 'start',
112
+ };
113
+ }
114
+ // Has escrow — ready
115
+ return {
116
+ icon: 'checkmark-circle',
117
+ title: "You're ready!",
118
+ description: `Escrow: ${formatCurrency(escrowBalance, competition.market_type)} available`,
119
+ accentColor: '#10B981',
120
+ showCountdown: 'start',
121
+ };
122
+ }
123
+
124
+ // Auction active
125
+ if (lifecycleState === 'auctioning' && hasJoined) {
126
+ if (activeBidCount > 0) {
127
+ return {
128
+ icon: 'trophy-outline',
129
+ title: `You have ${activeBidCount} active bid${activeBidCount !== 1 ? 's' : ''}`,
130
+ description: competition.auction_type === 'sealed_bid'
131
+ ? 'Bids are sealed — results revealed when auction closes.'
132
+ : 'Keep watching for outbid notifications.',
133
+ accentColor: '#D97706',
134
+ showCountdown: competition.auction_type === 'sealed_bid' ? 'end' : undefined,
135
+ };
136
+ }
137
+ return {
138
+ icon: 'flash-outline',
139
+ title: 'Auction is live!',
140
+ description: 'Start placing your bids now.',
141
+ accentColor: '#D97706',
142
+ showCountdown: competition.auction_type === 'sealed_bid' ? 'end' : undefined,
143
+ };
144
+ }
145
+
146
+ // Tournament in progress
147
+ if (lifecycleState === 'tournament') {
148
+ return {
149
+ icon: 'football-outline',
150
+ title: 'Tournament in progress',
151
+ description: itemsWon > 0
152
+ ? `You own ${itemsWon} item${itemsWon !== 1 ? 's' : ''} — track results below.`
153
+ : 'Follow the action below.',
154
+ accentColor: '#8B5CF6',
155
+ };
156
+ }
157
+
158
+ // Completed
159
+ if (lifecycleState === 'completed') {
160
+ return {
161
+ icon: 'ribbon-outline',
162
+ title: 'Competition complete',
163
+ description: itemsWon > 0
164
+ ? `You owned ${itemsWon} item${itemsWon !== 1 ? 's' : ''}.`
165
+ : 'This competition has ended.',
166
+ accentColor: theme.colors.text.tertiary,
167
+ };
168
+ }
169
+
170
+ // Admin manage fallback
171
+ if (isAdmin && onManage) {
172
+ return {
173
+ icon: 'settings-outline',
174
+ title: 'Manage Competition',
175
+ description: 'Configure settings, items, and payouts.',
176
+ accentColor: theme.colors.primary.default,
177
+ ctaLabel: 'Manage',
178
+ ctaColor: theme.colors.primary.default,
179
+ ctaAction: onManage,
180
+ };
181
+ }
182
+
183
+ // Default
184
+ return {
185
+ icon: 'information-circle-outline',
186
+ title: competition.competition_name,
187
+ description: 'Competition details',
188
+ accentColor: theme.colors.text.tertiary,
189
+ };
190
+ };
191
+
192
+ const config = getConfig();
193
+
194
+ return (
195
+ <View variant="transparent" style={[styles.card, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
196
+ {/* Left accent strip */}
197
+ <View variant="transparent" style={[styles.accent, { backgroundColor: config.accentColor }]} />
198
+
199
+ <View variant="transparent" style={styles.content}>
200
+ {/* Icon + Text */}
201
+ <View variant="transparent" style={styles.topRow}>
202
+ <View variant="transparent" style={[styles.iconCircle, { backgroundColor: config.accentColor + '18' }]}>
203
+ <Ionicons name={config.icon as any} size={22} color={config.accentColor} />
204
+ </View>
205
+ <View variant="transparent" style={styles.textBlock}>
206
+ <Text variant="body" bold>{config.title}</Text>
207
+ <Text variant="caption" color="secondary" style={{ marginTop: 2 }}>{config.description}</Text>
208
+ </View>
209
+ </View>
210
+
211
+ {/* Countdown */}
212
+ {config.showCountdown === 'start' && competition.scheduled_datetime && (
213
+ <View variant="transparent" style={styles.countdownRow}>
214
+ <CalcuttaCountdown
215
+ targetDate={competition.scheduled_datetime}
216
+ label="Auction starts"
217
+ size="compact"
218
+ />
219
+ </View>
220
+ )}
221
+ {config.showCountdown === 'end' && competition.auction_end_datetime && (
222
+ <View variant="transparent" style={styles.countdownRow}>
223
+ <CalcuttaCountdown
224
+ targetDate={competition.auction_end_datetime}
225
+ label="Bidding closes"
226
+ size="compact"
227
+ />
228
+ </View>
229
+ )}
230
+
231
+ {/* CTA Button */}
232
+ {config.ctaLabel && config.ctaAction && (
233
+ <TouchableOpacity
234
+ style={[styles.ctaButton, { backgroundColor: config.ctaColor || theme.colors.primary.default }]}
235
+ onPress={config.ctaAction}
236
+ activeOpacity={0.7}
237
+ disabled={config.loading}
238
+ >
239
+ {config.loading ? (
240
+ <ActivityIndicator size="small" color="#FFFFFF" />
241
+ ) : (
242
+ <Text variant="body" bold style={{ color: '#FFFFFF' }}>{config.ctaLabel}</Text>
243
+ )}
244
+ </TouchableOpacity>
245
+ )}
246
+ </View>
247
+ </View>
248
+ );
249
+ };
250
+
251
+ const styles = StyleSheet.create({
252
+ card: {
253
+ borderRadius: 16,
254
+ borderWidth: 1,
255
+ flexDirection: 'row',
256
+ overflow: 'hidden',
257
+ },
258
+ accent: {
259
+ width: 4,
260
+ },
261
+ content: {
262
+ flex: 1,
263
+ padding: 16,
264
+ },
265
+ topRow: {
266
+ flexDirection: 'row',
267
+ alignItems: 'flex-start',
268
+ },
269
+ iconCircle: {
270
+ width: 42,
271
+ height: 42,
272
+ borderRadius: 21,
273
+ alignItems: 'center',
274
+ justifyContent: 'center',
275
+ },
276
+ textBlock: {
277
+ flex: 1,
278
+ marginLeft: 12,
279
+ justifyContent: 'center',
280
+ },
281
+ countdownRow: {
282
+ marginTop: 10,
283
+ },
284
+ ctaButton: {
285
+ marginTop: 14,
286
+ height: 44,
287
+ borderRadius: 10,
288
+ alignItems: 'center',
289
+ justifyContent: 'center',
290
+ },
291
+ });