chore: i18n user table messages
This commit is contained in:
parent
70d35e5fd5
commit
dc86cfacd8
@ -441,10 +441,34 @@
|
||||
"title": "Admin",
|
||||
"users": {
|
||||
"title": "Users",
|
||||
"error": "Failed to get users",
|
||||
"search": "Search users...",
|
||||
"columns": {
|
||||
"columns": "Columns",
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"role": "Role",
|
||||
"createdAt": "Created At",
|
||||
"customerId": "Customer ID",
|
||||
"status": "Status",
|
||||
"banReason": "Ban Reason",
|
||||
"banExpires": "Ban Expires"
|
||||
},
|
||||
"noResults": "No results",
|
||||
"firstPage": "First Page",
|
||||
"lastPage": "Last Page",
|
||||
"nextPage": "Next Page",
|
||||
"previousPage": "Previous Page",
|
||||
"rowsPerPage": "Rows per page",
|
||||
"page": "Page",
|
||||
"loading": "Loading...",
|
||||
"admin": "Admin",
|
||||
"user": "User",
|
||||
"email": {
|
||||
"verified": "Email Verified",
|
||||
"unverified": "Email Unverified"
|
||||
},
|
||||
"emailCopied": "Email copied to clipboard",
|
||||
"banned": "Banned",
|
||||
"active": "Active",
|
||||
"joined": "Joined",
|
||||
|
@ -442,10 +442,34 @@
|
||||
"title": "系统管理",
|
||||
"users": {
|
||||
"title": "用户管理",
|
||||
"error": "获取用户失败",
|
||||
"search": "搜索用户...",
|
||||
"columns": {
|
||||
"columns": "显示列",
|
||||
"name": "姓名",
|
||||
"email": "邮箱",
|
||||
"role": "角色",
|
||||
"createdAt": "创建时间",
|
||||
"customerId": "客户ID",
|
||||
"status": "状态",
|
||||
"banReason": "封禁原因",
|
||||
"banExpires": "封禁到期时间"
|
||||
},
|
||||
"noResults": "没有结果",
|
||||
"firstPage": "第一页",
|
||||
"lastPage": "最后一页",
|
||||
"nextPage": "下一页",
|
||||
"previousPage": "上一页",
|
||||
"rowsPerPage": "每页行数",
|
||||
"page": "页",
|
||||
"loading": "加载中...",
|
||||
"admin": "管理员",
|
||||
"user": "用户",
|
||||
"email": {
|
||||
"verified": "邮箱已验证",
|
||||
"unverified": "邮箱未验证"
|
||||
},
|
||||
"emailCopied": "邮箱已复制到剪贴板",
|
||||
"banned": "账号被封禁",
|
||||
"active": "账号正常",
|
||||
"joined": "加入时间",
|
||||
|
@ -158,13 +158,10 @@ export function UserDetailViewer({ user }: UserDetailViewerProps) {
|
||||
variant={user.role === 'admin' ? 'default' : 'outline'}
|
||||
className="px-1.5"
|
||||
>
|
||||
{user.role || 'user'}
|
||||
{t(user.role === 'admin' ? 'admin' : 'user')}
|
||||
</Badge>
|
||||
{/* email verified */}
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-muted-foreground px-1.5 hover:bg-accent"
|
||||
>
|
||||
<Badge variant="outline" className="px-1.5 hover:bg-accent">
|
||||
{user.emailVerified ? (
|
||||
<MailCheckIcon className="stroke-green-500 dark:stroke-green-400" />
|
||||
) : (
|
||||
@ -177,10 +174,7 @@ export function UserDetailViewer({ user }: UserDetailViewerProps) {
|
||||
|
||||
{/* user banned */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-muted-foreground px-1.5 hover:bg-accent"
|
||||
>
|
||||
<Badge variant="outline" className="px-1.5 hover:bg-accent">
|
||||
{user.banned ? (
|
||||
<UserRoundXIcon className="stroke-red-500 dark:stroke-red-400" />
|
||||
) : (
|
||||
|
@ -10,7 +10,7 @@ import { useEffect, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function UsersPageClient() {
|
||||
const t = useTranslations();
|
||||
const t = useTranslations('Dashboard.admin.users');
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [search, setSearch] = useState('');
|
||||
@ -35,14 +35,14 @@ export function UsersPageClient() {
|
||||
setData(result.data.data?.items || []);
|
||||
setTotal(result.data.data?.total || 0);
|
||||
} else {
|
||||
const errorMessage = result?.data?.error || 'Failed to fetch users';
|
||||
const errorMessage = result?.data?.error || t('error');
|
||||
toast.error(errorMessage);
|
||||
setData([]);
|
||||
setTotal(0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch users:', error);
|
||||
toast.error('An unexpected error occurred while fetching users');
|
||||
toast.error(t('error'));
|
||||
setData([]);
|
||||
setTotal(0);
|
||||
} finally {
|
||||
|
@ -51,6 +51,7 @@ import {
|
||||
UserRoundCheckIcon,
|
||||
UserRoundXIcon,
|
||||
} from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '../ui/badge';
|
||||
@ -85,155 +86,6 @@ function DataTableColumnHeader<TData, TValue>({
|
||||
);
|
||||
}
|
||||
|
||||
const columns: ColumnDef<User>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Name" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
return <UserDetailViewer user={user} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Email" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-sm px-1.5 cursor-pointer hover:bg-accent"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(user.email);
|
||||
toast.success('Email copied to clipboard');
|
||||
}}
|
||||
>
|
||||
{user.emailVerified ? (
|
||||
<MailCheckIcon className="stroke-green-500 dark:stroke-green-400" />
|
||||
) : (
|
||||
<MailQuestionIcon className="stroke-red-500 dark:stroke-red-400" />
|
||||
)}
|
||||
{user.email}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'role',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Role" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
const role = user.role || 'user';
|
||||
return (
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
<Badge
|
||||
variant={role === 'admin' ? 'default' : 'outline'}
|
||||
className="px-1.5"
|
||||
>
|
||||
{role}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Created At" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
{formatDate(user.createdAt)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'customerId',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Customer ID" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
{user.customerId ? (
|
||||
<a
|
||||
href={getStripeDashboardCustomerUrl(user.customerId)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline hover:underline-offset-4"
|
||||
>
|
||||
{user.customerId}
|
||||
</a>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'banned',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Status" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
<Badge variant="outline" className="px-1.5 hover:bg-accent">
|
||||
{user.banned ? (
|
||||
<UserRoundXIcon className="stroke-red-500 dark:stroke-red-400" />
|
||||
) : (
|
||||
<UserRoundCheckIcon className="stroke-green-500 dark:stroke-green-400" />
|
||||
)}
|
||||
{user.banned ? 'Banned' : 'Active'}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'banReason',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Ban Reason" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
{user.banReason || '-'}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'banExpires',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Ban Expires" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
{user.banExpires ? formatDate(user.banExpires) : '-'}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
interface UsersTableProps {
|
||||
data: User[];
|
||||
total: number;
|
||||
@ -262,10 +114,179 @@ export function UsersTable({
|
||||
onPageSizeChange,
|
||||
onSortingChange,
|
||||
}: UsersTableProps) {
|
||||
const t = useTranslations('Dashboard.admin.users');
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
|
||||
// Map column IDs to translation keys
|
||||
const columnIdToTranslationKey = {
|
||||
name: 'columns.name' as const,
|
||||
email: 'columns.email' as const,
|
||||
role: 'columns.role' as const,
|
||||
createdAt: 'columns.createdAt' as const,
|
||||
customerId: 'columns.customerId' as const,
|
||||
banned: 'columns.status' as const,
|
||||
banReason: 'columns.banReason' as const,
|
||||
banExpires: 'columns.banExpires' as const,
|
||||
} as const;
|
||||
|
||||
// Table columns definition
|
||||
const columns: ColumnDef<User>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('columns.name')} />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
return <UserDetailViewer user={user} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('columns.email')} />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-sm px-1.5 cursor-pointer hover:bg-accent"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(user.email);
|
||||
toast.success(t('emailCopied'));
|
||||
}}
|
||||
>
|
||||
{user.emailVerified ? (
|
||||
<MailCheckIcon className="stroke-green-500 dark:stroke-green-400" />
|
||||
) : (
|
||||
<MailQuestionIcon className="stroke-red-500 dark:stroke-red-400" />
|
||||
)}
|
||||
{user.email}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'role',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('columns.role')} />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
const role = user.role || 'user';
|
||||
return (
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
<Badge
|
||||
variant={role === 'admin' ? 'default' : 'outline'}
|
||||
className="px-1.5"
|
||||
>
|
||||
{t(role === 'admin' ? 'admin' : 'user')}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('columns.createdAt')} />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
{formatDate(user.createdAt)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'customerId',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={t('columns.customerId')}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
{user.customerId ? (
|
||||
<a
|
||||
href={getStripeDashboardCustomerUrl(user.customerId)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline hover:underline-offset-4"
|
||||
>
|
||||
{user.customerId}
|
||||
</a>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'banned',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('columns.status')} />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
<Badge variant="outline" className="px-1.5 hover:bg-accent">
|
||||
{user.banned ? (
|
||||
<UserRoundXIcon className="stroke-red-500 dark:stroke-red-400" />
|
||||
) : (
|
||||
<UserRoundCheckIcon className="stroke-green-500 dark:stroke-green-400" />
|
||||
)}
|
||||
{user.banned ? t('banned') : t('active')}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'banReason',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('columns.banReason')} />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
{user.banReason || '-'}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'banExpires',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={t('columns.banExpires')}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
{user.banExpires ? formatDate(user.banExpires) : '-'}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
@ -303,7 +324,7 @@ export function UsersTable({
|
||||
<div className="w-full flex-col justify-start gap-6 space-y-4">
|
||||
<div className="flex items-center justify-between px-4 lg:px-6 gap-4">
|
||||
<Input
|
||||
placeholder="Search users..."
|
||||
placeholder={t('search')}
|
||||
value={search}
|
||||
onChange={(event) => {
|
||||
onSearch(event.target.value);
|
||||
@ -315,7 +336,7 @@ export function UsersTable({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="cursor-pointer">
|
||||
{/* <IconLayoutColumns /> */}
|
||||
<span className="inline">Columns</span>
|
||||
<span className="inline">{t('columns.columns')}</span>
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@ -333,7 +354,11 @@ export function UsersTable({
|
||||
column.toggleVisibility(!!value)
|
||||
}
|
||||
>
|
||||
{column.id}
|
||||
{t(
|
||||
columnIdToTranslationKey[
|
||||
column.id as keyof typeof columnIdToTranslationKey
|
||||
] || 'columns.columns'
|
||||
)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})}
|
||||
@ -368,7 +393,7 @@ export function UsersTable({
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
Loading...
|
||||
{t('loading')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows?.length ? (
|
||||
@ -393,7 +418,7 @@ export function UsersTable({
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
{t('noResults')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
@ -407,7 +432,7 @@ export function UsersTable({
|
||||
<div className="flex w-full items-center gap-8 lg:w-fit">
|
||||
<div className="hidden items-center gap-2 lg:flex">
|
||||
<Label htmlFor="rows-per-page" className="text-sm font-medium">
|
||||
Rows per page
|
||||
{t('rowsPerPage')}
|
||||
</Label>
|
||||
<Select
|
||||
value={`${pageSize}`}
|
||||
@ -433,7 +458,8 @@ export function UsersTable({
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex w-fit items-center justify-center text-sm font-medium">
|
||||
Page {pageIndex + 1} of {Math.max(1, Math.ceil(total / pageSize))}
|
||||
{t('page')} {pageIndex + 1} {' / '}
|
||||
{Math.max(1, Math.ceil(total / pageSize))}
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2 lg:ml-0">
|
||||
<Button
|
||||
@ -442,7 +468,7 @@ export function UsersTable({
|
||||
onClick={() => onPageChange(0)}
|
||||
disabled={pageIndex === 0}
|
||||
>
|
||||
<span className="sr-only">Go to first page</span>
|
||||
<span className="sr-only">{t('firstPage')}</span>
|
||||
<ChevronsLeftIcon />
|
||||
</Button>
|
||||
<Button
|
||||
@ -452,7 +478,7 @@ export function UsersTable({
|
||||
onClick={() => onPageChange(pageIndex - 1)}
|
||||
disabled={pageIndex === 0}
|
||||
>
|
||||
<span className="sr-only">Go to previous page</span>
|
||||
<span className="sr-only">{t('previousPage')}</span>
|
||||
<ChevronLeftIcon />
|
||||
</Button>
|
||||
<Button
|
||||
@ -462,7 +488,7 @@ export function UsersTable({
|
||||
onClick={() => onPageChange(pageIndex + 1)}
|
||||
disabled={pageIndex + 1 >= Math.ceil(total / pageSize)}
|
||||
>
|
||||
<span className="sr-only">Go to next page</span>
|
||||
<span className="sr-only">{t('nextPage')}</span>
|
||||
<ChevronRightIcon />
|
||||
</Button>
|
||||
<Button
|
||||
@ -474,7 +500,7 @@ export function UsersTable({
|
||||
}
|
||||
disabled={pageIndex + 1 >= Math.ceil(total / pageSize)}
|
||||
>
|
||||
<span className="sr-only">Go to last page</span>
|
||||
<span className="sr-only">{t('lastPage')}</span>
|
||||
<ChevronsRightIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user