1+ "use client" ;
2+
3+ import { useState } from "react" ;
4+ import { useForm } from "react-hook-form" ;
5+ import { zodResolver } from "@hookform/resolvers/zod" ;
6+ import { z } from "zod" ;
7+ import { X , Upload , Loader2 } from "lucide-react" ;
8+ import {
9+ Dialog ,
10+ DialogContent ,
11+ DialogHeader ,
12+ DialogTitle ,
13+ } from "@/components/ui/dialog" ;
14+ import { Button } from "@/components/ui/button" ;
15+ import Input from "@/components/ui/forms/input" ;
16+ import { Label } from "@codevs/ui/label" ;
17+ import { Textarea } from "@codevs/ui/textarea" ;
18+ import { useToast } from "@/components/ui/use-toast" ;
19+ import { JobListing } from "../_types/job-listings" ;
20+
21+ const applicationSchema = z . object ( {
22+ firstName : z . string ( ) . min ( 2 , "First name must be at least 2 characters" ) ,
23+ lastName : z . string ( ) . min ( 2 , "Last name must be at least 2 characters" ) ,
24+ email : z . string ( ) . email ( "Invalid email address" ) ,
25+ phone : z . string ( ) . min ( 10 , "Phone number must be at least 10 digits" ) ,
26+ linkedIn : z . string ( ) . url ( "Invalid LinkedIn URL" ) . optional ( ) . or ( z . literal ( "" ) ) ,
27+ portfolio : z . string ( ) . url ( "Invalid portfolio URL" ) . optional ( ) . or ( z . literal ( "" ) ) ,
28+ coverLetter : z . string ( ) . min ( 50 , "Cover letter must be at least 50 characters" ) ,
29+ experience : z . string ( ) . min ( 20 , "Please describe your relevant experience" ) ,
30+ resume : z . any ( ) . optional ( ) ,
31+ } ) ;
32+
33+ type ApplicationFormData = z . infer < typeof applicationSchema > ;
34+
35+ interface JobApplicationModalProps {
36+ isOpen : boolean ;
37+ onClose : ( ) => void ;
38+ job : JobListing | null ;
39+ }
40+
41+ export default function JobApplicationModal ( {
42+ isOpen,
43+ onClose,
44+ job,
45+ } : JobApplicationModalProps ) {
46+ const { toast } = useToast ( ) ;
47+ const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
48+ const [ resumeFile , setResumeFile ] = useState < File | null > ( null ) ;
49+
50+ const {
51+ register,
52+ handleSubmit,
53+ formState : { errors } ,
54+ reset,
55+ } = useForm < ApplicationFormData > ( {
56+ resolver : zodResolver ( applicationSchema ) ,
57+ } ) ;
58+
59+ const handleFileChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
60+ const file = e . target . files ?. [ 0 ] ;
61+ if ( file ) {
62+ if ( file . size > 5 * 1024 * 1024 ) {
63+ toast ( {
64+ title : "File too large" ,
65+ description : "Resume must be less than 5MB" ,
66+ variant : "destructive" ,
67+ } ) ;
68+ return ;
69+ }
70+ setResumeFile ( file ) ;
71+ }
72+ } ;
73+
74+ const onSubmit = async ( data : ApplicationFormData ) => {
75+ if ( ! job ) return ;
76+
77+ setIsSubmitting ( true ) ;
78+
79+ try {
80+ // Simulate API call
81+ await new Promise ( resolve => setTimeout ( resolve , 2000 ) ) ;
82+
83+ // Here you would typically send the application to your backend
84+ const applicationData = {
85+ ...data ,
86+ jobId : job . id ,
87+ jobTitle : job . title ,
88+ resume : resumeFile ?. name ,
89+ appliedAt : new Date ( ) . toISOString ( ) ,
90+ } ;
91+
92+ console . log ( "Application submitted:" , applicationData ) ;
93+
94+ toast ( {
95+ title : "Application Submitted!" ,
96+ description : `Your application for ${ job . title } has been received. We'll be in touch soon.` ,
97+ } ) ;
98+
99+ reset ( ) ;
100+ setResumeFile ( null ) ;
101+ onClose ( ) ;
102+ } catch ( error ) {
103+ toast ( {
104+ title : "Submission failed" ,
105+ description : "Please try again later." ,
106+ variant : "destructive" ,
107+ } ) ;
108+ } finally {
109+ setIsSubmitting ( false ) ;
110+ }
111+ } ;
112+
113+ if ( ! job ) return null ;
114+
115+ return (
116+ < Dialog open = { isOpen } onOpenChange = { onClose } >
117+ < DialogContent className = "max-h-[90vh] max-w-2xl overflow-y-auto bg-gray-900 border-gray-800" >
118+ < DialogHeader >
119+ < DialogTitle className = "text-2xl font-light text-white" >
120+ Apply for { job . title }
121+ </ DialogTitle >
122+ </ DialogHeader >
123+
124+ < form onSubmit = { handleSubmit ( onSubmit ) } className = "space-y-6 py-4" >
125+ { /* Personal Information */ }
126+ < div className = "space-y-4" >
127+ < h3 className = "text-lg font-medium text-white" > Personal Information</ h3 >
128+
129+ < div className = "grid grid-cols-2 gap-4" >
130+ < div className = "space-y-2" >
131+ < Label htmlFor = "firstName" className = "text-gray-300" >
132+ First Name *
133+ </ Label >
134+ < Input
135+ id = "firstName"
136+ { ...register ( "firstName" ) }
137+ className = "bg-gray-800 border-gray-700 text-white placeholder:text-gray-500"
138+ placeholder = "John"
139+ />
140+ { errors . firstName && (
141+ < p className = "text-sm text-red-400" > { errors . firstName . message } </ p >
142+ ) }
143+ </ div >
144+
145+ < div className = "space-y-2" >
146+ < Label htmlFor = "lastName" className = "text-gray-300" >
147+ Last Name *
148+ </ Label >
149+ < Input
150+ id = "lastName"
151+ { ...register ( "lastName" ) }
152+ className = "bg-gray-800 border-gray-700 text-white placeholder:text-gray-500"
153+ placeholder = "Doe"
154+ />
155+ { errors . lastName && (
156+ < p className = "text-sm text-red-400" > { errors . lastName . message } </ p >
157+ ) }
158+ </ div >
159+ </ div >
160+
161+ < div className = "space-y-2" >
162+ < Label htmlFor = "email" className = "text-gray-300" >
163+ Email *
164+ </ Label >
165+ < Input
166+ id = "email"
167+ type = "email"
168+ { ...register ( "email" ) }
169+ className = "bg-gray-800 border-gray-700 text-white placeholder:text-gray-500"
170+ placeholder = "john.doe@example.com"
171+ />
172+ { errors . email && (
173+ < p className = "text-sm text-red-400" > { errors . email . message } </ p >
174+ ) }
175+ </ div >
176+
177+ < div className = "space-y-2" >
178+ < Label htmlFor = "phone" className = "text-gray-300" >
179+ Phone Number *
180+ </ Label >
181+ < Input
182+ id = "phone"
183+ type = "tel"
184+ { ...register ( "phone" ) }
185+ className = "bg-gray-800 border-gray-700 text-white placeholder:text-gray-500"
186+ placeholder = "+63 912 345 6789"
187+ />
188+ { errors . phone && (
189+ < p className = "text-sm text-red-400" > { errors . phone . message } </ p >
190+ ) }
191+ </ div >
192+ </ div >
193+
194+ { /* Professional Links */ }
195+ < div className = "space-y-4" >
196+ < h3 className = "text-lg font-medium text-white" > Professional Links</ h3 >
197+
198+ < div className = "space-y-2" >
199+ < Label htmlFor = "linkedIn" className = "text-gray-300" >
200+ LinkedIn Profile
201+ </ Label >
202+ < Input
203+ id = "linkedIn"
204+ type = "url"
205+ { ...register ( "linkedIn" ) }
206+ className = "bg-gray-800 border-gray-700 text-white placeholder:text-gray-500"
207+ placeholder = "https://linkedin.com/in/johndoe"
208+ />
209+ { errors . linkedIn && (
210+ < p className = "text-sm text-red-400" > { errors . linkedIn . message } </ p >
211+ ) }
212+ </ div >
213+
214+ < div className = "space-y-2" >
215+ < Label htmlFor = "portfolio" className = "text-gray-300" >
216+ Portfolio/Website
217+ </ Label >
218+ < Input
219+ id = "portfolio"
220+ type = "url"
221+ { ...register ( "portfolio" ) }
222+ className = "bg-gray-800 border-gray-700 text-white placeholder:text-gray-500"
223+ placeholder = "https://johndoe.com"
224+ />
225+ { errors . portfolio && (
226+ < p className = "text-sm text-red-400" > { errors . portfolio . message } </ p >
227+ ) }
228+ </ div >
229+ </ div >
230+
231+ { /* Application Details */ }
232+ < div className = "space-y-4" >
233+ < h3 className = "text-lg font-medium text-white" > Application Details</ h3 >
234+
235+ < div className = "space-y-2" >
236+ < Label htmlFor = "experience" className = "text-gray-300" >
237+ Relevant Experience *
238+ </ Label >
239+ < Textarea
240+ id = "experience"
241+ { ...register ( "experience" ) }
242+ className = "bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 min-h-[100px]"
243+ placeholder = "Describe your relevant experience for this position..."
244+ />
245+ { errors . experience && (
246+ < p className = "text-sm text-red-400" > { errors . experience . message } </ p >
247+ ) }
248+ </ div >
249+
250+ < div className = "space-y-2" >
251+ < Label htmlFor = "coverLetter" className = "text-gray-300" >
252+ Cover Letter *
253+ </ Label >
254+ < Textarea
255+ id = "coverLetter"
256+ { ...register ( "coverLetter" ) }
257+ className = "bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 min-h-[150px]"
258+ placeholder = "Tell us why you're interested in this position..."
259+ />
260+ { errors . coverLetter && (
261+ < p className = "text-sm text-red-400" > { errors . coverLetter . message } </ p >
262+ ) }
263+ </ div >
264+
265+ < div className = "space-y-2" >
266+ < Label htmlFor = "resume" className = "text-gray-300" >
267+ Resume/CV (PDF, DOC, DOCX - Max 5MB)
268+ </ Label >
269+ < div className = "flex items-center gap-4" >
270+ < Input
271+ id = "resume"
272+ type = "file"
273+ accept = ".pdf,.doc,.docx"
274+ onChange = { handleFileChange }
275+ className = "hidden"
276+ />
277+ < Button
278+ type = "button"
279+ variant = "outline"
280+ size = "sm"
281+ onClick = { ( ) => document . getElementById ( "resume" ) ?. click ( ) }
282+ className = "border-gray-700 bg-gray-800 text-white hover:bg-gray-700"
283+ >
284+ < Upload className = "mr-2 h-4 w-4" />
285+ Choose File
286+ </ Button >
287+ { resumeFile && (
288+ < span className = "text-sm text-gray-400" >
289+ { resumeFile . name }
290+ </ span >
291+ ) }
292+ </ div >
293+ </ div >
294+ </ div >
295+
296+ { /* Submit Button */ }
297+ < div className = "flex gap-4 pt-4" >
298+ < Button
299+ type = "submit"
300+ variant = "purple"
301+ disabled = { isSubmitting }
302+ className = "flex-1"
303+ >
304+ { isSubmitting ? (
305+ < >
306+ < Loader2 className = "mr-2 h-4 w-4 animate-spin" />
307+ Submitting...
308+ </ >
309+ ) : (
310+ "Submit Application"
311+ ) }
312+ </ Button >
313+ < Button
314+ type = "button"
315+ variant = "outline"
316+ onClick = { onClose }
317+ disabled = { isSubmitting }
318+ className = "border-gray-700 bg-gray-800 text-white hover:bg-gray-700"
319+ >
320+ Cancel
321+ </ Button >
322+ </ div >
323+ </ form >
324+ </ DialogContent >
325+ </ Dialog >
326+ ) ;
327+ }
0 commit comments