Skip to content

Commit 48b77e3

Browse files
Zeff01claude
andcommitted
feat: implement complete job management and hiring system
- Added job listings page at /careers with pagination - Created admin job management dashboard at /home/hire - Implemented job application form without sign-in requirement - Added application tracking with status management - Created job listing CRUD operations (create, edit, delete) - Added breadcrumb navigation for better UX - Fixed icon color and alignment issues - Implemented View Details modal for applications - Added resume download functionality - Fixed dark mode text colors in status dropdowns - Added "Hire" link to sidebar under Management section 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ddc6775 commit 48b77e3

12 files changed

Lines changed: 2245 additions & 2 deletions

File tree

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
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

Comments
 (0)