explore Groups
This commit is contained in:
@@ -1,17 +1,23 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { StatusBar, StyleSheet, View } from "react-native";
|
import { StatusBar, StyleSheet, View, ViewStyle } from "react-native";
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
|
||||||
type AppViewProps = {
|
type AppViewProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
barStyle?: "default" | "light-content" | "dark-content";
|
barStyle?: "default" | "light-content" | "dark-content";
|
||||||
|
paddingHorizontal?: number;
|
||||||
|
paddingTop?: number;
|
||||||
|
style?: ViewStyle;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AppView({
|
export default function AppView({
|
||||||
children,
|
children,
|
||||||
backgroundColor = "#f5f5f5",
|
backgroundColor = "#f5f5f5",
|
||||||
barStyle = "dark-content",
|
barStyle = "dark-content",
|
||||||
|
paddingHorizontal = 16, // default
|
||||||
|
paddingTop = 8, // default
|
||||||
|
style,
|
||||||
}: AppViewProps) {
|
}: AppViewProps) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.safeArea, { backgroundColor }]}>
|
<SafeAreaView style={[styles.safeArea, { backgroundColor }]}>
|
||||||
@@ -20,7 +26,15 @@ export default function AppView({
|
|||||||
backgroundColor={backgroundColor}
|
backgroundColor={backgroundColor}
|
||||||
barStyle={barStyle}
|
barStyle={barStyle}
|
||||||
/>
|
/>
|
||||||
<View style={styles.container}>{children}</View>
|
<View
|
||||||
|
style={[
|
||||||
|
styles.container,
|
||||||
|
{ paddingHorizontal, paddingTop },
|
||||||
|
style, // user custom overrides
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -31,7 +45,5 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingTop: 8,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const TAB_CONFIG = {
|
|||||||
inactiveColor: "#8e8e93",
|
inactiveColor: "#8e8e93",
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "#fff",
|
||||||
height: 70,
|
height: 70,
|
||||||
iconSize: 26,
|
iconSize: 28,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Define a color map for active tabs
|
// 1. Define a color map for active tabs
|
||||||
@@ -28,7 +28,7 @@ const activeIconColors: { [key: string]: string } = {
|
|||||||
Home: "#333",
|
Home: "#333",
|
||||||
Profile: "#333",
|
Profile: "#333",
|
||||||
Group: "#333",
|
Group: "#333",
|
||||||
Settings: "#e17055",
|
Settings: "#333",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -128,14 +128,14 @@ const styles = StyleSheet.create({
|
|||||||
marginHorizontal: 10,
|
marginHorizontal: 10,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
|
paddingVertical: 5,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
marginBottom: 5,
|
|
||||||
},
|
},
|
||||||
iconContainer: {
|
iconContainer: {
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
width: 50, // Reduced size for a sleeker look
|
width: 80, // Reduced size for a sleeker look
|
||||||
height: 50, // Reduced size
|
height: 60, // Reduced size
|
||||||
marginTop: 10,
|
marginTop: 10,
|
||||||
borderRadius: 15,
|
borderRadius: 15,
|
||||||
},
|
},
|
||||||
|
|||||||
+122
-156
@@ -1,15 +1,79 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from "react-native";
|
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Modal, ActivityIndicator } from "react-native";
|
||||||
import { Ionicons } from "@react-native-vector-icons/ionicons";
|
import { Ionicons } from "@react-native-vector-icons/ionicons";
|
||||||
import AppView from "../components/AppView";
|
import AppView from "../components/AppView";
|
||||||
import { apiCall } from "../utils/ApiHelper";
|
import { apiCall } from "../utils/ApiHelper";
|
||||||
import { GROUP_CREATION_URL } from "../utils/ApiUrls";
|
import { GROUP_CREATION_URL } from "../utils/ApiUrls";
|
||||||
import CustomButton from "../components/CustomButton";
|
|
||||||
|
// --- Simplified Dialog Component ---
|
||||||
|
const ThemedDialog = ({ visible, isSuccess, onClose }: { visible: boolean; isSuccess: boolean; onClose: () => void; }) => {
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
const dialogConfig = {
|
||||||
|
success: {
|
||||||
|
icon: "checkmark-circle" as const,
|
||||||
|
color: "#2ecc71",
|
||||||
|
title: "Success!",
|
||||||
|
message: "Your group has been created.",
|
||||||
|
buttonText: "Awesome!",
|
||||||
|
},
|
||||||
|
failure: {
|
||||||
|
icon: "close-circle" as const,
|
||||||
|
color: "#e74c3c",
|
||||||
|
title: "Oops!",
|
||||||
|
message: "Something went wrong. Please try again.",
|
||||||
|
buttonText: "Try Again",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = isSuccess ? dialogConfig.success : dialogConfig.failure;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal animationType="fade" transparent={true} visible={visible} onRequestClose={onClose}>
|
||||||
|
<View style={styles.modalBackdrop}>
|
||||||
|
<View style={styles.dialogBox}>
|
||||||
|
<Ionicons name={config.icon} size={60} color={config.color} />
|
||||||
|
<Text style={styles.dialogTitle}>{config.title}</Text>
|
||||||
|
<Text style={styles.dialogMessage}>{config.message}</Text>
|
||||||
|
{/* Only show button for failure case */}
|
||||||
|
{!isSuccess && (
|
||||||
|
<TouchableOpacity style={styles.dialogButton} onPress={onClose}>
|
||||||
|
<Text style={styles.dialogButtonText}>{config.buttonText}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function GroupReview({ route, navigation }: any) {
|
export default function GroupReview({ route, navigation }: any) {
|
||||||
const { form } = route.params;
|
const { form } = route.params;
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
// --- Simplified State ---
|
||||||
|
const [isSuccess, setIsSuccess] = useState(false);
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
|
||||||
|
// --- Automatic Navigation Effect ---
|
||||||
|
useEffect(() => {
|
||||||
|
// If the modal is visible and the operation was a success...
|
||||||
|
if (modalVisible && isSuccess) {
|
||||||
|
// ...wait for 2 seconds, then navigate.
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setModalVisible(false); // Close the dialog
|
||||||
|
navigation.reset({ // Reset the navigation stack
|
||||||
|
index: 0,
|
||||||
|
routes: [{ name: 'Home',}],
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer); // Cleanup timer on unmount
|
||||||
|
}
|
||||||
|
}, [modalVisible, isSuccess, navigation]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
...form,
|
...form,
|
||||||
contributionAmount: Number(form.contributionAmount) || 0,
|
contributionAmount: Number(form.contributionAmount) || 0,
|
||||||
@@ -17,35 +81,24 @@ export default function GroupReview({ route, navigation }: any) {
|
|||||||
maxParticipants: Number(form.maxParticipants) || 0,
|
maxParticipants: Number(form.maxParticipants) || 0,
|
||||||
minParticipants: Number(form.minParticipants) || 0,
|
minParticipants: Number(form.minParticipants) || 0,
|
||||||
};
|
};
|
||||||
await apiCall(GROUP_CREATION_URL, "POST", payload);
|
const res = await apiCall(GROUP_CREATION_URL, "POST", payload) as { statusCode?: number };
|
||||||
|
|
||||||
|
if (res?.statusCode === 201) {
|
||||||
|
setIsSuccess(true);
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to create group.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setIsSuccess(false);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
setModalVisible(true); // Show the dialog for both success and failure
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const TimelineSection = ({ icon, title, children }: { icon: any; title: string; children: React.ReactNode }) => (
|
// --- UI COMPONENTS ---
|
||||||
<View style={styles.section}>
|
const TimelineSection = ({ icon, title, children }: { icon: any; title: string; children: React.ReactNode }) => ( <View style={styles.section}> <View style={styles.timeline}> <View style={styles.timelineIcon}> <Ionicons name={icon} size={22} color="#fff" /> </View> <View style={styles.timelineLine} /> </View> <View style={styles.content}> <Text style={styles.sectionTitle}>{title}</Text> <View style={styles.card}> {children} </View> </View> </View> );
|
||||||
<View style={styles.timeline}>
|
const renderRow = (label: string, value: string | number) => { if (!value && typeof value !== 'number') return null; return ( <View style={styles.row}> <Text style={styles.label}>{label}</Text> <Text style={styles.value}>{value}</Text> </View> ); };
|
||||||
<View style={styles.timelineIcon}>
|
|
||||||
<Ionicons name={icon} size={22} color="#fff" />
|
|
||||||
</View>
|
|
||||||
<View style={styles.timelineLine} />
|
|
||||||
</View>
|
|
||||||
<View style={styles.content}>
|
|
||||||
<Text style={styles.sectionTitle}>{title}</Text>
|
|
||||||
<View style={styles.card}>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderRow = (label: string, value: string | number) => {
|
|
||||||
if (!value && typeof value !== 'number') return null;
|
|
||||||
return (
|
|
||||||
<View style={styles.row}>
|
|
||||||
<Text style={styles.label}>{label}</Text>
|
|
||||||
<Text style={styles.value}>{value}</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppView>
|
<AppView>
|
||||||
@@ -55,137 +108,50 @@ export default function GroupReview({ route, navigation }: any) {
|
|||||||
<Text style={styles.subtitle}>Confirm the details to launch your group.</Text>
|
<Text style={styles.subtitle}>Confirm the details to launch your group.</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TimelineSection icon="information-circle-outline" title="Basic Information">
|
{/* --- Timeline Sections --- */}
|
||||||
{renderRow("Name", form.groupName)}
|
<TimelineSection icon="information-circle-outline" title="Basic Information">{renderRow("Name", form.groupName)}{form.description ? ( <View style={styles.descriptionContainer}> <Text style={styles.label}>Description</Text> <Text style={styles.descriptionText}>{form.description}</Text> </View> ) : null}</TimelineSection>
|
||||||
{form.description ? (
|
<TimelineSection icon="shield-checkmark-outline" title="Rules & Privacy">{renderRow("Visibility", form.visibility)}{renderRow("Distribution", form.fundDistribution)}{renderRow("Min Members", form.minParticipants)}{renderRow("Max Members", form.maxParticipants)}</TimelineSection>
|
||||||
<View style={styles.descriptionContainer}>
|
<TimelineSection icon="wallet-outline" title="Financial Details">{renderRow("Contribution", `${form.contributionAmount} INR`)}{renderRow("Cycle", form.cycleType)}{form.cycleType === "CUSTOM" && renderRow("Cycle Days", `${form.customCycleDays} days`)}{renderRow("Start Date", form.startDate)}</TimelineSection>
|
||||||
<Text style={styles.label}>Description</Text>
|
|
||||||
<Text style={styles.descriptionText}>{form.description}</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
</TimelineSection>
|
|
||||||
|
|
||||||
<TimelineSection icon="shield-checkmark-outline" title="Rules & Privacy">
|
<TouchableOpacity onPress={handleSubmit} style={styles.submitBtn} disabled={isLoading}>
|
||||||
{renderRow("Visibility", form.visibility)}
|
{isLoading ? <ActivityIndicator color="#333" /> : ( <> <Ionicons name="rocket-outline" size={22} color="#333" /> <Text style={styles.btnText}>Launch Group</Text> </> )}
|
||||||
{renderRow("Distribution", form.fundDistribution)}
|
</TouchableOpacity>
|
||||||
{renderRow("Min Members", form.minParticipants)}
|
|
||||||
{renderRow("Max Members", form.maxParticipants)}
|
|
||||||
</TimelineSection>
|
|
||||||
|
|
||||||
<TimelineSection icon="wallet-outline" title="Financial Details">
|
|
||||||
{renderRow("Contribution", `${form.contributionAmount} INR`)}
|
|
||||||
{renderRow("Cycle", form.cycleType)}
|
|
||||||
{form.cycleType === "CUSTOM" && renderRow("Cycle Days", `${form.customCycleDays} days`)}
|
|
||||||
{renderRow("Start Date", form.startDate)}
|
|
||||||
</TimelineSection>
|
|
||||||
<CustomButton title="Create Group" onPress={handleSubmit} />
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* --- Simplified Dialog Call --- */}
|
||||||
|
<ThemedDialog
|
||||||
|
visible={modalVisible}
|
||||||
|
isSuccess={isSuccess}
|
||||||
|
onClose={() => setModalVisible(false)}
|
||||||
|
/>
|
||||||
</AppView>
|
</AppView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
// ... (All previous styles remain the same)
|
||||||
padding: 20,
|
container: { padding: 20, backgroundColor: "#f5f6fa" },
|
||||||
backgroundColor: "#f5f6fa",
|
header: { marginBottom: 30, alignItems: 'flex-start' },
|
||||||
},
|
title: { fontSize: 28, fontWeight: "bold", color: "#333" },
|
||||||
header: {
|
subtitle: { fontSize: 16, color: "#666", marginTop: 2 },
|
||||||
marginBottom: 30,
|
section: { flexDirection: 'row', marginBottom: 10 },
|
||||||
alignItems: 'flex-start',
|
timeline: { alignItems: 'center', width: 40 },
|
||||||
},
|
timelineIcon: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#83f5cc', justifyContent: 'center', alignItems: 'center', elevation: 3 },
|
||||||
title: {
|
timelineLine: { flex: 1, width: 2, backgroundColor: '#e0e0e0' },
|
||||||
fontSize: 28,
|
content: { flex: 1, paddingLeft: 10, paddingBottom: 20 },
|
||||||
fontWeight: "bold",
|
sectionTitle: { fontSize: 18, fontWeight: 'bold', color: '#444', marginBottom: 10 },
|
||||||
color: "#333",
|
card: { backgroundColor: "#fff", borderRadius: 12, padding: 15, elevation: 2 },
|
||||||
},
|
row: { flexDirection: "row", justifyContent: "space-between", paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#f5f5f5' },
|
||||||
subtitle: {
|
label: { fontSize: 15, color: "#666", fontWeight: "500" },
|
||||||
fontSize: 16,
|
value: { fontSize: 15, color: "#333", fontWeight: "600", textAlign: 'right', textTransform: 'capitalize' },
|
||||||
color: "#666",
|
descriptionContainer: { paddingVertical: 10 },
|
||||||
marginTop: 2,
|
descriptionText: { fontSize: 15, color: '#333', marginTop: 4, lineHeight: 22 },
|
||||||
},
|
submitBtn: { backgroundColor: "#83f5cc", flexDirection: 'row', justifyContent: 'center', alignItems: "center", paddingVertical: 16, borderRadius: 12, elevation: 3, marginTop: 20, borderWidth: 1.5, borderColor: '#62b59a' },
|
||||||
section: {
|
btnText: { color: "#333", fontWeight: "bold", fontSize: 18, marginLeft: 10 },
|
||||||
flexDirection: 'row',
|
modalBackdrop: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.6)', justifyContent: 'center', alignItems: 'center' },
|
||||||
marginBottom: 10,
|
dialogBox: { width: '85%', backgroundColor: 'white', borderRadius: 16, padding: 25, alignItems: 'center', elevation: 10 },
|
||||||
},
|
dialogTitle: { fontSize: 22, fontWeight: 'bold', color: '#333', marginTop: 15, marginBottom: 5 },
|
||||||
timeline: {
|
dialogMessage: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 25 },
|
||||||
alignItems: 'center',
|
dialogButton: { backgroundColor: '#83f5cc', paddingVertical: 12, paddingHorizontal: 40, borderRadius: 10 },
|
||||||
width: 40,
|
dialogButtonText: { color: '#333', fontWeight: 'bold', fontSize: 16 },
|
||||||
},
|
|
||||||
timelineIcon: {
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
borderRadius: 20,
|
|
||||||
backgroundColor: '#83f5cc',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
elevation: 3,
|
|
||||||
},
|
|
||||||
timelineLine: {
|
|
||||||
flex: 1,
|
|
||||||
width: 2,
|
|
||||||
backgroundColor: '#e0e0e0',
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flex: 1,
|
|
||||||
paddingLeft: 10,
|
|
||||||
paddingBottom: 20,
|
|
||||||
},
|
|
||||||
sectionTitle: {
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: '#444',
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
card: {
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 15,
|
|
||||||
elevation: 2,
|
|
||||||
},
|
|
||||||
row: {
|
|
||||||
flexDirection: "row",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
paddingVertical: 10,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: '#f5f5f5',
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
fontSize: 15,
|
|
||||||
color: "#666",
|
|
||||||
fontWeight: "500",
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
fontSize: 15,
|
|
||||||
color: "#333",
|
|
||||||
fontWeight: "600",
|
|
||||||
textAlign: 'right',
|
|
||||||
textTransform: 'capitalize',
|
|
||||||
},
|
|
||||||
descriptionContainer: {
|
|
||||||
paddingVertical: 10,
|
|
||||||
},
|
|
||||||
descriptionText: {
|
|
||||||
fontSize: 15,
|
|
||||||
color: '#333',
|
|
||||||
marginTop: 4,
|
|
||||||
lineHeight: 22,
|
|
||||||
},
|
|
||||||
submitBtn: {
|
|
||||||
backgroundColor: "#83f5cc",
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: "center",
|
|
||||||
paddingVertical: 16,
|
|
||||||
borderRadius: 12,
|
|
||||||
elevation: 3,
|
|
||||||
marginTop: 20,
|
|
||||||
borderWidth: 1.5,
|
|
||||||
borderColor: '#62b59a',
|
|
||||||
},
|
|
||||||
btnText: {
|
|
||||||
color: "#333",
|
|
||||||
fontWeight: "bold",
|
|
||||||
fontSize: 18,
|
|
||||||
marginLeft: 10,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
+233
-149
@@ -1,180 +1,264 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { View, Text, StyleSheet, FlatList, TouchableOpacity } from "react-native";
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
FlatList,
|
||||||
|
TouchableOpacity,
|
||||||
|
TextInput,
|
||||||
|
Dimensions,
|
||||||
|
ActivityIndicator,
|
||||||
|
Image,
|
||||||
|
ScrollView,
|
||||||
|
Alert,
|
||||||
|
} from "react-native";
|
||||||
import { FAB } from "react-native-paper";
|
import { FAB } from "react-native-paper";
|
||||||
import { MaterialIcons } from '@react-native-vector-icons/material-icons';
|
import { Ionicons } from "@react-native-vector-icons/ionicons";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { MaterialIcons } from "@react-native-vector-icons/material-icons";
|
||||||
|
import { useNavigation, useFocusEffect } from "@react-navigation/native";
|
||||||
import { StackNavigationProp } from "@react-navigation/stack";
|
import { StackNavigationProp } from "@react-navigation/stack";
|
||||||
import { RootStackParamList } from "../navigation/AppNavigator";
|
import { RootStackParamList } from "../navigation/AppNavigator";
|
||||||
import AppView from "../components/AppView";
|
import AppView from "../components/AppView";
|
||||||
|
import { apiCall } from "../utils/ApiHelper";
|
||||||
|
import { JOIN_GROUP_URL } from "../utils/ApiUrls";
|
||||||
|
|
||||||
// --- Placeholder Data ---
|
const { width } = Dimensions.get('window');
|
||||||
const myGroupsData = [
|
const PUBLIC_GROUPS_URL = "https://api.godemo.in/FundCollect/api/fund/user/public";
|
||||||
{ id: '1', name: 'Family Savings', members: 5 },
|
|
||||||
{ id: '2', name: 'Office Lunch Club', members: 12 },
|
|
||||||
];
|
|
||||||
const joinedGroupsData = [
|
|
||||||
{ id: '3', name: 'Apartment Committee', members: 8 },
|
|
||||||
];
|
|
||||||
const publicGroupsData = [
|
|
||||||
{ id: '4', name: 'City Runners', members: 50 },
|
|
||||||
{ id: '5', name: 'Book Readers', members: 25 },
|
|
||||||
];
|
|
||||||
|
|
||||||
// --- Placeholder Group List Component ---
|
|
||||||
const GroupList = ({ data }: { data: any[] }) => {
|
// --- Types ---
|
||||||
if (data.length === 0) {
|
interface Group {
|
||||||
return <Text style={styles.emptyText}>No groups found.</Text>;
|
id: string;
|
||||||
|
groupName: string;
|
||||||
|
description: string;
|
||||||
|
maxParticipants: number;
|
||||||
|
currentParticipants: number;
|
||||||
|
contributionAmount: number;
|
||||||
|
cycleType: string;
|
||||||
|
createdByName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- UI Components ---
|
||||||
|
const GroupCard = ({ item, onJoin, onView, isGrid }: { item: Group; onJoin: (groupId: string) => void; onView: (group: Group) => void; isGrid?: boolean; }) => {
|
||||||
|
const isFull = item.currentParticipants >= item.maxParticipants;
|
||||||
|
const cardStyle = isGrid ? styles.gridCard : styles.listCard;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={cardStyle}>
|
||||||
|
<Text style={styles.groupName} numberOfLines={1}>{item.groupName}</Text>
|
||||||
|
<Text style={styles.groupDescription} numberOfLines={isGrid ? 2 : 1}>{item.description}</Text>
|
||||||
|
<View style={styles.groupMeta}>
|
||||||
|
<Text style={styles.groupMetaText}>{item.currentParticipants}/{item.maxParticipants} Members</Text>
|
||||||
|
<Text style={styles.groupMetaText}>{item.cycleType}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.buttonContainer}>
|
||||||
|
<TouchableOpacity style={styles.viewButton} onPress={() => onView(item)}>
|
||||||
|
<Text style={styles.viewButtonText}>View</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.joinButton, isFull && styles.joinButtonDisabled]}
|
||||||
|
onPress={() => onJoin(item.id)}
|
||||||
|
disabled={isFull}
|
||||||
|
>
|
||||||
|
<Text style={styles.joinButtonText}>{isFull ? "Full" : "Join"}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FeaturedGroupCard = ({ item }: { item: Group }) => (
|
||||||
|
<View style={styles.featuredCard}>
|
||||||
|
<Image source={{ uri: `https://i.pravatar.cc/150?u=${item.id}` }} style={styles.featuredImage} />
|
||||||
|
<View style={styles.featuredContent}>
|
||||||
|
<Text style={styles.featuredName} numberOfLines={1}>{item.groupName}</Text>
|
||||||
|
<Text style={styles.featuredCreator}>by {item.createdByName}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function GroupScreen() {
|
||||||
|
const [activeTab, setActiveTab] = useState("Public");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [isGridView, setIsGridView] = useState(false);
|
||||||
|
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>();
|
||||||
|
|
||||||
|
const [myGroups, setMyGroups] = useState<Group[]>([]);
|
||||||
|
const [joinedGroups, setJoinedGroups] = useState<Group[]>([]);
|
||||||
|
const [publicGroups, setPublicGroups] = useState<Group[]>([]);
|
||||||
|
const [featuredGroups, setFeaturedGroups] = useState<Group[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchGroups = async () => {
|
||||||
|
if (loading) return;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const res = await apiCall(PUBLIC_GROUPS_URL, "GET") as { data?: Group[] };
|
||||||
|
if (res && res.data) {
|
||||||
|
setPublicGroups(res.data);
|
||||||
|
setFeaturedGroups(res.data.slice(0, 5));
|
||||||
|
} else {
|
||||||
|
throw new Error("No data received");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError("Failed to fetch groups. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useFocusEffect(React.useCallback(() => { fetchGroups(); }, []));
|
||||||
|
|
||||||
|
const filterData = (data: Group[]) => {
|
||||||
|
if (!searchQuery.trim()) return data;
|
||||||
|
return data.filter(g =>
|
||||||
|
g.groupName.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJoinGroup = async (groupId: string, userId: string) => {
|
||||||
|
console.log("Joining group:", groupId, "User:", userId);
|
||||||
|
try {
|
||||||
|
const url = `${JOIN_GROUP_URL}/groups/${groupId}/join/0`;
|
||||||
|
console.log("Joining group URL:", url);
|
||||||
|
const res = await apiCall(url, "POST") as { statusCode?: number; message?: string };
|
||||||
|
|
||||||
|
console.log("JOIN_GROUP_URL response:", res);
|
||||||
|
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
Alert.alert("Success", "You requested to join the group.");
|
||||||
|
} else {
|
||||||
|
Alert.alert("Error", res.message || "Failed to join group.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Join Group Error:", error);
|
||||||
|
Alert.alert("Error", "An unexpected error occurred.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleViewGroup = (group: Group) => {
|
||||||
|
// navigation.navigate('GroupDetails', { group });
|
||||||
|
Alert.alert("View Group", `Viewing details for ${group.groupName}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
let data;
|
||||||
|
switch (activeTab) {
|
||||||
|
case "My Groups": data = myGroups; break;
|
||||||
|
case "Joined": data = joinedGroups; break;
|
||||||
|
case "Public": data = publicGroups; break;
|
||||||
|
default: data = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading && !publicGroups.length) return <ActivityIndicator size="large" color="#62b59a" style={{ marginTop: 40 }} />;
|
||||||
|
if (error) return <Text style={styles.emptyText}>{error}</Text>;
|
||||||
|
|
||||||
|
const filteredData = filterData(data);
|
||||||
|
if (filteredData.length === 0 && !loading) return <Text style={styles.emptyText}>No groups found.</Text>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={data}
|
data={filteredData}
|
||||||
|
key={isGridView ? 'grid' : 'list'}
|
||||||
|
numColumns={isGridView ? 2 : 1}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<View style={styles.groupCard}>
|
<GroupCard
|
||||||
<Text style={styles.groupName}>{item.name}</Text>
|
item={item}
|
||||||
<Text style={styles.groupMembers}>{item.members} members</Text>
|
onJoin={activeTab === 'Public' ? handleJoinGroup : () => { }}
|
||||||
</View>
|
onView={handleViewGroup}
|
||||||
|
isGrid={isGridView}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
columnWrapperStyle={isGridView ? { justifyContent: 'space-between' } : null}
|
||||||
|
contentContainerStyle={{ paddingBottom: 150 }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function GroupScreen() {
|
|
||||||
const [activeTab, setActiveTab] = useState("My Groups");
|
|
||||||
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>();
|
|
||||||
|
|
||||||
const renderContent = () => {
|
|
||||||
switch (activeTab) {
|
|
||||||
case "My Groups":
|
|
||||||
return <GroupList data={myGroupsData} />;
|
|
||||||
case "Joined":
|
|
||||||
return <GroupList data={joinedGroupsData} />;
|
|
||||||
case "Public":
|
|
||||||
return <GroupList data={publicGroupsData} />;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppView>
|
<AppView paddingHorizontal={0} style={styles.container}>
|
||||||
<View style={styles.container}>
|
{/* Fixed Header */}
|
||||||
{/* --- Header and Segmented Control --- */}
|
<View style={styles.headerContainer}>
|
||||||
<Text style={styles.title}>Your Groups</Text>
|
<Text style={styles.title}>Explore Groups</Text>
|
||||||
|
<View style={styles.searchContainer}>
|
||||||
|
<Ionicons name="search" size={20} color="#888" style={styles.searchIcon} />
|
||||||
|
<TextInput style={styles.searchInput} placeholder="Search for groups..." placeholderTextColor="#888" value={searchQuery} onChangeText={setSearchQuery} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Scrollable Content */}
|
||||||
|
<ScrollView style={styles.container}>
|
||||||
|
<Text style={styles.sectionTitle}>Featured</Text>
|
||||||
|
<FlatList
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
data={featuredGroups}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
renderItem={({ item }) => <FeaturedGroupCard item={item} />}
|
||||||
|
style={styles.featuredList}
|
||||||
|
ListEmptyComponent={!loading && featuredGroups.length === 0 ? <Text style={styles.emptyFeaturedText}>No featured groups.</Text> : null}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={styles.tabHeader}>
|
||||||
<View style={styles.segmentedControl}>
|
<View style={styles.segmentedControl}>
|
||||||
{["My Groups", "Joined", "Public"].map((tab) => (
|
{["My Groups", "Joined", "Public"].map((tab) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity key={tab} style={[styles.segmentButton, activeTab === tab && styles.segmentButtonActive]} onPress={() => setActiveTab(tab)}>
|
||||||
key={tab}
|
<Text style={[styles.segmentText, activeTab === tab && styles.segmentTextActive]}>{tab}</Text>
|
||||||
style={[
|
|
||||||
styles.segmentButton,
|
|
||||||
activeTab === tab && styles.segmentButtonActive,
|
|
||||||
]}
|
|
||||||
onPress={() => setActiveTab(tab)}
|
|
||||||
>
|
|
||||||
<Text style={[
|
|
||||||
styles.segmentText,
|
|
||||||
activeTab === tab && styles.segmentTextActive,
|
|
||||||
]}>
|
|
||||||
{tab}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
<TouchableOpacity onPress={() => setIsGridView(!isGridView)} style={styles.layoutToggleButton}>
|
||||||
{/* --- Dynamic Content Area --- */}
|
<Ionicons name={isGridView ? "list" : "grid"} size={22} color="#555" />
|
||||||
<View style={styles.contentArea}>
|
</TouchableOpacity>
|
||||||
{renderContent()}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<FAB
|
{/* The list now renders inside a simple View, as FlatList handles its own scroll */}
|
||||||
style={styles.fab}
|
<View style={styles.contentArea}>{renderContent()}</View>
|
||||||
icon={() => <MaterialIcons name="group-add" size={24} color="#333" />}
|
</ScrollView>
|
||||||
onPress={() => navigation.navigate('GroupCreation')}
|
<FAB style={styles.fab} icon={() => <MaterialIcons name="group-add" size={24} color="#333" />} onPress={() => navigation.navigate('GroupCreation')} />
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</AppView>
|
</AppView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: { flex: 1, backgroundColor: '#f5f6fa' },
|
||||||
flex: 1,
|
headerContainer: { backgroundColor: '#f5f6fa', paddingTop: 40, paddingHorizontal: 16 },
|
||||||
padding: 16,
|
title: { fontSize: 28, fontWeight: "bold", color: "#222", marginBottom: 10 },
|
||||||
paddingTop: 40,
|
searchContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff', borderRadius: 12, paddingHorizontal: 10, elevation: 2, marginBottom: 20 },
|
||||||
backgroundColor: '#f5f6fa',
|
searchIcon: { marginRight: 8 },
|
||||||
},
|
searchInput: { flex: 1, height: 45, fontSize: 16, color: '#333' },
|
||||||
title: {
|
sectionTitle: { fontSize: 20, fontWeight: 'bold', color: '#333', marginBottom: 12, paddingHorizontal: 16 },
|
||||||
fontSize: 26,
|
featuredList: { paddingLeft: 16, marginBottom: 20 },
|
||||||
fontWeight: "bold",
|
featuredCard: { width: width * 0.45, marginRight: 15, backgroundColor: '#fff', borderRadius: 12, elevation: 3, overflow: 'hidden' },
|
||||||
color: "#333",
|
featuredImage: { width: '100%', height: 100 },
|
||||||
marginBottom: 20,
|
featuredContent: { padding: 10 },
|
||||||
},
|
featuredName: { fontSize: 14, fontWeight: 'bold', color: '#333' },
|
||||||
segmentedControl: {
|
featuredCreator: { fontSize: 12, color: '#777', marginTop: 2 },
|
||||||
flexDirection: 'row',
|
tabHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, marginBottom: 16 },
|
||||||
backgroundColor: '#e0e0e0',
|
segmentedControl: { flex: 1, flexDirection: 'row', backgroundColor: '#e9ecef', borderRadius: 10, padding: 4, marginRight: 10 },
|
||||||
borderRadius: 10,
|
segmentButton: { flex: 1, paddingVertical: 10, borderRadius: 8, alignItems: 'center' },
|
||||||
padding: 4,
|
segmentButtonActive: { backgroundColor: '#fff', elevation: 2, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 },
|
||||||
marginBottom: 20,
|
segmentText: { fontSize: 14, fontWeight: '600', color: '#555' },
|
||||||
},
|
segmentTextActive: { color: '#333' },
|
||||||
segmentButton: {
|
layoutToggleButton: { padding: 8 },
|
||||||
flex: 1,
|
contentArea: { paddingHorizontal: 16 },
|
||||||
paddingVertical: 8,
|
fab: { position: "absolute", right: 20, bottom: 90, width: 60, height: 60, justifyContent: "center", alignItems: "center", backgroundColor: "#83f5cc", borderRadius: 30, elevation: 4 },
|
||||||
borderRadius: 8,
|
listCard: { backgroundColor: '#fff', padding: 16, borderRadius: 12, marginBottom: 12, elevation: 1 },
|
||||||
alignItems: 'center',
|
gridCard: { backgroundColor: '#fff', padding: 12, borderRadius: 12, marginBottom: 12, elevation: 1, width: (width - 48) / 2 },
|
||||||
},
|
groupName: { fontSize: 16, fontWeight: 'bold', color: '#333' },
|
||||||
segmentButtonActive: {
|
groupDescription: { fontSize: 14, color: '#666', marginVertical: 4 },
|
||||||
backgroundColor: '#fff',
|
groupMeta: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8, borderTopWidth: 1, borderTopColor: '#f0f0f0', paddingTop: 8, marginBottom: 10 },
|
||||||
elevation: 2,
|
groupMetaText: { fontSize: 12, color: '#888', fontWeight: '500', textTransform: 'capitalize' },
|
||||||
shadowColor: '#000',
|
emptyText: { textAlign: 'center', marginTop: 40, fontSize: 16, color: '#888' },
|
||||||
shadowOpacity: 0.1,
|
emptyFeaturedText: { marginLeft: 20, color: '#888' },
|
||||||
shadowRadius: 5,
|
buttonContainer: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8 },
|
||||||
},
|
viewButton: { flex: 1, backgroundColor: '#e9ecef', paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginRight: 5 },
|
||||||
segmentText: {
|
viewButtonText: { color: '#333', fontWeight: 'bold', fontSize: 14 },
|
||||||
fontSize: 14,
|
joinButton: { flex: 1, backgroundColor: '#83f5cc', paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginLeft: 5 },
|
||||||
fontWeight: '600',
|
joinButtonDisabled: { backgroundColor: '#bdc3c7' },
|
||||||
color: '#555',
|
joinButtonText: { color: '#333', fontWeight: 'bold', fontSize: 14 },
|
||||||
},
|
|
||||||
segmentTextActive: {
|
|
||||||
color: '#333',
|
|
||||||
},
|
|
||||||
contentArea: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
fab: {
|
|
||||||
position: "absolute",
|
|
||||||
right: 20,
|
|
||||||
bottom: 90,
|
|
||||||
width: 70,
|
|
||||||
height: 70,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: "#83f5cc",
|
|
||||||
borderRadius: 35,
|
|
||||||
elevation: 4,
|
|
||||||
},
|
|
||||||
// --- Group List Styles ---
|
|
||||||
groupCard: {
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
padding: 16,
|
|
||||||
borderRadius: 12,
|
|
||||||
marginBottom: 12,
|
|
||||||
elevation: 1,
|
|
||||||
},
|
|
||||||
groupName: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: '#333',
|
|
||||||
},
|
|
||||||
groupMembers: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: '#777',
|
|
||||||
marginTop: 4,
|
|
||||||
},
|
|
||||||
emptyText: {
|
|
||||||
textAlign: 'center',
|
|
||||||
marginTop: 50,
|
|
||||||
fontSize: 16,
|
|
||||||
color: '#888',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,5 +2,8 @@ export const BASE_URL = "https://api.godemo.in/FundCollect"
|
|||||||
export const LOGIN_URL = `${BASE_URL}/api/user/generate/otp?mobile=`
|
export const LOGIN_URL = `${BASE_URL}/api/user/generate/otp?mobile=`
|
||||||
export const VARIFY_OTP_URL = `${BASE_URL}/api/user/otp/verify`
|
export const VARIFY_OTP_URL = `${BASE_URL}/api/user/otp/verify`
|
||||||
export const ORGRAGISTRATION = `${BASE_URL}/api/organizer/registration/apply`
|
export const ORGRAGISTRATION = `${BASE_URL}/api/organizer/registration/apply`
|
||||||
export const GROUP_CREATION_URL = `${BASE_URL}/api/group/create`
|
export const GROUP_CREATION_URL = `${BASE_URL}/api/fund/groups/{organizerId}`
|
||||||
export const PROFILE_URL =`${BASE_URL}/api/user/profile/view`
|
export const PROFILE_URL =`${BASE_URL}/api/user/profile/view`
|
||||||
|
export const PUBLIC_GROUPS_URL =`${BASE_URL}/api/fund/user/public`
|
||||||
|
export const JOIN_GROUP_URL = `${BASE_URL}/api/fund`;
|
||||||
|
//https://api.godemo.in/FundCollect/api/fund/groups/2a80cdaa-8532-4b8a-87f0-9c1023d18a7f/join/64
|
||||||
Reference in New Issue
Block a user