build notifications list with React
This commit is contained in:
parent
a6dc3bd068
commit
8e104c2422
@ -30,11 +30,13 @@ class ViewServiceProvider extends ServiceProvider
|
||||
View::composer('shared.head', Composers\HeadComposer::class);
|
||||
|
||||
View::composer('shared.notifications', function ($view) {
|
||||
$notifications = auth()->user()->unreadNotifications;
|
||||
$view->with([
|
||||
'notifications' => $notifications,
|
||||
'amount' => count($notifications),
|
||||
]);
|
||||
$notifications = auth()->user()->unreadNotifications->map(function ($notification) {
|
||||
return [
|
||||
'id' => $notification->id,
|
||||
'title' => $notification->data['title'],
|
||||
];
|
||||
});
|
||||
$view->with(['notifications' => $notifications]);
|
||||
});
|
||||
|
||||
View::composer(
|
||||
|
@ -1,34 +0,0 @@
|
||||
import { get } from './net'
|
||||
import { showModal } from './notify'
|
||||
|
||||
export default async function handler(event: Event) {
|
||||
const item = event.target as HTMLAnchorElement
|
||||
const id = item.getAttribute('data-nid')
|
||||
const {
|
||||
title, content, time,
|
||||
}: {
|
||||
title: string
|
||||
content: string
|
||||
time: string
|
||||
} = await get(`/user/notifications/${id!}`)
|
||||
showModal({
|
||||
mode: 'alert',
|
||||
title,
|
||||
dangerousHTML: `${content}<br><small>${time}</small>`,
|
||||
})
|
||||
item.remove()
|
||||
const counter = document
|
||||
.querySelector('.notifications-counter') as HTMLSpanElement
|
||||
const value = Number.parseInt(counter.textContent!) - 1
|
||||
if (value > 0) {
|
||||
counter.textContent = value.toString()
|
||||
} else {
|
||||
counter.remove()
|
||||
}
|
||||
}
|
||||
|
||||
const el = document.querySelector('.notifications-list')
|
||||
// istanbul ignore next
|
||||
if (el) {
|
||||
el.addEventListener('click', handler)
|
||||
}
|
8
resources/assets/src/scripts/notification.tsx
Normal file
8
resources/assets/src/scripts/notification.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import NotificationsList from '@/views/widgets/NotificationsList'
|
||||
|
||||
const container = document.querySelector('[data-notifications]')
|
||||
if (container) {
|
||||
ReactDOM.render(<NotificationsList />, container)
|
||||
}
|
84
resources/assets/src/views/widgets/NotificationsList.tsx
Normal file
84
resources/assets/src/views/widgets/NotificationsList.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import * as fetch from '@/scripts/net'
|
||||
import { showModal } from '@/scripts/notify'
|
||||
|
||||
export type Notification = {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
|
||||
const NotificationsList: React.FC = () => {
|
||||
const [notifications, setNotifications] = useState<Notification[]>([])
|
||||
const [noUnreadText, setNoUnreadText] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const dataset = document.querySelector<HTMLLIElement>(
|
||||
'[data-notifications]',
|
||||
)?.dataset
|
||||
if (dataset) {
|
||||
const notifications: Notification[] = JSON.parse(dataset.notifications!)
|
||||
setNotifications(notifications)
|
||||
setNoUnreadText(dataset.t!)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const read = async (id: string) => {
|
||||
const { title, content, time } = await fetch.get<{
|
||||
title: string
|
||||
content: string
|
||||
time: string
|
||||
}>(`/user/notifications/${id}`)
|
||||
|
||||
showModal({
|
||||
mode: 'alert',
|
||||
title,
|
||||
children: (
|
||||
<>
|
||||
<div dangerouslySetInnerHTML={{ __html: content }}></div>
|
||||
<br />
|
||||
<small>{time}</small>
|
||||
</>
|
||||
),
|
||||
})
|
||||
setNotifications(notifications =>
|
||||
notifications.filter(notification => notification.id !== id),
|
||||
)
|
||||
}
|
||||
|
||||
const hasUnread = notifications.length > 0
|
||||
|
||||
return (
|
||||
<>
|
||||
<a className="nav-link" data-toggle="dropdown" href="#">
|
||||
<i className="far fa-bell"></i>
|
||||
{hasUnread && (
|
||||
<span className="badge badge-warning navbar-badge">
|
||||
{notifications.length}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
<div className="dropdown-menu dropdown-menu-lg dropdown-menu-right">
|
||||
{hasUnread ? (
|
||||
notifications.map(notification => (
|
||||
<>
|
||||
<a
|
||||
href="#"
|
||||
className="dropdown-item"
|
||||
key={notification.id}
|
||||
onClick={() => read(notification.id)}
|
||||
>
|
||||
<i className="far fa-circle text-info mr-2"></i>
|
||||
{notification.title}
|
||||
</a>
|
||||
<div className="dropdown-divider"></div>
|
||||
</>
|
||||
))
|
||||
) : (
|
||||
<p className="text-center text-muted pt-2 pb-2">{noUnreadText}</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotificationsList
|
@ -0,0 +1,66 @@
|
||||
import React from 'react'
|
||||
import { render, fireEvent, wait } from '@testing-library/react'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import * as fetch from '@/scripts/net'
|
||||
import NotificationsList, {
|
||||
Notification,
|
||||
} from '@/views/widgets/NotificationsList'
|
||||
|
||||
jest.mock('@/scripts/net')
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
function createContainer(notifications: Notification[]) {
|
||||
const container = document.createElement('div')
|
||||
container.dataset.notifications = JSON.stringify(notifications)
|
||||
container.dataset.t = 'no unread'
|
||||
document.body.appendChild(container)
|
||||
}
|
||||
|
||||
test('should not throw if element does not exist', () => {
|
||||
render(<NotificationsList />)
|
||||
})
|
||||
|
||||
test('no unread notifications', () => {
|
||||
createContainer([])
|
||||
|
||||
const { queryByText } = render(<NotificationsList />)
|
||||
|
||||
expect(queryByText('no unread')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('with unread notifications', () => {
|
||||
createContainer([{ id: '1', title: 'hi' }])
|
||||
|
||||
const { queryByText } = render(<NotificationsList />)
|
||||
|
||||
expect(queryByText('1')).toBeInTheDocument()
|
||||
expect(queryByText('hi')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('read notification', async () => {
|
||||
const time = new Date().toLocaleTimeString()
|
||||
const fixture = {
|
||||
title: 'hi - title',
|
||||
content: 'content here',
|
||||
time,
|
||||
}
|
||||
|
||||
createContainer([{ id: '1', title: 'hi' }])
|
||||
fetch.get.mockResolvedValue(fixture)
|
||||
|
||||
const { getByText, queryByText } = render(<NotificationsList />)
|
||||
|
||||
fireEvent.click(getByText('hi'))
|
||||
await wait()
|
||||
|
||||
expect(queryByText(fixture.title)).toBeInTheDocument()
|
||||
expect(queryByText(fixture.content)).toBeInTheDocument()
|
||||
expect(queryByText(fixture.time)).toBeInTheDocument()
|
||||
expect(queryByText('no unread')).toBeInTheDocument()
|
||||
expect(queryByText('1')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
})
|
@ -1,24 +1,6 @@
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||
<i class="far fa-bell"></i>
|
||||
{% if amount > 0 %}
|
||||
<span class="badge badge-warning navbar-badge notifications-counter">
|
||||
{{ amount }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
|
||||
{% if amount == 0 %}
|
||||
<p class="text-center text-muted pt-2 pb-2">{{ trans('user.no-unread') }}</p>
|
||||
{% else %}
|
||||
<div class="notifications-list">
|
||||
{% for notification in notifications %}
|
||||
<a href="#" class="dropdown-item" data-nid="{{ notification.id }}">
|
||||
<i class="far fa-circle text-info"></i> {{ notification.data.title }}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<li
|
||||
class="nav-item dropdown"
|
||||
data-t="{{ trans('user.no-unread') }}"
|
||||
data-notifications="{{ notifications|json_encode }}"
|
||||
>
|
||||
</li>
|
||||
|
Loading…
Reference in New Issue
Block a user