colors
This commit is contained in:
Generated
+6
-14
@@ -4858,6 +4858,7 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
@@ -8223,6 +8224,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
|
"devOptional": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/istanbul-lib-coverage": {
|
"node_modules/istanbul-lib-coverage": {
|
||||||
@@ -8973,6 +8975,7 @@
|
|||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||||
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/json-schema-traverse": {
|
"node_modules/json-schema-traverse": {
|
||||||
@@ -9856,6 +9859,7 @@
|
|||||||
"version": "9.0.5",
|
"version": "9.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^2.0.1"
|
"brace-expansion": "^2.0.1"
|
||||||
@@ -13885,7 +13889,6 @@
|
|||||||
"version": "6.4.0",
|
"version": "6.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz",
|
||||||
"integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==",
|
"integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerate": "^1.4.2",
|
"regenerate": "^1.4.2",
|
||||||
@@ -13909,7 +13912,6 @@
|
|||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz",
|
||||||
"integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==",
|
"integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jsesc": "~3.1.0"
|
"jsesc": "~3.1.0"
|
||||||
@@ -13918,18 +13920,6 @@
|
|||||||
"regjsparser": "bin/parser"
|
"regjsparser": "bin/parser"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/regjsparser/node_modules/jsesc": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"jsesc": "bin/jsesc"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/require-directory": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
@@ -14951,6 +14941,7 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/throat": {
|
"node_modules/throat": {
|
||||||
@@ -15405,6 +15396,7 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
|
"devOptional": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isexe": "^2.0.0"
|
"isexe": "^2.0.0"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export type RootStackParamList = {
|
|||||||
Invite: { groupId: string | number; code: string | number };
|
Invite: { groupId: string | number; code: string | number };
|
||||||
ManagementTabs:{ groupId: string | number;}
|
ManagementTabs:{ groupId: string | number;}
|
||||||
TransactionList:undefined
|
TransactionList:undefined
|
||||||
|
RandomSelectUserScreen: { groupId: string | number, cycleNumber: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<RootStackParamList>();
|
const Stack = createNativeStackNavigator<RootStackParamList>();
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ const Home: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<AppView barStyle="dark-content" style={styles.container}>
|
<AppView barStyle="dark-content" style={styles.container}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.greeting}>Hello, {user.firstName || apiUser?.firstName} {user.lastName || apiUser?.lastName}👋</Text>
|
<Text style={styles.greeting}>Hello, {user.firstName || apiUser?.firstName} {user.lastName || apiUser?.lastName}</Text>
|
||||||
<TouchableOpacity onPress={() => navigation.navigate("Profile" as never)}>
|
<TouchableOpacity onPress={() => navigation.navigate("Profile" as never)}>
|
||||||
<Image
|
<Image
|
||||||
source={
|
source={
|
||||||
@@ -270,7 +270,7 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: "space-around",
|
justifyContent: "space-around",
|
||||||
},
|
},
|
||||||
greeting: { fontSize: 22, fontWeight: "600", color: colors.Home },
|
greeting: { fontSize: 22, fontWeight: "600", color: colors.pastelPink },
|
||||||
gridButton: {
|
gridButton: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
margin: 8,
|
margin: 8,
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ const UserProfileStatus: React.FC = () => {
|
|||||||
{/* <Text style={styles.title}>Welcome Back 👋</Text> */}
|
{/* <Text style={styles.title}>Welcome Back 👋</Text> */}
|
||||||
|
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[colors.primary, colors.Home]}
|
colors={[colors.pastelPink, colors.pastelPink
|
||||||
|
]}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 1, y: 1 }}
|
||||||
style={styles.card}
|
style={styles.card}
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ import AppView from '../../components/AppView';
|
|||||||
import { Picker } from '@react-native-picker/picker';
|
import { Picker } from '@react-native-picker/picker';
|
||||||
import colors from '../../components/colors';
|
import colors from '../../components/colors';
|
||||||
import Clipboard from '@react-native-clipboard/clipboard';
|
import Clipboard from '@react-native-clipboard/clipboard';
|
||||||
import { CompositeScreenProps } from '@react-navigation/native';
|
import { CompositeScreenProps, useNavigation } from '@react-navigation/native';
|
||||||
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
|
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
|
||||||
import { ManagementTabParamList } from './ManagementTabNavigation'; // Adjust path
|
import { ManagementTabParamList } from './ManagementTabNavigation'; // Adjust path
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
|
|
||||||
|
|
||||||
// --- Type Definitions ---
|
// --- Type Definitions ---
|
||||||
@@ -79,11 +80,11 @@ type EditGroupScreenProps = CompositeScreenProps<
|
|||||||
|
|
||||||
type EditingCard = 'groupInfo' | 'financialRules' | 'configuration' | null;
|
type EditingCard = 'groupInfo' | 'financialRules' | 'configuration' | null;
|
||||||
|
|
||||||
|
type GroupManagementNavProp = StackNavigationProp<RootStackParamList, 'EditGroup'>;
|
||||||
// --- Main Screen Component ---
|
// --- Main Screen Component ---
|
||||||
const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||||
const { groupId } = route.params;
|
const { groupId } = route.params;
|
||||||
|
const navigation = useNavigation<GroupManagementNavProp>();
|
||||||
|
|
||||||
const [editingCard, setEditingCard] = useState<EditingCard>(null);
|
const [editingCard, setEditingCard] = useState<EditingCard>(null);
|
||||||
const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null);
|
const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null);
|
||||||
@@ -105,7 +106,8 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
|||||||
const [starting, setStarting] = useState(false);
|
const [starting, setStarting] = useState(false);
|
||||||
const [inviting, setInviting] = useState(false);
|
const [inviting, setInviting] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [paymentStatus, setPaymentStatus] = useState<boolean | null>(null);
|
||||||
|
const [currentCycle, setCurrentCycle] = useState<number | null>(null);
|
||||||
|
|
||||||
const initializeEditableData = (data: GroupDetails) => {
|
const initializeEditableData = (data: GroupDetails) => {
|
||||||
setEditableData({
|
setEditableData({
|
||||||
@@ -141,10 +143,19 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [groupId]);
|
}, [groupId]);
|
||||||
|
const getPaymentStatus = async()=>{
|
||||||
|
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/payments/group/${groupId}/payment/paid`, 'GET') as { status: string; data?: { currentCycle: number ,status: boolean }; message?: string };
|
||||||
|
console.log("paymentStatus",response)
|
||||||
|
if (response.status === 'success') {
|
||||||
|
setPaymentStatus(response.data?.status || false);
|
||||||
|
setCurrentCycle(response.data?.currentCycle || null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchGroupDetails();
|
fetchGroupDetails();
|
||||||
|
getPaymentStatus();
|
||||||
}, [fetchGroupDetails]);
|
}, [fetchGroupDetails]);
|
||||||
|
|
||||||
|
|
||||||
@@ -177,7 +188,9 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
|||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const handleDistribute = async () => {
|
||||||
|
navigation.navigate('RandomSelectUserScreen', { groupId: groupDetails?.id || '', cycleNumber: currentCycle || 0 });
|
||||||
|
};
|
||||||
|
|
||||||
const handleStartGroup = async () => {
|
const handleStartGroup = async () => {
|
||||||
if (!groupDetails) return;
|
if (!groupDetails) return;
|
||||||
@@ -322,7 +335,6 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (loading) return <View style={styles.centered}><ActivityIndicator size="large" color="#4F46E5" /></View>;
|
if (loading) return <View style={styles.centered}><ActivityIndicator size="large" color="#4F46E5" /></View>;
|
||||||
if (error) return <View style={styles.centered}><Text style={styles.errorText}>{error}</Text></View>;
|
if (error) return <View style={styles.centered}><Text style={styles.errorText}>{error}</Text></View>;
|
||||||
if (!groupDetails) return null;
|
if (!groupDetails) return null;
|
||||||
@@ -332,7 +344,6 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
|||||||
const isInvitable = groupDetails.status === 'APPROVED';
|
const isInvitable = groupDetails.status === 'APPROVED';
|
||||||
const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants;
|
const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppView style={{ flex: 1, paddingHorizontal: 0 }}>
|
<AppView style={{ flex: 1, paddingHorizontal: 0 }}>
|
||||||
<View style={styles.headerContainer}>
|
<View style={styles.headerContainer}>
|
||||||
@@ -414,6 +425,21 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
{paymentStatus && (
|
||||||
|
<View style={styles.floatingButtonContainer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.activateButton}
|
||||||
|
onPress={handleDistribute}
|
||||||
|
disabled={starting}
|
||||||
|
>
|
||||||
|
{starting ? (
|
||||||
|
<ActivityIndicator color="#FFFFFF" size="small" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.activateButtonText}>Distribute</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</AppView>
|
</AppView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,248 +1,285 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, Animated, Easing, SafeAreaView, Dimensions, Alert } from 'react-native';
|
import { View, Text, StyleSheet, TouchableOpacity, FlatList, Dimensions, Alert, ActivityIndicator, Animated } from 'react-native';
|
||||||
import { Confetti, ConfettiMethods } from 'react-native-fast-confetti';
|
import { apiCall } from '../../utils/ApiHelper';
|
||||||
|
import { Ionicons } from "@react-native-vector-icons/ionicons";
|
||||||
|
|
||||||
import AppView from '../../components/AppView';
|
|
||||||
|
|
||||||
|
|
||||||
// --- PROPS & TYPE DEFINITIONS ---
|
|
||||||
export interface RandomWinnerScreenProps {
|
|
||||||
participants: Participant[];
|
|
||||||
excludedParticipantIds: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Participant {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- CONSTANTS ---
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
const ITEM_HEIGHT = 70; // Height of each name item
|
const NUM_COLUMNS = 3;
|
||||||
const ANIMATION_DURATION = 8000; // 8 seconds for suspense
|
const ITEM_SIZE = width / NUM_COLUMNS - 20;
|
||||||
const SHUFFLE_MULTIPLIER = 5; // Repeats the list for a longer scroll effect
|
|
||||||
|
|
||||||
const RandomSelectUserScreen: React.FC = () => {
|
const RandomSelectUserScreen: React.FC<{ route: any }> = ({ route }) => {
|
||||||
// --- MOCK DATA (Replace with navigation props) ---
|
const { groupId, cycleNumber } = route.params;
|
||||||
const mockProps: RandomWinnerScreenProps = {
|
const [users, setUsers] = useState<{ id: string; name: string }[]>([]);
|
||||||
participants: [
|
const [highlightedIndex, setHighlightedIndex] = useState<number | null>(null);
|
||||||
{ id: '1', name: 'Vinay M.' },
|
const [winner, setWinner] = useState<{ id: string; name: string } | null>(null);
|
||||||
{ id: '2', name: 'Nitesh Y.' },
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
{ id: '3', name: 'Alice' },
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
{ id: '4', name: 'Bob' },
|
|
||||||
{ id: '5', name: 'Charlie' },
|
|
||||||
{ id: '6', name: 'David' },
|
|
||||||
{ id: '7', name: 'Eve' },
|
|
||||||
],
|
|
||||||
excludedParticipantIds: ['4'], // Exclude Bob
|
|
||||||
};
|
|
||||||
// --- END MOCK DATA ---
|
|
||||||
|
|
||||||
const { participants, excludedParticipantIds } = mockProps;
|
const scaleAnim = useRef(new Animated.Value(1)).current;
|
||||||
|
|
||||||
const [eligibleParticipants, setEligibleParticipants] = useState<Participant[]>([]);
|
|
||||||
const [isSelecting, setIsSelecting] = useState(false);
|
|
||||||
const [winner, setWinner] = useState<Participant | null>(null);
|
|
||||||
const [animationList, setAnimationList] = useState<Participant[]>([]);
|
|
||||||
|
|
||||||
const scrollY = useRef(new Animated.Value(0)).current;
|
|
||||||
const confettiRef = useRef<ConfettiMethods>(null);
|
|
||||||
|
|
||||||
// Filter participants
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const eligible = participants.filter(p => !excludedParticipantIds.includes(p.id));
|
const fetchParticipants = async () => {
|
||||||
setEligibleParticipants(eligible);
|
try {
|
||||||
}, [participants, excludedParticipantIds]);
|
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/all/member?groupId=${groupId}`, "GET") as any;
|
||||||
|
if (response.status === 'success' && response.data) {
|
||||||
|
const formattedUsers = response.data.map((u: any) => ({
|
||||||
|
id: u.id,
|
||||||
|
name: `${u.firstName} ${u.lastName}`,
|
||||||
|
}));
|
||||||
|
setUsers(formattedUsers);
|
||||||
|
} else {
|
||||||
|
Alert.alert('Error', 'Failed to fetch participants.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Error', 'Could not connect to the server to fetch participants.');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchParticipants();
|
||||||
|
}, [groupId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (highlightedIndex !== null) {
|
||||||
|
Animated.sequence([
|
||||||
|
Animated.timing(scaleAnim, { toValue: 1.15, duration: 80, useNativeDriver: true }),
|
||||||
|
Animated.timing(scaleAnim, { toValue: 1, duration: 80, useNativeDriver: true }),
|
||||||
|
]).start();
|
||||||
|
}
|
||||||
|
}, [highlightedIndex]);
|
||||||
|
|
||||||
const handleStartSelection = () => {
|
const handleStartSelection = () => {
|
||||||
if (eligibleParticipants.length < 2) {
|
if (users.length === 0 || isAnimating) return;
|
||||||
Alert.alert('Not enough eligible participants.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSelecting(true);
|
|
||||||
setWinner(null);
|
setWinner(null);
|
||||||
scrollY.setValue(0);
|
setIsAnimating(true);
|
||||||
|
|
||||||
// 1. Secretly select winner
|
let highlightCount = 0;
|
||||||
const winnerIndex = Math.floor(Math.random() * eligibleParticipants.length);
|
const maxHighlights = 30 + Math.floor(Math.random() * 10); // Add some randomness
|
||||||
const selectedWinner = eligibleParticipants[winnerIndex];
|
|
||||||
|
|
||||||
// 2. Create long, shuffled list for animation
|
const highlightInterval = setInterval(() => {
|
||||||
let listForAnimation: Participant[] = [];
|
const randomIndex = Math.floor(Math.random() * users.length);
|
||||||
for (let i = 0; i < SHUFFLE_MULTIPLIER; i++) {
|
setHighlightedIndex(randomIndex);
|
||||||
const shuffled = [...eligibleParticipants].sort(() => Math.random() - 0.5);
|
highlightCount++;
|
||||||
listForAnimation.push(...shuffled);
|
|
||||||
|
if (highlightCount >= maxHighlights) {
|
||||||
|
clearInterval(highlightInterval);
|
||||||
|
fetchWinner();
|
||||||
}
|
}
|
||||||
|
}, 120);
|
||||||
// 3. Place winner at the end
|
|
||||||
listForAnimation.push(selectedWinner);
|
|
||||||
setAnimationList(listForAnimation);
|
|
||||||
|
|
||||||
// 4. Calculate final scroll position
|
|
||||||
const finalY = -(listForAnimation.length - 1) * ITEM_HEIGHT;
|
|
||||||
|
|
||||||
// 5. Start animation
|
|
||||||
Animated.timing(scrollY, {
|
|
||||||
toValue: finalY,
|
|
||||||
duration: ANIMATION_DURATION,
|
|
||||||
useNativeDriver: true,
|
|
||||||
easing: Easing.bezier(0.3, 0, 0.1, 1),
|
|
||||||
}).start(() => {
|
|
||||||
setWinner(selectedWinner);
|
|
||||||
confettiRef.current?.start();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const fetchWinner = async () => {
|
||||||
setIsSelecting(false);
|
const payload = { groupId: groupId };
|
||||||
setWinner(null);
|
try {
|
||||||
scrollY.setValue(0);
|
const response = await apiCall('https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/method/based/userSelect', 'POST', payload) as any;
|
||||||
confettiRef.current?.stop();
|
|
||||||
|
if (response.status === 'success' && response.data?.id) {
|
||||||
|
const winnerData = { id: response.data.id, name: `${response.data.firstName} ${response.data.lastName}` };
|
||||||
|
const winnerIndex = users.findIndex(u => u.id === winnerData.id);
|
||||||
|
|
||||||
|
if (winnerIndex !== -1) {
|
||||||
|
setWinner(winnerData);
|
||||||
|
setHighlightedIndex(winnerIndex);
|
||||||
|
} else {
|
||||||
|
Alert.alert('Error', 'Winner not found among participants.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Alert.alert('Error', 'Failed to determine a winner from the API.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Error', 'Could not connect to the server to fetch the winner.');
|
||||||
|
} finally {
|
||||||
|
setIsAnimating(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderListItem = ({ item, index }: { item: Participant, index: number }) => (
|
const renderItem = ({ item, index }: { item: { id: string; name: string }, index: number }) => {
|
||||||
<View style={styles.itemContainer} key={`${item.id}-${index}`}>
|
const isHighlighted = index === highlightedIndex;
|
||||||
<Text style={styles.itemName}>{item.name}</Text>
|
const isWinner = winner?.id === item.id && !isAnimating;
|
||||||
</View>
|
|
||||||
);
|
const animatedStyle = {
|
||||||
|
transform: [{ scale: isHighlighted ? scaleAnim : 1 }],
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppView>
|
<Animated.View
|
||||||
<Confetti ref={confettiRef} />
|
key={item.id}
|
||||||
|
style={[
|
||||||
<Text style={styles.title}>Select a Winner</Text>
|
styles.gridItem,
|
||||||
|
isHighlighted && styles.highlightedItem,
|
||||||
<View style={styles.selectionMachine}>
|
isWinner && styles.winnerItem,
|
||||||
<View style={styles.winnerPointer} />
|
animatedStyle,
|
||||||
{isSelecting || winner ? (
|
]}
|
||||||
<Animated.View style={{ transform: [{ translateY: scrollY }] }}>
|
>
|
||||||
{animationList.map((item, index) => renderListItem({ item, index }))}
|
<Ionicons name="person" size={40} color={isHighlighted || isWinner ? '#000000ff' : '#4b5563'} />
|
||||||
|
<Text style={[styles.gridText, (isHighlighted || isWinner) && styles.highlightedText]} numberOfLines={2}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
) : (
|
);
|
||||||
<View style={styles.placeholderContainer}>
|
};
|
||||||
<Text style={styles.placeholderText}>Press Start to Select</Text>
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<View style={styles.centeredContainer}>
|
||||||
|
<ActivityIndicator size="large" color="#4f46e5" />
|
||||||
|
<Text style={styles.loadingText}>Loading Participants...</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.title}>Select a Winner</Text>
|
||||||
|
<Text style={styles.subtitle}>Cycle #{cycleNumber}</Text>
|
||||||
|
</View>
|
||||||
|
<FlatList
|
||||||
|
data={users}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
numColumns={NUM_COLUMNS}
|
||||||
|
extraData={{ highlightedIndex, winner }}
|
||||||
|
contentContainerStyle={styles.grid}
|
||||||
|
/>
|
||||||
|
<View style={styles.footer}>
|
||||||
|
{winner && !isAnimating && (
|
||||||
|
<View style={styles.winnerTextContainer}>
|
||||||
|
<Text style={styles.winnerText}>🎉 Winner: {winner.name}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
<TouchableOpacity
|
||||||
|
style={[styles.button, isAnimating && styles.buttonDisabled]}
|
||||||
{winner && (
|
onPress={handleStartSelection}
|
||||||
<View style={styles.winnerBanner}>
|
disabled={isAnimating}
|
||||||
<Text style={styles.winnerText}>Winner is</Text>
|
>
|
||||||
<Text style={styles.winnerName}>{winner.name}!</Text>
|
<Text style={styles.buttonText}>{isAnimating ? 'Selecting...' : 'Start Selection'}</Text>
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isSelecting && !winner && (
|
|
||||||
<TouchableOpacity style={styles.startButton} onPress={handleStartSelection}>
|
|
||||||
<Text style={styles.startButtonText}>Start Selection</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
</View>
|
||||||
|
</View>
|
||||||
{winner && (
|
|
||||||
<TouchableOpacity style={styles.resetButton} onPress={handleReset}>
|
|
||||||
<Text style={styles.resetButtonText}>Select Again</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</AppView>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
centeredContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#f9fafb',
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
marginTop: 15,
|
||||||
|
fontSize: 18,
|
||||||
|
color: '#374151',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#1C1C1E',
|
backgroundColor: '#f9fafb',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
},
|
||||||
paddingVertical: 40,
|
header: {
|
||||||
|
paddingTop: 50,
|
||||||
|
paddingBottom: 20,
|
||||||
|
alignItems: 'center'
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: '#FFFFFF',
|
color: '#111827',
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
color: '#6b7280',
|
||||||
|
marginTop: 4
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
},
|
||||||
|
gridItem: {
|
||||||
|
margin: 8,
|
||||||
|
width: ITEM_SIZE,
|
||||||
|
height: ITEM_SIZE,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 12,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#e5e7eb',
|
||||||
|
padding: 8,
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 2,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 3.84,
|
||||||
|
elevation: 5,
|
||||||
|
},
|
||||||
|
highlightedItem: {
|
||||||
|
borderWidth: 5,
|
||||||
|
borderColor: '#4f46e5',
|
||||||
|
},
|
||||||
|
winnerItem: {
|
||||||
|
backgroundColor: '#10b981',
|
||||||
|
borderColor: '#059669',
|
||||||
|
},
|
||||||
|
gridText: {
|
||||||
|
marginTop: 8,
|
||||||
|
color: '#1f2937',
|
||||||
|
fontWeight: '600',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
marginBottom: 30,
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
selectionMachine: {
|
highlightedText: {
|
||||||
height: ITEM_HEIGHT,
|
color: '#000000ff',
|
||||||
width: '90%',
|
|
||||||
backgroundColor: '#2C2C2E',
|
|
||||||
borderRadius: 20,
|
|
||||||
overflow: 'hidden',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
},
|
||||||
winnerPointer: {
|
footer: {
|
||||||
position: 'absolute',
|
width: '100%',
|
||||||
width: '105%',
|
|
||||||
height: ITEM_HEIGHT,
|
|
||||||
borderRadius: 20,
|
|
||||||
borderWidth: 3,
|
|
||||||
borderColor: '#FBC02D',
|
|
||||||
alignSelf: 'center',
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
itemContainer: {
|
|
||||||
height: ITEM_HEIGHT,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
itemName: {
|
|
||||||
fontSize: 36,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: '#FFF',
|
|
||||||
},
|
|
||||||
placeholderContainer: {
|
|
||||||
height: ITEM_HEIGHT,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
placeholderText: {
|
|
||||||
fontSize: 24,
|
|
||||||
color: '#666',
|
|
||||||
fontStyle: 'italic',
|
|
||||||
},
|
|
||||||
winnerBanner: {
|
|
||||||
marginTop: 30,
|
|
||||||
padding: 20,
|
padding: 20,
|
||||||
backgroundColor: '#FBC02D',
|
borderTopWidth: 1,
|
||||||
borderRadius: 15,
|
borderTopColor: '#e5e7eb',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#fff'
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
paddingVertical: 16,
|
||||||
|
paddingHorizontal: 50,
|
||||||
|
backgroundColor: '#4f46e5',
|
||||||
|
borderRadius: 30,
|
||||||
|
elevation: 3,
|
||||||
|
shadowColor: "#4f46e5",
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 4,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 4.65,
|
||||||
|
},
|
||||||
|
buttonDisabled: {
|
||||||
|
backgroundColor: '#9ca3af',
|
||||||
|
shadowColor: 'transparent',
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
fontSize: 18,
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
winnerTextContainer: {
|
||||||
|
marginBottom: 20,
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: '#d1fae5',
|
||||||
|
borderRadius: 12,
|
||||||
|
width: '100%',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
winnerText: {
|
winnerText: {
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
color: '#000',
|
|
||||||
},
|
|
||||||
winnerName: {
|
|
||||||
fontSize: 40,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: '#000',
|
|
||||||
},
|
|
||||||
startButton: {
|
|
||||||
backgroundColor: '#007AFF',
|
|
||||||
paddingVertical: 18,
|
|
||||||
paddingHorizontal: 40,
|
|
||||||
borderRadius: 30,
|
|
||||||
marginTop: 40,
|
|
||||||
},
|
|
||||||
startButtonText: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
resetButton: {
|
|
||||||
backgroundColor: '#333',
|
|
||||||
paddingVertical: 18,
|
|
||||||
paddingHorizontal: 40,
|
|
||||||
borderRadius: 30,
|
|
||||||
marginTop: 40,
|
|
||||||
},
|
|
||||||
resetButtonText: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
|
color: '#065f46',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default RandomSelectUserScreen;
|
export default RandomSelectUserScreen;
|
||||||
|
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ export async function apiCall<T>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Response Logging ---
|
// --- Response Logging ---
|
||||||
console.log(`--- API Response: [${response.status}] ${url} ---`);
|
console.log(`--- API Response: [${response}] ${url} ---`);
|
||||||
// Uncomment the line below to see the full response data in logs
|
// Uncomment the line below to see the full response data in logs
|
||||||
// console.log("Response Data:", JSON.stringify(response.data, null, 2));
|
// console.log("Response Data:", JSON.stringify(response.data, null, 2));
|
||||||
console.log("---------------------------------------");
|
console.log("---------------------------------------",response);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
Reference in New Issue
Block a user