diff --git a/app/Providers/ViewServiceProvider.php b/app/Providers/ViewServiceProvider.php index 46e794c2..7eb2c09e 100644 --- a/app/Providers/ViewServiceProvider.php +++ b/app/Providers/ViewServiceProvider.php @@ -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( diff --git a/resources/assets/src/scripts/notification.ts b/resources/assets/src/scripts/notification.ts deleted file mode 100644 index 38599406..00000000 --- a/resources/assets/src/scripts/notification.ts +++ /dev/null @@ -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}
${time}`, - }) - 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) -} diff --git a/resources/assets/src/scripts/notification.tsx b/resources/assets/src/scripts/notification.tsx new file mode 100644 index 00000000..e807c4e1 --- /dev/null +++ b/resources/assets/src/scripts/notification.tsx @@ -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(, container) +} diff --git a/resources/assets/src/views/widgets/NotificationsList.tsx b/resources/assets/src/views/widgets/NotificationsList.tsx new file mode 100644 index 00000000..96564343 --- /dev/null +++ b/resources/assets/src/views/widgets/NotificationsList.tsx @@ -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([]) + const [noUnreadText, setNoUnreadText] = useState('') + + useEffect(() => { + const dataset = document.querySelector( + '[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: ( + <> +
+
+ {time} + + ), + }) + setNotifications(notifications => + notifications.filter(notification => notification.id !== id), + ) + } + + const hasUnread = notifications.length > 0 + + return ( + <> + + + {hasUnread && ( + + {notifications.length} + + )} + +
+ {hasUnread ? ( + notifications.map(notification => ( + <> + read(notification.id)} + > + + {notification.title} + +
+ + )) + ) : ( +

{noUnreadText}

+ )} +
+ + ) +} + +export default NotificationsList diff --git a/resources/assets/tests/views/widgets/NotificationsList.test.tsx b/resources/assets/tests/views/widgets/NotificationsList.test.tsx new file mode 100644 index 00000000..62c50afd --- /dev/null +++ b/resources/assets/tests/views/widgets/NotificationsList.test.tsx @@ -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() +}) + +test('no unread notifications', () => { + createContainer([]) + + const { queryByText } = render() + + expect(queryByText('no unread')).toBeInTheDocument() +}) + +test('with unread notifications', () => { + createContainer([{ id: '1', title: 'hi' }]) + + const { queryByText } = render() + + 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() + + 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'))) +}) diff --git a/resources/views/shared/notifications.twig b/resources/views/shared/notifications.twig index 0442d031..bd7c561d 100644 --- a/resources/views/shared/notifications.twig +++ b/resources/views/shared/notifications.twig @@ -1,24 +1,6 @@ -