diff --git a/package-lock.json b/package-lock.json
index da875cc170..4e333b398e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3601,6 +3601,16 @@
"color-convert": "^2.0.1"
}
},
+ "aria-query": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz",
+ "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=",
+ "dev": true,
+ "requires": {
+ "ast-types-flow": "0.0.7",
+ "commander": "^2.11.0"
+ }
+ },
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
@@ -3633,6 +3643,12 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
+ "commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true
+ },
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
@@ -3642,6 +3658,29 @@
"ms": "2.1.2"
}
},
+ "emoji-regex": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+ "dev": true
+ },
+ "eslint-plugin-jsx-a11y": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz",
+ "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.4.5",
+ "aria-query": "^3.0.0",
+ "array-includes": "^3.0.3",
+ "ast-types-flow": "^0.0.7",
+ "axobject-query": "^2.0.2",
+ "damerau-levenshtein": "^1.0.4",
+ "emoji-regex": "^7.0.2",
+ "has": "^1.0.3",
+ "jsx-ast-utils": "^2.2.1"
+ }
+ },
"jest": {
"version": "26.6.3",
"resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz",
@@ -5762,6 +5801,14 @@
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",
"integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow=="
},
+ "@types/debug": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
+ "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==",
+ "requires": {
+ "@types/ms": "*"
+ }
+ },
"@types/eslint": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.1.tgz",
@@ -5837,6 +5884,14 @@
"@types/node": "*"
}
},
+ "@types/hast": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
+ "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==",
+ "requires": {
+ "@types/unist": "*"
+ }
+ },
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
@@ -5909,6 +5964,19 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
+ "@types/mdast": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
+ "integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==",
+ "requires": {
+ "@types/unist": "*"
+ }
+ },
+ "@types/mdurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
+ "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA=="
+ },
"@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@@ -5920,6 +5988,11 @@
"integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
"dev": true
},
+ "@types/ms": {
+ "version": "0.7.31",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
+ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
+ },
"@types/needle": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/@types/needle/-/needle-2.5.2.tgz",
@@ -6121,6 +6194,11 @@
"source-map": "^0.6.1"
}
},
+ "@types/unist": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
+ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
+ },
"@types/warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
@@ -6709,21 +6787,13 @@
}
},
"aria-query": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz",
- "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz",
+ "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==",
"dev": true,
"requires": {
- "ast-types-flow": "0.0.7",
- "commander": "^2.11.0"
- },
- "dependencies": {
- "commander": {
- "version": "2.20.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
- "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
- "dev": true
- }
+ "@babel/runtime": "^7.10.2",
+ "@babel/runtime-corejs3": "^7.10.2"
}
},
"arr-diff": {
@@ -6901,6 +6971,12 @@
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.4.tgz",
"integrity": "sha512-SA5mXJWrId1TaQjfxUYghbqQ/hYioKmLJvPJyDuYRtXXenFNMjj4hSSt1Cf1xsuXSXrtxrVC5Ot4eU6cOtBDdA=="
},
+ "axe-core": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.1.tgz",
+ "integrity": "sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==",
+ "dev": true
+ },
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
@@ -7270,6 +7346,11 @@
}
}
},
+ "bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="
+ },
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -8177,6 +8258,11 @@
"integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
"dev": true
},
+ "character-entities": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.1.tgz",
+ "integrity": "sha512-OzmutCf2Kmc+6DrFrrPS8/tDh2+DpnrfzdICHWhcVC9eOd0N1PXmQEE1a8iM4IziIAG+8tmTq3K+oo0ubH6RRQ=="
+ },
"chardet": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
@@ -8548,6 +8634,11 @@
"delayed-stream": "~1.0.0"
}
},
+ "comma-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg=="
+ },
"commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -9065,9 +9156,9 @@
}
},
"damerau-levenshtein": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz",
- "integrity": "sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"dev": true
},
"data-urls": {
@@ -9106,6 +9197,14 @@
"integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==",
"dev": true
},
+ "decode-named-character-reference": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.1.tgz",
+ "integrity": "sha512-YV/0HQHreRwKb7uBopyIkLG17jG6Sv2qUchk9qSoVJ2f+flwRsPNBO0hAnjt6mTNYUT+vw9Gy2ihXg4sUWPi2w==",
+ "requires": {
+ "character-entities": "^2.0.0"
+ }
+ },
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
@@ -9521,6 +9620,11 @@
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
"integrity": "sha1-PvqHMj67hj5mls67AILUj/PW96E="
},
+ "diff": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
+ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w=="
+ },
"diff-sequences": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz",
@@ -10345,27 +10449,49 @@
}
},
"eslint-plugin-jsx-a11y": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz",
- "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==",
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz",
+ "integrity": "sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g==",
"dev": true,
"requires": {
- "@babel/runtime": "^7.4.5",
- "aria-query": "^3.0.0",
- "array-includes": "^3.0.3",
+ "@babel/runtime": "^7.16.3",
+ "aria-query": "^4.2.2",
+ "array-includes": "^3.1.4",
"ast-types-flow": "^0.0.7",
- "axobject-query": "^2.0.2",
- "damerau-levenshtein": "^1.0.4",
- "emoji-regex": "^7.0.2",
+ "axe-core": "^4.3.5",
+ "axobject-query": "^2.2.0",
+ "damerau-levenshtein": "^1.0.7",
+ "emoji-regex": "^9.2.2",
"has": "^1.0.3",
- "jsx-ast-utils": "^2.2.1"
+ "jsx-ast-utils": "^3.2.1",
+ "language-tags": "^1.0.5",
+ "minimatch": "^3.0.4"
},
"dependencies": {
+ "@babel/runtime": {
+ "version": "7.17.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz",
+ "integrity": "sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==",
+ "dev": true,
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ },
"emoji-regex": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
- "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
+ },
+ "jsx-ast-utils": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz",
+ "integrity": "sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==",
+ "dev": true,
+ "requires": {
+ "array-includes": "^3.1.3",
+ "object.assign": "^4.1.2"
+ }
}
}
},
@@ -10826,8 +10952,7 @@
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
- "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
- "dev": true
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"extend-shallow": {
"version": "3.0.2",
@@ -12004,6 +12129,11 @@
}
}
},
+ "hast-util-whitespace": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz",
+ "integrity": "sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg=="
+ },
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -12363,6 +12493,11 @@
"dev": true,
"optional": true
},
+ "iframe-resizer": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/iframe-resizer/-/iframe-resizer-4.3.2.tgz",
+ "integrity": "sha512-gOWo2hmdPjMQsQ+zTKbses08mDfDEMh4NneGQNP4qwePYujY1lguqP6gnbeJkf154gojWlBhIltlgnMfYjGHWA=="
+ },
"ignore": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
@@ -12936,6 +13071,11 @@
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
+ "inline-style-parser": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
+ "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
+ },
"inquirer": {
"version": "7.3.3",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz",
@@ -16703,6 +16843,21 @@
"integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==",
"dev": true
},
+ "language-subtag-registry": {
+ "version": "0.3.21",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz",
+ "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==",
+ "dev": true
+ },
+ "language-tags": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz",
+ "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=",
+ "dev": true,
+ "requires": {
+ "language-subtag-registry": "~0.3.2"
+ }
+ },
"leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -17197,12 +17352,80 @@
"is-buffer": "~1.1.6"
}
},
+ "mdast-util-definitions": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz",
+ "integrity": "sha512-5hcR7FL2EuZ4q6lLMUK5w4lHT2H3vqL9quPvYZ/Ku5iifrirfMHiGdhxdXMUbUkDmz5I+TYMd7nbaxUhbQkfpQ==",
+ "requires": {
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.0",
+ "unist-util-visit": "^3.0.0"
+ },
+ "dependencies": {
+ "unist-util-visit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-3.1.0.tgz",
+ "integrity": "sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==",
+ "requires": {
+ "@types/unist": "^2.0.0",
+ "unist-util-is": "^5.0.0",
+ "unist-util-visit-parents": "^4.0.0"
+ }
+ }
+ }
+ },
+ "mdast-util-from-markdown": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz",
+ "integrity": "sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==",
+ "requires": {
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "mdast-util-to-string": "^3.1.0",
+ "micromark": "^3.0.0",
+ "micromark-util-decode-numeric-character-reference": "^1.0.0",
+ "micromark-util-decode-string": "^1.0.0",
+ "micromark-util-normalize-identifier": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "unist-util-stringify-position": "^3.0.0",
+ "uvu": "^0.5.0"
+ }
+ },
+ "mdast-util-to-hast": {
+ "version": "12.1.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.1.1.tgz",
+ "integrity": "sha512-qE09zD6ylVP14jV4mjLIhDBOrpFdShHZcEsYvvKGABlr9mGbV7mTlRWdoFxL/EYSTNDiC9GZXy7y8Shgb9Dtzw==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "@types/mdast": "^3.0.0",
+ "@types/mdurl": "^1.0.0",
+ "mdast-util-definitions": "^5.0.0",
+ "mdurl": "^1.0.0",
+ "micromark-util-sanitize-uri": "^1.0.0",
+ "unist-builder": "^3.0.0",
+ "unist-util-generated": "^2.0.0",
+ "unist-util-position": "^4.0.0",
+ "unist-util-visit": "^4.0.0"
+ }
+ },
+ "mdast-util-to-string": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz",
+ "integrity": "sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA=="
+ },
"mdn-data": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
"integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
"dev": true
},
+ "mdurl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
+ "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
+ },
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -17379,6 +17602,233 @@
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
+ "micromark": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.0.10.tgz",
+ "integrity": "sha512-ryTDy6UUunOXy2HPjelppgJ2sNfcPz1pLlMdA6Rz9jPzhLikWXv/irpWV/I2jd68Uhmny7hHxAlAhk4+vWggpg==",
+ "requires": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-core-commonmark": "^1.0.1",
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-combine-extensions": "^1.0.0",
+ "micromark-util-decode-numeric-character-reference": "^1.0.0",
+ "micromark-util-encode": "^1.0.0",
+ "micromark-util-normalize-identifier": "^1.0.0",
+ "micromark-util-resolve-all": "^1.0.0",
+ "micromark-util-sanitize-uri": "^1.0.0",
+ "micromark-util-subtokenize": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.1",
+ "uvu": "^0.5.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
+ "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ }
+ }
+ },
+ "micromark-core-commonmark": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz",
+ "integrity": "sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==",
+ "requires": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-factory-destination": "^1.0.0",
+ "micromark-factory-label": "^1.0.0",
+ "micromark-factory-space": "^1.0.0",
+ "micromark-factory-title": "^1.0.0",
+ "micromark-factory-whitespace": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-classify-character": "^1.0.0",
+ "micromark-util-html-tag-name": "^1.0.0",
+ "micromark-util-normalize-identifier": "^1.0.0",
+ "micromark-util-resolve-all": "^1.0.0",
+ "micromark-util-subtokenize": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.1",
+ "uvu": "^0.5.0"
+ }
+ },
+ "micromark-factory-destination": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz",
+ "integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==",
+ "requires": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "micromark-factory-label": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz",
+ "integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==",
+ "requires": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "uvu": "^0.5.0"
+ }
+ },
+ "micromark-factory-space": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz",
+ "integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==",
+ "requires": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "micromark-factory-title": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz",
+ "integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==",
+ "requires": {
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "uvu": "^0.5.0"
+ }
+ },
+ "micromark-factory-whitespace": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz",
+ "integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==",
+ "requires": {
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "micromark-util-character": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz",
+ "integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==",
+ "requires": {
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "micromark-util-chunked": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz",
+ "integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==",
+ "requires": {
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "micromark-util-classify-character": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz",
+ "integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==",
+ "requires": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "micromark-util-combine-extensions": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz",
+ "integrity": "sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==",
+ "requires": {
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "micromark-util-decode-numeric-character-reference": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz",
+ "integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==",
+ "requires": {
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "micromark-util-decode-string": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz",
+ "integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==",
+ "requires": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-decode-numeric-character-reference": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "micromark-util-encode": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz",
+ "integrity": "sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA=="
+ },
+ "micromark-util-html-tag-name": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.0.0.tgz",
+ "integrity": "sha512-NenEKIshW2ZI/ERv9HtFNsrn3llSPZtY337LID/24WeLqMzeZhBEE6BQ0vS2ZBjshm5n40chKtJ3qjAbVV8S0g=="
+ },
+ "micromark-util-normalize-identifier": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz",
+ "integrity": "sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==",
+ "requires": {
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "micromark-util-resolve-all": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz",
+ "integrity": "sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==",
+ "requires": {
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "micromark-util-sanitize-uri": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.0.0.tgz",
+ "integrity": "sha512-cCxvBKlmac4rxCGx6ejlIviRaMKZc0fWm5HdCHEeDWRSkn44l6NdYVRyU+0nT1XC72EQJMZV8IPHF+jTr56lAg==",
+ "requires": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-encode": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "micromark-util-subtokenize": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz",
+ "integrity": "sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==",
+ "requires": {
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "uvu": "^0.5.0"
+ }
+ },
+ "micromark-util-symbol": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz",
+ "integrity": "sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ=="
+ },
+ "micromark-util-types": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.2.tgz",
+ "integrity": "sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w=="
+ },
"micromatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
@@ -17406,14 +17856,22 @@
"mime-db": {
"version": "1.47.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz",
- "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw=="
+ "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==",
+ "dev": true
},
"mime-types": {
- "version": "2.1.30",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz",
- "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==",
+ "version": "2.1.34",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
+ "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
"requires": {
- "mime-db": "1.47.0"
+ "mime-db": "1.51.0"
+ },
+ "dependencies": {
+ "mime-db": {
+ "version": "1.51.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
+ "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="
+ }
}
},
"mimic-fn": {
@@ -17520,6 +17978,19 @@
"minimist": "^1.2.5"
}
},
+ "moment": {
+ "version": "2.29.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
+ "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
+ },
+ "moment-timezone": {
+ "version": "0.5.34",
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz",
+ "integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==",
+ "requires": {
+ "moment": ">= 2.9.0"
+ }
+ },
"mozjpeg": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/mozjpeg/-/mozjpeg-7.1.1.tgz",
@@ -19194,6 +19665,11 @@
"warning": "^4.0.0"
}
},
+ "property-information": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz",
+ "integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w=="
+ },
"proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
@@ -19660,6 +20136,14 @@
"scheduler": "^0.20.2"
}
},
+ "react-download-link": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/react-download-link/-/react-download-link-2.3.0.tgz",
+ "integrity": "sha512-0aoj2DJFBfiD9jtdIn+WAseO1GSYmgkB5y5Ljt3DeC7j1RUlx0rR5y4S+wZwdxGhvgFogCzyh5Pa4W4YE+Pg/Q==",
+ "requires": {
+ "prop-types": "^15.6.0"
+ }
+ },
"react-error-overlay": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz",
@@ -19731,6 +20215,39 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
+ "react-markdown": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.0.tgz",
+ "integrity": "sha512-qbrWpLny6Ef2xHqnYqtot948LXP+4FtC+MWIuaN1kvSnowM+r1qEeEHpSaU0TDBOisQuj+Qe6eFY15cNL3gLAw==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "@types/unist": "^2.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-whitespace": "^2.0.0",
+ "prop-types": "^15.0.0",
+ "property-information": "^6.0.0",
+ "react-is": "^17.0.0",
+ "remark-parse": "^10.0.0",
+ "remark-rehype": "^10.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-object": "^0.3.0",
+ "unified": "^10.0.0",
+ "unist-util-visit": "^4.0.0",
+ "vfile": "^5.0.0"
+ },
+ "dependencies": {
+ "react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
+ }
+ }
+ },
+ "react-moment": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/react-moment/-/react-moment-1.1.1.tgz",
+ "integrity": "sha512-WjwvxBSnmLMRcU33do0KixDB+9vP3e84eCse+rd+HNklAMNWyRgZTDEQlay/qK6lcXFPRuEIASJTpEt6pyK7Ww=="
+ },
"react-overlays": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.1.1.tgz",
@@ -20168,6 +20685,27 @@
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
"dev": true
},
+ "remark-parse": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.1.tgz",
+ "integrity": "sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==",
+ "requires": {
+ "@types/mdast": "^3.0.0",
+ "mdast-util-from-markdown": "^1.0.0",
+ "unified": "^10.0.0"
+ }
+ },
+ "remark-rehype": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz",
+ "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "@types/mdast": "^3.0.0",
+ "mdast-util-to-hast": "^12.1.0",
+ "unified": "^10.0.0"
+ }
+ },
"remove-trailing-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@@ -20524,6 +21062,14 @@
}
}
},
+ "sade": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
+ "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
+ "requires": {
+ "mri": "^1.1.0"
+ }
+ },
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -21263,6 +21809,11 @@
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
"dev": true
},
+ "space-separated-tokens": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz",
+ "integrity": "sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw=="
+ },
"spawn-sync": {
"version": "1.0.15",
"resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz",
@@ -21693,6 +22244,14 @@
"schema-utils": "^3.0.0"
}
},
+ "style-to-object": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz",
+ "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==",
+ "requires": {
+ "inline-style-parser": "0.1.1"
+ }
+ },
"stylehacks": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.0.1.tgz",
@@ -22526,6 +23085,11 @@
"escape-string-regexp": "^1.0.2"
}
},
+ "trough": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz",
+ "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g=="
+ },
"truncate-html": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/truncate-html/-/truncate-html-1.0.4.tgz",
@@ -22700,6 +23264,32 @@
"integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==",
"dev": true
},
+ "unified": {
+ "version": "10.1.2",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz",
+ "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==",
+ "requires": {
+ "@types/unist": "^2.0.0",
+ "bail": "^2.0.0",
+ "extend": "^3.0.0",
+ "is-buffer": "^2.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^5.0.0"
+ },
+ "dependencies": {
+ "is-buffer": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
+ "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
+ },
+ "is-plain-obj": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.0.0.tgz",
+ "integrity": "sha512-NXRbBtUdBioI73y/HmOhogw/U5msYPC9DAtGkJXeFcFWSFZw0mCUsPxk/snTuJHzNKA8kLBK4rH97RMB1BfCXw=="
+ }
+ }
+ },
"union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@@ -22712,6 +23302,67 @@
"set-value": "^2.0.1"
}
},
+ "unist-builder": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-3.0.0.tgz",
+ "integrity": "sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ==",
+ "requires": {
+ "@types/unist": "^2.0.0"
+ }
+ },
+ "unist-util-generated": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.0.tgz",
+ "integrity": "sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw=="
+ },
+ "unist-util-is": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.1.1.tgz",
+ "integrity": "sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ=="
+ },
+ "unist-util-position": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.1.tgz",
+ "integrity": "sha512-mgy/zI9fQ2HlbOtTdr2w9lhVaiFUHWQnZrFF2EUoVOqtAUdzqMtNiD99qA5a1IcjWVR8O6aVYE9u7Z2z1v0SQA=="
+ },
+ "unist-util-stringify-position": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.2.tgz",
+ "integrity": "sha512-7A6eiDCs9UtjcwZOcCpM4aPII3bAAGv13E96IkawkOAW0OhH+yRxtY0lzo8KiHpzEMfH7Q+FizUmwp8Iqy5EWg==",
+ "requires": {
+ "@types/unist": "^2.0.0"
+ }
+ },
+ "unist-util-visit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.0.tgz",
+ "integrity": "sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ==",
+ "requires": {
+ "@types/unist": "^2.0.0",
+ "unist-util-is": "^5.0.0",
+ "unist-util-visit-parents": "^5.0.0"
+ },
+ "dependencies": {
+ "unist-util-visit-parents": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.0.tgz",
+ "integrity": "sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg==",
+ "requires": {
+ "@types/unist": "^2.0.0",
+ "unist-util-is": "^5.0.0"
+ }
+ }
+ }
+ },
+ "unist-util-visit-parents": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz",
+ "integrity": "sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==",
+ "requires": {
+ "@types/unist": "^2.0.0",
+ "unist-util-is": "^5.0.0"
+ }
+ },
"universal-cookie": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz",
@@ -23008,6 +23659,24 @@
"dev": true,
"optional": true
},
+ "uvu": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.3.tgz",
+ "integrity": "sha512-brFwqA3FXzilmtnIyJ+CxdkInkY/i4ErvP7uV0DnUVxQcQ55reuHphorpF+tZoVHK2MniZ/VJzI7zJQoc9T9Yw==",
+ "requires": {
+ "dequal": "^2.0.0",
+ "diff": "^5.0.0",
+ "kleur": "^4.0.3",
+ "sade": "^1.7.3"
+ },
+ "dependencies": {
+ "kleur": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz",
+ "integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA=="
+ }
+ }
+ },
"v8-compile-cache": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
@@ -23053,6 +23722,33 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
+ "vfile": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.2.tgz",
+ "integrity": "sha512-w0PLIugRY3Crkgw89TeMvHCzqCs/zpreR31hl4D92y6SOE07+bfJe+dK5Q2akwS+i/c801kzjoOr9gMcTe6IAA==",
+ "requires": {
+ "@types/unist": "^2.0.0",
+ "is-buffer": "^2.0.0",
+ "unist-util-stringify-position": "^3.0.0",
+ "vfile-message": "^3.0.0"
+ },
+ "dependencies": {
+ "is-buffer": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
+ "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
+ }
+ }
+ },
+ "vfile-message": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.2.tgz",
+ "integrity": "sha512-QjSNP6Yxzyycd4SVOtmKKyTsSvClqBPJcd00Z0zuPj3hOIjg0rUPG6DbFGPvUKRgYyaIWLPKpuEclcuvb3H8qA==",
+ "requires": {
+ "@types/unist": "^2.0.0",
+ "unist-util-stringify-position": "^3.0.0"
+ }
+ },
"w3c-hr-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
diff --git a/package.json b/package.json
index 1d8b34c006..158ebe98a3 100644
--- a/package.json
+++ b/package.json
@@ -47,13 +47,21 @@
"@reduxjs/toolkit": "1.6.2",
"classnames": "2.3.1",
"core-js": "3.18.3",
+ "iframe-resizer": "^4.3.2",
"js-cookie": "3.0.1",
"lodash.camelcase": "4.3.0",
+ "lodash.snakecase": "^4.1.1",
+ "mime-types": "^2.1.34",
+ "moment": "^2.29.1",
+ "moment-timezone": "^0.5.34",
"prop-types": "15.7.2",
"react": "17.0.2",
"react-break": "1.3.2",
"react-dom": "17.0.2",
+ "react-download-link": "^2.3.0",
"react-helmet": "6.1.0",
+ "react-markdown": "^8.0.0",
+ "react-moment": "^1.1.1",
"react-redux": "7.2.5",
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
@@ -73,6 +81,7 @@
"axios-mock-adapter": "1.20.0",
"codecov": "3.8.3",
"es-check": "6.0.0",
+ "eslint-plugin-jsx-a11y": "^6.5.1",
"glob": "7.2.0",
"husky": "7.0.2",
"jest": "27.2.5",
diff --git a/src/course-home/badges-tab/BadgeLeaderboardTab.jsx b/src/course-home/badges-tab/BadgeLeaderboardTab.jsx
new file mode 100644
index 0000000000..a490236b65
--- /dev/null
+++ b/src/course-home/badges-tab/BadgeLeaderboardTab.jsx
@@ -0,0 +1,49 @@
+import React from 'react';
+// import PropTypes from 'prop-types';
+import { useSelector } from 'react-redux';
+import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
+// import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { useModel } from '../../generic/model-store';
+
+import { BadgeTabsNavigation } from './badge-header';
+
+// {
+// intl
+// }
+function BadgeLeaderboardTab() {
+ const {
+ courseId,
+ } = useSelector(state => state.courseHome);
+
+ const { administrator, username } = getAuthenticatedUser();
+
+ const {
+ enrollmentMode,
+ } = useModel('courses', courseId);
+
+ const activeTabSlug = 'leaderboard';
+
+ return (
+ <>
+
+
+
+
+
+ the user is {username} and they are enrolled as an {enrollmentMode} learner
+ {administrator
+ &&
}
+
+
+
+
+ >
+ );
+}
+
+// BadgeLeaderboardTab.propTypes = {
+// intl: intlShape.isRequired,
+// };
+
+// export default injectIntl(BadgeLeaderboardTab);
+export default BadgeLeaderboardTab;
diff --git a/src/course-home/badges-tab/BadgeProgressTab.jsx b/src/course-home/badges-tab/BadgeProgressTab.jsx
new file mode 100644
index 0000000000..ebba28bacb
--- /dev/null
+++ b/src/course-home/badges-tab/BadgeProgressTab.jsx
@@ -0,0 +1,142 @@
+import React, { useEffect, useState } from 'react';
+// import PropTypes from 'prop-types';
+import snakeCase from 'lodash.snakecase';
+import { useSelector } from 'react-redux';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { StatusAlert } from '@edx/paragon';
+import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
+
+import { useModel } from '../../generic/model-store';
+
+import { BadgeTabsNavigation } from './badge-header';
+import { BadgeProgressBanner, BadgeProgressCard, BadgeProgressCourseList } from './badge-progress';
+
+import { headingMapper } from './utils';
+
+function BadgeProgressTab({ intl }) { // eslint-disable-line no-unused-vars
+ const activeTabSlug = 'progress';
+
+ const {
+ courseId,
+ } = useSelector(state => state.courseHome);
+
+ const {
+ administrator,
+ username,
+ roles, // eslint-disable-line no-unused-vars
+ } = getAuthenticatedUser();
+
+ const hasInstructorStaffRights = () => administrator;
+
+ const [progress, setProgress] = useState([]);
+
+ const {
+ id,
+ ...badgeProgressState
+ } = useModel('badge-progress', courseId);
+
+ const hasBadgeProgress = () => progress && progress.length > 0;
+ useEffect(() => {
+ let classProgressExists = 0;
+ const badgeProgress = Object.values(badgeProgressState);
+ if (hasInstructorStaffRights()) {
+ badgeProgress.forEach(student => {
+ if (student.progress.length) {
+ classProgressExists += 1;
+ }
+ });
+ if (classProgressExists) {
+ setProgress(badgeProgress);
+ }
+ } else {
+ setProgress(badgeProgress);
+ }
+ }, [courseId, administrator]);
+
+ const renderBadgeProgress = () => {
+ const defaultAssignmentFilter = 'All';
+
+ if (hasInstructorStaffRights()) {
+ return (
+ <>
+
+
+
+ >
+ );
+ }
+
+ /*
+
+
+ the user is {username} and they are enrolled as an {enrollmentMode} learner
+ {administrator
+ &&
}
+
+
+ */
+ return (
+ <>
+
+
+
+
+
+
+ {progress && (
+
+ {progress.map(learnerProgress => {
+ const itemKey = snakeCase(`card ${learnerProgress.blockDisplayName} ${username}`);
+ return (
+
+ );
+ })}
+
+ )}
+
+
+
+
+ >
+ );
+ };
+
+ const renderNoBadgeProgress = () => (
+
+
+ There is no course badge progress to show.
+ >
+ )}
+ alertType="info"
+ dismissible={false}
+ open
+ />
+ );
+
+ return (
+ <>
+ {hasBadgeProgress() && (
+ renderBadgeProgress()
+ )}
+ {!hasBadgeProgress() && (
+ renderNoBadgeProgress()
+ )}
+ >
+ );
+}
+
+BadgeProgressTab.propTypes = {
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(BadgeProgressTab);
diff --git a/src/course-home/badges-tab/BadgeProgressTab.test.jsx b/src/course-home/badges-tab/BadgeProgressTab.test.jsx
new file mode 100644
index 0000000000..540a685bfc
--- /dev/null
+++ b/src/course-home/badges-tab/BadgeProgressTab.test.jsx
@@ -0,0 +1,83 @@
+/* eslint-disable */
+// TODO Need to complete these tests.
+
+// import React from 'react';
+// import { Route } from 'react-router';
+// import MockAdapter from 'axios-mock-adapter';
+// import { Factory } from 'rosie';
+// import { getConfig, history } from '@edx/frontend-platform';
+// import { sendTrackEvent } from '@edx/frontend-platform/analytics';
+// import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+// import { AppProvider } from '@edx/frontend-platform/react';
+// import { waitForElementToBeRemoved } from '@testing-library/dom';
+// import { render, screen, within } from '@testing-library/react'; // eslint-disable-line no-unused-vars
+// import userEvent from '@testing-library/user-event';
+
+// import BadgeProgressTab from './BadgeProgressTab';
+// import { fetchBadgeProgressTab } from '../data';
+import { fireEvent, initializeMockApp, waitFor } from '../../setupTest'; // eslint-disable-line no-unused-vars
+import initializeStore from '../../store';
+// import { TabContainer } from '../../tab-page';
+// import { appendBrowserTimezoneToUrl } from '../../utils';
+// import { UserMessagesProvider } from '../../generic/user-messages';
+
+initializeMockApp();
+jest.mock('@edx/frontend-platform/analytics');
+
+describe('BadgeProgressTab', () => {
+ let axiosMock;
+ let component;
+
+ const store = initializeStore();
+
+ // beforeEach(() => {
+ // axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+ // store = initializeStore();
+ // // component = (
+ // //
+ // //
+ // //
+ // //
+ // //
+ // //
+ // //
+ // //
+ // //
+ // // );
+ // });
+
+ // const badgeProgressTabData = Factory.build('badgeProgressTabData');
+ // let courseMetadata = Factory.build('courseHomeMetadata', { user_timezone: 'America/New_York' });
+ // const { id: courseId } = courseMetadata;
+
+ // const badgeProgressUrl = `${getConfig().LMS_BASE_URL}/api/badges/v1/progress/${courseId}`;
+ // let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`;
+ // courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
+
+ // function setMetadata(attributes, options) { // eslint-disable-line no-unused-vars
+ // courseMetadata = Factory.build('courseHomeMetadata', { id: courseId, ...attributes }, options);
+ // axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
+ // }
+
+ it('Todo: Need Test', () => {
+
+ });
+
+ // describe('when receiving a full set of dates data', () => {
+ // beforeEach(() => {
+ // axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
+ // axiosMock.onGet(badgeProgressUrl).reply(200, badgeProgressTabData);
+ // history.push(`/course/${courseId}/badges/progress`); // so tab can pull course id from url
+
+ // render(component);
+ // });
+
+ // it('handles unreleased & complete', async () => {
+ // // const { header } = await getDay('Sun, May 3, 2020');
+ // // const badges = within(header).getAllByTestId('dates-badge');
+ // // expect(badges).toHaveLength(2);
+ // // expect(badges[0]).toHaveTextContent('Completed');
+ // // expect(badges[1]).toHaveTextContent('Not yet released');
+ // });
+ // });
+});
diff --git a/src/course-home/badges-tab/assets/logo-badgr-black.svg b/src/course-home/badges-tab/assets/logo-badgr-black.svg
new file mode 100644
index 0000000000..ec6300017a
--- /dev/null
+++ b/src/course-home/badges-tab/assets/logo-badgr-black.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/course-home/badges-tab/assets/logo-badgr-white.svg b/src/course-home/badges-tab/assets/logo-badgr-white.svg
new file mode 100644
index 0000000000..0bca86431b
--- /dev/null
+++ b/src/course-home/badges-tab/assets/logo-badgr-white.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx b/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx
new file mode 100644
index 0000000000..8a861a9867
--- /dev/null
+++ b/src/course-home/badges-tab/badge-header/BadgeTabsNavigation.jsx
@@ -0,0 +1,76 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import classNames from 'classnames';
+// import { getConfig } from '@edx/frontend-platform';
+
+import messages from './messages';
+import Tabs from '../../../generic/tabs/Tabs';
+import LinkedLogo from '../logos'; // eslint-disable-line no-unused-vars
+import logo from '../assets/logo-badgr-black.svg'; // eslint-disable-line no-unused-vars
+
+function BadgeTabsNavigation({
+ activeTabSlug, className, intl,
+}) {
+ const tabs = [
+ {
+ title: 'Progress',
+ priority: 1,
+ slug: 'progress',
+ type: 'progress',
+ url: './progress',
+ disabled: false,
+ },
+ {
+ title: 'Leaderboard',
+ priority: 2,
+ slug: 'leaderboard',
+ type: 'leaderboard',
+ url: './leaderboard',
+ disabled: true,
+ },
+ ];
+ // flex-shrink-0
+ return (
+
+
+
+ {/* */}
+ {tabs.map(({
+ url, title, slug, disabled,
+ }) => (
+
+ {title}
+
+ ))}
+
+
+
+ );
+}
+
+BadgeTabsNavigation.propTypes = {
+ activeTabSlug: PropTypes.string,
+ className: PropTypes.string,
+ intl: intlShape.isRequired,
+};
+
+BadgeTabsNavigation.defaultProps = {
+ activeTabSlug: undefined,
+ className: null,
+};
+
+export default injectIntl(BadgeTabsNavigation);
diff --git a/src/course-home/badges-tab/badge-header/index.js b/src/course-home/badges-tab/badge-header/index.js
new file mode 100644
index 0000000000..d3f32f24a1
--- /dev/null
+++ b/src/course-home/badges-tab/badge-header/index.js
@@ -0,0 +1,2 @@
+/* eslint-disable import/prefer-default-export */
+export { default as BadgeTabsNavigation } from './BadgeTabsNavigation';
diff --git a/src/course-home/badges-tab/badge-header/messages.js b/src/course-home/badges-tab/badge-header/messages.js
new file mode 100644
index 0000000000..e2ae0bf3ba
--- /dev/null
+++ b/src/course-home/badges-tab/badge-header/messages.js
@@ -0,0 +1,11 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ 'learn.navigation.badge.tabs.label': {
+ id: 'learn.navigation.badge.tabs.label',
+ defaultMessage: 'Badge Material',
+ description: 'The accessible label for badge tabs navigation',
+ },
+});
+
+export default messages;
diff --git a/src/course-home/badges-tab/badge-progress/BadgeProgressTab.scss b/src/course-home/badges-tab/badge-progress/BadgeProgressTab.scss
new file mode 100644
index 0000000000..0537e25ea0
--- /dev/null
+++ b/src/course-home/badges-tab/badge-progress/BadgeProgressTab.scss
@@ -0,0 +1,62 @@
+
+.badge-tabs-navigation {
+
+ border-bottom: solid 3px theme-color('dark', 400);
+ background-color: theme-color('gray', 200);
+
+ .badge-nav-tabs {
+
+ margin: 0 20px 0 20px;
+
+ .dropdown-menu {
+ .nav-link {
+ display: block;
+ }
+ }
+ .nav-link {
+
+ display: inline-block;
+
+ &.active {
+ background-color: theme-color('brand', 200);
+ }
+
+ &.disabled {
+ &:hover,
+ &:focus,
+ &.active {
+ background-color: none !important;
+ color: theme-color('light', 400);
+ }
+ }
+
+ &:hover,
+ &:focus {
+ background-color: theme-color('brand', 400);
+ color: white;
+ }
+
+ }
+
+ }
+
+ .logo {
+ display: block;
+ box-sizing: content-box;
+ position: relative;
+ top: .10em;
+ height: 1.75rem;
+ margin-right: 1rem;
+ margin-top: 0.5rem;
+ img {
+ display: block;
+ height: 100%;
+ }
+ }
+
+}
+
+// Import badge-progress-specific sass files
+@import './banner/index.scss';
+@import './card/index.scss';
+@import './course-list/index.scss';
diff --git a/src/course-home/badges-tab/badge-progress/banner/BadgeProgressBanner.jsx b/src/course-home/badges-tab/badge-progress/banner/BadgeProgressBanner.jsx
new file mode 100644
index 0000000000..ceb0154f76
--- /dev/null
+++ b/src/course-home/badges-tab/badge-progress/banner/BadgeProgressBanner.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+const BadgeProgressBanner = ({ hasProgress, hasRights }) => {
+ const indicatorProgress = (hasProgress ? '' : 'no-progress');
+
+ const getPathTitle = () => (hasRights ? 'Class Progress' : 'My Learning Path');
+
+ const getPathSubHeading = () => (
+ hasRights
+ ? "Here is all learner progress through the badges available for this course. Click each badge to learn more about an individual student's credential."
+ : "Here is your progress through the badges available for this course. Click each badge to learn more or to save and share badges you've earned."
+ );
+
+ // d-flex justify-content-left
+ return (
+
+ { hasProgress && (
+ <>
+
+
{getPathTitle()}
+
+
+
+ {getPathSubHeading()}
+
+
+ >
+ )}
+
+ { !hasProgress && (
+
This course either has no learner progress or is not setup for badging.
+ )}
+
+ );
+};
+
+BadgeProgressBanner.propTypes = {
+ hasProgress: PropTypes.bool.isRequired,
+ hasRights: PropTypes.bool.isRequired,
+};
+
+export default BadgeProgressBanner;
diff --git a/src/course-home/badges-tab/badge-progress/banner/index.scss b/src/course-home/badges-tab/badge-progress/banner/index.scss
new file mode 100644
index 0000000000..26ed245a3a
--- /dev/null
+++ b/src/course-home/badges-tab/badge-progress/banner/index.scss
@@ -0,0 +1,25 @@
+
+/* BadgeProgressBanner
+ --------------------------------------- */
+
+.learningpath-empty {
+
+ .learningpath-empty-header {
+
+ span {
+ margin-right: 10px;
+ }
+
+ }
+ }
+
+ .learningpath {
+ background: #f2f2f2;
+
+ h2 {
+ font-weight: lighter;
+ font-size: 30px;
+ }
+
+ }
+
\ No newline at end of file
diff --git a/src/course-home/badges-tab/badge-progress/card/BadgeProgressCard.jsx b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCard.jsx
new file mode 100644
index 0000000000..a7f2460a00
--- /dev/null
+++ b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCard.jsx
@@ -0,0 +1,118 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import { isEmptyObject } from '../../../../utils/empty';
+import BadgeProgressCardStatus from './BadgeProgressCardStatus';
+import BadgeProgressCardDetailsModal from './BadgeProgressCardDetailsModal';
+
+const BadgeProgressCard = (props) => {
+ const { data, minimal } = props;
+
+ const isProgressComplete = () => {
+ if (isEmptyObject(data.assertion)) {
+ return false;
+ }
+ return data.assertion.imageUrl.length > 0;
+ };
+
+ const getBadgeProgressCardDetails = (earned) => (
+ <>
+
+ >
+ );
+
+ const getBadgeImage = () => {
+ const { assertionUrl } = data.assertion;
+
+ return (
+ <>
+ {assertionUrl && (
+
+ )}
+ {!assertionUrl && (
+
+ )}
+ >
+ );
+ };
+
+ return (
+ <>
+ {minimal && (
+
+
+ {getBadgeImage('minimal')}
+
+
+ )}
+ {!minimal && (
+
+
+
+
+
+
+ {getBadgeImage()}
+
+
+
{data.badgeClass.displayName}
+
+
+
+
+ )}
+ >
+ );
+};
+
+BadgeProgressCard.defaultProps = {
+ minimal: '',
+};
+
+BadgeProgressCard.propTypes = {
+ data: PropTypes.shape({
+ courseId: PropTypes.string,
+ blockId: PropTypes.string,
+ blockDisplayName: PropTypes.string,
+ blockOrder: PropTypes.number,
+ eventType: PropTypes.string,
+ badgeClass: PropTypes.shape({
+ slug: PropTypes.string,
+ issuingComponent: PropTypes.string,
+ displayName: PropTypes.string,
+ courseId: PropTypes.string,
+ description: PropTypes.string,
+ criteria: PropTypes.string,
+ image: PropTypes.string,
+ }),
+ assertion: PropTypes.shape({
+ issuedOn: PropTypes.string,
+ entityId: PropTypes.string,
+ expires: PropTypes.string,
+ revoked: PropTypes.bool,
+ imageUrl: PropTypes.string,
+ assertionUrl: PropTypes.string,
+ recipient: PropTypes.shape({
+ plaintextIdentity: PropTypes.string,
+ }),
+ issuer: PropTypes.shape({
+ entityType: PropTypes.string,
+ entityId: PropTypes.string,
+ openBadgeId: PropTypes.string,
+ name: PropTypes.string,
+ image: PropTypes.string,
+ email: PropTypes.string,
+ description: PropTypes.string,
+ url: PropTypes.string,
+ }),
+ }),
+ }).isRequired,
+ minimal: PropTypes.string,
+};
+
+export default BadgeProgressCard;
diff --git a/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardDetailsModal.jsx b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardDetailsModal.jsx
new file mode 100644
index 0000000000..e0906a56a5
--- /dev/null
+++ b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardDetailsModal.jsx
@@ -0,0 +1,224 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import ReactMarkdown from 'react-markdown';
+import {
+ ActionRow,
+ Button,
+ Collapsible,
+ Hyperlink,
+ ModalDialog,
+ useToggle,
+} from '@edx/paragon';
+
+import LinkedLogo from '../../logos';
+import logo from '../../assets/logo-badgr-white.svg';
+
+const BadgeProgressCardDetailsModal = (props) => {
+ const {
+ progress,
+ minimal,
+ badgeProgressCardStatus,
+ } = props;
+ // const [modalOpen, setModalOpen] = useState(false);
+
+ const [isOpen, open, close] = useToggle(false);
+ const [modalSize] = useState('xl');
+ const [modalVariant] = useState('dark');
+
+ const getBadgrLogo = () => (
+
+ );
+
+ const redirectBackpack = () => window.open('https://badgr.com/recipient/badges', '_blank');
+
+ const renderModal = () => (
+ /* Todo: May consider going back to `src = progress.assertion.imageUrl` to
+ reflect actual image earned. I was not able to render
+ "http://example.com/image.png" because it produces a 404 error.
+ */
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{progress.badgeClass.displayName}
+
+
+
+ {progress.badgeClass.description}
+
+
+
+
+
+
+
+
+
+
+
+
+ View Backpack
+
+
+
+ {progress.assertion.issuedOn && progress.blockDisplayName && (
+
+ {badgeProgressCardStatus}
+
+ )}
+ {progress.assertion.recipient.plaintextIdentity && (
+
+
Recipient
+
{progress.assertion.recipient.plaintextIdentity}
+
+ )}
+ {progress.badgeClass.criteria && (
+
+
Criteria
+ {progress.badgeClass.criteria}
+
+ )}
+ {progress.assertion.issuer && (
+
+
Issuer
+
+
+ {progress.assertion.issuer.name}
+ {progress.assertion.issuer.email}
+
+
+
+ )}
+
+
+
+
+
+
+
Share your Open Badge with Badgr
+
+ Your achievement has been recognized with an Open Badge , a digital image file with information
+ embedded in it that uniquely identifies your accomplishments.
+
+
+ Badgr is a service that creates and stores Open Badges and lets you share them with others.
+ To share your badge using Badgr, you can send a link to a web page about your badge to others.
+ You can also send the badge image file directly to others, and they can use a badge verification service from Badgr to confirm your accomplishment.
+ For more options, you must first have a Badgr account.
+ You should have received an email the first time you received a badge with
+ instructions about creating a Badgr account. Once you have a Badgr account, you can organize
+ your badges in a Backpack and access tools to help share your badges on social media, embed
+ them in web pages, and more.
+
+
+
+
+
+
+
+ Create a Badgr account, or log in to your existing account;
+ Share this public URL to your badge ; or
+
+ Download your badge (right-click or option-click, save as) and share it
+ directly with others. They can verify it's really yours at badgecheck.io .
+
+
+
+
+ {getBadgrLogo()}
+
+
+
+
+
+
+
+
+
+ Close
+
+
+
+
+
+ >
+ );
+
+ return renderModal();
+};
+
+BadgeProgressCardDetailsModal.propTypes = {
+ progress: PropTypes.shape({
+ courseId: PropTypes.string,
+ blockId: PropTypes.string,
+ blockDisplayName: PropTypes.string,
+ blockOrder: PropTypes.number,
+ eventType: PropTypes.string,
+ badgeClass: PropTypes.shape({
+ slug: PropTypes.string,
+ issuingComponent: PropTypes.string,
+ displayName: PropTypes.string,
+ courseId: PropTypes.string,
+ description: PropTypes.string,
+ criteria: PropTypes.string,
+ image: PropTypes.string,
+ }),
+ assertion: PropTypes.shape({
+ issuedOn: PropTypes.string,
+ entityId: PropTypes.string,
+ expires: PropTypes.string,
+ revoked: PropTypes.bool,
+ imageUrl: PropTypes.string,
+ assertionUrl: PropTypes.string,
+ recipient: PropTypes.shape({
+ plaintextIdentity: PropTypes.string,
+ }),
+ issuer: PropTypes.shape({
+ entityType: PropTypes.string,
+ entityId: PropTypes.string,
+ openBadgeId: PropTypes.string,
+ name: PropTypes.string,
+ image: PropTypes.string,
+ email: PropTypes.string,
+ description: PropTypes.string,
+ url: PropTypes.string,
+ }),
+ }),
+ }).isRequired,
+ minimal: PropTypes.string,
+ badgeProgressCardStatus: PropTypes.oneOfType([PropTypes.object]).isRequired,
+};
+
+BadgeProgressCardDetailsModal.defaultProps = {
+ minimal: '',
+};
+
+export default BadgeProgressCardDetailsModal;
diff --git a/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardStatus.jsx b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardStatus.jsx
new file mode 100644
index 0000000000..844486e394
--- /dev/null
+++ b/src/course-home/badges-tab/badge-progress/card/BadgeProgressCardStatus.jsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Moment from 'react-moment';
+import classNames from 'classnames';
+
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faCheckCircle } from '@fortawesome/free-solid-svg-icons';
+import { faCircle } from '@fortawesome/free-regular-svg-icons';
+
+const BadgeProgressCardStatus = (props) => {
+ const { status, title, earned } = props;
+
+ const getStatusEarned = () => {
+ // Set the output format for every react-moment instance.
+ Moment.globalFormat = 'MMMM D, YYYY';
+
+ // Set the timezone for every instance.
+ // Moment.globalTimezone = 'America/Los_Angeles';
+
+ // Set the output timezone for local for every instance.
+ Moment.globalLocal = true;
+
+ // Use a tag for every react-moment instance.
+ Moment.globalElement = 'span';
+
+ return (
+ <>
+ {earned && (
+
+ Earned: {earned}
+
+ )}
+ >
+ );
+ };
+
+ const getStatusIndicator = () => {
+ const indicatorIcon = (status ? faCheckCircle : faCircle);
+ const indicatorStatus = (status ? 'complete' : 'incomplete');
+ return (
+
+ );
+ };
+
+ const getStatusTitle = () => {
+ const stripNumPrefix = title.replace(/[0-9]+\./g, '');
+ return (
+
+ {stripNumPrefix}
+
+ );
+ };
+
+ return (
+ <>
+ {!earned && (
+
+ {getStatusIndicator()}
+ {getStatusTitle()}
+
+ )}
+ {earned && (
+
+
+
+ {getStatusIndicator()}
+ {getStatusTitle()}
+ {getStatusEarned()}
+
+
+
+ )}
+ >
+ );
+};
+
+BadgeProgressCardStatus.defaultProps = {
+ earned: '',
+};
+
+BadgeProgressCardStatus.propTypes = {
+ status: PropTypes.bool.isRequired,
+ title: PropTypes.string.isRequired,
+ earned: PropTypes.string,
+};
+
+export default BadgeProgressCardStatus;
diff --git a/src/course-home/badges-tab/badge-progress/card/index.scss b/src/course-home/badges-tab/badge-progress/card/index.scss
new file mode 100644
index 0000000000..023f3d6736
--- /dev/null
+++ b/src/course-home/badges-tab/badge-progress/card/index.scss
@@ -0,0 +1,435 @@
+// https://stackoverflow.com/questions/11989546/wrap-a-text-within-only-two-lines-inside-div
+@mixin multiLineEllipsis($lineHeight: 1.2rem, $lineCount: 2, $bgColor: white, $padding-right: 0.3125rem, $width: 1rem, $ellipsis-right: 0) {
+ overflow: hidden; /* hide text if it is more than $lineCount lines */
+ position: relative; /* for set '...' in absolute position */
+ line-height: $lineHeight; /* use this value to count block height */
+ max-height: $lineHeight * $lineCount; /* max-height = line-height * lines max number */
+ padding-right: $padding-right; /* place for '...' */
+ white-space: normal; /* overwrite any white-space styles */
+ // word-break: break-all; /* will break each letter in word */
+ text-overflow: ellipsis; /* show ellipsis if text is broken */
+
+ &::before {
+ content: '...'; /* create the '...'' points in the end */
+ position: absolute;
+ right: $ellipsis-right;
+ bottom: 0;
+ }
+
+ &::after {
+ content: ''; /* hide '...'' if we have text, which is less than or equal to max lines and add $bgColor */
+ position: absolute;
+ right: 0;
+ width: $width;
+ height: 1rem * $lineCount;
+ margin-top: 0.2rem;
+ background: $bgColor; /* because we are cutting off the diff we need to add the color back. */
+ }
+}
+
+/* BadgeProgressCard
+ --------------------------------------- */
+.card {
+ //border-radius: 1rem;
+ border: none;
+
+ .card-header {
+ padding: 0.5rem 0.5rem;
+ background-color: transparent;
+ border-radius: 5px;
+ box-shadow: 1px 1px 5px 0 rgba(46,61,73,.2);
+ font-weight: bold;
+ height: 85px; /* 120px */
+ overflow: hidden;
+ line-height: 1.2;
+ }
+
+ .card-badge {
+
+ border-radius: 5px;
+ box-shadow: 1px 1px 5px 0 rgba(46,61,73,.2);
+ height: 100%;
+
+ .card-img-top {
+ width: 100%;
+ height: 12vw;
+ object-fit: contain;
+ margin: 5px 0px;
+
+ @media only screen and (min-width: 320px) {
+ height: 50vw;
+ }
+ @media only screen and (min-width: 768px) {
+ height: 16vw;
+ }
+ @media only screen and (min-width: 1024px) {
+ height: 12vw;
+ }
+ @media only screen and (min-width: 1200px) {
+ height: 10vw;
+ }
+ @media only screen and (min-width: 1400px) {
+ height: 8vw;
+ }
+ @media only screen and (min-width: 2560px) {
+ height: 5vw;
+ }
+ }
+
+ .minimal {
+ height: 70px;
+ }
+
+ .card-body {
+ padding: .75rem 1.25rem;
+ border-top: 1px solid #8080805c;
+ // height: 40px;
+ text-align: center;
+ background-color: rgba(237,237,237,1);
+
+ .card-title {
+ font-size: 0.90rem;
+ // text-overflow: ellipsis;
+ // overflow: hidden;
+ // white-space: nowrap;
+ @include multiLineEllipsis($lineCount: 2, $bgColor: rgba(237,237,237,1));
+ padding: 0px 10px !important;
+ min-height: 40px;
+ }
+
+ }
+
+ }
+
+ }
+
+ .asserted {
+ color: rgba(0,0,0,1);
+
+ &:hover {
+ background-color: rgba(13,129,1,0.2);
+ }
+ }
+
+ .not-asserted {
+ /* https://stackoverflow.com/questions/35374021/css-grayscale-filter-changing-logos-to-different-shades */
+
+ //color: rgba(132, 132, 132, 0.15);
+ -webkit-filter: grayscale(100%) brightness(60%) contrast(100%); /* Safari 6.0 - 9.0 */
+ filter: grayscale(100%) brightness(60%) contrast(100%);
+ filter: alpha(opacity=50);
+ opacity: 0.50 !important;
+ //filter: alpha(opacity=15);
+ }
+
+
+
+/* BadgeProgressCardStatus
+ --------------------------------------- */
+
+.card-status {
+ position: relative;
+ top: -5px;
+ display: flex;
+ align-items: center;
+ margin: 0px 10px;
+
+ .card-status-icon {
+ display: inline-block;
+ vertical-align: middle;
+ width: 15%;
+ margin: 16px 16px 16px 0px;
+ //position: relative;
+ //top: 5px;
+
+ &.complete {
+ color: rgb(0, 129, 0);
+ }
+
+ &.incomplete {
+ color: rgb(165, 165, 165);
+ }
+
+ }
+
+ /*
+ h3 {
+ max-width: 280px;
+ display: inline-block;
+ @include line-clamp(2,2);
+ }
+ */
+ .card-status-title {
+ font-size: 14px;
+ text-align: left;
+ display: inline-block;
+ vertical-align: middle;
+ text-transform: uppercase;
+ @include multiLineEllipsis($lineCount: 3, $padding-right: 1rem)
+ }
+
+ }
+
+
+/* BadgeProgressCardDetailsModal
+ --------------------------------------- */
+ /*
+ .modal-progress-details {
+
+ .modal {
+
+ .modal-dialog {
+
+ min-width: 60%;
+
+ .modal-content {
+
+ padding: 15px;
+
+ .modal-body {
+ */
+
+ .progress-details {
+
+ text-align: left;
+
+ .progress-details-header {
+
+ background: #48555d !important;
+ color: white !important;
+
+ .pgn__modal-hero {
+ background: inherit !important;
+ color: inherit !important;
+ }
+
+ .progress-details-title {
+
+ h2 {
+ font-size: 30px !important;
+ font-weight: bold !important;
+ }
+
+ }
+
+ .progress-details-description {
+
+ div, p {
+ font-size: 16px !important;
+ font-weight: normal !important;
+ }
+
+ }
+
+ }
+
+ .progress-details-body {
+
+ .progress-details-image {
+
+ img {
+ max-width: 70%;
+ margin: 0px 40px;
+ }
+
+ button {
+ max-width: 70%;
+ width: 100%;
+ margin: 5px 40px;
+ }
+
+ }
+
+ .progress-details-meta {
+
+ font-size: 16px;
+ line-height: 8px;
+
+ .progress-details-meta-earned,
+ .progress-details-meta-recipient,
+ .progress-details-meta-criteria,
+ .progress-details-meta-issuer {
+
+ h3 {
+ color: #767676 !important;
+ font-size: 18px;
+ }
+
+ margin-bottom: 20px;
+
+ }
+
+ .progress-details-meta-earned {
+
+ .card-status {
+ position: relative;
+ top: -5px;
+ display: flex;
+ align-items: center;
+ margin: 0px 10px;
+
+ .card-status-icon {
+ display: inline-block;
+ vertical-align: middle;
+ width: 15%;
+ margin: 16px 16px 16px 0px;
+ //position: relative;
+ //top: 5px;
+
+ &.complete {
+ color: rgb(0, 129, 0);
+ }
+
+ &.incomplete {
+ color: rgb(165, 165, 165);
+ }
+
+ }
+
+ /*
+ h3 {
+ max-width: 280px;
+ display: inline-block;
+ @include line-clamp(2,2);
+ }
+ */
+ .card-status-title {
+ position: relative;
+ top: -8px;
+ font-size: 14px;
+ text-align: left;
+ display: inline-block;
+ vertical-align: middle;
+ text-transform: uppercase;
+ // @include multiLineEllipsis($lineCount: 3, $padding-right: 1rem)
+ }
+
+ .card-status-earned {
+ display: block;
+ position: relative;
+ top: 12px;
+ left: -90px;
+ font-size: 14px;
+ font-weight: normal;
+ font-style: italic;
+ }
+
+ }
+
+ }
+
+ .progress-details-meta-issuer {
+
+ ul {
+
+ li {
+ list-style: none;
+
+ img {
+ width: 40px;
+ height: 40px;
+ }
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ .progress-details-share {
+
+ background-color: #f2f2f2;
+
+ a {
+ font-weight: bold;
+ }
+
+ .collapsible-trigger {
+ background-color: bisque;
+ }
+
+ .progress-details-share-instructions {
+
+ padding-bottom: 0px !important;
+
+ .share-introduction {
+
+ hr {
+ border-color: rgba(0,0,0,0.30);
+ }
+
+ }
+
+ }
+
+ .progress-details-share-badgr-instructions {
+
+ padding: 0px 10px 10px 10px !important;
+
+ .badgr-instructions {
+
+ ol {
+ padding-left: 20px;
+ list-style: none;
+ list-style-position: outside;
+ display: table;
+ margin: 1em 0;
+ margin-block-start: 1em;
+ margin-block-end: 1em;
+ margin-inline-start: 0px;
+ margin-inline-end: 0px;
+ padding-inline-start: 40px;
+ /*counter-reset: badgr-instr-counter;*/
+
+ li {
+ //list-style: none;
+ list-style-type: decimal;
+ //counter-increment: badgr-instr-counter;
+
+ /*
+ &:before {
+ content: counter(badgr-instr-counter) ". ";
+ color: #444444;
+ font-weight: bold;
+ }*/
+ }
+
+ }
+
+ }
+
+ .badgr-image {
+
+ display: inline-block;
+ background-color: #525dc7;
+ padding: 10px;
+ max-height: 120px;
+
+ img {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ left: 18px;
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+/*
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+*/
\ No newline at end of file
diff --git a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx
new file mode 100644
index 0000000000..3259fd3590
--- /dev/null
+++ b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseList.jsx
@@ -0,0 +1,109 @@
+/* eslint-disable no-console */
+import React from 'react';
+import PropTypes from 'prop-types';
+import snakeCase from 'lodash.snakecase';
+import { DataTable, TextFilter } from '@edx/paragon';
+
+import BadgeProgressCard from '../card/BadgeProgressCard';
+import BadgeProgressCourseListTable from './BadgeProgressCourseListTable';
+
+const BadgeProgressCourseList = (props) => {
+ const { data, headings } = props;
+
+ const getProgressCourseListData = () => {
+ const results = [];
+
+ data.forEach((item) => {
+ const itemUserName = item.userName;
+ const learnerData = {
+ username: `'${item.userName}' (${item.email})`,
+ };
+
+ item.progress.forEach((i) => {
+ const itemKey = snakeCase(`card ${i.blockDisplayName} ${itemUserName}`);
+ learnerData[snakeCase(i.blockDisplayName)] = (
+
+ );
+ });
+
+ results.push(learnerData);
+ });
+
+ return results;
+ };
+
+ const getLearnerCount = () => {
+ const results = [];
+ data.forEach((item) => results.push(item.userName));
+ return results.length;
+ };
+
+ return (
+ <>
+ console.log(`This function will be called with the value: ${JSON.stringify(currentState)}}`)}
+ data={getProgressCourseListData()}
+ columns={headings}
+ >
+
+
+
+
+ >
+ );
+};
+
+BadgeProgressCourseList.propTypes = {
+ data: PropTypes.arrayOf(PropTypes.shape({
+ courseId: PropTypes.string,
+ blockId: PropTypes.string,
+ blockDisplayName: PropTypes.string,
+ blockOrder: PropTypes.number,
+ eventType: PropTypes.string,
+ badgeClass: PropTypes.shape({
+ slug: PropTypes.string,
+ issuingComponent: PropTypes.string,
+ displayName: PropTypes.string,
+ courseId: PropTypes.string,
+ description: PropTypes.string,
+ criteria: PropTypes.string,
+ image: PropTypes.string,
+ }),
+ assertion: PropTypes.shape({
+ issuedOn: PropTypes.string,
+ entityId: PropTypes.string,
+ expires: PropTypes.string,
+ revoked: PropTypes.bool,
+ imageUrl: PropTypes.string,
+ assertionUrl: PropTypes.string,
+ recipient: PropTypes.shape({
+ plaintextIdentity: PropTypes.string,
+ }),
+ issuer: PropTypes.shape({
+ entityType: PropTypes.string,
+ entityId: PropTypes.string,
+ openBadgeId: PropTypes.string,
+ name: PropTypes.string,
+ image: PropTypes.string,
+ email: PropTypes.string,
+ description: PropTypes.string,
+ url: PropTypes.string,
+ }),
+ }),
+ })).isRequired,
+ headings: PropTypes.arrayOf(PropTypes.shape({
+ label: PropTypes.string,
+ key: PropTypes.string,
+ })).isRequired,
+};
+
+export default BadgeProgressCourseList;
diff --git a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTable.jsx b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTable.jsx
new file mode 100644
index 0000000000..3999c8dec5
--- /dev/null
+++ b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTable.jsx
@@ -0,0 +1,61 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import { DataTableContext } from '@edx/paragon';
+
+import BadgeProgressCourseListTableHeaderRow from './BadgeProgressCourseListTableHeaderRow';
+import BadgeProgressCourseListTableRow from './BadgeProgressCourseListTableRow';
+
+const BadgeProgressCourseListTable = ({ isStriped }) => {
+ const useRows = () => {
+ const {
+ getTableProps, prepareRow, page, rows, headerGroups, getTableBodyProps,
+ } = useContext(DataTableContext);
+
+ const displayRows = page || rows;
+
+ return {
+ getTableProps, prepareRow, displayRows, headerGroups, getTableBodyProps,
+ };
+ };
+
+ const {
+ getTableProps, prepareRow, displayRows, headerGroups, getTableBodyProps,
+ } = useRows();
+
+ const renderRows = () => displayRows.map((row) => {
+ prepareRow(row);
+ return (
+
+ );
+ });
+
+ if (!getTableProps) {
+ return null;
+ }
+
+ return (
+
+ );
+};
+
+BadgeProgressCourseListTable.defaultProps = {
+ isStriped: true,
+};
+
+BadgeProgressCourseListTable.propTypes = {
+ /** should table rows be striped */
+ isStriped: PropTypes.bool,
+};
+
+export default BadgeProgressCourseListTable;
diff --git a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableCell.jsx b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableCell.jsx
new file mode 100644
index 0000000000..37b7c6c294
--- /dev/null
+++ b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableCell.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+import BadgeProgressCard from '../card/BadgeProgressCard';
+
+const BadgeProgressCourseListTableCell = (
+ {
+ getCellProps, render, column, value,
+ },
+) => (
+
+
+ {column.id === 'username' && (
+ render('Cell')
+ )}
+ {column.id !== 'username' && (
+
+ )}
+
+
+);
+
+BadgeProgressCourseListTableCell.defaultProps = {
+ value: '',
+};
+
+BadgeProgressCourseListTableCell.propTypes = {
+ /** Props for the td element */
+ getCellProps: PropTypes.func.isRequired,
+ /** Function that renders the cell contents. Will be called with the string 'Cell' */
+ render: PropTypes.func.isRequired,
+ /** Table column */
+ column: PropTypes.shape({
+ cellClassName: PropTypes.string,
+ id: PropTypes.string,
+ }).isRequired,
+ value: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.shape(),
+ ]),
+};
+
+export default BadgeProgressCourseListTableCell;
diff --git a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableHeaderRow.jsx b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableHeaderRow.jsx
new file mode 100644
index 0000000000..edced33f02
--- /dev/null
+++ b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableHeaderRow.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { TableHeaderCell } from '@edx/paragon';
+
+const BadgeProgressCourseListTableHeaderRow = ({ headerGroups }) => (
+
+ {headerGroups.map(headerGroup => (
+
+ {headerGroup.headers.map(column => (
+
+ ))}
+
+ ))}
+
+);
+
+BadgeProgressCourseListTableHeaderRow.propTypes = {
+ headerGroups: PropTypes.arrayOf(PropTypes.shape({
+ headers: PropTypes.arrayOf(PropTypes.shape({
+ /** Props for the TableHeaderCell component. Must include a key */
+ getHeaderProps: PropTypes.func.isRequired,
+ })).isRequired,
+ /** Returns props for the header tr element */
+ getHeaderGroupProps: PropTypes.func.isRequired,
+ })).isRequired,
+};
+
+export default BadgeProgressCourseListTableHeaderRow;
diff --git a/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableRow.jsx b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableRow.jsx
new file mode 100644
index 0000000000..ee0a6107df
--- /dev/null
+++ b/src/course-home/badges-tab/badge-progress/course-list/BadgeProgressCourseListTableRow.jsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import snakeCase from 'lodash.snakecase';
+
+import BadgeProgressCourseListTableCell from './BadgeProgressCourseListTableCell';
+
+/* key={`${cell.column.Header}${id}`} */
+const BadgeProgressCourseListTableRow = ({
+ getRowProps, cells, id, isSelected,
+}) => (
+
+ {cells.map(cell => )}
+
+);
+
+BadgeProgressCourseListTableRow.defaultProps = {
+ isSelected: false,
+};
+
+BadgeProgressCourseListTableRow.propTypes = {
+ /** props to include on the tr tag (must include id) */
+ getRowProps: PropTypes.func.isRequired,
+ /** cells in the row */
+ cells: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
+ /** row id */
+ id: PropTypes.string.isRequired,
+ /** indicates if row has been selected */
+ isSelected: PropTypes.bool,
+};
+
+export default BadgeProgressCourseListTableRow;
diff --git a/src/course-home/badges-tab/badge-progress/course-list/index.scss b/src/course-home/badges-tab/badge-progress/course-list/index.scss
new file mode 100644
index 0000000000..3d2c6a0810
--- /dev/null
+++ b/src/course-home/badges-tab/badge-progress/course-list/index.scss
@@ -0,0 +1,87 @@
+
+/* BadgeProgressCourseList
+ --------------------------------------- */
+.badge-progress-course-list {
+
+ thead {
+ border-bottom: solid 3px theme-color('dark', 400);
+ background-color: theme-color('gray', 200);
+ }
+
+}
+
+.thead-overflow-hidden {
+
+ thead {
+
+ th {
+ overflow: hidden visible;
+ overflow: -moz-hidden-unscrollable;
+ text-overflow: clip;
+ text-transform: uppercase;
+
+ /* Controls the max-width of the badges in table view using table-responsive as well.
+ This is in change of using 'hasFixedColumnWidths' for the table.
+ */
+
+ @media only screen and (min-width: 320px) {
+ max-width: 100px;
+ }
+ @media only screen and (min-width: 768px) {
+ max-width: 100px;
+ }
+ @media only screen and (min-width: 1024px) {
+ max-width: 100px;
+ }
+ @media only screen and (min-width: 1200px) {
+ max-width: 140px;
+ }
+ @media only screen and (min-width: 1400px) {
+ max-width: 140px;
+ }
+ @media only screen and (min-width: 2560px) {
+ max-width: 140px;
+ }
+ }
+
+ }
+
+}
+
+/* BadgeProgressCourseListTable
+ --------------------------------------- */
+.progress-list-item {
+
+ .badge-name {
+
+ min-width: 400px;
+
+ img {
+ max-height: 50px;
+ max-width: 50px;
+ }
+
+ span {
+ display: inline-block;
+ margin-left: 20px;
+ }
+ }
+
+}
+
+.asserted {
+ color: rgba(0,0,0,1);
+}
+
+.not-asserted {
+ color: rgba(0,0,0,0.25);
+
+ .badge-name {
+
+ img {
+ opacity: 0.25;
+ filter: alpha(opacity=25);
+ }
+ }
+}
+
diff --git a/src/course-home/badges-tab/badge-progress/index.js b/src/course-home/badges-tab/badge-progress/index.js
new file mode 100644
index 0000000000..6ddbba0298
--- /dev/null
+++ b/src/course-home/badges-tab/badge-progress/index.js
@@ -0,0 +1,5 @@
+/* eslint-disable import/prefer-default-export */
+export { default as BadgeProgressBanner } from './banner/BadgeProgressBanner';
+export { default as BadgeProgressCard } from './card/BadgeProgressCard';
+export { default as BadgeProgressCourseList } from './course-list/BadgeProgressCourseList';
+export { default as BadgeProgressCourseListTable } from './course-list/BadgeProgressCourseListTable';
diff --git a/src/course-home/badges-tab/index.js b/src/course-home/badges-tab/index.js
new file mode 100644
index 0000000000..9e82bd06bc
--- /dev/null
+++ b/src/course-home/badges-tab/index.js
@@ -0,0 +1,2 @@
+export { default as BadgeProgressTab } from './BadgeProgressTab';
+export { default as BadgeLeaderboardTab } from './BadgeLeaderboardTab';
diff --git a/src/course-home/badges-tab/logos.jsx b/src/course-home/badges-tab/logos.jsx
new file mode 100644
index 0000000000..9605f70270
--- /dev/null
+++ b/src/course-home/badges-tab/logos.jsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+function LinkedLogo({
+ href,
+ src,
+ alt,
+ ...attributes
+}) {
+ return (
+
+
+
+ );
+}
+
+LinkedLogo.propTypes = {
+ href: PropTypes.string.isRequired,
+ src: PropTypes.string.isRequired,
+ alt: PropTypes.string.isRequired,
+};
+
+export default LinkedLogo;
diff --git a/src/course-home/badges-tab/messages.js b/src/course-home/badges-tab/messages.js
new file mode 100644
index 0000000000..25bdbcdc81
--- /dev/null
+++ b/src/course-home/badges-tab/messages.js
@@ -0,0 +1,16 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messagesBadgeProgress = defineMessages({
+ failure: {
+ id: 'badge.progress.loading.failure',
+ defaultMessage: 'There was an error loading the course badge progress.',
+ description: 'Message when course badge progress page fails to load',
+ },
+ loading: {
+ id: 'badge.progress.loading',
+ defaultMessage: 'Loading course badge progress page...',
+ description: 'Message when course badge progress page is being loaded',
+ },
+});
+
+export default messagesBadgeProgress;
diff --git a/src/course-home/badges-tab/utils.js b/src/course-home/badges-tab/utils.js
new file mode 100644
index 0000000000..fb37713a89
--- /dev/null
+++ b/src/course-home/badges-tab/utils.js
@@ -0,0 +1,83 @@
+/* eslint-disable import/prefer-default-export */
+
+import snakeCase from 'lodash.snakecase';
+import { DropdownFilter } from '@edx/paragon';
+import { isEmptyObject } from '../../utils/empty';
+
+// Defines a custom filter filter function for finding badge assertion
+const filterContainsBadgeAssertion = (rows, id, filterValue) => {
+ const isBadgeProgressComplete = (data) => {
+ if (isEmptyObject(data.assertion)) {
+ return false;
+ }
+ return data.assertion.imageUrl.length > 0;
+ };
+
+ return rows.filter(row => {
+ const rowValue = row.values[id];
+ const badgeAsserted = isBadgeProgressComplete(rowValue.props.data);
+
+ return badgeAsserted ? snakeCase(`${rowValue.props.data.blockDisplayName} awarded`) === filterValue : snakeCase(`${rowValue.props.data.blockDisplayName} not awarded`) === filterValue;
+ });
+};
+
+// This is an autoRemove method on the filter function that
+// when given the new filter value and returns true, the filter
+// will be automatically removed. Normally this is just an undefined
+// check, but here, we want to remove the filter if it's not a string or string is empty
+filterContainsBadgeAssertion.autoRemove = val => typeof val !== 'string' || !val;
+
+const headingMapper = (filterKey, data) => {
+ // eslint-disable-next-line no-unused-vars
+ const dataSortable = data.slice();
+
+ function all(entry) {
+ if (entry) {
+ const results = [{
+ Header: 'Student',
+ label: 'Student',
+ key: 'username',
+ accessor: 'username',
+ width: 'col-2',
+ }];
+
+ const progressHeadings = entry.progress
+ .filter(blocks => blocks.blockDisplayName)
+ .map(b => ({
+ Header: b.blockDisplayName.replace(/[0-9]+\./g, ''),
+ accessor: snakeCase(b.blockDisplayName),
+ Filter: DropdownFilter,
+ filter: filterContainsBadgeAssertion,
+ filterChoices: [
+ {
+ name: 'Awarded',
+ value: snakeCase(`${b.blockDisplayName} awarded`),
+ },
+ {
+ name: 'Not Awarded',
+ value: snakeCase(`${b.blockDisplayName} not awarded`),
+ },
+ ],
+ }));
+
+ return results.concat(progressHeadings);
+ }
+ return [];
+ }
+
+ // Todo: Need to implement this.
+ // eslint-disable-next-line no-unused-vars
+ function some(entry) {
+ return [{
+ label: '',
+ key: '',
+ width: 'col-1',
+ }];
+ }
+
+ return filterKey === 'All' ? all : some;
+};
+
+export {
+ headingMapper,
+};
diff --git a/src/course-home/data/__factories__/badgeProgress.factory.js b/src/course-home/data/__factories__/badgeProgress.factory.js
new file mode 100644
index 0000000000..2eda245627
--- /dev/null
+++ b/src/course-home/data/__factories__/badgeProgress.factory.js
@@ -0,0 +1,121 @@
+import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies
+
+Factory.define('badge-progress')
+ .attrs({
+ course_id: 'course-v1:edX+DemoX+Demo_Course',
+ block_id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@dc1e160e5dc348a48a98fa0f4a6e8675',
+ block_display_name: 'Example Week 1: Getting Started',
+ event_type: 'chapter_complete',
+ });
+
+// Default to one badge_class. If badge_class was given, fill in
+// whatever attributes might be missing.
+// .attr('badge_class', ['badge_class'], (badge_class) => {
+// if (!badge_class) {
+// badge_class = [{}];
+// }
+// return badge_class.map((data) => Factory.attributes('badge_class', data));
+// })
+
+// Default to one assertion. If assertion was given, fill in
+// whatever attributes might be missing.
+// .attr('assertion', ['assertion'], (assertion) => {
+// if (!assertion) {
+// assertion = [{}];
+// }
+// return assertion.map((data) => Factory.attributes('assertion', data));
+// })
+
+// {
+// course_id: 'course-v1:edX+DemoX+Demo_Course',
+// block_id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@dc1e160e5dc348a48a98fa0f4a6e8675',
+// block_display_name: 'Example Week 1: Getting Started',
+// event_type: 'chapter_complete',
+// badge_class: {
+// slug: 'special_award',
+// issuing_component: 'openedx__course',
+// display_name: 'Very Special Award',
+// course_id: 'course-v1:edX+DemoX+Demo_Course',
+// description: 'Awarded for people who did something incredibly special',
+// criteria: 'Do something incredibly special.',
+// image: 'http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png'
+// },
+// assertion: {
+// issuedOn: '2019-04-20T02:43:06.566955Z',
+// expires: '2019-04-30T00:00:00.000000Z',
+// revoked: false,
+// image_url: 'http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png',
+// assertion_url: 'http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6',
+// recipient: {
+// plaintextIdentity: 'john.doe@example.com'
+// },
+// issuer: {
+// entityType: 'Issuer',
+// entityId: 'npqlh0acRFG5pKKbnb4SRg',
+// openBadgeId: 'https://api.badgr.io/public/issuers/npqlh0acRFG5pKKbnb4SRg',
+// name: 'EducateWorkforce',
+// image: 'https://media.us.badgr.io/uploads/issuers/issuer_logo_77bb4498-838b-45b7-8722-22878fedb5e8.svg',
+// email: 'cucwd.developer@gmail.com',
+// description: 'An online learning solution offered with partnering 2-year colleges to help integrate'
+// 'web and digital solutions into their existing courses. The platform was designed by'
+// 'multiple instructional design, usability, and computing experts to include research-based'
+// 'learning features.',
+// url: 'https://ew-localhost.com'
+// }
+// }
+// },
+
+Factory.define('badge_class')
+ .attrs({
+ slug: 'special_award',
+ issuing_component: 'openedx__course',
+ display_name: 'Very Special Award',
+ course_id: 'course-v1:edX+DemoX+Demo_Course',
+ description: 'Awarded for people who did something incredibly special',
+ criteria: 'Do something incredibly special.',
+ image: 'http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png',
+ });
+
+Factory.define('assertion')
+ .attrs({
+ issuedOn: '2019-04-20T02:43:06.566955Z',
+ expires: '2019-04-30T00:00:00.000000Z',
+ revoked: false,
+ image_url: 'http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png',
+ assertion_url: 'http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6',
+ })
+
+ // Default to one recipient. If recipient was given, fill in
+ // whatever attributes might be missing.
+ .attr('recipient', ['recipient'], (recipient) => {
+ if (!recipient) {
+ return {};
+ }
+ return recipient.map((data) => Factory.attributes('recipient', data));
+ })
+
+ // Default to one issuer. If issuer was given, fill in
+ // whatever attributes might be missing.
+ .attr('issuer', ['issuer'], (issuer) => {
+ if (!issuer) {
+ return {};
+ }
+ return issuer.map((data) => Factory.attributes('issuer', data));
+ });
+
+Factory.define('recipient')
+ .attrs({
+ plaintextIdentity: 'john.doe@example.com',
+ });
+
+Factory.define('issuer')
+ .attrs({
+ entityType: 'Issuer',
+ entityId: 'npqlh0acRFG5pKKbnb4SRg',
+ openBadgeId: 'https://api.badgr.io/public/issuers/npqlh0acRFG5pKKbnb4SRg',
+ name: 'EducateWorkforce',
+ image: 'https://media.us.badgr.io/uploads/issuers/issuer_logo_77bb4498-838b-45b7-8722-22878fedb5e8.svg',
+ email: 'cucwd.developer@gmail.com',
+ description: 'An online learning solution offered with partnering 2-year colleges to help integrate web and digital solutions into their existing courses. The platform was designed by multiple instructional design, usability, and computing experts to include research-based learning features.',
+ url: 'https://ew-localhost.com',
+ });
diff --git a/src/course-home/data/__factories__/badgeProgressTabData.factory.js b/src/course-home/data/__factories__/badgeProgressTabData.factory.js
new file mode 100644
index 0000000000..6086b31124
--- /dev/null
+++ b/src/course-home/data/__factories__/badgeProgressTabData.factory.js
@@ -0,0 +1,61 @@
+import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies
+
+import './badgeProgress.factory';
+
+// Sample data helpful when developing & testing, to see a variety of configurations.
+// This set of data is not realistic (mix of having access and not), but it
+// is intended to demonstrate many UI results.
+Factory.define('badgeProgressTabData')
+ .sequence('id', (i) => `course-v1:edX+DemoX+Demo_Course_${i}`)
+ .sequence('user_id')
+ .attrs({
+ user_name: 'TestUser',
+ name: 'Test Username',
+ email: 'test@edx.org',
+ })
+ .attrs(
+ 'progress', ['id'], (id) => { // eslint-disable-line no-unused-vars
+ const progress = [
+ Factory.build(
+ 'badge-progress',
+ {
+ course_id: 'course-v1:edX+DemoX+Demo_Course',
+ block_id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@dc1e160e5dc348a48a98fa0f4a6e8675',
+ block_display_name: 'Example Week 1: Getting Started',
+ event_type: 'chapter_complete',
+ badge_class: {
+ slug: 'special_award',
+ issuing_component: 'openedx__course',
+ display_name: 'Very Special Award',
+ course_id: 'course-v1:edX+DemoX+Demo_Course',
+ description: 'Awarded for people who did something incredibly special',
+ criteria: 'Do something incredibly special.',
+ image: 'http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png',
+ },
+ assertion: {
+ issuedOn: '2019-04-20T02:43:06.566955Z',
+ expires: '2019-04-30T00:00:00.000000Z',
+ revoked: false,
+ image_url: 'http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png',
+ assertion_url: 'http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6',
+ recipient: {
+ plaintextIdentity: 'john.doe@example.com',
+ },
+ issuer: {
+ entityType: 'Issuer',
+ entityId: 'npqlh0acRFG5pKKbnb4SRg',
+ openBadgeId: 'https://api.badgr.io/public/issuers/npqlh0acRFG5pKKbnb4SRg',
+ name: 'EducateWorkforce',
+ image: 'https://media.us.badgr.io/uploads/issuers/issuer_logo_77bb4498-838b-45b7-8722-22878fedb5e8.svg',
+ email: 'cucwd.developer@gmail.com',
+ description: 'An online learning solution offered with partnering 2-year colleges to help integrate web and digital solutions into their existing courses. The platform was designed by multiple instructional design, usability, and computing experts to include research-based learning features.',
+ url: 'https://ew-localhost.com',
+ },
+ },
+ },
+ ),
+ ];
+
+ return progress;
+ },
+ );
diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap
index 7be629c665..0e67f6f830 100644
--- a/src/course-home/data/__snapshots__/redux.test.js.snap
+++ b/src/course-home/data/__snapshots__/redux.test.js.snap
@@ -68,6 +68,11 @@ Object {
"title": "Dates",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates",
},
+ Object {
+ "slug": "badges-progress",
+ "title": "Badges",
+ "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/badges-progress",
+ },
],
"title": "Demonstration Course",
"userTimezone": "UTC",
@@ -372,6 +377,11 @@ Object {
"title": "Dates",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates",
},
+ Object {
+ "slug": "badges-progress",
+ "title": "Badges",
+ "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/badges-progress",
+ },
],
"title": "Demonstration Course",
"userTimezone": "UTC",
@@ -407,6 +417,7 @@ Object {
},
"sections": Object {
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": Object {
+ "badgeProgress": undefined,
"complete": false,
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
"estimatedTime": undefined,
@@ -558,6 +569,11 @@ Object {
"title": "Dates",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates",
},
+ Object {
+ "slug": "badges-progress",
+ "title": "Badges",
+ "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/badges-progress",
+ },
],
"title": "Demonstration Course",
"userTimezone": "UTC",
diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js
index aaadebf08c..08c6df5b16 100644
--- a/src/course-home/data/api.js
+++ b/src/course-home/data/api.js
@@ -1,5 +1,5 @@
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
-import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { logInfo } from '@edx/frontend-platform/logging';
import { appendBrowserTimezoneToUrl } from '../../utils';
@@ -123,6 +123,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
case 'chapter':
models.sections[block.id] = {
+ badgeProgress: block.badge_progress,
complete: block.complete,
id: block.id,
title: block.display_name,
@@ -192,10 +193,54 @@ export async function getCourseHomeCourseMetadata(courseId) {
return normalizeCourseHomeCourseMetadata(data);
}
+export async function getBadgeProgressTabData(courseId) {
+ const { administrator, username } = getAuthenticatedUser();
+ const getProgressApiEndPoint = () => (
+ administrator
+ ? `${getConfig().LMS_BASE_URL}/api/badges/v1/progress/${courseId}`
+ : `${getConfig().LMS_BASE_URL}/api/badges/v1/progress/${courseId}/user/${username}`
+ );
+
+ try {
+ const { data } = await getAuthenticatedHttpClient().get(getProgressApiEndPoint());
+ return camelCaseObject(data);
+ } catch (error) {
+ const { httpErrorStatus } = error && error.customAttributes;
+ if (httpErrorStatus === 404) {
+ global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/badges/progress`);
+ return {};
+ }
+ if (httpErrorStatus === 401) {
+ // The backend sends this for unenrolled and unauthenticated learners, but we handle those cases by examining
+ // courseAccess in the metadata call, so just ignore this status for now.
+ return {};
+ }
+ throw error;
+ }
+}
+
+export async function getBadgeLeaderboardTabData(courseId) {
+ // Todo: Need to define an Badge Leaderboard API endpoint for the LMS
+ // and return the result.
+ const url = `${getConfig().LMS_BASE_URL}/api/badges/v1/leaderboard/courses/${courseId}`;
+ try {
+ const { data } = await getAuthenticatedHttpClient().get(url);
+ return camelCaseObject(data);
+ } catch (error) {
+ const { httpErrorStatus } = error && error.customAttributes;
+ if (httpErrorStatus === 404) {
+ // global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/badges/progress`);
+ return {};
+ }
+ throw error;
+ }
+}
+
// For debugging purposes, you might like to see a fully loaded dates tab.
// Just uncomment the next few lines and the immediate 'return' in the function below
// import { Factory } from 'rosie';
// import './__factories__';
+
export async function getDatesTabData(courseId) {
// return camelCaseObject(Factory.build('datesTabData'));
const url = `${getConfig().LMS_BASE_URL}/api/course_home/dates/${courseId}`;
@@ -286,6 +331,41 @@ export async function getProgressTabData(courseId, targetUserId) {
}
}
+export async function getGlossaryTabData(courseId) {
+ const url = `${getConfig().LMS_BASE_URL}/api/course_home/glossary/${courseId}`;
+ try {
+ const { data } = await getAuthenticatedHttpClient().get(url);
+ return camelCaseObject(data);
+ } catch (error) {
+ const { httpErrorStatus } = error && error.customAttributes;
+ if (httpErrorStatus === 404) {
+ global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/glossary`);
+ return {};
+ }
+ if (httpErrorStatus === 401) {
+ // The backend sends this for unenrolled and unauthenticated learners, but we handle those cases by examining
+ // courseAccess in the metadata call, so just ignore this status for now.
+ return {};
+ }
+ throw error;
+ }
+}
+
+export async function getGlossaryData(courseId) {
+ const encodedCourse = courseId.replace(' ', '+');
+ const url = `http://localhost:18500/api/v1/course_terms?course_id=${encodedCourse}`;
+ try {
+ const { data } = await getAuthenticatedHttpClient().get(url);
+ return data;
+ } catch (error) {
+ const { httpErrorStatus } = error && error.customAttributes;
+ if (httpErrorStatus === 404) {
+ return {};
+ }
+ throw error;
+ }
+}
+
export async function getProctoringInfoData(courseId, username) {
let url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?is_learning_mfe=true&course_id=${encodeURIComponent(courseId)}`;
if (username) {
diff --git a/src/course-home/data/index.js b/src/course-home/data/index.js
index c5f79fa6f6..8541ce730d 100644
--- a/src/course-home/data/index.js
+++ b/src/course-home/data/index.js
@@ -1,7 +1,10 @@
export {
+ fetchBadgeProgressTab,
+ fetchBadgeLeaderboardTab,
fetchDatesTab,
fetchOutlineTab,
fetchProgressTab,
+ fetchGlossaryTab,
resetDeadlines,
saveCourseGoal,
} from './thunks';
diff --git a/src/course-home/data/slice.js b/src/course-home/data/slice.js
index b195b420ed..b8efcfe565 100644
--- a/src/course-home/data/slice.js
+++ b/src/course-home/data/slice.js
@@ -9,7 +9,7 @@ export const DENIED = 'denied';
const slice = createSlice({
name: 'course-home',
initialState: {
- courseStatus: 'loading',
+ courseStatus: LOADING,
courseId: null,
toastBodyText: null,
toastBodyLink: null,
diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js
index 99fdc6fdf8..e1a0424a62 100644
--- a/src/course-home/data/thunks.js
+++ b/src/course-home/data/thunks.js
@@ -3,9 +3,12 @@ import { camelCaseObject } from '@edx/frontend-platform';
import {
executePostFromPostEvent,
getCourseHomeCourseMetadata,
+ getBadgeProgressTabData,
+ getBadgeLeaderboardTabData,
getDatesTabData,
getOutlineTabData,
getProgressTabData,
+ getGlossaryTabData,
postCourseDeadlines,
postCourseGoals,
postDismissWelcomeMessage,
@@ -74,6 +77,14 @@ export function fetchTab(courseId, tab, getTabData, targetUserId) {
};
}
+export function fetchBadgeProgressTab(courseId) {
+ return fetchTab(courseId, 'badge-progress', getBadgeProgressTabData);
+}
+
+export function fetchBadgeLeaderboardTab(courseId) {
+ return fetchTab(courseId, 'badge-leaderboard', getBadgeLeaderboardTabData);
+}
+
export function fetchDatesTab(courseId) {
return fetchTab(courseId, 'dates', getDatesTabData);
}
@@ -82,6 +93,10 @@ export function fetchProgressTab(courseId, targetUserId) {
return fetchTab(courseId, 'progress', getProgressTabData, parseInt(targetUserId, 10) || targetUserId);
}
+export function fetchGlossaryTab(courseId) {
+ return fetchTab(courseId, 'glossary', getGlossaryTabData);
+}
+
export function fetchOutlineTab(courseId) {
return fetchTab(courseId, 'outline', getOutlineTabData);
}
diff --git a/src/course-home/glossary-tab/GlossaryTab.jsx b/src/course-home/glossary-tab/GlossaryTab.jsx
new file mode 100644
index 0000000000..2226c302e9
--- /dev/null
+++ b/src/course-home/glossary-tab/GlossaryTab.jsx
@@ -0,0 +1,362 @@
+import React, {
+ useState,
+ useEffect,
+ useContext,
+ createContext,
+} from 'react';
+
+import PropTypes from 'prop-types';
+import { useSelector } from 'react-redux';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import './GlossaryTab.scss';
+
+import {
+ DropdownButton,
+ Collapsible,
+ Icon,
+ ActionRow,
+ SearchField,
+ Pagination,
+ Form,
+} from '@edx/paragon';
+
+import { ExpandLess, ExpandMore } from '@edx/paragon/icons';
+import { getGlossaryData } from '../data/api';
+import messages from './messages';
+
+// Getting all necessary contexts and variables
+export const CourseContext = createContext();
+export const KeyTermContext = createContext();
+const ListViewContext = createContext();
+const queryParams = new URLSearchParams(window.location.search);
+const scrolltoParam = queryParams.get('scrollTo');
+const paginationLength = 15;
+
+// Lists all resources
+function ResourceList() {
+ const { resources } = useContext(KeyTermContext);
+
+ resources.sort((a, b) => (a.friendly_name > b.friendly_name ? 1 : -1));
+
+ if (resources.length > 0) {
+ return (
+
+ );
+ }
+ return null;
+}
+
+// Lists all lessons
+function Lessons() {
+ const { lessons } = useContext(KeyTermContext);
+
+ // Sorting list by module name then by lesson name
+ lessons.sort((a, b) => {
+ if (a.module_name === b.module_name) {
+ if (a.lesson_name > b.lesson_name) { return 1; }
+ return -1;
+ }
+ if (a.module_name > b.module_name) { return 1; }
+
+ return -1;
+ });
+
+ if (lessons.length > 0) {
+ return (
+
+ Lessons
+ {
+ lessons.map(lesson => (
+
+ ))
+ }
+
+ );
+ }
+ return null;
+}
+
+// Gets a specific textbook
+function Lesson({ lesson }) {
+ const { courseId } = useContext(CourseContext);
+ const encodedCourse = courseId.replace(' ', '+');
+ return (
+
+
+ {lesson.module_name}>{lesson.lesson_name}>{lesson.unit_name}
+
+
+ );
+}
+
+// Gets a specific textbook
+function Textbook({ textbook }) {
+ const { courseId } = useContext(CourseContext);
+ const assetId = courseId.replace('course', 'asset');
+
+ const lmsTextbookLink = `http://localhost:18000/${assetId}+type@asset+block@${textbook.textbook_link}#page=${textbook.page_num}`;
+
+ return (
+
+ {textbook.chapter}, pg. {textbook.page_num}
+
+ );
+}
+
+// Lists all textbooks
+function TextbookList() {
+ const { textbooks } = useContext(KeyTermContext);
+ if (textbooks.length > 0) {
+ return (
+
+ Textbooks
+ {
+ textbooks.map(textbook => (
+
+ ))
+ }
+
+ );
+ }
+ return null;
+}
+
+// Lists all definitions
+function DefinitionsList() {
+ const { definitions } = useContext(KeyTermContext);
+ if (definitions.length > 0) {
+ return (
+
+
Definitions
+ {definitions.map((descr) => (
+
+ ))}
+
+ );
+ }
+ return null;
+}
+
+// Refers to one key term.
+function KeyTerm({ index }) {
+ /* eslint-disable camelcase */
+ const { key_name } = useContext(KeyTermContext);
+
+ return (
+
+ {key_name}}
+ styling="card-lg"
+ iconWhenOpen={ }
+ iconWhenClosed={ }
+ >
+
+
+
+ );
+ /* eslint-enable camelcase */
+}
+
+// All the data needed for a keyterm.
+function KeyTermData() {
+ return (
+
+
+
+
+
+
+ );
+}
+
+// Filter modules button
+function ModuleDropdown(termData) {
+ const { filterModules, setFilterModules } = useContext(ListViewContext);
+ const lessons = [];
+ const newSet = new Set();
+
+ termData.value.termData.filter(keyTerm => keyTerm.lessons.forEach(lesson => {
+ if (lessons.find(object => object.module_name === lesson.module_name) === undefined) { lessons.push(lesson); }
+ }));
+
+ lessons.sort((a, b) => (a.module_name > b.module_name ? 1 : -1));
+
+ const handleChange = e => {
+ filterModules.forEach(item => { newSet.add(item); });
+ if (e.target.checked) { newSet.add(e.target.value); } else { newSet.delete(e.target.value); }
+ return setFilterModules(newSet);
+ };
+
+ const buttontitle = filterModules.size > 0 ? `Filter Modules (${filterModules.size})` : 'Filter Modules';
+ return (
+
+
+
+ {lessons.map(lesson => {lesson.module_name} )}
+
+
+
+ );
+}
+
+// Lists all keyterms
+function KeyTermList() {
+ const {
+ filterModules, searchQuery, selectedPage, setPagination,
+ } = useContext(ListViewContext);
+ const { termData } = useContext(CourseContext);
+
+ function paginate(termList, pageSize, pageNumber) {
+ return termList.slice((pageNumber - 1) * pageSize, pageNumber * pageSize);
+ }
+
+ const displayTerms = termData
+ .filter(keyTerm => (
+ // First finds all keyterms that have been filtered for
+ filterModules.size === 0
+ || keyTerm.lessons.find(object => filterModules.has(object.module_name)) !== undefined)
+ // Returns keyterms with names or definitions matching search query
+ && (keyTerm.key_name.toString().toLowerCase().includes(searchQuery.toLowerCase())
+ || keyTerm.definitions.find(object => object.description.toLowerCase()
+ .includes(searchQuery.toLowerCase())) !== undefined))
+ .sort((a, b) => {
+ if (a.key_name < b.key_name) { return -1; }
+ if (a.key_name > b.key_name) { return 1; }
+ return 0;
+ });
+
+ setPagination(displayTerms.length / paginationLength);
+ if (displayTerms.length === 0) { setPagination(0); }
+
+ return (
+
+ {displayTerms.length === 0 ? (
No Terms to Display... ) : null}
+ {paginate(displayTerms, paginationLength, selectedPage).map((keyTerm, index) => (
+
+
+
+ ))}
+
+ );
+}
+
+// Refers to the whole glossary page
+function GlossaryTab({ intl }) {
+ const { courseId } = useSelector(state => state.courseHome);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [filterModules, setFilterModules] = useState(new Set());
+ const [termData, setTermData] = useState([]);
+ const [selectedPage, setSelectedPage] = useState(1);
+ const [pagination, setPagination] = useState();
+ const [expandAll, setExpandAll] = useState(false);
+
+ useEffect(() => {
+ getGlossaryData(courseId)
+ .then((keytermData) => setTermData(keytermData));
+ }, []);
+
+ return (
+ <>
+ {/* Header */}
+
+ {intl.formatMessage(messages.glossaryHeader)}
+
+
+ {/* Search Functions */}
+
+
+ Displaying {pagination > 0 ? 1 + paginationLength * (selectedPage - 1) : 0}
+ -
+ {pagination * paginationLength < paginationLength
+ ? parseInt(pagination * paginationLength, 10)
+ : paginationLength * selectedPage}{' '}
+ of {parseInt(pagination * paginationLength, 10)} items
+
+
+
+ {
+ setSearchQuery(value);
+ }}
+ onClear={() => setSearchQuery('')}
+ placeholder="Search"
+ />
+
+
+
+
+
+ {/* List of Key Terms */}
+
+
+
+
+
+
+ {pagination === 0 ? null : (
+
parseInt(pagination, 10)
+ ? parseInt(pagination, 10) + 1
+ : pagination
+ }
+ onPageSelect={(value) => setSelectedPage(value)}
+ />
+ )}
+
+
+
+ >
+ );
+}
+
+GlossaryTab.propTypes = {
+ intl: intlShape.isRequired,
+};
+
+Textbook.propTypes = {
+ textbook: PropTypes.shape({
+ textbook_link: PropTypes.string,
+ chapter: PropTypes.string,
+ page_num: PropTypes.number,
+ }).isRequired,
+};
+
+Lesson.propTypes = {
+ lesson: PropTypes.shape({
+ id: PropTypes.number,
+ lesson_link: PropTypes.string,
+ module_name: PropTypes.string,
+ lesson_name: PropTypes.string,
+ unit_name: PropTypes.string,
+ }).isRequired,
+};
+
+KeyTerm.propTypes = {
+ index: PropTypes.number.isRequired,
+};
+
+export default injectIntl(GlossaryTab);
diff --git a/src/course-home/glossary-tab/GlossaryTab.scss b/src/course-home/glossary-tab/GlossaryTab.scss
new file mode 100644
index 0000000000..693a740681
--- /dev/null
+++ b/src/course-home/glossary-tab/GlossaryTab.scss
@@ -0,0 +1,138 @@
+
+ .dashboard-container {
+ min-height: 80vh;
+ }
+
+ .key-term_list {
+ display: flex;
+ flex-direction: column;
+ padding:10px;
+ }
+
+ #dropdown-basic-button {
+ margin-left: 10px;
+ }
+
+ .key-term_search {
+ padding-right: 10px;
+ width: 50%;
+ }
+
+ .dropdown-menu {
+ max-height: 250px;
+ overflow-y: scroll;
+ padding: 10px;
+ }
+
+ .pgn__form-label {
+ width: 200px;
+ }
+ // .key-term-container {
+ // display: flex;
+ // flex-direction: column;
+ // }
+
+ .flex-col {
+ display: flex;
+ flex-direction: column;
+ padding-right: 15px;
+ padding-left: 15px;
+ width: 25%;
+ // border-right: 2px solid lightgray;
+ }
+
+ .flex-row {
+ display: flex;
+ flex: 1;
+ }
+
+ .key-term-info {
+ display: flex;
+ align-content: flex-start;
+ flex: 1;
+ }
+
+ .row {
+ width:100%;
+ }
+
+ p {
+ font-size: 0.75em;
+ line-height: 1.5em;
+ /* max-width: 35%; */
+ }
+
+ .sidebar > p {
+ text-align: center;
+ }
+
+ .textbook-container {
+ flex: 1;
+ }
+
+ .menu-bar {
+ padding-bottom: 10px;
+ }
+
+ .bulk-insert-container {
+ text-align: center;
+ }
+
+ .filter-container {
+ text-align: center;
+ }
+
+ .create-key-term {
+ text-align: center;
+ margin-top: 1em;
+ }
+
+ .drag-n-drop {
+ padding-left: 1em;
+ padding-right: 1em;
+ margin-bottom: 10px;
+ }
+
+ p.drag-n-drop {
+ padding-left: 1em;
+ padding-right: 1em;
+ margin-bottom: 25px;
+ margin-top: 10px;
+ }
+
+ br.drag-n-drop {
+ margin: 10px;
+ }
+
+ .footer-container {
+ padding: 20px;
+ display: flex;
+ justify-content: center;
+ }
+
+ .modal-col-left {
+ width: 45%;
+ margin: 10px;
+ }
+
+ .modal-col-right {
+ width: 45%;
+ margin: 10px;
+ }
+
+ .action-row {
+ margin-left: 1.5em;
+ margin-right: 0.5em;
+ }
+
+ .float-left {
+ float: left;
+ }
+
+ .float-right {
+ float: right;
+ }
+
+ p.error-message {
+ color: red;
+ }
\ No newline at end of file
diff --git a/src/course-home/glossary-tab/GlossaryTab.test.jsx b/src/course-home/glossary-tab/GlossaryTab.test.jsx
new file mode 100644
index 0000000000..86327f1538
--- /dev/null
+++ b/src/course-home/glossary-tab/GlossaryTab.test.jsx
@@ -0,0 +1,14 @@
+import { screen } from '@testing-library/react';
+
+import {
+ initializeMockApp,
+} from '../../setupTest';
+
+initializeMockApp();
+jest.mock('@edx/frontend-platform/analytics');
+
+describe('Glossary Tab', () => {
+ it('has a title', async () => {
+ expect(screen.getByText('Glossary')).toBeInTheDocument();
+ });
+});
diff --git a/src/course-home/glossary-tab/index.jsx b/src/course-home/glossary-tab/index.jsx
new file mode 100644
index 0000000000..d684d52048
--- /dev/null
+++ b/src/course-home/glossary-tab/index.jsx
@@ -0,0 +1,3 @@
+import GlossaryTab from './GlossaryTab';
+
+export default GlossaryTab;
diff --git a/src/course-home/glossary-tab/messages.js b/src/course-home/glossary-tab/messages.js
new file mode 100644
index 0000000000..7011568bf9
--- /dev/null
+++ b/src/course-home/glossary-tab/messages.js
@@ -0,0 +1,14 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ glossaryHeader: {
+ id: 'glossary.header',
+ defaultMessage: 'Glossary',
+ },
+ studioLink: {
+ id: 'glossary.link.studio',
+ defaultMessage: 'View grading in Studio',
+ },
+});
+
+export default messages;
diff --git a/src/course-home/outline-tab/Section.jsx b/src/course-home/outline-tab/Section.jsx
index 65cd84f396..00eebe37f1 100644
--- a/src/course-home/outline-tab/Section.jsx
+++ b/src/course-home/outline-tab/Section.jsx
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import { Collapsible, IconButton } from '@edx/paragon';
+import { Button, Collapsible, IconButton } from '@edx/paragon';
import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -20,6 +20,7 @@ function Section({
section,
}) {
const {
+ badgeProgress,
complete,
sequenceIds,
title,
@@ -42,6 +43,7 @@ function Section({
}, []);
const estimatedTimeMinutes = parseInt(estimatedTime / 60, 10) + (estimatedTime % 60 > 0);
+ const badgeProgressUrl = './badges/progress';
const sectionTitle = (
@@ -74,6 +76,19 @@ function Section({
, {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
+ {badgeProgress ? (
+ <>
+ {badgeProgressUrl && (
+
+
+ Badge Progress
+
+
+ )}
+ >
+ ) : (
+ <>>
+ )}
);
diff --git a/src/index.jsx b/src/index.jsx
index fd6d8c3567..b3486b16ba 100755
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -20,12 +20,16 @@ import OutlineTab from './course-home/outline-tab';
import { CourseExit } from './courseware/course/course-exit';
import CoursewareContainer from './courseware';
import CoursewareRedirectLandingPage from './courseware/CoursewareRedirectLandingPage';
+import { BadgeProgressTab, BadgeLeaderboardTab } from './course-home/badges-tab';
import DatesTab from './course-home/dates-tab';
import GoalUnsubscribe from './course-home/goal-unsubscribe';
import ProgressTab from './course-home/progress-tab/ProgressTab';
+import GlossaryTab from './course-home/glossary-tab';
import { TabContainer } from './tab-page';
-import { fetchDatesTab, fetchOutlineTab, fetchProgressTab } from './course-home/data';
+import {
+ fetchBadgeProgressTab, fetchBadgeLeaderboardTab, fetchDatesTab, fetchOutlineTab, fetchProgressTab, fetchGlossaryTab,
+} from './course-home/data';
import { fetchCourse } from './courseware/data';
import initializeStore from './store';
import NoticesProvider from './generic/notices';
@@ -43,11 +47,26 @@ subscribe(APP_READY, () => {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
>
@@ -84,12 +94,21 @@ function TabPage({ intl, ...props }) {
);
}
+ let notificationMessage;
+ switch (activeTabSlug) {
+ case 'badge_progress':
+ notificationMessage = messagesBadgeProgress.failure;
+ break;
+ default:
+ notificationMessage = messages.failure;
+ }
+
// courseStatus 'failed' and any other unexpected course status.
return (
<>
- {intl.formatMessage(messages.failure)}
+ {intl.formatMessage(notificationMessage)}
>
diff --git a/src/utils/empty.js b/src/utils/empty.js
new file mode 100644
index 0000000000..16351140ec
--- /dev/null
+++ b/src/utils/empty.js
@@ -0,0 +1,10 @@
+/* eslint-disable import/prefer-default-export */
+
+export const isEmptyObject = (obj) => {
+ Object.keys(obj).forEach(k => {
+ if (Object.prototype.hasOwnProperty.call(obj, k)) {
+ return false;
+ }
+ return true;
+ });
+};