before merge

This commit is contained in:
Abin Toppo
2026-01-12 15:21:14 +05:30
parent ed11bb479e
commit 360097da70
28 changed files with 4821 additions and 177 deletions
+511
View File
@@ -0,0 +1,511 @@
import { useNavigation } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
Platform,
KeyboardAvoidingView,
SafeAreaView,
ScrollView,
ToastAndroid,
} from 'react-native';
import colors from '../../Constant/Color';
import { User, Phone, Mail, Lock, Eye, EyeOff, ArrowLeft, CheckSquare, Square } from 'lucide-react-native';
import { useGetDesignation, useRegisterContractor } from '../../Services/BBPS/Hooks';
import MyDropdown from '../../Component/Dropdown/MyDropdown';
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
import TermsAndConditionsModal from '../../Component/TermsAndConditionsModal/TermsAndConditionsModal';
import { Formik } from 'formik';
import * as Yup from 'yup';
import Animated, { FadeInUp } from 'react-native-reanimated';
import { InsideHeader } from '../../Component/Index';
export type ContractorFormData = {
firstName: string;
middleName: string;
lastName: string;
dialCode: string;
mobNo: string;
emailAdd: string;
contractorType: string;
designation: number;
password: string;
conPassword: string;
otherDesignation: null;
};
type DesignationOption = {
label: string;
value: string; // string id from API
};
const SignupSchema = Yup.object().shape({
bidderType: Yup.string().required('Please select bidder type.'),
firstName: Yup.string().required('First name is required.'),
middleName: Yup.string().optional(),
lastName: Yup.string().required('Last name is required.'),
designation: Yup.string().required('Please select a designation.'), // string
mobileNumber: Yup.string()
.matches(/^[6-9]\d{9}$/, 'Invalid mobile number.')
.required('Mobile number is required.'),
email: Yup.string().email('Invalid email address.').required('Email is required.'),
password: Yup.string()
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
'Password must be at least 8 characters and include uppercase, lowercase, number, and special character.',
)
.required('Password is required.'),
confirmPassword: Yup.string()
.oneOf([Yup.ref('password')], 'Passwords do not match.')
.required('Confirm password is required.'),
});
const Signup = () => {
const navigation = useNavigation<any>();
const [designation, setDesignation] = useState<DesignationOption[]>([]);
const { data, isLoading: isDesignationLoading } = useGetDesignation();
const [checkModal, setCheckModal] = useState(false);
const [isCheck, setIsCheck] = useState(false);
const { mutate: registerContractor, isPending: isRegistering } = useRegisterContractor();
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
useEffect(() => {
const list = (data as any)?.data;
if (!Array.isArray(list)) return;
const formattedDesignation: DesignationOption[] = list.map((item: any) => ({
label: item.designationName,
value: String(item.designationid), // e.g. "3"
}));
setDesignation(formattedDesignation);
}, [data]);
const dropdownStyle = {
borderWidth: 1,
borderColor: colors.border,
borderRadius: 12,
paddingHorizontal: 14,
height: 50,
backgroundColor: colors.white,
marginBottom: 10,
};
const renderInputWithIcon = (
icon: 'user' | 'phone' | 'mail',
placeholder: string,
value: string,
onChangeText: (text: string) => void,
onBlur: () => void,
error?: string,
touched?: boolean,
props: object = {},
) => {
const IconComponent = icon === 'user' ? User : icon === 'phone' ? Phone : Mail;
return (
<>
<View style={styles.inputWrapper}>
<IconComponent size={18} color={colors.grayText} style={styles.icon} />
<TextInput
placeholder={placeholder}
style={styles.input}
placeholderTextColor={colors.placeholder}
value={value}
onChangeText={onChangeText}
onBlur={onBlur}
{...props}
/>
</View>
{touched && error && <Text style={styles.errorText}>{error}</Text>}
</>
);
};
const renderPasswordInput = (
placeholder: string,
value: string,
onChangeText: (text: string) => void,
onBlur: () => void,
show: boolean,
setShow: (show: boolean) => void,
error?: string,
touched?: boolean,
) => (
<>
<View style={styles.inputWrapper}>
<Lock size={18} color={colors.grayText} style={styles.icon} />
<TextInput
placeholder={placeholder}
style={styles.input}
placeholderTextColor={colors.placeholder}
value={value}
onChangeText={onChangeText}
onBlur={onBlur}
secureTextEntry={!show}
/>
<TouchableOpacity onPress={() => setShow(!show)}>
{show ? <Eye size={18} color={colors.grayText} /> : <EyeOff size={18} color={colors.grayText} />}
</TouchableOpacity>
</View>
{touched && error && <Text style={styles.errorText}>{error}</Text>}
</>
);
const handleSubmitForm = (values: any) => {
console.log('values', values);
if (!isCheck) {
Alert.alert('Check terms and condition');
return;
}
const formData: ContractorFormData = {
firstName: values.firstName.trim(),
middleName: values.middleName.trim(),
lastName: values.lastName.trim(),
dialCode: '+91',
mobNo: values.mobileNumber.trim(),
emailAdd: values.email.trim(),
contractorType: values.bidderType,
designation: values.designation ? Number(values.designation) : 0, // convert string -> number
password: values.password.trim(),
otherDesignation: null,
conPassword: values.confirmPassword.trim(),
};
registerContractor(formData, {
onSuccess: (res: any) => {
if (res?.status === '201') {
ToastAndroid.show(`${res.message}`, ToastAndroid.SHORT);
navigation.pop();
} else {
ToastAndroid.show(`Registration failed : ${res.message}`, ToastAndroid.SHORT);
}
},
onError: (error: any) => {
ToastAndroid.show(error.message, ToastAndroid.SHORT);
},
});
};
return (
<View style={styles.container}>
<SafeAreaView style={styles.container}>
<InsideHeader title="Sign Up" showArrow />
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={40}
>
<Formik
initialValues={{
bidderType: '',
firstName: '',
middleName: '',
lastName: '',
designation: '', // string, not 0
mobileNumber: '',
email: '',
password: '',
confirmPassword: '',
}}
validationSchema={SignupSchema}
onSubmit={handleSubmitForm}
>
{({
handleChange,
handleBlur,
handleSubmit,
values,
errors,
touched,
setFieldValue,
}) => (
<ScrollView
contentContainerStyle={styles.scrollContainer}
keyboardShouldPersistTaps="handled"
bounces={false}
>
<Animated.View entering={FadeInUp.duration(450).delay(80)}>
<Text style={styles.label}>Bidder Type</Text>
<View style={styles.toggleContainer}>
{['Indian', 'Foreign'].map(type => (
<TouchableOpacity
key={type}
style={[
styles.toggleButton,
values.bidderType === type && styles.selectedToggle,
]}
onPress={() => setFieldValue('bidderType', type)}
>
<Text
style={[
styles.toggleText,
values.bidderType === type && styles.selectedToggleText,
]}
>
{type}
</Text>
</TouchableOpacity>
))}
</View>
{touched.bidderType && errors.bidderType && (
<Text style={styles.errorText}>{errors.bidderType}</Text>
)}
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(120)}>
<View style={styles.row}>
<View style={styles.halfInput}>
{renderInputWithIcon(
'user',
'First Name *',
values.firstName,
handleChange('firstName'),
() => handleBlur('firstName'),
errors.firstName,
touched.firstName,
)}
</View>
<View style={styles.halfInput}>
{renderInputWithIcon(
'user',
'Middle Name',
values.middleName,
handleChange('middleName'),
() => handleBlur('middleName'),
errors.middleName,
touched.middleName,
)}
</View>
</View>
{renderInputWithIcon(
'user',
'Last Name *',
values.lastName,
handleChange('lastName'),
() => handleBlur('lastName'),
errors.lastName,
touched.lastName,
)}
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(160)}>
<Text style={styles.label}>Designation</Text>
<MyDropdown
label="Select Designation"
labelField="label"
valueField="value"
data={designation}
value={values.designation}
onChange={(item: any) => {
setFieldValue('designation', item.value);
}}
placeholder={isDesignationLoading ? 'Loading...' : 'Choose Designation'}
style={dropdownStyle}
/>
{touched.designation && errors.designation && (
<Text style={styles.errorText}>{errors.designation}</Text>
)}
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(200)}>
{renderInputWithIcon(
'phone',
'Mobile Number *',
values.mobileNumber,
handleChange('mobileNumber'),
() => handleBlur('mobileNumber'),
errors.mobileNumber,
touched.mobileNumber,
{
keyboardType: 'phone-pad',
maxLength: 10,
},
)}
{renderInputWithIcon(
'mail',
'Email *',
values.email,
handleChange('email'),
() => handleBlur('email'),
errors.email,
touched.email,
{
keyboardType: 'email-address',
autoCapitalize: 'none',
},
)}
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(240)}>
{renderPasswordInput(
'Password *',
values.password,
handleChange('password'),
() => handleBlur('password'),
showPassword,
setShowPassword,
errors.password,
touched.password,
)}
{renderPasswordInput(
'Confirm Password *',
values.confirmPassword,
handleChange('confirmPassword'),
() => handleBlur('confirmPassword'),
showConfirmPassword,
setShowConfirmPassword,
errors.confirmPassword,
touched.confirmPassword,
)}
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(280)}>
<View style={styles.termsRow}>
<TouchableOpacity onPress={() => setCheckModal(true)}>
{isCheck ? (
<CheckSquare size={20} color={colors.primary} />
) : (
<Square size={20} color={colors.black} />
)}
</TouchableOpacity>
<Text style={styles.termsText}>
By continuing, I agree to the Terms and Conditions
</Text>
</View>
</Animated.View>
<Animated.View entering={FadeInUp.duration(450).delay(320)} style={styles.buttonWrapper}>
<PrimaryButton
title={isRegistering ? 'Submitting...' : 'Submit'}
disabled={isRegistering}
onPress={() => handleSubmit()}
/>
</Animated.View>
{checkModal && (
<TermsAndConditionsModal
visible={checkModal}
onClose={() => setCheckModal(false)}
onChecked={() => setIsCheck(true)}
/>
)}
</ScrollView>
)}
</Formik>
</KeyboardAvoidingView>
</SafeAreaView>
</View>
);
};
export default Signup;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.white,
},
scrollContainer: {
flexGrow: 1,
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: 24,
justifyContent: 'flex-start',
},
label: {
fontSize: 15,
fontWeight: '600',
marginTop: 6,
marginBottom: 4,
color: colors.darkText,
},
toggleContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 4,
},
toggleButton: {
flex: 1,
paddingVertical: 10,
borderRadius: 10,
borderWidth: 1,
borderColor: colors.primary,
alignItems: 'center',
marginHorizontal: 4,
backgroundColor: colors.white,
},
selectedToggle: {
backgroundColor: colors.primary,
borderColor: colors.primary,
},
toggleText: {
fontSize: 14,
color: colors.grayText,
},
selectedToggleText: {
color: colors.white,
fontWeight: '600',
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 6,
},
halfInput: {
width: '48%',
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 12,
borderWidth: 1,
borderColor: colors.border,
paddingHorizontal: 10,
marginTop: 8,
height: 52,
},
icon: {
marginRight: 10,
},
input: {
flex: 1,
fontSize: 15,
color: colors.darkText,
paddingVertical: 0,
},
errorText: {
color: colors.error,
fontSize: 12,
marginTop: 3,
marginLeft: 4,
},
termsRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 14,
marginHorizontal: 2,
},
termsText: {
flex: 1,
fontSize: 13,
color: colors.darkText,
marginLeft: 8,
},
buttonWrapper: {
marginTop: 18,
},
});