diff --git a/.prettierrc.js b/.prettierrc.js
new file mode 100644
index 00000000..2d18ac52
--- /dev/null
+++ b/.prettierrc.js
@@ -0,0 +1,15 @@
+module.exports = {
+ printWidth: 80,
+ singleQuote: true,
+ trailingComma: 'all',
+ arrowParens: 'always',
+ // semi: false,
+ overrides: [
+ {
+ files: '*.yaml',
+ options: {
+ singleQuote: false,
+ },
+ },
+ ],
+};
diff --git a/.prettierrc.json b/.prettierrc.json
deleted file mode 100644
index 97d80aaa..00000000
--- a/.prettierrc.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "printWidth": 80,
- "singleQuote": true,
- "trailingComma": "all",
- "arrowParens": "always",
- "overrides": [
- {
- "files": "*.yaml",
- "options": {
- "singleQuote": false
- }
- }
- ]
-}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 198c7d37..19538c44 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,186 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+## 0.2.0 (2022-02-28)
+
+### Features
+
+- add , refactor fetching ([110300a](https://github.com/MetaFam/TheGame/commit/110300a2c4ffda5161c502895394511570d29de2))
+- add a couple of modals ([86ac996](https://github.com/MetaFam/TheGame/commit/86ac996a31d8a12b859399e45e9fe23606e8b586))
+- add additional roles: Bridgebuilding, Rainmaking, Videomaking ([ae18b82](https://github.com/MetaFam/TheGame/commit/ae18b82887831d23c4a2b1212f6e54af61b98946))
+- add api route for fetching data ([5cfa859](https://github.com/MetaFam/TheGame/commit/5cfa859d50c7a2d87ed375a299fbcc0b4f4f61bf))
+- add cards ([29fe89e](https://github.com/MetaFam/TheGame/commit/29fe89e2135bfb123ff7e59f7d13fad3f124aa08))
+- add confirmation modal to reset/cancel actions ([f3a2631](https://github.com/MetaFam/TheGame/commit/f3a26311584c9059c422f060af436f13fea09698))
+- add dashboard link and icons to profile menu ([f661dca](https://github.com/MetaFam/TheGame/commit/f661dcaa46db0caa85c722f5f2b2465fa2a9eff6))
+- add faq section ([64811fe](https://github.com/MetaFam/TheGame/commit/64811fe1c962f7e2bf587d2953e8fb802ecdd203))
+- add footer ([22c8ba2](https://github.com/MetaFam/TheGame/commit/22c8ba251100b6e3682800b41011fed9be753cb0))
+- add guests ([96ea0e4](https://github.com/MetaFam/TheGame/commit/96ea0e481c74218bfe1214aaafca963cffc85ea8))
+- add guild box types and refactor player box types ([4a8a6f1](https://github.com/MetaFam/TheGame/commit/4a8a6f1d2c5f2b34f5706bc99efca006014cc760))
+- add location ([25c824e](https://github.com/MetaFam/TheGame/commit/25c824e2443700197dd7f747705b216643e20717))
+- add megamenu demo link ([c8b1b04](https://github.com/MetaFam/TheGame/commit/c8b1b04ff8ce1b2f8e6f9259c8645e19084d9aeb))
+- add polygon and xdai chains when pulling DAO membership ([0b3b254](https://github.com/MetaFam/TheGame/commit/0b3b254cc05cae3f9cd14d4ae5722e9123197481))
+- add polygon and xdai chains when pulling DAO membership ([b2d53b0](https://github.com/MetaFam/TheGame/commit/b2d53b06b67970292c1f4b9a6b75b7ac1ff611e4))
+- add pronouns input to edit form ([d164de0](https://github.com/MetaFam/TheGame/commit/d164de09c537e79e10e086b42594d97fe32e0afb))
+- add role selector to create quest form ([a74c059](https://github.com/MetaFam/TheGame/commit/a74c059bb7cf3ba48c256bc2c81bbcb1bf3bd2b6))
+- add roles to /quests cards ([ff5f5b8](https://github.com/MetaFam/TheGame/commit/ff5f5b86109452ae2626ea09ad241c2c7b0875f1))
+- add WTF is XP section, fix descriptions ([29561d8](https://github.com/MetaFam/TheGame/commit/29561d81e9d4c85ad0659d27588732e26e45ecfa))
+- added link in profile to grid layout ([8bd80db](https://github.com/MetaFam/TheGame/commit/8bd80dba7bfc8565b2527760e0fca662f9a0aa9e))
+- added opensea api key to gcp-deploy ([f3eb221](https://github.com/MetaFam/TheGame/commit/f3eb2213e3e3724f56b268ad9fd0a67349464d0b))
+- allow multiple embedded links ([843ebfd](https://github.com/MetaFam/TheGame/commit/843ebfd3a2c648ea4e91cbd0a7056a8a97c1d365))
+- better error handling in opensea hook ([3c7c590](https://github.com/MetaFam/TheGame/commit/3c7c590dce63e19d615324857b126c865f621dd9))
+- better loading states ([da68a8b](https://github.com/MetaFam/TheGame/commit/da68a8b1455b0f7fe3929a6d07d13a3c1439b8da))
+- can edit roles from profile page ([aba2e51](https://github.com/MetaFam/TheGame/commit/aba2e513f33084c5eee2aac888b11342ab1cdff9))
+- can edit roles from profile page ([e82952b](https://github.com/MetaFam/TheGame/commit/e82952b438c63f77cf135853b73a51a1b26a346e))
+- default layout button ([05fabc0](https://github.com/MetaFam/TheGame/commit/05fabc0994e1a00773539743335f107f8acac30b))
+- determine timezone based on user ([ab0119e](https://github.com/MetaFam/TheGame/commit/ab0119e66665eda3b05d770bcc2e1c01027d476d))
+- direct user to dashboard on MetaLink click ([b7a1296](https://github.com/MetaFam/TheGame/commit/b7a1296bcf40d0c0fb1ca0a07cceba5e6a3ef542))
+- display roles on /quest/id card ([dbeffb1](https://github.com/MetaFam/TheGame/commit/dbeffb1dbb415c03cad2c33e4cd8fe8153655910))
+- dynamic resizing of grid items ([5ab027b](https://github.com/MetaFam/TheGame/commit/5ab027b0f301cadc21b46a2b3c2522598ca647d7))
+- embedded url profile section ([fe03537](https://github.com/MetaFam/TheGame/commit/fe03537e8ee1e51d7dfa2f3c4462949e1333471e))
+- fetch rss data from anchor.fm -> conver to json ([2f158e2](https://github.com/MetaFam/TheGame/commit/2f158e25ec7036c283999fdeea0dfb8855b6ec55))
+- fetching metagame rss feed ([41e83d2](https://github.com/MetaFam/TheGame/commit/41e83d25e7d25e7a3637cd2468531a271e59c31d))
+- filter by roles ([59afe6f](https://github.com/MetaFam/TheGame/commit/59afe6f6dc132c988f1c91d31f756feac23ac70f))
+- get calendar data into the app ([e7e4cd1](https://github.com/MetaFam/TheGame/commit/e7e4cd109fe9a44ea25b21ba2f0347efda40552e))
+- google data api imported ([835819a](https://github.com/MetaFam/TheGame/commit/835819a41255db696dc4b5bd6014012e88f019fc))
+- iframes for invest section items ([3a9d8e3](https://github.com/MetaFam/TheGame/commit/3a9d8e37d1c296b1afa1d69780c1527c8c5deb26))
+- iframes for the all possible learn section items ([b49d0ee](https://github.com/MetaFam/TheGame/commit/b49d0ee24a0466185808bb064e991add6e7df719))
+- implement opening of events in iframe ([278dc93](https://github.com/MetaFam/TheGame/commit/278dc9394a2e23990de4ce41c000a6923eb0b4d5))
+- implement the rest of modals ([b7c38ae](https://github.com/MetaFam/TheGame/commit/b7c38ae24d56e167c989fa28a1d05c7dab070dc4))
+- install date-fns ([87498f6](https://github.com/MetaFam/TheGame/commit/87498f61084c72d1c22b9cb12bb6423cb82a2022))
+- install standard version ([c1115ab](https://github.com/MetaFam/TheGame/commit/c1115ab9136e7116148144ef053b6fa6d6dccb01))
+- install swr and fetch data with it ([5e7f6bb](https://github.com/MetaFam/TheGame/commit/5e7f6bb2aa96c8e7ae3dc81f2c835a093725110a))
+- landing ( Improved file structure for landing page and tidied improrts ([a222977](https://github.com/MetaFam/TheGame/commit/a2229779060dc19565b863fc27eafaf3543430f6))
+- latest read section added ([6e5fabe](https://github.com/MetaFam/TheGame/commit/6e5fabe41ccedadbfa06e70c00b5c589c5a29e8d))
+- lazy loading added for watch section of latest content ([e6e3ce7](https://github.com/MetaFam/TheGame/commit/e6e3ce729944f4758a5bdc38046292c4bcb16370))
+- least surprise in failures ([04a4ca5](https://github.com/MetaFam/TheGame/commit/04a4ca53d6e00706064204017bad99ec97447206))
+- least surprise in failures ([1063e24](https://github.com/MetaFam/TheGame/commit/1063e245c829cda7c3292814dfc9cee85b47e135))
+- make list items into links ([08198ec](https://github.com/MetaFam/TheGame/commit/08198ecdcb7eec8f8a50c4bbdb5fd9ef2305c0c2))
+- make whole card clickable ([7bf049e](https://github.com/MetaFam/TheGame/commit/7bf049ecf58e68e7a9b5b6f932ec2a0cb271b3ab))
+- moved to react-grid-layout on player/[username] ([901e1cb](https://github.com/MetaFam/TheGame/commit/901e1cb21d3ecfe3380615a4ff6388b7069fe565))
+- new file added for shared components ([4a5f7f8](https://github.com/MetaFam/TheGame/commit/4a5f7f82a08862f0a1e6cfabd3cc270ca77ef130))
+- open modal on clicking learn more ([870d79c](https://github.com/MetaFam/TheGame/commit/870d79c82690a94cfc64d4b75a273ef30b2fbd8f))
+- organise the pages structure ([fbbc1da](https://github.com/MetaFam/TheGame/commit/fbbc1da0f404ea0058ca9c47ad8dea7c6e6462eb))
+- persisting profile layout changes in hasura ([19f3b7f](https://github.com/MetaFam/TheGame/commit/19f3b7fac2dae373ade906171ea63054b7f84c5b))
+- persisting profile layout changes in hasura ([b00fc2a](https://github.com/MetaFam/TheGame/commit/b00fc2a7e807388d5c733f484d479e59719bcaeb))
+- player profile in react-grid-layout ([f0d7ad6](https://github.com/MetaFam/TheGame/commit/f0d7ad61fe2d346e3d7d16f433c567e5420ac8d3))
+- podcast player + title + description ([b3a99c3](https://github.com/MetaFam/TheGame/commit/b3a99c35621119f4b3d57d2ab1cc6858b438c209))
+- profile layout edit + section add/remove ([c76839f](https://github.com/MetaFam/TheGame/commit/c76839f05636914fc0edf844cecb51f3ad985017))
+- remove old hacky calendar, use default google iframe calendar ([fb6ebf4](https://github.com/MetaFam/TheGame/commit/fb6ebf4691b7d828176215251b2ea0bfadc1f44f))
+- remove old seeds pages, set up new page ([006087b](https://github.com/MetaFam/TheGame/commit/006087bf9d59097ff4b09256c7ffba06407d96f3))
+- save roles to backend ([9859116](https://github.com/MetaFam/TheGame/commit/9859116d4a81a5207462d07edf6f42d09331ad07))
+- Seed section pulling in data from CoinGecko ([f6fe5aa](https://github.com/MetaFam/TheGame/commit/f6fe5aacb65657e541f51c7d119560f68637574f))
+- set default cover image for guild ([afb54fb](https://github.com/MetaFam/TheGame/commit/afb54fb31a4bcbef0ca01cf8d1a7a3bc0d85b4ba))
+- set real calendar data + slight refactor ([aedb9c9](https://github.com/MetaFam/TheGame/commit/aedb9c9d381648785db9f0eebb45d87c08c66a0f))
+- set up dashboard page ([690bad1](https://github.com/MetaFam/TheGame/commit/690bad19a04e2359f72404e9ccf1c1b01447c718))
+- set up tabs components ([616a865](https://github.com/MetaFam/TheGame/commit/616a865eb0eb86bb0cc1776c4d387e3275be053b))
+- show DAOs in a modal and match NFT gallery styling ([62376d0](https://github.com/MetaFam/TheGame/commit/62376d0f9b647a57ce9715ac03abd954d6bca737))
+- show reset to default button only when changed ([84c707c](https://github.com/MetaFam/TheGame/commit/84c707c4c03e3ebc4917704eb25cb445d61b7189))
+- sort filter added for leaderboard ([16de20f](https://github.com/MetaFam/TheGame/commit/16de20fda0b1c4dc70d82290d1882fa9622740a0))
+- support for editing roles on quests ([2d9669a](https://github.com/MetaFam/TheGame/commit/2d9669a5b5c0749bd8bc2b2742204e19c6d763b7))
+- temporary profile editor with beginning sections ([c7fadef](https://github.com/MetaFam/TheGame/commit/c7fadef207291ce0c145772d511a85ef16aaffcb))
+- wip: mobile seeds page ([3facb7b](https://github.com/MetaFam/TheGame/commit/3facb7bd5ff1f59e39db8e531f0a97694c75852c))
+- working on profile editing form for primary modal ([06b360b](https://github.com/MetaFam/TheGame/commit/06b360b1a76881d99310cd73515cff53735ee701))
+- working on profile editing form for primary modal ([c295171](https://github.com/MetaFam/TheGame/commit/c2951715485f70788789960ed66f75f1f4a33bbe))
+
+### Bug Fixes
+
+- add field to update quest ([239f000](https://github.com/MetaFam/TheGame/commit/239f0009c11bbfb9bc7f9d791536b08799631490))
+- add field to update quest ([b590cb7](https://github.com/MetaFam/TheGame/commit/b590cb7785670840c147576e1efacee15bb36749))
+- add loaders to the remaining possible iframes ([c01f8fd](https://github.com/MetaFam/TheGame/commit/c01f8fd4ad2131e2677af5b739725eb9f7ecd45c))
+- add migration for new roles & isBasic -> basic ([6635363](https://github.com/MetaFam/TheGame/commit/663536348c9b33bf6d29dbb57de3307be0712e0d))
+- added 404 page if player not found ([9c8615b](https://github.com/MetaFam/TheGame/commit/9c8615b5cb80746c72312a0b7d8f9c9aea017d21))
+- added padding on top ([cf8338c](https://github.com/MetaFam/TheGame/commit/cf8338cf1108de2b34949324158c69e1b6f4fada))
+- after rebase ([d54838f](https://github.com/MetaFam/TheGame/commit/d54838fc94dd825f015312b022f12f5665ad1c17))
+- alec's corrections ([16ee324](https://github.com/MetaFam/TheGame/commit/16ee3246fbe27d5f462f44918ca561afe526243b))
+- alec's corrections pt. 1 ([1021cf2](https://github.com/MetaFam/TheGame/commit/1021cf2bd757df1530ee18e586323b25df2880b9))
+- availabilty input bug fixed ([90c9963](https://github.com/MetaFam/TheGame/commit/90c9963487cb19ab71dd708b2c6fceea483d7c51))
+- better meta tags for pages ([5684b8a](https://github.com/MetaFam/TheGame/commit/5684b8a73017dfeed5f2f2335a19a2aa5003b19e))
+- bug when clicking the only selected value in filter ([d152e19](https://github.com/MetaFam/TheGame/commit/d152e19d136466868d31771932dbfd4dd1ef163d))
+- bugs in EmbedUrl + SetupRoles ([0c9bec1](https://github.com/MetaFam/TheGame/commit/0c9bec1752148fb824866668c5b35b4acca3b95a))
+- bugs in EmbedUrl + SetupRoles ([3047a5f](https://github.com/MetaFam/TheGame/commit/3047a5f663f1d08d63f3c17e82c326031239aa25))
+- bugs in sort/filter ui ([507f1bf](https://github.com/MetaFam/TheGame/commit/507f1bfaf58d95fc6d14e605070394b07f3cb464))
+- capitalisation and disabling button when no user ([ff8929b](https://github.com/MetaFam/TheGame/commit/ff8929b4746f38ad23650c9ff30896c082448d2e))
+- catch opensea errors + disable player achievements section ([63579d8](https://github.com/MetaFam/TheGame/commit/63579d849523a89a8ae659499c33bdf22a5adf31))
+- change the metagame logo navigation to /dashboard ([685fc91](https://github.com/MetaFam/TheGame/commit/685fc91115fa40b5dfb8847705aac635a4a9e864))
+- clean up username page ([54222f3](https://github.com/MetaFam/TheGame/commit/54222f36675ce9be58bcb50fc28a677c035c108d))
+- create clients once ([8cfd347](https://github.com/MetaFam/TheGame/commit/8cfd347a48c8fc9897109564f23e8ea769737328))
+- create clients once ([3dda8aa](https://github.com/MetaFam/TheGame/commit/3dda8aa7ca9eac6676fe4accf275c6edfef10253))
+- dao memberships without title are also displayed ([bd453ba](https://github.com/MetaFam/TheGame/commit/bd453ba39cc7cfa8cb2475a5dea3a7f74ed90b2b))
+- delete the pages which won't load iframes ([f044dff](https://github.com/MetaFam/TheGame/commit/f044dffcaab3824dd66b7242af9684aebbb405f8))
+- deselect all options should search for all roles ([4ed66b9](https://github.com/MetaFam/TheGame/commit/4ed66b9d281e6134ddcad98745fccfaac4f404cf))
+- disable adding same embedded url in onAddBox ([b91538f](https://github.com/MetaFam/TheGame/commit/b91538fc0a522196b483970a56433637c2fc1d23))
+- display message only when loading is complete ([af4ca2f](https://github.com/MetaFam/TheGame/commit/af4ca2f5e142ca5403e3775c8141f3776aad1f03))
+- displayed HTML hex strings as emojis ([8175050](https://github.com/MetaFam/TheGame/commit/8175050ce3fd59f2842e405b3cb86c37dc6498a0))
+- don't redirect to /dashboard yet ([755a273](https://github.com/MetaFam/TheGame/commit/755a2731b0b1e44ba0f61fdca21b45972a9976f1))
+- embedded url text gap ([b8ab03b](https://github.com/MetaFam/TheGame/commit/b8ab03b051f3e7500eed3175c13afed3e5f0f9bf))
+- eslint & fetching layout from user on login ([981d3a9](https://github.com/MetaFam/TheGame/commit/981d3a93e9171877fb6b46363123c18686009a23))
+- even more lint ([f323729](https://github.com/MetaFam/TheGame/commit/f323729161fc5eae099dfba287cf34bb7d4a5c9b))
+- even more lint ([a3aa2d4](https://github.com/MetaFam/TheGame/commit/a3aa2d4eb22f165a177b9e58e21298fe0be40238))
+- exclude ./tests from linting ([40de5fb](https://github.com/MetaFam/TheGame/commit/40de5fb9b6c0dc41f650ba892c0f19574dc4555c))
+- fade animation in profile sections ([df9899b](https://github.com/MetaFam/TheGame/commit/df9899bd8c98f23587cf1bca798426716a8a0d2b))
+- fixed default player profile layout ([c0eacb6](https://github.com/MetaFam/TheGame/commit/c0eacb6673b3178a4dd8b20df4aa4c00f94ab58e))
+- fixed default player profile layout ([0a0cb89](https://github.com/MetaFam/TheGame/commit/0a0cb890d24c3ea0ee56b6b3f92eed3de38fa5da))
+- fixed high level layouting ([d9dff14](https://github.com/MetaFam/TheGame/commit/d9dff14760bcc7df2b5976a328c0d48c7c108745))
+- fixed high level layouting ([bb67c95](https://github.com/MetaFam/TheGame/commit/bb67c95a5f5728eb60bb80dd75bb278ce2fab2a9))
+- heights not updating when adding new box ([f04b02f](https://github.com/MetaFam/TheGame/commit/f04b02f8c5c1f000458198ee23e926577532bd75))
+- hide quests demo app link ([e7d9dc7](https://github.com/MetaFam/TheGame/commit/e7d9dc73b45d13de76203f1916e27d6a90720640))
+- ignore "object is of type unknown" lint message ([17ce45e](https://github.com/MetaFam/TheGame/commit/17ce45ed6041f69b4982251038706867929cbcc7))
+- inputs bugs on editprofile form ([cb84da4](https://github.com/MetaFam/TheGame/commit/cb84da4a10827723acb0d8cd48ee35352210c8fb))
+- intersection observer bug ([37e047b](https://github.com/MetaFam/TheGame/commit/37e047b62b0f075e40213296fa06f92fee28513c))
+- landing page ui issues ([1c92f63](https://github.com/MetaFam/TheGame/commit/1c92f63b9f527293abc4059525090df078450084))
+- lint ([5f2a44b](https://github.com/MetaFam/TheGame/commit/5f2a44bc88b4285519b5f6a54a7db1f46352ea98))
+- lint error ([3791611](https://github.com/MetaFam/TheGame/commit/3791611fd5e07de8577f06bfa7b7a606f02d0054))
+- lint error ([f441179](https://github.com/MetaFam/TheGame/commit/f441179be2abb3a6fe24641190c1c5f38a09b761))
+- lint error ([873379a](https://github.com/MetaFam/TheGame/commit/873379a70bb1bd27889770614ce623b83a0ca8cf))
+- load component on client ([f79f6d0](https://github.com/MetaFam/TheGame/commit/f79f6d0a90f66a6af39a4345dc65268be12afce9))
+- login issues ([ac5f3fe](https://github.com/MetaFam/TheGame/commit/ac5f3fe98e9fc4f8729b5779e6f0ed7d727dcb53))
+- me page ([8ec8e33](https://github.com/MetaFam/TheGame/commit/8ec8e336b40f6ed02a057d372e6ee397fed68c13))
+- megamenu padding issues ([fd9b039](https://github.com/MetaFam/TheGame/commit/fd9b039149576504106b59d62eeada863e39b114))
+- minor bugs in ui ([4e0ea9b](https://github.com/MetaFam/TheGame/commit/4e0ea9b8c108dc6731e2f42d0c558fbe86054184))
+- minor ui bugs ([4f70351](https://github.com/MetaFam/TheGame/commit/4f703515759614da5276d50f49bc809e995a9661))
+- missing roles ([5b61955](https://github.com/MetaFam/TheGame/commit/5b619553d478741d5ded02b752ce9a49cbdd98b5))
+- mistakes were made ([fc4e30f](https://github.com/MetaFam/TheGame/commit/fc4e30f07469b82ed385b1c352c510b97eaf1ea7))
+- more lint ([6f7915e](https://github.com/MetaFam/TheGame/commit/6f7915e55c8028f6bb0ea379de613c4ab35b158a))
+- move discord-bot RUNTIME_ENV to the last stage ([4f1e475](https://github.com/MetaFam/TheGame/commit/4f1e47562ccf2b65d2d24aa80002e19abf5cd5de))
+- moved opensea to api & fixed env var ([c36c675](https://github.com/MetaFam/TheGame/commit/c36c67504f008fed67ffb28af3a3f118f69fc0d5))
+- navaigating between profiles and edit button ([bdabf65](https://github.com/MetaFam/TheGame/commit/bdabf657a54edd6491417176159ea35953019d66))
+- order of steps ([3063828](https://github.com/MetaFam/TheGame/commit/30638286c6cc9367edceda94121fb837db94be8c))
+- pin nock version ([47d4a01](https://github.com/MetaFam/TheGame/commit/47d4a01660187a010797ba440fdfcc1c424c0914))
+- position of menu in relation to the triangle ([6003009](https://github.com/MetaFam/TheGame/commit/6003009befece8f78d73219619bacbc5ce6d5b5b))
+- prefetching personalityInfo on profile page ([32a6405](https://github.com/MetaFam/TheGame/commit/32a6405de490c6d26ebd61deabda56f3d1bdcbeb))
+- prevent rendering loop ([4b0d196](https://github.com/MetaFam/TheGame/commit/4b0d1964d4020a513424c3b1443c78efd71e4463))
+- rebasing/merging problem ([3013f55](https://github.com/MetaFam/TheGame/commit/3013f559522c153c1c29fa4584ce4833e575ba90))
+- reduced profile padding top ([8dcfd16](https://github.com/MetaFam/TheGame/commit/8dcfd1678658a5d81aff051af8569b3dcaddff54))
+- remove jstz, get timezone with Intl ([4f7d0ef](https://github.com/MetaFam/TheGame/commit/4f7d0ef770368ccb4f38c5602cff98c9f2640e32))
+- remove nextjs-cors package ([5fef811](https://github.com/MetaFam/TheGame/commit/5fef8118f1e9e47d9674a683124cd2dafad2375b))
+- removed verify on brightId button ([52372f9](https://github.com/MetaFam/TheGame/commit/52372f9deef043cc62feed5da82c2245262d7593))
+- removing unnecessary dropdown options ([cc41fb7](https://github.com/MetaFam/TheGame/commit/cc41fb77eee757a00ac37eb60c31039cee8623ee))
+- replace next/image with BoxedNextImage ([a7ab1ea](https://github.com/MetaFam/TheGame/commit/a7ab1ea5b09a6c541042292b82212e5523fc7dd0))
+- revert formatting changes ([42aba14](https://github.com/MetaFam/TheGame/commit/42aba14546cb183d5a50c33e4a718ed82c42af25))
+- revert the @graphql-tools/schema -> graphql-tools ([3189399](https://github.com/MetaFam/TheGame/commit/318939926cb1f9c73ca80e393dfcedb0860f532c))
+- reverted un-intended changes ([648d595](https://github.com/MetaFam/TheGame/commit/648d5954240e4a8baa615afa86fe41080c883eda))
+- review changes ([a39c0c4](https://github.com/MetaFam/TheGame/commit/a39c0c42c1714d6819b52425cd7cf61fcc4d1858))
+- s-s rendered page is trying to access an unavailable component ([f01abfc](https://github.com/MetaFam/TheGame/commit/f01abfce77664ee0a8a4effcbc5a6eee696740d8))
+- set a default DAO title ([62d37b0](https://github.com/MetaFam/TheGame/commit/62d37b0ea8972d99341e71294d1d75ca3f1b15ef))
+- set a default DAO title ([6c20a59](https://github.com/MetaFam/TheGame/commit/6c20a59549c01ef7752fc10502738740b7908f0a))
+- set up redirects ([dd56741](https://github.com/MetaFam/TheGame/commit/dd56741b2dc1592b7fb220c62a5445cf2d63caa8))
+- setting intial state using useState ([f4fda80](https://github.com/MetaFam/TheGame/commit/f4fda8054acc1d55b91e8b5424f56f72cb5472c9))
+- Setup Header Images ([70950eb](https://github.com/MetaFam/TheGame/commit/70950eba8393ddc8775365a6e46cab0d63e977c6))
+- sort out unknown types ([5ff716c](https://github.com/MetaFam/TheGame/commit/5ff716c1b6463737d8946602359f64a5fe047c95))
+- strip html from description ([9a6fd74](https://github.com/MetaFam/TheGame/commit/9a6fd74e9627e66e8e96fa333689421eff140b50))
+- style roles buttons ([9f47e72](https://github.com/MetaFam/TheGame/commit/9f47e7267858e7d6e1330ce05616c7dd16485d1b))
+- temporarily disabled BrightId ([9c8cb63](https://github.com/MetaFam/TheGame/commit/9c8cb636e246845426c99bfa007b351d2c9230c6))
+- temporarily disabled BrightId ([61dbec5](https://github.com/MetaFam/TheGame/commit/61dbec52342aadbeaf509df76c635b68427e7c7f))
+- triangular menu icon position and width ([a3a3081](https://github.com/MetaFam/TheGame/commit/a3a308165e65a1d1135b59716e6bfff1a81b9f48))
+- typo ([499d031](https://github.com/MetaFam/TheGame/commit/499d0312155297eb678fa3e04fe849be1477ae7b))
+- update schema ([4840b7d](https://github.com/MetaFam/TheGame/commit/4840b7dfd6adcf964693f33c0ff5b2ad5b26bec8))
+- Updated bot help text ([df35d33](https://github.com/MetaFam/TheGame/commit/df35d33b36f17cda29a345d424f96a9696bd29c6))
+- updated interface for video ([4a07840](https://github.com/MetaFam/TheGame/commit/4a078406863ff3cc236776e933c743f4f64941dc))
+- use console.warn ([76e4205](https://github.com/MetaFam/TheGame/commit/76e42054586453ec7d49efec04e2f7028b4a868a))
+- use req object ([79dc6f9](https://github.com/MetaFam/TheGame/commit/79dc6f9e505fe67251ff3839f6f38d7bc1357ce2))
+- useEffect ([6562835](https://github.com/MetaFam/TheGame/commit/65628356b3de25ca0b077ed97c40f941365dfbbf))
+- useEffect dependency ([5c6e886](https://github.com/MetaFam/TheGame/commit/5c6e886b2b9c9448f20465413101566527d42fac))
+
## 0.1.0 (2021-12-02)
### Features
diff --git a/hasura/metadata/actions.graphql b/hasura/metadata/actions.graphql
index bf92547c..277e2a8a 100644
--- a/hasura/metadata/actions.graphql
+++ b/hasura/metadata/actions.graphql
@@ -141,9 +141,7 @@ type DiscordGuildAuthResponse {
}
type CacheProcessOutput {
- success : Boolean!
- queued : Boolean!
- error : String
+ updateIDXProfile : uuid
}
type ExpiredPlayerProfiles {
diff --git a/hasura/metadata/actions.yaml b/hasura/metadata/actions.yaml
index 269e81d5..bae864d8 100644
--- a/hasura/metadata/actions.yaml
+++ b/hasura/metadata/actions.yaml
@@ -36,7 +36,6 @@ actions:
definition:
kind: asynchronous
handler: '{{ACTION_BASE_ENDPOINT}}/idxCache/updateSingle'
- forward_client_headers: true
permissions:
- role: player
- role: public
diff --git a/hasura/migrations/1644880076911_clear_profile_layouts/up.sql b/hasura/migrations/1644880076911_clear_profile_layouts/up.sql
new file mode 100644
index 00000000..735e3f7f
--- /dev/null
+++ b/hasura/migrations/1644880076911_clear_profile_layouts/up.sql
@@ -0,0 +1,5 @@
+-- shortened the names of some variables and the old versions
+-- need to be removed
+UPDATE public.player
+ SET profile_layout = NULL
+;
diff --git a/hasura/migrations/1645463938163_guarantee_players_exist_in_the_profile_table/up.sql b/hasura/migrations/1645463938163_guarantee_players_exist_in_the_profile_table/up.sql
new file mode 100644
index 00000000..dd78c231
--- /dev/null
+++ b/hasura/migrations/1645463938163_guarantee_players_exist_in_the_profile_table/up.sql
@@ -0,0 +1,6 @@
+INSERT INTO profile (player_id)
+ SELECT id FROM player
+ WHERE id NOT IN (
+ SELECT player_id FROM profile
+ )
+;
diff --git a/package.json b/package.json
index 15cf656d..a1d2d5f7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@metafam/the-game",
- "version": "0.1.0",
+ "version": "0.2.0",
"license": "GPL-3.0",
"engines": {
"node": ">=12"
@@ -12,6 +12,7 @@
"docker:build": "docker-compose up --build -d",
"docker:stop": "docker-compose down",
"docker:clean": "docker-compose down -v",
+ "docker:debug": "COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose up --build",
"build": "lerna run build",
"web:dev": "lerna run dev --parallel --scope @metafam/web --include-dependencies",
"web:build": "lerna run build --scope @metafam/web --include-dependencies --stream",
@@ -21,10 +22,11 @@
"hasura:console": "yarn hasura console --no-browser",
"hasura:migrate:init": "yarn hasura migrate create \"init\" --from-server",
"hasura:seed-db": "node hasura/seed-db.mjs",
- "test": "lerna run test --parallel --",
"generate": "lerna run generate --parallel --",
+ "test": "lerna run test --parallel --",
"test:full": "yarn lint && yarn typecheck && yarn test",
"clean": "lerna clean",
+ "clean:full": "lerna clean && rm -rfv node_modules/ packages/*/node_modules/ packages/*/dist/ packages/web/.next/",
"format": "prettier --write \"{*,**/*}.{ts,tsx,js,jsx,json,yml,yaml,md}\"",
"lint": "eslint --ignore-path .gitignore \"./packages/**/*.{ts,tsx,js,jsx}\"",
"typecheck": "lerna run typecheck",
diff --git a/packages/backend/src/handlers/actions/idxCache/updateSingle.ts b/packages/backend/src/handlers/actions/idxCache/updateSingle.ts
index 6b1b48c2..38c4d829 100644
--- a/packages/backend/src/handlers/actions/idxCache/updateSingle.ts
+++ b/packages/backend/src/handlers/actions/idxCache/updateSingle.ts
@@ -80,8 +80,6 @@ export default async (playerId: string): Promise => {
basicProfile = await store.get('basicProfile', did);
}
- // This isn't called if they haven't created a mainnet DID
- // This should be checked even without a DID
if (!basicProfile) {
basicProfile = await getLegacy3BoxProfileAsBasicProfile(ethereumAddress);
}
diff --git a/packages/backend/src/handlers/remote-schemas/resolvers/daohaus/resolver.ts b/packages/backend/src/handlers/remote-schemas/resolvers/daohaus/resolver.ts
index c4c8472d..8914f8f9 100644
--- a/packages/backend/src/handlers/remote-schemas/resolvers/daohaus/resolver.ts
+++ b/packages/backend/src/handlers/remote-schemas/resolvers/daohaus/resolver.ts
@@ -12,7 +12,7 @@ const addChain = (memberAddress: string) => async (chain: string) => {
const metadataForDaos = await Promise.all(
members.map(async ({ moloch: { id } }) => {
- console.log('fetching metadata for ', id);
+ console.log(`Fetching DAO Metadata For: ${id}`);
const response = await fetch(`${CONFIG.daoHausMetadataUrl}/${id}`);
const metadataArr = response.ok
? ((await response.json()) as DaoMetadata[])
@@ -33,13 +33,14 @@ const addChain = (memberAddress: string) => async (chain: string) => {
const metadata: DaoMetadata =
metadataByContract[updatedMember.molochAddress];
+
updatedMember.moloch.title = metadata?.name;
- if (metadata?.avatarImg) {
- const imgUrl = metadata.avatarImg.startsWith('Qm')
- ? `ipfs://${metadata.avatarImg}`
- : metadata.avatarImg;
- updatedMember.moloch.avatarUrl = imageLink(imgUrl);
+
+ let imgURL = metadata?.avatarImg;
+ if (imgURL?.startsWith('Qm')) {
+ imgURL = `ipfs://${imgURL}`;
}
+ updatedMember.moloch.avatarURL = imageLink(imgURL);
return updatedMember;
});
diff --git a/packages/backend/src/handlers/remote-schemas/typeDefs.ts b/packages/backend/src/handlers/remote-schemas/typeDefs.ts
index 91154103..bee8d8df 100644
--- a/packages/backend/src/handlers/remote-schemas/typeDefs.ts
+++ b/packages/backend/src/handlers/remote-schemas/typeDefs.ts
@@ -35,7 +35,7 @@ export const typeDefs = gql`
chain: String!
title: String
version: String
- avatarUrl: String
+ avatarURL: String
}
type Member {
diff --git a/packages/design-system/src/MetaButton.tsx b/packages/design-system/src/MetaButton.tsx
index 3f918f0e..0e21cacf 100644
--- a/packages/design-system/src/MetaButton.tsx
+++ b/packages/design-system/src/MetaButton.tsx
@@ -16,8 +16,7 @@ export const MetaButton: React.FC<
fontSize="sm"
bg="purple.400"
color="white"
- {...{ ref }}
- {...props}
+ {...{ ref, ...props }}
>
{children}
diff --git a/packages/design-system/src/MetaTag.tsx b/packages/design-system/src/MetaTag.tsx
index 94529575..1e5d1137 100644
--- a/packages/design-system/src/MetaTag.tsx
+++ b/packages/design-system/src/MetaTag.tsx
@@ -1,14 +1,14 @@
import { Tag, TagProps } from '@chakra-ui/react';
import React from 'react';
-export const MetaTag: React.FC = React.forwardRef(
+export const MetaTag = React.forwardRef(
({ children, ...props }, ref) => (
{children}
diff --git a/packages/design-system/src/SelectTimeZone.tsx b/packages/design-system/src/SelectTimeZone.tsx
index 731aa9f0..b99d14ab 100644
--- a/packages/design-system/src/SelectTimeZone.tsx
+++ b/packages/design-system/src/SelectTimeZone.tsx
@@ -1,6 +1,6 @@
/* istanbul ignore file */
-import { Maybe } from '@metafam/utils';
+import { Maybe, Optional } from '@metafam/utils';
import cityTimeZones from 'city-timezones';
import React, { useCallback, useState } from 'react';
import TimeZoneSelect, {
@@ -38,9 +38,9 @@ export interface TimeZoneSelectProps extends Record {
const timeZoneSelectStyles: typeof chakraesqueStyles = {
...chakraesqueStyles,
- control: (styles, props) => ({
+ container: (styles, props) => ({
...styles,
- ...chakraesqueStyles.control?.(styles, props),
+ ...chakraesqueStyles.container?.(styles, props),
width: '100%',
maxWidth: 'calc(100vw - 2rem)',
}),
@@ -112,7 +112,7 @@ export const TimeZoneOptions: TimeZoneType[] = Object.entries(i18nTimeZones)
export const timeZonesFilter = (
search: string,
- cityZones: string[] | undefined = undefined,
+ cityZones: Optional> = undefined,
) => (tz: TimeZoneType): boolean => {
if (!cityZones) {
// eslint-disable-next-line no-param-reassign
diff --git a/packages/design-system/src/StatusedSubmitButton.tsx b/packages/design-system/src/StatusedSubmitButton.tsx
index c784293a..e0bb6f0e 100644
--- a/packages/design-system/src/StatusedSubmitButton.tsx
+++ b/packages/design-system/src/StatusedSubmitButton.tsx
@@ -1,23 +1,24 @@
-import { Flex, Spinner, Text } from '@chakra-ui/react';
+import { ButtonProps, Flex, Spinner, Text } from '@chakra-ui/react';
import { Maybe } from '@metafam/utils';
import React, { ReactElement } from 'react';
import { MetaButton } from './MetaButton';
-export const StatusedSubmitButton = ({
- label = 'Submit',
- status = null,
- ...props
-}: {
- label?: Maybe;
+type StatusedSubmitProps = {
+ label?: Maybe;
status?: Maybe;
-}) => (
+};
+
+export const StatusedSubmitButton: React.FC<
+ StatusedSubmitProps & ButtonProps
+> = ({ label = 'Submit', status = null, ...props }) => (
{status == null ? (
diff --git a/packages/design-system/src/ViewAllButton.tsx b/packages/design-system/src/ViewAllButton.tsx
new file mode 100644
index 00000000..1397d2ce
--- /dev/null
+++ b/packages/design-system/src/ViewAllButton.tsx
@@ -0,0 +1,28 @@
+import { Text } from '@chakra-ui/react';
+import React from 'react';
+
+export const ViewAllButton: React.FC<{
+ onClick: () => void;
+ size?: string | number;
+}> = ({ onClick, size }) => (
+
+ View All{size != null ? ` (${size})` : null}
+
+);
+
+export default ViewAllButton;
diff --git a/packages/design-system/src/icons/ChainIcon.tsx b/packages/design-system/src/icons/ChainIcon.tsx
index eb1b06d6..0e53aaee 100644
--- a/packages/design-system/src/icons/ChainIcon.tsx
+++ b/packages/design-system/src/icons/ChainIcon.tsx
@@ -1,4 +1,5 @@
import { IconProps } from '@chakra-ui/icons';
+import { Tooltip } from '@chakra-ui/react';
import React from 'react';
import { EthereumIcon } from './EthereumIcon';
@@ -6,12 +7,23 @@ import { PolygonIcon } from './PolygonIcon';
import { XDaiIcon } from './XDaiIcon';
type Props = {
- chain: string | undefined;
+ chain?: string;
};
export const ChainIcon: React.FC = ({ chain, ...props }) => {
- if (chain?.toLowerCase().includes('xdai')) return ;
- if (chain?.toLowerCase().includes('polygon'))
- return ;
- return ;
+ const lower = chain?.toLowerCase();
+ const info = (() => {
+ if (lower?.includes('xdai')) {
+ return { Icon: XDaiIcon, name: 'xDAI' };
+ }
+ if (lower?.includes('polygon')) {
+ return { Icon: PolygonIcon, name: 'Polygon' };
+ }
+ return { Icon: EthereumIcon, name: 'Ethereum' };
+ })();
+ return (
+
+
+
+ );
};
diff --git a/packages/design-system/src/index.ts b/packages/design-system/src/index.ts
index 36b3a502..a00295fe 100644
--- a/packages/design-system/src/index.ts
+++ b/packages/design-system/src/index.ts
@@ -43,6 +43,7 @@ export {
selectStyles,
} from './theme';
export { H1, P } from './typography';
+export * from './ViewAllButton';
export {
AddIcon,
ArrowBackIcon,
@@ -82,6 +83,7 @@ export {
ButtonProps,
Center,
chakra,
+ ChakraComponent,
ChakraProps,
ChakraProvider,
ComponentWithAs,
@@ -120,6 +122,7 @@ export {
Link,
LinkBox,
LinkOverlay,
+ LinkProps,
List,
ListIcon,
ListItem,
diff --git a/packages/web/assets/cursive-title-small.png b/packages/web/assets/cursive-title-small.png
new file mode 100644
index 00000000..806facb3
Binary files /dev/null and b/packages/web/assets/cursive-title-small.png differ
diff --git a/packages/web/assets/cursive-title.png b/packages/web/assets/cursive-title.png
new file mode 100644
index 00000000..ba9b8b75
Binary files /dev/null and b/packages/web/assets/cursive-title.png differ
diff --git a/packages/web/assets/discord.svg b/packages/web/assets/discord.svg
new file mode 100644
index 00000000..c4cfed9f
--- /dev/null
+++ b/packages/web/assets/discord.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/packages/web/components/Container.tsx b/packages/web/components/Container.tsx
index f002a316..e99b0803 100644
--- a/packages/web/components/Container.tsx
+++ b/packages/web/components/Container.tsx
@@ -5,7 +5,7 @@ export const PageContainer: React.FC = ({ children, ...props }) => (
= ({ children, ...props }) => (
);
export const FlexContainer: React.FC = ({ children, ...props }) => (
-
+
{children}
);
diff --git a/packages/web/components/EditProfileForm.tsx b/packages/web/components/EditProfileForm.tsx
index b4ce8373..5e4e5aeb 100644
--- a/packages/web/components/EditProfileForm.tsx
+++ b/packages/web/components/EditProfileForm.tsx
@@ -52,6 +52,7 @@ import { useRouter } from 'next/router';
import React, {
ReactElement,
RefObject,
+ SyntheticEvent,
useCallback,
useEffect,
useMemo,
@@ -65,7 +66,7 @@ import { isEmpty } from 'utils/objectHelpers';
const MAX_DESC_LEN = 420; // characters
export type ProfileEditorProps = {
- player: Maybe;
+ player?: Maybe;
onClose: () => void;
};
@@ -80,25 +81,43 @@ const Label: React.FC = React.forwardRef(
},
);
-const Input: React.FC = React.forwardRef(
- ({ children, ...props }, reference) => {
+const Input = React.forwardRef(
+ ({ children, ...props }, fwdRef) => {
const [width, setWidth] = useState('9em');
- const ref = reference as RefObject;
- const textRef = useRef(null);
+ const ref = fwdRef as RefObject;
+ const textRef = useRef(null);
const isText = !props.type || props.type === 'text';
- const calcWidth = (text: string) => {
- const input = textRef.current;
- if (text && input) {
- input.textContent = text;
- setWidth(
- `min(calc(100vw - 2rem), calc(${input.scrollWidth}px + 2.25em))`,
- );
+
+ const calcWidth = useCallback((text?: string) => {
+ const layout = textRef.current;
+ const modal = layout?.closest('form');
+ if (layout && modal && text) {
+ layout.textContent = text;
+ const widths = [
+ `calc(${modal.clientWidth}px - 2rem)`,
+ `calc(${layout.scrollWidth}px + 2.25em)`,
+ ];
+ setWidth(`min(${widths.join(',')})`);
+ }
+ }, []);
+
+ const recalcText = (event: SyntheticEvent) => {
+ if (isText) {
+ const {
+ currentTarget: { value },
+ } = event;
+ calcWidth(value);
}
};
return (
-
+
= React.forwardRef(
caretColor: 'white',
},
}}
- // event is supposed to have a type definition in
- // @types/react if the DOM library is included,
- // but VS doesn't think it does.
- onInput={(event /* { target: { value } } */) => {
- const {
- target: { value },
- } = (event as unknown) as { target: { value: string } };
- if (isText) calcWidth(value);
- }}
- onFocus={(evt) => {
- if (isText) calcWidth(evt.target.value);
- }}
+ onInput={recalcText}
+ onFocus={recalcText}
+ {...{ width, ref }}
{...props}
- {...{ ref, width }}
>
{children}
@@ -182,7 +191,6 @@ export const EditProfileForm: React.FC = ({
const { value } = useProfileField({
field: key,
player,
- owner: true,
});
return [key, value];
}),
@@ -427,7 +435,7 @@ export const EditProfileForm: React.FC = ({
};
return (
-
+
@@ -705,8 +713,8 @@ export const EditProfileForm: React.FC = ({
type="number"
placeholder="23"
pl={9}
- minW="5em"
- maxW="7em"
+ minW={20}
+ maxW={22}
borderTopEndRadius={0}
borderBottomEndRadius={0}
borderRight={0}
@@ -734,7 +742,7 @@ export const EditProfileForm: React.FC = ({
-
+
{
- const { connected, connect, connecting, disconnect } = useWeb3();
- const { user, fetching } = useUser();
+ const { connecting, connected, connect, disconnect } = useWeb3();
+ const { fetching, user } = useUser();
+ const mounted = useMounted();
+ const { name } = useProfileField({
+ field: 'name',
+ player: user,
+ getter: getPlayerName,
+ });
return (
{
left={0}
bottom={0}
justify={user ? 'space-between' : 'center'}
- w="100%"
- h="5rem"
- bg="rgba(0,0,0,0.75)"
+ w="full"
+ h={20}
+ bg="rgba(0, 0, 0, 0.75)"
borderColor="#2B2244"
- px="1rem"
+ px={4}
sx={{ backdropFilter: 'blur(10px)' }}
>
{connected && !!user && !fetching && !connecting ? (
@@ -42,10 +51,11 @@ export const MegaMenuFooter = () => {
diff --git a/packages/web/components/MegaMenu/MegaMenuHeader.tsx b/packages/web/components/MegaMenu/MegaMenuHeader.tsx
index 45450a21..5915e64d 100644
--- a/packages/web/components/MegaMenu/MegaMenuHeader.tsx
+++ b/packages/web/components/MegaMenu/MegaMenuHeader.tsx
@@ -17,7 +17,7 @@ import LogoImage from 'assets/logo-new.png';
import { MetaLink } from 'components/Link';
import { DesktopNavLinks } from 'components/MegaMenu/DesktopNavLinks';
import { DesktopPlayerStats } from 'components/MegaMenu/DesktopPlayerStats';
-import { useUser, useWeb3 } from 'lib/hooks';
+import { useMounted, useUser, useWeb3 } from 'lib/hooks';
import { useRouter } from 'next/router';
import React from 'react';
import { menuIcons } from 'utils/menuIcons';
@@ -34,7 +34,7 @@ const Logo = ({ link }: LogoProps) => {
const h = useBreakpointValue({ base: 12, lg: 14 }) ?? 12;
return (
-
+
{
const { connected, connect, connecting } = useWeb3();
const router = useRouter();
const { user, fetching } = useUser();
+ const mounted = useMounted();
const { isOpen, onOpen, onClose } = useDisclosure();
const menuToggle = () => (isOpen ? onClose() : onOpen());
@@ -71,7 +72,7 @@ export const MegaMenuHeader: React.FC = () => {
>
{
flexWrap="nowrap"
alignItems="center"
cursor="pointer"
- h="2rem"
- w="2rem"
+ h={8}
+ w={8}
display={{ base: 'flex', lg: 'none' }}
p={2}
my="auto"
grow={1}
>
{isOpen ? (
-
+
) : (
-
+
)}
{
{/* */}
-
+
{connected && !!user && !fetching && !connecting ? (
) : (
-
- Connect
-
+
+ Connect
+
+ Mainnet Required
+
)}
diff --git a/packages/web/components/MegaMenu/XPSeedsBalance.tsx b/packages/web/components/MegaMenu/XPSeedsBalance.tsx
index d83a87c9..9971fddd 100644
--- a/packages/web/components/MegaMenu/XPSeedsBalance.tsx
+++ b/packages/web/components/MegaMenu/XPSeedsBalance.tsx
@@ -1,4 +1,4 @@
-import { HStack, Image, Text, Tooltip } from '@metafam/ds';
+import { Flex, HStack, Image, MetaTheme, Text, Tooltip } from '@metafam/ds';
import { numbers } from '@metafam/utils';
import SeedMarket from 'assets/seed-icon.svg';
import XPStar from 'assets/xp-star.svg';
@@ -13,19 +13,16 @@ type Props = {
mobile?: boolean;
};
// Display player XP and Seed
-export const XPSeedsBalance: React.FC = ({
- totalXP,
- mobile = false,
-}) => {
+export const XPSeedsBalance: React.FC = ({ totalXP }) => {
const { pSeedBalance } = usePSeedBalance();
return (
-
+
= ({
src={XPStar}
alignSelf="center"
alt="XP"
- boxSize={mobile ? '1.5rem' : '1rem'}
+ boxSize={['1.5rem', '1rem']}
/>
{Math.trunc(totalXP).toLocaleString()}
@@ -48,9 +47,9 @@ export const XPSeedsBalance: React.FC = ({
= ({
src={SeedMarket}
alignSelf="center"
alt="Seed"
- boxSize={mobile ? '1.5rem' : '1rem'}
+ boxSize={['1.5rem', '1rem']}
/>
{parseInt(
@@ -74,6 +75,6 @@ export const XPSeedsBalance: React.FC = ({
-
+
);
};
diff --git a/packages/web/components/Player/ColorBar.tsx b/packages/web/components/Player/ColorBar.tsx
index 56fef316..e928c99e 100644
--- a/packages/web/components/Player/ColorBar.tsx
+++ b/packages/web/components/Player/ColorBar.tsx
@@ -22,42 +22,48 @@ const maskImageStyle = ({ url }: { url: string }): Record => ({
WebkitMaskRepeat: 'no-repeat',
});
+export type ColorBarProps = ChakraProps & {
+ mask: Maybe;
+ types: PersonalityInfo;
+ loading: boolean;
+};
+
/* The color bar is below the attribute selection screen,
* and shows an equally proportioned set of colors with
* monochrome icons above them and a term for the
* combination below.
*/
-export const ColorBar = ({
+export const ColorBar: React.FC = ({
mask = null,
types = null,
+ loading = false,
...props
-}: ChakraProps & {
- mask: Maybe;
- types: PersonalityInfo;
-}): JSX.Element => {
- if (types == null) {
+}) => {
+ let status = null;
+
+ if (loading) {
+ status = 'Loading Settings…';
+ } else if (mask === null) {
+ status = 'Colors have not yet been chosen.';
+ } else if (types == null) {
+ status = 'Loading Personality Information…';
+ } else if (types[mask] == null) {
+ status = `Error Loading Information For Mask: “0b${mask
+ .toString(2)
+ .padStart(5, '0')}
+ ”.`;
+ }
+
+ if (status) {
return (
-
- Loading Personality Information…
+
+ {status}
);
}
- if (mask === null) {
- return (
-
- Colors have not yet been chosen.
-
- );
- }
-
- if (types[mask] == null) {
- return (
-
- Error Loading Information For Mask: “{mask.toString(2).padStart(5, '0')}
- ”
-
- );
+ if (mask === null || types == null) {
+ return null; // unreachable; for typescript
}
type ImagesArgProps = {
diff --git a/packages/web/components/Player/PlayerAvatar.tsx b/packages/web/components/Player/PlayerAvatar.tsx
index 2f534840..d57fbb9d 100644
--- a/packages/web/components/Player/PlayerAvatar.tsx
+++ b/packages/web/components/Player/PlayerAvatar.tsx
@@ -7,25 +7,21 @@ import { getPlayerImage, getPlayerName, hasImage } from 'utils/playerHelpers';
type PlayerAvatarProps = AvatarProps & {
player?: Player | GuildPlayer;
- omitBackground?: boolean;
- isOwnProfile?: boolean;
};
export const PlayerAvatar: React.FC = React.forwardRef<
HTMLSpanElement,
PlayerAvatarProps
->(({ player: user, isOwnProfile = false, src, ...props }, ref) => {
+>(({ player: user, src, ...props }, ref) => {
const player = user as Player;
const { value: image } = useProfileField({
field: 'profileImageURL',
player,
- owner: isOwnProfile,
getter: getPlayerImage,
});
const { name } = useProfileField({
field: 'name',
player,
- owner: isOwnProfile,
getter: getPlayerName,
});
const attrs = {
diff --git a/packages/web/components/Player/PlayerContacts.tsx b/packages/web/components/Player/PlayerContacts.tsx
index eb55fe76..5d986ab7 100644
--- a/packages/web/components/Player/PlayerContacts.tsx
+++ b/packages/web/components/Player/PlayerContacts.tsx
@@ -18,7 +18,7 @@ export const PlayerContacts: React.FC = ({
}) => {
const [copied, handleCopy] = useCopyToClipboard();
return (
-
+
{player?.accounts?.map((acc) => {
switch (acc.type) {
case 'TWITTER': {
diff --git a/packages/web/components/Player/PlayerGuild.tsx b/packages/web/components/Player/PlayerGuild.tsx
index c7d58593..a94338fa 100644
--- a/packages/web/components/Player/PlayerGuild.tsx
+++ b/packages/web/components/Player/PlayerGuild.tsx
@@ -1,21 +1,21 @@
-import { Link } from '@metafam/ds';
+import { Link, LinkProps } from '@metafam/ds';
import React from 'react';
type LinkGuildProps = {
- daoUrl: string | null;
+ daoURL: string | null;
guildname: string | undefined | null;
};
export const LinkGuild: React.FC = ({
- daoUrl,
+ daoURL,
guildname,
children,
}) => {
if (guildname != null) {
- return ;
+ return ;
}
- if (daoUrl != null) {
- return ;
+ if (daoURL != null) {
+ return ;
}
return <>{children}>;
};
@@ -34,14 +34,24 @@ export const InternalGuildLink: React.FC = ({
);
type DaoHausLinkProps = {
- daoUrl: string | null;
+ daoURL: string | null;
};
-export const DaoHausLink: React.FC = ({ daoUrl, children }) =>
- daoUrl != null ? (
-
- {children}
-
- ) : (
- <>{children}>
- );
+export const DaoHausLink: React.FC = ({
+ daoURL,
+ children,
+ _hover = {},
+ ...props
+}) => {
+ _hover.textDecoration = 'none'; // eslint-disable-line no-param-reassign
+
+ if (daoURL != null) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return <>{children}>;
+};
diff --git a/packages/web/components/Player/Section/PlayerAchievements.tsx b/packages/web/components/Player/Section/PlayerAchievements.tsx
index 903beec9..0ba20fbe 100644
--- a/packages/web/components/Player/Section/PlayerAchievements.tsx
+++ b/packages/web/components/Player/Section/PlayerAchievements.tsx
@@ -3,17 +3,17 @@ import { ProfileSection } from 'components/Profile/ProfileSection';
import { Player } from 'graphql/autogen/types';
import React from 'react';
import { FaMedal } from 'react-icons/fa';
-import { BoxType } from 'utils/boxTypes';
+import { BoxTypes } from 'utils/boxTypes';
// TODO Fake data
type Props = {
player: Player;
isOwnProfile?: boolean;
- canEdit?: boolean;
+ editing?: boolean;
};
export const PlayerAchievements: React.FC = ({
isOwnProfile,
- canEdit,
+ editing,
}) => {
const [show, setShow] = React.useState(false);
const fakeData = [
@@ -25,9 +25,8 @@ export const PlayerAchievements: React.FC = ({
return (
{(fakeData || []).slice(0, show ? 999 : 3).map((title) => (
diff --git a/packages/web/components/Player/Section/PlayerAddSection.tsx b/packages/web/components/Player/Section/PlayerAddSection.tsx
index bc8cb3c6..8af5bcc3 100644
--- a/packages/web/components/Player/Section/PlayerAddSection.tsx
+++ b/packages/web/components/Player/Section/PlayerAddSection.tsx
@@ -3,6 +3,7 @@ import {
Flex,
FlexProps,
Input,
+ MetaTheme,
Modal,
ModalBody,
ModalCloseButton,
@@ -18,46 +19,44 @@ import { Maybe } from '@metafam/utils';
import BackgroundImage from 'assets/main-background.jpg';
import { PlayerSection } from 'components/Profile/PlayerSection';
import { Player } from 'graphql/autogen/types';
-import { PersonalityInfo } from 'graphql/queries/enums/getPersonalityInfo';
import React, { useCallback, useEffect, useState } from 'react';
-import { BoxMetadata, BoxType } from 'utils/boxTypes';
+import { BoxMetadata, BoxType, BoxTypes } from 'utils/boxTypes';
type Props = FlexProps & {
player: Player;
- personalityInfo: PersonalityInfo;
- boxList: BoxType[];
+ boxes: Array;
onAddBox: (arg0: BoxType, arg1: BoxMetadata) => void;
};
export const PlayerAddSection = React.forwardRef(
- ({ player, personalityInfo, boxList = [], onAddBox, ...props }, ref) => {
+ ({ player, boxes = [], onAddBox, ...props }, ref) => {
const { isOpen, onOpen, onClose } = useDisclosure();
- const [boxType, setBoxType] = useState>(null);
- const [boxMetadata, setBoxMetadata] = useState({});
+ const [type, setType] = useState>(null);
+ const [metadata, setMetadata] = useState({});
const selectBoxType = useCallback(
- ({ target: { value: boxId } }) => setBoxType(boxId),
+ ({ target: { value: boxId } }) => setType(boxId),
[],
);
const addSection = useCallback(() => {
- if (!boxType) return;
- onAddBox(boxType, boxMetadata);
+ if (!type) return;
+ onAddBox(type, metadata);
onClose();
- }, [boxType, boxMetadata, onAddBox, onClose]);
+ }, [type, metadata, onAddBox, onClose]);
useEffect(() => {
- setBoxMetadata({});
- setBoxType(null);
+ setMetadata({});
+ setType(null);
}, [isOpen]);
return (
(
m={0}
bg="blue20"
color="offwhite"
+ textTransform="uppercase"
opacity={0.4}
size="lg"
_hover={{ bg: 'purpleBoxLight', opacity: '0.8' }}
>
- ADD NEW BLOCK
+ Add New Block
-
+
(
p={4}
top={0}
right={0}
- _focus={{
- boxShadow: 'none',
- }}
+ _focus={{ boxShadow: 'none' }}
/>
Add New Block
@@ -112,39 +110,42 @@ export const PlayerAddSection = React.forwardRef(
color="white"
w={{ base: '100%', sm: '30rem' }}
maxW="30rem"
- minH="30rem"
>
- {boxType === BoxType.EMBEDDED_URL && (
+ {type === BoxTypes.EMBEDDED_URL && (
- setBoxMetadata({ url })
+ setMetadata({ url })
}
size="lg"
borderRadius={0}
@@ -153,7 +154,7 @@ export const PlayerAddSection = React.forwardRef(
borderWidth="2px"
/>
)}
- {boxType && (
+ {type && (
(
>
@@ -179,7 +179,7 @@ export const PlayerAddSection = React.forwardRef(
colorScheme="blue"
mr={3}
onClick={addSection}
- isDisabled={!boxType}
+ isDisabled={!type}
>
Save Block
diff --git a/packages/web/components/Player/Section/PlayerColorDisposition.tsx b/packages/web/components/Player/Section/PlayerColorDisposition.tsx
index 35e1cfa8..0e2da5a1 100644
--- a/packages/web/components/Player/Section/PlayerColorDisposition.tsx
+++ b/packages/web/components/Player/Section/PlayerColorDisposition.tsx
@@ -2,43 +2,54 @@ import { Text } from '@metafam/ds';
import { ColorBar } from 'components/Player/ColorBar';
import { ProfileSection } from 'components/Profile/ProfileSection';
import { Player } from 'graphql/autogen/types';
-import { PersonalityInfo } from 'graphql/queries/enums/getPersonalityInfo';
+import {
+ getPersonalityInfo,
+ PersonalityInfo,
+} from 'graphql/queries/enums/getPersonalityInfo';
import { useProfileField } from 'lib/hooks';
-import React from 'react';
-import { BoxType } from 'utils/boxTypes';
+import React, { useEffect, useState } from 'react';
+import { BoxTypes } from 'utils/boxTypes';
export type ColorDispositionProps = {
player: Player;
- types: PersonalityInfo;
- isOwnProfile?: boolean;
- canEdit?: boolean;
+ editing?: boolean;
};
export const PlayerColorDisposition: React.FC = ({
player,
- types,
- isOwnProfile,
- canEdit,
+ editing = false,
}) => {
- const { value: mask } = useProfileField({
+ const {
+ value: mask,
+ owner: isOwnProfile,
+ fetching,
+ } = useProfileField({
field: 'colorMask',
player,
- owner: isOwnProfile,
});
+ const [types, setTypes] = useState(null);
+
+ useEffect(() => {
+ const getInfo = async () => {
+ setTypes(await getPersonalityInfo());
+ };
+ getInfo();
+ }, []);
return (
{mask == null ? (
Unspecified
) : (
-
+
)}
);
diff --git a/packages/web/components/Player/Section/PlayerCompletedQuests.tsx b/packages/web/components/Player/Section/PlayerCompletedQuests.tsx
index 53507595..0af50c09 100644
--- a/packages/web/components/Player/Section/PlayerCompletedQuests.tsx
+++ b/packages/web/components/Player/Section/PlayerCompletedQuests.tsx
@@ -1,4 +1,12 @@
-import { Box, Button, ExternalLinkIcon, Link, Stack, Text } from '@metafam/ds';
+import {
+ Box,
+ BoxProps,
+ Button,
+ ExternalLinkIcon,
+ Link,
+ Stack,
+ Text,
+} from '@metafam/ds';
import { ProfileSection } from 'components/Profile/ProfileSection';
import {
Player,
@@ -7,18 +15,18 @@ import {
} from 'graphql/autogen/types';
import { getAcceptedQuestsByPlayerQuery } from 'graphql/getQuests';
import React, { useEffect, useState } from 'react';
-import { BoxType } from 'utils/boxTypes';
+import { BoxTypes } from 'utils/boxTypes';
type Props = {
player: Player;
isOwnProfile?: boolean;
- canEdit?: boolean;
+ editing?: boolean;
};
export const PlayerCompletedQuests: React.FC = ({
player,
isOwnProfile,
- canEdit,
+ editing,
}) => {
const [quests, setQuests] = useState>([]);
@@ -44,13 +52,12 @@ export const PlayerCompletedQuests: React.FC = ({
return (
}
- subheader='A quest is considered "complete" when it is accepted by the
- quest owner.'
+ modalPrompt={quests.length ? 'Show All' : undefined}
+ modal={}
+ subheader="A quest is considered “complete” when it is accepted by the quest owner."
>
{quests.length ? (
@@ -67,13 +74,16 @@ export const PlayerCompletedQuests: React.FC = ({
interface QuestProps {
quests: Array;
- mb?: number;
}
-const QuestList: React.FC = ({ quests, mb = 2 }) => (
+const QuestList: React.FC = ({
+ quests,
+ mb = 2,
+ ...props
+}) => (
<>
{quests.map((quest) => (
-
+
{quest.completed?.title}
diff --git a/packages/web/components/Player/Section/PlayerGallery.tsx b/packages/web/components/Player/Section/PlayerGallery.tsx
index 6a7fa863..6756ea18 100644
--- a/packages/web/components/Player/Section/PlayerGallery.tsx
+++ b/packages/web/components/Player/Section/PlayerGallery.tsx
@@ -10,7 +10,9 @@ import {
ModalOverlay,
SimpleGrid,
Text,
+ Tooltip,
useDisclosure,
+ ViewAllButton,
} from '@metafam/ds';
import BackgroundImage from 'assets/main-background.jpg';
import { MetaLink as Link } from 'components/Link';
@@ -18,19 +20,11 @@ import { ProfileSection } from 'components/Profile/ProfileSection';
import { Player } from 'graphql/autogen/types';
import { useOpenSeaCollectibles } from 'lib/hooks/opensea';
import React from 'react';
-import { BoxType } from 'utils/boxTypes';
+import { BoxTypes } from 'utils/boxTypes';
import { Collectible } from 'utils/openseaHelpers';
-const GalleryItem: React.FC<{ nft: Collectible; noMargin?: boolean }> = ({
- nft,
- noMargin = false,
-}) => (
-
+const GalleryItem: React.FC<{ nft: Collectible }> = ({ nft }) => (
+
= ({
minW={28}
minH={28}
/>
-
-
+
- {nft.title}
-
- {nft.priceString}
-
+
+ {nft.title}
+
+ {nft.priceString}
+
+
);
+type GalleryModalProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ nfts: Array;
+};
+
+const GalleryModal: React.FC = ({
+ isOpen,
+ onClose,
+ nfts,
+}) => (
+
+
+
+
+
+
+ NFT Gallery
+
+
+
+
+
+
+
+ {nfts?.map((nft) => (
+
+ ))}
+
+
+
+
+
+
+);
+
type Props = {
player: Player;
isOwnProfile?: boolean;
- canEdit?: boolean;
+ editing?: boolean;
};
export const PlayerGallery: React.FC = ({
player,
isOwnProfile,
- canEdit,
+ editing,
}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
- const { favorites, data, loading } = useOpenSeaCollectibles({ player });
+ const { favorites, data: nfts, loading, error } = useOpenSeaCollectibles({
+ player,
+ });
return (
- {loading && }
- {!loading &&
- favorites?.map((nft) => )}
- {!loading && data.length === 0 && (
-
- No{' '}
-
- NFT
-
- s found for {isOwnProfile ? 'you' : 'this player'}.
-
- )}
- {!loading && data?.length > 3 && (
-
- View all
-
- )}
-
-
-
-
-
-
- NFT Gallery
-
-
-
-
-
- {
+ if (loading) {
+ return ;
+ }
+ if (error) {
+ return (
+
+ Error: {error}
+
+ );
+ }
+ if (nfts.length === 0) {
+ return (
+
+ No{' '}
+
-
- {data?.map((nft) => (
-
- ))}
-
+ NFT
+
+ s found for {isOwnProfile ? 'you' : 'this player'}.
+
+ );
+ }
+ return (
+ <>
+
+ {favorites?.map((nft) => (
+
+ ))}
+
+ {nfts.length > 3 && (
+
+
+
-
-
-
-
+ )}
+ >
+ );
+ })()}
);
};
diff --git a/packages/web/components/Player/Section/PlayerHero.tsx b/packages/web/components/Player/Section/PlayerHero.tsx
index 58275771..c09df749 100644
--- a/packages/web/components/Player/Section/PlayerHero.tsx
+++ b/packages/web/components/Player/Section/PlayerHero.tsx
@@ -3,9 +3,10 @@ import {
EditIcon,
Flex,
getTimeZoneFor,
+ Grid,
HStack,
IconButton,
- Link,
+ MetaTag,
Modal,
ModalBody,
ModalCloseButton,
@@ -19,67 +20,48 @@ import {
Wrap,
WrapItem,
} from '@metafam/ds';
+import { Maybe } from '@metafam/utils';
import BackgroundImage from 'assets/main-background.jpg';
import { FlexContainer } from 'components/Container';
import { EditProfileForm } from 'components/EditProfileForm';
import { PlayerAvatar } from 'components/Player/PlayerAvatar';
-import { PlayerContacts } from 'components/Player/PlayerContacts';
+import { PlayerContacts as Contacts } from 'components/Player/PlayerContacts';
import { PlayerHeroTile } from 'components/Player/Section/PlayerHeroTile';
-import { PlayerPronouns } from 'components/Player/Section/PlayerPronouns';
import { ProfileSection } from 'components/Profile/ProfileSection';
import { Player } from 'graphql/autogen/types';
-import { Maybe } from 'graphql/jsutils/Maybe';
-import { PersonalityInfo } from 'graphql/queries/enums/getPersonalityInfo';
-import { useUser } from 'lib/hooks';
+import { useProfileField, useUser } from 'lib/hooks';
import { useAnimateProfileChanges } from 'lib/hooks/players';
import React, { useEffect, useState } from 'react';
import { FaClock, FaGlobe } from 'react-icons/fa';
-import { BoxType } from 'utils/boxTypes';
-import { getPlayerDescription, getPlayerName } from 'utils/playerHelpers';
-
-import { ColorBar } from '../ColorBar';
+import { BoxTypes } from 'utils/boxTypes';
+import { getPlayerName } from 'utils/playerHelpers';
const MAX_BIO_LENGTH = 240;
-type Props = {
+type HeroProps = {
player: Player;
- personalityInfo: PersonalityInfo;
- isOwnProfile?: boolean;
- canEdit?: boolean;
+ editing?: boolean;
};
-type AvailabilityProps = { person?: Maybe };
-type TimeZoneDisplayProps = {
- person?: Maybe;
-};
-type ColorDispositionProps = {
- person?: Maybe;
- personalityInfo: PersonalityInfo;
+type DisplayComponentProps = {
+ player?: Maybe;
+ Wrapper?: React.FC;
};
-export const PlayerHero: React.FC = ({
- player,
- isOwnProfile,
- canEdit,
-}) => {
- const description = getPlayerDescription(player);
- const [show, setShow] = useState(
- (description ?? '').length <= MAX_BIO_LENGTH,
- );
- const { isOpen, onOpen, onClose } = useDisclosure();
- const [playerName, setPlayerName] = useState();
+export const PlayerHero: React.FC = ({ player, editing }) => {
const { user } = useUser();
-
- const person = isOwnProfile ? user : player;
- useEffect(() => {
- if (person) {
- setPlayerName(getPlayerName(person));
- }
- }, [person]);
+ const isOwnProfile = user ? user.id === player?.id : null;
+ const { isOpen, onOpen, onClose } = useDisclosure();
return (
-
- {isOwnProfile && !canEdit && (
-
+
+ {isOwnProfile && !editing && (
+
= ({
isRound
_active={{
transform: 'scale(0.8)',
- backgroundColor: 'transparent',
+ bg: 'transparent',
}}
/>
)}
-
+
-
- {playerName}
-
-
-
- {description && (
-
-
-
- {show
- ? description
- : `${description.substring(0, MAX_BIO_LENGTH - 9)}…`}
- {description.length > MAX_BIO_LENGTH && (
- setShow((s) => !s)}
- pl={1}
- >
- Read {show ? 'less' : 'more'}
-
- )}
-
-
-
- )}
+
+
+
-
+
- {person?.profile?.pronouns && }
- {/*
+ {/*
+
www.mycoolportfolio.com
- */}
+
+ */}
-
-
-
-
-
-
-
-
-
-
-
- {player?.profile?.emoji && (
-
-
- {player.profile.emoji}
-
-
- )}
-
+
+
+
+
+
+
{/*
@@ -197,118 +134,251 @@ export const PlayerHero: React.FC = ({
*/}
-
- {/* player?.profile?.colorMask && (
-
-
-
- ) */}
-
-
-
-
+
+
- Edit Profile
-
-
-
-
-
-
-
+
+ Edit Profile
+
+
+
+
+
+
+
+ )}
);
};
-const Availability: React.FC = ({ person }) => {
- const [hours, setHours] = useState(
- person?.profile?.availableHours ?? null,
- );
- const updateFN = () => setHours(person?.profile?.availableHours ?? null);
- const { animation } = useAnimateProfileChanges(
- person?.profile?.availableHours,
- updateFN,
- );
+export const Pronouns: React.FC = ({
+ player,
+ Wrapper = React.Fragment,
+}) => {
+ const { pronouns } = useProfileField({
+ field: 'pronouns',
+ player,
+ });
+ // This is broken now…
+ // Fix it by making the animation into a component which
+ // saves the children and replaces them after fading in
+ // and out. (If such a thing is possible…)
+ //
+ // const { animation } = useAnimateProfileChanges(pronouns)
+
+ if (!pronouns || pronouns === '') {
+ return null;
+ }
+
return (
-
-
-
-
-
-
- {hours == null ? (
- Unspecified
- ) : (
- <>
-
- {hours}
-
-
- hr⁄week
-
- >
- )}
-
-
-
+
+
+
+ {pronouns}
+
+
+
);
};
-const TimeZoneDisplay: React.FC = ({ person }) => {
- const tz = getTimeZoneFor({ title: person?.profile?.timeZone });
- const [timeZone, setTimeZone] = useState(
- tz?.abbreviation ?? null,
- );
- const [offset, setOffset] = useState(tz?.utc ?? '');
- const updateFN = () => {
- if (tz?.abbreviation) setTimeZone(tz.abbreviation);
- if (tz?.utc) setOffset(tz.utc);
- };
- const short = offset.replace(/:00\)$/, ')').replace(/ +/g, '');
- const { animation } = useAnimateProfileChanges(timeZone, updateFN);
+const Emoji: React.FC = ({
+ player,
+ Wrapper = React.Fragment,
+}) => {
+ const { emoji } = useProfileField({
+ field: 'emoji',
+ player,
+ });
+
+ if (!emoji || emoji === '') {
+ return null;
+ }
return (
-
-
+
+
+ {emoji}
+
+
+
+ );
+};
+
+const Description: React.FC = ({
+ player,
+ Wrapper = React.Fragment,
+}) => {
+ const { description } = useProfileField({
+ field: 'description',
+ player,
+ });
+ const [show, setShow] = useState(false);
+
+ useEffect(() => {
+ setShow((description ?? '').length <= MAX_BIO_LENGTH);
+ }, [description]);
+
+ if (!description || description === '') {
+ return null;
+ }
+
+ return (
+
+
+
+ {show || description.length <= MAX_BIO_LENGTH
+ ? description
+ : `${description.substring(0, MAX_BIO_LENGTH - 9)}…`}
+ {description.length > MAX_BIO_LENGTH && (
+ setShow((s) => !s)}
+ px={0.5}
+ ml={2}
+ bg="#FFFFFF22"
+ border="1px solid #FFFFFF99"
+ borderRadius="15%"
+ _hover={{ bg: '#FFFFFF44' }}
+ >
+ Read {show ? 'Less' : 'More'}
+
+ )}
+
+
+
+ );
+};
+
+const Name: React.FC = ({
+ player,
+ Wrapper = React.Fragment,
+}) => {
+ const { name } = useProfileField({
+ field: 'name',
+ player,
+ getter: getPlayerName,
+ });
+
+ return (
+
+
+ {name}
+
+
+ );
+};
+
+const Availability: React.FC = ({
+ player,
+ Wrapper = React.Fragment,
+}) => {
+ const { value: current } = useProfileField({
+ field: 'availableHours',
+ player,
+ });
+ const [hours, setHours] = useState>(current);
+ const updateFN = () => setHours(current ?? null);
+ const { animation } = useAnimateProfileChanges(current, updateFN);
+
+ return (
+
+
+
+
+
+
+
+
+ {hours == null ? (
+ Unspecified
+ ) : (
+ <>
+
+ {hours}
+
+
+ hr⁄week
+
+ >
+ )}
+
+
+
+
+
+ );
+};
+
+const TimeZone: React.FC = ({
+ player,
+ Wrapper = React.Fragment,
+}) => {
+ const { value: current } = useProfileField({
+ field: 'timeZone',
+ player,
+ });
+ const tz = getTimeZoneFor({ title: current });
+ const timeZone = tz?.abbreviation ?? null;
+ const short = (tz?.utc ?? '').replace(/:00\)$/, ')').replace(/ +/g, '');
+
+ return (
+
+
{timeZone === null ? (
- Unspecified
+
+ Unspecified
+
) : (
-
+
= ({ person }) => {
)}
-
-
- );
-};
-
-export const ColorDispositionDisplay: React.FC = ({
- person,
- personalityInfo: types,
-}) => {
- const [mask, setMask] = useState(
- person?.profile?.colorMask ?? null,
- );
-
- const updateFN = () => setMask(mask);
- const { animation } = useAnimateProfileChanges(mask, updateFN);
-
- return (
-
-
- {mask == null ? (
-
- Unspecified
-
- ) : (
-
-
-
- )}
-
-
+
+
);
};
diff --git a/packages/web/components/Player/Section/PlayerHeroTile.tsx b/packages/web/components/Player/Section/PlayerHeroTile.tsx
index d57f81dc..a4737afc 100644
--- a/packages/web/components/Player/Section/PlayerHeroTile.tsx
+++ b/packages/web/components/Player/Section/PlayerHeroTile.tsx
@@ -3,7 +3,9 @@ import React from 'react';
type Props = {
title: string;
- children: React.ReactNode;
+ // shim b/c I'm getting an error I don't understand
+ // when specifying `align` as an attribute
+ align?: string;
};
export const PlayerHeroTile: React.FC = ({
@@ -11,7 +13,7 @@ export const PlayerHeroTile: React.FC = ({
title,
...props
}) => (
-
+
{title}
diff --git a/packages/web/components/Player/Section/PlayerMemberships.tsx b/packages/web/components/Player/Section/PlayerMemberships.tsx
index 5e068df9..4ba2324d 100644
--- a/packages/web/components/Player/Section/PlayerMemberships.tsx
+++ b/packages/web/components/Player/Section/PlayerMemberships.tsx
@@ -1,7 +1,7 @@
import {
Box,
- Center,
ChainIcon,
+ chakra,
Flex,
Heading,
HStack,
@@ -14,6 +14,9 @@ import {
SimpleGrid,
Text,
useDisclosure,
+ ViewAllButton,
+ Wrap,
+ WrapItem,
} from '@metafam/ds';
import BackgroundImage from 'assets/main-background.jpg';
import { LinkGuild } from 'components/Player/PlayerGuild';
@@ -21,99 +24,197 @@ import { ProfileSection } from 'components/Profile/ProfileSection';
import { Player } from 'graphql/autogen/types';
import { getAllMemberships, GuildMembership } from 'graphql/getMemberships';
import React, { useEffect, useMemo, useState } from 'react';
-import { BoxType } from 'utils/boxTypes';
-import { getDaoLink } from 'utils/daoHelpers';
+import { BoxTypes } from 'utils/boxTypes';
+import { getDAOLink } from 'utils/daoHelpers';
-type DaoListingProps = {
+type DAOListingProps = {
membership: GuildMembership;
};
-const DaoListing: React.FC = ({ membership }) => {
- const {
+const DAOListing: React.FC = ({
+ membership: {
title,
memberShares,
daoShares,
memberRank,
- memberXp,
+ memberXP,
chain,
address,
- logoUrl,
+ logoURL,
guildname,
- } = membership;
-
+ },
+}) => {
const stake = useMemo(() => {
- if (memberXp != null) {
- return `XP: ${Math.floor(memberXp)}`;
+ if (memberXP != null) {
+ return `XP: ${Math.floor(memberXP)}`;
}
if (daoShares != null) {
- return `Shares: ${memberShares ?? 'Unknown'} / ${daoShares}`;
+ const member = memberShares ? Number(memberShares) : null;
+ const dao = Number(daoShares);
+ const percent = member != null ? ((member * 100) / dao).toFixed(3) : '?';
+ return (
+
+
+ Shares
+
+
+
+ {member != null ? member.toLocaleString() : 'Unknown'}
+ {' '}
+
+ ⁄
+ {' '}
+ {dao.toLocaleString()}
+
+
+ );
}
- return '';
- }, [memberShares, memberXp, daoShares]);
+ return null;
+ }, [memberShares, memberXP, daoShares]);
- const daoUrl = useMemo(() => getDaoLink(chain, address), [chain, address]);
+ const daoURL = useMemo(() => getDAOLink(chain, address), [chain, address]);
return (
-
-
-
- {logoUrl ? (
-
- ) : (
-
- )}
+
+
+
+
+ {logoURL ? (
+
+ ) : (
+
+ )}
+
+
-
+
-
- {title ?? (
-
- Unknown{' '}
-
- {chain}
- {' '}
- DAO
-
- )}
-
-
+ {title ?? (
+
+ Unknown{' '}
+
+ {chain}
+ {' '}
+ DAO
+
+ )}
-
+
{memberRank && (
{memberRank}
)}
- {stake}
-
-
-
+
+ {stake}
+
+
+
+
);
};
+type MembershipListProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ memberships: Array;
+};
+
+const MembershipListModal: React.FC = ({
+ isOpen,
+ onClose,
+ memberships,
+}) => (
+
+
+
+
+
+
+ Memberships
+
+
+
+
+
+
+
+
+ {memberships.map((membership) => (
+
+ ))}
+
+
+
+
+
+);
+
type MembershipSectionProps = {
player: Player;
isOwnProfile?: boolean;
- canEdit?: boolean;
+ editing?: boolean;
};
export const PlayerMemberships: React.FC = ({
player,
isOwnProfile,
- canEdit,
+ editing,
}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const [memberships, setMemberships] = useState([]);
@@ -129,98 +230,33 @@ export const PlayerMemberships: React.FC = ({
return (
{loading && }
{!loading && memberships.length === 0 && (
-
+
No DAO memberships found for{' '}
{isOwnProfile ? 'you' : 'this player'}.
)}
- {memberships.slice(0, 4).map((membership) => (
-
- ))}
+
+ {memberships.slice(0, 4).map((membership) => (
+
+
+
+ ))}
+
{memberships.length > 4 && (
-
- View All ({memberships.length})
-
+
+
+
+
)}
-
-
-
-
-
-
-
- Memberships
-
-
-
-
-
-
-
-
- {memberships.map((membership) => (
-
- ))}
-
-
-
-
-
-
);
};
diff --git a/packages/web/components/Player/Section/PlayerPronouns.tsx b/packages/web/components/Player/Section/PlayerPronouns.tsx
deleted file mode 100644
index cacf6ac8..00000000
--- a/packages/web/components/Player/Section/PlayerPronouns.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { MetaTag } from '@metafam/ds';
-import { FlexContainer } from 'components/Container';
-import { PlayerHeroTile } from 'components/Player/Section/PlayerHeroTile';
-import { Player } from 'graphql/autogen/types';
-import { useAnimateProfileChanges } from 'lib/hooks/players';
-import React, { useState } from 'react';
-
-type Props = { person: Player | null | undefined };
-
-export const PlayerPronouns: React.FC = ({ person }) => {
- const [pronouns, setPronouns] = useState(
- person?.profile?.pronouns ?? '',
- );
- const updateFN = () => {
- setPronouns(person?.profile?.pronouns ?? '');
- };
- const { animation } = useAnimateProfileChanges(
- person?.profile?.pronouns,
- updateFN,
- );
-
- return pronouns ? (
-
-
-
- {pronouns}
-
-
-
- ) : (
- <>>
- );
-};
diff --git a/packages/web/components/Player/Section/PlayerRoles.tsx b/packages/web/components/Player/Section/PlayerRoles.tsx
index 1e9cd764..7a09938c 100644
--- a/packages/web/components/Player/Section/PlayerRoles.tsx
+++ b/packages/web/components/Player/Section/PlayerRoles.tsx
@@ -1,42 +1,50 @@
-import { BoxedNextImage, MetaTag, Text, Wrap } from '@metafam/ds';
+import { BoxedNextImage, MetaTag, Text, Wrap, WrapItem } from '@metafam/ds';
import { ProfileSection } from 'components/Profile/ProfileSection';
import { Player } from 'graphql/autogen/types';
+import { useOverridableField } from 'lib/hooks';
import React from 'react';
-import { BoxType } from 'utils/boxTypes';
+import { BoxTypes } from 'utils/boxTypes';
type Props = {
player: Player;
isOwnProfile?: boolean;
- canEdit?: boolean;
+ editing?: boolean;
};
export const PlayerRoles: React.FC = ({
player,
isOwnProfile,
- canEdit,
-}) => (
-
- {!player.roles ||
- (player.roles.length === 0 && (
+ editing,
+}) => {
+ const field = 'roles';
+ const { value: roles } = useOverridableField({
+ field,
+ defaultValue: (player.roles ?? [])
+ .sort((a, b) => a.rank - b.rank)
+ .map(({ role }) => role),
+ });
+
+ return (
+
+ {roles?.length === 0 && (
- No Roles found for {isOwnProfile ? 'you' : 'this player'}.
+ No roles assigned to {isOwnProfile ? 'you' : 'this player'}.
- ))}
-
- {player.roles &&
- player.roles
- .sort((a, b) => (a.rank > b.rank ? 1 : -1))
- .map(({ role, rank, PlayerRole }) => (
-
+ )}
+
+ {(roles ?? []).map((role, rank) => (
+
+
= ({
casing="uppercase"
my={{ base: 0, md: 2 }}
>
- {PlayerRole.label}
+ {role}
- ))}
-
-
-);
+
+ ))}
+
+
+ );
+};
diff --git a/packages/web/components/Player/Section/PlayerSkills.tsx b/packages/web/components/Player/Section/PlayerSkills.tsx
index 3bd91891..ae1814aa 100644
--- a/packages/web/components/Player/Section/PlayerSkills.tsx
+++ b/packages/web/components/Player/Section/PlayerSkills.tsx
@@ -1,65 +1,44 @@
import { MetaTag, Text, Wrap, WrapItem } from '@metafam/ds';
import { ProfileSection } from 'components/Profile/ProfileSection';
-import { Player, SkillCategory_Enum } from 'graphql/autogen/types';
+import { Player } from 'graphql/autogen/types';
import { SkillColors } from 'graphql/types';
-import { useAnimateProfileChanges } from 'lib/hooks/players';
-import React, { useState } from 'react';
-import { BoxType } from 'utils/boxTypes';
+import { useOverridableField } from 'lib/hooks';
+import React from 'react';
+import { BoxTypes } from 'utils/boxTypes';
type Props = {
player: Player;
isOwnProfile?: boolean;
- canEdit?: boolean;
+ editing?: boolean;
};
export const PlayerSkills: React.FC = ({
player,
- isOwnProfile,
- canEdit,
+ isOwnProfile = false,
+ editing = false,
}) => {
- const [playerSkills, setPlayerSkills] = useState<
- Array<{ id: number; name: string; category: SkillCategory_Enum }>
- >(
- player.skills?.map((s) => ({
- id: s.Skill.id,
- name: s.Skill.name,
- category: s.Skill.category,
- })) ?? [],
- );
-
- const updateFN = () => {
- if (player.skills) {
- setPlayerSkills(
- player.skills.map((s) => ({
- id: s.Skill.id,
- name: s.Skill.name,
- category: s.Skill.category,
- })),
- );
- }
- };
-
- const { animation } = useAnimateProfileChanges(player.skills, updateFN);
+ const field = 'skills';
+ const { value: skills } = useOverridableField({
+ field,
+ defaultValue: player.skills.map(({ Skill: skill }) => skill),
+ });
return (
- {!player?.skills?.length ? (
+ {!skills?.length ? (
{isOwnProfile ? 'You haven’t ' : 'This player hasn’t '}
defined any skills.
) : (
-
- {(playerSkills || []).map(({ id, name, category }) => (
+
+ {(skills || []).map(({ id, name, category }) => (
= ({
- player,
- isOwnProfile,
- canEdit,
-}) => {
- const [playerType, setPlayerType] = useState(
- (player.profile?.explorerType as ExplorerType) ?? null,
- );
- const updateFN = () =>
- setPlayerType(player.profile?.explorerType as ExplorerType);
- const { animation } = useAnimateProfileChanges(
- player.profile?.explorerType,
- updateFN,
- );
+export const PlayerType: React.FC = ({ player, editing }) => {
+ const { explorerType, owner: isOwnProfile } = useProfileField({
+ field: 'explorerType',
+ player,
+ });
return (
- {!playerType ? (
+ {!explorerType ? (
Unspecified
) : (
-
+ <>
- {playerType.title}
+ {explorerType.title}
- {playerType.description}
+ {explorerType.description}
-
+ >
)}
);
diff --git a/packages/web/components/Player/Section/config.ts b/packages/web/components/Player/Section/config.ts
index ac8a2e3a..d9627717 100644
--- a/packages/web/components/Player/Section/config.ts
+++ b/packages/web/components/Player/Section/config.ts
@@ -1,208 +1,138 @@
import { Layout, Layouts } from 'react-grid-layout';
-import { BoxMetadata, BoxType, getBoxKey } from 'utils/boxTypes';
+import { BoxMetadata, BoxType, BoxTypes, createBoxKey } from 'utils/boxTypes';
export type LayoutItem = {
- boxKey: string;
- boxType: BoxType;
- boxMetadata: BoxMetadata;
+ key: string;
+ type: BoxType;
+ metadata?: BoxMetadata;
};
export type ProfileLayoutData = {
- layoutItems: LayoutItem[];
+ layoutItems: Array;
layouts: Layouts;
};
export const GRID_ROW_HEIGHT = 32;
+export const HEIGHT_MODIFIER = 1.8;
export const ALL_BOXES = [
- BoxType.PLAYER_HERO,
- BoxType.PLAYER_SKILLS,
- BoxType.PLAYER_COLOR_DISPOSITION,
- BoxType.PLAYER_TYPE,
- BoxType.PLAYER_NFT_GALLERY,
- BoxType.PLAYER_DAO_MEMBERSHIPS,
- BoxType.PLAYER_ROLES,
- BoxType.EMBEDDED_URL,
- BoxType.PLAYER_COMPLETED_QUESTS,
- // BoxType.PLAYER_ACHIEVEMENTS,
+ BoxTypes.PLAYER_HERO,
+ BoxTypes.PLAYER_SKILLS,
+ BoxTypes.PLAYER_COLOR_DISPOSITION,
+ BoxTypes.PLAYER_TYPE,
+ BoxTypes.PLAYER_NFT_GALLERY,
+ BoxTypes.PLAYER_DAO_MEMBERSHIPS,
+ BoxTypes.PLAYER_ROLES,
+ BoxTypes.EMBEDDED_URL,
+ BoxTypes.PLAYER_COMPLETED_QUESTS,
+ // BoxTypes.PLAYER_ACHIEVEMENTS,
// TODO: Add more types of sections
];
-export const MULTIPLE_ALLOWED_BOXES = [BoxType.EMBEDDED_URL];
+export const MULTIPLE_ALLOWED_BOXES = [BoxTypes.EMBEDDED_URL] as Array;
export type LayoutMetadata = {
[key: string]: {
- boxType: BoxType;
- boxMetadata: BoxMetadata;
+ type: BoxType;
+ metadata: BoxMetadata;
};
};
-export const getBoxLayoutItemDefaults = (boxId: BoxType): Layout => {
- switch (boxId) {
- case BoxType.PLAYER_HERO:
- return {
- i: getBoxKey(BoxType.PLAYER_HERO, {}),
- x: 0,
- y: 0,
- w: 1,
- h: 22,
- maxW: 1,
- };
- case BoxType.PLAYER_SKILLS:
- return {
- i: getBoxKey(BoxType.PLAYER_SKILLS, {}),
- x: 0,
- y: 0,
- w: 1,
- h: 7,
- maxW: 1,
- };
- case BoxType.PLAYER_NFT_GALLERY:
- return {
- i: getBoxKey(BoxType.PLAYER_NFT_GALLERY, {}),
- x: 0,
- y: 0,
- w: 1,
- h: 14,
- maxW: 1,
- };
- case BoxType.PLAYER_DAO_MEMBERSHIPS:
- return {
- i: getBoxKey(BoxType.PLAYER_DAO_MEMBERSHIPS, {}),
- x: 0,
- y: 0,
- w: 1,
- h: 9,
- maxW: 1,
- };
- case BoxType.PLAYER_ACHIEVEMENTS:
- return {
- i: getBoxKey(BoxType.PLAYER_ACHIEVEMENTS, {}),
- x: 0,
- y: 0,
- w: 1,
- h: 4,
- maxW: 1,
- };
- case BoxType.PLAYER_TYPE:
- return {
- i: getBoxKey(BoxType.PLAYER_TYPE, {}),
- x: 0,
- y: 0,
- w: 1,
- h: 7,
- maxW: 1,
- };
- case BoxType.PLAYER_COLOR_DISPOSITION:
- return {
- i: getBoxKey(BoxType.PLAYER_COLOR_DISPOSITION, {}),
- x: 0,
- y: 0,
- w: 1,
- h: 5.5,
- maxW: 1,
- };
- case BoxType.PLAYER_ROLES:
- return {
- i: getBoxKey(BoxType.PLAYER_ROLES, {}),
- x: 0,
- y: 0,
- w: 1,
- h: 3,
- maxW: 1,
- };
- case BoxType.PLAYER_ADD_BOX:
- return {
- i: getBoxKey(BoxType.PLAYER_ADD_BOX, {}),
- x: 0,
- y: 0,
- w: 1,
- h: 3,
- maxW: 1,
- isResizable: false,
- isDraggable: false,
- };
- case BoxType.EMBEDDED_URL:
- return {
- i: getBoxKey(BoxType.EMBEDDED_URL, {
- url: 'https://github.com/MetaFam/TheGame', // TODO: remove tempUrl
- }),
- x: 0,
- y: 0,
- w: 1,
- h: 6,
- maxW: 1,
- isResizable: false,
- };
+export const getBoxLayoutItemDefaults = (type: BoxType): Layout => {
+ const heights = {
+ [BoxTypes.PLAYER_HERO]: 22,
+ [BoxTypes.PLAYER_SKILLS]: 7,
+ [BoxTypes.PLAYER_NFT_GALLERY]: 14,
+ [BoxTypes.PLAYER_DAO_MEMBERSHIPS]: 9,
+ [BoxTypes.PLAYER_ACHIEVEMENTS]: 4,
+ [BoxTypes.PLAYER_TYPE]: 7,
+ [BoxTypes.PLAYER_COLOR_DISPOSITION]: 5.5,
+ [BoxTypes.PLAYER_ROLES]: 3,
+ [BoxTypes.PLAYER_ADD_BOX]: 3,
+ [BoxTypes.EMBEDDED_URL]: 6,
+ } as Record;
+
+ const ret: Layout = {
+ i: createBoxKey(type),
+ x: 0,
+ y: 0,
+ w: 1,
+ h: heights[type],
+ maxW: 1,
+ };
+
+ switch (type) {
+ case BoxTypes.PLAYER_ADD_BOX: {
+ ret.isResizable = false;
+ ret.isDraggable = false;
+ break;
+ }
+ case BoxTypes.EMBEDDED_URL: {
+ ret.i = createBoxKey(type, {
+ url: 'https://github.com/MetaFam/TheGame', // TODO: remove tempUrl
+ });
+ ret.isResizable = false;
+ break;
+ }
default:
- return {
- i: '',
- x: 0,
- y: 0,
- w: 1,
- h: 1,
- maxW: 1,
- };
}
+
+ return ret;
};
-const DEFAULT_BOXES = [
- BoxType.PLAYER_HERO,
- BoxType.PLAYER_SKILLS,
- BoxType.PLAYER_NFT_GALLERY,
- BoxType.PLAYER_DAO_MEMBERSHIPS,
- BoxType.PLAYER_COLOR_DISPOSITION,
+export const DEFAULT_BOXES = [
+ BoxTypes.PLAYER_HERO,
+ BoxTypes.PLAYER_SKILLS,
+ BoxTypes.PLAYER_NFT_GALLERY,
+ BoxTypes.PLAYER_DAO_MEMBERSHIPS,
+ BoxTypes.PLAYER_COLOR_DISPOSITION,
// Adding default boxes MUST be accompanied by adding default box positions as well
];
-const DEFAULT_BOX_POSITIONS_LG: {
- [boxType: string]: { x: number; y: number };
-} = {
- [BoxType.PLAYER_HERO]: { x: 0, y: 0 },
- [BoxType.PLAYER_COLOR_DISPOSITION]: { x: 0, y: 7 },
- [BoxType.PLAYER_DAO_MEMBERSHIPS]: { x: 1, y: 0 },
- [BoxType.PLAYER_SKILLS]: { x: 1, y: 9 },
- [BoxType.PLAYER_NFT_GALLERY]: { x: 2, y: 0 },
+export type ChakraSize = 'sm' | 'md' | 'lg';
+export type Coordinates = {
+ x: number;
+ y: number;
};
-const DEFAULT_BOX_POSITIONS_MD: {
- [boxType: string]: { x: number; y: number };
-} = {
- [BoxType.PLAYER_HERO]: { x: 0, y: 0 },
- [BoxType.PLAYER_COLOR_DISPOSITION]: { x: 0, y: 5 },
- [BoxType.PLAYER_NFT_GALLERY]: { x: 0, y: 7 },
- [BoxType.PLAYER_DAO_MEMBERSHIPS]: { x: 1, y: 0 },
- [BoxType.PLAYER_SKILLS]: { x: 1, y: 9 },
-};
-const DEFAULT_BOX_POSITIONS_SM: {
- [boxType: string]: { x: number; y: number };
-} = {
- [BoxType.PLAYER_HERO]: { x: 0, y: 0 },
- [BoxType.PLAYER_COLOR_DISPOSITION]: { x: 0, y: 5 },
- [BoxType.PLAYER_DAO_MEMBERSHIPS]: { x: 0, y: 7 },
- [BoxType.PLAYER_SKILLS]: { x: 0, y: 15 },
- [BoxType.PLAYER_NFT_GALLERY]: { x: 0, y: 20 },
- [BoxType.PLAYER_COLOR_DISPOSITION]: { x: 2, y: 9 },
+export type Positions = Record;
+
+const DEFAULT_BOX_POSITIONS: Record = {
+ lg: {
+ [BoxTypes.PLAYER_HERO]: { x: 0, y: 0 },
+ [BoxTypes.PLAYER_COLOR_DISPOSITION]: { x: 1, y: 0 },
+ [BoxTypes.PLAYER_DAO_MEMBERSHIPS]: { x: 2, y: 1 },
+ [BoxTypes.PLAYER_SKILLS]: { x: 1, y: 2 },
+ [BoxTypes.PLAYER_NFT_GALLERY]: { x: 2, y: 0 },
+ } as Positions,
+ md: {
+ [BoxTypes.PLAYER_HERO]: { x: 0, y: 0 },
+ [BoxTypes.PLAYER_COLOR_DISPOSITION]: { x: 1, y: 0 },
+ [BoxTypes.PLAYER_NFT_GALLERY]: { x: 1, y: 3 },
+ [BoxTypes.PLAYER_DAO_MEMBERSHIPS]: { x: 1, y: 2 },
+ [BoxTypes.PLAYER_SKILLS]: { x: 1, y: 1 },
+ } as Positions,
+ sm: {
+ [BoxTypes.PLAYER_HERO]: { x: 0, y: 0 },
+ [BoxTypes.PLAYER_DAO_MEMBERSHIPS]: { x: 0, y: 3 },
+ [BoxTypes.PLAYER_SKILLS]: { x: 0, y: 2 },
+ [BoxTypes.PLAYER_NFT_GALLERY]: { x: 0, y: 4 },
+ [BoxTypes.PLAYER_COLOR_DISPOSITION]: { x: 0, y: 1 },
+ } as Positions,
};
-const DEFAULT_PLAYER_LAYOUTS: Layouts = {
- lg: DEFAULT_BOXES.map((boxType) => ({
- ...getBoxLayoutItemDefaults(boxType),
- ...DEFAULT_BOX_POSITIONS_LG[boxType],
- })),
- md: DEFAULT_BOXES.map((boxType) => ({
- ...getBoxLayoutItemDefaults(boxType),
- ...DEFAULT_BOX_POSITIONS_MD[boxType],
- })),
- sm: DEFAULT_BOXES.map((boxType) => ({
- ...getBoxLayoutItemDefaults(boxType),
- ...DEFAULT_BOX_POSITIONS_SM[boxType],
- })),
-};
+export const DEFAULT_PLAYER_LAYOUTS: Layouts = Object.fromEntries(
+ ['sm', 'md', 'lg'].map((size) => [
+ size,
+ DEFAULT_BOXES.map((boxType) => ({
+ ...getBoxLayoutItemDefaults(boxType),
+ ...DEFAULT_BOX_POSITIONS[size as ChakraSize][boxType],
+ })),
+ ]),
+);
-const DEFAULT_LAYOUT_ITEMS = DEFAULT_BOXES.map((boxType) => ({
- boxType,
- boxMetadata: {},
- boxKey: getBoxKey(boxType, {}),
+export const DEFAULT_LAYOUT_ITEMS = DEFAULT_BOXES.map((type: BoxType) => ({
+ type,
+ key: createBoxKey(type),
}));
export const DEFAULT_PLAYER_LAYOUT_DATA = {
diff --git a/packages/web/components/Profile/EmbeddedUrlSection.tsx b/packages/web/components/Profile/EmbeddedUrlSection.tsx
index df2c3542..01f5c0d7 100644
--- a/packages/web/components/Profile/EmbeddedUrlSection.tsx
+++ b/packages/web/components/Profile/EmbeddedUrlSection.tsx
@@ -10,29 +10,29 @@ import { ProfileSection } from 'components/Profile/ProfileSection';
import { Maybe } from 'graphql/autogen/types';
import { useDelay } from 'lib/hooks/useDelay';
import React, { useCallback, useEffect, useState } from 'react';
-import { BoxType } from 'utils/boxTypes';
+import { BoxTypes } from 'utils/boxTypes';
const metadataLink = '/api/metadata?url=';
type EmbeddedUrlProps = {
url?: string;
- canEdit?: boolean;
+ editing?: boolean;
};
-export const EmbeddedUrl: React.FC = ({ url, canEdit }) => (
+export const EmbeddedUrl: React.FC = ({ url, editing }) => (
-
+
);
interface LinkPreviewProps {
url?: string;
- canEdit?: boolean;
+ editing?: boolean;
}
interface URIMetadata {
@@ -47,21 +47,22 @@ const LinkPreview: React.FC = ({ url: inputUrl = '' }) => {
const [metadata, setMetadata] = useState>(null);
const [loading, setLoading] = useState(true);
- const updateMetadata = useCallback((uri: string) => {
+ const updateMetadata = useCallback(async (uri: string) => {
setLoading(true);
- fetch(metadataLink + uri)
- .then((res) => res.json())
- .then(({ error, response }) => {
- if (error) throw error;
- setMetadata((response.og as unknown) as URIMetadata);
- setLoading(false);
- })
- .catch((err: Error) => {
- // eslint-disable-next-line no-console
- console.error('No metadata could be found for the given URL.', err);
- setMetadata(null);
- setLoading(false);
- });
+ try {
+ const res = await fetch(metadataLink + uri);
+ const { error, response } = await res.json();
+
+ if (error) throw error;
+
+ setMetadata((response.og as unknown) as URIMetadata);
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error(`No metadata found for the URL: ${uri}.`, err);
+ setMetadata(null);
+ } finally {
+ setLoading(false);
+ }
}, []);
const delayedUpdate = useDelay(updateMetadata);
@@ -119,7 +120,7 @@ const LinkPreview: React.FC = ({ url: inputUrl = '' }) => {
)}
- {siteName && {siteName} • }
+ {siteName && {siteName} • }
{url}
diff --git a/packages/web/components/Profile/PlayerSection.tsx b/packages/web/components/Profile/PlayerSection.tsx
index d064cd6c..9747e418 100644
--- a/packages/web/components/Profile/PlayerSection.tsx
+++ b/packages/web/components/Profile/PlayerSection.tsx
@@ -10,82 +10,62 @@ import { PlayerSkills } from 'components/Player/Section/PlayerSkills';
import { PlayerType } from 'components/Player/Section/PlayerType';
import { EmbeddedUrl } from 'components/Profile/EmbeddedUrlSection';
import { Player } from 'graphql/autogen/types';
-import { PersonalityInfo } from 'graphql/queries/enums/getPersonalityInfo';
import React from 'react';
import { FaTimes } from 'react-icons/fa';
-import { BoxMetadata, BoxType, getBoxKey } from 'utils/boxTypes';
+import { BoxMetadata, BoxType, BoxTypes, createBoxKey } from 'utils/boxTypes';
type Props = {
- boxType: BoxType;
- boxMetadata: BoxMetadata;
+ type: BoxType;
+ metadata?: BoxMetadata;
player: Player;
- personalityInfo: PersonalityInfo;
isOwnProfile?: boolean;
- canEdit?: boolean;
+ editing?: boolean;
onRemoveBox?: (boxKey: string) => void;
};
const PlayerSectionInner: React.FC = ({
- boxMetadata,
- boxType,
+ metadata,
+ type,
player,
isOwnProfile,
- personalityInfo,
- canEdit,
+ editing,
}) => {
- switch (boxType) {
- case BoxType.PLAYER_HERO:
- return (
-
- );
- case BoxType.PLAYER_SKILLS:
- return ;
- case BoxType.PLAYER_NFT_GALLERY:
- return ;
- case BoxType.PLAYER_DAO_MEMBERSHIPS:
- return ;
- case BoxType.PLAYER_COLOR_DISPOSITION:
- return (
-
- );
- case BoxType.PLAYER_TYPE:
- return ;
- case BoxType.PLAYER_ROLES:
- return ;
- case BoxType.PLAYER_ACHIEVEMENTS:
- return ;
- case BoxType.PLAYER_COMPLETED_QUESTS:
- return ;
- case BoxType.EMBEDDED_URL: {
- const url = boxMetadata?.url as string;
- return url ? : <>>;
+ switch (type) {
+ case BoxTypes.PLAYER_HERO:
+ return ;
+ case BoxTypes.PLAYER_SKILLS:
+ return ;
+ case BoxTypes.PLAYER_NFT_GALLERY:
+ return ;
+ case BoxTypes.PLAYER_DAO_MEMBERSHIPS:
+ return ;
+ case BoxTypes.PLAYER_COLOR_DISPOSITION:
+ return ;
+ case BoxTypes.PLAYER_TYPE:
+ return ;
+ case BoxTypes.PLAYER_ROLES:
+ return ;
+ case BoxTypes.PLAYER_ACHIEVEMENTS:
+ return ;
+ case BoxTypes.PLAYER_COMPLETED_QUESTS:
+ return ;
+ case BoxTypes.EMBEDDED_URL: {
+ const { url } = metadata ?? {};
+ return url ? : null;
}
default:
- return <>>;
+ return null;
}
};
export const PlayerSection = React.forwardRef(
- (
- {
- boxMetadata,
- boxType,
- player,
- isOwnProfile,
- canEdit,
- onRemoveBox,
- personalityInfo,
- },
- ref,
- ) => {
- const boxKey = getBoxKey(boxType, boxMetadata);
+ ({ metadata, type, player, isOwnProfile, editing, onRemoveBox }, ref) => {
+ const key = createBoxKey(type, metadata);
return (
(
>
- {canEdit && (
+ {editing && (
(
left={0}
/>
)}
- {canEdit && boxType && boxType !== BoxType.PLAYER_HERO && (
+ {editing && type && type !== BoxTypes.PLAYER_HERO && (
(
color="pinkShadeOne"
icon={}
_hover={{ color: 'white' }}
- onClick={() => onRemoveBox?.(boxKey)}
+ onClick={() => onRemoveBox?.(key)}
_focus={{
boxShadow: 'none',
backgroundColor: 'transparent',
diff --git a/packages/web/components/Profile/ProfileSection.tsx b/packages/web/components/Profile/ProfileSection.tsx
index 17b4cdc0..20a537bb 100644
--- a/packages/web/components/Profile/ProfileSection.tsx
+++ b/packages/web/components/Profile/ProfileSection.tsx
@@ -1,10 +1,9 @@
import {
Box,
- BoxProps,
Button,
EditIcon,
Flex,
- HStack,
+ FlexProps,
IconButton,
Modal,
ModalBody,
@@ -18,34 +17,36 @@ import {
} from '@metafam/ds';
import { Maybe } from '@metafam/utils';
import BackgroundImage from 'assets/main-background.jpg';
-import { SetupPersonalityType } from 'components/Setup/SetupPersonalityType';
+import { SetupColorDisposition } from 'components/Setup/SetupColorDisposition';
import { SetupPlayerType } from 'components/Setup/SetupPlayerType';
import { SetupRoles } from 'components/Setup/SetupRoles';
import { SetupSkills } from 'components/Setup/SetupSkills';
import React from 'react';
-import { BoxType } from 'utils/boxTypes';
+import { BoxType, BoxTypes } from 'utils/boxTypes';
export type ProfileSectionProps = {
children?: React.ReactNode;
- isOwnProfile?: boolean;
- canEdit?: boolean;
- boxType?: BoxType;
- title?: string;
+ isOwnProfile?: Maybe;
+ editing?: boolean;
+ type?: BoxType;
+ title?: string | false;
withoutBG?: boolean;
- modalText?: string;
- modalTitle?: string;
+ modalPrompt?: string;
+ modalTitle?: string | false;
modal?: React.ReactNode;
subheader?: string;
-} & BoxProps;
+};
-export const ProfileSection: React.FC = ({
+export const ProfileSection: React.FC<
+ ProfileSectionProps & Omit
+> = ({
children,
isOwnProfile,
- canEdit,
- boxType,
+ editing,
+ type: boxType,
title,
withoutBG = false,
- modalText,
+ modalPrompt,
modal,
modalTitle,
subheader,
@@ -55,129 +56,116 @@ export const ProfileSection: React.FC = ({
return (
- {title && (
-
-
-
- {title}
-
- {!modal && isOwnProfile && !canEdit && isBoxDataEditable(boxType) && (
+ {title !== false && (
+
+
+ {title && (
+
+ {title}
+
+ )}
+ {isOwnProfile && !editing && isEditable(boxType) && (
}
_hover={{ color: 'white' }}
- onClick={onOpen}
- _focus={{
- boxShadow: 'none',
- backgroundColor: 'transparent',
- }}
- _active={{
- transform: 'scale(0.8)',
- backgroundColor: 'transparent',
- }}
+ _focus={{ boxShadow: 'none' }}
+ _active={{ transform: 'scale(0.8)' }}
isRound
+ onClick={onOpen}
/>
)}
- {modal && modalText && (
+ {modal && modalPrompt && (
)}
-
+
)}
{children}
- {boxType && (
-
+ {(boxType || modal) && (
+
-
- {modalTitle || title}
+ {modalTitle !== false && (
+
+ {modalTitle ?? title}
- {subheader && (
-
- {subheader}
-
- )}
-
+ {subheader && (
+
+ {subheader}
+
+ )}
+
+ )}
- {!modal && !modalText && (
-
- )}
- {modalText && modal && {modalText && modal}}
-
+
+ {modal ?? }
+
{/* we should figure out how to unify modal footers (edit sections have their own,
look into EditSectionBox components - they have footers with 'save' and 'cancel' buttons) */}
- {modalText && modal && (
-
+ {modal && (
+
@@ -422,7 +394,7 @@ const Role: React.FC = ({
)}
{showDetails && (
-
+
{role.description}
)}
diff --git a/packages/web/components/Setup/SetupSkills.tsx b/packages/web/components/Setup/SetupSkills.tsx
index 10ce6563..ad3db5ba 100644
--- a/packages/web/components/Setup/SetupSkills.tsx
+++ b/packages/web/components/Setup/SetupSkills.tsx
@@ -1,16 +1,12 @@
import {
- Button,
- MetaButton,
- MetaHeading,
+ Center,
+ Flex,
MetaTheme,
- ModalBody,
- ModalFooter,
- searchSelectStyles,
+ multiSelectStyles,
SelectSearch,
- useToast,
+ Spinner,
+ Text,
} from '@metafam/ds';
-import { FlexContainer } from 'components/Container';
-import { useSetupFlow } from 'contexts/SetupContext';
import {
Skill,
SkillCategory_Enum,
@@ -18,20 +14,30 @@ import {
} from 'graphql/autogen/types';
import { getSkills } from 'graphql/queries/enums/getSkills';
import { SkillColors } from 'graphql/types';
-import { useUser } from 'lib/hooks';
-import React, { CSSProperties, useCallback, useEffect, useState } from 'react';
+import { useMounted, useOverridableField, useUser } from 'lib/hooks';
+import React, { CSSProperties, useEffect, useMemo, useState } from 'react';
import { CategoryOption, parseSkills, SkillOption } from 'utils/skillHelpers';
+import {
+ MaybeModalProps,
+ WizardPane,
+ WizardPaneCallbackProps,
+} from './WizardPane';
+
export type SetupSkillsProps = {
isEdit?: boolean;
onClose?: () => void;
};
-const styles: typeof searchSelectStyles = {
- ...searchSelectStyles,
+const styles: typeof multiSelectStyles = {
+ ...multiSelectStyles,
+ container: (s: CSSProperties) => ({
+ ...s,
+ width: 'min(95vw, 40rem)',
+ }),
menuList: (s: CSSProperties) => ({
...s,
- minHeight: '75vh',
+ minHeight: 'min(15rem, 60vh)',
}),
multiValue: (s: CSSProperties, { data }: { data: Skill }) => ({
...s,
@@ -48,7 +54,7 @@ const styles: typeof searchSelectStyles = {
{ children }: { children: SkillCategory_Enum },
) => ({
...s,
- ...searchSelectStyles.groupHeading?.(s, { children }),
+ ...multiSelectStyles.groupHeading?.(s, { children }),
background: SkillColors[children],
}),
option: (
@@ -58,140 +64,142 @@ const styles: typeof searchSelectStyles = {
...s,
color:
isSelected || isFocused ? MetaTheme.colors.black : MetaTheme.colors.white,
- ':hover': {
+ background:
+ isSelected || isFocused
+ ? MetaTheme.colors.blue[50]
+ : MetaTheme.colors.dark,
+ ':hover, :focus, :active': {
background: MetaTheme.colors.green[50],
color: MetaTheme.colors.black,
},
}),
};
-export const SetupSkills: React.FC = ({
- isEdit,
+export const SetupSkills: React.FC = ({
onClose,
+ buttonLabel,
}) => {
- const { onNextPress, nextButtonLabel } = useSetupFlow();
- const { user } = useUser({ requestPolicy: 'network-only' });
- const toast = useToast();
- const [skillChoices, setSkillChoices] = useState>([]);
- const [updateSkillsRes, updateSkills] = useUpdatePlayerSkillsMutation();
- const [loading, setLoading] = useState(false);
- const [playerSkills, setPlayerSkills] = useState>([]);
- const isWizard = !isEdit;
+ const field = 'skills';
+ const mounted = useMounted();
+ const [choices, setChoices] = useState>();
+ const { user } = useUser();
+ const { value: strippedSkills, setter: setValue } = useOverridableField<
+ Array
+ >({
+ field: 'skills',
+ loaded: !!user,
+ });
+ const modal = !!onClose;
+ const [, updateSkills] = useUpdatePlayerSkillsMutation();
+ const skills = useMemo(
+ () =>
+ strippedSkills?.map(
+ (skill) =>
+ ({
+ ...skill,
+ get label() {
+ return this.name;
+ },
+ get value() {
+ return this.id;
+ },
+ } as SkillOption),
+ ),
+ [strippedSkills],
+ );
useEffect(() => {
- if (user) {
- if (user.skills && user.skills.length > 0 && playerSkills.length === 0) {
- setPlayerSkills(
- user.skills.map(({ Skill: skill }) => ({
- value: skill.id,
- label: skill.name,
- ...skill,
- })),
+ if (user && setValue && choices && !skills) {
+ if (user.skills.length > 0) {
+ const options = choices.map(({ options: opts }) => opts).flat();
+ setValue(
+ user.skills.map(({ Skill: { id: sid } }) =>
+ options.find(({ id: cid }) => sid === cid),
+ ),
);
}
}
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
+ }, [choices, setValue, user, skills]);
useEffect(() => {
const fetchSkills = async () => {
- const skills = await getSkills();
- setSkillChoices(parseSkills(skills));
+ const skillChoices = await getSkills();
+ setChoices(parseSkills(skillChoices));
};
fetchSkills();
}, []);
- const save = useCallback(async () => {
- if (!user) return;
-
- setLoading(true);
+ const onSave = async ({
+ values: { skills: skillList },
+ setStatus,
+ }: {
+ values: Record;
+ setStatus?: (msg: string) => void;
+ }) => {
+ setStatus?.('Writing to Hasura…');
const { error } = await updateSkills({
- skills: playerSkills.map(({ id }) => ({ skill_id: id })),
+ skills: (skillList as Array).map(({ id }) => ({
+ skill_id: id,
+ })),
});
if (error) {
- console.warn(error); // eslint-disable-line no-console
- toast({
- title: 'Update Error',
- description: `Unable to update skills. Error: ${error}`,
- status: 'error',
- isClosable: true,
- });
- setLoading(false);
+ throw new Error(`Unable to update skills. Error: ${error}`);
}
- }, [user, playerSkills, toast, updateSkills]);
- const handleNextPress = useCallback(async () => {
- setLoading(true);
- await save();
- onNextPress();
- }, [save, onNextPress]);
+ if (setValue) {
+ setStatus?.('Setting Local State…');
+ setValue(skillList);
+ }
+ };
- const setup = (
-
- {isWizard && (
-
- What are your superpowers?
-
- )}
-
- setPlayerSkills(value as Array)}
- options={skillChoices}
- autoFocus
- closeMenuOnSelect={false}
- placeholder="Add Your Skills…"
- />
-
+ return (
+ >
+ {...{ field, onClose, onSave, buttonLabel }}
+ title="Skills"
+ prompt="What are your superpowers?"
+ fetching={!user}
+ value={skills}
+ >
+ {({
+ register,
+ setter,
+ current,
+ }: WizardPaneCallbackProps>) => {
+ const { ref: registerRef, onChange, ...props } = register(field, {});
- {isWizard && (
-
- {nextButtonLabel}
-
- )}
-
- );
- return isWizard ? (
- setup
- ) : (
- <>
- {setup}
- {isEdit && onClose && (
-
-
- {
- await save();
- onClose();
+ if (choices == null || !mounted) {
+ return (
+
+
+ Loading Options…
+
+ );
+ }
+
+ return (
+
+ {
+ const values = (newValue as unknown) as Array;
+ setter(values);
}}
- >
- Save Changes
-
-
- Close
-
-
-
- )}
- >
+ options={choices}
+ value={current}
+ autoFocus
+ closeMenuOnSelect={false}
+ placeholder="Add your skills…"
+ menuShouldScrollIntoView={true}
+ menuPlacement={modal ? 'auto' : 'top'}
+ {...props}
+ />
+
+ );
+ }}
+
);
};
diff --git a/packages/web/components/Setup/SetupTimeZone.tsx b/packages/web/components/Setup/SetupTimeZone.tsx
index 387a45b5..34c378a3 100644
--- a/packages/web/components/Setup/SetupTimeZone.tsx
+++ b/packages/web/components/Setup/SetupTimeZone.tsx
@@ -1,79 +1,41 @@
-import { MetaButton, MetaHeading, SelectTimeZone, useToast } from '@metafam/ds';
-import { FlexContainer } from 'components/Container';
-import { useSetupFlow } from 'contexts/SetupContext';
-import { useUpdateProfileMutation } from 'graphql/autogen/types';
-import { useUser } from 'lib/hooks';
-import React, { useEffect, useState } from 'react';
+import { Center, SelectTimeZone, Text } from '@metafam/ds';
+import { useMounted } from 'lib/hooks';
+import React from 'react';
+import { Controller } from 'react-hook-form';
+
+import { ProfileWizardPane } from './ProfileWizardPane';
+import { WizardPaneCallbackProps } from './WizardPane';
export const SetupTimeZone: React.FC = () => {
- const { onNextPress, nextButtonLabel } = useSetupFlow();
- const [timeZone, setTimeZone] = useState('');
- const { user } = useUser();
- const toast = useToast();
-
- const [updateProfileRes, updateProfile] = useUpdateProfileMutation();
- const [loading, setLoading] = useState(false);
-
- useEffect(() => {
- if (user) {
- if (user.profile?.timeZone && !timeZone) {
- setTimeZone(user.profile.timeZone);
- }
- }
- }, [user, timeZone]);
-
- const handleNextPress = async () => {
- if (!user) return;
-
- setLoading(true);
- const { error } = await updateProfile({
- playerId: user.id,
- input: { timeZone },
- });
-
- if (error) {
- toast({
- title: 'Error',
- description: `Unable to update your time zone: ${error.message}`,
- status: 'error',
- isClosable: true,
- });
- setLoading(false);
- return;
- }
-
- onNextPress();
- };
-
- const [isComponentMounted, setIsComponentMounted] = useState(false);
-
- useEffect(() => setIsComponentMounted(true), []);
-
- if (!isComponentMounted) {
- return null;
- }
+ const field = 'timeZone';
+ const mounted = useMounted();
return (
-
-
- Which time zone are you in?
-
-
- setTimeZone(tz.value)}
- labelStyle="abbrev"
- />
-
-
- {nextButtonLabel}
-
-
+
+ {({ control }: WizardPaneCallbackProps) => (
+
+
+ !mounted ? (
+ ⸘Not Mounted‽ // avoiding “different className” error
+ ) : (
+ onChange(tz.value)}
+ {...props}
+ />
+ )
+ }
+ />
+
+ )}
+
);
};
diff --git a/packages/web/components/Setup/SetupUsername.tsx b/packages/web/components/Setup/SetupUsername.tsx
index ef3711f5..03fb17d2 100644
--- a/packages/web/components/Setup/SetupUsername.tsx
+++ b/packages/web/components/Setup/SetupUsername.tsx
@@ -1,78 +1,61 @@
-import { Input, MetaButton, MetaHeading, useToast } from '@metafam/ds';
-import { FlexContainer } from 'components/Container';
-import { useSetupFlow } from 'contexts/SetupContext';
-import { useUpdatePlayerUsernameMutation } from 'graphql/autogen/types';
-import { useUser } from 'lib/hooks';
-import React, { useState } from 'react';
+import { Flex, Input } from '@metafam/ds';
+import { getPlayer } from 'graphql/getPlayer';
+import React from 'react';
-export type SetupUsernameProps = {
- username: string | undefined;
- setUsername: React.Dispatch>;
-};
+import { ProfileWizardPane } from './ProfileWizardPane';
+import { WizardPaneCallbackProps } from './WizardPane';
-export const SetupUsername: React.FC = ({
- username,
- setUsername,
-}) => {
- const { onNextPress, nextButtonLabel } = useSetupFlow();
- const { user } = useUser();
- const toast = useToast();
-
- const [updateUsernameRes, updateUsername] = useUpdatePlayerUsernameMutation();
- const [loading, setLoading] = useState(false);
-
- const handleNextPress = async () => {
- if (!user) return;
-
- setLoading(true);
- const { error } = await updateUsername({
- playerId: user.id,
- username: username ?? '',
- });
-
- if (error) {
- let errorDetail = 'The octo is sad 😢';
- if (error.message.includes('Uniqueness violation')) {
- errorDetail = 'This username is already taken 😢';
- } else if (error.message.includes('username_is_valid')) {
- errorDetail =
- 'A username can only contain lowercase letters, numbers, and dashes.';
- }
- toast({
- title: 'Error',
- description: `Unable to update Player Username. ${errorDetail}`,
- status: 'error',
- isClosable: true,
- });
- setLoading(false);
- return;
- }
-
- onNextPress();
- };
+export const SetupUsername: React.FC = () => {
+ const field = 'username';
return (
-
-
- What username would you like?
-
- setUsername(value)}
- w="auto"
- />
+
+ {({ register, dirty, errored }: WizardPaneCallbackProps) => {
+ const { ref: registerRef, ...props } = register(field, {
+ validate: async (value: string) => {
+ if (/^0x[0-9a-z]{40}$/i.test(value)) {
+ return `Username “${value}” has the same format as an Ethereum address.`;
+ }
+ if (dirty && (await getPlayer(value))) {
+ return `Username “${value}” is already in use.`;
+ }
+ return true;
+ },
+ pattern: {
+ value: /^[a-z0-9-_]+$/,
+ message:
+ 'Only lowercase letters, digits, dashes, & underscores allowed.',
+ },
+ minLength: {
+ value: 3,
+ message: 'Must have at least three characters.',
+ },
+ maxLength: {
+ value: 150,
+ message: 'Maximum length is 150 characters.',
+ },
+ });
-
- {nextButtonLabel}
-
-
+ return (
+
+ {
+ ref?.focus();
+ registerRef(ref);
+ }}
+ {...props}
+ />
+
+ );
+ }}
+
);
};
diff --git a/packages/web/components/Setup/WizardPane.tsx b/packages/web/components/Setup/WizardPane.tsx
new file mode 100644
index 00000000..d950da43
--- /dev/null
+++ b/packages/web/components/Setup/WizardPane.tsx
@@ -0,0 +1,260 @@
+import {
+ Box,
+ Button,
+ chakra,
+ Flex,
+ FormControl,
+ FormErrorMessage,
+ Image,
+ MetaButton,
+ MetaHeading,
+ Spinner,
+ Stack,
+ StatusedSubmitButton,
+ Text,
+ Tooltip,
+ useToast,
+ Wrap,
+ WrapItem,
+} from '@metafam/ds';
+import { Maybe, Optional } from '@metafam/utils';
+import cursiveTitle from 'assets/cursive-title-small.png';
+import discord from 'assets/discord.svg';
+import { FlexContainer } from 'components/Container';
+import { HeadComponent } from 'components/Seo';
+import { useSetupFlow } from 'contexts/SetupContext';
+import { CeramicError, useWeb3 } from 'lib/hooks';
+import {
+ PropsWithChildren,
+ ReactElement,
+ useCallback,
+ useEffect,
+ useState,
+} from 'react';
+import { Control, useForm, UseFormRegisterReturn } from 'react-hook-form';
+
+export type MaybeModalProps = {
+ buttonLabel?: string | ReactElement;
+ onClose?: () => void;
+};
+
+export type WizardPaneProps = {
+ field: string;
+ title?: string | ReactElement;
+ prompt?: string | ReactElement;
+ buttonLabel?: string | ReactElement;
+ onClose?: () => void;
+};
+
+export type PaneProps = WizardPaneProps & {
+ value: Optional>;
+ fetching?: boolean;
+ authenticating?: boolean;
+ onSave?: ({
+ values,
+ setStatus,
+ }: {
+ values: Record;
+ setStatus?: (msg: string) => void;
+ }) => Promise;
+};
+
+export type WizardPaneCallbackProps = {
+ register: (
+ field: string,
+ opts: Record,
+ ) => UseFormRegisterReturn;
+ control: Control;
+ loading: boolean;
+ errored: boolean;
+ dirty: boolean;
+ current: T;
+ setter: (arg: T | ((prev: Optional>) => Maybe)) => void;
+};
+
+export const WizardPane = ({
+ field,
+ title,
+ prompt,
+ buttonLabel,
+ onClose,
+ onSave,
+ value: existing,
+ fetching = false,
+ children,
+}: PropsWithChildren>) => {
+ const { onNextPress, nextButtonLabel } = useSetupFlow();
+ const [status, setStatus] = useState>();
+ const {
+ register,
+ control,
+ handleSubmit,
+ setValue,
+ watch,
+ formState: { errors, isValidating: validating, dirtyFields },
+ } = useForm();
+ const current = watch(field, existing);
+ const dirty = current !== existing || dirtyFields[field];
+ const { connecting, connected, connect } = useWeb3();
+ const toast = useToast();
+
+ useEffect(() => {
+ setValue(field, existing);
+ }, [existing, field, setValue]);
+
+ const onSubmit = useCallback(
+ async (values) => {
+ try {
+ if (!dirty) {
+ setStatus('No Change. Skipping Save…');
+ await new Promise((resolve) => {
+ setTimeout(resolve, 10);
+ });
+ } else if (onSave) {
+ setStatus('Saving…');
+ await onSave({ values, setStatus });
+ }
+
+ (onClose ?? onNextPress).call(this);
+ } catch (err) {
+ const heading = err instanceof CeramicError ? 'Ceramic Error' : 'Error';
+ toast({
+ title: heading,
+ description: (err as Error).message,
+ status: 'error',
+ isClosable: true,
+ duration: 12000,
+ });
+ setStatus(null);
+ }
+ },
+ [dirty, onClose, onNextPress, onSave, toast],
+ );
+
+ const setter = useCallback(
+ (val: unknown) => {
+ let next = val;
+ if (val instanceof Function) {
+ next = val(current);
+ }
+ setValue(field, next);
+ },
+ [current, field, setValue],
+ );
+
+ if (!connecting && !connected) {
+ return (
+
+
+
+
+ Connect To Progress
+
+
+
+
+
+ Get Help
+
+
+
+
+ 📚 Learn More
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ {title && (
+
+ {title}
+
+ )}
+ {prompt && (
+
+ {typeof prompt === 'string' ? (
+
+ {prompt}
+
+ ) : (
+ prompt
+ )}
+
+ )}
+
+ {(!connected || fetching || validating) && (
+
+
+
+ {(() => {
+ if (!connected) return 'Authenticating…';
+ if (validating) return 'Validating…';
+ return 'Loading Current Value…';
+ })()}
+
+
+ )}
+
+ {typeof children === 'function'
+ ? children.call(null, {
+ register,
+ control,
+ loading: !connected || fetching,
+ errored: !!errors[field],
+ dirty,
+ current,
+ setter,
+ })
+ : children}
+
+ {errors[field]?.message}
+
+
+
+
+
+
+
+
+ {onClose && (
+
+
+ Close
+
+
+ )}
+
+
+ );
+};
diff --git a/packages/web/graphql/fragments.ts b/packages/web/graphql/fragments.ts
index 7b55fde1..a0bb41a5 100644
--- a/packages/web/graphql/fragments.ts
+++ b/packages/web/graphql/fragments.ts
@@ -44,12 +44,7 @@ export const PlayerFragment = /* GraphQL */ `
availableHours
timeZone
colorMask
- explorerType {
- id
- title
- description
- imageURL
- }
+ explorerTypeTitle
}
daohausMemberships @skip(if: $forLoginDisplay) {
@@ -62,7 +57,7 @@ export const PlayerFragment = /* GraphQL */ `
version
totalShares
chain
- avatarUrl
+ avatarURL
}
}
diff --git a/packages/web/graphql/getMemberships.ts b/packages/web/graphql/getMemberships.ts
index 23a77209..4ab6d279 100644
--- a/packages/web/graphql/getMemberships.ts
+++ b/packages/web/graphql/getMemberships.ts
@@ -16,7 +16,7 @@ const daoMembershipsQuery = /* GraphQL */ `
title
version
chain
- avatarUrl
+ avatarURL
}
}
}
@@ -70,12 +70,12 @@ export type GuildMembership = {
memberId: string;
memberShares?: string;
memberRank?: string;
- memberXp?: number;
+ memberXP?: number;
title?: string;
daoShares?: string;
chain?: string;
address?: string;
- logoUrl?: string;
+ logoURL?: string;
guildname?: string;
};
@@ -91,22 +91,22 @@ export const getAllMemberships = async (player: Player) => {
),
);
- const memberships: GuildMembership[] = [
+ const memberships: Array = [
...(guildPlayers || []).map((gp) => ({
memberId: `${gp.guild_id}:${player.id}`,
title: gp.Guild.name,
guildname: gp.Guild.guildname,
- memberRank: gp.discordRoles[0].name || undefined,
- memberXp: gp.Guild.guildname === 'metafam' ? player.totalXP : null,
- logoUrl: gp.Guild.logo || undefined,
+ memberRank: gp.discordRoles[0].name ?? undefined,
+ memberXP: gp.Guild.guildname === 'metafam' ? player.totalXP : null,
+ logoURL: gp.Guild.logo ?? undefined,
})),
...(daohausMemberships || []).map((m) => ({
memberId: m.id,
- title: m.moloch.title || undefined,
+ title: m.moloch.title ?? undefined,
memberShares: m.shares,
daoShares: m.moloch.totalShares,
chain: m.moloch.chain,
- logoUrl: m.moloch.avatarUrl || undefined,
+ logoURL: m.moloch.avatarURL ?? undefined,
address: m.molochAddress,
})),
];
diff --git a/packages/web/graphql/getPlayers.ts b/packages/web/graphql/getPlayers.ts
index edd471d3..a90ca5a0 100644
--- a/packages/web/graphql/getPlayers.ts
+++ b/packages/web/graphql/getPlayers.ts
@@ -133,6 +133,7 @@ export const getPlayersWithCount = async (
const playerUsernamesQuery = /* GraphQL */ `
query GetPlayerUsernames($limit: Int) {
player(order_by: { totalXP: desc }, limit: $limit) {
+ ethereumAddress
profile {
username
}
@@ -140,7 +141,9 @@ const playerUsernamesQuery = /* GraphQL */ `
}
`;
-export const getPlayerUsernames = async (limit = 150): Promise => {
+export const getPlayerUsernames = async (
+ limit = 150,
+): Promise }>> => {
const { data, error } = await defaultClient
.query(
playerUsernamesQuery,
@@ -148,15 +151,12 @@ export const getPlayerUsernames = async (limit = 150): Promise => {
)
.toPromise();
- if (!data) {
- if (error) {
- throw error;
- }
- return [];
- }
- return data.player
- .map(({ profile }) => profile?.username ?? null)
- .filter((u) => !!u) as Array;
+ if (error) throw error;
+
+ return (data?.player ?? []).map(({ ethereumAddress: address, profile }) => ({
+ address,
+ username: profile?.username ?? null,
+ }));
};
export const getTopPlayerUsernames = getPlayerUsernames;
diff --git a/packages/web/graphql/mutations/idxCache.ts b/packages/web/graphql/mutations/idxCache.ts
index 59dd7e09..21a9be4c 100644
--- a/packages/web/graphql/mutations/idxCache.ts
+++ b/packages/web/graphql/mutations/idxCache.ts
@@ -1,7 +1,5 @@
export const InsertCacheInvalidation = /* GraphQL */ `
mutation InsertCacheInvalidation($playerId: uuid!) {
- updateIDXProfile(playerId: $playerId) {
- success
- }
+ updateIDXProfile(playerId: $playerId)
}
`;
diff --git a/packages/web/graphql/types.ts b/packages/web/graphql/types.ts
index 9e421834..157df470 100644
--- a/packages/web/graphql/types.ts
+++ b/packages/web/graphql/types.ts
@@ -27,7 +27,7 @@ export type PersonalityOption = {
};
export type Membership = Pick & {
- moloch: Pick;
+ moloch: Pick;
};
export type MeType =
diff --git a/packages/web/lib/hooks/brightId.ts b/packages/web/lib/hooks/brightId.ts
index 8c47fec9..cbab5c81 100644
--- a/packages/web/lib/hooks/brightId.ts
+++ b/packages/web/lib/hooks/brightId.ts
@@ -40,7 +40,6 @@ export const useBrightIdStatus = ({
const verified = isStatusVerified(player.brightid_status, contextId);
const deeplink = `${DEEPLINK_ENDPOINT}/${contextId}`;
const universalLink = `${UNIVERSAL_LINK_ENDPOINT}/${contextId}`;
-
return { verified, deeplink, universalLink };
}
return undefined;
@@ -66,7 +65,7 @@ export const useBrightIdUpdated = ({
player: Player;
poll: boolean;
}): void => {
- const contextId = player.id;
+ const contextId = player?.id;
useEffect(() => {
if (!contextId || !poll) return () => undefined;
diff --git a/packages/web/lib/hooks/opensea.ts b/packages/web/lib/hooks/opensea.ts
index e4764885..c806a1a8 100644
--- a/packages/web/lib/hooks/opensea.ts
+++ b/packages/web/lib/hooks/opensea.ts
@@ -1,41 +1,44 @@
+import { Maybe } from '@metafam/utils';
import { Player } from 'graphql/autogen/types';
import { useEffect, useState } from 'react';
import { Collectible } from 'utils/openseaHelpers';
export const useOpenSeaCollectibles = ({
- player,
+ player: { ethereumAddress: owner },
}: {
player: Player;
}): {
favorites: Array;
data: Array;
loading: boolean;
+ error: Maybe;
} => {
const [favorites, setFavorites] = useState>([]);
const [data, setData] = useState>([]);
const [loading, setLoading] = useState(false);
- const owner = player.ethereumAddress;
+ const [error, setError] = useState>(null);
useEffect(() => {
- async function load() {
+ const load = async () => {
setLoading(true);
+ setError(null);
try {
- if (owner) {
- const allData = await fetchAllOpenSeaData(owner);
- setData(allData);
- setFavorites(allData.slice(0, 3));
- }
+ const allData = await fetchAllOpenSeaData(owner);
+ setData(allData);
+ setFavorites(allData.slice(0, 3));
+ } catch (err) {
+ setError((err as Error).message);
} finally {
setLoading(false);
}
- }
+ };
if (owner) {
load();
}
}, [owner]);
- return { favorites, data, loading };
+ return { favorites, data, loading, error };
};
const fetchAllOpenSeaData = async (
@@ -59,17 +62,11 @@ const fetchOpenSeaData = async (
offset: number,
limit: number,
): Promise> => {
- try {
- const res = await fetch(
- `/api/opensea?owner=${owner}&offset=${offset}&limit=${limit}`,
- );
- const { assets, error } = await res.json();
- if (error) throw new Error(error);
- if (!assets) throw new Error('Received empty assets');
- return assets;
- } catch (err) {
- // eslint-disable-next-line no-console
- console.error(`Error Retrieving OpenSea Assets: ${(err as Error).message}`);
- return Promise.resolve([]);
- }
+ const res = await fetch(
+ `/api/opensea?owner=${owner}&offset=${offset}&limit=${limit}`,
+ );
+ const { assets, error } = await res.json();
+ if (error) throw new Error(error);
+ if (!assets) throw new Error(`Received ${assets} assets`);
+ return assets;
};
diff --git a/packages/web/lib/hooks/useField.ts b/packages/web/lib/hooks/useField.ts
index cafea155..4d2db7c8 100644
--- a/packages/web/lib/hooks/useField.ts
+++ b/packages/web/lib/hooks/useField.ts
@@ -1,13 +1,19 @@
import { httpLink, Maybe, Optional } from '@metafam/utils';
import { ExplorerType, Player, Profile } from 'graphql/autogen/types';
import { Atom, atom as newAtom, PrimitiveAtom, useAtom } from 'jotai';
-import { useMemo } from 'react';
+import { optimizedImage } from 'utils/imageHelpers';
+
+// eslint-disable-next-line import/no-cycle
+import { useUser } from './useUser';
export type ProfileFieldType = {
[field in keyof Profile]?: Maybe;
} & {
value: Maybe;
setter: Maybe<(value: unknown) => void>;
+ owner: Maybe;
+ user: Maybe;
+ fetching: boolean;
};
export type ProfileValueType = string | number | Array | ExplorerType;
@@ -24,40 +30,53 @@ export const clearJotaiState = () => {
export const useProfileField = ({
field,
player = null,
- owner = false,
getter = null,
}: {
field: string;
player?: Maybe;
- owner?: boolean;
getter?: Maybe<(player: Maybe) => Optional>>;
}): ProfileFieldType => {
+ const { fetching, user } = useUser();
+ player ??= user; // eslint-disable-line no-param-reassign
+ const owner = user ? user.id === player?.id : null;
const key = field as keyof Profile;
+ let value = player?.profile?.[key];
let setter: Maybe<(val: unknown) => void> = null;
- let value = useMemo(
- () => (getter ? getter(player) : player?.profile?.[key]) ?? null,
- [key, getter, player],
- );
let atom = owner ? fields[field] : null;
- if (!atom && owner && player) {
+ if (!atom && owner) {
// eslint-disable-next-line no-multi-assign
fields[field] = atom = newAtom>(value);
}
// eslint-disable-next-line @typescript-eslint/no-shadow
- const ret = useAtom((atom ?? nullAtom) as PrimitiveAtom>);
+ const response = useAtom(
+ (atom ?? nullAtom) as PrimitiveAtom>,
+ );
+ console.debug({ field, player, value, response });
if (atom) {
- [value, setter] = ret;
+ [value, setter] = response;
}
- if (field.endsWith('ImageURL')) {
- value = httpLink(value);
+ // to unset, set value = null
+ if (value == null) {
+ value = getter?.(player);
+ }
+
+ if (typeof value === 'string' && /^\w{1,10}:\/\/./.test(value)) {
+ if (field.endsWith('ImageURL')) {
+ value = optimizedImage(field, value);
+ } else if (field.endsWith('URL')) {
+ value = httpLink(value);
+ }
}
return {
value,
setter,
[field]: value,
+ owner,
+ user,
+ fetching,
};
};
diff --git a/packages/web/lib/hooks/useSaveCeramicProfile.ts b/packages/web/lib/hooks/useSaveCeramicProfile.ts
index 1e00f0eb..0ed62099 100644
--- a/packages/web/lib/hooks/useSaveCeramicProfile.ts
+++ b/packages/web/lib/hooks/useSaveCeramicProfile.ts
@@ -60,7 +60,6 @@ export const useSaveCeramicProfile = ({
useProfileField({
field: key,
player: user,
- owner: true,
});
return [key, setter];
}),
diff --git a/packages/web/lib/hooks/useUser.ts b/packages/web/lib/hooks/useUser.ts
index 33db4914..e338603f 100644
--- a/packages/web/lib/hooks/useUser.ts
+++ b/packages/web/lib/hooks/useUser.ts
@@ -1,5 +1,6 @@
import { Maybe } from '@metafam/utils';
import { Player, useGetMeQuery } from 'graphql/autogen/types';
+// eslint-disable-next-line import/no-cycle
import { useWeb3 } from 'lib/hooks/useWeb3';
import { useRouter } from 'next/router';
import { useEffect, useMemo } from 'react';
@@ -16,13 +17,14 @@ export const useUser = ({
redirectIfNotFound = false,
requestPolicy = 'cache-first',
}: UseUserOpts = {}): {
+ connecting: boolean;
+ connected: boolean;
user: Maybe;
fetching: boolean;
error?: CombinedError;
} => {
const { authToken, connecting, connected } = useWeb3();
const router = useRouter();
-
const [{ data, error, fetching }] = useGetMeQuery({
pause: connecting || !connected || !authToken,
requestPolicy,
@@ -48,5 +50,11 @@ export const useUser = ({
}
}, [router, user, fetching, connecting, redirectIfNotFound, redirectTo]);
- return { user, fetching, error };
+ return {
+ connecting,
+ connected,
+ user,
+ fetching,
+ error,
+ };
};
diff --git a/packages/web/next.config.js b/packages/web/next.config.js
index a5c4656b..4fde2eb4 100644
--- a/packages/web/next.config.js
+++ b/packages/web/next.config.js
@@ -25,6 +25,16 @@ module.exports = withTM(
destination: '/community/guilds',
permanent: false,
},
+ {
+ source: '/profile/setup',
+ destination: '/profile/setup/username',
+ permanent: false,
+ },
+ {
+ source: '/join',
+ destination: '/profile/setup',
+ permanent: false,
+ },
];
},
async rewrites() {
diff --git a/packages/web/package.json b/packages/web/package.json
index 1b98926e..e966ef2f 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -34,6 +34,7 @@
"cids": "1.1.9",
"classnames": "2.3.1",
"copy-to-clipboard": "3.3.1",
+ "deep-equal": "^2.0.5",
"dids": "2.4.3",
"draft-js": "0.11.7",
"draftjs-to-html": "0.9.1",
@@ -71,6 +72,7 @@
},
"devDependencies": {
"@types/busboy": "0.3.1",
+ "@types/deep-equal": "^1.0.1",
"@types/react": "17.0.6",
"@types/react-grid-layout": "1.1.3",
"@types/react-vis": "1.11.10"
diff --git a/packages/web/pages/api/opensea.ts b/packages/web/pages/api/opensea.ts
index bb15f570..2fa8ee7e 100644
--- a/packages/web/pages/api/opensea.ts
+++ b/packages/web/pages/api/opensea.ts
@@ -3,6 +3,7 @@ import { utils } from 'ethers';
import { NextApiRequest, NextApiResponse } from 'next';
import { OpenSeaAPI } from 'opensea-js';
import { OpenSeaAssetQuery } from 'opensea-js/lib/types';
+import { isEmpty } from 'utils/objectHelpers';
import { Collectible, parseOpenSeaAssets } from 'utils/openseaHelpers';
const opensea = new OpenSeaAPI({ apiKey: CONFIG.openseaApiKey });
@@ -20,12 +21,23 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return res.json({ assets });
} catch (err) {
- return res.json({ error: (err as Error).message });
+ let status = 500;
+ let msg = (err as Error).message;
+ if (/403.*unauthorized/i.test(msg)) {
+ status = 403;
+ msg = 'Unauthorized';
+ if (CONFIG.openseaApiKey == null || isEmpty(CONFIG.openseaApiKey)) {
+ msg += ': Missing OPENSEA_API_KEY Environment Variable';
+ }
+ }
+ return res.status(status).json({ error: msg });
}
} else if (!utils.isAddress(owner as string)) {
- return res.json({ error: `Invalid Owner Address` });
+ return res.status(400).json({ error: `Invalid Owner Address` });
} else {
- return res.json({ error: `Incorrect Method: ${req.method}` });
+ return res
+ .status(405)
+ .json({ error: `Incorrect Method: ${req.method} (GET Supported)` });
}
}
diff --git a/packages/web/pages/guild/[guildname].tsx b/packages/web/pages/guild/[guildname].tsx
index 5b14e74b..7ebecf8d 100644
--- a/packages/web/pages/guild/[guildname].tsx
+++ b/packages/web/pages/guild/[guildname].tsx
@@ -16,7 +16,7 @@ import {
import { useRouter } from 'next/router';
import Page404 from 'pages/404';
import React from 'react';
-import { BoxType } from 'utils/boxTypes';
+import { BoxTypes } from 'utils/boxTypes';
import { getGuildCoverImageFull } from 'utils/playerHelpers';
type Props = InferGetStaticPropsType;
@@ -31,8 +31,8 @@ const GuildPage: React.FC = ({ guild }) => {
// BoxType.GUILD_GALLERY,
const boxes = [
- [BoxType.GUILD_PLAYERS],
- [BoxType.GUILD_ANNOUNCEMENTS, BoxType.GUILD_LINKS],
+ [BoxTypes.GUILD_PLAYERS],
+ [BoxTypes.GUILD_ANNOUNCEMENTS, BoxTypes.GUILD_LINKS],
];
if (router.isFallback) {
@@ -45,11 +45,11 @@ const GuildPage: React.FC = ({ guild }) => {
const getBox = (name: string): React.ReactNode => {
switch (name) {
- case BoxType.GUILD_PLAYERS:
+ case BoxTypes.GUILD_PLAYERS:
return ;
- case BoxType.GUILD_LINKS:
+ case BoxTypes.GUILD_LINKS:
return ;
- case BoxType.GUILD_ANNOUNCEMENTS:
+ case BoxTypes.GUILD_ANNOUNCEMENTS:
return (
No announcements.
diff --git a/packages/web/pages/me.tsx b/packages/web/pages/me.tsx
index 6cafbdc0..d0d1ea04 100644
--- a/packages/web/pages/me.tsx
+++ b/packages/web/pages/me.tsx
@@ -1,23 +1,16 @@
-import { Flex, Link, MetaButton, Spinner, Text, Tooltip } from '@metafam/ds';
-import { getPersonalityInfo } from 'graphql/queries/enums/getPersonalityInfo';
+import { Center, Link, MetaButton, Spinner, Stack, Text } from '@metafam/ds';
import { useMounted, useUser, useWeb3 } from 'lib/hooks';
import { InferGetStaticPropsType } from 'next';
import { PlayerPage } from 'pages/player/[username]';
-export const getStaticProps = async () => {
- const personalityInfo = await getPersonalityInfo();
-
- return {
- props: {
- personalityInfo,
- },
- revalidate: 1,
- };
-};
+export const getStaticProps = async () => ({
+ props: {},
+ revalidate: 1,
+});
type Props = InferGetStaticPropsType;
-const CurrentUserPage: React.FC = ({ personalityInfo }) => {
+const CurrentUserPage: React.FC = () => {
const { connect, connecting, connected } = useWeb3();
const { user, fetching, error } = useUser();
const mounted = useMounted();
@@ -31,25 +24,33 @@ const CurrentUserPage: React.FC = ({ personalityInfo }) => {
);
}
- if (mounted && (connecting || fetching)) {
+ if (connecting || fetching) {
return (
-
-
+
+
+
+ {connecting ? 'Connecting…' : 'Fetching User…'}
+
-
-
+
+
);
}
if (user) {
- return ;
+ return ;
}
if (error) {
return (
-
- Error Loading User: {error.message}
-
+
+
+
+ Error Loading User: {error.message}
+
+ Try Again
+
+
);
}
diff --git a/packages/web/pages/player/[username].tsx b/packages/web/pages/player/[username].tsx
index f0fcd880..1a0f25f8 100644
--- a/packages/web/pages/player/[username].tsx
+++ b/packages/web/pages/player/[username].tsx
@@ -12,8 +12,10 @@ import {
MetaButton,
RepeatClockIcon,
ResponsiveText,
+ useBreakpointValue,
useToast,
} from '@metafam/ds';
+import { Maybe } from '@metafam/utils';
import { PageContainer } from 'components/Container';
import {
ALL_BOXES,
@@ -26,22 +28,19 @@ import {
import { PlayerAddSection } from 'components/Player/Section/PlayerAddSection';
import { PlayerSection } from 'components/Profile/PlayerSection';
import { HeadComponent } from 'components/Seo';
+import deepEquals from 'deep-equal';
import {
Player,
- useInsertCacheInvalidationMutation,
- useUpdatePlayerProfileLayoutMutation,
+ useInsertCacheInvalidationMutation as useInvalidateCache,
+ useUpdatePlayerProfileLayoutMutation as useUpdateLayout,
} from 'graphql/autogen/types';
import { getPlayer } from 'graphql/getPlayer';
import { getTopPlayerUsernames } from 'graphql/getPlayers';
-import {
- getPersonalityInfo,
- PersonalityInfo,
-} from 'graphql/queries/enums/getPersonalityInfo';
-import { useUser, useWeb3 } from 'lib/hooks';
+import { useProfileField, useUser, useWeb3 } from 'lib/hooks';
import { GetStaticPaths, GetStaticPropsContext } from 'next';
import { useRouter } from 'next/router';
import Page404 from 'pages/404';
-import {
+import React, {
ReactElement,
useCallback,
useEffect,
@@ -50,14 +49,20 @@ import {
useState,
} from 'react';
import { Layout, Layouts, Responsive, WidthProvider } from 'react-grid-layout';
-import { BoxMetadata, BoxType, getBoxKey } from 'utils/boxTypes';
+import {
+ BoxMetadata,
+ BoxType,
+ BoxTypes,
+ createBoxKey,
+ getBoxKey,
+} from 'utils/boxTypes';
import {
addBoxToLayouts,
- disableAddBoxInLayoutData,
- enableAddBoxInLayoutData,
+ disableAddBox,
+ enableAddBox,
isSameLayouts,
- onRemoveBoxFromLayouts,
- updateHeightsInLayouts,
+ removeBoxFromLayouts,
+ updatedLayouts,
} from 'utils/layoutHelpers';
import {
getPlayerBannerFull,
@@ -71,14 +76,22 @@ const ResponsiveGridLayout = WidthProvider(Responsive);
type Props = {
player: Player;
- personalityInfo: PersonalityInfo;
};
-export const PlayerPage: React.FC = ({
- player,
- personalityInfo,
-}): ReactElement => {
+export const PlayerPage: React.FC = ({ player }): ReactElement => {
const router = useRouter();
+ const { value: banner } = useProfileField({
+ field: 'bannerImageURL',
+ player,
+ getter: getPlayerBannerFull,
+ });
+ const [, invalidateCache] = useInvalidateCache();
+
+ useEffect(() => {
+ if (player?.id) {
+ invalidateCache({ playerId: player.id });
+ }
+ }, [player?.id, invalidateCache]);
if (router.isFallback) {
return ;
@@ -87,23 +100,24 @@ export const PlayerPage: React.FC = ({
if (!player) return ;
return (
-
+
-
-
+
+
);
@@ -111,34 +125,38 @@ export const PlayerPage: React.FC = ({
export default PlayerPage;
-const getBoxKeyFromTarget = (target: HTMLElement | null): string =>
- (target?.offsetParent as HTMLElement)?.offsetParent?.id ?? '';
-
-const useItemHeights = (items: HTMLElement[]): { [boxKey: string]: number } => {
- const [heights, setHeights] = useState<{ [boxKey: string]: number }>({});
+const useItemHeights = (items: Array>) => {
+ const [heights, setHeights] = useState>({});
useEffect(() => {
const observer = new ResizeObserver((entries) => {
setHeights((oldHeights) => {
- const newHeights = { ...oldHeights };
- entries.forEach((entry) => {
- newHeights[getBoxKeyFromTarget(entry.target as HTMLElement)] =
- entry.contentRect.height;
- });
- return newHeights;
+ const entryHeights = Object.fromEntries(
+ entries.map(({ target }) => [
+ getBoxKey(target as HTMLElement),
+ target.scrollHeight, // entry.contentRect.height,
+ ]),
+ );
+ return { ...oldHeights, ...entryHeights };
});
});
- const newHeights: { [boxKey: string]: number } = {};
+
+ const newHeights: Record = {};
items.forEach((item) => {
- const target = item.children[0] as HTMLElement;
- if (target) {
- newHeights[
- getBoxKeyFromTarget(target)
- ] = target.getBoundingClientRect().height;
- observer.observe(target);
+ if (item) {
+ const target = item.children[0] as HTMLElement;
+ const key = getBoxKey(target);
+ if (key && target) {
+ newHeights[key] = target.scrollHeight;
+ observer.observe(target);
+ } else {
+ // eslint-disable-next-line no-console
+ console.warn(`Missing:`, target, key);
+ }
}
});
setHeights(newHeights);
+
return () => {
observer.disconnect();
};
@@ -147,40 +165,27 @@ const useItemHeights = (items: HTMLElement[]): { [boxKey: string]: number } => {
return heights;
};
-export const Grid: React.FC = ({
- player: initPlayer,
- personalityInfo,
-}): ReactElement => {
+export const Grid: React.FC = ({ player }): ReactElement => {
const [isOwnProfile, setIsOwnProfile] = useState(false);
- const [, invalidateCache] = useInsertCacheInvalidationMutation();
const { user, fetching } = useUser();
const { connected } = useWeb3();
- const [player, setPlayer] = useState(initPlayer);
+ const [saving, setSaving] = useState(false);
const [exitAlertCancel, setExitAlertCancel] = useState(false);
const [exitAlertReset, setExitAlertReset] = useState(false);
-
- useEffect(() => {
- if (!fetching && user && user.id === player?.id) {
- setPlayer(user);
- if (connected) {
- setIsOwnProfile(true);
- }
- }
- }, [user, fetching, connected, player?.id]);
-
- useEffect(() => {
- if (player?.id) {
- invalidateCache({ playerId: player.id });
- }
- }, [player?.id, invalidateCache]);
-
+ const [changed, setChanged] = useState(false);
+ const [editing, setEditing] = useState(false);
+ const itemsRef = useRef>>([]);
+ const heights = useItemHeights(itemsRef.current);
+ const mobile = useBreakpointValue({ base: true, sm: false });
const toast = useToast();
- const [
- { fetching: fetchingSaveRes },
- saveLayoutData,
- ] = useUpdatePlayerProfileLayoutMutation();
- const [saving, setSaving] = useState(false);
+ const [{ fetching: updating }, saveLayoutData] = useUpdateLayout();
+
+ useEffect(() => {
+ if (!fetching && user && user.id === player.id && connected) {
+ setIsOwnProfile(true);
+ }
+ }, [user, fetching, connected, player?.id]);
const savedLayoutData = useMemo(
() =>
@@ -199,17 +204,13 @@ export const Grid: React.FC = ({
layouts: currentLayouts,
} = currentLayoutData;
- const itemsRef = useRef([]);
-
useEffect(() => {
itemsRef.current = itemsRef.current.slice(0, currentLayoutItems.length);
}, [currentLayoutItems]);
- const heights = useItemHeights(itemsRef.current);
-
useEffect(() => {
- const layouts = updateHeightsInLayouts(currentLayouts, heights);
- if (JSON.stringify(layouts) !== JSON.stringify(currentLayouts)) {
+ const layouts = updatedLayouts(currentLayouts, heights);
+ if (!deepEquals(layouts, currentLayouts)) {
setCurrentLayoutData(({ layoutItems }) => ({
layouts,
layoutItems,
@@ -217,21 +218,17 @@ export const Grid: React.FC = ({
}
}, [currentLayouts, heights]);
- const [changed, setChanged] = useState(false);
-
- const [canEdit, setCanEdit] = useState(false);
+ const handleReset = useCallback(() => {
+ setCurrentLayoutData(enableAddBox(DEFAULT_PLAYER_LAYOUT_DATA));
+ setExitAlertReset(false);
+ }, []);
const handleCancel = useCallback(() => {
setCurrentLayoutData(savedLayoutData);
- setCanEdit(false);
+ setEditing(false);
setExitAlertCancel(false);
}, [savedLayoutData]);
- const handleReset = useCallback(() => {
- setCurrentLayoutData(enableAddBoxInLayoutData(DEFAULT_PLAYER_LAYOUT_DATA));
- setExitAlertReset(false);
- }, []);
-
const isDefaultLayout = useMemo(
() => isSameLayouts(DEFAULT_PLAYER_LAYOUT_DATA, currentLayoutData),
[currentLayoutData],
@@ -239,57 +236,64 @@ export const Grid: React.FC = ({
const persistLayoutData = useCallback(
async (layoutData: ProfileLayoutData) => {
- if (!user) return;
+ if (!user) throw new Error('User is not set.');
- setSaving(true);
const { error } = await saveLayoutData({
playerId: user.id,
layout: JSON.stringify(layoutData),
});
- if (error) {
- toast({
- title: 'Error',
- description: `Unable to save layout. Error: ${error}`,
- status: 'error',
- isClosable: true,
- });
- handleCancel();
- } else {
- setCurrentLayoutData(layoutData);
- }
- setSaving(false);
+ if (error) throw error;
},
- [handleCancel, saveLayoutData, toast, user],
+ [saveLayoutData, user],
);
const toggleEditLayout = useCallback(async () => {
- if (canEdit) {
- await persistLayoutData(disableAddBoxInLayoutData(currentLayoutData));
- } else {
- setCurrentLayoutData(enableAddBoxInLayoutData(currentLayoutData));
+ try {
+ let layoutData = DEFAULT_PLAYER_LAYOUT_DATA;
+ if (editing) {
+ setSaving(true);
+ layoutData = disableAddBox(currentLayoutData);
+ await persistLayoutData(layoutData);
+ } else {
+ layoutData = enableAddBox(currentLayoutData);
+ }
+ setCurrentLayoutData(layoutData);
+ setEditing((e) => !e);
+ setChanged(false);
+ } catch (err) {
+ toast({
+ title: 'Error',
+ description: `Unable to save layout. Error: ${(err as Error).message}`,
+ status: 'error',
+ isClosable: true,
+ });
+ } finally {
+ setSaving(false);
}
- setCanEdit(!canEdit);
- setChanged(false);
- }, [canEdit, currentLayoutData, persistLayoutData]);
+ }, [editing, currentLayoutData, persistLayoutData, toast]);
const handleLayoutChange = useCallback(
- (_layoutItems: Layout[], layouts: Layouts) => {
- setCurrentLayoutData({ layouts, layoutItems: currentLayoutItems });
- setChanged(true);
+ (_items: Array, layouts: Layouts) => {
+ const oldData = {
+ layouts: currentLayouts,
+ layoutItems: currentLayoutItems,
+ };
+ const newData = { layouts, layoutItems: currentLayoutItems };
+ // automatic height adjustments dirty `changed`
+ setChanged(changed || (editing && !isSameLayouts(oldData, newData)));
+ setCurrentLayoutData(newData);
},
- [currentLayoutItems],
+ [currentLayouts, currentLayoutItems, editing, changed],
);
- const wrapperSX = useMemo(() => gridConfig.wrapper(canEdit), [canEdit]);
+ const wrapperSX = useMemo(() => gridConfig.wrapper(editing), [editing]);
const onRemoveBox = useCallback(
(boxKey: string): void => {
const layoutData = {
- layouts: onRemoveBoxFromLayouts(boxKey, currentLayouts),
- layoutItems: currentLayoutItems.filter(
- (item) => item.boxKey !== boxKey,
- ),
+ layouts: removeBoxFromLayouts(currentLayouts, boxKey),
+ layoutItems: currentLayoutItems.filter((item) => item.key !== boxKey),
};
setCurrentLayoutData(layoutData);
setChanged(true);
@@ -298,17 +302,14 @@ export const Grid: React.FC = ({
);
const onAddBox = useCallback(
- (boxType: BoxType, boxMetadata: BoxMetadata): void => {
- const boxKey = getBoxKey(boxType, boxMetadata);
- if (currentLayoutItems.find((item) => item.boxKey === boxKey)) {
+ (type: BoxType, metadata: BoxMetadata): void => {
+ const key = createBoxKey(type, metadata);
+ if (currentLayoutItems.find((item) => item.key === key)) {
return;
}
const layoutData = {
- layouts: addBoxToLayouts(boxType, boxMetadata, currentLayouts),
- layoutItems: [
- ...currentLayoutItems,
- { boxType, boxMetadata, boxKey: getBoxKey(boxType, boxMetadata) },
- ],
+ layouts: addBoxToLayouts(currentLayouts, type, metadata),
+ layoutItems: [...currentLayoutItems, { type, metadata, key }],
};
setCurrentLayoutData(layoutData);
@@ -317,11 +318,11 @@ export const Grid: React.FC = ({
[currentLayouts, currentLayoutItems],
);
- const availableBoxList = useMemo(
+ const availableBoxes = useMemo(
() =>
ALL_BOXES.filter(
(box) =>
- !currentLayoutItems.map(({ boxType }) => boxType).includes(box) ||
+ !currentLayoutItems.map(({ type }) => type).includes(box) ||
MULTIPLE_ALLOWED_BOXES.includes(box),
),
[currentLayoutItems],
@@ -333,58 +334,55 @@ export const Grid: React.FC = ({
{isOwnProfile && (
- {changed && canEdit && !isDefaultLayout && (
+ {changed && editing && !isDefaultLayout && (
setExitAlertReset(true)}
- leftIcon={}
- whiteSpace="pre-wrap"
+ leftIcon={mobile ? undefined : }
>
Reset
)}
- {changed && canEdit && (
+ {editing && (
setExitAlertCancel(true)}
- leftIcon={}
+ leftIcon={mobile ? undefined : }
>
Cancel
)}
-
setExitAlertReset(false)}
onYep={handleReset}
- header="Are you sure you want to reset the layout to default?"
+ header="Are you sure you want to reset the layout to its default?"
/>
= ({
onYep={handleCancel}
header="Are you sure you want to cancel editing the layout?"
/>
-
- }
- transition="color 0.2s ease"
- isLoading={saving || fetchingSaveRes}
- onClick={toggleEditLayout}
- >
-
-
+ {(!editing || changed) && (
+ }
+ transition="color 0.2s ease"
+ isLoading={saving || updating}
+ onClick={toggleEditLayout}
+ >
+
+
+ )}
)}
= ({
breakpoints={{ lg: 1180, md: 900, sm: 0 }}
cols={{ lg: 3, md: 2, sm: 1 }}
rowHeight={GRID_ROW_HEIGHT}
- isDraggable={!!canEdit}
+ isDraggable={!!editing}
isResizable={false}
margin={{
lg: [30, 30],
@@ -440,29 +437,28 @@ export const Grid: React.FC = ({
sm: [20, 20],
}}
>
- {currentLayoutItems.map(({ boxKey, boxType, boxMetadata }, i) => (
-
- {boxType === BoxType.PLAYER_ADD_BOX ? (
+ {currentLayoutItems.map(({ key, type, metadata }, i) => (
+
+ {type === BoxTypes.PLAYER_ADD_BOX ? (
{
- itemsRef.current[i] = e as HTMLElement;
+ boxes={availableBoxes}
+ {...{ player, onAddBox }}
+ ref={(e: Maybe) => {
+ itemsRef.current[i] = e;
}}
/>
) : (
{
- itemsRef.current[i] = e as HTMLElement;
+ ref={(e: Maybe) => {
+ itemsRef.current[i] = e;
}}
/>
)}
@@ -476,12 +472,19 @@ export const Grid: React.FC = ({
type QueryParams = { username: string };
export const getStaticPaths: GetStaticPaths = async () => {
- const usernames = await getTopPlayerUsernames();
+ const names = await getTopPlayerUsernames();
return {
- paths: usernames.map((username) => ({
- params: { username },
- })),
+ paths: names
+ .map(({ username, address }) => {
+ const out = [];
+ if (username) {
+ out.push({ params: { username } });
+ }
+ out.push({ params: { username: address } });
+ return out;
+ })
+ .flat(),
fallback: 'blocking',
};
};
@@ -500,11 +503,9 @@ export const getStaticProps = async (
}
const player = await getPlayer(username);
- const personalityInfo = await getPersonalityInfo();
return {
props: {
- personalityInfo,
player: player ?? null, // must be serializable
key: username.toLowerCase(),
hideTopMenu: !player,
diff --git a/packages/web/pages/profile/setup/availability.tsx b/packages/web/pages/profile/setup/availability.tsx
index 16cc3165..781bccf0 100644
--- a/packages/web/pages/profile/setup/availability.tsx
+++ b/packages/web/pages/profile/setup/availability.tsx
@@ -1,10 +1,8 @@
import { SetupAvailability } from 'components/Setup/SetupAvailability';
import { SetupProfile } from 'components/Setup/SetupProfile';
import { SetupContextProvider } from 'contexts/SetupContext';
-import { Maybe } from 'graphql/autogen/types';
-import { useUser } from 'lib/hooks';
import { InferGetStaticPropsType } from 'next';
-import React, { useState } from 'react';
+import React from 'react';
export const getStaticProps = async () => ({
props: {
@@ -14,25 +12,12 @@ export const getStaticProps = async () => ({
export type DefaultSetupProps = InferGetStaticPropsType;
-const AvailabilitySetup: React.FC = () => {
- const { user } = useUser();
- const [available, setAvailability] = useState>(
- user?.profile?.availableHours ?? null,
- );
-
- if (user) {
- if (user.profile?.availableHours != null && available === null) {
- setAvailability(user.profile.availableHours);
- }
- }
-
- return (
-
-
-
-
-
- );
-};
+const AvailabilitySetup: React.FC = () => (
+
+
+
+
+
+);
export default AvailabilitySetup;
diff --git a/packages/web/pages/profile/setup/personalityType.tsx b/packages/web/pages/profile/setup/colorDisposition.tsx
similarity index 67%
rename from packages/web/pages/profile/setup/personalityType.tsx
rename to packages/web/pages/profile/setup/colorDisposition.tsx
index 4658f0d3..7e0ef53f 100644
--- a/packages/web/pages/profile/setup/personalityType.tsx
+++ b/packages/web/pages/profile/setup/colorDisposition.tsx
@@ -1,4 +1,4 @@
-import { SetupPersonalityType } from 'components/Setup/SetupPersonalityType';
+import { SetupColorDisposition } from 'components/Setup/SetupColorDisposition';
import { SetupProfile } from 'components/Setup/SetupProfile';
import { SetupContextProvider } from 'contexts/SetupContext';
import { InferGetStaticPropsType } from 'next';
@@ -12,11 +12,12 @@ export const getStaticProps = async () => ({
export type DefaultSetupProps = InferGetStaticPropsType;
-const PersonalityTypeSetup: React.FC = () => (
+const ColorDispositionSetup: React.FC = () => (
-
+
);
-export default PersonalityTypeSetup;
+
+export default ColorDispositionSetup;
diff --git a/packages/web/pages/profile/setup/pronouns.tsx b/packages/web/pages/profile/setup/pronouns.tsx
index 9d058cbb..de6592fb 100644
--- a/packages/web/pages/profile/setup/pronouns.tsx
+++ b/packages/web/pages/profile/setup/pronouns.tsx
@@ -1,9 +1,8 @@
import { SetupProfile } from 'components/Setup/SetupProfile';
import { SetupPronouns } from 'components/Setup/SetupPronouns';
import { SetupContextProvider } from 'contexts/SetupContext';
-import { useUser } from 'lib/hooks';
import { InferGetStaticPropsType } from 'next';
-import React, { useState } from 'react';
+import React from 'react';
export const getStaticProps = async () => ({
props: {
@@ -13,20 +12,12 @@ export const getStaticProps = async () => ({
export type DefaultSetupProps = InferGetStaticPropsType;
-const PronounsSetup: React.FC = () => {
- const [pronouns, setPronouns] = useState();
- const { user } = useUser();
+const PronounsSetup: React.FC = () => (
+
+
+
+
+
+);
- if (user?.profile?.pronouns && pronouns === undefined) {
- setPronouns(user.profile.pronouns);
- }
-
- return (
-
-
-
-
-
- );
-};
export default PronounsSetup;
diff --git a/packages/web/pages/profile/setup/roles.tsx b/packages/web/pages/profile/setup/roles.tsx
index 6084be92..d01ce55f 100644
--- a/packages/web/pages/profile/setup/roles.tsx
+++ b/packages/web/pages/profile/setup/roles.tsx
@@ -9,7 +9,7 @@ export const getStaticProps = async () => {
return {
props: {
- roleChoices: roleChoices.filter(({ basic }) => basic),
+ choices: roleChoices.filter(({ basic }) => basic),
hideTopMenu: true,
},
};
@@ -17,10 +17,10 @@ export const getStaticProps = async () => {
type Props = InferGetStaticPropsType;
-const PlayerRolesSetup: React.FC = ({ roleChoices }) => (
+const PlayerRolesSetup: React.FC = ({ choices }) => (
-
+
);
diff --git a/packages/web/pages/profile/setup/username.tsx b/packages/web/pages/profile/setup/username.tsx
index 04fc6628..09b743a2 100644
--- a/packages/web/pages/profile/setup/username.tsx
+++ b/packages/web/pages/profile/setup/username.tsx
@@ -1,9 +1,8 @@
import { SetupProfile } from 'components/Setup/SetupProfile';
import { SetupUsername } from 'components/Setup/SetupUsername';
import { SetupContextProvider } from 'contexts/SetupContext';
-import { useUser, useWeb3 } from 'lib/hooks';
import { InferGetStaticPropsType } from 'next';
-import React, { useState } from 'react';
+import React from 'react';
export const getStaticProps = async () => ({
props: {
@@ -13,26 +12,12 @@ export const getStaticProps = async () => ({
export type DefaultSetupProps = InferGetStaticPropsType;
-const UsernameSetup: React.FC = () => {
- const [username, setUsername] = useState();
- const { address } = useWeb3();
- const { user } = useUser();
- const { username: name } = user?.profile ?? {};
+const UsernameSetup: React.FC = () => (
+
+
+
+
+
+);
- if (
- name &&
- name.toLowerCase() !== address?.toLowerCase() &&
- username === undefined
- ) {
- setUsername(name);
- }
-
- return (
-
-
-
-
-
- );
-};
export default UsernameSetup;
diff --git a/packages/web/public/assets/roles/artist.svg b/packages/web/public/assets/roles/artist.svg
index 421d0c36..524203fb 100644
--- a/packages/web/public/assets/roles/artist.svg
+++ b/packages/web/public/assets/roles/artist.svg
@@ -1,4 +1,4 @@
-