diff --git a/package-lock.json b/package-lock.json index ef44305..76b3c80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "@react-native-clipboard/clipboard": "^1.16.3", + "@react-native-community/blur": "^4.4.1", "@react-native-community/slider": "^5.0.1", "@react-native-picker/picker": "^2.11.2", "@react-native-vector-icons/ionicons": "^12.3.0", @@ -36,6 +37,7 @@ "react-native-safe-area-context": "^5.6.1", "react-native-screens": "^4.16.0", "react-native-svg": "^15.13.0", + "react-native-vector-icons": "^10.3.0", "react-native-walkthrough-tooltip": "^1.6.0", "react-redux": "^9.2.0" }, @@ -2766,6 +2768,16 @@ } } }, + "node_modules/@react-native-community/blur": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@react-native-community/blur/-/blur-4.4.1.tgz", + "integrity": "sha512-XBSsRiYxE/MOEln2ayunShfJtWztHwUxLFcSL20o+HNNRnuUDv+GXkF6FmM2zE8ZUfrnhQ/zeTqvnuDPGw6O8A==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@react-native-community/cli": { "version": "20.0.0", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.0.0.tgz", @@ -13578,7 +13590,6 @@ "integrity": "sha512-IFQ0RE57819hOUdFvgK4FowM5aMXg7C7XKsuGLevqXkkIJatc3QopN0wYrb2IrzUgmdpfP+QVIbI3S6h7M0btw==", "deprecated": "react-native-vector-icons package has moved to a new model of per-icon-family packages. See the https://github.com/oblador/react-native-vector-icons/blob/master/MIGRATION.md on how to migrate", "license": "MIT", - "peer": true, "dependencies": { "prop-types": "^15.7.2", "yargs": "^16.1.1" @@ -13595,7 +13606,6 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "license": "ISC", - "peer": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -13607,7 +13617,6 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "license": "MIT", - "peer": true, "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -13626,7 +13635,6 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "license": "ISC", - "peer": true, "engines": { "node": ">=10" } diff --git a/package.json b/package.json index 8774c41..86868e2 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@react-native-clipboard/clipboard": "^1.16.3", + "@react-native-community/blur": "^4.4.1", "@react-native-community/slider": "^5.0.1", "@react-native-picker/picker": "^2.11.2", "@react-native-vector-icons/ionicons": "^12.3.0", @@ -38,6 +39,7 @@ "react-native-safe-area-context": "^5.6.1", "react-native-screens": "^4.16.0", "react-native-svg": "^15.13.0", + "react-native-vector-icons": "^10.3.0", "react-native-walkthrough-tooltip": "^1.6.0", "react-redux": "^9.2.0" }, diff --git a/src/assets/images/placeholder.png b/src/assets/images/placeholder.png new file mode 100644 index 0000000..c7645dd Binary files /dev/null and b/src/assets/images/placeholder.png differ diff --git a/src/components/CostomAlart.tsx b/src/components/CostomAlart.tsx new file mode 100644 index 0000000..25cb593 --- /dev/null +++ b/src/components/CostomAlart.tsx @@ -0,0 +1,177 @@ +// src/components/CustomAlert.tsx + +import React from "react"; +import { + Modal, + View, + Text, + TouchableOpacity, + StyleSheet, + 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 + +interface AlertButton { + text: string; + onPress: () => void; + style?: "cancel" | "destructive" | "default"; +} + +interface CustomAlertProps { + visible: boolean; + title: string; + message: string; + buttons: AlertButton[]; + alertType?: 'error' | 'success' | 'info'; +} + +const CustomAlert: React.FC = ({ + visible, + title, + message, + buttons, + alertType = 'info', +}) => { + + const getIcon = () => { + switch (alertType) { + case 'error': + return { name: "close-circle", color: colors.error }; + case 'success': + return { name: "checkmark-circle", color: colors.success }; + default: + return { name: "information-circle", color: colors.primary }; + } + }; + + const icon = getIcon(); + + return ( + { + const closeButton = buttons.find(b => b.style === 'cancel') || buttons[0]; + closeButton?.onPress(); + }} + > + + {Platform.OS === 'ios' ? ( + + ) : ( + + )} + + + + + + {title} + {message} + + + {buttons.map((button, index) => ( + + + {button.text} + + + ))} + + + + + ); +}; + +const styles = StyleSheet.create({ + centeredView: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + absolute: { + position: 'absolute', + top: 0, + left: 0, + 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, + width: '90%', + maxWidth: 400, + }, + iconContainer: { + marginBottom: 16, + }, + alertTitle: { + fontSize: 22, + fontWeight: "bold", + color: colors.textDark, + marginBottom: 8, + textAlign: "center", + }, + alertMessage: { + fontSize: 16, + color: colors.textLight, + textAlign: "center", + marginBottom: 24, + lineHeight: 22, + }, + buttonContainer: { + flexDirection: "row", + justifyContent: 'flex-end', + width: "100%", + gap: 12, + }, + button: { + paddingVertical: 14, + paddingHorizontal: 24, + borderRadius: 16, + justifyContent: 'center', + alignItems: 'center', + }, + defaultButton: { + backgroundColor: colors.primary, + }, + destructiveButton: { + backgroundColor: colors.error, + }, + buttonText: { + fontSize: 16, + fontWeight: "700", + }, + defaultButtonText: { + color: colors.white, + }, + destructiveButtonText: { + color: colors.white, + }, +}); + +export default CustomAlert; diff --git a/src/components/colors.ts b/src/components/colors.ts index 0abc115..a318f82 100644 --- a/src/components/colors.ts +++ b/src/components/colors.ts @@ -1,22 +1,27 @@ -// colors.ts -const colors = { - pastelPink: "#decbfcff", // darker pink - pastelBlue: "#D7E9F7", // darker blue - pastelGreen: "#DFF6DD", // darker green - pastelOrange: "#FFF4D2", // darker orange +// src/components/colors.ts - // darker accent colors - Home: "#995dfaff", // Rose red for contrast - Profile: "#2D9CDB", // Deep sky blue - Group: "#27AE60", // Green for freshness - Settings: "#F39C12", // Amber for warmth +const colors = { + // Pastel palette + pastelPink: "#decbfc", + pastelBlue: "#D7E9F7", + pastelGreen: "#DFF6DD", + pastelOrange: "#FFF4D2", - // common text + UI colors - textDark: "#212121", - textLight: "#555555", + // Primary and status colors + primary: "#711ff7", + error: "#EF4444", + success: "#22C55E", + + // Common UI colors + textDark: "#1F2937", + textLight: "#6B7280", white: "#FFFFFF", black: "#000000", - primary: "#711ff7ff", + inputBorder: "#E5E7EB", + + // Special background colors with transparency + alertBackground: "rgba(255, 255, 255, 0.8)", + androidBackdrop: "rgba(0, 0, 0, 0.4)", }; export default colors; diff --git a/src/components/common/AnimatedCard.tsx b/src/components/common/AnimatedCard.tsx index c4296a9..b1fa10c 100644 --- a/src/components/common/AnimatedCard.tsx +++ b/src/components/common/AnimatedCard.tsx @@ -5,10 +5,12 @@ import { StyleSheet, TouchableWithoutFeedback, Animated, + ViewStyle, } from "react-native"; import LinearGradient from "react-native-linear-gradient"; -import { Ionicons, IoniconsIconName } from "@react-native-vector-icons/ionicons"; -import colors from "../colors"; +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 type CardProps = { title: string; @@ -16,7 +18,8 @@ type CardProps = { iconName?: IoniconsIconName; onPress?: () => void; backOne?: string; - backTwo?: string + backTwo?: string; + style?: ViewStyle; }; const AnimatedCard: React.FC = ({ @@ -24,33 +27,27 @@ const AnimatedCard: React.FC = ({ subtitle, iconName = "ellipse", onPress, - backOne = '#4facfe', - backTwo = '#00f2fe' + backOne = colors.pastelPink, // Defaulting to pastelPink + backTwo = colors.primary, + style, }) => { const scale = useRef(new Animated.Value(1)).current; 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 = () => { - Animated.spring(scale, { - toValue: 1, - useNativeDriver: true, - friction: 4, - }).start(() => onPress && onPress()); + Animated.spring(scale, { toValue: 1, useNativeDriver: true, friction: 4 }).start(() => { + if (onPress) { + onPress(); + } + }); }; return ( - - + + = ({ > {/* Decorative Graphics */} - - - {/* Content */} - {iconName && ( - - )} + {iconName && } {title} {subtitle && {subtitle}} @@ -87,102 +74,70 @@ const AnimatedCard: React.FC = ({ const styles = StyleSheet.create({ card: { - // marginHorizontal: 16, marginVertical: 10, - borderRadius: 20, + borderRadius: 24, overflow: "hidden", shadowColor: "#000", - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.25, - shadowRadius: 8, - elevation: 8, + shadowOffset: { width: 0, height: 5 }, + shadowOpacity: 0.15, + shadowRadius: 12, + elevation: 5, + backgroundColor: colors.white, }, gradient: { - minHeight: 120, + minHeight: 150, // Increased height for better spacing position: "relative", + flex: 1, + justifyContent: "center", + alignItems: "center", }, - // Graphics graphicCircleLarge: { position: "absolute", - top: -20, - right: -20, - width: 80, - height: 80, - borderRadius: 40, - backgroundColor: "rgba(124, 80, 80, 0.42)", - }, - graphicCircleMedium: { - position: "absolute", - bottom: 30, - left: -15, - width: 60, - height: 60, - borderRadius: 30, - backgroundColor: "rgba(122, 41, 41, 0.39)", + top: -30, + right: -40, + width: 100, + height: 100, + borderRadius: 50, + backgroundColor: "rgba(255, 255, 255, 0.1)", }, graphicCircleSmall: { position: "absolute", - bottom: -10, - right: 40, - width: 40, - height: 40, - borderRadius: 20, - backgroundColor: "rgba(207, 110, 80, 0.25)", - }, - graphicTriangleTop: { - position: "absolute", - top: 0, - left: 0, - width: 0, - height: 0, - borderStyle: "solid", - borderRightWidth: 60, - borderTopWidth: 60, - borderRightColor: "transparent", - borderTopColor: "rgba(143, 62, 62, 0.31)", - }, - graphicTriangleBottom: { - position: "absolute", - bottom: 0, - right: 0, - width: 0, - height: 0, - borderStyle: "solid", - borderLeftWidth: 40, - borderBottomWidth: 40, - borderLeftColor: "transparent", - borderBottomColor: "rgba(172, 51, 51, 0.27)", + bottom: -20, + left: -25, + width: 70, + height: 70, + borderRadius: 35, + backgroundColor: "rgba(255, 255, 255, 0.08)", }, content: { - flexDirection: "row", // stack vertically - alignItems: "center", // center horizontally - justifyContent: "center", // center vertically padding: 20, position: "relative", zIndex: 1, - alignSelf: "center", - flex:1 - } - , + flex: 1, + justifyContent: "center", + alignItems: "center", + }, icon: { - marginRight: 12, + marginBottom: 12, }, textContainer: { - // alignItems: "center", // centers title and subtitle + alignItems: 'center', }, title: { fontSize: 18, - fontWeight: "700", + fontWeight: "bold", color: "#fff", - marginBottom: 4, - textTransform:'uppercase' - // textAlign: "center", + textTransform: 'uppercase', + letterSpacing: 1, + textAlign: 'center', }, subtitle: { - fontSize: 14, - color: colors.primary, + fontSize: 13, + color: colors.white, + opacity: 0.9, fontWeight: "500", textAlign: "center", + marginTop: 4, }, }); diff --git a/src/components/common/Custom.tsx b/src/components/common/Custom.tsx index 5855ceb..3585eb7 100644 --- a/src/components/common/Custom.tsx +++ b/src/components/common/Custom.tsx @@ -15,10 +15,12 @@ type CustomInputProps = TextInputProps & { label?: string; icon?: IoniconsIconName; error?:string + value?: string | number; }; const CustomInput: React.FC = ({ label, + value, icon, style, error, @@ -28,7 +30,7 @@ const CustomInput: React.FC = ({ {label ? {label} : null} - {icon && } + {icon && } = ({ // --- Styles --- const styles = StyleSheet.create({ - inputContainer: { marginBottom: 12 }, + inputContainer: { marginBottom: 12,elevation:5 }, inputLabel: { fontSize: 14, fontWeight: "500", color: colors.textDark, marginBottom: 6 }, inputWrapper: { flexDirection: "row", @@ -97,9 +99,7 @@ const styles = StyleSheet.create({ }, input: { flex: 1, - borderWidth: 1, - borderColor: colors.textLight, - borderRadius: 10, + borderRadius: 15, padding: 12, paddingLeft: 12, backgroundColor: colors.white, @@ -107,6 +107,7 @@ const styles = StyleSheet.create({ fontSize: 15, }, button: { + elevation:5, paddingVertical: 12, borderRadius: 12, alignItems: "center", diff --git a/src/screens/LoginScreen.tsx b/src/screens/LoginScreen.tsx index 5b37f77..3a44f1b 100644 --- a/src/screens/LoginScreen.tsx +++ b/src/screens/LoginScreen.tsx @@ -1,3 +1,5 @@ +// src/screens/LoginScreen.tsx + import React, { useState, useRef, useEffect } from "react"; import { View, @@ -5,7 +7,6 @@ import { TextInput, StyleSheet, TouchableOpacity, - Alert, KeyboardAvoidingView, Platform, Animated, @@ -17,175 +18,268 @@ import axios from "axios"; import { useNavigation } from "@react-navigation/native"; import type { StackNavigationProp } from "@react-navigation/stack"; import { RootStackParamList } from "../navigation/AppNavigator"; -import { CustomButton, CustomInput } from "../components/common/Custom"; -import colors from "../components/colors"; - -const theme = { - primary: "#111111", - accent: colors.primary, - background: "#F9F9F9", - surface: "#FFFFFF", - text: "#1A1A1A", - subText: "#6B6B6B", - outline: "#E5E5E5", - error: "#E53935", - success: "#2E7D32", -}; +import LinearGradient from "react-native-linear-gradient"; +import Ionicons from '@react-native-vector-icons/ionicons'; +import CustomAlert from "../components/CostomAlart"; +import colors from "../components/colors"; // Centralized colors from your file export default function LoginScreen() { const navigation = useNavigation>(); const [mobile, setMobile] = useState(""); const [loading, setLoading] = useState(false); - // animations + const [alertInfo, setAlertInfo] = useState({ + visible: false, + title: "", + message: "", + buttons: [] as { text: string; onPress: () => void; style?: "cancel" | "destructive" | "default" }[], + alertType: 'info' as 'info' | 'success' | 'error', + }); + const fade = useRef(new Animated.Value(0)).current; - const slide = useRef(new Animated.Value(24)).current; + const slide = useRef(new Animated.Value(30)).current; const ctaScale = useRef(new Animated.Value(1)).current; useEffect(() => { Animated.parallel([ - Animated.timing(fade, { toValue: 1, duration: 600, useNativeDriver: true }), - Animated.timing(slide, { toValue: 0, duration: 600, useNativeDriver: true }), + Animated.timing(fade, { toValue: 1, duration: 800, useNativeDriver: true }), + Animated.spring(slide, { toValue: 0, friction: 5, tension: 40, useNativeDriver: true }), ]).start(); }, []); - const handleContinue = async () => { - if (mobile.length === 10) { - setLoading(true); - try { - const res = await axios.get(LOGIN_URL, { - params: { mobile }, - }); + const showAlert = ( + title: string, + message: string, + buttons: { text: string; onPress: () => void; style?: "cancel" | "destructive" | "default" }[], + alertType: 'info' | 'success' | 'error' = 'info' + ) => { + setAlertInfo({ visible: true, title, message, buttons, alertType }); + }; + + const hideAlert = () => { + setAlertInfo({ ...alertInfo, visible: false }); + }; - console.log("OTP sent response:", res.data); - - if (res.data.statusCode === 200) { - navigation.navigate("OtpVarify", { mobile }); - } else { - Alert.alert("Error", res.data.message || "Failed to send OTP"); + const handleContinue = async () => { + if (mobile.length === 10) { + setLoading(true); + try { + // FIX: Used the imported LOGIN_URL constant instead of a string literal + const res = await axios.get(LOGIN_URL, { params: { mobile } }); + console.log("Login response:", res.data); + if (res.data.statusCode === 200) { + navigation.navigate("OtpVarify", { mobile }); + } else { + showAlert( + "Login Failed", + res.data.message || "Could not send OTP.", + [{ text: "Try Again", onPress: hideAlert }], + 'error' + ); + } + } catch (error: any) { + console.error("Login error:", error); + showAlert( + "An Error Occurred", + "Failed to send OTP. Please check your connection and try again.", + [{ text: "OK", onPress: hideAlert }], + 'error' + ); + } finally { + setLoading(false); } - } catch (error: any) { - console.error("Login error:", error); - - if (axios.isAxiosError(error)) { - console.log("Error message:", error.message); - console.log("Error response:", error.response?.data); - console.log("Error status:", error.response?.status); - console.log("Error headers:", error.response?.headers); - } else { - console.log("Unexpected error:", error); - } - - Alert.alert("Error", "Failed to send OTP. Please try again."); - } finally { - setLoading(false); + } else { + showAlert( + "Invalid Number", + "Please enter a valid 10-digit mobile number.", + [{ text: "Got it", onPress: hideAlert }], + 'info' + ); } - } else { - Alert.alert("Please enter a valid 10-digit mobile number"); - } -}; + }; -return ( - - - - {/* Top content */} - - CircleCollect - Welcome Back 👋 - - Enter your mobile number to sign in and continue. - - - + - - - + + + + + - {/* Bottom Button */} - - + + + CircleCollect + Welcome! + + Glad to see you again. Let's get you signed in to your community. + + + - - By continuing, you agree to our{" "} - - Terms & Privacy - - - - -); + + + + + + + + + + + + {loading ? ( + + ) : ( + Continue with Mobile + )} + + + Your privacy is our priority. Your information is always secure. + + + + + + + + ); } const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: theme.background, padding: 20 }, - topSection: { flex: 1, justifyContent: "center" }, + gradientContainer: { + flex: 1, + }, + container: { + flex: 1, + padding: 24, + justifyContent: "space-between", + }, + shape: { + position: 'absolute', + borderRadius: 1000, + opacity: 0.6, + }, + shape1: { + width: 400, + height: 400, + backgroundColor: colors.pastelPink, + top: -150, + left: -100, + }, + shape2: { + width: 300, + height: 300, + backgroundColor: colors.pastelBlue, + bottom: -100, + right: -120, + }, + shape3: { + width: 200, + height: 200, + backgroundColor: colors.pastelGreen, + bottom: '40%', + left: -80, + opacity: 0.5 + }, + topSection: { + flex: 0.8, + justifyContent: "center", + alignItems: 'center' + }, + formSection: { + flex: 1, + justifyContent: 'flex-start' + }, brand: { - fontSize: 30, - fontWeight: "700", - color: theme.accent, - marginBottom: 24, + fontSize: 24, + fontWeight: "600", + color: colors.primary, + textAlign: "center", + letterSpacing: 1.5, + marginBottom: 16, }, title: { - fontSize: 26, - fontWeight: "800", - color: theme.text, - marginBottom: 6, + fontSize: 38, + fontWeight: "bold", + color: colors.textDark, + textAlign: "center", + marginBottom: 12, }, subtitle: { - fontSize: 15, - color: theme.subText, - marginBottom: 30, - lineHeight: 22, + fontSize: 16, + color: colors.textLight, + textAlign: "center", + lineHeight: 24, + maxWidth: 320, }, - inputWrapper: { gap: 8 }, - inputLabel: { - fontSize: 14, - fontWeight: "600", - color: theme.text, + inputContainer: { + flexDirection: "row", + alignItems: "center", + backgroundColor: colors.white, + borderRadius: 16, + borderWidth: 1, + borderColor: colors.inputBorder, + paddingHorizontal: 16, + marginTop: 32, + shadowColor: colors.black, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.05, + shadowRadius: 10, + elevation: 3, + }, + inputIcon: { + marginRight: 12, }, input: { - backgroundColor: theme.surface, - borderRadius: 14, - padding: 14, + flex: 1, + height: 58, fontSize: 16, - color: theme.text, - borderWidth: 1, - borderColor: theme.outline, + color: colors.textDark, }, bottomSection: { - marginBottom: 30, + paddingBottom: Platform.OS === "ios" ? 20 : 10, + paddingHorizontal: 10 }, button: { - backgroundColor: theme.primary, + backgroundColor: colors.primary, paddingVertical: 18, - borderRadius: 14, + borderRadius: 30, alignItems: "center", justifyContent: "center", + flexDirection: "row", + shadowColor: colors.primary, + shadowOffset: { width: 0, height: 8 }, + shadowOpacity: 0.3, + shadowRadius: 15, + elevation: 8, + }, + buttonText: { + color: colors.white, + fontWeight: "700", + fontSize: 16, }, - buttonText: { color: "#fff", fontWeight: "700", fontSize: 16 }, footerText: { textAlign: "center", - color: theme.subText, + color: colors.textLight, fontSize: 12, - marginTop: 14, + marginTop: 20, }, }); diff --git a/src/screens/OtpVarificationScreen.tsx b/src/screens/OtpVarificationScreen.tsx index 0bd381c..61fabff 100644 --- a/src/screens/OtpVarificationScreen.tsx +++ b/src/screens/OtpVarificationScreen.tsx @@ -13,41 +13,54 @@ import { TextInputKeyPressEventData, KeyboardAvoidingView, StatusBar, - Alert, - ToastAndroid, } from "react-native"; import { RootStackParamList } from "../navigation/AppNavigator"; import * as Keychain from "react-native-keychain"; -import { BASE_URL, LOGIN_URL, VARIFY_OTP_URL } from "../utils/ApiUrls"; +import { LOGIN_URL, VARIFY_OTP_URL } from "../utils/ApiUrls"; import colors from "../components/colors"; -import { fetchUserProfile } from "../utils/HelperFunction"; - -const theme = { - primary: "#111111", - accent: colors.primary, - background: "#F9F9F9", - text: "#1A1A1A", - subText: "#6B6B6B", - outline: "#E5E5E5", -}; +import LinearGradient from "react-native-linear-gradient"; +import CustomAlert from "../components/CostomAlart"; const CELL_COUNT = 6; type OtpVerifyRouteProp = RouteProp; +interface AlertButton { + text: string; + onPress: () => void; + style?: "cancel" | "destructive" | "default"; +} + export default function OtpVerificationScreen() { const [code, setCode] = useState(Array(CELL_COUNT).fill("")); const [timer, setTimer] = useState(30); const [isResending, setIsResending] = useState(false); + const [alertConfig, setAlertConfig] = useState<{ + visible: boolean; + title: string; + message: string; + buttons: AlertButton[]; + alertType: 'error' | 'success' | 'info'; + }>({ + visible: false, + title: "", + message: "", + buttons: [], + alertType: "info", + }); + const inputs = useRef>([]); const route = useRoute(); const { mobile } = route.params; const navigation = useNavigation(); - // animations + + // Refs for auto-closing logic + const successActionRef = useRef<(() => void) | null>(null); + const successTimerRef = useRef(null); + const fade = useRef(new Animated.Value(0)).current; const slide = useRef(new Animated.Value(24)).current; const ctaScale = useRef(new Animated.Value(1)).current; - useEffect(() => { Animated.parallel([ Animated.timing(fade, { toValue: 1, duration: 600, useNativeDriver: true }), @@ -55,88 +68,133 @@ export default function OtpVerificationScreen() { ]).start(); }, [fade, slide]); - // Countdown useEffect(() => { if (timer <= 0) return; - const interval = setInterval(() => { - setTimer((prev) => prev - 1); - }, 1000); + const interval = setInterval(() => setTimer((prev) => prev - 1), 1000); return () => clearInterval(interval); }, [timer]); + const hideAlert = () => { + if (successTimerRef.current) { + clearTimeout(successTimerRef.current); + successTimerRef.current = null; + } + setAlertConfig(prev => ({ ...prev, visible: false })); + }; + + const triggerAlert = ( + title: string, + message: string, + buttons: AlertButton[], + alertType: 'error' | 'success' | 'info' = 'info', + autoCloseAction?: () => void + ) => { + setAlertConfig({ visible: true, title, message, buttons, alertType }); + + if (alertType === 'success' && autoCloseAction) { + successActionRef.current = autoCloseAction; + successTimerRef.current = setTimeout(() => { + successActionRef.current?.(); + }, 2000); // Auto-close after 2 seconds + } + }; + const saveToken = async (token: string) => { try { - await Keychain.setGenericPassword("auth", token, { - accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED, - }); + await Keychain.setGenericPassword("auth", token, { accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED }); console.log("✅ Token saved securely"); } catch (error) { console.error("❌ Error saving token:", error); } }; - const onSubmit = async (otp: string, mobile: string) => { - try { - const payload = { mobile, otp }; - const res = await axios.post( - VARIFY_OTP_URL, - payload, - { - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - } - ); - - console.log("✅ OTP Verified:", res.data); - - if (res.data?.statusCode === 200) { - const { jwt, role } = res.data?.data || {}; - - saveToken(jwt); - - navigation.navigate("Home"); - - ToastAndroid.show("OTP Verified Successfully!", ToastAndroid.SHORT); + if (otp.length !== CELL_COUNT) { + triggerAlert( + 'Incomplete Code', + 'Please enter all 6 digits of the OTP.', + [{ text: 'OK', onPress: hideAlert }], + 'error' + ); + return; } - } catch (err: any) { - console.error("❌ OTP Verification Error:", err?.response?.data || err.message); - ToastAndroid.show( - err?.response?.data?.message || "Invalid OTP. Please try again.", - ToastAndroid.SHORT - ); - } -}; + try { + const res = await axios.post(VARIFY_OTP_URL, { mobile, otp }, { + headers: { "Content-Type": "application/json", Accept: "application/json" }, + }); + if (res.data?.statusCode === 200) { + const { jwt } = res.data?.data || {}; + await saveToken(jwt); + + let hasActionRun = false; + const successAction = () => { + if (hasActionRun) return; + hasActionRun = true; + hideAlert(); + navigation.navigate("Home"); + }; + + triggerAlert( + 'Success!', + 'OTP verified successfully. Redirecting...', + [{ text: 'OK', onPress: successAction }], + 'success', + successAction + ); + } + } catch (err: any) { + console.error("❌ OTP Verification Error:", err?.response?.data || err.message); + triggerAlert( + 'Verification Failed', + err?.response?.data?.message || "Invalid OTP. Please try again.", + [{ text: 'Try Again', onPress: hideAlert, style: 'cancel' }], + 'error' + ); + } + }; const resendOtp = async () => { try { setIsResending(true); - setTimer(30); // reset timer - - const res = await axios.get( - LOGIN_URL + mobile, - { headers: { "Content-Type": "application/json", Accept: "*/*" } } - ); - - console.log("📩 OTP Resent:", res); + setTimer(30); + await axios.get(`${LOGIN_URL}?mobile=${mobile}`, { + headers: { "Content-Type": "application/json", Accept: "*/*" }, + }); + setCode(Array(CELL_COUNT).fill("")); inputs.current[0]?.focus(); - ToastAndroid.show("OTP Sent", ToastAndroid.SHORT); + + let hasActionRun = false; + const successAction = () => { + if (hasActionRun) return; + hasActionRun = true; + hideAlert(); + }; + + triggerAlert( + 'OTP Sent', + 'A new code has been sent to your mobile number.', + [{ text: 'OK', onPress: successAction }], + 'success', + successAction + ); } catch (err: any) { console.error("❌ Resend OTP Error:", err?.response?.data || err.message); - ToastAndroid.show("Failed to resend OTP. Try again later.", ToastAndroid.SHORT); + triggerAlert( + 'Error', + 'Failed to resend OTP. Try again later.', + [{ text: 'OK', onPress: hideAlert, style: 'cancel' }], + 'error' + ); } finally { setIsResending(false); } }; const submitIfComplete = (arr: string[]) => { - const joined = arr.join(""); - if (joined.length === CELL_COUNT) { - onSubmit(joined, mobile); + if (arr.join("").length === CELL_COUNT) { + onSubmit(arr.join(""), mobile); } }; @@ -155,13 +213,14 @@ export default function OtpVerificationScreen() { updateIndex(0, digits); return; } - const next = Array(CELL_COUNT) - .fill("") - .map((_, i) => digits[i] ?? ""); + const next = Array(CELL_COUNT).fill("").map((_, i) => digits[i] ?? ""); setCode(next); const firstEmpty = next.findIndex((c) => !c); - if (firstEmpty >= 0) inputs.current[firstEmpty]?.focus(); - else inputs.current[CELL_COUNT - 1]?.blur(); + if (firstEmpty >= 0) { + inputs.current[firstEmpty]?.focus(); + } else { + inputs.current[CELL_COUNT - 1]?.blur(); + } submitIfComplete(next); }; @@ -172,107 +231,71 @@ export default function OtpVerificationScreen() { }; return ( - - + + + + + - {/* Top Section */} - - Verify Code 🔒 - - We’ve sent a 6-digit verification code to {mobile}. - + + Verify Your Code 🔒 + We’ve sent a 6-digit verification code to +91 {mobile}. - - {Array.from({ length: CELL_COUNT }).map((_, i) => ( - { - inputs.current[i] = r; - }} - style={[styles.cell, code[i] ? styles.cellFilled : null]} - keyboardType="number-pad" - maxLength={1} - value={code[i]} - onChangeText={(t) => (i === 0 ? handleFirstCellChange(t) : updateIndex(i, t))} - onKeyPress={(e) => handleKeyPress(i, e)} - placeholder="•" - placeholderTextColor="#BDBDBD" - textContentType="oneTimeCode" - /> - ))} - - + + {Array.from({ length: CELL_COUNT }).map((_, i) => ( + { inputs.current[i] = r; }} + style={[styles.cell, code[i] ? styles.cellFilled : null]} + keyboardType="number-pad" + maxLength={i === 0 ? CELL_COUNT : 1} + value={code[i]} + onChangeText={(t) => (i === 0 ? handleFirstCellChange(t) : updateIndex(i, t))} + onKeyPress={(e) => handleKeyPress(i, e)} + placeholder="•" + placeholderTextColor="#BDBDBD" + textContentType="oneTimeCode" + /> + ))} + + - {/* Bottom Section */} - - - Animated.spring(ctaScale, { toValue: 0.97, useNativeDriver: true }).start() - } - onPressOut={() => - Animated.spring(ctaScale, { toValue: 1, useNativeDriver: true }).start() - } - onPress={() => onSubmit(code.join(""), mobile)} - style={styles.button} - > - Continue - - - {timer > 0 ? ( - Resend code in 00:{timer.toString().padStart(2, "0")} - ) : ( - - - {isResending ? "Resending..." : "Resend Code"} - + + Animated.spring(ctaScale, { toValue: 0.97, useNativeDriver: true }).start()} + onPressOut={() => Animated.spring(ctaScale, { toValue: 1, useNativeDriver: true }).start()} + onPress={() => onSubmit(code.join(""), mobile)} + style={styles.button} + > + Verify & Continue - )} - - + + {timer > 0 ? ( + Resend code in 00:{timer.toString().padStart(2, "0")} + ) : ( + + + {isResending ? "Resending..." : "Resend Code"} + + + )} + + + ); } const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: theme.background, padding: 20 }, - topSection: { flex: 1, justifyContent: "center" }, - title: { fontSize: 26, fontWeight: "800", color: theme.text, marginBottom: 8 }, - subtitle: { - fontSize: 15, - color: theme.subText, - marginBottom: 24, - lineHeight: 22, - }, - otpRow: { flexDirection: "row", justifyContent: "space-between" }, - cell: { - width: 50, - height: 58, - borderRadius: 12, - borderWidth: 1, - borderColor: theme.outline, - backgroundColor: "#fff", - textAlign: "center", - fontSize: 20, - color: theme.text, - }, - cellFilled: { borderColor: "#D1D1D1" }, - bottomSection: { marginBottom: 30 }, - button: { - backgroundColor: colors.primary, - paddingVertical: 18, - borderRadius: 14, - alignItems: "center", - justifyContent: "center", - }, - buttonText: { color: "#000", fontWeight: "700", fontSize: 16 }, - footerText: { - textAlign: "center", - color: theme.subText, - fontSize: 13, - marginTop: 14, - }, + gradientContainer: { flex: 1 }, + container: { flex: 1, padding: 24, justifyContent: "space-between" }, + topSection: { flex: 1, justifyContent: "center", alignItems: 'center' }, + title: { fontSize: 28, fontWeight: "bold", color: colors.textDark, textAlign: 'center', marginBottom: 12 }, + subtitle: { fontSize: 16, color: colors.textLight, textAlign: 'center', marginBottom: 32, lineHeight: 24, maxWidth: 300 }, + otpRow: { flexDirection: "row", justifyContent: "space-between", width: '100%', paddingHorizontal: 10 }, + cell: { width: 48, height: 56, borderRadius: 12, borderWidth: 1.5, borderColor: colors.inputBorder, backgroundColor: colors.white, textAlign: "center", fontSize: 22, fontWeight: '700', color: colors.textDark, shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.08, shadowRadius: 8, elevation: 3 }, + cellFilled: { borderColor: colors.primary, backgroundColor: '#F7F2FF' }, + bottomSection: { paddingBottom: Platform.OS === "ios" ? 20 : 10 }, + button: { backgroundColor: colors.primary, paddingVertical: 18, borderRadius: 30, alignItems: "center", justifyContent: "center", shadowColor: colors.primary, shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.3, shadowRadius: 15, elevation: 8 }, + buttonText: { color: colors.white, fontWeight: "700", fontSize: 16 }, + footerText: { textAlign: "center", color: colors.textLight, fontSize: 14, marginTop: 20 }, }); diff --git a/src/screens/home/Home.tsx b/src/screens/home/Home.tsx index 25cef32..58ca3a6 100644 --- a/src/screens/home/Home.tsx +++ b/src/screens/home/Home.tsx @@ -9,23 +9,20 @@ import { Image, Alert, FlatList, - ListRenderItem, // Import ListRenderItem for better typing + ListRenderItem, } from "react-native"; import AppView from "../../components/AppView"; import colors from "../../components/colors"; import { useDispatch, useSelector } from "react-redux"; import { useNavigation } from "@react-navigation/native"; import { PROFILE_URL } from "../../utils/ApiUrls"; -import * as Keychain from "react-native-keychain"; import { setUserData } from "../../redux/userSlice"; import { TabNavigationProp } from "../../navigation/BottomTabs"; -import GroupsTabs from "./GroupsTabs"; import UserProfileStatus from "./UserProfileStatus"; -import UserHome from "./user-home/UserHome"; import AnimatedCard from "../../components/common/AnimatedCard"; import { IoniconsIconName } from "@react-native-vector-icons/ionicons"; +import { apiCall } from "../../utils/ApiHelper"; -// Enable layout animation for Android if (Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental) { UIManager.setLayoutAnimationEnabledExperimental(true); } @@ -37,212 +34,108 @@ type UserProfile = { lastName: string | null; imageBlob: string | null; role?: string; -} -// --- TYPE DEFINITIONS --- +}; + interface ActionItem { id: string; title: string; action: string; tab?: "My Groups" | "Joined Groups" | "Requests" | "Your Requests"; - iconName?: IoniconsIconName; backOne?: string; backTwo?: string; - subtitle?: string + subtitle?: string; } - - -// Organizer Actions +// Updated action items with a prettier pastel palette const organizerActions: ActionItem[] = [ - { - id: "1", - title: "My Groups", - subtitle: "Manage your own groups", - action: "NAVIGATE_MANAGEMENT", - tab: "My Groups", - iconName: "people-outline", - backOne: "#4facfe", - backTwo: "#00f2fe", - }, - { - id: "2", - title: "Joined Groups", - subtitle: "View groups you’ve joined", - action: "NAVIGATE_MANAGEMENT", - tab: "Joined Groups", - iconName: "person-outline", - backOne: "#43e97b", - backTwo: "#38f9d7", - }, - { - id: "3", - title: "Requests", - subtitle: "Check group requests", - action: "NAVIGATE_MANAGEMENT", - tab: "Requests", - iconName: "notifications-outline", - backOne: "#fa709a", - backTwo: "#fee140", - }, + { 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: "3", title: "Requests", subtitle: "Check new requests", action: "NAVIGATE_MANAGEMENT", tab: "Requests", iconName: "notifications-outline", backOne: '#DFF6DD', backTwo: '#88D18A' }, ]; -// User Actions const userActions: ActionItem[] = [ - { - id: "1", - title: "Joined Groups", - subtitle: "See your joined groups", - action: "NAVIGATE_MANAGEMENT", - tab: "Joined Groups", - iconName: "person-outline", - backOne: "#43e97b", - backTwo: "#38f9d7", - }, - { - id: "2", - title: "Your Requests", - subtitle: "Track your group requests", - action: "NAVIGATE_MANAGEMENT", - tab: "Your Requests", - iconName: "mail-outline", - backOne: "#ff9a9e", - backTwo: "#fad0c4", - }, - { - id: "3", - title: "Transaction", - subtitle: "View your transaction history", - action: "TRANSACTION", - iconName: "calculator-outline", - backOne: "#4facfe", - backTwo: "#00f2fe", - }, + { id: "1", title: "Joined Groups", subtitle: "See your groups", action: "NAVIGATE_MANAGEMENT", tab: "Joined Groups", iconName: "person-outline", backOne: '#D7E9F7', backTwo: '#82AAE3' }, + { id: "2", title: "Your Requests", subtitle: "Track your requests", action: "NAVIGATE_MANAGEMENT", tab: "Your Requests", iconName: "mail-open-outline", backOne: '#FFF4D2', backTwo: '#FFB344' }, + { id: "3", title: "Transactions", subtitle: "View your history", action: "TRANSACTION", iconName: "receipt-outline", backOne: '#decbfcff', backTwo: '#B799FF' }, ]; - const Home: React.FC = () => { const user = useSelector((state: any) => state.user); const navigation = useNavigation(); const dispatch = useDispatch(); - // --- FIXED: Conditional logic moved inside the component --- const actions = user.role === 'ORGANIZER' ? organizerActions : userActions; - const [apiUser, setApiUser] = useState(null) + const [apiUser, setApiUser] = useState(null); - const fetchUserProfile = async () => { - try { - const credentials = await Keychain.getGenericPassword(); - if (!credentials) { - return; - } - const token = credentials.password; - - console.log("token : ", token) - const response = await fetch( - PROFILE_URL, - { - method: "GET", - headers: { - Accept: "*/*", - Authorization: `Bearer ${token}`, - }, + useEffect(() => { + const fetchUserProfile = async () => { + try { + const result = await apiCall(PROFILE_URL, "GET") as any; + if (result.status === "success") { + setApiUser(result.data); + dispatch(setUserData({ ...result.data, profileStatus: true })); } - ); - - const result = await response.json(); - console.log("👤 Profile Response:", result); - if (result.status === "success") { - setApiUser(result.data); - dispatch( - setUserData({ - id: result.data.id, - userId: result.data.userId, - username: result.data.username, - firstName: result.data.firstName, - lastName: result.data.lastName, - gender: result.data.gender, - mobile: result.data.mobile, - email: result.data.email, - address: result.data.address, - bankAccountNumber: result.data.bankAccountNumber, - bankName: result.data.bankName, - branchCode: result.data.branchCode, - profilePicture: result.data.filePath, - filePath: result.data.filePath, - imageBlob: result.data.imageBlob, - role: result.data.role, - organizerId: result.data.organizerId, - organizerStatus: result.data.organizerStatus, - profileStatus: true, - }) - ); + } catch (error) { + console.error("❌ Error fetching profile:", error); } - } catch (error) { - console.error("❌ Error fetching profile:", error); - } - }; - - useEffect(() => { + }; + const unsubscribe = navigation.addListener('focus', fetchUserProfile); fetchUserProfile(); - }, []); + return unsubscribe; + }, [navigation, dispatch]); - - useEffect(() => { - fetchUserProfile(); - }, []); - - // --- REFACTORED: Simplified action handler --- const handleActionPress = (item: ActionItem) => { if (item.action === "NAVIGATE_MANAGEMENT" && item.tab) { navigation.navigate('Management', { actionTab: item.tab }); } else if (item.action === "TRANSACTION") { - navigation.navigate("TransactionList" as never) - } else if (item.action === "ACTION_4") { - Alert.alert("Action 4 pressed"); + navigation.navigate("TransactionList" as never); } else { Alert.alert("Unknown action", item.title); } }; - // Render function for each grid item const renderGridItem: ListRenderItem = ({ item }) => ( - - handleActionPress(item)} - /> + + handleActionPress(item)} + /> + ); + const getProfileImageUri = () => { + const blob = user.imageBlob || apiUser?.imageBlob; + return blob ? { uri: `data:image/jpeg;base64,${blob}` } : require('../../assets/images/placeholder.png'); + }; + return ( - Hello, {user.firstName || apiUser?.firstName} {user.lastName || apiUser?.lastName} + + Hello, + {user.firstName || apiUser?.firstName || 'User'} + navigation.navigate("Profile" as never)}> - + - {/* Status */} + - {/* {user?.role === "USER" || apiUser?.role === "USER" ? : - } */} + + Quick Actions + item.id} - // columnWrapperStyle={styles.row} - // contentContainerStyle={{ paddingHorizontal: 8 }} + numColumns={2} + contentContainerStyle={{ paddingHorizontal: 8 }} /> ); @@ -253,48 +146,41 @@ export default Home; const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#F4F6F8' + backgroundColor: colors.white, // Set background to white }, header: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", - marginBottom: 12, + paddingHorizontal: 20, + paddingTop: 24, + paddingBottom: 16, }, - headerTitle: { + greeting: { fontSize: 22, - fontWeight: "600", - color: colors.Home, + color: colors.textSecondary, }, - row: { + userName: { + fontSize: 28, + fontWeight: 'bold', + color: colors.textDark, + }, + profileImage: { + width: 50, + height: 50, + borderRadius: 25, + borderColor: colors.pastelPink, // Using pastelPink for border + borderWidth: 2, + }, + sectionTitle: { + fontSize: 20, + fontWeight: '600', + color: colors.textDark, + paddingHorizontal: 20, + marginBottom: 8, + }, + cardWrapper: { flex: 1, - justifyContent: "space-around", - }, - greeting: { fontSize: 22, fontWeight: "600", color: colors.pastelPink }, - gridButton: { - flex: 1, - margin: 8, - backgroundColor: "#FFFFFF", - borderRadius: 12, - padding: 16, - alignItems: "center", - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 5, - elevation: 4, - }, - gridIcon: { - width: 60, - height: 60, - marginBottom: 12, - borderRadius: 8, - backgroundColor: '#E0E0E0', - }, - gridLabel: { - fontSize: 16, - fontWeight: "500", - color: colors.Home, - textAlign: "center", + marginHorizontal: 8, }, }); diff --git a/src/screens/management/EditGroupScreen.tsx b/src/screens/management/EditGroupScreen.tsx index cf0e5f4..b2894bf 100644 --- a/src/screens/management/EditGroupScreen.tsx +++ b/src/screens/management/EditGroupScreen.tsx @@ -154,10 +154,10 @@ const EditGroupScreen: React.FC = ({ route }) => { } }, [groupId]); 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 ,status: boolean }; message?: string }; + 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 }; console.log("paymentStatus",response) if (response.status === 'success') { - setPaymentStatus(response.data?.status || false); + setPaymentStatus(response.data?.allPaid || false); setCurrentCycle(response.data?.currentCycle || null); } } diff --git a/src/screens/profile/Profile.tsx b/src/screens/profile/Profile.tsx index 0400b69..c0d23e2 100644 --- a/src/screens/profile/Profile.tsx +++ b/src/screens/profile/Profile.tsx @@ -660,7 +660,7 @@ const Profile = () => { onPress={() => setEditing(false)} icon="close" style={styles.cancelButton} - buttonBackground="" + buttonBackground={colors.white } /> @@ -756,12 +756,14 @@ const styles = StyleSheet.create({ width: "80%", paddingHorizontal: 10, borderRadius: 25, + elevation: 5, + }, detailsSection: { marginBottom: "40%", }, sectionTitle: { - fontSize: 18, + fontSize: 18, fontWeight: "700", color: "#1e293b", // marginBottom: 16, @@ -777,6 +779,7 @@ const styles = StyleSheet.create({ }, detailIcon: { marginRight: 12, + elevation:5 }, detailLabel: { fontSize: 12, @@ -789,7 +792,9 @@ const styles = StyleSheet.create({ fontWeight: "500", }, editButton: { - paddingHorizontal: 8, + elevation:5, + paddingHorizontal: 10, + borderRadius: 15, }, editSection: { marginBottom: '100%',