refactor(frontend): replace mocks with real api integration in my-jobs
This commit is contained in:
parent
31e269034d
commit
6a7759b039
1 changed files with 100 additions and 104 deletions
|
|
@ -1,10 +1,9 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useEffect, useState, useCallback } from "react"
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card"
|
} from "@/components/ui/card"
|
||||||
|
|
@ -37,6 +36,7 @@ import {
|
||||||
Pause,
|
Pause,
|
||||||
Play,
|
Play,
|
||||||
Zap,
|
Zap,
|
||||||
|
Loader2,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import {
|
import {
|
||||||
|
|
@ -46,81 +46,18 @@ import {
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import { toast } from "sonner"
|
||||||
// Mock data - in production this would come from API
|
import { authApi, jobsApi, ApiJob, ApiUser } from "@/lib/api"
|
||||||
const mockJobs = [
|
import { formatDistanceToNow } from "date-fns"
|
||||||
{
|
import { ptBR } from "date-fns/locale"
|
||||||
id: "1",
|
|
||||||
title: "Senior Full Stack Developer",
|
|
||||||
type: "Full Time",
|
|
||||||
location: "São Paulo, SP",
|
|
||||||
workMode: "hybrid",
|
|
||||||
salary: "R$ 12,000 - R$ 18,000",
|
|
||||||
applications: 45,
|
|
||||||
views: 320,
|
|
||||||
postedAt: "2 days ago",
|
|
||||||
expiresAt: "28 days left",
|
|
||||||
status: "active",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
title: "Designer UX/UI",
|
|
||||||
type: "Full Time",
|
|
||||||
location: "Remote",
|
|
||||||
workMode: "remote",
|
|
||||||
salary: "R$ 8,000 - R$ 12,000",
|
|
||||||
applications: 32,
|
|
||||||
views: 256,
|
|
||||||
postedAt: "5 days ago",
|
|
||||||
expiresAt: "25 days left",
|
|
||||||
status: "active",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
title: "Product Manager",
|
|
||||||
type: "Full Time",
|
|
||||||
location: "São Paulo, SP",
|
|
||||||
workMode: "onsite",
|
|
||||||
salary: "R$ 15,000 - R$ 20,000",
|
|
||||||
applications: 28,
|
|
||||||
views: 189,
|
|
||||||
postedAt: "1 week ago",
|
|
||||||
expiresAt: "21 days left",
|
|
||||||
status: "active",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "4",
|
|
||||||
title: "DevOps Engineer",
|
|
||||||
type: "Full Time",
|
|
||||||
location: "São Paulo, SP",
|
|
||||||
workMode: "hybrid",
|
|
||||||
salary: "R$ 14,000 - R$ 20,000",
|
|
||||||
applications: 15,
|
|
||||||
views: 98,
|
|
||||||
postedAt: "2 weeks ago",
|
|
||||||
expiresAt: "14 days left",
|
|
||||||
status: "paused",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "5",
|
|
||||||
title: "Junior Frontend Developer",
|
|
||||||
type: "Full Time",
|
|
||||||
location: "São Paulo, SP",
|
|
||||||
workMode: "onsite",
|
|
||||||
salary: "R$ 4,000 - R$ 6,000",
|
|
||||||
applications: 120,
|
|
||||||
views: 450,
|
|
||||||
postedAt: "1 month ago",
|
|
||||||
expiresAt: "Expired",
|
|
||||||
status: "closed",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
active: { label: "Active", color: "bg-green-100 text-green-800 border-green-200" },
|
active: { label: "Active", color: "bg-green-100 text-green-800 border-green-200" },
|
||||||
paused: { label: "Paused", color: "bg-yellow-100 text-yellow-800 border-yellow-200" },
|
paused: { label: "Paused", color: "bg-yellow-100 text-yellow-800 border-yellow-200" },
|
||||||
closed: { label: "Closed", color: "bg-gray-100 text-gray-800 border-gray-200" },
|
closed: { label: "Closed", color: "bg-gray-100 text-gray-800 border-gray-200" },
|
||||||
draft: { label: "Draft", color: "bg-blue-100 text-blue-800 border-blue-200" },
|
draft: { label: "Draft", color: "bg-blue-100 text-blue-800 border-blue-200" },
|
||||||
|
published: { label: "Active", color: "bg-green-100 text-green-800 border-green-200" }, // Mapping published to active style
|
||||||
|
open: { label: "Active", color: "bg-green-100 text-green-800 border-green-200" },
|
||||||
}
|
}
|
||||||
|
|
||||||
const workModeConfig = {
|
const workModeConfig = {
|
||||||
|
|
@ -130,10 +67,46 @@ const workModeConfig = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MyJobsPage() {
|
export default function MyJobsPage() {
|
||||||
const [jobs] = useState(mockJobs)
|
const [jobs, setJobs] = useState<ApiJob[]>([])
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [user, setUser] = useState<ApiUser | null>(null)
|
||||||
const [statusFilter, setStatusFilter] = useState("all")
|
const [statusFilter, setStatusFilter] = useState("all")
|
||||||
const [searchTerm, setSearchTerm] = useState("")
|
const [searchTerm, setSearchTerm] = useState("")
|
||||||
|
|
||||||
|
const fetchJobs = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
// 1. Get User to know Company ID
|
||||||
|
const currentUser = await authApi.getCurrentUser()
|
||||||
|
setUser(currentUser)
|
||||||
|
|
||||||
|
if (!currentUser.companyId) {
|
||||||
|
// If user has no company, they shouldn't see jobs or should be prompted to create one
|
||||||
|
// For now just show empty list
|
||||||
|
setJobs([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fetch Jobs for that Company
|
||||||
|
const response = await jobsApi.list({
|
||||||
|
companyId: currentUser.companyId,
|
||||||
|
limit: 100 // Fetch all for now to client-side filter
|
||||||
|
})
|
||||||
|
setJobs(response.data)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching jobs:", error)
|
||||||
|
toast.error("Failed to load jobs")
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchJobs()
|
||||||
|
}, [fetchJobs])
|
||||||
|
|
||||||
|
|
||||||
const filteredJobs = jobs.filter((job) => {
|
const filteredJobs = jobs.filter((job) => {
|
||||||
const matchesStatus = statusFilter === "all" || job.status === statusFilter
|
const matchesStatus = statusFilter === "all" || job.status === statusFilter
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
|
|
@ -144,9 +117,25 @@ export default function MyJobsPage() {
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
total: jobs.length,
|
total: jobs.length,
|
||||||
active: jobs.filter((j) => j.status === "active").length,
|
active: jobs.filter((j) => j.status === "published" || j.status === "open").length,
|
||||||
applications: jobs.reduce((acc, j) => acc + j.applications, 0),
|
applications: jobs.reduce((acc, j) => acc + (j.applicationCount || 0), 0),
|
||||||
views: jobs.reduce((acc, j) => acc + j.views, 0),
|
views: 0, // Backend does not strictly track views yet per ApiJob interface
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to format currency
|
||||||
|
const formatSalary = (min?: number, max?: number) => {
|
||||||
|
if (!min && !max) return "Negotiable"
|
||||||
|
if (min && max) return `R$ ${min.toLocaleString()} - R$ ${max.toLocaleString()}`
|
||||||
|
if (min) return `From R$ ${min.toLocaleString()}`
|
||||||
|
return `Up to R$ ${max?.toLocaleString()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -158,13 +147,15 @@ export default function MyJobsPage() {
|
||||||
My Jobs
|
My Jobs
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Manage your job postings
|
Manage your job postings for {user?.companyId ? "your company" : "..."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Link href="/dashboard/company/jobs/new">
|
||||||
<Button size="lg" className="w-full sm:w-auto">
|
<Button size="lg" className="w-full sm:w-auto">
|
||||||
<Plus className="h-5 w-5 mr-2" />
|
<Plus className="h-5 w-5 mr-2" />
|
||||||
Post New Job
|
Post New Job
|
||||||
</Button>
|
</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
|
|
@ -232,10 +223,9 @@ export default function MyJobsPage() {
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">All Status</SelectItem>
|
<SelectItem value="all">All Status</SelectItem>
|
||||||
<SelectItem value="active">Active</SelectItem>
|
<SelectItem value="published">Active (Published)</SelectItem>
|
||||||
<SelectItem value="paused">Paused</SelectItem>
|
|
||||||
<SelectItem value="closed">Closed</SelectItem>
|
|
||||||
<SelectItem value="draft">Draft</SelectItem>
|
<SelectItem value="draft">Draft</SelectItem>
|
||||||
|
<SelectItem value="closed">Closed</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -247,11 +237,13 @@ export default function MyJobsPage() {
|
||||||
<CardContent className="p-8 text-center text-muted-foreground">
|
<CardContent className="p-8 text-center text-muted-foreground">
|
||||||
<Briefcase className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
<Briefcase className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||||
<h3 className="font-semibold text-lg mb-2">No jobs found</h3>
|
<h3 className="font-semibold text-lg mb-2">No jobs found</h3>
|
||||||
<p>Start by posting your first job.</p>
|
<p>Start by posting your first job using the button above.</p>
|
||||||
|
<Link href="/dashboard/company/jobs/new">
|
||||||
<Button className="mt-4">
|
<Button className="mt-4">
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Post New Job
|
Post New Job
|
||||||
</Button>
|
</Button>
|
||||||
|
</Link>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -266,15 +258,15 @@ export default function MyJobsPage() {
|
||||||
<h3 className="font-semibold text-lg">{job.title}</h3>
|
<h3 className="font-semibold text-lg">{job.title}</h3>
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={statusConfig[job.status as keyof typeof statusConfig].color}
|
className={statusConfig[job.status as keyof typeof statusConfig]?.color || "bg-gray-100"}
|
||||||
>
|
>
|
||||||
{statusConfig[job.status as keyof typeof statusConfig].label}
|
{statusConfig[job.status as keyof typeof statusConfig]?.label || job.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className={workModeConfig[job.workMode as keyof typeof workModeConfig].color}
|
className={workModeConfig[job.workMode as keyof typeof workModeConfig]?.color || "bg-gray-100"}
|
||||||
>
|
>
|
||||||
{workModeConfig[job.workMode as keyof typeof workModeConfig].label}
|
{workModeConfig[job.workMode as keyof typeof workModeConfig]?.label || job.workMode}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|
@ -309,17 +301,17 @@ export default function MyJobsPage() {
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
{job.status === "active" ? (
|
{job.status === "published" || job.status === "open" ? (
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Pause className="h-4 w-4 mr-2" />
|
<Pause className="h-4 w-4 mr-2" />
|
||||||
Pause
|
Pause
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : job.status === "paused" ? (
|
) : (
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Play className="h-4 w-4 mr-2" />
|
<Play className="h-4 w-4 mr-2" />
|
||||||
Activate
|
Activate
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : null}
|
)}
|
||||||
<DropdownMenuItem className="text-destructive">
|
<DropdownMenuItem className="text-destructive">
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
Delete
|
Delete
|
||||||
|
|
@ -335,28 +327,30 @@ export default function MyJobsPage() {
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<DollarSign className="h-4 w-4" />
|
<DollarSign className="h-4 w-4" />
|
||||||
{job.salary}
|
{formatSalary(job.salaryMin, job.salaryMax)}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Clock className="h-4 w-4" />
|
<Clock className="h-4 w-4" />
|
||||||
{job.expiresAt}
|
Posted {formatDistanceToNow(new Date(job.createdAt), { addSuffix: true })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-6 text-sm">
|
<div className="flex flex-wrap gap-6 text-sm">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Users className="h-4 w-4 text-blue-500" />
|
<Users className="h-4 w-4 text-blue-500" />
|
||||||
<span className="font-medium">{job.applications}</span>
|
<span className="font-medium">{job.applicationCount || 0}</span>
|
||||||
<span className="text-muted-foreground">applications</span>
|
<span className="text-muted-foreground">applications</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Eye className="h-4 w-4 text-purple-500" />
|
<Eye className="h-4 w-4 text-purple-500" />
|
||||||
<span className="font-medium">{job.views}</span>
|
<span className="font-medium">{0}</span>
|
||||||
<span className="text-muted-foreground">views</span>
|
<span className="text-muted-foreground">views</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Calendar className="h-4 w-4 text-gray-500" />
|
<Calendar className="h-4 w-4 text-gray-500" />
|
||||||
<span className="text-muted-foreground">Posted {job.postedAt}</span>
|
<span className="text-muted-foreground">
|
||||||
|
Created {new Date(job.createdAt).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -369,10 +363,12 @@ export default function MyJobsPage() {
|
||||||
Applications
|
Applications
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Button variant="outline" size="sm" className="flex-1 lg:flex-none">
|
<Link href={`/jobs/${job.id}/edit`} className="flex-1 lg:flex-none">
|
||||||
|
<Button variant="outline" size="sm" className="w-full">
|
||||||
<Edit className="h-4 w-4 mr-2" />
|
<Edit className="h-4 w-4 mr-2" />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue