refactor(routes): redirect /register/job to /jobs/new
Consolidates the two public job posting routes into the single canonical flow at /jobs/new. The /publicar-vaga route already redirected there. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3a26af3df5
commit
878a987749
1 changed files with 3 additions and 470 deletions
|
|
@ -1,472 +1,5 @@
|
||||||
"use client";
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
export default function RegisterJobRedirectPage() {
|
||||||
import { useRouter } from "next/navigation";
|
redirect("/jobs/new")
|
||||||
import Link from "next/link";
|
|
||||||
import Image from "next/image";
|
|
||||||
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 {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
|
||||||
Briefcase,
|
|
||||||
Building2,
|
|
||||||
ArrowLeft,
|
|
||||||
MapPin,
|
|
||||||
DollarSign,
|
|
||||||
Clock,
|
|
||||||
Loader2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { useTranslation } from "@/lib/i18n";
|
|
||||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
|
||||||
import { jobsApi, adminCompaniesApi, type CreateJobPayload, type AdminCompany } from "@/lib/api";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export default function PublicPostJobPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [companies, setCompanies] = useState<AdminCompany[]>([]);
|
|
||||||
const [loadingCompanies, setLoadingCompanies] = useState(true);
|
|
||||||
const [currentStep, setCurrentStep] = useState(1);
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
title: "",
|
|
||||||
description: "",
|
|
||||||
location: "",
|
|
||||||
salaryMin: "",
|
|
||||||
salaryMax: "",
|
|
||||||
salaryType: "monthly",
|
|
||||||
currency: "BRL",
|
|
||||||
employmentType: "",
|
|
||||||
workingHours: "",
|
|
||||||
companyId: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
} finally {
|
|
||||||
setLoadingCompanies(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadCompanies();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateField = (field: string, value: string) => {
|
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const canSubmit = () => {
|
|
||||||
return (
|
|
||||||
formData.title.length >= 5 &&
|
|
||||||
formData.description.length >= 20 &&
|
|
||||||
formData.companyId !== ""
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!canSubmit()) {
|
|
||||||
toast.error("Please fill in all required fields");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(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,
|
|
||||||
currency: formData.currency as CreateJobPayload["currency"] || undefined,
|
|
||||||
workingHours: formData.workingHours || undefined,
|
|
||||||
status: "draft",
|
|
||||||
};
|
|
||||||
|
|
||||||
await jobsApi.create(payload);
|
|
||||||
toast.success("Job posted successfully! It will be reviewed soon.");
|
|
||||||
router.push("/jobs");
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Failed to post job:", error);
|
|
||||||
toast.error(error.message || "Failed to post job. Please try again.");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextStep = () => {
|
|
||||||
if (currentStep < 2) setCurrentStep(currentStep + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const prevStep = () => {
|
|
||||||
if (currentStep > 1) setCurrentStep(currentStep - 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const stepVariants = {
|
|
||||||
hidden: { opacity: 0, x: 20 },
|
|
||||||
visible: { opacity: 1, x: 0 },
|
|
||||||
exit: { opacity: 0, x: -20 },
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-background to-muted/20 flex">
|
|
||||||
{/* Left Panel - Information */}
|
|
||||||
<div className="hidden lg:flex lg:flex-1 bg-gradient-to-br from-primary to-primary/80 p-8 flex-col justify-center items-center text-primary-foreground relative">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="max-w-md text-center"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center gap-3 mb-8">
|
|
||||||
<Image
|
|
||||||
src="/logohorse1.png"
|
|
||||||
alt="GoHorse Jobs"
|
|
||||||
width={80}
|
|
||||||
height={80}
|
|
||||||
className="rounded-lg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 className="text-4xl font-bold mb-4">Post a Job</h1>
|
|
||||||
|
|
||||||
<p className="text-lg opacity-90 leading-relaxed mb-6">
|
|
||||||
Reach thousands of qualified candidates looking for their next opportunity.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="space-y-4 text-left">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
|
||||||
<span>Free job posting</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
|
||||||
<span>Access to verified candidates</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
|
||||||
<span>Easy application management</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
|
||||||
<span>Professional dashboard</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Panel - Form */}
|
|
||||||
<div className="flex-1 p-8 flex flex-col justify-center relative">
|
|
||||||
<div className="absolute top-4 right-4">
|
|
||||||
<LanguageSwitcher />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full max-w-md mx-auto">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<Link
|
|
||||||
href="/jobs"
|
|
||||||
className="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground mb-4 transition-colors"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4" />
|
|
||||||
Back to jobs
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-2">
|
|
||||||
Post a new job
|
|
||||||
</h2>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Fill in the details below to create your job listing
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress Indicator */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
Step {currentStep} of 2
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{currentStep === 1 && "Job Details"}
|
|
||||||
{currentStep === 2 && "Salary & Company"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-muted rounded-full h-2">
|
|
||||||
<div
|
|
||||||
className="bg-primary h-2 rounded-full transition-all duration-300"
|
|
||||||
style={{ width: `${(currentStep / 2) * 100}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={onSubmit} className="space-y-6">
|
|
||||||
{/* Step 1: Job Details */}
|
|
||||||
{currentStep === 1 && (
|
|
||||||
<motion.div
|
|
||||||
key="step1"
|
|
||||||
variants={stepVariants}
|
|
||||||
initial="hidden"
|
|
||||||
animate="visible"
|
|
||||||
exit="exit"
|
|
||||||
className="space-y-4"
|
|
||||||
>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="title">
|
|
||||||
<Briefcase className="inline h-4 w-4 mr-1" />
|
|
||||||
Job Title *
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="title"
|
|
||||||
placeholder="e.g. Senior Software Engineer"
|
|
||||||
value={formData.title}
|
|
||||||
onChange={(e) => updateField("title", e.target.value)}
|
|
||||||
/>
|
|
||||||
{formData.title.length > 0 && formData.title.length < 5 && (
|
|
||||||
<span className="text-sm text-destructive">
|
|
||||||
Title must be at least 5 characters
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="description">Description *</Label>
|
|
||||||
<Textarea
|
|
||||||
id="description"
|
|
||||||
placeholder="Describe the role, responsibilities, and requirements..."
|
|
||||||
rows={5}
|
|
||||||
value={formData.description}
|
|
||||||
onChange={(e) => updateField("description", e.target.value)}
|
|
||||||
/>
|
|
||||||
{formData.description.length > 0 &&
|
|
||||||
formData.description.length < 20 && (
|
|
||||||
<span className="text-sm text-destructive">
|
|
||||||
Description must be at least 20 characters
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="location">
|
|
||||||
<MapPin className="inline h-4 w-4 mr-1" />
|
|
||||||
Location
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="location"
|
|
||||||
placeholder="e.g. São Paulo, SP or Remote"
|
|
||||||
value={formData.location}
|
|
||||||
onChange={(e) => updateField("location", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>
|
|
||||||
<Clock className="inline h-4 w-4 mr-1" />
|
|
||||||
Contract Type
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.employmentType}
|
|
||||||
onValueChange={(v) => updateField("employmentType", v)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select contract type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="permanent">Permanent</SelectItem>
|
|
||||||
<SelectItem value="full-time">Full-time</SelectItem>
|
|
||||||
<SelectItem value="part-time">Part-time</SelectItem>
|
|
||||||
<SelectItem value="contract">Contract</SelectItem>
|
|
||||||
<SelectItem value="temporary">Temporary</SelectItem>
|
|
||||||
<SelectItem value="training">Training</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button type="button" onClick={nextStep} className="w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Step 2: Salary & Company */}
|
|
||||||
{currentStep === 2 && (
|
|
||||||
<motion.div
|
|
||||||
key="step2"
|
|
||||||
variants={stepVariants}
|
|
||||||
initial="hidden"
|
|
||||||
animate="visible"
|
|
||||||
exit="exit"
|
|
||||||
className="space-y-4"
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>
|
|
||||||
<DollarSign className="inline h-4 w-4 mr-1" />
|
|
||||||
Min Salary
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
placeholder="e.g. 5000"
|
|
||||||
value={formData.salaryMin}
|
|
||||||
onChange={(e) => updateField("salaryMin", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Max Salary</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
placeholder="e.g. 10000"
|
|
||||||
value={formData.salaryMax}
|
|
||||||
onChange={(e) => updateField("salaryMax", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Currency</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.currency}
|
|
||||||
onValueChange={(v) => updateField("currency", v)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="BRL">R$ (BRL)</SelectItem>
|
|
||||||
<SelectItem value="USD">$ (USD)</SelectItem>
|
|
||||||
<SelectItem value="EUR">€ (EUR)</SelectItem>
|
|
||||||
<SelectItem value="JPY">¥ (JPY)</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Period</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.salaryType}
|
|
||||||
onValueChange={(v) => updateField("salaryType", v)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="hourly">Per hour</SelectItem>
|
|
||||||
<SelectItem value="daily">Per day</SelectItem>
|
|
||||||
<SelectItem value="weekly">Per week</SelectItem>
|
|
||||||
<SelectItem value="monthly">Per month</SelectItem>
|
|
||||||
<SelectItem value="yearly">Per year</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>
|
|
||||||
<Clock className="inline h-4 w-4 mr-1" />
|
|
||||||
Working Hours
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="e.g. 9:00 - 18:00, Mon-Fri"
|
|
||||||
value={formData.workingHours}
|
|
||||||
onChange={(e) => updateField("workingHours", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>
|
|
||||||
<Building2 className="inline h-4 w-4 mr-1" />
|
|
||||||
Company *
|
|
||||||
</Label>
|
|
||||||
{loadingCompanies ? (
|
|
||||||
<div className="flex items-center gap-2 text-muted-foreground py-2">
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
Loading companies...
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Select
|
|
||||||
value={formData.companyId}
|
|
||||||
onValueChange={(v) => updateField("companyId", v)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a company" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{companies.length === 0 ? (
|
|
||||||
<SelectItem value="__none" disabled>
|
|
||||||
No companies available
|
|
||||||
</SelectItem>
|
|
||||||
) : (
|
|
||||||
companies.map((company) => (
|
|
||||||
<SelectItem key={company.id} value={company.id}>
|
|
||||||
{company.name}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={prevStep}
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading || !canSubmit()}
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
||||||
Posting...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Briefcase className="h-4 w-4 mr-2" />
|
|
||||||
Post Job
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-6 text-center">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Want to manage your jobs?{" "}
|
|
||||||
<Link
|
|
||||||
href="/login"
|
|
||||||
className="text-primary hover:underline font-medium"
|
|
||||||
>
|
|
||||||
Sign in
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue