619 lines
No EOL
24 KiB
TypeScript
619 lines
No EOL
24 KiB
TypeScript
"use client";
|
|
|
|
import { useState, use } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import Link from "next/link";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import {
|
|
ChevronRight,
|
|
ChevronLeft,
|
|
Upload,
|
|
CheckCircle2,
|
|
Briefcase,
|
|
FileText,
|
|
User,
|
|
MessageSquare,
|
|
Save,
|
|
ArrowLeft,
|
|
} from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardFooter,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
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 { Checkbox } from "@/components/ui/checkbox";
|
|
import { Progress } from "@/components/ui/progress";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Navbar } from "@/components/navbar";
|
|
import { Footer } from "@/components/footer";
|
|
import { useNotify } from "@/contexts/notification-context";
|
|
import { mockJobs } from "@/lib/mock-data";
|
|
|
|
// Definição Dos Passos
|
|
const steps = [
|
|
{ id: 1, title: "Dados Pessoais", icon: User },
|
|
{ id: 2, title: "Currículo e Documentos", icon: FileText },
|
|
{ id: 3, title: "Experiência", icon: Briefcase },
|
|
{ id: 4, title: "Perguntas Adicionais", icon: MessageSquare },
|
|
];
|
|
|
|
|
|
export const runtime = 'edge';
|
|
|
|
export default function JobApplicationPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{ id: string }>;
|
|
}) {
|
|
const { id } = use(params);
|
|
const router = useRouter();
|
|
const notify = useNotify();
|
|
const [currentStep, setCurrentStep] = useState(1);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
// Achar os detalhes da vaga
|
|
const job = mockJobs.find((j) => j.id === id) || mockJobs[0];
|
|
|
|
// Estado do formulário
|
|
const [formData, setFormData] = useState({
|
|
// Etapa 1
|
|
fullName: "",
|
|
email: "",
|
|
phone: "",
|
|
linkedin: "",
|
|
privacyAccepted: false,
|
|
// Etapa 2
|
|
resume: null as File | null,
|
|
coverLetter: "",
|
|
portfolioUrl: "",
|
|
// Etapa 3
|
|
salaryExpectation: "",
|
|
hasExperience: "",
|
|
// Etapa 4
|
|
whyUs: "",
|
|
availability: [] as string[],
|
|
});
|
|
|
|
const handleInputChange = (field: string, value: any) => {
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
const validateStep = (step: number) => {
|
|
switch (step) {
|
|
case 1:
|
|
if (!formData.fullName || !formData.email || !formData.phone) {
|
|
notify.error(
|
|
"Campos obrigatórios",
|
|
"Por favor, preencha todos os campos obrigatórios."
|
|
);
|
|
return false;
|
|
}
|
|
if (!formData.email.includes("@")) {
|
|
notify.error(
|
|
"E-mail inválido",
|
|
"Por favor, insira um e-mail válido."
|
|
);
|
|
return false;
|
|
}
|
|
if (!formData.privacyAccepted) {
|
|
notify.error(
|
|
"Termos de Privacidade",
|
|
"Você precisa aceitar a política de privacidade para continuar."
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
case 2:
|
|
return true;
|
|
case 3:
|
|
if (!formData.salaryExpectation || !formData.hasExperience) {
|
|
notify.error(
|
|
"Campos obrigatórios",
|
|
"Por favor, responda todas as perguntas."
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
case 4:
|
|
if (!formData.whyUs || formData.availability.length === 0) {
|
|
notify.error(
|
|
"Campos obrigatórios",
|
|
"Por favor, preencha o motivo e selecione pelo menos uma disponibilidade."
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
default:
|
|
return true;
|
|
}
|
|
};
|
|
|
|
const handleNext = () => {
|
|
if (validateStep(currentStep)) {
|
|
if (currentStep < steps.length) {
|
|
setCurrentStep((prev) => prev + 1);
|
|
window.scrollTo(0, 0);
|
|
} else {
|
|
handleSubmit();
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleBack = () => {
|
|
if (currentStep > 1) {
|
|
setCurrentStep((prev) => prev - 1);
|
|
window.scrollTo(0, 0);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
setIsSubmitting(true);
|
|
// Simular um chamado de API
|
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
|
|
notify.success(
|
|
"Candidatura enviada com sucesso!",
|
|
`Boa sorte! Sua candidatura para ${job.title} foi recebida.`
|
|
);
|
|
|
|
router.push("/dashboard/candidato/candidaturas");
|
|
};
|
|
|
|
const handleSaveDraft = () => {
|
|
notify.info(
|
|
"Rascunho salvo",
|
|
"Você pode continuar sua candidatura mais tarde."
|
|
);
|
|
};
|
|
|
|
const progress = (currentStep / steps.length) * 100;
|
|
|
|
if (!job) return null;
|
|
|
|
return (
|
|
<div className="min-h-screen flex flex-col bg-muted/30">
|
|
<Navbar />
|
|
|
|
<main className="flex-1 py-8">
|
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl">
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<Link
|
|
href={`/jobs/${id}`}
|
|
className="inline-flex items-center text-sm text-muted-foreground hover:text-primary mb-4 transition-colors"
|
|
>
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
Voltar para detalhes da vaga
|
|
</Link>
|
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-2xl md:text-3xl font-bold text-foreground">
|
|
Candidatura: {job.title}
|
|
</h1>
|
|
<p className="text-muted-foreground mt-1">
|
|
{job.company} • {job.location}
|
|
</p>
|
|
</div>
|
|
<div className="text-sm font-medium bg-primary/10 text-primary px-3 py-1 rounded-full self-start md:self-center">
|
|
Tempo estimado: 5 min
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progresso das etapas */}
|
|
<div className="mb-8">
|
|
<div className="flex justify-between mb-2">
|
|
<span className="text-sm font-medium text-muted-foreground">
|
|
Etapa {currentStep} de {steps.length}:{" "}
|
|
<span className="text-foreground">
|
|
{steps[currentStep - 1].title}
|
|
</span>
|
|
</span>
|
|
<span className="text-sm font-medium text-primary">
|
|
{Math.round(progress)}%
|
|
</span>
|
|
</div>
|
|
<Progress value={progress} className="h-2" />
|
|
|
|
{/* Indicador de etapas (DESKTOP) */}
|
|
<div className="hidden md:flex justify-between mt-4 px-2">
|
|
{steps.map((step) => {
|
|
const Icon = step.icon;
|
|
const isActive = step.id === currentStep;
|
|
const isCompleted = step.id < currentStep;
|
|
|
|
return (
|
|
<div
|
|
key={step.id}
|
|
className={`flex flex-col items-center gap-2 ${isActive
|
|
? "text-primary"
|
|
: isCompleted
|
|
? "text-primary/60"
|
|
: "text-muted-foreground"
|
|
}`}
|
|
>
|
|
<div
|
|
className={`
|
|
w-8 h-8 rounded-full flex items-center justify-center border-2 transition-colors
|
|
${isActive
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
: isCompleted
|
|
? "border-primary bg-primary/10 text-primary"
|
|
: "border-muted-foreground/30 bg-background"
|
|
}
|
|
`}
|
|
>
|
|
{isCompleted ? (
|
|
<CheckCircle2 className="h-5 w-5" />
|
|
) : (
|
|
<Icon className="h-4 w-4" />
|
|
)}
|
|
</div>
|
|
<span className="text-xs font-medium">{step.title}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Conteúdo do formulário */}
|
|
<div className="grid gap-6">
|
|
<AnimatePresence mode="wait">
|
|
<motion.div
|
|
key={currentStep}
|
|
initial={{ opacity: 0, x: 20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
exit={{ opacity: 0, x: -20 }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
<Card className="border-t-4 border-t-primary">
|
|
<CardHeader>
|
|
<CardTitle>{steps[currentStep - 1].title}</CardTitle>
|
|
<CardDescription>
|
|
Preencha as informações abaixo para continuar.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent className="space-y-6">
|
|
{/* Etapa 1: Dados Pessoais */}
|
|
{currentStep === 1 && (
|
|
<div className="space-y-4">
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="fullName">Nome Completo *</Label>
|
|
<Input
|
|
id="fullName"
|
|
placeholder="Seu nome completo"
|
|
value={formData.fullName}
|
|
onChange={(e) =>
|
|
handleInputChange("fullName", e.target.value)
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email">E-mail *</Label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
placeholder="seu@email.com"
|
|
value={formData.email}
|
|
onChange={(e) =>
|
|
handleInputChange("email", e.target.value)
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="phone">Telefone / WhatsApp *</Label>
|
|
<Input
|
|
id="phone"
|
|
placeholder="(00) 00000-0000"
|
|
value={formData.phone}
|
|
onChange={(e) =>
|
|
handleInputChange("phone", e.target.value)
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="linkedin">LinkedIn (URL)</Label>
|
|
<Input
|
|
id="linkedin"
|
|
placeholder="linkedin.com/in/seu-perfil"
|
|
value={formData.linkedin}
|
|
onChange={(e) =>
|
|
handleInputChange("linkedin", e.target.value)
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2 pt-4">
|
|
<Checkbox
|
|
id="privacy"
|
|
checked={formData.privacyAccepted}
|
|
onCheckedChange={(checked) =>
|
|
handleInputChange("privacyAccepted", checked)
|
|
}
|
|
/>
|
|
<Label
|
|
htmlFor="privacy"
|
|
className="text-sm font-normal text-muted-foreground"
|
|
>
|
|
Li e concordo com a{" "}
|
|
<a href="#" className="text-primary underline">
|
|
Política de Privacidade
|
|
</a>{" "}
|
|
e autorizo o tratamento dos meus dados para fins de
|
|
recrutamento.
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Etapa 2: Dccumentos */}
|
|
{currentStep === 2 && (
|
|
<div className="space-y-6">
|
|
<div className="space-y-3">
|
|
<Label>Currículo (CV) *</Label>
|
|
<div className="border-2 border-dashed border-muted-foreground/25 rounded-lg p-8 text-center hover:bg-muted/50 transition-colors cursor-pointer">
|
|
<div className="flex flex-col items-center gap-2">
|
|
<div className="p-3 bg-primary/10 rounded-full text-primary">
|
|
<Upload className="h-6 w-6" />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="text-sm font-medium">
|
|
Clique para fazer upload ou arraste o arquivo
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
PDF, DOCX ou TXT (Máx. 5MB)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="portfolio">
|
|
Portfólio / Site Pessoal (Opcional)
|
|
</Label>
|
|
<Input
|
|
id="portfolio"
|
|
placeholder="https://..."
|
|
value={formData.portfolioUrl}
|
|
onChange={(e) =>
|
|
handleInputChange("portfolioUrl", e.target.value)
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="coverLetter">
|
|
Carta de Apresentação (Opcional)
|
|
</Label>
|
|
<Textarea
|
|
id="coverLetter"
|
|
placeholder="Escreva uma breve apresentação sobre você..."
|
|
className="min-h-[150px]"
|
|
value={formData.coverLetter}
|
|
onChange={(e) =>
|
|
handleInputChange("coverLetter", e.target.value)
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Etapa 3: Experiências */}
|
|
{currentStep === 3 && (
|
|
<div className="space-y-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="salary">Pretensão Salarial *</Label>
|
|
<Select
|
|
value={formData.salaryExpectation}
|
|
onValueChange={(val) =>
|
|
handleInputChange("salaryExpectation", val)
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Selecione uma faixa" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="ate-3k">
|
|
Até R$ 3.000
|
|
</SelectItem>
|
|
<SelectItem value="3k-5k">
|
|
R$ 3.000 - R$ 5.000
|
|
</SelectItem>
|
|
<SelectItem value="5k-8k">
|
|
R$ 5.000 - R$ 8.000
|
|
</SelectItem>
|
|
<SelectItem value="8k-12k">
|
|
R$ 8.000 - R$ 12.000
|
|
</SelectItem>
|
|
<SelectItem value="12k-plus">
|
|
Acima de R$ 12.000
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label>
|
|
Você possui a experiência mínima requisitada para a
|
|
vaga? *
|
|
</Label>
|
|
<div className="flex gap-4">
|
|
<div className="flex items-center space-x-2 border p-3 rounded-md flex-1 hover:bg-muted/50 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name="experience"
|
|
id="exp-yes"
|
|
className="accent-primary h-4 w-4"
|
|
checked={formData.hasExperience === "yes"}
|
|
onChange={() =>
|
|
handleInputChange("hasExperience", "yes")
|
|
}
|
|
/>
|
|
<Label
|
|
htmlFor="exp-yes"
|
|
className="cursor-pointer flex-1"
|
|
>
|
|
Sim, possuo
|
|
</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-2 border p-3 rounded-md flex-1 hover:bg-muted/50 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name="experience"
|
|
id="exp-no"
|
|
className="accent-primary h-4 w-4"
|
|
checked={formData.hasExperience === "no"}
|
|
onChange={() =>
|
|
handleInputChange("hasExperience", "no")
|
|
}
|
|
/>
|
|
<Label
|
|
htmlFor="exp-no"
|
|
className="cursor-pointer flex-1"
|
|
>
|
|
Não possuo
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Etapa 4: Adicional */}
|
|
{currentStep === 4 && (
|
|
<div className="space-y-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="whyUs">
|
|
Por que você quer trabalhar na {job.company}? *
|
|
</Label>
|
|
<Textarea
|
|
id="whyUs"
|
|
placeholder="Conte-nos o que te atrai na nossa empresa e nesta vaga..."
|
|
className="min-h-[150px]"
|
|
maxLength={1000}
|
|
value={formData.whyUs}
|
|
onChange={(e) =>
|
|
handleInputChange("whyUs", e.target.value)
|
|
}
|
|
/>
|
|
<div className="text-xs text-right text-muted-foreground">
|
|
{formData.whyUs.length}/1000 caracteres
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label>Disponibilidade *</Label>
|
|
<div className="grid gap-2">
|
|
{[
|
|
"Trabalho Presencial",
|
|
"Trabalho Remoto",
|
|
"Viagens",
|
|
"Início Imediato",
|
|
].map((item) => (
|
|
<div
|
|
key={item}
|
|
className="flex items-center space-x-2"
|
|
>
|
|
<Checkbox
|
|
id={`avail-${item}`}
|
|
checked={formData.availability.includes(item)}
|
|
onCheckedChange={(checked) => {
|
|
if (checked) {
|
|
handleInputChange("availability", [
|
|
...formData.availability,
|
|
item,
|
|
]);
|
|
} else {
|
|
handleInputChange(
|
|
"availability",
|
|
formData.availability.filter(
|
|
(i) => i !== item
|
|
)
|
|
);
|
|
}
|
|
}}
|
|
/>
|
|
<Label htmlFor={`avail-${item}`}>{item}</Label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
|
|
<CardFooter className="flex justify-between border-t pt-6">
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleBack}
|
|
disabled={currentStep === 1 || isSubmitting}
|
|
>
|
|
<ChevronLeft className="mr-2 h-4 w-4" />
|
|
Voltar
|
|
</Button>
|
|
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
onClick={handleSaveDraft}
|
|
disabled={isSubmitting}
|
|
className="hidden sm:flex"
|
|
>
|
|
<Save className="mr-2 h-4 w-4" />
|
|
Salvar Rascunho
|
|
</Button>
|
|
|
|
<Button
|
|
onClick={handleNext}
|
|
disabled={isSubmitting}
|
|
className="min-w-[120px]"
|
|
>
|
|
{isSubmitting ? (
|
|
"Enviando..."
|
|
) : currentStep === steps.length ? (
|
|
<>
|
|
Enviar Candidatura{" "}
|
|
<CheckCircle2 className="ml-2 h-4 w-4" />
|
|
</>
|
|
) : (
|
|
<>
|
|
Próxima Etapa{" "}
|
|
<ChevronRight className="ml-2 h-4 w-4" />
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</CardFooter>
|
|
</Card>
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<Footer />
|
|
</div>
|
|
);
|
|
} |