11"use client" ;
22import "antd/dist/reset.css" ;
3- import { useEffect , useState } from "react" ;
3+ import { useEffect , useState , useRef } from "react" ;
44import { Row , Col , Steps , Space , Button , Alert } from "antd" ;
55import { FormikWizard , RenderProps } from "formik-wizard-form" ;
66import axios from "axios" ;
@@ -17,6 +17,7 @@ import {
1717 recruiterDetailsValidationSchema ,
1818 jobDetailsValidationSchema ,
1919} from "../../validation/jaf.validation" ;
20+ import ReCAPTCHA from "react-google-recaptcha" ;
2021
2122const baseUrl = process . env . NEXT_PUBLIC_BACKEND_URL ;
2223const { Step } = Steps ;
@@ -72,9 +73,12 @@ const getErrorMessages = (errors: any): string[] => {
7273function JAF ( ) {
7374 const accessToken = Cookies . get ( "accessToken" ) ;
7475 const [ isLoading , setIsLoading ] = useState ( true ) ;
76+ const [ captchaToken , setCaptchaToken ] = useState ( "" ) ;
77+ const [ showCaptcha , setShowCaptcha ] = useState ( false ) ;
78+ const recaptchaRef = useRef < ReCAPTCHA | null > ( null ) ;
7579 const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
7680
77- useEffect ( ( ) => {
81+ useEffect ( ( ) => {
7882 const timer = setTimeout ( ( ) => setIsLoading ( false ) , 500 ) ;
7983 return ( ) => clearTimeout ( timer ) ;
8084 } , [ ] ) ;
@@ -88,105 +92,102 @@ function JAF() {
8892 }
8993
9094 return (
91- < div className = "flex flex-col w-full gap-8 md:gap-20 p-2 md:p-10" > < FormikWizard
95+ < div className = "flex flex-col w-full gap-8 md:gap-20 p-2 md:p-10" >
96+ < FormikWizard
9297 key = "jaf-form-wizard"
9398 initialValues = { DEFAULT_FORM_VALUES }
9499 onSubmit = { async ( values : JAFFormValues ) => {
95- setIsSubmitting ( true ) ;
96- try {
97- // Filter and format recruiters
98- const recruiters = [ 1 , 2 , 3 ]
99- . map ( ( i ) => ( {
100- name : values [ `recName${ i } ` ] || "" ,
101- designation : values [ `designation${ i } ` ] || "" ,
102- email : values [ `email${ i } ` ] || "" ,
103- contact : values [ `phoneNumber${ i } ` ]
104- ? "+91 " + values [ `phoneNumber${ i } ` ]
105- : "" ,
106- landline : values [ `landline${ i } ` ] || "" ,
107- } ) )
108- . filter (
109- ( r ) =>
110- r . name . trim ( ) ||
111- r . designation . trim ( ) ||
112- r . email . trim ( ) ||
113- r . contact . trim ( ) ||
114- r . landline . trim ( ) ,
115- ) ;
100+ if ( ! captchaToken ) {
101+ setShowCaptcha ( true ) ;
102+ toast . error ( "Please complete the CAPTCHA to submit." ) ;
103+ return ;
104+ }
116105
117- // Ensure at least primary recruiter is present
118- if ( recruiters . length === 0 ) {
119- toast . error ( "At least one recruiter contact is required" ) ;
120- return ;
121- }
106+ const recruiters = [ 1 , 2 , 3 ]
107+ . map ( ( i ) => ( {
108+ name : values [ `recName${ i } ` ] || "" ,
109+ designation : values [ `designation${ i } ` ] || "" ,
110+ email : values [ `email${ i } ` ] || "" ,
111+ contact : values [ `phoneNumber${ i } ` ] ? "+91 " + values [ `phoneNumber${ i } ` ] : "" ,
112+ landline : values [ `landline${ i } ` ] || "" ,
113+ } ) )
114+ . filter (
115+ ( r ) =>
116+ r . name . trim ( ) ||
117+ r . designation . trim ( ) ||
118+ r . email . trim ( ) ||
119+ r . contact . trim ( ) ||
120+ r . landline . trim ( ) ,
121+ ) ;
122122
123- // Assemble payload matching backend DTO structure exactly
124- const payload : JafDto = {
125- job : {
126- seasonId : values . seasonId ,
127- role : values . role ,
128- description : values . description || undefined ,
129- recruiterDetailsFilled : recruiters ,
130- attachments : values . attachments ?. length
131- ? values . attachments . map ( ( file ) =>
132- typeof file === "string" ? file : file . name ,
133- )
134- : undefined ,
135- others : values . jobOthers || undefined ,
136- skills : values . skills ?. length ? values . skills : undefined ,
137- location : values . location ,
138- minNoOfHires : values . minNoOfHires
139- ? Number ( values . minNoOfHires )
140- : undefined ,
141- expectedNoOfHires : values . expectedNoOfHires
142- ? Number ( values . expectedNoOfHires )
143- : undefined ,
144- offerLetterReleaseDate : values . offerLetterReleaseDate
145- ? new Date ( values . offerLetterReleaseDate )
146- : undefined ,
147- joiningDate : values . joiningDate
148- ? new Date ( values . joiningDate )
149- : undefined ,
150- duration : values . duration || undefined ,
151- selectionProcedure : {
152- selectionMode : values . selectionMode ,
153- shortlistFromResume : values . shortlistFromResume ,
154- groupDiscussion : values . groupDiscussion ,
155- tests : values . tests || [ ] ,
156- interviews : values . interviews || [ ] ,
157- others : values . others || undefined ,
158- requirements :
159- values . numberOfMembers ||
160- values . numberOfRooms ||
161- values . otherRequirements
162- ? {
163- numberOfMembers : values . numberOfMembers
164- ? Number ( values . numberOfMembers )
165- : undefined ,
166- numberOfRooms : values . numberOfRooms
167- ? Number ( values . numberOfRooms )
168- : undefined ,
169- otherRequirements :
170- values . otherRequirements || undefined ,
171- }
172- : undefined ,
173- } ,
123+ if ( recruiters . length === 0 ) {
124+ toast . error ( "At least one recruiter contact is required" ) ;
125+ return ;
126+ }
127+ const payload : JafDto & { captchaToken : string } = {
128+ job : {
129+ seasonId : values . seasonId ,
130+ role : values . role ,
131+ description : values . description || undefined ,
132+ recruiterDetailsFilled : recruiters ,
133+ attachments : values . attachments ?. length
134+ ? values . attachments . map ( ( file ) =>
135+ typeof file === "string" ? file : file . name ,
136+ )
137+ : undefined ,
138+ others : values . jobOthers || undefined ,
139+ skills : values . skills ?. length ? values . skills : undefined ,
140+ location : values . location ,
141+ minNoOfHires : values . minNoOfHires
142+ ? Number ( values . minNoOfHires )
143+ : undefined ,
144+ expectedNoOfHires : values . expectedNoOfHires
145+ ? Number ( values . expectedNoOfHires )
146+ : undefined ,
147+ offerLetterReleaseDate : values . offerLetterReleaseDate
148+ ? new Date ( values . offerLetterReleaseDate )
149+ : undefined ,
150+ joiningDate : values . joiningDate
151+ ? new Date ( values . joiningDate )
152+ : undefined ,
153+ duration : values . duration || undefined ,
154+ selectionProcedure : {
155+ selectionMode : values . selectionMode ,
156+ shortlistFromResume : values . shortlistFromResume ,
157+ groupDiscussion : values . groupDiscussion ,
158+ tests : values . tests || [ ] ,
159+ interviews : values . interviews || [ ] ,
160+ others : values . others || undefined ,
161+ requirements :
162+ values . numberOfMembers ||
163+ values . numberOfRooms ||
164+ values . otherRequirements
165+ ? {
166+ numberOfMembers : values . numberOfMembers
167+ ? Number ( values . numberOfMembers )
168+ : undefined ,
169+ numberOfRooms : values . numberOfRooms
170+ ? Number ( values . numberOfRooms )
171+ : undefined ,
172+ otherRequirements :
173+ values . otherRequirements || undefined ,
174+ }
175+ : undefined ,
174176 } ,
175- salaries : values . salaries || [ ] ,
176- } ;
177+ } ,
178+ salaries : values . salaries || [ ] ,
179+ captchaToken : captchaToken ,
180+ } ;
177181
178- // Submit to backend
182+ try {
179183 await axios . post ( `${ baseUrl } ${ API_ENDPOINTS . SUBMIT_JAF } ` , payload , {
180184 headers : {
181185 Authorization : `Bearer ${ accessToken } ` ,
182186 "Content-Type" : "application/json" ,
183187 } ,
184188 } ) ;
185189
186- toast . success (
187- "JAF Form submitted successfully! Your application has been received." ,
188- ) ;
189- // Reset form or redirect as needed
190+ toast . success ( "JAF Form submitted successfully! Your application has been received." ) ;
190191 window . location . reload ( ) ;
191192 } catch ( error : any ) {
192193 console . error ( "JAF submission error:" , error ) ;
@@ -195,9 +196,13 @@ function JAF() {
195196 error . response ?. data ?. message ||
196197 error . response ?. data ?. error ||
197198 "Failed to submit JAF form. Please try again." ;
199+
198200 toast . error ( errorMessage ) ;
199201 } finally {
200202 setIsSubmitting ( false ) ;
203+ setCaptchaToken ( "" ) ;
204+ setShowCaptcha ( false ) ;
205+ recaptchaRef . current ?. reset ( ) ;
201206 }
202207 } }
203208 validateOnNext
@@ -225,6 +230,7 @@ function JAF() {
225230 isNextDisabled,
226231 isPrevDisabled,
227232 errors,
233+ isSubmitting,
228234 } : RenderProps ) => {
229235 const errorMessages = getErrorMessages ( errors ) ;
230236
@@ -245,37 +251,61 @@ function JAF() {
245251
246252 { renderComponent ( ) }
247253
248- < Row justify = "center" className = "px-2 md:px-0" >
249- < Space
250- size = "large"
251- className = "w-full max-w-xs flex justify-center"
252- >
253- < Button
254- disabled = { isPrevDisabled || isSubmitting }
255- onClick = { handlePrev }
256- className = "flex-1 min-w-20"
257- size = "large"
258- >
259- Previous
260- </ Button >
261- < Button
262- disabled = { isNextDisabled || isSubmitting }
263- onClick = { handleNext }
264- className = "flex-1 min-w-20"
265- size = "large"
266- type = "primary"
267- loading = { isSubmitting && currentStepIndex === 2 }
268- >
269- { isSubmitting && currentStepIndex === 2
270- ? "Submitting..."
271- : currentStepIndex === 2
272- ? "Finish"
273- : "Next" }
274- </ Button >
275- </ Space >
276- </ Row >
277-
278- { /* Show validation errors below the finish button */ }
254+ { currentStepIndex === 2 ? (
255+ < Row justify = "center" className = "pt-6" >
256+ < div className = "flex flex-col items-center gap-4" >
257+ { showCaptcha && (
258+ < ReCAPTCHA
259+ ref = { recaptchaRef }
260+ sitekey = { process . env . NEXT_PUBLIC_RECAPTCHA_SITE_KEY ! }
261+ onChange = { ( token ) => setCaptchaToken ( token || "" ) }
262+ />
263+ ) }
264+ < div className = "flex gap-2" >
265+ < Button
266+ disabled = { isPrevDisabled || isSubmitting }
267+ onClick = { handlePrev }
268+ className = "min-w-20"
269+ size = "large"
270+ >
271+ Previous
272+ </ Button >
273+ < Button
274+ disabled = { isPrevDisabled || isSubmitting }
275+ onClick = { handleNext }
276+ className = "min-w-20"
277+ size = "large"
278+ type = "primary"
279+ loading = { isSubmitting && currentStepIndex === 2 }
280+ >
281+ Finish
282+ </ Button >
283+ </ div >
284+ </ div >
285+ </ Row >
286+ ) : (
287+ < Row justify = "center" className = "px-2 md:px-0" >
288+ < Space size = "large" className = "w-full max-w-xs flex justify-center" >
289+ < Button
290+ disabled = { isPrevDisabled }
291+ onClick = { handlePrev }
292+ className = "flex-1 min-w-20"
293+ size = "large"
294+ >
295+ Previous
296+ </ Button >
297+ < Button
298+ disabled = { isNextDisabled }
299+ onClick = { handleNext }
300+ className = "flex-1 min-w-20"
301+ size = "large"
302+ type = "primary"
303+ >
304+ Next
305+ </ Button >
306+ </ Space >
307+ </ Row >
308+ ) }
279309 { currentStepIndex === 2 &&
280310 isNextDisabled &&
281311 errorMessages . length > 0 && (
0 commit comments