continue: add pagination for notification page and some featur
This commit is contained in:
@@ -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,11 +59,11 @@ 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"
|
||||||
statuses: [] //"NEW"
|
statuses: [] //"NEW"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -5374,4 +5321,63 @@
|
|||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
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,
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user