refactor(frontend): replace mocks with real api integration in my-jobs

This commit is contained in:
Tiago Yamamoto 2025-12-27 23:40:35 -03:00
parent 31e269034d
commit 6a7759b039

View file

@ -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>
<Button size="lg" className="w-full sm:w-auto"> <Link href="/dashboard/company/jobs/new">
<Plus className="h-5 w-5 mr-2" /> <Button size="lg" className="w-full sm:w-auto">
Post New Job <Plus className="h-5 w-5 mr-2" />
</Button> Post New Job
</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>
<Button className="mt-4"> <Link href="/dashboard/company/jobs/new">
<Plus className="h-4 w-4 mr-2" /> <Button className="mt-4">
Post New Job <Plus className="h-4 w-4 mr-2" />
</Button> Post New Job
</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">
<Edit className="h-4 w-4 mr-2" /> <Button variant="outline" size="sm" className="w-full">
Edit <Edit className="h-4 w-4 mr-2" />
</Button> Edit
</Button>
</Link>
</div> </div>
</div> </div>
</CardContent> </CardContent>