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 + &&

the user is admin

} +
+
+
+
+ + ); +} + +// 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 + &&

the user is admin

} +
+
+ */ + 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 && ( + {data.badgeClass.displayName} + )} + + ); + }; + + 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.displayName}

+
+
+
+ {progress.badgeClass.description} +
+
+
+
+
+
+ + +
+
+ {progress.badgeClass.displayName} + +
+
+ {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. +

+
+
+
+
+
+
    +
  1. Create a Badgr account, or log in to your existing account;
  2. +
  3. Share this public URL to your badge; or
  4. +
  5. + 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. +
  6. +
+
+
+ {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 ( +
+ + + + {renderRows()} + +
+
+ ); +}; + +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 ( + + {alt} + + ); +} + +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 ( +
+ References: + { + resources.map(resource => ( +

+ {resource.friendly_name} +

+ )) + } +
+ ); + } + 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) => ( +
+

{descr.description}

+
+ ))} +
+ ); + } + 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 && ( + + + + )} + + ) : ( + <> + )}
); 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, () => { + + + + + + + + + + + + + + +