import React, { useState, useEffect } from "react"; import { ChevronLeft } from "lucide-react"; import { useDispatch, useSelector } from "react-redux"; import { updatePersonalDetails, updateEducationalDetails, updateFamilyDetails, updateLifestyleDetails, updatePartnerPreferences, submitForm, } from "../redux/registrationFormSlice"; import PersonalDetailsForm from "./PersonalDetailsForm"; import EducationalDetailsForm from "./EducationalDetailsForm"; import FamilyDetailsForm from "./FamilyDetailsForm"; import LifestyleDetailsForm from "./LifestyleDetailsForm"; import PartnerPreferencesForm from "./PartnerPreferencesForm"; import PreviewScreen from "./PreviewScreen"; import { useLocation } from "react-router-dom"; import { useRegisterStep1, useRegisterStep2, useRegisterStep3, useRegisterStep4, useRegisterStep5, } from "../hooks/useRegister"; import axiosInstance, { setAccessToken } from "../api/axiosInstance"; import toast from "react-hot-toast"; const STEP_FIELD_ORDER = { 1: [ "name", "gender", "mobileNumber", "dob", "height", "weight", "maritalStatus", "religion", "profileFor", "caste", "subCaste", "gothram", "raasi", "star", "email", "password", "confirmPassword", "state", "city", "pincode", "profiles", ], 2: [ "fieldOfStudy", "qualification", "collegeName", "occupation", "organization", "employeeType", "income", "workLocation", ], 3: [ "fatherName", "fatherOccupation", "motherName", "motherOccupation", "brotherCount", "sisterCount", "familyStatus", "nativePlace", ], 4: ["diets", "hobbies", "dob", "tob", "placeOfBirth"], 5: [ "ageRange", "castes", "subCastes", "occupations", "educations", "hobbies", "annualIncome", "states", "districts", ], }; const STEP1_SERVER_FIELD_MAP = { name: "name", mobile: "mobileNumber", mobile_number: "mobileNumber", mobileNumber: "mobileNumber", phone: "mobileNumber", email: "email", gender: "gender", dob: "dob", height: "height", weight: "weight", marital_status: "maritalStatus", maritalStatus: "maritalStatus", religion: "religion", profile_for: "profileFor", profileFor: "profileFor", caste: "caste", sub_caste: "subCaste", subCaste: "subCaste", gothram: "gothram", raasi: "raasi", star: "star", state: "state", district: "city", city: "city", pincode: "pincode", password: "password", confirm_password: "confirmPassword", confirmPassword: "confirmPassword", profiles: "profiles", profile_images: "profiles", }; const Stepper = ({ currentStep, onStepClick }) => { const steps = [ { num: 1, label: "Personal" }, { num: 2, label: "Educational" }, { num: 3, label: "Family" }, { num: 4, label: "Lifestyle" }, { num: 5, label: "Partner" }, { num: 6, label: "Preview" }, ]; return (
{steps.map((step, index) => (
onStepClick(step.num)} >
= step.num ? "bg-red-600 text-white" : "bg-gray-300 text-gray-600" }`} > {step.num}
{index < steps.length - 1 && (
step.num ? "bg-red-600" : "bg-gray-300" }`} /> )} ))}
); }; const StepperForm = () => { const dispatch = useDispatch(); const location = useLocation(); const personalDetails = useSelector( (state) => state.registerform.personalDetails ); const educationalDetails = useSelector( (state) => state.registerform.educationalDetails ); const familyDetails = useSelector((state) => state.registerform.familyDetails); const lifestyleDetails = useSelector( (state) => state.registerform.lifestyleDetails ); const partnerPreferences = useSelector( (state) => state.registerform.partnerPreferences ); const initialStep = location.state?.step || 1; const [currentStep, setCurrentStep] = useState(initialStep); const [isStep1Update, setIsStep1Update] = useState(false); const [errors, setErrors] = useState({}); const registerStep1 = useRegisterStep1(); const registerStep2 = useRegisterStep2(); const registerStep3 = useRegisterStep3(); const registerStep4 = useRegisterStep4(); const registerStep5 = useRegisterStep5(); const normalizeStep1Field = (key) => { if (!key) return key; const trimmed = String(key).trim(); if (!trimmed) return trimmed; // Map backend profile_images errors (e.g., profile_images.0) to the profiles field if (trimmed.startsWith("profile_images")) { return "profiles"; } return ( STEP1_SERVER_FIELD_MAP[trimmed] || STEP1_SERVER_FIELD_MAP[trimmed.toLowerCase()] || trimmed ); }; const coerceErrorMessage = (value) => { if (Array.isArray(value)) { return value.filter(Boolean).join(" "); } if (typeof value === "string") return value; if (value && typeof value === "object") { return ( value.message || value.msg || value.error || value.detail || "" ); } if (value === null || value === undefined) return ""; return String(value); }; const mapServerErrors = (error) => { const data = error?.response?.data ?? error?.data ?? error; if (!data) return {}; const payload = data.errors ?? data.error ?? data.data ?? data; if (typeof payload === "string") { return { _form: payload }; } if (Array.isArray(payload)) { const out = {}; payload.forEach((item) => { if (!item) return; if (typeof item === "string") { out._form = out._form ? `${out._form} ${item}` : item; return; } const key = item.field || item.name || item.param || item.key; const message = item.message || item.msg || item.error || item.detail || ""; if (key) { out[normalizeStep1Field(key)] = String(message || "Invalid value"); } }); return out; } if (typeof payload === "object") { const out = {}; Object.entries(payload).forEach(([key, value]) => { if ( (key === "message" || key === "error" || key === "detail") && typeof value === "string" ) { out._form = out._form ? `${out._form} ${value}` : value; return; } const normalizedKey = normalizeStep1Field(key); const message = coerceErrorMessage(value); if (!normalizedKey) return; out[normalizedKey] = message || "Invalid value"; }); return out; } return {}; }; const focusFirstError = (errorMap, fieldOrder = []) => { if (!errorMap || Object.keys(errorMap).length === 0) return; const order = fieldOrder.filter((key) => errorMap[key]); const firstKey = order[0] || Object.keys(errorMap).find((key) => key !== "_form"); if (!firstKey) return; setTimeout(() => { const byAria = document.querySelector( `[aria-labelledby~="${firstKey}-label"]` ); if (byAria && typeof byAria.focus === "function") { byAria.focus(); return; } const byName = document.querySelector(`[name="${firstKey}"]`); if (byName && typeof byName.focus === "function") { byName.focus(); return; } const byId = document.getElementById(firstKey); if (byId && typeof byId.focus === "function") { byId.focus(); } }, 0); }; const clearFieldErrors = (fields) => { if (!fields) return; const list = Array.isArray(fields) ? fields : [fields]; setErrors((prev) => { if (!prev || Object.keys(prev).length === 0) return prev; let changed = false; const next = { ...prev }; list.forEach((field) => { if (field in next) { delete next[field]; changed = true; } }); if (next._form) { delete next._form; changed = true; } return changed ? next : prev; }); }; useEffect(() => { // in case user comes again with a different step if (location.state?.step) { setCurrentStep(location.state.step); window.scrollTo(0, 0); } }, [location.state?.step]); useEffect(() => { const fetchPersonalDetails = async () => { try { const response = await axiosInstance.get("/get_personal_details"); const data = response.data; if (data.status === "success" && data.personal_details) { const pd = data.personal_details; setIsStep1Update(true); const mappedImages = (pd.images || []).map((url, index) => ({ id: `server-${index}`, preview: url, name: `image-${index}.png`, })); const formattedDob = pd.dob ? pd.dob.split("T")[0] : ""; dispatch( updatePersonalDetails({ name: pd.name || "", mobileNumber: pd.mobile || "", email: pd.email || "", gender: pd.gender || "", dob: formattedDob, height: pd.height || "", weight: pd.weight || "", maritalStatus: pd.marital_status_id || "", religion: pd.religion_id || "", profileFor: pd.profile_for_id || "", caste: pd.caste_id || "", subCaste: pd.sub_caste_id || "", gothram: pd.gothram_id || "", raasi: pd.raasi_id || "", star: pd.star_id || "", state: pd.state_id || "", city: pd.district_id || "", pincode: pd.pincode || "", profiles: mappedImages, }) ); } } catch (error) { console.error("Error fetching personal details:", error); } }; fetchPersonalDetails(); }, [dispatch]); const validateStep = (step) => { const newErrors = {}; if (step === 1) { const required = [ "name", "mobileNumber", "gender", "dob", "maritalStatus", "profileFor", "caste", "email", "state", "city", "pincode", ]; required.forEach((field) => { if (!personalDetails[field]) { newErrors[field] = "This field is required"; } }); if (!isStep1Update) { if (!personalDetails.password) newErrors.password = "This field is required"; if (!personalDetails.confirmPassword) newErrors.confirmPassword = "This field is required"; } if ( personalDetails.email && !/\S+@\S+\.\S+/.test(personalDetails.email) ) { newErrors.email = "Invalid email format"; } if ( personalDetails.mobileNumber && personalDetails.mobileNumber.length !== 10 ) { newErrors.mobileNumber = "Mobile number must be 10 digits"; } if (personalDetails.height && Number(personalDetails.height) > 10) { newErrors.height = "Height must be 10 or less"; } if (personalDetails.weight && Number(personalDetails.weight) > 300) { newErrors.weight = "Weight must be 300 or less"; } if ( personalDetails.password && personalDetails.confirmPassword && personalDetails.password !== personalDetails.confirmPassword ) { newErrors.confirmPassword = "Passwords do not match"; } } else if (step === 2) { const required = [ "qualification", "fieldOfStudy", "occupation", "organization", "employeeType", "income", ]; required.forEach((field) => { if (!educationalDetails[field]) { newErrors[field] = "This field is required"; } }); } else if (step === 3) { const required = [ "fatherName", "motherName", "familyStatus", ]; required.forEach((field) => { if (!familyDetails[field]) { newErrors[field] = "This field is required"; } }); } else if (step === 4) { const required = ["diets", "hobbies", "dob", "tob"]; required.forEach((field) => { const value = lifestyleDetails[field]; if (Array.isArray(value)) { if (value.length === 0) newErrors[field] = "This field is required"; } else if (!value) { newErrors[field] = "This field is required"; } }); } else if (step === 5) { const required = [ "ageRange", "castes", "subCastes", "occupations", "educations", "hobbies", "annualIncome", "states", "districts", ]; required.forEach((field) => { const value = partnerPreferences[field]; if (Array.isArray(value)) { if (value.length === 0) { newErrors[field] = "This field is required"; } return; } if (!value) { newErrors[field] = "This field is required"; } }); } setErrors(newErrors); if (Object.keys(newErrors).length > 0) { focusFirstError(newErrors, STEP_FIELD_ORDER[step] || []); } return Object.keys(newErrors).length === 0; }; const buildRegisterStep1Payload = async () => { const formData = new FormData(); formData.append("name", personalDetails.name); formData.append("mobile", personalDetails.mobileNumber); formData.append("email", personalDetails.email); formData.append("pincode", personalDetails.pincode); formData.append("gender", personalDetails.gender); formData.append("dob", personalDetails.dob); formData.append("height", personalDetails.height || ""); formData.append("weight", personalDetails.weight || ""); formData.append("marital_status", personalDetails.maritalStatus); formData.append("religion", personalDetails.religion); formData.append("profile_for", personalDetails.profileFor || ""); formData.append("caste", personalDetails.caste); formData.append("sub_caste", personalDetails.subCaste || ""); formData.append("gothram", personalDetails.gothram || ""); formData.append("raasi", personalDetails.raasi || ""); formData.append("star", personalDetails.star || ""); formData.append("state", personalDetails.state); formData.append("district", personalDetails.city); formData.append("password", personalDetails.password || ""); formData.append("web_fcm_token", localStorage.getItem("fcm_token") || ""); if (personalDetails.profiles && Array.isArray(personalDetails.profiles)) { for (const [index, item] of personalDetails.profiles.entries()) { // If the file object is intact (e.g., just added), use it directly. if (item.file && (item.file instanceof Blob || (typeof item.file === 'object' && item.file !== null && item.file.name))) { formData.append(`profile_images[${index}]`, item.file); } // If the file object was lost due to Redux serialization, // try to recover it from the blob URL preview. else if ( item.preview && typeof item.preview === "string" ) { try { const response = await fetch(item.preview); const blob = await response.blob(); const fileName = item.name || `profile_image_${index}.jpg`; const fileType = item.type || "image/jpeg"; const recoveredFile = new File([blob], fileName, { type: fileType }); formData.append(`profile_images[${index}]`, recoveredFile); } catch (e) { console.error(`Could not recover file from blob URL: ${item.preview}`, e); } } } } return formData; }; const buildRegisterStep2Payload = () => { const formData = new FormData(); formData.append("college_name", educationalDetails.collegeName || ""); formData.append("study_field", educationalDetails.fieldOfStudy || ""); formData.append("education", educationalDetails.qualification || ""); formData.append("occupation", educationalDetails.occupation || ""); formData.append("company_name", educationalDetails.organization || ""); formData.append("employee_type", educationalDetails.employeeType || ""); formData.append("annual_income", educationalDetails.income || ""); formData.append("work_location", educationalDetails.workLocation || ""); return formData; }; const buildRegisterStep3Payload = () => { const formData = new FormData(); formData.append("father_name", familyDetails.fatherName); formData.append("father_occupation", familyDetails.fatherOccupation || ""); formData.append("mother_name", familyDetails.motherName); formData.append("mother_occupation", familyDetails.motherOccupation || ""); formData.append("family_status", familyDetails.familyStatus); formData.append("native_place", familyDetails.nativePlace || ""); formData.append("brother_count", familyDetails.brotherCount || 0); formData.append("sister_count", familyDetails.sisterCount || 0); (familyDetails.brothers || []).forEach((brother, index) => { formData.append(`brothers[${index}][name]`, brother?.name || ""); formData.append(`brothers[${index}][occupation]`, brother?.occupation || ""); formData.append(`brothers[${index}][marital_status]`, brother?.maritalStatus || ""); formData.append(`brothers[${index}][have_childrens]`, brother?.haveChildrens ?? ""); }); (familyDetails.sisters || []).forEach((sister, index) => { formData.append(`sisters[${index}][name]`, sister?.name || ""); formData.append(`sisters[${index}][occupation]`, sister?.occupation || ""); formData.append(`sisters[${index}][marital_status]`, sister?.maritalStatus || ""); formData.append(`sisters[${index}][have_childrens]`, sister?.haveChildrens ?? ""); }); return formData; }; const buildRegisterStep4Payload = () => { const formData = new FormData(); formData.append("dob", lifestyleDetails.dob || ""); formData.append("tob", lifestyleDetails.tob || ""); formData.append("place_of_birth", lifestyleDetails.placeOfBirth || ""); (lifestyleDetails.diets || []).forEach((id, index) => { formData.append(`diets[${index}]`, id); }); (lifestyleDetails.hobbies || []).forEach((id, index) => { formData.append(`hobbies[${index}]`, id); }); const graha = lifestyleDetails.graha || {}; Object.keys(graha).forEach((house) => { const values = graha[house] || []; values.forEach((value, index) => { formData.append(`graha_${house}[${index}]`, value); }); }); const amsam = lifestyleDetails.amsam || {}; Object.keys(amsam).forEach((house) => { const values = amsam[house] || []; values.forEach((value, index) => { formData.append(`amsam_${house}[${index}]`, value); }); }); return formData; }; const buildRegisterStep5Payload = () => { const formData = new FormData(); formData.append("age_range", partnerPreferences.ageRange || ""); formData.append("annual_income", partnerPreferences.annualIncome || ""); (partnerPreferences.castes || []).forEach((id, index) => { formData.append(`castes[${index}]`, id); }); (partnerPreferences.subCastes || []).forEach((id, index) => { formData.append(`sub_castes[${index}]`, id); }); (partnerPreferences.occupations || []).forEach((id, index) => { formData.append(`occupations[${index}]`, id); }); (partnerPreferences.educations || []).forEach((id, index) => { formData.append(`educations[${index}]`, id); }); (partnerPreferences.hobbies || []).forEach((id, index) => { formData.append(`hobbies[${index}]`, id); }); (partnerPreferences.states || []).forEach((id, index) => { formData.append(`states[${index}]`, id); }); (partnerPreferences.districts || []).forEach((id, index) => { formData.append(`districts[${index}]`, id); }); return formData; }; const extractAccessToken = (res) => res?.access_token || res?.accessToken || res?.token || res?.data?.access_token || res?.data?.accessToken || res?.data?.token || res?.result?.access_token || res?.result?.accessToken || res?.result?.token || null; const handleStepSubmit = async () => { const isValid = validateStep(currentStep); if (!isValid) return; try { if (currentStep === 1) { const payload = await buildRegisterStep1Payload(); let res; if (isStep1Update) { res = await axiosInstance.post("/update_personal_details", payload); } else { res = await registerStep1.mutateAsync(payload); } const token = extractAccessToken(res.data || res); if (token) { setAccessToken(token); } } else if (currentStep === 2) { const payload = buildRegisterStep2Payload(); await registerStep2.mutateAsync(payload); } else if (currentStep === 3) { const payload = buildRegisterStep3Payload(); await registerStep3.mutateAsync(payload); } else if (currentStep === 4) { const payload = buildRegisterStep4Payload(); await registerStep4.mutateAsync(payload); } else if (currentStep === 5) { const payload = buildRegisterStep5Payload(); await registerStep5.mutateAsync(payload); } setErrors({}); setCurrentStep((prev) => Math.min(prev + 1, 6)); window.scrollTo(0, 0); } catch (e) { const serverErrors = mapServerErrors(e); let handled = false; if (Object.keys(serverErrors).length > 0) { const hasFieldErrors = Object.keys(serverErrors).some( (key) => key !== "_form" ); if (hasFieldErrors) { setErrors(serverErrors); focusFirstError(serverErrors, STEP_FIELD_ORDER[currentStep]); // For step 1, we know mapping is correct so field errors are sufficient feedback if (currentStep === 1) handled = true; } if (serverErrors._form) { toast.error(serverErrors._form, { position: "top-right" }); handled = true; } } if (!handled) { const msg = e?.response?.data?.message || e?.message || "Failed to submit step. Please try again."; toast.error(msg, { position: "top-right" }); } } }; const handleSkip = () => { setErrors({}); setCurrentStep((prev) => Math.min(prev + 1, 6)); window.scrollTo(0, 0); }; const handleStepClick = (step) => { setCurrentStep(step); setErrors({}); window.scrollTo(0, 0); }; const handleEdit = (step) => { setCurrentStep(step); setErrors({}); window.scrollTo(0, 0); }; const handleFinalSubmit = async () => { for (let i = 1; i <= 5; i++) { const ok = validateStep(i); if (!ok) { setCurrentStep(i); return; } } try { // final combined API call - replace with your final API await new Promise((resolve) => setTimeout(resolve, 500)); toast.success("Form submitted successfully!", { position: "top-right" }); } catch (e) { toast.error("Failed to submit form.", { position: "top-right" }); } }; const renderStepContent = () => { switch (currentStep) { case 1: return ( ); case 2: return ( ); case 3: return ( ); case 4: return ( ); case 5: return ( ); case 6: return ; default: return null; } }; const getTitle = () => { const titles = { 1: "Personal Details", 2: "Educational & Professional Details", 3: "Family Details", 4: "Lifestyle & Habits", 5: "Partner Preferences", 6: "Details Preview", }; return titles[currentStep] || ""; }; return (
{/* Header */}
{currentStep > 1 && currentStep < 6 && ( )}

{getTitle()}

{/* Content */}
{renderStepContent()}
); }; export default StepperForm;