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.head', Composers\HeadComposer::class);
|
||||||
|
|
||||||
View::composer('shared.notifications', function ($view) {
|
View::composer('shared.notifications', function ($view) {
|
||||||
$notifications = auth()->user()->unreadNotifications;
|
$notifications = auth()->user()->unreadNotifications->map(function ($notification) {
|
||||||
$view->with([
|
return [
|
||||||
'notifications' => $notifications,
|
'id' => $notification->id,
|
||||||
'amount' => count($notifications),
|
'title' => $notification->data['title'],
|
||||||
]);
|
];
|
||||||
|
});
|
||||||
|
$view->with(['notifications' => $notifications]);
|
||||||
});
|
});
|
||||||
|
|
||||||
View::composer(
|
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">
|
<li
|
||||||
<a class="nav-link" data-toggle="dropdown" href="#">
|
class="nav-item dropdown"
|
||||||
<i class="far fa-bell"></i>
|
data-t="{{ trans('user.no-unread') }}"
|
||||||
{% if amount > 0 %}
|
data-notifications="{{ notifications|json_encode }}"
|
||||||
<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>
|
</li>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user