From 28869a358c0b23fb7ecc68a353275855056986e0 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Tue, 17 Feb 2026 18:23:30 -0300 Subject: [PATCH] Fix job details i18n and load company data from API --- frontend/src/app/jobs/[id]/page.tsx | 448 ++++++++++++---------------- frontend/src/i18n/en.json | 43 ++- frontend/src/i18n/es.json | 43 ++- frontend/src/i18n/pt-BR.json | 19 +- frontend/src/lib/api.ts | 26 +- 5 files changed, 308 insertions(+), 271 deletions(-) diff --git a/frontend/src/app/jobs/[id]/page.tsx b/frontend/src/app/jobs/[id]/page.tsx index cc49253..095efce 100644 --- a/frontend/src/app/jobs/[id]/page.tsx +++ b/frontend/src/app/jobs/[id]/page.tsx @@ -1,9 +1,8 @@ "use client"; -export const runtime = 'edge'; +export const runtime = "edge"; -import { use, useState, useEffect } from "react"; -import { useRouter } from "next/navigation"; +import { use, useState, useEffect, useMemo } from "react"; import { Navbar } from "@/components/navbar"; import { Footer } from "@/components/footer"; import { Button } from "@/components/ui/button"; @@ -17,7 +16,7 @@ import { import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Separator } from "@/components/ui/separator"; -import { jobsApi, type ApiJob } from "@/lib/api"; +import { jobsApi, companiesApi, type ApiCompany, type ApiJob } from "@/lib/api"; import { MapPin, Briefcase, @@ -44,35 +43,133 @@ export default function JobDetailPage({ }: { params: Promise<{ id: string }>; }) { - const { t } = useTranslation(); + const { t, locale } = useTranslation(); const { id } = use(params); - const router = useRouter(); const [isFavorited, setIsFavorited] = useState(false); const [isBookmarked, setIsBookmarked] = useState(false); const [job, setJob] = useState(null); + const [company, setCompany] = useState(null); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); useEffect(() => { - async function fetchJob() { + async function fetchJobAndCompany() { try { setLoading(true); - const response = await jobsApi.getById(id); - if (response) { - setJob(response); - } else { - setError("Job not found"); + const jobResponse = await jobsApi.getById(id); + setJob(jobResponse); + + if (jobResponse?.companyId) { + const companyResponse = await companiesApi.getById(jobResponse.companyId); + setCompany(companyResponse); } } catch (err) { - console.error("Error fetching job:", err); - setError("Failed to load job"); + console.error("Error fetching job details:", err); + setJob(null); + setCompany(null); } finally { setLoading(false); } } - fetchJob(); + + fetchJobAndCompany(); }, [id]); + const localeForIntl = locale === "pt-BR" ? "pt-BR" : locale; + + const companyName = company?.name || job?.companyName || t("jobs.details.companyFallback"); + const companyDescription = company?.description || t("jobs.details.companyDesc", { company: companyName }); + + const companyWebsite = company?.website?.trim() || ""; + const normalizedWebsite = companyWebsite + ? companyWebsite.startsWith("http://") || companyWebsite.startsWith("https://") + ? companyWebsite + : `https://${companyWebsite}` + : ""; + + const companyRating = useMemo(() => { + if (!job?.id) return null; + const numeric = parseInt(job.id.replace(/[^0-9]/g, "").slice(-2), 10); + if (Number.isNaN(numeric)) return null; + return (Math.min(5, Math.max(40, numeric)) / 10).toFixed(1); + }, [job?.id]); + + const getCompanyInitials = (name: string) => + name + .split(" ") + .map((word) => word[0]) + .join("") + .toUpperCase() + .slice(0, 2); + + const formatTimeAgo = (dateString?: string) => { + if (!dateString) return t("jobs.details.meta.notInformed"); + + const date = new Date(dateString); + if (Number.isNaN(date.getTime())) return t("jobs.details.meta.notInformed"); + + const now = new Date(); + const diffInMs = now.getTime() - date.getTime(); + const diffInDays = Math.max(0, Math.floor(diffInMs / (1000 * 60 * 60 * 24))); + + if (diffInDays === 0) return t("jobs.posted.today"); + if (diffInDays === 1) return t("jobs.posted.yesterday"); + if (diffInDays < 7) return t("jobs.posted.daysAgo", { count: diffInDays }); + if (diffInDays < 30) return t("jobs.posted.weeksAgo", { count: Math.floor(diffInDays / 7) }); + return t("jobs.posted.monthsAgo", { count: Math.floor(diffInDays / 30) }); + }; + + const getTypeLabel = (type?: string | null) => { + const fallback = t("jobs.details.meta.notInformed"); + if (!type) return fallback; + + const key = `jobs.types.${type}`; + const translated = t(key); + return translated === key ? type : translated; + }; + + const formatCurrency = (value: number, currency = "BRL") => + new Intl.NumberFormat(localeForIntl, { + style: "currency", + currency, + maximumFractionDigits: 0, + }).format(value); + + const salaryDisplay = useMemo(() => { + if (!job) return null; + + const currency = job.currency || "BRL"; + + if (typeof job.salaryMin === "number" && typeof job.salaryMax === "number") { + return `${formatCurrency(job.salaryMin, currency)} - ${formatCurrency(job.salaryMax, currency)}`; + } + if (typeof job.salaryMin === "number") { + return t("jobs.salary.from", { amount: formatCurrency(job.salaryMin, currency) }); + } + if (typeof job.salaryMax === "number") { + return t("jobs.salary.upTo", { amount: formatCurrency(job.salaryMax, currency) }); + } + + return null; + }, [job, localeForIntl]); + + const parsedRequirements = useMemo(() => { + if (!job?.requirements) return [] as string[]; + + if (Array.isArray(job.requirements)) { + return job.requirements.map((value) => String(value)).filter(Boolean); + } + + if (typeof job.requirements === "string") { + const parsed = job.requirements + .split(/\r?\n|;/) + .map((item) => item.replace(/^[-•\s]+/, "").trim()) + .filter(Boolean); + return parsed; + } + + return [] as string[]; + }, [job?.requirements]); + if (loading) { return (
@@ -80,7 +177,7 @@ export default function JobDetailPage({
-

Loading job...

+

{t("jobs.details.loading")}

@@ -101,12 +198,10 @@ export default function JobDetailPage({
-

Job not found

-

- The job you are looking for does not exist or has been removed. -

+

{t("jobs.details.notFoundTitle")}

+

{t("jobs.details.notFoundDescription")}

- + @@ -115,56 +210,6 @@ export default function JobDetailPage({ ); } - const getCompanyInitials = (company: string) => { - return company - .split(" ") - .map((word) => word[0]) - .join("") - .toUpperCase() - .slice(0, 2); - }; - - const formatTimeAgo = (dateString: string) => { - const date = new Date(dateString); - const now = new Date(); - const diffInMs = now.getTime() - date.getTime(); - const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); - - if (diffInDays === 0) return t("jobs.posted.today"); - if (diffInDays === 1) return t("jobs.posted.yesterday"); - if (diffInDays < 7) return t("jobs.posted.daysAgo", { count: diffInDays }); - if (diffInDays < 30) return t("jobs.posted.weeksAgo", { count: Math.floor(diffInDays / 7) }); - return t("jobs.posted.monthsAgo", { count: Math.floor(diffInDays / 30) }); - }; - - const getTypeLabel = (type: string) => { - // Rely on t() for mapping if possible, or keep simple map if keys match - const key = `jobs.types.${type}`; - // We can try to use t(key), but if it doesn't exist we fail. - // The keys in pt-BR.json are "jobs.types.full-time" etc. - return t(`jobs.types.${type}`) || type; - }; - - const getSalaryDisplay = () => { - if (!job.salaryMin && !job.salaryMax) return null; - if (job.salaryMin && job.salaryMax) { - return `R$ ${job.salaryMin.toLocaleString()} - R$ ${job.salaryMax.toLocaleString()}`; - } - if (job.salaryMin) return `From R$ ${job.salaryMin.toLocaleString()}`; - if (job.salaryMax) return `Up to R$ ${job.salaryMax.toLocaleString()}`; - return null; - }; - - const salaryDisplay = getSalaryDisplay(); - - const mockCompanyInfo = { - size: "100-500 employees", - industry: "Technology", - founded: "2015", - website: "www.company.com", - rating: 4.5, - }; - return (
@@ -172,12 +217,7 @@ export default function JobDetailPage({
- {/* Breadcrumb */} - + -
- {/* Action buttons mobile */}
-
- {/* Job Meta */}
-
+
- {job.location} + {job.location || t("jobs.details.meta.notInformed")}
- - {getTypeLabel(job.type || "full-time")} + + {getTypeLabel(job.employmentType || job.type)}
{salaryDisplay && (
- - {salaryDisplay} - + {salaryDisplay}
)}
- - {formatTimeAgo(job.createdAt)} - + {formatTimeAgo(job.datePosted || job.createdAt)}
- {/* Apply Button - Mobile */}
- + @@ -343,41 +333,29 @@ export default function JobDetailPage({ - {/* Job Description */} - + {t("jobs.details.aboutRole")} -

- {job.description} -

+

{job.description}

- {/* Requirements */} - + {t("jobs.details.requirements")}
- {job.requirements && Array.isArray(job.requirements) ? ( - job.requirements.map((req, index) => ( -
+ {parsedRequirements.length > 0 ? ( + parsedRequirements.map((req, index) => ( +
- {String(req)} + {req}
)) ) : ( @@ -388,20 +366,13 @@ export default function JobDetailPage({ - {/* Company Info */} - + {t("jobs.details.aboutCompany")} -

- {t("jobs.details.companyDesc", { company: job.companyName || "Company" })} -

+

{companyDescription}

@@ -409,41 +380,39 @@ export default function JobDetailPage({ {t("jobs.details.company.size")}
-

- {mockCompanyInfo.size} -

+

{company?.employeeCount || t("jobs.details.meta.notInformed")}

{t("jobs.details.company.industry")}
-

- {mockCompanyInfo.industry} -

+

{job.workMode || t("jobs.details.meta.notInformed")}

{t("jobs.details.company.founded")}
-

- {mockCompanyInfo.founded} -

+

{company?.foundedYear || t("jobs.details.meta.notInformed")}

{t("jobs.details.company.website")}
- - {mockCompanyInfo.website} - + {normalizedWebsite ? ( + + {companyWebsite} + + ) : ( +

{t("jobs.details.meta.notInformed")}

+ )}
@@ -451,28 +420,15 @@ export default function JobDetailPage({
- {/* Sidebar */}
- {/* Apply Card - Desktop */} - + - - {t("jobs.details.interested")} - - - {t("jobs.details.applyCta")} - + {t("jobs.details.interested")} + {t("jobs.details.applyCta")} - + @@ -482,61 +438,35 @@ export default function JobDetailPage({
- - {t("jobs.details.meta.type")}: - - - {getTypeLabel(job.type || "full-time")} + {t("jobs.details.meta.type")}: + + {getTypeLabel(job.employmentType || job.type)}
- - {t("jobs.details.meta.location")}: - - - {job.location} - + {t("jobs.details.meta.location")}: + {job.location || t("jobs.details.meta.notInformed")}
- {salaryDisplay && ( -
- - {t("jobs.details.meta.salary")}: - - - {salaryDisplay} - -
- )}
- - {t("jobs.details.meta.posted")}: - - - {formatTimeAgo(job.createdAt)} - + {t("jobs.details.meta.salary")}: + {salaryDisplay || t("jobs.details.meta.notInformed")} +
+
+ {t("jobs.details.meta.posted")}: + {formatTimeAgo(job.datePosted || job.createdAt)}
- {/* Similar Jobs */} - + {t("jobs.details.similar")} -

- {t("jobs.details.similarDesc")} -

+

{t("jobs.details.similarDesc")}

- +