From e31335729ce7e36e6c4227d85b9670bfcf711937 Mon Sep 17 00:00:00 2001 From: Dan Yishai Date: Thu, 13 Apr 2023 19:04:16 +0300 Subject: [PATCH] Added notification store and badge --- .../src/components/layout/header/Header.tsx | 2 +- .../layout/header/NotificationBadge.tsx | 13 --- .../notifications/NotificationBadge.tsx | 93 +++++++++++++++++++ .../notifications/NotificationItem.tsx | 26 ++++++ frontend/src/stores/useNotifications.ts | 83 +++++++++++++++++ 5 files changed, 203 insertions(+), 14 deletions(-) delete mode 100644 frontend/src/components/layout/header/NotificationBadge.tsx create mode 100644 frontend/src/components/notifications/NotificationBadge.tsx create mode 100644 frontend/src/components/notifications/NotificationItem.tsx create mode 100644 frontend/src/stores/useNotifications.ts diff --git a/frontend/src/components/layout/header/Header.tsx b/frontend/src/components/layout/header/Header.tsx index 610d3fa..0d83baa 100644 --- a/frontend/src/components/layout/header/Header.tsx +++ b/frontend/src/components/layout/header/Header.tsx @@ -1,7 +1,7 @@ -import NotificationBadge from "@components/layout/header/NotificationBadge" import SearchBox from "@components/layout/header/SearchBox" import WSStatus from "@components/layout/header/WSStatus" import { DRAWER_WIDTH } from "@components/layout/menu/Menu" +import NotificationBadge from "@components/notifications/NotificationBadge" import AppBar from "@mui/material/AppBar" import Box from "@mui/material/Box" import Slide from "@mui/material/Slide" diff --git a/frontend/src/components/layout/header/NotificationBadge.tsx b/frontend/src/components/layout/header/NotificationBadge.tsx deleted file mode 100644 index 6eb06e0..0000000 --- a/frontend/src/components/layout/header/NotificationBadge.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import NotificationsIcon from "@mui/icons-material/Notifications" -import Badge from "@mui/material/Badge" -import IconButton from "@mui/material/IconButton" -import React from "react" - -const NotificationBadge: React.FC = () => ( - - - - - -) -export default NotificationBadge diff --git a/frontend/src/components/notifications/NotificationBadge.tsx b/frontend/src/components/notifications/NotificationBadge.tsx new file mode 100644 index 0000000..0bfda10 --- /dev/null +++ b/frontend/src/components/notifications/NotificationBadge.tsx @@ -0,0 +1,93 @@ +import NotificationItem from "@components/notifications/NotificationItem" +import ClearAllIcon from "@mui/icons-material/ClearAll" +import NotificationsIcon from "@mui/icons-material/Notifications" +import NotificationsOffIcon from "@mui/icons-material/NotificationsOff" +import Badge from "@mui/material/Badge" +import IconButton from "@mui/material/IconButton" +import List from "@mui/material/List" +import ListItem from "@mui/material/ListItem" +import ListItemSecondaryAction from "@mui/material/ListItemSecondaryAction" +import ListItemText from "@mui/material/ListItemText" +import Popover from "@mui/material/Popover" +import Switch from "@mui/material/Switch" +import Tooltip from "@mui/material/Tooltip" +import { + addNotification, + clearNotifications, + toggleNotifications, + useNotificationStore, +} from "@stores/useNotifications" +import React, { useEffect, useMemo } from "react" + +const NotificationBadge: React.FC = () => { + const [anchorEl, setAnchorEl] = React.useState(null) + + const notifications = useNotificationStore((state) => state.notifications) + const unseenNotifications = useMemo( + () => notifications.filter((notification) => !notification.seen), + [notifications] + ) + const enabled = useNotificationStore((state) => state.enabled) + const id = anchorEl ? "notification-popover" : undefined + + useEffect(() => { + // Create fake notifications when dropdown is open + if (anchorEl) { + const token = setInterval( + () => + addNotification({ + type: "info", + message: "Hello world!", + }), + 5 * 1000 + ) + return () => clearInterval(token) + } + }, [anchorEl]) + + return ( + <> + setAnchorEl(event.currentTarget)}> + + {enabled ? : } + + + setAnchorEl(null)} + > + + + + + await toggleNotifications()} /> + + + clearNotifications()}> + + + + + + } + > + {notifications.map((notification) => ( + + ))} + {notifications.length === 0 && ( + + + + )} + + + + ) +} +export default NotificationBadge diff --git a/frontend/src/components/notifications/NotificationItem.tsx b/frontend/src/components/notifications/NotificationItem.tsx new file mode 100644 index 0000000..75a6a23 --- /dev/null +++ b/frontend/src/components/notifications/NotificationItem.tsx @@ -0,0 +1,26 @@ +import ListItem from "@mui/material/ListItem" +import ListItemText from "@mui/material/ListItemText" +import { Notification, setNotificationSeen } from "@stores/useNotifications" +import React, { useEffect } from "react" + +interface NotificationItemProps { + notification: Notification +} + +const NotificationItem: React.FC = ({ notification }) => { + useEffect(() => { + // Automatically mark notification as seen after 5 seconds + const token = setTimeout(() => { + if (!notification.seen) { + setNotificationSeen(notification.id) + } + }, 1000) + return () => clearTimeout(token) + }, [notification.id, notification.seen]) + return ( + + + + ) +} +export default NotificationItem diff --git a/frontend/src/stores/useNotifications.ts b/frontend/src/stores/useNotifications.ts new file mode 100644 index 0000000..a144f0a --- /dev/null +++ b/frontend/src/stores/useNotifications.ts @@ -0,0 +1,83 @@ +import create from "zustand" + +type NotificationType = "error" | "info" | "success" + +export interface Notification { + id: number + timestamp: Date + seen: boolean + icon?: string + link?: string + message: string + type: NotificationType +} + +interface NotificationStore { + notifications: Notification[] + enabled: boolean + limit: number +} + +export const useNotificationStore = create(() => ({ + notifications: [], + enabled: Notification.permission === "granted", + limit: 30, +})) + +export const addNotification = (notification: Omit) => { + const enabled = useNotificationStore.getState().enabled + + if (enabled && Notification.permission === "granted") { + const notificationOptions = { + body: notification.message, + icon: notification.icon, + data: { + url: notification.link, + }, + } + + new Notification(notification.type, notificationOptions) + useNotificationStore.setState((state) => ({ + notifications: [ + { + id: state.notifications.length + 1, + timestamp: new Date(), + seen: false, + ...notification, + }, + ...state.notifications.slice(0, state.limit - 1), + ], + })) + } +} + +export const toggleNotifications = async () => { + const enabled = useNotificationStore.getState().enabled + + if (enabled) { + useNotificationStore.setState({ enabled: false }) + } else { + if (Notification.permission === "granted") { + useNotificationStore.setState({ enabled: true }) + } else if (Notification.permission !== "denied") { + const permission = await Notification.requestPermission() + if (permission === "granted") { + useNotificationStore.setState({ enabled: true }) + } + } + } +} + +export const clearNotifications = () => useNotificationStore.setState({ notifications: [] }) + +export const setNotificationSeen = (notificationId: number) => + useNotificationStore.setState((state) => { + const notifications = state.notifications.map((notification) => { + if (notification.id === notificationId) { + return { ...notification, seen: true } + } else { + return notification + } + }) + return { notifications, enabled: state.enabled } + })