From 80d38ee6154347d30f79370f8fbc584205116b51 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Tue, 23 Dec 2025 22:40:19 -0300 Subject: [PATCH] feat(frontend): add multi-step job posting wizard with API integration --- frontend/src/app/dashboard/jobs/new/page.tsx | 401 +++++++++++++++++++ frontend/src/app/dashboard/jobs/page.tsx | 268 ++++--------- frontend/src/lib/api.ts | 20 + 3 files changed, 495 insertions(+), 194 deletions(-) create mode 100644 frontend/src/app/dashboard/jobs/new/page.tsx diff --git a/frontend/src/app/dashboard/jobs/new/page.tsx b/frontend/src/app/dashboard/jobs/new/page.tsx new file mode 100644 index 0000000..35af57e --- /dev/null +++ b/frontend/src/app/dashboard/jobs/new/page.tsx @@ -0,0 +1,401 @@ +"use client" + +import { useState, useEffect } from "react" +import { useRouter } from "next/navigation" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { ArrowLeft, ArrowRight, Check, Loader2, Building2, DollarSign, FileText, Eye } from "lucide-react" +import { jobsApi, adminCompaniesApi, type CreateJobPayload, type AdminCompany } from "@/lib/api" +import { toast } from "sonner" +import { cn } from "@/lib/utils" + +const STEPS = [ + { id: 1, title: "Job Details", icon: FileText }, + { id: 2, title: "Salary & Type", icon: DollarSign }, + { id: 3, title: "Company", icon: Building2 }, + { id: 4, title: "Review", icon: Eye }, +] + +export default function NewJobPage() { + const router = useRouter() + const [currentStep, setCurrentStep] = useState(1) + const [isSubmitting, setIsSubmitting] = useState(false) + const [companies, setCompanies] = useState([]) + const [loadingCompanies, setLoadingCompanies] = useState(true) + + const [formData, setFormData] = useState({ + title: "", + description: "", + companyId: "", + location: "", + employmentType: "", + salaryMin: "", + salaryMax: "", + salaryType: "", + workingHours: "", + }) + + useEffect(() => { + const loadCompanies = async () => { + try { + setLoadingCompanies(true) + const data = await adminCompaniesApi.list(undefined, 1, 100) + setCompanies(data.data ?? []) + } catch (error) { + console.error("Failed to load companies:", error) + toast.error("Failed to load companies") + } finally { + setLoadingCompanies(false) + } + } + loadCompanies() + }, []) + + const updateField = (field: string, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })) + } + + const canProceed = () => { + switch (currentStep) { + case 1: + return formData.title.length >= 5 && formData.description.length >= 20 + case 2: + return true // Salary is optional + case 3: + return formData.companyId !== "" + default: + return true + } + } + + const handleSubmit = async () => { + if (!formData.companyId || !formData.title || !formData.description) { + toast.error("Please fill in all required fields") + return + } + + setIsSubmitting(true) + try { + const payload: CreateJobPayload = { + companyId: formData.companyId, + title: formData.title, + description: formData.description, + location: formData.location || undefined, + employmentType: formData.employmentType as CreateJobPayload['employmentType'] || undefined, + salaryMin: formData.salaryMin ? parseFloat(formData.salaryMin) : undefined, + salaryMax: formData.salaryMax ? parseFloat(formData.salaryMax) : undefined, + salaryType: formData.salaryType as CreateJobPayload['salaryType'] || undefined, + workingHours: formData.workingHours || undefined, + status: "published", + } + + console.log("[DEBUG] Submitting job:", payload) + await jobsApi.create(payload) + toast.success("Job posted successfully!") + router.push("/dashboard/jobs") + } catch (error) { + console.error("Failed to create job:", error) + toast.error("Failed to post job. Please try again.") + } finally { + setIsSubmitting(false) + } + } + + const nextStep = () => { + if (currentStep < 4 && canProceed()) { + setCurrentStep(prev => prev + 1) + } + } + + const prevStep = () => { + if (currentStep > 1) { + setCurrentStep(prev => prev - 1) + } + } + + return ( +
+ {/* Header */} +
+ +
+

Post a new job

+

Fill in the details for your job listing

+
+
+ + {/* Progress Steps */} +
+ {STEPS.map((step, index) => { + const Icon = step.icon + const isActive = currentStep === step.id + const isCompleted = currentStep > step.id + + return ( +
+
+
+ {isCompleted ? : } +
+ + {step.title} + +
+ {index < STEPS.length - 1 && ( +
step.id ? "bg-primary" : "bg-muted-foreground/30" + )} + /> + )} +
+ ) + })} +
+ + {/* Step Content */} + + + {STEPS[currentStep - 1].title} + + {currentStep === 1 && "Enter the basic information about this job"} + {currentStep === 2 && "Set the compensation details"} + {currentStep === 3 && "Select the company posting this job"} + {currentStep === 4 && "Review your job listing before publishing"} + + + + {/* Step 1: Job Details */} + {currentStep === 1 && ( + <> +
+ + updateField("title", e.target.value)} + /> + {formData.title.length > 0 && formData.title.length < 5 && ( +

Title must be at least 5 characters

+ )} +
+
+ +