feat: connect Apply Now to applications API

Frontend changes:
- Add applicationsApi with create() and getByJob() in api.ts
- Update apply/page.tsx to fetch job from API and submit to backend
- Fix job detail page requirements null check
- Use ApiJob type instead of mock Job type
- Replace job.company with job.companyName throughout

Note: Backend has type mismatch issues that need fixing:
- jobs endpoint: varchar vs integer comparison
- applications: null id constraint
This commit is contained in:
Tiago Yamamoto 2025-12-23 08:29:15 -03:00
parent ce0531fefc
commit 592af3216e
3 changed files with 131 additions and 28 deletions

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, use } from "react"; import { useState, use, useEffect } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
@ -15,6 +15,7 @@ import {
MessageSquare, MessageSquare,
Save, Save,
ArrowLeft, ArrowLeft,
Loader2,
} from "lucide-react"; } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -42,7 +43,7 @@ import { Separator } from "@/components/ui/separator";
import { Navbar } from "@/components/navbar"; import { Navbar } from "@/components/navbar";
import { Footer } from "@/components/footer"; import { Footer } from "@/components/footer";
import { useNotify } from "@/contexts/notification-context"; import { useNotify } from "@/contexts/notification-context";
import { mockJobs } from "@/lib/mock-data"; import { jobsApi, applicationsApi, type ApiJob } from "@/lib/api";
// Step definitions // Step definitions
const steps = [ const steps = [
@ -65,9 +66,8 @@ export default function JobApplicationPage({
const notify = useNotify(); const notify = useNotify();
const [currentStep, setCurrentStep] = useState(1); const [currentStep, setCurrentStep] = useState(1);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [job, setJob] = useState<ApiJob | null>(null);
// Find job details const [loading, setLoading] = useState(true);
const job = mockJobs.find((j) => j.id === id) || mockJobs[0];
// Form state // Form state
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@ -161,17 +161,56 @@ export default function JobApplicationPage({
} }
}; };
useEffect(() => {
async function fetchJob() {
try {
setLoading(true);
const response = await jobsApi.getById(id);
if (response) {
setJob(response);
}
} catch (err) {
console.error("Error fetching job:", err);
notify.error("Error", "Failed to load job details");
} finally {
setLoading(false);
}
}
fetchJob();
}, [id, notify]);
const handleSubmit = async () => { const handleSubmit = async () => {
setIsSubmitting(true); setIsSubmitting(true);
// Simulate API call try {
await new Promise((resolve) => setTimeout(resolve, 2000)); await applicationsApi.create({
jobId: id,
name: formData.fullName,
email: formData.email,
phone: formData.phone,
linkedin: formData.linkedin,
coverLetter: formData.coverLetter,
portfolioUrl: formData.portfolioUrl,
salaryExpectation: formData.salaryExpectation,
hasExperience: formData.hasExperience,
whyUs: formData.whyUs,
availability: formData.availability,
});
notify.success( notify.success(
"Application submitted!", "Application submitted!",
`Good luck! Your application for ${job.title} has been received.` `Good luck! Your application for ${job?.title || 'this position'} has been received.`
); );
router.push("/dashboard/my-applications"); router.push("/dashboard/my-applications");
} catch (error: any) {
console.error("Submit error:", error);
notify.error(
"Error submitting",
error.message || "Please try again later."
);
} finally {
setIsSubmitting(false);
}
}; };
const handleSaveDraft = () => { const handleSaveDraft = () => {
@ -206,7 +245,7 @@ export default function JobApplicationPage({
Application: {job.title} Application: {job.title}
</h1> </h1>
<p className="text-muted-foreground mt-1"> <p className="text-muted-foreground mt-1">
{job.company} {job.location} {job.companyName || 'Company'} {job.location || 'Remote'}
</p> </p>
</div> </div>
<div className="text-sm font-medium bg-primary/10 text-primary px-3 py-1 rounded-full self-start md:self-center"> <div className="text-sm font-medium bg-primary/10 text-primary px-3 py-1 rounded-full self-start md:self-center">
@ -241,10 +280,10 @@ export default function JobApplicationPage({
<div <div
key={step.id} key={step.id}
className={`flex flex-col items-center gap-2 ${isActive className={`flex flex-col items-center gap-2 ${isActive
? "text-primary" ? "text-primary"
: isCompleted : isCompleted
? "text-primary/60" ? "text-primary/60"
: "text-muted-foreground" : "text-muted-foreground"
}`} }`}
> >
<div <div
@ -505,7 +544,7 @@ export default function JobApplicationPage({
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="whyUs"> <Label htmlFor="whyUs">
Why do you want to work at {job.company}? * Why do you want to work at {job.companyName || 'this company'}? *
</Label> </Label>
<Textarea <Textarea
id="whyUs" id="whyUs"

View file

@ -15,7 +15,7 @@ import {
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { jobsApi, transformJob, type Job } from "@/lib/api"; import { jobsApi, type ApiJob } from "@/lib/api";
import { import {
MapPin, MapPin,
Briefcase, Briefcase,
@ -48,7 +48,7 @@ export default function JobDetailPage({
const router = useRouter(); const router = useRouter();
const [isFavorited, setIsFavorited] = useState(false); const [isFavorited, setIsFavorited] = useState(false);
const [isBookmarked, setIsBookmarked] = useState(false); const [isBookmarked, setIsBookmarked] = useState(false);
const [job, setJob] = useState<Job | null>(null); const [job, setJob] = useState<ApiJob | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -57,8 +57,8 @@ export default function JobDetailPage({
try { try {
setLoading(true); setLoading(true);
const response = await jobsApi.getById(id); const response = await jobsApi.getById(id);
if (response.data) { if (response) {
setJob(transformJob(response.data)); setJob(response);
} else { } else {
setError("Job not found"); setError("Job not found");
} }
@ -362,12 +362,16 @@ export default function JobDetailPage({
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid gap-3"> <div className="grid gap-3">
{job.requirements.map((req, index) => ( {job.requirements && Array.isArray(job.requirements) ? (
<div key={index} className="flex items-start gap-3"> job.requirements.map((req, index) => (
<CheckCircle2 className="h-5 w-5 text-primary mt-0.5 shrink-0" /> <div key={index} className="flex items-start gap-3">
<span className="text-muted-foreground">{req}</span> <CheckCircle2 className="h-5 w-5 text-primary mt-0.5 shrink-0" />
</div> <span className="text-muted-foreground">{String(req)}</span>
))} </div>
))
) : (
<p className="text-muted-foreground">No specific requirements listed.</p>
)}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View file

@ -197,6 +197,66 @@ export const jobsApi = {
}, },
}; };
// Applications API
export interface CreateApplicationRequest {
jobId: string;
userId?: string;
name: string;
email: string;
phone: string;
linkedin?: string;
coverLetter?: string;
portfolioUrl?: string;
resumeUrl?: string;
salaryExpectation?: string;
hasExperience?: string;
whyUs?: string;
availability?: string[];
}
export interface Application {
id: number;
jobId: number;
userId?: number;
name?: string;
email?: string;
phone?: string;
status: string;
createdAt: string;
}
export const applicationsApi = {
create: async (data: CreateApplicationRequest) => {
logCrudAction("create", "applications", data);
// Map frontend data to backend DTO
const payload = {
jobId: parseInt(data.jobId) || 0,
name: data.name,
email: data.email,
phone: data.phone,
whatsapp: data.phone,
message: data.coverLetter || data.whyUs,
resumeUrl: data.resumeUrl,
documents: {
linkedin: data.linkedin,
portfolio: data.portfolioUrl,
salaryExpectation: data.salaryExpectation,
hasExperience: data.hasExperience,
availability: data.availability,
}
};
return apiRequest<Application>('/applications', {
method: 'POST',
body: JSON.stringify(payload),
});
},
getByJob: async (jobId: string) => {
logCrudAction("read", "applications", { jobId });
return apiRequest<Application[]>(`/applications?jobId=${jobId}`);
},
};
// Admin Backoffice API // Admin Backoffice API
export interface AdminRoleAccess { export interface AdminRoleAccess {
role: string; role: string;