From f8fc7944585038289899573890786f9f065d5260 Mon Sep 17 00:00:00 2001 From: SundasNoreen Date: Fri, 19 May 2023 16:17:50 +0500 Subject: [PATCH] feat: added notification tray --- example/index.js | 4 +- package-lock.json | 53 ++++- package.json | 5 +- src/DesktopHeader.jsx | 10 +- src/Header.jsx | 25 --- src/Notifications/NotificationIcon.jsx | 90 -------- src/Notifications/NotificationRow.jsx | 29 --- src/Notifications/NotificationRowItem.jsx | 128 +++++++---- src/Notifications/NotificationSections.jsx | 41 ++++ src/Notifications/NotificationTabs.jsx | 53 +++++ src/Notifications/Notifications.jsx | 77 +++++++ src/Notifications/data/api.js | 212 +++++++++++++++--- src/Notifications/data/index.js | 1 + src/Notifications/data/selectors.js | 3 + src/Notifications/data/slice.js | 62 +++++ src/Notifications/data/thunks.js | 36 +++ src/Notifications/icons/CheckCircleFilled.jsx | 19 ++ src/Notifications/icons/EditOutline.jsx | 19 ++ .../icons/QuestionAnswerOutline.jsx | 19 ++ src/Notifications/icons/Report.jsx | 19 ++ src/Notifications/icons/ThumbsUpOutline.jsx | 22 ++ src/Notifications/icons/Verified.jsx | 19 ++ src/Notifications/icons/index.js | 6 + src/Notifications/messages.js | 79 ++++++- src/constants.js | 37 +++ src/index.scss | 190 +++++++++------- src/store.js | 16 ++ src/time-locale.js | 19 ++ 28 files changed, 981 insertions(+), 312 deletions(-) delete mode 100644 src/Notifications/NotificationIcon.jsx delete mode 100644 src/Notifications/NotificationRow.jsx create mode 100644 src/Notifications/NotificationSections.jsx create mode 100644 src/Notifications/NotificationTabs.jsx create mode 100644 src/Notifications/Notifications.jsx create mode 100644 src/Notifications/data/index.js create mode 100644 src/Notifications/data/selectors.js create mode 100644 src/Notifications/data/slice.js create mode 100644 src/Notifications/data/thunks.js create mode 100644 src/Notifications/icons/CheckCircleFilled.jsx create mode 100644 src/Notifications/icons/EditOutline.jsx create mode 100644 src/Notifications/icons/QuestionAnswerOutline.jsx create mode 100644 src/Notifications/icons/Report.jsx create mode 100644 src/Notifications/icons/ThumbsUpOutline.jsx create mode 100644 src/Notifications/icons/Verified.jsx create mode 100644 src/constants.js create mode 100644 src/store.js create mode 100644 src/time-locale.js diff --git a/example/index.js b/example/index.js index e9b44a5..5a93ed0 100644 --- a/example/index.js +++ b/example/index.js @@ -5,12 +5,14 @@ import ReactDOM from 'react-dom'; import { initialize, getConfig, subscribe, APP_READY } from '@edx/frontend-platform'; import { AppContext, AppProvider } from '@edx/frontend-platform/react'; import Header from '@edx/frontend-component-header'; +import store from '../src/store'; + import './index.scss'; subscribe(APP_READY, () => { ReactDOM.render( - + {/* We can fake out authentication by including another provider here with the data we want */} - {loggedIn && } + {loggedIn && } {loggedIn ? this.renderUserMenu() : this.renderLoggedOutItems()} @@ -182,10 +181,6 @@ DesktopHeader.propTypes = { avatar: PropTypes.string, username: PropTypes.string, loggedIn: PropTypes.bool, - notificationCounts: PropTypes.arrayOf(PropTypes.shape({ - type: PropTypes.string, - count: PropTypes.string, - })), // i18n intl: intlShape.isRequired, @@ -216,7 +211,6 @@ DesktopHeader.defaultProps = { username: null, loggedIn: false, appMenu: null, - notificationCounts: [], }; export default injectIntl(DesktopHeader); diff --git a/src/Header.jsx b/src/Header.jsx index da435a0..97c094e 100644 --- a/src/Header.jsx +++ b/src/Header.jsx @@ -9,7 +9,6 @@ import { getConfig, subscribe, } from '@edx/frontend-platform'; - import DesktopHeader from './DesktopHeader'; import MobileHeader from './MobileHeader'; @@ -88,29 +87,6 @@ const Header = ({ intl }) => { }, ]; - const notificationCounts = [ - { - type: 'total', - count: 25, - }, - { - type: 'reminders', - count: 1, - }, - { - type: 'discussions', - count: 0, - }, - { - type: 'grades', - count: 0, - }, - { - type: 'authoring', - count: 24, - }, - ]; - const props = { logo: config.LOGO_URL, logoAltText: config.SITE_NAME, @@ -121,7 +97,6 @@ const Header = ({ intl }) => { mainMenu: getConfig().AUTHN_MINIMAL_HEADER ? [] : mainMenu, userMenu: getConfig().AUTHN_MINIMAL_HEADER ? [] : userMenu, loggedOutItems: getConfig().AUTHN_MINIMAL_HEADER ? [] : loggedOutItems, - notificationCounts, }; return ( diff --git a/src/Notifications/NotificationIcon.jsx b/src/Notifications/NotificationIcon.jsx deleted file mode 100644 index 85ab5cf..0000000 --- a/src/Notifications/NotificationIcon.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useState, useCallback } from 'react'; -import PropTypes from 'prop-types'; -import { NotificationsNone, Settings } from '@edx/paragon/icons'; -import { - Tabs, Tab, Badge, Form, Icon, IconButton, OverlayTrigger, Popover, -} from '@edx/paragon'; -import NotificationRow from './NotificationRow'; - -const NotificationIcon = ({ notificationCounts }) => { - const [showNotificationTray, setShowNotificationTray] = useState(false); - - const handleNotificationTray = useCallback((value) => { - setShowNotificationTray(value); - }, []); - - return ( -
- - -

Notifications

-
- -
-
- - - - Hello I am the first panel. - - - - - - Hello I am the third panel. - - - Hello I am the fourth panel. - - - Hello I am the fifth panel. - - - Hello I am the sixth panel. - - - - - )} - > - <> - - {notificationCounts[0]?.count} - -
- { handleNotificationTray(!showNotificationTray); }} - onBlur={() => { handleNotificationTray(false); }} - src={NotificationsNone} - iconAs={Icon} - className="d-inline-block align-bottom ml-1 bell-icon" - /> -
- -
-
- ); -}; - -NotificationIcon.propTypes = { - notificationCounts: PropTypes.arrayOf(PropTypes.shape({ - type: PropTypes.string, - count: PropTypes.string, - })), -}; - -NotificationIcon.defaultProps = { - notificationCounts: [], -}; -export default NotificationIcon; diff --git a/src/Notifications/NotificationRow.jsx b/src/Notifications/NotificationRow.jsx deleted file mode 100644 index 9618ab8..0000000 --- a/src/Notifications/NotificationRow.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { useIntl } from '@edx/frontend-platform/i18n'; -import { messages } from './messages'; -import NotificationRowItem from './NotificationRowItem'; - -const NotificationRow = () => { - const intl = useIntl(); - - return ( -
-
- - {intl.formatMessage(messages.notificationTodayHeading)} - - - {intl.formatMessage(messages.notificationMarkAsRead)} - -
-
- -
-
- ); -}; - -NotificationRow.propTypes = { -}; - -export default React.memo(NotificationRow); diff --git a/src/Notifications/NotificationRowItem.jsx b/src/Notifications/NotificationRowItem.jsx index bd6688d..b8c97d3 100644 --- a/src/Notifications/NotificationRowItem.jsx +++ b/src/Notifications/NotificationRowItem.jsx @@ -1,69 +1,107 @@ -import React from 'react'; +/* eslint-disable react/forbid-prop-types */ +import React, { useCallback, useContext } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Icon } from '@edx/paragon'; -// import * as timeago from 'timeago.js'; +import * as timeago from 'timeago.js'; +import PropTypes from 'prop-types'; +import { AppContext } from '@edx/frontend-platform/react'; import { messages } from './messages'; -import { PostOutline } from './icons'; +import { + PostOutline, HelpOutline, QuestionAnswerOutline, CheckCircleFilled, Verified, Report, ThumbsUpOutline, EditOutline, +} from './icons'; +import timeLocale from '../time-locale'; -const NotificationRowItem = () => { +const NotificationRowItem = ({ notification }) => { const intl = useIntl(); + timeago.register('time-locale', timeLocale); + const { authenticatedUser } = useContext(AppContext); + + const handleIconByType = (type) => { + switch (type) { + case 'post': return PostOutline; + case 'help': return HelpOutline; + case 'respond': return QuestionAnswerOutline; + case 'comment': return QuestionAnswerOutline; + case 'question': return QuestionAnswerOutline; + case 'answer': return CheckCircleFilled; + case 'endorsed': return Verified; + case 'reported': return Report; + case 'postLiked': return ThumbsUpOutline; + case 'commentLiked': return ThumbsUpOutline; + case 'edited': return EditOutline; + default: return null; + } + }; + + const getContentMessageByType = useCallback(() => { + const typeMap = { + post: messages.notificationPostedContent, + help: messages.notificationHelpedContent, + respond: authenticatedUser.username !== notification.author + ? messages.notificationResponseOnOtherPostLabel : null, + comment: notification.targetUser + ? messages.notificationCommentedOnLabel : messages.notificationCommentedOnYourPostLabel, + question: messages.notificationQuestionLabel, + answer: messages.notificationAnswerLabel, + endorsed: messages.notificationEndorsedLabel, + reported: messages.notificationReportedLabel, + postLiked: messages.notificationPostLikedLabel, + commentLiked: messages.notificationCommentLikedLabel, + edited: messages.notificationEditedLabel, + }; + return typeMap[notification.type] ? intl.formatMessage(typeMap[notification.type]) : null; + }, [authenticatedUser, notification, intl]); return ( -
-
+
+
-
-
- - SCM_Lead posted - - Hello and welcome to SC0x! +
+
+ + {notification?.respondingUser} {' '} + {getContentMessageByType()} + {notification?.targetUser && ( + <> + {notification?.targetUser} + + {authenticatedUser.username !== notification.author + ? intl.formatMessage(messages.notificationResponseOnOtherPostLabel) + : intl.formatMessage(messages.notificationResponseOnYourPostLabel)} + + + )} + + {' '}{notification?.notificationContent} -
-
+
+ {notification.status === 'unread' + &&
} +
+
+ + {notification?.courseName} + + {intl.formatMessage(messages.fullStop)} + + + {timeago.format(notification?.time, 'time-locale')} + +
- {/*
- - Supply Chain Analytics - - {intl.formatMessage(messages.fullStop)} - - - {timeago.format(postCreatedAt, 'time-locale')} - - - - -
*/}
); }; NotificationRowItem.propTypes = { + notification: PropTypes.object.isRequired, }; export default React.memo(NotificationRowItem); diff --git a/src/Notifications/NotificationSections.jsx b/src/Notifications/NotificationSections.jsx new file mode 100644 index 0000000..18b79ff --- /dev/null +++ b/src/Notifications/NotificationSections.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { useSelector } from 'react-redux'; +import { messages } from './messages'; +import NotificationRowItem from './NotificationRowItem'; +import { getNotifications } from './data/selectors'; + +const NotificationSections = () => { + const intl = useIntl(); + const notifications = useSelector(getNotifications()); + + return ( + notifications && ( +
+
+ + {notifications && notifications.TODAY && intl.formatMessage(messages.notificationTodayHeading)} + + + {intl.formatMessage(messages.notificationMarkAsRead)} + +
+
+ {notifications && notifications.TODAY && notifications?.TODAY.map( + (notification) => , + )} +
+ + {notifications && notifications.EARLIER && intl.formatMessage(messages.notificationEarlierHeading)} + +
+ {notifications && notifications.EARLIER && notifications?.EARLIER.map( + (notification) => , + )} +
+
+ ) + ); +}; + +export default React.memo(NotificationSections); diff --git a/src/Notifications/NotificationTabs.jsx b/src/Notifications/NotificationTabs.jsx new file mode 100644 index 0000000..0669c70 --- /dev/null +++ b/src/Notifications/NotificationTabs.jsx @@ -0,0 +1,53 @@ +import React, { + useState, useCallback, useMemo, useEffect, +} from 'react'; +import { Tabs, Tab } from '@edx/paragon'; +import { useSelector, useDispatch } from 'react-redux'; +import NotificationSections from './NotificationSections'; +import { getNotificationTotalUnseenCounts } from './data/selectors'; +import { fetchNotificationList } from './data/thunks'; +import { notificationTabsOptions } from '../constants'; + +const NotificationTabs = () => { + const notificationUnseenCounts = useSelector(getNotificationTotalUnseenCounts()); + const [activeTab, setActiveTab] = useState(notificationTabsOptions[0].key); + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchNotificationList({ notificationType: activeTab || 'reminders' })); + }, [dispatch, activeTab]); + + useEffect(() => { + setActiveTab(activeTab || 'reminders'); + }, [activeTab]); + + const handleActiveTab = useCallback((tab) => { + setActiveTab(tab); + }, []); + + const tabArray = useMemo(() => notificationTabsOptions.map((option) => ( + + + + )), [notificationUnseenCounts]); + + return ( + activeTab && ( + + {tabArray} + + ) + ); +}; + +export default NotificationTabs; diff --git a/src/Notifications/Notifications.jsx b/src/Notifications/Notifications.jsx new file mode 100644 index 0000000..f246265 --- /dev/null +++ b/src/Notifications/Notifications.jsx @@ -0,0 +1,77 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import { NotificationsNone, Settings } from '@edx/paragon/icons'; +import { + Badge, Form, Icon, IconButton, OverlayTrigger, Popover, +} from '@edx/paragon'; +import { useSelector, useDispatch } from 'react-redux'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import NotificationTabs from './NotificationTabs'; +import { getNotificationTotalUnseenCounts, getNotificationStatus } from './data/selectors'; +import { fetchNotificationsCountsList } from './data/thunks'; +import { messages } from './messages'; + +const Notifications = () => { + const [showNotificationTray, setShowNotificationTray] = useState(false); + const notificationCounts = useSelector(getNotificationTotalUnseenCounts()); + const intl = useIntl(); + + const dispatch = useDispatch(); + const notificationStatus = useSelector(getNotificationStatus()); + + useEffect(() => { + if (notificationStatus === 'idle') { + dispatch(fetchNotificationsCountsList()); + } + }, [dispatch, notificationStatus]); + + const handleNotificationTray = useCallback((value) => { + setShowNotificationTray(value); + }, []); + + return ( +
+ + +

+ {intl.formatMessage(messages.notificationTitle)} +

+
+ +
+
+ + + + + )} + > + <> + {notificationCounts?.Total > 0 && ( + + {notificationCounts?.Total} + + )} +
+ { handleNotificationTray(!showNotificationTray); }} + src={NotificationsNone} + iconAs={Icon} + className="d-inline-block align-bottom ml-1 bell-icon" + /> +
+ +
+
+ ); +}; + +export default Notifications; diff --git a/src/Notifications/data/api.js b/src/Notifications/data/api.js index 18398ba..67a5198 100644 --- a/src/Notifications/data/api.js +++ b/src/Notifications/data/api.js @@ -4,35 +4,197 @@ import { getConfig } from '@edx/frontend-platform'; export const getApiBaseUrl = () => getConfig().LMS_BASE_URL; -export async function getCourseTopics() { +export async function getNotifications(notificationType) { // const url = `${getApiBaseUrl()}/api/discussion/v1/notifications/`; // const { data } = await getAuthenticatedHttpClient() // .get(url); - const data = [{ - TODAY: [ - { - type: 'post', - respondingUser: 'SCM_Lead', - notificationContent: 'Hello and welcome to SC0x!', - targetUser: '', - courseName: 'Supply Chain Analytics', - URL: '', - status: 'unread', - time: '15m', - }, - { - type: 'help', - respondingUser: 'MITx_Learner', - notificationContent: 'What grade does a student need to get in order to pass the course and earn a certificate?', - targetUser: '', - courseName: 'Supply Chain Analytics', - URL: '', - status: 'unread', - time: '15m', - }, - ], - }]; + const data = { + discussions: { + TODAY: [ + { + type: 'post', + respondingUser: 'SCM_Lead', + notificationContent: 'Hello and welcome to SC0x!', + targetUser: '', + courseName: 'Supply Chain Analytics', + URL: '', + status: 'unread', + time: '15m', + }, + { + type: 'help', + respondingUser: 'MITx_Learner', + notificationContent: 'What grade does a student need to get in order to pass the course and earn a certificate?', + targetUser: '', + courseName: 'Supply Chain Analytics', + URL: '', + status: 'unread', + time: '15m', + }, + ], + }, + reminders: { + TODAY: [ + { + type: 'post', + respondingUser: 'SCM_Lead', + notificationContent: 'Hello and welcome to SC0x!', + targetUser: '', + courseName: 'Supply Chain Analytics', + URL: '', + status: 'unread', + time: '1684253634808', + author: '', + }, + { + type: 'help', + respondingUser: 'MITx_Learner', + notificationContent: 'What grade does a student need to get in order to pass the course and earn a certificate?', + targetUser: '', + courseName: 'Supply Chain Analytics', + URL: '', + status: 'unread', + time: '1684253736371', + author: '', + }, + { + type: 'respond', + respondingUser: 'MITx_Learner', + notificationContent: 'Can’t find linear regression in section 3 review', + targetUser: '', + courseName: 'Supply Chain Analytics', + URL: '', + status: 'unread', + time: '1684253736371', + author: '', + }, + { + type: 'comment', + respondingUser: 'MITx_Learner', + notificationContent: 'Can’t find linear regression in section 3 review', + targetUser: 'MITx_Expert’s ', + courseName: 'Supply Chain Analytics', + URL: '', + status: 'unread', + time: '1684253736371', + author: '', + }, + { + type: 'question', + respondingUser: 'MITx_Learner', + notificationContent: 'Examples of quadratic equations in supply chains', + targetUser: '', + courseName: 'Supply Chain Analytics', + URL: '', + status: 'unread', + time: '1684253736371', + author: '', + }, + { + type: 'comment', + respondingUser: 'MITx_Learner', + notificationContent: 'What grade does a student need to get in order to pass the course and earn a certificate?', + targetUser: 'MITx_Expert’s ', + courseName: 'Supply Chain Analytics', + URL: '', + status: 'unread', + time: '1684253736371', + author: 'testuser', + }, + { + type: 'comment', + respondingUser: 'MITx_Learner', + notificationContent: 'Convexity of f(x)=1/x , x>1', + targetUser: '', + courseName: 'Supply Chain Analytics', + URL: '', + status: 'unread', + time: '1684253736371', + author: 'testuser', + }, + ], + EARLIER: [ + { + type: 'answer', + respondingUser: 'SCM_Lead', + notificationContent: 'Quiz in section 3 - Please explain the F-Significance value', + targetUser: '', + courseName: 'Supply Chain Analytics', + URL: '', + status: 'unread', + time: '15m', + author: 'testuser', + }, + { + type: 'endorsed', + respondingUser: '', + notificationContent: 'Quiz in section 3 - Please explain the F-Significance value', + targetUser: '', + courseName: 'Supply Chain Analytics', + URL: '', + status: 'unread', + time: '15m', + author: 'testuser', + }, + { + type: 'reported', + respondingUser: 'MITx Learner’s', + notificationContent: '“Here are the exam answers. Question 1 - CSA stands for Compliance Safety Ac...”', + targetUser: '', + courseName: 'Supply Chain Analytics', + URL: '', + status: 'unread', + time: '15m', + author: '', + }, + { + type: 'postLiked', + respondingUser: 'SCM_Lead', + notificationContent: 'Retaking the course', + targetUser: '', + courseName: 'Supply Chain Analytics', + URL: '', + status: 'unread', + time: '15m', + author: '', + }, + { + type: 'commentLiked', + respondingUser: 'MITx_Expert ', + notificationContent: 'Final exam answers', + targetUser: '', + courseName: 'Supply Chain Analytics', + URL: '', + status: 'unread', + time: '15m', + author: '', + }, + { + type: 'edited', + respondingUser: 'MITx_Expert ', + notificationContent: 'Question 1', + targetUser: '', + courseName: 'Supply Chain Analytics', + URL: '', + status: 'unread', + time: '15m', + author: '', + }, + ], + }, + }; + return data[notificationType]; +} + +export async function getNotificationCounts() { + const data = { + Total: 25, + Reminders: 10, + Discussions: 5, + Grades: 4, + Authoring: 6, + }; return data; } diff --git a/src/Notifications/data/index.js b/src/Notifications/data/index.js new file mode 100644 index 0000000..4285022 --- /dev/null +++ b/src/Notifications/data/index.js @@ -0,0 +1 @@ +export * from './slice'; diff --git a/src/Notifications/data/selectors.js b/src/Notifications/data/selectors.js new file mode 100644 index 0000000..b0bb9d4 --- /dev/null +++ b/src/Notifications/data/selectors.js @@ -0,0 +1,3 @@ +export const getNotificationStatus = () => state => state.notifications.notificationStatus; +export const getNotificationTotalUnseenCounts = () => state => state.notifications.totalUnseenCounts; +export const getNotifications = () => state => state.notifications.notifications; diff --git a/src/Notifications/data/slice.js b/src/Notifications/data/slice.js new file mode 100644 index 0000000..b302f99 --- /dev/null +++ b/src/Notifications/data/slice.js @@ -0,0 +1,62 @@ +/* eslint-disable no-param-reassign */ +import { createSlice } from '@reduxjs/toolkit'; + +export const LOADING = 'loading'; +export const LOADED = 'loaded'; +export const FAILED = 'failed'; +export const DENIED = 'denied'; + +const slice = createSlice({ + name: 'notifications', + initialState: { + notificationStatus: 'idle', + notifications: {}, + totalUnseenCounts: {}, + notificationType: '', + }, + reducers: { + fetchNotificationDenied: (state, { payload }) => { + state.notificationType = payload.notificationType; + state.notificationStatus = DENIED; + }, + fetchNotificationFailure: (state, { payload }) => { + state.notificationType = payload.notificationType; + state.notificationStatus = FAILED; + }, + fetchNotificationRequest: (state, { payload }) => { + state.notificationType = payload.notificationType; + state.notificationStatus = LOADING; + }, + fetchNotificationSuccess: (state, { payload }) => { + state.notifications = payload; + state.notificationStatus = LOADED; + }, + fetchNotificationsCountDenied: (state) => { + state.notificationStatus = DENIED; + }, + fetchNotificationsCountFailure: (state) => { + state.notificationStatus = FAILED; + }, + fetchNotificationsCountRequest: (state) => { + state.notificationStatus = LOADING; + }, + fetchNotificationsCountSuccess: (state, { payload }) => { + state.tabsCount = payload; + state.notificationStatus = LOADED; + state.totalUnseenCounts = payload; + }, + }, +}); + +export const { + fetchNotificationDenied, + fetchNotificationFailure, + fetchNotificationRequest, + fetchNotificationSuccess, + fetchNotificationsCountDenied, + fetchNotificationsCountFailure, + fetchNotificationsCountRequest, + fetchNotificationsCountSuccess, +} = slice.actions; + +export const notificationsReducer = slice.reducer; diff --git a/src/Notifications/data/thunks.js b/src/Notifications/data/thunks.js new file mode 100644 index 0000000..6b3b5cc --- /dev/null +++ b/src/Notifications/data/thunks.js @@ -0,0 +1,36 @@ +import { + fetchNotificationSuccess, + fetchNotificationRequest, + fetchNotificationFailure, + fetchNotificationsCountFailure, + fetchNotificationsCountRequest, + fetchNotificationsCountSuccess, +} from './slice'; +import { + getNotifications, + getNotificationCounts, +} from './api'; + +export const fetchNotificationList = ({ notificationType }) => ( + async (dispatch) => { + try { + dispatch(fetchNotificationRequest({ notificationType })); + const data = await getNotifications(notificationType); + dispatch(fetchNotificationSuccess(data)); + } catch (errors) { + dispatch(fetchNotificationFailure({ notificationType })); + } + } +); + +export const fetchNotificationsCountsList = () => ( + async (dispatch) => { + try { + dispatch(fetchNotificationsCountRequest()); + const data = await getNotificationCounts(); + dispatch(fetchNotificationsCountSuccess(data)); + } catch (errors) { + dispatch(fetchNotificationsCountFailure()); + } + } +); diff --git a/src/Notifications/icons/CheckCircleFilled.jsx b/src/Notifications/icons/CheckCircleFilled.jsx new file mode 100644 index 0000000..bd51c42 --- /dev/null +++ b/src/Notifications/icons/CheckCircleFilled.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +const CheckCircleFilled = () => ( + + + + +); + +export default CheckCircleFilled; diff --git a/src/Notifications/icons/EditOutline.jsx b/src/Notifications/icons/EditOutline.jsx new file mode 100644 index 0000000..4b158a9 --- /dev/null +++ b/src/Notifications/icons/EditOutline.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +const EditOutline = () => ( + + + + +); + +export default EditOutline; diff --git a/src/Notifications/icons/QuestionAnswerOutline.jsx b/src/Notifications/icons/QuestionAnswerOutline.jsx new file mode 100644 index 0000000..0137d49 --- /dev/null +++ b/src/Notifications/icons/QuestionAnswerOutline.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +const QuestionAnswerOutline = () => ( + + + + +); + +export default QuestionAnswerOutline; diff --git a/src/Notifications/icons/Report.jsx b/src/Notifications/icons/Report.jsx new file mode 100644 index 0000000..87123c6 --- /dev/null +++ b/src/Notifications/icons/Report.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +const Report = () => ( + + + + +); + +export default Report; diff --git a/src/Notifications/icons/ThumbsUpOutline.jsx b/src/Notifications/icons/ThumbsUpOutline.jsx new file mode 100644 index 0000000..a38b685 --- /dev/null +++ b/src/Notifications/icons/ThumbsUpOutline.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const ThumbsUpOutline = () => ( + + + + + +); + +export default ThumbsUpOutline; diff --git a/src/Notifications/icons/Verified.jsx b/src/Notifications/icons/Verified.jsx new file mode 100644 index 0000000..65cda1b --- /dev/null +++ b/src/Notifications/icons/Verified.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +const Verified = () => ( + + + + +); + +export default Verified; diff --git a/src/Notifications/icons/index.js b/src/Notifications/icons/index.js index c27b06d..8cde1f2 100644 --- a/src/Notifications/icons/index.js +++ b/src/Notifications/icons/index.js @@ -1,2 +1,8 @@ export { default as PostOutline } from './PostOutline'; export { default as HelpOutline } from './HelpOutline'; +export { default as QuestionAnswerOutline } from './QuestionAnswerOutline'; +export { default as CheckCircleFilled } from './CheckCircleFilled'; +export { default as Verified } from './Verified'; +export { default as Report } from './Report'; +export { default as ThumbsUpOutline } from './ThumbsUpOutline'; +export { default as EditOutline } from './EditOutline'; diff --git a/src/Notifications/messages.js b/src/Notifications/messages.js index e7a3db8..c288449 100644 --- a/src/Notifications/messages.js +++ b/src/Notifications/messages.js @@ -2,11 +2,21 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; // eslint-disable-next-line import/prefer-default-export export const messages = defineMessages({ + notificationTitle: { + id: 'notification.title', + defaultMessage: 'Notifications', + description: 'Notifications', + }, notificationTodayHeading: { id: 'notification.today.heading', defaultMessage: 'Today', description: 'Today Notifications', }, + notificationEarlierHeading: { + id: 'notification.earlier.heading', + defaultMessage: 'Earlier', + description: 'Earlier Notifications', + }, notificationMarkAsRead: { id: 'notification.mark.as.read', defaultMessage: 'Mark all as read', @@ -14,12 +24,77 @@ export const messages = defineMessages({ }, notificationPostedContent: { id: 'notification.posted.content', - defaultMessage: '{respondingUser} posted {notificationContent}', + defaultMessage: 'posted', description: 'Display notification content for post type', }, notificationHelpedContent: { id: 'notification.helped.content', - defaultMessage: '{respondingUser} asked {notificationContent}', + defaultMessage: 'asked', description: 'Display notification content for help type', }, + notificationRespondedLabel: { + id: 'notification.responded.label', + defaultMessage: 'responded to a post you’re following', + description: 'Display notification content for respond type', + }, + notificationCommentedOnLabel: { + id: 'notification.commented.on.label', + defaultMessage: 'commented on', + description: 'Display notification content for comment type', + }, + notificationResponseOnOtherPostLabel: { + id: 'notification.response.on.other.post.label', + defaultMessage: 'response on a post you’re following:', + description: 'Display notification content for comment type for other posts', + }, + notificationQuestionLabel: { + id: 'notification.question.label', + defaultMessage: 'responded to your question', + description: 'Display notification content for question type', + }, + notificationResponseOnYourPostLabel: { + id: 'notification.response.on.your.post.label', + defaultMessage: 'response to your post', + description: 'Display notification content for comment type for your post', + }, + notificationCommentedOnYourPostLabel: { + id: 'notification.commented.on.your.post.label', + defaultMessage: 'commented on your response in', + description: 'Display notification content for comment type on your response', + }, + notificationAnswerLabel: { + id: 'notification.answer.label', + defaultMessage: 'response has been marked as answer in your post', + description: 'Display notification content for answer type', + }, + notificationEndorsedLabel: { + id: 'notification.endorsed.label', + defaultMessage: 'Your response has been endorsed in', + description: 'Display notification content for endorsed type', + }, + notificationReportedLabel: { + id: 'notification.reported.label', + defaultMessage: 'post has been reported', + description: 'Display notification content for reported type', + }, + notificationPostLikedLabel: { + id: 'notification.post.liked.label', + defaultMessage: 'liked your post', + description: 'Display notification content for post liked type', + }, + notificationCommentLikedLabel: { + id: 'notification.comment.liked.label', + defaultMessage: 'liked your response in', + description: 'Display notification content for response liked type', + }, + notificationEditedLabel: { + id: 'notification.edited.label', + defaultMessage: 'edited your post', + description: 'Display notification content for edited type', + }, + fullStop: { + id: 'notification.fullStop', + defaultMessage: '•', + description: 'Fullstop shown to users to indicate who edited a post.', + }, }); diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..fd66983 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,37 @@ +export const IDLE_STATUS = 'idle'; +export const LOADING_STATUS = 'loading'; +export const SUCCESS_STATUS = 'success'; +export const FAILURE_STATUS = 'failure'; + +export const notificationTabs = { + REMINDERS: 'reminders', + DISCUSSIONS: 'discussions', + GRADES: 'grades', + AUTHORING: 'authoring', +}; + +export const notificationTabsLabel = { + [notificationTabs.REMINDERS]: 'Reminders', + [notificationTabs.DISCUSSIONS]: 'Discussions', + [notificationTabs.GRADES]: 'Grades', + [notificationTabs.AUTHORING]: 'Authoring', +}; + +export const notificationTabsOptions = [ + { + key: notificationTabs.REMINDERS, + title: notificationTabsLabel[notificationTabs.REMINDERS], + }, + { + key: notificationTabs.DISCUSSIONS, + title: notificationTabsLabel[notificationTabs.DISCUSSIONS], + }, + { + key: notificationTabs.GRADES, + title: notificationTabsLabel[notificationTabs.GRADES], + }, + { + key: notificationTabs.AUTHORING, + title: notificationTabsLabel[notificationTabs.AUTHORING], + }, +]; diff --git a/src/index.scss b/src/index.scss index ae08e2c..2979af8 100644 --- a/src/index.scss +++ b/src/index.scss @@ -118,83 +118,7 @@ $white: #fff; border-radius: $rounded-pill; } } - -.popover .arrow{ - display: none !important; - } -.notification-title{ - line-height: 24px; - font-weight: 700; - font-size: 18px; -} -.setting-icon-container{ - display: flex; - flex-direction: row; - justify-content: flex-end; - align-items: flex-end; - width: 100%; - position: absolute; - margin-left: -40px; - margin-top: -7px; - span{ - height:20px; - width: 20px; - } -} - -.notification-content{ - padding-left: 0px; - width: 549px; - } -.notification-tabs{ - height: 38px; - width:549px; - padding-left: 12px; - button{ - font-size: 14px; - } - .dropdown-toggle{ - height: 36px; - padding-top: 0px !important; - padding-left: 12px !important; - padding-right: 26px !important; - - div{ - margin-top: 4px; - height: 20px; - width: 20px; - } - } - .dropdown{ - height: 36px; - } - .notification-tab, .dropdown-item{ - display: flex; - flex-direction: row; - align-items: center; - padding: 0px 12px 12px !important; - height: 36px; - font-size:14px; - font-weight: 500; - line-height: 24px; - -} -.expandable{ - height: 20px; - width: 20px; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - padding: 6px 7px; - gap: 8px; - position: relative; - margin-left: 4px; - } -} - .bell-container{ - button{ position: relative; background: transparent; @@ -213,17 +137,15 @@ $white: #fff; width: 36px; height: 36px; border-radius: 1e+16px; - color: black !important; &:hover{ - background: #EAE6E5; + background: #EAE6E5; } .bell-icon{ - margin-left: -4px !important; + margin-left: -3px !important; &:focus{ box-shadow: none !important } } - } .badge{ z-index: 1; @@ -244,3 +166,111 @@ $white: #fff; } } } + +.notification-tray-container{ + width: 549px; + height: 100vh; + margin-top: 9px !important; + padding: 32px 0px 24px; + max-width: 549px; + overflow: scroll; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.15), 0px 2px 8px rgba(0, 0, 0, 0.15); + border-radius: 0px 0px 4px 4px; + .notification-title{ + line-height: 24px; + font-weight: 700; + font-size: 18px; + } + .setting-icon-container{ + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: flex-end; + width: 100%; + position: absolute; + margin-left: -40px; + margin-top: -7px; + span{ + height:20px; + width: 20px; + } + } + .notification-section{ + padding: 10px 24px 10px 24px; + } + .notification-item{ + padding: 10px 24px 10px 24px + } + .icon-container{ + padding: 12px 12px 12px 0px + } + .notification-content{ + padding: 0px; + .notification-tabs{ + height: 38px; + width:501px; + margin-left: 12px; + button{ + font-size: 14px; + } + .dropdown-toggle::after{ + display: none; + } + .dropdown-toggle{ + height: 36px; + padding-top: 0px !important; + padding-left: 12px !important; + div{ + min-height: 6px; + min-width: 6px; + } + } + .notification-tab, .dropdown-item{ + display: flex; + flex-direction: row; + align-items: center; + padding: 0px 12px 12px !important; + height: 36px; + font-size:14px; + font-weight: 500; + line-height: 24px; + padding-bottom: 18px; + } + .expandable{ + height: 20px; + width: 20px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 6px 7px; + gap: 8px; + position: relative; + margin-left: 4px; + } + } + .notification-item-content{ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + line-height: 24px; + width: 417px; + } + .unread{ + max-height: 48px; + width: 24px; + div{ + background: #D23228; + border-radius: 100px; + height: 10px; + width: 10px; + } + } + .course-container{ + line-height: 20px; + font-size: 12px; + } + } +} diff --git a/src/store.js b/src/store.js new file mode 100644 index 0000000..342022c --- /dev/null +++ b/src/store.js @@ -0,0 +1,16 @@ +import { configureStore } from '@reduxjs/toolkit'; + +import { notificationsReducer } from './Notifications/data'; + +export function initializeStore(preloadedState = undefined) { + return configureStore({ + reducer: { + notifications: notificationsReducer, + }, + preloadedState, + }); +} + +const store = initializeStore(); + +export default store; diff --git a/src/time-locale.js b/src/time-locale.js new file mode 100644 index 0000000..19ae993 --- /dev/null +++ b/src/time-locale.js @@ -0,0 +1,19 @@ +// eslint-disable-next-line no-unused-vars +export default function timeLocale(number, index, totalSec) { + return [ + ['just now', 'right now'], + ['%ss', 'in %s seconds'], + ['1m', 'in 1 minute'], + ['%sm', 'in %s minutes'], + ['1h', 'in 1 hour'], + ['%sh', 'in %s hours'], + ['1d', 'in 1 day'], + ['%sd', 'in %s days'], + ['1w', 'in 1 week'], + ['%sw', 'in %s weeks'], + ['4w', 'in 1 month'], + [`${number * 4}w`, 'in %s months'], + ['1y', 'in 1 year'], + ['%sy', 'in %s years'], + ][index]; +}