From 29ac0307db039b929be973965f2a7a02d2602ad2 Mon Sep 17 00:00:00 2001 From: Abin Toppo Date: Mon, 15 Sep 2025 10:50:58 +0530 Subject: [PATCH] profile not done --- App.tsx | 2 +- android/app/src/main/AndroidManifest.xml | 53 +- package-lock.json | 196 +++++- package.json | 3 + src/components/AppView.tsx | 37 ++ src/components/CustomButton.tsx | 50 ++ src/navigation/AppNavigator.tsx | 19 +- src/navigation/BottomTabs.tsx | 9 +- src/screens/GroupCreation.tsx | 204 ++++++ src/screens/GroupReview.tsx | 94 +++ src/screens/GroupScreen.tsx | 10 +- src/screens/ProfileScreen.tsx | 769 +++++++++++++++++++++++ src/utils/ApiHelper.tsx | 53 ++ src/utils/ApiUrls.tsx | 3 +- 14 files changed, 1462 insertions(+), 40 deletions(-) create mode 100644 src/components/AppView.tsx create mode 100644 src/components/CustomButton.tsx create mode 100644 src/screens/GroupReview.tsx create mode 100644 src/screens/ProfileScreen.tsx create mode 100644 src/utils/ApiHelper.tsx diff --git a/App.tsx b/App.tsx index 316c24d..b57233d 100644 --- a/App.tsx +++ b/App.tsx @@ -16,7 +16,7 @@ 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 82d8250..7ec7f75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "CircleCollect", "version": "0.0.1", "dependencies": { + "@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", @@ -21,6 +22,8 @@ "npm": "^11.6.0", "react": "19.1.0", "react-native": "0.81.1", + "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", @@ -2964,6 +2967,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", @@ -3747,6 +3763,25 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-native": { + "version": "0.70.19", + "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.19.tgz", + "integrity": "sha512-c6WbyCgWTBgKKMESj/8b4w+zWcZSsCforson7UdXtXMecG3MxCinYi6ihhrHVPyUrVzORsvEzK8zg32z4pK6Sg==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-native-vector-icons": { + "version": "6.4.18", + "resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.18.tgz", + "integrity": "sha512-YGlNWb+k5laTBHd7+uZowB9DpIK3SXUneZqAiKQaj1jnJCZM0x71GDim5JCTMi4IFkhc9m8H/Gm28T5BjyivUw==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@types/react-native": "^0.70" + } + }, "node_modules/@types/react-test-renderer": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz", @@ -4433,9 +4468,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", + "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -5476,7 +5511,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8894,7 +8928,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { @@ -8904,6 +8937,13 @@ "devOptional": true, "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -12420,6 +12460,15 @@ "node": ">=8" } }, + "node_modules/opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "license": "MIT", + "bin": { + "opencollective-postinstall": "index.js" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -13062,6 +13111,52 @@ } } }, + "node_modules/react-native-elements": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/react-native-elements/-/react-native-elements-3.4.3.tgz", + "integrity": "sha512-VtZc25EecPZyUBER85zFK9ZbY6kkUdcm1ZwJ9hdoGSCr1R/GFgxor4jngOcSYeMvQ+qimd5No44OVJW3rSJECA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@types/react-native-vector-icons": "^6.4.6", + "color": "^3.1.2", + "deepmerge": "^4.2.2", + "hoist-non-react-statics": "^3.3.2", + "lodash.isequal": "^4.5.0", + "opencollective-postinstall": "^2.0.3", + "react-native-ratings": "8.0.4", + "react-native-size-matters": "^0.3.1" + }, + "peerDependencies": { + "react-native-safe-area-context": ">= 3.0.0", + "react-native-vector-icons": ">7.0.0" + } + }, + "node_modules/react-native-elements/node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/react-native-elements/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/react-native-elements/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, "node_modules/react-native-gesture-handler": { "version": "2.28.0", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz", @@ -13078,6 +13173,16 @@ "react-native": "*" } }, + "node_modules/react-native-image-picker": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-8.2.1.tgz", + "integrity": "sha512-FBeGYJGFDjMdGCcyubDJgBAPCQ4L1D3hwLXyUU91jY9ahOZMTbluceVvRmrEKqnDPFJ0gF1NVhJ0nr1nROFLdg==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-is-edge-to-edge": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz", @@ -13156,6 +13261,19 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, + "node_modules/react-native-ratings": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/react-native-ratings/-/react-native-ratings-8.0.4.tgz", + "integrity": "sha512-Xczu5lskIIRD6BEdz9A0jDRpEck/SFxRqiglkXi0u67yAtI1/pcJC76P4MukCbT8K4BPVl+42w83YqXBoBRl7A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-safe-area-context": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz", @@ -13181,6 +13299,74 @@ "react-native": "*" } }, + "node_modules/react-native-size-matters": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/react-native-size-matters/-/react-native-size-matters-0.3.1.tgz", + "integrity": "sha512-mKOfBLIBFBcs9br1rlZDvxD5+mAl8Gfr5CounwJtxI6Z82rGrMO+Kgl9EIg3RMVf3G855a85YVqHJL2f5EDRlw==", + "license": "MIT", + "peerDependencies": { + "react-native": "*" + } + }, + "node_modules/react-native-vector-icons": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz", + "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" + }, + "bin": { + "fa-upgrade.sh": "bin/fa-upgrade.sh", + "fa5-upgrade": "bin/fa5-upgrade.sh", + "fa6-upgrade": "bin/fa6-upgrade.sh", + "generate-icon": "bin/generate-icon.js" + } + }, + "node_modules/react-native-vector-icons/node_modules/cliui": { + "version": "7.0.4", + "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", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/react-native-vector-icons/node_modules/yargs": { + "version": "16.2.0", + "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", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native-vector-icons/node_modules/yargs-parser": { + "version": "20.2.9", + "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" + } + }, "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", diff --git a/package.json b/package.json index d5eae8e..12709bc 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "test": "jest" }, "dependencies": { + "@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", @@ -23,6 +24,8 @@ "npm": "^11.6.0", "react": "19.1.0", "react-native": "0.81.1", + "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", 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/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 91dfc43..8f0c0d1 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -7,6 +7,8 @@ import OtpVerificationScreen from "../screens/OtpVarificationScreen"; import BottomTabs from "./BottomTabs"; import SplashScreen from "../screens/SplashScreen"; import OrganizerRegisterScreen from "../screens/OrgnizerRagister"; +import GroupCreation from "../screens/GroupCreation"; +import GroupReview from "../screens/GroupReview"; const Stack = createNativeStackNavigator(); export type RootStackParamList = { @@ -16,6 +18,9 @@ const Stack = createNativeStackNavigator(); HomeTabs:undefined splash: undefined OrganizerRegister:undefined + Profile: undefined; + GroupCreation: undefined; + GroupReview: undefined; }; @@ -39,15 +44,25 @@ function ProfileScreen() { } 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 72810e9..1542985 100644 --- a/src/navigation/BottomTabs.tsx +++ b/src/navigation/BottomTabs.tsx @@ -3,6 +3,7 @@ import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; import { View, Text, StyleSheet, Platform } from "react-native"; import { Ionicons } from '@react-native-vector-icons/ionicons'; import GroupScreen from "../screens/GroupScreen"; +import ProfileScreen from "../screens/ProfileScreen"; const Tab = createBottomTabNavigator(); @@ -24,13 +25,7 @@ function HomeScreen() { ); } -function ProfileScreen() { - return ( - - 👤 Profile - - ); -} + function SettingsScreen() { return ( diff --git a/src/screens/GroupCreation.tsx b/src/screens/GroupCreation.tsx index e69de29..30b79cd 100644 --- a/src/screens/GroupCreation.tsx +++ 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 index 3c43e35..d9b370e 100644 --- a/src/screens/GroupScreen.tsx +++ b/src/screens/GroupScreen.tsx @@ -3,10 +3,13 @@ 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 ( @@ -16,7 +19,9 @@ export default function GroupScreen() { } - onPress={() => setTooltipVisible(true)} + onPress={() => { + navigation.navigate('GroupCreation') + }} /> @@ -30,6 +35,7 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: "#dae4ed", padding: 16, + paddingTop: 40, }, title: { fontSize: 22, diff --git a/src/screens/ProfileScreen.tsx b/src/screens/ProfileScreen.tsx new file mode 100644 index 0000000..44fe699 --- /dev/null +++ b/src/screens/ProfileScreen.tsx @@ -0,0 +1,769 @@ +import React, { useState, useEffect } from "react"; +import { + StyleSheet, + View, + Text, + ScrollView, + Image, + TouchableOpacity, + Alert, + ActivityIndicator, + StatusBar, + TextInput, + Platform, +} from "react-native"; +import * as Keychain from "react-native-keychain"; +import Ionicons from "react-native-vector-icons/Ionicons"; +import { launchImageLibrary, ImageLibraryOptions, Asset } from "react-native-image-picker"; + +type ButtonType = "primary" | "secondary" | "danger" | "outline"; + +type CustomButtonProps = { + title: string; + onPress: () => void; + type?: ButtonType; + icon?: string; + style?: any; + disabled?: boolean; +}; + +const CustomButton = ({ + title, + onPress, + type = "primary", + icon, + style, + disabled = false, +}: 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; + } + }; + + const textColor = + Array.isArray(getTextStyle()) + ? getTextStyle().find((s: any) => s && typeof s === "object" && "color" in s)?.color || "#fff" + : (getTextStyle() as any).color || "#fff"; + + return ( + + {icon ? : null} + {title} + + ); +}; + +type CustomInputProps = { + label?: string; + placeholder?: string; + value?: string; + onChangeText?: (text: string) => void; + icon?: string; + [key: string]: any; +}; + +const CustomInput = ({ + label, + placeholder, + value, + onChangeText, + icon, + ...props +}: CustomInputProps) => { + return ( + + {label ? {label} : null} + + {icon ? : null} + + + + ); +}; + +type UserProfile = { + id: string; + userId: number; + firstName: string | null; + lastName: string | null; + mobile: string | null; + gender: string | null; + email: string | null; + username: string | null; + address: string | null; + bankName: string | null; + branchCode: string | null; + filePath: string | null; // local selection + bankAccountNumber: string | null; + imageBlob: string | null; // base64 from API + organizerStatus?: string; + role?: string; +}; + +const ProfileScreen = () => { + const [user, setUser] = useState(null); + const [fetching, setFetching] = useState(true); + const [editing, setEditing] = useState(false); + const [imageUpdating, setImageUpdating] = useState(false); + + const fetchUserProfile = async () => { + try { + const credentials = await Keychain.getGenericPassword(); + if (!credentials) { + Alert.alert("Error", "You are not logged in"); + return; + } + const token = credentials.password; + + setFetching(true); + const response = await fetch( + "https://exotic-humpback-mistakenly.ngrok-free.app/api/user/profile/view", + { + method: "GET", + headers: { + Accept: "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); + + if (!response.ok) { + Alert.alert("Error", `Failed to fetch user profile (${response.status})`); + setFetching(false); + return; + } + + const result = await response.json(); + if (result.status === "success") { + setUser(result.data as UserProfile); + } else { + Alert.alert("Error", result.message || "Failed to fetch user profile"); + } + } catch (error) { + console.error("❌ Error fetching profile:", error); + Alert.alert("Error", "Could not connect to server"); + } finally { + setFetching(false); + } + }; + + const updateProfile = async () => { + try { + if (!user) return; + const credentials = await Keychain.getGenericPassword(); + if (!credentials) { + Alert.alert("Error", "You are not logged in"); + return; + } + const token = credentials.password; + + const response = await fetch( + "https://exotic-humpback-mistakenly.ngrok-free.app/api/user/profile/update", + { + method: "PUT", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + address: user.address || null, + email: user.email || null, + firstName: user.firstName || null, + gender: user.gender || null, + lastName: user.lastName || null, + username: user.username || null, + }), + } + ); + + if (!response.ok) { + Alert.alert("Error", `Failed to update profile (${response.status})`); + return; + } + + const result = await response.json(); + if (result.status === "success") { + Alert.alert("Success", "Profile updated successfully"); + setEditing(false); + fetchUserProfile(); + } else { + Alert.alert("Error", result.message || "Failed to update profile"); + } + } catch (error) { + console.error("❌ Error updating profile:", error); + Alert.alert("Error", "Could not update profile"); + } + }; + + const pickImage = () => { + const options: ImageLibraryOptions = { + mediaType: "photo", + quality: 0.7, + selectionLimit: 1, + includeBase64: false, + }; + launchImageLibrary(options, (response) => { + if (response.didCancel) return; + if (response.errorCode) { + Alert.alert("Error", response.errorMessage || "Image picker error"); + return; + } + const asset: Asset | undefined = response.assets?.[0]; + if (!asset) return; + setUser((prev) => (prev ? { ...prev, filePath: asset.uri ?? null } : prev)); + }); + }; + + 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); + + // Normalize uri for iOS if needed + const normalizedUri = + Platform.OS === "ios" ? user.filePath.replace("file://", "") : user.filePath; + + const formData = new FormData(); + // Best effort mime/name; servers often ignore name but it's helpful + const name = "profile.jpg"; + const type = "image/jpeg"; + + formData.append("file", { + uri: normalizedUri, + name, + type, + } as any); + + const response = await fetch( + "https://exotic-humpback-mistakenly.ngrok-free.app/api/user/update/userImg", + { + method: "PUT", + headers: { + Accept: "application/json", + Authorization: `Bearer ${token}`, + // Do NOT set Content-Type manually; let fetch add proper multipart boundary + }, + body: formData, + } + ); + + if (!response.ok) { + // Try to parse JSON; if fails, provide status + let message = `Failed to update profile image (${response.status})`; + try { + const err = await response.json(); + if (err?.message) message = err.message; + } catch {} + Alert.alert("Error", message); + return; + } + + const result = await response.json(); + if (result.status === "success") { + Alert.alert("Success", "Profile image updated successfully"); + + if (result.data?.imageBlob) { + setUser((prev) => + prev ? { ...prev, imageBlob: result.data.imageBlob, filePath: null } : prev + ); + } else { + setUser((prev) => prev ? { ...prev, filePath: user.filePath } : prev); + } + + 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); + } + }; + + useEffect(() => { + fetchUserProfile(); + }, []); + + 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) { + return ( + + + Loading profile... + + ); + } + + return ( + + + + + User Profile + + + + + + + + + + console.log("Error loading image")} + /> + + + + + + + + + {!editing ? ( + <> + + 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 Profile + + + setUser((prev) => (prev ? { ...prev, firstName: text } : prev)) + } + icon="person" + /> + + + setUser((prev) => (prev ? { ...prev, lastName: text } : prev)) + } + icon="person" + /> + + + setUser((prev) => (prev ? { ...prev, email: text } : prev)) + } + icon="mail" + keyboardType="email-address" + autoCapitalize="none" + /> + + + setUser((prev) => (prev ? { ...prev, username: text } : prev)) + } + icon="at-circle" + autoCapitalize="none" + /> + + + setUser((prev) => (prev ? { ...prev, address: text } : prev)) + } + icon="location" + multiline + /> + + + setUser((prev) => (prev ? { ...prev, gender: text } : prev)) + } + icon="male-female" + /> + + + + + setEditing(false)} + icon="close" + style={styles.cancelButton} + /> + + + )} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#f8fafc", + }, + loadingContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + backgroundColor: "#f8fafc", + }, + loadingText: { + marginTop: 10, + fontSize: 16, + color: "#64748b", + }, + header: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + padding: 16, + paddingTop: 50, + backgroundColor: "#6366f1", + borderBottomLeftRadius: 20, + borderBottomRightRadius: 20, + elevation: 4, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 4, + }, + headerTitle: { + fontSize: 22, + fontWeight: "700", + color: "#fff", + }, + refreshButton: { + padding: 4, + }, + scrollContainer: { + flex: 1, + }, + profileCard: { + borderRadius: 20, + margin: 16, + backgroundColor: "#fff", + padding: 20, + shadowColor: "#000", + shadowOpacity: 0.1, + shadowRadius: 10, + shadowOffset: { width: 0, height: 4 }, + elevation: 5, + }, + profileImageSection: { + alignItems: "center", + marginBottom: 24, + paddingBottom: 20, + borderBottomWidth: 1, + borderBottomColor: "#f1f5f9", + }, + avatarContainer: { + position: "relative", + marginBottom: 16, + }, + avatar: { + width: 120, + height: 120, + borderRadius: 60, + borderWidth: 4, + borderColor: "#e2e8f0", + }, + cameraIcon: { + position: "absolute", + right: 0, + bottom: 0, + backgroundColor: "#6366f1", + borderRadius: 20, + width: 40, + height: 40, + justifyContent: "center", + alignItems: "center", + borderWidth: 3, + borderColor: "#fff", + elevation: 2, + }, + updateImageButton: { + width: "80%", + }, + detailsSection: { + marginBottom: 24, + }, + sectionTitle: { + fontSize: 18, + fontWeight: "700", + color: "#1e293b", + marginBottom: 16, + paddingBottom: 8, + borderBottomWidth: 1, + borderBottomColor: "#f1f5f9", + }, + detailRow: { + flexDirection: "row", + alignItems: "center", + marginBottom: 16, + paddingHorizontal: 8, + }, + detailIcon: { + marginRight: 12, + }, + detailLabel: { + fontSize: 12, + color: "#64748b", + marginBottom: 2, + }, + detailText: { + fontSize: 16, + color: "#334155", + fontWeight: "500", + }, + editButton: { + marginTop: 8, + }, + editSection: { + marginBottom: 20, + }, + editButtons: { + gap: 12, + }, + saveButton: { + marginTop: 8, + }, + cancelButton: { + marginTop: 4, + }, + inputContainer: { + marginBottom: 16, + }, + inputLabel: { + fontSize: 14, + fontWeight: "600", + color: "#374151", + marginBottom: 6, + }, + inputWrapper: { + flexDirection: "row", + alignItems: "center", + position: "relative", + }, + inputIcon: { + position: "absolute", + left: 12, + zIndex: 1, + }, + input: { + borderWidth: 1, + borderColor: "#d1d5db", + borderRadius: 12, + padding: 12, + backgroundColor: "#fff", + color: "#1f2937", + fontSize: 16, + flex: 1, + }, + inputWithIcon: { + paddingLeft: 40, + }, + button: { + backgroundColor: "#6366f1", + paddingVertical: 14, + borderRadius: 12, + alignItems: "center", + justifyContent: "center", + flexDirection: "row", + shadowColor: "#6366f1", + shadowOpacity: 0.3, + shadowRadius: 6, + shadowOffset: { width: 0, height: 3 }, + elevation: 4, + }, + buttonSecondary: { + backgroundColor: "#f1f5f9", + }, + buttonDanger: { + backgroundColor: "#ef4444", + }, + buttonOutline: { + backgroundColor: "transparent", + borderWidth: 1, + borderColor: "#6366f1", + }, + buttonText: { + color: "#fff", + fontSize: 16, + fontWeight: "600", + }, + buttonTextSecondary: { + color: "#334155", + }, + buttonTextDanger: { + color: "#fff", + }, + buttonTextOutline: { + color: "#6366f1", + }, + buttonIcon: { + marginRight: 8, + }, +}); + +export default ProfileScreen; 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 c28ce26..03cbc67 100644 --- a/src/utils/ApiUrls.tsx +++ b/src/utils/ApiUrls.tsx @@ -1,3 +1,4 @@ export const LOGIN_URL = "https://exotic-humpback-mistakenly.ngrok-free.app/api/user/generate/otp?mobile=" export const VARIFY_OTP_URL = "https://exotic-humpback-mistakenly.ngrok-free.app/api/user/otp/verify" -export const ORGRAGISTRATION = "https://exotic-humpback-mistakenly.ngrok-free.app/api/organizer/registration/apply " \ No newline at end of file +export const ORGRAGISTRATION = "https://exotic-humpback-mistakenly.ngrok-free.app/api/organizer/registration/apply" +export const GROUP_CREATION_URL = "https://exotic-humpback-mistakenly.ngrok-free.app/api/group/create"