From d0901961be199ec88c62f1551465ca60f3c33082 Mon Sep 17 00:00:00 2001 From: smanylov Date: Mon, 13 Apr 2026 15:10:24 +0700 Subject: [PATCH] rework notification-table --- src/app/styles/pages-styles.scss | 305 +++++++++++-- .../ui/notifications/notifications-list.tsx | 406 ++++++++++-------- 2 files changed, 511 insertions(+), 200 deletions(-) diff --git a/src/app/styles/pages-styles.scss b/src/app/styles/pages-styles.scss index 2aef8b2..b04e0dc 100644 --- a/src/app/styles/pages-styles.scss +++ b/src/app/styles/pages-styles.scss @@ -5131,7 +5131,6 @@ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); color: v.$white; - display: flex; justify-content: space-between; align-items: center; @@ -5142,6 +5141,35 @@ display: flex; align-items: center; } + + .select-all-header { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; + + .btn-primary { + background: white; + color: black; + font-weight: 600; + font-size: 14px; + padding: 5px 20px; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + border: none; + cursor: pointer; + border-radius: 6px; + transition: all 0.2s ease; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 15px 20px -3px rgba(0, 0, 0, 0.15); + } + + &:active { + transform: translateY(0); + } + } + } } &-action { @@ -5162,30 +5190,76 @@ padding: 0; background: v.$white; - .notification { - &-item { - border-bottom: 1px solid v.$b-color-2; - padding: 20px; + .notifications-table { + width: 100%; + } + + .table-row { + display: flex; + border-bottom: 1px solid v.$b-color-2; + transition: background-color 0.2s ease; + + &:last-child { + border-bottom: none; + } + + &.n-header { + background-color: #f8f9fa; + border-bottom: 2px solid v.$b-color-2; + + .table-cell { + padding: 15px 20px; + font-weight: 600; + color: v.$text-m; + font-size: 14px; + } + } + + &.loading { + opacity: 0.6; + pointer-events: none; + } + + &.READIED { + + .notification-title, + .notification-text { + font-weight: 500; + opacity: 0.8; + } + } + + &:hover:not(.n-header) { + background-color: #fafafa; + } + } + + .table-cell { + padding: 20px; + + &:first-child { +/* width: 60px; + min-width: 60px; + max-width: 60px; */ display: flex; - gap: 15px; + align-items: center; + } - &:last-child { - border-bottom: none; - } + &:last-child { + flex: 1; + } + } - &.loading { - opacity: 0.6; - } + .notification { + &-body { + flex-grow: 1; } &-header { display: flex; justify-content: space-between; align-items: center; - } - - &-body { - flex-grow: 1; + margin-bottom: 8px; } &-title { @@ -5196,9 +5270,10 @@ &-text { color: v.$text-s; - margin-bottom: 4px; + margin-bottom: 12px; font-weight: 800; font-size: 16px; + line-height: 1.4; } &-status { @@ -5213,21 +5288,39 @@ display: flex; justify-content: space-between; align-items: center; + gap: 15px; .btn-remove { color: v.$white; background: v.$status-new; - padding: 4px 18px; + padding: 6px 18px; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: 600; + font-size: 13px; + transition: all 0.2s ease; &:hover { background: #ff9100; + transform: translateY(-1px); + } + + &:active { + transform: translateY(0); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; } } } &-date { color: v.$text-m; - font-size: 14px; + font-size: 13px; text-align: start; } @@ -5238,16 +5331,29 @@ button { cursor: pointer; border: 2px solid v.$border-color-1; - border-radius: 4px; + border-radius: 6px; justify-content: center; align-items: center; width: 24px; height: 24px; - transition: all .3s ease-in; + transition: all 0.2s ease; display: flex; + background: white; + padding: 0; &:hover { - border: 2px solid v.$border-color-1-hover; + border-color: v.$border-color-1-hover; + transform: scale(1.05); + } + + &:active { + transform: scale(0.95); + } + + svg { + width: 14px; + height: 14px; + color: v.$status-new; } } @@ -5258,19 +5364,162 @@ } } - .notification-item.READIED { + .no-notifications { + text-align: center; + padding: 60px 20px; + color: v.$text-m; + font-size: 16px; - .notification-title, - .notification-text { - font-weight: 500; - opacity: 0.8; + &::before { + content: "🔔"; + display: block; + font-size: 48px; + margin-bottom: 16px; + opacity: 0.5; } } } &-footer { - padding: 10px; + padding: 15px; border-top: 1px solid v.$b-color-2; + background: #fafafa; + } + + .pagination-controls { + display: flex; + gap: 8px; + align-items: center; + justify-content: center; + + button { + cursor: pointer; + border: 1px solid v.$b-color-2; + background: white; + padding: 8px 12px; + border-radius: 6px; + transition: all 0.2s ease; + color: v.$text-s; + font-weight: 500; + min-width: 36px; + + &:hover:not(:disabled) { + background: v.$b-color-2; + border-color: v.$border-color-1-hover; + transform: translateY(-1px); + } + + &:active:not(:disabled) { + transform: translateY(0); + } + + &.current { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: white; + border-color: transparent; + } + + &.other { + background: white; + } + + &.arrow { + display: flex; + align-items: center; + justify-content: center; + padding: 8px 10px; + + svg { + width: 16px; + height: 16px; + } + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + transform: none; + } + } + } + + .pagination-controls-pages { + display: flex; + gap: 6px; + margin: 0 8px; + } + + .error-message { + position: fixed; + bottom: 20px; + right: 20px; + background: #ef4444; + color: white; + padding: 12px 20px; + border-radius: 8px; + font-size: 14px; + z-index: 1000; + animation: slideIn 0.3s ease; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + } + + @keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + + to { + transform: translateX(0); + opacity: 1; + } + } +} + +// Responsive adjustments +@media (max-width: 768px) { + .notifications-list { + &-wrapper { + max-width: calc(100vw - var(--side-bar-width) - 20px); + } + + &-header { + flex-direction: column; + gap: 15px; + align-items: flex-start; + + h4 { + height: auto; + } + } + + .table-cell { + &:first-child { + width: 80px !important; + min-width: 80px !important; + max-width: 80px !important; + } + } + + .notification-footer { + flex-direction: column; + align-items: flex-start !important; + gap: 10px !important; + } + + .pagination-controls { + flex-wrap: wrap; + gap: 6px; + } + + .select-all-header { + flex-wrap: wrap; + + .btn-primary { + font-size: 12px; + padding: 4px 12px; + } + } } } diff --git a/src/app/ui/notifications/notifications-list.tsx b/src/app/ui/notifications/notifications-list.tsx index 6f42a29..5a0e5e5 100644 --- a/src/app/ui/notifications/notifications-list.tsx +++ b/src/app/ui/notifications/notifications-list.tsx @@ -4,34 +4,205 @@ import { useTranslations } from 'next-intl'; import { IconCheck, IconArrowLeft, IconArrowRight, IconDoubleArrowLeft, IconDoubleArrowRight } from '@/app/ui/icons/icons'; import { useEffect, useState } from 'react'; import { useAllNotifications } from '@/app/hooks/react-query/useAllNotificationsList'; -import { notificationsMarkAsRead, notificationsDelete } from '@/app/actions/notificationActions'; +import { notificationsMarkAsRead, notificationsDelete, NotificationBody } from '@/app/actions/notificationActions'; import { useQueryClient } from '@tanstack/react-query'; import { formatDate, formatDateTime } from '@/app/lib/formatDate'; +import { + useReactTable, + getCoreRowModel, + getPaginationRowModel, + flexRender, + createColumnHelper, + RowSelectionState, +} from '@tanstack/react-table'; + + export function NotificationsList() { const t = useTranslations('Global'); const norificationTranslate = useTranslations('Notifications'); const queryClient = useQueryClient(); - const [selectedFiles, setSelectedFiles] = useState>(new Set()); + const [rowSelection, setRowSelection] = useState({}); const [loadingIds, setLoadingIds] = useState>(new Set()); const [error, setError] = useState(null); - const [currentPage, setCurrentPage] = useState(0); - const [pageSize] = useState(10); - const { data: allNotifications, isLoading } = useAllNotifications(currentPage, pageSize); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const { data: allNotifications, isLoading } = useAllNotifications( + pagination.pageIndex, + pagination.pageSize + ); const totalPages = allNotifications?.totalPages || 0; - const currentPageNum = allNotifications?.currentPage || 0; + const totalRecords = allNotifications?.totalElements || 0; - const goToFirstPage = () => setCurrentPage(0); - const goToPrevPage = () => setCurrentPage(prev => Math.max(0, prev - 1)); - const goToNextPage = () => setCurrentPage(prev => Math.min(totalPages - 1, prev + 1)); - const goToLastPage = () => setCurrentPage(totalPages - 1); - const goToPage = (page: number) => setCurrentPage(page); + const columnHelper = createColumnHelper(); + + const columns = [ + columnHelper.display({ + id: 'select', + header: ({ table }) => { + const currentPageRows = table.getCoreRowModel().rows; + const unreadRowsOnPage = currentPageRows.filter(row => row.original.status !== 'READIED'); + const allUnreadSelectedOnPage = unreadRowsOnPage.every(row => rowSelection[row.original.id]); + + return ( +
+ + + {Object.keys(rowSelection).length > 0 && ( + + )} +
+ ); + }, + cell: ({ row }) => { + const isReadied = row.original.status === 'READIED'; + const isSelected = !!rowSelection[row.original.id]; + + return ( +
+ {!isReadied ? ( + + ) : ( +
+ )} +
+ ); + }, + }), + columnHelper.accessor('message', { + id: 'message', + header: () => null, // No header text, using custom header above + cell: ({ row }) => { + const item = row.original; + return ( +
+
+ {/* Optional header content */} +
+
+ {norificationTranslate.has(item.message) + ? norificationTranslate(item.message) + : item.message} ({item.id}) +
+
+
+ {item.createdAt + ? `${formatDate(item.createdAt)}: ${formatDateTime(item.createdAt)}` + : '---'} +
+ +
+
+ ); + }, + }), + ]; + + const table = useReactTable({ + data: allNotifications?.notifications || [], + columns, + state: { + rowSelection, + pagination, + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + manualPagination: true, + pageCount: totalPages, + }); + + async function notificationActionHandler(ids: number[], action: 'remove' | 'mark-as-read') { + setLoadingIds(prev => { + const newSet = new Set(prev); + ids.forEach(id => newSet.add(id)); + return newSet; + }); + + try { + let response; + + if (action === 'mark-as-read') { + response = await notificationsMarkAsRead(ids); + } else if (action === 'remove') { + response = await notificationsDelete(ids); + } + + if (response?.success) { + await queryClient.invalidateQueries({ queryKey: ['activeNotifications'] }); + await queryClient.invalidateQueries({ queryKey: ['allNotifications'] }); + + // Clear selection after successful action + setRowSelection({}); + } + } catch (error) { + const errorMessage = action === 'mark-as-read' ? 'Failed to mark as read' : 'Failed to remove'; + setError(errorMessage); + } finally { + setLoadingIds(prev => { + const newSet = new Set(prev); + ids.forEach(id => newSet.delete(id)); + return newSet; + }); + } + } const getPageNumbers = (): (number | string)[] => { const delta: number = 2; + const currentPageNum = pagination.pageIndex; const range: number[] = []; const rangeWithDots: (number | string)[] = []; let l: number | undefined; @@ -57,85 +228,25 @@ export function NotificationsList() { return rangeWithDots; }; - useEffect(() => { - console.log(allNotifications); - }, [allNotifications]) - - function selectHandler(fileId: number) { - setSelectedFiles((prev) => { - const newSet = new Set(prev); - - if (newSet.has(fileId)) { - newSet.delete(fileId) - } else { - newSet.add(fileId) - } - return newSet; - }); - } - - function clearHandler() { - setSelectedFiles(new Set()); - } - - function selectAllHandler() { - const idsToSelect = allNotifications?.notifications - .filter(item => item.status !== 'READIED') - .map(item => item.id); - - setSelectedFiles(new Set(idsToSelect)); - } - - async function notificationActionHandler(ids: number[], action: 'remove' | 'mark-as-read') { - setLoadingIds(prev => { - const newSet = new Set(prev); - ids.forEach(id => newSet.add(id)); - return newSet; - }); - - try { - let response; - - if (action === 'mark-as-read') { - response = await notificationsMarkAsRead(ids); - } else if (action === 'remove') { - response = await notificationsDelete(ids); - } - - if (response?.success) { - await queryClient.invalidateQueries({ queryKey: ['activeNotifications'] }); - await queryClient.invalidateQueries({ queryKey: ['allNotifications'] }); - } - } catch (error) { - const errorMessage = action === 'mark-as-read' ? 'Failed to mark as read' : 'Failed to remove'; - setError(errorMessage); - } finally { - setLoadingIds(prev => { - const newSet = new Set(prev); - ids.forEach(id => newSet.delete(id)); - return newSet; - }); - - setSelectedFiles(prev => { - const newSet = new Set(prev); - ids.forEach(id => newSet.delete(id)); - return newSet; - }); - } - } - return ( -
-
-

- {t('all-notifications')} -

- -
+
+

{t('all-notifications')}

+
+ {table.getHeaderGroups().map((headerGroup) => ( +
+ {headerGroup.headers.map((header) => ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ ))} +
+ ))} +
+ {/*
{(selectedFiles.size !== 0) && ( @@ -168,100 +279,49 @@ export function NotificationsList() { > {t('select-all')} -
+
*/}
-
- {(allNotifications?.notifications.length !== 0) && ( - allNotifications?.notifications.map(item => { - return ( -
+ +
+ {allNotifications?.notifications.length !== 0 ? ( +
+
+ {table.getRowModel().rows.map((row) => (
- {item.status !== 'READIED' ? ( - - ) : ( -
- )} -
- -
-
- {/*
- {item.type} {item.id} -
*/} - {/*
- {item.status} -
*/} -
-
- {norificationTranslate.has(item.message) ? norificationTranslate(item.message) : item.message} -
-
-
- {item.createdAt ? `${formatDate(item.createdAt)}: ${formatDateTime(item.createdAt)}` : '---'}
- - -
+ ))}
-
- ) - }) + ))} +
+
+ ) : ( +
{t('no-notifications')}
)}
-
+
{totalPages > 0 && (
@@ -270,8 +330,8 @@ export function NotificationsList() { {getPageNumbers().map((pageNum, index) => (
)}
+ + {error &&
{error}
}
- ) + ); } \ No newline at end of file