User Page works

This commit is contained in:
zkldi
2021-08-27 08:38:57 +01:00
parent 4a8f2e3969
commit 8d0fdab607
21 changed files with 1153 additions and 408 deletions
+1
View File
@@ -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",
+449 -1
View File
@@ -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
@@ -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 (
<div className="row">
<AsyncLoader
promiseFn={async () => {
const res = await APIFetchV1<UGSWithRankingData[]>(
`/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 (
<div
className={`col-12 col-md-${mdSize} col-lg-${lgSize}`}
key={`${e.game}:${e.playtype}`}
>
<GameStatContainer ugs={e} reqUser={reqUser} />
</div>
);
})
}
</AsyncLoader>
</div>
);
}
function GameStatContainer({ ugs, reqUser }: { ugs: UGSWithRankingData } & Props) {
return (
<Card
className="mb-4"
footer={
<div className="d-flex justify-content-end">
<LinkButton
to={`/dashboard/users/${reqUser.username}/games/${ugs.game}/${ugs.playtype}`}
>
View Game Profile
</LinkButton>
</div>
}
header={FormatGame(ugs.game, ugs.playtype)}
>
<UGPTRatingsTable ugs={ugs} />
<RankingData ranking={ugs.__rankingData.ranking} outOf={ugs.__rankingData.outOf} />
</Card>
);
}
@@ -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 <Loading />;
return <AboutMeCard reqUser={reqUser} />;
}
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 === "<script>alert(1)</script>" || content === "<script>alert(1);</script>")
) {
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 (
<div className="d-flex justify-content-end">
<Button onClick={() => setEditMode(true)} variant="info">
Edit
</Button>
</div>
);
}
return (
<div className="d-flex">
<Button className="mr-auto" onClick={() => setEditMode(false)} variant="secondary">
Cancel
</Button>
<Button
className="ml-auto"
onClick={() => SubmitNewAboutMe(content)}
variant="success"
>
Submit
</Button>
</div>
);
}, [editMode, reqUser]);
return (
<Card header="About Me" footer={footer}>
{editMode && (
<>
<textarea
style={{ height: 200 }}
className="w-100"
value={content}
onChange={e => setContent(e.target.value)}
/>
<Muted>
You can use{" "}
<ExternalLink href="https://www.markdownguide.org/basic-syntax/">
markdown
</ExternalLink>{" "}
here!
</Muted>
<Divider className="mt-4 mb-4" />
</>
)}
<ReactMarkdown>{content}</ReactMarkdown>
</Card>
);
}
@@ -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({
<small className="text-muted">/{stats.thisUsersRanking.outOf}</small>
)}
</td>
<td>{userMap.get(s.userID)?.username}</td>
<td>
<GentleLink to={`/dashboard/users/${userMap.get(s.userID)?.username}`}>
{userMap.get(s.userID)?.username}
</GentleLink>
</td>
<td>{(s.ratings[gptConfig.defaultProfileRatingAlg] ?? 0).toFixed(2)}</td>
{/* temp */}
<td>
@@ -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 (
<Card
className="card-dark"
header={
<CardHeader
rightContent={
userIsReqUser ? (
<CardNavButton
type="cog"
to={`/dashboard/users/${
user!.username
}/games/${game}/${playtype}/settings`}
hoverText="Modify your statistics showcase."
/>
) : null
}
>
<h3>
{projectingStats
? `${user!.username}'s Stat Showcase (Projected onto ${
reqUser.username
})`
: `${reqUser.username}'s Stat Showcase`}
</h3>
</CardHeader>
}
footer={
<div className="d-flex w-100 justify-content-center">
<div className="btn-group">
{hasUserPlayedGame &&
!userIsReqUser &&
(projectingStats ? (
<OverlayTrigger
placement="top"
overlay={
<Tooltip id="quick-panel-tooltip">
Return to {reqUser.username}'s selected stats.
</Tooltip>
}
>
<div
className="btn btn-success"
onClick={() => setProjectingStats(false)}
>
<i className="fas fa-sync" style={{ paddingRight: 0 }} />
</div>
</OverlayTrigger>
) : (
<OverlayTrigger
placement="top"
overlay={
<Tooltip id={nanoid()}>
Change the displayed stats to the same ones you use!
</Tooltip>
}
>
<div
className="btn btn-outline-secondary"
onClick={() => setProjectingStats(true)}
>
<i className="fas fa-sync" style={{ paddingRight: 0 }} />
</div>
</OverlayTrigger>
))}
<OverlayTrigger
placement="top"
overlay={
<Tooltip id="quick-panel-tooltip">
Evaluate a custom statistic.
</Tooltip>
}
>
<div className="btn btn-outline-secondary">
<i className="fas fa-file-signature" style={{ paddingRight: 0 }} />
</div>
</OverlayTrigger>
</div>
</div>
}
>
<AsyncLoader
promiseFn={async () => {
const res = await APIFetchV1<UGPTPreferenceStatsReturn[]>(
`/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<UGPTPreferenceStatsReturn[]>(
`/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 => (
<div className="container">
{data.reqUserData.length === 0 ? (
<div className="row">
<div className="col-12 text-center">
{userIsReqUser ? "You have" : "This user has"} no stats
configured.
</div>
{userIsReqUser && (
<div className="col-12 mt-2 text-center">
Why not{" "}
<Link
to={`/dashboard/users/${
user!.username
}/games/${game}/${playtype}/settings`}
>
Set Some?
</Link>
</div>
)}
</div>
) : (
<div className="row justify-content-center">
{data.reqUserData.map((e, i) => (
<div
key={nanoid()}
className="col-12 col-md-4 d-flex align-items-stretch mt-8"
>
<StatDisplay
statData={e}
compareData={
data.thisUserData ? data.thisUserData[i] : undefined
}
game={game}
playtype={playtype}
/>
</div>
))}
</div>
)}
</div>
)}
</AsyncLoader>
</Card>
);
}
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 (
<div className={`mt-2 text-${colour}`}>
<span>
You: {formattedV2} ({v2 > v1 ? `+${d}` : d})
</span>
</div>
);
}
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 (
<Card
className="text-center stat-overview-card w-100"
header={<h5 className="text-muted mb-0">Chart</h5>}
>
<>
<h4>{FormatChart(game, song, chart)}</h4>
<h4>
{stat.property[0].toUpperCase() + stat.property.substring(1)}:{" "}
{value.value
? stat.property === "percent"
? `${value.value.toFixed(2)}%`
: value.value
: "No Data."}
</h4>
<StatDelta
v1={statData.value.value}
v2={compareData?.value.value}
mode={stat.mode}
property={stat.property}
game={game}
playtype={playtype}
/>
</>
</Card>
);
} 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 (
<Card
className="text-center stat-overview-card w-100"
header={<h5 className="text-muted mb-0">Folder</h5>}
>
<>
<h4>{headerStr}</h4>
<h5>
{stat.property[0].toUpperCase() + stat.property.substring(1)} &gt;={" "}
{FormatPropertyGTE(game, playtype, stat.property, stat.gte)}
</h5>
<h4>
{value.value}
{/* @ts-expect-error temp */}
<small className="text-muted">/{value.outOf}</small>
</h4>
<StatDelta
v1={statData.value.value}
v2={compareData?.value.value}
mode={stat.mode}
property={stat.property}
game={game}
playtype={playtype}
/>
</>
</Card>
);
}
return <></>;
}
@@ -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";
+4
View File
@@ -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 }) {
<Route exact path="/dashboard/users/:userID">
<UserPage reqUser={reqUser} />
</Route>
<Route exact path="/dashboard/users/:userID/games">
<UserGamesPage reqUser={reqUser} />
</Route>
<Route exact path="/dashboard/users/:userID/settings">
<RequireAuthAsUserParam>Settings Page</RequireAuthAsUserParam>
</Route>
@@ -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")})`,
}}
></div>
);
@@ -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 (
<td style={{ textAlign: "left", minWidth: "140px", maxWidth: "300px" }}>
<Link className="song-title-link" to={CreateChartLink(chart, game)}>
<GentleLink to={CreateChartLink(chart, game)}>
{song.title}
{!noArtist && (
<>
@@ -26,7 +26,7 @@ export default function TitleCell({
<small>{song.artist}</small>
</>
)}
</Link>
</GentleLink>
{comment && (
<>
<br />
@@ -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<I extends IDStrings = IDStrings>({
body = <DebugContent data={data} />;
} else if (view === "moreInfo") {
body = <DocComponent score={thisScore as any} scoreState={scoreState} pbData={data} />;
} else if (view === "vsPB") {
body = <PBCompare data={data} DocComponent={DocComponent} scoreState={scoreState} />;
}
return (
@@ -102,8 +105,8 @@ export default function GenericScoreDropdown<I extends IDStrings = IDStrings>({
This Score
</SelectButton>
<SelectButton setValue={setView} value={view} id="vsPB">
<Icon type="balance-scale-right" />
Versus PB
<Icon type="trophy" />
Chart PB
</SelectButton>
<SelectButton setValue={setView} value={view} id="history">
<Icon type="history" />
@@ -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 (
<div className="col-12">
<table className="table">
@@ -33,7 +33,8 @@ export function ScoreInfo({ score }: { score: ScoreDocument }) {
grade={score.scoreData.grade}
/>
<LampCell score={score} />
<TimestampCell time={score.timeAchieved} service={score.service} />
{/* @ts-expect-error yeah we know service doesnt necessarily exist */}
<TimestampCell time={score.timeAchieved} service={score?.service} />
</tr>
</tbody>
</table>
@@ -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({
</>
) : (
<div className="col-12 align-self-end">
{forceScoreData && !showSingleScoreNote && <ScoreInfo score={score} />}
<CommentContainer
comment={pbData.scores
.map(e => e.comment)
@@ -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<I extends IDStrings>({
data,
scoreState,
DocComponent,
}: {
data: UGPTChartPBComposition<I>;
scoreState: ScoreState;
DocComponent: (props: {
score: ScoreDocument<I> | PBScoreDocument<I>;
scoreState: ScoreState;
pbData: UGPTChartPBComposition;
forceScoreData: boolean;
}) => JSX.Element;
}) {
return <DocComponent score={data.pb} pbData={data} scoreState={scoreState} forceScoreData />;
}
+3 -52
View File
@@ -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({
</MiniTable>
</div>
<div className="col-12 col-md-6 col-lg-3">
<MiniTable className="table-sm text-center" headers={["Player Stats"]} colSpan={2}>
<>
{(Object.keys(
gptConfig.classHumanisedFormat
) as GameClassSets[IDStrings][]).map(k => (
<tr key={k}>
<td>{UppercaseFirst(k)}</td>
<td>
{stats.gameStats.classes[k] ? (
<ClassBadge
showSetOnHover={false}
key={`${k}:${stats.gameStats.classes[k]}`}
game={game}
playtype={playtype}
classSet={k}
classValue={stats.gameStats.classes[k]!}
/>
) : (
"No Data"
)}
</td>
</tr>
))}
{Object.entries(stats.gameStats.ratings).map(([k, v]) => (
<tr key={k}>
<td>{UppercaseFirst(k)}</td>
<td>
{FormatGPTRating(
game,
playtype,
k as ScoreCalculatedDataLookup[IDStrings],
v
)}
</td>
</tr>
))}
</>
</MiniTable>
<UGPTRatingsTable ugs={stats.gameStats} />
</div>
<div className="col-12 col-lg-3">
<RankingData ranking={stats.rankingData.ranking} outOf={stats.rankingData.outOf} />
@@ -119,20 +84,6 @@ export function UGPTHeaderBody({
);
}
function RankingData({ ranking, outOf }: { ranking: number; outOf: number }) {
return (
<div className="row text-center">
<div className="col-12">
<h4>Ranking</h4>
</div>
<div className="col-12">
<strong className="display-4">#{ranking}</strong>
<span className="text-muted">/{outOf}</span>
</div>
</div>
);
}
export function UGPTBottomNav({ baseUrl }: { baseUrl: string }) {
return (
<div className="row align-items-center mb-0">
@@ -0,0 +1,15 @@
import React from "react";
export default function RankingData({ ranking, outOf }: { ranking: number; outOf: number }) {
return (
<div className="row text-center">
<div className="col-12">
<h4>Ranking</h4>
</div>
<div className="col-12">
<strong className="display-4">#{ranking}</strong>
<span className="text-muted">/{outOf}</span>
</div>
</div>
);
}
@@ -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 (
<Card
className="card-dark"
header={
<CardHeader
rightContent={
userIsReqUser ? (
<CardNavButton
type="edit"
to={`/dashboard/users/${
user!.username
}/games/${game}/${playtype}/settings`}
hoverText="Modify your statistics showcase."
/>
) : null
}
>
<h3>
{projectingStats
? `${user!.username}'s Stat Showcase (Projected onto ${
reqUser.username
})`
: `${reqUser.username}'s Stat Showcase`}
</h3>
</CardHeader>
}
footer={
<div className="d-flex w-100 justify-content-center">
<div className="btn-group">
{hasUserPlayedGame &&
!userIsReqUser &&
(projectingStats ? (
<OverlayTrigger
placement="top"
overlay={
<Tooltip id="quick-panel-tooltip">
Return to {reqUser.username}'s selected stats.
</Tooltip>
}
>
<div
className="btn btn-success"
onClick={() => setProjectingStats(false)}
>
<i className="fas fa-sync" style={{ paddingRight: 0 }} />
</div>
</OverlayTrigger>
) : (
<OverlayTrigger
placement="top"
overlay={
<Tooltip id={nanoid()}>
Change the displayed stats to the same ones you use!
</Tooltip>
}
>
<div
className="btn btn-outline-secondary"
onClick={() => setProjectingStats(true)}
>
<i className="fas fa-sync" style={{ paddingRight: 0 }} />
</div>
</OverlayTrigger>
))}
<OverlayTrigger
placement="top"
overlay={
<Tooltip id="quick-panel-tooltip">
Evaluate a custom statistic.
</Tooltip>
}
>
<div className="btn btn-outline-secondary">
<i className="fas fa-file-signature" style={{ paddingRight: 0 }} />
</div>
</OverlayTrigger>
</div>
</div>
}
>
<AsyncLoader
promiseFn={async () => {
const res = await APIFetchV1<UGPTPreferenceStatsReturn[]>(
`/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<UGPTPreferenceStatsReturn[]>(
`/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 => (
<div className="container">
{data.reqUserData.length === 0 ? (
<div className="row">
<div className="col-12 text-center">
{userIsReqUser ? "You have" : "This user has"} no stats
configured.
</div>
{userIsReqUser && (
<div className="col-12 mt-2 text-center">
Why not{" "}
<Link
to={`/dashboard/users/${
user!.username
}/games/${game}/${playtype}/settings`}
>
Set Some?
</Link>
</div>
)}
</div>
) : (
<div className="row justify-content-center">
{data.reqUserData.map((e, i) => (
<div
key={nanoid()}
className="col-12 col-md-4 d-flex align-items-stretch mt-8"
>
<StatDisplay
statData={e}
compareData={
data.thisUserData ? data.thisUserData[i] : undefined
}
game={game}
playtype={playtype}
/>
</div>
))}
</div>
)}
</div>
)}
</AsyncLoader>
</Card>
);
}
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 (
<div className={`mt-2 text-${colour}`}>
<span>
You: {formattedV2} ({v2 > v1 ? `+${d}` : d})
</span>
</div>
);
}
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 (
<Card
className="text-center stat-overview-card w-100"
header={<h5 className="text-muted mb-0">Chart</h5>}
>
<>
<h4>{FormatChart(game, song, chart)}</h4>
<h4>
{stat.property[0].toUpperCase() + stat.property.substring(1)}:{" "}
{value.value
? stat.property === "percent"
? `${value.value.toFixed(2)}%`
: value.value
: "No Data."}
</h4>
<StatDelta
v1={statData.value.value}
v2={compareData?.value.value}
mode={stat.mode}
property={stat.property}
game={game}
playtype={playtype}
/>
</>
</Card>
);
} 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 (
<Card
className="text-center stat-overview-card w-100"
header={<h5 className="text-muted mb-0">Folder</h5>}
>
<>
<h4>{headerStr}</h4>
<h5>
{stat.property[0].toUpperCase() + stat.property.substring(1)} &gt;={" "}
{FormatPropertyGTE(game, playtype, stat.property, stat.gte)}
</h5>
<h4>
{value.value}
{/* @ts-expect-error temp */}
<small className="text-muted">/{value.outOf}</small>
</h4>
<StatDelta
v1={statData.value.value}
v2={compareData?.value.value}
mode={stat.mode}
property={stat.property}
game={game}
playtype={playtype}
/>
</>
</Card>
);
}
return <></>;
}
@@ -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 (
<MiniTable className="table-sm text-center" headers={["Player Stats"]} colSpan={2}>
<>
{(Object.keys(gptConfig.classHumanisedFormat) as GameClassSets[IDStrings][]).map(
k => (
<tr key={k}>
<td>{UppercaseFirst(k)}</td>
<td>
{ugs.classes[k] ? (
<ClassBadge
showSetOnHover={false}
key={`${k}:${ugs.classes[k]}`}
game={ugs.game}
playtype={ugs.playtype}
classSet={k}
classValue={ugs.classes[k]!}
/>
) : (
"No Data"
)}
</td>
</tr>
)
)}
{Object.entries(ugs.ratings).map(([k, v]) => (
<tr key={k}>
<td>{UppercaseFirst(k)}</td>
<td>
{FormatGPTRating(
ugs.game,
ugs.playtype,
k as ScoreCalculatedDataLookup[IDStrings],
v
)}
</td>
</tr>
))}
</>
</MiniTable>
);
}
+6 -1
View File
@@ -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 }) {
<Divider className="mt-4 mb-4" />
</div>
</div>
<div className="col-12 col-lg-6 d-flex justify-content-center">{reqUser.about}</div>
<div className="col-12 col-lg-3 d-flex justify-content-center">
<Muted>UserID: {reqUser.id}</Muted>
</div>
</>
);
}
@@ -38,7 +43,7 @@ export function UserBottomNav({
const navItems = [
<NavItem key="about" to={`${baseUrl}/`}>
About
Overview
</NavItem>,
<NavItem key="games" to={`${baseUrl}/games`}>
Games
+17
View File
@@ -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 (
<Link to={to} className="gentle-link">
{children}
</Link>
);
}
+6
View File
@@ -0,0 +1,6 @@
import React from "react";
import { JustChildren } from "types/react";
export default function Muted({ children }: JustChildren) {
return <small className="text-muted">{children}</small>;
}
+4
View File
@@ -80,3 +80,7 @@ export interface SpecificSessionReturns<I extends IDStrings = IDStrings> {
charts: ChartDocument<I>[];
user: PublicUserDocument;
}
export type UGSWithRankingData<I extends IDStrings = IDStrings> = UserGameStats<I> & {
__rankingData: { outOf: number; ranking: number };
};