diff --git a/package-lock.json b/package-lock.json
index a4fabf852..b6462660b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.0.1",
"license": "AGPL-3.0",
"dependencies": {
- "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
+ "@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^8.0.0",
"@edx/frontend-enterprise-hotjar": "7.2.0",
@@ -142,7 +142,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
"integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.24.7",
@@ -2255,7 +2254,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
},
@@ -2278,7 +2276,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
}
@@ -2504,7 +2501,6 @@
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.5.2.tgz",
"integrity": "sha512-YlxNWs8NW/I7F03k/jH6grWIuY/GJrspq7fqWm5K0ocvNEf+B8XKcaLUof+jVUuCItK93SoVRDZewwejnjty5w==",
"license": "AGPL-3.0",
- "peer": true,
"dependencies": {
"@cospired/i18n-iso-languages": "4.2.0",
"@formatjs/intl-pluralrules": "4.3.3",
@@ -3134,7 +3130,6 @@
"integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==",
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "^0.2.36"
},
@@ -4196,7 +4191,6 @@
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz",
"integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/core": "^7.11.6",
"@jest/types": "^29.6.3",
@@ -4232,7 +4226,6 @@
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
"integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jest/schemas": "^29.6.3",
"@types/istanbul-lib-coverage": "^2.0.0",
@@ -4528,7 +4521,6 @@
"integrity": "sha512-Iu4/GPq90Xr/MSWnonn2qX8VDhI89HN7KOYBZ0/sxmAQgvXXNc7OYNC7kumvzbYzKueJQTyZoUYS7UjKB/n1WA==",
"devOptional": true,
"license": "AGPL-3.0",
- "peer": true,
"dependencies": {
"@babel/cli": "7.24.8",
"@babel/core": "7.24.9",
@@ -4751,7 +4743,6 @@
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.18.0.tgz",
"integrity": "sha512-xrYhTzi6wQW6C9bRMfQl8smRf/u8Cg0MCZiQFKl6rpP9dpKJ4+lpADrVN8EU6wzSDi65dF38kZ6QD5yvBRSRAQ==",
"license": "Apache-2.0",
- "peer": true,
"workspaces": [
"example",
"component-generator",
@@ -5313,7 +5304,6 @@
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
- "peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@@ -5362,8 +5352,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@reduxjs/toolkit/node_modules/redux-thunk": {
"version": "3.1.0",
@@ -5622,7 +5611,6 @@
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0",
@@ -5876,7 +5864,8 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -6280,7 +6269,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -6419,7 +6407,6 @@
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.4.0",
"@typescript-eslint/scope-manager": "5.62.0",
@@ -6468,7 +6455,6 @@
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
"devOptional": true,
"license": "BSD-2-Clause",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/types": "5.62.0",
@@ -7197,7 +7183,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -7279,7 +7264,6 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -7783,7 +7767,6 @@
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",
"integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
@@ -7825,7 +7808,6 @@
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
"integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jest/transform": "^29.7.0",
"@types/babel__core": "^7.1.14",
@@ -8289,7 +8271,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001718",
"electron-to-chromium": "^1.5.160",
@@ -9923,7 +9904,8 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/dom-converter": {
"version": "0.2.0",
@@ -10500,7 +10482,6 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
@@ -10558,7 +10539,6 @@
"integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"eslint-config-airbnb-base": "^15.0.0",
"object.assign": "^4.1.2",
@@ -10601,7 +10581,6 @@
"integrity": "sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"eslint-config-airbnb-base": "^15.0.0"
},
@@ -11004,7 +10983,6 @@
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8",
@@ -11062,7 +11040,6 @@
"integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.20.7",
"aria-query": "^5.1.3",
@@ -11101,7 +11078,6 @@
"integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"array-includes": "^3.1.6",
"array.prototype.flatmap": "^1.3.1",
@@ -11133,7 +11109,6 @@
"integrity": "sha512-Ck77j8hF7l9N4S/rzSLOWEKpn994YH6iwUK8fr9mXIaQvGpQYmOnQLbiue1u5kI5T1y+gdgqosnEAO9NCz0DBg==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -15118,7 +15093,6 @@
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
"integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jest/types": "^29.6.3",
"@types/node": "*",
@@ -17191,7 +17165,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.1",
@@ -17977,6 +17950,7 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -17992,6 +17966,7 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=10"
},
@@ -18033,7 +18008,6 @@
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@@ -18299,7 +18273,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -18479,7 +18452,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -18628,7 +18600,6 @@
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.8.9.tgz",
"integrity": "sha512-TUfj5E7lyUDvz/GtovC9OMh441kBr08rtIbgh3p0R8iF3hVY+V2W9Am7rb8BpJ/29BH1utJOqOOhmvEVh3GfZg==",
"license": "BSD-3-Clause",
- "peer": true,
"dependencies": {
"@formatjs/ecma402-abstract": "2.2.4",
"@formatjs/icu-messageformat-parser": "2.9.4",
@@ -18759,7 +18730,6 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.15.4",
"@types/react-redux": "^7.1.20",
@@ -18786,7 +18756,6 @@
"integrity": "sha512-FPvF2XxTSikpJxcr+bHut2H4gJ17+18Uy20D5/F+SKzFap62R3cM5wH6b8WN3LyGSYeQilLEcJcR1fjBSI2S1A==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -18876,7 +18845,6 @@
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz",
"integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@remix-run/router": "1.23.1",
"react-router": "6.30.2"
@@ -19101,7 +19069,6 @@
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.9.2"
}
@@ -19748,7 +19715,6 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -20753,7 +20719,6 @@
"integrity": "sha512-+xU0IA1StzqAqFs/QtXkK+XJa7wpS4X5H+JQccRKsRCElgeLGocFU1U/UMvMUylKFw6vwGV+Y/a2wb2pm5rFFQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@bundled-es-modules/deepmerge": "^4.3.1",
"@bundled-es-modules/glob": "^10.4.2",
@@ -21242,7 +21207,6 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -21519,8 +21483,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD",
- "peer": true
+ "license": "0BSD"
},
"node_modules/tsutils": {
"version": "3.21.0",
@@ -21572,7 +21535,6 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"license": "(MIT OR CC0-1.0)",
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -21824,7 +21786,6 @@
"devOptional": true,
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"napi-postinstall": "^0.2.2"
},
@@ -22178,7 +22139,6 @@
"integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6",
@@ -22286,7 +22246,6 @@
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^2.1.1",
@@ -22367,7 +22326,6 @@
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/bonjour": "^3.5.9",
"@types/connect-history-api-fallback": "^1.3.5",
diff --git a/package.json b/package.json
index 1cf63398c..d2da5ffa2 100755
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
"access": "public"
},
"dependencies": {
- "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
+ "@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^8.0.0",
"@edx/frontend-enterprise-hotjar": "7.2.0",
diff --git a/src/App.jsx b/src/App.jsx
index 2c148f98e..bccd74dc3 100755
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -6,7 +6,6 @@ import { logError } from '@edx/frontend-platform/logging';
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
import { ErrorPage, AppContext } from '@edx/frontend-platform/react';
-import { FooterSlot } from '@edx/frontend-component-footer';
import { Alert } from '@openedx/paragon';
import { RequestKeys } from 'data/constants/requests';
@@ -22,9 +21,6 @@ import track from 'tracking';
import fakeData from 'data/services/lms/fakeData/courses';
-import AppWrapper from 'containers/AppWrapper';
-import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
-
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
import './App.scss';
@@ -77,22 +73,16 @@ export const App = () => {
{formatMessage(messages.pageTitle)}
-
-
-
-
- {hasNetworkFailure
- ? (
-
-
-
- ) : (
-
- )}
-
-
-
-
+
+ {hasNetworkFailure
+ ? (
+
+
+
+ ) : (
+
+ )}
+
>
);
};
diff --git a/src/App.test.jsx b/src/App.test.jsx
index 102d3792d..37b054936 100644
--- a/src/App.test.jsx
+++ b/src/App.test.jsx
@@ -8,12 +8,7 @@ import { reduxHooks } from 'hooks';
import { App } from './App';
import messages from './messages';
-jest.mock('@edx/frontend-component-footer', () => ({
- FooterSlot: jest.fn(() => FooterSlot
),
-}));
jest.mock('containers/Dashboard', () => jest.fn(() => Dashboard
));
-jest.mock('containers/LearnerDashboardHeader', () => jest.fn(() => LearnerDashboardHeader
));
-jest.mock('containers/AppWrapper', () => jest.fn(({ children }) => {children}
));
jest.mock('data/redux', () => ({
selectors: 'redux.selectors',
actions: 'redux.actions',
@@ -49,19 +44,6 @@ describe('App router component', () => {
it('displays title in helmet component', async () => {
await waitFor(() => expect(document.title).toEqual(messages.pageTitle.defaultMessage));
});
- it('displays learner dashboard header', () => {
- const learnerDashboardHeader = screen.getByText('LearnerDashboardHeader');
- expect(learnerDashboardHeader).toBeInTheDocument();
- });
- it('wraps the header and main components in an AppWrapper widget container', () => {
- const appWrapper = screen.getByText('LearnerDashboardHeader').parentElement;
- expect(appWrapper).toHaveClass('AppWrapper');
- expect(appWrapper.children[1].id).toEqual('main');
- });
- it('displays footer slot', () => {
- const footerSlot = screen.getByText('FooterSlot');
- expect(footerSlot).toBeInTheDocument();
- });
};
describe('no network failure', () => {
beforeEach(() => {
diff --git a/src/containers/Dashboard/index.jsx b/src/containers/Dashboard/index.jsx
index ebdb1ef78..fc0ca08e0 100644
--- a/src/containers/Dashboard/index.jsx
+++ b/src/containers/Dashboard/index.jsx
@@ -13,14 +13,12 @@ import './index.scss';
export const Dashboard = () => {
hooks.useInitializeDashboard();
- const { pageTitle } = hooks.useDashboardMessages();
const hasCourses = reduxHooks.useHasCourses();
const initIsPending = reduxHooks.useRequestIsPending(RequestKeys.initialize);
const showSelectSessionModal = reduxHooks.useShowSelectSessionModal();
return (
-
{pageTitle}
{!initIsPending && (
<>
diff --git a/src/containers/Dashboard/index.test.jsx b/src/containers/Dashboard/index.test.jsx
index 48f03cfbe..323ecd380 100644
--- a/src/containers/Dashboard/index.test.jsx
+++ b/src/containers/Dashboard/index.test.jsx
@@ -2,7 +2,6 @@ import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
-import hooks from './hooks';
import Dashboard from '.';
jest.mock('hooks', () => ({
@@ -24,8 +23,6 @@ jest.mock('./LoadingView', () => jest.fn(() =>
LoadingView
));
jest.mock('containers/SelectSessionModal', () => jest.fn(() =>
SelectSessionModal
));
jest.mock('./DashboardLayout', () => jest.fn(() =>
DashboardLayout
));
-const pageTitle = 'test-page-title';
-
describe('Dashboard', () => {
const createWrapper = (props = {}) => {
const {
@@ -33,7 +30,6 @@ describe('Dashboard', () => {
initIsPending = true,
showSelectSessionModal = true,
} = props;
- hooks.useDashboardMessages.mockReturnValue({ pageTitle });
reduxHooks.useHasCourses.mockReturnValue(hasCourses);
reduxHooks.useRequestIsPending.mockReturnValue(initIsPending);
reduxHooks.useShowSelectSessionModal.mockReturnValue(showSelectSessionModal);
@@ -41,11 +37,6 @@ describe('Dashboard', () => {
};
describe('render', () => {
- it('page title is displayed in sr-only h1 tag', () => {
- createWrapper();
- const heading = screen.getByText(pageTitle);
- expect(heading).toHaveClass('sr-only');
- });
describe('initIsPending false', () => {
it('should render DashboardModalSlot', () => {
createWrapper({ initIsPending: false });
diff --git a/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx b/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx
index f13177ffa..1af991898 100644
--- a/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx
+++ b/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx
@@ -9,18 +9,20 @@ const getLearnerHeaderMenu = (
courseSearchUrl,
authenticatedUser,
exploreCoursesClick,
+ pathname,
) => ({
mainMenu: [
{
type: 'item',
href: '/',
content: formatMessage(messages.course),
- isActive: true,
+ isActive: pathname === '/',
},
...(getConfig().ENABLE_PROGRAMS ? [{
type: 'item',
- href: `${urls.programsUrl()}`,
+ href: getConfig().ENABLE_PROGRAM_DASHBOARD ? '/programs' : `${urls.programsUrl()}`,
content: formatMessage(messages.program),
+ isActive: pathname === '/programs',
}] : []),
...(!getConfig().NON_BROWSABLE_COURSES ? [{
type: 'item',
diff --git a/src/containers/LearnerDashboardHeader/hooks.js b/src/containers/LearnerDashboardHeader/hooks.js
index 5367ab3b5..d585b2d75 100644
--- a/src/containers/LearnerDashboardHeader/hooks.js
+++ b/src/containers/LearnerDashboardHeader/hooks.js
@@ -15,10 +15,10 @@ export const findCoursesNavClicked = (href) => track.findCourses.findCoursesClic
});
export const useLearnerDashboardHeaderMenu = ({
- courseSearchUrl, authenticatedUser, exploreCoursesClick,
+ courseSearchUrl, authenticatedUser, exploreCoursesClick, pathname,
}) => {
const { formatMessage } = useIntl();
- return getLearnerHeaderMenu(formatMessage, courseSearchUrl, authenticatedUser, exploreCoursesClick);
+ return getLearnerHeaderMenu(formatMessage, courseSearchUrl, authenticatedUser, exploreCoursesClick, pathname);
};
export default {
diff --git a/src/containers/LearnerDashboardHeader/index.jsx b/src/containers/LearnerDashboardHeader/index.jsx
index 2cd167658..b782355e1 100644
--- a/src/containers/LearnerDashboardHeader/index.jsx
+++ b/src/containers/LearnerDashboardHeader/index.jsx
@@ -6,15 +6,19 @@ import Header from '@edx/frontend-component-header';
import { reduxHooks } from 'hooks';
import urls from 'data/services/lms/urls';
+import { useLocation } from 'react-router-dom';
+import { useDashboardMessages } from 'containers/Dashboard/hooks';
import ConfirmEmailBanner from './ConfirmEmailBanner';
import { useLearnerDashboardHeaderMenu, findCoursesNavClicked } from './hooks';
-
import './index.scss';
export const LearnerDashboardHeader = () => {
const { authenticatedUser } = React.useContext(AppContext);
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
+ const { pageTitle } = useDashboardMessages();
+ const location = useLocation();
+ const { pathname } = location;
const exploreCoursesClick = () => {
findCoursesNavClicked(urls.baseAppUrl(courseSearchUrl));
@@ -24,6 +28,7 @@ export const LearnerDashboardHeader = () => {
courseSearchUrl,
authenticatedUser,
exploreCoursesClick,
+ pathname,
});
return (
@@ -34,6 +39,7 @@ export const LearnerDashboardHeader = () => {
secondaryMenuItems={learnerHomeHeaderMenu.secondaryMenu}
userMenuItems={learnerHomeHeaderMenu.userMenu}
/>
+
{pageTitle}
>
);
diff --git a/src/containers/LearnerDashboardHeader/index.test.jsx b/src/containers/LearnerDashboardHeader/index.test.jsx
index 6179b4526..930701d3e 100644
--- a/src/containers/LearnerDashboardHeader/index.test.jsx
+++ b/src/containers/LearnerDashboardHeader/index.test.jsx
@@ -1,8 +1,10 @@
import { mergeConfig } from '@edx/frontend-platform';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { useLocation } from 'react-router-dom';
import urls from 'data/services/lms/urls';
+import { useDashboardMessages } from 'containers/Dashboard/hooks';
import LearnerDashboardHeader from '.';
import { findCoursesNavClicked } from './hooks';
@@ -20,6 +22,12 @@ jest.mock('./hooks', () => ({
findCoursesNavClicked: jest.fn(),
}));
+jest.mock('react-router-dom', () => ({
+ useLocation: jest.fn(() => ({
+ pathname: '/',
+ })),
+}));
+
const mockedHeaderProps = jest.fn();
jest.mock('containers/MasqueradeBar', () => jest.fn(() =>
MasqueradeBar
));
jest.mock('./ConfirmEmailBanner', () => jest.fn(() =>
ConfirmEmailBanner
));
@@ -27,9 +35,21 @@ jest.mock('@edx/frontend-component-header', () => jest.fn((props) => {
mockedHeaderProps(props);
return
Header
;
}));
+jest.mock('containers/Dashboard/hooks', () => ({
+ useDashboardMessages: jest.fn(),
+}));
+
+const pageTitle = 'test-page-title';
describe('LearnerDashboardHeader', () => {
beforeEach(() => jest.clearAllMocks());
+
+ it('page title is displayed in sr-only h1 tag', () => {
+ useDashboardMessages.mockReturnValue({ pageTitle });
+ render(
);
+ const heading = screen.getByText(pageTitle);
+ expect(heading).toHaveClass('sr-only');
+ });
it('renders and discover url is correct', () => {
mergeConfig({ ORDER_HISTORY_URL: 'test-url' });
render(
);
@@ -58,6 +78,26 @@ describe('LearnerDashboardHeader', () => {
const { mainMenuItems } = props;
expect(mainMenuItems.length).toBe(3);
});
+
+ it('should highlight the active tab depending on the pathname', () => {
+ render(
);
+ const props = mockedHeaderProps.mock.calls[0][0];
+ const { mainMenuItems } = props;
+ expect(mainMenuItems[0].isActive).toBe(true);
+ });
+
+ it('should highlight the active tab depending on the pathname', () => {
+ mergeConfig({ ENABLE_PROGRAMS: true, ENABLE_PROGRAM_DASHBOARD: true });
+ useLocation.mockReturnValueOnce({
+ pathname: '/programs',
+ });
+ render(
);
+ const props = mockedHeaderProps.mock.calls[0][0];
+ const { mainMenuItems } = props;
+ expect(mainMenuItems[0].isActive).toBe(false);
+ expect(mainMenuItems[1].isActive).toBe(true);
+ });
+
it('should not display Discover New tab if it is disabled by configuration', () => {
mergeConfig({ NON_BROWSABLE_COURSES: true });
render(
);
diff --git a/src/containers/ProgramDashboard/ProgramsList/ExploreProgramsCTA.test.tsx b/src/containers/ProgramDashboard/ProgramsList/ExploreProgramsCTA.test.tsx
new file mode 100644
index 000000000..97ed8a2e6
--- /dev/null
+++ b/src/containers/ProgramDashboard/ProgramsList/ExploreProgramsCTA.test.tsx
@@ -0,0 +1,64 @@
+import { render, screen } from '@testing-library/react';
+import { getConfig } from '@edx/frontend-platform';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+
+import ExploreProgramsCTA from './ExploreProgramsCTA';
+import messages from './messages';
+
+jest.mock('@edx/frontend-platform', () => ({
+ getConfig: jest.fn(() => ({
+ LMS_BASE_URL: 'https://courses.example.com',
+ EXPLORE_PROGRAMS_URL: null,
+ })),
+}));
+
+describe('ExploreProgramsCTA', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const renderComponent = (props = {}) => render(
+
+
+ ,
+ );
+
+ it('renders the expected CTA text when there are enrollments', () => {
+ renderComponent();
+
+ expect(screen.getByText(messages.exploreProgramsCTAText.defaultMessage)).toBeInTheDocument();
+ });
+
+ it('renders the expected CTA when there are no enrollments', () => {
+ renderComponent({ hasEnrollments: false });
+
+ expect(screen.getByText(messages.hasNoEnrollmentsText.defaultMessage)).toBeInTheDocument();
+ });
+
+ it('renders the button with the expected text', () => {
+ renderComponent();
+
+ expect(screen.getByRole('link', { name: messages.exploreProgramsCTAButtonText.defaultMessage })).toBeInTheDocument();
+ });
+
+ it('uses EXPLORE_PROGRAMS_URL when it is defined', () => {
+ const customUrl = 'https://custom.explore.url/programs';
+ getConfig.mockReturnValueOnce({
+ LMS_BASE_URL: 'https://courses.example.com',
+ EXPLORE_PROGRAMS_URL: customUrl,
+ });
+
+ renderComponent();
+
+ const button = screen.getByRole('link', { name: messages.exploreProgramsCTAButtonText.defaultMessage });
+ expect(button).toHaveAttribute('href', customUrl);
+ });
+
+ it('falls back to LMS_BASE_URL/courses when EXPLORE_PROGRAMS_URL is not defined', () => {
+ renderComponent();
+
+ const button = screen.getByRole('link', { name: messages.exploreProgramsCTAButtonText.defaultMessage });
+ const expectedFallbackUrl = `${getConfig().LMS_BASE_URL}/courses`;
+ expect(button).toHaveAttribute('href', expectedFallbackUrl);
+ });
+});
diff --git a/src/containers/ProgramDashboard/ProgramsList/ExploreProgramsCTA.tsx b/src/containers/ProgramDashboard/ProgramsList/ExploreProgramsCTA.tsx
new file mode 100644
index 000000000..bca5446c3
--- /dev/null
+++ b/src/containers/ProgramDashboard/ProgramsList/ExploreProgramsCTA.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import { getConfig } from '@edx/frontend-platform';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Card, Button } from '@openedx/paragon';
+import { Search } from '@openedx/paragon/icons';
+import { ExploreProgramsCTAProps } from '../data/types';
+import messages from './messages';
+
+const ExploreProgramsCTA: React.FC
= ({
+ hasEnrollments = true,
+}) => {
+ const { formatMessage } = useIntl();
+
+ const href = getConfig().EXPLORE_PROGRAMS_URL || `${getConfig().LMS_BASE_URL}/courses`;
+ return (
+
+
+ {hasEnrollments ? (
+ formatMessage(messages.exploreProgramsCTAText)
+ ) : (
+
+ {formatMessage(messages.hasNoEnrollmentsText)}
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default ExploreProgramsCTA;
diff --git a/src/containers/ProgramDashboard/ProgramsList/ProgramListCard.test.tsx b/src/containers/ProgramDashboard/ProgramsList/ProgramListCard.test.tsx
new file mode 100644
index 000000000..9493abb47
--- /dev/null
+++ b/src/containers/ProgramDashboard/ProgramsList/ProgramListCard.test.tsx
@@ -0,0 +1,131 @@
+import { render, RenderResult, screen } from '@testing-library/react';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import ProgramListCard from './ProgramListCard';
+import { ProgramData } from '../data/types';
+
+jest.mock('react-router-dom', () => ({
+ Link: jest.fn(({ children, ...props }) => {children}),
+}));
+
+jest.mock('@edx/frontend-platform', () => ({
+ getConfig: jest.fn(() => ({
+ LMS_BASE_URL: 'test-base-url',
+ })),
+}));
+
+const mockBaseProgram = {
+ uuid: 'test-uuid',
+ title: 'test-title',
+ type: 'test-type',
+ bannerImage: {
+ xSmall: { url: 'banner-xSmall.jpg', width: 348, height: 116 },
+ small: { url: 'banner-small.jpg', width: 435, height: 145 },
+ medium: { url: 'banner-medium.jpg', width: 726, height: 242 },
+ large: { url: 'banner-large.jpg', width: 1440, height: 480 },
+ },
+ authoringOrganizations: [
+ {
+ uuid: 'org-uuid-1',
+ key: 'test-key',
+ name: 'test-org-1',
+ logoImageUrl: 'test-logo.png',
+ certificateLogoImageUrl: 'test-cert-logo.png',
+ },
+ ],
+ progress: {
+ inProgress: 1,
+ notStarted: 2,
+ completed: 3,
+ },
+};
+
+const mockMultipleOrgProgram = {
+ ...mockBaseProgram,
+ authoringOrganizations: [
+ {
+ uuid: 'org-uuid-1',
+ name: 'MIT',
+ key: 'MITx',
+ logoImageUrl: 'mit-logo.png',
+ certificateLogoImageUrl: 'mit-cert-logo-1.png',
+ },
+ {
+ uuid: 'org-uuid-2',
+ name: 'Harvard',
+ key: 'Harvardx',
+ logoImageUrl: 'harvard-logo.png',
+ certificateLogoImageUrl: 'harvard-cert-logo-2.png',
+ },
+ ],
+};
+
+describe('ProgramListCard', () => {
+ const renderComponent = (programData: ProgramData = mockBaseProgram): RenderResult => render(
+
+
+ ,
+ );
+
+ it('renders all data for program', () => {
+ renderComponent();
+ expect(screen.getByText(mockBaseProgram.title)).toBeInTheDocument();
+ expect(screen.getByText(mockBaseProgram.type)).toBeInTheDocument();
+ expect(screen.getByText(mockBaseProgram.authoringOrganizations[0].key)).toBeInTheDocument();
+ const logoImageNode = screen.getByAltText(mockBaseProgram.authoringOrganizations[0].key);
+ expect(logoImageNode).toHaveAttribute('src', mockBaseProgram.authoringOrganizations[0].logoImageUrl);
+ expect(screen.getByText(mockBaseProgram.progress.inProgress)).toBeInTheDocument();
+ expect(screen.getByText('In progress')).toBeInTheDocument();
+ expect(screen.getByText(mockBaseProgram.progress.completed)).toBeInTheDocument();
+ expect(screen.getByText('Completed')).toBeInTheDocument();
+ expect(screen.getByText(mockBaseProgram.progress.notStarted)).toBeInTheDocument();
+ expect(screen.getByText('Remaining')).toBeInTheDocument();
+ });
+
+ it('renders names of all organizations when more than one', () => {
+ renderComponent(mockMultipleOrgProgram);
+ const aggregatedOrganizations = mockMultipleOrgProgram.authoringOrganizations.map(org => org.key).join(', ');
+ expect(screen.getByText(aggregatedOrganizations)).toBeInTheDocument();
+ });
+
+ it('doesnt render logo of organizations when more than one', () => {
+ const { queryByAltText } = renderComponent(mockMultipleOrgProgram);
+ const logoImageNode = queryByAltText(mockMultipleOrgProgram.authoringOrganizations[0].key);
+ expect(logoImageNode).toBeNull();
+ });
+
+ it('each card links to a progress page using the program uuid', async () => {
+ const { getByTestId } = renderComponent();
+ const programCard = getByTestId('program-list-card');
+ expect(programCard).toHaveAttribute('to', 'test-base-url/dashboard/programs/test-uuid');
+ });
+
+ it.each([{
+ width: 1450,
+ size: 'large',
+ },
+ {
+ width: 1300,
+ size: 'large',
+ },
+ {
+ width: 1000,
+ size: 'large',
+ },
+ {
+ width: 800,
+ size: 'medium',
+ },
+ {
+ width: 600,
+ size: 'small',
+ },
+ {
+ width: 500,
+ size: 'xSmall',
+ }])('tests window size', ({ width, size }) => {
+ global.innerWidth = width;
+ const { getByAltText } = renderComponent();
+ const imageCap = getByAltText('program card image for test-title');
+ expect(imageCap).toHaveAttribute('src', `banner-${size}.jpg`);
+ });
+});
diff --git a/src/containers/ProgramDashboard/ProgramsList/ProgramListCard.tsx b/src/containers/ProgramDashboard/ProgramsList/ProgramListCard.tsx
new file mode 100644
index 000000000..d1af152bb
--- /dev/null
+++ b/src/containers/ProgramDashboard/ProgramsList/ProgramListCard.tsx
@@ -0,0 +1,96 @@
+import React, { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+import { getConfig } from '@edx/frontend-platform';
+import cardFallbackImg from '@edx/brand/paragon/images/card-imagecap-fallback.png';
+import {
+ breakpoints,
+ Card,
+ Row,
+} from '@openedx/paragon';
+import { ProgramCardProps } from '../data/types';
+import ProgressCategoryBubbles from './ProgressCategoryBubbles';
+
+const ProgramListCard: React.FC = ({
+ program,
+}) => {
+ const [windowWidth, setWindowWidth] = useState(window.innerWidth);
+
+ useEffect(() => {
+ const handleWindowResize = () => {
+ setWindowWidth(window.innerWidth);
+ };
+
+ window.addEventListener('resize', handleWindowResize);
+
+ return () => {
+ window.removeEventListener('resize', handleWindowResize);
+ };
+ }, []);
+
+ const getBannerImageURL = (): string => {
+ let imageURL = '';
+ // We need to check that the breakpoint value exists before using it
+ // Otherwise TypeScript will flag it as it can potentially be undefined in Paragon
+ if (typeof breakpoints.large.minWidth === 'number' && windowWidth >= breakpoints.large.minWidth) {
+ imageURL = program.bannerImage.large.url;
+ } else if (typeof breakpoints.medium.minWidth === 'number' && windowWidth >= breakpoints.medium.minWidth) {
+ imageURL = program.bannerImage.medium.url;
+ } else if (typeof breakpoints.small.minWidth === 'number' && windowWidth >= breakpoints.small.minWidth) {
+ imageURL = program.bannerImage.small.url;
+ } else {
+ imageURL = program.bannerImage.xSmall.url;
+ }
+ return imageURL;
+ };
+
+ const getOrgImageUrl = (): string => {
+ // Otherwise use the logoImageUrl and key for the organization
+ if (program.authoringOrganizations?.length === 1 && program.authoringOrganizations[0].logoImageUrl) {
+ return program.authoringOrganizations[0].logoImageUrl;
+ }
+ return '';
+ };
+
+ return (
+
+
+
+
+ {program.authoringOrganizations && (
+
+ {program.authoringOrganizations.map(org => org.key).join(', ')}
+
+ )}
+
+ {program.type}
+
+
+
+
+ {program.title}
+
+
+
+
+
+ );
+};
+
+export default ProgramListCard;
diff --git a/src/containers/ProgramDashboard/ProgramsList/ProgressCategoryBubbles.test.tsx b/src/containers/ProgramDashboard/ProgramsList/ProgressCategoryBubbles.test.tsx
new file mode 100644
index 000000000..1bc5c5bd9
--- /dev/null
+++ b/src/containers/ProgramDashboard/ProgramsList/ProgressCategoryBubbles.test.tsx
@@ -0,0 +1,18 @@
+import { render, screen } from '@testing-library/react';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+
+import ProgressCategoryBubbles from './ProgressCategoryBubbles';
+
+describe('ProgressCategoryBubbles', () => {
+ it('renders the correct values for each category', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByTestId('completed-count')).toHaveTextContent('0');
+ expect(screen.getByTestId('in-progress-count')).toHaveTextContent('1');
+ expect(screen.getByTestId('remaining-count')).toHaveTextContent('2');
+ });
+});
diff --git a/src/containers/ProgramDashboard/ProgramsList/ProgressCategoryBubbles.tsx b/src/containers/ProgramDashboard/ProgramsList/ProgressCategoryBubbles.tsx
new file mode 100644
index 000000000..3a9c0b17a
--- /dev/null
+++ b/src/containers/ProgramDashboard/ProgramsList/ProgressCategoryBubbles.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { Bubble, Stack } from '@openedx/paragon';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import messages from './messages';
+
+import { Progress } from '../data/types';
+
+const ProgressCategoryBubbles: React.FC