build notifications list with React

This commit is contained in:
Pig Fang 2020-02-08 18:22:52 +08:00
parent a6dc3bd068
commit 8e104c2422
6 changed files with 170 additions and 62 deletions

View File

@ -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(

View File

@ -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)
}

View 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)
}

View 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

View File

@ -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')))
})

View File

@ -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>