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`