start work on content moderation table

This commit is contained in:
smanylov
2026-05-25 16:10:11 +07:00
parent f5c99f1e09
commit 9db7f6b08d
5 changed files with 707 additions and 21 deletions
@@ -0,0 +1,632 @@
'use client';
import { useState, useMemo, useEffect, ReactNode, useCallback } from 'react';
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getPaginationRowModel,
getFilteredRowModel,
ColumnDef,
SortingState,
ColumnFiltersState,
} from '@tanstack/react-table';
import { IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter } 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 { useDebouncedCallback } from 'use-debounce';
import { fetchUsesInfo } from '@/app/actions/usersActions';
import { UserInfoModalWindow } from '@/app/ui/users/user-info-modal-window';
import { useContentForModeration, ContentForModeration } from '@/app/hooks/react-query/useContentForModeration';
// Форматирование даты из 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 ContentModerationTable({ permission }: { permission: number }) {
// Состояния
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [searchQuery, setSearchQuery] = useState('');
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 10,
});
const [searchInputValue, setSearchInputValue] = useState<string>('');
const [openWindow, setOpenWindow] = useState<boolean>(false);
const [openWindowChildren, setOpenWindowChildren] = useState<ReactNode>(null);
const debouncedSetSearchQuery = useDebouncedCallback(
(value: string) => {
setSearchQuery(value);
setPagination(prev => ({ ...prev, pageIndex: 0 }));
},
500
);
const handleSearchInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setSearchInputValue(value);
debouncedSetSearchQuery(value);
}, [debouncedSetSearchQuery]);
const t = useTranslations("Global");
const locale = useLocale();
const getSortParams = useMemo(() => {
if (sorting.length === 0) {
return { sortBy: '', sortOrder: undefined };
}
const sort = sorting[0];
const sortByMap: Record<string, string> = {
'id': 'supportId',
'userName': 'fullName',
'email': 'email',
'verificationStatus': 'verificationStatus',
'createdAt': 'createdAt',
'tariff': 'tariff',
'tokens': 'tokens',
'userCompany': 'userCompany',
};
console.log(sortByMap[sort.id]);
return {
sortBy: sortByMap[sort.id] || sort.id,
sortDirection: sort.desc ? 'desc' : 'asc'
};
}, [sorting]);
const { data: tableData,
isLoading,
isError,
isFetching,
error
} = useContentForModeration(pagination.pageIndex, pagination.pageSize, getSortParams?.sortBy, getSortParams?.sortDirection, searchQuery);
useEffect(() => {
console.log(tableData);
}, [tableData])
const queryClient = useQueryClient();
// Определение колонок
const columns = useMemo<ColumnDef<ContentForModeration>[]>(() => {
const allColumns: ColumnDef<ContentForModeration>[] = [
{
accessorKey: 'id',
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 table-item table-item-id">
<div>
<div className="font-medium w-full truncate" title={'row.original.id.toString()'}>
{'row.original.id'}
</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`}>
{'row.original.userName'}
</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 }) => {
return (
<div className={`text-center font-semibold text-green-600`}>
{'row.original.userEmail'}
</div>
)
},
},
{
accessorKey: 'verificationStatus',
header: ({ column }) => (
<div className="column">
<span>
статус KYC
</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.verificationStatus'}
</div>
),
},
{
accessorKey: 'tariff',
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.tariff'}
</div>
)
},
enableColumnFilter: false,
},
{
accessorKey: 'tokens',
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`}>
{'row.original.tokens'}
</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 font-semibold`}>
<>
{formatDate(0)}
<br />
{formatDateTime(0)}
</>
</div>
)
},
}
]
if (permission === 3) {
allColumns.push({
id: 'actions',
header: () => <div className="column">{t('actions')}</div>,
cell: ({ row }) => (
<div className="actions">
<div className="actions-group">
<button
onClick={() => {}}
className="bg-blue-500 hover:bg-blue-600"
>
<IconEye />
</button>
</div>
</div>
),
enableSorting: false,
enableColumnFilter: false,
});
}
return allColumns;
}, [permission]);
// Фильтрация по типу файла и дате
const filteredData = useMemo(() => {
/* let result = tableData?.users; */
let result = [];
return []
if (!result) {
return [];
}
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, 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
},
manualPagination: true,
manualSorting: true,
manualFiltering: true,
/* pageCount: tableData?.pagination?.totalPages, */
pageCount: 0,
onPaginationChange: setPagination,
onSortingChange: (updater) => {
const newSorting = typeof updater === 'function' ? updater(sorting) : updater;
setSorting(newSorting);
setPagination(prev => ({ ...prev, pageIndex: 0 }));
},
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel()
});
return (
<div className="tanstak-table-wrapper">
<ModalWindow state={openWindow} callBack={setOpenWindow}>
{openWindowChildren}
</ModalWindow>
{/* Фильтры */}
<div className="tanstak-table-filtres">
<div className="table-filtres-wrapper">
<div className="table-filtres-item">
<div className="table-filtres-label">{t('search')}:</div>
<input
type="text"
value={searchInputValue}
onChange={handleSearchInputChange}
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 ${isFetching ? 'loading' : ''}`}>
{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>
{(tableData?.pagination?.currentPage ?? 0) + 1} {t('out-of')} {tableData?.pagination?.totalPages ?? 1}
</strong>
</span>
<span className="pagination-info-files">
| {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {tableData?.pagination?.totalElements ?? 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">
{(() => {
const totalPages = tableData?.pagination?.totalPages;
if (!totalPages || totalPages === 0) return null;
const currentPageIndex = table.getState().pagination.pageIndex;
const maxVisiblePages = 5;
const visiblePagesCount = Math.min(maxVisiblePages, totalPages);
let startPage = currentPageIndex - Math.floor(maxVisiblePages / 2);
startPage = Math.max(0, Math.min(startPage, totalPages - visiblePagesCount));
return Array.from({ length: visiblePagesCount }, (_, i) => {
const pageIndex = startPage + i;
return (
<button
key={pageIndex}
className={currentPageIndex === pageIndex ? 'current' : 'other'}
onClick={() => table.setPageIndex(pageIndex)}
>
{pageIndex + 1}
</button>
);
});
})()}
</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>
);
}