feat(admin): Control Center redesign, Explore dual-view и полный RU/EN i18n
Refs EventHub/EventHubFrontAdmin#32
This commit is contained in:
@@ -1,56 +1,172 @@
|
||||
import React from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Card, Descriptions, Spin, Button, Tag } from 'antd';
|
||||
import { useUser } from '../../hooks/useUsers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { useUser } from '@/hooks/useUsers';
|
||||
import { formatDisplayValue } from '@/lib/utils';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { PageHeader } from '@/components/PageHeader';
|
||||
|
||||
const isBadValue = (val: unknown) =>
|
||||
val === '-' || val === 'undefined' || val === '' || val === null || val === undefined;
|
||||
|
||||
const roleBadgeVariant = (role: string) => {
|
||||
if (role === 'user') return 'default' as const;
|
||||
if (role === 'bot') return 'secondary' as const;
|
||||
return 'outline' as const;
|
||||
};
|
||||
|
||||
const statusBadgeVariant = (status: string) => {
|
||||
if (status === 'active') return 'default' as const;
|
||||
if (status === 'frozen') return 'warning' as const;
|
||||
return 'destructive' as const;
|
||||
};
|
||||
|
||||
const DetailValue: React.FC<{ value: unknown; emptyLabel: string }> = ({ value, emptyLabel }) => {
|
||||
const text = formatDisplayValue(value);
|
||||
if (text === '-') return <>{emptyLabel}</>;
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
return <pre className="overflow-auto rounded-md bg-muted p-2 text-xs">{text}</pre>;
|
||||
}
|
||||
return <>{text}</>;
|
||||
};
|
||||
|
||||
const UserDetailPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { data: user, isLoading } = useUser(id || '');
|
||||
|
||||
const isBadValue = (val: any) =>
|
||||
val === '-' || val === 'undefined' || val === '' || val === null || val === undefined;
|
||||
|
||||
const displayValue = (val: any) => (isBadValue(val) ? '-' : val);
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (isBadValue(dateStr)) return '-';
|
||||
if (isBadValue(dateStr)) return t('common.emDash');
|
||||
const d = dayjs(dateStr);
|
||||
return d.isValid() ? d.format('DD.MM.YYYY HH:mm') : dateStr;
|
||||
};
|
||||
|
||||
if (isLoading) return <Spin size="large" style={{ display: 'block', margin: '100px auto' }} />;
|
||||
if (!user) return <p>Пользователь не найден</p>;
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-8 w-64" />
|
||||
<Skeleton className="h-96 w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) return <p className="text-muted-foreground">{t('users.notFound')}</p>;
|
||||
|
||||
const titleName = !isBadValue(user.nickname) ? user.nickname : user.email || user.id;
|
||||
const emDash = t('common.emDash');
|
||||
|
||||
return (
|
||||
<Card title={`Пользователь ${displayValue(user.nickname) !== '-' ? user.nickname : user.email || user.id}`}>
|
||||
<Descriptions bordered column={1}>
|
||||
<Descriptions.Item label="ID">{user.id}</Descriptions.Item>
|
||||
<Descriptions.Item label="Email">{displayValue(user.email)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Ник">{displayValue(user.nickname)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Роль">
|
||||
<Tag color={user.role === 'user' ? 'blue' : 'purple'}>{user.role}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Статус">
|
||||
<Tag color={user.status === 'active' ? 'green' : user.status === 'frozen' ? 'orange' : 'red'}>
|
||||
{user.status}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Причина">{displayValue(user.reason)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Телефон">{displayValue(user.phone)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Язык">{displayValue(user.language)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Часовой пояс">{displayValue(user.timezone)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Аватар URL">{displayValue(user.avatar_url)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Социальные ссылки">{displayValue(user.social_links)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Настройки">{displayValue(user.preferences)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Последний вход">{formatDate(user.last_login)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Создано">{formatDate(user.created_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="Обновлено">{formatDate(user.updated_at)}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Button style={{ marginTop: 16 }} onClick={() => navigate('/users')}>Назад к списку</Button>
|
||||
</Card>
|
||||
<div>
|
||||
<PageHeader
|
||||
title={String(titleName)}
|
||||
description={`ID ${user.id}`}
|
||||
breadcrumbs={[
|
||||
{ label: t('users.title'), href: '/users' },
|
||||
{ label: String(titleName) },
|
||||
]}
|
||||
actions={
|
||||
<Button variant="outline" onClick={() => navigate('/users')}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{t('common.backToList')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Card>
|
||||
<CardContent className="space-y-6 pt-6">
|
||||
<dl className="grid gap-3 text-sm">
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.id')}</dt>
|
||||
<dd>{user.id}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.email')}</dt>
|
||||
<dd>
|
||||
<DetailValue value={user.email} emptyLabel={emDash} />
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.nickname')}</dt>
|
||||
<dd>
|
||||
<DetailValue value={user.nickname} emptyLabel={emDash} />
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.role')}</dt>
|
||||
<dd>
|
||||
<Badge variant={roleBadgeVariant(user.role)}>{user.role}</Badge>
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.status')}</dt>
|
||||
<dd>
|
||||
<Badge variant={statusBadgeVariant(user.status)}>{user.status}</Badge>
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.reason')}</dt>
|
||||
<dd>
|
||||
<DetailValue value={user.reason} emptyLabel={emDash} />
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.phone')}</dt>
|
||||
<dd>
|
||||
<DetailValue value={user.phone} emptyLabel={emDash} />
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.language')}</dt>
|
||||
<dd>
|
||||
<DetailValue value={user.language} emptyLabel={emDash} />
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.timezone')}</dt>
|
||||
<dd>
|
||||
<DetailValue value={user.timezone} emptyLabel={emDash} />
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('profile.avatarUrl')}</dt>
|
||||
<dd>
|
||||
<DetailValue value={user.avatar_url} emptyLabel={emDash} />
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.socialLinks')}</dt>
|
||||
<dd>
|
||||
<DetailValue value={user.social_links} emptyLabel={emDash} />
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('profile.preferencesJson')}</dt>
|
||||
<dd>
|
||||
<DetailValue value={user.preferences} emptyLabel={emDash} />
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.lastLogin')}</dt>
|
||||
<dd>{formatDate(user.last_login)}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.createdAt')}</dt>
|
||||
<dd>{formatDate(user.created_at)}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.updatedAt')}</dt>
|
||||
<dd>{formatDate(user.updated_at)}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserDetailPage;
|
||||
export default UserDetailPage;
|
||||
|
||||
Reference in New Issue
Block a user