diff --git a/package-lock.json b/package-lock.json index aad1534..97dd7ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@edx/brand": "npm:@edx/brand-edx.org@^2.0.3", "@edx/frontend-component-footer": "10.1.6", "@edx/frontend-component-header": "^2.4.6", - "@edx/frontend-platform": "^1.15.6", + "@edx/frontend-platform": "^2.2.0", "@edx/paragon": "19.25.0", "@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/free-brands-svg-icons": "^5.15.4", @@ -3425,11 +3425,13 @@ } }, "node_modules/@edx/frontend-platform": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-1.15.6.tgz", - "integrity": "sha512-hvcJwRLy4JBdyBjHgu11nrqmMTWI901q6Ax83pf+yQpz68PpsJ0KdFjerxnkNJjU//XrWUuhSLesOPY2ntIjjg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-2.2.0.tgz", + "integrity": "sha512-OyoAgFFZqUTi//KLhmH4ooN0yZH8q4ovYEG+/yyA1HTyPa1Du/xJR6a3igdKrlW/6yeDTMMHC9MDG31SGrK89w==", "dependencies": { "@cospired/i18n-iso-languages": "2.2.0", + "@formatjs/intl-pluralrules": "^4.3.3", + "@formatjs/intl-relativetimeformat": "^10.0.1", "axios": "0.26.1", "axios-cache-adapter": "2.7.3", "form-urlencoded": "4.1.4", @@ -3444,11 +3446,10 @@ "lodash.merge": "4.6.2", "lodash.snakecase": "4.1.1", "pubsub-js": "1.9.4", - "react-intl": "2.9.0", + "react-intl": "^5.25.0", "universal-cookie": "4.0.4" }, "bin": { - "transifex-Makefile": "i18n/scripts/Makefile", "transifex-utils.js": "i18n/scripts/transifex-utils.js" }, "peerDependencies": { @@ -3482,36 +3483,6 @@ "value-equal": "^1.0.1" } }, - "node_modules/@edx/frontend-platform/node_modules/intl-messageformat": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-2.2.0.tgz", - "integrity": "sha1-NFvNRt5jC3aDMwwuUhd/9eq0hPw=", - "dependencies": { - "intl-messageformat-parser": "1.4.0" - } - }, - "node_modules/@edx/frontend-platform/node_modules/intl-messageformat-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-1.4.0.tgz", - "integrity": "sha1-tD1FqXRoytvkQzHXS7Ho3qRPwHU=", - "deprecated": "We've written a new parser that's 6x faster and is backwards compatible. Please use @formatjs/icu-messageformat-parser" - }, - "node_modules/@edx/frontend-platform/node_modules/react-intl": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.9.0.tgz", - "integrity": "sha512-27jnDlb/d2A7mSJwrbOBnUgD+rPep+abmoJE511Tf8BnoONIAUehy/U1zZCHGO17mnOwMWxqN4qC0nW11cD6rA==", - "dependencies": { - "hoist-non-react-statics": "^3.3.0", - "intl-format-cache": "^2.0.5", - "intl-messageformat": "^2.1.0", - "intl-relativeformat": "^2.1.0", - "invariant": "^2.1.1" - }, - "peerDependencies": { - "prop-types": "^15.5.4", - "react": "^0.14.9 || ^15.0.0 || ^16.0.0" - } - }, "node_modules/@edx/new-relic-source-map-webpack-plugin": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@edx/new-relic-source-map-webpack-plugin/-/new-relic-source-map-webpack-plugin-1.0.1.tgz", @@ -3694,6 +3665,44 @@ "tslib": "^2.0.1" } }, + "node_modules/@formatjs/intl-pluralrules": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.3.3.tgz", + "integrity": "sha512-NLZN8gf2qLpCuc0m565IbKLNUarEGOzk0mkdTkE4XTuNCofzoQTurW6lL3fmDlneAoYl2FiTdHa5q4o2vZF50g==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.11.4", + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/intl-pluralrules/node_modules/@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "dependencies": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/intl-relativetimeformat": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-10.0.1.tgz", + "integrity": "sha512-AABPQtPjFilXegQsnmVHrSlzjFNUffAEk5DgowY6b7WSwDI7g2W6QgW903/lbZ58emhphAbgHdtKeUBXqTiLpw==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.11.4", + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/intl-relativetimeformat/node_modules/@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "dependencies": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, "node_modules/@formatjs/intl/node_modules/@formatjs/ecma402-abstract": { "version": "1.11.4", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", @@ -16078,11 +16087,6 @@ "node": ">= 0.10" } }, - "node_modules/intl-format-cache": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/intl-format-cache/-/intl-format-cache-2.2.9.tgz", - "integrity": "sha512-Zv/u8wRpekckv0cLkwpVdABYST4hZNTDaX7reFetrYTJwxExR2VyTqQm+l0WmL0Qo8Mjb9Tf33qnfj0T7pjxdQ==" - }, "node_modules/intl-messageformat": { "version": "9.13.0", "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz", @@ -16113,29 +16117,6 @@ "tslib": "^2.1.0" } }, - "node_modules/intl-relativeformat": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/intl-relativeformat/-/intl-relativeformat-2.2.0.tgz", - "integrity": "sha512-4bV/7kSKaPEmu6ArxXf9xjv1ny74Zkwuey8Pm01NH4zggPP7JHwg2STk8Y3JdspCKRDriwIyLRfEXnj2ZLr4Bw==", - "deprecated": "This package has been deprecated, please see migration guide at 'https://github.com/formatjs/formatjs/tree/master/packages/intl-relativeformat#migration-guide'", - "dependencies": { - "intl-messageformat": "^2.0.0" - } - }, - "node_modules/intl-relativeformat/node_modules/intl-messageformat": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-2.2.0.tgz", - "integrity": "sha1-NFvNRt5jC3aDMwwuUhd/9eq0hPw=", - "dependencies": { - "intl-messageformat-parser": "1.4.0" - } - }, - "node_modules/intl-relativeformat/node_modules/intl-messageformat-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-1.4.0.tgz", - "integrity": "sha1-tD1FqXRoytvkQzHXS7Ho3qRPwHU=", - "deprecated": "We've written a new parser that's 6x faster and is backwards compatible. Please use @formatjs/icu-messageformat-parser" - }, "node_modules/into-stream": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", @@ -36165,11 +36146,13 @@ } }, "@edx/frontend-platform": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-1.15.6.tgz", - "integrity": "sha512-hvcJwRLy4JBdyBjHgu11nrqmMTWI901q6Ax83pf+yQpz68PpsJ0KdFjerxnkNJjU//XrWUuhSLesOPY2ntIjjg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-2.2.0.tgz", + "integrity": "sha512-OyoAgFFZqUTi//KLhmH4ooN0yZH8q4ovYEG+/yyA1HTyPa1Du/xJR6a3igdKrlW/6yeDTMMHC9MDG31SGrK89w==", "requires": { "@cospired/i18n-iso-languages": "2.2.0", + "@formatjs/intl-pluralrules": "^4.3.3", + "@formatjs/intl-relativetimeformat": "^10.0.1", "axios": "0.26.1", "axios-cache-adapter": "2.7.3", "form-urlencoded": "4.1.4", @@ -36184,7 +36167,7 @@ "lodash.merge": "4.6.2", "lodash.snakecase": "4.1.1", "pubsub-js": "1.9.4", - "react-intl": "2.9.0", + "react-intl": "^5.25.0", "universal-cookie": "4.0.4" }, "dependencies": { @@ -36208,31 +36191,6 @@ "tiny-warning": "^1.0.0", "value-equal": "^1.0.1" } - }, - "intl-messageformat": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-2.2.0.tgz", - "integrity": "sha1-NFvNRt5jC3aDMwwuUhd/9eq0hPw=", - "requires": { - "intl-messageformat-parser": "1.4.0" - } - }, - "intl-messageformat-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-1.4.0.tgz", - "integrity": "sha1-tD1FqXRoytvkQzHXS7Ho3qRPwHU=" - }, - "react-intl": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.9.0.tgz", - "integrity": "sha512-27jnDlb/d2A7mSJwrbOBnUgD+rPep+abmoJE511Tf8BnoONIAUehy/U1zZCHGO17mnOwMWxqN4qC0nW11cD6rA==", - "requires": { - "hoist-non-react-statics": "^3.3.0", - "intl-format-cache": "^2.0.5", - "intl-messageformat": "^2.1.0", - "intl-relativeformat": "^2.1.0", - "invariant": "^2.1.1" - } } } }, @@ -36427,6 +36385,48 @@ "tslib": "^2.0.1" } }, + "@formatjs/intl-pluralrules": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.3.3.tgz", + "integrity": "sha512-NLZN8gf2qLpCuc0m565IbKLNUarEGOzk0mkdTkE4XTuNCofzoQTurW6lL3fmDlneAoYl2FiTdHa5q4o2vZF50g==", + "requires": { + "@formatjs/ecma402-abstract": "1.11.4", + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + }, + "dependencies": { + "@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "requires": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + } + } + }, + "@formatjs/intl-relativetimeformat": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-10.0.1.tgz", + "integrity": "sha512-AABPQtPjFilXegQsnmVHrSlzjFNUffAEk5DgowY6b7WSwDI7g2W6QgW903/lbZ58emhphAbgHdtKeUBXqTiLpw==", + "requires": { + "@formatjs/ecma402-abstract": "1.11.4", + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + }, + "dependencies": { + "@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "requires": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + } + } + }, "@fortawesome/fontawesome-common-types": { "version": "0.2.36", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz", @@ -45976,11 +45976,6 @@ "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", "dev": true }, - "intl-format-cache": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/intl-format-cache/-/intl-format-cache-2.2.9.tgz", - "integrity": "sha512-Zv/u8wRpekckv0cLkwpVdABYST4hZNTDaX7reFetrYTJwxExR2VyTqQm+l0WmL0Qo8Mjb9Tf33qnfj0T7pjxdQ==" - }, "intl-messageformat": { "version": "9.13.0", "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz", @@ -46012,29 +46007,6 @@ "@formatjs/intl-numberformat": "^5.5.2" } }, - "intl-relativeformat": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/intl-relativeformat/-/intl-relativeformat-2.2.0.tgz", - "integrity": "sha512-4bV/7kSKaPEmu6ArxXf9xjv1ny74Zkwuey8Pm01NH4zggPP7JHwg2STk8Y3JdspCKRDriwIyLRfEXnj2ZLr4Bw==", - "requires": { - "intl-messageformat": "^2.0.0" - }, - "dependencies": { - "intl-messageformat": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-2.2.0.tgz", - "integrity": "sha1-NFvNRt5jC3aDMwwuUhd/9eq0hPw=", - "requires": { - "intl-messageformat-parser": "1.4.0" - } - }, - "intl-messageformat-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-1.4.0.tgz", - "integrity": "sha1-tD1FqXRoytvkQzHXS7Ho3qRPwHU=" - } - } - }, "into-stream": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", diff --git a/package.json b/package.json index f4bacbf..e3b347c 100755 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@edx/brand": "npm:@edx/brand-edx.org@^2.0.3", "@edx/frontend-component-footer": "10.1.6", "@edx/frontend-component-header": "^2.4.6", - "@edx/frontend-platform": "^1.15.6", + "@edx/frontend-platform": "^2.2.0", "@edx/paragon": "19.25.0", "@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/free-brands-svg-icons": "^5.15.4", diff --git a/src/containers/CourseCard/components/CourseCardMenu.jsx b/src/containers/CourseCard/components/CourseCardMenu.jsx deleted file mode 100644 index eb5bf31..0000000 --- a/src/containers/CourseCard/components/CourseCardMenu.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { Dropdown, Icon, IconButton } from '@edx/paragon'; -import { MoreVert } from '@edx/paragon/icons'; - -export const CourseCardMenu = () => ( - - - - Unenroll - Email Settings - Share to Facebook - Share to Twitter - - -); - -export default CourseCardMenu; diff --git a/src/containers/CourseCard/components/CourseCardMenu/hooks.js b/src/containers/CourseCard/components/CourseCardMenu/hooks.js new file mode 100644 index 0000000..a0aad13 --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardMenu/hooks.js @@ -0,0 +1,39 @@ +import React from 'react'; +import { StrictDict } from 'utils'; +import * as module from './hooks'; + +export const state = StrictDict({ + isUnenrollConfirmVisible: (val) => React.useState(val), + isEmailSettingsVisible: (val) => React.useState(val), +}); + +export const unenrollModalHooks = () => { + const [isVisible, setIsVisible] = module.state.isUnenrollConfirmVisible(false); + return { + show: () => setIsVisible(true), + hide: () => setIsVisible(false), + isVisible, + }; +}; + +export const emailSettingsModalHooks = () => { + const [isVisible, setIsVisible] = module.state.isEmailSettingsVisible(false); + return { + show: () => setIsVisible(true), + hide: () => setIsVisible(false), + isVisible, + }; +}; + +export const menuHooks = () => { + const unenrollModal = module.unenrollModalHooks(); + const emailSettingsModal = module.emailSettingsModalHooks(); + const ref = React.useRef(null); + return { + emailSettingsModal, + unenrollModal, + ref, + }; +}; + +export default menuHooks; diff --git a/src/containers/CourseCard/components/CourseCardMenu/index.jsx b/src/containers/CourseCard/components/CourseCardMenu/index.jsx new file mode 100644 index 0000000..4b23f16 --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardMenu/index.jsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import { Dropdown, Icon, IconButton } from '@edx/paragon'; +import { MoreVert } from '@edx/paragon/icons'; + +import shapes from 'data/services/lms/shapes'; +import EmailSettingsModal from 'containers/EmailSettingsModal'; +import UnenrollConfirmModal from 'containers/UnenrollConfirmModal'; +import hooks from './hooks'; + +export const CourseCardMenu = ({ cardData }) => { + const { + ref, + emailSettingsModal, + unenrollModal, + } = hooks(); + return ( + <> + + + + Unenroll + Email Settings + Share to Facebook + Share to Twitter + + + + + + ); +}; +CourseCardMenu.propTypes = { + cardData: shapes.courseRunCardData.isRequired, +}; + +export default CourseCardMenu; diff --git a/src/containers/CourseCard/components/RelatedProgram.jsx b/src/containers/CourseCard/components/RelatedProgram.jsx deleted file mode 100644 index 68914bf..0000000 --- a/src/containers/CourseCard/components/RelatedProgram.jsx +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable quotes */ -import React from 'react'; -import { Button, useToggle, ModalDialog } from '@edx/paragon'; -import { Program } from '@edx/paragon/icons'; - -export const RelatedProgram = () => { - const [isOpen, open, close] = useToggle(false); - return ( - <> - - - - - Related Programs - - - - -

- {/* eslint-disable-next-line */} - I am baby palo santo ugh celiac fashion axe. La croix lo-fi venmo whatever. Beard man braid migas single-origin coffee forage ramps. Tumeric messenger bag bicycle rights wayfarers, try-hard cronut blue bottle health goth. Sriracha tumblr cardigan, cloud bread succulents tumeric copper mug marfa semiotics woke next level organic roof party +1 try-hard. -

-
-
- - ); -}; - -export default RelatedProgram; diff --git a/src/containers/CourseCard/components/RelatedProgramsBadge.jsx b/src/containers/CourseCard/components/RelatedProgramsBadge.jsx new file mode 100644 index 0000000..8c94307 --- /dev/null +++ b/src/containers/CourseCard/components/RelatedProgramsBadge.jsx @@ -0,0 +1,25 @@ +/* eslint-disable quotes */ +import React from 'react'; +import { Button, useToggle } from '@edx/paragon'; +import { Program } from '@edx/paragon/icons'; + +import RelatedProgramsBadgeModal from 'containers/RelatedProgramsModal'; + +export const RelatedProgramsBadge = ({ cardData }) => { + const [isOpen, open, closeModal] = useToggle(false); + return ( + <> + + + + ); +}; + +export default RelatedProgramsBadge; diff --git a/src/containers/CourseCard/index.jsx b/src/containers/CourseCard/index.jsx index cfaf29f..85a1e2e 100644 --- a/src/containers/CourseCard/index.jsx +++ b/src/containers/CourseCard/index.jsx @@ -4,7 +4,7 @@ import { Card } from '@edx/paragon'; import shapes from 'data/services/lms/shapes'; -import RelatedProgram from './components/RelatedProgram'; +import RelatedProgramsBadge from './components/RelatedProgramsBadge'; import CourseCardMenu from './components/CourseCardMenu'; import CourseCardBanners from './components/CourseCardBanners'; import CourseCardActions from './components/CourseCardActions'; @@ -33,12 +33,15 @@ export const CourseCard = ({ cardData }) => { } + actions={} /> {providerName || 'Unkown'} • {courseNumber} • Access expires {accessExpirationDate} - }> + } + > diff --git a/src/containers/EmailSettingsModal/hooks.js b/src/containers/EmailSettingsModal/hooks.js new file mode 100644 index 0000000..dc78a23 --- /dev/null +++ b/src/containers/EmailSettingsModal/hooks.js @@ -0,0 +1,36 @@ +import React from 'react'; + +import { StrictDict } from 'utils'; +import { thunkActions } from 'data/redux'; + +import * as module from './hooks'; + +export const state = StrictDict({ + toggle: (val) => React.useState(val), +}); + +export const modalHooks = ({ + cardData, + closeModal, + // dispatch, +}) => { + const { isEmailEnabled } = cardData.enrollment; + const [toggleValue, setToggleValue] = module.state.toggle(isEmailEnabled); + + const onToggle = React.useCallback(() => setToggleValue(!toggleValue), [toggleValue]); + const save = React.useCallback( + () => { + console.log("save email settings"); + closeModal(); + }, + [], + ); + + return { + onToggle, + save, + toggleValue, + }; +}; + +export default modalHooks; diff --git a/src/containers/EmailSettingsModal/index.jsx b/src/containers/EmailSettingsModal/index.jsx new file mode 100644 index 0000000..b0789b0 --- /dev/null +++ b/src/containers/EmailSettingsModal/index.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import PropTypes from 'prop-types'; +import { useIntl } from 'react-intl'; + +import { + ActionRow, + Button, + Form, + ModalPopup, +} from '@edx/paragon'; + +import { nullMethod } from 'hooks'; +import shapes from 'data/services/lms/shapes'; + +import hooks from './hooks'; +import messages from './messages'; + +export const EmailSettingsModal = ({ + closeModal, + show, + menuRef, + cardData, +}) => { + if (!menuRef.current) { + return null; + } + const dispatch = useDispatch(); + const { + toggleValue, + onToggle, + save, + } = hooks({ dispatch, closeModal, cardData }); + const { formatMessage } = useIntl(); + + return ( + +
+

{formatMessage(messages.header)}

+ + {formatMessage(toggleValue ? messages.emailsOff : messages.emailsOn)} + +

{formatMessage(messages.description)}

+ + + + +
+
+ ); +}; +EmailSettingsModal.propTypes = { + cardData: shapes.courseRunCardData.isRequired, + closeModal: PropTypes.func.isRequired, + show: PropTypes.bool.isRequired, + menuRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]).isRequired, +}; + +export default EmailSettingsModal; diff --git a/src/containers/EmailSettingsModal/messages.js b/src/containers/EmailSettingsModal/messages.js new file mode 100644 index 0000000..a0af55a --- /dev/null +++ b/src/containers/EmailSettingsModal/messages.js @@ -0,0 +1,38 @@ +/* eslint-disable quotes */ +import { StrictDict } from 'utils'; + +export const messages = StrictDict({ + header: { + id: 'learner-dash.emailSettings.header', + description: 'Header for email settings modal', + defaultMessage: 'Receive course emails?', + }, + emailsOff: { + id: 'learner-dash.emailSettings.emailsOff', + description: 'Toggle text for email settings modal when email is disabled', + defaultMessage: 'Course emails are off', + }, + emailsOn: { + id: 'learner-dash.emailSettings.emailsOn', + description: 'Toggle text for email settings modal when email is enabled', + defaultMessage: 'Course emails are on', + }, + description: { + id: 'learner-dash.emailSettings.description', + description: 'Description for email settings modal', + defaultMessage: 'Course emailsi include important information about your course.', + }, + nevermind: { + id: 'learner-dash.emailSettings.nevermind', + description: 'Cancel action for email settings modal', + defaultMessage: 'Nevermind', + }, + save: { + id: 'learner-dash.emailSettings.save', + description: 'Save action for email settings modal', + defaultMessage: 'Save settings', + }, + +}); + +export default messages; diff --git a/src/containers/RelatedProgramsModal/components/ProgramCard.jsx b/src/containers/RelatedProgramsModal/components/ProgramCard.jsx new file mode 100644 index 0000000..09b83a9 --- /dev/null +++ b/src/containers/RelatedProgramsModal/components/ProgramCard.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useIntl } from 'react-intl'; + +import { + Badge, + Card, + Hyperlink, + Icon, +} from '@edx/paragon'; +import { Program } from '@edx/paragon/icons'; + +import shapes from 'data/services/lms/shapes'; +import './index.scss'; + +export const whiteFontWrapper = (node) => ({node}); + +export const messages = { + courses: { + id: 'learnerDashboard.programCard.courses', + defaultMessage: '{numCourses} Courses', + description: 'Number of courses in a program, displayed at the bottom of program card', + }, +}; + +export const ProgramCard = ({ data }) => { + const { formatMessage } = useIntl(); + const numCoursesMessage = formatMessage(messages.courses, { numCourses: data.numberOfCourses }); + return ( + + + +
+ + {data.programType} + +
+ {numCoursesMessage} • {data.estimatedDuration} +
+
+
+ ); +}; +ProgramCard.propTypes = { + data: shapes.programCard.isRequired, +}; + +export default ProgramCard; diff --git a/src/containers/RelatedProgramsModal/components/index.scss b/src/containers/RelatedProgramsModal/components/index.scss new file mode 100644 index 0000000..505980a --- /dev/null +++ b/src/containers/RelatedProgramsModal/components/index.scss @@ -0,0 +1,23 @@ +.program-card { + color: white !important; + .program-card-banner { + .pgn__card-image-cap { + height: 6rem; + } + } + .program-type-badge { + text-align: left; + height: 24px; + width: 195px; + font-size: .75rem; + vertical-align: center; + .pgn__icon { + width: .75rem !important; + height: .75rem !important; + } + } + .program-summary { + font-size: .75rem; + line-height: 1.25rem; + } +} diff --git a/src/containers/RelatedProgramsModal/hooks.js b/src/containers/RelatedProgramsModal/hooks.js new file mode 100644 index 0000000..dc78a23 --- /dev/null +++ b/src/containers/RelatedProgramsModal/hooks.js @@ -0,0 +1,36 @@ +import React from 'react'; + +import { StrictDict } from 'utils'; +import { thunkActions } from 'data/redux'; + +import * as module from './hooks'; + +export const state = StrictDict({ + toggle: (val) => React.useState(val), +}); + +export const modalHooks = ({ + cardData, + closeModal, + // dispatch, +}) => { + const { isEmailEnabled } = cardData.enrollment; + const [toggleValue, setToggleValue] = module.state.toggle(isEmailEnabled); + + const onToggle = React.useCallback(() => setToggleValue(!toggleValue), [toggleValue]); + const save = React.useCallback( + () => { + console.log("save email settings"); + closeModal(); + }, + [], + ); + + return { + onToggle, + save, + toggleValue, + }; +}; + +export default modalHooks; diff --git a/src/containers/RelatedProgramsModal/index.jsx b/src/containers/RelatedProgramsModal/index.jsx new file mode 100644 index 0000000..dac93b3 --- /dev/null +++ b/src/containers/RelatedProgramsModal/index.jsx @@ -0,0 +1,49 @@ +/* eslint-disable quotes */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from 'react-intl'; + +import { CardGrid, ModalDialog } from '@edx/paragon'; + +import shapes from 'data/services/lms/shapes'; + +import ProgramCard from './components/ProgramCard'; +import messages from './messages'; +import './index.scss'; + +export const RelatedProgramsModal = ({ isOpen, closeModal, cardData }) => { + const { formatMessage } = useIntl(); + return ( + + + {formatMessage(messages.header)} + + + {cardData.course.title} + + +

{formatMessage(messages.description)}

+ + {cardData.relatedPrograms.map(programData => )} + +
+
+ ); +}; +RelatedProgramsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + closeModal: PropTypes.func.isRequired, + cardData: shapes.courseRunCardData.isRequired, +}; + +export default RelatedProgramsModal; diff --git a/src/containers/RelatedProgramsModal/index.scss b/src/containers/RelatedProgramsModal/index.scss new file mode 100644 index 0000000..f55dc7e --- /dev/null +++ b/src/containers/RelatedProgramsModal/index.scss @@ -0,0 +1,5 @@ +.related-programs-modal { + .programs-title { + font-size: 1.5rem; + } +} diff --git a/src/containers/RelatedProgramsModal/messages.js b/src/containers/RelatedProgramsModal/messages.js new file mode 100644 index 0000000..8120aab --- /dev/null +++ b/src/containers/RelatedProgramsModal/messages.js @@ -0,0 +1,17 @@ +/* eslint-disable quotes */ +import { StrictDict } from 'utils'; + +export const messages = StrictDict({ + header: { + id: 'learner-dash.relatedPrograms.header', + description: 'Header for related settings modal', + defaultMessage: 'Related Programs', + }, + description: { + id: 'learner-dash.relatedPrograms.description', + description: 'Description for related settings modal', + defaultMessage: `Are you looking to expand your knowledge? Enrolling in a Program lets you take a series of courses in the subject that you're interested in`, + }, +}); + +export default messages; diff --git a/src/containers/UnenrollConfirmModal/hooks.js b/src/containers/UnenrollConfirmModal/hooks.js new file mode 100644 index 0000000..a806bc8 --- /dev/null +++ b/src/containers/UnenrollConfirmModal/hooks.js @@ -0,0 +1,56 @@ +import React from 'react'; + +import { StrictDict } from 'utils'; +import { thunkActions } from 'data/redux'; + +import * as module from './hooks'; + +export const state = StrictDict({ + confirmed: (val) => React.useState(val), + customReason: (val) => React.useState(val), + selectedReason: (val) => React.useState(val), + submittedReason: (val) => React.useState(val), +}); + +export const modalHooks = ({ closeModal, dispatch }) => { + const [isConfirmed, setIsConfirmed] = module.state.confirmed(false); + const [selectedReason, setSelectedReason] = module.state.selectedReason(null); + const [submittedReason, setSubmittedReason] = module.state.submittedReason(null); + const [customOption, setCustomOption] = module.state.customReason(''); + + const confirm = React.useCallback(() => setIsConfirmed(true), []); + + const reason = { + value: submittedReason, + skip: React.useCallback(() => setSubmittedReason('')), + selectOption: React.useCallback((e) => setSelectedReason(e.target.value), []), + customOption: { + value: customOption, + onChange: React.useCallback((e) => setCustomOption(e.target.value), []), + }, + selected: selectedReason, + submit: React.useCallback(() => { + console.log({ customOption, selectedReason }); + if (selectedReason === 'custom') { + setSubmittedReason(customOption); + } else { + setSubmittedReason(selectedReason); + } + }, [customOption, selectedReason]), + isSubmitted: submittedReason !== null, + }; + + const closeAndRefresh = React.useCallback(() => { + dispatch(thunkActions.app.refreshList()); + closeModal(); + }, []); + + return { + isConfirmed, + confirm, + reason, + closeAndRefresh, + }; +}; + +export default modalHooks; diff --git a/src/containers/UnenrollConfirmModal/index.jsx b/src/containers/UnenrollConfirmModal/index.jsx new file mode 100644 index 0000000..992f8eb --- /dev/null +++ b/src/containers/UnenrollConfirmModal/index.jsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import PropTypes from 'prop-types'; +import { useIntl } from 'react-intl'; + +import { + ActionRow, + Button, + Form, + ModalPopup, +} from '@edx/paragon'; + +import { nullMethod } from 'hooks'; + +import reasons from './reasons'; +import hooks from './hooks'; +import messages from './messages'; + +export const UnenrollConfirmModal = ({ + closeModal, + show, + menuRef, +}) => { + if (!menuRef.current) { + return null; + } + const dispatch = useDispatch(); + const { + isConfirmed, + confirm, + reason, + closeAndRefresh, + } = hooks({ dispatch, closeModal }); + const { formatMessage } = useIntl(); + + const option = (key) => ( + + {formatMessage(reasons.messages[key])} + + ); + return ( + +
+ {!isConfirmed && ( + <> +

{formatMessage(messages.confirmHeader)}

+

{formatMessage(messages.confirmText)}

+ + + + + + )} + {isConfirmed && !reason.isSubmitted && ( + <> +

{formatMessage(messages.reasonHeading)}

+ + {reasons.order.map(option)} + + + + + + + + + + )} + {isConfirmed && reason.isSubmitted && ( + <> +

{formatMessage(messages.finishHeading)}

+

{formatMessage(messages.finishText)}

+ + + + + )} +
+
+ ); +}; +UnenrollConfirmModal.propTypes = { + closeModal: PropTypes.func.isRequired, + show: PropTypes.bool.isRequired, + menuRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]).isRequired, +}; + + +export default UnenrollConfirmModal; diff --git a/src/containers/UnenrollConfirmModal/messages.js b/src/containers/UnenrollConfirmModal/messages.js new file mode 100644 index 0000000..365aa4d --- /dev/null +++ b/src/containers/UnenrollConfirmModal/messages.js @@ -0,0 +1,57 @@ +/* eslint-disable quotes */ +import { StrictDict } from 'utils'; + +export const messages = StrictDict({ + confirmHeader: { + id: 'learner-dash.unenrollConfirm.confirm.header', + description: 'Header for confirm unenroll modal', + defaultMessage: 'Unenroll from course?', + }, + confirmText: { + id: 'learner-dash.unenrollConfirm.confirm.text', + description: 'Content for confirm unenroll modal', + defaultMessage: `Progress that you've made so far will not be saved`, + }, + confirmCancel: { + id: 'learner-dash.unenrollConfirm.confirm.cancel', + description: 'Cancel action for confirm unenroll modal', + defaultMessage: 'Nevermind', + }, + confirmUnenroll: { + id: 'learner-dash.unenrollConfirm.confirm.unenroll', + description: 'Confirm action for confirm unenroll modal', + defaultMessage: 'Unenroll', + }, + reasonHeading: { + id: 'learner-dash.unenrollConfirm.confirm.reason.heading', + description: 'Heading for unenroll reason modal', + defaultMessage: `What's your main reason for unenrolling?`, + }, + reasonSkip: { + id: 'learner-dash.unenrollConfirm.confirm.reason.skip', + description: 'Skip action for unenroll reason modal', + defaultMessage: 'Skip', + }, + reasonSubmit: { + id: 'learner-dash.unenrollConfirm.confirm.reason.submit', + description: 'Submit action for unenroll reason modal', + defaultMessage: 'Submit', + }, + finishHeading: { + id: 'learner-dash.unenrollConfirm.confirm.finish.heading', + description: 'Heading for unenroll finish modal', + defaultMessage: 'You are unenrolled', + }, + finishText: { + id: 'learner-dash.unenrollConfirm.confirm.finish.heading', + description: 'Text for unenroll finish modal', + defaultMessage: 'Thank you for sharing your reason for unenrolling', + }, + finishReturn: { + id: 'learner-dash.unenrollConfirm.confirm.finish.return', + description: 'Return action for unenroll finish modal', + defaultMessage: 'Return to dashboard', + }, +}); + +export default messages; diff --git a/src/containers/UnenrollConfirmModal/reasons.js b/src/containers/UnenrollConfirmModal/reasons.js new file mode 100644 index 0000000..b4033f4 --- /dev/null +++ b/src/containers/UnenrollConfirmModal/reasons.js @@ -0,0 +1,86 @@ +/* eslint-disable quotes */ +import { StrictDict } from 'utils'; + +export const reasonKeys = StrictDict({ + prereqs: 'prereqs', + difficulty: 'difficulty', + goals: 'goals', + broken: 'broken', + time: 'time', + browse: 'browse', + support: 'support', + quality: 'quality', + easy: 'easy', + custom: 'custom', +}); + +export const order = [ + reasonKeys.prereqs, + reasonKeys.difficulty, + reasonKeys.goals, + reasonKeys.broken, + reasonKeys.time, + reasonKeys.browse, + reasonKeys.support, + reasonKeys.quality, + reasonKeys.easy, +]; + +const messages = StrictDict({ + [reasonKeys.prereqs]: { + id: 'learner-dash.unenrollConfirm.reasons.prereqs', + description: 'Unenroll reason option - missing prerequisites', + defaultMessage: `I don't have the academic or language prerequisites`, + }, + [reasonKeys.difficulty]: { + id: 'learner-dash.unenrollConfirm.reasons.difficulty', + description: 'Unenroll reason option - material is too hard', + defaultMessage: 'The course material was too hard', + }, + [reasonKeys.goals]: { + id: 'learner-dash.unenrollConfirm.reasons.goals', + description: 'Unenroll reason option - goals-related', + defaultMessage: `This won't help me reach my goals`, + }, + [reasonKeys.broken]: { + id: 'learner-dash.unenrollConfirm.reasons.broken', + description: 'Unenroll reason option - something broken', + defaultMessage: 'Something was broken', + }, + [reasonKeys.time]: { + id: 'learner-dash.unenrollConfirm.reasons.time', + description: 'Unenroll reason option - time-related', + defaultMessage: `I don't have the time`, + }, + [reasonKeys.browse]: { + id: 'learner-dash.unenrollConfirm.reasons.browse', + description: 'Unenroll reason option - wanted to browse', + defaultMessage: 'I just wanted to browse the material', + }, + [reasonKeys.support]: { + id: 'learner-dash.unenrollConfirm.reasons.support', + description: 'Unenroll reason option - lacking support', + defaultMessage: `I don't have enough support`, + }, + [reasonKeys.quality]: { + id: 'learner-dash.unenrollConfirm.reasons.quality', + description: 'Unenroll reason option - quality-related', + defaultMessage: 'I am not happy with the quality of the content', + }, + [reasonKeys.easy]: { + id: 'learner-dash.unenrollConfirm.reasons.easy', + description: 'Unenroll reason option - too easy', + defaultMessage: 'The course material was too easy', + }, + customPlaceholder: { + id: 'learner-dash.unenrollConfirm.reasons.custom-placeholder', + description: 'Unenroll custom reason option placeholder text', + defaultMessage: 'Other', + }, +}); + +export default { + messages, + order, + reasonKeys, +}; diff --git a/src/data/redux/thunkActions/app.js b/src/data/redux/thunkActions/app.js index a55ff1b..783de4f 100644 --- a/src/data/redux/thunkActions/app.js +++ b/src/data/redux/thunkActions/app.js @@ -13,7 +13,16 @@ import requests from './requests'; * submission list data. */ export const initialize = () => (dispatch) => ( - requests.initialize().then( + requests.initializeList().then( + ({ enrollments, entitlements }) => { + dispatch(actions.app.loadEnrollments(enrollments)); + dispatch(actions.app.loadEntitlements(entitlements)); + }, + ) +); + +export const refreshList = () => (dispatch) => ( + requests.initializeList().then( ({ enrollments, entitlements }) => { dispatch(actions.app.loadEnrollments(enrollments)); dispatch(actions.app.loadEntitlements(entitlements)); @@ -23,4 +32,5 @@ export const initialize = () => (dispatch) => ( export default StrictDict({ initialize, + refreshList, }); diff --git a/src/data/redux/thunkActions/requests.js b/src/data/redux/thunkActions/requests.js index d3a9391..4e097e0 100644 --- a/src/data/redux/thunkActions/requests.js +++ b/src/data/redux/thunkActions/requests.js @@ -34,7 +34,7 @@ export const networkRequest = ({ }); }; -export const initialize = () => ( +export const initializeList = () => ( Promise.resolve({ enrollments: fakeData.courseRunData, entitlements: fakeData.entitlementCourses, @@ -42,5 +42,5 @@ export const initialize = () => ( ); export default StrictDict({ - initialize, + initializeList, }); diff --git a/src/data/services/lms/fakeData/courses.js b/src/data/services/lms/fakeData/courses.js index e399aac..f8dc78e 100644 --- a/src/data/services/lms/fakeData/courses.js +++ b/src/data/services/lms/fakeData/courses.js @@ -13,6 +13,31 @@ export const providers = StrictDict({ }, }); +export const relatedPrograms = [ + { + provider: 'HarvardX', + bannerUrl: 'https://prod-discovery.edx-cdn.org/media/course/image/327c8e4f-315a-417b-9857-046dfc90c243-677b97464958.small.jpg', + logoUrl: 'https://prod-discovery.edx-cdn.org/organization/certificate_logos/44022f13-20df-4666-9111-cede3e5dc5b6-770e00385e7e.png', + title: 'Relativity in Modern Mechanics', + programUrl: 'www.edx/my-program', + programType: 'MicroBachelors Program', + programTypeUrl: 'www.edx/my-program-type', + numberOfCourses: 3, + estimatedDuration: '4 weeks', + }, + { + provider: 'University of Maryland', + bannerUrl: 'https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg', + logoUrl: 'https://prod-discovery.edx-cdn.org/organization/certificate_logos/b9dc96da-b3fc-45a6-b6b7-b8e12eb79335-ac60112330e3.png', + title: 'Pandering for Modern Professionals', + programUrl: 'www.edx/my-program', + programType: 'MicroBachelors Program', + programTypeUrl: 'www.edx/my-program-type', + numberOfCourses: 3, + estimatedDuration: '4 weeks', + }, +]; + export const genCourseID = (index) => `course-id${index}`; export const genCourseTitle = (index) => `Course Name ${index}`; @@ -40,6 +65,7 @@ export const genEnrollmentData = (data = {}) => ({ isVerified: false, canUpgrade: data.verified ? null : true, isAuditAccessExpired: data.verified ? null : false, + isEmailEnabled: false, ...data, }); @@ -318,6 +344,7 @@ export const courseRunData = courseRuns.map( ]; return { ...data, + relatedPrograms, courseRun: genCourseRunData({ ...data.courseRun, courseNumber }), ...iteratedData[providerIndex], credit: { isPurchased: false, requestStatus: null }, diff --git a/src/data/services/lms/shapes.js b/src/data/services/lms/shapes.js index b148a01..0344575 100644 --- a/src/data/services/lms/shapes.js +++ b/src/data/services/lms/shapes.js @@ -54,6 +54,17 @@ export const shapes = StrictDict({ grades: PropTypes.shape({ isPassing: PropTypes.bool, }), + programCard: PropTypes.shape({ + provider: PropTypes.string, + bannerUrl: PropTypes.string, + logoUrl: PropTypes.string, + title: PropTypes.string, + programUrl: PropTypes.string, + programType: PropTypes.string, + programTypeUrl: PropTypes.string, + numberOfCourses: PropTypes.number, + estimatedDuration: PropTypes.string, + }), }); shapes.courseRunCardData = PropTypes.shape({ @@ -65,6 +76,7 @@ shapes.courseRunCardData = PropTypes.shape({ enrollment: shapes.enrollment, entitlement: shapes.entitlement, grades: shapes.grades, + relatedPrograms: PropTypes.arrayOf(shapes.programCard), }); export default shapes;