diff --git a/client/package.json b/client/package.json
index 1d4d5ac32..46c5e4b13 100644
--- a/client/package.json
+++ b/client/package.json
@@ -66,6 +66,7 @@
"react-bootstrap": "1.0.1",
"react-dom": "16.12.0",
"react-hot-toast": "2.0.0",
+ "react-markdown": "^7.0.1",
"react-perfect-scrollbar": "1.5.8",
"react-query": "3.18.1",
"react-router": "5.2.0",
diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml
index 8c1e5c475..10b4f4324 100644
--- a/client/pnpm-lock.yaml
+++ b/client/pnpm-lock.yaml
@@ -65,6 +65,7 @@ specifiers:
react-dev-utils: ^9.1.0
react-dom: 16.12.0
react-hot-toast: 2.0.0
+ react-markdown: ^7.0.1
react-perfect-scrollbar: 1.5.8
react-query: 3.18.1
react-router: 5.2.0
@@ -132,6 +133,7 @@ dependencies:
react-bootstrap: 1.0.1_react-dom@16.12.0+react@17.0.2
react-dom: 16.12.0_react@17.0.2
react-hot-toast: 2.0.0_react-dom@16.12.0+react@17.0.2
+ react-markdown: 7.0.1_43559efe81628b42e1e465743e6a3ffa
react-perfect-scrollbar: 1.5.8_react-dom@16.12.0+react@17.0.2
react-query: 3.18.1_react-dom@16.12.0+react@17.0.2
react-router: 5.2.0_react@17.0.2
@@ -2923,6 +2925,18 @@ packages:
'@types/node': 16.0.0
dev: false
+ /@types/debug/4.1.7:
+ resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
+ dependencies:
+ '@types/ms': 0.7.31
+ dev: false
+
+ /@types/hast/2.3.4:
+ resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==}
+ dependencies:
+ '@types/unist': 2.0.6
+ dev: false
+
/@types/history/4.7.9:
resolution: {integrity: sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==}
dev: true
@@ -2966,6 +2980,16 @@ packages:
resolution: {integrity: sha512-cPiXpOvPFDr2edMnOXlz3UBDApwUfR+cpizvxCy0n3vp9bz/qe8BWzHPIEFcy+ogUOyjKuCISgyq77ELZPmkkg==}
dev: true
+ /@types/mdast/3.0.10:
+ resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==}
+ dependencies:
+ '@types/unist': 2.0.6
+ dev: false
+
+ /@types/mdurl/1.0.2:
+ resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==}
+ dev: false
+
/@types/mongodb/3.6.20:
resolution: {integrity: sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==}
dependencies:
@@ -2973,6 +2997,10 @@ packages:
'@types/node': 16.0.0
dev: false
+ /@types/ms/0.7.31:
+ resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
+ dev: false
+
/@types/node/16.0.0:
resolution: {integrity: sha512-TmCW5HoZ2o2/z2EYi109jLqIaPIi9y/lc2LmDCWzuCi35bcaQ+OtUh6nwBiFK7SOu25FAU5+YKdqFZUwtqGSdg==}
@@ -3057,6 +3085,10 @@ packages:
source-map: 0.6.1
dev: false
+ /@types/unist/2.0.6:
+ resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
+ dev: false
+
/@types/warning/3.0.0:
resolution: {integrity: sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=}
dev: false
@@ -3838,6 +3870,10 @@ packages:
hasBin: true
dev: true
+ /bail/2.0.1:
+ resolution: {integrity: sha512-d5FoTAr2S5DSUPKl85WNm2yUwsINN8eidIdIwsOge2t33DaOfOdSmmsI11jMN3GmALCXaw+Y6HMVHDzePshFAA==}
+ dev: false
+
/balanced-match/1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -4276,6 +4312,18 @@ packages:
supports-color: 7.2.0
dev: true
+ /character-entities-legacy/2.0.0:
+ resolution: {integrity: sha512-YwaEtEvWLpFa6Wh3uVLrvirA/ahr9fki/NUd/Bd4OR6EdJ8D22hovYQEOUCBfQfcqnC4IAMGMsHXY1eXgL4ZZA==}
+ dev: false
+
+ /character-entities/2.0.0:
+ resolution: {integrity: sha512-oHqMj3eAuJ77/P5PaIRcqk+C3hdfNwyCD2DAUcD5gyXkegAuF2USC40CEqPscDk4I8FRGMTojGJQkXDsN5QlJA==}
+ dev: false
+
+ /character-reference-invalid/2.0.0:
+ resolution: {integrity: sha512-pE3Z15lLRxDzWJy7bBHBopRwfI20sbrMVLQTC7xsPglCHf4Wv1e167OgYAFP78co2XlhojDyAqA+IAJse27//g==}
+ dev: false
+
/chardet/0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
dev: true
@@ -4496,6 +4544,10 @@ packages:
delayed-stream: 1.0.0
dev: false
+ /comma-separated-tokens/2.0.2:
+ resolution: {integrity: sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==}
+ dev: false
+
/commander/2.1.0:
resolution: {integrity: sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E=}
engines: {node: '>= 0.6.x'}
@@ -7040,6 +7092,10 @@ packages:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
dev: true
+ /inline-style-parser/0.1.1:
+ resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==}
+ dev: false
+
/inquirer/6.5.0:
resolution: {integrity: sha512-scfHejeG/lVZSpvCXpsB4j/wQNPM5JC8kiElOI0OUTwmc1RTpXr4H32/HOlQHcZiYl2z2VElwuCVDRG8vFmbnA==}
engines: {node: '>=6.0.0'}
@@ -7127,6 +7183,17 @@ packages:
kind-of: 6.0.3
dev: true
+ /is-alphabetical/2.0.0:
+ resolution: {integrity: sha512-5OV8Toyq3oh4eq6sbWTYzlGdnMT/DPI5I0zxUBxjiigQsZycpkKF3kskkao3JyYGuYDHvhgJF+DrjMQp9SX86w==}
+ dev: false
+
+ /is-alphanumerical/2.0.0:
+ resolution: {integrity: sha512-t+2GlJ+hO9yagJ+jU3+HSh80VKvz/3cG2cxbGGm4S0hjKuhWQXgPVUVOZz3tqZzMjhmphZ+1TIJTlRZRoe6GCQ==}
+ dependencies:
+ is-alphabetical: 2.0.0
+ is-decimal: 2.0.0
+ dev: false
+
/is-arguments/1.1.1:
resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
engines: {node: '>= 0.4'}
@@ -7169,6 +7236,11 @@ packages:
/is-buffer/1.1.6:
resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
+ /is-buffer/2.0.5:
+ resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==}
+ engines: {node: '>=4'}
+ dev: false
+
/is-callable/1.2.4:
resolution: {integrity: sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==}
engines: {node: '>= 0.4'}
@@ -7208,6 +7280,10 @@ packages:
dependencies:
has-tostringtag: 1.0.0
+ /is-decimal/2.0.0:
+ resolution: {integrity: sha512-QfrfjQV0LjoWQ1K1XSoEZkTAzSa14RKVMa5zg3SdAfzEmQzRM4+tbSFWb78creCeA9rNBzaZal92opi1TwPWZw==}
+ dev: false
+
/is-descriptor/0.1.6:
resolution: {integrity: sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==}
engines: {node: '>=0.10.0'}
@@ -7272,6 +7348,10 @@ packages:
dependencies:
is-extglob: 2.1.1
+ /is-hexadecimal/2.0.0:
+ resolution: {integrity: sha512-vGOtYkiaxwIiR0+Ng/zNId+ZZehGfINwTzdrDqc6iubbnQWhnPuYymOzOKUDqa2cSl59yHnEh2h6MvRLQsyNug==}
+ dev: false
+
/is-in-browser/1.1.3:
resolution: {integrity: sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=}
dev: false
@@ -7330,6 +7410,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: false
+ /is-plain-obj/4.0.0:
+ resolution: {integrity: sha512-NXRbBtUdBioI73y/HmOhogw/U5msYPC9DAtGkJXeFcFWSFZw0mCUsPxk/snTuJHzNKA8kLBK4rH97RMB1BfCXw==}
+ engines: {node: '>=12'}
+ dev: false
+
/is-plain-object/2.0.4:
resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
engines: {node: '>=0.10.0'}
@@ -8004,12 +8089,59 @@ packages:
safe-buffer: 5.2.1
dev: true
+ /mdast-util-definitions/5.1.0:
+ resolution: {integrity: sha512-5hcR7FL2EuZ4q6lLMUK5w4lHT2H3vqL9quPvYZ/Ku5iifrirfMHiGdhxdXMUbUkDmz5I+TYMd7nbaxUhbQkfpQ==}
+ dependencies:
+ '@types/mdast': 3.0.10
+ '@types/unist': 2.0.6
+ unist-util-visit: 3.1.0
+ dev: false
+
+ /mdast-util-from-markdown/1.0.0:
+ resolution: {integrity: sha512-uj2G60sb7z1PNOeElFwCC9b/Se/lFXuLhVKFOAY2EHz/VvgbupTQRNXPoZl7rGpXYL6BNZgcgaybrlSWbo7n/g==}
+ dependencies:
+ '@types/mdast': 3.0.10
+ '@types/unist': 2.0.6
+ mdast-util-to-string: 3.1.0
+ micromark: 3.0.5
+ micromark-util-decode-numeric-character-reference: 1.0.0
+ micromark-util-normalize-identifier: 1.0.0
+ micromark-util-symbol: 1.0.0
+ micromark-util-types: 1.0.1
+ parse-entities: 3.0.0
+ unist-util-stringify-position: 3.0.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /mdast-util-to-hast/11.2.0:
+ resolution: {integrity: sha512-KSYbg4PA9wk5YwoCZCPxbUAjdYunNN5TqTXoZp/9taRDGQS65cL2fFgKc78l0f3deg4p1LP9xdhmMuUrhAUSZA==}
+ dependencies:
+ '@types/hast': 2.3.4
+ '@types/mdast': 3.0.10
+ '@types/mdurl': 1.0.2
+ mdast-util-definitions: 5.1.0
+ mdurl: 1.0.1
+ unist-builder: 3.0.0
+ unist-util-generated: 2.0.0
+ unist-util-position: 4.0.1
+ unist-util-visit: 3.1.0
+ dev: false
+
+ /mdast-util-to-string/3.1.0:
+ resolution: {integrity: sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==}
+ dev: false
+
/mdn-data/2.0.14:
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
/mdn-data/2.0.4:
resolution: {integrity: sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==}
+ /mdurl/1.0.1:
+ resolution: {integrity: sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=}
+ dev: false
+
/media-typer/0.3.0:
resolution: {integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=}
engines: {node: '>= 0.6'}
@@ -8071,6 +8203,168 @@ packages:
resolution: {integrity: sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==}
dev: true
+ /micromark-core-commonmark/1.0.1:
+ resolution: {integrity: sha512-vEOw8hcQ3nwHkKKNIyP9wBi8M50zjNajtmI+cCUWcVfJS+v5/3WCh4PLKf7PPRZFUutjzl4ZjlHwBWUKfb/SkA==}
+ dependencies:
+ 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.1.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
+ parse-entities: 3.0.0
+ dev: false
+
+ /micromark-factory-destination/1.0.0:
+ resolution: {integrity: sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==}
+ dependencies:
+ micromark-util-character: 1.1.0
+ micromark-util-symbol: 1.0.0
+ micromark-util-types: 1.0.1
+ dev: false
+
+ /micromark-factory-label/1.0.0:
+ resolution: {integrity: sha512-XWEucVZb+qBCe2jmlOnWr6sWSY6NHx+wtpgYFsm4G+dufOf6tTQRRo0bdO7XSlGPu5fyjpJenth6Ksnc5Mwfww==}
+ dependencies:
+ micromark-util-character: 1.1.0
+ micromark-util-symbol: 1.0.0
+ micromark-util-types: 1.0.1
+ dev: false
+
+ /micromark-factory-space/1.0.0:
+ resolution: {integrity: sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==}
+ dependencies:
+ micromark-util-character: 1.1.0
+ micromark-util-types: 1.0.1
+ dev: false
+
+ /micromark-factory-title/1.0.0:
+ resolution: {integrity: sha512-flvC7Gx0dWVWorXuBl09Cr3wB5FTuYec8pMGVySIp2ZlqTcIjN/lFohZcP0EG//krTptm34kozHk7aK/CleCfA==}
+ dependencies:
+ micromark-factory-space: 1.0.0
+ micromark-util-character: 1.1.0
+ micromark-util-symbol: 1.0.0
+ micromark-util-types: 1.0.1
+ dev: false
+
+ /micromark-factory-whitespace/1.0.0:
+ resolution: {integrity: sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==}
+ dependencies:
+ micromark-factory-space: 1.0.0
+ micromark-util-character: 1.1.0
+ micromark-util-symbol: 1.0.0
+ micromark-util-types: 1.0.1
+ dev: false
+
+ /micromark-util-character/1.1.0:
+ resolution: {integrity: sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==}
+ dependencies:
+ micromark-util-symbol: 1.0.0
+ micromark-util-types: 1.0.1
+ dev: false
+
+ /micromark-util-chunked/1.0.0:
+ resolution: {integrity: sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==}
+ dependencies:
+ micromark-util-symbol: 1.0.0
+ dev: false
+
+ /micromark-util-classify-character/1.0.0:
+ resolution: {integrity: sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==}
+ dependencies:
+ micromark-util-character: 1.1.0
+ micromark-util-symbol: 1.0.0
+ micromark-util-types: 1.0.1
+ dev: false
+
+ /micromark-util-combine-extensions/1.0.0:
+ resolution: {integrity: sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==}
+ dependencies:
+ micromark-util-chunked: 1.0.0
+ micromark-util-types: 1.0.1
+ dev: false
+
+ /micromark-util-decode-numeric-character-reference/1.0.0:
+ resolution: {integrity: sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==}
+ dependencies:
+ micromark-util-symbol: 1.0.0
+ dev: false
+
+ /micromark-util-encode/1.0.0:
+ resolution: {integrity: sha512-cJpFVM768h6zkd8qJ1LNRrITfY4gwFt+tziPcIf71Ui8yFzY9wG3snZQqiWVq93PG4Sw6YOtcNiKJfVIs9qfGg==}
+ dev: false
+
+ /micromark-util-html-tag-name/1.0.0:
+ resolution: {integrity: sha512-NenEKIshW2ZI/ERv9HtFNsrn3llSPZtY337LID/24WeLqMzeZhBEE6BQ0vS2ZBjshm5n40chKtJ3qjAbVV8S0g==}
+ dev: false
+
+ /micromark-util-normalize-identifier/1.0.0:
+ resolution: {integrity: sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==}
+ dependencies:
+ micromark-util-symbol: 1.0.0
+ dev: false
+
+ /micromark-util-resolve-all/1.0.0:
+ resolution: {integrity: sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==}
+ dependencies:
+ micromark-util-types: 1.0.1
+ dev: false
+
+ /micromark-util-sanitize-uri/1.0.0:
+ resolution: {integrity: sha512-cCxvBKlmac4rxCGx6ejlIviRaMKZc0fWm5HdCHEeDWRSkn44l6NdYVRyU+0nT1XC72EQJMZV8IPHF+jTr56lAg==}
+ dependencies:
+ micromark-util-character: 1.1.0
+ micromark-util-encode: 1.0.0
+ micromark-util-symbol: 1.0.0
+ dev: false
+
+ /micromark-util-subtokenize/1.0.0:
+ resolution: {integrity: sha512-EsnG2qscmcN5XhkqQBZni/4oQbLFjz9yk3ZM/P8a3YUjwV6+6On2wehr1ALx0MxK3+XXXLTzuBKHDFeDFYRdgQ==}
+ dependencies:
+ micromark-util-chunked: 1.0.0
+ micromark-util-symbol: 1.0.0
+ micromark-util-types: 1.0.1
+ dev: false
+
+ /micromark-util-symbol/1.0.0:
+ resolution: {integrity: sha512-NZA01jHRNCt4KlOROn8/bGi6vvpEmlXld7EHcRH+aYWUfL3Wc8JLUNNlqUMKa0hhz6GrpUWsHtzPmKof57v0gQ==}
+ dev: false
+
+ /micromark-util-types/1.0.1:
+ resolution: {integrity: sha512-UT0ylWEEy80RFYzK9pEaugTqaxoD/j0Y9WhHpSyitxd99zjoQz7JJ+iKuhPAgOW2MiPSUAx+c09dcqokeyaROA==}
+ dev: false
+
+ /micromark/3.0.5:
+ resolution: {integrity: sha512-QfjERBnPw0G9mxhOCkkbRP0n8SX8lIBLrEKeEVceviUukqVMv3hWE4AgNTOK/W6GWqtPvvIHg2Apl3j1Dxm6aQ==}
+ dependencies:
+ '@types/debug': 4.1.7
+ debug: 4.3.2
+ micromark-core-commonmark: 1.0.1
+ micromark-factory-space: 1.0.0
+ micromark-util-character: 1.1.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
+ parse-entities: 3.0.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
/micromatch/3.1.10:
resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==}
engines: {node: '>=0.10.0'}
@@ -8916,6 +9210,17 @@ packages:
safe-buffer: 5.2.1
dev: true
+ /parse-entities/3.0.0:
+ resolution: {integrity: sha512-AJlcIFDNPEP33KyJLguv0xJc83BNvjxwpuUIcetyXUsLpVXAUCePJ5kIoYtEN2R1ac0cYaRu/vk9dVFkewHQhQ==}
+ dependencies:
+ character-entities: 2.0.0
+ character-entities-legacy: 2.0.0
+ character-reference-invalid: 2.0.0
+ is-alphanumerical: 2.0.0
+ is-decimal: 2.0.0
+ is-hexadecimal: 2.0.0
+ dev: false
+
/parse-json/4.0.0:
resolution: {integrity: sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=}
engines: {node: '>=4'}
@@ -9845,6 +10150,10 @@ packages:
object-assign: 4.1.1
react-is: 16.13.1
+ /property-information/6.0.1:
+ resolution: {integrity: sha512-F4WUUAF7fMeF4/JUFHNBWDaKDXi2jbvqBW/y6o5wsf3j19wTZ7S60TmtB5HoBhtgw7NKQRMWuz5vk2PR0CygUg==}
+ dev: false
+
/proxy-addr/2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
@@ -10101,12 +10410,36 @@ packages:
/react-is/17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
- dev: true
/react-lifecycles-compat/3.0.4:
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
dev: false
+ /react-markdown/7.0.1_43559efe81628b42e1e465743e6a3ffa:
+ resolution: {integrity: sha512-pthNPaoiwg0q7hukoE04F2ENwSzijIlWHJ4UMs/96LUe/G/P3FnbP4qHzx3FoNqae+2SqDG8vzniTLnJDeWneg==}
+ peerDependencies:
+ '@types/react': '>=16'
+ react: '>=16'
+ dependencies:
+ '@types/hast': 2.3.4
+ '@types/react': 17.0.13
+ '@types/unist': 2.0.6
+ comma-separated-tokens: 2.0.2
+ prop-types: 15.7.2
+ property-information: 6.0.1
+ react: 17.0.2
+ react-is: 17.0.2
+ remark-parse: 10.0.0
+ remark-rehype: 9.0.0
+ space-separated-tokens: 2.0.1
+ style-to-object: 0.3.0
+ unified: 10.1.0
+ unist-util-visit: 4.0.0
+ vfile: 5.1.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
/react-motion/0.5.2_react@17.0.2:
resolution: {integrity: sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ==}
peerDependencies:
@@ -10415,6 +10748,25 @@ packages:
es6-error: 4.1.1
dev: false
+ /remark-parse/10.0.0:
+ resolution: {integrity: sha512-07ei47p2Xl7Bqbn9H2VYQYirnAFJPwdMuypdozWsSbnmrkgA2e2sZLZdnDNrrsxR4onmIzH/J6KXqKxCuqHtPQ==}
+ dependencies:
+ '@types/mdast': 3.0.10
+ mdast-util-from-markdown: 1.0.0
+ unified: 10.1.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /remark-rehype/9.0.0:
+ resolution: {integrity: sha512-SFA+mPWu45ynFPKeT3h5eNNVAYoMp3wizr3KSKh1IQ9L6dLSyD25/df6/vv8EW8ji3O3dnZGdbLQl592Tn+ydg==}
+ dependencies:
+ '@types/hast': 2.3.4
+ '@types/mdast': 3.0.10
+ mdast-util-to-hast: 11.2.0
+ unified: 10.1.0
+ dev: false
+
/remove-accents/0.4.2:
resolution: {integrity: sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=}
dev: false
@@ -11054,6 +11406,10 @@ packages:
engines: {node: '>= 8'}
dev: false
+ /space-separated-tokens/2.0.1:
+ resolution: {integrity: sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw==}
+ dev: false
+
/sparse-bitfield/3.0.3:
resolution: {integrity: sha1-/0rm5oZWBWuks+eSqzM004JzyhE=}
dependencies:
@@ -11346,6 +11702,12 @@ packages:
webpack: 4.41.0
dev: true
+ /style-to-object/0.3.0:
+ resolution: {integrity: sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==}
+ dependencies:
+ inline-style-parser: 0.1.1
+ dev: false
+
/stylehacks/4.0.3:
resolution: {integrity: sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==}
engines: {node: '>=6.9.0'}
@@ -11733,6 +12095,10 @@ packages:
resolution: {integrity: sha1-N21NKdlR1jaKb3oK6FwvTV4GWPM=}
dev: false
+ /trough/2.0.2:
+ resolution: {integrity: sha512-FnHq5sTMxC0sk957wHDzRnemFnNBvt/gSY99HzK8F7UP5WAbvP70yX5bd7CjEQkN+TjdxwI7g7lJ6podqrG2/w==}
+ dev: false
+
/ts-pnp/1.1.4_typescript@4.3.5:
resolution: {integrity: sha512-1J/vefLC+BWSo+qe8OnJQfWTYRS6ingxjwqmHMqaMxXMj7kFtKLgAaYW3JeX3mktjgUL+etlU8/B4VUAUI9QGw==}
engines: {node: '>=6'}
@@ -11887,6 +12253,18 @@ packages:
engines: {node: '>=4'}
dev: false
+ /unified/10.1.0:
+ resolution: {integrity: sha512-4U3ru/BRXYYhKbwXV6lU6bufLikoAavTwev89H5UxY8enDFaAT2VXmIXYNm6hb5oHPng/EXr77PVyDFcptbk5g==}
+ dependencies:
+ '@types/unist': 2.0.6
+ bail: 2.0.1
+ extend: 3.0.2
+ is-buffer: 2.0.5
+ is-plain-obj: 4.0.0
+ trough: 2.0.2
+ vfile: 5.1.0
+ dev: false
+
/union-value/1.0.1:
resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==}
engines: {node: '>=0.10.0'}
@@ -11915,6 +12293,60 @@ packages:
imurmurhash: 0.1.4
dev: true
+ /unist-builder/3.0.0:
+ resolution: {integrity: sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ==}
+ dependencies:
+ '@types/unist': 2.0.6
+ dev: false
+
+ /unist-util-generated/2.0.0:
+ resolution: {integrity: sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw==}
+ dev: false
+
+ /unist-util-is/5.1.1:
+ resolution: {integrity: sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==}
+ dev: false
+
+ /unist-util-position/4.0.1:
+ resolution: {integrity: sha512-mgy/zI9fQ2HlbOtTdr2w9lhVaiFUHWQnZrFF2EUoVOqtAUdzqMtNiD99qA5a1IcjWVR8O6aVYE9u7Z2z1v0SQA==}
+ dev: false
+
+ /unist-util-stringify-position/3.0.0:
+ resolution: {integrity: sha512-SdfAl8fsDclywZpfMDTVDxA2V7LjtRDTOFd44wUJamgl6OlVngsqWjxvermMYf60elWHbxhuRCZml7AnuXCaSA==}
+ dependencies:
+ '@types/unist': 2.0.6
+ dev: false
+
+ /unist-util-visit-parents/4.1.1:
+ resolution: {integrity: sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==}
+ dependencies:
+ '@types/unist': 2.0.6
+ unist-util-is: 5.1.1
+ dev: false
+
+ /unist-util-visit-parents/5.0.0:
+ resolution: {integrity: sha512-CVaLOYPM/EaFTYMytbaju3Tw4QI3DHnHFnL358FkEu0hZOzSm/hqBdVwOQDR60jF5ZzhB1tlZlRH0ll/yekZIQ==}
+ dependencies:
+ '@types/unist': 2.0.6
+ unist-util-is: 5.1.1
+ dev: false
+
+ /unist-util-visit/3.1.0:
+ resolution: {integrity: sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==}
+ dependencies:
+ '@types/unist': 2.0.6
+ unist-util-is: 5.1.1
+ unist-util-visit-parents: 4.1.1
+ dev: false
+
+ /unist-util-visit/4.0.0:
+ resolution: {integrity: sha512-3HWTvrtU10/E7qgPznBfiOyG0TXj9W8c1GSfaI8L9GkaG1pLePiQPZ7E35a0R3ToQ/zcy4Im6aZ9WBgOTnv1MQ==}
+ dependencies:
+ '@types/unist': 2.0.6
+ unist-util-is: 5.1.1
+ unist-util-visit-parents: 5.0.0
+ dev: false
+
/universalify/0.1.2:
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
engines: {node: '>= 4.0.0'}
@@ -12073,6 +12505,22 @@ packages:
extsprintf: 1.3.0
dev: false
+ /vfile-message/3.0.2:
+ resolution: {integrity: sha512-UUjZYIOg9lDRwwiBAuezLIsu9KlXntdxwG+nXnjuQAHvBpcX3x0eN8h+I7TkY5nkCXj+cWVp4ZqebtGBvok8ww==}
+ dependencies:
+ '@types/unist': 2.0.6
+ unist-util-stringify-position: 3.0.0
+ dev: false
+
+ /vfile/5.1.0:
+ resolution: {integrity: sha512-4o7/DJjEaFPYSh0ckv5kcYkJTHQgCKdL8ozMM1jLAxO9ox95IzveDPXCZp08HamdWq8JXTkClDvfAKaeLQeKtg==}
+ dependencies:
+ '@types/unist': 2.0.6
+ is-buffer: 2.0.5
+ unist-util-stringify-position: 3.0.0
+ vfile-message: 3.0.2
+ dev: false
+
/vm-browserify/1.1.2:
resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==}
dev: true
diff --git a/client/src/app/pages/dashboard/users/UserGamesPage.tsx b/client/src/app/pages/dashboard/users/UserGamesPage.tsx
new file mode 100644
index 000000000..17b809c8f
--- /dev/null
+++ b/client/src/app/pages/dashboard/users/UserGamesPage.tsx
@@ -0,0 +1,89 @@
+import React, { useContext, useState, useMemo, useEffect } from "react";
+import { FormatGame, PublicUserDocument, UserGameStats } from "tachi-common";
+import useSetSubheader from "components/layout/header/useSetSubheader";
+import Card from "components/layout/page/Card";
+import ReactMarkdown from "react-markdown";
+import { UserContext } from "context/UserContext";
+import { Button } from "react-bootstrap";
+import { APIFetchV1 } from "util/api";
+import Divider from "components/util/Divider";
+import Muted from "components/util/Muted";
+import ExternalLink from "components/util/ExternalLink";
+import AsyncLoader from "components/util/AsyncLoader";
+import UGPTRatingsTable from "components/user/UGPTStatsOverview";
+import LinkButton from "components/util/LinkButton";
+import { UGSWithRankingData } from "types/api-returns";
+import RankingData from "components/user/UGPTRankingData";
+
+interface Props {
+ reqUser: PublicUserDocument;
+}
+
+export default function UserGamesPage({ reqUser }: Props) {
+ useSetSubheader(["Users", reqUser.username], [reqUser], `${reqUser.username}'s Profile`);
+
+ return (
+
+
{
+ const res = await APIFetchV1(
+ `/users/${reqUser.id}/game-stats`
+ );
+
+ if (!res.success) {
+ throw new Error(res.description);
+ }
+
+ return res.body;
+ }}
+ >
+ {ugs =>
+ ugs.map((e, i) => {
+ // uses modulo to determine whether we're on the
+ // last row or not
+ // to do sizing like [12] or [6][6] instead of [3][3][3]
+ let mdSize = "6";
+ let lgSize = "4";
+ if (ugs.length - i <= ugs.length % 3) {
+ lgSize = (12 / (ugs.length % 3)).toString();
+ }
+
+ if (ugs.length - i <= ugs.length % 2) {
+ mdSize = (12 / (ugs.length % 2)).toString();
+ }
+
+ return (
+
+
+
+ );
+ })
+ }
+
+
+ );
+}
+
+function GameStatContainer({ ugs, reqUser }: { ugs: UGSWithRankingData } & Props) {
+ return (
+
+
+ View Game Profile
+
+
+ }
+ header={FormatGame(ugs.game, ugs.playtype)}
+ >
+
+
+
+ );
+}
diff --git a/client/src/app/pages/dashboard/users/UserPage.tsx b/client/src/app/pages/dashboard/users/UserPage.tsx
index 215f7aa57..861806018 100644
--- a/client/src/app/pages/dashboard/users/UserPage.tsx
+++ b/client/src/app/pages/dashboard/users/UserPage.tsx
@@ -1,16 +1,109 @@
-import Loading from "components/util/Loading";
-import React from "react";
+import React, { useContext, useState, useMemo, useEffect } from "react";
import { PublicUserDocument } from "tachi-common";
-import { ToAPIURL } from "util/api";
-import Divider from "components/util/Divider";
import useSetSubheader from "components/layout/header/useSetSubheader";
+import Card from "components/layout/page/Card";
+import ReactMarkdown from "react-markdown";
+import { UserContext } from "context/UserContext";
+import { Button } from "react-bootstrap";
+import { APIFetchV1 } from "util/api";
+import Divider from "components/util/Divider";
+import Muted from "components/util/Muted";
+import ExternalLink from "components/util/ExternalLink";
-export default function UserPage({ reqUser }: { reqUser: PublicUserDocument }) {
+interface Props {
+ reqUser: PublicUserDocument;
+}
+
+export default function UserPage({ reqUser }: Props) {
useSetSubheader(["Users", reqUser.username], [reqUser], `${reqUser.username}'s Profile`);
- if (!reqUser) {
- return ;
+ return ;
+}
+
+function AboutMeCard({ reqUser }: Props) {
+ const { user } = useContext(UserContext);
+
+ const [editMode, setEditMode] = useState(false);
+ const [content, setContent] = useState(reqUser.about);
+
+ async function SubmitNewAboutMe(text: string) {
+ const res = await APIFetchV1(
+ `/users/${user!.id}`,
+ {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ about: text }),
+ },
+ true,
+ true
+ );
+
+ setEditMode(!res.success);
}
- return <>>;
+ useEffect(() => {
+ if (
+ editMode &&
+ (content === "" || content === "")
+ ) {
+ alert(`1\n\n\n...Just kidding, this is an easter egg :)`);
+ }
+ }, [content]);
+
+ const footer = useMemo(() => {
+ if (user?.id !== reqUser.id) {
+ return <>>;
+ }
+
+ if (!editMode) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ );
+ }, [editMode, reqUser]);
+
+ return (
+
+ {editMode && (
+ <>
+
+ );
}
diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/LeaderboardsPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/LeaderboardsPage.tsx
index 0b4144484..9d5b8d5d7 100644
--- a/client/src/app/pages/dashboard/users/games/_game/_playtype/LeaderboardsPage.tsx
+++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/LeaderboardsPage.tsx
@@ -2,6 +2,7 @@ import ClassBadge from "components/game/ClassBadge";
import useSetSubheader from "components/layout/header/useSetSubheader";
import Card from "components/layout/page/Card";
import MiniTable from "components/tables/components/MiniTable";
+import GentleLink from "components/util/GentleLink";
import LoadingWrapper from "components/util/LoadingWrapper";
import React from "react";
import { useQuery } from "react-query";
@@ -115,7 +116,11 @@ function LeaderboardsPageContent({
/{stats.thisUsersRanking.outOf}
)}
- {userMap.get(s.userID)?.username} |
+
+
+ {userMap.get(s.userID)?.username}
+
+ |
{(s.ratings[gptConfig.defaultProfileRatingAlg] ?? 0).toFixed(2)} |
{/* temp */}
diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/OverviewPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/OverviewPage.tsx
index b2dd4ace5..a00cc2133 100644
--- a/client/src/app/pages/dashboard/users/games/_game/_playtype/OverviewPage.tsx
+++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/OverviewPage.tsx
@@ -43,6 +43,7 @@ import IIDXScoreTable from "components/tables/scores/IIDXScoreTable";
import { NumericSOV } from "util/sorts";
import { ScoreDataset } from "types/tables";
import ScoreTable from "components/tables/scores/ScoreTable";
+import StatShowcase from "components/user/UGPTStatShowcase";
export default function OverviewPage({
reqUser,
@@ -480,339 +481,3 @@ function RankingTimeline({ data }: { data: UGPTHistory }) {
/>
);
}
-
-function StatShowcase({ reqUser, game, playtype }: { reqUser: PublicUserDocument } & GamePT) {
- const { ugs } = useContext(UserGameStatsContext);
- const { user } = useContext(UserContext);
-
- const [projectingStats, setProjectingStats] = useState(false);
-
- const hasUserPlayedGame =
- ugs && !!ugs.filter(e => e.game === game && e.playtype === playtype).length;
-
- const userIsReqUser = user && user.id === reqUser.id;
-
- const shouldFetchThisUserData = hasUserPlayedGame && !userIsReqUser;
-
- return (
-
- ) : null
- }
- >
-
- {projectingStats
- ? `${user!.username}'s Stat Showcase (Projected onto ${
- reqUser.username
- })`
- : `${reqUser.username}'s Stat Showcase`}
-
-
- }
- footer={
-
-
- {hasUserPlayedGame &&
- !userIsReqUser &&
- (projectingStats ? (
-
- Return to {reqUser.username}'s selected stats.
-
- }
- >
- setProjectingStats(false)}
- >
-
-
-
- ) : (
-
- Change the displayed stats to the same ones you use!
-
- }
- >
- setProjectingStats(true)}
- >
-
-
-
- ))}
-
- Evaluate a custom statistic.
-
- }
- >
-
-
-
-
-
-
- }
- >
- {
- const res = await APIFetchV1(
- `/users/${reqUser.id}/games/${game}/${playtype}/showcase${
- projectingStats ? `?projectUser=${user!.id}` : ""
- }`
- );
-
- if (!res.success) {
- throw new Error(res.description);
- }
-
- if (shouldFetchThisUserData) {
- const res2 = await APIFetchV1(
- `/users/${user!.id}/games/${game}/${playtype}/showcase${
- !projectingStats ? `?projectUser=${reqUser.id}` : ""
- }`
- );
-
- if (!res2.success) {
- throw new Error(res2.description);
- }
-
- return { reqUserData: res.body, thisUserData: res2.body };
- }
-
- return { reqUserData: res.body };
- }}
- >
- {data => (
-
- {data.reqUserData.length === 0 ? (
-
-
- {userIsReqUser ? "You have" : "This user has"} no stats
- configured.
-
- {userIsReqUser && (
-
- Why not{" "}
-
- Set Some?
-
-
- )}
-
- ) : (
-
- {data.reqUserData.map((e, i) => (
-
-
-
- ))}
-
- )}
-
- )}
-
-
- );
-}
-
-function StatDelta({
- v1,
- v2,
- mode,
- property,
- game,
- playtype,
-}: {
- v1: number;
- v2?: number;
- mode: "folder" | "chart";
- property: UGPTPreferenceStatsReturn["stat"]["property"];
- game: Game;
- playtype: Playtype;
-}) {
- if (!v2) {
- return null;
- }
-
- let d: string | number = v2 - v1;
- const formattedV2 = FormatValue(game, playtype, mode, property, v2);
- if (property === "percent") {
- d = `${d.toFixed(2)}%`;
- }
-
- let colour;
- if (v2 === v1) {
- colour = "warning";
- } else if (v2 > v1) {
- colour = "success";
- } else {
- colour = "danger";
- }
-
- return (
-
-
- You: {formattedV2} ({v2 > v1 ? `+${d}` : d})
-
-
- );
-}
-
-function FormatValue(
- game: Game,
- playtype: Playtype,
- mode: "folder" | "chart",
- prop: "grade" | "lamp" | "score" | "percent" | "playcount",
- value: number
-) {
- if (mode === "folder") {
- return value;
- }
-
- const gptConfig = GetGamePTConfig(game, playtype);
-
- if (prop === "percent") {
- return `${value.toFixed(2)}%`;
- } else if (prop === "grade") {
- return gptConfig.grades[value];
- } else if (prop === "lamp") {
- return gptConfig.lamps[value];
- }
-
- return value;
-}
-
-function FormatPropertyGTE(
- game: Game,
- playtype: Playtype,
- prop: "grade" | "lamp" | "score" | "percent" | "playcount",
- gte: number
-) {
- const gptConfig = GetGamePTConfig(game, playtype);
-
- if (prop === "grade") {
- return gptConfig.grades[gte];
- } else if (prop === "lamp") {
- return gptConfig.lamps[gte];
- } else if (prop === "score" || prop === "playcount") {
- return gte;
- }
- // else if (prop === "percent") {
- return gte.toFixed(2);
- // }
-}
-
-function StatDisplay({
- statData,
- compareData,
- game,
- playtype,
-}: { statData: UGPTPreferenceStatsReturn; compareData?: UGPTPreferenceStatsReturn } & GamePT) {
- const { stat, value, related } = statData;
-
- if (stat.mode === "chart") {
- const { song, chart } = related as { song: SongDocument; chart: ChartDocument };
-
- return (
- Chart}
- >
- <>
- {FormatChart(game, song, chart)}
-
- {stat.property[0].toUpperCase() + stat.property.substring(1)}:{" "}
- {value.value
- ? stat.property === "percent"
- ? `${value.value.toFixed(2)}%`
- : value.value
- : "No Data."}
-
-
- >
-
- );
- } else if (stat.mode === "folder") {
- const { folders } = related as { folders: FolderDocument[] };
-
- let headerStr;
- if (folders.length === 1) {
- headerStr = folders[0].title;
- } else {
- headerStr = folders.map(e => e.title).join(" and ");
- }
-
- return (
- Folder}
- >
- <>
- {headerStr}
-
- {stat.property[0].toUpperCase() + stat.property.substring(1)} >={" "}
- {FormatPropertyGTE(game, playtype, stat.property, stat.gte)}
-
-
- {value.value}
- {/* @ts-expect-error temp */}
- /{value.outOf}
-
-
-
- >
-
- );
- }
-
- return <>>;
-}
diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/ScoresPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/ScoresPage.tsx
index 422ab3684..0e60ef266 100644
--- a/client/src/app/pages/dashboard/users/games/_game/_playtype/ScoresPage.tsx
+++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/ScoresPage.tsx
@@ -16,7 +16,6 @@ import {
} from "tachi-common";
import { GamePT } from "types/react";
import { APIFetchV1 } from "util/api";
-import IIDXScoreTable from "components/tables/scores/IIDXScoreTable";
import LoadingWrapper from "components/util/LoadingWrapper";
import Icon from "components/util/Icon";
import SelectButton from "components/util/SelectButton";
diff --git a/client/src/app/routes/UserRoutes.tsx b/client/src/app/routes/UserRoutes.tsx
index 43d22dfb4..6d0040876 100644
--- a/client/src/app/routes/UserRoutes.tsx
+++ b/client/src/app/routes/UserRoutes.tsx
@@ -2,6 +2,7 @@ import PlaytypeSelect from "app/pages/dashboard/games/_game/PlaytypeSelect";
import LeaderboardsPage from "app/pages/dashboard/users/games/_game/_playtype/LeaderboardsPage";
import OverviewPage from "app/pages/dashboard/users/games/_game/_playtype/OverviewPage";
import SessionsPage from "app/pages/dashboard/users/games/_game/_playtype/SessionsPage";
+import UserGamesPage from "app/pages/dashboard/users/UserGamesPage";
import { ErrorPage } from "app/pages/ErrorPage";
import RequireAuthAsUserParam from "components/auth/RequireAuthAsUserParam";
import { UGPTBottomNav, UGPTHeaderBody } from "components/user/UGPTHeader";
@@ -92,6 +93,9 @@ function UserProfileRoutes({ reqUser }: { reqUser: PublicUserDocument }) {
+
+
+
Settings Page
diff --git a/client/src/components/layout/misc/BackgroundImage.tsx b/client/src/components/layout/misc/BackgroundImage.tsx
index 3a8b61809..3245f36da 100644
--- a/client/src/components/layout/misc/BackgroundImage.tsx
+++ b/client/src/components/layout/misc/BackgroundImage.tsx
@@ -11,7 +11,7 @@ export default function BackgroundImage() {
style={{
backgroundImage: background
? `url(${background})`
- : `url(${ToServerURL("/cdn/users/default/banner.png")})`,
+ : `url(${ToServerURL("/cdn/splashes/default.png")})`,
}}
>
);
diff --git a/client/src/components/tables/cells/TitleCell.tsx b/client/src/components/tables/cells/TitleCell.tsx
index 71e272f8a..c94f7302d 100644
--- a/client/src/components/tables/cells/TitleCell.tsx
+++ b/client/src/components/tables/cells/TitleCell.tsx
@@ -1,5 +1,5 @@
+import GentleLink from "components/util/GentleLink";
import React from "react";
-import { Link } from "react-router-dom";
import { ChartDocument, SongDocument, Game } from "tachi-common";
import { CreateChartLink } from "util/data";
@@ -18,7 +18,7 @@ export default function TitleCell({
}) {
return (
|
-
+
{song.title}
{!noArtist && (
<>
@@ -26,7 +26,7 @@ export default function TitleCell({
{song.artist}
>
)}
-
+
{comment && (
<>
diff --git a/client/src/components/tables/dropdowns/GenericScoreDropdown.tsx b/client/src/components/tables/dropdowns/GenericScoreDropdown.tsx
index 02a21bd88..c794c9c3e 100644
--- a/client/src/components/tables/dropdowns/GenericScoreDropdown.tsx
+++ b/client/src/components/tables/dropdowns/GenericScoreDropdown.tsx
@@ -20,6 +20,7 @@ import GenericScoreContentDropdown from "./components/GenericScoreContentDropdow
import PlayHistory from "./components/PlayHistory";
import useApiQuery from "components/util/query/useApiQuery";
import HasDevModeOn from "components/util/HasDevModeOn";
+import PBCompare from "./components/PBCompare";
export interface ScoreState {
highlight: boolean;
@@ -91,6 +92,8 @@ export default function GenericScoreDropdown({
body = ;
} else if (view === "moreInfo") {
body = ;
+ } else if (view === "vsPB") {
+ body = ;
}
return (
@@ -102,8 +105,8 @@ export default function GenericScoreDropdown({
This Score
-
- Versus PB
+
+ Chart PB
diff --git a/client/src/components/tables/dropdowns/components/GenericScoreContentDropdown.tsx b/client/src/components/tables/dropdowns/components/GenericScoreContentDropdown.tsx
index 7408bf4d3..595420917 100644
--- a/client/src/components/tables/dropdowns/components/GenericScoreContentDropdown.tsx
+++ b/client/src/components/tables/dropdowns/components/GenericScoreContentDropdown.tsx
@@ -13,7 +13,7 @@ import JudgementTable from "./JudgementTable";
import PBNote from "./PBNote";
import ScoreEditButtons from "./ScoreEditButtons";
-export function ScoreInfo({ score }: { score: ScoreDocument }) {
+export function ScoreInfo({ score }: { score: ScoreDocument | PBScoreDocument }) {
return (
@@ -33,7 +33,8 @@ export function ScoreInfo({ score }: { score: ScoreDocument }) {
grade={score.scoreData.grade}
/>
-
+ {/* @ts-expect-error yeah we know service doesnt necessarily exist */}
+
@@ -47,6 +48,7 @@ export default function GenericScoreContentDropdown({
renderScoreInfo = true,
showSingleScoreNote = false,
GraphComponent = null,
+ forceScoreData = false,
pbData,
}: {
score: ScoreDocument | PBScoreDocument;
@@ -54,6 +56,7 @@ export default function GenericScoreContentDropdown({
renderScoreInfo?: boolean;
showSingleScoreNote?: boolean;
pbData: UGPTChartPBComposition;
+ forceScoreData?: boolean;
GraphComponent?:
| (({ score }: { score: ScoreDocument | PBScoreDocument }) => JSX.Element)
| null;
@@ -97,6 +100,7 @@ export default function GenericScoreContentDropdown({
>
) : (
+ {forceScoreData && !showSingleScoreNote && }
e.comment)
diff --git a/client/src/components/tables/dropdowns/components/PBCompare.tsx b/client/src/components/tables/dropdowns/components/PBCompare.tsx
new file mode 100644
index 000000000..b4a434fcb
--- /dev/null
+++ b/client/src/components/tables/dropdowns/components/PBCompare.tsx
@@ -0,0 +1,21 @@
+import React from "react";
+import { ScoreDocument, PBScoreDocument, IDStrings } from "tachi-common";
+import { UGPTChartPBComposition } from "types/api-returns";
+import { ScoreState } from "../GenericScoreDropdown";
+
+export default function PBCompare({
+ data,
+ scoreState,
+ DocComponent,
+}: {
+ data: UGPTChartPBComposition;
+ scoreState: ScoreState;
+ DocComponent: (props: {
+ score: ScoreDocument | PBScoreDocument;
+ scoreState: ScoreState;
+ pbData: UGPTChartPBComposition;
+ forceScoreData: boolean;
+ }) => JSX.Element;
+}) {
+ return ;
+}
diff --git a/client/src/components/user/UGPTHeader.tsx b/client/src/components/user/UGPTHeader.tsx
index c3603a801..712e751ba 100644
--- a/client/src/components/user/UGPTHeader.tsx
+++ b/client/src/components/user/UGPTHeader.tsx
@@ -19,6 +19,8 @@ import ClassBadge from "components/game/ClassBadge";
import { FormatGPTRating, UppercaseFirst } from "util/misc";
import ProfileBadges from "./ProfileBadges";
import { GameClassSets } from "tachi-common/js/game-classes";
+import UGPTRatingsTable from "./UGPTStatsOverview";
+import RankingData from "./UGPTRankingData";
export function UGPTHeaderBody({
reqUser,
@@ -73,44 +75,7 @@ export function UGPTHeaderBody({
-
- <>
- {(Object.keys(
- gptConfig.classHumanisedFormat
- ) as GameClassSets[IDStrings][]).map(k => (
-
- | {UppercaseFirst(k)} |
-
- {stats.gameStats.classes[k] ? (
-
- ) : (
- "No Data"
- )}
- |
-
- ))}
- {Object.entries(stats.gameStats.ratings).map(([k, v]) => (
-
- | {UppercaseFirst(k)} |
-
- {FormatGPTRating(
- game,
- playtype,
- k as ScoreCalculatedDataLookup[IDStrings],
- v
- )}
- |
-
- ))}
- >
-
+
@@ -119,20 +84,6 @@ export function UGPTHeaderBody({
);
}
-function RankingData({ ranking, outOf }: { ranking: number; outOf: number }) {
- return (
-
-
- Ranking
-
-
- #{ranking}
- /{outOf}
-
-
- );
-}
-
export function UGPTBottomNav({ baseUrl }: { baseUrl: string }) {
return (
diff --git a/client/src/components/user/UGPTRankingData.tsx b/client/src/components/user/UGPTRankingData.tsx
new file mode 100644
index 000000000..c901cd6bb
--- /dev/null
+++ b/client/src/components/user/UGPTRankingData.tsx
@@ -0,0 +1,15 @@
+import React from "react";
+
+export default function RankingData({ ranking, outOf }: { ranking: number; outOf: number }) {
+ return (
+
+
+ Ranking
+
+
+ #{ranking}
+ /{outOf}
+
+
+ );
+}
diff --git a/client/src/components/user/UGPTStatShowcase.tsx b/client/src/components/user/UGPTStatShowcase.tsx
new file mode 100644
index 000000000..0a2ca26e4
--- /dev/null
+++ b/client/src/components/user/UGPTStatShowcase.tsx
@@ -0,0 +1,363 @@
+import Card from "components/layout/page/Card";
+import CardHeader from "components/layout/page/CardHeader";
+import CardNavButton from "components/layout/page/CardNavButton";
+import AsyncLoader from "components/util/AsyncLoader";
+import { UserContext } from "context/UserContext";
+import { UserGameStatsContext } from "context/UserGameStatsContext";
+import { nanoid } from "nanoid";
+import React, { useContext, useState } from "react";
+import { OverlayTrigger, Tooltip } from "react-bootstrap";
+import { Link } from "react-router-dom";
+import {
+ PublicUserDocument,
+ Game,
+ GetGamePTConfig,
+ SongDocument,
+ ChartDocument,
+ FormatChart,
+ FolderDocument,
+} from "tachi-common";
+import { UGPTPreferenceStatsReturn } from "types/api-returns";
+import { GamePT } from "types/react";
+import { Playtype } from "types/tachi";
+import { APIFetchV1 } from "util/api";
+
+export default function StatShowcase({
+ reqUser,
+ game,
+ playtype,
+}: { reqUser: PublicUserDocument } & GamePT) {
+ const { ugs } = useContext(UserGameStatsContext);
+ const { user } = useContext(UserContext);
+
+ const [projectingStats, setProjectingStats] = useState(false);
+
+ const hasUserPlayedGame =
+ ugs && !!ugs.filter(e => e.game === game && e.playtype === playtype).length;
+
+ const userIsReqUser = user && user.id === reqUser.id;
+
+ const shouldFetchThisUserData = hasUserPlayedGame && !userIsReqUser;
+
+ return (
+
+ ) : null
+ }
+ >
+
+ {projectingStats
+ ? `${user!.username}'s Stat Showcase (Projected onto ${
+ reqUser.username
+ })`
+ : `${reqUser.username}'s Stat Showcase`}
+
+
+ }
+ footer={
+
+
+ {hasUserPlayedGame &&
+ !userIsReqUser &&
+ (projectingStats ? (
+
+ Return to {reqUser.username}'s selected stats.
+
+ }
+ >
+ setProjectingStats(false)}
+ >
+
+
+
+ ) : (
+
+ Change the displayed stats to the same ones you use!
+
+ }
+ >
+ setProjectingStats(true)}
+ >
+
+
+
+ ))}
+
+ Evaluate a custom statistic.
+
+ }
+ >
+
+
+
+
+
+
+ }
+ >
+ {
+ const res = await APIFetchV1(
+ `/users/${reqUser.id}/games/${game}/${playtype}/showcase${
+ projectingStats ? `?projectUser=${user!.id}` : ""
+ }`
+ );
+
+ if (!res.success) {
+ throw new Error(res.description);
+ }
+
+ if (shouldFetchThisUserData) {
+ const res2 = await APIFetchV1(
+ `/users/${user!.id}/games/${game}/${playtype}/showcase${
+ !projectingStats ? `?projectUser=${reqUser.id}` : ""
+ }`
+ );
+
+ if (!res2.success) {
+ throw new Error(res2.description);
+ }
+
+ return { reqUserData: res.body, thisUserData: res2.body };
+ }
+
+ return { reqUserData: res.body };
+ }}
+ >
+ {data => (
+
+ {data.reqUserData.length === 0 ? (
+
+
+ {userIsReqUser ? "You have" : "This user has"} no stats
+ configured.
+
+ {userIsReqUser && (
+
+ Why not{" "}
+
+ Set Some?
+
+
+ )}
+
+ ) : (
+
+ {data.reqUserData.map((e, i) => (
+
+
+
+ ))}
+
+ )}
+
+ )}
+
+
+ );
+}
+
+function StatDelta({
+ v1,
+ v2,
+ mode,
+ property,
+ game,
+ playtype,
+}: {
+ v1: number;
+ v2?: number;
+ mode: "folder" | "chart";
+ property: UGPTPreferenceStatsReturn["stat"]["property"];
+ game: Game;
+ playtype: Playtype;
+}) {
+ if (!v2) {
+ return null;
+ }
+
+ let d: string | number = v2 - v1;
+ const formattedV2 = FormatValue(game, playtype, mode, property, v2);
+ if (property === "percent") {
+ d = `${d.toFixed(2)}%`;
+ }
+
+ let colour;
+ if (v2 === v1) {
+ colour = "warning";
+ } else if (v2 > v1) {
+ colour = "success";
+ } else {
+ colour = "danger";
+ }
+
+ return (
+
+
+ You: {formattedV2} ({v2 > v1 ? `+${d}` : d})
+
+
+ );
+}
+
+function FormatValue(
+ game: Game,
+ playtype: Playtype,
+ mode: "folder" | "chart",
+ prop: "grade" | "lamp" | "score" | "percent" | "playcount",
+ value: number
+) {
+ if (mode === "folder") {
+ return value;
+ }
+
+ const gptConfig = GetGamePTConfig(game, playtype);
+
+ if (prop === "percent") {
+ return `${value.toFixed(2)}%`;
+ } else if (prop === "grade") {
+ return gptConfig.grades[value];
+ } else if (prop === "lamp") {
+ return gptConfig.lamps[value];
+ }
+
+ return value;
+}
+
+function FormatPropertyGTE(
+ game: Game,
+ playtype: Playtype,
+ prop: "grade" | "lamp" | "score" | "percent" | "playcount",
+ gte: number
+) {
+ const gptConfig = GetGamePTConfig(game, playtype);
+
+ if (prop === "grade") {
+ return gptConfig.grades[gte];
+ } else if (prop === "lamp") {
+ return gptConfig.lamps[gte];
+ } else if (prop === "score" || prop === "playcount") {
+ return gte;
+ }
+ // else if (prop === "percent") {
+ return gte.toFixed(2);
+ // }
+}
+
+function StatDisplay({
+ statData,
+ compareData,
+ game,
+ playtype,
+}: { statData: UGPTPreferenceStatsReturn; compareData?: UGPTPreferenceStatsReturn } & GamePT) {
+ const { stat, value, related } = statData;
+
+ if (stat.mode === "chart") {
+ const { song, chart } = related as { song: SongDocument; chart: ChartDocument };
+
+ return (
+ Chart}
+ >
+ <>
+ {FormatChart(game, song, chart)}
+
+ {stat.property[0].toUpperCase() + stat.property.substring(1)}:{" "}
+ {value.value
+ ? stat.property === "percent"
+ ? `${value.value.toFixed(2)}%`
+ : value.value
+ : "No Data."}
+
+
+ >
+
+ );
+ } else if (stat.mode === "folder") {
+ const { folders } = related as { folders: FolderDocument[] };
+
+ let headerStr;
+ if (folders.length === 1) {
+ headerStr = folders[0].title;
+ } else {
+ headerStr = folders.map(e => e.title).join(" and ");
+ }
+
+ return (
+ Folder}
+ >
+ <>
+ {headerStr}
+
+ {stat.property[0].toUpperCase() + stat.property.substring(1)} >={" "}
+ {FormatPropertyGTE(game, playtype, stat.property, stat.gte)}
+
+
+ {value.value}
+ {/* @ts-expect-error temp */}
+ /{value.outOf}
+
+
+
+ >
+
+ );
+ }
+
+ return <>>;
+}
diff --git a/client/src/components/user/UGPTStatsOverview.tsx b/client/src/components/user/UGPTStatsOverview.tsx
new file mode 100644
index 000000000..1d0025cfe
--- /dev/null
+++ b/client/src/components/user/UGPTStatsOverview.tsx
@@ -0,0 +1,52 @@
+import ClassBadge from "components/game/ClassBadge";
+import MiniTable from "components/tables/components/MiniTable";
+import React from "react";
+import { GetGamePTConfig, IDStrings, ScoreCalculatedDataLookup, UserGameStats } from "tachi-common";
+import { GameClassSets } from "tachi-common/js/game-classes";
+import { UppercaseFirst, FormatGPTRating } from "util/misc";
+import { MillisToSince } from "util/time";
+
+export default function UGPTRatingsTable({ ugs }: { ugs: UserGameStats }) {
+ const gptConfig = GetGamePTConfig(ugs.game, ugs.playtype);
+
+ return (
+
+ <>
+ {(Object.keys(gptConfig.classHumanisedFormat) as GameClassSets[IDStrings][]).map(
+ k => (
+
+ | {UppercaseFirst(k)} |
+
+ {ugs.classes[k] ? (
+
+ ) : (
+ "No Data"
+ )}
+ |
+
+ )
+ )}
+ {Object.entries(ugs.ratings).map(([k, v]) => (
+
+ | {UppercaseFirst(k)} |
+
+ {FormatGPTRating(
+ ugs.game,
+ ugs.playtype,
+ k as ScoreCalculatedDataLookup[IDStrings],
+ v
+ )}
+ |
+
+ ))}
+ >
+
+ );
+}
diff --git a/client/src/components/user/UserHeader.tsx b/client/src/components/user/UserHeader.tsx
index e784fd973..581a53f68 100644
--- a/client/src/components/user/UserHeader.tsx
+++ b/client/src/components/user/UserHeader.tsx
@@ -6,6 +6,7 @@ import Navbar from "components/nav/Navbar";
import NavItem from "components/nav/NavItem";
import ProfileBadges from "./ProfileBadges";
import { UserContext } from "context/UserContext";
+import Muted from "components/util/Muted";
export function UserHeaderBody({ reqUser }: { reqUser: PublicUserDocument }) {
return (
@@ -21,6 +22,10 @@ export function UserHeaderBody({ reqUser }: { reqUser: PublicUserDocument }) {
+ {reqUser.about}
+
+ UserID: {reqUser.id}
+
>
);
}
@@ -38,7 +43,7 @@ export function UserBottomNav({
const navItems = [
- About
+ Overview
,
Games
diff --git a/client/src/components/util/GentleLink.tsx b/client/src/components/util/GentleLink.tsx
new file mode 100644
index 000000000..938993dbe
--- /dev/null
+++ b/client/src/components/util/GentleLink.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import { Link } from "react-router-dom";
+import { JustChildren } from "types/react";
+
+export default function GentleLink({
+ to,
+ children,
+}: {
+ to: string;
+ className?: string;
+} & JustChildren) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/client/src/components/util/Muted.tsx b/client/src/components/util/Muted.tsx
new file mode 100644
index 000000000..836b79ef7
--- /dev/null
+++ b/client/src/components/util/Muted.tsx
@@ -0,0 +1,6 @@
+import React from "react";
+import { JustChildren } from "types/react";
+
+export default function Muted({ children }: JustChildren) {
+ return {children};
+}
diff --git a/client/src/types/api-returns.ts b/client/src/types/api-returns.ts
index 575475c94..4fa93f0ec 100644
--- a/client/src/types/api-returns.ts
+++ b/client/src/types/api-returns.ts
@@ -80,3 +80,7 @@ export interface SpecificSessionReturns {
charts: ChartDocument[];
user: PublicUserDocument;
}
+
+export type UGSWithRankingData = UserGameStats & {
+ __rankingData: { outOf: number; ranking: number };
+};
|