feat(frontend): i18n for admin users page

This commit is contained in:
Tiago Yamamoto 2025-12-31 11:47:15 -03:00
parent e845ba63c8
commit 3cb1db81c0
4 changed files with 227 additions and 51 deletions

View file

@ -23,6 +23,7 @@ import { usersApi, adminCompaniesApi, type ApiUser, type AdminCompany } from "@/
import { getCurrentUser, isAdminUser } from "@/lib/auth"
import { toast } from "sonner"
import { Skeleton } from "@/components/ui/skeleton"
import { useTranslation } from "@/lib/i18n"
const userDateFormatter = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
@ -31,6 +32,7 @@ const userDateFormatter = new Intl.DateTimeFormat("en-US", {
export default function AdminUsersPage() {
const router = useRouter()
const { t } = useTranslation()
const [users, setUsers] = useState<ApiUser[]>([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState("")
@ -105,7 +107,7 @@ export default function AdminUsersPage() {
setPage(data?.pagination?.page || targetPage)
} catch (error) {
console.error("[USER_FLOW] Error loading users:", error)
toast.error("Failed to load users")
toast.error(t('admin.users.messages.load_error'))
} finally {
setLoading(false)
}
@ -120,14 +122,14 @@ export default function AdminUsersPage() {
roles: [formData.role],
}
await usersApi.create(payload)
toast.success("User created successfully!")
toast.success(t('admin.users.messages.create_success'))
setIsDialogOpen(false)
setFormData({ name: "", email: "", password: "", role: "candidate", status: "active", companyId: "" })
setPage(1)
loadUsers(1)
} catch (error) {
console.error("[USER_FLOW] Error creating user:", error)
toast.error("Failed to create user")
toast.error(t('admin.users.messages.create_error'))
} finally {
setCreating(false)
}
@ -169,12 +171,12 @@ export default function AdminUsersPage() {
roles: [editFormData.role],
}
await usersApi.update(selectedUser.id, payload)
toast.success("User updated successfully!")
toast.success(t('admin.users.messages.update_success'))
setIsEditDialogOpen(false)
loadUsers()
} catch (error) {
console.error("[USER_FLOW] Error updating user:", error)
toast.error("Failed to update user")
toast.error(t('admin.users.messages.update_error'))
} finally {
setUpdating(false)
}
@ -193,7 +195,7 @@ export default function AdminUsersPage() {
try {
setDeleting(true)
await usersApi.delete(selectedUser.id)
toast.success("User deleted!")
toast.success(t('admin.users.messages.delete_success'))
// UI Update logic
if (users.length === 1 && page > 1) {
@ -210,7 +212,7 @@ export default function AdminUsersPage() {
if (error.message && error.message.includes("403")) {
toast.error("You don't have permission to delete this user (403)")
} else {
toast.error("Failed to delete user")
toast.error(t('admin.users.messages.delete_error'))
}
} finally {
setDeleting(false)
@ -247,44 +249,44 @@ export default function AdminUsersPage() {
{/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground">User management</h1>
<p className="text-sm sm:text-base text-muted-foreground mt-1">Manage all platform users</p>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground">{t('admin.users.title')}</h1>
<p className="text-sm sm:text-base text-muted-foreground mt-1">{t('admin.users.subtitle')}</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => loadUsers()} disabled={loading}>
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} />
Refresh
{t('admin.users.refresh')}
</Button>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button className="gap-2">
<Plus className="h-4 w-4" />
New user
{t('admin.users.new_user')}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create new user</DialogTitle>
<DialogDescription>Fill in the new user details</DialogDescription>
<DialogTitle>{t('admin.users.create_dialog.title')}</DialogTitle>
<DialogDescription>{t('admin.users.create_dialog.description')}</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Label htmlFor="name">{t('admin.users.table.name')}</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Full name"
placeholder={t('admin.users.table.name')}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Label htmlFor="email">{t('admin.users.table.email')}</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="email@example.com"
placeholder={t('admin.users.table.email')}
/>
</div>
<div className="grid gap-2">
@ -314,7 +316,7 @@ export default function AdminUsersPage() {
</div>
)}
<div className="grid gap-2">
<Label htmlFor="status">Status</Label>
<Label htmlFor="status">{t('admin.users.table.status')}</Label>
<Select value={formData.status} onValueChange={(v) => setFormData({ ...formData, status: v })}>
<SelectTrigger>
<SelectValue />
@ -326,7 +328,7 @@ export default function AdminUsersPage() {
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="role">Role</Label>
<Label htmlFor="role">{t('admin.users.table.role')}</Label>
<Select value={formData.role} onValueChange={(v) => setFormData({ ...formData, role: v })}>
<SelectTrigger>
<SelectValue />
@ -341,10 +343,10 @@ export default function AdminUsersPage() {
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Cancel</Button>
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>{t('admin.users.create_dialog.cancel')}</Button>
<Button onClick={handleCreate} disabled={creating}>
{creating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Create user
{t('admin.users.create_dialog.submit')}
</Button>
</DialogFooter>
</DialogContent>
@ -355,29 +357,29 @@ export default function AdminUsersPage() {
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogContent className="max-w-md max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{viewing ? "User Details" : "Edit User"}</DialogTitle>
<DialogDescription>{viewing ? "View user information" : "Update user details"}</DialogDescription>
<DialogTitle>{viewing ? t('admin.users.edit_dialog.title_view') : t('admin.users.edit_dialog.title_edit')}</DialogTitle>
<DialogDescription>{viewing ? t('admin.users.edit_dialog.description_view') : t('admin.users.edit_dialog.description_edit')}</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="edit-name">Name</Label>
<Label htmlFor="edit-name">{t('admin.users.table.name')}</Label>
<Input
id="edit-name"
value={editFormData.name}
onChange={(e) => setEditFormData({ ...editFormData, name: e.target.value })}
placeholder="Full name"
placeholder={t('admin.users.table.name')}
readOnly={viewing}
disabled={viewing}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-email">Email</Label>
<Label htmlFor="edit-email">{t('admin.users.table.email')}</Label>
<Input
id="edit-email"
type="email"
value={editFormData.email}
onChange={(e) => setEditFormData({ ...editFormData, email: e.target.value })}
placeholder="email@example.com"
placeholder={t('admin.users.table.email')}
readOnly={viewing}
disabled={viewing}
/>
@ -421,7 +423,7 @@ export default function AdminUsersPage() {
{viewing ? (
<>
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>
Close
{t('admin.users.edit_dialog.close')}
</Button>
<Button onClick={() => setViewing(false)}>
Edit
@ -430,11 +432,11 @@ export default function AdminUsersPage() {
) : (
<>
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>
Cancel
{t('admin.users.create_dialog.cancel')}
</Button>
<Button onClick={handleUpdate} disabled={updating}>
{updating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Save Changes
{t('admin.users.edit_dialog.save')}
</Button>
</>
)}
@ -446,9 +448,9 @@ export default function AdminUsersPage() {
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete User</DialogTitle>
<DialogTitle>{t('admin.users.delete_confirm.title')}</DialogTitle>
<DialogDescription>
Are you sure you want to delete this user? This action cannot be undone.
{t('admin.users.delete_confirm.description')}
</DialogDescription>
</DialogHeader>
<div className="py-4">
@ -462,11 +464,11 @@ export default function AdminUsersPage() {
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsDeleteDialogOpen(false)}>
Cancel
{t('admin.users.delete_confirm.cancel')}
</Button>
<Button variant="destructive" onClick={confirmDelete} disabled={deleting}>
{deleting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Delete User
{t('admin.users.delete_confirm.confirm')}
</Button>
</DialogFooter>
</DialogContent>
@ -478,13 +480,13 @@ export default function AdminUsersPage() {
<div className="grid gap-3 sm:gap-4 grid-cols-2 md:grid-cols-4">
<Card>
<CardHeader className="pb-3">
<CardDescription>Total users</CardDescription>
<CardDescription>{t('admin.users.total')}</CardDescription>
<CardTitle className="text-3xl">{totalUsers}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Admins (page)</CardDescription>
<CardDescription>{t('admin.users.admins')}</CardDescription>
<CardTitle className="text-3xl">
{users.filter((u) => u.role === "superadmin" || u.role === "admin" || u.role === "admin").length}
</CardTitle>
@ -492,13 +494,13 @@ export default function AdminUsersPage() {
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Recruiters (page)</CardDescription>
<CardDescription>{t('admin.users.recruiters')}</CardDescription>
<CardTitle className="text-3xl">{users.filter((u) => u.role === "recruiter").length}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Candidates (page)</CardDescription>
<CardDescription>{t('admin.users.candidates')}</CardDescription>
<CardTitle className="text-3xl">{users.filter((u) => u.role === "candidate" || u.role === "candidate").length}</CardTitle>
</CardHeader>
</Card>
@ -511,7 +513,7 @@ export default function AdminUsersPage() {
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search users by name or email..."
placeholder={t('admin.users.search_placeholder')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
@ -534,19 +536,19 @@ export default function AdminUsersPage() {
<Table className="min-w-[600px]">
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead className="hidden sm:table-cell">Email</TableHead>
<TableHead>Role</TableHead>
<TableHead className="hidden md:table-cell">Status</TableHead>
<TableHead className="hidden lg:table-cell">Created</TableHead>
<TableHead className="text-right">Actions</TableHead>
<TableHead>{t('admin.users.table.name')}</TableHead>
<TableHead className="hidden sm:table-cell">{t('admin.users.table.email')}</TableHead>
<TableHead>{t('admin.users.table.role')}</TableHead>
<TableHead className="hidden md:table-cell">{t('admin.users.table.status')}</TableHead>
<TableHead className="hidden lg:table-cell">{t('admin.users.table.created')}</TableHead>
<TableHead className="text-right">{t('admin.users.table.actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredUsers.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
No users found
{t('admin.users.table.no_users')}
</TableCell>
</TableRow>
) : (
@ -607,8 +609,8 @@ export default function AdminUsersPage() {
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 text-sm text-muted-foreground pt-4">
<span className="text-center sm:text-left">
{totalUsers === 0
? "No users to display"
: `Showing ${(page - 1) * limit + 1}-${Math.min(page * limit, totalUsers)} of ${totalUsers}`}
? t('admin.users.pagination.no_users_display')
: t('admin.users.pagination.showing', { start: (page - 1) * limit + 1, end: Math.min(page * limit, totalUsers), total: totalUsers })}
</span>
<div className="flex items-center gap-2">
<Button
@ -617,10 +619,10 @@ export default function AdminUsersPage() {
onClick={() => loadUsers(page - 1)}
disabled={page <= 1 || loading}
>
Previous
{t('admin.users.pagination.previous')}
</Button>
<span>
Page {page} of {totalPages}
{t('admin.users.pagination.page', { current: page, total: totalPages })}
</span>
<Button
variant="outline"
@ -628,7 +630,7 @@ export default function AdminUsersPage() {
onClick={() => loadUsers(page + 1)}
disabled={page >= totalPages || loading}
>
Next
{t('admin.users.pagination.next')}
</Button>
</div>
</div>

View file

@ -660,5 +660,63 @@
"a": "Use the Contact page to reach our support team."
}
}
},
"admin": {
"users": {
"title": "User management",
"subtitle": "Manage all platform users",
"total": "Total users",
"admins": "Admins",
"recruiters": "Recruiters",
"candidates": "Candidates",
"new_user": "New user",
"refresh": "Refresh",
"search_placeholder": "Search users by name or email...",
"table": {
"name": "Name",
"email": "Email",
"role": "Role",
"status": "Status",
"created": "Created",
"actions": "Actions",
"no_users": "No users found"
},
"pagination": {
"showing": "Showing {start}-{end} of {total}",
"previous": "Previous",
"next": "Next",
"page": "Page {current} of {total}",
"no_users_display": "No users to display"
},
"delete_confirm": {
"title": "Delete User",
"description": "Are you sure you want to delete this user? This action cannot be undone.",
"cancel": "Cancel",
"confirm": "Delete User"
},
"create_dialog": {
"title": "Create new user",
"description": "Fill in the new user details",
"cancel": "Cancel",
"submit": "Create user"
},
"edit_dialog": {
"title_edit": "Edit User",
"title_view": "User Details",
"description_edit": "Update user details",
"description_view": "View user information",
"close": "Close",
"save": "Save Changes"
},
"messages": {
"create_success": "User created successfully!",
"create_error": "Failed to create user",
"update_success": "User updated successfully!",
"update_error": "Failed to update user",
"delete_success": "User deleted!",
"delete_error": "Failed to delete user",
"load_error": "Failed to load users"
}
}
}
}

View file

@ -660,5 +660,63 @@
"a": "Usa la página de contacto para hablar con nuestro equipo."
}
}
},
"admin": {
"users": {
"title": "Gestión de Usuarios",
"subtitle": "Gestione todos los usuarios de la plataforma",
"total": "Total de usuarios",
"admins": "Admins",
"recruiters": "Reclutadores",
"candidates": "Candidatos",
"new_user": "Nuevo usuario",
"refresh": "Actualizar",
"search_placeholder": "Buscar usuarios por nombre o correo...",
"table": {
"name": "Nombre",
"email": "Correo",
"role": "Rol",
"status": "Estado",
"created": "Creado el",
"actions": "Acciones",
"no_users": "No se encontraron usuarios"
},
"pagination": {
"showing": "Mostrando {start}-{end} de {total}",
"previous": "Anterior",
"next": "Siguiente",
"page": "Página {current} de {total}",
"no_users_display": "No hay usuarios para mostrar"
},
"delete_confirm": {
"title": "Eliminar Usuario",
"description": "¿Está seguro de querer eliminar este usuario? Esta acción no se puede deshacer.",
"cancel": "Cancelar",
"confirm": "Eliminar Usuario"
},
"create_dialog": {
"title": "Crear nuevo usuario",
"description": "Complete los detalles del nuevo usuario",
"cancel": "Cancelar",
"submit": "Crear usuario"
},
"edit_dialog": {
"title_edit": "Editar Usuario",
"title_view": "Detalles del Usuario",
"description_edit": "Actualizar detalles del usuario",
"description_view": "Ver información del usuario",
"close": "Cerrar",
"save": "Guardar Cambios"
},
"messages": {
"create_success": "¡Usuario creado con éxito!",
"create_error": "Error al crear usuario",
"update_success": "¡Usuario actualizado con éxito!",
"update_error": "Error al actualizar usuario",
"delete_success": "¡Usuario eliminado!",
"delete_error": "Error al eliminar usuario",
"load_error": "Error al cargar usuarios"
}
}
}
}

View file

@ -660,5 +660,63 @@
"a": "Use a página de contato para falar com nosso suporte."
}
}
},
"admin": {
"users": {
"title": "Gestão de Usuários",
"subtitle": "Gerencie todos os usuários da plataforma",
"total": "Total de usuários",
"admins": "Admins",
"recruiters": "Recrutadores",
"candidates": "Candidatos",
"new_user": "Novo usuário",
"refresh": "Atualizar",
"search_placeholder": "Buscar usuários por nome ou e-mail...",
"table": {
"name": "Nome",
"email": "E-mail",
"role": "Função",
"status": "Status",
"created": "Criado em",
"actions": "Ações",
"no_users": "Nenhum usuário encontrado"
},
"pagination": {
"showing": "Mostrando {start}-{end} de {total}",
"previous": "Anterior",
"next": "Próximo",
"page": "Página {current} de {total}",
"no_users_display": "Nenhum usuário para mostrar"
},
"delete_confirm": {
"title": "Excluir Usuário",
"description": "Tem certeza que deseja excluir este usuário? Esta ação não pode ser desfeita.",
"cancel": "Cancelar",
"confirm": "Excluir Usuário"
},
"create_dialog": {
"title": "Criar novo usuário",
"description": "Preencha os detalhes do novo usuário",
"cancel": "Cancelar",
"submit": "Criar usuário"
},
"edit_dialog": {
"title_edit": "Editar Usuário",
"title_view": "Detalhes do Usuário",
"description_edit": "Atualizar detalhes do usuário",
"description_view": "Ver informações do usuário",
"close": "Fechar",
"save": "Salvar Alterações"
},
"messages": {
"create_success": "Usuário criado com sucesso!",
"create_error": "Falha ao criar usuário",
"update_success": "Usuário atualizado com sucesso!",
"update_error": "Falha ao atualizar usuário",
"delete_success": "Usuário excluído!",
"delete_error": "Falha ao excluir usuário",
"load_error": "Falha ao carregar usuários"
}
}
}
}