mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-29 02:18:09 +03:00
User Page works
This commit is contained in:
@@ -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",
|
||||
|
||||
Generated
+449
-1
@@ -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)} >={" "}
|
||||
{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";
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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)} >={" "}
|
||||
{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,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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user