testing
This commit is contained in:
@@ -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'],
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+16
-1941
File diff suppressed because it is too large
Load Diff
@@ -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": {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
@@ -57,21 +60,14 @@ const CustomButton: React.FC<CustomButtonProps> = ({
|
|||||||
type = "primary",
|
type = "primary",
|
||||||
icon,
|
icon,
|
||||||
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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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('')
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ export default function GroupCreationForm() {
|
|||||||
>
|
>
|
||||||
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
|
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
|
||||||
<View style={styles.titleContainer}><Text style={styles.title}>Create New Group</Text></View>
|
<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="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} />
|
<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} />
|
||||||
|
|
||||||
|
|||||||
+57
-49
@@ -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,21 +171,21 @@ 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.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const userStatus = async () => {
|
const userStatus = async () => {
|
||||||
try {
|
try {
|
||||||
const credentials = await Keychain.getGenericPassword();
|
const credentials = await Keychain.getGenericPassword();
|
||||||
@@ -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 },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.');
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
const token = credentials.password;
|
||||||
|
|
||||||
if (!selectedImage) {
|
if (!selectedImage) {
|
||||||
Alert.alert("Error", "Please select an image first");
|
Alert.alert("Error", "Please select an image first");
|
||||||
return;
|
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);
|
const getProfileImage = () => {
|
||||||
|
return selectedImage
|
||||||
// Get file extension and mime type dynamically
|
? selectedImage
|
||||||
const fileExtension = selectedImage.split('.').pop();
|
: groupDetails?.imageBlob
|
||||||
|
? `data:image/jpeg;base64,${groupDetails.imageBlob}`
|
||||||
const formData = new FormData();
|
: "https://img.freepik.com/premium-vector/people-group-frame-icon_791764-2649.jpg";
|
||||||
formData.append("file", {
|
};
|
||||||
uri: selectedImage,
|
// Helper function to get MIME type
|
||||||
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>
|
||||||
@@ -477,19 +522,19 @@ const getProfileImage = () => {
|
|||||||
], 'groupInfo')}
|
], 'groupInfo')}
|
||||||
|
|
||||||
|
|
||||||
{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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) =
|
|||||||
|
|
||||||
// --- Main Tab Navigator Component ---
|
// --- Main Tab Navigator Component ---
|
||||||
const ManagementTabNavigation: React.FC<Props> = ({ route }) => {
|
const ManagementTabNavigation: React.FC<Props> = ({ route }) => {
|
||||||
const { groupId } = route.params
|
const { groupId } = route.params
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
@@ -97,10 +97,10 @@ const ManagementTabNavigation: React.FC<Props> = ({ route }) => {
|
|||||||
options={{
|
options={{
|
||||||
tabBarLabel: 'Settings',
|
tabBarLabel: 'Settings',
|
||||||
// Custom prop to define the icon for our FloatingTabBar
|
// Custom prop to define the icon for our FloatingTabBar
|
||||||
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<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,16 +111,16 @@ 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',
|
||||||
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
{/* You can easily add more tabs here in the future */}
|
{/* You can easily add more tabs here in the future */}
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
|
|||||||
@@ -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'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,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`
|
||||||
|
|||||||
Reference in New Issue
Block a user