This commit is contained in:
Abin Toppo
2025-10-01 15:43:42 +05:30
27 changed files with 526 additions and 2300 deletions
+1
View File
@@ -1,3 +1,4 @@
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: ['react-native-reanimated/plugin'],
};
+16 -1941
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -33,14 +33,17 @@
"react-native-elements": "^3.4.3",
"react-native-fast-confetti": "^1.1.1",
"react-native-image-picker": "^8.2.1",
"react-native-inset-shadow": "^1.0.3",
"react-native-keychain": "^10.0.0",
"react-native-linear-gradient": "^2.8.3",
"react-native-paper": "^5.14.5",
"react-native-reanimated": "^4.1.2",
"react-native-safe-area-context": "^5.6.1",
"react-native-screens": "^4.16.0",
"react-native-svg": "^15.13.0",
"react-native-vector-icons": "^10.3.0",
"react-native-walkthrough-tooltip": "^1.6.0",
"react-native-worklets": "^0.6.0",
"react-redux": "^9.2.0"
},
"devDependencies": {
+1 -1
View File
@@ -15,7 +15,7 @@ export default function AppView({
children,
backgroundColor = "#f5f5f5",
barStyle = "dark-content",
paddingHorizontal = 16, // default
paddingHorizontal = 12, // default
paddingTop = 8, // default
style,
}: AppViewProps) {
+4 -2
View File
@@ -1,5 +1,6 @@
import React from "react";
import { TouchableOpacity, Text, StyleSheet, ActivityIndicator, ViewStyle } from "react-native";
import colors from "./colors";
type CustomButtonProps = {
title: string;
@@ -13,8 +14,8 @@ type CustomButtonProps = {
export default function CustomButton({
title,
onPress,
backgroundColor = "#000000ff",
textColor = "#fff",
backgroundColor = colors.pastelGreen,
textColor = colors.textDark,
loading = false,
style,
}: CustomButtonProps) {
@@ -36,6 +37,7 @@ export default function CustomButton({
const styles = StyleSheet.create({
button: {
elevation:5,
paddingVertical: 16,
paddingHorizontal: 20,
borderRadius: 12,
+15 -23
View File
@@ -4,7 +4,6 @@ import React, { FC } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import colors from './colors';
import { useNavigation } from '@react-navigation/native';
// Update the type to match the full data structure
interface JoinedGroup {
id: string;
@@ -80,8 +79,10 @@ export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item, onViewPress })
{/* Financial Info */}
<View style={styles.infoRow}>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>Contribution</Text>
<Text style={styles.infoValue}>₹{item.contributionAmount.toLocaleString('en-IN')}</Text>
<Text style={styles.infoLabel}>Contribution</Text>
<Text style={styles.infoValue}>₹{item.contributionAmount.toLocaleString('en-IN')}</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>Cycle</Text>
@@ -89,25 +90,12 @@ export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item, onViewPress })
</View>
</View>
{/* Participants Progress */}
<View style={styles.progressContainer}>
<View style={styles.progressLabels}>
<Text style={styles.participantText}>Participants</Text>
<Text style={styles.participantCount}>{item.currentParticipants} / {item.maxParticipants}</Text>
</View>
<View style={styles.progressBarBackground}>
<View style={[styles.progressBarFill, { width: `${progress}%` }]} />
</View>
</View>
{/* Footer with Dates and View Button */}
<View style={styles.footer}>
<Text style={styles.dateText}>
{formatDate(item.startDate)} - {formatDate(item.endDate)}
</Text>
<TouchableOpacity style={styles.viewButton} onPress={onViewPress}>
<Text style={styles.viewButtonText}>View</Text>
</TouchableOpacity>
</View>
</TouchableOpacity>
);
@@ -115,7 +103,7 @@ export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item, onViewPress })
const styles = StyleSheet.create({
card: {
backgroundColor: '#FFFFFF',
backgroundColor:colors.pastelOrange,
borderRadius: 16,
padding: 20,
marginBottom: 16,
@@ -123,7 +111,8 @@ const styles = StyleSheet.create({
shadowOpacity: 0.08,
shadowRadius: 10,
shadowOffset: { width: 0, height: 4 },
elevation: 3,
elevation: 5,
marginHorizontal:12
},
header: {
flexDirection: 'row',
@@ -160,8 +149,14 @@ const styles = StyleSheet.create({
borderBottomWidth: 1,
borderColor: colors.pastelOrange,
marginBottom: 16,
backgroundColor:colors.white,
borderRadius:15,
},
infoItem: {
padding:5,
paddingHorizontal:10,
backgroundColor:colors.white,
alignItems: 'flex-start',
},
infoLabel: {
@@ -207,10 +202,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: 8,
borderTopWidth: 1,
borderTopColor: '#F3F4F6',
paddingTop: 16,
},
dateText: {
fontSize: 14,
+30 -12
View File
@@ -6,20 +6,20 @@ import {
TouchableWithoutFeedback,
Animated,
ViewStyle,
StyleProp,
} from "react-native";
import LinearGradient from "react-native-linear-gradient";
import Ionicons from "react-native-vector-icons/Ionicons";
import { IoniconsIconName } from "react-native-vector-icons/Ionicons";
import { Ionicons } from "@react-native-vector-icons/ionicons";
import colors from "../colors"; // Assuming your pastel colors are defined here
type CardProps = {
title: string;
subtitle?: string;
iconName?: IoniconsIconName;
iconName?: string;
onPress?: () => void;
backOne?: string;
backTwo?: string;
style?: ViewStyle;
style?: StyleProp<ViewStyle>;
};
const AnimatedCard: React.FC<CardProps> = ({
@@ -27,18 +27,26 @@ const AnimatedCard: React.FC<CardProps> = ({
subtitle,
iconName = "ellipse",
onPress,
backOne = colors.pastelPink, // Defaulting to pastelPink
backOne = colors.pastelPink, // Defaulting to pastelPink from your preferences
backTwo = colors.primary,
style,
}) => {
const scale = useRef(new Animated.Value(1)).current;
const handlePressIn = () => {
Animated.spring(scale, { toValue: 0.96, useNativeDriver: true, friction: 4 }).start();
Animated.spring(scale, {
toValue: 0.96,
useNativeDriver: true,
friction: 4,
}).start();
};
const handlePressOut = () => {
Animated.spring(scale, { toValue: 1, useNativeDriver: true, friction: 4 }).start(() => {
Animated.spring(scale, {
toValue: 1,
useNativeDriver: true,
friction: 4,
}).start(() => {
if (onPress) {
onPress();
}
@@ -60,7 +68,14 @@ const AnimatedCard: React.FC<CardProps> = ({
{/* Content */}
<View style={styles.content}>
{iconName && <Ionicons name={iconName} size={38} color="#fff" style={styles.icon} />}
{iconName && (
<Ionicons
name={iconName}
size={38}
color="#fff"
style={styles.icon}
/>
)}
<View style={styles.textContainer}>
<Text style={styles.title}>{title}</Text>
{subtitle && <Text style={styles.subtitle}>{subtitle}</Text>}
@@ -74,6 +89,10 @@ const AnimatedCard: React.FC<CardProps> = ({
const styles = StyleSheet.create({
card: {
minWidth: 170,
flex: 1,
height: 170,
marginHorizontal: 8,
marginVertical: 10,
borderRadius: 24,
overflow: "hidden",
@@ -85,7 +104,6 @@ const styles = StyleSheet.create({
backgroundColor: colors.white,
},
gradient: {
minHeight: 150, // Increased height for better spacing
position: "relative",
flex: 1,
justifyContent: "center",
@@ -121,15 +139,15 @@ const styles = StyleSheet.create({
marginBottom: 12,
},
textContainer: {
alignItems: 'center',
alignItems: "center",
},
title: {
fontSize: 18,
fontWeight: "bold",
color: "#fff",
textTransform: 'uppercase',
textTransform: "uppercase",
letterSpacing: 1,
textAlign: 'center',
textAlign: "center",
},
subtitle: {
fontSize: 13,
+64
View File
@@ -0,0 +1,64 @@
import React, { useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withRepeat,
withSequence,
withDelay,
Easing,
} from 'react-native-reanimated';
const AnimatedText = ({ text, style, delay = 0 }) => {
const opacity = useSharedValue(0);
const translateY = useSharedValue(20);
useEffect(() => {
opacity.value = withDelay(delay, withTiming(1, { duration: 800 }));
translateY.value = withDelay(delay, withTiming(0, { duration: 800 }));
}, []);
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
transform: [{ translateY: translateY.value }],
}));
return (
<Animated.Text style={[styles.text, style, animatedStyle]}>
{text}
</Animated.Text>
);
};
export default AnimatedText;
// Usage
const MyComponent = () => {
return (
<View style={styles.container}>
<AnimatedText text="Hello World" style={styles.title} />
<AnimatedText text="Welcome to Reanimated" style={styles.subtitle} delay={200} />
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 32,
fontWeight: 'bold',
marginBottom: 10,
},
subtitle: {
fontSize: 18,
color: '#666',
},
text: {
textAlign: 'center',
},
});
+5 -9
View File
@@ -34,6 +34,9 @@ const CustomInput: React.FC<CustomInputProps> = ({
<TextInput
style={[styles.input, icon && { paddingLeft: 36 }, style]}
placeholderTextColor={colors.textLight}
value={value ? String(value) : undefined}
{...(error ? {borderColor:'red', borderWidth:1} : {})}
{...rest}
/>
</View>
@@ -57,21 +60,14 @@ const CustomButton: React.FC<CustomButtonProps> = ({
type = "primary",
icon,
style = {},
buttonBackground,
buttonBackground,
}) => {
const backgroundColor =
buttonBackground ??
(type === "secondary"
? colors.pastelBlue
: type === "danger"
? colors.pastelOrange
: colors.primary);
const textColor = type === "secondary" ? colors.textDark : '#000';
return (
<TouchableOpacity
style={[styles.button, { backgroundColor }, style]}
style={[styles.button, { backgroundColor: buttonBackground }, style]}
onPress={onPress}
activeOpacity={0.8}
>
+1 -8
View File
@@ -17,7 +17,7 @@ const Header: React.FC<HeaderProps> = ({
showBack = true,
onBackPress,
rightAction,
titleColor = 'pink',
titleColor = "",
arrowColor='pink'
}) => {
const navigation = useNavigation();
@@ -32,13 +32,6 @@ const Header: React.FC<HeaderProps> = ({
return (
<View style={styles.header}>
{showBack ? (
<TouchableOpacity onPress={handleBack}>
<Ionicons name="arrow-back" size={24} color={arrowColor} />
</TouchableOpacity>
) : (
<View style={{ width: 24 }} /> // placeholder to keep spacing
)}
<Text style={[styles.headerTitle, {color:titleColor}]}>{title}</Text>
<View>{rightAction}</View>
</View>
+7 -3
View File
@@ -6,7 +6,7 @@ import {
Animated,
Easing,
} from "react-native";
import {Ionicons} from "@react-native-vector-icons/ionicons";
import { Ionicons } from "@react-native-vector-icons/ionicons";
import colors from "../components/colors";
import CustomModal from "../components/common/CustomModal";
@@ -77,9 +77,13 @@ const ContributionDetails: React.FC<ContributionProps> = ({
>
<Text style={styles.label}>Base Amount</Text>
<Text style={styles.value}>₹{data?.baseAmount}</Text>
{
data?.alreadyPaid ? <Text style={styles?.valueHighlight}>Already Paid Amount</Text> :
<View>
<Text style={styles.label}>Total Payable</Text>
<Text style={styles.valueHighlight}>₹{data?.totalPayable}</Text>
</View>}
<Text style={styles.label}>Total Payable</Text>
<Text style={styles.valueHighlight}>₹{data?.totalPayable}</Text>
<Text style={styles.label}>Due Date</Text>
<Text style={styles.value}>{data?.dueDate}</Text>
+3 -2
View File
@@ -55,7 +55,7 @@ export default function GroupCreationForm() {
if (!form.minParticipants || Number(form.minParticipants) <= 1) newErrors.minParticipants = "Min participants must be at least 2.";
if (!form.maxParticipants || Number(form.maxParticipants) < Number(form.minParticipants)) newErrors.maxParticipants = "Max must be greater than or equal to min.";
if (!form.contributionAmount || Number(form.contributionAmount) <= 0) newErrors.contributionAmount = "Amount must be greater than 0.";
if (form.cycleType === "CUSTOM" && (!form.customCycleDays || Number(form.customCycleDays) <= 0)) newErrors.customCycleDays = "Custom days must be greater than 0.";
// if (form.cycleType === "CUSTOM" && (!form.customCycleDays || Number(form.customCycleDays) <= 0)) newErrors.customCycleDays = "Custom days must be greater than 0.";
if (form.lateFeeMode && (!form.lateFeeValue || Number(form.lateFeeValue) <= 0)) newErrors.lateFeeValue = "Late fee value is required.";
setErrors(newErrors);
@@ -76,6 +76,7 @@ export default function GroupCreationForm() {
navigation.navigate("GroupReview", { form: payload });
} else {
Alert.alert("Incomplete Form", "Please fill out all required fields correctly.");
setErrors('')
}
};
@@ -88,7 +89,7 @@ export default function GroupCreationForm() {
>
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
<View style={styles.titleContainer}><Text style={styles.title}>Create New Group</Text></View>
<CustomInput label="Group Name" placeholder="e.g., 'Family Savings'" value={form.groupName} onChangeText={(val) => handleChange("groupName", val)} icon="people" error={errors.groupName} />
<CustomInput label="Description" placeholder="e.g., 'Monthly fund for family needs'" value={form.description} onChangeText={(val) => handleChange("description", val)} multiline icon="document-text" error={errors.description} />
View File
+57 -49
View File
@@ -27,8 +27,6 @@ import { ApiResponse } from "./home/UserProfileStatus";
import * as Keychain from "react-native-keychain";
const { width } = Dimensions.get('window');
// --- Types ---
interface Group {
id: string;
@@ -39,46 +37,66 @@ interface Group {
contributionAmount: number;
cycleType: string;
createdByName: string;
imageBlob: string
}
// --- UI Components ---
const GroupCard = ({ item, onJoin, onView, isGrid }: { item: Group; onJoin: (groupId: string) => void; onView: (group: Group) => void; isGrid?: boolean; }) => {
const GroupCard = ({ item, onJoin, onView, isGrid }: { item: Group; onJoin:()=>void; onView: (group: Group) => void; isGrid?: boolean; }) => {
const isFull = item.currentParticipants >= item.maxParticipants;
const cardStyle = isGrid ? styles.gridCard : styles.listCard;
const [loading, setLoading] = useState<boolean>(false)
const navigation = useNavigation()
return (
<View style={cardStyle}>
<Text style={styles.groupName} numberOfLines={1}>{item.groupName}</Text>
<Text style={styles.groupDescription} numberOfLines={isGrid ? 2 : 1}>{item.description}</Text>
{/* <TouchableOpacity onPress={()=> navigation.navigate("GroupDetails" as never)}> */}
<View style={{ flex: 1, flexDirection: 'row', justifyContent: "space-between" }}>
<View>
<Text style={[styles.groupName,{width:isGrid?"50%":"100%"}]} numberOfLines={isGrid ? 2 : 1}>{item.groupName}</Text>
<Text style={styles.groupDescription} numberOfLines={isGrid ? 2 : 1}>{item.description}</Text>
</View>
<Image
source={{ uri: item?.imageBlob !== null ? `data:image/jpeg;base64,${item?.imageBlob}` : `https://i.pravatar.cc/150?u=${item.id}` }}
style={{ width: 50, borderWidth: 1, height: 50, borderColor: colors.pastelGreen, borderRadius: 25, }}
/>
</View>
<View style={styles.groupMeta}>
<Text style={styles.groupMetaText}>{item.currentParticipants}/{item.maxParticipants} Members</Text>
<Text style={styles.groupMetaText}>{item.cycleType}</Text>
</View>
{/* </TouchableOpacity> */}
<View style={styles.buttonContainer}>
<TouchableOpacity style={styles.viewButton} onPress={() => onView(item)}>
{/* <TouchableOpacity style={styles.viewButton} onPress={() => onView(item)}>
<Text style={styles.viewButtonText}>View</Text>
</TouchableOpacity>
</TouchableOpacity> */}
<TouchableOpacity
style={[styles.joinButton, isFull && styles.joinButtonDisabled]}
onPress={() => onJoin(item.id)}
disabled={isFull}
onPress={async () => {
setLoading(true);
await onJoin(item.id);
setLoading(false);
}}
disabled={isFull || loading} // Also disable button while loading
>
<Text style={styles.joinButtonText}>{isFull ? "Full" : "Join"}</Text>
{loading ? <ActivityIndicator color={colors.black} /> : <Text style={styles.joinButtonText}>{isFull ? "Full" : "Join"}</Text>}
</TouchableOpacity>
</View>
</View>
);
};
const FeaturedGroupCard = ({ item }: { item: Group }) => (
<View style={styles.featuredCard}>
<Image source={{ uri: `https://i.pravatar.cc/150?u=${item.id}` }} style={styles.featuredImage} />
<View style={styles.featuredContent}>
<Text style={styles.featuredName} numberOfLines={1}>{item.groupName}</Text>
<Text style={styles.featuredCreator}>by {item.createdByName}</Text>
const FeaturedGroupCard = ({ item }: { item: Group }) => {
return (
<View style={styles.featuredCard}>
<Image source={{ uri: item?.imageBlob !== null ? `data:image/jpeg;base64${item?.imageBlob}` : `https://i.pravatar.cc/150?u=${item.id}` }} style={styles.featuredImage} />
<View style={styles.featuredContent}>
<Text style={styles.featuredName} numberOfLines={1}>{item.groupName}</Text>
<Text style={styles.featuredCreator}>by {item.createdByName}</Text>
</View>
</View>
</View>
);
)
};
export default function GroupScreen() {
const [activeTab, setActiveTab] = useState("Public");
@@ -114,6 +132,7 @@ export default function GroupScreen() {
}
};
const fetchJoinedGroups = async () => {
if (loading) return;
setLoading(true);
@@ -152,21 +171,21 @@ export default function GroupScreen() {
try {
const url = `${JOIN_GROUP_URL}groups/${groupId}/join/10`;
console.log("Joining group URL:", url);
const res = await apiCall(url, "POST");
const res = await apiCall(url, "POST") as any;
console.log("JOIN_GROUP_URL response:", res);
// if (res.statusCode === 200) {
// Alert.alert("Success", "You requested to join the group.");
// } else {
// Alert.alert("Error", res.message || "Failed to join group.");
// }
if (res.statusCode === 200) {
Alert.alert("Success", "You requested to join the group.");
} else {
Alert.alert("Error", res.message || "Failed to join group.");
}
} catch (error) {
console.error("Join Group Error:", error);
Alert.alert("Error", "An unexpected error occurred.");
}
};
const userStatus = async () => {
try {
const credentials = await Keychain.getGenericPassword();
@@ -264,13 +283,13 @@ export default function GroupScreen() {
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
// ListFooterComponent={<ActivityIndicator size={'large'} color={colors.Group} />}
// ListFooterComponent={<ActivityIndicator size={'large'} color={colors.Group} />}
/>
);
};
return (
<AppView paddingHorizontal={0} style={styles.container}>
<AppView paddingHorizontal={0}>
{/* Fixed Header */}
<View style={styles.headerContainer}>
<Text style={styles.title}>Explore Groups</Text>
@@ -282,21 +301,11 @@ export default function GroupScreen() {
{/* Scrollable Content */}
<ScrollView style={styles.container}>
<Text style={styles.sectionTitle}>Featured</Text>
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
data={featuredGroups}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <FeaturedGroupCard item={item} />}
style={styles.featuredList}
// ListEmptyComponent={!loading && featuredGroups.length === 0 ? <Text style={styles.emptyFeaturedText}>No featured groups.</Text> : null}
/>
<View style={styles.tabHeader}>
<View style={styles.segmentedControl}>
{role === "ORGANIZER" ? (["My Groups", "Joined", "Public"].map((tab) => (
{role === "ORGANIZER" ? (["Public"].map((tab) => (
<TouchableOpacity key={tab} style={[styles.segmentButton, activeTab === tab && styles.segmentButtonActive]} onPress={() => setActiveTab(tab)}>
<Text style={[styles.segmentText, activeTab === tab && styles.segmentTextActive]}>{tab}</Text>
</TouchableOpacity>
@@ -312,7 +321,7 @@ export default function GroupScreen() {
</View>
{/* The list now renders inside a simple View, as FlatList handles its own scroll */}
<View style={styles.contentArea}>{renderContent()}</View>
<View style={{}}>{renderContent()}</View>
</ScrollView>
{role === "ORGANIZER" && (
<FAB style={styles.fab} icon={() => <MaterialIcons name="group-add" size={24} color="#333" />} onPress={() => navigation.navigate('GroupCreation')} />
@@ -322,10 +331,10 @@ export default function GroupScreen() {
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f5f6fa' },
headerContainer: { backgroundColor: '#f5f6fa', paddingHorizontal: 16 },
title: { fontSize: 22, fontWeight: "600", color: colors.Group },
searchContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff', borderRadius: 12, paddingHorizontal: 10, elevation: 2, marginVertical: 20 },
searchContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff', borderRadius: 20, paddingHorizontal: 10, elevation: 5, marginVertical: 20 , borderWidth:2,borderColor:colors.pastelGreen },
searchIcon: { marginRight: 8 },
searchInput: { flex: 1, height: 45, fontSize: 16, color: '#333' },
sectionTitle: { fontSize: 20, fontWeight: 'bold', color: colors.Group, marginBottom: 12, paddingHorizontal: 16 },
@@ -336,18 +345,17 @@ const styles = StyleSheet.create({
featuredName: { fontSize: 14, fontWeight: 'bold', color: '#333' },
featuredCreator: { fontSize: 12, color: '#777', marginTop: 2 },
tabHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, marginBottom: 16 },
segmentedControl: { flex: 1, flexDirection: 'row', backgroundColor: '#e9ecef', borderRadius: 10, padding: 4, marginRight: 10 },
segmentedControl: { flex: 1, flexDirection: 'row', borderRadius: 10, padding: 4, marginRight: 10 },
segmentButton: { flex: 1, paddingVertical: 10, borderRadius: 8, alignItems: 'center' },
segmentButtonActive: { backgroundColor: colors.pastelGreen, elevation: 2, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 },
segmentButtonActive: { backgroundColor: colors.pastelGreen, elevation: 5, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 , borderRadius:15 },
segmentText: { fontSize: 14, fontWeight: '600', color: '#555' },
segmentTextActive: { color: '#333' },
layoutToggleButton: { padding: 8 },
contentArea: { paddingHorizontal: 16 },
fab: { position: "absolute", right: 20, bottom: 90, width: 60, height: 60, justifyContent: "center", alignItems: "center", backgroundColor: colors.pastelGreen, borderRadius: 30, elevation: 4 },
listCard: { backgroundColor: '#fff', padding: 16, borderRadius: 12, marginBottom: 12, elevation: 1 },
gridCard: { backgroundColor: '#fff', padding: 12, borderRadius: 12, marginBottom: 12, elevation: 1, width: (width - 48) / 2 },
groupName: { fontSize: 16, fontWeight: 'bold', color: '#333' },
groupDescription: { fontSize: 14, color: '#666', marginVertical: 4 },
listCard: { backgroundColor: colors.pastelGreen, padding: 16, borderRadius: 20, marginBottom: 12,marginHorizontal:15, elevation: 5 },
gridCard: { backgroundColor: colors.pastelGreen, padding: 12, borderRadius: 20, marginBottom: 12,marginHorizontal:15, elevation: 5, width: (width - 48) / 2 },
groupName: { fontSize: 16, fontWeight: 'bold', color: '#333', width: '50%' },
groupDescription: { fontSize: 14, color: '#666', marginVertical: 4, width: '40%' },
groupMeta: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8, borderTopWidth: 1, borderTopColor: '#f0f0f0', paddingTop: 8, marginBottom: 10 },
groupMetaText: { fontSize: 12, color: '#888', fontWeight: '500', textTransform: 'capitalize' },
emptyText: { textAlign: 'center', marginTop: 40, fontSize: 16, color: '#888' },
@@ -355,7 +363,7 @@ const styles = StyleSheet.create({
buttonContainer: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8 },
viewButton: { flex: 1, backgroundColor: '#e9ecef', paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginRight: 5 },
viewButtonText: { color: '#333', fontWeight: 'bold', fontSize: 14 },
joinButton: { flex: 1, backgroundColor: colors.pastelGreen, paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginLeft: 5 },
joinButton: { flex: 1, backgroundColor: colors.white, paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginLeft: 5 },
joinButtonDisabled: { backgroundColor: '#bdc3c7' },
joinButtonText: { color: '#333', fontWeight: 'bold', fontSize: 14 },
});
+2 -2
View File
@@ -52,7 +52,7 @@ const InviteScreen: FC<InviteScreenProps> = ({ route }) => {
setLoading(true);
setError(null);
try {
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string };
const response = await apiCall(`http://192.168.1.9:9060/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string };
if (response.status === 'success' && response.data) {
setGroupDetails(response.data);
} else {
@@ -76,7 +76,7 @@ const InviteScreen: FC<InviteScreenProps> = ({ route }) => {
try {
// Replace with your actual API call to join the group
// For now, it just shows an alert.
await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/groups/join/invite/${code}`, 'POST');
await apiCall(`http://192.168.1.9:9060/api/fund/groups/join/invite/${code}`, 'POST');
Alert.alert('Success!', `You have requested to join "${groupDetails.groupName}".`);
} catch (e: any) {
Alert.alert('Join Error', e.message || 'An unexpected error occurred.');
+5 -2
View File
@@ -20,7 +20,7 @@ import { setUserData } from "../../redux/userSlice";
import { TabNavigationProp } from "../../navigation/BottomTabs";
import UserProfileStatus from "./UserProfileStatus";
import AnimatedCard from "../../components/common/AnimatedCard";
import { IoniconsIconName } from "@react-native-vector-icons/ionicons";
import { Ionicons } from "@react-native-vector-icons/ionicons";
import { apiCall } from "../../utils/ApiHelper";
if (Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental) {
@@ -52,6 +52,7 @@ const organizerActions: ActionItem[] = [
{ id: "1", title: "My Groups", subtitle: "Manage your groups", action: "NAVIGATE_MANAGEMENT", tab: "My Groups", iconName: "people-circle-outline", backOne: '#decbfcff', backTwo: '#B799FF' },
{ id: "2", title: "Joined Groups", subtitle: "View member groups", action: "NAVIGATE_MANAGEMENT", tab: "Joined Groups", iconName: "people-outline", backOne: '#D7E9F7', backTwo: '#82AAE3' },
{ id: "3", title: "Requests", subtitle: "Check new requests", action: "NAVIGATE_MANAGEMENT", tab: "Requests", iconName: "notifications-outline", backOne: '#DFF6DD', backTwo: '#88D18A' },
{ id: "4", title: "Create Group", subtitle: "Create a new group", action: "NAVIGATE_CREATE_GROUP", tab: "Requests", iconName: "people-outline", backOne: '#DFF6DD', backTwo: '#88D18A' },
];
const userActions: ActionItem[] = [
@@ -90,6 +91,8 @@ const Home: React.FC = () => {
navigation.navigate('Management', { actionTab: item.tab });
} else if (item.action === "TRANSACTION") {
navigation.navigate("TransactionList" as never);
} else if (item.action === "NAVIGATE_CREATE_GROUP") {
navigation.navigate("GroupCreation" as never);
} else {
Alert.alert("Unknown action", item.title);
}
@@ -158,7 +161,7 @@ const styles = StyleSheet.create({
},
greeting: {
fontSize: 22,
color: colors.textSecondary,
color: colors.textDark,
},
userName: {
fontSize: 28,
+3 -3
View File
@@ -58,7 +58,7 @@ const RequestList: React.FC = () => {
setError(null);
const response = await fetch(
"https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/pending/request",
"http://192.168.1.9:9060/api/fund/group/pending/request",
{
method: "GET",
headers: {
@@ -124,7 +124,7 @@ const RequestList: React.FC = () => {
// Make API call to approve the request
const response = await fetch(
`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/requests/${requestId}/approve`,
`http://192.168.1.9:9060/api/fund/requests/${requestId}/approve`,
{
method: "PUT",
headers: {
@@ -170,7 +170,7 @@ const RequestList: React.FC = () => {
// Make API call to decline the request
const response = await fetch(
`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/requests/${requestedId}/reject?reason=/${reason}`,
`http://192.168.1.9:9060/api/fund/requests/${requestedId}/reject?reason=/${reason}`,
{
method: "PUT",
headers: {
+3 -2
View File
@@ -37,8 +37,9 @@ const GroupDetails = () => {
const [memberData, setMemberData] = useState<Member[]>([]);
const [loading, setLoading] = useState(false);
const [openModal, setOpenModal] = useState(false)
const [payable, setPayable] = useState()
const [payable, setPayable] = useState() as any
console.log("route param ggggggggggg : ", route?.params)
// Group object
const group = {
id: route.params.item?.id,
@@ -108,7 +109,7 @@ const GroupDetails = () => {
const confirmPayment = async () => {
try {
const res = await apiCall(`${BASE_URL}payments/contribute/${group?.id}?amount=${group?.collected}`, 'POST') as any
const res = await apiCall(`${BASE_URL}payments/contribute/${group?.id}?amount=${payable?.totalPayable}`, 'POST') as any
console.log("payment response : ", res)
if (res.status) {
ToastAndroid.show('Payment done!', ToastAndroid.SHORT)
+132 -87
View File
@@ -61,7 +61,7 @@ export interface GroupDetails {
organizerId: string;
totalParticipants: number;
participants: Participant[];
imageBlob:string
imageBlob: string
}
@@ -139,7 +139,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
setLoading(true);
setError(null);
try {
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string };
const response = await apiCall(`http://192.168.1.9:9060/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string };
console.log("groupDetails", response)
if (response.status === 'success' && response.data) {
setGroupDetails(response.data);
@@ -154,7 +154,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
}
}, [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 ,allPaid: boolean }; message?: string };
const response = await apiCall(`http://192.168.1.9:9060/api/payments/group/${groupId}/payment/paid`, 'GET') as { status: string; data?: { currentCycle: number ,allPaid: boolean }; message?: string };
console.log("paymentStatus",response)
if (response.status === 'success') {
setPaymentStatus(response.data?.allPaid || false);
@@ -188,7 +188,7 @@ const getPaymentStatus = async()=>{
setSaving(true);
try {
await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/groups/${groupDetails.id}/edit`, 'PUT', editableData);
await apiCall(`http://192.168.1.9:9060/api/fund/groups/${groupDetails.id}/edit`, 'PUT', editableData);
Alert.alert('Success', 'Group has been updated.');
setEditingCard(null);
await fetchGroupDetails();
@@ -199,7 +199,7 @@ const getPaymentStatus = async()=>{
}
};
const handleDistribute = async () => {
navigation.navigate('RandomSelectUserScreen', { groupId: groupDetails?.id || '', cycleNumber: currentCycle || 0 });
navigation.navigate('RandomSelectUserScreen', { groupId: groupDetails?.id || '', cycleNumber: currentCycle || 0 });
};
const handleStartGroup = async () => {
@@ -216,7 +216,7 @@ const getPaymentStatus = async()=>{
onPress: async () => {
setStarting(true);
try {
await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/start?groupId=${groupDetails.id}`, 'GET');
await apiCall(`http://192.168.1.9:9060/api/fund/group/start?groupId=${groupDetails.id}`, 'GET');
Alert.alert('Success', 'Group has been started successfully.');
await fetchGroupDetails();
} catch (e: any) {
@@ -235,7 +235,7 @@ const getPaymentStatus = async()=>{
if (!groupDetails) return;
setInviting(true);
try {
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/invite/generate?groupId=${groupDetails.id}`, 'GET') as { data?: string };
const response = await apiCall(`http://192.168.1.9:9060/api/invite/generate?groupId=${groupDetails.id}`, 'GET') as { data?: string };
const inviteLink = response.data;
@@ -354,82 +354,127 @@ const getPaymentStatus = async()=>{
const isInvitable = groupDetails.status === 'APPROVED';
const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants;
// const pickImage = () => {
// launchImageLibrary({ mediaType: "photo", quality: 0.5 }, (response) => {
// if (response.didCancel) return;
// if (response.errorCode) {
// Alert.alert("Error", response.errorMessage || "Something went wrong");
// } else if (response.assets && response.assets.length > 0) {
// const selectedImage = response.assets[0];
// console.log(selectedImage?.uri)
// setSelectedImage(selectedImage?.uri)
// }
// });
// };
const pickImage = () => {
launchImageLibrary({ mediaType: "photo", quality: 0.5 }, (response) => {
if (response.didCancel) return;
if (response.errorCode) {
Alert.alert("Error", response.errorMessage || "Something went wrong");
} else if (response.assets && response.assets.length > 0) {
const selectedImage = response.assets[0];
console.log(selectedImage?.uri)
setSelectedImage(selectedImage?.uri)
return;
}
if (!response.assets || response.assets.length === 0) {
Alert.alert("Error", "No image selected");
return;
}
const selectedImage = response.assets[0];
const maxSizeKB = 1000;
const maxSizeBytes = maxSizeKB * 1024;
// Size validation
if (selectedImage.fileSize && selectedImage.fileSize > maxSizeBytes) {
const currentSizeKB = (selectedImage.fileSize / 1024).toFixed(2);
Alert.alert(
"File Too Large",
`Selected image is ${currentSizeKB}KB. Please select an image smaller than ${maxSizeKB}KB.`
);
return;
}
// Additional validation for required fields
if (!selectedImage.uri) {
Alert.alert("Error", "Invalid image file");
return;
}
console.log("✅ Image selected:", {
uri: selectedImage.uri,
size: (selectedImage.fileSize / 1024).toFixed(2) + "KB",
width: selectedImage.width,
height: selectedImage.height
});
setSelectedImage(selectedImage.uri);
});
};
const handleUpdateImage = async () => {
try {
const credentials = await Keychain.getGenericPassword();
if (!credentials) {
Alert.alert("Error", "You are not logged in");
return;
}
const token = credentials.password;
if (!selectedImage) {
Alert.alert("Error", "Please select an image first");
return;
try {
const credentials = await Keychain.getGenericPassword();
if (!credentials) {
Alert.alert("Error", "You are not logged in");
return;
}
const token = credentials.password;
if (!selectedImage) {
Alert.alert("Error", "Please select an image first");
return;
}
setImageUpdating(true);
// Get file extension and mime type dynamically
const fileExtension = selectedImage.split('.').pop();
const formData = new FormData();
formData.append("file", {
uri: selectedImage,
type: "image/jpeg",
name: `group_${groupId}_profile.${fileExtension || "jpg"}`,
});
const url = `${UPLOAD_GROUP_IMAGE}${groupId}/upload-image`;
console.log("url : ", url)
console.log("formdata : ", formData)
// return
const response = await fetch(url, {
method: "PUT",
headers: {
"Content-Type": "multipart/form-data",
"Authorization": `Bearer ${token}`,
},
body: formData,
});
console.log("image response : ", response)
if (response?.status === 200) {
fetchGroupDetails()
ToastAndroid.show('Group uploaded successfully', ToastAndroid.SHORT)
}
} catch (error: any) {
console.error("❌ Error updating group image:", error);
Alert.alert("Error", error.message || "Could not update group image");
} finally {
setImageUpdating(false);
}
};
setImageUpdating(true);
// Get file extension and mime type dynamically
const fileExtension = selectedImage.split('.').pop();
const formData = new FormData();
formData.append("file", {
uri: selectedImage,
type: "image/jpeg",
name: `group_${groupId}_profile.${fileExtension || "jpg"}`,
});
const url = `${UPLOAD_GROUP_IMAGE}${groupId}/upload-image`;
console.log("url : ", url)
console.log("formdata : ", formData)
// return
const response = await fetch(url, {
method: "PUT",
headers: {
"Content-Type": "multipart/form-data",
"Authorization": `Bearer ${token}`,
},
body: formData,
});
console.log("image response : ", response)
if(response?.status === 200){
fetchGroupDetails()
ToastAndroid.show('Group uploaded successfully', ToastAndroid.SHORT)
}
} catch (error:any) {
console.error("❌ Error updating group image:", error);
Alert.alert("Error", error.message || "Could not update group image");
} finally {
setImageUpdating(false);
}
};
const getProfileImage = () => {
return selectedImage
? selectedImage
: groupDetails?.imageBlob
? `data:image/jpeg;base64,${groupDetails.imageBlob}`
: "https://img.freepik.com/premium-vector/people-group-frame-icon_791764-2649.jpg";
};
// Helper function to get MIME type
const getProfileImage = () => {
return selectedImage
? selectedImage
: groupDetails?.imageBlob
? `data:image/jpeg;base64,${groupDetails.imageBlob}`
: "https://img.freepik.com/premium-vector/people-group-frame-icon_791764-2649.jpg";
};
// Helper function to get MIME type
return (
@@ -461,12 +506,12 @@ const getProfileImage = () => {
<Ionicons name="camera" size={18} color="#000" />
</TouchableOpacity>
</View>
<View style={{justifyContent:'center'}}>
<View style={{ justifyContent: 'center' }}>
<CustomButton
title={imageUpdating ? "Updating..." : "Update Image"}
onPress={handleUpdateImage}
icon={imageUpdating ? undefined : "cloud-upload"}
style={{paddingHorizontal:20}}
style={{ paddingHorizontal: 20 }}
buttonBackground={colors.pastelBlue}
/>
</View>
@@ -477,19 +522,19 @@ const getProfileImage = () => {
], 'groupInfo')}
{groupDetails.status === 'ACTIVE' ?
renderCard('Financial Rules', [
{ field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
{ field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
{ field: 'maxParticipants', label: 'Current Participants', displayValue: () => `${groupDetails.totalParticipants}` },
], 'financialRules')
:
renderCard('Financial Rules', [
{ field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
{ field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` },
{ field: 'minParticipants', label: 'Min Participants', displayValue: () => `${groupDetails.minParticipants}` },
{ field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
], 'financialRules')
{groupDetails.status === 'ACTIVE' ?
renderCard('Financial Rules', [
{ field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
{ field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
{ field: 'maxParticipants', label: 'Current Participants', displayValue: () => `${groupDetails.totalParticipants}` },
], 'financialRules')
:
renderCard('Financial Rules', [
{ field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
{ field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` },
{ field: 'minParticipants', label: 'Min Participants', displayValue: () => `${groupDetails.minParticipants}` },
{ field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
], 'financialRules')
}
@@ -535,7 +580,7 @@ const getProfileImage = () => {
</TouchableOpacity>
</View>
)}
{paymentStatus && (
{paymentStatus && (
<View style={styles.floatingButtonContainer}>
<TouchableOpacity
style={styles.activateButton}
+13 -20
View File
@@ -1,5 +1,4 @@
// src/screens/ManagementScreen.tsx
import React, { useState, useCallback, FC, useEffect } from "react";
import {
View,
@@ -16,13 +15,13 @@ import { APPROVE_REQUEST_URL, DECLINE_REQUEST_URL, GET_JOINED_GROUP_URL, MY_GROU
import colors from "../../components/colors";
import AppView from "../../components/AppView";
import { apiCall } from "../../utils/ApiHelper";
import { MyGroup, JoinedGroup, JoinRequest, ApiResponse, UserJoinRequest } from '../../types'; // Adjust import path
import { MyGroup, JoinedGroup, JoinRequest, ApiResponse, UserJoinRequest } from '../../types';
import { MyGroupCard } from "../../components/MyGroupCard";
import { JoinedGroupCard } from "../../components/JoinedGroupCard";
import { RequestCard } from "../../components/RequestCard";
import { useSelector } from "react-redux";
import { UserRequested } from "../../components/UserRequested";
import { RootTabParamList } from "../../navigation/BottomTabs"; // Adjust import path
import { RootTabParamList } from "../../navigation/BottomTabs";
import { RootStackParamList } from "../../navigation/AppNavigator";
// --- TYPE DEFINITIONS ---
@@ -34,7 +33,6 @@ const ManagementScreen: FC = () => {
const user = useSelector((state: any) => state.user);
const navigation = useNavigation<GroupManagementNavProp>();
const route = useRoute<ManagementRouteProp>();
const [activeTab, setActiveTab] = useState<ActiveTab>(user.role === "USER" ? "Joined Groups" : "My Groups");
const [myGroups, setMyGroups] = useState<MyGroup[]>([]);
const [joinedGroups, setJoinedGroups] = useState<JoinedGroup[]>([]);
@@ -43,9 +41,7 @@ const ManagementScreen: FC = () => {
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
// --- HOOKS ---
// Effect to set the initial tab from route params
useEffect(() => {
const passedTab = route.params?.actionTab;
if (passedTab) {
@@ -56,9 +52,9 @@ const ManagementScreen: FC = () => {
}
}, [route.params?.actionTab, user.role]);
// --- FIXED: fetchData dependency array is corrected ---
const fetchData = useCallback(async () => {
// Check for loading state at the start to prevent multiple simultaneous fetches
if (loading) return;
setLoading(true);
@@ -87,20 +83,18 @@ const ManagementScreen: FC = () => {
} finally {
setLoading(false);
}
// The only external dependency that should trigger a re-fetch is the user's role.
}, [user.role]); // **FIXED**: Removed `loading` from the dependency array
// --- FIXED: useFocusEffect now has a stable dependency ---
}, [user.role]);
useFocusEffect(
useCallback(() => {
fetchData();
}, [fetchData]) // This will now only re-run if fetchData (and thus user.role) changes.
}, [fetchData])
);
// --- HANDLERS (No changes needed here) ---
const handleViewGroup = (item: JoinedGroup) => {
// Assuming you have an 'Invite' screen in your RootTabParamList or another navigator
// navigation.navigate('Invite' as any, { groupId: item.id });
};
const handleAction = async (url: string, successMessage: string, failureMessage: string) => {
@@ -122,10 +116,9 @@ const ManagementScreen: FC = () => {
const handleDecline = (req: JoinRequest) => handleAction(DECLINE_REQUEST_URL + req.requestId + "/reject?reason=/", `Request from ${req.userName} has been declined.`, "Failed to decline request.");
const handleManageGroup = (group: MyGroup) => navigation.navigate('ManagementTabs', { groupId: group.id });
// --- RENDER LOGIC ---
const renderContent = () => {
if (error) return <Text style={styles.emptyText}>{error}</Text>;
// Improved loading logic to avoid hiding existing data during a refresh
if (loading && myGroups.length === 0 && joinedGroups.length === 0 && requests.length === 0) {
return <ActivityIndicator size="large" color={colors.primary} style={styles.loader} />;
}
@@ -168,12 +161,12 @@ const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f5f6fa' },
header: { paddingHorizontal: 20, paddingBottom: 15, paddingTop: 10 },
title: { fontSize: 22, fontWeight: "600", color: colors.Settings },
segmentedControl: { flexDirection: 'row', backgroundColor: '#e9ecef', borderRadius: 10, marginHorizontal: 20, marginBottom: 20, padding: 3 },
segmentedControl: { flexDirection: 'row',borderRadius: 10, marginHorizontal: 20, marginBottom: 20, padding: 3 },
segmentButton: { flex: 1, paddingVertical: 12, alignItems: 'center', borderRadius: 8 },
segmentActive: { backgroundColor: colors.pastelOrange, elevation: 3, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 },
segmentActive: { backgroundColor: colors.pastelOrange, elevation: 5,borderRadius:15, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 },
segmentText: { fontSize: 14, fontWeight: '600', color: '#6c757d' },
segmentTextActive: { color: colors.textDark },
contentArea: { flex: 1, paddingHorizontal: 20 },
contentArea: { flex: 1},
loader: { marginTop: 50 },
emptyText: { textAlign: 'center', marginTop: 60, fontSize: 16, color: '#6c757d' },
});
@@ -2,7 +2,7 @@ import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { View, Text, StyleSheet, Platform, TouchableOpacity } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Ionicons from 'react-native-vector-icons/Ionicons'; // Correct import for react-native-vector-icons
import {Ionicons} from '@react-native-vector-icons/ionicons'; // Correct import for react-native-vector-icons
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path
@@ -28,7 +28,7 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) =
console.log("option : ", options)
// A custom option to pass the icon name
const iconName = options.tabBarIconName ?? 'help-circle';
const iconName = label === "Settings" ? "trail-sign-outline" : label === "Payments" ? "trending-up" : label === "Members" ? "people-outline" : "flower";
const isFocused = state.index === index;
@@ -79,7 +79,7 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) =
// --- Main Tab Navigator Component ---
const ManagementTabNavigation: React.FC<Props> = ({ route }) => {
const { groupId } = route.params
const { groupId } = route.params
return (
<Tab.Navigator
@@ -97,10 +97,10 @@ const ManagementTabNavigation: React.FC<Props> = ({ route }) => {
options={{
tabBarLabel: 'Settings',
// Custom prop to define the icon for our FloatingTabBar
}}
/>
<Tab.Screen
<Tab.Screen
name="Payments"
component={Payments}
// Pass the groupId to the EditGroupScreen as an initial parameter
@@ -111,16 +111,16 @@ const ManagementTabNavigation: React.FC<Props> = ({ route }) => {
}}
/>
<Tab.Screen
<Tab.Screen
name="Member"
component={Member}
// Pass the groupId to the EditGroupScreen as an initial parameter
initialParams={{ groupId }}
initialParams={{ groupId }}
options={{
tabBarLabel: 'Members',
}}
/>
{/* You can easily add more tabs here in the future */}
</Tab.Navigator>
+1 -1
View File
@@ -56,7 +56,7 @@ const MemberScreen = ({ route }) => {
setLoading(true);
try {
const response = await apiCall<ApiResponse>(
`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/all/member?groupId=${groupId}`,
`http://192.168.1.9:9060/api/fund/group/all/member?groupId=${groupId}`,
'GET'
);
+18 -1
View File
@@ -7,7 +7,7 @@ import AppView from '../../components/AppView';
import { apiCall } from '../../utils/ApiHelper';
// --- CONSTANTS ---
const API_BASE_URL = 'https://exotic-humpback-mistakenly.ngrok-free.app/api/payments';
const API_BASE_URL = 'http://192.168.1.9:9060/api/payments';
// --- TYPE DEFINITIONS ---
interface ApiResponse<T> {
@@ -54,6 +54,21 @@ const Payments: React.FC = () => {
const [isModalVisible, setModalVisible] = useState(false);
const [currentParticipant, setCurrentParticipant] = useState<Participant | null>(null);
const [waiverAmount, setWaiverAmount] = useState('');
const [cycleData, setCycleData] = useState<any>();
const getCycleData = async () => {
try {
const response = await apiCall(`${API_BASE_URL}/groups/${groupId}/cycle-dashboard`, 'GET') as ApiResponse<any>;
if (response && response.statusCode === 200) {
setCycleData(response.data);
} else {
Alert.alert("Error", response?.message || "Failed to fetch cycle data.");
}
} catch (error: any) {
Alert.alert("Error", error.message || "An unexpected error occurred.");
}
};
const fetchPaymentDetails = useCallback(async () => {
if (!refreshing) setLoading(true);
@@ -73,11 +88,13 @@ const Payments: React.FC = () => {
}, [groupId, refreshing]);
useEffect(() => {
getCycleData();
fetchPaymentDetails();
}, [fetchPaymentDetails]);
const onRefresh = useCallback(() => {
setRefreshing(true);
getCycleData();
fetchPaymentDetails();
}, [fetchPaymentDetails]);
@@ -1,26 +1,30 @@
import React, { useState, useEffect, useRef } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, FlatList, Dimensions, Alert, ActivityIndicator, Animated } from 'react-native';
import { View, Text, StyleSheet, TouchableOpacity, FlatList, Dimensions, Alert, ActivityIndicator, Animated, Button } from 'react-native';
import { apiCall } from '../../utils/ApiHelper';
import { Ionicons } from "@react-native-vector-icons/ionicons";
import { useNavigation } from '@react-navigation/native';
import ContributionDetails from '../ContributionScreen';
const { width } = Dimensions.get('window');
const NUM_COLUMNS = 3;
const ITEM_SIZE = width / NUM_COLUMNS - 20;
const RandomSelectUserScreen: React.FC<{ route: any }> = ({ route }) => {
const navigation = useNavigation() as never
const { groupId, cycleNumber } = route.params;
const [users, setUsers] = useState<{ id: string; name: string }[]>([]);
const [highlightedIndex, setHighlightedIndex] = useState<number | null>(null);
const [winner, setWinner] = useState<{ id: string; name: string } | null>(null);
const [isAnimating, setIsAnimating] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [openModal, setOpenModal] = useState(false)
const scaleAnim = useRef(new Animated.Value(1)).current;
useEffect(() => {
const fetchParticipants = async () => {
try {
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/all/member?groupId=${groupId}`, "GET") as any;
const response = await apiCall(`http://192.168.1.9:9060/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,
@@ -73,10 +77,10 @@ const RandomSelectUserScreen: React.FC<{ route: any }> = ({ route }) => {
const fetchWinner = async () => {
const payload = { groupId: groupId };
try {
const response = await apiCall('https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/method/based/userSelect', 'POST', payload) as any;
if (response.status === 'success' && response.data?.id) {
const winnerData = { id: response.data.id, name: `${response.data.firstName} ${response.data.lastName}` };
const response = await apiCall('http://192.168.1.9:9060/api/fund/method/based/userSelect', 'POST', payload) as any;
console.log("random select user response : ", response)
if (response.status === 'success' && response.data?.selectedUser?.id) {
const winnerData = { id: response?.data?.selectedUser?.id, name: `${response.data.selectedUser.firstName} ${response.data.selectedUser.lastName}` };
const winnerIndex = users.findIndex(u => u.id === winnerData.id);
if (winnerIndex !== -1) {
@@ -89,7 +93,7 @@ const RandomSelectUserScreen: React.FC<{ route: any }> = ({ route }) => {
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.');
// Alert.alert('Error', 'Could not connect to the server to fetch the winner.');
} finally {
setIsAnimating(false);
}
@@ -147,9 +151,16 @@ const RandomSelectUserScreen: React.FC<{ route: any }> = ({ route }) => {
<View style={styles.footer}>
{winner && !isAnimating && (
<View style={styles.winnerTextContainer}>
<Text style={styles.winnerText}>🎉 Winner: {winner.name}</Text>
<Text style={styles.winnerText}>🎉 Winner: {winner.name}</Text>
</View>
)}
<Button title='Pay' onPress={() => setOpenModal(true)} />
{
openModal && <ContributionDetails data={undefined} visible={openModal} onClose={() => setOpenModal(false)} onConfirm={() => setOpenModal(false)} />
}
<TouchableOpacity
style={[styles.button, isAnimating && styles.buttonDisabled]}
onPress={handleStartSelection}
@@ -181,9 +192,9 @@ const styles = StyleSheet.create({
alignItems: 'center',
},
header: {
paddingTop: 50,
paddingBottom: 20,
alignItems: 'center'
paddingTop: 50,
paddingBottom: 20,
alignItems: 'center'
},
title: {
fontSize: 32,
@@ -191,9 +202,9 @@ const styles = StyleSheet.create({
color: '#111827',
},
subtitle: {
fontSize: 18,
color: '#6b7280',
marginTop: 4
fontSize: 18,
color: '#6b7280',
marginTop: 4
},
grid: {
paddingHorizontal: 10,
@@ -211,8 +222,8 @@ const styles = StyleSheet.create({
padding: 8,
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 2,
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 3.84,
@@ -237,12 +248,12 @@ const styles = StyleSheet.create({
color: '#000000ff',
},
footer: {
width: '100%',
padding: 20,
borderTopWidth: 1,
borderTopColor: '#e5e7eb',
alignItems: 'center',
backgroundColor: '#fff'
width: '100%',
padding: 20,
borderTopWidth: 1,
borderTopColor: '#e5e7eb',
alignItems: 'center',
backgroundColor: '#fff'
},
button: {
paddingVertical: 16,
@@ -252,8 +263,8 @@ const styles = StyleSheet.create({
elevation: 3,
shadowColor: "#4f46e5",
shadowOffset: {
width: 0,
height: 4,
width: 0,
height: 4,
},
shadowOpacity: 0.3,
shadowRadius: 4.65,
+1 -1
View File
@@ -43,7 +43,7 @@ const BecomeOrg = () => {
const token = credentials.password;
const response = await fetch(
"https://exotic-humpback-mistakenly.ngrok-free.app/api/organizer/registration/apply",
"http://192.168.1.9:9060/api/organizer/registration/apply",
{
method: "POST",
headers: {
+96 -97
View File
@@ -369,7 +369,7 @@ const Profile = () => {
return (
<AppView>
<AppView paddingHorizontal={5} >
{/* Header */}
<View style={styles.header}>
<Text style={styles.headerTitle}>User Profile</Text>
@@ -425,7 +425,6 @@ const Profile = () => {
)
}
<View style={styles.profileImageSection}>
<View style={styles.avatarContainer}>
<Image
@@ -472,12 +471,13 @@ const Profile = () => {
onPress={() => setEditing(true)}
icon="create"
style={styles.editButton}
buttonBackground={colors.pastelBlue}
buttonBackground={colors.white}
/>
</View>
</View>
<View style ={{backgroundColor:colors.pastelBlue,borderRadius:20,elevation:5,paddingVertical:10}}>
<View style={styles.detailRow}>
<Ionicons name="person" size={20} color={colors.pastelBlue} style={styles.detailIcon} />
<Ionicons name="person" size={20} color={colors.black} style={styles.detailIcon} />
<View>
<Text style={styles.detailLabel}>Name</Text>
<Text style={styles.detailText}>{user?.firstName} {user?.lastName}</Text>
@@ -486,7 +486,7 @@ const Profile = () => {
<View style={styles.detailRow}>
<Ionicons name="mail" size={20} color={colors.pastelBlue} style={styles.detailIcon} />
<Ionicons name="mail" size={20} color={colors.black} style={styles.detailIcon} />
<View>
<Text style={styles.detailLabel}>Email</Text>
<Text style={styles.detailText}>{user?.email || "Not provided"}</Text>
@@ -495,7 +495,7 @@ const Profile = () => {
<View style={styles.detailRow}>
<Ionicons name="call" size={20} color={colors.pastelBlue} style={styles.detailIcon} />
<Ionicons name="call" size={20} color={colors.black} style={styles.detailIcon} />
<View>
<Text style={styles.detailLabel}>Mobile</Text>
<Text style={styles.detailText}>{user?.mobile || "Not provided"}</Text>
@@ -504,16 +504,16 @@ const Profile = () => {
<View style={styles.detailRow}>
<Ionicons name="at-circle" size={20} color={colors.pastelBlue} style={styles.detailIcon} />
<Ionicons name="at-circle" size={20} color={colors.black} style={styles.detailIcon} />
<View>
<Text style={styles.detailLabel}>Username</Text>
<Text style={styles.detailLabel}>Usernames</Text>
<Text style={styles.detailText}>{user?.username}</Text>
</View>
</View>
<View style={styles.detailRow}>
<Ionicons name="location" size={20} color={colors.pastelBlue} style={styles.detailIcon} />
<Ionicons name="location" size={20} color={colors.black} style={styles.detailIcon} />
<View>
<Text style={styles.detailLabel}>Address</Text>
<Text style={styles.detailText}>{user?.address || "Not provided"}</Text>
@@ -522,7 +522,7 @@ const Profile = () => {
<View style={styles.detailRow}>
<Ionicons name="male-female" size={20} color={colors.pastelBlue} style={styles.detailIcon} />
<Ionicons name="male-female" size={20} color={colors.black} style={styles.detailIcon} />
<View>
<Text style={styles.detailLabel}>Gender</Text>
<Text style={styles.detailText}>{user?.gender || "Not specified"}</Text>
@@ -531,18 +531,19 @@ const Profile = () => {
<View style={styles.detailRow}>
<Ionicons name="key" size={20} color={colors.pastelBlue} style={styles.detailIcon} />
<Ionicons name="key" size={20} color={colors.black} style={styles.detailIcon} />
<View>
<Text style={styles.detailLabel}>Role</Text>
<Text style={styles.detailText}>{user?.role || "User"}</Text>
</View>
</View>
</View>
{/* BANK DETAILS SECTION */}
<View style={{ marginTop: 20 }}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={styles.sectionTitle}>Bank Information</Text>
{/* Navigate to a screen you will create for editing bank details */}
<CustomButton
title="Edit Bank Details"
onPress={() =>{ navigation.navigate("EditBankDetails", {
@@ -553,120 +554,115 @@ const Profile = () => {
icon="create-outline"
style={styles.editButton}
buttonBackground={colors.pastelBlue}
buttonBackground={colors.white}
/>
</View>
<View style ={{backgroundColor:colors.pastelBlue,borderRadius:20,elevation:5,paddingVertical:10}}>
<View style={styles.detailRow}>
<Ionicons name="business" size={20} color={colors.pastelBlue} style={styles.detailIcon} />
<Ionicons name="business" size={20} color={colors.black}style={styles.detailIcon} />
<View>
<Text style={styles.detailLabel}>Bank Name</Text>
<Text style={styles.detailText}>{user?.bankName || "Not provided"}</Text>
</View>
</View>
<View style={styles.detailRow}>
<Ionicons name="barcode" size={20} color={colors.pastelBlue} style={styles.detailIcon} />
<Ionicons name="barcode" size={20} color={colors.black} style={styles.detailIcon} />
<View>
<Text style={styles.detailLabel}>Branch Code</Text>
<Text style={styles.detailText}>{user?.branchCode || "Not provided"}</Text>
</View>
</View>
<View style={styles.detailRow}>
<Ionicons name="card" size={20} color={colors.pastelBlue} style={styles.detailIcon} />
<Ionicons name="card" size={20} color={colors.black} style={styles.detailIcon} />
<View>
<Text style={styles.detailLabel}>Account Number</Text>
<Text style={styles.detailText}>{user?.bankAccountNumber || "Not provided"}</Text>
</View>
</View>
</View>
</View>
</View>
</>
) : (
<>
{/* Edit Form */}
<View style={styles.editSection}>
<Text style={styles.sectionTitle}>Edit Profile</Text>
<>
{/* Edit Form */}
<View style={styles.editSection}>
<Text style={styles.sectionTitle}>Edit Profile</Text>
<CustomInput
error=""
label="First Name"
placeholder="Enter first name"
value={user?.firstName ?? ""}
onChangeText={(text) => setUser({ ...user, firstName: text })}
icon="person"
/>
<CustomInput
error=""
label="First Name"
placeholder="Enter first name"
value={user?.firstName ?? ""}
onChangeText={(text) => setUser({ ...user, firstName: text })}
icon="person"
/>
<CustomInput
error=""
label="Last Name"
placeholder="Enter last name"
value={user?.lastName ?? ""}
onChangeText={(text) => setUser({ ...user, lastName: text })}
icon="person"
/>
<CustomInput
error=""
label="Email"
placeholder="Enter email"
value={user?.email ?? ""}
onChangeText={(text) => setUser({ ...user, email: text })}
icon="mail"
keyboardType="email-address"
/>
<CustomInput
error=""
label="Last Name"
placeholder="Enter last name"
value={user?.lastName ?? ""}
onChangeText={(text) => setUser({ ...user, lastName: text })}
icon="person"
/>
<CustomInput
error=""
label="Username"
placeholder="Enter username"
value={user?.username ?? ""}
onChangeText={(text) => setUser({ ...user, username: text })}
icon="at"
/>
<CustomInput
error=""
label="Address"
placeholder="Enter address"
value={user?.address ?? ""}
onChangeText={(text) => setUser({ ...user, address: text })}
icon="location"
multiline
/>
<CustomInput
error=""
label="Email"
placeholder="Enter email"
value={user?.email ?? ""}
onChangeText={(text) => setUser({ ...user, email: text })}
icon="mail"
keyboardType="email-address"
/>
<CustomInput
error=""
label="Gender"
placeholder="Enter gender"
value={user?.gender ?? ""}
onChangeText={(text) => setUser({ ...user, gender: text })}
icon="male-female"
/>
<CustomInput
error=""
label="Username"
placeholder="Enter username"
value={user?.username ?? ""}
onChangeText={(text) => setUser({ ...user, username: text })}
icon="at"
/>
<CustomInput
error=""
label="Address"
placeholder="Enter address"
value={user?.address ?? ""}
onChangeText={(text) => setUser({ ...user, address: text })}
icon="location"
multiline
/>
<CustomInput
error=""
label="Gender"
placeholder="Enter gender"
value={user?.gender ?? ""}
onChangeText={(text) => setUser({ ...user, gender: text })}
icon="male-female"
/>
<View style={styles.editButtons}>
<CustomButton
title="Save Changes"
onPress={updateProfile}
icon="save"
style={styles.saveButton}
buttonBackground={colors.pastelBlue}
/>
<CustomButton
title="Cancel"
onPress={() => setEditing(false)}
icon="close"
style={styles.cancelButton}
buttonBackground={colors.white }
/>
</View>
</View>
</>
<View style={styles.editButtons}>
<CustomButton
title="Save Changes"
onPress={updateProfile}
icon="save"
style={styles.saveButton}
buttonBackground={colors.pastelBlue}
/>
<CustomButton
title="Cancel"
onPress={() => setEditing(false)}
icon="close"
style={styles.cancelButton}
buttonBackground={colors.white}
/>
</View>
</View>
</>
)}
</View>
</ScrollView>
@@ -775,10 +771,10 @@ const styles = StyleSheet.create({
flexDirection: "row",
alignItems: "center",
marginBottom: 16,
paddingHorizontal: 8,
paddingHorizontal: 12,
},
detailIcon: {
marginRight: 12,
marginRight: 20,
elevation:5
},
detailLabel: {
@@ -794,7 +790,10 @@ const styles = StyleSheet.create({
editButton: {
elevation:5,
paddingHorizontal: 10,
paddingVertical:4,
borderRadius: 15,
borderWidth:2,
borderColor:colors.pastelBlue
},
editSection: {
marginBottom: '100%',
+1 -1
View File
@@ -1,4 +1,4 @@
export const BASE_URL = "https://exotic-humpback-mistakenly.ngrok-free.app/api/"
export const BASE_URL = "http://192.168.1.9:9060/api/"
// export const BASE_URL = "https://api.godemo.in/FundCollect/api/"
export const LOGIN_URL = `${BASE_URL}user/generate/otp?mobiles=`
export const VARIFY_OTP_URL = `${BASE_URL}user/otp/verify`