diff --git a/package-lock.json b/package-lock.json index 12148bce5..225bb5085 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": "^6.6.0", "@edx/frontend-enterprise-hotjar": "7.2.0", @@ -24,7 +24,7 @@ "@redux-devtools/extension": "3.3.0", "@reduxjs/toolkit": "^2.0.0", "classnames": "^2.3.1", - "core-js": "3.45.1", + "core-js": "3.46.0", "filesize": "^10.0.0", "font-awesome": "4.7.0", "history": "5.3.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.1.tgz", "integrity": "sha512-8u3EdO0o7xX4vqorjOx3k2wbs2bu3DXlIA3bnD+Y56vSB5QYw6k5GzYqo9pPaTMGeq9TuLRvPLE/QFFlc3xvPg==", "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" }, @@ -4159,7 +4154,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", @@ -4195,7 +4189,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", @@ -4491,7 +4484,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", @@ -4710,11 +4702,10 @@ } }, "node_modules/@openedx/paragon": { - "version": "23.14.4", - "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.14.4.tgz", - "integrity": "sha512-iY0CX4THmtvmsrVwGYbxgo86PnJirgfYIS4LzqH2umxhBB5oGN41lOxs8EspUglraGb6LPs017hZBVlZFb0z8g==", + "version": "23.14.8", + "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.14.8.tgz", + "integrity": "sha512-YCoS00Pu4GVJSvxw6qAQPAvOGGBxmegydwTWLGqyROULTklh4xZvp0DXc01EZrEMz/YeLSeVvIUHzhiEmoR01g==", "license": "Apache-2.0", - "peer": true, "workspaces": [ "example", "component-generator", @@ -4725,7 +4716,7 @@ "dependencies": { "@popperjs/core": "^2.11.4", "@tokens-studio/sd-transforms": "^1.2.4", - "axios": "^0.27.2", + "axios": "^0.30.2", "bootstrap": "^4.6.2", "chalk": "^4.1.2", "child_process": "^1.0.2", @@ -4734,7 +4725,7 @@ "cli-progress": "^3.12.0", "commander": "^9.4.1", "email-prop-type": "^3.0.0", - "file-selector": "^0.6.0", + "file-selector": "^0.10.0", "glob": "^8.0.3", "inquirer": "^8.2.5", "js-toml": "^1.0.0", @@ -4759,11 +4750,11 @@ "react-loading-skeleton": "^3.1.0", "react-popper": "^2.2.5", "react-proptype-conditional-require": "^1.0.4", - "react-responsive": "^8.2.0", + "react-responsive": "^10.0.0", "react-table": "^7.7.0", "react-transition-group": "^4.4.2", "sass": "^1.58.3", - "style-dictionary": "^4.3.2", + "style-dictionary": "^4.4.0", "tabbable": "^5.3.3", "uncontrollable": "^7.2.1", "uuid": "^9.0.0" @@ -4778,13 +4769,14 @@ } }, "node_modules/@openedx/paragon/node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "0.30.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.30.2.tgz", + "integrity": "sha512-0pE4RQ4UQi1jKY6p7u6i1Tkzqmu+d+/tHS7Q7rKunWLB9WyilBTpHHpXzPNMDj5hTbK0B0PTLSz07yqMBiF6xg==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.4", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" } }, "node_modules/@openedx/paragon/node_modules/brace-expansion": { @@ -4825,6 +4817,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@openedx/paragon/node_modules/matchmediaquery": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.4.2.tgz", + "integrity": "sha512-wrZpoT50ehYOudhDjt/YvUJc6eUzcdFPdmbizfgvswCKNHD1/OBOHYJpHie+HXpu6bSkEGieFMYk6VuutaiRfA==", + "license": "MIT", + "dependencies": { + "css-mediaquery": "^0.1.2" + } + }, "node_modules/@openedx/paragon/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -4865,6 +4866,30 @@ "postcss": "^8.4" } }, + "node_modules/@openedx/paragon/node_modules/react-responsive": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-10.0.1.tgz", + "integrity": "sha512-OM5/cRvbtUWEX8le8RCT8scA8y2OPtb0Q/IViEyCEM5FBN8lRrkUOZnu87I88A6njxDldvxG+rLBxWiA7/UM9g==", + "license": "MIT", + "dependencies": { + "hyphenate-style-name": "^1.0.0", + "matchmediaquery": "^0.4.2", + "prop-types": "^15.6.1", + "shallow-equal": "^3.1.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@openedx/paragon/node_modules/shallow-equal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz", + "integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==", + "license": "MIT" + }, "node_modules/@paralleldrive/cuid2": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", @@ -5242,7 +5267,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" @@ -5291,8 +5315,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", @@ -5551,7 +5574,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", @@ -5805,7 +5827,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", @@ -6208,7 +6231,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" @@ -6347,7 +6369,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", @@ -6396,7 +6417,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", @@ -7125,7 +7145,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" }, @@ -7207,7 +7226,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", @@ -7710,7 +7728,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", @@ -7752,7 +7769,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", @@ -8216,7 +8232,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -8981,9 +8996,9 @@ } }, "node_modules/core-js": { - "version": "3.45.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz", - "integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz", + "integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -9850,7 +9865,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", @@ -10427,7 +10443,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", @@ -10485,7 +10500,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", @@ -10528,7 +10542,6 @@ "integrity": "sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "eslint-config-airbnb-base": "^15.0.0" }, @@ -10931,7 +10944,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", @@ -10989,7 +11001,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", @@ -11028,7 +11039,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", @@ -11060,7 +11070,6 @@ "integrity": "sha512-Ck77j8hF7l9N4S/rzSLOWEKpn994YH6iwUK8fr9mXIaQvGpQYmOnQLbiue1u5kI5T1y+gdgqosnEAO9NCz0DBg==", "devOptional": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -11706,12 +11715,12 @@ } }, "node_modules/file-selector": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", - "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.10.0.tgz", + "integrity": "sha512-iXLQxZTDe9qtBDkpaU4msOWNbh/4JxYSux7BsVxgt+0HBCpj9qPUFjD3SDBPLCJDoU3MsJh1i+CseQ/9488F/A==", "license": "MIT", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.7.0" }, "engines": { "node": ">= 12" @@ -15057,7 +15066,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": "*", @@ -17126,7 +17134,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", @@ -17912,6 +17919,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -17927,6 +17935,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -17968,7 +17977,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", @@ -18234,7 +18242,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" }, @@ -18414,7 +18421,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" @@ -18563,7 +18569,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", @@ -18694,7 +18699,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", @@ -18721,7 +18725,6 @@ "integrity": "sha512-FPvF2XxTSikpJxcr+bHut2H4gJ17+18Uy20D5/F+SKzFap62R3cM5wH6b8WN3LyGSYeQilLEcJcR1fjBSI2S1A==", "devOptional": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -18811,7 +18814,6 @@ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", "license": "MIT", - "peer": true, "dependencies": { "@remix-run/router": "1.23.0", "react-router": "6.30.1" @@ -19036,7 +19038,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" } @@ -19682,7 +19683,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", @@ -20687,7 +20687,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", @@ -21176,7 +21175,6 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "devOptional": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -21456,8 +21454,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", @@ -21509,7 +21506,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" }, @@ -21761,7 +21757,6 @@ "devOptional": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "napi-postinstall": "^0.2.2" }, @@ -22115,7 +22110,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", @@ -22223,7 +22217,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", @@ -22304,7 +22297,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 ae35ff0eb..bbdbab7a5 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": "^6.6.0", "@edx/frontend-enterprise-hotjar": "7.2.0", @@ -44,7 +44,7 @@ "@redux-devtools/extension": "3.3.0", "@reduxjs/toolkit": "^2.0.0", "classnames": "^2.3.1", - "core-js": "3.45.1", + "core-js": "3.46.0", "filesize": "^10.0.0", "font-awesome": "4.7.0", "history": "5.3.0", diff --git a/src/containers/ProgramDashboard/ProgramsList/ExploreProgramsCTA.test.tsx b/src/containers/ProgramDashboard/ProgramsList/ExploreProgramsCTA.test.tsx new file mode 100644 index 000000000..4793ba39b --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramsList/ExploreProgramsCTA.test.tsx @@ -0,0 +1,58 @@ +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, // Default to null for testing fallbacks + })), +})); + +describe('ExploreProgramsCTA', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const renderComponent = () => render( + + + , + ); + + it('renders the expected CTA text using i18n', () => { + renderComponent(); + + expect(screen.getByText(messages.exploreCoursesCTAText.defaultMessage)).toBeInTheDocument(); + }); + + it('renders the button with the expected text using i18n', () => { + renderComponent(); + + expect(screen.getByRole('link', { name: messages.exploreCoursesCTAButtonText.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.exploreCoursesCTAButtonText.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.exploreCoursesCTAButtonText.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..6ae6d93a3 --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramsList/ExploreProgramsCTA.tsx @@ -0,0 +1,30 @@ +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 messages from './messages'; + +const ExploreProgramsCTA: React.FC = () => { + const { formatMessage } = useIntl(); + + const href = getConfig().EXPLORE_PROGRAMS_URL || `${getConfig().LMS_BASE_URL}/courses`; + return ( + + + {formatMessage(messages.exploreCoursesCTAText)} + + + + + + ); +}; + +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..b9cbc091a --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramsList/ProgramListCard.test.tsx @@ -0,0 +1,107 @@ +import { render, 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}), +})); + +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: [ + { key: 'test-key', logoImageUrl: 'test-logo.png' }, + ], + progress: { + inProgress: 1, + notStarted: 2, + completed: 3, + }, +}; + +const mockMultipleOrgProgram = { + ...mockBaseProgram, + authoringOrganizations: [ + { key: 'MIT', logoImageUrl: 'mit-logo.png' }, + { key: 'HU', logoImageUrl: 'harvard-logo.png' }, + ], +}; + +describe('ProgramListCard', () => { + const renderComponent = (programData: ProgramData = mockBaseProgram) => 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-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..9ef066405 --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramsList/ProgramListCard.tsx @@ -0,0 +1,100 @@ +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import cardFallbackImg from '@edx/brand/paragon/images/card-imagecap-fallback.png'; +import { + breakpoints, + Card, + Row, +} from '@openedx/paragon'; +import { ProgramCardProps, AuthoringOrganization } 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 = () => { + 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; + }; + + // Set key and logoImageUrl to empty strings for fallback image or instances where there are multiple organizations + let authoringOrganization : AuthoringOrganization = { + key: '', + logoImageUrl: '', + }; + // Otherwise use the logoImageUrl and key for the organization + if (program.authoringOrganizations?.length === 1 && program.authoringOrganizations[0].logoImageUrl) { + authoringOrganization = { + logoImageUrl: program.authoringOrganizations[0].logoImageUrl, + key: program.authoringOrganizations[0].key, + }; + } + + 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 = ({ notStarted, inProgress, completed }) => { + const { formatMessage } = useIntl(); + return ( + + + + {completed} + +
+ {formatMessage(messages.progressCategoryBubblesSuccess)} +
+
+ + + + {inProgress} + +
+ {formatMessage(messages.progressCategoryBubblesInProgress)} +
+
+ + + + {notStarted} + +
+ {formatMessage(messages.progressCategoryBubblesRemaining)} +
+
+
+ ); +}; + +export default ProgressCategoryBubbles; diff --git a/src/containers/ProgramDashboard/ProgramsList/index.scss b/src/containers/ProgramDashboard/ProgramsList/index.scss new file mode 100644 index 000000000..c5ea45b70 --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramsList/index.scss @@ -0,0 +1,16 @@ +// The current Truncate component in Paragon is deprecated and soon to be removed +// See https://github.com/openedx/paragon/issues/3311 for developments on this issue + +.truncate-text-1 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; +} + +.truncate-text-2 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} diff --git a/src/containers/ProgramDashboard/ProgramsList/index.test.tsx b/src/containers/ProgramDashboard/ProgramsList/index.test.tsx new file mode 100644 index 000000000..5122079aa --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramsList/index.test.tsx @@ -0,0 +1,112 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { logError } from '@edx/frontend-platform/logging'; + +import ProgramsList from '.'; +import { getProgramsListData } from '../data/api'; +import ProgramListCard from './ProgramListCard'; +import messages from './messages'; + +// Mock API and external utilities +jest.mock('../data/api', () => ({ + getProgramsListData: jest.fn(), +})); +jest.mock('@edx/frontend-platform/logging', () => ({ + logError: jest.fn(), +})); + +// Mock Child Components +jest.mock('./ProgramListCard', () => jest.fn(({ program }) => ( +
{program.title}
+))); +jest.mock('./ExploreProgramsCTA', () => jest.fn(() => ( +
+))); + +// Mock Data +const mockApiData = { + data: [ + { uuid: '111-aaa', title: 'Data Science Program' }, + { uuid: '222-bbb', title: 'UX Design Program' }, + ], +}; + +const mockExtractedData = mockApiData.data.map(item => ({ + ...item, +})); + +describe('ProgramsList', () => { + beforeEach(() => { + jest.clearAllMocks(); + // Set up a successful mock API response by default + (getProgramsListData as jest.Mock).mockResolvedValue(mockApiData); + }); + + const renderComponent = () => render( + + + , + ); + + it('sets the correct page title', async () => { + renderComponent(); + + await waitFor(() => { + expect(document.title).toEqual(messages.programDashboardPageTitle.defaultMessage); + }); + }); + + it('renders header text and ExploreProgramsCTA', () => { + renderComponent(); + + expect(screen.getByText(messages.programsListHeaderText.defaultMessage)).toBeInTheDocument(); + expect(screen.getByTestId('explore-programs-cta')).toBeInTheDocument(); + }); + + it('fetches program data on mount', async () => { + renderComponent(); + + expect(getProgramsListData).toHaveBeenCalledTimes(1); + }); + + it('renders ProgramListCard components upon successful API response', async () => { + renderComponent(); + + // Wait for the data to load and cards to render + await waitFor(() => { + // Expect both cards to be rendered + expect(screen.getAllByTestId('program-list-card')).toHaveLength(2); + + // Check if ProgramListCard was called with the processed data + // Check for the first card + expect(ProgramListCard).toHaveBeenCalledWith( + expect.objectContaining({ + program: mockExtractedData[0], + }), + {}, + ); + // Check for the second card + expect(ProgramListCard).toHaveBeenCalledWith( + expect.objectContaining({ + program: mockExtractedData[1], + }), + {}, + ); + }); + }); + + it('calls logError if the API request fails', async () => { + const mockError = new Error('Network failed'); + (getProgramsListData as jest.Mock).mockRejectedValue(mockError); + + renderComponent(); + + // Wait for the asynchronous error handling path to execute + await waitFor(() => { + expect(logError).toHaveBeenCalledWith(mockError); + }); + + // Ensure no cards are rendered on failure + expect(screen.queryAllByTestId('program-list-card')).toHaveLength(0); + }); +}); diff --git a/src/containers/ProgramDashboard/ProgramsList/index.tsx b/src/containers/ProgramDashboard/ProgramsList/index.tsx new file mode 100644 index 000000000..fb910c943 --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramsList/index.tsx @@ -0,0 +1,59 @@ +import React, { useState, useEffect } from 'react'; +import { Helmet } from 'react-helmet'; +import { + CardGrid, Col, Container, Row, +} from '@openedx/paragon'; +import { logError } from '@edx/frontend-platform/logging'; +import { camelCaseObject } from '@edx/frontend-platform/utils'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { getProgramsListData } from '../data/api'; +import { ProgramData } from '../data/types'; +import ProgramListCard from './ProgramListCard'; +import ExploreProgramsCTA from './ExploreProgramsCTA'; +import messages from './messages'; + +import './index.scss'; + +const ProgramsList: React.FC = () => { + const { formatMessage } = useIntl(); + + const [programsData, setProgramsData] = useState([]); + + useEffect(() => { + getProgramsListData() + .then(responseData => { + setProgramsData(camelCaseObject(responseData.data)); + }) + .catch(err => logError(err)); + }, []); + + return ( + <> + + + {formatMessage(messages.programDashboardPageTitle)} + + + +
+ {formatMessage(messages.programsListHeaderText)} +
+ + + + {programsData.map(program => ( + + ))} + + + + + + +
+ + ); +}; + +export default ProgramsList; diff --git a/src/containers/ProgramDashboard/ProgramsList/messages.ts b/src/containers/ProgramDashboard/ProgramsList/messages.ts new file mode 100644 index 000000000..eca4ba21a --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramsList/messages.ts @@ -0,0 +1,41 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + programDashboardPageTitle: { + defaultMessage: 'Program Dashboard', + id: 'program.dashboard.page.title', + description: 'Page title for Program Dashboard', + }, + programsListHeaderText: { + defaultMessage: 'My Programs', + id: 'programs.list.header.text', + description: 'Header text for the programs list', + }, + exploreCoursesCTAText: { + defaultMessage: 'Browse recently launched courses and see what's new in your favorite subjects', + id: 'explore.courses.cta.text', + description: 'Call-to-action text for the explore courses component', + }, + exploreCoursesCTAButtonText: { + defaultMessage: 'Explore new courses', + id: 'explore.courses.cta.button.text', + description: 'Button text for that links to course search page', + }, + progressCategoryBubblesRemaining: { + id: 'enterprise.dashboard.programs.program.listing.card.remaining.courses.count', + defaultMessage: 'Remaining', + description: 'Label for remaining courses count on program card', + }, + progressCategoryBubblesInProgress: { + id: 'enterprise.dashboard.programs.program.listing.card.inProgress.courses.count', + defaultMessage: 'In progress', + description: 'Label for in progress courses count on program card', + }, + progressCategoryBubblesSuccess: { + id: 'enterprise.dashboard.programs.program.listing.card.completed.courses.count', + defaultMessage: 'Completed', + description: 'Label for completed courses count on program card', + }, +}); + +export default messages; diff --git a/src/containers/ProgramDashboard/data/api.ts b/src/containers/ProgramDashboard/data/api.ts new file mode 100644 index 000000000..22f8c0b32 --- /dev/null +++ b/src/containers/ProgramDashboard/data/api.ts @@ -0,0 +1,8 @@ +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { getConfig } from '@edx/frontend-platform'; + +export async function getProgramsListData() { + const url = `${getConfig().LMS_BASE_URL}/api/dashboard/v0/programs/`; + const response = await getAuthenticatedHttpClient().get(url); + return response; +} diff --git a/src/containers/ProgramDashboard/data/types.d.ts b/src/containers/ProgramDashboard/data/types.d.ts new file mode 100644 index 000000000..23faef96f --- /dev/null +++ b/src/containers/ProgramDashboard/data/types.d.ts @@ -0,0 +1,34 @@ +export interface ProgramData { + uuid: string, + title: string, + type: string, + bannerImage: { + small: ImageData, + medium: ImageData, + large: ImageData, + xSmall: ImageData, + }, + authoringOrganizations?: AuthoringOrganization[], + progress: Progress, +} + +export interface ImageData { + height: number, + width: number, + url: string, +} + +export interface AuthoringOrganization { + key: string, + logoImageUrl: string, +} + +export interface Progress { + inProgress: number, + notStarted: number, + completed: number, +} + +export interface ProgramCardProps { + program: ProgramData, +} diff --git a/src/containers/ProgramDashboard/index.tsx b/src/containers/ProgramDashboard/index.tsx index 86cf38cb4..e7cf91d18 100644 --- a/src/containers/ProgramDashboard/index.tsx +++ b/src/containers/ProgramDashboard/index.tsx @@ -1,40 +1,5 @@ -import { useEffect, useState } from 'react'; -import { Helmet } from 'react-helmet'; -import { logError } from '@edx/frontend-platform/logging'; -import { getProgramsListData } from './api'; +import ProgramsList from './ProgramsList'; -interface ProgramsData { - uuid: String, - title: String, - type: String, - banner_image: object, - authorizing_organizations: object[], - progress: object, -} - -const ProgramDashboard = () => { - const [programsData, setProgramsData] = useState([]); - - useEffect(() => { - getProgramsListData() - .then(responseData => setProgramsData(responseData.data)) - .catch(err => logError(err)); - }, []); - - return ( - <> - - Program Dashboard - -
- {programsData.map(item => ( -
- {item.title} -
- ))} -
- - ); +export { + ProgramsList, }; - -export default ProgramDashboard; diff --git a/src/custom.d.ts b/src/custom.d.ts new file mode 100644 index 000000000..1c5923252 --- /dev/null +++ b/src/custom.d.ts @@ -0,0 +1,4 @@ +declare module '*.png' { + const value: string; + export default value; +} diff --git a/src/index.jsx b/src/index.jsx index 4989f2b88..a219ba75c 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -5,7 +5,7 @@ import 'regenerator-runtime/runtime'; import React, { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { - Route, Navigate, Routes, + Navigate, Route, Routes, } from 'react-router-dom'; import { @@ -25,7 +25,7 @@ import { import { FooterSlot } from '@edx/frontend-component-footer'; import LearnerDashboardHeader from 'containers/LearnerDashboardHeader'; -import ProgramDashboard from './containers/ProgramDashboard'; +import { ProgramsList } from 'containers/ProgramDashboard'; import { configuration } from './config'; @@ -43,7 +43,10 @@ subscribe(APP_READY, () => { } /> {getConfig().ENABLE_PROGRAM_DASHBOARD && ( - } /> + <> + } /> + program details page
} /> + )} } />