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 = () => { @@ -57,7 +67,7 @@ export const MegaMenuFooter = () => { p={0} lineHeight={1} > - {getPlayerName(user)} + {name} {user.rank && ( @@ -103,7 +113,7 @@ export const MegaMenuFooter = () => { my={3.5} px={8} onClick={connect} - isLoading={connecting || fetching} + isLoading={!mounted || connecting || fetching} > Connect Wallet 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} - - - hrweek - - - )} - - - + + + + {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} + + + hrweek + + + )} + + + + + + ); +}; + +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 member­ships 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 && ( + - - )} - - - ); -}; diff --git a/packages/web/components/Setup/ProfileWizardPane.tsx b/packages/web/components/Setup/ProfileWizardPane.tsx index d9954d51..ade6b215 100644 --- a/packages/web/components/Setup/ProfileWizardPane.tsx +++ b/packages/web/components/Setup/ProfileWizardPane.tsx @@ -1,213 +1,40 @@ -import { - Box, - Button, - Flex, - FormControl, - FormErrorMessage, - MetaHeading, - Spinner, - Stack, - StatusedSubmitButton, - Text, - useToast, - Wrap, - WrapItem, -} from '@metafam/ds'; -import { Maybe } from '@metafam/utils'; -import { FlexContainer } from 'components/Container'; -import { useSetupFlow } from 'contexts/SetupContext'; import { useInsertCacheInvalidationMutation } from 'graphql/autogen/types'; import { - CeramicError, ProfileValueType, useProfileField, useSaveCeramicProfile, - useUser, } from 'lib/hooks'; -import { - PropsWithChildren, - ReactElement, - useCallback, - useEffect, - useState, -} from 'react'; -import { Control, useForm, UseFormRegisterReturn } from 'react-hook-form'; +import { PropsWithChildren, useCallback } from 'react'; -export type MaybeModalProps = { - onClose?: () => void; -}; - -export type WizardPaneProps = { - field: string; - title?: string | ReactElement; - prompt?: string | ReactElement; - onClose?: () => void; -}; - -export type WizardPaneCallbackProps = { - register: ( - field: string, - opts: Record, - ) => UseFormRegisterReturn; - control: Control; - loading: boolean; - errored: boolean; - dirty: boolean; - current: T; - setter: (arg: T | ((prev: T) => T)) => void; -}; +import { WizardPane, WizardPaneProps } from './WizardPane'; export const ProfileWizardPane = ({ field, - title, - prompt, - onClose, children, + ...props }: PropsWithChildren) => { - const { onNextPress, nextButtonLabel } = useSetupFlow(); - const [status, setStatus] = useState>(); - const { user } = useUser(); - const { value: existing } = useProfileField({ + const { value, user } = useProfileField({ field, - player: user, - owner: true, }); - const { - register, - control, - handleSubmit, - setValue, - watch, - formState: { errors, isValidating: validating }, - } = useForm(); - const current = watch(field, existing); const saveToCeramic = useSaveCeramicProfile({ - setStatus, fields: [field], }); const [, invalidateCache] = useInsertCacheInvalidationMutation(); - const toast = useToast(); - useEffect(() => { - setValue(field, existing); - }, [existing, field, setValue]); + const onSave = useCallback( + async ({ values, setStatus }) => { + setStatus('Saving to Ceramic…'); + await saveToCeramic({ values }); - const onSubmit = useCallback( - async (values) => { - try { - if (current === existing) { - setStatus('No Change. Skipping Save…'); - await new Promise((resolve) => { - setTimeout(resolve, 10); - }); - } else { - setStatus('Saving to Ceramic…'); - await saveToCeramic({ values }); - - if (user) { - setStatus('Invalidating Cache…'); - await invalidateCache({ playerId: user.id }); - } - } - - onNextPress(); - } 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); + if (user) { + setStatus('Invalidating Cache…'); + await invalidateCache({ playerId: user.id }); } }, - [ - current, - existing, - invalidateCache, - onNextPress, - saveToCeramic, - toast, - user, - ], - ); - - const setter = useCallback( - (val: T | ((val: T) => T)) => { - let next = val; - if (val instanceof Function) { - next = val(current); - } - setValue(field, next); - }, - [current, field, setValue], + [invalidateCache, saveToCeramic, user], ); return ( - - {title && ( - - {title} - - )} - {typeof prompt === 'string' ? ( - - {prompt} - - ) : ( - prompt - )} - - {!user && ( - - - Loading Current Value… - - )} - {validating && ( - - - Validating… - - )} - {typeof children === 'function' - ? children.call(null, { - register, - control, - loading: !user, - errored: !!errors[field], - dirty: current !== existing, - current, - setter, - }) - : children} - - - {errors[field]?.message} - - - - - - - - - {onClose && ( - - - - )} - - + {children} ); }; diff --git a/packages/web/components/Setup/SetupAvailability.tsx b/packages/web/components/Setup/SetupAvailability.tsx index d88d4277..905f63fb 100644 --- a/packages/web/components/Setup/SetupAvailability.tsx +++ b/packages/web/components/Setup/SetupAvailability.tsx @@ -3,102 +3,74 @@ import { InputGroup, InputLeftElement, InputRightAddon, - MetaButton, - MetaHeading, Text, - 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 React from 'react'; -export type SetupAvailabilityProps = { - available: number | null; - setAvailability: React.Dispatch>; -}; +import { ProfileWizardPane } from './ProfileWizardPane'; +import { WizardPaneCallbackProps } from './WizardPane'; -export const SetupAvailability: React.FC = ({ - available, - setAvailability, -}) => { - const { onNextPress, nextButtonLabel } = useSetupFlow(); - - const [invalid, setInvalid] = useState(false); - const { user } = useUser(); - const toast = useToast(); - - useEffect(() => { - const value = Number(available); - setInvalid(value < 0 || value > 24 * 7); - }, [available]); - - const [updateProfileRes, updateProfile] = useUpdateProfileMutation(); - const [loading, setLoading] = useState(false); - - const handleNextPress = async () => { - if (!user) return; - setLoading(true); - - const { error } = await updateProfile({ - playerId: user.id, - input: { - availableHours: Number(available), - }, - }); - - if (error) { - toast({ - title: 'Error', - description: `Unable to update availability: "${error}"`, - status: 'error', - isClosable: true, - }); - setLoading(false); - return; - } - - onNextPress(); - }; +export const SetupAvailability: React.FC = () => { + const field = 'availableHours'; return ( - - - Avail­ability - - - What is your weekly availability for any kind of freelance work? - - - - - 🕛 - - - { - setAvailability(parseFloat(value)); - }} - isInvalid={invalid} - /> - hr ⁄ week - + + {({ register, errored = false }: WizardPaneCallbackProps) => { + const { ref: registerRef, ...props } = register(field, { + valueAsNumber: true, + min: { + value: 0, + message: 'It’s not possible to be available for negative time.', + }, + max: { + value: 24 * 7, + message: `There’s only ${24 * 7} hours in a week.`, + }, + }); - - {nextButtonLabel} - - + return ( + + + + 🕛 + + + { + ref?.focus(); + registerRef(ref); + }} + {...props} + /> + + hrweek + + + ); + }} + ); }; diff --git a/packages/web/components/Setup/SetupColorDisposition.tsx b/packages/web/components/Setup/SetupColorDisposition.tsx new file mode 100644 index 00000000..729cfbab --- /dev/null +++ b/packages/web/components/Setup/SetupColorDisposition.tsx @@ -0,0 +1,217 @@ +import { + Button, + Flex, + Image, + Input, + Stack, + Text, + Wrap, + WrapItem, +} from '@metafam/ds'; +import { Maybe, Optional } from '@metafam/utils'; +import { MetaLink } from 'components/Link'; +import { ColorBar } from 'components/Player/ColorBar'; +import { + getPersonalityInfo, + images as MaskImages, + PersonalityInfo, +} from 'graphql/queries/enums/getPersonalityInfo'; +import { PersonalityOption } from 'graphql/types'; +import React, { useEffect, useState } from 'react'; + +import { ProfileWizardPane } from './ProfileWizardPane'; +import { MaybeModalProps, WizardPaneCallbackProps } from './WizardPane'; + +export type ColorButtonsProps = { + mask: number; + setMask: ( + bit: number | ((prev: Optional>) => Maybe), + ) => void; + types: NonNullable; + disabled: boolean; +}; + +// newMask should always only have at most a single bit +// set — the one being toggled +const toggleBit = ({ + base = 0, + bit = 0, +}: { + base?: number; + bit?: number; +}): number => { + if ((base & bit) > 0) { + // if the bit in mask is set + return base & ~bit; // unset it + } + return base | bit; // otherwise set it +}; + +export const ColorButtons: React.FC = ({ + mask, + setMask, + types, + disabled = false, +}) => ( + + {Object.entries(MaskImages) + .reverse() + .map(([bitString, image], idx) => { + const type = types[bitString]; + + if (!type) { + return ( + + Could not find a type for 0b + {Number(bitString).toString(2).padStart(5, '0')}. + + ); + } + + const { name, mask: bit = 0, description } = type; + const selected = (mask & bit) > 0; + + return ( + + + + ); + })} + +); + +export const SetupColorDisposition: React.FC = ({ + buttonLabel, + onClose, +}) => { + const field = 'colorMask'; + + const [types, setTypes] = useState>>( + null, + ); + + useEffect(() => { + const load = async () => { + setTypes(await getPersonalityInfo()); + }; + load(); + }, []); + + return ( + + Please select your personality components below. Not sure what type + you are? + Take + + a quick exam + + or + + a longer quiz + + . +
+ } + > + {({ + register, + loading, + current = 0, + setter, + }: WizardPaneCallbackProps) => { + if (types == null) { + return ( + + Loading Personality Information… + + ); + } + + return ( + + + + {!loading && ( + + )} + + ); + }} + + ); +}; diff --git a/packages/web/components/Setup/SetupDone.tsx b/packages/web/components/Setup/SetupDone.tsx index da9650ae..62e582ad 100644 --- a/packages/web/components/Setup/SetupDone.tsx +++ b/packages/web/components/Setup/SetupDone.tsx @@ -2,13 +2,10 @@ import { MetaButton, MetaHeading, Stack } from '@metafam/ds'; import { FlexContainer } from 'components/Container'; import { PlayerTile } from 'components/Player/PlayerTile'; import { useUser } from 'lib/hooks'; -import { useRouter } from 'next/router'; -import React, { useState } from 'react'; +import React from 'react'; export const SetupDone: React.FC = () => { - const router = useRouter(); const { user } = useUser(); - const [loading, setLoading] = useState(false); return ( Game On! @@ -20,18 +17,31 @@ export const SetupDone: React.FC = () => { align="center" > {user && } - { - setLoading(true); - router.push('/'); - }} - px={20} - py={8} - fontSize="xl" - isLoading={loading} - > - Play - + + + Play + + + Explore + + ); diff --git a/packages/web/components/Setup/SetupHeader.tsx b/packages/web/components/Setup/SetupHeader.tsx index 01edaba0..912969d1 100644 --- a/packages/web/components/Setup/SetupHeader.tsx +++ b/packages/web/components/Setup/SetupHeader.tsx @@ -1,44 +1,56 @@ -import { Box, BoxedNextImage, Flex, Grid, ResponsiveText } from '@metafam/ds'; -import BackImage from 'assets/Back.svg'; +import { + Box, + BoxedNextImage, + Flex, + Grid, + ResponsiveText, + Text, +} from '@metafam/ds'; import LogoImage from 'assets/logo.png'; -import SkipImage from 'assets/Skip.svg'; import { FlexContainer } from 'components/Container'; import { useSetupFlow } from 'contexts/SetupContext'; -import React from 'react'; +import React, { ReactElement } from 'react'; export const SetupHeader: React.FC = () => { - const { stepIndex, onNextPress, onBackPress, options } = useSetupFlow(); + const { + stepIndex, + onNextPress, + onBackPress, + options: { steps, sections }, + } = useSetupFlow(); - const { sectionIndex } = options.steps[stepIndex]; + const { sectionIndex } = steps[stepIndex]; - const templateColumns = [ - '0.5', - ...options.sections.map(() => '1'), - '0.5', - ].map((col) => `${col}fr`); + const templateColumns = [0, ...sections.map(() => 1), 0].map( + (col) => `${col}fr`, + ); return ( - + - + + < + - {options.sections.map((option, id) => ( + {sections.map(({ label, title }, id) => ( id} /> ))} - + + > + ); }; interface StepProps { - title: { [any: string]: string | undefined }; + title: { [any: string]: string | undefined | ReactElement }; isDone: boolean; isActive: boolean; } @@ -49,8 +61,8 @@ export const SectionProgress: React.FC = ({ isActive, }) => { const { options, stepIndex } = useSetupFlow(); - const progress = isDone ? 100 : options.progressWithinSection(stepIndex); + return ( = ({ fontWeight="bold" color="offwhite" opacity={isActive ? 1 : 0.4} - mb={4} + mb={2} content={title} + ml={[0, '3.5em']} + pr={2} + sx={{ textIndent: [0, '-1.5em'] }} + h={4} /> - + {(isActive || isDone) && ( )} @@ -78,12 +88,12 @@ export const SectionProgress: React.FC = ({ )} diff --git a/packages/web/components/Setup/SetupMemberships.tsx b/packages/web/components/Setup/SetupMemberships.tsx index 69f74ea7..5d206420 100644 --- a/packages/web/components/Setup/SetupMemberships.tsx +++ b/packages/web/components/Setup/SetupMemberships.tsx @@ -1,6 +1,5 @@ import { Box, - Center, ChainIcon, Flex, Heading, @@ -8,61 +7,79 @@ import { Image, MetaButton, MetaHeading, - SimpleGrid, + Spinner, Text, + Wrap, + WrapItem, } from '@metafam/ds'; +import { Maybe, Optional } from '@metafam/utils'; import { FlexContainer } from 'components/Container'; import { useSetupFlow } from 'contexts/SetupContext'; import { Membership } from 'graphql/types'; import React, { useState } from 'react'; -import { getDaoLink } from 'utils/daoHelpers'; +import { getDAOLink } from 'utils/daoHelpers'; -import { useWeb3 } from '../../lib/hooks'; +import { useMounted, useWeb3 } from '../../lib/hooks'; import { DaoHausLink } from '../Player/PlayerGuild'; export type SetupMembershipsProps = { memberships: Array | null | undefined; setMemberships: React.Dispatch< - React.SetStateAction | null | undefined> + React.SetStateAction>>> >; }; export const SetupMemberships: React.FC = ({ memberships, }) => { - const { connected } = useWeb3(); + const { connecting, connected } = useWeb3(); const { onNextPress, nextButtonLabel } = useSetupFlow(); const [loading, setLoading] = useState(false); + const mounted = useMounted(); return ( - Memberships + Member­ships - {!memberships && ( - - {connected ? 'Loading…' : 'Account Not Connected'} - - )} - {memberships && - (memberships.length > 0 ? ( - - - We found the following guilds associated with your account and - automatically added them to your profile. You can edit them later - in your profile. + {(() => { + if (!memberships) { + return ( + + + + {!mounted || connecting || connected + ? 'Loading…' + : 'Account Not Connected'} + + + ); + } + + if (memberships.length === 0) { + return ( + + We did not find any guilds associated with your account. - - {memberships.map((member) => ( - + ); + } + + return ( + + + We found the following guilds associated with your account and + automatically added them to your profile. + + + {memberships?.map((member) => ( + + + ))} - + - ) : ( - - We did not find any guilds associated with your account. - - ))} + ); + })()} { setLoading(true); @@ -81,49 +98,52 @@ type MembershipListingProps = { member: Membership; }; -const MembershipListing: React.FC = ({ member }) => { - const daoUrl = getDaoLink(member.moloch.chain, member.moloch.id); - - const { avatarUrl, chain, title } = member.moloch; +const MembershipListing: React.FC = ({ + member: { moloch }, +}) => { + const { id: molochId, avatarURL, chain, title } = moloch; + const daoURL = getDAOLink(chain, molochId); return ( - - - - {avatarUrl ? ( + + + + {avatarURL ? ( ) : ( - + )} - - -
- {title ?? ( - - Unknown{' '} - - {chain} - {' '} - DAO - - )} - -
-
-
+ + {title ?? ( + + Unknown{' '} + + {chain} + {' '} + DAO + + )} + +
); diff --git a/packages/web/components/Setup/SetupPersonalityType.tsx b/packages/web/components/Setup/SetupPersonalityType.tsx deleted file mode 100644 index a547c54c..00000000 --- a/packages/web/components/Setup/SetupPersonalityType.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import { ModelManager } from '@glazed/devtools'; -import { DIDDataStore } from '@glazed/did-datastore'; -import { TileLoader } from '@glazed/tile-loader'; -import { - Button, - Flex, - Image, - LoadingState, - MetaButton, - MetaHeading, - ModalBody, - ModalFooter, - Spinner, - Text, - useToast, - Wrap, - WrapItem, -} from '@metafam/ds'; -import { extendedProfileModel } from '@metafam/utils'; -import { FlexContainer } from 'components/Container'; -import { MetaLink } from 'components/Link'; -import { ColorBar } from 'components/Player/ColorBar'; -import { useSetupFlow } from 'contexts/SetupContext'; -import { Maybe } from 'graphql/autogen/types'; -import { - getPersonalityInfo, - images as BaseImages, - PersonalityInfo, -} from 'graphql/queries/enums/getPersonalityInfo'; -import { useUser, useWeb3 } from 'lib/hooks'; -import React, { ReactElement, useEffect, useMemo, useState } from 'react'; -import { dispositionFor } from 'utils/playerHelpers'; - -export type SetupPersonalityTypeProps = { - isEdit?: boolean; - onClose?: () => void; -}; - -export const SetupPersonalityType: React.FC = ({ - isEdit, - onClose, -}) => { - const { onNextPress, nextButtonLabel } = useSetupFlow(); - const { fetching: fetchingUser, user } = useUser(); - const { ceramic } = useWeb3(); - const toast = useToast(); - const [status, setStatus] = useState>(null); - const [colorMask, setColorMask] = useState | undefined>(); - const [types, setPersonalityInfo] = useState({}); - const isWizard = !isEdit; - - useEffect(() => { - if (user?.profile?.colorMask != null) { - setColorMask(user.profile.colorMask); - } - }, [colorMask, user]); - - const [fetchingInfo, setFetchingInfo] = useState(true); - - const fetching = useMemo(() => fetchingUser || fetchingInfo || !user, [ - fetchingUser, - fetchingInfo, - user, - ]); - - useEffect(() => { - const fetchInfo = async () => - setPersonalityInfo(await getPersonalityInfo()); - - fetchInfo().then(() => setFetchingInfo(false)); - }, []); - - const handleNextPress = async () => { - setStatus('Saving…'); - - save(); - - onNextPress(); - }; - - const save = async () => { - if (!user) return; - - if (!ceramic) { - toast({ - title: 'Ceramic Error', - description: 'Ceramic is not defined. Cannot update.', - status: 'error', - isClosable: true, - }); - return; - } - - if (user.profile?.colorMask !== colorMask) { - try { - if (!ceramic.did?.authenticated) { - setStatus(Authenticating DID…); - await ceramic.did?.authenticate(); - } - - setStatus(Saving Color Disposition…); - - const cache = new Map(); - const loader = new TileLoader({ ceramic, cache }); - const manager = new ModelManager(ceramic); - manager.addJSONModel(extendedProfileModel); - - const store = new DIDDataStore({ - ceramic, - loader, - model: await manager.toPublished(), - }); - - const colorDisposition = dispositionFor(colorMask); - await store.merge('extendedProfile', { colorDisposition }); - } catch (err) { - console.warn(err); // eslint-disable-line no-console - toast({ - title: 'Error', - description: `Unable to update personality type. Error: ${ - (err as Error).message - }`, - status: 'error', - isClosable: true, - }); - setStatus(null); - } - } - }; - - // mask should always only have at most a single bit set - const toggleMaskElement = (mask = 0): void => { - setColorMask((current = 0) => { - // eslint-disable-next-line no-param-reassign - current ??= 0; // in case of null - if ((mask & current) > 0) { - // if the bit in mask is set - return current & ~mask; // unset it - } - return current | mask; // otherwise set it - }); - }; - - const setup = ( - - {isWizard && ( - Person­ality Type - )} - - Please select your personality components below. Not sure what type you - are? - Take - - a quick exam - - or - - a longer quiz - - . - - {fetching ? ( - - ) : ( - <> - - {Object.keys(types ?? {}).length && - Object.entries(BaseImages) - .reverse() - .map(([orig, image], idx) => { - const option = types?.[parseInt(orig, 10)]; - const { mask = 0 } = option ?? {}; - const selected = ((colorMask ?? 0) & mask) > 0; - - return ( - - - - ); - })} - - - - {isWizard && ( - - {nextButtonLabel} - - )} - - )} - - ); - - return isWizard ? ( - setup - ) : ( - <> - {setup}\ - {isEdit && onClose && ( - - - - - { - await save(); - onClose(); - }} - > - {!status ? ( - 'Save Changes' - ) : ( - - - {typeof status === 'string' ? ( - {status} - ) : ( - status - )} - - )} - - - - - - - - - )} - - ); -}; diff --git a/packages/web/components/Setup/SetupPlayerType.tsx b/packages/web/components/Setup/SetupPlayerType.tsx index 97120919..5a5861e2 100644 --- a/packages/web/components/Setup/SetupPlayerType.tsx +++ b/packages/web/components/Setup/SetupPlayerType.tsx @@ -1,222 +1,108 @@ -import { ModelManager } from '@glazed/devtools'; -import { DIDDataStore } from '@glazed/did-datastore'; -import { TileLoader } from '@glazed/tile-loader'; import { Button, - Flex, - MetaButton, - MetaHeading, - ModalBody, - ModalFooter, + Center, + Input, + InputGroup, SimpleGrid, - Spinner, + Stack, Text, - useToast, - Wrap, - WrapItem, } from '@metafam/ds'; -import { extendedProfileModel, Maybe } from '@metafam/utils'; -import { FlexContainer } from 'components/Container'; -import { useSetupFlow } from 'contexts/SetupContext'; +import { Maybe, Optional } from '@metafam/utils'; import { ExplorerType } from 'graphql/autogen/types'; import { getExplorerTypes } from 'graphql/queries/enums/getExplorerTypes'; -import { useUser, useWeb3 } from 'lib/hooks'; -import React, { ReactElement, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; -export type Props = { - isEdit?: boolean; - onClose?: () => void; +import { ProfileWizardPane } from './ProfileWizardPane'; +import { MaybeModalProps, WizardPaneCallbackProps } from './WizardPane'; + +export type ExplorerTypesType = { + selectedType: Maybe; + setSelectedType: ( + arg: string | ((type: Optional>) => Maybe), + ) => void; + disabled?: boolean; }; -export const SetupPlayerType: React.FC = ({ isEdit, onClose }) => { - const { onNextPress, nextButtonLabel } = useSetupFlow(); - const { user } = useUser(); - const { ceramic } = useWeb3(); - const toast = useToast(); - const [status, setStatus] = useState>(null); - const [explorerType, setExplorerType] = useState(); - const [typeChoices, setTypeChoices] = useState([]); - const isWizard = !isEdit; - - const load = () => { - if (user) { - if (explorerType === undefined && user.profile?.explorerType != null) { - setExplorerType(user.profile.explorerType); - } - } - }; - useEffect(load, [explorerType, user, user?.profile?.explorerType]); +export const ExplorerTypes: React.FC = ({ + selectedType, + setSelectedType, + disabled = false, +}) => { + const [choices, setChoices] = useState>([]); useEffect(() => { const fetchTypes = async () => { - const response = await getExplorerTypes(); - setTypeChoices(response); + setChoices(await getExplorerTypes()); }; fetchTypes(); - }, [setTypeChoices]); + }, [setChoices]); - const handleNextPress = async () => { - setStatus('Saving Type Selection…'); - await save(); - onNextPress(); - }; + return ( + + + {choices.map((choice) => { + const selected = selectedType === choice.title; - const save = async () => { - if (!user) return; - - if (!ceramic) { - toast({ - title: 'Ceramic Error', - description: 'Ceramic is not defined. Cannot update.', - status: 'error', - isClosable: true, - }); - return; - } - - if (user?.profile?.explorerType?.id !== explorerType?.id) { - try { - if (!ceramic.did?.authenticated) { - setStatus('Authenticating DID…'); - await ceramic.did?.authenticate(); - } - - setStatus('Loading Profile Configuration…'); - - const cache = new Map(); - const loader = new TileLoader({ ceramic, cache }); - const manager = new ModelManager(ceramic); - manager.addJSONModel(extendedProfileModel); - - const store = new DIDDataStore({ - ceramic, - loader, - model: await manager.toPublished(), - }); - - setStatus('Saving to Ceramic…'); - await store.merge('extendedProfile', { - explorerType: explorerType?.title, - }); - } catch (err) { - console.warn(err); // eslint-disable-line no-console - toast({ - title: 'Error', - description: `Unable to update player type. Error: ${ - (err as Error).message - }`, - status: 'error', - isClosable: true, - }); - setStatus(null); - } - } - }; - - const setup = ( - - {isWizard && ( - - Player Type - - )} - - Please read the features of each player type below, and select the one - that suits you best. - - - {typeChoices.map((choice) => ( - setExplorerType(choice)} - align="stretch" - justify="flex-start" - border="2px" - borderColor={ - explorerType?.id === choice.id ? 'purple.400' : 'transparent' - } - > - - {choice.title} - - - {choice.description} - - - ))} + return ( + + ); + })} - {isWizard && ( - - {nextButtonLabel} - - )} - - ); - return isWizard ? ( - setup - ) : ( - <> - {setup} - - {isEdit && onClose && ( - - - - - { - await save(); - onClose(); - }} - > - {!status ? ( - 'Save Changes' - ) : ( - - - {typeof status === 'string' ? ( - {status} - ) : ( - status - )} - - )} - - - - - - - - - )} - + + ); +}; +export const SetupPlayerType: React.FC = ({ + onClose, + buttonLabel, +}) => { + const field = 'explorerTypeTitle'; + + return ( + + {({ register, loading, current, setter }: WizardPaneCallbackProps) => ( +
+ + +
+ )} +
); }; diff --git a/packages/web/components/Setup/SetupProfile.tsx b/packages/web/components/Setup/SetupProfile.tsx index e89c55f6..52392365 100644 --- a/packages/web/components/Setup/SetupProfile.tsx +++ b/packages/web/components/Setup/SetupProfile.tsx @@ -9,7 +9,7 @@ export const SetupProfile: React.FC = ({ children }) => { return ( {options.numSteps - 1 > stepIndex && } - + {children} diff --git a/packages/web/components/Setup/SetupPronouns.tsx b/packages/web/components/Setup/SetupPronouns.tsx index 275a7a82..f084ae64 100644 --- a/packages/web/components/Setup/SetupPronouns.tsx +++ b/packages/web/components/Setup/SetupPronouns.tsx @@ -1,73 +1,42 @@ -import { Input, MetaButton, MetaHeading, useToast } from '@metafam/ds'; -import { FlexContainer } from 'components/Container'; -import { useSetupFlow } from 'contexts/SetupContext'; -import { useUpdateProfilePronounsMutation } from 'graphql/autogen/types'; -import { useUser } from 'lib/hooks'; -import React, { useState } from 'react'; +import { Flex, Input } from '@metafam/ds'; +import React from 'react'; -export type SetupPronounsProps = { - pronouns: string | undefined; - setPronouns: React.Dispatch>; -}; +import { ProfileWizardPane } from './ProfileWizardPane'; +import { WizardPaneCallbackProps } from './WizardPane'; -export const SetupPronouns: React.FC = ({ - pronouns, - setPronouns, -}) => { - const { onNextPress, nextButtonLabel } = useSetupFlow(); - const { user } = useUser(); - const toast = useToast(); - - const [{ fetching }, updatePronouns] = useUpdateProfilePronounsMutation(); - const [loading, setLoading] = useState(false); - - const handleNextPress = async () => { - if (!user) return; - - setLoading(true); - const { error } = await updatePronouns({ - playerId: user.id, - input: { - pronouns: pronouns ?? '', - }, - }); - - if (error) { - toast({ - title: 'Error', - description: `Unable to update player pronouns. ${error.message}`, - status: 'error', - isClosable: true, - }); - setLoading(false); - return; - } - - onNextPress(); - }; +export const SetupPronouns: React.FC = () => { + const field = 'pronouns'; return ( - - - Which pronouns do you prefer? - - setPronouns(value)} - w="auto" - /> + + {({ register, errored }: WizardPaneCallbackProps) => { + const { ref: registerRef, ...props } = register(field, { + maxLength: { + value: 150, + message: 'Maximum length is 150 characters.', + }, + }); - - {nextButtonLabel} - - + return ( + + { + ref?.focus(); + registerRef(ref); + }} + {...props} + /> + + ); + }} + ); }; diff --git a/packages/web/components/Setup/SetupRoles.tsx b/packages/web/components/Setup/SetupRoles.tsx index d9fefa8b..60d9cb1f 100644 --- a/packages/web/components/Setup/SetupRoles.tsx +++ b/packages/web/components/Setup/SetupRoles.tsx @@ -4,359 +4,331 @@ import { Button, CloseIcon, Flex, + Heading, InfoIcon, - LoadingState, - MetaButton, - MetaHeading, - ModalBody, - ModalFooter, + Input, + SimpleGrid, Spacer, + Stack, Text, useBreakpointValue, - useToast, } from '@metafam/ds'; -import { FlexContainer } from 'components/Container'; -import { useSetupFlow } from 'contexts/SetupContext'; +import { Maybe, Optional } from '@metafam/utils'; import { PlayerRole, - useUpdatePlayerRolesMutation, + useUpdatePlayerRolesMutation as useUpdateRoles, } from 'graphql/autogen/types'; import { getPlayerRoles } from 'graphql/queries/enums/getRoles'; -import { useUser } from 'lib/hooks'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useOverridableField, useUser } from 'lib/hooks'; +import React, { ReactElement, useEffect, useState } from 'react'; +import { isEmpty } from 'utils/objectHelpers'; + +import { WizardPane, WizardPaneCallbackProps } from './WizardPane'; export type RoleValue = string; export type SetupRolesProps = { - roleChoices?: Array; + choices?: Maybe>; isEdit?: boolean; onClose?: () => void; + buttonLabel?: Optional; }; export const SetupRoles: React.FC = ({ - roleChoices: inputRoleChoices = [], - isEdit, + choices: inputChoices = null, onClose, + buttonLabel, }) => { - const isWizard = !isEdit; - const { onNextPress, nextButtonLabel } = useSetupFlow(); - const toast = useToast(); - const { fetching: fetchingUser, user } = useUser({ - requestPolicy: 'network-only', - }); - const [fetchingRoleChoices, setFetchingRoleChoices] = useState(true); - const [roleChoices, setRoleChoices] = useState( - inputRoleChoices, + const field = 'roles'; + const { user } = useUser(); + const [choices, setChoices] = useState>>( + inputChoices, ); + const [, updateRoles] = useUpdateRoles(); + const { value: roles, setter: setRoles } = useOverridableField>( + { + field, + loaded: !!user, + }, + ); + const mobile = useBreakpointValue({ base: true, sm: false }) ?? false; + useEffect(() => { - if (inputRoleChoices.length === 0 && roleChoices.length === 0) { - getPlayerRoles().then((s) => { - setRoleChoices(s.filter(({ basic }) => basic)); - setFetchingRoleChoices(false); - }); - } else { - setFetchingRoleChoices(false); + const fetchRoles = async () => { + const roleChoices = await getPlayerRoles(); + setChoices(roleChoices.filter(({ basic }) => basic)); + }; + + if (!choices) { + fetchRoles(); } - }, [inputRoleChoices, roleChoices]); - const fetching = useMemo(() => fetchingUser || fetchingRoleChoices, [ - fetchingUser, - fetchingRoleChoices, - ]); - const [roles, setRoles] = useState([]); + }, [choices]); + useEffect(() => { - setRoles(user?.roles.map((r) => r.role) ?? []); - }, [user]); + if (user && setRoles && !roles) { + setRoles(user.roles.map(({ role }) => role)); + } + }, [user, setRoles, roles]); - const availableRoles = useMemo( - () => - roleChoices.filter(({ role, basic }) => !roles.includes(role) && basic), - [roles, roleChoices], - ); + const onSave = async ({ + values, + setStatus, + }: { + values: Record; + setStatus?: (msg: string) => void; + }) => { + const { roles: toSet } = values as { ['roles']: Array }; - const [updateRolesResult, updateRoles] = useUpdatePlayerRolesMutation(); - const [loading, setLoading] = useState(false); - - const save = useCallback(async () => { - if (!user) return; - - setLoading(true); + setStatus?.('Writing to Hasura…'); const { error } = await updateRoles({ - roles: roles.map((r, i) => ({ - rank: i, - role: r, - })), + [field]: toSet.map((role, rank) => ({ rank, role })), }); if (error) { - toast({ - title: 'Error', - description: 'Unable to update roles. The octo is sad 😢', - status: 'error', - isClosable: true, - }); - setLoading(false); + throw new Error(`Unable to update roles. Error: ${error}`); } - }, [user, roles, toast, updateRoles]); - const handleNextPress = useCallback(async () => { - setLoading(true); - await save(); - onNextPress(); - }, [save, onNextPress]); - - const handleSelection = (role: PlayerRole, isPrimary?: boolean) => { - if (isPrimary === false && roles.length < 1) { - return; + if (setRoles) { + setStatus?.('Setting Local State…'); + setRoles(toSet); } - let newRoles: RoleValue[] = []; - const otherRoles = roles.filter((r) => r !== role.role); - if (isPrimary === true) { - newRoles = [role.role, ...otherRoles]; - } else { - newRoles = [...otherRoles, role.role]; - } - setRoles(newRoles); }; - const handleRemoval = (role: PlayerRole) => { - const newRoles = roles.filter((r) => r !== role.role); - setRoles(newRoles); - }; - - const roleContainerStyles = { - width: { - base: 'calc(100% - 4px)', - md: 'calc(50% - 16px)', - lg: 'calc(33% - 20px)', - }, - mr: { base: 0, md: 4 }, - mb: { base: 2, md: 4 }, - }; - - const isMobile = useBreakpointValue({ base: true, md: false }); - - const setup = ( - > + {...{ field, onClose, onSave, buttonLabel }} + value={roles} + title="Roles" + prompt={ + + Unlike other role-playing games, in MetaGame a player is free to take + multiple roles at the same time. + + } + fetching={!user} > - {isWizard && ( - - Select your role(s) - - )} - {fetching && } - {!fetching && - (roles.length === 0 ? ( - - Unlike other role-playing games, in MetaGame, anyone is free to play - multiple roles at the same time. -
- Players are required to specify their primary role, whereas any - secondary roles are optional. -
- ) : ( - - {roles.map((r, i) => { - const choice = roleChoices.find( - (roleChoice) => roleChoice.role === r, - ); - return ( - choice && ( - <> - - - {i === 0 && 'Primary Role'} - {i > 0 && roles.length === 2 && 'Secondary Role'} - {i === 1 && roles.length > 2 && 'Secondary Roles'} - {/* we still need a placeholder */} - {!isMobile && roles.length > 2 && i > 1 && ( -   - )} - + {({ + register, + current, + setter, + }: WizardPaneCallbackProps>) => { + if (!choices) { + return Loading Role Choices…; + } - - - {/* wrap after the primary */} - {i === 0 && } - - ) - ); - })} - - ))} - {availableRoles.length > 0 && !fetching && ( - <> - - Available Roles - - - {availableRoles.map((r) => ( - - - - ))} - - - )} + if (!current) return null; - {isWizard && !fetching && ( - - - {nextButtonLabel} - - - )} -
- ); - return isWizard ? ( - setup - ) : ( - <> - {setup} - {isEdit && onClose && ( - - - { - await save(); - onClose(); - }} - > - Save Changes - - - - - )} - + const availableRoles = + choices + ?.filter(({ role, basic }) => !current?.includes(role) && basic) + .map(({ role }) => role) ?? []; + + const select = ({ role }: PlayerRole, isPrimary?: boolean) => { + if (current) { + let out = null; + const otherRoles = current.filter((r) => r !== role); + if (isPrimary || isEmpty(otherRoles)) { + out = [role, ...otherRoles]; + } else { + out = [...otherRoles, role]; + } + setter(out); + } + }; + + const remove = ({ role }: PlayerRole) => { + if (current) { + const out = current.filter((r) => r !== role); + setter(out); + } + }; + + return ( + + + + + + + ); + }} + ); }; +export type RoleGroupProps = { + roles: Array; + choices: Array; + title: string; + active?: boolean; + primary?: boolean; + numSelectedRoles?: number; + select?: (role: PlayerRole, primary?: boolean) => void; + remove?: (role: PlayerRole, primary?: boolean) => void; + mobile: boolean; +}; + +const RoleGroup: React.FC = ({ + roles, + choices, + title, + active, + primary, + numSelectedRoles, + select, + remove, + mobile, +}) => + roles.length === 0 ? null : ( + + {title && ( + + {title} + {roles.length > 1 ? 's' : null} + + )} + + {roles.map((r) => { + const choice = choices?.find(({ role }) => role === r); + + return ( + + {!choice ? ( + Couldn't find role “{r}”. + ) : ( + + )} + + ); + })} + + + ); + type RoleProps = { role: PlayerRole; - selectionIndex?: number; + selected?: boolean; + primary?: boolean; numSelectedRoles?: number; - onSelect: (role: PlayerRole, isPrimary?: boolean) => void; + onSelect?: (role: PlayerRole, isPrimary?: boolean) => void; onRemove?: (role: PlayerRole) => void; + mobile: boolean; }; const Role: React.FC = ({ role, - selectionIndex, + selected = false, + primary = false, numSelectedRoles, onSelect, onRemove, + mobile = false, }) => { - const handleContainerClick = () => { - if (selectionIndex == null) { + const onClick = () => { + if (!selected && onSelect) { onSelect(role); } }; - const handleRemoveClick = () => { - if (onRemove) { - onRemove(role); - } - }; - const [showDetails, setShowDetails] = useState(false); - const isMobile = useBreakpointValue({ base: true, md: false }); - return ( - + { + if (selected) { + evt.stopPropagation(); + setShowDetails((show) => !show); + } + }} > {role.label} - {!isMobile && {role.description}} - - {isMobile && ( + {!mobile && ( + + {role.description} + + )} + + {mobile && (numSelectedRoles == null || numSelectedRoles <= 1) && ( { e.stopPropagation(); - setShowDetails(!showDetails); + setShowDetails((show) => !show); }} /> )} - {selectionIndex != null && ( - + {selected && ( + {numSelectedRoles != null && numSelectedRoles > 1 && - (isMobile ? ( + (mobile ? ( ) : ( ))} - {isMobile ? ( + {mobile ? ( @@ -411,9 +384,8 @@ const Role: React.FC = ({ borderColor="red.500" borderWidth={2} _hover={{ color: 'white', bgColor: 'red.500' }} - fontSize={{ md: '0.875rem', lg: '1rem' }} - whiteSpace="pre-wrap" - onClick={handleRemoveClick} + fontSize="sm" + onClick={() => onRemove?.(role)} > Remove @@ -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 super­powers? - - )} - - setPlayerSkills(value as Array)} - options={skillChoices} - autoFocus - closeMenuOnSelect={false} - placeholder="Add Your Skills…" - /> - + return ( + > + {...{ field, onClose, onSave, buttonLabel }} + title="Skills" + prompt="What are your super­powers?" + 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 - - - - - )} - + 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 && ( + + + + )} + + + ); +}; 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 @@ - + diff --git a/packages/web/public/assets/roles/builder.svg b/packages/web/public/assets/roles/builder.svg index eca1c088..424ba40f 100644 --- a/packages/web/public/assets/roles/builder.svg +++ b/packages/web/public/assets/roles/builder.svg @@ -1,4 +1,4 @@ - + diff --git a/packages/web/public/assets/roles/designer.svg b/packages/web/public/assets/roles/designer.svg index ddef8199..dd45ced3 100644 --- a/packages/web/public/assets/roles/designer.svg +++ b/packages/web/public/assets/roles/designer.svg @@ -1,3 +1,3 @@ - + diff --git a/packages/web/public/assets/roles/innkeeper.svg b/packages/web/public/assets/roles/innkeeper.svg index 8e10d941..bfa86bfd 100644 --- a/packages/web/public/assets/roles/innkeeper.svg +++ b/packages/web/public/assets/roles/innkeeper.svg @@ -1,4 +1,4 @@ - + diff --git a/packages/web/public/assets/roles/patron.svg b/packages/web/public/assets/roles/patron.svg index 70f6232b..f91102d4 100644 --- a/packages/web/public/assets/roles/patron.svg +++ b/packages/web/public/assets/roles/patron.svg @@ -1,4 +1,4 @@ - + diff --git a/packages/web/public/assets/roles/shiller.svg b/packages/web/public/assets/roles/shiller.svg index 687d1429..3bb2074c 100644 --- a/packages/web/public/assets/roles/shiller.svg +++ b/packages/web/public/assets/roles/shiller.svg @@ -1,4 +1,4 @@ - + diff --git a/packages/web/public/assets/roles/writer.svg b/packages/web/public/assets/roles/writer.svg index a8d62c3d..1d41163e 100644 --- a/packages/web/public/assets/roles/writer.svg +++ b/packages/web/public/assets/roles/writer.svg @@ -1,4 +1,4 @@ - + diff --git a/packages/web/utils/boxTypes.ts b/packages/web/utils/boxTypes.ts index 56e334ac..38e96162 100644 --- a/packages/web/utils/boxTypes.ts +++ b/packages/web/utils/boxTypes.ts @@ -1,37 +1,43 @@ +import { Maybe, Values } from '@metafam/utils'; import { hashCode } from 'utils/stringHelpers'; -export enum BoxType { +export const BoxTypes = { // Player Profile Boxes - PLAYER_HERO = 'player-hero', - PLAYER_SKILLS = 'player-skills', - PLAYER_NFT_GALLERY = 'player-nft-gallery', - PLAYER_DAO_MEMBERSHIPS = 'player-dao-memberships', - PLAYER_ACHIEVEMENTS = 'player-achievements', - PLAYER_TYPE = 'player-type', - PLAYER_COLOR_DISPOSITION = 'player-color-disposition', - PLAYER_ROLES = 'player-roles', - PLAYER_COMPLETED_QUESTS = 'player-completed-quests', - PLAYER_ADD_BOX = 'player-add-box', + PLAYER_HERO: 'player-hero', + PLAYER_SKILLS: 'player-skills', + PLAYER_NFT_GALLERY: 'nft-gallery', + PLAYER_DAO_MEMBERSHIPS: 'dao-memberships', + PLAYER_ACHIEVEMENTS: 'player-achievements', // WIP + PLAYER_TYPE: 'player-type', + PLAYER_COLOR_DISPOSITION: 'color-disposition', + PLAYER_ROLES: 'player-roles', + PLAYER_COMPLETED_QUESTS: 'completed-quests', + PLAYER_ADD_BOX: 'player-add-box', // Guild Profile Boxes - GUILD_SKILLS = 'guild-skills', - GUILD_GALLERY = 'guild-gallery', - GUILD_ANNOUNCEMENTS = 'guild-announcements', - GUILD_PLAYERS = 'guild-players', - GUILD_QUESTS = 'quild-quests', - GUILD_STATS = 'guild-stats', - GUILD_LINKS = 'guild-links', + GUILD_SKILLS: 'guild-skills', + GUILD_GALLERY: 'guild-gallery', + GUILD_ANNOUNCEMENTS: 'guild-announcements', + GUILD_PLAYERS: 'guild-players', + GUILD_QUESTS: 'quild-quests', + GUILD_STATS: 'guild-stats', + GUILD_LINKS: 'guild-links', // Common Profile Boxes - EMBEDDED_URL = 'embedded-url', -} + EMBEDDED_URL: 'embedded-url', +} as const; + +export type BoxType = Values; export type BoxMetadata = { [record: string]: string; }; -export const getBoxKey = ( - boxType: BoxType, - boxMetadata: { [record: string]: string }, -): string => `${boxType}-${hashCode(JSON.stringify(boxMetadata))}`; +export const createBoxKey = ( + type: BoxType, + metadata: Record = {}, +) => `${type}-${hashCode(JSON.stringify(metadata))}`; -export const getBoxTypeFromKey = (boxKey: string): BoxType => +export const getBoxKey = (target: Maybe) => + (target?.offsetParent as HTMLElement)?.offsetParent?.id; + +export const getBoxType = (boxKey: string): BoxType => boxKey.split('-').slice(0, -1).join('-') as BoxType; diff --git a/packages/web/utils/daoHelpers.ts b/packages/web/utils/daoHelpers.ts index b44cf688..37bf359c 100644 --- a/packages/web/utils/daoHelpers.ts +++ b/packages/web/utils/daoHelpers.ts @@ -1,6 +1,6 @@ import { getHexChainId } from '@metafam/utils'; -export const getDaoLink = (chain?: string, address?: string): string | null => { +export const getDAOLink = (chain?: string, address?: string): string | null => { if (address && chain) { const hexChainId = getHexChainId(chain); return `https://app.daohaus.club/dao/${hexChainId}/${address.toLowerCase()}`; diff --git a/packages/web/utils/dashboardHelpers.ts b/packages/web/utils/dashboardHelpers.ts index af0117f8..6473eb54 100644 --- a/packages/web/utils/dashboardHelpers.ts +++ b/packages/web/utils/dashboardHelpers.ts @@ -19,20 +19,17 @@ export function findHighLowPrice( export function volumeChange( vols: Array>, - todayVol: Record, + { usd: today }: Record, ): number { - const plots = []; - let element: Array = []; - - for (let i = 0; i < vols.length; i++) { - element = vols[i]; - plots.push({ date: element[0], volume: element[1] }); + const plots = vols.map(([date, volume]) => ({ date, volume })); + if (plots.length < 2) { + throw new Error('Insufficient Data'); } - const lastVol = plots[plots.length - 2].volume; - const diff = +todayVol.usd - +lastVol; - const volPercent = Number((diff / todayVol.usd) * 100); + const [{ volume: previous }] = plots.slice(-2); + const Δ = today - previous; + const percent = Number((Δ / today) * 100); - return volPercent; + return percent; } type TickerProps = { diff --git a/packages/web/utils/layoutHelpers.ts b/packages/web/utils/layoutHelpers.ts index 993dc01a..3d073ffd 100644 --- a/packages/web/utils/layoutHelpers.ts +++ b/packages/web/utils/layoutHelpers.ts @@ -1,6 +1,7 @@ import { getBoxLayoutItemDefaults, GRID_ROW_HEIGHT, + HEIGHT_MODIFIER, LayoutItem, ProfileLayoutData, } from 'components/Player/Section/config'; @@ -8,23 +9,14 @@ import { Layout, Layouts } from 'react-grid-layout'; import { BoxMetadata, BoxType, - getBoxKey, - getBoxTypeFromKey, + BoxTypes, + createBoxKey, + getBoxType, } from 'utils/boxTypes'; -export const makeLayouts = (canEdit: boolean, layouts: Layouts): Layouts => - Object.fromEntries( - Object.entries(layouts).map(([key, items]) => [ - key, - items.map((item) => - item.i === 'hero' ? { ...item, isResizable: canEdit } : item, - ), - ]), - ); - -export const onRemoveBoxFromLayouts = ( - boxKey: string, +export const removeBoxFromLayouts = ( layouts: Layouts, + boxKey: string, ): Layouts => Object.fromEntries( Object.entries(layouts).map(([key, items]) => [ @@ -34,110 +26,103 @@ export const onRemoveBoxFromLayouts = ( ); export const addBoxToLayouts = ( - boxType: BoxType, - boxMetadata: BoxMetadata, layouts: Layouts, + type: BoxType, + metadata: BoxMetadata = {}, + opts: Partial = {}, ): Layouts => Object.fromEntries( Object.entries(layouts).map(([key, items]) => { const heroItem = items.find( - (item) => item.i === getBoxKey(BoxType.PLAYER_HERO, {}), + (item) => item.i === createBoxKey(BoxTypes.PLAYER_HERO), ); return [ key, [ ...items, { - ...getBoxLayoutItemDefaults(boxType), + ...getBoxLayoutItemDefaults(type), x: 0, y: heroItem ? heroItem.y + heroItem.h : 0, - i: getBoxKey(boxType, boxMetadata), + i: createBoxKey(type, metadata), + ...opts, }, ], ]; }), ); -const HEIGHT_MODIFIER = 0.57; // not sure why 0.57 but for some reason it works! - -export const updateHeightsInLayouts = ( +export const updatedLayouts = ( layouts: Layouts, - heights: { [boxKey: string]: number }, + heights: Record, ): Layouts => Object.fromEntries( Object.entries(layouts).map(([key, items]) => [ key, items.map((item) => { const itemHeight = - (HEIGHT_MODIFIER * (heights[item.i] || 0)) / GRID_ROW_HEIGHT; - const boxType = getBoxTypeFromKey(item.i); - return boxType === BoxType.PLAYER_ADD_BOX + (heights[item.i] ?? 0) / (GRID_ROW_HEIGHT * HEIGHT_MODIFIER); + const type = getBoxType(item.i); + return type === BoxTypes.PLAYER_ADD_BOX ? item : { ...item, - h: itemHeight >= 1 ? itemHeight : 1, + h: Math.max(itemHeight, 1), }; }), ]), ); -export const disableAddBoxInLayoutData = ({ +export const disableAddBox = ({ layouts, layoutItems, }: ProfileLayoutData): ProfileLayoutData => ({ - layouts: onRemoveBoxFromLayouts( - getBoxKey(BoxType.PLAYER_ADD_BOX, {}), - layouts, - ), + layouts: removeBoxFromLayouts(layouts, createBoxKey(BoxTypes.PLAYER_ADD_BOX)), layoutItems: layoutItems.filter( - (item) => item.boxType !== BoxType.PLAYER_ADD_BOX, + (item) => item.type !== BoxTypes.PLAYER_ADD_BOX, ), }); -export const enableAddBoxInLayoutData = ({ +export const enableAddBox = ({ layouts, layoutItems, }: ProfileLayoutData): ProfileLayoutData => ({ - layouts: addBoxToLayouts(BoxType.PLAYER_ADD_BOX, {}, layouts), + layouts: addBoxToLayouts( + layouts, + BoxTypes.PLAYER_ADD_BOX, + {}, + { x: 1, y: -1 }, + ), layoutItems: [ ...layoutItems, { - boxType: BoxType.PLAYER_ADD_BOX, - boxMetadata: {}, - boxKey: getBoxKey(BoxType.PLAYER_ADD_BOX, {}), + type: BoxTypes.PLAYER_ADD_BOX, + key: createBoxKey(BoxTypes.PLAYER_ADD_BOX), }, ], }); const layoutItemSorter = (i: LayoutItem, j: LayoutItem) => - i.boxKey > j.boxKey ? 1 : -1; + i.key.localeCompare(j.key); const layoutSorter = (i: Layout, j: Layout) => { - if (i.x > j.x) { - return 1; + let diff = i.x - j.x; + if (diff === 0) { + diff = i.y - j.y; } - if (i.x === j.x) { - if (i.y > j.y) { - return 1; - } - if (i.y === j.y) { - return 0; - } - return -1; - } - return -1; + return diff; }; export const isSameLayouts = ( inputA: ProfileLayoutData, inputB: ProfileLayoutData, ) => { - const a = disableAddBoxInLayoutData(inputA); - const b = disableAddBoxInLayoutData(inputB); + const a = disableAddBox(inputA); + const b = disableAddBox(inputB); const itemsA = a.layoutItems.sort(layoutItemSorter); const itemsB = b.layoutItems.sort(layoutItemSorter); const isSameItems = itemsA.reduce( - (t, item, i) => t && item.boxKey === itemsB[i].boxKey, + (t, item, i) => t && item.key === itemsB[i].key, true, ); if (isSameItems) { diff --git a/packages/web/utils/setupOptions.tsx b/packages/web/utils/setupOptions.tsx index 9b135613..71b1e9cd 100644 --- a/packages/web/utils/setupOptions.tsx +++ b/packages/web/utils/setupOptions.tsx @@ -1,3 +1,6 @@ +import { Maybe } from '@metafam/utils'; +import { ReactElement } from 'react'; + export type SetupStep = { label: string; slug?: string; @@ -7,7 +10,7 @@ export type SetupStep = { export type SetupSection = { label: string; title: { - [any: string]: string | undefined; + [any: string]: string | undefined | ReactElement; }; }; @@ -21,8 +24,8 @@ export class SetupOptions { label: 'Professional Profile', title: { base: 'Pro', - sm: '2. Professional', - lg: '2. Professional Profile', + sm: <>2. Pro­fess­ional, + lg: <>2. Pro­fess­ional Profile, }, }, { @@ -50,8 +53,8 @@ export class SetupOptions { sectionIndex: 0, }, { - label: 'Personality Type', - slug: 'personalityType', + label: 'Color Disposition', + slug: 'colorDisposition', sectionIndex: 0, }, { @@ -96,7 +99,7 @@ export class SetupOptions { }, ]; - stepIndexMatchingSlug(slug: string | null): number { + stepIndexMatchingSlug(slug: Maybe): number { return this.steps.findIndex((step) => step.slug === slug); } diff --git a/schema.graphql b/schema.graphql index 9328e6f5..f1be1b27 100644 --- a/schema.graphql +++ b/schema.graphql @@ -201,9 +201,7 @@ type BrightIdStatus { } type CacheProcessOutput { - error: String - queued: Boolean! - success: Boolean! + updateIDXProfile: uuid } type CollectiblesFavorites { @@ -2640,7 +2638,7 @@ type Member { } type Moloch { - avatarUrl: String + avatarURL: String chain: String! id: ID! summoner: String! @@ -3550,7 +3548,7 @@ type mutation_root { """ perform the action: "updateIDXProfile" """ - updateIDXProfile(playerId: uuid): CacheProcessOutput + updateIDXProfile(playerId: uuid): uuid! """ perform the action: "updateQuestCompletion" @@ -5305,6 +5303,9 @@ unique or primary key constraints on table "player_skill" enum player_skill_constraint { """unique or primary key constraint""" player_skill_pkey + + """unique or primary key constraint""" + player_skill_player_id_skill_id_key } """ @@ -7761,6 +7762,16 @@ type query_root { """fetch data from the table: "skill" using primary key columns""" skill_by_pk(id: uuid!): skill + + """ + retrieve the result of action: "updateIDXProfile" + """ + updateIDXProfile( + """ + id of the action: "updateIDXProfile" + """ + id: uuid! + ): updateIDXProfile } """ @@ -11282,6 +11293,16 @@ type subscription_root { """fetch data from the table: "skill" using primary key columns""" skill_by_pk(id: uuid!): skill + + """ + retrieve the result of action: "updateIDXProfile" + """ + updateIDXProfile( + """ + id of the action: "updateIDXProfile" + """ + id: uuid! + ): updateIDXProfile } scalar timestamptz @@ -11307,6 +11328,23 @@ type TokenBalances { seedBalance: String! } +""" +fields of action: "updateIDXProfile" +""" +type updateIDXProfile { + """the time at which this action was created""" + created_at: timestamptz + + """errors related to the invocation""" + errors: json + + """the unique id of an action""" + id: uuid + + """the output fields of this action""" + output: CacheProcessOutput +} + type UpdateIDXProfileResponse { accountLinks: [String] ceramic: String! diff --git a/tokenlog.json b/tokenlog.json index 8406d9a2..1c08b110 100644 --- a/tokenlog.json +++ b/tokenlog.json @@ -1,7 +1,7 @@ { "org": "MetaFam", "repo": "TheGame", - "tokenAddress": "0x30cF203b48edaA42c3B4918E955fED26Cd012A3F", + "tokenAddress": "0xeaecc18198a475c921b24b8a6c1c1f0f5f3f7ea0", "labels": ["RoadMap"], "votingMethod": "QUADRATIC", "chainId": 1 diff --git a/yarn.lock b/yarn.lock index 93ee50a7..03d14c8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5464,6 +5464,11 @@ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== +"@types/deep-equal@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.1.tgz#71cfabb247c22bcc16d536111f50c0ed12476b03" + integrity sha512-mMUu4nWHLBlHtxXY17Fg6+ucS/MnndyOWyOe7MmwkoMYxvfQU2ajtRaEvqSUv+aVkMqH/C0NCI8UoVfRNQ10yg== + "@types/draft-js@*", "@types/draft-js@0.11.3": version "0.11.3" resolved "https://registry.yarnpkg.com/@types/draft-js/-/draft-js-0.11.3.tgz#8110c143e0ab419c04f749480b313e6ccc8713bb" @@ -10298,6 +10303,27 @@ deep-equal@^1.0.1, deep-equal@~1.1.1: object-keys "^1.1.1" regexp.prototype.flags "^1.2.0" +deep-equal@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.5.tgz#55cd2fe326d83f9cbf7261ef0e060b3f724c5cb9" + integrity sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw== + dependencies: + call-bind "^1.0.0" + es-get-iterator "^1.1.1" + get-intrinsic "^1.0.1" + is-arguments "^1.0.4" + is-date-object "^1.0.2" + is-regex "^1.1.1" + isarray "^2.0.5" + object-is "^1.1.4" + object-keys "^1.1.1" + object.assign "^4.1.2" + regexp.prototype.flags "^1.3.0" + side-channel "^1.0.3" + which-boxed-primitive "^1.0.1" + which-collection "^1.0.1" + which-typed-array "^1.1.2" + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -11109,6 +11135,20 @@ es-abstract@^1.19.0, es-abstract@^1.19.1: string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.1" +es-get-iterator@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7" + integrity sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.0" + has-symbols "^1.0.1" + is-arguments "^1.1.0" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.5" + isarray "^2.0.5" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -12988,7 +13028,7 @@ get-graphql-schema@2.1.2: minimist "^1.2.0" node-fetch "^2.2.0" -get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: +get-intrinsic@^1.0.1, get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== @@ -14390,6 +14430,14 @@ is-arguments@^1.0.4: dependencies: call-bind "^1.0.0" +is-arguments@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -14481,6 +14529,13 @@ is-date-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== +is-date-object@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + is-decimal@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" @@ -14638,6 +14693,11 @@ is-lower-case@^2.0.2: dependencies: tslib "^2.0.3" +is-map@^2.0.1, is-map@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" + integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== + is-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" @@ -14737,7 +14797,7 @@ is-regex@^1.0.4, is-regex@^1.1.2: call-bind "^1.0.2" has-symbols "^1.0.1" -is-regex@^1.1.4: +is-regex@^1.1.1, is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== @@ -14759,6 +14819,11 @@ is-relative@^1.0.0: dependencies: is-unc-path "^1.0.0" +is-set@^2.0.1, is-set@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" + integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== + is-shared-array-buffer@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" @@ -14847,6 +14912,11 @@ is-utf8@^0.2.0: resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= +is-weakmap@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" + integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== + is-weakref@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.1.tgz#842dba4ec17fa9ac9850df2d6efbc1737274f2a2" @@ -14854,6 +14924,14 @@ is-weakref@^1.0.1: dependencies: call-bind "^1.0.0" +is-weakset@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d" + integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + is-whitespace-character@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7" @@ -14886,7 +14964,7 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= -isarray@^2.0.1: +isarray@^2.0.1, isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== @@ -18392,7 +18470,7 @@ object-inspect@~1.7.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== -object-is@^1.0.1: +object-is@^1.0.1, object-is@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== @@ -20511,6 +20589,14 @@ regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.1: call-bind "^1.0.2" define-properties "^1.1.3" +regexp.prototype.flags@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz#b3f4c0059af9e47eca9f3f660e51d81307e72307" + integrity sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + regexpp@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" @@ -21398,7 +21484,7 @@ shortid@^2.2.8: dependencies: nanoid "^2.1.0" -side-channel@^1.0.4: +side-channel@^1.0.3, side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== @@ -24172,7 +24258,7 @@ whatwg-url@^8.0.0, whatwg-url@^8.4.0, whatwg-url@^8.5.0: tr46 "^2.0.2" webidl-conversions "^6.1.0" -which-boxed-primitive@^1.0.2: +which-boxed-primitive@^1.0.1, which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== @@ -24183,6 +24269,16 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-collection@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" + integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + dependencies: + is-map "^2.0.1" + is-set "^2.0.1" + is-weakmap "^2.0.1" + is-weakset "^2.0.1" + which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"