This commit is contained in:
Abin Toppo
2025-09-26 18:14:07 +05:30
parent 906cec7d66
commit 7f130c0b8e
7 changed files with 282 additions and 225 deletions
+6 -14
View File
@@ -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"
+1
View File
@@ -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>();
+2 -2
View File
@@ -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,
+2 -1
View File
@@ -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}
+33 -7
View File
@@ -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>
); );
}; };
+237 -200
View File
@@ -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;
+2 -2
View File
@@ -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) {