diff --git a/package-lock.json b/package-lock.json
index 466225a..16fa975 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7176,6 +7176,15 @@
"loader-utils": "^2.0.0"
}
},
+ "@tinymce/tinymce-react": {
+ "version": "3.13.0",
+ "resolved": "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-3.13.0.tgz",
+ "integrity": "sha512-8+OHYIUP9W5D5z7gknausaG48ovQtEvjcK4c+zpha7QppRVVX0ltaINpo10V6Vb4qj9Jf7ZFfZpMRxxcFL2YvQ==",
+ "requires": {
+ "prop-types": "^15.6.2",
+ "tinymce": "^5.5.1"
+ }
+ },
"@tootallnate/once": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@@ -22486,6 +22495,16 @@
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
+ "tinymce": {
+ "version": "5.10.2",
+ "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-5.10.2.tgz",
+ "integrity": "sha512-5QhnZ6c8F28fYucLLc00MM37fZoAZ4g7QCYzwIl38i5TwJR5xGqzOv6YMideyLM4tytCzLCRwJoQen2LI66p5A=="
+ },
+ "tinymce-language-selector": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/tinymce-language-selector/-/tinymce-language-selector-1.0.4.tgz",
+ "integrity": "sha512-Ko2n2oRFGIZAtWqoG8O8B5XHxZEWZ/zRabPZ5rVeMzDTZhnbVX9oOgW+Ie18PRNMTNtOpI15OUhvrT07c0rVkw=="
+ },
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
diff --git a/package.json b/package.json
index bae78fc..3cb08fb 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,8 @@
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.1.16",
+ "@tinymce/tinymce-react": "^3.13.0",
+ "classnames": "^2.3.1",
"core-js": "3.15.2",
"prop-types": "15.7.2",
"react": "16.14.0",
@@ -53,7 +55,9 @@
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
"redux": "4.1.2",
- "regenerator-runtime": "0.13.9"
+ "regenerator-runtime": "0.13.9",
+ "tinymce": "^5.10.2",
+ "tinymce-language-selector": "^1.0.4"
},
"devDependencies": {
"@edx/frontend-build": "8.1.6",
diff --git a/src/components/bulk-email-tool/BulkEmailBody.jsx b/src/components/bulk-email-tool/BulkEmailBody.jsx
index f91b0e6..7c0a83f 100644
--- a/src/components/bulk-email-tool/BulkEmailBody.jsx
+++ b/src/components/bulk-email-tool/BulkEmailBody.jsx
@@ -1,5 +1,44 @@
-import React from 'react';
+import React, { useRef } from 'react';
+import { Button, Form } from '@edx/paragon';
+
+import { FormattedMessage } from '@edx/frontend-platform/i18n';
+import TextEditor from './TextEditor';
export default function BulkEmailBody() {
- return
;
+ const editorRef = useRef(null);
+ const onInit = (event, editor) => { editorRef.current = editor; };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
}
diff --git a/src/components/bulk-email-tool/BulkEmailTool.jsx b/src/components/bulk-email-tool/BulkEmailTool.jsx
index 0d3e74d..bd44d3c 100644
--- a/src/components/bulk-email-tool/BulkEmailTool.jsx
+++ b/src/components/bulk-email-tool/BulkEmailTool.jsx
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
+import classnames from 'classnames';
import { useParams } from 'react-router-dom';
import { Spinner } from '@edx/paragon';
@@ -8,15 +9,26 @@ import BulkEmailBody from './BulkEmailBody';
import BulkEmailTaskManager from './bulk-email-task-manager/BulkEmailTaskManager';
import Navigationtabs from '../navigation-tabs/NavigationTabs';
import { getCourseHomeCourseMetadata } from './api';
+import useMobileResponsive from '../../utils/useMobileResponsive';
export default function BulkEmailTool() {
const { courseId } = useParams();
const [courseMetadata, setCourseMetadata] = useState();
+ const isMobile = useMobileResponsive();
useEffect(() => {
async function fetchTabData() {
- const data = await getCourseHomeCourseMetadata(courseId);
+ let data;
+ try {
+ data = await getCourseHomeCourseMetadata(courseId);
+ } catch (e) {
+ setCourseMetadata({
+ isStaff: false,
+ tabs: [],
+ });
+ return;
+ }
const { tabs, is_staff: isStaff } = data;
setCourseMetadata({
isStaff,
@@ -31,7 +43,7 @@ export default function BulkEmailTool() {
courseMetadata.isStaff ? (
-
+
diff --git a/src/components/bulk-email-tool/TextEditor.jsx b/src/components/bulk-email-tool/TextEditor.jsx
new file mode 100644
index 0000000..b473a9e
--- /dev/null
+++ b/src/components/bulk-email-tool/TextEditor.jsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { Editor } from '@tinymce/tinymce-react';
+import PropTypes from 'prop-types';
+import 'tinymce';
+
+import 'tinymce/icons/default';
+import 'tinymce/themes/silver';
+import 'tinymce/skins/ui/oxide/skin.css';
+import 'tinymce/plugins/advlist';
+import 'tinymce/plugins/code';
+import 'tinymce/plugins/emoticons';
+import 'tinymce/plugins/emoticons/js/emojis';
+import 'tinymce/plugins/link';
+import 'tinymce/plugins/lists';
+import 'tinymce/plugins/table';
+import 'tinymce-language-selector';
+
+import contentUiCss from 'tinymce/skins/ui/oxide/content.css';
+import contentCss from 'tinymce/skins/content/default/content.css';
+
+export default function TextEditor(props) {
+ const {
+ onChange, onKeyUp, onInit, disabled,
+ } = props;
+
+ return (
+
+ );
+}
+
+TextEditor.defaultProps = {
+ onChange: () => {},
+ onKeyUp: () => {},
+ onInit: () => {},
+ disabled: false,
+};
+
+TextEditor.propTypes = {
+ onChange: PropTypes.func,
+ onKeyUp: PropTypes.func,
+ onInit: PropTypes.func,
+ disabled: PropTypes.bool,
+};
diff --git a/src/utils/useMobileResponsive.js b/src/utils/useMobileResponsive.js
new file mode 100644
index 0000000..44df6dc
--- /dev/null
+++ b/src/utils/useMobileResponsive.js
@@ -0,0 +1,42 @@
+import { useState, useEffect } from 'react';
+
+// NOTE: These are the breakpoints used in Bootstrap v4.0.0 as seen in
+// the documentation (https://getbootstrap.com/docs/4.0/layout/overview/#responsive-breakpoints)
+const breakpoints = {
+ extraSmall: {
+ maxWidth: 575.98,
+ },
+ small: {
+ minWidth: 576,
+ maxWidth: 767.98,
+ },
+ medium: {
+ minWidth: 768,
+ maxWidth: 991.98,
+ },
+ large: {
+ minWidth: 992,
+ maxWidth: 1199.98,
+ },
+ extraLarge: {
+ minWidth: 1200,
+ },
+};
+
+/**
+ * A react hook used to determine if the current window is mobile or not.
+ * returns true if the window is of mobile size.
+ */
+export default function useMobileResponsive(breakpoint) {
+ const [isMobileWindow, setIsMobileWindow] = useState();
+ const checkForMobile = () => {
+ setIsMobileWindow(window.matchMedia(`(max-width: ${breakpoint || breakpoints.small.maxWidth}px)`).matches);
+ };
+ useEffect(() => {
+ checkForMobile();
+ window.addEventListener('resize', checkForMobile);
+ // return this function here to clean up the event listener
+ return () => window.removeEventListener('resize', checkForMobile);
+ }, []);
+ return isMobileWindow;
+}