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;