From 8eeecf76d74d6c09cd546408d4a8c0a9677a5fd8 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Fri, 26 Dec 2025 16:42:19 -0300 Subject: [PATCH] feat: admin tickets, dashboard i18n, user edit fix and location picker bugfix --- .../src/app/dashboard/tickets/[id]/page.tsx | 231 +++++++++++++++ frontend/src/app/dashboard/tickets/page.tsx | 173 +++++++++++ frontend/src/app/dashboard/translations.ts | 110 +++++++ frontend/src/app/dashboard/users/page.tsx | 27 +- frontend/src/app/post-job/page.tsx | 185 +++++++----- frontend/src/app/post-job/translations.ts | 274 ++++++++++++++++++ .../dashboard-contents/admin-dashboard.tsx | 50 ++-- frontend/src/components/dashboard-header.tsx | 2 + frontend/src/components/language-selector.tsx | 29 ++ frontend/src/components/location-picker.tsx | 4 +- frontend/src/components/sidebar.tsx | 7 +- frontend/src/lib/api.ts | 21 +- frontend/src/lib/store/language-store.ts | 21 ++ 13 files changed, 1021 insertions(+), 113 deletions(-) create mode 100644 frontend/src/app/dashboard/tickets/[id]/page.tsx create mode 100644 frontend/src/app/dashboard/tickets/page.tsx create mode 100644 frontend/src/app/dashboard/translations.ts create mode 100644 frontend/src/app/post-job/translations.ts create mode 100644 frontend/src/components/language-selector.tsx create mode 100644 frontend/src/lib/store/language-store.ts diff --git a/frontend/src/app/dashboard/tickets/[id]/page.tsx b/frontend/src/app/dashboard/tickets/[id]/page.tsx new file mode 100644 index 0000000..93d91a6 --- /dev/null +++ b/frontend/src/app/dashboard/tickets/[id]/page.tsx @@ -0,0 +1,231 @@ +"use client" + +import { useState, useEffect } from "react" +import { useParams, useRouter } from "next/navigation" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Badge } from "@/components/ui/badge" +import { Ticket, TicketMessage, ticketsApi } from "@/lib/api" +import { toast } from "sonner" +import { Send, ArrowLeft, Trash2 } from "lucide-react" +import { formatDistanceToNow } from "date-fns" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" + +export default function AdminTicketDetailsPage() { + const params = useParams() + const router = useRouter() + const ticketId = params.id as string + + const [ticket, setTicket] = useState(null) + const [messages, setMessages] = useState([]) + const [loading, setLoading] = useState(true) + const [inputText, setInputText] = useState("") + const [deleteOpen, setDeleteOpen] = useState(false) + + const fetchTicket = async () => { + try { + const data = await ticketsApi.get(ticketId) + setTicket(data.ticket) + setMessages(data.messages) + setLoading(false) + } catch (error) { + console.error("Failed to load ticket", error) + toast.error("Failed to load ticket") + setLoading(false) + } + } + + useEffect(() => { + if (ticketId) { + fetchTicket() + } + }, [ticketId]) + + const handleSendMessage = async () => { + if (!inputText.trim()) return + + try { + const newMsg = await ticketsApi.sendMessage(ticketId, inputText) + setMessages([...messages, newMsg]) + setInputText("") + } catch (error) { + console.error("Failed to send message", error) + toast.error("Failed to send message") + } + } + + const handleStatusChange = async (status: string) => { + try { + await ticketsApi.update(ticketId, { status }) + setTicket(prev => prev ? { ...prev, status: status as any } : null) + toast.success("Status updated") + } catch (error) { + toast.error("Failed to update status") + } + } + + const handlePriorityChange = async (priority: string) => { + try { + await ticketsApi.update(ticketId, { priority }) + setTicket(prev => prev ? { ...prev, priority: priority as any } : null) + toast.success("Priority updated") + } catch (error) { + toast.error("Failed to update priority") + } + } + + const handleDelete = async () => { + try { + await ticketsApi.delete(ticketId) + toast.success("Ticket deleted") + router.push("/dashboard/tickets") + } catch (error) { + toast.error("Failed to delete ticket") + } + } + + if (loading) { + return
Loading ticket details...
+ } + + if (!ticket) { + return
Ticket not found
+ } + + return ( +
+ {/* Header */} +
+
+ +
+

+ Ticket #{ticket.id.substring(0, 8)} + {ticket.status} +

+

{ticket.subject}

+
+
+ +
+ +
+ {/* Main Chat Area */} + + + Conversation + + +
+ {messages.map((msg) => { + const isUser = msg.userId.toString() === ticket.userId.toString() // Assuming simple string match + return ( +
+
+

{msg.message}

+ + {isUser ? 'User' : 'Admin'} • {formatDistanceToNow(new Date(msg.createdAt))} ago + +
+
+ ) + })} +
+
+
+ setInputText(e.target.value)} + placeholder="Type a reply..." + onKeyDown={(e) => { + if (e.key === 'Enter') handleSendMessage() + }} + /> + +
+
+ + {/* Sidebar / Meta */} + + + Admin Controls + + +
+ Status + +
+
+ Priority + +
+
+ User ID +

{ticket.userId}

+
+
+ Created +

{new Date(ticket.createdAt).toLocaleString()}

+
+
+
+
+ + + + + Delete Ticket + + Are you sure? This will delete the ticket and all messages permanently. + + + + + + + + +
+ ) +} diff --git a/frontend/src/app/dashboard/tickets/page.tsx b/frontend/src/app/dashboard/tickets/page.tsx new file mode 100644 index 0000000..5cc4709 --- /dev/null +++ b/frontend/src/app/dashboard/tickets/page.tsx @@ -0,0 +1,173 @@ +"use client" + +import { useState, useEffect } from "react" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" +import { Ticket, ticketsApi } from "@/lib/api" +import { toast } from "sonner" +import Link from "next/link" +import { MessageSquare, Trash2, Eye } from "lucide-react" +import { format } from "date-fns" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" + +export default function AdminTicketsPage() { + const [tickets, setTickets] = useState([]) + const [loading, setLoading] = useState(true) + const [deleteId, setDeleteId] = useState(null) + + const fetchTickets = async () => { + try { + setLoading(true) + const data = await ticketsApi.listAll() + setTickets(data) + } catch (error) { + console.error("Failed to fetch tickets", error) + toast.error("Failed to fetch tickets") + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchTickets() + }, []) + + const handleDelete = async () => { + if (!deleteId) return + try { + await ticketsApi.delete(deleteId) + toast.success("Ticket deleted") + setDeleteId(null) + fetchTickets() + } catch (error) { + console.error("Failed to delete ticket", error) + toast.error("Failed to delete ticket") + } + } + + const getStatusBadge = (status: string) => { + switch (status) { + case "open": return Open + case "in_progress": return In Progress + case "closed": return Closed + default: return {status} + } + } + + const getPriorityBadge = (priority: string) => { + switch (priority) { + case "high": return High + case "medium": return Medium + case "low": return Low + default: return {priority} + } + } + + return ( +
+
+
+

Support Tickets (Admin)

+

Manage all user support tickets.

+
+
+ + + + All Tickets + A list of all support tickets from users. + + + + + + ID + Subject + User ID + Status + Priority + Created + Actions + + + + {loading ? ( + + Loading... + + ) : tickets.length === 0 ? ( + + No tickets found + + ) : ( + tickets.map((ticket) => ( + + + {ticket.id.substring(0, 8)}... + + {ticket.subject} + + {ticket.userId} + + {getStatusBadge(ticket.status)} + {getPriorityBadge(ticket.priority)} + {format(new Date(ticket.createdAt), "MMM d, yyyy")} + +
+ + + + +
+
+
+ )) + )} +
+
+
+
+ + !open && setDeleteId(null)}> + + + Delete Ticket + + Are you sure you want to delete this ticket? This action cannot be undone. + + + + + + + + +
+ ) +} diff --git a/frontend/src/app/dashboard/translations.ts b/frontend/src/app/dashboard/translations.ts new file mode 100644 index 0000000..e019d0d --- /dev/null +++ b/frontend/src/app/dashboard/translations.ts @@ -0,0 +1,110 @@ +export const dashboardTranslations = { + pt: { + title: "Dashboard", + subtitle: "Visão geral do portal de vagas", + stats: { + activeJobs: "Vagas Ativas", + activeJobsDesc: "Total de vagas publicadas", + candidates: "Candidatos", + candidatesDesc: "Usuários registrados", + applications: "Candidaturas", + applicationsDesc: "Em andamento", + hiringRate: "Taxa de Contratação", + hiringRateDesc: "Candidaturas por vaga" + }, + jobs: { + title: "Gerenciamento de Vagas", + add: "Nova Vaga", + table: { + title: "Título", + company: "Empresa", + status: "Status", + created: "Criado em", + actions: "Ações" + }, + empty: "Nenhuma vaga encontrada." + }, + candidates: { + title: "Gerenciamento de Candidatos", + table: { + name: "Nome", + email: "Email", + location: "Localização", + actions: "Ações" + }, + empty: "Nenhum candidato encontrado." + } + }, + en: { + title: "Dashboard", + subtitle: "Overview of the jobs portal", + stats: { + activeJobs: "Active Jobs", + activeJobsDesc: "Total posted jobs", + candidates: "Total Candidates", + candidatesDesc: "Registered users", + applications: "Active Applications", + applicationsDesc: "Current pipeline", + hiringRate: "Hiring Rate", + hiringRateDesc: "Applications per job" + }, + jobs: { + title: "Job Management", + add: "Add Job", + table: { + title: "Title", + company: "Company", + status: "Status", + created: "Created At", + actions: "Actions" + }, + empty: "No jobs found." + }, + candidates: { + title: "Candidate Management", + table: { + name: "Name", + email: "Email", + location: "Location", + actions: "Actions" + }, + empty: "No candidates found." + } + }, + es: { + title: "Panel de Control", + subtitle: "Visión general del portal de empleos", + stats: { + activeJobs: "Empleos Activos", + activeJobsDesc: "Total publicados", + candidates: "Candidatos Total", + candidatesDesc: "Usuarios registrados", + applications: "Aplicaciones Activas", + applicationsDesc: "En proceso", + hiringRate: "Tasa de Contratación", + hiringRateDesc: "Aplicaciones por empleo" + }, + jobs: { + title: "Gestión de Empleos", + add: "Nuevo Empleo", + table: { + title: "Título", + company: "Empresa", + status: "Estado", + created: "Creado en", + actions: "Acciones" + }, + empty: "No se encontraron empleos." + }, + candidates: { + title: "Gestión de Candidatos", + table: { + name: "Nombre", + email: "Correo", + location: "Ubicación", + actions: "Acciones" + }, + empty: "No se encontraron candidatos." + } + } +} diff --git a/frontend/src/app/dashboard/users/page.tsx b/frontend/src/app/dashboard/users/page.tsx index 33b9daf..c7645e8 100644 --- a/frontend/src/app/dashboard/users/page.tsx +++ b/frontend/src/app/dashboard/users/page.tsx @@ -418,14 +418,25 @@ export default function AdminUsersPage() { - - {!viewing && ( - + {viewing ? ( + <> + + + + ) : ( + <> + + + )} diff --git a/frontend/src/app/post-job/page.tsx b/frontend/src/app/post-job/page.tsx index b154783..854c0e6 100644 --- a/frontend/src/app/post-job/page.tsx +++ b/frontend/src/app/post-job/page.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; +import { translations, Language } from "./translations"; import { toast } from "sonner"; import { Navbar } from "@/components/navbar"; import { Footer } from "@/components/footer"; @@ -47,18 +48,28 @@ const getCurrencySymbol = (code: string): string => { return symbols[code] || code; }; -// Salary period label helper -const getSalaryPeriodLabel = (type: string): string => { - const labels: Record = { - 'hourly': '/hora', 'daily': '/dia', 'weekly': '/semana', - 'monthly': '/mês', 'yearly': '/ano' - }; - return labels[type] || ''; -}; + export default function PostJobPage() { const router = useRouter(); const [step, setStep] = useState<1 | 2>(1); + + // Language State + const [lang, setLang] = useState('pt'); + const t = translations[lang]; + + // Helper inside to use t + const getSalaryPeriodLabel = (type: string): string => { + const labels: Record = { + 'hourly': t.options.period.hourly, + 'daily': t.options.period.daily, + 'weekly': t.options.period.weekly, + 'monthly': t.options.period.monthly, + 'yearly': t.options.period.yearly + }; + return labels[type] || ''; + }; + const [loading, setLoading] = useState(false); // Company/User data @@ -208,11 +219,25 @@ export default function PostJobPage() {
-
-

Postar uma Vaga

-

- Cadastre sua empresa e publique sua vaga em poucos minutos -

+
+
+ +
+
+

{t.title}

+

+ {t.subtitle} +

+
{/* Progress Steps */} @@ -226,7 +251,7 @@ export default function PostJobPage() { {s}
- {s === 1 ? "Dados" : "Confirmar"} + {s === 1 ? t.steps.data : t.steps.confirm}
))} @@ -235,12 +260,12 @@ export default function PostJobPage() { - {step === 1 && "Empresa & Vaga"} - {step === 2 && "Confirmar e Publicar"} + {step === 1 && t.cardTitle.step1} + {step === 2 && t.cardTitle.step2} - {step === 1 && "Informe os dados da empresa e da vaga"} - {step === 2 && "Revise as informações antes de publicar"} + {step === 1 && t.cardDesc.step1} + {step === 2 && t.cardDesc.step2} @@ -248,19 +273,19 @@ export default function PostJobPage() { {step === 1 && (
- +
setCompany({ ...company, name: e.target.value })} - placeholder="Minha Empresa Ltda" + placeholder={t.company.namePlaceholder} className="pl-10" />
- +
- +
- +
- -
-
+ +
+
- +
- + - + setCompany({ ...company, description: val })} @@ -415,24 +440,24 @@ export default function PostJobPage() { {/* Separator */}

- Dados da Vaga + {t.job.title}

- +
setJob({ ...job, title: e.target.value })} - placeholder="Desenvolvedor Full Stack" + placeholder={t.job.jobTitlePlaceholder} className="pl-10" />
- + setJob({ ...job, description: val })} @@ -452,7 +477,7 @@ export default function PostJobPage() { {/* Salary Section */}
- +
@@ -469,7 +494,7 @@ export default function PostJobPage() { {/* Currency and Period Row */}
- +
- +
@@ -542,48 +567,48 @@ export default function PostJobPage() {
- +
- +
- +
)} @@ -593,33 +618,39 @@ export default function PostJobPage() {

- Empresa + {t.common.company}

-

Nome: {company.name}

-

Email: {company.email}

- {company.phone &&

Telefone: {company.ddi} {company.phone}

} +

{t.common.name}: {company.name}

+

{t.common.email}: {company.email}

+ {company.phone &&

{t.common.phone}: {company.ddi} {company.phone}

}

- Vaga + {t.common.job}

-

Título: {job.title}

-

Localização: {job.location || "Não informado"}

-

Salário: { +

{t.common.title}: {job.title}

+

{t.common.location}: {job.location || "Não informado"}

+

{t.common.salary}: { job.salaryNegotiable - ? "Candidato envia proposta" + ? t.job.salaryNegotiable : salaryMode === 'fixed' - ? (job.salaryFixed ? `${getCurrencySymbol(job.currency)} ${job.salaryFixed} ${getSalaryPeriodLabel(job.salaryType)}` : "A combinar") - : (job.salaryMin && job.salaryMax ? `${getCurrencySymbol(job.currency)} ${job.salaryMin} - ${job.salaryMax} ${getSalaryPeriodLabel(job.salaryType)}` : "A combinar") + ? (job.salaryFixed ? `${getCurrencySymbol(job.currency)} ${job.salaryFixed} ${getSalaryPeriodLabel(job.salaryType)}` : t.job.salaryNegotiable) + : (job.salaryMin && job.salaryMax ? `${getCurrencySymbol(job.currency)} ${job.salaryMin} - ${job.salaryMax} ${getSalaryPeriodLabel(job.salaryType)}` : t.job.salaryNegotiable) }

-

Tipo: {job.employmentType || "Qualquer"} / {job.workingHours === 'full-time' ? 'Integral' : job.workingHours === 'part-time' ? 'Meio Período' : 'Qualquer'} / {job.workMode}

+

{t.common.type}: { + (job.employmentType ? (t.options.contract[job.employmentType as keyof typeof t.options.contract] || job.employmentType) : t.options.any) + } / { + job.workingHours === 'full-time' ? t.options.hours.fullTime : job.workingHours === 'part-time' ? t.options.hours.partTime : t.options.any + } / { + job.workMode === 'remote' ? t.options.mode.remote : job.workMode === 'hybrid' ? t.options.mode.hybrid : t.options.mode.onsite + }

diff --git a/frontend/src/app/post-job/translations.ts b/frontend/src/app/post-job/translations.ts new file mode 100644 index 0000000..0ee2b6f --- /dev/null +++ b/frontend/src/app/post-job/translations.ts @@ -0,0 +1,274 @@ +export const translations = { + pt: { + title: "Postar uma Vaga", + subtitle: "Cadastre sua empresa e publique sua vaga em poucos minutos", + steps: { + data: "Dados", + confirm: "Confirmar" + }, + cardTitle: { + step1: "Empresa & Vaga", + step2: "Confirmar e Publicar" + }, + cardDesc: { + step1: "Informe os dados da empresa e da vaga", + step2: "Revise as informações antes de publicar" + }, + common: { + company: "Empresa", + job: "Vaga", + name: "Nome", + email: "Email", + phone: "Telefone", + title: "Título", + location: "Localização", + salary: "Salário", + type: "Tipo" + }, + company: { + name: "Nome da Empresa *", + namePlaceholder: "Minha Empresa Ltda", + email: "Email *", + emailPlaceholder: "contato@empresa.com", + password: "Senha *", + confirmPassword: "Confirmar Senha *", + phone: "Telefone", + phoneHelp: "Selecione o código do país e digite o número com DDD.", + website: "Site da Empresa", + websitePlaceholder: "https://minhaempresa.com", + size: "Tamanho da Empresa", + sizePlaceholder: "Selecione o tamanho", + founded: "Ano de Fundação", + description: "Sobre a Empresa" + }, + job: { + title: "Dados da Vaga", + jobTitle: "Título da Vaga *", + jobTitlePlaceholder: "ex: Desenvolvedor Frontend Senior", + location: "Localização", + locationPlaceholder: "ex: São Paulo, SP (ou Remoto)", + salary: "Salário", + salaryNegotiable: "A combinar", + currency: "Moeda", + period: "Período", + contractType: "Tipo de Contrato", + workMode: "Modelo de Trabalho", + workingHours: "Jornada de Trabalho", + description: "Descrição da Vaga" + }, + options: { + any: "Qualquer", + period: { + hourly: "por hora", + daily: "por dia", + weekly: "por semana", + monthly: "por mês", + yearly: "por ano" + }, + contract: { + permanent: "Permanente", + contract: "Contrato (PJ)", + training: "Estágio/Trainee", + temporary: "Temporário", + voluntary: "Voluntário" + }, + hours: { + fullTime: "Tempo Integral", + partTime: "Meio Período" + }, + mode: { + remote: "Remoto", + hybrid: "Híbrido", + onsite: "Presencial" + } + }, + buttons: { + back: "Voltar", + next: "Próximo: Confirmar", + publish: "Publicar Vaga", + publishing: "Publicando..." + } + }, + en: { + title: "Post a Job", + subtitle: "Register your company and publish your job in minutes", + steps: { + data: "Details", + confirm: "Confirm" + }, + cardTitle: { + step1: "Company & Job", + step2: "Confirm & Publish" + }, + cardDesc: { + step1: "Enter company and job details", + step2: "Review information before publishing" + }, + common: { + company: "Company", + job: "Job", + name: "Name", + email: "Email", + phone: "Phone", + title: "Title", + location: "Location", + salary: "Salary", + type: "Type" + }, + company: { + name: "Company Name *", + namePlaceholder: "My Company Ltd", + email: "Email *", + emailPlaceholder: "contact@company.com", + password: "Password *", + confirmPassword: "Confirm Password *", + phone: "Phone", + phoneHelp: "Select country code and enter number.", + website: "Company Website", + websitePlaceholder: "https://mycompany.com", + size: "Company Size", + sizePlaceholder: "Select size", + founded: "Founded Year", + description: "About the Company" + }, + job: { + title: "Job Details", + jobTitle: "Job Title *", + jobTitlePlaceholder: "e.g. Senior Frontend Developer", + location: "Location", + locationPlaceholder: "e.g. New York, NY (or Remote)", + salary: "Salary", + salaryNegotiable: "Negotiable", + currency: "Currency", + period: "Period", + contractType: "Contract Type", + workMode: "Work Mode", + workingHours: "Working Hours", + description: "Job Description" + }, + options: { + any: "Any", + period: { + hourly: "/hour", + daily: "/day", + weekly: "/week", + monthly: "/month", + yearly: "/year" + }, + contract: { + permanent: "Permanent", + contract: "Contract", + training: "Internship/Trainee", + temporary: "Temporary", + voluntary: "Voluntary" + }, + hours: { + fullTime: "Full-time", + partTime: "Part-time" + }, + mode: { + remote: "Remote", + hybrid: "Hybrid", + onsite: "On-site" + } + }, + buttons: { + back: "Back", + next: "Next: Confirm", + publish: "Publish Job", + publishing: "Publishing..." + } + }, + es: { + title: "Publicar un Empleo", + subtitle: "Registre su empresa y publique su vacante en minutos", + steps: { + data: "Datos", + confirm: "Confirmar" + }, + cardTitle: { + step1: "Empresa y Empleo", + step2: "Confirmar y Publicar" + }, + cardDesc: { + step1: "Ingrese los datos de la empresa y del empleo", + step2: "Revise la información antes de publicar" + }, + common: { + company: "Empresa", + job: "Empleo", + name: "Nombre", + email: "Correo", + phone: "Teléfono", + title: "Título", + location: "Ubicación", + salary: "Salario", + type: "Tipo" + }, + company: { + name: "Nombre de la Empresa *", + namePlaceholder: "Mi Empresa Ltda", + email: "Correo Electrónico *", + emailPlaceholder: "contacto@empresa.com", + password: "Contraseña *", + confirmPassword: "Confirmar Contraseña *", + phone: "Teléfono", + phoneHelp: "Seleccione el código de país e ingrese el número.", + website: "Sitio Web", + websitePlaceholder: "https://miempresa.com", + size: "Tamaño de la Empresa", + sizePlaceholder: "Seleccione tamaño", + founded: "Año de Fundación", + description: "Sobre la Empresa" + }, + job: { + title: "Datos del Empleo", + jobTitle: "Título del Puesto *", + jobTitlePlaceholder: "ej: Desarrollador Frontend Senior", + location: "Ubicación", + locationPlaceholder: "ej: Ciudad de México (o Remoto)", + salary: "Salario", + salaryNegotiable: "A convenir", + currency: "Moneda", + period: "Periodo", + contractType: "Tipo de Contrato", + workMode: "Modalidad", + workingHours: "Jornada Laboral", + description: "Descripción del Puesto" + }, + options: { + any: "Cualquiera", + period: { + hourly: "por hora", + daily: "por día", + weekly: "por semana", + monthly: "por mes", + yearly: "por año" + }, + contract: { + permanent: "Permanente", + contract: "Contrato", + training: "Pasantía", + temporary: "Temporal", + voluntary: "Voluntario" + }, + hours: { + fullTime: "Tiempo Completo", + partTime: "Medio Tiempo" + }, + mode: { + remote: "Remoto", + hybrid: "Híbrido", + onsite: "Presencial" + } + }, + buttons: { + back: "Volver", + next: "Siguiente: Confirmar", + publish: "Publicar Empleo", + publishing: "Publicando..." + } + } +} + +export type Language = 'pt' | 'en' | 'es' diff --git a/frontend/src/components/dashboard-contents/admin-dashboard.tsx b/frontend/src/components/dashboard-contents/admin-dashboard.tsx index 283b586..8068da0 100644 --- a/frontend/src/components/dashboard-contents/admin-dashboard.tsx +++ b/frontend/src/components/dashboard-contents/admin-dashboard.tsx @@ -23,10 +23,14 @@ import { Briefcase, Users, TrendingUp, FileText, Plus, MoreHorizontal, Loader2 } import { motion } from "framer-motion" import { adminJobsApi, adminCandidatesApi, type AdminJob, type AdminCandidate, type AdminCandidateStats } from "@/lib/api" import { toast } from "sonner" +import { useLanguageStore } from "@/lib/store/language-store" +import { dashboardTranslations } from "@/app/dashboard/translations" export function AdminDashboardContent() { const [isDialogOpen, setIsDialogOpen] = useState(false) const [isLoading, setIsLoading] = useState(true) + const { language } = useLanguageStore() + const t = dashboardTranslations[language] const [stats, setStats] = useState({ activeJobs: 0, @@ -100,8 +104,8 @@ export function AdminDashboardContent() { animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} > -

Dashboard

-

Overview of the jobs portal

+

{t.title}

+

{t.subtitle}

{/* Stats */} @@ -112,28 +116,28 @@ export function AdminDashboardContent() { className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6" > @@ -145,12 +149,12 @@ export function AdminDashboardContent() { > - Job management + {t.jobs.title} @@ -195,17 +199,17 @@ export function AdminDashboardContent() { - Title - Company - Status - Created At - Actions + {t.jobs.table.title} + {t.jobs.table.company} + {t.jobs.table.status} + {t.jobs.table.created} + {t.jobs.table.actions} {recentJobs.length === 0 ? ( - No jobs found. + {t.jobs.empty} ) : ( recentJobs.map((job) => ( @@ -238,16 +242,16 @@ export function AdminDashboardContent() { > - Candidate management + {t.candidates.title}
- Name - Email - Location - Actions + {t.candidates.table.name} + {t.candidates.table.email} + {t.candidates.table.location} + {t.candidates.table.actions} diff --git a/frontend/src/components/dashboard-header.tsx b/frontend/src/components/dashboard-header.tsx index d4cdeb5..4634f16 100644 --- a/frontend/src/components/dashboard-header.tsx +++ b/frontend/src/components/dashboard-header.tsx @@ -16,6 +16,7 @@ import { import { LogOut, User } from "lucide-react"; import { logout, getCurrentUser } from "@/lib/auth"; import { NotificationsDropdown } from "@/components/notifications-dropdown"; +import { LanguageSelector } from "@/components/language-selector"; export function DashboardHeader() { const router = useRouter(); @@ -49,6 +50,7 @@ export function DashboardHeader() {
+ diff --git a/frontend/src/components/language-selector.tsx b/frontend/src/components/language-selector.tsx new file mode 100644 index 0000000..b2c2315 --- /dev/null +++ b/frontend/src/components/language-selector.tsx @@ -0,0 +1,29 @@ +"use client" + +import { useLanguageStore, Language } from "@/lib/store/language-store" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Globe } from "lucide-react" + +export function LanguageSelector() { + const { language, setLanguage } = useLanguageStore() + + return ( + + ) +} diff --git a/frontend/src/components/location-picker.tsx b/frontend/src/components/location-picker.tsx index 8d945cc..e9415e9 100644 --- a/frontend/src/components/location-picker.tsx +++ b/frontend/src/components/location-picker.tsx @@ -47,7 +47,7 @@ export function LocationPicker({ value, onChange }: LocationPickerProps) { const timeout = setTimeout(() => { setLoading(true); locationsApi.search(query, selectedCountry) - .then(setResults) + .then(res => setResults(res || [])) .catch(err => { console.error("Search failed", err); setResults([]); @@ -123,7 +123,7 @@ export function LocationPicker({ value, onChange }: LocationPickerProps) {
{/* Results Dropdown */} - {showResults && results.length > 0 && ( + {showResults && results?.length > 0 && (
{results.map((item) => (