add admin pages layouts

This commit is contained in:
smanylov 2026-01-22 17:30:46 +07:00
parent 2de0cc444d
commit e406833057
22 changed files with 3533 additions and 256 deletions

View File

@ -0,0 +1,37 @@
export default function Page() {
return (
<div className="admin-content">
<div className="content-header">
<h3>📈 Аналитика</h3>
<div className="content-header-actions">
<select id="analytics-period">
<option value="today">Сегодня</option>
<option value="week">Неделя</option>
<option value="month">Месяц</option>
<option value="year">Год</option>
</select>
<button className="btn btn-primary">
<span>📊</span>
<span>Сгенерировать отчет</span>
</button>
</div>
</div>
<div className="content-body">
<div className="chart-container">
<h4>Регистрации пользователей</h4>
<canvas id="registrations-chart" width="400" height="100"></canvas>
</div>
<div className="chart-container">
<h4>Загрузка контента</h4>
<canvas id="content-chart" width="400" height="100"></canvas>
</div>
<div className="chart-container">
<h4>Доходы</h4>
<canvas id="revenue-chart" width="400" height="100"></canvas>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,22 @@
export default function Page() {
return (
<div className="admin-content">
<div className="content-header">
<h3>🔌 Управление API</h3>
<div className="content-header-actions">
<button className="btn btn-primary">
<span></span>
<span>Создать API ключ</span>
</button>
</div>
</div>
<div className="content-body" id="api-container">
<div className="empty-state">
<div className="empty-state-icon">🔌</div>
<h4>Нет API ключей</h4>
<p>Создайте первый API ключ для интеграции</p>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,29 @@
export default function Page() {
return (
<div className="admin-content">
<div className="content-header">
<h3>Управление контентом</h3>
<div className="content-header-actions">
<div className="search-box">
<span>🔍</span>
<input type="text" id="content-search" placeholder="Поиск контента..." />
</div>
<select id="content-type-filter">
<option value="all">Все типы</option>
<option value="image">Изображения</option>
<option value="video">Видео</option>
<option value="audio">Аудио</option>
<option value="document">Документы</option>
</select>
</div>
</div>
<div className="content-body" id="content-container">
<div className="empty-state">
<div className="empty-state-icon">🗂</div>
<h4>Контент не найден</h4>
<p>Попробуйте изменить параметры поиска</p>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,78 @@
export default function Page() {
return (
<div className="admin-content">
<div className="content-header">
<h3>DMCA жалобы</h3>
<div className="content-header-actions">
<button className="btn btn-primary">
<span>🔗</span>
<span>Открыть полную панель DMCA</span>
</button>
</div>
</div>
<div className="content-body">
<div className="stats-grid">
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Всего жалоб</span>
<div className="stat-card-icon blue">📤</div>
</div>
<div className="stat-card-value">3</div>
<div className="stat-card-label">Создано жалоб</div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Ожидают</span>
<div className="stat-card-icon orange"></div>
</div>
<div className="stat-card-value">0</div>
<div className="stat-card-label">На рассмотрении</div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Отправлено</span>
<div className="stat-card-icon cyan">📨</div>
</div>
<div className="stat-card-value">1</div>
<div className="stat-card-label">Отправлено платформам</div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Успешно</span>
<div className="stat-card-icon green"></div>
</div>
<div className="stat-card-value">0</div>
<div className="stat-card-label">Успешно обработано</div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Ошибки</span>
<div className="stat-card-icon red"></div>
</div>
<div className="stat-card-value">0</div>
<div className="stat-card-label">С ошибками</div>
</div>
</div>
<div>
<h4>Быстрые действия</h4>
<div>
<button className="btn btn-primary">
Все жалобы
</button>
<button className="btn btn-secondary">
Создать жалобу
</button>
<button className="btn btn-outline">
Ожидающие
</button>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,49 @@
export default function Page() {
return (
<div className="admin-content">
<div className="content-header">
<h3>Email уведомления</h3>
<div className="content-header-actions">
<button className="btn btn-primary">
<span>🔗</span>
<span>Открыть панель Email</span>
</button>
</div>
</div>
<div className="content-body">
<div className="content-body-section">
<h4>Еженедельные отчеты</h4>
<p>
Отправка еженедельных email-отчетов пользователям о мониторинге авторских прав
</p>
<div className="flex gap-4">
<button className="btn btn-primary">
Управление отчетами
</button>
<button className="btn btn-secondary">
Предпросмотр
</button>
</div>
</div>
<div className="content-body-section">
<h4>Массовая рассылка</h4>
<form>
<div className="form-group">
<label>Тема письма</label>
<input type="text" id="broadcast-subject" placeholder="Введите тему письма" />
</div>
<div className="form-group">
<label>Сообщение</label>
<textarea id="broadcast-message" placeholder="Введите текст сообщения"></textarea>
</div>
<button type="submit" className="btn btn-success">
<span>📧</span>
<span>Отправить всем пользователям</span>
</button>
</form>
</div>
</div>
</div>
)
}

View File

@ -6,7 +6,7 @@ import { getQueryClient } from '@/app/providers/getQueryClient';
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { prefetchLayoutQueries } from '@/app/lib/prefetch-queries';
import { routing } from '@/i18n/routing';
import AdminNavLinks from '@/app/ui/admin-nav-links'
import AdminNavLinks from '@/app/ui/navigation/admin-nav-links'
import AdminHeaderPanel from '@/app/ui/admin-header-panel';
import "../styles/globals.css";

View File

@ -1,7 +1,12 @@
import SubscriptionTable from '@/app/ui/subscriptions/subscriptions-table';
export default function Page() {
return (
<div>
subscriptions-page
<div className="admin-content">
<div className="content-header">
<h3>Управление подписками</h3>
</div>
<SubscriptionTable />
</div>
)
}

View File

@ -0,0 +1,11 @@
import SystemLogsTable from '@/app/ui/system-logs/system-logs-table';
export default function Page() {
return (
<div className="admin-content">
<div className="content-header">
<h3>Системные логи</h3>
</div>
<SystemLogsTable />
</div>
)
}

View File

@ -0,0 +1,88 @@
export default function Page() {
return (
<div className="admin-content">
<div className="content-header">
<h3> Системные настройки</h3>
</div>
<div className="content-body">
<div className="stats-grid">
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">База данных</span>
<div className="stat-card-icon blue">💾</div>
</div>
<div className="stat-card-value" id="db-size">12.55</div>
<div className="stat-card-label">Размер БД (MB)</div>
<div className="stat-card-footer">
<button className="btn btn-outline btn-sm">
Оптимизировать
</button>
</div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">PHP Version</span>
<div className="stat-card-icon green">🐘</div>
</div>
<div className="stat-card-value" id="php-version">8.2.29</div>
<div className="stat-card-label">Версия PHP</div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">MySQL Version</span>
<div className="stat-card-icon orange">🐬</div>
</div>
<div className="stat-card-value" id="mysql-version">5.7.44-48</div>
<div className="stat-card-label">Версия MySQL</div>
</div>
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Свободное место</span>
<div className="stat-card-icon purple">💿</div>
</div>
<div className="stat-card-value" id="disk-free">767.02</div>
<div className="stat-card-label">GB доступно</div>
</div>
</div>
<div className="system-grid-wrapper">
<div className="content-body-section">
<h4>Обслуживание системы</h4>
<div className="system-action-list">
<button className="btn btn-primary">
<span>Проверка здоровья системы</span>
</button>
<button className="btn btn-secondary">
<span>Оптимизация БД</span>
</button>
<button className="btn btn-success">
<span>Создать резервную копию</span>
</button>
<button className="btn btn-warning">
<span>Очистка системы</span>
</button>
</div>
</div>
<div className="content-body-section">
<h4>Обновления и мониторинг</h4>
<div className="system-action-list">
<button className="btn btn-primary">
<span>Проверить обновления</span>
</button>
<button className="btn btn-primary">
<span>Обновить статистику</span>
</button>
<button className="btn btn-primary">
<span>Экспорт всех данных</span>
</button>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -2,7 +2,7 @@ export default function Page() {
return (
<div className="admin-content">
<div className="content-header">
<h3>📋 Управление тарифными планами</h3>
<h3>Управление тарифными планами</h3>
<div className="content-header-actions">
<button className="btn btn-primary">
<span></span>

View File

@ -1,8 +1,10 @@
import { UsersTable } from '@/app/ui/users/users-table';
export default async function Page() {
return (
<div>
users-page
<div className="admin-content">
<div className="content-header">
<h3>Управление пользователями</h3>
</div>
<UsersTable />
</div>
)

View File

@ -0,0 +1,12 @@
import ViolationsTable from '@/app/ui/violations/violations-table';
export default async function Page() {
return (
<div className="admin-content">
<div className="content-header">
<h3>Управление нарушениями</h3>
</div>
<ViolationsTable />
</div>
)
}

View File

@ -4,9 +4,12 @@
--primary-color: #2563eb;
--primary-hover: #1d4ed8;
--secondary-color: #64748b;
--secondary-hover: #4c5869;
--success-color: #10b981;
--success-hover: #0d8a60;
--danger-color: #ef4444;
--warning-color: #f59e0b;
--warning-hover: #b3750b;
--info-color: #3b82f6;
--background: #f8fafc;
--surface: #ffffff;
@ -210,11 +213,44 @@
padding: 24px;
max-width: 100%;
overflow-x: hidden;
min-height: 100vh;
background: var(--background);
}
.btn-primary {
background: var(--primary-color);
color: white;
&:hover {
background: var(--primary-hover);
}
}
.btn-secondary {
background: var(--secondary-color);
color: white;
&:hover {
background: var(--secondary-hover);
}
}
.btn-success {
background: var(--success-color);
color: white;
&:hover {
background: var(--success-hover);
}
}
.btn-warning {
background: var(--warning-color);
color: white;
&:hover {
background: var(--warning-hover);
}
}
.btn {
@ -247,23 +283,172 @@
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.stat-card {
background: var(--surface);
padding: 20px;
border-radius: 12px;
box-shadow: var(--shadow);
transition: all 0.3s;
border: 1px solid var(--border);
.users-top {
&-user {
font-weight: 500;
}
.stat-card {
background: var(--surface);
padding: 20px;
&-mail {
font-size: 12px;
color: var(--text-secondary);
}
}
}
.admin-content {
background: var(--surface);
border-radius: 12px;
box-shadow: var(--shadow);
overflow: hidden;
.content-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
&-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
h3 {
font-size: 18px;
font-weight: 600;
}
}
.content-body {
padding: 24px;
max-width: 100%;
overflow-x: auto;
}
.content-body-section {
padding: 20px;
background: var(--background);
border-radius: 8px;
margin-bottom: 20px;
h4,
p {
margin-bottom: 12px;
}
p {
color: var(--text-secondary);
}
}
.chart-container {
background: var(--surface);
padding: 24px;
border-radius: 12px;
box-shadow: var(--shadow);
margin-bottom: 24px;
h4 {
font-size: 16px;
font-weight: 600;
margin-bottom: 20px;
color: var(--text-primary);
}
}
.admin-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
min-width: 800px;
thead {
background: var(--background);
}
th {
padding: 12px 16px;
text-align: left;
font-weight: 600;
color: var(--text-secondary);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 2px solid var(--border);
}
td {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
color: var(--text-primary);
}
tbody tr {
transition: background 0.2s;
}
.badge {
padding: 4px 12px;
border-radius: 12px;
box-shadow: var(--shadow);
transition: all 0.3s;
border: 1px solid var(--border);
font-size: 12px;
font-weight: 500;
display: inline-block;
}
.badge-primary {
background: #eff6ff;
color: var(--primary-color);
}
}
}
.form-group {
margin-bottom: 20px;
label {
display: block;
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 8px;
}
input,
textarea {
width: 100%;
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s;
background: #fff;
}
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 24px;
.stat-card {
background: var(--surface);
padding: 20px;
border-radius: 12px;
box-shadow: var(--shadow);
transition: all 0.3s;
border: 1px solid var(--border);
&:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
.stat-card-header {
@ -273,6 +458,26 @@
margin-bottom: 12px;
}
.stat-card-title {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-card-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
background: #eff6ff;
}
.stat-card-value {
font-size: 32px;
font-weight: 700;
@ -292,96 +497,15 @@
font-size: 12px;
color: var(--text-secondary);
}
}
.admin-content {
background: var(--surface);
border-radius: 12px;
box-shadow: var(--shadow);
overflow: hidden;
.btn-outline {
background: transparent;
border: 1px solid var(--border);
color: var(--text-primary);
.content-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
h3 {
font-size: 18px;
font-weight: 600;
}
}
.content-body {
padding: 24px;
max-width: 100%;
overflow-x: auto;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.admin-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
min-width: 800px;
thead {
&:hover {
background: var(--background);
}
th {
padding: 12px 16px;
text-align: left;
font-weight: 600;
color: var(--text-secondary);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 2px solid var(--border);
}
td {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
color: var(--text-primary);
}
tbody tr {
transition: background 0.2s;
}
.badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
display: inline-block;
}
.badge-primary {
background: #eff6ff;
color: var(--primary-color);
}
}
}
.users-top {
&-user {
font-weight: 500;
}
&-mail {
font-size: 12px;
color: var(--text-secondary);
}
}
}
@ -741,77 +865,38 @@
}
}
#plans-container {
.stats-grid {
.stat-card {
background: var(--surface);
padding: 20px;
border-radius: 12px;
box-shadow: var(--shadow);
transition: all 0.3s;
border: 1px solid var(--border);
&:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary);
.stat-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
p {
font-size: 14px;
}
.stat-card-title {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
h4 {
font-size: 18px;
margin-bottom: 8px;
color: var(--text-primary);
}
.stat-card-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
.empty-state-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.5;
}
}
background: #eff6ff;
}
.system-grid-wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-top: 24px;
.stat-card-value {
font-size: 32px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 4px;
}
.stat-card-label {
font-size: 12px;
color: var(--text-secondary);
}
.stat-card-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border);
font-size: 12px;
color: var(--text-secondary);
}
.btn-outline {
background: transparent;
border: 1px solid var(--border);
color: var(--text-primary);
&:hover {
background: var(--background);
}
}
}
.system-action-list {
display: flex;
flex-direction: column;
gap: 12px;
}
}

View File

@ -1,80 +0,0 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import clsx from 'clsx';
import { useTranslations } from 'next-intl';
export default function AdminNavLinks() {
const pathname = usePathname().replace('/en/', '/').replace('/ru/', '/');
const t = useTranslations('Global');
const links = [
{
name: 'dashboard',
href: '/',
img: 'M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z'
},
{
name: 'users',
href: '/users',
img: 'M16 17v2H2v-2s0-4 7-4s7 4 7 4m-3.5-9.5A3.5 3.5 0 1 0 9 11a3.5 3.5 0 0 0 3.5-3.5m3.44 5.5A5.32 5.32 0 0 1 18 17v2h4v-2s0-3.63-6.06-4M15 4a3.4 3.4 0 0 0-1.93.59a5 5 0 0 1 0 5.82A3.4 3.4 0 0 0 15 11a3.5 3.5 0 0 0 0-7'
},
{
name: 'subscriptions',
href: '/subscriptions',
img: 'M22 6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2zm-2 2H4V6h16zM4 11h16v7H4z'
},
{
name: 'tariff management',
href: '/tariff-management',
img: 'M4 14v-2h7v2zm0-4V8h11v2zm0-4V4h11v2zm9 14v-3.075l5.525-5.5q.225-.225.5-.325t.55-.1q.3 0 .575.113t.5.337l.925.925q.2.225.313.5t.112.55t-.1.563t-.325.512l-5.5 5.5zm6.575-5.6l.925-.975l-.925-.925l-.95.95z'
}
];
return (
<div className=''>
<nav className="admin-sidebar">
<div className="admin-logo">
<h1>NO COPY</h1>
<p>Админ-панель</p>
</div>
<ul className="admin-nav">
{links.map((link) => {
return (
<Link
key={link.name}
href={link.href}
className={clsx(
`admin-nav-item`,
{
'active': pathname === link.href,
},
)}
>
<svg viewBox="0 0 24 24" fill="currentColor">
<path d={link.img}></path>
</svg>
<p className="">{link.name}</p>
</Link>
);
})}
<Link
key={t('exit')}
href='/login'
className="admin-nav-item"
onClick={(e) => {
e.preventDefault();
}}
>
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"></path>
</svg>
<p className="">{t('exit')}</p>
</Link>
</ul>
</nav>
</div>
);
}

View File

@ -1,6 +1,6 @@
export default function DashboardGeneralInfo() {
return (
<div className="dashboard-general-info">
<div className="stats-grid">
<div className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">Всего пользователей</span>

View File

@ -0,0 +1,120 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import clsx from 'clsx';
import { useTranslations } from 'next-intl';
export default function AdminNavLinks() {
const pathname = usePathname().replace('/en/', '/').replace('/ru/', '/');
const t = useTranslations('Global');
const links = [
{
name: 'dashboard',
href: '/',
img: 'M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z'
},
{
name: 'users',
href: '/users',
img: 'M16 17v2H2v-2s0-4 7-4s7 4 7 4m-3.5-9.5A3.5 3.5 0 1 0 9 11a3.5 3.5 0 0 0 3.5-3.5m3.44 5.5A5.32 5.32 0 0 1 18 17v2h4v-2s0-3.63-6.06-4M15 4a3.4 3.4 0 0 0-1.93.59a5 5 0 0 1 0 5.82A3.4 3.4 0 0 0 15 11a3.5 3.5 0 0 0 0-7'
},
{
name: 'subscriptions',
href: '/subscriptions',
img: 'M22 6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2zm-2 2H4V6h16zM4 11h16v7H4z'
},
{
name: 'tariff management',
href: '/tariff-management',
img: 'M4 14v-2h7v2zm0-4V8h11v2zm0-4V4h11v2zm9 14v-3.075l5.525-5.5q.225-.225.5-.325t.55-.1q.3 0 .575.113t.5.337l.925.925q.2.225.313.5t.112.55t-.1.563t-.325.512l-5.5 5.5zm6.575-5.6l.925-.975l-.925-.925l-.95.95z'
},
{
name: 'content',
href: '/content',
img: 'M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h6l2 2h8q.825 0 1.413.588T22 8v10q0 .825-.587 1.413T20 20zm0-2h16V8h-8.825l-2-2H4zm0 0V6z'
},
{
name: 'violations',
href: '/violations',
img: 'M1 21L12 2l11 19zm3.45-2h15.1L12 6zM12 18q.425 0 .713-.288T13 17t-.288-.712T12 16t-.712.288T11 17t.288.713T12 18m-1-3h2v-5h-2zm1-2.5'
},
{
name: 'dmc',
href: '/dmc',
img: 'M12 20a8 8 0 1 0 0-16a8 8 0 0 0 0 16m0 2C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m-1-6h2v2h-2zm0-10h2v8h-2z'
},
{
name: 'email notification',
href: '/email-notification',
img: 'M2 20V4h20v16zm10-7L4 8v10h16V8zm0-2l8-5H4zM4 8V6v12z'
},
{
name: 'api control',
href: '/api-control',
img: 'M13.26 10.5h2v1h-2zM8.4 15L8 13.77H6.06L5.62 15H4l2.2-6h1.62L10 15Zm8.36-3.5a1.47 1.47 0 0 1-1.5 1.5h-2v2h-1.5V9h3.5a1.47 1.47 0 0 1 1.5 1.5ZM20 15h-1.5V9H20ZM6.43 12.77h1.16l-.58-1.59zM20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2'
},
{
name: 'analitic',
href: '/analitic',
img: 'm16 11.78l4.24-7.33l1.73 1l-5.23 9.05l-6.51-3.75L5.46 19H22v2H2V3h2v14.54L9.5 8z'
},
{
name: 'system logs',
href: '/system-logs',
img: 'M4.5 2.25v19.5h15V7.195l-.21-.235l-4.5-4.5l-.236-.21H4.5zm1.5 1.5h7.5v4.5h4.5v12H6zm9 1.078L16.922 6.75H15zM8.25 9.75v1.5h7.5v-1.5zm0 3v1.5h7.5v-1.5zm0 3v1.5h7.5v-1.5z'
},
{
name: 'system',
href: '/system',
img: 'M12 8a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 2a2 2 0 0 0-2 2a2 2 0 0 0 2 2a2 2 0 0 0 2-2a2 2 0 0 0-2-2m-2 12c-.25 0-.46-.18-.5-.42l-.37-2.65c-.63-.25-1.17-.59-1.69-.99l-2.49 1.01c-.22.08-.49 0-.61-.22l-2-3.46a.493.493 0 0 1 .12-.64l2.11-1.66L4.5 12l.07-1l-2.11-1.63a.493.493 0 0 1-.12-.64l2-3.46c.12-.22.39-.31.61-.22l2.49 1c.52-.39 1.06-.73 1.69-.98l.37-2.65c.04-.24.25-.42.5-.42h4c.25 0 .46.18.5.42l.37 2.65c.63.25 1.17.59 1.69.98l2.49-1c.22-.09.49 0 .61.22l2 3.46c.13.22.07.49-.12.64L19.43 11l.07 1l-.07 1l2.11 1.63c.19.15.25.42.12.64l-2 3.46c-.12.22-.39.31-.61.22l-2.49-1c-.52.39-1.06.73-1.69.98l-.37 2.65c-.04.24-.25.42-.5.42zm1.25-18l-.37 2.61c-1.2.25-2.26.89-3.03 1.78L5.44 7.35l-.75 1.3L6.8 10.2a5.55 5.55 0 0 0 0 3.6l-2.12 1.56l.75 1.3l2.43-1.04c.77.88 1.82 1.52 3.01 1.76l.37 2.62h1.52l.37-2.61c1.19-.25 2.24-.89 3.01-1.77l2.43 1.04l.75-1.3l-2.12-1.55c.4-1.17.4-2.44 0-3.61l2.11-1.55l-.75-1.3l-2.41 1.04a5.42 5.42 0 0 0-3.03-1.77L12.75 4z'
}
];
return (
<div className=''>
<nav className="admin-sidebar">
<div className="admin-logo">
<h1>NO COPY</h1>
<p>Админ-панель</p>
</div>
<ul className="admin-nav">
{links.map((link) => {
return (
<Link
key={link.name}
href={link.href}
className={clsx(
`admin-nav-item`,
{
'active': pathname === link.href,
},
)}
>
<svg viewBox="0 0 24 24" fill="currentColor">
<path d={link.img} />
</svg>
<p className="">{link.name}</p>
</Link>
);
})}
<Link
key={t('exit')}
href='/login'
className="admin-nav-item"
onClick={(e) => {
e.preventDefault();
}}
>
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"></path>
</svg>
<p className="">{t('exit')}</p>
</Link>
</ul>
</nav>
</div>
);
}

View File

@ -102,7 +102,7 @@ const cutFileName = (userId: string) => {
return nameWithoutExtension.substring(0, maxNameLength) + '...' + extension;
}
export default function TanstakFilesTable() {
export default function SubscriptionTable() {
const {
data: tableData,
isLoading,

View File

@ -0,0 +1,940 @@
'use client';
import { useState, useMemo, useEffect, ReactNode } from 'react';
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getPaginationRowModel,
getFilteredRowModel,
ColumnDef,
SortingState,
ColumnFiltersState,
} from '@tanstack/react-table';
import { IconImageFile, IconVideoFile, IconAudioFile, IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconShieldExclamation, IconDelete } from '@/app/ui/icons/icons';
import { useTranslations, useLocale } from 'next-intl';
import DropDownList from '@/app/components/dropDownList';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import ModalWindow from '@/app/components/modalWindow';
import { toast } from 'sonner';
import { pluralize } from '@/app/lib/pluralize';
type FileItem = {
id: string;
userId: string;
userName: string;
userEmail?: number | undefined;
userCompany?: number | undefined;
userSubscription?: number;
_original?: ApiFile;
};
type ApiFile = {
id: string;
originalFileName: string;
mimeType: string;
userEmail: number;
userCompany: number;
updatedAt: string;
};
type ApiResponse = any;
// Иконки для типов файлов
const FileTypeIcon = ({ type }: { type: string }) => {
switch (type) {
case 'image':
return <span className="color-image">
<IconImageFile />
</span>;
case 'video':
return <span className="color-video">
<IconVideoFile />
</span>;
case 'audio':
return <span className="color-audio">
<IconAudioFile />
</span>;
default:
return <span>📄</span>;
}
};
// Форматирование даты из timestamp
const formatDate = (timestamp: number) => {
const date = new Date(timestamp);
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`;
};
const formatDateTime = (timestamp: number) => {
const date = new Date(timestamp);
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
};
const cutFileName = (userId: string) => {
const MAX_FILE_NAME_LENGTH = 26;
const lastDotIndex = userId.lastIndexOf('.');
if (lastDotIndex <= 0) {
return userId.length > MAX_FILE_NAME_LENGTH ? userId.substring(0, MAX_FILE_NAME_LENGTH - 3) + '...' : userId;
}
const nameWithoutExtension = userId.substring(0, lastDotIndex);
const extension = userId.substring(lastDotIndex);
if (userId.length <= MAX_FILE_NAME_LENGTH) {
return userId;
}
const maxNameLength = MAX_FILE_NAME_LENGTH - extension.length - 3;
if (maxNameLength <= 0) {
return '...' + extension;
}
return nameWithoutExtension.substring(0, maxNameLength) + '...' + extension;
}
export default function SystemLogsTable() {
const {
data: tableData,
isLoading,
isError,
error,
} = useQuery<ApiResponse, Error, any>({
queryKey: ['userFilesData'],
queryFn: () => {
return []
},
select: (data: ApiResponse): any => {
if (!data?.files) return [
{
id: 1,
userId: 1,
userName: 'userName',
userEmail: 'userEmail',
userCompany: 'userCompany',
userSubscription: 'userSubscription',
userRole: 'role',
userContent: 0,
}
];
return data.files.map((item: ApiFile) => {
const [datePart, timePart] = item.updatedAt.split(' ');
const [day, month, year] = datePart.split('-').map(Number);
const [hours, minutes, seconds] = timePart.split(':').map(Number);
const newDate = new Date(year, month - 1, day, hours, minutes, seconds).getTime();
return {
id: item.id,
userId: item.originalFileName,
userName: item.mimeType.toLocaleLowerCase(),
userEmail: item.userEmail,
userCompany: item.userCompany,
userSubscription: newDate,
userRole: 'role',
userContent: 0,
_original: item
};
});
},
});
const queryClient = useQueryClient();
// Состояния
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [dateFilter, setDateFilter] = useState<string>('all');
const [typeFilter, setTypeFilter] = useState<string>('all');
const [searchQuery, setSearchQuery] = useState('');
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 10,
});
const [openWindow, setOpenWindow] = useState<boolean>(false);
const [openWindowChildren, setOpenWindowChildren] = useState<ReactNode>(null);
const [isFileLoading, setIsFileLoading] = useState(false);
const t = useTranslations("Global");
const locale = useLocale();
// Определение колонок
const columns = useMemo<ColumnDef<FileItem>[]>(
() => [
{
accessorKey: 'userId',
header: ({ column }) => (
<div className="column start">
<span>
ID
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => (
<div className="flex items-center space-x-2">
<div>
<div className="font-medium w-full truncate" title={row.original.userId}>
{/* {row.original.userId} */}
{row.original.userId}
</div>
</div>
</div>
),
enableColumnFilter: false,
},
{
accessorKey: 'userName',
header: ({ column }) => (
<div className="column">
<span>
Пользователь
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
return (
<div className={`text-center font-semibold`}>
User name
</div>
)
},
},
{
accessorKey: 'userEmail',
header: ({ column }) => (
<div className="column">
<span>
email
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
let classCollor = () => {
let result = ''
if (row.original.userEmail !== undefined) {
result = row.original.userEmail > 0 ? 'text-red-600' : 'text-green-600'
}
return result;
}
return (
<div className={`text-center font-semibold ${classCollor()}`}>
{row.original.userEmail !== undefined ? row.original.userEmail : '-'}
</div>
)
},
},
{
accessorKey: 'userCompany',
header: ({ column }) => (
<div className="column">
<span>
Компания
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => (
<div className="text-center">
{row.original.userCompany !== undefined ? row.original.userCompany : '-'}
</div>
),
},
{
accessorKey: 'userSubscription',
header: ({ column }) => (
<div className="column">
<span>
Подписка
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
return (
<div className="text-center">
{row.original.userSubscription ? (
<>
{formatDate(row.original.userSubscription)}
<br />
{formatDateTime(row.original.userSubscription)}
</>
) : (
<div>-</div>
)}
</div>
)
},
enableColumnFilter: false,
},
{
accessorKey: 'userRole',
header: ({ column }) => (
<div className="column">
<span>
Роль
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
return (
<div className={`text-center font-semibold`}>
User Роль
</div>
)
},
}, {
accessorKey: 'userContent',
header: ({ column }) => (
<div className="column">
<span>
Контент
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
return (
<div className={`text-center font-semibold`}>
User Роль
</div>
)
},
},
{
id: 'actions',
header: () => {
return (
<div className="column">
{t('actions')}
</div>
)
},
cell: ({ row }) => (
<div className="actions">
<div className="actions-group">
<button
onClick={() => handleView(row.original)}
className="bg-blue-500 hover:bg-blue-600"
>
<IconEye />
</button>
<button
onClick={() => handleDownload(row.original)}
disabled={isFileLoading}
className="bg-green-500 hover:bg-green-600"
>
<IconFileDownload />
</button>
</div>
<div className="actions-group">
<button
onClick={() => handleProtect(row.original)}
className="bg-violet-500 hover:bg-violet-600"
>
<IconShieldExclamation />
</button>
<button
onClick={() => handleOpenWindowForRemove(row.original)}
className="bg-red-700 hover:bg-red-800"
>
<IconDelete />
</button>
</div>
</div>
),
enableSorting: false,
enableColumnFilter: false,
},
],
[]
);
// Обработчики действий
const handleView = (file: FileItem) => {
console.log(`Просмотр файла: ${file.userId}`);
};
const handleDownload = async (file: FileItem) => {
setIsFileLoading(true);
try {
const response = await fetch(`/api/download/${file.id}`);
if (!response.ok) {
throw new Error(`error: ${response.status}`);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.userId || `file-${file.id}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success(`${t('file-is-downloading')} - ${file.userId}`);
} catch (error) {
toast.error(t('failed-to-download-file'))
} finally {
setIsFileLoading(false);
}
};
const handleProtect = (file: FileItem) => {
console.log(`Щиток: ${file.userId}`);
};
const handleOpenWindowForRemove = async (file: FileItem) => {
setOpenWindowChildren(() => {
return (
<div>
<h3 className="text-2xl">
{t('you-sure-you-want-to-delete')}
</h3>
<div className="mt-2 mb-8 text-center">{file.userId}</div>
<div className="flex justify-center gap-4">
<button className="btn-primary btn-modal"
onClick={() => {
/* deleteMutation.mutate(file.id) */
}}
/* disabled={deleteMutation.isPending} */
>
{/* {deleteMutation.isPending ? '...' : t('yes')} */}
{t('yes')}
</button>
<button className="btn-primary btn-modal"
onClick={() => {
setOpenWindow(false);
setOpenWindowChildren(null);
}}
>
{t('no')}
</button>
</div>
</div>
)
})
setOpenWindow(true);
};
/* const deleteMutation = useMutation({
mutationFn: removeUserFile,
onMutate: async (fileId: string) => {
await queryClient.cancelQueries({ queryKey: ['userFilesData'] });
queryClient.setQueryData<ApiResponse>(['userFilesData'], (old) => {
if (!old?.files) return old;
return {
...old,
files: old.files.filter(file => file.id !== fileId)
};
});
},
onError: () => {
queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
toast.error(t('error'));
},
onSuccess: (response, fileId) => {
if (response === fileId) {
queryClient.invalidateQueries({
queryKey: ['userFilesData'],
refetchType: 'active'
});
queryClient.invalidateQueries({
queryKey: ['userFilesInfo']
});
setOpenWindow(false);
setOpenWindowChildren(null);
toast.success(t('file-has-been-deleted'));
}
},
}); */
// Фильтрация по типу файла и дате
const filteredData = useMemo(() => {
let result = tableData;
if (!result) {
return [];
}
// Фильтр по типу файла
if (typeFilter !== 'all') {
result = result.filter((item: any) => {
return item.userName === typeFilter
});
}
// Фильтр по дате
if (dateFilter !== 'all') {
const now = Date.now();
const oneDay = 24 * 60 * 60 * 1000;
const sevenDays = 7 * oneDay;
const thirtyDays = 30 * oneDay;
switch (dateFilter) {
case 'today':
const todayStart = new Date().setHours(0, 0, 0, 0);
result = result.filter((item: any) => {
if (item.userSubscription) {
return item.userSubscription >= todayStart
} else {
return 0
}
});
break;
case 'week':
const weekAgo = now - sevenDays;
result = result.filter((item: any) => {
if (item.userSubscription) {
return item.userSubscription >= weekAgo;
}
});
break;
case 'month':
const monthAgo = now - thirtyDays;
result = result.filter((item: any) => {
if (item.userSubscription) {
return item.userSubscription >= monthAgo;
}
});
break;
case 'older':
const monthAgo2 = now - thirtyDays;
result = result.filter((item: any) => {
if (item.userSubscription) {
return item.userSubscription < monthAgo2;
}
});
break;
}
}
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase().trim();
result = result.filter((item: any) => {
// Ищем по всем строковым полям
return Object.keys(item).some(key => {
const value = item[key];
if (typeof value === 'string') {
return value.toLowerCase().includes(query);
}
// Если нужно искать по числам или другим типам
if (typeof value === 'number' || typeof value === 'boolean') {
return value.toString().toLowerCase().includes(query);
}
return false;
});
});
}
return result;
}, [tableData, typeFilter, dateFilter, searchQuery]);
useEffect(() => {
const currentPageRows = table.getRowModel().rows;
const pageCount = table.getPageCount();
if (currentPageRows.length === 0 && pagination.pageIndex > 0 && pageCount > 0) {
table.setPageIndex(pagination.pageIndex - 1);
}
}, [filteredData, pagination.pageIndex]);
// Создание таблицы
const table = useReactTable({
data: filteredData,
columns,
state: {
sorting,
columnFilters,
pagination
},
autoResetPageIndex: false,
onPaginationChange: setPagination,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getFilteredRowModel: getFilteredRowModel(),
initialState: {
pagination: {
pageSize: 10,
},
},
});
const pluralizeFiles = (number: number) => {
const translate = [t('file'), t('files-few'), t('files')];
return pluralize(number, translate[0], translate[1], translate[2], locale);
};
return (
<div className="tanstak-table-wrapper">
<ModalWindow children={openWindowChildren} state={openWindow} callBack={setOpenWindow}></ModalWindow>
{/* Фильтры */}
<div className="tanstak-table-filtres">
<div className="table-filtres-wrapper">
<div className="table-filtres-item">
<div className="table-filtres-label">{t('date-filter')}:</div>
<DropDownList
value={dateFilter}
translatedValue={(() => {
switch (dateFilter) {
case 'all':
return t('all-dates')
case 'week':
return t('for-a-week')
case 'month':
return t('for-a-month')
case 'older':
return t('older-than-a-month')
default:
return t('today')
}
})()}
callBack={setDateFilter}
>
<li value="all">
{t('all-dates')}
</li>
<li value="today">
{t('today')}
</li>
<li value="week">
{t('for-a-week')}
</li>
<li value="month">
{t('for-a-month')}
</li>
<li value="older">
{t('older-than-a-month')}
</li>
</DropDownList>
</div>
<div className="table-filtres-item">
<div className="table-filtres-label">{t('type-filter')}:</div>
<DropDownList
value={typeFilter}
translatedValue={
(() => {
switch (typeFilter) {
case 'image':
return t('images')
case 'video':
return t('videos')
case 'audio':
return t('audios')
default:
return t('all-types')
}
})()
}
callBack={setTypeFilter}
>
<li value="all">
{t('all-types')}
</li>
<li value="image">
{t('images-few')}
</li>
<li value="video">
{t('videos')}
</li>
<li value="audio">
{t('audios')}
</li>
</DropDownList>
</div>
<div className="table-filtres-item">
<div className="table-filtres-label">{t('search')}:</div>
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder="Поиск пользователей"
className="table-filtres-input"
/>
</div>
</div>
<div className="table-filtres-item">
<div className="table-filtres-label">{t('items-per-page')}:</div>
<DropDownList
value={table.getState().pagination.pageSize}
//@ts-ignore сделал так потому что для table.setPageSize нужна цифра
callBack={table.setPageSize}
>
{[5, 10, 20, 50, 100].map(pageSize => (
<li key={pageSize} value={pageSize}>
{t('show')} {pageSize}
</li>
))}
</DropDownList>
</div>
</div>
{/* Таблица */}
<div className="tanstak-table-block">
<table className="tanstak-table">
<thead className="tanstak-table-head">
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
key={header.id}
>
{header.isPlaceholder
? null
: typeof header.column.columnDef.header === 'function'
? header.column.columnDef.header(header.getContext())
: header.column.columnDef.header as string}
</th>
))}
</tr>
))}
</thead>
<tbody className="tanstak-table-body">
{table.getRowModel().rows.length > 0 ? (
table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{typeof cell.column.columnDef.cell === 'function'
? cell.column.columnDef.cell(cell.getContext())
: cell.getValue() as string}
</td>
))}
</tr>
))
) : (
<tr>
<td colSpan={columns.length} className="text-center">
{t('no-data-for-selected-filters')}
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Пагинация */}
<div className="tanstak-table-pagination">
<div className="pagination-info">
<span className="pagination-info-pages">
{t('page')}{' '}
<strong>
{table.getState().pagination.pageIndex + 1} {t('out-of')} {table.getPageCount() ? table.getPageCount() : 1}
</strong>
</span>
<span className="pagination-info-files">
| {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {filteredData.length} {pluralizeFiles(filteredData.length || 0)}
</span>
</div>
<div className="pagination-controls">
<button
className="arrow"
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
>
<IconDoubleArrowLeft />
</button>
<button
className="arrow"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<IconArrowLeft />
</button>
<div className="pagination-controls-pages">
{Array.from({ length: Math.min(5, table.getPageCount()) }, (_, i) => {
const pageIndex = Math.max(
0,
Math.min(
table.getPageCount() - 5,
table.getState().pagination.pageIndex - 2
)
) + i;
if (pageIndex < table.getPageCount()) {
return (
<button
key={pageIndex}
className={`${table.getState().pagination.pageIndex === pageIndex
? 'current'
: 'other'
}`}
onClick={() => table.setPageIndex(pageIndex)}
>
{pageIndex + 1}
</button>
);
}
return null;
})}
</div>
<button
className="arrow"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<IconArrowRight />
</button>
<button
className="arrow"
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
>
<IconDoubleArrowRight />
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,940 @@
'use client';
import { useState, useMemo, useEffect, ReactNode } from 'react';
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getPaginationRowModel,
getFilteredRowModel,
ColumnDef,
SortingState,
ColumnFiltersState,
} from '@tanstack/react-table';
import { IconImageFile, IconVideoFile, IconAudioFile, IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconShieldExclamation, IconDelete } from '@/app/ui/icons/icons';
import { useTranslations, useLocale } from 'next-intl';
import DropDownList from '@/app/components/dropDownList';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import ModalWindow from '@/app/components/modalWindow';
import { toast } from 'sonner';
import { pluralize } from '@/app/lib/pluralize';
type FileItem = {
id: string;
userId: string;
userName: string;
userEmail?: number | undefined;
userCompany?: number | undefined;
userSubscription?: number;
_original?: ApiFile;
};
type ApiFile = {
id: string;
originalFileName: string;
mimeType: string;
userEmail: number;
userCompany: number;
updatedAt: string;
};
type ApiResponse = any;
// Иконки для типов файлов
const FileTypeIcon = ({ type }: { type: string }) => {
switch (type) {
case 'image':
return <span className="color-image">
<IconImageFile />
</span>;
case 'video':
return <span className="color-video">
<IconVideoFile />
</span>;
case 'audio':
return <span className="color-audio">
<IconAudioFile />
</span>;
default:
return <span>📄</span>;
}
};
// Форматирование даты из timestamp
const formatDate = (timestamp: number) => {
const date = new Date(timestamp);
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`;
};
const formatDateTime = (timestamp: number) => {
const date = new Date(timestamp);
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
};
const cutFileName = (userId: string) => {
const MAX_FILE_NAME_LENGTH = 26;
const lastDotIndex = userId.lastIndexOf('.');
if (lastDotIndex <= 0) {
return userId.length > MAX_FILE_NAME_LENGTH ? userId.substring(0, MAX_FILE_NAME_LENGTH - 3) + '...' : userId;
}
const nameWithoutExtension = userId.substring(0, lastDotIndex);
const extension = userId.substring(lastDotIndex);
if (userId.length <= MAX_FILE_NAME_LENGTH) {
return userId;
}
const maxNameLength = MAX_FILE_NAME_LENGTH - extension.length - 3;
if (maxNameLength <= 0) {
return '...' + extension;
}
return nameWithoutExtension.substring(0, maxNameLength) + '...' + extension;
}
export default function TanstakUsersTable() {
const {
data: tableData,
isLoading,
isError,
error,
} = useQuery<ApiResponse, Error, any>({
queryKey: ['userFilesData'],
queryFn: () => {
return []
},
select: (data: ApiResponse): any => {
if (!data?.files) return [
{
id: 1,
userId: 1,
userName: 'userName',
userEmail: 'userEmail',
userCompany: 'userCompany',
userSubscription: 'userSubscription',
userRole: 'role',
userContent: 0,
}
];
return data.files.map((item: ApiFile) => {
const [datePart, timePart] = item.updatedAt.split(' ');
const [day, month, year] = datePart.split('-').map(Number);
const [hours, minutes, seconds] = timePart.split(':').map(Number);
const newDate = new Date(year, month - 1, day, hours, minutes, seconds).getTime();
return {
id: item.id,
userId: item.originalFileName,
userName: item.mimeType.toLocaleLowerCase(),
userEmail: item.userEmail,
userCompany: item.userCompany,
userSubscription: newDate,
userRole: 'role',
userContent: 0,
_original: item
};
});
},
});
const queryClient = useQueryClient();
// Состояния
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [dateFilter, setDateFilter] = useState<string>('all');
const [typeFilter, setTypeFilter] = useState<string>('all');
const [searchQuery, setSearchQuery] = useState('');
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 10,
});
const [openWindow, setOpenWindow] = useState<boolean>(false);
const [openWindowChildren, setOpenWindowChildren] = useState<ReactNode>(null);
const [isFileLoading, setIsFileLoading] = useState(false);
const t = useTranslations("Global");
const locale = useLocale();
// Определение колонок
const columns = useMemo<ColumnDef<FileItem>[]>(
() => [
{
accessorKey: 'userId',
header: ({ column }) => (
<div className="column start">
<span>
ID
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => (
<div className="flex items-center space-x-2">
<div>
<div className="font-medium w-full truncate" title={row.original.userId}>
{/* {row.original.userId} */}
{row.original.userId}
</div>
</div>
</div>
),
enableColumnFilter: false,
},
{
accessorKey: 'userName',
header: ({ column }) => (
<div className="column">
<span>
Пользователь
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
return (
<div className={`text-center font-semibold`}>
User name
</div>
)
},
},
{
accessorKey: 'userEmail',
header: ({ column }) => (
<div className="column">
<span>
email
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
let classCollor = () => {
let result = ''
if (row.original.userEmail !== undefined) {
result = row.original.userEmail > 0 ? 'text-red-600' : 'text-green-600'
}
return result;
}
return (
<div className={`text-center font-semibold ${classCollor()}`}>
{row.original.userEmail !== undefined ? row.original.userEmail : '-'}
</div>
)
},
},
{
accessorKey: 'userCompany',
header: ({ column }) => (
<div className="column">
<span>
Компания
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => (
<div className="text-center">
{row.original.userCompany !== undefined ? row.original.userCompany : '-'}
</div>
),
},
{
accessorKey: 'userSubscription',
header: ({ column }) => (
<div className="column">
<span>
Подписка
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
return (
<div className="text-center">
{row.original.userSubscription ? (
<>
{formatDate(row.original.userSubscription)}
<br />
{formatDateTime(row.original.userSubscription)}
</>
) : (
<div>-</div>
)}
</div>
)
},
enableColumnFilter: false,
},
{
accessorKey: 'userRole',
header: ({ column }) => (
<div className="column">
<span>
Роль
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
return (
<div className={`text-center font-semibold`}>
User Роль
</div>
)
},
}, {
accessorKey: 'userContent',
header: ({ column }) => (
<div className="column">
<span>
Контент
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
return (
<div className={`text-center font-semibold`}>
User Роль
</div>
)
},
},
{
id: 'actions',
header: () => {
return (
<div className="column">
{t('actions')}
</div>
)
},
cell: ({ row }) => (
<div className="actions">
<div className="actions-group">
<button
onClick={() => handleView(row.original)}
className="bg-blue-500 hover:bg-blue-600"
>
<IconEye />
</button>
<button
onClick={() => handleDownload(row.original)}
disabled={isFileLoading}
className="bg-green-500 hover:bg-green-600"
>
<IconFileDownload />
</button>
</div>
<div className="actions-group">
<button
onClick={() => handleProtect(row.original)}
className="bg-violet-500 hover:bg-violet-600"
>
<IconShieldExclamation />
</button>
<button
onClick={() => handleOpenWindowForRemove(row.original)}
className="bg-red-700 hover:bg-red-800"
>
<IconDelete />
</button>
</div>
</div>
),
enableSorting: false,
enableColumnFilter: false,
},
],
[]
);
// Обработчики действий
const handleView = (file: FileItem) => {
console.log(`Просмотр файла: ${file.userId}`);
};
const handleDownload = async (file: FileItem) => {
setIsFileLoading(true);
try {
const response = await fetch(`/api/download/${file.id}`);
if (!response.ok) {
throw new Error(`error: ${response.status}`);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.userId || `file-${file.id}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success(`${t('file-is-downloading')} - ${file.userId}`);
} catch (error) {
toast.error(t('failed-to-download-file'))
} finally {
setIsFileLoading(false);
}
};
const handleProtect = (file: FileItem) => {
console.log(`Щиток: ${file.userId}`);
};
const handleOpenWindowForRemove = async (file: FileItem) => {
setOpenWindowChildren(() => {
return (
<div>
<h3 className="text-2xl">
{t('you-sure-you-want-to-delete')}
</h3>
<div className="mt-2 mb-8 text-center">{file.userId}</div>
<div className="flex justify-center gap-4">
<button className="btn-primary btn-modal"
onClick={() => {
/* deleteMutation.mutate(file.id) */
}}
/* disabled={deleteMutation.isPending} */
>
{/* {deleteMutation.isPending ? '...' : t('yes')} */}
{t('yes')}
</button>
<button className="btn-primary btn-modal"
onClick={() => {
setOpenWindow(false);
setOpenWindowChildren(null);
}}
>
{t('no')}
</button>
</div>
</div>
)
})
setOpenWindow(true);
};
/* const deleteMutation = useMutation({
mutationFn: removeUserFile,
onMutate: async (fileId: string) => {
await queryClient.cancelQueries({ queryKey: ['userFilesData'] });
queryClient.setQueryData<ApiResponse>(['userFilesData'], (old) => {
if (!old?.files) return old;
return {
...old,
files: old.files.filter(file => file.id !== fileId)
};
});
},
onError: () => {
queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
toast.error(t('error'));
},
onSuccess: (response, fileId) => {
if (response === fileId) {
queryClient.invalidateQueries({
queryKey: ['userFilesData'],
refetchType: 'active'
});
queryClient.invalidateQueries({
queryKey: ['userFilesInfo']
});
setOpenWindow(false);
setOpenWindowChildren(null);
toast.success(t('file-has-been-deleted'));
}
},
}); */
// Фильтрация по типу файла и дате
const filteredData = useMemo(() => {
let result = tableData;
if (!result) {
return [];
}
// Фильтр по типу файла
if (typeFilter !== 'all') {
result = result.filter((item: any) => {
return item.userName === typeFilter
});
}
// Фильтр по дате
if (dateFilter !== 'all') {
const now = Date.now();
const oneDay = 24 * 60 * 60 * 1000;
const sevenDays = 7 * oneDay;
const thirtyDays = 30 * oneDay;
switch (dateFilter) {
case 'today':
const todayStart = new Date().setHours(0, 0, 0, 0);
result = result.filter((item: any) => {
if (item.userSubscription) {
return item.userSubscription >= todayStart
} else {
return 0
}
});
break;
case 'week':
const weekAgo = now - sevenDays;
result = result.filter((item: any) => {
if (item.userSubscription) {
return item.userSubscription >= weekAgo;
}
});
break;
case 'month':
const monthAgo = now - thirtyDays;
result = result.filter((item: any) => {
if (item.userSubscription) {
return item.userSubscription >= monthAgo;
}
});
break;
case 'older':
const monthAgo2 = now - thirtyDays;
result = result.filter((item: any) => {
if (item.userSubscription) {
return item.userSubscription < monthAgo2;
}
});
break;
}
}
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase().trim();
result = result.filter((item: any) => {
// Ищем по всем строковым полям
return Object.keys(item).some(key => {
const value = item[key];
if (typeof value === 'string') {
return value.toLowerCase().includes(query);
}
// Если нужно искать по числам или другим типам
if (typeof value === 'number' || typeof value === 'boolean') {
return value.toString().toLowerCase().includes(query);
}
return false;
});
});
}
return result;
}, [tableData, typeFilter, dateFilter, searchQuery]);
useEffect(() => {
const currentPageRows = table.getRowModel().rows;
const pageCount = table.getPageCount();
if (currentPageRows.length === 0 && pagination.pageIndex > 0 && pageCount > 0) {
table.setPageIndex(pagination.pageIndex - 1);
}
}, [filteredData, pagination.pageIndex]);
// Создание таблицы
const table = useReactTable({
data: filteredData,
columns,
state: {
sorting,
columnFilters,
pagination
},
autoResetPageIndex: false,
onPaginationChange: setPagination,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getFilteredRowModel: getFilteredRowModel(),
initialState: {
pagination: {
pageSize: 10,
},
},
});
const pluralizeFiles = (number: number) => {
const translate = [t('file'), t('files-few'), t('files')];
return pluralize(number, translate[0], translate[1], translate[2], locale);
};
return (
<div className="tanstak-table-wrapper">
<ModalWindow children={openWindowChildren} state={openWindow} callBack={setOpenWindow}></ModalWindow>
{/* Фильтры */}
<div className="tanstak-table-filtres">
<div className="table-filtres-wrapper">
<div className="table-filtres-item">
<div className="table-filtres-label">{t('date-filter')}:</div>
<DropDownList
value={dateFilter}
translatedValue={(() => {
switch (dateFilter) {
case 'all':
return t('all-dates')
case 'week':
return t('for-a-week')
case 'month':
return t('for-a-month')
case 'older':
return t('older-than-a-month')
default:
return t('today')
}
})()}
callBack={setDateFilter}
>
<li value="all">
{t('all-dates')}
</li>
<li value="today">
{t('today')}
</li>
<li value="week">
{t('for-a-week')}
</li>
<li value="month">
{t('for-a-month')}
</li>
<li value="older">
{t('older-than-a-month')}
</li>
</DropDownList>
</div>
<div className="table-filtres-item">
<div className="table-filtres-label">{t('type-filter')}:</div>
<DropDownList
value={typeFilter}
translatedValue={
(() => {
switch (typeFilter) {
case 'image':
return t('images')
case 'video':
return t('videos')
case 'audio':
return t('audios')
default:
return t('all-types')
}
})()
}
callBack={setTypeFilter}
>
<li value="all">
{t('all-types')}
</li>
<li value="image">
{t('images-few')}
</li>
<li value="video">
{t('videos')}
</li>
<li value="audio">
{t('audios')}
</li>
</DropDownList>
</div>
<div className="table-filtres-item">
<div className="table-filtres-label">{t('search')}:</div>
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder="Поиск пользователей"
className="table-filtres-input"
/>
</div>
</div>
<div className="table-filtres-item">
<div className="table-filtres-label">{t('items-per-page')}:</div>
<DropDownList
value={table.getState().pagination.pageSize}
//@ts-ignore сделал так потому что для table.setPageSize нужна цифра
callBack={table.setPageSize}
>
{[5, 10, 20, 50, 100].map(pageSize => (
<li key={pageSize} value={pageSize}>
{t('show')} {pageSize}
</li>
))}
</DropDownList>
</div>
</div>
{/* Таблица */}
<div className="tanstak-table-block">
<table className="tanstak-table">
<thead className="tanstak-table-head">
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
key={header.id}
>
{header.isPlaceholder
? null
: typeof header.column.columnDef.header === 'function'
? header.column.columnDef.header(header.getContext())
: header.column.columnDef.header as string}
</th>
))}
</tr>
))}
</thead>
<tbody className="tanstak-table-body">
{table.getRowModel().rows.length > 0 ? (
table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{typeof cell.column.columnDef.cell === 'function'
? cell.column.columnDef.cell(cell.getContext())
: cell.getValue() as string}
</td>
))}
</tr>
))
) : (
<tr>
<td colSpan={columns.length} className="text-center">
{t('no-data-for-selected-filters')}
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Пагинация */}
<div className="tanstak-table-pagination">
<div className="pagination-info">
<span className="pagination-info-pages">
{t('page')}{' '}
<strong>
{table.getState().pagination.pageIndex + 1} {t('out-of')} {table.getPageCount() ? table.getPageCount() : 1}
</strong>
</span>
<span className="pagination-info-files">
| {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {filteredData.length} {pluralizeFiles(filteredData.length || 0)}
</span>
</div>
<div className="pagination-controls">
<button
className="arrow"
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
>
<IconDoubleArrowLeft />
</button>
<button
className="arrow"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<IconArrowLeft />
</button>
<div className="pagination-controls-pages">
{Array.from({ length: Math.min(5, table.getPageCount()) }, (_, i) => {
const pageIndex = Math.max(
0,
Math.min(
table.getPageCount() - 5,
table.getState().pagination.pageIndex - 2
)
) + i;
if (pageIndex < table.getPageCount()) {
return (
<button
key={pageIndex}
className={`${table.getState().pagination.pageIndex === pageIndex
? 'current'
: 'other'
}`}
onClick={() => table.setPageIndex(pageIndex)}
>
{pageIndex + 1}
</button>
);
}
return null;
})}
</div>
<button
className="arrow"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<IconArrowRight />
</button>
<button
className="arrow"
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
>
<IconDoubleArrowRight />
</button>
</div>
</div>
</div>
);
}

View File

@ -1,4 +1,4 @@
import TanstakFilesTable from '@/app/components/tanstakTable';
import TanstakFilesTable from '@/app/ui/users/tanstak-users-table';
export function UsersTable() {
return (
<div>

View File

@ -0,0 +1,940 @@
'use client';
import { useState, useMemo, useEffect, ReactNode } from 'react';
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getPaginationRowModel,
getFilteredRowModel,
ColumnDef,
SortingState,
ColumnFiltersState,
} from '@tanstack/react-table';
import { IconImageFile, IconVideoFile, IconAudioFile, IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconShieldExclamation, IconDelete } from '@/app/ui/icons/icons';
import { useTranslations, useLocale } from 'next-intl';
import DropDownList from '@/app/components/dropDownList';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import ModalWindow from '@/app/components/modalWindow';
import { toast } from 'sonner';
import { pluralize } from '@/app/lib/pluralize';
type FileItem = {
id: string;
userId: string;
userName: string;
userEmail?: number | undefined;
userCompany?: number | undefined;
userSubscription?: number;
_original?: ApiFile;
};
type ApiFile = {
id: string;
originalFileName: string;
mimeType: string;
userEmail: number;
userCompany: number;
updatedAt: string;
};
type ApiResponse = any;
// Иконки для типов файлов
const FileTypeIcon = ({ type }: { type: string }) => {
switch (type) {
case 'image':
return <span className="color-image">
<IconImageFile />
</span>;
case 'video':
return <span className="color-video">
<IconVideoFile />
</span>;
case 'audio':
return <span className="color-audio">
<IconAudioFile />
</span>;
default:
return <span>📄</span>;
}
};
// Форматирование даты из timestamp
const formatDate = (timestamp: number) => {
const date = new Date(timestamp);
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`;
};
const formatDateTime = (timestamp: number) => {
const date = new Date(timestamp);
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
};
const cutFileName = (userId: string) => {
const MAX_FILE_NAME_LENGTH = 26;
const lastDotIndex = userId.lastIndexOf('.');
if (lastDotIndex <= 0) {
return userId.length > MAX_FILE_NAME_LENGTH ? userId.substring(0, MAX_FILE_NAME_LENGTH - 3) + '...' : userId;
}
const nameWithoutExtension = userId.substring(0, lastDotIndex);
const extension = userId.substring(lastDotIndex);
if (userId.length <= MAX_FILE_NAME_LENGTH) {
return userId;
}
const maxNameLength = MAX_FILE_NAME_LENGTH - extension.length - 3;
if (maxNameLength <= 0) {
return '...' + extension;
}
return nameWithoutExtension.substring(0, maxNameLength) + '...' + extension;
}
export default function ViolationsTable() {
const {
data: tableData,
isLoading,
isError,
error,
} = useQuery<ApiResponse, Error, any>({
queryKey: ['userFilesData'],
queryFn: () => {
return []
},
select: (data: ApiResponse): any => {
if (!data?.files) return [
{
id: 1,
userId: 1,
userName: 'userName',
userEmail: 'userEmail',
userCompany: 'userCompany',
userSubscription: 'userSubscription',
userRole: 'role',
userContent: 0,
}
];
return data.files.map((item: ApiFile) => {
const [datePart, timePart] = item.updatedAt.split(' ');
const [day, month, year] = datePart.split('-').map(Number);
const [hours, minutes, seconds] = timePart.split(':').map(Number);
const newDate = new Date(year, month - 1, day, hours, minutes, seconds).getTime();
return {
id: item.id,
userId: item.originalFileName,
userName: item.mimeType.toLocaleLowerCase(),
userEmail: item.userEmail,
userCompany: item.userCompany,
userSubscription: newDate,
userRole: 'role',
userContent: 0,
_original: item
};
});
},
});
const queryClient = useQueryClient();
// Состояния
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [dateFilter, setDateFilter] = useState<string>('all');
const [typeFilter, setTypeFilter] = useState<string>('all');
const [searchQuery, setSearchQuery] = useState('');
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 10,
});
const [openWindow, setOpenWindow] = useState<boolean>(false);
const [openWindowChildren, setOpenWindowChildren] = useState<ReactNode>(null);
const [isFileLoading, setIsFileLoading] = useState(false);
const t = useTranslations("Global");
const locale = useLocale();
// Определение колонок
const columns = useMemo<ColumnDef<FileItem>[]>(
() => [
{
accessorKey: 'userId',
header: ({ column }) => (
<div className="column start">
<span>
ID
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => (
<div className="flex items-center space-x-2">
<div>
<div className="font-medium w-full truncate" title={row.original.userId}>
{/* {row.original.userId} */}
{row.original.userId}
</div>
</div>
</div>
),
enableColumnFilter: false,
},
{
accessorKey: 'userName',
header: ({ column }) => (
<div className="column">
<span>
Пользователь
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
return (
<div className={`text-center font-semibold`}>
User name
</div>
)
},
},
{
accessorKey: 'userEmail',
header: ({ column }) => (
<div className="column">
<span>
email
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
let classCollor = () => {
let result = ''
if (row.original.userEmail !== undefined) {
result = row.original.userEmail > 0 ? 'text-red-600' : 'text-green-600'
}
return result;
}
return (
<div className={`text-center font-semibold ${classCollor()}`}>
{row.original.userEmail !== undefined ? row.original.userEmail : '-'}
</div>
)
},
},
{
accessorKey: 'userCompany',
header: ({ column }) => (
<div className="column">
<span>
Компания
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => (
<div className="text-center">
{row.original.userCompany !== undefined ? row.original.userCompany : '-'}
</div>
),
},
{
accessorKey: 'userSubscription',
header: ({ column }) => (
<div className="column">
<span>
Подписка
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
return (
<div className="text-center">
{row.original.userSubscription ? (
<>
{formatDate(row.original.userSubscription)}
<br />
{formatDateTime(row.original.userSubscription)}
</>
) : (
<div>-</div>
)}
</div>
)
},
enableColumnFilter: false,
},
{
accessorKey: 'userRole',
header: ({ column }) => (
<div className="column">
<span>
Роль
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
return (
<div className={`text-center font-semibold`}>
User Роль
</div>
)
},
}, {
accessorKey: 'userContent',
header: ({ column }) => (
<div className="column">
<span>
Контент
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
return (
<div className={`text-center font-semibold`}>
User Роль
</div>
)
},
},
{
id: 'actions',
header: () => {
return (
<div className="column">
{t('actions')}
</div>
)
},
cell: ({ row }) => (
<div className="actions">
<div className="actions-group">
<button
onClick={() => handleView(row.original)}
className="bg-blue-500 hover:bg-blue-600"
>
<IconEye />
</button>
<button
onClick={() => handleDownload(row.original)}
disabled={isFileLoading}
className="bg-green-500 hover:bg-green-600"
>
<IconFileDownload />
</button>
</div>
<div className="actions-group">
<button
onClick={() => handleProtect(row.original)}
className="bg-violet-500 hover:bg-violet-600"
>
<IconShieldExclamation />
</button>
<button
onClick={() => handleOpenWindowForRemove(row.original)}
className="bg-red-700 hover:bg-red-800"
>
<IconDelete />
</button>
</div>
</div>
),
enableSorting: false,
enableColumnFilter: false,
},
],
[]
);
// Обработчики действий
const handleView = (file: FileItem) => {
console.log(`Просмотр файла: ${file.userId}`);
};
const handleDownload = async (file: FileItem) => {
setIsFileLoading(true);
try {
const response = await fetch(`/api/download/${file.id}`);
if (!response.ok) {
throw new Error(`error: ${response.status}`);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.userId || `file-${file.id}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success(`${t('file-is-downloading')} - ${file.userId}`);
} catch (error) {
toast.error(t('failed-to-download-file'))
} finally {
setIsFileLoading(false);
}
};
const handleProtect = (file: FileItem) => {
console.log(`Щиток: ${file.userId}`);
};
const handleOpenWindowForRemove = async (file: FileItem) => {
setOpenWindowChildren(() => {
return (
<div>
<h3 className="text-2xl">
{t('you-sure-you-want-to-delete')}
</h3>
<div className="mt-2 mb-8 text-center">{file.userId}</div>
<div className="flex justify-center gap-4">
<button className="btn-primary btn-modal"
onClick={() => {
/* deleteMutation.mutate(file.id) */
}}
/* disabled={deleteMutation.isPending} */
>
{/* {deleteMutation.isPending ? '...' : t('yes')} */}
{t('yes')}
</button>
<button className="btn-primary btn-modal"
onClick={() => {
setOpenWindow(false);
setOpenWindowChildren(null);
}}
>
{t('no')}
</button>
</div>
</div>
)
})
setOpenWindow(true);
};
/* const deleteMutation = useMutation({
mutationFn: removeUserFile,
onMutate: async (fileId: string) => {
await queryClient.cancelQueries({ queryKey: ['userFilesData'] });
queryClient.setQueryData<ApiResponse>(['userFilesData'], (old) => {
if (!old?.files) return old;
return {
...old,
files: old.files.filter(file => file.id !== fileId)
};
});
},
onError: () => {
queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
toast.error(t('error'));
},
onSuccess: (response, fileId) => {
if (response === fileId) {
queryClient.invalidateQueries({
queryKey: ['userFilesData'],
refetchType: 'active'
});
queryClient.invalidateQueries({
queryKey: ['userFilesInfo']
});
setOpenWindow(false);
setOpenWindowChildren(null);
toast.success(t('file-has-been-deleted'));
}
},
}); */
// Фильтрация по типу файла и дате
const filteredData = useMemo(() => {
let result = tableData;
if (!result) {
return [];
}
// Фильтр по типу файла
if (typeFilter !== 'all') {
result = result.filter((item: any) => {
return item.userName === typeFilter
});
}
// Фильтр по дате
if (dateFilter !== 'all') {
const now = Date.now();
const oneDay = 24 * 60 * 60 * 1000;
const sevenDays = 7 * oneDay;
const thirtyDays = 30 * oneDay;
switch (dateFilter) {
case 'today':
const todayStart = new Date().setHours(0, 0, 0, 0);
result = result.filter((item: any) => {
if (item.userSubscription) {
return item.userSubscription >= todayStart
} else {
return 0
}
});
break;
case 'week':
const weekAgo = now - sevenDays;
result = result.filter((item: any) => {
if (item.userSubscription) {
return item.userSubscription >= weekAgo;
}
});
break;
case 'month':
const monthAgo = now - thirtyDays;
result = result.filter((item: any) => {
if (item.userSubscription) {
return item.userSubscription >= monthAgo;
}
});
break;
case 'older':
const monthAgo2 = now - thirtyDays;
result = result.filter((item: any) => {
if (item.userSubscription) {
return item.userSubscription < monthAgo2;
}
});
break;
}
}
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase().trim();
result = result.filter((item: any) => {
// Ищем по всем строковым полям
return Object.keys(item).some(key => {
const value = item[key];
if (typeof value === 'string') {
return value.toLowerCase().includes(query);
}
// Если нужно искать по числам или другим типам
if (typeof value === 'number' || typeof value === 'boolean') {
return value.toString().toLowerCase().includes(query);
}
return false;
});
});
}
return result;
}, [tableData, typeFilter, dateFilter, searchQuery]);
useEffect(() => {
const currentPageRows = table.getRowModel().rows;
const pageCount = table.getPageCount();
if (currentPageRows.length === 0 && pagination.pageIndex > 0 && pageCount > 0) {
table.setPageIndex(pagination.pageIndex - 1);
}
}, [filteredData, pagination.pageIndex]);
// Создание таблицы
const table = useReactTable({
data: filteredData,
columns,
state: {
sorting,
columnFilters,
pagination
},
autoResetPageIndex: false,
onPaginationChange: setPagination,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getFilteredRowModel: getFilteredRowModel(),
initialState: {
pagination: {
pageSize: 10,
},
},
});
const pluralizeFiles = (number: number) => {
const translate = [t('file'), t('files-few'), t('files')];
return pluralize(number, translate[0], translate[1], translate[2], locale);
};
return (
<div className="tanstak-table-wrapper">
<ModalWindow children={openWindowChildren} state={openWindow} callBack={setOpenWindow}></ModalWindow>
{/* Фильтры */}
<div className="tanstak-table-filtres">
<div className="table-filtres-wrapper">
<div className="table-filtres-item">
<div className="table-filtres-label">{t('date-filter')}:</div>
<DropDownList
value={dateFilter}
translatedValue={(() => {
switch (dateFilter) {
case 'all':
return t('all-dates')
case 'week':
return t('for-a-week')
case 'month':
return t('for-a-month')
case 'older':
return t('older-than-a-month')
default:
return t('today')
}
})()}
callBack={setDateFilter}
>
<li value="all">
{t('all-dates')}
</li>
<li value="today">
{t('today')}
</li>
<li value="week">
{t('for-a-week')}
</li>
<li value="month">
{t('for-a-month')}
</li>
<li value="older">
{t('older-than-a-month')}
</li>
</DropDownList>
</div>
<div className="table-filtres-item">
<div className="table-filtres-label">{t('type-filter')}:</div>
<DropDownList
value={typeFilter}
translatedValue={
(() => {
switch (typeFilter) {
case 'image':
return t('images')
case 'video':
return t('videos')
case 'audio':
return t('audios')
default:
return t('all-types')
}
})()
}
callBack={setTypeFilter}
>
<li value="all">
{t('all-types')}
</li>
<li value="image">
{t('images-few')}
</li>
<li value="video">
{t('videos')}
</li>
<li value="audio">
{t('audios')}
</li>
</DropDownList>
</div>
<div className="table-filtres-item">
<div className="table-filtres-label">{t('search')}:</div>
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder="Поиск пользователей"
className="table-filtres-input"
/>
</div>
</div>
<div className="table-filtres-item">
<div className="table-filtres-label">{t('items-per-page')}:</div>
<DropDownList
value={table.getState().pagination.pageSize}
//@ts-ignore сделал так потому что для table.setPageSize нужна цифра
callBack={table.setPageSize}
>
{[5, 10, 20, 50, 100].map(pageSize => (
<li key={pageSize} value={pageSize}>
{t('show')} {pageSize}
</li>
))}
</DropDownList>
</div>
</div>
{/* Таблица */}
<div className="tanstak-table-block">
<table className="tanstak-table">
<thead className="tanstak-table-head">
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
key={header.id}
>
{header.isPlaceholder
? null
: typeof header.column.columnDef.header === 'function'
? header.column.columnDef.header(header.getContext())
: header.column.columnDef.header as string}
</th>
))}
</tr>
))}
</thead>
<tbody className="tanstak-table-body">
{table.getRowModel().rows.length > 0 ? (
table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{typeof cell.column.columnDef.cell === 'function'
? cell.column.columnDef.cell(cell.getContext())
: cell.getValue() as string}
</td>
))}
</tr>
))
) : (
<tr>
<td colSpan={columns.length} className="text-center">
{t('no-data-for-selected-filters')}
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Пагинация */}
<div className="tanstak-table-pagination">
<div className="pagination-info">
<span className="pagination-info-pages">
{t('page')}{' '}
<strong>
{table.getState().pagination.pageIndex + 1} {t('out-of')} {table.getPageCount() ? table.getPageCount() : 1}
</strong>
</span>
<span className="pagination-info-files">
| {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {filteredData.length} {pluralizeFiles(filteredData.length || 0)}
</span>
</div>
<div className="pagination-controls">
<button
className="arrow"
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
>
<IconDoubleArrowLeft />
</button>
<button
className="arrow"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<IconArrowLeft />
</button>
<div className="pagination-controls-pages">
{Array.from({ length: Math.min(5, table.getPageCount()) }, (_, i) => {
const pageIndex = Math.max(
0,
Math.min(
table.getPageCount() - 5,
table.getState().pagination.pageIndex - 2
)
) + i;
if (pageIndex < table.getPageCount()) {
return (
<button
key={pageIndex}
className={`${table.getState().pagination.pageIndex === pageIndex
? 'current'
: 'other'
}`}
onClick={() => table.setPageIndex(pageIndex)}
>
{pageIndex + 1}
</button>
);
}
return null;
})}
</div>
<button
className="arrow"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<IconArrowRight />
</button>
<button
className="arrow"
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
>
<IconDoubleArrowRight />
</button>
</div>
</div>
</div>
);
}

View File

@ -10,7 +10,6 @@ export default async function proxy(request: NextRequest) {
if (
path.startsWith('/_next') ||
path.includes('/api/') ||
path.includes('.')
) {
return NextResponse.next()
@ -26,5 +25,5 @@ export default async function proxy(request: NextRequest) {
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)']
matcher: ['/((?!_next/static|_next/image|.*\\.png$).*)']
};