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:
parent
ce0531fefc
commit
592af3216e
3 changed files with 131 additions and 28 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue