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