diff --git a/android/app/release/CircleCollect.apk b/android/app/release/app-release.apk similarity index 97% rename from android/app/release/CircleCollect.apk rename to android/app/release/app-release.apk index bfad9c0..19b281a 100644 Binary files a/android/app/release/CircleCollect.apk and b/android/app/release/app-release.apk differ diff --git a/android/app/release/baselineProfiles/0/app-release.dm b/android/app/release/baselineProfiles/0/app-release.dm index a8daac4..17e7a8d 100644 Binary files a/android/app/release/baselineProfiles/0/app-release.dm and b/android/app/release/baselineProfiles/0/app-release.dm differ diff --git a/android/app/release/baselineProfiles/1/app-release.dm b/android/app/release/baselineProfiles/1/app-release.dm index d8c7c1a..eb636ce 100644 Binary files a/android/app/release/baselineProfiles/1/app-release.dm and b/android/app/release/baselineProfiles/1/app-release.dm differ diff --git a/package-lock.json b/package-lock.json index 0bb25eb..b207b68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@react-navigation/native-stack": "^7.3.26", "@react-navigation/stack": "^7.4.8", "@reduxjs/toolkit": "^2.9.0", + "@types/react-native-material-ripple": "^0.9.6", "axios": "^1.12.2", "install": "^0.13.0", "npm": "^11.6.0", @@ -4319,6 +4320,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-native-material-ripple": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@types/react-native-material-ripple/-/react-native-material-ripple-0.9.6.tgz", + "integrity": "sha512-4rIu/tAMHejFN9jyMNrknO8OOJVSEQJK07/8BPj61wkLaCesy3nPsaKF0ASCvLTtQ6sdOPI1sRQjSE/euHO/YQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "react-native": "*" + } + }, "node_modules/@types/react-native-vector-icons": { "version": "6.4.18", "license": "MIT", diff --git a/package.json b/package.json index e43c628..1850dc6 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@react-navigation/native-stack": "^7.3.26", "@react-navigation/stack": "^7.4.8", "@reduxjs/toolkit": "^2.9.0", + "@types/react-native-material-ripple": "^0.9.6", "axios": "^1.12.2", "install": "^0.13.0", "npm": "^11.6.0", diff --git a/src/components/CostomAlart.tsx b/src/components/CostomAlart.tsx index 25cb593..ec27f18 100644 --- a/src/components/CostomAlart.tsx +++ b/src/components/CostomAlart.tsx @@ -1,5 +1,3 @@ -// src/components/CustomAlert.tsx - import React from "react"; import { Modal, @@ -10,31 +8,35 @@ import { Platform, } from "react-native"; import { BlurView } from "@react-native-community/blur"; -import Ionicons from '@react-native-vector-icons/ionicons'; -import colors from "./colors"; // Import centralized colors +import { Ionicons } from "@react-native-vector-icons/ionicons"; +import colors from "./colors"; -interface AlertButton { +export interface AlertButton { text: string; onPress: () => void; - style?: "cancel" | "destructive" | "default"; + style?: "cancel" | "destructive" | "default" | "primary" | "secondary" | "outline"; } -interface CustomAlertProps { +export interface CustomAlertProps { visible: boolean; title: string; message: string; - buttons: AlertButton[]; + buttons?: AlertButton[]; alertType?: 'error' | 'success' | 'info'; + onClose?: () => void; } +const BORDER_WIDTH = 6; +const RADIUS = 24; + const CustomAlert: React.FC = ({ visible, title, message, - buttons, + buttons = [], alertType = 'info', + onClose, }) => { - const getIcon = () => { switch (alertType) { case 'error': @@ -45,18 +47,57 @@ const CustomAlert: React.FC = ({ return { name: "information-circle", color: colors.primary }; } }; - const icon = getIcon(); + const handleRequestClose = () => { + if (buttons.length > 0) { + const closeButton = buttons.find(b => b.style === 'cancel') || buttons[0]; + closeButton?.onPress(); + } else { + onClose?.(); + } + }; + + const getButtonStyle = (styleType:string|undefined) => { + switch (styleType) { + case "primary": + return [styles.button, styles.primaryButton]; + case "secondary": + return [styles.button, styles.secondaryButton]; + case "outline": + return [styles.button, styles.outlineButton]; + case "destructive": + return [styles.button, styles.destructiveButton]; + case "cancel": + return [styles.button, styles.cancelButton]; + default: + return [styles.button, styles.defaultButton]; + } + }; + + const getButtonTextStyle = (styleType:string|undefined) => { + switch (styleType) { + case "primary": + return [styles.buttonText, styles.primaryButtonText]; + case "secondary": + return [styles.buttonText, styles.secondaryButtonText]; + case "outline": + return [styles.buttonText, styles.outlineButtonText]; + case "destructive": + return [styles.buttonText, styles.destructiveButtonText]; + case "cancel": + return [styles.buttonText, styles.cancelButtonText]; + default: + return [styles.buttonText, styles.defaultButtonText]; + } + }; + return ( { - const closeButton = buttons.find(b => b.style === 'cancel') || buttons[0]; - closeButton?.onPress(); - }} + onRequestClose={handleRequestClose} > {Platform.OS === 'ios' ? ( @@ -65,31 +106,29 @@ const CustomAlert: React.FC = ({ )} - - - - - {title} - {message} - - - {buttons.map((button, index) => ( - - - {button.text} - - - ))} + + + + + + + {title} + {message} + {buttons.length > 0 && ( + + {buttons.map((button, index) => ( + + + {button.text} + + + ))} + + )} @@ -110,21 +149,28 @@ const styles = StyleSheet.create({ bottom: 0, right: 0, }, - alertBox: { - backgroundColor: colors.alertBackground, - borderRadius: 24, - padding: 24, - alignItems: "center", - margin: 24, - borderWidth: 1, - borderColor: 'rgba(255, 255, 255, 0.5)', - shadowColor: colors.black, - shadowOffset: { width: 0, height: 10 }, - shadowOpacity: 0.1, - shadowRadius: 30, - elevation: 20, + box: { + paddingHorizontal: 4, + padding: BORDER_WIDTH + 4, + borderRadius: RADIUS + BORDER_WIDTH, width: '90%', maxWidth: 400, + alignSelf: 'center', + margin: 20, + backgroundColor: colors.pastelPink, + position: 'relative', + overflow: 'hidden', + }, + faintOverlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: "rgba(255,255,255,0.77)", + zIndex: 1, + borderRadius: RADIUS, + }, + contentContainer: { + zIndex: 2, + padding: 24, + alignItems: "center", }, iconContainer: { marginBottom: 16, @@ -155,13 +201,33 @@ const styles = StyleSheet.create({ borderRadius: 16, justifyContent: 'center', alignItems: 'center', + minWidth: 80, + borderWidth: 0, // default border, override for outline/cancel }, + // === BUTTON STYLE VARIANTS === defaultButton: { - backgroundColor: colors.primary, + backgroundColor: colors.pastelPink, }, destructiveButton: { backgroundColor: colors.error, }, + primaryButton: { + backgroundColor: colors.primary, + }, + secondaryButton: { + backgroundColor: colors.pastelBlue, + }, + outlineButton: { + borderColor: colors.primary, + borderWidth: 2, + backgroundColor: "transparent", + }, + cancelButton: { + borderColor: colors.error, + borderWidth: 1, + backgroundColor: "transparent", + }, + // === BUTTON TEXT COLOR VARIANTS === buttonText: { fontSize: 16, fontWeight: "700", @@ -172,6 +238,18 @@ const styles = StyleSheet.create({ destructiveButtonText: { color: colors.white, }, + primaryButtonText: { + color: colors.white, + }, + secondaryButtonText: { + color: colors.textDark, + }, + outlineButtonText: { + color: colors.primary, + }, + cancelButtonText: { + color: colors.error, + }, }); export default CustomAlert; diff --git a/src/components/JoinedGroupCard.tsx b/src/components/JoinedGroupCard.tsx index a742639..5543c46 100644 --- a/src/components/JoinedGroupCard.tsx +++ b/src/components/JoinedGroupCard.tsx @@ -1,17 +1,15 @@ -// src/components/JoinedGroupCard.tsx - import React, { FC } from 'react'; import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; import colors from './colors'; import { useNavigation } from '@react-navigation/native'; -// Update the type to match the full data structure + interface JoinedGroup { id: string; groupName: string; description: string; visibility: 'PUBLIC' | 'PRIVATE'; createdByUserId: number; - createdByName: string; // Renamed from createdBy for clarity + createdByName: string; startDate: [number, number, number] | null; endDate: [number, number, number] | null; minParticipants: number; @@ -24,33 +22,33 @@ interface JoinedGroup { interface JoinedGroupCardProps { item: JoinedGroup | any; - onViewPress: () => void; // Function prop for the View button + onViewPress: () => void; + onPayPress: () => void; // function for Pay button + hasPaid?: boolean; // whether user has already paid } -// Helper function to format dates const formatDate = (dateArray: [number, number, number] | null): string => { if (!dateArray) return 'N/A'; const date = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }; -// Helper to determine status color const getStatusStyle = (status: JoinedGroup['status']) => { switch (status) { case 'ACTIVE': return { - backgroundColor: '#D1FAE5', // Green tint - textColor: '#065F46', // Dark Green + backgroundColor: '#D1FAE5', + textColor: '#065F46', }; case 'PENDING': return { - backgroundColor: '#FEF3C7', // Yellow tint - textColor: '#92400E', // Dark Yellow/Brown + backgroundColor: '#FEF3C7', + textColor: '#92400E', }; case 'COMPLETED': return { - backgroundColor: '#E5E7EB', // Gray tint - textColor: '#4B5563', // Dark Gray + backgroundColor: '#E5E7EB', + textColor: '#4B5563', }; default: return { @@ -60,13 +58,13 @@ const getStatusStyle = (status: JoinedGroup['status']) => { } }; -export const JoinedGroupCard: FC = ({ item, onViewPress }) => { +export const JoinedGroupCard: FC = ({ item, onViewPress, onPayPress, hasPaid }) => { const progress = (item.currentParticipants / item.maxParticipants) * 100; const statusStyle = getStatusStyle(item.status); - const navigation = useNavigation() + const navigation = useNavigation(); return ( - navigation.navigate("GroupDetails", {item, color:colors.black})}> + navigation.navigate("GroupDetails", {item, color:colors.black})}> {/* Header */} {item.groupName} @@ -75,27 +73,32 @@ export const JoinedGroupCard: FC = ({ item, onViewPress }) Created by {item.createdByName} - {/* Financial Info */} - - Contribution - ₹{item.contributionAmount.toLocaleString('en-IN')} - + Contribution + ₹{item.contributionAmount.toLocaleString('en-IN')} Cycle {item.cycleType} - - {/* Footer with Dates and View Button */} + {/* Footer with Dates & Buttons */} {formatDate(item.startDate)} - {formatDate(item.endDate)} - + {/* Add Pay / Paid button here */} + {hasPaid ? ( + + Paid + + ) : ( + + Pay + + )} ); @@ -149,13 +152,12 @@ const styles = StyleSheet.create({ borderBottomWidth: 1, borderColor: colors.pastelOrange, marginBottom: 16, - backgroundColor:colors.white, - borderRadius:15, + backgroundColor: colors.white, + borderRadius:15, }, infoItem: { padding:5, paddingHorizontal:10, - backgroundColor:colors.white, alignItems: 'flex-start', }, @@ -202,14 +204,37 @@ const styles = StyleSheet.create({ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - }, dateText: { fontSize: 14, color: '#4B5563', fontWeight: '500', }, - // --- NEW STYLES for the View Button --- + // Pay button + payButton: { + elevation:5, + backgroundColor: colors.white, + paddingHorizontal: 25, + paddingVertical: 10, + borderRadius: 10, + }, + payButtonText: { + color: colors.black, + fontSize: 14, + fontWeight: '600', + }, + // Paid button + paidButton: { + backgroundColor: '#D1FAE5', + paddingHorizontal: 20, + paddingVertical: 10, + borderRadius: 10, + }, + paidButtonText: { + color: '#065F46', + fontSize: 14, + fontWeight: '600', + }, viewButton: { backgroundColor: colors.pastelOrange, paddingHorizontal: 20, diff --git a/src/components/common/PastelActivityIndicator.tsx b/src/components/common/PastelActivityIndicator.tsx new file mode 100644 index 0000000..abd8aca --- /dev/null +++ b/src/components/common/PastelActivityIndicator.tsx @@ -0,0 +1,86 @@ +import React, { useRef, useEffect } from 'react'; +import { View, Animated, StyleSheet, Easing, ViewStyle } from 'react-native'; +import colors from '../colors'; + +const pastelColors: string[] = [ + colors.pastelPink, // pastelPink + '#D7E9F7', // pastelBlue + '#DFF6DD', // pastelGreen + '#FFF4D2', // pastelOrange +]; + +const DOT_SIZE = 16; +const RADIUS = 30; +const DURATION = 1200; + +const PastelActivityIndicator: React.FC = () => { + const rotateAnim = useRef(new Animated.Value(0)).current; + + useEffect(() => { + Animated.loop( + Animated.timing(rotateAnim, { + toValue: 1, + duration: DURATION, + easing: Easing.inOut(Easing.ease), + useNativeDriver: true, + }) + ).start(); + }, [rotateAnim]); + + const getDotStyle = (index: number): ViewStyle => { + const angle = (index * (360 / pastelColors.length)) * (Math.PI / 180); + return { + position: 'absolute', + left: RADIUS + Math.cos(angle) * RADIUS - DOT_SIZE / 2, + top: RADIUS + Math.sin(angle) * RADIUS - DOT_SIZE / 2, + backgroundColor: pastelColors[index], + }; + }; + + // Pulsating scale for each dot (same scale for demo) + const scales = pastelColors.map((_color, idx) => + rotateAnim.interpolate({ + inputRange: [0, 1], + outputRange: [1, 1.3], + extrapolate: 'clamp', + }) + ); + + return ( + + {pastelColors.map((color, idx) => ( + + ))} + + ); +}; + +const styles = StyleSheet.create({ + dot: { + width: DOT_SIZE, + height: DOT_SIZE, + borderRadius: DOT_SIZE / 2, + }, +}); + +export default PastelActivityIndicator; diff --git a/src/screens/LoginScreen.tsx b/src/screens/LoginScreen.tsx index abaad1b..57a7d00 100644 --- a/src/screens/LoginScreen.tsx +++ b/src/screens/LoginScreen.tsx @@ -19,7 +19,7 @@ import type { StackNavigationProp } from '@react-navigation/stack'; import { RootStackParamList } from '../navigation/AppNavigator'; import LinearGradient from 'react-native-linear-gradient'; import Ionicons from '@react-native-vector-icons/ionicons'; -import MaterialRipple from 'react-native-material-ripple'; // For the button ripple effect +import MaterialRipple from 'react-native-material-ripple'; import CustomAlert from '../components/CostomAlart'; import colors from '../components/colors'; @@ -310,4 +310,4 @@ const styles = StyleSheet.create({ textAlign: 'center', maxWidth: 300, }, -}); +}); \ No newline at end of file diff --git a/src/screens/home/Home.tsx b/src/screens/home/Home.tsx index e10113b..902200a 100644 --- a/src/screens/home/Home.tsx +++ b/src/screens/home/Home.tsx @@ -22,6 +22,7 @@ import UserProfileStatus from "./UserProfileStatus"; import AnimatedCard from "../../components/common/AnimatedCard"; import { Ionicons, IoniconsIconName } from "@react-native-vector-icons/ionicons"; import { apiCall } from "../../utils/ApiHelper"; +import CustomAlert, { CustomAlertProps } from "../../components/CostomAlart"; if (Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental) { UIManager.setLayoutAnimationEnabledExperimental(true); @@ -34,6 +35,7 @@ type UserProfile = { lastName: string | null; imageBlob: string | null; role?: string; + organizerStatus?: string; }; interface ActionItem { @@ -65,18 +67,21 @@ const userActions: ActionItem[] = [ const Home: React.FC = () => { const user = useSelector((state: any) => state.user); const notifications = useSelector( - (state: any) => state.notifications.notifications - ); + (state: any) => state.notifications.notifications + ); const navigation = useNavigation(); const dispatch = useDispatch(); const actions = user.role === 'ORGANIZER' ? organizerActions : userActions; const [apiUser, setApiUser] = useState(null); + const [alartVisible, setalartVisible] = useState(false) + const [alartAction, setAlartAction] = useState(null) useEffect(() => { const fetchUserProfile = async () => { try { const result = await apiCall(PROFILE_URL, "GET") as any; + console.log(PROFILE_URL, result) if (result.status === "success") { setApiUser(result.data); dispatch(setUserData({ ...result.data, profileStatus: true })); @@ -96,10 +101,61 @@ const Home: React.FC = () => { } else if (item.action === "TRANSACTION") { navigation.navigate("TransactionList" as never); } else if (item.action === "NAVIGATE_CREATE_GROUP") { - - navigation.navigate("GroupCreation" as never); - } else { - Alert.alert("Unknown action", item.title); + switch (apiUser?.organizerStatus) { + case 'NOT_APPLIED': + setalartVisible(true); + setAlartAction({ + visible: true, + title: "Become organizer", + message: "You have to become organizer for creating groups", + buttons: [ + { + text: "Continue", + onPress: () => { + setalartVisible(false); + navigation.navigate("BecomeOrg" as never); + }, + style: "default", + }, + { + text: "Close", + onPress: () => { + setalartVisible(false); + }, + style: "cancel", + }, + ], + alertType: 'error', + onClose: () => setAlartAction(null), + }); + break; + + case 'PENDING': + setalartVisible(true); + setAlartAction({ + visible: true, + title: "Organizer Application Pending", + message: "Your application to become an organizer is pending approval. You cannot create groups until it's approved.", + buttons: [ + { + text: "Close", + onPress: () => { + setalartVisible(false); + }, + style: "cancel", + }, + ], + alertType: 'info', + onClose: () => setAlartAction(null), + }); + break; + case 'APPROVED': + navigation.navigate("GroupCreation" as never); + break; + default: + Alert.alert("Unknown action", item.title); + break; + } } }; @@ -133,7 +189,7 @@ const Home: React.FC = () => { navigation.navigate("Notification" as never)}> {/* */} - {unreadCount > 0 ? unreadCount : ''} + {unreadCount > 0 ? unreadCount : ''} @@ -149,6 +205,13 @@ const Home: React.FC = () => { numColumns={2} contentContainerStyle={{ paddingHorizontal: 8 }} /> + {/* */} ); }; diff --git a/src/screens/management/ManagementScreen.tsx b/src/screens/management/ManagementScreen.tsx index 0bcad58..f8c9f32 100644 --- a/src/screens/management/ManagementScreen.tsx +++ b/src/screens/management/ManagementScreen.tsx @@ -74,6 +74,7 @@ const ManagementScreen: FC = () => { apiCall(GET_JOINED_GROUP_URL, "GET") as Promise>, apiCall(USER_REQUEST, "GET") as Promise>, ]); + console.log("GET_JOINED_GROUP_URL",joinedGroupsRes) setJoinedGroups(joinedGroupsRes.data || []); setUserRequest(userRequestRes.data || []); } diff --git a/src/screens/profile/Profile.tsx b/src/screens/profile/Profile.tsx index 2cf62e0..85b880d 100644 --- a/src/screens/profile/Profile.tsx +++ b/src/screens/profile/Profile.tsx @@ -361,15 +361,18 @@ const Profile = () => { console.log("user image :", user?.filePath); - const getProfileImage = () => { - if (user?.imageBlob) { - return `data:image/jpeg;base64,${user?.imageBlob}`; - } - if (user?.filePath) { - return user?.filePath; - } - return "https://randomuser?.me/api/portraits/men/1.jpg"; - }; + const getProfileImage = () => { + return require('../../assets/images/placeholder.png'); + if (user?.imageBlob) { + return { uri: `data:image/jpeg;base64,${user.imageBlob}` }; + } + if (user?.filePath) { + return { uri: user.filePath }; + } + // Return a require for local asset if nothing is available + return require('../../assets/images/placeholder.png'); +}; + @@ -381,6 +384,7 @@ const Profile = () => { User Profile confirmLogout(navigation)} style={styles.refreshButton}> + Logout @@ -435,9 +439,10 @@ const Profile = () => { console.log("Error loading image")} + resizeMode='contain' /> @@ -465,9 +470,7 @@ const Profile = () => { Become Organizer - navigation.navigate('Transaction')}> - View Transaction - + setEditing(true)} @@ -702,10 +705,11 @@ const styles = StyleSheet.create({ borderBottomRightRadius: 20, }, headerTitle: { - fontSize: 22, fontWeight: "600", color: colors.black + fontSize: 22, fontWeight: "600", color: colors.black }, refreshButton: { padding: 4, + flexDirection: "row", }, @@ -733,7 +737,7 @@ const styles = StyleSheet.create({ }, avatar: { width: 120, - height: 120, + height: 130, borderRadius: 60, borderWidth: 4, borderColor: "#e2e8f0", diff --git a/src/utils/firebase/firebaseServices.ts b/src/utils/firebase/firebaseServices.ts index 8f4422a..58cbd50 100644 --- a/src/utils/firebase/firebaseServices.ts +++ b/src/utils/firebase/firebaseServices.ts @@ -261,7 +261,7 @@ export const foregroundListener = () => { const body = notification?.body || data?.body || ''; // Display local notification - await displayNotification(title, body, data); + await displayNotification(title as string, body as string, data); // Add to Redux store processAndAddNotification(remoteMessage); @@ -277,11 +277,12 @@ export const backgroundHandler = () => { if (!remoteMessage) return; const { notification, data } = remoteMessage; - const title = notification?.title || data?.title || 'New Message'; + const title = notification?.title ?? data?.title ?? 'New Message'; + const body = notification?.body || data?.body || ''; // Display local notification - await displayNotification(title, body, data); + await displayNotification(title as string, body as string, data); // Add to Redux store processAndAddNotification(remoteMessage);