create-gufran-expo-app 2.0.4 → 2.0.6
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/README.md +60 -321
- package/bin/cli.js +0 -1
- package/package.json +4 -3
- package/template/src/navigation/AuthStack.tsx +6 -25
- package/template/src/navigation/MainStack.tsx +0 -148
- package/template/src/navigation/RootNavigator.tsx +4 -26
- package/template/src/navigation/index.ts +0 -1
- package/template/src/navigation/navigationRef.ts +1 -1
- package/template/src/screens/HomeScreen.tsx +3 -215
- package/template/src/screens/auth/LoginScreen.tsx +13 -13
- package/template/src/screens/auth/index.ts +1 -6
- package/template/src/screens/index.ts +0 -35
- package/template/src/services/api.ts +5 -5
- package/template/src/services/authService.ts +3 -299
- package/template/src/services/mainServices.ts +19 -1914
- package/template/src/types/navigation.ts +5 -155
- package/template/src/utils/index.ts +5 -8
- package/template/src/navigation/MiddleStack.tsx +0 -35
- package/template/src/screens/auth/AddMamber.tsx +0 -428
- package/template/src/screens/auth/ForgotPasswordScreen.tsx +0 -176
- package/template/src/screens/auth/OTPVerifyScreen.tsx +0 -359
- package/template/src/screens/auth/RegisterScreen.tsx +0 -430
- package/template/src/screens/auth/SuccessScreen.tsx +0 -201
- package/template/src/screens/chat/ChatScreen.tsx +0 -1819
- package/template/src/screens/chat/ChatThreadsScreen.tsx +0 -360
- package/template/src/screens/chat/ReportMessageScreen.tsx +0 -238
- package/template/src/screens/clubs/Announcements.tsx +0 -426
- package/template/src/screens/clubs/BuyRaffleTicketsScreen.tsx +0 -568
- package/template/src/screens/clubs/ClubDeteils.tsx +0 -497
- package/template/src/screens/clubs/JoinClub.tsx +0 -841
- package/template/src/screens/events/EventScreen.tsx +0 -460
- package/template/src/screens/raffles/MyReferralMembersScreen.tsx +0 -758
- package/template/src/screens/raffles/RaffleDetailsScreen.tsx +0 -762
- package/template/src/screens/raffles/RafflesScreen.tsx +0 -495
- package/template/src/screens/raffles/SetRaffleReminderScreen.tsx +0 -390
- package/template/src/screens/teams/JoinTeamScreen.tsx +0 -464
- package/template/src/screens/teams/MyTeamDetailsScreen.tsx +0 -979
- package/template/src/screens/teams/MyTeamScreen.tsx +0 -568
- package/template/src/screens/teams/PendingRequestsScreen.tsx +0 -426
- package/template/src/screens/volunteerOpportunities/SetReminderScreen.tsx +0 -631
- package/template/src/screens/volunteerOpportunities/VolunteerOpportunitiesDetailsScreen.tsx +0 -1049
- package/template/src/screens/volunteerOpportunities/VolunteerOpportunitiesScreen.tsx +0 -608
- package/template/src/utils/ClubSearchManager.ts +0 -222
|
@@ -1,841 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
-
import { View, Text, StyleSheet, FlatList, StatusBar, Platform, TouchableOpacity, Image, RefreshControl, ActivityIndicator, Modal, Animated, Dimensions, PanResponder, Keyboard } from 'react-native';
|
|
3
|
-
import { JoinClubScreenProps } from '../../types/navigation';
|
|
4
|
-
import { theme } from '../../constants';
|
|
5
|
-
import { moderateScale } from '../../utils/scaling';
|
|
6
|
-
import { Fonts } from '../../constants/Fonts';
|
|
7
|
-
import SVG from '../../assets/icons';
|
|
8
|
-
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
9
|
-
import { useInfiniteClubSearch, useJoinClub } from '../../services/mainServices';
|
|
10
|
-
import { useQueryClient } from '@tanstack/react-query';
|
|
11
|
-
import { Button, ClubCard, TextInput } from '../../components/common';
|
|
12
|
-
import ToastManager from '../../components/common/ToastManager';
|
|
13
|
-
import { getApiErrorInfo, useMembers, useGetImageUrl } from '../../services';
|
|
14
|
-
import { useFocusEffect } from '@react-navigation/native';
|
|
15
|
-
const PAGE_SIZE = 10;
|
|
16
|
-
|
|
17
|
-
type MemberListItemProps = {
|
|
18
|
-
item: any;
|
|
19
|
-
isSelected: boolean;
|
|
20
|
-
onSelect: (member: any) => void;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const MemberListItem: React.FC<MemberListItemProps> = ({ item, isSelected, onSelect }) => {
|
|
24
|
-
const [memberImageError, setMemberImageError] = useState(false);
|
|
25
|
-
return (
|
|
26
|
-
<TouchableOpacity style={styles.memberListItem} onPress={() => onSelect(item)}>
|
|
27
|
-
{item?.profileImage && !memberImageError ? (
|
|
28
|
-
<Image
|
|
29
|
-
source={{ uri: item?.profileImage }}
|
|
30
|
-
onError={() => {
|
|
31
|
-
setMemberImageError(true);
|
|
32
|
-
}}
|
|
33
|
-
style={styles.profileImage}
|
|
34
|
-
/>
|
|
35
|
-
) : (
|
|
36
|
-
<SVG.emptyUser style={{ marginRight: moderateScale(10) }} width={moderateScale(50)} height={moderateScale(50)} />
|
|
37
|
-
)}
|
|
38
|
-
{/* {memberImageError && (
|
|
39
|
-
<SVG.emptyUser style={{ marginRight: moderateScale(10) }} width={moderateScale(50)} height={moderateScale(50)} />
|
|
40
|
-
)} */}
|
|
41
|
-
<View style={styles.memberNameContainer}>
|
|
42
|
-
<Text style={styles.memberName}>{item?.name}</Text>
|
|
43
|
-
{item?.isOwner && (
|
|
44
|
-
<Text style={styles.memberNameSubtitle}>Yourself</Text>
|
|
45
|
-
)}
|
|
46
|
-
</View>
|
|
47
|
-
<View>
|
|
48
|
-
{isSelected ? (
|
|
49
|
-
<SVG.checkRadio width={moderateScale(20)} height={moderateScale(20)} />
|
|
50
|
-
) : (
|
|
51
|
-
<SVG.uncheckRadio width={moderateScale(20)} height={moderateScale(20)} />
|
|
52
|
-
)}
|
|
53
|
-
</View>
|
|
54
|
-
</TouchableOpacity>
|
|
55
|
-
);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
export const JoinClubScreen: React.FC<JoinClubScreenProps> = ({ navigation }) => {
|
|
59
|
-
|
|
60
|
-
// Pagination state
|
|
61
|
-
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
62
|
-
const [membersModal, setMembersModal] = useState<boolean>(false);
|
|
63
|
-
const [selectedMember, setSelectedMember] = useState<any>(null);
|
|
64
|
-
// Bottom sheet animation values
|
|
65
|
-
const translateY = useRef(new Animated.Value(0)).current;
|
|
66
|
-
const screenHeight = Dimensions.get('window').height;
|
|
67
|
-
const bottomSheetHeight = screenHeight * 0.45; // 45% of screen height
|
|
68
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
69
|
-
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
|
|
70
|
-
const [joinClubDetails, setJoinClubDetails] = useState<any>(null);
|
|
71
|
-
const [joinClubImageUrls, setJoinClubImageUrls] = useState<Record<number, string>>({});
|
|
72
|
-
const [joinClubMemberImageUrls, setJoinClubMemberImageUrls] = useState<Record<number, string>>({});
|
|
73
|
-
const queryClient = useQueryClient();
|
|
74
|
-
const joinClubMutation = useJoinClub();
|
|
75
|
-
const getImageUrlMutation = useGetImageUrl();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const keyExtractor = (item: any, index: number) => {
|
|
81
|
-
// Create a more robust key that combines multiple attributes to ensure uniqueness
|
|
82
|
-
const id = item?.id?.toString() || '';
|
|
83
|
-
const name = item?.name || item?.clubName || '';
|
|
84
|
-
const clubCode = item?.clubCode || item?.uniqueCode || '';
|
|
85
|
-
const timestamp = item?.createdAt || item?.timestamp || '';
|
|
86
|
-
|
|
87
|
-
// Create a unique composite key using multiple attributes
|
|
88
|
-
const keyParts = [id, name, clubCode, timestamp].filter(part => part && part.toString().trim() !== '');
|
|
89
|
-
|
|
90
|
-
if (keyParts.length > 0) {
|
|
91
|
-
return `club-${keyParts.join('-')}-${index}`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Ultimate fallback with index to ensure uniqueness
|
|
95
|
-
return `club-unknown-${index}`;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const { data, fetchNextPage,
|
|
100
|
-
hasNextPage,
|
|
101
|
-
isFetchingNextPage,
|
|
102
|
-
isLoading,
|
|
103
|
-
isError,
|
|
104
|
-
refetch
|
|
105
|
-
} = useInfiniteClubSearch(searchQuery, PAGE_SIZE);
|
|
106
|
-
|
|
107
|
-
// Flatten all pages into a single array and add clubImageUrl from state
|
|
108
|
-
const allClubs = data?.pages?.flatMap(page =>
|
|
109
|
-
(page?.data?.data || []).map((club: any) => ({
|
|
110
|
-
...club,
|
|
111
|
-
clubImageUrl: joinClubImageUrls[club.id] || undefined
|
|
112
|
-
}))
|
|
113
|
-
) || [];
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const { data: membersData, isLoading: isLoadingMembers, isError: isErrorMembers, refetch: refetchMembers } = useMembers({ PageNumber: 0, PageSize: 10 });
|
|
117
|
-
const rawMembers = Array.isArray(membersData?.data?.data) ? membersData.data.data : [];
|
|
118
|
-
const sortedMembers = rawMembers.sort((a: any, b: any) => {
|
|
119
|
-
if (a?.isOwner) return -1;
|
|
120
|
-
if (b?.isOwner) return 1;
|
|
121
|
-
return 0;
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const members = sortedMembers.map((member: any) => ({
|
|
125
|
-
...member,
|
|
126
|
-
profileImageUrl: joinClubMemberImageUrls[member.id] || undefined
|
|
127
|
-
}));
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
useFocusEffect(useCallback(() => {
|
|
131
|
-
refetch();
|
|
132
|
-
}, [refetch]));
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// Pull to refresh
|
|
136
|
-
const onRefresh = useCallback(async () => {
|
|
137
|
-
try {
|
|
138
|
-
setIsRefreshing(true);
|
|
139
|
-
// Clear all cached data for this query to start fresh from page 0
|
|
140
|
-
queryClient.removeQueries({
|
|
141
|
-
queryKey: ['infiniteClubSearch', searchQuery, PAGE_SIZE]
|
|
142
|
-
});
|
|
143
|
-
// This will trigger a fresh fetch starting from page 0
|
|
144
|
-
await refetch();
|
|
145
|
-
} catch (error) {
|
|
146
|
-
console.error('Refresh failed:', error);
|
|
147
|
-
} finally {
|
|
148
|
-
setIsRefreshing(false);
|
|
149
|
-
}
|
|
150
|
-
}, [refetch, queryClient, searchQuery]);
|
|
151
|
-
|
|
152
|
-
// Handle search input with debouncing
|
|
153
|
-
const handleSearch = useCallback((text: string) => {
|
|
154
|
-
setSearchQuery(text);
|
|
155
|
-
if (text.length < 3) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Clear existing timeout
|
|
160
|
-
if (searchTimeout) {
|
|
161
|
-
clearTimeout(searchTimeout);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Set new timeout for debounced search
|
|
165
|
-
const timeout = setTimeout(() => {
|
|
166
|
-
// Clear cache and refetch with new search term
|
|
167
|
-
queryClient.removeQueries({
|
|
168
|
-
queryKey: ['infiniteClubSearch', text || 'club', PAGE_SIZE]
|
|
169
|
-
});
|
|
170
|
-
}, 500); // 500ms debounce
|
|
171
|
-
|
|
172
|
-
setSearchTimeout(timeout);
|
|
173
|
-
}, [searchTimeout, queryClient]);
|
|
174
|
-
|
|
175
|
-
// Cleanup timeout on unmount
|
|
176
|
-
useEffect(() => {
|
|
177
|
-
return () => {
|
|
178
|
-
if (searchTimeout) {
|
|
179
|
-
clearTimeout(searchTimeout);
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
}, [searchTimeout]);
|
|
183
|
-
|
|
184
|
-
// Load more data
|
|
185
|
-
const onEndReached = useCallback(() => {
|
|
186
|
-
if (isFetchingNextPage || !hasNextPage) return;
|
|
187
|
-
fetchNextPage();
|
|
188
|
-
}, [isFetchingNextPage, hasNextPage, fetchNextPage, allClubs.length]);
|
|
189
|
-
|
|
190
|
-
// Render loading footer
|
|
191
|
-
const renderFooter = () => {
|
|
192
|
-
if (!isFetchingNextPage) return null;
|
|
193
|
-
return (
|
|
194
|
-
<View style={styles.footerLoader}>
|
|
195
|
-
<ActivityIndicator size="small" color={theme.colors.primary} />
|
|
196
|
-
<Text style={styles.footerText}>Loading more clubs...</Text>
|
|
197
|
-
</View>
|
|
198
|
-
);
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const renderEmptyComponent = () => {
|
|
204
|
-
if (isLoading) return null;
|
|
205
|
-
return (
|
|
206
|
-
<View style={styles.emptyContainer}>
|
|
207
|
-
<Text style={styles.emptyListText}>No clubs found</Text>
|
|
208
|
-
<Text style={styles.emptySubText}>Try refreshing or check back later</Text>
|
|
209
|
-
</View>
|
|
210
|
-
);
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
const handleScroll = useCallback(() => {
|
|
214
|
-
Keyboard.dismiss();
|
|
215
|
-
}, []);
|
|
216
|
-
|
|
217
|
-
const handleMemberSelection = (member: any) => {
|
|
218
|
-
setSelectedMember(member);
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
// Bottom sheet animation functions
|
|
222
|
-
const showBottomSheet = () => {
|
|
223
|
-
setSelectedMember(null);
|
|
224
|
-
setMembersModal(true);
|
|
225
|
-
Animated.spring(translateY, {
|
|
226
|
-
toValue: 0,
|
|
227
|
-
useNativeDriver: true,
|
|
228
|
-
tension: 100,
|
|
229
|
-
friction: 8,
|
|
230
|
-
}).start();
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
const hideBottomSheet = () => {
|
|
234
|
-
Animated.timing(translateY, {
|
|
235
|
-
toValue: bottomSheetHeight,
|
|
236
|
-
duration: 300,
|
|
237
|
-
useNativeDriver: true,
|
|
238
|
-
}).start(() => {
|
|
239
|
-
setMembersModal(false);
|
|
240
|
-
translateY.setValue(bottomSheetHeight);
|
|
241
|
-
});
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
// PanResponder for swipe gestures
|
|
245
|
-
const panResponder = useRef(
|
|
246
|
-
PanResponder.create({
|
|
247
|
-
onMoveShouldSetPanResponder: (_, gestureState) => {
|
|
248
|
-
return Math.abs(gestureState.dy) > 5;
|
|
249
|
-
},
|
|
250
|
-
onPanResponderGrant: () => {
|
|
251
|
-
translateY.setOffset((translateY as any)._value);
|
|
252
|
-
translateY.setValue(0);
|
|
253
|
-
},
|
|
254
|
-
onPanResponderMove: (_, gestureState) => {
|
|
255
|
-
// Only allow downward movement
|
|
256
|
-
if (gestureState.dy > 0) {
|
|
257
|
-
translateY.setValue(gestureState.dy);
|
|
258
|
-
}
|
|
259
|
-
},
|
|
260
|
-
onPanResponderRelease: (_, gestureState) => {
|
|
261
|
-
translateY.flattenOffset();
|
|
262
|
-
|
|
263
|
-
// If swiped down more than 100px or with high velocity, close the modal
|
|
264
|
-
if (gestureState.dy > 100 || gestureState.vy > 0.5) {
|
|
265
|
-
hideBottomSheet();
|
|
266
|
-
} else {
|
|
267
|
-
// Snap back to original position
|
|
268
|
-
Animated.spring(translateY, {
|
|
269
|
-
toValue: 0,
|
|
270
|
-
useNativeDriver: true,
|
|
271
|
-
tension: 100,
|
|
272
|
-
friction: 8,
|
|
273
|
-
}).start();
|
|
274
|
-
}
|
|
275
|
-
},
|
|
276
|
-
})
|
|
277
|
-
).current;
|
|
278
|
-
|
|
279
|
-
const renderMemberListItem = ({ item }: { item: any }) => (
|
|
280
|
-
<MemberListItem
|
|
281
|
-
item={item}
|
|
282
|
-
isSelected={selectedMember?.id === item?.id}
|
|
283
|
-
onSelect={handleMemberSelection}
|
|
284
|
-
/>
|
|
285
|
-
);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const getJoinClubImageUrls = async (clubItem: any) => {
|
|
289
|
-
if (clubItem?.clubImage == null) return;
|
|
290
|
-
|
|
291
|
-
// Skip if already fetched
|
|
292
|
-
if (joinClubImageUrls[clubItem.id] !== undefined) return;
|
|
293
|
-
|
|
294
|
-
const [folder, blobName] = clubItem?.clubImage.split('/');
|
|
295
|
-
|
|
296
|
-
try {
|
|
297
|
-
getImageUrlMutation.mutate({
|
|
298
|
-
containerName: folder,
|
|
299
|
-
blobName: blobName
|
|
300
|
-
}, {
|
|
301
|
-
onSuccess: (response) => {
|
|
302
|
-
const imageUrl = response?.data?.data?.url;
|
|
303
|
-
console.log('JoinClub Image URL response', response.data);
|
|
304
|
-
|
|
305
|
-
// Prefetch image into React Native cache to avoid blink when scrolling back
|
|
306
|
-
if (imageUrl) {
|
|
307
|
-
Image.prefetch(imageUrl).catch((prefetchError) => {
|
|
308
|
-
console.warn('JoinClub Image prefetch failed:', prefetchError);
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Simple state update instead of recreating entire query cache
|
|
313
|
-
setJoinClubImageUrls(prev => ({
|
|
314
|
-
...prev,
|
|
315
|
-
[clubItem.id]: imageUrl
|
|
316
|
-
}));
|
|
317
|
-
},
|
|
318
|
-
onError: (error) => {
|
|
319
|
-
const errorInfo = getApiErrorInfo(error);
|
|
320
|
-
ToastManager.error(errorInfo.message);
|
|
321
|
-
setJoinClubImageUrls(prev => ({
|
|
322
|
-
...prev,
|
|
323
|
-
[clubItem.id]: ""
|
|
324
|
-
}));
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
} catch (err) {
|
|
328
|
-
setJoinClubImageUrls(prev => ({
|
|
329
|
-
...prev,
|
|
330
|
-
[clubItem.id]: ""
|
|
331
|
-
}));
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const getJoinClubImageMembersUrls = async (memberItem: any) => {
|
|
337
|
-
console.log('JoinClub getJoinClubImageMembersUrls>>>>>14798', JSON.stringify(memberItem));
|
|
338
|
-
if (memberItem?.profileImage == null) return;
|
|
339
|
-
|
|
340
|
-
// Skip if already fetched
|
|
341
|
-
if (joinClubMemberImageUrls[memberItem.id] !== undefined) return;
|
|
342
|
-
console.log('JoinClub getJoinClubImageMembersUrls>>>>>147', JSON.stringify(memberItem));
|
|
343
|
-
|
|
344
|
-
const [folder, blobName] = memberItem?.profileImage.split('/');
|
|
345
|
-
|
|
346
|
-
try {
|
|
347
|
-
getImageUrlMutation.mutate({
|
|
348
|
-
containerName: folder,
|
|
349
|
-
blobName: blobName
|
|
350
|
-
}, {
|
|
351
|
-
onSuccess: (response) => {
|
|
352
|
-
const imageUrl = response?.data?.data?.url;
|
|
353
|
-
console.log('JoinClub Image Members URL response', response.data);
|
|
354
|
-
|
|
355
|
-
// Prefetch image into React Native cache to avoid blink when scrolling back
|
|
356
|
-
if (imageUrl) {
|
|
357
|
-
Image.prefetch(imageUrl).catch((prefetchError) => {
|
|
358
|
-
console.warn('JoinClub Image prefetch failed:', prefetchError);
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Simple state update instead of recreating entire query cache
|
|
363
|
-
setJoinClubMemberImageUrls(prev => ({
|
|
364
|
-
...prev,
|
|
365
|
-
[memberItem.id]: imageUrl
|
|
366
|
-
}));
|
|
367
|
-
},
|
|
368
|
-
onError: (error) => {
|
|
369
|
-
const errorInfo = getApiErrorInfo(error);
|
|
370
|
-
ToastManager.error(errorInfo.message);
|
|
371
|
-
setJoinClubMemberImageUrls(prev => ({
|
|
372
|
-
...prev,
|
|
373
|
-
[memberItem.id]: ""
|
|
374
|
-
}));
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
} catch (err) {
|
|
378
|
-
setJoinClubMemberImageUrls(prev => ({
|
|
379
|
-
...prev,
|
|
380
|
-
[memberItem.id]: ""
|
|
381
|
-
}));
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
|
|
385
|
-
const onJoinClubViewableItemsChanged = useCallback(
|
|
386
|
-
({
|
|
387
|
-
viewableItems,
|
|
388
|
-
}: {
|
|
389
|
-
viewableItems: Array<{ item: any }>;
|
|
390
|
-
}) => {
|
|
391
|
-
const items = viewableItems.map(({ item }) => item);
|
|
392
|
-
items.forEach((item) => {
|
|
393
|
-
if (item && !item.clubImageUrl) {
|
|
394
|
-
console.log('JoinClub onViewableItemsChanged>>>>>', JSON.stringify(item));
|
|
395
|
-
getJoinClubImageUrls(item);
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
},
|
|
399
|
-
[getJoinClubImageUrls]
|
|
400
|
-
);
|
|
401
|
-
const onJoinClubViewableItemsChangedMembers = useCallback(
|
|
402
|
-
({
|
|
403
|
-
viewableItems,
|
|
404
|
-
}: {
|
|
405
|
-
viewableItems: Array<{ item: any }>;
|
|
406
|
-
}) => {
|
|
407
|
-
const items = viewableItems.map(({ item }) => item);
|
|
408
|
-
items.forEach((item) => {
|
|
409
|
-
if (item && !item.profileImageUrl) {
|
|
410
|
-
getJoinClubImageMembersUrls(item);
|
|
411
|
-
}
|
|
412
|
-
});
|
|
413
|
-
},
|
|
414
|
-
[getJoinClubImageMembersUrls]
|
|
415
|
-
);
|
|
416
|
-
|
|
417
|
-
const joinClubViewabilityConfig = useRef({
|
|
418
|
-
viewAreaCoveragePercentThreshold: 50, // Increased threshold for better on-demand loading
|
|
419
|
-
minimumViewTime: 100, // Minimum time item must be visible before triggering
|
|
420
|
-
waitForInteraction: false, // Don't wait for user interaction
|
|
421
|
-
}).current;
|
|
422
|
-
|
|
423
|
-
const joinClubViewabilityConfigMembers = useRef({
|
|
424
|
-
viewAreaCoveragePercentThreshold: 50, // Increased threshold for better on-demand loading
|
|
425
|
-
minimumViewTime: 100, // Minimum time item must be visible before triggering
|
|
426
|
-
waitForInteraction: false, // Don't wait for user interaction
|
|
427
|
-
}).current;
|
|
428
|
-
|
|
429
|
-
const renderClubItem = ({ item }: any) => {
|
|
430
|
-
return (
|
|
431
|
-
<ClubCard
|
|
432
|
-
club={item}
|
|
433
|
-
onJoin={true}
|
|
434
|
-
clubCodeShow={true}
|
|
435
|
-
onJoinPress={() => {
|
|
436
|
-
setJoinClubDetails(item);
|
|
437
|
-
|
|
438
|
-
// Check if there's only one member with isOwner: true
|
|
439
|
-
if (rawMembers.length === 1 && rawMembers[0].isOwner === true) {
|
|
440
|
-
// Auto-select the single owner member and join directly
|
|
441
|
-
setSelectedMember(rawMembers[0]);
|
|
442
|
-
|
|
443
|
-
// Join club directly without showing modal
|
|
444
|
-
joinClubMutation.mutate({
|
|
445
|
-
clubId: item.id,
|
|
446
|
-
memberId: rawMembers[0].id,
|
|
447
|
-
}, {
|
|
448
|
-
onSuccess: (response) => {
|
|
449
|
-
ToastManager.success(response.data.message || 'Join club successful');
|
|
450
|
-
navigation.goBack();
|
|
451
|
-
},
|
|
452
|
-
onError: (error) => {
|
|
453
|
-
const errorInfo = getApiErrorInfo(error);
|
|
454
|
-
ToastManager.error(errorInfo?.message);
|
|
455
|
-
console.error('Join club failed:', error);
|
|
456
|
-
},
|
|
457
|
-
});
|
|
458
|
-
} else {
|
|
459
|
-
// Show modal for member selection as usual
|
|
460
|
-
setMembersModal(true);
|
|
461
|
-
showBottomSheet();
|
|
462
|
-
}
|
|
463
|
-
}}
|
|
464
|
-
/>
|
|
465
|
-
);
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
const joinClub = () => {
|
|
469
|
-
if (joinClubDetails && selectedMember) {
|
|
470
|
-
|
|
471
|
-
joinClubMutation.mutate({
|
|
472
|
-
clubId: joinClubDetails?.id,
|
|
473
|
-
memberId: selectedMember?.id,
|
|
474
|
-
}, {
|
|
475
|
-
onSuccess: (response) => {
|
|
476
|
-
ToastManager.success(response.data.message || 'Join club successful');
|
|
477
|
-
navigation.goBack();
|
|
478
|
-
},
|
|
479
|
-
onError: (error) => {
|
|
480
|
-
const errorInfo = getApiErrorInfo(error);
|
|
481
|
-
ToastManager.error(errorInfo?.message);
|
|
482
|
-
|
|
483
|
-
console.error('Join club failed:', error);
|
|
484
|
-
},
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
return (
|
|
490
|
-
<View style={styles.container}>
|
|
491
|
-
<StatusBar
|
|
492
|
-
barStyle="light-content"
|
|
493
|
-
backgroundColor={theme.colors.blue}
|
|
494
|
-
translucent={Platform.OS === 'android' ? true : false}
|
|
495
|
-
/>
|
|
496
|
-
{Platform.OS === 'ios' && <View style={styles.statusBarBackground} />}
|
|
497
|
-
<SafeAreaView style={styles.header} >
|
|
498
|
-
<View style={{ paddingHorizontal: theme.spacing.lg }}>
|
|
499
|
-
<TouchableOpacity onPress={() => navigation.goBack()}>
|
|
500
|
-
<SVG.arrowLeft_white width={moderateScale(25)} height={moderateScale(25)} />
|
|
501
|
-
</TouchableOpacity>
|
|
502
|
-
<Text style={styles.headerTitle}>Search and join club</Text>
|
|
503
|
-
</View>
|
|
504
|
-
<View style={styles.addMemberContainer}>
|
|
505
|
-
</View>
|
|
506
|
-
<View style={styles.content}>
|
|
507
|
-
<View style={styles.searchContainer}>
|
|
508
|
-
<TextInput
|
|
509
|
-
leftIconStyle={{ marginLeft: theme.spacing.sm }}
|
|
510
|
-
leftIconSizeWidth={moderateScale(17)}
|
|
511
|
-
leftIconSizeHeight={moderateScale(17)}
|
|
512
|
-
placeholder="Unique code and club name"
|
|
513
|
-
style={styles.searchInput}
|
|
514
|
-
leftIcon={SVG.search}
|
|
515
|
-
variant="outlined"
|
|
516
|
-
maxLength={50}
|
|
517
|
-
value={searchQuery}
|
|
518
|
-
onChangeText={handleSearch}
|
|
519
|
-
/>
|
|
520
|
-
</View>
|
|
521
|
-
<FlatList
|
|
522
|
-
data={allClubs}
|
|
523
|
-
renderItem={renderClubItem}
|
|
524
|
-
keyExtractor={keyExtractor}
|
|
525
|
-
style={styles.clubsList}
|
|
526
|
-
showsVerticalScrollIndicator={false}
|
|
527
|
-
contentContainerStyle={styles.flatListContent}
|
|
528
|
-
removeClippedSubviews={false}
|
|
529
|
-
initialNumToRender={10}
|
|
530
|
-
onEndReached={onEndReached}
|
|
531
|
-
onEndReachedThreshold={0.9}
|
|
532
|
-
ListFooterComponent={renderFooter}
|
|
533
|
-
ListEmptyComponent={renderEmptyComponent}
|
|
534
|
-
// onViewableItemsChanged={onJoinClubViewableItemsChanged}
|
|
535
|
-
// viewabilityConfig={joinClubViewabilityConfig}
|
|
536
|
-
onScroll={handleScroll}
|
|
537
|
-
scrollEventThrottle={16}
|
|
538
|
-
refreshControl={
|
|
539
|
-
<RefreshControl
|
|
540
|
-
refreshing={isRefreshing}
|
|
541
|
-
onRefresh={onRefresh}
|
|
542
|
-
colors={[theme.colors.primary]}
|
|
543
|
-
tintColor={theme.colors.primary}
|
|
544
|
-
/>
|
|
545
|
-
}
|
|
546
|
-
/>
|
|
547
|
-
</View>
|
|
548
|
-
</SafeAreaView>
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
<Modal
|
|
552
|
-
visible={membersModal}
|
|
553
|
-
animationType="fade"
|
|
554
|
-
transparent={true}
|
|
555
|
-
onRequestClose={hideBottomSheet}
|
|
556
|
-
>
|
|
557
|
-
<View style={styles.modalOverlay}>
|
|
558
|
-
<TouchableOpacity
|
|
559
|
-
style={styles.modalBackdrop}
|
|
560
|
-
activeOpacity={1}
|
|
561
|
-
onPress={hideBottomSheet}
|
|
562
|
-
/>
|
|
563
|
-
<Animated.View
|
|
564
|
-
style={[
|
|
565
|
-
styles.modalContainer,
|
|
566
|
-
{
|
|
567
|
-
transform: [{ translateY }],
|
|
568
|
-
},
|
|
569
|
-
]}
|
|
570
|
-
>
|
|
571
|
-
<View style={styles.dragHandle} {...panResponder.panHandlers} />
|
|
572
|
-
<FlatList
|
|
573
|
-
data={members}
|
|
574
|
-
renderItem={renderMemberListItem}
|
|
575
|
-
style={styles.membersList}
|
|
576
|
-
showsVerticalScrollIndicator={false}
|
|
577
|
-
ListHeaderComponent={() => {
|
|
578
|
-
return (
|
|
579
|
-
<View>
|
|
580
|
-
<View style={styles.headerContainer}>
|
|
581
|
-
<Text style={styles.mamberHeaderTitle}>Continue as</Text>
|
|
582
|
-
</View>
|
|
583
|
-
</View>
|
|
584
|
-
)
|
|
585
|
-
}}
|
|
586
|
-
keyExtractor={(item, index) => {
|
|
587
|
-
// Create a more robust key for members
|
|
588
|
-
const id = item?.id?.toString() || '';
|
|
589
|
-
const name = item?.name || '';
|
|
590
|
-
const email = item?.email || '';
|
|
591
|
-
const memberCode = item?.membershipCode || '';
|
|
592
|
-
const createdAt = item?.createdAt || '';
|
|
593
|
-
|
|
594
|
-
// Create a unique composite key using multiple attributes
|
|
595
|
-
const keyParts = [id, name, email, memberCode, createdAt].filter(part => part && part.toString().trim() !== '');
|
|
596
|
-
|
|
597
|
-
if (keyParts.length > 0) {
|
|
598
|
-
return `member-${keyParts.join('-')}`;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Ultimate fallback with index to ensure uniqueness
|
|
602
|
-
return `member-unknown-${index}`;
|
|
603
|
-
}}
|
|
604
|
-
|
|
605
|
-
/>
|
|
606
|
-
|
|
607
|
-
<Button
|
|
608
|
-
title="Continue"
|
|
609
|
-
onPress={() => {
|
|
610
|
-
joinClub();
|
|
611
|
-
hideBottomSheet();
|
|
612
|
-
}}
|
|
613
|
-
variant="primary"
|
|
614
|
-
size="medium"
|
|
615
|
-
style={styles.nextButton}
|
|
616
|
-
disabled={selectedMember == null ? true : false}
|
|
617
|
-
/>
|
|
618
|
-
</Animated.View>
|
|
619
|
-
</View>
|
|
620
|
-
</Modal>
|
|
621
|
-
</View>
|
|
622
|
-
);
|
|
623
|
-
};
|
|
624
|
-
|
|
625
|
-
const styles = StyleSheet.create({
|
|
626
|
-
container: {
|
|
627
|
-
flex: 1,
|
|
628
|
-
backgroundColor: theme.colors.blue,
|
|
629
|
-
},
|
|
630
|
-
statusBarBackground: {
|
|
631
|
-
position: 'absolute',
|
|
632
|
-
top: 0,
|
|
633
|
-
left: 0,
|
|
634
|
-
right: 0,
|
|
635
|
-
height: Platform.OS === 'ios' ? 44 : 0, // Status bar height for iOS
|
|
636
|
-
backgroundColor: theme.colors.blue,
|
|
637
|
-
zIndex: 1000,
|
|
638
|
-
},
|
|
639
|
-
header: {
|
|
640
|
-
flex: 1,
|
|
641
|
-
backgroundColor: theme.colors.blue,
|
|
642
|
-
paddingVertical: theme.spacing.xs,
|
|
643
|
-
paddingTop: Platform.OS === 'android' ? theme.spacing.lg : theme.spacing.xs,
|
|
644
|
-
},
|
|
645
|
-
headerTitle: {
|
|
646
|
-
marginTop: moderateScale(20),
|
|
647
|
-
fontFamily: Fonts.outfitMedium,
|
|
648
|
-
fontSize: moderateScale(22),
|
|
649
|
-
color: theme.colors.white,
|
|
650
|
-
},
|
|
651
|
-
skipButton: {
|
|
652
|
-
alignSelf: 'flex-end',
|
|
653
|
-
paddingHorizontal: theme.spacing.sm,
|
|
654
|
-
paddingVertical: theme.spacing.xs,
|
|
655
|
-
},
|
|
656
|
-
skipButtonText: {
|
|
657
|
-
fontFamily: Fonts.outfitSemiBold,
|
|
658
|
-
fontSize: theme.typography.fontSize.md,
|
|
659
|
-
color: theme.colors.white,
|
|
660
|
-
},
|
|
661
|
-
addMemberContainer: {
|
|
662
|
-
backgroundColor: theme.colors.blue,
|
|
663
|
-
paddingHorizontal: theme.spacing.lg,
|
|
664
|
-
paddingBottom: theme.spacing.md,
|
|
665
|
-
},
|
|
666
|
-
addMemberButton: {
|
|
667
|
-
flexDirection: 'row',
|
|
668
|
-
alignItems: 'center',
|
|
669
|
-
justifyContent: 'center',
|
|
670
|
-
backgroundColor: 'transparent',
|
|
671
|
-
borderWidth: 1.5,
|
|
672
|
-
borderColor: theme.colors.appleGreen,
|
|
673
|
-
borderRadius: theme.borderRadius.xxl,
|
|
674
|
-
paddingVertical: theme.spacing.sm,
|
|
675
|
-
paddingHorizontal: theme.spacing.lg,
|
|
676
|
-
gap: theme.spacing.sm,
|
|
677
|
-
},
|
|
678
|
-
addMemberButtonText: {
|
|
679
|
-
fontFamily: Fonts.outfitMedium,
|
|
680
|
-
fontSize: theme.typography.fontSize.md,
|
|
681
|
-
color: theme.colors.appleGreen,
|
|
682
|
-
},
|
|
683
|
-
content: {
|
|
684
|
-
flex: 1,
|
|
685
|
-
backgroundColor: theme.colors.background,
|
|
686
|
-
paddingHorizontal: theme.spacing.lg,
|
|
687
|
-
paddingTop: theme.spacing.md,
|
|
688
|
-
borderTopLeftRadius: moderateScale(30),
|
|
689
|
-
borderTopRightRadius: moderateScale(30),
|
|
690
|
-
},
|
|
691
|
-
sectionTitle: {
|
|
692
|
-
fontFamily: Fonts.outfitMedium,
|
|
693
|
-
fontSize: theme.typography.fontSize.xs,
|
|
694
|
-
color: theme.colors.blue,
|
|
695
|
-
marginBottom: theme.spacing.md,
|
|
696
|
-
letterSpacing: 0.5,
|
|
697
|
-
},
|
|
698
|
-
clubsList: {
|
|
699
|
-
flex: 1,
|
|
700
|
-
},
|
|
701
|
-
flatListContent: {
|
|
702
|
-
paddingBottom: theme.spacing.xl,
|
|
703
|
-
},
|
|
704
|
-
emptyContainer: {
|
|
705
|
-
flex: 1,
|
|
706
|
-
justifyContent: 'center',
|
|
707
|
-
alignItems: 'center',
|
|
708
|
-
paddingVertical: theme.spacing.xl,
|
|
709
|
-
},
|
|
710
|
-
bottomContainer: {
|
|
711
|
-
height: moderateScale(120),
|
|
712
|
-
paddingHorizontal: theme.spacing.lg,
|
|
713
|
-
paddingVertical: theme.spacing.md,
|
|
714
|
-
backgroundColor: theme.colors.white,
|
|
715
|
-
},
|
|
716
|
-
nextButton: {
|
|
717
|
-
width: '100%',
|
|
718
|
-
marginBottom: theme.spacing.md,
|
|
719
|
-
},
|
|
720
|
-
emptyListText: {
|
|
721
|
-
fontFamily: Fonts.outfitMedium,
|
|
722
|
-
fontSize: theme.typography.fontSize.md,
|
|
723
|
-
color: theme.colors.text,
|
|
724
|
-
textAlign: 'center',
|
|
725
|
-
marginBottom: theme.spacing.xs,
|
|
726
|
-
},
|
|
727
|
-
emptySubText: {
|
|
728
|
-
fontFamily: Fonts.outfitRegular,
|
|
729
|
-
fontSize: theme.typography.fontSize.sm,
|
|
730
|
-
color: theme.colors.textSecondary,
|
|
731
|
-
textAlign: 'center',
|
|
732
|
-
},
|
|
733
|
-
footerLoader: {
|
|
734
|
-
flexDirection: 'row',
|
|
735
|
-
justifyContent: 'center',
|
|
736
|
-
alignItems: 'center',
|
|
737
|
-
paddingVertical: theme.spacing.md,
|
|
738
|
-
gap: theme.spacing.sm,
|
|
739
|
-
},
|
|
740
|
-
footerText: {
|
|
741
|
-
fontFamily: Fonts.outfitMedium,
|
|
742
|
-
fontSize: theme.typography.fontSize.sm,
|
|
743
|
-
color: theme.colors.textSecondary,
|
|
744
|
-
},
|
|
745
|
-
errorText: {
|
|
746
|
-
fontFamily: Fonts.outfitMedium,
|
|
747
|
-
fontSize: theme.typography.fontSize.md,
|
|
748
|
-
color: theme.colors.error,
|
|
749
|
-
textAlign: 'center',
|
|
750
|
-
marginBottom: theme.spacing.md,
|
|
751
|
-
},
|
|
752
|
-
retryButton: {
|
|
753
|
-
backgroundColor: theme.colors.primary,
|
|
754
|
-
paddingHorizontal: theme.spacing.lg,
|
|
755
|
-
paddingVertical: theme.spacing.sm,
|
|
756
|
-
borderRadius: theme.borderRadius.md,
|
|
757
|
-
},
|
|
758
|
-
retryButtonText: {
|
|
759
|
-
fontFamily: Fonts.outfitMedium,
|
|
760
|
-
fontSize: theme.typography.fontSize.md,
|
|
761
|
-
color: theme.colors.white,
|
|
762
|
-
textAlign: 'center',
|
|
763
|
-
},
|
|
764
|
-
modalOverlay: {
|
|
765
|
-
flex: 1,
|
|
766
|
-
justifyContent: 'flex-end',
|
|
767
|
-
},
|
|
768
|
-
modalBackdrop: {
|
|
769
|
-
position: 'absolute',
|
|
770
|
-
top: 0,
|
|
771
|
-
left: 0,
|
|
772
|
-
right: 0,
|
|
773
|
-
bottom: 0,
|
|
774
|
-
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
775
|
-
},
|
|
776
|
-
modalContainer: {
|
|
777
|
-
backgroundColor: theme.colors.white,
|
|
778
|
-
borderTopLeftRadius: moderateScale(20),
|
|
779
|
-
borderTopRightRadius: moderateScale(20),
|
|
780
|
-
paddingHorizontal: theme.spacing.lg,
|
|
781
|
-
paddingTop: theme.spacing.sm,
|
|
782
|
-
paddingBottom: theme.spacing.xl,
|
|
783
|
-
height: '45%',
|
|
784
|
-
maxHeight: '45%',
|
|
785
|
-
},
|
|
786
|
-
dragHandle: {
|
|
787
|
-
width: moderateScale(40),
|
|
788
|
-
height: moderateScale(4),
|
|
789
|
-
backgroundColor: theme.colors.border,
|
|
790
|
-
borderRadius: moderateScale(2),
|
|
791
|
-
alignSelf: 'center',
|
|
792
|
-
marginBottom: theme.spacing.sm,
|
|
793
|
-
},
|
|
794
|
-
membersList: {
|
|
795
|
-
flex: 1,
|
|
796
|
-
},
|
|
797
|
-
memberListItem: {
|
|
798
|
-
height: moderateScale(70),
|
|
799
|
-
alignItems: 'center',
|
|
800
|
-
paddingVertical: theme.spacing.md,
|
|
801
|
-
flexDirection: 'row',
|
|
802
|
-
},
|
|
803
|
-
headerContainer: {
|
|
804
|
-
paddingVertical: theme.spacing.xs,
|
|
805
|
-
},
|
|
806
|
-
mamberHeaderTitle: {
|
|
807
|
-
fontFamily: Fonts.outfitSemiBold,
|
|
808
|
-
fontSize: moderateScale(14),
|
|
809
|
-
color: theme.colors.text,
|
|
810
|
-
},
|
|
811
|
-
profileImage: {
|
|
812
|
-
width: moderateScale(50),
|
|
813
|
-
height: moderateScale(50),
|
|
814
|
-
borderRadius: moderateScale(25),
|
|
815
|
-
marginRight: moderateScale(10),
|
|
816
|
-
},
|
|
817
|
-
memberName: {
|
|
818
|
-
fontFamily: Fonts.outfitSemiBold,
|
|
819
|
-
fontSize: moderateScale(14),
|
|
820
|
-
color: theme.colors.text,
|
|
821
|
-
},
|
|
822
|
-
memberNameContainer: {
|
|
823
|
-
flex: 1,
|
|
824
|
-
justifyContent: 'center',
|
|
825
|
-
},
|
|
826
|
-
memberNameSubtitle: {
|
|
827
|
-
fontFamily: Fonts.outfitMedium,
|
|
828
|
-
fontSize: moderateScale(12),
|
|
829
|
-
color: theme.colors.border,
|
|
830
|
-
},
|
|
831
|
-
searchInput: {
|
|
832
|
-
},
|
|
833
|
-
searchContainer: {
|
|
834
|
-
marginBottom: theme.spacing.md,
|
|
835
|
-
},
|
|
836
|
-
placeholderStyle: {
|
|
837
|
-
fontFamily: Fonts.outfitRegular,
|
|
838
|
-
fontSize: moderateScale(5),
|
|
839
|
-
color: theme.colors.textSecondary,
|
|
840
|
-
},
|
|
841
|
-
});
|