Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/shared/api/contracts/user/UserBaseEntitry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface PlayerBaseEntity {
expiredDate: string;
textureSkinUrl: string;
textureCloakUrl: string;
externalTextureSkinUrl: string;
externalTextureCloakUrl: string;
textureSkinGuid: string;
textureCloakGuid: string;
fullSkinUrl?: any;
Expand Down
270 changes: 145 additions & 125 deletions src/widgets/players-table/lib/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@
import React, { useMemo, useState } from 'react';
import { createColumnHelper } from '@tanstack/table-core';
import { format } from 'date-fns';
import { Ban as BanIcon, GavelIcon, MoreVertical, ShieldCheck, Trash, User, Monitor, Laptop, Smartphone, Tablet } from 'lucide-react';
import {
Ban as BanIcon,
GavelIcon,
Laptop,
Monitor,
MoreVertical,
ShieldCheck,
Smartphone,
Tablet,
Trash,
User,
} from 'lucide-react';

import { DataTableColumnHeader } from '@/entities/Table';
import { PlayerBaseEntity } from '@/shared/api/contracts';
Expand Down Expand Up @@ -129,144 +140,153 @@ function PlayerDetailsDialog({
{/* Tabs layout for player card */}
<div className="w-full">
<Tabs defaultValue="overview" className="w-full">
<TabsList className="w-full flex flex-wrap gap-2" >
<TabsTrigger value="overview">Обзор</TabsTrigger>
<TabsTrigger value="textures">Текстуры</TabsTrigger>
<TabsTrigger value="auth">Авторизации</TabsTrigger>
<TabsTrigger value="network">Сети/IP</TabsTrigger>
<TabsTrigger value="server">Сервер</TabsTrigger>
</TabsList>
<TabsList className="w-full flex flex-wrap gap-2">
<TabsTrigger value="overview">Обзор</TabsTrigger>
<TabsTrigger value="textures">Текстуры</TabsTrigger>
<TabsTrigger value="auth">Авторизации</TabsTrigger>
<TabsTrigger value="network">Сети/IP</TabsTrigger>
<TabsTrigger value="server">Сервер</TabsTrigger>
</TabsList>

<div className="mt-3 min-h-[320px] max-h-[480px] overflow-auto">
<TabsContent value="overview">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-3">
<div>
<div className="text-sm text-muted-foreground">Статус</div>
<div className="mt-1 flex items-center gap-2">
{player.isBanned ? (
<span className="text-red-500 flex items-center gap-1">
<BanIcon size={16} /> Заблокирован
</span>
) : (
<span className="text-emerald-600 flex items-center gap-1">
<ShieldCheck size={16} /> Не заблокирован
</span>
<div className="mt-3 min-h-[320px] max-h-[480px] overflow-auto">
<TabsContent value="overview">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-3">
<div>
<div className="text-sm text-muted-foreground">Статус</div>
<div className="mt-1 flex items-center gap-2">
{player.isBanned ? (
<span className="text-red-500 flex items-center gap-1">
<BanIcon size={16} /> Заблокирован
</span>
) : (
<span className="text-emerald-600 flex items-center gap-1">
<ShieldCheck size={16} /> Не заблокирован
</span>
)}
</div>
{player.isLauncherStarted && (
<div className="mt-2 relative flex items-center gap-x-2">
<span className="flex items-center justify-center min-w-3 min-h-3 max-w-3 max-h-3 rounded-full after:flex after:rounded-full after:min-w-5 after:min-h-5 bg-green-600 after:opacity-30 after:bg-green-600"></span>
<span className="text-xs font-medium text-emerald-700">
Лаунчер запущен
</span>
</div>
)}
</div>
<div>
<div className="text-sm text-muted-foreground">Сессия истекает</div>
<div className="mt-1">{sessionStr}</div>
</div>
</div>
<div className="space-y-3">
{addresses.length > 0 && (
<div>
<div className="text-sm text-muted-foreground">IP адреса</div>
<div className="mt-1 text-sm break-words">{addresses.join(', ')}</div>
</div>
)}
</div>
{player.isLauncherStarted && (
<div className="mt-2 relative flex items-center gap-x-2">
<span className="flex items-center justify-center min-w-3 min-h-3 max-w-3 max-h-3 rounded-full after:flex after:rounded-full after:min-w-5 after:min-h-5 bg-green-600 after:opacity-30 after:bg-green-600"></span>
<span className="text-xs font-medium text-emerald-700">Лаунчер запущен</span>
</div>
)}
</div>
</TabsContent>

<TabsContent value="textures">
<div>
<div className="text-sm text-muted-foreground">Сессия истекает</div>
<div className="mt-1">{sessionStr}</div>
<div className="text-sm text-muted-foreground">Текстуры</div>
<div className="mt-2 flex items-center gap-3">
{player.externalTextureSkinUrl && (
<img
src={player.externalTextureSkinUrl}
alt="skin"
className="h-16 w-16 rounded border object-cover"
/>
)}
{player.externalTextureCloakUrl && (
<img
src={player.externalTextureCloakUrl}
alt="cloak"
className="h-16 w-16 rounded border object-cover"
/>
)}
{!player.externalTextureSkinUrl && !player.externalTextureCloakUrl && (
<div className="text-sm text-muted-foreground">Нет текстур.</div>
)}
</div>
</div>
</div>
<div className="space-y-3">
{addresses.length > 0 && (
<div>
<div className="text-sm text-muted-foreground">IP адреса</div>
<div className="mt-1 text-sm break-words">{addresses.join(', ')}</div>
</TabsContent>

<TabsContent value="auth">
<div>
<div className="text-sm text-muted-foreground">Авторизации</div>
<div className="mt-2 flex flex-col gap-2 pr-1">
{(player.authHistory || []).map((h, idx) => (
<div key={idx} className="flex items-start gap-3 rounded-md border p-3">
<div className="mt-0.5">{getDeviceIcon(h.device)}</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 min-w-0">
<div
className="font-medium truncate"
title={h.device || 'Неизвестное устройство'}
style={{ maxWidth: '100%' }}
>
{h.device || 'Неизвестное устройство'}
</div>
{h.hwid && (
<span
className="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground truncate max-w-[180px]"
title={h.hwid}
>
HWID: {h.hwid}
</span>
)}
</div>
<div className="mt-1 text-sm font-mono break-words">
{h.address || '-'}
</div>
<div className="mt-1 text-xs text-muted-foreground flex items-center gap-2 flex-wrap">
<span>{timeAgo(h.date)}</span>
<span className="px-1.5 py-0.5 rounded bg-muted text-muted-foreground">
{h.protocol}
</span>
</div>
</div>
</div>
))}
{(!player.authHistory || player.authHistory.length === 0) && (
<div className="text-sm text-muted-foreground">Нет данных.</div>
)}
</div>
)}
</div>
</div>
</TabsContent>
</div>
</TabsContent>

<TabsContent value="textures">
<div>
<div className="text-sm text-muted-foreground">Текстуры</div>
<div className="mt-2 flex items-center gap-3">
{player.textureSkinUrl && (
<img
src={player.textureSkinUrl}
alt="skin"
className="h-16 w-16 rounded border object-cover"
/>
)}
{player.textureCloakUrl && (
<img
src={player.textureCloakUrl}
alt="cloak"
className="h-16 w-16 rounded border object-cover"
/>
)}
{!player.textureSkinUrl && !player.textureCloakUrl && (
<div className="text-sm text-muted-foreground">Нет текстур.</div>
<TabsContent value="network">
{addresses.length > 0 ? (
<div className="flex flex-wrap gap-2">
{addresses.map((a) => (
<span
key={a}
className="px-2 py-1 rounded bg-muted text-xs text-muted-foreground"
>
{a}
</span>
))}
</div>
) : (
<div className="text-sm text-muted-foreground">Нет IP адресов.</div>
)}
</div>
</div>
</TabsContent>
</TabsContent>

<TabsContent value="auth">
<div>
<div className="text-sm text-muted-foreground">Авторизации</div>
<div className="mt-2 flex flex-col gap-2 pr-1">
{(player.authHistory || []).map((h, idx) => (
<div key={idx} className="flex items-start gap-3 rounded-md border p-3">
<div className="mt-0.5">
{getDeviceIcon(h.device)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 min-w-0">
<div className="font-medium truncate" title={h.device || 'Неизвестное устройство'} style={{maxWidth:'100%'}}>
{h.device || 'Неизвестное устройство'}
</div>
{h.hwid && (
<span
className="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground truncate max-w-[180px]"
title={h.hwid}
>
HWID: {h.hwid}
</span>
)}
</div>
<div className="mt-1 text-sm font-mono break-words">
{h.address || '-'}
</div>
<div className="mt-1 text-xs text-muted-foreground flex items-center gap-2 flex-wrap">
<span>{timeAgo(h.date)}</span>
<span className="px-1.5 py-0.5 rounded bg-muted text-muted-foreground">{h.protocol}</span>
</div>
</div>
<TabsContent value="server">
{Array.isArray((player as any).serverJoinHistory) ? (
<div className="text-sm text-muted-foreground">
{(player as any).serverJoinHistory?.length || 0} записей
</div>
))}
{(!player.authHistory || player.authHistory.length === 0) && (
) : (
<div className="text-sm text-muted-foreground">Нет данных.</div>
)}
</div>
</TabsContent>
</div>
</TabsContent>

<TabsContent value="network">
{addresses.length > 0 ? (
<div className="flex flex-wrap gap-2">
{addresses.map((a) => (
<span key={a} className="px-2 py-1 rounded bg-muted text-xs text-muted-foreground">
{a}
</span>
))}
</div>
) : (
<div className="text-sm text-muted-foreground">Нет IP адресов.</div>
)}
</TabsContent>

<TabsContent value="server">
{Array.isArray((player as any).serverJoinHistory) ? (
<div className="text-sm text-muted-foreground">
{(player as any).serverJoinHistory?.length || 0} записей
</div>
) : (
<div className="text-sm text-muted-foreground">Нет данных.</div>
)}
</TabsContent>
</div>
</Tabs>
</Tabs>
</div>
</DialogContent>
</Dialog>
Expand Down