continue: add pagination for notification page and some featur

This commit is contained in:
smanylov
2026-04-05 15:06:08 +07:00
parent 27ca7ddf8e
commit 1fe49c24e8
5 changed files with 222 additions and 75 deletions
+3 -3
View File
@@ -47,7 +47,7 @@ export async function fetchActiveNotifications() {
} }
} }
export async function fetchAllNotifications() { export async function fetchAllNotifications(page: number = 0, size: number = 10) {
const token = await getSessionData('token'); const token = await getSessionData('token');
try { try {
@@ -59,8 +59,8 @@ export async function fetchAllNotifications() {
message_body: { message_body: {
action: 'list', action: 'list',
authToken: token, authToken: token,
page: 0, page: page,
size: 10, size: size,
sortBy: "createdAt", sortBy: "createdAt",
sortDirection: "DESC", sortDirection: "DESC",
types: [], //"SEARCH_RESULT", "MONITORING_RESULT" types: [], //"SEARCH_RESULT", "MONITORING_RESULT"
@@ -3,20 +3,24 @@ import { fetchAllNotifications, NotificationBody } from '@/app/actions/notificat
export interface UseAllNotifications { export interface UseAllNotifications {
notifications: NotificationBody[], notifications: NotificationBody[],
unreadCount: number currentPage: number,
totalElements: number,
totalPages: number,
} }
export const useAllNotifications = () => { export const useAllNotifications = (page: number, size: number = 10) => {
return useQuery({ return useQuery({
queryKey: ['allNotifications'], queryKey: ['allNotifications', page, size],
queryFn: () => { queryFn: () => {
return fetchAllNotifications() return fetchAllNotifications(page, size)
}, },
select: (data: UseAllNotifications | null) => { select: (data: UseAllNotifications | null) => {
if (!data) { if (!data) {
return { return {
notifications: [], notifications: [],
unreadCount: 0 currentPage: 0,
totalElements: 0,
totalPages: 0,
} }
} }
return data; return data;
+65 -59
View File
@@ -4735,65 +4735,6 @@
} }
} }
.pagination-controls {
display: flex;
align-items: center;
gap: 0.5rem;
.arrow {
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
border: 2px solid v.$border-color-1;
border-radius: 10px;
box-shadow: 0 1px 2px #0000000d;
color: v.$p-color;
.icon {
width: 18px;
}
&:hover {
background-color: v.$bg-light;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
&-pages {
display: flex;
align-items: center;
gap: 0.25rem;
button {
border: 2px solid v.$border-color-1;
padding: 0.25rem 0.75rem;
border-radius: 10px;
color: v.$p-color;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&.current {
background-color: v.$p-color;
color: v.$white;
}
&.other {
&:hover {
background-color: v.$bg-light;
}
}
}
}
}
.violation-page-note { .violation-page-note {
margin-top: auto; margin-top: auto;
margin-bottom: 0; margin-bottom: 0;
@@ -5117,6 +5058,7 @@
border-radius: 0.5rem; border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
max-width: calc(100vw - var(--side-bar-width) - 60px); max-width: calc(100vw - var(--side-bar-width) - 60px);
background-color: v.$white;
@media (max-width: 440px) { @media (max-width: 440px) {
max-width: calc(100vw - var(--side-bar-width) - 30px); max-width: calc(100vw - var(--side-bar-width) - 30px);
@@ -5247,6 +5189,11 @@
} }
} }
} }
&-footer {
padding: 10px;
border-top: 1px solid v.$b-color-2;
}
} }
.payment-user-cards { .payment-user-cards {
@@ -5375,3 +5322,62 @@
line-height: 1.6; line-height: 1.6;
} }
} }
.pagination-controls {
display: flex;
align-items: center;
gap: 0.5rem;
.arrow {
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
border: 2px solid v.$border-color-1;
border-radius: 10px;
box-shadow: 0 1px 2px #0000000d;
color: v.$p-color;
.icon {
width: 18px;
}
&:hover {
background-color: v.$bg-light;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
&-pages {
display: flex;
align-items: center;
gap: 0.25rem;
button {
border: 2px solid v.$border-color-1;
padding: 0.25rem 0.75rem;
border-radius: 10px;
color: v.$p-color;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&.current {
background-color: v.$p-color;
color: v.$white;
}
&.other {
&:hover {
background-color: v.$bg-light;
}
}
}
}
}
@@ -20,10 +20,6 @@ export default function NotificationsButton() {
const { data: notificationList } = useActiveNotifications(); const { data: notificationList } = useActiveNotifications();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
useEffect(() => {
console.log(notificationList);
}, [notificationList])
useClickOutside( useClickOutside(
menuRef, menuRef,
() => { () => {
+144 -3
View File
@@ -1,14 +1,59 @@
'use client' 'use client'
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { IconCheck } from '@/app/ui/icons/icons'; import { IconCheck, IconArrowLeft, IconArrowRight, IconDoubleArrowLeft, IconDoubleArrowRight } from '@/app/ui/icons/icons';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useAllNotifications } from '@/app/hooks/react-query/useAllNotificationsList'; import { useAllNotifications } from '@/app/hooks/react-query/useAllNotificationsList';
import { notificationsMarkAsRead } from '@/app/actions/notificationActions';
import { useQueryClient } from '@tanstack/react-query';
export function NotificationsList() { export function NotificationsList() {
const t = useTranslations('Global'); const t = useTranslations('Global');
const queryClient = useQueryClient();
const [selectedFiles, setSelectedFiles] = useState<Set<number>>(new Set()); const [selectedFiles, setSelectedFiles] = useState<Set<number>>(new Set());
const { data: allNotifications } = useAllNotifications(); const [loadingIds, setLoadingIds] = useState<Set<number>>(new Set());
const [error, setError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(0);
const [pageSize] = useState(10);
const { data: allNotifications, isLoading } = useAllNotifications(currentPage, pageSize);
const totalPages = allNotifications?.totalPages || 0;
const currentPageNum = allNotifications?.currentPage || 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 getPageNumbers = (): (number | string)[] => {
const delta: number = 2;
const range: number[] = [];
const rangeWithDots: (number | string)[] = [];
let l: number | undefined;
for (let i = 0; i < totalPages; i++) {
if (i === 0 || i === totalPages - 1 || (i >= currentPageNum - delta && i <= currentPageNum + delta)) {
range.push(i);
}
}
range.forEach((i: number) => {
if (l !== undefined) {
if (i - l === 2) {
rangeWithDots.push(l + 1);
} else if (i - l !== 1) {
rangeWithDots.push('...');
}
}
rangeWithDots.push(i);
l = i;
});
return rangeWithDots;
};
useEffect(() => { useEffect(() => {
console.log(allNotifications); console.log(allNotifications);
@@ -35,6 +80,46 @@ export function NotificationsList() {
setSelectedFiles(new Set(allNotifications?.notifications.map(item => item.id))); setSelectedFiles(new Set(allNotifications?.notifications.map(item => item.id)));
} }
async function markAsReadHandler(ids: number[]) {
setLoadingIds(prev => {
const newSet = new Set(prev);
ids.forEach(id => newSet.add(id));
return newSet;
});
try {
/* const response = await notificationsMarkAsRead(ids);
if (response.success) {
await queryClient.invalidateQueries({ queryKey: ['activeNotifications'] });
await queryClient.invalidateQueries({ queryKey: ['allNotifications'] });
} */
} catch (error) {
setError(error instanceof Error ? error.message : 'Failed to mark as read');
} 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;
});
}
}
function removeHandler(id: number) {
console.log(id);
setSelectedFiles(prev => {
prev.delete(id)
return new Set(prev);
});
}
return ( return (
<div <div
className="notifications-list-wrapper" className="notifications-list-wrapper"
@@ -63,7 +148,8 @@ export function NotificationsList() {
<button <button
className="btn btn-primary" className="btn btn-primary"
onClick={() => { onClick={() => {
clearHandler(); const array = Array.from(selectedFiles);
markAsReadHandler(array);
}} }}
> >
{t('mark-all-as-read')} {t('mark-all-as-read')}
@@ -137,6 +223,10 @@ export function NotificationsList() {
<button <button
className="btn btn-remove" className="btn btn-remove"
onClick={() => {
removeHandler(item.id);
}}
disabled={false}
> >
{t('remove')} {t('remove')}
</button> </button>
@@ -147,6 +237,57 @@ export function NotificationsList() {
}) })
)} )}
</div> </div>
<div
className="notifications-list-footer"
>
{totalPages > 0 && (
<div className="pagination-controls">
<button
className="arrow"
onClick={goToFirstPage}
disabled={currentPageNum === 0}
>
<IconDoubleArrowLeft />
</button>
<button
onClick={goToPrevPage}
className="arrow"
disabled={currentPageNum === 0}
>
<IconArrowLeft />
</button>
<div className="pagination-controls-pages">
{getPageNumbers().map((pageNum, index) => (
<button
key={index}
onClick={() => typeof pageNum === 'number' && goToPage(pageNum)}
className={currentPageNum === pageNum ? 'current' : 'other'}
disabled={pageNum === '...'}
>
{pageNum === '...' ? '...' : (pageNum as number) + 1}
</button>
))}
</div>
<button
onClick={goToNextPage}
className="arrow"
disabled={currentPageNum === totalPages - 1}
>
<IconArrowRight />
</button>
<button
className="arrow"
onClick={goToLastPage}
disabled={currentPageNum === totalPages - 1}
>
<IconDoubleArrowRight />
</button>
</div>
)}
</div>
</div> </div>
) )
} }