diff --git a/.env b/.env index 2b6ea295c..8407c826d 100644 --- a/.env +++ b/.env @@ -16,3 +16,4 @@ NEW_RELIC_APP_ID=null NEW_RELIC_LICENSE_KEY=null APP_ID='' MFE_CONFIG_API_URL='' +SITEWIDE_BANNER_CONTENT = "" diff --git a/.env.development b/.env.development index 1dd00d5c3..334b6ab5b 100644 --- a/.env.development +++ b/.env.development @@ -21,3 +21,4 @@ ADDITIONAL_METADATA_REQUIRED_FIELDS='{}' IS_NEW_SLUG_FORMAT_ENABLED='false' MARKETING_SITE_PREVIEW_URL_ROOT='' COURSE_URL_SLUGS_PATTERN = '{}' +SITEWIDE_BANNER_CONTENT = "" diff --git a/package-lock.json b/package-lock.json index c5ae72ff9..c6dd7eab1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "fast-json-stable-stringify": "2.1.0", "font-awesome": "4.7.0", "history": "4.10.1", + "js-cookie": "^3.0.5", "jsx-to-string": "1.4.0", "moment": "2.30.1", "moment-timezone": "0.5.45", @@ -33,6 +34,7 @@ "react": "^17.0.2", "react-autosuggest": "10.1.0", "react-beautiful-dnd": "13.1.1", + "react-bootstrap": "^1.6.5", "react-copy-to-clipboard": "5.1.0", "react-dom": "^17.0.2", "react-helmet": "6.1.0", @@ -14920,9 +14922,9 @@ } }, "node_modules/react-bootstrap": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.8.tgz", - "integrity": "sha512-yD6uN78XlFOkETQp6GRuVe0s5509x3XYx8PfPbirwFTYCj5/RfmSs9YZGCwkUrhZNFzj7tZPdpb+3k50mK1E4g==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.5.tgz", + "integrity": "sha512-l2rm5LtDI7JMtdGrzaxNl4OJwH0fMIJDlvwQ2TMvs9h9d0E4ELLpG3J45Pox6xUkpuFfXdWUiGazZXyIuv/OKA==", "dependencies": { "@babel/runtime": "^7.14.0", "@restart/context": "^2.1.4", diff --git a/package.json b/package.json index bb758e492..04ff9b0c4 100644 --- a/package.json +++ b/package.json @@ -26,17 +26,18 @@ "@edx/frontend-component-footer": "npm:@edx/frontend-component-footer-edx@^7.1.0", "@edx/frontend-platform": "^8.0.0", "@edx/openedx-atlas": "^0.6.0", - "@openedx/paragon": "^21.11.3", "@edx/tinymce-language-selector": "1.1.0", "@fortawesome/free-regular-svg-icons": "6.6.0", "@fortawesome/free-solid-svg-icons": "6.6.0", "@fortawesome/react-fontawesome": "0.2.2", + "@openedx/paragon": "^21.11.3", "@tinymce/tinymce-react": "3.9.0", "classnames": "2.5.1", "core-js": "3.38.1", "fast-json-stable-stringify": "2.1.0", "font-awesome": "4.7.0", "history": "4.10.1", + "js-cookie": "^3.0.5", "jsx-to-string": "1.4.0", "moment": "2.30.1", "moment-timezone": "0.5.45", @@ -46,6 +47,7 @@ "react": "^17.0.2", "react-autosuggest": "10.1.0", "react-beautiful-dnd": "13.1.1", + "react-bootstrap": "^1.6.5", "react-copy-to-clipboard": "5.1.0", "react-dom": "^17.0.2", "react-helmet": "6.1.0", @@ -64,8 +66,8 @@ }, "devDependencies": { "@edx/browserslist-config": "^1.2.0", - "@openedx/frontend-build": "^14.0.14", "@edx/stylelint-config-edx": "^2.3.0", + "@openedx/frontend-build": "^14.0.14", "@wojtekmaj/enzyme-adapter-react-17": "^0.8.0", "axios": "0.27.2", "axios-mock-adapter": "1.22.0", diff --git a/src/components/SitewideBanner/SitewideBanner.test.jsx b/src/components/SitewideBanner/SitewideBanner.test.jsx new file mode 100644 index 000000000..55eddc70b --- /dev/null +++ b/src/components/SitewideBanner/SitewideBanner.test.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Cookies from 'js-cookie'; +import { Alert } from 'react-bootstrap'; +import SitewideBanner from './index'; + +describe('SitewideBanner', () => { + it('renders correctly when visible', () => { + const wrapper = shallow( + , + ); + + expect(wrapper.find(Alert).props().variant).toBe('success'); + expect(wrapper.find(Alert).props().dismissible).toBe(true); + const alertContent = wrapper.find(Alert) + .dive() + .find('div') + .first() + .html(); + expect(alertContent).toContain('Dummy Message'); + }); + + it('calls handleDismiss and sets cookie when dismissed', () => { + const setCookieMock = jest.spyOn(Cookies, 'set'); + const wrapper = shallow( + , + ); + wrapper.find(Alert).simulate('close'); + expect(wrapper.isEmptyRender()).toBe(true); + expect(setCookieMock).toHaveBeenCalledWith('bannerCookie', 'true', { + expires: 7, + }); + setCookieMock.mockRestore(); + }); + + it('handles non-dismissible banner correctly', () => { + const wrapper = shallow( + , + ); + expect(wrapper.find(Alert).props().dismissible).toBe(false); + }); +}); diff --git a/src/components/SitewideBanner/index.jsx b/src/components/SitewideBanner/index.jsx new file mode 100644 index 000000000..715830256 --- /dev/null +++ b/src/components/SitewideBanner/index.jsx @@ -0,0 +1,59 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import Cookies from 'js-cookie'; +import { + Alert, Container, +} from 'react-bootstrap'; + +const SitewideBanner = ({ + message, type, dismissible, cookieName, cookieExpiryDays, +}) => { + const [isVisible, setIsVisible] = useState(true); + + useEffect(() => { + if (cookieName && Cookies.get(cookieName)) { + setIsVisible(false); + } + }, [cookieName]); + + const handleDismiss = () => { + setIsVisible(false); + if (cookieName) { + Cookies.set(cookieName, 'true', { expires: cookieExpiryDays }); + } + }; + + if (isVisible) { + return ( + + +
+ + + ); + } else { // eslint-disable-line no-else-return + return null; + } +}; + +SitewideBanner.propTypes = { + message: PropTypes.string.isRequired, + type: PropTypes.oneOf(['primary', 'success', 'warning', 'danger', 'info', 'secondary', 'light', 'dark']), + dismissible: PropTypes.bool, + cookieName: PropTypes.string, + cookieExpiryDays: PropTypes.number, +}; + +SitewideBanner.defaultProps = { + type: 'info', + dismissible: false, + cookieName: null, + cookieExpiryDays: 7, +}; + +export default SitewideBanner; diff --git a/src/containers/MainApp/index.jsx b/src/containers/MainApp/index.jsx index b8ce8ef9e..85e93a1cc 100644 --- a/src/containers/MainApp/index.jsx +++ b/src/containers/MainApp/index.jsx @@ -15,10 +15,18 @@ import EditStaffer from '../EditStaffer'; import CreateCollaborator from '../CreateCollaborator'; import EditCollaborator from '../EditCollaborator'; import EditCourse from '../EditCourse'; +import SitewideBanner from '../../components/SitewideBanner'; const MainApp = () => (
+
} />