theme update latest project

This commit is contained in:
Abin Toppo
2026-02-16 10:41:38 +05:30
parent bd7dbd864c
commit a98e0b6cdd
75 changed files with 10173 additions and 7915 deletions
+170 -200
View File
@@ -1,10 +1,12 @@
import React, { FC } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ToastAndroid } from 'react-native';
import colors from './colors';
import { useNavigation } from '@react-navigation/native';
import { BASE_URL, CONTRIBUTE } from '../utils/ApiUrls';
import { apiCall } from '../utils/ApiHelper';
import { responsiveFontSize } from 'react-native-responsive-dimensions';
import { useTheme } from '../hooks/ui/useTheme';
import { useFetch } from '../hooks/core/useFetch';
import { useCreate } from '../hooks/core/useCreate';
import { CONTRIBUTE } from '../utils/ApiUrls';
import FontsFamily from '../constents/FontsFamily';
interface JoinedGroup {
id: string;
@@ -13,8 +15,8 @@ interface JoinedGroup {
visibility: 'PUBLIC' | 'PRIVATE';
createdByUserId: number;
createdByName: string;
startDate: [number, number, number] | null;
endDate: [number, number, number] | null;
startDate: string | null;
endDate: string | null;
minParticipants: number;
maxParticipants: number;
currentParticipants: number;
@@ -24,138 +26,141 @@ interface JoinedGroup {
}
interface JoinedGroupCardProps {
item: JoinedGroup | any;
onViewPress: () => void;
onPayPress: () => void; // function for Pay button
hasPaid?: boolean; // whether user has already paid
item: JoinedGroup;
hasPaid?: boolean;
}
const formatDate = (dateStr: string | null): string => {
if (!dateStr) return 'N/A';
const [year, month, day] = dateStr.split('-');
return `${day}-${month}-${year}`;
const months = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
];
const monthIndex = parseInt(month, 10) - 1;
const dayNum = parseInt(day, 10);
return `${dayNum} ${months[monthIndex]} ${year}`;
};
const getStatusStyle = (status: JoinedGroup['status']) => {
switch (status) {
case 'ACTIVE':
return {
backgroundColor: '#D1FAE5',
textColor: '#065F46',
};
case 'PENDING':
return {
backgroundColor: '#FEF3C7',
textColor: '#92400E',
};
case 'COMPLETED':
return {
backgroundColor: '#E5E7EB',
textColor: '#4B5563',
};
default:
return {
backgroundColor: '#E5E7EB',
textColor: '#4B5563',
};
}
};
export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item, onViewPress, hasPaid }) => {
const progress = (item.currentParticipants / item.maxParticipants) * 100;
const statusStyle = getStatusStyle(item.status);
export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item }) => {
const theme = useTheme();
const navigation = useNavigation<any>();
const [loading, setLoading] = React.useState(false);
const [payable, setPayable] = React.useState<any>();
const { data: payableRes } = useFetch<any>(`${CONTRIBUTE}${item.id}/calculate-contribution`);
const payable = payableRes?.data;
const { mutate: contribute, isPending: loading } = useCreate<any, any>(
`payments/contribute/${item?.id}?amount=${payable?.totalPayable}`,
{
onSuccess: () => {
ToastAndroid.show('Payment successful!', ToastAndroid.SHORT);
},
onError: () => {
ToastAndroid.show('Payment failed. Please try again.', ToastAndroid.SHORT);
},
}
);
const onPayPress = async () => {
// Navigate to payment screen or handle payment logic
// navigation.navigate('PaymentScreen', { groupId: item.id, amount: item.contributionAmount });
setLoading(true);
try {
const res = await apiCall(`${BASE_URL}payments/contribute/${item?.id}?amount=${payable?.totalPayable}`, 'POST') as any;
console.log('payment res ', res)
if (res.status === 200 || res.status === 201 || res.statusCode === 200) {
ToastAndroid.show('Payment done!', ToastAndroid.SHORT);
setLoading(false);
}
} catch (error) {
ToastAndroid.show("An error occurred during payment.", ToastAndroid.SHORT);
} finally {
setLoading(false);
const getStatusColors = (status: JoinedGroup['status']) => {
switch (status) {
case 'ACTIVE':
return { bg: theme.palette.greyExtraLight, text: theme.palette.black };
case 'COMPLETED':
return { bg: theme.palette.greyLight, text: theme.palette.greyDark };
default:
return { bg: theme.palette.greyExtraLight, text: theme.palette.greyDark };
}
};
const handleContribution = async () => {
try {
const res = await apiCall(`${CONTRIBUTE}${item.id}/calculate-contribution`, 'GET') as any;
setPayable(res.data);
} catch (error) {
console.log("Error in handleContribution: ", error);
}
};
React.useEffect(() => {
handleContribution();
}, []);
const statusColors = getStatusColors(item.status);
return (
<TouchableOpacity style={styles.card} onPress={() => navigation.navigate("GroupDetails", { item, color: colors.black })}>
{/* Header */}
<TouchableOpacity
style={[styles.card, { backgroundColor: theme.colors.card, borderColor: theme.colors.border }]}
onPress={() => navigation.navigate('GroupDetails', { item })}
activeOpacity={0.7}
>
<View style={styles.header}>
<Text style={styles.groupName}>{item.groupName}</Text>
<View style={[styles.statusBadge, { backgroundColor: statusStyle.backgroundColor }]}>
<Text style={[styles.statusText, { color: statusStyle.textColor }]}>{item.status}</Text>
<View style={styles.titleContainer}>
<Text style={[styles.groupName, { color: theme.colors.textPrimary }]}>{item.groupName}</Text>
<Text style={[styles.createdBy, { color: theme.colors.textSecondary }]}>
by {item.createdByName}
</Text>
</View>
<View style={[styles.statusBadge, { backgroundColor: statusColors.bg }]}>
<Text style={[styles.statusText, { color: statusColors.text }]}>{item.status}</Text>
</View>
</View>
<Text style={styles.createdBy}>Created by {item.createdByName}</Text>
{/* Financial Info */}
<View style={styles.infoRow}>
{/* Contribution Amount — always visible */}
<View style={[styles.infoRow, { borderColor: theme.colors.border }]}>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>Contribution</Text>
<Text style={styles.infoValue}>₹{item.contributionAmount.toLocaleString('en-IN')}</Text>
<Text style={[styles.infoLabel, { color: theme.colors.textMuted }]}>Contribution</Text>
<Text style={[styles.infoValue, { color: theme.colors.textPrimary }]}>
UGX {item.contributionAmount.toLocaleString()}
</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>Cycle</Text>
<Text style={styles.infoValue}>{item.cycleType}</Text>
<Text style={[styles.infoLabel, { color: theme.colors.textMuted }]}>Type</Text>
<Text style={[styles.infoValue, { color: theme.colors.textPrimary }]}>{item.cycleType}</Text>
</View>
</View>
{payable?.effectiveLateFee && (
<>
<Text style={{ marginBottom: 8, fontSize: 14, color: '#050911ff' }}>Total Payable: ₹{payable?.totalPayable.toLocaleString('en-IN')}</Text>
<Text style={{ marginBottom: 8, fontSize: 14, color: '#050911ff' }}>Effective Late Fee : ₹{payable?.effectiveLateFee.toLocaleString('en-IN')}</Text>
</>
{/* Payment Details — shown when NOT paid and there's an amount due */}
{payable && !payable.hasPaid && payable.totalPayable > 0 && (
<View style={styles.payableSection}>
<View style={styles.payableRow}>
<Text style={[styles.payableLabel, { color: theme.colors.textSecondary }]}>Total Payable</Text>
<Text style={[styles.payableValue, { color: theme.colors.textPrimary }]}>
UGX {payable.totalPayable.toLocaleString()}
</Text>
</View>
{payable.effectiveLateFee > 0 && (
<View style={styles.payableRow}>
<Text style={[styles.payableLabel, { color: theme.colors.warning }]}>Late Fee</Text>
<Text style={[styles.payableValue, { color: theme.colors.warning }]}>
+ UGX {payable.effectiveLateFee.toLocaleString()}
</Text>
</View>
)}
{payable.alreadyPaid > 0 && (
<View style={styles.payableRow}>
<Text style={[styles.payableLabel, { color: theme.colors.success }]}>Already Paid</Text>
<Text style={[styles.payableValue, { color: theme.colors.success }]}>
UGX {payable.alreadyPaid.toLocaleString()}
</Text>
</View>
)}
</View>
)}
{/* Footer with Dates & Buttons */}
<View style={styles.footer}>
<Text style={styles.dateText}>
{formatDate(item.startDate)} to {formatDate(item.endDate)}
<Text style={[styles.dateText, { color: theme.colors.textMuted }]}>
{formatDate(item.startDate)} — {formatDate(item.endDate)}
</Text>
{/* Add Pay / Paid button here */}
{/* {hasPaid ? (
<View style={styles.paidButton}>
<Text style={styles.paidButtonText}>Paid</Text>
{payable?.hasPaid ? (
<View style={[styles.paidBadge, { backgroundColor: theme.colors.success + '18' }]}>
<Text style={[styles.paidText, { color: theme.colors.success }]}>✓ PAID</Text>
</View>
) : (
<TouchableOpacity style={styles.payButton} onPress={onPayPress}>
<Text style={styles.payButtonText}>{loading ? 'Loading...' : ' Pay'}</Text>
</TouchableOpacity>
)} */}
{
hasPaid && <View style={styles.paidButton}>
<Text style={styles.paidButtonText}>Paid</Text>
</View>
}
{
item.status !== 'COMPLETED' && <TouchableOpacity style={styles.payButton} onPress={onPayPress}>
<Text style={styles.payButtonText}>{loading ? 'Loading...' : ' Pay'}</Text>
</TouchableOpacity>
}
item.status !== 'COMPLETED' && (
<TouchableOpacity
style={[styles.payButton, { backgroundColor: theme.colors.primary }]}
onPress={() => {
navigation.navigate('PaymentMode' as any, {
groupId: item.id,
routeFrom: 'GroupDetails'
})
}}
disabled={loading}
>
<Text style={[styles.payButtonText, { color: theme.colors.onPrimary }]}>
{loading ? '...' : 'Pay'}
</Text>
</TouchableOpacity>
)
)}
</View>
</TouchableOpacity>
);
@@ -163,144 +168,109 @@ export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item, onViewPress, h
const styles = StyleSheet.create({
card: {
backgroundColor: colors.pastelOrange,
borderRadius: 16,
padding: 20,
marginBottom: 16,
shadowColor: '#374151',
shadowOpacity: 0.08,
shadowRadius: 10,
shadowOffset: { width: 0, height: 4 },
elevation: 5,
marginHorizontal: 12
padding: 14,
marginBottom: 12,
marginHorizontal: 12,
borderWidth: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 8,
elevation: 2,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 4,
marginBottom: 10,
},
titleContainer: {
flex: 1,
},
groupName: {
fontSize: responsiveFontSize(2),
fontWeight: 'bold',
color: '#1F2937',
flexShrink: 1,
fontFamily: FontsFamily.nunitoExtraBold,
marginBottom: 2,
},
createdBy: {
fontSize: responsiveFontSize(1.4),
color: '#6B7280',
marginBottom: 16,
fontFamily: FontsFamily.nunitoRegular,
},
statusBadge: {
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 12,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 6,
},
statusText: {
fontSize: responsiveFontSize(1.2),
fontWeight: '600',
textTransform: 'uppercase',
fontFamily: FontsFamily.nunitoBold,
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 16,
paddingVertical: 10,
borderTopWidth: 1,
borderBottomWidth: 1,
borderColor: colors.pastelOrange,
marginBottom: 16,
backgroundColor: colors.white,
borderRadius: 15,
marginBottom: 10,
},
infoItem: {
padding: 5,
paddingHorizontal: 10,
backgroundColor: colors.white,
alignItems: 'flex-start',
flex: 1,
},
infoLabel: {
fontSize: responsiveFontSize(1.4),
color: '#6B7280',
marginBottom: 4,
fontSize: responsiveFontSize(1.3),
fontFamily: FontsFamily.nunitoRegular,
marginBottom: 2,
},
infoValue: {
fontSize: responsiveFontSize(1.6),
fontWeight: '600',
color: '#111827',
textTransform: 'capitalize',
fontFamily: FontsFamily.nunitoBold,
},
progressContainer: {
marginBottom: 16,
payableSection: {
marginBottom: 12,
gap: 6,
},
progressLabels: {
payableRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
alignItems: 'center',
},
participantText: {
fontSize: responsiveFontSize(1.4),
color: '#6B7280',
payableLabel: {
fontSize: responsiveFontSize(1.35),
fontFamily: FontsFamily.nunitoSemiBold,
},
participantCount: {
fontSize: responsiveFontSize(1.4),
fontWeight: '600',
color: '#111827',
},
progressBarBackground: {
height: 8,
backgroundColor: '#E5E7EB',
borderRadius: 4,
overflow: 'hidden',
},
progressBarFill: {
height: '100%',
backgroundColor: colors.pastelOrange,
borderRadius: 4,
payableValue: {
fontSize: responsiveFontSize(1.5),
fontFamily: FontsFamily.nunitoBold,
},
footer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: 4,
},
dateText: {
fontSize: responsiveFontSize(1.4),
color: '#4B5563',
fontWeight: '500',
fontSize: responsiveFontSize(1.3),
fontFamily: FontsFamily.nunitoMedium,
},
// Pay button
payButton: {
elevation: 5,
backgroundColor: colors.white,
paddingHorizontal: 25,
paddingVertical: 10,
borderRadius: 10,
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 8,
minWidth: 80,
alignItems: 'center',
},
payButtonText: {
color: colors.black,
fontSize: responsiveFontSize(1.4),
fontWeight: '600',
fontSize: responsiveFontSize(1.5),
fontFamily: FontsFamily.nunitoBold,
},
// Paid button
paidButton: {
backgroundColor: '#D1FAE5',
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 10,
paidBadge: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 6,
},
paidButtonText: {
color: '#065F46',
fontSize: responsiveFontSize(1.4),
fontWeight: '600',
},
viewButton: {
backgroundColor: colors.pastelOrange,
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 10,
},
viewButtonText: {
color: '#000000ff',
fontSize: responsiveFontSize(1.4),
fontWeight: '600',
paidText: {
fontSize: responsiveFontSize(1.3),
fontFamily: FontsFamily.nunitoBold,
letterSpacing: 0.5,
},
});