@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.
Files changed (38) hide show
  1. package/package.json +46 -0
  2. package/src/components/CalcuttaAuction.tsx +453 -0
  3. package/src/components/CalcuttaAuctionItem.tsx +292 -0
  4. package/src/components/CalcuttaBidInput.tsx +214 -0
  5. package/src/components/CalcuttaCard.tsx +131 -0
  6. package/src/components/CalcuttaDetail.tsx +377 -0
  7. package/src/components/CalcuttaEscrow.tsx +464 -0
  8. package/src/components/CalcuttaItemResults.tsx +207 -0
  9. package/src/components/CalcuttaLeaderboard.tsx +179 -0
  10. package/src/components/CalcuttaPayoutPreview.tsx +194 -0
  11. package/src/components/CalcuttaRoundResults.tsx +250 -0
  12. package/src/components/CalcuttaTemplateSelector.tsx +124 -0
  13. package/src/components/sealed/AuctionResultsModal.tsx +165 -0
  14. package/src/components/sealed/EscrowBottomSheet.tsx +185 -0
  15. package/src/components/sealed/SealedBidAuction.tsx +541 -0
  16. package/src/components/sealed/SealedBidHeader.tsx +116 -0
  17. package/src/components/sealed/SealedBidInfoTab.tsx +247 -0
  18. package/src/components/sealed/SealedBidItemCard.tsx +385 -0
  19. package/src/components/sealed/SealedBidItemsTab.tsx +235 -0
  20. package/src/components/sealed/SealedBidMyBidsTab.tsx +512 -0
  21. package/src/components/sealed/SealedBidPlayersTab.tsx +220 -0
  22. package/src/components/sealed/SealedBidStatusBar.tsx +415 -0
  23. package/src/components/sealed/SealedBidTabBar.tsx +172 -0
  24. package/src/helpers/formatting.ts +56 -0
  25. package/src/helpers/lifecycleState.ts +71 -0
  26. package/src/helpers/payout.ts +39 -0
  27. package/src/helpers/validation.ts +64 -0
  28. package/src/hooks/useCalcuttaAuction.ts +164 -0
  29. package/src/hooks/useCalcuttaBid.ts +43 -0
  30. package/src/hooks/useCalcuttaCompetition.ts +63 -0
  31. package/src/hooks/useCalcuttaEscrow.ts +52 -0
  32. package/src/hooks/useCalcuttaItemImages.ts +79 -0
  33. package/src/hooks/useCalcuttaPlayers.ts +46 -0
  34. package/src/hooks/useCalcuttaResults.ts +58 -0
  35. package/src/hooks/useCalcuttaSocket.ts +131 -0
  36. package/src/hooks/useCalcuttaTemplates.ts +36 -0
  37. package/src/index.ts +74 -0
  38. package/src/types.ts +31 -0
@@ -0,0 +1,124 @@
1
+ import React from 'react';
2
+ import { StyleSheet, TouchableOpacity, FlatList, ActivityIndicator } from 'react-native';
3
+ import { View, Text, useTheme } from '@bettoredge/styles';
4
+ import { Ionicons } from '@expo/vector-icons';
5
+ import type { CalcuttaTemplateProps } from '@bettoredge/types';
6
+ import { useCalcuttaTemplates } from '../hooks/useCalcuttaTemplates';
7
+
8
+ export interface CalcuttaTemplateSelectorProps {
9
+ selectedTemplateId?: string;
10
+ onSelect: (template: CalcuttaTemplateProps | undefined) => void;
11
+ }
12
+
13
+ export const CalcuttaTemplateSelector: React.FC<CalcuttaTemplateSelectorProps> = ({
14
+ selectedTemplateId,
15
+ onSelect,
16
+ }) => {
17
+ const { theme } = useTheme();
18
+ const { loading, templates } = useCalcuttaTemplates();
19
+
20
+ if (loading) {
21
+ return (
22
+ <View variant="transparent" style={styles.centered}>
23
+ <ActivityIndicator size="small" color={theme.colors.primary.default} />
24
+ </View>
25
+ );
26
+ }
27
+
28
+ if (templates.length === 0) {
29
+ return null;
30
+ }
31
+
32
+ return (
33
+ <View variant="transparent" style={styles.container}>
34
+ <Text variant="caption" style={{ color: theme.colors.text.secondary, marginBottom: 8 }}>
35
+ Use a Template (optional)
36
+ </Text>
37
+
38
+ {selectedTemplateId && (
39
+ <TouchableOpacity
40
+ style={[styles.clearButton, { borderColor: theme.colors.border.subtle }]}
41
+ onPress={() => onSelect(undefined)}
42
+ >
43
+ <Ionicons name="close-circle-outline" size={16} color={theme.colors.text.secondary} />
44
+ <Text variant="caption" style={{ color: theme.colors.text.secondary, marginLeft: 4 }}>
45
+ Clear template
46
+ </Text>
47
+ </TouchableOpacity>
48
+ )}
49
+
50
+ <FlatList
51
+ data={templates}
52
+ horizontal
53
+ showsHorizontalScrollIndicator={false}
54
+ keyExtractor={(item) => item.calcutta_template_id}
55
+ renderItem={({ item }) => {
56
+ const isSelected = item.calcutta_template_id === selectedTemplateId;
57
+ return (
58
+ <TouchableOpacity
59
+ style={[
60
+ styles.card,
61
+ {
62
+ borderColor: isSelected ? theme.colors.primary.default : theme.colors.border.subtle,
63
+ backgroundColor: isSelected ? theme.colors.primary.subtle : theme.colors.surface.elevated,
64
+ },
65
+ ]}
66
+ activeOpacity={0.7}
67
+ onPress={() => onSelect(isSelected ? undefined : item)}
68
+ >
69
+ <Ionicons
70
+ name="copy-outline"
71
+ size={20}
72
+ color={isSelected ? theme.colors.primary.default : theme.colors.text.tertiary}
73
+ />
74
+ <Text
75
+ variant="body"
76
+ bold
77
+ numberOfLines={1}
78
+ style={{ marginTop: 4 }}
79
+ >
80
+ {item.template_name}
81
+ </Text>
82
+ {item.sport && (
83
+ <Text variant="caption" style={{ color: theme.colors.text.secondary }}>
84
+ {item.sport}
85
+ </Text>
86
+ )}
87
+ <Text variant="caption" style={{ color: theme.colors.text.tertiary, marginTop: 2 }}>
88
+ {item.items?.length ?? '?'} items · {item.rounds?.length ?? '?'} rounds
89
+ </Text>
90
+ </TouchableOpacity>
91
+ );
92
+ }}
93
+ />
94
+ </View>
95
+ );
96
+ };
97
+
98
+ const styles = StyleSheet.create({
99
+ container: {
100
+ marginVertical: 8,
101
+ },
102
+ centered: {
103
+ alignItems: 'center',
104
+ justifyContent: 'center',
105
+ paddingVertical: 16,
106
+ },
107
+ clearButton: {
108
+ flexDirection: 'row',
109
+ alignItems: 'center',
110
+ alignSelf: 'flex-start',
111
+ paddingHorizontal: 8,
112
+ paddingVertical: 4,
113
+ borderRadius: 12,
114
+ borderWidth: 1,
115
+ marginBottom: 8,
116
+ },
117
+ card: {
118
+ width: 140,
119
+ padding: 12,
120
+ borderRadius: 8,
121
+ borderWidth: 1.5,
122
+ marginRight: 10,
123
+ },
124
+ });
@@ -0,0 +1,165 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { StyleSheet, TouchableOpacity, Animated, Dimensions, FlatList } from 'react-native';
3
+ import { View, Text, useTheme } from '@bettoredge/styles';
4
+ import { Ionicons } from '@expo/vector-icons';
5
+ import type { CalcuttaAuctionItemProps } from '@bettoredge/types';
6
+ import { formatCurrency } from '../../helpers/formatting';
7
+
8
+ interface AuctionResultsModalProps {
9
+ visible: boolean;
10
+ onDismiss: () => void;
11
+ wonItems: CalcuttaAuctionItemProps[];
12
+ market_type: string;
13
+ }
14
+
15
+ const SCREEN_HEIGHT = Dimensions.get('window').height;
16
+
17
+ export const AuctionResultsModal: React.FC<AuctionResultsModalProps> = ({
18
+ visible,
19
+ onDismiss,
20
+ wonItems,
21
+ market_type,
22
+ }) => {
23
+ const { theme } = useTheme();
24
+ const scaleAnim = useRef(new Animated.Value(0.8)).current;
25
+ const opacityAnim = useRef(new Animated.Value(0)).current;
26
+
27
+ useEffect(() => {
28
+ if (visible) {
29
+ Animated.parallel([
30
+ Animated.spring(scaleAnim, {
31
+ toValue: 1,
32
+ friction: 6,
33
+ tension: 40,
34
+ useNativeDriver: true,
35
+ }),
36
+ Animated.timing(opacityAnim, {
37
+ toValue: 1,
38
+ duration: 300,
39
+ useNativeDriver: true,
40
+ }),
41
+ ]).start();
42
+ } else {
43
+ scaleAnim.setValue(0.8);
44
+ opacityAnim.setValue(0);
45
+ }
46
+ }, [visible]);
47
+
48
+ if (!visible) return null;
49
+
50
+ const totalSpent = wonItems.reduce((s, i) => s + Number(i.winning_bid), 0);
51
+ const hasWins = wonItems.length > 0;
52
+
53
+ return (
54
+ <Animated.View style={[StyleSheet.absoluteFill, styles.overlay, { opacity: opacityAnim }]}>
55
+ <Animated.View
56
+ style={[
57
+ styles.modal,
58
+ {
59
+ backgroundColor: theme.colors.surface.base,
60
+ transform: [{ scale: scaleAnim }],
61
+ },
62
+ ]}
63
+ >
64
+ {/* Header */}
65
+ <View variant="transparent" style={styles.header}>
66
+ {hasWins ? (
67
+ <>
68
+ <Ionicons name="trophy" size={48} color="#F59E0B" />
69
+ <Text variant="h2" bold style={{ marginTop: 12 }}>
70
+ Congratulations!
71
+ </Text>
72
+ <Text variant="body" color="secondary" style={{ marginTop: 4, textAlign: 'center' }}>
73
+ You won {wonItems.length} item{wonItems.length !== 1 ? 's' : ''} for {formatCurrency(totalSpent, market_type)}
74
+ </Text>
75
+ </>
76
+ ) : (
77
+ <>
78
+ <Ionicons name="ribbon-outline" size={48} color={theme.colors.text.tertiary} />
79
+ <Text variant="h2" bold style={{ marginTop: 12 }}>
80
+ Auction Complete
81
+ </Text>
82
+ <Text variant="body" color="secondary" style={{ marginTop: 4, textAlign: 'center' }}>
83
+ You didn't win any items this time. Better luck next auction!
84
+ </Text>
85
+ </>
86
+ )}
87
+ </View>
88
+
89
+ {/* Won items list */}
90
+ {hasWins && (
91
+ <View variant="transparent" style={styles.itemsList}>
92
+ {wonItems.slice(0, 6).map(item => (
93
+ <View
94
+ key={item.calcutta_auction_item_id}
95
+ variant="transparent"
96
+ style={[styles.wonItem, { backgroundColor: theme.colors.status.successSubtle }]}
97
+ >
98
+ <Ionicons name="checkmark-circle" size={16} color={theme.colors.status.success} />
99
+ <Text variant="body" bold style={{ flex: 1, marginLeft: 8 }} numberOfLines={1}>
100
+ {item.item_name}
101
+ </Text>
102
+ <Text variant="body" bold style={{ color: theme.colors.status.success }}>
103
+ {formatCurrency(item.winning_bid, market_type)}
104
+ </Text>
105
+ </View>
106
+ ))}
107
+ {wonItems.length > 6 && (
108
+ <Text variant="caption" color="tertiary" style={{ textAlign: 'center', marginTop: 4 }}>
109
+ +{wonItems.length - 6} more item{wonItems.length - 6 !== 1 ? 's' : ''}
110
+ </Text>
111
+ )}
112
+ </View>
113
+ )}
114
+
115
+ {/* CTA */}
116
+ <TouchableOpacity
117
+ style={[styles.ctaButton, { backgroundColor: theme.colors.primary.default }]}
118
+ onPress={onDismiss}
119
+ activeOpacity={0.7}
120
+ >
121
+ <Text variant="body" bold style={{ color: '#FFFFFF' }}>
122
+ {hasWins ? 'View My Items' : 'View Results'}
123
+ </Text>
124
+ </TouchableOpacity>
125
+ </Animated.View>
126
+ </Animated.View>
127
+ );
128
+ };
129
+
130
+ const styles = StyleSheet.create({
131
+ overlay: {
132
+ backgroundColor: 'rgba(0,0,0,0.6)',
133
+ alignItems: 'center',
134
+ justifyContent: 'center',
135
+ padding: 24,
136
+ },
137
+ modal: {
138
+ width: '100%',
139
+ maxWidth: 360,
140
+ borderRadius: 16,
141
+ padding: 24,
142
+ alignItems: 'center',
143
+ },
144
+ header: {
145
+ alignItems: 'center',
146
+ marginBottom: 20,
147
+ },
148
+ itemsList: {
149
+ width: '100%',
150
+ marginBottom: 20,
151
+ gap: 8,
152
+ },
153
+ wonItem: {
154
+ flexDirection: 'row',
155
+ alignItems: 'center',
156
+ padding: 10,
157
+ borderRadius: 8,
158
+ },
159
+ ctaButton: {
160
+ width: '100%',
161
+ paddingVertical: 14,
162
+ borderRadius: 10,
163
+ alignItems: 'center',
164
+ },
165
+ });
@@ -0,0 +1,185 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import {
3
+ StyleSheet,
4
+ TouchableWithoutFeedback,
5
+ Animated,
6
+ Dimensions,
7
+ Keyboard,
8
+ ScrollView,
9
+ Platform,
10
+ } from 'react-native';
11
+ import { View, Text, useTheme } from '@bettoredge/styles';
12
+ import type { CalcuttaEscrowProps } from '@bettoredge/types';
13
+ import type { CalcuttaLifecycleState } from '../../helpers/lifecycleState';
14
+ import { canManageEscrow } from '../../helpers/lifecycleState';
15
+ import { CalcuttaEscrow } from '../CalcuttaEscrow';
16
+
17
+ interface EscrowBottomSheetProps {
18
+ visible: boolean;
19
+ onDismiss: () => void;
20
+ calcutta_competition_id: string;
21
+ escrow?: CalcuttaEscrowProps;
22
+ market_type: string;
23
+ max_escrow?: number;
24
+ player_balance?: number;
25
+ onDepositFunds?: (amount: number) => void;
26
+ onUpdate: () => void;
27
+ lifecycleState: CalcuttaLifecycleState;
28
+ }
29
+
30
+ const SCREEN_HEIGHT = Dimensions.get('window').height;
31
+
32
+ export const EscrowBottomSheet: React.FC<EscrowBottomSheetProps> = ({
33
+ visible,
34
+ onDismiss,
35
+ calcutta_competition_id,
36
+ escrow,
37
+ market_type,
38
+ max_escrow,
39
+ player_balance,
40
+ onDepositFunds,
41
+ onUpdate,
42
+ lifecycleState,
43
+ }) => {
44
+ const { theme } = useTheme();
45
+ const slideAnim = useRef(new Animated.Value(SCREEN_HEIGHT)).current;
46
+ const overlayAnim = useRef(new Animated.Value(0)).current;
47
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
48
+ const readOnly = !canManageEscrow(lifecycleState);
49
+
50
+ useEffect(() => {
51
+ const showSub = Keyboard.addListener(
52
+ Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow',
53
+ (e) => setKeyboardHeight(e.endCoordinates.height),
54
+ );
55
+ const hideSub = Keyboard.addListener(
56
+ Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide',
57
+ () => setKeyboardHeight(0),
58
+ );
59
+ return () => {
60
+ showSub.remove();
61
+ hideSub.remove();
62
+ };
63
+ }, []);
64
+
65
+ useEffect(() => {
66
+ if (visible) {
67
+ Animated.parallel([
68
+ Animated.timing(slideAnim, {
69
+ toValue: 0,
70
+ duration: 300,
71
+ useNativeDriver: true,
72
+ }),
73
+ Animated.timing(overlayAnim, {
74
+ toValue: 1,
75
+ duration: 300,
76
+ useNativeDriver: true,
77
+ }),
78
+ ]).start();
79
+ } else {
80
+ Keyboard.dismiss();
81
+ Animated.parallel([
82
+ Animated.timing(slideAnim, {
83
+ toValue: SCREEN_HEIGHT,
84
+ duration: 250,
85
+ useNativeDriver: true,
86
+ }),
87
+ Animated.timing(overlayAnim, {
88
+ toValue: 0,
89
+ duration: 250,
90
+ useNativeDriver: true,
91
+ }),
92
+ ]).start();
93
+ }
94
+ }, [visible]);
95
+
96
+ const handleDismiss = () => {
97
+ Keyboard.dismiss();
98
+ onDismiss();
99
+ };
100
+
101
+ if (!visible) return null;
102
+
103
+ return (
104
+ <View variant="transparent" style={StyleSheet.absoluteFill}>
105
+ {/* Overlay — tapping dismisses keyboard + modal */}
106
+ <TouchableWithoutFeedback onPress={handleDismiss}>
107
+ <Animated.View style={[styles.overlay, { opacity: overlayAnim }]} />
108
+ </TouchableWithoutFeedback>
109
+
110
+ {/* Sheet */}
111
+ <Animated.View
112
+ style={[
113
+ styles.sheet,
114
+ {
115
+ backgroundColor: theme.colors.surface.base,
116
+ transform: [{ translateY: slideAnim }],
117
+ maxHeight: SCREEN_HEIGHT * 0.85,
118
+ paddingBottom: Math.max(40, keyboardHeight),
119
+ },
120
+ ]}
121
+ >
122
+ {/* Handle — tapping dismisses keyboard */}
123
+ <TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
124
+ <View variant="transparent" style={styles.handleArea}>
125
+ <View variant="transparent" style={[styles.handle, { backgroundColor: theme.colors.border.default }]} />
126
+ <Text variant="body" bold style={styles.title}>
127
+ {readOnly ? 'Escrow Balance' : 'Manage Escrow'}
128
+ </Text>
129
+ </View>
130
+ </TouchableWithoutFeedback>
131
+
132
+ <ScrollView
133
+ keyboardShouldPersistTaps="handled"
134
+ showsVerticalScrollIndicator={false}
135
+ bounces={false}
136
+ >
137
+ <TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
138
+ <View variant="transparent">
139
+ <CalcuttaEscrow
140
+ calcutta_competition_id={calcutta_competition_id}
141
+ escrow={escrow}
142
+ market_type={market_type}
143
+ max_escrow={max_escrow}
144
+ player_balance={player_balance}
145
+ onDepositFunds={onDepositFunds}
146
+ onUpdate={onUpdate}
147
+ readOnly={readOnly}
148
+ />
149
+ </View>
150
+ </TouchableWithoutFeedback>
151
+ </ScrollView>
152
+ </Animated.View>
153
+ </View>
154
+ );
155
+ };
156
+
157
+ const styles = StyleSheet.create({
158
+ overlay: {
159
+ ...StyleSheet.absoluteFillObject,
160
+ backgroundColor: 'rgba(0,0,0,0.4)',
161
+ },
162
+ sheet: {
163
+ position: 'absolute',
164
+ bottom: 0,
165
+ left: 0,
166
+ right: 0,
167
+ borderTopLeftRadius: 16,
168
+ borderTopRightRadius: 16,
169
+ },
170
+ handleArea: {
171
+ alignItems: 'center',
172
+ paddingTop: 10,
173
+ paddingBottom: 4,
174
+ },
175
+ handle: {
176
+ width: 36,
177
+ height: 4,
178
+ borderRadius: 2,
179
+ },
180
+ title: {
181
+ textAlign: 'center',
182
+ marginTop: 8,
183
+ marginBottom: 4,
184
+ },
185
+ });