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 = { module.exports = {
presets: ['module:@react-native/babel-preset'], 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-elements": "^3.4.3",
"react-native-fast-confetti": "^1.1.1", "react-native-fast-confetti": "^1.1.1",
"react-native-image-picker": "^8.2.1", "react-native-image-picker": "^8.2.1",
"react-native-inset-shadow": "^1.0.3",
"react-native-keychain": "^10.0.0", "react-native-keychain": "^10.0.0",
"react-native-linear-gradient": "^2.8.3", "react-native-linear-gradient": "^2.8.3",
"react-native-paper": "^5.14.5", "react-native-paper": "^5.14.5",
"react-native-reanimated": "^4.1.2",
"react-native-safe-area-context": "^5.6.1", "react-native-safe-area-context": "^5.6.1",
"react-native-screens": "^4.16.0", "react-native-screens": "^4.16.0",
"react-native-svg": "^15.13.0", "react-native-svg": "^15.13.0",
"react-native-vector-icons": "^10.3.0", "react-native-vector-icons": "^10.3.0",
"react-native-walkthrough-tooltip": "^1.6.0", "react-native-walkthrough-tooltip": "^1.6.0",
"react-native-worklets": "^0.6.0",
"react-redux": "^9.2.0" "react-redux": "^9.2.0"
}, },
"devDependencies": { "devDependencies": {
+1 -1
View File
@@ -15,7 +15,7 @@ export default function AppView({
children, children,
backgroundColor = "#f5f5f5", backgroundColor = "#f5f5f5",
barStyle = "dark-content", barStyle = "dark-content",
paddingHorizontal = 16, // default paddingHorizontal = 12, // default
paddingTop = 8, // default paddingTop = 8, // default
style, style,
}: AppViewProps) { }: AppViewProps) {
+4 -2
View File
@@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { TouchableOpacity, Text, StyleSheet, ActivityIndicator, ViewStyle } from "react-native"; import { TouchableOpacity, Text, StyleSheet, ActivityIndicator, ViewStyle } from "react-native";
import colors from "./colors";
type CustomButtonProps = { type CustomButtonProps = {
title: string; title: string;
@@ -13,8 +14,8 @@ type CustomButtonProps = {
export default function CustomButton({ export default function CustomButton({
title, title,
onPress, onPress,
backgroundColor = "#000000ff", backgroundColor = colors.pastelGreen,
textColor = "#fff", textColor = colors.textDark,
loading = false, loading = false,
style, style,
}: CustomButtonProps) { }: CustomButtonProps) {
@@ -36,6 +37,7 @@ export default function CustomButton({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
elevation:5,
paddingVertical: 16, paddingVertical: 16,
paddingHorizontal: 20, paddingHorizontal: 20,
borderRadius: 12, borderRadius: 12,
+15 -23
View File
@@ -4,7 +4,6 @@ import React, { FC } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import colors from './colors'; import colors from './colors';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
// Update the type to match the full data structure // Update the type to match the full data structure
interface JoinedGroup { interface JoinedGroup {
id: string; id: string;
@@ -80,8 +79,10 @@ export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item, onViewPress })
{/* Financial Info */} {/* Financial Info */}
<View style={styles.infoRow}> <View style={styles.infoRow}>
<View style={styles.infoItem}> <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>
<View style={styles.infoItem}> <View style={styles.infoItem}>
<Text style={styles.infoLabel}>Cycle</Text> <Text style={styles.infoLabel}>Cycle</Text>
@@ -89,25 +90,12 @@ export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item, onViewPress })
</View> </View>
</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 */} {/* Footer with Dates and View Button */}
<View style={styles.footer}> <View style={styles.footer}>
<Text style={styles.dateText}> <Text style={styles.dateText}>
{formatDate(item.startDate)} - {formatDate(item.endDate)} {formatDate(item.startDate)} - {formatDate(item.endDate)}
</Text> </Text>
<TouchableOpacity style={styles.viewButton} onPress={onViewPress}>
<Text style={styles.viewButtonText}>View</Text>
</TouchableOpacity>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
); );
@@ -115,7 +103,7 @@ export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item, onViewPress })
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
backgroundColor: '#FFFFFF', backgroundColor:colors.pastelOrange,
borderRadius: 16, borderRadius: 16,
padding: 20, padding: 20,
marginBottom: 16, marginBottom: 16,
@@ -123,7 +111,8 @@ const styles = StyleSheet.create({
shadowOpacity: 0.08, shadowOpacity: 0.08,
shadowRadius: 10, shadowRadius: 10,
shadowOffset: { width: 0, height: 4 }, shadowOffset: { width: 0, height: 4 },
elevation: 3, elevation: 5,
marginHorizontal:12
}, },
header: { header: {
flexDirection: 'row', flexDirection: 'row',
@@ -160,8 +149,14 @@ const styles = StyleSheet.create({
borderBottomWidth: 1, borderBottomWidth: 1,
borderColor: colors.pastelOrange, borderColor: colors.pastelOrange,
marginBottom: 16, marginBottom: 16,
backgroundColor:colors.white,
borderRadius:15,
}, },
infoItem: { infoItem: {
padding:5,
paddingHorizontal:10,
backgroundColor:colors.white,
alignItems: 'flex-start', alignItems: 'flex-start',
}, },
infoLabel: { infoLabel: {
@@ -207,10 +202,7 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginTop: 8,
borderTopWidth: 1,
borderTopColor: '#F3F4F6',
paddingTop: 16,
}, },
dateText: { dateText: {
fontSize: 14, fontSize: 14,
+30 -12
View File
@@ -6,20 +6,20 @@ import {
TouchableWithoutFeedback, TouchableWithoutFeedback,
Animated, Animated,
ViewStyle, ViewStyle,
StyleProp,
} from "react-native"; } from "react-native";
import LinearGradient from "react-native-linear-gradient"; import LinearGradient from "react-native-linear-gradient";
import Ionicons from "react-native-vector-icons/Ionicons"; import { Ionicons } from "@react-native-vector-icons/ionicons";
import { IoniconsIconName } from "react-native-vector-icons/Ionicons";
import colors from "../colors"; // Assuming your pastel colors are defined here import colors from "../colors"; // Assuming your pastel colors are defined here
type CardProps = { type CardProps = {
title: string; title: string;
subtitle?: string; subtitle?: string;
iconName?: IoniconsIconName; iconName?: string;
onPress?: () => void; onPress?: () => void;
backOne?: string; backOne?: string;
backTwo?: string; backTwo?: string;
style?: ViewStyle; style?: StyleProp<ViewStyle>;
}; };
const AnimatedCard: React.FC<CardProps> = ({ const AnimatedCard: React.FC<CardProps> = ({
@@ -27,18 +27,26 @@ const AnimatedCard: React.FC<CardProps> = ({
subtitle, subtitle,
iconName = "ellipse", iconName = "ellipse",
onPress, onPress,
backOne = colors.pastelPink, // Defaulting to pastelPink backOne = colors.pastelPink, // Defaulting to pastelPink from your preferences
backTwo = colors.primary, backTwo = colors.primary,
style, style,
}) => { }) => {
const scale = useRef(new Animated.Value(1)).current; const scale = useRef(new Animated.Value(1)).current;
const handlePressIn = () => { 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 = () => { const handlePressOut = () => {
Animated.spring(scale, { toValue: 1, useNativeDriver: true, friction: 4 }).start(() => { Animated.spring(scale, {
toValue: 1,
useNativeDriver: true,
friction: 4,
}).start(() => {
if (onPress) { if (onPress) {
onPress(); onPress();
} }
@@ -60,7 +68,14 @@ const AnimatedCard: React.FC<CardProps> = ({
{/* Content */} {/* Content */}
<View style={styles.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}> <View style={styles.textContainer}>
<Text style={styles.title}>{title}</Text> <Text style={styles.title}>{title}</Text>
{subtitle && <Text style={styles.subtitle}>{subtitle}</Text>} {subtitle && <Text style={styles.subtitle}>{subtitle}</Text>}
@@ -74,6 +89,10 @@ const AnimatedCard: React.FC<CardProps> = ({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
minWidth: 170,
flex: 1,
height: 170,
marginHorizontal: 8,
marginVertical: 10, marginVertical: 10,
borderRadius: 24, borderRadius: 24,
overflow: "hidden", overflow: "hidden",
@@ -85,7 +104,6 @@ const styles = StyleSheet.create({
backgroundColor: colors.white, backgroundColor: colors.white,
}, },
gradient: { gradient: {
minHeight: 150, // Increased height for better spacing
position: "relative", position: "relative",
flex: 1, flex: 1,
justifyContent: "center", justifyContent: "center",
@@ -121,15 +139,15 @@ const styles = StyleSheet.create({
marginBottom: 12, marginBottom: 12,
}, },
textContainer: { textContainer: {
alignItems: 'center', alignItems: "center",
}, },
title: { title: {
fontSize: 18, fontSize: 18,
fontWeight: "bold", fontWeight: "bold",
color: "#fff", color: "#fff",
textTransform: 'uppercase', textTransform: "uppercase",
letterSpacing: 1, letterSpacing: 1,
textAlign: 'center', textAlign: "center",
}, },
subtitle: { subtitle: {
fontSize: 13, 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',
},
});
+4 -8
View File
@@ -34,6 +34,9 @@ const CustomInput: React.FC<CustomInputProps> = ({
<TextInput <TextInput
style={[styles.input, icon && { paddingLeft: 36 }, style]} style={[styles.input, icon && { paddingLeft: 36 }, style]}
placeholderTextColor={colors.textLight} placeholderTextColor={colors.textLight}
value={value ? String(value) : undefined}
{...(error ? {borderColor:'red', borderWidth:1} : {})}
{...rest} {...rest}
/> />
</View> </View>
@@ -59,19 +62,12 @@ const CustomButton: React.FC<CustomButtonProps> = ({
style = {}, style = {},
buttonBackground, buttonBackground,
}) => { }) => {
const backgroundColor =
buttonBackground ??
(type === "secondary"
? colors.pastelBlue
: type === "danger"
? colors.pastelOrange
: colors.primary);
const textColor = type === "secondary" ? colors.textDark : '#000'; const textColor = type === "secondary" ? colors.textDark : '#000';
return ( return (
<TouchableOpacity <TouchableOpacity
style={[styles.button, { backgroundColor }, style]} style={[styles.button, { backgroundColor: buttonBackground }, style]}
onPress={onPress} onPress={onPress}
activeOpacity={0.8} activeOpacity={0.8}
> >
+1 -8
View File
@@ -17,7 +17,7 @@ const Header: React.FC<HeaderProps> = ({
showBack = true, showBack = true,
onBackPress, onBackPress,
rightAction, rightAction,
titleColor = 'pink', titleColor = "",
arrowColor='pink' arrowColor='pink'
}) => { }) => {
const navigation = useNavigation(); const navigation = useNavigation();
@@ -32,13 +32,6 @@ const Header: React.FC<HeaderProps> = ({
return ( return (
<View style={styles.header}> <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> <Text style={[styles.headerTitle, {color:titleColor}]}>{title}</Text>
<View>{rightAction}</View> <View>{rightAction}</View>
</View> </View>
+7 -3
View File
@@ -6,7 +6,7 @@ import {
Animated, Animated,
Easing, Easing,
} from "react-native"; } 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 colors from "../components/colors";
import CustomModal from "../components/common/CustomModal"; 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.label}>Base Amount</Text>
<Text style={styles.value}>₹{data?.baseAmount}</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.label}>Due Date</Text>
<Text style={styles.value}>{data?.dueDate}</Text> <Text style={styles.value}>{data?.dueDate}</Text>
+2 -1
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.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.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.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."; if (form.lateFeeMode && (!form.lateFeeValue || Number(form.lateFeeValue) <= 0)) newErrors.lateFeeValue = "Late fee value is required.";
setErrors(newErrors); setErrors(newErrors);
@@ -76,6 +76,7 @@ export default function GroupCreationForm() {
navigation.navigate("GroupReview", { form: payload }); navigation.navigate("GroupReview", { form: payload });
} else { } else {
Alert.alert("Incomplete Form", "Please fill out all required fields correctly."); Alert.alert("Incomplete Form", "Please fill out all required fields correctly.");
setErrors('')
} }
}; };
View File
+56 -48
View File
@@ -27,8 +27,6 @@ import { ApiResponse } from "./home/UserProfileStatus";
import * as Keychain from "react-native-keychain"; import * as Keychain from "react-native-keychain";
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
// --- Types --- // --- Types ---
interface Group { interface Group {
id: string; id: string;
@@ -39,46 +37,66 @@ interface Group {
contributionAmount: number; contributionAmount: number;
cycleType: string; cycleType: string;
createdByName: string; createdByName: string;
imageBlob: string
} }
// --- UI Components --- // --- 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 isFull = item.currentParticipants >= item.maxParticipants;
const cardStyle = isGrid ? styles.gridCard : styles.listCard; const cardStyle = isGrid ? styles.gridCard : styles.listCard;
const [loading, setLoading] = useState<boolean>(false)
const navigation = useNavigation()
return ( return (
<View style={cardStyle}> <View style={cardStyle}>
<Text style={styles.groupName} numberOfLines={1}>{item.groupName}</Text> {/* <TouchableOpacity onPress={()=> navigation.navigate("GroupDetails" as never)}> */}
<Text style={styles.groupDescription} numberOfLines={isGrid ? 2 : 1}>{item.description}</Text> <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}> <View style={styles.groupMeta}>
<Text style={styles.groupMetaText}>{item.currentParticipants}/{item.maxParticipants} Members</Text> <Text style={styles.groupMetaText}>{item.currentParticipants}/{item.maxParticipants} Members</Text>
<Text style={styles.groupMetaText}>{item.cycleType}</Text> <Text style={styles.groupMetaText}>{item.cycleType}</Text>
</View> </View>
{/* </TouchableOpacity> */}
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
<TouchableOpacity style={styles.viewButton} onPress={() => onView(item)}> {/* <TouchableOpacity style={styles.viewButton} onPress={() => onView(item)}>
<Text style={styles.viewButtonText}>View</Text> <Text style={styles.viewButtonText}>View</Text>
</TouchableOpacity> </TouchableOpacity> */}
<TouchableOpacity <TouchableOpacity
style={[styles.joinButton, isFull && styles.joinButtonDisabled]} style={[styles.joinButton, isFull && styles.joinButtonDisabled]}
onPress={() => onJoin(item.id)} onPress={async () => {
disabled={isFull} 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> </TouchableOpacity>
</View> </View>
</View> </View>
); );
}; };
const FeaturedGroupCard = ({ item }: { item: Group }) => ( const FeaturedGroupCard = ({ item }: { item: Group }) => {
<View style={styles.featuredCard}> return (
<Image source={{ uri: `https://i.pravatar.cc/150?u=${item.id}` }} style={styles.featuredImage} /> <View style={styles.featuredCard}>
<View style={styles.featuredContent}> <Image source={{ uri: item?.imageBlob !== null ? `data:image/jpeg;base64${item?.imageBlob}` : `https://i.pravatar.cc/150?u=${item.id}` }} style={styles.featuredImage} />
<Text style={styles.featuredName} numberOfLines={1}>{item.groupName}</Text> <View style={styles.featuredContent}>
<Text style={styles.featuredCreator}>by {item.createdByName}</Text> <Text style={styles.featuredName} numberOfLines={1}>{item.groupName}</Text>
<Text style={styles.featuredCreator}>by {item.createdByName}</Text>
</View>
</View> </View>
</View> )
); };
export default function GroupScreen() { export default function GroupScreen() {
const [activeTab, setActiveTab] = useState("Public"); const [activeTab, setActiveTab] = useState("Public");
@@ -114,6 +132,7 @@ export default function GroupScreen() {
} }
}; };
const fetchJoinedGroups = async () => { const fetchJoinedGroups = async () => {
if (loading) return; if (loading) return;
setLoading(true); setLoading(true);
@@ -152,15 +171,15 @@ export default function GroupScreen() {
try { try {
const url = `${JOIN_GROUP_URL}groups/${groupId}/join/10`; const url = `${JOIN_GROUP_URL}groups/${groupId}/join/10`;
console.log("Joining group URL:", url); 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); console.log("JOIN_GROUP_URL response:", res);
// if (res.statusCode === 200) { if (res.statusCode === 200) {
// Alert.alert("Success", "You requested to join the group."); Alert.alert("Success", "You requested to join the group.");
// } else { } else {
// Alert.alert("Error", res.message || "Failed to join group."); Alert.alert("Error", res.message || "Failed to join group.");
// } }
} catch (error) { } catch (error) {
console.error("Join Group Error:", error); console.error("Join Group Error:", error);
Alert.alert("Error", "An unexpected error occurred."); Alert.alert("Error", "An unexpected error occurred.");
@@ -264,13 +283,13 @@ export default function GroupScreen() {
refreshControl={ refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} /> <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
} }
// ListFooterComponent={<ActivityIndicator size={'large'} color={colors.Group} />} // ListFooterComponent={<ActivityIndicator size={'large'} color={colors.Group} />}
/> />
); );
}; };
return ( return (
<AppView paddingHorizontal={0} style={styles.container}> <AppView paddingHorizontal={0}>
{/* Fixed Header */} {/* Fixed Header */}
<View style={styles.headerContainer}> <View style={styles.headerContainer}>
<Text style={styles.title}>Explore Groups</Text> <Text style={styles.title}>Explore Groups</Text>
@@ -282,21 +301,11 @@ export default function GroupScreen() {
{/* Scrollable Content */} {/* Scrollable Content */}
<ScrollView style={styles.container}> <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.tabHeader}>
<View style={styles.segmentedControl}> <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)}> <TouchableOpacity key={tab} style={[styles.segmentButton, activeTab === tab && styles.segmentButtonActive]} onPress={() => setActiveTab(tab)}>
<Text style={[styles.segmentText, activeTab === tab && styles.segmentTextActive]}>{tab}</Text> <Text style={[styles.segmentText, activeTab === tab && styles.segmentTextActive]}>{tab}</Text>
</TouchableOpacity> </TouchableOpacity>
@@ -312,7 +321,7 @@ export default function GroupScreen() {
</View> </View>
{/* The list now renders inside a simple View, as FlatList handles its own scroll */} {/* 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> </ScrollView>
{role === "ORGANIZER" && ( {role === "ORGANIZER" && (
<FAB style={styles.fab} icon={() => <MaterialIcons name="group-add" size={24} color="#333" />} onPress={() => navigation.navigate('GroupCreation')} /> <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({ const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f5f6fa' },
headerContainer: { backgroundColor: '#f5f6fa', paddingHorizontal: 16 }, headerContainer: { backgroundColor: '#f5f6fa', paddingHorizontal: 16 },
title: { fontSize: 22, fontWeight: "600", color: colors.Group }, 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 }, searchIcon: { marginRight: 8 },
searchInput: { flex: 1, height: 45, fontSize: 16, color: '#333' }, searchInput: { flex: 1, height: 45, fontSize: 16, color: '#333' },
sectionTitle: { fontSize: 20, fontWeight: 'bold', color: colors.Group, marginBottom: 12, paddingHorizontal: 16 }, 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' }, featuredName: { fontSize: 14, fontWeight: 'bold', color: '#333' },
featuredCreator: { fontSize: 12, color: '#777', marginTop: 2 }, featuredCreator: { fontSize: 12, color: '#777', marginTop: 2 },
tabHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, marginBottom: 16 }, 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' }, 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' }, segmentText: { fontSize: 14, fontWeight: '600', color: '#555' },
segmentTextActive: { color: '#333' }, segmentTextActive: { color: '#333' },
layoutToggleButton: { padding: 8 }, 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 }, 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 }, listCard: { backgroundColor: colors.pastelGreen, padding: 16, borderRadius: 20, marginBottom: 12,marginHorizontal:15, elevation: 5 },
gridCard: { backgroundColor: '#fff', padding: 12, borderRadius: 12, marginBottom: 12, elevation: 1, width: (width - 48) / 2 }, 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' }, groupName: { fontSize: 16, fontWeight: 'bold', color: '#333', width: '50%' },
groupDescription: { fontSize: 14, color: '#666', marginVertical: 4 }, 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 }, 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' }, groupMetaText: { fontSize: 12, color: '#888', fontWeight: '500', textTransform: 'capitalize' },
emptyText: { textAlign: 'center', marginTop: 40, fontSize: 16, color: '#888' }, 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 }, buttonContainer: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8 },
viewButton: { flex: 1, backgroundColor: '#e9ecef', paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginRight: 5 }, viewButton: { flex: 1, backgroundColor: '#e9ecef', paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginRight: 5 },
viewButtonText: { color: '#333', fontWeight: 'bold', fontSize: 14 }, 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' }, joinButtonDisabled: { backgroundColor: '#bdc3c7' },
joinButtonText: { color: '#333', fontWeight: 'bold', fontSize: 14 }, joinButtonText: { color: '#333', fontWeight: 'bold', fontSize: 14 },
}); });
+2 -2
View File
@@ -52,7 +52,7 @@ const InviteScreen: FC<InviteScreenProps> = ({ route }) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { 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) { if (response.status === 'success' && response.data) {
setGroupDetails(response.data); setGroupDetails(response.data);
} else { } else {
@@ -76,7 +76,7 @@ const InviteScreen: FC<InviteScreenProps> = ({ route }) => {
try { try {
// Replace with your actual API call to join the group // Replace with your actual API call to join the group
// For now, it just shows an alert. // 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}".`); Alert.alert('Success!', `You have requested to join "${groupDetails.groupName}".`);
} catch (e: any) { } catch (e: any) {
Alert.alert('Join Error', e.message || 'An unexpected error occurred.'); 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 { TabNavigationProp } from "../../navigation/BottomTabs";
import UserProfileStatus from "./UserProfileStatus"; import UserProfileStatus from "./UserProfileStatus";
import AnimatedCard from "../../components/common/AnimatedCard"; 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"; import { apiCall } from "../../utils/ApiHelper";
if (Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental) { 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: "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: "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: "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[] = [ const userActions: ActionItem[] = [
@@ -90,6 +91,8 @@ const Home: React.FC = () => {
navigation.navigate('Management', { actionTab: item.tab }); navigation.navigate('Management', { actionTab: item.tab });
} else if (item.action === "TRANSACTION") { } else if (item.action === "TRANSACTION") {
navigation.navigate("TransactionList" as never); navigation.navigate("TransactionList" as never);
} else if (item.action === "NAVIGATE_CREATE_GROUP") {
navigation.navigate("GroupCreation" as never);
} else { } else {
Alert.alert("Unknown action", item.title); Alert.alert("Unknown action", item.title);
} }
@@ -158,7 +161,7 @@ const styles = StyleSheet.create({
}, },
greeting: { greeting: {
fontSize: 22, fontSize: 22,
color: colors.textSecondary, color: colors.textDark,
}, },
userName: { userName: {
fontSize: 28, fontSize: 28,
+3 -3
View File
@@ -58,7 +58,7 @@ const RequestList: React.FC = () => {
setError(null); setError(null);
const response = await fetch( 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", method: "GET",
headers: { headers: {
@@ -124,7 +124,7 @@ const RequestList: React.FC = () => {
// Make API call to approve the request // Make API call to approve the request
const response = await fetch( 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", method: "PUT",
headers: { headers: {
@@ -170,7 +170,7 @@ const RequestList: React.FC = () => {
// Make API call to decline the request // Make API call to decline the request
const response = await fetch( 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", method: "PUT",
headers: { headers: {
+3 -2
View File
@@ -37,8 +37,9 @@ const GroupDetails = () => {
const [memberData, setMemberData] = useState<Member[]>([]); const [memberData, setMemberData] = useState<Member[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [openModal, setOpenModal] = 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 // Group object
const group = { const group = {
id: route.params.item?.id, id: route.params.item?.id,
@@ -108,7 +109,7 @@ const GroupDetails = () => {
const confirmPayment = async () => { const confirmPayment = async () => {
try { 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) console.log("payment response : ", res)
if (res.status) { if (res.status) {
ToastAndroid.show('Payment done!', ToastAndroid.SHORT) ToastAndroid.show('Payment done!', ToastAndroid.SHORT)
+131 -86
View File
@@ -61,7 +61,7 @@ export interface GroupDetails {
organizerId: string; organizerId: string;
totalParticipants: number; totalParticipants: number;
participants: Participant[]; participants: Participant[];
imageBlob:string imageBlob: string
} }
@@ -139,7 +139,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { 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) console.log("groupDetails", response)
if (response.status === 'success' && response.data) { if (response.status === 'success' && response.data) {
setGroupDetails(response.data); setGroupDetails(response.data);
@@ -154,7 +154,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
} }
}, [groupId]); }, [groupId]);
const getPaymentStatus = async()=>{ 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) console.log("paymentStatus",response)
if (response.status === 'success') { if (response.status === 'success') {
setPaymentStatus(response.data?.allPaid || false); setPaymentStatus(response.data?.allPaid || false);
@@ -188,7 +188,7 @@ const getPaymentStatus = async()=>{
setSaving(true); setSaving(true);
try { 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.'); Alert.alert('Success', 'Group has been updated.');
setEditingCard(null); setEditingCard(null);
await fetchGroupDetails(); await fetchGroupDetails();
@@ -199,7 +199,7 @@ const getPaymentStatus = async()=>{
} }
}; };
const handleDistribute = 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 () => { const handleStartGroup = async () => {
@@ -216,7 +216,7 @@ const getPaymentStatus = async()=>{
onPress: async () => { onPress: async () => {
setStarting(true); setStarting(true);
try { 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.'); Alert.alert('Success', 'Group has been started successfully.');
await fetchGroupDetails(); await fetchGroupDetails();
} catch (e: any) { } catch (e: any) {
@@ -235,7 +235,7 @@ const getPaymentStatus = async()=>{
if (!groupDetails) return; if (!groupDetails) return;
setInviting(true); setInviting(true);
try { 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; const inviteLink = response.data;
@@ -354,82 +354,127 @@ const getPaymentStatus = async()=>{
const isInvitable = groupDetails.status === 'APPROVED'; const isInvitable = groupDetails.status === 'APPROVED';
const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants; const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants;
// 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 = () => { const pickImage = () => {
launchImageLibrary({ mediaType: "photo", quality: 0.5 }, (response) => { launchImageLibrary({ mediaType: "photo", quality: 0.5 }, (response) => {
if (response.didCancel) return; if (response.didCancel) return;
if (response.errorCode) { if (response.errorCode) {
Alert.alert("Error", response.errorMessage || "Something went wrong"); Alert.alert("Error", response.errorMessage || "Something went wrong");
} else if (response.assets && response.assets.length > 0) { return;
const selectedImage = response.assets[0];
console.log(selectedImage?.uri)
setSelectedImage(selectedImage?.uri)
} }
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 () => { const handleUpdateImage = async () => {
try { try {
const credentials = await Keychain.getGenericPassword(); const credentials = await Keychain.getGenericPassword();
if (!credentials) { if (!credentials) {
Alert.alert("Error", "You are not logged in"); Alert.alert("Error", "You are not logged in");
return; 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);
} }
const token = credentials.password; };
if (!selectedImage) { const getProfileImage = () => {
Alert.alert("Error", "Please select an image first"); return selectedImage
return; ? selectedImage
} : groupDetails?.imageBlob
? `data:image/jpeg;base64,${groupDetails.imageBlob}`
setImageUpdating(true); : "https://img.freepik.com/premium-vector/people-group-frame-icon_791764-2649.jpg";
};
// Get file extension and mime type dynamically // Helper function to get MIME type
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
return ( return (
@@ -461,12 +506,12 @@ const getProfileImage = () => {
<Ionicons name="camera" size={18} color="#000" /> <Ionicons name="camera" size={18} color="#000" />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={{justifyContent:'center'}}> <View style={{ justifyContent: 'center' }}>
<CustomButton <CustomButton
title={imageUpdating ? "Updating..." : "Update Image"} title={imageUpdating ? "Updating..." : "Update Image"}
onPress={handleUpdateImage} onPress={handleUpdateImage}
icon={imageUpdating ? undefined : "cloud-upload"} icon={imageUpdating ? undefined : "cloud-upload"}
style={{paddingHorizontal:20}} style={{ paddingHorizontal: 20 }}
buttonBackground={colors.pastelBlue} buttonBackground={colors.pastelBlue}
/> />
</View> </View>
@@ -478,18 +523,18 @@ const getProfileImage = () => {
{groupDetails.status === 'ACTIVE' ? {groupDetails.status === 'ACTIVE' ?
renderCard('Financial Rules', [ renderCard('Financial Rules', [
{ field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` }, { field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
{ field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` }, { field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
{ field: 'maxParticipants', label: 'Current Participants', displayValue: () => `${groupDetails.totalParticipants}` }, { field: 'maxParticipants', label: 'Current Participants', displayValue: () => `${groupDetails.totalParticipants}` },
], 'financialRules') ], 'financialRules')
: :
renderCard('Financial Rules', [ renderCard('Financial Rules', [
{ field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` }, { field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
{ field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` }, { field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` },
{ field: 'minParticipants', label: 'Min Participants', displayValue: () => `${groupDetails.minParticipants}` }, { field: 'minParticipants', label: 'Min Participants', displayValue: () => `${groupDetails.minParticipants}` },
{ field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` }, { field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
], 'financialRules') ], 'financialRules')
} }
@@ -535,7 +580,7 @@ const getProfileImage = () => {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
)} )}
{paymentStatus && ( {paymentStatus && (
<View style={styles.floatingButtonContainer}> <View style={styles.floatingButtonContainer}>
<TouchableOpacity <TouchableOpacity
style={styles.activateButton} style={styles.activateButton}
+13 -20
View File
@@ -1,5 +1,4 @@
// src/screens/ManagementScreen.tsx // src/screens/ManagementScreen.tsx
import React, { useState, useCallback, FC, useEffect } from "react"; import React, { useState, useCallback, FC, useEffect } from "react";
import { import {
View, View,
@@ -16,13 +15,13 @@ import { APPROVE_REQUEST_URL, DECLINE_REQUEST_URL, GET_JOINED_GROUP_URL, MY_GROU
import colors from "../../components/colors"; import colors from "../../components/colors";
import AppView from "../../components/AppView"; import AppView from "../../components/AppView";
import { apiCall } from "../../utils/ApiHelper"; 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 { MyGroupCard } from "../../components/MyGroupCard";
import { JoinedGroupCard } from "../../components/JoinedGroupCard"; import { JoinedGroupCard } from "../../components/JoinedGroupCard";
import { RequestCard } from "../../components/RequestCard"; import { RequestCard } from "../../components/RequestCard";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { UserRequested } from "../../components/UserRequested"; import { UserRequested } from "../../components/UserRequested";
import { RootTabParamList } from "../../navigation/BottomTabs"; // Adjust import path import { RootTabParamList } from "../../navigation/BottomTabs";
import { RootStackParamList } from "../../navigation/AppNavigator"; import { RootStackParamList } from "../../navigation/AppNavigator";
// --- TYPE DEFINITIONS --- // --- TYPE DEFINITIONS ---
@@ -34,7 +33,6 @@ const ManagementScreen: FC = () => {
const user = useSelector((state: any) => state.user); const user = useSelector((state: any) => state.user);
const navigation = useNavigation<GroupManagementNavProp>(); const navigation = useNavigation<GroupManagementNavProp>();
const route = useRoute<ManagementRouteProp>(); const route = useRoute<ManagementRouteProp>();
const [activeTab, setActiveTab] = useState<ActiveTab>(user.role === "USER" ? "Joined Groups" : "My Groups"); const [activeTab, setActiveTab] = useState<ActiveTab>(user.role === "USER" ? "Joined Groups" : "My Groups");
const [myGroups, setMyGroups] = useState<MyGroup[]>([]); const [myGroups, setMyGroups] = useState<MyGroup[]>([]);
const [joinedGroups, setJoinedGroups] = useState<JoinedGroup[]>([]); const [joinedGroups, setJoinedGroups] = useState<JoinedGroup[]>([]);
@@ -43,9 +41,7 @@ const ManagementScreen: FC = () => {
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// --- HOOKS ---
// Effect to set the initial tab from route params
useEffect(() => { useEffect(() => {
const passedTab = route.params?.actionTab; const passedTab = route.params?.actionTab;
if (passedTab) { if (passedTab) {
@@ -56,9 +52,9 @@ const ManagementScreen: FC = () => {
} }
}, [route.params?.actionTab, user.role]); }, [route.params?.actionTab, user.role]);
// --- FIXED: fetchData dependency array is corrected ---
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
// Check for loading state at the start to prevent multiple simultaneous fetches
if (loading) return; if (loading) return;
setLoading(true); setLoading(true);
@@ -87,20 +83,18 @@ const ManagementScreen: FC = () => {
} finally { } finally {
setLoading(false); 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( useFocusEffect(
useCallback(() => { useCallback(() => {
fetchData(); 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) => { 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) => { 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 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 }); const handleManageGroup = (group: MyGroup) => navigation.navigate('ManagementTabs', { groupId: group.id });
// --- RENDER LOGIC ---
const renderContent = () => { const renderContent = () => {
if (error) return <Text style={styles.emptyText}>{error}</Text>; 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) { if (loading && myGroups.length === 0 && joinedGroups.length === 0 && requests.length === 0) {
return <ActivityIndicator size="large" color={colors.primary} style={styles.loader} />; return <ActivityIndicator size="large" color={colors.primary} style={styles.loader} />;
} }
@@ -168,12 +161,12 @@ const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f5f6fa' }, container: { flex: 1, backgroundColor: '#f5f6fa' },
header: { paddingHorizontal: 20, paddingBottom: 15, paddingTop: 10 }, header: { paddingHorizontal: 20, paddingBottom: 15, paddingTop: 10 },
title: { fontSize: 22, fontWeight: "600", color: colors.Settings }, 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 }, 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' }, segmentText: { fontSize: 14, fontWeight: '600', color: '#6c757d' },
segmentTextActive: { color: colors.textDark }, segmentTextActive: { color: colors.textDark },
contentArea: { flex: 1, paddingHorizontal: 20 }, contentArea: { flex: 1},
loader: { marginTop: 50 }, loader: { marginTop: 50 },
emptyText: { textAlign: 'center', marginTop: 60, fontSize: 16, color: '#6c757d' }, 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 { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { View, Text, StyleSheet, Platform, TouchableOpacity } from 'react-native'; import { View, Text, StyleSheet, Platform, TouchableOpacity } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; 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 { NativeStackScreenProps } from '@react-navigation/native-stack';
import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path
@@ -28,7 +28,7 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) =
console.log("option : ", options) console.log("option : ", options)
// A custom option to pass the icon name // 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; const isFocused = state.index === index;
@@ -100,7 +100,7 @@ const ManagementTabNavigation: React.FC<Props> = ({ route }) => {
}} }}
/> />
<Tab.Screen <Tab.Screen
name="Payments" name="Payments"
component={Payments} component={Payments}
// Pass the groupId to the EditGroupScreen as an initial parameter // Pass the groupId to the EditGroupScreen as an initial parameter
@@ -111,11 +111,11 @@ const ManagementTabNavigation: React.FC<Props> = ({ route }) => {
}} }}
/> />
<Tab.Screen <Tab.Screen
name="Member" name="Member"
component={Member} component={Member}
// Pass the groupId to the EditGroupScreen as an initial parameter // Pass the groupId to the EditGroupScreen as an initial parameter
initialParams={{ groupId }} initialParams={{ groupId }}
options={{ options={{
tabBarLabel: 'Members', tabBarLabel: 'Members',
+1 -1
View File
@@ -56,7 +56,7 @@ const MemberScreen = ({ route }) => {
setLoading(true); setLoading(true);
try { try {
const response = await apiCall<ApiResponse>( 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' 'GET'
); );
+18 -1
View File
@@ -7,7 +7,7 @@ import AppView from '../../components/AppView';
import { apiCall } from '../../utils/ApiHelper'; import { apiCall } from '../../utils/ApiHelper';
// --- CONSTANTS --- // --- 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 --- // --- TYPE DEFINITIONS ---
interface ApiResponse<T> { interface ApiResponse<T> {
@@ -54,6 +54,21 @@ const Payments: React.FC = () => {
const [isModalVisible, setModalVisible] = useState(false); const [isModalVisible, setModalVisible] = useState(false);
const [currentParticipant, setCurrentParticipant] = useState<Participant | null>(null); const [currentParticipant, setCurrentParticipant] = useState<Participant | null>(null);
const [waiverAmount, setWaiverAmount] = useState(''); 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 () => { const fetchPaymentDetails = useCallback(async () => {
if (!refreshing) setLoading(true); if (!refreshing) setLoading(true);
@@ -73,11 +88,13 @@ const Payments: React.FC = () => {
}, [groupId, refreshing]); }, [groupId, refreshing]);
useEffect(() => { useEffect(() => {
getCycleData();
fetchPaymentDetails(); fetchPaymentDetails();
}, [fetchPaymentDetails]); }, [fetchPaymentDetails]);
const onRefresh = useCallback(() => { const onRefresh = useCallback(() => {
setRefreshing(true); setRefreshing(true);
getCycleData();
fetchPaymentDetails(); fetchPaymentDetails();
}, [fetchPaymentDetails]); }, [fetchPaymentDetails]);
@@ -1,26 +1,30 @@
import React, { useState, useEffect, useRef } from 'react'; 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 { apiCall } from '../../utils/ApiHelper';
import { Ionicons } from "@react-native-vector-icons/ionicons"; import { Ionicons } from "@react-native-vector-icons/ionicons";
import { useNavigation } from '@react-navigation/native';
import ContributionDetails from '../ContributionScreen';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
const NUM_COLUMNS = 3; const NUM_COLUMNS = 3;
const ITEM_SIZE = width / NUM_COLUMNS - 20; const ITEM_SIZE = width / NUM_COLUMNS - 20;
const RandomSelectUserScreen: React.FC<{ route: any }> = ({ route }) => { const RandomSelectUserScreen: React.FC<{ route: any }> = ({ route }) => {
const navigation = useNavigation() as never
const { groupId, cycleNumber } = route.params; const { groupId, cycleNumber } = route.params;
const [users, setUsers] = useState<{ id: string; name: string }[]>([]); const [users, setUsers] = useState<{ id: string; name: string }[]>([]);
const [highlightedIndex, setHighlightedIndex] = useState<number | null>(null); const [highlightedIndex, setHighlightedIndex] = useState<number | null>(null);
const [winner, setWinner] = useState<{ id: string; name: string } | null>(null); const [winner, setWinner] = useState<{ id: string; name: string } | null>(null);
const [isAnimating, setIsAnimating] = useState(false); const [isAnimating, setIsAnimating] = useState(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [openModal, setOpenModal] = useState(false)
const scaleAnim = useRef(new Animated.Value(1)).current; const scaleAnim = useRef(new Animated.Value(1)).current;
useEffect(() => { useEffect(() => {
const fetchParticipants = async () => { const fetchParticipants = async () => {
try { 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) { if (response.status === 'success' && response.data) {
const formattedUsers = response.data.map((u: any) => ({ const formattedUsers = response.data.map((u: any) => ({
id: u.id, id: u.id,
@@ -73,10 +77,10 @@ const RandomSelectUserScreen: React.FC<{ route: any }> = ({ route }) => {
const fetchWinner = async () => { const fetchWinner = async () => {
const payload = { groupId: groupId }; const payload = { groupId: groupId };
try { try {
const response = await apiCall('https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/method/based/userSelect', 'POST', payload) as any; 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?.id) { if (response.status === 'success' && response.data?.selectedUser?.id) {
const winnerData = { id: response.data.id, name: `${response.data.firstName} ${response.data.lastName}` }; 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); const winnerIndex = users.findIndex(u => u.id === winnerData.id);
if (winnerIndex !== -1) { 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.'); Alert.alert('Error', 'Failed to determine a winner from the API.');
} }
} catch (error) { } 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 { } finally {
setIsAnimating(false); setIsAnimating(false);
} }
@@ -147,9 +151,16 @@ const RandomSelectUserScreen: React.FC<{ route: any }> = ({ route }) => {
<View style={styles.footer}> <View style={styles.footer}>
{winner && !isAnimating && ( {winner && !isAnimating && (
<View style={styles.winnerTextContainer}> <View style={styles.winnerTextContainer}>
<Text style={styles.winnerText}>🎉 Winner: {winner.name}</Text> <Text style={styles.winnerText}>🎉 Winner: {winner.name}</Text>
</View> </View>
)} )}
<Button title='Pay' onPress={() => setOpenModal(true)} />
{
openModal && <ContributionDetails data={undefined} visible={openModal} onClose={() => setOpenModal(false)} onConfirm={() => setOpenModal(false)} />
}
<TouchableOpacity <TouchableOpacity
style={[styles.button, isAnimating && styles.buttonDisabled]} style={[styles.button, isAnimating && styles.buttonDisabled]}
onPress={handleStartSelection} onPress={handleStartSelection}
@@ -181,9 +192,9 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
}, },
header: { header: {
paddingTop: 50, paddingTop: 50,
paddingBottom: 20, paddingBottom: 20,
alignItems: 'center' alignItems: 'center'
}, },
title: { title: {
fontSize: 32, fontSize: 32,
@@ -191,9 +202,9 @@ const styles = StyleSheet.create({
color: '#111827', color: '#111827',
}, },
subtitle: { subtitle: {
fontSize: 18, fontSize: 18,
color: '#6b7280', color: '#6b7280',
marginTop: 4 marginTop: 4
}, },
grid: { grid: {
paddingHorizontal: 10, paddingHorizontal: 10,
@@ -211,8 +222,8 @@ const styles = StyleSheet.create({
padding: 8, padding: 8,
shadowColor: "#000", shadowColor: "#000",
shadowOffset: { shadowOffset: {
width: 0, width: 0,
height: 2, height: 2,
}, },
shadowOpacity: 0.1, shadowOpacity: 0.1,
shadowRadius: 3.84, shadowRadius: 3.84,
@@ -237,12 +248,12 @@ const styles = StyleSheet.create({
color: '#000000ff', color: '#000000ff',
}, },
footer: { footer: {
width: '100%', width: '100%',
padding: 20, padding: 20,
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: '#e5e7eb', borderTopColor: '#e5e7eb',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#fff' backgroundColor: '#fff'
}, },
button: { button: {
paddingVertical: 16, paddingVertical: 16,
@@ -252,8 +263,8 @@ const styles = StyleSheet.create({
elevation: 3, elevation: 3,
shadowColor: "#4f46e5", shadowColor: "#4f46e5",
shadowOffset: { shadowOffset: {
width: 0, width: 0,
height: 4, height: 4,
}, },
shadowOpacity: 0.3, shadowOpacity: 0.3,
shadowRadius: 4.65, shadowRadius: 4.65,
+1 -1
View File
@@ -43,7 +43,7 @@ const BecomeOrg = () => {
const token = credentials.password; const token = credentials.password;
const response = await fetch( 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", method: "POST",
headers: { headers: {
+96 -97
View File
@@ -369,7 +369,7 @@ const Profile = () => {
return ( return (
<AppView> <AppView paddingHorizontal={5} >
{/* Header */} {/* Header */}
<View style={styles.header}> <View style={styles.header}>
<Text style={styles.headerTitle}>User Profile</Text> <Text style={styles.headerTitle}>User Profile</Text>
@@ -425,7 +425,6 @@ const Profile = () => {
) )
} }
<View style={styles.profileImageSection}> <View style={styles.profileImageSection}>
<View style={styles.avatarContainer}> <View style={styles.avatarContainer}>
<Image <Image
@@ -472,12 +471,13 @@ const Profile = () => {
onPress={() => setEditing(true)} onPress={() => setEditing(true)}
icon="create" icon="create"
style={styles.editButton} style={styles.editButton}
buttonBackground={colors.pastelBlue} buttonBackground={colors.white}
/> />
</View> </View>
</View> </View>
<View style ={{backgroundColor:colors.pastelBlue,borderRadius:20,elevation:5,paddingVertical:10}}>
<View style={styles.detailRow}> <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> <View>
<Text style={styles.detailLabel}>Name</Text> <Text style={styles.detailLabel}>Name</Text>
<Text style={styles.detailText}>{user?.firstName} {user?.lastName}</Text> <Text style={styles.detailText}>{user?.firstName} {user?.lastName}</Text>
@@ -486,7 +486,7 @@ const Profile = () => {
<View style={styles.detailRow}> <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> <View>
<Text style={styles.detailLabel}>Email</Text> <Text style={styles.detailLabel}>Email</Text>
<Text style={styles.detailText}>{user?.email || "Not provided"}</Text> <Text style={styles.detailText}>{user?.email || "Not provided"}</Text>
@@ -495,7 +495,7 @@ const Profile = () => {
<View style={styles.detailRow}> <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> <View>
<Text style={styles.detailLabel}>Mobile</Text> <Text style={styles.detailLabel}>Mobile</Text>
<Text style={styles.detailText}>{user?.mobile || "Not provided"}</Text> <Text style={styles.detailText}>{user?.mobile || "Not provided"}</Text>
@@ -504,16 +504,16 @@ const Profile = () => {
<View style={styles.detailRow}> <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> <View>
<Text style={styles.detailLabel}>Username</Text> <Text style={styles.detailLabel}>Usernames</Text>
<Text style={styles.detailText}>{user?.username}</Text> <Text style={styles.detailText}>{user?.username}</Text>
</View> </View>
</View> </View>
<View style={styles.detailRow}> <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> <View>
<Text style={styles.detailLabel}>Address</Text> <Text style={styles.detailLabel}>Address</Text>
<Text style={styles.detailText}>{user?.address || "Not provided"}</Text> <Text style={styles.detailText}>{user?.address || "Not provided"}</Text>
@@ -522,7 +522,7 @@ const Profile = () => {
<View style={styles.detailRow}> <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> <View>
<Text style={styles.detailLabel}>Gender</Text> <Text style={styles.detailLabel}>Gender</Text>
<Text style={styles.detailText}>{user?.gender || "Not specified"}</Text> <Text style={styles.detailText}>{user?.gender || "Not specified"}</Text>
@@ -531,18 +531,19 @@ const Profile = () => {
<View style={styles.detailRow}> <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> <View>
<Text style={styles.detailLabel}>Role</Text> <Text style={styles.detailLabel}>Role</Text>
<Text style={styles.detailText}>{user?.role || "User"}</Text> <Text style={styles.detailText}>{user?.role || "User"}</Text>
</View> </View>
</View> </View>
</View>
{/* BANK DETAILS SECTION */} {/* BANK DETAILS SECTION */}
<View style={{ marginTop: 20 }}> <View style={{ marginTop: 20 }}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}> <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={styles.sectionTitle}>Bank Information</Text> <Text style={styles.sectionTitle}>Bank Information</Text>
{/* Navigate to a screen you will create for editing bank details */} {/* Navigate to a screen you will create for editing bank details */}
<CustomButton <CustomButton
title="Edit Bank Details" title="Edit Bank Details"
onPress={() =>{ navigation.navigate("EditBankDetails", { onPress={() =>{ navigation.navigate("EditBankDetails", {
@@ -553,120 +554,115 @@ const Profile = () => {
icon="create-outline" icon="create-outline"
style={styles.editButton} 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}> <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> <View>
<Text style={styles.detailLabel}>Bank Name</Text> <Text style={styles.detailLabel}>Bank Name</Text>
<Text style={styles.detailText}>{user?.bankName || "Not provided"}</Text> <Text style={styles.detailText}>{user?.bankName || "Not provided"}</Text>
</View> </View>
</View> </View>
<View style={styles.detailRow}> <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> <View>
<Text style={styles.detailLabel}>Branch Code</Text> <Text style={styles.detailLabel}>Branch Code</Text>
<Text style={styles.detailText}>{user?.branchCode || "Not provided"}</Text> <Text style={styles.detailText}>{user?.branchCode || "Not provided"}</Text>
</View> </View>
</View> </View>
<View style={styles.detailRow}> <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> <View>
<Text style={styles.detailLabel}>Account Number</Text> <Text style={styles.detailLabel}>Account Number</Text>
<Text style={styles.detailText}>{user?.bankAccountNumber || "Not provided"}</Text> <Text style={styles.detailText}>{user?.bankAccountNumber || "Not provided"}</Text>
</View> </View>
</View> </View>
</View>
</View> </View>
</View> </View>
</> </>
) : ( ) : (
<> <>
{/* Edit Form */} {/* Edit Form */}
<View style={styles.editSection}> <View style={styles.editSection}>
<Text style={styles.sectionTitle}>Edit Profile</Text> <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 <CustomInput
error="" error=""
label="First Name" label="Last Name"
placeholder="Enter first name" placeholder="Enter last name"
value={user?.firstName ?? ""} value={user?.lastName ?? ""}
onChangeText={(text) => setUser({ ...user, firstName: text })} onChangeText={(text) => setUser({ ...user, lastName: text })}
icon="person" icon="person"
/> />
<CustomInput
error=""
label="Email"
placeholder="Enter email"
value={user?.email ?? ""}
onChangeText={(text) => setUser({ ...user, email: text })}
icon="mail"
keyboardType="email-address"
/>
<CustomInput <CustomInput
error="" error=""
label="Last Name" label="Username"
placeholder="Enter last name" placeholder="Enter username"
value={user?.lastName ?? ""} value={user?.username ?? ""}
onChangeText={(text) => setUser({ ...user, lastName: text })} onChangeText={(text) => setUser({ ...user, username: text })}
icon="person" icon="at"
/> />
<CustomInput
error=""
label="Address"
placeholder="Enter address"
value={user?.address ?? ""}
onChangeText={(text) => setUser({ ...user, address: text })}
icon="location"
multiline
/>
<CustomInput <CustomInput
error="" error=""
label="Email" label="Gender"
placeholder="Enter email" placeholder="Enter gender"
value={user?.email ?? ""} value={user?.gender ?? ""}
onChangeText={(text) => setUser({ ...user, email: text })} onChangeText={(text) => setUser({ ...user, gender: text })}
icon="mail" icon="male-female"
keyboardType="email-address" />
/>
<View style={styles.editButtons}>
<CustomInput <CustomButton
error="" title="Save Changes"
label="Username" onPress={updateProfile}
placeholder="Enter username" icon="save"
value={user?.username ?? ""} style={styles.saveButton}
onChangeText={(text) => setUser({ ...user, username: text })} buttonBackground={colors.pastelBlue}
icon="at" />
/> <CustomButton
<CustomInput title="Cancel"
error="" onPress={() => setEditing(false)}
label="Address" icon="close"
placeholder="Enter address" style={styles.cancelButton}
value={user?.address ?? ""} buttonBackground={colors.white}
onChangeText={(text) => setUser({ ...user, address: text })} />
icon="location" </View>
multiline </View>
/> </>
<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> </View>
</ScrollView> </ScrollView>
@@ -775,10 +771,10 @@ const styles = StyleSheet.create({
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
marginBottom: 16, marginBottom: 16,
paddingHorizontal: 8, paddingHorizontal: 12,
}, },
detailIcon: { detailIcon: {
marginRight: 12, marginRight: 20,
elevation:5 elevation:5
}, },
detailLabel: { detailLabel: {
@@ -794,7 +790,10 @@ const styles = StyleSheet.create({
editButton: { editButton: {
elevation:5, elevation:5,
paddingHorizontal: 10, paddingHorizontal: 10,
paddingVertical:4,
borderRadius: 15, borderRadius: 15,
borderWidth:2,
borderColor:colors.pastelBlue
}, },
editSection: { editSection: {
marginBottom: '100%', 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 BASE_URL = "https://api.godemo.in/FundCollect/api/"
export const LOGIN_URL = `${BASE_URL}user/generate/otp?mobiles=` export const LOGIN_URL = `${BASE_URL}user/generate/otp?mobiles=`
export const VARIFY_OTP_URL = `${BASE_URL}user/otp/verify` export const VARIFY_OTP_URL = `${BASE_URL}user/otp/verify`