diff --git a/App.tsx b/App.tsx index 59b3ead..b57233d 100644 --- a/App.tsx +++ b/App.tsx @@ -12,13 +12,16 @@ import { useSafeAreaInsets, } from 'react-native-safe-area-context'; import AppNavigator from './src/navigation/AppNavigator'; +import { PaperProvider } from 'react-native-paper'; function App() { const isDarkMode = useColorScheme() === 'dark'; - + return ( - + + + ); } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e189252..f8fef7e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,26 +1,35 @@ - + - + - - - - - - - - + android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" + android:launchMode="singleTask" + android:windowSoftInputMode="adjustResize" + android:exported="true"> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a1147bc..d4efc6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "0.0.1", "dependencies": { "@react-native-async-storage/async-storage": "^2.2.0", + "@react-native-picker/picker": "^2.11.2", "@react-native-vector-icons/ionicons": "^12.3.0", + "@react-native-vector-icons/material-icons": "^12.3.0", "@react-native/new-app-screen": "0.81.1", "@react-navigation/bottom-tabs": "^7.4.7", "@react-navigation/elements": "^2.6.4", @@ -24,8 +26,11 @@ "react-native-elements": "^3.4.3", "react-native-image-picker": "^8.2.1", "react-native-keychain": "^10.0.0", + "react-native-linear-gradient": "^2.8.3", + "react-native-paper": "^5.14.5", "react-native-safe-area-context": "^5.6.1", - "react-native-screens": "^4.16.0" + "react-native-screens": "^4.16.0", + "react-native-walkthrough-tooltip": "^1.6.0" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -1997,6 +2002,28 @@ "dev": true, "license": "MIT" }, + "node_modules/@callstack/react-theme-provider": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@callstack/react-theme-provider/-/react-theme-provider-3.0.9.tgz", + "integrity": "sha512-tTQ0uDSCL0ypeMa8T/E9wAZRGKWj8kXP7+6RYgPTfOPs9N07C9xM8P02GJ3feETap4Ux5S69D9nteq9mEj86NA==", + "license": "MIT", + "dependencies": { + "deepmerge": "^3.2.0", + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, + "node_modules/@callstack/react-theme-provider/node_modules/deepmerge": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.3.0.tgz", + "integrity": "sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@egjs/hammerjs": { "version": "2.0.17", "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", @@ -2953,6 +2980,19 @@ "node": ">=10" } }, + "node_modules/@react-native-picker/picker": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.2.tgz", + "integrity": "sha512-2zyFdW4jgHjF+NeuDZ4nl3hJ+8suey69bI3yljqhNyowfklW2NwNrdDUaJ2iwtPCpk2pt7834aPF8TI6iyZRhA==", + "license": "MIT", + "workspaces": [ + "example" + ], + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@react-native-vector-icons/common": { "version": "12.3.0", "resolved": "https://registry.npmjs.org/@react-native-vector-icons/common/-/common-12.3.0.tgz", @@ -3079,6 +3119,22 @@ "react-native": "*" } }, + "node_modules/@react-native-vector-icons/material-icons": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/@react-native-vector-icons/material-icons/-/material-icons-12.3.0.tgz", + "integrity": "sha512-W9rhgfc8zKKXhZa/099un/RSNFgO6aHhJR2r6rodvTlO+ZRXq2GbU38CMBwhomJxyErcfmlR7En6wSdtLykYVQ==", + "license": "MIT", + "dependencies": { + "@react-native-vector-icons/common": "^12.3.0" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.81.1", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.1.tgz", @@ -13059,6 +13115,12 @@ } } }, + "node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==", + "license": "MIT" + }, "node_modules/react-freeze": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz", @@ -13204,6 +13266,36 @@ "node": ">=16" } }, + "node_modules/react-native-linear-gradient": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.3.tgz", + "integrity": "sha512-KflAXZcEg54PXkLyflaSZQ3PJp4uC4whM7nT/Uot9m0e/qxFV3p6uor1983D1YOBJbJN7rrWdqIjq0T42jOJyA==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-paper": { + "version": "5.14.5", + "resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.14.5.tgz", + "integrity": "sha512-eaIH5bUQjJ/mYm4AkI6caaiyc7BcHDwX6CqNDi6RIxfxfWxROsHpll1oBuwn/cFvknvA8uEAkqLk/vzVihI3AQ==", + "license": "MIT", + "workspaces": [ + "example", + "docs" + ], + "dependencies": { + "@callstack/react-theme-provider": "^3.0.9", + "color": "^3.1.2", + "use-latest-callback": "^0.2.3" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-safe-area-context": "*" + } + }, "node_modules/react-native-ratings": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/react-native-ratings/-/react-native-ratings-8.0.4.tgz", @@ -13310,6 +13402,19 @@ "node": ">=10" } }, + "node_modules/react-native-walkthrough-tooltip": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/react-native-walkthrough-tooltip/-/react-native-walkthrough-tooltip-1.6.0.tgz", + "integrity": "sha512-8DHpcqsF+2VULYnKJCSZtllhlFUFiZcWCwOlw5RLkS451ElxPWJn9ShOFVZtkPMcwQAirq3SovumVM7Y/li0Fw==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.6.1", + "react-fast-compare": "^2.0.4" + }, + "peerDependencies": { + "@types/react": ">=16.8.24" + } + }, "node_modules/react-native/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", diff --git a/package.json b/package.json index 6b04bd6..e30f84f 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ }, "dependencies": { "@react-native-async-storage/async-storage": "^2.2.0", + "@react-native-picker/picker": "^2.11.2", "@react-native-vector-icons/ionicons": "^12.3.0", + "@react-native-vector-icons/material-icons": "^12.3.0", "@react-native/new-app-screen": "0.81.1", "@react-navigation/bottom-tabs": "^7.4.7", "@react-navigation/elements": "^2.6.4", @@ -26,8 +28,11 @@ "react-native-elements": "^3.4.3", "react-native-image-picker": "^8.2.1", "react-native-keychain": "^10.0.0", + "react-native-linear-gradient": "^2.8.3", + "react-native-paper": "^5.14.5", "react-native-safe-area-context": "^5.6.1", - "react-native-screens": "^4.16.0" + "react-native-screens": "^4.16.0", + "react-native-walkthrough-tooltip": "^1.6.0" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/src/components/AppView.tsx b/src/components/AppView.tsx new file mode 100644 index 0000000..a304a69 --- /dev/null +++ b/src/components/AppView.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { StatusBar, StyleSheet, View } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; + +type AppViewProps = { + children: React.ReactNode; + backgroundColor?: string; + barStyle?: "default" | "light-content" | "dark-content"; +}; + +export default function AppView({ + children, + backgroundColor = "#f5f5f5", + barStyle = "dark-content", +}: AppViewProps) { + return ( + + + {children} + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + }, + container: { + flex: 1, + paddingHorizontal: 16, + paddingTop: 8, + }, +}); diff --git a/src/components/CustomButton.tsx b/src/components/CustomButton.tsx new file mode 100644 index 0000000..185fb67 --- /dev/null +++ b/src/components/CustomButton.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { TouchableOpacity, Text, StyleSheet, ActivityIndicator, ViewStyle } from "react-native"; + +type CustomButtonProps = { + title: string; + onPress: () => void; + backgroundColor?: string; + textColor?: string; + loading?: boolean; + style?: ViewStyle; +}; + +export default function CustomButton({ + title, + onPress, + backgroundColor = "#6C63FF", + textColor = "#fff", + loading = false, + style, +}: CustomButtonProps) { + return ( + + {loading ? ( + + ) : ( + {title} + )} + + ); +} + +const styles = StyleSheet.create({ + button: { + paddingVertical: 16, + paddingHorizontal: 20, + borderRadius: 12, + alignItems: "center", + justifyContent: "center", + marginVertical: 5, + }, + text: { + fontSize: 16, + fontWeight: "bold", + }, +}); diff --git a/src/common/Custom.tsx b/src/components/common/Custom.tsx similarity index 66% rename from src/common/Custom.tsx rename to src/components/common/Custom.tsx index 8bf1c9b..e5ae7e9 100644 --- a/src/common/Custom.tsx +++ b/src/components/common/Custom.tsx @@ -6,12 +6,15 @@ import { TextInput, TouchableOpacity, } from "react-native"; +import { TextInputProps } from "react-native"; +import {Ionicons} from "@react-native-vector-icons/ionicons"; -type CustomInputProps = { +type CustomInputProps = TextInputProps & { label: string; value: string; placeholder: string; onChangeText: (text: string) => void; + icon?: string; }; const CustomInput: React.FC = ({ @@ -19,29 +22,38 @@ const CustomInput: React.FC = ({ value, placeholder, onChangeText, + icon, + ...rest }) => ( {label} - + + {icon && } + + ); type CustomButtonProps = { title: string; onPress: () => void; - type?: "primary" | "secondary" | "danger"; + type?: "primary" | "secondary" | "danger" | "outline"; + icon?: string | null; + style?: object; }; const CustomButton: React.FC = ({ title, onPress, type = "primary", + icon, }) => ( = ({ onPress={onPress} activeOpacity={0.8} > - - {title} - + + {icon && } + + {title} + + ); @@ -110,11 +125,23 @@ const styles = StyleSheet.create({ }, inputContainer: { marginBottom: 12 }, inputLabel: { fontSize: 14, fontWeight: "500", color: "#555", marginBottom: 6 }, + inputWrapper: { + flexDirection: "row", + alignItems: "center", + position: "relative", + }, + inputIcon: { + position: "absolute", + left: 10, + zIndex: 1, + }, input: { + flex: 1, borderWidth: 1, borderColor: "#ddd", borderRadius: 10, padding: 12, + paddingLeft: 12, backgroundColor: "#fdfdfd", color: "#000", fontSize: 15, @@ -136,9 +163,13 @@ const styles = StyleSheet.create({ buttonDanger: { backgroundColor: "#ff3b30", }, + buttonContent: { + flexDirection: "row", + alignItems: "center", + }, buttonText: { color: "#fff", fontSize: 16, fontWeight: "600" }, buttonTextSecondary: { color: "#333" }, buttonTextDanger: { color: "#fff" }, }); -export { CustomInput, CustomButton, styles }; +export { CustomInput, styles, CustomButton }; diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index edda1a7..a762f87 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -4,9 +4,12 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { View, Text, Button } from "react-native"; import LoginScreen from "../screens/LoginScreen"; import OtpVerificationScreen from "../screens/OtpVarificationScreen"; -import BottomTabs from "./BottomTabs"; import SplashScreen from "../screens/SplashScreen"; import Profile from "../screens/profile/Profile"; +import OrganizerRegisterScreen from "../screens/OrgnizerRagister"; +import GroupCreation from "../screens/GroupCreation"; +import GroupReview from "../screens/GroupReview"; +import BottomTabs from "./BottomTabs"; const Stack = createNativeStackNavigator(); export type RootStackParamList = { @@ -15,6 +18,10 @@ export type RootStackParamList = { Home: undefined HomeTabs: undefined splash: undefined + OrganizerRegister:undefined + Profile: undefined; + GroupCreation: undefined; + GroupReview: undefined; }; @@ -32,14 +39,25 @@ function HomeScreen() { export default function AppNavigator() { + const linking = { + prefixes: ["myapp://", "https://api.godemo.in"], // supports both + config: { + screens: { + Profile: "EProcurementSB", // dynamic route + }, + }, +}; return ( - + - + + + + ); diff --git a/src/navigation/BottomTabs.tsx b/src/navigation/BottomTabs.tsx index 16dee78..c828df2 100644 --- a/src/navigation/BottomTabs.tsx +++ b/src/navigation/BottomTabs.tsx @@ -1,24 +1,35 @@ import React from "react"; import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; -import { View, Text } from "react-native"; -import {Ionicons} from '@react-native-vector-icons/ionicons'; +import { View, Text, StyleSheet, Platform } from "react-native"; +import { Ionicons } from '@react-native-vector-icons/ionicons'; +import GroupScreen from "../screens/GroupScreen"; import Profile from "../screens/profile/Profile"; const Tab = createBottomTabNavigator(); -// Dummy screens for now +const TAB_CONFIG = { + activeColor: "#eb87a3ff", // coffee-like orange + inactiveColor: "#000000ff", + backgroundColor: "#e3e9efff", + height: 80, + iconSize: 30, + labelSize: 12, + labelWeight: "600", +}; + +// Dummy Screens function HomeScreen() { return ( - - 🏠 Home + + 🏠 Home ); } function SettingsScreen() { return ( - - ⚙️ Settings + + ⚙️ Settings ); } @@ -28,24 +39,72 @@ export default function BottomTabs() { ({ headerShown: false, - tabBarShowLabel: true, - tabBarStyle: { backgroundColor: "#111", paddingBottom: 5, height: 60 }, - tabBarActiveTintColor: "#FF7A00", - tabBarInactiveTintColor: "gray", - tabBarIcon: ({ color, size }) => { - let iconName: React.ComponentProps['name'] = "home-outline"; + tabBarStyle: styles.tabBar, + tabBarActiveTintColor: TAB_CONFIG.activeColor, + tabBarInactiveTintColor: TAB_CONFIG.inactiveColor, + tabBarLabelStyle: { + fontSize: TAB_CONFIG.labelSize, + fontWeight: TAB_CONFIG.labelWeight as any, + marginBottom: Platform.OS === "ios" ? 0 : 5, + }, + tabBarIcon: ({ color, focused }) => { + let iconName: + | "home" + | "home-outline" + | "person" + | "person-outline" + | "settings" + | "settings-outline" + | "people" + | "people-outline" = "home-outline"; - if (route.name === "Home") iconName = "home-outline"; - else if (route.name === "Profile") iconName = "person-outline"; - else if (route.name === "Settings") iconName = "settings-outline"; + if (route.name === "Home") iconName = focused ? "home" : "home-outline"; + else if (route.name === "Profile") + iconName = focused ? "person" : "person-outline"; + else if (route.name === "Settings") + iconName = focused ? "settings" : "settings-outline"; + else if (route.name === "Group") + iconName = focused ? "people" : "people-outline"; - return ; + return ; }, })} > + ); } + +const styles = StyleSheet.create({ + screen: { + flex: 1, + justifyContent: "center", + alignItems: "center", + backgroundColor: "#dae4ed", + }, + text: { + fontSize: 20, + fontWeight: "600", + color: "#111", + }, + tabBar: { + position: "absolute", + bottom: 0, + left: 0, + right: 0, + height: TAB_CONFIG.height, + backgroundColor: TAB_CONFIG.backgroundColor, + borderTopLeftRadius: 30, + borderTopRightRadius: 30, + shadowColor: "#000", + shadowOpacity: 0.10, + shadowOffset: { width: 0, height: -2 }, + shadowRadius: 8, + elevation: 20, + paddingTop:10, + borderTopWidth: 0, + }, +}); diff --git a/src/screens/GroupCreation.tsx b/src/screens/GroupCreation.tsx new file mode 100644 index 0000000..30b79cd --- /dev/null +++ b/src/screens/GroupCreation.tsx @@ -0,0 +1,204 @@ +import { Picker } from "@react-native-picker/picker"; +import React, { useState } from "react"; +import { + View, + Text, + TextInput, + TouchableOpacity, + StyleSheet, + ScrollView, + KeyboardAvoidingView, + Platform, +} from "react-native"; +import { useNavigation } from "@react-navigation/native"; +import CustomButton from "../components/CustomButton"; + +export default function GroupCreationForm() { + const navigation = useNavigation(); + + const [form, setForm] = useState({ + contributionAmount: "", + customCycleDays: "", + cycleType: "CUSTOM", + description: "", + endDate: "", + groupName: "", + maxParticipants: "", + minParticipants: "", + startDate: "", + visibility: "PRIVATE", + }); + + const handleChange = (key: string, value: string) => { + setForm({ ...form, [key]: value }); + }; + + return ( + + + Group Creation + + {/* Group Info */} + Group Name + handleChange("groupName", val)} + /> + + Description + handleChange("description", val)} + /> + + Visibility + + handleChange("visibility", val)} + style={styles.picker} + > + + + + + + {/* Participants */} + Minimum Participants + handleChange("minParticipants", val)} + /> + + Maximum Participants + handleChange("maxParticipants", val)} + /> + + {/* Contribution */} + Contribution Amount (₹) + handleChange("contributionAmount", val)} + /> + + Cycle Type + + handleChange("cycleType", val)} + style={styles.picker} + > + + + + + + + + + + {form.cycleType === "CUSTOM" && ( + <> + Custom Cycle Days + handleChange("customCycleDays", val)} + /> + + )} + + {/* Timeline */} + Start Date + handleChange("startDate", val)} + /> + + End Date + handleChange("endDate", val)} + /> + + {/* Next Button + navigation.navigate("GroupReview", { form })} + style={styles.nextBtn} + > + ➡ Review + */} + navigation.navigate("GroupReview", { form })} + /> + + + ); +} + +const styles = StyleSheet.create({ + container: { flexGrow: 1, padding: 20 }, + title: { fontSize: 22, fontWeight: "bold", marginBottom: 15, color: "#333" }, + label: { fontSize: 14, fontWeight: "600", marginBottom: 5, color: "#444" }, + input: { + borderWidth: 1, + borderColor: "#ddd", + borderRadius: 12, + padding: 12, + marginBottom: 15, + fontSize: 16, + backgroundColor: "#fff", + elevation: 1, + }, + pickerWrapper: { + borderWidth: 1, + borderColor: "#ddd", + borderRadius: 12, + marginBottom: 15, + backgroundColor: "#fff", + elevation: 1, + }, + picker: { height: 60, width: "100%" }, + nextBtn: { + backgroundColor: "#eb87a3ff", + paddingVertical: 16, + borderRadius: 12, + alignItems: "center", + marginTop: 10, + }, + btnText: { color: "#fff", fontWeight: "bold", fontSize: 16 }, +}); diff --git a/src/screens/GroupReview.tsx b/src/screens/GroupReview.tsx new file mode 100644 index 0000000..40f3673 --- /dev/null +++ b/src/screens/GroupReview.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import { View, Text, StyleSheet, TouchableOpacity, Alert, ScrollView } from "react-native"; +import axios from "axios"; +import { GROUP_CREATION_URL } from "../utils/ApiUrls"; +import App from "../../App"; +import AppView from "../components/AppView"; +import { apiCall } from "../utils/ApiHelper"; + +export default function GroupReview({ route, navigation }: any) { + const { form } = route.params; + + const handleSubmit = async () => { + const payload = { + ...form, + contributionAmount: Number(form.contributionAmount), + customCycleDays: Number(form.customCycleDays), + maxParticipants: Number(form.maxParticipants), + minParticipants: Number(form.minParticipants), + }; + await apiCall(GROUP_CREATION_URL, "POST", payload); + }; + + const renderRow = (label: string, value: string | number) => ( + + {label} + {value} + + ); + + return ( + + + Review Your Group + + {renderRow("Group Name", form.groupName)} + {renderRow("Description", form.description)} + {renderRow("Min Participants", form.minParticipants)} + {renderRow("Max Participants", form.maxParticipants)} + {renderRow("Contribution", `₹${form.contributionAmount}`)} + {renderRow("Cycle Type", form.cycleType)} + {form.cycleType === "CUSTOM" && renderRow("Custom Cycle", `${form.customCycleDays} days`)} + {renderRow("Start Date", form.startDate)} + {renderRow("End Date", form.endDate)} + {renderRow("Visibility", form.visibility)} + + + + Submit + + + + ); +} + +const styles = StyleSheet.create({ + container: { flexGrow: 1, padding: 20, backgroundColor: "#f5f5f5" }, + title: { + fontSize: 24, + fontWeight: "bold", + marginBottom: 20, + color: "#222", + textAlign: "center", + }, + reviewBox: { + padding: 18, + borderWidth: 1, + borderColor: "#ddd", + borderRadius: 16, + backgroundColor: "#fff", + marginBottom: 25, + elevation: 3, + shadowColor: "#000", + shadowOpacity: 0.1, + shadowOffset: { width: 0, height: 2 }, + shadowRadius: 5, + }, + row: { + flexDirection: "row", + justifyContent: "space-between", + paddingVertical: 10, + borderBottomWidth: 1, + borderBottomColor: "#eee", + }, + label: { fontSize: 16, fontWeight: "600", color: "#444" }, + value: { fontSize: 16, fontWeight: "500", color: "#111" }, + submitBtn: { + backgroundColor: "#008e02", + paddingVertical: 18, + borderRadius: 14, + alignItems: "center", + elevation: 2, + }, + btnText: { color: "#fff", fontWeight: "bold", fontSize: 18 }, +}); diff --git a/src/screens/GroupScreen.tsx b/src/screens/GroupScreen.tsx new file mode 100644 index 0000000..d9b370e --- /dev/null +++ b/src/screens/GroupScreen.tsx @@ -0,0 +1,61 @@ +import React, { useState } from "react"; +import { View, Text, StyleSheet } from "react-native"; +import { FAB, Portal, Provider } from "react-native-paper"; +import Tooltip from "react-native-walkthrough-tooltip"; +import { MaterialIcons } from '@react-native-vector-icons/material-icons'; +import { useNavigation } from "@react-navigation/native"; +import { StackNavigationProp } from "@react-navigation/stack"; +import { RootStackParamList } from "../navigation/AppNavigator"; + +export default function GroupScreen() { + const [tooltipVisible, setTooltipVisible] = useState(false); + const navigation = useNavigation>(); + return ( + + + Groups + + + } + onPress={() => { + navigation.navigate('GroupCreation') + }} + /> + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#dae4ed", + padding: 16, + paddingTop: 40, + }, + title: { + fontSize: 22, + fontWeight: "bold", + color: "#333", + }, + fab: { + position: "absolute", + right: 20, + bottom: 90, + width: 70, + height: 70, + justifyContent: "center", + alignItems: "center", + backgroundColor: "#dae4ed", + borderRadius: 60, + }, + tooltipText: { + color: "#fff", + fontSize: 14, + fontWeight: "500", + }, +}); diff --git a/src/screens/OrgnizerRagister.tsx b/src/screens/OrgnizerRagister.tsx new file mode 100644 index 0000000..b9bf3c5 --- /dev/null +++ b/src/screens/OrgnizerRagister.tsx @@ -0,0 +1,149 @@ +import React, { useState } from "react"; +import { + View, + Text, + TextInput, + Pressable, + StyleSheet, + Alert, + KeyboardAvoidingView, + Platform, + ScrollView, +} from "react-native"; +import axios from "axios"; +import { ORGRAGISTRATION } from "../utils/ApiUrls"; +import * as Keychain from "react-native-keychain"; + +const theme = { + primary: "#111", + accent: "#FF7A00", + background: "#F9F9F9", + text: "#1A1A1A", + subText: "#6B6B6B", + outline: "#E5E5E5", +}; + +export default function OrganizerRegisterScreen() { + const [bankAccountNumber, setBankAccountNumber] = useState(""); + const [bankName, setBankName] = useState(""); + const [branchCode, setBranchCode] = useState(""); + const [loading, setLoading] = useState(false); +const handleSubmit = async () => { + if (!bankAccountNumber || !bankName || !branchCode) { + Alert.alert("Error", "All fields are required."); + return; + } + + try { + setLoading(true); + + // 🔑 get token from Keychain + const creds = await Keychain.getGenericPassword(); + const token = creds ? creds.password : null; + + if (!token) { + Alert.alert("Error", "No token found. Please log in again."); + setLoading(false); + return; + } + + const payload = { + bankAccountNumber, + bankName, + branchCode, + }; + + const res = await axios.post( + ORGRAGISTRATION, // your API endpoint + payload, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, // 👈 token added here + }, + } + ); + + console.log("✅ Organizer Registered:", res.data); + Alert.alert("Success", "Organizer registered successfully!"); + } catch (err: any) { + console.error("❌ Register Error:", err?.response?.data || err.message); + Alert.alert("Error", "Failed to register. Please try again."); + } finally { + setLoading(false); + } +}; + + + return ( + + + Organizer Registration 📝 + + + + + + + + + {loading ? "Submitting..." : "Submit"} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flexGrow: 1, + justifyContent: "center", + padding: 20, + }, + title: { + fontSize: 22, + fontWeight: "700", + color: theme.text, + marginBottom: 20, + textAlign: "center", + }, + input: { + borderWidth: 1, + borderColor: theme.outline, + borderRadius: 12, + padding: 14, + marginBottom: 15, + fontSize: 16, + backgroundColor: "#fff", + }, + button: { + backgroundColor: theme.primary, + paddingVertical: 16, + borderRadius: 12, + alignItems: "center", + }, + buttonText: { + color: "#fff", + fontWeight: "600", + fontSize: 16, + }, +}); diff --git a/src/screens/SplashScreen.tsx b/src/screens/SplashScreen.tsx index 9580b97..541c028 100644 --- a/src/screens/SplashScreen.tsx +++ b/src/screens/SplashScreen.tsx @@ -1,12 +1,13 @@ -import React, { useEffect } from "react"; -import { View, Text, StyleSheet, ActivityIndicator } from "react-native"; +import React, { useEffect, useRef } from "react"; +import { View, Text, StyleSheet, ActivityIndicator, Animated } from "react-native"; import * as Keychain from "react-native-keychain"; import { StackNavigationProp } from "@react-navigation/stack"; import { useNavigation } from "@react-navigation/native"; import { RootStackParamList } from "../navigation/AppNavigator"; // adjust path const SplashScreen = () => { - const navigation = useNavigation>(); + const navigation = useNavigation>(); + const fadeAnim = useRef(new Animated.Value(1)).current; // opacity starts at 1 useEffect(() => { const checkLogin = async () => { @@ -16,7 +17,7 @@ const SplashScreen = () => { console.log("🔑 Found token:", creds.password); navigation.replace("Home"); // go to bottom tabs if logged in } else { - navigation.replace("Login"); // go to login if not logged in + navigation.replace("Login"); } } catch (error) { console.error("Error checking login:", error); @@ -24,12 +25,22 @@ const SplashScreen = () => { } }; - setTimeout(checkLogin, 1500); // show splash for 1.5s - }, [navigation]); + // Start fade out animation + Animated.timing(fadeAnim, { + toValue: 0, + duration: 1500, // fade out in 1.5s + useNativeDriver: true, + delay: 1500, // wait before fading + }).start(() => { + checkLogin(); // only navigate after animation + }); + }, [navigation, fadeAnim]); return ( - Circle Collect + + Circle Collect + ); @@ -42,7 +53,7 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: "center", alignItems: "center", - backgroundColor: "#111111", + backgroundColor: "#FFFFFF", // white background }, title: { fontSize: 28, diff --git a/src/screens/profile/Profile.tsx b/src/screens/profile/Profile.tsx index f04da5b..1a30dec 100644 --- a/src/screens/profile/Profile.tsx +++ b/src/screens/profile/Profile.tsx @@ -10,113 +10,16 @@ import { ActivityIndicator, StatusBar, TextInput, + KeyboardAvoidingView, + Platform, } from "react-native"; import * as Keychain from "react-native-keychain"; -import {Ionicons} from "@react-native-vector-icons/ionicons"; +import { Ionicons } from "@react-native-vector-icons/ionicons"; import { launchImageLibrary } from "react-native-image-picker"; -import { IconProps } from "react-native-elements"; import { base_URL } from "../../utils/ApiUrls"; +import { CustomButton, CustomInput } from "../../components/common/Custom"; -type CustomButtonProps = { - title: string; - onPress: () => void; - type?: "primary" | "secondary" | "danger" | "outline"; - icon?: React.ComponentProps['name']; - style?: any; -}; - -const CustomButton = ({ - title, - onPress, - type = "primary", - icon, - style -}: CustomButtonProps) => { - const getButtonStyle = () => { - switch (type) { - case "secondary": - return [styles.button, styles.buttonSecondary]; - case "danger": - return [styles.button, styles.buttonDanger]; - case "outline": - return [styles.button, styles.buttonOutline]; - default: - return styles.button; - } - }; - - const getTextStyle = () => { - switch (type) { - case "secondary": - return [styles.buttonText, styles.buttonTextSecondary]; - case "danger": - return [styles.buttonText, styles.buttonTextDanger]; - case "outline": - return [styles.buttonText, styles.buttonTextOutline]; - default: - return styles.buttonText; - } - }; - - return ( - - {icon && ( - s && typeof s === "object" && "color" in s)?.color || "#fff" - : (getTextStyle() as any).color || "#fff" - } - style={styles.buttonIcon} - /> - )} - {title} - - ); -}; - -// Custom Input Component -type CustomInputProps = { - label?: string; - placeholder?: string; - value?: string; - onChangeText?: (text: string) => void; - icon?: React.ComponentProps['name']; - [key: string]: any; -}; - -const CustomInput = ({ - label, - placeholder, - value, - onChangeText, - icon, - ...props -}: CustomInputProps) => { - return ( - - {label && {label}} - - {icon && } - - - - ); -}; type UserProfile = { id: string; @@ -236,64 +139,64 @@ const Profile = () => { }; const handleUpdateImage = async () => { - try { - const credentials = await Keychain.getGenericPassword(); - if (!credentials) { - Alert.alert("Error", "You are not logged in"); - return; - } - const token = credentials.password; - - if (!user?.filePath) { - Alert.alert("Error", "Please select an image first"); - return; - } - - setImageUpdating(true); - - const formData = new FormData(); - formData.append("file", { - uri: user.filePath, - type: "image/jpeg", - name: "profile.jpg", - } as any); - - const response = await fetch( - `${base_URL}user/update/userImg`, - { - method: "PUT", - headers: { - Accept: "application/json", - Authorization: `Bearer ${token}`, - }, - body: formData, + try { + const credentials = await Keychain.getGenericPassword(); + if (!credentials) { + Alert.alert("Error", "You are not logged in"); + return; } - ); + const token = credentials.password; - const result = await response.json(); - if (result.status === "success") { - Alert.alert("Success", "Profile image updated successfully"); + if (!user?.filePath) { + Alert.alert("Error", "Please select an image first"); + return; + } - // If API returns updated image, set it - if (result.data?.imageBlob) { - setUser((prev) => prev ? { ...prev, imageBlob: result.data.imageBlob, filePath: null } : prev); + setImageUpdating(true); + + const formData = new FormData(); + formData.append("file", { + uri: user.filePath, + type: "image/jpeg", + name: "profile.jpg", + } as any); + + const response = await fetch( + `${base_URL}user/update/userImg`, + { + method: "PUT", + headers: { + Accept: "application/json", + Authorization: `Bearer ${token}`, + }, + body: formData, + } + ); + + const result = await response.json(); + if (result.status === "success") { + Alert.alert("Success", "Profile image updated successfully"); + + // If API returns updated image, set it + if (result.data?.imageBlob) { + setUser((prev) => prev ? { ...prev, imageBlob: result.data.imageBlob, filePath: null } : prev); + } else { + // Otherwise, keep the local filePath until next fetch + setUser((prev) => prev ? { ...prev, filePath: user.filePath } : prev); + } + + // Refresh profile from API as well + fetchUserProfile(); } else { - // Otherwise, keep the local filePath until next fetch - setUser((prev) => prev ? { ...prev, filePath: user.filePath } : prev); + Alert.alert("Error", result.message || "Failed to update profile image"); } - - // Refresh profile from API as well - fetchUserProfile(); - } else { - Alert.alert("Error", result.message || "Failed to update profile image"); + } catch (error) { + console.error("❌ Error updating profile image:", error); + Alert.alert("Error", "Could not update profile image"); + } finally { + setImageUpdating(false); } - } catch (error) { - console.error("❌ Error updating profile image:", error); - Alert.alert("Error", "Could not update profile image"); - } finally { - setImageUpdating(false); - } -}; + }; useEffect(() => { @@ -301,15 +204,15 @@ 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 = () => { + 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"; + }; if (fetching || !user) { @@ -324,7 +227,7 @@ const getProfileImage = () => { return ( - + {/* Header */} User Profile @@ -332,201 +235,207 @@ const getProfileImage = () => { + + + {/* Profile Card */} + + {/* Profile Image Section */} + + + console.log("Error loading image")} + /> + + + + - - {/* Profile Card */} - - {/* Profile Image Section */} - - - console.log("Error loading image")} - /> - - - - - - - - {!editing ? ( - <> - {/* Profile Details */} - - Personal Information - - - Become Organizer - - - - - - Name - {user.firstName} {user.lastName} - - - - - - - Email - {user.email || "Not provided"} - - - - - - - Mobile - {user.mobile || "Not provided"} - - - - - - - Username - {user.username} - - - - - - - Address - {user.address || "Not provided"} - - - - - - - Gender - {user.gender || "Not specified"} - - - - - - - Role - {user.role || "User"} - - - - - setEditing(true)} - icon="create" - style={styles.editButton} /> - - ) : ( - <> - {/* Edit Form */} - - Edit Profile - - setUser({ ...user, firstName: text })} - icon="person" - /> - - setUser({ ...user, lastName: text })} - icon="person" - /> - - setUser({ ...user, email: text })} - icon="mail" - keyboardType="email-address" - /> - - setUser({ ...user, username: text })} - icon="at" - /> - - setUser({ ...user, address: text })} - icon="location" - multiline - /> - - setUser({ ...user, gender: text })} - icon="male-female" - /> - + + + {!editing ? ( + <> + {/* Profile Details */} + + Personal Information + + + Become Organizer + + + + + + Name + {user.firstName} {user.lastName} + + + + + + + Email + {user.email || "Not provided"} + + + + + + + Mobile + {user.mobile || "Not provided"} + + + + + + + Username + {user.username} + + + + + + + Address + {user.address || "Not provided"} + + + + + + + Gender + {user.gender || "Not specified"} + + + + + + + Role + {user.role || "User"} + + + - - setEditing(false)} - icon="close" - style={styles.cancelButton} + title="Edit Profile" + onPress={() => setEditing(true)} + icon="create" + style={styles.editButton} /> - - - )} - - + + ) : ( + <> + {/* Edit Form */} + + Edit Profile + + setUser({ ...user, firstName: text })} + icon="person" + /> + + setUser({ ...user, lastName: text })} + icon="person" + /> + + setUser({ ...user, email: text })} + icon="mail" + keyboardType="email-address" + /> + + setUser({ ...user, username: text })} + icon="at" + /> + setUser({ ...user, address: text })} + icon="location" + multiline + /> + setUser({ ...user, gender: text })} + icon="male-female" + /> + + + + setEditing(false)} + icon="close" + style={styles.cancelButton} + /> + + + )} + + + ); }; const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: "#f8fafc" + container: { + flex: 1, + backgroundColor: "#f8fafc", + marginBottom: '30%', }, - loadingContainer: { - flex: 1, - justifyContent: "center", + loadingContainer: { + flex: 1, + justifyContent: "center", alignItems: "center", backgroundColor: "#f8fafc" }, - loadingText: { - marginTop: 10, - fontSize: 16, - color: "#64748b" + loadingText: { + marginTop: 10, + fontSize: 16, + color: "#64748b" }, header: { flexDirection: "row", @@ -543,17 +452,15 @@ const styles = StyleSheet.create({ shadowOpacity: 0.2, shadowRadius: 4, }, - headerTitle: { - fontSize: 22, - fontWeight: "700", - color: "#fff" + headerTitle: { + fontSize: 22, + fontWeight: "700", + color: "#fff" }, refreshButton: { padding: 4, }, - scrollContainer: { - flex: 1, - }, + profileCard: { borderRadius: 20, margin: 16, @@ -576,10 +483,10 @@ const styles = StyleSheet.create({ position: "relative", marginBottom: 16, }, - avatar: { - width: 120, - height: 120, - borderRadius: 60, + avatar: { + width: 120, + height: 120, + borderRadius: 60, borderWidth: 4, borderColor: "#e2e8f0", }, @@ -636,6 +543,7 @@ const styles = StyleSheet.create({ }, editSection: { marginBottom: 20, + flex: 1, }, editButtons: { gap: 12, diff --git a/src/utils/ApiHelper.tsx b/src/utils/ApiHelper.tsx new file mode 100644 index 0000000..897d140 --- /dev/null +++ b/src/utils/ApiHelper.tsx @@ -0,0 +1,53 @@ +import axios, { AxiosRequestConfig } from "axios"; +import * as Keychain from "react-native-keychain"; + +/** + * Generic API call function with proper error handling and optional auth. + * @param url - API endpoint + * @param method - HTTP method: GET, POST, PUT, DELETE + * @param data - Request body for POST/PUT + * @param config - Optional Axios config (headers, params, etc.) + */ +export async function apiCall( + url: string, + method: "GET" | "POST" | "PUT" | "DELETE" = "GET", + data?: any, + config?: AxiosRequestConfig +): Promise { + try { + // Get saved credentials from Keychain + const creds = await Keychain.getGenericPassword(); + + const headers = { + "Content-Type": "application/json", + ...(creds ? { Authorization: `Bearer ${creds.password}` } : {}), + ...config?.headers, + }; + + const response = await axios({ + url, + method, + data, + ...config, + headers, + }); + + return response.data; + } catch (error: any) { + if (error.response) { + // Server responded with a status other than 2xx + console.error( + "API Error:", + `Status: ${error.response.status}`, + `Data: ${JSON.stringify(error.response.data)}` + ); + } else if (error.request) { + // Request was made but no response received + console.error("API Error: No response received", error.request); + } else { + // Something else went wrong while setting up the request + console.error("API Error:", error.message); + } + return null; // return null so the caller can handle it + } +} diff --git a/src/utils/ApiUrls.tsx b/src/utils/ApiUrls.tsx index 6241a2b..6ff1acf 100644 --- a/src/utils/ApiUrls.tsx +++ b/src/utils/ApiUrls.tsx @@ -1,3 +1,3 @@ export const base_URL = "https://api.godemo.in/FundCollect/api/" export const LOGIN_URL = `${base_URL}user/generate/otp?mobile=` -export const VARIFY_OTP_URL = `${base_URL}user/otp/verify` \ No newline at end of file +export const VARIFY_OTP_URL = `${base_URL}user/otp/verify`