feat: added notification tray

This commit is contained in:
SundasNoreen
2023-05-19 16:17:50 +05:00
parent a5069edd94
commit f8fc794458
28 changed files with 981 additions and 312 deletions

View File

@@ -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(
<AppProvider>
<AppProvider store={store}>
{/* We can fake out authentication by including another provider here with the data we want */}
<AppContext.Provider value={{
authenticatedUser: null,

53
package-lock.json generated
View File

@@ -15,9 +15,11 @@
"@fortawesome/free-regular-svg-icons": "6.3.0",
"@fortawesome/free-solid-svg-icons": "6.3.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@reduxjs/toolkit": "^1.9.5",
"babel-polyfill": "6.26.0",
"react-responsive": "8.2.0",
"react-transition-group": "4.4.5"
"react-transition-group": "4.4.5",
"timeago.js": "^4.0.2"
},
"devDependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
@@ -5703,6 +5705,29 @@
"integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==",
"dev": true
},
"node_modules/@reduxjs/toolkit": {
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz",
"integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==",
"dependencies": {
"immer": "^9.0.21",
"redux": "^4.2.1",
"redux-thunk": "^2.4.2",
"reselect": "^4.1.8"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18",
"react-redux": "^7.2.1 || ^8.0.2"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@restart/context": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz",
@@ -6677,7 +6702,7 @@
"version": "7.1.25",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz",
"integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==",
"dev": true,
"devOptional": true,
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
@@ -13189,7 +13214,6 @@
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -21738,7 +21762,7 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true
"devOptional": true
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
@@ -21795,7 +21819,7 @@
"version": "7.2.9",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
"dev": true,
"devOptional": true,
"dependencies": {
"@babel/runtime": "^7.15.4",
"@types/react-redux": "^7.1.20",
@@ -22183,7 +22207,6 @@
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.9.2"
}
@@ -22197,6 +22220,14 @@
"@redux-saga/core": "^1.2.3"
}
},
"node_modules/redux-thunk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
"peerDependencies": {
"redux": "^4"
}
},
"node_modules/reflect.ownkeys": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz",
@@ -22385,6 +22416,11 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true
},
"node_modules/reselect": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz",
"integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="
},
"node_modules/resolve": {
"version": "1.22.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
@@ -24618,6 +24654,11 @@
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
"dev": true
},
"node_modules/timeago.js": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz",
"integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w=="
},
"node_modules/tiny-invariant": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",

View File

@@ -62,9 +62,12 @@
"@fortawesome/free-regular-svg-icons": "6.3.0",
"@fortawesome/free-solid-svg-icons": "6.3.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@reduxjs/toolkit": "^1.9.5",
"babel-polyfill": "6.26.0",
"react-responsive": "8.2.0",
"react-transition-group": "4.4.5"
"react-transition-group": "4.4.5",
"timeago.js": "^4.0.2",
"react-redux": "7.2.9"
},
"peerDependencies": {
"@edx/frontend-platform": "^4.0.0",

View File

@@ -13,7 +13,7 @@ import messages from './Header.messages';
// Assets
import { CaretIcon } from './Icons';
import NotificationIcon from './Notifications/NotificationIcon';
import Notifications from './Notifications/Notifications';
class DesktopHeader extends React.Component {
constructor(props) { // eslint-disable-line no-useless-constructor
@@ -122,7 +122,6 @@ class DesktopHeader extends React.Component {
loggedIn,
intl,
appMenu,
notificationCounts,
} = this.props;
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null;
@@ -151,7 +150,7 @@ class DesktopHeader extends React.Component {
aria-label={intl.formatMessage(messages['header.label.secondary.nav'])}
className="nav secondary-menu-container align-items-center ml-auto"
>
{loggedIn && <NotificationIcon notificationCounts={notificationCounts} />}
{loggedIn && <Notifications />}
{loggedIn ? this.renderUserMenu() : this.renderLoggedOutItems()}
</nav>
</div>
@@ -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);

View File

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

View File

@@ -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 (
<div className="d-flex mx-4 my-3 bell-container">
<OverlayTrigger
trigger="click"
key="bottom"
placement="bottom"
show
overlay={(
<Popover
id="popover-positioned-bottom"
style={{
width: '549px', height: '100vh', marginTop: 34, padding: '32px 0px 24px', maxWidth: '549px',
}}
>
<Popover.Title as="h3" style={{ padding: '0px 26px 16px 24px', border: 'none' }}>
<h3 className="text-primary-500 notification-title"> Notifications </h3>
<div className="setting-icon-container">
<Icon src={Settings} />
</div>
</Popover.Title>
<Popover.Content className="notification-content">
<Tabs defaultActiveKey="discussions" id="uncontrolled-tab-example" className="notification-tabs">
<Tab eventKey="reminders" title="Reminders" notification={10} tabClassName="notification-tab">
Hello I am the first panel.
</Tab>
<Tab eventKey="discussions" title="Discussions" tabClassName="notification-tab">
<NotificationRow />
</Tab>
<Tab eventKey="grades" title="Grades" notification={1} tabClassName="notification-tab">
Hello I am the third panel.
</Tab>
<Tab eventKey="authoringg" title="Authoring" notification={5} tabClassName="notification-tab">
Hello I am the fourth panel.
</Tab>
<Tab eventKey="help" title="Help" notification={10} tabClassName="notification-tab">
Hello I am the fifth panel.
</Tab>
<Tab eventKey="about" title="About" tabClassName="notification-tab">
Hello I am the sixth panel.
</Tab>
</Tabs>
</Popover.Content>
</Popover>
)}
>
<>
<Badge variant="danger position-absolute d-flex flex-row justify-content-center align-items-center">
<Form.Label className="count">{notificationCounts[0]?.count}</Form.Label>
</Badge>
<div className="bell-icon-container">
<IconButton
onClick={() => { handleNotificationTray(!showNotificationTray); }}
onBlur={() => { handleNotificationTray(false); }}
src={NotificationsNone}
iconAs={Icon}
className="d-inline-block align-bottom ml-1 bell-icon"
/>
</div>
</>
</OverlayTrigger>
</div>
);
};
NotificationIcon.propTypes = {
notificationCounts: PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.string,
count: PropTypes.string,
})),
};
NotificationIcon.defaultProps = {
notificationCounts: [],
};
export default NotificationIcon;

View File

@@ -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 (
<div className="pt-4">
<div style={{ padding: '10px 24px 10px 24px' }} className="d-flex pb-2">
<span className="w-100 px-0">
{intl.formatMessage(messages.notificationTodayHeading)}
</span>
<span className="w-100 px-0 text-right text-info-500">
{intl.formatMessage(messages.notificationMarkAsRead)}
</span>
</div>
<div>
<NotificationRowItem />
</div>
</div>
);
};
NotificationRow.propTypes = {
};
export default React.memo(NotificationRow);

View File

@@ -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 (
<div style={{ padding: '10px 24px 10px 24px' }} className="d-flex pb-2">
<div style={{ padding: '12px 12px 12px 0px' }} className="mr-2">
<div className="d-flex notification-item mb-2 notification-item">
<div className="mr-2 icon-container">
<Icon
src={PostOutline}
className="post-summary-comment-count-dimensions mr-0.5"
src={handleIconByType(notification.type)}
style={{ height: '28px', width: '28px' }}
/>
</div>
<div className="d-flex w-100">
<div style={{ display: 'contents' }}>
<span className="px-0 text-primary-500 mb-2 w-100" style={{ lineHeight: '24px', width: '417px' }}>
SCM_Lead <span className="text-gray-500">posted </span>
<a
className="text-primary-500"
href="url"
>
Hello and welcome to SC0x!
<div className="row d-flex w-100 ml-0">
<div style={{ display: 'contents' }} className="col-md-12 w-100">
<span className="col-md-11 px-0 text-primary-500 mb-2 w-100 notification-item-content">
{notification?.respondingUser} {' '}
<span className="text-gray-500">{getContentMessageByType()} </span>
{notification?.targetUser && (
<>
{notification?.targetUser}
<span className="text-gray-500">
{authenticatedUser.username !== notification.author
? intl.formatMessage(messages.notificationResponseOnOtherPostLabel)
: intl.formatMessage(messages.notificationResponseOnYourPostLabel)}
</span>
</>
)}
<a className="text-primary-500" href={notification.URL}>
{' '}{notification?.notificationContent}
</a>
</span>
<div
className="d-flex flex-column justify-content-end"
style={{
height: '24px', width: '24px',
}}
>
<div
className="bg-brand-500"
style={{
background: '#D23228', borderRadius: '100px', height: '10px', width: '10px',
}}
/>
<div className="col-md-1 d-flex flex-column justify-content-end mb-2 unread">
{notification.status === 'unread'
&& <div className="bg-brand-500" />}
</div>
<div className="w-100 px-0">
<span className="text-gray-500 mb-2 w-100 course-container">
<span className="">{notification?.courseName}</span>
<span className="mr-1.5 font-size-8 font-style text-light-700" style={{ padding: '0px 6px' }}>
{intl.formatMessage(messages.fullStop)}
</span>
<span>
{timeago.format(notification?.time, 'time-locale')}
</span>
</span>
</div>
</div>
{/* <div style={{ display: 'contents' }}>
<span className="px-0 text-primary-500 mb-2 w-100" style={{ lineHeight: '24px', width: '417px' }}>
<span className="text-gray-500">Supply Chain Analytics</span>
<span
className="mr-1.5 font-size-8 font-style text-light-700"
style={{ lineHeight: '15px' }}
>
{intl.formatMessage(messages.fullStop)}
</span>
<span>
{timeago.format(postCreatedAt, 'time-locale')}
</span>
</span>
</div> */}
</div>
</div>
);
};
NotificationRowItem.propTypes = {
notification: PropTypes.object.isRequired,
};
export default React.memo(NotificationRowItem);

View File

@@ -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 && (
<div className="pt-4">
<div className="d-flex pb-2 notification-section">
<span className="w-100 px-0 text-gray-500">
{notifications && notifications.TODAY && intl.formatMessage(messages.notificationTodayHeading)}
</span>
<span className="w-100 px-0 text-right text-info-500">
{intl.formatMessage(messages.notificationMarkAsRead)}
</span>
</div>
<div>
{notifications && notifications.TODAY && notifications?.TODAY.map(
(notification) => <NotificationRowItem notification={notification} />,
)}
<div className="d-flex pb-2 notification-section">
<span className="w-100 px-0 text-gray-500">
{notifications && notifications.EARLIER && intl.formatMessage(messages.notificationEarlierHeading)}
</span>
</div>
{notifications && notifications.EARLIER && notifications?.EARLIER.map(
(notification) => <NotificationRowItem notification={notification} />,
)}
</div>
</div>
)
);
};
export default React.memo(NotificationSections);

View File

@@ -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) => (
<Tab
eventKey={option.key}
title={option.title}
notification={notificationUnseenCounts[option.title]}
tabClassName="notification-tab"
>
<NotificationSections />
</Tab>
)), [notificationUnseenCounts]);
return (
activeTab && (
<Tabs
defaultActiveKey={activeTab}
id="uncontrolled-tab-example"
className="notification-tabs"
onSelect={handleActiveTab}
>
{tabArray}
</Tabs>
)
);
};
export default NotificationTabs;

View File

@@ -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 (
<div className="d-flex mx-4 my-3 bell-container">
<OverlayTrigger
trigger="click"
key="bottom"
placement="bottom"
show={showNotificationTray}
overlay={(
<Popover
id="popover-positioned-bottom"
className="notification-tray-container"
>
<Popover.Title as="h3" style={{ padding: '0px 26px 16px 24px', border: 'none' }}>
<h2 className="text-primary-500 notification-title">
{intl.formatMessage(messages.notificationTitle)}
</h2>
<div className="setting-icon-container">
<Icon src={Settings} />
</div>
</Popover.Title>
<Popover.Content className="notification-content">
<NotificationTabs />
</Popover.Content>
</Popover>
)}
>
<>
{notificationCounts?.Total > 0 && (
<Badge variant="danger position-absolute d-flex flex-row justify-content-center align-items-center">
<Form.Label className="count">{notificationCounts?.Total}</Form.Label>
</Badge>
)}
<div className="bell-icon-container">
<IconButton
onClick={() => { handleNotificationTray(!showNotificationTray); }}
src={NotificationsNone}
iconAs={Icon}
className="d-inline-block align-bottom ml-1 bell-icon"
/>
</div>
</>
</OverlayTrigger>
</div>
);
};
export default Notifications;

View File

@@ -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: 'Cant find linear regression in section 3 review',
targetUser: '',
courseName: 'Supply Chain Analytics',
URL: '',
status: 'unread',
time: '1684253736371',
author: '',
},
{
type: 'comment',
respondingUser: 'MITx_Learner',
notificationContent: 'Cant find linear regression in section 3 review',
targetUser: 'MITx_Experts ',
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_Experts ',
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 Learners',
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;
}

View File

@@ -0,0 +1 @@
export * from './slice';

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
import React from 'react';
const CheckCircleFilled = () => (
<svg
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14 2.3335C7.56004 2.3335 2.33337 7.56016 2.33337 14.0002C2.33337 20.4402 7.56004 25.6668 14 25.6668C20.44 25.6668 25.6667 20.4402 25.6667 14.0002C25.6667 7.56016 20.44 2.3335 14 2.3335ZM11.6667 19.8335L5.83337 14.0002L7.47837 12.3552L11.6667 16.5318L20.5217 7.67683L22.1667 9.3335L11.6667 19.8335Z"
fill="#0D7D4D"
/>
</svg>
);
export default CheckCircleFilled;

View File

@@ -0,0 +1,19 @@
import React from 'react';
const EditOutline = () => (
<svg
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16.4033 10.5233L17.4767 11.5967L6.90667 22.1667H5.83333V21.0933L16.4033 10.5233ZM20.6033 3.5C20.3117 3.5 20.0083 3.61667 19.7867 3.83833L17.6517 5.97333L22.0267 10.3483L24.1617 8.21333C24.6167 7.75833 24.6167 7.02333 24.1617 6.56833L21.4317 3.83833C21.1983 3.605 20.9067 3.5 20.6033 3.5ZM16.4033 7.22167L3.5 20.125V24.5H7.875L20.7783 11.5967L16.4033 7.22167Z"
fill="#00262B"
/>
</svg>
);
export default EditOutline;

View File

@@ -0,0 +1,19 @@
import React from 'react';
const QuestionAnswerOutline = () => (
<svg
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.5 4.66634V12.833H6.03171L5.34337 13.5213L4.66671 14.198V4.66634H17.5ZM18.6667 2.33301H3.50004C2.85837 2.33301 2.33337 2.85801 2.33337 3.49967V19.833L7.00004 15.1663H18.6667C19.3084 15.1663 19.8334 14.6413 19.8334 13.9997V3.49967C19.8334 2.85801 19.3084 2.33301 18.6667 2.33301ZM24.5 6.99967H22.1667V17.4997H7.00004V19.833C7.00004 20.4747 7.52504 20.9997 8.16671 20.9997H21L25.6667 25.6663V8.16634C25.6667 7.52467 25.1417 6.99967 24.5 6.99967Z"
fill="#00262B"
/>
</svg>
);
export default QuestionAnswerOutline;

View File

@@ -0,0 +1,19 @@
import React from 'react';
const Report = () => (
<svg
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.3517 3.5H9.64833L3.5 9.64833V18.3517L9.64833 24.5H18.3517L24.5 18.3517V9.64833L18.3517 3.5ZM14 20.1833C13.16 20.1833 12.4833 19.5067 12.4833 18.6667C12.4833 17.8267 13.16 17.15 14 17.15C14.84 17.15 15.5167 17.8267 15.5167 18.6667C15.5167 19.5067 14.84 20.1833 14 20.1833ZM15.1667 15.1667H12.8333V8.16667H15.1667V15.1667Z"
fill="#AB0D02"
/>
</svg>
);
export default Report;

View File

@@ -0,0 +1,22 @@
import React from 'react';
const ThumbsUpOutline = () => (
<svg
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M26.8333 9.33317V14.4665L22.5283 24.4998L8.16663 24.4998L8.16663 9.54317L16.5316 1.1665L18.445 3.0565L17.1383 9.33317L26.8333 9.33317ZM10.5 10.5115V22.1665L20.9883 22.1665L24.5 13.9882V11.6665L14.2683 11.6665L15.5633 5.4365L10.5 10.5115Z"
fill="#00262B"
/>
<path d="M5.83329 24.4998H1.16663L1.16663 10.4998H5.83329L5.83329 24.4998Z" fill="#00262B" />
</svg>
);
export default ThumbsUpOutline;

View File

@@ -0,0 +1,19 @@
import React from 'react';
const Verified = () => (
<svg
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M26.8333 14L23.9866 10.745L24.3833 6.44L20.1716 5.48333L17.9666 1.75L14 3.45333L10.0333 1.75L7.82829 5.47167L3.61663 6.41667L4.01329 10.7333L1.16663 14L4.01329 17.255L3.61663 21.5717L7.82829 22.5283L10.0333 26.25L14 24.535L17.9666 26.2383L20.1716 22.5167L24.3833 21.56L23.9866 17.255L26.8333 14ZM11.7716 19.5067L7.33829 15.0617L9.06496 13.335L11.7716 16.0533L18.5966 9.205L20.3233 10.9317L11.7716 19.5067Z"
fill="#00262B"
/>
</svg>
);
export default Verified;

View File

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

View File

@@ -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 youre 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 youre 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.',
},
});

37
src/constants.js Normal file
View File

@@ -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],
},
];

View File

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

16
src/store.js Normal file
View File

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

19
src/time-locale.js Normal file
View File

@@ -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];
}