add notification list, add notification page

This commit is contained in:
smanylov
2026-03-30 19:11:48 +07:00
parent 884fb8dc50
commit f4b0a63e23
7 changed files with 376 additions and 47 deletions
@@ -0,0 +1,12 @@
import PageTitle from '@/app/ui/page-title';
import { NotificationsList } from '@/app/ui/notifications/notifications-list';
export default function Page() {
return (
<div>
<PageTitle title='notifications' />
<NotificationsList />
</div>
)
}
+41 -18
View File
@@ -124,8 +124,10 @@
} }
} }
.notification-dropdown { .notification {
.notification-header { /* &-dropdown {} */
&-header {
padding: 15px 20px; padding: 15px 20px;
border-bottom: 1px solid v.$b-color-1; border-bottom: 1px solid v.$b-color-1;
display: flex; display: flex;
@@ -138,29 +140,30 @@
font-weight: 600; font-weight: 600;
color: v.$text-p; color: v.$text-p;
} }
}
.notification-count { &-count {
font-size: 12px; font-size: 12px;
color: v.$p-color; color: v.$p-color;
font-weight: 500; font-weight: 500;
} cursor: pointer;
} }
.notification-list { &-list {
max-height: 400px; max-height: 600px;
overflow-y: auto; overflow-y: auto;
} }
.notification-item { &-item {
display: flex; display: flex;
gap: 12px; gap: 4px;
padding: 15px 20px; padding: 5px;
border-bottom: 1px solid v.$b-color-1; border-bottom: 1px solid v.$b-color-1;
transition: background-color 0.2s; transition: background-color 0.2s;
cursor: pointer; cursor: pointer;
} }
.notification-title { &-title {
display: block; display: block;
font-size: 15px; font-size: 15px;
font-weight: 700; font-weight: 700;
@@ -168,24 +171,43 @@
margin-bottom: 6px; margin-bottom: 6px;
} }
.notification-icon { &-content {
font-size: 24px; width: 100%;
flex-shrink: 0; position: relative;
padding: 15px 5px;
/* display: flex;
align-items: center; */
} }
.notification-text { &-icon {
font-size: 24px;
flex-shrink: 0;
display: flex;
align-items: center;
.icon {
color: v.$p-color;
}
}
&-text {
font-size: 13px; font-size: 13px;
color: v.$text-s; color: v.$text-s;
margin-bottom: 4px; /* margin-bottom: 4px; */
font-weight: 500; font-weight: 500;
} }
.notification-time { &-data {
font-size: 12px; font-size: 10px;
color: v.$text-m; color: v.$text-m;
display: flex;
justify-content: end;
position: absolute;
top: 0px;
right: 0px;
} }
.notification-footer { &-footer {
padding: 12px 20px; padding: 12px 20px;
border-top: 1px solid v.$b-color-1; border-top: 1px solid v.$b-color-1;
text-align: center; text-align: center;
@@ -195,6 +217,7 @@
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
text-decoration: none; text-decoration: none;
cursor: pointer;
} }
} }
} }
+76
View File
@@ -5047,3 +5047,79 @@
padding: 10px 16px; padding: 10px 16px;
} }
} }
.notifications-list {
&-wrapper {
width: 100%;
overflow-x: auto;
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);
}
&-header {
background-color: v.$bg-light;
padding: 20px 20px;
font-size: 18px;
font-weight: 600;
border-bottom: 1px solid v.$b-color-2;
display: flex;
justify-content: space-between;
align-items: center;
h4 {
font-size: 20px;
height: 36px;
display: flex;
align-items: center;
}
}
&-action {
.btn-primary {
/* background: v.$p-color; */
background: #2b7fff;
padding: 5px 20px;
}
}
&-body {
padding: 0;
.notification {
&-item {
border-bottom: 1px solid v.$b-color-2;
padding: 20px;
display: flex;
gap: 15px;
&:last-child {
border-bottom: none;
}
}
&-check {
display: flex;
align-items: center;
button {
cursor: pointer;
border: 2px solid v.$border-color-1;
border-radius: 4px;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
transition: all .3s ease-in;
display: flex;
&:hover {
border: 2px solid v.$border-color-1-hover;
}
}
}
&-text {}
}
}
}
+95 -17
View File
@@ -1,18 +1,60 @@
"use client" "use client"
import { useState, useRef } from 'react'; import { useState, useRef, useEffect } from 'react';
import { useClickOutside } from '@/app/hooks/useClickOutside'; import { useClickOutside } from '@/app/hooks/useClickOutside';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import Link from 'next/link'; import Link from 'next/link';
import { IconNotification } from '@/app/ui/icons/icons'; import { IconNotification } from '@/app/ui/icons/icons';
interface Notification {
id: string,
notificationText: string,
data: string,
link: string,
}
export default function NotificationsButton() { export default function NotificationsButton() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [data, setData] = useState<string | null>(null); const [notificationData, setNotificationData] = useState<Notification[] | []>([]);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const menuRef = useRef<HTMLDivElement | null>(null); const menuRef = useRef<HTMLDivElement | null>(null);
const t = useTranslations('Global'); const t = useTranslations('Global');
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: '#'
}
])
}, [])
useClickOutside( useClickOutside(
menuRef, menuRef,
() => { () => {
@@ -29,6 +71,14 @@ export default function NotificationsButton() {
} }
}; };
function markAsReadHandler() {
console.log('click: mark as read');
}
function notificationClickHandler(id: string) {
console.log(`click: ${id}`);
}
return ( return (
<div className="notification-wrapper" ref={menuRef}> <div className="notification-wrapper" ref={menuRef}>
<button <button
@@ -50,35 +100,63 @@ export default function NotificationsButton() {
<h4> <h4>
{t('notifications')} {t('notifications')}
</h4> </h4>
<span className="notification-count"> <Link
{t('no-new')} href={'/pages/notifications'}
</span> className="notification-count"
onClick={() => {
setIsOpen(false);
}}
>
{t('no-new')} / {t('all')}
</Link>
</div> </div>
{data ? ( {notificationData.length !== 0 ? (
<div className="notification-list"> <div
<div className="notification-item"> className="notification-list"
>
{notificationData.map((item) => {
return (
<div
key={item.id}
className="notification-item"
onClick={() => {
notificationClickHandler(item.id);
}}
>
<div className="notification-icon"> <div className="notification-icon">
<IconNotification /> <IconNotification />
</div> </div>
<div className="notification-content"> <div className="notification-content">
<div className="notification-title"> <div className="notification-data">
{item.data}
</div>
{/* <div className="notification-title">
{t('tokens-added')} {t('tokens-added')}
</div> </div> */}
<div className="notification-text">{t('added')} 100 {t('tokens')}.</div>
<div className="notification-time">4 {t('days-ago')}</div> <div className="notification-text">
{item.notificationText}
</div> </div>
</div> </div>
{JSON.stringify(data)} </div>
)
})}
</div> </div>
) : ( ) : (
<p> <p>
{t('no-data')} {/* {t('no-data')} */}
</p> </p>
)} )}
<div className="notification-footer"> <div className="notification-footer">
<Link href="#" className="notification-link"> <button
{t('view-all')} className="notification-link"
</Link> onClick={() => {
markAsReadHandler();
}}
>
{t('mark-all-as-read')}
</button>
</div> </div>
</> </>
)} )}
@@ -0,0 +1,132 @@
'use client'
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,
}
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: '#'
},
{
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: '#'
}
];
export function NotificationsList() {
const t = useTranslations('Global');
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
function selectHandler(fileId: string) {
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());
}
return (
<div
className="block-wrapper"
>
<div
className="notifications-list-wrapper"
>
<div
className="notifications-list-header"
>
<h4>
{t('all-notifications')}
</h4>
<div
className="notifications-list-action"
>
{(selectedFiles.size !== 0) && (
<button
className="btn btn-primary"
onClick={() => {
clearHandler();
}}
>
{t('deselect')}
</button>
)}
</div>
</div>
<div
className="notifications-list-body"
>
{(noitficationList.length !== 0) && (
noitficationList.map(item => {
return (
<div
className="notification-item"
>
<div
className="notification-check"
>
<button
onClick={() => {
selectHandler(item.id);
}}
>
{selectedFiles.has(item.id) && (
<IconCheck />
)}
</button>
</div>
<div
className="notification-text"
>
{item.notificationText}
</div>
</div>
)
})
)}
</div>
</div>
</div>
)
}
+5 -1
View File
@@ -382,7 +382,11 @@
"no-information-about-the-complaint": "No information about the complaint", "no-information-about-the-complaint": "No information about the complaint",
"the-complaint-has-not-been-registered": "The complaint has not been registered", "the-complaint-has-not-been-registered": "The complaint has not been registered",
"the-complaint-has-been-registered": "The complaint has been registered", "the-complaint-has-been-registered": "The complaint has been registered",
"all-statuses": "All statuses" "all-statuses": "All statuses",
"all": "All",
"mark-all-as-read": "Mark all as read",
"all-notifications": "All notifications",
"deselect": "Deselect"
}, },
"Login-register-form": { "Login-register-form": {
"and": "and", "and": "and",
+5 -1
View File
@@ -382,7 +382,11 @@
"no-information-about-the-complaint": "Нет информации о жалобе", "no-information-about-the-complaint": "Нет информации о жалобе",
"the-complaint-has-not-been-registered": "Жалоба не зарегистрирована", "the-complaint-has-not-been-registered": "Жалоба не зарегистрирована",
"the-complaint-has-been-registered": "Жалоба зарегистрирована", "the-complaint-has-been-registered": "Жалоба зарегистрирована",
"all-statuses": "Все статусы" "all-statuses": "Все статусы",
"all": "Все",
"mark-all-as-read": "Отметить все как прочитанное",
"all-notifications": "Все уведомления",
"deselect": "Снять выделение"
}, },
"Login-register-form": { "Login-register-form": {
"and": "и", "and": "и",