feat(admin): Control Center redesign, Explore dual-view и полный RU/EN i18n
CI / test (push) Failing after 3m9s
CI / push-image (push) Has been skipped
CI / ci-done (push) Failing after 0s

Refs EventHub/EventHubFrontAdmin#32
This commit is contained in:
2026-07-14 17:58:33 +03:00
parent de379e28bf
commit fd2c1f10ad
95 changed files with 12699 additions and 4410 deletions
+153 -37
View File
@@ -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;