feat: added notification tray
This commit is contained in:
@@ -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
53
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
41
src/Notifications/NotificationSections.jsx
Normal file
41
src/Notifications/NotificationSections.jsx
Normal 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);
|
||||
53
src/Notifications/NotificationTabs.jsx
Normal file
53
src/Notifications/NotificationTabs.jsx
Normal 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;
|
||||
77
src/Notifications/Notifications.jsx
Normal file
77
src/Notifications/Notifications.jsx
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
1
src/Notifications/data/index.js
Normal file
1
src/Notifications/data/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './slice';
|
||||
3
src/Notifications/data/selectors.js
Normal file
3
src/Notifications/data/selectors.js
Normal 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;
|
||||
62
src/Notifications/data/slice.js
Normal file
62
src/Notifications/data/slice.js
Normal 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;
|
||||
36
src/Notifications/data/thunks.js
Normal file
36
src/Notifications/data/thunks.js
Normal 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());
|
||||
}
|
||||
}
|
||||
);
|
||||
19
src/Notifications/icons/CheckCircleFilled.jsx
Normal file
19
src/Notifications/icons/CheckCircleFilled.jsx
Normal 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;
|
||||
19
src/Notifications/icons/EditOutline.jsx
Normal file
19
src/Notifications/icons/EditOutline.jsx
Normal 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;
|
||||
19
src/Notifications/icons/QuestionAnswerOutline.jsx
Normal file
19
src/Notifications/icons/QuestionAnswerOutline.jsx
Normal 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;
|
||||
19
src/Notifications/icons/Report.jsx
Normal file
19
src/Notifications/icons/Report.jsx
Normal 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;
|
||||
22
src/Notifications/icons/ThumbsUpOutline.jsx
Normal file
22
src/Notifications/icons/ThumbsUpOutline.jsx
Normal 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;
|
||||
19
src/Notifications/icons/Verified.jsx
Normal file
19
src/Notifications/icons/Verified.jsx
Normal 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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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.',
|
||||
},
|
||||
});
|
||||
|
||||
37
src/constants.js
Normal file
37
src/constants.js
Normal 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],
|
||||
},
|
||||
];
|
||||
190
src/index.scss
190
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
src/store.js
Normal file
16
src/store.js
Normal 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
19
src/time-locale.js
Normal 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];
|
||||
}
|
||||
Reference in New Issue
Block a user