diff --git a/package.json b/package.json index 57d6a90..fdd11bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "no-copy-frontend", - "version": "0.78.0", + "version": "0.79.0", "private": true, "scripts": { "dev": "next dev -p 2999", diff --git a/src/app/actions/notificationActions.ts b/src/app/actions/notificationActions.ts new file mode 100644 index 0000000..81d3fe2 --- /dev/null +++ b/src/app/actions/notificationActions.ts @@ -0,0 +1,130 @@ +'use server' + +import { getSessionData } from '@/app/actions/session'; +import { API_BASE_URL } from '@/app/actions/definitions'; + +export interface NotificationBody { + createdAt: string | null, + id: number, + message: string, + status: string | null, + type: string | null +} + +export async function fetchActiveNotifications() { + const token = await getSessionData('token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 30015, + message_body: { + action: 'active', + authToken: token, + } + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (response.ok) { + let parsed = await response.json(); + + if (parsed?.message_body) { + return parsed?.message_body + } else { + throw parsed; + } + } else { + throw (`${response.status}`); + } + } catch (error) { + return false + } +} + +export async function fetchAllNotifications() { + const token = await getSessionData('token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 30015, + message_body: { + action: 'list', + authToken: token, + page: 0, + size: 10, + sortBy: "createdAt", + sortDirection: "DESC", + types: [], //"SEARCH_RESULT", "MONITORING_RESULT" + statuses: [] //"NEW" + } + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (response.ok) { + let parsed = await response.json(); + + if (parsed?.message_body) { + return parsed?.message_body + } else { + throw parsed; + } + } else { + throw (`${response.status}`); + } + } catch (error) { + return false + } +} + +export async function notificationsMarkAsRead(ids: number[]) { + const token = await getSessionData('token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 30015, + message_body: { + action: 'markRead', + authToken: token, + notificationIds: ids + } + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + + if (response.ok) { + let parsed = await response.json(); + + if (parsed?.message_body) { + return parsed?.message_body + } else { + throw parsed; + } + } else { + throw (`${response.status}`); + } + } catch (error) { + return { + success: false + } + } +} diff --git a/src/app/hooks/react-query/useActiveNotificationsList.ts b/src/app/hooks/react-query/useActiveNotificationsList.ts new file mode 100644 index 0000000..dbcda12 --- /dev/null +++ b/src/app/hooks/react-query/useActiveNotificationsList.ts @@ -0,0 +1,27 @@ +import { useQuery } from '@tanstack/react-query'; +import { fetchActiveNotifications, NotificationBody } from '@/app/actions/notificationActions'; + +export interface UseActiveNotifications { + notifications: NotificationBody[], + unreadCount: number +} + +export const useActiveNotifications = () => { + return useQuery({ + queryKey: ['activeNotifications'], + queryFn: () => { + return fetchActiveNotifications() + }, + select: (data: UseActiveNotifications | null) => { + if (!data) { + return { + notifications: [], + unreadCount: 0 + } + } + return data; + }, + retry: false, + refetchInterval: 10000 + }); +}; \ No newline at end of file diff --git a/src/app/hooks/react-query/useAllNotificationsList.ts b/src/app/hooks/react-query/useAllNotificationsList.ts new file mode 100644 index 0000000..9bc5d90 --- /dev/null +++ b/src/app/hooks/react-query/useAllNotificationsList.ts @@ -0,0 +1,27 @@ +import { useQuery } from '@tanstack/react-query'; +import { fetchAllNotifications, NotificationBody } from '@/app/actions/notificationActions'; + +export interface UseAllNotifications { + notifications: NotificationBody[], + unreadCount: number +} + +export const useAllNotifications = () => { + return useQuery({ + queryKey: ['allNotifications'], + queryFn: () => { + return fetchAllNotifications() + }, + select: (data: UseAllNotifications | null) => { + if (!data) { + return { + notifications: [], + unreadCount: 0 + } + } + return data; + }, + retry: false, + refetchInterval: 10000 + }); +}; \ No newline at end of file diff --git a/src/app/styles/header.scss b/src/app/styles/header.scss index 67158ae..1390e53 100644 --- a/src/app/styles/header.scss +++ b/src/app/styles/header.scss @@ -148,6 +148,10 @@ color: v.$p-color; font-weight: 500; cursor: pointer; + + &:hover { + color: v.$p-color-hover; + } } &-list { @@ -161,7 +165,10 @@ padding: 5px 10px 5px 10px; border-bottom: 1px solid v.$b-color-1; transition: background-color 0.2s; - cursor: pointer; + + &.loading { + opacity: 0.5; + } } &-title { @@ -180,17 +187,6 @@ align-items: center; */ } - &-icon { - font-size: 24px; - flex-shrink: 0; - display: flex; - align-items: center; - - .icon { - color: v.$p-color; - } - } - &-text { font-size: 13px; color: v.$text-s; @@ -198,7 +194,8 @@ font-weight: 500; } - &-data { + &-data, + &-mark-as-read { font-size: 10px; color: v.$text-m; display: flex; @@ -208,10 +205,25 @@ right: 0px; } + &-mark-as-read { + font-size: 12px; + bottom: 0px; + top: auto; + cursor: pointer; + height: max-content; + + color: v.$p-color; + + &:hover { + color: v.$p-color-hover; + } + } + &-footer { padding: 12px 20px; border-top: 1px solid v.$b-color-1; text-align: center; + position: relative; .notification-link { color: v.$p-color; @@ -219,6 +231,14 @@ font-weight: 500; text-decoration: none; cursor: pointer; + + &:disabled { + opacity: 0.5; + } + + &:hover { + color: v.$p-color-hover; + } } } } diff --git a/src/app/styles/variable.scss b/src/app/styles/variable.scss index f6902bf..a7a8be8 100644 --- a/src/app/styles/variable.scss +++ b/src/app/styles/variable.scss @@ -1,4 +1,5 @@ $p-color: #6366f1; +$p-color-hover: #1e0ff0; $s-color: #8b5cf6; $text-p: #1f2937; $text-s: #4b5563; diff --git a/src/app/ui/header/notificationsButton.tsx b/src/app/ui/header/notificationsButton.tsx index 38a23c7..3b113f9 100644 --- a/src/app/ui/header/notificationsButton.tsx +++ b/src/app/ui/header/notificationsButton.tsx @@ -5,55 +5,24 @@ import { useClickOutside } from '@/app/hooks/useClickOutside'; import { useTranslations } from 'next-intl'; import Link from 'next/link'; import { IconNotification } from '@/app/ui/icons/icons'; - -interface Notification { - id: string, - notificationText: string, - data: string, - link: string, -} +import { useActiveNotifications } from '@/app/hooks/react-query/useActiveNotificationsList'; +import { NotificationBody, notificationsMarkAsRead } from '@/app/actions/notificationActions'; +import { useQueryClient } from '@tanstack/react-query'; export default function NotificationsButton() { const [isOpen, setIsOpen] = useState(false); - const [notificationData, setNotificationData] = useState([]); const [error, setError] = useState(null); const menuRef = useRef(null); + const [loadingIds, setLoadingIds] = useState>(new Set()); + const [isMarkingAll, setIsMarkingAll] = useState(false); const t = useTranslations('Global'); + const { data: notificationList } = useActiveNotifications(); + const queryClient = useQueryClient(); + useEffect(() => { - setNotificationData([ - { - id: '1', - notificationText: 'text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1', - data: '20.20.20', - link: '#' - }, - { - id: '2', - notificationText: 'text2', - data: '20.20.20', - link: '#' - }, - { - id: '3', - notificationText: 'text3', - data: '20.20.20', - link: '#' - }, - { - id: '4', - notificationText: 'text4', - data: '20.20.20', - link: '#' - }, - { - id: '5', - notificationText: 'text5', - data: '20.20.20', - link: '#' - } - ]) - }, []) + console.log(notificationList); + }, [notificationList]) useClickOutside( menuRef, @@ -71,12 +40,42 @@ export default function NotificationsButton() { } }; - function markAsReadHandler() { - console.log('click: mark as read'); + 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; + }); + } } - function notificationClickHandler(id: string) { - console.log(`click: ${id}`); + async function markAllAsReadHandler() { + const array = notificationList?.notifications?.map(e => e.id) ?? []; + if (array.length === 0) return; + + setIsMarkingAll(true); + + try { + await markAsReadHandler(array); + } finally { + setIsMarkingAll(false); + } } return ( @@ -107,57 +106,79 @@ export default function NotificationsButton() { setIsOpen(false); }} > - {t('no-new')} / {t('all')} + {notificationList?.unreadCount ? notificationList?.unreadCount : t('no-new')} / {t('all')} - {notificationData.length !== 0 ? ( -
- {notificationData.map((item) => { - return ( -
{ - notificationClickHandler(item.id); - }} - > -
- -
-
-
- {item.data} -
+ {notificationList?.notifications.length !== 0 ? ( + <> +
+ {notificationList?.notifications?.map((item) => { + return ( +
+
+
+ {item.createdAt ? item.createdAt : '#'} +
- {/*
+ {/*
{t('tokens-added')}
*/} -
- {item.notificationText} +
+ {item.message} +
+ + + + {loadingIds.has(item.id) && ( +
+
+
+ )}
-
- ) - })} -
+ ) + })} +
+ +
+ +
+ ) : ( -

- {/* {t('no-data')} */} +

+ {t('no-new-notifications')}

)} -
- -
)}
diff --git a/src/app/ui/notifications/notifications-list.tsx b/src/app/ui/notifications/notifications-list.tsx index bf11567..3326878 100644 --- a/src/app/ui/notifications/notifications-list.tsx +++ b/src/app/ui/notifications/notifications-list.tsx @@ -2,65 +2,19 @@ import { useTranslations } from 'next-intl'; import { IconCheck } from '@/app/ui/icons/icons'; -import { useState } from 'react'; - -interface Notification { - id: string, - notificationText: string, - data: string, - link: string, - title: string, - status: string, -} - -const noitficationList: Notification[] = [ - { - id: '1', - notificationText: 'text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1 text1', - data: '20.20.20', - link: '#', - title: 'title 1', - status: 'status' - }, - { - id: '2', - notificationText: 'text2 text2 text2 text2 text2 text2 text2 text2 text2 text2 text2 text2 text2 text2 text2 text2 text2 text2 text2 text2 text2 text2 text2 text2 text2 ', - data: '20.20.20', - link: '#', - title: 'title 2', - status: 'status' - }, - { - id: '3', - notificationText: 'text3 text3 text3 text3 text3 text3 text3 text3 text3 text3 text3 text3 text3 text3 text3 text3 text3 text3 text3 text3 text3 text3 text3 text3 ', - data: '20.20.20', - link: '#', - title: 'title 3', - status: 'status' - }, - { - id: '4', - notificationText: 'text4 text4 text4 text4 text4 text4 text4 text4 text4 text4 text4 text4 text4 text4 text4 text4 text4 text4 text4 text4 text4 text4 text4 text4 text4 ', - data: '20.20.20', - link: '#', - title: 'title 4', - status: 'status' - }, - { - id: '5', - notificationText: 'text5 text5 text5 text5 text5 text5 text5 text5 text5 text5 text5 text5 text5 text5 text5 text5 text5 text5 text5 text5 text5 text5 text5 text5 text5 ', - data: '20.20.20', - link: '#', - title: 'title 5', - status: 'status' - } -]; +import { useEffect, useState } from 'react'; +import { useAllNotifications } from '@/app/hooks/react-query/useAllNotificationsList'; export function NotificationsList() { const t = useTranslations('Global'); - const [selectedFiles, setSelectedFiles] = useState>(new Set()); + const [selectedFiles, setSelectedFiles] = useState>(new Set()); + const { data: allNotifications } = useAllNotifications(); - function selectHandler(fileId: string) { + useEffect(() => { + console.log(allNotifications); + }, [allNotifications]) + + function selectHandler(fileId: number) { setSelectedFiles((prev) => { const newSet = new Set(prev); @@ -78,7 +32,7 @@ export function NotificationsList() { } function selectAllHandler() { - setSelectedFiles(new Set(noitficationList.map(item => item.id))); + setSelectedFiles(new Set(allNotifications?.notifications.map(item => item.id))); } return ( @@ -129,8 +83,8 @@ export function NotificationsList() {
- {(noitficationList.length !== 0) && ( - noitficationList.map(item => { + {(allNotifications?.notifications.length !== 0) && ( + allNotifications?.notifications.map(item => { return (
- {item.title} + {item.type}
- {item.notificationText} + {item.message}
- {item.data} + {item.createdAt}