floating-games-sdk 1.0.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 +44 -0
- package/src/FloatingGames.tsx +25 -0
- package/src/assets/fonts/Creepster-Regular.ttf +0 -0
- package/src/assets/images/android-icon-background.png +0 -0
- package/src/assets/images/android-icon-foreground.png +0 -0
- package/src/assets/images/android-icon-monochrome.png +0 -0
- package/src/assets/images/blocks-icon.png +0 -0
- package/src/assets/images/bright-rubber-puzzles-blue.png +0 -0
- package/src/assets/images/close.png +0 -0
- package/src/assets/images/eriri.png +0 -0
- package/src/assets/images/favicon.png +0 -0
- package/src/assets/images/first-play.png +0 -0
- package/src/assets/images/game-over.gif +0 -0
- package/src/assets/images/game-pad.png +0 -0
- package/src/assets/images/game-pad2.png +0 -0
- package/src/assets/images/game-page-bg1.png +0 -0
- package/src/assets/images/hands-down-bg.png +0 -0
- package/src/assets/images/icon.png +0 -0
- package/src/assets/images/lets-play.gif +0 -0
- package/src/assets/images/lucky-day.gif +0 -0
- package/src/assets/images/maths.png +0 -0
- package/src/assets/images/not-playing.png +0 -0
- package/src/assets/images/number-puzzle-select.png +0 -0
- package/src/assets/images/number-puzzle-selector.png +0 -0
- package/src/assets/images/pause-clock.png +0 -0
- package/src/assets/images/pause-icon.png +0 -0
- package/src/assets/images/play-icon.png +0 -0
- package/src/assets/images/play-now.gif +0 -0
- package/src/assets/images/play-play.png +0 -0
- package/src/assets/images/play.png +0 -0
- package/src/assets/images/quit-game.png +0 -0
- package/src/assets/images/quit-icon.png +0 -0
- package/src/assets/images/quit.png +0 -0
- package/src/assets/images/review-win-selector.png +0 -0
- package/src/assets/images/select-bg.png +0 -0
- package/src/assets/images/select-logo.png +0 -0
- package/src/assets/images/spin-wheel-select.png +0 -0
- package/src/assets/images/spin-wheel-selector.png +0 -0
- package/src/assets/images/splash-icon.png +0 -0
- package/src/assets/images/start-button.png +0 -0
- package/src/assets/images/start-game.png +0 -0
- package/src/assets/images/stop-quit.png +0 -0
- package/src/assets/images/waiting-for-you.gif +0 -0
- package/src/assets/sounds/selector-music.mp3 +0 -0
- package/src/assets/sounds/timeup-sound.wav +0 -0
- package/src/components/atoms/Amount.tsx +48 -0
- package/src/components/atoms/FloatingMenu.tsx +119 -0
- package/src/components/atoms/ShimmerHR.tsx +64 -0
- package/src/components/atoms/UseAnimatedCounter.tsx +31 -0
- package/src/components/games/Chain.tsx +26 -0
- package/src/components/games/GameItem.tsx +38 -0
- package/src/components/games/GameSelectorScreen.tsx +221 -0
- package/src/components/games/GameSplash.tsx +44 -0
- package/src/components/games/HangingSign.tsx +75 -0
- package/src/components/games/SplashScreen.tsx +69 -0
- package/src/components/games/number-puzzle/DragTile.tsx +65 -0
- package/src/components/games/number-puzzle/GameScreen.tsx +597 -0
- package/src/components/games/number-puzzle/PuzzleSplash.tsx +50 -0
- package/src/components/games/number-puzzle/TileComponent.tsx +47 -0
- package/src/components/games/review-and-win/EmojiTracker.tsx +96 -0
- package/src/components/games/review-and-win/GameScreen.tsx +135 -0
- package/src/components/games/review-and-win/Tile.tsx +60 -0
- package/src/components/games/spin-wheel/SpinAndWin.tsx +507 -0
- package/src/components/payment/CheckoutScreen.tsx +50 -0
- package/src/components/payment/FundAccount.tsx +281 -0
- package/src/components/payment/Payment.tsx +45 -0
- package/src/components/reusables/AppSelect.tsx +70 -0
- package/src/components/reusables/CountdownTimer.tsx +116 -0
- package/src/components/reusables/FloatingButton.tsx +71 -0
- package/src/components/reusables/SoundPlayer.ts +28 -0
- package/src/components/utils/shuffle.ts +48 -0
- package/src/context/GameContext.tsx +15 -0
- package/src/index.tsx +5 -0
- package/src/modals/BottomSheetModal.tsx +38 -0
- package/src/modals/GameOverModal.tsx +76 -0
- package/src/modals/PauseModal.tsx +95 -0
- package/src/navigation/GameNavigator.tsx +48 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
ScrollView,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Text,
|
|
7
|
+
TextInput,
|
|
8
|
+
TouchableOpacity,
|
|
9
|
+
View,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
|
|
12
|
+
import Animated, {
|
|
13
|
+
FadeInDown,
|
|
14
|
+
useAnimatedStyle,
|
|
15
|
+
useSharedValue,
|
|
16
|
+
withSpring,
|
|
17
|
+
withTiming,
|
|
18
|
+
} from 'react-native-reanimated';
|
|
19
|
+
|
|
20
|
+
import { Amount } from '../atoms/Amount';
|
|
21
|
+
|
|
22
|
+
const quickAmounts = [1000, 2000, 3000, 4000, 5000, 6000, 7000, 10000];
|
|
23
|
+
|
|
24
|
+
export default function FundAccount({ navigation }: any) {
|
|
25
|
+
const [amount, setAmount] = useState('');
|
|
26
|
+
|
|
27
|
+
const balance = 1267;
|
|
28
|
+
|
|
29
|
+
const isMobileWeb =
|
|
30
|
+
Platform.OS === 'web' &&
|
|
31
|
+
!/Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
32
|
+
|
|
33
|
+
// ✅ Reanimated shared values
|
|
34
|
+
const button1Width = useSharedValue(1); // 1 = 100%, 0.48 = 48%
|
|
35
|
+
const button2Opacity = useSharedValue(0);
|
|
36
|
+
const button2Scale = useSharedValue(0.8);
|
|
37
|
+
|
|
38
|
+
// ✅ Animate based on amount
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (amount) {
|
|
41
|
+
button1Width.value = withTiming(0.48, { duration: 300 });
|
|
42
|
+
button2Opacity.value = withTiming(1, { duration: 250 });
|
|
43
|
+
button2Scale.value = withSpring(1);
|
|
44
|
+
} else {
|
|
45
|
+
button1Width.value = withTiming(1, { duration: 300 });
|
|
46
|
+
button2Opacity.value = withTiming(0, { duration: 200 });
|
|
47
|
+
button2Scale.value = withTiming(0.8);
|
|
48
|
+
}
|
|
49
|
+
}, [amount]);
|
|
50
|
+
|
|
51
|
+
// ✅ Animated styles
|
|
52
|
+
const button1Style = useAnimatedStyle(() => ({
|
|
53
|
+
width: `${button1Width.value * 100}%`,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
const button2Style = useAnimatedStyle(() => ({
|
|
57
|
+
opacity: button2Opacity.value,
|
|
58
|
+
transform: [{ scale: button2Scale.value }],
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
const formatNumber = (value: string) => {
|
|
62
|
+
const cleaned = value.replace(/[^0-9]/g, '');
|
|
63
|
+
if (!cleaned) return '';
|
|
64
|
+
return Number(cleaned).toLocaleString();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const clearAmount = () => setAmount('');
|
|
68
|
+
|
|
69
|
+
const handlePressIn = () => {};
|
|
70
|
+
const handlePressOut = () => {
|
|
71
|
+
navigation.replace('GameSelector');
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<ScrollView
|
|
76
|
+
showsVerticalScrollIndicator={ false }
|
|
77
|
+
style={ [
|
|
78
|
+
styles.container,
|
|
79
|
+
{ paddingHorizontal: isMobileWeb ? 200 : 20 },
|
|
80
|
+
] }
|
|
81
|
+
>
|
|
82
|
+
{ /* Title */ }
|
|
83
|
+
<Animated.Text entering={ FadeInDown.delay(100) } style={ styles.title }>
|
|
84
|
+
Fund Account
|
|
85
|
+
</Animated.Text>
|
|
86
|
+
|
|
87
|
+
{ /* Balance */ }
|
|
88
|
+
<Animated.View entering={ FadeInDown.delay(200) } style={ styles.balanceCard }>
|
|
89
|
+
<Text style={ styles.balanceLabel }>Current Balance</Text>
|
|
90
|
+
<Amount value={ balance } fontSize={ 28 } color="#fff" fontWeight="bold" />
|
|
91
|
+
</Animated.View>
|
|
92
|
+
|
|
93
|
+
{ /* Input */ }
|
|
94
|
+
<Animated.Text entering={ FadeInDown.delay(300) } style={ styles.label }>
|
|
95
|
+
Enter Amount
|
|
96
|
+
</Animated.Text>
|
|
97
|
+
|
|
98
|
+
<Animated.View entering={ FadeInDown.delay(350) }>
|
|
99
|
+
<View style={ { position: 'relative' } }>
|
|
100
|
+
<TextInput
|
|
101
|
+
style={ styles.input }
|
|
102
|
+
placeholder={ '0' }
|
|
103
|
+
placeholderTextColor={ '#666' }
|
|
104
|
+
keyboardType="number-pad"
|
|
105
|
+
value={ amount ? `₦${formatNumber(amount)}` : '' }
|
|
106
|
+
maxLength={ 10 }
|
|
107
|
+
onChangeText={ (text) => {
|
|
108
|
+
const cleaned = text.replace(/[^0-9]/g, '');
|
|
109
|
+
setAmount(cleaned);
|
|
110
|
+
} }
|
|
111
|
+
/>
|
|
112
|
+
|
|
113
|
+
{ amount ? (
|
|
114
|
+
<Text onPress={ clearAmount } style={ styles.clear }>
|
|
115
|
+
❌
|
|
116
|
+
</Text>
|
|
117
|
+
) : null }
|
|
118
|
+
</View>
|
|
119
|
+
</Animated.View>
|
|
120
|
+
|
|
121
|
+
{ /* Quick amounts */ }
|
|
122
|
+
<Animated.View
|
|
123
|
+
entering={ FadeInDown.delay(400) }
|
|
124
|
+
style={ styles.quickContainer }
|
|
125
|
+
>
|
|
126
|
+
{ quickAmounts.map((amt) => (
|
|
127
|
+
<TouchableOpacity
|
|
128
|
+
key={ amt }
|
|
129
|
+
style={ [
|
|
130
|
+
styles.quickBtn,
|
|
131
|
+
amount === String(amt) && styles.activeQuick,
|
|
132
|
+
] }
|
|
133
|
+
onPress={ () => setAmount(String(amt)) }
|
|
134
|
+
>
|
|
135
|
+
<Amount value={ amt } fontSize={ 15 } color="#fff" fontWeight="400" />
|
|
136
|
+
</TouchableOpacity>
|
|
137
|
+
)) }
|
|
138
|
+
</Animated.View>
|
|
139
|
+
|
|
140
|
+
{ /* Payment */ }
|
|
141
|
+
<Animated.Text entering={ FadeInDown.delay(500) } style={ styles.label }>
|
|
142
|
+
Payment Method
|
|
143
|
+
</Animated.Text>
|
|
144
|
+
|
|
145
|
+
<Animated.View entering={ FadeInDown.delay(550) } style={ styles.paymentCard }>
|
|
146
|
+
<Text style={ styles.paymentText }>💳 NetApps Pay</Text>
|
|
147
|
+
</Animated.View>
|
|
148
|
+
|
|
149
|
+
{ /* Buttons */ }
|
|
150
|
+
<Animated.View entering={ FadeInDown.delay(600) }>
|
|
151
|
+
<View style={ [styles.buttonDiv, { marginTop: 20 }] }>
|
|
152
|
+
{ /* Button 1 */ }
|
|
153
|
+
<Animated.View style={ button1Style }>
|
|
154
|
+
<TouchableOpacity
|
|
155
|
+
style={ [styles.button, styles.button1] }
|
|
156
|
+
onPressIn={ handlePressIn }
|
|
157
|
+
onPressOut={ handlePressOut }
|
|
158
|
+
>
|
|
159
|
+
<Text style={ styles.buttonText }>Continue to Game</Text>
|
|
160
|
+
</TouchableOpacity>
|
|
161
|
+
</Animated.View>
|
|
162
|
+
|
|
163
|
+
{ /* Button 2 */ }
|
|
164
|
+
{ amount ? (
|
|
165
|
+
<Animated.View
|
|
166
|
+
style={ [
|
|
167
|
+
styles.payWrapper,
|
|
168
|
+
button2Style,
|
|
169
|
+
] }
|
|
170
|
+
>
|
|
171
|
+
<TouchableOpacity style={ [styles.button, styles.button2, { flexDirection:'row' }] }>
|
|
172
|
+
<Text style={ [styles.buttonText, { marginEnd: 4 }] }>Pay</Text>
|
|
173
|
+
<Amount
|
|
174
|
+
value={ Number(amount) }
|
|
175
|
+
fontSize={ 15 }
|
|
176
|
+
color="#fff"
|
|
177
|
+
fontWeight="normal"
|
|
178
|
+
/>
|
|
179
|
+
</TouchableOpacity>
|
|
180
|
+
</Animated.View>
|
|
181
|
+
) : null }
|
|
182
|
+
</View>
|
|
183
|
+
</Animated.View>
|
|
184
|
+
|
|
185
|
+
</ScrollView>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const styles = StyleSheet.create({
|
|
189
|
+
container: {
|
|
190
|
+
flex: 1,
|
|
191
|
+
padding: 20,
|
|
192
|
+
backgroundColor: '#0f172a',
|
|
193
|
+
paddingVertical: 60,
|
|
194
|
+
},
|
|
195
|
+
title: {
|
|
196
|
+
fontSize: 26,
|
|
197
|
+
fontWeight: 'bold',
|
|
198
|
+
color: '#fff',
|
|
199
|
+
marginBottom: 20,
|
|
200
|
+
},
|
|
201
|
+
balanceCard: {
|
|
202
|
+
backgroundColor: '#1e293b',
|
|
203
|
+
padding: 20,
|
|
204
|
+
borderRadius: 6,
|
|
205
|
+
marginBottom: 25,
|
|
206
|
+
},
|
|
207
|
+
balanceLabel: {
|
|
208
|
+
color: '#94a3b8',
|
|
209
|
+
fontSize: 14,
|
|
210
|
+
},
|
|
211
|
+
label: {
|
|
212
|
+
color: '#cbd5f5',
|
|
213
|
+
marginBottom: 8,
|
|
214
|
+
fontSize: 14,
|
|
215
|
+
},
|
|
216
|
+
input: {
|
|
217
|
+
backgroundColor: '#1e293b',
|
|
218
|
+
padding: 15,
|
|
219
|
+
borderRadius: 6,
|
|
220
|
+
color: '#fff',
|
|
221
|
+
fontSize: 18,
|
|
222
|
+
marginBottom: 15,
|
|
223
|
+
},
|
|
224
|
+
clear: {
|
|
225
|
+
position: 'absolute',
|
|
226
|
+
right: 10,
|
|
227
|
+
top: 10,
|
|
228
|
+
padding: 8,
|
|
229
|
+
},
|
|
230
|
+
quickContainer: {
|
|
231
|
+
flexDirection: 'row',
|
|
232
|
+
flexWrap: 'wrap',
|
|
233
|
+
justifyContent: 'space-between',
|
|
234
|
+
marginBottom: 20
|
|
235
|
+
},
|
|
236
|
+
quickBtn: {
|
|
237
|
+
backgroundColor: '#334155',
|
|
238
|
+
paddingVertical: 10,
|
|
239
|
+
paddingHorizontal: 15,
|
|
240
|
+
borderRadius: 6,
|
|
241
|
+
marginVertical: 8,
|
|
242
|
+
fontWeight: '500'
|
|
243
|
+
},
|
|
244
|
+
activeQuick: {
|
|
245
|
+
backgroundColor: '#22c55e',
|
|
246
|
+
},
|
|
247
|
+
paymentCard: {
|
|
248
|
+
backgroundColor: '#1e293b',
|
|
249
|
+
padding: 15,
|
|
250
|
+
borderRadius: 6,
|
|
251
|
+
marginBottom: 30,
|
|
252
|
+
},
|
|
253
|
+
paymentText: {
|
|
254
|
+
color: '#fff',
|
|
255
|
+
fontSize: 16,
|
|
256
|
+
},
|
|
257
|
+
buttonDiv: {
|
|
258
|
+
flexDirection: 'row',
|
|
259
|
+
justifyContent: 'space-between',
|
|
260
|
+
alignItems: 'center',
|
|
261
|
+
},
|
|
262
|
+
button: {
|
|
263
|
+
padding: 16,
|
|
264
|
+
borderRadius: 6,
|
|
265
|
+
alignItems: 'center',
|
|
266
|
+
justifyContent: 'center',
|
|
267
|
+
},
|
|
268
|
+
button1: {
|
|
269
|
+
backgroundColor: '#4c68af',
|
|
270
|
+
},
|
|
271
|
+
button2: {
|
|
272
|
+
backgroundColor: '#22c55e',
|
|
273
|
+
},
|
|
274
|
+
payWrapper: {
|
|
275
|
+
width: '48%',
|
|
276
|
+
},
|
|
277
|
+
buttonText: {
|
|
278
|
+
color: '#fff',
|
|
279
|
+
fontSize: 15,
|
|
280
|
+
},
|
|
281
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// import type { PaymentConfig } from '@netappsng/react-native-pay';
|
|
2
|
+
// import { presentPayment } from '@netappsng/react-native-pay';
|
|
3
|
+
//
|
|
4
|
+
// function Payment() {
|
|
5
|
+
// const amountNaira = 100;
|
|
6
|
+
// const amountKobo = Math.round(amountNaira * 100);
|
|
7
|
+
//
|
|
8
|
+
// const config: PaymentConfig = {
|
|
9
|
+
// publicKey: 'pk_live_xxxxxxxxxxxxxxxxxxxx',
|
|
10
|
+
// amount: amountKobo, // ₦100.00 = 10000 kobo
|
|
11
|
+
// currency: 'NGN',
|
|
12
|
+
// email: 'john@example.com',
|
|
13
|
+
// fullName: 'John Doe',
|
|
14
|
+
// phoneNumber: '08106720418',
|
|
15
|
+
// narration: 'Dev Test Payment',
|
|
16
|
+
// paymentChannels: ['card', 'transfer', 'ussd', 'payattitude', 'moniflow'],
|
|
17
|
+
// defaultChannel: 'card',
|
|
18
|
+
// address1: 'Customer Address',
|
|
19
|
+
// metadata: { inputAmount: amountNaira, env: 'development' },
|
|
20
|
+
// businessName: 'Demo Store',
|
|
21
|
+
// showTransactionSummary: true,
|
|
22
|
+
// };
|
|
23
|
+
//
|
|
24
|
+
// const cleanup = presentPayment(config, {
|
|
25
|
+
// onSuccess: (payload) => {
|
|
26
|
+
// console.log('Payment successful!', payload.transactionRef);
|
|
27
|
+
// // payload: { status, merchantRef, transactionRef, amount, currency, channel, message, timestamp }
|
|
28
|
+
// },
|
|
29
|
+
// onFailed: (payload) => {
|
|
30
|
+
// console.log('Payment failed:', payload.message);
|
|
31
|
+
// // payload: { status, amount, currency, message, timestamp, errorCode? }
|
|
32
|
+
// },
|
|
33
|
+
// onCancel: () => {
|
|
34
|
+
// console.log('Payment cancelled');
|
|
35
|
+
// },
|
|
36
|
+
// onReady: () => {
|
|
37
|
+
// console.log('Payment UI ready');
|
|
38
|
+
// },
|
|
39
|
+
// });
|
|
40
|
+
//
|
|
41
|
+
// // Call cleanup() to remove event listeners when unmounting
|
|
42
|
+
// // e.g. in useEffect: return cleanup;
|
|
43
|
+
// }
|
|
44
|
+
//
|
|
45
|
+
// export default Payment
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import DropDownPicker from 'react-native-dropdown-picker';
|
|
3
|
+
|
|
4
|
+
export type Option = {
|
|
5
|
+
label: string;
|
|
6
|
+
value: string | number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
open: boolean;
|
|
11
|
+
value: string | number | null;
|
|
12
|
+
items: Option[];
|
|
13
|
+
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
14
|
+
setValue: React.Dispatch<React.SetStateAction<any>>;
|
|
15
|
+
setItems?: React.Dispatch<React.SetStateAction<Option[]>>;
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
fontSize?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const AppSelect = ({
|
|
21
|
+
open,
|
|
22
|
+
value,
|
|
23
|
+
items,
|
|
24
|
+
setOpen,
|
|
25
|
+
setValue,
|
|
26
|
+
setItems,
|
|
27
|
+
placeholder = 'Select option',
|
|
28
|
+
fontSize = 16,
|
|
29
|
+
}: Props) => {
|
|
30
|
+
return (
|
|
31
|
+
<DropDownPicker
|
|
32
|
+
open={open}
|
|
33
|
+
value={value}
|
|
34
|
+
items={items}
|
|
35
|
+
setOpen={setOpen}
|
|
36
|
+
setValue={setValue}
|
|
37
|
+
setItems={setItems}
|
|
38
|
+
placeholder={placeholder}
|
|
39
|
+
style={{
|
|
40
|
+
borderRadius: 25,
|
|
41
|
+
minHeight: 38,
|
|
42
|
+
width: '100%',
|
|
43
|
+
borderColor: '#72aaf1',
|
|
44
|
+
borderWidth: 3,
|
|
45
|
+
// borderWidth: 0,
|
|
46
|
+
backgroundColor: 'transparent'
|
|
47
|
+
}}
|
|
48
|
+
textStyle={{
|
|
49
|
+
fontSize,
|
|
50
|
+
fontWeight: '500',
|
|
51
|
+
textAlign: 'center',
|
|
52
|
+
color: '#666'
|
|
53
|
+
}}
|
|
54
|
+
listItemLabelStyle={{
|
|
55
|
+
fontSize,
|
|
56
|
+
textAlign: 'left',
|
|
57
|
+
fontWeight: '500',
|
|
58
|
+
color: '#666'
|
|
59
|
+
}}
|
|
60
|
+
dropDownContainerStyle={{
|
|
61
|
+
borderRadius: 8,
|
|
62
|
+
width: '100%',
|
|
63
|
+
borderWidth: 0,
|
|
64
|
+
marginBottom: 1,
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default AppSelect;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useEffect,
|
|
3
|
+
useState,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
forwardRef,
|
|
6
|
+
} from 'react';
|
|
7
|
+
import {View, Text, StyleSheet, Vibration, Platform} from 'react-native';
|
|
8
|
+
import * as Haptics from 'expo-haptics';
|
|
9
|
+
|
|
10
|
+
type CountdownTimerProps = {
|
|
11
|
+
initialSeconds: number;
|
|
12
|
+
onComplete: () => void;
|
|
13
|
+
color?: string;
|
|
14
|
+
warningThreshold?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type CountdownTimerRef = {
|
|
18
|
+
reset: () => void;
|
|
19
|
+
pause: () => void;
|
|
20
|
+
resume: () => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const CountdownTimer = forwardRef<CountdownTimerRef, CountdownTimerProps>(
|
|
24
|
+
(
|
|
25
|
+
{initialSeconds, onComplete, color = '#333', warningThreshold = 10},
|
|
26
|
+
ref
|
|
27
|
+
) => {
|
|
28
|
+
const [seconds, setSeconds] = useState(initialSeconds);
|
|
29
|
+
const [show, setShow] = useState(true);
|
|
30
|
+
const [paused, setPaused] = useState(false); // ✅ NEW
|
|
31
|
+
|
|
32
|
+
// ✅ Expose ALL methods
|
|
33
|
+
useImperativeHandle(ref, () => ({
|
|
34
|
+
reset: () => {
|
|
35
|
+
setSeconds(initialSeconds);
|
|
36
|
+
setPaused(false);
|
|
37
|
+
},
|
|
38
|
+
pause: () => setPaused(true),
|
|
39
|
+
resume: () => setPaused(false),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
// ✅ Countdown effect
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (paused) return; // 🚀 stop if paused
|
|
45
|
+
|
|
46
|
+
if (seconds <= 0) {
|
|
47
|
+
onComplete();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const interval = setInterval(() => {
|
|
52
|
+
setSeconds(prev => prev - 1);
|
|
53
|
+
}, 1000);
|
|
54
|
+
|
|
55
|
+
return () => clearInterval(interval);
|
|
56
|
+
}, [seconds, paused]);
|
|
57
|
+
|
|
58
|
+
// ✅ Blinking
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (seconds <= warningThreshold && seconds > 0 && !paused) {
|
|
61
|
+
const blinkInterval = setInterval(() => {
|
|
62
|
+
setShow(prev => !prev);
|
|
63
|
+
}, 500);
|
|
64
|
+
|
|
65
|
+
return () => clearInterval(blinkInterval);
|
|
66
|
+
} else {
|
|
67
|
+
setShow(true);
|
|
68
|
+
}
|
|
69
|
+
}, [seconds, paused]);
|
|
70
|
+
|
|
71
|
+
// ✅ Move vibration into effect (important fix)
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const vibrateSoft = async () => {
|
|
74
|
+
if (Platform.OS === 'ios') {
|
|
75
|
+
await Haptics.impactAsync(
|
|
76
|
+
Haptics.ImpactFeedbackStyle.Light
|
|
77
|
+
);
|
|
78
|
+
} else {
|
|
79
|
+
Vibration.vibrate(30);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (seconds <= warningThreshold && seconds > 0 && !paused) {
|
|
84
|
+
vibrateSoft();
|
|
85
|
+
}
|
|
86
|
+
}, [seconds, paused]);
|
|
87
|
+
|
|
88
|
+
const formatTime = (secs: number) => {
|
|
89
|
+
const minutes = Math.floor(secs / 60);
|
|
90
|
+
const remainderSeconds = secs % 60;
|
|
91
|
+
return `${minutes < 10 ? '0' : ''}${minutes}:${
|
|
92
|
+
remainderSeconds < 10 ? '0' : ''
|
|
93
|
+
}${remainderSeconds}`;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const textColor =
|
|
97
|
+
seconds <= warningThreshold ? '#f67373' : color;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<View style={styles.container}>
|
|
101
|
+
{show && (
|
|
102
|
+
<Text style={[styles.text, {color: textColor}]}>
|
|
103
|
+
{formatTime(seconds)}
|
|
104
|
+
</Text>
|
|
105
|
+
)}
|
|
106
|
+
</View>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
export default CountdownTimer;
|
|
112
|
+
|
|
113
|
+
const styles = StyleSheet.create({
|
|
114
|
+
container: {justifyContent: 'center', alignItems: 'center'},
|
|
115
|
+
text: {fontSize: 30, fontWeight: 'bold'},
|
|
116
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, {useEffect} from 'react';
|
|
2
|
+
import Animated, {Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming,} from 'react-native-reanimated';
|
|
3
|
+
|
|
4
|
+
export type FloatPreset = 'left' | 'right' | 'center' | 'normal';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
preset?: FloatPreset;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default function FloatingButton({children, preset = 'center'}: Props) {
|
|
12
|
+
const translateY = useSharedValue(0);
|
|
13
|
+
const translateX = useSharedValue(0);
|
|
14
|
+
const rotate = useSharedValue(0);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let x = 0;
|
|
18
|
+
let r = 0;
|
|
19
|
+
|
|
20
|
+
if (preset === 'left') {
|
|
21
|
+
x = -8;
|
|
22
|
+
r = 1;
|
|
23
|
+
} else if (preset === 'right') {
|
|
24
|
+
x = 18;
|
|
25
|
+
r = 1;
|
|
26
|
+
} else if (preset === 'normal') {
|
|
27
|
+
x = 11;
|
|
28
|
+
r = 0;
|
|
29
|
+
} else {
|
|
30
|
+
x = 34;
|
|
31
|
+
r = 1.5;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
translateY.value = withRepeat(
|
|
35
|
+
withTiming(-12, {
|
|
36
|
+
duration: 2300 + Math.random() * 600,
|
|
37
|
+
easing: Easing.inOut(Easing.ease),
|
|
38
|
+
}),
|
|
39
|
+
-1,
|
|
40
|
+
true
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
translateX.value = withRepeat(
|
|
44
|
+
withTiming(x, {
|
|
45
|
+
duration: 1700 + Math.random() * 500,
|
|
46
|
+
easing: Easing.inOut(Easing.ease),
|
|
47
|
+
}),
|
|
48
|
+
-1,
|
|
49
|
+
true
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
rotate.value = withRepeat(
|
|
53
|
+
withTiming(r, {
|
|
54
|
+
duration: 1500 + Math.random() * 500,
|
|
55
|
+
easing: Easing.inOut(Easing.ease),
|
|
56
|
+
}),
|
|
57
|
+
-1,
|
|
58
|
+
true
|
|
59
|
+
);
|
|
60
|
+
}, [preset]);
|
|
61
|
+
|
|
62
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
63
|
+
transform: [
|
|
64
|
+
{translateY: translateY.value},
|
|
65
|
+
{translateX: translateX.value},
|
|
66
|
+
{rotate: `${rotate.value}deg`},
|
|
67
|
+
],
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
return <Animated.View style={animatedStyle}>{children}</Animated.View>;
|
|
71
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useAudioPlayer } from 'expo-audio';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reusable hook to play a sound
|
|
5
|
+
* @param asset - require('../assets/sounds/file.wav') or mp3
|
|
6
|
+
* @param initialVolume - number between 0.0 and 1.0
|
|
7
|
+
*/
|
|
8
|
+
export function useSoundPlayer(asset: any, initialVolume: number = 0.1) {
|
|
9
|
+
const player = useAudioPlayer(asset);
|
|
10
|
+
|
|
11
|
+
// Set volume (clamp between 0 and 1)
|
|
12
|
+
player.volume = Math.max(0, Math.min(1, initialVolume));
|
|
13
|
+
|
|
14
|
+
const playSound = () => {
|
|
15
|
+
player.seekTo(0);
|
|
16
|
+
player.play();
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const stopSound = () => {
|
|
20
|
+
player.pause();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const setVolume = (volume: number) => {
|
|
24
|
+
player.volume = Math.max(0, Math.min(1, volume));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return { playSound, stopSound, setVolume };
|
|
28
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// utils/shuffle.ts
|
|
2
|
+
export type TileType = "😀" | "🔥" | "⭐";
|
|
3
|
+
export type TileData = {
|
|
4
|
+
id: number;
|
|
5
|
+
type: TileType;
|
|
6
|
+
opened: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const EMOJIS: TileType[] = ["😀","🔥","⭐"];
|
|
10
|
+
|
|
11
|
+
export const createBoard = (): TileData[] => {
|
|
12
|
+
|
|
13
|
+
const tiles: TileType[] = [];
|
|
14
|
+
let remaining = 16;
|
|
15
|
+
const counts: Record<TileType, number> = {"😀":0,"🔥":0,"⭐":0};
|
|
16
|
+
|
|
17
|
+
// Distribute emojis to fill 16 tiles, each emoji 2-6 times but total = 16
|
|
18
|
+
// Make sure at least 2 of each
|
|
19
|
+
let emojiCounts: Record<TileType, number> = {"😀":0,"🔥":0,"⭐":0};
|
|
20
|
+
|
|
21
|
+
// Step 1: assign minimum 2 to each
|
|
22
|
+
EMOJIS.forEach(emoji=>{
|
|
23
|
+
emojiCounts[emoji] = 2;
|
|
24
|
+
remaining -= 2;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Step 2: randomly distribute remaining 10 tiles
|
|
28
|
+
const allEmojis: TileType[] = [];
|
|
29
|
+
EMOJIS.forEach(e=>{ for(let i=0;i<emojiCounts[e];i++) allEmojis.push(e) });
|
|
30
|
+
|
|
31
|
+
while(remaining>0){
|
|
32
|
+
const randIndex = Math.floor(Math.random()*EMOJIS.length);
|
|
33
|
+
const emoji = EMOJIS[randIndex];
|
|
34
|
+
allEmojis.push(emoji);
|
|
35
|
+
emojiCounts[emoji] +=1;
|
|
36
|
+
remaining -=1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// shuffle
|
|
40
|
+
const shuffled = allEmojis.sort(()=> Math.random() - 0.5);
|
|
41
|
+
|
|
42
|
+
return shuffled.map((type,index)=>({
|
|
43
|
+
id:index,
|
|
44
|
+
type,
|
|
45
|
+
opened:false
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
type GameContextType = { name: string; balance: number };
|
|
4
|
+
|
|
5
|
+
const GameContext = createContext<GameContextType>({ name: '', balance: 0 });
|
|
6
|
+
|
|
7
|
+
export const GameProvider = ({
|
|
8
|
+
name, balance, children
|
|
9
|
+
}: GameContextType & { children: React.ReactNode }) => (
|
|
10
|
+
<GameContext.Provider value={{ name, balance }}>
|
|
11
|
+
{children}
|
|
12
|
+
</GameContext.Provider>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const useGameContext = () => useContext(GameContext);
|