From 7aacea387eb47ab178abfed243d2bb4ee94978f0 Mon Sep 17 00:00:00 2001 From: smanylov Date: Fri, 12 Dec 2025 20:28:59 +0700 Subject: [PATCH] add tanstak table --- src/app/components/dropDownList.tsx | 79 ++++ src/app/components/tanstakTable.tsx | 606 ++++++++++++++++++--------- src/app/styles/dashboard.scss | 7 - src/app/ui/dashboard/files-table.tsx | 6 +- src/app/ui/dashboard/stats-grid.tsx | 2 +- src/app/ui/icons/icons.tsx | 51 +++ src/i18n/messages/en.json | 20 +- src/i18n/messages/ru.json | 20 +- 8 files changed, 585 insertions(+), 206 deletions(-) create mode 100644 src/app/components/dropDownList.tsx diff --git a/src/app/components/dropDownList.tsx b/src/app/components/dropDownList.tsx new file mode 100644 index 0000000..8a3a9e5 --- /dev/null +++ b/src/app/components/dropDownList.tsx @@ -0,0 +1,79 @@ +import React, { ReactNode, useState, useRef } from 'react'; +import { useClickOutside } from '@/app/hooks/useClickOutside'; + +interface DropDownListProps { + children: ReactNode; + value: string | number; + callBack: (value: string) => void; +} + +interface ChildProps { + value?: string; + onClick?: () => void; + className?: string; + children?: React.ReactNode; +} + +export default function DropDownList({ children, value, callBack }: DropDownListProps) { + const [isOpen, setIsOpen] = useState(false) + const dropdownRef = useRef(null) + useClickOutside(dropdownRef, () => { + setIsOpen(false); + }); + + const enhancedChildren = React.Children.map(children, (child, index) => { + if (React.isValidElement(child)) { + const childValue = child.props.value || undefined; + + return React.cloneElement(child, { + onClick: () => { + callBack(childValue || ""); + setIsOpen(false); + }, + className: `flex items-center w-full px-4 py-2 text-sm text-left hover:bg-blue-100 text-[#6366f1]`, + key: index + }); + } + return child; + }); + + return ( +
+ + {isOpen && ( +
+
    + {enhancedChildren} +
+
+ )} +
+ ) +} \ No newline at end of file diff --git a/src/app/components/tanstakTable.tsx b/src/app/components/tanstakTable.tsx index 70f265a..7b6d976 100644 --- a/src/app/components/tanstakTable.tsx +++ b/src/app/components/tanstakTable.tsx @@ -10,195 +10,400 @@ import { ColumnDef, SortingState, ColumnFiltersState, - FilterFn, } from '@tanstack/react-table'; -import { rankItem } from '@tanstack/match-sorter-utils'; +import { IconImageFile, IconVideoFile, IconAudioFile, IconEye, IconDownload, IconShield, DoubleArrowRight, ArrowRight, DoubleArrowLeft, ArrowLeft, ArrowUp, ArrowDown, Filter } from '@/app/ui/icons/icons'; +import { useTranslations } from 'next-intl'; +import DropDownList from '@/app/components/dropDownList'; // Типы данных -type Person = { +type FileType = 'image' | 'video' | 'audio'; +type FileItem = { id: number; - name: string; - age: number; - email: string; - department: string; - salary: number; + fileName: string; + fileType: FileType; + violations: number; + checks: number; + lastCheck: number; // timestamp в миллисекундах }; -// Кастомная функция фильтрации -const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { - const itemRank = rankItem(row.getValue(columnId), value); - addMeta({ itemRank }); - return itemRank.passed; -}; - -// Определяем тип для фильтров -declare module '@tanstack/react-table' { - interface FilterFns { - fuzzy: FilterFn; +// Иконки для типов файлов +const FileTypeIcon = ({ type }: { type: FileType }) => { + switch (type) { + case 'image': + return + + ; + case 'video': + return + + ; + case 'audio': + return + + ; + default: + return 📄; } -} +}; -export default function DataTable() { - // Данные - const [data] = useState([ - { id: 1, name: 'Иван Петров', age: 28, email: 'ivan@example.com', department: 'IT', salary: 80000 }, - { id: 2, name: 'Анна Сидорова', age: 32, email: 'anna@example.com', department: 'HR', salary: 65000 }, - { id: 3, name: 'Сергей Иванов', age: 45, email: 'sergey@example.com', department: 'Финансы', salary: 95000 }, - { id: 4, name: 'Мария Кузнецова', age: 26, email: 'maria@example.com', department: 'Маркетинг', salary: 60000 }, - { id: 5, name: 'Алексей Смирнов', age: 38, email: 'alex@example.com', department: 'IT', salary: 85000 }, - { id: 6, name: 'Ольга Васнецова', age: 29, email: 'olga@example.com', department: 'HR', salary: 62000 }, - { id: 7, name: 'Дмитрий Орлов', age: 41, email: 'dmitry@example.com', department: 'Финансы', salary: 92000 }, - { id: 8, name: 'Екатерина Новикова', age: 35, email: 'ekaterina@example.com', department: 'Маркетинг', salary: 70000 }, +// Форматирование даты из 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}`; +}; + +// Компонент таблицы +export default function TanstakFilesTable() { + // Данные с timestamp вместо Date объектов + const [data] = useState([ + { id: 1, fileName: 'image1.jpg', fileType: 'image', violations: 5, checks: 12, lastCheck: 1705267200000 }, // 15.01.2024 + { id: 2, fileName: 'presentation.mp4', fileType: 'video', violations: 2, checks: 8, lastCheck: 1705180800000 }, // 14.01.2024 + { id: 3, fileName: 'song.mp3', fileType: 'audio', violations: 0, checks: 5, lastCheck: 1705094400000 }, // 13.01.2024 + { id: 4, fileName: 'photo1.jpg', fileType: 'image', violations: 3, checks: 10, lastCheck: 1705008000000 }, // 12.01.2024 + { id: 5, fileName: 'movie.mp4', fileType: 'video', violations: 1, checks: 7, lastCheck: 1704921600000 }, // 11.01.2024 + { id: 6, fileName: 'podcast.mp3', fileType: 'audio', violations: 4, checks: 9, lastCheck: 1704835200000 }, // 10.01.2024 + { id: 7, fileName: 'screenshot.png', fileType: 'image', violations: 0, checks: 3, lastCheck: 1704748800000 }, // 09.01.2024 + { id: 8, fileName: 'tutorial.mp4', fileType: 'video', violations: 2, checks: 6, lastCheck: 1704662400000 }, // 08.01.2024 + { id: 9, fileName: 'music.mp3', fileType: 'audio', violations: 1, checks: 4, lastCheck: 1704576000000 }, // 07.01.2024 + { id: 10, fileName: 'image2.jpg', fileType: 'image', violations: 5, checks: 11, lastCheck: 1704489600000 }, // 06.01.2024 + { id: 11, fileName: 'film.mov', fileType: 'video', violations: 0, checks: 2, lastCheck: 1704403200000 }, // 05.01.2024 + { id: 12, fileName: 'sound.wav', fileType: 'audio', violations: 3, checks: 8, lastCheck: 1704316800000 }, // 04.01.2024 + { id: 13, fileName: 'picture.png', fileType: 'image', violations: 2, checks: 7, lastCheck: 1704230400000 }, // 03.01.2024 + { id: 14, fileName: 'video2.mp4', fileType: 'video', violations: 1, checks: 5, lastCheck: 1704144000000 }, // 02.01.2024 + { id: 15, fileName: 'audio2.mp3', fileType: 'audio', violations: 0, checks: 3, lastCheck: 1704057600000 }, // 01.01.2024 ]); // Состояния const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState([]); - const [globalFilter, setGlobalFilter] = useState(''); - const [rowSelection, setRowSelection] = useState({}); + const [dateFilter, setDateFilter] = useState('all'); + const [typeFilter, setTypeFilter] = useState('all'); + + const t = useTranslations("Global"); // Определение колонок - const columns = useMemo[]>( + const columns = useMemo[]>( () => [ { - id: 'select', - header: ({ table }) => ( - + accessorKey: 'fileName', + header: ({ column }) => ( +
+ + {t('file')} + + +
), cell: ({ row }) => ( - +
+ +
+
{row.original.fileName}
+
+
), - enableSorting: false, enableColumnFilter: false, }, { - accessorKey: 'id', - header: 'ID', - cell: (info) => info.getValue(), + accessorKey: 'violations', + header: ({ column }) => ( +
+ + {t('violations')} + + +
+ ), + cell: ({ row }) => ( +
0 ? 'text-red-600' : 'text-green-600'}`}> + {row.original.violations} +
+ ), }, { - accessorKey: 'name', - header: 'Имя', - cell: (info) => info.getValue(), - filterFn: fuzzyFilter, + accessorKey: 'checks', + header: ({ column }) => ( +
+ + {t('checks')} + + +
+ ), + cell: ({ row }) => ( +
+ {row.original.checks} +
+ ), }, { - accessorKey: 'age', - header: 'Возраст', - cell: (info) => info.getValue(), + accessorKey: 'lastCheck', + header: ({ column }) => ( +
+ + {t('last-check')} + + +
+ ), + cell: ({ row }) => ( +
+ {formatDate(row.original.lastCheck)} +
+ ), + enableColumnFilter: false, }, { - accessorKey: 'email', - header: 'Email', - cell: (info) => info.getValue(), - }, - { - accessorKey: 'department', - header: 'Отдел', - cell: (info) => info.getValue(), - }, - { - accessorKey: 'salary', - header: 'Зарплата', - cell: (info) => `$${Number(info.getValue()).toLocaleString()}`, + id: 'actions', + header: () => { + return ( +
+ {t('actions')} +
+ ) + }, + cell: ({ row }) => ( +
+ + + +
+ ), + enableSorting: false, + enableColumnFilter: false, }, ], [] ); + // Обработчики действий + const handleView = (file: FileItem) => { + console.log(`Просмотр файла: ${file.fileName}`); + }; + + const handleDownload = (file: FileItem) => { + console.log(`Скачать: ${file.fileName}`); + }; + + const handleProtect = (file: FileItem) => { + console.log(`Щиток: ${file.fileName}`); + }; + + // Фильтрация по типу файла и дате + const filteredData = useMemo(() => { + let result = data; + + // Фильтр по типу файла + if (typeFilter !== 'all') { + result = result.filter(item => item.fileType === 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 => item.lastCheck >= todayStart); + break; + case 'week': + const weekAgo = now - sevenDays; + result = result.filter(item => item.lastCheck >= weekAgo); + break; + case 'month': + const monthAgo = now - thirtyDays; + result = result.filter(item => item.lastCheck >= monthAgo); + break; + case 'older': + const monthAgo2 = now - thirtyDays; + result = result.filter(item => item.lastCheck < monthAgo2); + break; + } + } + + return result; + }, [data, typeFilter, dateFilter]); + // Создание таблицы const table = useReactTable({ - data, + data: filteredData, columns, - filterFns: { - fuzzy: fuzzyFilter, - }, state: { sorting, columnFilters, - globalFilter, - rowSelection, }, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, - onGlobalFilterChange: setGlobalFilter, - onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), getFilteredRowModel: getFilteredRowModel(), - globalFilterFn: fuzzyFilter, // Используем саму функцию, а не строку + initialState: { + pagination: { + pageSize: 10, + }, + }, }); return ( -
-

Таблица сотрудников

+
+ {/* Фильтры */} +
+
+
+
{t('date-filter')}:
+ +
  • + {t('all-dates')} +
  • +
  • + {t('today')} +
  • +
  • + {t('for-a-week')} +
  • +
  • + {t('for-a-month')} +
  • +
  • + {t('older-than-a-month')} +
  • +
    +
    - {/* Глобальный поиск */} -
    - setGlobalFilter(e.target.value)} - className="px-4 py-2 border rounded-md w-full max-w-md" - /> -
    +
    +
    {t('type-filter')}:
    + +
  • + {t('all-types')} +
  • +
  • + {t('images')} +
  • +
  • + {t('videos')} +
  • +
  • + {t('audios')} +
  • +
    +
    +
    - {/* Фильтры по колонкам */} -
    - {table.getAllColumns() - .filter(column => column.getCanFilter() && column.id !== 'select') - .map(column => ( -
    - - {column.id === 'department' ? ( - - ) : column.id === 'age' ? ( - column.setFilterValue(e.target.value ? parseInt(e.target.value) : undefined)} - className="px-3 py-1 border rounded w-32" - /> - ) : ( - column.setFilterValue(e.target.value)} - className="px-3 py-1 border rounded" - /> - )} -
    - ))} +
    +
    {t('items-per-page')}:
    + + {[5, 10, 20, 50, 100].map(pageSize => ( +
  • + {t('show')} {pageSize} +
  • + ))} +
    +
    {/* Таблица */} -
    +
    {table.getHeaderGroups().map(headerGroup => ( @@ -206,96 +411,115 @@ export default function DataTable() { {headerGroup.headers.map(header => ( ))} ))} - {table.getRowModel().rows.map(row => ( - - {row.getVisibleCells().map(cell => ( - - ))} + {table.getRowModel().rows.length > 0 ? ( + table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} + + )) + ) : ( + + - ))} + )}
    -
    - {header.isPlaceholder - ? null - : typeof header.column.columnDef.header === 'string' - ? header.column.columnDef.header - : header.column.id} - {{ - asc: ' 🔼', - desc: ' 🔽', - }[header.column.getIsSorted() as string] ?? null} -
    + {header.isPlaceholder + ? null + : typeof header.column.columnDef.header === 'function' + ? header.column.columnDef.header(header.getContext()) + : header.column.columnDef.header as string}
    - {typeof cell.column.columnDef.cell === 'function' - ? cell.column.columnDef.cell(cell.getContext()) - : cell.getValue() as string} -
    + {typeof cell.column.columnDef.cell === 'function' + ? cell.column.columnDef.cell(cell.getContext()) + : cell.getValue() as string} +
    + {t('no-data-for-selected-filters')} +
    {/* Пагинация */} -
    +
    +
    + + {t('page')}{' '} + + {table.getState().pagination.pageIndex + 1} {t('out-of')} {table.getPageCount()} + + + + | {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {filteredData.length} {t('files')} + +
    +
    + +
    + {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 ( + + ); + } + return null; + })} +
    +
    - -
    - - Страница{' '} - - {table.getState().pagination.pageIndex + 1} из {table.getPageCount()} - - - - - Всего строк: {data.length} | Отфильтровано: {table.getFilteredRowModel().rows.length} - -
    ); diff --git a/src/app/styles/dashboard.scss b/src/app/styles/dashboard.scss index 369a144..9507ebd 100644 --- a/src/app/styles/dashboard.scss +++ b/src/app/styles/dashboard.scss @@ -128,13 +128,6 @@ } .stats-wrapper { - border-radius: 20px; - overflow: hidden; - background: white; - box-shadow: 0 4px 20px #00000014; - margin-bottom: 30px; - padding: 20px; - h3 { font-size: 20px; margin-bottom: 10px; diff --git a/src/app/ui/dashboard/files-table.tsx b/src/app/ui/dashboard/files-table.tsx index 6c96b70..e0b6de3 100644 --- a/src/app/ui/dashboard/files-table.tsx +++ b/src/app/ui/dashboard/files-table.tsx @@ -1,8 +1,8 @@ -import DataTable from '@/app/components/tanstakTable'; +import TanstakFilesTable from '@/app/components/tanstakTable'; export default function FilesTable() { return ( -
    - +
    +
    ) } \ No newline at end of file diff --git a/src/app/ui/dashboard/stats-grid.tsx b/src/app/ui/dashboard/stats-grid.tsx index 13ab89f..e6514ea 100644 --- a/src/app/ui/dashboard/stats-grid.tsx +++ b/src/app/ui/dashboard/stats-grid.tsx @@ -4,7 +4,7 @@ export default function StatsGrid() { const t = useTranslations("Global"); return ( -
    +

    {t('your-content')}

    diff --git a/src/app/ui/icons/icons.tsx b/src/app/ui/icons/icons.tsx index d9b324d..dd5572d 100644 --- a/src/app/ui/icons/icons.tsx +++ b/src/app/ui/icons/icons.tsx @@ -36,4 +36,55 @@ export function IconDownload() { ) +} + +export function IconEye() { + return ( + + + + ) +} + +export function DoubleArrowRight() { + return ( + + ) +} + +export function DoubleArrowLeft() { + return ( + + ) +} + +export function ArrowRight() { + return ( + + + ) +} + +export function ArrowLeft() { + return ( + + ) +} + +export function ArrowUp() { + return ( + + ) +} + +export function ArrowDown() { + return ( + + ) +} + +export function Filter() { + return ( + + ) } \ No newline at end of file diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 721a1f8..bfddbfb 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -4,9 +4,12 @@ "description": "This is a demo application with i18n support" }, "Global": { + "actions": "Actions", + "all-types": "All types", "statistics": "Statistics", "your-content": "Your content", - "fail": "Fail", + "file": "File", + "files": "Files", "content-type": "Content type", "quantity": "Quantity", "size": "Size", @@ -37,7 +40,20 @@ "current-status-of": "Current status of security and activity of the monitoring system", "total-files": "Total files", "disk-space-used": "Disk space used", - "out-of": "out of" + "out-of": "out of", + "last-check": "Last check", + "all-dates": "All dates", + "today": "Today", + "for-a-week": "For a week", + "for-a-month": "For a month", + "older-than-a-month": "Older than a month", + "show": "Show", + "no-data-for-selected-filters": "No data for selected filters", + "page": "Page", + "shown": "Shown", + "date-filter": "Date filter", + "type-filter": "Type filter", + "items-per-page": "Items per page" }, "Login-register-form": { "and": "and", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 5ac29b0..e4adcc1 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -4,9 +4,12 @@ "description": "Это демонстрационное приложение с поддержкой i18n" }, "Global": { + "actions": "Действия", + "all-types": "Все типы", "statistics": "Статистика", "your-content": "Ваш контент", - "fail": "Фаил", + "file": "Фаил", + "files": "Фаил", "content-type": "Тип контента", "quantity": "Количество", "size": "Размер", @@ -37,7 +40,20 @@ "current-status-of": "Текущий статус защищенности и активности системы мониторинга", "total-files": "Всего файлов", "disk-space-used": "Использовано места на диске", - "out-of": "из" + "out-of": "из", + "last-check": "Последняя проверка", + "all-dates": "Все даты", + "today": "Сегодня", + "for-a-week": "За неделю", + "for-a-month": "За месяц", + "older-than-a-month": "Старше месяца", + "show": "Показать", + "no-data-for-selected-filters": "Нет данных по выбранным фильтрам", + "page": "Страница", + "shown": "Показано", + "date-filter": "Фильтр по дате", + "type-filter": "Фильтр по типу", + "items-per-page": "Записей на странице" }, "Login-register-form": { "and": "и",