173 lines
8.0 KiB
TypeScript
173 lines
8.0 KiB
TypeScript
import React from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
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 formatDate = (dateStr: string) => {
|
|
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 (
|
|
<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 (
|
|
<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;
|