370 lines
15 KiB
TypeScript
370 lines
15 KiB
TypeScript
import React, { useState, useEffect, useCallback } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
FlatList,
|
|
TouchableOpacity,
|
|
TextInput,
|
|
Dimensions,
|
|
ActivityIndicator,
|
|
Image,
|
|
ScrollView,
|
|
Alert,
|
|
RefreshControl,
|
|
} from "react-native";
|
|
import { FAB } from "react-native-paper";
|
|
import { Ionicons } from "@react-native-vector-icons/ionicons";
|
|
import { MaterialIcons } from "@react-native-vector-icons/material-icons";
|
|
import { useNavigation, useFocusEffect } from "@react-navigation/native";
|
|
import { StackNavigationProp } from "@react-navigation/stack";
|
|
import { RootStackParamList } from "../navigation/AppNavigator";
|
|
import AppView from "../components/AppView";
|
|
import { apiCall } from "../utils/ApiHelper";
|
|
import { BASE_URL, GET_JOINED_GROUP_URL, JOIN_GROUP_URL, PUBLIC_GROUPS_URL, PUT_USER_STATUS } from "../utils/ApiUrls";
|
|
import colors from "../components/colors";
|
|
import { ApiResponse } from "./home/UserProfileStatus";
|
|
import * as Keychain from "react-native-keychain";
|
|
const { width } = Dimensions.get('window');
|
|
|
|
// --- Types ---
|
|
interface Group {
|
|
id: string;
|
|
groupName: string;
|
|
description: string;
|
|
maxParticipants: number;
|
|
currentParticipants: number;
|
|
contributionAmount: number;
|
|
cycleType: string;
|
|
createdByName: string;
|
|
imageBlob: string
|
|
}
|
|
|
|
// --- UI Components ---
|
|
const GroupCard = ({ item, onJoin, onView, isGrid }: { item: Group; onJoin:()=>void; onView: (group: Group) => void; isGrid?: boolean; }) => {
|
|
const isFull = item.currentParticipants >= item.maxParticipants;
|
|
const cardStyle = isGrid ? styles.gridCard : styles.listCard;
|
|
const [loading, setLoading] = useState<boolean>(false)
|
|
const navigation = useNavigation()
|
|
|
|
return (
|
|
<View style={cardStyle}>
|
|
{/* <TouchableOpacity onPress={()=> navigation.navigate("GroupDetails" as never)}> */}
|
|
<View style={{ flex: 1, flexDirection: 'row', justifyContent: "space-between" }}>
|
|
<View>
|
|
<Text style={[styles.groupName,{width:isGrid?"50%":"100%"}]} numberOfLines={isGrid ? 2 : 1}>{item.groupName}</Text>
|
|
<Text style={styles.groupDescription} numberOfLines={isGrid ? 2 : 1}>{item.description}</Text>
|
|
</View>
|
|
<Image
|
|
source={{ uri: item?.imageBlob !== null ? `data:image/jpeg;base64,${item?.imageBlob}` : `https://i.pravatar.cc/150?u=${item.id}` }}
|
|
style={{ width: 50, borderWidth: 1, height: 50, borderColor: colors.pastelGreen, borderRadius: 25, }}
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.groupMeta}>
|
|
<Text style={styles.groupMetaText}>{item.currentParticipants}/{item.maxParticipants} Members</Text>
|
|
<Text style={styles.groupMetaText}>{item.cycleType}</Text>
|
|
</View>
|
|
{/* </TouchableOpacity> */}
|
|
<View style={styles.buttonContainer}>
|
|
{/* <TouchableOpacity style={styles.viewButton} onPress={() => onView(item)}>
|
|
<Text style={styles.viewButtonText}>View</Text>
|
|
</TouchableOpacity> */}
|
|
<TouchableOpacity
|
|
style={[styles.joinButton, isFull && styles.joinButtonDisabled]}
|
|
onPress={async () => {
|
|
setLoading(true);
|
|
await onJoin(item.id);
|
|
setLoading(false);
|
|
}}
|
|
disabled={isFull || loading} // Also disable button while loading
|
|
>
|
|
{loading ? <ActivityIndicator color={colors.black} /> : <Text style={styles.joinButtonText}>{isFull ? "Full" : "Join"}</Text>}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const FeaturedGroupCard = ({ item }: { item: Group }) => {
|
|
return (
|
|
<View style={styles.featuredCard}>
|
|
<Image source={{ uri: item?.imageBlob !== null ? `data:image/jpeg;base64${item?.imageBlob}` : `https://i.pravatar.cc/150?u=${item.id}` }} style={styles.featuredImage} />
|
|
<View style={styles.featuredContent}>
|
|
<Text style={styles.featuredName} numberOfLines={1}>{item.groupName}</Text>
|
|
<Text style={styles.featuredCreator}>by {item.createdByName}</Text>
|
|
</View>
|
|
</View>
|
|
)
|
|
};
|
|
|
|
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 [role, setRole] = useState<string | null>(null);
|
|
const [refreshing, setRefreshing] = useState<boolean>(false)
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
|
|
const fetchJoinedGroups = async () => {
|
|
if (loading) return;
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
console.log("first-????-")
|
|
const res = await apiCall(GET_JOINED_GROUP_URL, "GET") as { data?: Group[] };
|
|
console.log("first---", res)
|
|
if (res && res.data) {
|
|
setJoinedGroups(res.data);
|
|
|
|
} else {
|
|
throw new Error("No data received");
|
|
}
|
|
} catch (e) {
|
|
setError("Failed to fetch groups. Please try again.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useFocusEffect(React.useCallback(() => {
|
|
fetchGroups()
|
|
fetchJoinedGroups()
|
|
}, []));
|
|
|
|
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/10`;
|
|
console.log("Joining group URL:", url);
|
|
const res = await apiCall(url, "POST") as any;
|
|
|
|
console.log("JOIN_GROUP_URL response:", res);
|
|
|
|
if (res.statusCode === 200) {
|
|
Alert.alert("Success", "You requested to join the group.");
|
|
} else {
|
|
// Alert.alert("Error", res.message || "Failed to join group.");
|
|
}
|
|
} catch (error) {
|
|
console.error("Join Group Error:", error);
|
|
// Alert.alert("Error", "An unexpected error occurred.");
|
|
}
|
|
};
|
|
|
|
const userStatus = async () => {
|
|
try {
|
|
const credentials = await Keychain.getGenericPassword();
|
|
if (!credentials) {
|
|
Alert.alert("Error", "You are not logged in");
|
|
return;
|
|
}
|
|
const token = credentials.password;
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
const response = await fetch(PUT_USER_STATUS, {
|
|
method: "GET",
|
|
headers: {
|
|
Accept: "*/*",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
const json: ApiResponse = await response.json();
|
|
|
|
console.log("👤 Profile Status Response:", json);
|
|
if (json.status === "success") {
|
|
setRole(json.data.role);
|
|
|
|
if (json.data.profileStatus === false) {
|
|
|
|
}
|
|
} else {
|
|
setError(json.message || "Failed to fetch status");
|
|
}
|
|
} catch (err) {
|
|
setError("Something went wrong. Please try again.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
userStatus();
|
|
}, [])
|
|
);
|
|
|
|
|
|
const handleViewGroup = (group: Group) => {
|
|
// navigation.navigate('GroupDetails', { group });
|
|
Alert.alert("View Group", `Viewing details for ${group.groupName}`);
|
|
};
|
|
|
|
const onRefresh = () => {
|
|
setRefreshing(true)
|
|
fetchGroups()
|
|
setTimeout(() => {
|
|
setRefreshing(false)
|
|
}, 20000);
|
|
}
|
|
|
|
const renderContent = () => {
|
|
let data: Group[];
|
|
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 (
|
|
<FlatList
|
|
data={filteredData}
|
|
key={isGridView ? 'grid' : 'list'}
|
|
numColumns={isGridView ? 2 : 1}
|
|
keyExtractor={(item) => item.id}
|
|
renderItem={({ item }) => (
|
|
<GroupCard
|
|
item={item}
|
|
onJoin={() => {
|
|
console.log('mmmm')
|
|
if (activeTab === 'Public') {
|
|
handleJoinGroup(item.id, 'currentUserId');
|
|
}
|
|
}}
|
|
onView={handleViewGroup}
|
|
isGrid={isGridView}
|
|
/>
|
|
)}
|
|
columnWrapperStyle={isGridView ? { justifyContent: 'space-between' } : null}
|
|
contentContainerStyle={{ paddingBottom: 150 }}
|
|
refreshControl={
|
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
|
}
|
|
// ListFooterComponent={<ActivityIndicator size={'large'} color={colors.Group} />}
|
|
/>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<AppView paddingHorizontal={0}>
|
|
{/* Fixed Header */}
|
|
<View style={styles.headerContainer}>
|
|
<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}>
|
|
|
|
<View style={styles.tabHeader}>
|
|
<View style={styles.segmentedControl}>
|
|
|
|
{role === "ORGANIZER" ? (["Public"].map((tab) => (
|
|
<TouchableOpacity key={tab} style={[styles.segmentButton, activeTab === tab && styles.segmentButtonActive]} onPress={() => setActiveTab(tab)}>
|
|
<Text style={[styles.segmentText, activeTab === tab && styles.segmentTextActive]}>{tab}</Text>
|
|
</TouchableOpacity>
|
|
))) : (["Joined", "Public"].map((tab) => (
|
|
<TouchableOpacity key={tab} style={[styles.segmentButton, activeTab === tab && styles.segmentButtonActive]} onPress={() => setActiveTab(tab)}>
|
|
<Text style={[styles.segmentText, activeTab === tab && styles.segmentTextActive]}>{tab}</Text>
|
|
</TouchableOpacity>
|
|
)))}
|
|
</View>
|
|
<TouchableOpacity onPress={() => setIsGridView(!isGridView)} style={styles.layoutToggleButton}>
|
|
<Ionicons name={isGridView ? "list" : "grid"} size={22} color="#555" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* The list now renders inside a simple View, as FlatList handles its own scroll */}
|
|
<View style={{}}>{renderContent()}</View>
|
|
</ScrollView>
|
|
{role === "ORGANIZER" && (
|
|
<FAB style={styles.fab} icon={() => <MaterialIcons name="group-add" size={24} color="#333" />} onPress={() => navigation.navigate('GroupCreation')} />
|
|
)}
|
|
</AppView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
|
|
headerContainer: { backgroundColor: '#f5f6fa', paddingHorizontal: 16 },
|
|
title: { fontSize: 22, fontWeight: "600", color: colors.Group },
|
|
searchContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff', borderRadius: 20, paddingHorizontal: 10, elevation: 5, marginVertical: 20 , borderWidth:2,borderColor:colors.pastelGreen },
|
|
searchIcon: { marginRight: 8 },
|
|
searchInput: { flex: 1, height: 45, fontSize: 16, color: '#333' },
|
|
sectionTitle: { fontSize: 20, fontWeight: 'bold', color: colors.Group, marginBottom: 12, paddingHorizontal: 16 },
|
|
featuredList: { paddingLeft: 16, marginBottom: 20 },
|
|
featuredCard: { width: width * 0.45, marginRight: 15, backgroundColor: '#fff', borderRadius: 12, elevation: 3, overflow: 'hidden' },
|
|
featuredImage: { width: '100%', height: 100 },
|
|
featuredContent: { padding: 10 },
|
|
featuredName: { fontSize: 14, fontWeight: 'bold', color: '#333' },
|
|
featuredCreator: { fontSize: 12, color: '#777', marginTop: 2 },
|
|
tabHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, marginBottom: 16 },
|
|
segmentedControl: { flex: 1, flexDirection: 'row', borderRadius: 10, padding: 4, marginRight: 10 },
|
|
segmentButton: { flex: 1, paddingVertical: 10, borderRadius: 8, alignItems: 'center' },
|
|
segmentButtonActive: { backgroundColor: colors.pastelGreen, elevation: 5, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 , borderRadius:15 },
|
|
segmentText: { fontSize: 14, fontWeight: '600', color: '#555' },
|
|
segmentTextActive: { color: '#333' },
|
|
layoutToggleButton: { padding: 8 },
|
|
fab: { position: "absolute", right: 20, bottom: 90, width: 60, height: 60, justifyContent: "center", alignItems: "center", backgroundColor: colors.pastelGreen, borderRadius: 30, elevation: 4 },
|
|
listCard: { backgroundColor: colors.pastelGreen, padding: 16, borderRadius: 20, marginBottom: 12,marginHorizontal:15, elevation: 5 },
|
|
gridCard: { backgroundColor: colors.pastelGreen, padding: 12, borderRadius: 20, marginBottom: 12,marginHorizontal:15, elevation: 5, width: (width - 48) / 2 },
|
|
groupName: { fontSize: 16, fontWeight: 'bold', color: '#333', width: '50%' },
|
|
groupDescription: { fontSize: 14, color: '#666', marginVertical: 4, width: '40%' },
|
|
groupMeta: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8, borderTopWidth: 1, borderTopColor: '#f0f0f0', paddingTop: 8, marginBottom: 10 },
|
|
groupMetaText: { fontSize: 12, color: '#888', fontWeight: '500', textTransform: 'capitalize' },
|
|
emptyText: { textAlign: 'center', marginTop: 40, fontSize: 16, color: '#888' },
|
|
emptyFeaturedText: { marginLeft: 20, color: '#888' },
|
|
buttonContainer: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8 },
|
|
viewButton: { flex: 1, backgroundColor: '#e9ecef', paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginRight: 5 },
|
|
viewButtonText: { color: '#333', fontWeight: 'bold', fontSize: 14 },
|
|
joinButton: { flex: 1, backgroundColor: colors.white, paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginLeft: 5 },
|
|
joinButtonDisabled: { backgroundColor: '#bdc3c7' },
|
|
joinButtonText: { color: '#333', fontWeight: 'bold', fontSize: 14 },
|
|
});
|